source: tools/area_editor.py@ 414:060420389033

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

more zooming

  • Property exe set to *
File size: 40.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 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 = self._level_coordinates(ev.pos)
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 = corrected_pos
619 else:
620 self.level.add_line(self._start_pos, corrected_pos)
621 self._start_pos = None
622 else:
623 print "Click: %r" % (
624 self.level.point_to_pymunk(corrected_pos),)
625 if ev.button == 4: # Scroll up
626 self._move_view((0, -10))
627 elif ev.button == 5: # Scroll down
628 self._move_view((0, 10))
629 elif ev.button == 6: # Scroll left
630 self._move_view((-10, 0))
631 elif ev.button == 7: # Scroll right
632 self._move_view((10, 0))
633 elif self.cur_poly and ev.button == 1:
634 # Add a point
635 self.level.add_point(self.cur_poly, corrected_pos)
636 elif ev.button == 3:
637 self._mouse_drag = True
638
639 def mouse_up(self, ev):
640 if ev.button == 3:
641 self._mouse_drag = False
642
643 def close_poly(self):
644 if self.cur_poly is None:
645 return
646 if self.level.close_poly(self.cur_poly):
647 alert("Successfully closed the polygon")
648 self.change_poly(None)
649 else:
650 alert("Failed to close the polygon")
651
652 def _edit_class(self, classname, cls, data):
653 # Dialog for class properties
654 dialog = EditClassDialog(classname, cls, data, self)
655 if dialog.present() == 'OK':
656 return dialog
657 return None
658
659 def _edit_selected(self, obj):
660 data = self.level.lookup[obj]
661 cls = obj.__class__
662 classname = obj.__class__.__name__
663 dialog = EditClassDialog(classname, cls, data, self, True)
664 res = dialog.present()
665 if res == 'OK':
666 edited = dialog.get_data()
667 if edited is not None:
668 for target in [self.level._game_objects, self.level._enemies]:
669 if data in target:
670 if self.level.try_new_object(classname, target,
671 edited, data):
672 dialog.cleanup()
673 break
674 elif res == 'Delete':
675 for target in [self.level._game_objects, self.level._enemies]:
676 if data in target:
677 target.remove(data)
678 self.level.reset_objs()
679 break
680
681 def _make_edit_dialog(self, entries):
682 # Dialog to hold the editor
683 edit_box = Dialog()
684 edit_box.rect = pygame.rect.Rect(0, 0, 700, 500)
685 table = ObjectTable(entries)
686 edit_box.add(table)
687 buttons = []
688 for text in ['OK', 'Delete', 'Cancel']:
689 but = Button(text, action=lambda x=text: edit_box.dismiss(x))
690 buttons.append(but)
691 row = Row(buttons)
692 row.rect = pygame.rect.Rect(250, 450, 700, 50)
693 edit_box.add(row)
694 edit_box.get_selection = lambda: table.get_selection()
695 return edit_box
696
697 def edit_objects(self):
698 edit_box = self._make_edit_dialog(self.level._game_objects)
699 res = edit_box.present()
700 choice = edit_box.get_selection()
701 if choice is None:
702 return
703 if res == 'OK':
704 cls = self.level.get_class(choice['classname'])
705 edit_dlg = self._edit_class(choice['classname'], cls, choice)
706 if edit_dlg is not None:
707 edited = edit_dlg.get_data()
708 if self.level.try_new_object(choice["classname"],
709 self.level._game_objects,
710 edited, choice):
711 edit_dlg.cleanup()
712 elif res == 'Delete':
713 self.level._game_objects.remove(choice)
714 self.level.reset_objs()
715
716 def edit_enemies(self):
717 edit_box = self._make_edit_dialog(self.level._enemies)
718 res = edit_box.present()
719 choice = edit_box.get_selection()
720 if choice is None:
721 return
722 if res == 'OK':
723 cls = self.level.get_class(choice['classname'], ne)
724 edit_dlg = self._edit_class(choice['classname'], cls, choice)
725 if edit_dlg is not None:
726 edited = edit_dlg.get_data()
727 if self.level.try_new_object(choice["classname"],
728 self.level._enemies,
729 edited, choice):
730 edit_dlg.cleanup()
731 elif res == 'Delete':
732 self.level._enemies.remove(choice)
733 self.level.reset_objs()
734
735 def _make_choice_dialog(self, classes):
736 # Dialog to hold the editor
737 data = []
738 for cls_name, cls in classes:
739 data.append({"classname": cls_name, "class": cls})
740 choice_box = Dialog()
741 choice_box.rect = pygame.rect.Rect(0, 0, 700, 500)
742 table = ObjectTable(data)
743 choice_box.add(table)
744 buttons = []
745 for text in ['OK', 'Cancel']:
746 but = Button(text, action=lambda x=text: choice_box.dismiss(x))
747 buttons.append(but)
748 row = Row(buttons)
749 row.rect = pygame.rect.Rect(250, 450, 700, 50)
750 choice_box.add(row)
751 choice_box.get_selection = lambda: table.get_selection()
752 return choice_box
753
754 def add_game_object(self):
755 classes = ngo.get_editable_game_objects()
756 choose = self._make_choice_dialog(classes)
757 res = choose.present()
758 choice = choose.get_selection()
759 if res == 'OK' and choice is not None:
760 classname = choice['classname']
761 cls = choice['class']
762 edit_dlg = self._edit_class(classname, cls, None)
763 if edit_dlg is not None:
764 new_cls = edit_dlg.get_data()
765 if self.level.try_new_object(classname,
766 self.level._game_objects,
767 new_cls, None):
768 edit_dlg.cleanup()
769
770 def add_enemy(self):
771 classes = ne.get_editable_enemies()
772 choose = self._make_choice_dialog(classes)
773 res = choose.present()
774 choice = choose.get_selection()
775 if res == 'OK' and choice is not None:
776 classname = choice['classname']
777 cls = choice['class']
778 edit_dlg = self._edit_class(classname, cls, None)
779 if edit_dlg is not None:
780 new_cls = edit_dlg.get_data()
781 if self.level.try_new_object(classname, self.level._enemies,
782 new_cls, None):
783 edit_dlg.cleanup()
784
785 def add_puzzler(self):
786 classes = np.get_editable_puzzlers()
787 choose = self._make_choice_dialog(classes)
788 res = choose.present()
789 choice = choose.get_selection()
790 if res == 'OK' and choice is not None:
791 classname = choice['classname']
792 cls = choice['class']
793 edit_dlg = self._edit_class(classname, cls, None)
794 if edit_dlg is not None:
795 new_cls = edit_dlg.get_data()
796 if self.level.try_new_object(classname,
797 self.level._game_objects,
798 new_cls, None):
799 edit_dlg.cleanup()
800
801
802class HighLightButton(Button):
803 """Button with highlight support"""
804 def __init__(self, text, parent, **kwds):
805 super(HighLightButton, self).__init__(text, **kwds)
806 self._parent = parent
807
808 def highlight(self):
809 self.border_color = pygame.color.Color('red')
810
811 def reset(self):
812 self.border_color = self.fg_color
813
814
815class PolyButton(HighLightButton):
816 """Button for coosing the correct polygon"""
817
818 def __init__(self, index, level_widget, parent):
819 if index is not None:
820 text = "Draw: %s" % index
821 else:
822 text = 'Exit Draw Mode'
823 super(PolyButton, self).__init__(text, parent)
824 self.index = index
825 self.level_widget = level_widget
826
827 def action(self):
828 self.level_widget.change_poly(self.index)
829 self._parent.reset_lit_buttons()
830 if self.index is not None:
831 self.highlight()
832
833
834class EditorApp(RootWidget):
835
836 def __init__(self, level, surface):
837 super(EditorApp, self).__init__(surface)
838 self.level = level
839 self.level_widget = LevelWidget(self.level, self)
840 self.add(self.level_widget)
841
842 self._dMenus = {}
843
844 self._light_buttons = []
845
846 self._make_draw_menu()
847 self._make_objects_menu()
848
849 self._menu_mode = 'drawing'
850 self._populate_menu()
851
852 self._zoom = 1
853
854 def _make_draw_menu(self):
855 widgets = []
856
857 # Add poly buttons
858 y = 15
859 for poly in range(1, 7):
860 but = PolyButton(poly, self.level_widget, self)
861 but.rect = pygame.rect.Rect(0, 0, MENU_WIDTH // 2 - MENU_PAD,
862 MENU_BUTTON_HEIGHT)
863 if poly % 2:
864 but.rect.move_ip(MENU_LEFT, y)
865 else:
866 but.rect.move_ip(MENU_LEFT + MENU_WIDTH // 2 - MENU_HALF_PAD,
867 y)
868 y += MENU_BUTTON_HEIGHT + MENU_PAD
869 self._light_buttons.append(but)
870 widgets.append(but)
871
872 end_poly_but = PolyButton(None, self.level_widget, self)
873 end_poly_but.rect = BUTTON_RECT.copy()
874 end_poly_but.rect.move_ip(MENU_LEFT, y)
875 widgets.append(end_poly_but)
876 y += MENU_BUTTON_HEIGHT + MENU_PAD
877
878 self.move_point_but = HighLightButton("Move Point", self,
879 action=self.move_point)
880 self.move_point_but.rect = BUTTON_RECT.copy()
881 self.move_point_but.rect.move_ip(MENU_LEFT, y)
882 widgets.append(self.move_point_but)
883 self._light_buttons.append(self.move_point_but)
884 y += MENU_BUTTON_HEIGHT + MENU_PAD
885
886 self.draw_line_but = HighLightButton("Draw interior wall", self,
887 action=self.set_line_mode)
888 self.draw_line_but.rect = BUTTON_RECT.copy()
889 self.draw_line_but.rect.move_ip(MENU_LEFT, y)
890 widgets.append(self.draw_line_but)
891 self._light_buttons.append(self.draw_line_but)
892 y += MENU_BUTTON_HEIGHT + MENU_PAD
893
894 fill_but = Button('Fill exterior', action=self.level_widget.set_filled)
895 fill_but.rect = BUTTON_RECT.copy()
896 fill_but.rect.move_ip(MENU_LEFT, y)
897 widgets.append(fill_but)
898 y += MENU_BUTTON_HEIGHT + MENU_PAD
899
900 close_poly_but = Button('Close Polygon',
901 action=self.level_widget.close_poly)
902 close_poly_but.rect = BUTTON_RECT.copy()
903 close_poly_but.rect.move_ip(MENU_LEFT, y)
904 widgets.append(close_poly_but)
905 y += MENU_BUTTON_HEIGHT + MENU_PAD
906
907 white = pygame.color.Color("white")
908 self.show_objs = CheckBox(fg_color=white)
909 self.show_objs.rect = CHECK_RECT.copy()
910 self.show_objs.rect.move_ip(MENU_LEFT, y)
911 label = Label("Show Objects", fg_color=white)
912 label.rect.move_ip(MENU_LEFT + MENU_BUTTON_HEIGHT // 2 + MENU_PAD, y)
913 widgets.append(self.show_objs)
914 widgets.append(label)
915 y += label.rect.height + MENU_PAD
916
917 self.show_enemies = CheckBox(fg_color=white)
918 self.show_enemies.rect = CHECK_RECT.copy()
919 self.show_enemies.rect.move_ip(MENU_LEFT, y)
920 label = Label("Show enemy start pos", fg_color=white)
921 label.rect.move_ip(MENU_LEFT + MENU_BUTTON_HEIGHT // 2 + MENU_PAD, y)
922 widgets.append(self.show_enemies)
923 widgets.append(label)
924 y += label.rect.height + MENU_PAD
925
926 y += MENU_PAD
927 switch_but = Button('Switch to Objects', action=self.switch_to_objects)
928 switch_but.rect = BUTTON_RECT.copy()
929 switch_but.rect.move_ip(MENU_LEFT, y)
930 widgets.append(switch_but)
931 y += switch_but.rect.height + MENU_PAD
932
933 save_but = Button('Save Level', action=self.save)
934 save_but.rect = BUTTON_RECT.copy()
935 save_but.rect.move_ip(MENU_LEFT, y)
936 widgets.append(save_but)
937 y += MENU_BUTTON_HEIGHT + MENU_PAD
938
939 zoom_out = Button('Zoom out', action=self.level_widget.zoom_out)
940 zoom_out.rect = BUTTON_RECT.copy()
941 zoom_out.rect.move_ip(MENU_LEFT, y)
942 widgets.append(zoom_out)
943 y += MENU_BUTTON_HEIGHT + MENU_PAD
944
945 zoom_in = Button('Zoom in', action=self.level_widget.zoom_in)
946 zoom_in.rect = BUTTON_RECT.copy()
947 zoom_in.rect.move_ip(MENU_LEFT, y)
948 widgets.append(zoom_in)
949
950 y = SCREEN[1] - MENU_BUTTON_HEIGHT - MENU_PAD
951 quit_but = Button('Quit', action=self.quit)
952 quit_but.rect = BUTTON_RECT.copy()
953 quit_but.rect.move_ip(MENU_LEFT, y)
954 widgets.append(quit_but)
955
956 self._dMenus['drawing'] = widgets
957
958 def _make_objects_menu(self):
959 widgets = []
960
961 # Add poly buttons
962 y = 15
963
964 edit_objs_but = Button('Edit Objects',
965 action=self.level_widget.edit_objects)
966 edit_objs_but.rect = BUTTON_RECT.copy()
967 edit_objs_but.rect.move_ip(MENU_LEFT, y)
968 widgets.append(edit_objs_but)
969 y += MENU_BUTTON_HEIGHT + MENU_PAD
970
971 edir_enemies_but = Button('Edit Enemies',
972 action=self.level_widget.edit_enemies)
973 edir_enemies_but.rect = BUTTON_RECT.copy()
974 edir_enemies_but.rect.move_ip(MENU_LEFT, y)
975 widgets.append(edir_enemies_but)
976 y += MENU_BUTTON_HEIGHT + MENU_PAD
977
978 add_obj_but = Button('Add Game Object',
979 action=self.level_widget.add_game_object)
980 add_obj_but.rect = BUTTON_RECT.copy()
981 add_obj_but.rect.move_ip(MENU_LEFT, y)
982 widgets.append(add_obj_but)
983 y += MENU_BUTTON_HEIGHT + MENU_PAD
984
985 add_puzzle_but = Button('Add Puzzler',
986 action=self.level_widget.add_puzzler)
987 add_puzzle_but.rect = BUTTON_RECT.copy()
988 add_puzzle_but.rect.move_ip(MENU_LEFT, y)
989 widgets.append(add_puzzle_but)
990 y += MENU_BUTTON_HEIGHT + MENU_PAD
991
992 add_enemy_but = Button('Add Enemy',
993 action=self.level_widget.add_enemy)
994 add_enemy_but.rect = BUTTON_RECT.copy()
995 add_enemy_but.rect.move_ip(MENU_LEFT, y)
996 widgets.append(add_enemy_but)
997 y += MENU_BUTTON_HEIGHT + MENU_PAD
998
999 y += MENU_PAD
1000 self.sel_mode_but = HighLightButton('Select Object', self,
1001 action=self.sel_mode)
1002 self.sel_mode_but.rect = BUTTON_RECT.copy()
1003 self.sel_mode_but.rect.move_ip(MENU_LEFT, y)
1004 widgets.append(self.sel_mode_but)
1005 self._light_buttons.append(self.sel_mode_but)
1006 y += MENU_BUTTON_HEIGHT + MENU_PAD
1007
1008 y += MENU_PAD
1009 switch_but = Button('Switch to Drawing', action=self.switch_to_draw)
1010 switch_but.rect = BUTTON_RECT.copy()
1011 switch_but.rect.move_ip(MENU_LEFT, y)
1012 widgets.append(switch_but)
1013 y += switch_but.rect.height + MENU_PAD
1014
1015 save_but = Button('Save Level', action=self.save)
1016 save_but.rect = BUTTON_RECT.copy()
1017 save_but.rect.move_ip(MENU_LEFT, y)
1018 widgets.append(save_but)
1019 y += MENU_BUTTON_HEIGHT + MENU_PAD
1020
1021 zoom_out = Button('Zoom out', action=self.level_widget.zoom_out)
1022 zoom_out.rect = BUTTON_RECT.copy()
1023 zoom_out.rect.move_ip(MENU_LEFT, y)
1024 widgets.append(zoom_out)
1025 y += MENU_BUTTON_HEIGHT + MENU_PAD
1026
1027 zoom_in = Button('Zoom in', action=self.level_widget.zoom_in)
1028 zoom_in.rect = BUTTON_RECT.copy()
1029 zoom_in.rect.move_ip(MENU_LEFT, y)
1030 widgets.append(zoom_in)
1031
1032 y = SCREEN[1] - MENU_BUTTON_HEIGHT - MENU_PAD
1033 quit_but = Button('Quit', action=self.quit)
1034 quit_but.rect = BUTTON_RECT.copy()
1035 quit_but.rect.move_ip(MENU_LEFT, y)
1036 widgets.append(quit_but)
1037
1038 self._dMenus['objects'] = widgets
1039
1040 def key_down(self, ev):
1041 if ev.key == pgl.K_ESCAPE:
1042 self.quit()
1043 elif ev.key == pgl.K_s:
1044 self.save()
1045 else:
1046 self.level_widget.key_down(ev)
1047
1048 def save(self):
1049 closed, messages = self.level.all_closed()
1050 if closed:
1051 self.level.save()
1052 # display success
1053 alert("Level %s saved successfully." % self.level.name)
1054 else:
1055 # display errors
1056 alert("Failed to save level.\n\n%s" % '\n'.join(messages))
1057
1058 def switch_to_draw(self):
1059 if self._menu_mode != 'drawing':
1060 self._clear_menu()
1061 self._menu_mode = 'drawing'
1062 self._populate_menu()
1063
1064 def switch_to_objects(self):
1065 if self._menu_mode != 'objects':
1066 self._clear_menu()
1067 self._menu_mode = 'objects'
1068 self._populate_menu()
1069
1070 def _clear_menu(self):
1071 for widget in self._dMenus[self._menu_mode]:
1072 self.remove(widget)
1073
1074 def reset_lit_buttons(self):
1075 for but in self._light_buttons:
1076 but.reset()
1077
1078 def _populate_menu(self):
1079 self.level_widget.change_poly(None)
1080 self.level_widget.sel_mode = False
1081 for widget in self._dMenus[self._menu_mode]:
1082 self.add(widget)
1083 self.invalidate()
1084
1085 def set_line_mode(self):
1086 self.level_widget.line_mode()
1087 self.draw_line_but.highlight()
1088
1089 def sel_mode(self):
1090 self.level_widget.sel_mode = not self.level_widget.sel_mode
1091 if self.level_widget.sel_mode:
1092 self.sel_mode_but.highlight()
1093 else:
1094 self.sel_mode_but.reset()
1095
1096 def mouse_move(self, ev):
1097 self.level_widget.mouse_move(ev)
1098
1099 def move_point(self):
1100 self.level_widget.set_move_mode()
1101 self.move_point_but.highlight()
1102
1103 def draw(self, surface):
1104 # Update checkbox state
1105 if self._menu_mode == 'drawing':
1106 self.level_widget.set_objects(self.show_objs.value)
1107 self.level_widget.set_enemies(self.show_enemies.value)
1108 else:
1109 self.level_widget.set_objects(True)
1110 self.level_widget.set_enemies(True)
1111 super(EditorApp, self).draw(surface)
1112
1113
1114if __name__ == "__main__":
1115 if len(sys.argv) not in [2, 4]:
1116 print 'Please supply a levelname or levelname and level size'
1117 sys.exit()
1118 # Need to ensure we have defaults for rendering
1119 parse_args([])
1120 pygame.display.init()
1121 pygame.font.init()
1122 pygame.display.set_mode((SCREEN[0] + MENU_WIDTH, SCREEN[1]),
1123 pgl.SWSURFACE)
1124 if len(sys.argv) == 2:
1125 level = EditorLevel(sys.argv[1])
1126 level.load(pymunk.Space())
1127 elif len(sys.argv) == 4:
1128 level = EditorLevel(sys.argv[1], int(sys.argv[2]), int(sys.argv[3]))
1129 pygame.display.set_caption('Nagslang Area Editor')
1130 pygame.key.set_repeat(200, 100)
1131 app = EditorApp(level, pygame.display.get_surface())
1132 app.run()
Note: See TracBrowser for help on using the repository browser.