source: nagslang/game_object.py@ 180:026297a03963

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

Add DoorEvent and tweak ScreenChange to keep more state when the player goes through a door

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