Skip to main content

manabrew_engine/staticability/
static_ability_cast_with_flash.rs

1use forge_foundation::ZoneType;
2
3use crate::card::{valid_filter, Card};
4use crate::ids::PlayerId;
5use crate::parsing::{keys, raw_get};
6use crate::staticability::StaticMode;
7
8pub fn any_with_flash(
9    cards: &[Card],
10    spell_card: &Card,
11    caster: PlayerId,
12    spell_abilities: &[String],
13) -> bool {
14    // Java includes both global static sources and the card itself.
15    for source in cards.iter().filter(|c| {
16        c.zone == ZoneType::Battlefield || c.zone == ZoneType::Command || c.id == spell_card.id
17    }) {
18        for st_ab in source
19            .static_abilities
20            .iter()
21            .filter(|sa| sa.check_mode(&StaticMode::CastWithFlash))
22        {
23            if !matches_valid_card(st_ab.ir.valid_card.as_ref(), spell_card, source) {
24                continue;
25            }
26            if !matches_valid_player(st_ab.ir.caster.as_ref(), caster, source.controller) {
27                continue;
28            }
29            if let Some(valid_sa) = st_ab.ir.valid_sa.as_deref() {
30                // "Spell" matches any card being cast as a spell (creatures,
31                // sorceries, etc.) — not just cards with explicit SP$ lines.
32                // Java treats the inherent spell ability of a card as matching.
33                let sa_matches = valid_sa
34                    .split(',')
35                    .map(str::trim)
36                    .any(|tok| tok.eq_ignore_ascii_case("Spell"))
37                    || spell_abilities
38                        .iter()
39                        .any(|line| spell_ability_matches(valid_sa, line));
40                if !sa_matches {
41                    continue;
42                }
43            }
44            return true;
45        }
46    }
47    false
48}
49
50pub fn any_with_flash_for_card(cards: &[Card], spell_card: &Card, caster: PlayerId) -> bool {
51    for source in cards.iter().filter(|c| {
52        c.zone == ZoneType::Battlefield || c.zone == ZoneType::Command || c.id == spell_card.id
53    }) {
54        for st_ab in source
55            .static_abilities
56            .iter()
57            .filter(|sa| sa.check_mode(&StaticMode::CastWithFlash))
58        {
59            if !matches_valid_card(st_ab.ir.valid_card.as_ref(), spell_card, source) {
60                continue;
61            }
62            if !matches_valid_player(st_ab.ir.caster.as_ref(), caster, source.controller) {
63                continue;
64            }
65            if let Some(valid_sa) = st_ab.ir.valid_sa.as_deref() {
66                let sa_matches = if spell_card.action_spell_specs.is_empty() {
67                    valid_sa
68                        .split(',')
69                        .map(str::trim)
70                        .filter(|tok| !tok.is_empty())
71                        .all(|tok| tok.eq_ignore_ascii_case("Spell"))
72                } else {
73                    spell_card
74                        .action_spell_specs
75                        .iter()
76                        .any(|spec| spell_ability_spec_matches(valid_sa, spec))
77                };
78                if !sa_matches {
79                    continue;
80                }
81            }
82            return true;
83        }
84    }
85    false
86}
87
88pub fn any_with_flash_needs_info(
89    cards: &[Card],
90    spell_card: &Card,
91    caster: PlayerId,
92    spell_abilities: &[String],
93) -> bool {
94    any_with_flash(cards, spell_card, caster, spell_abilities)
95}
96
97pub fn apply_with_flash_needs_info(
98    st_ab: &crate::staticability::StaticAbility,
99    spell_card: &Card,
100    source: &Card,
101    caster: PlayerId,
102    spell_abilities: &[String],
103) -> bool {
104    if !matches_valid_card(st_ab.ir.valid_card.as_ref(), spell_card, source) {
105        return false;
106    }
107    if !matches_valid_player(st_ab.ir.caster.as_ref(), caster, source.controller) {
108        return false;
109    }
110    if let Some(valid_sa) = st_ab.ir.valid_sa.as_deref() {
111        let sa_matches = valid_sa
112            .split(',')
113            .map(str::trim)
114            .any(|tok| tok.eq_ignore_ascii_case("Spell"))
115            || spell_abilities
116                .iter()
117                .any(|line| spell_ability_matches(valid_sa, line));
118        if !sa_matches {
119            return false;
120        }
121    }
122    true
123}
124
125pub fn apply_with_flash_ability(
126    st_ab: &crate::staticability::StaticAbility,
127    spell_card: &Card,
128    source: &Card,
129    caster: PlayerId,
130) -> bool {
131    matches_valid_card(st_ab.ir.valid_card.as_ref(), spell_card, source)
132        && matches_valid_player(st_ab.ir.caster.as_ref(), caster, source.controller)
133}
134
135fn matches_valid_player(
136    valid: Option<&crate::parsing::CompiledSelector>,
137    player: PlayerId,
138    source_controller: PlayerId,
139) -> bool {
140    valid_filter::matches_valid_player_selector_opt(valid, player, source_controller)
141}
142
143fn matches_valid_card(
144    valid: Option<&crate::parsing::CompiledSelector>,
145    card: &Card,
146    source: &Card,
147) -> bool {
148    valid_filter::matches_valid_card_selector_opt(valid, card, source)
149}
150
151fn spell_ability_matches(valid_sa: &str, ability_line: &str) -> bool {
152    let is_spell = ability_line.trim_start().starts_with("SP$");
153    if !is_spell {
154        return false;
155    }
156    let tokens: Vec<&str> = valid_sa
157        .split(',')
158        .map(|s| s.trim())
159        .filter(|s| !s.is_empty())
160        .collect();
161    if tokens.is_empty() {
162        return true;
163    }
164
165    tokens
166        .iter()
167        .all(|tok| match tok.to_ascii_lowercase().as_str() {
168            "spell" => true,
169            "istargeting" => ability_line.contains("ValidTgts$"),
170            "xcost" => raw_get(ability_line, keys::COST).is_some_and(|cost| cost.contains('X')),
171            _ => false,
172        })
173}
174
175fn spell_ability_spec_matches(valid_sa: &str, spec: &crate::card::CardActionSpellSpec) -> bool {
176    let tokens: Vec<&str> = valid_sa
177        .split(',')
178        .map(|s| s.trim())
179        .filter(|s| !s.is_empty())
180        .collect();
181    if tokens.is_empty() {
182        return true;
183    }
184
185    tokens
186        .iter()
187        .all(|tok| match tok.to_ascii_lowercase().as_str() {
188            "spell" => true,
189            "istargeting" => spec.has_valid_tgts,
190            "xcost" => spec.cost_contains_x,
191            _ => false,
192        })
193}