source: nagslang/game_object.py @ 570:3c7593a84b06

Last change on this file since 570:3c7593a84b06 was 570:3c7593a84b06, checked in by Neil Muller <drnlmuller@…>, 7 years ago

Make hatches movable

File size: 25.3 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        if physicser is not None:
160            physicser.set_game_object(self)
161            self.physicser.add_to_space()
162        self.renderer = renderer
163        renderer.set_game_object(self)
164        self.puzzler = puzzler
165        if puzzler is not None:
166            puzzler.set_game_object(self)
167        self.overlay = overlay
168        if overlay is not None:
169            self.overlay.set_game_object(self)
170        self.interactible = interactible
171        if interactible is not None:
172            self.interactible.set_game_object(self)
173        self._timers = {}
174        self._active_timers = {}
175
176    def add_timer(self, name, secs):
177        self._timers[name] = secs
178
179    def start_timer(self, name, secs=None):
180        if secs is None:
181            secs = self._timers[name]
182        self._active_timers[name] = secs
183
184    def check_timer(self, name):
185        return name in self._active_timers
186
187    def set_stored_state_dict(self, stored_state):
188        """Override this to set up whatever state storage you want.
189
190        The `stored_state` dict passed in contains whatever saved state we
191        might have for this object. If the return value of this method
192        evaluates to `True`, the contents of the `stored_state` dict will be
193        saved, otherwise it will be discarded.
194        """
195        pass
196
197    def get_space(self):
198        return self.physicser.get_space()
199
200    def get_shape(self):
201        return self.physicser.get_shape()
202
203    def get_render_position(self, surface):
204        return self.physicser.get_render_position(surface)
205
206    def get_render_angle(self):
207        return self.physicser.get_angle()
208
209    def get_facing_direction(self):
210        """Used by rendererd that care what direction an object is facing.
211        """
212        return None
213
214    def render(self, surface):
215        return self.renderer.render(surface)
216
217    def update(self, dt):
218        self.lifetime += dt
219        for timer in self._active_timers.keys():
220            self._active_timers[timer] -= dt
221            if self._active_timers[timer] <= 0:
222                self._active_timers.pop(timer)
223        self.renderer.update(dt)
224
225    def hit(self, weapon):
226        '''Was hit with a weapon (such as a bullet)'''
227        pass
228
229    def collide_with_protagonist(self, protagonist):
230        """Called as a `pre_solve` collision callback with the protagonist.
231
232        You can return `False` to ignore the collision, anything else
233        (including `None`) to process the collision as normal.
234        """
235        return True
236
237    def collide_with_furniture(self, furniture):
238        return True
239
240    def collide_with_claw_attack(self, claw_attack):
241        return True
242
243    def environmental_movement(self, vec):
244        self.physicser.apply_impulse(vec)
245
246    @classmethod
247    def requires(cls):
248        """Hints for the level editor"""
249        return [("name", "string")]
250
251    @classmethod
252    def movable(cls):
253        # Are we movable
254        hints = cls.requires()
255        for x in hints:
256            if 'position' in x:
257                return True
258        return False
259
260
261class FloorSwitch(GameObject):
262    zorder = ZORDER_FLOOR
263
264    def __init__(self, space, position):
265        body = make_body(None, None, position)
266        self.shape = pymunk.Circle(body, 30)
267        self.shape.collision_type = COLLISION_TYPE_SWITCH
268        self.shape.sensor = True
269        super(FloorSwitch, self).__init__(
270            SingleShapePhysicser(space, self.shape),
271            render.ImageStateRenderer({
272                True: resources.get_image('objects', 'sensor_on.png'),
273                False: resources.get_image('objects', 'sensor_off.png'),
274            }),
275            puzzle.CollidePuzzler(*SWITCH_PUSHERS),
276        )
277
278    @classmethod
279    def requires(cls):
280        return [("name", "string"), ("position", "coordinates")]
281
282
283class Note(GameObject):
284    zorder = ZORDER_FLOOR
285
286    def __init__(self, space, position, message):
287        body = make_body(None, None, position)
288        self.shape = pymunk.Circle(body, 30)
289        self.shape.sensor = True
290        super(Note, self).__init__(
291            SingleShapePhysicser(space, self.shape),
292            render.ImageRenderer(resources.get_image('objects', 'note.png')),
293            puzzle.CollidePuzzler(),
294            render.TextOverlay(message),
295        )
296
297    @classmethod
298    def requires(cls):
299        return [("name", "string"), ("position", "coordinates"),
300                ("message", "text")]
301
302
303class EphemeralNote(GameObject):
304    def __init__(self, message, timeout, **kwargs):
305        kwargs.setdefault('bg_colour', (255, 180, 180, 192))
306        super(EphemeralNote, self).__init__(
307            None,
308            render.NullRenderer(),
309            puzzle.YesPuzzler(),
310            render.TextOverlay(message, **kwargs),
311        )
312        self.add_timer('timeout', timeout)
313        self.start_timer('timeout')
314
315    def update(self, dt):
316        super(EphemeralNote, self).update(dt)
317        if not self.check_timer('timeout'):
318            return Result(remove=[self])
319
320
321class FloorLight(GameObject):
322    zorder = ZORDER_FLOOR
323
324    def __init__(self, space, position, state_source):
325        body = make_body(None, None, position)
326        self.shape = pymunk.Circle(body, 10)
327        self.shape.collision_type = COLLISION_TYPE_SWITCH
328        self.shape.sensor = True
329        super(FloorLight, self).__init__(
330            SingleShapePhysicser(space, self.shape),
331            render.ImageStateRenderer({
332                True: resources.get_image('objects', 'light_on.png'),
333                False: resources.get_image('objects', 'light_off.png'),
334            }),
335            puzzle.StateProxyPuzzler(state_source),
336        )
337
338    @classmethod
339    def requires(cls):
340        return [("name", "string"), ("position", "coordinates"),
341                ("state_source", "puzzler")]
342
343
344class Box(GameObject):
345    def __init__(self, space, position):
346        body = make_body(10, 10000, position, damping=0.5)
347        self.shape = pymunk.Poly(
348            body, [(-20, -20), (20, -20), (20, 20), (-20, 20)])
349        self.shape.friction = 0.5
350        self.shape.collision_type = COLLISION_TYPE_FURNITURE
351        super(Box, self).__init__(
352            SingleShapePhysicser(space, self.shape),
353            render.ImageRenderer(resources.get_image('objects', 'crate.png')),
354        )
355
356    @classmethod
357    def requires(cls):
358        return [("name", "string"), ("position", "coordinates"),
359                ("state_source", "puzzler")]
360
361
362class SokoBox(GameObject):
363    def __init__(self, space, position):
364        body = make_body(1, pymunk.inf, position, 0.2)
365        self.shape = pymunk.Poly(
366            body, [(-40, -40), (40, -40), (40, 40), (-40, 40)])
367        self.shape.friction = 0.5
368        self.shape.collision_type = COLLISION_TYPE_FURNITURE
369        super(SokoBox, self).__init__(
370            SingleShapePhysicser(space, self.shape),
371            render.ImageRenderer(
372                resources.get_image('objects', 'sokobox.png')),
373        )
374
375    @classmethod
376    def requires(cls):
377        return [("name", "string"), ("position", "coordinates"),
378                ("state_source", "puzzler")]
379
380
381class BaseDoor(GameObject):
382    zorder = ZORDER_FLOOR
383    is_open = True
384
385    def __init__(self, space, position, destination, dest_pos, angle,
386                 renderer, condition):
387        body = make_body(pymunk.inf, pymunk.inf, position, damping=0.5)
388        self.shape = pymunk.Circle(body, 30)
389        self.shape.collision_type = COLLISION_TYPE_DOOR
390        self.shape.body.angle = float(angle) / 180 * math.pi
391        self.shape.sensor = True
392        self.destination = destination
393        self.dest_pos = tuple(dest_pos)
394        super(BaseDoor, self).__init__(
395            SingleShapePhysicser(space, self.shape),
396            renderer,
397            puzzle.ParentAttrPuzzler('is_open'),
398            interactible=environment.Interactible(
399                environment.Action(self._post_door_event, condition)),
400        )
401
402    def _post_door_event(self, protagonist):
403        self.door_opened()
404        DoorEvent.post(self.destination, self.dest_pos)
405
406    def door_opened(self):
407        pass
408
409
410class Door(BaseDoor):
411    def __init__(self, space, position, destination, dest_pos, angle):
412        super(Door, self).__init__(
413            space, position, destination, dest_pos, angle,
414            render.ImageRenderer(resources.get_image('objects', 'door.png')),
415            environment.YesCondition(),
416        )
417
418    @classmethod
419    def requires(cls):
420        return [("name", "string"), ("position", "coordinates"),
421                ("destination", "level name"), ("dest_pos", "coordinate"),
422                ("angle", "degrees")]
423
424
425class RestartGameDoor(Door):
426    def _post_door_event(self, protagonist):
427        protagonist.world.reset()
428        super(RestartGameDoor, self)._post_door_event(protagonist)
429
430
431class ContinueGameDoor(Door):
432    def _post_door_event(self, protagonist):
433        world = protagonist.world
434        if world.level[0]:
435            DoorEvent.post(world.level[0], world.level[1])
436        else:
437            # New game?
438            super(ContinueGameDoor, self)._post_door_event(protagonist)
439
440
441def make_overlay_image(image_name, angle):
442    transforms = ()
443    if angle != 0:
444        transforms = (rotator(-angle),)
445    return resources.get_image('objects', image_name, transforms=transforms)
446
447
448class PuzzleDoor(BaseDoor):
449    def __init__(self, space, position, destination, dest_pos, angle,
450                 key_state):
451        self._key_state = key_state
452        overlay = ImageOverlay(make_overlay_image('lock.png', angle))
453        super(PuzzleDoor, self).__init__(
454            space, position, destination, dest_pos, angle,
455            render.ImageStateRenderer({
456                True: resources.get_image('objects', 'door.png'),
457                False: resources.get_image(
458                    'objects', 'door.png', transforms=(overlay,)),
459            }),
460            environment.FunctionCondition(lambda p: self.is_open),
461        )
462
463    @property
464    def is_open(self):
465        if self._stored_state['is_open']:
466            return True
467        return self.puzzler.glue.get_state_of(self._key_state)
468
469    def door_opened(self):
470        self._stored_state['is_open'] = True
471
472    def set_stored_state_dict(self, stored_state):
473        self._stored_state = stored_state
474        self._stored_state.setdefault('is_open', False)
475        return True
476
477    @classmethod
478    def requires(cls):
479        return [("name", "string"), ("position", "coordinates"),
480                ("destination", "level name"), ("dest_pos", "coordinate"),
481                ("angle", "degrees"),
482                ("key_state", "puzzler")]
483
484
485class KeyedDoor(BaseDoor):
486    def __init__(self, space, position, destination, dest_pos, angle,
487                 key_item=None):
488        self._key_item = key_item
489        overlay = ImageOverlay(
490            make_overlay_image('%s.png' % (key_item,), angle))
491        super(KeyedDoor, self).__init__(
492            space, position, destination, dest_pos, angle,
493            render.ImageRenderer(resources.get_image(
494                'objects', 'door.png', transforms=(overlay,))),
495            environment.ItemRequiredCondition(key_item),
496        )
497
498    @classmethod
499    def requires(cls):
500        return [("name", "string"), ("position", "coordinates"),
501                ("destination", "level name"), ("dest_pos", "coordinate"),
502                ("angle", "degrees"), ("key_item", "item name")]
503
504
505class Hatch(GameObject):
506    zorder = ZORDER_FLOOR
507
508    def __init__(self, space, end1, end2, key_state=None):
509        body = make_body(None, None, (0, 0))
510        self.shape = pymunk.Segment(body, tuple(end1), tuple(end2), 7)
511        self.shape.collision_type = COLLISION_TYPE_DOOR
512        if key_state is None:
513            puzzler = puzzle.YesPuzzler()
514        else:
515            puzzler = puzzle.StateProxyPuzzler(key_state)
516        super(Hatch, self).__init__(
517            SingleShapePhysicser(space, self.shape),
518            render.HatchRenderer(),
519            puzzler,
520        )
521
522    def collide_with_protagonist(self, protagonist):
523        if self.puzzler.get_state():
524            # Reject the collision, we can walk through.
525            return False
526        return True
527
528    collide_with_furniture = collide_with_protagonist
529
530    @classmethod
531    def requires(cls):
532        return [("name", "string"), ("end1", "coordinates"),
533                ("end2", "coordinates"), ("key_state", "puzzler")]
534
535    # The level knows that bulkheads are magical
536    @classmethod
537    def movable(cls):
538        return True
539
540
541class ToggleSwitch(GameObject):
542    zorder = ZORDER_LOW
543
544    def __init__(self, space, position):
545        body = make_body(None, None, position)
546        self.shape = pymunk.Circle(body, 20)
547        self.shape.sensor = True
548        super(ToggleSwitch, self).__init__(
549            SingleShapePhysicser(space, self.shape),
550            render.ImageStateRenderer({
551                True: resources.get_image('objects', 'lever.png'),
552                False: resources.get_image(
553                    'objects', 'lever.png', transforms=(FLIP_H,)),
554            }),
555            puzzle.ParentAttrPuzzler('toggle_on'),
556            interactible=environment.Interactible(
557                environment.Action(self._toggle)),
558        )
559
560    @property
561    def toggle_on(self):
562        return self._stored_state['toggle_on']
563
564    def _toggle(self, protagonist):
565        self._stored_state['toggle_on'] = not self.toggle_on
566
567    def set_stored_state_dict(self, stored_state):
568        self._stored_state = stored_state
569        # We start in the "off" position.
570        self._stored_state.setdefault('toggle_on', False)
571        return True
572
573    @classmethod
574    def requires(cls):
575        return [("name", "string"), ("position", "coordinates")]
576
577
578class Bullet(GameObject):
579    def __init__(self, space, position, impulse, damage, bullet_type,
580                 source_collision_type):
581        body = make_body(1, pymunk.inf, position)
582        body.angle = impulse.angle
583        self.last_position = position
584        self.shape = pymunk.Circle(body, 2)
585        self.shape.sensor = True
586        self.shape.collision_type = COLLISION_TYPE_PROJECTILE
587        self.damage = damage
588        self.type = bullet_type
589        self.source_collision_type = source_collision_type
590        super(Bullet, self).__init__(
591            SingleShapePhysicser(space, self.shape),
592            render.ImageRenderer(resources.get_image(
593                'objects', '%s.png' % self.type)),
594        )
595        self.physicser.apply_impulse(impulse)
596
597    def update(self, dt):
598        super(Bullet, self).update(dt)
599        position = (self.physicser.position.x, self.physicser.position.y)
600        r = self.get_space().segment_query(self.last_position, position)
601        self.last_position = position
602        for collision in r:
603            shape = collision.shape
604            if (shape.collision_type == self.source_collision_type
605                    or shape == self.physicser.get_shape()
606                    or shape.sensor):
607                continue
608            if hasattr(shape, 'physicser'):
609                shape.physicser.game_object.hit(self)
610            self.physicser.remove_from_space()
611            return Result(remove=[self])
612
613
614class ClawAttack(GameObject):
615    def __init__(self, space, pos, vector, damage):
616        body = make_body(1, pymunk.inf,
617                         (pos[0] + (vector.length * math.cos(vector.angle)),
618                          pos[1] + (vector.length * math.sin(vector.angle))))
619        body.angle = vector.angle
620        self.shape = pymunk.Circle(body, 30)
621        self.shape.sensor = True
622        self.shape.collision_type = COLLISION_TYPE_WEREWOLF_ATTACK
623        self.damage = damage
624        super(ClawAttack, self).__init__(
625            SingleShapePhysicser(space, self.shape),
626            render.ImageRenderer(resources.get_image(
627                'objects', 'werewolf_SW_claw_attack.png',
628                transforms=(FLIP_H,))),
629        )
630
631    def update(self, dt):
632        super(ClawAttack, self).update(dt)
633        if self.lifetime > 0.1:
634            self.physicser.remove_from_space()
635            return Result(remove=[self])
636
637
638class HostileTerrain(GameObject):
639    zorder = ZORDER_FLOOR
640    damage = None
641    tiles = []
642    tile_alpha = 255
643    tile_frame_ticks = 3
644    # How often to hit the player
645    rate = 5
646
647    def __init__(self, space, position, outline):
648        body = make_body(10, pymunk.inf, position)
649        # Adjust shape relative to position
650        shape_outline = [(p[0] - position[0], p[1] - position[1]) for
651                         p in outline]
652        self.shape = pymunk.Poly(body, shape_outline)
653        self._ticks = 0
654        self.shape.collision_type = COLLISION_TYPE_SWITCH
655        self.shape.sensor = True
656        renderer = self._fix_image(outline)
657        super(HostileTerrain, self).__init__(
658            SingleShapePhysicser(space, self.shape),
659            renderer)
660
661    def _fix_image(self, outline):
662        if len(self.tiles) > 1:
663            tile_images = [resources.get_image('tiles', x)
664                           for x in self.tiles]
665            renderer = render.TimedTiledRenderer(outline, tile_images,
666                                                 self.tile_frame_ticks,
667                                                 self.tile_alpha)
668        else:
669            tile_image = resources.get_image('tiles', self.tiles[0])
670            renderer = render.TiledRenderer(outline, tile_image,
671                                            self.tile_alpha)
672        return renderer
673
674    def update_image(self, new_outline):
675        self.renderer = self._fix_image(new_outline)
676
677    def collide_with_protagonist(self, protagonist):
678        # We're called every frame we're colliding, so
679        # There are timing issues with stepping on and
680        # off terrian, but as long as the rate is reasonably
681        # low, they shouldn't impact gameplay
682        if self._ticks == 0:
683            self.apply_effect(protagonist)
684        self._ticks += 1
685        if self._ticks > self.rate:
686            self._ticks = 0
687
688    def apply_effect(self, protagonist):
689        protagonist.lose_health(self.damage)
690
691    @classmethod
692    def requires(cls):
693        return [("name", "string"), ("position", "coordinates"),
694                ("outline", "polygon (convex)")]
695
696
697class AcidFloor(HostileTerrain):
698    damage = 1
699    tiles = ['acid.png', 'acid2.png', 'acid3.png']
700    tile_alpha = 200
701    tile_frame_ticks = 10
702
703
704class ForceWolfFloor(HostileTerrain):
705    tiles = ['moonlight.png']
706    rate = 0
707    tile_alpha = 150
708    zorder = ZORDER_HIGH
709
710    def apply_effect(self, protagonist):
711        protagonist.force_wolf_form()
712
713
714class GravityWell(GameObject):
715    zorder = ZORDER_FLOOR
716    # How often to hit the player
717    rate = 5
718
719    def __init__(self, space, position, radius, force):
720        body = make_body(None, None, position)
721        # Adjust shape relative to position
722        self._radius = radius
723        self.shape = pymunk.Circle(body, radius)
724        self.centre = pymunk.Circle(body, 10)
725        self.centre.friction = pymunk.inf
726        self._ticks = 0
727        self.force = force
728        self.shape.collision_type = COLLISION_TYPE_SWITCH
729        self.shape.sensor = True
730        super(GravityWell, self).__init__(
731            MultiShapePhysicser(space, self.shape, self.centre),
732            render.ImageRenderer(resources.get_image(
733                'objects', 'gravity_well.png')),
734        )
735
736    def collide_with_protagonist(self, protagonist):
737        # We're called every frame we're colliding, so
738        # There are timing issues with stepping on and
739        # off terrian, but as long as the rate is reasonably
740        # low, they shouldn't impact gameplay
741        self.apply_effect(protagonist)
742
743    def collide_with_furniture(self, furniture):
744        # We're called every frame we're colliding, so
745        # There are timing issues with stepping on and
746        # off terrian, but as long as the rate is reasonably
747        # low, they shouldn't impact gameplay
748        self.apply_effect(furniture)
749
750    def apply_effect(self, object_to_move):
751        movement = self.physicser.position - object_to_move.physicser.position
752        local_force = self.force * (1 - (self._radius / movement.length))
753        movement.length = local_force
754        object_to_move.environmental_movement(-movement)
755
756    @classmethod
757    def requires(cls):
758        return [("name", "string"), ("position", "coordinates"),
759                ("radius", "int"), ("force", "int")]
760
761
762class SheepPen(GameObject):
763    zorder = ZORDER_FLOOR
764
765    def __init__(self, space, position, outline, sheep_count):
766        body = make_body(None, None, position)
767        # Adjust shape relative to position
768        shape_outline = [(p[0] - position[0], p[1] - position[1]) for
769                         p in outline]
770        self.shape = pymunk.Poly(body, shape_outline)
771        self.shape.collision_type = COLLISION_TYPE_SHEEP_PEN
772        self.shape.sensor = True
773        super(SheepPen, self).__init__(
774            SingleShapePhysicser(space, self.shape),
775            render.Renderer(),
776            puzzle.MultiCollidePuzzler(sheep_count, COLLISION_TYPE_SHEEP),
777        )
778
779    @classmethod
780    def requires(cls):
781        return [("name", "string"), ("position", "coordinates"),
782                ("outline", "polygon (convex)"), ("sheep_count", "int")]
Note: See TracBrowser for help on using the repository browser.