source: tools/area_editor.py@ 485:69b8d6cbc692

Last change on this file since 485:69b8d6cbc692 was 483:0ef66a84b24d, checked in by Neil Muller <drnlmuller@…>, 9 years ago

Print object details

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