Skip to main content

manabrew_engine/ability/effects/
life_exchange_effect.rs

1use super::{resolve_defined_player, EffectContext};
2use crate::event::RunParams;
3use crate::trigger::TriggerType;
4
5/// Resolve `SP$ LifeExchange` — exchange life totals between two players.
6///
7/// Mirrors Java `LifeExchangeEffect.java`.
8///
9/// # Card script examples
10/// ```text
11/// A:SP$ LifeExchange | ValidTgts$ Player
12/// A:SP$ LifeExchange | Defined$ Opponent
13/// ```
14/// Struct form of this effect so it can participate in the
15/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
16/// `LifeExchangeEffect` class extending `SpellAbilityEffect`.
17#[manabrew_engine_macros::spell_effect(LifeExchangeEffect)]
18fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
19    let controller = sa.activating_player;
20
21    // Determine the other player: targeted or Defined$
22    let other = if let Some(target_player) = sa.target_chosen.target_player {
23        target_player
24    } else {
25        let defined = sa.defined().unwrap_or("Opponent");
26        resolve_defined_player(defined, controller, ctx.game)
27            .unwrap_or_else(|| ctx.game.opponent_of(controller))
28    };
29
30    if !ctx.game.player(controller).is_alive() || !ctx.game.player(other).is_alive() {
31        return;
32    }
33
34    let life_a = ctx.game.player(controller).life;
35    let life_b = ctx.game.player(other).life;
36
37    // Set each player's life to the other's previous total
38    let diff_a = ctx.game.player_set_life(controller, life_b);
39    let diff_b = ctx.game.player_set_life(other, life_a);
40
41    // Fire triggers for controller
42    if diff_a > 0 {
43        ctx.trigger_handler.run_trigger(
44            TriggerType::LifeGained,
45            RunParams {
46                player: Some(controller),
47                life_amount: Some(diff_a),
48                first_time: Some(ctx.game.player(controller).life_gained_this_turn == diff_a),
49                source_card: sa.source,
50                source_sa: Some(sa.clone()),
51                ..Default::default()
52            },
53            false,
54        );
55    } else if diff_a < 0 {
56        ctx.trigger_handler.run_trigger(
57            TriggerType::LifeLost,
58            RunParams {
59                player: Some(controller),
60                life_amount: Some(diff_a.abs()),
61                first_time: Some(ctx.game.player(controller).life_lost_this_turn == diff_a.abs()),
62                source_card: sa.source,
63                source_sa: Some(sa.clone()),
64                ..Default::default()
65            },
66            false,
67        );
68    }
69
70    // Fire triggers for the other player
71    if diff_b > 0 {
72        ctx.trigger_handler.run_trigger(
73            TriggerType::LifeGained,
74            RunParams {
75                player: Some(other),
76                life_amount: Some(diff_b),
77                first_time: Some(ctx.game.player(other).life_gained_this_turn == diff_b),
78                source_card: sa.source,
79                source_sa: Some(sa.clone()),
80                ..Default::default()
81            },
82            false,
83        );
84    } else if diff_b < 0 {
85        ctx.trigger_handler.run_trigger(
86            TriggerType::LifeLost,
87            RunParams {
88                player: Some(other),
89                life_amount: Some(diff_b.abs()),
90                first_time: Some(ctx.game.player(other).life_lost_this_turn == diff_b.abs()),
91                source_card: sa.source,
92                source_sa: Some(sa.clone()),
93                ..Default::default()
94            },
95            false,
96        );
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use crate::ability::spell_ability_effect::SpellAbilityEffect;
103    use std::collections::HashMap;
104
105    use crate::ability::effects::EffectContext;
106    use crate::agent::PassAgent;
107    use crate::game::GameState;
108    use crate::ids::PlayerId;
109    use crate::mana::ManaPool;
110    use crate::spellability::SpellAbility;
111    use crate::trigger::handler::TriggerHandler;
112
113    #[test]
114    fn life_exchange_swaps_totals() {
115        let mut game = GameState::new(&["Alice", "Bob"], 20);
116        let p0 = PlayerId(0);
117        let p1 = PlayerId(1);
118
119        // Set different life totals
120        game.player_mut(p0).life = 5;
121        game.player_mut(p1).life = 30;
122
123        let sa = SpellAbility::new_simple(None, p0, "SP$ LifeExchange | Defined$ Opponent");
124
125        let mut th = TriggerHandler::new();
126        let mut agents: Vec<Box<dyn crate::agent::PlayerAgent>> =
127            vec![Box::new(PassAgent), Box::new(PassAgent)];
128        let mut mp = vec![ManaPool::default(), ManaPool::default()];
129        let templates = HashMap::new();
130        let templates_variants = HashMap::new();
131        let token_fallback = HashMap::new();
132        let edition_dates: HashMap<String, String> = HashMap::new();
133        let mut rng_adapter = crate::game_rng::ThreadRngAdapter;
134        let mut ctx = EffectContext {
135            game: &mut game,
136            combat: None,
137            agents: &mut agents,
138            trigger_handler: &mut th,
139            token_templates: &templates,
140            token_art_variants: &templates_variants,
141            token_fallback: &token_fallback,
142            edition_dates: &edition_dates,
143            mana_pools: &mut mp,
144            parent_target_card: None,
145            rng: &mut rng_adapter,
146        };
147        super::LifeExchangeEffect::resolve(&mut ctx, &sa);
148
149        assert_eq!(ctx.game.player(p0).life, 30);
150        assert_eq!(ctx.game.player(p1).life, 5);
151    }
152}