view nagslang/game_object.py @ 235:831e4f6b3d18

Add hints for the level editor
author Neil Muller <drnlmuller@gmail.com>
date Wed, 04 Sep 2013 21:16:09 +0200
parents 329b3044ddef
children 2a0bad886956
line wrap: on
line source

import pymunk
import pymunk.pygame_util

from nagslang import puzzle
from nagslang import render
from nagslang.constants import (
    SWITCH_PUSHERS, COLLISION_TYPE_SWITCH, COLLISION_TYPE_BOX, ZORDER_LOW,
    ZORDER_FLOOR, COLLISION_TYPE_DOOR)
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_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):
        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)

    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 animate(self):
        self.renderer.animate()

    def collide_with_protagonist(self):
        """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

    @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_BOX
        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, key_state=None):
        body = make_body(pymunk.inf, pymunk.inf, position, damping=0.5)
        self.shape = pymunk.Poly(
            body, [(-32, -32), (32, -32), (32, 32), (-32, 32)])
        self.shape.collision_type = COLLISION_TYPE_DOOR
        self.shape.sensor = True
        self.destination = destination
        self.dest_pos = tuple(dest_pos)
        if key_state is None:
            puzzler = puzzle.YesPuzzler()
        else:
            puzzler = puzzle.StateProxyPuzzler(key_state)
        super(Door, self).__init__(
            SingleShapePhysicser(space, self.shape),
            render.ImageRenderer(resources.get_image('objects', 'door.png')),
            puzzler,
        )

    def collide_with_protagonist(self):
        if self.puzzler.get_state():
            DoorEvent.post(self.destination, self.dest_pos)

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


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):
        if self.puzzler.get_state():
            # Reject the collision, we can walk through.
            return False
        return True

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