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
RevLine 
[71]1#!/usr/bin/env python
2
[51]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
[71]14import os
15import sys
16
[51]17import pygame
18import pygame.locals as pgl
19
[152]20sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
21
[153]22import pymunk
23
[108]24from albow.root import RootWidget
25from albow.widget import Widget
[198]26from albow.controls import Button, Label, CheckBox
[122]27from albow.dialogs import alert
[108]28
[199]29from nagslang.options import parse_args
[109]30from nagslang.constants import SCREEN
[204]31from nagslang.level import Level, POLY_COLORS, LINE_COLOR
[199]32from nagslang.enemies import Enemy
[51]33
34
[108]35# layout constants
36MENU_BUTTON_HEIGHT = 35
[115]37MENU_PAD = 6
38MENU_HALF_PAD = MENU_PAD // 2
39MENU_LEFT = SCREEN[0] + MENU_HALF_PAD
40MENU_WIDTH = 200 - MENU_PAD
[108]41
42
[51]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]:
[99]62 point = self.point_to_pymunk(self.round_point(pos))
63 self.polygons[poly_index].append(point)
[51]64 else:
[205]65 add_pos = self.fix_poly_angle(poly_index, pos)
[51]66 self.polygons[poly_index].append(add_pos)
67
[205]68 def _fix_angle(self, point1, pos):
[51]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
[205]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
[51]100 def delete_point(self, index):
101 if index in self.polygons and len(self.polygons[index]) > 0:
102 self.polygons[index].pop()
103
[135]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]
[205]113 if self.fix_poly_angle(index, self.point_to_pygame(first)) == first:
[135]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:
[205]127 if self.fix_poly_angle(index, self.point_to_pygame(cand)) == cand:
[135]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
[205]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):
[51]143 self._draw_background(True)
144 # Draw polygons as needed for the editor
[96]145 if filled:
146 self._draw_exterior(True)
[51]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:
[205]153 endpoint = self.fix_poly_angle(index, mouse_pos)
[51]154 pygame.draw.line(self._surface, color,
[99]155 self.point_to_pygame(polygon[-1]),
156 self.point_to_pygame(endpoint))
[204]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)
[205]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),
[206]163 self.point_to_pygame(endpoint)]
[205]164 pygame.draw.lines(self._surface, LINE_COLOR, False, pointlist, 1)
[199]165 return self._surface.copy()
[51]166
[109]167
[108]168class LevelWidget(Widget):
[51]169
[108]170 def __init__(self, level):
171 super(LevelWidget, self).__init__(pygame.rect.Rect(0, 0,
[109]172 SCREEN[0], SCREEN[1]))
[51]173 self.level = level
174 self.pos = (0, 0)
[108]175 self.filled_mode = False
[51]176 self.mouse_pos = None
[108]177 self.cur_poly = None
[167]178 self._mouse_drag = False
[199]179 self._draw_objects = False
180 self._draw_enemies = False
[204]181 self._draw_lines = False
[205]182 self._start_pos = None
[51]183
[108]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):
[51]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
[199]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
[108]212 def draw(self, surface):
[51]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)
[205]217 elif self._draw_lines:
218 # Interior wall mode
219 mouse_pos = self._level_coordinates(self.mouse_pos)
[51]220 else:
221 mouse_pos = None
[205]222 level_surface = level.draw(mouse_pos, self.cur_poly, self.filled_mode,
[206]223 self._draw_lines, self._start_pos)
[199]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)
[51]234
[115]235 def change_poly(self, new_poly):
236 self.cur_poly = new_poly
[204]237 self._draw_lines = False
[115]238 if self.cur_poly is not None:
239 self.filled_mode = False
240
[204]241 def line_mode(self):
242 self.cur_poly = None
243 self._draw_lines = True
244 self.filled_mode = False
[205]245 self._start_pos = None
[204]246
[108]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))
[115]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)
[108]258 elif ev.key == pgl.K_0:
[115]259 self.change_poly(None)
[108]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:
[117]263 self.set_filled()
[135]264 elif ev.key == pgl.K_c:
265 self.close_poly()
[117]266
267 def set_filled(self):
[165]268 closed, _ = self.level.all_closed()
269 if closed:
[117]270 self.cur_poly = None
271 self.filled_mode = True
[204]272 self._draw_lines = False
[117]273 else:
[165]274 alert('Not all polygons closed, so not filling')
[108]275
[109]276 def mouse_move(self, ev):
277 old_pos = self.mouse_pos
278 self.mouse_pos = ev.pos
[205]279 if old_pos != self.mouse_pos and (self.cur_poly or self._draw_lines):
[109]280 self.invalidate()
281
[167]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
[109]291 def mouse_down(self, ev):
[184]292 if ev.button == 1:
[204]293 if self._draw_lines:
[205]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
[204]299 else:
300 print "Click: %r" % (
301 self.level.point_to_pymunk(
302 self._level_coordinates(ev.pos)),)
[157]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))
[167]311 elif self.cur_poly and ev.button == 1:
[109]312 # Add a point
313 self.level.add_point(self.cur_poly,
314 self._level_coordinates(ev.pos))
[167]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
[108]321
[135]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
[108]331
[115]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
[108]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
[115]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
[134]370 button_rect = pygame.rect.Rect(0, 0, MENU_WIDTH, MENU_BUTTON_HEIGHT)
371
[198]372 check_rect = pygame.rect.Rect(0, 0, MENU_BUTTON_HEIGHT // 2,
373 MENU_BUTTON_HEIGHT // 2)
374
[115]375 end_poly_but = PolyButton(None, self.level_widget)
[134]376 end_poly_but.rect = button_rect.copy()
[115]377 end_poly_but.rect.move_ip(MENU_LEFT, y)
378 self.add(end_poly_but)
379 y += MENU_BUTTON_HEIGHT + MENU_PAD
380
[204]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
[117]387 fill_but = Button('Fill exterior', action=self.level_widget.set_filled)
[134]388 fill_but.rect = button_rect.copy()
[117]389 fill_but.rect.move_ip(MENU_LEFT, y)
390 self.add(fill_but)
391 y += MENU_BUTTON_HEIGHT + MENU_PAD
392
[116]393 save_but = Button('Save Level', action=self.save)
[134]394 save_but.rect = button_rect.copy()
[116]395 save_but.rect.move_ip(MENU_LEFT, y)
396 self.add(save_but)
397 y += MENU_BUTTON_HEIGHT + MENU_PAD
398
[135]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
[198]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
[108]425 quit_but = Button('Quit', action=self.quit)
[134]426 quit_but.rect = button_rect.copy()
[115]427 quit_but.rect.move_ip(MENU_LEFT, y)
[108]428 self.add(quit_but)
429
430 def key_down(self, ev):
431 if ev.key == pgl.K_ESCAPE:
432 self.quit()
[116]433 elif ev.key == pgl.K_s:
434 self.save()
[108]435 else:
436 self.level_widget.key_down(ev)
[51]437
[116]438 def save(self):
[122]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))
[116]447
[109]448 def mouse_move(self, ev):
449 self.level_widget.mouse_move(ev)
450
[199]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
[51]457
458if __name__ == "__main__":
[195]459 if len(sys.argv) not in [2, 4]:
[51]460 print 'Please supply a levelname or levelname and level size'
461 sys.exit()
[199]462 # Need to ensure we have defaults for rendering
463 parse_args([])
[51]464 pygame.display.init()
465 pygame.font.init()
[108]466 pygame.display.set_mode((SCREEN[0] + MENU_WIDTH, SCREEN[1]),
[109]467 pgl.SWSURFACE)
[195]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]))
[51]473 pygame.display.set_caption('Nagslang Area Editor')
[108]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.