1use crate::ability::ability_ir::{DefinedExpr, DefinedRef};
9use crate::ability::api_type::ApiType;
10use crate::ability::AbilityKey;
11use crate::agent::PlayerAgent;
12use crate::game::GameState;
13use crate::ids::{CardId, PlayerId};
14use crate::parsing::keys;
15use crate::player::player_factory_util::add_replacement_effect;
16use crate::spellability::SpellAbility;
17use crate::spellability::{AbilityDuration, ReplaceDyingCondition};
18
19use super::ability_factory::AbilityRecordType;
20use super::ability_utils;
21use super::effects::EffectContext;
22
23pub trait SpellAbilityEffect {
33 fn resolve(ctx: &mut EffectContext, sa: &SpellAbility);
35
36 fn get_stack_description(sa: &SpellAbility) -> String {
39 sa.ability_text.clone()
40 }
41
42 fn build_spell_ability(_sa: &mut SpellAbility) {}
45
46 fn tokenize_string(game: &GameState, sa: &SpellAbility, desc: &str) -> String {
49 tokenize_string(game, sa, desc)
50 }
51
52 fn add_forget_on_moved_trigger(
55 game: &mut GameState,
56 host_id: CardId,
57 remembered_card_id: CardId,
58 ) {
59 add_forget_on_moved_trigger(game, host_id, remembered_card_id)
60 }
61
62 fn create_effect(game: &mut GameState, sa: &SpellAbility, name: &str, image: &str) -> CardId {
65 create_effect(game, sa, name, image)
66 }
67
68 fn run(ctx: &mut super::effects::EffectContext, sa: &SpellAbility) {
71 run(ctx, sa)
72 }
73
74 fn handle_exiled_with(game: &mut GameState, sa: &SpellAbility, exiled_card_id: CardId) {
77 handle_exiled_with(game, sa, exiled_card_id)
78 }
79
80 fn exile_effect_command(
83 game: &mut GameState,
84 trigger_handler: &mut crate::trigger::handler::TriggerHandler,
85 sa: &SpellAbility,
86 card_id: CardId,
87 ) {
88 exile_effect_command(game, trigger_handler, sa, card_id)
89 }
90
91 fn get_stack_description_with_subs(game: &GameState, sa: &SpellAbility) -> String {
94 let fallback = Self::get_stack_description(sa);
95 get_stack_description_with_subs(game, sa, &fallback)
96 }
97
98 fn get_new_chooser(
101 game: &GameState,
102 agents: &mut [Box<dyn PlayerAgent>],
103 sa: &SpellAbility,
104 loser: PlayerId,
105 ) -> Option<PlayerId> {
106 get_new_chooser(game, agents, sa, loser)
107 }
108}
109
110pub fn get_target_cards(game: &GameState, sa: &SpellAbility) -> Vec<CardId> {
118 get_cards(game, sa, false, "Defined")
119}
120
121pub fn get_defined_cards_or_targeted(game: &GameState, sa: &SpellAbility) -> Vec<CardId> {
125 get_cards(game, sa, true, "Defined")
126}
127
128pub fn get_defined_cards_or_targeted_param(
132 game: &GameState,
133 sa: &SpellAbility,
134 defined_param: &str,
135) -> Vec<CardId> {
136 get_cards(game, sa, true, defined_param)
137}
138
139fn get_cards(
142 game: &GameState,
143 sa: &SpellAbility,
144 defined_first: bool,
145 defined_param: &str,
146) -> Vec<CardId> {
147 let ir_defined = ir_defined_param(sa, defined_param);
148 let has_defined = ir_defined.is_some_and(|defined| defined.is_some());
149 let use_targets = sa.uses_targeting() && (!defined_first || !has_defined);
150
151 if use_targets {
152 sa.target_chosen.target_card.into_iter().collect()
154 } else {
155 let defined = ir_defined;
157 let mut result = Vec::new();
158 if let Some(Some(defined)) = defined {
159 for d in &defined.refs {
160 let cards = resolve_defined_cards_for_sa_ref(game, sa, d);
161 result.extend(cards);
162 }
163 } else {
164 result.extend(resolve_defined_cards_for_sa(game, sa, "Self"));
165 }
166 result
167 }
168}
169
170pub fn get_target_players(game: &GameState, sa: &SpellAbility) -> Vec<PlayerId> {
176 get_players(game, sa, false, "Defined")
177}
178
179pub fn get_defined_players_or_targeted(game: &GameState, sa: &SpellAbility) -> Vec<PlayerId> {
183 get_players(game, sa, true, "Defined")
184}
185
186fn get_players(
189 game: &GameState,
190 sa: &SpellAbility,
191 defined_first: bool,
192 defined_param: &str,
193) -> Vec<PlayerId> {
194 fn unique_push(players: &mut Vec<PlayerId>, player: PlayerId) {
195 if !players.contains(&player) {
196 players.push(player);
197 }
198 }
199
200 fn ordered_players(game: &GameState, starter: PlayerId) -> Vec<PlayerId> {
201 let Some(offset) = game.player_order.iter().position(|&pid| pid == starter) else {
202 return game.player_order.clone();
203 };
204
205 (0..game.player_order.len())
206 .map(|idx| game.player_order[(offset + idx) % game.player_order.len()])
207 .collect()
208 }
209
210 fn sort_in_turn_order(game: &GameState, sa: &SpellAbility, players: &mut [PlayerId]) {
211 let starter = sa
212 .ir
213 .starting_with
214 .as_deref()
215 .and_then(|defined| {
216 ability_utils::resolve_defined_players_with_sa(
217 defined,
218 sa,
219 sa.activating_player,
220 game,
221 )
222 .into_iter()
223 .next()
224 })
225 .unwrap_or(game.turn.active_player);
226 let ordered = ordered_players(game, starter);
227
228 players.sort_by_key(|pid| {
229 ordered
230 .iter()
231 .position(|ordered_pid| ordered_pid == pid)
232 .unwrap_or(usize::MAX)
233 });
234 }
235
236 let ir_defined = ir_defined_param(sa, defined_param);
237 let has_defined = ir_defined.is_some_and(|defined| defined.is_some());
238 let use_targets = sa.uses_targeting() && (!defined_first || !has_defined);
239
240 if use_targets {
241 let mut result = sa.target_chosen.all_target_players();
242 sort_in_turn_order(game, sa, &mut result);
243 result
244 } else {
245 let mut result = Vec::new();
246 if let Some(Some(defined)) = ir_defined {
247 for d in &defined.refs {
248 let players = ability_utils::resolve_defined_players_with_sa(
249 d.as_legacy_str(),
250 sa,
251 sa.activating_player,
252 game,
253 );
254 for player in players {
255 unique_push(&mut result, player);
256 }
257 }
258 } else {
259 for player in ability_utils::resolve_defined_players_with_sa(
260 "You",
261 sa,
262 sa.activating_player,
263 game,
264 ) {
265 unique_push(&mut result, player);
266 }
267 }
268 sort_in_turn_order(game, sa, &mut result);
269 result
270 }
271}
272
273fn ir_defined_param<'a>(
274 sa: &'a SpellAbility,
275 defined_param: &str,
276) -> Option<Option<&'a DefinedExpr>> {
277 if defined_param == keys::DEFINED {
278 Some(sa.ir.defined.as_ref())
279 } else if defined_param == keys::DEFINED_PLAYER {
280 Some(sa.ir.defined_player.as_ref())
281 } else {
282 None
283 }
284}
285
286fn resolve_defined_cards_for_sa_ref(
287 game: &GameState,
288 sa: &SpellAbility,
289 defined: &DefinedRef,
290) -> Vec<CardId> {
291 resolve_defined_cards_for_sa_ref_inner(game, sa, defined)
292}
293
294fn resolve_defined_cards_for_sa(game: &GameState, sa: &SpellAbility, defined: &str) -> Vec<CardId> {
298 let defined_ref = DefinedRef::parse(defined);
299 resolve_defined_cards_for_sa_ref_inner(game, sa, &defined_ref)
300}
301
302fn resolve_defined_cards_for_sa_ref_inner(
303 game: &GameState,
304 sa: &SpellAbility,
305 defined: &DefinedRef,
306) -> Vec<CardId> {
307 match defined {
308 DefinedRef::SelfCard => {
309 if sa.is_trigger {
310 if let (Some(source), Some(created_at)) =
311 (sa.trigger_source, sa.trigger_source_zone_timestamp)
312 {
313 let current = game.card(source);
314 if current.zone_timestamp != created_at {
315 return Vec::new();
316 }
317 }
318 }
319 sa.source.into_iter().collect()
320 }
321 DefinedRef::Targeted | DefinedRef::TargetedCard | DefinedRef::ThisTargetedCard => {
322 sa.target_chosen.target_card.into_iter().collect()
323 }
324 DefinedRef::TriggeredTargetLkiCopy => triggered_target_lki_cards(sa),
325 DefinedRef::TriggeredCard | DefinedRef::TriggeredCardLkiCopy => {
326 let cards = sa.get_triggering_cards(AbilityKey::Card);
327 if cards.is_empty() {
328 sa.trigger_source.into_iter().collect()
329 } else {
330 cards
331 }
332 }
333 DefinedRef::ReplacedCard | DefinedRef::ReplacedCardLki => {
334 let cards = sa.get_triggering_cards(AbilityKey::ReplacedCard);
335 if cards.is_empty() {
336 sa.get_triggering_cards(AbilityKey::Card)
337 } else {
338 cards
339 }
340 }
341 DefinedRef::TriggeredNewCard | DefinedRef::TriggeredNewCardLkiCopy => {
342 let cards = sa.get_triggering_cards(AbilityKey::NewCard);
343 if cards.is_empty() {
344 sa.trigger_source.into_iter().collect()
345 } else {
346 cards
347 }
348 }
349 DefinedRef::TriggeredAttackers => sa.get_triggering_cards(AbilityKey::Attackers),
350 DefinedRef::TriggeredAttacker => sa.get_triggering_cards(AbilityKey::Attacker),
351 DefinedRef::TriggeredBlocker => sa.get_triggering_cards(AbilityKey::Blocker),
352 DefinedRef::Explorer => sa.get_triggering_cards(AbilityKey::Explorer),
353 DefinedRef::Explored => sa.get_triggering_cards(AbilityKey::Explored),
354 DefinedRef::Discarded => sa.discarded_cost_cards.clone(),
358 DefinedRef::Sacrificed => game.last_sacrificed_card.into_iter().collect(),
359 _ => ability_utils::get_defined_cards(
360 game,
361 sa.source,
362 defined.as_legacy_str(),
363 Some(sa.activating_player),
364 ),
365 }
366}
367
368fn triggering_cards_from_key(sa: &SpellAbility, key: AbilityKey) -> Vec<CardId> {
369 let cards = sa.get_triggering_cards(key);
370 if !cards.is_empty() {
371 return cards;
372 }
373 sa.get_triggering_object(key)
374 .and_then(|raw| raw.parse::<u32>().ok())
375 .map(CardId)
376 .into_iter()
377 .collect()
378}
379
380fn triggered_target_lki_cards(sa: &SpellAbility) -> Vec<CardId> {
381 let target_cards = triggering_cards_from_key(sa, AbilityKey::TargetCard);
382 if !target_cards.is_empty() {
383 return target_cards;
384 }
385 if sa.get_triggering_player(AbilityKey::TargetPlayer).is_some() {
386 return Vec::new();
387 }
388 triggering_cards_from_key(sa, AbilityKey::Target)
389}
390
391pub fn tokenize_string(game: &GameState, sa: &SpellAbility, desc: &str) -> String {
397 let card_name = sa
398 .source
399 .map(|cid| game.card(cid).card_name.clone())
400 .unwrap_or_else(|| "CARDNAME".to_string());
401
402 let mut result = desc.to_string();
403 result = result.replace("CARDNAME", &card_name);
404 result = result.replace("NICKNAME", &card_name);
405
406 if let Some(num_dmg) = sa.ir.num_dmg_text.as_deref() {
408 result = result.replace("DAMAGE", num_dmg);
409 }
410
411 if let Some(amount) = sa.ir.amount.as_deref().or(sa.ir.num_cards_text.as_deref()) {
413 result = result.replace("AMOUNT", amount);
414 }
415
416 result
417}
418
419pub fn add_forget_on_moved_trigger(
423 game: &mut GameState,
424 host_id: CardId,
425 remembered_card_id: CardId,
426) {
427 game.card_mut(remembered_card_id)
432 .set_s_var("ForgetOnZoneChangeHost", host_id.0.to_string());
433}
434
435pub fn create_effect(game: &mut GameState, sa: &SpellAbility, name: &str, _image: &str) -> CardId {
442 use forge_foundation::{CardTypeLine, ColorSet, ManaCost};
443
444 let owner = sa.activating_player;
445 let source_name = sa
446 .source
447 .map(|cid| game.card(cid).card_name.clone())
448 .unwrap_or_default();
449
450 let effect_name = if name.is_empty() {
451 format!("{} Effect", source_name)
452 } else {
453 name.to_string()
454 };
455
456 let effect_card = crate::card::Card::new(
457 CardId(0), effect_name,
459 owner,
460 CardTypeLine::parse("Effect"),
461 ManaCost::parse(""),
462 ColorSet::COLORLESS,
463 None,
464 None,
465 vec![],
466 vec![],
467 );
468
469 let effect_id = game.create_card(effect_card);
470 game.move_card(effect_id, forge_foundation::ZoneType::Command, owner);
471
472 if let Some(source_id) = sa.source {
474 game.card_mut(effect_id).effect_source = Some(source_id);
475 let source_svars = game.card(source_id).svars.clone();
476 game.card_mut(effect_id).set_svars_map(source_svars);
477 }
478
479 game.card_mut(effect_id).set_s_var("IsEffectCard", "True");
481
482 effect_id
483}
484
485pub fn run(ctx: &mut super::effects::EffectContext, sa: &SpellAbility) {
490 super::effects::resolve_effect(ctx, sa);
491}
492
493pub fn handle_exiled_with(game: &mut GameState, sa: &SpellAbility, exiled_card_id: CardId) {
499 let source_id = match sa.source {
500 Some(id) => id,
501 None => return,
502 };
503
504 game.card_mut(exiled_card_id).set_exiled_by(Some(source_id));
505 game.card_mut(source_id).add_imprinted_card(exiled_card_id);
506}
507
508pub fn exile_effect_command(
514 game: &mut GameState,
515 trigger_handler: &mut crate::trigger::handler::TriggerHandler,
516 sa: &SpellAbility,
517 card_id: CardId,
518) {
519 let owner = game.card(card_id).owner;
520 let old_zone = game.card(card_id).zone;
521
522 game.move_card(card_id, forge_foundation::ZoneType::Exile, owner);
524
525 trigger_handler.register_active_trigger(game, card_id);
527 super::effects::zone_triggers::emit_zone_trigger(
528 trigger_handler,
529 card_id,
530 old_zone,
531 forge_foundation::ZoneType::Exile,
532 );
533
534 handle_exiled_with(game, sa, card_id);
536
537 if sa.ir.remember_exiled {
539 if let Some(source_id) = sa.source {
540 game.card_mut(source_id).add_remembered_card(card_id);
541 }
542 }
543}
544
545pub fn get_stack_description_with_subs(
552 game: &GameState,
553 sa: &SpellAbility,
554 stack_desc_fallback: &str,
555) -> String {
556 let mut sb = String::new();
557
558 let is_permanent_api = matches!(
559 sa.api,
560 Some(ApiType::PermanentCreature) | Some(ApiType::PermanentNoncreature)
561 );
562 let is_sub = matches!(sa.record_type, AbilityRecordType::SubAbility);
563
564 if !is_permanent_api {
565 if !is_sub {
566 if let Some(src_id) = sa.source {
567 sb.push_str(&game.card(src_id).card_name);
568 sb.push_str(" -");
569 }
570 }
571 sb.push(' ');
572 }
573
574 if let Some(raw_stack_desc) = sa.ir.stack_description_text.as_deref() {
576 let (stack_desc, reps): (&str, Option<Vec<(String, String)>>) =
577 if let Some(rest) = raw_stack_desc.strip_prefix("REP") {
578 let pairs = rest
579 .trim_start()
580 .split(" & ")
581 .filter_map(|s| {
582 s.split_once('_')
583 .map(|(a, b)| (a.to_string(), b.to_string()))
584 })
585 .collect();
586 ("SpellDescription", Some(pairs))
587 } else {
588 (raw_stack_desc, None)
589 };
590
591 if stack_desc.eq_ignore_ascii_case("SpellDescription") {
592 if let Some(raw_sdesc) = sa.ir.spell_description_text.as_deref() {
593 let mut spell_desc = raw_sdesc.replace(",,,,,,", " ").replace(",,,", " ");
594 if let (Some(l), Some(r)) = (spell_desc.find(" ("), spell_desc.find(')')) {
596 if r > l {
597 let reminder = spell_desc[l..=r].to_string();
598 spell_desc = spell_desc.replacen(&reminder, "", 1);
599 }
600 }
601 if let Some(replacements) = &reps {
602 for (from, to) in replacements {
603 if let Some(idx) = spell_desc.find(from) {
604 spell_desc.replace_range(idx..idx + from.len(), to);
605 }
606 }
607 sb.push_str(&tokenize_string(game, sa, &spell_desc));
608 } else {
609 sb.push_str(&spell_desc);
610 }
611 }
612 if reps.is_none() && has_any_target(sa) {
613 sb.push_str(" (Targeting: ");
614 sb.push_str(&join_targets(game, sa));
615 sb.push(')');
616 }
617 } else if !stack_desc.eq_ignore_ascii_case("None") {
618 sb.push_str(&tokenize_string(game, sa, stack_desc));
619 }
620 } else {
621 let cond_desc = sa.ir.condition_description_text.as_deref();
622 let after_desc = sa.ir.after_description_text.as_deref();
623 let base_desc = stack_desc_fallback.to_string();
624 if let Some(cd) = cond_desc {
625 sb.push_str(cd);
626 sb.push(' ');
627 if cd.ends_with(',') {
628 let mut chars = base_desc.chars();
630 if let Some(c) = chars.next() {
631 sb.extend(c.to_lowercase());
632 sb.push_str(chars.as_str());
633 }
634 } else {
635 sb.push_str(&base_desc);
636 }
637 } else {
638 sb.push_str(&base_desc);
639 }
640 if let Some(ad) = after_desc {
641 sb.push(' ');
642 sb.push_str(ad);
643 }
644 }
645
646 if !is_permanent_api {
649 if let Some(sub) = sa.sub_ability.as_deref() {
650 let sub_fallback = sub.description.clone();
651 sb.push_str(&get_stack_description_with_subs(game, sub, &sub_fallback));
652 }
653 }
654
655 if let Some(svar) = sa.ir.announce_text.as_deref() {
657 let amount = calculate_amount_for_sa(game, sa, svar);
658 sb.push_str(&format!(" ({svar}={amount})"));
659 } else if sa.cost_has_mana_x() {
660 sb.push_str(&format!(" (X={})", sa.x_mana_cost_paid));
661 }
662
663 if let Some(src_id) = sa.source {
666 let name = game.card(src_id).card_name.clone();
667 sb = sb.replace("CARDNAME", &name);
668 sb = sb.replace("NICKNAME", &name);
669 }
670
671 sb
672}
673
674fn has_any_target(sa: &SpellAbility) -> bool {
675 !sa.target_chosen.all_target_cards().is_empty()
676 || !sa.target_chosen.all_target_players().is_empty()
677}
678
679fn join_targets(game: &GameState, sa: &SpellAbility) -> String {
680 let mut names = Vec::new();
681 for cid in sa.target_chosen.all_target_cards() {
682 names.push(game.card(cid).card_name.clone());
683 }
684 for pid in sa.target_chosen.all_target_players() {
685 names.push(format!("P{}", pid.index()));
686 }
687 names.join(", ")
688}
689
690fn calculate_amount_for_sa(game: &GameState, sa: &SpellAbility, svar: &str) -> i32 {
691 if let Ok(n) = svar.parse::<i32>() {
692 return n;
693 }
694 let Some(src) = sa.source else {
695 return 0;
696 };
697 if sa.ir.semantic_numeric_params.contains_key(svar) {
698 return crate::svar::resolve_numeric_svar(game, sa, svar, 0);
699 }
700 let Some(expr) = game.card(src).get_s_var(svar) else {
701 return 0;
702 };
703 crate::svar::resolve_count_svar_for_sa(expr, game, src, sa.activating_player, sa)
704}
705
706pub fn get_new_chooser(
711 game: &GameState,
712 agents: &mut [Box<dyn PlayerAgent>],
713 sa: &SpellAbility,
714 loser: PlayerId,
715) -> Option<PlayerId> {
716 let activator = sa.activating_player;
717 let loser_is_opponent = loser != activator;
718 let options: Vec<PlayerId> = game
719 .alive_players()
720 .into_iter()
721 .filter(|&pid| pid != activator)
722 .filter(|&pid| {
723 if loser_is_opponent {
724 pid != activator
726 } else {
727 true
729 }
730 })
731 .collect();
732
733 if options.is_empty() {
734 return None;
735 }
736 agents[activator.index()].choose_target_player(activator, &options, Some(sa))
737}
738
739#[derive(Debug, Clone, Copy, PartialEq, Eq, strum_macros::EnumString, Default)]
742#[strum(ascii_case_insensitive)]
743pub enum AtEotAction {
744 #[default]
746 Sacrifice,
747 SacrificeCtrl,
749 Destroy,
750 Exile,
751 Hand,
752 Library,
753}
754
755impl AtEotAction {
756 pub fn execute_svar(self) -> &'static str {
758 match self {
759 AtEotAction::Sacrifice => {
760 "DB$ SacrificeAll | Defined$ DelayTriggerRememberedLKI | Controller$ You"
761 }
762 AtEotAction::SacrificeCtrl => "DB$ SacrificeAll | Defined$ DelayTriggerRememberedLKI",
763 AtEotAction::Exile => {
764 "DB$ ChangeZone | Defined$ DelayTriggerRememberedLKI | Origin$ Battlefield | \
765 Destination$ Exile"
766 }
767 AtEotAction::Hand => {
768 "DB$ ChangeZone | Defined$ DelayTriggerRememberedLKI | Origin$ Battlefield | \
769 Destination$ Hand"
770 }
771 AtEotAction::Library => {
772 "DB$ ChangeZone | Defined$ DelayTriggerRememberedLKI | Origin$ Battlefield | \
773 Destination$ Library | Shuffle$ True"
774 }
775 AtEotAction::Destroy => "DB$ Destroy | Defined$ DelayTriggerRememberedLKI",
776 }
777 }
778}
779
780pub fn register_at_eot(
787 trigger_handler: &mut crate::trigger::handler::TriggerHandler,
788 game: &crate::game::GameState,
789 sa: &SpellAbility,
790 action: &str,
791 remembered: Vec<CardId>,
792) {
793 if remembered.is_empty() {
794 return;
795 }
796 let action = action.parse::<AtEotAction>().unwrap_or_default();
797 let execute_svar = action.execute_svar().to_string();
798 trigger_handler.register_delayed_trigger(crate::trigger::handler::DelayedTrigger {
799 mode: crate::trigger::TriggerType::Phase,
800 trigger_mode: Box::new(crate::trigger::trigger_phase::TriggerPhase {
801 phases: vec![forge_foundation::PhaseType::EndOfTurn],
802 valid_player: None,
803 }) as Box<dyn crate::trigger::TriggerBehavior>,
804 params: crate::parsing::Params::default(),
805 execute_svar,
806 controller: sa.activating_player,
807 source_card: sa.source.unwrap_or(remembered[0]),
808 created_turn: game.turn.turn_number,
809 created_phase: game.turn.phase,
810 target_card: None,
811 remembered_amount: 0,
812 remembered_cards: remembered.clone(),
813 remembered_players: Vec::new(),
814 remembered_lki_cards: remembered,
815 sort_after_active: false,
816 trigger_order: None,
817 });
818}
819
820pub fn add_self_trigger_at_eot(
821 trigger_handler: &mut crate::trigger::handler::TriggerHandler,
822 game: &mut crate::game::GameState,
823 location: &str,
824 card_id: CardId,
825) {
826 let mut player = "";
827 let mut action = location;
828 let mut whose = " the ";
829 if let Some((prefix, suffix)) = location.split_once('_') {
830 player = prefix;
831 action = suffix;
832 if player.eq_ignore_ascii_case("You") {
833 whose = " your next ";
834 }
835 }
836
837 let mut trigger_raw = format!(
838 "Mode$ Phase | Phase$ End of Turn | TriggerZones$ Battlefield | TriggerDescription$ At the beginning of{}end step, {} CARDNAME.",
839 whose,
840 action.to_ascii_lowercase()
841 );
842 if !player.is_empty() {
843 trigger_raw.push_str(" | Player$ ");
844 trigger_raw.push_str(player);
845 }
846
847 let Some(mut trigger) = trigger_handler.parse_trigger(&trigger_raw) else {
848 return;
849 };
850
851 let effect = match action {
852 "Sacrifice" => "DB$ Sacrifice | SacValid$ Self",
853 "Exile" => "DB$ ChangeZone | Origin$ Battlefield | Destination$ Exile | Defined$ Self",
854 _ => "",
855 };
856 if !effect.is_empty() {
857 trigger.execute = "EndOfTurnLeavePlay".to_string();
858 game.card_mut(card_id)
859 .set_s_var("EndOfTurnLeavePlay", effect);
860 }
861 game.card_mut(card_id).add_trigger(trigger);
862}
863
864pub fn check_valid_duration(
871 game: &GameState,
872 sa: &SpellAbility,
873 duration: Option<&AbilityDuration>,
874) -> bool {
875 let Some(duration) = duration else {
876 return true;
877 };
878 let Some(host_id) = sa.source else {
879 return true;
880 };
881 let host = game.card(host_id);
882 let in_play_or_stack = matches!(
883 host.zone,
884 forge_foundation::ZoneType::Battlefield | forge_foundation::ZoneType::Stack
885 );
886
887 if duration.needs_host_in_play_or_stack() && !in_play_or_stack {
888 return false;
889 }
890 if duration.needs_host_not_phased_out() && host.phased_out {
891 return false;
892 }
893 if duration.needs_host_control() && host.controller != sa.activating_player {
894 return false;
895 }
896 if duration.needs_host_tapped() && !host.tapped {
897 return false;
898 }
899 if duration.needs_targeted_card_tapped() {
900 if let Some(tgt_id) = sa.target_chosen.target_card {
901 let tgt = game.card(tgt_id);
902 if !tgt.tapped || tgt.phased_out {
903 return false;
904 }
905 }
906 }
907 true
908}
909
910pub fn replace_dying(game: &mut GameState, sa: &SpellAbility) -> Vec<CardId> {
915 if sa.ir.replace_dying_defined.is_none() && sa.ir.replace_dying_valid.is_none() {
916 return Vec::new();
917 }
918
919 if let Some(cond) = sa.ir.replace_dying_condition.as_ref() {
921 if matches!(cond, ReplaceDyingCondition::Kicked) && !sa.kicked {
922 return Vec::new();
923 }
924 }
925
926 let cards = if let Some(defined) = sa.ir.replace_dying_defined_text.as_deref() {
927 let cards = resolve_defined_cards_for_sa(game, sa, defined);
928 if cards.is_empty() {
929 return Vec::new();
930 }
931 cards
932 } else {
933 Vec::new()
934 };
935
936 let effect_name = sa
937 .source
938 .map(|source_id| format!("{}'s Effect", game.card(source_id).card_name))
939 .unwrap_or_else(|| "Effect".to_string());
940 let effect_id = create_effect(game, sa, &effect_name, "");
941 {
942 let effect = game.card_mut(effect_id);
943 effect.add_remembered_cards(cards.iter().copied());
944 effect.set_forget_on_moved_origin(Some(forge_foundation::ZoneType::Battlefield));
945 effect.set_exile_when_no_remembered(true);
946 effect.set_temp_effect_until_eot(true);
947 }
948
949 let valid = sa
950 .ir
951 .replace_dying_valid
952 .as_deref()
953 .unwrap_or("Card.IsRemembered");
954 let zone = sa.ir.replace_dying_zone_text.as_deref().unwrap_or("Exile");
955 let mut replacement_raw = format!(
956 "R$ Event$ Moved | ValidLKI$ {} | Origin$ Battlefield | Destination$ Graveyard | NewDestination$ {} | Description$ If that permanent would die this turn, exile it instead.",
957 valid, zone
958 );
959 if sa.ir.replace_dying_exiled_with {
960 replacement_raw.push_str(" | ExiledWithEffectSource$ True");
961 }
962
963 let effect = game.card_mut(effect_id);
964 add_replacement_effect(effect, &replacement_raw);
965
966 cards
967}