source: nagslang/game_object.py@ 202:63d7ab8ede83

Last change on this file since 202:63d7ab8ede83 was 201:3495a2025bc6, checked in by Stefano Rivera <stefano@…>, 8 years ago

Break puzzlers out of game_object.py

File size: 11.9 KB
Line 
1import math
2
3import pygame
4import pymunk
5import pymunk.pygame_util
6
7from nagslang import puzzle
8from nagslang.constants import (
9 SWITCH_PUSHERS, COLLISION_TYPE_SWITCH, COLLISION_TYPE_BOX, ZORDER_LOW,
10 ZORDER_FLOOR, COLLISION_TYPE_DOOR)
11from nagslang.options import options
12from nagslang.resources import resources
13from nagslang.events import DoorEvent
14from nagslang.widgets.text import LabelWidget
15
16
17# For levels to import, until we get module names in 'classname'
18StateProxyPuzzler = puzzle.StateProxyPuzzler
19StateLogicalAndPuzzler = puzzle.StateLogicalAndPuzzler
20
21
22class Physicser(object):
23 def __init__(self, space):
24 self._space = space
25
26 def get_space(self):
27 return self._space
28
29 def set_game_object(self, game_object):
30 self.game_object = game_object
31
32 def get_shape(self):
33 raise NotImplementedError()
34
35 def add_to_space(self):
36 raise NotImplementedError()
37
38 def remove_from_space(self):
39 raise NotImplementedError()
40
41 def get_render_position(self, surface):
42 raise NotImplementedError()
43
44 def get_angle(self):
45 raise NotImplementedError()
46
47 def apply_impulse(self, j, r=(0, 0)):
48 raise NotImplementedError()
49
50
51class SingleShapePhysicser(Physicser):
52 def __init__(self, space, shape):
53 super(SingleShapePhysicser, self).__init__(space)
54 self._shape = shape
55 shape.physicser = self
56
57 def get_shape(self):
58 return self._shape
59
60 def add_to_space(self):
61 self.get_space().add(self._shape)
62 if not self._shape.body.is_static:
63 self.get_space().add(self._shape.body)
64
65 def remove_from_space(self):
66 self.get_space().remove(self._shape)
67 if not self._shape.body.is_static:
68 self.get_space().remove(self._shape.body)
69
70 def get_render_position(self, surface):
71 pos = self._shape.body.position
72 return pymunk.pygame_util.to_pygame(pos, surface)
73
74 def get_angle(self):
75 return self._shape.body.angle
76
77 def apply_impulse(self, j, r=(0, 0)):
78 return self._shape.body.apply_impulse(j, r)
79
80
81class Renderer(object):
82 def set_game_object(self, game_object):
83 self.game_object = game_object
84
85 def _render_shape(self, surface):
86 shape = self.game_object.get_shape()
87 # Less general that pymunk.pygame_util.draw, but also a lot less noisy.
88 color = getattr(shape, 'color', pygame.color.THECOLORS['lightblue'])
89 # We only explicitly draw Circle and Poly shapes. Everything else we
90 # forward to pymunk.
91 if isinstance(shape, pymunk.Circle):
92 centre = pymunk.pygame_util.to_pygame(shape.body.position, surface)
93 radius = int(shape.radius)
94 pygame.draw.circle(surface, color, centre, radius, 2)
95 elif isinstance(shape, pymunk.Poly):
96 # polygon bounding box
97 points = [pymunk.pygame_util.to_pygame(p, surface)
98 for p in shape.get_vertices()]
99 pygame.draw.lines(surface, color, True, points, 2)
100 else:
101 pymunk.pygame_util.draw(surface, shape)
102
103 def render(self, surface):
104 if options.debug:
105 self._render_shape(surface)
106
107 def animate(self):
108 # Used by time animatations to advance the clock
109 pass
110
111
112def image_pos(image, pos):
113 return (pos[0] - image.get_width() / 2,
114 pos[1] - image.get_height() / 2)
115
116
117class ImageRenderer(Renderer):
118 def __init__(self, image):
119 self._image = image
120
121 def get_image(self):
122 return self._image
123
124 def rotate_image(self, image):
125 angle = self.game_object.get_render_angle() * 180 / math.pi
126 return pygame.transform.rotate(image, angle)
127
128 def render_image(self, surface, image):
129 image = self.rotate_image(image)
130 pos = self.game_object.get_render_position(surface)
131 surface.blit(image, image_pos(image, pos))
132
133 def render(self, surface):
134 self.render_image(surface, self.get_image())
135 super(ImageRenderer, self).render(surface)
136
137
138class ImageStateRenderer(ImageRenderer):
139 def __init__(self, state_images):
140 self._state_images = state_images
141
142 def get_image(self):
143 return self._state_images[self.game_object.puzzler.get_state()]
144
145
146class FacingImageRenderer(ImageRenderer):
147 def __init__(self, left_image, right_image):
148 self._images = {
149 'left': left_image,
150 'right': right_image,
151 }
152 self._face = 'left'
153
154 def _update_facing(self, angle):
155 if abs(angle) < math.pi / 2:
156 self._face = 'right'
157 elif abs(angle) > math.pi / 2:
158 self._face = 'left'
159
160 def rotate_image(self, image):
161 # Facing images don't get rotated.
162 return image
163
164 def get_facing_image(self):
165 return self._images[self._face]
166
167 def get_image(self):
168 angle = self.game_object.get_render_angle()
169 self._update_facing(angle)
170 return self.get_facing_image()
171
172
173class AnimatedFacingImageRenderer(FacingImageRenderer):
174 def __init__(self, left_images, right_images):
175 self._images = {
176 'left': left_images,
177 'right': right_images,
178 }
179 self._frame = 0
180 self._moving = False
181 self._face = 'left'
182
183 def get_facing_image(self):
184 if self._frame >= len(self._images[self._face]):
185 self._frame = 0
186 return self._images[self._face][self._frame]
187
188 def animate(self):
189 if self._moving:
190 self._frame += 1
191 else:
192 self._frame = 0
193
194 def start(self):
195 self._moving = True
196
197 def stop(self):
198 self._moving = False
199
200
201class TimedAnimatedRenderer(ImageRenderer):
202
203 def __init__(self, images):
204 self._images = images
205 self._frame = 0
206 self._image = None
207
208 def get_image(self):
209 if self._frame > len(self._imaages):
210 self._frame = 0
211 return self._images[self._frame]
212
213 def animate(self):
214 self._frame += 1
215
216
217class ShapeRenderer(Renderer):
218 def render(self, surface):
219 self._render_shape(surface)
220 super(ShapeRenderer, self).render(surface)
221
222
223class ShapeStateRenderer(ShapeRenderer):
224 """Renders the shape in a different colour depending on the state.
225
226 Requires the game object it's attached to to have a puzzler.
227 """
228 def render(self, surface):
229 if self.game_object.puzzler.get_state():
230 color = pygame.color.THECOLORS['green']
231 else:
232 color = pygame.color.THECOLORS['red']
233
234 self.game_object.get_shape().color = color
235 super(ShapeStateRenderer, self).render(surface)
236
237
238def damping_velocity_func(body, gravity, damping, dt):
239 """Apply custom damping to this body's velocity.
240 """
241 damping = getattr(body, 'damping', damping)
242 return pymunk.Body.update_velocity(body, gravity, damping, dt)
243
244
245def make_body(mass, moment, position, damping=None):
246 body = pymunk.Body(mass, moment)
247 body.position = tuple(position)
248 if damping is not None:
249 body.damping = damping
250 body.velocity_func = damping_velocity_func
251 return body
252
253
254class Overlay(object):
255 def set_game_object(self, game_object):
256 self.game_object = game_object
257
258 def render(self, surface):
259 pass
260
261 def is_visible(self):
262 return self.game_object.puzzler.get_state()
263
264
265class TextOverlay(Overlay):
266 def __init__(self, text):
267 self.text = text
268 self.widget = LabelWidget((20, 20), self.text)
269
270 def render(self, surface):
271 self.widget.draw(surface)
272
273
274class GameObject(object):
275 """A representation of a thing in the game world.
276
277 This has a rendery thing, physicsy things and maybe some other things.
278 """
279
280 zorder = ZORDER_LOW
281
282 def __init__(self, physicser, renderer, puzzler=None, overlay=None):
283 self.physicser = physicser
284 physicser.set_game_object(self)
285 self.physicser.add_to_space()
286 self.renderer = renderer
287 renderer.set_game_object(self)
288 self.puzzler = puzzler
289 if puzzler is not None:
290 puzzler.set_game_object(self)
291 self.overlay = overlay
292 if overlay is not None:
293 self.overlay.set_game_object(self)
294
295 def get_space(self):
296 return self.physicser.get_space()
297
298 def get_shape(self):
299 return self.physicser.get_shape()
300
301 def get_render_position(self, surface):
302 return self.physicser.get_render_position(surface)
303
304 def get_render_angle(self):
305 return self.physicser.get_angle()
306
307 def render(self, surface):
308 return self.renderer.render(surface)
309
310 def animate(self):
311 self.renderer.animate()
312
313 def collide_with_protagonist(self):
314 """Called as a `pre_solve` collision callback with the protagonist.
315
316 You can return `False` to ignore the collision, anything else
317 (including `None`) to process the collision as normal.
318 """
319 return True
320
321
322class FloorSwitch(GameObject):
323 zorder = ZORDER_FLOOR
324
325 def __init__(self, space, position):
326 body = make_body(None, None, position)
327 self.shape = pymunk.Circle(body, 30)
328 self.shape.collision_type = COLLISION_TYPE_SWITCH
329 self.shape.sensor = True
330 super(FloorSwitch, self).__init__(
331 SingleShapePhysicser(space, self.shape),
332 ImageStateRenderer({
333 True: resources.get_image('objects', 'sensor_on.png'),
334 False: resources.get_image('objects', 'sensor_off.png'),
335 }),
336 puzzle.CollidePuzzler(*SWITCH_PUSHERS),
337 )
338
339
340class Note(GameObject):
341 zorder = ZORDER_FLOOR
342
343 def __init__(self, space, position, message):
344 body = make_body(None, None, position)
345 self.shape = pymunk.Circle(body, 30)
346 self.shape.sensor = True
347 super(Note, self).__init__(
348 SingleShapePhysicser(space, self.shape),
349 ImageRenderer(resources.get_image('objects', 'note.png')),
350 puzzle.CollidePuzzler(),
351 TextOverlay(message),
352 )
353
354
355class FloorLight(GameObject):
356 zorder = ZORDER_FLOOR
357
358 def __init__(self, space, position, state_source):
359 body = make_body(None, None, position)
360 self.shape = pymunk.Circle(body, 10)
361 self.shape.collision_type = COLLISION_TYPE_SWITCH
362 self.shape.sensor = True
363 super(FloorLight, self).__init__(
364 SingleShapePhysicser(space, self.shape),
365 ImageStateRenderer({
366 True: resources.get_image('objects', 'light_on.png'),
367 False: resources.get_image('objects', 'light_off.png'),
368 }),
369 puzzle.StateProxyPuzzler(state_source),
370 )
371
372
373class Box(GameObject):
374 def __init__(self, space, position):
375 body = make_body(10, 10000, position, damping=0.5)
376 self.shape = pymunk.Poly(
377 body, [(-20, -20), (20, -20), (20, 20), (-20, 20)])
378 self.shape.collision_type = COLLISION_TYPE_BOX
379 super(Box, self).__init__(
380 SingleShapePhysicser(space, self.shape),
381 ImageRenderer(resources.get_image('objects', 'crate.png')),
382 )
383
384
385class Door(GameObject):
386 zorder = ZORDER_FLOOR
387
388 def __init__(self, space, position, destination, dest_pos, key_state=None):
389 body = make_body(pymunk.inf, pymunk.inf, position, damping=0.5)
390 self.shape = pymunk.Poly(
391 body, [(-32, -32), (32, -32), (32, 32), (-32, 32)])
392 self.shape.collision_type = COLLISION_TYPE_DOOR
393 self.shape.sensor = True
394 self.destination = destination
395 self.dest_pos = tuple(dest_pos)
396 if key_state is None:
397 puzzler = puzzle.YesPuzzler()
398 else:
399 puzzler = puzzle.StateProxyPuzzler(key_state)
400 super(Door, self).__init__(
401 SingleShapePhysicser(space, self.shape),
402 ImageRenderer(resources.get_image('objects', 'door.png')),
403 puzzler,
404 )
405
406 def collide_with_protagonist(self):
407 if self.puzzler.get_state():
408 DoorEvent.post(self.destination, self.dest_pos)
Note: See TracBrowser for help on using the repository browser.