source: tools/area_editor.py@ 461:49f6adde49d0

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

Protect against corner case crash

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