Skip to main content

dotzuki_rules/
bindings.rs

1//! The game binding seam (doc 11 §1, doc 12 §3.2).
2//!
3//! `dotzuki-rules` stays game-agnostic: it never names a concrete `P::Stat` /
4//! `P::Status` / `P::Type`. The data uses **names**, interned to `usize` indices
5//! at load. To actually apply a `Boost`/`InflictStatus`/`HasType`/`ApplyTypeChart`
6//! against a [`BattlerState<P>`](dotzuki_engine::battle::BattlerState) the interpreter
7//! asks the **game** to resolve those indices via this trait. This mirrors doc 12
8//! §3.2's two defaulted provider seams (`defender_types`, `type_chart_mult`), but
9//! lives game-side (no engine edit), keyed by the interned index the loader owns.
10//!
11//! All methods are **pure / RNG-free** (determinism, doc 11 §4.1): a binding may
12//! read battler state and the interned chart but must never draw randomness. The
13//! interpreter's only entropy is `ctx.rng`.
14
15use dotzuki_engine::battle::stack::{BattleCtx, EffectProvider};
16use dotzuki_engine::battle::{BattlerRef, BattlerState};
17
18/// Resolves interned data-layer indices to concrete `P::Stat`/`P::Status` and
19/// supplies the type-chart fold + defender-type membership, all pure.
20///
21/// A game implements this once for its provider; the loader carries it so the
22/// zero-capture [`interpret`](crate::interpret) bridge can reach it. The trait is
23/// generic over `P: EffectProvider`, so the engine learns nothing.
24pub trait RuleBindings<P: EffectProvider + ?Sized>: 'static {
25    /// Apply a signed stat-stage delta to `who` for the interned `stat_index`.
26    /// Returns `false` if the index is unknown (a no-op; the loader validates
27    /// names at compile, so this is defense-in-depth). Phase 1 applies directly;
28    /// the nested-`TryBoost` veto is driver orchestration (doc 11 §3).
29    fn apply_boost(&self, b: &mut BattlerState<P>, stat_index: usize, stages: i8) -> bool;
30
31    /// Set `who`'s non-volatile status for the interned `status_index`. Returns
32    /// `false` if the index is unknown.
33    fn set_status(&self, b: &mut BattlerState<P>, status_index: usize) -> bool;
34
35    /// Set `who`'s non-volatile status carrying a game-interpreted numeric
36    /// `amount` (e.g. Gen-1 sleep turns). The engine resolves `amount` from the
37    /// op's [`AmountSpec`](crate::AmountSpec) — drawing its OWN rng — and hands
38    /// the pure number here. **Defaulted** to delegate to
39    /// [`set_status`](Self::set_status) and ignore the amount, so a game whose
40    /// statuses carry no duration is unaffected. Pure; no entropy.
41    fn set_status_with_amount(
42        &self,
43        b: &mut BattlerState<P>,
44        status_index: usize,
45        _amount: u16,
46    ) -> bool {
47        self.set_status(b, status_index)
48    }
49
50    /// Build the game's OPAQUE `P::EffectStateKind` volatile for the vocabulary
51    /// `name` + already-resolved `amount` (the [`InflictVolatile`](crate::Op::InflictVolatile)
52    /// op). The engine installs whatever is returned generically (fresh arena
53    /// id) and never learns what the volatile means — only the game does.
54    /// **Defaulted to `None`** ⇒ a game with no volatiles (or that doesn't
55    /// recognise `name`) makes the op inert. Pure — the engine already drew any
56    /// rng needed for `amount`.
57    fn make_volatile(&self, _name: &str, _amount: u16) -> Option<P::EffectStateKind> {
58        None
59    }
60
61    /// Whether `who` has the type with interned chart `type_index` (the `HasType`
62    /// predicate, doc 11 §1.1). Pure read.
63    fn has_type(&self, b: &BattlerState<P>, type_index: usize) -> bool;
64
65    /// The chart fold for the in-flight `move_type_index` against `defender`'s
66    /// type(s), as ONE pre-combined integer rational `(num, den)` (doc 12 §3.2,
67    /// §5.3 — one rational ⇒ exactly one `scale`, avoiding per-step truncation).
68    /// Default `(1, 1)` ⇒ inert (no chart). Pure / RNG-free.
69    fn type_chart_mult(
70        &self,
71        _ctx: &BattleCtx<'_, P>,
72        _move_type_index: usize,
73        _defender: BattlerRef,
74    ) -> (u32, u32) {
75        (1, 1)
76    }
77
78    /// The in-flight folded stat index, if the driver stashed one for a
79    /// `StatIs` predicate (the Sandstorm `WeatherModifyStat` case, doc 11 §1).
80    /// Default `None` ⇒ `StatIs` never matches. Pure.
81    fn current_stat_index(&self, _ctx: &BattleCtx<'_, P>) -> Option<usize> {
82        None
83    }
84
85    /// Whether `who` currently has the live volatile named by `name` (the
86    /// `HasVolatile` predicate, blueprint `15` §2/§3 — the Substitute block on
87    /// side-status). The game inspects its own `ctx.effects` arena (the engine
88    /// treats `EffectStateKind` opaquely, so only the game can tell which arena
89    /// entry IS "Substitute"). **Defaulted to `false`** so a game with no volatiles
90    /// (every game built so far) is unaffected and the predicate never matches.
91    /// Pure read; no entropy.
92    fn has_volatile(&self, _ctx: &BattleCtx<'_, P>, _who: BattlerRef, _name: &str) -> bool {
93        false
94    }
95
96    /// **Damage-redirection seam** for the DIRECT-MUTATE ops (`SetHp` / `DamageFraction`
97    /// / `DamageCurrentHpFraction` / `RepeatHits`) that apply HP OUTSIDE the driver's
98    /// `Event::Damage` fold. Before such an op subtracts `amount` HP from `who`
99    /// (attributed to `source`), the interpreter asks the game whether a **damage sink**
100    /// on `who` should swallow it instead — a monster Substitute doll, a cross-game
101    /// shield/ward/decoy. Returning `true` means the game HANDLED the loss (it mutated
102    /// its own sink via `ctx`); the interpreter then SKIPS the direct HP write. Returning
103    /// `false` (the default) leaves the op to apply HP exactly as before.
104    ///
105    /// This is the ONLY binding permitted to MUTATE through `ctx` (every other is a pure
106    /// read) — it is the redirect analogue of the `TryBoost`/`Event::Damage` interception
107    /// the driver already fires for formula damage, extended to the ops the driver never
108    /// routes. **Defaulted to `false`** so every existing game (and every op) is
109    /// byte-identical: the loss applies unredirected, no `ctx` mutation, no entropy.
110    /// `source` lets a game exempt self-inflicted loss (recoil / self-KO) from its own
111    /// sink. Draws NO randomness.
112    fn redirect_hp_loss(
113        &self,
114        _ctx: &mut BattleCtx<'_, P>,
115        _who: BattlerRef,
116        _source: BattlerRef,
117        _amount: u16,
118    ) -> bool {
119        false
120    }
121
122    /// Whether the in-flight move's type (`move_type_index`, recovered from the
123    /// record's `type:`) equals one of `who`'s types (the `MoveTypeIsDefenderType`
124    /// predicate — Gen-1 burn/freeze/paralyze self-type-immunity quirk #23,
125    /// blueprint `15` §2/§3). **Defaulted to `false`** ⇒ the quirk never fires for a
126    /// game that does not implement it. Pure read; no entropy. The default body
127    /// delegates to [`has_type`](Self::has_type) so a game whose `has_type` already
128    /// answers chart membership gets the quirk for free by overriding nothing — but
129    /// the engine has no `move_type_index` for a generic predicate, so the loader
130    /// passes it through and the binding decides.
131    fn move_type_is_defender_type(
132        &self,
133        ctx: &BattleCtx<'_, P>,
134        move_type_index: usize,
135        who: BattlerRef,
136    ) -> bool {
137        self.has_type(ctx.battler(who), move_type_index)
138    }
139
140    /// Whether `who` currently has the non-volatile status at interned
141    /// `status_index` (the `TargetHasStatus` predicate — the Dream Eater sleep
142    /// gate, blueprint `15` §2). The status index is the game's vocabulary (the same
143    /// indices `set_status` consumes). **Defaulted to `false`** ⇒ a game that does
144    /// not implement it never matches. Pure read; no entropy.
145    fn has_status(&self, _b: &BattlerState<P>, _status_index: usize) -> bool {
146        false
147    }
148
149    /// Whether `b` has ANY non-volatile status (the `TargetHasAnyStatus`
150    /// predicate — the Toxic "already-statused ⇒ fail" guard). The engine knows
151    /// no concrete status, so the game answers. **Defaulted to `false`** ⇒ a game
152    /// that doesn't implement it never matches. Pure read; no entropy.
153    fn has_any_status(&self, _b: &BattlerState<P>) -> bool {
154        false
155    }
156
157    /// The level of battler `b` (the `LevelGE` predicate's gate + the level-based
158    /// [`SetDamage`](crate::Op::SetDamage) sources — Seismic Toss / Night Shade /
159    /// Psywave; blueprint `15` §2/§3). [`BattlerState<P>`](dotzuki_engine::battle::BattlerState)
160    /// carries no `level` field (the engine is level-agnostic), so the game answers
161    /// it here. **Defaulted to `0`** ⇒ a game that authors no level-gated op is
162    /// unaffected (it never calls this) and `LevelGE` is `0 >= 0 == true`. Pure
163    /// read; no entropy. Game-agnostic: "a number the binding supplies per battler".
164    fn battler_level(&self, _b: &BattlerState<P>) -> u16 {
165        0
166    }
167
168    /// Map an interned resource index (the ruleset's `resources:` order) to the
169    /// engine's opaque resource id used in [`ResourcePool`](dotzuki_engine::battle::ResourcePool)
170    /// / [`BattleProvider::move_cost`](dotzuki_engine::battle::BattleProvider::move_cost)
171    /// (the MP/SP/mana cost gate, doc 13 §4). **Defaulted** so games with no
172    /// resources need not implement it — they never reference a resource, so it is
173    /// never called. Default: identity (`index as u16`). Pure.
174    fn resource_id(&self, resource_index: usize) -> u16 {
175        resource_index as u16
176    }
177
178    /// Whether `b` can pay `amount` of the resource at interned `resource_index`
179    /// (the `PayResource` op's gate, doc 13 §4). Pure read. The default delegates
180    /// to the engine's [`ResourcePool`](dotzuki_engine::battle::ResourcePool) via
181    /// [`resource_id`](Self::resource_id) — a game that stores its pool on
182    /// `BattlerState.resources` (the engine default) gets correct behavior for free.
183    fn can_pay_resource(&self, b: &BattlerState<P>, resource_index: usize, amount: u16) -> bool {
184        b.can_pay_resource(self.resource_id(resource_index), amount)
185    }
186
187    /// Deduct `amount` of the resource at interned `resource_index` from `b` (the
188    /// `PayResource` op's deduction). **Pure arithmetic — no rng.** Default
189    /// delegates to the engine `ResourcePool`.
190    fn pay_resource(&self, b: &mut BattlerState<P>, resource_index: usize, amount: u16) {
191        b.pay_resource(self.resource_id(resource_index), amount);
192    }
193}