Skip to main content

manabrew_engine/spellability/
mod.rs

1pub mod ability;
2pub mod ability_activated;
3pub mod ability_mana_part;
4pub mod ability_static;
5pub mod ability_sub;
6pub mod alternative_cost;
7pub mod land_ability;
8pub mod optional_cost;
9pub mod optional_cost_value;
10pub mod params;
11pub mod runtime_types;
12pub mod spell;
13pub mod spell_ability_condition;
14pub mod spell_ability_predicates;
15pub mod spell_ability_restriction;
16pub mod spell_ability_stack_instance;
17pub mod spell_ability_variables;
18pub mod spell_permanent;
19pub mod target_choices;
20pub mod target_restrictions;
21pub mod trait_spell_ability;
22pub mod valid_sa;
23
24use std::collections::HashMap;
25use std::sync::atomic::{AtomicU32, Ordering};
26
27use serde::{Deserialize, Serialize};
28
29use crate::ability::ability_factory::AbilityRecordType;
30use crate::ability::ability_ir::SpellAbilityIr;
31use crate::ability::api_type::ApiType;
32use crate::ability::AbilityKey;
33use crate::agent::PlayerAgent;
34use crate::card::card_damage_map::CardDamageMap;
35use crate::card::card_zone_table::CardZoneTable;
36use crate::card_trait_base::CardTraitIrOwner;
37use crate::cost::{parse_cost, Cost};
38use crate::event::AbilityValue;
39use crate::game::GameState;
40use crate::ids::{CardId, PlayerId};
41use crate::mana::ManaPool;
42use crate::parsing::{keys, Params, ParsedParams};
43
44pub use ability_mana_part::AbilityManaPart;
45pub use alternative_cost::{AlternativeCost, MORPH_GENERIC_COST, MORPH_PT};
46pub use optional_cost::OptionalCost;
47pub use optional_cost_value::OptionalCostValue;
48pub use runtime_types::{
49    AbilityDuration, ReplaceDyingCondition, SpellAbilityMode, TriggerCondition,
50};
51pub use spell_ability_condition::SpellAbilityCondition;
52pub use spell_ability_predicates::{has_sub_ability_api, is_api, is_valid};
53pub use spell_ability_restriction::SpellAbilityRestriction;
54pub use spell_ability_variables::SpellAbilityVariables;
55pub use target_choices::TargetChoices;
56pub use target_restrictions::{TargetKind, TargetRestrictions};
57pub use valid_sa::matches_valid_sa;
58
59static NEXT_SPELL_ABILITY_ID: AtomicU32 = AtomicU32::new(1);
60
61fn next_spell_ability_id() -> u32 {
62    NEXT_SPELL_ABILITY_ID.fetch_add(1, Ordering::Relaxed)
63}
64
65pub trait TriggerKeyInput {
66    fn into_ability_key(self) -> Option<AbilityKey>;
67}
68
69impl TriggerKeyInput for AbilityKey {
70    fn into_ability_key(self) -> Option<AbilityKey> {
71        Some(self)
72    }
73}
74
75impl TriggerKeyInput for &str {
76    fn into_ability_key(self) -> Option<AbilityKey> {
77        crate::ability::ability_key::from_string(self)
78    }
79}
80
81impl TriggerKeyInput for String {
82    fn into_ability_key(self) -> Option<AbilityKey> {
83        crate::ability::ability_key::from_string(&self)
84    }
85}
86
87impl TriggerKeyInput for &String {
88    fn into_ability_key(self) -> Option<AbilityKey> {
89        crate::ability::ability_key::from_string(self)
90    }
91}
92
93// ── SpellAbility (mirrors Java's SpellAbility.java) ──────────────────
94
95/// A spell or ability with its own targeting, costs, and sub-ability chain.
96/// Mirrors Java's `SpellAbility` class — each node in the chain has its own
97/// `target_restrictions`, `target_chosen`, `sub_ability`, `api`, etc.
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct SpellAbility {
100    #[serde(default)]
101    pub id: u32,
102    /// Effect API type (e.g. DealDamage, Destroy, Draw).
103    /// Mirrors Java's `ApiType api` field.
104    pub api: Option<ApiType>,
105    /// The card that hosts this ability. Mirrors Java's `hostCard`.
106    pub source: Option<CardId>,
107    /// Java parity: original host card for granted/copied abilities.
108    /// Used by costs like `Unattach<OriginalHost>`.
109    #[serde(default)]
110    pub original_host: Option<CardId>,
111    /// The player who activated/cast this. Mirrors Java's `activatingPlayer`.
112    pub activating_player: PlayerId,
113    /// The player who chooses this ability's targets. Mirrors Java's
114    /// `targetingPlayer` field.
115    pub targeting_player: Option<PlayerId>,
116    /// The raw ability text (pipe-delimited params).
117    pub ability_text: String,
118    /// Java parity: AB/SP/ST/DB record kind used to distinguish sub-abilities.
119    #[serde(default)]
120    pub record_type: AbilityRecordType,
121    /// Compiled Forge script IR; resolution reads typed fields from here.
122    /// Skipped on serde: a deserialized `SpellAbility` must rebuild it from
123    /// `ability_text` before use or every typed param reads as absent.
124    #[serde(skip)]
125    pub ir: SpellAbilityIr,
126    /// Targeting restrictions parsed from `ValidTgts$`.
127    /// `None` means this ability doesn't use targeting.
128    /// Mirrors Java's `targetRestrictions` field.
129    pub target_restrictions: Option<TargetRestrictions>,
130    /// The chosen targets for this ability.
131    /// Mirrors Java's `targetChosen` field.
132    pub target_chosen: TargetChoices,
133    /// Parsed costs from `Cost$` parameter.
134    /// Mirrors Java's `payCosts` field.
135    pub pay_costs: Option<Cost>,
136    /// Linked sub-ability chain. Mirrors Java's `subAbility` field
137    /// (AbilitySub extends SpellAbility).
138    pub sub_ability: Option<Box<SpellAbility>>,
139    /// Java parity: payload carried by `WrappedAbility`.
140    #[serde(default)]
141    pub wrapped_ability: Option<Box<SpellAbility>>,
142    /// Whether this is a spell (not an ability).
143    pub is_spell: bool,
144    /// Whether this is a triggered ability.
145    pub is_trigger: bool,
146    /// Whether this is an activated ability.
147    pub is_activated: bool,
148    /// Java parity: whether this ability is intrinsic to its host.
149    #[serde(default)]
150    pub intrinsic: bool,
151    /// Card that owns the trigger (for intervening-if recheck).
152    pub trigger_source: Option<CardId>,
153    /// Zone timestamp of the trigger source when this triggered ability was created.
154    /// Used to preserve object identity across zone changes (CR 400.7).
155    #[serde(default)]
156    pub trigger_source_zone_timestamp: Option<u64>,
157    /// Zone timestamp of `source` when this SpellAbility instance was created.
158    /// Used for non-target references like `Defined$ Self` to preserve object identity.
159    #[serde(default)]
160    pub source_zone_timestamp: Option<u64>,
161    /// Source trigger id (Java `sourceTrigger`), used for state-trigger dedupe.
162    pub source_trigger_id: Option<u32>,
163    /// Index into card.triggers for intervening-if recheck.
164    pub trigger_index: Option<usize>,
165    /// Alternative cost used to cast this spell (Flashback, Spectacle, Evoke, Dash, etc.).
166    pub alt_cost: Option<AlternativeCost>,
167    /// Index within the card's list of same-kind alternative costs. Zero for
168    /// all cases except multi-cost Evoke (intrinsic + granted by Ashling-style
169    /// static AddKeyword): 0 = first payable Evoke, 1 = second, …
170    #[serde(default)]
171    pub alt_cost_index: u8,
172    /// Number of Evoke keywords on the card at cast time (intrinsic + granted
173    /// from hand — e.g. Ashling, the Limitless's `AddKeyword$ Evoke:4`).
174    /// Java parity: `CardFactoryUtil` attaches one Evoke "sacrifice when it
175    /// enters" trigger per Evoke keyword, so a card with two Evoke keywords
176    /// carries two sac triggers. Captured at cast because granted keywords from
177    /// zone-gated statics (`AffectedZone$ Hand`) are gone once the card moves
178    /// to the stack.
179    #[serde(default)]
180    pub evoke_keyword_count: u8,
181    /// Whether the kicker cost was paid.
182    pub kicked: bool,
183    /// Whether buyback was paid (spell returns to hand on resolve).
184    pub buyback_paid: bool,
185    /// Whether this spell is overloaded (targets all valid instead of one).
186    pub overloaded: bool,
187    /// Whether this spell is a copy (created by Storm, Replicate, etc.).
188    pub is_copy: bool,
189    /// Java parity: life paid while activating or casting this ability.
190    #[serde(default)]
191    pub paid_life_amount: i32,
192    /// Number of times the kicker/multikicker cost was paid.
193    pub kick_count: u32,
194    /// Number of times the replicate cost was paid.
195    pub replicate_count: u32,
196    /// Whether a generic optional additional cost was paid.
197    pub optional_generic_cost_paid: bool,
198    /// Sum of integer values remembered on the trigger that spawned this
199    /// ability (Java: TriggerRememberAmount / sa.getTriggerRemembered()).
200    pub trigger_remembered_amount: i32,
201    /// The value chosen for X in the mana cost (e.g. Fireball X=5 means 5 damage).
202    /// Mirrors Java's `SpellAbility.getXManaCostPaid()`.
203    pub x_mana_cost_paid: u32,
204    /// Cards discarded as part of the cost payment.
205    /// Mirrors Java's `CostPayment.getPaidList("Discarded")`.
206    pub discarded_cost_cards: Vec<crate::ids::CardId>,
207    /// Optional costs that have been paid for this spell.
208    /// Mirrors Java's `SpellAbility.optionalCosts`.
209    #[serde(default)]
210    pub optional_costs: Vec<OptionalCost>,
211    /// Hash of costs paid, keyed by cost type with list of values.
212    /// Mirrors Java's `SpellAbility.paidHash`.
213    #[serde(default)]
214    pub paid_hash: HashMap<String, Vec<String>>,
215    /// Java parity: mana atoms used to pay this spell or ability.
216    #[serde(default)]
217    pub paying_mana: Vec<u16>,
218    /// Java parity: paid abilities list.
219    #[serde(default)]
220    pub paid_abilities: Vec<SpellAbility>,
221    /// Mana-producing part of this ability (for mana abilities).
222    /// Mirrors Java's `SpellAbility.manaPart`.
223    pub mana_part: Option<AbilityManaPart>,
224    /// Express mana choice forced by callback/autopay for flexible mana abilities.
225    #[serde(default)]
226    pub express_mana_choice: Option<u16>,
227    /// Cards tapped for convoke cost reduction.
228    /// Mirrors Java's `SpellAbility.tappedForConvoke`.
229    #[serde(default)]
230    pub convoke_tapped: Vec<CardId>,
231    /// Cards spliced onto this spell.
232    /// Mirrors Java's `SpellAbility.splicedCards`.
233    #[serde(default)]
234    pub spliced_cards: Vec<CardId>,
235    /// Announced variable values (e.g. X, number of targets).
236    /// Mirrors Java's `SpellAbility.announceVars`.
237    #[serde(default)]
238    pub announce_vars: HashMap<String, i32>,
239    /// Card sacrificed as part of emerge cost.
240    /// Mirrors Java's `SpellAbility.sacrificedAsEmerge`.
241    pub sacrificed_as_emerge: Option<CardId>,
242    /// Card sacrificed as part of offering cost.
243    /// Mirrors Java's `SpellAbility.sacrificedAsOffering`.
244    pub sacrificed_as_offering: Option<CardId>,
245    /// Human-readable description of this ability.
246    /// Mirrors Java's `SpellAbility.description`.
247    #[serde(default)]
248    pub description: String,
249    /// Description used when this ability is on the stack.
250    /// Mirrors Java's `SpellAbility.stackDescription`.
251    #[serde(default)]
252    pub stack_description: String,
253    /// Whether this is a mana ability (doesn't use the stack).
254    /// Mirrors Java's `SpellAbility.isManaAbility`.
255    #[serde(default)]
256    pub is_mana_ability: bool,
257    /// Whether this is a land ability (play land action).
258    /// Mirrors Java's `LandAbility` subclass flag.
259    #[serde(default)]
260    pub is_land_ability: bool,
261    /// Runtime-only face-down cast state used by morph/disguise-style spells.
262    #[serde(default)]
263    pub cast_face_down: bool,
264    /// Trigger objects map for tracking trigger context.
265    #[serde(default)]
266    pub trigger_objects: HashMap<AbilityKey, AbilityValue>,
267    /// Java parity: non-scalar trigger objects that carry spell/ability context.
268    #[serde(default)]
269    pub trigger_spell_abilities: HashMap<AbilityKey, SpellAbility>,
270    /// Java parity: additional ability lists used by mode/charm-style abilities.
271    #[serde(default)]
272    pub additional_ability_lists: HashMap<String, Vec<SpellAbility>>,
273    /// Java parity: replacing-objects payload.
274    #[serde(default)]
275    pub replacing_objects: HashMap<AbilityKey, AbilityValue>,
276    /// Java parity: trigger remembered objects copied from the originating trigger.
277    #[serde(default)]
278    pub trigger_remembered: Vec<AbilityValue>,
279    /// Activation restriction for this ability.
280    #[serde(default)]
281    pub restriction: SpellAbilityRestriction,
282    /// Condition that must be met for the effect to apply.
283    #[serde(default)]
284    pub condition: SpellAbilityCondition,
285    /// Rollback effects tracked for undo support.
286    #[serde(default)]
287    pub rollback_effects: Vec<String>,
288    /// Keyword amounts for optional keyword costs.
289    #[serde(default)]
290    pub optional_keyword_amounts: HashMap<String, i32>,
291    /// Pips to reduce from cost.
292    #[serde(default)]
293    pub pips_to_reduce: Vec<String>,
294    /// Java parity: whether copied effects may choose new targets.
295    #[serde(default)]
296    pub may_choose_new_targets: bool,
297    /// Last known state for LKI tracking.
298    #[serde(default)]
299    pub last_state: HashMap<String, String>,
300    /// Java parity: batched zone-change table accumulated for `ChangeZoneResolve`.
301    #[serde(skip)]
302    pub change_zone_table: Option<CardZoneTable>,
303    /// Java parity: accumulated damage map for `DamageResolve`.
304    #[serde(skip)]
305    pub damage_map: Option<CardDamageMap>,
306    /// Java parity: accumulated prevented-damage map for `DamageResolve`.
307    #[serde(skip)]
308    pub prevent_map: Option<CardDamageMap>,
309}
310
311/// Mirrors Java's `SpellAbility.toString()`.
312/// Walks the sub-ability chain, concatenating descriptions.
313impl std::fmt::Display for SpellAbility {
314    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
315        let mut node = Some(self);
316        let mut first = true;
317        while let Some(current) = node {
318            if !first {
319                write!(f, " ")?;
320            }
321            first = false;
322            write!(f, "{}", current.description)?;
323            node = current.sub_ability.as_deref();
324        }
325        Ok(())
326    }
327}
328
329impl CardTraitIrOwner for SpellAbility {
330    type Ir = SpellAbilityIr;
331
332    fn ir(&self) -> &Self::Ir {
333        &self.ir
334    }
335
336    fn card_trait_requirements(&self) -> &crate::card::valid_filter::CardTraitRequirementsIr {
337        &self.ir.card_trait_requirements
338    }
339}
340
341impl SpellAbility {
342    /// Whether this ability uses targeting.
343    /// Mirrors Java's `usesTargeting()`: `return targetRestrictions != null`.
344    pub fn uses_targeting(&self) -> bool {
345        self.target_restrictions.is_some()
346    }
347
348    /// Check if a parameter is set to "True" (case-insensitive).
349    /// Common pattern for boolean params like `Ninjutsu$ True`, `Mega$ True`, etc.
350    pub fn param_is_true(&self, key: &str) -> bool {
351        match key {
352            keys::OPTIONAL => self.ir.optional,
353            keys::MANDATORY => self.ir.mandatory,
354            keys::TAPPED => self.ir.tapped,
355            keys::HIDDEN => self.ir.hidden,
356            keys::IMPRINT => self.ir.imprint,
357            keys::CHOOSE_FROM_DEFINED_CARDS => self.ir.choose_from_defined_cards,
358            keys::FACE_DOWN => self.ir.face_down,
359            keys::EXILE_FACE_DOWN => self.ir.exile_face_down,
360            keys::TRANSFORMED => self.ir.transformed,
361            keys::AT_RANDOM => self.ir.at_random,
362            keys::REMEMBER_ALTERED => self.ir.remember_altered,
363            keys::REMEMBER_AMASS => self.ir.remember_amass,
364            keys::REMEMBER => self.ir.remember_flag,
365            keys::REMOVE_FROM_COMBAT => self.ir.remove_from_combat,
366            keys::RANDOM_TARGET => self.ir.random_target,
367            keys::REMEMBER_CHOSEN => self.ir.remember_chosen,
368            keys::REMEMBER_CLASHER => self.ir.remember_clasher,
369            keys::REMEMBER_CLOAKED => self.ir.remember_cloaked,
370            keys::REMEMBER_DISCOVERED => self.ir.remember_discovered,
371            keys::REMEMBER_DRAFTED => self.ir.remember_drafted,
372            keys::REMEMBER_EXCHANGED => self.ir.remember_exchanged,
373            keys::REMEMBER_INVESTIGATING_PLAYERS => self.ir.remember_investigating_players,
374            keys::REMEMBER_MADE => self.ir.remember_made,
375            keys::IMPRINT_MADE => self.ir.imprint_made,
376            keys::RANDOM_CHOSEN => self.ir.random_chosen,
377            keys::SNEAK => self.ir.sneak,
378            keys::MEGA => self.ir.mega,
379            keys::STORE_VOTE_NUM => self.ir.store_vote_num,
380            keys::REMEMBER_VOTED_OBJECTS => self.ir.remember_voted_objects,
381            "ToVisitYourAttractions" => self.ir.to_visit_your_attractions,
382            "RememberHighestPlayer" => self.ir.remember_highest_player,
383            "UseHighestRoll" => self.ir.use_highest_roll,
384            "UseDifferenceBetweenRolls" => self.ir.use_difference_between_rolls,
385            "StoreResults" => self.ir.store_results,
386            "EvenOddResults" => self.ir.even_odd_results,
387            "DifferentResults" => self.ir.different_results,
388            "MaxRollsResults" => self.ir.max_rolls_results,
389            "NoteDoubles" => self.ir.note_doubles,
390            "SubsForEach" => self.ir.subs_for_each,
391            "RerollResults" => self.ir.reroll_results,
392            keys::NINJUTSU => self.ir.ninjutsu,
393            keys::UNEARTH => self.ir.unearth,
394            keys::ATTACKING => self.ir.attacking,
395            keys::OVERWRITE_COLORS => self.ir.overwrite_colors,
396            keys::FORETOLD => self.ir.foretold,
397            keys::FORETOLD_COST => self.ir.foretold_cost,
398            keys::IMPRINT_LAST => self.ir.imprint_last,
399            keys::RANDOM_ORDER => self.ir.random_order,
400            keys::SHUFFLE_CHANGED_PILE => self.ir.shuffle_changed_pile,
401            keys::WARP => self.ir.warp,
402            keys::CAN_REPEAT_MODES => self.ir.can_repeat_modes,
403            keys::ENTWINE => self.ir.entwine,
404            keys::REMOVE_CREATURE_TYPES => self.ir.animate_remove_creature_types,
405            keys::REMOVE_ALL_ABILITIES => self.ir.animate_remove_all_abilities,
406            keys::REMEMBER_REMOVED_CARDS => self.ir.remember_removed_cards,
407            keys::TOKEN_TAPPED => self.ir.token_tapped,
408            keys::REMEMBER_TOKENS => self.ir.remember_tokens,
409            keys::REMEMBER_ORIGINAL_TOKENS => self.ir.remember_original_tokens,
410            keys::IMPRINT_TOKENS => self.ir.imprint_tokens,
411            keys::REMEMBER_SOURCE => self.ir.remember_source,
412            keys::CLEANUP_FOR_EACH => self.ir.cleanup_for_each,
413            "Morph" => self.ir.morph,
414            "MorphUp" => self.ir.morph_up,
415            "Megamorph" => self.ir.megamorph,
416            "RememberAbandoned" => self.ir.remember_abandoned,
417            _ => false,
418        }
419    }
420
421    pub fn param_value(&self, key: &str) -> Option<&str> {
422        match key {
423            keys::MODE => self.ir.mode_text.as_deref(),
424            keys::VALID_CARDS => self.ir.valid_cards_text.as_deref(),
425            keys::VALID_CARD => self.ir.valid_card_text.as_deref(),
426            keys::VALID_PLAYERS => self.ir.valid_players_text.as_deref(),
427            keys::VALID_PLAYER => self.ir.valid_player_text.as_deref(),
428            keys::VALID_TGTS => self.ir.valid_tgts_text.as_deref(),
429            keys::VALID_TARGET => self.ir.valid_target_text.as_deref(),
430            keys::DEFINED => self.ir.defined_text.as_deref(),
431            keys::DEFINED_PLAYER => self.ir.defined_player_text.as_deref(),
432            keys::CONTROLLER => self.ir.controller_text.as_deref(),
433            keys::ORIGIN => self.ir.origin_text.as_deref(),
434            keys::DESTINATION => self.ir.destination_text.as_deref(),
435            keys::CHOICES => self.ir.choices.as_deref(),
436            keys::FOR_EACH => self.ir.for_each_text.as_deref(),
437            keys::TRIGGERS => self.ir.triggers.as_deref(),
438            keys::COUNTER_TYPE => self.ir.counter_type_text.as_deref(),
439            keys::TOKEN_SCRIPT => self.ir.token_script.as_deref(),
440            keys::TOKEN_OWNER => self.ir.token_owner.as_deref(),
441            keys::TOKEN_NAME => self.ir.token_name_text.as_deref(),
442            keys::TOKEN_TYPES => self.ir.token_types_text.as_deref(),
443            keys::TOKEN_COLORS => self.ir.token_colors_text.as_deref(),
444            keys::TOKEN_KEYWORDS => self.ir.token_keywords_text.as_deref(),
445            keys::TOKEN_ATTACKING => self.ir.token_attacking_text.as_deref(),
446            keys::TOKEN_BLOCKING => self.ir.token_blocking_text.as_deref(),
447            keys::TOKEN_REMEMBERED => self.ir.token_remembered.as_deref(),
448            keys::ADD_TRIGGERS_FROM => self.ir.add_triggers_from_text.as_deref(),
449            keys::AT_EOT => self.ir.at_eot.as_deref(),
450            keys::AT_EOT_TRIG => self.ir.at_eot_trig_text.as_deref(),
451            keys::ATTACHED_TO => self.ir.attached_to.as_deref(),
452            keys::ATTACH_AFTER => self.ir.attach_after_text.as_deref(),
453            keys::WITH_COUNTERS_TYPE => self.ir.with_counters_type_text.as_deref(),
454            keys::WITH_COUNTERS_AMOUNT => self.ir.with_counters_amount_text.as_deref(),
455            keys::PUMP_KEYWORDS => self.ir.pump_keywords.as_deref(),
456            keys::PUMP_DURATION => self.ir.pump_duration_text.as_deref(),
457            "Keyword" => self.ir.keyword_text.as_deref(),
458            keys::CHOOSER => self.ir.chooser.as_deref(),
459            keys::NAME => self.ir.name_text.as_deref(),
460            keys::NAMES => self.ir.names_text.as_deref(),
461            keys::CHOOSE_FROM_LIST => self.ir.choose_from_list_text.as_deref(),
462            keys::GAIN_CONTROL => self.ir.gain_control_text.as_deref(),
463            keys::SPELLBOOK => self.ir.spellbook_text.as_deref(),
464            keys::VOTE_MESSAGE => self.ir.vote_message_text.as_deref(),
465            keys::DEFINED_MAGNET => self.ir.defined_magnet_text.as_deref(),
466            "PhaseInOrOut" => self.ir.phase_in_or_out_text.as_deref(),
467            "ExtraPhase" => self.ir.extra_phase_text.as_deref(),
468            "CardState" => self.ir.card_state_name.as_deref(),
469            _ => None,
470        }
471    }
472
473    /// Get the chosen targets. Mirrors Java's `getTargets()`.
474    pub fn get_targets(&self) -> &TargetChoices {
475        &self.target_chosen
476    }
477
478    /// Get the chosen targets mutably. Mirrors Java's `getTargets()` for mutation.
479    pub fn get_targets_mut(&mut self) -> &mut TargetChoices {
480        &mut self.target_chosen
481    }
482
483    /// Get the sub-ability. Mirrors Java's `getSubAbility()`.
484    pub fn get_sub_ability(&self) -> Option<&SpellAbility> {
485        self.sub_ability.as_deref()
486    }
487
488    /// Get the sub-ability mutably.
489    pub fn get_sub_ability_mut(&mut self) -> Option<&mut SpellAbility> {
490        self.sub_ability.as_deref_mut()
491    }
492
493    /// Mirrors Java's `SpellAbility.isWrapper()`.
494    pub fn is_wrapper(&self) -> bool {
495        self.wrapped_ability.is_some()
496    }
497
498    /// Mirrors Java's `WrappedAbility.getWrappedAbility()`.
499    pub fn get_wrapped_ability(&self) -> &SpellAbility {
500        self.wrapped_ability
501            .as_deref()
502            .expect("SpellAbility.get_wrapped_ability called on non-wrapper")
503    }
504
505    pub fn get_wrapped_ability_mut(&mut self) -> &mut SpellAbility {
506        self.wrapped_ability
507            .as_deref_mut()
508            .expect("SpellAbility.get_wrapped_ability_mut called on non-wrapper")
509    }
510
511    pub fn set_wrapped_ability(&mut self, wrapped: SpellAbility) {
512        self.wrapped_ability = Some(Box::new(wrapped));
513    }
514
515    /// Clear the chosen targets. Mirrors Java's `clearTargets()`.
516    pub fn clear_targets(&mut self) {
517        self.target_chosen = TargetChoices::default();
518    }
519
520    /// Walk the entire ability chain and choose targets for each node that
521    /// uses targeting. Mirrors Java's `SpellAbility.setupTargets()` do/while loop.
522    ///
523    /// Returns `true` if all targeting succeeded, `false` if any node couldn't
524    /// find valid targets.
525    pub fn setup_targets(
526        &mut self,
527        game: &GameState,
528        agents: &mut [Box<dyn PlayerAgent>],
529        mana_pools: &[ManaPool],
530    ) -> bool {
531        // Walk self, then sub_ability chain — mirrors Java's do/while
532        if self.uses_targeting() {
533            self.clear_targets();
534            self.targeting_player = choose_targeting_player(self, game, agents);
535            let player = self.targeting_player.unwrap_or(self.activating_player);
536            if !agents[player.index()].choose_targets_for(self, game, mana_pools) {
537                return false;
538            }
539        }
540
541        // Walk sub-ability chain
542        let mut current = self.sub_ability.as_deref_mut();
543        while let Some(sa) = current {
544            if sa.uses_targeting() {
545                sa.clear_targets();
546                sa.targeting_player = choose_targeting_player(sa, game, agents);
547                let player = sa.targeting_player.unwrap_or(sa.activating_player);
548                if !agents[player.index()].choose_targets_for(sa, game, mana_pools) {
549                    return false;
550                }
551            }
552            current = sa.sub_ability.as_deref_mut();
553        }
554
555        if !crate::staticability::static_ability_must_target::meets_must_target_restriction(
556            game, self,
557        ) {
558            return false;
559        }
560
561        true
562    }
563
564    /// Create a simple SpellAbility for tests and triggers.
565    pub fn new_simple(source: Option<CardId>, player: PlayerId, ability_text: &str) -> Self {
566        let _perf_scope = crate::perf::ParamsLookupScopeGuard::enter(
567            crate::perf::ParamsLookupScope::AbilityBuild,
568        );
569        let parsed = ParsedParams::parse(ability_text);
570        let params = Params::from_parsed(&parsed);
571        let api = parsed
572            .get(keys::SP)
573            .or_else(|| parsed.get(keys::DB))
574            .or_else(|| parsed.get(keys::AB))
575            .and_then(ApiType::smart_value_of);
576        let record_type = crate::ability::ability_factory::AbilityRecordType::from_parsed(&parsed)
577            .unwrap_or_default();
578        let target_restrictions = if parsed.has(keys::VALID_TGTS) {
579            TargetRestrictions::new_from_parsed(&parsed, &params)
580        } else {
581            None
582        };
583        let cost = parsed.get(keys::COST).map(parse_cost);
584        let mut ir = crate::ability::ability_ir::SpellAbilityIr::from_parsed(api, &parsed);
585        ir.compile_numeric_params_from_runtime(&params);
586
587        SpellAbility {
588            id: next_spell_ability_id(),
589            api,
590            source,
591            original_host: None,
592            activating_player: player,
593            targeting_player: None,
594            ability_text: ability_text.to_string(),
595            record_type,
596            ir,
597            target_restrictions,
598            target_chosen: TargetChoices::default(),
599            pay_costs: cost,
600            sub_ability: None,
601            wrapped_ability: None,
602            is_spell: false,
603            is_trigger: false,
604            is_activated: false,
605            intrinsic: false,
606            trigger_source: None,
607            trigger_source_zone_timestamp: None,
608            source_zone_timestamp: None,
609            source_trigger_id: None,
610            trigger_index: None,
611            alt_cost: None,
612            alt_cost_index: 0,
613            evoke_keyword_count: 0,
614            kicked: false,
615            buyback_paid: false,
616            overloaded: false,
617            is_copy: false,
618            paid_life_amount: 0,
619            kick_count: 0,
620            replicate_count: 0,
621            optional_generic_cost_paid: false,
622            trigger_remembered_amount: 0,
623            x_mana_cost_paid: 0,
624            discarded_cost_cards: Vec::new(),
625            optional_costs: Vec::new(),
626            paid_hash: HashMap::new(),
627            paying_mana: Vec::new(),
628            paid_abilities: Vec::new(),
629            mana_part: None,
630            express_mana_choice: None,
631            convoke_tapped: Vec::new(),
632            spliced_cards: Vec::new(),
633            announce_vars: HashMap::new(),
634            sacrificed_as_emerge: None,
635            sacrificed_as_offering: None,
636            description: String::new(),
637            stack_description: String::new(),
638            is_mana_ability: false,
639            is_land_ability: false,
640            cast_face_down: false,
641            trigger_objects: HashMap::new(),
642            trigger_spell_abilities: HashMap::new(),
643            additional_ability_lists: HashMap::new(),
644            replacing_objects: HashMap::new(),
645            trigger_remembered: Vec::new(),
646            restriction: SpellAbilityRestriction::default(),
647            condition: SpellAbilityCondition::default(),
648            rollback_effects: Vec::new(),
649            optional_keyword_amounts: HashMap::new(),
650            pips_to_reduce: Vec::new(),
651            may_choose_new_targets: false,
652            last_state: HashMap::new(),
653            change_zone_table: None,
654            damage_map: None,
655            prevent_map: None,
656        }
657    }
658
659    /// Create a minimal empty SpellAbility stub.
660    /// Mirrors Java's common `SpellAbility.EmptySa` usage.
661    pub fn new_empty(source: Option<CardId>, player: PlayerId) -> Self {
662        Self::new_simple(source, player, "")
663    }
664
665    /// Create a minimal land-play SpellAbility stub.
666    pub fn new_land(source: Option<CardId>, player: PlayerId) -> Self {
667        let mut sa = Self::new_empty(source, player);
668        sa.is_land_ability = true;
669        sa
670    }
671
672    // ── Sub-ability chain walking ─────────────────────────────────────────
673
674    /// Walk the sub-ability chain looking for a specific API type.
675    /// Mirrors Java's `SpellAbility.findSubAbilityByType(ApiType)`.
676    pub fn find_sub_ability_by_type(&self, api: ApiType) -> Option<&SpellAbility> {
677        let mut current = self.sub_ability.as_deref();
678        while let Some(sub) = current {
679            if sub.api == Some(api) {
680                return Some(sub);
681            }
682            current = sub.sub_ability.as_deref();
683        }
684        None
685    }
686
687    // ── Mana part delegation ──────────────────────────────────────────────
688
689    /// Whether this ability can produce mana.
690    /// Mirrors Java's `SpellAbility.canThisProduce()`.
691    pub fn can_this_produce(&self) -> bool {
692        match &self.mana_part {
693            Some(mp) => mp.can_this_produce(),
694            None => false,
695        }
696    }
697
698    /// Whether this ability can produce a specific color.
699    /// Mirrors Java's `SpellAbility.canProduce(String)`.
700    pub fn can_produce(&self, color: &str) -> bool {
701        match &self.mana_part {
702            Some(mp) => mp.can_produce(color),
703            None => false,
704        }
705    }
706
707    /// Amount of mana generated by this ability.
708    /// Mirrors Java's `SpellAbility.amountOfManaGenerated()`.
709    pub fn amount_of_mana_generated(&self) -> i32 {
710        match &self.mana_part {
711            Some(mp) => mp.amount_of_mana_generated(),
712            None => 0,
713        }
714    }
715
716    /// Total amount of mana generated, counting Any/All as 1.
717    /// Mirrors Java's `SpellAbility.totalAmountOfManaGenerated()`.
718    pub fn total_amount_of_mana_generated(&self) -> i32 {
719        match &self.mana_part {
720            Some(mp) => mp.total_amount_of_mana_generated(),
721            None => 0,
722        }
723    }
724
725    // ── Cost and payment ──────────────────────────────────────────────────
726
727    /// Whether paying with shard mana is allowed.
728    /// Mirrors Java's `SpellAbility.allowsPayingWithShard()`.
729    pub fn allows_paying_with_shard(&self) -> bool {
730        self.ir.allows_paying_with_shard
731    }
732
733    /// Whether this ability cannot be copied.
734    /// Mirrors Java's `SpellAbility.cantBeCopied()`.
735    pub fn cant_be_copied(&self) -> bool {
736        self.ir.cant_be_copied_ability
737    }
738
739    /// Whether this ability can be played (checks restrictions).
740    /// Mirrors Java's `SpellAbility.canPlay()`.
741    pub fn can_play(&self, game: &GameState) -> bool {
742        if let Some(card_id) = self.source {
743            if !self
744                .restriction
745                .can_play_with_sa(game, card_id, self.activating_player, Some(self))
746            {
747                return false;
748            }
749
750            let card = game.card(card_id);
751            if let Some(limit_expr) = self.restriction.variables.limit_to_check() {
752                let limit = crate::svar::resolve_numeric_value(game, self, limit_expr, 0);
753                if card.get_ability_activated_this_turn(Some(self)) as i32 >= limit {
754                    return false;
755                }
756            }
757            if let Some(limit_expr) = self.restriction.variables.game_limit_to_check() {
758                let limit = crate::svar::resolve_numeric_value(game, self, limit_expr, 0);
759                if card.get_ability_activated_this_game(Some(self)) as i32 >= limit {
760                    return false;
761                }
762            }
763
764            true
765        } else {
766            true
767        }
768    }
769
770    /// Whether this ability can be played with optional costs.
771    /// Mirrors Java's `SpellAbility.canPlayWithOptionalCost()`.
772    pub fn can_play_with_optional_cost(&self) -> bool {
773        !self.optional_costs.is_empty()
774    }
775
776    /// Whether to prompt even if this is the only possible ability.
777    /// Mirrors Java's `SpellAbility.promptIfOnlyPossibleAbility()`.
778    pub fn prompt_if_only_possible_ability(&self) -> bool {
779        self.ir.prompt_if_only_possible_ability
780    }
781
782    /// Add an optional cost to this ability.
783    /// Mirrors Java's `SpellAbility.addOptionalCost(OptionalCost)`.
784    pub fn add_optional_cost(&mut self, cost: OptionalCost) {
785        if !self.optional_costs.contains(&cost) {
786            self.optional_costs.push(cost);
787        }
788    }
789
790    /// Whether the mana cost contains X.
791    /// Mirrors Java's `SpellAbility.costHasX()`.
792    pub fn cost_has_x(&self) -> bool {
793        self.ir.cost_has_x
794    }
795
796    /// Whether the mana cost contains X (mana-specific check).
797    /// Mirrors Java's `SpellAbility.costHasManaX()`.
798    pub fn cost_has_mana_x(&self) -> bool {
799        self.ir.cost_has_x
800    }
801
802    /// Whether conditions are met for this ability.
803    /// Mirrors Java's `SpellAbility.metConditions()`.
804    pub fn met_conditions(&self, game: &GameState) -> bool {
805        self.condition.are_met(game, self)
806    }
807
808    /// Clear mana paid tracking.
809    /// Mirrors Java's `SpellAbility.clearManaPaid()`.
810    pub fn clear_mana_paid(&mut self) {
811        self.x_mana_cost_paid = 0;
812    }
813
814    /// Apply effects from paying mana (e.g. Sunburst).
815    /// Mirrors Java's `SpellAbility.applyPayingManaEffects()`.
816    pub fn apply_paying_mana_effects(&mut self) {
817        // Mana payment effects are applied during resolution based on
818        // the colors of mana spent, tracked in the card's colors_spent_to_cast.
819    }
820
821    /// Run this ability (no-op in Rust; Java resolves via resolveStack).
822    /// Mirrors Java's `SpellAbility.run()`.
823    pub fn run(&self) {
824        // Resolution is handled by the stack resolution system in Rust.
825        // This method exists for API parity with Java.
826    }
827
828    // ── Paid cost tracking ────────────────────────────────────────────────
829
830    /// Add a value to the paid cost hash.
831    /// Mirrors Java's `SpellAbility.addCostToHashList(String, String)`.
832    pub fn add_cost_to_hash_list(&mut self, key: &str, value: &str) {
833        self.paid_hash
834            .entry(key.to_string())
835            .or_default()
836            .push(value.to_string());
837    }
838
839    /// Reset the paid cost hash.
840    /// Mirrors Java's `SpellAbility.resetPaidHash()`.
841    pub fn reset_paid_hash(&mut self) {
842        self.paid_hash.clear();
843    }
844
845    // ── Trigger objects ───────────────────────────────────────────────────
846
847    /// Check if a triggering object is set.
848    /// Mirrors Java's `SpellAbility.hasTriggeringObject(String)`.
849    pub fn has_triggering_object<K: TriggerKeyInput>(&self, key: K) -> bool {
850        key.into_ability_key()
851            .map(|parsed| self.trigger_objects.contains_key(&parsed))
852            .unwrap_or(false)
853    }
854
855    /// Get a triggering object value by key.
856    pub fn get_triggering_value(&self, key: AbilityKey) -> Option<&AbilityValue> {
857        self.trigger_objects.get(&key)
858    }
859
860    /// Get a triggering card by key.
861    pub fn get_triggering_card(&self, key: AbilityKey) -> Option<CardId> {
862        match self.get_triggering_value(key) {
863            Some(AbilityValue::Card(card)) => Some(*card),
864            Some(AbilityValue::Cards(cards)) => cards.first().copied(),
865            _ => None,
866        }
867    }
868
869    /// Get a triggering player by key.
870    pub fn get_triggering_player(&self, key: AbilityKey) -> Option<PlayerId> {
871        match self.get_triggering_value(key) {
872            Some(AbilityValue::Player(player)) => Some(*player),
873            Some(AbilityValue::Players(players)) => players.first().copied(),
874            _ => None,
875        }
876    }
877
878    /// Get triggering cards by key.
879    pub fn get_triggering_cards(&self, key: AbilityKey) -> Vec<CardId> {
880        match self.get_triggering_value(key) {
881            Some(AbilityValue::Card(card)) => vec![*card],
882            Some(AbilityValue::Cards(cards)) => cards.clone(),
883            _ => Vec::new(),
884        }
885    }
886
887    /// Get triggering players by key.
888    pub fn get_triggering_players(&self, key: AbilityKey) -> Vec<PlayerId> {
889        match self.get_triggering_value(key) {
890            Some(AbilityValue::Player(player)) => vec![*player],
891            Some(AbilityValue::Players(players)) => players.clone(),
892            _ => Vec::new(),
893        }
894    }
895
896    /// Get a triggering object by key.
897    /// Mirrors Java's `SpellAbility.getTriggeringObject(String)`.
898    pub fn get_triggering_object<K: TriggerKeyInput>(&self, key: K) -> Option<&str> {
899        key.into_ability_key()
900            .and_then(|parsed| self.get_triggering_value(parsed))
901            .and_then(|value| match value {
902                AbilityValue::String(raw) => Some(raw.as_str()),
903                _ => None,
904            })
905    }
906
907    /// Clear all triggering objects.
908    /// Mirrors Java's `SpellAbility.resetTriggeringObjects()`.
909    pub fn reset_triggering_objects(&mut self) {
910        self.trigger_objects.clear();
911    }
912
913    /// Cleanup after resolution — reset targets, trigger objects, paid hash.
914    /// Mirrors Java's `SpellAbility.resetOnceResolved()`.
915    pub fn reset_once_resolved(&mut self) {
916        self.clear_targets();
917        self.reset_triggering_objects();
918        self.reset_paid_hash();
919        self.x_mana_cost_paid = 0;
920        self.kick_count = 0;
921        self.replicate_count = 0;
922        self.optional_generic_cost_paid = false;
923        self.discarded_cost_cards.clear();
924        self.optional_costs.clear();
925        self.convoke_tapped.clear();
926        self.spliced_cards.clear();
927        self.announce_vars.clear();
928        self.sacrificed_as_emerge = None;
929        self.sacrificed_as_offering = None;
930    }
931
932    // ── Description and text ──────────────────────────────────────────────
933
934    /// Generate a unique key for this ability.
935    /// Mirrors Java's `SpellAbility.yieldKey()`.
936    pub fn yield_key(&self) -> String {
937        let api_str = self.api.map(|a| format!("{:?}", a)).unwrap_or_default();
938        let source_str = self.source.map(|s| format!("{}", s.0)).unwrap_or_default();
939        format!("{}_{}", api_str, source_str)
940    }
941
942    /// Build a description from params.
943    /// Mirrors Java's `SpellAbility.rebuiltDescription()`.
944    pub fn rebuilt_description(&self) -> String {
945        if !self.description.is_empty() {
946            return self.description.clone();
947        }
948        if let Some(desc) = self.ir.sp_desc_text.as_deref() {
949            return desc.to_string();
950        }
951        self.ability_text.clone()
952    }
953
954    /// Full text without suppression.
955    /// Mirrors Java's `SpellAbility.toUnsuppressedString()`.
956    pub fn to_unsuppressed_string(&self) -> String {
957        self.rebuilt_description()
958    }
959
960    // ── Sub-abilities ─────────────────────────────────────────────────────
961
962    /// Check if an additional ability with the given key exists.
963    /// Mirrors Java's `SpellAbility.hasAdditionalAbility(String)`.
964    pub fn has_additional_ability<K: TriggerKeyInput>(&self, key: K) -> bool {
965        key.into_ability_key()
966            .map(|parsed| self.trigger_spell_abilities.contains_key(&parsed))
967            .unwrap_or(false)
968    }
969
970    /// Get an additional ability by key.
971    /// Mirrors Java's `SpellAbility.getAdditionalAbility(String)`.
972    pub fn get_additional_ability<K: TriggerKeyInput>(&self, key: K) -> Option<&SpellAbility> {
973        key.into_ability_key()
974            .and_then(|parsed| self.trigger_spell_abilities.get(&parsed))
975    }
976
977    /// Set an additional ability by key.
978    /// Mirrors Java's `SpellAbility.setAdditionalAbility(String, SpellAbility)`.
979    pub fn set_additional_ability<K: TriggerKeyInput>(&mut self, key: K, ability: SpellAbility) {
980        if let Some(parsed) = key.into_ability_key() {
981            self.trigger_spell_abilities.insert(parsed, ability);
982        }
983    }
984
985    /// Append a sub-ability to the end of the chain.
986    /// Mirrors Java's `SpellAbility.appendSubAbility(SpellAbility)`.
987    pub fn append_sub_ability(&mut self, sub: SpellAbility) {
988        if self.sub_ability.is_none() {
989            self.sub_ability = Some(Box::new(sub));
990        } else {
991            // Walk to end of chain
992            let mut current = self.sub_ability.as_deref_mut();
993            while let Some(sa) = current {
994                if sa.sub_ability.is_none() {
995                    sa.sub_ability = Some(Box::new(sub));
996                    return;
997                }
998                current = sa.sub_ability.as_deref_mut();
999            }
1000        }
1001    }
1002
1003    // ── Copying ───────────────────────────────────────────────────────────
1004
1005    /// Clone this spell ability.
1006    /// Mirrors Java's `SpellAbility.copy()`.
1007    pub fn copy(&self) -> Self {
1008        crate::perf::increment(crate::perf::Metric::SpellAbilityClones, 1);
1009        self.clone()
1010    }
1011
1012    pub fn copy_for_player(&self, activ: PlayerId) -> Self {
1013        crate::perf::increment(crate::perf::Metric::SpellAbilityClones, 1);
1014        let mut clone = self.clone();
1015        clone.activating_player = activ;
1016        clone
1017    }
1018
1019    pub fn copy_with_host_lki(&self, host: crate::card::Card, lki: bool) -> Self {
1020        self.copy_with_host_activating_lki_keep_text_changes(
1021            host,
1022            self.activating_player,
1023            lki,
1024            false,
1025        )
1026    }
1027
1028    pub fn copy_with_host_lki_keep_text_changes(
1029        &self,
1030        host: crate::card::Card,
1031        lki: bool,
1032        keep_text_changes: bool,
1033    ) -> Self {
1034        self.copy_with_host_activating_lki_keep_text_changes(
1035            host,
1036            self.activating_player,
1037            lki,
1038            keep_text_changes,
1039        )
1040    }
1041
1042    pub fn copy_with_host_activating_lki(
1043        &self,
1044        host: crate::card::Card,
1045        activ: PlayerId,
1046        lki: bool,
1047    ) -> Self {
1048        self.copy_with_host_activating_lki_keep_text_changes(host, activ, lki, false)
1049    }
1050
1051    pub fn copy_with_host_activating_lki_keep_text_changes(
1052        &self,
1053        host: crate::card::Card,
1054        activ: PlayerId,
1055        lki: bool,
1056        keep_text_changes: bool,
1057    ) -> Self {
1058        crate::perf::increment(crate::perf::Metric::SpellAbilityClones, 1);
1059        let mut clone = self.clone();
1060        clone.id = if lki {
1061            self.id
1062        } else {
1063            next_spell_ability_id()
1064        };
1065
1066        clone.source = Some(host.id);
1067        clone.may_choose_new_targets = false;
1068        clone.trigger_objects = self.trigger_objects.clone();
1069        if !lki {
1070            clone.replacing_objects = HashMap::new();
1071        }
1072
1073        clone.pay_costs = self.pay_costs.clone();
1074        if self.mana_part.is_some() {
1075            clone.mana_part = self.mana_part.clone();
1076        }
1077
1078        clone.optional_keyword_amounts = self.optional_keyword_amounts.clone();
1079        clone.damage_map = self.damage_map.clone();
1080        clone.prevent_map = self.prevent_map.clone();
1081        clone.change_zone_table = self.change_zone_table.clone();
1082        clone.paying_mana = self.paying_mana.clone();
1083        clone.paid_abilities = Vec::new();
1084        clone.paid_hash = self.paid_hash.clone();
1085
1086        if self.uses_targeting() {
1087            clone.target_chosen = self.target_chosen.clone();
1088        }
1089
1090        clone.trigger_spell_abilities = HashMap::new();
1091        clone.additional_ability_lists = HashMap::new();
1092
1093        if let Some(sub_ability) = &self.sub_ability {
1094            clone.sub_ability = Some(Box::new(
1095                sub_ability.copy_with_host_activating_lki_keep_text_changes(
1096                    host.clone(),
1097                    activ,
1098                    lki,
1099                    keep_text_changes,
1100                ),
1101            ));
1102        }
1103
1104        for (name, ability) in &self.trigger_spell_abilities {
1105            clone.trigger_spell_abilities.insert(
1106                *name,
1107                ability.copy_with_host_activating_lki_keep_text_changes(
1108                    host.clone(),
1109                    activ,
1110                    lki,
1111                    keep_text_changes,
1112                ),
1113            );
1114        }
1115
1116        for (name, abilities) in &self.additional_ability_lists {
1117            clone.additional_ability_lists.insert(
1118                name.clone(),
1119                abilities
1120                    .iter()
1121                    .map(|ability| {
1122                        ability.copy_with_host_activating_lki_keep_text_changes(
1123                            host.clone(),
1124                            activ,
1125                            lki,
1126                            keep_text_changes,
1127                        )
1128                    })
1129                    .collect(),
1130            );
1131        }
1132
1133        clone.restriction = self.restriction.clone();
1134        clone.condition = self.condition.clone();
1135        clone.activating_player = activ;
1136
1137        let _ = keep_text_changes;
1138        clone
1139    }
1140
1141    /// Clone with no mana cost.
1142    /// Mirrors Java's `SpellAbility.copyWithNoManaCost()`.
1143    pub fn copy_with_no_mana_cost(&self) -> Self {
1144        crate::perf::increment(crate::perf::Metric::SpellAbilityClones, 1);
1145        let mut copied = self.clone();
1146        copied.pay_costs = None;
1147        copied
1148    }
1149
1150    /// Clone with a specific cost.
1151    /// Mirrors Java's `SpellAbility.copyWithDefinedCost(String)`.
1152    pub fn copy_with_defined_cost(&self, cost: &str) -> Self {
1153        crate::perf::increment(crate::perf::Metric::SpellAbilityClones, 1);
1154        let mut copied = self.clone();
1155        copied.pay_costs = Some(parse_cost(cost));
1156        copied
1157    }
1158
1159    /// Clone with mana cost replacement.
1160    /// Mirrors Java's `SpellAbility.copyWithManaCostReplaced(String, String)`.
1161    pub fn copy_with_mana_cost_replaced(&self, old: &str, new: &str) -> Self {
1162        crate::perf::increment(crate::perf::Metric::SpellAbilityClones, 1);
1163        let mut copied = self.clone();
1164        if let Some(ref cost) = self.pay_costs {
1165            let cost_str = format!("{:?}", cost);
1166            let replaced = cost_str.replace(old, new);
1167            copied.pay_costs = Some(parse_cost(&replaced));
1168        }
1169        copied
1170    }
1171
1172    // ── Targeting ─────────────────────────────────────────────────────────
1173
1174    /// Check if this ability can target a specific card.
1175    /// Mirrors Java's `SpellAbility.canTarget(Card)`.
1176    pub fn can_target(&self, card: CardId, game: &GameState) -> bool {
1177        if let Some(ref tr) = self.target_restrictions {
1178            tr.has_candidates(game, self.activating_player, self.source)
1179                && self
1180                    .ir
1181                    .targets_with_defined_controller_text
1182                    .as_deref()
1183                    .map(|defined| {
1184                        crate::ability::ability_utils::resolve_defined_players_with_sa(
1185                            defined,
1186                            self,
1187                            self.activating_player,
1188                            game,
1189                        )
1190                    })
1191                    .map(|players| {
1192                        players.is_empty() || players.contains(&game.card(card).controller)
1193                    })
1194                    .unwrap_or(true)
1195                && target_restrictions::can_be_targeted_by_sa(
1196                    game,
1197                    card,
1198                    self.activating_player,
1199                    self,
1200                )
1201        } else {
1202            false
1203        }
1204    }
1205
1206    /// Reset targets (alias for clear_targets).
1207    /// Mirrors Java's `SpellAbility.resetTargets()`.
1208    pub fn reset_targets(&mut self) {
1209        self.clear_targets();
1210    }
1211
1212    /// Add divided allocation for a target.
1213    /// Mirrors Java's `SpellAbility.addDividedAllocation(Card, int)`.
1214    pub fn add_divided_allocation(&mut self, card: CardId, amount: i32) {
1215        self.target_chosen.add_divided_allocation(card, amount);
1216    }
1217
1218    /// Reset only the first target in the chain.
1219    /// Mirrors Java's `SpellAbility.resetFirstTarget()`.
1220    pub fn reset_first_target(&mut self) {
1221        self.target_chosen = TargetChoices::default();
1222    }
1223
1224    /// Check if more targets can be added.
1225    /// Mirrors Java's `SpellAbility.canAddMoreTarget()`.
1226    pub fn can_add_more_target(&self, game: &GameState) -> bool {
1227        if let Some(ref tr) = self.target_restrictions {
1228            let max = tr.get_max_targets(game, self);
1229            let current = self.target_chosen.all_target_cards().len() as i32
1230                + self.target_chosen.all_target_players().len() as i32;
1231            current < max
1232        } else {
1233            false
1234        }
1235    }
1236
1237    /// Collect all targeted cards from the entire chain.
1238    /// Mirrors Java's `SpellAbility.findTargetedCards()`.
1239    pub fn find_targeted_cards(&self) -> Vec<CardId> {
1240        let mut cards = Vec::new();
1241        cards.extend(self.target_chosen.all_target_cards());
1242        let mut current = self.sub_ability.as_deref();
1243        while let Some(sub) = current {
1244            cards.extend(sub.target_chosen.all_target_cards());
1245            current = sub.sub_ability.as_deref();
1246        }
1247        cards
1248    }
1249
1250    /// Collect all targeted players from the entire chain.
1251    /// Mirrors Java's `SpellAbility.findTargetedPlayers()`.
1252    pub fn find_targeted_players(&self) -> Vec<PlayerId> {
1253        let mut players = Vec::new();
1254        players.extend(self.target_chosen.all_target_players());
1255        let mut current = self.sub_ability.as_deref();
1256        while let Some(sub) = current {
1257            for player in sub.target_chosen.all_target_players() {
1258                if !players.contains(&player) {
1259                    players.push(player);
1260                }
1261            }
1262            current = sub.sub_ability.as_deref();
1263        }
1264        players
1265    }
1266
1267    /// Whether this ability targets spells/abilities on the stack.
1268    /// Mirrors Java's `SpellAbility.canTargetSpellAbility()`.
1269    pub fn can_target_spell_ability(&self) -> bool {
1270        matches!(
1271            self.target_restrictions.as_ref().map(|tr| &tr.target_kind),
1272            Some(TargetKind::Spell)
1273        )
1274    }
1275
1276    /// Setup new targets for a retargeting scenario.
1277    /// Mirrors Java's `SpellAbility.setupNewTargets()`.
1278    pub fn setup_new_targets(
1279        &mut self,
1280        game: &GameState,
1281        agents: &mut [Box<dyn PlayerAgent>],
1282        mana_pools: &[ManaPool],
1283    ) -> bool {
1284        self.clear_targets();
1285        self.setup_targets(game, agents, mana_pools)
1286    }
1287
1288    // ── Convoke / Emerge / Offering ───────────────────────────────────────
1289
1290    /// Clear pip reduction tracking.
1291    /// Mirrors Java's `SpellAbility.clearPipsToReduce()`.
1292    pub fn clear_pips_to_reduce(&mut self) {
1293        self.pips_to_reduce.clear();
1294    }
1295
1296    /// Add a card tapped for convoke.
1297    /// Mirrors Java's `SpellAbility.addTappedForConvoke(Card)`.
1298    pub fn add_tapped_for_convoke(&mut self, card: CardId) {
1299        self.convoke_tapped.push(card);
1300    }
1301
1302    /// Clear convoke tracking.
1303    /// Mirrors Java's `SpellAbility.clearTappedForConvoke()`.
1304    pub fn clear_tapped_for_convoke(&mut self) {
1305        self.convoke_tapped.clear();
1306    }
1307
1308    /// Reset the sacrificed-as-emerge card.
1309    /// Mirrors Java's `SpellAbility.resetSacrificedAsEmerge()`.
1310    pub fn reset_sacrificed_as_emerge(&mut self) {
1311        self.sacrificed_as_emerge = None;
1312    }
1313
1314    /// Reset the sacrificed-as-offering card.
1315    /// Mirrors Java's `SpellAbility.resetSacrificedAsOffering()`.
1316    pub fn reset_sacrificed_as_offering(&mut self) {
1317        self.sacrificed_as_offering = None;
1318    }
1319
1320    // ── Splice ────────────────────────────────────────────────────────────
1321
1322    /// Add spliced cards to this spell.
1323    /// Mirrors Java's `SpellAbility.addSplicedCards(List<Card>)`.
1324    pub fn add_spliced_cards(&mut self, cards: Vec<CardId>) {
1325        self.spliced_cards.extend(cards);
1326    }
1327
1328    // ── Deterministic checks ──────────────────────────────────────────────
1329
1330    /// Whether `Defined$` resolves to a deterministic set of objects.
1331    /// Mirrors Java's `SpellAbility.knownDetermineDefined()`.
1332    pub fn known_determine_defined(&self) -> bool {
1333        match self.defined() {
1334            Some(defined) => matches!(
1335                defined,
1336                "Self"
1337                    | "You"
1338                    | "Targeted"
1339                    | "TargetedPlayer"
1340                    | "Remembered"
1341                    | "ParentTarget"
1342                    | "SourceController"
1343                    | "Imprinted"
1344            ),
1345            None => true,
1346        }
1347    }
1348
1349    // ── Undo ──────────────────────────────────────────────────────────────
1350
1351    /// Undo this ability.
1352    /// Mirrors Java's `SpellAbility.undo()`.
1353    pub fn undo(&mut self) -> bool {
1354        self.clear_tapped_for_convoke();
1355        self.reset_sacrificed_as_emerge();
1356        self.reset_sacrificed_as_offering();
1357        self.reset_paid_hash();
1358        self.clear_mana_paid();
1359        true
1360    }
1361
1362    // ── Announce vars ─────────────────────────────────────────────────────
1363
1364    /// Add an announced variable value.
1365    /// Mirrors Java's `SpellAbility.addAnnounceVar(String, int)`.
1366    pub fn add_announce_var(&mut self, key: &str, value: i32) {
1367        self.announce_vars.insert(key.to_string(), value);
1368    }
1369
1370    // ── Targeting by SA ───────────────────────────────────────────────────
1371
1372    /// Check if this spell ability can be targeted by another SA.
1373    /// Mirrors Java's `SpellAbility.canBeTargetedBy(SpellAbility)`.
1374    pub fn can_be_targeted_by(&self, _sa: &SpellAbility) -> bool {
1375        // Spells on the stack can generally be targeted unless they have
1376        // "can't be countered" or similar protection. The basic check is
1377        // whether this is a spell (on the stack).
1378        if self.is_spell {
1379            return !self.cant_be_copied();
1380        }
1381        true
1382    }
1383
1384    // ── Property checks ───────────────────────────────────────────────────
1385
1386    /// Check if this ability has a specific property.
1387    /// Mirrors Java's `SpellAbility.hasProperty(String)`.
1388    pub fn has_property(&self, property: &str) -> bool {
1389        match property {
1390            "Spell" => self.is_spell,
1391            "Trigger" => self.is_trigger,
1392            "Activated" => self.is_activated,
1393            "ManaAbility" => self.is_mana_ability,
1394            "Optional" => self.ir.optional,
1395            "Mandatory" => self.ir.mandatory,
1396            "Tapped" => self.ir.tapped,
1397            "Hidden" => self.ir.hidden,
1398            "FaceDown" => self.ir.face_down,
1399            "ExileFaceDown" => self.ir.exile_face_down,
1400            "Transformed" => self.ir.transformed,
1401            "AtRandom" => self.ir.at_random,
1402            "Imprint" => self.ir.imprint,
1403            "Morph" => self.ir.morph,
1404            "MorphUp" => self.ir.morph_up,
1405            "Megamorph" => self.ir.megamorph,
1406            "PwAbility" => self.ir.pw_ability,
1407            "Flash" => self.ir.flash,
1408            "SplitSecond" => self.ir.split_second,
1409            _ => false,
1410        }
1411    }
1412
1413    /// Whether this ability tracks mana spent.
1414    /// Mirrors Java's `SpellAbility.tracksManaSpent()`.
1415    pub fn tracks_mana_spent(&self) -> bool {
1416        self.ir.track_mana_spent
1417    }
1418
1419    // ── Text changes ──────────────────────────────────────────────────────
1420
1421    /// Apply text replacement.
1422    /// Mirrors Java's `SpellAbility.changeText(String, String)`.
1423    pub fn apply_text_change(&mut self, original: &str, replacement: &str) {
1424        if original == replacement {
1425            return;
1426        }
1427        self.description = self.description.replace(original, replacement);
1428        self.stack_description = self.stack_description.replace(original, replacement);
1429        if let Some(ref mut tr) = self.target_restrictions {
1430            tr.apply_target_text_changes(&[(original, replacement)]);
1431        }
1432
1433        if let Some(sub_ability) = self.sub_ability.as_deref_mut() {
1434            sub_ability.apply_text_change(original, replacement);
1435        }
1436
1437        for ability in self.trigger_spell_abilities.values_mut() {
1438            ability.apply_text_change(original, replacement);
1439        }
1440    }
1441
1442    /// Apply intrinsic text replacement.
1443    /// Mirrors Java's `SpellAbility.changeTextIntrinsic(String, String)`.
1444    pub fn apply_text_change_intrinsic(&mut self, original: &str, replacement: &str) {
1445        self.apply_text_change(original, replacement);
1446    }
1447
1448    /// Apply a batch of text replacements to this ability and linked abilities.
1449    pub fn apply_text_changes(&mut self, pairs: &[(String, String)]) {
1450        for (original, replacement) in pairs {
1451            self.apply_text_change(original, replacement);
1452        }
1453    }
1454
1455    /// Apply intrinsic text changes to this ability and linked abilities.
1456    pub fn apply_text_changes_intrinsic(
1457        &mut self,
1458        color_map: &HashMap<String, String>,
1459        type_map: &HashMap<String, String>,
1460    ) {
1461        for (original, replacement) in color_map.iter().chain(type_map.iter()) {
1462            self.apply_text_change_intrinsic(original, replacement);
1463        }
1464    }
1465
1466    /// Java parity hook for `SpellAbility.setHostCard(Card)`.
1467    pub fn set_host_card(&mut self, card: crate::card::Card) {
1468        self.set_host_card_id(card.id);
1469    }
1470
1471    pub fn set_host_card_id(&mut self, card_id: CardId) {
1472        self.source = Some(card_id);
1473        if self.original_host.is_none() {
1474            self.original_host = Some(card_id);
1475        }
1476
1477        if let Some(sub_ability) = self.sub_ability.as_deref_mut() {
1478            sub_ability.set_host_card_id(card_id);
1479        }
1480
1481        for ability in self.trigger_spell_abilities.values_mut() {
1482            ability.set_host_card_id(card_id);
1483        }
1484    }
1485
1486    /// Java parity hook for `SpellAbility.setKeyword(KeywordInterface)`.
1487    pub fn set_keyword(&mut self, keyword: crate::keyword::keyword_interface::KeywordInterface) {
1488        if let Some(sub_ability) = self.sub_ability.as_deref_mut() {
1489            sub_ability.set_keyword(keyword.clone());
1490        }
1491
1492        for ability in self.trigger_spell_abilities.values_mut() {
1493            ability.set_keyword(keyword.clone());
1494        }
1495    }
1496
1497    /// Java parity hook for `SpellAbility.setCardState(CardState)`.
1498    #[allow(clippy::only_used_in_recursion)]
1499    pub fn set_card_state(&mut self, state: &crate::card::card_state::CardState) {
1500        if let Some(sub_ability) = self.sub_ability.as_deref_mut() {
1501            sub_ability.set_card_state(state);
1502        }
1503
1504        for ability in self.trigger_spell_abilities.values_mut() {
1505            ability.set_card_state(state);
1506        }
1507    }
1508
1509    /// Java parity hook for `SpellAbility.setIntrinsic(boolean)`.
1510    pub fn set_intrinsic(&mut self, intrinsic: bool) {
1511        self.intrinsic = intrinsic;
1512
1513        if let Some(sub_ability) = self.sub_ability.as_deref_mut() {
1514            if sub_ability.is_intrinsic() != intrinsic {
1515                sub_ability.set_intrinsic(intrinsic);
1516            }
1517        }
1518
1519        for ability in self.trigger_spell_abilities.values_mut() {
1520            if ability.is_intrinsic() != intrinsic {
1521                ability.set_intrinsic(intrinsic);
1522            }
1523        }
1524    }
1525
1526    pub fn is_intrinsic(&self) -> bool {
1527        self.intrinsic
1528    }
1529
1530    /// Mirrors Java's `SpellAbility.getAmountLifePaid()`.
1531    pub fn get_amount_life_paid(&self) -> i32 {
1532        self.paid_life_amount
1533    }
1534
1535    /// Mirrors Java's `SpellAbility.setAmountLifePaid(int)`.
1536    pub fn set_amount_life_paid(&mut self, value: i32) {
1537        self.paid_life_amount = value;
1538    }
1539
1540    // ── AI scoring ────────────────────────────────────────────────────────
1541
1542    /// Calculate an AI score for this mana ability.
1543    /// Mirrors Java's `SpellAbility.calculateScoreForManaAbility()`.
1544    pub fn calculate_score_for_mana_ability(&self) -> i32 {
1545        if !self.is_mana_ability {
1546            return 0;
1547        }
1548        let base = self.total_amount_of_mana_generated();
1549        // Prefer abilities that produce more mana and have fewer restrictions
1550        let restriction_penalty = if self.restriction.variables.sorcery_speed() {
1551            -1
1552        } else {
1553            0
1554        };
1555        base + restriction_penalty
1556    }
1557
1558    // ── Timing checks ─────────────────────────────────────────────────────
1559
1560    /// Check if this ability can be cast at the current timing.
1561    /// Mirrors Java's `SpellAbility.canCastTiming(Game)`.
1562    pub fn can_cast_timing(&self, game: &GameState) -> bool {
1563        let can_cast_sorcery = game.turn.phase.is_main()
1564            && game.stack.is_empty()
1565            && game.turn.active_player == self.activating_player;
1566
1567        // Non-spell, non-activated abilities do not have default timing checks here.
1568        if !self.is_spell && !self.is_activated {
1569            return true;
1570        }
1571
1572        if can_cast_sorcery || self.with_flash(game) {
1573            return true;
1574        }
1575
1576        // Spells are sorcery-speed by default unless an explicit timing permission applies.
1577        if self.is_spell {
1578            return false;
1579        }
1580
1581        // Activated abilities are instant-speed by default except for explicit
1582        // sorcery-speed restrictions and planeswalker abilities.
1583        if self.is_activated {
1584            return !self.ir.pw_ability && !self.restriction.variables.sorcery_speed();
1585        }
1586
1587        true
1588    }
1589
1590    /// Check if this spell has flash.
1591    /// Mirrors Java's `SpellAbility.withFlash(Game)`.
1592    pub fn with_flash(&self, game: &GameState) -> bool {
1593        if self.restriction.variables.instant_speed() {
1594            return true;
1595        }
1596        if self.ir.flash {
1597            return true;
1598        }
1599        if let Some(card_id) = self.source {
1600            let card = game.card(card_id);
1601            if ((self.is_spell || self.is_land_ability) && card.type_line.is_instant())
1602                || card.has_keyword("Flash")
1603            {
1604                return true;
1605            }
1606            return crate::staticability::static_ability_cast_with_flash::any_with_flash_for_card(
1607                &game.cards,
1608                card,
1609                self.activating_player,
1610            );
1611        }
1612        false
1613    }
1614
1615    /// Check restrictions for this ability.
1616    /// Mirrors Java's `SpellAbility.checkRestrictions(Game)`.
1617    pub fn check_restrictions(&self, game: &GameState) -> bool {
1618        self.can_play(game)
1619    }
1620
1621    // ── Rollback ──────────────────────────────────────────────────────────
1622
1623    /// Add a rollback effect.
1624    /// Mirrors Java's `SpellAbility.addRollbackEffect(String)`.
1625    pub fn add_rollback_effect(&mut self, effect: String) {
1626        self.rollback_effects.push(effect);
1627    }
1628
1629    /// Rollback all tracked effects.
1630    /// Mirrors Java's `SpellAbility.rollback()`.
1631    pub fn rollback(&mut self) -> bool {
1632        let had_effects = !self.rollback_effects.is_empty();
1633        self.rollback_effects.clear();
1634        had_effects
1635    }
1636
1637    // ── Optional keyword amounts ──────────────────────────────────────────
1638
1639    /// Check if this ability has an optional keyword with a specific amount.
1640    /// Mirrors Java's `SpellAbility.hasOptionalKeywordAmount(String)`.
1641    pub fn has_optional_keyword_amount(&self, keyword: &str) -> bool {
1642        self.optional_keyword_amounts.contains_key(keyword)
1643    }
1644
1645    /// Clear all optional keyword amounts.
1646    /// Mirrors Java's `SpellAbility.clearOptionalKeywordAmount()`.
1647    pub fn clear_optional_keyword_amount(&mut self) {
1648        self.optional_keyword_amounts.clear();
1649    }
1650
1651    /// Clear last known state tracking.
1652    /// Mirrors Java's `SpellAbility.clearLastState()`.
1653    pub fn clear_last_state(&mut self) {
1654        self.last_state.clear();
1655    }
1656
1657    // ── Trigger object management ─────────────────────────────────────────
1658
1659    /// Set a triggering object in the map.
1660    /// Mirrors Java's `SpellAbility.setTriggeringObject(AbilityKey, Object)`.
1661    pub fn set_triggering_object<K: TriggerKeyInput, V: Into<AbilityValue>>(
1662        &mut self,
1663        key: K,
1664        value: V,
1665    ) {
1666        if let Some(parsed) = key.into_ability_key() {
1667            self.trigger_objects.insert(parsed, value.into());
1668        }
1669    }
1670
1671    /// Typed trigger value setter.
1672    pub fn set_triggering_value<V: Into<AbilityValue>>(&mut self, key: AbilityKey, value: V) {
1673        self.trigger_objects.insert(key, value.into());
1674    }
1675
1676    /// Set a triggering spell ability in the map.
1677    /// Mirrors Java's `SpellAbility.setTriggeringObject(AbilityKey, Object)` for SpellAbility values.
1678    pub fn set_triggering_spell_ability<K: TriggerKeyInput>(
1679        &mut self,
1680        key: K,
1681        value: SpellAbility,
1682    ) {
1683        if let Some(parsed) = key.into_ability_key() {
1684            self.trigger_spell_abilities.insert(parsed, value);
1685        }
1686    }
1687
1688    /// Get a triggering spell ability from the map.
1689    pub fn get_triggering_spell_ability<K: TriggerKeyInput>(
1690        &self,
1691        key: K,
1692    ) -> Option<&SpellAbility> {
1693        key.into_ability_key()
1694            .and_then(|parsed| self.trigger_spell_abilities.get(&parsed))
1695    }
1696
1697    /// Update an existing triggering object.
1698    /// Mirrors Java's `SpellAbility.updateTriggeringObject(String, Object)`.
1699    pub fn update_triggering_object<K: TriggerKeyInput, V: Into<AbilityValue>>(
1700        &mut self,
1701        key: K,
1702        value: V,
1703    ) {
1704        self.set_triggering_object(key, value);
1705    }
1706
1707    // ── Target management ─────────────────────────────────────────────────
1708
1709    /// Update a target in the chosen targets.
1710    /// Mirrors Java's `SpellAbility.updateTarget(Card, Card)`.
1711    pub fn update_target(&mut self, old: CardId, new: CardId) {
1712        self.target_chosen.replace_target_card(old, new);
1713    }
1714
1715    /// Whether this targets a single target only.
1716    /// Mirrors Java's `SpellAbility.targetsSingleTarget()`.
1717    pub fn targets_single_target(&self) -> bool {
1718        if let Some(ref tr) = self.target_restrictions {
1719            tr.max_targets == "1"
1720        } else {
1721            false
1722        }
1723    }
1724
1725    // ── Variable operand getters/setters ──────────────────────────────────
1726    // These mirror Java's SpellAbilityVariables Operand/ToCheck/Operator accessors.
1727    // In Rust, they are stored in the SpellAbilityVariables but accessed via SA.
1728
1729    /// Get variable operand 1.
1730    /// Mirrors Java's `SpellAbility.getSVar("Operand")`.
1731    pub fn gets_var_operand(&self) -> Option<&str> {
1732        self.condition
1733            .variables
1734            .gets_var_operand()
1735            .or_else(|| self.restriction.variables.gets_var_operand())
1736    }
1737
1738    /// Get variable operand 2.
1739    /// Mirrors Java's `SpellAbility.getSVar("Operand2")`.
1740    pub fn gets_var_operand2(&self) -> Option<&str> {
1741        self.condition
1742            .variables
1743            .gets_var_operand2()
1744            .or_else(|| self.restriction.variables.gets_var_operand2())
1745    }
1746
1747    /// Set variable operand 1.
1748    /// Mirrors Java's `SpellAbility.setSVar("Operand", val)`.
1749    pub fn sets_var_operand(&mut self, value: &str) {
1750        self.condition.variables.sets_var_operand(value);
1751        self.restriction.variables.sets_var_operand(value);
1752    }
1753
1754    /// Set variable operand 2.
1755    /// Mirrors Java's `SpellAbility.setSVar("Operand2", val)`.
1756    pub fn sets_var_operand2(&mut self, value: &str) {
1757        self.condition.variables.sets_var_operand2(value);
1758        self.restriction.variables.sets_var_operand2(value);
1759    }
1760
1761    /// Get variable to check 1.
1762    /// Mirrors Java's `SpellAbility.getSVar("VarToCheck")`.
1763    pub fn gets_var_to_check(&self) -> Option<&str> {
1764        self.condition
1765            .variables
1766            .gets_var_to_check()
1767            .or_else(|| self.restriction.variables.gets_var_to_check())
1768    }
1769
1770    /// Get variable to check 2.
1771    /// Mirrors Java's `SpellAbility.getSVar("VarToCheck2")`.
1772    pub fn gets_var_to_check2(&self) -> Option<&str> {
1773        self.condition
1774            .variables
1775            .gets_var_to_check2()
1776            .or_else(|| self.restriction.variables.gets_var_to_check2())
1777    }
1778
1779    /// Set variable to check 1.
1780    /// Mirrors Java's `SpellAbility.setSVar("VarToCheck", val)`.
1781    pub fn sets_var_to_check(&mut self, value: &str) {
1782        self.condition.variables.sets_var_to_check(value);
1783        self.restriction.variables.sets_var_to_check(value);
1784    }
1785
1786    /// Set variable to check 2.
1787    /// Mirrors Java's `SpellAbility.setSVar("VarToCheck2", val)`.
1788    pub fn sets_var_to_check2(&mut self, value: &str) {
1789        self.condition.variables.sets_var_to_check2(value);
1790        self.restriction.variables.sets_var_to_check2(value);
1791    }
1792
1793    /// Get variable operator 1.
1794    /// Mirrors Java's `SpellAbility.getSVar("Operator")`.
1795    pub fn gets_var_operator(&self) -> Option<&str> {
1796        self.condition
1797            .variables
1798            .gets_var_operator()
1799            .or_else(|| self.restriction.variables.gets_var_operator())
1800    }
1801
1802    /// Get variable operator 2.
1803    /// Mirrors Java's `SpellAbility.getSVar("Operator2")`.
1804    pub fn gets_var_operator2(&self) -> Option<&str> {
1805        self.condition
1806            .variables
1807            .gets_var_operator2()
1808            .or_else(|| self.restriction.variables.gets_var_operator2())
1809    }
1810
1811    /// Set variable operator 1.
1812    /// Mirrors Java's `SpellAbility.setSVar("Operator", val)`.
1813    pub fn sets_var_operator(&mut self, value: &str) {
1814        self.condition.variables.sets_var_operator(value);
1815        self.restriction.variables.sets_var_operator(value);
1816    }
1817
1818    /// Set variable operator 2.
1819    /// Mirrors Java's `SpellAbility.setSVar("Operator2", val)`.
1820    pub fn sets_var_operator2(&mut self, value: &str) {
1821        self.condition.variables.sets_var_operator2(value);
1822        self.restriction.variables.sets_var_operator2(value);
1823    }
1824}
1825
1826// build_spell_ability now lives in ability::ability_factory.
1827// Re-export here for backward compatibility.
1828pub use crate::ability::ability_factory::build_spell_ability;
1829pub use crate::ability::ability_factory::build_spell_ability_for_card_cast;
1830pub use crate::ability::ability_factory::build_spell_ability_from_host_card;
1831
1832/// Check whether any spell on the stack has split second.
1833/// Split second prevents players from casting spells or activating abilities
1834/// (except mana abilities) while it's on the stack.
1835/// Single source of truth — used by spell, ability, and ability_activated modules.
1836pub fn has_split_second_on_stack(game: &GameState) -> bool {
1837    for entry in game.stack.iter() {
1838        if entry.spell_ability.ir.split_second {
1839            return true;
1840        }
1841        if let Some(card_id) = entry.spell_ability.source {
1842            let card = game.card(card_id);
1843            if card.has_keyword("Split second") {
1844                return true;
1845            }
1846        }
1847    }
1848    false
1849}
1850
1851pub fn choose_targets_by_kind(
1852    agent: &mut dyn PlayerAgent,
1853    sa: &mut SpellAbility,
1854    game: &GameState,
1855    mana_pools: &[ManaPool],
1856) -> bool {
1857    use crate::card::card_util;
1858
1859    let tr = match &sa.target_restrictions {
1860        Some(tr) => tr,
1861        None => return true,
1862    };
1863
1864    let player = sa.targeting_player.unwrap_or(sa.activating_player);
1865
1866    let min_targets = tr.get_min_targets(game, sa);
1867    let max_targets = tr.get_max_targets(game, sa);
1868    if max_targets <= 0 {
1869        return true;
1870    }
1871
1872    if !matches!(tr.target_kind, TargetKind::CardInZone { .. })
1873        && !tr.has_candidates(game, player, sa.source)
1874    {
1875        return min_targets <= 0;
1876    }
1877
1878    sa.target_chosen.target_card = None;
1879    sa.target_chosen.target_card_zone_timestamp = None;
1880    sa.target_chosen.divided_map.clear();
1881
1882    match &tr.target_kind {
1883        TargetKind::None => {}
1884        TargetKind::Player => {
1885            agent.snapshot_state(game, mana_pools);
1886            let is_opponent_only = tr
1887                .valid_tgts
1888                .iter()
1889                .any(|v| v.eq_ignore_ascii_case("Opponent"));
1890            let valid_players: Vec<PlayerId> = game
1891                .alive_players()
1892                .into_iter()
1893                .filter(|&pid| !is_opponent_only || pid != player)
1894                .collect();
1895            if max_targets > 1 {
1896                let mut chosen = Vec::new();
1897                while (chosen.len() as i32) < max_targets {
1898                    let Some(pid) = agent.choose_target_player(player, &valid_players, Some(&*sa))
1899                    else {
1900                        break;
1901                    };
1902                    if !chosen.contains(&pid) {
1903                        chosen.push(pid);
1904                    }
1905                    if chosen.len() == valid_players.len() {
1906                        break;
1907                    }
1908                }
1909                sa.target_chosen.target_player = chosen.first().copied();
1910                sa.target_chosen.additional_target_players = chosen.into_iter().skip(1).collect();
1911            } else {
1912                sa.target_chosen.target_player =
1913                    agent.choose_target_player(player, &valid_players, Some(&*sa));
1914            }
1915        }
1916        TargetKind::Any => {
1917            let valid_players: Vec<PlayerId> =
1918                if target_restrictions::any_target_allows_players(&tr.valid_tgts) {
1919                    game.alive_players().into_iter().collect()
1920                } else {
1921                    Vec::new()
1922                };
1923            let valid_cards: Vec<CardId> = card_util::get_valid_cards_to_target(game, sa);
1924            agent.snapshot_state(game, mana_pools);
1925            match agent.choose_target_any(player, &valid_players, &valid_cards, Some(&*sa)) {
1926                crate::agent::TargetChoice::Player(pid) => {
1927                    sa.target_chosen.target_player = Some(pid)
1928                }
1929                crate::agent::TargetChoice::Card(cid) => {
1930                    sa.target_chosen.target_card = Some(cid);
1931                    sa.target_chosen.target_card_zone_timestamp =
1932                        Some(game.card(cid).zone_timestamp);
1933                }
1934                crate::agent::TargetChoice::None => {}
1935            }
1936        }
1937        TargetKind::Creature(_) => {
1938            let valid: Vec<CardId> = card_util::get_valid_cards_to_target(game, sa)
1939                .into_iter()
1940                .filter(|&cid| target_allowed_by_defined_controller(game, sa, cid))
1941                .collect();
1942            agent.snapshot_state(game, mana_pools);
1943            if max_targets > 1 {
1944                let chosen = agent.choose_cards_for_effect(
1945                    player,
1946                    &valid,
1947                    min_targets.max(0) as usize,
1948                    max_targets as usize,
1949                );
1950                if let Some(&first) = chosen.first() {
1951                    sa.target_chosen.target_card = Some(first);
1952                    sa.target_chosen.target_card_zone_timestamp =
1953                        Some(game.card(first).zone_timestamp);
1954                    for &extra in chosen.iter().skip(1) {
1955                        sa.target_chosen.divided_map.insert(extra, 0);
1956                    }
1957                }
1958            } else {
1959                sa.target_chosen.target_card = agent.choose_target_card(player, &valid, Some(&*sa));
1960                if let Some(cid) = sa.target_chosen.target_card {
1961                    sa.target_chosen.target_card_zone_timestamp =
1962                        Some(game.card(cid).zone_timestamp);
1963                }
1964            }
1965        }
1966        TargetKind::Permanent(_) => {
1967            let valid: Vec<CardId> = card_util::get_valid_cards_to_target(game, sa)
1968                .into_iter()
1969                .filter(|&cid| target_allowed_by_defined_controller(game, sa, cid))
1970                .collect();
1971            agent.snapshot_state(game, mana_pools);
1972            if max_targets > 1 {
1973                let chosen = agent.choose_cards_for_effect(
1974                    player,
1975                    &valid,
1976                    min_targets.max(0) as usize,
1977                    max_targets as usize,
1978                );
1979                if let Some(&first) = chosen.first() {
1980                    sa.target_chosen.target_card = Some(first);
1981                    sa.target_chosen.target_card_zone_timestamp =
1982                        Some(game.card(first).zone_timestamp);
1983                    for &extra in chosen.iter().skip(1) {
1984                        sa.target_chosen.divided_map.insert(extra, 0);
1985                    }
1986                }
1987            } else {
1988                sa.target_chosen.target_card = agent.choose_target_card(player, &valid, Some(&*sa));
1989                if let Some(cid) = sa.target_chosen.target_card {
1990                    sa.target_chosen.target_card_zone_timestamp =
1991                        Some(game.card(cid).zone_timestamp);
1992                }
1993            }
1994        }
1995        TargetKind::CardInZone { zone, .. } => {
1996            let valid: Vec<CardId> = card_util::get_valid_cards_to_target(game, sa)
1997                .into_iter()
1998                .filter(|&cid| target_allowed_by_defined_controller(game, sa, cid))
1999                .collect();
2000            if valid.is_empty() {
2001                return min_targets <= 0;
2002            }
2003            agent.snapshot_state(game, mana_pools);
2004            if max_targets > 1 {
2005                let chosen = agent.choose_cards_for_effect(
2006                    player,
2007                    &valid,
2008                    min_targets.max(0) as usize,
2009                    max_targets as usize,
2010                );
2011                if let Some(&first) = chosen.first() {
2012                    sa.target_chosen.target_card = Some(first);
2013                    sa.target_chosen.target_card_zone_timestamp =
2014                        Some(game.card(first).zone_timestamp);
2015                    for &extra in chosen.iter().skip(1) {
2016                        sa.target_chosen.divided_map.insert(extra, 0);
2017                    }
2018                }
2019            } else {
2020                sa.target_chosen.target_card =
2021                    agent.choose_target_card_from_zone(player, *zone, &valid, Some(&*sa));
2022                if let Some(cid) = sa.target_chosen.target_card {
2023                    sa.target_chosen.target_card_zone_timestamp =
2024                        Some(game.card(cid).zone_timestamp);
2025                }
2026            }
2027        }
2028        TargetKind::Spell => {
2029            let valid = target_restrictions::get_all_candidates_spells(game);
2030            let valid = if let Some(ref restrictions) = sa.target_restrictions {
2031                target_restrictions::filter_spells_for_target_restrictions(
2032                    game,
2033                    &valid,
2034                    restrictions,
2035                )
2036            } else {
2037                valid
2038            };
2039            agent.snapshot_state(game, mana_pools);
2040            sa.target_chosen.target_stack_entry =
2041                agent.choose_target_spell(player, &valid, sa.source);
2042        }
2043    }
2044
2045    let chosen_targets = sa.target_chosen.all_target_cards().len() as i32
2046        + sa.target_chosen.all_target_players().len() as i32
2047        + i32::from(sa.target_chosen.target_stack_entry.is_some());
2048    chosen_targets >= min_targets
2049}
2050
2051fn target_allowed_by_defined_controller(
2052    game: &GameState,
2053    sa: &SpellAbility,
2054    card_id: CardId,
2055) -> bool {
2056    let Some(defined) = sa.ir.targets_with_defined_controller_text.as_deref() else {
2057        return true;
2058    };
2059    let players = crate::ability::ability_utils::resolve_defined_players_with_sa(
2060        defined,
2061        sa,
2062        sa.activating_player,
2063        game,
2064    );
2065    players.is_empty() || players.contains(&game.card(card_id).controller)
2066}
2067
2068fn choose_targeting_player(
2069    sa: &SpellAbility,
2070    game: &GameState,
2071    agents: &mut [Box<dyn PlayerAgent>],
2072) -> Option<PlayerId> {
2073    if let Some(defined) = sa.ir.targeting_player_text.as_deref() {
2074        let candidates = crate::ability::ability_utils::resolve_defined_players_with_sa(
2075            defined,
2076            sa,
2077            sa.activating_player,
2078            game,
2079        );
2080        if candidates.is_empty() {
2081            return None;
2082        }
2083        return agents[sa.activating_player.index()].choose_target_player(
2084            sa.activating_player,
2085            &candidates,
2086            None,
2087        );
2088    }
2089    Some(sa.activating_player)
2090}
2091
2092// Re-export MagicStack and StackEntry from zone module (their canonical home,
2093// matching Java's `forge.game.zone.MagicStack`).
2094pub use crate::zone::magic_stack::{MagicStack, StackEntry};