Skip to main content

manabrew_engine/ability/effects/
life_set_effect.rs

1use super::{resolve_defined_players, resolve_numeric_svar, EffectContext};
2use crate::ability::ability_ir::EffectIr;
3use crate::event::RunParams;
4use crate::spellability::SpellAbility;
5use crate::trigger::TriggerType;
6
7/// Resolve `SP$ LifeSet` — set a player's life total to a specific value.
8///
9/// Mirrors Java `LifeSetEffect.java`.
10/// Supports multi-player targeting via `Defined$ Each`.
11///
12/// # Card script examples
13/// ```text
14/// A:SP$ LifeSet | Defined$ You | LifeAmount$ 10
15/// A:SP$ LifeSet | Defined$ Opponent | LifeAmount$ 1
16/// A:SP$ LifeSet | Defined$ Each | LifeAmount$ 20
17/// ```
18/// Struct form of this effect so it can participate in the
19/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
20/// `LifeSetEffect` class extending `SpellAbilityEffect`.
21#[manabrew_engine_macros::spell_effect(LifeSetEffect)]
22fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
23    let controller = sa.activating_player;
24
25    let amount = resolve_life_set_amount(ctx, sa);
26
27    let defined = sa.defined().unwrap_or("You");
28
29    let targets = resolve_defined_players(defined, controller, ctx.game);
30
31    for target in targets {
32        if !ctx.game.player(target).is_alive() {
33            continue;
34        }
35
36        let diff = ctx.game.player_set_life(target, amount);
37
38        // Fire the appropriate life trigger based on the difference
39        if diff > 0 {
40            ctx.trigger_handler.run_trigger(
41                TriggerType::LifeGained,
42                RunParams {
43                    player: Some(target),
44                    life_amount: Some(diff),
45                    first_time: Some(ctx.game.player(target).life_gained_this_turn == diff),
46                    source_card: sa.source,
47                    source_sa: Some(sa.clone()),
48                    ..Default::default()
49                },
50                false,
51            );
52        } else if diff < 0 {
53            ctx.trigger_handler.run_trigger(
54                TriggerType::LifeLost,
55                RunParams {
56                    player: Some(target),
57                    life_amount: Some(diff.abs()),
58                    first_time: Some(ctx.game.player(target).life_lost_this_turn == diff.abs()),
59                    source_card: sa.source,
60                    source_sa: Some(sa.clone()),
61                    ..Default::default()
62                },
63                false,
64            );
65        }
66    }
67}
68
69fn resolve_life_set_amount(ctx: &EffectContext, sa: &SpellAbility) -> i32 {
70    if let Some(EffectIr::LifeSet(ir)) = &sa.ir.effect {
71        if let Some(amount) = &ir.amount {
72            let resolved = amount.resolve_for_spell_ability(ctx.game, sa, 0);
73            #[cfg(debug_assertions)]
74            debug_assert_eq!(
75                resolved,
76                resolve_numeric_svar(ctx.game, sa, crate::parsing::keys::LIFE_AMOUNT, 0),
77                "compiled SetLife amount diverged from string params"
78            );
79            return resolved;
80        }
81    }
82
83    resolve_numeric_svar(ctx.game, sa, crate::parsing::keys::LIFE_AMOUNT, 0)
84}
85
86#[cfg(test)]
87mod tests {
88    use crate::ability::spell_ability_effect::SpellAbilityEffect;
89    use std::collections::HashMap;
90
91    use crate::ability::effects::EffectContext;
92    use crate::agent::PassAgent;
93    use crate::game::GameState;
94    use crate::ids::PlayerId;
95    use crate::mana::ManaPool;
96    use crate::spellability::SpellAbility;
97    use crate::trigger::handler::TriggerHandler;
98
99    #[test]
100    fn life_set_reduces_life() {
101        let mut game = GameState::new(&["Alice", "Bob"], 20);
102        let p0 = PlayerId(0);
103
104        let sa = SpellAbility::new_simple(None, p0, "SP$ LifeSet | Defined$ You | LifeAmount$ 10");
105
106        let mut th = TriggerHandler::new();
107        let mut agents: Vec<Box<dyn crate::agent::PlayerAgent>> =
108            vec![Box::new(PassAgent), Box::new(PassAgent)];
109        let mut mp = vec![ManaPool::default(), ManaPool::default()];
110        let templates = HashMap::new();
111        let templates_variants = HashMap::new();
112        let token_fallback = HashMap::new();
113        let edition_dates: HashMap<String, String> = HashMap::new();
114        let mut rng_adapter = crate::game_rng::ThreadRngAdapter;
115        let mut ctx = EffectContext {
116            game: &mut game,
117            combat: None,
118            agents: &mut agents,
119            trigger_handler: &mut th,
120            token_templates: &templates,
121            token_art_variants: &templates_variants,
122            token_fallback: &token_fallback,
123            edition_dates: &edition_dates,
124            mana_pools: &mut mp,
125            parent_target_card: None,
126            rng: &mut rng_adapter,
127        };
128        super::LifeSetEffect::resolve(&mut ctx, &sa);
129
130        assert_eq!(ctx.game.player(p0).life, 10);
131        assert_eq!(ctx.game.player(p0).life_lost_this_turn, 10);
132    }
133
134    #[test]
135    fn life_set_increases_life() {
136        let mut game = GameState::new(&["Alice", "Bob"], 5);
137        let p0 = PlayerId(0);
138
139        let sa = SpellAbility::new_simple(None, p0, "SP$ LifeSet | Defined$ You | LifeAmount$ 20");
140
141        let mut th = TriggerHandler::new();
142        let mut agents: Vec<Box<dyn crate::agent::PlayerAgent>> =
143            vec![Box::new(PassAgent), Box::new(PassAgent)];
144        let mut mp = vec![ManaPool::default(), ManaPool::default()];
145        let templates = HashMap::new();
146        let templates_variants = HashMap::new();
147        let token_fallback = HashMap::new();
148        let edition_dates: HashMap<String, String> = HashMap::new();
149        let mut rng_adapter = crate::game_rng::ThreadRngAdapter;
150        let mut ctx = EffectContext {
151            game: &mut game,
152            combat: None,
153            agents: &mut agents,
154            trigger_handler: &mut th,
155            token_templates: &templates,
156            token_art_variants: &templates_variants,
157            token_fallback: &token_fallback,
158            edition_dates: &edition_dates,
159            mana_pools: &mut mp,
160            parent_target_card: None,
161            rng: &mut rng_adapter,
162        };
163        super::LifeSetEffect::resolve(&mut ctx, &sa);
164
165        assert_eq!(ctx.game.player(p0).life, 20);
166        assert_eq!(ctx.game.player(p0).life_gained_this_turn, 15);
167    }
168}