Skip to main content

manabrew_engine/cost/
cost_part.rs

1//! Shared helpers for Java `CostPart` parity on top of enum-based Rust cost parts.
2
3use std::cmp::Ordering;
4
5use crate::card::CounterType;
6use crate::cost::CostPart;
7use crate::game::GameState;
8use crate::ids::{CardId, PlayerId};
9
10/// Mirrors Java's `CostPart.payCostFromSource()`.
11pub 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
34/// Mirrors Java's `CostPart.convertAmount()`. Returns the raw amount slot
35/// without resolving — callers that need an `i32` should pair this with
36/// [`AmountSpec::resolve`].
37pub 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
78/// Mirrors Java's `CostPart.refund(Card source)` dispatch.
79pub 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
116/// Mirrors Java's `CostPart.applyTextChangeEffects(CardTraitBase)`.
117///
118/// The current Rust `CostPart` representation does not store immutable original
119/// type/description fields like Java, so this is currently a no-op parity shim.
120pub fn apply_text_change_effects(_part: &mut CostPart, _game: &GameState, _host: CardId) {}
121
122/// Mirrors Java's `CostPart.paymentOrder()`.
123pub fn payment_order(part: &CostPart) -> i32 {
124    part.payment_order()
125}
126
127/// Mirrors Java's `CostPart.copy()`.
128pub fn copy(part: &CostPart) -> CostPart {
129    part.clone()
130}
131
132/// Mirrors Java's `CostPart.getMaxAmountX(...)`.
133pub 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
267/// Mirrors Java's `CostPart.getAbilityAmount(SpellAbility)`.
268pub 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
279/// Mirrors Java's `CostPart.isReusable()`.
280pub 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
296/// Mirrors Java's `CostPart.isRenewable()`.
297pub 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
308/// Mirrors Java's `CostPart.isUndoable()`.
309pub fn is_undoable(part: &CostPart) -> bool {
310    matches!(
311        part,
312        CostPart::Tap | CostPart::Untap | CostPart::ChooseColor(_) | CostPart::Mana { .. }
313    )
314}
315
316/// Mirrors Java's `CostPart.getTypeDescription()`.
317pub 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
327/// Mirrors Java's `CostPart.getDescriptiveType()`.
328pub 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}