source: tools/area_editor.py@ 467:04127e7219cd

Last change on this file since 467:04127e7219cd was 467:04127e7219cd, checked in by Neil Muller <drnlmuller@…>, 9 years ago

More object checks

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