source: tools/area_editor.py@ 486:04908862a0b2

Last change on this file since 486:04908862a0b2 was 486:04908862a0b2, checked in by Simon Cross <hodgestar@…>, 9 years ago

Sort object editor list.

  • Property exe set to *
File size: 46.4 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 = sorted(data,
290 key=lambda d: (d.get('classname'), d.get('name')))
291 self.selected_row = -1
292
293 def num_rows(self):
294 return len(self.data)
295
296 def row_data(self, i):
297 data = self.data[i]
298 if 'name' in data:
299 return ('%s (%s)' % (data['classname'], data['name']), )
300 return (data['classname'], )
301
302 def row_is_selected(self, i):
303 return self.selected_row == i
304
305 def click_row(self, i, ev):
306 self.selected_row = i
307
308 def get_selection(self):
309 if self.selected_row >= 0:
310 return self.data[self.selected_row]
311 return None
312
313
314class EditClassDialog(Dialog):
315
316 def __init__(self, classname, cls, data, level_widget,
317 delete=False):
318 super(EditClassDialog, self).__init__()
319 self.level_widget = level_widget
320 self.classname = classname
321 self.rect = pygame.rect.Rect(0, 0, 900, 550)
322 title = Label("Editing %s" % classname)
323 title.rect = pygame.rect.Rect(100, 10, 600, 25)
324 self.add(title)
325 self.requires = cls.requires()
326 y = 40
327 self.fields = {}
328 index = 0
329 self.poly_field = None
330 self.needs_cleanup = False
331 for requirement, hint in self.requires:
332 label = Label(requirement)
333 label.rect = pygame.rect.Rect(40, y, 200, 25)
334 self.add(label)
335 field = TextField()
336 field.rect = pygame.rect.Rect(220, y, 400, 25)
337 self.add(field)
338 if data is not None:
339 if requirement in data:
340 field.set_text('%s' % data[requirement])
341 elif 'args' in data and requirement != 'name':
342 # NB: The ordering assumptions in requires should make
343 # this safe, but it's really, really, really fragile
344 try:
345 field.set_text('%s' % data['args'][index])
346 index += 1
347 except IndexError:
348 # Assumed to be arguments with the default value
349 pass
350 if hint.startswith('polygon'):
351 self.poly_field = field
352 self.fields[requirement] = field
353 hintlabel = Label(hint)
354 hintlabel.rect = pygame.rect.Rect(640, y, 250, 25)
355 self.add(hintlabel)
356 y += 30
357 if self.poly_field:
358 y += 20
359 button = Button('Use Polygon 6', action=self.get_poly)
360 button.rect = pygame.rect.Rect(350, y, 250, 30)
361 self.add(button)
362 buttons = []
363 if delete:
364 labels = ['OK', 'Delete', 'Cancel']
365 else:
366 labels = ['OK', 'Cancel']
367 for text in labels:
368 but = Button(text, action=lambda x=text: self.dismiss(x))
369 buttons.append(but)
370 row = Row(buttons)
371 row.rect = pygame.rect.Rect(250, 500, 700, 50)
372 self.add(row)
373
374 def get_poly(self):
375 try:
376 data = self.level_widget.level.polygons[6][:]
377 except KeyError:
378 data = []
379 if data:
380 # We unclose the polygon, because that's what pymunk
381 # wants
382 if data[0] == data[-1] and len(data) > 1:
383 data.pop()
384 data = [list(x) for x in data]
385 self.needs_cleanup = True
386 self.poly_field.set_text('%s' % data)
387
388 def cleanup(self):
389 if self.needs_cleanup:
390 self.level_widget.level.polygons[6] = []
391 self.level_widget.invalidate()
392
393 def get_data(self):
394 result = {}
395 result['classname'] = self.classname
396 args = []
397 # We arrange to bounce this through yaml'ish to convert
398 # stuff to the expected type
399 for val, _ in self.requires:
400 text = self.fields[val].get_text()
401 if not text:
402 # skip empty fields
403 continue
404 if val == 'name':
405 result['name'] = text
406 elif self.fields[val] == self.poly_field and text:
407 # Evil, but faster than good
408 try:
409 l = eval(text)
410 except Exception:
411 alert("Invalid polygon %s" % text)
412 self.needs_cleanup = False
413 return None
414 # Check for convexity
415 if not pymunk.util.is_convex(l):
416 alert("Invalid polygon %s - not convex" % text)
417 return None
418 if not pymunk.util.is_clockwise(l):
419 l.reverse()
420 if not pymunk.util.is_clockwise(l):
421 alert("Invalid polygon %s - unable to make clockwise"
422 % text)
423 return None
424 args.append(' - - %s' % l[0])
425 for coord in l[1:]:
426 args.append(' - %s' % coord)
427 else:
428 args.append(' - ' + text)
429 data = "args:\n" + '\n'.join(args)
430 result['args'] = load_s(data)['args']
431 return result
432
433
434class LevelWidget(Widget):
435
436 def __init__(self, level, parent):
437 super(LevelWidget, self).__init__(pygame.rect.Rect(0, 0,
438 SCREEN[0], SCREEN[1]))
439 self.level = level
440 self.pos = (0, 0)
441 self.filled_mode = False
442 self.mouse_pos = None
443 self.cur_poly = None
444 self._mouse_drag = False
445 self._draw_objects = False
446 self._draw_enemies = False
447 self._draw_lines = False
448 self.grid_size = 1
449 self.sel_mode = False
450 self.move_obj_mode = False
451 self.move_obj = None
452 self._start_pos = None
453 self._parent = parent
454 self._move_point_mode = False
455 self._move_poly_mode = False
456 self._move_point = False
457 self._zoom_factor = 1.0
458
459 def _level_coordinates(self, pos):
460 # Move positions to level values
461 if not pos:
462 return (0, 0)
463 # Apply zoom_factor
464 pos = pos[0] + self.pos[0], pos[1] + self.pos[1]
465 zoomed = (pos[0] * self._zoom_factor, pos[1] * self._zoom_factor)
466 return int(zoomed[0]), int(zoomed[1])
467
468 def _move_view(self, offset):
469 new_pos = [self.pos[0] + offset[0], self.pos[1] + offset[1]]
470 if new_pos[0] < 0:
471 new_pos[0] = self.pos[0]
472 elif new_pos[0] > self.level.x - SCREEN[0]:
473 new_pos[0] = self.pos[0]
474 if new_pos[1] < 0:
475 new_pos[1] = self.pos[1]
476 elif new_pos[1] > self.level.y - SCREEN[1]:
477 new_pos[1] = self.pos[1]
478 self.pos = tuple(new_pos)
479
480 def inc_grid_size(self, amount):
481 self.grid_size = max(1, self.grid_size + amount)
482 if self.grid_size > 1:
483 self.grid_size = self.grid_size - (self.grid_size % 5)
484
485 def snap_to_grid(self, pos):
486 x = pos[0] - (pos[0] % self.grid_size)
487 y = pos[1] - (pos[1] % self.grid_size)
488 return (x, y)
489
490 def set_objects(self, value):
491 if self._draw_objects != value:
492 self._draw_objects = value
493 self.invalidate()
494
495 def set_enemies(self, value):
496 if self._draw_enemies != value:
497 self._draw_enemies = value
498 self.invalidate()
499
500 def zoom_out(self):
501 self._zoom_factor = self._zoom_factor * 2.0
502 if self._zoom_factor > 8:
503 self._zoom_factor = 8
504
505 def zoom_in(self):
506 self._zoom_factor = self._zoom_factor // 2.0
507 if self._zoom_factor < 1:
508 self._zoom_factor = 1
509
510 def draw(self, surface):
511 if (self.cur_poly is not None and self.cur_poly in self.level.polygons
512 and len(self.level.polygons[self.cur_poly])):
513 # We have an active polygon
514 mouse_pos = self._level_coordinates(self.mouse_pos)
515 mouse_pos = self.snap_to_grid(mouse_pos)
516 elif self._draw_lines:
517 # Interior wall mode
518 mouse_pos = self._level_coordinates(self.mouse_pos)
519 mouse_pos = self.snap_to_grid(mouse_pos)
520 elif self._move_point_mode or self._move_poly_mode:
521 mouse_pos = self._level_coordinates(self.mouse_pos)
522 mouse_pos = self.snap_to_grid(mouse_pos)
523 else:
524 mouse_pos = None
525 level_surface = level.draw(mouse_pos, self.cur_poly, self.filled_mode,
526 self._draw_lines, self._start_pos,
527 self._move_point_mode, self._move_poly_mode,
528 self._move_point, self._zoom_factor, self)
529 if self._draw_objects:
530 for thing in self.level.drawables:
531 if not isinstance(thing, ne.Enemy):
532 thing.render(level_surface)
533 if self._draw_enemies:
534 for thing in self.level.drawables:
535 if isinstance(thing, ne.Enemy):
536 thing.render(level_surface)
537 surface_area = pygame.rect.Rect(self.pos, SCREEN)
538 zoomed_surface = level_surface.copy()
539 if self._zoom_factor != 1:
540 zoomed_surface = pygame.transform.scale(
541 level_surface,
542 (int(level_surface.get_width() / self._zoom_factor),
543 int(level_surface.get_height() / self._zoom_factor)))
544 surface.blit(zoomed_surface, (0, 0), surface_area)
545
546 def change_poly(self, new_poly):
547 self.cur_poly = new_poly
548 self._draw_lines = False
549 self._move_point_mode = False
550 if self.cur_poly is not None:
551 self._parent.reset_lit_buttons()
552 self.filled_mode = False
553
554 def line_mode(self):
555 self.cur_poly = None
556 self._parent.reset_lit_buttons()
557 self._draw_lines = True
558 self.filled_mode = False
559 self._start_pos = None
560 self._move_point_mode = False
561 self._move_poly_mode = False
562
563 def key_down(self, ev):
564 if ev.key == pgl.K_LEFT:
565 self._move_view((-10, 0))
566 elif ev.key == pgl.K_RIGHT:
567 self._move_view((10, 0))
568 elif ev.key == pgl.K_UP:
569 self._move_view((0, -10))
570 elif ev.key == pgl.K_DOWN:
571 self._move_view((0, 10))
572 elif ev.key in (pgl.K_1, pgl.K_2, pgl.K_3, pgl.K_4, pgl.K_5, pgl.K_6):
573 self.change_poly(ev.key - pgl.K_0)
574 elif ev.key == pgl.K_0:
575 self.change_poly(None)
576 elif ev.key == pgl.K_d and self.cur_poly:
577 self.level.delete_point(self.cur_poly)
578 elif ev.key == pgl.K_f:
579 self.set_filled()
580 elif ev.key == pgl.K_c:
581 self.close_poly()
582
583 def set_move_poly_mode(self):
584 self._draw_lines = False
585 self._move_point_mode = False
586 self._move_poly_mode = True
587 self.filled_mode = False
588 self._parent.reset_lit_buttons()
589 self._move_point = None
590
591 def set_move_mode(self):
592 self._draw_lines = False
593 self._move_point_mode = True
594 self._move_poly_mode = False
595 self.filled_mode = False
596 self._parent.reset_lit_buttons()
597 self._move_point = None
598
599 def set_filled(self):
600 closed, _ = self.level.all_closed()
601 if closed:
602 self.cur_poly = None
603 self._parent.reset_lit_buttons()
604 self.filled_mode = True
605 self._draw_lines = False
606 self._move_point_mode = False
607 self._move_poly_mode = False
608 else:
609 alert('Not all polygons closed, so not filling')
610
611 def mouse_move(self, ev):
612 old_pos = self.mouse_pos
613 self.mouse_pos = ev.pos
614 if self.move_obj:
615 corrected_pos = self._level_coordinates(ev.pos)
616 snapped_pos = self.snap_to_grid(corrected_pos)
617 self._update_pos(self.move_obj, snapped_pos)
618 if old_pos != self.mouse_pos and (self.cur_poly or self._draw_lines
619 or self._move_point_mode
620 or self._move_poly_mode):
621 self.invalidate()
622
623 def mouse_drag(self, ev):
624 if self._mouse_drag:
625 old_pos = self.mouse_pos
626 self.mouse_pos = ev.pos
627 diff = (-self.mouse_pos[0] + old_pos[0],
628 -self.mouse_pos[1] + old_pos[1])
629 self._move_view(diff)
630 self.invalidate()
631
632 def mouse_down(self, ev):
633 corrected_pos = self._level_coordinates(ev.pos)
634 snapped_pos = self.snap_to_grid(corrected_pos)
635 if self.sel_mode and ev.button == 1:
636 obj = self.level.find_obj_at_pos(corrected_pos)
637 if obj is not None:
638 self._edit_selected(obj)
639 elif self.move_obj_mode and ev.button == 1 and not self.move_obj:
640 obj = self.level.find_obj_at_pos(corrected_pos)
641 if obj is not None:
642 if obj.movable():
643 self.move_obj = obj
644 data = self.level.lookup[obj]
645 print data
646 elif self.move_obj_mode and ev.button == 1 and self.move_obj:
647 self._update_pos(self.move_obj, snapped_pos)
648 self.move_obj = None
649 elif self._move_poly_mode and ev.button == 1:
650 if self._move_point:
651 # Place the current point
652 self.level.replace_poly(self._move_point, snapped_pos)
653 self._move_point = None
654 self.invalidate()
655 else:
656 # find the current point
657 self._move_point = self.level.find_vertex(corrected_pos)
658 elif self._move_point_mode and ev.button == 1:
659 if self._move_point:
660 # Place the current point
661 self.level.replace_vertex(self._move_point, snapped_pos)
662 self._move_point = None
663 self.invalidate()
664 else:
665 # find the current point
666 self._move_point = self.level.find_vertex(corrected_pos)
667 elif ev.button == 1:
668 if self._draw_lines:
669 if self._start_pos is None:
670 self._start_pos = corrected_pos
671 else:
672 self.level.add_line(self._start_pos, snapped_pos)
673 self._start_pos = None
674 else:
675 print "Click: %r" % (
676 self.level.point_to_pymunk(corrected_pos),)
677 if ev.button == 4: # Scroll up
678 self._move_view((0, -10))
679 elif ev.button == 5: # Scroll down
680 self._move_view((0, 10))
681 elif ev.button == 6: # Scroll left
682 self._move_view((-10, 0))
683 elif ev.button == 7: # Scroll right
684 self._move_view((10, 0))
685 elif self.cur_poly and ev.button == 1:
686 # Add a point
687 if not self.level.add_point(self.cur_poly, snapped_pos):
688 alert("Failed to place point")
689 elif ev.button == 3:
690 self._mouse_drag = True
691
692 def mouse_up(self, ev):
693 if ev.button == 3:
694 self._mouse_drag = False
695
696 def close_poly(self):
697 if self.cur_poly is None:
698 return
699 if self.level.close_poly(self.cur_poly):
700 alert("Successfully closed the polygon")
701 self.change_poly(None)
702 else:
703 alert("Failed to close the polygon")
704
705 def _edit_class(self, classname, cls, data):
706 # Dialog for class properties
707 dialog = EditClassDialog(classname, cls, data, self)
708 if dialog.present() == 'OK':
709 return dialog
710 return None
711
712 def _edit_selected(self, obj):
713 data = self.level.lookup[obj]
714 cls = obj.__class__
715 classname = obj.__class__.__name__
716 dialog = EditClassDialog(classname, cls, data, self, True)
717 res = dialog.present()
718 if res == 'OK':
719 edited = dialog.get_data()
720 if edited is not None:
721 for target in [self.level._game_objects, self.level._enemies]:
722 if data in target:
723 if self.level.try_new_object(classname, target,
724 edited, data):
725 dialog.cleanup()
726 break
727 elif res == 'Delete':
728 for target in [self.level._game_objects, self.level._enemies]:
729 if data in target:
730 target.remove(data)
731 self.level.reset_objs()
732 break
733
734 def _make_edit_dialog(self, entries):
735 # Dialog to hold the editor
736 edit_box = Dialog()
737 edit_box.rect = pygame.rect.Rect(0, 0, 700, 500)
738 table = ObjectTable(entries)
739 edit_box.add(table)
740 buttons = []
741 for text in ['OK', 'Delete', 'Cancel']:
742 but = Button(text, action=lambda x=text: edit_box.dismiss(x))
743 buttons.append(but)
744 row = Row(buttons)
745 row.rect = pygame.rect.Rect(250, 450, 700, 50)
746 edit_box.add(row)
747 edit_box.get_selection = lambda: table.get_selection()
748 return edit_box
749
750 def edit_objects(self):
751 edit_box = self._make_edit_dialog(self.level._game_objects)
752 res = edit_box.present()
753 choice = edit_box.get_selection()
754 if choice is None:
755 return
756 if res == 'OK':
757 cls = self.level.get_class(choice['classname'])
758 edit_dlg = self._edit_class(choice['classname'], cls, choice)
759 if edit_dlg is not None:
760 edited = edit_dlg.get_data()
761 if self.level.try_new_object(choice["classname"],
762 self.level._game_objects,
763 edited, choice):
764 edit_dlg.cleanup()
765 elif res == 'Delete':
766 self.level._game_objects.remove(choice)
767 self.level.reset_objs()
768
769 def edit_enemies(self):
770 edit_box = self._make_edit_dialog(self.level._enemies)
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'], ne)
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._enemies,
782 edited, choice):
783 edit_dlg.cleanup()
784 elif res == 'Delete':
785 self.level._enemies.remove(choice)
786 self.level.reset_objs()
787
788 def _make_choice_dialog(self, classes):
789 # Dialog to hold the editor
790 data = []
791 for cls_name, cls in classes:
792 data.append({"classname": cls_name, "class": cls})
793 choice_box = Dialog()
794 choice_box.rect = pygame.rect.Rect(0, 0, 700, 500)
795 table = ObjectTable(data)
796 choice_box.add(table)
797 buttons = []
798 for text in ['OK', 'Cancel']:
799 but = Button(text, action=lambda x=text: choice_box.dismiss(x))
800 buttons.append(but)
801 row = Row(buttons)
802 row.rect = pygame.rect.Rect(250, 450, 700, 50)
803 choice_box.add(row)
804 choice_box.get_selection = lambda: table.get_selection()
805 return choice_box
806
807 def add_game_object(self):
808 classes = ngo.get_editable_game_objects()
809 choose = self._make_choice_dialog(classes)
810 res = choose.present()
811 choice = choose.get_selection()
812 if res == 'OK' and choice is not None:
813 classname = choice['classname']
814 cls = choice['class']
815 edit_dlg = self._edit_class(classname, cls, None)
816 if edit_dlg is not None:
817 new_cls = edit_dlg.get_data()
818 if self.level.try_new_object(classname,
819 self.level._game_objects,
820 new_cls, None):
821 edit_dlg.cleanup()
822
823 def add_enemy(self):
824 classes = ne.get_editable_enemies()
825 choose = self._make_choice_dialog(classes)
826 res = choose.present()
827 choice = choose.get_selection()
828 if res == 'OK' and choice is not None:
829 classname = choice['classname']
830 cls = choice['class']
831 edit_dlg = self._edit_class(classname, cls, None)
832 if edit_dlg is not None:
833 new_cls = edit_dlg.get_data()
834 if self.level.try_new_object(classname, self.level._enemies,
835 new_cls, None):
836 edit_dlg.cleanup()
837
838 def add_puzzler(self):
839 classes = np.get_editable_puzzlers()
840 choose = self._make_choice_dialog(classes)
841 res = choose.present()
842 choice = choose.get_selection()
843 if res == 'OK' and choice is not None:
844 classname = choice['classname']
845 cls = choice['class']
846 edit_dlg = self._edit_class(classname, cls, None)
847 if edit_dlg is not None:
848 new_cls = edit_dlg.get_data()
849 if self.level.try_new_object(classname,
850 self.level._game_objects,
851 new_cls, None):
852 edit_dlg.cleanup()
853
854 def _update_pos(self, obj, new_pos):
855 data = self.level.lookup[obj]
856 new_coords = self.level.point_to_pymunk(new_pos)
857 data['args'][0][0] = new_coords[0]
858 data['args'][0][1] = new_coords[1]
859 self.level.reset_objs()
860 self.invalidate()
861
862
863class HighLightButton(Button):
864 """Button with highlight support"""
865 def __init__(self, text, parent, **kwds):
866 super(HighLightButton, self).__init__(text, **kwds)
867 self._parent = parent
868
869 def highlight(self):
870 self.border_color = pygame.color.Color('red')
871
872 def reset(self):
873 self.border_color = self.fg_color
874
875
876class PolyButton(HighLightButton):
877 """Button for coosing the correct polygon"""
878
879 def __init__(self, index, level_widget, parent):
880 if index is not None:
881 text = "Draw: %s" % index
882 else:
883 text = 'Exit Draw Mode'
884 super(PolyButton, self).__init__(text, parent)
885 self.index = index
886 self.level_widget = level_widget
887
888 def action(self):
889 self.level_widget.change_poly(self.index)
890 self._parent.reset_lit_buttons()
891 if self.index is not None:
892 self.highlight()
893
894
895class GridSizeLabel(Label):
896 """Label and setter for grid size."""
897
898 def __init__(self, level_widget, **kwds):
899 self.level_widget = level_widget
900 super(GridSizeLabel, self).__init__(self.grid_text(), **kwds)
901
902 def grid_text(self):
903 return "Grid size: %d" % self.level_widget.grid_size
904
905 def inc_grid_size(self, amount):
906 self.level_widget.inc_grid_size(amount)
907 self.set_text(self.grid_text())
908
909
910class SnapButton(Button):
911 """Button for increasing or decreasing snap-to-grid size."""
912
913 def __init__(self, grid_size_label, parent, inc_amount):
914 self.grid_size_label = grid_size_label
915 self.inc_amount = inc_amount
916 text = "Grid %s%d" % (
917 '-' if inc_amount < 0 else '+',
918 abs(inc_amount))
919 self._parent = parent
920 super(SnapButton, self).__init__(text)
921
922 def action(self):
923 self.grid_size_label.inc_grid_size(self.inc_amount)
924
925
926class EditorApp(RootWidget):
927
928 def __init__(self, level, surface):
929 super(EditorApp, self).__init__(surface)
930 self.level = level
931 self.level_widget = LevelWidget(self.level, self)
932 self.add(self.level_widget)
933
934 self._dMenus = {}
935
936 self._light_buttons = []
937
938 self._make_draw_menu()
939 self._make_objects_menu()
940
941 self._menu_mode = 'drawing'
942 self._populate_menu()
943
944 self._zoom = 1
945
946 def _make_draw_menu(self):
947 widgets = []
948
949 white = pygame.color.Color("white")
950
951 # Add poly buttons
952 y = 5
953 for poly in range(1, 7):
954 but = PolyButton(poly, self.level_widget, self)
955 but.rect = pygame.rect.Rect(0, 0, MENU_WIDTH // 2 - MENU_PAD,
956 MENU_BUTTON_HEIGHT)
957 if poly % 2:
958 but.rect.move_ip(MENU_LEFT, y)
959 else:
960 but.rect.move_ip(MENU_LEFT + MENU_WIDTH // 2 - MENU_HALF_PAD,
961 y)
962 y += MENU_BUTTON_HEIGHT + MENU_PAD
963 self._light_buttons.append(but)
964 widgets.append(but)
965
966 end_poly_but = PolyButton(None, self.level_widget, self)
967 end_poly_but.rect = BUTTON_RECT.copy()
968 end_poly_but.rect.move_ip(MENU_LEFT, y)
969 widgets.append(end_poly_but)
970 y += MENU_BUTTON_HEIGHT + MENU_PAD
971
972 self.move_point_but = HighLightButton("Mv Point", self,
973 action=self.move_point)
974 self.move_point_but.rect = HALF_BUTTON_RECT.copy()
975 self.move_point_but.rect.move_ip(MENU_LEFT, y)
976 widgets.append(self.move_point_but)
977 self._light_buttons.append(self.move_point_but)
978
979 self.move_poly_but = HighLightButton("Mv Poly", self,
980 action=self.move_poly)
981 self.move_poly_but.rect = HALF_BUTTON_RECT.copy()
982 self.move_poly_but.rect.move_ip(MENU_LEFT + MENU_HALF_WIDTH, y)
983 widgets.append(self.move_poly_but)
984 self._light_buttons.append(self.move_poly_but)
985
986 y += MENU_BUTTON_HEIGHT + MENU_PAD
987
988 # grid size widgets
989 grid_size_label = GridSizeLabel(
990 self.level_widget, width=BUTTON_RECT.width,
991 align="c", fg_color=white)
992 grid_size_label.rect.move_ip(MENU_LEFT, y)
993 widgets.append(grid_size_label)
994 y += grid_size_label.rect.height + MENU_PAD
995 inc_snap_but = SnapButton(grid_size_label, self, 5)
996 inc_snap_but.rect = HALF_BUTTON_RECT.copy()
997 inc_snap_but.rect.move_ip(MENU_LEFT, y)
998 widgets.append(inc_snap_but)
999 dec_snap_but = SnapButton(grid_size_label, self, -5)
1000 dec_snap_but.rect = HALF_BUTTON_RECT.copy()
1001 dec_snap_but.rect.move_ip(
1002 MENU_LEFT + MENU_HALF_WIDTH, y)
1003 widgets.append(dec_snap_but)
1004 y += MENU_BUTTON_HEIGHT + MENU_PAD
1005
1006 self.draw_line_but = HighLightButton("Draw interior wall", self,
1007 action=self.set_line_mode)
1008 self.draw_line_but.rect = BUTTON_RECT.copy()
1009 self.draw_line_but.rect.move_ip(MENU_LEFT, y)
1010 widgets.append(self.draw_line_but)
1011 self._light_buttons.append(self.draw_line_but)
1012 y += MENU_BUTTON_HEIGHT + MENU_PAD
1013
1014 fill_but = Button('Fill exterior', action=self.level_widget.set_filled)
1015 fill_but.rect = BUTTON_RECT.copy()
1016 fill_but.rect.move_ip(MENU_LEFT, y)
1017 widgets.append(fill_but)
1018 y += MENU_BUTTON_HEIGHT + MENU_PAD
1019
1020 close_poly_but = Button('Close Polygon',
1021 action=self.level_widget.close_poly)
1022 close_poly_but.rect = BUTTON_RECT.copy()
1023 close_poly_but.rect.move_ip(MENU_LEFT, y)
1024 widgets.append(close_poly_but)
1025 y += MENU_BUTTON_HEIGHT + MENU_PAD
1026
1027 self.show_objs = CheckBox(fg_color=white)
1028 self.show_objs.rect = CHECK_RECT.copy()
1029 self.show_objs.rect.move_ip(MENU_LEFT, y)
1030 label = Label("Show Objects", fg_color=white)
1031 label.rect.move_ip(MENU_LEFT + MENU_BUTTON_HEIGHT // 2 + MENU_PAD, y)
1032 widgets.append(self.show_objs)
1033 widgets.append(label)
1034 y += label.rect.height + MENU_PAD
1035
1036 self.show_enemies = CheckBox(fg_color=white)
1037 self.show_enemies.rect = CHECK_RECT.copy()
1038 self.show_enemies.rect.move_ip(MENU_LEFT, y)
1039 label = Label("Show enemy start pos", fg_color=white)
1040 label.rect.move_ip(MENU_LEFT + MENU_BUTTON_HEIGHT // 2 + MENU_PAD, y)
1041 widgets.append(self.show_enemies)
1042 widgets.append(label)
1043 y += label.rect.height + MENU_PAD
1044
1045 y += MENU_PAD
1046 switch_but = Button('Switch to Objects', action=self.switch_to_objects)
1047 switch_but.rect = BUTTON_RECT.copy()
1048 switch_but.rect.move_ip(MENU_LEFT, y)
1049 widgets.append(switch_but)
1050 y += switch_but.rect.height + MENU_PAD
1051
1052 save_but = Button('Save Level', action=self.save)
1053 save_but.rect = BUTTON_RECT.copy()
1054 save_but.rect.move_ip(MENU_LEFT, y)
1055 widgets.append(save_but)
1056 y += MENU_BUTTON_HEIGHT + MENU_PAD
1057
1058 zoom_out = Button('Zoom out', action=self.level_widget.zoom_out)
1059 zoom_out.rect = BUTTON_RECT.copy()
1060 zoom_out.rect.width = zoom_out.rect.width // 2
1061 zoom_out.rect.move_ip(MENU_LEFT, y)
1062 widgets.append(zoom_out)
1063
1064 zoom_in = Button('Zoom in', action=self.level_widget.zoom_in)
1065 zoom_in.rect = BUTTON_RECT.copy()
1066 zoom_in.width = zoom_in.width // 2
1067 zoom_in.rect.move_ip(MENU_LEFT + zoom_out.width, y)
1068 widgets.append(zoom_in)
1069
1070 y = SCREEN[1] - MENU_BUTTON_HEIGHT - MENU_PAD
1071 quit_but = Button('Quit', action=self.quit)
1072 quit_but.rect = BUTTON_RECT.copy()
1073 quit_but.rect.move_ip(MENU_LEFT, y)
1074 widgets.append(quit_but)
1075
1076 self._dMenus['drawing'] = widgets
1077
1078 def _make_objects_menu(self):
1079 widgets = []
1080
1081 # Add poly buttons
1082 y = 15
1083
1084 edit_objs_but = Button('Edit Objects',
1085 action=self.level_widget.edit_objects)
1086 edit_objs_but.rect = BUTTON_RECT.copy()
1087 edit_objs_but.rect.move_ip(MENU_LEFT, y)
1088 widgets.append(edit_objs_but)
1089 y += MENU_BUTTON_HEIGHT + MENU_PAD
1090
1091 edir_enemies_but = Button('Edit Enemies',
1092 action=self.level_widget.edit_enemies)
1093 edir_enemies_but.rect = BUTTON_RECT.copy()
1094 edir_enemies_but.rect.move_ip(MENU_LEFT, y)
1095 widgets.append(edir_enemies_but)
1096 y += MENU_BUTTON_HEIGHT + MENU_PAD
1097
1098 add_obj_but = Button('Add Game Object',
1099 action=self.level_widget.add_game_object)
1100 add_obj_but.rect = BUTTON_RECT.copy()
1101 add_obj_but.rect.move_ip(MENU_LEFT, y)
1102 widgets.append(add_obj_but)
1103 y += MENU_BUTTON_HEIGHT + MENU_PAD
1104
1105 add_puzzle_but = Button('Add Puzzler',
1106 action=self.level_widget.add_puzzler)
1107 add_puzzle_but.rect = BUTTON_RECT.copy()
1108 add_puzzle_but.rect.move_ip(MENU_LEFT, y)
1109 widgets.append(add_puzzle_but)
1110 y += MENU_BUTTON_HEIGHT + MENU_PAD
1111
1112 add_enemy_but = Button('Add Enemy',
1113 action=self.level_widget.add_enemy)
1114 add_enemy_but.rect = BUTTON_RECT.copy()
1115 add_enemy_but.rect.move_ip(MENU_LEFT, y)
1116 widgets.append(add_enemy_but)
1117 y += MENU_BUTTON_HEIGHT + MENU_PAD
1118
1119 y += MENU_PAD
1120 self.sel_mode_but = HighLightButton('Select Object', self,
1121 action=self.sel_mode)
1122 self.sel_mode_but.rect = BUTTON_RECT.copy()
1123 self.sel_mode_but.rect.move_ip(MENU_LEFT, y)
1124 widgets.append(self.sel_mode_but)
1125 self._light_buttons.append(self.sel_mode_but)
1126 y += MENU_BUTTON_HEIGHT + MENU_PAD
1127
1128 y += MENU_PAD
1129 self.move_obj_mode_but = HighLightButton('Move Object', self,
1130 action=self.move_obj_mode)
1131 self.move_obj_mode_but.rect = BUTTON_RECT.copy()
1132 self.move_obj_mode_but.rect.move_ip(MENU_LEFT, y)
1133 widgets.append(self.move_obj_mode_but)
1134 self._light_buttons.append(self.move_obj_mode_but)
1135 y += MENU_BUTTON_HEIGHT + MENU_PAD
1136
1137 y += MENU_PAD
1138 switch_but = Button('Switch to Drawing', action=self.switch_to_draw)
1139 switch_but.rect = BUTTON_RECT.copy()
1140 switch_but.rect.move_ip(MENU_LEFT, y)
1141 widgets.append(switch_but)
1142 y += switch_but.rect.height + MENU_PAD
1143
1144 save_but = Button('Save Level', action=self.save)
1145 save_but.rect = BUTTON_RECT.copy()
1146 save_but.rect.move_ip(MENU_LEFT, y)
1147 widgets.append(save_but)
1148 y += MENU_BUTTON_HEIGHT + MENU_PAD
1149
1150 zoom_out = Button('Zoom out', action=self.level_widget.zoom_out)
1151 zoom_out.rect = BUTTON_RECT.copy()
1152 zoom_out.rect.width = zoom_out.rect.width // 2
1153 zoom_out.rect.move_ip(MENU_LEFT, y)
1154 widgets.append(zoom_out)
1155
1156 zoom_in = Button('Zoom in', action=self.level_widget.zoom_in)
1157 zoom_in.rect = BUTTON_RECT.copy()
1158 zoom_in.width = zoom_in.width // 2
1159 zoom_in.rect.move_ip(MENU_LEFT + zoom_out.width, y)
1160 widgets.append(zoom_in)
1161 y += MENU_BUTTON_HEIGHT + MENU_PAD
1162
1163 y = SCREEN[1] - MENU_BUTTON_HEIGHT - MENU_PAD
1164 quit_but = Button('Quit', action=self.quit)
1165 quit_but.rect = BUTTON_RECT.copy()
1166 quit_but.rect.move_ip(MENU_LEFT, y)
1167 widgets.append(quit_but)
1168
1169 self._dMenus['objects'] = widgets
1170
1171 def key_down(self, ev):
1172 if ev.key == pgl.K_ESCAPE:
1173 self.quit()
1174 elif ev.key == pgl.K_s:
1175 self.save()
1176 else:
1177 self.level_widget.key_down(ev)
1178
1179 def save(self):
1180 closed, messages = self.level.all_closed()
1181 if closed:
1182 self.level.save()
1183 # display success
1184 alert("Level %s saved successfully." % self.level.name)
1185 else:
1186 # display errors
1187 alert("Failed to save level.\n\n%s" % '\n'.join(messages))
1188
1189 def switch_to_draw(self):
1190 if self._menu_mode != 'drawing':
1191 self._clear_menu()
1192 self._menu_mode = 'drawing'
1193 self._populate_menu()
1194
1195 def switch_to_objects(self):
1196 if self._menu_mode != 'objects':
1197 self._clear_menu()
1198 self._menu_mode = 'objects'
1199 self._populate_menu()
1200
1201 def _clear_menu(self):
1202 for widget in self._dMenus[self._menu_mode]:
1203 self.remove(widget)
1204
1205 def reset_lit_buttons(self):
1206 for but in self._light_buttons:
1207 but.reset()
1208
1209 def _populate_menu(self):
1210 self.level_widget.change_poly(None)
1211 self.level_widget.sel_mode = False
1212 self.level_widget.move_obj_mode = False
1213 self.level_widget.move_obj = None
1214 for widget in self._dMenus[self._menu_mode]:
1215 self.add(widget)
1216 self.invalidate()
1217
1218 def set_line_mode(self):
1219 self.level_widget.line_mode()
1220 self.draw_line_but.highlight()
1221
1222 def sel_mode(self):
1223 self.level_widget.sel_mode = not self.level_widget.sel_mode
1224 if self.level_widget.sel_mode:
1225 self.move_obj_mode_but.reset()
1226 self.sel_mode_but.highlight()
1227 self.level_widget.move_obj_mode = False
1228 self.level_widget.move_obj = None
1229 else:
1230 self.sel_mode_but.reset()
1231
1232 def move_obj_mode(self):
1233 self.level_widget.move_obj_mode = not self.level_widget.move_obj_mode
1234 if self.level_widget.move_obj_mode:
1235 self.sel_mode_but.reset()
1236 self.move_obj_mode_but.highlight()
1237 self.level_widget.sel_mode = False
1238 else:
1239 self.move_obj_mode_but.reset()
1240
1241 def mouse_move(self, ev):
1242 self.level_widget.mouse_move(ev)
1243
1244 def move_point(self):
1245 self.level_widget.set_move_mode()
1246 self.move_point_but.highlight()
1247
1248 def move_poly(self):
1249 self.level_widget.set_move_poly_mode()
1250 self.move_poly_but.highlight()
1251
1252 def draw(self, surface):
1253 # Update checkbox state
1254 if self._menu_mode == 'drawing':
1255 self.level_widget.set_objects(self.show_objs.value)
1256 self.level_widget.set_enemies(self.show_enemies.value)
1257 else:
1258 self.level_widget.set_objects(True)
1259 self.level_widget.set_enemies(True)
1260 super(EditorApp, self).draw(surface)
1261
1262
1263if __name__ == "__main__":
1264 if len(sys.argv) not in [2, 4]:
1265 print 'Please supply a levelname or levelname and level size'
1266 sys.exit()
1267 # Need to ensure we have defaults for rendering
1268 parse_args([])
1269 pygame.display.init()
1270 pygame.font.init()
1271 pygame.display.set_mode((SCREEN[0] + MENU_WIDTH, SCREEN[1]),
1272 pgl.SWSURFACE)
1273 if len(sys.argv) == 2:
1274 level = EditorLevel(sys.argv[1])
1275 level.load(pymunk.Space())
1276 elif len(sys.argv) == 4:
1277 level = EditorLevel(sys.argv[1], int(sys.argv[2]), int(sys.argv[3]))
1278 pygame.display.set_caption('Nagslang Area Editor')
1279 pygame.key.set_repeat(200, 100)
1280 app = EditorApp(level, pygame.display.get_surface())
1281 app.run()
Note: See TracBrowser for help on using the repository browser.