Skip to main content

manabrew_engine/spellability/
valid_sa.rs

1use crate::ability::api_type::ApiType;
2use crate::card::Card;
3use crate::keyword::keyword_instance::Keyword;
4use crate::spellability::SpellAbility;
5
6pub fn matches_valid_sa(
7    filter: &str,
8    sa: &SpellAbility,
9    source: &Card,
10    ability_host: Option<&Card>,
11) -> bool {
12    let filter = filter.trim();
13    if filter.is_empty() {
14        return true;
15    }
16
17    filter
18        .split(',')
19        .map(str::trim)
20        .filter(|restriction| !restriction.is_empty())
21        .any(|restriction| matches_restriction(restriction, sa, source, ability_host))
22}
23
24fn matches_restriction(
25    restriction: &str,
26    sa: &SpellAbility,
27    source: &Card,
28    ability_host: Option<&Card>,
29) -> bool {
30    let mut parts = restriction.splitn(2, '.');
31    let base = parts.next().unwrap_or("").trim();
32    let properties = parts.next();
33
34    let base_matches = matches_base_token(base, sa, ability_host);
35    if !base_matches {
36        return false;
37    }
38
39    if let Some(properties) = properties {
40        for property in properties
41            .split('+')
42            .map(str::trim)
43            .filter(|property| !property.is_empty())
44        {
45            if !matches_property_token(property, sa, source, ability_host) {
46                return false;
47            }
48        }
49    }
50
51    true
52}
53
54fn matches_base_token(token: &str, sa: &SpellAbility, ability_host: Option<&Card>) -> bool {
55    let token = token.trim();
56    let (negated, token) = match token.strip_prefix('!') {
57        Some(stripped) => (true, stripped),
58        None => (false, token),
59    };
60
61    let matched = match token.to_ascii_lowercase().as_str() {
62        "spell" => sa.is_spell,
63        "ability" => !sa.is_spell,
64        "activated" => sa.is_activated,
65        "trigger" | "triggered" => sa.is_trigger,
66        "spellability" => true,
67        "instant" => ability_host.is_some_and(|card| card.type_line.is_instant()),
68        "sorcery" => ability_host.is_some_and(|card| card.type_line.is_sorcery()),
69        _ => false,
70    };
71
72    matched != negated
73}
74
75fn matches_property_token(
76    token: &str,
77    sa: &SpellAbility,
78    source: &Card,
79    ability_host: Option<&Card>,
80) -> bool {
81    let token = token.trim();
82    let (negated, token) = match token.strip_prefix('!') {
83        Some(stripped) => (true, stripped),
84        None => (false, token),
85    };
86
87    let matched = matches_property_token_positive(token, sa, source, ability_host);
88    matched != negated
89}
90
91fn matches_property_token_positive(
92    token: &str,
93    sa: &SpellAbility,
94    source: &Card,
95    ability_host: Option<&Card>,
96) -> bool {
97    match token.to_ascii_lowercase().as_str() {
98        "self" => ability_host.is_some_and(|host| host.id == source.id),
99        "youctrl" => sa.activating_player == source.controller,
100        "oppctrl" => sa.activating_player != source.controller,
101        "manaability" => sa.is_mana_ability || sa.api == Some(ApiType::Mana),
102        "nonmanaability" => !(sa.is_mana_ability || sa.api == Some(ApiType::Mana)),
103        "istargeting" => sa.target_restrictions.is_some(),
104        "xcost" => sa.cost_has_x(),
105        "singletarget" => sa.targets_single_target(),
106        "crew" => is_crew(sa, ability_host),
107        "saddle" => is_keyword_ability(sa, ability_host, Keyword::Saddle, "saddle"),
108        "station" => is_keyword_ability(sa, ability_host, Keyword::Station, "station"),
109        "vehicle" | "mount" | "spacecraft" | "planet" => {
110            ability_host.is_some_and(|host| host.type_line.has_subtype(token))
111        }
112        _ => {
113            if sa.has_property(token) {
114                return true;
115            }
116            if sa
117                .api
118                .is_some_and(|api| api.name().eq_ignore_ascii_case(token))
119            {
120                return true;
121            }
122            ability_host.is_some_and(|host| host.has_property(token))
123        }
124    }
125}
126
127fn is_crew(sa: &SpellAbility, ability_host: Option<&Card>) -> bool {
128    if ability_text_has_keyword(sa, "crew") {
129        return true;
130    }
131
132    ability_host.is_some_and(|host| {
133        host.has_keyword_enum(Keyword::Crew)
134            && sa.api == Some(ApiType::Animate)
135            && sa.description.trim_start().starts_with("Crew")
136    })
137}
138
139fn is_keyword_ability(
140    sa: &SpellAbility,
141    ability_host: Option<&Card>,
142    keyword: Keyword,
143    text: &str,
144) -> bool {
145    if ability_text_has_keyword(sa, text) {
146        return true;
147    }
148
149    ability_host.is_some_and(|host| {
150        host.has_keyword_enum(keyword)
151            && sa
152                .description
153                .trim_start()
154                .to_ascii_lowercase()
155                .starts_with(text)
156    })
157}
158
159fn ability_text_has_keyword(sa: &SpellAbility, keyword: &str) -> bool {
160    let keyword = keyword.to_ascii_lowercase();
161    let description = sa.description.trim_start().to_ascii_lowercase();
162    let ability_text = sa.ability_text.to_ascii_lowercase();
163
164    description.starts_with(&keyword)
165        || ability_text.contains(&format!("precostdesc$ {keyword}"))
166        || ability_text.contains(&format!("spelldescription$ {keyword}"))
167}