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}