source: nagslang/game_object.py@ 519:ddd86cb25945

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

Updated hangar.

File size: 23.8 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, rotator
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 @classmethod
251 def movable(cls):
252 # Are we movable
253 hints = cls.requires()
254 for x in hints:
255 if 'position' in x:
256 return True
257 return False
258
259
260class FloorSwitch(GameObject):
261 zorder = ZORDER_FLOOR
262
263 def __init__(self, space, position):
264 body = make_body(None, None, position)
265 self.shape = pymunk.Circle(body, 30)
266 self.shape.collision_type = COLLISION_TYPE_SWITCH
267 self.shape.sensor = True
268 super(FloorSwitch, self).__init__(
269 SingleShapePhysicser(space, self.shape),
270 render.ImageStateRenderer({
271 True: resources.get_image('objects', 'sensor_on.png'),
272 False: resources.get_image('objects', 'sensor_off.png'),
273 }),
274 puzzle.CollidePuzzler(*SWITCH_PUSHERS),
275 )
276
277 @classmethod
278 def requires(cls):
279 return [("name", "string"), ("position", "coordinates")]
280
281
282class Note(GameObject):
283 zorder = ZORDER_FLOOR
284
285 def __init__(self, space, position, message):
286 body = make_body(None, None, position)
287 self.shape = pymunk.Circle(body, 30)
288 self.shape.sensor = True
289 super(Note, self).__init__(
290 SingleShapePhysicser(space, self.shape),
291 render.ImageRenderer(resources.get_image('objects', 'note.png')),
292 puzzle.CollidePuzzler(),
293 render.TextOverlay(message),
294 )
295
296 @classmethod
297 def requires(cls):
298 return [("name", "string"), ("position", "coordinates"),
299 ("message", "text")]
300
301
302class FloorLight(GameObject):
303 zorder = ZORDER_FLOOR
304
305 def __init__(self, space, position, state_source):
306 body = make_body(None, None, position)
307 self.shape = pymunk.Circle(body, 10)
308 self.shape.collision_type = COLLISION_TYPE_SWITCH
309 self.shape.sensor = True
310 super(FloorLight, self).__init__(
311 SingleShapePhysicser(space, self.shape),
312 render.ImageStateRenderer({
313 True: resources.get_image('objects', 'light_on.png'),
314 False: resources.get_image('objects', 'light_off.png'),
315 }),
316 puzzle.StateProxyPuzzler(state_source),
317 )
318
319 @classmethod
320 def requires(cls):
321 return [("name", "string"), ("position", "coordinates"),
322 ("state_source", "puzzler")]
323
324
325class Box(GameObject):
326 def __init__(self, space, position):
327 body = make_body(10, 10000, position, damping=0.5)
328 self.shape = pymunk.Poly(
329 body, [(-20, -20), (20, -20), (20, 20), (-20, 20)])
330 self.shape.friction = 0.5
331 self.shape.collision_type = COLLISION_TYPE_FURNITURE
332 super(Box, self).__init__(
333 SingleShapePhysicser(space, self.shape),
334 render.ImageRenderer(resources.get_image('objects', 'crate.png')),
335 )
336
337 @classmethod
338 def requires(cls):
339 return [("name", "string"), ("position", "coordinates"),
340 ("state_source", "puzzler")]
341
342
343class BaseDoor(GameObject):
344 zorder = ZORDER_FLOOR
345 is_open = True
346
347 def __init__(self, space, position, destination, dest_pos, angle,
348 renderer, condition):
349 body = make_body(pymunk.inf, pymunk.inf, position, damping=0.5)
350 self.shape = pymunk.Circle(body, 30)
351 self.shape.collision_type = COLLISION_TYPE_DOOR
352 self.shape.body.angle = float(angle) / 180 * math.pi
353 self.shape.sensor = True
354 self.destination = destination
355 self.dest_pos = tuple(dest_pos)
356 super(BaseDoor, self).__init__(
357 SingleShapePhysicser(space, self.shape),
358 renderer,
359 puzzle.ParentAttrPuzzler('is_open'),
360 interactible=environment.Interactible(
361 environment.Action(self._post_door_event, condition)),
362 )
363
364 def _post_door_event(self, protagonist):
365 DoorEvent.post(self.destination, self.dest_pos)
366
367
368class Door(BaseDoor):
369 def __init__(self, space, position, destination, dest_pos, angle):
370 super(Door, self).__init__(
371 space, position, destination, dest_pos, angle,
372 render.ImageRenderer(resources.get_image('objects', 'door.png')),
373 environment.YesCondition(),
374 )
375
376 @classmethod
377 def requires(cls):
378 return [("name", "string"), ("position", "coordinates"),
379 ("destination", "level name"), ("dest_pos", "coordinate"),
380 ("angle", "degrees")]
381
382
383class RestartGameDoor(Door):
384 def _post_door_event(self, protagonist):
385 protagonist.world.reset()
386 super(RestartGameDoor, self)._post_door_event(protagonist)
387
388
389class ContinueGameDoor(Door):
390 def _post_door_event(self, protagonist):
391 world = protagonist.world
392 if world.level[0]:
393 DoorEvent.post(world.level[0], world.level[1])
394 else:
395 # New game?
396 super(ContinueGameDoor, self)._post_door_event(protagonist)
397
398
399def make_overlay_image(image_name, angle):
400 transforms = ()
401 if angle != 0:
402 transforms = (rotator(-angle),)
403 return resources.get_image('objects', image_name, transforms=transforms)
404
405
406class PuzzleDoor(BaseDoor):
407 def __init__(self, space, position, destination, dest_pos, angle,
408 key_state):
409 self._key_state = key_state
410 overlay = ImageOverlay(make_overlay_image('lock.png', angle))
411 super(PuzzleDoor, self).__init__(
412 space, position, destination, dest_pos, angle,
413 render.ImageStateRenderer({
414 True: resources.get_image('objects', 'door.png'),
415 False: resources.get_image(
416 'objects', 'door.png', transforms=(overlay,)),
417 }),
418 environment.FunctionCondition(lambda p: self.is_open),
419 )
420
421 @property
422 def is_open(self):
423 return self._stored_state['is_open']
424
425 def set_stored_state_dict(self, stored_state):
426 self._stored_state = stored_state
427 self._stored_state.setdefault('is_open', False)
428 return True
429
430 def update(self, dt):
431 if not self.is_open:
432 self._stored_state['is_open'] = self.puzzler.glue.get_state_of(
433 self._key_state)
434 super(PuzzleDoor, self).update(dt)
435
436 @classmethod
437 def requires(cls):
438 return [("name", "string"), ("position", "coordinates"),
439 ("destination", "level name"), ("dest_pos", "coordinate"),
440 ("angle", "degrees"),
441 ("key_state", "puzzler")]
442
443
444class KeyedDoor(BaseDoor):
445 def __init__(self, space, position, destination, dest_pos, angle,
446 key_item=None):
447 self._key_item = key_item
448 overlay = ImageOverlay(
449 make_overlay_image('%s.png' % (key_item,), angle))
450 super(KeyedDoor, self).__init__(
451 space, position, destination, dest_pos, angle,
452 render.ImageRenderer(resources.get_image(
453 'objects', 'door.png', transforms=(overlay,))),
454 environment.ItemRequiredCondition(key_item),
455 )
456
457 @classmethod
458 def requires(cls):
459 return [("name", "string"), ("position", "coordinates"),
460 ("destination", "level name"), ("dest_pos", "coordinate"),
461 ("angle", "degrees"), ("key_item", "item name")]
462
463
464class Bulkhead(GameObject):
465 zorder = ZORDER_FLOOR
466
467 def __init__(self, space, end1, end2, key_state=None):
468 body = make_body(None, None, (0, 0))
469 self.shape = pymunk.Segment(body, tuple(end1), tuple(end2), 7)
470 self.shape.collision_type = COLLISION_TYPE_DOOR
471 if key_state is None:
472 puzzler = puzzle.YesPuzzler()
473 else:
474 puzzler = puzzle.StateProxyPuzzler(key_state)
475 super(Bulkhead, self).__init__(
476 SingleShapePhysicser(space, self.shape),
477 render.BulkheadRenderer(),
478 puzzler,
479 )
480
481 def collide_with_protagonist(self, protagonist):
482 if self.puzzler.get_state():
483 # Reject the collision, we can walk through.
484 return False
485 return True
486
487 collide_with_furniture = collide_with_protagonist
488
489 @classmethod
490 def requires(cls):
491 return [("name", "string"), ("end1", "coordinates"),
492 ("end2", "coordinates"), ("key_state", "puzzler")]
493
494
495class ToggleSwitch(GameObject):
496 zorder = ZORDER_LOW
497
498 def __init__(self, space, position):
499 body = make_body(None, None, position)
500 self.shape = pymunk.Circle(body, 20)
501 self.shape.sensor = True
502 super(ToggleSwitch, self).__init__(
503 SingleShapePhysicser(space, self.shape),
504 render.ImageStateRenderer({
505 True: resources.get_image('objects', 'lever.png'),
506 False: resources.get_image(
507 'objects', 'lever.png', transforms=(FLIP_H,)),
508 }),
509 puzzle.ParentAttrPuzzler('toggle_on'),
510 interactible=environment.Interactible(
511 environment.Action(self._toggle)),
512 )
513
514 @property
515 def toggle_on(self):
516 return self._stored_state['toggle_on']
517
518 def _toggle(self, protagonist):
519 self._stored_state['toggle_on'] = not self.toggle_on
520
521 def set_stored_state_dict(self, stored_state):
522 self._stored_state = stored_state
523 # We start in the "off" position.
524 self._stored_state.setdefault('toggle_on', False)
525 return True
526
527 @classmethod
528 def requires(cls):
529 return [("name", "string"), ("position", "coordinates")]
530
531
532class Bullet(GameObject):
533 def __init__(self, space, position, impulse, damage, bullet_type,
534 source_collision_type):
535 body = make_body(1, pymunk.inf, position)
536 body.angle = impulse.angle
537 self.last_position = position
538 self.shape = pymunk.Circle(body, 2)
539 self.shape.sensor = True
540 self.shape.collision_type = COLLISION_TYPE_PROJECTILE
541 self.damage = damage
542 self.type = bullet_type
543 self.source_collision_type = source_collision_type
544 super(Bullet, self).__init__(
545 SingleShapePhysicser(space, self.shape),
546 render.ImageRenderer(resources.get_image(
547 'objects', '%s.png' % self.type)),
548 )
549 self.physicser.apply_impulse(impulse)
550
551 def update(self, dt):
552 super(Bullet, self).update(dt)
553 position = (self.physicser.position.x, self.physicser.position.y)
554 r = self.get_space().segment_query(self.last_position, position)
555 self.last_position = position
556 for collision in r:
557 shape = collision.shape
558 if (shape.collision_type == self.source_collision_type
559 or shape == self.physicser.get_shape()
560 or shape.sensor):
561 continue
562 if hasattr(shape, 'physicser'):
563 shape.physicser.game_object.hit(self)
564 self.physicser.remove_from_space()
565 return Result(remove=[self])
566
567
568class ClawAttack(GameObject):
569 def __init__(self, space, pos, vector, damage):
570 body = make_body(1, pymunk.inf,
571 (pos[0] + (vector.length * math.cos(vector.angle)),
572 pos[1] + (vector.length * math.sin(vector.angle))))
573 body.angle = vector.angle
574 self.shape = pymunk.Circle(body, 30)
575 self.shape.sensor = True
576 self.shape.collision_type = COLLISION_TYPE_WEREWOLF_ATTACK
577 self.damage = damage
578 super(ClawAttack, self).__init__(
579 SingleShapePhysicser(space, self.shape),
580 render.ImageRenderer(resources.get_image(
581 'objects', 'werewolf_SW_claw_attack.png',
582 transforms=(FLIP_H,))),
583 )
584
585 def update(self, dt):
586 super(ClawAttack, self).update(dt)
587 if self.lifetime > 0.1:
588 self.physicser.remove_from_space()
589 return Result(remove=[self])
590
591
592class HostileTerrain(GameObject):
593 zorder = ZORDER_FLOOR
594 damage = None
595 tiles = []
596 tile_alpha = 255
597 tile_frame_ticks = 3
598 # How often to hit the player
599 rate = 5
600
601 def __init__(self, space, position, outline):
602 body = make_body(10, pymunk.inf, position)
603 # Adjust shape relative to position
604 shape_outline = [(p[0] - position[0], p[1] - position[1]) for
605 p in outline]
606 self.shape = pymunk.Poly(body, shape_outline)
607 self._ticks = 0
608 self.shape.collision_type = COLLISION_TYPE_SWITCH
609 self.shape.sensor = True
610 if len(self.tiles) > 1:
611 tile_images = [resources.get_image('tiles', x)
612 for x in self.tiles]
613 renderer = render.TimedTiledRenderer(outline, tile_images,
614 self.tile_frame_ticks,
615 self.tile_alpha)
616 else:
617 tile_image = resources.get_image('tiles', self.tiles[0])
618 renderer = render.TiledRenderer(outline, tile_image,
619 self.tile_alpha)
620 super(HostileTerrain, self).__init__(
621 SingleShapePhysicser(space, self.shape),
622 renderer)
623
624 def collide_with_protagonist(self, protagonist):
625 # We're called every frame we're colliding, so
626 # There are timing issues with stepping on and
627 # off terrian, but as long as the rate is reasonably
628 # low, they shouldn't impact gameplay
629 if self._ticks == 0:
630 self.apply_effect(protagonist)
631 self._ticks += 1
632 if self._ticks > self.rate:
633 self._ticks = 0
634
635 def apply_effect(self, protagonist):
636 protagonist.lose_health(self.damage)
637
638 @classmethod
639 def requires(cls):
640 return [("name", "string"), ("position", "coordinates"),
641 ("outline", "polygon (convex)")]
642
643
644class AcidFloor(HostileTerrain):
645 damage = 1
646 tiles = ['acid.png', 'acid2.png', 'acid3.png']
647 tile_alpha = 200
648 tile_frame_ticks = 10
649
650
651class ForceWolfFloor(HostileTerrain):
652 tiles = ['moonlight.png']
653 rate = 0
654 tile_alpha = 150
655 zorder = ZORDER_HIGH
656
657 def apply_effect(self, protagonist):
658 protagonist.force_wolf_form()
659
660
661class GravityWell(GameObject):
662 zorder = ZORDER_FLOOR
663 # How often to hit the player
664 rate = 5
665
666 def __init__(self, space, position, radius, force):
667 body = make_body(None, None, position)
668 # Adjust shape relative to position
669 self._radius = radius
670 self.shape = pymunk.Circle(body, radius)
671 self.centre = pymunk.Circle(body, 10)
672 self.centre.friction = pymunk.inf
673 self._ticks = 0
674 self.force = force
675 self.shape.collision_type = COLLISION_TYPE_SWITCH
676 self.shape.sensor = True
677 super(GravityWell, self).__init__(
678 MultiShapePhysicser(space, self.shape, self.centre),
679 render.ImageRenderer(resources.get_image(
680 'objects', 'gravity_well.png')),
681 )
682
683 def collide_with_protagonist(self, protagonist):
684 # We're called every frame we're colliding, so
685 # There are timing issues with stepping on and
686 # off terrian, but as long as the rate is reasonably
687 # low, they shouldn't impact gameplay
688 self.apply_effect(protagonist)
689
690 def collide_with_furniture(self, furniture):
691 # We're called every frame we're colliding, so
692 # There are timing issues with stepping on and
693 # off terrian, but as long as the rate is reasonably
694 # low, they shouldn't impact gameplay
695 self.apply_effect(furniture)
696
697 def apply_effect(self, object_to_move):
698 movement = self.physicser.position - object_to_move.physicser.position
699 local_force = self.force * (1 - (self._radius / movement.length))
700 movement.length = local_force
701 object_to_move.environmental_movement(-movement)
702
703 @classmethod
704 def requires(cls):
705 return [("name", "string"), ("position", "coordinates"),
706 ("outline", "polygon (convex)")]
707
708
709class SheepPen(GameObject):
710 zorder = ZORDER_FLOOR
711
712 def __init__(self, space, position, outline, sheep_count):
713 body = make_body(None, None, position)
714 # Adjust shape relative to position
715 shape_outline = [(p[0] - position[0], p[1] - position[1]) for
716 p in outline]
717 self.shape = pymunk.Poly(body, shape_outline)
718 self.shape.collision_type = COLLISION_TYPE_SHEEP_PEN
719 self.shape.sensor = True
720 super(SheepPen, self).__init__(
721 SingleShapePhysicser(space, self.shape),
722 render.Renderer(),
723 puzzle.MultiCollidePuzzler(sheep_count, COLLISION_TYPE_SHEEP),
724 )
725
726 @classmethod
727 def requires(cls):
728 return [("name", "string"), ("position", "coordinates"),
729 ("outline", "polygon (convex)"), ("sheep_count", "int")]
Note: See TracBrowser for help on using the repository browser.