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 =
137                provider.calc_stat(self.species, stat, self.level, &self.genetics, &self.training);
138            self.stats.set(stat, value);
139        }
140        let max = self.max_hp(provider);
141        if self.current_hp > max {
142            self.current_hp = max;
143        }
144    }
145
146    /// Max HP, using the provider-declared HP stat.
147    pub fn max_hp(&self, provider: &P) -> u16 {
148        self.stats.get(provider.hp_stat())
149    }
150
151    /// Whether the monster has fainted (0 HP).
152    pub fn is_fainted(&self) -> bool {
153        self.current_hp == 0
154    }
155}
156
157impl<P: ExpProvider> MonsterInstance<P> {
158    /// Add EXP, advancing level while the threshold for the next level is met.
159    ///
160    /// Recomputes stats on each level gained. Caps at the provider's
161    /// [`ExpProvider::max_level`]. Returns a summary of what happened.
162    ///
163    /// Note: the engine drives the *mechanism* (cross thresholds, recalc). Any
164    /// game-specific quirks (e.g. the Gen-1 experience underflow) live in the
165    /// game's [`ExpProvider::exp_for_level`] implementation and in how the game
166    /// chooses to call this method.
167    pub fn gain_exp(&mut self, provider: &P, amount: u32) -> LevelUp<P> {
168        let old_level = self.level;
169        self.exp = self.exp.saturating_add(amount);
170
171        let max_level = provider.max_level();
172        // Clamp accumulated exp to never exceed the max-level threshold.
173        let max_exp = provider.exp_for_level(self.species, max_level);
174        if self.exp > max_exp {
175            self.exp = max_exp;
176        }
177
178        let mut learned_moves: Vec<P::MoveId> = Vec::new();
179
180        while self.level < max_level {
181            let next = self.level + 1;
182            let needed = provider.exp_for_level(self.species, next);
183            if self.exp >= needed {
184                // Capture max HP before the recalc so the HP-growth policy hook
185                // can apply the game's delta rule (Gen-1: grow by the increase).
186                let old_max_hp = self.max_hp(provider);
187                self.level = next;
188                // Recompute stats for the new level. `recalc_stats` clamps
189                // `current_hp` to the new max; the policy hook below then sets
190                // the authoritative value (and is re-clamped), so the interim
191                // clamp here is harmless.
192                self.recalc_stats(provider);
193                let new_max_hp = self.max_hp(provider);
194                let new_hp = provider.levelup_current_hp(old_max_hp, new_max_hp, self.current_hp);
195                self.current_hp = new_hp.min(new_max_hp);
196
197                // Learn any moves gained at this level (default: none).
198                let mut gained =
199                    provider.learn_moves_on_levelup(self.species, next, &mut self.moves);
200                learned_moves.append(&mut gained);
201            } else {
202                break;
203            }
204        }
205
206        LevelUp {
207            levels_gained: self.level - old_level,
208            old_level,
209            new_level: self.level,
210            learned_moves,
211        }
212    }
213}
214
215impl<P: EvolutionProvider> MonsterInstance<P> {
216    /// If the provider says we evolve, switch species and recalc stats.
217    ///
218    /// Returns the new species id if evolution happened, otherwise `None`
219    /// (the provider returning `None` is how evolution is canceled).
220    pub fn try_evolve(
221        &mut self,
222        provider: &P,
223        trigger: EvolutionTrigger<P::EvoItem>,
224    ) -> Option<P::SpeciesId> {
225        let target = provider.evolution_target(self, trigger)?;
226        self.species = target;
227        self.recalc_stats(provider);
228        Some(target)
229    }
230}
231
232impl<P: MonsterProvider> MonsterInstance<P> {
233    /// Build a [`BattlerState`] snapshot for the battle engine.
234    ///
235    /// This is the seam the later battle-migration milestone builds on. The
236    /// engine's [`BattlerState`] is generic over a single [`BattleProvider`]
237    /// `B`, whose associated `Species` / `Move` / `Stat` types are *independent*
238    /// of this monster's [`MonsterProvider`]. To stay fully game-agnostic the
239    /// caller supplies the conversions:
240    ///
241    /// - `map_species`: `P::SpeciesId -> B::Species`
242    /// - `map_move`: `P::MoveId -> B::Move`
243    /// - `map_stat`: `P::Stat -> B::Stat`
244    ///
245    /// Stat stages start neutral, status defaults to `None`, and the live HP /
246    /// level / species / stats / moves are carried over. Status is intentionally
247    /// **not** mapped here (it is a battle-provider-defined `Option<B::Status>`);
248    /// the caller can set `bs.status` afterwards if needed. This keeps the
249    /// mapping total and the engine decoupled from any concrete status model.
250    pub fn to_battler<B, FS, FM, FST>(
251        &self,
252        provider: &P,
253        mut map_species: FS,
254        mut map_move: FM,
255        mut map_stat: FST,
256    ) -> BattlerState<B>
257    where
258        B: BattleProvider,
259        B::Stat: Copy + PartialEq,
260        FS: FnMut(P::SpeciesId) -> B::Species,
261        FM: FnMut(P::MoveId) -> B::Move,
262        FST: FnMut(P::Stat) -> B::Stat,
263    {
264        let mut stats: EnumMap<B::Stat, u16> = EnumMap::new();
265        for (stat, value) in self.stats.iter() {
266            stats.set(map_stat(stat), value);
267        }
268        let moves: Vec<B::Move> = self.moves.iter().map(|m| map_move(m.move_id)).collect();
269        let species = map_species(self.species);
270        let max_hp = self.max_hp(provider);
271        BattlerState::new(species, self.current_hp, max_hp, stats, moves)
272    }
273}