manabrew_engine/staticability/
static_ability_cast_with_flash.rs1use 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 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 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}