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 "{stack_item_name} fizzles (all targets invalid)"
142 ))
143 .with_player(entry.spell_ability.activating_player),
144 );
145 if !entry.spell_ability.is_trigger
149 && !entry.spell_ability.is_activated
150 && !entry.spell_ability.is_copy
151 {
152 if let Some(card_id) = entry.spell_ability.source {
153 let owner = game.card(card_id).owner;
154 let dest = if entry.spell_ability.alt_cost
155 == Some(crate::spellability::AlternativeCost::Harmonize)
156 {
157 apply_moved_replacement(game, card_id, ZoneType::Graveyard, Some(agents))
158 } else if entry.spell_ability.alt_cost
159 == Some(crate::spellability::AlternativeCost::Flashback)
160 || entry.spell_ability.alt_cost
161 == Some(crate::spellability::AlternativeCost::Escape)
162 {
163 ZoneType::Exile
164 } else {
165 apply_moved_replacement(game, card_id, ZoneType::Graveyard, Some(agents))
168 };
169 self.move_card_with_runtime(game, card_id, dest, owner, agents);
170 }
171 }
172 apply_continuous_effects(game);
173 return;
174 }
175
176 if entry.spell_ability.is_trigger || entry.spell_ability.is_activated {
177 if let Some(decider) = entry.optional_trigger_decider {
182 let mut description = entry
183 .optional_trigger_description
184 .clone()
185 .unwrap_or_default();
186 if let Some(triggered_card_id) = entry
187 .spell_ability
188 .trigger_objects
189 .get(&crate::ability::AbilityKey::Card)
190 .and_then(|s| s.parse::<u32>().ok())
191 .map(crate::ids::CardId)
192 {
193 let triggered_name = game.card(triggered_card_id).card_name.clone();
194 if !triggered_name.is_empty() && !description.contains(&triggered_name) {
195 if !description.is_empty() {
196 description.push(' ');
197 }
198 description.push_str(&format!("Triggered by {triggered_name}."));
199 }
200 }
201 let api = entry.spell_ability.api;
202 let accepted = agents[decider.index()].choose_optional_trigger(
203 decider,
204 &description,
205 entry.spell_ability.source,
206 api,
207 );
208 if !accepted {
209 apply_continuous_effects(game);
210 return;
211 }
212 }
213
214 let pay_trigger_cost_after_effect = (entry.spell_ability.is_trigger
215 || entry.spell_ability.trigger_source.is_some())
216 && Self::trigger_cost_after_effect(&entry.spell_ability);
217 if (entry.spell_ability.is_trigger || entry.spell_ability.trigger_source.is_some())
218 && !pay_trigger_cost_after_effect
219 {
220 if let Some(cost) = entry.spell_ability.pay_costs.clone() {
221 let player = entry.spell_ability.activating_player;
222 let source = entry.spell_ability.source.unwrap_or(CardId(0));
223 let api = entry.spell_ability.api;
224 let available = crate::mana::calculate_available_mana_excluding(
225 &self.mana_pools[player.index()],
226 game,
227 player,
228 Some(source),
229 );
230 if !crate::cost::can_pay_with_ability(
231 &cost,
232 game,
233 &available,
234 source,
235 player,
236 Some(&entry.spell_ability),
237 ) {
238 apply_continuous_effects(game);
239 return;
240 }
241 if !self.pay_ability_cost(
242 game,
243 agents,
244 player,
245 source,
246 &cost,
247 api,
248 cost.mandatory,
249 CostPaymentContext::TriggerResolve,
250 Some(&mut entry.spell_ability),
251 ) {
252 apply_continuous_effects(game);
253 return;
254 }
255 }
256 }
257
258 if let Some(source_id) = entry
260 .spell_ability
261 .trigger_source
262 .or(entry.spell_ability.source)
263 {
264 game.card_mut(source_id)
265 .add_ability_resolved_for(Some(&entry.spell_ability));
266 }
267 self.resolve_spell_effect(game, agents, &entry);
268 if pay_trigger_cost_after_effect {
269 if let Some(cost) = entry.spell_ability.pay_costs.clone() {
270 let player = entry.spell_ability.activating_player;
271 let source = entry.spell_ability.source.unwrap_or(CardId(0));
272 let api = entry.spell_ability.api;
273 let _ = self.pay_ability_cost(
274 game,
275 agents,
276 player,
277 source,
278 &cost,
279 api,
280 cost.mandatory,
281 CostPaymentContext::TriggerResolve,
282 Some(&mut entry.spell_ability),
283 );
284 }
285 }
286 crate::perf::increment(crate::perf::Metric::SpellAbilityClones, 3);
287 self.trigger_handler.run_trigger(
288 TriggerType::AbilityResolves,
289 RunParams {
290 card: entry.spell_ability.source,
291 spell_card: entry.spell_ability.source,
292 spell_controller: Some(entry.spell_ability.activating_player),
293 spell_ability: Some(entry.spell_ability.clone()),
294 source_sa: Some(entry.spell_ability.clone()),
295 cause: Some(entry.spell_ability.clone()),
296 cause_card: entry.spell_ability.source,
297 ..Default::default()
298 },
299 false,
300 );
301 if entry.spell_ability.is_activated {
304 let is_cycling = entry
305 .spell_ability
306 .ir
307 .precost_desc
308 .as_deref()
309 .is_some_and(|d| d.to_lowercase().contains("cycling"));
310 if is_cycling {
311 if let Some(source_card) = entry.spell_ability.source {
312 self.trigger_handler.run_trigger(
313 TriggerType::Cycled,
314 RunParams {
315 card: Some(source_card),
316 player: Some(entry.spell_ability.activating_player),
317 ..Default::default()
318 },
319 false,
320 );
321 }
322 }
323 }
324 } else if let Some(card_id) = entry.spell_ability.source {
325 let alt_cost = entry.spell_ability.alt_cost;
326 let player = entry.spell_ability.activating_player;
327
328 if entry.is_creature_spell || entry.is_permanent_spell {
329 let origin = game.card(card_id).zone;
331
332 if entry.spell_ability.kicked {
335 game.card_mut(card_id).set_kicked(true);
336 }
337
338 game.card_mut(card_id).cast_from = entry.cast_from_zone;
343
344 self.resolve_spell_effect(game, agents, &entry);
346 crate::perf::increment(crate::perf::Metric::SpellAbilityClones, 3);
347 self.trigger_handler.run_trigger(
348 TriggerType::AbilityResolves,
349 RunParams {
350 card: Some(card_id),
351 spell_card: Some(card_id),
352 spell_controller: Some(player),
353 spell_ability: Some(entry.spell_ability.clone()),
354 source_sa: Some(entry.spell_ability.clone()),
355 cause: Some(entry.spell_ability.clone()),
356 cause_card: Some(card_id),
357 ..Default::default()
358 },
359 false,
360 );
361
362 if origin != ZoneType::Battlefield {
363 self.move_card_with_runtime(
364 game,
365 card_id,
366 ZoneType::Battlefield,
367 player,
368 agents,
369 );
370 }
371
372 if game.card(card_id).type_line.has_subtype("Aura")
385 && game.card(card_id).attached_to.is_none()
386 && game.card(card_id).attached_to_player.is_none()
387 {
388 let enchant_type = game
389 .card(card_id)
390 .keywords
391 .iter_strings()
392 .find_map(|kw| crate::keyword::extract_keyword_cost_str(kw, "Enchant"))
393 .unwrap_or_default()
394 .to_string();
395 let normalized = enchant_type
396 .split_once(':')
397 .map(|(k, _)| k)
398 .unwrap_or(&enchant_type);
399 let can_target_player =
400 normalized.starts_with("Player") || normalized.starts_with("Opponent");
401
402 let mut candidates: Vec<crate::agent::types::GameEntity> = Vec::new();
403 if let Some(target_card) = entry.spell_ability.target_chosen.target_card {
404 if crate::parsing::enchant_type_matches_card(
408 &enchant_type,
409 game.card(target_card),
410 Some(game.card(card_id)),
411 ) && !crate::staticability::static_ability_cant_attach::cant_attach(
412 &game.cards,
413 game.card(card_id),
414 game.card(target_card),
415 false,
416 ) {
417 candidates.push(crate::agent::types::GameEntity::Card(target_card));
418 }
419 } else if let Some(target_player) =
420 entry.spell_ability.target_chosen.target_player
421 {
422 if Self::is_player_target_valid(target_player, game) {
423 candidates.push(crate::agent::types::GameEntity::Player(target_player));
424 }
425 } else if can_target_player {
426 for i in 0..game.players.len() {
427 let p = crate::ids::PlayerId(i as u32);
428 if Self::is_player_target_valid(p, game) {
429 candidates.push(crate::agent::types::GameEntity::Player(p));
430 }
431 }
432 } else {
433 let battlefield: Vec<CardId> =
434 game.cards_in_all_zones(ZoneType::Battlefield).collect();
435 for cid in battlefield {
436 if !crate::parsing::enchant_type_matches_card(
437 &enchant_type,
438 game.card(cid),
439 Some(game.card(card_id)),
440 ) {
441 continue;
442 }
443 if crate::staticability::static_ability_cant_attach::cant_attach(
444 &game.cards,
445 game.card(card_id),
446 game.card(cid),
447 false,
448 ) {
449 continue;
450 }
451 candidates.push(crate::agent::types::GameEntity::Card(cid));
452 }
453 }
454
455 if !candidates.is_empty() {
456 let chooser = entry.spell_ability.activating_player;
457 let chosen = agents[chooser.index()].choose_single_entity_for_effect(
458 chooser,
459 &candidates,
460 false,
461 );
462 let attached = matches!(
463 chosen,
464 Some(crate::agent::types::GameEntity::Card(_))
465 | Some(crate::agent::types::GameEntity::Player(_))
466 );
467 match chosen {
468 Some(crate::agent::types::GameEntity::Card(c)) => {
469 game.attach_to(card_id, c);
470 }
471 Some(crate::agent::types::GameEntity::Player(p)) => {
472 game.attach_to_player(card_id, p);
473 }
474 None => {}
475 }
476 if attached {
485 crate::staticability::layer::apply_continuous_effects(game);
486 }
487 }
488 }
489
490 if alt_cost == Some(crate::spellability::AlternativeCost::Evoke) {
501 let evoke_keyword_count =
502 (entry.spell_ability.evoke_keyword_count as usize).max(1);
503 self.register_evoke_sacrifice_triggers(
504 game,
505 card_id,
506 player,
507 evoke_keyword_count,
508 );
509 }
510
511 self.trigger_handler.register_active_trigger(game, card_id);
513
514 if game.card(card_id).type_line.has_subtype("Room")
515 && game.card(card_id).has_s_var("RoomRightSplitCost")
516 {
517 let card_state_name = entry
518 .spell_ability
519 .ir
520 .card_state_name
521 .clone()
522 .or_else(|| Some("LeftSplit".to_string()));
523 self.trigger_handler.run_trigger(
524 TriggerType::UnlockDoor,
525 RunParams {
526 card: Some(card_id),
527 player: Some(player),
528 card_state_name,
529 ..Default::default()
530 },
531 true,
532 );
533 }
534
535 crate::ability::effects::emit_zone_trigger(
537 &mut self.trigger_handler,
538 card_id,
539 origin,
540 ZoneType::Battlefield,
541 );
542
543 if alt_cost == Some(crate::spellability::AlternativeCost::Dash) {
547 game.card_mut(card_id).pump_keywords.add("Haste");
548 self.trigger_handler.register_delayed_trigger(
549 crate::trigger::handler::DelayedTrigger {
550 mode: TriggerType::Phase,
551 trigger_mode: Box::new(crate::trigger::trigger_phase::TriggerPhase {
552 phases: vec![forge_foundation::PhaseType::EndOfTurn],
553 valid_player: None,
554 }) as Box<dyn crate::trigger::TriggerBehavior>,
555 params: crate::parsing::Params::default(),
556 execute_svar: format!(
557 "DB$ ChangeZone | Origin$ Battlefield | Destination$ Hand | Defined$ CardUID_{}", card_id.0
558 ),
559 controller: player,
560 source_card: card_id,
561 created_turn: game.turn.turn_number,
562 created_phase: game.turn.phase,
563 target_card: Some(card_id),
564 remembered_amount: 0,
565 remembered_cards: Vec::new(),
566 remembered_players: Vec::new(),
567 remembered_lki_cards: Vec::new(),
568 sort_after_active: false,
569 trigger_order: None,
570 },
571 );
572 }
573
574 if alt_cost == Some(crate::spellability::AlternativeCost::Warp) {
578 game.card_mut(card_id)
579 .keywords
580 .add(crate::card::KEYWORD_WARP_EXILED);
581 self.trigger_handler.register_delayed_trigger(
582 crate::trigger::handler::DelayedTrigger {
583 mode: TriggerType::Phase,
584 trigger_mode: Box::new(crate::trigger::trigger_phase::TriggerPhase {
585 phases: vec![forge_foundation::PhaseType::EndOfTurn],
586 valid_player: None,
587 }) as Box<dyn crate::trigger::TriggerBehavior>,
588 params: crate::parsing::Params::default(),
589 execute_svar: format!(
590 "DB$ ChangeZone | Origin$ Battlefield | Destination$ Exile | Defined$ CardUID_{}", card_id.0
591 ),
592 controller: player,
593 source_card: card_id,
594 created_turn: game.turn.turn_number,
595 created_phase: game.turn.phase,
596 target_card: Some(card_id),
597 remembered_amount: 0,
598 remembered_cards: Vec::new(),
599 remembered_players: Vec::new(),
600 remembered_lki_cards: Vec::new(),
601 sort_after_active: false,
602 trigger_order: None,
603 },
604 );
605 }
606
607 if alt_cost == Some(crate::spellability::AlternativeCost::Bestow) {
609 if let Some(target) = entry.spell_ability.target_chosen.target_card {
610 game.card_mut(card_id).unanimate_bestow();
611 game.attach_to(card_id, target);
612 }
613 }
614
615 if alt_cost.is_some_and(|ac| ac.is_morph()) {
617 let is_mega = alt_cost == Some(crate::spellability::AlternativeCost::Megamorph);
618 let c = game.card_mut(card_id);
619 c.set_face_down(true);
620 c.static_set_power = Some(crate::spellability::MORPH_PT);
621 c.static_set_toughness = Some(crate::spellability::MORPH_PT);
622
623 let morph_cost = c
627 .get_keyword_cost(if is_mega { "Megamorph" } else { "Morph" })
628 .unwrap_or_else(|| "3".to_string());
629 let mega_param = if is_mega { " | Mega$ True" } else { "" };
630 let ab_text =
631 format!("AB$ SetState | Cost$ {morph_cost} | Mode$ TurnFaceUp{mega_param}");
632 let ab_index = c.activated_abilities.len();
633 if let Some(parsed) =
634 crate::ability::activated::parse_activated_ability(&ab_text, ab_index)
635 {
636 c.activated_abilities.push(parsed);
637 }
638 }
639
640 if alt_cost == Some(crate::spellability::AlternativeCost::Blitz) {
642 game.card_mut(card_id).pump_keywords.add("Haste");
643 let trig_id = game.card(card_id).triggers.len() as u32;
644 let params = crate::parsing::Params::from_raw(
645 "Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self"
646 );
647 let dies_trigger = crate::trigger::Trigger {
648 id: trig_id,
649 base: {
650 let mut base =
651 crate::game_loop::trigger_replacement_base::TriggerReplacementBase::default();
652 base.card_trait_base.set_id(trig_id as i32);
653 base.card_trait_base.set_intrinsic(false);
654 base.valid_host_zones = Some(vec![ZoneType::Battlefield]);
655 base
656 },
657 kind: crate::trigger::TriggerType::ChangesZone,
658 mode: Box::new(crate::trigger::trigger_changes_zone::TriggerChangesZone),
659 ir: crate::trigger::TriggerIr::from_params(¶ms),
660 execute: "BlitzDiesDraw".to_string(),
661 optional: false,
662 description: "When this creature dies, draw a card.".to_string(),
663 static_trigger: false,
664 trigger_remembered: Vec::new(),
665 spawning_ability: None,
666 };
667 game.card_mut(card_id).add_trigger(dies_trigger);
668 game.card_mut(card_id)
669 .set_s_var("BlitzDiesDraw", "DB$ Draw | NumCards$ 1 | Defined$ You");
670 self.trigger_handler.unregister_active_triggers(card_id);
671 self.trigger_handler.register_active_trigger(game, card_id);
672
673 self.trigger_handler.register_delayed_trigger(
674 crate::trigger::handler::DelayedTrigger {
675 mode: TriggerType::Phase,
676 trigger_mode: Box::new(crate::trigger::trigger_phase::TriggerPhase {
677 phases: vec![forge_foundation::PhaseType::EndOfTurn],
678 valid_player: None,
679 })
680 as Box<dyn crate::trigger::TriggerBehavior>,
681 params: crate::parsing::Params::default(),
682 execute_svar: format!("DB$ Sacrifice | Defined$ CardUID_{}", card_id.0),
683 controller: player,
684 source_card: card_id,
685 created_turn: game.turn.turn_number,
686 created_phase: game.turn.phase,
687 target_card: Some(card_id),
688 remembered_amount: 0,
689 remembered_cards: Vec::new(),
690 remembered_players: Vec::new(),
691 remembered_lki_cards: Vec::new(),
692 sort_after_active: false,
693 trigger_order: None,
694 },
695 );
696 }
697 } else {
698 self.resolve_spell_effect(game, agents, &entry);
700 crate::perf::increment(crate::perf::Metric::SpellAbilityClones, 3);
701 self.trigger_handler.run_trigger(
702 TriggerType::AbilityResolves,
703 RunParams {
704 card: Some(card_id),
705 spell_card: Some(card_id),
706 spell_controller: Some(player),
707 spell_ability: Some(entry.spell_ability.clone()),
708 source_sa: Some(entry.spell_ability.clone()),
709 cause: Some(entry.spell_ability.clone()),
710 cause_card: Some(card_id),
711 ..Default::default()
712 },
713 false,
714 );
715 let owner = game.card(card_id).owner;
716 if game.card(card_id).zone != ZoneType::Exile
718 && game.card(card_id).zone != ZoneType::Library
719 && game.card(card_id).zone != ZoneType::Hand
720 {
721 let dest = if alt_cost == Some(crate::spellability::AlternativeCost::Harmonize)
723 {
724 apply_moved_replacement(game, card_id, ZoneType::Graveyard, Some(agents))
725 } else if alt_cost == Some(crate::spellability::AlternativeCost::Flashback)
726 || alt_cost == Some(crate::spellability::AlternativeCost::Escape)
727 {
728 ZoneType::Exile
729 } else if entry.spell_ability.buyback_paid {
730 ZoneType::Hand
732 } else if game.card(card_id).has_rebound()
733 && entry.cast_from_zone == Some(ZoneType::Hand)
734 {
735 self.trigger_handler.register_delayed_trigger(
737 crate::trigger::handler::DelayedTrigger {
738 mode: TriggerType::Phase,
739 trigger_mode: Box::new(
740 crate::trigger::trigger_phase::TriggerPhase {
741 phases: vec![forge_foundation::PhaseType::Upkeep],
742 valid_player: Some(
743 crate::parsing::cached_compiled_selector("You"),
744 ),
745 },
746 )
747 as Box<dyn crate::trigger::TriggerBehavior>,
748 params: crate::parsing::Params::default(),
749 execute_svar: format!(
750 "DB$ Play | Defined$ CardUID_{} | WithoutManaCost$ True",
751 card_id.0
752 ),
753 controller: player,
754 source_card: card_id,
755 created_turn: game.turn.turn_number,
756 created_phase: game.turn.phase,
757 target_card: Some(card_id),
758 remembered_amount: 0,
759 remembered_cards: Vec::new(),
760 remembered_players: Vec::new(),
761 remembered_lki_cards: Vec::new(),
762 sort_after_active: false,
763 trigger_order: None,
764 },
765 );
766 ZoneType::Exile
767 } else {
768 apply_moved_replacement(game, card_id, ZoneType::Graveyard, Some(agents))
769 };
770 self.move_card_with_runtime(game, card_id, dest, owner, agents);
771 }
772 }
773 }
774
775 game.stack.finish_resolving();
777
778 apply_continuous_effects(game);
780
781 game.copy_last_state();
787
788 self.trigger_handler.flush_waiting_triggers(game);
790 }
791
792 pub(crate) fn resolve_spell_effect(
793 &mut self,
794 game: &mut GameState,
795 agents: &mut [Box<dyn PlayerAgent>],
796 entry: &StackEntry,
797 ) {
798 game.clear_pending_damage_maps();
803 game.clear_pending_change_zone_table();
804 game.ensure_pending_change_zone_table();
805
806 crate::ability::ability_utils::handle_remembering(game, &entry.spell_ability);
811
812 let mut parent_target_card: Option<CardId> = None;
816 let mut parent_target_player = None;
817 let mut parent_target_stack_entry: Option<u32> = None;
818 let mut inherited_trigger_index = entry.spell_ability.trigger_index;
819 let root_kicked = entry.spell_ability.kicked;
820 let mut current = Some(&entry.spell_ability);
821 let mut is_first = true;
822 while let Some(sa) = current {
823 if !is_first {
827 for agent in agents.iter_mut() {
828 agent.snapshot_state(game, &self.mana_pools);
829 }
830 }
831 is_first = false;
832
833 let mut sa_with_ctx;
835 let needs_ctx_clone = (root_kicked && !sa.kicked)
836 || (parent_target_card.is_some() && sa.target_chosen.target_card.is_none())
837 || (parent_target_player.is_some() && sa.target_chosen.target_player.is_none())
838 || (parent_target_stack_entry.is_some()
839 && sa.target_chosen.target_stack_entry.is_none())
840 || (inherited_trigger_index.is_some() && sa.trigger_index.is_none());
841 let sa_ref = if needs_ctx_clone {
842 sa_with_ctx = sa.clone();
843 if root_kicked && !sa_with_ctx.kicked {
844 sa_with_ctx.kicked = true;
845 }
846 if sa_with_ctx.target_chosen.target_card.is_none() {
847 sa_with_ctx.target_chosen.target_card = parent_target_card;
848 }
849 if sa_with_ctx.target_chosen.target_player.is_none() {
850 sa_with_ctx.target_chosen.target_player = parent_target_player;
851 }
852 if sa_with_ctx.target_chosen.target_stack_entry.is_none() {
853 sa_with_ctx.target_chosen.target_stack_entry = parent_target_stack_entry;
854 }
855 if sa_with_ctx.trigger_index.is_none() {
856 sa_with_ctx.trigger_index = inherited_trigger_index;
857 }
858 &sa_with_ctx
859 } else {
860 sa
861 };
862 self.resolve_single_effect(game, agents, sa_ref, parent_target_card);
863 parent_target_card = sa_ref.target_chosen.target_card;
864 parent_target_player = sa_ref.target_chosen.target_player;
865 parent_target_stack_entry = sa_ref.target_chosen.target_stack_entry;
866 inherited_trigger_index = sa_ref.trigger_index;
867 current = sa.get_sub_ability();
868 }
869
870 if let Some(table) = game.pending_change_zone_table.take() {
876 table.trigger_changes_zone_all(&mut self.trigger_handler, game, None);
877 }
878
879 game.clear_pending_damage_maps();
881 game.clear_pending_change_zone_table();
882 game.stack.clear_recently_removed();
883 }
884
885 fn has_fizzled(sa: &mut SpellAbility, game: &GameState) -> bool {
892 let result = Self::has_fizzled_inner(sa, game, None);
893 result.unwrap_or(false)
895 }
896
897 fn has_fizzled_inner(
903 sa: &mut SpellAbility,
904 game: &GameState,
905 mut fizzle: Option<bool>,
906 ) -> Option<bool> {
907 if sa.uses_targeting() {
908 let has_any_chosen = !sa.target_chosen.all_target_cards().is_empty()
912 || sa.target_chosen.target_player.is_some()
913 || sa.target_chosen.target_stack_entry.is_some();
914
915 if has_any_chosen {
916 if fizzle.is_none() {
919 fizzle = Some(true);
920 }
921
922 for target_card_id in sa.target_chosen.all_target_cards() {
926 if Self::is_card_target_valid(
927 sa,
928 target_card_id,
929 if sa.target_chosen.target_card == Some(target_card_id) {
930 sa.target_chosen.target_card_zone_timestamp
931 } else {
932 None
933 },
934 game,
935 ) {
936 fizzle = Some(false);
937 } else {
938 if sa.target_chosen.target_card == Some(target_card_id) {
939 sa.target_chosen.target_card = None;
940 sa.target_chosen.target_card_zone_timestamp = None;
941 }
942 sa.target_chosen.divided_map.remove(&target_card_id);
943 }
944 }
945
946 if let Some(target_player_id) = sa.target_chosen.target_player {
947 if Self::is_player_target_valid(target_player_id, game) {
948 fizzle = Some(false);
949 } else {
950 sa.target_chosen.target_player = None;
951 }
952 }
953
954 if let Some(target_stack_id) = sa.target_chosen.target_stack_entry {
955 if game.stack.find_by_id(target_stack_id).is_some() {
956 fizzle = Some(false);
957 } else {
958 sa.target_chosen.target_stack_entry = None;
959 }
960 }
961
962 if sa.ir.cant_fizzle {
964 fizzle = Some(false);
965 }
966 }
967 }
968
969 if let Some(sub) = sa.get_sub_ability_mut() {
973 fizzle = Self::has_fizzled_inner(sub, game, fizzle);
974 }
975
976 fizzle
977 }
978
979 fn is_card_target_valid(
985 sa: &SpellAbility,
986 target_card_id: CardId,
987 target_zone_timestamp: Option<u64>,
988 game: &GameState,
989 ) -> bool {
990 if target_card_id.index() >= game.cards.len() {
992 return false;
993 }
994
995 let card = game.card(target_card_id);
996
997 if let Some(chosen_ts) = target_zone_timestamp {
999 if card.zone_timestamp != chosen_ts {
1000 return false;
1001 }
1002 }
1003
1004 let expected_zone = if let Some(ref tr) = sa.target_restrictions {
1006 match &tr.target_kind {
1007 TargetKind::Creature(_) | TargetKind::Permanent(_) | TargetKind::Any => {
1008 Some(ZoneType::Battlefield)
1009 }
1010 TargetKind::CardInZone { zone, .. } => Some(*zone),
1011 _ => Some(ZoneType::Battlefield), }
1013 } else {
1014 Some(ZoneType::Battlefield) };
1016
1017 if let Some(zone) = expected_zone {
1019 if card.zone != zone {
1020 return false;
1021 }
1022 }
1023
1024 crate::spellability::target_restrictions::can_be_targeted_by_sa(
1027 game,
1028 target_card_id,
1029 sa.activating_player,
1030 sa,
1031 )
1032 }
1033
1034 fn is_player_target_valid(target_player_id: PlayerId, game: &GameState) -> bool {
1036 if target_player_id.index() >= game.players.len() {
1037 return false;
1038 }
1039 !game.player(target_player_id).has_lost
1040 }
1041
1042 pub(crate) fn resolve_single_effect(
1044 &mut self,
1045 game: &mut GameState,
1046 agents: &mut [Box<dyn PlayerAgent>],
1047 sa: &SpellAbility,
1048 parent_target_card: Option<CardId>,
1049 ) {
1050 let source_name = sa
1051 .source
1052 .and_then(|cid| game.cards.get(cid.index()).map(|c| c.card_name.clone()))
1053 .unwrap_or_else(|| "Unknown source".to_string());
1054 let effect_kind = Self::effect_kind_for_sa(sa);
1055 let mut event = crate::agent::GameLogEvent::stack(format!(
1056 "Effect resolved: {effect_kind} | source={source_name}"
1057 ))
1058 .with_player(sa.activating_player);
1059 if let Some(source_id) = sa.source {
1060 event = event.with_source_card(source_id);
1061 }
1062 if let Some(target_id) = sa.target_chosen.target_card {
1063 event = event.with_target_card(target_id);
1064 }
1065 crate::agent::notify_all_agents(agents, event);
1066
1067 let mut ctx = EffectContext {
1068 game,
1069 combat: Some(&mut self.combat),
1070 agents,
1071 trigger_handler: &mut self.trigger_handler,
1072 token_templates: &self.token_templates,
1073 token_art_variants: &self.token_art_variants,
1074 token_fallback: &self.token_fallback,
1075 edition_dates: &self.edition_dates,
1076 mana_pools: &mut self.mana_pools,
1077 parent_target_card,
1078 rng: &mut *self.game_rng,
1079 };
1080 effects::resolve_effect(&mut ctx, sa);
1081 }
1082
1083 fn register_evoke_sacrifice_triggers(
1085 &mut self,
1086 game: &GameState,
1087 card_id: CardId,
1088 player: PlayerId,
1089 evoke_keyword_count: usize,
1090 ) {
1091 for i in 0..evoke_keyword_count {
1092 self.trigger_handler.register_delayed_trigger(
1093 crate::trigger::handler::DelayedTrigger {
1094 mode: TriggerType::ChangesZone,
1095 trigger_mode: Box::new(crate::trigger::trigger_changes_zone::TriggerChangesZone)
1096 as Box<dyn crate::trigger::TriggerBehavior>,
1097 params: crate::parsing::Params::from_raw(
1098 "Mode$ ChangesZone | Destination$ Battlefield | ValidCard$ Card.Self",
1099 ),
1100 execute_svar: "DB$ Sacrifice".to_string(),
1101 controller: player,
1102 source_card: card_id,
1103 created_turn: game.turn.turn_number,
1104 created_phase: game.turn.phase,
1105 target_card: Some(card_id),
1106 remembered_amount: 0,
1107 remembered_cards: Vec::new(),
1108 remembered_players: Vec::new(),
1109 remembered_lki_cards: Vec::new(),
1110 sort_after_active: i > 0,
1111 trigger_order: None,
1112 },
1113 );
1114 }
1115 }
1116
1117 fn resolve_copied_permanent_as_token(
1118 &mut self,
1119 game: &mut GameState,
1120 agents: &mut [Box<dyn PlayerAgent>],
1121 entry: &StackEntry,
1122 ) {
1123 use crate::ability::effects::token_effect_base::{TokenCreateTable, TokenEffectBase};
1124 let Some(original_id) = entry.spell_ability.source else {
1125 return;
1126 };
1127 let player = entry.spell_ability.activating_player;
1128 let original = game.card(original_id).clone();
1129 let mut proto = crate::ability::effects::copy_permanent_effect::get_proto_type(
1130 &entry.spell_ability,
1131 &original,
1132 player,
1133 );
1134 proto.copied_permanent = Some(original_id);
1135 let mut token_table = TokenCreateTable::default();
1136 token_table.put(player, proto, 1);
1137 let mut trigger_list = crate::card::card_zone_table::CardZoneTable::default();
1138 let mut ctx = EffectContext {
1139 game,
1140 combat: Some(&mut self.combat),
1141 agents,
1142 trigger_handler: &mut self.trigger_handler,
1143 token_templates: &self.token_templates,
1144 token_art_variants: &self.token_art_variants,
1145 token_fallback: &self.token_fallback,
1146 edition_dates: &self.edition_dates,
1147 mana_pools: &mut self.mana_pools,
1148 parent_target_card: None,
1149 rng: &mut *self.game_rng,
1150 };
1151 let created = crate::ability::effects::token_effect_base::TOKEN_EFFECT_BASE
1152 .make_token_table(
1153 &mut ctx,
1154 token_table,
1155 true,
1156 &mut trigger_list,
1157 &entry.spell_ability,
1158 );
1159 if entry.spell_ability.alt_cost == Some(crate::spellability::AlternativeCost::Evoke) {
1160 let evoke_keyword_count = (entry.spell_ability.evoke_keyword_count as usize).max(1);
1161 for &token_id in &created.created {
1162 self.register_evoke_sacrifice_triggers(game, token_id, player, evoke_keyword_count);
1163 }
1164 }
1165 trigger_list.trigger_changes_zone_all(
1166 &mut self.trigger_handler,
1167 game,
1168 Some(&entry.spell_ability),
1169 );
1170 }
1171}