source: tools/area_editor.py@ 424:a64d894aa1bd

Last change on this file since 424:a64d894aa1bd was 424:a64d894aa1bd, checked in by Simon Cross <hodgestar@…>, 8 years ago

Actually snap to grid.

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