feat(actor): implement player / actor class
This commit is contained in:
+164
@@ -0,0 +1,164 @@
|
||||
"""Actor state with switchable good/bad sprites.
|
||||
|
||||
The drawing itself still happens through ``draw_actor``. This class only stores
|
||||
which side of the actor is active and exposes functions to switch it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Mapping, Tuple
|
||||
|
||||
import pygame
|
||||
|
||||
from src.actor_sprites import Position, draw_actor
|
||||
from src.settings import DEFAULT_FRAME_TIME, Direction
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ActorSpriteAsset:
|
||||
"""Loaded animation frames for one actor/skin."""
|
||||
|
||||
name: str
|
||||
folder: Path
|
||||
frames: Mapping[Direction, Tuple[pygame.Surface, ...]]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ActorSpriteSides:
|
||||
"""The good and bad sprite assets for the same actor.
|
||||
|
||||
Example: ``normal_knoll`` is the good side and ``angry_knoll`` is the bad side.
|
||||
"""
|
||||
|
||||
name: str
|
||||
good: ActorSpriteAsset
|
||||
bad: ActorSpriteAsset
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ActorDraw:
|
||||
"""Small data object for drawing one actor with ``draw_actors``."""
|
||||
|
||||
asset: ActorSpriteAsset
|
||||
position: Position
|
||||
direction: Direction = "down"
|
||||
moving: bool = False
|
||||
animation_time: float = 0.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class Actor:
|
||||
"""One game actor that can switch between good and bad visuals."""
|
||||
|
||||
name: str
|
||||
good_asset: ActorSpriteAsset
|
||||
bad_asset: ActorSpriteAsset
|
||||
position: pygame.Vector2
|
||||
direction: Direction = "down"
|
||||
moving: bool = False
|
||||
animation_time: float = 0.0
|
||||
is_bad: bool = False
|
||||
previous_position: pygame.Vector2 = field(init=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
# Always store positions as Vector2, even if a tuple was passed in.
|
||||
self.position = pygame.Vector2(self.position)
|
||||
self.previous_position = self.position.copy()
|
||||
|
||||
@classmethod
|
||||
def from_sides(
|
||||
cls,
|
||||
sides: ActorSpriteSides,
|
||||
position: Position,
|
||||
) -> "Actor":
|
||||
"""Create an actor from a loaded good/bad sprite pair."""
|
||||
|
||||
return cls(
|
||||
name=sides.name,
|
||||
good_asset=sides.good,
|
||||
bad_asset=sides.bad,
|
||||
position=pygame.Vector2(position),
|
||||
)
|
||||
|
||||
@property
|
||||
def current_asset(self) -> ActorSpriteAsset:
|
||||
"""The sprite asset that should currently be drawn."""
|
||||
|
||||
return self.bad_asset if self.is_bad else self.good_asset
|
||||
|
||||
def become_bad(self) -> None:
|
||||
"""Switch this actor to its bad/angry sprite."""
|
||||
|
||||
self.is_bad = True
|
||||
|
||||
def become_good(self) -> None:
|
||||
"""Switch this actor back to its good/normal sprite."""
|
||||
|
||||
self.is_bad = False
|
||||
|
||||
def toggle_side(self) -> None:
|
||||
"""Switch good -> bad or bad -> good."""
|
||||
|
||||
self.is_bad = not self.is_bad
|
||||
|
||||
def move_by(self, movement: Position) -> None:
|
||||
"""Move the actor and remember the old position for wall collisions.
|
||||
|
||||
Call this before collision checks. If the new position is inside a wall,
|
||||
call ``actor.hit_wall()`` to move the actor back and stop the walking
|
||||
animation.
|
||||
"""
|
||||
|
||||
movement_vector = pygame.Vector2(movement)
|
||||
self.previous_position = self.position.copy()
|
||||
|
||||
if movement_vector.length_squared() == 0:
|
||||
self.stop_walking()
|
||||
return
|
||||
|
||||
self.position += movement_vector
|
||||
self.moving = True
|
||||
|
||||
def set_position(self, position: Position) -> None:
|
||||
"""Set actor position while keeping the last position for collision rollback."""
|
||||
|
||||
self.previous_position = self.position.copy()
|
||||
self.position = pygame.Vector2(position)
|
||||
|
||||
def stop_walking(self) -> None:
|
||||
"""Stop movement and show the idle standing frame."""
|
||||
|
||||
self.moving = False
|
||||
self.animation_time = DEFAULT_FRAME_TIME
|
||||
|
||||
def hit_wall(self) -> None:
|
||||
"""Call when this actor collides with a wall.
|
||||
|
||||
This moves the actor back to the last safe position and stops the walking
|
||||
animation, so usage can look like: ``knoll.hit_wall()``.
|
||||
"""
|
||||
|
||||
self.position = self.previous_position.copy()
|
||||
self.stop_walking()
|
||||
|
||||
def update_animation(self, dt: float) -> None:
|
||||
"""Advance walking animation time."""
|
||||
|
||||
if self.moving:
|
||||
self.animation_time += dt
|
||||
else:
|
||||
self.animation_time = DEFAULT_FRAME_TIME
|
||||
|
||||
def draw(self, surface: pygame.Surface) -> pygame.Rect:
|
||||
"""Draw this actor onto any Pygame surface."""
|
||||
|
||||
return draw_actor(
|
||||
surface,
|
||||
self.current_asset,
|
||||
self.position,
|
||||
direction=self.direction,
|
||||
moving=self.moving,
|
||||
animation_time=self.animation_time,
|
||||
)
|
||||
Reference in New Issue
Block a user