source: tools/area_editor.py@ 411:ddff1f8668d5

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

Unbreak polygon drawing

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