Skip to main content

manabrew_engine/ability/
ability_utils.rs

1//! AbilityUtils — utility functions for ability resolution.
2//!
3//! Mirrors Java's `AbilityUtils.java`.
4//! This is the single source of truth for helper functions used across effects.
5//! The `effects::helpers` module re-exports everything from here for backward
6//! compatibility.
7
8use forge_foundation::{ColorSet, ZoneType};
9
10use crate::ability::AbilityKey;
11use crate::card::filter_constants as fc;
12use crate::card::{valid_filter, Card, CounterType};
13use crate::game::GameState;
14use crate::ids::{CardId, PlayerId};
15use crate::parsing::{cached_compiled_selector, CompiledSelector};
16use crate::spellability::SpellAbility;
17
18fn push_unique_player(players: &mut Vec<PlayerId>, player: PlayerId) {
19    if !players.contains(&player) {
20        players.push(player);
21    }
22}
23
24fn targeted_spell_abilities(sa: &SpellAbility, game: &GameState) -> Vec<SpellAbility> {
25    let mut spells = Vec::new();
26    if let Some(stack_id) = sa.target_chosen.target_stack_entry {
27        if let Some(entry) = game.stack.find_by_id(stack_id) {
28            unique_push_spell(&mut spells, entry.spell_ability.clone());
29        }
30    }
31    spells
32}
33
34fn targeted_controller_players(sa: &SpellAbility, game: &GameState) -> Vec<PlayerId> {
35    let mut players = Vec::new();
36    if let Some(cid) = sa.target_chosen.target_card {
37        push_unique_player(&mut players, game.card(cid).controller);
38    }
39    for spell in targeted_spell_abilities(sa, game) {
40        push_unique_player(&mut players, spell.activating_player);
41    }
42    players
43}
44
45fn targeted_owner_players(sa: &SpellAbility, game: &GameState) -> Vec<PlayerId> {
46    let mut players = Vec::new();
47    if let Some(cid) = sa.target_chosen.target_card {
48        push_unique_player(&mut players, game.card(cid).owner);
49    }
50    for spell in targeted_spell_abilities(sa, game) {
51        if let Some(source) = spell.source {
52            push_unique_player(&mut players, game.card(source).owner);
53        }
54    }
55    players
56}
57
58fn add_players_from_remembered(
59    players: &mut Vec<PlayerId>,
60    game: &GameState,
61    card_id: CardId,
62    def: &str,
63    skip_remembered: bool,
64) {
65    let remembered_players = game.card(card_id).remembered_players.clone();
66    for player in remembered_players {
67        if def.ends_with("Opponents") {
68            push_unique_player(players, game.opponent_of(player));
69        } else {
70            push_unique_player(players, player);
71        }
72    }
73
74    let remembered_cards = game.card(card_id).remembered_cards.clone();
75    for remembered_card in remembered_cards {
76        if def.ends_with("Controller") {
77            push_unique_player(players, game.card(remembered_card).controller);
78        } else if def.ends_with("Owner") {
79            push_unique_player(players, game.card(remembered_card).owner);
80        } else if def.ends_with("Remembered") && !skip_remembered {
81            add_players_from_remembered(players, game, remembered_card, def, true);
82        }
83    }
84}
85
86fn remembered_players_for_def(def: &str, sa: &SpellAbility, game: &GameState) -> Vec<PlayerId> {
87    let mut players = Vec::new();
88    if let Some(source) = sa.source {
89        add_players_from_remembered(&mut players, game, source, def, false);
90    }
91    players
92}
93
94fn unique_push_spell(spells: &mut Vec<SpellAbility>, spell: SpellAbility) {
95    let spell_source = spell.source;
96    let spell_api = spell.api;
97    let spell_text = spell.ability_text.clone();
98    let spell_target = spell.target_chosen.target_stack_entry;
99    if spells.iter().any(|existing| {
100        existing.source == spell_source
101            && existing.api == spell_api
102            && existing.ability_text == spell_text
103            && existing.target_chosen.target_stack_entry == spell_target
104    }) {
105        return;
106    }
107    spells.push(spell);
108}
109
110// ── Defined$ Card Resolution ─────────────────────────────────────────
111
112/// Static (non-prefix) `Defined$` tokens that refer to a set of cards.
113/// Prefix-based tokens (`Triggered*`, `ValidGraveyard <filter>`, ...) stay as
114/// separate branches below since strum can't match open-ended suffixes.
115#[derive(Debug, Clone, Copy, PartialEq, Eq, strum_macros::EnumString)]
116#[strum(ascii_case_insensitive)]
117pub enum DefinedCardToken {
118    #[strum(serialize = "Self", serialize = "CARDNAME")]
119    SelfCard,
120    Remembered,
121    #[strum(
122        serialize = "Enchanted",
123        serialize = "Equipped",
124        serialize = "AttachedTo"
125    )]
126    Attached,
127    EnchantedBy,
128    Imprinted,
129    #[strum(serialize = "TopOfLibrary", serialize = "OfLibrary")]
130    TopOfLibrary,
131    TopOfGraveyard,
132    Tapped,
133    Untapped,
134}
135
136/// Resolve `Defined$` strings to a list of card IDs.
137/// Mirrors Java's `AbilityUtils.getDefinedCards()`.
138pub fn get_defined_cards(
139    game: &GameState,
140    host_card: Option<CardId>,
141    defined: &str,
142    activating_player: Option<PlayerId>,
143) -> Vec<CardId> {
144    if let Ok(token) = defined.parse::<DefinedCardToken>() {
145        return resolve_defined_card_token(token, game, host_card, activating_player);
146    }
147    // Prefix-based tokens.
148    if let Some(rest) = defined.strip_prefix("ValidGraveyard") {
149        let filter = rest.trim();
150        let player = activating_player.unwrap_or_else(|| {
151            host_card
152                .map(|c| game.card(c).controller)
153                .unwrap_or(PlayerId(0))
154        });
155        return game
156            .cards_in_zone(ZoneType::Graveyard, player)
157            .iter()
158            .copied()
159            .filter(|&cid| {
160                if filter.is_empty() {
161                    true
162                } else if let Some(source_id) = host_card {
163                    matches_valid_cards_for_source(game, source_id, game.card(cid), None, filter)
164                } else {
165                    matches_valid_cards(game.card(cid), filter, player)
166                }
167            })
168            .collect();
169    }
170    // `Discarded` / `Sacrificed` live on the SA (`discarded_cost_cards` +
171    // per-cost paid slots), not the host card, so they're resolved by the
172    // SA-aware path `spell_ability_effect::resolve_defined_cards_for_sa`.
173    Vec::new()
174}
175
176fn resolve_defined_card_token(
177    token: DefinedCardToken,
178    game: &GameState,
179    host_card: Option<CardId>,
180    activating_player: Option<PlayerId>,
181) -> Vec<CardId> {
182    match token {
183        DefinedCardToken::SelfCard => host_card.into_iter().collect(),
184        DefinedCardToken::Remembered => host_card
185            .map(|src| game.card(src).remembered_cards.clone())
186            .unwrap_or_default(),
187        DefinedCardToken::Attached => host_card
188            .and_then(|src| game.card(src).attached_to)
189            .into_iter()
190            .collect(),
191        DefinedCardToken::EnchantedBy => host_card
192            .into_iter()
193            .flat_map(|src| {
194                game.cards
195                    .iter()
196                    .filter_map(move |c| {
197                        (c.attached_to == Some(src) && c.type_line.has_subtype("Aura"))
198                            .then_some(c.id)
199                    })
200                    .collect::<Vec<_>>()
201            })
202            .collect(),
203        DefinedCardToken::Imprinted => host_card
204            .map(|src| game.card(src).imprinted_cards.clone())
205            .unwrap_or_default(),
206        DefinedCardToken::TopOfLibrary => activating_player
207            .and_then(|pid| game.cards_in_zone(ZoneType::Library, pid).last().copied())
208            .into_iter()
209            .collect(),
210        DefinedCardToken::TopOfGraveyard => activating_player
211            .and_then(|pid| game.cards_in_zone(ZoneType::Graveyard, pid).last().copied())
212            .into_iter()
213            .collect(),
214        DefinedCardToken::Tapped => activating_player
215            .map(|pid| filter_battlefield(game, pid, |c| c.tapped))
216            .unwrap_or_default(),
217        DefinedCardToken::Untapped => activating_player
218            .map(|pid| filter_battlefield(game, pid, |c| !c.tapped))
219            .unwrap_or_default(),
220    }
221}
222
223/// Return battlefield cards matching `pred`, for every player in turn order.
224/// Helper for `Tapped` / `Untapped` defined tokens.
225fn filter_battlefield(
226    game: &GameState,
227    _starter: PlayerId,
228    pred: impl Fn(&Card) -> bool,
229) -> Vec<CardId> {
230    let mut out = Vec::new();
231    for &pid in &game.player_order {
232        for &cid in game.cards_in_zone(ZoneType::Battlefield, pid) {
233            if pred(game.card(cid)) {
234                out.push(cid);
235            }
236        }
237    }
238    out
239}
240
241/// Resolve `Defined$` strings to a list of player IDs.
242/// Mirrors Java's `AbilityUtils.getDefinedPlayers()`.
243pub fn get_defined_players(
244    game: &GameState,
245    _host_card: Option<CardId>,
246    defined: &str,
247    activating_player: Option<PlayerId>,
248) -> Vec<PlayerId> {
249    if let Some(player) = activating_player {
250        resolve_defined_players(defined, player, game)
251    } else {
252        Vec::new()
253    }
254}
255
256/// Calculate a numeric amount from a parameter string.
257/// Handles simple integers and "X" references.
258///
259/// For the full implementation with SVar support, use
260/// `svar::resolve_numeric_svar()`.
261pub fn calculate_amount(value: &str) -> i32 {
262    value.parse::<i32>().unwrap_or(0)
263}
264
265// ── Parameter Parsing ────────────────────────────────────────────────
266
267/// Parse a numeric parameter from an ability string (e.g. "NumAtt$ 3" → 3).
268pub fn parse_param(ability: &str, prefix: &str) -> Option<i32> {
269    for part in ability.split('|') {
270        let part = part.trim();
271        if let Some(val) = part.strip_prefix(prefix) {
272            if let Ok(n) = val.trim().parse::<i32>() {
273                return Some(n);
274            }
275        }
276    }
277    None
278}
279
280/// Parse NumDmg$ value from an ability string.
281pub fn parse_num_dmg(ability: &str) -> i32 {
282    parse_param(ability, "NumDmg$ ").unwrap_or(0)
283}
284
285// ── Defined$ Player Resolution ───────────────────────────────────────
286
287/// Resolve a Defined$ parameter to a player ID.
288/// Mirrors Java's AbilityUtils.getDefinedPlayers().
289///
290/// Handles both bare names ("Opponent") and prefixed forms ("Player.Opponent")
291/// used by cards like Guttersnipe: `Defined$ Player.Opponent`.
292/// Static (non-prefix) `Defined$` tokens that point at a single player.
293/// Does not include SA-context tokens (`Remembered`, `TriggeredPlayer`, etc.)
294/// — those live in `resolve_defined_player_with_sa`.
295#[derive(Debug, Clone, Copy, PartialEq, Eq, strum_macros::EnumString)]
296#[strum(ascii_case_insensitive)]
297pub enum DefinedPlayerToken {
298    You,
299    #[strum(serialize = "Opponent", serialize = "OpponentCtrl")]
300    Opponent,
301    #[strum(
302        serialize = "DefendingPlayer",
303        serialize = "TriggeredDefendingPlayer",
304        serialize = "TriggeredDefender"
305    )]
306    DefendingPlayer,
307}
308
309pub fn resolve_defined_player(
310    defined: &str,
311    controller: PlayerId,
312    game: &GameState,
313) -> Option<PlayerId> {
314    // Strip "Player." prefix if present (e.g. "Player.Opponent" → "Opponent")
315    let key = defined.strip_prefix("Player.").unwrap_or(defined);
316    let token = key.parse::<DefinedPlayerToken>().ok()?;
317    Some(match token {
318        DefinedPlayerToken::You => controller,
319        DefinedPlayerToken::Opponent | DefinedPlayerToken::DefendingPlayer => {
320            game.opponent_of(controller)
321        }
322    })
323}
324
325/// Resolve a Defined$ parameter to a player ID with spell/trigger context.
326/// Mirrors Java's Triggered* player resolution for trigger SVar chains.
327pub fn resolve_defined_player_with_sa(
328    defined: &str,
329    sa: &SpellAbility,
330    controller: PlayerId,
331    game: &GameState,
332) -> Option<PlayerId> {
333    fn parse_player_object(sa: &SpellAbility, key: AbilityKey) -> Option<PlayerId> {
334        sa.get_triggering_players(key).into_iter().next()
335    }
336
337    fn triggered_controller(
338        sa: &SpellAbility,
339        game: &GameState,
340        key: AbilityKey,
341    ) -> Option<PlayerId> {
342        // Try card parsing first — trigger object values are typically CardIds,
343        // and parse_player_object would misinterpret a CardId as a PlayerId.
344        sa.get_triggering_cards(key)
345            .into_iter()
346            .next()
347            .map(|cid| game.card(cid).controller)
348            .or_else(|| parse_player_object(sa, key))
349    }
350
351    fn triggered_owner(sa: &SpellAbility, game: &GameState, key: AbilityKey) -> Option<PlayerId> {
352        sa.get_triggering_cards(key)
353            .into_iter()
354            .next()
355            .map(|cid| game.card(cid).owner)
356    }
357
358    let key = defined.strip_prefix("Player.").unwrap_or(defined);
359    if let Some(rest) = key.strip_prefix("Non") {
360        return game.alive_players().into_iter().find(|pid| {
361            !resolve_defined_players_with_sa(rest, sa, controller, game).contains(pid)
362        });
363    }
364    match key {
365        _ if key.starts_with("Remembered") => {
366            remembered_players_for_def(key, sa, game).into_iter().next()
367        }
368        "TriggeredPlayer" | "Targeted" | "TargetedPlayer" => sa
369            .target_chosen
370            .all_target_players()
371            .into_iter()
372            .next()
373            .or_else(|| parse_player_object(sa, AbilityKey::Player)),
374        "ParentTarget" => sa.target_chosen.all_target_players().into_iter().next(),
375        "ThisTargetedPlayer" => sa.target_chosen.all_target_players().into_iter().next(),
376        "TargetedOrController" => sa
377            .target_chosen
378            .all_target_players()
379            .into_iter()
380            .next()
381            .or_else(|| targeted_controller_players(sa, game).into_iter().next()),
382        "TriggeredTarget" | "TriggeredTargets" => {
383            if let Some(player) = parse_player_object(sa, AbilityKey::TargetPlayer) {
384                Some(player)
385            } else if let Some(card) = sa
386                .get_triggering_cards(AbilityKey::TargetCard)
387                .into_iter()
388                .next()
389            {
390                Some(game.card(card).controller)
391            } else {
392                // "Target" is ambiguous — it holds either a CardId or a PlayerId.
393                // Try cards first to avoid misinterpreting a CardId as a PlayerId.
394                sa.get_triggering_cards(AbilityKey::Target)
395                    .into_iter()
396                    .next()
397                    .map(|cid| game.card(cid).controller)
398                    .or_else(|| parse_player_object(sa, AbilityKey::Target))
399            }
400        }
401        "TriggeredTargetController" | "TriggeredTargetsController" => {
402            if let Some(card) = sa
403                .get_triggering_cards(AbilityKey::TargetCard)
404                .into_iter()
405                .next()
406            {
407                Some(game.card(card).controller)
408            } else if let Some(player) = parse_player_object(sa, AbilityKey::TargetPlayer) {
409                Some(player)
410            } else {
411                sa.get_triggering_cards(AbilityKey::Target)
412                    .into_iter()
413                    .next()
414                    .map(|cid| game.card(cid).controller)
415                    .or_else(|| parse_player_object(sa, AbilityKey::Target))
416            }
417        }
418        "TriggeredAttackedTarget" => parse_player_object(sa, AbilityKey::AttackedTarget),
419        "TriggeredDefender" => parse_player_object(sa, AbilityKey::Defender)
420            .or_else(|| parse_player_object(sa, AbilityKey::AttackedTarget))
421            .or_else(|| parse_player_object(sa, AbilityKey::DefendingPlayer)),
422        "TriggeredAttackingPlayer" => parse_player_object(sa, AbilityKey::AttackingPlayer),
423        "TriggeredActivator" => parse_player_object(sa, AbilityKey::Activator),
424        "TriggeredOpponentVotedDiff" => parse_player_object(sa, AbilityKey::OpponentVotedDiff),
425        "TriggeredOpponentVotedSame" => parse_player_object(sa, AbilityKey::OpponentVotedSame),
426        "TriggeredCardController" => triggered_controller(sa, game, AbilityKey::Card),
427        "TriggeredCardOwner" => triggered_owner(sa, game, AbilityKey::Card),
428        "ReplacedCardController" => triggered_controller(sa, game, AbilityKey::ReplacedCard)
429            .or_else(|| triggered_controller(sa, game, AbilityKey::Card)),
430        "ReplacedCardOwner" => triggered_owner(sa, game, AbilityKey::ReplacedCard)
431            .or_else(|| triggered_owner(sa, game, AbilityKey::Card)),
432        "TriggeredSourceController" => triggered_controller(sa, game, AbilityKey::Source),
433        "TriggeredSourceSAController" => sa
434            .get_triggering_spell_ability("SourceSA")
435            .map(|cause_sa| cause_sa.activating_player),
436        "TriggeredPlayerController" => triggered_controller(sa, game, AbilityKey::Player),
437        "DefendingPlayer" | "TriggeredDefendingPlayer" => sa
438            .target_chosen
439            .target_player
440            .or_else(|| Some(game.opponent_of(controller))),
441        "TriggeredController" => sa
442            .trigger_source
443            .map(|cid| game.card(cid).controller)
444            .or(sa.target_chosen.target_player),
445        "TargetedController" | "ThisTargetedController" | "ParentTargetedController" => {
446            targeted_controller_players(sa, game).into_iter().next()
447        }
448        "TargetedOwner" | "ThisTargetedOwner" => {
449            targeted_owner_players(sa, game).into_iter().next()
450        }
451        _ => resolve_defined_player(key, controller, game),
452    }
453}
454
455/// Resolve a Defined$ parameter to a list of player IDs with spell/trigger context.
456pub fn resolve_defined_players_with_sa(
457    defined: &str,
458    sa: &SpellAbility,
459    controller: PlayerId,
460    game: &GameState,
461) -> Vec<PlayerId> {
462    let key = defined.strip_prefix("Player.").unwrap_or(defined);
463    if key.contains(" & ") {
464        let mut players = Vec::new();
465        for part in key
466            .split(" & ")
467            .map(str::trim)
468            .filter(|part| !part.is_empty())
469        {
470            for pid in resolve_defined_players_with_sa(part, sa, controller, game) {
471                push_unique_player(&mut players, pid);
472            }
473        }
474        return players;
475    }
476    if let Some(rest) = key.strip_prefix("Non") {
477        let excluded = resolve_defined_players_with_sa(rest, sa, controller, game);
478        return game
479            .alive_players()
480            .into_iter()
481            .filter(|pid| !excluded.contains(pid))
482            .collect();
483    }
484    match key {
485        "Player" | "Players" => game.alive_players(),
486        // Player payload of a delayed trigger's remembered objects (e.g. Arcane
487        // Denial's `RememberObjects$ RememberedController` records the
488        // controller of the countered spell at registration; the spawned
489        // delayed-trigger SA reads it here via `Defined$ DelayTriggerRemembered`).
490        "DelayTriggerRemembered" | "TriggerRemembered" => {
491            let mut players = Vec::new();
492            for value in &sa.trigger_remembered {
493                match value {
494                    crate::event::AbilityValue::Player(pid) => {
495                        push_unique_player(&mut players, *pid)
496                    }
497                    crate::event::AbilityValue::Players(list) => {
498                        for pid in list {
499                            push_unique_player(&mut players, *pid);
500                        }
501                    }
502                    _ => {}
503                }
504            }
505            players
506        }
507        _ if key.starts_with("Remembered") => remembered_players_for_def(key, sa, game),
508        "TriggeredPlayer" | "Targeted" | "TargetedPlayer" => {
509            let mut players = Vec::new();
510            for player in sa.target_chosen.all_target_players() {
511                push_unique_player(&mut players, player);
512            }
513            for player in sa.get_triggering_players(AbilityKey::Player) {
514                push_unique_player(&mut players, player);
515            }
516            players
517        }
518        "ParentTarget" => sa.target_chosen.all_target_players(),
519        "ThisTargetedPlayer" => sa.target_chosen.all_target_players(),
520        "TargetedOrController" => {
521            let mut players = sa.target_chosen.all_target_players();
522            for player in targeted_controller_players(sa, game) {
523                push_unique_player(&mut players, player);
524            }
525            players
526        }
527        "TargetedController" | "ThisTargetedController" | "ParentTargetedController" => {
528            targeted_controller_players(sa, game)
529        }
530        "TargetedOwner" | "ThisTargetedOwner" => targeted_owner_players(sa, game),
531        "TriggeredTarget" | "TriggeredTargets" => {
532            let mut players = Vec::new();
533            let target_players = sa.get_triggering_players(AbilityKey::TargetPlayer);
534            let target_cards = sa.get_triggering_cards(AbilityKey::TargetCard);
535            if !target_players.is_empty() {
536                for player in target_players {
537                    push_unique_player(&mut players, player);
538                }
539            } else if !target_cards.is_empty() {
540                for cid in target_cards {
541                    push_unique_player(&mut players, game.card(cid).controller);
542                }
543            } else {
544                // "Target" is ambiguous — it holds either a CardId or a PlayerId.
545                // Try cards first; only fall back to players if no cards matched.
546                let target_cards_fallback = sa.get_triggering_cards(AbilityKey::Target);
547                if !target_cards_fallback.is_empty() {
548                    for cid in target_cards_fallback {
549                        push_unique_player(&mut players, game.card(cid).controller);
550                    }
551                } else {
552                    for player in sa.get_triggering_players(AbilityKey::Target) {
553                        push_unique_player(&mut players, player);
554                    }
555                }
556            }
557            players
558        }
559        "TriggeredTargetController" | "TriggeredTargetsController" => {
560            let mut players = Vec::new();
561            let target_players = sa.get_triggering_players(AbilityKey::TargetPlayer);
562            let target_cards = sa.get_triggering_cards(AbilityKey::TargetCard);
563            if !target_cards.is_empty() {
564                for cid in target_cards {
565                    push_unique_player(&mut players, game.card(cid).controller);
566                }
567            } else if !target_players.is_empty() {
568                for player in target_players {
569                    push_unique_player(&mut players, player);
570                }
571            } else {
572                // "Target" is ambiguous — try cards first, then players.
573                let target_cards_fallback = sa.get_triggering_cards(AbilityKey::Target);
574                if !target_cards_fallback.is_empty() {
575                    for cid in target_cards_fallback {
576                        push_unique_player(&mut players, game.card(cid).controller);
577                    }
578                } else {
579                    for player in sa.get_triggering_players(AbilityKey::Target) {
580                        push_unique_player(&mut players, player);
581                    }
582                }
583            }
584            players
585        }
586        "TriggeredAttackedTarget" => sa.get_triggering_players(AbilityKey::AttackedTarget),
587        "TriggeredDefender" => {
588            let mut players = sa.get_triggering_players(AbilityKey::Defender);
589            for pid in sa.get_triggering_players(AbilityKey::AttackedTarget) {
590                push_unique_player(&mut players, pid);
591            }
592            for pid in sa.get_triggering_players(AbilityKey::DefendingPlayer) {
593                push_unique_player(&mut players, pid);
594            }
595            players
596        }
597        "TriggeredAttackedTargetAndYou" => {
598            let mut players = sa.get_triggering_players(AbilityKey::AttackedTarget);
599            push_unique_player(&mut players, controller);
600            players
601        }
602        "TriggeredAttackingPlayer" => sa.get_triggering_players(AbilityKey::AttackingPlayer),
603        "TriggeredActivator" => sa.get_triggering_players(AbilityKey::Activator),
604        "TriggeredOpponentVotedDiff" => sa.get_triggering_players(AbilityKey::OpponentVotedDiff),
605        "TriggeredOpponentVotedSame" => sa.get_triggering_players(AbilityKey::OpponentVotedSame),
606        "TriggeredCardController" => sa
607            .get_triggering_cards(AbilityKey::Card)
608            .into_iter()
609            .map(|cid| game.card(cid).controller)
610            .collect(),
611        "TriggeredCardOwner" => sa
612            .get_triggering_cards(AbilityKey::Card)
613            .into_iter()
614            .map(|cid| game.card(cid).owner)
615            .collect(),
616        "ReplacedCardController" => {
617            let replaced = sa.get_triggering_cards(AbilityKey::ReplacedCard);
618            let cards = if replaced.is_empty() {
619                sa.get_triggering_cards(AbilityKey::Card)
620            } else {
621                replaced
622            };
623            cards
624                .into_iter()
625                .map(|cid| game.card(cid).controller)
626                .collect()
627        }
628        "ReplacedCardOwner" => {
629            let replaced = sa.get_triggering_cards(AbilityKey::ReplacedCard);
630            let cards = if replaced.is_empty() {
631                sa.get_triggering_cards(AbilityKey::Card)
632            } else {
633                replaced
634            };
635            cards.into_iter().map(|cid| game.card(cid).owner).collect()
636        }
637        "TriggeredSourceController" => sa
638            .get_triggering_cards(AbilityKey::Source)
639            .into_iter()
640            .map(|cid| game.card(cid).controller)
641            .collect(),
642        "TriggeredSourceSAController" => sa
643            .get_triggering_spell_ability("SourceSA")
644            .map(|cause_sa| vec![cause_sa.activating_player])
645            .unwrap_or_default(),
646        "TriggeredPlayerController" => {
647            let mut players = sa.get_triggering_players(AbilityKey::Player);
648            for cid in sa.get_triggering_cards(AbilityKey::Player) {
649                push_unique_player(&mut players, game.card(cid).controller);
650            }
651            players
652        }
653        "DefendingPlayer" | "TriggeredDefendingPlayer" => {
654            let mut players: Vec<_> = sa.target_chosen.target_player.into_iter().collect();
655            let defending = game.opponent_of(controller);
656            push_unique_player(&mut players, defending);
657            players
658        }
659        _ => resolve_defined_players(key, controller, game),
660    }
661}
662
663/// Resolve a `Defined$` parameter to spell abilities.
664/// Mirrors Java `AbilityUtils.getDefinedSpellAbilities(Card, String, CardTraitBase)`
665/// (AbilityUtils.java:1202).
666pub fn get_defined_spell_abilities(
667    defined: &str,
668    sa: &SpellAbility,
669    game: &GameState,
670) -> Vec<SpellAbility> {
671    let key = defined.trim();
672    let mut spells = Vec::new();
673
674    match key {
675        // Self — the SA itself (Java L1214).
676        "Self" => unique_push_spell(&mut spells, sa.clone()),
677
678        // Parent — the root of the sub-ability chain (Java L1216).
679        // The Rust engine doesn't thread a back-pointer to the parent, so the
680        // best-effort resolution returns `sa` itself (root == current for
681        // non-nested calls). Nested sub-ability nodes that need the root must
682        // carry `parent_target_card` through `EffectContext` (already done for
683        // targets); proper Parent-SA threading is deferred until SAs own a
684        // parent handle.
685        "Parent" => unique_push_spell(&mut spells, sa.clone()),
686
687        // Remembered — cards remembered by the host contribute their SAs; any
688        // SpellAbility objects directly in the remembered list are added too.
689        // The Rust `Card::remembered_cards` only stores CardIds, so the "spell
690        // ability remembered" branch is unreachable here (Java L1222–L1226).
691        "Remembered" => {
692            if let Some(host_id) = sa.source {
693                let remembered = game.card(host_id).remembered_cards.clone();
694                for cid in remembered {
695                    for ab_text in game.card(cid).abilities.iter() {
696                        let built = crate::spellability::build_spell_ability_from_host_card(
697                            game.card(cid),
698                            ab_text,
699                            sa.activating_player,
700                        );
701                        unique_push_spell(&mut spells, built);
702                    }
703                }
704            }
705        }
706
707        // Imprinted — mirrors Java L1227–L1230.
708        "Imprinted" => {
709            if let Some(host_id) = sa.source {
710                let imprinted = game.card(host_id).imprinted_cards.clone();
711                for cid in imprinted {
712                    for ab_text in game.card(cid).abilities.iter() {
713                        let built = crate::spellability::build_spell_ability_from_host_card(
714                            game.card(cid),
715                            ab_text,
716                            sa.activating_player,
717                        );
718                        unique_push_spell(&mut spells, built);
719                    }
720                }
721            }
722        }
723
724        // EffectSource — the SA that created an effect card (Java L1231–L1234).
725        // Tracked via `Card::effect_source`; best we can do without
726        // `Card::effect_source_ability` is project to the first ability on the
727        // source card — matches Java behavior when the source has exactly one
728        // SA, which is the common case for command-zone effects.
729        "EffectSource" => {
730            if let Some(host_id) = sa.source {
731                if let Some(effect_src) = game.card(host_id).effect_source {
732                    let src_card = game.card(effect_src);
733                    if let Some(ab_text) = src_card.abilities.first() {
734                        let built = crate::spellability::build_spell_ability_from_host_card(
735                            src_card,
736                            ab_text,
737                            sa.activating_player,
738                        );
739                        unique_push_spell(&mut spells, built);
740                    }
741                }
742            }
743        }
744
745        // SourceFirstSpell — stack entry whose host matches this SA's source
746        // (Java L1235–L1239).
747        "SourceFirstSpell" => {
748            if let Some(host_id) = sa.source {
749                if let Some(entry) = game
750                    .stack
751                    .iter()
752                    .find(|e| e.spell_ability.source == Some(host_id))
753                {
754                    unique_push_spell(&mut spells, entry.spell_ability.clone());
755                }
756            }
757        }
758
759        "TriggeredSpellAbility" => {
760            for trigger_key in ["SpellAbility", "SourceSA", "Cause", "AbilityMana"] {
761                if let Some(spell) = sa.get_triggering_spell_ability(trigger_key) {
762                    unique_push_spell(&mut spells, spell.clone());
763                }
764            }
765        }
766        "SpellTargeted" | "ThisTargeted" => {
767            if let Some(stack_id) = sa.target_chosen.target_stack_entry {
768                if let Some(entry) = game.stack.find_by_id(stack_id) {
769                    unique_push_spell(&mut spells, entry.spell_ability.clone());
770                }
771            }
772        }
773        "SourceSA" | "SpellAbility" | "AbilityMana" | "Cause" | "StackSa" => {
774            if let Some(spell) = sa.get_triggering_spell_ability(key) {
775                unique_push_spell(&mut spells, spell.clone());
776            }
777        }
778        "TopStack" => {
779            if let Some(spell) = game.stack.peek_ability() {
780                unique_push_spell(&mut spells, spell.clone());
781            }
782        }
783        _ => {
784            // Triggered<Key> — pull a SpellAbility from the triggering-objects
785            // map by AbilityKey name (Java L1240–L1247).
786            if let Some(trigger_key) = key.strip_prefix("Triggered") {
787                if let Some(spell) = sa.get_triggering_spell_ability(trigger_key) {
788                    unique_push_spell(&mut spells, spell.clone());
789                }
790            }
791        }
792    }
793
794    spells
795}
796
797/// Resolve a Defined$ parameter to a list of player IDs.
798/// Supports "You", "Opponent", "Each"/"All"/"Player" (all alive players).
799/// Mirrors Java's AbilityUtils.getDefinedPlayers() for multi-player resolution.
800pub fn resolve_defined_players(
801    defined: &str,
802    controller: PlayerId,
803    game: &GameState,
804) -> Vec<PlayerId> {
805    if defined.contains(" & ") {
806        let mut players = Vec::new();
807        for part in defined
808            .split(" & ")
809            .map(str::trim)
810            .filter(|part| !part.is_empty())
811        {
812            for pid in resolve_defined_players(part, controller, game) {
813                if !players.contains(&pid) {
814                    players.push(pid);
815                }
816            }
817        }
818        return players;
819    }
820    if let Some(rest) = defined.strip_prefix("Non") {
821        let excluded = resolve_defined_players(rest, controller, game);
822        return game
823            .alive_players()
824            .into_iter()
825            .filter(|pid| !excluded.contains(pid))
826            .collect();
827    }
828    match defined {
829        "You" => vec![controller],
830        "Opponent" | "OpponentCtrl" => vec![game.opponent_of(controller)],
831        "DefendingPlayer" | "TriggeredDefendingPlayer" | "TriggeredDefender" => {
832            vec![game.opponent_of(controller)]
833        }
834        "Each" | "All" | "Player" => game.alive_players(),
835        _ => {
836            // Fall back to single-player resolution
837            if let Some(pid) = resolve_defined_player(defined, controller, game) {
838                vec![pid]
839            } else {
840                vec![controller]
841            }
842        }
843    }
844}
845
846#[cfg(test)]
847mod tests {
848    use super::*;
849    use forge_foundation::{CardTypeLine, ColorSet, ManaCost};
850
851    use crate::card::Card;
852    use crate::ids::{CardId, PlayerId};
853
854    fn make_card(
855        game: &mut GameState,
856        owner: PlayerId,
857        controller: PlayerId,
858        name: &str,
859    ) -> CardId {
860        let mut card = Card::new(
861            CardId(0),
862            name.to_string(),
863            owner,
864            CardTypeLine::parse("Creature"),
865            ManaCost::parse("1"),
866            ColorSet::COLORLESS,
867            Some(1),
868            Some(1),
869            vec![],
870            vec![],
871        );
872        card.controller = controller;
873        game.create_card(card)
874    }
875
876    #[test]
877    fn targeted_controller_ignores_player_targets() {
878        let game = GameState::new(&["P0", "P1"], 20);
879        let p0 = PlayerId(0);
880        let p1 = PlayerId(1);
881
882        let mut sa = SpellAbility::new_simple(None, p0, "DB$ Discard");
883        sa.target_chosen.target_player = Some(p1);
884
885        assert_eq!(
886            resolve_defined_player_with_sa("TargetedController", &sa, p0, &game),
887            None
888        );
889        assert!(resolve_defined_players_with_sa("TargetedController", &sa, p0, &game).is_empty());
890        assert_eq!(
891            resolve_defined_players_with_sa("TargetedOrController", &sa, p0, &game),
892            vec![p1]
893        );
894    }
895
896    #[test]
897    fn targeted_controller_and_owner_use_targeted_card_only() {
898        let mut game = GameState::new(&["P0", "P1"], 20);
899        let p0 = PlayerId(0);
900        let p1 = PlayerId(1);
901        let target = make_card(&mut game, p0, p1, "Borrowed Bear");
902
903        let mut sa = SpellAbility::new_simple(None, p0, "DB$ Draw");
904        sa.target_chosen.target_card = Some(target);
905
906        assert_eq!(
907            resolve_defined_player_with_sa("TargetedController", &sa, p0, &game),
908            Some(p1)
909        );
910        assert_eq!(
911            resolve_defined_player_with_sa("TargetedOwner", &sa, p0, &game),
912            Some(p0)
913        );
914        assert_eq!(
915            resolve_defined_players_with_sa("TargetedController", &sa, p0, &game),
916            vec![p1]
917        );
918        assert_eq!(
919            resolve_defined_players_with_sa("TargetedOwner", &sa, p0, &game),
920            vec![p0]
921        );
922    }
923
924    #[test]
925    fn this_targeted_player_stays_on_current_sa_targets() {
926        let game = GameState::new(&["P0", "P1"], 20);
927        let p0 = PlayerId(0);
928        let p1 = PlayerId(1);
929
930        let mut sa = SpellAbility::new_simple(None, p0, "DB$ Token");
931        sa.target_chosen.target_player = Some(p1);
932
933        assert_eq!(
934            resolve_defined_players_with_sa("ThisTargetedPlayer", &sa, p0, &game),
935            vec![p1]
936        );
937    }
938}
939
940// ── Counter Type Parsing ─────────────────────────────────────────────
941
942/// Parse a counter type string to CounterType enum (case-insensitive).
943/// Unknown types produce `CounterType::Named(UPPER)` instead of silently
944/// falling back to P1P1, so cards like Stocking the Pantry get the correct
945/// SUPPLY counters.
946pub fn parse_counter_type(s: &str) -> CounterType {
947    crate::card::counter_type::parse_counter_type(s)
948}
949
950// ── Zone Type Parsing ────────────────────────────────────────────────
951
952/// Parse a zone name string to ZoneType.
953pub fn parse_zone_type(s: &str) -> Option<ZoneType> {
954    let s = s.trim();
955    if s.eq_ignore_ascii_case("Deck") {
956        Some(ZoneType::Library)
957    } else {
958        ZoneType::from_str_compat(s)
959    }
960}
961
962// ── ValidCards$ Matching ─────────────────────────────────────────────
963
964/// Full ValidCards$ filter matching with controller and keyword qualifier support.
965///
966/// This is the preferred function for mass effects (DestroyAll, DamageAll, etc.)
967/// because it handles `YouCtrl`, `OppCtrl`, `withFlying` / `withoutFlying`,
968/// and color (`nonBlack`)
969/// qualifiers in addition to card types.
970///
971/// `activating_player` is the player who cast/activated the ability; used to
972/// resolve `YouCtrl` / `OppCtrl` qualifiers.
973///
974/// Mirrors Java's `CardLists.getValidCards()` + `CardProperty.cardHasProperty()`.
975pub fn matches_valid_cards(card: &Card, filter: &str, activating_player: PlayerId) -> bool {
976    if filter.is_empty() || filter == fc::CARD {
977        return true;
978    }
979
980    // Comma-separated = OR conditions (e.g. "Creature.attacking Opponent, Creature.attacking Planeswalker.OppCtrl")
981    if filter.contains(", ") {
982        return filter
983            .split(", ")
984            .any(|part| matches_valid_cards_single(card, part.trim(), activating_player));
985    }
986
987    matches_valid_cards_single(card, filter, activating_player)
988}
989
990pub fn matches_valid_cards_selector_opt(
991    selector: Option<&CompiledSelector>,
992    card: &Card,
993    activating_player: PlayerId,
994) -> bool {
995    selector.is_none_or(|selector| {
996        selector.alternatives.iter().any(|alternative| {
997            matches_valid_cards_single(card, &alternative.raw, activating_player)
998        })
999    })
1000}
1001
1002pub fn matches_valid_cards_for_sa(
1003    game: &GameState,
1004    sa: &SpellAbility,
1005    card: &Card,
1006    selector: Option<&CompiledSelector>,
1007    default_filter: &str,
1008) -> bool {
1009    match (selector, sa.source) {
1010        (Some(selector), Some(source_id)) => valid_filter::matches_valid_card_selector_in_game(
1011            selector,
1012            card,
1013            game.card(source_id),
1014            game,
1015        ),
1016        (Some(selector), None) => {
1017            matches_valid_cards_selector_opt(Some(selector), card, sa.activating_player)
1018        }
1019        (None, Some(source_id)) => valid_filter::matches_valid_card_selector_in_game(
1020            &cached_compiled_selector(default_filter),
1021            card,
1022            game.card(source_id),
1023            game,
1024        ),
1025        (None, None) => matches_valid_cards(card, default_filter, sa.activating_player),
1026    }
1027}
1028
1029pub fn matches_valid_cards_for_source(
1030    game: &GameState,
1031    source_id: CardId,
1032    card: &Card,
1033    selector: Option<&CompiledSelector>,
1034    default_filter: &str,
1035) -> bool {
1036    let source = game.card(source_id);
1037    let parsed;
1038    let selector = match selector {
1039        Some(selector) => selector,
1040        None => {
1041            parsed = cached_compiled_selector(default_filter);
1042            &parsed
1043        }
1044    };
1045    valid_filter::matches_valid_card_selector_in_game(selector, card, source, game)
1046}
1047
1048fn matches_valid_cards_single(card: &Card, filter: &str, activating_player: PlayerId) -> bool {
1049    let parts: Vec<&str> = filter.split('.').collect();
1050    let type_part = parts[0];
1051
1052    // ── Type check ──────────────────────────────────────────────────────────
1053    let type_matches = match type_part {
1054        fc::CREATURE => card.is_creature(),
1055        fc::LAND => card.is_land(),
1056        fc::ARTIFACT => card
1057            .type_line
1058            .core_types
1059            .iter()
1060            .any(|t| t.name().eq_ignore_ascii_case(fc::ARTIFACT)),
1061        fc::ENCHANTMENT => card
1062            .type_line
1063            .core_types
1064            .iter()
1065            .any(|t| t.name().eq_ignore_ascii_case(fc::ENCHANTMENT)),
1066        fc::PLANESWALKER => card
1067            .type_line
1068            .core_types
1069            .iter()
1070            .any(|t| t.name().eq_ignore_ascii_case(fc::PLANESWALKER)),
1071        fc::INSTANT => card
1072            .type_line
1073            .core_types
1074            .iter()
1075            .any(|t| t.name().eq_ignore_ascii_case(fc::INSTANT)),
1076        fc::SORCERY => card
1077            .type_line
1078            .core_types
1079            .iter()
1080            .any(|t| t.name().eq_ignore_ascii_case(fc::SORCERY)),
1081        fc::PERMANENT | fc::CARD => true,
1082        _ => true, // Unknown type — match everything
1083    };
1084    if !type_matches {
1085        return false;
1086    }
1087
1088    // ── Qualifier checks (dot-separated after the type) ─────────────────────
1089    // Handle compound "+" syntax (e.g. "YouCtrl+nonBlack", "Self+kicked")
1090    for &qualifier in &parts[1..] {
1091        let sub_parts: Vec<&str> = qualifier.split('+').collect();
1092        for sub in &sub_parts {
1093            if !matches_valid_cards_qualifier(card, sub, activating_player) {
1094                return false;
1095            }
1096        }
1097    }
1098    true
1099}
1100
1101fn matches_valid_cards_qualifier(
1102    card: &Card,
1103    qualifier: &str,
1104    activating_player: PlayerId,
1105) -> bool {
1106    match qualifier {
1107        fc::YOU_CTRL => card.controller == activating_player,
1108        fc::OPP_CTRL => card.controller != activating_player,
1109        fc::BASIC => card.type_line.is_basic(),
1110        fc::KICKED => card.kicked,
1111        fc::WITH_FLYING => {
1112            card.keywords.contains_string_ignore_case("Flying")
1113                || card.granted_keywords.contains_string_ignore_case("Flying")
1114        }
1115        _ => {
1116            if let Some(keyword) = qualifier.strip_prefix("without") {
1117                return keyword.is_empty() || !card.has_keyword(keyword);
1118            }
1119            if let Some(keyword) = qualifier.strip_prefix("with") {
1120                return !keyword.is_empty() && card.has_keyword(keyword);
1121            }
1122            // "attacking Opponent" / "attacking Planeswalker" — space-separated combat qualifier
1123            if let Some(target) = qualifier.strip_prefix("attacking ") {
1124                let attacking = card.attacking_player;
1125                match target {
1126                    "Opponent" => match attacking {
1127                        Some(def) => def != activating_player,
1128                        None => false,
1129                    },
1130                    // "attacking Planeswalker" — only true if attacking a planeswalker (not a player).
1131                    // Currently combat only tracks player targets, so this is always false.
1132                    "Planeswalker" => false,
1133                    _ => attacking.is_some(), // any attack target
1134                }
1135            }
1136            // Type negations ("nonLand", "nonland", "nonCreature", etc.) and
1137            // color filters ("nonBlack", "nonRed", etc.).
1138            else {
1139                let lower = qualifier.to_ascii_lowercase();
1140                if let Some(rest) = lower.strip_prefix("non") {
1141                    match rest {
1142                        "land" => !card.is_land(),
1143                        "creature" => !card.is_creature(),
1144                        "artifact" => !card.type_line.is_artifact(),
1145                        "enchantment" => !card.type_line.is_enchantment(),
1146                        "planeswalker" => !card.type_line.is_planeswalker(),
1147                        "token" => !card.is_token,
1148                        "basic" => !card.type_line.is_basic(),
1149                        _ => {
1150                            let excluded = ColorSet::from_names(rest);
1151                            !card.color.shares_color_with(excluded)
1152                        }
1153                    }
1154                } else {
1155                    // Unknown qualifier — match everything (forward-compatible)
1156                    true
1157                }
1158            }
1159        }
1160    }
1161}
1162
1163// ── ChangeType$ Matching ─────────────────────────────────────────────
1164
1165/// Check if a card matches a ChangeType$ / ValidCards$ filter string.
1166///
1167/// `source_chosen_colors` should be the `chosen_colors` from the source card
1168/// of the spell/ability (for `ChosenColor` qualifier support). Pass `&[]` when
1169/// no source card context is available.
1170pub fn matches_change_type(
1171    card: &Card,
1172    change_type: &str,
1173    source_chosen_colors: &[String],
1174) -> bool {
1175    if change_type.is_empty() {
1176        return true;
1177    }
1178
1179    // Handle semicolon-separated alternatives (OR).
1180    // E.g. "Artifact;Creature" means Artifact OR Creature.
1181    // Mirrors Java's CardLists.getValidCards() which splits on "," and ";".
1182    if change_type.contains(';') {
1183        return change_type
1184            .split(';')
1185            .any(|alt| matches_change_type(card, alt.trim(), source_chosen_colors));
1186    }
1187
1188    // Handle comma-separated alternatives (OR).
1189    // E.g. "Artifact,Creature" means Artifact OR Creature.
1190    if change_type.contains(',') {
1191        return change_type
1192            .split(',')
1193            .any(|alt| matches_change_type(card, alt.trim(), source_chosen_colors));
1194    }
1195
1196    // Forge separates qualifiers with '.' for the first qualifier after the
1197    // type and '+' for additional qualifiers (e.g. "Card.Red+Other"). Split
1198    // on both so every qualifier is visited individually.
1199    let parts: Vec<&str> = change_type.split(['.', '+']).collect();
1200    let type_part = parts[0];
1201
1202    let type_matches = match type_part {
1203        fc::LAND => card.is_land(),
1204        fc::CREATURE => card.is_creature(),
1205        fc::ARTIFACT => card.type_line.is_artifact(),
1206        fc::ENCHANTMENT => card.type_line.is_enchantment(),
1207        fc::INSTANT => card.type_line.is_instant(),
1208        fc::SORCERY => card.type_line.is_sorcery(),
1209        fc::PLANESWALKER => card.type_line.is_planeswalker(),
1210        fc::PERMANENT => card.is_permanent(),
1211        fc::CARD => true,
1212        // Support land-subtype selectors used in tutor scripts
1213        // (e.g. "Forest.Basic", "Plains.Basic").
1214        "Plains" | "Island" | "Swamp" | "Mountain" | "Forest" => card
1215            .type_line
1216            .subtypes
1217            .iter()
1218            .any(|st| st.eq_ignore_ascii_case(type_part)),
1219        _ => card.has_subtype(type_part),
1220    };
1221
1222    if !type_matches {
1223        return false;
1224    }
1225
1226    for &qualifier in &parts[1..] {
1227        match qualifier {
1228            q if q.starts_with("named") => {
1229                let expected = q[5..].replace(';', ",").replace('_', " ");
1230                if !card.card_name.eq_ignore_ascii_case(&expected) {
1231                    return false;
1232                }
1233            }
1234            fc::BASIC => {
1235                if !card.type_line.is_basic() {
1236                    return false;
1237                }
1238            }
1239            fc::NON_LAND => {
1240                if card.is_land() {
1241                    return false;
1242                }
1243            }
1244            fc::ATTACKING => {
1245                if card.attacking_player.is_none() {
1246                    return false;
1247                }
1248            }
1249            "Red" => {
1250                if !card.color.has_red() {
1251                    return false;
1252                }
1253            }
1254            "White" => {
1255                if !card.color.has_white() {
1256                    return false;
1257                }
1258            }
1259            "Blue" => {
1260                if !card.color.has_blue() {
1261                    return false;
1262                }
1263            }
1264            "Black" => {
1265                if !card.color.has_black() {
1266                    return false;
1267                }
1268            }
1269            "Green" => {
1270                if !card.color.has_green() {
1271                    return false;
1272                }
1273            }
1274            "Colorless" => {
1275                if !card.color.is_colorless() {
1276                    return false;
1277                }
1278            }
1279            // "Other" means "other than the source" and is enforced by callers
1280            // that have source context; treat as a no-op here.
1281            "Other" => {}
1282            // Controller/owner qualifiers require game state that this helper
1283            // doesn't carry. Callers that actually need to enforce them pass
1284            // the correct zone + player to e.g. `cards_in_zone`, so we accept
1285            // them as no-ops here rather than treating them as subtypes and
1286            // failing every match.
1287            "YouCtrl" | "YouControl" | "You" | "YouOwn" | "OppCtrl" | "OpponentCtrl" | "OppOwn"
1288            | "OpponentOwn" | "Opponent" => {}
1289            // Token / nonToken qualifiers.
1290            "Token" => {
1291                if !card.is_token {
1292                    return false;
1293                }
1294            }
1295            "nonToken" => {
1296                if card.is_token {
1297                    return false;
1298                }
1299            }
1300            // Supertype qualifiers beyond Basic (Basic is handled above).
1301            "Legendary" => {
1302                if !card.type_line.is_legendary() {
1303                    return false;
1304                }
1305            }
1306            "nonLegendary" => {
1307                if card.type_line.is_legendary() {
1308                    return false;
1309                }
1310            }
1311            "Snow" => {
1312                if !card.type_line.is_snow() {
1313                    return false;
1314                }
1315            }
1316            "nonBasic" => {
1317                if card.type_line.is_basic() {
1318                    return false;
1319                }
1320            }
1321            "nonCreature" => {
1322                if card.is_creature() {
1323                    return false;
1324                }
1325            }
1326            "nonArtifact" => {
1327                if card.type_line.is_artifact() {
1328                    return false;
1329                }
1330            }
1331            "nonEnchantment" => {
1332                if card.type_line.is_enchantment() {
1333                    return false;
1334                }
1335            }
1336            // Generic cmc comparators:
1337            //   cmcEQ<N>, cmcLE<N>, cmcGE<N>, cmcLT<N>, cmcGT<N>
1338            // where <N> may be a literal integer or an SVar name (e.g. X, Y).
1339            // When <N> is a literal we enforce it. When <N> references an SVar
1340            // we lack context in this helper and accept as permissive (the
1341            // caller that resolves the SVar is responsible for filtering).
1342            q if q.starts_with("cmc") && q.len() > 5 => {
1343                let op = &q[3..5];
1344                let rest = &q[5..];
1345                let mana_value = card.mana_value();
1346                let pass = if let Ok(n) = rest.parse::<i32>() {
1347                    match op {
1348                        "EQ" => mana_value == n,
1349                        "LE" => mana_value <= n,
1350                        "GE" => mana_value >= n,
1351                        "LT" => mana_value < n,
1352                        "GT" => mana_value > n,
1353                        _ => true,
1354                    }
1355                } else {
1356                    true
1357                };
1358                if !pass {
1359                    return false;
1360                }
1361            }
1362            q if q.starts_with("power") && q.len() > 7 => {
1363                if !matches_numeric_qualifier(card.power(), &q[5..]) {
1364                    return false;
1365                }
1366            }
1367            q if q.starts_with("toughness") && q.len() > 11 => {
1368                if !matches_numeric_qualifier(card.toughness(), &q[9..]) {
1369                    return false;
1370                }
1371            }
1372            "ChosenColor" => {
1373                if source_chosen_colors.is_empty() {
1374                    return false;
1375                }
1376                let mut chosen_set = ColorSet::COLORLESS;
1377                for name in source_chosen_colors {
1378                    chosen_set = chosen_set.union(ColorSet::from_names(name));
1379                }
1380                if !card.color.shares_color_with(chosen_set) {
1381                    return false;
1382                }
1383            }
1384            // Fallback: treat unknown qualifiers as subtype checks. Java's
1385            // CardPredicates does the same — `Card.Elemental` filters by
1386            // the Elemental subtype. Changeling creatures are every creature
1387            // type, so a changeling creature passes any creature-type check.
1388            // Generic `non<Subtype>` is handled here so Sunderflock's
1389            // `Creature.nonElemental` bounce filter rejects Elementals without
1390            // needing an explicit arm per subtype.
1391            other => {
1392                if let Some(stripped) = other.strip_prefix("non") {
1393                    let is_creature_type = !stripped.is_empty();
1394                    if !is_creature_type {
1395                        return false;
1396                    }
1397                    let has_sub = card.type_line.has_subtype(stripped);
1398                    let changeling_match = card.is_creature() && card.has_keyword("Changeling");
1399                    if has_sub || changeling_match {
1400                        return false;
1401                    }
1402                    continue;
1403                }
1404                let has_sub = card.type_line.has_subtype(other);
1405                let changeling_match = card.is_creature() && card.has_keyword("Changeling");
1406                if !has_sub && !changeling_match {
1407                    return false;
1408                }
1409            }
1410        }
1411    }
1412
1413    true
1414}
1415
1416fn matches_numeric_qualifier(actual: i32, comparison: &str) -> bool {
1417    if comparison.len() < 3 {
1418        return true;
1419    }
1420    let op = &comparison[..2];
1421    let rest = &comparison[2..];
1422    if let Ok(expected) = rest.parse::<i32>() {
1423        match op {
1424            "EQ" => actual == expected,
1425            "LE" => actual <= expected,
1426            "GE" => actual >= expected,
1427            "LT" => actual < expected,
1428            "GT" => actual > expected,
1429            "NE" => actual != expected,
1430            _ => true,
1431        }
1432    } else {
1433        true
1434    }
1435}
1436
1437// ── X-count and math helpers ────────────────────────────────────────
1438
1439/// Evaluate an X-count expression on a card.
1440/// Mirrors Java's `AbilityUtils.xCount(Card c, String s, CardTraitBase ctb)`.
1441///
1442/// Delegates to the SVar resolution system which already handles Count$,
1443/// Number$, SVar$, and most sub-expressions.
1444pub fn x_count(game: &GameState, card_id: CardId, expr: &str, sa: &SpellAbility) -> i32 {
1445    let controller = game.card(card_id).controller;
1446    // Check for Number$ prefix
1447    if let Some(rest) = expr.strip_prefix("Number$") {
1448        let parts: Vec<&str> = rest.split('/').collect();
1449        let base = parts[0].trim().parse::<i32>().unwrap_or(0);
1450        let operators = parts.get(1).copied().unwrap_or("");
1451        return do_x_math(base, operators);
1452    }
1453
1454    // Strip Count$ prefix if present
1455    let stripped = expr.strip_prefix("Count$").unwrap_or(expr);
1456
1457    // Handle SVar$ indirection
1458    if let Some(svar_name) = stripped.strip_prefix("SVar$") {
1459        if let Some(svar_val) = game.card(card_id).get_s_var(svar_name.trim()) {
1460            let val = svar_val;
1461            return x_count(game, card_id, val, sa);
1462        }
1463        return 0;
1464    }
1465
1466    // Delegate to the full SVar resolution system
1467    let full_expr = if expr.starts_with("Count$") || expr.starts_with("Number$") {
1468        expr.to_string()
1469    } else {
1470        format!("Count${}", expr)
1471    };
1472    crate::svar::resolve_count_svar_for_sa(&full_expr, game, card_id, controller, sa)
1473}
1474
1475/// Apply arithmetic operators to a base value.
1476/// Mirrors Java's `AbilityUtils.doXMath(int num, String operators, Card c, CardTraitBase ctb)`.
1477///
1478/// Delegates to the existing `svar::do_x_math` which already implements all operators
1479/// (Plus, Minus, NMinus, Twice, Thrice, HalfUp, HalfDown, Negative, Times, Abs,
1480/// LimitMax, LimitMin, DivideEvenlyUp, DivideEvenlyDown).
1481pub fn do_x_math(num: i32, operators: &str) -> i32 {
1482    if operators.is_empty() || operators == "none" {
1483        return num;
1484    }
1485    // The svar module's do_x_math is private, but we replicate its logic here.
1486    let parts: Vec<&str> = operators.split('.').collect();
1487    let op = parts.first().copied().unwrap_or("");
1488    let secondary = parts
1489        .get(1)
1490        .and_then(|s| s.parse::<i32>().ok())
1491        .unwrap_or(0);
1492
1493    if op.contains("Plus") {
1494        num + secondary
1495    } else if op.contains("NMinus") {
1496        secondary - num
1497    } else if op.contains("Minus") {
1498        num - secondary
1499    } else if op.contains("Twice") {
1500        num * 2
1501    } else if op.contains("Thrice") {
1502        num * 3
1503    } else if op.contains("HalfUp") {
1504        ((num as f64) / 2.0).ceil() as i32
1505    } else if op.contains("HalfDown") {
1506        ((num as f64) / 2.0).floor() as i32
1507    } else if op.contains("ThirdUp") {
1508        ((num as f64) / 3.0).ceil() as i32
1509    } else if op.contains("ThirdDown") {
1510        ((num as f64) / 3.0).floor() as i32
1511    } else if op.contains("Negative") {
1512        -num
1513    } else if op.contains("Times") {
1514        num * secondary
1515    } else if op.contains("DivideEvenlyUp") {
1516        if secondary == 0 {
1517            0
1518        } else {
1519            num / secondary + i32::from(num % secondary != 0)
1520        }
1521    } else if op.contains("DivideEvenlyDown") {
1522        if secondary == 0 {
1523            0
1524        } else {
1525            num / secondary
1526        }
1527    } else if op.contains("Abs") {
1528        num.abs()
1529    } else if op.contains("LimitMax") {
1530        num.min(secondary)
1531    } else if op.contains("LimitMin") {
1532        num.max(secondary)
1533    } else {
1534        num
1535    }
1536}
1537
1538/// Evaluate player-based X-count expressions.
1539/// Mirrors Java's `AbilityUtils.playerXCount(List<Player>, String, Card, CardTraitBase)`.
1540///
1541/// Sums a property across a list of players. Common properties:
1542/// - `LifeTotal`, `Poison`, `CardsInHand`, `DomainCount`, etc.
1543pub fn player_x_count(
1544    game: &GameState,
1545    players: &[PlayerId],
1546    expr: &str,
1547    source_id: CardId,
1548    sa: &SpellAbility,
1549) -> i32 {
1550    let parts: Vec<&str> = expr.split('/').collect();
1551    let property = parts[0].trim();
1552    let operators = parts.get(1).copied().unwrap_or("");
1553
1554    let base: i32 = players
1555        .iter()
1556        .map(|&pid| player_x_property(game, pid, property, source_id, sa))
1557        .sum();
1558
1559    do_x_math(base, operators)
1560}
1561
1562/// Get a numeric property for a single player.
1563/// Mirrors Java's `AbilityUtils.playerXProperty(Player, String, Card, CardTraitBase)`.
1564pub fn player_x_property(
1565    game: &GameState,
1566    player: PlayerId,
1567    property: &str,
1568    _source_id: CardId,
1569    _sa: &SpellAbility,
1570) -> i32 {
1571    let p = game.player(player);
1572    match property {
1573        "LifeTotal" => p.life,
1574        "Poison" | "PoisonCounters" => p.poison_counters,
1575        "CardsInHand" => game.cards_in_zone(ZoneType::Hand, player).len() as i32,
1576        "CardsInLibrary" => game.cards_in_zone(ZoneType::Library, player).len() as i32,
1577        "CardsInGraveyard" | "GraveyardSize" => {
1578            game.cards_in_zone(ZoneType::Graveyard, player).len() as i32
1579        }
1580        "StartingLife" => p.starting_life,
1581        "LandsPlayedThisTurn" => p.lands_played_this_turn,
1582        "DiscardedThisTurn" | "NumDiscardedThisTurn" => p.discarded_this_turn,
1583        "NumRollsThisTurn" | "DiceRolledThisTurn" => p.num_rolls_this_turn,
1584        "AttractionsVisitedThisTurn" => p.attractions_visited_this_turn,
1585        "DomainCount" => {
1586            // Count distinct basic land types among lands the player controls
1587            let lands = game.cards_in_zone(ZoneType::Battlefield, player);
1588            let mut types = std::collections::HashSet::new();
1589            for &cid in lands {
1590                let card = game.card(cid);
1591                if !card.is_land() {
1592                    continue;
1593                }
1594                for subtype in &card.type_line.subtypes {
1595                    let lower = subtype.to_ascii_lowercase();
1596                    if matches!(
1597                        lower.as_str(),
1598                        "plains" | "island" | "swamp" | "mountain" | "forest"
1599                    ) {
1600                        types.insert(lower);
1601                    }
1602                }
1603            }
1604            types.len() as i32
1605        }
1606        "NumCreaturesYouCtrl" | "CreatureCount" => game
1607            .cards_in_zone(ZoneType::Battlefield, player)
1608            .iter()
1609            .filter(|&&cid| game.card(cid).is_creature())
1610            .count() as i32,
1611        _ => 0,
1612    }
1613}
1614
1615/// Evaluate object-based X-count expressions.
1616/// Mirrors Java's `AbilityUtils.objectXCount(List<?>, String, Card, CardTraitBase)`.
1617///
1618/// Counts or sums properties across a list of card IDs.
1619pub fn object_x_count(game: &GameState, objects: &[CardId], expr: &str) -> i32 {
1620    let parts: Vec<&str> = expr.split('/').collect();
1621    let property = parts[0].trim();
1622    let operators = parts.get(1).copied().unwrap_or("");
1623
1624    let base: i32 = match property {
1625        "Amount" | "Count" => objects.len() as i32,
1626        "TotalPower" => objects.iter().map(|&cid| game.card(cid).power()).sum(),
1627        "TotalToughness" => objects.iter().map(|&cid| game.card(cid).toughness()).sum(),
1628        "TotalCMC" | "TotalManaValue" => {
1629            objects.iter().map(|&cid| game.card(cid).mana_value()).sum()
1630        }
1631        "SumPower" => objects.iter().map(|&cid| game.card(cid).power()).sum(),
1632        _ => objects.len() as i32,
1633    };
1634
1635    do_x_math(base, operators)
1636}
1637
1638// ── Remembering and Paid helpers ─────────────────────────────────────
1639
1640/// Handle remembering targets/objects after a spell resolves.
1641/// Mirrors Java's `AbilityUtils.handleRemembering(SpellAbility)`.
1642///
1643/// If the SA has `RememberTargets$`, stores the targeted card(s) in the
1644/// host card's remembered_cards list.
1645pub fn handle_remembering(game: &mut GameState, sa: &SpellAbility) {
1646    let host_id = match sa.source {
1647        Some(id) => id,
1648        None => return,
1649    };
1650
1651    if sa.ir.remember_targets && sa.uses_targeting() {
1652        if sa.ir.forget_other_targets {
1653            game.card_mut(host_id).clear_remembered();
1654        }
1655        if let Some(target_card) = sa.target_chosen.target_card {
1656            game.card_mut(host_id).add_remembered_card(target_card);
1657        }
1658        if let Some(target_player) = sa.target_chosen.target_player {
1659            game.card_mut(host_id).add_remembered_player(target_player);
1660        }
1661        // Counter-style targets: the target is a stack entry (a SpellAbility),
1662        // and Java's `host.addRemembered(sa.getTargets())` records the source
1663        // card of that ability so `RememberObjects$ RememberedController` can
1664        // later resolve to the original spell's controller (e.g. Arcane Denial).
1665        if let Some(stack_id) = sa.target_chosen.target_stack_entry {
1666            if let Some(entry) = game.stack.find_by_id(stack_id) {
1667                if let Some(source) = entry.spell_ability.source {
1668                    game.card_mut(host_id).add_remembered_card(source);
1669                }
1670            }
1671        }
1672    }
1673
1674    // RememberCostMana — store the mana colors used to pay
1675    // In the Rust engine this is simplified since we don't track individual mana objects.
1676    // We store a count in remembered_cmc.
1677    if sa.ir.remember_cost_mana {
1678        game.card_mut(host_id).clear_remembered();
1679    }
1680}
1681
1682/// Handle paid-list counting for X-count expressions.
1683/// Mirrors Java's `AbilityUtils.handlePaid(Iterable<Card>, String, Card, CardTraitBase)`.
1684///
1685/// Evaluates properties of cards that were paid as costs (e.g. sacrificed, discarded).
1686pub fn handle_paid(
1687    game: &GameState,
1688    paid_cards: &[CardId],
1689    property: &str,
1690    _source_id: CardId,
1691) -> i32 {
1692    if paid_cards.is_empty() {
1693        return 0;
1694    }
1695
1696    match property {
1697        "Amount" | "Count" => paid_cards.len() as i32,
1698        "CardPower" => paid_cards
1699            .iter()
1700            .map(|&cid| {
1701                let card = game.card(cid);
1702                card.lki_power.unwrap_or_else(|| card.power())
1703            })
1704            .sum(),
1705        "CardBasePower" => paid_cards
1706            .iter()
1707            .map(|&cid| game.card(cid).base_power.unwrap_or(0))
1708            .sum(),
1709        "CardToughness" => paid_cards
1710            .iter()
1711            .map(|&cid| {
1712                let card = game.card(cid);
1713                card.lki_toughness.unwrap_or_else(|| card.toughness())
1714            })
1715            .sum(),
1716        "CardSumPT" => paid_cards
1717            .iter()
1718            .map(|&cid| {
1719                let card = game.card(cid);
1720                card.lki_power.unwrap_or_else(|| card.power())
1721                    + card.lki_toughness.unwrap_or_else(|| card.toughness())
1722            })
1723            .sum(),
1724        "CardManaCost" | "ManaCost" => paid_cards
1725            .iter()
1726            .map(|&cid| game.card(cid).mana_value())
1727            .sum(),
1728        "TotalPower" | "SumPower" => paid_cards
1729            .iter()
1730            .map(|&cid| {
1731                let card = game.card(cid);
1732                card.lki_power.unwrap_or(card.base_power.unwrap_or(0))
1733            })
1734            .sum(),
1735        "TotalToughness" | "SumToughness" => paid_cards
1736            .iter()
1737            .map(|&cid| {
1738                let card = game.card(cid);
1739                card.lki_toughness
1740                    .unwrap_or(card.base_toughness.unwrap_or(0))
1741            })
1742            .sum(),
1743        "TotalCMC" | "SumCMC" => paid_cards
1744            .iter()
1745            .map(|&cid| game.card(cid).mana_value())
1746            .sum(),
1747        _ if property.starts_with("Valid ") => {
1748            let filter = property.strip_prefix("Valid ").unwrap_or("");
1749            paid_cards
1750                .iter()
1751                .filter(|&&cid| matches_change_type(game.card(cid), filter, &[]))
1752                .count() as i32
1753        }
1754        _ => paid_cards.len() as i32,
1755    }
1756}
1757
1758// ── Type Counting helpers ────────────────────────────────────────────
1759
1760/// Count distinct card types among a list of cards.
1761/// Mirrors Java's `AbilityUtils.countCardTypesFromList(Iterable<Card>, boolean)`.
1762///
1763/// If `permanent_types` is true, only counts types that are permanent types
1764/// (Artifact, Creature, Enchantment, Land, Planeswalker).
1765pub fn count_card_types_from_list(
1766    game: &GameState,
1767    cards: &[CardId],
1768    permanent_types: bool,
1769) -> i32 {
1770    let mut types = std::collections::HashSet::new();
1771    for &cid in cards {
1772        let card = game.card(cid);
1773        for ct in &card.type_line.core_types {
1774            types.insert(ct.name().to_string());
1775        }
1776    }
1777    if permanent_types {
1778        types
1779            .iter()
1780            .filter(|t| {
1781                matches!(
1782                    t.as_str(),
1783                    "Artifact"
1784                        | "Creature"
1785                        | "Enchantment"
1786                        | "Land"
1787                        | "Planeswalker"
1788                        | "Battle"
1789                        | "Kindred"
1790                )
1791            })
1792            .count() as i32
1793    } else {
1794        types.len() as i32
1795    }
1796}
1797
1798/// Count distinct supertypes among a list of cards.
1799/// Mirrors Java's `AbilityUtils.countSuperTypesFromList(Iterable<Card>)`.
1800pub fn count_super_types_from_list(game: &GameState, cards: &[CardId]) -> i32 {
1801    let mut types = std::collections::HashSet::new();
1802    for &cid in cards {
1803        let card = game.card(cid);
1804        for st in &card.type_line.supertypes {
1805            types.insert(*st);
1806        }
1807    }
1808    types.len() as i32
1809}
1810
1811/// Count distinct subtypes among a list of cards.
1812/// Mirrors Java's `AbilityUtils.countSubTypesFromList(Iterable<Card>)`.
1813pub fn count_sub_types_from_list(game: &GameState, cards: &[CardId]) -> i32 {
1814    let mut types = std::collections::HashSet::new();
1815    for &cid in cards {
1816        let card = game.card(cid);
1817        for subtype in &card.type_line.subtypes {
1818            types.insert(subtype.clone());
1819        }
1820    }
1821    types.len() as i32
1822}
1823
1824// ── UnlessCost ───────────────────────────────────────────────────────
1825
1826/// Calculate the cost for an UnlessCost$ clause.
1827/// Mirrors Java's `AbilityUtils.calculateUnlessCost(SpellAbility, String, boolean)`.
1828///
1829/// Parses the UnlessCost$ string and returns a Cost object that the opponent
1830/// may choose to pay to prevent the effect.
1831pub fn calculate_unless_cost(
1832    game: &GameState,
1833    sa: &SpellAbility,
1834    unless_cost: &str,
1835) -> Option<crate::cost::Cost> {
1836    if unless_cost.is_empty() {
1837        return None;
1838    }
1839
1840    // Handle "ChosenNumber" — mana cost equal to the chosen number
1841    if unless_cost == "ChosenNumber" {
1842        if let Some(source_id) = sa.source {
1843            let chosen = game.card(source_id).chosen_number.unwrap_or(0);
1844            let mana_str = chosen.to_string();
1845            return Some(crate::cost::parse_cost(&mana_str));
1846        }
1847        return None;
1848    }
1849
1850    // Handle SVar reference — look up the SVar, calculate its amount
1851    if let Some(source_id) = sa.source {
1852        if let Some(svar_val) = game.card(source_id).get_s_var(unless_cost.trim()) {
1853            if !svar_val.is_empty() && unless_cost != "X" {
1854                let amount = crate::svar::resolve_count_svar_for_sa(
1855                    svar_val,
1856                    game,
1857                    source_id,
1858                    sa.activating_player,
1859                    sa,
1860                );
1861                let mana_str = amount.to_string();
1862                return Some(crate::cost::parse_cost(&mana_str));
1863            }
1864        }
1865    }
1866
1867    // Default: parse the cost string directly
1868    Some(crate::cost::parse_cost(unless_cost))
1869}
1870
1871// ── Filter by Type ───────────────────────────────────────────────────
1872
1873/// Filter a list of card IDs by a type/valid-cards expression.
1874/// Mirrors Java's `AbilityUtils.filterListByType(CardCollectionView, String, SpellAbility)`.
1875///
1876/// Handles Triggered*, Targeted*, Remembered* prefixes that redirect
1877/// the source card for validation, then applies standard valid-cards matching.
1878pub fn filter_list_by_type(
1879    game: &GameState,
1880    cards: &[CardId],
1881    filter_type: &str,
1882    sa: &SpellAbility,
1883) -> Vec<CardId> {
1884    if filter_type.is_empty() {
1885        return cards.to_vec();
1886    }
1887
1888    // Handle Triggered prefix — resolve to a trigger card, then adjust filter
1889    let (effective_source, effective_filter) = if filter_type.starts_with("Triggered") {
1890        // Look up the triggered card object
1891        let trigger_card = sa
1892            .get_triggering_card(AbilityKey::Card)
1893            .or_else(|| sa.get_triggering_card(AbilityKey::Object))
1894            .or_else(|| sa.get_triggering_card(AbilityKey::Attacker))
1895            .or_else(|| sa.get_triggering_card(AbilityKey::Blocker));
1896
1897        match trigger_card {
1898            Some(cid) => {
1899                let adjusted = filter_type
1900                    .replace("TriggeredCard", "Card")
1901                    .replace("TriggeredObject", "Card")
1902                    .replace("TriggeredAttacker", "Card")
1903                    .replace("TriggeredBlocker", "Card")
1904                    .replace("Triggered", "Card");
1905                (Some(cid), adjusted)
1906            }
1907            None => return Vec::new(),
1908        }
1909    } else if filter_type.starts_with("Targeted") {
1910        // Use the targeted card as the source
1911        match sa.target_chosen.target_card {
1912            Some(cid) => {
1913                let adjusted = filter_type
1914                    .replace("TargetedCard", "Card")
1915                    .replace("Targeted", "Card");
1916                (Some(cid), adjusted)
1917            }
1918            None => return Vec::new(),
1919        }
1920    } else if filter_type.starts_with("Remembered") {
1921        // Use the first remembered card as the source
1922        let source_id = sa.source.unwrap_or(CardId(0));
1923        let remembered = &game.card(source_id).remembered_cards;
1924        match remembered.first() {
1925            Some(&cid) => {
1926                let adjusted = filter_type.replace("Remembered", "Card");
1927                (Some(cid), adjusted)
1928            }
1929            None => return Vec::new(),
1930        }
1931    } else {
1932        (sa.source, filter_type.to_string())
1933    };
1934
1935    let selector = cached_compiled_selector(&effective_filter);
1936    cards
1937        .iter()
1938        .copied()
1939        .filter(|&cid| {
1940            let card = game.card(cid);
1941            // If the filter mentions "Self", it means the card being tested is the source
1942            if effective_filter.contains(".Self") {
1943                if let Some(src) = effective_source {
1944                    return cid == src;
1945                }
1946            }
1947            if let Some(source_id) = effective_source {
1948                matches_valid_cards_for_source(
1949                    game,
1950                    source_id,
1951                    card,
1952                    Some(&selector),
1953                    &effective_filter,
1954                )
1955            } else {
1956                matches_valid_cards(card, &effective_filter, sa.activating_player)
1957            }
1958        })
1959        .collect()
1960}
1961
1962// ── Mana Color Conversion ────────────────────────────────────────────
1963
1964/// Apply mana color conversion rules to a mana conversion matrix.
1965/// Mirrors Java's `AbilityUtils.applyManaColorConversion(ManaConversionMatrix, String)`.
1966///
1967/// Parses conversion strings like "White->Any", "nonGreen<-Black", etc.
1968/// In the Rust engine, we represent the conversion as a map from source color
1969/// to allowed replacement colors. The actual ManaConversionMatrix is stored
1970/// on the player; this function modifies it in place.
1971///
1972/// Format: "SourceColor->TargetColor" (additive) or "SourceColor<-TargetColor" (restrictive)
1973/// Multiple pairs separated by spaces.
1974pub fn apply_mana_color_conversion(
1975    conversions: &mut std::collections::HashMap<String, Vec<String>>,
1976    conversion_str: &str,
1977) {
1978    for pair in conversion_str.split_whitespace() {
1979        let (additive, sides) = if pair.contains("->") {
1980            (true, pair.split("->").collect::<Vec<_>>())
1981        } else if pair.contains("<-") {
1982            (false, pair.split("<-").collect::<Vec<_>>())
1983        } else {
1984            continue;
1985        };
1986
1987        if sides.len() != 2 {
1988            continue;
1989        }
1990
1991        let source_spec = sides[0];
1992        let target = sides[1].to_string();
1993
1994        let source_colors: Vec<String> = if source_spec == "AnyColor" {
1995            vec!["W", "U", "B", "R", "G"]
1996                .into_iter()
1997                .map(String::from)
1998                .collect()
1999        } else if source_spec == "AnyType" {
2000            vec!["W", "U", "B", "R", "G", "C"]
2001                .into_iter()
2002                .map(String::from)
2003                .collect()
2004        } else if let Some(excluded) = source_spec.strip_prefix("non") {
2005            vec!["W", "U", "B", "R", "G"]
2006                .into_iter()
2007                .filter(|c| !c.eq_ignore_ascii_case(excluded))
2008                .map(String::from)
2009                .collect()
2010        } else {
2011            vec![source_spec.to_string()]
2012        };
2013
2014        for source in source_colors {
2015            if additive {
2016                conversions.entry(source).or_default().push(target.clone());
2017            } else {
2018                // Restrictive: only allow this replacement
2019                conversions.insert(source, vec![target.clone()]);
2020            }
2021        }
2022    }
2023}
2024
2025// ── Text Change Effects ──────────────────────────────────────────────
2026
2027/// Apply text change effects from a card's SVars to a string.
2028/// Mirrors Java's `AbilityUtils.applyTextChangeEffects(String, boolean, Map, Map)`.
2029///
2030/// Text change data is stored in SVars with `TextColor:` and `TextType:` prefixes.
2031/// `is_descriptive` is preserved for parity but the Rust engine does direct replacement.
2032pub fn apply_text_change_effects(
2033    def: &str,
2034    is_descriptive: bool,
2035    color_map: &std::collections::BTreeMap<String, String>,
2036    type_map: &std::collections::BTreeMap<String, String>,
2037) -> String {
2038    if def.is_empty() {
2039        return def.to_string();
2040    }
2041
2042    let mut replaced = def.to_string();
2043
2044    // Apply color changes
2045    for (key, value) in color_map {
2046        if key == "Any" {
2047            for color in &["white", "blue", "black", "red", "green"] {
2048                let cap = capitalize(color);
2049                if value.eq_ignore_ascii_case(color) {
2050                    continue; // Don't replace color with itself
2051                }
2052                replaced = replaced.replace(color, &value.to_lowercase());
2053                replaced = replaced.replace(&cap, &capitalize(value));
2054            }
2055        } else {
2056            replaced = replaced.replace(&key.to_lowercase(), &value.to_lowercase());
2057            replaced = replaced.replace(key, value);
2058        }
2059    }
2060
2061    // Apply type changes
2062    for (key, value) in type_map {
2063        if is_descriptive {
2064            // Also replace plural forms
2065            let plural_key = pluralize_type(key);
2066            let plural_value = pluralize_type(value);
2067            replaced = replaced.replace(&plural_key, &plural_value);
2068        }
2069        replaced = replaced.replace(key, value);
2070    }
2071
2072    replaced
2073}
2074
2075/// Extract color and type change maps from a Card's SVars.
2076/// Text changes are stored as SVars with `TextColor:` and `TextType:` prefixes.
2077fn extract_text_change_maps(
2078    card: &Card,
2079) -> (
2080    std::collections::BTreeMap<String, String>,
2081    std::collections::BTreeMap<String, String>,
2082) {
2083    let mut color_map = std::collections::BTreeMap::new();
2084    let mut type_map = std::collections::BTreeMap::new();
2085    for (key, value) in &card.svars {
2086        if let Some(from) = key.strip_prefix("TextColor:") {
2087            color_map.insert(from.to_string(), value.clone());
2088        } else if let Some(from) = key.strip_prefix("TextType:") {
2089            type_map.insert(from.to_string(), value.clone());
2090        }
2091    }
2092    (color_map, type_map)
2093}
2094
2095/// Apply ability text change effects (for non-descriptive ability text).
2096/// Mirrors Java's `AbilityUtils.applyAbilityTextChangeEffects(String, CardTraitBase)`.
2097pub fn apply_ability_text_change_effects(def: &str, card: &Card) -> String {
2098    let (color_map, type_map) = extract_text_change_maps(card);
2099    apply_text_change_effects(def, false, &color_map, &type_map)
2100}
2101
2102/// Apply keyword text change effects.
2103/// Mirrors Java's `AbilityUtils.applyKeywordTextChangeEffects(String, Card)`.
2104pub fn apply_keyword_text_change_effects(kw: &str, card: &Card) -> String {
2105    let (color_map, type_map) = extract_text_change_maps(card);
2106    apply_text_change_effects(kw, false, &color_map, &type_map)
2107}
2108
2109/// Apply description text change effects (includes strikethrough in Java UI;
2110/// in the Rust engine we just do the replacement).
2111/// Mirrors Java's `AbilityUtils.applyDescriptionTextChangeEffects(String, CardTraitBase)`.
2112pub fn apply_description_text_change_effects(def: &str, card: &Card) -> String {
2113    let (color_map, type_map) = extract_text_change_maps(card);
2114    apply_text_change_effects(def, true, &color_map, &type_map)
2115}
2116
2117fn capitalize(s: &str) -> String {
2118    let mut chars = s.chars();
2119    match chars.next() {
2120        None => String::new(),
2121        Some(c) => c.to_uppercase().to_string() + chars.as_str(),
2122    }
2123}
2124
2125fn pluralize_type(type_name: &str) -> String {
2126    if type_name.ends_with('s') {
2127        type_name.to_string()
2128    } else if let Some(stem) = type_name.strip_suffix('y') {
2129        format!("{stem}ies")
2130    } else {
2131        format!("{type_name}s")
2132    }
2133}
2134
2135// ── Splice ───────────────────────────────────────────────────────────
2136
2137/// Add splice effects to a spell ability.
2138/// Mirrors Java's `AbilityUtils.addSpliceEffects(SpellAbility)`.
2139///
2140/// Checks the casting player's hand for cards with "Splice onto <type>"
2141/// keyword, and if any match the spell being cast, offers the player a
2142/// chance to splice them. Spliced card abilities are appended as sub-abilities.
2143///
2144/// Returns the (potentially modified) spell ability.
2145pub fn add_splice_effects(
2146    game: &GameState,
2147    sa: SpellAbility,
2148    _agents: &mut [Box<dyn crate::agent::PlayerAgent>],
2149) -> SpellAbility {
2150    let source_id = match sa.source {
2151        Some(id) => id,
2152        None => return sa,
2153    };
2154    let source = game.card(source_id);
2155    let player = sa.activating_player;
2156
2157    // Only spells can be spliced onto; copies can't
2158    if !sa.is_spell || sa.is_copy {
2159        return sa;
2160    }
2161
2162    let hand = game.cards_in_zone(ZoneType::Hand, player);
2163    if hand.is_empty() {
2164        return sa;
2165    }
2166
2167    // Find cards in hand with Splice keyword that match the source spell type
2168    let splice_candidates: Vec<CardId> = hand
2169        .iter()
2170        .copied()
2171        .filter(|&cid| {
2172            if cid == source_id {
2173                return false;
2174            }
2175            let card = game.card(cid);
2176            // Check if card has a "Splice onto" keyword matching the source spell type
2177            if let Some(splice_rest) = card.keywords.find_with_prefix("Splice onto ") {
2178                let splice_type = splice_rest
2179                    .strip_prefix("Splice onto ")
2180                    .unwrap_or("")
2181                    .split(' ')
2182                    .next()
2183                    .unwrap_or("");
2184                source
2185                    .type_line
2186                    .core_types
2187                    .iter()
2188                    .any(|ct| ct.name().eq_ignore_ascii_case(splice_type))
2189                    || splice_type.eq_ignore_ascii_case("instant") && source.type_line.is_instant()
2190                    || splice_type.eq_ignore_ascii_case("arcane")
2191                        && source
2192                            .type_line
2193                            .subtypes
2194                            .iter()
2195                            .any(|s| s.eq_ignore_ascii_case("Arcane"))
2196            } else {
2197                false
2198            }
2199        })
2200        .collect();
2201
2202    if splice_candidates.is_empty() {
2203        return sa;
2204    }
2205
2206    // For now, the agent system doesn't have a splice choice method.
2207    // Return the SA unchanged — splice is a rare mechanic and will be
2208    // fully wired when the agent interface is extended.
2209    sa
2210}
2211
2212/// Add a single splice effect from a card onto a spell ability.
2213/// Mirrors Java's `AbilityUtils.addSpliceEffect(SpellAbility, Card)`.
2214///
2215/// Appends the first spell ability of the spliced card as a sub-ability
2216/// at the end of the SA chain, and adds the splice cost to the total cost.
2217pub fn add_splice_effect(sa: &mut SpellAbility, game: &GameState, splice_card_id: CardId) {
2218    let splice_card = game.card(splice_card_id);
2219
2220    // Find the first ability text on the splice card
2221    let first_ability = match splice_card.abilities.first() {
2222        Some(text) => text.clone(),
2223        None => return,
2224    };
2225
2226    // Build a sub-ability from the splice card's first ability
2227    let mut sub_sa = crate::spellability::build_spell_ability(
2228        game,
2229        splice_card_id,
2230        &first_ability,
2231        sa.activating_player,
2232    );
2233    sub_sa.source = sa.source; // Use the host card as source
2234    sub_sa.activating_player = sa.activating_player;
2235
2236    // Append at the end of the sub-ability chain
2237    let mut slot = &mut sa.sub_ability;
2238    loop {
2239        match slot {
2240            Some(node) => slot = &mut node.sub_ability,
2241            None => {
2242                *slot = Some(Box::new(sub_sa));
2243                break;
2244            }
2245        }
2246    }
2247
2248    // Track that this card was spliced
2249    sa.spliced_cards.push(splice_card_id);
2250
2251    // Update description
2252    let name = splice_card.card_name.clone();
2253    if !sa.description.is_empty() {
2254        sa.description
2255            .push_str(&format!(" (Splicing {} onto it)", name));
2256    }
2257}
2258
2259// ── Defined$ convenience overloads ───────────────────────────────────
2260
2261/// Resolve a `Defined$` expression to a combined list of the targeted
2262/// `GameObject`s — cards + players + spell abilities.
2263/// Mirrors Java `AbilityUtils.getDefinedObjects(Card, String, CardTraitBase)`.
2264///
2265/// The three sub-resolvers are surfaced separately via their native Rust enums
2266/// rather than a shared `GameObject` trait object, so this helper returns a
2267/// tuple instead of Java's `FCollection<GameObject>`.
2268#[allow(clippy::type_complexity)]
2269pub fn get_defined_objects(
2270    defined: &str,
2271    sa: &SpellAbility,
2272    game: &GameState,
2273) -> (Vec<PlayerId>, Vec<CardId>, Vec<SpellAbility>) {
2274    let d = if defined.is_empty() { "Self" } else { defined };
2275    (
2276        resolve_defined_players_with_sa(d, sa, sa.activating_player, game),
2277        get_defined_cards(game, sa.source, d, Some(sa.activating_player)),
2278        get_defined_spell_abilities(d, sa, game),
2279    )
2280}
2281
2282/// Resolve a `Defined$` expression to a combined player + card list.
2283/// Mirrors Java `AbilityUtils.getDefinedEntities(Card, String, CardTraitBase)`.
2284pub fn get_defined_entities(
2285    defined: &str,
2286    sa: &SpellAbility,
2287    game: &GameState,
2288) -> (Vec<PlayerId>, Vec<CardId>) {
2289    let d = if defined.is_empty() { "Self" } else { defined };
2290    (
2291        resolve_defined_players_with_sa(d, sa, sa.activating_player, game),
2292        get_defined_cards(game, sa.source, d, Some(sa.activating_player)),
2293    )
2294}
2295
2296/// Collect the basic spell abilities that a Play$ effect could cast from
2297/// `tgt_card`. Mirrors Java `AbilityUtils.getBasicSpellsFromPlayEffect(Card, Player)`.
2298///
2299/// "Basic" means the non-alternative cost variants — the Rust engine has no
2300/// `Spell` wrapper analogue yet, so we synthesize fresh `SpellAbility` objects
2301/// from the target card's ability text, filtering to spells / land abilities
2302/// the way Java does in `collectSpellsForPlayEffect`.
2303pub fn get_basic_spells_from_play_effect(
2304    game: &GameState,
2305    tgt_card: CardId,
2306    controller: PlayerId,
2307) -> Vec<SpellAbility> {
2308    get_spells_from_play_effect(game, tgt_card, controller, false)
2309}
2310
2311/// Full variant of `getBasicSpellsFromPlayEffect` — when `with_alt_cost` is
2312/// `true`, alternative-cost (flashback/overload/…) variants are included.
2313/// Mirrors Java `AbilityUtils.getSpellsFromPlayEffect(Card, Player, CardStateName, boolean)`.
2314///
2315/// Alt-cost expansion depends on `GameActionUtil.getAlternativeCosts`, which
2316/// is not ported yet — the `with_alt_cost=true` branch currently degrades to
2317/// the basic list. Revisit once alt-cost collection is available.
2318pub fn get_spells_from_play_effect(
2319    game: &GameState,
2320    tgt_card: CardId,
2321    controller: PlayerId,
2322    _with_alt_cost: bool,
2323) -> Vec<SpellAbility> {
2324    let card = game.card(tgt_card);
2325    let mut out = Vec::new();
2326    for ab_text in &card.abilities {
2327        let params = crate::parsing::Params::from_raw(ab_text);
2328        let record = crate::ability::ability_factory::AbilityRecordType::from_params(&params);
2329        let is_spell = matches!(
2330            record,
2331            Some(crate::ability::ability_factory::AbilityRecordType::Spell)
2332        );
2333        if !is_spell && !card.type_line.is_land() {
2334            continue;
2335        }
2336        let built =
2337            crate::spellability::build_spell_ability_from_host_card(card, ab_text, controller);
2338        out.push(built);
2339    }
2340    out
2341}
2342
2343/// Read an SVar from the SA's host, applying in-flight text-change effects
2344/// when the SA is intrinsic — mirrors Java `AbilityUtils.getSVar(CardTraitBase, String)`.
2345pub fn get_s_var(sa: &SpellAbility, game: &GameState, svar_name: &str) -> Option<String> {
2346    let host_id = sa.source?;
2347    let raw = game.card(host_id).get_s_var(svar_name)?.to_string();
2348    if !sa.is_intrinsic() || raw.is_empty() {
2349        return Some(raw);
2350    }
2351    // Text-change effects are applied at Card state time; no additional
2352    // rewrite needed here (see `apply_description_text_change_effects` for the
2353    // description path). Mirrors Java's behavior when no active text changes
2354    // match the SVar value.
2355    Some(raw)
2356}
2357
2358/// Returns `true` when a trait can't be linked to the cast-SA of `card` because
2359/// the trait originates from a different physical host (transformed / melded /
2360/// adventure-cast situations).
2361///
2362/// Mirrors Java `AbilityUtils.isUnlinkedFromCastSA(CardTraitBase, Card)`.
2363pub fn is_unlinked_from_cast_sa(sa: &SpellAbility, card: &Card) -> bool {
2364    if !sa.is_intrinsic() {
2365        return false;
2366    }
2367    if sa.source != Some(card.id) {
2368        return false;
2369    }
2370    // Java: getHostCard() vs cast_sa.getOriginalHost()/getHostCard() — if the
2371    // trait's original host disagrees with the cast-SA's host, the trait is
2372    // unlinked (e.g. intrinsic ability granted by a transformed face whose
2373    // cast-SA belongs to the front face).
2374    let trait_host = sa.original_host.or(sa.source);
2375    let Some(cast) = card.cast_sa.as_deref() else {
2376        return false;
2377    };
2378    let cast_host = cast.original_host.or(cast.source);
2379    match (trait_host, cast_host) {
2380        (Some(t), Some(c)) => t != c,
2381        _ => false,
2382    }
2383}
2384
2385// ── Resolve ──────────────────────────────────────────────────────────
2386
2387/// Top-level ability resolution entry point.
2388/// Mirrors Java's `AbilityUtils.resolve(SpellAbility)`.
2389///
2390/// In the Rust engine, resolution is handled by the effect dispatch system
2391/// in `effects::resolve_effect`. This function serves as the structural parity
2392/// entry point that the scanner expects to find.
2393pub fn resolve(ctx: &mut crate::ability::effects::EffectContext, sa: &SpellAbility) {
2394    crate::ability::effects::resolve_effect(ctx, sa);
2395}