source: nagslang/game_object.py@ 346:282113d86d75

Last change on this file since 346:282113d86d75 was 346:282113d86d75, checked in by Jeremy Thurgood <firxen@…>, 8 years ago

Save door and lever state.

File size: 15.6 KB
Line 
1import pymunk
2import pymunk.pygame_util
3
4import math
5
6from nagslang import environment
7from nagslang import puzzle
8from nagslang import render
9from nagslang.mutators import FLIP_H
10from nagslang.constants import (
11 COLLISION_TYPE_DOOR, COLLISION_TYPE_FURNITURE, COLLISION_TYPE_PROJECTILE,
12 COLLISION_TYPE_SWITCH, COLLISION_TYPE_WEREWOLF_ATTACK,
13 SWITCH_PUSHERS, ZORDER_FLOOR, ZORDER_LOW)
14from nagslang.resources import resources
15from nagslang.events import DoorEvent
16
17
18def get_editable_game_objects():
19 classes = []
20 for cls_name, cls in globals().iteritems():
21 if isinstance(cls, type) and hasattr(cls, 'requires'):
22 classes.append((cls_name, cls))
23 return classes
24
25
26class Physicser(object):
27 def __init__(self, space):
28 self._space = space
29
30 def get_space(self):
31 return self._space
32
33 def set_space(self, new_space):
34 self._space = new_space
35
36 def set_game_object(self, game_object):
37 self.game_object = game_object
38
39 def get_shape(self):
40 raise NotImplementedError()
41
42 def add_to_space(self):
43 shape = self.get_shape()
44 self.get_space().add(shape)
45 if not shape.body.is_static:
46 self.get_space().add(shape.body)
47
48 def remove_from_space(self):
49 shape = self.get_shape()
50 self.get_space().remove(shape)
51 if not shape.body.is_static:
52 self.get_space().remove(shape.body)
53
54 def get_render_position(self, surface):
55 pos = self.get_shape().body.position
56 return pymunk.pygame_util.to_pygame(pos, surface)
57
58 def get_angle(self):
59 return self.get_shape().body.angle
60
61 def get_velocity(self):
62 return self.get_shape().body.velocity
63
64 def _get_position(self):
65 return self.get_shape().body.position
66
67 def _set_position(self, position):
68 self.get_shape().body.position = position
69
70 position = property(_get_position, _set_position)
71
72 def apply_impulse(self, j, r=(0, 0)):
73 return self.get_shape().body.apply_impulse(j, r)
74
75
76class SingleShapePhysicser(Physicser):
77 def __init__(self, space, shape):
78 super(SingleShapePhysicser, self).__init__(space)
79 self._shape = shape
80 shape.physicser = self
81
82 def get_shape(self):
83 return self._shape
84
85
86def damping_velocity_func(body, gravity, damping, dt):
87 """Apply custom damping to this body's velocity.
88 """
89 damping = getattr(body, 'damping', damping)
90 return pymunk.Body.update_velocity(body, gravity, damping, dt)
91
92
93def make_body(mass, moment, position, damping=None):
94 body = pymunk.Body(mass, moment)
95 body.position = tuple(position)
96 if damping is not None:
97 body.damping = damping
98 body.velocity_func = damping_velocity_func
99 return body
100
101
102class GameObject(object):
103 """A representation of a thing in the game world.
104
105 This has a rendery thing, physicsy things and maybe some other things.
106 """
107
108 zorder = ZORDER_LOW
109 is_moving = False # `True` if a movement animation should play.
110
111 def __init__(self, physicser, renderer, puzzler=None, overlay=None,
112 interactible=None):
113 self.lifetime = 0
114 self.physicser = physicser
115 physicser.set_game_object(self)
116 self.physicser.add_to_space()
117 self.renderer = renderer
118 renderer.set_game_object(self)
119 self.puzzler = puzzler
120 if puzzler is not None:
121 puzzler.set_game_object(self)
122 self.overlay = overlay
123 if overlay is not None:
124 self.overlay.set_game_object(self)
125 self.interactible = interactible
126 if interactible is not None:
127 self.interactible.set_game_object(self)
128 self.remove = False # If true, will be removed from drawables
129
130 def set_stored_state_dict(self, stored_state):
131 """Override this to set up whatever state storage you want.
132
133 The `stored_state` dict passed in contains whatever saved state we
134 might have for this object. If the return value of this method
135 evaluates to `True`, the contents of the `stored_state` dict will be
136 saved, otherwise it will be discarded.
137 """
138 pass
139
140 def get_space(self):
141 return self.physicser.get_space()
142
143 def get_shape(self):
144 return self.physicser.get_shape()
145
146 def get_render_position(self, surface):
147 return self.physicser.get_render_position(surface)
148
149 def get_render_angle(self):
150 return self.physicser.get_angle()
151
152 def get_facing_direction(self):
153 """Used by rendererd that care what direction an object is facing.
154 """
155 return None
156
157 def render(self, surface):
158 return self.renderer.render(surface)
159
160 def update(self, dt):
161 self.lifetime += dt
162 self.renderer.update(dt)
163
164 def hit(self, weapon):
165 '''Was hit with a weapon (such as a bullet)'''
166 pass
167
168 def collide_with_protagonist(self, protagonist):
169 """Called as a `pre_solve` collision callback with the protagonist.
170
171 You can return `False` to ignore the collision, anything else
172 (including `None`) to process the collision as normal.
173 """
174 return True
175
176 def collide_with_furniture(self, furniture):
177 return True
178
179 def collide_with_claw_attack(self, claw_attack):
180 return True
181
182 @classmethod
183 def requires(cls):
184 """Hints for the level editor"""
185 return [("name", "string")]
186
187
188class FloorSwitch(GameObject):
189 zorder = ZORDER_FLOOR
190
191 def __init__(self, space, position):
192 body = make_body(None, None, position)
193 self.shape = pymunk.Circle(body, 30)
194 self.shape.collision_type = COLLISION_TYPE_SWITCH
195 self.shape.sensor = True
196 super(FloorSwitch, self).__init__(
197 SingleShapePhysicser(space, self.shape),
198 render.ImageStateRenderer({
199 True: resources.get_image('objects', 'sensor_on.png'),
200 False: resources.get_image('objects', 'sensor_off.png'),
201 }),
202 puzzle.CollidePuzzler(*SWITCH_PUSHERS),
203 )
204
205 @classmethod
206 def requires(cls):
207 return [("name", "string"), ("position", "coordinates")]
208
209
210class Note(GameObject):
211 zorder = ZORDER_FLOOR
212
213 def __init__(self, space, position, message):
214 body = make_body(None, None, position)
215 self.shape = pymunk.Circle(body, 30)
216 self.shape.sensor = True
217 super(Note, self).__init__(
218 SingleShapePhysicser(space, self.shape),
219 render.ImageRenderer(resources.get_image('objects', 'note.png')),
220 puzzle.CollidePuzzler(),
221 render.TextOverlay(message),
222 )
223
224 @classmethod
225 def requires(cls):
226 return [("name", "string"), ("position", "coordinates"),
227 ("message", "text")]
228
229
230class FloorLight(GameObject):
231 zorder = ZORDER_FLOOR
232
233 def __init__(self, space, position, state_source):
234 body = make_body(None, None, position)
235 self.shape = pymunk.Circle(body, 10)
236 self.shape.collision_type = COLLISION_TYPE_SWITCH
237 self.shape.sensor = True
238 super(FloorLight, self).__init__(
239 SingleShapePhysicser(space, self.shape),
240 render.ImageStateRenderer({
241 True: resources.get_image('objects', 'light_on.png'),
242 False: resources.get_image('objects', 'light_off.png'),
243 }),
244 puzzle.StateProxyPuzzler(state_source),
245 )
246
247 @classmethod
248 def requires(cls):
249 return [("name", "string"), ("position", "coordinates"),
250 ("state_source", "puzzler")]
251
252
253class Box(GameObject):
254 def __init__(self, space, position):
255 body = make_body(10, 10000, position, damping=0.5)
256 self.shape = pymunk.Poly(
257 body, [(-20, -20), (20, -20), (20, 20), (-20, 20)])
258 self.shape.friction = 0.5
259 self.shape.collision_type = COLLISION_TYPE_FURNITURE
260 super(Box, self).__init__(
261 SingleShapePhysicser(space, self.shape),
262 render.ImageRenderer(resources.get_image('objects', 'crate.png')),
263 )
264
265 @classmethod
266 def requires(cls):
267 return [("name", "string"), ("position", "coordinates"),
268 ("state_source", "puzzler")]
269
270
271class Door(GameObject):
272 zorder = ZORDER_FLOOR
273
274 def __init__(self, space, position, destination, dest_pos, angle,
275 key_state=None):
276 body = make_body(pymunk.inf, pymunk.inf, position, damping=0.5)
277 self.shape = pymunk.Circle(body, 30)
278 self.shape.collision_type = COLLISION_TYPE_DOOR
279 self.shape.body.angle = float(angle) / 180 * math.pi
280 self.shape.sensor = True
281 self.destination = destination
282 self.dest_pos = tuple(dest_pos)
283 self._key_state = key_state
284 super(Door, self).__init__(
285 SingleShapePhysicser(space, self.shape),
286 render.ImageStateRenderer({
287 True: resources.get_image('objects', 'door.png'),
288 # TODO: Locked door image.
289 False: resources.get_image('objects', 'door.png'),
290 }),
291 puzzle.ParentAttrPuzzler('is_open'),
292 interactible=environment.Interactible(
293 environment.Action(
294 self._post_door_event,
295 environment.FunctionCondition(lambda p: self.is_open))),
296 )
297
298 @property
299 def is_open(self):
300 return self._stored_state['is_open']
301
302 def _post_door_event(self, protagonist):
303 DoorEvent.post(self.destination, self.dest_pos)
304
305 def set_stored_state_dict(self, stored_state):
306 self._stored_state = stored_state
307 if self._key_state is not None:
308 # We're lockable, so we start locked and want to save our state.
309 self._stored_state.setdefault('is_open', False)
310 return True
311 # Not lockable, so we're always open and don't bother saving state.
312 self._stored_state['is_open'] = True
313 return False
314
315 def update(self, dt):
316 if not self.is_open:
317 self._stored_state['is_open'] = self.puzzler.glue.get_state_of(
318 self._key_state)
319 super(Door, self).update(dt)
320
321 @classmethod
322 def requires(cls):
323 return [("name", "string"), ("position", "coordinates"),
324 ("destination", "level name"), ("dest_pos", "coordinate"),
325 ("angle", "degrees"),
326 ("key_state", "puzzler (optional)")]
327
328
329class Bulkhead(GameObject):
330 zorder = ZORDER_FLOOR
331
332 def __init__(self, space, end1, end2, key_state=None):
333 body = make_body(None, None, (0, 0))
334 self.shape = pymunk.Segment(body, tuple(end1), tuple(end2), 3)
335 self.shape.collision_type = COLLISION_TYPE_DOOR
336 if key_state is None:
337 puzzler = puzzle.YesPuzzler()
338 else:
339 puzzler = puzzle.StateProxyPuzzler(key_state)
340 super(Bulkhead, self).__init__(
341 SingleShapePhysicser(space, self.shape),
342 render.ShapeStateRenderer(),
343 puzzler,
344 )
345
346 def collide_with_protagonist(self, protagonist):
347 if self.puzzler.get_state():
348 # Reject the collision, we can walk through.
349 return False
350 return True
351
352 collide_with_furniture = collide_with_protagonist
353
354 @classmethod
355 def requires(cls):
356 return [("name", "string"), ("end1", "coordinates"),
357 ("end2", "coordinates"), ("key_state", "puzzler")]
358
359
360class ToggleSwitch(GameObject):
361 zorder = ZORDER_LOW
362
363 def __init__(self, space, position):
364 body = make_body(None, None, position)
365 self.shape = pymunk.Circle(body, 20)
366 self.shape.sensor = True
367 super(ToggleSwitch, self).__init__(
368 SingleShapePhysicser(space, self.shape),
369 render.ImageStateRenderer({
370 True: resources.get_image('objects', 'lever.png'),
371 False: resources.get_image(
372 'objects', 'lever.png', transforms=(FLIP_H,)),
373 }),
374 puzzle.ParentAttrPuzzler('toggle_on'),
375 interactible=environment.Interactible(
376 environment.Action(self._toggle)),
377 )
378
379 @property
380 def toggle_on(self):
381 return self._stored_state['toggle_on']
382
383 def _toggle(self, protagonist):
384 self._stored_state['toggle_on'] = not self.toggle_on
385
386 def set_stored_state_dict(self, stored_state):
387 self._stored_state = stored_state
388 # We start in the "off" position.
389 self._stored_state.setdefault('toggle_on', False)
390 return True
391
392 @classmethod
393 def requires(cls):
394 return [("name", "string"), ("position", "coordinates")]
395
396
397class Bullet(GameObject):
398 def __init__(self, space, position, impulse, damage,
399 source_collision_type):
400 body = make_body(1, pymunk.inf, position)
401 self.last_position = position
402 self.shape = pymunk.Circle(body, 2)
403 self.shape.sensor = True
404 self.shape.collision_type = COLLISION_TYPE_PROJECTILE
405 self.damage = damage
406 self.source_collision_type = source_collision_type
407 super(Bullet, self).__init__(
408 SingleShapePhysicser(space, self.shape),
409 render.ImageRenderer(resources.get_image('objects', 'bullet.png')),
410 )
411 self.physicser.apply_impulse(impulse)
412
413 def update(self, dt):
414 super(Bullet, self).update(dt)
415 position = (self.physicser.position.x, self.physicser.position.y)
416 r = self.get_space().segment_query(self.last_position, position)
417 self.last_position = position
418 for collision in r:
419 shape = collision.shape
420 if (shape.collision_type == self.source_collision_type
421 or shape == self.physicser.get_shape()
422 or shape.sensor):
423 continue
424 if hasattr(shape, 'physicser'):
425 shape.physicser.game_object.hit(self)
426 self.physicser.remove_from_space()
427 self.remove = True
428 break
429
430
431class CollectibleGameObject(GameObject):
432 zorder = ZORDER_LOW
433
434 def __init__(self, space, name, shape, renderer):
435 self._name = name
436 shape.sensor = True
437 super(CollectibleGameObject, self).__init__(
438 SingleShapePhysicser(space, shape),
439 renderer,
440 interactible=environment.Interactible(
441 environment.Action(
442 self._collect, environment.HumanFormCondition())),
443 )
444
445 def _collect(self, protagonist):
446 protagonist.inventory[self._name] = self
447 # TODO: Make this less hacky.
448 self.physicser.remove_from_space()
449 self.renderer = render.NullRenderer()
450
451
452class Gun(CollectibleGameObject):
453 def __init__(self, space, position):
454 body = make_body(None, None, position)
455 self.shape = pymunk.Circle(body, 20)
456 super(Gun, self).__init__(
457 space, 'gun', self.shape,
458 render.ImageRenderer(resources.get_image('objects', 'gun.png')),
459 )
460
461
462class ClawAttack(GameObject):
463 def __init__(self, space, position, vector, damage):
464 body = make_body(1, pymunk.inf, position)
465 body.angle = vector.angle
466 self.shape = pymunk.Circle(body, 30)
467 self.shape.sensor = True
468 self.shape.collision_type = COLLISION_TYPE_WEREWOLF_ATTACK
469 self.damage = damage
470 super(ClawAttack, self).__init__(
471 SingleShapePhysicser(space, self.shape),
472 render.ImageRenderer(resources.get_image(
473 'objects', 'werewolf_SW_claw_attack.png',
474 transforms=(FLIP_H,))),
475 )
476 self.physicser.apply_impulse(vector)
477
478 def update(self, dt):
479 super(ClawAttack, self).update(dt)
480 if self.lifetime > 0.1:
481 self.physicser.remove_from_space()
482 self.remove = True
Note: See TracBrowser for help on using the repository browser.