source: nagslang/game_object.py@ 188:3894cfe15823

Last change on this file since 188:3894cfe15823 was 188:3894cfe15823, checked in by Jeremy Thurgood <firxen@…>, 8 years ago

Better collision handling, potentially locked doors.

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