manabrew_engine/staticability/
static_ability_alternative_cost.rs1use forge_foundation::ZoneType;
2
3use crate::card::{valid_filter, Card};
4use crate::cost::{parse_cost, Cost};
5use crate::game::GameState;
6use crate::ids::PlayerId;
7use crate::spellability::{matches_valid_sa, SpellAbility};
8use crate::staticability::StaticMode;
9
10#[derive(Debug, Clone)]
12pub struct AlternativeCostEntry {
13 pub cost: Cost,
15 pub cost_string: String,
17 pub is_ability: bool,
19 pub is_spell: bool,
21 pub x_alternative: Option<String>,
23 pub announce: Option<String>,
25 pub mana_restriction: Option<String>,
27 pub zone_restriction: Option<ZoneType>,
29 pub stack_description: Option<String>,
31 pub cost_desc: Option<String>,
33 pub description: String,
35 pub named: Option<String>,
37 pub is_own_alt_cost: bool,
39}
40
41pub fn alternative_costs(
43 game: &GameState,
44 cards: &[Card],
45 sa: &SpellAbility,
46 source: &Card,
47 player: PlayerId,
48) -> Vec<AlternativeCostEntry> {
49 let mut result = Vec::new();
50
51 let source_id = source.id;
52
53 collect_from_card(game, source, sa, source, player, &mut result);
55
56 for ca in cards
58 .iter()
59 .filter(|c| c.zone.is_static_ability_source() && c.id != source_id)
60 {
61 collect_from_card(game, ca, sa, source, player, &mut result);
62 }
63
64 result
65}
66
67pub fn has_alternative_cost(
71 game: &GameState,
72 cards: &[Card],
73 sa: &SpellAbility,
74 source: &Card,
75 player: PlayerId,
76) -> bool {
77 let source_id = source.id;
78
79 for st_ab in source
81 .static_abilities
82 .iter()
83 .filter(|s| s.is_active_for(StaticMode::AlternativeCost, source.zone))
84 {
85 if !st_ab.check_conditions(source, game) {
86 continue;
87 }
88 if apply(st_ab, sa, source, source, player) {
89 return true;
90 }
91 }
92
93 for ca in cards
95 .iter()
96 .filter(|c| c.zone.is_static_ability_source() && c.id != source_id)
97 {
98 for st_ab in ca
99 .static_abilities
100 .iter()
101 .filter(|s| s.is_active_for(StaticMode::AlternativeCost, ca.zone))
102 {
103 if !st_ab.check_conditions(ca, game) {
104 continue;
105 }
106 if apply(st_ab, sa, source, ca, player) {
107 return true;
108 }
109 }
110 }
111
112 false
113}
114
115fn collect_from_card(
118 game: &GameState,
119 ca: &Card,
120 sa: &SpellAbility,
121 source: &Card,
122 player: PlayerId,
123 result: &mut Vec<AlternativeCostEntry>,
124) {
125 for st_ab in ca
126 .static_abilities
127 .iter()
128 .filter(|s| s.is_active_for(StaticMode::AlternativeCost, ca.zone))
129 {
130 if !st_ab.check_conditions(ca, game) {
131 continue;
132 }
133 if !apply(st_ab, sa, source, ca, player) {
134 continue;
135 }
136
137 let Some(cost_template_raw) = st_ab.ir.cost.as_deref() else {
139 continue;
140 };
141
142 let cmc = source.mana_value();
144 let cost_template = cost_template_raw.replace("ConvertedManaCost", &cmc.to_string());
145
146 let is_ability = sa.is_activated;
147 let is_spell = sa.is_spell;
148
149 let cost = parse_cost(&cost_template);
150
151 let x_alternative = st_ab.ir.x_alternative_text.clone();
153
154 let announce = st_ab.ir.announce_text.clone();
156
157 let mana_restriction = st_ab.ir.mana_restriction_text.clone();
159
160 let zone_restriction = match st_ab.ir.active_zones.as_slice() {
164 [zone] => Some(*zone),
165 _ => None,
166 };
167
168 let stack_description = st_ab.ir.stack_description_text.clone();
170
171 let mut desc = String::new();
172 let cost_desc = if is_ability {
173 let cd = if let Some(cd_param) = st_ab.ir.cost_desc_text.as_deref() {
175 cd_param.to_string()
177 } else {
178 cost_template.clone()
179 };
180 desc.push_str(&cd);
181 Some(cd)
182 } else {
183 None
184 };
185
186 if is_spell {
187 desc.push_str(&sa.ability_text);
190
191 if source.id == ca.id {
193 if let Some(alt_desc) = st_ab.ir.description_text.as_deref() {
195 desc.push_str(" (");
196 desc.push_str(alt_desc);
197 desc.push_str(") ");
198 }
199 } else {
200 desc.push_str(" (by paying ");
202 desc.push_str(&cost_template);
203 desc.push_str(" instead of its mana cost)");
204 }
205 }
206
207 let is_own_alt_cost = is_spell && source.id == ca.id;
209
210 let named = st_ab.ir.named_text.clone();
212
213 result.push(AlternativeCostEntry {
214 cost,
215 cost_string: cost_template,
216 is_ability,
217 is_spell,
218 x_alternative,
219 announce,
220 mana_restriction,
221 zone_restriction,
222 stack_description,
223 cost_desc,
224 description: desc,
225 named,
226 is_own_alt_cost,
227 });
228 }
229}
230
231fn apply(
232 st_ab: &crate::staticability::StaticAbility,
233 sa: &SpellAbility,
234 source: &Card,
235 host: &Card,
236 player: PlayerId,
237) -> bool {
238 if let Some(valid_sa) = st_ab.ir.valid_sa.as_deref() {
239 if !matches_valid_sa(valid_sa, sa, host, Some(source)) {
240 return false;
241 }
242 }
243
244 if !valid_filter::matches_valid_card_selector_opt(st_ab.ir.valid_card.as_ref(), source, host) {
245 return false;
246 }
247
248 if !valid_filter::matches_valid_player_selector_opt(
249 st_ab.ir.valid_player.as_ref(),
250 player,
251 host.controller,
252 ) {
253 return false;
254 }
255
256 true
257}
258
259pub fn apply_alternative_cost_to_sa(sa: &mut SpellAbility, entry: &AlternativeCostEntry) {
261 sa.pay_costs = Some(entry.cost.clone());
263
264 if let Some(ref announce) = entry.announce {
268 sa.ir.announce_text = Some(announce.clone());
269 }
270 if let Some(ref stack_desc) = entry.stack_description {
271 sa.ir.stack_description_text = Some(stack_desc.clone());
272 }
273 if let Some(ref cost_desc) = entry.cost_desc {
274 sa.ir.precost_desc = Some(cost_desc.clone());
275 }
276
277 if let Some(ref named) = entry.named {
279 sa.ir.name_text = Some(named.clone());
280 }
281
282 let _ = &entry.x_alternative;
283 let _ = &entry.mana_restriction;
284
285 }