Skip to main content

dotzuki_engine/battle/stack/
log.rs

1//! P6a — a generic, additive per-turn **event log** for stack-driven battles.
2//!
3//! [`StackDriver::execute_turn`](super::driver::StackDriver::execute_turn) returns
4//! only [`StackTurnResult`](super::driver::StackTurnResult) `{ first,
5//! second_cancelled }` — enough to *sequence* a turn, but NOT enough for a frontend
6//! to *narrate* it ("X used Y!", "Critical hit!", "X fainted!"). A game that renders
7//! a turn needs a structured record of WHAT HAPPENED.
8//!
9//! This module adds that record as a **generic, game-agnostic** [`TurnLog`] of
10//! [`TurnEvent`]s, populated by
11//! [`StackDriver::execute_turn_logged`](super::driver::StackDriver::execute_turn_logged).
12//! It is **ADDITIVE + DEFAULTED**: the plain `execute_turn` is unchanged and draws
13//! no log; the logging path runs the SAME turn (identical `rng` draw order,
14//! identical final [`BattleState`](crate::battle::BattleState)) and merely records a
15//! structural before/after diff at the driver's existing event sites. Every existing
16//! battle / test that calls `execute_turn` is byte-identical.
17//!
18//! The vocabulary is the universal JRPG turn surface — move used, miss, crit,
19//! damage, heal, status inflicted/cured, stat-stage change, faint — keyed by the
20//! engine's existing generic associated types (`P::Move` / `P::Status` / `P::Stat`)
21//! and [`BattlerRef`]. Game-specific PRESENTATION (effectiveness wording, exact
22//! phrasing, animation choice) is the frontend's job: the engine reports the
23//! structural truth in order, the game translates it to text/animation.
24
25use core::fmt;
26
27use crate::battle::stack::ctx::EffectProvider;
28use crate::battle::BattlerRef;
29
30/// One structural event observed during a stack-driven turn (see module docs).
31///
32/// Generic over the game's [`BattleProvider`](crate::battle::BattleProvider) associated types; the engine never
33/// interprets the move/status/stat values — it only records them, in order.
34pub enum TurnEvent<P: EffectProvider + ?Sized> {
35    /// `actor` began executing `move_` (it passed the `BeforeMove` gate + cost).
36    MoveUsed {
37        /// The battler whose move executed.
38        actor: BattlerRef,
39        /// The move that executed (the *effective* move — a lock-in override is
40        /// already resolved by the driver before this is recorded).
41        move_: P::Move,
42    },
43    /// `actor`'s move missed (the accuracy / immunity miss branch).
44    Missed {
45        /// The battler whose move missed.
46        actor: BattlerRef,
47    },
48    /// `actor`'s move was PREVENTED before it executed — a `BeforeMove` gate aborted
49    /// it (e.g. asleep / frozen / fully paralyzed / a confusion self-hit) or it could
50    /// not pay its resource cost. No `MoveUsed` is logged for a blocked move. The
51    /// engine reports only THAT the move was prevented; the game derives the *reason*
52    /// from the battler's status / volatiles (e.g. "is fast asleep!").
53    Blocked {
54        /// The battler whose move was prevented.
55        actor: BattlerRef,
56    },
57    /// `actor`'s move landed a critical hit.
58    Crit {
59        /// The battler who landed the crit.
60        actor: BattlerRef,
61    },
62    /// `target` lost `amount` HP this step.
63    Damaged {
64        /// The battler that lost HP.
65        target: BattlerRef,
66        /// HP lost (> 0).
67        amount: u16,
68        /// Why the HP was lost — `None` for move damage; `Some(..)` for a residual
69        /// tick (burn/poison/toxic/leech). See [`HpChangeCause`].
70        cause: Option<HpChangeCause<P>>,
71    },
72    /// `target` recovered `amount` HP this step.
73    Healed {
74        /// The battler that gained HP.
75        target: BattlerRef,
76        /// HP gained (> 0).
77        amount: u16,
78        /// Why the HP was gained — `None` for a move heal (drain/Recover); `Some(..)`
79        /// for a residual drain-to-source (Leech Seed's seeder gain).
80        cause: Option<HpChangeCause<P>>,
81    },
82    /// `target` gained the non-volatile `status`.
83    StatusInflicted {
84        /// The battler that gained a status.
85        target: BattlerRef,
86        /// The new non-volatile status.
87        status: P::Status,
88    },
89    /// `target`'s non-volatile status was cleared / cured.
90    StatusCured {
91        /// The battler whose status was cleared.
92        target: BattlerRef,
93        /// The status that was cleared.
94        status: P::Status,
95    },
96    /// `target`'s `stat` stage changed by `delta` signed steps.
97    StatChanged {
98        /// The battler whose stat stage changed.
99        target: BattlerRef,
100        /// The stat that changed.
101        stat: P::Stat,
102        /// The signed change in stage (e.g. `+1`, `-2`).
103        delta: i8,
104    },
105    /// `who` fainted (HP reached 0 this step).
106    Fainted {
107        /// The battler that fainted.
108        who: BattlerRef,
109    },
110}
111
112// Manual `Clone` / `Debug` (NOT derived): a derive would wrongly add `P: Clone` /
113// `P: Debug` bounds — `P` is a provider, never `Clone`. The fields are all `Clone` +
114// `Debug` via the trait's assoc-type bounds. (Same pattern as `EffectState<P>`.)
115impl<P: EffectProvider + ?Sized> Clone for TurnEvent<P> {
116    fn clone(&self) -> Self {
117        match self {
118            Self::MoveUsed { actor, move_ } => Self::MoveUsed { actor: *actor, move_: move_.clone() },
119            Self::Missed { actor } => Self::Missed { actor: *actor },
120            Self::Blocked { actor } => Self::Blocked { actor: *actor },
121            Self::Crit { actor } => Self::Crit { actor: *actor },
122            Self::Damaged { target, amount, cause } => Self::Damaged { target: *target, amount: *amount, cause: cause.clone() },
123            Self::Healed { target, amount, cause } => Self::Healed { target: *target, amount: *amount, cause: cause.clone() },
124            Self::StatusInflicted { target, status } => {
125                Self::StatusInflicted { target: *target, status: status.clone() }
126            }
127            Self::StatusCured { target, status } => {
128                Self::StatusCured { target: *target, status: status.clone() }
129            }
130            Self::StatChanged { target, stat, delta } => {
131                Self::StatChanged { target: *target, stat: *stat, delta: *delta }
132            }
133            Self::Fainted { who } => Self::Fainted { who: *who },
134        }
135    }
136}
137
138impl<P: EffectProvider + ?Sized> fmt::Debug for TurnEvent<P> {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        match self {
141            Self::MoveUsed { actor, move_ } => f
142                .debug_struct("MoveUsed")
143                .field("actor", actor)
144                .field("move_", move_)
145                .finish(),
146            Self::Missed { actor } => f.debug_struct("Missed").field("actor", actor).finish(),
147            Self::Blocked { actor } => f.debug_struct("Blocked").field("actor", actor).finish(),
148            Self::Crit { actor } => f.debug_struct("Crit").field("actor", actor).finish(),
149            Self::Damaged { target, amount, cause } => f
150                .debug_struct("Damaged")
151                .field("target", target)
152                .field("amount", amount)
153                .field("cause", cause)
154                .finish(),
155            Self::Healed { target, amount, cause } => f
156                .debug_struct("Healed")
157                .field("target", target)
158                .field("amount", amount)
159                .field("cause", cause)
160                .finish(),
161            Self::StatusInflicted { target, status } => f
162                .debug_struct("StatusInflicted")
163                .field("target", target)
164                .field("status", status)
165                .finish(),
166            Self::StatusCured { target, status } => f
167                .debug_struct("StatusCured")
168                .field("target", target)
169                .field("status", status)
170                .finish(),
171            Self::StatChanged { target, stat, delta } => f
172                .debug_struct("StatChanged")
173                .field("target", target)
174                .field("stat", stat)
175                .field("delta", delta)
176                .finish(),
177            Self::Fainted { who } => f.debug_struct("Fainted").field("who", who).finish(),
178        }
179    }
180}
181
182/// Why a battler's HP changed — carried on [`TurnEvent::Damaged`] / [`TurnEvent::Healed`]
183/// so a game can narrate residual ticks distinctly ("hurt by POISON!", "sapped by LEECH
184/// SEED!"). `None` (the default) means ordinary move damage/heal — today's behaviour,
185/// unchanged. The engine records only what it holds at the residual fire site: the
186/// non-volatile [`Status`](crate::battle::BattleProvider::Status), or the volatile's opaque
187/// [`EffectStateKind`](EffectProvider::EffectStateKind) for a volatile-hosted residual —
188/// the game maps that token back to the concrete volatile (Toxic / Leech Seed / …).
189/// Engine-agnostic: the engine stores the token but never interprets it (symmetric with
190/// the `Status` variant).
191pub enum HpChangeCause<P: EffectProvider + ?Sized> {
192    /// A non-volatile status residual (Gen-1 burn / poison chip).
193    Status(P::Status),
194    /// A volatile-effect residual, carrying the game's opaque per-volatile token
195    /// ([`EffectStateKind`](EffectProvider::EffectStateKind)) so the game can tell
196    /// WHICH volatile ticked — Toxic ramp vs Leech Seed sap vs Bide unleash.
197    Volatile(P::EffectStateKind),
198}
199
200// Manual Clone/Debug (a derive would wrongly demand `P: Clone`/`P: Debug`). The kind
201// is `Clone` via the `EffectStateKind: Clone` bound; Debug omits it (it has no Debug
202// bound — and the parity oracle checks state/consumed, never the log's Debug text).
203impl<P: EffectProvider + ?Sized> Clone for HpChangeCause<P> {
204    fn clone(&self) -> Self {
205        match self {
206            Self::Status(s) => Self::Status(s.clone()),
207            Self::Volatile(k) => Self::Volatile(k.clone()),
208        }
209    }
210}
211impl<P: EffectProvider + ?Sized> fmt::Debug for HpChangeCause<P> {
212    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213        match self {
214            Self::Status(s) => f.debug_tuple("Status").field(s).finish(),
215            Self::Volatile(_) => f.write_str("Volatile(..)"),
216        }
217    }
218}
219
220/// An ordered log of [`TurnEvent`]s for one stack-driven turn.
221pub struct TurnLog<P: EffectProvider + ?Sized> {
222    /// The events, in the order they occurred this turn.
223    pub events: Vec<TurnEvent<P>>,
224}
225
226impl<P: EffectProvider + ?Sized> TurnLog<P> {
227    /// An empty log.
228    pub fn new() -> Self {
229        Self { events: Vec::new() }
230    }
231
232    /// Append one event.
233    pub fn push(&mut self, ev: TurnEvent<P>) {
234        self.events.push(ev);
235    }
236
237    /// The number of recorded events.
238    pub fn len(&self) -> usize {
239        self.events.len()
240    }
241
242    /// Whether no events were recorded.
243    pub fn is_empty(&self) -> bool {
244        self.events.is_empty()
245    }
246}
247
248impl<P: EffectProvider + ?Sized> Default for TurnLog<P> {
249    fn default() -> Self {
250        Self::new()
251    }
252}
253
254impl<P: EffectProvider + ?Sized> Clone for TurnLog<P> {
255    fn clone(&self) -> Self {
256        Self { events: self.events.clone() }
257    }
258}
259
260impl<P: EffectProvider + ?Sized> fmt::Debug for TurnLog<P> {
261    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262        f.debug_struct("TurnLog").field("events", &self.events).finish()
263    }
264}