nodes/battler.pyΒΆ

Part of GDQuest Open RPG.

  1"""Battler: a sprite + stats + actions, used in BattleScene."""
  2
  3from __future__ import annotations
  4
  5import math
  6import random
  7
  8from simvx.core import Sprite2D
  9from simvx.core.math.types import Vec2
 10
 11from . import sprites as art
 12from .actions import BattlerAction, actions_for_class
 13from .stats import ENEMY_PROTOS, PARTY_PROTOS, BattlerStats
 14
 15
 16def _sprite_for_class(class_id: str):
 17    if class_id == "knight":
 18        return art.knight_sprite()
 19    if class_id == "wizard":
 20        return art.wizard_sprite()
 21    if class_id == "squirrel":
 22        return art.squirrel_sprite()
 23    if class_id == "wolf":
 24        return art.wolf_sprite()
 25    if class_id == "bear":
 26        return art.bear_sprite()
 27    if class_id == "bugcat":
 28        return art.bugcat_sprite()
 29    return art.knight_sprite()
 30
 31
 32class Battler(Sprite2D):
 33    """A combatant in the battle scene."""
 34
 35    def __init__(self, class_id: str, is_player: bool, position: Vec2) -> None:
 36        super().__init__(texture=_sprite_for_class(class_id))
 37        self.filter = "nearest"
 38        self.width = 56
 39        self.height = 56
 40        self.class_id = class_id
 41        self.is_player = is_player
 42        protos = PARTY_PROTOS if is_player else ENEMY_PROTOS
 43        self.stats = BattlerStats(protos[class_id])
 44        self.actions = list(actions_for_class(class_id))
 45        self.position = position
 46        self._home_pos = Vec2(position)
 47        # Battle state
 48        self.cached_action: BattlerAction | None = None
 49        self.cached_targets: list[Battler] = []
 50        self.is_active = True
 51
 52        self.stats.health_depleted.connect(self._on_died)
 53
 54        # Attack-flash + hit-shake state
 55        self._flash_t = 0.0
 56        self._shake_t = 0.0
 57        self._shake_amt = 0.0
 58        # Selection bob
 59        self.is_selected = False
 60        self._bob_t = 0.0
 61
 62    def _on_died(self) -> None:
 63        self.is_active = False
 64        # Fade out in place
 65        self.colour = (0.6, 0.4, 0.4, 0.6)
 66
 67    def move_home(self, position: Vec2) -> None:
 68        """Re-anchor the battler (used when the viewport resizes mid-battle)."""
 69        self._home_pos = Vec2(position)
 70        self.position = Vec2(position)
 71
 72    def flash(self, duration: float = 0.15) -> None:
 73        self._flash_t = duration
 74
 75    def shake(self, amount: float = 4.0, duration: float = 0.20) -> None:
 76        self._shake_t = duration
 77        self._shake_amt = amount
 78
 79    def on_update(self, dt: float) -> None:
 80        if self._flash_t > 0:
 81            self._flash_t -= dt
 82            if self._flash_t > 0:
 83                # Quick red-tint flash
 84                self.colour = (1.0, 0.4, 0.4, 1.0)
 85            else:
 86                self.colour = (1.0, 1.0, 1.0, 1.0)
 87        if self._shake_t > 0:
 88            self._shake_t -= dt
 89            jitter_x = random.uniform(-1, 1) * self._shake_amt
 90            jitter_y = random.uniform(-1, 1) * self._shake_amt
 91            base = self._home_pos
 92            self.position = Vec2(base.x + jitter_x, base.y + jitter_y)
 93            if self._shake_t <= 0:
 94                self.position = self._home_pos
 95        # Selection bob
 96        if self.is_selected and self.is_active:
 97            self._bob_t += dt
 98            offset = math.sin(self._bob_t * 7.0) * 3.0
 99            base = self._home_pos
100            if self._shake_t <= 0:
101                self.position = Vec2(base.x, base.y + offset)
102        elif self._shake_t <= 0:
103            # Snap back if no shake / no selection
104            if (self.position - self._home_pos).length() > 0.5:
105                # Smooth lerp to home
106                self.position = Vec2(
107                    self._home_pos.x + (self.position.x - self._home_pos.x) * 0.5,
108                    self._home_pos.y + (self.position.y - self._home_pos.y) * 0.5,
109                )