Mercurial > skaapsteker
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)