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(AbilityKey::Origin, format!("{origin:?}").as_str());
455 node.set_triggering_object(
456 AbilityKey::Destination,
457 format!("{destination:?}").as_str(),
458 );
459 }
460 ReplacementEvent::DamageToCard {
461 target,
462 amount,
463 source,
464 ..
465 } => {
466 let target_csv = target.0.to_string();
469 node.set_triggering_object(AbilityKey::Target, target_csv.as_str());
470 node.set_triggering_object(AbilityKey::Affected, target_csv.as_str());
471 node.set_triggering_object(
472 AbilityKey::DamageAmount,
473 amount.to_string().as_str(),
474 );
475 if let Some(src) = source {
476 let src_csv = src.0.to_string();
477 node.set_triggering_object(AbilityKey::Source, src_csv.as_str());
478 node.set_triggering_object(AbilityKey::DamageSource, src_csv.as_str());
479 }
480 }
481 ReplacementEvent::DamageToPlayer {
482 target,
483 amount,
484 source,
485 ..
486 } => {
487 node.set_triggering_object(
488 AbilityKey::DamageAmount,
489 amount.to_string().as_str(),
490 );
491 if let Some(src) = source {
492 let src_csv = src.0.to_string();
493 node.set_triggering_object(AbilityKey::Source, src_csv.as_str());
494 node.set_triggering_object(AbilityKey::DamageSource, src_csv.as_str());
495 }
496 node.set_triggering_object(
497 AbilityKey::TriggeredPlayer,
498 target.index().to_string().as_str(),
499 );
500 }
501 ReplacementEvent::Destroy { target } => {
502 let target_csv = target.0.to_string();
504 node.set_triggering_object(AbilityKey::Card, target_csv.as_str());
505 node.set_triggering_object(AbilityKey::Affected, target_csv.as_str());
506 }
507 ReplacementEvent::AddCounter {
508 target,
509 counter_type,
510 count,
511 ..
512 } => {
513 let target_csv = match target {
516 crate::agent::GameEntity::Card(card) => card.0.to_string(),
517 crate::agent::GameEntity::Player(player) => player.0.to_string(),
518 };
519 match target {
520 crate::agent::GameEntity::Card(_) => {
521 node.set_triggering_object(AbilityKey::Card, target_csv.as_str());
522 }
523 crate::agent::GameEntity::Player(_) => {
524 node.set_triggering_object(AbilityKey::Player, target_csv.as_str());
525 }
526 }
527 node.set_triggering_object(AbilityKey::Affected, target_csv.as_str());
528 node.set_triggering_object(AbilityKey::Object, target_csv.as_str());
529 node.set_triggering_object(
530 AbilityKey::CounterMap,
531 format!("{counter_type:?}:{count}").as_str(),
532 );
533 }
534 ReplacementEvent::Draw {
535 player,
536 extra_draws,
537 ..
538 } => {
539 let pid = player.index().to_string();
542 node.set_triggering_object(AbilityKey::TriggeredPlayer, pid.as_str());
543 node.set_triggering_object(AbilityKey::Affected, pid.as_str());
544 node.set_triggering_object(AbilityKey::Num, extra_draws.to_string().as_str());
545 }
546 ReplacementEvent::DrawCards { player, count } => {
547 let pid = player.index().to_string();
549 node.set_triggering_object(AbilityKey::TriggeredPlayer, pid.as_str());
550 node.set_triggering_object(AbilityKey::Affected, pid.as_str());
551 node.set_triggering_object(AbilityKey::Num, count.to_string().as_str());
552 }
553 ReplacementEvent::CreateToken { player, count, .. } => {
554 let pid = player.index().to_string();
557 node.set_triggering_object(AbilityKey::TriggeredPlayer, pid.as_str());
558 node.set_triggering_object(AbilityKey::Affected, pid.as_str());
559 node.set_triggering_object(AbilityKey::TokenNum, count.to_string().as_str());
560 }
561 ReplacementEvent::GainLife { player, amount }
562 | ReplacementEvent::PayLife { player, amount }
563 | ReplacementEvent::LifeReduced { player, amount, .. } => {
564 let pid = player.index().to_string();
565 node.set_triggering_object(AbilityKey::TriggeredPlayer, pid.as_str());
566 node.set_triggering_object(AbilityKey::Affected, pid.as_str());
567 node.set_triggering_object(AbilityKey::LifeAmount, amount.to_string().as_str());
568 }
569 ReplacementEvent::Mill { player, count }
570 | ReplacementEvent::Scry { player, count }
571 | ReplacementEvent::Proliferate { player, count }
572 | ReplacementEvent::CopySpell { player, count } => {
573 let pid = player.index().to_string();
574 node.set_triggering_object(AbilityKey::TriggeredPlayer, pid.as_str());
575 node.set_triggering_object(AbilityKey::Affected, pid.as_str());
576 node.set_triggering_object(AbilityKey::Num, count.to_string().as_str());
577 }
578 ReplacementEvent::BeginTurn { player }
579 | ReplacementEvent::BeginPhase { player, .. }
580 | ReplacementEvent::DeclareBlocker { player }
581 | ReplacementEvent::RollPlanarDice { player }
582 | ReplacementEvent::PlanarDiceResult { player }
583 | ReplacementEvent::LoseMana { player }
584 | ReplacementEvent::GameLoss { player, .. }
585 | ReplacementEvent::GameWin { player }
586 | ReplacementEvent::Cascade { player }
587 | ReplacementEvent::Learn { player }
588 | ReplacementEvent::Planeswalk { player }
589 | ReplacementEvent::SetInMotion { player }
590 | ReplacementEvent::AssembleContraption { player } => {
591 let pid = player.index().to_string();
592 node.set_triggering_object(AbilityKey::TriggeredPlayer, pid.as_str());
593 node.set_triggering_object(AbilityKey::Affected, pid.as_str());
594 }
595 ReplacementEvent::Counter { card }
596 | ReplacementEvent::Tap { card }
597 | ReplacementEvent::Untap { card, .. }
598 | ReplacementEvent::Explore { card }
599 | ReplacementEvent::Transform { card }
600 | ReplacementEvent::TurnFaceUp { card }
601 | ReplacementEvent::AssignDealDamage { card } => {
602 let csv = card.0.to_string();
603 node.set_triggering_object(AbilityKey::Card, csv.as_str());
604 node.set_triggering_object(AbilityKey::Affected, csv.as_str());
605 }
606 ReplacementEvent::DealtDamage {
607 target,
608 amount,
609 source,
610 } => {
611 let target_csv = target.0.to_string();
612 node.set_triggering_object(AbilityKey::Target, target_csv.as_str());
613 node.set_triggering_object(AbilityKey::Affected, target_csv.as_str());
614 node.set_triggering_object(
615 AbilityKey::DamageAmount,
616 amount.to_string().as_str(),
617 );
618 if let Some(src) = source {
619 node.set_triggering_object(
620 AbilityKey::DamageSource,
621 src.0.to_string().as_str(),
622 );
623 }
624 }
625 ReplacementEvent::RemoveCounter {
626 target,
627 counter_type,
628 count,
629 } => {
630 let target_csv = target.0.to_string();
631 node.set_triggering_object(AbilityKey::Card, target_csv.as_str());
632 node.set_triggering_object(
633 AbilityKey::CounterMap,
634 format!("{counter_type:?}:{count}").as_str(),
635 );
636 }
637 ReplacementEvent::Attached { card, target } => {
638 node.set_triggering_object(AbilityKey::Card, card.0.to_string().as_str());
639 node.set_triggering_object(AbilityKey::Target, target.0.to_string().as_str());
640 node.set_triggering_object(AbilityKey::Affected, target.0.to_string().as_str());
641 }
642 ReplacementEvent::ProduceMana {
643 source,
644 activator,
645 mana,
646 } => {
647 node.set_triggering_object(AbilityKey::Source, source.0.to_string().as_str());
648 node.set_triggering_object(AbilityKey::Card, source.0.to_string().as_str());
649 node.set_triggering_object(
650 AbilityKey::TriggeredPlayer,
651 activator.index().to_string().as_str(),
652 );
653 node.set_triggering_object(AbilityKey::Produced, mana.as_str());
654 }
655 ReplacementEvent::RollDice {
656 player,
657 sides,
658 number,
659 ..
660 } => {
661 node.set_triggering_object(
662 AbilityKey::TriggeredPlayer,
663 player.index().to_string().as_str(),
664 );
665 node.set_triggering_object(AbilityKey::Num, number.to_string().as_str());
666 node.set_triggering_object(AbilityKey::Sides, sides.to_string().as_str());
667 }
668 }
669 current = node.get_sub_ability_mut();
670 }
671 }
672
673 pub fn mode_check(&self, event: &ReplacementType) -> bool {
680 if self.event == *event {
681 return true;
682 }
683 if self.event == ReplacementType::AddCounter && *event == ReplacementType::Moved {
686 return self.ir.counter_map;
687 }
688 false
689 }
690}
691
692impl ReplacementEffectIr {
693 fn from_params(params: &Params) -> Self {
694 Self {
695 card_trait_requirements: CardTraitRequirementsIr::from_key_values(
696 params.iter(),
697 params.selector_untracked(keys::IS_PRESENT).cloned(),
698 params.selector_untracked("IsPresent2").cloned(),
699 ),
700 replace_with: params.get(keys::REPLACE_WITH).map(str::to_string),
701 description_text: params.get(keys::DESCRIPTION).map(str::to_string),
702 player_turn_text: params.get(keys::PLAYER_TURN).map(str::to_string),
703 new_destination_text: params.get(keys::NEW_DESTINATION).map(str::to_string),
704 new_destination_zone: parsed_zone_type(params.get(keys::NEW_DESTINATION)),
705 origin_text: params.get(keys::ORIGIN).map(str::to_string),
706 origin_zone: parsed_zone_type(params.get(keys::ORIGIN)),
707 destination_text: params.get(keys::DESTINATION).map(str::to_string),
708 destination_zone: parsed_zone_type(params.get(keys::DESTINATION)),
709 exclude_destination_text: params.get("ExcludeDestination").map(str::to_string),
710 valid_card_text: params.get(keys::VALID_CARD).map(str::to_string),
711 valid_card_selector: params.selector_cloned(keys::VALID_CARD),
712 valid_player_text: params.get(keys::VALID_PLAYER).map(str::to_string),
713 valid_player_selector: params.selector_cloned(keys::VALID_PLAYER),
714 valid_object_selector: params.selector_cloned("ValidObject"),
715 valid_cause_text: params.get(keys::VALID_CAUSE).map(str::to_string),
716 valid_target_selector: params.selector_cloned(keys::VALID_TARGET),
717 valid_source_selector: params.selector_cloned(keys::VALID_SOURCE),
718 valid_activator_text: params.get(keys::VALID_ACTIVATOR).map(str::to_string),
719 valid_step_turn_to_controller_text: params
720 .get("ValidStepTurnToController")
721 .map(str::to_string),
722 valid_explorer_text: params.get(keys::VALID_EXPLORER).map(str::to_string),
723 valid_counter_type_text: params.get(keys::VALID_COUNTER_TYPE).map(str::to_string),
724 valid_lose_reason_text: params.get(keys::VALID_LOSE_REASON).map(str::to_string),
725 valid_sides_text: params.get(keys::VALID_SIDES).map(str::to_string),
726 active_phases: parsed_phase_types(
727 params
728 .get(keys::PHASE)
729 .or_else(|| params.get(keys::ACTIVE_PHASES)),
730 ),
731 amount_text: params.get(keys::AMOUNT).map(str::to_string),
732 damage_amount_text: params.get(keys::DAMAGE_AMOUNT).map(str::to_string),
733 result_text: params.get(keys::RESULT).map(str::to_string),
734 number_text: params.get(keys::NUMBER).map(str::to_string),
735 dredge_amount: params.get("DredgeAmount").and_then(|s| s.parse().ok()),
736 skip: params.has(keys::SKIP),
737 prevent: parsed_true(params.get(keys::PREVENT)),
738 optional: params.has(keys::OPTIONAL),
739 effect_only: params.has("EffectOnly"),
740 discard: parsed_bool(params.get("Discard")),
741 flashback_cast: parsed_bool(params.get("FlashbackCast")),
742 harmonize_cast: parsed_bool(params.get("HarmonizeCast")),
743 not_first_card_in_draw_step: parsed_true(params.get("NotFirstCardInDrawStep")),
744 exiled_with_effect_source: params.has("ExiledWithEffectSource"),
745 is_combat: parsed_bool(params.get(keys::IS_COMBAT)),
746 is_damage: parsed_bool(params.get(keys::IS_DAMAGE)),
747 max_speed: parsed_bool(params.get("MaxSpeed")),
748 replacement_result: params.get("ReplacementResult").map(str::to_string),
749 optional_decider_text: params.get(keys::OPTIONAL_DECIDER).map(str::to_string),
750 replace_mana_text: params.get(keys::REPLACE_MANA).map(str::to_string),
751 replace_type_text: params.get(keys::REPLACE_TYPE).map(str::to_string),
752 replace_color_text: params.get(keys::REPLACE_COLOR).map(str::to_string),
753 replace_only_text: params.get(keys::REPLACE_ONLY).map(str::to_string),
754 replace_amount_text: params.get(keys::REPLACE_AMOUNT).map(str::to_string),
755 valid_lki_text: params.get("ValidLKI").map(str::to_string),
756 counter_map: params.get("CounterMap").is_some(),
757 replace_with_chain: None,
758 }
759 }
760}
761
762fn append_shield_remaining(mut desc: String, rep_sa: &SpellAbility, host: &Card) -> String {
769 use crate::ability::api_type::ApiType;
770
771 let api = match rep_sa.api {
772 Some(a) => a,
773 None => return desc,
774 };
775
776 let (param_value, default_one) = match api {
777 ApiType::ReplaceDamage => match rep_sa.ir.amount.as_deref() {
778 Some(v) => (v.to_string(), false),
779 None => return desc,
780 },
781 ApiType::ReplaceSplitDamage => (
782 rep_sa
783 .ir
784 .var_name_text
785 .clone()
786 .unwrap_or_else(|| "1".to_string()),
787 true,
788 ),
789 _ => return desc,
790 };
791
792 if param_value.chars().all(|c| c.is_ascii_digit()) {
795 if default_one && param_value == "1" {
796 desc.push_str(" \nShields remain: 1");
797 }
798 return desc;
799 }
800
801 if let Some(resolved) = host.get_svar(¶m_value) {
803 if let Some(rest) = resolved.strip_prefix("Number$") {
804 desc.push_str(" \nShields remain: ");
805 desc.push_str(rest);
806 }
807 }
808 desc
809}
810
811pub fn zone_matches(expr: &str, zone: ZoneType) -> bool {
821 expr.split(',').any(|part| match part.trim() {
822 "Battlefield" => zone == ZoneType::Battlefield,
823 "Graveyard" => zone == ZoneType::Graveyard,
824 "Hand" => zone == ZoneType::Hand,
825 "Library" => zone == ZoneType::Library,
826 "Exile" => zone == ZoneType::Exile,
827 "Command" => zone == ZoneType::Command,
828 "Stack" => zone == ZoneType::Stack,
829 _ => false,
830 })
831}
832
833pub fn parse_replacement_effect(raw: &str) -> Option<ReplacementEffect> {
850 let trimmed = raw.trim();
851 let body = if let Some(rest) = trimmed.strip_prefix("R$ ") {
856 rest
857 } else if let Some(rest) = trimmed.strip_prefix("R:") {
858 rest.trim_start()
859 } else {
860 trimmed
861 };
862
863 let params = Params::from_raw(body);
865
866 let event = match params.get(keys::EVENT) {
867 Some(s) => ReplacementType::smart_value_of(s),
868 None => return None,
869 };
870
871 let layer = params
873 .get(keys::LAYER)
874 .and_then(ReplacementLayer::smart_value_of)
875 .unwrap_or(ReplacementLayer::Other);
876
877 let active_zones = params
879 .get(keys::ACTIVE_ZONES)
880 .map(parse_zone_list)
881 .unwrap_or_default();
882
883 Some(ReplacementEffect::new(event, layer, params, active_zones))
884}
885
886pub(super) fn parse_zone_list(s: &str) -> Vec<ZoneType> {
888 s.split([',', ' '])
889 .filter_map(|tok| match tok.trim() {
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 _ => None,
897 })
898 .collect()
899}
900
901fn parsed_zone_type(value: Option<&str>) -> Option<ZoneType> {
902 match value? {
903 "Battlefield" => Some(ZoneType::Battlefield),
904 "Graveyard" => Some(ZoneType::Graveyard),
905 "Hand" => Some(ZoneType::Hand),
906 "Library" => Some(ZoneType::Library),
907 "Exile" => Some(ZoneType::Exile),
908 "Command" => Some(ZoneType::Command),
909 "Stack" => Some(ZoneType::Stack),
910 _ => None,
911 }
912}
913
914pub(crate) fn parse_replacement_chain(
915 raw: &str,
916 svars: &impl HasSVars,
917) -> Option<ReplacementChainIr> {
918 let params = Params::from_raw(raw);
919 let db = params.get(keys::DB)?;
920 match db {
921 "ReplaceToken" => Some(ReplacementChainIr::ReplaceToken {
922 token_type: params.get("Type").map(str::to_string),
923 amount_expr: params.get("Amount").map(str::to_string),
924 }),
925 "ReplaceCounter" => Some(ReplacementChainIr::ReplaceCounter {
926 amount_expr: params.get("Amount")?.to_string(),
927 choose_counter: params.has("ChooseCounter"),
928 after_replacement_static_abilities: params
929 .get(keys::SUB_ABILITY)
930 .and_then(|name| svars.get_svar(name))
931 .map(Params::from_raw)
932 .filter(|sub| {
933 sub.get(keys::DB) == Some("ImmediateTrigger") && sub.has("AfterReplacement")
934 })
935 .and_then(|sub| sub.get(keys::EXECUTE).and_then(|name| svars.get_svar(name)))
936 .map(Params::from_raw)
937 .filter(|execute| execute.get(keys::DB) == Some("Effect"))
938 .and_then(|execute| execute.get(keys::STATIC_ABILITIES).map(str::to_string))
939 .map(|names| {
940 names
941 .split(',')
942 .filter_map(|name| svars.get_svar(name.trim()).map(str::to_string))
943 .collect()
944 })
945 .unwrap_or_default(),
946 }),
947 "ReplaceEffect" => {
948 let sub_ability = params
949 .get(keys::SUB_ABILITY)
950 .and_then(|name| svars.get_svar(name))
951 .and_then(|sub_raw| parse_replacement_chain(sub_raw, svars))
952 .map(Box::new);
953 Some(ReplacementChainIr::ReplaceEffect {
954 var_name: params.get("VarName").map(str::to_string),
955 var_type: params.get("VarType").map(str::to_string),
956 var_key: params.get("VarKey").map(str::to_string),
957 var_value: params.get("VarValue").map(str::to_string),
958 sub_ability,
959 })
960 }
961 _ => None,
962 }
963}
964
965pub(crate) fn resolve_replace_with_chain(
966 effect: &ReplacementEffect,
967 source_card: &Card,
968) -> Option<ReplacementChainIr> {
969 effect.ir.replace_with_chain.clone().or_else(|| {
970 effect
971 .replace_with()
972 .and_then(|svar_name| source_card.get_svar(svar_name))
973 .and_then(|raw| parse_replacement_chain(raw, source_card))
974 })
975}
976
977fn parsed_phase_types(value: Option<&str>) -> Vec<PhaseType> {
978 value
979 .into_iter()
980 .flat_map(|raw| raw.split(','))
981 .filter_map(|phase| PhaseType::from_script_name(phase.trim()))
982 .collect()
983}
984
985fn parsed_true(value: Option<&str>) -> bool {
986 matches!(value, Some("True") | Some("true"))
987}
988
989fn parsed_bool(value: Option<&str>) -> Option<bool> {
990 value.map(|raw| matches!(raw, "True" | "true"))
991}
992
993#[cfg(test)]
996mod tests {
997 use super::*;
998
999 #[test]
1002 fn parse_damage_prevention() {
1003 let raw = "R$ Event$ DamageDone | ActiveZones$ Battlefield | ValidCard$ Card.Self | Prevent$ True | Description$ Prevent all damage dealt to ~.";
1004 let re = parse_replacement_effect(raw).expect("should parse");
1005 assert_eq!(re.event, ReplacementType::DamageDone);
1006 assert_eq!(re.layer, ReplacementLayer::Other);
1007 assert!(re.ir.prevent);
1008 assert_eq!(re.active_zones, vec![ZoneType::Battlefield]);
1009 }
1010
1011 #[test]
1012 fn parse_draw_skip() {
1013 let raw = "R$ Event$ Draw | ValidPlayer$ You | Description$ Skip your draw step.";
1014 let re = parse_replacement_effect(raw).expect("should parse");
1015 assert_eq!(re.event, ReplacementType::Draw);
1016 assert_eq!(re.ir.valid_player_text.as_deref(), Some("You"));
1017 assert!(re.active_zones.is_empty());
1018 }
1019
1020 #[test]
1021 fn parse_destroy_replacement() {
1022 let raw = "R$ Event$ Destroy | ValidCard$ Card.Self | Description$ ~ is indestructible.";
1023 let re = parse_replacement_effect(raw).expect("should parse");
1024 assert_eq!(re.event, ReplacementType::Destroy);
1025 assert_eq!(re.ir.valid_card_text.as_deref(), Some("Card.Self"));
1026 }
1027
1028 #[test]
1029 fn parse_moved_exile_instead() {
1030 let raw = "R$ Event$ Moved | Destination$ Graveyard | Origin$ Battlefield | ValidCard$ Card.Self | NewDestination$ Exile | Description$ If ~ would die, exile it instead.";
1031 let re = parse_replacement_effect(raw).expect("should parse");
1032 assert_eq!(re.event, ReplacementType::Moved);
1033 assert_eq!(re.ir.destination_text.as_deref(), Some("Graveyard"));
1034 assert_eq!(re.ir.origin_text.as_deref(), Some("Battlefield"));
1035 assert_eq!(re.ir.new_destination_text.as_deref(), Some("Exile"));
1036 }
1037
1038 #[test]
1039 fn parse_cant_happen_layer() {
1040 let raw = "R$ Event$ Destroy | Layer$ CantHappen | ValidCard$ Card.Self";
1041 let re = parse_replacement_effect(raw).expect("should parse");
1042 assert_eq!(re.layer, ReplacementLayer::CantHappen);
1043 }
1044
1045 #[test]
1046 fn parse_r_colon_prefix() {
1047 let raw = "R: Event$ Draw | ValidPlayer$ You";
1048 let re = parse_replacement_effect(raw).expect("should parse R: prefix");
1049 assert_eq!(re.event, ReplacementType::Draw);
1050 }
1051
1052 #[test]
1053 fn non_replacement_line_returns_none() {
1054 assert!(parse_replacement_effect("AB$ Mana | Cost$ T | Produced$ G").is_none());
1055 assert!(
1056 parse_replacement_effect("S$ Mode$ Continuous | Affected$ Creature.YouControl")
1057 .is_none()
1058 );
1059 assert!(parse_replacement_effect("").is_none());
1060 }
1061
1062 #[test]
1065 fn active_in_zone_empty_means_always() {
1066 let raw = "R$ Event$ Draw | ValidPlayer$ You";
1067 let re = parse_replacement_effect(raw).unwrap();
1068 assert!(re.active_in_zone(ZoneType::Battlefield));
1070 assert!(re.active_in_zone(ZoneType::Hand));
1071 assert!(re.active_in_zone(ZoneType::Graveyard));
1072 }
1073
1074 #[test]
1075 fn active_in_zone_respects_active_zones() {
1076 let raw = "R$ Event$ DamageDone | ActiveZones$ Battlefield | Prevent$ True";
1077 let re = parse_replacement_effect(raw).unwrap();
1078 assert!(re.active_in_zone(ZoneType::Battlefield));
1079 assert!(!re.active_in_zone(ZoneType::Graveyard));
1080 assert!(!re.active_in_zone(ZoneType::Hand));
1081 }
1082
1083 #[test]
1086 fn parse_all_new_event_types() {
1087 for (event_str, expected) in [
1088 ("Tap", ReplacementType::Tap),
1089 ("Untap", ReplacementType::Untap),
1090 ("Mill", ReplacementType::Mill),
1091 ("Scry", ReplacementType::Scry),
1092 ("Explore", ReplacementType::Explore),
1093 ("Cascade", ReplacementType::Cascade),
1094 ("Learn", ReplacementType::Learn),
1095 ("Proliferate", ReplacementType::Proliferate),
1096 ("Transform", ReplacementType::Transform),
1097 ("TurnFaceUp", ReplacementType::TurnFaceUp),
1098 ("RollDice", ReplacementType::RollDice),
1099 ] {
1100 let raw = format!("R$ Event$ {event_str} | Description$ test");
1101 let re = parse_replacement_effect(&raw).expect(&format!("should parse {event_str}"));
1102 assert_eq!(re.event, expected, "failed for {event_str}");
1103 }
1104 }
1105}