Skip to main content

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}