Skip to main content

manabrew_engine/ability/effects/
life_lose_effect.rs

1use super::{resolve_defined_player_with_sa, resolve_numeric_svar, EffectContext};
2use crate::ability::ability_ir::EffectIr;
3use crate::event::RunParams;
4use crate::replacement::replacement_handler::{apply_replacements, ReplacementEvent};
5use crate::replacement::ReplacementResult;
6use crate::spellability::SpellAbility;
7use crate::trigger::TriggerType;
8
9/// Struct form of this effect so it can participate in the
10/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
11/// `LifeLoseEffect` class extending `SpellAbilityEffect`.
12#[manabrew_engine_macros::spell_effect(LifeLoseEffect)]
13fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
14    let amount = resolve_life_amount(ctx, sa);
15    // Mirror Java getTargetPlayers(): targeted player first, then Defined, then activator.
16    let target = sa
17        .target_chosen
18        .target_player
19        .or_else(|| {
20            sa.defined().and_then(|defined| {
21                resolve_defined_player_with_sa(defined, sa, sa.activating_player, ctx.game)
22            })
23        })
24        .unwrap_or(sa.activating_player);
25    if crate::staticability::static_ability_cant_gain_lose_pay_life::cant_lose_life(
26        ctx.game, target,
27    ) {
28        return;
29    }
30
31    // Run LifeReduced replacement effects before losing life.
32    let mut event = ReplacementEvent::LifeReduced {
33        player: target,
34        amount,
35        is_damage: false,
36    };
37    let result = apply_replacements(ctx.game, &mut event);
38    if result == ReplacementResult::Skipped || result == ReplacementResult::Replaced {
39        return;
40    }
41    let amount = if let ReplacementEvent::LifeReduced {
42        amount: final_amount,
43        ..
44    } = event
45    {
46        final_amount
47    } else {
48        amount
49    };
50    if amount <= 0 {
51        return;
52    }
53
54    ctx.game.player_lose_life(target, amount);
55
56    // Set AFLifeLost SVar on source card so chained sub-abilities (e.g. GainLife) can read it.
57    // Mirrors Java's `sa.setSVar("AFLifeLost", "Number$" + lifeLost)`.
58    if let Some(source_id) = sa.source {
59        ctx.game
60            .card_mut(source_id)
61            .svars
62            .insert("AFLifeLost".to_string(), format!("Number${amount}"));
63    }
64
65    // Per-player `LifeLost` trigger.
66    ctx.trigger_handler.run_trigger(
67        TriggerType::LifeLost,
68        RunParams {
69            player: Some(target),
70            life_amount: Some(amount),
71            first_time: Some(ctx.game.player(target).life_lost_this_turn == amount),
72            source_card: sa.source,
73            source_sa: Some(sa.clone()),
74            ..Default::default()
75        },
76        false,
77    );
78
79    // Java fires one aggregated `LifeLostAll` per effect; the Rust engine's
80    // `LifeLose` only processes one target per call, so the aggregate map has
81    // a single entry. Fire here so trigger-on-opponent-life-loss cards (e.g.
82    // the Speed mechanic) see the event.
83    ctx.trigger_handler.run_trigger(
84        TriggerType::LifeLostAll,
85        RunParams {
86            player: Some(target),
87            life_amount: Some(amount),
88            source_card: sa.source,
89            source_sa: Some(sa.clone()),
90            ..Default::default()
91        },
92        false,
93    );
94}
95
96fn resolve_life_amount(ctx: &EffectContext, sa: &SpellAbility) -> i32 {
97    if let Some(EffectIr::LoseLife(ir)) = &sa.ir.effect {
98        if let Some(amount) = &ir.amount {
99            let resolved = amount.resolve_for_spell_ability(ctx.game, sa, 1);
100            #[cfg(debug_assertions)]
101            debug_assert_eq!(
102                resolved,
103                resolve_numeric_svar(ctx.game, sa, crate::parsing::keys::LIFE_AMOUNT, 1),
104                "compiled LoseLife amount diverged from string params"
105            );
106            return resolved;
107        }
108    }
109
110    resolve_numeric_svar(ctx.game, sa, crate::parsing::keys::LIFE_AMOUNT, 1)
111}