source: tools/area_editor.py@ 516:b5838fb35db3

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

More polygon buttons

  • Property exe set to *
File size: 46.9 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 += 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 = snapped_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 = "P %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, 10):
960 but = PolyButton(poly, self.level_widget, self)
961 but.rect = pygame.rect.Rect(0, 0, MENU_WIDTH // 3 - MENU_PAD,
962 MENU_BUTTON_HEIGHT)
963 index = poly % 3
964 if index == 1:
965 but.rect.move_ip(MENU_LEFT, y)
966 elif index == 2:
967 but.rect.move_ip(MENU_LEFT + MENU_WIDTH // 3 - MENU_HALF_PAD,
968 y)
969 else:
970 left = MENU_LEFT + 2 * MENU_WIDTH // 3 - MENU_HALF_PAD
971 but.rect.move_ip(left, y)
972 y += MENU_BUTTON_HEIGHT + MENU_PAD
973 self._light_buttons.append(but)
974 widgets.append(but)
975
976 end_poly_but = PolyButton(None, self.level_widget, self)
977 end_poly_but.rect = BUTTON_RECT.copy()
978 end_poly_but.rect.move_ip(MENU_LEFT, y)
979 widgets.append(end_poly_but)
980 y += MENU_BUTTON_HEIGHT + MENU_PAD
981
982 self.move_point_but = HighLightButton("Mv Point", self,
983 action=self.move_point)
984 self.move_point_but.rect = HALF_BUTTON_RECT.copy()
985 self.move_point_but.rect.move_ip(MENU_LEFT, y)
986 widgets.append(self.move_point_but)
987 self._light_buttons.append(self.move_point_but)
988
989 self.move_poly_but = HighLightButton("Mv Poly", self,
990 action=self.move_poly)
991 self.move_poly_but.rect = HALF_BUTTON_RECT.copy()
992 self.move_poly_but.rect.move_ip(MENU_LEFT + MENU_HALF_WIDTH, y)
993 widgets.append(self.move_poly_but)
994 self._light_buttons.append(self.move_poly_but)
995
996 y += MENU_BUTTON_HEIGHT + MENU_PAD
997
998 # grid size widgets
999 grid_size_label = GridSizeLabel(
1000 self.level_widget, width=BUTTON_RECT.width,
1001 align="c", fg_color=white)
1002 grid_size_label.rect.move_ip(MENU_LEFT, y)
1003 widgets.append(grid_size_label)
1004 y += grid_size_label.rect.height + MENU_PAD
1005 inc_snap_but = SnapButton(grid_size_label, self, 5)
1006 inc_snap_but.rect = HALF_BUTTON_RECT.copy()
1007 inc_snap_but.rect.move_ip(MENU_LEFT, y)
1008 widgets.append(inc_snap_but)
1009 dec_snap_but = SnapButton(grid_size_label, self, -5)
1010 dec_snap_but.rect = HALF_BUTTON_RECT.copy()
1011 dec_snap_but.rect.move_ip(
1012 MENU_LEFT + MENU_HALF_WIDTH, y)
1013 widgets.append(dec_snap_but)
1014 y += MENU_BUTTON_HEIGHT + MENU_PAD
1015
1016 self.draw_line_but = HighLightButton("Draw interior wall", self,
1017 action=self.set_line_mode)
1018 self.draw_line_but.rect = BUTTON_RECT.copy()
1019 self.draw_line_but.rect.move_ip(MENU_LEFT, y)
1020 widgets.append(self.draw_line_but)
1021 self._light_buttons.append(self.draw_line_but)
1022 y += MENU_BUTTON_HEIGHT + MENU_PAD
1023
1024 fill_but = Button('Fill exterior', action=self.level_widget.set_filled)
1025 fill_but.rect = BUTTON_RECT.copy()
1026 fill_but.rect.move_ip(MENU_LEFT, y)
1027 widgets.append(fill_but)
1028 y += MENU_BUTTON_HEIGHT + MENU_PAD
1029
1030 close_poly_but = Button('Close Polygon',
1031 action=self.level_widget.close_poly)
1032 close_poly_but.rect = BUTTON_RECT.copy()
1033 close_poly_but.rect.move_ip(MENU_LEFT, y)
1034 widgets.append(close_poly_but)
1035 y += MENU_BUTTON_HEIGHT + MENU_PAD
1036
1037 self.show_objs = CheckBox(fg_color=white)
1038 self.show_objs.rect = CHECK_RECT.copy()
1039 self.show_objs.rect.move_ip(MENU_LEFT, y)
1040 label = Label("Show Objects", fg_color=white)
1041 label.rect.move_ip(MENU_LEFT + MENU_BUTTON_HEIGHT // 2 + MENU_PAD, y)
1042 widgets.append(self.show_objs)
1043 widgets.append(label)
1044 y += label.rect.height + MENU_PAD
1045
1046 self.show_enemies = CheckBox(fg_color=white)
1047 self.show_enemies.rect = CHECK_RECT.copy()
1048 self.show_enemies.rect.move_ip(MENU_LEFT, y)
1049 label = Label("Show enemy start pos", fg_color=white)
1050 label.rect.move_ip(MENU_LEFT + MENU_BUTTON_HEIGHT // 2 + MENU_PAD, y)
1051 widgets.append(self.show_enemies)
1052 widgets.append(label)
1053 y += label.rect.height + MENU_PAD
1054
1055 y += MENU_PAD
1056 switch_but = Button('Switch to Objects', action=self.switch_to_objects)
1057 switch_but.rect = BUTTON_RECT.copy()
1058 switch_but.rect.move_ip(MENU_LEFT, y)
1059 widgets.append(switch_but)
1060 y += switch_but.rect.height + MENU_PAD
1061
1062 save_but = Button('Save Level', action=self.save)
1063 save_but.rect = BUTTON_RECT.copy()
1064 save_but.rect.move_ip(MENU_LEFT, y)
1065 widgets.append(save_but)
1066 y += MENU_BUTTON_HEIGHT + MENU_PAD
1067
1068 zoom_out = Button('Zoom out', action=self.level_widget.zoom_out)
1069 zoom_out.rect = BUTTON_RECT.copy()
1070 zoom_out.rect.width = zoom_out.rect.width // 2
1071 zoom_out.rect.move_ip(MENU_LEFT, y)
1072 widgets.append(zoom_out)
1073
1074 zoom_in = Button('Zoom in', action=self.level_widget.zoom_in)
1075 zoom_in.rect = BUTTON_RECT.copy()
1076 zoom_in.width = zoom_in.width // 2
1077 zoom_in.rect.move_ip(MENU_LEFT + zoom_out.width, y)
1078 widgets.append(zoom_in)
1079
1080 y = SCREEN[1] - MENU_BUTTON_HEIGHT - MENU_PAD
1081 quit_but = Button('Quit', action=self.do_quit)
1082 quit_but.rect = BUTTON_RECT.copy()
1083 quit_but.rect.move_ip(MENU_LEFT, y)
1084 widgets.append(quit_but)
1085
1086 self._dMenus['drawing'] = widgets
1087
1088 def _make_objects_menu(self):
1089 widgets = []
1090
1091 # Add poly buttons
1092 y = 15
1093
1094 edit_objs_but = Button('Edit Objects',
1095 action=self.level_widget.edit_objects)
1096 edit_objs_but.rect = BUTTON_RECT.copy()
1097 edit_objs_but.rect.move_ip(MENU_LEFT, y)
1098 widgets.append(edit_objs_but)
1099 y += MENU_BUTTON_HEIGHT + MENU_PAD
1100
1101 edir_enemies_but = Button('Edit Enemies',
1102 action=self.level_widget.edit_enemies)
1103 edir_enemies_but.rect = BUTTON_RECT.copy()
1104 edir_enemies_but.rect.move_ip(MENU_LEFT, y)
1105 widgets.append(edir_enemies_but)
1106 y += MENU_BUTTON_HEIGHT + MENU_PAD
1107
1108 add_obj_but = Button('Add Game Object',
1109 action=self.level_widget.add_game_object)
1110 add_obj_but.rect = BUTTON_RECT.copy()
1111 add_obj_but.rect.move_ip(MENU_LEFT, y)
1112 widgets.append(add_obj_but)
1113 y += MENU_BUTTON_HEIGHT + MENU_PAD
1114
1115 add_puzzle_but = Button('Add Puzzler',
1116 action=self.level_widget.add_puzzler)
1117 add_puzzle_but.rect = BUTTON_RECT.copy()
1118 add_puzzle_but.rect.move_ip(MENU_LEFT, y)
1119 widgets.append(add_puzzle_but)
1120 y += MENU_BUTTON_HEIGHT + MENU_PAD
1121
1122 add_enemy_but = Button('Add Enemy',
1123 action=self.level_widget.add_enemy)
1124 add_enemy_but.rect = BUTTON_RECT.copy()
1125 add_enemy_but.rect.move_ip(MENU_LEFT, y)
1126 widgets.append(add_enemy_but)
1127 y += MENU_BUTTON_HEIGHT + MENU_PAD
1128
1129 y += MENU_PAD
1130 self.sel_mode_but = HighLightButton('Select Object', self,
1131 action=self.sel_mode)
1132 self.sel_mode_but.rect = BUTTON_RECT.copy()
1133 self.sel_mode_but.rect.move_ip(MENU_LEFT, y)
1134 widgets.append(self.sel_mode_but)
1135 self._light_buttons.append(self.sel_mode_but)
1136 y += MENU_BUTTON_HEIGHT + MENU_PAD
1137
1138 y += MENU_PAD
1139 self.move_obj_mode_but = HighLightButton('Move Object', self,
1140 action=self.move_obj_mode)
1141 self.move_obj_mode_but.rect = BUTTON_RECT.copy()
1142 self.move_obj_mode_but.rect.move_ip(MENU_LEFT, y)
1143 widgets.append(self.move_obj_mode_but)
1144 self._light_buttons.append(self.move_obj_mode_but)
1145 y += MENU_BUTTON_HEIGHT + MENU_PAD
1146
1147 y += MENU_PAD
1148 switch_but = Button('Switch to Drawing', action=self.switch_to_draw)
1149 switch_but.rect = BUTTON_RECT.copy()
1150 switch_but.rect.move_ip(MENU_LEFT, y)
1151 widgets.append(switch_but)
1152 y += switch_but.rect.height + MENU_PAD
1153
1154 save_but = Button('Save Level', action=self.save)
1155 save_but.rect = BUTTON_RECT.copy()
1156 save_but.rect.move_ip(MENU_LEFT, y)
1157 widgets.append(save_but)
1158 y += MENU_BUTTON_HEIGHT + MENU_PAD
1159
1160 zoom_out = Button('Zoom out', action=self.level_widget.zoom_out)
1161 zoom_out.rect = BUTTON_RECT.copy()
1162 zoom_out.rect.width = zoom_out.rect.width // 2
1163 zoom_out.rect.move_ip(MENU_LEFT, y)
1164 widgets.append(zoom_out)
1165
1166 zoom_in = Button('Zoom in', action=self.level_widget.zoom_in)
1167 zoom_in.rect = BUTTON_RECT.copy()
1168 zoom_in.width = zoom_in.width // 2
1169 zoom_in.rect.move_ip(MENU_LEFT + zoom_out.width, y)
1170 widgets.append(zoom_in)
1171 y += MENU_BUTTON_HEIGHT + MENU_PAD
1172
1173 y = SCREEN[1] - MENU_BUTTON_HEIGHT - MENU_PAD
1174 quit_but = Button('Quit', action=self.do_quit)
1175 quit_but.rect = BUTTON_RECT.copy()
1176 quit_but.rect.move_ip(MENU_LEFT, y)
1177 widgets.append(quit_but)
1178
1179 self._dMenus['objects'] = widgets
1180
1181 def key_down(self, ev):
1182 if ev.key == pgl.K_ESCAPE:
1183 self.do_quit()
1184 elif ev.key == pgl.K_s:
1185 self.save()
1186 else:
1187 self.level_widget.key_down(ev)
1188
1189 def do_quit(self):
1190 res = ask("Really Quit?")
1191 if res == "OK":
1192 self.quit()
1193
1194 def save(self):
1195 closed, messages = self.level.all_closed()
1196 if closed:
1197 self.level.save()
1198 # display success
1199 alert("Level %s saved successfully." % self.level.name)
1200 else:
1201 # display errors
1202 alert("Failed to save level.\n\n%s" % '\n'.join(messages))
1203
1204 def switch_to_draw(self):
1205 if self._menu_mode != 'drawing':
1206 self._clear_menu()
1207 self._menu_mode = 'drawing'
1208 self._populate_menu()
1209
1210 def switch_to_objects(self):
1211 if self._menu_mode != 'objects':
1212 self._clear_menu()
1213 self._menu_mode = 'objects'
1214 self._populate_menu()
1215
1216 def _clear_menu(self):
1217 for widget in self._dMenus[self._menu_mode]:
1218 self.remove(widget)
1219
1220 def reset_lit_buttons(self):
1221 for but in self._light_buttons:
1222 but.reset()
1223
1224 def _populate_menu(self):
1225 self.level_widget.change_poly(None)
1226 self.level_widget.sel_mode = False
1227 self.level_widget.move_obj_mode = False
1228 self.level_widget.move_obj = None
1229 for widget in self._dMenus[self._menu_mode]:
1230 self.add(widget)
1231 self.invalidate()
1232
1233 def set_line_mode(self):
1234 self.level_widget.line_mode()
1235 self.draw_line_but.highlight()
1236
1237 def sel_mode(self):
1238 self.level_widget.sel_mode = not self.level_widget.sel_mode
1239 if self.level_widget.sel_mode:
1240 self.move_obj_mode_but.reset()
1241 self.sel_mode_but.highlight()
1242 self.level_widget.move_obj_mode = False
1243 self.level_widget.move_obj = None
1244 else:
1245 self.sel_mode_but.reset()
1246
1247 def move_obj_mode(self):
1248 self.level_widget.move_obj_mode = not self.level_widget.move_obj_mode
1249 if self.level_widget.move_obj_mode:
1250 self.sel_mode_but.reset()
1251 self.move_obj_mode_but.highlight()
1252 self.level_widget.sel_mode = False
1253 else:
1254 self.move_obj_mode_but.reset()
1255
1256 def mouse_move(self, ev):
1257 self.level_widget.mouse_move(ev)
1258
1259 def move_point(self):
1260 self.level_widget.set_move_mode()
1261 self.move_point_but.highlight()
1262
1263 def move_poly(self):
1264 self.level_widget.set_move_poly_mode()
1265 self.move_poly_but.highlight()
1266
1267 def draw(self, surface):
1268 # Update checkbox state
1269 if self._menu_mode == 'drawing':
1270 self.level_widget.set_objects(self.show_objs.value)
1271 self.level_widget.set_enemies(self.show_enemies.value)
1272 else:
1273 self.level_widget.set_objects(True)
1274 self.level_widget.set_enemies(True)
1275 super(EditorApp, self).draw(surface)
1276
1277
1278if __name__ == "__main__":
1279 if len(sys.argv) not in [2, 4]:
1280 print 'Please supply a levelname or levelname and level size'
1281 sys.exit()
1282 # Need to ensure we have defaults for rendering
1283 parse_args([])
1284 pygame.display.init()
1285 pygame.font.init()
1286 pygame.display.set_mode((SCREEN[0] + MENU_WIDTH, SCREEN[1]),
1287 pgl.SWSURFACE)
1288 if len(sys.argv) == 2:
1289 level = EditorLevel(sys.argv[1])
1290 level.load(pymunk.Space())
1291 elif len(sys.argv) == 4:
1292 level = EditorLevel(sys.argv[1], int(sys.argv[2]), int(sys.argv[3]))
1293 pygame.display.set_caption('Nagslang Area Editor')
1294 pygame.key.set_repeat(200, 100)
1295 app = EditorApp(level, pygame.display.get_surface())
1296 app.run()
Note: See TracBrowser for help on using the repository browser.