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 log::{HpChangeCause, TurnEvent, TurnLog};
35pub use event::{
36 Effect, EffectId, EffectType, Event, EventHook, HandlerFn, HandlerResult, RelayVar,
37};
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, EnumMap, MoveEffect,
50 EffectResult,
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 ) -> (BattleState<TProvider>, Vec<EffectState<TProvider>>, MoveContext) {
203 let state = BattleState::new(vec![mon(hp_a)], vec![mon(hp_b)]);
204 (state, Vec::new(), MoveContext::default())
205 }
206
207 #[test]
210 fn fold_dispatch_runs_and_threads_relay() {
211 let (mut state, mut effects, mut mv) = make_ctx_parts(100, 100);
212 let mut rng = ScriptedRng::new(vec![0]);
213 let mut ctx = BattleCtx {
214 state: &mut state,
215 effects: &mut effects,
216 mv: &mut mv,
217 rng: &mut rng,
218 };
219 let hs = vec![
222 CollectedHandler {
223 order: 1,
224 priority: 0,
225 speed: 0,
226 sub_order: 6,
227 effect_order: 0,
228 target: BattlerRef::OPPONENT,
229 source: BattlerRef::PLAYER,
230 source_effect: EffectId(1),
231 call: add_int::<TProvider>,
232 },
233 CollectedHandler {
234 order: 2,
235 priority: 0,
236 speed: 0,
237 sub_order: 6,
238 effect_order: 1,
239 target: BattlerRef::OPPONENT,
240 source: BattlerRef::PLAYER,
241 source_effect: EffectId(2),
242 call: deal_5::<TProvider>,
243 },
244 ];
245 let out = run_event(&mut ctx, hs, RelayVar::Int(0), false);
246 assert_eq!(out, RelayVar::Int(1), "relay threaded through add_int");
247 assert_eq!(state.opponent_battlers[0].hp, 95, "deal_5 mutated target");
248 }
249
250 #[test]
253 fn pair_mut_cross_side_branch_works() {
254 let (mut state, mut effects, mut mv) = make_ctx_parts(100, 50);
255 let mut rng = ScriptedRng::new(vec![0]);
256 let mut ctx = BattleCtx {
257 state: &mut state,
258 effects: &mut effects,
259 mv: &mut mv,
260 rng: &mut rng,
261 };
262 let (player, opp) = ctx.pair_mut(BattlerRef::PLAYER, BattlerRef::OPPONENT);
263 assert_eq!(player.hp, 100);
264 assert_eq!(opp.hp, 50);
265 let opp_hp = opp.hp;
267 player.take_damage(opp_hp);
268 assert_eq!(state.player_battlers[0].hp, 50);
269 assert_eq!(state.opponent_battlers[0].hp, 50);
270 }
271
272 #[test]
275 fn pair_mut_same_side_branch_works() {
276 let mut state = BattleState::new(vec![mon(100), mon(40)], vec![mon(100)]);
278 let mut effects: Vec<EffectState<TProvider>> = Vec::new();
279 let mut mv = MoveContext::default();
280 let mut rng = ScriptedRng::new(vec![0]);
281 let mut ctx = BattleCtx {
282 state: &mut state,
283 effects: &mut effects,
284 mv: &mut mv,
285 rng: &mut rng,
286 };
287 let a = BattlerRef::new(0, 1);
289 let b = BattlerRef::new(0, 0);
290 let (slot1, slot0) = ctx.pair_mut(a, b);
291 assert_eq!(slot1.hp, 40);
292 assert_eq!(slot0.hp, 100);
293 let read = slot0.hp;
294 slot1.take_damage(read / 4); assert_eq!(state.player_battlers[1].hp, 15);
296 assert_eq!(state.player_battlers[0].hp, 100);
297 }
298
299 #[test]
302 fn counter_shaped_handler_compiles_no_refcell() {
303 let (mut state, mut effects, mut mv) = make_ctx_parts(100, 70);
304 let mut rng = ScriptedRng::new(vec![0]);
305 let mut ctx = BattleCtx {
306 state: &mut state,
307 effects: &mut effects,
308 mv: &mut mv,
309 rng: &mut rng,
310 };
311 let hs = vec![CollectedHandler {
314 order: 1,
315 priority: 0,
316 speed: 0,
317 sub_order: 6,
318 effect_order: 0,
319 target: BattlerRef::OPPONENT,
320 source: BattlerRef::PLAYER,
321 source_effect: EffectId(1),
322 call: counter_shaped::<TProvider>,
323 }];
324 run_event(&mut ctx, hs, RelayVar::Unit, false);
325 assert_eq!(state.opponent_battlers[0].hp, 60);
327 }
328
329 #[test]
332 fn typed_effect_state_toxic_counter() {
333 let (mut state, mut effects, mut mv) = make_ctx_parts(100, 100);
334 effects.push(EffectState {
335 id: EffectId(7),
336 host: BattlerRef::PLAYER,
337 effect_order: 0,
338 kind: TKind::Toxic { counter: 0 },
339 });
340 let mut rng = ScriptedRng::new(vec![0]);
341 let mut ctx = BattleCtx {
342 state: &mut state,
343 effects: &mut effects,
344 mv: &mut mv,
345 rng: &mut rng,
346 };
347 let n = match &mut ctx.effect_mut(EffectId(7)).unwrap().kind {
348 TKind::Toxic { counter } => {
349 *counter = counter.saturating_add(1);
350 *counter
351 }
352 TKind::None => 0,
353 };
354 assert_eq!(n, 1);
355 assert!(ctx.effect_mut(EffectId(99)).is_none());
357 }
358
359 use crate::battle::BattleAction;
369 use crate::battle::rng::ScriptedRng as EngineScriptedRng;
370
371 fn force_dmg<P: EffectProvider + ?Sized>(
374 ctx: &mut BattleCtx<'_, P>,
375 _relay: RelayVar,
376 _target: BattlerRef,
377 _source: BattlerRef,
378 _eff: EffectId,
379 ) -> HandlerResult {
380 ctx.mv.damage = 10;
381 HandlerResult::Unchanged
382 }
383
384 static FORCE_DMG_EFFECT: Effect<TForce> = Effect {
385 id: EffectId(1),
386 kind: EffectType::Move,
387 hooks: &[EventHook {
388 event: Event::ModifyDamage,
389 call: force_dmg::<TForce>,
390 order: 1000,
391 priority: 0,
392 sub_order: None,
393 }],
394 };
395
396 struct TForce;
400
401 impl BattleProvider for TForce {
402 type Monster = ();
403 type Move = TMove;
404 type Ability = ();
405 type Status = TStatus;
406 type Stat = TStat;
407 type Species = TSpecies;
408 type Type = TType;
409 type Item = ();
410 fn calculate_damage(
411 &self,
412 _m: &Self::Move,
413 _a: &BattlerState<Self>,
414 _d: &BattlerState<Self>,
415 _r: u8,
416 _c: bool,
417 ) -> DamageResult {
418 DamageResult { damage: 0, effectiveness: 1.0, is_miss: false }
419 }
420 fn select_move(&self, b: &BattlerState<Self>, _s: &BattleState<Self>) -> Self::Move {
421 b.moves.first().cloned().unwrap()
422 }
423 fn apply_move_effect(
424 &self,
425 _e: MoveEffect,
426 _u: &mut BattlerState<Self>,
427 _t: &mut BattlerState<Self>,
428 ) -> EffectResult {
429 EffectResult::NoEffect
430 }
431 fn create_monster(&self, s: Self::Species, _l: u8) -> BattlerState<Self> {
432 BattlerState::new(s, 100, 100, EnumMap::new(), vec![])
433 }
434 }
435
436 impl EffectProvider for TForce {
437 type EffectStateKind = TKind;
438 fn effect_for_move(&self, _m: &Self::Move) -> Option<&'static Effect<Self>> {
439 Some(&FORCE_DMG_EFFECT)
440 }
441 fn effect_for_status(&self, _s: &Self::Status) -> Option<&'static Effect<Self>> {
442 None
443 }
444 fn turn_order_rank(
445 &self,
446 _state: &BattleState<Self>,
447 who: BattlerRef,
448 _action: &Self::Move,
449 ) -> (i32, i32) {
450 if who.side == 0 { (0, 0) } else { (1, 0) }
452 }
453 fn forced_action(
454 &self,
455 effects: &[EffectState<Self>],
456 actor: BattlerRef,
457 chosen: &BattleAction<Self>,
458 ) -> Option<BattleAction<Self>> {
459 let locked = effects
461 .iter()
462 .any(|e| e.host == actor && matches!(e.kind, TKind::Toxic { .. }));
463 if locked {
464 Some(BattleAction::Nothing)
465 } else {
466 let _ = chosen;
467 None
468 }
469 }
470 }
471
472 fn force_mon(hp: u16) -> BattlerState<TForce> {
473 let mut stats = EnumMap::new();
474 stats.set(TStat::Hp, hp);
475 BattlerState::new(TSpecies::Mon, hp, hp, stats, vec![TMove { power: 10 }])
476 }
477
478 #[test]
479 fn forced_action_overrides_chosen_action() {
480 let mut state = BattleState::new(vec![force_mon(100)], vec![force_mon(100)]);
485 let mut effects: Vec<EffectState<TForce>> = vec![EffectState {
486 id: EffectId(50),
487 host: BattlerRef::PLAYER,
488 effect_order: 0,
489 kind: TKind::Toxic { counter: 0 },
490 }];
491 let actions = [
492 BattleAction::<TForce>::Fight { move_: TMove { power: 10 } },
493 BattleAction::<TForce>::Fight { move_: TMove { power: 10 } },
494 ];
495 let mut rng = EngineScriptedRng::new(vec![]);
496 let provider = TForce;
497 StackDriver::execute_turn(&provider, &mut state, &mut effects, actions, &mut rng);
498 assert_eq!(state.opponent_battlers[0].hp, 100, "locked player forced to Nothing");
500 assert_eq!(state.player_battlers[0].hp, 90, "unlocked opponent's Fight ran");
502 }
503
504 #[test]
505 fn forced_action_default_is_inert() {
506 let mut state = BattleState::new(vec![force_mon(100)], vec![force_mon(100)]);
508 let mut effects: Vec<EffectState<TForce>> = Vec::new();
509 let actions = [
510 BattleAction::<TForce>::Fight { move_: TMove { power: 10 } },
511 BattleAction::<TForce>::Fight { move_: TMove { power: 10 } },
512 ];
513 let mut rng = EngineScriptedRng::new(vec![]);
514 let provider = TForce;
515 StackDriver::execute_turn(&provider, &mut state, &mut effects, actions, &mut rng);
516 assert_eq!(state.opponent_battlers[0].hp, 90, "player Fight ran (no lock)");
517 assert_eq!(state.player_battlers[0].hp, 90, "opponent Fight ran (no lock)");
518 }
519
520 use super::log::TurnEvent;
523
524 #[test]
528 fn logged_turn_is_behaviorally_identical_to_plain() {
529 let actions = || {
530 [
531 BattleAction::<TForce>::Fight { move_: TMove { power: 10 } },
532 BattleAction::<TForce>::Fight { move_: TMove { power: 10 } },
533 ]
534 };
535 let mut s_plain = BattleState::new(vec![force_mon(100)], vec![force_mon(100)]);
537 let mut e_plain: Vec<EffectState<TForce>> = Vec::new();
538 let mut rng_plain = EngineScriptedRng::new(vec![]);
539 let r_plain =
540 StackDriver::execute_turn(&TForce, &mut s_plain, &mut e_plain, actions(), &mut rng_plain);
541 let mut s_log = BattleState::new(vec![force_mon(100)], vec![force_mon(100)]);
543 let mut e_log: Vec<EffectState<TForce>> = Vec::new();
544 let mut rng_log = EngineScriptedRng::new(vec![]);
545 let (r_log, log) =
546 StackDriver::execute_turn_logged(&TForce, &mut s_log, &mut e_log, actions(), &mut rng_log);
547
548 assert_eq!(r_plain, r_log, "same StackTurnResult");
549 assert_eq!(rng_plain.consumed(), rng_log.consumed(), "same rng draw count");
550 assert_eq!(s_plain.player_battlers[0].hp, s_log.player_battlers[0].hp, "same player hp");
551 assert_eq!(
552 s_plain.opponent_battlers[0].hp, s_log.opponent_battlers[0].hp,
553 "same opponent hp"
554 );
555 assert!(!log.is_empty(), "the logged path recorded events");
556 }
557
558 #[test]
562 fn log_records_move_used_and_damage_both_movers() {
563 let mut state = BattleState::new(vec![force_mon(100)], vec![force_mon(100)]);
564 let mut effects: Vec<EffectState<TForce>> = Vec::new();
565 let actions = [
566 BattleAction::<TForce>::Fight { move_: TMove { power: 10 } },
567 BattleAction::<TForce>::Fight { move_: TMove { power: 10 } },
568 ];
569 let mut rng = EngineScriptedRng::new(vec![]);
570 let (_r, log) =
571 StackDriver::execute_turn_logged(&TForce, &mut state, &mut effects, actions, &mut rng);
572
573 let p = BattlerRef::PLAYER;
574 let o = BattlerRef::OPPONENT;
575 let ev = &log.events;
576 assert!(matches!(ev[0], TurnEvent::MoveUsed { actor, .. } if actor == p), "1: player MoveUsed");
577 assert!(matches!(ev[1], TurnEvent::Damaged { target, amount: 10, .. } if target == o), "2: opponent took 10");
578 assert!(matches!(ev[2], TurnEvent::MoveUsed { actor, .. } if actor == o), "3: opponent MoveUsed");
579 assert!(matches!(ev[3], TurnEvent::Damaged { target, amount: 10, .. } if target == p), "4: player took 10");
580 assert_eq!(ev.len(), 4, "exactly MoveUsed+Damaged ×2 (no crit/miss/status)");
581 }
582
583 #[test]
586 fn log_records_faint_and_cancels_second() {
587 let mut state = BattleState::new(vec![force_mon(100)], vec![force_mon(10)]);
588 let mut effects: Vec<EffectState<TForce>> = Vec::new();
589 let actions = [
590 BattleAction::<TForce>::Fight { move_: TMove { power: 10 } },
591 BattleAction::<TForce>::Fight { move_: TMove { power: 10 } },
592 ];
593 let mut rng = EngineScriptedRng::new(vec![]);
594 let (r, log) =
595 StackDriver::execute_turn_logged(&TForce, &mut state, &mut effects, actions, &mut rng);
596
597 assert!(r.second_cancelled, "defender KO'd → second move cancelled");
598 let o = BattlerRef::OPPONENT;
599 let ev = &log.events;
600 assert!(matches!(ev[0], TurnEvent::MoveUsed { .. }), "player MoveUsed");
601 assert!(matches!(ev[1], TurnEvent::Damaged { target, amount: 10, .. } if target == o), "opponent took 10");
602 assert!(matches!(ev[2], TurnEvent::Fainted { who } if who == o), "opponent fainted");
603 assert!(
604 !ev.iter().any(|e| matches!(e, TurnEvent::MoveUsed { actor, .. } if *actor == o)),
605 "cancelled opponent never logs a MoveUsed"
606 );
607 }
608
609 fn log_rich(
613 ctx: &mut BattleCtx<'_, TRich>,
614 _relay: RelayVar,
615 target: BattlerRef,
616 source: BattlerRef,
617 _eff: EffectId,
618 ) -> HandlerResult {
619 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);
623 b.hp = (b.hp + 20).min(b.max_hp); HandlerResult::Unchanged
625 }
626
627 static LOG_RICH_EFFECT: Effect<TRich> = Effect {
628 id: EffectId(1),
629 kind: EffectType::Move,
630 hooks: &[EventHook {
631 event: Event::ModifyDamage,
632 call: log_rich,
633 order: 1000,
634 priority: 0,
635 sub_order: None,
636 }],
637 };
638
639 struct TRich;
642 impl BattleProvider for TRich {
643 type Monster = ();
644 type Move = TMove;
645 type Ability = ();
646 type Status = TStatus;
647 type Stat = TStat;
648 type Species = TSpecies;
649 type Type = TType;
650 type Item = ();
651 fn calculate_damage(&self, _m: &Self::Move, _a: &BattlerState<Self>, _d: &BattlerState<Self>, _r: u8, _c: bool) -> DamageResult {
652 DamageResult { damage: 0, effectiveness: 1.0, is_miss: false }
653 }
654 fn select_move(&self, b: &BattlerState<Self>, _s: &BattleState<Self>) -> Self::Move {
655 b.moves.first().cloned().unwrap()
656 }
657 fn apply_move_effect(&self, _e: MoveEffect, _u: &mut BattlerState<Self>, _t: &mut BattlerState<Self>) -> EffectResult {
658 EffectResult::NoEffect
659 }
660 fn create_monster(&self, s: Self::Species, _l: u8) -> BattlerState<Self> {
661 BattlerState::new(s, 100, 100, EnumMap::new(), vec![])
662 }
663 }
664 impl EffectProvider for TRich {
665 type EffectStateKind = TKind;
666 fn effect_for_move(&self, _m: &Self::Move) -> Option<&'static Effect<Self>> {
667 Some(&LOG_RICH_EFFECT)
668 }
669 fn effect_for_status(&self, _s: &Self::Status) -> Option<&'static Effect<Self>> {
670 None
671 }
672 fn turn_order_rank(&self, _state: &BattleState<Self>, who: BattlerRef, _action: &Self::Move) -> (i32, i32) {
673 if who.side == 0 { (0, 0) } else { (1, 0) }
674 }
675 }
676
677 #[test]
678 fn log_records_stat_status_and_heal_diffs() {
679 let mut player = BattlerState::<TRich>::new(TSpecies::Mon, 50, 100, EnumMap::new(), vec![TMove { power: 10 }]);
681 player.max_hp = 100;
682 let opponent = BattlerState::<TRich>::new(TSpecies::Mon, 100, 100, EnumMap::new(), vec![TMove { power: 10 }]);
683 let mut state = BattleState::new(vec![player], vec![opponent]);
684 let mut effects: Vec<EffectState<TRich>> = Vec::new();
685 let actions = [
686 BattleAction::<TRich>::Fight { move_: TMove { power: 10 } },
687 BattleAction::<TRich>::Nothing,
688 ];
689 let mut rng = EngineScriptedRng::new(vec![]);
690 let (_r, log) =
691 StackDriver::execute_turn_logged(&TRich, &mut state, &mut effects, actions, &mut rng);
692
693 let p = BattlerRef::PLAYER;
694 let o = BattlerRef::OPPONENT;
695 let ev = &log.events;
696 assert!(ev.iter().any(|e| matches!(e, TurnEvent::MoveUsed { actor, .. } if *actor == p)), "MoveUsed");
697 assert!(ev.iter().any(|e| matches!(e, TurnEvent::Damaged { target, amount: 10, .. } if *target == o)), "Damaged target");
698 assert!(ev.iter().any(|e| matches!(e, TurnEvent::StatusInflicted { target, .. } if *target == o)), "StatusInflicted target");
699 assert!(ev.iter().any(|e| matches!(e, TurnEvent::StatChanged { target, delta: 1, .. } if *target == p)), "StatChanged actor +1");
700 assert!(ev.iter().any(|e| matches!(e, TurnEvent::Healed { target, amount: 20, .. } if *target == p)), "Healed actor +20");
701 }
702
703 const MP: u16 = 0; static COST_DMG_EFFECT: Effect<TCost> = Effect {
718 id: EffectId(1),
719 kind: EffectType::Move,
720 hooks: &[EventHook {
721 event: Event::ModifyDamage,
722 call: force_dmg::<TCost>,
723 order: 1000,
724 priority: 0,
725 sub_order: None,
726 }],
727 };
728
729 struct TCost;
731
732 impl BattleProvider for TCost {
733 type Monster = ();
734 type Move = TMove;
735 type Ability = ();
736 type Status = TStatus;
737 type Stat = TStat;
738 type Species = TSpecies;
739 type Type = TType;
740 type Item = ();
741 fn calculate_damage(
742 &self,
743 _m: &Self::Move,
744 _a: &BattlerState<Self>,
745 _d: &BattlerState<Self>,
746 _r: u8,
747 _c: bool,
748 ) -> DamageResult {
749 DamageResult { damage: 0, effectiveness: 1.0, is_miss: false }
750 }
751 fn select_move(&self, b: &BattlerState<Self>, _s: &BattleState<Self>) -> Self::Move {
752 b.moves.first().cloned().unwrap()
753 }
754 fn apply_move_effect(
755 &self,
756 _e: MoveEffect,
757 _u: &mut BattlerState<Self>,
758 _t: &mut BattlerState<Self>,
759 ) -> EffectResult {
760 EffectResult::NoEffect
761 }
762 fn create_monster(&self, s: Self::Species, _l: u8) -> BattlerState<Self> {
763 BattlerState::new(s, 100, 100, EnumMap::new(), vec![])
764 }
765 fn move_cost(&self, _move_: &Self::Move) -> &[(u16, u16)] {
767 &[(MP, 4)]
768 }
769 }
770
771 impl EffectProvider for TCost {
772 type EffectStateKind = TKind;
773 fn effect_for_move(&self, _m: &Self::Move) -> Option<&'static Effect<Self>> {
774 Some(&COST_DMG_EFFECT)
775 }
776 fn effect_for_status(&self, _s: &Self::Status) -> Option<&'static Effect<Self>> {
777 None
778 }
779 fn turn_order_rank(
780 &self,
781 _state: &BattleState<Self>,
782 who: BattlerRef,
783 _action: &Self::Move,
784 ) -> (i32, i32) {
785 if who.side == 0 { (0, 0) } else { (1, 0) }
787 }
788 }
789
790 fn cost_mon(hp: u16, mp: u16) -> BattlerState<TCost> {
792 let mut stats = EnumMap::new();
793 stats.set(TStat::Hp, hp);
794 BattlerState::new(TSpecies::Mon, hp, hp, stats, vec![TMove { power: 10 }])
795 .with_resource(MP, mp)
796 }
797
798 #[test]
799 fn cost_gate_pays_and_deducts_when_affordable() {
800 let mut state = BattleState::new(vec![cost_mon(100, 10)], vec![cost_mon(100, 0)]);
805 let mut effects: Vec<EffectState<TCost>> = Vec::new();
806 let actions = [
807 BattleAction::<TCost>::Fight { move_: TMove { power: 10 } },
808 BattleAction::<TCost>::Fight { move_: TMove { power: 10 } },
809 ];
810 let mut rng = EngineScriptedRng::new(vec![]);
811 StackDriver::execute_turn(&TCost, &mut state, &mut effects, actions, &mut rng);
812
813 assert_eq!(state.player_battlers[0].resources.current(MP), Some(6),
815 "affordable: 10 MP - 4 cost = 6 left");
816 assert_eq!(state.opponent_battlers[0].hp, 90, "paid move dealt its 10 damage");
817
818 assert_eq!(state.player_battlers[0].hp, 100, "insufficient opp move prevented → no damage");
822 assert_eq!(state.opponent_battlers[0].resources.current(MP), Some(0),
823 "insufficient: MP unchanged (no deduction on a prevented move)");
824 }
825
826 #[test]
827 fn cost_gate_prevents_when_unaffordable_and_leaves_mp_unchanged() {
828 let mut state = BattleState::new(vec![cost_mon(100, 3)], vec![cost_mon(100, 0)]);
831 let mut effects: Vec<EffectState<TCost>> = Vec::new();
832 let actions = [
833 BattleAction::<TCost>::Fight { move_: TMove { power: 10 } },
834 BattleAction::<TCost>::Fight { move_: TMove { power: 10 } },
835 ];
836 let mut rng = EngineScriptedRng::new(vec![]);
837 StackDriver::execute_turn(&TCost, &mut state, &mut effects, actions, &mut rng);
838
839 assert_eq!(state.opponent_battlers[0].hp, 100,
840 "player could not pay → its move was prevented → opp unharmed");
841 assert_eq!(state.player_battlers[0].resources.current(MP), Some(3),
842 "prevented move deducts NOTHING → MP unchanged at 3");
843 }
844
845 #[test]
846 fn cost_gate_consumes_no_rng() {
847 let mut state = BattleState::new(vec![cost_mon(100, 10)], vec![cost_mon(100, 1)]);
853 let mut effects: Vec<EffectState<TCost>> = Vec::new();
854 let actions = [
855 BattleAction::<TCost>::Fight { move_: TMove { power: 10 } },
856 BattleAction::<TCost>::Fight { move_: TMove { power: 10 } },
857 ];
858 let mut rng = EngineScriptedRng::new(vec![]);
859 StackDriver::execute_turn(&TCost, &mut state, &mut effects, actions, &mut rng);
860 assert_eq!(rng.consumed(), 0,
861 "the resource cost check + deduction consume NO randomness");
862 }
863
864 #[test]
865 fn no_cost_no_resources_is_inert() {
866 let mut state = BattleState::new(vec![force_mon(100)], vec![force_mon(100)]);
871 let mut effects: Vec<EffectState<TForce>> = Vec::new();
872 let actions = [
873 BattleAction::<TForce>::Fight { move_: TMove { power: 10 } },
874 BattleAction::<TForce>::Fight { move_: TMove { power: 10 } },
875 ];
876 let mut rng = EngineScriptedRng::new(vec![]);
877 StackDriver::execute_turn(&TForce, &mut state, &mut effects, actions, &mut rng);
878 assert_eq!(state.opponent_battlers[0].hp, 90, "no-cost move ran (inert gate)");
879 assert_eq!(state.player_battlers[0].hp, 90, "no-cost move ran (inert gate)");
880 assert!(state.player_battlers[0].resources.is_empty(), "pool defaulted EMPTY");
881 }
882
883 #[test]
886 fn speed_tiebreak_draws_only_on_tie() {
887 let mk = |eo: u64| CollectedHandler::<TProvider> {
889 order: 1,
890 priority: 0,
891 speed: 0,
892 sub_order: 6,
893 effect_order: eo,
894 target: BattlerRef::OPPONENT,
895 source: BattlerRef::PLAYER,
896 source_effect: EffectId(eo as u32),
897 call: add_int::<TProvider>,
898 };
899 let mut distinct = vec![mk(0), mk(1)];
901 let mut rng = ScriptedRng::new(vec![200, 200]);
902 dispatch::speed_sort_tiebreak(&mut distinct, &mut rng);
903 assert_eq!(rng.consumed(), 0, "distinct effect_order ⇒ no tie ⇒ no draw");
904
905 let mut tied = vec![mk(0), mk(0)];
907 let mut rng2 = ScriptedRng::new(vec![200]);
908 dispatch::speed_sort_tiebreak(&mut tied, &mut rng2);
909 assert_eq!(rng2.consumed(), 1, "one tied pair ⇒ exactly one draw");
910 }
911
912 #[test]
915 fn comparator_lexical_order() {
916 use std::cmp::Ordering;
917 let h = |order, priority, eo| CollectedHandler::<TProvider> {
918 order,
919 priority,
920 speed: 0,
921 sub_order: 6,
922 effect_order: eo,
923 target: BattlerRef::OPPONENT,
924 source: BattlerRef::PLAYER,
925 source_effect: EffectId(0),
926 call: add_int::<TProvider>,
927 };
928 assert_eq!(compare(&h(1, 0, 0), &h(2, 0, 0)), Ordering::Less);
930 assert_eq!(compare(&h(1, 5, 0), &h(1, 1, 0)), Ordering::Less);
932 assert_eq!(compare(&h(1, 0, 3), &h(1, 0, 9)), Ordering::Less);
934 assert_eq!(compare(&h(1, 0, 0), &h(1, 0, 0)), Ordering::Equal);
936 }
937}
938
939#[cfg(test)]
947mod multi_source_tests {
948 use super::dispatch::{collect_handlers, run_event, run_event_checked};
949 use super::tests_support::{
950 marker, mon, parts, TKind, TProvider, TSpecies, MOCK_VOLATILE,
951 };
952 use super::*;
953 use crate::battle::stack::ctx::EffectHost;
954 use crate::battle::{BattlerRef, EffectResult};
955
956 fn ctx_from<'a>(
957 state: &'a mut crate::battle::BattleState<TProvider>,
958 effects: &'a mut Vec<EffectState<TProvider>>,
959 mv: &'a mut MoveContext,
960 rng: &'a mut crate::battle::rng::ScriptedRng,
961 ) -> BattleCtx<'a, TProvider> {
962 BattleCtx { state, effects, mv, rng }
963 }
964
965 #[test]
968 fn collects_ability_item_field_in_order() {
969 let provider = TProvider { field_on: true };
972 let (mut state, mut effects, mut mv, mut rng) =
973 parts(mon(100, TSpecies::HasBoth), mon(100, TSpecies::Plain));
974 let mut ctx = ctx_from(&mut state, &mut effects, &mut mv, &mut rng);
975
976 let mut hs = Vec::new();
977 collect_handlers(
978 &ctx,
979 &provider,
980 None, Event::DamagingHit,
982 BattlerRef::OPPONENT, BattlerRef::PLAYER, &mut hs,
985 );
986 assert_eq!(hs.len(), 3, "ability + item + field collected");
988
989 run_event(&mut ctx, hs, RelayVar::Unit, false);
990 assert_eq!(marker(&state, BattlerRef::PLAYER), 11, "ability+item fired on host");
994 assert_eq!(marker(&state, BattlerRef::OPPONENT), 100, "field fired on target");
995 }
996
997 #[test]
1000 fn multi_source_comparator_order() {
1001 let provider = TProvider { field_on: true };
1002 let (mut state, mut effects, mut mv, mut rng) =
1003 parts(mon(100, TSpecies::HasBoth), mon(100, TSpecies::Plain));
1004 effects.push(EffectState {
1006 id: MOCK_VOLATILE.id,
1007 host: BattlerRef::PLAYER,
1008 effect_order: 0,
1009 kind: TKind::Vol,
1010 });
1011 let ctx = ctx_from(&mut state, &mut effects, &mut mv, &mut rng);
1012
1013 let mut hs = Vec::new();
1014 collect_handlers(
1015 &ctx,
1016 &provider,
1017 None,
1018 Event::DamagingHit,
1019 BattlerRef::OPPONENT,
1020 BattlerRef::PLAYER,
1021 &mut hs,
1022 );
1023 hs.sort_by(super::dispatch::compare);
1024 let orders: Vec<u32> = hs.iter().map(|h| h.order).collect();
1025 assert_eq!(orders, vec![10, 15, 20, 30], "ability<volatile<item<field");
1026 }
1027
1028 fn ko_target<P: EffectProvider + ?Sized>(
1031 ctx: &mut BattleCtx<'_, P>,
1032 _r: RelayVar,
1033 target: BattlerRef,
1034 _s: BattlerRef,
1035 _e: EffectId,
1036 ) -> HandlerResult {
1037 ctx.battler_mut(target).hp = 0; HandlerResult::Unchanged
1039 }
1040 fn touch_target<P: EffectProvider<Stat = super::tests_support::TStat> + ?Sized>(
1041 ctx: &mut BattleCtx<'_, P>,
1042 _r: RelayVar,
1043 target: BattlerRef,
1044 _s: BattlerRef,
1045 _e: EffectId,
1046 ) -> HandlerResult {
1047 super::tests_support::mark(ctx, target, 7); HandlerResult::Unchanged
1049 }
1050
1051 #[test]
1052 fn re_check_skips_handler_whose_target_was_koed_midfold() {
1053 let (mut state, mut effects, mut mv, mut rng) =
1054 parts(mon(100, TSpecies::Plain), mon(100, TSpecies::Plain));
1055 let mut ctx = ctx_from(&mut state, &mut effects, &mut mv, &mut rng);
1056 let hs = vec![
1058 CollectedHandler {
1059 order: 1, priority: 0, speed: 0, sub_order: 6, effect_order: 0,
1060 target: BattlerRef::OPPONENT, source: BattlerRef::PLAYER,
1061 source_effect: EffectId(1), call: ko_target::<TProvider>,
1062 },
1063 CollectedHandler {
1064 order: 2, priority: 0, speed: 0, sub_order: 6, effect_order: 1,
1065 target: BattlerRef::OPPONENT, source: BattlerRef::PLAYER,
1066 source_effect: EffectId(2), call: touch_target::<TProvider>,
1067 },
1068 ];
1069 run_event_checked(&mut ctx, hs, RelayVar::Unit, false);
1070 assert_eq!(state.opponent_battlers[0].hp, 0, "first handler KO'd target");
1071 assert_eq!(
1072 marker(&state, BattlerRef::OPPONENT), 0,
1073 "re-check SKIPPED the second handler (dead target)"
1074 );
1075 }
1076
1077 #[test]
1080 fn plain_run_event_does_not_re_check() {
1081 let provider = TProvider::default();
1082 let _ = &provider; let (mut state, mut effects, mut mv, mut rng) =
1084 parts(mon(100, TSpecies::Plain), mon(100, TSpecies::Plain));
1085 let mut ctx = ctx_from(&mut state, &mut effects, &mut mv, &mut rng);
1086 let hs = vec![
1087 CollectedHandler {
1088 order: 1, priority: 0, speed: 0, sub_order: 6, effect_order: 0,
1089 target: BattlerRef::OPPONENT, source: BattlerRef::PLAYER,
1090 source_effect: EffectId(1), call: ko_target::<TProvider>,
1091 },
1092 CollectedHandler {
1093 order: 2, priority: 0, speed: 0, sub_order: 6, effect_order: 1,
1094 target: BattlerRef::OPPONENT, source: BattlerRef::PLAYER,
1095 source_effect: EffectId(2), call: touch_target::<TProvider>,
1096 },
1097 ];
1098 run_event(&mut ctx, hs, RelayVar::Unit, false);
1099 assert_eq!(
1100 marker(&state, BattlerRef::OPPONENT), 7,
1101 "plain run_event fired the second handler (no re-check) — slice contract"
1102 );
1103 }
1104
1105 fn remove_other_effect<P: EffectProvider + ?Sized>(
1108 ctx: &mut BattleCtx<'_, P>,
1109 _r: RelayVar,
1110 _t: BattlerRef,
1111 _s: BattlerRef,
1112 _e: EffectId,
1113 ) -> HandlerResult {
1114 ctx.effects.retain(|e| e.id != MOCK_VOLATILE.id);
1116 HandlerResult::Unchanged
1117 }
1118
1119 #[test]
1120 fn handler_removing_effect_midfold_is_snapshot_safe() {
1121 let provider = TProvider::default();
1122 let _ = &provider; let (mut state, mut effects, mut mv, mut rng) =
1124 parts(mon(100, TSpecies::Plain), mon(100, TSpecies::Plain));
1125 effects.push(EffectState {
1126 id: MOCK_VOLATILE.id,
1127 host: BattlerRef::PLAYER,
1128 effect_order: 0,
1129 kind: TKind::Vol,
1130 });
1131 let mut ctx = ctx_from(&mut state, &mut effects, &mut mv, &mut rng);
1132 let hs = vec![
1134 CollectedHandler {
1135 order: 1, priority: 0, speed: 0, sub_order: 6, effect_order: 0,
1136 target: BattlerRef::PLAYER, source: BattlerRef::PLAYER,
1137 source_effect: EffectId(1), call: remove_other_effect::<TProvider>,
1138 },
1139 CollectedHandler {
1140 order: 2, priority: 0, speed: 0, sub_order: 6, effect_order: 1,
1141 target: BattlerRef::PLAYER, source: BattlerRef::PLAYER,
1142 source_effect: EffectId(2), call: touch_target::<TProvider>,
1143 },
1144 ];
1145 run_event_checked(&mut ctx, hs, RelayVar::Unit, false);
1146 assert!(effects.is_empty(), "volatile removed mid-fold");
1147 assert_eq!(
1148 marker(&state, BattlerRef::PLAYER), 7,
1149 "second handler still fired from the owned snapshot (no invalidation)"
1150 );
1151 }
1152
1153 #[test]
1155 fn effect_host_routes_field_and_battler() {
1156 let es = EffectState::<TProvider> {
1158 id: EffectId(1),
1159 host: BattlerRef::PLAYER,
1160 effect_order: 0,
1161 kind: TKind::None,
1162 };
1163 assert_eq!(es.host_scope(), EffectHost::Battler(BattlerRef::PLAYER));
1164
1165 let h: EffectHost = BattlerRef::OPPONENT.into();
1167 assert_eq!(h, EffectHost::Battler(BattlerRef::OPPONENT));
1168 assert!(h == BattlerRef::OPPONENT, "EffectHost == BattlerRef");
1169 assert!(BattlerRef::OPPONENT == h, "BattlerRef == EffectHost");
1170 assert_ne!(EffectHost::Side(1), EffectHost::Field);
1171
1172 let provider = TProvider { field_on: true };
1176 let (mut state, mut effects, mut mv, mut rng) =
1177 parts(mon(100, TSpecies::Plain), mon(100, TSpecies::Plain));
1178 let mut ctx = ctx_from(&mut state, &mut effects, &mut mv, &mut rng);
1179 let mut hs = Vec::new();
1180 collect_handlers(
1181 &ctx, &provider, None, Event::DamagingHit,
1182 BattlerRef::PLAYER, BattlerRef::PLAYER, &mut hs,
1183 );
1184 assert_eq!(hs.len(), 1, "only the field effect (no ability/item)");
1185 run_event_checked(&mut ctx, hs, RelayVar::Unit, false);
1186 assert_eq!(marker(&state, BattlerRef::PLAYER), 100, "field residual routed");
1187 }
1188
1189 #[test]
1191 fn event_with_no_subscribers_is_noop() {
1192 let provider = TProvider::default(); let (mut state, mut effects, mut mv, mut rng) =
1194 parts(mon(100, TSpecies::Plain), mon(100, TSpecies::Plain));
1195 let ctx = ctx_from(&mut state, &mut effects, &mut mv, &mut rng);
1196 let mut hs = Vec::new();
1197 collect_handlers(
1198 &ctx, &provider, None, Event::DamagingHit,
1199 BattlerRef::OPPONENT, BattlerRef::PLAYER, &mut hs,
1200 );
1201 assert!(hs.is_empty(), "no live source ⇒ no handlers ⇒ no-op");
1202 drop(ctx);
1203 assert_eq!(marker(&state, BattlerRef::PLAYER), 0);
1204 assert_eq!(marker(&state, BattlerRef::OPPONENT), 0);
1205 let _ = EffectResult::NoEffect; }
1207
1208 #[test]
1211 fn default_resolvers_reduce_to_single_source_identity() {
1212 use super::dispatch::collect_from_effect;
1216 static SRC: Effect<TProvider> = Effect {
1217 id: EffectId(0x99),
1218 kind: EffectType::Move,
1219 hooks: &[EventHook {
1220 event: Event::DamagingHit,
1221 call: super::tests_support::mock_src_hit::<TProvider>,
1222 order: 5,
1223 priority: 0,
1224 sub_order: None,
1225 }],
1226 };
1227 let provider = TProvider::default();
1228 let (mut state, mut effects, mut mv, mut rng) =
1229 parts(mon(100, TSpecies::Plain), mon(100, TSpecies::Plain));
1230 let ctx = ctx_from(&mut state, &mut effects, &mut mv, &mut rng);
1231
1232 let mut single = Vec::new();
1233 collect_from_effect(
1234 &ctx, &SRC, Event::DamagingHit,
1235 BattlerRef::OPPONENT, BattlerRef::PLAYER, &mut single,
1236 );
1237 let mut multi = Vec::new();
1238 collect_handlers(
1239 &ctx, &provider, Some(&SRC), Event::DamagingHit,
1240 BattlerRef::OPPONENT, BattlerRef::PLAYER, &mut multi,
1241 );
1242 assert_eq!(single.len(), 1);
1243 assert_eq!(multi.len(), single.len(), "identity: same count");
1244 for (a, b) in single.iter().zip(multi.iter()) {
1245 assert_eq!(a.order, b.order);
1246 assert_eq!(a.priority, b.priority);
1247 assert_eq!(a.speed, b.speed);
1248 assert_eq!(a.sub_order, b.sub_order);
1249 assert_eq!(a.effect_order, b.effect_order);
1250 assert_eq!(a.target, b.target);
1251 assert_eq!(a.source, b.source);
1252 assert_eq!(a.source_effect, b.source_effect);
1253 }
1254 }
1255}
1256
1257#[cfg(test)]
1264mod effectiveness_fold_tests {
1265 use super::*;
1266 use crate::battle::rng::ScriptedRng as EngineScriptedRng;
1267 use crate::battle::stack::ctx::{BattleCtx, EffectProvider, EffectState};
1268 use crate::battle::stack::event::{
1269 Effect, EffectId, EffectType, Event, EventHook, HandlerResult, RelayVar,
1270 };
1271 use crate::battle::{
1272 BattleAction, BattleProvider, BattleState, BattlerRef, BattlerState, DamageResult,
1273 EffectResult, EnumMap, MoveEffect,
1274 };
1275
1276 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1277 enum EStat {
1278 Hp,
1279 }
1280 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1281 #[allow(dead_code)]
1282 enum EStatus {
1283 Poisoned,
1284 }
1285 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1286 #[allow(dead_code)]
1287 enum EType {
1288 N,
1289 }
1290 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1294 enum ESpecies {
1295 NoSub,
1297 Double,
1299 Half,
1301 Zero,
1303 }
1304 #[derive(Debug, Clone, PartialEq)]
1305 struct EMove {
1306 power: u8,
1307 }
1308 #[derive(Clone)]
1309 #[allow(dead_code)]
1310 enum EKind {
1311 None,
1312 }
1313
1314 struct EProvider;
1315
1316 impl BattleProvider for EProvider {
1317 type Monster = ();
1318 type Move = EMove;
1319 type Ability = ();
1320 type Status = EStatus;
1321 type Stat = EStat;
1322 type Species = ESpecies;
1323 type Type = EType;
1324 type Item = ();
1325 fn calculate_damage(
1326 &self,
1327 _m: &Self::Move,
1328 _a: &BattlerState<Self>,
1329 _d: &BattlerState<Self>,
1330 _r: u8,
1331 _c: bool,
1332 ) -> DamageResult {
1333 DamageResult { damage: 0, effectiveness: 1.0, is_miss: false }
1334 }
1335 fn select_move(&self, b: &BattlerState<Self>, _s: &BattleState<Self>) -> Self::Move {
1336 b.moves.first().cloned().unwrap()
1337 }
1338 fn apply_move_effect(
1339 &self,
1340 _e: MoveEffect,
1341 _u: &mut BattlerState<Self>,
1342 _t: &mut BattlerState<Self>,
1343 ) -> EffectResult {
1344 EffectResult::NoEffect
1345 }
1346 fn create_monster(&self, s: Self::Species, _l: u8) -> BattlerState<Self> {
1347 BattlerState::new(s, 100, 100, EnumMap::new(), vec![])
1348 }
1349 }
1350
1351 impl EffectProvider for EProvider {
1352 type EffectStateKind = EKind;
1353 fn effect_for_move(&self, _m: &Self::Move) -> Option<&'static Effect<Self>> {
1358 Some(&EFF_MOVE_EFFECT)
1362 }
1363 fn effect_for_status(&self, _s: &Self::Status) -> Option<&'static Effect<Self>> {
1364 None
1365 }
1366 fn turn_order_rank(
1367 &self,
1368 _state: &BattleState<Self>,
1369 who: BattlerRef,
1370 _action: &Self::Move,
1371 ) -> (i32, i32) {
1372 if who.side == 0 { (0, 0) } else { (1, 0) }
1374 }
1375 }
1376
1377 fn eff_modify_damage(
1380 ctx: &mut BattleCtx<'_, EProvider>,
1381 _r: RelayVar,
1382 _t: BattlerRef,
1383 _s: BattlerRef,
1384 _e: EffectId,
1385 ) -> HandlerResult {
1386 ctx.mv.damage = 80;
1387 HandlerResult::Unchanged
1388 }
1389
1390 fn eff_effectiveness(
1394 ctx: &mut BattleCtx<'_, EProvider>,
1395 relay: RelayVar,
1396 _target: BattlerRef,
1397 source: BattlerRef,
1398 _e: EffectId,
1399 ) -> HandlerResult {
1400 let sp = ctx.battler(source).species;
1401 let (num, den) = match sp {
1402 ESpecies::Double => (2u32, 1u32),
1403 ESpecies::Half => (1, 2),
1404 ESpecies::Zero => (0, 1),
1405 ESpecies::NoSub => (1, 1), };
1407 HandlerResult::Set(relay.scale(num, den))
1408 }
1409
1410 static EFF_MOVE_EFFECT: Effect<EProvider> = Effect {
1412 id: EffectId(1),
1413 kind: EffectType::Move,
1414 hooks: &[
1415 EventHook {
1416 event: Event::ModifyDamage,
1417 call: eff_modify_damage,
1418 order: 1000,
1419 priority: 0,
1420 sub_order: None,
1421 },
1422 EventHook {
1423 event: Event::Effectiveness,
1424 call: eff_effectiveness,
1425 order: 1000,
1426 priority: 0,
1427 sub_order: None,
1428 },
1429 ],
1430 };
1431
1432 struct EProviderNoSub;
1435 impl BattleProvider for EProviderNoSub {
1436 type Monster = ();
1437 type Move = EMove;
1438 type Ability = ();
1439 type Status = EStatus;
1440 type Stat = EStat;
1441 type Species = ESpecies;
1442 type Type = EType;
1443 type Item = ();
1444 fn calculate_damage(
1445 &self,
1446 _m: &Self::Move,
1447 _a: &BattlerState<Self>,
1448 _d: &BattlerState<Self>,
1449 _r: u8,
1450 _c: bool,
1451 ) -> DamageResult {
1452 DamageResult { damage: 0, effectiveness: 1.0, is_miss: false }
1453 }
1454 fn select_move(&self, b: &BattlerState<Self>, _s: &BattleState<Self>) -> Self::Move {
1455 b.moves.first().cloned().unwrap()
1456 }
1457 fn apply_move_effect(
1458 &self,
1459 _e: MoveEffect,
1460 _u: &mut BattlerState<Self>,
1461 _t: &mut BattlerState<Self>,
1462 ) -> EffectResult {
1463 EffectResult::NoEffect
1464 }
1465 fn create_monster(&self, s: Self::Species, _l: u8) -> BattlerState<Self> {
1466 BattlerState::new(s, 100, 100, EnumMap::new(), vec![])
1467 }
1468 }
1469 impl EffectProvider for EProviderNoSub {
1470 type EffectStateKind = EKind;
1471 fn effect_for_move(&self, _m: &Self::Move) -> Option<&'static Effect<Self>> {
1472 Some(&EFF_MOVE_NO_SUB_NS)
1473 }
1474 fn effect_for_status(&self, _s: &Self::Status) -> Option<&'static Effect<Self>> {
1475 None
1476 }
1477 fn turn_order_rank(
1478 &self,
1479 _state: &BattleState<Self>,
1480 who: BattlerRef,
1481 _action: &Self::Move,
1482 ) -> (i32, i32) {
1483 if who.side == 0 { (0, 0) } else { (1, 0) }
1484 }
1485 }
1486 fn eff_modify_damage_ns(
1487 ctx: &mut BattleCtx<'_, EProviderNoSub>,
1488 _r: RelayVar,
1489 _t: BattlerRef,
1490 _s: BattlerRef,
1491 _e: EffectId,
1492 ) -> HandlerResult {
1493 ctx.mv.damage = 80;
1494 HandlerResult::Unchanged
1495 }
1496 static EFF_MOVE_NO_SUB_NS: Effect<EProviderNoSub> = Effect {
1497 id: EffectId(2),
1498 kind: EffectType::Move,
1499 hooks: &[EventHook {
1500 event: Event::ModifyDamage,
1501 call: eff_modify_damage_ns,
1502 order: 1000,
1503 priority: 0,
1504 sub_order: None,
1505 }],
1506 };
1507
1508 fn mon_eff(species: ESpecies) -> BattlerState<EProvider> {
1509 let mut stats = EnumMap::new();
1510 stats.set(EStat::Hp, 200);
1511 BattlerState::new(species, 200, 200, stats, vec![EMove { power: 80 }])
1512 }
1513 fn mon_eff_ns(species: ESpecies) -> BattlerState<EProviderNoSub> {
1514 let mut stats = EnumMap::new();
1515 stats.set(EStat::Hp, 200);
1516 BattlerState::new(species, 200, 200, stats, vec![EMove { power: 80 }])
1517 }
1518
1519 fn run_player_turn(attacker: ESpecies) -> u16 {
1521 let mut state = BattleState::new(vec![mon_eff(attacker)], vec![mon_eff(ESpecies::NoSub)]);
1522 let mut effects: Vec<EffectState<EProvider>> = Vec::new();
1523 let actions = [
1524 BattleAction::<EProvider>::Fight { move_: EMove { power: 80 } },
1525 BattleAction::<EProvider>::Fight { move_: EMove { power: 80 } },
1526 ];
1527 let mut rng = EngineScriptedRng::new(vec![]);
1528 StackDriver::execute_turn(&EProvider, &mut state, &mut effects, actions, &mut rng);
1529 200 - state.opponent_battlers[0].hp
1530 }
1531
1532 #[test]
1533 fn effectiveness_fold_is_inert_with_no_subscriber() {
1534 let mut state = BattleState::new(
1538 vec![mon_eff_ns(ESpecies::NoSub)],
1539 vec![mon_eff_ns(ESpecies::NoSub)],
1540 );
1541 let mut effects: Vec<EffectState<EProviderNoSub>> = Vec::new();
1542 let actions = [
1543 BattleAction::<EProviderNoSub>::Fight { move_: EMove { power: 80 } },
1544 BattleAction::<EProviderNoSub>::Fight { move_: EMove { power: 80 } },
1545 ];
1546 let mut rng = EngineScriptedRng::new(vec![]);
1547 StackDriver::execute_turn(&EProviderNoSub, &mut state, &mut effects, actions, &mut rng);
1548 let dmg = 200 - state.opponent_battlers[0].hp;
1549 assert_eq!(dmg, 80, "no Effectiveness subscriber ⇒ damage unchanged (inert 1×)");
1550 }
1551
1552 #[test]
1553 fn effectiveness_subscriber_scale_2_1_doubles() {
1554 assert_eq!(run_player_turn(ESpecies::Double), 160, "scale(2,1): 80 → 160");
1555 }
1556
1557 #[test]
1558 fn effectiveness_subscriber_scale_1_2_halves() {
1559 assert_eq!(run_player_turn(ESpecies::Half), 40, "scale(1,2): 80 → 40");
1560 }
1561
1562 #[test]
1563 fn effectiveness_subscriber_scale_0_1_zeroes() {
1564 assert_eq!(run_player_turn(ESpecies::Zero), 0, "scale(0,1): 80 → 0 (immune)");
1565 }
1566}