manabrew_engine/ability/effects/
sacrifice_all_effect.rs1use 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#[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 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 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; }
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 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 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 {
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 {
157 let card = ctx.game.card_mut(card_id);
158 card.clear_pump_triggers();
159 }
160 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}