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::{
87 EffectId, EffectType, Event, HandlerResult, RelayVar,
88 };
89 use crate::battle::BattlerRef;
90
91 // A generic zero-capture handler usable by any provider.
92 fn noop<P: EffectProvider + ?Sized>(
93 _c: &mut BattleCtx<'_, P>,
94 _r: RelayVar,
95 _t: BattlerRef,
96 _s: BattlerRef,
97 _e: EffectId,
98 ) -> HandlerResult {
99 HandlerResult::Unchanged
100 }
101
102 // A provider just to monomorphize the macro output.
103 use crate::battle::stack::tests_support::TProvider;
104
105 #[test]
106 fn effect_macro_builds_expected_hooks() {
107 let eff = effect!(EffectId(0x10), EffectType::Move, {
108 DamagingHit => noop::<TProvider>,
109 Residual(20) => noop::<TProvider>,
110 });
111 assert_eq!(eff.id, EffectId(0x10));
112 assert_eq!(eff.kind, EffectType::Move);
113 assert_eq!(eff.hooks.len(), 2);
114 // First hook: default order = u32::MAX (fires last).
115 assert_eq!(eff.hooks[0].event, Event::DamagingHit);
116 assert_eq!(eff.hooks[0].order, u32::MAX);
117 assert_eq!(eff.hooks[0].priority, 0);
118 assert_eq!(eff.hooks[0].sub_order, None);
119 // Second hook: explicit order = 20.
120 assert_eq!(eff.hooks[1].event, Event::Residual);
121 assert_eq!(eff.hooks[1].order, 20);
122 }
123
124 #[test]
125 fn relay_typed_accessors_and_scale() {
126 assert_eq!(RelayVar::Int(7).as_int(), 7);
127 assert_eq!(RelayVar::Unit.as_int(), 0);
128 assert_eq!(RelayVar::Damage(40).as_damage(), 40);
129 assert_eq!(RelayVar::Int(0).as_damage(), 0);
130 assert_eq!(RelayVar::Accuracy(200).as_accuracy(), 200);
131 assert!(RelayVar::Bool(true).as_bool());
132 assert!(!RelayVar::Bool(false).as_bool());
133 assert!(!RelayVar::Unit.as_bool());
134 // scale keeps the lane and applies num/den.
135 assert_eq!(RelayVar::Int(10).scale(3, 2), RelayVar::Int(15));
136 assert_eq!(RelayVar::Damage(40).scale(3, 2), RelayVar::Damage(60));
137 assert_eq!(RelayVar::Damage(100).scale(1, 2), RelayVar::Damage(50));
138 // div-by-zero is clamped to /1 (no panic).
139 assert_eq!(RelayVar::Int(10).scale(2, 0), RelayVar::Int(20));
140 // non-numeric relays pass through.
141 assert_eq!(RelayVar::Unit.scale(3, 2), RelayVar::Unit);
142 }
143}