Skip to main content

manabrew_engine/ability/effects/
add_turn_effect.rs

1use super::{resolve_defined_player, resolve_numeric_svar, EffectContext};
2use crate::parsing::keys;
3use crate::phase::ExtraTurn;
4use crate::spellability::SpellAbility;
5
6/// Resolve `SP$ AddTurn` — give a player extra turns.
7///
8/// Mirrors Java `AddTurnEffect.java`.
9/// Pushes the player onto the `extra_turns` queue in `GameState`.
10/// The game loop's `AdvanceTurn` pops from this queue instead of
11/// advancing to the next player in turn order.
12///
13/// # Card script examples
14/// ```text
15/// A:SP$ AddTurn | Defined$ You | NumTurns$ 1
16/// A:SP$ AddTurn | Defined$ You | NumTurns$ 2
17/// A:SP$ AddTurn | Defined$ You | NumTurns$ 1 | SkipUntap$ True
18/// ```
19/// Struct form of this effect so it can participate in the
20/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
21/// `AddTurnEffect` class extending `SpellAbilityEffect`.
22#[manabrew_engine_macros::spell_effect(AddTurnEffect)]
23fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
24    let controller = sa.activating_player;
25
26    let num_turns = resolve_numeric_svar(ctx.game, sa, keys::NUM_TURNS, 1);
27    let skip_untap = sa.ir.skip_untap;
28
29    let defined = sa.defined().unwrap_or("You");
30
31    let target = resolve_defined_player(defined, controller, ctx.game).unwrap_or(controller);
32
33    if !ctx.game.player(target).is_alive() {
34        return;
35    }
36
37    for _ in 0..num_turns {
38        let mut et = ExtraTurn::new(target);
39        et.set_skip_untap(skip_untap);
40        ctx.game.extra_turns.push_back(et);
41    }
42}
43
44/// Create an effect that prevents schemes from being set in motion during
45/// the extra turn. Mirrors Java `AddTurnEffect.createCantSetSchemesInMotionEffect`.
46///
47/// In Archenemy, when extra turns are granted, schemes can't be set in motion
48/// during those extra turns. This creates an effect card in the command zone
49/// with a replacement effect that intercepts SetInMotion events, then exiles
50/// the effect at end of turn.
51pub fn create_cant_set_schemes_in_motion_effect(ctx: &mut EffectContext, sa: &SpellAbility) {
52    use forge_foundation::{CardTypeLine, ColorSet, ManaCost, ZoneType};
53
54    let source_id = match sa.source {
55        Some(id) => id,
56        None => return,
57    };
58    let controller = sa.activating_player;
59    let card_name = ctx.game.card(source_id).card_name.clone();
60
61    // Create a minimal effect card (mirrors Java's createEffect helper)
62    let effect_card = crate::card::Card::new(
63        crate::ids::CardId(0),
64        format!("{card_name}'s Effect"),
65        controller,
66        CardTypeLine::default(),
67        ManaCost::no_cost(),
68        ColorSet::COLORLESS,
69        None,
70        None,
71        vec![],
72        vec![],
73    );
74    let effect_id = ctx.game.create_card(effect_card);
75
76    // Store the replacement effect description — the replacement handler will
77    // check this SVar when processing SetInMotion events.
78    ctx.game.card_mut(effect_id).set_s_var(
79        "ReplacementEffect",
80        "Event$ SetInMotion | EffectZone$ Command | Layer$ CantHappen | Description$ Schemes can't be set in Motion".to_string(),
81    );
82
83    // Move to command zone so it stays active
84    ctx.game.move_card(effect_id, ZoneType::Command, controller);
85
86    // Mark for end-of-turn exile (the cleanup step will remove cards
87    // with this marker from the command zone)
88    ctx.game
89        .card_mut(effect_id)
90        .set_s_var("ExileAtEndOfTurn", "True".to_string());
91}
92
93#[cfg(test)]
94mod tests {
95    use crate::ability::spell_ability_effect::SpellAbilityEffect;
96    use std::collections::HashMap;
97
98    use crate::ability::effects::EffectContext;
99    use crate::agent::PassAgent;
100    use crate::game::GameState;
101    use crate::ids::PlayerId;
102    use crate::mana::ManaPool;
103    use crate::spellability::SpellAbility;
104    use crate::trigger::handler::TriggerHandler;
105
106    #[test]
107    fn add_turn_queues_extra_turns() {
108        let mut game = GameState::new(&["Alice", "Bob"], 20);
109        let p0 = PlayerId(0);
110
111        let sa = SpellAbility::new_simple(None, p0, "SP$ AddTurn | Defined$ You | NumTurns$ 2");
112
113        let mut th = TriggerHandler::new();
114        let mut agents: Vec<Box<dyn crate::agent::PlayerAgent>> =
115            vec![Box::new(PassAgent), Box::new(PassAgent)];
116        let mut mp = vec![ManaPool::default(), ManaPool::default()];
117        let templates = HashMap::new();
118        let templates_variants = HashMap::new();
119        let token_fallback = HashMap::new();
120        let edition_dates: HashMap<String, String> = HashMap::new();
121        let mut rng_adapter = crate::game_rng::ThreadRngAdapter;
122        let mut ctx = EffectContext {
123            game: &mut game,
124            combat: None,
125            agents: &mut agents,
126            trigger_handler: &mut th,
127            token_templates: &templates,
128            token_art_variants: &templates_variants,
129            token_fallback: &token_fallback,
130            edition_dates: &edition_dates,
131            mana_pools: &mut mp,
132            parent_target_card: None,
133            rng: &mut rng_adapter,
134        };
135        super::AddTurnEffect::resolve(&mut ctx, &sa);
136
137        assert_eq!(ctx.game.extra_turns.len(), 2);
138        assert_eq!(ctx.game.extra_turns[0].player, p0);
139        assert_eq!(ctx.game.extra_turns[1].player, p0);
140    }
141
142    #[test]
143    fn add_turn_default_one() {
144        let mut game = GameState::new(&["Alice", "Bob"], 20);
145        let p0 = PlayerId(0);
146
147        let sa = SpellAbility::new_simple(None, p0, "SP$ AddTurn | Defined$ You");
148
149        let mut th = TriggerHandler::new();
150        let mut agents: Vec<Box<dyn crate::agent::PlayerAgent>> =
151            vec![Box::new(PassAgent), Box::new(PassAgent)];
152        let mut mp = vec![ManaPool::default(), ManaPool::default()];
153        let templates = HashMap::new();
154        let templates_variants = HashMap::new();
155        let token_fallback = HashMap::new();
156        let edition_dates: HashMap<String, String> = HashMap::new();
157        let mut rng_adapter = crate::game_rng::ThreadRngAdapter;
158        let mut ctx = EffectContext {
159            game: &mut game,
160            combat: None,
161            agents: &mut agents,
162            trigger_handler: &mut th,
163            token_templates: &templates,
164            token_art_variants: &templates_variants,
165            token_fallback: &token_fallback,
166            edition_dates: &edition_dates,
167            mana_pools: &mut mp,
168            parent_target_card: None,
169            rng: &mut rng_adapter,
170        };
171        super::AddTurnEffect::resolve(&mut ctx, &sa);
172
173        assert_eq!(ctx.game.extra_turns.len(), 1);
174        assert_eq!(ctx.game.extra_turns[0].player, p0);
175        assert!(!ctx.game.extra_turns[0].skip_untap);
176    }
177}