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            Some(AbilityValue::GameEntities(entities)) => {
866                entities.iter().find_map(|entity| match entity {
867                    crate::agent::GameEntity::Card(card) => Some(*card),
868                    crate::agent::GameEntity::Player(_) => None,
869                })
870            }
871            _ => None,
872        }
873    }
874
875    /// Get a triggering player by key.
876    pub fn get_triggering_player(&self, key: AbilityKey) -> Option<PlayerId> {
877        match self.get_triggering_value(key) {
878            Some(AbilityValue::Player(player)) => Some(*player),
879            Some(AbilityValue::Players(players)) => players.first().copied(),
880            Some(AbilityValue::GameEntities(entities)) => {
881                entities.iter().find_map(|entity| match entity {
882                    crate::agent::GameEntity::Player(player) => Some(*player),
883                    crate::agent::GameEntity::Card(_) => None,
884                })
885            }
886            _ => None,
887        }
888    }
889
890    /// Get triggering cards by key.
891    pub fn get_triggering_cards(&self, key: AbilityKey) -> Vec<CardId> {
892        match self.get_triggering_value(key) {
893            Some(AbilityValue::Card(card)) => vec![*card],
894            Some(AbilityValue::Cards(cards)) => cards.clone(),
895            Some(AbilityValue::GameEntities(entities)) => entities
896                .iter()
897                .filter_map(|entity| match entity {
898                    crate::agent::GameEntity::Card(card) => Some(*card),
899                    crate::agent::GameEntity::Player(_) => None,
900                })
901                .collect(),
902            _ => Vec::new(),
903        }
904    }
905
906    /// Get triggering players by key.
907    pub fn get_triggering_players(&self, key: AbilityKey) -> Vec<PlayerId> {
908        match self.get_triggering_value(key) {
909            Some(AbilityValue::Player(player)) => vec![*player],
910            Some(AbilityValue::Players(players)) => players.clone(),
911            Some(AbilityValue::GameEntities(entities)) => entities
912                .iter()
913                .filter_map(|entity| match entity {
914                    crate::agent::GameEntity::Player(player) => Some(*player),
915                    crate::agent::GameEntity::Card(_) => None,
916                })
917                .collect(),
918            _ => Vec::new(),
919        }
920    }
921
922    /// Get a triggering object by key.
923    /// Mirrors Java's `SpellAbility.getTriggeringObject(String)`.
924    pub fn get_triggering_object<K: TriggerKeyInput>(&self, key: K) -> Option<&str> {
925        key.into_ability_key()
926            .and_then(|parsed| self.get_triggering_value(parsed))
927            .and_then(|value| match value {
928                AbilityValue::String(raw) => Some(raw.as_str()),
929                _ => None,
930            })
931    }
932
933    /// Clear all triggering objects.
934    /// Mirrors Java's `SpellAbility.resetTriggeringObjects()`.
935    pub fn reset_triggering_objects(&mut self) {
936        self.trigger_objects.clear();
937    }
938
939    /// Cleanup after resolution — reset targets, trigger objects, paid hash.
940    /// Mirrors Java's `SpellAbility.resetOnceResolved()`.
941    pub fn reset_once_resolved(&mut self) {
942        self.clear_targets();
943        self.reset_triggering_objects();
944        self.reset_paid_hash();
945        self.x_mana_cost_paid = 0;
946        self.kick_count = 0;
947        self.replicate_count = 0;
948        self.optional_generic_cost_paid = false;
949        self.discarded_cost_cards.clear();
950        self.optional_costs.clear();
951        self.convoke_tapped.clear();
952        self.spliced_cards.clear();
953        self.announce_vars.clear();
954        self.sacrificed_as_emerge = None;
955        self.sacrificed_as_offering = None;
956    }
957
958    // ── Description and text ──────────────────────────────────────────────
959
960    /// Generate a unique key for this ability.
961    /// Mirrors Java's `SpellAbility.yieldKey()`.
962    pub fn yield_key(&self) -> String {
963        let api_str = self.api.map(|a| format!("{a:?}")).unwrap_or_default();
964        let source_str = self.source.map(|s| format!("{}", s.0)).unwrap_or_default();
965        format!("{api_str}_{source_str}")
966    }
967
968    /// Build a description from params.
969    /// Mirrors Java's `SpellAbility.rebuiltDescription()`.
970    pub fn rebuilt_description(&self) -> String {
971        if !self.description.is_empty() {
972            return self.description.clone();
973        }
974        if let Some(desc) = self.ir.sp_desc_text.as_deref() {
975            return desc.to_string();
976        }
977        self.ability_text.clone()
978    }
979
980    /// Full text without suppression.
981    /// Mirrors Java's `SpellAbility.toUnsuppressedString()`.
982    pub fn to_unsuppressed_string(&self) -> String {
983        self.rebuilt_description()
984    }
985
986    // ── Sub-abilities ─────────────────────────────────────────────────────
987
988    /// Check if an additional ability with the given key exists.
989    /// Mirrors Java's `SpellAbility.hasAdditionalAbility(String)`.
990    pub fn has_additional_ability<K: TriggerKeyInput>(&self, key: K) -> bool {
991        key.into_ability_key()
992            .map(|parsed| self.trigger_spell_abilities.contains_key(&parsed))
993            .unwrap_or(false)
994    }
995
996    /// Get an additional ability by key.
997    /// Mirrors Java's `SpellAbility.getAdditionalAbility(String)`.
998    pub fn get_additional_ability<K: TriggerKeyInput>(&self, key: K) -> Option<&SpellAbility> {
999        key.into_ability_key()
1000            .and_then(|parsed| self.trigger_spell_abilities.get(&parsed))
1001    }
1002
1003    /// Set an additional ability by key.
1004    /// Mirrors Java's `SpellAbility.setAdditionalAbility(String, SpellAbility)`.
1005    pub fn set_additional_ability<K: TriggerKeyInput>(&mut self, key: K, ability: SpellAbility) {
1006        if let Some(parsed) = key.into_ability_key() {
1007            self.trigger_spell_abilities.insert(parsed, ability);
1008        }
1009    }
1010
1011    /// Append a sub-ability to the end of the chain.
1012    /// Mirrors Java's `SpellAbility.appendSubAbility(SpellAbility)`.
1013    pub fn append_sub_ability(&mut self, sub: SpellAbility) {
1014        if self.sub_ability.is_none() {
1015            self.sub_ability = Some(Box::new(sub));
1016        } else {
1017            // Walk to end of chain
1018            let mut current = self.sub_ability.as_deref_mut();
1019            while let Some(sa) = current {
1020                if sa.sub_ability.is_none() {
1021                    sa.sub_ability = Some(Box::new(sub));
1022                    return;
1023                }
1024                current = sa.sub_ability.as_deref_mut();
1025            }
1026        }
1027    }
1028
1029    // ── Copying ───────────────────────────────────────────────────────────
1030
1031    /// Clone this spell ability.
1032    /// Mirrors Java's `SpellAbility.copy()`.
1033    pub fn copy(&self) -> Self {
1034        crate::perf::increment(crate::perf::Metric::SpellAbilityClones, 1);
1035        self.clone()
1036    }
1037
1038    pub fn copy_for_player(&self, activ: PlayerId) -> Self {
1039        crate::perf::increment(crate::perf::Metric::SpellAbilityClones, 1);
1040        let mut clone = self.clone();
1041        clone.activating_player = activ;
1042        clone
1043    }
1044
1045    pub fn copy_with_host_lki(&self, host: crate::card::Card, lki: bool) -> Self {
1046        self.copy_with_host_activating_lki_keep_text_changes(
1047            host,
1048            self.activating_player,
1049            lki,
1050            false,
1051        )
1052    }
1053
1054    pub fn copy_with_host_lki_keep_text_changes(
1055        &self,
1056        host: crate::card::Card,
1057        lki: bool,
1058        keep_text_changes: bool,
1059    ) -> Self {
1060        self.copy_with_host_activating_lki_keep_text_changes(
1061            host,
1062            self.activating_player,
1063            lki,
1064            keep_text_changes,
1065        )
1066    }
1067
1068    pub fn copy_with_host_activating_lki(
1069        &self,
1070        host: crate::card::Card,
1071        activ: PlayerId,
1072        lki: bool,
1073    ) -> Self {
1074        self.copy_with_host_activating_lki_keep_text_changes(host, activ, lki, false)
1075    }
1076
1077    pub fn copy_with_host_activating_lki_keep_text_changes(
1078        &self,
1079        host: crate::card::Card,
1080        activ: PlayerId,
1081        lki: bool,
1082        keep_text_changes: bool,
1083    ) -> Self {
1084        crate::perf::increment(crate::perf::Metric::SpellAbilityClones, 1);
1085        let mut clone = self.clone();
1086        clone.id = if lki {
1087            self.id
1088        } else {
1089            next_spell_ability_id()
1090        };
1091
1092        clone.source = Some(host.id);
1093        clone.may_choose_new_targets = false;
1094        clone.trigger_objects = self.trigger_objects.clone();
1095        if !lki {
1096            clone.replacing_objects = HashMap::new();
1097        }
1098
1099        clone.pay_costs = self.pay_costs.clone();
1100        if self.mana_part.is_some() {
1101            clone.mana_part = self.mana_part.clone();
1102        }
1103
1104        clone.optional_keyword_amounts = self.optional_keyword_amounts.clone();
1105        clone.damage_map = self.damage_map.clone();
1106        clone.prevent_map = self.prevent_map.clone();
1107        clone.change_zone_table = self.change_zone_table.clone();
1108        clone.paying_mana = self.paying_mana.clone();
1109        clone.paid_abilities = Vec::new();
1110        clone.paid_hash = self.paid_hash.clone();
1111
1112        if self.uses_targeting() {
1113            clone.target_chosen = self.target_chosen.clone();
1114        }
1115
1116        clone.trigger_spell_abilities = HashMap::new();
1117        clone.additional_ability_lists = HashMap::new();
1118
1119        if let Some(sub_ability) = &self.sub_ability {
1120            clone.sub_ability = Some(Box::new(
1121                sub_ability.copy_with_host_activating_lki_keep_text_changes(
1122                    host.clone(),
1123                    activ,
1124                    lki,
1125                    keep_text_changes,
1126                ),
1127            ));
1128        }
1129
1130        for (name, ability) in &self.trigger_spell_abilities {
1131            clone.trigger_spell_abilities.insert(
1132                *name,
1133                ability.copy_with_host_activating_lki_keep_text_changes(
1134                    host.clone(),
1135                    activ,
1136                    lki,
1137                    keep_text_changes,
1138                ),
1139            );
1140        }
1141
1142        for (name, abilities) in &self.additional_ability_lists {
1143            clone.additional_ability_lists.insert(
1144                name.clone(),
1145                abilities
1146                    .iter()
1147                    .map(|ability| {
1148                        ability.copy_with_host_activating_lki_keep_text_changes(
1149                            host.clone(),
1150                            activ,
1151                            lki,
1152                            keep_text_changes,
1153                        )
1154                    })
1155                    .collect(),
1156            );
1157        }
1158
1159        clone.restriction = self.restriction.clone();
1160        clone.condition = self.condition.clone();
1161        clone.activating_player = activ;
1162
1163        let _ = keep_text_changes;
1164        clone
1165    }
1166
1167    /// Clone with no mana cost.
1168    /// Mirrors Java's `SpellAbility.copyWithNoManaCost()`.
1169    pub fn copy_with_no_mana_cost(&self) -> Self {
1170        crate::perf::increment(crate::perf::Metric::SpellAbilityClones, 1);
1171        let mut copied = self.clone();
1172        copied.pay_costs = None;
1173        copied
1174    }
1175
1176    /// Clone with a specific cost.
1177    /// Mirrors Java's `SpellAbility.copyWithDefinedCost(String)`.
1178    pub fn copy_with_defined_cost(&self, cost: &str) -> Self {
1179        crate::perf::increment(crate::perf::Metric::SpellAbilityClones, 1);
1180        let mut copied = self.clone();
1181        copied.pay_costs = Some(parse_cost(cost));
1182        copied
1183    }
1184
1185    /// Clone with mana cost replacement.
1186    /// Mirrors Java's `SpellAbility.copyWithManaCostReplaced(String, String)`.
1187    pub fn copy_with_mana_cost_replaced(&self, old: &str, new: &str) -> Self {
1188        crate::perf::increment(crate::perf::Metric::SpellAbilityClones, 1);
1189        let mut copied = self.clone();
1190        if let Some(ref cost) = self.pay_costs {
1191            let cost_str = format!("{cost:?}");
1192            let replaced = cost_str.replace(old, new);
1193            copied.pay_costs = Some(parse_cost(&replaced));
1194        }
1195        copied
1196    }
1197
1198    // ── Targeting ─────────────────────────────────────────────────────────
1199
1200    /// Check if this ability can target a specific card.
1201    /// Mirrors Java's `SpellAbility.canTarget(Card)`.
1202    pub fn can_target(&self, card: CardId, game: &GameState) -> bool {
1203        if let Some(ref tr) = self.target_restrictions {
1204            tr.has_candidates(game, self.activating_player, self.source)
1205                && self
1206                    .ir
1207                    .targets_with_defined_controller_text
1208                    .as_deref()
1209                    .map(|defined| {
1210                        crate::ability::ability_utils::resolve_defined_players_with_sa(
1211                            defined,
1212                            self,
1213                            self.activating_player,
1214                            game,
1215                        )
1216                    })
1217                    .map(|players| {
1218                        players.is_empty() || players.contains(&game.card(card).controller)
1219                    })
1220                    .unwrap_or(true)
1221                && target_restrictions::can_be_targeted_by_sa(
1222                    game,
1223                    card,
1224                    self.activating_player,
1225                    self,
1226                )
1227        } else {
1228            false
1229        }
1230    }
1231
1232    /// Reset targets (alias for clear_targets).
1233    /// Mirrors Java's `SpellAbility.resetTargets()`.
1234    pub fn reset_targets(&mut self) {
1235        self.clear_targets();
1236    }
1237
1238    /// Add divided allocation for a target.
1239    /// Mirrors Java's `SpellAbility.addDividedAllocation(Card, int)`.
1240    pub fn add_divided_allocation(&mut self, card: CardId, amount: i32) {
1241        self.target_chosen.add_divided_allocation(card, amount);
1242    }
1243
1244    /// Reset only the first target in the chain.
1245    /// Mirrors Java's `SpellAbility.resetFirstTarget()`.
1246    pub fn reset_first_target(&mut self) {
1247        self.target_chosen = TargetChoices::default();
1248    }
1249
1250    /// Check if more targets can be added.
1251    /// Mirrors Java's `SpellAbility.canAddMoreTarget()`.
1252    pub fn can_add_more_target(&self, game: &GameState) -> bool {
1253        if let Some(ref tr) = self.target_restrictions {
1254            let max = tr.get_max_targets(game, self);
1255            let current = self.target_chosen.all_target_cards().len() as i32
1256                + self.target_chosen.all_target_players().len() as i32;
1257            current < max
1258        } else {
1259            false
1260        }
1261    }
1262
1263    /// Collect all targeted cards from the entire chain.
1264    /// Mirrors Java's `SpellAbility.findTargetedCards()`.
1265    pub fn find_targeted_cards(&self) -> Vec<CardId> {
1266        let mut cards = Vec::new();
1267        cards.extend(self.target_chosen.all_target_cards());
1268        let mut current = self.sub_ability.as_deref();
1269        while let Some(sub) = current {
1270            cards.extend(sub.target_chosen.all_target_cards());
1271            current = sub.sub_ability.as_deref();
1272        }
1273        cards
1274    }
1275
1276    /// Collect all targeted players from the entire chain.
1277    /// Mirrors Java's `SpellAbility.findTargetedPlayers()`.
1278    pub fn find_targeted_players(&self) -> Vec<PlayerId> {
1279        let mut players = Vec::new();
1280        players.extend(self.target_chosen.all_target_players());
1281        let mut current = self.sub_ability.as_deref();
1282        while let Some(sub) = current {
1283            for player in sub.target_chosen.all_target_players() {
1284                if !players.contains(&player) {
1285                    players.push(player);
1286                }
1287            }
1288            current = sub.sub_ability.as_deref();
1289        }
1290        players
1291    }
1292
1293    /// Whether this ability targets spells/abilities on the stack.
1294    /// Mirrors Java's `SpellAbility.canTargetSpellAbility()`.
1295    pub fn can_target_spell_ability(&self) -> bool {
1296        matches!(
1297            self.target_restrictions.as_ref().map(|tr| &tr.target_kind),
1298            Some(TargetKind::Spell)
1299        )
1300    }
1301
1302    /// Setup new targets for a retargeting scenario.
1303    /// Mirrors Java's `SpellAbility.setupNewTargets()`.
1304    pub fn setup_new_targets(
1305        &mut self,
1306        game: &GameState,
1307        agents: &mut [Box<dyn PlayerAgent>],
1308        mana_pools: &[ManaPool],
1309    ) -> bool {
1310        self.clear_targets();
1311        self.setup_targets(game, agents, mana_pools)
1312    }
1313
1314    // ── Convoke / Emerge / Offering ───────────────────────────────────────
1315
1316    /// Clear pip reduction tracking.
1317    /// Mirrors Java's `SpellAbility.clearPipsToReduce()`.
1318    pub fn clear_pips_to_reduce(&mut self) {
1319        self.pips_to_reduce.clear();
1320    }
1321
1322    /// Add a card tapped for convoke.
1323    /// Mirrors Java's `SpellAbility.addTappedForConvoke(Card)`.
1324    pub fn add_tapped_for_convoke(&mut self, card: CardId) {
1325        self.convoke_tapped.push(card);
1326    }
1327
1328    /// Clear convoke tracking.
1329    /// Mirrors Java's `SpellAbility.clearTappedForConvoke()`.
1330    pub fn clear_tapped_for_convoke(&mut self) {
1331        self.convoke_tapped.clear();
1332    }
1333
1334    /// Reset the sacrificed-as-emerge card.
1335    /// Mirrors Java's `SpellAbility.resetSacrificedAsEmerge()`.
1336    pub fn reset_sacrificed_as_emerge(&mut self) {
1337        self.sacrificed_as_emerge = None;
1338    }
1339
1340    /// Reset the sacrificed-as-offering card.
1341    /// Mirrors Java's `SpellAbility.resetSacrificedAsOffering()`.
1342    pub fn reset_sacrificed_as_offering(&mut self) {
1343        self.sacrificed_as_offering = None;
1344    }
1345
1346    // ── Splice ────────────────────────────────────────────────────────────
1347
1348    /// Add spliced cards to this spell.
1349    /// Mirrors Java's `SpellAbility.addSplicedCards(List<Card>)`.
1350    pub fn add_spliced_cards(&mut self, cards: Vec<CardId>) {
1351        self.spliced_cards.extend(cards);
1352    }
1353
1354    // ── Deterministic checks ──────────────────────────────────────────────
1355
1356    /// Whether `Defined$` resolves to a deterministic set of objects.
1357    /// Mirrors Java's `SpellAbility.knownDetermineDefined()`.
1358    pub fn known_determine_defined(&self) -> bool {
1359        match self.defined() {
1360            Some(defined) => matches!(
1361                defined,
1362                "Self"
1363                    | "You"
1364                    | "Targeted"
1365                    | "TargetedPlayer"
1366                    | "Remembered"
1367                    | "ParentTarget"
1368                    | "SourceController"
1369                    | "Imprinted"
1370            ),
1371            None => true,
1372        }
1373    }
1374
1375    // ── Undo ──────────────────────────────────────────────────────────────
1376
1377    /// Undo this ability.
1378    /// Mirrors Java's `SpellAbility.undo()`.
1379    pub fn undo(&mut self) -> bool {
1380        self.clear_tapped_for_convoke();
1381        self.reset_sacrificed_as_emerge();
1382        self.reset_sacrificed_as_offering();
1383        self.reset_paid_hash();
1384        self.clear_mana_paid();
1385        true
1386    }
1387
1388    // ── Announce vars ─────────────────────────────────────────────────────
1389
1390    /// Add an announced variable value.
1391    /// Mirrors Java's `SpellAbility.addAnnounceVar(String, int)`.
1392    pub fn add_announce_var(&mut self, key: &str, value: i32) {
1393        self.announce_vars.insert(key.to_string(), value);
1394    }
1395
1396    // ── Targeting by SA ───────────────────────────────────────────────────
1397
1398    /// Check if this spell ability can be targeted by another SA.
1399    /// Mirrors Java's `SpellAbility.canBeTargetedBy(SpellAbility)`.
1400    pub fn can_be_targeted_by(&self, _sa: &SpellAbility) -> bool {
1401        // Spells on the stack can generally be targeted unless they have
1402        // "can't be countered" or similar protection. The basic check is
1403        // whether this is a spell (on the stack).
1404        if self.is_spell {
1405            return !self.cant_be_copied();
1406        }
1407        true
1408    }
1409
1410    // ── Property checks ───────────────────────────────────────────────────
1411
1412    /// Check if this ability has a specific property.
1413    /// Mirrors Java's `SpellAbility.hasProperty(String)`.
1414    pub fn has_property(&self, property: &str) -> bool {
1415        match property {
1416            "Spell" => self.is_spell,
1417            "Trigger" => self.is_trigger,
1418            "Activated" => self.is_activated,
1419            "ManaAbility" => self.is_mana_ability,
1420            "Optional" => self.ir.optional,
1421            "Mandatory" => self.ir.mandatory,
1422            "Tapped" => self.ir.tapped,
1423            "Hidden" => self.ir.hidden,
1424            "FaceDown" => self.ir.face_down,
1425            "ExileFaceDown" => self.ir.exile_face_down,
1426            "Transformed" => self.ir.transformed,
1427            "AtRandom" => self.ir.at_random,
1428            "Imprint" => self.ir.imprint,
1429            "Morph" => self.ir.morph,
1430            "MorphUp" => self.ir.morph_up,
1431            "Megamorph" => self.ir.megamorph,
1432            "PwAbility" => self.ir.pw_ability,
1433            "Flash" => self.ir.flash,
1434            "SplitSecond" => self.ir.split_second,
1435            _ => false,
1436        }
1437    }
1438
1439    pub fn is_last_chapter(&self, game: &crate::game::GameState) -> bool {
1440        let Some(source) = self.trigger_source.or(self.source) else {
1441            return false;
1442        };
1443        let Some(trigger_id) = self.source_trigger_id else {
1444            return false;
1445        };
1446        let card = game.card(source);
1447        card.triggers
1448            .iter()
1449            .find(|trigger| trigger.id == trigger_id)
1450            .is_some_and(|trigger| trigger.is_last_chapter(card))
1451    }
1452
1453    /// Whether this ability tracks mana spent.
1454    /// Mirrors Java's `SpellAbility.tracksManaSpent()`.
1455    pub fn tracks_mana_spent(&self) -> bool {
1456        self.ir.track_mana_spent
1457    }
1458
1459    // ── Text changes ──────────────────────────────────────────────────────
1460
1461    /// Apply text replacement.
1462    /// Mirrors Java's `SpellAbility.changeText(String, String)`.
1463    pub fn apply_text_change(&mut self, original: &str, replacement: &str) {
1464        if original == replacement {
1465            return;
1466        }
1467        self.description = self.description.replace(original, replacement);
1468        self.stack_description = self.stack_description.replace(original, replacement);
1469        if let Some(ref mut tr) = self.target_restrictions {
1470            tr.apply_target_text_changes(&[(original, replacement)]);
1471        }
1472
1473        if let Some(sub_ability) = self.sub_ability.as_deref_mut() {
1474            sub_ability.apply_text_change(original, replacement);
1475        }
1476
1477        for ability in self.trigger_spell_abilities.values_mut() {
1478            ability.apply_text_change(original, replacement);
1479        }
1480    }
1481
1482    /// Apply intrinsic text replacement.
1483    /// Mirrors Java's `SpellAbility.changeTextIntrinsic(String, String)`.
1484    pub fn apply_text_change_intrinsic(&mut self, original: &str, replacement: &str) {
1485        self.apply_text_change(original, replacement);
1486    }
1487
1488    /// Apply a batch of text replacements to this ability and linked abilities.
1489    pub fn apply_text_changes(&mut self, pairs: &[(String, String)]) {
1490        for (original, replacement) in pairs {
1491            self.apply_text_change(original, replacement);
1492        }
1493    }
1494
1495    /// Apply intrinsic text changes to this ability and linked abilities.
1496    pub fn apply_text_changes_intrinsic(
1497        &mut self,
1498        color_map: &HashMap<String, String>,
1499        type_map: &HashMap<String, String>,
1500    ) {
1501        for (original, replacement) in color_map.iter().chain(type_map.iter()) {
1502            self.apply_text_change_intrinsic(original, replacement);
1503        }
1504    }
1505
1506    /// Java parity hook for `SpellAbility.setHostCard(Card)`.
1507    pub fn set_host_card(&mut self, card: crate::card::Card) {
1508        self.set_host_card_id(card.id);
1509    }
1510
1511    pub fn set_host_card_id(&mut self, card_id: CardId) {
1512        self.source = Some(card_id);
1513        if self.original_host.is_none() {
1514            self.original_host = Some(card_id);
1515        }
1516
1517        if let Some(sub_ability) = self.sub_ability.as_deref_mut() {
1518            sub_ability.set_host_card_id(card_id);
1519        }
1520
1521        for ability in self.trigger_spell_abilities.values_mut() {
1522            ability.set_host_card_id(card_id);
1523        }
1524    }
1525
1526    /// Java parity hook for `SpellAbility.setKeyword(KeywordInterface)`.
1527    pub fn set_keyword(&mut self, keyword: crate::keyword::keyword_interface::KeywordInterface) {
1528        if let Some(sub_ability) = self.sub_ability.as_deref_mut() {
1529            sub_ability.set_keyword(keyword.clone());
1530        }
1531
1532        for ability in self.trigger_spell_abilities.values_mut() {
1533            ability.set_keyword(keyword.clone());
1534        }
1535    }
1536
1537    /// Java parity hook for `SpellAbility.setCardState(CardState)`.
1538    #[allow(clippy::only_used_in_recursion)]
1539    pub fn set_card_state(&mut self, state: &crate::card::card_state::CardState) {
1540        if let Some(sub_ability) = self.sub_ability.as_deref_mut() {
1541            sub_ability.set_card_state(state);
1542        }
1543
1544        for ability in self.trigger_spell_abilities.values_mut() {
1545            ability.set_card_state(state);
1546        }
1547    }
1548
1549    /// Java parity hook for `SpellAbility.setIntrinsic(boolean)`.
1550    pub fn set_intrinsic(&mut self, intrinsic: bool) {
1551        self.intrinsic = intrinsic;
1552
1553        if let Some(sub_ability) = self.sub_ability.as_deref_mut() {
1554            if sub_ability.is_intrinsic() != intrinsic {
1555                sub_ability.set_intrinsic(intrinsic);
1556            }
1557        }
1558
1559        for ability in self.trigger_spell_abilities.values_mut() {
1560            if ability.is_intrinsic() != intrinsic {
1561                ability.set_intrinsic(intrinsic);
1562            }
1563        }
1564    }
1565
1566    pub fn is_intrinsic(&self) -> bool {
1567        self.intrinsic
1568    }
1569
1570    /// Mirrors Java's `SpellAbility.getAmountLifePaid()`.
1571    pub fn get_amount_life_paid(&self) -> i32 {
1572        self.paid_life_amount
1573    }
1574
1575    /// Mirrors Java's `SpellAbility.setAmountLifePaid(int)`.
1576    pub fn set_amount_life_paid(&mut self, value: i32) {
1577        self.paid_life_amount = value;
1578    }
1579
1580    // ── AI scoring ────────────────────────────────────────────────────────
1581
1582    /// Calculate an AI score for this mana ability.
1583    /// Mirrors Java's `SpellAbility.calculateScoreForManaAbility()`.
1584    pub fn calculate_score_for_mana_ability(&self) -> i32 {
1585        if !self.is_mana_ability {
1586            return 0;
1587        }
1588        let base = self.total_amount_of_mana_generated();
1589        // Prefer abilities that produce more mana and have fewer restrictions
1590        let restriction_penalty = if self.restriction.variables.sorcery_speed() {
1591            -1
1592        } else {
1593            0
1594        };
1595        base + restriction_penalty
1596    }
1597
1598    // ── Timing checks ─────────────────────────────────────────────────────
1599
1600    /// Check if this ability can be cast at the current timing.
1601    /// Mirrors Java's `SpellAbility.canCastTiming(Game)`.
1602    pub fn can_cast_timing(&self, game: &GameState) -> bool {
1603        let can_cast_sorcery = game.turn.phase.is_main()
1604            && game.stack.is_empty()
1605            && game.turn.active_player == self.activating_player;
1606
1607        // Non-spell, non-activated abilities do not have default timing checks here.
1608        if !self.is_spell && !self.is_activated {
1609            return true;
1610        }
1611
1612        if can_cast_sorcery || self.with_flash(game) {
1613            return true;
1614        }
1615
1616        // Spells are sorcery-speed by default unless an explicit timing permission applies.
1617        if self.is_spell {
1618            return false;
1619        }
1620
1621        // Activated abilities are instant-speed by default except for explicit
1622        // sorcery-speed restrictions and planeswalker abilities.
1623        if self.is_activated {
1624            return !self.ir.pw_ability && !self.restriction.variables.sorcery_speed();
1625        }
1626
1627        true
1628    }
1629
1630    /// Check if this spell has flash.
1631    /// Mirrors Java's `SpellAbility.withFlash(Game)`.
1632    pub fn with_flash(&self, game: &GameState) -> bool {
1633        if self.restriction.variables.instant_speed() {
1634            return true;
1635        }
1636        if self.ir.flash {
1637            return true;
1638        }
1639        if let Some(card_id) = self.source {
1640            let card = game.card(card_id);
1641            if ((self.is_spell || self.is_land_ability) && card.type_line.is_instant())
1642                || card.has_keyword("Flash")
1643            {
1644                return true;
1645            }
1646            return crate::staticability::static_ability_cast_with_flash::any_with_flash_for_card(
1647                &game.cards,
1648                card,
1649                self.activating_player,
1650            );
1651        }
1652        false
1653    }
1654
1655    /// Check restrictions for this ability.
1656    /// Mirrors Java's `SpellAbility.checkRestrictions(Game)`.
1657    pub fn check_restrictions(&self, game: &GameState) -> bool {
1658        self.can_play(game)
1659    }
1660
1661    // ── Rollback ──────────────────────────────────────────────────────────
1662
1663    /// Add a rollback effect.
1664    /// Mirrors Java's `SpellAbility.addRollbackEffect(String)`.
1665    pub fn add_rollback_effect(&mut self, effect: String) {
1666        self.rollback_effects.push(effect);
1667    }
1668
1669    /// Rollback all tracked effects.
1670    /// Mirrors Java's `SpellAbility.rollback()`.
1671    pub fn rollback(&mut self) -> bool {
1672        let had_effects = !self.rollback_effects.is_empty();
1673        self.rollback_effects.clear();
1674        had_effects
1675    }
1676
1677    // ── Optional keyword amounts ──────────────────────────────────────────
1678
1679    /// Check if this ability has an optional keyword with a specific amount.
1680    /// Mirrors Java's `SpellAbility.hasOptionalKeywordAmount(String)`.
1681    pub fn has_optional_keyword_amount(&self, keyword: &str) -> bool {
1682        self.optional_keyword_amounts.contains_key(keyword)
1683    }
1684
1685    /// Clear all optional keyword amounts.
1686    /// Mirrors Java's `SpellAbility.clearOptionalKeywordAmount()`.
1687    pub fn clear_optional_keyword_amount(&mut self) {
1688        self.optional_keyword_amounts.clear();
1689    }
1690
1691    /// Clear last known state tracking.
1692    /// Mirrors Java's `SpellAbility.clearLastState()`.
1693    pub fn clear_last_state(&mut self) {
1694        self.last_state.clear();
1695    }
1696
1697    // ── Trigger object management ─────────────────────────────────────────
1698
1699    /// Set a triggering object in the map.
1700    /// Mirrors Java's `SpellAbility.setTriggeringObject(AbilityKey, Object)`.
1701    pub fn set_triggering_object<K: TriggerKeyInput, V: Into<AbilityValue>>(
1702        &mut self,
1703        key: K,
1704        value: V,
1705    ) {
1706        if let Some(parsed) = key.into_ability_key() {
1707            self.trigger_objects.insert(parsed, value.into());
1708        }
1709    }
1710
1711    /// Typed trigger value setter.
1712    pub fn set_triggering_value<V: Into<AbilityValue>>(&mut self, key: AbilityKey, value: V) {
1713        self.trigger_objects.insert(key, value.into());
1714    }
1715
1716    /// Set a triggering spell ability in the map.
1717    /// Mirrors Java's `SpellAbility.setTriggeringObject(AbilityKey, Object)` for SpellAbility values.
1718    pub fn set_triggering_spell_ability<K: TriggerKeyInput>(
1719        &mut self,
1720        key: K,
1721        value: SpellAbility,
1722    ) {
1723        if let Some(parsed) = key.into_ability_key() {
1724            self.trigger_spell_abilities.insert(parsed, value);
1725        }
1726    }
1727
1728    /// Get a triggering spell ability from the map.
1729    pub fn get_triggering_spell_ability<K: TriggerKeyInput>(
1730        &self,
1731        key: K,
1732    ) -> Option<&SpellAbility> {
1733        key.into_ability_key()
1734            .and_then(|parsed| self.trigger_spell_abilities.get(&parsed))
1735    }
1736
1737    /// Update an existing triggering object.
1738    /// Mirrors Java's `SpellAbility.updateTriggeringObject(String, Object)`.
1739    pub fn update_triggering_object<K: TriggerKeyInput, V: Into<AbilityValue>>(
1740        &mut self,
1741        key: K,
1742        value: V,
1743    ) {
1744        self.set_triggering_object(key, value);
1745    }
1746
1747    // ── Target management ─────────────────────────────────────────────────
1748
1749    /// Update a target in the chosen targets.
1750    /// Mirrors Java's `SpellAbility.updateTarget(Card, Card)`.
1751    pub fn update_target(&mut self, old: CardId, new: CardId) {
1752        self.target_chosen.replace_target_card(old, new);
1753    }
1754
1755    /// Whether this targets a single target only.
1756    /// Mirrors Java's `SpellAbility.targetsSingleTarget()`.
1757    pub fn targets_single_target(&self) -> bool {
1758        if let Some(ref tr) = self.target_restrictions {
1759            tr.max_targets == "1"
1760        } else {
1761            false
1762        }
1763    }
1764
1765    // ── Variable operand getters/setters ──────────────────────────────────
1766    // These mirror Java's SpellAbilityVariables Operand/ToCheck/Operator accessors.
1767    // In Rust, they are stored in the SpellAbilityVariables but accessed via SA.
1768
1769    /// Get variable operand 1.
1770    /// Mirrors Java's `SpellAbility.getSVar("Operand")`.
1771    pub fn gets_var_operand(&self) -> Option<&str> {
1772        self.condition
1773            .variables
1774            .gets_var_operand()
1775            .or_else(|| self.restriction.variables.gets_var_operand())
1776    }
1777
1778    /// Get variable operand 2.
1779    /// Mirrors Java's `SpellAbility.getSVar("Operand2")`.
1780    pub fn gets_var_operand2(&self) -> Option<&str> {
1781        self.condition
1782            .variables
1783            .gets_var_operand2()
1784            .or_else(|| self.restriction.variables.gets_var_operand2())
1785    }
1786
1787    /// Set variable operand 1.
1788    /// Mirrors Java's `SpellAbility.setSVar("Operand", val)`.
1789    pub fn sets_var_operand(&mut self, value: &str) {
1790        self.condition.variables.sets_var_operand(value);
1791        self.restriction.variables.sets_var_operand(value);
1792    }
1793
1794    /// Set variable operand 2.
1795    /// Mirrors Java's `SpellAbility.setSVar("Operand2", val)`.
1796    pub fn sets_var_operand2(&mut self, value: &str) {
1797        self.condition.variables.sets_var_operand2(value);
1798        self.restriction.variables.sets_var_operand2(value);
1799    }
1800
1801    /// Get variable to check 1.
1802    /// Mirrors Java's `SpellAbility.getSVar("VarToCheck")`.
1803    pub fn gets_var_to_check(&self) -> Option<&str> {
1804        self.condition
1805            .variables
1806            .gets_var_to_check()
1807            .or_else(|| self.restriction.variables.gets_var_to_check())
1808    }
1809
1810    /// Get variable to check 2.
1811    /// Mirrors Java's `SpellAbility.getSVar("VarToCheck2")`.
1812    pub fn gets_var_to_check2(&self) -> Option<&str> {
1813        self.condition
1814            .variables
1815            .gets_var_to_check2()
1816            .or_else(|| self.restriction.variables.gets_var_to_check2())
1817    }
1818
1819    /// Set variable to check 1.
1820    /// Mirrors Java's `SpellAbility.setSVar("VarToCheck", val)`.
1821    pub fn sets_var_to_check(&mut self, value: &str) {
1822        self.condition.variables.sets_var_to_check(value);
1823        self.restriction.variables.sets_var_to_check(value);
1824    }
1825
1826    /// Set variable to check 2.
1827    /// Mirrors Java's `SpellAbility.setSVar("VarToCheck2", val)`.
1828    pub fn sets_var_to_check2(&mut self, value: &str) {
1829        self.condition.variables.sets_var_to_check2(value);
1830        self.restriction.variables.sets_var_to_check2(value);
1831    }
1832
1833    /// Get variable operator 1.
1834    /// Mirrors Java's `SpellAbility.getSVar("Operator")`.
1835    pub fn gets_var_operator(&self) -> Option<&str> {
1836        self.condition
1837            .variables
1838            .gets_var_operator()
1839            .or_else(|| self.restriction.variables.gets_var_operator())
1840    }
1841
1842    /// Get variable operator 2.
1843    /// Mirrors Java's `SpellAbility.getSVar("Operator2")`.
1844    pub fn gets_var_operator2(&self) -> Option<&str> {
1845        self.condition
1846            .variables
1847            .gets_var_operator2()
1848            .or_else(|| self.restriction.variables.gets_var_operator2())
1849    }
1850
1851    /// Set variable operator 1.
1852    /// Mirrors Java's `SpellAbility.setSVar("Operator", val)`.
1853    pub fn sets_var_operator(&mut self, value: &str) {
1854        self.condition.variables.sets_var_operator(value);
1855        self.restriction.variables.sets_var_operator(value);
1856    }
1857
1858    /// Set variable operator 2.
1859    /// Mirrors Java's `SpellAbility.setSVar("Operator2", val)`.
1860    pub fn sets_var_operator2(&mut self, value: &str) {
1861        self.condition.variables.sets_var_operator2(value);
1862        self.restriction.variables.sets_var_operator2(value);
1863    }
1864}
1865
1866// build_spell_ability now lives in ability::ability_factory.
1867// Re-export here for backward compatibility.
1868pub use crate::ability::ability_factory::build_spell_ability;
1869pub use crate::ability::ability_factory::build_spell_ability_for_card_cast;
1870pub use crate::ability::ability_factory::build_spell_ability_from_host_card;
1871
1872/// Check whether any spell on the stack has split second.
1873/// Split second prevents players from casting spells or activating abilities
1874/// (except mana abilities) while it's on the stack.
1875/// Single source of truth — used by spell, ability, and ability_activated modules.
1876pub fn has_split_second_on_stack(game: &GameState) -> bool {
1877    for entry in game.stack.iter() {
1878        if entry.spell_ability.ir.split_second {
1879            return true;
1880        }
1881        if let Some(card_id) = entry.spell_ability.source {
1882            let card = game.card(card_id);
1883            if card.has_keyword("Split second") {
1884                return true;
1885            }
1886        }
1887    }
1888    false
1889}
1890
1891pub fn choose_targets_by_kind(
1892    agent: &mut dyn PlayerAgent,
1893    sa: &mut SpellAbility,
1894    game: &GameState,
1895    mana_pools: &[ManaPool],
1896) -> bool {
1897    use crate::card::card_util;
1898
1899    let tr = match &sa.target_restrictions {
1900        Some(tr) => tr,
1901        None => return true,
1902    };
1903
1904    let player = sa.targeting_player.unwrap_or(sa.activating_player);
1905
1906    let min_targets = tr.get_min_targets(game, sa);
1907    let max_targets = tr.get_max_targets(game, sa);
1908    if max_targets <= 0 {
1909        return true;
1910    }
1911
1912    if !matches!(tr.target_kind, TargetKind::CardInZone { .. })
1913        && !tr.has_candidates(game, player, sa.source)
1914    {
1915        return min_targets <= 0;
1916    }
1917
1918    sa.target_chosen.target_card = None;
1919    sa.target_chosen.target_card_zone_timestamp = None;
1920    sa.target_chosen.divided_map.clear();
1921
1922    match &tr.target_kind {
1923        TargetKind::None => {}
1924        TargetKind::Player => {
1925            agent.snapshot_state(game, mana_pools);
1926            let is_opponent_only = tr
1927                .valid_tgts
1928                .iter()
1929                .any(|v| v.eq_ignore_ascii_case("Opponent"));
1930            let valid_players: Vec<PlayerId> = game
1931                .alive_players()
1932                .into_iter()
1933                .filter(|&pid| !is_opponent_only || pid != player)
1934                .collect();
1935            if max_targets > 1 {
1936                let mut chosen = Vec::new();
1937                while (chosen.len() as i32) < max_targets {
1938                    let Some(pid) = agent.choose_target_player(player, &valid_players, Some(&*sa))
1939                    else {
1940                        break;
1941                    };
1942                    if !chosen.contains(&pid) {
1943                        chosen.push(pid);
1944                    }
1945                    if chosen.len() == valid_players.len() {
1946                        break;
1947                    }
1948                }
1949                sa.target_chosen.target_player = chosen.first().copied();
1950                sa.target_chosen.additional_target_players = chosen.into_iter().skip(1).collect();
1951            } else {
1952                sa.target_chosen.target_player =
1953                    agent.choose_target_player(player, &valid_players, Some(&*sa));
1954            }
1955        }
1956        TargetKind::Any => {
1957            let valid_players: Vec<PlayerId> =
1958                if target_restrictions::any_target_allows_players(&tr.valid_tgts) {
1959                    game.alive_players().into_iter().collect()
1960                } else {
1961                    Vec::new()
1962                };
1963            let valid_cards: Vec<CardId> = card_util::get_valid_cards_to_target(game, sa);
1964            agent.snapshot_state(game, mana_pools);
1965            match agent.choose_target_any(player, &valid_players, &valid_cards, Some(&*sa)) {
1966                crate::agent::TargetChoice::Player(pid) => {
1967                    sa.target_chosen.target_player = Some(pid)
1968                }
1969                crate::agent::TargetChoice::Card(cid) => {
1970                    sa.target_chosen.target_card = Some(cid);
1971                    sa.target_chosen.target_card_zone_timestamp =
1972                        Some(game.card(cid).zone_timestamp);
1973                }
1974                crate::agent::TargetChoice::None => {}
1975            }
1976        }
1977        TargetKind::Creature(_) => {
1978            let valid: Vec<CardId> = card_util::get_valid_cards_to_target(game, sa)
1979                .into_iter()
1980                .filter(|&cid| target_allowed_by_defined_controller(game, sa, cid))
1981                .collect();
1982            agent.snapshot_state(game, mana_pools);
1983            if max_targets > 1 {
1984                let chosen = agent.choose_cards_for_effect(
1985                    player,
1986                    &valid,
1987                    min_targets.max(0) as usize,
1988                    max_targets as usize,
1989                );
1990                if let Some(&first) = chosen.first() {
1991                    sa.target_chosen.target_card = Some(first);
1992                    sa.target_chosen.target_card_zone_timestamp =
1993                        Some(game.card(first).zone_timestamp);
1994                    for &extra in chosen.iter().skip(1) {
1995                        sa.target_chosen.divided_map.insert(extra, 0);
1996                    }
1997                }
1998            } else {
1999                sa.target_chosen.target_card = agent.choose_target_card(player, &valid, Some(&*sa));
2000                if let Some(cid) = sa.target_chosen.target_card {
2001                    sa.target_chosen.target_card_zone_timestamp =
2002                        Some(game.card(cid).zone_timestamp);
2003                }
2004            }
2005        }
2006        TargetKind::Permanent(_) => {
2007            let valid: Vec<CardId> = card_util::get_valid_cards_to_target(game, sa)
2008                .into_iter()
2009                .filter(|&cid| target_allowed_by_defined_controller(game, sa, cid))
2010                .collect();
2011            agent.snapshot_state(game, mana_pools);
2012            if max_targets > 1 {
2013                let chosen = agent.choose_cards_for_effect(
2014                    player,
2015                    &valid,
2016                    min_targets.max(0) as usize,
2017                    max_targets as usize,
2018                );
2019                if let Some(&first) = chosen.first() {
2020                    sa.target_chosen.target_card = Some(first);
2021                    sa.target_chosen.target_card_zone_timestamp =
2022                        Some(game.card(first).zone_timestamp);
2023                    for &extra in chosen.iter().skip(1) {
2024                        sa.target_chosen.divided_map.insert(extra, 0);
2025                    }
2026                }
2027            } else {
2028                sa.target_chosen.target_card = agent.choose_target_card(player, &valid, Some(&*sa));
2029                if let Some(cid) = sa.target_chosen.target_card {
2030                    sa.target_chosen.target_card_zone_timestamp =
2031                        Some(game.card(cid).zone_timestamp);
2032                }
2033            }
2034        }
2035        TargetKind::CardInZone { zone, .. } => {
2036            let valid: Vec<CardId> = card_util::get_valid_cards_to_target(game, sa)
2037                .into_iter()
2038                .filter(|&cid| target_allowed_by_defined_controller(game, sa, cid))
2039                .collect();
2040            if valid.is_empty() {
2041                return min_targets <= 0;
2042            }
2043            agent.snapshot_state(game, mana_pools);
2044            if max_targets > 1 {
2045                let chosen = agent.choose_cards_for_effect(
2046                    player,
2047                    &valid,
2048                    min_targets.max(0) as usize,
2049                    max_targets as usize,
2050                );
2051                if let Some(&first) = chosen.first() {
2052                    sa.target_chosen.target_card = Some(first);
2053                    sa.target_chosen.target_card_zone_timestamp =
2054                        Some(game.card(first).zone_timestamp);
2055                    for &extra in chosen.iter().skip(1) {
2056                        sa.target_chosen.divided_map.insert(extra, 0);
2057                    }
2058                }
2059            } else {
2060                sa.target_chosen.target_card =
2061                    agent.choose_target_card_from_zone(player, *zone, &valid, Some(&*sa));
2062                if let Some(cid) = sa.target_chosen.target_card {
2063                    sa.target_chosen.target_card_zone_timestamp =
2064                        Some(game.card(cid).zone_timestamp);
2065                }
2066            }
2067        }
2068        TargetKind::Spell => {
2069            let valid = target_restrictions::get_all_candidates_spells(game);
2070            let valid = if let Some(ref restrictions) = sa.target_restrictions {
2071                target_restrictions::filter_spells_for_target_restrictions(
2072                    game,
2073                    &valid,
2074                    restrictions,
2075                )
2076            } else {
2077                valid
2078            };
2079            agent.snapshot_state(game, mana_pools);
2080            sa.target_chosen.target_stack_entry =
2081                agent.choose_target_spell(player, &valid, sa.source);
2082        }
2083    }
2084
2085    let chosen_targets = sa.target_chosen.all_target_cards().len() as i32
2086        + sa.target_chosen.all_target_players().len() as i32
2087        + i32::from(sa.target_chosen.target_stack_entry.is_some());
2088    chosen_targets >= min_targets
2089}
2090
2091fn target_allowed_by_defined_controller(
2092    game: &GameState,
2093    sa: &SpellAbility,
2094    card_id: CardId,
2095) -> bool {
2096    let Some(defined) = sa.ir.targets_with_defined_controller_text.as_deref() else {
2097        return true;
2098    };
2099    let players = crate::ability::ability_utils::resolve_defined_players_with_sa(
2100        defined,
2101        sa,
2102        sa.activating_player,
2103        game,
2104    );
2105    players.is_empty() || players.contains(&game.card(card_id).controller)
2106}
2107
2108fn choose_targeting_player(
2109    sa: &SpellAbility,
2110    game: &GameState,
2111    agents: &mut [Box<dyn PlayerAgent>],
2112) -> Option<PlayerId> {
2113    if let Some(defined) = sa.ir.targeting_player_text.as_deref() {
2114        let candidates = crate::ability::ability_utils::resolve_defined_players_with_sa(
2115            defined,
2116            sa,
2117            sa.activating_player,
2118            game,
2119        );
2120        if candidates.is_empty() {
2121            return None;
2122        }
2123        return agents[sa.activating_player.index()].choose_target_player(
2124            sa.activating_player,
2125            &candidates,
2126            None,
2127        );
2128    }
2129    Some(sa.activating_player)
2130}
2131
2132// Re-export MagicStack and StackEntry from zone module (their canonical home,
2133// matching Java's `forge.game.zone.MagicStack`).
2134pub use crate::zone::magic_stack::{MagicStack, StackEntry};