source: nagslang/game_object.py@ 206:42c565c5ce76

Last change on this file since 206:42c565c5ce76 was 203:917e721f170e, checked in by Stefano Rivera <stefano@…>, 8 years ago

And another puzzler for level-importing

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