source: tools/area_editor.py@ 518:83f3a376e9a7

Last change on this file since 518:83f3a376e9a7 was 518:83f3a376e9a7, checked in by Neil Muller <drnlmuller@…>, 9 years ago

Allow choosing polygon for outlines

  • Property exe set to *
File size: 47.3 KB
Line 
1#!/usr/bin/env python
2
3# The basic area editor
4#
5# To edit an existing level, use
6# editor levelname
7#
8# To create a new level:
9#
10# editor levelname <xsize> <ysize>
11# (size specified in pixels
12#
13
14import os
15import sys
16
17import pygame
18import pygame.locals as pgl
19
20sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
21
22import pymunk
23
24from albow.root import RootWidget
25from albow.widget import Widget
26from albow.controls import Button, Label, CheckBox
27from albow.dialogs import alert, Dialog, 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 TypeError:
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 data['args'][0][0] = new_coords[0]
875 data['args'][0][1] = new_coords[1]
876 self.level.reset_objs()
877 self.invalidate()
878
879
880class HighLightButton(Button):
881 """Button with highlight support"""
882 def __init__(self, text, parent, **kwds):
883 super(HighLightButton, self).__init__(text, **kwds)
884 self._parent = parent
885
886 def highlight(self):
887 self.border_color = pygame.color.Color('red')
888
889 def reset(self):
890 self.border_color = self.fg_color
891
892
893class PolyButton(HighLightButton):
894 """Button for coosing the correct polygon"""
895
896 def __init__(self, index, level_widget, parent):
897 if index is not None:
898 text = "P %s" % index
899 else:
900 text = 'Exit Draw Mode'
901 super(PolyButton, self).__init__(text, parent)
902 self.index = index
903 self.level_widget = level_widget
904
905 def action(self):
906 self.level_widget.change_poly(self.index)
907 self._parent.reset_lit_buttons()
908 if self.index is not None:
909 self.highlight()
910
911
912class GridSizeLabel(Label):
913 """Label and setter for grid size."""
914
915 def __init__(self, level_widget, **kwds):
916 self.level_widget = level_widget
917 super(GridSizeLabel, self).__init__(self.grid_text(), **kwds)
918
919 def grid_text(self):
920 return "Grid size: %d" % self.level_widget.grid_size
921
922 def inc_grid_size(self, amount):
923 self.level_widget.inc_grid_size(amount)
924 self.set_text(self.grid_text())
925
926
927class SnapButton(Button):
928 """Button for increasing or decreasing snap-to-grid size."""
929
930 def __init__(self, grid_size_label, parent, inc_amount):
931 self.grid_size_label = grid_size_label
932 self.inc_amount = inc_amount
933 text = "Grid %s%d" % (
934 '-' if inc_amount < 0 else '+',
935 abs(inc_amount))
936 self._parent = parent
937 super(SnapButton, self).__init__(text)
938
939 def action(self):
940 self.grid_size_label.inc_grid_size(self.inc_amount)
941
942
943class EditorApp(RootWidget):
944
945 def __init__(self, level, surface):
946 super(EditorApp, self).__init__(surface)
947 self.level = level
948 self.level_widget = LevelWidget(self.level, self)
949 self.add(self.level_widget)
950
951 self._dMenus = {}
952
953 self._light_buttons = []
954
955 self._make_draw_menu()
956 self._make_objects_menu()
957
958 self._menu_mode = 'drawing'
959 self._populate_menu()
960
961 self._zoom = 1
962
963 def _make_draw_menu(self):
964 widgets = []
965
966 white = pygame.color.Color("white")
967
968 # Add poly buttons
969 y = 5
970 for poly in range(1, 10):
971 but = PolyButton(poly, self.level_widget, self)
972 but.rect = pygame.rect.Rect(0, 0, MENU_WIDTH // 3 - MENU_PAD,
973 MENU_BUTTON_HEIGHT)
974 index = poly % 3
975 if index == 1:
976 but.rect.move_ip(MENU_LEFT, y)
977 elif index == 2:
978 but.rect.move_ip(MENU_LEFT + MENU_WIDTH // 3 - MENU_HALF_PAD,
979 y)
980 else:
981 left = MENU_LEFT + 2 * MENU_WIDTH // 3 - MENU_HALF_PAD
982 but.rect.move_ip(left, y)
983 y += MENU_BUTTON_HEIGHT + MENU_PAD
984 self._light_buttons.append(but)
985 widgets.append(but)
986
987 end_poly_but = PolyButton(None, self.level_widget, self)
988 end_poly_but.rect = BUTTON_RECT.copy()
989 end_poly_but.rect.move_ip(MENU_LEFT, y)
990 widgets.append(end_poly_but)
991 y += MENU_BUTTON_HEIGHT + MENU_PAD
992
993 self.move_point_but = HighLightButton("Mv Point", self,
994 action=self.move_point)
995 self.move_point_but.rect = HALF_BUTTON_RECT.copy()
996 self.move_point_but.rect.move_ip(MENU_LEFT, y)
997 widgets.append(self.move_point_but)
998 self._light_buttons.append(self.move_point_but)
999
1000 self.move_poly_but = HighLightButton("Mv Poly", self,
1001 action=self.move_poly)
1002 self.move_poly_but.rect = HALF_BUTTON_RECT.copy()
1003 self.move_poly_but.rect.move_ip(MENU_LEFT + MENU_HALF_WIDTH, y)
1004 widgets.append(self.move_poly_but)
1005 self._light_buttons.append(self.move_poly_but)
1006
1007 y += MENU_BUTTON_HEIGHT + MENU_PAD
1008
1009 # grid size widgets
1010 grid_size_label = GridSizeLabel(
1011 self.level_widget, width=BUTTON_RECT.width,
1012 align="c", fg_color=white)
1013 grid_size_label.rect.move_ip(MENU_LEFT, y)
1014 widgets.append(grid_size_label)
1015 y += grid_size_label.rect.height + MENU_PAD
1016 inc_snap_but = SnapButton(grid_size_label, self, 5)
1017 inc_snap_but.rect = HALF_BUTTON_RECT.copy()
1018 inc_snap_but.rect.move_ip(MENU_LEFT, y)
1019 widgets.append(inc_snap_but)
1020 dec_snap_but = SnapButton(grid_size_label, self, -5)
1021 dec_snap_but.rect = HALF_BUTTON_RECT.copy()
1022 dec_snap_but.rect.move_ip(
1023 MENU_LEFT + MENU_HALF_WIDTH, y)
1024 widgets.append(dec_snap_but)
1025 y += MENU_BUTTON_HEIGHT + MENU_PAD
1026
1027 self.draw_line_but = HighLightButton("Draw interior wall", self,
1028 action=self.set_line_mode)
1029 self.draw_line_but.rect = BUTTON_RECT.copy()
1030 self.draw_line_but.rect.move_ip(MENU_LEFT, y)
1031 widgets.append(self.draw_line_but)
1032 self._light_buttons.append(self.draw_line_but)
1033 y += MENU_BUTTON_HEIGHT + MENU_PAD
1034
1035 fill_but = Button('Fill exterior', action=self.level_widget.set_filled)
1036 fill_but.rect = BUTTON_RECT.copy()
1037 fill_but.rect.move_ip(MENU_LEFT, y)
1038 widgets.append(fill_but)
1039 y += MENU_BUTTON_HEIGHT + MENU_PAD
1040
1041 close_poly_but = Button('Close Polygon',
1042 action=self.level_widget.close_poly)
1043 close_poly_but.rect = BUTTON_RECT.copy()
1044 close_poly_but.rect.move_ip(MENU_LEFT, y)
1045 widgets.append(close_poly_but)
1046 y += MENU_BUTTON_HEIGHT + MENU_PAD
1047
1048 self.show_objs = CheckBox(fg_color=white)
1049 self.show_objs.rect = CHECK_RECT.copy()
1050 self.show_objs.rect.move_ip(MENU_LEFT, y)
1051 label = Label("Show Objects", fg_color=white)
1052 label.rect.move_ip(MENU_LEFT + MENU_BUTTON_HEIGHT // 2 + MENU_PAD, y)
1053 widgets.append(self.show_objs)
1054 widgets.append(label)
1055 y += label.rect.height + MENU_PAD
1056
1057 self.show_enemies = CheckBox(fg_color=white)
1058 self.show_enemies.rect = CHECK_RECT.copy()
1059 self.show_enemies.rect.move_ip(MENU_LEFT, y)
1060 label = Label("Show enemy start pos", fg_color=white)
1061 label.rect.move_ip(MENU_LEFT + MENU_BUTTON_HEIGHT // 2 + MENU_PAD, y)
1062 widgets.append(self.show_enemies)
1063 widgets.append(label)
1064 y += label.rect.height + MENU_PAD
1065
1066 y += MENU_PAD
1067 switch_but = Button('Switch to Objects', action=self.switch_to_objects)
1068 switch_but.rect = BUTTON_RECT.copy()
1069 switch_but.rect.move_ip(MENU_LEFT, y)
1070 widgets.append(switch_but)
1071 y += switch_but.rect.height + MENU_PAD
1072
1073 save_but = Button('Save Level', action=self.save)
1074 save_but.rect = BUTTON_RECT.copy()
1075 save_but.rect.move_ip(MENU_LEFT, y)
1076 widgets.append(save_but)
1077 y += MENU_BUTTON_HEIGHT + MENU_PAD
1078
1079 zoom_out = Button('Zoom out', action=self.level_widget.zoom_out)
1080 zoom_out.rect = BUTTON_RECT.copy()
1081 zoom_out.rect.width = zoom_out.rect.width // 2
1082 zoom_out.rect.move_ip(MENU_LEFT, y)
1083 widgets.append(zoom_out)
1084
1085 zoom_in = Button('Zoom in', action=self.level_widget.zoom_in)
1086 zoom_in.rect = BUTTON_RECT.copy()
1087 zoom_in.width = zoom_in.width // 2
1088 zoom_in.rect.move_ip(MENU_LEFT + zoom_out.width, y)
1089 widgets.append(zoom_in)
1090
1091 y = SCREEN[1] - MENU_BUTTON_HEIGHT - MENU_PAD
1092 quit_but = Button('Quit', action=self.do_quit)
1093 quit_but.rect = BUTTON_RECT.copy()
1094 quit_but.rect.move_ip(MENU_LEFT, y)
1095 widgets.append(quit_but)
1096
1097 self._dMenus['drawing'] = widgets
1098
1099 def _make_objects_menu(self):
1100 widgets = []
1101
1102 # Add poly buttons
1103 y = 15
1104
1105 edit_objs_but = Button('Edit Objects',
1106 action=self.level_widget.edit_objects)
1107 edit_objs_but.rect = BUTTON_RECT.copy()
1108 edit_objs_but.rect.move_ip(MENU_LEFT, y)
1109 widgets.append(edit_objs_but)
1110 y += MENU_BUTTON_HEIGHT + MENU_PAD
1111
1112 edir_enemies_but = Button('Edit Enemies',
1113 action=self.level_widget.edit_enemies)
1114 edir_enemies_but.rect = BUTTON_RECT.copy()
1115 edir_enemies_but.rect.move_ip(MENU_LEFT, y)
1116 widgets.append(edir_enemies_but)
1117 y += MENU_BUTTON_HEIGHT + MENU_PAD
1118
1119 add_obj_but = Button('Add Game Object',
1120 action=self.level_widget.add_game_object)
1121 add_obj_but.rect = BUTTON_RECT.copy()
1122 add_obj_but.rect.move_ip(MENU_LEFT, y)
1123 widgets.append(add_obj_but)
1124 y += MENU_BUTTON_HEIGHT + MENU_PAD
1125
1126 add_puzzle_but = Button('Add Puzzler',
1127 action=self.level_widget.add_puzzler)
1128 add_puzzle_but.rect = BUTTON_RECT.copy()
1129 add_puzzle_but.rect.move_ip(MENU_LEFT, y)
1130 widgets.append(add_puzzle_but)
1131 y += MENU_BUTTON_HEIGHT + MENU_PAD
1132
1133 add_enemy_but = Button('Add Enemy',
1134 action=self.level_widget.add_enemy)
1135 add_enemy_but.rect = BUTTON_RECT.copy()
1136 add_enemy_but.rect.move_ip(MENU_LEFT, y)
1137 widgets.append(add_enemy_but)
1138 y += MENU_BUTTON_HEIGHT + MENU_PAD
1139
1140 y += MENU_PAD
1141 self.sel_mode_but = HighLightButton('Select Object', self,
1142 action=self.sel_mode)
1143 self.sel_mode_but.rect = BUTTON_RECT.copy()
1144 self.sel_mode_but.rect.move_ip(MENU_LEFT, y)
1145 widgets.append(self.sel_mode_but)
1146 self._light_buttons.append(self.sel_mode_but)
1147 y += MENU_BUTTON_HEIGHT + MENU_PAD
1148
1149 y += MENU_PAD
1150 self.move_obj_mode_but = HighLightButton('Move Object', self,
1151 action=self.move_obj_mode)
1152 self.move_obj_mode_but.rect = BUTTON_RECT.copy()
1153 self.move_obj_mode_but.rect.move_ip(MENU_LEFT, y)
1154 widgets.append(self.move_obj_mode_but)
1155 self._light_buttons.append(self.move_obj_mode_but)
1156 y += MENU_BUTTON_HEIGHT + MENU_PAD
1157
1158 y += MENU_PAD
1159 switch_but = Button('Switch to Drawing', action=self.switch_to_draw)
1160 switch_but.rect = BUTTON_RECT.copy()
1161 switch_but.rect.move_ip(MENU_LEFT, y)
1162 widgets.append(switch_but)
1163 y += switch_but.rect.height + MENU_PAD
1164
1165 save_but = Button('Save Level', action=self.save)
1166 save_but.rect = BUTTON_RECT.copy()
1167 save_but.rect.move_ip(MENU_LEFT, y)
1168 widgets.append(save_but)
1169 y += MENU_BUTTON_HEIGHT + MENU_PAD
1170
1171 zoom_out = Button('Zoom out', action=self.level_widget.zoom_out)
1172 zoom_out.rect = BUTTON_RECT.copy()
1173 zoom_out.rect.width = zoom_out.rect.width // 2
1174 zoom_out.rect.move_ip(MENU_LEFT, y)
1175 widgets.append(zoom_out)
1176
1177 zoom_in = Button('Zoom in', action=self.level_widget.zoom_in)
1178 zoom_in.rect = BUTTON_RECT.copy()
1179 zoom_in.width = zoom_in.width // 2
1180 zoom_in.rect.move_ip(MENU_LEFT + zoom_out.width, y)
1181 widgets.append(zoom_in)
1182 y += MENU_BUTTON_HEIGHT + MENU_PAD
1183
1184 y = SCREEN[1] - MENU_BUTTON_HEIGHT - MENU_PAD
1185 quit_but = Button('Quit', action=self.do_quit)
1186 quit_but.rect = BUTTON_RECT.copy()
1187 quit_but.rect.move_ip(MENU_LEFT, y)
1188 widgets.append(quit_but)
1189
1190 self._dMenus['objects'] = widgets
1191
1192 def key_down(self, ev):
1193 if ev.key == pgl.K_ESCAPE:
1194 self.do_quit()
1195 elif ev.key == pgl.K_s:
1196 self.save()
1197 else:
1198 self.level_widget.key_down(ev)
1199
1200 def do_quit(self):
1201 res = ask("Really Quit?")
1202 if res == "OK":
1203 self.quit()
1204
1205 def save(self):
1206 closed, messages = self.level.all_closed()
1207 if closed:
1208 self.level.save()
1209 # display success
1210 alert("Level %s saved successfully." % self.level.name)
1211 else:
1212 # display errors
1213 alert("Failed to save level.\n\n%s" % '\n'.join(messages))
1214
1215 def switch_to_draw(self):
1216 if self._menu_mode != 'drawing':
1217 self._clear_menu()
1218 self._menu_mode = 'drawing'
1219 self._populate_menu()
1220
1221 def switch_to_objects(self):
1222 if self._menu_mode != 'objects':
1223 self._clear_menu()
1224 self._menu_mode = 'objects'
1225 self._populate_menu()
1226
1227 def _clear_menu(self):
1228 for widget in self._dMenus[self._menu_mode]:
1229 self.remove(widget)
1230
1231 def reset_lit_buttons(self):
1232 for but in self._light_buttons:
1233 but.reset()
1234
1235 def _populate_menu(self):
1236 self.level_widget.change_poly(None)
1237 self.level_widget.sel_mode = False
1238 self.level_widget.move_obj_mode = False
1239 self.level_widget.move_obj = None
1240 for widget in self._dMenus[self._menu_mode]:
1241 self.add(widget)
1242 self.invalidate()
1243
1244 def set_line_mode(self):
1245 self.level_widget.line_mode()
1246 self.draw_line_but.highlight()
1247
1248 def sel_mode(self):
1249 self.level_widget.sel_mode = not self.level_widget.sel_mode
1250 if self.level_widget.sel_mode:
1251 self.move_obj_mode_but.reset()
1252 self.sel_mode_but.highlight()
1253 self.level_widget.move_obj_mode = False
1254 self.level_widget.move_obj = None
1255 else:
1256 self.sel_mode_but.reset()
1257
1258 def move_obj_mode(self):
1259 self.level_widget.move_obj_mode = not self.level_widget.move_obj_mode
1260 if self.level_widget.move_obj_mode:
1261 self.sel_mode_but.reset()
1262 self.move_obj_mode_but.highlight()
1263 self.level_widget.sel_mode = False
1264 else:
1265 self.move_obj_mode_but.reset()
1266
1267 def mouse_move(self, ev):
1268 self.level_widget.mouse_move(ev)
1269
1270 def move_point(self):
1271 self.level_widget.set_move_mode()
1272 self.move_point_but.highlight()
1273
1274 def move_poly(self):
1275 self.level_widget.set_move_poly_mode()
1276 self.move_poly_but.highlight()
1277
1278 def draw(self, surface):
1279 # Update checkbox state
1280 if self._menu_mode == 'drawing':
1281 self.level_widget.set_objects(self.show_objs.value)
1282 self.level_widget.set_enemies(self.show_enemies.value)
1283 else:
1284 self.level_widget.set_objects(True)
1285 self.level_widget.set_enemies(True)
1286 super(EditorApp, self).draw(surface)
1287
1288
1289if __name__ == "__main__":
1290 if len(sys.argv) not in [2, 4]:
1291 print 'Please supply a levelname or levelname and level size'
1292 sys.exit()
1293 # Need to ensure we have defaults for rendering
1294 parse_args([])
1295 pygame.display.init()
1296 pygame.font.init()
1297 pygame.display.set_mode((SCREEN[0] + MENU_WIDTH, SCREEN[1]),
1298 pgl.SWSURFACE)
1299 if len(sys.argv) == 2:
1300 level = EditorLevel(sys.argv[1])
1301 level.load(pymunk.Space())
1302 elif len(sys.argv) == 4:
1303 level = EditorLevel(sys.argv[1], int(sys.argv[2]), int(sys.argv[3]))
1304 pygame.display.set_caption('Nagslang Area Editor')
1305 pygame.key.set_repeat(200, 100)
1306 app = EditorApp(level, pygame.display.get_surface())
1307 app.run()
Note: See TracBrowser for help on using the repository browser.