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        self.get_triggers()
475            .iter()
476            .filter_map(|trigger| trigger.get_chapter())
477            .max()
478            .unwrap_or(0)
479    }
480    pub fn get_manifest_up(&self) -> Option<SpellAbility> {
481        self.manifest_up.clone()
482    }
483    pub fn get_cloak_up(&self) -> Option<SpellAbility> {
484        self.cloak_up.clone()
485    }
486    pub fn get_ability_for_trigger(&self, svar: String) -> Option<SpellAbility> {
487        self.ability_for_trigger.get(&svar).cloned()
488    }
489}
490
491impl HasSVars for CardState {
492    fn get_svar(&self, name: &str) -> Option<&str> {
493        self.s_vars.get(name).map(String::as_str)
494    }
495
496    fn set_svar(&mut self, name: String, value: String) {
497        self.s_vars.insert(name, value);
498    }
499
500    fn set_svars(&mut self, new_svars: HashMap<String, String>) {
501        self.s_vars = new_svars;
502    }
503
504    fn get_svars(&self) -> &HashMap<String, String> {
505        &self.s_vars
506    }
507
508    fn remove_svar(&mut self, var: &str) {
509        self.s_vars.remove(var);
510    }
511}
512
513impl GameObject for CardState {
514    fn is_valid_single(
515        &self,
516        restriction: &str,
517        _source_controller: PlayerId,
518        _source: &Card,
519        _spell_ability: &CardTraitBase,
520    ) -> bool {
521        self.has_property(restriction, _source_controller, _source, _spell_ability)
522    }
523
524    fn has_property(
525        &self,
526        property: &str,
527        source_controller: PlayerId,
528        _source: &Card,
529        _spell_ability: &CardTraitBase,
530    ) -> bool {
531        match property {
532            "Card" | "card" => true,
533            "Permanent" => self.r#type.is_permanent(),
534            "Creature" => self.r#type.is_creature(),
535            "Land" => self.r#type.is_land(),
536            "Artifact" => self.r#type.is_artifact(),
537            "Enchantment" => self.r#type.is_enchantment(),
538            "Planeswalker" => self.r#type.is_planeswalker(),
539            "Instant" => self.r#type.is_instant(),
540            "Sorcery" => self.r#type.is_sorcery(),
541            "Basic" => self.r#type.is_basic(),
542            "Legendary" => self.r#type.is_legendary(),
543            "YouCtrl" => self.card.controller == source_controller,
544            "OppCtrl" => self.card.controller != source_controller,
545            _ => false,
546        }
547    }
548}
549
550impl HasName for CardState {
551    fn get_name(&self) -> &str {
552        &self.name
553    }
554}
555
556impl ITranslatable for CardState {
557    fn get_translation_key(&self) -> String {
558        self.flavor_name
559            .clone()
560            .unwrap_or_else(|| self.name.clone())
561    }
562
563    fn get_untranslated_type(&self) -> String {
564        self.r#type.to_string()
565    }
566
567    fn get_translated_name(&self) -> String {
568        self.get_translation_key()
569    }
570}
571
572pub fn update_types(card: &mut Card) {
573    card.type_line = CardTypeLine::parse(&card.type_line.to_string());
574}
575
576pub fn update_types_for_view(card: &mut Card) {
577    let _ = card.type_line.to_string();
578}
579
580pub fn add_type(card: &mut Card, ty: &str) {
581    card.type_line.add_type(ty);
582}
583
584pub fn remove_type(card: &mut Card, ty: &str) {
585    card.type_line
586        .supertypes
587        .retain(|st| !st.name().eq_ignore_ascii_case(ty));
588    card.type_line
589        .core_types
590        .retain(|ct| !ct.name().eq_ignore_ascii_case(ty));
591    card.type_line
592        .subtypes
593        .retain(|s| !s.eq_ignore_ascii_case(ty));
594}
595
596pub fn remove_card_types(card: &mut Card) {
597    card.type_line.core_types.clear();
598    card.type_line.subtypes.clear();
599}
600
601pub fn calculate_perpetual_adjusted_mana_cost(card: &mut Card) {
602    card.update_mana_cost_for_view();
603}
604
605pub fn add_color(card: &mut Card, color: ColorSet) {
606    card.color = card.color.union(color);
607}
608
609pub fn has_keyword(card: &Card, keyword: &str) -> bool {
610    if card
611        .cant_have_keywords
612        .contains(&keyword.to_ascii_lowercase())
613    {
614        return false;
615    }
616    card.keywords.contains_string_ignore_case(keyword)
617        || card.granted_keywords.contains_string_ignore_case(keyword)
618        || card.pump_keywords.contains_string_ignore_case(keyword)
619}
620
621pub fn has_intrinsic_keyword(card: &Card, keyword: &str) -> bool {
622    card.keywords.contains_string_ignore_case(keyword)
623}
624
625pub fn update_keywords_cache(card: &mut Card) {
626    // Only collapse duplicates of "redundant" keywords (Flying, Trample, ...).
627    // Stackable keywords like Cascade or Annihilator must keep every instance
628    // because they trigger once per copy on the source.
629    let mut seen = HashSet::new();
630    let keywords = card.keywords.as_string_list();
631    card.keywords.clear();
632    for kw in keywords {
633        let parsed = crate::keyword::keyword_collection::parse_keyword_string(&kw).0;
634        let stackable = !parsed.is_multiple_redundant();
635        let key = kw.to_ascii_lowercase();
636        if stackable || seen.insert(key) {
637            card.keywords.add(&kw);
638        }
639    }
640}
641
642pub fn add_intrinsic_keyword(card: &mut Card, keyword: &str) -> bool {
643    if keyword.trim().is_empty() {
644        return false;
645    }
646    card.keywords.add(keyword)
647}
648
649pub fn add_intrinsic_keywords<'a>(
650    card: &mut Card,
651    keywords: impl IntoIterator<Item = &'a str>,
652) -> bool {
653    let mut changed = false;
654    for kw in keywords {
655        changed |= add_intrinsic_keyword(card, kw);
656    }
657    changed
658}
659
660pub fn remove_intrinsic_keyword(card: &mut Card, keyword: &str) -> bool {
661    card.keywords.remove(keyword)
662}
663
664pub fn apply_spell_ability(
665    layer: &crate::card::card_trait_changes::CardTraitChanges,
666    list: Vec<SpellAbility>,
667) -> Vec<SpellAbility> {
668    layer.apply_spell_ability(list)
669}
670
671pub fn apply_trigger(
672    layer: &crate::card::card_trait_changes::CardTraitChanges,
673    list: Vec<Trigger>,
674) -> Vec<Trigger> {
675    layer.apply_trigger(list)
676}
677
678pub fn apply_replacement_effect(
679    layer: &crate::card::card_trait_changes::CardTraitChanges,
680    list: Vec<ReplacementEffect>,
681) -> Vec<ReplacementEffect> {
682    layer.apply_replacement_effect(list)
683}
684
685pub fn apply_static_ability(
686    layer: &crate::card::card_trait_changes::CardTraitChanges,
687    list: Vec<StaticAbility>,
688) -> Vec<StaticAbility> {
689    layer.apply_static_ability(list)
690}
691
692pub fn apply_keywords(
693    layer: &crate::card::card_trait_changes::CardTraitChanges,
694    mut list: crate::keyword::keyword_collection::KeywordCollection,
695) -> crate::keyword::keyword_collection::KeywordCollection {
696    if layer.remove_all {
697        list.clear();
698    }
699    list
700}
701
702pub fn copy(from: &Card, to: &mut Card) {
703    copy_from(from, to);
704}
705
706pub fn has_spell_ability(card: &Card, sa: &SpellAbility) -> bool {
707    card.activated_abilities
708        .iter()
709        .any(|ab| ab.ability_text == sa.ability_text)
710}
711
712pub fn add_spell_ability(card: &mut Card, sa: &SpellAbility) -> bool {
713    card.abilities.push(sa.ability_text.clone());
714    if let Some(parsed) = parse_activated_ability(&sa.ability_text, card.activated_abilities.len())
715    {
716        card.activated_abilities.push(parsed);
717        return true;
718    }
719    false
720}
721
722pub fn has_trigger(card: &Card, trigger_id: u32) -> bool {
723    card.triggers.iter().any(|t| t.id == trigger_id)
724}
725
726pub fn add_trigger(card: &mut Card, mut trig: Trigger) -> bool {
727    trig.bind_host_card_id(card.id);
728    card.triggers.push(trig);
729    true
730}
731
732pub fn add_static_ability(card: &mut Card, st_ab: StaticAbility) -> bool {
733    card.static_abilities.push(st_ab);
734    true
735}
736
737pub fn remove_static_ability(card: &mut Card, mode: crate::staticability::StaticMode) -> bool {
738    let before = card.static_abilities.len();
739    card.static_abilities.retain(|sa| !sa.check_mode(&mode));
740    before != card.static_abilities.len()
741}
742
743pub fn add_replacement_effect(card: &mut Card, mut re: ReplacementEffect) -> bool {
744    re.set_host_card(card);
745    card.replacement_effects.push(re);
746    true
747}
748
749pub fn has_replacement_effect(card: &Card) -> bool {
750    !card.replacement_effects.is_empty()
751}
752
753pub fn has_s_var(card: &Card, key: &str) -> bool {
754    card.svars.contains_key(key) || card.granted_svars.contains_key(key)
755}
756
757pub fn remove_s_var(card: &mut Card, key: &str) {
758    card.svars.remove(key);
759}
760
761pub fn copy_from(source: &Card, target: &mut Card) {
762    target.changed_card_traits = source.changed_card_traits.clone();
763    target.changed_card_traits_by_text = source.changed_card_traits_by_text.clone();
764
765    target
766        .svars
767        .retain(|k, _| !k.starts_with("TextColor:") && !k.starts_with("TextType:"));
768    target.copy_changed_text_from(source);
769
770    target.update_type_cache();
771}
772
773pub fn add_abilities_from(source: &Card, target: &mut Card) {
774    // Reuse copy-service for copiable characteristics.
775    card_copy_service::copy_copiable_characteristics(source, target);
776
777    target
778        .activated_abilities
779        .extend(source.activated_abilities.iter().cloned());
780    target.triggers.extend(source.triggers.iter().cloned());
781    target
782        .replacement_effects
783        .extend(source.replacement_effects.iter().cloned());
784    target
785        .static_abilities
786        .extend(source.static_abilities.iter().cloned());
787}
788
789pub fn has_property(card: &Card, property: &str) -> bool {
790    card_property::card_has_property(card, property, card.controller)
791}
792
793pub fn reset_original_host(card: &mut Card) {
794    card.effect_source = None;
795}
796
797pub fn update_changed_text(card: &mut Card) {
798    card.update_rules_view();
799}
800
801pub fn change_text_intrinsic(card: &mut Card) {
802    card.update_changed_text();
803}
804
805pub fn has_chapter(card: &Card) -> bool {
806    card.triggers.iter().any(Trigger::is_chapter)
807}
808
809pub fn get_final_chapter_nr(card: &Card) -> i32 {
810    card.triggers
811        .iter()
812        .filter_map(Trigger::get_chapter)
813        .max()
814        .unwrap_or(0)
815}
816
817pub fn set_type(card: &mut Card, type_line: &str) {
818    card.type_line = CardTypeLine::parse(type_line);
819}