source: nagslang/game_object.py@ 390:52c94435e38b

Last change on this file since 390:52c94435e38b was 390:52c94435e38b, checked in by Jeremy Thurgood <firxen@…>, 8 years ago

Goodbye foul locked_door!

File size: 18.7 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, ImageOverlay
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, ZORDER_HIGH)
14from nagslang.resources import resources
15from nagslang.events import DoorEvent
16
17
18class Result(object):
19 '''
20 Return from an update() function, to add new objects to the world, and/or
21 remove old objects.
22 '''
23 def __init__(self, add=(), remove=()):
24 self.add = add
25 self.remove = remove
26
27
28def get_editable_game_objects():
29 classes = []
30 for cls_name, cls in globals().iteritems():
31 if isinstance(cls, type) and hasattr(cls, 'requires'):
32 classes.append((cls_name, cls))
33 return classes
34
35
36class Physicser(object):
37 def __init__(self, space):
38 self._space = space
39
40 def get_space(self):
41 return self._space
42
43 def set_space(self, new_space):
44 self._space = new_space
45
46 def set_game_object(self, game_object):
47 self.game_object = game_object
48
49 def get_shape(self):
50 raise NotImplementedError()
51
52 def add_to_space(self):
53 shape = self.get_shape()
54 self.get_space().add(shape)
55 if not shape.body.is_static:
56 self.get_space().add(shape.body)
57
58 def remove_from_space(self):
59 shape = self.get_shape()
60 self.get_space().remove(shape)
61 if not shape.body.is_static:
62 self.get_space().remove(shape.body)
63
64 def get_render_position(self, surface):
65 pos = self.get_shape().body.position
66 return pymunk.pygame_util.to_pygame(pos, surface)
67
68 def get_angle(self):
69 return self.get_shape().body.angle
70
71 def get_velocity(self):
72 return self.get_shape().body.velocity
73
74 def _get_position(self):
75 return self.get_shape().body.position
76
77 def _set_position(self, position):
78 self.get_shape().body.position = position
79
80 position = property(_get_position, _set_position)
81
82 def apply_impulse(self, j, r=(0, 0)):
83 return self.get_shape().body.apply_impulse(j, r)
84
85
86class SingleShapePhysicser(Physicser):
87 def __init__(self, space, shape):
88 super(SingleShapePhysicser, self).__init__(space)
89 self._shape = shape
90 shape.physicser = self
91
92 def get_shape(self):
93 return self._shape
94
95
96def damping_velocity_func(body, gravity, damping, dt):
97 """Apply custom damping to this body's velocity.
98 """
99 damping = getattr(body, 'damping', damping)
100 return pymunk.Body.update_velocity(body, gravity, damping, dt)
101
102
103def make_body(mass, moment, position, damping=None):
104 body = pymunk.Body(mass, moment)
105 body.position = tuple(position)
106 if damping is not None:
107 body.damping = damping
108 body.velocity_func = damping_velocity_func
109 return body
110
111
112class GameObject(object):
113 """A representation of a thing in the game world.
114
115 This has a rendery thing, physicsy things and maybe some other things.
116 """
117
118 zorder = ZORDER_LOW
119 is_moving = False # `True` if a movement animation should play.
120
121 def __init__(self, physicser, renderer, puzzler=None, overlay=None,
122 interactible=None):
123 self.lifetime = 0
124 self.physicser = physicser
125 physicser.set_game_object(self)
126 self.physicser.add_to_space()
127 self.renderer = renderer
128 renderer.set_game_object(self)
129 self.puzzler = puzzler
130 if puzzler is not None:
131 puzzler.set_game_object(self)
132 self.overlay = overlay
133 if overlay is not None:
134 self.overlay.set_game_object(self)
135 self.interactible = interactible
136 if interactible is not None:
137 self.interactible.set_game_object(self)
138 self._timers = {}
139 self._active_timers = {}
140
141 def add_timer(self, name, secs):
142 self._timers[name] = secs
143
144 def start_timer(self, name, secs=None):
145 if secs is None:
146 secs = self._timers[name]
147 self._active_timers[name] = secs
148
149 def check_timer(self, name):
150 return name in self._active_timers
151
152 def set_stored_state_dict(self, stored_state):
153 """Override this to set up whatever state storage you want.
154
155 The `stored_state` dict passed in contains whatever saved state we
156 might have for this object. If the return value of this method
157 evaluates to `True`, the contents of the `stored_state` dict will be
158 saved, otherwise it will be discarded.
159 """
160 pass
161
162 def get_space(self):
163 return self.physicser.get_space()
164
165 def get_shape(self):
166 return self.physicser.get_shape()
167
168 def get_render_position(self, surface):
169 return self.physicser.get_render_position(surface)
170
171 def get_render_angle(self):
172 return self.physicser.get_angle()
173
174 def get_facing_direction(self):
175 """Used by rendererd that care what direction an object is facing.
176 """
177 return None
178
179 def render(self, surface):
180 return self.renderer.render(surface)
181
182 def update(self, dt):
183 self.lifetime += dt
184 for timer in self._active_timers.keys():
185 self._active_timers[timer] -= dt
186 if self._active_timers[timer] <= 0:
187 self._active_timers.pop(timer)
188 self.renderer.update(dt)
189
190 def hit(self, weapon):
191 '''Was hit with a weapon (such as a bullet)'''
192 pass
193
194 def collide_with_protagonist(self, protagonist):
195 """Called as a `pre_solve` collision callback with the protagonist.
196
197 You can return `False` to ignore the collision, anything else
198 (including `None`) to process the collision as normal.
199 """
200 return True
201
202 def collide_with_furniture(self, furniture):
203 return True
204
205 def collide_with_claw_attack(self, claw_attack):
206 return True
207
208 @classmethod
209 def requires(cls):
210 """Hints for the level editor"""
211 return [("name", "string")]
212
213
214class FloorSwitch(GameObject):
215 zorder = ZORDER_FLOOR
216
217 def __init__(self, space, position):
218 body = make_body(None, None, position)
219 self.shape = pymunk.Circle(body, 30)
220 self.shape.collision_type = COLLISION_TYPE_SWITCH
221 self.shape.sensor = True
222 super(FloorSwitch, self).__init__(
223 SingleShapePhysicser(space, self.shape),
224 render.ImageStateRenderer({
225 True: resources.get_image('objects', 'sensor_on.png'),
226 False: resources.get_image('objects', 'sensor_off.png'),
227 }),
228 puzzle.CollidePuzzler(*SWITCH_PUSHERS),
229 )
230
231 @classmethod
232 def requires(cls):
233 return [("name", "string"), ("position", "coordinates")]
234
235
236class Note(GameObject):
237 zorder = ZORDER_FLOOR
238
239 def __init__(self, space, position, message):
240 body = make_body(None, None, position)
241 self.shape = pymunk.Circle(body, 30)
242 self.shape.sensor = True
243 super(Note, self).__init__(
244 SingleShapePhysicser(space, self.shape),
245 render.ImageRenderer(resources.get_image('objects', 'note.png')),
246 puzzle.CollidePuzzler(),
247 render.TextOverlay(message),
248 )
249
250 @classmethod
251 def requires(cls):
252 return [("name", "string"), ("position", "coordinates"),
253 ("message", "text")]
254
255
256class FloorLight(GameObject):
257 zorder = ZORDER_FLOOR
258
259 def __init__(self, space, position, state_source):
260 body = make_body(None, None, position)
261 self.shape = pymunk.Circle(body, 10)
262 self.shape.collision_type = COLLISION_TYPE_SWITCH
263 self.shape.sensor = True
264 super(FloorLight, self).__init__(
265 SingleShapePhysicser(space, self.shape),
266 render.ImageStateRenderer({
267 True: resources.get_image('objects', 'light_on.png'),
268 False: resources.get_image('objects', 'light_off.png'),
269 }),
270 puzzle.StateProxyPuzzler(state_source),
271 )
272
273 @classmethod
274 def requires(cls):
275 return [("name", "string"), ("position", "coordinates"),
276 ("state_source", "puzzler")]
277
278
279class Box(GameObject):
280 def __init__(self, space, position):
281 body = make_body(10, 10000, position, damping=0.5)
282 self.shape = pymunk.Poly(
283 body, [(-20, -20), (20, -20), (20, 20), (-20, 20)])
284 self.shape.friction = 0.5
285 self.shape.collision_type = COLLISION_TYPE_FURNITURE
286 super(Box, self).__init__(
287 SingleShapePhysicser(space, self.shape),
288 render.ImageRenderer(resources.get_image('objects', 'crate.png')),
289 )
290
291 @classmethod
292 def requires(cls):
293 return [("name", "string"), ("position", "coordinates"),
294 ("state_source", "puzzler")]
295
296
297class BaseDoor(GameObject):
298 zorder = ZORDER_FLOOR
299 is_open = True
300
301 def __init__(self, space, position, destination, dest_pos, angle,
302 renderer, condition):
303 body = make_body(pymunk.inf, pymunk.inf, position, damping=0.5)
304 self.shape = pymunk.Circle(body, 30)
305 self.shape.collision_type = COLLISION_TYPE_DOOR
306 self.shape.body.angle = float(angle) / 180 * math.pi
307 self.shape.sensor = True
308 self.destination = destination
309 self.dest_pos = tuple(dest_pos)
310 super(BaseDoor, self).__init__(
311 SingleShapePhysicser(space, self.shape),
312 renderer,
313 puzzle.ParentAttrPuzzler('is_open'),
314 interactible=environment.Interactible(
315 environment.Action(self._post_door_event, condition)),
316 )
317
318 def _post_door_event(self, protagonist):
319 DoorEvent.post(self.destination, self.dest_pos)
320
321
322class Door(BaseDoor):
323 def __init__(self, space, position, destination, dest_pos, angle):
324 super(Door, self).__init__(
325 space, position, destination, dest_pos, angle,
326 render.ImageRenderer(resources.get_image('objects', 'door.png')),
327 environment.YesCondition(),
328 )
329
330 @classmethod
331 def requires(cls):
332 return [("name", "string"), ("position", "coordinates"),
333 ("destination", "level name"), ("dest_pos", "coordinate"),
334 ("angle", "degrees")]
335
336
337class PuzzleDoor(BaseDoor):
338 def __init__(self, space, position, destination, dest_pos, angle,
339 key_state):
340 self._key_state = key_state
341 overlay = ImageOverlay(resources.get_image('objects', 'lock.png'))
342 super(PuzzleDoor, self).__init__(
343 space, position, destination, dest_pos, angle,
344 render.ImageStateRenderer({
345 True: resources.get_image('objects', 'door.png'),
346 False: resources.get_image(
347 'objects', 'door.png', transforms=(overlay,)),
348 }),
349 environment.FunctionCondition(lambda p: self.is_open),
350 )
351
352 @property
353 def is_open(self):
354 return self._stored_state['is_open']
355
356 def set_stored_state_dict(self, stored_state):
357 self._stored_state = stored_state
358 self._stored_state.setdefault('is_open', False)
359 return True
360
361 def update(self, dt):
362 if not self.is_open:
363 self._stored_state['is_open'] = self.puzzler.glue.get_state_of(
364 self._key_state)
365 super(PuzzleDoor, self).update(dt)
366
367 @classmethod
368 def requires(cls):
369 return [("name", "string"), ("position", "coordinates"),
370 ("destination", "level name"), ("dest_pos", "coordinate"),
371 ("angle", "degrees"),
372 ("key_state", "puzzler")]
373
374
375class KeyedDoor(BaseDoor):
376 def __init__(self, space, position, destination, dest_pos, angle,
377 key_item=None):
378 self._key_item = key_item
379 overlay = ImageOverlay(
380 resources.get_image('objects', '%s.png' % (key_item,)))
381 super(KeyedDoor, self).__init__(
382 space, position, destination, dest_pos, angle,
383 render.ImageRenderer(resources.get_image(
384 'objects', 'door.png', transforms=(overlay,))),
385 environment.ItemRequiredCondition(key_item),
386 )
387
388 @classmethod
389 def requires(cls):
390 return [("name", "string"), ("position", "coordinates"),
391 ("destination", "level name"), ("dest_pos", "coordinate"),
392 ("angle", "degrees"), ("key_item", "item name")]
393
394
395class Bulkhead(GameObject):
396 zorder = ZORDER_FLOOR
397
398 def __init__(self, space, end1, end2, key_state=None):
399 body = make_body(None, None, (0, 0))
400 self.shape = pymunk.Segment(body, tuple(end1), tuple(end2), 3)
401 self.shape.collision_type = COLLISION_TYPE_DOOR
402 if key_state is None:
403 puzzler = puzzle.YesPuzzler()
404 else:
405 puzzler = puzzle.StateProxyPuzzler(key_state)
406 super(Bulkhead, self).__init__(
407 SingleShapePhysicser(space, self.shape),
408 render.ShapeStateRenderer(),
409 puzzler,
410 )
411
412 def collide_with_protagonist(self, protagonist):
413 if self.puzzler.get_state():
414 # Reject the collision, we can walk through.
415 return False
416 return True
417
418 collide_with_furniture = collide_with_protagonist
419
420 @classmethod
421 def requires(cls):
422 return [("name", "string"), ("end1", "coordinates"),
423 ("end2", "coordinates"), ("key_state", "puzzler")]
424
425
426class ToggleSwitch(GameObject):
427 zorder = ZORDER_LOW
428
429 def __init__(self, space, position):
430 body = make_body(None, None, position)
431 self.shape = pymunk.Circle(body, 20)
432 self.shape.sensor = True
433 super(ToggleSwitch, self).__init__(
434 SingleShapePhysicser(space, self.shape),
435 render.ImageStateRenderer({
436 True: resources.get_image('objects', 'lever.png'),
437 False: resources.get_image(
438 'objects', 'lever.png', transforms=(FLIP_H,)),
439 }),
440 puzzle.ParentAttrPuzzler('toggle_on'),
441 interactible=environment.Interactible(
442 environment.Action(self._toggle)),
443 )
444
445 @property
446 def toggle_on(self):
447 return self._stored_state['toggle_on']
448
449 def _toggle(self, protagonist):
450 self._stored_state['toggle_on'] = not self.toggle_on
451
452 def set_stored_state_dict(self, stored_state):
453 self._stored_state = stored_state
454 # We start in the "off" position.
455 self._stored_state.setdefault('toggle_on', False)
456 return True
457
458 @classmethod
459 def requires(cls):
460 return [("name", "string"), ("position", "coordinates")]
461
462
463class Bullet(GameObject):
464 def __init__(self, space, position, impulse, damage, bullet_type,
465 source_collision_type):
466 body = make_body(1, pymunk.inf, position)
467 body.angle = impulse.angle
468 self.last_position = position
469 self.shape = pymunk.Circle(body, 2)
470 self.shape.sensor = True
471 self.shape.collision_type = COLLISION_TYPE_PROJECTILE
472 self.damage = damage
473 self.type = bullet_type
474 self.source_collision_type = source_collision_type
475 super(Bullet, self).__init__(
476 SingleShapePhysicser(space, self.shape),
477 render.ImageRenderer(resources.get_image(
478 'objects', '%s.png' % self.type)),
479 )
480 self.physicser.apply_impulse(impulse)
481
482 def update(self, dt):
483 super(Bullet, self).update(dt)
484 position = (self.physicser.position.x, self.physicser.position.y)
485 r = self.get_space().segment_query(self.last_position, position)
486 self.last_position = position
487 for collision in r:
488 shape = collision.shape
489 if (shape.collision_type == self.source_collision_type
490 or shape == self.physicser.get_shape()
491 or shape.sensor):
492 continue
493 if hasattr(shape, 'physicser'):
494 shape.physicser.game_object.hit(self)
495 self.physicser.remove_from_space()
496 return Result(remove=[self])
497
498
499class ClawAttack(GameObject):
500 def __init__(self, space, pos, vector, damage):
501 body = make_body(1, pymunk.inf,
502 (pos[0] + (vector.length * math.cos(vector.angle)),
503 pos[1] + (vector.length * math.sin(vector.angle))))
504 body.angle = vector.angle
505 self.shape = pymunk.Circle(body, 30)
506 self.shape.sensor = True
507 self.shape.collision_type = COLLISION_TYPE_WEREWOLF_ATTACK
508 self.damage = damage
509 super(ClawAttack, self).__init__(
510 SingleShapePhysicser(space, self.shape),
511 render.ImageRenderer(resources.get_image(
512 'objects', 'werewolf_SW_claw_attack.png',
513 transforms=(FLIP_H,))),
514 )
515
516 def update(self, dt):
517 super(ClawAttack, self).update(dt)
518 if self.lifetime > 0.1:
519 self.physicser.remove_from_space()
520 return Result(remove=[self])
521
522
523class HostileTerrain(GameObject):
524 zorder = ZORDER_FLOOR
525 damage = None
526 tile = None
527 tile_alpha = 255
528 # How often to hit the player
529 rate = 5
530
531 def __init__(self, space, position, outline):
532 body = make_body(10, pymunk.inf, position)
533 # Adjust shape relative to position
534 shape_outline = [(p[0] - position[0], p[1] - position[1]) for
535 p in outline]
536 self.shape = pymunk.Poly(body, shape_outline)
537 self._ticks = 0
538 self.shape.collision_type = COLLISION_TYPE_SWITCH
539 self.shape.sensor = True
540 super(HostileTerrain, self).__init__(
541 SingleShapePhysicser(space, self.shape),
542 render.TiledRenderer(outline,
543 resources.get_image('tiles', self.tile),
544 self.tile_alpha))
545
546 def collide_with_protagonist(self, protagonist):
547 # We're called every frame we're colliding, so
548 # There are timing issues with stepping on and
549 # off terrian, but as long as the rate is reasonably
550 # low, they shouldn't impact gameplay
551 if self._ticks == 0:
552 self.apply_effect(protagonist)
553 self._ticks += 1
554 if self._ticks > self.rate:
555 self._ticks = 0
556
557 def apply_effect(self, protagonist):
558 protagonist.lose_health(self.damage)
559
560 @classmethod
561 def requires(cls):
562 return [("name", "string"), ("position", "coordinates"),
563 ("outline", "polygon (convex)")]
564
565
566class AcidFloor(HostileTerrain):
567 damage = 1
568 tile = 'acid.png'
569 tile_alpha = 200
570
571
572class ForceWolfFloor(HostileTerrain):
573 tile = 'moonlight.png'
574 rate = 0
575 tile_alpha = 150
576 zorder = ZORDER_HIGH
577
578 def apply_effect(self, protagonist):
579 protagonist.force_wolf_form()
Note: See TracBrowser for help on using the repository browser.