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_object_selector: Option<CompiledSelector>,
92 pub valid_cause_text: Option<String>,
93 pub valid_target_selector: Option<CompiledSelector>,
94 pub valid_source_selector: Option<CompiledSelector>,
95 pub valid_activator_text: Option<String>,
96 pub valid_step_turn_to_controller_text: Option<String>,
97 pub valid_explorer_text: Option<String>,
98 pub valid_counter_type_text: Option<String>,
99 pub valid_lose_reason_text: Option<String>,
100 pub valid_sides_text: Option<String>,
101 pub active_phases: Vec<PhaseType>,
102 pub amount_text: Option<String>,
103 pub damage_amount_text: Option<String>,
104 pub result_text: Option<String>,
105 pub number_text: Option<String>,
106 pub dredge_amount: Option<usize>,
107 pub skip: bool,
108 pub prevent: bool,
109 pub optional: bool,
110 pub effect_only: bool,
111 pub discard: Option<bool>,
112 pub flashback_cast: Option<bool>,
113 pub harmonize_cast: Option<bool>,
114 pub not_first_card_in_draw_step: bool,
115 pub exiled_with_effect_source: bool,
116 pub is_combat: Option<bool>,
117 pub is_damage: Option<bool>,
118 pub max_speed: Option<bool>,
119 pub replacement_result: Option<String>,
120 pub optional_decider_text: Option<String>,
121 pub replace_mana_text: Option<String>,
122 pub replace_type_text: Option<String>,
123 pub replace_color_text: Option<String>,
124 pub replace_only_text: Option<String>,
125 pub replace_amount_text: Option<String>,
126 pub valid_lki_text: Option<String>,
127 pub counter_map: bool,
128 pub replace_with_chain: Option<ReplacementChainIr>,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub enum ReplacementChainIr {
133 ReplaceToken {
134 token_type: Option<String>,
135 amount_expr: Option<String>,
136 },
137 ReplaceCounter {
138 amount_expr: String,
139 choose_counter: bool,
140 after_replacement_static_abilities: Vec<String>,
141 },
142 ReplaceEffect {
143 var_name: Option<String>,
144 var_type: Option<String>,
145 var_key: Option<String>,
146 var_value: Option<String>,
147 sub_ability: Option<Box<ReplacementChainIr>>,
148 },
149}
150
151impl CardTrait for ReplacementEffect {
152 fn base(&self) -> &CardTraitBase {
153 &self.base.card_trait_base
154 }
155}
156
157impl CardTraitIrOwner for ReplacementEffect {
158 type Ir = ReplacementEffectIr;
159
160 fn ir(&self) -> &Self::Ir {
161 &self.ir
162 }
163
164 fn card_trait_requirements(&self) -> &CardTraitRequirementsIr {
165 &self.ir.card_trait_requirements
166 }
167}
168
169impl ReplacementEffect {
170 pub fn set_host_card(&mut self, host: &Card) {
183 self.ir.replace_with_chain = self
184 .replace_with()
185 .and_then(|svar_name| host.get_svar(svar_name))
186 .and_then(|raw| parse_replacement_chain(raw, host));
187 self.base.set_host_card_id(host.id);
188 }
189}
190
191impl ReplacementEffect {
192 pub fn new(
193 event: ReplacementType,
194 layer: ReplacementLayer,
195 params: Params,
196 active_zones: Vec<ZoneType>,
197 ) -> Self {
198 let mut effect = Self {
199 base: Box::new(TriggerReplacementBase::default()),
200 event,
201 layer,
202 ir: ReplacementEffectIr::from_params(¶ms),
203 active_zones,
204 suppressed: false,
205 };
206 effect.sync_trait_base_params(¶ms);
207 effect
208 }
209
210 pub fn replace_with(&self) -> Option<&str> {
211 self.ir.replace_with.as_deref()
212 }
213
214 pub fn has_skip(&self) -> bool {
215 self.ir.skip
216 }
217
218 pub fn prevents(&self) -> bool {
219 self.ir.prevent
220 }
221
222 pub fn matches_phase(&self, phase: PhaseType) -> bool {
223 self.ir.active_phases.is_empty() || self.ir.active_phases.contains(&phase)
224 }
225
226 fn sync_trait_base_params(&mut self, params: &Params) {
227 let map = params
228 .iter()
229 .map(|(k, v)| (k.to_string(), v.to_string()))
230 .collect();
231 self.base.card_trait_base.set_map_params(map);
232 }
233
234 pub fn active_in_zone(&self, zone: ZoneType) -> bool {
239 !self.suppressed && (self.active_zones.is_empty() || self.active_zones.contains(&zone))
240 }
241
242 pub fn description(&self, host: &Card, game: &GameState) -> String {
258 if self.suppressed || self.base.card_trait_base.is_suppressed() {
259 return String::new();
260 }
261 let Some(raw) = self.ir.description_text.as_deref() else {
262 return String::new();
263 };
264 let mut desc =
265 crate::ability::ability_utils::apply_description_text_change_effects(raw, host);
266 desc = desc.replace("CARDNAME", &host.card_name);
267 desc = desc.replace("NICKNAME", &host.card_name);
270 if desc.contains("EFFECTSOURCE") {
271 let source_name = host
272 .effect_source
273 .map(|id| game.card(id).card_name.clone())
274 .unwrap_or_else(|| host.card_name.clone());
275 desc = desc.replace("EFFECTSOURCE", &source_name);
276 }
277
278 if self.event == ReplacementType::DamageDone {
280 if let Some(rep_sa) = self.base.get_overriding_ability() {
281 desc = append_shield_remaining(desc, rep_sa, host);
282 }
283 }
284
285 desc
286 }
287
288 pub fn has_run(&self) -> bool {
295 false
296 }
297
298 pub fn requirements_check(&self, game: &GameState, source: &Card) -> bool {
300 if self.suppressed || self.base.card_trait_base.is_suppressed() {
301 return false;
302 }
303
304 if let Some(pt) = self.ir.player_turn_text.as_deref() {
305 if pt == "True" {
306 if game.active_player() != source.controller {
307 return false;
308 }
309 } else {
310 let players = crate::ability::ability_utils::resolve_defined_players(
311 pt,
312 source.controller,
313 game,
314 );
315 if !players.contains(&game.active_player()) {
316 return false;
317 }
318 }
319 }
320
321 if !self.matches_phase(game.turn.phase) {
323 return false;
324 }
325
326 self.meets_card_trait_requirements(game, source, source)
327 }
328
329 pub fn copy(&self) -> Self {
334 self.clone()
335 }
336
337 pub fn ensure_ability(
349 &self,
350 game: &GameState,
351 host_card: CardId,
352 activating_player: PlayerId,
353 ) -> Option<SpellAbility> {
354 if let Some(overriding) = self.base.get_overriding_ability() {
355 return Some(overriding.clone());
356 }
357 let svar_name = self.replace_with()?;
358 let host = game.card(host_card);
359 let script = host.get_svar(svar_name)?;
360 Some(build_spell_ability(
361 game,
362 host_card,
363 script,
364 activating_player,
365 ))
366 }
367
368 pub fn ensure_ability_mut(
373 &mut self,
374 game: &GameState,
375 host_card: CardId,
376 activating_player: PlayerId,
377 ) -> Option<&mut SpellAbility> {
378 if self.base.get_overriding_ability().is_none() {
379 let ability = self.ensure_ability(game, host_card, activating_player)?;
380 self.base.overriding_ability = Some(ability);
383 }
384 self.base.overriding_ability.as_mut()
385 }
386
387 pub fn can_replace_etb(&self, source: &Card, affected: &Card) -> bool {
401 let targets_self_only = self
402 .ir
403 .valid_card_text
404 .as_deref()
405 .map(|v| v.starts_with("Card.Self"))
406 .unwrap_or(false);
407 if !targets_self_only && source.id == affected.id {
408 return false;
409 }
410 true
411 }
412
413 pub fn set_replacing_objects(
432 &self,
433 event: &ReplacementEvent,
434 sa: &mut crate::spellability::SpellAbility,
435 ) {
436 let mut current = Some(sa);
437 while let Some(node) = current {
438 match event {
439 ReplacementEvent::Moved {
440 card,
441 origin,
442 destination,
443 ..
444 } => {
445 let card_csv = card.0.to_string();
446 node.set_triggering_object(AbilityKey::Card, card_csv.as_str());
452 node.set_triggering_object(AbilityKey::ReplacedCard, card_csv.as_str());
453 node.set_triggering_object(AbilityKey::Affected, card_csv.as_str());
454 node.set_triggering_object(
455 AbilityKey::Origin,
456 format!("{:?}", origin).as_str(),
457 );
458 node.set_triggering_object(
459 AbilityKey::Destination,
460 format!("{:?}", destination).as_str(),
461 );
462 }
463 ReplacementEvent::DamageToCard {
464 target,
465 amount,
466 source,
467 ..
468 } => {
469 let target_csv = target.0.to_string();
472 node.set_triggering_object(AbilityKey::Target, target_csv.as_str());
473 node.set_triggering_object(AbilityKey::Affected, target_csv.as_str());
474 node.set_triggering_object(
475 AbilityKey::DamageAmount,
476 amount.to_string().as_str(),
477 );
478 if let Some(src) = source {
479 let src_csv = src.0.to_string();
480 node.set_triggering_object(AbilityKey::Source, src_csv.as_str());
481 node.set_triggering_object(AbilityKey::DamageSource, src_csv.as_str());
482 }
483 }
484 ReplacementEvent::DamageToPlayer {
485 target,
486 amount,
487 source,
488 ..
489 } => {
490 node.set_triggering_object(
491 AbilityKey::DamageAmount,
492 amount.to_string().as_str(),
493 );
494 if let Some(src) = source {
495 let src_csv = src.0.to_string();
496 node.set_triggering_object(AbilityKey::Source, src_csv.as_str());
497 node.set_triggering_object(AbilityKey::DamageSource, src_csv.as_str());
498 }
499 node.set_triggering_object(
500 AbilityKey::TriggeredPlayer,
501 target.index().to_string().as_str(),
502 );
503 }
504 ReplacementEvent::Destroy { target } => {
505 let target_csv = target.0.to_string();
507 node.set_triggering_object(AbilityKey::Card, target_csv.as_str());
508 node.set_triggering_object(AbilityKey::Affected, target_csv.as_str());
509 }
510 ReplacementEvent::AddCounter {
511 target,
512 counter_type,
513 count,
514 ..
515 } => {
516 let target_csv = match target {
519 crate::agent::GameEntity::Card(card) => card.0.to_string(),
520 crate::agent::GameEntity::Player(player) => player.0.to_string(),
521 };
522 match target {
523 crate::agent::GameEntity::Card(_) => {
524 node.set_triggering_object(AbilityKey::Card, target_csv.as_str());
525 }
526 crate::agent::GameEntity::Player(_) => {
527 node.set_triggering_object(AbilityKey::Player, target_csv.as_str());
528 }
529 }
530 node.set_triggering_object(AbilityKey::Affected, target_csv.as_str());
531 node.set_triggering_object(AbilityKey::Object, target_csv.as_str());
532 node.set_triggering_object(
533 AbilityKey::CounterMap,
534 format!("{:?}:{}", counter_type, count).as_str(),
535 );
536 }
537 ReplacementEvent::Draw {
538 player,
539 extra_draws,
540 ..
541 } => {
542 let pid = player.index().to_string();
545 node.set_triggering_object(AbilityKey::TriggeredPlayer, pid.as_str());
546 node.set_triggering_object(AbilityKey::Affected, pid.as_str());
547 node.set_triggering_object(AbilityKey::Num, extra_draws.to_string().as_str());
548 }
549 ReplacementEvent::DrawCards { player, count } => {
550 let pid = player.index().to_string();
552 node.set_triggering_object(AbilityKey::TriggeredPlayer, pid.as_str());
553 node.set_triggering_object(AbilityKey::Affected, pid.as_str());
554 node.set_triggering_object(AbilityKey::Num, count.to_string().as_str());
555 }
556 ReplacementEvent::CreateToken { player, count, .. } => {
557 let pid = player.index().to_string();
560 node.set_triggering_object(AbilityKey::TriggeredPlayer, pid.as_str());
561 node.set_triggering_object(AbilityKey::Affected, pid.as_str());
562 node.set_triggering_object(AbilityKey::TokenNum, count.to_string().as_str());
563 }
564 ReplacementEvent::GainLife { player, amount }
565 | ReplacementEvent::PayLife { player, amount }
566 | ReplacementEvent::LifeReduced { player, amount, .. } => {
567 let pid = player.index().to_string();
568 node.set_triggering_object(AbilityKey::TriggeredPlayer, pid.as_str());
569 node.set_triggering_object(AbilityKey::Affected, pid.as_str());
570 node.set_triggering_object(AbilityKey::LifeAmount, amount.to_string().as_str());
571 }
572 ReplacementEvent::Mill { player, count }
573 | ReplacementEvent::Scry { player, count }
574 | ReplacementEvent::Proliferate { player, count }
575 | ReplacementEvent::CopySpell { player, count } => {
576 let pid = player.index().to_string();
577 node.set_triggering_object(AbilityKey::TriggeredPlayer, pid.as_str());
578 node.set_triggering_object(AbilityKey::Affected, pid.as_str());
579 node.set_triggering_object(AbilityKey::Num, count.to_string().as_str());
580 }
581 ReplacementEvent::BeginTurn { player }
582 | ReplacementEvent::BeginPhase { player, .. }
583 | ReplacementEvent::DeclareBlocker { player }
584 | ReplacementEvent::RollPlanarDice { player }
585 | ReplacementEvent::PlanarDiceResult { player }
586 | ReplacementEvent::LoseMana { player }
587 | ReplacementEvent::GameLoss { player, .. }
588 | ReplacementEvent::GameWin { player }
589 | ReplacementEvent::Cascade { player }
590 | ReplacementEvent::Learn { player }
591 | ReplacementEvent::Planeswalk { player }
592 | ReplacementEvent::SetInMotion { player }
593 | ReplacementEvent::AssembleContraption { player } => {
594 let pid = player.index().to_string();
595 node.set_triggering_object(AbilityKey::TriggeredPlayer, pid.as_str());
596 node.set_triggering_object(AbilityKey::Affected, pid.as_str());
597 }
598 ReplacementEvent::Counter { card }
599 | ReplacementEvent::Tap { card }
600 | ReplacementEvent::Untap { card, .. }
601 | ReplacementEvent::Explore { card }
602 | ReplacementEvent::Transform { card }
603 | ReplacementEvent::TurnFaceUp { card }
604 | ReplacementEvent::AssignDealDamage { card } => {
605 let csv = card.0.to_string();
606 node.set_triggering_object(AbilityKey::Card, csv.as_str());
607 node.set_triggering_object(AbilityKey::Affected, csv.as_str());
608 }
609 ReplacementEvent::DealtDamage {
610 target,
611 amount,
612 source,
613 } => {
614 let target_csv = target.0.to_string();
615 node.set_triggering_object(AbilityKey::Target, target_csv.as_str());
616 node.set_triggering_object(AbilityKey::Affected, target_csv.as_str());
617 node.set_triggering_object(
618 AbilityKey::DamageAmount,
619 amount.to_string().as_str(),
620 );
621 if let Some(src) = source {
622 node.set_triggering_object(
623 AbilityKey::DamageSource,
624 src.0.to_string().as_str(),
625 );
626 }
627 }
628 ReplacementEvent::RemoveCounter {
629 target,
630 counter_type,
631 count,
632 } => {
633 let target_csv = target.0.to_string();
634 node.set_triggering_object(AbilityKey::Card, target_csv.as_str());
635 node.set_triggering_object(
636 AbilityKey::CounterMap,
637 format!("{:?}:{}", counter_type, count).as_str(),
638 );
639 }
640 ReplacementEvent::Attached { card, target } => {
641 node.set_triggering_object(AbilityKey::Card, card.0.to_string().as_str());
642 node.set_triggering_object(AbilityKey::Target, target.0.to_string().as_str());
643 node.set_triggering_object(AbilityKey::Affected, target.0.to_string().as_str());
644 }
645 ReplacementEvent::ProduceMana {
646 source,
647 activator,
648 mana,
649 } => {
650 node.set_triggering_object(AbilityKey::Source, source.0.to_string().as_str());
651 node.set_triggering_object(AbilityKey::Card, source.0.to_string().as_str());
652 node.set_triggering_object(
653 AbilityKey::TriggeredPlayer,
654 activator.index().to_string().as_str(),
655 );
656 node.set_triggering_object(AbilityKey::Produced, mana.as_str());
657 }
658 ReplacementEvent::RollDice {
659 player,
660 sides,
661 number,
662 ..
663 } => {
664 node.set_triggering_object(
665 AbilityKey::TriggeredPlayer,
666 player.index().to_string().as_str(),
667 );
668 node.set_triggering_object(AbilityKey::Num, number.to_string().as_str());
669 node.set_triggering_object(AbilityKey::Sides, sides.to_string().as_str());
670 }
671 }
672 current = node.get_sub_ability_mut();
673 }
674 }
675
676 pub fn mode_check(&self, event: &ReplacementType) -> bool {
683 if self.event == *event {
684 return true;
685 }
686 if self.event == ReplacementType::AddCounter && *event == ReplacementType::Moved {
689 return self.ir.counter_map;
690 }
691 false
692 }
693}
694
695impl ReplacementEffectIr {
696 fn from_params(params: &Params) -> Self {
697 Self {
698 card_trait_requirements: CardTraitRequirementsIr::from_key_values(
699 params.iter(),
700 params.selector_untracked(keys::IS_PRESENT).cloned(),
701 params.selector_untracked("IsPresent2").cloned(),
702 ),
703 replace_with: params.get(keys::REPLACE_WITH).map(str::to_string),
704 description_text: params.get(keys::DESCRIPTION).map(str::to_string),
705 player_turn_text: params.get(keys::PLAYER_TURN).map(str::to_string),
706 new_destination_text: params.get(keys::NEW_DESTINATION).map(str::to_string),
707 new_destination_zone: parsed_zone_type(params.get(keys::NEW_DESTINATION)),
708 origin_text: params.get(keys::ORIGIN).map(str::to_string),
709 origin_zone: parsed_zone_type(params.get(keys::ORIGIN)),
710 destination_text: params.get(keys::DESTINATION).map(str::to_string),
711 destination_zone: parsed_zone_type(params.get(keys::DESTINATION)),
712 exclude_destination_text: params.get("ExcludeDestination").map(str::to_string),
713 valid_card_text: params.get(keys::VALID_CARD).map(str::to_string),
714 valid_card_selector: params.selector_cloned(keys::VALID_CARD),
715 valid_player_text: params.get(keys::VALID_PLAYER).map(str::to_string),
716 valid_player_selector: params.selector_cloned(keys::VALID_PLAYER),
717 valid_object_selector: params.selector_cloned("ValidObject"),
718 valid_cause_text: params.get(keys::VALID_CAUSE).map(str::to_string),
719 valid_target_selector: params.selector_cloned(keys::VALID_TARGET),
720 valid_source_selector: params.selector_cloned(keys::VALID_SOURCE),
721 valid_activator_text: params.get(keys::VALID_ACTIVATOR).map(str::to_string),
722 valid_step_turn_to_controller_text: params
723 .get("ValidStepTurnToController")
724 .map(str::to_string),
725 valid_explorer_text: params.get(keys::VALID_EXPLORER).map(str::to_string),
726 valid_counter_type_text: params.get(keys::VALID_COUNTER_TYPE).map(str::to_string),
727 valid_lose_reason_text: params.get(keys::VALID_LOSE_REASON).map(str::to_string),
728 valid_sides_text: params.get(keys::VALID_SIDES).map(str::to_string),
729 active_phases: parsed_phase_types(
730 params
731 .get(keys::PHASE)
732 .or_else(|| params.get(keys::ACTIVE_PHASES)),
733 ),
734 amount_text: params.get(keys::AMOUNT).map(str::to_string),
735 damage_amount_text: params.get(keys::DAMAGE_AMOUNT).map(str::to_string),
736 result_text: params.get(keys::RESULT).map(str::to_string),
737 number_text: params.get(keys::NUMBER).map(str::to_string),
738 dredge_amount: params.get("DredgeAmount").and_then(|s| s.parse().ok()),
739 skip: params.has(keys::SKIP),
740 prevent: parsed_true(params.get(keys::PREVENT)),
741 optional: params.has(keys::OPTIONAL),
742 effect_only: params.has("EffectOnly"),
743 discard: parsed_bool(params.get("Discard")),
744 flashback_cast: parsed_bool(params.get("FlashbackCast")),
745 harmonize_cast: parsed_bool(params.get("HarmonizeCast")),
746 not_first_card_in_draw_step: parsed_true(params.get("NotFirstCardInDrawStep")),
747 exiled_with_effect_source: params.has("ExiledWithEffectSource"),
748 is_combat: parsed_bool(params.get(keys::IS_COMBAT)),
749 is_damage: parsed_bool(params.get(keys::IS_DAMAGE)),
750 max_speed: parsed_bool(params.get("MaxSpeed")),
751 replacement_result: params.get("ReplacementResult").map(str::to_string),
752 optional_decider_text: params.get(keys::OPTIONAL_DECIDER).map(str::to_string),
753 replace_mana_text: params.get(keys::REPLACE_MANA).map(str::to_string),
754 replace_type_text: params.get(keys::REPLACE_TYPE).map(str::to_string),
755 replace_color_text: params.get(keys::REPLACE_COLOR).map(str::to_string),
756 replace_only_text: params.get(keys::REPLACE_ONLY).map(str::to_string),
757 replace_amount_text: params.get(keys::REPLACE_AMOUNT).map(str::to_string),
758 valid_lki_text: params.get("ValidLKI").map(str::to_string),
759 counter_map: params.get("CounterMap").is_some(),
760 replace_with_chain: None,
761 }
762 }
763}
764
765fn append_shield_remaining(mut desc: String, rep_sa: &SpellAbility, host: &Card) -> String {
772 use crate::ability::api_type::ApiType;
773
774 let api = match rep_sa.api {
775 Some(a) => a,
776 None => return desc,
777 };
778
779 let (param_value, default_one) = match api {
780 ApiType::ReplaceDamage => match rep_sa.ir.amount.as_deref() {
781 Some(v) => (v.to_string(), false),
782 None => return desc,
783 },
784 ApiType::ReplaceSplitDamage => (
785 rep_sa
786 .ir
787 .var_name_text
788 .clone()
789 .unwrap_or_else(|| "1".to_string()),
790 true,
791 ),
792 _ => return desc,
793 };
794
795 if param_value.chars().all(|c| c.is_ascii_digit()) {
798 if default_one && param_value == "1" {
799 desc.push_str(" \nShields remain: 1");
800 }
801 return desc;
802 }
803
804 if let Some(resolved) = host.get_svar(¶m_value) {
806 if let Some(rest) = resolved.strip_prefix("Number$") {
807 desc.push_str(" \nShields remain: ");
808 desc.push_str(rest);
809 }
810 }
811 desc
812}
813
814pub fn zone_matches(expr: &str, zone: ZoneType) -> bool {
824 expr.split(',').any(|part| match part.trim() {
825 "Battlefield" => zone == ZoneType::Battlefield,
826 "Graveyard" => zone == ZoneType::Graveyard,
827 "Hand" => zone == ZoneType::Hand,
828 "Library" => zone == ZoneType::Library,
829 "Exile" => zone == ZoneType::Exile,
830 "Command" => zone == ZoneType::Command,
831 "Stack" => zone == ZoneType::Stack,
832 _ => false,
833 })
834}
835
836pub fn parse_replacement_effect(raw: &str) -> Option<ReplacementEffect> {
853 let trimmed = raw.trim();
854 let body = if let Some(rest) = trimmed.strip_prefix("R$ ") {
859 rest
860 } else if let Some(rest) = trimmed.strip_prefix("R:") {
861 rest.trim_start()
862 } else {
863 trimmed
864 };
865
866 let params = Params::from_raw(body);
868
869 let event = match params.get(keys::EVENT) {
870 Some(s) => ReplacementType::smart_value_of(s),
871 None => return None,
872 };
873
874 let layer = params
876 .get(keys::LAYER)
877 .and_then(ReplacementLayer::smart_value_of)
878 .unwrap_or(ReplacementLayer::Other);
879
880 let active_zones = params
882 .get(keys::ACTIVE_ZONES)
883 .map(parse_zone_list)
884 .unwrap_or_default();
885
886 Some(ReplacementEffect::new(event, layer, params, active_zones))
887}
888
889pub(super) fn parse_zone_list(s: &str) -> Vec<ZoneType> {
891 s.split([',', ' '])
892 .filter_map(|tok| match tok.trim() {
893 "Battlefield" => Some(ZoneType::Battlefield),
894 "Graveyard" => Some(ZoneType::Graveyard),
895 "Hand" => Some(ZoneType::Hand),
896 "Library" => Some(ZoneType::Library),
897 "Exile" => Some(ZoneType::Exile),
898 "Command" => Some(ZoneType::Command),
899 _ => None,
900 })
901 .collect()
902}
903
904fn parsed_zone_type(value: Option<&str>) -> Option<ZoneType> {
905 match value? {
906 "Battlefield" => Some(ZoneType::Battlefield),
907 "Graveyard" => Some(ZoneType::Graveyard),
908 "Hand" => Some(ZoneType::Hand),
909 "Library" => Some(ZoneType::Library),
910 "Exile" => Some(ZoneType::Exile),
911 "Command" => Some(ZoneType::Command),
912 "Stack" => Some(ZoneType::Stack),
913 _ => None,
914 }
915}
916
917pub(crate) fn parse_replacement_chain(
918 raw: &str,
919 svars: &impl HasSVars,
920) -> Option<ReplacementChainIr> {
921 let params = Params::from_raw(raw);
922 let db = params.get(keys::DB)?;
923 match db {
924 "ReplaceToken" => Some(ReplacementChainIr::ReplaceToken {
925 token_type: params.get("Type").map(str::to_string),
926 amount_expr: params.get("Amount").map(str::to_string),
927 }),
928 "ReplaceCounter" => Some(ReplacementChainIr::ReplaceCounter {
929 amount_expr: params.get("Amount")?.to_string(),
930 choose_counter: params.has("ChooseCounter"),
931 after_replacement_static_abilities: params
932 .get(keys::SUB_ABILITY)
933 .and_then(|name| svars.get_svar(name))
934 .map(Params::from_raw)
935 .filter(|sub| {
936 sub.get(keys::DB) == Some("ImmediateTrigger") && sub.has("AfterReplacement")
937 })
938 .and_then(|sub| sub.get(keys::EXECUTE).and_then(|name| svars.get_svar(name)))
939 .map(Params::from_raw)
940 .filter(|execute| execute.get(keys::DB) == Some("Effect"))
941 .and_then(|execute| execute.get(keys::STATIC_ABILITIES).map(str::to_string))
942 .map(|names| {
943 names
944 .split(',')
945 .filter_map(|name| svars.get_svar(name.trim()).map(str::to_string))
946 .collect()
947 })
948 .unwrap_or_default(),
949 }),
950 "ReplaceEffect" => {
951 let sub_ability = params
952 .get(keys::SUB_ABILITY)
953 .and_then(|name| svars.get_svar(name))
954 .and_then(|sub_raw| parse_replacement_chain(sub_raw, svars))
955 .map(Box::new);
956 Some(ReplacementChainIr::ReplaceEffect {
957 var_name: params.get("VarName").map(str::to_string),
958 var_type: params.get("VarType").map(str::to_string),
959 var_key: params.get("VarKey").map(str::to_string),
960 var_value: params.get("VarValue").map(str::to_string),
961 sub_ability,
962 })
963 }
964 _ => None,
965 }
966}
967
968pub(crate) fn resolve_replace_with_chain(
969 effect: &ReplacementEffect,
970 source_card: &Card,
971) -> Option<ReplacementChainIr> {
972 effect.ir.replace_with_chain.clone().or_else(|| {
973 effect
974 .replace_with()
975 .and_then(|svar_name| source_card.get_svar(svar_name))
976 .and_then(|raw| parse_replacement_chain(raw, source_card))
977 })
978}
979
980fn parsed_phase_types(value: Option<&str>) -> Vec<PhaseType> {
981 value
982 .into_iter()
983 .flat_map(|raw| raw.split(','))
984 .filter_map(|phase| PhaseType::from_script_name(phase.trim()))
985 .collect()
986}
987
988fn parsed_true(value: Option<&str>) -> bool {
989 matches!(value, Some("True") | Some("true"))
990}
991
992fn parsed_bool(value: Option<&str>) -> Option<bool> {
993 value.map(|raw| matches!(raw, "True" | "true"))
994}
995
996#[cfg(test)]
999mod tests {
1000 use super::*;
1001
1002 #[test]
1005 fn parse_damage_prevention() {
1006 let raw = "R$ Event$ DamageDone | ActiveZones$ Battlefield | ValidCard$ Card.Self | Prevent$ True | Description$ Prevent all damage dealt to ~.";
1007 let re = parse_replacement_effect(raw).expect("should parse");
1008 assert_eq!(re.event, ReplacementType::DamageDone);
1009 assert_eq!(re.layer, ReplacementLayer::Other);
1010 assert!(re.ir.prevent);
1011 assert_eq!(re.active_zones, vec![ZoneType::Battlefield]);
1012 }
1013
1014 #[test]
1015 fn parse_draw_skip() {
1016 let raw = "R$ Event$ Draw | ValidPlayer$ You | Description$ Skip your draw step.";
1017 let re = parse_replacement_effect(raw).expect("should parse");
1018 assert_eq!(re.event, ReplacementType::Draw);
1019 assert_eq!(re.ir.valid_player_text.as_deref(), Some("You"));
1020 assert!(re.active_zones.is_empty());
1021 }
1022
1023 #[test]
1024 fn parse_destroy_replacement() {
1025 let raw = "R$ Event$ Destroy | ValidCard$ Card.Self | Description$ ~ is indestructible.";
1026 let re = parse_replacement_effect(raw).expect("should parse");
1027 assert_eq!(re.event, ReplacementType::Destroy);
1028 assert_eq!(re.ir.valid_card_text.as_deref(), Some("Card.Self"));
1029 }
1030
1031 #[test]
1032 fn parse_moved_exile_instead() {
1033 let raw = "R$ Event$ Moved | Destination$ Graveyard | Origin$ Battlefield | ValidCard$ Card.Self | NewDestination$ Exile | Description$ If ~ would die, exile it instead.";
1034 let re = parse_replacement_effect(raw).expect("should parse");
1035 assert_eq!(re.event, ReplacementType::Moved);
1036 assert_eq!(re.ir.destination_text.as_deref(), Some("Graveyard"));
1037 assert_eq!(re.ir.origin_text.as_deref(), Some("Battlefield"));
1038 assert_eq!(re.ir.new_destination_text.as_deref(), Some("Exile"));
1039 }
1040
1041 #[test]
1042 fn parse_cant_happen_layer() {
1043 let raw = "R$ Event$ Destroy | Layer$ CantHappen | ValidCard$ Card.Self";
1044 let re = parse_replacement_effect(raw).expect("should parse");
1045 assert_eq!(re.layer, ReplacementLayer::CantHappen);
1046 }
1047
1048 #[test]
1049 fn parse_r_colon_prefix() {
1050 let raw = "R: Event$ Draw | ValidPlayer$ You";
1051 let re = parse_replacement_effect(raw).expect("should parse R: prefix");
1052 assert_eq!(re.event, ReplacementType::Draw);
1053 }
1054
1055 #[test]
1056 fn non_replacement_line_returns_none() {
1057 assert!(parse_replacement_effect("AB$ Mana | Cost$ T | Produced$ G").is_none());
1058 assert!(
1059 parse_replacement_effect("S$ Mode$ Continuous | Affected$ Creature.YouControl")
1060 .is_none()
1061 );
1062 assert!(parse_replacement_effect("").is_none());
1063 }
1064
1065 #[test]
1068 fn active_in_zone_empty_means_always() {
1069 let raw = "R$ Event$ Draw | ValidPlayer$ You";
1070 let re = parse_replacement_effect(raw).unwrap();
1071 assert!(re.active_in_zone(ZoneType::Battlefield));
1073 assert!(re.active_in_zone(ZoneType::Hand));
1074 assert!(re.active_in_zone(ZoneType::Graveyard));
1075 }
1076
1077 #[test]
1078 fn active_in_zone_respects_active_zones() {
1079 let raw = "R$ Event$ DamageDone | ActiveZones$ Battlefield | Prevent$ True";
1080 let re = parse_replacement_effect(raw).unwrap();
1081 assert!(re.active_in_zone(ZoneType::Battlefield));
1082 assert!(!re.active_in_zone(ZoneType::Graveyard));
1083 assert!(!re.active_in_zone(ZoneType::Hand));
1084 }
1085
1086 #[test]
1089 fn parse_all_new_event_types() {
1090 for (event_str, expected) in [
1091 ("Tap", ReplacementType::Tap),
1092 ("Untap", ReplacementType::Untap),
1093 ("Mill", ReplacementType::Mill),
1094 ("Scry", ReplacementType::Scry),
1095 ("Explore", ReplacementType::Explore),
1096 ("Cascade", ReplacementType::Cascade),
1097 ("Learn", ReplacementType::Learn),
1098 ("Proliferate", ReplacementType::Proliferate),
1099 ("Transform", ReplacementType::Transform),
1100 ("TurnFaceUp", ReplacementType::TurnFaceUp),
1101 ("RollDice", ReplacementType::RollDice),
1102 ] {
1103 let raw = format!("R$ Event$ {event_str} | Description$ test");
1104 let re = parse_replacement_effect(&raw).expect(&format!("should parse {event_str}"));
1105 assert_eq!(re.event, expected, "failed for {event_str}");
1106 }
1107 }
1108}