Add base support for deferring world updates.
[koperkapel.git] / koperkapel / scenes / base.py
1 """ Scene utilities. """
2
3 import functools
4
5
6 def apply_events(f):
7     """ Decorator that applies events to an engine. """
8     @functools.wraps(f)
9     def wrap(self, *args, **kw):
10         events = f(self, *args, **kw)
11         self._apply_events(events)
12     return wrap
13
14
15 class Engine:
16     """ A holder for game state & scene management.
17         """
18
19     def __init__(self, app, scene, world):
20         self._app = app
21         self._scene = scene
22         self._world = world
23
24     def _apply_events(self, events):
25         if not events:
26             return
27         for ev in events:
28             ev.apply(self)
29
30     def change_scene(self, scene):
31         self._apply_events(self._scene.exit(self._world.proxy()))
32         self._scene = scene
33         self._apply_events(self._scene.enter(self._world.proxy()))
34
35     def change_world(self, *args, **kw):
36         self._world.apply_event(*args, **kw)
37
38     def quit_game(self):
39         from pgzero.game import exit
40         exit()
41
42     def move_screen(self, offset):
43         self._scene.move_screen(offset)
44
45     @apply_events
46     def update(self, dt):
47         return self._scene.update(self._world.proxy(), self, dt)
48
49     def draw(self):
50         self._scene.draw(self._app.screen)
51
52     @apply_events
53     def on_mouse_down(self, pos, button):
54         return self._scene.on_mouse_down(pos, button)
55
56     @apply_events
57     def on_mouse_up(self, pos, button):
58         return self._scene.on_mouse_up(pos, button)
59
60     @apply_events
61     def on_key_down(self, key, mod, unicode):
62         return self._scene.on_key_down(key, mod, unicode)
63
64     @apply_events
65     def on_key_up(self, key, mod):
66         return self._scene.on_key_up(key, mod)
67
68     @apply_events
69     def on_music_end(self):
70         return self._scene.on_music_end()
71
72
73 class Event:
74     """ Base class for events. """
75
76     ENGINE_METHOD = "unknown_event"
77
78     def __init__(self, *args, **kw):
79         self._args = args
80         self._kw = kw
81
82     def apply(self, engine):
83         getattr(engine, self.ENGINE_METHOD)(*self._args, **self._kw)
84
85
86 class ChangeSceneEvent(Event):
87     """ Change to a new scene. """
88
89     ENGINE_METHOD = "change_scene"
90
91
92 class WorldEvent(Event):
93     """ Be a hero. Change the world. """
94
95     ENGINE_METHOD = "change_world"
96
97
98 class QuitEvent(Event):
99     """ Quit the game. """
100
101     ENGINE_METHOD = "quit_game"
102
103
104 class MoveViewportEvent(Event):
105     """ Change to a new scene. """
106
107     ENGINE_METHOD = "move_screen"
108
109
110 class Layer:
111     """ A single layer of actors. """
112
113     def __init__(self, name):
114         self.name = name
115         self.actors = []
116
117     def __iter__(self):
118         return iter(self.actors)
119
120     def __getitem__(self, i):
121         return self.actors[i]
122
123     def __len__(self):
124         return len(self.actors)
125
126     def add(self, actor):
127         self.actors.append(actor)
128         return actor
129
130     def clear(self):
131         self.actors.clear()
132
133     def remove(self, actor):
134         self.actors.remove(actor)
135         return actor
136
137
138 class Actors:
139     """ Layers of actors.
140
141     Actors may be rendered in different layers. Layers with lower levels
142     are rendered lower than layers with higher ones.
143     """
144
145     def __init__(self):
146         self._ordered_layers = []
147         self._layers = {}
148         self.add_layer("default", 0)
149
150     def __getattr__(self, name):
151         return self._layers[name]
152
153     def add_layer(self, name, level):
154         layer = self._layers[name] = Layer(name)
155         self._ordered_layers.append((level, name))
156         self._ordered_layers.sort()
157         return layer
158
159     def add(self, actor, layer="default"):
160         return self._layers[layer].add(actor)
161
162     def remove(self, actor, layer="default"):
163         return self._layers[layer].remove(actor)
164
165     def draw(self, screen):
166         for lvl, name in self._ordered_layers:
167             for actor in self._layers[name]:
168                 # actor.draw doesn't allow blitting to anything other than
169                 # the game scene
170                 screen.blit(actor._surf, actor.topleft)
171
172
173 def defer_to_update(f):
174     """ Defers a function until the next update run. """
175     @functools.wraps(f)
176     def wrapper(self, *args, **kw):
177         self._deferred_updates.append((f, args, kw))
178     return wrapper
179
180
181 class Scene:
182     """ Base class for scenes. """
183
184     def __init__(self):
185         self.actors = Actors()
186         self.viewport = (0, 0)
187         self._deferred_updates = []
188
189     def move_screen(self, offset):
190         self.viewport = (self.viewport[0] + offset[0],
191                          self.viewport[1] + offset[1])
192
193     def calc_offset(self, x, y):
194         """ Return a position offset by the viewport. """
195         return x - self.viewport[0], y - self.viewport[1]
196
197     def enter(self, world):
198         pass
199
200     def exit(self, world):
201         pass
202
203     def update(self, world, engine, dt):
204         deferred_updates, self._deferred_updates = self._deferred_updates, []
205         for f, args, kw in deferred_updates:
206             f(self, world, *args, **kw)
207
208     def draw(self, screen):
209         screen.clear()
210         self.actors.draw(screen)
211
212     def on_mouse_down(self, pos, button):
213         pass
214
215     def on_mouse_up(self, pos, button):
216         pass
217
218     def on_key_down(self, key, mod, unicode):
219         pass
220
221     def on_key_up(self, key, mod):
222         pass
223
224     def on_music_end(self):
225         pass