source: nagslang/game_object.py @ 655:baacd0462d8e

bugfixes
Last change on this file since 655:baacd0462d8e was 655:baacd0462d8e, checked in by Neil Muller <drnlmuller@…>, 7 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.