source: tools/area_editor.py@ 204:687459429550

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

Display interior walls and start working towards drawing them

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