1#[macro_use]
24pub mod authoring;
25pub mod ctx;
26pub mod dispatch;
27pub mod driver;
28pub mod event;
29pub mod log;
30
31pub use ctx::{BattleCtx, EffectHost, EffectProvider, EffectState, MoveContext};
32pub use dispatch::{collect_handlers, compare, run_event, run_event_checked, CollectedHandler};
33pub use driver::{FirstMover, StackDriver, StackTurnResult};
34pub use event::{
35 Effect, EffectId, EffectType, Event, EventHook, HandlerFn, HandlerResult, RelayVar,
36};
37pub use log::{HpChangeCause, TurnEvent, TurnLog};
38
39#[cfg(test)]
40pub(crate) mod tests_support;
41
42#[cfg(test)]
45mod tests {
46 use super::*;
47 use crate::battle::rng::ScriptedRng;
48 use crate::battle::{
49 BattleProvider, BattleState, BattlerRef, BattlerState, DamageResult, EffectResult, EnumMap,
50 MoveEffect,
51 };
52
53 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
56 enum TStat {
57 Hp,
58 }
59 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
60 #[allow(dead_code)] enum TStatus {
62 Poisoned,
63 }
64 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
65 #[allow(dead_code)]
66 enum TType {
67 N,
68 }
69 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
70 enum TSpecies {
71 Mon,
72 }
73 #[derive(Debug, Clone, PartialEq)]
74 struct TMove {
75 power: u8,
76 }
77
78 #[derive(Clone)]
80 #[allow(dead_code)] enum TKind {
82 None,
83 Toxic { counter: u8 },
84 }
85
86 struct TProvider;
87
88 impl BattleProvider for TProvider {
89 type Monster = ();
90 type Move = TMove;
91 type Ability = ();
92 type Status = TStatus;
93 type Stat = TStat;
94 type Species = TSpecies;
95 type Type = TType;
96 type Item = ();
97
98 fn calculate_damage(
99 &self,
100 move_: &Self::Move,
101 _a: &BattlerState<Self>,
102 _d: &BattlerState<Self>,
103 _r: u8,
104 _c: bool,
105 ) -> DamageResult {
106 DamageResult {
107 damage: move_.power as u16,
108 effectiveness: 1.0,
109 is_miss: false,
110 }
111 }
112 fn select_move(&self, b: &BattlerState<Self>, _s: &BattleState<Self>) -> Self::Move {
113 b.moves.first().cloned().unwrap()
114 }
115 fn apply_move_effect(
116 &self,
117 _e: MoveEffect,
118 _u: &mut BattlerState<Self>,
119 _t: &mut BattlerState<Self>,
120 ) -> EffectResult {
121 EffectResult::NoEffect
122 }
123 fn create_monster(&self, s: Self::Species, _l: u8) -> BattlerState<Self> {
124 BattlerState::new(s, 100, 100, EnumMap::new(), vec![])
125 }
126 }
127
128 impl EffectProvider for TProvider {
129 type EffectStateKind = TKind;
130 fn effect_for_move(&self, _m: &Self::Move) -> Option<&'static Effect<Self>> {
131 None
132 }
133 fn effect_for_status(&self, _s: &Self::Status) -> Option<&'static Effect<Self>> {
134 None
135 }
136 fn turn_order_rank(
137 &self,
138 _state: &BattleState<Self>,
139 _who: BattlerRef,
140 _action: &Self::Move,
141 ) -> (i32, i32) {
142 (0, 0) }
144 }
145
146 fn mon(hp: u16) -> BattlerState<TProvider> {
147 let mut stats = EnumMap::new();
148 stats.set(TStat::Hp, hp);
149 BattlerState::new(TSpecies::Mon, hp, hp, stats, vec![TMove { power: 10 }])
150 }
151
152 fn deal_5<P: EffectProvider<EffectStateKind = TKind> + ?Sized>(
156 ctx: &mut BattleCtx<'_, P>,
157 _relay: RelayVar,
158 target: BattlerRef,
159 _source: BattlerRef,
160 _eff: EffectId,
161 ) -> HandlerResult {
162 ctx.battler_mut(target).take_damage(5);
163 HandlerResult::Unchanged
164 }
165
166 fn add_int<P: EffectProvider + ?Sized>(
169 _ctx: &mut BattleCtx<'_, P>,
170 relay: RelayVar,
171 _t: BattlerRef,
172 _s: BattlerRef,
173 _e: EffectId,
174 ) -> HandlerResult {
175 let n = match relay {
176 RelayVar::Int(n) => n,
177 _ => 0,
178 };
179 HandlerResult::Set(RelayVar::Int(n + 1))
180 }
181
182 fn counter_shaped<P: EffectProvider + ?Sized>(
187 ctx: &mut BattleCtx<'_, P>,
188 _relay: RelayVar,
189 target: BattlerRef,
190 source: BattlerRef,
191 _eff: EffectId,
192 ) -> HandlerResult {
193 let (tgt, src) = ctx.pair_mut(target, source);
194 let reflected = src.hp / 10; tgt.take_damage(reflected); HandlerResult::Unchanged
197 }
198
199 fn make_ctx_parts(
200 hp_a: u16,
201 hp_b: u16,
202 ) -> (
203 BattleState<TProvider>,
204 Vec<EffectState<TProvider>>,
205 MoveContext,
206 ) {
207 let state = BattleState::new(vec![mon(hp_a)], vec![mon(hp_b)]);
208 (state, Vec::new(), MoveContext::default())
209 }
210
211 #[test]
214 fn fold_dispatch_runs_and_threads_relay() {
215 let (mut state, mut effects, mut mv) = make_ctx_parts(100, 100);
216 let mut rng = ScriptedRng::new(vec![0]);
217 let mut ctx = BattleCtx {
218 state: &mut state,
219 effects: &mut effects,
220 mv: &mut mv,
221 rng: &mut rng,
222 };
223 let hs = vec![
226 CollectedHandler {
227 order: 1,
228 priority: 0,
229 speed: 0,
230 sub_order: 6,
231 effect_order: 0,
232 target: BattlerRef::OPPONENT,
233 source: BattlerRef::PLAYER,
234 source_effect: EffectId(1),
235 call: add_int::<TProvider>,
236 },
237 CollectedHandler {
238 order: 2,
239 priority: 0,
240 speed: 0,
241 sub_order: 6,
242 effect_order: 1,
243 target: BattlerRef::OPPONENT,
244 source: BattlerRef::PLAYER,
245 source_effect: EffectId(2),
246 call: deal_5::<TProvider>,
247 },
248 ];
249 let out = run_event(&mut ctx, hs, RelayVar::Int(0), false);
250 assert_eq!(out, RelayVar::Int(1), "relay threaded through add_int");
251 assert_eq!(state.opponent_battlers[0].hp, 95, "deal_5 mutated target");
252 }
253
254 #[test]
257 fn pair_mut_cross_side_branch_works() {
258 let (mut state, mut effects, mut mv) = make_ctx_parts(100, 50);
259 let mut rng = ScriptedRng::new(vec![0]);
260 let mut ctx = BattleCtx {
261 state: &mut state,
262 effects: &mut effects,
263 mv: &mut mv,
264 rng: &mut rng,
265 };
266 let (player, opp) = ctx.pair_mut(BattlerRef::PLAYER, BattlerRef::OPPONENT);
267 assert_eq!(player.hp, 100);
268 assert_eq!(opp.hp, 50);
269 let opp_hp = opp.hp;
271 player.take_damage(opp_hp);
272 assert_eq!(state.player_battlers[0].hp, 50);
273 assert_eq!(state.opponent_battlers[0].hp, 50);
274 }
275
276 #[test]
279 fn pair_mut_same_side_branch_works() {
280 let mut state = BattleState::new(vec![mon(100), mon(40)], vec![mon(100)]);
282 let mut effects: Vec<EffectState<TProvider>> = Vec::new();
283 let mut mv = MoveContext::default();
284 let mut rng = ScriptedRng::new(vec![0]);
285 let mut ctx = BattleCtx {
286 state: &mut state,
287 effects: &mut effects,
288 mv: &mut mv,
289 rng: &mut rng,
290 };
291 let a = BattlerRef::new(0, 1);
293 let b = BattlerRef::new(0, 0);
294 let (slot1, slot0) = ctx.pair_mut(a, b);
295 assert_eq!(slot1.hp, 40);
296 assert_eq!(slot0.hp, 100);
297 let read = slot0.hp;
298 slot1.take_damage(read / 4); assert_eq!(state.player_battlers[1].hp, 15);
300 assert_eq!(state.player_battlers[0].hp, 100);
301 }
302
303 #[test]
306 fn counter_shaped_handler_compiles_no_refcell() {
307 let (mut state, mut effects, mut mv) = make_ctx_parts(100, 70);
308 let mut rng = ScriptedRng::new(vec![0]);
309 let mut ctx = BattleCtx {
310 state: &mut state,
311 effects: &mut effects,
312 mv: &mut mv,
313 rng: &mut rng,
314 };
315 let hs = vec![CollectedHandler {
318 order: 1,
319 priority: 0,
320 speed: 0,
321 sub_order: 6,
322 effect_order: 0,
323 target: BattlerRef::OPPONENT,
324 source: BattlerRef::PLAYER,
325 source_effect: EffectId(1),
326 call: counter_shaped::<TProvider>,
327 }];
328 run_event(&mut ctx, hs, RelayVar::Unit, false);
329 assert_eq!(state.opponent_battlers[0].hp, 60);
331 }
332
333 #[test]
336 fn typed_effect_state_toxic_counter() {
337 let (mut state, mut effects, mut mv) = make_ctx_parts(100, 100);
338 effects.push(EffectState {
339 id: EffectId(7),
340 host: BattlerRef::PLAYER,
341 effect_order: 0,
342 kind: TKind::Toxic { counter: 0 },
343 });
344 let mut rng = ScriptedRng::new(vec![0]);
345 let mut ctx = BattleCtx {
346 state: &mut state,
347 effects: &mut effects,
348 mv: &mut mv,
349 rng: &mut rng,
350 };
351 let n = match &mut ctx.effect_mut(EffectId(7)).unwrap().kind {
352 TKind::Toxic { counter } => {
353 *counter = counter.saturating_add(1);
354 *counter
355 }
356 TKind::None => 0,
357 };
358 assert_eq!(n, 1);
359 assert!(ctx.effect_mut(EffectId(99)).is_none());
361 }
362
363 use crate::battle::rng::ScriptedRng as EngineScriptedRng;
373 use crate::battle::BattleAction;
374
375 fn force_dmg<P: EffectProvider + ?Sized>(
378 ctx: &mut BattleCtx<'_, P>,
379 _relay: RelayVar,
380 _target: BattlerRef,
381 _source: BattlerRef,
382 _eff: EffectId,
383 ) -> HandlerResult {
384 ctx.mv.damage = 10;
385 HandlerResult::Unchanged
386 }
387
388 static FORCE_DMG_EFFECT: Effect<TForce> = Effect {
389 id: EffectId(1),
390 kind: EffectType::Move,
391 hooks: &[EventHook {
392 event: Event::ModifyDamage,
393 call: force_dmg::<TForce>,
394 order: 1000,
395 priority: 0,
396 sub_order: None,
397 }],
398 };
399
400 struct TForce;
404
405 impl BattleProvider for TForce {
406 type Monster = ();
407 type Move = TMove;
408 type Ability = ();
409 type Status = TStatus;
410 type Stat = TStat;
411 type Species = TSpecies;
412 type Type = TType;
413 type Item = ();
414 fn calculate_damage(
415 &self,
416 _m: &Self::Move,
417 _a: &BattlerState<Self>,
418 _d: &BattlerState<Self>,
419 _r: u8,
420 _c: bool,
421 ) -> DamageResult {
422 DamageResult {
423 damage: 0,
424 effectiveness: 1.0,
425 is_miss: false,
426 }
427 }
428 fn select_move(&self, b: &BattlerState<Self>, _s: &BattleState<Self>) -> Self::Move {
429 b.moves.first().cloned().unwrap()
430 }
431 fn apply_move_effect(
432 &self,
433 _e: MoveEffect,
434 _u: &mut BattlerState<Self>,
435 _t: &mut BattlerState<Self>,
436 ) -> EffectResult {
437 EffectResult::NoEffect
438 }
439 fn create_monster(&self, s: Self::Species, _l: u8) -> BattlerState<Self> {
440 BattlerState::new(s, 100, 100, EnumMap::new(), vec![])
441 }
442 }
443
444 impl EffectProvider for TForce {
445 type EffectStateKind = TKind;
446 fn effect_for_move(&self, _m: &Self::Move) -> Option<&'static Effect<Self>> {
447 Some(&FORCE_DMG_EFFECT)
448 }
449 fn effect_for_status(&self, _s: &Self::Status) -> Option<&'static Effect<Self>> {
450 None
451 }
452 fn turn_order_rank(
453 &self,
454 _state: &BattleState<Self>,
455 who: BattlerRef,
456 _action: &Self::Move,
457 ) -> (i32, i32) {
458 if who.side == 0 {
460 (0, 0)
461 } else {
462 (1, 0)
463 }
464 }
465 fn forced_action(
466 &self,
467 effects: &[EffectState<Self>],
468 actor: BattlerRef,
469 chosen: &BattleAction<Self>,
470 ) -> Option<BattleAction<Self>> {
471 let locked = effects
473 .iter()
474 .any(|e| e.host == actor && matches!(e.kind, TKind::Toxic { .. }));
475 if locked {
476 Some(BattleAction::Nothing)
477 } else {
478 let _ = chosen;
479 None
480 }
481 }
482 }
483
484 fn force_mon(hp: u16) -> BattlerState<TForce> {
485 let mut stats = EnumMap::new();
486 stats.set(TStat::Hp, hp);
487 BattlerState::new(TSpecies::Mon, hp, hp, stats, vec![TMove { power: 10 }])
488 }
489
490 #[test]
491 fn forced_action_overrides_chosen_action() {
492 let mut state = BattleState::new(vec![force_mon(100)], vec![force_mon(100)]);
497 let mut effects: Vec<EffectState<TForce>> = vec![EffectState {
498 id: EffectId(50),
499 host: BattlerRef::PLAYER,
500 effect_order: 0,
501 kind: TKind::Toxic { counter: 0 },
502 }];
503 let actions = [
504 BattleAction::<TForce>::Fight {
505 move_: TMove { power: 10 },
506 },
507 BattleAction::<TForce>::Fight {
508 move_: TMove { power: 10 },
509 },
510 ];
511 let mut rng = EngineScriptedRng::new(vec![]);
512 let provider = TForce;
513 StackDriver::execute_turn(&provider, &mut state, &mut effects, actions, &mut rng);
514 assert_eq!(
516 state.opponent_battlers[0].hp, 100,
517 "locked player forced to Nothing"
518 );
519 assert_eq!(
521 state.player_battlers[0].hp, 90,
522 "unlocked opponent's Fight ran"
523 );
524 }
525
526 #[test]
527 fn forced_action_default_is_inert() {
528 let mut state = BattleState::new(vec![force_mon(100)], vec![force_mon(100)]);
530 let mut effects: Vec<EffectState<TForce>> = Vec::new();
531 let actions = [
532 BattleAction::<TForce>::Fight {
533 move_: TMove { power: 10 },
534 },
535 BattleAction::<TForce>::Fight {
536 move_: TMove { power: 10 },
537 },
538 ];
539 let mut rng = EngineScriptedRng::new(vec![]);
540 let provider = TForce;
541 StackDriver::execute_turn(&provider, &mut state, &mut effects, actions, &mut rng);
542 assert_eq!(
543 state.opponent_battlers[0].hp, 90,
544 "player Fight ran (no lock)"
545 );
546 assert_eq!(
547 state.player_battlers[0].hp, 90,
548 "opponent Fight ran (no lock)"
549 );
550 }
551
552 use super::log::TurnEvent;
555
556 #[test]
560 fn logged_turn_is_behaviorally_identical_to_plain() {
561 let actions = || {
562 [
563 BattleAction::<TForce>::Fight {
564 move_: TMove { power: 10 },
565 },
566 BattleAction::<TForce>::Fight {
567 move_: TMove { power: 10 },
568 },
569 ]
570 };
571 let mut s_plain = BattleState::new(vec![force_mon(100)], vec![force_mon(100)]);
573 let mut e_plain: Vec<EffectState<TForce>> = Vec::new();
574 let mut rng_plain = EngineScriptedRng::new(vec![]);
575 let r_plain = StackDriver::execute_turn(
576 &TForce,
577 &mut s_plain,
578 &mut e_plain,
579 actions(),
580 &mut rng_plain,
581 );
582 let mut s_log = BattleState::new(vec![force_mon(100)], vec![force_mon(100)]);
584 let mut e_log: Vec<EffectState<TForce>> = Vec::new();
585 let mut rng_log = EngineScriptedRng::new(vec![]);
586 let (r_log, log) = StackDriver::execute_turn_logged(
587 &TForce,
588 &mut s_log,
589 &mut e_log,
590 actions(),
591 &mut rng_log,
592 );
593
594 assert_eq!(r_plain, r_log, "same StackTurnResult");
595 assert_eq!(
596 rng_plain.consumed(),
597 rng_log.consumed(),
598 "same rng draw count"
599 );
600 assert_eq!(
601 s_plain.player_battlers[0].hp, s_log.player_battlers[0].hp,
602 "same player hp"
603 );
604 assert_eq!(
605 s_plain.opponent_battlers[0].hp, s_log.opponent_battlers[0].hp,
606 "same opponent hp"
607 );
608 assert!(!log.is_empty(), "the logged path recorded events");
609 }
610
611 #[test]
615 fn log_records_move_used_and_damage_both_movers() {
616 let mut state = BattleState::new(vec![force_mon(100)], vec![force_mon(100)]);
617 let mut effects: Vec<EffectState<TForce>> = Vec::new();
618 let actions = [
619 BattleAction::<TForce>::Fight {
620 move_: TMove { power: 10 },
621 },
622 BattleAction::<TForce>::Fight {
623 move_: TMove { power: 10 },
624 },
625 ];
626 let mut rng = EngineScriptedRng::new(vec![]);
627 let (_r, log) =
628 StackDriver::execute_turn_logged(&TForce, &mut state, &mut effects, actions, &mut rng);
629
630 let p = BattlerRef::PLAYER;
631 let o = BattlerRef::OPPONENT;
632 let ev = &log.events;
633 assert!(
634 matches!(ev[0], TurnEvent::MoveUsed { actor, .. } if actor == p),
635 "1: player MoveUsed"
636 );
637 assert!(
638 matches!(ev[1], TurnEvent::Damaged { target, amount: 10, .. } if target == o),
639 "2: opponent took 10"
640 );
641 assert!(
642 matches!(ev[2], TurnEvent::MoveUsed { actor, .. } if actor == o),
643 "3: opponent MoveUsed"
644 );
645 assert!(
646 matches!(ev[3], TurnEvent::Damaged { target, amount: 10, .. } if target == p),
647 "4: player took 10"
648 );
649 assert_eq!(
650 ev.len(),
651 4,
652 "exactly MoveUsed+Damaged ×2 (no crit/miss/status)"
653 );
654 }
655
656 #[test]
659 fn log_records_faint_and_cancels_second() {
660 let mut state = BattleState::new(vec![force_mon(100)], vec![force_mon(10)]);
661 let mut effects: Vec<EffectState<TForce>> = Vec::new();
662 let actions = [
663 BattleAction::<TForce>::Fight {
664 move_: TMove { power: 10 },
665 },
666 BattleAction::<TForce>::Fight {
667 move_: TMove { power: 10 },
668 },
669 ];
670 let mut rng = EngineScriptedRng::new(vec![]);
671 let (r, log) =
672 StackDriver::execute_turn_logged(&TForce, &mut state, &mut effects, actions, &mut rng);
673
674 assert!(r.second_cancelled, "defender KO'd → second move cancelled");
675 let o = BattlerRef::OPPONENT;
676 let ev = &log.events;
677 assert!(
678 matches!(ev[0], TurnEvent::MoveUsed { .. }),
679 "player MoveUsed"
680 );
681 assert!(
682 matches!(ev[1], TurnEvent::Damaged { target, amount: 10, .. } if target == o),
683 "opponent took 10"
684 );
685 assert!(
686 matches!(ev[2], TurnEvent::Fainted { who } if who == o),
687 "opponent fainted"
688 );
689 assert!(
690 !ev.iter()
691 .any(|e| matches!(e, TurnEvent::MoveUsed { actor, .. } if *actor == o)),
692 "cancelled opponent never logs a MoveUsed"
693 );
694 }
695
696 fn log_rich(
700 ctx: &mut BattleCtx<'_, TRich>,
701 _relay: RelayVar,
702 target: BattlerRef,
703 source: BattlerRef,
704 _eff: EffectId,
705 ) -> HandlerResult {
706 ctx.mv.damage = 10; ctx.battler_mut(source).stat_stages.set(TStat::Hp, 1); ctx.battler_mut(target).status = Some(TStatus::Poisoned); let b = ctx.battler_mut(source);
710 b.hp = (b.hp + 20).min(b.max_hp); HandlerResult::Unchanged
712 }
713
714 static LOG_RICH_EFFECT: Effect<TRich> = Effect {
715 id: EffectId(1),
716 kind: EffectType::Move,
717 hooks: &[EventHook {
718 event: Event::ModifyDamage,
719 call: log_rich,
720 order: 1000,
721 priority: 0,
722 sub_order: None,
723 }],
724 };
725
726 struct TRich;
729 impl BattleProvider for TRich {
730 type Monster = ();
731 type Move = TMove;
732 type Ability = ();
733 type Status = TStatus;
734 type Stat = TStat;
735 type Species = TSpecies;
736 type Type = TType;
737 type Item = ();
738 fn calculate_damage(
739 &self,
740 _m: &Self::Move,
741 _a: &BattlerState<Self>,
742 _d: &BattlerState<Self>,
743 _r: u8,
744 _c: bool,
745 ) -> DamageResult {
746 DamageResult {
747 damage: 0,
748 effectiveness: 1.0,
749 is_miss: false,
750 }
751 }
752 fn select_move(&self, b: &BattlerState<Self>, _s: &BattleState<Self>) -> Self::Move {
753 b.moves.first().cloned().unwrap()
754 }
755 fn apply_move_effect(
756 &self,
757 _e: MoveEffect,
758 _u: &mut BattlerState<Self>,
759 _t: &mut BattlerState<Self>,
760 ) -> EffectResult {
761 EffectResult::NoEffect
762 }
763 fn create_monster(&self, s: Self::Species, _l: u8) -> BattlerState<Self> {
764 BattlerState::new(s, 100, 100, EnumMap::new(), vec![])
765 }
766 }
767 impl EffectProvider for TRich {
768 type EffectStateKind = TKind;
769 fn effect_for_move(&self, _m: &Self::Move) -> Option<&'static Effect<Self>> {
770 Some(&LOG_RICH_EFFECT)
771 }
772 fn effect_for_status(&self, _s: &Self::Status) -> Option<&'static Effect<Self>> {
773 None
774 }
775 fn turn_order_rank(
776 &self,
777 _state: &BattleState<Self>,
778 who: BattlerRef,
779 _action: &Self::Move,
780 ) -> (i32, i32) {
781 if who.side == 0 {
782 (0, 0)
783 } else {
784 (1, 0)
785 }
786 }
787 }
788
789 #[test]
790 fn log_records_stat_status_and_heal_diffs() {
791 let mut player = BattlerState::<TRich>::new(
793 TSpecies::Mon,
794 50,
795 100,
796 EnumMap::new(),
797 vec![TMove { power: 10 }],
798 );
799 player.max_hp = 100;
800 let opponent = BattlerState::<TRich>::new(
801 TSpecies::Mon,
802 100,
803 100,
804 EnumMap::new(),
805 vec![TMove { power: 10 }],
806 );
807 let mut state = BattleState::new(vec![player], vec![opponent]);
808 let mut effects: Vec<EffectState<TRich>> = Vec::new();
809 let actions = [
810 BattleAction::<TRich>::Fight {
811 move_: TMove { power: 10 },
812 },
813 BattleAction::<TRich>::Nothing,
814 ];
815 let mut rng = EngineScriptedRng::new(vec![]);
816 let (_r, log) =
817 StackDriver::execute_turn_logged(&TRich, &mut state, &mut effects, actions, &mut rng);
818
819 let p = BattlerRef::PLAYER;
820 let o = BattlerRef::OPPONENT;
821 let ev = &log.events;
822 assert!(
823 ev.iter()
824 .any(|e| matches!(e, TurnEvent::MoveUsed { actor, .. } if *actor == p)),
825 "MoveUsed"
826 );
827 assert!(
828 ev.iter().any(
829 |e| matches!(e, TurnEvent::Damaged { target, amount: 10, .. } if *target == o)
830 ),
831 "Damaged target"
832 );
833 assert!(
834 ev.iter()
835 .any(|e| matches!(e, TurnEvent::StatusInflicted { target, .. } if *target == o)),
836 "StatusInflicted target"
837 );
838 assert!(
839 ev.iter().any(
840 |e| matches!(e, TurnEvent::StatChanged { target, delta: 1, .. } if *target == p)
841 ),
842 "StatChanged actor +1"
843 );
844 assert!(
845 ev.iter()
846 .any(|e| matches!(e, TurnEvent::Healed { target, amount: 20, .. } if *target == p)),
847 "Healed actor +20"
848 );
849 }
850
851 const MP: u16 = 0; static COST_DMG_EFFECT: Effect<TCost> = Effect {
866 id: EffectId(1),
867 kind: EffectType::Move,
868 hooks: &[EventHook {
869 event: Event::ModifyDamage,
870 call: force_dmg::<TCost>,
871 order: 1000,
872 priority: 0,
873 sub_order: None,
874 }],
875 };
876
877 struct TCost;
879
880 impl BattleProvider for TCost {
881 type Monster = ();
882 type Move = TMove;
883 type Ability = ();
884 type Status = TStatus;
885 type Stat = TStat;
886 type Species = TSpecies;
887 type Type = TType;
888 type Item = ();
889 fn calculate_damage(
890 &self,
891 _m: &Self::Move,
892 _a: &BattlerState<Self>,
893 _d: &BattlerState<Self>,
894 _r: u8,
895 _c: bool,
896 ) -> DamageResult {
897 DamageResult {
898 damage: 0,
899 effectiveness: 1.0,
900 is_miss: false,
901 }
902 }
903 fn select_move(&self, b: &BattlerState<Self>, _s: &BattleState<Self>) -> Self::Move {
904 b.moves.first().cloned().unwrap()
905 }
906 fn apply_move_effect(
907 &self,
908 _e: MoveEffect,
909 _u: &mut BattlerState<Self>,
910 _t: &mut BattlerState<Self>,
911 ) -> EffectResult {
912 EffectResult::NoEffect
913 }
914 fn create_monster(&self, s: Self::Species, _l: u8) -> BattlerState<Self> {
915 BattlerState::new(s, 100, 100, EnumMap::new(), vec![])
916 }
917 fn move_cost(&self, _move_: &Self::Move) -> &[(u16, u16)] {
919 &[(MP, 4)]
920 }
921 }
922
923 impl EffectProvider for TCost {
924 type EffectStateKind = TKind;
925 fn effect_for_move(&self, _m: &Self::Move) -> Option<&'static Effect<Self>> {
926 Some(&COST_DMG_EFFECT)
927 }
928 fn effect_for_status(&self, _s: &Self::Status) -> Option<&'static Effect<Self>> {
929 None
930 }
931 fn turn_order_rank(
932 &self,
933 _state: &BattleState<Self>,
934 who: BattlerRef,
935 _action: &Self::Move,
936 ) -> (i32, i32) {
937 if who.side == 0 {
939 (0, 0)
940 } else {
941 (1, 0)
942 }
943 }
944 }
945
946 fn cost_mon(hp: u16, mp: u16) -> BattlerState<TCost> {
948 let mut stats = EnumMap::new();
949 stats.set(TStat::Hp, hp);
950 BattlerState::new(TSpecies::Mon, hp, hp, stats, vec![TMove { power: 10 }])
951 .with_resource(MP, mp)
952 }
953
954 #[test]
955 fn cost_gate_pays_and_deducts_when_affordable() {
956 let mut state = BattleState::new(vec![cost_mon(100, 10)], vec![cost_mon(100, 0)]);
961 let mut effects: Vec<EffectState<TCost>> = Vec::new();
962 let actions = [
963 BattleAction::<TCost>::Fight {
964 move_: TMove { power: 10 },
965 },
966 BattleAction::<TCost>::Fight {
967 move_: TMove { power: 10 },
968 },
969 ];
970 let mut rng = EngineScriptedRng::new(vec![]);
971 StackDriver::execute_turn(&TCost, &mut state, &mut effects, actions, &mut rng);
972
973 assert_eq!(
975 state.player_battlers[0].resources.current(MP),
976 Some(6),
977 "affordable: 10 MP - 4 cost = 6 left"
978 );
979 assert_eq!(
980 state.opponent_battlers[0].hp, 90,
981 "paid move dealt its 10 damage"
982 );
983
984 assert_eq!(
988 state.player_battlers[0].hp, 100,
989 "insufficient opp move prevented → no damage"
990 );
991 assert_eq!(
992 state.opponent_battlers[0].resources.current(MP),
993 Some(0),
994 "insufficient: MP unchanged (no deduction on a prevented move)"
995 );
996 }
997
998 #[test]
999 fn cost_gate_prevents_when_unaffordable_and_leaves_mp_unchanged() {
1000 let mut state = BattleState::new(vec![cost_mon(100, 3)], vec![cost_mon(100, 0)]);
1003 let mut effects: Vec<EffectState<TCost>> = Vec::new();
1004 let actions = [
1005 BattleAction::<TCost>::Fight {
1006 move_: TMove { power: 10 },
1007 },
1008 BattleAction::<TCost>::Fight {
1009 move_: TMove { power: 10 },
1010 },
1011 ];
1012 let mut rng = EngineScriptedRng::new(vec![]);
1013 StackDriver::execute_turn(&TCost, &mut state, &mut effects, actions, &mut rng);
1014
1015 assert_eq!(
1016 state.opponent_battlers[0].hp, 100,
1017 "player could not pay → its move was prevented → opp unharmed"
1018 );
1019 assert_eq!(
1020 state.player_battlers[0].resources.current(MP),
1021 Some(3),
1022 "prevented move deducts NOTHING → MP unchanged at 3"
1023 );
1024 }
1025
1026 #[test]
1027 fn cost_gate_consumes_no_rng() {
1028 let mut state = BattleState::new(vec![cost_mon(100, 10)], vec![cost_mon(100, 1)]);
1034 let mut effects: Vec<EffectState<TCost>> = Vec::new();
1035 let actions = [
1036 BattleAction::<TCost>::Fight {
1037 move_: TMove { power: 10 },
1038 },
1039 BattleAction::<TCost>::Fight {
1040 move_: TMove { power: 10 },
1041 },
1042 ];
1043 let mut rng = EngineScriptedRng::new(vec![]);
1044 StackDriver::execute_turn(&TCost, &mut state, &mut effects, actions, &mut rng);
1045 assert_eq!(
1046 rng.consumed(),
1047 0,
1048 "the resource cost check + deduction consume NO randomness"
1049 );
1050 }
1051
1052 #[test]
1053 fn no_cost_no_resources_is_inert() {
1054 let mut state = BattleState::new(vec![force_mon(100)], vec![force_mon(100)]);
1059 let mut effects: Vec<EffectState<TForce>> = Vec::new();
1060 let actions = [
1061 BattleAction::<TForce>::Fight {
1062 move_: TMove { power: 10 },
1063 },
1064 BattleAction::<TForce>::Fight {
1065 move_: TMove { power: 10 },
1066 },
1067 ];
1068 let mut rng = EngineScriptedRng::new(vec![]);
1069 StackDriver::execute_turn(&TForce, &mut state, &mut effects, actions, &mut rng);
1070 assert_eq!(
1071 state.opponent_battlers[0].hp, 90,
1072 "no-cost move ran (inert gate)"
1073 );
1074 assert_eq!(
1075 state.player_battlers[0].hp, 90,
1076 "no-cost move ran (inert gate)"
1077 );
1078 assert!(
1079 state.player_battlers[0].resources.is_empty(),
1080 "pool defaulted EMPTY"
1081 );
1082 }
1083
1084 #[test]
1087 fn speed_tiebreak_draws_only_on_tie() {
1088 let mk = |eo: u64| CollectedHandler::<TProvider> {
1090 order: 1,
1091 priority: 0,
1092 speed: 0,
1093 sub_order: 6,
1094 effect_order: eo,
1095 target: BattlerRef::OPPONENT,
1096 source: BattlerRef::PLAYER,
1097 source_effect: EffectId(eo as u32),
1098 call: add_int::<TProvider>,
1099 };
1100 let mut distinct = vec![mk(0), mk(1)];
1102 let mut rng = ScriptedRng::new(vec![200, 200]);
1103 dispatch::speed_sort_tiebreak(&mut distinct, &mut rng);
1104 assert_eq!(
1105 rng.consumed(),
1106 0,
1107 "distinct effect_order ⇒ no tie ⇒ no draw"
1108 );
1109
1110 let mut tied = vec![mk(0), mk(0)];
1112 let mut rng2 = ScriptedRng::new(vec![200]);
1113 dispatch::speed_sort_tiebreak(&mut tied, &mut rng2);
1114 assert_eq!(rng2.consumed(), 1, "one tied pair ⇒ exactly one draw");
1115 }
1116
1117 #[test]
1120 fn comparator_lexical_order() {
1121 use core::cmp::Ordering;
1122 let h = |order, priority, eo| CollectedHandler::<TProvider> {
1123 order,
1124 priority,
1125 speed: 0,
1126 sub_order: 6,
1127 effect_order: eo,
1128 target: BattlerRef::OPPONENT,
1129 source: BattlerRef::PLAYER,
1130 source_effect: EffectId(0),
1131 call: add_int::<TProvider>,
1132 };
1133 assert_eq!(compare(&h(1, 0, 0), &h(2, 0, 0)), Ordering::Less);
1135 assert_eq!(compare(&h(1, 5, 0), &h(1, 1, 0)), Ordering::Less);
1137 assert_eq!(compare(&h(1, 0, 3), &h(1, 0, 9)), Ordering::Less);
1139 assert_eq!(compare(&h(1, 0, 0), &h(1, 0, 0)), Ordering::Equal);
1141 }
1142}
1143
1144#[cfg(test)]
1152mod multi_source_tests {
1153 use super::dispatch::{collect_handlers, run_event, run_event_checked};
1154 use super::tests_support::{marker, mon, parts, TKind, TProvider, TSpecies, MOCK_VOLATILE};
1155 use super::*;
1156 use crate::battle::stack::ctx::EffectHost;
1157 use crate::battle::{BattlerRef, EffectResult};
1158
1159 fn ctx_from<'a>(
1160 state: &'a mut crate::battle::BattleState<TProvider>,
1161 effects: &'a mut Vec<EffectState<TProvider>>,
1162 mv: &'a mut MoveContext,
1163 rng: &'a mut crate::battle::rng::ScriptedRng,
1164 ) -> BattleCtx<'a, TProvider> {
1165 BattleCtx {
1166 state,
1167 effects,
1168 mv,
1169 rng,
1170 }
1171 }
1172
1173 #[test]
1176 fn collects_ability_item_field_in_order() {
1177 let provider = TProvider { field_on: true };
1180 let (mut state, mut effects, mut mv, mut rng) =
1181 parts(mon(100, TSpecies::HasBoth), mon(100, TSpecies::Plain));
1182 let mut ctx = ctx_from(&mut state, &mut effects, &mut mv, &mut rng);
1183
1184 let mut hs = Vec::new();
1185 collect_handlers(
1186 &ctx,
1187 &provider,
1188 None, Event::DamagingHit,
1190 BattlerRef::OPPONENT, BattlerRef::PLAYER, &mut hs,
1193 );
1194 assert_eq!(hs.len(), 3, "ability + item + field collected");
1196
1197 run_event(&mut ctx, hs, RelayVar::Unit, false);
1198 assert_eq!(
1202 marker(&state, BattlerRef::PLAYER),
1203 11,
1204 "ability+item fired on host"
1205 );
1206 assert_eq!(
1207 marker(&state, BattlerRef::OPPONENT),
1208 100,
1209 "field fired on target"
1210 );
1211 }
1212
1213 #[test]
1216 fn multi_source_comparator_order() {
1217 let provider = TProvider { field_on: true };
1218 let (mut state, mut effects, mut mv, mut rng) =
1219 parts(mon(100, TSpecies::HasBoth), mon(100, TSpecies::Plain));
1220 effects.push(EffectState {
1222 id: MOCK_VOLATILE.id,
1223 host: BattlerRef::PLAYER,
1224 effect_order: 0,
1225 kind: TKind::Vol,
1226 });
1227 let ctx = ctx_from(&mut state, &mut effects, &mut mv, &mut rng);
1228
1229 let mut hs = Vec::new();
1230 collect_handlers(
1231 &ctx,
1232 &provider,
1233 None,
1234 Event::DamagingHit,
1235 BattlerRef::OPPONENT,
1236 BattlerRef::PLAYER,
1237 &mut hs,
1238 );
1239 hs.sort_by(super::dispatch::compare);
1240 let orders: Vec<u32> = hs.iter().map(|h| h.order).collect();
1241 assert_eq!(orders, vec![10, 15, 20, 30], "ability<volatile<item<field");
1242 }
1243
1244 fn ko_target<P: EffectProvider + ?Sized>(
1247 ctx: &mut BattleCtx<'_, P>,
1248 _r: RelayVar,
1249 target: BattlerRef,
1250 _s: BattlerRef,
1251 _e: EffectId,
1252 ) -> HandlerResult {
1253 ctx.battler_mut(target).hp = 0; HandlerResult::Unchanged
1255 }
1256 fn touch_target<P: EffectProvider<Stat = super::tests_support::TStat> + ?Sized>(
1257 ctx: &mut BattleCtx<'_, P>,
1258 _r: RelayVar,
1259 target: BattlerRef,
1260 _s: BattlerRef,
1261 _e: EffectId,
1262 ) -> HandlerResult {
1263 super::tests_support::mark(ctx, target, 7); HandlerResult::Unchanged
1265 }
1266
1267 #[test]
1268 fn re_check_skips_handler_whose_target_was_koed_midfold() {
1269 let (mut state, mut effects, mut mv, mut rng) =
1270 parts(mon(100, TSpecies::Plain), mon(100, TSpecies::Plain));
1271 let mut ctx = ctx_from(&mut state, &mut effects, &mut mv, &mut rng);
1272 let hs = vec![
1274 CollectedHandler {
1275 order: 1,
1276 priority: 0,
1277 speed: 0,
1278 sub_order: 6,
1279 effect_order: 0,
1280 target: BattlerRef::OPPONENT,
1281 source: BattlerRef::PLAYER,
1282 source_effect: EffectId(1),
1283 call: ko_target::<TProvider>,
1284 },
1285 CollectedHandler {
1286 order: 2,
1287 priority: 0,
1288 speed: 0,
1289 sub_order: 6,
1290 effect_order: 1,
1291 target: BattlerRef::OPPONENT,
1292 source: BattlerRef::PLAYER,
1293 source_effect: EffectId(2),
1294 call: touch_target::<TProvider>,
1295 },
1296 ];
1297 run_event_checked(&mut ctx, hs, RelayVar::Unit, false);
1298 assert_eq!(
1299 state.opponent_battlers[0].hp, 0,
1300 "first handler KO'd target"
1301 );
1302 assert_eq!(
1303 marker(&state, BattlerRef::OPPONENT),
1304 0,
1305 "re-check SKIPPED the second handler (dead target)"
1306 );
1307 }
1308
1309 #[test]
1312 fn plain_run_event_does_not_re_check() {
1313 let provider = TProvider::default();
1314 let _ = &provider; let (mut state, mut effects, mut mv, mut rng) =
1316 parts(mon(100, TSpecies::Plain), mon(100, TSpecies::Plain));
1317 let mut ctx = ctx_from(&mut state, &mut effects, &mut mv, &mut rng);
1318 let hs = vec![
1319 CollectedHandler {
1320 order: 1,
1321 priority: 0,
1322 speed: 0,
1323 sub_order: 6,
1324 effect_order: 0,
1325 target: BattlerRef::OPPONENT,
1326 source: BattlerRef::PLAYER,
1327 source_effect: EffectId(1),
1328 call: ko_target::<TProvider>,
1329 },
1330 CollectedHandler {
1331 order: 2,
1332 priority: 0,
1333 speed: 0,
1334 sub_order: 6,
1335 effect_order: 1,
1336 target: BattlerRef::OPPONENT,
1337 source: BattlerRef::PLAYER,
1338 source_effect: EffectId(2),
1339 call: touch_target::<TProvider>,
1340 },
1341 ];
1342 run_event(&mut ctx, hs, RelayVar::Unit, false);
1343 assert_eq!(
1344 marker(&state, BattlerRef::OPPONENT),
1345 7,
1346 "plain run_event fired the second handler (no re-check) — slice contract"
1347 );
1348 }
1349
1350 fn remove_other_effect<P: EffectProvider + ?Sized>(
1353 ctx: &mut BattleCtx<'_, P>,
1354 _r: RelayVar,
1355 _t: BattlerRef,
1356 _s: BattlerRef,
1357 _e: EffectId,
1358 ) -> HandlerResult {
1359 ctx.effects.retain(|e| e.id != MOCK_VOLATILE.id);
1361 HandlerResult::Unchanged
1362 }
1363
1364 #[test]
1365 fn handler_removing_effect_midfold_is_snapshot_safe() {
1366 let provider = TProvider::default();
1367 let _ = &provider; let (mut state, mut effects, mut mv, mut rng) =
1369 parts(mon(100, TSpecies::Plain), mon(100, TSpecies::Plain));
1370 effects.push(EffectState {
1371 id: MOCK_VOLATILE.id,
1372 host: BattlerRef::PLAYER,
1373 effect_order: 0,
1374 kind: TKind::Vol,
1375 });
1376 let mut ctx = ctx_from(&mut state, &mut effects, &mut mv, &mut rng);
1377 let hs = vec![
1379 CollectedHandler {
1380 order: 1,
1381 priority: 0,
1382 speed: 0,
1383 sub_order: 6,
1384 effect_order: 0,
1385 target: BattlerRef::PLAYER,
1386 source: BattlerRef::PLAYER,
1387 source_effect: EffectId(1),
1388 call: remove_other_effect::<TProvider>,
1389 },
1390 CollectedHandler {
1391 order: 2,
1392 priority: 0,
1393 speed: 0,
1394 sub_order: 6,
1395 effect_order: 1,
1396 target: BattlerRef::PLAYER,
1397 source: BattlerRef::PLAYER,
1398 source_effect: EffectId(2),
1399 call: touch_target::<TProvider>,
1400 },
1401 ];
1402 run_event_checked(&mut ctx, hs, RelayVar::Unit, false);
1403 assert!(effects.is_empty(), "volatile removed mid-fold");
1404 assert_eq!(
1405 marker(&state, BattlerRef::PLAYER),
1406 7,
1407 "second handler still fired from the owned snapshot (no invalidation)"
1408 );
1409 }
1410
1411 #[test]
1413 fn effect_host_routes_field_and_battler() {
1414 let es = EffectState::<TProvider> {
1416 id: EffectId(1),
1417 host: BattlerRef::PLAYER,
1418 effect_order: 0,
1419 kind: TKind::None,
1420 };
1421 assert_eq!(es.host_scope(), EffectHost::Battler(BattlerRef::PLAYER));
1422
1423 let h: EffectHost = BattlerRef::OPPONENT.into();
1425 assert_eq!(h, EffectHost::Battler(BattlerRef::OPPONENT));
1426 assert!(h == BattlerRef::OPPONENT, "EffectHost == BattlerRef");
1427 assert!(BattlerRef::OPPONENT == h, "BattlerRef == EffectHost");
1428 assert_ne!(EffectHost::Side(1), EffectHost::Field);
1429
1430 let provider = TProvider { field_on: true };
1434 let (mut state, mut effects, mut mv, mut rng) =
1435 parts(mon(100, TSpecies::Plain), mon(100, TSpecies::Plain));
1436 let mut ctx = ctx_from(&mut state, &mut effects, &mut mv, &mut rng);
1437 let mut hs = Vec::new();
1438 collect_handlers(
1439 &ctx,
1440 &provider,
1441 None,
1442 Event::DamagingHit,
1443 BattlerRef::PLAYER,
1444 BattlerRef::PLAYER,
1445 &mut hs,
1446 );
1447 assert_eq!(hs.len(), 1, "only the field effect (no ability/item)");
1448 run_event_checked(&mut ctx, hs, RelayVar::Unit, false);
1449 assert_eq!(
1450 marker(&state, BattlerRef::PLAYER),
1451 100,
1452 "field residual routed"
1453 );
1454 }
1455
1456 #[test]
1458 fn event_with_no_subscribers_is_noop() {
1459 let provider = TProvider::default(); let (mut state, mut effects, mut mv, mut rng) =
1461 parts(mon(100, TSpecies::Plain), mon(100, TSpecies::Plain));
1462 let ctx = ctx_from(&mut state, &mut effects, &mut mv, &mut rng);
1463 let mut hs = Vec::new();
1464 collect_handlers(
1465 &ctx,
1466 &provider,
1467 None,
1468 Event::DamagingHit,
1469 BattlerRef::OPPONENT,
1470 BattlerRef::PLAYER,
1471 &mut hs,
1472 );
1473 assert!(hs.is_empty(), "no live source ⇒ no handlers ⇒ no-op");
1474 drop(ctx);
1475 assert_eq!(marker(&state, BattlerRef::PLAYER), 0);
1476 assert_eq!(marker(&state, BattlerRef::OPPONENT), 0);
1477 let _ = EffectResult::NoEffect; }
1479
1480 #[test]
1483 fn default_resolvers_reduce_to_single_source_identity() {
1484 use super::dispatch::collect_from_effect;
1488 static SRC: Effect<TProvider> = Effect {
1489 id: EffectId(0x99),
1490 kind: EffectType::Move,
1491 hooks: &[EventHook {
1492 event: Event::DamagingHit,
1493 call: super::tests_support::mock_src_hit::<TProvider>,
1494 order: 5,
1495 priority: 0,
1496 sub_order: None,
1497 }],
1498 };
1499 let provider = TProvider::default();
1500 let (mut state, mut effects, mut mv, mut rng) =
1501 parts(mon(100, TSpecies::Plain), mon(100, TSpecies::Plain));
1502 let ctx = ctx_from(&mut state, &mut effects, &mut mv, &mut rng);
1503
1504 let mut single = Vec::new();
1505 collect_from_effect(
1506 &ctx,
1507 &SRC,
1508 Event::DamagingHit,
1509 BattlerRef::OPPONENT,
1510 BattlerRef::PLAYER,
1511 &mut single,
1512 );
1513 let mut multi = Vec::new();
1514 collect_handlers(
1515 &ctx,
1516 &provider,
1517 Some(&SRC),
1518 Event::DamagingHit,
1519 BattlerRef::OPPONENT,
1520 BattlerRef::PLAYER,
1521 &mut multi,
1522 );
1523 assert_eq!(single.len(), 1);
1524 assert_eq!(multi.len(), single.len(), "identity: same count");
1525 for (a, b) in single.iter().zip(multi.iter()) {
1526 assert_eq!(a.order, b.order);
1527 assert_eq!(a.priority, b.priority);
1528 assert_eq!(a.speed, b.speed);
1529 assert_eq!(a.sub_order, b.sub_order);
1530 assert_eq!(a.effect_order, b.effect_order);
1531 assert_eq!(a.target, b.target);
1532 assert_eq!(a.source, b.source);
1533 assert_eq!(a.source_effect, b.source_effect);
1534 }
1535 }
1536}
1537
1538#[cfg(test)]
1545mod effectiveness_fold_tests {
1546 use super::*;
1547 use crate::battle::rng::ScriptedRng as EngineScriptedRng;
1548 use crate::battle::stack::ctx::{BattleCtx, EffectProvider, EffectState};
1549 use crate::battle::stack::event::{
1550 Effect, EffectId, EffectType, Event, EventHook, HandlerResult, RelayVar,
1551 };
1552 use crate::battle::{
1553 BattleAction, BattleProvider, BattleState, BattlerRef, BattlerState, DamageResult,
1554 EffectResult, EnumMap, MoveEffect,
1555 };
1556
1557 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1558 enum EStat {
1559 Hp,
1560 }
1561 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1562 #[allow(dead_code)]
1563 enum EStatus {
1564 Poisoned,
1565 }
1566 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1567 #[allow(dead_code)]
1568 enum EType {
1569 N,
1570 }
1571 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1575 enum ESpecies {
1576 NoSub,
1578 Double,
1580 Half,
1582 Zero,
1584 }
1585 #[derive(Debug, Clone, PartialEq)]
1586 struct EMove {
1587 power: u8,
1588 }
1589 #[derive(Clone)]
1590 #[allow(dead_code)]
1591 enum EKind {
1592 None,
1593 }
1594
1595 struct EProvider;
1596
1597 impl BattleProvider for EProvider {
1598 type Monster = ();
1599 type Move = EMove;
1600 type Ability = ();
1601 type Status = EStatus;
1602 type Stat = EStat;
1603 type Species = ESpecies;
1604 type Type = EType;
1605 type Item = ();
1606 fn calculate_damage(
1607 &self,
1608 _m: &Self::Move,
1609 _a: &BattlerState<Self>,
1610 _d: &BattlerState<Self>,
1611 _r: u8,
1612 _c: bool,
1613 ) -> DamageResult {
1614 DamageResult {
1615 damage: 0,
1616 effectiveness: 1.0,
1617 is_miss: false,
1618 }
1619 }
1620 fn select_move(&self, b: &BattlerState<Self>, _s: &BattleState<Self>) -> Self::Move {
1621 b.moves.first().cloned().unwrap()
1622 }
1623 fn apply_move_effect(
1624 &self,
1625 _e: MoveEffect,
1626 _u: &mut BattlerState<Self>,
1627 _t: &mut BattlerState<Self>,
1628 ) -> EffectResult {
1629 EffectResult::NoEffect
1630 }
1631 fn create_monster(&self, s: Self::Species, _l: u8) -> BattlerState<Self> {
1632 BattlerState::new(s, 100, 100, EnumMap::new(), vec![])
1633 }
1634 }
1635
1636 impl EffectProvider for EProvider {
1637 type EffectStateKind = EKind;
1638 fn effect_for_move(&self, _m: &Self::Move) -> Option<&'static Effect<Self>> {
1643 Some(&EFF_MOVE_EFFECT)
1647 }
1648 fn effect_for_status(&self, _s: &Self::Status) -> Option<&'static Effect<Self>> {
1649 None
1650 }
1651 fn turn_order_rank(
1652 &self,
1653 _state: &BattleState<Self>,
1654 who: BattlerRef,
1655 _action: &Self::Move,
1656 ) -> (i32, i32) {
1657 if who.side == 0 {
1659 (0, 0)
1660 } else {
1661 (1, 0)
1662 }
1663 }
1664 }
1665
1666 fn eff_modify_damage(
1669 ctx: &mut BattleCtx<'_, EProvider>,
1670 _r: RelayVar,
1671 _t: BattlerRef,
1672 _s: BattlerRef,
1673 _e: EffectId,
1674 ) -> HandlerResult {
1675 ctx.mv.damage = 80;
1676 HandlerResult::Unchanged
1677 }
1678
1679 fn eff_effectiveness(
1683 ctx: &mut BattleCtx<'_, EProvider>,
1684 relay: RelayVar,
1685 _target: BattlerRef,
1686 source: BattlerRef,
1687 _e: EffectId,
1688 ) -> HandlerResult {
1689 let sp = ctx.battler(source).species;
1690 let (num, den) = match sp {
1691 ESpecies::Double => (2u32, 1u32),
1692 ESpecies::Half => (1, 2),
1693 ESpecies::Zero => (0, 1),
1694 ESpecies::NoSub => (1, 1), };
1696 HandlerResult::Set(relay.scale(num, den))
1697 }
1698
1699 static EFF_MOVE_EFFECT: Effect<EProvider> = Effect {
1701 id: EffectId(1),
1702 kind: EffectType::Move,
1703 hooks: &[
1704 EventHook {
1705 event: Event::ModifyDamage,
1706 call: eff_modify_damage,
1707 order: 1000,
1708 priority: 0,
1709 sub_order: None,
1710 },
1711 EventHook {
1712 event: Event::Effectiveness,
1713 call: eff_effectiveness,
1714 order: 1000,
1715 priority: 0,
1716 sub_order: None,
1717 },
1718 ],
1719 };
1720
1721 struct EProviderNoSub;
1724 impl BattleProvider for EProviderNoSub {
1725 type Monster = ();
1726 type Move = EMove;
1727 type Ability = ();
1728 type Status = EStatus;
1729 type Stat = EStat;
1730 type Species = ESpecies;
1731 type Type = EType;
1732 type Item = ();
1733 fn calculate_damage(
1734 &self,
1735 _m: &Self::Move,
1736 _a: &BattlerState<Self>,
1737 _d: &BattlerState<Self>,
1738 _r: u8,
1739 _c: bool,
1740 ) -> DamageResult {
1741 DamageResult {
1742 damage: 0,
1743 effectiveness: 1.0,
1744 is_miss: false,
1745 }
1746 }
1747 fn select_move(&self, b: &BattlerState<Self>, _s: &BattleState<Self>) -> Self::Move {
1748 b.moves.first().cloned().unwrap()
1749 }
1750 fn apply_move_effect(
1751 &self,
1752 _e: MoveEffect,
1753 _u: &mut BattlerState<Self>,
1754 _t: &mut BattlerState<Self>,
1755 ) -> EffectResult {
1756 EffectResult::NoEffect
1757 }
1758 fn create_monster(&self, s: Self::Species, _l: u8) -> BattlerState<Self> {
1759 BattlerState::new(s, 100, 100, EnumMap::new(), vec![])
1760 }
1761 }
1762 impl EffectProvider for EProviderNoSub {
1763 type EffectStateKind = EKind;
1764 fn effect_for_move(&self, _m: &Self::Move) -> Option<&'static Effect<Self>> {
1765 Some(&EFF_MOVE_NO_SUB_NS)
1766 }
1767 fn effect_for_status(&self, _s: &Self::Status) -> Option<&'static Effect<Self>> {
1768 None
1769 }
1770 fn turn_order_rank(
1771 &self,
1772 _state: &BattleState<Self>,
1773 who: BattlerRef,
1774 _action: &Self::Move,
1775 ) -> (i32, i32) {
1776 if who.side == 0 {
1777 (0, 0)
1778 } else {
1779 (1, 0)
1780 }
1781 }
1782 }
1783 fn eff_modify_damage_ns(
1784 ctx: &mut BattleCtx<'_, EProviderNoSub>,
1785 _r: RelayVar,
1786 _t: BattlerRef,
1787 _s: BattlerRef,
1788 _e: EffectId,
1789 ) -> HandlerResult {
1790 ctx.mv.damage = 80;
1791 HandlerResult::Unchanged
1792 }
1793 static EFF_MOVE_NO_SUB_NS: Effect<EProviderNoSub> = Effect {
1794 id: EffectId(2),
1795 kind: EffectType::Move,
1796 hooks: &[EventHook {
1797 event: Event::ModifyDamage,
1798 call: eff_modify_damage_ns,
1799 order: 1000,
1800 priority: 0,
1801 sub_order: None,
1802 }],
1803 };
1804
1805 fn mon_eff(species: ESpecies) -> BattlerState<EProvider> {
1806 let mut stats = EnumMap::new();
1807 stats.set(EStat::Hp, 200);
1808 BattlerState::new(species, 200, 200, stats, vec![EMove { power: 80 }])
1809 }
1810 fn mon_eff_ns(species: ESpecies) -> BattlerState<EProviderNoSub> {
1811 let mut stats = EnumMap::new();
1812 stats.set(EStat::Hp, 200);
1813 BattlerState::new(species, 200, 200, stats, vec![EMove { power: 80 }])
1814 }
1815
1816 fn run_player_turn(attacker: ESpecies) -> u16 {
1818 let mut state = BattleState::new(vec![mon_eff(attacker)], vec![mon_eff(ESpecies::NoSub)]);
1819 let mut effects: Vec<EffectState<EProvider>> = Vec::new();
1820 let actions = [
1821 BattleAction::<EProvider>::Fight {
1822 move_: EMove { power: 80 },
1823 },
1824 BattleAction::<EProvider>::Fight {
1825 move_: EMove { power: 80 },
1826 },
1827 ];
1828 let mut rng = EngineScriptedRng::new(vec![]);
1829 StackDriver::execute_turn(&EProvider, &mut state, &mut effects, actions, &mut rng);
1830 200 - state.opponent_battlers[0].hp
1831 }
1832
1833 #[test]
1834 fn effectiveness_fold_is_inert_with_no_subscriber() {
1835 let mut state = BattleState::new(
1839 vec![mon_eff_ns(ESpecies::NoSub)],
1840 vec![mon_eff_ns(ESpecies::NoSub)],
1841 );
1842 let mut effects: Vec<EffectState<EProviderNoSub>> = Vec::new();
1843 let actions = [
1844 BattleAction::<EProviderNoSub>::Fight {
1845 move_: EMove { power: 80 },
1846 },
1847 BattleAction::<EProviderNoSub>::Fight {
1848 move_: EMove { power: 80 },
1849 },
1850 ];
1851 let mut rng = EngineScriptedRng::new(vec![]);
1852 StackDriver::execute_turn(&EProviderNoSub, &mut state, &mut effects, actions, &mut rng);
1853 let dmg = 200 - state.opponent_battlers[0].hp;
1854 assert_eq!(
1855 dmg, 80,
1856 "no Effectiveness subscriber ⇒ damage unchanged (inert 1×)"
1857 );
1858 }
1859
1860 #[test]
1861 fn effectiveness_subscriber_scale_2_1_doubles() {
1862 assert_eq!(
1863 run_player_turn(ESpecies::Double),
1864 160,
1865 "scale(2,1): 80 → 160"
1866 );
1867 }
1868
1869 #[test]
1870 fn effectiveness_subscriber_scale_1_2_halves() {
1871 assert_eq!(run_player_turn(ESpecies::Half), 40, "scale(1,2): 80 → 40");
1872 }
1873
1874 #[test]
1875 fn effectiveness_subscriber_scale_0_1_zeroes() {
1876 assert_eq!(
1877 run_player_turn(ESpecies::Zero),
1878 0,
1879 "scale(0,1): 80 → 0 (immune)"
1880 );
1881 }
1882}