Skip to main content

dotzuki_engine/battle/stack/
event.rs

1//! Event taxonomy, handler signature, and effect registration types for the
2//! Showdown-style effect-stack battle engine (design doc §1).
3//!
4//! Everything here is **100% game-agnostic**: no game-specific concrete
5//! types appear. The game registers `Effect`s carrying `HandlerFn` pointers via
6//! the [`EffectProvider`](super::ctx::EffectProvider) trait.
7//!
8//! ## The broadened taxonomy (design §1, P0b)
9//!
10//! The enum below is the **multi-gen authoring surface** (design §1.4): the 6
11//! groups / 31 kinds + the `Custom(u16)` escape hatch + the legacy `Residual`
12//! kept for the Gen-1 slices (see the note on `Residual`). Adding a variant is a
13//! **non-breaking engine change**: handlers subscribe by *listing*
14//! `EventHook { event: Event::X, .. }` and never `match` the whole enum — the
15//! only exhaustive matches live inside the engine driver, which we control. So
16//! growing this enum only *offers* new subscription points; it forces no
17//! existing handler or game to change. Kinds the engine driver does not yet
18//! *fire* are simply never collected for, hence inert (zero behavioral change),
19//! exactly as `Start`/`End`/`Faint` were in the POC.
20
21/// The closed taxonomy of dispatch *kinds* (design §1.1, broadened §1.4).
22///
23/// Events are an enum of keys with **no payload** — the payload rides in a
24/// typed [`RelayVar`] threaded through the fold. A closed enum (not a
25/// string-keyed bus) keeps the comparator and the parity tests auditable, which
26/// the Gen-1 quirks demand; [`Event::Custom`] is the open tail so a game is
27/// never *blocked* by the closed set.
28///
29/// Grouped per design §1.2. Variants beyond the POC's `BeforeMove`/
30/// `ModifyCritRatio`/`Accuracy`/`ModifyDamage`/`Damage`/`DamagingHit`/`Residual`
31/// are present as **subscription seams** — inert until a driver extension fires
32/// them.
33#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
34pub enum Event {
35    // ── Group A — Turn lifecycle (3) ──────────────────────────────────────
36    /// After speed-sort settled, before each move resolves.
37    BeforeTurn,
38    /// End-of-turn batch ordering (weather tick → status tick → leftovers → …);
39    /// drives the Gen-2+ end-of-turn sequence.
40    ResidualOrder,
41    /// Post-residual cleanup (Gen-5 form reset, counters).
42    AfterTurn,
43
44    // ── Group B — Action / move pipeline (9) ──────────────────────────────
45    /// Pre-move status gate (sleep/freeze/trap/flinch/recharge/confusion/para/
46    /// Truant) — the veto point. In the POC slice, paralysis full-para is gated
47    /// here.
48    BeforeMove,
49    /// Mutate the move in flight (multi-hit count, type override, Normalize).
50    ModifyMove,
51    /// Per-hit type override (Pixilate/Aerilate, Hidden Power) — split from
52    /// `ModifyMove` because abilities re-type *after* the move's own type pick.
53    ModifyType,
54    /// Numeric fold producing the critical-hit threshold (Focus Energy `/4`,
55    /// high-crit `×8`, Super Luck, Razor Claw). Crit is drawn here — *before*
56    /// `Accuracy` (design §4).
57    ModifyCritRatio,
58    /// Accuracy fold returning a `0..=255` INT (the Gen-1 1/256 miss; Compound
59    /// Eyes, Hustle, Sand Veil).
60    Accuracy,
61    /// Fly/Dig/Dive/Bounce gate; `fast_exit` veto.
62    Invulnerability,
63    /// Numeric fold scaling the final damage (the damage roll lands here; Life
64    /// Orb, weather boost, STAB-via-Adaptability).
65    ModifyDamage,
66    /// Type-effectiveness fold (Gen-1 immunity-as-miss; Levitate, Scrappy,
67    /// Tinted Lens, Wonder Guard).
68    Effectiveness,
69    /// Per-action cleanup (Hyper Beam recharge set, Life Orb recoil, Rocky
70    /// Helmet contact damage via the contact flag).
71    AfterMove,
72
73    // ── Group C — Hit / damage application (5) ─────────────────────────────
74    /// Pre-damage interception veto (Substitute swap-target, Protect/Detect,
75    /// Magic Bounce redirect).
76    TryHit,
77    /// Damage-application fold (Substitute absorb, Disguise, Endure/Sturdy
78    /// floor-to-1).
79    Damage,
80    /// A hit connected — secondary-effect + reactive fire-point (Counter/Bide
81    /// read, Static/Flame Body, recoil, drain).
82    DamagingHit,
83    /// Healing fold (Heal Block veto, Big Root item boost).
84    Heal,
85    /// Post-KO (Moxie/Beast Boost, Aftermath, Destiny Bond resolution).
86    AfterFaint,
87
88    // ── Group D — Status & stat changes (6) ────────────────────────────────
89    /// Veto setting a non-volatile status (type immunity, Immunity/Limber,
90    /// Safeguard, Substitute block).
91    TrySetStatus,
92    /// Status applied (Synchronize, Toxic Orb self-status).
93    AfterSetStatus,
94    /// Veto/modify a stat-stage change (Clear Body, Hyper Cutter, White Smoke).
95    TryBoost,
96    /// Stat change applied (Defiant/Competitive).
97    AfterBoost,
98    /// Persistent stat fold for the damage-formula reads (Huge Power, Choice
99    /// Band, para ÷4 speed, burn ÷2 atk) — one fold parameterized by `P::Stat`.
100    ModifyStat,
101    /// Weather/ability stat multipliers that layer *after* `ModifyStat` (Sand
102    /// Force, Chlorophyll, Swift Swim).
103    WeatherModifyStat,
104
105    // ── Group E — Lifecycle / presence (5) ─────────────────────────────────
106    /// An effect was added to a host (volatile applied; ability/item attached
107    /// on switch-in).
108    Start,
109    /// An effect was removed (volatile expired → Thrash self-confuse; item
110    /// consumed).
111    End,
112    /// A battler fainted.
113    Faint,
114    /// A battler entered — the cross-gen ability/item/hazard fire-point
115    /// (Intimidate, Drizzle, Stealth Rock damage, Toxic Spikes).
116    SwitchIn,
117    /// A battler is leaving (Regenerator, Natural Cure, pursuit, Baton Pass).
118    SwitchOut,
119
120    // ── Group F — Field / side (3) ─────────────────────────────────────────
121    /// Veto/replace a weather change (Air Lock/Cloud Nine suppress, Damp Rock
122    /// duration).
123    SetWeather,
124    /// Field-hosted end-of-turn tick (weather chip damage, Trick Room countdown,
125    /// terrain).
126    FieldResidual,
127    /// Side-hosted end-of-turn tick (Spikes, Wish, Reflect/Light Screen
128    /// countdown).
129    SideResidual,
130
131    /// **Accuracy-miss reaction** (the one true Gen-1 core touch, blueprint `15`
132    /// §3). Fired by the [`StackDriver`](super::driver::StackDriver) on the
133    /// accuracy-miss branch — the point where a move whiffed. Gen-1 Jump Kick /
134    /// Hi Jump Kick crash the user for 1 HP here. **Additive + DEFAULTED**: the
135    /// driver fires it through the move's own effect, so it is INERT for a move
136    /// (and a game) that registers no `OnMiss` hook — every existing slice/game
137    /// collects zero handlers for it, so the fold is a no-op and `consumed()` /
138    /// the byte stream are byte-identical. (`Custom` could not serve this: the
139    /// fire-point is INSIDE the driver's miss branch, which only the engine drives.)
140    OnMiss,
141
142    // ── Legacy (kept for the Gen-1 regression slices) ──────────────────────
143    /// PER-MOVER end-of-action residual (burn/psn/toxic → leech). Order is
144    /// load-bearing (design §6 #7).
145    ///
146    /// The §1.4 taxonomy folds this into `ResidualOrder`/`FieldResidual`/
147    /// `SideResidual`, but the 88 Gen-1 stack-parity slices fire `Residual`
148    /// directly — it is kept as an existing variant so those slices compile and
149    /// pass **unchanged** (the additive/non-breaking constraint trumps the
150    /// rename). New games should prefer the §1.4 kinds.
151    Residual,
152
153    // ── The open tail (design §1.4) ────────────────────────────────────────
154    /// Game-defined dispatch key. The engine dispatches it like any other event
155    /// (collect → sort → fold) but assigns it no built-in meaning — a game's
156    /// driver extension fires it. Lets a game add an interaction point WITHOUT
157    /// an engine change, at the cost of the closed-set audit guarantee for that
158    /// key.
159    Custom(u16),
160}
161
162/// The typed value threaded through a dispatch fold (design §1.2). Mirrors
163/// Showdown's relay variable.
164#[derive(Clone, Copy, Debug, PartialEq)]
165pub enum RelayVar {
166    /// No meaningful payload (side-effecting events such as `Residual`).
167    Unit,
168    /// A signed integer fold value.
169    Int(i64),
170    /// Accumulated/last damage.
171    Damage(u16),
172    /// An accuracy threshold in `0..=255`.
173    Accuracy(u8),
174    /// A boolean verdict (e.g. "did the gate allow the move?").
175    Bool(bool),
176}
177
178impl RelayVar {
179    /// Read the relay as a signed integer (design §4.1). Non-`Int` ⇒ `0`, so a
180    /// handler can fold an int relay without hand-matching every variant.
181    pub fn as_int(self) -> i64 {
182        if let RelayVar::Int(v) = self {
183            v
184        } else {
185            0
186        }
187    }
188
189    /// Read the relay as a damage value (design §4.1). Non-`Damage` ⇒ `0`.
190    pub fn as_damage(self) -> u16 {
191        if let RelayVar::Damage(v) = self {
192            v
193        } else {
194            0
195        }
196    }
197
198    /// Read the relay as an accuracy threshold (design §4.1). Non-`Accuracy`
199    /// ⇒ `0`.
200    pub fn as_accuracy(self) -> u8 {
201        if let RelayVar::Accuracy(v) = self {
202            v
203        } else {
204            0
205        }
206    }
207
208    /// Read the relay as a boolean verdict (design §4.1). Non-`Bool` ⇒ `false`.
209    pub fn as_bool(self) -> bool {
210        matches!(self, RelayVar::Bool(true))
211    }
212
213    /// Fold-friendly multiply: scale a numeric relay (`Int`/`Damage`/`Accuracy`)
214    /// by `num/den` (design §4.1, the `×1.5` / `×0.5` modifier shape). Damage and
215    /// accuracy clamp into their lanes; the result keeps the relay's lane so a
216    /// `ModifyDamage`/`Accuracy`/`ModifyStat` fold composes. A non-numeric relay
217    /// passes through untouched.
218    pub fn scale(self, num: u32, den: u32) -> RelayVar {
219        let den = den.max(1);
220        match self {
221            RelayVar::Int(v) => {
222                let scaled = (v as i128) * (num as i128) / (den as i128);
223                RelayVar::Int(scaled as i64)
224            }
225            RelayVar::Damage(v) => {
226                let scaled = (v as u64) * (num as u64) / (den as u64);
227                RelayVar::Damage(scaled.min(u16::MAX as u64) as u16)
228            }
229            RelayVar::Accuracy(v) => {
230                let scaled = (v as u32) * num / den;
231                RelayVar::Accuracy(scaled.min(u8::MAX as u32) as u8)
232            }
233            other => other,
234        }
235    }
236}
237
238/// The verdict a handler returns, mirroring Showdown's
239/// `undefined / value / false / null` (design §1.2).
240#[derive(Clone, Copy, Debug, PartialEq)]
241pub enum HandlerResult {
242    /// Relay passes through untouched, continue (Showdown `undefined`).
243    Unchanged,
244    /// `relay` becomes this value, continue (Showdown returns a value).
245    Set(RelayVar),
246    /// Relay → falsy: STOP, show "but it failed!" (Showdown `false`).
247    Fail,
248    /// Relay → falsy: STOP, no message (Showdown `null`).
249    FailSilent,
250}
251
252/// An effect identifier. The engine treats it opaquely; the game assigns ids to
253/// moves/statuses/volatiles. Used as the binary-search key into the
254/// `EffectState` arena and to address handler `source_effect`.
255#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
256pub struct EffectId(pub u32);
257
258/// The effect category, feeding the comparator's `sub_order` default
259/// (design §1.3). Gen-1 leaves this defaulting; kept for generality.
260#[derive(Clone, Copy, Debug, PartialEq, Eq)]
261pub enum EffectType {
262    /// A move's own effect.
263    Move,
264    /// A non-volatile status (burn/poison/…).
265    Status,
266    /// A volatile condition (Focus Energy, Substitute, …).
267    Condition,
268}
269
270impl EffectType {
271    /// The Showdown `effectType` table sub-order (design §1.3): lower fires
272    /// first. Gen-1 does not exercise this seam, but it is wired for review.
273    pub fn sub_order(self) -> u8 {
274        match self {
275            EffectType::Condition => 2,
276            EffectType::Status => 4,
277            EffectType::Move => 6,
278        }
279    }
280}
281
282/// A handler is a **zero-capture `fn` pointer** (design §1.2): it cannot capture
283/// or alias battle state, so the only mutable path is through `ctx`. Per-effect
284/// counter state lives in `EffectState`, not a closure.
285///
286/// `P` is the game's [`EffectProvider`](super::ctx::EffectProvider).
287pub type HandlerFn<P> = fn(
288    ctx: &mut super::ctx::BattleCtx<'_, P>,
289    relay: RelayVar,
290    target: crate::battle::BattlerRef,
291    source: crate::battle::BattlerRef,
292    source_effect: EffectId,
293) -> HandlerResult;
294
295/// One `(Event → HandlerFn)` subscription with its ordering metadata
296/// (design §1.5). `order`/`priority`/`sub_order` mirror Showdown's
297/// `on<Event>Order` / `on<Event>Priority` / effect-type sub-order.
298pub struct EventHook<P: super::ctx::EffectProvider + ?Sized> {
299    /// Which event this hook subscribes to.
300    pub event: Event,
301    /// The native handler.
302    pub call: HandlerFn<P>,
303    /// `on<Event>Order`; default `u32::MAX` fires last; LOW first.
304    pub order: u32,
305    /// `on<Event>Priority`; HIGH first.
306    pub priority: i32,
307    /// Optional explicit sub-order override; `None` ⇒ derive from effect type.
308    pub sub_order: Option<u8>,
309}
310
311impl<P: super::ctx::EffectProvider + ?Sized> Clone for EventHook<P> {
312    fn clone(&self) -> Self {
313        Self {
314            event: self.event,
315            call: self.call,
316            order: self.order,
317            priority: self.priority,
318            sub_order: self.sub_order,
319        }
320    }
321}
322
323impl<P: super::ctx::EffectProvider + ?Sized> Copy for EventHook<P> {}
324
325/// An effect = id/type + a sparse table of hooks (design §1.5). Moves,
326/// statuses, abilities, items all share this shape (Showdown's `BasicEffect`).
327/// The hook table is `'static` so registrations are zero-alloc constants.
328pub struct Effect<P: super::ctx::EffectProvider + ?Sized> {
329    /// The effect's id.
330    pub id: EffectId,
331    /// The effect category (feeds `sub_order`).
332    pub kind: EffectType,
333    /// The sparse `(Event → HandlerFn)` table.
334    pub hooks: &'static [EventHook<P>],
335}