1use std::cmp::Ordering;
4
5use crate::card::CounterType;
6use crate::cost::CostPart;
7use crate::game::GameState;
8use crate::ids::{CardId, PlayerId};
9
10pub fn pay_cost_from_source(part: &CostPart) -> bool {
12 match part {
13 CostPart::Sacrifice { type_filter, .. }
14 | CostPart::Discard { type_filter, .. }
15 | CostPart::Exile { type_filter, .. }
16 | CostPart::Return { type_filter, .. }
17 | CostPart::TapType { type_filter, .. }
18 | CostPart::UntapType { type_filter, .. }
19 | CostPart::GainControl { type_filter, .. }
20 | CostPart::RemoveAnyCounter { type_filter, .. }
21 | CostPart::ExiledMoveToGrave { type_filter, .. }
22 | CostPart::ExileFromStack { type_filter, .. }
23 | CostPart::PutCardToLib { type_filter, .. }
24 | CostPart::Enlist { type_filter, .. }
25 | CostPart::Behold { type_filter, .. }
26 | CostPart::ExileCtrlOrGrave { type_filter, .. }
27 | CostPart::Unattach { type_filter, .. } => {
28 type_filter == "CARDNAME" || type_filter == "NICKNAME"
29 }
30 _ => false,
31 }
32}
33
34pub fn convert_amount(part: &CostPart) -> Option<&crate::cost::AmountSpec> {
38 match part {
39 CostPart::PayLife(v)
40 | CostPart::PayEnergy(v)
41 | CostPart::PayShards(v)
42 | CostPart::DamageYou(v)
43 | CostPart::Draw(v)
44 | CostPart::Mill(v)
45 | CostPart::GainLife(v)
46 | CostPart::CollectEvidence(v)
47 | CostPart::ChooseColor(v)
48 | CostPart::ChooseCreatureType(v)
49 | CostPart::FlipCoin(v)
50 | CostPart::Blight(v) => Some(v),
51 CostPart::SubCounter { amount, .. }
52 | CostPart::AddCounter { amount, .. }
53 | CostPart::Sacrifice { amount, .. }
54 | CostPart::Discard { amount, .. }
55 | CostPart::Exile { amount, .. }
56 | CostPart::ExileFromAnyGrave { amount, .. }
57 | CostPart::ExileFromSameGrave { amount, .. }
58 | CostPart::Return { amount, .. }
59 | CostPart::TapType { amount, .. }
60 | CostPart::UntapType { amount, .. }
61 | CostPart::GainControl { amount, .. }
62 | CostPart::RemoveAnyCounter { amount, .. }
63 | CostPart::ExiledMoveToGrave { amount, .. }
64 | CostPart::AddMana { amount, .. }
65 | CostPart::ExileFromStack { amount, .. }
66 | CostPart::PutCardToLib { amount, .. }
67 | CostPart::Enlist { amount, .. }
68 | CostPart::Behold { amount, .. }
69 | CostPart::ExileCtrlOrGrave { amount, .. } => Some(amount),
70 CostPart::Waterbend { amount } => Some(amount),
71 CostPart::Reveal { amount, .. } => Some(amount),
72 CostPart::Exert { amount, .. } => Some(amount),
73 CostPart::RollDice { amount, .. } => Some(amount),
74 _ => None,
75 }
76}
77
78pub fn refund(game: &mut GameState, source: CardId, player: PlayerId, part: &CostPart) {
80 let resolve = |a: &crate::cost::AmountSpec, g: &GameState| a.resolve(g, source, player);
81 match part {
82 CostPart::Tap => crate::cost::cost_tap::refund(game, source),
83 CostPart::Untap => crate::cost::cost_untap::refund(game, source),
84 CostPart::PayLife(amount) => {
85 let n = resolve(amount, game);
86 crate::cost::cost_pay_life::refund(game, player, n);
87 }
88 CostPart::PayEnergy(amount) => {
89 let n = resolve(amount, game);
90 crate::cost::cost_pay_energy::refund(game, player, n);
91 }
92 CostPart::PayShards(amount) => {
93 let n = resolve(amount, game);
94 crate::cost::cost_pay_shards::refund(game, player, n);
95 }
96 CostPart::SubCounter {
97 amount,
98 counter_type,
99 ..
100 } => {
101 let n = resolve(amount, game);
102 crate::cost::cost_remove_counter::refund(game, source, n, counter_type);
103 }
104 CostPart::AddCounter {
105 amount,
106 counter_type,
107 } => {
108 let n = resolve(amount, game);
109 crate::cost::cost_put_counter::refund(game, source, n, counter_type);
110 }
111 CostPart::ChooseColor(_) => crate::cost::cost_choose_color::refund(game, source),
112 _ => {}
113 }
114}
115
116pub fn apply_text_change_effects(_part: &mut CostPart, _game: &GameState, _host: CardId) {}
121
122pub fn payment_order(part: &CostPart) -> i32 {
124 part.payment_order()
125}
126
127pub fn copy(part: &CostPart) -> CostPart {
129 part.clone()
130}
131
132pub fn get_max_amount_x(
134 game: &GameState,
135 ability: &crate::spellability::SpellAbility,
136 player: PlayerId,
137 part: &CostPart,
138 _effect: bool,
139) -> Option<i32> {
140 let source = ability.source?;
141 match part {
142 CostPart::PayEnergy(_) => Some(game.player(player).energy_counters),
143 CostPart::PayShards(_) => Some(game.player(player).mana_shards),
144 CostPart::PayLife(_) => Some(game.player(player).life.max(0)),
145 CostPart::SubCounter {
146 amount,
147 counter_type,
148 ..
149 } => {
150 let current = game.card(source).counter_count(counter_type);
151 Some(current.min(amount.resolve(game, source, player)))
152 }
153 CostPart::Sacrifice { type_filter, .. } => {
154 let (type_filter, different_names) =
155 if let Some(stripped) = strip_with_different_names(type_filter) {
156 (stripped, true)
157 } else {
158 (type_filter.clone(), false)
159 };
160 let type_list = if type_filter.contains('X') {
161 let static_sources = crate::cost::static_ability_source_cards(game);
162 game.cards_in_zone(forge_foundation::ZoneType::Battlefield, player)
163 .iter()
164 .copied()
165 .filter(|&cid| {
166 !crate::staticability::static_ability_cant_sacrifice::cant_sacrifice(
167 &static_sources,
168 game.card(cid),
169 Some(ability),
170 true,
171 )
172 })
173 .collect()
174 } else {
175 crate::cost::get_sacrifice_targets_for_cost(
176 game,
177 player,
178 &type_filter,
179 Some(ability),
180 )
181 };
182 if different_names {
183 Some(different_names_count(game, &type_list))
184 } else {
185 Some(type_list.len() as i32)
186 }
187 }
188 CostPart::Discard { type_filter, .. } => {
189 let (type_filter, different_names) =
190 if let Some(stripped) = strip_with_different_names(type_filter) {
191 (stripped, true)
192 } else {
193 (type_filter.clone(), false)
194 };
195 let hand_list: Vec<CardId> = game
196 .cards_in_zone(forge_foundation::ZoneType::Hand, player)
197 .iter()
198 .copied()
199 .filter(|&cid| {
200 type_filter == "Random"
201 || crate::ability::effects::matches_change_type(
202 game.card(cid),
203 &type_filter,
204 &[],
205 )
206 })
207 .collect();
208 if different_names {
209 Some(different_names_count(game, &hand_list))
210 } else {
211 Some(hand_list.len() as i32)
212 }
213 }
214 CostPart::Return { type_filter, .. } => {
215 Some(crate::cost::get_sacrifice_targets(game, player, type_filter).len() as i32)
216 }
217 CostPart::TapType { type_filter, .. } => {
218 Some(crate::cost::get_tap_type_targets(game, player, type_filter, source).len() as i32)
219 }
220 CostPart::Reveal {
221 type_filter, from, ..
222 } => {
223 let zone = match from {
224 crate::cost::RevealFrom::Hand
225 | crate::cost::RevealFrom::HandOrBattlefield
226 | crate::cost::RevealFrom::All => forge_foundation::ZoneType::Hand,
227 crate::cost::RevealFrom::Exile => forge_foundation::ZoneType::Exile,
228 };
229 let list: Vec<CardId> = game
230 .cards_in_zone(zone, player)
231 .iter()
232 .copied()
233 .filter(|&cid| {
234 !(ability.is_spell && cid == source)
235 && (type_filter == "Card"
236 || type_filter.is_empty()
237 || crate::ability::effects::matches_change_type(
238 game.card(cid),
239 type_filter,
240 &[],
241 ))
242 })
243 .collect();
244 Some(list.len() as i32)
245 }
246 CostPart::Exile {
247 type_filter, from, ..
248 } => Some(crate::cost::get_zone_targets(game, player, *from, type_filter).len() as i32),
249 _ => None,
250 }
251}
252
253fn strip_with_different_names(type_filter: &str) -> Option<String> {
254 type_filter
255 .contains("+WithDifferentNames")
256 .then(|| type_filter.replace("+WithDifferentNames", ""))
257}
258
259fn different_names_count(game: &GameState, cards: &[CardId]) -> i32 {
260 cards
261 .iter()
262 .map(|&cid| game.card(cid).card_name.as_str())
263 .collect::<std::collections::BTreeSet<_>>()
264 .len() as i32
265}
266
267pub fn get_ability_amount(
269 game: &GameState,
270 source: CardId,
271 player: PlayerId,
272 part: &CostPart,
273) -> i32 {
274 convert_amount(part)
275 .map(|spec| spec.resolve(game, source, player))
276 .unwrap_or(0)
277}
278
279pub fn is_reusable(part: &CostPart) -> bool {
281 match part {
282 CostPart::Tap
283 | CostPart::Untap
284 | CostPart::Mana { .. }
285 | CostPart::Reveal { .. }
286 | CostPart::TapType { .. }
287 | CostPart::UntapType { .. }
288 | CostPart::Unattach { .. }
289 | CostPart::FlipCoin(_)
290 | CostPart::RollDice { .. } => true,
291 CostPart::AddCounter { counter_type, .. } => *counter_type != CounterType::M1M1,
292 _ => false,
293 }
294}
295
296pub fn is_renewable(part: &CostPart) -> bool {
298 matches!(
299 part,
300 CostPart::Tap
301 | CostPart::Untap
302 | CostPart::Reveal { .. }
303 | CostPart::TapType { .. }
304 | CostPart::UntapType { .. }
305 )
306}
307
308pub fn is_undoable(part: &CostPart) -> bool {
310 matches!(
311 part,
312 CostPart::Tap | CostPart::Untap | CostPart::ChooseColor(_) | CostPart::Mana { .. }
313 )
314}
315
316pub fn get_type_description(part: &CostPart) -> Option<&str> {
318 match part {
319 CostPart::Unattach {
320 description: Some(d),
321 ..
322 } => Some(d.as_str()),
323 _ => None,
324 }
325}
326
327pub fn get_descriptive_type(part: &CostPart) -> String {
329 if let Some(desc) = get_type_description(part) {
330 return desc.to_string();
331 }
332
333 match part {
334 CostPart::Sacrifice { type_filter, .. }
335 | CostPart::Discard { type_filter, .. }
336 | CostPart::Exile { type_filter, .. }
337 | CostPart::Return { type_filter, .. }
338 | CostPart::TapType { type_filter, .. }
339 | CostPart::UntapType { type_filter, .. }
340 | CostPart::GainControl { type_filter, .. }
341 | CostPart::RemoveAnyCounter { type_filter, .. }
342 | CostPart::ExiledMoveToGrave { type_filter, .. }
343 | CostPart::ExileFromStack { type_filter, .. }
344 | CostPart::PutCardToLib { type_filter, .. }
345 | CostPart::Enlist { type_filter, .. }
346 | CostPart::Behold { type_filter, .. }
347 | CostPart::ExileCtrlOrGrave { type_filter, .. }
348 | CostPart::ExileFromAnyGrave { type_filter, .. }
349 | CostPart::ExileFromSameGrave { type_filter, .. }
350 | CostPart::Reveal { type_filter, .. }
351 | CostPart::Exert { type_filter, .. }
352 | CostPart::Unattach { type_filter, .. } => type_filter.to_lowercase(),
353 _ => format!("{part:?}").to_lowercase(),
354 }
355}
356
357pub fn is_core_type(s: &str) -> bool {
358 matches!(
359 s,
360 "Creature"
361 | "Artifact"
362 | "Enchantment"
363 | "Land"
364 | "Planeswalker"
365 | "Instant"
366 | "Sorcery"
367 | "Tribal"
368 | "Battle"
369 | "Kindred"
370 )
371}
372
373impl PartialEq for CostPart {
374 fn eq(&self, other: &Self) -> bool {
375 payment_order(self) == payment_order(other)
376 }
377}
378
379impl Eq for CostPart {}
380
381impl PartialOrd for CostPart {
382 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
383 Some(self.cmp(other))
384 }
385}
386
387impl Ord for CostPart {
388 fn cmp(&self, other: &Self) -> Ordering {
389 payment_order(self).cmp(&payment_order(other))
390 }
391}