nodes/stats.pyΒΆ

Part of GDQuest Open RPG.

 1"""Battler stats: hp, energy, attack/defense, speed, hit/evasion."""
 2
 3from __future__ import annotations
 4
 5from dataclasses import dataclass
 6
 7from simvx.core.signals import Signal
 8
 9
10@dataclass
11class StatsSpec:
12    """Static prototype of a battler's stats."""
13
14    name: str
15    max_health: int = 30
16    max_energy: int = 10
17    attack: int = 8
18    defense: int = 4
19    speed: int = 50
20    hit_chance: int = 95  # % to hit before evasion
21    evasion: int = 5  # % chance to dodge
22
23
24class BattlerStats:
25    """Live mutable stats with signals."""
26
27    def __init__(self, spec: StatsSpec) -> None:
28        self.spec = spec
29        self.max_health = spec.max_health
30        self.max_energy = spec.max_energy
31        self.attack = spec.attack
32        self.defense = spec.defense
33        self.speed = spec.speed
34        self.hit_chance = spec.hit_chance
35        self.evasion = spec.evasion
36        self.health = spec.max_health
37        self.energy = spec.max_energy
38
39        self.health_changed = Signal()  # (current, max)
40        self.energy_changed = Signal()  # (current, max)
41        self.health_depleted = Signal()
42
43    def take_damage(self, amount: int) -> int:
44        prev = self.health
45        self.health = max(0, self.health - max(0, amount))
46        actual = prev - self.health
47        self.health_changed.emit(self.health, self.max_health)
48        if self.health == 0:
49            self.health_depleted.emit()
50        return actual
51
52    def heal(self, amount: int) -> int:
53        prev = self.health
54        self.health = min(self.max_health, self.health + max(0, amount))
55        delta = self.health - prev
56        self.health_changed.emit(self.health, self.max_health)
57        return delta
58
59    def spend_energy(self, amount: int) -> bool:
60        if self.energy < amount:
61            return False
62        self.energy = max(0, self.energy - amount)
63        self.energy_changed.emit(self.energy, self.max_energy)
64        return True
65
66    def restore_energy(self, amount: int) -> None:
67        self.energy = min(self.max_energy, self.energy + amount)
68        self.energy_changed.emit(self.energy, self.max_energy)
69
70    @property
71    def is_alive(self) -> bool:
72        return self.health > 0
73
74
75# ---- prototypes ----------------------------------------------------------
76
77PARTY_PROTOS = {
78    "knight": StatsSpec(
79        name="Knight", max_health=42, max_energy=8, attack=10, defense=6, speed=45, hit_chance=95, evasion=5
80    ),
81    "wizard": StatsSpec(
82        name="Wizard", max_health=24, max_energy=14, attack=8, defense=3, speed=55, hit_chance=90, evasion=10
83    ),
84    "squirrel": StatsSpec(
85        name="Squirrel", max_health=20, max_energy=10, attack=6, defense=2, speed=70, hit_chance=98, evasion=20
86    ),
87}
88
89ENEMY_PROTOS = {
90    "wolf": StatsSpec(
91        name="Wolf", max_health=18, max_energy=4, attack=8, defense=3, speed=60, hit_chance=85, evasion=15
92    ),
93    "bear": StatsSpec(
94        name="Bear", max_health=32, max_energy=4, attack=12, defense=6, speed=35, hit_chance=80, evasion=5
95    ),
96    "bugcat": StatsSpec(
97        name="Bugcat", max_health=14, max_energy=6, attack=6, defense=2, speed=65, hit_chance=88, evasion=18
98    ),
99}