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}