Skip to main content

manabrew_engine/ability/effects/
power_exchange_effect.rs

1use forge_foundation::ZoneType;
2
3use super::EffectContext;
4use crate::card::perpetual::perpetual_interface::PerpetualInterface;
5use crate::card::perpetual::perpetual_new_pt;
6
7/// End-of-turn revert for power exchange. Mirrors the `GameCommand.run()` in Java
8/// `PowerExchangeEffect` that calls `removeNewPT` on both cards when the
9/// duration expires, restoring their original power values.
10///
11/// Takes two card IDs and the original power values to restore.
12pub fn run(
13    game: &mut crate::game::GameState,
14    card1: crate::ids::CardId,
15    card2: crate::ids::CardId,
16) {
17    // Reset power modifiers to 0 to revert the exchange
18    if game.card(card1).zone == ZoneType::Battlefield {
19        game.card_mut(card1).set_power_modifier(0);
20    }
21    if game.card(card2).zone == ZoneType::Battlefield {
22        game.card_mut(card2).set_power_modifier(0);
23    }
24}
25
26/// Resolve `SP$ PowerExchange` — swap power between two creatures.
27///
28/// Mirrors Java `PowerExchangeEffect.java`.
29///
30/// # Card script examples
31/// ```text
32/// A:SP$ PowerExchange | ValidTgts$ Creature | TgtPrompt$ Select target creature
33/// ```
34/// Struct form of this effect so it can participate in the
35/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
36/// `PowerExchangeEffect` class extending `SpellAbilityEffect`.
37#[manabrew_engine_macros::spell_effect(PowerExchangeEffect)]
38fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
39    let source = sa.source;
40    let target = sa.target_chosen.target_card;
41
42    let (c1, c2) = match (source, target) {
43        (Some(s), Some(t)) => (s, t),
44        _ => return,
45    };
46
47    if ctx.game.card(c1).zone != ZoneType::Battlefield
48        || ctx.game.card(c2).zone != ZoneType::Battlefield
49    {
50        return;
51    }
52
53    // Both must be creatures
54    if !ctx.game.card(c1).is_creature() || !ctx.game.card(c2).is_creature() {
55        return;
56    }
57
58    let base_power = sa.ir.base_power;
59    let p1 = if base_power {
60        ctx.game.card(c1).base_power.unwrap_or(0)
61    } else {
62        ctx.game.card(c1).power()
63    };
64    let p2 = if base_power {
65        ctx.game.card(c2).base_power.unwrap_or(0)
66    } else {
67        ctx.game.card(c2).power()
68    };
69    let is_perpetual = matches!(
70        sa.ir.duration,
71        Some(crate::spellability::AbilityDuration::Perpetual)
72    );
73
74    if is_perpetual {
75        let ts = ctx.game.next_effect_timestamp();
76        perpetual_new_pt::PerpetualNewPt {
77            timestamp: ts,
78            power: Some(p2),
79            toughness: None,
80        }
81        .apply_effect(ctx.game.card_mut(c1));
82        perpetual_new_pt::PerpetualNewPt {
83            timestamp: ts,
84            power: Some(p1),
85            toughness: None,
86        }
87        .apply_effect(ctx.game.card_mut(c2));
88        return;
89    }
90
91    // Calculate the modifier deltas needed to swap powers
92    let c1_base = ctx
93        .game
94        .card(c1)
95        .static_set_power
96        .unwrap_or(ctx.game.card(c1).base_power.unwrap_or(0));
97    let c2_base = ctx
98        .game
99        .card(c2)
100        .static_set_power
101        .unwrap_or(ctx.game.card(c2).base_power.unwrap_or(0));
102
103    // Set power modifiers so effective power = the other's power
104    let c1_static = ctx.game.card(c1).static_power_modifier;
105    let c2_static = ctx.game.card(c2).static_power_modifier;
106    ctx.game
107        .card_mut(c1)
108        .set_power_modifier(p2 - c1_base - c1_static);
109    ctx.game
110        .card_mut(c2)
111        .set_power_modifier(p1 - c2_base - c2_static);
112}
113
114#[cfg(test)]
115mod tests {
116    use crate::ability::spell_ability_effect::SpellAbilityEffect;
117    use forge_foundation::{CardTypeLine, ColorSet, ManaCost, ZoneType};
118    use std::collections::HashMap;
119
120    use crate::ability::effects::EffectContext;
121    use crate::agent::PassAgent;
122    use crate::card::Card;
123    use crate::game::GameState;
124    use crate::ids::{CardId, PlayerId};
125    use crate::mana::ManaPool;
126    use crate::spellability::SpellAbility;
127    use crate::trigger::handler::TriggerHandler;
128
129    #[test]
130    fn power_exchange_swaps() {
131        let mut game = GameState::new(&["Alice", "Bob"], 20);
132        let p0 = PlayerId(0);
133
134        let c1 = game.create_card(Card::new(
135            CardId(0),
136            "Bear".into(),
137            p0,
138            CardTypeLine::parse("Creature - Bear"),
139            ManaCost::parse("1 G"),
140            ColorSet::GREEN,
141            Some(2),
142            Some(2),
143            vec![],
144            vec![],
145        ));
146        game.move_card(c1, ZoneType::Battlefield, p0);
147
148        let c2 = game.create_card(Card::new(
149            CardId(0),
150            "Dragon".into(),
151            p0,
152            CardTypeLine::parse("Creature - Dragon"),
153            ManaCost::parse("4 R R"),
154            ColorSet::RED,
155            Some(5),
156            Some(5),
157            vec![],
158            vec![],
159        ));
160        game.move_card(c2, ZoneType::Battlefield, p0);
161
162        assert_eq!(game.card(c1).power(), 2);
163        assert_eq!(game.card(c2).power(), 5);
164
165        let mut sa = SpellAbility::new_simple(Some(c1), p0, "SP$ PowerExchange");
166        sa.target_chosen.target_card = Some(c2);
167
168        let mut th = TriggerHandler::new();
169        let mut agents: Vec<Box<dyn crate::agent::PlayerAgent>> =
170            vec![Box::new(PassAgent), Box::new(PassAgent)];
171        let mut mp = vec![ManaPool::default(), ManaPool::default()];
172        let templates = HashMap::new();
173        let templates_variants = HashMap::new();
174        let token_fallback = HashMap::new();
175        let edition_dates: HashMap<String, String> = HashMap::new();
176        let mut rng_adapter = crate::game_rng::ThreadRngAdapter;
177        let mut ctx = EffectContext {
178            game: &mut game,
179            combat: None,
180            agents: &mut agents,
181            trigger_handler: &mut th,
182            token_templates: &templates,
183            token_art_variants: &templates_variants,
184            token_fallback: &token_fallback,
185            edition_dates: &edition_dates,
186            mana_pools: &mut mp,
187            parent_target_card: None,
188            rng: &mut rng_adapter,
189        };
190        super::PowerExchangeEffect::resolve(&mut ctx, &sa);
191
192        assert_eq!(ctx.game.card(c1).power(), 5);
193        assert_eq!(ctx.game.card(c2).power(), 2);
194    }
195}