1use forge_foundation::{PhaseType, ZoneType};
15use serde::{Deserialize, Serialize};
16
17use crate::ability::ability_factory::build_spell_ability;
18use crate::ability::AbilityKey;
19use crate::card::valid_filter::CardTraitRequirementsIr;
20use crate::card::Card;
21use crate::card_trait_base::{CardTrait, CardTraitBase, CardTraitIrOwner};
22use crate::core::HasSVars;
23use crate::game::GameState;
24use crate::game_loop::trigger_replacement_base::TriggerReplacementBase;
25use crate::ids::{CardId, PlayerId};
26use crate::parsing::{keys, CompiledSelector, Params};
27pub use crate::player::GameLossReason;
28
29use super::replacement_handler::ReplacementEvent;
30use crate::spellability::SpellAbility;
31
32pub use super::replacement_layer::ReplacementLayer;
35pub use super::replacement_type::ReplacementType;
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ReplacementEffect {
44 #[serde(skip, default)]
57 pub base: Box<TriggerReplacementBase>,
58 pub event: ReplacementType,
60 pub layer: ReplacementLayer,
62 pub ir: ReplacementEffectIr,
64 pub active_zones: Vec<ZoneType>,
68 #[serde(default)]
70 pub suppressed: bool,
71}
72
73#[derive(Debug, Clone, Default, Serialize, Deserialize)]
74pub struct ReplacementEffectIr {
75 #[serde(skip, default)]
76 pub card_trait_requirements: CardTraitRequirementsIr,
77 pub replace_with: Option<String>,
78 pub description_text: Option<String>,
79 pub player_turn_text: Option<String>,
80 pub new_destination_text: Option<String>,
81 pub new_destination_zone: Option<ZoneType>,
82 pub origin_text: Option<String>,
83 pub origin_zone: Option<ZoneType>,
84 pub destination_text: Option<String>,
85 pub destination_zone: Option<ZoneType>,
86 pub exclude_destination_text: Option<String>,
87 pub valid_card_text: Option<String>,
88 pub valid_card_selector: Option<CompiledSelector>,
89 pub valid_player_text: Option<String>,
90 pub valid_player_selector: Option<CompiledSelector>,
91 pub valid_target_selector: Option<CompiledSelector>,
92 pub valid_source_selector: Option<CompiledSelector>,
93 pub valid_activator_text: Option<String>,
94 pub valid_step_turn_to_controller_text: Option<String>,
95 pub valid_explorer_text: Option<String>,
96 pub valid_counter_type_text: Option<String>,
97 pub valid_lose_reason_text: Option<String>,
98 pub valid_sides_text: Option<String>,
99 pub active_phases: Vec<PhaseType>,
100 pub amount_text: Option<String>,
101 pub damage_amount_text: Option<String>,
102 pub result_text: Option<String>,
103 pub number_text: Option<String>,
104 pub dredge_amount: Option<usize>,
105 pub skip: bool,
106 pub prevent: bool,
107 pub optional: bool,
108 pub effect_only: bool,
109 pub discard: Option<bool>,
110 pub flashback_cast: Option<bool>,
111 pub harmonize_cast: Option<bool>,
112 pub not_first_card_in_draw_step: bool,
113 pub exiled_with_effect_source: bool,
114 pub is_combat: Option<bool>,
115 pub is_damage: Option<bool>,
116 pub max_speed: Option<bool>,
117 pub replacement_result: Option<String>,
118 pub optional_decider_text: Option<String>,
119 pub replace_mana_text: Option<String>,
120 pub replace_type_text: Option<String>,
121 pub replace_color_text: Option<String>,
122 pub replace_only_text: Option<String>,
123 pub replace_amount_text: Option<String>,
124 pub valid_lki_text: Option<String>,
125 pub counter_map: bool,
126 pub replace_with_chain: Option<ReplacementChainIr>,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub enum ReplacementChainIr {
131 ReplaceToken {
132 token_type: Option<String>,
133 amount_expr: Option<String>,
134 },
135 ReplaceCounter {
136 amount_expr: String,
137 },
138 ReplaceEffect {
139 var_name: Option<String>,
140 var_type: Option<String>,
141 var_key: Option<String>,
142 var_value: Option<String>,
143 sub_ability: Option<Box<ReplacementChainIr>>,
144 },
145}
146
147impl CardTrait for ReplacementEffect {
148 fn base(&self) -> &CardTraitBase {
149 &self.base.card_trait_base
150 }
151}
152
153impl CardTraitIrOwner for ReplacementEffect {
154 type Ir = ReplacementEffectIr;
155
156 fn ir(&self) -> &Self::Ir {
157 &self.ir
158 }
159
160 fn card_trait_requirements(&self) -> &CardTraitRequirementsIr {
161 &self.ir.card_trait_requirements
162 }
163}
164
165impl ReplacementEffect {
166 pub fn set_host_card(&mut self, host: &Card) {
179 self.ir.replace_with_chain = self
180 .replace_with()
181 .and_then(|svar_name| host.get_svar(svar_name))
182 .and_then(|raw| parse_replacement_chain(raw, host));
183 self.base.set_host_card_id(host.id);
184 }
185}
186
187impl ReplacementEffect {
188 pub fn new(
189 event: ReplacementType,
190 layer: ReplacementLayer,
191 params: Params,
192 active_zones: Vec<ZoneType>,
193 ) -> Self {
194 let mut effect = Self {
195 base: Box::new(TriggerReplacementBase::default()),
196 event,
197 layer,
198 ir: ReplacementEffectIr::from_params(¶ms),
199 active_zones,
200 suppressed: false,
201 };
202 effect.sync_trait_base_params(¶ms);
203 effect
204 }
205
206 pub fn replace_with(&self) -> Option<&str> {
207 self.ir.replace_with.as_deref()
208 }
209
210 pub fn has_skip(&self) -> bool {
211 self.ir.skip
212 }
213
214 pub fn prevents(&self) -> bool {
215 self.ir.prevent
216 }
217
218 pub fn matches_phase(&self, phase: PhaseType) -> bool {
219 self.ir.active_phases.is_empty() || self.ir.active_phases.contains(&phase)
220 }
221
222 fn sync_trait_base_params(&mut self, params: &Params) {
223 let map = params
224 .iter()
225 .map(|(k, v)| (k.to_string(), v.to_string()))
226 .collect();
227 self.base.card_trait_base.set_map_params(map);
228 }
229
230 pub fn active_in_zone(&self, zone: ZoneType) -> bool {
235 !self.suppressed && (self.active_zones.is_empty() || self.active_zones.contains(&zone))
236 }
237
238 pub fn description(&self, host: &Card, game: &GameState) -> String {
254 if self.suppressed || self.base.card_trait_base.is_suppressed() {
255 return String::new();
256 }
257 let Some(raw) = self.ir.description_text.as_deref() else {
258 return String::new();
259 };
260 let mut desc =
261 crate::ability::ability_utils::apply_description_text_change_effects(raw, host);
262 desc = desc.replace("CARDNAME", &host.card_name);
263 desc = desc.replace("NICKNAME", &host.card_name);
266 if desc.contains("EFFECTSOURCE") {
267 let source_name = host
268 .effect_source
269 .map(|id| game.card(id).card_name.clone())
270 .unwrap_or_else(|| host.card_name.clone());
271 desc = desc.replace("EFFECTSOURCE", &source_name);
272 }
273
274 if self.event == ReplacementType::DamageDone {
276 if let Some(rep_sa) = self.base.get_overriding_ability() {
277 desc = append_shield_remaining(desc, rep_sa, host);
278 }
279 }
280
281 desc
282 }
283
284 pub fn has_run(&self) -> bool {
291 false
292 }
293
294 pub fn requirements_check(&self, game: &GameState, source: &Card) -> bool {
296 if self.suppressed || self.base.card_trait_base.is_suppressed() {
297 return false;
298 }
299
300 if let Some(pt) = self.ir.player_turn_text.as_deref() {
301 if pt == "True" {
302 if game.active_player() != source.controller {
303 return false;
304 }
305 } else {
306 let players = crate::ability::ability_utils::resolve_defined_players(
307 pt,
308 source.controller,
309 game,
310 );
311 if !players.contains(&game.active_player()) {
312 return false;
313 }
314 }
315 }
316
317 if !self.matches_phase(game.turn.phase) {
319 return false;
320 }
321
322 self.meets_card_trait_requirements(game, source, source)
323 }
324
325 pub fn copy(&self) -> Self {
330 self.clone()
331 }
332
333 pub fn ensure_ability(
345 &self,
346 game: &GameState,
347 host_card: CardId,
348 activating_player: PlayerId,
349 ) -> Option<SpellAbility> {
350 if let Some(overriding) = self.base.get_overriding_ability() {
351 return Some(overriding.clone());
352 }
353 let svar_name = self.replace_with()?;
354 let host = game.card(host_card);
355 let script = host.get_svar(svar_name)?;
356 Some(build_spell_ability(
357 game,
358 host_card,
359 script,
360 activating_player,
361 ))
362 }
363
364 pub fn ensure_ability_mut(
369 &mut self,
370 game: &GameState,
371 host_card: CardId,
372 activating_player: PlayerId,
373 ) -> Option<&mut SpellAbility> {
374 if self.base.get_overriding_ability().is_none() {
375 let ability = self.ensure_ability(game, host_card, activating_player)?;
376 self.base.overriding_ability = Some(ability);
379 }
380 self.base.overriding_ability.as_mut()
381 }
382
383 pub fn can_replace_etb(&self, source: &Card, affected: &Card) -> bool {
397 let targets_self_only = self
398 .ir
399 .valid_card_text
400 .as_deref()
401 .map(|v| v.starts_with("Card.Self"))
402 .unwrap_or(false);
403 if !targets_self_only && source.id == affected.id {
404 return false;
405 }
406 true
407 }
408
409 pub fn set_replacing_objects(
428 &self,
429 event: &ReplacementEvent,
430 sa: &mut crate::spellability::SpellAbility,
431 ) {
432 let mut current = Some(sa);
433 while let Some(node) = current {
434 match event {
435 ReplacementEvent::Moved {
436 card,
437 origin,
438 destination,
439 ..
440 } => {
441 let card_csv = card.0.to_string();
442 node.set_triggering_object(AbilityKey::Card, card_csv.as_str());
448 node.set_triggering_object(AbilityKey::ReplacedCard, card_csv.as_str());
449 node.set_triggering_object(AbilityKey::Affected, card_csv.as_str());
450 node.set_triggering_object(
451 AbilityKey::Origin,
452 format!("{:?}", origin).as_str(),
453 );
454 node.set_triggering_object(
455 AbilityKey::Destination,
456 format!("{:?}", destination).as_str(),
457 );
458 }
459 ReplacementEvent::DamageToCard {
460 target,
461 amount,
462 source,
463 ..
464 } => {
465 let target_csv = target.0.to_string();
468 node.set_triggering_object(AbilityKey::Target, target_csv.as_str());
469 node.set_triggering_object(AbilityKey::Affected, target_csv.as_str());
470 node.set_triggering_object(
471 AbilityKey::DamageAmount,
472 amount.to_string().as_str(),
473 );
474 if let Some(src) = source {
475 let src_csv = src.0.to_string();
476 node.set_triggering_object(AbilityKey::Source, src_csv.as_str());
477 node.set_triggering_object(AbilityKey::DamageSource, src_csv.as_str());
478 }
479 }
480 ReplacementEvent::DamageToPlayer {
481 target,
482 amount,
483 source,
484 ..
485 } => {
486 node.set_triggering_object(
487 AbilityKey::DamageAmount,
488 amount.to_string().as_str(),
489 );
490 if let Some(src) = source {
491 let src_csv = src.0.to_string();
492 node.set_triggering_object(AbilityKey::Source, src_csv.as_str());
493 node.set_triggering_object(AbilityKey::DamageSource, src_csv.as_str());
494 }
495 node.set_triggering_object(
496 AbilityKey::TriggeredPlayer,
497 target.index().to_string().as_str(),
498 );
499 }
500 ReplacementEvent::Destroy { target } => {
501 let target_csv = target.0.to_string();
503 node.set_triggering_object(AbilityKey::Card, target_csv.as_str());
504 node.set_triggering_object(AbilityKey::Affected, target_csv.as_str());
505 }
506 ReplacementEvent::AddCounter {
507 target,
508 counter_type,
509 count,
510 ..
511 } => {
512 let target_csv = target.0.to_string();
515 node.set_triggering_object(AbilityKey::Card, target_csv.as_str());
516 node.set_triggering_object(AbilityKey::Affected, target_csv.as_str());
517 node.set_triggering_object(AbilityKey::Object, target_csv.as_str());
518 node.set_triggering_object(
519 AbilityKey::CounterMap,
520 format!("{:?}:{}", counter_type, count).as_str(),
521 );
522 }
523 ReplacementEvent::Draw {
524 player,
525 extra_draws,
526 ..
527 } => {
528 let pid = player.index().to_string();
531 node.set_triggering_object(AbilityKey::TriggeredPlayer, pid.as_str());
532 node.set_triggering_object(AbilityKey::Affected, pid.as_str());
533 node.set_triggering_object(AbilityKey::Num, extra_draws.to_string().as_str());
534 }
535 ReplacementEvent::DrawCards { player, count } => {
536 let pid = player.index().to_string();
538 node.set_triggering_object(AbilityKey::TriggeredPlayer, pid.as_str());
539 node.set_triggering_object(AbilityKey::Affected, pid.as_str());
540 node.set_triggering_object(AbilityKey::Num, count.to_string().as_str());
541 }
542 ReplacementEvent::CreateToken { player, count, .. } => {
543 let pid = player.index().to_string();
546 node.set_triggering_object(AbilityKey::TriggeredPlayer, pid.as_str());
547 node.set_triggering_object(AbilityKey::Affected, pid.as_str());
548 node.set_triggering_object(AbilityKey::TokenNum, count.to_string().as_str());
549 }
550 ReplacementEvent::GainLife { player, amount }
551 | ReplacementEvent::PayLife { player, amount }
552 | ReplacementEvent::LifeReduced { player, amount, .. } => {
553 let pid = player.index().to_string();
554 node.set_triggering_object(AbilityKey::TriggeredPlayer, pid.as_str());
555 node.set_triggering_object(AbilityKey::Affected, pid.as_str());
556 node.set_triggering_object(AbilityKey::LifeAmount, amount.to_string().as_str());
557 }
558 ReplacementEvent::Mill { player, count }
559 | ReplacementEvent::Scry { player, count }
560 | ReplacementEvent::Proliferate { player, count }
561 | ReplacementEvent::CopySpell { player, count } => {
562 let pid = player.index().to_string();
563 node.set_triggering_object(AbilityKey::TriggeredPlayer, pid.as_str());
564 node.set_triggering_object(AbilityKey::Affected, pid.as_str());
565 node.set_triggering_object(AbilityKey::Num, count.to_string().as_str());
566 }
567 ReplacementEvent::BeginTurn { player }
568 | ReplacementEvent::BeginPhase { player, .. }
569 | ReplacementEvent::DeclareBlocker { player }
570 | ReplacementEvent::RollPlanarDice { player }
571 | ReplacementEvent::PlanarDiceResult { player }
572 | ReplacementEvent::LoseMana { player }
573 | ReplacementEvent::GameLoss { player, .. }
574 | ReplacementEvent::GameWin { player }
575 | ReplacementEvent::Cascade { player }
576 | ReplacementEvent::Learn { player }
577 | ReplacementEvent::Planeswalk { player }
578 | ReplacementEvent::SetInMotion { player }
579 | ReplacementEvent::AssembleContraption { player } => {
580 let pid = player.index().to_string();
581 node.set_triggering_object(AbilityKey::TriggeredPlayer, pid.as_str());
582 node.set_triggering_object(AbilityKey::Affected, pid.as_str());
583 }
584 ReplacementEvent::Counter { card }
585 | ReplacementEvent::Tap { card }
586 | ReplacementEvent::Untap { card, .. }
587 | ReplacementEvent::Explore { card }
588 | ReplacementEvent::Transform { card }
589 | ReplacementEvent::TurnFaceUp { card }
590 | ReplacementEvent::AssignDealDamage { card } => {
591 let csv = card.0.to_string();
592 node.set_triggering_object(AbilityKey::Card, csv.as_str());
593 node.set_triggering_object(AbilityKey::Affected, csv.as_str());
594 }
595 ReplacementEvent::DealtDamage {
596 target,
597 amount,
598 source,
599 } => {
600 let target_csv = target.0.to_string();
601 node.set_triggering_object(AbilityKey::Target, target_csv.as_str());
602 node.set_triggering_object(AbilityKey::Affected, target_csv.as_str());
603 node.set_triggering_object(
604 AbilityKey::DamageAmount,
605 amount.to_string().as_str(),
606 );
607 if let Some(src) = source {
608 node.set_triggering_object(
609 AbilityKey::DamageSource,
610 src.0.to_string().as_str(),
611 );
612 }
613 }
614 ReplacementEvent::RemoveCounter {
615 target,
616 counter_type,
617 count,
618 } => {
619 let target_csv = target.0.to_string();
620 node.set_triggering_object(AbilityKey::Card, target_csv.as_str());
621 node.set_triggering_object(
622 AbilityKey::CounterMap,
623 format!("{:?}:{}", counter_type, count).as_str(),
624 );
625 }
626 ReplacementEvent::Attached { card, target } => {
627 node.set_triggering_object(AbilityKey::Card, card.0.to_string().as_str());
628 node.set_triggering_object(AbilityKey::Target, target.0.to_string().as_str());
629 node.set_triggering_object(AbilityKey::Affected, target.0.to_string().as_str());
630 }
631 ReplacementEvent::ProduceMana {
632 source,
633 activator,
634 mana,
635 } => {
636 node.set_triggering_object(AbilityKey::Source, source.0.to_string().as_str());
637 node.set_triggering_object(AbilityKey::Card, source.0.to_string().as_str());
638 node.set_triggering_object(
639 AbilityKey::TriggeredPlayer,
640 activator.index().to_string().as_str(),
641 );
642 node.set_triggering_object(AbilityKey::Produced, mana.as_str());
643 }
644 ReplacementEvent::RollDice {
645 player,
646 sides,
647 number,
648 ..
649 } => {
650 node.set_triggering_object(
651 AbilityKey::TriggeredPlayer,
652 player.index().to_string().as_str(),
653 );
654 node.set_triggering_object(AbilityKey::Num, number.to_string().as_str());
655 node.set_triggering_object(AbilityKey::Sides, sides.to_string().as_str());
656 }
657 }
658 current = node.get_sub_ability_mut();
659 }
660 }
661
662 pub fn mode_check(&self, event: &ReplacementType) -> bool {
669 if self.event == *event {
670 return true;
671 }
672 if self.event == ReplacementType::AddCounter && *event == ReplacementType::Moved {
675 return self.ir.counter_map;
676 }
677 false
678 }
679}
680
681impl ReplacementEffectIr {
682 fn from_params(params: &Params) -> Self {
683 Self {
684 card_trait_requirements: CardTraitRequirementsIr::from_key_values(
685 params.iter(),
686 params.selector_untracked(keys::IS_PRESENT).cloned(),
687 params.selector_untracked("IsPresent2").cloned(),
688 ),
689 replace_with: params.get(keys::REPLACE_WITH).map(str::to_string),
690 description_text: params.get(keys::DESCRIPTION).map(str::to_string),
691 player_turn_text: params.get(keys::PLAYER_TURN).map(str::to_string),
692 new_destination_text: params.get(keys::NEW_DESTINATION).map(str::to_string),
693 new_destination_zone: parsed_zone_type(params.get(keys::NEW_DESTINATION)),
694 origin_text: params.get(keys::ORIGIN).map(str::to_string),
695 origin_zone: parsed_zone_type(params.get(keys::ORIGIN)),
696 destination_text: params.get(keys::DESTINATION).map(str::to_string),
697 destination_zone: parsed_zone_type(params.get(keys::DESTINATION)),
698 exclude_destination_text: params.get("ExcludeDestination").map(str::to_string),
699 valid_card_text: params.get(keys::VALID_CARD).map(str::to_string),
700 valid_card_selector: params.selector_cloned(keys::VALID_CARD),
701 valid_player_text: params.get(keys::VALID_PLAYER).map(str::to_string),
702 valid_player_selector: params.selector_cloned(keys::VALID_PLAYER),
703 valid_target_selector: params.selector_cloned(keys::VALID_TARGET),
704 valid_source_selector: params.selector_cloned(keys::VALID_SOURCE),
705 valid_activator_text: params.get(keys::VALID_ACTIVATOR).map(str::to_string),
706 valid_step_turn_to_controller_text: params
707 .get("ValidStepTurnToController")
708 .map(str::to_string),
709 valid_explorer_text: params.get(keys::VALID_EXPLORER).map(str::to_string),
710 valid_counter_type_text: params.get(keys::VALID_COUNTER_TYPE).map(str::to_string),
711 valid_lose_reason_text: params.get(keys::VALID_LOSE_REASON).map(str::to_string),
712 valid_sides_text: params.get(keys::VALID_SIDES).map(str::to_string),
713 active_phases: parsed_phase_types(
714 params
715 .get(keys::PHASE)
716 .or_else(|| params.get(keys::ACTIVE_PHASES)),
717 ),
718 amount_text: params.get(keys::AMOUNT).map(str::to_string),
719 damage_amount_text: params.get(keys::DAMAGE_AMOUNT).map(str::to_string),
720 result_text: params.get(keys::RESULT).map(str::to_string),
721 number_text: params.get(keys::NUMBER).map(str::to_string),
722 dredge_amount: params.get("DredgeAmount").and_then(|s| s.parse().ok()),
723 skip: params.has(keys::SKIP),
724 prevent: parsed_true(params.get(keys::PREVENT)),
725 optional: params.has(keys::OPTIONAL),
726 effect_only: parsed_true(params.get("EffectOnly")),
727 discard: parsed_bool(params.get("Discard")),
728 flashback_cast: parsed_bool(params.get("FlashbackCast")),
729 harmonize_cast: parsed_bool(params.get("HarmonizeCast")),
730 not_first_card_in_draw_step: parsed_true(params.get("NotFirstCardInDrawStep")),
731 exiled_with_effect_source: params.has("ExiledWithEffectSource"),
732 is_combat: parsed_bool(params.get(keys::IS_COMBAT)),
733 is_damage: parsed_bool(params.get(keys::IS_DAMAGE)),
734 max_speed: parsed_bool(params.get("MaxSpeed")),
735 replacement_result: params.get("ReplacementResult").map(str::to_string),
736 optional_decider_text: params.get(keys::OPTIONAL_DECIDER).map(str::to_string),
737 replace_mana_text: params.get(keys::REPLACE_MANA).map(str::to_string),
738 replace_type_text: params.get(keys::REPLACE_TYPE).map(str::to_string),
739 replace_color_text: params.get(keys::REPLACE_COLOR).map(str::to_string),
740 replace_only_text: params.get(keys::REPLACE_ONLY).map(str::to_string),
741 replace_amount_text: params.get(keys::REPLACE_AMOUNT).map(str::to_string),
742 valid_lki_text: params.get("ValidLKI").map(str::to_string),
743 counter_map: params.get("CounterMap").is_some(),
744 replace_with_chain: None,
745 }
746 }
747}
748
749fn append_shield_remaining(mut desc: String, rep_sa: &SpellAbility, host: &Card) -> String {
756 use crate::ability::api_type::ApiType;
757
758 let api = match rep_sa.api {
759 Some(a) => a,
760 None => return desc,
761 };
762
763 let (param_value, default_one) = match api {
764 ApiType::ReplaceDamage => match rep_sa.ir.amount.as_deref() {
765 Some(v) => (v.to_string(), false),
766 None => return desc,
767 },
768 ApiType::ReplaceSplitDamage => (
769 rep_sa
770 .ir
771 .var_name_text
772 .clone()
773 .unwrap_or_else(|| "1".to_string()),
774 true,
775 ),
776 _ => return desc,
777 };
778
779 if param_value.chars().all(|c| c.is_ascii_digit()) {
782 if default_one && param_value == "1" {
783 desc.push_str(" \nShields remain: 1");
784 }
785 return desc;
786 }
787
788 if let Some(resolved) = host.get_svar(¶m_value) {
790 if let Some(rest) = resolved.strip_prefix("Number$") {
791 desc.push_str(" \nShields remain: ");
792 desc.push_str(rest);
793 }
794 }
795 desc
796}
797
798pub fn zone_matches(expr: &str, zone: ZoneType) -> bool {
808 expr.split(',').any(|part| match part.trim() {
809 "Battlefield" => zone == ZoneType::Battlefield,
810 "Graveyard" => zone == ZoneType::Graveyard,
811 "Hand" => zone == ZoneType::Hand,
812 "Library" => zone == ZoneType::Library,
813 "Exile" => zone == ZoneType::Exile,
814 "Command" => zone == ZoneType::Command,
815 "Stack" => zone == ZoneType::Stack,
816 _ => false,
817 })
818}
819
820pub fn parse_replacement_effect(raw: &str) -> Option<ReplacementEffect> {
837 let trimmed = raw.trim();
838 let body = if let Some(rest) = trimmed.strip_prefix("R$ ") {
843 rest
844 } else if let Some(rest) = trimmed.strip_prefix("R:") {
845 rest.trim_start()
846 } else {
847 trimmed
848 };
849
850 let params = Params::from_raw(body);
852
853 let event = match params.get(keys::EVENT) {
854 Some(s) => ReplacementType::smart_value_of(s),
855 None => return None,
856 };
857
858 let layer = params
860 .get(keys::LAYER)
861 .and_then(ReplacementLayer::smart_value_of)
862 .unwrap_or(ReplacementLayer::Other);
863
864 let active_zones = params
866 .get(keys::ACTIVE_ZONES)
867 .map(parse_zone_list)
868 .unwrap_or_default();
869
870 Some(ReplacementEffect::new(event, layer, params, active_zones))
871}
872
873pub(super) fn parse_zone_list(s: &str) -> Vec<ZoneType> {
875 s.split([',', ' '])
876 .filter_map(|tok| match tok.trim() {
877 "Battlefield" => Some(ZoneType::Battlefield),
878 "Graveyard" => Some(ZoneType::Graveyard),
879 "Hand" => Some(ZoneType::Hand),
880 "Library" => Some(ZoneType::Library),
881 "Exile" => Some(ZoneType::Exile),
882 "Command" => Some(ZoneType::Command),
883 _ => None,
884 })
885 .collect()
886}
887
888fn parsed_zone_type(value: Option<&str>) -> Option<ZoneType> {
889 match value? {
890 "Battlefield" => Some(ZoneType::Battlefield),
891 "Graveyard" => Some(ZoneType::Graveyard),
892 "Hand" => Some(ZoneType::Hand),
893 "Library" => Some(ZoneType::Library),
894 "Exile" => Some(ZoneType::Exile),
895 "Command" => Some(ZoneType::Command),
896 "Stack" => Some(ZoneType::Stack),
897 _ => None,
898 }
899}
900
901pub(crate) fn parse_replacement_chain(
902 raw: &str,
903 svars: &impl HasSVars,
904) -> Option<ReplacementChainIr> {
905 let params = Params::from_raw(raw);
906 let db = params.get(keys::DB)?;
907 match db {
908 "ReplaceToken" => Some(ReplacementChainIr::ReplaceToken {
909 token_type: params.get("Type").map(str::to_string),
910 amount_expr: params.get("Amount").map(str::to_string),
911 }),
912 "ReplaceCounter" => Some(ReplacementChainIr::ReplaceCounter {
913 amount_expr: params.get("Amount")?.to_string(),
914 }),
915 "ReplaceEffect" => {
916 let sub_ability = params
917 .get(keys::SUB_ABILITY)
918 .and_then(|name| svars.get_svar(name))
919 .and_then(|sub_raw| parse_replacement_chain(sub_raw, svars))
920 .map(Box::new);
921 Some(ReplacementChainIr::ReplaceEffect {
922 var_name: params.get("VarName").map(str::to_string),
923 var_type: params.get("VarType").map(str::to_string),
924 var_key: params.get("VarKey").map(str::to_string),
925 var_value: params.get("VarValue").map(str::to_string),
926 sub_ability,
927 })
928 }
929 _ => None,
930 }
931}
932
933pub(crate) fn resolve_replace_with_chain(
934 effect: &ReplacementEffect,
935 source_card: &Card,
936) -> Option<ReplacementChainIr> {
937 effect.ir.replace_with_chain.clone().or_else(|| {
938 effect
939 .replace_with()
940 .and_then(|svar_name| source_card.get_svar(svar_name))
941 .and_then(|raw| parse_replacement_chain(raw, source_card))
942 })
943}
944
945fn parsed_phase_types(value: Option<&str>) -> Vec<PhaseType> {
946 value
947 .into_iter()
948 .flat_map(|raw| raw.split(','))
949 .filter_map(|phase| PhaseType::from_script_name(phase.trim()))
950 .collect()
951}
952
953fn parsed_true(value: Option<&str>) -> bool {
954 matches!(value, Some("True") | Some("true"))
955}
956
957fn parsed_bool(value: Option<&str>) -> Option<bool> {
958 value.map(|raw| matches!(raw, "True" | "true"))
959}
960
961#[cfg(test)]
964mod tests {
965 use super::*;
966
967 #[test]
970 fn parse_damage_prevention() {
971 let raw = "R$ Event$ DamageDone | ActiveZones$ Battlefield | ValidCard$ Card.Self | Prevent$ True | Description$ Prevent all damage dealt to ~.";
972 let re = parse_replacement_effect(raw).expect("should parse");
973 assert_eq!(re.event, ReplacementType::DamageDone);
974 assert_eq!(re.layer, ReplacementLayer::Other);
975 assert!(re.ir.prevent);
976 assert_eq!(re.active_zones, vec![ZoneType::Battlefield]);
977 }
978
979 #[test]
980 fn parse_draw_skip() {
981 let raw = "R$ Event$ Draw | ValidPlayer$ You | Description$ Skip your draw step.";
982 let re = parse_replacement_effect(raw).expect("should parse");
983 assert_eq!(re.event, ReplacementType::Draw);
984 assert_eq!(re.ir.valid_player_text.as_deref(), Some("You"));
985 assert!(re.active_zones.is_empty());
986 }
987
988 #[test]
989 fn parse_destroy_replacement() {
990 let raw = "R$ Event$ Destroy | ValidCard$ Card.Self | Description$ ~ is indestructible.";
991 let re = parse_replacement_effect(raw).expect("should parse");
992 assert_eq!(re.event, ReplacementType::Destroy);
993 assert_eq!(re.ir.valid_card_text.as_deref(), Some("Card.Self"));
994 }
995
996 #[test]
997 fn parse_moved_exile_instead() {
998 let raw = "R$ Event$ Moved | Destination$ Graveyard | Origin$ Battlefield | ValidCard$ Card.Self | NewDestination$ Exile | Description$ If ~ would die, exile it instead.";
999 let re = parse_replacement_effect(raw).expect("should parse");
1000 assert_eq!(re.event, ReplacementType::Moved);
1001 assert_eq!(re.ir.destination_text.as_deref(), Some("Graveyard"));
1002 assert_eq!(re.ir.origin_text.as_deref(), Some("Battlefield"));
1003 assert_eq!(re.ir.new_destination_text.as_deref(), Some("Exile"));
1004 }
1005
1006 #[test]
1007 fn parse_cant_happen_layer() {
1008 let raw = "R$ Event$ Destroy | Layer$ CantHappen | ValidCard$ Card.Self";
1009 let re = parse_replacement_effect(raw).expect("should parse");
1010 assert_eq!(re.layer, ReplacementLayer::CantHappen);
1011 }
1012
1013 #[test]
1014 fn parse_r_colon_prefix() {
1015 let raw = "R: Event$ Draw | ValidPlayer$ You";
1016 let re = parse_replacement_effect(raw).expect("should parse R: prefix");
1017 assert_eq!(re.event, ReplacementType::Draw);
1018 }
1019
1020 #[test]
1021 fn non_replacement_line_returns_none() {
1022 assert!(parse_replacement_effect("AB$ Mana | Cost$ T | Produced$ G").is_none());
1023 assert!(
1024 parse_replacement_effect("S$ Mode$ Continuous | Affected$ Creature.YouControl")
1025 .is_none()
1026 );
1027 assert!(parse_replacement_effect("").is_none());
1028 }
1029
1030 #[test]
1033 fn active_in_zone_empty_means_always() {
1034 let raw = "R$ Event$ Draw | ValidPlayer$ You";
1035 let re = parse_replacement_effect(raw).unwrap();
1036 assert!(re.active_in_zone(ZoneType::Battlefield));
1038 assert!(re.active_in_zone(ZoneType::Hand));
1039 assert!(re.active_in_zone(ZoneType::Graveyard));
1040 }
1041
1042 #[test]
1043 fn active_in_zone_respects_active_zones() {
1044 let raw = "R$ Event$ DamageDone | ActiveZones$ Battlefield | Prevent$ True";
1045 let re = parse_replacement_effect(raw).unwrap();
1046 assert!(re.active_in_zone(ZoneType::Battlefield));
1047 assert!(!re.active_in_zone(ZoneType::Graveyard));
1048 assert!(!re.active_in_zone(ZoneType::Hand));
1049 }
1050
1051 #[test]
1054 fn parse_all_new_event_types() {
1055 for (event_str, expected) in [
1056 ("Tap", ReplacementType::Tap),
1057 ("Untap", ReplacementType::Untap),
1058 ("Mill", ReplacementType::Mill),
1059 ("Scry", ReplacementType::Scry),
1060 ("Explore", ReplacementType::Explore),
1061 ("Cascade", ReplacementType::Cascade),
1062 ("Learn", ReplacementType::Learn),
1063 ("Proliferate", ReplacementType::Proliferate),
1064 ("Transform", ReplacementType::Transform),
1065 ("TurnFaceUp", ReplacementType::TurnFaceUp),
1066 ("RollDice", ReplacementType::RollDice),
1067 ] {
1068 let raw = format!("R$ Event$ {event_str} | Description$ test");
1069 let re = parse_replacement_effect(&raw).expect(&format!("should parse {event_str}"));
1070 assert_eq!(re.event, expected, "failed for {event_str}");
1071 }
1072 }
1073}