source: nagslang/game_object.py@ 474:9775055ba2f0

Last change on this file since 474:9775055ba2f0 was 474:9775055ba2f0, checked in by Jeremy Thurgood <firxen@…>, 9 years ago

Prettier bulkheads.

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