1use super::*;
2
3impl GameLoop {
4 pub(crate) fn run_turn_state_machine(
5 &mut self,
6 game: &mut GameState,
7 agents: &mut [Box<dyn PlayerAgent>],
8 ) {
9 let _perf_scope =
10 crate::perf::ParamsLookupScopeGuard::enter(crate::perf::ParamsLookupScope::Phase);
11 let active = game.active_player();
13 {
14 use crate::replacement::replacement_handler::{apply_replacements, ReplacementEvent};
15 use crate::replacement::ReplacementResult;
16 let mut event = ReplacementEvent::BeginTurn { player: active };
17 let result = apply_replacements(game, &mut event);
18 if result == ReplacementResult::Skipped || result == ReplacementResult::Replaced {
19 return;
20 }
21 }
22
23 let active = game.active_player();
25 crate::player::expire_until_your_next_turn_keywords(game, active);
26 self.trigger_handler
27 .handle_player_defined_del_triggers(active);
28 self.trigger_handler.run_trigger(
29 TriggerType::TurnBegin,
30 RunParams {
31 player: Some(active),
32 ..Default::default()
33 },
34 false,
35 );
36
37 let mut state = TurnMachineState::Untap;
38 while !game.game_over && state != TurnMachineState::Done {
39 if self.is_aborted() {
40 game.game_over = true;
41 break;
42 }
43 if game.end_turn_requested
45 && state != TurnMachineState::Cleanup
46 && state != TurnMachineState::Done
47 {
48 game.end_turn_requested = false;
49 self.combat.clear_with_cards(&mut game.cards);
50 state = TurnMachineState::Cleanup;
51 continue;
52 }
53 state = match state {
54 TurnMachineState::Untap => {
55 let active = game.active_player();
57 if game.player(active).skip_next_untap {
58 game.player_clear_skip_untap(active);
59 TurnMachineState::Upkeep
60 } else {
61 self.apply_turn_event(
62 game,
63 agents,
64 TurnEvent::EnterPhase {
65 phase: PhaseType::Untap,
66 emit_phase_trigger: false,
67 },
68 );
69 self.apply_turn_event(game, agents, TurnEvent::UntapStep);
70 TurnMachineState::Upkeep
71 }
72 }
73 TurnMachineState::Upkeep => {
74 self.apply_turn_event(
75 game,
76 agents,
77 TurnEvent::EnterPhase {
78 phase: PhaseType::Upkeep,
79 emit_phase_trigger: true,
80 },
81 );
82 self.apply_turn_event(
83 game,
84 agents,
85 TurnEvent::PriorityWindow {
86 is_main_phase: false,
87 },
88 );
89 self.clear_came_under_control_since_last_upkeep(game);
90 TurnMachineState::Draw
95 }
96 TurnMachineState::Draw => {
97 let active = game.active_player();
99 if game.player(active).skip_next_draw
100 || crate::replacement::replacement_handler::would_phase_be_skipped(
101 game,
102 active,
103 PhaseType::Draw,
104 )
105 {
106 game.player_clear_skip_draw(active);
107 TurnMachineState::Main1
108 } else {
109 let skip_draw_phase = game.turn.turn_number == 1 && game.players.len() == 2;
113 if skip_draw_phase {
114 self.apply_turn_event(
115 game,
116 agents,
117 TurnEvent::EnterPhase {
118 phase: PhaseType::Draw,
119 emit_phase_trigger: false,
120 },
121 );
122 TurnMachineState::Main1
123 } else {
124 self.apply_turn_event(
125 game,
126 agents,
127 TurnEvent::EnterPhase {
128 phase: PhaseType::Draw,
129 emit_phase_trigger: true,
130 },
131 );
132 self.apply_turn_event(game, agents, TurnEvent::DrawStep);
133 self.apply_turn_event(
134 game,
135 agents,
136 TurnEvent::PriorityWindow {
137 is_main_phase: false,
138 },
139 );
140 TurnMachineState::Main1
141 }
142 }
143 }
144 TurnMachineState::Main1 => {
145 self.apply_turn_event(
146 game,
147 agents,
148 TurnEvent::EnterPhase {
149 phase: PhaseType::Main1,
150 emit_phase_trigger: true,
151 },
152 );
153 if game
154 .cards_in_zone(ZoneType::Battlefield, game.active_player())
155 .iter()
156 .any(|&card_id| {
157 game.card(card_id)
158 .type_line
159 .subtypes
160 .iter()
161 .any(|s| s.eq_ignore_ascii_case("Attraction"))
162 })
163 {
164 crate::ability::effects::roll_dice_effect::roll_to_visit_attractions(
165 game,
166 &mut self.trigger_handler,
167 &mut *self.game_rng,
168 agents,
169 &mut self.mana_pools,
170 game.active_player(),
171 );
172 }
173 self.apply_turn_event(
174 game,
175 agents,
176 TurnEvent::PriorityWindow {
177 is_main_phase: true,
178 },
179 );
180 TurnMachineState::Combat
181 }
182 TurnMachineState::Combat => {
183 let active = game.active_player();
185 if game.player(active).skip_next_combat {
186 game.player_clear_skip_combat(active);
187 TurnMachineState::Main2
188 } else {
189 self.apply_turn_event(game, agents, TurnEvent::CombatStep);
190 if game.extra_combat_phases > 0 {
192 game.extra_combat_phases -= 1;
193 TurnMachineState::Combat } else {
195 TurnMachineState::Main2
196 }
197 }
198 }
199 TurnMachineState::Main2 => {
200 self.apply_turn_event(
201 game,
202 agents,
203 TurnEvent::EnterPhase {
204 phase: PhaseType::Main2,
205 emit_phase_trigger: true,
206 },
207 );
208 self.apply_turn_event(
209 game,
210 agents,
211 TurnEvent::PriorityWindow {
212 is_main_phase: true,
213 },
214 );
215 TurnMachineState::EndOfTurn
216 }
217 TurnMachineState::EndOfTurn => {
218 self.apply_turn_event(
219 game,
220 agents,
221 TurnEvent::EnterPhase {
222 phase: PhaseType::EndOfTurn,
223 emit_phase_trigger: true,
224 },
225 );
226 self.apply_turn_event(
227 game,
228 agents,
229 TurnEvent::PriorityWindow {
230 is_main_phase: false,
231 },
232 );
233 TurnMachineState::Cleanup
234 }
235 TurnMachineState::Cleanup => {
236 self.apply_turn_event(
237 game,
238 agents,
239 TurnEvent::EnterPhase {
240 phase: PhaseType::Cleanup,
241 emit_phase_trigger: false,
242 },
243 );
244 self.apply_turn_event(game, agents, TurnEvent::CleanupStep);
245 self.apply_turn_event(game, agents, TurnEvent::AdvanceTurn);
246 TurnMachineState::Done
247 }
248 TurnMachineState::Done => TurnMachineState::Done,
249 };
250 }
251 }
252
253 fn apply_turn_event(
254 &mut self,
255 game: &mut GameState,
256 agents: &mut [Box<dyn PlayerAgent>],
257 event: TurnEvent,
258 ) {
259 if game.game_over {
260 return;
261 }
262 match event {
263 TurnEvent::EnterPhase {
264 phase,
265 emit_phase_trigger,
266 } => {
267 game.stack.clear_undo_stack();
269
270 {
272 use crate::replacement::replacement_handler::{
273 apply_replacements, ReplacementEvent,
274 };
275 use crate::replacement::ReplacementResult;
276 let active = game.active_player();
277 let mut event = ReplacementEvent::BeginPhase {
278 player: active,
279 phase,
280 };
281 let result = apply_replacements(game, &mut event);
282 if result == ReplacementResult::Skipped || result == ReplacementResult::Replaced
283 {
284 return;
285 }
286 }
287
288 game.copy_last_state();
291 self.set_phase(game, agents, phase);
292 if emit_phase_trigger {
293 self.emit_phase_trigger(game, phase);
294 }
295 if phase == PhaseType::Upkeep {
298 self.process_suspend_upkeep(game, agents);
299 }
300 }
301 TurnEvent::PriorityWindow { is_main_phase } => {
302 self.step_with_priority(game, agents, is_main_phase);
303 }
304 TurnEvent::UntapStep => {
305 self.with_shared_state_mutation(game, agents, |this, game, agents| {
306 this.step_untap(game, agents);
307 });
308 }
309 TurnEvent::DrawStep => {
310 self.with_shared_state_mutation(game, agents, |this, game, agents| {
311 this.step_draw(game, agents);
312 });
313 }
314 TurnEvent::CombatStep => {
315 self.step_combat(game, agents);
316 }
317 TurnEvent::CleanupStep => {
318 self.with_shared_state_mutation(game, agents, |this, game, agents| {
319 this.step_cleanup(game, agents);
320 });
321 }
322 TurnEvent::AdvanceTurn => {
323 self.with_shared_state_mutation(game, agents, |_this, game, _agents| {
324 game.stack.on_next_turn();
326 game.reset_zone_turn_tracking();
328 game.reset_card_turn_tracking();
329 let player_order = game.player_order.clone();
330 if let Some((player, _skip)) =
331 game.turn.advance_turn(&mut game.extra_turns, &player_order)
332 {
333 game.player_set_skip_untap(player);
334 }
335 });
336 }
337 }
338 }
339
340 pub fn step_untap(&mut self, game: &mut GameState, agents: &mut [Box<dyn PlayerAgent>]) {
341 let active = game.active_player();
342 let cards_snapshot = game.cards.clone();
343
344 crate::phase::untap::do_phasing(game, active);
346
347 let cards: Vec<crate::ids::CardId> =
349 game.cards_in_zone(ZoneType::Battlefield, active).to_vec();
350 for cid in cards {
351 if game.card(cid).exerted {
352 game.card_mut(cid).clear_exerted();
353 } else {
354 let optional_keyword =
355 "You may choose not to untap CARDNAME during your untap step.";
356 let should_untap = if game.card(cid).has_keyword(optional_keyword) {
357 let question = format!("Untap {}?", game.card(cid).card_name);
358 let source_name = game.card(cid).card_name.clone();
359 agents[active.index()].choose_binary(
360 active,
361 &question,
362 crate::agent::BinaryChoiceKind::UntapOrLeaveTapped,
363 Some(true),
364 Some(cid),
365 None,
366 )
367 } else {
368 true
369 };
370 if should_untap {
371 game.untap_during_untap_step(cid, active);
372 }
373 }
374 }
375
376 let other_battlefield_cards: Vec<_> = game
380 .player_order
381 .iter()
382 .copied()
383 .filter(|&pid| pid != active)
384 .flat_map(|pid| {
385 game.cards_in_zone(ZoneType::Battlefield, pid)
386 .iter()
387 .copied()
388 })
389 .collect();
390 for cid in other_battlefield_cards {
391 let card = game.card(cid);
392 if !card.tapped {
393 continue;
394 }
395 if crate::staticability::static_ability_untap_other_player::untap(
396 &cards_snapshot,
397 card,
398 active,
399 ) {
400 game.untap_during_untap_step(cid, active);
401 }
402 }
403
404 self.pool_mut(active).reset_pool();
405 }
406
407 pub fn step_draw(&mut self, game: &mut GameState, agents: &mut [Box<dyn PlayerAgent>]) {
408 let active = game.active_player();
409 if game.turn.turn_number > 1 {
411 use crate::replacement::replacement_handler::{
412 apply_replacements_with_agents, ReplacementEvent,
413 };
414 use crate::replacement::ReplacementResult;
415
416 let mut draw_count = 1;
424 let mut draw_cards_event = ReplacementEvent::DrawCards {
425 player: active,
426 count: draw_count,
427 };
428 let result = apply_replacements_with_agents(game, agents, &mut draw_cards_event);
429 if result == ReplacementResult::Skipped || result == ReplacementResult::Replaced {
430 return;
431 }
432 if let ReplacementEvent::DrawCards { count, .. } = draw_cards_event {
433 draw_count = count;
434 }
435
436 for draw_index in 0..draw_count {
437 if let Some(card_id) =
438 game.player_draw_one_internal(active, draw_index == 0, Some(agents))
439 {
440 let drawn_snapshot = game.player(active).drawn_this_turn;
441 self.trigger_handler.run_trigger(
443 TriggerType::Drawn,
444 RunParams {
445 card: Some(card_id),
446 player: Some(active),
447 drawn_this_turn_snapshot: Some(drawn_snapshot),
448 ..Default::default()
449 },
450 false,
451 );
452 if self.trigger_handler.has_number_drawn_triggers(game) {
455 self.trigger_handler.flush_waiting_triggers(game);
456 }
457 }
458 }
459 }
460 }
461
462 pub fn step_with_priority(
463 &mut self,
464 game: &mut GameState,
465 agents: &mut [Box<dyn PlayerAgent>],
466 is_main_phase: bool,
467 ) {
468 let _perf_scope =
469 crate::perf::ParamsLookupScopeGuard::enter(crate::perf::ParamsLookupScope::Priority);
470 self.game_log.log(
471 GameLogEntryType::Info,
472 2,
473 format!(
474 "Priority window opened ({}, stack depth: {})",
475 if is_main_phase {
476 "main-phase speed"
477 } else {
478 "instant speed only"
479 },
480 game.stack.len()
481 ),
482 );
483 self.with_shared_state_mutation(game, agents, |_this, game, _agents| {
484 game.turn.priority_player = game.active_player();
485 });
486 loop {
487 if game.game_over {
488 return;
489 }
490
491 self.priority_round(game, agents, is_main_phase);
496
497 if std::env::var("FORGE_STACK_TRACE").is_ok() {
498 let names: Vec<String> = game
499 .stack
500 .iter()
501 .map(|entry| {
502 entry
503 .spell_ability
504 .source
505 .map(|cid| game.card(cid).card_name.clone())
506 .unwrap_or_else(|| "<effect>".to_string())
507 })
508 .collect();
509 eprintln!(
510 "[stack-trace] AFTER priority_round phase={:?} active={:?} priority={:?} depth={} {:?}",
511 game.turn.phase,
512 game.active_player(),
513 game.turn.priority_player,
514 game.stack.len(),
515 names
516 );
517 }
518
519 if game.game_over {
520 return;
521 }
522
523 if game.stack.is_empty() {
525 break;
526 }
527
528 self.with_shared_state_mutation(game, agents, |this, game, agents| {
530 this.resolve_stack(game, agents);
531 });
532 }
533 }
534 pub fn step_main_phase(&mut self, game: &mut GameState, agents: &mut [Box<dyn PlayerAgent>]) {
535 self.step_with_priority(game, agents, true);
536 }
537
538 pub fn step_cleanup(&mut self, game: &mut GameState, agents: &mut [Box<dyn PlayerAgent>]) {
539 let mut is_repeat = false;
544 loop {
545 if is_repeat {
548 self.notify_phase_changed(game, agents);
549 }
550
551 let active = game.active_player();
552
553 let hand_size = game.zone(ZoneType::Hand, active).len() as i32;
557 let max = game.player(active).max_hand_size;
558 let unlimited = game.player(active).unlimited_hand_size;
559 if !unlimited && hand_size > max {
560 let to_discard = (hand_size - max) as usize;
561 let hand: Vec<CardId> = game.cards_in_zone(ZoneType::Hand, active).to_vec();
562 agents[active.index()].snapshot_state(game, &self.mana_pools);
563 self.game_log.log(
564 GameLogEntryType::PriorityWaiting,
565 2,
566 format!(
567 "Waiting for {} discard decision (choose {})",
568 game.player(active).name,
569 to_discard
570 ),
571 );
572 let chosen = agents[active.index()].choose_discard(active, &hand, to_discard);
573 self.game_log.log(
574 GameLogEntryType::PriorityResponse,
575 2,
576 format!(
577 "{} selected {} card(s) to discard",
578 game.player(active).name,
579 chosen.len().min(to_discard)
580 ),
581 );
582 for card_id in chosen.iter().take(to_discard) {
583 if game.card(*card_id).zone == ZoneType::Hand {
584 game.discard_card(
585 *card_id,
586 active,
587 None,
588 Some(agents),
589 &mut self.trigger_handler,
590 );
591 }
592 }
593 }
594
595 self.cleanup_damage_and_eot(game);
599
600 self.trigger_handler.flush_waiting_triggers(game);
601 if game.stack.is_empty() && self.trigger_handler.pre_matched_trigger_count() == 0 {
602 break;
603 }
604 self.step_with_priority(game, agents, false);
605 is_repeat = true;
606 }
607
608 game.prevent_all_combat_damage = false;
612
613 game.end_turn_requested = false;
615 game.end_combat_requested = false;
616 game.extra_combat_phases = 0;
617 game.stack.reset_max_distinct_sources();
618
619 let active = game.active_player();
621 self.pool_mut(active).reset_pool();
622 self.trigger_handler.clear_this_turn_delayed_trigger();
623 }
624
625 fn cleanup_damage_and_eot(&mut self, game: &mut GameState) {
629 let temp_effect_ids: Vec<CardId> = game
632 .cards
633 .iter()
634 .filter(|c| c.zone == ZoneType::Command && c.temp_effect_until_eot)
635 .map(|c| c.id)
636 .collect();
637 for effect_id in temp_effect_ids {
638 if game.card(effect_id).zone == ZoneType::Command {
639 let controller = game.card(effect_id).controller;
640 game.remove_card_from_zone(ZoneType::Command, controller, effect_id);
641 game.card_mut(effect_id).set_zone(ZoneType::None);
642 }
643 }
644
645 let stolen: Vec<(CardId, crate::ids::PlayerId)> = game
647 .cards
648 .iter()
649 .filter(|c| c.zone == ZoneType::Battlefield && c.original_controller_eot.is_some())
650 .map(|c| (c.id, c.original_controller_eot.unwrap()))
651 .collect();
652 for (card_id, original) in stolen {
653 let current = game.card(card_id).controller;
654 game.card_mut(card_id).clear_original_controller_eot();
655 game.card_mut(card_id).clear_granted_keywords();
657 if current != original {
658 game.change_controller(card_id, original);
659 }
660 }
661
662 let player_ids = game.player_order.clone();
664 for player in player_ids {
665 game.player_cleanup_turn_state(player);
666 crate::player::expire_end_of_turn_keywords(game, player);
667 }
668
669 for i in 0..game.cards.len() {
670 let card_id = game.cards[i].id;
671 crate::ability::effects::change_text_effect::run(game, card_id);
672 }
673
674 for i in 0..game.cards.len() {
676 if game.cards[i].zone == ZoneType::Battlefield {
677 if let Some(state) = game.cards[i].animate_state.take() {
679 game.cards[i].restore_animate_snapshot(
680 state.original_type_line,
681 state.original_base_power,
682 state.original_base_toughness,
683 state.original_color,
684 );
685 game.cards[i].clear_damage();
686 }
687 if game.cards[i]
688 .clone_state
689 .as_ref()
690 .is_some_and(|state| state.expires_at_cleanup)
691 {
692 if let Some(state) = game.cards[i].clone_state.take() {
693 game.cards[i].restore_clone_snapshot(state);
694 }
695 }
696
697 game.cards[i].reset_turn_modifiers();
698 game.cards[i].cant_have_keywords.clear();
699 game.cards[i].clear_pump_keywords();
700 game.cards[i].clear_pump_triggers();
701 game.cards[i].clear_deathtouch_damage();
702 game.cards[i].reset_regeneration_shields();
703 game.cards[i].reset_shield_count();
704 game.cards[i].reset_crewed();
705
706 if game.cards[i].is_creature() {
707 let keep_damage =
708 crate::staticability::static_ability_no_cleanup_damage::damage_not_removed(
709 &game.cards,
710 &game.cards[i],
711 );
712 if !keep_damage {
713 game.cards[i].clear_damage();
714 }
715 game.cards[i].reset_turn_modifiers();
716 game.cards[i].clear_deathtouch_damage();
717 game.cards[i].reset_regeneration_shields();
718 game.cards[i].reset_shield_count();
719 game.cards[i].damage_history.new_turn();
720 }
721 game.cards[i].cant_have_keywords.clear();
729 game.cards[i].clear_pump_keywords();
730 game.cards[i].clear_pump_triggers();
731 } else {
732 game.cards[i].cant_have_keywords.clear();
733 game.cards[i].clear_pump_keywords();
734 game.cards[i].clear_pump_triggers();
735 }
736 }
737 }
738 pub(crate) fn emit_phase_trigger(&mut self, game: &mut GameState, phase: PhaseType) {
739 let active = game.active_player();
740 self.trigger_handler.run_trigger(
741 TriggerType::Phase,
742 RunParams {
743 phase: Some(phase),
744 player: Some(active),
745 ..Default::default()
746 },
747 false,
748 );
749 self.trigger_handler.run_trigger(
750 TriggerType::Always,
751 RunParams {
752 phase: Some(phase),
753 player: Some(active),
754 ..Default::default()
755 },
756 false,
757 );
758 }
759
760 fn clear_came_under_control_since_last_upkeep(&mut self, game: &mut GameState) {
761 let active = game.active_player();
762 let turn_number = game.turn.turn_number;
763 for card in &mut game.cards {
764 if card.controller == active
765 && card.zone == ZoneType::Battlefield
766 && card.turn_in_zone < turn_number
767 {
768 card.came_under_control_since_last_upkeep = false;
769 }
770 }
771 }
772
773 pub(crate) fn fire_combat_damage_triggers(&mut self, events: &[combat::CombatDamageEvent]) {
777 use crate::card::card_damage_map::{CardDamageMap, DamageTarget};
778 use crate::ids::{CardId, PlayerId};
779 use std::collections::HashMap;
780
781 for event in events {
783 self.trigger_handler.run_trigger(
784 TriggerType::DamageDone,
785 RunParams {
786 damage_source: Some(event.source),
787 damage_target_player: event.target_player,
788 damage_target_card: event.target_card,
789 damage_amount: Some(event.amount),
790 is_combat_damage: Some(event.is_combat),
791 ..Default::default()
792 },
793 false,
794 );
795 if let Some(player) = event.lifelink_player {
796 if event.lifelink_amount > 0 {
797 self.trigger_handler.run_trigger(
798 TriggerType::LifeGained,
799 RunParams {
800 player: Some(player),
801 life_amount: Some(event.lifelink_amount),
802 source_card: Some(event.source),
803 ..Default::default()
804 },
805 false,
806 );
807 }
808 }
809 }
810
811 #[derive(Hash, PartialEq, Eq, Clone, Copy)]
815 enum Target {
816 Card(CardId),
817 Player(PlayerId),
818 }
819 let mut by_target: HashMap<Target, i32> = HashMap::new();
820 let mut maps_by_target: HashMap<Target, CardDamageMap> = HashMap::new();
821 for event in events {
822 if event.amount <= 0 {
823 continue;
824 }
825 let target = if let Some(cid) = event.target_card {
826 Target::Card(cid)
827 } else if let Some(pid) = event.target_player {
828 Target::Player(pid)
829 } else {
830 continue;
831 };
832 *by_target.entry(target).or_insert(0) += event.amount;
833 let damage_target = match target {
834 Target::Card(cid) => DamageTarget::Card(cid),
835 Target::Player(pid) => DamageTarget::Player(pid),
836 };
837 maps_by_target.entry(target).or_default().put(
838 event.source,
839 damage_target,
840 event.amount,
841 );
842 }
843 for (target, total) in by_target {
844 if total <= 0 {
845 continue;
846 }
847 let params = match target {
848 Target::Card(cid) => RunParams {
849 damage_target_card: Some(cid),
850 damage_amount: Some(total),
851 damage_map: maps_by_target.get(&target).cloned(),
852 is_combat_damage: Some(true),
853 ..Default::default()
854 },
855 Target::Player(pid) => RunParams {
856 damage_target_player: Some(pid),
857 damage_amount: Some(total),
858 damage_map: maps_by_target.get(&target).cloned(),
859 is_combat_damage: Some(true),
860 ..Default::default()
861 },
862 };
863 self.trigger_handler
864 .run_trigger(TriggerType::DamageDoneOnce, params, false);
865 }
866 }
867
868 pub(crate) fn process_suspend_upkeep(
873 &mut self,
874 game: &mut GameState,
875 agents: &mut [Box<dyn PlayerAgent>],
876 ) {
877 let active = game.active_player();
878 let exile: Vec<CardId> = game.cards_in_zone(ZoneType::Exile, active).to_vec();
879 for card_id in exile {
880 let card = game.card(card_id);
881 if card.get_suspend_cost().is_none() {
883 continue;
884 }
885 let time_counters = *card
886 .counters
887 .get(&crate::card::CounterType::Time)
888 .unwrap_or(&0);
889 if time_counters <= 0 {
890 continue;
891 }
892 game.card_mut(card_id)
894 .remove_counter(&crate::card::CounterType::Time, 1);
895
896 self.trigger_handler.run_trigger(
898 TriggerType::CounterRemoved,
899 RunParams {
900 card: Some(card_id),
901 player: Some(active),
902 ..Default::default()
903 },
904 false,
905 );
906
907 let remaining = *game
908 .card(card_id)
909 .counters
910 .get(&crate::card::CounterType::Time)
911 .unwrap_or(&0);
912 if remaining <= 0 {
913 let card_name = game.card(card_id).card_name.clone();
915 let is_creature = game.card(card_id).is_creature();
916 let is_permanent = game.card(card_id).is_permanent();
917 game.player_record_spell_cast(active, card_id);
919
920 self.trigger_handler.run_trigger(
922 TriggerType::SpellCast,
923 RunParams {
924 spell_card: Some(card_id),
925 spell_controller: Some(active),
926 ..Default::default()
927 },
928 false,
929 );
930
931 let mut sa =
933 crate::spellability::build_spell_ability_for_card_cast(game, card_id, active);
934 sa.alt_cost = Some(crate::spellability::AlternativeCost::Suspend);
935 sa.setup_targets(game, agents, &self.mana_pools);
936
937 let entry = StackEntry {
938 id: 0,
939 spell_ability: sa,
940 is_pending_cast: false,
941 is_creature_spell: is_creature,
942 is_permanent_spell: is_permanent,
943 cast_from_zone: Some(ZoneType::Exile),
944 optional_trigger_decider: None,
945 optional_trigger_description: None,
946 optional_trigger_source_name: None,
947 };
948
949 game.stack.push(entry);
950 self.log_stack_push(&card_name, &game.player(active).name);
951 self.move_card_with_runtime(game, card_id, ZoneType::Stack, active, agents);
952 crate::agent::notify_all_agents(
953 agents,
954 crate::agent::GameLogEvent::stack(format!(
955 "Suspend: casting {} for free!",
956 card_name
957 ))
958 .with_player(active)
959 .with_card(card_id),
960 );
961
962 if is_creature && !game.card(card_id).has_haste() {
964 game.card_mut(card_id).granted_keywords.add("Haste");
965 }
966 }
967 }
968 }
969}