Skip to main content

manabrew_engine/ability/effects/
skip_phase_effect.rs

1use super::{resolve_defined_players, EffectContext};
2
3/// Create a skip-phase effect for use by static or replacement abilities.
4/// Mirrors Java's `SkipPhaseEffect.createSkipPhaseEffect(...)`.
5///
6/// Creates an effect that will cause the specified player to skip the
7/// specified phase on their next turn. Used by cards like Stasis,
8/// Teferi's Protection, etc.
9pub fn create_skip_phase_effect(
10    game: &mut crate::game::GameState,
11    player: crate::ids::PlayerId,
12    phase: &str,
13) {
14    match phase {
15        "Draw" => game.player_mut(player).skip_next_draw = true,
16        "Combat" | "BeginCombat" => game.player_mut(player).skip_next_combat = true,
17        "Untap" => game.player_mut(player).skip_next_untap = true,
18        _ => {
19            eprintln!("SkipPhaseEffect: Unknown phase to skip: {phase:?}");
20        }
21    }
22}
23
24/// Run the skip-phase effect (entry point for non-resolve contexts).
25/// Mirrors Java's `SkipPhaseEffect.run(SpellAbility)`.
26///
27/// Parses the Phase$ and Defined$ parameters and applies the skip.
28pub fn run(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
29    use crate::ability::spell_ability_effect::SpellAbilityEffect;
30    SkipPhaseEffect::resolve(ctx, sa);
31}
32
33/// Resolve `SP$ SkipPhase` — make a player skip their next occurrence of a phase.
34///
35/// Mirrors Java `SkipPhaseEffect.java`.
36/// Sets per-player phase skip flags. The game loop checks these before
37/// entering each phase and skips accordingly.
38///
39/// # Card script examples
40/// ```text
41/// A:SP$ SkipPhase | Defined$ Opponent | Phase$ Draw
42/// A:SP$ SkipPhase | Defined$ You | Phase$ Combat
43/// A:SP$ SkipPhase | Defined$ Opponent | Phase$ Untap
44/// ```
45/// Struct form of this effect so it can participate in the
46/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
47/// `SkipPhaseEffect` class extending `SpellAbilityEffect`.
48#[manabrew_engine_macros::spell_effect(SkipPhaseEffect)]
49fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
50    let controller = sa.activating_player;
51
52    let phase = sa
53        .ir
54        .phase_text
55        .as_deref()
56        .or(sa.ir.step_text.as_deref())
57        .unwrap_or("");
58
59    let defined = sa.defined().unwrap_or("You");
60
61    let targets = resolve_defined_players(defined, controller, ctx.game);
62    for target in targets {
63        if !ctx.game.player(target).is_alive() {
64            continue;
65        }
66        match phase {
67            "Draw" => ctx.game.player_set_skip_draw(target),
68            "Combat" | "BeginCombat" => ctx.game.player_set_skip_combat(target),
69            "Untap" => ctx.game.player_set_skip_untap(target),
70            _ => {
71                let err = crate::ability::IllegalAbilityException::new(format!(
72                    "Unknown phase to skip: {phase:?}"
73                ));
74                eprintln!("{err}");
75            }
76        }
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use std::collections::HashMap;
83
84    use super::SkipPhaseEffect;
85    use crate::ability::effects::EffectContext;
86    use crate::ability::spell_ability_effect::SpellAbilityEffect;
87    use crate::agent::PassAgent;
88    use crate::game::GameState;
89    use crate::ids::PlayerId;
90    use crate::mana::ManaPool;
91    use crate::spellability::SpellAbility;
92    use crate::trigger::handler::TriggerHandler;
93
94    #[test]
95    fn skip_draw_phase() {
96        let mut game = GameState::new(&["Alice", "Bob"], 20);
97        let p0 = PlayerId(0);
98        let p1 = PlayerId(1);
99
100        let sa =
101            SpellAbility::new_simple(None, p0, "SP$ SkipPhase | Defined$ Opponent | Phase$ Draw");
102
103        let mut th = TriggerHandler::new();
104        let mut agents: Vec<Box<dyn crate::agent::PlayerAgent>> =
105            vec![Box::new(PassAgent), Box::new(PassAgent)];
106        let mut mp = vec![ManaPool::default(), ManaPool::default()];
107        let templates = HashMap::new();
108        let templates_variants = HashMap::new();
109        let token_fallback = HashMap::new();
110        let edition_dates: HashMap<String, String> = HashMap::new();
111        let mut rng_adapter = crate::game_rng::ThreadRngAdapter;
112        let mut ctx = EffectContext {
113            game: &mut game,
114            combat: None,
115            agents: &mut agents,
116            trigger_handler: &mut th,
117            token_templates: &templates,
118            token_art_variants: &templates_variants,
119            token_fallback: &token_fallback,
120            edition_dates: &edition_dates,
121            mana_pools: &mut mp,
122            parent_target_card: None,
123            rng: &mut rng_adapter,
124        };
125        super::SkipPhaseEffect::resolve(&mut ctx, &sa);
126
127        assert!(ctx.game.player(p1).skip_next_draw);
128        assert!(!ctx.game.player(p1).skip_next_combat);
129    }
130
131    #[test]
132    fn skip_combat_phase() {
133        let mut game = GameState::new(&["Alice", "Bob"], 20);
134        let p0 = PlayerId(0);
135
136        let sa = SpellAbility::new_simple(None, p0, "SP$ SkipPhase | Defined$ You | Phase$ Combat");
137
138        let mut th = TriggerHandler::new();
139        let mut agents: Vec<Box<dyn crate::agent::PlayerAgent>> =
140            vec![Box::new(PassAgent), Box::new(PassAgent)];
141        let mut mp = vec![ManaPool::default(), ManaPool::default()];
142        let templates = HashMap::new();
143        let templates_variants = HashMap::new();
144        let token_fallback = HashMap::new();
145        let edition_dates: HashMap<String, String> = HashMap::new();
146        let mut rng_adapter = crate::game_rng::ThreadRngAdapter;
147        let mut ctx = EffectContext {
148            game: &mut game,
149            combat: None,
150            agents: &mut agents,
151            trigger_handler: &mut th,
152            token_templates: &templates,
153            token_art_variants: &templates_variants,
154            token_fallback: &token_fallback,
155            edition_dates: &edition_dates,
156            mana_pools: &mut mp,
157            parent_target_card: None,
158            rng: &mut rng_adapter,
159        };
160        super::SkipPhaseEffect::resolve(&mut ctx, &sa);
161
162        assert!(ctx.game.player(p0).skip_next_combat);
163    }
164}