source: nagslang/game_object.py @ 370:b5a0081f5784

Last change on this file since 370:b5a0081f5784 was 370:b5a0081f5784, checked in by Stefano Rivera <stefano@…>, 7 years ago

Make the inventory a set

File size: 19.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
10from nagslang.constants import (
11    COLLISION_TYPE_DOOR, COLLISION_TYPE_FURNITURE, COLLISION_TYPE_PROJECTILE,
12    COLLISION_TYPE_SWITCH, COLLISION_TYPE_WEREWOLF_ATTACK,
13    SWITCH_PUSHERS, ZORDER_FLOOR, ZORDER_LOW, ZORDER_HIGH)
14from nagslang.resources import resources
15from nagslang.events import DoorEvent
16
17
18def get_editable_game_objects():
19    classes = []
20    for cls_name, cls in globals().iteritems():
21        if isinstance(cls, type) and hasattr(cls, 'requires'):
22            classes.append((cls_name, cls))
23    return classes
24
25
26class Physicser(object):
27    def __init__(self, space):
28        self._space = space
29
30    def get_space(self):
31        return self._space
32
33    def set_space(self, new_space):
34        self._space = new_space
35
36    def set_game_object(self, game_object):
37        self.game_object = game_object
38
39    def get_shape(self):
40        raise NotImplementedError()
41
42    def add_to_space(self):
43        shape = self.get_shape()
44        self.get_space().add(shape)
45        if not shape.body.is_static:
46            self.get_space().add(shape.body)
47
48    def remove_from_space(self):
49        shape = self.get_shape()
50        self.get_space().remove(shape)
51        if not shape.body.is_static:
52            self.get_space().remove(shape.body)
53
54    def get_render_position(self, surface):
55        pos = self.get_shape().body.position
56        return pymunk.pygame_util.to_pygame(pos, surface)
57
58    def get_angle(self):
59        return self.get_shape().body.angle
60
61    def get_velocity(self):
62        return self.get_shape().body.velocity
63
64    def _get_position(self):
65        return self.get_shape().body.position
66
67    def _set_position(self, position):
68        self.get_shape().body.position = position
69
70    position = property(_get_position, _set_position)
71
72    def apply_impulse(self, j, r=(0, 0)):
73        return self.get_shape().body.apply_impulse(j, r)
74
75
76class SingleShapePhysicser(Physicser):
77    def __init__(self, space, shape):
78        super(SingleShapePhysicser, self).__init__(space)
79        self._shape = shape
80        shape.physicser = self
81
82    def get_shape(self):
83        return self._shape
84
85
86def damping_velocity_func(body, gravity, damping, dt):
87    """Apply custom damping to this body's velocity.
88    """
89    damping = getattr(body, 'damping', damping)
90    return pymunk.Body.update_velocity(body, gravity, damping, dt)
91
92
93def make_body(mass, moment, position, damping=None):
94    body = pymunk.Body(mass, moment)
95    body.position = tuple(position)
96    if damping is not None:
97        body.damping = damping
98        body.velocity_func = damping_velocity_func
99    return body
100
101
102class GameObject(object):
103    """A representation of a thing in the game world.
104
105    This has a rendery thing, physicsy things and maybe some other things.
106    """
107
108    zorder = ZORDER_LOW
109    is_moving = False  # `True` if a movement animation should play.
110
111    def __init__(self, physicser, renderer, puzzler=None, overlay=None,
112                 interactible=None):
113        self.lifetime = 0
114        self.physicser = physicser
115        physicser.set_game_object(self)
116        self.physicser.add_to_space()
117        self.renderer = renderer
118        renderer.set_game_object(self)
119        self.puzzler = puzzler
120        if puzzler is not None:
121            puzzler.set_game_object(self)
122        self.overlay = overlay
123        if overlay is not None:
124            self.overlay.set_game_object(self)
125        self.interactible = interactible
126        if interactible is not None:
127            self.interactible.set_game_object(self)
128        self.remove = False  # If true, will be removed from drawables
129
130    def set_stored_state_dict(self, stored_state):
131        """Override this to set up whatever state storage you want.
132
133        The `stored_state` dict passed in contains whatever saved state we
134        might have for this object. If the return value of this method
135        evaluates to `True`, the contents of the `stored_state` dict will be
136        saved, otherwise it will be discarded.
137        """
138        pass
139
140    def get_space(self):
141        return self.physicser.get_space()
142
143    def get_shape(self):
144        return self.physicser.get_shape()
145
146    def get_render_position(self, surface):
147        return self.physicser.get_render_position(surface)
148
149    def get_render_angle(self):
150        return self.physicser.get_angle()
151
152    def get_facing_direction(self):
153        """Used by rendererd that care what direction an object is facing.
154        """
155        return None
156
157    def render(self, surface):
158        return self.renderer.render(surface)
159
160    def update(self, dt):
161        self.lifetime += dt
162        self.renderer.update(dt)
163
164    def hit(self, weapon):
165        '''Was hit with a weapon (such as a bullet)'''
166        pass
167
168    def collide_with_protagonist(self, protagonist):
169        """Called as a `pre_solve` collision callback with the protagonist.
170
171        You can return `False` to ignore the collision, anything else
172        (including `None`) to process the collision as normal.
173        """
174        return True
175
176    def collide_with_furniture(self, furniture):
177        return True
178
179    def collide_with_claw_attack(self, claw_attack):
180        return True
181
182    @classmethod
183    def requires(cls):
184        """Hints for the level editor"""
185        return [("name", "string")]
186
187
188class FloorSwitch(GameObject):
189    zorder = ZORDER_FLOOR
190
191    def __init__(self, space, position):
192        body = make_body(None, None, position)
193        self.shape = pymunk.Circle(body, 30)
194        self.shape.collision_type = COLLISION_TYPE_SWITCH
195        self.shape.sensor = True
196        super(FloorSwitch, self).__init__(
197            SingleShapePhysicser(space, self.shape),
198            render.ImageStateRenderer({
199                True: resources.get_image('objects', 'sensor_on.png'),
200                False: resources.get_image('objects', 'sensor_off.png'),
201            }),
202            puzzle.CollidePuzzler(*SWITCH_PUSHERS),
203        )
204
205    @classmethod
206    def requires(cls):
207        return [("name", "string"), ("position", "coordinates")]
208
209
210class Note(GameObject):
211    zorder = ZORDER_FLOOR
212
213    def __init__(self, space, position, message):
214        body = make_body(None, None, position)
215        self.shape = pymunk.Circle(body, 30)
216        self.shape.sensor = True
217        super(Note, self).__init__(
218            SingleShapePhysicser(space, self.shape),
219            render.ImageRenderer(resources.get_image('objects', 'note.png')),
220            puzzle.CollidePuzzler(),
221            render.TextOverlay(message),
222        )
223
224    @classmethod
225    def requires(cls):
226        return [("name", "string"), ("position", "coordinates"),
227                ("message", "text")]
228
229
230class FloorLight(GameObject):
231    zorder = ZORDER_FLOOR
232
233    def __init__(self, space, position, state_source):
234        body = make_body(None, None, position)
235        self.shape = pymunk.Circle(body, 10)
236        self.shape.collision_type = COLLISION_TYPE_SWITCH
237        self.shape.sensor = True
238        super(FloorLight, self).__init__(
239            SingleShapePhysicser(space, self.shape),
240            render.ImageStateRenderer({
241                True: resources.get_image('objects', 'light_on.png'),
242                False: resources.get_image('objects', 'light_off.png'),
243            }),
244            puzzle.StateProxyPuzzler(state_source),
245        )
246
247    @classmethod
248    def requires(cls):
249        return [("name", "string"), ("position", "coordinates"),
250                ("state_source", "puzzler")]
251
252
253class Box(GameObject):
254    def __init__(self, space, position):
255        body = make_body(10, 10000, position, damping=0.5)
256        self.shape = pymunk.Poly(
257            body, [(-20, -20), (20, -20), (20, 20), (-20, 20)])
258        self.shape.friction = 0.5
259        self.shape.collision_type = COLLISION_TYPE_FURNITURE
260        super(Box, self).__init__(
261            SingleShapePhysicser(space, self.shape),
262            render.ImageRenderer(resources.get_image('objects', 'crate.png')),
263        )
264
265    @classmethod
266    def requires(cls):
267        return [("name", "string"), ("position", "coordinates"),
268                ("state_source", "puzzler")]
269
270
271class BaseDoor(GameObject):
272    zorder = ZORDER_FLOOR
273    is_open = True
274
275    def __init__(self, space, position, destination, dest_pos, angle,
276                 renderer, condition):
277        body = make_body(pymunk.inf, pymunk.inf, position, damping=0.5)
278        self.shape = pymunk.Circle(body, 30)
279        self.shape.collision_type = COLLISION_TYPE_DOOR
280        self.shape.body.angle = float(angle) / 180 * math.pi
281        self.shape.sensor = True
282        self.destination = destination
283        self.dest_pos = tuple(dest_pos)
284        super(BaseDoor, self).__init__(
285            SingleShapePhysicser(space, self.shape),
286            renderer,
287            puzzle.ParentAttrPuzzler('is_open'),
288            interactible=environment.Interactible(
289                environment.Action(self._post_door_event, condition)),
290        )
291
292    def _post_door_event(self, protagonist):
293        DoorEvent.post(self.destination, self.dest_pos)
294
295
296class Door(BaseDoor):
297    def __init__(self, space, position, destination, dest_pos, angle):
298        super(Door, self).__init__(
299            space, position, destination, dest_pos, angle,
300            render.ImageRenderer(resources.get_image('objects', 'door.png')),
301            environment.YesCondition(),
302        )
303
304    @classmethod
305    def requires(cls):
306        return [("name", "string"), ("position", "coordinates"),
307                ("destination", "level name"), ("dest_pos", "coordinate"),
308                ("angle", "degrees")]
309
310
311class PuzzleDoor(BaseDoor):
312    def __init__(self, space, position, destination, dest_pos, angle,
313                 key_state):
314        self._key_state = key_state
315        super(PuzzleDoor, self).__init__(
316            space, position, destination, dest_pos, angle,
317            render.ImageStateRenderer({
318                True: resources.get_image('objects', 'door.png'),
319                False: resources.get_image('objects', 'locked_door.png'),
320            }),
321            environment.FunctionCondition(lambda p: self.is_open),
322        )
323
324    @property
325    def is_open(self):
326        return self._stored_state['is_open']
327
328    def set_stored_state_dict(self, stored_state):
329        self._stored_state = stored_state
330        self._stored_state.setdefault('is_open', False)
331        return True
332
333    def update(self, dt):
334        if not self.is_open:
335            self._stored_state['is_open'] = self.puzzler.glue.get_state_of(
336                self._key_state)
337        super(PuzzleDoor, self).update(dt)
338
339    @classmethod
340    def requires(cls):
341        return [("name", "string"), ("position", "coordinates"),
342                ("destination", "level name"), ("dest_pos", "coordinate"),
343                ("angle", "degrees"),
344                ("key_state", "puzzler")]
345
346
347class KeyedDoor(BaseDoor):
348    def __init__(self, space, position, destination, dest_pos, angle,
349                 key_item=None):
350        self._key_item = key_item
351        overlay = ImageOverlay(
352            resources.get_image('objects', '%s.png' % (key_item,)))
353        super(KeyedDoor, self).__init__(
354            space, position, destination, dest_pos, angle,
355            render.ImageRenderer(resources.get_image(
356                'objects', 'door.png', transforms=(overlay,))),
357            environment.ItemRequiredCondition(key_item),
358        )
359
360    @classmethod
361    def requires(cls):
362        return [("name", "string"), ("position", "coordinates"),
363                ("destination", "level name"), ("dest_pos", "coordinate"),
364                ("angle", "degrees"), ("key_item", "item name")]
365
366
367class Bulkhead(GameObject):
368    zorder = ZORDER_FLOOR
369
370    def __init__(self, space, end1, end2, key_state=None):
371        body = make_body(None, None, (0, 0))
372        self.shape = pymunk.Segment(body, tuple(end1), tuple(end2), 3)
373        self.shape.collision_type = COLLISION_TYPE_DOOR
374        if key_state is None:
375            puzzler = puzzle.YesPuzzler()
376        else:
377            puzzler = puzzle.StateProxyPuzzler(key_state)
378        super(Bulkhead, self).__init__(
379            SingleShapePhysicser(space, self.shape),
380            render.ShapeStateRenderer(),
381            puzzler,
382        )
383
384    def collide_with_protagonist(self, protagonist):
385        if self.puzzler.get_state():
386            # Reject the collision, we can walk through.
387            return False
388        return True
389
390    collide_with_furniture = collide_with_protagonist
391
392    @classmethod
393    def requires(cls):
394        return [("name", "string"), ("end1", "coordinates"),
395                ("end2", "coordinates"), ("key_state", "puzzler")]
396
397
398class ToggleSwitch(GameObject):
399    zorder = ZORDER_LOW
400
401    def __init__(self, space, position):
402        body = make_body(None, None, position)
403        self.shape = pymunk.Circle(body, 20)
404        self.shape.sensor = True
405        super(ToggleSwitch, self).__init__(
406            SingleShapePhysicser(space, self.shape),
407            render.ImageStateRenderer({
408                True: resources.get_image('objects', 'lever.png'),
409                False: resources.get_image(
410                    'objects', 'lever.png', transforms=(FLIP_H,)),
411            }),
412            puzzle.ParentAttrPuzzler('toggle_on'),
413            interactible=environment.Interactible(
414                environment.Action(self._toggle)),
415        )
416
417    @property
418    def toggle_on(self):
419        return self._stored_state['toggle_on']
420
421    def _toggle(self, protagonist):
422        self._stored_state['toggle_on'] = not self.toggle_on
423
424    def set_stored_state_dict(self, stored_state):
425        self._stored_state = stored_state
426        # We start in the "off" position.
427        self._stored_state.setdefault('toggle_on', False)
428        return True
429
430    @classmethod
431    def requires(cls):
432        return [("name", "string"), ("position", "coordinates")]
433
434
435class Bullet(GameObject):
436    def __init__(self, space, position, impulse, damage, bullet_type,
437                 source_collision_type):
438        body = make_body(1, pymunk.inf, position)
439        body.angle = impulse.angle
440        self.last_position = position
441        self.shape = pymunk.Circle(body, 2)
442        self.shape.sensor = True
443        self.shape.collision_type = COLLISION_TYPE_PROJECTILE
444        self.damage = damage
445        self.type = bullet_type
446        self.source_collision_type = source_collision_type
447        super(Bullet, self).__init__(
448            SingleShapePhysicser(space, self.shape),
449            render.ImageRenderer(resources.get_image(
450                'objects', '%s.png' % self.type)),
451        )
452        self.physicser.apply_impulse(impulse)
453
454    def update(self, dt):
455        super(Bullet, self).update(dt)
456        position = (self.physicser.position.x, self.physicser.position.y)
457        r = self.get_space().segment_query(self.last_position, position)
458        self.last_position = position
459        for collision in r:
460            shape = collision.shape
461            if (shape.collision_type == self.source_collision_type
462                    or shape == self.physicser.get_shape()
463                    or shape.sensor):
464                continue
465            if hasattr(shape, 'physicser'):
466                shape.physicser.game_object.hit(self)
467            self.physicser.remove_from_space()
468            self.remove = True
469            break
470
471
472class CollectibleGameObject(GameObject):
473    zorder = ZORDER_LOW
474
475    def __init__(self, space, name, shape, renderer):
476        self._name = name
477        shape.sensor = True
478        super(CollectibleGameObject, self).__init__(
479            SingleShapePhysicser(space, shape),
480            renderer,
481            interactible=environment.Interactible(
482                environment.Action(
483                    self._collect, environment.HumanFormCondition())),
484        )
485
486    def _collect(self, protagonist):
487        protagonist.inventory.add(self._name)
488        # TODO: Make this less hacky.
489        self.physicser.remove_from_space()
490        self.renderer = render.NullRenderer()
491
492
493class Gun(CollectibleGameObject):
494    def __init__(self, space, position):
495        body = make_body(None, None, position)
496        self.shape = pymunk.Circle(body, 20)
497        super(Gun, self).__init__(
498            space, 'gun', self.shape,
499            render.ImageRenderer(resources.get_image('objects', 'gun.png')),
500        )
501
502
503class ClawAttack(GameObject):
504    def __init__(self, space, pos, vector, damage):
505        body = make_body(1, pymunk.inf,
506                         (pos[0] + (vector.length * math.cos(vector.angle)),
507                          pos[1] + (vector.length * math.sin(vector.angle))))
508        body.angle = vector.angle
509        self.shape = pymunk.Circle(body, 30)
510        self.shape.sensor = True
511        self.shape.collision_type = COLLISION_TYPE_WEREWOLF_ATTACK
512        self.damage = damage
513        super(ClawAttack, self).__init__(
514            SingleShapePhysicser(space, self.shape),
515            render.ImageRenderer(resources.get_image(
516                'objects', 'werewolf_SW_claw_attack.png',
517                transforms=(FLIP_H,))),
518        )
519
520    def update(self, dt):
521        super(ClawAttack, self).update(dt)
522        if self.lifetime > 0.1:
523            self.physicser.remove_from_space()
524            self.remove = True
525
526
527class HostileTerrain(GameObject):
528    zorder = ZORDER_FLOOR
529    damage = None
530    tile = None
531    tile_alpha = 255
532    # How often to hit the player
533    rate = 5
534
535    def __init__(self, space, position, outline):
536        body = make_body(10, pymunk.inf, position)
537        # Adjust shape relative to position
538        shape_outline = [(p[0] - position[0], p[1] - position[1]) for
539                         p in outline]
540        self.shape = pymunk.Poly(body, shape_outline)
541        self._ticks = 0
542        self.shape.collision_type = COLLISION_TYPE_SWITCH
543        self.shape.sensor = True
544        super(HostileTerrain, self).__init__(
545            SingleShapePhysicser(space, self.shape),
546            render.TiledRenderer(outline,
547                                 resources.get_image('tiles', self.tile),
548                                 self.tile_alpha))
549
550    def collide_with_protagonist(self, protagonist):
551        # We're called every frame we're colliding, so
552        # There are timing issues with stepping on and
553        # off terrian, but as long as the rate is reasonably
554        # low, they shouldn't impact gameplay
555        if self._ticks == 0:
556            self.apply_effect(protagonist)
557        self._ticks += 1
558        if self._ticks > self.rate:
559            self._ticks = 0
560
561    def apply_effect(self, protagonist):
562        protagonist.lose_health(self.damage)
563
564    @classmethod
565    def requires(cls):
566        return [("name", "string"), ("position", "coordinates"),
567                ("outline", "polygon (convex)")]
568
569
570class AcidFloor(HostileTerrain):
571    damage = 1
572    tile = 'acid.png'
573
574
575class ForceWolfFloor(HostileTerrain):
576    tile = 'moonlight.png'
577    rate = 0
578    tile_alpha = 150
579    zorder = ZORDER_HIGH
580
581    def apply_effect(self, protagonist):
582        protagonist.force_wolf_form()
583
584
585class KeyCard(CollectibleGameObject):
586    def __init__(self, space, position, name):
587        body = make_body(None, None, position)
588        self.shape = pymunk.Circle(body, 20)
589        super(KeyCard, self).__init__(
590            space, name, self.shape,
591            render.ImageRenderer(
592                resources.get_image('objects', '%s.png' % (name,))),
593        )
594
595    @classmethod
596    def requires(cls):
597        return [("name", "string"), ("position", "coordinates"),
598                ("item_name", "string")]
Note: See TracBrowser for help on using the repository browser.