source: tools/area_editor.py@ 423:eb1a4a269d37

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

Tweak button layout

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