source: tools/area_editor.py@ 528:811481b20689

Last change on this file since 528:811481b20689 was 528:811481b20689, checked in by Simon Cross <hodgestar@…>, 8 years ago

Add support for collectables.

  • Property exe set to *
File size: 48.0 KB
Line 
1#!/usr/bin/env python
2
3# The basic area editor
4#
5# To edit an existing level, use
6# editor levelname
7#
8# To create a new level:
9#
10# editor levelname <xsize> <ysize>
11# (size specified in pixels
12#
13
14import os
15import sys
16
17import pygame
18import pygame.locals as pgl
19
20sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
21
22import pymunk
23
24from albow.root import RootWidget
25from albow.widget import Widget
26from albow.controls import Button, Label, CheckBox
27from albow.dialogs import alert, Dialog, ask
28from albow.layout import Row
29from albow.fields import TextField
30from albow.table_view import TableView, TableColumn
31
32from nagslang.options import parse_args
33from nagslang.constants import SCREEN
34from nagslang.level import Level, POLY_COLORS, LINE_COLOR
35from nagslang.yamlish import load_s
36import nagslang.enemies as ne
37import nagslang.game_object as ngo
38import nagslang.collectable as collectable
39import nagslang.puzzle as np
40
41# layout constants
42MENU_BUTTON_HEIGHT = 35
43MENU_PAD = 4
44MENU_HALF_PAD = MENU_PAD // 2
45MENU_LEFT = SCREEN[0] + MENU_HALF_PAD
46MENU_WIDTH = 200 - MENU_PAD
47MENU_HALF_WIDTH = MENU_WIDTH // 2 - MENU_HALF_PAD
48
49BUTTON_RECT = pygame.rect.Rect(0, 0, MENU_WIDTH, MENU_BUTTON_HEIGHT)
50HALF_BUTTON_RECT = pygame.rect.Rect(0, 0, MENU_HALF_WIDTH, MENU_BUTTON_HEIGHT)
51CHECK_RECT = pygame.rect.Rect(0, 0, MENU_BUTTON_HEIGHT // 2,
52 MENU_BUTTON_HEIGHT // 2)
53
54
55class TestWorld(object):
56
57 def __init__(self):
58 self.level_state = {}
59 self.inventory = {}
60
61
62def distance(tup1, tup2):
63 return (tup1[0] - tup2[0]) ** 2 + (tup1[1] - tup2[1]) ** 2
64
65
66class EditorLevel(Level):
67
68 def __init__(self, name, x=800, y=600):
69 world = TestWorld()
70 super(EditorLevel, self).__init__(name, world)
71 self.x = x
72 self.y = y
73 # Lookup initiliasition info from the objects
74 self.lookup = {}
75 self._move_poly = None
76
77 def _in_bounds(self, pos):
78 if pos[0] < 0 or pos[0] >= self.x:
79 return False
80 if pos[1] < 0 or pos[1] >= self.y:
81 return False
82 return True
83
84 def load(self, space):
85 super(EditorLevel, self).load(space)
86 # Needed to fill in the lookup dict
87 self.reset_objs()
88
89 def point_to_pygame(self, pos):
90 # inverse of point_to_pymunk
91 # (this is also the same as point_to_pymunk, but an additional
92 # function for sanity later in pyweek).
93 return (pos[0], self.y - pos[1])
94
95 def point_to_pymunk(self, pos):
96 # inverse of point_to_pygame
97 # (this is also the same as point_to_pygame, but an additional
98 # function for sanity later in pyweek).
99 return (pos[0], self.y - pos[1])
100
101 def add_point(self, poly_index, pos):
102 self.polygons.setdefault(poly_index, [])
103 if not self._in_bounds(pos):
104 return False
105 if not self.polygons[poly_index]:
106 point = self.point_to_pymunk(pos)
107 self.polygons[poly_index].append(point)
108 else:
109 add_pos = self.point_to_pymunk(pos)
110 self.polygons[poly_index].append(add_pos)
111 return True
112
113 def delete_point(self, index):
114 if index in self.polygons and len(self.polygons[index]) > 0:
115 self.polygons[index].pop()
116
117 def close_poly(self, index):
118 """Attempts to close the current polygon.
119
120 We allow a small additional step to close the polygon, but
121 it's limited as it's a magic point addition"""
122 if len(self.polygons[index]) < 2:
123 # Too small
124 return False
125 first = self.polygons[index][0]
126 if self.add_point(index, self.point_to_pygame(first)):
127 return True
128 return False
129
130 def add_line(self, start_pos, end_pos):
131 startpoint = self.point_to_pymunk(start_pos)
132 endpoint = self.point_to_pymunk(end_pos)
133 self.lines.append([startpoint, endpoint])
134
135 def draw(self, mouse_pos, mouse_poly, filled, draw_cand_line, start_pos,
136 move_point_mode, move_poly_mode, move_point, zoom_factor,
137 level_widget):
138 self._draw_background(True)
139 # Draw polygons as needed for the editor
140 line_width = int(2 * zoom_factor)
141 if filled:
142 self._draw_exterior(True)
143 for index, polygon in self.polygons.items():
144 color = POLY_COLORS.get(index, pygame.color.THECOLORS['black'])
145 if move_point_mode and index == self._move_poly and move_point:
146 pointlist = [p for p in polygon]
147 pointlist = [self.point_to_pygame(p) if p != move_point else
148 mouse_pos for p in pointlist]
149 pygame.draw.lines(self._surface, color, False, pointlist,
150 line_width)
151 break
152 if move_poly_mode and index == self._move_poly and move_point:
153 new_point = self.point_to_pymunk(mouse_pos)
154 pointlist = [self.point_to_pygame(p)
155 for p in self.translate_poly(
156 polygon, move_point, new_point)]
157 pygame.draw.lines(self._surface, color, False, pointlist,
158 line_width)
159 #break
160 if len(polygon) > 1:
161 pointlist = [self.point_to_pygame(p) for p in polygon]
162 pygame.draw.lines(self._surface, color, False, pointlist,
163 line_width)
164 if index == mouse_poly and mouse_pos and len(polygon) > 0:
165 endpoint = self.point_to_pymunk(mouse_pos)
166 pygame.draw.line(self._surface, color,
167 self.point_to_pygame(polygon[-1]),
168 self.point_to_pygame(endpoint),
169 line_width // 2)
170 line_found = False # Hack for sane behaviour if lines overlap
171 for line in self.lines:
172 pointlist = [self.point_to_pygame(p) for p in line]
173 if move_point_mode and not self._move_poly and not line_found:
174 if move_point in line:
175 line_found = True
176 pointlist.remove(self.point_to_pygame(move_point))
177 pointlist.append(mouse_pos)
178 pygame.draw.lines(self._surface, LINE_COLOR, False, pointlist,
179 line_width)
180 if draw_cand_line and start_pos and mouse_pos:
181 endpoint = level_widget.snap_to_grid(mouse_pos)
182 endpoint = self.point_to_pymunk(endpoint)
183 pointlist = [start_pos,
184 self.point_to_pygame(endpoint)]
185 pygame.draw.lines(self._surface, LINE_COLOR, False, pointlist, 1)
186 return self._surface
187
188 def reset_objs(self):
189 # Reset the object state - needed when changing stuff
190 self.drawables = []
191 self.overlay_drawables = []
192 self._glue = np.PuzzleGlue()
193 for game_object_dict in self._game_objects:
194 obj = self._create_game_object(pymunk.Space(), **game_object_dict)
195 self.lookup[obj] = game_object_dict
196 for enemy_dict in self._enemies:
197 obj = self._create_enemy(pymunk.Space(), **enemy_dict)
198 self.lookup[obj] = enemy_dict
199
200 def get_class(self, classname, mod=None):
201 # Get the class given the classname
202 modules = {
203 'game_object': ngo,
204 'collectable': collectable,
205 'enemies': ne,
206 'puzzle': np,
207 }
208 if '.' in classname:
209 modname, classname = classname.split('.')
210 mod = modules[modname]
211 if mod is None:
212 mod = ngo
213 return getattr(mod, classname)
214
215 def try_new_object(self, classname, target, new, old=None):
216 if old in target:
217 target.remove(old)
218 try:
219 target.append(new)
220 self.reset_objs()
221 test_surface = pygame.surface.Surface((self.x, self.y))
222 # Check for initialisation stuff that happens in render
223 for thing in self.drawables:
224 if not isinstance(thing, ne.Enemy):
225 thing.render(test_surface)
226 return True
227 except Exception as e:
228 target.remove(new)
229 if old is not None:
230 target.append(old)
231 self.reset_objs()
232 alert("Failed to update object %s: %s" % (classname, e))
233 return False
234
235 def find_obj_at_pos(self, mouse_pos):
236 pymunk_pos = self.point_to_pymunk(mouse_pos)
237 # Search visible objects
238 for obj in self.drawables:
239 if obj.get_shape().point_query(pymunk_pos):
240 return obj
241 return None
242
243 def find_vertex(self, mouse_pos):
244 # search for vertexes closest to where we've killed
245 mindist = 1000
246 move_point = None
247 search_point = self.point_to_pymunk(mouse_pos)
248 for index, polygon in self.polygons.items():
249 for point in polygon:
250 dist = distance(point, search_point)
251 if dist < mindist:
252 mindist = dist
253 move_point = point
254 self._move_poly = index
255 # Also check lines
256 for line in self.lines:
257 for point in line:
258 dist = distance(point, search_point)
259 if dist < mindist:
260 mindist = dist
261 move_point = point
262 self._move_poly = None
263 return move_point
264
265 def translate_poly(self, poly, move_point, new_point):
266 dx = new_point[0] - move_point[0]
267 dy = new_point[1] - move_point[1]
268 new_poly = [(p[0] + dx, p[1] + dy) for p in poly]
269 return new_poly
270
271 def replace_poly(self, move_point, new_point):
272 new_point = self.point_to_pymunk(new_point)
273 if self._move_poly:
274 self.polygons[self._move_poly] = self.translate_poly(
275 self.polygons[self._move_poly], move_point, new_point)
276
277 def replace_vertex(self, old_point, new_point):
278 new_point = self.point_to_pymunk(new_point)
279 if self._move_poly:
280 new_polygon = [p if p != old_point else new_point for p in
281 self.polygons[self._move_poly]]
282 self.polygons[self._move_poly] = new_polygon
283 else:
284 for line in self.lines:
285 if old_point in line:
286 line.remove(old_point)
287 line.append(new_point)
288 break
289
290
291class ObjectTable(TableView):
292
293 columns = [TableColumn("Object", 690, 'l', '%r')]
294
295 def __init__(self, data):
296 super(ObjectTable, self).__init__(height=450)
297 self.data = sorted(data,
298 key=lambda d: (d.get('classname'), d.get('name')))
299 self.selected_row = -1
300
301 def num_rows(self):
302 return len(self.data)
303
304 def row_data(self, i):
305 data = self.data[i]
306 if 'name' in data:
307 return ('%s (%s)' % (data['classname'], data['name']), )
308 return (data['classname'], )
309
310 def row_is_selected(self, i):
311 return self.selected_row == i
312
313 def click_row(self, i, ev):
314 self.selected_row = i
315
316 def get_selection(self):
317 if self.selected_row >= 0:
318 return self.data[self.selected_row]
319 return None
320
321
322class EditClassDialog(Dialog):
323
324 def __init__(self, classname, cls, data, level_widget,
325 delete=False):
326 super(EditClassDialog, self).__init__()
327 self.level_widget = level_widget
328 self.classname = classname
329 self.rect = pygame.rect.Rect(0, 0, 900, 550)
330 title = Label("Editing %s" % classname)
331 title.rect = pygame.rect.Rect(100, 10, 600, 25)
332 self.add(title)
333 self.requires = cls.requires()
334 y = 40
335 self.fields = {}
336 index = 0
337 self.poly_field = None
338 self.needs_cleanup = False
339 for requirement, hint in self.requires:
340 label = Label(requirement)
341 label.rect = pygame.rect.Rect(40, y, 200, 25)
342 self.add(label)
343 field = TextField()
344 field.rect = pygame.rect.Rect(220, y, 400, 25)
345 self.add(field)
346 if data is not None:
347 if requirement in data:
348 field.set_text('%s' % data[requirement])
349 elif 'args' in data and requirement != 'name':
350 # NB: The ordering assumptions in requires should make
351 # this safe, but it's really, really, really fragile
352 try:
353 field.set_text('%s' % data['args'][index])
354 index += 1
355 except IndexError:
356 # Assumed to be arguments with the default value
357 pass
358 if hint.startswith('polygon'):
359 self.poly_field = field
360 self.fields[requirement] = field
361 hintlabel = Label(hint)
362 hintlabel.rect = pygame.rect.Rect(640, y, 250, 25)
363 self.add(hintlabel)
364 y += 30
365 if self.poly_field:
366 y += 40
367 label = Label("Polygon to use:")
368 label.rect = pygame.rect.Rect(40, y, 200, 25)
369 self.add(label)
370 self.poly_choice = TextField()
371 self.poly_choice.rect = pygame.Rect(280, y, 400, 25)
372 self.add(self.poly_choice)
373 y += 30
374 button = Button('Use Polygon X', action=self.get_poly)
375 button.rect = pygame.rect.Rect(350, y, 250, 30)
376 self.add(button)
377 buttons = []
378 if delete:
379 labels = ['OK', 'Delete', 'Cancel']
380 else:
381 labels = ['OK', 'Cancel']
382 for text in labels:
383 but = Button(text, action=lambda x=text: self.dismiss(x))
384 buttons.append(but)
385 row = Row(buttons)
386 row.rect = pygame.rect.Rect(250, 500, 700, 50)
387 self.add(row)
388
389 def get_poly(self):
390 try:
391 try:
392 index = int(self.poly_choice.get_text())
393 except ValueError:
394 index = 0
395 data = self.level_widget.level.polygons[index][:]
396 except KeyError:
397 data = []
398 if data:
399 # We unclose the polygon, because that's what pymunk
400 # wants
401 if data[0] == data[-1] and len(data) > 1:
402 data.pop()
403 data = [list(x) for x in data]
404 self.needs_cleanup = True
405 self.poly_field.set_text('%s' % data)
406
407 def cleanup(self):
408 if self.needs_cleanup:
409 self.level_widget.level.polygons[6] = []
410 self.level_widget.invalidate()
411
412 def get_data(self):
413 result = {}
414 result['classname'] = self.classname
415 args = []
416 # We arrange to bounce this through yaml'ish to convert
417 # stuff to the expected type
418 for val, _ in self.requires:
419 text = self.fields[val].get_text()
420 if not text:
421 # skip empty fields
422 continue
423 if val == 'name':
424 result['name'] = text
425 elif self.fields[val] == self.poly_field and text:
426 # Evil, but faster than good
427 try:
428 l = eval(text)
429 except Exception:
430 alert("Invalid polygon %s" % text)
431 self.needs_cleanup = False
432 return None
433 # Check for convexity
434 if not pymunk.util.is_convex(l):
435 alert("Invalid polygon %s - not convex" % text)
436 return None
437 if not pymunk.util.is_clockwise(l):
438 l.reverse()
439 if not pymunk.util.is_clockwise(l):
440 alert("Invalid polygon %s - unable to make clockwise"
441 % text)
442 return None
443 args.append(' - - %s' % l[0])
444 for coord in l[1:]:
445 args.append(' - %s' % coord)
446 else:
447 args.append(' - ' + text)
448 data = "args:\n" + '\n'.join(args)
449 result['args'] = load_s(data)['args']
450 return result
451
452
453class LevelWidget(Widget):
454
455 def __init__(self, level, parent):
456 super(LevelWidget, self).__init__(pygame.rect.Rect(0, 0,
457 SCREEN[0], SCREEN[1]))
458 self.level = level
459 self.pos = (0, 0)
460 self.filled_mode = False
461 self.mouse_pos = None
462 self.cur_poly = None
463 self._mouse_drag = False
464 self._draw_objects = False
465 self._draw_enemies = False
466 self._draw_lines = False
467 self.grid_size = 1
468 self.sel_mode = False
469 self.move_obj_mode = False
470 self.move_obj = None
471 self._start_pos = None
472 self._parent = parent
473 self._move_point_mode = False
474 self._move_poly_mode = False
475 self._move_point = False
476 self._zoom_factor = 1.0
477
478 def _level_coordinates(self, pos):
479 # Move positions to level values
480 if not pos:
481 return (0, 0)
482 # Apply zoom_factor
483 pos = pos[0] + self.pos[0], pos[1] + self.pos[1]
484 zoomed = (pos[0] * self._zoom_factor, pos[1] * self._zoom_factor)
485 return int(zoomed[0]), int(zoomed[1])
486
487 def _move_view(self, offset):
488 new_pos = [self.pos[0] + offset[0], self.pos[1] + offset[1]]
489 if new_pos[0] < 0:
490 new_pos[0] = self.pos[0]
491 elif new_pos[0] > self.level.x - SCREEN[0]:
492 new_pos[0] = self.pos[0]
493 if new_pos[1] < 0:
494 new_pos[1] = self.pos[1]
495 elif new_pos[1] > self.level.y - SCREEN[1]:
496 new_pos[1] = self.pos[1]
497 self.pos = tuple(new_pos)
498
499 def inc_grid_size(self, amount):
500 self.grid_size = max(1, self.grid_size + amount)
501 if self.grid_size > 1:
502 self.grid_size = self.grid_size - (self.grid_size % 5)
503
504 def snap_to_grid(self, pos):
505 x = pos[0] - (pos[0] % self.grid_size)
506 y = pos[1] - (pos[1] % self.grid_size)
507 return (x, y)
508
509 def set_objects(self, value):
510 if self._draw_objects != value:
511 self._draw_objects = value
512 self.invalidate()
513
514 def set_enemies(self, value):
515 if self._draw_enemies != value:
516 self._draw_enemies = value
517 self.invalidate()
518
519 def zoom_out(self):
520 self._zoom_factor = self._zoom_factor * 2.0
521 if self._zoom_factor > 8:
522 self._zoom_factor = 8
523
524 def zoom_in(self):
525 self._zoom_factor = self._zoom_factor // 2.0
526 if self._zoom_factor < 1:
527 self._zoom_factor = 1
528
529 def draw(self, surface):
530 if (self.cur_poly is not None and self.cur_poly in self.level.polygons
531 and len(self.level.polygons[self.cur_poly])):
532 # We have an active polygon
533 mouse_pos = self._level_coordinates(self.mouse_pos)
534 mouse_pos = self.snap_to_grid(mouse_pos)
535 elif self._draw_lines:
536 # Interior wall mode
537 mouse_pos = self._level_coordinates(self.mouse_pos)
538 mouse_pos = self.snap_to_grid(mouse_pos)
539 elif self._move_point_mode or self._move_poly_mode:
540 mouse_pos = self._level_coordinates(self.mouse_pos)
541 mouse_pos = self.snap_to_grid(mouse_pos)
542 else:
543 mouse_pos = None
544 level_surface = level.draw(mouse_pos, self.cur_poly, self.filled_mode,
545 self._draw_lines, self._start_pos,
546 self._move_point_mode, self._move_poly_mode,
547 self._move_point, self._zoom_factor, self)
548 if self._draw_objects:
549 for thing in self.level.drawables:
550 if not isinstance(thing, ne.Enemy):
551 thing.render(level_surface)
552 if self._draw_enemies:
553 for thing in self.level.drawables:
554 if isinstance(thing, ne.Enemy):
555 thing.render(level_surface)
556 surface_area = pygame.rect.Rect(self.pos, SCREEN)
557 zoomed_surface = level_surface.copy()
558 if self._zoom_factor != 1:
559 zoomed_surface = pygame.transform.scale(
560 level_surface,
561 (int(level_surface.get_width() / self._zoom_factor),
562 int(level_surface.get_height() / self._zoom_factor)))
563 surface.blit(zoomed_surface, (0, 0), surface_area)
564
565 def change_poly(self, new_poly):
566 self.cur_poly = new_poly
567 self._draw_lines = False
568 self._move_point_mode = False
569 if self.cur_poly is not None:
570 self._parent.reset_lit_buttons()
571 self.filled_mode = False
572
573 def line_mode(self):
574 self.cur_poly = None
575 self._parent.reset_lit_buttons()
576 self._draw_lines = True
577 self.filled_mode = False
578 self._start_pos = None
579 self._move_point_mode = False
580 self._move_poly_mode = False
581
582 def key_down(self, ev):
583 if ev.key == pgl.K_LEFT:
584 self._move_view((-10, 0))
585 elif ev.key == pgl.K_RIGHT:
586 self._move_view((10, 0))
587 elif ev.key == pgl.K_UP:
588 self._move_view((0, -10))
589 elif ev.key == pgl.K_DOWN:
590 self._move_view((0, 10))
591 elif ev.key in (pgl.K_1, pgl.K_2, pgl.K_3, pgl.K_4, pgl.K_5, pgl.K_6):
592 self.change_poly(ev.key - pgl.K_0)
593 elif ev.key == pgl.K_0:
594 self.change_poly(None)
595 elif ev.key == pgl.K_d and self.cur_poly:
596 self.level.delete_point(self.cur_poly)
597 elif ev.key == pgl.K_f:
598 self.set_filled()
599 elif ev.key == pgl.K_c:
600 self.close_poly()
601
602 def set_move_poly_mode(self):
603 self._draw_lines = False
604 self._move_point_mode = False
605 self._move_poly_mode = True
606 self.filled_mode = False
607 self._parent.reset_lit_buttons()
608 self._move_point = None
609
610 def set_move_mode(self):
611 self._draw_lines = False
612 self._move_point_mode = True
613 self._move_poly_mode = False
614 self.filled_mode = False
615 self._parent.reset_lit_buttons()
616 self._move_point = None
617
618 def set_filled(self):
619 closed, _ = self.level.all_closed()
620 if closed:
621 self.cur_poly = None
622 self._parent.reset_lit_buttons()
623 self.filled_mode = True
624 self._draw_lines = False
625 self._move_point_mode = False
626 self._move_poly_mode = False
627 else:
628 alert('Not all polygons closed, so not filling')
629
630 def mouse_move(self, ev):
631 old_pos = self.mouse_pos
632 self.mouse_pos = ev.pos
633 if self.move_obj:
634 corrected_pos = self._level_coordinates(ev.pos)
635 snapped_pos = self.snap_to_grid(corrected_pos)
636 self._update_pos(self.move_obj, snapped_pos)
637 if old_pos != self.mouse_pos and (self.cur_poly or self._draw_lines
638 or self._move_point_mode
639 or self._move_poly_mode):
640 self.invalidate()
641
642 def mouse_drag(self, ev):
643 if self._mouse_drag:
644 old_pos = self.mouse_pos
645 self.mouse_pos = ev.pos
646 diff = (-self.mouse_pos[0] + old_pos[0],
647 -self.mouse_pos[1] + old_pos[1])
648 self._move_view(diff)
649 self.invalidate()
650
651 def mouse_down(self, ev):
652 corrected_pos = self._level_coordinates(ev.pos)
653 snapped_pos = self.snap_to_grid(corrected_pos)
654 if self.sel_mode and ev.button == 1:
655 obj = self.level.find_obj_at_pos(corrected_pos)
656 if obj is not None:
657 self._edit_selected(obj)
658 elif self.move_obj_mode and ev.button == 1 and not self.move_obj:
659 obj = self.level.find_obj_at_pos(corrected_pos)
660 if obj is not None:
661 if obj.movable():
662 self.move_obj = obj
663 data = self.level.lookup[obj]
664 print data
665 elif self.move_obj_mode and ev.button == 1 and self.move_obj:
666 self._update_pos(self.move_obj, snapped_pos)
667 self.move_obj = None
668 elif self._move_poly_mode and ev.button == 1:
669 if self._move_point:
670 # Place the current point
671 self.level.replace_poly(self._move_point, snapped_pos)
672 self._move_point = None
673 self.invalidate()
674 else:
675 # find the current point
676 self._move_point = self.level.find_vertex(corrected_pos)
677 elif self._move_point_mode and ev.button == 1:
678 if self._move_point:
679 # Place the current point
680 self.level.replace_vertex(self._move_point, snapped_pos)
681 self._move_point = None
682 self.invalidate()
683 else:
684 # find the current point
685 self._move_point = self.level.find_vertex(corrected_pos)
686 elif ev.button == 1:
687 if self._draw_lines:
688 if self._start_pos is None:
689 self._start_pos = snapped_pos
690 else:
691 self.level.add_line(self._start_pos, snapped_pos)
692 self._start_pos = None
693 else:
694 print "Click: %r" % (
695 self.level.point_to_pymunk(corrected_pos),)
696 if ev.button == 4: # Scroll up
697 self._move_view((0, -10))
698 elif ev.button == 5: # Scroll down
699 self._move_view((0, 10))
700 elif ev.button == 6: # Scroll left
701 self._move_view((-10, 0))
702 elif ev.button == 7: # Scroll right
703 self._move_view((10, 0))
704 elif self.cur_poly and ev.button == 1:
705 # Add a point
706 if not self.level.add_point(self.cur_poly, snapped_pos):
707 alert("Failed to place point")
708 elif ev.button == 3:
709 self._mouse_drag = True
710
711 def mouse_up(self, ev):
712 if ev.button == 3:
713 self._mouse_drag = False
714
715 def close_poly(self):
716 if self.cur_poly is None:
717 return
718 if self.level.close_poly(self.cur_poly):
719 alert("Successfully closed the polygon")
720 self.change_poly(None)
721 else:
722 alert("Failed to close the polygon")
723
724 def _edit_class(self, classname, cls, data):
725 # Dialog for class properties
726 dialog = EditClassDialog(classname, cls, data, self)
727 if dialog.present() == 'OK':
728 return dialog
729 return None
730
731 def _edit_selected(self, obj):
732 data = self.level.lookup[obj]
733 cls = obj.__class__
734 classname = obj.__class__.__name__
735 dialog = EditClassDialog(classname, cls, data, self, True)
736 res = dialog.present()
737 if res == 'OK':
738 edited = dialog.get_data()
739 if edited is not None:
740 for target in [self.level._game_objects, self.level._enemies]:
741 if data in target:
742 if self.level.try_new_object(classname, target,
743 edited, data):
744 dialog.cleanup()
745 break
746 elif res == 'Delete':
747 for target in [self.level._game_objects, self.level._enemies]:
748 if data in target:
749 target.remove(data)
750 self.level.reset_objs()
751 break
752
753 def _make_edit_dialog(self, entries):
754 # Dialog to hold the editor
755 edit_box = Dialog()
756 edit_box.rect = pygame.rect.Rect(0, 0, 700, 500)
757 table = ObjectTable(entries)
758 edit_box.add(table)
759 buttons = []
760 for text in ['OK', 'Delete', 'Cancel']:
761 but = Button(text, action=lambda x=text: edit_box.dismiss(x))
762 buttons.append(but)
763 row = Row(buttons)
764 row.rect = pygame.rect.Rect(250, 450, 700, 50)
765 edit_box.add(row)
766 edit_box.get_selection = lambda: table.get_selection()
767 return edit_box
768
769 def edit_objects(self):
770 edit_box = self._make_edit_dialog(self.level._game_objects)
771 res = edit_box.present()
772 choice = edit_box.get_selection()
773 if choice is None:
774 return
775 if res == 'OK':
776 cls = self.level.get_class(choice['classname'])
777 edit_dlg = self._edit_class(choice['classname'], cls, choice)
778 if edit_dlg is not None:
779 edited = edit_dlg.get_data()
780 if self.level.try_new_object(choice["classname"],
781 self.level._game_objects,
782 edited, choice):
783 edit_dlg.cleanup()
784 elif res == 'Delete':
785 self.level._game_objects.remove(choice)
786 self.level.reset_objs()
787
788 def edit_enemies(self):
789 edit_box = self._make_edit_dialog(self.level._enemies)
790 res = edit_box.present()
791 choice = edit_box.get_selection()
792 if choice is None:
793 return
794 if res == 'OK':
795 cls = self.level.get_class(choice['classname'], ne)
796 edit_dlg = self._edit_class(choice['classname'], cls, choice)
797 if edit_dlg is not None:
798 edited = edit_dlg.get_data()
799 if self.level.try_new_object(choice["classname"],
800 self.level._enemies,
801 edited, choice):
802 edit_dlg.cleanup()
803 elif res == 'Delete':
804 self.level._enemies.remove(choice)
805 self.level.reset_objs()
806
807 def _make_choice_dialog(self, classes):
808 # Dialog to hold the editor
809 data = []
810 for cls_name, cls in classes:
811 data.append({"classname": cls_name, "class": cls})
812 choice_box = Dialog()
813 choice_box.rect = pygame.rect.Rect(0, 0, 700, 500)
814 table = ObjectTable(data)
815 choice_box.add(table)
816 buttons = []
817 for text in ['OK', 'Cancel']:
818 but = Button(text, action=lambda x=text: choice_box.dismiss(x))
819 buttons.append(but)
820 row = Row(buttons)
821 row.rect = pygame.rect.Rect(250, 450, 700, 50)
822 choice_box.add(row)
823 choice_box.get_selection = lambda: table.get_selection()
824 return choice_box
825
826 def add_game_object(self):
827 classes = ngo.get_editable_game_objects()
828 classes.extend(("collectable.%s" % cls_name, cls)
829 for cls_name, cls
830 in collectable.get_editable_game_objects())
831 choose = self._make_choice_dialog(classes)
832 res = choose.present()
833 choice = choose.get_selection()
834 if res == 'OK' and choice is not None:
835 classname = choice['classname']
836 cls = choice['class']
837 edit_dlg = self._edit_class(classname, cls, None)
838 if edit_dlg is not None:
839 new_cls = edit_dlg.get_data()
840 if self.level.try_new_object(classname,
841 self.level._game_objects,
842 new_cls, None):
843 edit_dlg.cleanup()
844
845 def add_enemy(self):
846 classes = ne.get_editable_enemies()
847 choose = self._make_choice_dialog(classes)
848 res = choose.present()
849 choice = choose.get_selection()
850 if res == 'OK' and choice is not None:
851 classname = choice['classname']
852 cls = choice['class']
853 edit_dlg = self._edit_class(classname, cls, None)
854 if edit_dlg is not None:
855 new_cls = edit_dlg.get_data()
856 if self.level.try_new_object(classname, self.level._enemies,
857 new_cls, None):
858 edit_dlg.cleanup()
859
860 def add_puzzler(self):
861 classes = np.get_editable_puzzlers()
862 choose = self._make_choice_dialog(classes)
863 res = choose.present()
864 choice = choose.get_selection()
865 if res == 'OK' and choice is not None:
866 classname = choice['classname']
867 cls = choice['class']
868 edit_dlg = self._edit_class(classname, cls, None)
869 if edit_dlg is not None:
870 new_cls = edit_dlg.get_data()
871 if self.level.try_new_object(classname,
872 self.level._game_objects,
873 new_cls, None):
874 edit_dlg.cleanup()
875
876 def _update_pos(self, obj, new_pos):
877 data = self.level.lookup[obj]
878 new_coords = self.level.point_to_pymunk(new_pos)
879 args = data['args']
880 old_coords = list(args[0])
881 args[0][0] = new_coords[0]
882 args[0][1] = new_coords[1]
883 param_defs = obj.requires()[1:] # chop off name
884 for i, (_key, key_type) in enumerate(param_defs):
885 if i > len(args):
886 break
887 if key_type == "polygon (convex)":
888 args[i] = self.level.translate_poly(
889 args[i], old_coords, new_coords)
890 print args[i]
891 self.level.reset_objs()
892 self.invalidate()
893
894
895class HighLightButton(Button):
896 """Button with highlight support"""
897 def __init__(self, text, parent, **kwds):
898 super(HighLightButton, self).__init__(text, **kwds)
899 self._parent = parent
900
901 def highlight(self):
902 self.border_color = pygame.color.Color('red')
903
904 def reset(self):
905 self.border_color = self.fg_color
906
907
908class PolyButton(HighLightButton):
909 """Button for coosing the correct polygon"""
910
911 def __init__(self, index, level_widget, parent):
912 if index is not None:
913 text = "P %s" % index
914 else:
915 text = 'Exit Draw Mode'
916 super(PolyButton, self).__init__(text, parent)
917 self.index = index
918 self.level_widget = level_widget
919
920 def action(self):
921 self.level_widget.change_poly(self.index)
922 self._parent.reset_lit_buttons()
923 if self.index is not None:
924 self.highlight()
925
926
927class GridSizeLabel(Label):
928 """Label and setter for grid size."""
929
930 def __init__(self, level_widget, **kwds):
931 self.level_widget = level_widget
932 super(GridSizeLabel, self).__init__(self.grid_text(), **kwds)
933
934 def grid_text(self):
935 return "Grid size: %d" % self.level_widget.grid_size
936
937 def inc_grid_size(self, amount):
938 self.level_widget.inc_grid_size(amount)
939 self.set_text(self.grid_text())
940
941
942class SnapButton(Button):
943 """Button for increasing or decreasing snap-to-grid size."""
944
945 def __init__(self, grid_size_label, parent, inc_amount):
946 self.grid_size_label = grid_size_label
947 self.inc_amount = inc_amount
948 text = "Grid %s%d" % (
949 '-' if inc_amount < 0 else '+',
950 abs(inc_amount))
951 self._parent = parent
952 super(SnapButton, self).__init__(text)
953
954 def action(self):
955 self.grid_size_label.inc_grid_size(self.inc_amount)
956
957
958class EditorApp(RootWidget):
959
960 def __init__(self, level, surface):
961 super(EditorApp, self).__init__(surface)
962 self.level = level
963 self.level_widget = LevelWidget(self.level, self)
964 self.add(self.level_widget)
965
966 self._dMenus = {}
967
968 self._light_buttons = []
969
970 self._make_draw_menu()
971 self._make_objects_menu()
972
973 self._menu_mode = 'drawing'
974 self._populate_menu()
975
976 self._zoom = 1
977
978 def _make_draw_menu(self):
979 widgets = []
980
981 white = pygame.color.Color("white")
982
983 # Add poly buttons
984 y = 5
985 for poly in range(1, 10):
986 but = PolyButton(poly, self.level_widget, self)
987 but.rect = pygame.rect.Rect(0, 0, MENU_WIDTH // 3 - MENU_PAD,
988 MENU_BUTTON_HEIGHT)
989 index = poly % 3
990 if index == 1:
991 but.rect.move_ip(MENU_LEFT, y)
992 elif index == 2:
993 but.rect.move_ip(MENU_LEFT + MENU_WIDTH // 3 - MENU_HALF_PAD,
994 y)
995 else:
996 left = MENU_LEFT + 2 * MENU_WIDTH // 3 - MENU_HALF_PAD
997 but.rect.move_ip(left, y)
998 y += MENU_BUTTON_HEIGHT + MENU_PAD
999 self._light_buttons.append(but)
1000 widgets.append(but)
1001
1002 end_poly_but = PolyButton(None, self.level_widget, self)
1003 end_poly_but.rect = BUTTON_RECT.copy()
1004 end_poly_but.rect.move_ip(MENU_LEFT, y)
1005 widgets.append(end_poly_but)
1006 y += MENU_BUTTON_HEIGHT + MENU_PAD
1007
1008 self.move_point_but = HighLightButton("Mv Point", self,
1009 action=self.move_point)
1010 self.move_point_but.rect = HALF_BUTTON_RECT.copy()
1011 self.move_point_but.rect.move_ip(MENU_LEFT, y)
1012 widgets.append(self.move_point_but)
1013 self._light_buttons.append(self.move_point_but)
1014
1015 self.move_poly_but = HighLightButton("Mv Poly", self,
1016 action=self.move_poly)
1017 self.move_poly_but.rect = HALF_BUTTON_RECT.copy()
1018 self.move_poly_but.rect.move_ip(MENU_LEFT + MENU_HALF_WIDTH, y)
1019 widgets.append(self.move_poly_but)
1020 self._light_buttons.append(self.move_poly_but)
1021
1022 y += MENU_BUTTON_HEIGHT + MENU_PAD
1023
1024 # grid size widgets
1025 grid_size_label = GridSizeLabel(
1026 self.level_widget, width=BUTTON_RECT.width,
1027 align="c", fg_color=white)
1028 grid_size_label.rect.move_ip(MENU_LEFT, y)
1029 widgets.append(grid_size_label)
1030 y += grid_size_label.rect.height + MENU_PAD
1031 inc_snap_but = SnapButton(grid_size_label, self, 5)
1032 inc_snap_but.rect = HALF_BUTTON_RECT.copy()
1033 inc_snap_but.rect.move_ip(MENU_LEFT, y)
1034 widgets.append(inc_snap_but)
1035 dec_snap_but = SnapButton(grid_size_label, self, -5)
1036 dec_snap_but.rect = HALF_BUTTON_RECT.copy()
1037 dec_snap_but.rect.move_ip(
1038 MENU_LEFT + MENU_HALF_WIDTH, y)
1039 widgets.append(dec_snap_but)
1040 y += MENU_BUTTON_HEIGHT + MENU_PAD
1041
1042 self.draw_line_but = HighLightButton("Draw interior wall", self,
1043 action=self.set_line_mode)
1044 self.draw_line_but.rect = BUTTON_RECT.copy()
1045 self.draw_line_but.rect.move_ip(MENU_LEFT, y)
1046 widgets.append(self.draw_line_but)
1047 self._light_buttons.append(self.draw_line_but)
1048 y += MENU_BUTTON_HEIGHT + MENU_PAD
1049
1050 fill_but = Button('Fill exterior', action=self.level_widget.set_filled)
1051 fill_but.rect = BUTTON_RECT.copy()
1052 fill_but.rect.move_ip(MENU_LEFT, y)
1053 widgets.append(fill_but)
1054 y += MENU_BUTTON_HEIGHT + MENU_PAD
1055
1056 close_poly_but = Button('Close Polygon',
1057 action=self.level_widget.close_poly)
1058 close_poly_but.rect = BUTTON_RECT.copy()
1059 close_poly_but.rect.move_ip(MENU_LEFT, y)
1060 widgets.append(close_poly_but)
1061 y += MENU_BUTTON_HEIGHT + MENU_PAD
1062
1063 self.show_objs = CheckBox(fg_color=white)
1064 self.show_objs.rect = CHECK_RECT.copy()
1065 self.show_objs.rect.move_ip(MENU_LEFT, y)
1066 label = Label("Show Objects", fg_color=white)
1067 label.rect.move_ip(MENU_LEFT + MENU_BUTTON_HEIGHT // 2 + MENU_PAD, y)
1068 widgets.append(self.show_objs)
1069 widgets.append(label)
1070 y += label.rect.height + MENU_PAD
1071
1072 self.show_enemies = CheckBox(fg_color=white)
1073 self.show_enemies.rect = CHECK_RECT.copy()
1074 self.show_enemies.rect.move_ip(MENU_LEFT, y)
1075 label = Label("Show enemy start pos", fg_color=white)
1076 label.rect.move_ip(MENU_LEFT + MENU_BUTTON_HEIGHT // 2 + MENU_PAD, y)
1077 widgets.append(self.show_enemies)
1078 widgets.append(label)
1079 y += label.rect.height + MENU_PAD
1080
1081 y += MENU_PAD
1082 switch_but = Button('Switch to Objects', action=self.switch_to_objects)
1083 switch_but.rect = BUTTON_RECT.copy()
1084 switch_but.rect.move_ip(MENU_LEFT, y)
1085 widgets.append(switch_but)
1086 y += switch_but.rect.height + MENU_PAD
1087
1088 save_but = Button('Save Level', action=self.save)
1089 save_but.rect = BUTTON_RECT.copy()
1090 save_but.rect.move_ip(MENU_LEFT, y)
1091 widgets.append(save_but)
1092 y += MENU_BUTTON_HEIGHT + MENU_PAD
1093
1094 zoom_out = Button('Zoom out', action=self.level_widget.zoom_out)
1095 zoom_out.rect = BUTTON_RECT.copy()
1096 zoom_out.rect.width = zoom_out.rect.width // 2
1097 zoom_out.rect.move_ip(MENU_LEFT, y)
1098 widgets.append(zoom_out)
1099
1100 zoom_in = Button('Zoom in', action=self.level_widget.zoom_in)
1101 zoom_in.rect = BUTTON_RECT.copy()
1102 zoom_in.width = zoom_in.width // 2
1103 zoom_in.rect.move_ip(MENU_LEFT + zoom_out.width, y)
1104 widgets.append(zoom_in)
1105
1106 y = SCREEN[1] - MENU_BUTTON_HEIGHT - MENU_PAD
1107 quit_but = Button('Quit', action=self.do_quit)
1108 quit_but.rect = BUTTON_RECT.copy()
1109 quit_but.rect.move_ip(MENU_LEFT, y)
1110 widgets.append(quit_but)
1111
1112 self._dMenus['drawing'] = widgets
1113
1114 def _make_objects_menu(self):
1115 widgets = []
1116
1117 # Add poly buttons
1118 y = 15
1119
1120 edit_objs_but = Button('Edit Objects',
1121 action=self.level_widget.edit_objects)
1122 edit_objs_but.rect = BUTTON_RECT.copy()
1123 edit_objs_but.rect.move_ip(MENU_LEFT, y)
1124 widgets.append(edit_objs_but)
1125 y += MENU_BUTTON_HEIGHT + MENU_PAD
1126
1127 edir_enemies_but = Button('Edit Enemies',
1128 action=self.level_widget.edit_enemies)
1129 edir_enemies_but.rect = BUTTON_RECT.copy()
1130 edir_enemies_but.rect.move_ip(MENU_LEFT, y)
1131 widgets.append(edir_enemies_but)
1132 y += MENU_BUTTON_HEIGHT + MENU_PAD
1133
1134 add_obj_but = Button('Add Game Object',
1135 action=self.level_widget.add_game_object)
1136 add_obj_but.rect = BUTTON_RECT.copy()
1137 add_obj_but.rect.move_ip(MENU_LEFT, y)
1138 widgets.append(add_obj_but)
1139 y += MENU_BUTTON_HEIGHT + MENU_PAD
1140
1141 add_puzzle_but = Button('Add Puzzler',
1142 action=self.level_widget.add_puzzler)
1143 add_puzzle_but.rect = BUTTON_RECT.copy()
1144 add_puzzle_but.rect.move_ip(MENU_LEFT, y)
1145 widgets.append(add_puzzle_but)
1146 y += MENU_BUTTON_HEIGHT + MENU_PAD
1147
1148 add_enemy_but = Button('Add Enemy',
1149 action=self.level_widget.add_enemy)
1150 add_enemy_but.rect = BUTTON_RECT.copy()
1151 add_enemy_but.rect.move_ip(MENU_LEFT, y)
1152 widgets.append(add_enemy_but)
1153 y += MENU_BUTTON_HEIGHT + MENU_PAD
1154
1155 y += MENU_PAD
1156 self.sel_mode_but = HighLightButton('Select Object', self,
1157 action=self.sel_mode)
1158 self.sel_mode_but.rect = BUTTON_RECT.copy()
1159 self.sel_mode_but.rect.move_ip(MENU_LEFT, y)
1160 widgets.append(self.sel_mode_but)
1161 self._light_buttons.append(self.sel_mode_but)
1162 y += MENU_BUTTON_HEIGHT + MENU_PAD
1163
1164 y += MENU_PAD
1165 self.move_obj_mode_but = HighLightButton('Move Object', self,
1166 action=self.move_obj_mode)
1167 self.move_obj_mode_but.rect = BUTTON_RECT.copy()
1168 self.move_obj_mode_but.rect.move_ip(MENU_LEFT, y)
1169 widgets.append(self.move_obj_mode_but)
1170 self._light_buttons.append(self.move_obj_mode_but)
1171 y += MENU_BUTTON_HEIGHT + MENU_PAD
1172
1173 y += MENU_PAD
1174 switch_but = Button('Switch to Drawing', action=self.switch_to_draw)
1175 switch_but.rect = BUTTON_RECT.copy()
1176 switch_but.rect.move_ip(MENU_LEFT, y)
1177 widgets.append(switch_but)
1178 y += switch_but.rect.height + MENU_PAD
1179
1180 save_but = Button('Save Level', action=self.save)
1181 save_but.rect = BUTTON_RECT.copy()
1182 save_but.rect.move_ip(MENU_LEFT, y)
1183 widgets.append(save_but)
1184 y += MENU_BUTTON_HEIGHT + MENU_PAD
1185
1186 zoom_out = Button('Zoom out', action=self.level_widget.zoom_out)
1187 zoom_out.rect = BUTTON_RECT.copy()
1188 zoom_out.rect.width = zoom_out.rect.width // 2
1189 zoom_out.rect.move_ip(MENU_LEFT, y)
1190 widgets.append(zoom_out)
1191
1192 zoom_in = Button('Zoom in', action=self.level_widget.zoom_in)
1193 zoom_in.rect = BUTTON_RECT.copy()
1194 zoom_in.width = zoom_in.width // 2
1195 zoom_in.rect.move_ip(MENU_LEFT + zoom_out.width, y)
1196 widgets.append(zoom_in)
1197 y += MENU_BUTTON_HEIGHT + MENU_PAD
1198
1199 y = SCREEN[1] - MENU_BUTTON_HEIGHT - MENU_PAD
1200 quit_but = Button('Quit', action=self.do_quit)
1201 quit_but.rect = BUTTON_RECT.copy()
1202 quit_but.rect.move_ip(MENU_LEFT, y)
1203 widgets.append(quit_but)
1204
1205 self._dMenus['objects'] = widgets
1206
1207 def key_down(self, ev):
1208 if ev.key == pgl.K_ESCAPE:
1209 self.do_quit()
1210 elif ev.key == pgl.K_s:
1211 self.save()
1212 else:
1213 self.level_widget.key_down(ev)
1214
1215 def do_quit(self):
1216 res = ask("Really Quit?")
1217 if res == "OK":
1218 self.quit()
1219
1220 def save(self):
1221 closed, messages = self.level.all_closed()
1222 if closed:
1223 self.level.save()
1224 # display success
1225 alert("Level %s saved successfully." % self.level.name)
1226 else:
1227 # display errors
1228 alert("Failed to save level.\n\n%s" % '\n'.join(messages))
1229
1230 def switch_to_draw(self):
1231 if self._menu_mode != 'drawing':
1232 self._clear_menu()
1233 self._menu_mode = 'drawing'
1234 self._populate_menu()
1235
1236 def switch_to_objects(self):
1237 if self._menu_mode != 'objects':
1238 self._clear_menu()
1239 self._menu_mode = 'objects'
1240 self._populate_menu()
1241
1242 def _clear_menu(self):
1243 for widget in self._dMenus[self._menu_mode]:
1244 self.remove(widget)
1245
1246 def reset_lit_buttons(self):
1247 for but in self._light_buttons:
1248 but.reset()
1249
1250 def _populate_menu(self):
1251 self.level_widget.change_poly(None)
1252 self.level_widget.sel_mode = False
1253 self.level_widget.move_obj_mode = False
1254 self.level_widget.move_obj = None
1255 for widget in self._dMenus[self._menu_mode]:
1256 self.add(widget)
1257 self.invalidate()
1258
1259 def set_line_mode(self):
1260 self.level_widget.line_mode()
1261 self.draw_line_but.highlight()
1262
1263 def sel_mode(self):
1264 self.level_widget.sel_mode = not self.level_widget.sel_mode
1265 if self.level_widget.sel_mode:
1266 self.move_obj_mode_but.reset()
1267 self.sel_mode_but.highlight()
1268 self.level_widget.move_obj_mode = False
1269 self.level_widget.move_obj = None
1270 else:
1271 self.sel_mode_but.reset()
1272
1273 def move_obj_mode(self):
1274 self.level_widget.move_obj_mode = not self.level_widget.move_obj_mode
1275 if self.level_widget.move_obj_mode:
1276 self.sel_mode_but.reset()
1277 self.move_obj_mode_but.highlight()
1278 self.level_widget.sel_mode = False
1279 else:
1280 self.move_obj_mode_but.reset()
1281
1282 def mouse_move(self, ev):
1283 self.level_widget.mouse_move(ev)
1284
1285 def move_point(self):
1286 self.level_widget.set_move_mode()
1287 self.move_point_but.highlight()
1288
1289 def move_poly(self):
1290 self.level_widget.set_move_poly_mode()
1291 self.move_poly_but.highlight()
1292
1293 def draw(self, surface):
1294 # Update checkbox state
1295 if self._menu_mode == 'drawing':
1296 self.level_widget.set_objects(self.show_objs.value)
1297 self.level_widget.set_enemies(self.show_enemies.value)
1298 else:
1299 self.level_widget.set_objects(True)
1300 self.level_widget.set_enemies(True)
1301 super(EditorApp, self).draw(surface)
1302
1303
1304if __name__ == "__main__":
1305 if len(sys.argv) not in [2, 4]:
1306 print 'Please supply a levelname or levelname and level size'
1307 sys.exit()
1308 # Need to ensure we have defaults for rendering
1309 parse_args([])
1310 pygame.display.init()
1311 pygame.font.init()
1312 pygame.display.set_mode((SCREEN[0] + MENU_WIDTH, SCREEN[1]),
1313 pgl.SWSURFACE)
1314 if len(sys.argv) == 2:
1315 level = EditorLevel(sys.argv[1])
1316 level.load(pymunk.Space())
1317 elif len(sys.argv) == 4:
1318 level = EditorLevel(sys.argv[1], int(sys.argv[2]), int(sys.argv[3]))
1319 pygame.display.set_caption('Nagslang Area Editor')
1320 pygame.key.set_repeat(200, 100)
1321 app = EditorApp(level, pygame.display.get_surface())
1322 app.run()
Note: See TracBrowser for help on using the repository browser.