Skip to main content

dotzuki_engine/party/
monster.rs

1//! [`MonsterInstance`]: a generic, provider-driven monster instance.
2
3use super::{EvolutionProvider, EvolutionTrigger, ExpProvider, MonsterProvider, StatSet};
4use crate::battle::{BattleProvider, BattlerState, EnumMap};
5
6/// Engine-level status condition for a stored monster.
7///
8/// This is intentionally a small, game-agnostic enum; games map their own
9/// status model onto it. Battle-only / volatile statuses live in the battle
10/// layer (the battle `BattlerState` carries a provider-defined `Status`).
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum MonsterStatus {
13    /// No status condition.
14    Healthy,
15    /// Asleep for the given number of remaining turns.
16    Sleep(u8),
17    /// Poisoned.
18    Poison,
19    /// Burned.
20    Burn,
21    /// Frozen.
22    Freeze,
23    /// Paralyzed.
24    Paralysis,
25}
26
27impl Default for MonsterStatus {
28    fn default() -> Self {
29        MonsterStatus::Healthy
30    }
31}
32
33/// A single known move with its current and bonus PP.
34#[derive(Clone, Debug, PartialEq, Eq)]
35pub struct MoveSlot<P: MonsterProvider> {
36    /// The move identifier.
37    pub move_id: P::MoveId,
38    /// Current PP.
39    pub pp: u8,
40    /// Number of PP Ups applied (game decides what this means).
41    pub pp_up: u8,
42}
43
44/// Result of an EXP gain: what happened to the monster's level.
45///
46/// Parameterized by the [`MonsterProvider`] only so it can carry the game's
47/// opaque move ids in [`Self::learned_moves`]; the engine never inspects them.
48#[derive(Clone, Debug, PartialEq, Eq)]
49pub struct LevelUp<P: MonsterProvider> {
50    /// How many levels were gained (0 if none).
51    pub levels_gained: u8,
52    /// Level before the EXP was applied.
53    pub old_level: u8,
54    /// Level after the EXP was applied.
55    pub new_level: u8,
56    /// Moves newly learned across the levels crossed, in ascending-level order
57    /// (populated by [`ExpProvider::learn_moves_on_levelup`]; empty by default).
58    pub learned_moves: Vec<P::MoveId>,
59}
60
61impl<P: MonsterProvider> Default for LevelUp<P> {
62    fn default() -> Self {
63        LevelUp {
64            levels_gained: 0,
65            old_level: 0,
66            new_level: 0,
67            learned_moves: Vec::new(),
68        }
69    }
70}
71
72impl<P: MonsterProvider> LevelUp<P> {
73    /// Whether any level was gained.
74    pub fn gained(&self) -> bool {
75        self.levels_gained > 0
76    }
77}
78
79/// A generic monster instance: species, level, exp, computed stats, status, and
80/// known moves. All numbers are delegated to the [`MonsterProvider`].
81#[derive(Clone, Debug, PartialEq, Eq)]
82pub struct MonsterInstance<P: MonsterProvider> {
83    /// The species.
84    pub species: P::SpeciesId,
85    /// Current level.
86    pub level: u8,
87    /// Total accumulated experience.
88    pub exp: u32,
89    /// Per-instance genetics (opaque to the engine).
90    pub genetics: P::Genetics,
91    /// Accumulated training (opaque to the engine).
92    pub training: P::Training,
93    /// Cached computed stats, keyed by the provider's stat order.
94    pub stats: StatSet<P>,
95    /// Current HP.
96    pub current_hp: u16,
97    /// Status condition.
98    pub status: MonsterStatus,
99    /// Known moves.
100    pub moves: Vec<MoveSlot<P>>,
101}
102
103impl<P: MonsterProvider> MonsterInstance<P> {
104    /// Construct at a level. The caller supplies genetics/training; stats are
105    /// computed from the provider and `current_hp` is initialized to full.
106    pub fn new(
107        provider: &P,
108        species: P::SpeciesId,
109        level: u8,
110        genetics: P::Genetics,
111        training: P::Training,
112    ) -> Self {
113        let mut inst = Self {
114            species,
115            level,
116            exp: 0,
117            genetics,
118            training,
119            stats: StatSet::zeroed(provider),
120            current_hp: 0,
121            status: MonsterStatus::Healthy,
122            moves: Vec::new(),
123        };
124        inst.recalc_stats(provider);
125        inst.current_hp = inst.max_hp(provider);
126        inst
127    }
128
129    /// Recompute every stat from the provider.
130    ///
131    /// Preserves the absolute `current_hp`, clamped to the new max HP (this
132    /// matches Gen-1 recalculation behavior, where current HP is *not* scaled
133    /// by ratio on a stat recompute).
134    pub fn recalc_stats(&mut self, provider: &P) {
135        for &stat in provider.stats() {
136            let value = provider.calc_stat(
137                self.species,
138                stat,
139                self.level,
140                &self.genetics,
141                &self.training,
142            );
143            self.stats.set(stat, value);
144        }
145        let max = self.max_hp(provider);
146        if self.current_hp > max {
147            self.current_hp = max;
148        }
149    }
150
151    /// Max HP, using the provider-declared HP stat.
152    pub fn max_hp(&self, provider: &P) -> u16 {
153        self.stats.get(provider.hp_stat())
154    }
155
156    /// Whether the monster has fainted (0 HP).
157    pub fn is_fainted(&self) -> bool {
158        self.current_hp == 0
159    }
160}
161
162impl<P: ExpProvider> MonsterInstance<P> {
163    /// Add EXP, advancing level while the threshold for the next level is met.
164    ///
165    /// Recomputes stats on each level gained. Caps at the provider's
166    /// [`ExpProvider::max_level`]. Returns a summary of what happened.
167    ///
168    /// Note: the engine drives the *mechanism* (cross thresholds, recalc). Any
169    /// game-specific quirks (e.g. the Gen-1 experience underflow) live in the
170    /// game's [`ExpProvider::exp_for_level`] implementation and in how the game
171    /// chooses to call this method.
172    pub fn gain_exp(&mut self, provider: &P, amount: u32) -> LevelUp<P> {
173        let old_level = self.level;
174        self.exp = self.exp.saturating_add(amount);
175
176        let max_level = provider.max_level();
177        // Clamp accumulated exp to never exceed the max-level threshold.
178        let max_exp = provider.exp_for_level(self.species, max_level);
179        if self.exp > max_exp {
180            self.exp = max_exp;
181        }
182
183        let mut learned_moves: Vec<P::MoveId> = Vec::new();
184
185        while self.level < max_level {
186            let next = self.level + 1;
187            let needed = provider.exp_for_level(self.species, next);
188            if self.exp >= needed {
189                // Capture max HP before the recalc so the HP-growth policy hook
190                // can apply the game's delta rule (Gen-1: grow by the increase).
191                let old_max_hp = self.max_hp(provider);
192                self.level = next;
193                // Recompute stats for the new level. `recalc_stats` clamps
194                // `current_hp` to the new max; the policy hook below then sets
195                // the authoritative value (and is re-clamped), so the interim
196                // clamp here is harmless.
197                self.recalc_stats(provider);
198                let new_max_hp = self.max_hp(provider);
199                let new_hp = provider.levelup_current_hp(old_max_hp, new_max_hp, self.current_hp);
200                self.current_hp = new_hp.min(new_max_hp);
201
202                // Learn any moves gained at this level (default: none).
203                let mut gained =
204                    provider.learn_moves_on_levelup(self.species, next, &mut self.moves);
205                learned_moves.append(&mut gained);
206            } else {
207                break;
208            }
209        }
210
211        LevelUp {
212            levels_gained: self.level - old_level,
213            old_level,
214            new_level: self.level,
215            learned_moves,
216        }
217    }
218}
219
220impl<P: EvolutionProvider> MonsterInstance<P> {
221    /// If the provider says we evolve, switch species and recalc stats.
222    ///
223    /// Returns the new species id if evolution happened, otherwise `None`
224    /// (the provider returning `None` is how evolution is canceled).
225    pub fn try_evolve(
226        &mut self,
227        provider: &P,
228        trigger: EvolutionTrigger<P::EvoItem>,
229    ) -> Option<P::SpeciesId> {
230        let target = provider.evolution_target(self, trigger)?;
231        self.species = target;
232        self.recalc_stats(provider);
233        Some(target)
234    }
235}
236
237impl<P: MonsterProvider> MonsterInstance<P> {
238    /// Build a [`BattlerState`] snapshot for the battle engine.
239    ///
240    /// This is the seam the later battle-migration milestone builds on. The
241    /// engine's [`BattlerState`] is generic over a single [`BattleProvider`]
242    /// `B`, whose associated `Species` / `Move` / `Stat` types are *independent*
243    /// of this monster's [`MonsterProvider`]. To stay fully game-agnostic the
244    /// caller supplies the conversions:
245    ///
246    /// - `map_species`: `P::SpeciesId -> B::Species`
247    /// - `map_move`: `P::MoveId -> B::Move`
248    /// - `map_stat`: `P::Stat -> B::Stat`
249    ///
250    /// Stat stages start neutral, status defaults to `None`, and the live HP /
251    /// level / species / stats / moves are carried over. Status is intentionally
252    /// **not** mapped here (it is a battle-provider-defined `Option<B::Status>`);
253    /// the caller can set `bs.status` afterwards if needed. This keeps the
254    /// mapping total and the engine decoupled from any concrete status model.
255    pub fn to_battler<B, FS, FM, FST>(
256        &self,
257        provider: &P,
258        mut map_species: FS,
259        mut map_move: FM,
260        mut map_stat: FST,
261    ) -> BattlerState<B>
262    where
263        B: BattleProvider,
264        B::Stat: Copy + PartialEq,
265        FS: FnMut(P::SpeciesId) -> B::Species,
266        FM: FnMut(P::MoveId) -> B::Move,
267        FST: FnMut(P::Stat) -> B::Stat,
268    {
269        let mut stats: EnumMap<B::Stat, u16> = EnumMap::new();
270        for (stat, value) in self.stats.iter() {
271            stats.set(map_stat(stat), value);
272        }
273        let moves: Vec<B::Move> = self.moves.iter().map(|m| map_move(m.move_id)).collect();
274        let species = map_species(self.species);
275        let max_hp = self.max_hp(provider);
276        BattlerState::new(species, self.current_hp, max_hp, stats, moves)
277    }
278}