source: tools/area_editor.py@ 489:31d721121966

Last change on this file since 489:31d721121966 was 489:31d721121966, checked in by Jeremy Thurgood <firxen@…>, 9 years ago

Fix level editor.

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