Skip to main content

manabrew_engine/ability/effects/
sacrifice_all_effect.rs

1use forge_foundation::ZoneType;
2
3use super::{emit_zone_trigger_with_lki_counters, matches_change_type, EffectContext};
4use crate::ability::ability_ir::DefinedRef;
5use crate::card::valid_filter;
6use crate::event::{AbilityValue, RunParams};
7use crate::ids::CardId;
8use crate::trigger::TriggerType;
9
10/// Struct form of this effect so it can participate in the
11/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
12/// `SacrificeAllEffect` class extending `SpellAbilityEffect`.
13#[manabrew_engine_macros::spell_effect(SacrificeAllEffect)]
14fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
15    let valid_cards = sa.ir.valid_cards_selector.as_ref();
16    let valid_cards_filter = sa.ir.valid_cards_text.as_deref().unwrap_or("Creature");
17
18    // When Defined$ narrows the sacrifice to specific cards (e.g. Ashling's
19    // `DelayTriggerRememberedLKI` targets the token created by the parent
20    // trigger), sacrifice only those cards instead of every matching
21    // permanent on the battlefield.
22    let defined_cards: Option<Vec<CardId>> = match sa.defined_ref() {
23        Some(
24            DefinedRef::DelayTriggerRememberedLki
25            | DefinedRef::DelayTriggerRemembered
26            | DefinedRef::Remembered,
27        ) => {
28            let mut ids = Vec::new();
29            for value in &sa.trigger_remembered {
30                if let AbilityValue::Card(cid) = value {
31                    ids.push(*cid);
32                }
33            }
34            Some(ids)
35        }
36        _ => None,
37    };
38
39    // UnlessCost$ X | UnlessPayer$ You — offer the payer a chance to pay a
40    // cost to prevent the sacrifice entirely. Java's deterministic AI pays
41    // unless-costs when able (auto-tapping lands for mana).
42    if let Some(unless_cost_str) = sa.ir.unless_cost.as_deref() {
43        let source = sa.source.unwrap_or(CardId(0));
44        let cost = crate::cost::parse_cost(unless_cost_str);
45        let payers = super::helpers::resolve_defined_players(
46            sa.ir.unless_payer_text.as_deref().unwrap_or("You"),
47            sa.activating_player,
48            ctx.game,
49        );
50        for payer in payers {
51            if ctx.game.player(payer).has_lost {
52                continue;
53            }
54            let can_pay = can_pay_unless_cost(ctx, sa, source, payer, &cost);
55            let cost_kind = cost.to_simple_string();
56            let cost_display = crate::cost::to_prompt_string(&cost);
57            let prompt = if cost_display.is_empty() {
58                "Pay this cost?".to_string()
59            } else {
60                format!("Pay {}?", cost_display)
61            };
62            ctx.agents[payer.index()].snapshot_state(ctx.game, ctx.mana_pools);
63            let wants_to_pay = ctx.agents[payer.index()].pay_cost_to_prevent_effect(
64                payer,
65                if cost_kind.is_empty() {
66                    "UnlessCost"
67                } else {
68                    cost_kind.as_str()
69                },
70                &prompt,
71                sa.source,
72                sa.api,
73                can_pay,
74                &[],
75                &if sa.stack_description.is_empty() {
76                    sa.rebuilt_description()
77                } else {
78                    sa.stack_description.clone()
79                },
80            );
81            let paid = wants_to_pay
82                && can_pay
83                && super::try_pay_unless_cost(ctx, sa, source, payer, &cost);
84            if paid {
85                return; // Cost paid — sacrifice prevented
86            }
87        }
88    }
89
90    let player_ids = ctx.game.player_order.clone();
91    let mut to_sacrifice: Vec<CardId> = Vec::new();
92    if let Some(defined_ids) = defined_cards {
93        for cid in defined_ids {
94            if cid.index() < ctx.game.cards.len()
95                && ctx.game.card(cid).zone == ZoneType::Battlefield
96                && !ctx.game.card(cid).phased_out
97            {
98                to_sacrifice.push(cid);
99            }
100        }
101    } else {
102        for &pid in &player_ids {
103            let zone_cards = ctx.game.cards_in_zone(ZoneType::Battlefield, pid).to_vec();
104            for cid in zone_cards {
105                let card = ctx.game.card(cid);
106                if card.phased_out {
107                    continue;
108                }
109                let matches = match (valid_cards, sa.source) {
110                    (Some(selector), Some(source_id)) => {
111                        valid_filter::matches_valid_card_selector_in_game(
112                            selector,
113                            card,
114                            ctx.game.card(source_id),
115                            ctx.game,
116                        )
117                    }
118                    _ => matches_change_type(card, valid_cards_filter, &[]),
119                };
120                if matches {
121                    to_sacrifice.push(cid);
122                }
123            }
124        }
125    }
126
127    // Track per-controller batches to fire SacrificedOnce once per controller after
128    // the whole sweep (mirrors Java GameAction.sacrifice line 2133-2138).
129    let mut by_controller: std::collections::BTreeMap<crate::ids::PlayerId, Vec<CardId>> =
130        std::collections::BTreeMap::new();
131
132    for card_id in to_sacrifice {
133        if ctx.game.card(card_id).zone != ZoneType::Battlefield {
134            continue;
135        }
136        let controller = ctx.game.card(card_id).controller;
137        let owner = ctx.game.card(card_id).owner;
138        // Capture +1/+1 counter count before move (for Modular death triggers)
139        let lki_p1p1 = *ctx
140            .game
141            .card(card_id)
142            .counters
143            .get(&crate::card::CounterType::P1P1)
144            .unwrap_or(&0);
145        let lki_power = ctx.game.card(card_id).power();
146        let lki_toughness = ctx.game.card(card_id).toughness();
147        // Capture LKI counters for death triggers
148        {
149            let lki_counters = ctx.game.card(card_id).counters.clone();
150            ctx.game.card_mut(card_id).lki_counters = Some(lki_counters);
151            ctx.game
152                .card_mut(card_id)
153                .set_lki_power_toughness(Some(lki_power), Some(lki_toughness));
154        }
155        // Clear temporary Animate triggers before firing events (CR 400.7).
156        {
157            let card = ctx.game.card_mut(card_id);
158            card.clear_pump_triggers();
159        }
160        // Fire Sacrificed trigger
161        ctx.trigger_handler.run_trigger(
162            TriggerType::Sacrificed,
163            RunParams {
164                card: Some(card_id),
165                player: Some(controller),
166                ..Default::default()
167            },
168            false,
169        );
170        emit_zone_trigger_with_lki_counters(
171            ctx.trigger_handler,
172            card_id,
173            ZoneType::Battlefield,
174            ZoneType::Graveyard,
175            lki_p1p1,
176            lki_power,
177            lki_toughness,
178        );
179        ctx.move_card(card_id, ZoneType::Graveyard, owner);
180        ctx.trigger_handler.flush_waiting_triggers(ctx.game);
181        by_controller.entry(controller).or_default().push(card_id);
182    }
183
184    crate::game_loop::fire_sacrificed_once_for_batch(ctx.game, ctx.trigger_handler, &by_controller);
185}
186
187fn can_pay_unless_cost(
188    ctx: &EffectContext,
189    sa: &crate::spellability::SpellAbility,
190    source: CardId,
191    payer: crate::ids::PlayerId,
192    cost: &crate::cost::Cost,
193) -> bool {
194    if cost.has_mana_cost()
195        && !crate::mana::can_pay_mana_cost_with_reserved_sacrifices(
196            ctx.game,
197            &ctx.mana_pools[payer.index()],
198            payer,
199            source,
200            cost,
201            &[],
202            None,
203        )
204    {
205        return false;
206    }
207
208    let non_mana_cost = cost.copy_with_no_mana();
209    if non_mana_cost.parts.is_empty() {
210        return true;
211    }
212    let available =
213        crate::mana::calculate_available_mana(&ctx.mana_pools[payer.index()], ctx.game, payer);
214    crate::cost::can_pay_with_ability(
215        &non_mana_cost,
216        ctx.game,
217        &available,
218        source,
219        payer,
220        Some(sa),
221    )
222}