Skip to main content

manabrew_engine/card/
card_state.rs

1//! Java-parity helpers for `CardState` behavior.
2//!
3//! Rust now exposes a concrete `CardState` contract for Java parity while
4//! still keeping adapter helpers for the existing `Card`-centric engine.
5
6use std::collections::{HashMap, HashSet};
7
8use forge_foundation::{CardStateName, CardTypeLine, ColorSet, ManaCost};
9
10use crate::ability::activated::parse_activated_ability;
11use crate::card::trait_card_trait_changes::CardTraitChanges as ICardTraitChanges;
12use crate::card::{card_copy_service, card_property, Card};
13use crate::core::HasSVars;
14use crate::game_object::GameObject;
15use crate::ids::PlayerId;
16use crate::keyword::keyword_collection::KeywordCollection;
17use crate::keyword::keyword_interface::KeywordInterface;
18use crate::keyword::trait_keywords_change::KeywordsChange as IKeywordsChange;
19use crate::replacement::ReplacementEffect;
20use crate::spellability::SpellAbility;
21use crate::staticability::StaticAbility;
22use crate::trigger::Trigger;
23use crate::util::{HasName, ITranslatable};
24
25use crate::card_trait_base::CardTraitBase;
26
27pub type CardType = CardTypeLine;
28pub type CardTypeView = CardTypeLine;
29pub type FCollection<T> = Vec<T>;
30pub type FCollectionView<T> = Vec<T>;
31
32#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
33pub enum CardRarity {
34    #[default]
35    Unknown,
36}
37
38pub struct CardEdition;
39impl CardEdition {
40    pub const UNKNOWN_CODE: &'static str = "UNKNOWN";
41}
42
43#[derive(Debug, Clone, Default)]
44pub struct LandTraitChanges;
45
46impl LandTraitChanges {
47    pub fn new() -> Self {
48        Self
49    }
50}
51
52impl ICardTraitChanges for LandTraitChanges {
53    fn change_text(&mut self) {}
54    fn copy(&self, _host: crate::ids::CardId, _lki: bool) -> Box<dyn ICardTraitChanges> {
55        Box::new(self.clone())
56    }
57}
58
59impl IKeywordsChange for LandTraitChanges {
60    fn apply_keywords(&self, _list: &mut KeywordCollection) {}
61    fn copy(&self) -> Box<dyn IKeywordsChange> {
62        Box::new(self.clone())
63    }
64}
65
66/// Port of Java `CardState`.
67#[derive(Debug, Clone)]
68pub struct CardState {
69    pub state_name: CardStateName,
70    pub name: String,
71    pub r#type: CardType,
72    pub changed_type: Option<CardTypeView>,
73    pub mana_cost: ManaCost,
74    pub perpetual_adjusted_mana_cost: Option<ManaCost>,
75    pub color: ColorSet,
76    pub oracle_text: String,
77    pub functional_variant_name: Option<String>,
78    pub flavor_name: Option<String>,
79    pub base_power: i32,
80    pub base_toughness: i32,
81    pub base_power_string: Option<String>,
82    pub base_toughness_string: Option<String>,
83    pub base_loyalty: String,
84    pub base_defense: String,
85    pub intrinsic_keywords: KeywordCollection,
86    pub attraction_lights: Option<HashSet<i32>>,
87    pub abilities: FCollection<SpellAbility>,
88    pub triggers: FCollection<Trigger>,
89    pub replacement_effects: FCollection<ReplacementEffect>,
90    pub static_abilities: FCollection<StaticAbility>,
91    pub image_key: String,
92    pub s_vars: HashMap<String, String>,
93    pub ability_for_trigger: HashMap<String, SpellAbility>,
94    pub cached_keywords: KeywordCollection,
95    pub rarity: CardRarity,
96    pub set_code: String,
97    pub card: Card,
98    pub land_ability: Option<SpellAbility>,
99    pub aura_ability: Option<SpellAbility>,
100    pub permanent_ability: Option<SpellAbility>,
101    pub loyalty_rep: Option<ReplacementEffect>,
102    pub defense_rep: Option<ReplacementEffect>,
103    pub saga_rep: Option<ReplacementEffect>,
104    pub adventure_rep: Option<ReplacementEffect>,
105    pub omen_rep: Option<ReplacementEffect>,
106    pub manifest_up: Option<SpellAbility>,
107    pub cloak_up: Option<SpellAbility>,
108    pub land_trait_changes: LandTraitChanges,
109}
110
111impl CardState {
112    pub fn new(card: Card, name: CardStateName) -> Self {
113        Self {
114            state_name: name,
115            name: String::new(),
116            r#type: CardTypeLine::default(),
117            changed_type: None,
118            mana_cost: ManaCost::no_cost(),
119            perpetual_adjusted_mana_cost: None,
120            color: ColorSet::COLORLESS,
121            oracle_text: String::new(),
122            functional_variant_name: None,
123            flavor_name: None,
124            base_power: 0,
125            base_toughness: 0,
126            base_power_string: None,
127            base_toughness_string: None,
128            base_loyalty: String::new(),
129            base_defense: String::new(),
130            intrinsic_keywords: KeywordCollection::new(),
131            attraction_lights: None,
132            abilities: Vec::new(),
133            triggers: Vec::new(),
134            replacement_effects: Vec::new(),
135            static_abilities: Vec::new(),
136            image_key: String::new(),
137            s_vars: HashMap::new(),
138            ability_for_trigger: HashMap::new(),
139            cached_keywords: KeywordCollection::new(),
140            rarity: CardRarity::Unknown,
141            set_code: CardEdition::UNKNOWN_CODE.to_string(),
142            card,
143            land_ability: None,
144            aura_ability: None,
145            permanent_ability: None,
146            loyalty_rep: None,
147            defense_rep: None,
148            saga_rep: None,
149            adventure_rep: None,
150            omen_rep: None,
151            manifest_up: None,
152            cloak_up: None,
153            land_trait_changes: LandTraitChanges::new(),
154        }
155    }
156
157    pub fn get_card(&self) -> &Card {
158        &self.card
159    }
160    pub fn get_name(&self) -> &str {
161        &self.name
162    }
163    pub fn set_name(&mut self, name: String) {
164        self.name = name;
165    }
166    pub fn get_state_name(&self) -> CardStateName {
167        self.state_name
168    }
169    pub fn get_type_with_changes(&self) -> &CardTypeView {
170        self.changed_type.as_ref().unwrap_or(&self.r#type)
171    }
172    pub fn update_types(&mut self) {
173        self.changed_type = Some(self.r#type.clone());
174    }
175    pub fn update_types_for_view(&self) {}
176    pub fn get_type(&self) -> &CardTypeView {
177        &self.r#type
178    }
179    pub fn add_type(&mut self, type_value: String) {
180        self.r#type.add_type(&type_value);
181    }
182    pub fn add_types<I: IntoIterator<Item = String>>(&mut self, type_values: I) {
183        for t in type_values {
184            self.r#type.add_type(&t);
185        }
186    }
187    pub fn set_type(&mut self, type_value: CardType) {
188        self.r#type = type_value;
189    }
190    pub fn remove_card_types_with_sanitize(&mut self, _sanitize: bool) {
191        self.r#type.core_types.clear();
192        self.r#type.subtypes.clear();
193    }
194    pub fn get_mana_cost(&self) -> &ManaCost {
195        &self.mana_cost
196    }
197    pub fn set_mana_cost(&mut self, mana_cost: ManaCost) {
198        self.mana_cost = mana_cost;
199    }
200    pub fn calculate_perpetual_adjusted_mana_cost_for_state(&mut self) {}
201    pub fn get_perpetual_adjusted_mana_cost(&self) -> &ManaCost {
202        self.perpetual_adjusted_mana_cost
203            .as_ref()
204            .unwrap_or(&self.mana_cost)
205    }
206    pub fn get_color(&self) -> ColorSet {
207        self.color
208    }
209    pub fn add_color(&mut self, color: ColorSet) {
210        self.color = self.color.union(color);
211    }
212    pub fn set_color(&mut self, color: ColorSet) {
213        self.color = color;
214    }
215    pub fn get_oracle_text(&self) -> &str {
216        &self.oracle_text
217    }
218    pub fn set_oracle_text(&mut self, oracle_text: String) {
219        self.oracle_text = oracle_text;
220    }
221    pub fn get_functional_variant_name(&self) -> Option<&str> {
222        self.functional_variant_name.as_deref()
223    }
224    pub fn set_functional_variant_name(&mut self, functional_variant_name: Option<String>) {
225        self.functional_variant_name = functional_variant_name.filter(|s| !s.is_empty());
226    }
227    pub fn get_flavor_name(&self) -> Option<&str> {
228        self.flavor_name.as_deref()
229    }
230    pub fn set_flavor_name(&mut self, flavor_name: Option<String>) {
231        self.flavor_name = flavor_name;
232    }
233    pub fn get_base_power(&self) -> i32 {
234        self.base_power
235    }
236    pub fn set_base_power(&mut self, base_power: i32) {
237        self.base_power = base_power;
238    }
239    pub fn get_base_toughness(&self) -> i32 {
240        self.base_toughness
241    }
242    pub fn set_base_toughness(&mut self, base_toughness: i32) {
243        self.base_toughness = base_toughness;
244    }
245    pub fn get_base_power_string(&self) -> Option<&str> {
246        self.base_power_string.as_deref()
247    }
248    pub fn get_base_toughness_string(&self) -> Option<&str> {
249        self.base_toughness_string.as_deref()
250    }
251    pub fn set_base_power_string(&mut self, value: Option<String>) {
252        self.base_power_string = value;
253    }
254    pub fn set_base_toughness_string(&mut self, value: Option<String>) {
255        self.base_toughness_string = value;
256    }
257    pub fn get_base_loyalty(&self) -> &str {
258        &self.base_loyalty
259    }
260    pub fn set_base_loyalty(&mut self, value: String) {
261        self.base_loyalty = value;
262    }
263    pub fn get_base_defense(&self) -> &str {
264        &self.base_defense
265    }
266    pub fn set_base_defense(&mut self, value: String) {
267        self.base_defense = value;
268    }
269    pub fn get_attraction_lights(&self) -> Option<&HashSet<i32>> {
270        self.attraction_lights.as_ref()
271    }
272    pub fn set_attraction_lights(&mut self, attraction_lights: Option<HashSet<i32>>) {
273        self.attraction_lights = attraction_lights;
274    }
275    pub fn get_cached_keywords(
276        &self,
277    ) -> Vec<&crate::keyword::keyword_instance::KeywordInstanceData> {
278        self.cached_keywords.get_values()
279    }
280    pub fn set_cached_keywords(&mut self, collection: KeywordCollection) {
281        self.cached_keywords = collection;
282    }
283    pub fn has_keyword_enum(&self, key: crate::keyword::keyword_instance::Keyword) -> bool {
284        self.cached_keywords.contains_keyword(key)
285    }
286    pub fn get_intrinsic_keywords(
287        &self,
288    ) -> Vec<&crate::keyword::keyword_instance::KeywordInstanceData> {
289        self.intrinsic_keywords.get_values()
290    }
291    pub fn has_intrinsic_keyword_state(&self, keyword: &str) -> bool {
292        self.intrinsic_keywords.contains(keyword)
293    }
294    pub fn set_intrinsic_keywords(
295        &mut self,
296        _intrinsic_keywords: Vec<KeywordInterface>,
297        _lki: bool,
298    ) {
299    }
300    pub fn update_keywords_cache_for_state(&mut self) {
301        self.cached_keywords = self.intrinsic_keywords.clone();
302    }
303    pub fn add_intrinsic_keyword_with_init_traits(
304        &mut self,
305        keyword: String,
306        _init_traits: bool,
307    ) -> Option<KeywordInterface> {
308        if self.intrinsic_keywords.add(&keyword) {
309            Some(KeywordInterface::new(
310                crate::keyword::Keyword::smart_value_of(&keyword),
311                keyword,
312            ))
313        } else {
314            None
315        }
316    }
317    pub fn add_intrinsic_keywords_for_state(
318        &mut self,
319        keywords: Vec<String>,
320        init_traits: bool,
321    ) -> bool {
322        let mut changed = false;
323        for k in keywords {
324            changed |= self
325                .add_intrinsic_keyword_with_init_traits(k, init_traits)
326                .is_some();
327        }
328        changed
329    }
330    pub fn remove_intrinsic_keyword_for_state(&mut self, keyword: &str) -> bool {
331        self.intrinsic_keywords.remove(keyword)
332    }
333    pub fn get_spell_abilities(&self) -> FCollectionView<SpellAbility> {
334        crate::perf::increment(crate::perf::Metric::CardStateCollectionClones, 1);
335        self.abilities.clone()
336    }
337    pub fn get_mana_abilities(&self) -> FCollectionView<SpellAbility> {
338        crate::perf::increment(crate::perf::Metric::CardStateCollectionClones, 1);
339        self.abilities
340            .clone()
341            .into_iter()
342            .filter(|sa| sa.is_mana_ability)
343            .collect()
344    }
345    pub fn get_non_mana_abilities(&self) -> FCollectionView<SpellAbility> {
346        crate::perf::increment(crate::perf::Metric::CardStateCollectionClones, 1);
347        self.abilities
348            .clone()
349            .into_iter()
350            .filter(|sa| !sa.is_mana_ability)
351            .collect()
352    }
353    pub fn update_spell_abilities(&self, _new_col: &mut FCollection<SpellAbility>) {}
354    pub fn get_land_trait_changes(&self) -> &LandTraitChanges {
355        &self.land_trait_changes
356    }
357    pub fn get_intrinsic_spell_abilities(&self) -> Vec<SpellAbility> {
358        crate::perf::increment(crate::perf::Metric::CardStateCollectionClones, 1);
359        self.abilities
360            .clone()
361            .into_iter()
362            .filter(|sa| sa.is_activated || sa.is_trigger || sa.is_spell)
363            .collect()
364    }
365    pub fn get_first_ability(&self) -> Option<SpellAbility> {
366        self.get_intrinsic_spell_abilities().into_iter().next()
367    }
368    pub fn get_first_spell_ability(&self) -> Option<SpellAbility> {
369        self.get_non_mana_abilities().into_iter().next()
370    }
371    pub fn get_first_spell_ability_with_fallback(&self) -> Option<SpellAbility> {
372        self.get_first_spell_ability()
373            .or_else(|| self.get_first_ability())
374    }
375    pub fn get_aura_spell(&self) -> Option<SpellAbility> {
376        self.aura_ability.clone()
377    }
378    pub fn has_spell_ability_id(&self, id: i32) -> bool {
379        self.get_spell_abilities()
380            .iter()
381            .any(|sa| sa.source_trigger_id == Some(id as u32))
382    }
383    pub fn add_spell_ability_state(&mut self, ability: SpellAbility) -> bool {
384        self.abilities.push(ability);
385        true
386    }
387    pub fn get_triggers(&self) -> FCollectionView<Trigger> {
388        crate::perf::increment(crate::perf::Metric::CardStateCollectionClones, 1);
389        self.triggers.clone()
390    }
391    pub fn has_trigger_id(&self, id: i32) -> bool {
392        self.get_triggers().iter().any(|t| t.id == id as u32)
393    }
394    pub fn add_trigger_state(&mut self, trigger: Trigger) -> bool {
395        self.triggers.push(trigger);
396        true
397    }
398    pub fn get_static_abilities(&self) -> FCollectionView<StaticAbility> {
399        crate::perf::increment(crate::perf::Metric::CardStateCollectionClones, 1);
400        self.static_abilities.clone()
401    }
402    pub fn add_static_ability_state(&mut self, static_ability: StaticAbility) -> bool {
403        self.static_abilities.push(static_ability);
404        true
405    }
406    pub fn remove_static_ability_state(&mut self, _static_ability: StaticAbility) -> bool {
407        false
408    }
409    pub fn get_replacement_effects(&self) -> FCollectionView<ReplacementEffect> {
410        crate::perf::increment(crate::perf::Metric::CardStateCollectionClones, 1);
411        self.replacement_effects.clone()
412    }
413    pub fn add_replacement_effect_state(&mut self, replacement_effect: ReplacementEffect) -> bool {
414        self.replacement_effects.push(replacement_effect);
415        true
416    }
417    pub fn has_replacement_effect_id(&self, id: i32) -> bool {
418        self.get_replacement_effect(id).is_some()
419    }
420    pub fn get_replacement_effect(&self, id: i32) -> Option<ReplacementEffect> {
421        let _ = id;
422        None
423    }
424    pub fn get_foil(&self) -> i32 {
425        self.get_svar("Foil")
426            .and_then(|s| s.parse().ok())
427            .unwrap_or(0)
428    }
429    pub fn copy_from_state(&mut self, _source: &CardState, _lki: bool) {}
430    pub fn copy_from_with_trait_base(
431        &mut self,
432        _source: &CardState,
433        _lki: bool,
434        _ctb: &CardTraitBase,
435    ) {
436    }
437    pub fn add_abilities_from_state(&mut self, _source: &CardState, _lki: bool) {}
438    pub fn copy(&self, host: Card, name: CardStateName, _lki: bool) -> CardState {
439        CardState::new(host, name)
440    }
441    pub fn get_rarity(&self) -> CardRarity {
442        self.rarity
443    }
444    pub fn set_rarity(&mut self, rarity: CardRarity) {
445        self.rarity = rarity;
446    }
447    pub fn get_set_code(&self) -> &str {
448        &self.set_code
449    }
450    pub fn set_set_code(&mut self, set_code: String) {
451        self.set_code = set_code;
452    }
453    pub fn get_image_key(&self) -> &str {
454        &self.image_key
455    }
456    pub fn set_image_key(&mut self, image_key: String) {
457        self.image_key = image_key;
458    }
459    pub fn get_traits(&self) -> Vec<CardTraitBase> {
460        Vec::new()
461    }
462    pub fn reset_original_host(&mut self, _old_host: Card) {}
463    pub fn update_changed_text(&mut self) {}
464    pub fn change_text_intrinsic(
465        &mut self,
466        _color_map: HashMap<String, String>,
467        _type_map: HashMap<String, String>,
468    ) {
469    }
470    pub fn has_chapter(&self) -> bool {
471        self.get_triggers().iter().any(|t| t.ir.chapter.is_some())
472    }
473    pub fn get_final_chapter_nr(&self) -> i32 {
474        0
475    }
476    pub fn get_manifest_up(&self) -> Option<SpellAbility> {
477        self.manifest_up.clone()
478    }
479    pub fn get_cloak_up(&self) -> Option<SpellAbility> {
480        self.cloak_up.clone()
481    }
482    pub fn get_ability_for_trigger(&self, svar: String) -> Option<SpellAbility> {
483        self.ability_for_trigger.get(&svar).cloned()
484    }
485}
486
487impl HasSVars for CardState {
488    fn get_svar(&self, name: &str) -> Option<&str> {
489        self.s_vars.get(name).map(String::as_str)
490    }
491
492    fn set_svar(&mut self, name: String, value: String) {
493        self.s_vars.insert(name, value);
494    }
495
496    fn set_svars(&mut self, new_svars: HashMap<String, String>) {
497        self.s_vars = new_svars;
498    }
499
500    fn get_svars(&self) -> &HashMap<String, String> {
501        &self.s_vars
502    }
503
504    fn remove_svar(&mut self, var: &str) {
505        self.s_vars.remove(var);
506    }
507}
508
509impl GameObject for CardState {
510    fn is_valid_single(
511        &self,
512        restriction: &str,
513        _source_controller: PlayerId,
514        _source: &Card,
515        _spell_ability: &CardTraitBase,
516    ) -> bool {
517        self.has_property(restriction, _source_controller, _source, _spell_ability)
518    }
519
520    fn has_property(
521        &self,
522        property: &str,
523        source_controller: PlayerId,
524        _source: &Card,
525        _spell_ability: &CardTraitBase,
526    ) -> bool {
527        match property {
528            "Card" | "card" => true,
529            "Permanent" => self.r#type.is_permanent(),
530            "Creature" => self.r#type.is_creature(),
531            "Land" => self.r#type.is_land(),
532            "Artifact" => self.r#type.is_artifact(),
533            "Enchantment" => self.r#type.is_enchantment(),
534            "Planeswalker" => self.r#type.is_planeswalker(),
535            "Instant" => self.r#type.is_instant(),
536            "Sorcery" => self.r#type.is_sorcery(),
537            "Basic" => self.r#type.is_basic(),
538            "Legendary" => self.r#type.is_legendary(),
539            "YouCtrl" => self.card.controller == source_controller,
540            "OppCtrl" => self.card.controller != source_controller,
541            _ => false,
542        }
543    }
544}
545
546impl HasName for CardState {
547    fn get_name(&self) -> &str {
548        &self.name
549    }
550}
551
552impl ITranslatable for CardState {
553    fn get_translation_key(&self) -> String {
554        self.flavor_name
555            .clone()
556            .unwrap_or_else(|| self.name.clone())
557    }
558
559    fn get_untranslated_type(&self) -> String {
560        self.r#type.to_string()
561    }
562
563    fn get_translated_name(&self) -> String {
564        self.get_translation_key()
565    }
566}
567
568pub fn update_types(card: &mut Card) {
569    card.type_line = CardTypeLine::parse(&card.type_line.to_string());
570}
571
572pub fn update_types_for_view(card: &mut Card) {
573    let _ = card.type_line.to_string();
574}
575
576pub fn add_type(card: &mut Card, ty: &str) {
577    card.type_line.add_type(ty);
578}
579
580pub fn remove_type(card: &mut Card, ty: &str) {
581    card.type_line
582        .supertypes
583        .retain(|st| !st.name().eq_ignore_ascii_case(ty));
584    card.type_line
585        .core_types
586        .retain(|ct| !ct.name().eq_ignore_ascii_case(ty));
587    card.type_line
588        .subtypes
589        .retain(|s| !s.eq_ignore_ascii_case(ty));
590}
591
592pub fn remove_card_types(card: &mut Card) {
593    card.type_line.core_types.clear();
594    card.type_line.subtypes.clear();
595}
596
597pub fn calculate_perpetual_adjusted_mana_cost(card: &mut Card) {
598    card.update_mana_cost_for_view();
599}
600
601pub fn add_color(card: &mut Card, color: ColorSet) {
602    card.color = card.color.union(color);
603}
604
605pub fn has_keyword(card: &Card, keyword: &str) -> bool {
606    if card
607        .cant_have_keywords
608        .contains(&keyword.to_ascii_lowercase())
609    {
610        return false;
611    }
612    card.keywords.contains_string_ignore_case(keyword)
613        || card.granted_keywords.contains_string_ignore_case(keyword)
614        || card.pump_keywords.contains_string_ignore_case(keyword)
615}
616
617pub fn has_intrinsic_keyword(card: &Card, keyword: &str) -> bool {
618    card.keywords.contains_string_ignore_case(keyword)
619}
620
621pub fn update_keywords_cache(card: &mut Card) {
622    // Only collapse duplicates of "redundant" keywords (Flying, Trample, ...).
623    // Stackable keywords like Cascade or Annihilator must keep every instance
624    // because they trigger once per copy on the source.
625    let mut seen = HashSet::new();
626    let keywords = card.keywords.as_string_list();
627    card.keywords.clear();
628    for kw in keywords {
629        let parsed = crate::keyword::keyword_collection::parse_keyword_string(&kw).0;
630        let stackable = !parsed.is_multiple_redundant();
631        let key = kw.to_ascii_lowercase();
632        if stackable || seen.insert(key) {
633            card.keywords.add(&kw);
634        }
635    }
636}
637
638pub fn add_intrinsic_keyword(card: &mut Card, keyword: &str) -> bool {
639    if keyword.trim().is_empty() {
640        return false;
641    }
642    card.keywords.add(keyword)
643}
644
645pub fn add_intrinsic_keywords<'a>(
646    card: &mut Card,
647    keywords: impl IntoIterator<Item = &'a str>,
648) -> bool {
649    let mut changed = false;
650    for kw in keywords {
651        changed |= add_intrinsic_keyword(card, kw);
652    }
653    changed
654}
655
656pub fn remove_intrinsic_keyword(card: &mut Card, keyword: &str) -> bool {
657    card.keywords.remove(keyword)
658}
659
660pub fn apply_spell_ability(
661    layer: &crate::card::card_trait_changes::CardTraitChanges,
662    list: Vec<SpellAbility>,
663) -> Vec<SpellAbility> {
664    layer.apply_spell_ability(list)
665}
666
667pub fn apply_trigger(
668    layer: &crate::card::card_trait_changes::CardTraitChanges,
669    list: Vec<Trigger>,
670) -> Vec<Trigger> {
671    layer.apply_trigger(list)
672}
673
674pub fn apply_replacement_effect(
675    layer: &crate::card::card_trait_changes::CardTraitChanges,
676    list: Vec<ReplacementEffect>,
677) -> Vec<ReplacementEffect> {
678    layer.apply_replacement_effect(list)
679}
680
681pub fn apply_static_ability(
682    layer: &crate::card::card_trait_changes::CardTraitChanges,
683    list: Vec<StaticAbility>,
684) -> Vec<StaticAbility> {
685    layer.apply_static_ability(list)
686}
687
688pub fn apply_keywords(
689    layer: &crate::card::card_trait_changes::CardTraitChanges,
690    mut list: crate::keyword::keyword_collection::KeywordCollection,
691) -> crate::keyword::keyword_collection::KeywordCollection {
692    if layer.remove_all {
693        list.clear();
694    }
695    list
696}
697
698pub fn copy(from: &Card, to: &mut Card) {
699    copy_from(from, to);
700}
701
702pub fn has_spell_ability(card: &Card, sa: &SpellAbility) -> bool {
703    card.activated_abilities
704        .iter()
705        .any(|ab| ab.ability_text == sa.ability_text)
706}
707
708pub fn add_spell_ability(card: &mut Card, sa: &SpellAbility) -> bool {
709    card.abilities.push(sa.ability_text.clone());
710    if let Some(parsed) = parse_activated_ability(&sa.ability_text, card.activated_abilities.len())
711    {
712        card.activated_abilities.push(parsed);
713        return true;
714    }
715    false
716}
717
718pub fn has_trigger(card: &Card, trigger_id: u32) -> bool {
719    card.triggers.iter().any(|t| t.id == trigger_id)
720}
721
722pub fn add_trigger(card: &mut Card, mut trig: Trigger) -> bool {
723    trig.bind_host_card_id(card.id);
724    card.triggers.push(trig);
725    true
726}
727
728pub fn add_static_ability(card: &mut Card, st_ab: StaticAbility) -> bool {
729    card.static_abilities.push(st_ab);
730    true
731}
732
733pub fn remove_static_ability(card: &mut Card, mode: crate::staticability::StaticMode) -> bool {
734    let before = card.static_abilities.len();
735    card.static_abilities.retain(|sa| !sa.check_mode(&mode));
736    before != card.static_abilities.len()
737}
738
739pub fn add_replacement_effect(card: &mut Card, mut re: ReplacementEffect) -> bool {
740    re.set_host_card(card);
741    card.replacement_effects.push(re);
742    true
743}
744
745pub fn has_replacement_effect(card: &Card) -> bool {
746    !card.replacement_effects.is_empty()
747}
748
749pub fn has_s_var(card: &Card, key: &str) -> bool {
750    card.svars.contains_key(key) || card.granted_svars.contains_key(key)
751}
752
753pub fn remove_s_var(card: &mut Card, key: &str) {
754    card.svars.remove(key);
755}
756
757pub fn copy_from(source: &Card, target: &mut Card) {
758    target.changed_card_traits = source.changed_card_traits.clone();
759    target.changed_card_traits_by_text = source.changed_card_traits_by_text.clone();
760
761    target
762        .svars
763        .retain(|k, _| !k.starts_with("TextColor:") && !k.starts_with("TextType:"));
764    target.copy_changed_text_from(source);
765
766    target.update_type_cache();
767}
768
769pub fn add_abilities_from(source: &Card, target: &mut Card) {
770    // Reuse copy-service for copiable characteristics.
771    card_copy_service::copy_copiable_characteristics(source, target);
772
773    target
774        .activated_abilities
775        .extend(source.activated_abilities.iter().cloned());
776    target.triggers.extend(source.triggers.iter().cloned());
777    target
778        .replacement_effects
779        .extend(source.replacement_effects.iter().cloned());
780    target
781        .static_abilities
782        .extend(source.static_abilities.iter().cloned());
783}
784
785pub fn has_property(card: &Card, property: &str) -> bool {
786    card_property::card_has_property(card, property, card.controller)
787}
788
789pub fn reset_original_host(card: &mut Card) {
790    card.effect_source = None;
791}
792
793pub fn update_changed_text(card: &mut Card) {
794    card.update_rules_view();
795}
796
797pub fn change_text_intrinsic(card: &mut Card) {
798    card.update_changed_text();
799}
800
801pub fn has_chapter(card: &Card) -> bool {
802    card.triggers
803        .iter()
804        .any(|t| t.ir.chapter.is_some() || t.description.to_ascii_lowercase().contains("chapter"))
805}
806
807pub fn set_type(card: &mut Card, type_line: &str) {
808    card.type_line = CardTypeLine::parse(type_line);
809}