Skip to main content

manabrew_engine/card/
mod.rs

1pub mod activation_table;
2mod alt_costs;
3mod card_assembly;
4pub mod card_changed_words;
5pub mod card_clone_states;
6pub mod card_collection;
7pub mod card_collection_view;
8pub mod card_copy_service;
9pub mod card_damage_history;
10pub mod card_damage_map;
11pub mod card_factory;
12pub mod card_factory_util;
13pub mod card_lists;
14pub mod card_play_option;
15pub mod card_predicates;
16pub mod card_property;
17pub mod card_state;
18pub mod card_trait_changes;
19pub mod card_util;
20pub mod card_zone_table;
21pub mod counter_enum_type;
22pub mod counter_keyword_type;
23pub mod counter_type;
24pub mod damage_history;
25pub mod filter_constants;
26mod keyword_gen;
27pub mod perpetual;
28pub mod svar_cache;
29pub mod token;
30pub mod token_create_table;
31pub mod trait_card_trait_changes;
32pub mod valid_filter;
33use crate::card::activation_table::ActivationTable;
34use crate::core::HasSVars;
35pub use counter_type::CounterType;
36
37/// Type alias for the Keyword enum, used by keyword helper methods.
38use crate::keyword::keyword_instance::Keyword as Kw;
39
40// ── Keyword marker constants ──────────────────────────────────────────
41// These are synthetic keywords injected at runtime to track card state.
42// Using constants avoids magic strings scattered across the codebase.
43
44/// Prefix for the Plotted marker. Full keyword is `"Plotted:{turn}"`.
45/// The turn number prevents casting on the same turn the card was plotted.
46pub const KEYWORD_PLOTTED_PREFIX: &str = "Plotted:";
47
48/// Marker for cards exiled via Warp's end-of-turn trigger.
49/// These cards can be cast from exile on a later turn for their normal mana cost.
50pub const KEYWORD_WARP_EXILED: &str = "WarpExiled";
51
52use std::collections::{BTreeMap, HashMap, HashSet};
53
54use forge_carddb::CardRules;
55use forge_foundation::{CardTypeLine, ColorSet, CoreType, ManaCost, ZoneType};
56use serde::{Deserialize, Serialize};
57
58use crate::ability::activated::{parse_activated_ability, ActivatedAbility};
59use crate::card::perpetual::perpetual_record::PerpetualRecord;
60use crate::card::svar_cache::{ParsedSVar, ParsedSVarCache};
61use crate::cost::{parse_cost, Cost};
62use crate::game::GameState;
63use crate::ids::{CardId, PlayerId};
64use crate::parsing::{keys, parse_or_warn, Params, ParsedParams};
65use crate::replacement::{parse_replacement_effect, ReplacementEffect};
66use crate::spellability::{SpellAbility, TargetRestrictions};
67use crate::staticability::{parse_static_ability, StaticAbility};
68use crate::trigger::Trigger;
69
70/// Build the full `"Plotted:{turn}"` keyword string.
71fn colorless_color_set() -> ColorSet {
72    ColorSet::COLORLESS
73}
74
75fn parse_literal_target_count(expr: &str) -> Option<i32> {
76    if let Ok(n) = expr.trim().parse::<i32>() {
77        return Some(n);
78    }
79    expr.trim().strip_prefix('+')?.parse::<i32>().ok()
80}
81
82pub fn make_plotted_keyword(turn: u32) -> String {
83    format!("{KEYWORD_PLOTTED_PREFIX}{turn}")
84}
85
86/// Extract the turn number from a `"Plotted:{turn}"` keyword, if present.
87pub fn parse_plotted_turn(kw: &str) -> Option<u32> {
88    kw.strip_prefix(KEYWORD_PLOTTED_PREFIX)
89        .and_then(|s| s.parse().ok())
90}
91
92/// Stores alternate-face characteristics for double-faced cards (DFCs).
93/// The `transform()` method swaps `Card` fields with these values.
94/// Mirrors Java's `CardState` stored as the "backside" state.
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct CardOtherPart {
97    pub name: String,
98    #[serde(default)]
99    pub oracle_text: String,
100    /// True when the card's split type is `Modal` (MDFC). Only a modal back face
101    /// may be played from hand; transform/meld backs may not. Mirrors
102    /// `Card.isModal()` (`getRules().getSplitType() == CardSplitType.Modal`).
103    /// Invariant across `transform()`, so it is not swapped there.
104    #[serde(default)]
105    pub is_modal: bool,
106    pub type_line: CardTypeLine,
107    pub mana_cost: ManaCost,
108    pub color: ColorSet,
109    pub base_power: Option<i32>,
110    pub base_toughness: Option<i32>,
111    pub keywords: crate::keyword::keyword_collection::KeywordCollection,
112    pub abilities: Vec<String>,
113    pub triggers: Vec<Trigger>,
114    pub static_abilities: Vec<crate::staticability::StaticAbility>,
115    pub replacement_effects: Vec<crate::replacement::ReplacementEffect>,
116    pub svars: BTreeMap<String, String>,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize, Default)]
120pub struct CardActionSpellSpec {
121    pub ability_index: usize,
122    pub has_valid_tgts: bool,
123    pub cost_contains_x: bool,
124    #[serde(default)]
125    pub target_chain: Vec<CardActionTargetSpec>,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct CardActionTargetSpec {
130    pub target_restrictions: TargetRestrictions,
131    pub min_targets: Option<i32>,
132}
133
134/// When a `SP$ GainControl` steals a permanent, the revert trigger is stored
135/// here. Fires during the appropriate phase or event handler, at which point
136/// the card's `original_controller_eot` is restored.
137///
138/// Mirrors the subset of Java `ControlGainEffect.LoseControl$` variants that
139/// schedule a `GameCommand`. Java also has variants we intentionally skip
140/// here (`StaticCommandCheck` driven by an SVar comparator, `UntilSourceUnattached`,
141/// `UntilTheEndOfYourNextTurn`) — they require either a scheduler that scans
142/// every tick or a turn-owner counter that the engine doesn't maintain yet.
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum_macros::EnumString)]
144#[strum(ascii_case_insensitive)]
145pub enum LoseControlCondition {
146    /// Revert at the end of the current turn (default EOT branch).
147    #[strum(serialize = "EOT", serialize = "UntilEOT", serialize = "EndOfTurn")]
148    EndOfTurn,
149    /// Revert the next time this card untaps.
150    #[strum(serialize = "Untap", serialize = "UntilUntap", serialize = "NextUntap")]
151    NextUntap,
152    /// Revert at end of combat (Threaten-style steal-and-swing).
153    EndOfCombat,
154    /// Revert when the card leaves the battlefield.
155    LeavesPlay,
156}
157
158/// Saved pre-animate state for AnimateEffect, restored at cleanup.
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct AnimateState {
161    pub original_type_line: CardTypeLine,
162    pub original_base_power: Option<i32>,
163    pub original_base_toughness: Option<i32>,
164    pub original_color: ColorSet,
165    /// Snapshot of intrinsic keywords before animate added any. Restored
166    /// when the card leaves the battlefield (CR 400.7) so granted keywords
167    /// (e.g. Animate `Keywords$ Haste`) do not persist into the new object.
168    #[serde(default)]
169    pub original_keywords: Option<crate::keyword::keyword_collection::KeywordCollection>,
170}
171
172/// Saved pre-clone copiable characteristics.
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct CloneState {
175    #[serde(default)]
176    pub expires_at_cleanup: bool,
177    pub original_card_name: String,
178    #[serde(default)]
179    pub original_oracle_text: String,
180    pub original_type_line: CardTypeLine,
181    pub original_mana_cost: ManaCost,
182    pub original_color: ColorSet,
183    pub original_base_power: Option<i32>,
184    pub original_base_toughness: Option<i32>,
185    pub original_keywords: crate::keyword::keyword_collection::KeywordCollection,
186    pub original_abilities: Vec<String>,
187    pub original_activated_abilities: Vec<ActivatedAbility>,
188    pub original_triggers: Vec<Trigger>,
189    pub original_svars: BTreeMap<String, String>,
190    pub original_static_abilities: Vec<StaticAbility>,
191    pub original_replacement_effects: Vec<ReplacementEffect>,
192    /// Intrinsic ability count *before* the clone overwrote it. The static
193    /// layer truncates `activated_abilities` to `base_ability_count` each
194    /// pass, so reverting `activated_abilities` without also restoring this
195    /// would let the layer trim the recovered abilities right back to the
196    /// clone's count.
197    #[serde(default)]
198    pub original_base_ability_count: usize,
199    /// Intrinsic trigger count *before* the clone, for the same reason.
200    #[serde(default)]
201    pub original_base_trigger_count: usize,
202}
203
204/// A card instance in a game. This is the mutable game-state representation,
205/// as opposed to CardRules which is the immutable definition.
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct Card {
208    pub id: CardId,
209    /// The card's active face name (front face for single/split cards, active face for DFC).
210    /// On the battlefield, this is the name that's displayed.
211    pub card_name: String,
212    /// The full combined name for split/room cards (e.g. "Walk-In Closet // Forgotten Cellar").
213    /// For non-split cards, this equals `card_name`. Used for hand/graveyard display and
214    /// database lookups.
215    pub full_name: String,
216    #[serde(default)]
217    pub oracle_text: String,
218
219    // Ownership and control
220    pub owner: PlayerId,
221    pub controller: PlayerId,
222
223    // Current zone
224    pub zone: ZoneType,
225
226    // Type line (can be modified by effects)
227    pub type_line: CardTypeLine,
228
229    // Mana cost (can be modified)
230    pub mana_cost: ManaCost,
231
232    // Color (can be modified)
233    pub color: ColorSet,
234
235    /// Immutable color identity from the card's rules (CR 903.4): mana cost
236    /// colors plus any mana symbols found in the oracle text (outside reminder
237    /// text). Used for commander color-identity checks and Combo ColorIdentity
238    /// mana productions. Mirrors Java `CardRules.getColorIdentity()`.
239    #[serde(default = "colorless_color_set")]
240    pub color_identity: ColorSet,
241
242    // Power/Toughness (base values, can be modified)
243    pub base_power: Option<i32>,
244    pub base_toughness: Option<i32>,
245    /// Printed starting loyalty for planeswalkers.
246    pub initial_loyalty: Option<String>,
247    /// Temporary P/T modifications from spells/abilities resolving this turn
248    /// (e.g. Giant Growth).  Reset when leaving the battlefield.
249    pub power_modifier: i32,
250    pub toughness_modifier: i32,
251    /// Perpetual P/T modifications — persist across zone changes (never reset).
252    /// Applied by `PumpAll` / `Pump` effects with `Duration$ Perpetual`.
253    pub perpetual_power_modifier: i32,
254    pub perpetual_toughness_modifier: i32,
255    /// Java-parity storage of all perpetual effect records applied to this card.
256    #[serde(default)]
257    pub perpetual: Vec<PerpetualRecord>,
258    /// Layer 7b override: set by `SetPower$` / `SetToughness$` continuous effects.
259    /// `None` means use `base_power` / `base_toughness` as normal.
260    /// Reset to `None` each time [`layer::apply_continuous_effects`] runs.
261    pub static_set_power: Option<i32>,
262    pub static_set_toughness: Option<i32>,
263    /// Layer 7c bonus: accumulated from `AddPower$` / `AddToughness$` anthems.
264    /// Reset to 0 each time [`layer::apply_continuous_effects`] runs.
265    pub static_power_modifier: i32,
266    pub static_toughness_modifier: i32,
267
268    // Combat/state
269    pub tapped: bool,
270    /// Mana atoms produced the last time this land was tapped for mana.
271    /// Used for mana rollback — when untapping, remove exactly this mana from pool.
272    /// Covers base production + aura triggers + static doublers + any other source.
273    #[serde(skip)]
274    pub last_mana_produced: Option<Vec<u16>>,
275    pub flipped: bool,
276    pub face_down: bool,
277    /// True if this card has Morph or Megamorph and can be cast face-down for {3}.
278    pub has_morph: bool,
279    /// True if this card was discarded (CR 400.7k, for TrackDiscarded$ effects).
280    pub discarded: bool,
281    /// True if this card was unearthed (should be exiled at EOT or if leaving battlefield).
282    pub unearthed: bool,
283    /// Class enchantment level (1 = base, 2+ = leveled up).
284    pub class_level: i32,
285    /// Soulbond: paired creature (if any).
286    pub paired_with: Option<CardId>,
287    /// True if this card was manifested (face-down as 2/2 creature).
288    pub manifested: bool,
289    /// True if this card was cloaked (face-down with ward {2}).
290    pub cloaked: bool,
291    /// True if this card was foretold (exiled face-down via Foretell).
292    pub foretold: bool,
293    /// Other card(s) melded/merged with this one. When this card changes zones,
294    /// all melded parts move together (CR 712.4).
295    pub melded_with: Vec<CardId>,
296    /// True if foretold cost was set by an effect (not the card's own Foretell ability).
297    pub foretold_cost_by_effect: bool,
298    /// True if this card is currently bestowed (attached as an Aura via Bestow).
299    pub is_bestowed: bool,
300    pub summoning_sick: bool,
301    #[serde(default)]
302    pub came_under_control_since_last_upkeep: bool,
303    pub exerted: bool,
304    pub damage: i32,
305    /// Zone the card was cast from (mirrors Java `Card.castFrom`). `Some` only
306    /// while the card represents a spell that was actually cast — set during
307    /// cast resolution, cleared on every zone change so the next "object" the
308    /// card becomes (CR 400.7) starts with no cast history. Used by
309    /// `wasCast`/`wasCastByYou` valid filters (e.g. Sunderflock's ETB).
310    pub cast_from: Option<ZoneType>,
311
312    // Counters
313    pub counters: BTreeMap<CounterType, i32>,
314
315    // Keywords intrinsic to this card (from its card definition).
316    // Now stored as a `KeywordCollection` for structured typed lookups.
317    pub keywords: crate::keyword::keyword_collection::KeywordCollection,
318    /// Keywords granted by continuous static effects (Layer 6).
319    /// Reset and recomputed each time [`layer::apply_continuous_effects`] runs.
320    pub granted_keywords: crate::keyword::keyword_collection::KeywordCollection,
321    /// SVars supplied by granted text (e.g. AddTrigger$/AddAbility$).
322    /// Reset and recomputed each time [`layer::apply_continuous_effects`] runs.
323    #[serde(default)]
324    pub granted_svars: BTreeMap<String, String>,
325    /// Type tokens added by continuous static effects (Layer 4, `AddType$`).
326    /// Reset and recomputed each time [`layer::apply_continuous_effects`] runs.
327    /// The listed strings may be supertypes, core card types, or subtypes;
328    /// keeping a separate list lets us revert on reset without losing the
329    /// card's intrinsic type line.
330    pub static_added_subtypes: Vec<String>,
331    #[serde(skip)]
332    pub static_type_line_base: Option<CardTypeLine>,
333    #[serde(skip)]
334    pub changed_type_line_base: Option<CardTypeLine>,
335    #[serde(skip)]
336    pub changed_base_power: Option<Option<i32>>,
337    #[serde(skip)]
338    pub changed_base_toughness: Option<Option<i32>>,
339    /// Keywords granted temporarily by pump effects (`KW$` parameter) until end of turn.
340    /// Cleared during step_cleanup alongside power_modifier / toughness_modifier.
341    pub pump_keywords: crate::keyword::keyword_collection::KeywordCollection,
342    /// Number of triggers added temporarily by `DB$ Animate | Triggers$` effects.
343    /// At cleanup, this many triggers are popped from the end of the `triggers` vec.
344    pub pump_trigger_count: usize,
345
346    // Abilities (raw strings from card definition)
347    pub abilities: Vec<String>,
348    /// Prebound SP$ ability metadata used by action-space filters.
349    #[serde(default)]
350    pub action_spell_specs: Vec<CardActionSpellSpec>,
351    /// Prebound first SP$ Cost used by action-space non-mana cost checks.
352    #[serde(default)]
353    pub action_spell_cost: Option<Cost>,
354    /// Prebound AIPhyrexianPayment$ policy from printed ability text.
355    #[serde(default)]
356    pub ai_phyrexian_payment: Option<String>,
357    /// Prebound minimum Spree mode cost from Choices$ -> SVar ModeCost$.
358    #[serde(default)]
359    pub spree_min_mode_cost: Option<i32>,
360
361    // Parsed activated abilities (from AB$ lines in abilities)
362    pub activated_abilities: Vec<ActivatedAbility>,
363    /// Number of base activated abilities (before continuous effects add more via AddAbility$).
364    /// Used by `apply_continuous_effects` to truncate granted abilities on reset.
365    pub base_ability_count: usize,
366    /// Number of base triggers (before continuous effects add more via AddTrigger$).
367    /// Used by `apply_continuous_effects` to truncate granted triggers on reset.
368    pub base_trigger_count: usize,
369    /// Applied card-trait mutation layers keyed by (timestamp, static_id).
370    /// Mirrors Java `changedCardTraits` table.
371    pub changed_card_traits:
372        std::collections::BTreeMap<(i64, i64), card_trait_changes::CardTraitChanges>,
373    /// Text-layer trait changes keyed by (timestamp, static_id).
374    /// Mirrors Java `changedCardTraitsByText` table.
375    pub changed_card_traits_by_text:
376        std::collections::BTreeMap<(i64, i64), card_trait_changes::CardTraitChanges>,
377
378    /// Parsed static abilities (from S$ lines in abilities).
379    /// Mirrors Java Forge `Card.getStaticAbilities()`.
380    pub static_abilities: Vec<StaticAbility>,
381
382    // Combat tracking
383    pub has_deathtouch_damage: bool,
384    /// Set by `Mode$ CantAttack` static effects. Reset each time
385    /// [`layer::apply_continuous_effects`] runs.
386    pub cant_attack_static: bool,
387    /// Set by `Mode$ CantBlock` static effects. Reset each time
388    /// [`layer::apply_continuous_effects`] runs.
389    pub cant_block_static: bool,
390
391    // Turn tracking
392    #[serde(default)]
393    pub turn_in_zone: u32,
394    pub entered_battlefield_this_turn: bool,
395    pub attacked_this_turn: bool,
396    /// Snapshot of whether this permanent was tapped at the start of its
397    /// controller's current turn (before untap step).
398    pub started_turn_tapped: bool,
399
400    // Triggers — mirrors Java Card.getTriggers()
401    pub triggers: Vec<Trigger>,
402    // SVars — mirrors Java Card.getSVars()
403    pub svars: BTreeMap<String, String>,
404    #[serde(skip, default)]
405    pub parsed_svar_cache: ParsedSVarCache,
406
407    // Commander tracking
408    /// True if this card is designated as a commander.
409    pub is_commander: bool,
410    /// True if this commander entered graveyard or exile since the last SBA check
411    /// and may still be moved to the command zone.
412    pub move_to_command_zone: bool,
413    /// How many times this commander has been cast from the command zone (for tax).
414    pub commander_cast_count: u32,
415
416    /// True if this permanent is a token or a copy-token (ceases to exist on zone change).
417    pub is_token: bool,
418
419    /// Set when the card is cast from graveyard via Flashback. Used by the
420    /// flashback replacement effect to exile the card when it leaves the stack.
421    pub cast_with_flashback: bool,
422    /// Set when the card is cast from graveyard via Harmonize. Used by the
423    /// Harmonize replacement effect to exile the card when it leaves the stack.
424    pub cast_with_harmonize: bool,
425
426    // Replacement effects — parsed from R$ lines in card abilities.
427    // Mirrors Java `Card.getReplacementEffects()`.
428    pub replacement_effects: Vec<ReplacementEffect>,
429
430    // Attachment tracking (Auras / Equipment).
431    // Mirrors Java `Card.getAttachedTo()` / `Card.getAttachedCards()`.
432    /// The permanent this card is currently attached to (for Auras/Equipment).
433    pub attached_to: Option<CardId>,
434    pub attached_to_player: Option<PlayerId>,
435    /// Whether this equipment was attached/moved this turn (AI memory to prevent ping-ponging).
436    /// Cleared at start of each turn. Mirrors Java `AiCardMemory.MemorySet.ATTACHED_THIS_TURN`.
437    pub attached_this_turn: bool,
438    /// Cards currently attached to this permanent (inverse of `attached_to`).
439    pub attachments: Vec<CardId>,
440
441    // Memory for "Remember" and "Imprint" parameters
442    /// Cards remembered by this card (for RememberCountered, etc.)
443    pub remembered_cards: Vec<CardId>,
444    /// Players remembered by this card (for Player.IsRemembered checks).
445    pub remembered_players: Vec<PlayerId>,
446    /// Cards imprinted on this card (for Imprint mechanic, e.g. Chrome Mox).
447    pub imprinted_cards: Vec<CardId>,
448    /// Cards associated via gain-control effects.
449    pub gain_control_targets: Vec<CardId>,
450    /// Cards linked by "until leaves battlefield" tracking.
451    pub until_leaves_battlefield: Vec<CardId>,
452    /// Cards exiled by this card/effect.
453    pub exiled_cards: Vec<CardId>,
454    /// Cards exiled specifically to pay this card's current activation/cast cost.
455    /// This is reset at the start of each cost payment attempt.
456    pub paid_cost_exiled_cards: Vec<CardId>,
457    /// Cards haunting this card.
458    pub haunted_by: Vec<CardId>,
459    /// Card currently haunted by this card.
460    pub haunting: Option<CardId>,
461    /// Per-player chosen card map.
462    pub chosen_map: HashMap<PlayerId, Vec<CardId>>,
463    /// CMC values remembered by this card
464    pub remembered_cmc: Vec<i32>,
465    /// Source card that created this effect card (for Card.EffectSource checks).
466    pub effect_source: Option<CardId>,
467    #[serde(default)]
468    pub clone_origin: Option<CardId>,
469    #[serde(default)]
470    pub copied_permanent: Option<CardId>,
471    /// The spell ability used to cast this card instance onto the stack.
472    /// Mirrors Java `Card.getCastSA()`. Populated when the card hits the stack,
473    /// cleared when it leaves the battlefield.
474    #[serde(skip, default)]
475    pub cast_sa: Option<Box<SpellAbility>>,
476    /// For `SP$ Charm`: last turn each mode (keyed by its SVar name) was chosen
477    /// on this card instance. Feeds `ChoiceRestriction$` filtering.
478    /// Mirrors the per-card mode history Java keeps on `Card`.
479    #[serde(default)]
480    pub chosen_charm_modes: HashMap<String, i32>,
481    /// LKI (last-known-information) snapshots of cards remembered by this
482    /// card via `RememberLKI$`. Each entry is a frozen copy taken at remember
483    /// time via `CardCopyService::get_lki_copy`. Callers that care about
484    /// "what was this creature when it died" query this list instead of
485    /// `remembered_cards` (which stores live IDs and drifts).
486    #[serde(skip, default)]
487    pub remembered_lki_cards: Vec<Card>,
488    /// When set, the card's `original_controller_eot` must be restored on the
489    /// trigger described here. Mirrors the Java `ControlGainEffect` set of
490    /// `LoseControl$` variants that register distinct GameCommands.
491    #[serde(default)]
492    pub lose_control_condition: Option<LoseControlCondition>,
493    /// True if this temporary effect expires at end of turn cleanup.
494    pub temp_effect_until_eot: bool,
495    /// Host card this temporary effect is linked to; when host leaves the
496    /// battlefield, this effect expires.
497    pub temp_effect_host: Option<CardId>,
498    /// Forget remembered cards when they move from this origin zone.
499    pub forget_on_moved_origin: Option<ZoneType>,
500    /// Exile this effect when remembered cards become empty after forget logic.
501    pub exile_when_no_remembered: bool,
502    /// When this card is in exile, the card that caused it to be exiled here.
503    /// Used for `Duration$ UntilHostLeavesPlay` effects (e.g. Deputy of Detention):
504    /// when `exiled_by` leaves the battlefield, this card returns to its owner's battlefield.
505    pub exiled_by: Option<CardId>,
506
507    /// Original controller to restore at end of turn (for `LoseControl$ EOT`).
508    pub original_controller_eot: Option<PlayerId>,
509
510    // Double-faced card (DFC) state
511    /// True if this card is currently showing its back face.
512    pub is_transformed: bool,
513    /// Back-face characteristics for DFC cards. `None` for single-faced cards.
514    pub other_part: Option<CardOtherPart>,
515
516    /// Optional set code (e.g., "M21") for specific printings.
517    pub set_code: Option<String>,
518
519    /// Optional collector number within a set (e.g., "1", "42").
520    /// For tokens, this is the token's collector number in the token set
521    /// (e.g., collector "1" in set "THOU" for Adorned Pouncer token).
522    pub card_number: Option<String>,
523
524    #[serde(default)]
525    pub paper_foil: bool,
526
527    // Phase-out state (issue #22, Phases effect).
528    pub phased_out: bool,
529
530    // Regeneration shields (issue #22, Regenerate effect).
531    // Decremented instead of destroying; resets at end of turn.
532    pub regeneration_shields: i32,
533
534    /// Whether this permanent was kicked when cast.
535    /// Mirrors Java `Card.isKicked()`. Stored on the card so triggers
536    /// with `ValidCard$ Card.Self+kicked` can check it after resolution.
537    pub kicked: bool,
538    /// Whether this permanent has become monstrous.
539    /// Mirrors Java `Card.isMonstrous()`. Resets when the permanent changes zones.
540    pub monstrous: bool,
541
542    /// Colors chosen by ChooseColorEffect (stored for later reference by other effects).
543    pub chosen_colors: Vec<String>,
544    /// Cards chosen by ChooseCardEffect (stored for later reference by other effects).
545    pub chosen_cards: Vec<CardId>,
546
547    /// Saved state for AnimateEffect — restored during step_cleanup.
548    pub animate_state: Option<AnimateState>,
549    /// Saved state for temporary Clone effects — restored during step_cleanup.
550    pub clone_state: Option<CloneState>,
551
552    // ── Issue #53: High-priority effect fields ──────────────────────────
553    /// Type chosen by ChooseType effect (e.g. "Goblin", "Artifact").
554    pub chosen_type: Option<String>,
555    /// Secondary chosen type used by a subset of cards (e.g. Illusionary Terrain).
556    pub chosen_type2: Option<String>,
557    /// Noted types tracked by effects that accumulate type names.
558    pub noted_types: Vec<String>,
559    /// Card names chosen by NameCard effect.
560    pub named_cards: Vec<String>,
561    /// Number chosen by ChooseNumber effect.
562    pub chosen_number: Option<i32>,
563    /// Player chosen by ChoosePlayer effect.
564    pub chosen_player: Option<PlayerId>,
565    /// Controller who made the chosen-player choice.
566    pub chosen_player_controller: Option<PlayerId>,
567    /// Controller who made the chosen-type choice.
568    pub chosen_type_controller: Option<PlayerId>,
569    /// Whether the chosen player has been revealed.
570    pub chosen_player_revealed: bool,
571    /// Whether the chosen type has been revealed.
572    pub chosen_type_revealed: bool,
573    /// Opponent chosen for PromiseGift cost.
574    pub promised_gift: Option<PlayerId>,
575    /// Attraction lights printed on the card face.
576    pub attraction_lights: Vec<u32>,
577    /// Attraction sector assignment.
578    pub sector: Option<String>,
579    /// Chosen sector before assignment effects resolve.
580    pub chosen_sector: Option<String>,
581    /// Contraption sprocket assignment.
582    pub sprocket: i32,
583    /// Chosen even/odd marker.
584    pub chosen_even_odd: Option<String>,
585    /// True if detained — can't attack, block, or activate abilities. Clears at controller's next turn.
586    pub detained: bool,
587    /// Set during combat to the player this creature is attacking; None if not attacking.
588    pub attacking_player: Option<PlayerId>,
589    /// Player who goaded this creature. Goaded creature must attack but can't attack goader.
590    pub goaded_by: Option<PlayerId>,
591    /// Damage prevention shields (decremented when damage would be dealt). Resets at EOT.
592    pub damage_prevention: i32,
593    /// Damage assigned in current combat assignment step.
594    pub assigned_damage: i32,
595    /// True if this creature must block if able.
596    pub must_block: bool,
597    /// Spell cards encoded/ciphered onto this creature.
598    pub encoded_cards: Vec<CardId>,
599    /// Cards that dealt damage to this creature this turn (for DamagedBy trigger filters).
600    /// Mirrors Java `CardDamageHistory.getDamageReceivedThisTurn()`.
601    pub damage_sources_this_turn: Vec<CardId>,
602    /// Total damage dealt by this card this turn (for Count$TotalDamageDoneByThisTurn).
603    /// Mirrors Java `Card.getTotalDamageDoneBy()` via `DamageHistory.getDamageDoneThisTurn()`.
604    /// Reset each turn in `new_turn()`.
605    pub total_damage_done_this_turn: i32,
606    /// Last-known information: power when this card last left the battlefield.
607    /// Mirrors Java's LKI system for `TriggeredCard$CardPower`.
608    /// `None` means LKI was never captured; `Some(0)` means power was 0.
609    pub lki_power: Option<i32>,
610    /// Last-known information: toughness when this card last left the battlefield.
611    /// `None` means LKI was never captured; `Some(0)` means toughness was 0.
612    pub lki_toughness: Option<i32>,
613    /// Last-known information: counters when this card last left the battlefield.
614    /// Used by `TriggeredCard$CardCounters.TYPE` (e.g. Servant of the Scale death trigger).
615    pub lki_counters: Option<std::collections::BTreeMap<CounterType, i32>>,
616    /// Damage history tracking (attacks, blocks, damage dealt).
617    /// Mirrors Java `CardDamageHistory`.
618    #[serde(skip)]
619    pub damage_history: damage_history::DamageHistory,
620    /// Specific cards this creature must block (set by effects like Lure variants).
621    pub must_block_cards: Vec<CardId>,
622    /// +1/+1 counters to add on ETB (from mana that adds counters, e.g. Guildmages' Forum).
623    pub etb_counters_p1p1: i32,
624    /// Bitmask of colors of mana spent to cast this spell (for Sunburst/Converge).
625    /// Uses ManaAtom bit flags (W=1, U=2, B=4, R=8, G=16).
626    pub colors_spent_to_cast: u16,
627    /// Exact mana atoms spent to cast this spell, in payment order.
628    /// Mirrors Java's castSA.getPayingMana() use sites such as Adamant.
629    pub paying_mana_to_cast: Vec<u16>,
630    /// Pre-selected charm/mode indices (for Spree — modes chosen before payment).
631    /// If `Some`, charm_effect should use these instead of asking the player again.
632    pub chosen_modes: Option<Vec<usize>>,
633    /// Number of extra targets paid for via Strive (0 = no extra targets).
634    pub strive_extra_targets: u32,
635    /// Tracks if this card became a target this turn.
636    pub became_target_this_turn: bool,
637    /// Temporary controllers layered on this card.
638    pub temp_controllers: Vec<PlayerId>,
639    /// Players that may look at this card.
640    pub may_look_at: Vec<PlayerId>,
641    /// Players that may play this card.
642    pub may_play: Vec<PlayerId>,
643    /// Additional blockers this creature can declare.
644    pub can_block_additional: i32,
645    /// Whether this creature can block any number of creatures.
646    pub can_block_any: bool,
647    /// Keywords this card is prevented from having.
648    pub cant_have_keywords: HashSet<String>,
649    /// Intensity marker value.
650    pub intensity: i32,
651    /// Card was surveilled this turn.
652    pub surveilled: bool,
653    /// Card was milled this turn.
654    pub milled: bool,
655    /// Attraction visited this turn.
656    pub visited_this_turn: bool,
657    /// Number of times this permanent has crewed this turn.
658    pub times_crewed_this_turn: u32,
659    /// Whether this permanent is currently crewed.
660    pub is_crewed: bool,
661    /// Whether this card should ignore legend rule checks.
662    pub ignore_legend_rule_flag: bool,
663    /// Ability activation counts this turn.
664    pub ability_activated_this_turn: u32,
665    /// Ability resolution counts this turn.
666    pub ability_resolved_this_turn: u32,
667    /// Java parity: per-ability activation tracking this turn.
668    #[serde(skip)]
669    pub number_turn_activations: ActivationTable,
670    /// Java parity: per-ability activation tracking this game.
671    #[serde(skip)]
672    pub number_game_activations: ActivationTable,
673    /// Java parity: per-ability resolution tracking this turn.
674    #[serde(skip)]
675    pub number_ability_resolved: ActivationTable,
676    /// Planeswalker activation count this turn.
677    pub planeswalker_abilities_activated: u32,
678    /// Whether a static effect's increased planeswalker activation limit was used this turn.
679    pub planeswalker_activation_limit_used: bool,
680    /// Chosen mode count tracking turn marker.
681    pub chosen_modes_turn: Option<u32>,
682    /// Set when this creature enlisted another creature in the current combat.
683    pub enlisted_this_combat: bool,
684    /// Per-ability activation count this game (for PowerUp once-per-game restriction).
685    pub activations_this_game: std::collections::BTreeMap<usize, u32>,
686    /// True once Renown has triggered (creature dealt combat damage to a player).
687    /// Mirrors Java `Card.isRenowned()`.
688    pub is_renowned: bool,
689    /// Monotonically increasing timestamp set each time the card enters a zone.
690    /// Used to order same-player triggers by zone entry order, matching
691    /// Java's `Zone.cardList` insertion order used by `forEachCardInGame`.
692    pub zone_timestamp: u64,
693
694    /// Baseline snapshots used to recompute live lists when trait-change layers
695    /// are removed/cleared.
696    #[serde(skip)]
697    trait_base_activated_abilities: Option<Vec<ActivatedAbility>>,
698    #[serde(skip)]
699    trait_base_triggers: Option<Vec<Trigger>>,
700    #[serde(skip)]
701    trait_base_replacement_effects: Option<Vec<ReplacementEffect>>,
702    #[serde(skip)]
703    trait_base_static_abilities: Option<Vec<StaticAbility>>,
704    #[serde(skip)]
705    trait_base_keywords: Option<crate::keyword::keyword_collection::KeywordCollection>,
706}
707
708/// Transitional alias for downstream code still importing `CardInstance`.
709pub type CardInstance = Card;
710
711impl Card {
712    pub fn new(
713        id: CardId,
714        card_name: String,
715        owner: PlayerId,
716        type_line: CardTypeLine,
717        mana_cost: ManaCost,
718        color: ColorSet,
719        base_power: Option<i32>,
720        base_toughness: Option<i32>,
721        keywords: Vec<String>,
722        abilities: Vec<String>,
723    ) -> Self {
724        // Parse activated abilities from raw ability strings.
725        let activated_abilities: Vec<ActivatedAbility> = abilities
726            .iter()
727            .enumerate()
728            .filter_map(|(i, raw)| {
729                parse_or_warn(parse_activated_ability(raw, i), "ActivatedAbility", raw)
730            })
731            .collect();
732
733        // Parse replacement effects from R$ lines in card abilities.
734        // Mirrors Java Card constructor calling ReplacementHandler registration.
735        let replacement_effects: Vec<ReplacementEffect> = abilities
736            .iter()
737            .filter_map(|raw| {
738                parse_or_warn(parse_replacement_effect(raw), "ReplacementEffect", raw)
739            })
740            .collect();
741
742        // Parse static abilities from S$ lines.
743        // Mirrors Java Forge Card constructor calling StaticAbility.create().
744        let static_abilities: Vec<StaticAbility> = abilities
745            .iter()
746            .filter_map(|raw| parse_or_warn(parse_static_ability(raw), "StaticAbility", raw))
747            .collect();
748
749        let full_name = card_name.clone();
750        let color_identity = color;
751        let mut card = Card {
752            id,
753            card_name,
754            full_name,
755            oracle_text: String::new(),
756            owner,
757            controller: owner,
758            zone: ZoneType::None,
759            type_line,
760            mana_cost,
761            color,
762            color_identity,
763            base_power,
764            base_toughness,
765            initial_loyalty: None,
766            power_modifier: 0,
767            toughness_modifier: 0,
768            perpetual_power_modifier: 0,
769            perpetual_toughness_modifier: 0,
770            perpetual: Vec::new(),
771            static_set_power: None,
772            static_set_toughness: None,
773            static_power_modifier: 0,
774            static_toughness_modifier: 0,
775            tapped: false,
776            last_mana_produced: None,
777            flipped: false,
778            face_down: false,
779            has_morph: false,
780            discarded: false,
781            unearthed: false,
782            class_level: 1,
783            paired_with: None,
784            manifested: false,
785            cloaked: false,
786            foretold: false,
787            foretold_cost_by_effect: false,
788            melded_with: Vec::new(),
789            is_bestowed: false,
790            summoning_sick: true,
791            came_under_control_since_last_upkeep: false,
792            exerted: false,
793            damage: 0,
794            cast_from: None,
795            counters: BTreeMap::new(),
796            keywords: crate::keyword::keyword_collection::KeywordCollection::from_strings(
797                &keywords,
798            ),
799            granted_keywords: crate::keyword::keyword_collection::KeywordCollection::new(),
800            granted_svars: BTreeMap::new(),
801            static_added_subtypes: Vec::new(),
802            static_type_line_base: None,
803            changed_type_line_base: None,
804            changed_base_power: None,
805            changed_base_toughness: None,
806            pump_keywords: crate::keyword::keyword_collection::KeywordCollection::new(),
807            pump_trigger_count: 0,
808            abilities,
809            action_spell_specs: Vec::new(),
810            action_spell_cost: None,
811            ai_phyrexian_payment: None,
812            spree_min_mode_cost: None,
813            activated_abilities,
814            base_ability_count: 0,
815            base_trigger_count: 0,
816            changed_card_traits: std::collections::BTreeMap::new(),
817            changed_card_traits_by_text: std::collections::BTreeMap::new(),
818            static_abilities,
819            has_deathtouch_damage: false,
820            cant_attack_static: false,
821            cant_block_static: false,
822            turn_in_zone: 0,
823            entered_battlefield_this_turn: false,
824            attacked_this_turn: false,
825            started_turn_tapped: false,
826            triggers: Vec::new(),
827            svars: BTreeMap::new(),
828            parsed_svar_cache: ParsedSVarCache::default(),
829            is_commander: false,
830            move_to_command_zone: false,
831            commander_cast_count: 0,
832            is_token: false,
833            cast_with_flashback: false,
834            cast_with_harmonize: false,
835            replacement_effects,
836            attached_to: None,
837            attached_to_player: None,
838            attached_this_turn: false,
839            attachments: Vec::new(),
840            remembered_cards: Vec::new(),
841            remembered_players: Vec::new(),
842            imprinted_cards: Vec::new(),
843            gain_control_targets: Vec::new(),
844            until_leaves_battlefield: Vec::new(),
845            exiled_cards: Vec::new(),
846            paid_cost_exiled_cards: Vec::new(),
847            haunted_by: Vec::new(),
848            haunting: None,
849            chosen_map: HashMap::new(),
850            remembered_cmc: Vec::new(),
851            effect_source: None,
852            clone_origin: None,
853            copied_permanent: None,
854            cast_sa: None,
855            chosen_charm_modes: HashMap::new(),
856            remembered_lki_cards: Vec::new(),
857            lose_control_condition: None,
858            temp_effect_until_eot: false,
859            temp_effect_host: None,
860            forget_on_moved_origin: None,
861            exile_when_no_remembered: false,
862            exiled_by: None,
863            original_controller_eot: None,
864            is_transformed: false,
865            other_part: None,
866            set_code: None,
867            card_number: None,
868            paper_foil: false,
869            phased_out: false,
870            regeneration_shields: 0,
871            kicked: false,
872            monstrous: false,
873            chosen_colors: Vec::new(),
874            chosen_cards: Vec::new(),
875            animate_state: None,
876            clone_state: None,
877            chosen_type: None,
878            chosen_type2: None,
879            noted_types: Vec::new(),
880            named_cards: Vec::new(),
881            chosen_number: None,
882            chosen_player: None,
883            chosen_player_controller: None,
884            chosen_type_controller: None,
885            chosen_player_revealed: false,
886            chosen_type_revealed: false,
887            promised_gift: None,
888            attraction_lights: Vec::new(),
889            sector: None,
890            chosen_sector: None,
891            sprocket: 0,
892            chosen_even_odd: None,
893            detained: false,
894            attacking_player: None,
895            goaded_by: None,
896            damage_prevention: 0,
897            assigned_damage: 0,
898            must_block: false,
899            encoded_cards: Vec::new(),
900            damage_sources_this_turn: Vec::new(),
901            total_damage_done_this_turn: 0,
902            lki_power: None,
903            lki_toughness: None,
904            lki_counters: None,
905            damage_history: damage_history::DamageHistory::default(),
906            must_block_cards: Vec::new(),
907            etb_counters_p1p1: 0,
908            colors_spent_to_cast: 0,
909            paying_mana_to_cast: Vec::new(),
910            chosen_modes: None,
911            strive_extra_targets: 0,
912            became_target_this_turn: false,
913            temp_controllers: Vec::new(),
914            may_look_at: Vec::new(),
915            may_play: Vec::new(),
916            can_block_additional: 0,
917            can_block_any: false,
918            cant_have_keywords: HashSet::new(),
919            intensity: 0,
920            surveilled: false,
921            milled: false,
922            visited_this_turn: false,
923            times_crewed_this_turn: 0,
924            is_crewed: false,
925            ignore_legend_rule_flag: false,
926            ability_activated_this_turn: 0,
927            ability_resolved_this_turn: 0,
928            number_turn_activations: ActivationTable::default(),
929            number_game_activations: ActivationTable::default(),
930            number_ability_resolved: ActivationTable::default(),
931            planeswalker_abilities_activated: 0,
932            planeswalker_activation_limit_used: false,
933            chosen_modes_turn: None,
934            enlisted_this_combat: false,
935            activations_this_game: std::collections::BTreeMap::new(),
936            is_renowned: false,
937            zone_timestamp: 0,
938            trait_base_activated_abilities: None,
939            trait_base_triggers: None,
940            trait_base_replacement_effects: None,
941            trait_base_static_abilities: None,
942            trait_base_keywords: None,
943        };
944
945        // Generate intrinsic abilities from card properties (mirrors Java CardFactoryUtil)
946        card.generate_basic_land_mana_abilities();
947        card.generate_keyword_abilities();
948        card.generate_keyword_triggers();
949        crate::card::card_state::update_types(&mut card);
950        crate::card::card_state::update_keywords_cache(&mut card);
951        crate::card::card_state::calculate_perpetual_adjusted_mana_cost(&mut card);
952        card.refresh_action_specs();
953        // Record base ability count so continuous effects can truncate granted abilities.
954        card.base_ability_count = card.activated_abilities.len();
955        card.base_trigger_count = card.triggers.len();
956        card
957    }
958
959    pub fn clone_for_parity_snapshot(&self) -> Self {
960        let mut out = self.clone();
961        out.abilities.clear();
962        out.activated_abilities.clear();
963        out.triggers.clear();
964        for static_ability in &mut out.static_abilities {
965            *static_ability.base = crate::card_trait_base::CardTraitBase::default();
966        }
967        out.replacement_effects.clear();
968        out.cast_sa = None;
969        out.trait_base_activated_abilities = None;
970        out.trait_base_triggers = None;
971        out.trait_base_replacement_effects = None;
972        out.trait_base_static_abilities = None;
973        out.trait_base_keywords = None;
974        out
975    }
976
977    /// Construct a `Card` from a `CardRules` definition.
978    /// This is the single entry point for creating game-ready cards from the
979    /// card database. Mirrors Java's `CardFactory.readCard()` + `CardFactoryUtil`.
980    ///
981    /// Handles:
982    /// - Base stats (name, mana cost, type line, color, P/T, keywords, abilities)
983    /// - Trigger parsing (T: lines) including SpellCastOrCopy → SpellCopied duplication
984    /// - Static ability parsing (S: lines) with alternative cost keyword conversion
985    /// - Replacement effect parsing (R: lines)
986    /// - SVars
987    /// - Double-faced card back face setup
988    /// - Intrinsic mana abilities (basic land subtypes)
989    /// - Keyword-generated abilities and triggers (Cycling, Prowess, Bushido)
990    pub fn from_rules(rules: &CardRules, owner: PlayerId) -> Self {
991        card_factory::build_from_rules(rules, owner)
992    }
993
994    /// Effective power, accounting for all layer effects and counters.
995    ///
996    /// Calculation order (CR 613):
997    /// - Layer 7b: `static_set_power` overrides `base_power` if set.
998    /// - Layer 7c: `static_power_modifier` (anthem bonuses) is added.
999    /// - Temporary: `power_modifier` (from spells like Giant Growth) is added.
1000    /// - Layer 7d: +1/+1 and -1/-1 counters are factored in.
1001    pub fn power(&self) -> i32 {
1002        let base = self
1003            .static_set_power
1004            .unwrap_or(self.base_power.unwrap_or(0));
1005        base + self.static_power_modifier
1006            + self.power_modifier
1007            + self.perpetual_power_modifier
1008            + self.counter_count(&CounterType::P1P1)
1009            - self.counter_count(&CounterType::M1M1)
1010    }
1011
1012    /// Effective toughness, accounting for all layer effects and counters.
1013    pub fn toughness(&self) -> i32 {
1014        let base = self
1015            .static_set_toughness
1016            .unwrap_or(self.base_toughness.unwrap_or(0));
1017        base + self.static_toughness_modifier
1018            + self.toughness_modifier
1019            + self.perpetual_toughness_modifier
1020            + self.counter_count(&CounterType::P1P1)
1021            - self.counter_count(&CounterType::M1M1)
1022    }
1023
1024    pub fn lethal_damage(&self) -> bool {
1025        self.damage >= self.toughness()
1026    }
1027
1028    pub fn can_be_dealt_damage(&self) -> bool {
1029        self.zone == ZoneType::Battlefield
1030            && (self.is_creature()
1031                || self.type_line.is_planeswalker()
1032                || self.type_line.core_types.contains(&CoreType::Battle))
1033    }
1034
1035    pub fn is_creature(&self) -> bool {
1036        !self.is_bestowed && self.type_line.is_creature()
1037    }
1038
1039    pub fn is_land(&self) -> bool {
1040        self.type_line.is_land()
1041    }
1042
1043    pub fn is_permanent(&self) -> bool {
1044        self.type_line.is_permanent()
1045    }
1046
1047    // CardState-style adapters wired on Card for Java-parity call sites.
1048    pub fn update_types(&mut self) {
1049        crate::card::card_state::update_types(self);
1050    }
1051
1052    pub fn update_types_for_view(&mut self) {
1053        crate::card::card_state::update_types_for_view(self);
1054    }
1055
1056    pub fn add_type(&mut self, ty: &str) {
1057        crate::card::card_state::add_type(self, ty);
1058        self.update_types();
1059        self.update_types_for_view();
1060    }
1061
1062    pub fn remove_type(&mut self, ty: &str) {
1063        crate::card::card_state::remove_type(self, ty);
1064        self.update_types();
1065        self.update_types_for_view();
1066    }
1067
1068    pub fn remove_card_types(&mut self) {
1069        crate::card::card_state::remove_card_types(self);
1070        self.update_types();
1071        self.update_types_for_view();
1072    }
1073
1074    pub fn set_type(&mut self, type_line: &str) {
1075        crate::card::card_state::set_type(self, type_line);
1076        self.update_types();
1077        self.update_types_for_view();
1078    }
1079
1080    pub fn set_type_line(&mut self, type_line: CardTypeLine) {
1081        self.type_line = type_line;
1082        self.update_types();
1083        self.update_types_for_view();
1084    }
1085
1086    pub fn add_color(&mut self, color: ColorSet) {
1087        crate::card::card_state::add_color(self, color);
1088    }
1089
1090    pub fn has_intrinsic_keyword(&self, keyword: &str) -> bool {
1091        crate::card::card_state::has_intrinsic_keyword(self, keyword)
1092    }
1093
1094    pub fn add_intrinsic_keyword(&mut self, keyword: &str) -> bool {
1095        let changed = crate::card::card_state::add_intrinsic_keyword(self, keyword);
1096        if changed {
1097            crate::card::card_state::update_keywords_cache(self);
1098        }
1099        changed
1100    }
1101
1102    pub fn add_intrinsic_keywords<'a>(
1103        &mut self,
1104        keywords: impl IntoIterator<Item = &'a str>,
1105    ) -> bool {
1106        let changed = crate::card::card_state::add_intrinsic_keywords(self, keywords);
1107        if changed {
1108            crate::card::card_state::update_keywords_cache(self);
1109        }
1110        changed
1111    }
1112
1113    pub fn remove_intrinsic_keyword(&mut self, keyword: &str) -> bool {
1114        let changed = crate::card::card_state::remove_intrinsic_keyword(self, keyword);
1115        if changed {
1116            crate::card::card_state::update_keywords_cache(self);
1117        }
1118        changed
1119    }
1120
1121    pub fn has_spell_ability(&self, sa: &SpellAbility) -> bool {
1122        crate::card::card_state::has_spell_ability(self, sa)
1123    }
1124
1125    pub fn add_spell_ability(&mut self, sa: &SpellAbility) -> bool {
1126        let added = crate::card::card_state::add_spell_ability(self, sa);
1127        self.refresh_action_specs();
1128        added
1129    }
1130
1131    pub fn has_trigger(&self, trigger_id: u32) -> bool {
1132        crate::card::card_state::has_trigger(self, trigger_id)
1133    }
1134
1135    pub fn add_trigger(&mut self, trig: Trigger) -> bool {
1136        crate::card::card_state::add_trigger(self, trig)
1137    }
1138
1139    pub fn clear_pump_triggers(&mut self) {
1140        let count = self.pump_trigger_count;
1141        if count == 0 {
1142            return;
1143        }
1144        let new_len = self
1145            .triggers
1146            .len()
1147            .saturating_sub(count)
1148            .max(self.base_trigger_count);
1149        self.triggers.truncate(new_len);
1150        self.pump_trigger_count = 0;
1151    }
1152
1153    pub fn copiable_triggers(&self) -> Vec<Trigger> {
1154        self.trait_base_triggers
1155            .clone()
1156            .unwrap_or_else(|| self.triggers.clone())
1157    }
1158
1159    pub fn copiable_replacement_effects(&self) -> Vec<ReplacementEffect> {
1160        self.trait_base_replacement_effects
1161            .clone()
1162            .unwrap_or_else(|| self.replacement_effects.clone())
1163    }
1164
1165    pub fn add_static_ability(&mut self, st_ab: StaticAbility) -> bool {
1166        crate::card::card_state::add_static_ability(self, st_ab)
1167    }
1168
1169    pub fn remove_static_ability(&mut self, mode: crate::staticability::StaticMode) -> bool {
1170        crate::card::card_state::remove_static_ability(self, mode)
1171    }
1172
1173    pub fn add_replacement_effect(&mut self, re: ReplacementEffect) -> bool {
1174        crate::card::card_state::add_replacement_effect(self, re)
1175    }
1176
1177    pub fn has_replacement_effect(&self) -> bool {
1178        crate::card::card_state::has_replacement_effect(self)
1179    }
1180
1181    pub fn has_s_var(&self, key: &str) -> bool {
1182        crate::card::card_state::has_s_var(self, key)
1183    }
1184
1185    pub fn get_s_var(&self, key: &str) -> Option<&str> {
1186        self.svars
1187            .get(key)
1188            .or_else(|| self.granted_svars.get(key))
1189            .map(String::as_str)
1190    }
1191
1192    pub fn parsed_s_var(&mut self, key: &str) -> Option<ParsedSVar> {
1193        let raw = self.get_s_var(key)?.to_string();
1194        Some(self.parsed_svar_cache.get_or_parse(key, &raw).clone())
1195    }
1196
1197    pub fn remove_s_var(&mut self, key: &str) {
1198        crate::card::card_state::remove_s_var(self, key);
1199        self.parsed_svar_cache.remove(key);
1200        self.refresh_action_specs_after_svar_change();
1201    }
1202
1203    pub fn set_s_var(&mut self, key: impl Into<String>, value: impl Into<String>) {
1204        let key = key.into();
1205        self.parsed_svar_cache.remove(&key);
1206        self.svars.insert(key, value.into());
1207        self.refresh_action_specs_after_svar_change();
1208    }
1209
1210    pub fn set_s_var_if_absent(&mut self, key: impl Into<String>, value: impl Into<String>) {
1211        let key = key.into();
1212        if self.svars.contains_key(&key) {
1213            return;
1214        }
1215        self.svars.insert(key, value.into());
1216        self.refresh_action_specs_after_svar_change();
1217    }
1218
1219    pub fn set_svars_map(&mut self, svars: BTreeMap<String, String>) {
1220        self.svars = svars;
1221        self.parsed_svar_cache.clear();
1222        self.refresh_action_specs();
1223    }
1224
1225    pub fn copy_from_card_state(&mut self, source: &Card) {
1226        crate::card::card_state::copy_from(source, self);
1227    }
1228
1229    pub fn copy_from(&mut self, source: &Card) {
1230        self.copy_from_card_state(source);
1231    }
1232
1233    pub fn add_abilities_from(&mut self, source: &Card) {
1234        crate::card::card_state::add_abilities_from(source, self);
1235    }
1236
1237    pub fn has_property(&self, property: &str) -> bool {
1238        crate::card::card_state::has_property(self, property)
1239    }
1240
1241    pub fn reset_original_host(&mut self) {
1242        crate::card::card_state::reset_original_host(self);
1243    }
1244
1245    pub fn update_changed_text(&mut self) {
1246        crate::card::card_state::update_changed_text(self);
1247    }
1248
1249    pub fn update_keywords_cache(&mut self) {
1250        crate::card::card_state::update_keywords_cache(self);
1251    }
1252
1253    pub fn change_text_intrinsic(&mut self) {
1254        crate::card::card_state::change_text_intrinsic(self);
1255    }
1256
1257    pub fn has_chapter(&self) -> bool {
1258        crate::card::card_state::has_chapter(self)
1259    }
1260
1261    pub fn get_final_chapter_nr(&self) -> i32 {
1262        crate::card::card_state::get_final_chapter_nr(self)
1263    }
1264
1265    /// Check whether this card has a keyword — intrinsically, granted by a
1266    /// continuous static effect (Layer 6), or temporarily from a pump effect.
1267    /// Count distinct colors of mana spent to cast this spell (for Sunburst/Converge).
1268    pub fn sunburst_count(&self) -> i32 {
1269        use forge_foundation::mana::ManaAtom;
1270        let mut count = 0;
1271        for &bit in &[
1272            ManaAtom::WHITE,
1273            ManaAtom::BLUE,
1274            ManaAtom::BLACK,
1275            ManaAtom::RED,
1276            ManaAtom::GREEN,
1277        ] {
1278            if (self.colors_spent_to_cast & bit) != 0 {
1279                count += 1;
1280            }
1281        }
1282        count
1283    }
1284
1285    pub fn has_keyword(&self, kw: &str) -> bool {
1286        crate::card::card_state::has_keyword(self, kw)
1287    }
1288
1289    /// Check for a keyword using the typed Keyword enum.
1290    /// Checks the structured `keyword_collection` first (O(1) HashMap lookup),
1291    /// then falls back to string matching on granted/pump keywords.
1292    /// Mirrors Java's `Card.hasKeyword(Keyword)`.
1293    pub fn has_keyword_enum(&self, kw: Kw) -> bool {
1294        if self
1295            .cant_have_keywords
1296            .contains(&kw.display_name().to_ascii_lowercase())
1297        {
1298            return false;
1299        }
1300        self.keywords.contains_keyword(kw)
1301            || self.granted_keywords.contains_keyword(kw)
1302            || self.pump_keywords.contains_keyword(kw)
1303    }
1304
1305    pub fn has_haste(&self) -> bool {
1306        self.has_keyword_enum(crate::keyword::keyword_instance::Keyword::Haste)
1307    }
1308
1309    pub fn has_flying(&self) -> bool {
1310        self.has_keyword_enum(crate::keyword::keyword_instance::Keyword::Flying)
1311    }
1312
1313    pub fn has_reach(&self) -> bool {
1314        self.has_keyword_enum(Kw::Reach)
1315    }
1316
1317    pub fn has_first_strike(&self) -> bool {
1318        self.has_keyword_enum(Kw::FirstStrike)
1319    }
1320
1321    pub fn has_double_strike(&self) -> bool {
1322        self.has_keyword_enum(Kw::DoubleStrike)
1323    }
1324
1325    pub fn has_trample(&self) -> bool {
1326        self.has_keyword_enum(Kw::Trample)
1327    }
1328
1329    pub fn has_deathtouch(&self) -> bool {
1330        self.has_keyword_enum(Kw::Deathtouch)
1331    }
1332
1333    pub fn has_lifelink(&self) -> bool {
1334        self.has_keyword_enum(Kw::Lifelink)
1335    }
1336
1337    pub fn has_vigilance(&self) -> bool {
1338        self.has_keyword_enum(Kw::Vigilance)
1339    }
1340
1341    pub fn has_defender(&self) -> bool {
1342        self.has_keyword_enum(Kw::Defender)
1343    }
1344
1345    pub fn has_hexproof(&self) -> bool {
1346        self.has_keyword_enum(Kw::Hexproof)
1347    }
1348
1349    pub fn has_shroud(&self) -> bool {
1350        self.has_keyword_enum(Kw::Shroud)
1351    }
1352
1353    pub fn has_menace(&self) -> bool {
1354        self.has_keyword_enum(Kw::Menace)
1355    }
1356
1357    pub fn has_fear(&self) -> bool {
1358        self.has_keyword_enum(Kw::Fear)
1359    }
1360
1361    pub fn has_intimidate(&self) -> bool {
1362        self.has_keyword_enum(Kw::Intimidate)
1363    }
1364
1365    pub fn has_shadow(&self) -> bool {
1366        self.has_keyword_enum(Kw::Shadow)
1367    }
1368
1369    pub fn has_skulk(&self) -> bool {
1370        self.has_keyword_enum(Kw::Skulk)
1371    }
1372
1373    pub fn has_horsemanship(&self) -> bool {
1374        self.has_keyword_enum(Kw::Horsemanship)
1375    }
1376
1377    pub fn has_indestructible(&self) -> bool {
1378        self.has_keyword_enum(Kw::Indestructible)
1379    }
1380
1381    pub fn has_infect(&self) -> bool {
1382        self.has_keyword_enum(Kw::Infect)
1383    }
1384
1385    pub fn has_wither(&self) -> bool {
1386        self.has_keyword_enum(Kw::Wither)
1387    }
1388
1389    pub fn has_prowess(&self) -> bool {
1390        self.has_keyword_enum(Kw::Prowess)
1391    }
1392
1393    pub fn has_rebound(&self) -> bool {
1394        self.has_keyword_enum(Kw::Rebound)
1395    }
1396
1397    /// Check "Hexproof from <color>" variants (e.g. "Hexproof from blue").
1398    pub fn has_hexproof_from(&self, color: &str) -> bool {
1399        let target = format!("Hexproof from {color}");
1400        self.keywords.contains_string_ignore_case(&target)
1401            || self.granted_keywords.contains_string_ignore_case(&target)
1402    }
1403
1404    /// Get Toxic count (e.g. "Toxic:1" → Some(1)).
1405    pub fn get_toxic_count(&self) -> Option<i32> {
1406        self.get_keyword_cost("Toxic").and_then(|s| s.parse().ok())
1407    }
1408
1409    /// Whether this card has the Storm keyword.
1410    pub fn has_storm(&self) -> bool {
1411        self.has_keyword_enum(Kw::Storm)
1412    }
1413
1414    pub fn has_cascade(&self) -> bool {
1415        self.has_keyword_enum(Kw::Cascade)
1416    }
1417
1418    /// Converted mana cost (mana value).
1419    pub fn mana_value(&self) -> i32 {
1420        self.mana_cost.cmc()
1421    }
1422
1423    /// Check "Protection from <quality>" (e.g. "Protection from red").
1424    pub fn has_protection_from(&self, quality: &str) -> bool {
1425        let target = format!("Protection from {quality}");
1426        self.keywords.contains_string_ignore_case(&target)
1427            || self.granted_keywords.contains_string_ignore_case(&target)
1428    }
1429
1430    /// Get all "Protection from X" values this card has.
1431    pub fn get_protections(&self) -> Vec<String> {
1432        let mut prots = Vec::new();
1433        for kw in self
1434            .keywords
1435            .iter_strings()
1436            .chain(self.granted_keywords.iter_strings())
1437        {
1438            if let Some(from) = kw.strip_prefix("Protection from ") {
1439                prots.push(from.to_lowercase());
1440            }
1441        }
1442        prots
1443    }
1444
1445    /// Check if this card is protected from a source card.
1446    /// Protection from <color> checks source's color.
1447    /// Protection from <type> checks source's type (e.g. "artifacts", "creatures").
1448    pub fn is_protected_from(&self, source: &Card) -> bool {
1449        for prot in self.get_protections() {
1450            match prot.as_str() {
1451                "white" => {
1452                    if source.color.has_white() {
1453                        return true;
1454                    }
1455                }
1456                "blue" => {
1457                    if source.color.has_blue() {
1458                        return true;
1459                    }
1460                }
1461                "black" => {
1462                    if source.color.has_black() {
1463                        return true;
1464                    }
1465                }
1466                "red" => {
1467                    if source.color.has_red() {
1468                        return true;
1469                    }
1470                }
1471                "green" => {
1472                    if source.color.has_green() {
1473                        return true;
1474                    }
1475                }
1476                "colorless" => {
1477                    if source.color.is_colorless() {
1478                        return true;
1479                    }
1480                }
1481                "artifacts" => {
1482                    if source.type_line.is_artifact() {
1483                        return true;
1484                    }
1485                }
1486                "creatures" => {
1487                    if source.type_line.is_creature() {
1488                        return true;
1489                    }
1490                }
1491                "enchantments" => {
1492                    if source.type_line.is_enchantment() {
1493                        return true;
1494                    }
1495                }
1496                _ => {}
1497            }
1498        }
1499        false
1500    }
1501
1502    pub fn can_attack(&self) -> bool {
1503        self.is_creature()
1504            && !self.tapped
1505            && !self.has_defender()
1506            && !self.cant_attack_static
1507            && !self.detained
1508            && (self.has_haste() || !self.summoning_sick)
1509            && self.zone == ZoneType::Battlefield
1510    }
1511
1512    pub fn can_block(&self) -> bool {
1513        self.is_creature()
1514            && !self.tapped
1515            && !self.cant_block_static
1516            && !self.detained
1517            && self.zone == ZoneType::Battlefield
1518    }
1519
1520    /// Check if this card can be controlled by the given player
1521    /// (e.g., checks for "Other players can't gain control of CARDNAME.")
1522    pub fn can_be_controlled_by(&self, player: PlayerId) -> bool {
1523        if player == self.controller {
1524            return true;
1525        }
1526        !self.has_keyword("Other players can't gain control of CARDNAME.")
1527    }
1528
1529    pub fn counter_count(&self, ct: &CounterType) -> i32 {
1530        *self.counters.get(ct).unwrap_or(&0)
1531    }
1532
1533    pub fn add_counter(&mut self, ct: &CounterType, count: i32) {
1534        let entry = self.counters.entry(ct.clone()).or_insert(0);
1535        *entry += count;
1536    }
1537
1538    pub fn remove_counter(&mut self, ct: &CounterType, count: i32) {
1539        let entry = self.counters.entry(ct.clone()).or_insert(0);
1540        *entry = (*entry - count).max(0);
1541    }
1542
1543    /// Reset state when entering the battlefield.
1544    pub fn enter_battlefield(&mut self) {
1545        self.tapped = false;
1546        self.damage = 0;
1547        self.summoning_sick = true;
1548        self.came_under_control_since_last_upkeep = true;
1549        self.has_deathtouch_damage = false;
1550        self.entered_battlefield_this_turn = true;
1551        self.attacked_this_turn = false;
1552        self.damage_sources_this_turn.clear();
1553    }
1554
1555    /// Reset per-turn state at start of turn.
1556    pub fn clear_global_turn_state(&mut self) {
1557        self.entered_battlefield_this_turn = false;
1558        self.attacked_this_turn = false;
1559        self.attached_this_turn = false;
1560        self.has_deathtouch_damage = false;
1561        self.damage_sources_this_turn.clear();
1562        self.total_damage_done_this_turn = 0;
1563    }
1564
1565    /// Reset controller-specific state at the start of that player's turn.
1566    pub fn new_turn(&mut self) {
1567        self.clear_global_turn_state();
1568        if self.zone == ZoneType::Battlefield {
1569            if let Ok(filter) = std::env::var("FORGE_CARD_TRACE") {
1570                if !filter.is_empty()
1571                    && self.card_name.eq_ignore_ascii_case(&filter)
1572                    && self.summoning_sick
1573                {
1574                    eprintln!(
1575                        "[card-trace] new_turn clears sickness on {}#{:?} (controller={:?})",
1576                        self.card_name, self.id, self.controller,
1577                    );
1578                }
1579            }
1580            self.summoning_sick = false;
1581        }
1582    }
1583
1584    /// Add a remembered card (for RememberCountered, etc.)
1585    pub fn add_remembered_card(&mut self, card_id: CardId) {
1586        if !self.remembered_cards.contains(&card_id) {
1587            self.remembered_cards.push(card_id);
1588        }
1589    }
1590
1591    /// Add a remembered CMC value
1592    pub fn add_remembered_cmc(&mut self, cmc: i32) {
1593        self.remembered_cmc.push(cmc);
1594    }
1595
1596    pub fn add_remembered_player(&mut self, player: PlayerId) {
1597        if !self.remembered_players.contains(&player) {
1598            self.remembered_players.push(player);
1599        }
1600    }
1601
1602    pub fn add_remembered_players<I>(&mut self, players: I)
1603    where
1604        I: IntoIterator<Item = PlayerId>,
1605    {
1606        for p in players {
1607            self.add_remembered_player(p);
1608        }
1609    }
1610
1611    pub fn has_remembered(&self) -> bool {
1612        !self.remembered_cards.is_empty()
1613            || !self.remembered_players.is_empty()
1614            || !self.remembered_cmc.is_empty()
1615    }
1616
1617    pub fn add_remembered(&mut self, card_id: CardId) {
1618        self.add_remembered_card(card_id);
1619    }
1620
1621    pub fn remove_remembered(&mut self, card_id: CardId) {
1622        self.remembered_cards.retain(|&c| c != card_id);
1623    }
1624
1625    pub fn clear_remembered(&mut self) {
1626        self.remembered_cards.clear();
1627        self.remembered_players.clear();
1628        self.remembered_cmc.clear();
1629    }
1630
1631    pub fn update_remembered(&mut self) {
1632        let mut seen = HashSet::new();
1633        self.remembered_cards.retain(|c| seen.insert(*c));
1634    }
1635
1636    pub fn has_imprinted_card(&self) -> bool {
1637        !self.imprinted_cards.is_empty()
1638    }
1639
1640    pub fn add_imprinted_card(&mut self, card_id: CardId) {
1641        if !self.imprinted_cards.contains(&card_id) {
1642            self.imprinted_cards.push(card_id);
1643        }
1644    }
1645
1646    pub fn add_imprinted_cards(&mut self, cards: impl IntoIterator<Item = CardId>) {
1647        for c in cards {
1648            self.add_imprinted_card(c);
1649        }
1650    }
1651
1652    pub fn remove_imprinted_card(&mut self, card_id: CardId) {
1653        self.imprinted_cards.retain(|&c| c != card_id);
1654    }
1655
1656    pub fn remove_imprinted_cards(&mut self, cards: impl IntoIterator<Item = CardId>) {
1657        for c in cards {
1658            self.remove_imprinted_card(c);
1659        }
1660    }
1661
1662    pub fn clear_imprinted_cards(&mut self) {
1663        self.imprinted_cards.clear();
1664    }
1665
1666    pub fn add_to_chosen_map(&mut self, player: PlayerId, chosen: Vec<CardId>) {
1667        self.chosen_map.insert(player, chosen);
1668    }
1669
1670    pub fn add_gain_control_target(&mut self, card_id: CardId) {
1671        if !self.gain_control_targets.contains(&card_id) {
1672            self.gain_control_targets.push(card_id);
1673        }
1674    }
1675
1676    pub fn remove_gain_control_targets(&mut self, card_id: CardId) {
1677        self.gain_control_targets.retain(|&c| c != card_id);
1678    }
1679
1680    pub fn has_gain_control_target(&self) -> bool {
1681        !self.gain_control_targets.is_empty()
1682    }
1683
1684    pub fn add_until_leaves_battlefield(&mut self, card_id: CardId) {
1685        if !self.until_leaves_battlefield.contains(&card_id) {
1686            self.until_leaves_battlefield.push(card_id);
1687        }
1688    }
1689
1690    pub fn remove_until_leaves_battlefield(&mut self, card_id: CardId) {
1691        self.until_leaves_battlefield.retain(|&c| c != card_id);
1692    }
1693
1694    pub fn clear_until_leaves_battlefield(&mut self) {
1695        self.until_leaves_battlefield.clear();
1696    }
1697
1698    pub fn has_exiled_card(&self) -> bool {
1699        !self.exiled_cards.is_empty()
1700    }
1701
1702    pub fn add_exiled_card(&mut self, card_id: CardId) {
1703        if !self.exiled_cards.contains(&card_id) {
1704            self.exiled_cards.push(card_id);
1705        }
1706    }
1707
1708    pub fn add_exiled_cards(&mut self, cards: impl IntoIterator<Item = CardId>) {
1709        for c in cards {
1710            self.add_exiled_card(c);
1711        }
1712    }
1713
1714    pub fn remove_exiled_card(&mut self, card_id: CardId) {
1715        self.exiled_cards.retain(|&c| c != card_id);
1716    }
1717
1718    pub fn remove_exiled_cards(&mut self, cards: impl IntoIterator<Item = CardId>) {
1719        for c in cards {
1720            self.remove_exiled_card(c);
1721        }
1722    }
1723
1724    pub fn clear_exiled_cards(&mut self) {
1725        self.exiled_cards.clear();
1726    }
1727
1728    pub fn add_haunted_by(&mut self, card_id: CardId) {
1729        if !self.haunted_by.contains(&card_id) {
1730            self.haunted_by.push(card_id);
1731        }
1732    }
1733
1734    pub fn remove_haunted_by(&mut self, card_id: CardId) {
1735        self.haunted_by.retain(|&c| c != card_id);
1736    }
1737
1738    pub fn has_encoded_card(&self) -> bool {
1739        !self.encoded_cards.is_empty()
1740    }
1741
1742    pub fn add_encoded_card(&mut self, card_id: CardId) {
1743        if !self.encoded_cards.contains(&card_id) {
1744            self.encoded_cards.push(card_id);
1745        }
1746    }
1747
1748    pub fn add_encoded_cards(&mut self, cards: impl IntoIterator<Item = CardId>) {
1749        for c in cards {
1750            self.add_encoded_card(c);
1751        }
1752    }
1753
1754    pub fn remove_encoded_card(&mut self, card_id: CardId) {
1755        self.encoded_cards.retain(|&c| c != card_id);
1756    }
1757
1758    pub fn clear_encoded_cards(&mut self) {
1759        self.encoded_cards.clear();
1760    }
1761
1762    pub fn has_merged_card(&self) -> bool {
1763        !self.melded_with.is_empty()
1764    }
1765
1766    pub fn add_merged_card(&mut self, card_id: CardId) {
1767        if !self.melded_with.contains(&card_id) {
1768            self.melded_with.push(card_id);
1769        }
1770    }
1771
1772    pub fn add_merged_card_to_top(&mut self, card_id: CardId) {
1773        if !self.melded_with.contains(&card_id) {
1774            self.melded_with.insert(0, card_id);
1775        }
1776    }
1777
1778    pub fn remove_merged_card(&mut self, card_id: CardId) {
1779        self.melded_with.retain(|&c| c != card_id);
1780    }
1781
1782    pub fn clear_merged_cards(&mut self) {
1783        self.melded_with.clear();
1784    }
1785
1786    pub fn remove_mutated_states(&mut self) {
1787        self.clear_merged_cards();
1788    }
1789
1790    pub fn rebuild_mutated_states(&mut self) {
1791        let mut seen = HashSet::new();
1792        self.melded_with.retain(|c| seen.insert(*c));
1793    }
1794
1795    pub fn move_merged_to_subgame(&mut self) {
1796        self.clear_merged_cards();
1797    }
1798
1799    pub fn entered_this_turn(&self) -> bool {
1800        self.entered_battlefield_this_turn
1801    }
1802
1803    pub fn entered_current_zone_this_turn(&self, turn_number: u32) -> bool {
1804        self.turn_in_zone == turn_number
1805    }
1806
1807    pub fn calculate_perpetual_adjusted_mana_cost(&mut self) {
1808        crate::card::card_state::calculate_perpetual_adjusted_mana_cost(self);
1809    }
1810
1811    pub fn has_chosen_player(&self) -> bool {
1812        self.chosen_player.is_some()
1813    }
1814
1815    pub fn reveal_chosen_player(&mut self) {
1816        self.chosen_player_revealed = true;
1817    }
1818
1819    pub fn has_promised_gift(&self) -> bool {
1820        self.promised_gift.is_some()
1821    }
1822
1823    pub fn has_chosen_number(&self) -> bool {
1824        self.chosen_number.is_some()
1825    }
1826
1827    pub fn clear_chosen_number(&mut self) {
1828        self.chosen_number = None;
1829    }
1830
1831    pub fn has_chosen_type(&self) -> bool {
1832        self.chosen_type
1833            .as_ref()
1834            .map(|s| !s.is_empty())
1835            .unwrap_or(false)
1836    }
1837
1838    pub fn reveal_chosen_type(&mut self) {
1839        self.chosen_type_revealed = true;
1840    }
1841
1842    pub fn has_chosen_type2(&self) -> bool {
1843        self.chosen_type2
1844            .as_ref()
1845            .map(|s| !s.is_empty())
1846            .unwrap_or(false)
1847    }
1848
1849    pub fn has_any_noted_type(&self) -> bool {
1850        !self.noted_types.is_empty()
1851    }
1852
1853    pub fn add_noted_type(&mut self, ty: &str) {
1854        self.noted_types.push(ty.to_string());
1855    }
1856
1857    pub fn has_chosen_color(&self) -> bool {
1858        !self.chosen_colors.is_empty()
1859    }
1860
1861    pub fn has_chosen_card(&self) -> bool {
1862        !self.chosen_cards.is_empty()
1863    }
1864
1865    pub fn assign_sector(&mut self, sector: &str) {
1866        self.sector = Some(sector.to_string());
1867    }
1868
1869    pub fn has_attraction_light(&self, light: i32) -> bool {
1870        light > 0 && self.attraction_lights.contains(&(light as u32))
1871    }
1872
1873    pub fn has_sector(&self) -> bool {
1874        self.sector.is_some()
1875    }
1876
1877    pub fn handle_changed_controller_sprocket_reset(&mut self) {
1878        if self.sprocket != 0 {
1879            self.sprocket = -1;
1880        }
1881    }
1882
1883    pub fn add_named_card(&mut self, name: &str) {
1884        self.named_cards.push(name.to_string());
1885    }
1886
1887    pub fn has_named_card(&self) -> bool {
1888        !self.named_cards.is_empty()
1889    }
1890
1891    pub fn has_chosen_even_odd(&self) -> bool {
1892        self.chosen_even_odd.is_some()
1893    }
1894
1895    pub fn has_no_abilities(&self) -> bool {
1896        self.abilities.is_empty()
1897            && self.activated_abilities.is_empty()
1898            && self.triggers.is_empty()
1899            && self.static_abilities.is_empty()
1900            && self.replacement_effects.is_empty()
1901    }
1902
1903    pub fn can_tap(&self) -> bool {
1904        !self.tapped
1905    }
1906
1907    pub fn tap(&mut self) -> bool {
1908        if !self.can_tap() {
1909            return false;
1910        }
1911        self.tapped = true;
1912        true
1913    }
1914
1915    pub fn set_tapped(&mut self, tapped: bool) {
1916        if tapped {
1917            self.tap();
1918        } else {
1919            self.untap();
1920        }
1921    }
1922
1923    pub fn set_owner(&mut self, owner: PlayerId) {
1924        self.owner = owner;
1925    }
1926
1927    pub fn set_controller(&mut self, controller: PlayerId) {
1928        if self.controller != controller {
1929            self.came_under_control_since_last_upkeep = true;
1930        }
1931        self.controller = controller;
1932    }
1933
1934    pub fn set_is_token(&mut self, is_token: bool) {
1935        self.is_token = is_token;
1936    }
1937
1938    pub fn set_effect_source(&mut self, source: Option<CardId>) {
1939        self.effect_source = source;
1940    }
1941
1942    pub fn set_temp_effect_host(&mut self, host: Option<CardId>) {
1943        self.temp_effect_host = host;
1944    }
1945
1946    pub fn set_temp_effect_until_eot(&mut self, until_eot: bool) {
1947        self.temp_effect_until_eot = until_eot;
1948    }
1949
1950    pub fn set_forget_on_moved_origin(&mut self, zone: Option<ZoneType>) {
1951        self.forget_on_moved_origin = zone;
1952    }
1953
1954    pub fn set_exile_when_no_remembered(&mut self, exile: bool) {
1955        self.exile_when_no_remembered = exile;
1956    }
1957
1958    pub fn set_flipped(&mut self, flipped: bool) {
1959        self.flipped = flipped;
1960    }
1961
1962    pub fn can_untap(&self) -> bool {
1963        self.tapped
1964    }
1965
1966    pub fn untap(&mut self) -> bool {
1967        if !self.can_untap() {
1968            return false;
1969        }
1970        self.tapped = false;
1971        true
1972    }
1973
1974    pub fn exert(&mut self) {
1975        self.exerted = true;
1976    }
1977
1978    pub fn clear_exerted(&mut self) {
1979        self.exerted = false;
1980    }
1981
1982    pub fn remove_exerted_by(&mut self, _player: PlayerId) {
1983        self.exerted = false;
1984    }
1985
1986    pub fn detain(&mut self) {
1987        self.detained = true;
1988    }
1989
1990    pub fn add_goad(&mut self, player: PlayerId) {
1991        self.goaded_by = Some(player);
1992    }
1993
1994    pub fn remove_goad(&mut self, player: PlayerId) {
1995        if self.goaded_by == Some(player) {
1996            self.goaded_by = None;
1997        }
1998    }
1999
2000    pub fn un_goad(&mut self) {
2001        self.goaded_by = None;
2002    }
2003
2004    pub fn remove_detained_by(&mut self, _player: PlayerId) {
2005        self.detained = false;
2006    }
2007
2008    pub fn update_ability_text_for_view(&mut self) {
2009        self.update_spell_abilities();
2010    }
2011    pub fn update_non_ability_text_for_view(&mut self) {
2012        self.update_changed_text();
2013    }
2014    pub fn update_mana_cost_for_view(&mut self) {
2015        let _ = self.mana_value();
2016    }
2017    pub fn update_p_tfor_view(&mut self) {
2018        let _ = (self.power(), self.toughness());
2019    }
2020    pub fn update_color_for_view(&mut self) {
2021        let _ = self.color;
2022    }
2023    pub fn update_attacking_for_view(&mut self) {
2024        let _ = self.attacking_player;
2025    }
2026    pub fn update_blocking_for_view(&mut self) {
2027        let _ = self.must_block;
2028    }
2029    pub fn update_state_for_view(&mut self) {
2030        let _ = (self.zone, self.tapped, self.face_down);
2031    }
2032    pub fn update_namefor_view(&mut self) {
2033        self.card_name = self.card_name.trim().to_string();
2034    }
2035    pub fn update_token_view(&mut self) {
2036        let _ = self.is_token;
2037    }
2038    pub fn update_was_destroyed(&mut self) {
2039        let _ = self.damage >= self.toughness();
2040    }
2041    pub fn update_rules_view(&mut self) {
2042        let _ = (&self.abilities, &self.keywords);
2043    }
2044    pub fn update_commander_view(&mut self) {
2045        let _ = self.is_commander;
2046    }
2047    pub fn update_card(&mut self) {
2048        self.update_namefor_view();
2049        self.update_types_for_view();
2050        self.update_color_for_view();
2051        self.update_mana_cost_for_view();
2052        self.update_p_tfor_view();
2053        self.update_rules_view();
2054        self.update_state_for_view();
2055    }
2056    pub fn dangerously_set_game(&mut self) {
2057        self.update_card();
2058    }
2059    pub fn visit(&mut self) {
2060        self.update_card();
2061    }
2062
2063    pub fn has_state(&self) -> bool {
2064        self.is_transformed || self.other_part.is_some()
2065    }
2066
2067    /// Whether this card is double-faced (has a back side).
2068    /// Mirrors Java `Card.isDoubleFaced()`.
2069    pub fn is_double_faced(&self) -> bool {
2070        self.other_part.is_some()
2071    }
2072
2073    pub fn change_to_state(&mut self) {
2074        self.transform();
2075    }
2076
2077    pub fn add_alternate_state(&mut self, other: CardOtherPart) {
2078        self.other_part = Some(other);
2079    }
2080
2081    pub fn clear_states(&mut self) {
2082        self.other_part = None;
2083        self.is_transformed = false;
2084    }
2085
2086    pub fn change_card_state(&mut self) {
2087        self.transform();
2088    }
2089
2090    pub fn has_alternate_state(&self) -> bool {
2091        self.other_part.is_some()
2092    }
2093
2094    pub fn manifest(&mut self) {
2095        self.manifested = true;
2096        self.turn_face_down();
2097    }
2098
2099    pub fn cloak(&mut self) {
2100        self.cloaked = true;
2101        self.turn_face_down();
2102    }
2103
2104    pub fn turn_face_down(&mut self) {
2105        self.face_down = true;
2106    }
2107
2108    pub fn turn_face_down_no_update(&mut self) {
2109        self.face_down = true;
2110    }
2111
2112    pub fn can_be_turned_face_up(&self) -> bool {
2113        self.face_down
2114    }
2115
2116    pub fn force_turn_face_up(&mut self) {
2117        self.face_down = false;
2118    }
2119
2120    pub fn turn_face_up(&mut self) {
2121        self.face_down = false;
2122    }
2123
2124    pub fn set_face_down(&mut self, face_down: bool) {
2125        if face_down {
2126            self.turn_face_down();
2127        } else {
2128            self.turn_face_up();
2129        }
2130    }
2131
2132    pub fn set_manifested(&mut self, manifested: bool) {
2133        self.manifested = manifested;
2134    }
2135
2136    pub fn set_cloaked(&mut self, cloaked: bool) {
2137        self.cloaked = cloaked;
2138    }
2139
2140    pub fn set_discarded(&mut self, discarded: bool) {
2141        self.discarded = discarded;
2142    }
2143
2144    pub fn set_unearthed(&mut self, unearthed: bool) {
2145        self.unearthed = unearthed;
2146    }
2147
2148    pub fn set_summoning_sick(&mut self, summoning_sick: bool) {
2149        self.summoning_sick = summoning_sick;
2150    }
2151
2152    pub fn set_foretold(&mut self, foretold: bool) {
2153        self.foretold = foretold;
2154    }
2155
2156    pub fn set_foretold_cost_by_effect(&mut self, by_effect: bool) {
2157        self.foretold_cost_by_effect = by_effect;
2158    }
2159
2160    pub fn set_transformed(&mut self, transformed: bool) {
2161        self.is_transformed = transformed;
2162    }
2163
2164    pub fn set_attacking_player(&mut self, player: PlayerId) {
2165        self.attacking_player = Some(player);
2166    }
2167
2168    pub fn clear_attacking_player(&mut self) {
2169        self.attacking_player = None;
2170    }
2171
2172    pub fn mark_attacked_this_turn(&mut self) {
2173        self.attacked_this_turn = true;
2174    }
2175
2176    pub fn add_etb_counters_p1p1(&mut self, amount: i32) {
2177        self.etb_counters_p1p1 += amount;
2178    }
2179
2180    pub fn increment_commander_cast_count(&mut self) {
2181        self.commander_cast_count += 1;
2182    }
2183
2184    pub fn set_kicked(&mut self, kicked: bool) {
2185        self.kicked = kicked;
2186    }
2187
2188    pub fn mark_enlisted_this_combat(&mut self) {
2189        self.enlisted_this_combat = true;
2190    }
2191
2192    pub fn add_enlisted_power(&mut self, amount: i32) {
2193        self.power_modifier += amount;
2194    }
2195
2196    pub fn add_damage_source_this_turn(&mut self, source: CardId) {
2197        self.damage_sources_this_turn.push(source);
2198    }
2199
2200    pub fn mark_deathtouch_damage(&mut self) {
2201        self.has_deathtouch_damage = true;
2202    }
2203
2204    pub fn clear_deathtouch_damage(&mut self) {
2205        self.has_deathtouch_damage = false;
2206    }
2207
2208    pub fn clear_damage(&mut self) {
2209        self.damage = 0;
2210    }
2211
2212    pub fn reset_turn_modifiers(&mut self) {
2213        self.power_modifier = 0;
2214        self.toughness_modifier = 0;
2215    }
2216
2217    pub fn reset_regeneration_shields(&mut self) {
2218        self.regeneration_shields = 0;
2219    }
2220
2221    pub fn clear_original_controller_eot(&mut self) {
2222        self.original_controller_eot = None;
2223    }
2224
2225    pub fn set_chosen_modes(&mut self, modes: Vec<usize>) {
2226        self.chosen_modes = Some(modes);
2227    }
2228
2229    pub fn set_chosen_cards(&mut self, cards: Vec<CardId>) {
2230        self.chosen_cards = cards;
2231    }
2232
2233    pub fn set_chosen_number(&mut self, number: Option<i32>) {
2234        self.chosen_number = number;
2235    }
2236
2237    pub fn set_chosen_player(
2238        &mut self,
2239        player: Option<PlayerId>,
2240        chooser: Option<PlayerId>,
2241        revealed: bool,
2242    ) {
2243        self.chosen_player = player;
2244        self.chosen_player_controller = chooser;
2245        self.chosen_player_revealed = revealed;
2246    }
2247
2248    pub fn set_chosen_type(
2249        &mut self,
2250        chosen_type: Option<String>,
2251        chooser: Option<PlayerId>,
2252        revealed: bool,
2253    ) {
2254        self.chosen_type = chosen_type;
2255        self.chosen_type_controller = chooser;
2256        self.chosen_type_revealed = revealed;
2257    }
2258
2259    pub fn set_strive_extra_targets(&mut self, value: u32) {
2260        self.strive_extra_targets = value;
2261    }
2262
2263    pub fn set_colors_spent_to_cast(&mut self, colors: u16) {
2264        self.colors_spent_to_cast = colors;
2265    }
2266
2267    pub fn set_paying_mana_to_cast(&mut self, paying_mana: Vec<u16>) {
2268        self.paying_mana_to_cast = paying_mana;
2269    }
2270
2271    pub fn set_promised_gift(&mut self, player: Option<PlayerId>) {
2272        self.promised_gift = player;
2273    }
2274
2275    pub fn set_lki_power_toughness(&mut self, power: Option<i32>, toughness: Option<i32>) {
2276        self.lki_power = power;
2277        self.lki_toughness = toughness;
2278    }
2279
2280    pub fn restore_animate_snapshot(
2281        &mut self,
2282        type_line: CardTypeLine,
2283        base_power: Option<i32>,
2284        base_toughness: Option<i32>,
2285        color: ColorSet,
2286    ) {
2287        self.set_type_line(type_line);
2288        self.base_power = base_power;
2289        self.base_toughness = base_toughness;
2290        self.color = color;
2291    }
2292
2293    pub fn capture_clone_state(&self) -> CloneState {
2294        CloneState {
2295            expires_at_cleanup: false,
2296            original_card_name: self.card_name.clone(),
2297            original_oracle_text: self.oracle_text.clone(),
2298            original_type_line: self.type_line.clone(),
2299            original_mana_cost: self.mana_cost.clone(),
2300            original_color: self.color,
2301            original_base_power: self.base_power,
2302            original_base_toughness: self.base_toughness,
2303            original_keywords: self.keywords.clone(),
2304            original_abilities: self.abilities.clone(),
2305            original_activated_abilities: self.activated_abilities.clone(),
2306            original_triggers: self.triggers.clone(),
2307            original_svars: self.svars.clone(),
2308            original_static_abilities: self.static_abilities.clone(),
2309            original_replacement_effects: self.replacement_effects.clone(),
2310            original_base_ability_count: self.base_ability_count,
2311            original_base_trigger_count: self.base_trigger_count,
2312        }
2313    }
2314
2315    pub fn restore_clone_snapshot(&mut self, state: CloneState) {
2316        self.card_name = state.original_card_name;
2317        self.oracle_text = state.original_oracle_text;
2318        self.type_line = state.original_type_line;
2319        self.mana_cost = state.original_mana_cost;
2320        self.color = state.original_color;
2321        self.base_power = state.original_base_power;
2322        self.base_toughness = state.original_base_toughness;
2323        self.keywords = state.original_keywords;
2324        self.abilities = state.original_abilities;
2325        self.activated_abilities = state.original_activated_abilities;
2326        self.triggers = state.original_triggers;
2327        self.svars = state.original_svars;
2328        self.static_abilities = state.original_static_abilities;
2329        self.replacement_effects = state.original_replacement_effects;
2330        self.base_ability_count = state.original_base_ability_count;
2331        self.base_trigger_count = state.original_base_trigger_count;
2332        self.parsed_svar_cache.clear();
2333        self.refresh_action_specs();
2334        self.ensure_crew_activated_ability();
2335        self.remove_clone_state();
2336    }
2337
2338    pub fn set_counters_map(&mut self, counters: BTreeMap<CounterType, i32>) {
2339        self.counters = counters;
2340    }
2341
2342    pub fn set_zone(&mut self, zone: ZoneType) {
2343        self.zone = zone;
2344    }
2345
2346    pub fn set_color(&mut self, color: ColorSet) {
2347        self.color = color;
2348    }
2349
2350    pub fn set_animate_state(&mut self, state: Option<AnimateState>) {
2351        self.animate_state = state;
2352    }
2353
2354    pub fn set_clone_state(&mut self, state: Option<CloneState>) {
2355        self.clone_state = state;
2356    }
2357
2358    pub fn set_exiled_by(&mut self, source: Option<CardId>) {
2359        self.exiled_by = source;
2360    }
2361
2362    pub fn set_attached_to(&mut self, target: Option<CardId>) {
2363        self.attached_to = target;
2364    }
2365
2366    pub fn set_original_controller_eot(&mut self, controller: Option<PlayerId>) {
2367        self.original_controller_eot = controller;
2368    }
2369
2370    pub fn set_class_level(&mut self, level: i32) {
2371        self.class_level = level;
2372    }
2373
2374    pub fn set_paired_with(&mut self, pair: Option<CardId>) {
2375        self.paired_with = pair;
2376    }
2377
2378    pub fn set_must_block(&mut self, must_block: bool) {
2379        self.must_block = must_block;
2380    }
2381
2382    pub fn set_detained(&mut self, detained: bool) {
2383        self.detained = detained;
2384    }
2385
2386    pub fn set_goaded_by(&mut self, player: Option<PlayerId>) {
2387        self.goaded_by = player;
2388    }
2389
2390    pub fn set_phased_out(&mut self, phased_out: bool) {
2391        self.phased_out = phased_out;
2392    }
2393
2394    pub fn set_base_power(&mut self, power: Option<i32>) {
2395        self.base_power = power;
2396    }
2397
2398    pub fn set_base_toughness(&mut self, toughness: Option<i32>) {
2399        self.base_toughness = toughness;
2400    }
2401
2402    pub fn set_base_pt(&mut self, power: Option<i32>, toughness: Option<i32>) {
2403        self.base_power = power;
2404        self.base_toughness = toughness;
2405    }
2406
2407    pub fn capture_changed_characteristics_baseline_if_needed(&mut self) {
2408        if self.changed_type_line_base.is_none() {
2409            self.changed_type_line_base = Some(self.type_line.clone());
2410        }
2411        if self.changed_base_power.is_none() {
2412            self.changed_base_power = Some(self.base_power);
2413        }
2414        if self.changed_base_toughness.is_none() {
2415            self.changed_base_toughness = Some(self.base_toughness);
2416        }
2417    }
2418
2419    pub fn restore_changed_characteristics_baseline(&mut self) {
2420        if let Some(type_line) = self.changed_type_line_base.take() {
2421            self.set_type_line(type_line);
2422        }
2423        if let Some(power) = self.changed_base_power.take() {
2424            self.base_power = power;
2425        }
2426        if let Some(toughness) = self.changed_base_toughness.take() {
2427            self.base_toughness = toughness;
2428        }
2429    }
2430
2431    pub fn set_static_set_pt(&mut self, power: Option<i32>, toughness: Option<i32>) {
2432        self.static_set_power = power;
2433        self.static_set_toughness = toughness;
2434    }
2435
2436    pub fn set_power_modifier(&mut self, amount: i32) {
2437        self.power_modifier = amount;
2438    }
2439
2440    pub fn set_toughness_modifier(&mut self, amount: i32) {
2441        self.toughness_modifier = amount;
2442    }
2443
2444    pub fn set_card_name(&mut self, name: impl Into<String>) {
2445        self.card_name = name.into();
2446    }
2447
2448    pub fn set_mana_cost(&mut self, mana_cost: ManaCost) {
2449        self.mana_cost = mana_cost;
2450    }
2451
2452    pub fn set_abilities(&mut self, abilities: Vec<String>) {
2453        self.abilities = abilities;
2454        self.update_spell_abilities();
2455        self.refresh_action_specs();
2456    }
2457
2458    pub fn set_static_abilities(&mut self, abilities: Vec<StaticAbility>) {
2459        self.static_abilities = abilities;
2460    }
2461
2462    pub fn set_triggers(&mut self, triggers: Vec<Trigger>) {
2463        self.triggers = triggers;
2464        self.base_trigger_count = self.triggers.len();
2465    }
2466
2467    pub fn set_replacement_effects(&mut self, effects: Vec<ReplacementEffect>) {
2468        self.replacement_effects = effects;
2469    }
2470
2471    pub fn set_renowned(&mut self, renowned: bool) {
2472        self.is_renowned = renowned;
2473    }
2474
2475    pub fn set_monstrous(&mut self, monstrous: bool) {
2476        self.monstrous = monstrous;
2477    }
2478
2479    pub fn clear_granted_keywords(&mut self) {
2480        self.granted_keywords.clear();
2481    }
2482
2483    pub fn clear_pump_keywords(&mut self) {
2484        self.pump_keywords.clear();
2485    }
2486
2487    pub fn increment_pump_trigger_count(&mut self) {
2488        self.pump_trigger_count += 1;
2489    }
2490
2491    pub fn add_remembered_cards<I>(&mut self, cards: I)
2492    where
2493        I: IntoIterator<Item = CardId>,
2494    {
2495        for card in cards {
2496            self.add_remembered_card(card);
2497        }
2498    }
2499
2500    pub fn clear_chosen_colors(&mut self) {
2501        self.chosen_colors.clear();
2502    }
2503
2504    pub fn add_chosen_color(&mut self, color: impl Into<String>) {
2505        self.chosen_colors.push(color.into());
2506    }
2507
2508    pub fn add_chosen_card(&mut self, card: CardId) {
2509        if !self.chosen_cards.contains(&card) {
2510            self.chosen_cards.push(card);
2511        }
2512    }
2513
2514    pub fn add_pump_keyword(&mut self, keyword: &str) {
2515        self.pump_keywords.add(keyword);
2516    }
2517
2518    pub fn add_granted_keyword(&mut self, keyword: &str) {
2519        self.granted_keywords.add(keyword);
2520    }
2521
2522    pub fn was_turned_face_up_this_turn(&self) -> bool {
2523        !self.face_down && (self.manifested || self.cloaked)
2524    }
2525
2526    pub fn can_transform(&self) -> bool {
2527        self.other_part.is_some()
2528    }
2529
2530    pub fn has_name_overwrite(&self) -> bool {
2531        false
2532    }
2533
2534    pub fn has_non_legendary_creature_names(&self) -> bool {
2535        false
2536    }
2537
2538    pub fn add_changed_name(&mut self, name: &str) {
2539        if !self.has_s_var("OriginalName") {
2540            self.set_s_var("OriginalName", self.card_name.clone());
2541        }
2542        self.card_name = name.to_string();
2543    }
2544
2545    pub fn remove_changed_name(&mut self) {
2546        if let Some(orig) = self.svars.get("OriginalName").cloned() {
2547            self.card_name = orig;
2548        }
2549    }
2550
2551    pub fn clear_changed_name(&mut self) {
2552        self.remove_s_var("OriginalName");
2553    }
2554
2555    pub fn add_devoured(&mut self, card_id: CardId) {
2556        self.add_remembered_card(card_id);
2557        self.set_s_var("Devoured", "True");
2558    }
2559    pub fn add_exploited(&mut self, card_id: CardId) {
2560        self.add_remembered_card(card_id);
2561        self.set_s_var("Exploited", "True");
2562    }
2563    pub fn add_delved(&mut self, card_id: CardId) {
2564        self.add_remembered_card(card_id);
2565        self.set_s_var("Delved", "True");
2566    }
2567    pub fn clear_delved(&mut self) {
2568        self.remove_s_var("Delved");
2569    }
2570    pub fn retain_paid_list(&mut self) {
2571        self.remembered_cards.retain(|_| true);
2572    }
2573    pub fn add_stored_rolls(&mut self, roll: i32) {
2574        self.add_remembered_cmc(roll);
2575    }
2576    pub fn replace_stored_roll(&mut self, from: i32, to: i32) {
2577        for roll in &mut self.remembered_cmc {
2578            if *roll == from {
2579                *roll = to;
2580            }
2581        }
2582    }
2583    pub fn add_flip_result(&mut self, heads: bool) {
2584        self.set_s_var("FlipResult", if heads { "Heads" } else { "Tails" });
2585    }
2586    pub fn clear_flip_result(&mut self) {
2587        self.remove_s_var("FlipResult");
2588    }
2589    pub fn add_blocked_this_turn(&mut self, card_id: CardId) {
2590        self.add_remembered_card(card_id);
2591        self.set_s_var("BlockedThisTurn", "True");
2592    }
2593    pub fn clear_blocked_this_turn(&mut self) {
2594        self.remove_s_var("BlockedThisTurn");
2595    }
2596    pub fn add_blocked_by_this_turn(&mut self, card_id: CardId) {
2597        self.add_remembered_card(card_id);
2598        self.set_s_var("BlockedByThisTurn", "True");
2599    }
2600    pub fn clear_blocked_by_this_turn(&mut self) {
2601        self.remove_s_var("BlockedByThisTurn");
2602    }
2603
2604    pub fn add_must_block_card(&mut self, card_id: CardId) {
2605        if !self.must_block_cards.contains(&card_id) {
2606            self.must_block_cards.push(card_id);
2607        }
2608    }
2609
2610    pub fn add_must_block_cards(&mut self, cards: impl IntoIterator<Item = CardId>) {
2611        for c in cards {
2612            self.add_must_block_card(c);
2613        }
2614    }
2615
2616    pub fn remove_must_block_cards(&mut self, cards: impl IntoIterator<Item = CardId>) {
2617        let remove: HashSet<CardId> = cards.into_iter().collect();
2618        self.must_block_cards.retain(|c| !remove.contains(c));
2619    }
2620
2621    pub fn clear_must_block_cards(&mut self) {
2622        self.must_block_cards.clear();
2623    }
2624
2625    pub fn has_second_strike(&self) -> bool {
2626        self.has_double_strike()
2627    }
2628
2629    pub fn has_suspend(&self) -> bool {
2630        self.has_keyword("Suspend")
2631    }
2632
2633    pub fn has_converge(&self) -> bool {
2634        self.has_keyword("Converge")
2635    }
2636
2637    pub fn can_receive_counters(&self, _counter: &CounterType) -> bool {
2638        true
2639    }
2640
2641    pub fn can_remove_counters(&self, counter: &CounterType) -> bool {
2642        self.counter_count(counter) > 0
2643    }
2644
2645    pub fn add_counter_internal(&mut self, counter: &CounterType, amount: i32) {
2646        self.add_counter(counter, amount);
2647    }
2648
2649    pub fn create_counter_static(&mut self) {
2650        self.put_etb_counters();
2651    }
2652
2653    pub fn subtract_counter(&mut self, counter: &CounterType, amount: i32) {
2654        self.remove_counter(counter, amount);
2655    }
2656
2657    pub fn clear_counters(&mut self) {
2658        self.counters.clear();
2659    }
2660
2661    pub fn sum_all_counters(&self) -> i32 {
2662        self.counters.values().sum()
2663    }
2664
2665    pub fn put_etb_counters(&mut self) {
2666        if self.etb_counters_p1p1 > 0 {
2667            self.add_counter(&CounterType::P1P1, self.etb_counters_p1p1);
2668            self.etb_counters_p1p1 = 0;
2669        }
2670    }
2671
2672    pub fn copy_changed_s_vars_from(&mut self, other: &Card) {
2673        self.set_svars_map(other.svars.clone());
2674    }
2675
2676    pub fn add_changed_s_vars(&mut self, key: &str, value: &str) {
2677        self.set_s_var(key, value);
2678    }
2679
2680    pub fn remove_changed_s_vars(&mut self, key: &str) {
2681        self.remove_s_var(key);
2682    }
2683
2684    pub fn add_changed_mana_cost(&mut self, mana_cost: &str) {
2685        if !self.has_s_var("OriginalManaCost") {
2686            self.set_s_var("OriginalManaCost", self.mana_cost.to_string());
2687        }
2688        self.mana_cost = ManaCost::parse(mana_cost);
2689        self.calculate_perpetual_adjusted_mana_cost();
2690        self.update_mana_cost_for_view();
2691    }
2692    pub fn remove_changed_mana_cost(&mut self, _timestamp: i64, _static_id: i64) -> bool {
2693        let Some(original) = self.svars.get("OriginalManaCost").cloned() else {
2694            return false;
2695        };
2696        let before = self.mana_cost.clone();
2697        self.mana_cost = ManaCost::parse(&original);
2698        self.calculate_perpetual_adjusted_mana_cost();
2699        self.update_mana_cost_for_view();
2700        self.remove_s_var("OriginalManaCost");
2701        self.mana_cost != before
2702    }
2703
2704    pub fn cleanup_exiled_with(&mut self) {
2705        self.exiled_by = None;
2706    }
2707
2708    pub fn has_paper_foil(&self) -> bool {
2709        self.paper_foil
2710    }
2711
2712    pub fn has_marked_color(&self) -> bool {
2713        !self.color.is_colorless()
2714    }
2715
2716    pub fn can_produce_color_mana(
2717        &self,
2718        game: &GameState,
2719        colors: &std::collections::HashSet<String>,
2720    ) -> bool {
2721        crate::card::card_util::card_can_produce_color_mana(game, self.id, colors)
2722    }
2723
2724    pub fn can_produce_same_mana_type_with(&self, game: &GameState, other: &Card) -> bool {
2725        crate::card::card_util::card_can_produce_same_mana_type_with(game, self.id, other.id)
2726    }
2727
2728    pub fn has_remove_intrinsic(&self) -> bool {
2729        false
2730    }
2731
2732    pub fn update_spell_abilities(&mut self) {
2733        self.activated_abilities.clear();
2734        for (i, raw) in self.abilities.iter().enumerate() {
2735            if let Some(parsed) = crate::ability::activated::parse_activated_ability(raw, i) {
2736                self.activated_abilities.push(parsed);
2737            }
2738        }
2739    }
2740
2741    pub fn refresh_action_specs(&mut self) {
2742        let mut spell_specs = Vec::new();
2743        let mut spell_cost = None;
2744        let mut ai_phyrexian_payment = None;
2745        let mut spree_min_mode_cost = None;
2746
2747        for (ability_index, raw) in self.abilities.iter().enumerate() {
2748            let parsed = ParsedParams::parse(raw);
2749            if ai_phyrexian_payment.is_none() {
2750                ai_phyrexian_payment = parsed.get(keys::AI_PHYREXIAN_PAYMENT).map(str::to_string);
2751            }
2752            let Some(sp_kind) = parsed.get(keys::SP) else {
2753                continue;
2754            };
2755            let cost_contains_x = parsed
2756                .get(keys::COST)
2757                .is_some_and(|cost| cost.contains('X'));
2758            if spell_cost.is_none() {
2759                spell_cost = parsed.get(keys::COST).map(parse_cost);
2760            }
2761            spell_specs.push(CardActionSpellSpec {
2762                ability_index,
2763                has_valid_tgts: parsed.has(keys::VALID_TGTS),
2764                cost_contains_x,
2765                target_chain: self.collect_action_target_chain(raw),
2766            });
2767
2768            if sp_kind.eq_ignore_ascii_case("Charm") {
2769                if let Some(choices) = parsed.get(keys::CHOICES) {
2770                    let min_mode_cost = choices
2771                        .split(',')
2772                        .filter_map(|name| {
2773                            self.svars.get(name.trim()).and_then(|svar_val| {
2774                                ParsedParams::parse(svar_val)
2775                                    .get(keys::MODE_COST)
2776                                    .map(|cost| forge_foundation::ManaCost::parse(cost).cmc())
2777                            })
2778                        })
2779                        .min();
2780                    spree_min_mode_cost = spree_min_mode_cost.or(min_mode_cost);
2781                }
2782            }
2783        }
2784
2785        self.action_spell_specs = spell_specs;
2786        self.action_spell_cost = spell_cost;
2787        self.ai_phyrexian_payment = ai_phyrexian_payment;
2788        self.spree_min_mode_cost = spree_min_mode_cost;
2789    }
2790
2791    fn refresh_action_specs_after_svar_change(&mut self) {
2792        if self.action_spell_specs.is_empty() {
2793            return;
2794        }
2795        self.refresh_action_specs();
2796    }
2797
2798    fn collect_action_target_chain(&self, ability_text: &str) -> Vec<CardActionTargetSpec> {
2799        let mut specs = Vec::new();
2800        let mut current = Some(ability_text.to_string());
2801
2802        while let Some(text) = current {
2803            let parsed = ParsedParams::parse(&text);
2804            let params = Params::from_parsed(&parsed);
2805            if let Some(target_restrictions) = TargetRestrictions::new_from_parsed(&parsed, &params)
2806            {
2807                specs.push(CardActionTargetSpec {
2808                    min_targets: parse_literal_target_count(&target_restrictions.min_targets),
2809                    target_restrictions,
2810                });
2811            }
2812
2813            current = parsed
2814                .get(keys::SUB_ABILITY)
2815                .and_then(|name| self.svars.get(name.trim()))
2816                .cloned();
2817        }
2818
2819        specs
2820    }
2821
2822    pub fn inc_shield_count(&mut self) {
2823        self.damage_prevention += 1;
2824    }
2825
2826    pub fn dec_shield_count(&mut self) {
2827        self.damage_prevention = (self.damage_prevention - 1).max(0);
2828    }
2829
2830    pub fn reset_shield_count(&mut self) {
2831        self.damage_prevention = 0;
2832    }
2833
2834    pub fn add_regenerated_this_turn(&mut self) {
2835        self.regeneration_shields += 1;
2836    }
2837
2838    pub fn can_be_shielded(&self) -> bool {
2839        self.is_permanent()
2840    }
2841
2842    pub fn add_untap_command(&mut self) {
2843        self.set_s_var("_cmd_untap", "1");
2844    }
2845    pub fn add_unattach_command(&mut self) {
2846        self.set_s_var("_cmd_unattach", "1");
2847    }
2848    pub fn add_faceup_command(&mut self) {
2849        self.set_s_var("_cmd_faceup", "1");
2850    }
2851    pub fn add_facedown_command(&mut self) {
2852        self.set_s_var("_cmd_facedown", "1");
2853    }
2854    pub fn add_change_controller_command(&mut self) {
2855        self.set_s_var("_cmd_change_controller", "1");
2856    }
2857    pub fn add_phase_out_command(&mut self) {
2858        self.set_s_var("_cmd_phase_out", "1");
2859    }
2860    pub fn add_leaves_play_command(&mut self) {
2861        self.set_s_var("_cmd_leaves_play", "1");
2862    }
2863    pub fn add_static_command_list(&mut self) {
2864        self.set_s_var("_cmd_static", "1");
2865    }
2866    pub fn run_leaves_play_commands(&mut self) {
2867        if self.has_s_var("_cmd_leaves_play") {
2868            self.cleanup_exiled_with();
2869            self.remove_s_var("_cmd_leaves_play");
2870        }
2871    }
2872    pub fn run_untap_commands(&mut self) {
2873        if self.has_s_var("_cmd_untap") {
2874            self.untap();
2875            self.remove_s_var("_cmd_untap");
2876        }
2877    }
2878    pub fn run_unattach_commands(&mut self) {
2879        if self.has_s_var("_cmd_unattach") {
2880            self.unattach_from_entity();
2881            self.remove_s_var("_cmd_unattach");
2882        }
2883    }
2884    pub fn run_faceup_commands(&mut self) {
2885        if self.has_s_var("_cmd_faceup") {
2886            self.turn_face_up();
2887            self.remove_s_var("_cmd_faceup");
2888        }
2889    }
2890    pub fn run_facedown_commands(&mut self) {
2891        if self.has_s_var("_cmd_facedown") {
2892            self.turn_face_down();
2893            self.remove_s_var("_cmd_facedown");
2894        }
2895    }
2896    pub fn run_change_controller_commands(&mut self) {
2897        if self.has_s_var("_cmd_change_controller") {
2898            self.clear_temp_controllers();
2899            self.remove_s_var("_cmd_change_controller");
2900        }
2901    }
2902    pub fn run_phase_out_commands(&mut self) {
2903        if self.has_s_var("_cmd_phase_out") {
2904            self.phase();
2905            self.remove_s_var("_cmd_phase_out");
2906        }
2907    }
2908
2909    pub fn has_sickness(&self) -> bool {
2910        self.summoning_sick
2911    }
2912
2913    pub fn has_become_target_this_turn(&self) -> bool {
2914        self.became_target_this_turn
2915    }
2916
2917    pub fn add_target_from_this_turn(&mut self) {
2918        self.became_target_this_turn = true;
2919    }
2920
2921    pub fn has_started_the_turn_untapped(&self) -> bool {
2922        !self.started_turn_tapped
2923    }
2924
2925    pub fn came_under_control_since_last_upkeep(&self) -> bool {
2926        self.came_under_control_since_last_upkeep
2927    }
2928
2929    pub fn add_temp_controller(&mut self, player: PlayerId) {
2930        self.temp_controllers.push(player);
2931    }
2932
2933    pub fn remove_temp_controller(&mut self, player: PlayerId) {
2934        self.temp_controllers.retain(|&p| p != player);
2935    }
2936
2937    pub fn clear_temp_controllers(&mut self) {
2938        self.temp_controllers.clear();
2939    }
2940
2941    pub fn clear_controllers(&mut self) {
2942        self.controller = self.owner;
2943        self.clear_temp_controllers();
2944    }
2945
2946    pub fn may_player_look(&self, player: PlayerId) -> bool {
2947        self.may_look_at.contains(&player)
2948    }
2949
2950    pub fn add_may_look_face_down_exile(&mut self, player: PlayerId) {
2951        if !self.may_look_at.contains(&player) {
2952            self.may_look_at.push(player);
2953        }
2954    }
2955
2956    pub fn add_may_look_at(&mut self, player: PlayerId) {
2957        if !self.may_look_at.contains(&player) {
2958            self.may_look_at.push(player);
2959        }
2960    }
2961
2962    pub fn remove_may_look_at(&mut self, player: PlayerId) {
2963        self.may_look_at.retain(|&p| p != player);
2964    }
2965
2966    pub fn add_may_look_temp(&mut self, player: PlayerId) {
2967        self.add_may_look_at(player);
2968    }
2969
2970    pub fn remove_may_look_temp(&mut self, player: PlayerId) {
2971        self.remove_may_look_at(player);
2972    }
2973
2974    pub fn update_may_look(&mut self) {
2975        let mut seen = HashSet::new();
2976        self.may_look_at.retain(|p| seen.insert(*p));
2977    }
2978    pub fn update_may_play(&mut self) {
2979        let mut seen = HashSet::new();
2980        self.may_play.retain(|p| seen.insert(*p));
2981    }
2982
2983    pub fn may_play(&self, player: PlayerId) -> bool {
2984        self.may_play.contains(&player)
2985    }
2986
2987    pub fn remove_may_play(&mut self, player: PlayerId) {
2988        self.may_play.retain(|&p| p != player);
2989    }
2990
2991    pub fn reset_may_play_turn(&mut self) {
2992        self.may_play.clear();
2993    }
2994
2995    pub fn remove_attached_to(&mut self) {
2996        self.attached_to = None;
2997    }
2998
2999    pub fn attach_to_entity(&mut self, host: CardId) {
3000        self.attached_to = Some(host);
3001    }
3002
3003    pub fn add_attachment(&mut self, card_id: CardId) {
3004        if !self.attachments.contains(&card_id) {
3005            self.attachments.push(card_id);
3006        }
3007    }
3008
3009    pub fn remove_attachment(&mut self, card_id: CardId) {
3010        self.attachments.retain(|&id| id != card_id);
3011    }
3012
3013    pub fn unattach_from_entity(&mut self) {
3014        self.attached_to = None;
3015    }
3016
3017    pub fn clear_intrinsic_keywords(&mut self) {
3018        self.keywords.clear();
3019    }
3020
3021    pub fn clear_all_keyword_sets(&mut self) {
3022        self.keywords.clear();
3023        self.pump_keywords.clear();
3024        self.granted_keywords.clear();
3025    }
3026
3027    pub fn clear_subtypes(&mut self) {
3028        self.type_line.subtypes.clear();
3029    }
3030
3031    pub fn clear_changed_card_types(&mut self) {
3032        self.update_types();
3033    }
3034    pub fn clear_changed_card_colors(&mut self) {
3035        self.color = ColorSet::COLORLESS;
3036    }
3037    pub fn add_changed_card_types_by_text(&mut self) {
3038        self.update_types();
3039    }
3040    pub fn remove_changed_card_types_by_text(&mut self) {
3041        self.update_types();
3042    }
3043    pub fn add_changed_card_types(&mut self) {
3044        self.update_types();
3045    }
3046    pub fn remove_changed_card_types(&mut self) {
3047        self.update_types();
3048    }
3049    pub fn update_type_cache(&mut self) {
3050        self.type_line = CardTypeLine::parse(&self.type_line.to_string());
3051    }
3052    pub fn has_changed_card_colors(&self) -> bool {
3053        !self.color.is_colorless()
3054    }
3055    pub fn add_color_by_text(&mut self, color: ColorSet) {
3056        self.add_color(color);
3057    }
3058    pub fn remove_color_by_text(&mut self) {
3059        self.remove_color();
3060    }
3061    pub fn remove_color(&mut self) {
3062        self.color = ColorSet::COLORLESS;
3063    }
3064    pub fn add_clone_state(&mut self) {
3065        self.set_s_var("CloneState", "True");
3066    }
3067    pub fn remove_clone_state(&mut self) {
3068        self.remove_s_var("CloneState");
3069    }
3070    pub fn remove_clone_states(&mut self) {
3071        self.remove_s_var("CloneState");
3072    }
3073    pub fn add_new_pt_by_text(&mut self, p: i32, t: i32) {
3074        self.base_power = Some(p);
3075        self.base_toughness = Some(t);
3076    }
3077    pub fn remove_new_p_tby_text(&mut self) {
3078        self.clear_new_pt();
3079    }
3080    pub fn add_new_pt(&mut self, p: i32, t: i32) {
3081        self.base_power = Some(p);
3082        self.base_toughness = Some(t);
3083    }
3084    pub fn remove_new_pt(&mut self) {
3085        self.clear_new_pt();
3086    }
3087    pub fn clear_new_pt(&mut self) {
3088        self.base_power = None;
3089        self.base_toughness = None;
3090    }
3091    pub fn toughness_assigns_damage(&self) -> bool {
3092        self.has_keyword("CARDNAME assigns combat damage equal to its toughness")
3093    }
3094    pub fn assign_no_combat_damage(&self) -> bool {
3095        self.has_keyword("CARDNAME assigns no combat damage")
3096    }
3097    pub fn add_pt_boost(&mut self, p: i32, t: i32) {
3098        self.power_modifier += p;
3099        self.toughness_modifier += t;
3100    }
3101    pub fn remove_pt_boost(&mut self, p: i32, t: i32) {
3102        self.power_modifier -= p;
3103        self.toughness_modifier -= t;
3104    }
3105    pub fn add_draft_action(&mut self) {
3106        self.set_s_var("DraftAction", "True");
3107    }
3108    pub fn add_intensity(&mut self, v: i32) {
3109        self.intensity += v;
3110    }
3111    pub fn has_intensity(&self) -> bool {
3112        self.intensity > 0
3113    }
3114    pub fn has_perpetual(&self) -> bool {
3115        !self.perpetual.is_empty()
3116    }
3117    pub fn get_perpetual(&self) -> &[PerpetualRecord] {
3118        &self.perpetual
3119    }
3120    pub fn add_perpetual(&mut self, p: PerpetualRecord) {
3121        self.apply_perpetual_record(p, true);
3122    }
3123    pub fn remove_perpetual(&mut self, timestamp: i64) -> bool {
3124        if let Some(idx) = self
3125            .perpetual
3126            .iter()
3127            .position(|p| p.timestamp() == timestamp)
3128        {
3129            self.perpetual.remove(idx);
3130            true
3131        } else {
3132            false
3133        }
3134    }
3135    pub fn set_perpetual(&mut self, old_card: &Card, apply_effects: bool) {
3136        self.perpetual = old_card.perpetual.clone();
3137        if apply_effects {
3138            for p in self.perpetual.clone() {
3139                self.apply_perpetual_record(p, false);
3140            }
3141        }
3142    }
3143    pub fn set_perpetual_from(&mut self, old_card: &Card) {
3144        self.set_perpetual(old_card, true);
3145    }
3146    pub fn apply_perpetual_record(&mut self, p: PerpetualRecord, remember: bool) {
3147        if remember {
3148            self.perpetual.push(p.clone());
3149        }
3150        p.apply_effect(self);
3151    }
3152    pub fn add_trigger_for_static_ability(&mut self, trig: Trigger) {
3153        self.add_trigger(trig);
3154    }
3155    pub fn visit_keywords(&self) -> Vec<String> {
3156        self.keywords.as_string_list()
3157    }
3158    pub fn update_keywords(&mut self) {
3159        self.update_keywords_cache();
3160    }
3161    pub fn add_changed_card_keywords(&mut self, kw: &str) {
3162        self.add_intrinsic_keyword(kw);
3163    }
3164    pub fn add_keyword_for_static_ability(&mut self, kw: &str) {
3165        self.granted_keywords.add(kw);
3166    }
3167    pub fn add_changed_card_keywords_by_text(&mut self, kw: &str) {
3168        self.add_intrinsic_keyword(kw);
3169    }
3170    pub fn add_changed_card_keywords_internal(&mut self, kw: &str) {
3171        self.add_intrinsic_keyword(kw);
3172    }
3173    pub fn remove_changed_card_keywords(&mut self, kw: &str) {
3174        self.remove_intrinsic_keyword(kw);
3175    }
3176    pub fn remove_changed_card_keywords_by_text(&mut self, kw: &str) {
3177        self.remove_intrinsic_keyword(kw);
3178    }
3179    pub fn clear_changed_card_keywords(&mut self) {
3180        self.keywords.clear();
3181    }
3182    pub fn clear_static_changed_card_keywords(&mut self) {
3183        self.granted_keywords.clear();
3184    }
3185    pub fn add_hidden_extrinsic_keywords(&mut self, kw: &str) {
3186        self.granted_keywords.add(kw);
3187    }
3188    pub fn remove_hidden_extrinsic_keywords(&mut self, kw: &str) {
3189        self.granted_keywords.remove(kw);
3190    }
3191    pub fn remove_hidden_extrinsic_keyword(&mut self, kw: &str) {
3192        self.granted_keywords.remove(kw);
3193    }
3194    pub fn has_start_of_keyword(&self, prefix: &str) -> bool {
3195        self.keywords.iter_strings().any(|k| k.starts_with(prefix))
3196    }
3197    pub fn has_start_of_un_hidden_keyword(&self, prefix: &str) -> bool {
3198        self.has_start_of_keyword(prefix)
3199    }
3200    pub fn has_any_keyword(&self) -> bool {
3201        !self.keywords.as_string_list().is_empty()
3202            || !self.granted_keywords.as_string_list().is_empty()
3203            || !self.pump_keywords.as_string_list().is_empty()
3204    }
3205    pub fn add_cant_have_keyword(&mut self, kw: &str) {
3206        self.cant_have_keywords.insert(kw.to_ascii_lowercase());
3207    }
3208    pub fn remove_cant_have_keyword(&mut self, kw: &str) {
3209        self.cant_have_keywords.remove(&kw.to_ascii_lowercase());
3210    }
3211    pub fn add_changed_text_color_word(&mut self, from: &str, to: &str) {
3212        self.set_s_var(format!("TextColor:{from}"), to);
3213    }
3214    pub fn remove_changed_text_color_word(&mut self, from: &str) {
3215        self.remove_s_var(&format!("TextColor:{from}"));
3216    }
3217    pub fn add_changed_text_type_word(&mut self, from: &str, to: &str) {
3218        self.set_s_var(format!("TextType:{from}"), to);
3219    }
3220    pub fn remove_changed_text_type_word(&mut self, from: &str) {
3221        self.remove_s_var(&format!("TextType:{from}"));
3222    }
3223    pub fn copy_changed_text_from(&mut self, other: &Card) {
3224        for (k, v) in &other.svars {
3225            if k.starts_with("TextColor:") || k.starts_with("TextType:") {
3226                self.svars.insert(k.clone(), v.clone());
3227            }
3228        }
3229    }
3230    pub fn has_playable_land_face(&self) -> bool {
3231        self.is_land()
3232            || self
3233                .other_part
3234                .as_ref()
3235                .map(|p| p.type_line.is_land())
3236                .unwrap_or(false)
3237    }
3238    pub fn phase(&mut self) {
3239        self.phased_out = !self.phased_out;
3240    }
3241    pub fn associated_with_color(&self, game: &GameState, color: &str) -> bool {
3242        let mut colors = HashSet::new();
3243        colors.insert(color.to_string());
3244        forge_foundation::Color::from_name(&color.to_ascii_lowercase())
3245            .map(|parsed| self.color.has_any_color(parsed.mask()))
3246            .unwrap_or(false)
3247            || self.can_produce_color_mana(game, &colors)
3248    }
3249    pub fn has_no_name(&self) -> bool {
3250        self.card_name.trim().is_empty()
3251    }
3252    pub fn shares_name_with(&self, other: &Card) -> bool {
3253        self.card_name.eq_ignore_ascii_case(&other.card_name)
3254    }
3255    pub fn has_creature_type(&self, creature_type: &str) -> bool {
3256        if !self.is_creature() && !self.type_line.core_types.contains(&CoreType::Kindred) {
3257            return false;
3258        }
3259        if self.type_line.has_subtype(creature_type) {
3260            return true;
3261        }
3262        self.has_keyword("Changeling") && crate::game::TypeRegistry::is_creature_type(creature_type)
3263    }
3264    pub fn has_subtype(&self, subtype: &str) -> bool {
3265        self.type_line.has_subtype(subtype) || self.has_creature_type(subtype)
3266    }
3267    pub fn shares_color_with(&self, other: &Card) -> bool {
3268        (self.color.has_white() && other.color.has_white())
3269            || (self.color.has_blue() && other.color.has_blue())
3270            || (self.color.has_black() && other.color.has_black())
3271            || (self.color.has_red() && other.color.has_red())
3272            || (self.color.has_green() && other.color.has_green())
3273            || (self.color.is_colorless() && other.color.is_colorless())
3274    }
3275    pub fn shares_cmc_with(&self, other: &Card) -> bool {
3276        self.mana_value() == other.mana_value()
3277    }
3278    pub fn shares_creature_type_with(&self, other: &Card) -> bool {
3279        crate::game::TypeRegistry::creature_types()
3280            .iter()
3281            .any(|creature_type| {
3282                self.has_creature_type(creature_type) && other.has_creature_type(creature_type)
3283            })
3284    }
3285    pub fn shares_land_type_with(&self, other: &Card) -> bool {
3286        self.shares_creature_type_with(other) && self.is_land() && other.is_land()
3287    }
3288    pub fn shares_permanent_type_with(&self, other: &Card) -> bool {
3289        (self.is_creature() && other.is_creature())
3290            || (self.is_land() && other.is_land())
3291            || (self.type_line.is_artifact() && other.type_line.is_artifact())
3292            || (self.type_line.is_enchantment() && other.type_line.is_enchantment())
3293            || (self.type_line.is_planeswalker() && other.type_line.is_planeswalker())
3294    }
3295    pub fn shares_card_type_with(&self, other: &Card) -> bool {
3296        self.shares_permanent_type_with(other)
3297    }
3298    pub fn shares_all_card_types_with(&self, other: &Card) -> bool {
3299        self.type_line.core_types == other.type_line.core_types
3300    }
3301    pub fn shares_controller_with(&self, other: &Card) -> bool {
3302        self.controller == other.controller
3303    }
3304    pub fn has_a_basic_land_type(&self) -> bool {
3305        self.type_line.has_subtype("Plains")
3306            || self.type_line.has_subtype("Island")
3307            || self.type_line.has_subtype("Swamp")
3308            || self.type_line.has_subtype("Mountain")
3309            || self.type_line.has_subtype("Forest")
3310    }
3311    pub fn has_a_non_basic_land_type(&self) -> bool {
3312        self.is_land() && !self.has_a_basic_land_type()
3313    }
3314    pub fn has_dealt_damage_to_opponent_this_turn(&self) -> bool {
3315        self.total_damage_done_this_turn > 0
3316    }
3317    pub fn has_been_dealt_deathtouch_damage(&self) -> bool {
3318        self.has_deathtouch_damage
3319    }
3320    pub fn has_been_dealt_excess_damage_this_turn(&self) -> bool {
3321        self.damage > self.toughness()
3322    }
3323    pub fn log_excess_damage(&mut self) {
3324        self.set_s_var("ExcessDamageLogged", "True");
3325    }
3326    pub fn add_assigned_damage(&mut self, amount: i32) {
3327        self.assigned_damage += amount;
3328    }
3329    pub fn clear_assigned_damage(&mut self) {
3330        self.assigned_damage = 0;
3331    }
3332    pub fn can_damage_prevented(&self) -> bool {
3333        !self.has_keyword("Damage can't be prevented")
3334    }
3335    pub fn static_replace_damage(&self, amount: i32) -> i32 {
3336        amount
3337    }
3338    pub fn add_damage_after_prevention(&mut self, amount: i32) -> i32 {
3339        let dealt = if self.can_be_dealt_damage() {
3340            amount.max(0)
3341        } else {
3342            0
3343        };
3344        if dealt <= 0 {
3345            return 0;
3346        }
3347        if self.type_line.is_planeswalker() {
3348            self.remove_counter(&CounterType::Loyalty, dealt);
3349        }
3350        if self.type_line.core_types.contains(&CoreType::Battle) {
3351            self.remove_counter(&CounterType::Named("DEFENSE".to_string()), dealt);
3352        }
3353        if self.is_creature() {
3354            self.damage += dealt;
3355        }
3356        dealt
3357    }
3358    pub fn border_color(&self) -> &'static str {
3359        if self.color.is_colorless() {
3360            "Colorless"
3361        } else if self.color.has_white() {
3362            "White"
3363        } else if self.color.has_blue() {
3364            "Blue"
3365        } else if self.color.has_black() {
3366            "Black"
3367        } else if self.color.has_red() {
3368            "Red"
3369        } else {
3370            "Green"
3371        }
3372    }
3373    pub fn was_discarded(&self) -> bool {
3374        self.discarded
3375    }
3376    pub fn was_surveilled(&self) -> bool {
3377        self.surveilled
3378    }
3379    pub fn was_milled(&self) -> bool {
3380        self.milled
3381    }
3382    pub fn clear_ring_bearer(&mut self) {
3383        self.remove_s_var("RingBearer");
3384    }
3385    pub fn add_saddled_by_this_turn(&mut self, card: CardId) {
3386        self.set_s_var("SaddledBy", format!("{}", card.0));
3387    }
3388    pub fn reset_saddled(&mut self) {
3389        self.remove_s_var("SaddledBy");
3390    }
3391    pub fn can_specialize(&self) -> bool {
3392        self.has_keyword("Specialize")
3393    }
3394    pub fn can_crew(&self) -> bool {
3395        self.is_permanent()
3396    }
3397    pub fn reset_times_crewed_this_turn(&mut self) {
3398        self.times_crewed_this_turn = 0;
3399    }
3400    pub fn becomes_crewed(&mut self) {
3401        self.is_crewed = true;
3402        self.times_crewed_this_turn += 1;
3403    }
3404    pub fn reset_crewed(&mut self) {
3405        self.is_crewed = false;
3406    }
3407    pub fn add_crewed_by_this_turn(&mut self, _card: CardId) {
3408        self.times_crewed_this_turn += 1;
3409    }
3410    pub fn visit_attraction(&mut self) {
3411        self.visited_this_turn = true;
3412    }
3413    pub fn was_visited_this_turn(&self) -> bool {
3414        self.visited_this_turn
3415    }
3416    pub fn animate_bestow(&mut self) {
3417        self.is_bestowed = false;
3418    }
3419    pub fn unanimate_bestow(&mut self) {
3420        self.is_bestowed = true;
3421    }
3422    pub fn equals_with_game_timestamp(&self, other: &Card) -> bool {
3423        self.id == other.id && self.zone_timestamp == other.zone_timestamp
3424    }
3425    pub fn update_world_timestamp(&mut self) {
3426        self.zone_timestamp = self.zone_timestamp.saturating_add(1);
3427    }
3428    pub fn can_be_discarded_by(&self, _player: PlayerId) -> bool {
3429        true
3430    }
3431    pub fn can_be_destroyed(&self) -> bool {
3432        !self.has_indestructible()
3433    }
3434    pub fn can_be_targeted_by(&self, _player: PlayerId) -> bool {
3435        true
3436    }
3437    pub fn cant_be_attached_msg(&self) -> Option<String> {
3438        None
3439    }
3440    pub fn can_be_sacrificed_by(&self, _player: PlayerId) -> bool {
3441        true
3442    }
3443    pub fn can_exiled_by(&self, _player: PlayerId) -> bool {
3444        true
3445    }
3446    pub fn update_static_abilities(&mut self) {
3447        self.recompute_changed_card_traits();
3448    }
3449    pub fn update_triggers(&mut self) {
3450        self.recompute_changed_card_traits();
3451    }
3452    pub fn update_replacement_effects(&mut self) {
3453        self.recompute_changed_card_traits();
3454    }
3455    pub fn was_cast(&self) -> bool {
3456        // Mirrors Java `Card.wasCast()`: true iff `castFrom` was set during
3457        // cast resolution. Sneak Attack and other "put onto battlefield"
3458        // effects don't go through the cast pipeline and leave this `None`.
3459        self.cast_from.is_some()
3460    }
3461    pub fn on_end_of_combat(&mut self) {
3462        self.assigned_damage = 0;
3463    }
3464    pub fn on_cleanup_phase(&mut self) {
3465        self.became_target_this_turn = false;
3466        self.visited_this_turn = false;
3467        self.damage_prevention = 0;
3468    }
3469    pub fn has_etb_trigger(&self) -> bool {
3470        self.triggers.iter().any(|t| {
3471            t.kind == crate::trigger::TriggerType::ChangesZone
3472                && t.destination_zone() == Some(ZoneType::Battlefield)
3473        })
3474    }
3475    pub fn has_etb_replacement(&self) -> bool {
3476        self.has_replacement_effect()
3477    }
3478    pub fn can_move_to_command_zone(&self) -> bool {
3479        self.is_commander && self.move_to_command_zone
3480    }
3481    pub fn from_paper_card(&mut self) {
3482        self.is_token = false;
3483    }
3484    pub fn cleanup_copied_changes_from(&mut self) {
3485        self.clear_changed_card_traits();
3486    }
3487    pub fn activated_this_turn(&self) -> bool {
3488        self.ability_activated_this_turn > 0
3489    }
3490    pub fn add_ability_activated(&mut self) {
3491        self.ability_activated_this_turn += 1;
3492    }
3493    pub fn add_ability_activated_for(
3494        &mut self,
3495        ability: Option<&crate::spellability::SpellAbility>,
3496    ) {
3497        self.add_ability_activated_for_with_limit_increase(ability, false);
3498    }
3499    pub fn add_ability_activated_for_with_limit_increase(
3500        &mut self,
3501        ability: Option<&crate::spellability::SpellAbility>,
3502        loyalty_limit_increase: bool,
3503    ) {
3504        if let Some(ability) = ability {
3505            self.number_turn_activations.add(ability);
3506            self.number_game_activations.add(ability);
3507            if ability.ir.pw_ability {
3508                self.add_planeswalker_ability_activated(loyalty_limit_increase);
3509            }
3510        }
3511        self.add_ability_activated();
3512    }
3513    pub fn add_ability_resolved(&mut self) {
3514        self.ability_resolved_this_turn += 1;
3515    }
3516    pub fn add_ability_resolved_for(
3517        &mut self,
3518        ability: Option<&crate::spellability::SpellAbility>,
3519    ) {
3520        if let Some(ability) = ability {
3521            self.number_ability_resolved.add(ability);
3522        }
3523        self.add_ability_resolved();
3524    }
3525    pub fn get_ability_activated_this_turn(
3526        &self,
3527        ability: Option<&crate::spellability::SpellAbility>,
3528    ) -> u32 {
3529        ability
3530            .map(|ability| self.number_turn_activations.get(ability) as u32)
3531            .unwrap_or(0)
3532    }
3533    pub fn get_ability_activated_this_game(
3534        &self,
3535        ability: Option<&crate::spellability::SpellAbility>,
3536    ) -> u32 {
3537        ability
3538            .map(|ability| self.number_game_activations.get(ability) as u32)
3539            .unwrap_or(0)
3540    }
3541    pub fn get_ability_resolved_this_turn(
3542        &self,
3543        ability: Option<&crate::spellability::SpellAbility>,
3544    ) -> u32 {
3545        ability
3546            .map(|ability| self.number_ability_resolved.get(ability) as u32)
3547            .unwrap_or(0)
3548    }
3549    pub fn get_ability_resolved_this_turn_activators(
3550        &self,
3551        ability: Option<&crate::spellability::SpellAbility>,
3552    ) -> Vec<crate::ids::PlayerId> {
3553        ability
3554            .map(|ability| self.number_ability_resolved.get_activators(ability))
3555            .unwrap_or_default()
3556    }
3557    pub fn reset_ability_resolved_this_turn(&mut self) {
3558        self.ability_resolved_this_turn = 0;
3559        self.number_ability_resolved.clear();
3560    }
3561    pub fn add_chosen_modes(&mut self, modes: Vec<usize>, turn: u32) {
3562        self.chosen_modes = Some(modes);
3563        self.chosen_modes_turn = Some(turn);
3564    }
3565    pub fn reset_chosen_mode_turn(&mut self) {
3566        self.chosen_modes_turn = None;
3567        self.chosen_modes = None;
3568    }
3569    pub fn add_planeswalker_ability_activated(&mut self, loyalty_limit_increase: bool) {
3570        self.planeswalker_abilities_activated += 1;
3571        if self.planeswalker_abilities_activated == 2 && loyalty_limit_increase {
3572            self.planeswalker_activation_limit_used = true;
3573        }
3574    }
3575    pub fn planeswalker_activation_limit_used(&self) -> bool {
3576        self.planeswalker_activation_limit_used
3577    }
3578    pub fn reset_activations_per_turn(&mut self) {
3579        self.ability_activated_this_turn = 0;
3580        self.number_turn_activations.clear();
3581        self.planeswalker_abilities_activated = 0;
3582        self.planeswalker_activation_limit_used = false;
3583    }
3584    pub fn add_can_block_additional(&mut self, n: i32) {
3585        self.can_block_additional += n;
3586    }
3587    pub fn remove_can_block_additional(&mut self, n: i32) {
3588        self.can_block_additional = (self.can_block_additional - n).max(0);
3589    }
3590    pub fn can_block_additional(&self) -> i32 {
3591        self.can_block_additional
3592    }
3593    pub fn add_can_block_any(&mut self) {
3594        self.can_block_any = true;
3595    }
3596    pub fn remove_can_block_any(&mut self) {
3597        self.can_block_any = false;
3598    }
3599    pub fn can_block_any(&self) -> bool {
3600        self.can_block_any
3601    }
3602    pub fn ignore_legend_rule(&self) -> bool {
3603        self.ignore_legend_rule_flag
3604    }
3605    pub fn attack_vigilance(&self) -> bool {
3606        self.has_vigilance()
3607    }
3608    pub fn unlock_room(&mut self) {
3609        self.set_s_var("RoomLocked", "False");
3610    }
3611    pub fn lock_room(&mut self) {
3612        self.set_s_var("RoomLocked", "True");
3613    }
3614    pub fn update_rooms(&mut self) {
3615        if !self.has_s_var("RoomLocked") {
3616            self.set_s_var("RoomLocked", "False");
3617        }
3618    }
3619
3620    /// Transform this double-faced card to its other face.
3621    /// Swaps all face-dependent characteristics with `other_part`.
3622    /// No-op if `other_part` is `None`.
3623    /// Mirrors Java's `CardUtil.applyState(card, CardStateName.Backside)`.
3624    /// Mirror of `Card.isModal()`: true only for MDFC (modal) cards, whose back
3625    /// face may be played from hand. Transform / meld backs are not modal.
3626    pub fn is_modal(&self) -> bool {
3627        self.other_part.as_ref().is_some_and(|other| other.is_modal)
3628    }
3629
3630    pub fn transform(&mut self) {
3631        if let Some(other) = self.other_part.as_mut() {
3632            std::mem::swap(&mut self.card_name, &mut other.name);
3633            std::mem::swap(&mut self.oracle_text, &mut other.oracle_text);
3634            std::mem::swap(&mut self.type_line, &mut other.type_line);
3635            std::mem::swap(&mut self.mana_cost, &mut other.mana_cost);
3636            std::mem::swap(&mut self.color, &mut other.color);
3637            std::mem::swap(&mut self.base_power, &mut other.base_power);
3638            std::mem::swap(&mut self.base_toughness, &mut other.base_toughness);
3639            std::mem::swap(&mut self.keywords, &mut other.keywords);
3640            std::mem::swap(&mut self.abilities, &mut other.abilities);
3641            std::mem::swap(&mut self.triggers, &mut other.triggers);
3642            std::mem::swap(&mut self.static_abilities, &mut other.static_abilities);
3643            std::mem::swap(
3644                &mut self.replacement_effects,
3645                &mut other.replacement_effects,
3646            );
3647            std::mem::swap(&mut self.svars, &mut other.svars);
3648
3649            // Reset per-face transient state
3650            self.power_modifier = 0;
3651            self.toughness_modifier = 0;
3652            self.damage = 0;
3653            self.granted_keywords.clear();
3654
3655            // Re-parse activated abilities from new face's abilities
3656            self.activated_abilities = self
3657                .abilities
3658                .iter()
3659                .enumerate()
3660                .filter_map(|(i, raw)| {
3661                    parse_or_warn(parse_activated_ability(raw, i), "ActivatedAbility", raw)
3662                })
3663                .collect();
3664            self.base_ability_count = self.activated_abilities.len();
3665            self.base_trigger_count = self.triggers.len();
3666            self.parsed_svar_cache.clear();
3667            self.refresh_action_specs();
3668
3669            // Re-bind replacement hosts so SVar lookups hit the active face.
3670            let mut res = std::mem::take(&mut self.replacement_effects);
3671            for re in &mut res {
3672                re.set_host_card(self);
3673            }
3674            self.replacement_effects = res;
3675
3676            self.is_transformed = !self.is_transformed;
3677
3678            // Face characteristics changed; reset trait-change baseline and
3679            // re-apply active trait-change layers against the new face.
3680            self.reset_changed_card_traits_baseline();
3681            self.recompute_changed_card_traits();
3682        }
3683    }
3684
3685    fn activated_to_spell_abilities(&self, list: &[ActivatedAbility]) -> Vec<SpellAbility> {
3686        list.iter()
3687            .map(|ab| {
3688                let mut sa = crate::spellability::build_spell_ability_from_host_card(
3689                    self,
3690                    &ab.ability_text,
3691                    self.controller,
3692                );
3693                sa.is_activated = true;
3694                sa
3695            })
3696            .collect()
3697    }
3698
3699    fn spell_to_activated_abilities(list: &[SpellAbility]) -> Vec<ActivatedAbility> {
3700        list.iter()
3701            .enumerate()
3702            .filter_map(|(i, sa)| parse_activated_ability(&sa.ability_text, i))
3703            .collect()
3704    }
3705
3706    fn capture_changed_card_traits_baseline_if_needed(&mut self) {
3707        if self.trait_base_activated_abilities.is_none() {
3708            self.trait_base_activated_abilities = Some(self.activated_abilities.clone());
3709            self.trait_base_triggers = Some(self.triggers.clone());
3710            self.trait_base_replacement_effects = Some(self.replacement_effects.clone());
3711            self.trait_base_static_abilities = Some(self.static_abilities.clone());
3712            self.trait_base_keywords = Some(self.keywords.clone());
3713        }
3714    }
3715
3716    fn reset_changed_card_traits_baseline(&mut self) {
3717        self.trait_base_activated_abilities = Some(self.activated_abilities.clone());
3718        self.trait_base_triggers = Some(self.triggers.clone());
3719        self.trait_base_replacement_effects = Some(self.replacement_effects.clone());
3720        self.trait_base_static_abilities = Some(self.static_abilities.clone());
3721        self.trait_base_keywords = Some(self.keywords.clone());
3722    }
3723
3724    pub(crate) fn reset_changed_card_traits_baseline_to_current(&mut self) {
3725        self.reset_changed_card_traits_baseline();
3726        self.recompute_changed_card_traits();
3727    }
3728
3729    fn recompute_changed_card_traits(&mut self) {
3730        let Some(base_activated) = self.trait_base_activated_abilities.clone() else {
3731            return;
3732        };
3733        let Some(base_triggers) = self.trait_base_triggers.clone() else {
3734            return;
3735        };
3736        let Some(base_replacements) = self.trait_base_replacement_effects.clone() else {
3737            return;
3738        };
3739        let Some(base_static) = self.trait_base_static_abilities.clone() else {
3740            return;
3741        };
3742        let Some(base_keywords) = self.trait_base_keywords.clone() else {
3743            return;
3744        };
3745
3746        let mut spell_abilities = self.activated_to_spell_abilities(&base_activated);
3747        let mut triggers = base_triggers;
3748        let mut replacements = base_replacements;
3749        let mut static_abilities = base_static;
3750        let mut keywords = base_keywords;
3751
3752        for layer in self.changed_card_traits_by_text.values() {
3753            spell_abilities = crate::card::card_state::apply_spell_ability(layer, spell_abilities);
3754            triggers = crate::card::card_state::apply_trigger(layer, triggers);
3755            replacements = crate::card::card_state::apply_replacement_effect(layer, replacements);
3756            static_abilities =
3757                crate::card::card_state::apply_static_ability(layer, static_abilities);
3758            keywords = crate::card::card_state::apply_keywords(layer, keywords);
3759        }
3760        for layer in self.changed_card_traits.values() {
3761            spell_abilities = crate::card::card_state::apply_spell_ability(layer, spell_abilities);
3762            triggers = crate::card::card_state::apply_trigger(layer, triggers);
3763            replacements = crate::card::card_state::apply_replacement_effect(layer, replacements);
3764            static_abilities =
3765                crate::card::card_state::apply_static_ability(layer, static_abilities);
3766            keywords = crate::card::card_state::apply_keywords(layer, keywords);
3767        }
3768
3769        self.activated_abilities = Self::spell_to_activated_abilities(&spell_abilities);
3770        self.triggers = triggers;
3771        self.replacement_effects = replacements;
3772        self.static_abilities = static_abilities;
3773        self.keywords = keywords;
3774    }
3775
3776    /// Java parity: `addChangedCardTraits`.
3777    pub fn add_changed_card_traits(
3778        &mut self,
3779        layer: card_trait_changes::CardTraitChanges,
3780        timestamp: i64,
3781        static_id: i64,
3782    ) {
3783        self.capture_changed_card_traits_baseline_if_needed();
3784        self.changed_card_traits
3785            .insert((timestamp, static_id), layer);
3786        self.recompute_changed_card_traits();
3787    }
3788
3789    /// Java parity: `addChangedCardTraitsByText`.
3790    pub fn add_changed_card_traits_by_text(
3791        &mut self,
3792        layer: card_trait_changes::CardTraitChanges,
3793        timestamp: i64,
3794        static_id: i64,
3795    ) {
3796        self.capture_changed_card_traits_baseline_if_needed();
3797        self.changed_card_traits_by_text
3798            .insert((timestamp, static_id), layer);
3799        self.recompute_changed_card_traits();
3800    }
3801
3802    /// Java parity: `removeChangedCardTraits`.
3803    pub fn remove_changed_card_traits(&mut self, timestamp: i64, static_id: i64) -> bool {
3804        if self
3805            .changed_card_traits
3806            .remove(&(timestamp, static_id))
3807            .is_none()
3808        {
3809            return false;
3810        }
3811        if self.changed_card_traits.is_empty() && self.changed_card_traits_by_text.is_empty() {
3812            if let Some(v) = self.trait_base_activated_abilities.take() {
3813                self.activated_abilities = v;
3814            }
3815            if let Some(v) = self.trait_base_triggers.take() {
3816                self.triggers = v;
3817            }
3818            if let Some(v) = self.trait_base_replacement_effects.take() {
3819                self.replacement_effects = v;
3820            }
3821            if let Some(v) = self.trait_base_static_abilities.take() {
3822                self.static_abilities = v;
3823            }
3824            if let Some(v) = self.trait_base_keywords.take() {
3825                self.keywords = v;
3826            }
3827            return true;
3828        }
3829
3830        self.recompute_changed_card_traits();
3831        true
3832    }
3833
3834    /// Java parity: `removeChangedCardTraitsByText`.
3835    pub fn remove_changed_card_traits_by_text(&mut self, timestamp: i64, static_id: i64) -> bool {
3836        if self
3837            .changed_card_traits_by_text
3838            .remove(&(timestamp, static_id))
3839            .is_none()
3840        {
3841            return false;
3842        }
3843        if self.changed_card_traits.is_empty() && self.changed_card_traits_by_text.is_empty() {
3844            if let Some(v) = self.trait_base_activated_abilities.take() {
3845                self.activated_abilities = v;
3846            }
3847            if let Some(v) = self.trait_base_triggers.take() {
3848                self.triggers = v;
3849            }
3850            if let Some(v) = self.trait_base_replacement_effects.take() {
3851                self.replacement_effects = v;
3852            }
3853            if let Some(v) = self.trait_base_static_abilities.take() {
3854                self.static_abilities = v;
3855            }
3856            if let Some(v) = self.trait_base_keywords.take() {
3857                self.keywords = v;
3858            }
3859            return true;
3860        }
3861
3862        self.recompute_changed_card_traits();
3863        true
3864    }
3865
3866    /// Java parity: `clearChangedCardTraits`.
3867    pub fn clear_changed_card_traits(&mut self) {
3868        self.changed_card_traits.clear();
3869        self.changed_card_traits_by_text.clear();
3870        if let Some(v) = self.trait_base_activated_abilities.take() {
3871            self.activated_abilities = v;
3872        }
3873        if let Some(v) = self.trait_base_triggers.take() {
3874            self.triggers = v;
3875        }
3876        if let Some(v) = self.trait_base_replacement_effects.take() {
3877            self.replacement_effects = v;
3878        }
3879        if let Some(v) = self.trait_base_static_abilities.take() {
3880            self.static_abilities = v;
3881        }
3882        if let Some(v) = self.trait_base_keywords.take() {
3883            self.keywords = v;
3884        }
3885    }
3886
3887    /// Clear continuous static-ability trait changes from the previous layer pass.
3888    ///
3889    /// Static layer effects use negative static IDs so they can be recomputed
3890    /// each pass without disturbing perpetual/card-state trait changes.
3891    pub fn clear_static_layer_changed_card_traits(&mut self) {
3892        let before = self.changed_card_traits.len();
3893        self.changed_card_traits
3894            .retain(|(_, static_id), _| *static_id >= 0);
3895        if self.changed_card_traits.len() == before {
3896            return;
3897        }
3898        if self.changed_card_traits.is_empty() && self.changed_card_traits_by_text.is_empty() {
3899            if let Some(v) = self.trait_base_activated_abilities.take() {
3900                self.activated_abilities = v;
3901            }
3902            if let Some(v) = self.trait_base_triggers.take() {
3903                self.triggers = v;
3904            }
3905            if let Some(v) = self.trait_base_replacement_effects.take() {
3906                self.replacement_effects = v;
3907            }
3908            if let Some(v) = self.trait_base_static_abilities.take() {
3909                self.static_abilities = v;
3910            }
3911            if let Some(v) = self.trait_base_keywords.take() {
3912                self.keywords = v;
3913            }
3914            return;
3915        }
3916
3917        self.recompute_changed_card_traits();
3918    }
3919
3920    pub fn remove_changed_state(&mut self) {
3921        self.clear_changed_card_traits();
3922    }
3923}
3924
3925impl HasSVars for Card {
3926    fn get_svar(&self, name: &str) -> Option<&str> {
3927        self.get_s_var(name)
3928    }
3929
3930    fn set_svar(&mut self, name: String, value: String) {
3931        self.set_s_var(name, value);
3932    }
3933
3934    fn set_svars(&mut self, new_svars: std::collections::HashMap<String, String>) {
3935        self.set_svars_map(new_svars.into_iter().collect());
3936    }
3937
3938    fn get_svars(&self) -> &std::collections::HashMap<String, String> {
3939        panic!("Card::get_svars is not supported yet; use get_s_var/has_s_var parity accessors");
3940    }
3941
3942    fn remove_svar(&mut self, var: &str) {
3943        self.remove_s_var(var);
3944    }
3945}
3946
3947#[cfg(test)]
3948mod tests {
3949    use super::*;
3950    use std::collections::HashSet;
3951
3952    use forge_carddb::parse_card_script;
3953    use forge_foundation::ManaCost;
3954
3955    #[test]
3956    fn card_power_toughness() {
3957        let mut card = Card::new(
3958            CardId(0),
3959            "Test".to_string(),
3960            PlayerId(0),
3961            CardTypeLine::parse("Creature Bear"),
3962            ManaCost::parse("1 G"),
3963            ColorSet::GREEN,
3964            Some(2),
3965            Some(2),
3966            vec![],
3967            vec![],
3968        );
3969        assert_eq!(card.power(), 2);
3970        assert_eq!(card.toughness(), 2);
3971
3972        card.add_counter(&CounterType::P1P1, 1);
3973        assert_eq!(card.power(), 3);
3974        assert_eq!(card.toughness(), 3);
3975    }
3976
3977    #[test]
3978    fn can_attack() {
3979        let mut card = Card::new(
3980            CardId(0),
3981            "Test".to_string(),
3982            PlayerId(0),
3983            CardTypeLine::parse("Creature Bear"),
3984            ManaCost::parse("1 G"),
3985            ColorSet::GREEN,
3986            Some(2),
3987            Some(2),
3988            vec![],
3989            vec![],
3990        );
3991        card.zone = ZoneType::Battlefield;
3992        assert!(!card.can_attack()); // summoning sick
3993
3994        card.summoning_sick = false;
3995        assert!(card.can_attack());
3996
3997        card.tapped = true;
3998        assert!(!card.can_attack()); // tapped
3999    }
4000
4001    #[test]
4002    fn haste_bypasses_summoning_sickness() {
4003        let mut card = Card::new(
4004            CardId(0),
4005            "Test".to_string(),
4006            PlayerId(0),
4007            CardTypeLine::parse("Creature Bear"),
4008            ManaCost::parse("1 G"),
4009            ColorSet::GREEN,
4010            Some(2),
4011            Some(2),
4012            vec!["Haste".to_string()],
4013            vec![],
4014        );
4015        card.zone = ZoneType::Battlefield;
4016        assert!(card.can_attack()); // haste means no summoning sickness check
4017    }
4018
4019    #[test]
4020    fn keyword_helpers() {
4021        let card = Card::new(
4022            CardId(0),
4023            "Test".to_string(),
4024            PlayerId(0),
4025            CardTypeLine::parse("Creature Bear"),
4026            ManaCost::parse("1 G"),
4027            ColorSet::GREEN,
4028            Some(2),
4029            Some(2),
4030            vec![
4031                "Hexproof".to_string(),
4032                "Menace".to_string(),
4033                "Indestructible".to_string(),
4034            ],
4035            vec![],
4036        );
4037        assert!(card.has_hexproof());
4038        assert!(card.has_menace());
4039        assert!(card.has_indestructible());
4040        assert!(!card.has_shroud());
4041        assert!(!card.has_fear());
4042        assert!(!card.has_shadow());
4043    }
4044
4045    #[test]
4046    fn protection_from_color() {
4047        let knight = Card::new(
4048            CardId(0),
4049            "White Knight".to_string(),
4050            PlayerId(0),
4051            CardTypeLine::parse("Creature Knight"),
4052            ManaCost::parse("W W"),
4053            ColorSet::WHITE,
4054            Some(2),
4055            Some(2),
4056            vec!["Protection from black".to_string()],
4057            vec![],
4058        );
4059        let black_source = Card::new(
4060            CardId(1),
4061            "Doom Blade".to_string(),
4062            PlayerId(1),
4063            CardTypeLine::parse("Instant"),
4064            ManaCost::parse("1 B"),
4065            ColorSet::BLACK,
4066            None,
4067            None,
4068            vec![],
4069            vec![],
4070        );
4071        let green_source = Card::new(
4072            CardId(2),
4073            "Giant Growth".to_string(),
4074            PlayerId(1),
4075            CardTypeLine::parse("Instant"),
4076            ManaCost::parse("G"),
4077            ColorSet::GREEN,
4078            None,
4079            None,
4080            vec![],
4081            vec![],
4082        );
4083        assert!(knight.is_protected_from(&black_source));
4084        assert!(!knight.is_protected_from(&green_source));
4085        assert!(knight.has_protection_from("black"));
4086        assert!(!knight.has_protection_from("red"));
4087    }
4088
4089    #[test]
4090    fn ward_and_toxic_parsing() {
4091        let ward_card = Card::new(
4092            CardId(0),
4093            "Ward Bear".to_string(),
4094            PlayerId(0),
4095            CardTypeLine::parse("Creature Bear"),
4096            ManaCost::parse("1 U"),
4097            ColorSet::BLUE,
4098            Some(2),
4099            Some(2),
4100            vec!["Ward:2".to_string()],
4101            vec![],
4102        );
4103        assert_eq!(ward_card.get_ward_cost(), Some("2".to_string()));
4104
4105        let toxic_card = Card::new(
4106            CardId(1),
4107            "Toxic Elf".to_string(),
4108            PlayerId(0),
4109            CardTypeLine::parse("Creature Elf"),
4110            ManaCost::parse("G"),
4111            ColorSet::GREEN,
4112            Some(1),
4113            Some(1),
4114            vec!["Toxic:1".to_string()],
4115            vec![],
4116        );
4117        assert_eq!(toxic_card.get_toxic_count(), Some(1));
4118
4119        // No ward/toxic
4120        let plain = Card::new(
4121            CardId(2),
4122            "Bear".to_string(),
4123            PlayerId(0),
4124            CardTypeLine::parse("Creature Bear"),
4125            ManaCost::parse("1 G"),
4126            ColorSet::GREEN,
4127            Some(2),
4128            Some(2),
4129            vec![],
4130            vec![],
4131        );
4132        assert_eq!(plain.get_ward_cost(), None);
4133        assert_eq!(plain.get_toxic_count(), None);
4134    }
4135
4136    #[test]
4137    fn from_rules_copies_attraction_lights() {
4138        let rules = parse_card_script(
4139            "Name:Balloon Stand\nTypes:Artifact Attraction\nLights: 2 4 6\nOracle:Test.",
4140        )
4141        .expect("card script should parse");
4142        let card = Card::from_rules(&rules, PlayerId(0));
4143        assert_eq!(card.attraction_lights, vec![2, 4, 6]);
4144        assert!(card.has_attraction_light(4));
4145        assert!(!card.has_attraction_light(3));
4146    }
4147
4148    #[test]
4149    fn can_produce_color_mana_uses_mana_abilities_and_reflection() {
4150        let mut game = GameState::new(&["Alice", "Bob"], 20);
4151        let p0 = PlayerId(0);
4152
4153        let white_land = Card::new(
4154            CardId(0),
4155            "White Source".to_string(),
4156            p0,
4157            CardTypeLine::parse("Land"),
4158            ManaCost::parse(""),
4159            ColorSet::COLORLESS,
4160            None,
4161            None,
4162            vec![],
4163            vec!["AB$ Mana | Cost$ T | Produced$ W | SpellDescription$ Add {W}.".to_string()],
4164        );
4165        let reflecting_pool = Card::new(
4166            CardId(1),
4167            "Reflecting Pool".to_string(),
4168            p0,
4169            CardTypeLine::parse("Land"),
4170            ManaCost::parse(""),
4171            ColorSet::COLORLESS,
4172            None,
4173            None,
4174            vec![],
4175            vec!["AB$ ManaReflected | Cost$ T | Valid$ Land.YouCtrl | ReflectProperty$ Produce | ColorOrType$ Type | Produced$ W | SpellDescription$ Add one mana of any type that a land you control could produce.".to_string()],
4176        );
4177
4178        let white_id = game.create_card(white_land);
4179        let pool_id = game.create_card(reflecting_pool);
4180        game.move_card(white_id, ZoneType::Battlefield, p0);
4181        game.move_card(pool_id, ZoneType::Battlefield, p0);
4182
4183        let mut white = HashSet::new();
4184        white.insert("white".to_string());
4185        assert!(game.card(white_id).can_produce_color_mana(&game, &white));
4186        assert!(game.card(pool_id).can_produce_color_mana(&game, &white));
4187    }
4188
4189    #[test]
4190    fn can_produce_same_mana_type_with_uses_mana_ability_overlap() {
4191        let mut game = GameState::new(&["Alice", "Bob"], 20);
4192        let p0 = PlayerId(0);
4193
4194        let island = Card::new(
4195            CardId(0),
4196            "Island Source".to_string(),
4197            p0,
4198            CardTypeLine::parse("Land"),
4199            ManaCost::parse(""),
4200            ColorSet::COLORLESS,
4201            None,
4202            None,
4203            vec![],
4204            vec!["AB$ Mana | Cost$ T | Produced$ U | SpellDescription$ Add {U}.".to_string()],
4205        );
4206        let prism = Card::new(
4207            CardId(1),
4208            "Prism".to_string(),
4209            p0,
4210            CardTypeLine::parse("Artifact"),
4211            ManaCost::parse("2"),
4212            ColorSet::COLORLESS,
4213            None,
4214            None,
4215            vec![],
4216            vec!["AB$ Mana | Cost$ T | Produced$ U | SpellDescription$ Add {U}.".to_string()],
4217        );
4218
4219        let island_id = game.create_card(island);
4220        let prism_id = game.create_card(prism);
4221        game.move_card(island_id, ZoneType::Battlefield, p0);
4222        game.move_card(prism_id, ZoneType::Battlefield, p0);
4223
4224        assert!(game
4225            .card(prism_id)
4226            .can_produce_same_mana_type_with(&game, game.card(island_id)));
4227    }
4228}