Skip to main content

manabrew_engine/ability/effects/
phases_effect.rs

1use forge_foundation::ZoneType;
2
3use super::EffectContext;
4use crate::ability::ability_ir::DefinedRef;
5use crate::event::{AbilityValue, RunParams};
6use crate::trigger::TriggerType;
7
8/// Resolve `SP$ Phases` — phase permanents in or out.
9///
10/// Mirrors Java `PhasesEffect.java`.
11/// Toggles or sets `card.phased_out` on target cards. Phased-out permanents
12/// are treated as not on the battlefield for game purposes.
13///
14/// # Card script examples
15/// ```text
16/// A:SP$ Phases | ValidTgts$ Creature | TgtPrompt$ Select target creature
17/// A:SP$ Phases | Defined$ Self | PhaseInOrOut$ Out
18/// ```
19/// Struct form of this effect so it can participate in the
20/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
21/// `PhasesEffect` class extending `SpellAbilityEffect`.
22#[manabrew_engine_macros::spell_effect(PhasesEffect)]
23fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
24    let phase_mode = sa.ir.phase_in_or_out_text.as_deref().unwrap_or("Out");
25
26    // Defined$ DelayTriggerRememberedLKI / Remembered — phase the cards
27    // remembered by the parent delayed trigger (e.g. Teferi's Veil's
28    // "creature phases out at end of combat" queues the attacker's LKI).
29    if let Some(
30        DefinedRef::DelayTriggerRememberedLki
31        | DefinedRef::DelayTriggerRemembered
32        | DefinedRef::Remembered,
33    ) = sa.defined_ref()
34    {
35        let ids: Vec<crate::ids::CardId> = sa
36            .trigger_remembered
37            .iter()
38            .filter_map(|v| match v {
39                AbilityValue::Card(cid) => Some(*cid),
40                _ => None,
41            })
42            .collect();
43        for cid in ids {
44            if ctx.game.card(cid).zone == ZoneType::Battlefield {
45                apply_phase(ctx, cid, phase_mode);
46            }
47        }
48        return;
49    }
50
51    // Targeted: use the chosen target card.
52    if let Some(target_card) = sa.target_chosen.target_card {
53        if ctx.game.card(target_card).zone == ZoneType::Battlefield {
54            apply_phase(ctx, target_card, phase_mode);
55        }
56        return;
57    }
58
59    // Defined$ Self — phase the source card.
60    if let Some(source) = sa.source {
61        if ctx.game.card(source).zone == ZoneType::Battlefield {
62            apply_phase(ctx, source, phase_mode);
63        }
64    }
65}
66
67fn apply_phase(ctx: &mut EffectContext, card_id: crate::ids::CardId, mode: &str) {
68    match mode {
69        "In" => {
70            if ctx.game.card(card_id).phased_out {
71                ctx.game.card_mut(card_id).set_phased_out(false);
72                ctx.trigger_handler.run_trigger(
73                    TriggerType::PhasedIn,
74                    RunParams {
75                        card: Some(card_id),
76                        ..Default::default()
77                    },
78                    false,
79                );
80            }
81        }
82        _ => {
83            if !ctx.game.card(card_id).phased_out {
84                ctx.game.card_mut(card_id).set_phased_out(true);
85                ctx.trigger_handler.run_trigger(
86                    TriggerType::PhasedOut,
87                    RunParams {
88                        card: Some(card_id),
89                        ..Default::default()
90                    },
91                    false,
92                );
93            }
94        }
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use crate::ability::spell_ability_effect::SpellAbilityEffect;
101    use forge_foundation::{CardTypeLine, ColorSet, ManaCost, ZoneType};
102    use std::collections::HashMap;
103
104    use crate::ability::effects::EffectContext;
105    use crate::agent::PassAgent;
106    use crate::card::Card;
107    use crate::game::GameState;
108    use crate::ids::{CardId, PlayerId};
109    use crate::mana::ManaPool;
110    use crate::spellability::SpellAbility;
111    use crate::trigger::handler::TriggerHandler;
112
113    fn make_creature(game: &mut GameState, owner: PlayerId) -> CardId {
114        let c = Card::new(
115            CardId(0),
116            "Bear".into(),
117            owner,
118            CardTypeLine::parse("Creature - Bear"),
119            ManaCost::parse("1 G"),
120            ColorSet::GREEN,
121            Some(2),
122            Some(2),
123            vec![],
124            vec![],
125        );
126        game.create_card(c)
127    }
128
129    #[test]
130    fn phase_out_target() {
131        let mut game = GameState::new(&["Alice", "Bob"], 20);
132        let p0 = PlayerId(0);
133        let c1 = make_creature(&mut game, p0);
134        game.move_card(c1, ZoneType::Battlefield, p0);
135        assert!(!game.card(c1).phased_out);
136
137        let mut sa = SpellAbility::new_simple(None, p0, "SP$ Phases | PhaseInOrOut$ Out");
138        sa.target_chosen.target_card = Some(c1);
139
140        let mut th = TriggerHandler::new();
141        let mut agents: Vec<Box<dyn crate::agent::PlayerAgent>> =
142            vec![Box::new(PassAgent), Box::new(PassAgent)];
143        let mut mp = vec![ManaPool::default(), ManaPool::default()];
144        let templates = HashMap::new();
145        let templates_variants = HashMap::new();
146        let token_fallback = HashMap::new();
147        let edition_dates: HashMap<String, String> = HashMap::new();
148        let mut rng_adapter = crate::game_rng::ThreadRngAdapter;
149        let mut ctx = EffectContext {
150            game: &mut game,
151            combat: None,
152            agents: &mut agents,
153            trigger_handler: &mut th,
154            token_templates: &templates,
155            token_art_variants: &templates_variants,
156            token_fallback: &token_fallback,
157            edition_dates: &edition_dates,
158            mana_pools: &mut mp,
159            parent_target_card: None,
160            rng: &mut rng_adapter,
161        };
162        super::PhasesEffect::resolve(&mut ctx, &sa);
163
164        assert!(ctx.game.card(c1).phased_out);
165    }
166
167    #[test]
168    fn phase_in_target() {
169        let mut game = GameState::new(&["Alice", "Bob"], 20);
170        let p0 = PlayerId(0);
171        let c1 = make_creature(&mut game, p0);
172        game.move_card(c1, ZoneType::Battlefield, p0);
173        game.card_mut(c1).set_phased_out(true);
174
175        let mut sa = SpellAbility::new_simple(None, p0, "SP$ Phases | PhaseInOrOut$ In");
176        sa.target_chosen.target_card = Some(c1);
177
178        let mut th = TriggerHandler::new();
179        let mut agents: Vec<Box<dyn crate::agent::PlayerAgent>> =
180            vec![Box::new(PassAgent), Box::new(PassAgent)];
181        let mut mp = vec![ManaPool::default(), ManaPool::default()];
182        let templates = HashMap::new();
183        let templates_variants = HashMap::new();
184        let token_fallback = HashMap::new();
185        let edition_dates: HashMap<String, String> = HashMap::new();
186        let mut rng_adapter = crate::game_rng::ThreadRngAdapter;
187        let mut ctx = EffectContext {
188            game: &mut game,
189            combat: None,
190            agents: &mut agents,
191            trigger_handler: &mut th,
192            token_templates: &templates,
193            token_art_variants: &templates_variants,
194            token_fallback: &token_fallback,
195            edition_dates: &edition_dates,
196            mana_pools: &mut mp,
197            parent_target_card: None,
198            rng: &mut rng_adapter,
199        };
200        super::PhasesEffect::resolve(&mut ctx, &sa);
201
202        assert!(!ctx.game.card(c1).phased_out);
203    }
204}