junglejourney

view materials/make_packaging.py @ 208:daac9a5599a7

Fixed incoherent instructions.
author David Boddie <david@boddie.org.uk>
date Sun Oct 09 00:34:55 2011 +0200
parents c0287a81583a
children
line source
1 #!/usr/bin/env python
3 """
4 Copyright (C) 2011 David Boddie <david@boddie.org.uk>
6 This program is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with this program. If not, see <http://www.gnu.org/licenses/>.
18 """
20 import codecs, os, sys
21 from PyQt4.QtCore import QSize
22 from PyQt4.QtGui import *
24 def relpath(source, destination):
26 source = os.path.abspath(source)
27 destination = os.path.abspath(destination)
29 src_pieces = source.split(os.sep)
30 dest_pieces = destination.split(os.sep)
32 if os.path.isfile(source):
33 src_pieces.pop()
35 common = []
36 for i in range(min(len(src_pieces), len(dest_pieces))):
38 if src_pieces[i] == dest_pieces[i]:
39 common.append(src_pieces[i])
40 i -= 1
41 else:
42 break
44 to_common = os.sep.join([os.pardir]*(len(src_pieces)-len(common)))
45 return to_common + os.sep + os.sep.join(dest_pieces[len(common):])
48 class SVG:
50 def __init__(self, path):
52 self.path = path
54 def _escape(self, text):
56 for s, r in (("&", "&amp;"), ("<", "&lt;"), (">", "&gt;")):
57 text = text.replace(s, r)
59 return text
61 def open(self):
63 self.text = ('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n'
64 '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"\n'
65 ' "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n')
67 def add_page(self, width, height):
69 self.text += ('<svg version="1.1"\n'
70 ' xmlns="http://www.w3.org/2000/svg"\n'
71 ' xmlns:xlink="http://www.w3.org/1999/xlink"\n'
72 ' width="%fcm" height="%fcm"\n'
73 ' viewBox="0 0 %i %i">\n') % (width/100.0, height/100.0, width, height)
75 def add_image(self, x, y, width, height, path):
77 path = os.path.join(relpath(self.path, os.curdir), path)
78 self.text += '<image x="%f" y="%f" width="%f" height="%f"\n' % (x, y, width, height)
79 self.text += ' xlink:href="%s" />\n' % path
81 def add_text(self, x, y, font, text):
83 self.text += '<text x="%f" y="%f"\n' % (x, y)
84 self.text += (' font-family="%s"\n'
85 ' font-size="%i"\n') % (font["family"], font["size"])
86 if font.has_key("weight"):
87 self.text += ' font-weight="%s"\n' % font["weight"]
88 if font.has_key("style"):
89 self.text += ' font-style="%s"\n' % font["style"]
90 self.text += '>\n'
91 self.text += self._escape(text)
92 self.text += '</text>\n'
94 def close(self):
96 self.text += "</svg>\n"
97 codecs.open(self.path, "w", "utf-8").write(self.text)
100 class Inlay(SVG):
102 def __init__(self, path):
104 SVG.__init__(self, path)
106 self.page_offsets = [(0, 0), (650, 0), (2 * 650, 0), (3 * 650, 0)]
107 self.page_number = 0
109 def open(self):
111 SVG.open(self)
112 self.text += ('<svg version="1.1"\n'
113 ' xmlns="http://www.w3.org/2000/svg"\n'
114 ' xmlns:xlink="http://www.w3.org/1999/xlink"\n'
115 ' width="33.5cm" height="10cm"\n'
116 ' viewBox="0 0 3350 1000">\n')
118 def add_page(self, width, height):
120 self.ox, self.oy = self.page_offsets[self.page_number]
121 self.page_number += 1
123 def add_image(self, x, y, width, height, path):
125 SVG.add_image(self, self.ox + x, self.oy + y, width, height, path)
127 def add_text(self, x, y, font, text):
129 SVG.add_text(self, self.ox + x, self.oy + y, font, text)
131 def close(self):
133 self.text += ('<rect x="2600" y="0" width="100" height="1000"\n'
134 ' stroke="black" fill="none" stroke-width="1" />\n')
136 SVG.close(self)
139 class Page:
141 def __init__(self, size, objects):
143 self.size = size
144 self.objects = objects
146 def render(self, svg):
148 svg.add_page(self.size[0], self.size[1])
150 positions = [(0, 0)]
151 for obj in self.objects:
153 x, y = obj.render(svg, positions)
154 positions.append((x, y))
156 return svg
158 class TextBox:
160 def __init__(self, bbox, text_items, follow = False, index = -1):
162 self.bbox = bbox
163 self.text_items = text_items
164 self.follow = follow
165 self.index = index
167 def render(self, svg, positions):
169 x, y, width, height = self.bbox
171 if self.follow:
172 y = y + positions[self.index][1]
174 for text_item in self.text_items:
176 left_indent = text_item.font.get("left indent", 0)
177 right_indent = text_item.font.get("right indent", 0)
178 item_x = x + left_indent
179 item_width = width - left_indent - right_indent
181 for pieces, line_height in text_item.readline(item_width):
183 for font, word_x, text in pieces:
185 svg.add_text(item_x + word_x, y, font, text)
187 y += line_height
189 return x, y
191 class Text:
193 def __init__(self, font, text):
195 self.font = font
196 self.text = text
198 self.parse_text()
200 def parse_text(self):
202 lines = self.text.split("\n")
203 self.lines = []
205 for line in lines:
207 words = []
208 for word in line.split():
210 words.append(Word(self.font, word))
212 self.lines.append(words)
214 def readline(self, width):
216 for line in self.lines:
218 w = 0
219 used = 0
220 words = []
222 while w < len(line):
224 word = line[w]
225 word_width = word.width()
227 if used + word_width <= width:
228 # Add words while there is still space.
229 used += word_width + word.space()
230 words.append(word)
231 w += 1
233 elif words:
234 # When out of space, yield the words on the line.
235 yield self.format(words, width), self.height(words)
237 used = 0
238 words = []
240 else:
241 # If no words will fit on the line, just add the first
242 # word to the list.
243 yield self.format([word], width), self.height(words)
245 used = 0
246 w += 1
248 if words:
249 yield self.format(words, width, last = True), self.height(words)
250 elif not line:
251 yield [], self.line_height()/2
253 def format(self, words, width, last = False):
255 output = []
256 x = 0
258 if len(words) == 0:
259 spacing = 0
261 elif self.font.get("align", "left") == "justify" and not last:
262 # Full justify the text.
263 total_width = sum(map(lambda word: word.width(), words))
264 spacing = (width - total_width)/float(len(words) - 1)
266 elif self.font.get("align", "left") == "centre":
267 # Centre the text.
268 total_width = sum(map(lambda word: word.width(), words))
269 total_space = sum(map(lambda word: word.space(), words)[:-1])
270 x = width/2.0 - total_width/2.0 - total_space/2.0
271 spacing = None
273 else:
274 spacing = None
276 for word in words:
278 output.append((word._font, x, word.text))
279 x += word.width()
280 if spacing is not None:
281 x += spacing
282 else:
283 x += word.space()
285 return output
287 def height(self, words):
289 return max(map(lambda word: word.height(), words))
291 def line_height(self):
293 font = QFont(self.font.get("family"))
294 font.setPixelSize(self.font.get("size"))
295 if self.font.get("weight") == "bold":
296 font.setWeight(QFont.Bold)
297 if self.font.get("style") == "italic":
298 font.setItalic(True)
300 metrics = QFontMetrics(font)
301 return metrics.height()
303 class Word:
305 def __init__(self, font, text):
307 self._font = font
308 self.text = text
310 def font(self):
312 font = QFont(self._font.get("family"))
313 font.setPixelSize(self._font.get("size"))
314 if self._font.get("weight") == "bold":
315 font.setWeight(QFont.Bold)
316 if self._font.get("style") == "italic":
317 font.setItalic(True)
318 return font
320 def width(self):
322 metrics = QFontMetrics(self.font())
323 return metrics.width(self.text)
325 def height(self):
327 metrics = QFontMetrics(self.font())
328 return metrics.height()
330 def space(self):
332 metrics = QFontMetrics(self.font())
333 return metrics.width(" ")
336 class Image:
338 def __init__(self, bbox, path, scale = None, follow = False, index = -1):
340 self.bbox = bbox
341 self.path = path
342 self.follow = follow
343 self.index = index
344 self.scale = scale
346 def render(self, svg, positions):
348 x, y, width, height = self.bbox
350 if self.follow:
351 y = y + positions[self.index][1]
353 im = QImage(self.path)
354 width = im.size().width()
355 height = im.size().height()
357 if self.scale:
358 width = width * self.scale
359 height = height * self.scale
361 svg.add_image(x, y, width, height, self.path)
363 return x + width, y + height
366 if __name__ == "__main__":
368 app = QApplication(sys.argv)
370 if not 2 <= len(app.arguments()) <= 3:
372 sys.stderr.write("Usage: %s [--inlay] <output directory>\n" % app.arguments()[0])
373 sys.exit(1)
375 if app.arguments()[1] == "--inlay":
376 output_dir = sys.argv[2]
377 inlay = True
378 else:
379 output_dir = sys.argv[1]
380 inlay = False
382 if not os.path.exists(output_dir):
383 os.mkdir(output_dir)
385 regular = {"family": "FreeSerif",
386 "size": 24,
387 "align": "justify"}
389 title = {"family": "FreeSerif",
390 "size": 24,
391 "weight": "bold"}
393 italic_quote = {"family": "FreeSerif",
394 "size": 22,
395 "style": "italic",
396 "left indent": 40,
397 "right indent": 40}
399 quote = {"family": "FreeSerif",
400 "size": 22,
401 "left indent": 40,
402 "right indent": 40}
404 monospace_quote = {"family": "FreeMono",
405 "size": 22,
406 "left indent": 40,
407 "right indent": 40}
409 keys_quote = {"family": "FreeSerif",
410 "size": 24,
411 "left indent": 40,
412 "right indent": 40}
414 key_descriptions_quote = {"family": "FreeSerif",
415 "size": 24,
416 "left indent": 160,
417 "right indent": 0}
419 exclamation = {"family": "FreeSerif",
420 "size": 28,
421 "style": "italic",
422 "weight": "bold",
423 "align": "centre"}
425 back_cover_title = {"family": "FreeSerif",
426 "size": 36,
427 "weight": "bold",
428 "align": "centre"}
430 back_cover_subtitle = {"family": "FreeSerif",
431 "size": 28,
432 "weight": "bold",
433 "align": "centre"}
435 back_cover_centred = {"family": "FreeSerif",
436 "size": 24,
437 "align": "centre"}
439 pages = [
440 Page((650, 1000),
441 [TextBox((25, 35, 600, 0),
442 [Text(title, "Jungle Journey\n"),
443 Text(regular,
444 "The last flames of the campfire fade to glowing embers and I am alone. "
445 "My recent acquaintances, their packs and paraphernalia have gone, leaving "
446 "me stranded deep in the heart of this jungle realm. Clouds momentarily "
447 "sweep the cold face of the moon and I perceive the clicks, whistles and "
448 "cries of creatures in the hot air that cloaks this place. Desperately, I "
449 "try to stay my panic and remember those fragments of wilderness craft "
450 "learned and unlearned many years ago.\n"),
451 Text(italic_quote,
452 "Choose your weapon carefully,\n"
453 "Get ready for a fight.\n"
454 "The jungle can be dangerous\n"
455 "If you go there at night.\n"
456 "There's time to pick up treasure,\n"
457 "But no time to stop and stare.\n"
458 "If you don't find the hidden cave\n"
459 "You won't get out of there.\n"),
460 Text(regular,
461 "Hopeless, I scramble to my feet, reaching for any weapon still left to me. "
462 "Struggling through the dense undergrowth, I search for signs of a track or "
463 "trail. At first glance, paths that seemed to lead to safety turn out to be "
464 "impassable, overgrown by tangled and twisted vines. I remember the words of "
465 "an old teacher:\n"),
466 Text(quote,
467 u'\u201cDo not be tempted to use fire to make your way. '
468 'Many a traveller has strayed from the path, using fire to blaze a trail, '
469 'only to reach a dead end. Trying to return, they find that the jungle '
470 'has grown back. Those who are desperate enough will even seek out '
471 u'forgotten routes when the way home is in sight.\u201d\n'),
472 Text(regular,
473 "Sensing my presence, obscene creatures emerge from the darkness, hungry "
474 "for prey. Only through skill and luck am I able to dispatch them back "
475 "into the shadows. Even though I know I must journey deeper into this "
476 "uncharted land to find the way home, the thought of vengeance drives me on.")
477 ])
478 ]),
479 Page((650, 1000),
480 [TextBox((25, 35, 600, 0),
481 [Text(title, "Loading the Game\n"),
482 Text(regular, "Insert the cassette and type\n")]),
483 TextBox((25, -2, 600, 0),
484 [Text(monospace_quote, "*RUN JUNGLE\n")], follow = True),
485 TextBox((25, -2, 600, 0),
486 [Text(regular,
487 "then press Return. Press play on the cassette recorder. "
488 "The game should now load.\n\n"),
489 Text(title, "Playing the Game\n"),
490 Text(regular,
491 "The player must help the character reach the exit for each level. However, the "
492 "player must first find a key to unlock the exit. On the final level, the exit "
493 "does not require a key but it may be obstructed. Enemies will appear in each "
494 "location and attack the player's character. They can be destroyed by "
495 "projectiles fired by the current weapon.\n"),
496 Text(regular,
497 "Your character can be moved around the screen by using four control keys:\n")],
498 follow = True),
499 TextBox((25, -4, 600, 0),
500 [Text(keys_quote,
501 "Z\n"
502 "X\n"
503 ":\n"
504 "/")], follow = True),
505 TextBox((25, -4, 600, 0),
506 [Text(key_descriptions_quote,
507 "left\n"
508 "right\n"
509 "up\n"
510 "down\n"),
511 Text(regular,
512 "To fire a weapon, press the Return key. There are four different types of "
513 "weapon available in the game.\n\n"
514 "Alternatively, you may play using an analogue joystick. Select joystick controls by "
515 "pressing the Fire button on the title page to start the game. Press Space to "
516 "start the game with keyboard controls.\n\n"
517 "Other keys can be used to control the game:\n")],
518 follow = True, index = -2),
519 TextBox((25, -4, 600, 0),
520 [Text(keys_quote,
521 "S\n"
522 "Q\n"
523 "P\n"
524 "O\n"
525 "Escape")], follow = True),
526 TextBox((25, -4, 600, 0),
527 [Text(key_descriptions_quote,
528 "enable sound effects\n"
529 "disable sound effects\n"
530 "pause the game\n"
531 "resume the game\n"
532 "quit the game, returning to the title screen\n")],
533 follow = True, index = -2)
534 ]),
535 Page((650, 1000),
536 [TextBox((25, 35, 600, 0),
537 [Text(title, "Treasure\n"),
538 Text(regular, "Items of treasure can be found throughout the jungle. "
539 "Pick these up to increase your score.\n")]),
540 Image((45, -8, 515, 0), "../images/key.xpm", scale = 4,
541 follow = True),
542 TextBox((135, 20, 475, 0),
543 [Text(regular, "Find the key to open the door on all levels except the last. "
544 "Each key is worth 50 points.")],
545 follow = True, index = -2),
546 Image((45, 8, 515, 0), "../images/chest.xpm", scale = 4,
547 follow = True, index = -2),
548 TextBox((135, 48, 475, 0),
549 [Text(regular, "Treasure chests are worth 20 points.")],
550 follow = True, index = -3),
551 Image((45, 8, 515, 0), "../images/jewel.xpm", scale = 4,
552 follow = True, index = -2),
553 TextBox((135, 48, 475, 0),
554 [Text(regular, "Jewels are worth 5 points.")],
555 follow = True, index = -3),
556 Image((45, 8, 515, 0), "../images/statue.xpm", scale = 4,
557 follow = True, index = -2),
558 TextBox((135, 48, 475, 0),
559 [Text(regular, "Statues are worth 10 points.")],
560 follow = True, index = -3),
561 Image((45, 8, 515, 0), "../images/health.xpm", scale = 4,
562 follow = True, index = -2),
563 TextBox((135, 36, 475, 0),
564 [Text(regular, "Presents are worth 40 points and boost your strength by 20 units.")],
565 follow = True, index = -3),
566 TextBox((25, 48, 600, 0),
567 [Text(title, "Exits\n"),
568 Text(regular, "Each level has an exit that can be opened using a key. "
569 "On the last level you will find a gate that leads to safety. "
570 "This does not require a key, but it is well hidden.\n")],
571 follow = True),
572 Image((77, -4, 513, 0), "../images/exit1.xpm", scale = 4,
573 follow = True),
574 TextBox((215, 36, 400, 0),
575 [Text(regular, "The exit is initially locked. Find the key to unlock it.")],
576 follow = True, index = -2),
577 Image((45, 8, 545, 0), "../images/finalexitl.xpm", scale = 4,
578 follow = True, index = -2),
579 Image((109, 8, 481, 0), "../images/finalexitr.xpm", scale = 4,
580 follow = True, index = -3),
581 TextBox((215, 48, 400, 0),
582 [Text(regular, "The final exit is hidden somewhere on the final level.")],
583 follow = True, index = -4),
584 TextBox((25, 960, 600, 0),
585 [Text(exclamation, "Have a safe journey!")])
586 ]),
587 Page((650, 1000),
588 [TextBox((25, 50, 600, 0),
589 [Text(back_cover_title, "Jungle Journey"),
590 Text(back_cover_subtitle, "for the Acorn Electron and BBC Model B")]),
591 Image((101, 5, 450, 0), "screenshot1.png", scale = 0.7, follow = True),
592 TextBox((25, 55, 600, 0),
593 [Text(back_cover_centred,
594 u"Copyright \u00a9 2011 David Boddie\n"
595 u"An Infukor production for Retro Software\n"
596 u"http://www.retrosoftware.co.uk/")], follow = True),
597 TextBox((25, 25, 600, 0),
598 [Text(regular,
599 "This program is free software: you can redistribute it and/or modify "
600 "it under the terms of the GNU General Public License as published by "
601 "the Free Software Foundation, either version 3 of the License, or "
602 "(at your option) any later version.\n"
603 "\n"
604 "This program is distributed in the hope that it will be useful, "
605 "but WITHOUT ANY WARRANTY; without even the implied warranty of "
606 "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the "
607 "GNU General Public License for more details.\n"
608 "\n"
609 "You should have received a copy of the GNU General Public License "
610 "along with this program.\nIf not, see <http://www.gnu.org/licenses/>.")],
611 follow = True)
612 ]),
613 ]
615 if inlay:
616 path = os.path.join(output_dir, "inlay.svg")
617 inlay = Inlay(path)
618 inlay.open()
620 i = 0
621 for page in pages:
623 page.render(inlay)
624 i += 1
626 inlay.close()
628 else:
629 i = 0
630 for page in pages:
632 path = os.path.join(output_dir, "page-%i.svg" % i)
633 svg = SVG(path)
634 svg.open()
635 page.render(svg)
636 svg.close()
637 i += 1
639 sys.exit()