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;