Skip to main content

dotzuki_engine/party/
mod.rs

1//! Generic, game-agnostic party / monster model (milestone **P0a**).
2//!
3//! This module lifts the party/monster/box model out of any specific game into
4//! the engine. The engine knows *nothing* game-specific: species, moves, items,
5//! stat formulas, EXP curves, and evolution conditions are all supplied by the
6//! game through provider traits, mirroring the existing
7//! [`crate::GameData`] / [`crate::battle::BattleProvider`] /
8//! [`crate::battle::ItemProvider`](crate::items::ItemProvider) pattern.
9//!
10//! The engine offers *mechanism* (storing instances, driving level-up /
11//! evolution / party transitions); the game supplies *policy* (the numbers).
12//!
13//! There is **no `rand` dependency**: if a game needs randomness (e.g. for
14//! DV/IV generation) it computes the value itself and hands the result to the
15//! engine, which only ever stores it.
16
17mod monster;
18mod party;
19
20pub use monster::{LevelUp, MonsterInstance, MonsterStatus, MoveSlot};
21pub use party::{BoxStore, Party, PartyFull, StorageBox};
22
23use core::fmt::Debug;
24
25/// Game-supplied definition of how monsters work.
26///
27/// This is the master provider for the party model, analogous to
28/// [`crate::GameData`]. It binds the game's concrete id types and supplies the
29/// stat-calculation hooks. The engine stores instances and drives transitions;
30/// the game decides every number.
31pub trait MonsterProvider {
32    /// Opaque species identifier (e.g. a numeric dex id newtype/enum).
33    type SpeciesId: Copy + Eq + Debug;
34    /// Opaque move identifier.
35    type MoveId: Copy + Eq + Debug;
36    /// Per-instance "genetics" the game uses in stat calc (Gen-1 DVs, modern
37    /// IVs). The engine treats this as opaque and just stores it. `PartialEq +
38    /// Eq` are required so [`MonsterInstance`] / [`Party`] can derive them
39    /// (handy for round-trip identity tests and snapshot comparison).
40    type Genetics: Clone + Debug + Default + PartialEq + Eq;
41    /// Accumulated training data (Gen-1 stat-exp, modern EVs). Opaque to the
42    /// engine.
43    type Training: Clone + Debug + Default + PartialEq + Eq;
44    /// The set of stats this game uses. Must be index-mappable / comparable.
45    type Stat: Copy + Eq + Debug;
46
47    /// Base stat value for a species + stat.
48    fn base_stat(&self, species: Self::SpeciesId, stat: Self::Stat) -> u16;
49
50    /// Compute a single derived stat from base / level / genetics / training.
51    ///
52    /// This is where the Gen-1 formula (or any other) lives — in the **game**.
53    fn calc_stat(
54        &self,
55        species: Self::SpeciesId,
56        stat: Self::Stat,
57        level: u8,
58        genetics: &Self::Genetics,
59        training: &Self::Training,
60    ) -> u16;
61
62    /// All stats the game iterates over (for full recalculation).
63    fn stats(&self) -> &[Self::Stat];
64
65    /// Which [`Self::Stat`] represents HP. The engine cannot guess this, so the
66    /// game declares it; [`MonsterInstance::max_hp`] uses it.
67    fn hp_stat(&self) -> Self::Stat;
68
69    /// Max number of moves a monster can know (Gen-1 = 4).
70    fn max_moves(&self) -> usize;
71}
72
73/// A small map from `P::Stat` to `u16`.
74///
75/// Because `P::Stat` is opaque to the engine, this is stored as a `Vec` kept in
76/// the provider's [`MonsterProvider::stats`] order. (We deliberately do *not*
77/// require an `EnumMap` here — the game's stat enum is unknown to the engine, so
78/// a `Vec` keyed by the provider's stat order is the pragmatic choice.)
79#[derive(Clone, Debug, PartialEq, Eq)]
80pub struct StatSet<P: MonsterProvider> {
81    values: Vec<(P::Stat, u16)>,
82}
83
84impl<P: MonsterProvider> StatSet<P> {
85    /// One zeroed entry per `provider.stats()`, in provider order.
86    pub fn zeroed(provider: &P) -> Self {
87        Self {
88            values: provider.stats().iter().map(|&s| (s, 0)).collect(),
89        }
90    }
91
92    /// Get the value for `stat`, or `0` if the stat is not present.
93    pub fn get(&self, stat: P::Stat) -> u16 {
94        self.values
95            .iter()
96            .find(|(s, _)| *s == stat)
97            .map(|(_, v)| *v)
98            .unwrap_or(0)
99    }
100
101    /// Set the value for `stat`. If the stat is not already present (e.g. the
102    /// set was built before the stat existed) it is appended.
103    pub fn set(&mut self, stat: P::Stat, value: u16) {
104        if let Some(entry) = self.values.iter_mut().find(|(s, _)| *s == stat) {
105            entry.1 = value;
106        } else {
107            self.values.push((stat, value));
108        }
109    }
110
111    /// Iterate over `(stat, value)` pairs in provider order.
112    pub fn iter(&self) -> impl Iterator<Item = (P::Stat, u16)> + '_ {
113        self.values.iter().map(|(s, v)| (*s, *v))
114    }
115}
116
117/// Total EXP / leveling hooks. Layered on top of [`MonsterProvider`] so the
118/// same id types are reused.
119pub trait ExpProvider: MonsterProvider {
120    /// Total EXP required to *be* at `level` for this species' growth group.
121    fn exp_for_level(&self, species: Self::SpeciesId, level: u8) -> u32;
122
123    /// Maximum attainable level (Gen-1 = 100).
124    fn max_level(&self) -> u8;
125
126    /// Decide the new *current* HP when the monster levels up and its max HP
127    /// changes from `old_max_hp` to `new_max_hp`.
128    ///
129    /// This is the engine's **HP-growth policy hook**. The default is the
130    /// game-agnostic "preserve absolute HP, clamp to the new max" behavior
131    /// (matching [`MonsterInstance::recalc_stats`]). Games that want the Gen-1
132    /// behavior — grow current HP by the max-HP delta — override this to return
133    /// `current_hp + (new_max_hp - old_max_hp)`.
134    ///
135    /// The engine clamps the returned value to `new_max_hp` regardless, so an
136    /// override cannot produce HP above the new maximum.
137    fn levelup_current_hp(&self, old_max_hp: u16, new_max_hp: u16, current_hp: u16) -> u16 {
138        let _ = old_max_hp;
139        current_hp.min(new_max_hp)
140    }
141
142    /// Learn any moves the monster gains by reaching `level`, mutating its
143    /// `moves` list, and return the ids of the moves that were newly learned.
144    ///
145    /// This is the engine's **move-learning hook**. It is called once per level
146    /// crossed (in ascending order) during [`MonsterInstance::gain_exp`]. The
147    /// default learns nothing (returns an empty `Vec`) so the engine never needs
148    /// to know any move data. Games override this to consult their learnset and
149    /// insert moves into `moves` using whatever slot/PP rules they use; the ids
150    /// returned here are surfaced in [`LevelUp::learned_moves`].
151    fn learn_moves_on_levelup(
152        &self,
153        species: Self::SpeciesId,
154        level: u8,
155        moves: &mut Vec<MoveSlot<Self>>,
156    ) -> Vec<Self::MoveId>
157    where
158        Self: Sized,
159    {
160        let _ = (species, level, moves);
161        Vec::new()
162    }
163}
164
165/// Evolution hooks. Layered on top of [`MonsterProvider`].
166///
167/// The trigger carries an opaque, game-defined item id ([`Self::EvoItem`]) so
168/// the engine never couples to a concrete item system.
169pub trait EvolutionProvider: MonsterProvider {
170    /// Opaque item identifier used by item-based evolutions.
171    type EvoItem: Copy + Eq + Debug;
172
173    /// Decide what species (if any) this instance evolves into *right now*,
174    /// given a trigger. The game encodes all conditions; the engine just
175    /// applies the result.
176    fn evolution_target(
177        &self,
178        inst: &MonsterInstance<Self>,
179        trigger: EvolutionTrigger<Self::EvoItem>,
180    ) -> Option<Self::SpeciesId>
181    where
182        Self: Sized;
183}
184
185/// What caused an evolution check. Parameterized by the game's opaque item id
186/// so the engine stays decoupled from any item system.
187#[derive(Clone, Copy, Debug, PartialEq, Eq)]
188pub enum EvolutionTrigger<Item> {
189    /// Triggered by gaining a level.
190    LevelUp,
191    /// Triggered by using an evolution item.
192    Item(Item),
193    /// Triggered by trading.
194    Trade,
195}
196
197#[cfg(test)]
198mod tests;