source: tools/area_editor.py@ 355:9589e1db4433

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

Allow copying polygon 6 into terrain objects

  • Property exe set to *
File size: 35.5 KB
Line 
1#!/usr/bin/env python
2
3# The basic area editor
4#
5# To edit an existing level, use
6# editor levelname
7#
8# To create a new level:
9#
10# editor levelname <xsize> <ysize>
11# (size specified in pixels
12#
13
14import os
15import sys
16
17import pygame
18import pygame.locals as pgl
19
20sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
21
22import pymunk
23
24from albow.root import RootWidget
25from albow.widget import Widget
26from albow.controls import Button, Label, CheckBox
27from albow.dialogs import alert, Dialog
28from albow.layout import Row
29from albow.fields import TextField
30from albow.table_view import TableView, TableColumn
31
32from nagslang.options import parse_args
33from nagslang.constants import SCREEN
34from nagslang.level import Level, POLY_COLORS, LINE_COLOR
35from nagslang.yamlish import load_s
36import nagslang.enemies as ne
37import nagslang.game_object as ngo
38import nagslang.puzzle as np
39
40# layout constants
41MENU_BUTTON_HEIGHT = 35
42MENU_PAD = 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
57
58class EditorLevel(Level):
59
60 def __init__(self, name, x=800, y=600):
61 world = TestWorld()
62 super(EditorLevel, self).__init__(name, world)
63 self.x = x
64 self.y = y
65 # Lookup initiliasition info from the objects
66 self.lookup = {}
67
68 def round_point(self, pos):
69 return (10 * (pos[0] // 10), 10 * (pos[1] // 10))
70
71 def load(self, space):
72 super(EditorLevel, self).load(space)
73 # Needed to fill in the lookup dict
74 self.reset_objs()
75
76 def point_to_pymunk(self, pos):
77 # inverse of point_to_pygame
78 # (this is also the same as point_to_pygame, but a additional
79 # function for sanity later in pyweek).
80 return (pos[0], self.y - pos[1])
81
82 def add_point(self, poly_index, pos):
83 self.polygons.setdefault(poly_index, [])
84 if not self.polygons[poly_index]:
85 point = self.point_to_pymunk(self.round_point(pos))
86 self.polygons[poly_index].append(point)
87 else:
88 add_pos = self.fix_poly_angle(poly_index, pos)
89 self.polygons[poly_index].append(add_pos)
90
91 def _fix_angle(self, point1, pos):
92 # We want the line (point1 to pos) to be an angle of
93 # 0, 45, 90, 135, 180, 225, 270, 305
94 # However, we only need to consider half the circle
95 # This is a hack to approximate the right thing
96 pos0 = (pos[0], point1[1])
97 pos90 = (point1[0], pos[1])
98 dist = max(abs(point1[0] - pos[0]), abs(point1[1] - pos[1]))
99 pos45 = (point1[0] + dist, point1[1] + dist)
100 pos135 = (point1[0] + dist, point1[1] - dist)
101 pos225 = (point1[0] - dist, point1[1] - dist)
102 pos305 = (point1[0] - dist, point1[1] + dist)
103 min_dist = 9999999
104 new_pos = point1
105 for cand in [pos0, pos90, pos45, pos135, pos225, pos305]:
106 dist = (pos[0] - cand[0]) ** 2 + (pos[1] - cand[1]) ** 2
107 if dist < min_dist:
108 new_pos = cand
109 min_dist = dist
110 return self.point_to_pymunk(new_pos)
111
112 def fix_line_angle(self, start_pos, pos):
113 start_pos = self.round_point(start_pos)
114 pos = self.round_point(pos)
115 return self._fix_angle(start_pos, pos)
116
117 def fix_poly_angle(self, index, pos):
118 # Last point
119 point1 = self.point_to_pygame(self.polygons[index][-1])
120 pos = self.round_point(pos)
121 return self._fix_angle(point1, pos)
122
123 def delete_point(self, index):
124 if index in self.polygons and len(self.polygons[index]) > 0:
125 self.polygons[index].pop()
126
127 def close_poly(self, index):
128 """Attempts to close the current polygon.
129
130 We allow a small additional step to close the polygon, but
131 it's limited as it's a magic point addition"""
132 if len(self.polygons[index]) < 2:
133 # Too small
134 return False
135 first = self.polygons[index][0]
136 if self.fix_poly_angle(index, self.point_to_pygame(first)) == first:
137 self.add_point(index, self.point_to_pygame(first))
138 return True
139 candidates = [(first[0] + 10 * i, first[1]) for
140 i in (-3, -2, -1, 1, 2, 3)]
141 candidates.extend([(first[0], first[1] + 10 * i) for
142 i in (-3, -2, -1, 1, 2, 3)])
143 candidates.extend([(first[0] + 10 * i, first[1] + 10 * i) for
144 i in (-3, -2, -1, 1, 2, 3)])
145 candidates.extend([(first[0] + 10 * i, first[1] - 10 * i) for
146 i in (-3, -2, -1, 1, 2, 3)])
147 min_dist = 99999
148 poss = None
149 for cand in candidates:
150 if self.fix_poly_angle(index, self.point_to_pygame(cand)) == cand:
151 dist = (first[0] - cand[0]) ** 2 + (first[1] - cand[1]) ** 2
152 if dist < min_dist:
153 poss = cand
154 if poss is not None:
155 self.add_point(index, self.point_to_pygame(poss))
156 self.add_point(index, self.point_to_pygame(first))
157 return True
158 return False
159
160 def add_line(self, start_pos, end_pos):
161 endpoint = self.fix_line_angle(start_pos, end_pos)
162 startpoint = self.point_to_pymunk(self.round_point(start_pos))
163 self.lines.append([startpoint, endpoint])
164
165 def draw(self, mouse_pos, mouse_poly, filled, draw_cand_line, start_pos):
166 self._draw_background(True)
167 # Draw polygons as needed for the editor
168 if filled:
169 self._draw_exterior(True)
170 for index, polygon in self.polygons.items():
171 color = POLY_COLORS[index]
172 if len(polygon) > 1:
173 pointlist = [self.point_to_pygame(p) for p in polygon]
174 pygame.draw.lines(self._surface, color, False, pointlist, 2)
175 if index == mouse_poly and mouse_pos:
176 endpoint = self.fix_poly_angle(index, mouse_pos)
177 pygame.draw.line(self._surface, color,
178 self.point_to_pygame(polygon[-1]),
179 self.point_to_pygame(endpoint))
180 for line in self.lines:
181 pointlist = [self.point_to_pygame(p) for p in line]
182 pygame.draw.lines(self._surface, LINE_COLOR, False, pointlist, 2)
183 if draw_cand_line and start_pos and mouse_pos:
184 endpoint = self.fix_line_angle(start_pos, mouse_pos)
185 pointlist = [self.round_point(start_pos),
186 self.point_to_pygame(endpoint)]
187 pygame.draw.lines(self._surface, LINE_COLOR, False, pointlist, 1)
188 return self._surface.copy()
189
190 def reset_objs(self):
191 # Reset the object state - needed when changing stuff
192 self.drawables = []
193 self.overlay_drawables = []
194 self._glue = np.PuzzleGlue()
195 for game_object_dict in self._game_objects:
196 obj = self._create_game_object(pymunk.Space(), **game_object_dict)
197 self.lookup[obj] = game_object_dict
198 for enemy_dict in self._enemies:
199 obj = self._create_enemy(pymunk.Space(), **enemy_dict)
200 self.lookup[obj] = enemy_dict
201
202 def get_class(self, classname, mod=None):
203 # Get the class given the classname
204 modules = {
205 'game_object': ngo,
206 'enemies': ne,
207 'puzzle': np,
208 }
209 if '.' in classname:
210 modname, classname = classname.split('.')
211 mod = modules[modname]
212 if mod is None:
213 mod = ngo
214 return getattr(mod, classname)
215
216 def try_new_object(self, classname, target, new, old=None):
217 if old in target:
218 target.remove(old)
219 try:
220 target.append(new)
221 self.reset_objs()
222 return True
223 except Exception as e:
224 target.remove(new)
225 if old is not None:
226 target.append(old)
227 self.reset_objs()
228 alert("Failed to update object %s: %s" % (classname, e))
229 return False
230
231 def find_obj_at_pos(self, mouse_pos):
232 pymunk_pos = self.point_to_pymunk(mouse_pos)
233 # Search visible objects
234 for obj in self.drawables:
235 if obj.get_shape().point_query(pymunk_pos):
236 return obj
237 return None
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.sel_mode = False
400 self._start_pos = None
401 self._parent = parent
402
403 def _level_coordinates(self, pos):
404 # Move positions to level values
405 if not pos:
406 return (0, 0)
407 return pos[0] + self.pos[0], pos[1] + self.pos[1]
408
409 def _move_view(self, offset):
410 new_pos = [self.pos[0] + offset[0], self.pos[1] + offset[1]]
411 if new_pos[0] < 0:
412 new_pos[0] = self.pos[0]
413 elif new_pos[0] > self.level.x - SCREEN[0]:
414 new_pos[0] = self.pos[0]
415 if new_pos[1] < 0:
416 new_pos[1] = self.pos[1]
417 elif new_pos[1] > self.level.y - SCREEN[1]:
418 new_pos[1] = self.pos[1]
419 self.pos = tuple(new_pos)
420
421 def set_objects(self, value):
422 if self._draw_objects != value:
423 self._draw_objects = value
424 self.invalidate()
425
426 def set_enemies(self, value):
427 if self._draw_enemies != value:
428 self._draw_enemies = value
429 self.invalidate()
430
431 def draw(self, surface):
432 if (self.cur_poly is not None and self.cur_poly in self.level.polygons
433 and len(self.level.polygons[self.cur_poly])):
434 # We have an active polygon
435 mouse_pos = self._level_coordinates(self.mouse_pos)
436 elif self._draw_lines:
437 # Interior wall mode
438 mouse_pos = self._level_coordinates(self.mouse_pos)
439 else:
440 mouse_pos = None
441 level_surface = level.draw(mouse_pos, self.cur_poly, self.filled_mode,
442 self._draw_lines, self._start_pos)
443 if self._draw_objects:
444 for thing in self.level.drawables:
445 if not isinstance(thing, ne.Enemy):
446 thing.render(level_surface)
447 if self._draw_enemies:
448 for thing in self.level.drawables:
449 if isinstance(thing, ne.Enemy):
450 thing.render(level_surface)
451 surface_area = pygame.rect.Rect(self.pos, SCREEN)
452 surface.blit(level_surface, (0, 0), surface_area)
453
454 def change_poly(self, new_poly):
455 self.cur_poly = new_poly
456 self._draw_lines = False
457 if self.cur_poly is not None:
458 self._parent.reset_lit_buttons()
459 self.filled_mode = False
460
461 def line_mode(self):
462 self.cur_poly = None
463 self._parent.reset_lit_buttons()
464 self._draw_lines = True
465 self.filled_mode = False
466 self._start_pos = None
467
468 def key_down(self, ev):
469 if ev.key == pgl.K_LEFT:
470 self._move_view((-10, 0))
471 elif ev.key == pgl.K_RIGHT:
472 self._move_view((10, 0))
473 elif ev.key == pgl.K_UP:
474 self._move_view((0, -10))
475 elif ev.key == pgl.K_DOWN:
476 self._move_view((0, 10))
477 elif ev.key in (pgl.K_1, pgl.K_2, pgl.K_3, pgl.K_4, pgl.K_5, pgl.K_6):
478 self.change_poly(ev.key - pgl.K_0)
479 elif ev.key == pgl.K_0:
480 self.change_poly(None)
481 elif ev.key == pgl.K_d and self.cur_poly:
482 self.level.delete_point(self.cur_poly)
483 elif ev.key == pgl.K_f:
484 self.set_filled()
485 elif ev.key == pgl.K_c:
486 self.close_poly()
487
488 def set_filled(self):
489 closed, _ = self.level.all_closed()
490 if closed:
491 self.cur_poly = None
492 self._parent.reset_lit_buttons()
493 self.filled_mode = True
494 self._draw_lines = False
495 else:
496 alert('Not all polygons closed, so not filling')
497
498 def mouse_move(self, ev):
499 old_pos = self.mouse_pos
500 self.mouse_pos = ev.pos
501 if old_pos != self.mouse_pos and (self.cur_poly or self._draw_lines):
502 self.invalidate()
503
504 def mouse_drag(self, ev):
505 if self._mouse_drag:
506 old_pos = self.mouse_pos
507 self.mouse_pos = ev.pos
508 diff = (-self.mouse_pos[0] + old_pos[0],
509 -self.mouse_pos[1] + old_pos[1])
510 self._move_view(diff)
511 self.invalidate()
512
513 def mouse_down(self, ev):
514 if self.sel_mode and ev.button == 1:
515 corrected_pos = ev.pos[0] + self.pos[0], ev.pos[1] + self.pos[1]
516 obj = self.level.find_obj_at_pos(corrected_pos)
517 if obj is not None:
518 self._edit_selected(obj)
519 elif ev.button == 1:
520 if self._draw_lines:
521 if self._start_pos is None:
522 self._start_pos = ev.pos
523 else:
524 self.level.add_line(self._start_pos, ev.pos)
525 self._start_pos = None
526 else:
527 print "Click: %r" % (
528 self.level.point_to_pymunk(
529 self._level_coordinates(ev.pos)),)
530 if ev.button == 4: # Scroll up
531 self._move_view((0, -10))
532 elif ev.button == 5: # Scroll down
533 self._move_view((0, 10))
534 elif ev.button == 6: # Scroll left
535 self._move_view((-10, 0))
536 elif ev.button == 7: # Scroll right
537 self._move_view((10, 0))
538 elif self.cur_poly and ev.button == 1:
539 # Add a point
540 self.level.add_point(self.cur_poly,
541 self._level_coordinates(ev.pos))
542 elif ev.button == 3:
543 self._mouse_drag = True
544
545 def mouse_up(self, ev):
546 if ev.button == 3:
547 self._mouse_drag = False
548
549 def close_poly(self):
550 if self.cur_poly is None:
551 return
552 if self.level.close_poly(self.cur_poly):
553 alert("Successfully closed the polygon")
554 self.change_poly(None)
555 else:
556 alert("Failed to close the polygon")
557
558 def _edit_class(self, classname, cls, data):
559 # Dialog for class properties
560 dialog = EditClassDialog(classname, cls, data, self)
561 if dialog.present() == 'OK':
562 return dialog
563 return None
564
565 def _edit_selected(self, obj):
566 data = self.level.lookup[obj]
567 cls = obj.__class__
568 classname = obj.__class__.__name__
569 dialog = EditClassDialog(classname, cls, data, self, True)
570 res = dialog.present()
571 if res == 'OK':
572 edited = dialog.get_data()
573 if edited is not None:
574 for target in [self.level._game_objects, self.level._enemies]:
575 if data in target:
576 if self.level.try_new_object(classname, target,
577 edited, data):
578 dialog.cleanup()
579 break
580 elif res == 'Delete':
581 for target in [self.level._game_objects, self.level._enemies]:
582 if data in target:
583 target.remove(data)
584 self.level.reset_objs()
585 break
586
587 def _make_edit_dialog(self, entries):
588 # Dialog to hold the editor
589 edit_box = Dialog()
590 edit_box.rect = pygame.rect.Rect(0, 0, 700, 500)
591 table = ObjectTable(entries)
592 edit_box.add(table)
593 buttons = []
594 for text in ['OK', 'Delete', 'Cancel']:
595 but = Button(text, action=lambda x=text: edit_box.dismiss(x))
596 buttons.append(but)
597 row = Row(buttons)
598 row.rect = pygame.rect.Rect(250, 450, 700, 50)
599 edit_box.add(row)
600 edit_box.get_selection = lambda: table.get_selection()
601 return edit_box
602
603 def edit_objects(self):
604 edit_box = self._make_edit_dialog(self.level._game_objects)
605 res = edit_box.present()
606 choice = edit_box.get_selection()
607 if choice is None:
608 return
609 if res == 'OK':
610 cls = self.level.get_class(choice['classname'])
611 edit_dlg = self._edit_class(choice['classname'], cls, choice)
612 if edit_dlg is not None:
613 edited = edit_dlg.get_data()
614 if self.level.try_new_object(choice["classname"],
615 self.level._game_objects,
616 edited, choice):
617 edit_dlg.cleanup()
618 elif res == 'Delete':
619 self.level._game_objects.remove(choice)
620 self.level.reset_objs()
621
622 def edit_enemies(self):
623 edit_box = self._make_edit_dialog(self.level._enemies)
624 res = edit_box.present()
625 choice = edit_box.get_selection()
626 if choice is None:
627 return
628 if res == 'OK':
629 cls = self.level.get_class(choice['classname'], ne)
630 edit_dlg = self._edit_class(choice['classname'], cls, choice)
631 if edit_dlg is not None:
632 edited = edit_dlg.get_data()
633 if self.level.try_new_object(choice["classname"],
634 self.level._enemies,
635 edited, choice):
636 edit_dlg.cleanup()
637 elif res == 'Delete':
638 self.level._enemies.remove(choice)
639 self.level.reset_objs()
640
641 def _make_choice_dialog(self, classes):
642 # Dialog to hold the editor
643 data = []
644 for cls_name, cls in classes:
645 data.append({"classname": cls_name, "class": cls})
646 choice_box = Dialog()
647 choice_box.rect = pygame.rect.Rect(0, 0, 700, 500)
648 table = ObjectTable(data)
649 choice_box.add(table)
650 buttons = []
651 for text in ['OK', 'Cancel']:
652 but = Button(text, action=lambda x=text: choice_box.dismiss(x))
653 buttons.append(but)
654 row = Row(buttons)
655 row.rect = pygame.rect.Rect(250, 450, 700, 50)
656 choice_box.add(row)
657 choice_box.get_selection = lambda: table.get_selection()
658 return choice_box
659
660 def add_game_object(self):
661 classes = ngo.get_editable_game_objects()
662 choose = self._make_choice_dialog(classes)
663 res = choose.present()
664 choice = choose.get_selection()
665 if res == 'OK' and choice is not None:
666 classname = choice['classname']
667 cls = choice['class']
668 edit_dlg = self._edit_class(classname, cls, None)
669 if edit_dlg is not None:
670 new_cls = edit_dlg.get_data()
671 if self.level.try_new_object(classname,
672 self.level._game_objects,
673 new_cls, None):
674 edit_dlg.cleanup()
675
676 def add_enemy(self):
677 classes = ne.get_editable_enemies()
678 choose = self._make_choice_dialog(classes)
679 res = choose.present()
680 choice = choose.get_selection()
681 if res == 'OK' and choice is not None:
682 classname = choice['classname']
683 cls = choice['class']
684 edit_dlg = self._edit_class(classname, cls, None)
685 if edit_dlg is not None:
686 new_cls = edit_dlg.get_data()
687 if self.level.try_new_object(classname, self.level._enemies,
688 new_cls, None):
689 edit_dlg.cleanup()
690
691 def add_puzzler(self):
692 classes = np.get_editable_puzzlers()
693 choose = self._make_choice_dialog(classes)
694 res = choose.present()
695 choice = choose.get_selection()
696 if res == 'OK' and choice is not None:
697 classname = choice['classname']
698 cls = choice['class']
699 edit_dlg = self._edit_class(classname, cls, None)
700 if edit_dlg is not None:
701 new_cls = edit_dlg.get_data()
702 if self.level.try_new_object(classname,
703 self.level._game_objects,
704 new_cls, None):
705 edit_dlg.cleanup()
706
707
708class HighLightButton(Button):
709 """Button with highlight support"""
710 def __init__(self, text, parent, **kwds):
711 super(HighLightButton, self).__init__(text, **kwds)
712 self._parent = parent
713
714 def highlight(self):
715 self.border_color = pygame.color.Color('red')
716
717 def reset(self):
718 self.border_color = self.fg_color
719
720
721class PolyButton(HighLightButton):
722 """Button for coosing the correct polygon"""
723
724 def __init__(self, index, level_widget, parent):
725 if index is not None:
726 text = "Draw: %s" % index
727 else:
728 text = 'Exit Draw Mode'
729 super(PolyButton, self).__init__(text, parent)
730 self.index = index
731 self.level_widget = level_widget
732
733 def action(self):
734 self.level_widget.change_poly(self.index)
735 self._parent.reset_lit_buttons()
736 if self.index is not None:
737 self.highlight()
738
739
740class EditorApp(RootWidget):
741
742 def __init__(self, level, surface):
743 super(EditorApp, self).__init__(surface)
744 self.level = level
745 self.level_widget = LevelWidget(self.level, self)
746 self.add(self.level_widget)
747
748 self._dMenus = {}
749
750 self._light_buttons = []
751
752 self._make_draw_menu()
753 self._make_objects_menu()
754
755 self._menu_mode = 'drawing'
756 self._populate_menu()
757
758 def _make_draw_menu(self):
759 widgets = []
760
761 # Add poly buttons
762 y = 15
763 for poly in range(1, 7):
764 but = PolyButton(poly, self.level_widget, self)
765 but.rect = pygame.rect.Rect(0, 0, MENU_WIDTH // 2 - MENU_PAD,
766 MENU_BUTTON_HEIGHT)
767 if poly % 2:
768 but.rect.move_ip(MENU_LEFT, y)
769 else:
770 but.rect.move_ip(MENU_LEFT + MENU_WIDTH // 2 - MENU_HALF_PAD,
771 y)
772 y += MENU_BUTTON_HEIGHT + MENU_PAD
773 self._light_buttons.append(but)
774 widgets.append(but)
775
776 end_poly_but = PolyButton(None, self.level_widget, self)
777 end_poly_but.rect = BUTTON_RECT.copy()
778 end_poly_but.rect.move_ip(MENU_LEFT, y)
779 widgets.append(end_poly_but)
780 y += MENU_BUTTON_HEIGHT + MENU_PAD
781
782 self.draw_line_but = HighLightButton("Draw interior wall", self,
783 action=self.set_line_mode)
784 self.draw_line_but.rect = BUTTON_RECT.copy()
785 self.draw_line_but.rect.move_ip(MENU_LEFT, y)
786 widgets.append(self.draw_line_but)
787 self._light_buttons.append(self.draw_line_but)
788 y += MENU_BUTTON_HEIGHT + MENU_PAD
789
790 fill_but = Button('Fill exterior', action=self.level_widget.set_filled)
791 fill_but.rect = BUTTON_RECT.copy()
792 fill_but.rect.move_ip(MENU_LEFT, y)
793 widgets.append(fill_but)
794 y += MENU_BUTTON_HEIGHT + MENU_PAD
795
796 close_poly_but = Button('Close Polygon',
797 action=self.level_widget.close_poly)
798 close_poly_but.rect = BUTTON_RECT.copy()
799 close_poly_but.rect.move_ip(MENU_LEFT, y)
800 widgets.append(close_poly_but)
801 y += MENU_BUTTON_HEIGHT + MENU_PAD
802
803 white = pygame.color.Color("white")
804 self.show_objs = CheckBox(fg_color=white)
805 self.show_objs.rect = CHECK_RECT.copy()
806 self.show_objs.rect.move_ip(MENU_LEFT, y)
807 label = Label("Show Objects", fg_color=white)
808 label.rect.move_ip(MENU_LEFT + MENU_BUTTON_HEIGHT // 2 + MENU_PAD, y)
809 widgets.append(self.show_objs)
810 widgets.append(label)
811 y += label.rect.height + MENU_PAD
812
813 self.show_enemies = CheckBox(fg_color=white)
814 self.show_enemies.rect = CHECK_RECT.copy()
815 self.show_enemies.rect.move_ip(MENU_LEFT, y)
816 label = Label("Show enemy start pos", fg_color=white)
817 label.rect.move_ip(MENU_LEFT + MENU_BUTTON_HEIGHT // 2 + MENU_PAD, y)
818 widgets.append(self.show_enemies)
819 widgets.append(label)
820 y += label.rect.height + MENU_PAD
821
822 y += MENU_PAD
823 switch_but = Button('Switch to Objects', action=self.switch_to_objects)
824 switch_but.rect = BUTTON_RECT.copy()
825 switch_but.rect.move_ip(MENU_LEFT, y)
826 widgets.append(switch_but)
827 y += switch_but.rect.height + MENU_PAD
828
829 save_but = Button('Save Level', action=self.save)
830 save_but.rect = BUTTON_RECT.copy()
831 save_but.rect.move_ip(MENU_LEFT, y)
832 widgets.append(save_but)
833 y += MENU_BUTTON_HEIGHT + MENU_PAD
834
835 y = SCREEN[1] - MENU_BUTTON_HEIGHT - MENU_PAD
836 quit_but = Button('Quit', action=self.quit)
837 quit_but.rect = BUTTON_RECT.copy()
838 quit_but.rect.move_ip(MENU_LEFT, y)
839 widgets.append(quit_but)
840
841 self._dMenus['drawing'] = widgets
842
843 def _make_objects_menu(self):
844 widgets = []
845
846 # Add poly buttons
847 y = 15
848
849 edit_objs_but = Button('Edit Objects',
850 action=self.level_widget.edit_objects)
851 edit_objs_but.rect = BUTTON_RECT.copy()
852 edit_objs_but.rect.move_ip(MENU_LEFT, y)
853 widgets.append(edit_objs_but)
854 y += MENU_BUTTON_HEIGHT + MENU_PAD
855
856 edir_enemies_but = Button('Edit Enemies',
857 action=self.level_widget.edit_enemies)
858 edir_enemies_but.rect = BUTTON_RECT.copy()
859 edir_enemies_but.rect.move_ip(MENU_LEFT, y)
860 widgets.append(edir_enemies_but)
861 y += MENU_BUTTON_HEIGHT + MENU_PAD
862
863 add_obj_but = Button('Add Game Object',
864 action=self.level_widget.add_game_object)
865 add_obj_but.rect = BUTTON_RECT.copy()
866 add_obj_but.rect.move_ip(MENU_LEFT, y)
867 widgets.append(add_obj_but)
868 y += MENU_BUTTON_HEIGHT + MENU_PAD
869
870 add_puzzle_but = Button('Add Puzzler',
871 action=self.level_widget.add_puzzler)
872 add_puzzle_but.rect = BUTTON_RECT.copy()
873 add_puzzle_but.rect.move_ip(MENU_LEFT, y)
874 widgets.append(add_puzzle_but)
875 y += MENU_BUTTON_HEIGHT + MENU_PAD
876
877 add_enemy_but = Button('Add Enemy',
878 action=self.level_widget.add_enemy)
879 add_enemy_but.rect = BUTTON_RECT.copy()
880 add_enemy_but.rect.move_ip(MENU_LEFT, y)
881 widgets.append(add_enemy_but)
882 y += MENU_BUTTON_HEIGHT + MENU_PAD
883
884 y += MENU_PAD
885 self.sel_mode_but = HighLightButton('Select Object', self,
886 action=self.sel_mode)
887 self.sel_mode_but.rect = BUTTON_RECT.copy()
888 self.sel_mode_but.rect.move_ip(MENU_LEFT, y)
889 widgets.append(self.sel_mode_but)
890 self._light_buttons.append(self.sel_mode_but)
891 y += MENU_BUTTON_HEIGHT + MENU_PAD
892
893 y += MENU_PAD
894 switch_but = Button('Switch to Drawing', action=self.switch_to_draw)
895 switch_but.rect = BUTTON_RECT.copy()
896 switch_but.rect.move_ip(MENU_LEFT, y)
897 widgets.append(switch_but)
898 y += switch_but.rect.height + MENU_PAD
899
900 save_but = Button('Save Level', action=self.save)
901 save_but.rect = BUTTON_RECT.copy()
902 save_but.rect.move_ip(MENU_LEFT, y)
903 widgets.append(save_but)
904 y += MENU_BUTTON_HEIGHT + MENU_PAD
905
906 y = SCREEN[1] - MENU_BUTTON_HEIGHT - MENU_PAD
907 quit_but = Button('Quit', action=self.quit)
908 quit_but.rect = BUTTON_RECT.copy()
909 quit_but.rect.move_ip(MENU_LEFT, y)
910 widgets.append(quit_but)
911
912 self._dMenus['objects'] = widgets
913
914 def key_down(self, ev):
915 if ev.key == pgl.K_ESCAPE:
916 self.quit()
917 elif ev.key == pgl.K_s:
918 self.save()
919 else:
920 self.level_widget.key_down(ev)
921
922 def save(self):
923 closed, messages = self.level.all_closed()
924 if closed:
925 self.level.save()
926 # display success
927 alert("Level %s saved successfully." % self.level.name)
928 else:
929 # display errors
930 alert("Failed to save level.\n\n%s" % '\n'.join(messages))
931
932 def switch_to_draw(self):
933 if self._menu_mode != 'drawing':
934 self._clear_menu()
935 self._menu_mode = 'drawing'
936 self._populate_menu()
937
938 def switch_to_objects(self):
939 if self._menu_mode != 'objects':
940 self._clear_menu()
941 self._menu_mode = 'objects'
942 self._populate_menu()
943
944 def _clear_menu(self):
945 for widget in self._dMenus[self._menu_mode]:
946 self.remove(widget)
947
948 def reset_lit_buttons(self):
949 for but in self._light_buttons:
950 but.reset()
951
952 def _populate_menu(self):
953 self.level_widget.change_poly(None)
954 self.level_widget.sel_mode = False
955 for widget in self._dMenus[self._menu_mode]:
956 self.add(widget)
957 self.invalidate()
958
959 def set_line_mode(self):
960 self.level_widget.line_mode()
961 self.draw_line_but.highlight()
962
963 def sel_mode(self):
964 self.level_widget.sel_mode = not self.level_widget.sel_mode
965 if self.level_widget.sel_mode:
966 self.sel_mode_but.highlight()
967 else:
968 self.sel_mode_but.reset()
969
970 def mouse_move(self, ev):
971 self.level_widget.mouse_move(ev)
972
973 def draw(self, surface):
974 # Update checkbox state
975 if self._menu_mode == 'drawing':
976 self.level_widget.set_objects(self.show_objs.value)
977 self.level_widget.set_enemies(self.show_enemies.value)
978 else:
979 self.level_widget.set_objects(True)
980 self.level_widget.set_enemies(True)
981 super(EditorApp, self).draw(surface)
982
983
984if __name__ == "__main__":
985 if len(sys.argv) not in [2, 4]:
986 print 'Please supply a levelname or levelname and level size'
987 sys.exit()
988 # Need to ensure we have defaults for rendering
989 parse_args([])
990 pygame.display.init()
991 pygame.font.init()
992 pygame.display.set_mode((SCREEN[0] + MENU_WIDTH, SCREEN[1]),
993 pgl.SWSURFACE)
994 if len(sys.argv) == 2:
995 level = EditorLevel(sys.argv[1])
996 level.load(pymunk.Space())
997 elif len(sys.argv) == 4:
998 level = EditorLevel(sys.argv[1], int(sys.argv[2]), int(sys.argv[3]))
999 pygame.display.set_caption('Nagslang Area Editor')
1000 pygame.key.set_repeat(200, 100)
1001 app = EditorApp(level, pygame.display.get_surface())
1002 app.run()
Note: See TracBrowser for help on using the repository browser.