source: nagslang/game_object.py@ 575:afe748673076

Last change on this file since 575:afe748673076 was 575:afe748673076, checked in by Simon Cross <hodgestar@…>, 9 years ago

Tweak gravity well.

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 * math.sqrt(
753 object_to_move.get_shape().body.mass)
754 movement.length = local_force
755 object_to_move.environmental_movement(movement)
756
757 @classmethod
758 def requires(cls):
759 return [("name", "string"), ("position", "coordinates"),
760 ("radius", "int"), ("force", "int")]
761
762
763class SheepPen(GameObject):
764 zorder = ZORDER_FLOOR
765
766 def __init__(self, space, position, outline, sheep_count):
767 body = make_body(None, None, position)
768 # Adjust shape relative to position
769 shape_outline = [(p[0] - position[0], p[1] - position[1]) for
770 p in outline]
771 self.shape = pymunk.Poly(body, shape_outline)
772 self.shape.collision_type = COLLISION_TYPE_SHEEP_PEN
773 self.shape.sensor = True
774 super(SheepPen, self).__init__(
775 SingleShapePhysicser(space, self.shape),
776 render.Renderer(),
777 puzzle.MultiCollidePuzzler(sheep_count, COLLISION_TYPE_SHEEP),
778 )
779
780 @classmethod
781 def requires(cls):
782 return [("name", "string"), ("position", "coordinates"),
783 ("outline", "polygon (convex)"), ("sheep_count", "int")]
Note: See TracBrowser for help on using the repository browser.