1use std::collections::BTreeMap;
2
3use forge_foundation::ZoneType;
4
5use super::{emit_zone_trigger_with_lki_counters, EffectContext};
6use crate::ability::spell_ability_effect::get_target_players;
7use crate::card::CounterType;
8use crate::event::RunParams;
9use crate::ids::{CardId, PlayerId};
10use crate::spellability::SpellAbility;
11use crate::trigger::TriggerType;
12
13fn do_sacrifice(
21 ctx: &mut EffectContext,
22 sa: &SpellAbility,
23 card_id: crate::ids::CardId,
24 sacrificing_player: PlayerId,
25 exploit_source: Option<crate::ids::CardId>,
26) -> Option<CardId> {
27 if ctx.game.card(card_id).zone != ZoneType::Battlefield {
28 return None;
29 }
30 if crate::staticability::static_ability_cant_sacrifice::cant_sacrifice(
31 &ctx.game.cards,
32 ctx.game.card(card_id),
33 Some(sa),
34 false,
35 ) {
36 return None;
37 }
38 let owner = ctx.game.card(card_id).owner;
39
40 let lki_p1p1 = *ctx
44 .game
45 .card(card_id)
46 .counters
47 .get(&crate::card::CounterType::P1P1)
48 .unwrap_or(&0);
49 let lki_power = ctx.game.card(card_id).power();
50 let lki_toughness = ctx.game.card(card_id).toughness();
51 let lki_counters = ctx.game.card(card_id).counters.clone();
53 ctx.game.card_mut(card_id).lki_counters = Some(lki_counters);
54 ctx.game
55 .card_mut(card_id)
56 .set_lki_power_toughness(Some(lki_power), Some(lki_toughness));
57
58 {
60 let card = ctx.game.card_mut(card_id);
61 card.clear_pump_triggers();
62 }
63 ctx.trigger_handler.run_trigger(
65 TriggerType::Sacrificed,
66 RunParams {
67 card: Some(card_id),
68 player: Some(sacrificing_player),
69 ..Default::default()
70 },
71 false,
72 );
73 emit_zone_trigger_with_lki_counters(
76 ctx.trigger_handler,
77 card_id,
78 ZoneType::Battlefield,
79 ZoneType::Graveyard,
80 lki_p1p1,
81 lki_power,
82 lki_toughness,
83 );
84 ctx.move_card(card_id, ZoneType::Graveyard, owner);
85 ctx.trigger_handler.flush_waiting_triggers(ctx.game);
86 if let Some(source_id) = exploit_source {
88 ctx.trigger_handler.run_trigger(
89 TriggerType::Exploited,
90 RunParams {
91 card: Some(source_id),
92 exploited_card: Some(card_id),
93 player: Some(sacrificing_player),
94 ..Default::default()
95 },
96 false,
97 );
98 }
99 Some(card_id)
100}
101
102#[manabrew_engine_macros::spell_effect(SacrificeEffect)]
106fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
107 if let Some(echo_cost_str) = sa.ir.echo.as_deref() {
108 let source_id = match sa.source {
109 Some(cid) if ctx.game.card(cid).zone == ZoneType::Battlefield => cid,
110 _ => return,
111 };
112 let controller = ctx.game.card(source_id).controller;
113 let cost = crate::cost::parse_cost(echo_cost_str);
114 let available_mana = crate::mana::calculate_available_mana(
115 &ctx.mana_pools[controller.index()],
116 ctx.game,
117 controller,
118 );
119 let can_pay = crate::cost::can_pay_with_ability(
120 &cost,
121 ctx.game,
122 &available_mana,
123 source_id,
124 controller,
125 Some(sa),
126 );
127 let cost_kind = cost.to_simple_string();
128 let cost_display = crate::cost::to_prompt_string(&cost);
129 let prompt = if cost_display.is_empty() {
130 "Pay this cost?".to_string()
131 } else {
132 format!("Pay {}?", cost_display)
133 };
134 ctx.agents[controller.index()].snapshot_state(ctx.game, ctx.mana_pools);
135 let wants_to_pay = ctx.agents[controller.index()].pay_cost_to_prevent_effect(
136 controller,
137 if cost_kind.is_empty() {
138 "Echo"
139 } else {
140 cost_kind.as_str()
141 },
142 &prompt,
143 Some(source_id),
144 sa.api,
145 true,
146 &[],
147 &if sa.stack_description.is_empty() {
148 sa.rebuilt_description()
149 } else {
150 sa.stack_description.clone()
151 },
152 );
153 let paid =
154 wants_to_pay && can_pay && super::try_pay_echo(ctx, sa, source_id, controller, &cost);
155
156 ctx.trigger_handler.run_trigger(
157 TriggerType::PayEcho,
158 RunParams {
159 card: Some(source_id),
160 echo_paid: Some(paid),
161 ..Default::default()
162 },
163 false,
164 );
165
166 if paid || ctx.game.card(source_id).controller != controller {
167 return;
168 }
169 if let Some(cid) = do_sacrifice(ctx, sa, source_id, controller, None) {
170 let mut by_controller: BTreeMap<PlayerId, Vec<CardId>> = BTreeMap::new();
171 by_controller.insert(controller, vec![cid]);
172 crate::game_loop::fire_sacrificed_once_for_batch(
173 ctx.game,
174 ctx.trigger_handler,
175 &by_controller,
176 );
177 }
178 return;
179 }
180
181 if let Some(cum_cost_str) = sa.ir.cumulative_upkeep.as_deref() {
186 let source_id = match sa.source {
187 Some(cid) if ctx.game.card(cid).zone == ZoneType::Battlefield => cid,
188 _ => return,
189 };
190 let controller = ctx.game.card(source_id).controller;
191
192 ctx.game
194 .card_mut(source_id)
195 .add_counter(&CounterType::Age, 1);
196
197 let n = ctx
199 .game
200 .card(source_id)
201 .counters
202 .get(&CounterType::Age)
203 .copied()
204 .unwrap_or(0) as usize;
205
206 let base_cost = crate::cost::parse_cost(cum_cost_str);
209 let mut merged_parts = Vec::new();
210 let mut merged_mana: Option<(
211 forge_foundation::ManaCost,
212 i32,
213 bool,
214 bool,
215 bool,
216 Option<String>,
217 )> = None;
218 for _ in 0..n {
219 for part in base_cost.parts.iter().cloned() {
220 match part {
221 crate::cost::CostPart::Mana {
222 cost,
223 x_min,
224 is_exiled_creature_cost,
225 is_enchanted_creature_cost,
226 is_cost_pay_any_number_of_times,
227 max_waterbend,
228 } => {
229 if let Some((
230 total_cost,
231 total_x_min,
232 total_exiled,
233 total_enchanted,
234 total_any_times,
235 total_max_waterbend,
236 )) = &mut merged_mana
237 {
238 *total_cost = total_cost.add(&cost);
239 *total_x_min += x_min;
240 *total_exiled |= is_exiled_creature_cost;
241 *total_enchanted |= is_enchanted_creature_cost;
242 *total_any_times |= is_cost_pay_any_number_of_times;
243 if total_max_waterbend.is_none() {
244 *total_max_waterbend = max_waterbend;
245 }
246 } else {
247 merged_mana = Some((
248 cost,
249 x_min,
250 is_exiled_creature_cost,
251 is_enchanted_creature_cost,
252 is_cost_pay_any_number_of_times,
253 max_waterbend,
254 ));
255 }
256 }
257 other => merged_parts.push(other),
258 }
259 }
260 }
261 if let Some((
262 cost,
263 x_min,
264 is_exiled_creature_cost,
265 is_enchanted_creature_cost,
266 is_cost_pay_any_number_of_times,
267 max_waterbend,
268 )) = merged_mana
269 {
270 merged_parts.push(crate::cost::CostPart::Mana {
271 cost,
272 x_min,
273 is_exiled_creature_cost,
274 is_enchanted_creature_cost,
275 is_cost_pay_any_number_of_times,
276 max_waterbend,
277 });
278 }
279 let merged_cost = crate::cost::Cost {
280 parts: merged_parts,
281 has_tap: false,
282 mandatory: false,
283 };
284
285 let paid = super::try_pay_cumulative_upkeep(ctx, sa, source_id, controller, &merged_cost);
287
288 ctx.trigger_handler.run_trigger(
290 TriggerType::PayCumulativeUpkeep,
291 RunParams {
292 card: Some(source_id),
293 cumulative_upkeep_paid: Some(paid),
294 ..Default::default()
295 },
296 false,
297 );
298
299 if !paid {
301 if let Some(cid) = do_sacrifice(ctx, sa, source_id, controller, None) {
302 let mut by_controller: BTreeMap<PlayerId, Vec<CardId>> = BTreeMap::new();
303 by_controller.insert(controller, vec![cid]);
304 crate::game_loop::fire_sacrificed_once_for_batch(
305 ctx.game,
306 ctx.trigger_handler,
307 &by_controller,
308 );
309 }
310 }
311 return;
312 }
313
314 let sac_valid = sa
315 .ir
316 .sac_valid
317 .clone()
318 .unwrap_or_else(|| "Self".to_string());
319 let amount: usize = sa
321 .ir
322 .amount
323 .as_deref()
324 .and_then(|s| s.parse().ok())
325 .unwrap_or(1);
326
327 let is_exploit = sa.ir.exploit;
329 let exploit_source = if is_exploit { sa.source } else { None };
330
331 let optional = sa.ir.optional_present;
332 let is_strict = sa.ir.strict_amount;
333 let defined = sa.defined().map(|s| s.to_lowercase()).unwrap_or_default();
334
335 let sacrificing_players = get_target_players(ctx.game, sa);
336
337 let mut by_controller: BTreeMap<PlayerId, Vec<CardId>> = BTreeMap::new();
340 let mut record_sac = |player: PlayerId, sacrificed: Option<CardId>| {
341 if let Some(cid) = sacrificed {
342 by_controller.entry(player).or_default().push(cid);
343 }
344 };
345
346 for sacrificing_player in sacrificing_players {
347 if optional {
348 let source_name = sa.source.map(|cid| ctx.game.card(cid).card_name.as_str());
349 let accepted = ctx.agents[sacrificing_player.index()].confirm_action(
350 sacrificing_player,
351 None,
352 "Do you want to sacrifice?",
353 &[],
354 sa.source,
355 Some(crate::ability::api_type::ApiType::Sacrifice),
356 );
357 if !accepted {
358 continue;
359 }
360 }
361
362 if optional
366 && !sac_valid.eq_ignore_ascii_case("Self")
367 && defined.strip_prefix("carduid_").is_none()
368 {
369 let valid: Vec<_> = ctx
370 .game
371 .cards_in_zone(ZoneType::Battlefield, sacrificing_player)
372 .to_vec()
373 .into_iter()
374 .filter(|&cid| {
375 crate::ability::ability_utils::matches_valid_cards_for_sa(
376 ctx.game,
377 sa,
378 ctx.game.card(cid),
379 None,
380 &sac_valid,
381 )
382 })
383 .filter(|&cid| {
384 !crate::staticability::static_ability_cant_sacrifice::cant_sacrifice(
385 &ctx.game.cards,
386 ctx.game.card(cid),
387 Some(sa),
388 false,
389 )
390 })
391 .collect();
392
393 let min_targets = if is_strict { amount } else { 0 };
394 let chosen = if valid.is_empty() {
395 vec![]
396 } else {
397 ctx.agents[sacrificing_player.index()].choose_cards_for_effect(
398 sacrificing_player,
399 &valid,
400 min_targets,
401 amount,
402 )
403 };
404
405 for card_id in chosen {
406 let sacrificed = do_sacrifice(ctx, sa, card_id, sacrificing_player, exploit_source);
407 record_sac(sacrificing_player, sacrificed);
408 if sa.ir.remember_sacrificed {
409 if let Some(source_id) = sa.source {
410 ctx.game.card_mut(source_id).add_remembered_card(card_id);
411 }
412 }
413 }
414 continue;
415 }
416
417 for _ in 0..amount {
419 let card_to_sacrifice = if let Some(uid_str) = defined.strip_prefix("carduid_") {
420 uid_str
422 .parse::<u32>()
423 .ok()
424 .map(crate::ids::CardId)
425 .filter(|&cid| ctx.game.card(cid).zone == ZoneType::Battlefield)
426 } else if sac_valid.eq_ignore_ascii_case("Self") {
427 sa.source
429 .filter(|&cid| ctx.game.card(cid).zone == ZoneType::Battlefield)
430 } else {
431 let valid: Vec<_> = ctx
433 .game
434 .cards_in_zone(ZoneType::Battlefield, sacrificing_player)
435 .to_vec()
436 .into_iter()
437 .filter(|&cid| {
438 crate::ability::ability_utils::matches_valid_cards_for_sa(
439 ctx.game,
440 sa,
441 ctx.game.card(cid),
442 None,
443 &sac_valid,
444 )
445 })
446 .collect();
447
448 if valid.is_empty() {
449 None
450 } else if sa.ir.random {
451 let mut picked: Option<crate::ids::CardId> = None;
458 let mut i = 0i32;
459 for &cid in valid.iter() {
460 i += 1;
461 if i == 1 {
462 picked = Some(cid);
463 } else {
464 let j = ctx.rng.next_int(i);
465 if j < 1 {
466 picked = Some(cid);
467 }
468 }
469 }
470 picked
471 } else {
472 ctx.agents[sacrificing_player.index()].choose_sacrifice(
473 sacrificing_player,
474 &valid,
475 sa.source,
476 )
477 }
478 };
479
480 if let Some(card_id) = card_to_sacrifice {
481 let sacrificed = do_sacrifice(ctx, sa, card_id, sacrificing_player, exploit_source);
482 record_sac(sacrificing_player, sacrificed);
483 if sa.ir.remember_sacrificed {
486 if let Some(source_id) = sa.source {
487 ctx.game.card_mut(source_id).add_remembered_card(card_id);
488 }
489 }
490 }
491 }
492 }
493
494 crate::game_loop::fire_sacrificed_once_for_batch(ctx.game, ctx.trigger_handler, &by_controller);
497}