source: nagslang/game_object.py@ 201:3495a2025bc6

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

Break puzzlers out of game_object.py

File size: 11.9 KB
RevLine 
[63]1import math
[62]2
[81]3import pygame
4import pymunk
[93]5import pymunk.pygame_util
[81]6
[201]7from nagslang import puzzle
[107]8from nagslang.constants import (
[162]9 SWITCH_PUSHERS, COLLISION_TYPE_SWITCH, COLLISION_TYPE_BOX, ZORDER_LOW,
[201]10 ZORDER_FLOOR, COLLISION_TYPE_DOOR)
[104]11from nagslang.options import options
[155]12from nagslang.resources import resources
[180]13from nagslang.events import DoorEvent
[196]14from nagslang.widgets.text import LabelWidget
[81]15
[82]16
[201]17# For levels to import, until we get module names in 'classname'
18StateProxyPuzzler = puzzle.StateProxyPuzzler
19StateLogicalAndPuzzler = puzzle.StateLogicalAndPuzzler
[140]20
21
[59]22class Physicser(object):
[93]23 def __init__(self, space):
[123]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()
[93]34
35 def add_to_space(self):
[59]36 raise NotImplementedError()
37
[93]38 def remove_from_space(self):
[59]39 raise NotImplementedError()
40
[93]41 def get_render_position(self, surface):
[63]42 raise NotImplementedError()
43
[93]44 def get_angle(self):
45 raise NotImplementedError()
46
47 def apply_impulse(self, j, r=(0, 0)):
[59]48 raise NotImplementedError()
49
50
51class SingleShapePhysicser(Physicser):
[93]52 def __init__(self, space, shape):
53 super(SingleShapePhysicser, self).__init__(space)
[59]54 self._shape = shape
[186]55 shape.physicser = self
[59]56
[123]57 def get_shape(self):
58 return self._shape
59
[93]60 def add_to_space(self):
[123]61 self.get_space().add(self._shape)
[93]62 if not self._shape.body.is_static:
[123]63 self.get_space().add(self._shape.body)
[59]64
[93]65 def remove_from_space(self):
[123]66 self.get_space().remove(self._shape)
[93]67 if not self._shape.body.is_static:
[123]68 self.get_space().remove(self._shape.body)
[59]69
[93]70 def get_render_position(self, surface):
[59]71 pos = self._shape.body.position
72 return pymunk.pygame_util.to_pygame(pos, surface)
[63]73
[93]74 def get_angle(self):
[63]75 return self._shape.body.angle
[59]76
[93]77 def apply_impulse(self, j, r=(0, 0)):
78 return self._shape.body.apply_impulse(j, r)
79
[59]80
81class Renderer(object):
[123]82 def set_game_object(self, game_object):
83 self.game_object = game_object
[104]84
[123]85 def _render_shape(self, surface):
86 shape = self.game_object.get_shape()
[104]87 # Less general that pymunk.pygame_util.draw, but also a lot less noisy.
[123]88 color = getattr(shape, 'color', pygame.color.THECOLORS['lightblue'])
[104]89 # We only explicitly draw Circle and Poly shapes. Everything else we
90 # forward to pymunk.
[123]91 if isinstance(shape, pymunk.Circle):
92 centre = pymunk.pygame_util.to_pygame(shape.body.position, surface)
93 radius = int(shape.radius)
[104]94 pygame.draw.circle(surface, color, centre, radius, 2)
[123]95 elif isinstance(shape, pymunk.Poly):
[104]96 # polygon bounding box
97 points = [pymunk.pygame_util.to_pygame(p, surface)
[123]98 for p in shape.get_vertices()]
[104]99 pygame.draw.lines(surface, color, True, points, 2)
100 else:
[123]101 pymunk.pygame_util.draw(surface, shape)
[104]102
[123]103 def render(self, surface):
[104]104 if options.debug:
[155]105 self._render_shape(surface)
[59]106
[143]107 def animate(self):
108 # Used by time animatations to advance the clock
109 pass
110
[59]111
[63]112def image_pos(image, pos):
113 return (pos[0] - image.get_width() / 2,
114 pos[1] - image.get_height() / 2)
115
116
[59]117class ImageRenderer(Renderer):
[123]118 def __init__(self, image):
[59]119 self._image = image
120
[160]121 def get_image(self):
122 return self._image
123
124 def rotate_image(self, image):
[155]125 angle = self.game_object.get_render_angle() * 180 / math.pi
[160]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)
[155]131 surface.blit(image, image_pos(image, pos))
[160]132
133 def render(self, surface):
134 self.render_image(surface, self.get_image())
[123]135 super(ImageRenderer, self).render(surface)
[63]136
137
[162]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
[160]146class FacingImageRenderer(ImageRenderer):
[123]147 def __init__(self, left_image, right_image):
[63]148 self._images = {
149 'left': left_image,
150 'right': right_image,
151 }
[159]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'
[63]159
[160]160 def rotate_image(self, image):
161 # Facing images don't get rotated.
162 return image
163
164 def get_facing_image(self):
[159]165 return self._images[self._face]
[63]166
[160]167 def get_image(self):
168 angle = self.game_object.get_render_angle()
169 self._update_facing(angle)
170 return self.get_facing_image()
[59]171
172
[143]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
[159]181 self._face = 'left'
[143]182
[160]183 def get_facing_image(self):
[159]184 if self._frame >= len(self._images[self._face]):
[143]185 self._frame = 0
[159]186 return self._images[self._face][self._frame]
[143]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
[160]208 def get_image(self):
[143]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
[133]217class ShapeRenderer(Renderer):
218 def render(self, surface):
219 self._render_shape(surface)
220 super(ShapeRenderer, self).render(surface)
221
222
223class ShapeStateRenderer(ShapeRenderer):
[126]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 """
[123]228 def render(self, surface):
[126]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)
[59]236
237
[133]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)
[145]247 body.position = tuple(position)
[133]248 if damping is not None:
249 body.damping = damping
250 body.velocity_func = damping_velocity_func
251 return body
252
253
[191]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
[196]268 self.widget = LabelWidget((20, 20), self.text)
[191]269
270 def render(self, surface):
271 self.widget.draw(surface)
272
273
[59]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
[162]280 zorder = ZORDER_LOW
281
[191]282 def __init__(self, physicser, renderer, puzzler=None, overlay=None):
[93]283 self.physicser = physicser
[123]284 physicser.set_game_object(self)
[93]285 self.physicser.add_to_space()
[59]286 self.renderer = renderer
[123]287 renderer.set_game_object(self)
[81]288 self.puzzler = puzzler
[123]289 if puzzler is not None:
290 puzzler.set_game_object(self)
[191]291 self.overlay = overlay
292 if overlay is not None:
293 self.overlay.set_game_object(self)
[59]294
[123]295 def get_space(self):
296 return self.physicser.get_space()
297
298 def get_shape(self):
299 return self.physicser.get_shape()
300
[93]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()
[59]306
307 def render(self, surface):
[123]308 return self.renderer.render(surface)
[81]309
[143]310 def animate(self):
311 self.renderer.animate()
312
[186]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 """
[192]319 return True
[186]320
[81]321
322class FloorSwitch(GameObject):
[162]323 zorder = ZORDER_FLOOR
324
[93]325 def __init__(self, space, position):
[145]326 body = make_body(None, None, position)
[81]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__(
[93]331 SingleShapePhysicser(space, self.shape),
[162]332 ImageStateRenderer({
333 True: resources.get_image('objects', 'sensor_on.png'),
334 False: resources.get_image('objects', 'sensor_off.png'),
335 }),
[201]336 puzzle.CollidePuzzler(*SWITCH_PUSHERS),
[81]337 )
338
[106]339
[191]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')),
[201]350 puzzle.CollidePuzzler(),
[191]351 TextOverlay(message),
352 )
353
354
[106]355class FloorLight(GameObject):
[162]356 zorder = ZORDER_FLOOR
357
[106]358 def __init__(self, space, position, state_source):
[145]359 body = make_body(None, None, position)
[106]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),
[162]365 ImageStateRenderer({
366 True: resources.get_image('objects', 'light_on.png'),
367 False: resources.get_image('objects', 'light_off.png'),
368 }),
[201]369 puzzle.StateProxyPuzzler(state_source),
[106]370 )
[133]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),
[155]381 ImageRenderer(resources.get_image('objects', 'crate.png')),
[133]382 )
[176]383
384
385class Door(GameObject):
386 zorder = ZORDER_FLOOR
387
[186]388 def __init__(self, space, position, destination, dest_pos, key_state=None):
[176]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)
[186]396 if key_state is None:
[201]397 puzzler = puzzle.YesPuzzler()
[186]398 else:
[201]399 puzzler = puzzle.StateProxyPuzzler(key_state)
[176]400 super(Door, self).__init__(
401 SingleShapePhysicser(space, self.shape),
402 ImageRenderer(resources.get_image('objects', 'door.png')),
[186]403 puzzler,
[176]404 )
405
[188]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.