source: tools/area_editor.py@ 407:314ddad2d6d2

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

Move points button

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