view skaapsteker/sprites/player.py @ 623:65881746dc20

More Sprite hierarchy work.
author Jeremy Thurgood <firxen@gmail.com>
date Sat, 07 May 2011 13:59:00 +0200
parents da331c80ec08
children 35919d12b792
line wrap: on
line source

"""Class for dealing with the player"""

import time

import pygame.transform
from pygame.constants import BLEND_RGBA_MULT

from ..constants import Layers, FoxHud, DOUBLE_TAP_TIME
from ..data import get_files, load_image
from ..engine import PlayerDied, AddSpriteEvent, OpenNotification
from ..utils import cadd

from .base import (find_sprite, AnimatedGameSprite, Monster, NPC, Item, Doorway,
                   TILE_SIZE, PC_LAYER, MONSTER_LAYER, PROJECTILE_LAYER,
                   tile_midbottom)
from .projectiles import Fireball, Lightning
from .items import BreakableItem



class Player(AnimatedGameSprite):

    sprite_layer = Layers.PLAYER
    collision_layer = PC_LAYER
    collides_with = set([MONSTER_LAYER, PROJECTILE_LAYER])
    wants_updates = True
    block = True

    def __init__(self, the_world, soundsystem):
        super(Player, self).__init__((0, 0))
        self.image = None
        self.rect = None
        self._soundsystem = soundsystem
        self._soundsystem.load_sound('yelp', 'sounds/yelp.ogg')
        self._animation_frame = 0.0
        self._last_time = time.time()
        self._recharge_timers = {
            'fireball': [ time.time(), lambda : 2.0 / len(self._me.tails) ],
            'lightning': [ time.time(), lambda : 2.0 / len(self._me.tails) ],
            'shield': [ time.time(), lambda : 16.0 / len(self._me.tails) ],
        }
        self._inv_cache = {} # invisible fox image cache
        self._shield_cache = {} # shielded fox image cache
        self._shield_image = 0 # shield image
        # State flags and such
        self.attacking = 0
        self.running = False
        self.sprinting = 0
        self.jumping = False
        self.flying = 0
        self.prep_flight = 0.0
        self.invisible = 0
        self.using_shield = 0
        self.inventory_image = None
        # We muck with these in load for convience, so ensure they're right
        self.the_world = the_world
        self.shape = the_world.fox.shape
        self._me = the_world.fox
        self.facing = 'left'
        self.set_image()
        self.set_pos((0, 0))
        self._collisions_seen = 0
        self._last_collide = []


    def setup_image_data(self, pos, **opts):
        self.shape = 'fox'  # Needed so load image does the right thing
        self._image_dict = {}
        self._shield_image = load_image('sprites/kitsune_shield.png')
        for action in ['standing', 'running', 'jumping', 'attacking']:
            for tails in [0, 1, 2, 4]:
                directory = 'sprites/kitsune_%s/kitsune_%s_%dtail' % (action, action, tails)
                for facing in ['left', 'right']:
                    self.facing = facing
                    key = self._make_key(tails, action)
                    self._image_dict[key] = []
                    for image_file in get_files(directory):
                        if image_file.startswith('.'):
                            # Skip extra junk for now
                            continue
                        image = load_image('%s/%s' % (directory, image_file))
                        if action == 'running':
                            sprint_key = self._make_key(tails, 'sprinting')
                            if sprint_key not in self._image_dict:
                                self._image_dict[sprint_key] = []
                            shockwave = load_image('sprites/kitsune_shockwave.png')
                            if facing == 'right':
                                shockwave = pygame.transform.flip(shockwave, True, False)
                        if facing == 'right':
                            image = pygame.transform.flip(image, True, False)
                        self._image_dict[key].append(image)
                        if action == 'running':
                            sprint_image = image.copy()
                            sprint_image.blit(shockwave, (0, 0))
                            self._image_dict[sprint_key].append(sprint_image)
        for shape, name in [('human', 'disguise_'), ('human_with_fan', 'disguise-fan_')]:
            directory = 'sprites/kitsune_disguise'
            for facing in ['left', 'right']:
                key = '%s_running_%s' % (shape, facing)
                standing_key = '%s_standing_%s' % (shape, facing)
                self._image_dict[key] = []
                for image_file in get_files(directory):
                    if name not in image_file:
                        continue
                    image = load_image('%s/%s' % (directory, image_file))
                    if facing == 'right':
                        image = pygame.transform.flip(image, True, False)
                    self._image_dict[key].append(image)
                self._image_dict[standing_key] = [self._image_dict[key][0]]


    def set_image(self):
        key = self._make_key(len(self._me.tails))
        images = self._image_dict[key]
        if self._animation_frame >= len(images):
            self._animation_frame = 0.0
        if self.rect:
            cur_pos = self.collide_rect.midbottom
        else:
            cur_pos = (0, 0)
        # TODO: can save a lot of calculation here by caching collision rects
        cand_image = images[int(self._animation_frame)]
        cand_collide_rect = cand_image.get_bounding_rect(1).inflate(-2,-2)
        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
        # We always allow the attacking animation frames
        if not self.check_collide_rect(cand_collide_rect, cand_rect, cand_image) and not self.attacking:
            return False
        if self.invisible > 0:
            id_cand_image = id(cand_image)
            if id_cand_image in self._inv_cache:
                cand_image = self._inv_cache[id_cand_image]
            else:
                cand_image = cand_image.copy()
                cand_image.fill((0, 0, 0, 140), None, BLEND_RGBA_MULT)
                self._inv_cache[id_cand_image] = cand_image
        if self.using_shield > 0:
            id_cand_image = id(cand_image)
            if id_cand_image in self._shield_cache:
                cand_image = self._shield_cache[id_cand_image]
            else:
                cand_image = cand_image.copy()
                cand_image.blit(self._shield_image, (0, 0), None)
                self._shield_cache[id_cand_image] = cand_image
        self.image = cand_image
        self.collide_rect = cand_collide_rect
        self.rect = cand_rect
        self.rect_offset = cand_rect_offset
        self.init_pos()
        return True

    def fix_bounds(self, bounds):
        """Force the player back into the world."""
        if self.rect.bottom > bounds.bottom:
            self.rect.bottom = bounds.bottom - TILE_SIZE[1] - 5
        elif self.rect.top < bounds.top:
            self.rect.top = bounds.top + TILE_SIZE[1] + 10
        if self.rect.left < bounds.left:
            self.rect.left = bounds.left + TILE_SIZE[0]
        elif self.rect.right > bounds.right:
            self.rect.right = bounds.left - TILE_SIZE[0]
        # FIXME: Find clear tile
        # Hack -  bump vertical position up by 5 so we're not colliding
        # with the floor when we come back
        self.rect.bottom = self.rect.bottom - 5
        self.collide_rect.midbottom = self.rect.midbottom
        self.init_pos()
        self.set_image()

    def update(self):
        self._touching_actionables = []
        v_x, v_y = self.velocity
        # Never animate slower than !7 fps, never faster than ~15 fps
        if self.attacking > 0:
            if self._last_time:
                if time.time() - self._last_time > 0.15:
                    self._animation_frame += 1
                    self.attacking -= 1
                    self._last_time = time.time()
            else:
                self._last_time = time.time()
        else:
            old_frame = self._animation_frame
            self._animation_frame += abs(v_x) / 300
            time_diff = time.time() - self._last_time
            if int(self._animation_frame) - int(old_frame) > 0:
                # Check time diff
                if time_diff < 0.10:
                    # Delay animation frame jump
                    self._animation_frame -= abs(v_x) / 300
                else:
                    self._last_time = time.time()
            elif time_diff > 0.20:
                # Force animation frame jump
                self._animation_frame = old_frame + 1
                self._last_time = time.time()
        now = time.time()
        if self.sprinting > 0:
            if (now - self._sprint_start_time) > self._max_sprint_time:
                self.sprinting = 0
        if self.flying > 0:
            if (now - self._flight_start_time) > self._max_flight_time:
                self.flying = 0
        if self.invisible > 0:
            if (now - self._invisibility_start_time) > self._max_invisibility_time:
                self.invisible = 0
        if self.using_shield > 0:
            if (now - self._shield_start_time) > 1.0:
                self.using_shield = 0
        if abs(v_x) < 80:
            # Clamp when we're not moving at least 5 pixel / s
            self.velocity = (0, v_y)
            if self.sprinting == 1:
                self.sprinting = 0
            self.running = not self.on_solid # if you're not on something you can't stand
        else:
            self.velocity = (0, v_y) # Standard platformer physics
            if self.sprinting > 0:
                self.sprinting = 1
            self.running = True
        self.set_image()
        if self._collisions_seen > 2 * len(self._last_collide):
            # Can we find a position "nearby" that reduces the collision
            # surface
            best_move = (0, 0)
            clip_area = 0
            for obj in self._last_collide[:]:
                if not obj.collide_rect.colliderect(self.collide_rect):
                    # Prune stale objects from the list
                    self._last_collide.remove(obj)
                    continue
                clip = obj.collide_rect.clip(self.collide_rect)
                clip_area += clip.width * clip.height
                if clip.width > TILE_SIZE[0] / 2 and \
                        self.collide_rect.bottom < obj.collide_rect.top + TILE_SIZE[1] / 3:
                   delta = self.rect.bottom - self.collide_rect.bottom
                   self.collide_rect.bottom = obj.collide_rect.top - 1
                   self.rect.bottom = self.collide_rect.bottom + delta
                   self.init_pos()
                   return  # Jump out of this case
            min_area = clip_area
            for attempt in [(0, 2), (2, 0), (-2, 0), (2, 2), (-2, 2)]:
                clip_area = 0
                for obj in self._last_collide:
                    cand_rect = self.collide_rect.move(attempt)
                    clip = obj.collide_rect.clip(cand_rect)
                    clip_area += clip.width * clip.height
                if clip_area < min_area:
                    min_area = clip_area
                    best_move = attempt
                elif clip_area == min_area and attempt[1] > best_move[1]:
                    # Of equal choices, prefer that which moves us downwards
                    best_move = attempt
            self.collide_rect.move_ip(best_move)
            self.rect.move_ip(best_move)
            self.init_pos()
            self._last_collide = []
            self._collisions_seen = 0

    def set_facing(self, new_facing):
        if self.facing != new_facing:
            self.facing = new_facing
            self.set_image()

    def collided(self, other):
        if self.attacking and hasattr(other, 'damage'):
            # FIXME: Check if we're facing the right way
            other.damage(5)
        if other not in self._last_collide and (other.floor or other.block):
            self._last_collide.append(other)
            self._collide_pos = self.collide_rect.midbottom
            self._collisions_seen = 0
        elif other in self._last_collide:
            self._collisions_seen += 1
        if hasattr(other, 'collided_player'):
            other.collided_player(self)

    def damage(self, damage):
        if self.using_shield > 0:
            "Shield on."
            return
        elif 'shield' in self._me.tails and self.check_fire_rate('shield'):
            self._shield_start_time = time.time()
            self.using_shield = 1
            return
        self._me.cur_health -= damage
        self._soundsystem.play_sound('yelp')
        if self._me.cur_health <= 0:
            PlayerDied.post()

    def steal_life(self, damage_done):
        if 'steal' in self._me.tails:
            self._me.cur_health += damage_done * len(self._me.tails) / 32
            self._me.cur_health = min(self._me.cur_health, self._me.max_health)

    def restore(self):
        """Restore player to max health (for restarting levels, etc.)"""
        self._me.cur_health = self._me.max_health

    def set_pos(self, pos):
        self.rect.midbottom = cadd(tile_midbottom(pos), self.rect_offset)
        self.collide_rect.midbottom = tile_midbottom(pos)

    def action_left(self):
        self.set_facing('left')
        if self.shape != 'fox':
            self.deltav((-300.0, 0.0))
        elif self.sprinting > 0:
            self.sprinting = 1
            self.deltav((-900.0, 0.0))
        else:
            self.deltav((-450.0, 0.0))

    def action_double_left(self):
        if self.sprinting > 0 or self.flying > 0 or 'sprint' not in self._me.tails or self._me.shape != 'fox':
            return
        self.sprinting = 2
        self._max_sprint_time = float(len(self._me.tails)) / 4.0
        self._sprint_start_time = time.time()

    def action_double_right(self):
        if self.sprinting > 0 or self.flying > 0 or 'sprint' not in self._me.tails or self._me.shape != 'fox':
            return
        self.sprinting = 2
        self._max_sprint_time = float(len(self._me.tails)) / 4.0
        self._sprint_start_time = time.time()

    def action_double_up(self):
        if self.flying > 0 or 'flight' not in self._me.tails or \
               time.time() - self.prep_flight > 2.5 * DOUBLE_TAP_TIME \
               or self._me.shape != 'fox':
            return
        self.flying = 1
        self._max_flight_time = float(len(self._me.tails))
        self._flight_start_time = time.time()

    def action_invisible(self):
        if self.invisible > 0 or 'invisibility' not in self._me.tails:
            return
        self.invisible = 1
        self._max_invisibility_time = float(len(self._me.tails))
        self._invisibility_start_time = time.time()

    def action_transform(self):
        """Transform the fox"""
        if not self.on_solid:
            return
        if 'shapeshift' not in self._me.tails:
            return
        if self.shape == 'fox':
            # Become human
            if self._me.has_fan:
                self.shape = 'human_with_fan'
            else:
                self.shape = 'human'
        else:
            self.shape = 'fox'
        # Check the transformation is feasible
        if self.set_image():
            # Transformation succeeded
            self._me.shape = self.shape
        else:
            # Back out of transformation
            self.shape = self._me.shape

    def action_right(self):
        self.set_facing('right')
        if self.shape != 'fox':
            self.deltav((300.0, 0.0))
        elif self.sprinting > 0:
            self.sprinting = 1  # Flag so stopping works
            self.deltav((900.0, 0.0))
        else:
            self.deltav((450.0, 0.0))

    def action_up(self):
        if self.flying:
            self.deltav((0.0, -self.terminal_velocity[1] / 5.0))
        elif self.on_solid and self.shape == 'fox':
            self.prep_flight = time.time()
            self.deltav((0.0, -self.terminal_velocity[1]))
            self.on_solid = False

    def _action_key(self, sprite):
        # sort action items so Items are first, then NPCs, then others, then Doorways
        # This prevents problems if players drop items on NPCs or Doorways
        return (
            not isinstance(sprite, Item),
            not isinstance(sprite, NPC),
            not isinstance(sprite, Doorway))

    def action_down(self):
        if self.flying:
            if self.on_solid:
                self.flying = 0
            return
        elif self._touching_actionables:
            self.invisible = 0
            self._touching_actionables.sort(key=self._action_key)
            self._touching_actionables[0].player_action(self)
        elif self._me.item is not None and self.on_solid:
            self.drop_item()

    def add_tail(self, tail_type):
        if tail_type not in self._me.tails:
            self._me.tails.append(tail_type)

    def _bite_attack(self):
        self.invisible = 0
        self.attacking = 2
        self._last_time = time.time() # Reset the animation clock

    def _launch_projectile(self, cls):
        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=(Monster, BreakableItem), source=self)
        projectile.launch()
        AddSpriteEvent.post(projectile)

    def _fireball_attack(self):
        if not self.check_fire_rate('fireball'):
            return
        self.invisible = 0
        self.attacking = 2
        self._last_time = time.time() # Reset the animation clock
        self._launch_projectile(Fireball)

    def _lightning_attack(self):
        if not self.check_fire_rate('lightning'):
            return
        self.invisible = 0
        self.attacking = 2
        self._last_time = time.time() # Reset the animation clock
        self._launch_projectile(Lightning)

    def action_fire1(self):
        if self._me.shape != 'fox':
            return
        if "fireball" not in self._me.tails:
            self._bite_attack()
        else:
            self._fireball_attack()

    def action_fire2(self):
        if self._me.shape != 'fox':
            return
        if "lightning" not in self._me.tails:
            self._bite_attack()
        else:
            self._lightning_attack()

    def check_fire_rate(self, attack):
        if self.recharge_level(attack) < 1:
            return False
        self._recharge_timers[attack][0] = time.time()
        return True

    def recharge_level(self, attack):
        recharge_time = self._recharge_timers[attack][1]()
        return min((time.time() - self._recharge_timers[attack][0]) / recharge_time, 1)

    def discharge_level(self, tail):
        if tail == 'invisibility' and hasattr(self, '_invisibility_start_time') and self.invisible:
            start_time = self._invisibility_start_time
            max_time = self._max_invisibility_time
        elif tail == 'flight' and hasattr(self, '_flight_start_time') and self.flying:
            start_time = self._flight_start_time
            max_time = self._max_flight_time
        else:
            return 0
        discharge = (time.time() - start_time) / max_time
        if discharge > 1:
            return 0
        return discharge

    def _get_action(self):
        if self.attacking:
            return 'attacking'
        if (self.sprinting > 0) and self.running:
            return 'sprinting'
        if self.running:
            return 'running'
        if self.flying:
            return 'running'
        if self.jumping:
            return 'jumping'
        return 'standing'

    def _make_key(self, tails, action=None):
        if self.shape != 'fox':
            # special logic for human shapes
            if self.running:
                return '%s_running_%s' % (self.shape, self.facing)
            else:
                return '%s_standing_%s' % (self.shape, self.facing)
        if action is None:
            action = self._get_action()
        if tails >= 4:
            tails = 4
        elif tails >= 2:
            tails = 2
        return '%s %s %d' % (action, self.facing, tails)

    def discard_item(self):
        self._me.item = None


    def get_sprite(self, set_level):
        my_item = self._me.item
        if my_item is None:
            return None

        if set_level:
            to_level = self._me.level
            to_pos = self.get_tile_pos()
        else:
            to_level, to_pos = None, None

        return self.the_world.gamestate().create_item_sprite(
            my_item, to_level=to_level, to_pos=to_pos)


    def drop_item(self):
        sprite = self.get_sprite(True)
        if sprite is None:
            return
        self.discard_item()
        AddSpriteEvent.post(sprite)


    def take_item(self, item):
        self.take_item_by_name(item.name)
        # We create a scaled version of the image for the inventory display
        item.remove()


    def make_inventory_image(self):
        sprite = self.get_sprite(False)
        if sprite is None:
            self.inventory_image = None
        image = sprite.image
        if image.get_width() > image.get_height():
            new_width = FoxHud.INVENTORY_SIZE
            new_height = int(image.get_height() * (float(FoxHud.INVENTORY_SIZE) / image.get_width()))
        else:
            new_height = FoxHud.INVENTORY_SIZE
            new_width = int(image.get_width() * (float(FoxHud.INVENTORY_SIZE) / image.get_height()))
        if image.get_width() <= FoxHud.INVENTORY_SIZE and image.get_height() <= FoxHud.INVENTORY_SIZE:
            self.inventory_image = image
        else:
            self.inventory_image = pygame.transform.smoothscale(image, (new_width, new_height))
        sprite.kill() # ensure we don't leak into the scene at any point


    def take_item_by_name(self, item_name):
        self.drop_item()
        getattr(self.the_world.items, item_name).level = "_limbo"
        self._me.item = item_name
        self.make_inventory_image()


    def has_item(self, item):
        return self._me.item == item


    def add_actionable(self, actionable):
        self._touching_actionables.append(actionable)


    def eat_aburage(self):
        self._me.tofu += 1


    def collect_scroll(self, scroll):
        self._me.scrolls.append(scroll.text)


    def get_fan(self, fan):
        if self.shape == 'fox':
            OpenNotification.post("A fox cannot use a fan.")
            return
        fan.remove()
        self._me.has_fan = True
        self.shape = 'human_with_fan'
        self._me.shape = self.shape
        self.set_image()