Mercurial > skaapsteker
view skaapsteker/sprites/base.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 | 83569a6b3ad8 |
line wrap: on
line source
"""Basic sprite classes.""" import re import time from pygame import Rect import pygame.transform from .. import data from .. import dialogue from .. import sound from ..physics import Sprite from ..constants import Layers from ..engine import OpenDialog, AddSpriteEvent, OpenNotification from ..utils import cadd, csub, cmul, cdiv 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) def tile_midbottom(pos): return cadd(cmul(pos, TILE_SIZE), cdiv(TILE_SIZE, (2, 1))) class GameSprite(Sprite): image_dir = 'sprites/' image_file = None sprite_layer = None debug_color = (240, 0, 0) def __init__(self, pos, **opts): Sprite.__init__(self) if self.sprite_layer is not None: # pygame's Sprite class clobbers self._layer in __init__(), so we need to thwart it. self._layer = self.sprite_layer self.setup_image_data(pos, **opts) self.setup(**opts) def setup(self): pass def get_tile_pos(self): return cdiv(self.rect.center, TILE_SIZE) def setup_image_data(self, pos, **opts): self.image = data.load_image(self.image_dir + self.image_file) self.rect = self.image.get_rect(midbottom=tile_midbottom(pos)) self.collide_rect = self.rect.move(0, 0) def get_debug_color(self): return self.debug_color def draw_debug(self, surface): pygame.draw.rect(surface, self.get_debug_color(), self.collide_rect, 1) class AnimatedGameSprite(GameSprite): # 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 = None def setup_image_data(self, pos, **opts): 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 = tile_midbottom(pos) self._update_image() 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 = csub(cand_rect.midbottom, cand_collide_rect.midbottom) cand_rect.midbottom = cadd(cur_pos, cand_rect_offset) 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): sprite_layer = Layers.PLAYER collision_layer = MONSTER_LAYER collides_with = set([PC_LAYER, PROJECTILE_LAYER]) debug_color = (240, 120, 120) block = True attack_frame = None # Mark a speficic frame in the animation 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.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): sprite_layer = Layers.PLAYER 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 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 = cdiv(self.PROJECTILE_SIZE, (-2, 1)) dv = cmul(self.VELOCITY, (-1, 1)) else: shift = cdiv(self.PROJECTILE_SIZE, (2, 1)) dv = cmul(self.VELOCITY, (1, 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 sprite_layer = Layers.PLAYER collision_layer = NPC_LAYER debug_color = (240, 0, 240) 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(GameSprite): 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 setup(self, image): pass def setup_image_data(self, pos, image, **opts): 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 = csub(self.collide_rect.topleft, self.rect.topleft) self.rect.topleft = cmul(pos, TILE_SIZE) self.collide_rect.topleft = cadd(self.rect.topleft, self.rect_offset) self.floor_rect.topleft = cadd(self.rect.topleft, self.rect_offset) 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, **opts): super(Doorway, self).setup_image_data(pos, **opts) 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, **opts): self.image = pygame.Surface((0, 0)) self.rect = self.image.get_rect(midbottom=tile_midbottom(pos)) 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/' sprite_layer = Layers.BEHIND 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' super(Skeleton, self).__init__(pos, **opts) def setup_image_data(self, pos, **opts): super(Skeleton, self).setup_image_data(pos, **opts) # 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)