dotzuki_engine/battle/stack/authoring.rs
1//! Developer-facing **effect-authoring** helpers (design §4.1).
2//!
3//! This is the second of the two real gaps the generalization closes (design
4//! §0): today the only `HandlerFn`s that exist are buried in pokered's
5//! `#[cfg(test)]` parity harness, with no ergonomic, documented way to author a
6//! move / ability / item / weather. This module is **concept-free**: it adds no
7//! new runtime type — it is pure constructors over the existing
8//! [`Effect`](super::event::Effect) / [`EventHook`](super::event::EventHook)
9//! shape (plus the typed [`RelayVar`](super::event::RelayVar) accessors, which
10//! live on `RelayVar` itself). It introduces **no** game concept and **no**
11//! `rand`; a game authors a `const Effect` with zero-capture `fn` handlers.
12//!
13//! ## The `effect!` macro
14//!
15//! Builds the same `&'static [EventHook]` table the engine folds, in a
16//! `const`/`static` context, so registrations stay zero-alloc constants:
17//!
18//! ```
19//! # use dotzuki_engine::battle::stack::event::{Effect, EffectId, EffectType,
20//! # Event, EventHook, HandlerFn, HandlerResult, RelayVar};
21//! # use dotzuki_engine::battle::stack::ctx::{BattleCtx, EffectProvider};
22//! # use dotzuki_engine::battle::BattlerRef;
23//! # use dotzuki_engine::effect;
24//! # fn my_hit<P: EffectProvider + ?Sized>(
25//! # _c: &mut BattleCtx<'_, P>, _r: RelayVar,
26//! # _t: BattlerRef, _s: BattlerRef, _e: EffectId) -> HandlerResult {
27//! # HandlerResult::Unchanged
28//! # }
29//! # fn my_residual<P: EffectProvider + ?Sized>(
30//! # _c: &mut BattleCtx<'_, P>, _r: RelayVar,
31//! # _t: BattlerRef, _s: BattlerRef, _e: EffectId) -> HandlerResult {
32//! # HandlerResult::Unchanged
33//! # }
34//! # trait MyGame: EffectProvider {}
35//! fn flamethrower<P: EffectProvider + ?Sized>() -> Effect<P> {
36//! effect!(EffectId(0x10), EffectType::Move, {
37//! DamagingHit => my_hit::<P>,
38//! Residual(20) => my_residual::<P>, // explicit on<Event>Order
39//! })
40//! }
41//! ```
42//!
43//! The first form (`Event => fn`) defaults `order` to `u32::MAX` (fires last,
44//! matching the engine's `EventHook` default); the second form
45//! (`Event(order) => fn`) sets `on<Event>Order` explicitly. `priority` defaults
46//! to `0` and `sub_order` to `None` (derive from the effect's `EffectType`),
47//! exactly as a hand-written `EventHook` would.
48
49/// Declarative effect builder (design §4.1). Expands to an
50/// [`Effect`](super::event::Effect) whose `hooks` is a `&'static` slice of
51/// [`EventHook`](super::event::EventHook)s. Usable anywhere an `Effect`
52/// expression is expected (incl. the initializer of a `const`/`static`).
53///
54/// Syntax: `effect!(<id expr>, <EffectType expr>, { <Event> [(<order>)] => <fn path>, ... })`
55///
56/// * `<Event>` is bare (e.g. `DamagingHit`) — it is qualified to
57/// `$crate::battle::stack::event::Event::<Event>` by the macro.
58/// * `(<order>)` is optional; omitted ⇒ `u32::MAX` (fires last).
59/// * `<fn path>` is any path to a [`HandlerFn`](super::event::HandlerFn)
60/// (a zero-capture `fn`), e.g. `my_handler::<P>`.
61#[macro_export]
62macro_rules! effect {
63 ($id:expr, $kind:expr, { $( $ev:ident $( ( $ord:expr ) )? => $fn:path ),* $(,)? }) => {
64 $crate::battle::stack::event::Effect {
65 id: $id,
66 kind: $kind,
67 hooks: &[ $(
68 $crate::battle::stack::event::EventHook {
69 event: $crate::battle::stack::event::Event::$ev,
70 call: $fn,
71 order: $crate::effect!(@ord $( $ord )?),
72 priority: 0,
73 sub_order: None,
74 },
75 )* ],
76 }
77 };
78 // order helpers: explicit value, or the `u32::MAX` default (fires last).
79 (@ord $o:expr) => { $o };
80 (@ord) => { u32::MAX };
81}
82
83#[cfg(test)]
84mod tests {
85 use crate::battle::stack::ctx::{BattleCtx, EffectProvider};
86 use crate::battle::stack::event::{EffectId, EffectType, Event, HandlerResult, RelayVar};
87 use crate::battle::BattlerRef;
88
89 // A generic zero-capture handler usable by any provider.
90 fn noop<P: EffectProvider + ?Sized>(
91 _c: &mut BattleCtx<'_, P>,
92 _r: RelayVar,
93 _t: BattlerRef,
94 _s: BattlerRef,
95 _e: EffectId,
96 ) -> HandlerResult {
97 HandlerResult::Unchanged
98 }
99
100 // A provider just to monomorphize the macro output.
101 use crate::battle::stack::tests_support::TProvider;
102
103 #[test]
104 fn effect_macro_builds_expected_hooks() {
105 let eff = effect!(EffectId(0x10), EffectType::Move, {
106 DamagingHit => noop::<TProvider>,
107 Residual(20) => noop::<TProvider>,
108 });
109 assert_eq!(eff.id, EffectId(0x10));
110 assert_eq!(eff.kind, EffectType::Move);
111 assert_eq!(eff.hooks.len(), 2);
112 // First hook: default order = u32::MAX (fires last).
113 assert_eq!(eff.hooks[0].event, Event::DamagingHit);
114 assert_eq!(eff.hooks[0].order, u32::MAX);
115 assert_eq!(eff.hooks[0].priority, 0);
116 assert_eq!(eff.hooks[0].sub_order, None);
117 // Second hook: explicit order = 20.
118 assert_eq!(eff.hooks[1].event, Event::Residual);
119 assert_eq!(eff.hooks[1].order, 20);
120 }
121
122 #[test]
123 fn relay_typed_accessors_and_scale() {
124 assert_eq!(RelayVar::Int(7).as_int(), 7);
125 assert_eq!(RelayVar::Unit.as_int(), 0);
126 assert_eq!(RelayVar::Damage(40).as_damage(), 40);
127 assert_eq!(RelayVar::Int(0).as_damage(), 0);
128 assert_eq!(RelayVar::Accuracy(200).as_accuracy(), 200);
129 assert!(RelayVar::Bool(true).as_bool());
130 assert!(!RelayVar::Bool(false).as_bool());
131 assert!(!RelayVar::Unit.as_bool());
132 // scale keeps the lane and applies num/den.
133 assert_eq!(RelayVar::Int(10).scale(3, 2), RelayVar::Int(15));
134 assert_eq!(RelayVar::Damage(40).scale(3, 2), RelayVar::Damage(60));
135 assert_eq!(RelayVar::Damage(100).scale(1, 2), RelayVar::Damage(50));
136 // div-by-zero is clamped to /1 (no panic).
137 assert_eq!(RelayVar::Int(10).scale(2, 0), RelayVar::Int(20));
138 // non-numeric relays pass through.
139 assert_eq!(RelayVar::Unit.scale(3, 2), RelayVar::Unit);
140 }
141}