source: tools/area_editor.py@ 433:8cd9828828c0

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

PEP8

  • Property exe set to *
File size: 40.7 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.get(index, pygame.color.THECOLORS['black'])
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
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 if self.grid_size > 1:
430 self.grid_size = self.grid_size - (self.grid_size % 5)
431
432 def snap_to_grid(self, pos):
433 x = pos[0] - (pos[0] % self.grid_size)
434 y = pos[1] - (pos[1] % self.grid_size)
435 return (x, y)
436
437 def set_objects(self, value):
438 if self._draw_objects != value:
439 self._draw_objects = value
440 self.invalidate()
441
442 def set_enemies(self, value):
443 if self._draw_enemies != value:
444 self._draw_enemies = value
445 self.invalidate()
446
447 def zoom_out(self):
448 self._zoom_factor = self._zoom_factor * 2.0
449 if self._zoom_factor > 8:
450 self._zoom_factor = 8
451
452 def zoom_in(self):
453 self._zoom_factor = self._zoom_factor // 2.0
454 if self._zoom_factor < 1:
455 self._zoom_factor = 1
456
457 def draw(self, surface):
458 if (self.cur_poly is not None and self.cur_poly in self.level.polygons
459 and len(self.level.polygons[self.cur_poly])):
460 # We have an active polygon
461 mouse_pos = self._level_coordinates(self.mouse_pos)
462 mouse_pos = self.snap_to_grid(mouse_pos)
463 elif self._draw_lines:
464 # Interior wall mode
465 mouse_pos = self._level_coordinates(self.mouse_pos)
466 mouse_pos = self.snap_to_grid(mouse_pos)
467 elif self._move_point_mode:
468 mouse_pos = self._level_coordinates(self.mouse_pos)
469 mouse_pos = self.snap_to_grid(mouse_pos)
470 else:
471 mouse_pos = None
472 level_surface = level.draw(mouse_pos, self.cur_poly, self.filled_mode,
473 self._draw_lines, self._start_pos,
474 self._move_point_mode, self._move_point)
475 if self._draw_objects:
476 for thing in self.level.drawables:
477 if not isinstance(thing, ne.Enemy):
478 thing.render(level_surface)
479 if self._draw_enemies:
480 for thing in self.level.drawables:
481 if isinstance(thing, ne.Enemy):
482 thing.render(level_surface)
483 surface_area = pygame.rect.Rect(self.pos, SCREEN)
484 zoomed_surface = level_surface.copy()
485 if self._zoom_factor != 1:
486 zoomed_surface = pygame.transform.scale(
487 level_surface,
488 (int(level_surface.get_width() / self._zoom_factor),
489 int(level_surface.get_height() / self._zoom_factor)))
490 surface.blit(zoomed_surface, (0, 0), surface_area)
491
492 def change_poly(self, new_poly):
493 self.cur_poly = new_poly
494 self._draw_lines = False
495 if self.cur_poly is not None:
496 self._parent.reset_lit_buttons()
497 self.filled_mode = False
498
499 def line_mode(self):
500 self.cur_poly = None
501 self._parent.reset_lit_buttons()
502 self._draw_lines = True
503 self.filled_mode = False
504 self._start_pos = None
505 self._move_point_mode = False
506
507 def key_down(self, ev):
508 if ev.key == pgl.K_LEFT:
509 self._move_view((-10, 0))
510 elif ev.key == pgl.K_RIGHT:
511 self._move_view((10, 0))
512 elif ev.key == pgl.K_UP:
513 self._move_view((0, -10))
514 elif ev.key == pgl.K_DOWN:
515 self._move_view((0, 10))
516 elif ev.key in (pgl.K_1, pgl.K_2, pgl.K_3, pgl.K_4, pgl.K_5, pgl.K_6):
517 self.change_poly(ev.key - pgl.K_0)
518 elif ev.key == pgl.K_0:
519 self.change_poly(None)
520 elif ev.key == pgl.K_d and self.cur_poly:
521 self.level.delete_point(self.cur_poly)
522 elif ev.key == pgl.K_f:
523 self.set_filled()
524 elif ev.key == pgl.K_c:
525 self.close_poly()
526
527 def set_move_mode(self):
528 self._draw_lines = False
529 self._move_point_mode = True
530 self.filled_mode = False
531 self._parent.reset_lit_buttons()
532 self._move_point = None
533
534 def set_filled(self):
535 closed, _ = self.level.all_closed()
536 if closed:
537 self.cur_poly = None
538 self._parent.reset_lit_buttons()
539 self.filled_mode = True
540 self._draw_lines = False
541 self._move_point_mode = False
542 else:
543 alert('Not all polygons closed, so not filling')
544
545 def mouse_move(self, ev):
546 old_pos = self.mouse_pos
547 self.mouse_pos = ev.pos
548 if old_pos != self.mouse_pos and (self.cur_poly or self._draw_lines
549 or self._move_point_mode):
550 self.invalidate()
551
552 def mouse_drag(self, ev):
553 if self._mouse_drag:
554 old_pos = self.mouse_pos
555 self.mouse_pos = ev.pos
556 diff = (-self.mouse_pos[0] + old_pos[0],
557 -self.mouse_pos[1] + old_pos[1])
558 self._move_view(diff)
559 self.invalidate()
560
561 def mouse_down(self, ev):
562 corrected_pos = self._level_coordinates(ev.pos)
563 snapped_pos = self.snap_to_grid(corrected_pos)
564 if self.sel_mode and ev.button == 1:
565 obj = self.level.find_obj_at_pos(corrected_pos)
566 if obj is not None:
567 self._edit_selected(obj)
568 elif self._move_point_mode and ev.button == 1:
569 if self._move_point:
570 # Place the current point
571 self.level.replace_vertex(self._move_point, snapped_pos)
572 self._move_point = None
573 self.invalidate()
574 else:
575 # find the current point
576 self._move_point = self.level.find_vertex(snapped_pos)
577 elif ev.button == 1:
578 if self._draw_lines:
579 if self._start_pos is None:
580 self._start_pos = corrected_pos
581 else:
582 self.level.add_line(self._start_pos, snapped_pos)
583 self._start_pos = None
584 else:
585 print "Click: %r" % (
586 self.level.point_to_pymunk(corrected_pos),)
587 if ev.button == 4: # Scroll up
588 self._move_view((0, -10))
589 elif ev.button == 5: # Scroll down
590 self._move_view((0, 10))
591 elif ev.button == 6: # Scroll left
592 self._move_view((-10, 0))
593 elif ev.button == 7: # Scroll right
594 self._move_view((10, 0))
595 elif self.cur_poly and ev.button == 1:
596 # Add a point
597 self.level.add_point(self.cur_poly, snapped_pos)
598 elif ev.button == 3:
599 self._mouse_drag = True
600
601 def mouse_up(self, ev):
602 if ev.button == 3:
603 self._mouse_drag = False
604
605 def close_poly(self):
606 if self.cur_poly is None:
607 return
608 if self.level.close_poly(self.cur_poly):
609 alert("Successfully closed the polygon")
610 self.change_poly(None)
611 else:
612 alert("Failed to close the polygon")
613
614 def _edit_class(self, classname, cls, data):
615 # Dialog for class properties
616 dialog = EditClassDialog(classname, cls, data, self)
617 if dialog.present() == 'OK':
618 return dialog
619 return None
620
621 def _edit_selected(self, obj):
622 data = self.level.lookup[obj]
623 cls = obj.__class__
624 classname = obj.__class__.__name__
625 dialog = EditClassDialog(classname, cls, data, self, True)
626 res = dialog.present()
627 if res == 'OK':
628 edited = dialog.get_data()
629 if edited is not None:
630 for target in [self.level._game_objects, self.level._enemies]:
631 if data in target:
632 if self.level.try_new_object(classname, target,
633 edited, data):
634 dialog.cleanup()
635 break
636 elif res == 'Delete':
637 for target in [self.level._game_objects, self.level._enemies]:
638 if data in target:
639 target.remove(data)
640 self.level.reset_objs()
641 break
642
643 def _make_edit_dialog(self, entries):
644 # Dialog to hold the editor
645 edit_box = Dialog()
646 edit_box.rect = pygame.rect.Rect(0, 0, 700, 500)
647 table = ObjectTable(entries)
648 edit_box.add(table)
649 buttons = []
650 for text in ['OK', 'Delete', 'Cancel']:
651 but = Button(text, action=lambda x=text: edit_box.dismiss(x))
652 buttons.append(but)
653 row = Row(buttons)
654 row.rect = pygame.rect.Rect(250, 450, 700, 50)
655 edit_box.add(row)
656 edit_box.get_selection = lambda: table.get_selection()
657 return edit_box
658
659 def edit_objects(self):
660 edit_box = self._make_edit_dialog(self.level._game_objects)
661 res = edit_box.present()
662 choice = edit_box.get_selection()
663 if choice is None:
664 return
665 if res == 'OK':
666 cls = self.level.get_class(choice['classname'])
667 edit_dlg = self._edit_class(choice['classname'], cls, choice)
668 if edit_dlg is not None:
669 edited = edit_dlg.get_data()
670 if self.level.try_new_object(choice["classname"],
671 self.level._game_objects,
672 edited, choice):
673 edit_dlg.cleanup()
674 elif res == 'Delete':
675 self.level._game_objects.remove(choice)
676 self.level.reset_objs()
677
678 def edit_enemies(self):
679 edit_box = self._make_edit_dialog(self.level._enemies)
680 res = edit_box.present()
681 choice = edit_box.get_selection()
682 if choice is None:
683 return
684 if res == 'OK':
685 cls = self.level.get_class(choice['classname'], ne)
686 edit_dlg = self._edit_class(choice['classname'], cls, choice)
687 if edit_dlg is not None:
688 edited = edit_dlg.get_data()
689 if self.level.try_new_object(choice["classname"],
690 self.level._enemies,
691 edited, choice):
692 edit_dlg.cleanup()
693 elif res == 'Delete':
694 self.level._enemies.remove(choice)
695 self.level.reset_objs()
696
697 def _make_choice_dialog(self, classes):
698 # Dialog to hold the editor
699 data = []
700 for cls_name, cls in classes:
701 data.append({"classname": cls_name, "class": cls})
702 choice_box = Dialog()
703 choice_box.rect = pygame.rect.Rect(0, 0, 700, 500)
704 table = ObjectTable(data)
705 choice_box.add(table)
706 buttons = []
707 for text in ['OK', 'Cancel']:
708 but = Button(text, action=lambda x=text: choice_box.dismiss(x))
709 buttons.append(but)
710 row = Row(buttons)
711 row.rect = pygame.rect.Rect(250, 450, 700, 50)
712 choice_box.add(row)
713 choice_box.get_selection = lambda: table.get_selection()
714 return choice_box
715
716 def add_game_object(self):
717 classes = ngo.get_editable_game_objects()
718 choose = self._make_choice_dialog(classes)
719 res = choose.present()
720 choice = choose.get_selection()
721 if res == 'OK' and choice is not None:
722 classname = choice['classname']
723 cls = choice['class']
724 edit_dlg = self._edit_class(classname, cls, None)
725 if edit_dlg is not None:
726 new_cls = edit_dlg.get_data()
727 if self.level.try_new_object(classname,
728 self.level._game_objects,
729 new_cls, None):
730 edit_dlg.cleanup()
731
732 def add_enemy(self):
733 classes = ne.get_editable_enemies()
734 choose = self._make_choice_dialog(classes)
735 res = choose.present()
736 choice = choose.get_selection()
737 if res == 'OK' and choice is not None:
738 classname = choice['classname']
739 cls = choice['class']
740 edit_dlg = self._edit_class(classname, cls, None)
741 if edit_dlg is not None:
742 new_cls = edit_dlg.get_data()
743 if self.level.try_new_object(classname, self.level._enemies,
744 new_cls, None):
745 edit_dlg.cleanup()
746
747 def add_puzzler(self):
748 classes = np.get_editable_puzzlers()
749 choose = self._make_choice_dialog(classes)
750 res = choose.present()
751 choice = choose.get_selection()
752 if res == 'OK' and choice is not None:
753 classname = choice['classname']
754 cls = choice['class']
755 edit_dlg = self._edit_class(classname, cls, None)
756 if edit_dlg is not None:
757 new_cls = edit_dlg.get_data()
758 if self.level.try_new_object(classname,
759 self.level._game_objects,
760 new_cls, None):
761 edit_dlg.cleanup()
762
763
764class HighLightButton(Button):
765 """Button with highlight support"""
766 def __init__(self, text, parent, **kwds):
767 super(HighLightButton, self).__init__(text, **kwds)
768 self._parent = parent
769
770 def highlight(self):
771 self.border_color = pygame.color.Color('red')
772
773 def reset(self):
774 self.border_color = self.fg_color
775
776
777class PolyButton(HighLightButton):
778 """Button for coosing the correct polygon"""
779
780 def __init__(self, index, level_widget, parent):
781 if index is not None:
782 text = "Draw: %s" % index
783 else:
784 text = 'Exit Draw Mode'
785 super(PolyButton, self).__init__(text, parent)
786 self.index = index
787 self.level_widget = level_widget
788
789 def action(self):
790 self.level_widget.change_poly(self.index)
791 self._parent.reset_lit_buttons()
792 if self.index is not None:
793 self.highlight()
794
795
796class GridSizeLabel(Label):
797 """Label and setter for grid size."""
798
799 def __init__(self, level_widget, **kwds):
800 self.level_widget = level_widget
801 super(GridSizeLabel, self).__init__(self.grid_text(), **kwds)
802
803 def grid_text(self):
804 return "Grid size: %d" % self.level_widget.grid_size
805
806 def inc_grid_size(self, amount):
807 self.level_widget.inc_grid_size(amount)
808 self.set_text(self.grid_text())
809
810
811class SnapButton(Button):
812 """Button for increasing or decreasing snap-to-grid size."""
813
814 def __init__(self, grid_size_label, parent, inc_amount):
815 self.grid_size_label = grid_size_label
816 self.inc_amount = inc_amount
817 text = "Grid %s%d" % (
818 '-' if inc_amount < 0 else '+',
819 abs(inc_amount))
820 self._parent = parent
821 super(SnapButton, self).__init__(text)
822
823 def action(self):
824 self.grid_size_label.inc_grid_size(self.inc_amount)
825
826
827class EditorApp(RootWidget):
828
829 def __init__(self, level, surface):
830 super(EditorApp, self).__init__(surface)
831 self.level = level
832 self.level_widget = LevelWidget(self.level, self)
833 self.add(self.level_widget)
834
835 self._dMenus = {}
836
837 self._light_buttons = []
838
839 self._make_draw_menu()
840 self._make_objects_menu()
841
842 self._menu_mode = 'drawing'
843 self._populate_menu()
844
845 self._zoom = 1
846
847 def _make_draw_menu(self):
848 widgets = []
849
850 white = pygame.color.Color("white")
851
852 # Add poly buttons
853 y = 5
854 for poly in range(1, 7):
855 but = PolyButton(poly, self.level_widget, self)
856 but.rect = pygame.rect.Rect(0, 0, MENU_WIDTH // 2 - MENU_PAD,
857 MENU_BUTTON_HEIGHT)
858 if poly % 2:
859 but.rect.move_ip(MENU_LEFT, y)
860 else:
861 but.rect.move_ip(MENU_LEFT + MENU_WIDTH // 2 - MENU_HALF_PAD,
862 y)
863 y += MENU_BUTTON_HEIGHT + MENU_PAD
864 self._light_buttons.append(but)
865 widgets.append(but)
866
867 end_poly_but = PolyButton(None, self.level_widget, self)
868 end_poly_but.rect = BUTTON_RECT.copy()
869 end_poly_but.rect.move_ip(MENU_LEFT, y)
870 widgets.append(end_poly_but)
871 y += MENU_BUTTON_HEIGHT + MENU_PAD
872
873 self.move_point_but = HighLightButton("Move Point", self,
874 action=self.move_point)
875 self.move_point_but.rect = BUTTON_RECT.copy()
876 self.move_point_but.rect.move_ip(MENU_LEFT, y)
877 widgets.append(self.move_point_but)
878 self._light_buttons.append(self.move_point_but)
879 y += MENU_BUTTON_HEIGHT + MENU_PAD
880
881 # grid size widgets
882 grid_size_label = GridSizeLabel(
883 self.level_widget, width=BUTTON_RECT.width,
884 align="c", fg_color=white)
885 grid_size_label.rect.move_ip(MENU_LEFT, y)
886 widgets.append(grid_size_label)
887 y += grid_size_label.rect.height + MENU_PAD
888 inc_snap_but = SnapButton(grid_size_label, self, 5)
889 inc_snap_but.rect = HALF_BUTTON_RECT.copy()
890 inc_snap_but.rect.move_ip(MENU_LEFT, y)
891 widgets.append(inc_snap_but)
892 dec_snap_but = SnapButton(grid_size_label, self, -5)
893 dec_snap_but.rect = HALF_BUTTON_RECT.copy()
894 dec_snap_but.rect.move_ip(
895 MENU_LEFT + MENU_HALF_WIDTH, y)
896 widgets.append(dec_snap_but)
897 y += MENU_BUTTON_HEIGHT + MENU_PAD
898
899 self.draw_line_but = HighLightButton("Draw interior wall", self,
900 action=self.set_line_mode)
901 self.draw_line_but.rect = BUTTON_RECT.copy()
902 self.draw_line_but.rect.move_ip(MENU_LEFT, y)
903 widgets.append(self.draw_line_but)
904 self._light_buttons.append(self.draw_line_but)
905 y += MENU_BUTTON_HEIGHT + MENU_PAD
906
907 fill_but = Button('Fill exterior', action=self.level_widget.set_filled)
908 fill_but.rect = BUTTON_RECT.copy()
909 fill_but.rect.move_ip(MENU_LEFT, y)
910 widgets.append(fill_but)
911 y += MENU_BUTTON_HEIGHT + MENU_PAD
912
913 close_poly_but = Button('Close Polygon',
914 action=self.level_widget.close_poly)
915 close_poly_but.rect = BUTTON_RECT.copy()
916 close_poly_but.rect.move_ip(MENU_LEFT, y)
917 widgets.append(close_poly_but)
918 y += MENU_BUTTON_HEIGHT + MENU_PAD
919
920 self.show_objs = CheckBox(fg_color=white)
921 self.show_objs.rect = CHECK_RECT.copy()
922 self.show_objs.rect.move_ip(MENU_LEFT, y)
923 label = Label("Show Objects", fg_color=white)
924 label.rect.move_ip(MENU_LEFT + MENU_BUTTON_HEIGHT // 2 + MENU_PAD, y)
925 widgets.append(self.show_objs)
926 widgets.append(label)
927 y += label.rect.height + MENU_PAD
928
929 self.show_enemies = CheckBox(fg_color=white)
930 self.show_enemies.rect = CHECK_RECT.copy()
931 self.show_enemies.rect.move_ip(MENU_LEFT, y)
932 label = Label("Show enemy start pos", fg_color=white)
933 label.rect.move_ip(MENU_LEFT + MENU_BUTTON_HEIGHT // 2 + MENU_PAD, y)
934 widgets.append(self.show_enemies)
935 widgets.append(label)
936 y += label.rect.height + MENU_PAD
937
938 y += MENU_PAD
939 switch_but = Button('Switch to Objects', action=self.switch_to_objects)
940 switch_but.rect = BUTTON_RECT.copy()
941 switch_but.rect.move_ip(MENU_LEFT, y)
942 widgets.append(switch_but)
943 y += switch_but.rect.height + MENU_PAD
944
945 save_but = Button('Save Level', action=self.save)
946 save_but.rect = BUTTON_RECT.copy()
947 save_but.rect.move_ip(MENU_LEFT, y)
948 widgets.append(save_but)
949 y += MENU_BUTTON_HEIGHT + MENU_PAD
950
951 zoom_out = Button('Zoom out', action=self.level_widget.zoom_out)
952 zoom_out.rect = BUTTON_RECT.copy()
953 zoom_out.rect.width = zoom_out.rect.width // 2
954 zoom_out.rect.move_ip(MENU_LEFT, y)
955 widgets.append(zoom_out)
956
957 zoom_in = Button('Zoom in', action=self.level_widget.zoom_in)
958 zoom_in.rect = BUTTON_RECT.copy()
959 zoom_in.width = zoom_in.width // 2
960 zoom_in.rect.move_ip(MENU_LEFT + zoom_out.width, y)
961 widgets.append(zoom_in)
962
963 y = SCREEN[1] - MENU_BUTTON_HEIGHT - MENU_PAD
964 quit_but = Button('Quit', action=self.quit)
965 quit_but.rect = BUTTON_RECT.copy()
966 quit_but.rect.move_ip(MENU_LEFT, y)
967 widgets.append(quit_but)
968
969 self._dMenus['drawing'] = widgets
970
971 def _make_objects_menu(self):
972 widgets = []
973
974 # Add poly buttons
975 y = 15
976
977 edit_objs_but = Button('Edit Objects',
978 action=self.level_widget.edit_objects)
979 edit_objs_but.rect = BUTTON_RECT.copy()
980 edit_objs_but.rect.move_ip(MENU_LEFT, y)
981 widgets.append(edit_objs_but)
982 y += MENU_BUTTON_HEIGHT + MENU_PAD
983
984 edir_enemies_but = Button('Edit Enemies',
985 action=self.level_widget.edit_enemies)
986 edir_enemies_but.rect = BUTTON_RECT.copy()
987 edir_enemies_but.rect.move_ip(MENU_LEFT, y)
988 widgets.append(edir_enemies_but)
989 y += MENU_BUTTON_HEIGHT + MENU_PAD
990
991 add_obj_but = Button('Add Game Object',
992 action=self.level_widget.add_game_object)
993 add_obj_but.rect = BUTTON_RECT.copy()
994 add_obj_but.rect.move_ip(MENU_LEFT, y)
995 widgets.append(add_obj_but)
996 y += MENU_BUTTON_HEIGHT + MENU_PAD
997
998 add_puzzle_but = Button('Add Puzzler',
999 action=self.level_widget.add_puzzler)
1000 add_puzzle_but.rect = BUTTON_RECT.copy()
1001 add_puzzle_but.rect.move_ip(MENU_LEFT, y)
1002 widgets.append(add_puzzle_but)
1003 y += MENU_BUTTON_HEIGHT + MENU_PAD
1004
1005 add_enemy_but = Button('Add Enemy',
1006 action=self.level_widget.add_enemy)
1007 add_enemy_but.rect = BUTTON_RECT.copy()
1008 add_enemy_but.rect.move_ip(MENU_LEFT, y)
1009 widgets.append(add_enemy_but)
1010 y += MENU_BUTTON_HEIGHT + MENU_PAD
1011
1012 y += MENU_PAD
1013 self.sel_mode_but = HighLightButton('Select Object', self,
1014 action=self.sel_mode)
1015 self.sel_mode_but.rect = BUTTON_RECT.copy()
1016 self.sel_mode_but.rect.move_ip(MENU_LEFT, y)
1017 widgets.append(self.sel_mode_but)
1018 self._light_buttons.append(self.sel_mode_but)
1019 y += MENU_BUTTON_HEIGHT + MENU_PAD
1020
1021 y += MENU_PAD
1022 switch_but = Button('Switch to Drawing', action=self.switch_to_draw)
1023 switch_but.rect = BUTTON_RECT.copy()
1024 switch_but.rect.move_ip(MENU_LEFT, y)
1025 widgets.append(switch_but)
1026 y += switch_but.rect.height + MENU_PAD
1027
1028 save_but = Button('Save Level', action=self.save)
1029 save_but.rect = BUTTON_RECT.copy()
1030 save_but.rect.move_ip(MENU_LEFT, y)
1031 widgets.append(save_but)
1032 y += MENU_BUTTON_HEIGHT + MENU_PAD
1033
1034 zoom_out = Button('Zoom out', action=self.level_widget.zoom_out)
1035 zoom_out.rect = BUTTON_RECT.copy()
1036 zoom_out.rect.width = zoom_out.rect.width // 2
1037 zoom_out.rect.move_ip(MENU_LEFT, y)
1038 widgets.append(zoom_out)
1039
1040 zoom_in = Button('Zoom in', action=self.level_widget.zoom_in)
1041 zoom_in.rect = BUTTON_RECT.copy()
1042 zoom_in.width = zoom_in.width // 2
1043 zoom_in.rect.move_ip(MENU_LEFT + zoom_out.width, y)
1044 widgets.append(zoom_in)
1045 y += MENU_BUTTON_HEIGHT + MENU_PAD
1046
1047 y = SCREEN[1] - MENU_BUTTON_HEIGHT - MENU_PAD
1048 quit_but = Button('Quit', action=self.quit)
1049 quit_but.rect = BUTTON_RECT.copy()
1050 quit_but.rect.move_ip(MENU_LEFT, y)
1051 widgets.append(quit_but)
1052
1053 self._dMenus['objects'] = widgets
1054
1055 def key_down(self, ev):
1056 if ev.key == pgl.K_ESCAPE:
1057 self.quit()
1058 elif ev.key == pgl.K_s:
1059 self.save()
1060 else:
1061 self.level_widget.key_down(ev)
1062
1063 def save(self):
1064 closed, messages = self.level.all_closed()
1065 if closed:
1066 self.level.save()
1067 # display success
1068 alert("Level %s saved successfully." % self.level.name)
1069 else:
1070 # display errors
1071 alert("Failed to save level.\n\n%s" % '\n'.join(messages))
1072
1073 def switch_to_draw(self):
1074 if self._menu_mode != 'drawing':
1075 self._clear_menu()
1076 self._menu_mode = 'drawing'
1077 self._populate_menu()
1078
1079 def switch_to_objects(self):
1080 if self._menu_mode != 'objects':
1081 self._clear_menu()
1082 self._menu_mode = 'objects'
1083 self._populate_menu()
1084
1085 def _clear_menu(self):
1086 for widget in self._dMenus[self._menu_mode]:
1087 self.remove(widget)
1088
1089 def reset_lit_buttons(self):
1090 for but in self._light_buttons:
1091 but.reset()
1092
1093 def _populate_menu(self):
1094 self.level_widget.change_poly(None)
1095 self.level_widget.sel_mode = False
1096 for widget in self._dMenus[self._menu_mode]:
1097 self.add(widget)
1098 self.invalidate()
1099
1100 def set_line_mode(self):
1101 self.level_widget.line_mode()
1102 self.draw_line_but.highlight()
1103
1104 def sel_mode(self):
1105 self.level_widget.sel_mode = not self.level_widget.sel_mode
1106 if self.level_widget.sel_mode:
1107 self.sel_mode_but.highlight()
1108 else:
1109 self.sel_mode_but.reset()
1110
1111 def mouse_move(self, ev):
1112 self.level_widget.mouse_move(ev)
1113
1114 def move_point(self):
1115 self.level_widget.set_move_mode()
1116 self.move_point_but.highlight()
1117
1118 def draw(self, surface):
1119 # Update checkbox state
1120 if self._menu_mode == 'drawing':
1121 self.level_widget.set_objects(self.show_objs.value)
1122 self.level_widget.set_enemies(self.show_enemies.value)
1123 else:
1124 self.level_widget.set_objects(True)
1125 self.level_widget.set_enemies(True)
1126 super(EditorApp, self).draw(surface)
1127
1128
1129if __name__ == "__main__":
1130 if len(sys.argv) not in [2, 4]:
1131 print 'Please supply a levelname or levelname and level size'
1132 sys.exit()
1133 # Need to ensure we have defaults for rendering
1134 parse_args([])
1135 pygame.display.init()
1136 pygame.font.init()
1137 pygame.display.set_mode((SCREEN[0] + MENU_WIDTH, SCREEN[1]),
1138 pgl.SWSURFACE)
1139 if len(sys.argv) == 2:
1140 level = EditorLevel(sys.argv[1])
1141 level.load(pymunk.Space())
1142 elif len(sys.argv) == 4:
1143 level = EditorLevel(sys.argv[1], int(sys.argv[2]), int(sys.argv[3]))
1144 pygame.display.set_caption('Nagslang Area Editor')
1145 pygame.key.set_repeat(200, 100)
1146 app = EditorApp(level, pygame.display.get_surface())
1147 app.run()
Note: See TracBrowser for help on using the repository browser.