source: nagslang/game_object.py

bugfixes
Last change on this file was 655:baacd0462d8e, checked in by Neil Muller <drnlmuller@…>, 8 years ago

Fix tile animation

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