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}