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 {
119                actor: *actor,
120                move_: move_.clone(),
121            },
122            Self::Missed { actor } => Self::Missed { actor: *actor },
123            Self::Blocked { actor } => Self::Blocked { actor: *actor },
124            Self::Crit { actor } => Self::Crit { actor: *actor },
125            Self::Damaged {
126                target,
127                amount,
128                cause,
129            } => Self::Damaged {
130                target: *target,
131                amount: *amount,
132                cause: cause.clone(),
133            },
134            Self::Healed {
135                target,
136                amount,
137                cause,
138            } => Self::Healed {
139                target: *target,
140                amount: *amount,
141                cause: cause.clone(),
142            },
143            Self::StatusInflicted { target, status } => Self::StatusInflicted {
144                target: *target,
145                status: status.clone(),
146            },
147            Self::StatusCured { target, status } => Self::StatusCured {
148                target: *target,
149                status: status.clone(),
150            },
151            Self::StatChanged {
152                target,
153                stat,
154                delta,
155            } => Self::StatChanged {
156                target: *target,
157                stat: *stat,
158                delta: *delta,
159            },
160            Self::Fainted { who } => Self::Fainted { who: *who },
161        }
162    }
163}
164
165impl<P: EffectProvider + ?Sized> fmt::Debug for TurnEvent<P> {
166    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167        match self {
168            Self::MoveUsed { actor, move_ } => f
169                .debug_struct("MoveUsed")
170                .field("actor", actor)
171                .field("move_", move_)
172                .finish(),
173            Self::Missed { actor } => f.debug_struct("Missed").field("actor", actor).finish(),
174            Self::Blocked { actor } => f.debug_struct("Blocked").field("actor", actor).finish(),
175            Self::Crit { actor } => f.debug_struct("Crit").field("actor", actor).finish(),
176            Self::Damaged {
177                target,
178                amount,
179                cause,
180            } => f
181                .debug_struct("Damaged")
182                .field("target", target)
183                .field("amount", amount)
184                .field("cause", cause)
185                .finish(),
186            Self::Healed {
187                target,
188                amount,
189                cause,
190            } => f
191                .debug_struct("Healed")
192                .field("target", target)
193                .field("amount", amount)
194                .field("cause", cause)
195                .finish(),
196            Self::StatusInflicted { target, status } => f
197                .debug_struct("StatusInflicted")
198                .field("target", target)
199                .field("status", status)
200                .finish(),
201            Self::StatusCured { target, status } => f
202                .debug_struct("StatusCured")
203                .field("target", target)
204                .field("status", status)
205                .finish(),
206            Self::StatChanged {
207                target,
208                stat,
209                delta,
210            } => f
211                .debug_struct("StatChanged")
212                .field("target", target)
213                .field("stat", stat)
214                .field("delta", delta)
215                .finish(),
216            Self::Fainted { who } => f.debug_struct("Fainted").field("who", who).finish(),
217        }
218    }
219}
220
221/// Why a battler's HP changed — carried on [`TurnEvent::Damaged`] / [`TurnEvent::Healed`]
222/// so a game can narrate residual ticks distinctly ("hurt by POISON!", "sapped by LEECH
223/// SEED!"). `None` (the default) means ordinary move damage/heal — today's behaviour,
224/// unchanged. The engine records only what it holds at the residual fire site: the
225/// non-volatile [`Status`](crate::battle::BattleProvider::Status), or the volatile's opaque
226/// [`EffectStateKind`](EffectProvider::EffectStateKind) for a volatile-hosted residual —
227/// the game maps that token back to the concrete volatile (Toxic / Leech Seed / …).
228/// Engine-agnostic: the engine stores the token but never interprets it (symmetric with
229/// the `Status` variant).
230pub enum HpChangeCause<P: EffectProvider + ?Sized> {
231    /// A non-volatile status residual (Gen-1 burn / poison chip).
232    Status(P::Status),
233    /// A volatile-effect residual, carrying the game's opaque per-volatile token
234    /// ([`EffectStateKind`](EffectProvider::EffectStateKind)) so the game can tell
235    /// WHICH volatile ticked — Toxic ramp vs Leech Seed sap vs Bide unleash.
236    Volatile(P::EffectStateKind),
237}
238
239// Manual Clone/Debug (a derive would wrongly demand `P: Clone`/`P: Debug`). The kind
240// is `Clone` via the `EffectStateKind: Clone` bound; Debug omits it (it has no Debug
241// bound — and the parity oracle checks state/consumed, never the log's Debug text).
242impl<P: EffectProvider + ?Sized> Clone for HpChangeCause<P> {
243    fn clone(&self) -> Self {
244        match self {
245            Self::Status(s) => Self::Status(s.clone()),
246            Self::Volatile(k) => Self::Volatile(k.clone()),
247        }
248    }
249}
250impl<P: EffectProvider + ?Sized> fmt::Debug for HpChangeCause<P> {
251    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252        match self {
253            Self::Status(s) => f.debug_tuple("Status").field(s).finish(),
254            Self::Volatile(_) => f.write_str("Volatile(..)"),
255        }
256    }
257}
258
259/// An ordered log of [`TurnEvent`]s for one stack-driven turn.
260pub struct TurnLog<P: EffectProvider + ?Sized> {
261    /// The events, in the order they occurred this turn.
262    pub events: Vec<TurnEvent<P>>,
263}
264
265impl<P: EffectProvider + ?Sized> TurnLog<P> {
266    /// An empty log.
267    pub fn new() -> Self {
268        Self { events: Vec::new() }
269    }
270
271    /// Append one event.
272    pub fn push(&mut self, ev: TurnEvent<P>) {
273        self.events.push(ev);
274    }
275
276    /// The number of recorded events.
277    pub fn len(&self) -> usize {
278        self.events.len()
279    }
280
281    /// Whether no events were recorded.
282    pub fn is_empty(&self) -> bool {
283        self.events.is_empty()
284    }
285}
286
287impl<P: EffectProvider + ?Sized> Default for TurnLog<P> {
288    fn default() -> Self {
289        Self::new()
290    }
291}
292
293impl<P: EffectProvider + ?Sized> Clone for TurnLog<P> {
294    fn clone(&self) -> Self {
295        Self {
296            events: self.events.clone(),
297        }
298    }
299}
300
301impl<P: EffectProvider + ?Sized> fmt::Debug for TurnLog<P> {
302    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
303        f.debug_struct("TurnLog")
304            .field("events", &self.events)
305            .finish()
306    }
307}