source: tools/area_editor.py @ 184:e003b994c48b

Last change on this file since 184:e003b994c48b was 184:e003b994c48b, checked in by Stefano Rivera <stefano@…>, 7 years ago

Output coordinates from the level editor, to help place things

  • Property exe set to *
File size: 12.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> <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
27from albow.dialogs import alert
28
29from nagslang.constants import SCREEN
30from nagslang.level import Level, POLY_COLORS
31
32
33# layout constants
34MENU_BUTTON_HEIGHT = 35
35MENU_PAD = 6
36MENU_HALF_PAD = MENU_PAD // 2
37MENU_LEFT = SCREEN[0] + MENU_HALF_PAD
38MENU_WIDTH = 200 - MENU_PAD
39
40
41class EditorLevel(Level):
42
43    def __init__(self, name, x=800, y=600):
44        super(EditorLevel, self).__init__(name)
45        self.x = x
46        self.y = y
47
48    def round_point(self, pos):
49        return (10 * (pos[0] // 10), 10 * (pos[1] // 10))
50
51    def point_to_pymunk(self, pos):
52        # inverse of point_to_pygame
53        # (this is also the same as point_to_pygame, but a additional
54        # function for sanity later in pyweek).
55        return (pos[0], self.y - pos[1])
56
57    def add_point(self, poly_index, pos):
58        self.polygons.setdefault(poly_index, [])
59        if not self.polygons[poly_index]:
60            point = self.point_to_pymunk(self.round_point(pos))
61            self.polygons[poly_index].append(point)
62        else:
63            add_pos = self.fix_angle(poly_index, pos)
64            self.polygons[poly_index].append(add_pos)
65
66    def fix_angle(self, index, pos):
67        # Last point
68        point1 = self.point_to_pygame(self.polygons[index][-1])
69        pos = self.round_point(pos)
70        # We want the line (point1 to pos) to be an angle of
71        # 0, 45, 90, 135, 180, 225, 270, 305
72        # However, we only need to consider half the circle
73        # This is a hack to approximate the right thing
74        pos0 = (pos[0], point1[1])
75        pos90 = (point1[0], pos[1])
76        dist = max(abs(point1[0] - pos[0]), abs(point1[1] - pos[1]))
77        pos45 = (point1[0] + dist, point1[1] + dist)
78        pos135 = (point1[0] + dist, point1[1] - dist)
79        pos225 = (point1[0] - dist, point1[1] - dist)
80        pos305 = (point1[0] - dist, point1[1] + dist)
81        min_dist = 9999999
82        new_pos = point1
83        for cand in [pos0, pos90, pos45, pos135, pos225, pos305]:
84            dist = (pos[0] - cand[0]) ** 2 + (pos[1] - cand[1]) ** 2
85            if dist < min_dist:
86                new_pos = cand
87                min_dist = dist
88        return self.point_to_pymunk(new_pos)
89
90    def delete_point(self, index):
91        if index in self.polygons and len(self.polygons[index]) > 0:
92            self.polygons[index].pop()
93
94    def close_poly(self, index):
95        """Attempts to close the current polygon.
96
97           We allow a small additional step to close the polygon, but
98           it's limited as it's a magic point addition"""
99        if len(self.polygons[index]) < 2:
100            # Too small
101            return False
102        first = self.polygons[index][0]
103        if self.fix_angle(index, self.point_to_pygame(first)) == first:
104            self.add_point(index, self.point_to_pygame(first))
105            return True
106        candidates = [(first[0] + 10 * i, first[1]) for
107                      i in (-3, -2, -1, 1, 2, 3)]
108        candidates.extend([(first[0], first[1] + 10 * i) for
109                          i in (-3, -2, -1, 1, 2, 3)])
110        candidates.extend([(first[0] + 10 * i, 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        min_dist = 99999
115        poss = None
116        for cand in candidates:
117            if self.fix_angle(index, self.point_to_pygame(cand)) == cand:
118                dist = (first[0] - cand[0]) ** 2 + (first[1] - cand[1]) ** 2
119                if dist < min_dist:
120                    poss = cand
121        if poss is not None:
122            self.add_point(index, self.point_to_pygame(poss))
123            self.add_point(index, self.point_to_pygame(first))
124            return True
125        return False
126
127    def draw(self, surface, topleft, mouse_pos, mouse_poly, filled):
128        self._draw_background(True)
129        # Draw polygons as needed for the editor
130        if filled:
131            self._draw_exterior(True)
132        for index, polygon in self.polygons.items():
133            color = POLY_COLORS[index]
134            if len(polygon) > 1:
135                pointlist = [self.point_to_pygame(p) for p in polygon]
136                pygame.draw.lines(self._surface, color, False, pointlist, 2)
137            if index == mouse_poly and mouse_pos:
138                endpoint = self.fix_angle(index, mouse_pos)
139                pygame.draw.line(self._surface, color,
140                                 self.point_to_pygame(polygon[-1]),
141                                 self.point_to_pygame(endpoint))
142        surface_area = pygame.rect.Rect(topleft, SCREEN)
143        surface.blit(self._surface, (0, 0), surface_area)
144
145
146class LevelWidget(Widget):
147
148    def __init__(self, level):
149        super(LevelWidget, self).__init__(pygame.rect.Rect(0, 0,
150                                          SCREEN[0], SCREEN[1]))
151        self.level = level
152        self.pos = (0, 0)
153        self.filled_mode = False
154        self.mouse_pos = None
155        self.cur_poly = None
156        self._mouse_drag = False
157
158    def _level_coordinates(self, pos):
159        # Move positions to level values
160        if not pos:
161            return (0, 0)
162        return pos[0] + self.pos[0], pos[1] + self.pos[1]
163
164    def _move_view(self, offset):
165        new_pos = [self.pos[0] + offset[0], self.pos[1] + offset[1]]
166        if new_pos[0] < 0:
167            new_pos[0] = self.pos[0]
168        elif new_pos[0] > self.level.x - SCREEN[0]:
169            new_pos[0] = self.pos[0]
170        if new_pos[1] < 0:
171            new_pos[1] = self.pos[1]
172        elif new_pos[1] > self.level.y - SCREEN[1]:
173            new_pos[1] = self.pos[1]
174        self.pos = tuple(new_pos)
175
176    def draw(self, surface):
177        if (self.cur_poly is not None and self.cur_poly in self.level.polygons
178                and len(self.level.polygons[self.cur_poly])):
179            # We have an active polygon
180            mouse_pos = self._level_coordinates(self.mouse_pos)
181        else:
182            mouse_pos = None
183        level.draw(surface, self.pos, mouse_pos, self.cur_poly,
184                   self.filled_mode)
185
186    def change_poly(self, new_poly):
187        self.cur_poly = new_poly
188        if self.cur_poly is not None:
189            self.filled_mode = False
190
191    def key_down(self, ev):
192        if ev.key == pgl.K_LEFT:
193            self._move_view((-10, 0))
194        elif ev.key == pgl.K_RIGHT:
195            self._move_view((10, 0))
196        elif ev.key == pgl.K_UP:
197            self._move_view((0, -10))
198        elif ev.key == pgl.K_DOWN:
199            self._move_view((0, 10))
200        elif ev.key in (pgl.K_1, pgl.K_2, pgl.K_3, pgl.K_4, pgl.K_5, pgl.K_6):
201            self.change_poly(ev.key - pgl.K_0)
202        elif ev.key == pgl.K_0:
203            self.change_poly(None)
204        elif ev.key == pgl.K_d and self.cur_poly:
205            self.level.delete_point(self.cur_poly)
206        elif ev.key == pgl.K_f:
207            self.set_filled()
208        elif ev.key == pgl.K_c:
209            self.close_poly()
210
211    def set_filled(self):
212        closed, _ = self.level.all_closed()
213        if closed:
214            self.cur_poly = None
215            self.filled_mode = True
216        else:
217            alert('Not all polygons closed, so not filling')
218
219    def mouse_move(self, ev):
220        old_pos = self.mouse_pos
221        self.mouse_pos = ev.pos
222        if self.cur_poly and old_pos != self.mouse_pos:
223            self.invalidate()
224
225    def mouse_drag(self, ev):
226        if self._mouse_drag:
227            old_pos = self.mouse_pos
228            self.mouse_pos = ev.pos
229            diff = (-self.mouse_pos[0] + old_pos[0],
230                    -self.mouse_pos[1] + old_pos[1])
231            self._move_view(diff)
232            self.invalidate()
233
234    def mouse_down(self, ev):
235        if ev.button == 1:
236            print "Click: %r" % (self._level_coordinates(ev.pos),)
237        if ev.button == 4:  # Scroll up
238            self._move_view((0, -10))
239        elif ev.button == 5:  # Scroll down
240            self._move_view((0, 10))
241        elif ev.button == 6:  # Scroll left
242            self._move_view((-10, 0))
243        elif ev.button == 7:  # Scroll right
244            self._move_view((10, 0))
245        elif self.cur_poly and ev.button == 1:
246            # Add a point
247            self.level.add_point(self.cur_poly,
248                                 self._level_coordinates(ev.pos))
249        elif ev.button == 3:
250            self._mouse_drag = True
251
252    def mouse_up(self, ev):
253        if ev.button == 3:
254            self._mouse_drag = False
255
256    def close_poly(self):
257        if self.cur_poly is None:
258            return
259        if self.level.close_poly(self.cur_poly):
260            alert("Successfully closed the polygon")
261            self.change_poly(None)
262        else:
263            alert("Failed to close the polygon")
264
265
266class PolyButton(Button):
267    """Button for coosing the correct polygon"""
268
269    def __init__(self, index, level_widget):
270        if index is not None:
271            text = "Draw: %s" % index
272        else:
273            text = 'Exit Draw Mode'
274        super(PolyButton, self).__init__(text)
275        self.index = index
276        self.level_widget = level_widget
277
278    def action(self):
279        self.level_widget.change_poly(self.index)
280
281
282class EditorApp(RootWidget):
283
284    def __init__(self, level, surface):
285        super(EditorApp, self).__init__(surface)
286        self.level = level
287        self.level_widget = LevelWidget(self.level)
288        self.add(self.level_widget)
289
290        # Add poly buttons
291        y = 15
292        for poly in range(1, 7):
293            but = PolyButton(poly, self.level_widget)
294            but.rect = pygame.rect.Rect(0, 0, MENU_WIDTH // 2 - MENU_PAD,
295                                        MENU_BUTTON_HEIGHT)
296            if poly % 2:
297                but.rect.move_ip(MENU_LEFT, y)
298            else:
299                but.rect.move_ip(MENU_LEFT + MENU_WIDTH // 2 - MENU_HALF_PAD,
300                                 y)
301                y += MENU_BUTTON_HEIGHT + MENU_PAD
302            self.add(but)
303
304        button_rect = pygame.rect.Rect(0, 0, MENU_WIDTH, MENU_BUTTON_HEIGHT)
305
306        end_poly_but = PolyButton(None, self.level_widget)
307        end_poly_but.rect = button_rect.copy()
308        end_poly_but.rect.move_ip(MENU_LEFT, y)
309        self.add(end_poly_but)
310        y += MENU_BUTTON_HEIGHT + MENU_PAD
311
312        fill_but = Button('Fill exterior', action=self.level_widget.set_filled)
313        fill_but.rect = button_rect.copy()
314        fill_but.rect.move_ip(MENU_LEFT, y)
315        self.add(fill_but)
316        y += MENU_BUTTON_HEIGHT + MENU_PAD
317
318        save_but = Button('Save Level', action=self.save)
319        save_but.rect = button_rect.copy()
320        save_but.rect.move_ip(MENU_LEFT, y)
321        self.add(save_but)
322        y += MENU_BUTTON_HEIGHT + MENU_PAD
323
324        close_poly_but = Button('Close Polygon',
325                                action=self.level_widget.close_poly)
326        close_poly_but.rect = button_rect.copy()
327        close_poly_but.rect.move_ip(MENU_LEFT, y)
328        self.add(close_poly_but)
329        y += MENU_BUTTON_HEIGHT + MENU_PAD
330
331        quit_but = Button('Quit', action=self.quit)
332        quit_but.rect = button_rect.copy()
333        quit_but.rect.move_ip(MENU_LEFT, y)
334        self.add(quit_but)
335
336    def key_down(self, ev):
337        if ev.key == pgl.K_ESCAPE:
338            self.quit()
339        elif ev.key == pgl.K_s:
340            self.save()
341        else:
342            self.level_widget.key_down(ev)
343
344    def save(self):
345        closed, messages = self.level.all_closed()
346        if closed:
347            self.level.save()
348            # display success
349            alert("Level %s saved successfully." % self.level.name)
350        else:
351            # display errors
352            alert("Failed to save level.\n\n%s" % '\n'.join(messages))
353
354    def mouse_move(self, ev):
355        self.level_widget.mouse_move(ev)
356
357
358if __name__ == "__main__":
359    if len(sys.argv) == 2:
360        level = EditorLevel(sys.argv[1])
361        level.load(pymunk.Space())
362    elif len(sys.argv) == 4:
363        level = EditorLevel(sys.argv[1], int(sys.argv[2]), int(sys.argv[3]))
364    else:
365        print 'Please supply a levelname or levelname and level size'
366        sys.exit()
367    pygame.display.init()
368    pygame.font.init()
369    pygame.display.set_mode((SCREEN[0] + MENU_WIDTH, SCREEN[1]),
370                            pgl.SWSURFACE)
371    pygame.display.set_caption('Nagslang Area Editor')
372    pygame.key.set_repeat(200, 100)
373    app = EditorApp(level, pygame.display.get_surface())
374    app.run()
Note: See TracBrowser for help on using the repository browser.