view skaapsteker/sprites/base.py @ 302:78220c989e6a

Add supporting for flicking between speaking NPCs.
author Simon Cross <hodgestar@gmail.com>
date Fri, 08 Apr 2011 23:27:43 +0200
parents 15b2be883a40
children e499a10eb41f
line wrap: on
line source

"""Basic sprite classes."""

import re

from pygame import Rect
import pygame.transform

from ..physics import Sprite
from ..constants import Layers
from ..engine import OpenDialog
from .. import data
from .. import dialogue


TILE_SIZE = (64, 64)

# Collision Layers (values are ids not numbers)
PC_LAYER = 0
MONSTER_LAYER = 1
NPC_LAYER = 2



class GameSprite(Sprite):
    image_dir = 'sprites/'
    image_file = None

    def __init__(self, pos, **opts):
        Sprite.__init__(self)
        self._starting_tile_pos = pos
        self.setup_image_data(pos)
        self.setup(**opts)


    def setup_image_data(self, pos):
        self.image = data.load_image(self.image_dir + self.image_file)
        self.rect = self.image.get_rect(midbottom=(pos[0]*TILE_SIZE[0]+TILE_SIZE[0]/2, (pos[1]+1)*TILE_SIZE[1]))
        self.collide_rect = self.rect.move(0, 0)


class AnimatedGameSprite(Sprite):
    # folder for animation files, e.g. sprites/foo
    image_dir = None

    # first item is the starting animation
    animation_regexes = [
        # TODO: swap back once we know how to swap
        ("running", r"^.*_\d+.png$"),
        ("standing", r"^.*_standing.png$"),
        ("attacking", r"^.*_attacking.png$"),
    ]

    wants_updates = True

    facings = {}

    def __init__(self, pos, **opts):
        Sprite.__init__(self)
        self._animations = dict((k, []) for k, r in self.animation_regexes)
        self._frame = 0
        self._tick = 0 # TODO: hack to show some animation; kill shortly
        self._animation = self.animation_regexes[0][0]
        if self.facings and self._animation in self.facings:
            self.facing = self.facings[self._animation][0][0]
        else:
            self.facing = None

        for image in data.get_files(self.image_dir):
            for name, pattern in self.animation_regexes:
                if re.match(pattern, image):
                    img = data.load_image("%s/%s" % (self.image_dir, image))
                    if self.facings and name in self.facings:
                        if not self._animations[name]:
                            self._animations[name] = dict((k, []) for k, t in self.facings[name])
                        for facing, transform in self.facings[name]:
                            if transform:
                                mod_img = transform(img)
                            else:
                                mod_img = img
                            collide_rect = mod_img.get_bounding_rect(1).inflate(-2,-2)
                            self._animations[name][facing].append((mod_img, collide_rect))
                    else:
                        collide_rect = img.get_bounding_rect(1).inflate(-2,-2)
                        self._animations[name].append((img, collide_rect))

        self.collide_rect = Rect((0, 0), (2, 2))
        self.collide_rect.midbottom = (pos[0]*TILE_SIZE[0]+TILE_SIZE[0]/2, (pos[1]+1)*TILE_SIZE[1])
        self._update_image()
        self.setup(**opts)

    def _update_image(self, force=False):
        if self.facing:
            images = self._animations[self._animation][self.facing]
        else:
            images = self._animations[self._animation]
        if self._frame >= len(images):
            self._frame = 0
        cand_image, cand_collide_rect = images[self._frame]
        cand_collide_rect = cand_collide_rect.move(0, 0) # copy collide rect before we move it

        cur_pos = self.collide_rect.midbottom

        cand_rect = cand_image.get_rect()
        cand_rect_offset = cand_rect.centerx - cand_collide_rect.centerx, cand_rect.bottom - cand_collide_rect.bottom
        cand_rect.midbottom = cur_pos[0] + cand_rect_offset[0], cur_pos[1] + cand_rect_offset[1]
        cand_collide_rect.midbottom = cur_pos

        if not self.check_collide_rect(cand_collide_rect, cand_rect, cand_image) and not force:
            return

        self.image = cand_image
        self.collide_rect = cand_collide_rect
        self.rect = cand_rect
        self.rect_offset = cand_rect_offset
        self.init_pos()

    def update(self):
        if self._tick > 10:
            self._tick = 0
            self._frame += 1
            force = False
            if self._animation == 'attacking':
                force = True
            self._update_image(force)
        self._tick += 1


class Monster(AnimatedGameSprite):

    collision_layer = MONSTER_LAYER
    collides_with = set([PC_LAYER])

    debug_color = (240, 120, 120)

    block = True

    attack_frame = None  # Mark a spefici frame in the animatio n as when the attack lands
    attack_damage = 1

    def __init__(self, pos, **opts):
        AnimatedGameSprite.__init__(self, pos, **opts)
        self.floor_rect = Rect(self.collide_rect.topleft, (self.collide_rect.width, 2))
        self._layer = Layers.PLAYER
        self.health = 10
        self.setup(**opts)


    def collided_player(self, player):
        print "%s collided with player" % self
        self.start_attack(player)

    def update(self):
        AnimatedGameSprite.update(self)
        if self._animation == 'attacking':
            if self._frame == 0 and self._tick == 1:
                # FIXME: This will need to change when AnimatedGameSprite changes
                # We've just looped through the animation sequence
                self._animation = self._old_state
                self.facing = self._old_facing
                self._update_image(True)
            elif self._frame == self.attack_frame and self._tick == 5:
                # Attack the player
                self.do_attack()

    def do_attack(self):
        """Overriden by monster classes"""
        if self.check_collides(self._target):
            self._target.damage(self.attack_damage)

    def start_attack(self, player):
        if self._animation == 'attacking':
            return # We're already attacking
        elif self.attack_frame is not None:
            self._target = player
            self._old_state = self._animation
            self._old_facing = self.facing
            self._animation = 'attacking'
            self._tick = 1
            self._frame = 0  # Start the attack from the beginning
            self._update_image(True)
        else:
            player.damage(1)  # collision damage

    def damage(self, damage):
        print 'Damaged by ', damage
        self.health -= damage
        print 'Monster health', self.health
        if self.health < 0:
            self.kill()


class NPC(AnimatedGameSprite):

    collision_layer = NPC_LAYER

    debug_color = (240, 240, 240)
    bounce_factor = (0, 0)    #  NPC's don't bounce by default

    block = False
    actionable = True

    def __init__(self, pos, **opts):
        AnimatedGameSprite.__init__(self, pos, **opts)
        self._layer = Layers.PLAYER

    def setup(self, name, world, dsm, state):
        self.name = name
        self.dsm = dialogue.DSM(name, world, dsm, state)

    def player_action(self, player):
        OpenDialog.post(self)


class Projectile(GameSprite):
    gravitates = False


class Item(GameSprite):
    mobile = False
    gravitates = False

    collision_layer = NPC_LAYER

    portable = True
    actionable = True

    def __init__(self, pos, **opts):
        GameSprite.__init__(self, pos, **opts)
        self._layer = Layers.PLAYER


    def setup(self, name, world):
        self.name = name
        self.world = world
        self.item_state = getattr(self.world.items, self.name)


    def get_debug_color(self):
        if self.portable:
            return (240, 0, 240)
        return (0, 0, 240)


    def player_action(self, player):
        print "Player touched %s" % self
        player.take_item(self)



class Geography(Sprite):
    mobile = False
    gravitates = False
    collides_with = set([PC_LAYER, MONSTER_LAYER, NPC_LAYER])
    is_ground = True
    actionable = False
    bounce_factor = (0.0, 0.0)

    def __init__(self, pos, image):
        Sprite.__init__(self)
        self.tile_pos = pos
        self.image = image
        self.collide_rect = self.image.get_bounding_rect(1)
        self.floor_rect = Rect(self.collide_rect.topleft, (self.collide_rect.width, 2))
        self.rect = self.image.get_rect()
        self.rect_offset = self.collide_rect.left - self.rect.left, self.rect.top - self.rect.top
        self.collide_rect.topleft = pos[0] * TILE_SIZE[0] + self.rect_offset[0], pos[1] * TILE_SIZE[1] + self.rect_offset[1]
        self.floor_rect.topleft = pos[0] * TILE_SIZE[0] + self.rect_offset[0], pos[1] * TILE_SIZE[1] + self.rect_offset[1]
        self.rect.topleft = pos[0] * TILE_SIZE[0], pos[1] * TILE_SIZE[1]

    def get_debug_color(self):
        if self.floor or self.block:
            return (240, 240, 0)
        return (0, 240, 0)



class Doorway(GameSprite):
    mobile = False
    gravitates = False

    blocks = False
    actionable = True

    image_file = 'torii.png'


    def setup_image_data(self, pos):
        super(Doorway, self).setup_image_data(pos)
        self.image = pygame.transform.scale(self.image, self.image.get_rect().center)
        self.rect = self.image.get_rect(midbottom=self.rect.midbottom)
        self.collide_rect = self.rect


    def setup(self, facing, leadsto):
        self.facing = facing
        self.leadsto = leadsto
        print leadsto


    def player_action(self, player):
        print "Player touched %s" % self
        from .. import engine, levelscene
        engine.ChangeScene.post((levelscene.LevelScene, self.leadsto))



class StartingDoorway(Doorway):
    actionable = False

    def setup_image_data(self, pos):
        self.image = pygame.Surface((0, 0))
        self.rect = self.image.get_rect(midbottom=(pos[0]*TILE_SIZE[0]+TILE_SIZE[0]/2, (pos[1]+1)*TILE_SIZE[1]))
        self.collide_rect = self.rect.move(0, 0)


    def setup(self, facing):
        Doorway.setup(self, facing, None)



def find_sprite(descr, mod_name=None):
    """Create a sprite object from a dictionary describing it."""
    descr = dict((str(k), v) for k, v in descr.items()) # convert unicode keys
    cls_name = descr.pop("type")
    if mod_name is None:
        mod_name, cls_name = cls_name.rsplit(".", 1)
    mod_name = ".".join(["skaapsteker.sprites", mod_name])
    mod =  __import__(mod_name, fromlist=[cls_name])
    cls = getattr(mod, cls_name)
    return cls(**descr)