1use super::cost_payment::CostPaymentContext;
2use super::*;
3use crate::replacement::replacement_handler::apply_moved_replacement;
4use crate::spellability::TargetKind;
5
6impl GameLoop {
7 fn trigger_cost_after_effect(sa: &SpellAbility) -> bool {
8 if sa.api != Some(crate::ability::api_type::ApiType::Draw) {
9 return false;
10 }
11 let Some(cost) = sa.pay_costs.as_ref() else {
12 return false;
13 };
14 cost.parts.iter().any(|part| {
15 matches!(
16 part,
17 crate::cost::CostPart::Discard { type_filter, .. }
18 if type_filter == "Card"
19 )
20 }) && !cost.parts.iter().any(|part| {
21 matches!(
22 part,
23 crate::cost::CostPart::Discard { type_filter, .. }
24 if type_filter == "Hand"
25 ) || matches!(part, crate::cost::CostPart::Mana { .. })
26 })
27 }
28
29 fn effect_kind_for_sa(sa: &SpellAbility) -> String {
30 if let Some(api) = sa.api {
31 return api.name().to_string();
32 }
33 if sa.is_trigger {
34 if let Some(mode) = sa.ir.mode.as_ref() {
35 return format!("Trigger({mode})");
36 }
37 return "Trigger".to_string();
38 }
39 if sa.is_activated {
40 return "ActivatedAbility".to_string();
41 }
42 if sa.is_spell {
43 return "Spell".to_string();
44 }
45 "Effect".to_string()
46 }
47
48 pub fn resolve_stack(&mut self, game: &mut GameState, agents: &mut [Box<dyn PlayerAgent>]) {
49 let _perf_scope = crate::perf::ParamsLookupScopeGuard::enter(
50 crate::perf::ParamsLookupScope::StackResolution,
51 );
52 if game.stack.is_empty() {
53 return;
54 }
55
56 if std::env::var("FORGE_STACK_TRACE").is_ok() {
57 let names: Vec<String> = game
58 .stack
59 .iter()
60 .map(|entry| {
61 entry
62 .spell_ability
63 .source
64 .map(|cid| game.card(cid).card_name.clone())
65 .unwrap_or_else(|| "<effect>".to_string())
66 })
67 .collect();
68 eprintln!(
69 "[stack-trace] RESOLVE start phase={:?} active={:?} priority={:?} depth={} {:?}",
70 game.turn.phase,
71 game.active_player(),
72 game.turn.priority_player,
73 game.stack.len(),
74 names
75 );
76 }
77
78 game.copy_last_state();
81
82 let mut entry = game.stack.resolve_stack().unwrap();
83 let stack_item_name = entry
84 .spell_ability
85 .source
86 .and_then(|cid| game.cards.get(cid.index()).map(|c| c.card_name.clone()))
87 .unwrap_or_else(|| "Ability".to_string());
88 self.log_stack_resolved_item(&stack_item_name);
89 if std::env::var("FORGE_STACK_TRACE").is_ok() {
90 eprintln!(
91 "[stack-trace] POP resolving={} remaining_depth={}",
92 stack_item_name,
93 game.stack.len()
94 );
95 }
96
97 if entry.spell_ability.is_copy {
100 let should_create_token_copy = (entry.is_creature_spell || entry.is_permanent_spell)
101 && entry.spell_ability.source.is_some();
102 if should_create_token_copy {
103 self.resolve_copied_permanent_as_token(game, agents, &entry);
104 } else {
105 self.resolve_spell_effect(game, agents, &entry);
106 }
107 crate::perf::increment(crate::perf::Metric::SpellAbilityClones, 3);
108 self.trigger_handler.run_trigger(
109 TriggerType::AbilityResolves,
110 RunParams {
111 card: entry.spell_ability.source,
112 spell_card: entry.spell_ability.source,
113 spell_controller: Some(entry.spell_ability.activating_player),
114 spell_ability: Some(entry.spell_ability.clone()),
115 source_sa: Some(entry.spell_ability.clone()),
116 cause: Some(entry.spell_ability.clone()),
117 cause_card: entry.spell_ability.source,
118 ..Default::default()
119 },
120 false,
121 );
122 apply_continuous_effects(game);
123 return;
124 }
125
126 if std::env::var("FORGE_TRIGGER_TRACE").is_ok() && entry.optional_trigger_decider.is_some()
131 {
132 eprintln!(
133 "[trigger-trace] RESOLVING optional trigger from stack: {} api={:?}",
134 stack_item_name, entry.spell_ability.api
135 );
136 }
137 if Self::has_fizzled(&mut entry.spell_ability, game) {
138 crate::agent::notify_all_agents(
139 agents,
140 crate::agent::GameLogEvent::warning(format!(
141 "{} fizzles (all targets invalid)",
142 stack_item_name
143 ))
144 .with_player(entry.spell_ability.activating_player),
145 );
146 if !entry.spell_ability.is_trigger
150 && !entry.spell_ability.is_activated
151 && !entry.spell_ability.is_copy
152 {
153 if let Some(card_id) = entry.spell_ability.source {
154 let owner = game.card(card_id).owner;
155 let dest = if entry.spell_ability.alt_cost
156 == Some(crate::spellability::AlternativeCost::Harmonize)
157 {
158 apply_moved_replacement(game, card_id, ZoneType::Graveyard, Some(agents))
159 } else if entry.spell_ability.alt_cost
160 == Some(crate::spellability::AlternativeCost::Flashback)
161 || entry.spell_ability.alt_cost
162 == Some(crate::spellability::AlternativeCost::Escape)
163 {
164 ZoneType::Exile
165 } else {
166 apply_moved_replacement(game, card_id, ZoneType::Graveyard, Some(agents))
169 };
170 self.move_card_with_runtime(game, card_id, dest, owner, agents);
171 }
172 }
173 apply_continuous_effects(game);
174 return;
175 }
176
177 if entry.spell_ability.is_trigger || entry.spell_ability.is_activated {
178 if let Some(decider) = entry.optional_trigger_decider {
183 let mut description = entry
184 .optional_trigger_description
185 .clone()
186 .unwrap_or_default();
187 if let Some(triggered_card_id) = entry
188 .spell_ability
189 .trigger_objects
190 .get(&crate::ability::AbilityKey::Card)
191 .and_then(|s| s.parse::<u32>().ok())
192 .map(crate::ids::CardId)
193 {
194 let triggered_name = game.card(triggered_card_id).card_name.clone();
195 if !triggered_name.is_empty() && !description.contains(&triggered_name) {
196 if !description.is_empty() {
197 description.push(' ');
198 }
199 description.push_str(&format!("Triggered by {triggered_name}."));
200 }
201 }
202 let api = entry.spell_ability.api;
203 let accepted = agents[decider.index()].choose_optional_trigger(
204 decider,
205 &description,
206 entry.spell_ability.source,
207 api,
208 );
209 if !accepted {
210 apply_continuous_effects(game);
211 return;
212 }
213 }
214
215 let pay_trigger_cost_after_effect = (entry.spell_ability.is_trigger
216 || entry.spell_ability.trigger_source.is_some())
217 && Self::trigger_cost_after_effect(&entry.spell_ability);
218 if (entry.spell_ability.is_trigger || entry.spell_ability.trigger_source.is_some())
219 && !pay_trigger_cost_after_effect
220 {
221 if let Some(cost) = entry.spell_ability.pay_costs.clone() {
222 let player = entry.spell_ability.activating_player;
223 let source = entry.spell_ability.source.unwrap_or(CardId(0));
224 let api = entry.spell_ability.api;
225 let available = crate::mana::calculate_available_mana_excluding(
226 &self.mana_pools[player.index()],
227 game,
228 player,
229 Some(source),
230 );
231 if !crate::cost::can_pay_with_ability(
232 &cost,
233 game,
234 &available,
235 source,
236 player,
237 Some(&entry.spell_ability),
238 ) {
239 apply_continuous_effects(game);
240 return;
241 }
242 if !self.pay_ability_cost(
243 game,
244 agents,
245 player,
246 source,
247 &cost,
248 api,
249 cost.mandatory,
250 CostPaymentContext::TriggerResolve,
251 Some(&mut entry.spell_ability),
252 ) {
253 apply_continuous_effects(game);
254 return;
255 }
256 }
257 }
258
259 if let Some(source_id) = entry
261 .spell_ability
262 .trigger_source
263 .or(entry.spell_ability.source)
264 {
265 game.card_mut(source_id)
266 .add_ability_resolved_for(Some(&entry.spell_ability));
267 }
268 self.resolve_spell_effect(game, agents, &entry);
269 if pay_trigger_cost_after_effect {
270 if let Some(cost) = entry.spell_ability.pay_costs.clone() {
271 let player = entry.spell_ability.activating_player;
272 let source = entry.spell_ability.source.unwrap_or(CardId(0));
273 let api = entry.spell_ability.api;
274 let _ = self.pay_ability_cost(
275 game,
276 agents,
277 player,
278 source,
279 &cost,
280 api,
281 cost.mandatory,
282 CostPaymentContext::TriggerResolve,
283 Some(&mut entry.spell_ability),
284 );
285 }
286 }
287 crate::perf::increment(crate::perf::Metric::SpellAbilityClones, 3);
288 self.trigger_handler.run_trigger(
289 TriggerType::AbilityResolves,
290 RunParams {
291 card: entry.spell_ability.source,
292 spell_card: entry.spell_ability.source,
293 spell_controller: Some(entry.spell_ability.activating_player),
294 spell_ability: Some(entry.spell_ability.clone()),
295 source_sa: Some(entry.spell_ability.clone()),
296 cause: Some(entry.spell_ability.clone()),
297 cause_card: entry.spell_ability.source,
298 ..Default::default()
299 },
300 false,
301 );
302 if entry.spell_ability.is_activated {
305 let is_cycling = entry
306 .spell_ability
307 .ir
308 .precost_desc
309 .as_deref()
310 .is_some_and(|d| d.to_lowercase().contains("cycling"));
311 if is_cycling {
312 if let Some(source_card) = entry.spell_ability.source {
313 self.trigger_handler.run_trigger(
314 TriggerType::Cycled,
315 RunParams {
316 card: Some(source_card),
317 player: Some(entry.spell_ability.activating_player),
318 ..Default::default()
319 },
320 false,
321 );
322 }
323 }
324 }
325 } else if let Some(card_id) = entry.spell_ability.source {
326 let alt_cost = entry.spell_ability.alt_cost;
327 let player = entry.spell_ability.activating_player;
328
329 if entry.is_creature_spell || entry.is_permanent_spell {
330 let origin = game.card(card_id).zone;
332
333 if entry.spell_ability.kicked {
336 game.card_mut(card_id).set_kicked(true);
337 }
338
339 game.card_mut(card_id).cast_from = entry.cast_from_zone;
344
345 self.resolve_spell_effect(game, agents, &entry);
347 crate::perf::increment(crate::perf::Metric::SpellAbilityClones, 3);
348 self.trigger_handler.run_trigger(
349 TriggerType::AbilityResolves,
350 RunParams {
351 card: Some(card_id),
352 spell_card: Some(card_id),
353 spell_controller: Some(player),
354 spell_ability: Some(entry.spell_ability.clone()),
355 source_sa: Some(entry.spell_ability.clone()),
356 cause: Some(entry.spell_ability.clone()),
357 cause_card: Some(card_id),
358 ..Default::default()
359 },
360 false,
361 );
362
363 if origin != ZoneType::Battlefield {
364 self.move_card_with_runtime(
365 game,
366 card_id,
367 ZoneType::Battlefield,
368 player,
369 agents,
370 );
371 }
372
373 if game.card(card_id).type_line.has_subtype("Aura")
386 && game.card(card_id).attached_to.is_none()
387 && game.card(card_id).attached_to_player.is_none()
388 {
389 let enchant_type = game
390 .card(card_id)
391 .keywords
392 .iter_strings()
393 .find_map(|kw| crate::keyword::extract_keyword_cost_str(kw, "Enchant"))
394 .unwrap_or_default()
395 .to_string();
396 let normalized = enchant_type
397 .split_once(':')
398 .map(|(k, _)| k)
399 .unwrap_or(&enchant_type);
400 let can_target_player =
401 normalized.starts_with("Player") || normalized.starts_with("Opponent");
402
403 let mut candidates: Vec<crate::agent::types::GameEntity> = Vec::new();
404 if let Some(target_card) = entry.spell_ability.target_chosen.target_card {
405 if crate::parsing::enchant_type_matches_card(
409 &enchant_type,
410 game.card(target_card),
411 Some(game.card(card_id)),
412 ) && !crate::staticability::static_ability_cant_attach::cant_attach(
413 &game.cards,
414 game.card(card_id),
415 game.card(target_card),
416 false,
417 ) {
418 candidates.push(crate::agent::types::GameEntity::Card(target_card));
419 }
420 } else if let Some(target_player) =
421 entry.spell_ability.target_chosen.target_player
422 {
423 if Self::is_player_target_valid(target_player, game) {
424 candidates.push(crate::agent::types::GameEntity::Player(target_player));
425 }
426 } else if can_target_player {
427 for i in 0..game.players.len() {
428 let p = crate::ids::PlayerId(i as u32);
429 if Self::is_player_target_valid(p, game) {
430 candidates.push(crate::agent::types::GameEntity::Player(p));
431 }
432 }
433 } else {
434 let battlefield: Vec<CardId> =
435 game.cards_in_all_zones(ZoneType::Battlefield).collect();
436 for cid in battlefield {
437 if !crate::parsing::enchant_type_matches_card(
438 &enchant_type,
439 game.card(cid),
440 Some(game.card(card_id)),
441 ) {
442 continue;
443 }
444 if crate::staticability::static_ability_cant_attach::cant_attach(
445 &game.cards,
446 game.card(card_id),
447 game.card(cid),
448 false,
449 ) {
450 continue;
451 }
452 candidates.push(crate::agent::types::GameEntity::Card(cid));
453 }
454 }
455
456 if !candidates.is_empty() {
457 let chooser = entry.spell_ability.activating_player;
458 let chosen = agents[chooser.index()].choose_single_entity_for_effect(
459 chooser,
460 &candidates,
461 false,
462 );
463 let attached = matches!(
464 chosen,
465 Some(crate::agent::types::GameEntity::Card(_))
466 | Some(crate::agent::types::GameEntity::Player(_))
467 );
468 match chosen {
469 Some(crate::agent::types::GameEntity::Card(c)) => {
470 game.attach_to(card_id, c);
471 }
472 Some(crate::agent::types::GameEntity::Player(p)) => {
473 game.attach_to_player(card_id, p);
474 }
475 None => {}
476 }
477 if attached {
486 crate::staticability::layer::apply_continuous_effects(game);
487 }
488 }
489 }
490
491 if alt_cost == Some(crate::spellability::AlternativeCost::Evoke) {
502 let evoke_keyword_count =
503 (entry.spell_ability.evoke_keyword_count as usize).max(1);
504 self.register_evoke_sacrifice_triggers(
505 game,
506 card_id,
507 player,
508 evoke_keyword_count,
509 );
510 }
511
512 self.trigger_handler.register_active_trigger(game, card_id);
514
515 if game.card(card_id).type_line.has_subtype("Room")
516 && game.card(card_id).has_s_var("RoomRightSplitCost")
517 {
518 let card_state_name = entry
519 .spell_ability
520 .ir
521 .card_state_name
522 .clone()
523 .or_else(|| Some("LeftSplit".to_string()));
524 self.trigger_handler.run_trigger(
525 TriggerType::UnlockDoor,
526 RunParams {
527 card: Some(card_id),
528 player: Some(player),
529 card_state_name,
530 ..Default::default()
531 },
532 true,
533 );
534 }
535
536 crate::ability::effects::emit_zone_trigger(
538 &mut self.trigger_handler,
539 card_id,
540 origin,
541 ZoneType::Battlefield,
542 );
543
544 if alt_cost == Some(crate::spellability::AlternativeCost::Dash) {
548 game.card_mut(card_id).pump_keywords.add("Haste");
549 self.trigger_handler.register_delayed_trigger(
550 crate::trigger::handler::DelayedTrigger {
551 mode: TriggerType::Phase,
552 trigger_mode: Box::new(crate::trigger::trigger_phase::TriggerPhase {
553 phases: vec![forge_foundation::PhaseType::EndOfTurn],
554 valid_player: None,
555 }) as Box<dyn crate::trigger::TriggerBehavior>,
556 params: crate::parsing::Params::default(),
557 execute_svar: format!(
558 "DB$ ChangeZone | Origin$ Battlefield | Destination$ Hand | Defined$ CardUID_{}", card_id.0
559 ),
560 controller: player,
561 source_card: card_id,
562 created_turn: game.turn.turn_number,
563 created_phase: game.turn.phase,
564 target_card: Some(card_id),
565 remembered_amount: 0,
566 remembered_cards: Vec::new(),
567 remembered_players: Vec::new(),
568 remembered_lki_cards: Vec::new(),
569 sort_after_active: false,
570 trigger_order: None,
571 },
572 );
573 }
574
575 if alt_cost == Some(crate::spellability::AlternativeCost::Warp) {
579 game.card_mut(card_id)
580 .keywords
581 .add(crate::card::KEYWORD_WARP_EXILED);
582 self.trigger_handler.register_delayed_trigger(
583 crate::trigger::handler::DelayedTrigger {
584 mode: TriggerType::Phase,
585 trigger_mode: Box::new(crate::trigger::trigger_phase::TriggerPhase {
586 phases: vec![forge_foundation::PhaseType::EndOfTurn],
587 valid_player: None,
588 }) as Box<dyn crate::trigger::TriggerBehavior>,
589 params: crate::parsing::Params::default(),
590 execute_svar: format!(
591 "DB$ ChangeZone | Origin$ Battlefield | Destination$ Exile | Defined$ CardUID_{}", card_id.0
592 ),
593 controller: player,
594 source_card: card_id,
595 created_turn: game.turn.turn_number,
596 created_phase: game.turn.phase,
597 target_card: Some(card_id),
598 remembered_amount: 0,
599 remembered_cards: Vec::new(),
600 remembered_players: Vec::new(),
601 remembered_lki_cards: Vec::new(),
602 sort_after_active: false,
603 trigger_order: None,
604 },
605 );
606 }
607
608 if alt_cost == Some(crate::spellability::AlternativeCost::Bestow) {
610 if let Some(target) = entry.spell_ability.target_chosen.target_card {
611 game.card_mut(card_id).unanimate_bestow();
612 game.attach_to(card_id, target);
613 }
614 }
615
616 if alt_cost.is_some_and(|ac| ac.is_morph()) {
618 let is_mega = alt_cost == Some(crate::spellability::AlternativeCost::Megamorph);
619 let c = game.card_mut(card_id);
620 c.set_face_down(true);
621 c.static_set_power = Some(crate::spellability::MORPH_PT);
622 c.static_set_toughness = Some(crate::spellability::MORPH_PT);
623
624 let morph_cost = c
628 .get_keyword_cost(if is_mega { "Megamorph" } else { "Morph" })
629 .unwrap_or_else(|| "3".to_string());
630 let mega_param = if is_mega { " | Mega$ True" } else { "" };
631 let ab_text = format!(
632 "AB$ SetState | Cost$ {} | Mode$ TurnFaceUp{}",
633 morph_cost, mega_param
634 );
635 let ab_index = c.activated_abilities.len();
636 if let Some(parsed) =
637 crate::ability::activated::parse_activated_ability(&ab_text, ab_index)
638 {
639 c.activated_abilities.push(parsed);
640 }
641 }
642
643 if alt_cost == Some(crate::spellability::AlternativeCost::Blitz) {
645 game.card_mut(card_id).pump_keywords.add("Haste");
646 let trig_id = game.card(card_id).triggers.len() as u32;
647 let params = crate::parsing::Params::from_raw(
648 "Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self"
649 );
650 let dies_trigger = crate::trigger::Trigger {
651 id: trig_id,
652 base: {
653 let mut base =
654 crate::game_loop::trigger_replacement_base::TriggerReplacementBase::default();
655 base.card_trait_base.set_id(trig_id as i32);
656 base.card_trait_base.set_intrinsic(false);
657 base.valid_host_zones = Some(vec![ZoneType::Battlefield]);
658 base
659 },
660 kind: crate::trigger::TriggerType::ChangesZone,
661 mode: Box::new(crate::trigger::trigger_changes_zone::TriggerChangesZone),
662 ir: crate::trigger::TriggerIr::from_params(¶ms),
663 execute: "BlitzDiesDraw".to_string(),
664 optional: false,
665 description: "When this creature dies, draw a card.".to_string(),
666 static_trigger: false,
667 trigger_remembered: Vec::new(),
668 spawning_ability: None,
669 };
670 game.card_mut(card_id).add_trigger(dies_trigger);
671 game.card_mut(card_id)
672 .set_s_var("BlitzDiesDraw", "DB$ Draw | NumCards$ 1 | Defined$ You");
673 self.trigger_handler.unregister_active_triggers(card_id);
674 self.trigger_handler.register_active_trigger(game, card_id);
675
676 self.trigger_handler.register_delayed_trigger(
677 crate::trigger::handler::DelayedTrigger {
678 mode: TriggerType::Phase,
679 trigger_mode: Box::new(crate::trigger::trigger_phase::TriggerPhase {
680 phases: vec![forge_foundation::PhaseType::EndOfTurn],
681 valid_player: None,
682 })
683 as Box<dyn crate::trigger::TriggerBehavior>,
684 params: crate::parsing::Params::default(),
685 execute_svar: format!("DB$ Sacrifice | Defined$ CardUID_{}", card_id.0),
686 controller: player,
687 source_card: card_id,
688 created_turn: game.turn.turn_number,
689 created_phase: game.turn.phase,
690 target_card: Some(card_id),
691 remembered_amount: 0,
692 remembered_cards: Vec::new(),
693 remembered_players: Vec::new(),
694 remembered_lki_cards: Vec::new(),
695 sort_after_active: false,
696 trigger_order: None,
697 },
698 );
699 }
700 } else {
701 self.resolve_spell_effect(game, agents, &entry);
703 crate::perf::increment(crate::perf::Metric::SpellAbilityClones, 3);
704 self.trigger_handler.run_trigger(
705 TriggerType::AbilityResolves,
706 RunParams {
707 card: Some(card_id),
708 spell_card: Some(card_id),
709 spell_controller: Some(player),
710 spell_ability: Some(entry.spell_ability.clone()),
711 source_sa: Some(entry.spell_ability.clone()),
712 cause: Some(entry.spell_ability.clone()),
713 cause_card: Some(card_id),
714 ..Default::default()
715 },
716 false,
717 );
718 let owner = game.card(card_id).owner;
719 if game.card(card_id).zone != ZoneType::Exile
721 && game.card(card_id).zone != ZoneType::Library
722 && game.card(card_id).zone != ZoneType::Hand
723 {
724 let dest = if alt_cost == Some(crate::spellability::AlternativeCost::Harmonize)
726 {
727 apply_moved_replacement(game, card_id, ZoneType::Graveyard, Some(agents))
728 } else if alt_cost == Some(crate::spellability::AlternativeCost::Flashback)
729 || alt_cost == Some(crate::spellability::AlternativeCost::Escape)
730 {
731 ZoneType::Exile
732 } else if entry.spell_ability.buyback_paid {
733 ZoneType::Hand
735 } else if game.card(card_id).has_rebound()
736 && entry.cast_from_zone == Some(ZoneType::Hand)
737 {
738 self.trigger_handler.register_delayed_trigger(
740 crate::trigger::handler::DelayedTrigger {
741 mode: TriggerType::Phase,
742 trigger_mode: Box::new(
743 crate::trigger::trigger_phase::TriggerPhase {
744 phases: vec![forge_foundation::PhaseType::Upkeep],
745 valid_player: Some(
746 crate::parsing::cached_compiled_selector("You"),
747 ),
748 },
749 )
750 as Box<dyn crate::trigger::TriggerBehavior>,
751 params: crate::parsing::Params::default(),
752 execute_svar: format!(
753 "DB$ Play | Defined$ CardUID_{} | WithoutManaCost$ True",
754 card_id.0
755 ),
756 controller: player,
757 source_card: card_id,
758 created_turn: game.turn.turn_number,
759 created_phase: game.turn.phase,
760 target_card: Some(card_id),
761 remembered_amount: 0,
762 remembered_cards: Vec::new(),
763 remembered_players: Vec::new(),
764 remembered_lki_cards: Vec::new(),
765 sort_after_active: false,
766 trigger_order: None,
767 },
768 );
769 ZoneType::Exile
770 } else {
771 apply_moved_replacement(game, card_id, ZoneType::Graveyard, Some(agents))
772 };
773 self.move_card_with_runtime(game, card_id, dest, owner, agents);
774 }
775 }
776 }
777
778 game.stack.finish_resolving();
780
781 apply_continuous_effects(game);
783
784 game.copy_last_state();
790
791 self.trigger_handler.flush_waiting_triggers(game);
793 }
794
795 pub(crate) fn resolve_spell_effect(
796 &mut self,
797 game: &mut GameState,
798 agents: &mut [Box<dyn PlayerAgent>],
799 entry: &StackEntry,
800 ) {
801 game.clear_pending_damage_maps();
806 game.clear_pending_change_zone_table();
807 game.ensure_pending_change_zone_table();
808
809 crate::ability::ability_utils::handle_remembering(game, &entry.spell_ability);
814
815 let mut parent_target_card: Option<CardId> = None;
819 let mut parent_target_player = None;
820 let mut parent_target_stack_entry: Option<u32> = None;
821 let mut inherited_trigger_index = entry.spell_ability.trigger_index;
822 let root_kicked = entry.spell_ability.kicked;
823 let mut current = Some(&entry.spell_ability);
824 let mut is_first = true;
825 while let Some(sa) = current {
826 if !is_first {
830 for agent in agents.iter_mut() {
831 agent.snapshot_state(game, &self.mana_pools);
832 }
833 }
834 is_first = false;
835
836 let mut sa_with_ctx;
838 let needs_ctx_clone = (root_kicked && !sa.kicked)
839 || (parent_target_card.is_some() && sa.target_chosen.target_card.is_none())
840 || (parent_target_player.is_some() && sa.target_chosen.target_player.is_none())
841 || (parent_target_stack_entry.is_some()
842 && sa.target_chosen.target_stack_entry.is_none())
843 || (inherited_trigger_index.is_some() && sa.trigger_index.is_none());
844 let sa_ref = if needs_ctx_clone {
845 sa_with_ctx = sa.clone();
846 if root_kicked && !sa_with_ctx.kicked {
847 sa_with_ctx.kicked = true;
848 }
849 if sa_with_ctx.target_chosen.target_card.is_none() {
850 sa_with_ctx.target_chosen.target_card = parent_target_card;
851 }
852 if sa_with_ctx.target_chosen.target_player.is_none() {
853 sa_with_ctx.target_chosen.target_player = parent_target_player;
854 }
855 if sa_with_ctx.target_chosen.target_stack_entry.is_none() {
856 sa_with_ctx.target_chosen.target_stack_entry = parent_target_stack_entry;
857 }
858 if sa_with_ctx.trigger_index.is_none() {
859 sa_with_ctx.trigger_index = inherited_trigger_index;
860 }
861 &sa_with_ctx
862 } else {
863 sa
864 };
865 self.resolve_single_effect(game, agents, sa_ref, parent_target_card);
866 parent_target_card = sa_ref.target_chosen.target_card;
867 parent_target_player = sa_ref.target_chosen.target_player;
868 parent_target_stack_entry = sa_ref.target_chosen.target_stack_entry;
869 inherited_trigger_index = sa_ref.trigger_index;
870 current = sa.get_sub_ability();
871 }
872
873 if let Some(table) = game.pending_change_zone_table.take() {
879 table.trigger_changes_zone_all(&mut self.trigger_handler, game, None);
880 }
881
882 game.clear_pending_damage_maps();
884 game.clear_pending_change_zone_table();
885 game.stack.clear_recently_removed();
886 }
887
888 fn has_fizzled(sa: &mut SpellAbility, game: &GameState) -> bool {
895 let result = Self::has_fizzled_inner(sa, game, None);
896 result.unwrap_or(false)
898 }
899
900 fn has_fizzled_inner(
906 sa: &mut SpellAbility,
907 game: &GameState,
908 mut fizzle: Option<bool>,
909 ) -> Option<bool> {
910 if sa.uses_targeting() {
911 let has_any_chosen = !sa.target_chosen.all_target_cards().is_empty()
915 || sa.target_chosen.target_player.is_some()
916 || sa.target_chosen.target_stack_entry.is_some();
917
918 if has_any_chosen {
919 if fizzle.is_none() {
922 fizzle = Some(true);
923 }
924
925 for target_card_id in sa.target_chosen.all_target_cards() {
929 if Self::is_card_target_valid(
930 sa,
931 target_card_id,
932 if sa.target_chosen.target_card == Some(target_card_id) {
933 sa.target_chosen.target_card_zone_timestamp
934 } else {
935 None
936 },
937 game,
938 ) {
939 fizzle = Some(false);
940 } else {
941 if sa.target_chosen.target_card == Some(target_card_id) {
942 sa.target_chosen.target_card = None;
943 sa.target_chosen.target_card_zone_timestamp = None;
944 }
945 sa.target_chosen.divided_map.remove(&target_card_id);
946 }
947 }
948
949 if let Some(target_player_id) = sa.target_chosen.target_player {
950 if Self::is_player_target_valid(target_player_id, game) {
951 fizzle = Some(false);
952 } else {
953 sa.target_chosen.target_player = None;
954 }
955 }
956
957 if let Some(target_stack_id) = sa.target_chosen.target_stack_entry {
958 if game.stack.find_by_id(target_stack_id).is_some() {
959 fizzle = Some(false);
960 } else {
961 sa.target_chosen.target_stack_entry = None;
962 }
963 }
964
965 if sa.ir.cant_fizzle {
967 fizzle = Some(false);
968 }
969 }
970 }
971
972 if let Some(sub) = sa.get_sub_ability_mut() {
976 fizzle = Self::has_fizzled_inner(sub, game, fizzle);
977 }
978
979 fizzle
980 }
981
982 fn is_card_target_valid(
988 sa: &SpellAbility,
989 target_card_id: CardId,
990 target_zone_timestamp: Option<u64>,
991 game: &GameState,
992 ) -> bool {
993 if target_card_id.index() >= game.cards.len() {
995 return false;
996 }
997
998 let card = game.card(target_card_id);
999
1000 if let Some(chosen_ts) = target_zone_timestamp {
1002 if card.zone_timestamp != chosen_ts {
1003 return false;
1004 }
1005 }
1006
1007 let expected_zone = if let Some(ref tr) = sa.target_restrictions {
1009 match &tr.target_kind {
1010 TargetKind::Creature(_) | TargetKind::Permanent(_) | TargetKind::Any => {
1011 Some(ZoneType::Battlefield)
1012 }
1013 TargetKind::CardInZone { zone, .. } => Some(*zone),
1014 _ => Some(ZoneType::Battlefield), }
1016 } else {
1017 Some(ZoneType::Battlefield) };
1019
1020 if let Some(zone) = expected_zone {
1022 if card.zone != zone {
1023 return false;
1024 }
1025 }
1026
1027 crate::spellability::target_restrictions::can_be_targeted_by_sa(
1030 game,
1031 target_card_id,
1032 sa.activating_player,
1033 sa,
1034 )
1035 }
1036
1037 fn is_player_target_valid(target_player_id: PlayerId, game: &GameState) -> bool {
1039 if target_player_id.index() >= game.players.len() {
1040 return false;
1041 }
1042 !game.player(target_player_id).has_lost
1043 }
1044
1045 pub(crate) fn resolve_single_effect(
1047 &mut self,
1048 game: &mut GameState,
1049 agents: &mut [Box<dyn PlayerAgent>],
1050 sa: &SpellAbility,
1051 parent_target_card: Option<CardId>,
1052 ) {
1053 let source_name = sa
1054 .source
1055 .and_then(|cid| game.cards.get(cid.index()).map(|c| c.card_name.clone()))
1056 .unwrap_or_else(|| "Unknown source".to_string());
1057 let effect_kind = Self::effect_kind_for_sa(sa);
1058 let mut event = crate::agent::GameLogEvent::stack(format!(
1059 "Effect resolved: {} | source={}",
1060 effect_kind, source_name
1061 ))
1062 .with_player(sa.activating_player);
1063 if let Some(source_id) = sa.source {
1064 event = event.with_source_card(source_id);
1065 }
1066 if let Some(target_id) = sa.target_chosen.target_card {
1067 event = event.with_target_card(target_id);
1068 }
1069 crate::agent::notify_all_agents(agents, event);
1070
1071 let mut ctx = EffectContext {
1072 game,
1073 combat: Some(&mut self.combat),
1074 agents,
1075 trigger_handler: &mut self.trigger_handler,
1076 token_templates: &self.token_templates,
1077 token_art_variants: &self.token_art_variants,
1078 token_fallback: &self.token_fallback,
1079 edition_dates: &self.edition_dates,
1080 mana_pools: &mut self.mana_pools,
1081 parent_target_card,
1082 rng: &mut *self.game_rng,
1083 };
1084 effects::resolve_effect(&mut ctx, sa);
1085 }
1086
1087 fn register_evoke_sacrifice_triggers(
1089 &mut self,
1090 game: &GameState,
1091 card_id: CardId,
1092 player: PlayerId,
1093 evoke_keyword_count: usize,
1094 ) {
1095 for i in 0..evoke_keyword_count {
1096 self.trigger_handler.register_delayed_trigger(
1097 crate::trigger::handler::DelayedTrigger {
1098 mode: TriggerType::ChangesZone,
1099 trigger_mode: Box::new(crate::trigger::trigger_changes_zone::TriggerChangesZone)
1100 as Box<dyn crate::trigger::TriggerBehavior>,
1101 params: crate::parsing::Params::from_raw(
1102 "Mode$ ChangesZone | Destination$ Battlefield | ValidCard$ Card.Self",
1103 ),
1104 execute_svar: "DB$ Sacrifice".to_string(),
1105 controller: player,
1106 source_card: card_id,
1107 created_turn: game.turn.turn_number,
1108 created_phase: game.turn.phase,
1109 target_card: Some(card_id),
1110 remembered_amount: 0,
1111 remembered_cards: Vec::new(),
1112 remembered_players: Vec::new(),
1113 remembered_lki_cards: Vec::new(),
1114 sort_after_active: i > 0,
1115 trigger_order: None,
1116 },
1117 );
1118 }
1119 }
1120
1121 fn resolve_copied_permanent_as_token(
1122 &mut self,
1123 game: &mut GameState,
1124 agents: &mut [Box<dyn PlayerAgent>],
1125 entry: &StackEntry,
1126 ) {
1127 use crate::ability::effects::token_effect_base::{TokenCreateTable, TokenEffectBase};
1128 let Some(original_id) = entry.spell_ability.source else {
1129 return;
1130 };
1131 let player = entry.spell_ability.activating_player;
1132 let original = game.card(original_id).clone();
1133 let mut proto = crate::ability::effects::copy_permanent_effect::get_proto_type(
1134 &entry.spell_ability,
1135 &original,
1136 player,
1137 );
1138 proto.copied_permanent = Some(original_id);
1139 let mut token_table = TokenCreateTable::default();
1140 token_table.put(player, proto, 1);
1141 let mut trigger_list = crate::card::card_zone_table::CardZoneTable::default();
1142 let mut ctx = EffectContext {
1143 game,
1144 combat: Some(&mut self.combat),
1145 agents,
1146 trigger_handler: &mut self.trigger_handler,
1147 token_templates: &self.token_templates,
1148 token_art_variants: &self.token_art_variants,
1149 token_fallback: &self.token_fallback,
1150 edition_dates: &self.edition_dates,
1151 mana_pools: &mut self.mana_pools,
1152 parent_target_card: None,
1153 rng: &mut *self.game_rng,
1154 };
1155 let created = crate::ability::effects::token_effect_base::TOKEN_EFFECT_BASE
1156 .make_token_table(
1157 &mut ctx,
1158 token_table,
1159 true,
1160 &mut trigger_list,
1161 &entry.spell_ability,
1162 );
1163 if entry.spell_ability.alt_cost == Some(crate::spellability::AlternativeCost::Evoke) {
1164 let evoke_keyword_count = (entry.spell_ability.evoke_keyword_count as usize).max(1);
1165 for &token_id in &created.created {
1166 self.register_evoke_sacrifice_triggers(game, token_id, player, evoke_keyword_count);
1167 }
1168 }
1169 trigger_list.trigger_changes_zone_all(
1170 &mut self.trigger_handler,
1171 game,
1172 Some(&entry.spell_ability),
1173 );
1174 }
1175}