view nagslang/game_object.py @ 356:582a96e5fdac

Tweaks to the claw attack.
author David Sharpe
date Fri, 06 Sep 2013 18:41:29 +0200
parents 55752fc7b753
children d2c7e17299a7
line wrap: on
line source

import pymunk
import pymunk.pygame_util

import math

from nagslang import environment
from nagslang import puzzle
from nagslang import render
from nagslang.mutators import FLIP_H
from nagslang.constants import (
    COLLISION_TYPE_DOOR, COLLISION_TYPE_FURNITURE, COLLISION_TYPE_PROJECTILE,
    COLLISION_TYPE_SWITCH, COLLISION_TYPE_WEREWOLF_ATTACK,
    SWITCH_PUSHERS, ZORDER_FLOOR, ZORDER_LOW)
from nagslang.resources import resources
from nagslang.events import DoorEvent


def get_editable_game_objects():
    classes = []
    for cls_name, cls in globals().iteritems():
        if isinstance(cls, type) and hasattr(cls, 'requires'):
            classes.append((cls_name, cls))
    return classes


class Physicser(object):
    def __init__(self, space):
        self._space = space

    def get_space(self):
        return self._space

    def set_space(self, new_space):
        self._space = new_space

    def set_game_object(self, game_object):
        self.game_object = game_object

    def get_shape(self):
        raise NotImplementedError()

    def add_to_space(self):
        shape = self.get_shape()
        self.get_space().add(shape)
        if not shape.body.is_static:
            self.get_space().add(shape.body)

    def remove_from_space(self):
        shape = self.get_shape()
        self.get_space().remove(shape)
        if not shape.body.is_static:
            self.get_space().remove(shape.body)

    def get_render_position(self, surface):
        pos = self.get_shape().body.position
        return pymunk.pygame_util.to_pygame(pos, surface)

    def get_angle(self):
        return self.get_shape().body.angle

    def get_velocity(self):
        return self.get_shape().body.velocity

    def _get_position(self):
        return self.get_shape().body.position

    def _set_position(self, position):
        self.get_shape().body.position = position

    position = property(_get_position, _set_position)

    def apply_impulse(self, j, r=(0, 0)):
        return self.get_shape().body.apply_impulse(j, r)


class SingleShapePhysicser(Physicser):
    def __init__(self, space, shape):
        super(SingleShapePhysicser, self).__init__(space)
        self._shape = shape
        shape.physicser = self

    def get_shape(self):
        return self._shape


def damping_velocity_func(body, gravity, damping, dt):
    """Apply custom damping to this body's velocity.
    """
    damping = getattr(body, 'damping', damping)
    return pymunk.Body.update_velocity(body, gravity, damping, dt)


def make_body(mass, moment, position, damping=None):
    body = pymunk.Body(mass, moment)
    body.position = tuple(position)
    if damping is not None:
        body.damping = damping
        body.velocity_func = damping_velocity_func
    return body


class GameObject(object):
    """A representation of a thing in the game world.

    This has a rendery thing, physicsy things and maybe some other things.
    """

    zorder = ZORDER_LOW
    is_moving = False  # `True` if a movement animation should play.

    def __init__(self, physicser, renderer, puzzler=None, overlay=None,
                 interactible=None):
        self.lifetime = 0
        self.physicser = physicser
        physicser.set_game_object(self)
        self.physicser.add_to_space()
        self.renderer = renderer
        renderer.set_game_object(self)
        self.puzzler = puzzler
        if puzzler is not None:
            puzzler.set_game_object(self)
        self.overlay = overlay
        if overlay is not None:
            self.overlay.set_game_object(self)
        self.interactible = interactible
        if interactible is not None:
            self.interactible.set_game_object(self)
        self.remove = False  # If true, will be removed from drawables

    def set_stored_state_dict(self, stored_state):
        """Override this to set up whatever state storage you want.

        The `stored_state` dict passed in contains whatever saved state we
        might have for this object. If the return value of this method
        evaluates to `True`, the contents of the `stored_state` dict will be
        saved, otherwise it will be discarded.
        """
        pass

    def get_space(self):
        return self.physicser.get_space()

    def get_shape(self):
        return self.physicser.get_shape()

    def get_render_position(self, surface):
        return self.physicser.get_render_position(surface)

    def get_render_angle(self):
        return self.physicser.get_angle()

    def get_facing_direction(self):
        """Used by rendererd that care what direction an object is facing.
        """
        return None

    def render(self, surface):
        return self.renderer.render(surface)

    def update(self, dt):
        self.lifetime += dt
        self.renderer.update(dt)

    def hit(self, weapon):
        '''Was hit with a weapon (such as a bullet)'''
        pass

    def collide_with_protagonist(self, protagonist):
        """Called as a `pre_solve` collision callback with the protagonist.

        You can return `False` to ignore the collision, anything else
        (including `None`) to process the collision as normal.
        """
        return True

    def collide_with_furniture(self, furniture):
        return True

    def collide_with_claw_attack(self, claw_attack):
        return True

    @classmethod
    def requires(cls):
        """Hints for the level editor"""
        return [("name", "string")]


class FloorSwitch(GameObject):
    zorder = ZORDER_FLOOR

    def __init__(self, space, position):
        body = make_body(None, None, position)
        self.shape = pymunk.Circle(body, 30)
        self.shape.collision_type = COLLISION_TYPE_SWITCH
        self.shape.sensor = True
        super(FloorSwitch, self).__init__(
            SingleShapePhysicser(space, self.shape),
            render.ImageStateRenderer({
                True: resources.get_image('objects', 'sensor_on.png'),
                False: resources.get_image('objects', 'sensor_off.png'),
            }),
            puzzle.CollidePuzzler(*SWITCH_PUSHERS),
        )

    @classmethod
    def requires(cls):
        return [("name", "string"), ("position", "coordinates")]


class Note(GameObject):
    zorder = ZORDER_FLOOR

    def __init__(self, space, position, message):
        body = make_body(None, None, position)
        self.shape = pymunk.Circle(body, 30)
        self.shape.sensor = True
        super(Note, self).__init__(
            SingleShapePhysicser(space, self.shape),
            render.ImageRenderer(resources.get_image('objects', 'note.png')),
            puzzle.CollidePuzzler(),
            render.TextOverlay(message),
        )

    @classmethod
    def requires(cls):
        return [("name", "string"), ("position", "coordinates"),
                ("message", "text")]


class FloorLight(GameObject):
    zorder = ZORDER_FLOOR

    def __init__(self, space, position, state_source):
        body = make_body(None, None, position)
        self.shape = pymunk.Circle(body, 10)
        self.shape.collision_type = COLLISION_TYPE_SWITCH
        self.shape.sensor = True
        super(FloorLight, self).__init__(
            SingleShapePhysicser(space, self.shape),
            render.ImageStateRenderer({
                True: resources.get_image('objects', 'light_on.png'),
                False: resources.get_image('objects', 'light_off.png'),
            }),
            puzzle.StateProxyPuzzler(state_source),
        )

    @classmethod
    def requires(cls):
        return [("name", "string"), ("position", "coordinates"),
                ("state_source", "puzzler")]


class Box(GameObject):
    def __init__(self, space, position):
        body = make_body(10, 10000, position, damping=0.5)
        self.shape = pymunk.Poly(
            body, [(-20, -20), (20, -20), (20, 20), (-20, 20)])
        self.shape.friction = 0.5
        self.shape.collision_type = COLLISION_TYPE_FURNITURE
        super(Box, self).__init__(
            SingleShapePhysicser(space, self.shape),
            render.ImageRenderer(resources.get_image('objects', 'crate.png')),
        )

    @classmethod
    def requires(cls):
        return [("name", "string"), ("position", "coordinates"),
                ("state_source", "puzzler")]


class Door(GameObject):
    zorder = ZORDER_FLOOR

    def __init__(self, space, position, destination, dest_pos, angle,
                 key_state=None):
        body = make_body(pymunk.inf, pymunk.inf, position, damping=0.5)
        self.shape = pymunk.Circle(body, 30)
        self.shape.collision_type = COLLISION_TYPE_DOOR
        self.shape.body.angle = float(angle) / 180 * math.pi
        self.shape.sensor = True
        self.destination = destination
        self.dest_pos = tuple(dest_pos)
        self._key_state = key_state
        super(Door, self).__init__(
            SingleShapePhysicser(space, self.shape),
            render.ImageStateRenderer({
                True: resources.get_image('objects', 'door.png'),
                # TODO: Locked door image.
                False: resources.get_image('objects', 'door.png'),
            }),
            puzzle.ParentAttrPuzzler('is_open'),
            interactible=environment.Interactible(
                environment.Action(
                    self._post_door_event,
                    environment.FunctionCondition(lambda p: self.is_open))),
        )

    @property
    def is_open(self):
        return self._stored_state['is_open']

    def _post_door_event(self, protagonist):
        DoorEvent.post(self.destination, self.dest_pos)

    def set_stored_state_dict(self, stored_state):
        self._stored_state = stored_state
        if self._key_state is not None:
            # We're lockable, so we start locked and want to save our state.
            self._stored_state.setdefault('is_open', False)
            return True
        # Not lockable, so we're always open and don't bother saving state.
        self._stored_state['is_open'] = True
        return False

    def update(self, dt):
        if not self.is_open:
            self._stored_state['is_open'] = self.puzzler.glue.get_state_of(
                self._key_state)
        super(Door, self).update(dt)

    @classmethod
    def requires(cls):
        return [("name", "string"), ("position", "coordinates"),
                ("destination", "level name"), ("dest_pos", "coordinate"),
                ("angle", "degrees"),
                ("key_state", "puzzler (optional)")]


class Bulkhead(GameObject):
    zorder = ZORDER_FLOOR

    def __init__(self, space, end1, end2, key_state=None):
        body = make_body(None, None, (0, 0))
        self.shape = pymunk.Segment(body, tuple(end1), tuple(end2), 3)
        self.shape.collision_type = COLLISION_TYPE_DOOR
        if key_state is None:
            puzzler = puzzle.YesPuzzler()
        else:
            puzzler = puzzle.StateProxyPuzzler(key_state)
        super(Bulkhead, self).__init__(
            SingleShapePhysicser(space, self.shape),
            render.ShapeStateRenderer(),
            puzzler,
        )

    def collide_with_protagonist(self, protagonist):
        if self.puzzler.get_state():
            # Reject the collision, we can walk through.
            return False
        return True

    collide_with_furniture = collide_with_protagonist

    @classmethod
    def requires(cls):
        return [("name", "string"), ("end1", "coordinates"),
                ("end2", "coordinates"), ("key_state", "puzzler")]


class ToggleSwitch(GameObject):
    zorder = ZORDER_LOW

    def __init__(self, space, position):
        body = make_body(None, None, position)
        self.shape = pymunk.Circle(body, 20)
        self.shape.sensor = True
        super(ToggleSwitch, self).__init__(
            SingleShapePhysicser(space, self.shape),
            render.ImageStateRenderer({
                True: resources.get_image('objects', 'lever.png'),
                False: resources.get_image(
                    'objects', 'lever.png', transforms=(FLIP_H,)),
            }),
            puzzle.ParentAttrPuzzler('toggle_on'),
            interactible=environment.Interactible(
                environment.Action(self._toggle)),
        )

    @property
    def toggle_on(self):
        return self._stored_state['toggle_on']

    def _toggle(self, protagonist):
        self._stored_state['toggle_on'] = not self.toggle_on

    def set_stored_state_dict(self, stored_state):
        self._stored_state = stored_state
        # We start in the "off" position.
        self._stored_state.setdefault('toggle_on', False)
        return True

    @classmethod
    def requires(cls):
        return [("name", "string"), ("position", "coordinates")]


class Bullet(GameObject):
    def __init__(self, space, position, impulse, damage,
                 source_collision_type):
        body = make_body(1, pymunk.inf, position)
        self.last_position = position
        self.shape = pymunk.Circle(body, 2)
        self.shape.sensor = True
        self.shape.collision_type = COLLISION_TYPE_PROJECTILE
        self.damage = damage
        self.source_collision_type = source_collision_type
        super(Bullet, self).__init__(
            SingleShapePhysicser(space, self.shape),
            render.ImageRenderer(resources.get_image('objects', 'bullet.png')),
        )
        self.physicser.apply_impulse(impulse)

    def update(self, dt):
        super(Bullet, self).update(dt)
        position = (self.physicser.position.x, self.physicser.position.y)
        r = self.get_space().segment_query(self.last_position, position)
        self.last_position = position
        for collision in r:
            shape = collision.shape
            if (shape.collision_type == self.source_collision_type
                    or shape == self.physicser.get_shape()
                    or shape.sensor):
                continue
            if hasattr(shape, 'physicser'):
                shape.physicser.game_object.hit(self)
            self.physicser.remove_from_space()
            self.remove = True
            break


class CollectibleGameObject(GameObject):
    zorder = ZORDER_LOW

    def __init__(self, space, name, shape, renderer):
        self._name = name
        shape.sensor = True
        super(CollectibleGameObject, self).__init__(
            SingleShapePhysicser(space, shape),
            renderer,
            interactible=environment.Interactible(
                environment.Action(
                    self._collect, environment.HumanFormCondition())),
        )

    def _collect(self, protagonist):
        protagonist.inventory[self._name] = self
        # TODO: Make this less hacky.
        self.physicser.remove_from_space()
        self.renderer = render.NullRenderer()


class Gun(CollectibleGameObject):
    def __init__(self, space, position):
        body = make_body(None, None, position)
        self.shape = pymunk.Circle(body, 20)
        super(Gun, self).__init__(
            space, 'gun', self.shape,
            render.ImageRenderer(resources.get_image('objects', 'gun.png')),
        )


class ClawAttack(GameObject):
    def __init__(self, space, pos, vector, damage):
        body = make_body(1, pymunk.inf,
                         (pos[0] + (vector.length * math.cos(vector.angle)),
                         pos[1] + (vector.length * math.sin(vector.angle))))
        body.angle = vector.angle
        self.shape = pymunk.Circle(body, 30)
        self.shape.sensor = True
        self.shape.collision_type = COLLISION_TYPE_WEREWOLF_ATTACK
        self.damage = damage
        super(ClawAttack, self).__init__(
            SingleShapePhysicser(space, self.shape),
            render.ImageRenderer(resources.get_image(
                'objects', 'werewolf_SW_claw_attack.png',
                transforms=(FLIP_H,))),
        )

    def update(self, dt):
        super(ClawAttack, self).update(dt)
        if self.lifetime > 0.1:
            self.physicser.remove_from_space()
            self.remove = True


class HostileTerrain(GameObject):
    zorder = ZORDER_FLOOR
    damage = None
    tile = None
    # How often to hit the player
    rate = 5

    def __init__(self, space, position, outline):
        body = make_body(10, pymunk.inf, position)
        # Adjust shape relative to position
        shape_outline = [(p[0] - position[0], p[1] - position[1]) for
                         p in outline]
        self.shape = pymunk.Poly(body, shape_outline)
        self._ticks = 0
        self.shape.collision_type = COLLISION_TYPE_SWITCH
        self.shape.sensor = True
        super(HostileTerrain, self).__init__(
            SingleShapePhysicser(space, self.shape),
            render.TiledRenderer(outline,
                                 resources.get_image('tiles', self.tile)))

    def collide_with_protagonist(self, protagonist):
        # We're called every frame we're colliding, so
        # There are timing issues with stepping on and
        # off terrian, but as long as the rate is reasonably
        # low, they shouldn't impact gameplay
        if self._ticks == 0:
            protagonist.lose_health(self.damage)
        self._ticks += 1
        if self._ticks > self.rate:
            self._ticks = 0

    @classmethod
    def requires(cls):
        return [("name", "string"), ("position", "coordinates"),
                ("outline", "polygon (convex)")]


class AcidFloor(HostileTerrain):
    damage = 1
    tile = 'acid.png'