view skaapsteker/sprites/base.py @ 592:1386cae4cc15

Remove don't crash if we're colliding with a player after being bounced off the floor
author Neil Muller <drnlmuller@gmail.com>
date Sun, 10 Apr 2011 18:39:18 +0200
parents b8bed508036f
children 851c8726696c
line wrap: on
line source

"""Basic sprite classes."""

import re
import time

from pygame import Rect
import pygame.transform

from ..physics import Sprite
from ..constants import Layers
from ..engine import OpenDialog, AddSpriteEvent, OpenNotification
from .. import data
from .. import dialogue
from .. import sound


TILE_SIZE = (64, 64)

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

def notify(text):
    OpenNotification.post(text=text)


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

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

    def setup(self):
        pass

    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

    frame_pause = 0.1  # default time between animation frames

    facings = {}

    def __init__(self, pos, **opts):
        Sprite.__init__(self)
        self._animations = dict((k, []) for k, r in self.animation_regexes)
        self._frame = 0
        self._last_time = 0
        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))
        if isinstance(pos, pygame.Rect):
            self.collide_rect.midbottom = pos.midbottom
        else:
            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 setup(self):
        pass

    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._last_time is not None:
            if time.time() - self._last_time > self.frame_pause:
                self._frame += 1
                self._last_time = time.time()
                force = False
                if self._animation == 'attacking':
                    force = True
                self._update_image(force)
        else:
            self._last_time = time.time()


class Monster(AnimatedGameSprite):

    collision_layer = MONSTER_LAYER
    collides_with = set([PC_LAYER, PROJECTILE_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._done_attack = False
        self.setup(**opts)

    def setup(self, fishmonger_count=False):
        self.fishmonger_count = fishmonger_count

    def collided_player(self, player):
        self.start_attack(player)

    def update(self):
        AnimatedGameSprite.update(self)
        if self._animation == 'attacking':
            if self._frame == 0 and self._done_attack:
                # 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 not self._done_attack:
                # Attack the player
                self.do_attack()

    def _launch_projectile(self, cls):
        from .player import Player # avoid circular imports
        if self.facing == 'left':
            pos = pygame.Rect(self.rect.midleft, (0, 0))
        else:
            pos = pygame.Rect(self.rect.midright, (0, 0))
        projectile = cls(pos, direction=self.facing, hits=Player, source=self)
        projectile.launch()
        AddSpriteEvent.post(projectile)

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

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

    def damage(self, damage):
        self.health -= damage
        if self.health <= 0:
            AddSpriteEvent.post(Skeleton(self.rect.midbottom))
            self.kill()
            if self.fishmonger_count:
                self.world.missions.fishmonger_demons_killed += 1

class PatrollingMonster(Monster):
    """Monster that collides with horizontal geography"""

    debug_color = (120, 240, 120)

    patrol_speed = (200, 0)

    def update(self):
        Monster.update(self)
        if self._animation == 'running':
            if self.facing == 'left':
                self.velocity = (-self.patrol_speed[0], 0)
            elif self.facing == 'right':
                self.velocity = self.patrol_speed

    def collided(self, other):
        Monster.collided(self, other)
        # Check if the object we've collided with is the same height our higher than us
        if (other.block or other.floor) and other.collide_rect.bottom <= self.collide_rect.bottom:
            # Change direction
            self.change_facing()

    def change_facing(self):
        if self.facing == 'left':
            self.facing = 'right'
            self._update_image(True)
        else:
            self.facing = 'left'
            self._update_image(True)

    def check_floors(self, floors):
        """If we're only on 1 floor tile, and our centre is beyond half way,
           turn back"""
        if len(floors) != 1:
            return

        floor = floors[0]

        if self.facing == 'left':
            if self.collide_rect.centerx < floor.collide_rect.centerx:
                self.change_facing()
        else:
            if self.collide_rect.centerx > floor.collide_rect.centerx:
                self.change_facing()

class NPC(AnimatedGameSprite):

    collision_layer = NPC_LAYER
    collides_with = set([])

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

    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, facing=None):
        self.name = name
        self.world = world
        self.dsm = dialogue.DSM(name, world, dsm, state)
        self._me = getattr(world.npcs, self.name)
        self.facing = facing
        self._update_image(True)  # Force things to the right image

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

    def remove(self):
        self._me.level = '_limbo'
        self.kill()


class BlockingNPC(NPC):

    collides_with = set([PC_LAYER])
    mobile = False
    block = True

    def setup(self, name, world, dsm, state, block, facing=None):
        NPC.setup(self, name, world, dsm, state, facing)
        self.block = block
        self._animation = 'standing'


class Projectile(AnimatedGameSprite):

    collision_layer = PROJECTILE_LAYER
    collides_with = set()
    launch_sound = None, None

    gravitates = False

    DAMAGE = 10

    PROJECTILE_SIZE = (0, 0) # pixels
    VELOCITY = (10, 10) # pixels/s

    def setup(self, direction, hits, source, **opts):
        super(Projectile, self).setup(**opts)
        self.facing = direction
        self.source = source # source of the projectile (may be None)
        self._update_image(True)  # ensure we get the direction right
        if self.launch_sound[0]:
            sound.load_sound(self.launch_sound[0], self.launch_sound[0], self.launch_sound[1])

        if isinstance(hits, tuple):
            self.hits = hits + (Geography,)
        else:
            self.hits = (hits, Geography)

        if self.facing == "left":
            shift = (-self.PROJECTILE_SIZE[0] / 2, self.PROJECTILE_SIZE[1])
            dv = (-self.VELOCITY[0], self.VELOCITY[1])
        else:
            shift = (self.PROJECTILE_SIZE[0] / 2, self.PROJECTILE_SIZE[1])
            dv = (self.VELOCITY[0], self.VELOCITY[1])

        self.rect.move_ip(shift)
        self.collide_rect.move_ip(shift)
        self.deltav(dv)

    def launch(self):
        if self.launch_sound[0]:
            sound.play_sound(self.launch_sound[0])

    def explode(self):
        self.kill()

    def collided(self, other):
        if not isinstance(other, self.hits):
            return
        if hasattr(other, 'damage'):
            other.damage(self.DAMAGE)
            if hasattr(self.source, 'steal_life'):
                self.source.steal_life(self.DAMAGE)
        self.explode()


class Item(GameSprite):
    mobile = False
    gravitates = False
    actionable = True
    liftable = True

    collision_layer = NPC_LAYER

    debug_color = (240, 0, 240)

    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._me = getattr(self.world.items, self.name)


    def player_action(self, player):
        if self.liftable:
            player.take_item(self)


    def remove(self):
        self._me.level = '_limbo'
        self.kill()


class Geography(Sprite):
    mobile = False
    gravitates = False
    collides_with = set([PC_LAYER, MONSTER_LAYER, NPC_LAYER, PROJECTILE_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


    def player_action(self, player):
        from .. import engine, levelscene
        engine.ChangeScene.post((levelscene.LevelScene, self.leadsto))



class CelestialDoorway(Doorway):
    def player_action(self, player):
        from .. import engine
        if len(self.world.fox.tails) < 8:
            engine.OpenNotification.post(text="You need eight tails to enter the Celestial Plane.")
            return
        super(CelestialDoorway, self).player_action(player)



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)


class Skeleton(GameSprite):
    mobile = False
    gravitates = False
    actionable = False
    liftable = False
    image_dir = 'sprites/skulls/'
    debug_color = (255, 255, 0)

    def __init__(self, pos, player=False, **opts):
        self._pos = pos
        if player:
            self.image_file = 'kitsune.png'
        else:
            self.image_file = 'monster.png'
        GameSprite.__init__(self, pos, **opts)
        self._layer = Layers.BEHIND

    def setup_image_data(self, pos):
        GameSprite.setup_image_data(self, pos)
        # Pixel based rect, not tile:
        self.rect = self.image.get_rect(midbottom=self._pos)
        self.collide_rect = self.rect.move(0, 0)


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)