source: tools/area_editor.py@ 413:c5a3ed165df9

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

Partial zoom support

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