Mercurial > skaapsteker
view skaapsteker/sprites/player.py @ 627:35919d12b792
Path-based collision minimisation and axis-projection backout.
author | Jeremy Thurgood <firxen@gmail.com> |
---|---|
date | Sat, 07 May 2011 20:28:06 +0200 |
parents | 65881746dc20 |
children | 1fdfc7f03d98 |
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 # 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_collide_rect = cand_image.get_bounding_rect(1) 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()