source: tools/area_editor.py@ 206:42c565c5ce76

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

PEP8

  • Property exe set to *
File size: 16.7 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> <ysiz>
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
28
29from nagslang.options import parse_args
30from nagslang.constants import SCREEN
31from nagslang.level import Level, POLY_COLORS, LINE_COLOR
32from nagslang.enemies import Enemy
33
34
35# layout constants
36MENU_BUTTON_HEIGHT = 35
37MENU_PAD = 6
38MENU_HALF_PAD = MENU_PAD // 2
39MENU_LEFT = SCREEN[0] + MENU_HALF_PAD
40MENU_WIDTH = 200 - MENU_PAD
41
42
43class EditorLevel(Level):
44
45 def __init__(self, name, x=800, y=600):
46 super(EditorLevel, self).__init__(name)
47 self.x = x
48 self.y = y
49
50 def round_point(self, pos):
51 return (10 * (pos[0] // 10), 10 * (pos[1] // 10))
52
53 def point_to_pymunk(self, pos):
54 # inverse of point_to_pygame
55 # (this is also the same as point_to_pygame, but a additional
56 # function for sanity later in pyweek).
57 return (pos[0], self.y - pos[1])
58
59 def add_point(self, poly_index, pos):
60 self.polygons.setdefault(poly_index, [])
61 if not self.polygons[poly_index]:
62 point = self.point_to_pymunk(self.round_point(pos))
63 self.polygons[poly_index].append(point)
64 else:
65 add_pos = self.fix_poly_angle(poly_index, pos)
66 self.polygons[poly_index].append(add_pos)
67
68 def _fix_angle(self, point1, pos):
69 # We want the line (point1 to pos) to be an angle of
70 # 0, 45, 90, 135, 180, 225, 270, 305
71 # However, we only need to consider half the circle
72 # This is a hack to approximate the right thing
73 pos0 = (pos[0], point1[1])
74 pos90 = (point1[0], pos[1])
75 dist = max(abs(point1[0] - pos[0]), abs(point1[1] - pos[1]))
76 pos45 = (point1[0] + dist, point1[1] + dist)
77 pos135 = (point1[0] + dist, point1[1] - dist)
78 pos225 = (point1[0] - dist, point1[1] - dist)
79 pos305 = (point1[0] - dist, point1[1] + dist)
80 min_dist = 9999999
81 new_pos = point1
82 for cand in [pos0, pos90, pos45, pos135, pos225, pos305]:
83 dist = (pos[0] - cand[0]) ** 2 + (pos[1] - cand[1]) ** 2
84 if dist < min_dist:
85 new_pos = cand
86 min_dist = dist
87 return self.point_to_pymunk(new_pos)
88
89 def fix_line_angle(self, start_pos, pos):
90 start_pos = self.round_point(start_pos)
91 pos = self.round_point(pos)
92 return self._fix_angle(start_pos, pos)
93
94 def fix_poly_angle(self, index, pos):
95 # Last point
96 point1 = self.point_to_pygame(self.polygons[index][-1])
97 pos = self.round_point(pos)
98 return self._fix_angle(point1, pos)
99
100 def delete_point(self, index):
101 if index in self.polygons and len(self.polygons[index]) > 0:
102 self.polygons[index].pop()
103
104 def close_poly(self, index):
105 """Attempts to close the current polygon.
106
107 We allow a small additional step to close the polygon, but
108 it's limited as it's a magic point addition"""
109 if len(self.polygons[index]) < 2:
110 # Too small
111 return False
112 first = self.polygons[index][0]
113 if self.fix_poly_angle(index, self.point_to_pygame(first)) == first:
114 self.add_point(index, self.point_to_pygame(first))
115 return True
116 candidates = [(first[0] + 10 * i, first[1]) for
117 i in (-3, -2, -1, 1, 2, 3)]
118 candidates.extend([(first[0], first[1] + 10 * i) for
119 i in (-3, -2, -1, 1, 2, 3)])
120 candidates.extend([(first[0] + 10 * i, first[1] + 10 * i) for
121 i in (-3, -2, -1, 1, 2, 3)])
122 candidates.extend([(first[0] + 10 * i, first[1] - 10 * i) for
123 i in (-3, -2, -1, 1, 2, 3)])
124 min_dist = 99999
125 poss = None
126 for cand in candidates:
127 if self.fix_poly_angle(index, self.point_to_pygame(cand)) == cand:
128 dist = (first[0] - cand[0]) ** 2 + (first[1] - cand[1]) ** 2
129 if dist < min_dist:
130 poss = cand
131 if poss is not None:
132 self.add_point(index, self.point_to_pygame(poss))
133 self.add_point(index, self.point_to_pygame(first))
134 return True
135 return False
136
137 def add_line(self, start_pos, end_pos):
138 endpoint = self.fix_line_angle(start_pos, end_pos)
139 startpoint = self.point_to_pymunk(self.round_point(start_pos))
140 self.lines.append([startpoint, endpoint])
141
142 def draw(self, mouse_pos, mouse_poly, filled, draw_cand_line, start_pos):
143 self._draw_background(True)
144 # Draw polygons as needed for the editor
145 if filled:
146 self._draw_exterior(True)
147 for index, polygon in self.polygons.items():
148 color = POLY_COLORS[index]
149 if len(polygon) > 1:
150 pointlist = [self.point_to_pygame(p) for p in polygon]
151 pygame.draw.lines(self._surface, color, False, pointlist, 2)
152 if index == mouse_poly and mouse_pos:
153 endpoint = self.fix_poly_angle(index, mouse_pos)
154 pygame.draw.line(self._surface, color,
155 self.point_to_pygame(polygon[-1]),
156 self.point_to_pygame(endpoint))
157 for line in self.lines:
158 pointlist = [self.point_to_pygame(p) for p in line]
159 pygame.draw.lines(self._surface, LINE_COLOR, False, pointlist, 2)
160 if draw_cand_line and start_pos and mouse_pos:
161 endpoint = self.fix_line_angle(start_pos, mouse_pos)
162 pointlist = [self.round_point(start_pos),
163 self.point_to_pygame(endpoint)]
164 pygame.draw.lines(self._surface, LINE_COLOR, False, pointlist, 1)
165 return self._surface.copy()
166
167
168class LevelWidget(Widget):
169
170 def __init__(self, level):
171 super(LevelWidget, self).__init__(pygame.rect.Rect(0, 0,
172 SCREEN[0], SCREEN[1]))
173 self.level = level
174 self.pos = (0, 0)
175 self.filled_mode = False
176 self.mouse_pos = None
177 self.cur_poly = None
178 self._mouse_drag = False
179 self._draw_objects = False
180 self._draw_enemies = False
181 self._draw_lines = False
182 self._start_pos = None
183
184 def _level_coordinates(self, pos):
185 # Move positions to level values
186 if not pos:
187 return (0, 0)
188 return pos[0] + self.pos[0], pos[1] + self.pos[1]
189
190 def _move_view(self, offset):
191 new_pos = [self.pos[0] + offset[0], self.pos[1] + offset[1]]
192 if new_pos[0] < 0:
193 new_pos[0] = self.pos[0]
194 elif new_pos[0] > self.level.x - SCREEN[0]:
195 new_pos[0] = self.pos[0]
196 if new_pos[1] < 0:
197 new_pos[1] = self.pos[1]
198 elif new_pos[1] > self.level.y - SCREEN[1]:
199 new_pos[1] = self.pos[1]
200 self.pos = tuple(new_pos)
201
202 def set_objects(self, value):
203 if self._draw_objects != value:
204 self._draw_objects = value
205 self.invalidate()
206
207 def set_enemies(self, value):
208 if self._draw_enemies != value:
209 self._draw_enemies = value
210 self.invalidate()
211
212 def draw(self, surface):
213 if (self.cur_poly is not None and self.cur_poly in self.level.polygons
214 and len(self.level.polygons[self.cur_poly])):
215 # We have an active polygon
216 mouse_pos = self._level_coordinates(self.mouse_pos)
217 elif self._draw_lines:
218 # Interior wall mode
219 mouse_pos = self._level_coordinates(self.mouse_pos)
220 else:
221 mouse_pos = None
222 level_surface = level.draw(mouse_pos, self.cur_poly, self.filled_mode,
223 self._draw_lines, self._start_pos)
224 if self._draw_objects:
225 for thing in self.level.drawables:
226 if not isinstance(thing, Enemy):
227 thing.render(level_surface)
228 if self._draw_enemies:
229 for thing in self.level.drawables:
230 if isinstance(thing, Enemy):
231 thing.render(level_surface)
232 surface_area = pygame.rect.Rect(self.pos, SCREEN)
233 surface.blit(level_surface, (0, 0), surface_area)
234
235 def change_poly(self, new_poly):
236 self.cur_poly = new_poly
237 self._draw_lines = False
238 if self.cur_poly is not None:
239 self.filled_mode = False
240
241 def line_mode(self):
242 self.cur_poly = None
243 self._draw_lines = True
244 self.filled_mode = False
245 self._start_pos = None
246
247 def key_down(self, ev):
248 if ev.key == pgl.K_LEFT:
249 self._move_view((-10, 0))
250 elif ev.key == pgl.K_RIGHT:
251 self._move_view((10, 0))
252 elif ev.key == pgl.K_UP:
253 self._move_view((0, -10))
254 elif ev.key == pgl.K_DOWN:
255 self._move_view((0, 10))
256 elif ev.key in (pgl.K_1, pgl.K_2, pgl.K_3, pgl.K_4, pgl.K_5, pgl.K_6):
257 self.change_poly(ev.key - pgl.K_0)
258 elif ev.key == pgl.K_0:
259 self.change_poly(None)
260 elif ev.key == pgl.K_d and self.cur_poly:
261 self.level.delete_point(self.cur_poly)
262 elif ev.key == pgl.K_f:
263 self.set_filled()
264 elif ev.key == pgl.K_c:
265 self.close_poly()
266
267 def set_filled(self):
268 closed, _ = self.level.all_closed()
269 if closed:
270 self.cur_poly = None
271 self.filled_mode = True
272 self._draw_lines = False
273 else:
274 alert('Not all polygons closed, so not filling')
275
276 def mouse_move(self, ev):
277 old_pos = self.mouse_pos
278 self.mouse_pos = ev.pos
279 if old_pos != self.mouse_pos and (self.cur_poly or self._draw_lines):
280 self.invalidate()
281
282 def mouse_drag(self, ev):
283 if self._mouse_drag:
284 old_pos = self.mouse_pos
285 self.mouse_pos = ev.pos
286 diff = (-self.mouse_pos[0] + old_pos[0],
287 -self.mouse_pos[1] + old_pos[1])
288 self._move_view(diff)
289 self.invalidate()
290
291 def mouse_down(self, ev):
292 if ev.button == 1:
293 if self._draw_lines:
294 if self._start_pos is None:
295 self._start_pos = ev.pos
296 else:
297 self.level.add_line(self._start_pos, ev.pos)
298 self._start_pos = None
299 else:
300 print "Click: %r" % (
301 self.level.point_to_pymunk(
302 self._level_coordinates(ev.pos)),)
303 if ev.button == 4: # Scroll up
304 self._move_view((0, -10))
305 elif ev.button == 5: # Scroll down
306 self._move_view((0, 10))
307 elif ev.button == 6: # Scroll left
308 self._move_view((-10, 0))
309 elif ev.button == 7: # Scroll right
310 self._move_view((10, 0))
311 elif self.cur_poly and ev.button == 1:
312 # Add a point
313 self.level.add_point(self.cur_poly,
314 self._level_coordinates(ev.pos))
315 elif ev.button == 3:
316 self._mouse_drag = True
317
318 def mouse_up(self, ev):
319 if ev.button == 3:
320 self._mouse_drag = False
321
322 def close_poly(self):
323 if self.cur_poly is None:
324 return
325 if self.level.close_poly(self.cur_poly):
326 alert("Successfully closed the polygon")
327 self.change_poly(None)
328 else:
329 alert("Failed to close the polygon")
330
331
332class PolyButton(Button):
333 """Button for coosing the correct polygon"""
334
335 def __init__(self, index, level_widget):
336 if index is not None:
337 text = "Draw: %s" % index
338 else:
339 text = 'Exit Draw Mode'
340 super(PolyButton, self).__init__(text)
341 self.index = index
342 self.level_widget = level_widget
343
344 def action(self):
345 self.level_widget.change_poly(self.index)
346
347
348class EditorApp(RootWidget):
349
350 def __init__(self, level, surface):
351 super(EditorApp, self).__init__(surface)
352 self.level = level
353 self.level_widget = LevelWidget(self.level)
354 self.add(self.level_widget)
355
356 # Add poly buttons
357 y = 15
358 for poly in range(1, 7):
359 but = PolyButton(poly, self.level_widget)
360 but.rect = pygame.rect.Rect(0, 0, MENU_WIDTH // 2 - MENU_PAD,
361 MENU_BUTTON_HEIGHT)
362 if poly % 2:
363 but.rect.move_ip(MENU_LEFT, y)
364 else:
365 but.rect.move_ip(MENU_LEFT + MENU_WIDTH // 2 - MENU_HALF_PAD,
366 y)
367 y += MENU_BUTTON_HEIGHT + MENU_PAD
368 self.add(but)
369
370 button_rect = pygame.rect.Rect(0, 0, MENU_WIDTH, MENU_BUTTON_HEIGHT)
371
372 check_rect = pygame.rect.Rect(0, 0, MENU_BUTTON_HEIGHT // 2,
373 MENU_BUTTON_HEIGHT // 2)
374
375 end_poly_but = PolyButton(None, self.level_widget)
376 end_poly_but.rect = button_rect.copy()
377 end_poly_but.rect.move_ip(MENU_LEFT, y)
378 self.add(end_poly_but)
379 y += MENU_BUTTON_HEIGHT + MENU_PAD
380
381 draw_line = Button("Draw interior wall", self.level_widget.line_mode)
382 draw_line.rect = button_rect.copy()
383 draw_line.rect.move_ip(MENU_LEFT, y)
384 self.add(draw_line)
385 y += MENU_BUTTON_HEIGHT + MENU_PAD
386
387 fill_but = Button('Fill exterior', action=self.level_widget.set_filled)
388 fill_but.rect = button_rect.copy()
389 fill_but.rect.move_ip(MENU_LEFT, y)
390 self.add(fill_but)
391 y += MENU_BUTTON_HEIGHT + MENU_PAD
392
393 save_but = Button('Save Level', action=self.save)
394 save_but.rect = button_rect.copy()
395 save_but.rect.move_ip(MENU_LEFT, y)
396 self.add(save_but)
397 y += MENU_BUTTON_HEIGHT + MENU_PAD
398
399 close_poly_but = Button('Close Polygon',
400 action=self.level_widget.close_poly)
401 close_poly_but.rect = button_rect.copy()
402 close_poly_but.rect.move_ip(MENU_LEFT, y)
403 self.add(close_poly_but)
404 y += MENU_BUTTON_HEIGHT + MENU_PAD
405
406 white = pygame.color.Color("white")
407 self.show_objs = CheckBox(fg_color=white)
408 self.show_objs.rect = check_rect.copy()
409 self.show_objs.rect.move_ip(MENU_LEFT, y)
410 label = Label("Show Objects", fg_color=white)
411 label.rect.move_ip(MENU_LEFT + MENU_BUTTON_HEIGHT // 2 + MENU_PAD, y)
412 self.add(self.show_objs)
413 self.add(label)
414 y += label.rect.height + MENU_PAD
415
416 self.show_enemies = CheckBox(fg_color=white)
417 self.show_enemies.rect = check_rect.copy()
418 self.show_enemies.rect.move_ip(MENU_LEFT, y)
419 label = Label("Show enemy start pos", fg_color=white)
420 label.rect.move_ip(MENU_LEFT + MENU_BUTTON_HEIGHT // 2 + MENU_PAD, y)
421 self.add(self.show_enemies)
422 self.add(label)
423 y += label.rect.height + MENU_PAD
424
425 quit_but = Button('Quit', action=self.quit)
426 quit_but.rect = button_rect.copy()
427 quit_but.rect.move_ip(MENU_LEFT, y)
428 self.add(quit_but)
429
430 def key_down(self, ev):
431 if ev.key == pgl.K_ESCAPE:
432 self.quit()
433 elif ev.key == pgl.K_s:
434 self.save()
435 else:
436 self.level_widget.key_down(ev)
437
438 def save(self):
439 closed, messages = self.level.all_closed()
440 if closed:
441 self.level.save()
442 # display success
443 alert("Level %s saved successfully." % self.level.name)
444 else:
445 # display errors
446 alert("Failed to save level.\n\n%s" % '\n'.join(messages))
447
448 def mouse_move(self, ev):
449 self.level_widget.mouse_move(ev)
450
451 def draw(self, surface):
452 # Update checkbox state
453 self.level_widget.set_objects(self.show_objs.value)
454 self.level_widget.set_enemies(self.show_enemies.value)
455 super(EditorApp, self).draw(surface)
456
457
458if __name__ == "__main__":
459 if len(sys.argv) not in [2, 4]:
460 print 'Please supply a levelname or levelname and level size'
461 sys.exit()
462 # Need to ensure we have defaults for rendering
463 parse_args([])
464 pygame.display.init()
465 pygame.font.init()
466 pygame.display.set_mode((SCREEN[0] + MENU_WIDTH, SCREEN[1]),
467 pgl.SWSURFACE)
468 if len(sys.argv) == 2:
469 level = EditorLevel(sys.argv[1])
470 level.load(pymunk.Space())
471 elif len(sys.argv) == 4:
472 level = EditorLevel(sys.argv[1], int(sys.argv[2]), int(sys.argv[3]))
473 pygame.display.set_caption('Nagslang Area Editor')
474 pygame.key.set_repeat(200, 100)
475 app = EditorApp(level, pygame.display.get_surface())
476 app.run()
Note: See TracBrowser for help on using the repository browser.