source: tools/area_editor.py@ 524:011a087c4370

Last change on this file since 524:011a087c4370 was 524:011a087c4370, checked in by Neil Muller <drnlmuller@…>, 8 years ago

Fix error catch thinko

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