Skip to main content

manabrew_engine/staticability/
layer.rs

1//! CR 613 layer system — continuous effect application.
2//!
3//! Mirrors Java Forge's `GameAction.checkStaticAbilities()` and
4//! `StaticAbilityContinuous.applyContinuousAbility()`.
5//!
6//! # How to use
7//!
8//! Call [`apply_continuous_effects`] after any event that could change which
9//! static abilities are active (card entering/leaving the battlefield, spell
10//! resolution, etc.):
11//!
12//! ```ignore
13//! apply_continuous_effects(&mut game);
14//! ```
15//!
16//! The function resets all derived fields (`static_power_modifier`,
17//! `static_toughness_modifier`, `static_set_power`, `static_set_toughness`,
18//! `granted_keywords`, `cant_attack_static`, `cant_block_static`) and
19//! recomputes them from scratch.
20//!
21//! # Layer ordering (CR 613)
22//!
23//! 1. Copy effects (not yet implemented)
24//! 2. Control-changing
25//! 3. Text-changing (not yet implemented)
26//! 4. Type-changing  → [`Layer::Type`]
27//! 5. Color-changing → [`Layer::Color`]
28//! 6. Ability-adding/removing → [`Layer::Ability`]
29//! 7a. CDA P/T → [`Layer::Characteristic`]
30//! 7b. Set P/T → [`Layer::SetPT`]
31//! 7c. Modify P/T → [`Layer::ModifyPT`]
32//! 7d. Counters (handled intrinsically by `Card::power()`)
33//! 8. Forge rules-modifying layer → [`Layer::Rules`]
34
35use std::collections::BTreeMap;
36
37use forge_foundation::{CardTypeLine, CoreType, Supertype, ZoneType};
38
39use crate::agent::PlayerAgent;
40use crate::game::GameState;
41use crate::ids::{CardId, PlayerId};
42use crate::replacement::replacement_effect::ReplacementType;
43use crate::staticability::{CardFilter, Layer, StaticAbility, StaticMode};
44
45// ── Effect collection ────────────────────────────────────────────────────────
46
47/// An effect ready to be applied to a specific target card.
48struct PendingEffect {
49    /// CR 613 layer (used for sort ordering).
50    layer: Layer,
51    /// Target card index.
52    target: CardId,
53    /// Payload.
54    kind: EffectKind,
55}
56
57enum EffectKind {
58    SetController {
59        controller: PlayerId,
60    },
61    AddPT {
62        power: i32,
63        toughness: i32,
64    },
65    SetPT {
66        power: Option<i32>,
67        toughness: Option<i32>,
68    },
69    RemoveAllCardTraits {
70        timestamp: i64,
71        static_id: i64,
72    },
73    GrantKeyword(String),
74    /// Grant an activated ability (from AddAbility$). The string is the ability text.
75    GrantAbility {
76        text: String,
77        svars: BTreeMap<String, String>,
78    },
79    /// Add a type/subtype to the card (`AddType$`). Mirrors Java layer 4.
80    AddType(String),
81    /// Grant a triggered ability (from AddTrigger$). The string is the raw trigger text.
82    GrantTrigger {
83        text: String,
84        svars: BTreeMap<String, String>,
85    },
86}
87
88// ── Public API ───────────────────────────────────────────────────────────────
89
90/// CR 613 layers a `Continuous` static contributes to.
91///
92/// Mirrors Java `StaticAbility.generateLayer()`. The classification is derived
93/// at runtime from the authored params; `StaticAbilityIr` stores the parsed DSL
94/// facts only.
95pub fn classify_static_layers(sa: &StaticAbility) -> Vec<Layer> {
96    if !sa.check_mode(&StaticMode::Continuous) {
97        return Vec::new();
98    }
99
100    let ir = &sa.ir;
101    let mut layers = Vec::new();
102
103    push_layer(&mut layers, ir.gain_control_param, Layer::Control);
104    push_layer(&mut layers, ir.has_text_layer_key, Layer::Text);
105    push_layer(&mut layers, ir.has_type_layer_key, Layer::Type);
106    push_layer(&mut layers, ir.has_color_layer_key, Layer::Color);
107    push_layer(&mut layers, ir.has_ability_layer_key, Layer::Ability);
108
109    if ir.set_power || ir.set_toughness {
110        if ir.characteristic_defining {
111            push_unique_layer(&mut layers, Layer::Characteristic);
112        } else {
113            push_unique_layer(&mut layers, Layer::SetPT);
114        }
115    }
116
117    push_layer(
118        &mut layers,
119        ir.add_power || ir.add_toughness,
120        Layer::ModifyPT,
121    );
122    push_layer(&mut layers, ir.has_rules_layer_key, Layer::Rules);
123
124    if layers.is_empty() {
125        layers.push(Layer::Rules);
126    }
127
128    layers
129}
130
131fn push_layer(layers: &mut Vec<Layer>, condition: bool, layer: Layer) {
132    if condition {
133        push_unique_layer(layers, layer);
134    }
135}
136
137fn static_layer_trait_id(source_id: CardId, sa_idx: usize) -> i64 {
138    -(((source_id.index() as i64) + 1) * 10_000 + sa_idx as i64 + 1)
139}
140
141fn push_unique_layer(layers: &mut Vec<Layer>, layer: Layer) {
142    if !layers.contains(&layer) {
143        layers.push(layer);
144    }
145}
146
147fn type_line_has_token(type_line: &CardTypeLine, token: &str) -> bool {
148    if let Some(st) = Supertype::from_name(token) {
149        return type_line.supertypes.contains(&st);
150    }
151    if let Some(ct) = CoreType::from_name(token) {
152        return type_line.core_types.contains(&ct);
153    }
154    type_line
155        .subtypes
156        .iter()
157        .any(|subtype| subtype.eq_ignore_ascii_case(token))
158}
159
160/// Recompute all continuously-applied static-ability effects for the current
161/// game state.
162///
163/// This is the Rust equivalent of Java Forge's
164/// `GameAction.checkStaticAbilities()` + `StaticAbilityContinuous.applyContinuousAbility()`.
165///
166/// **Call this** after:
167/// - Any permanent enters or leaves the battlefield.
168/// - Any spell or ability resolves.
169/// - Any triggered ability fires.
170/// - Before querying `can_attack()` / `can_block()` for combat legality.
171pub fn apply_continuous_effects(game: &mut GameState) {
172    let _perf_timer = crate::perf::ScopeTimer::start(
173        crate::perf::Metric::ContinuousEffectsCalls,
174        crate::perf::Metric::ContinuousEffectsNs,
175    );
176    let _params_lookup_scope =
177        crate::perf::ParamsLookupScopeGuard::enter(crate::perf::ParamsLookupScope::Continuous);
178    // ── 1. Reset all derived fields ──────────────────────────────────────
179    for card in game.cards.iter_mut() {
180        card.clear_static_layer_changed_card_traits();
181        // Remove abilities granted by continuous effects (AddAbility$).
182        // The base_ability_count tracks how many abilities the card originally had.
183        if card.activated_abilities.len() > card.base_ability_count {
184            card.activated_abilities.truncate(card.base_ability_count);
185        }
186        for (ability_idx, ability) in card.activated_abilities.iter_mut().enumerate() {
187            ability.ability_index = ability_idx;
188        }
189        let intrinsic_trigger_count = card.base_trigger_count + card.pump_trigger_count;
190        if card.triggers.len() > intrinsic_trigger_count {
191            card.triggers.truncate(intrinsic_trigger_count);
192        }
193        card.static_power_modifier = 0;
194        card.static_toughness_modifier = 0;
195        // Preserve face-down morph P/T override (2/2); only reset for face-up cards.
196        if !card.face_down {
197            card.static_set_power = None;
198            card.static_set_toughness = None;
199        }
200        card.granted_keywords.clear();
201        card.granted_svars.clear();
202        // Restore the pre-layer type line before applying AddType$ statics.
203        if let Some(type_line) = card.static_type_line_base.take() {
204            card.set_type_line(type_line);
205        }
206        card.static_added_subtypes.clear();
207        card.cant_attack_static = false;
208        card.cant_block_static = false;
209    }
210    for player in game.players.iter_mut() {
211        player.max_land_plays_per_turn = 1;
212        player.unlimited_land_plays = false;
213    }
214
215    // ── 1b. Keyword-derived restrictions ────────────────────────────────
216    // Unleash: creatures with Unleash keyword and a +1/+1 counter can't block.
217    for card in game.cards.iter_mut() {
218        if card.zone == ZoneType::Battlefield
219            && card.has_keyword("Unleash")
220            && card.counter_count(&crate::card::CounterType::P1P1) > 0
221        {
222            card.cant_block_static = true;
223        }
224    }
225
226    for player_idx in 0..game.player_order.len() {
227        let pid = game.player_order[player_idx];
228        let player = game.player_mut(pid);
229        player.max_hand_size = 7;
230        player.unlimited_hand_size = false;
231    }
232    let player_ids: Vec<PlayerId> = game.player_order.clone();
233    for &pid in &player_ids {
234        let battlefield_cards: Vec<CardId> =
235            game.cards_in_zone(ZoneType::Battlefield, pid).to_vec();
236        for source_id in battlefield_cards {
237            let static_ability_count = game.card(source_id).static_abilities.len();
238            for sa_idx in 0..static_ability_count {
239                let card = game.card(source_id);
240                let sa = &card.static_abilities[sa_idx];
241                if !sa.check_conditions(card, game) {
242                    continue;
243                }
244                if !sa.check_mode(&StaticMode::Continuous) {
245                    continue;
246                }
247                let affected = sa.ir.affected_text.as_deref().unwrap_or("");
248                if !affected.eq_ignore_ascii_case("You") {
249                    continue;
250                }
251                let controller = card.controller;
252                let set_value = sa.ir.set_max_hand_size.clone();
253                let raise_value = sa.ir.raise_max_hand_size.clone();
254                if let Some(value) = set_value {
255                    let player = game.player_mut(controller);
256                    if value.eq_ignore_ascii_case("Unlimited") {
257                        player.unlimited_hand_size = true;
258                    } else if let Ok(n) = value.parse::<i32>() {
259                        player.max_hand_size = n;
260                    }
261                }
262                if let Some(value) = raise_value {
263                    if let Ok(n) = value.parse::<i32>() {
264                        let player = game.player_mut(controller);
265                        player.max_hand_size = player.max_hand_size.saturating_add(n);
266                    }
267                }
268            }
269        }
270    }
271
272    // ── 2. Build list of effects-to-apply (deferred to allow sorting) ────
273    let mut pending: Vec<PendingEffect> = Vec::new();
274    let mut cant_attack_targets: Vec<CardId> = Vec::new();
275    let mut cant_block_targets: Vec<CardId> = Vec::new();
276    let mut granted_player_rules: Vec<(CardId, StaticAbility)> = Vec::new();
277
278    let source_ids: Vec<CardId> = game.cards.iter().map(|card| card.id).collect();
279    for source_id in source_ids {
280        let static_ability_count = game.card(source_id).static_abilities.len();
281
282        for sa_idx in 0..static_ability_count {
283            let source_card = game.card(source_id).clone();
284            let sa = game.card(source_id).static_abilities[sa_idx].clone();
285
286            // Full static-ability condition gate (IsPresent$, CheckSVar$, Condition$, etc.).
287            // Mirrors Java static ability checks before applying continuous effects.
288            if !sa.check_conditions(&source_card, game) {
289                continue;
290            }
291
292            if sa.check_mode(&StaticMode::Continuous) {
293                apply_player_rules_effects(game, source_id, &sa);
294            }
295
296            // CharacteristicDefining statics always affect only the host card.
297            // Mirrors Java StaticAbilityContinuous.getAffectedCards() line 1036.
298            let is_cda = sa.ir.characteristic_defining;
299
300            // Determine which cards are affected by this static ability.
301            let affected_str = sa
302                .ir
303                .affected_text
304                .as_deref()
305                .or(sa.ir.valid_cards_text.as_deref())
306                .or(sa.ir.valid_card_text.as_deref())
307                .unwrap_or("Creature.YouControl");
308
309            let mut apply_to_target = |target: CardId| {
310                if sa.check_mode(&StaticMode::Continuous) {
311                    if let Some(gain_control) = sa.ir.gain_control_text.as_deref() {
312                        let new_controller = match gain_control {
313                            "You" | "YouCtrl" => Some(source_card.controller),
314                            "Opponent" => Some(game.opponent_of(source_card.controller)),
315                            _ => None,
316                        };
317                        if let Some(controller) = new_controller {
318                            pending.push(PendingEffect {
319                                layer: Layer::Control,
320                                target,
321                                kind: EffectKind::SetController { controller },
322                            });
323                        }
324                    }
325
326                    let add_power = sa.ir.add_power_text.as_deref();
327                    let add_toughness = sa.ir.add_toughness_text.as_deref();
328                    if add_power.is_some() || add_toughness.is_some() {
329                        let p = resolve_add_pt_value(game, source_id, add_power);
330                        let t = resolve_add_pt_value(game, source_id, add_toughness);
331                        pending.push(PendingEffect {
332                            layer: Layer::ModifyPT,
333                            target,
334                            kind: EffectKind::AddPT {
335                                power: p,
336                                toughness: t,
337                            },
338                        });
339                    }
340
341                    let add_type = sa.ir.add_type_text.as_deref();
342                    let source = game.card(source_id);
343                    for added_type in resolve_added_types(source, add_type) {
344                        pending.push(PendingEffect {
345                            layer: Layer::Type,
346                            target,
347                            kind: EffectKind::AddType(added_type),
348                        });
349                    }
350
351                    let set_power = sa.ir.set_power_text.as_deref();
352                    let set_toughness = sa.ir.set_toughness_text.as_deref();
353                    if set_power.is_some() || set_toughness.is_some() {
354                        let sp = resolve_set_pt_value(game, source_id, set_power);
355                        let st = resolve_set_pt_value(game, source_id, set_toughness);
356                        // Java parity: CharacteristicDefining$ True routes
357                        // SetP/T through layer 7a, otherwise 7b.
358                        let layer = if is_cda {
359                            Layer::Characteristic
360                        } else {
361                            Layer::SetPT
362                        };
363                        pending.push(PendingEffect {
364                            layer,
365                            target,
366                            kind: EffectKind::SetPT {
367                                power: sp,
368                                toughness: st,
369                            },
370                        });
371                    }
372
373                    if let Some(kws) = sa.ir.add_keyword_text.as_deref() {
374                        // AddKeyword$ supports multiple keywords separated by " & ".
375                        for kw in kws.split('&').map(str::trim).filter(|s| !s.is_empty()) {
376                            pending.push(PendingEffect {
377                                layer: Layer::Ability,
378                                target,
379                                kind: EffectKind::GrantKeyword(kw.to_string()),
380                            });
381                        }
382                    }
383
384                    if sa.ir.remove_all_abilities {
385                        pending.push(PendingEffect {
386                            layer: Layer::Ability,
387                            target,
388                            kind: EffectKind::RemoveAllCardTraits {
389                                timestamp: source_card.zone_timestamp as i64,
390                                static_id: static_layer_trait_id(source_id, sa_idx),
391                            },
392                        });
393                    }
394
395                    // AddAbility$ — grant an activated ability to the affected card.
396                    // The value is an SVar name on the source card containing the ability text.
397                    // E.g. Abundant Growth: AddAbility$ AbundantGrowthTap
398                    //   SVar:AbundantGrowthTap:AB$ Mana | Cost$ T | Produced$ Any
399                    if let Some(svar_name) = sa.ir.add_ability_text.as_deref() {
400                        if let Some(ab_text) = source_card.svars.get(svar_name).cloned() {
401                            pending.push(PendingEffect {
402                                layer: Layer::Ability,
403                                target,
404                                kind: EffectKind::GrantAbility {
405                                    text: ab_text,
406                                    svars: source_card.svars.clone(),
407                                },
408                            });
409                        }
410                    }
411
412                    if let Some(add_trigger) = sa.ir.add_trigger_text.as_deref() {
413                        for svar_name in add_trigger
414                            .split(" & ")
415                            .map(str::trim)
416                            .filter(|s| !s.is_empty())
417                        {
418                            if let Some(trig_text) = source_card.svars.get(svar_name).cloned() {
419                                pending.push(PendingEffect {
420                                    layer: Layer::Ability,
421                                    target,
422                                    kind: EffectKind::GrantTrigger {
423                                        text: trig_text,
424                                        svars: source_card.svars.clone(),
425                                    },
426                                });
427                            }
428                        }
429                    }
430
431                    if let Some(add_static) = sa.ir.add_static_ability_text.as_deref() {
432                        for svar_name in add_static
433                            .split(" & ")
434                            .map(str::trim)
435                            .filter(|s| !s.is_empty())
436                        {
437                            if let Some(static_text) = source_card.svars.get(svar_name).cloned() {
438                                if let Some(granted) =
439                                    crate::staticability::parse_static_ability(&static_text)
440                                {
441                                    granted_player_rules.push((target, granted));
442                                }
443                            }
444                        }
445                    }
446
447                    for subtype in resolve_added_basic_land_types(&source_card, add_type) {
448                        if let Some(ab_text) = basic_land_mana_ability_text(&subtype) {
449                            pending.push(PendingEffect {
450                                layer: Layer::Ability,
451                                target,
452                                kind: EffectKind::GrantAbility {
453                                    text: ab_text.to_string(),
454                                    svars: BTreeMap::new(),
455                                },
456                            });
457                        }
458                    }
459                }
460
461                if sa.check_mode(&StaticMode::CantAttack) {
462                    cant_attack_targets.push(target);
463                }
464                if sa.check_mode(&StaticMode::CantBlock) {
465                    cant_block_targets.push(target);
466                }
467            };
468
469            if is_cda {
470                // CDAs always affect only the source card itself.
471                if source_card.zone == ZoneType::Battlefield {
472                    apply_to_target(source_id);
473                }
474            } else if affected_str.eq_ignore_ascii_case("Card.Self")
475                || affected_str.starts_with("Card.Self+")
476            {
477                // Self-referencing static: only affects the source card itself,
478                // but qualifiers after "+" must still be checked (e.g.
479                // "Card.Self+counters_GE2_CHARGE" only matches when the card
480                // has >=2 charge counters). Mirrors Java's
481                // StaticAbilityContinuous.getAffectedCards() which validates
482                // all qualifiers even for self-referencing statics.
483                if source_card.zone == ZoneType::Battlefield
484                    && crate::card::valid_filter::matches_valid_card(
485                        affected_str,
486                        &source_card,
487                        &source_card,
488                    )
489                {
490                    apply_to_target(source_id);
491                }
492            } else if affected_str.eq_ignore_ascii_case("Card.EnchantedBy")
493                || affected_str.contains(".EquippedBy")
494                || affected_str.contains(".EnchantedBy")
495            {
496                // Aura / Equipment static effects: affect what this source is
497                // attached to. Java treats EquippedBy and EnchantedBy
498                // identically: both resolve to the entity the source is
499                // attached to. (e.g. Short Sword: "Creature.EquippedBy",
500                // Control Magic: "Card.EnchantedBy")
501                if let Some(cid) = source_card.attached_to {
502                    if game.card(cid).zone == ZoneType::Battlefield {
503                        apply_to_target(cid);
504                    }
505                }
506            } else {
507                let filter = CardFilter::parse(affected_str);
508                // AffectedZone$ overrides the default Battlefield filter (e.g.
509                // Ashling, the Limitless grants Evoke:4 to Elementals in Hand).
510                let affected_zones = if sa.ir.affected_zones.is_empty() {
511                    None
512                } else {
513                    Some(sa.ir.affected_zones.as_slice())
514                };
515                for card in &game.cards {
516                    let zone_matches = match &affected_zones {
517                        Some(zones) => zones.contains(&card.zone),
518                        None => card.zone == ZoneType::Battlefield,
519                    };
520                    if zone_matches && filter.matches_with_game(card, &source_card, game) {
521                        apply_to_target(card.id);
522                    }
523                }
524            }
525        }
526    }
527
528    for (source_id, granted) in granted_player_rules {
529        apply_player_rules_effects(game, source_id, &granted);
530    }
531
532    for target in cant_attack_targets {
533        game.cards[target.index()].cant_attack_static = true;
534    }
535    for target in cant_block_targets {
536        game.cards[target.index()].cant_block_static = true;
537    }
538
539    // ── 4. Sort by layer then apply ──────────────────────────────────────
540    // CR 613.1: apply layers 1→7c in order. Within the same layer, timestamp
541    // ordering is preserved by the stable sort (sources were collected in
542    // card-declaration order, which approximates timestamp order).
543    pending.sort_by_key(|e| e.layer);
544
545    for effect in pending {
546        match effect.kind {
547            EffectKind::SetController { controller } => {
548                game.change_controller(effect.target, controller);
549            }
550            EffectKind::AddPT { power, toughness } => {
551                let card = &mut game.cards[effect.target.index()];
552                card.static_power_modifier += power;
553                card.static_toughness_modifier += toughness;
554            }
555            EffectKind::SetPT { power, toughness } => {
556                let card = &mut game.cards[effect.target.index()];
557                // Layer 7b: override the base P/T for this calculation cycle.
558                // We use `static_set_power` rather than mutating `base_power`
559                // so the original base value is preserved for the next reset.
560                if let Some(p) = power {
561                    card.static_set_power = Some(p);
562                }
563                if let Some(t) = toughness {
564                    card.static_set_toughness = Some(t);
565                }
566            }
567            EffectKind::RemoveAllCardTraits {
568                timestamp,
569                static_id,
570            } => {
571                game.cards[effect.target.index()].add_changed_card_traits(
572                    crate::card::card_trait_changes::CardTraitChanges::remove_all_layer(
573                        Vec::new(),
574                        Vec::new(),
575                        Vec::new(),
576                        Vec::new(),
577                    ),
578                    timestamp,
579                    static_id,
580                );
581            }
582            EffectKind::GrantKeyword(kw) => {
583                let card = &mut game.cards[effect.target.index()];
584                card.granted_keywords.add(&kw);
585                if let Some(cost_str) = crate::keyword::extract_keyword_cost_str(&kw, "Ward") {
586                    let next_id = card
587                        .triggers
588                        .iter()
589                        .map(|t| t.id)
590                        .max()
591                        .unwrap_or(0)
592                        .saturating_add(1);
593                    let mut next_id_mut = next_id;
594                    let execute = format!("TrigWardGranted{next_id}");
595                    let raw = format!(
596                        "Mode$ BecomesTarget | ValidSource$ SpellAbility.OppCtrl | ValidTarget$ Card.Self | Secondary$ True | Execute$ {execute} | TriggerZones$ Battlefield | TriggerDescription$ Ward"
597                    );
598                    if let Some(mut trig) = crate::trigger::parse_trigger(&raw, &mut next_id_mut) {
599                        trig.execute = execute.clone();
600                        card.add_trigger(trig);
601                    }
602                    card.granted_svars.insert(
603                        execute,
604                        format!(
605                            "DB$ Counter | Defined$ TriggeredSourceSA | UnlessCost$ {cost_str}"
606                        ),
607                    );
608                }
609            }
610            EffectKind::AddType(t) => {
611                let card = &mut game.cards[effect.target.index()];
612                if !type_line_has_token(&card.type_line, &t) {
613                    if card.static_type_line_base.is_none() {
614                        card.static_type_line_base = Some(card.type_line.clone());
615                    }
616                    card.add_type(&t);
617                    card.static_added_subtypes.push(t);
618                }
619            }
620            EffectKind::GrantAbility { text, svars } => {
621                // Parse the ability text and add it to the target's activated abilities.
622                // This grants abilities like "{T}: Add one mana of any color."
623                game.cards[effect.target.index()]
624                    .granted_svars
625                    .extend(svars);
626                let target_idx = effect.target.index();
627                let next_idx = game.cards[target_idx].activated_abilities.len();
628                if let Some(ab) =
629                    crate::ability::activated::parse_activated_ability(&text, next_idx)
630                {
631                    game.cards[target_idx].activated_abilities.push(ab);
632                }
633            }
634            EffectKind::GrantTrigger { text, svars } => {
635                game.cards[effect.target.index()]
636                    .granted_svars
637                    .extend(svars);
638                let next_id = game.cards[effect.target.index()]
639                    .triggers
640                    .iter()
641                    .map(|t| t.id)
642                    .max()
643                    .unwrap_or(0)
644                    .saturating_add(1);
645                let mut next_id_mut = next_id;
646                if let Some(trig) = crate::trigger::parse_trigger(&text, &mut next_id_mut) {
647                    game.cards[effect.target.index()].add_trigger(trig);
648                }
649            }
650        }
651    }
652
653    // Rebuild intrinsic basic-land mana abilities after type-changing continuous
654    // effects have been applied (e.g. Urborg making lands into Swamps).
655    for card in game.cards.iter_mut() {
656        if card.zone == ZoneType::Battlefield {
657            card.generate_basic_land_mana_abilities();
658        }
659    }
660}
661
662fn apply_player_rules_effects(game: &mut GameState, source_id: CardId, sa: &StaticAbility) {
663    let Some(adjust_land_plays) = sa.ir.adjust_land_plays_text.as_deref() else {
664        return;
665    };
666    let affected_players = affected_players_for_static(game, source_id, sa);
667    if affected_players.is_empty() {
668        return;
669    }
670    if adjust_land_plays.eq_ignore_ascii_case("Unlimited") {
671        for player in affected_players {
672            game.player_mut(player).unlimited_land_plays = true;
673        }
674        return;
675    }
676    let amount = resolve_rules_amount(game, source_id, adjust_land_plays);
677    for player in affected_players {
678        game.player_mut(player).max_land_plays_per_turn += amount;
679    }
680}
681
682fn affected_players_for_static(
683    game: &GameState,
684    source_id: CardId,
685    sa: &StaticAbility,
686) -> Vec<PlayerId> {
687    let Some(affected) = sa.ir.affected_text.as_deref() else {
688        return Vec::new();
689    };
690    let source = game.card(source_id);
691    game.player_order
692        .iter()
693        .copied()
694        .filter(|&player| {
695            !sa.ignore_effect_players.contains(&player)
696                && crate::card::valid_filter::matches_valid(
697                    affected,
698                    None,
699                    Some(player),
700                    source,
701                    source.controller,
702                )
703        })
704        .collect()
705}
706
707fn resolve_rules_amount(game: &GameState, source_id: CardId, value: &str) -> i32 {
708    if let Ok(n) = value.trim().parse::<i32>() {
709        return n;
710    }
711    let source = game.card(source_id);
712    if let Some(svar_expr) = source.svars.get(value.trim()) {
713        if svar_expr.starts_with("Count$") {
714            return crate::ability::effects::resolve_count_svar(
715                svar_expr,
716                game,
717                source_id,
718                source.controller,
719            );
720        }
721        return crate::ability::effects::evaluate_svar(
722            svar_expr,
723            &crate::spellability::SpellAbility::new_empty(Some(source_id), source.controller),
724        );
725    }
726    0
727}
728
729/// Apply ETB-tapped effects to `entering_card` as it enters the battlefield.
730///
731/// Checks:
732/// 1. The card's own static abilities for `Mode$ ETBTapped` (intrinsic).
733/// 2. Any other battlefield permanent with `Mode$ ETBTapped` whose filter
734///    matches the entering card (extrinsic, e.g. Imposing Sovereign).
735///
736/// Call this immediately after [`GameState::move_card`] resolves a
737/// `Battlefield` destination and before triggers are fired.
738pub fn apply_etb_tapped(game: &mut GameState, entering_card: CardId) {
739    apply_etb_tapped_with_agents(game, entering_card, None);
740}
741
742fn applicable_etb_tapped_replacement_sources(
743    game: &GameState,
744    entering_card: CardId,
745) -> Vec<(CardId, String)> {
746    let mut repl_sources: Vec<(CardId, String, String)> = Vec::new();
747    for c in &game.cards {
748        if c.zone != ZoneType::Battlefield {
749            continue;
750        }
751        for re in &c.replacement_effects {
752            if re.event == ReplacementType::Moved
753                && re.replace_with() == Some("ETBTapped")
754                && re.ir.destination_zone == Some(ZoneType::Battlefield)
755                && re.active_in_zone(ZoneType::Battlefield)
756            {
757                let filter = re
758                    .ir
759                    .valid_card_text
760                    .as_deref()
761                    .unwrap_or("Card.Self")
762                    .to_string();
763                let desc = re.description(c, game);
764                repl_sources.push((c.id, filter, desc));
765            }
766        }
767    }
768
769    repl_sources
770        .into_iter()
771        .filter_map(|(source_id, filter_str, desc)| {
772            let tapped = if filter_str == "Card.Self" || filter_str.is_empty() {
773                source_id == entering_card
774            } else {
775                let source = &game.cards[source_id.index()];
776                let filter = CardFilter::parse(&filter_str);
777                filter.matches_with_game(&game.cards[entering_card.index()], source, game)
778            };
779            tapped.then_some((source_id, desc))
780        })
781        .collect()
782}
783
784pub fn prompt_etb_tapped_replacement_with_agents(
785    game: &mut GameState,
786    entering_card: CardId,
787    agents: &mut [Box<dyn PlayerAgent>],
788) {
789    let applicable = applicable_etb_tapped_replacement_sources(game, entering_card);
790    if applicable.is_empty() {
791        return;
792    }
793
794    let affected_player = game.cards[entering_card.index()].controller;
795    let descriptions: Vec<String> = applicable
796        .iter()
797        .map(|(source_id, desc)| format!("{}: {}", game.card(*source_id).card_name, desc))
798        .collect();
799    let _chosen = agents[affected_player.index()]
800        .choose_single_replacement_effect(affected_player, &descriptions)
801        .min(applicable.len().saturating_sub(1));
802}
803
804pub fn apply_etb_tapped_with_agents(
805    game: &mut GameState,
806    entering_card: CardId,
807    agents: Option<&mut [Box<dyn PlayerAgent>]>,
808) {
809    // Collect all ETBTapped sources: (source_id, filter_str).
810    // We need owned data to avoid aliasing the cards slice while mutating.
811    let etb_sources: Vec<(CardId, String)> = game
812        .cards
813        .iter()
814        .filter(|c| c.zone == ZoneType::Battlefield)
815        .flat_map(|c| {
816            c.static_abilities.iter().filter_map(move |sa| {
817                if sa.check_mode(&StaticMode::ETBTapped) {
818                    let filter_str = sa
819                        .ir
820                        .valid_cards_text
821                        .clone()
822                        .or_else(|| sa.ir.affected_text.clone())
823                        // Default: the card itself (intrinsic self-ETBTapped).
824                        .unwrap_or_else(|| "Card.Self".to_string());
825                    Some((c.id, filter_str))
826                } else {
827                    None
828                }
829            })
830        })
831        .collect();
832
833    for (source_id, filter_str) in etb_sources {
834        // "Card.Self" means only the card that owns the ability.
835        let tapped = if filter_str == "Card.Self" || filter_str.is_empty() {
836            source_id == entering_card
837        } else {
838            let source = &game.cards[source_id.index()];
839            let filter = CardFilter::parse(&filter_str);
840            filter.matches_with_game(&game.cards[entering_card.index()], source, game)
841        };
842
843        if tapped {
844            game.cards[entering_card.index()].tapped = true;
845            return; // once tapped, no need to check further sources
846        }
847    }
848
849    // ── Second pass: check replacement effects for ReplaceWith$ ETBTapped ──
850    // Many cards (e.g. Path of Ancestry, Temple of Mystery) use:
851    //   R:Event$ Moved | Destination$ Battlefield | ValidCard$ Card.Self | ReplaceWith$ ETBTapped
852    // Extrinsic sources (e.g. Kismet) may use broader ValidCard filters.
853    let applicable = applicable_etb_tapped_replacement_sources(game, entering_card);
854    if applicable.is_empty() {
855        return;
856    }
857
858    if let Some(agents) = agents {
859        prompt_etb_tapped_replacement_with_agents(game, entering_card, agents);
860    }
861
862    game.cards[entering_card.index()].tapped = true;
863}
864
865/// Check if a card has a shock-land-style "enters tapped unless you pay life" effect.
866///
867/// Looks for `R:Event$ Moved | Destination$ Battlefield | ReplaceWith$ <SVar>`
868/// where the SVar is `DB$ Tap | ETB$ True | UnlessCost$ PayLife<N>`.
869///
870/// Returns `Some(life_cost)` if found (e.g. `Some(2)` for shock lands), `None` otherwise.
871/// Called from `play_card` / `resolve_stack` where agents are available for prompting.
872pub fn get_etb_unless_life_cost(card: &crate::card::Card) -> Option<i32> {
873    for re in &card.replacement_effects {
874        if re.event != ReplacementType::Moved {
875            continue;
876        }
877        if re.ir.destination_zone != Some(ZoneType::Battlefield) {
878            continue;
879        }
880        if let Some(svar_name) = re.replace_with() {
881            if svar_name == "ETBTapped" {
882                continue;
883            }
884            if let Some(svar_val) = card.svars.get(svar_name) {
885                if svar_val.contains("DB$ Tap") && svar_val.contains("ETB$ True") {
886                    // Parse life cost from "UnlessCost$ PayLife<N>"
887                    if let Some(pos) = svar_val.find("PayLife<") {
888                        let after = &svar_val[pos + 8..]; // skip "PayLife<"
889                        if let Some(end) = after.find('>') {
890                            if let Ok(n) = after[..end].parse::<i32>() {
891                                return Some(n);
892                            }
893                        }
894                    }
895                }
896            }
897        }
898    }
899    None
900}
901
902/// Check if a card has a "enters tapped unless you reveal a <type> from hand" effect.
903///
904/// Looks for `R:Event$ Moved | Destination$ Battlefield | ReplaceWith$ <SVar>`
905/// where the SVar is `DB$ Tap | ETB$ True | UnlessCost$ Reveal<N/Filter>`.
906///
907/// Returns `Some((n, filter))` if found (e.g. `Some((1, "Merfolk"))` for Wanderwine Hub).
908pub fn get_etb_unless_reveal_cost(card: &crate::card::Card) -> Option<(i32, String)> {
909    for re in &card.replacement_effects {
910        if re.event != ReplacementType::Moved {
911            continue;
912        }
913        if re.ir.destination_zone != Some(ZoneType::Battlefield) {
914            continue;
915        }
916        if let Some(svar_name) = re.replace_with() {
917            if svar_name == "ETBTapped" {
918                continue;
919            }
920            if let Some(svar_val) = card.svars.get(svar_name) {
921                if svar_val.contains("DB$ Tap") && svar_val.contains("ETB$ True") {
922                    // Parse reveal cost from "UnlessCost$ Reveal<N/Filter>"
923                    if let Some(pos) = svar_val.find("Reveal<") {
924                        let after = &svar_val[pos + 7..]; // skip "Reveal<"
925                        if let Some(end) = after.find('>') {
926                            let inner = &after[..end]; // "1/Merfolk" or "1/Filter"
927                            let mut parts = inner.splitn(2, '/');
928                            let n = parts
929                                .next()
930                                .and_then(|s| s.trim().parse::<i32>().ok())
931                                .unwrap_or(1);
932                            let filter = parts.next().unwrap_or("").trim().to_string();
933                            return Some((n, filter));
934                        }
935                    }
936                }
937            }
938        }
939    }
940    None
941}
942
943/// Resolve an AddPower$/AddToughness$ parameter that may be a literal integer
944/// or an SVar reference (e.g. "X" → Count$Valid Enchantment.YouCtrl).
945fn resolve_add_pt_value(game: &GameState, source_id: CardId, val_str: Option<&str>) -> i32 {
946    let val_str = match val_str {
947        Some(val_str) => val_str,
948        None => return 0,
949    };
950
951    // Try direct integer parse first
952    if let Ok(n) = val_str.trim().parse::<i32>() {
953        return n;
954    }
955
956    // It's an SVar reference — look it up on the source card
957    let source = game.card(source_id);
958    if let Some(svar_expr) = source.svars.get(val_str.trim()) {
959        if svar_expr.starts_with("Count$") {
960            return crate::ability::effects::resolve_count_svar(
961                svar_expr,
962                game,
963                source_id,
964                source.controller,
965            );
966        }
967        return crate::ability::effects::evaluate_svar(
968            svar_expr,
969            &crate::spellability::SpellAbility::new_empty(Some(source_id), source.controller),
970        );
971    }
972
973    0
974}
975
976/// Resolve a SetPower$/SetToughness$ parameter that may be a literal integer or
977/// an SVar reference (e.g. "X" → SVar:X:Count$Valid Creature.ChosenType).
978/// Mirrors Java `AbilityUtils.calculateAmount(hostCard, param, stAb)`.
979fn resolve_set_pt_value(game: &GameState, source_id: CardId, val_str: Option<&str>) -> Option<i32> {
980    let val_str = val_str?;
981    // Try direct integer parse first
982    if let Ok(n) = val_str.trim().parse::<i32>() {
983        return Some(n);
984    }
985
986    // It's an SVar reference — look it up on the source card
987    let source = game.card(source_id);
988    if let Some(svar_expr) = source.svars.get(val_str.trim()) {
989        if svar_expr.starts_with("Count$") {
990            return Some(crate::ability::effects::resolve_count_svar(
991                svar_expr,
992                game,
993                source_id,
994                source.controller,
995            ));
996        }
997        // Simple SVar evaluation (e.g. Number$2)
998        return Some(crate::ability::effects::evaluate_svar(
999            svar_expr,
1000            &crate::spellability::SpellAbility::new_empty(Some(source_id), source.controller),
1001        ));
1002    }
1003
1004    None
1005}
1006
1007fn basic_land_mana_ability_text(subtype: &str) -> Option<&'static str> {
1008    match subtype {
1009        "Plains" => Some("AB$ Mana | Cost$ T | Produced$ W | SpellDescription$ Add {W}."),
1010        "Island" => Some("AB$ Mana | Cost$ T | Produced$ U | SpellDescription$ Add {U}."),
1011        "Swamp" => Some("AB$ Mana | Cost$ T | Produced$ B | SpellDescription$ Add {B}."),
1012        "Mountain" => Some("AB$ Mana | Cost$ T | Produced$ R | SpellDescription$ Add {R}."),
1013        "Forest" => Some("AB$ Mana | Cost$ T | Produced$ G | SpellDescription$ Add {G}."),
1014        _ => None,
1015    }
1016}
1017
1018fn resolve_added_basic_land_types(
1019    source: &crate::card::Card,
1020    add_type: Option<&str>,
1021) -> Vec<String> {
1022    resolve_added_types(source, add_type)
1023        .into_iter()
1024        .filter(|added| basic_land_mana_ability_text(added).is_some())
1025        .collect()
1026}
1027
1028fn resolve_added_types(source: &crate::card::Card, add_type: Option<&str>) -> Vec<String> {
1029    let Some(add_type) = add_type else {
1030        return Vec::new();
1031    };
1032    let mut resolved = Vec::new();
1033    for raw in add_type.split('&').map(str::trim).filter(|s| !s.is_empty()) {
1034        match raw {
1035            "ChosenType" => {
1036                if let Some(chosen) = source.chosen_type.as_ref() {
1037                    resolved.push(chosen.clone());
1038                }
1039            }
1040            "ChosenType2" => {
1041                if let Some(chosen) = source.chosen_type2.as_ref() {
1042                    resolved.push(chosen.clone());
1043                }
1044            }
1045            "AllBasicLandType" => {
1046                resolved.extend(
1047                    ["Plains", "Island", "Swamp", "Mountain", "Forest"]
1048                        .into_iter()
1049                        .map(str::to_string),
1050                );
1051            }
1052            other => resolved.push(other.to_string()),
1053        }
1054    }
1055    resolved
1056}
1057
1058// ── Tests ────────────────────────────────────────────────────────────────────
1059
1060#[cfg(test)]
1061mod tests {
1062    use super::*;
1063    use forge_foundation::{CardTypeLine, ColorSet, ManaCost, ZoneType};
1064
1065    use crate::card::Card;
1066    use crate::ids::{CardId, PlayerId};
1067
1068    // Build a minimal two-player game with empty zones.
1069    fn new_game() -> GameState {
1070        GameState::new(&["Alice", "Bob"], 20)
1071    }
1072
1073    fn add_creature(
1074        game: &mut GameState,
1075        owner: PlayerId,
1076        power: i32,
1077        toughness: i32,
1078        keywords: Vec<String>,
1079        abilities: Vec<String>,
1080    ) -> CardId {
1081        let card = Card::new(
1082            CardId(0), // reassigned by create_card
1083            "Creature".to_string(),
1084            owner,
1085            CardTypeLine::parse("Creature"),
1086            ManaCost::parse("1 G"),
1087            ColorSet::GREEN,
1088            Some(power),
1089            Some(toughness),
1090            keywords,
1091            abilities,
1092        );
1093        let id = game.create_card(card);
1094        game.move_card(id, ZoneType::Battlefield, owner);
1095        id
1096    }
1097
1098    fn add_enchantment(game: &mut GameState, owner: PlayerId, abilities: Vec<String>) -> CardId {
1099        let card = Card::new(
1100            CardId(0),
1101            "Enchantment".to_string(),
1102            owner,
1103            CardTypeLine::parse("Enchantment"),
1104            ManaCost::parse("2 W"),
1105            ColorSet::WHITE,
1106            None,
1107            None,
1108            vec![],
1109            abilities,
1110        );
1111        let id = game.create_card(card);
1112        game.move_card(id, ZoneType::Battlefield, owner);
1113        id
1114    }
1115
1116    fn add_land(
1117        game: &mut GameState,
1118        owner: PlayerId,
1119        name: &str,
1120        type_line: &str,
1121        abilities: Vec<String>,
1122    ) -> CardId {
1123        let card = Card::new(
1124            CardId(0),
1125            name.to_string(),
1126            owner,
1127            CardTypeLine::parse(type_line),
1128            ManaCost::no_cost(),
1129            ColorSet::COLORLESS,
1130            None,
1131            None,
1132            vec![],
1133            abilities,
1134        );
1135        let id = game.create_card(card);
1136        game.move_card(id, ZoneType::Battlefield, owner);
1137        id
1138    }
1139
1140    fn add_effect(game: &mut GameState, owner: PlayerId, abilities: Vec<String>) -> CardId {
1141        let card = Card::new(
1142            CardId(0),
1143            "Effect".to_string(),
1144            owner,
1145            CardTypeLine::parse("Effect"),
1146            ManaCost::parse("0"),
1147            ColorSet::COLORLESS,
1148            None,
1149            None,
1150            vec![],
1151            abilities,
1152        );
1153        let id = game.create_card(card);
1154        game.move_card(id, ZoneType::Command, owner);
1155        id
1156    }
1157
1158    // ── Anthem (+1/+1) ────────────────────────────────────────────────────
1159
1160    #[test]
1161    fn anthem_boosts_your_creatures() {
1162        let mut game = new_game();
1163        let alice = PlayerId(0);
1164        let bob = PlayerId(1);
1165
1166        // Add two creatures for Alice and one for Bob.
1167        let a1 = add_creature(&mut game, alice, 2, 2, vec![], vec![]);
1168        let a2 = add_creature(&mut game, alice, 1, 1, vec![], vec![]);
1169        let b1 = add_creature(&mut game, bob, 2, 2, vec![], vec![]);
1170
1171        // Add Glorious Anthem-style enchantment controlled by Alice.
1172        let _anthem = add_enchantment(
1173            &mut game,
1174            alice,
1175            vec!["S$ Mode$ Continuous | Affected$ Creature.YouControl | AddPower$ 1 | AddToughness$ 1 | Description$ Creatures you control get +1/+1.".to_string()],
1176        );
1177
1178        apply_continuous_effects(&mut game);
1179
1180        // Alice's creatures get +1/+1.
1181        assert_eq!(game.card(a1).power(), 3, "Alice's 2/2 should be 3/3");
1182        assert_eq!(game.card(a1).toughness(), 3);
1183        assert_eq!(game.card(a2).power(), 2, "Alice's 1/1 should be 2/2");
1184        assert_eq!(game.card(a2).toughness(), 2);
1185
1186        // Bob's creature is unaffected.
1187        assert_eq!(
1188            game.card(b1).power(),
1189            2,
1190            "Bob's creature should be unchanged"
1191        );
1192        assert_eq!(game.card(b1).toughness(), 2);
1193    }
1194
1195    #[test]
1196    fn command_effect_adjusts_land_plays_for_affected_player() {
1197        let mut game = new_game();
1198        let alice = PlayerId(0);
1199        let bob = PlayerId(1);
1200
1201        let effect = add_effect(
1202            &mut game,
1203            alice,
1204            vec![
1205                "S$ Mode$ Continuous | EffectZone$ Command | Affected$ You | AdjustLandPlays$ 1"
1206                    .to_string(),
1207            ],
1208        );
1209
1210        apply_continuous_effects(&mut game);
1211
1212        assert_eq!(game.player(alice).max_land_plays_per_turn, 2);
1213        assert_eq!(game.player(bob).max_land_plays_per_turn, 1);
1214
1215        game.move_card(effect, ZoneType::Exile, alice);
1216        apply_continuous_effects(&mut game);
1217
1218        assert_eq!(game.player(alice).max_land_plays_per_turn, 1);
1219    }
1220
1221    #[test]
1222    fn anthem_resets_when_removed() {
1223        let mut game = new_game();
1224        let alice = PlayerId(0);
1225
1226        let creature = add_creature(&mut game, alice, 2, 2, vec![], vec![]);
1227        let anthem = add_enchantment(
1228            &mut game,
1229            alice,
1230            vec!["S$ Mode$ Continuous | Affected$ Creature.YouControl | AddPower$ 1 | AddToughness$ 1".to_string()],
1231        );
1232
1233        apply_continuous_effects(&mut game);
1234        assert_eq!(game.card(creature).power(), 3);
1235
1236        // Remove the anthem from the battlefield.
1237        game.move_card(anthem, ZoneType::Graveyard, alice);
1238        apply_continuous_effects(&mut game);
1239
1240        assert_eq!(
1241            game.card(creature).power(),
1242            2,
1243            "Bonus should be gone after anthem leaves"
1244        );
1245    }
1246
1247    #[test]
1248    fn stacking_anthems() {
1249        let mut game = new_game();
1250        let alice = PlayerId(0);
1251
1252        let creature = add_creature(&mut game, alice, 1, 1, vec![], vec![]);
1253        // Two separate +1/+1 anthems.
1254        add_enchantment(
1255            &mut game,
1256            alice,
1257            vec!["S$ Mode$ Continuous | Affected$ Creature.YouControl | AddPower$ 1 | AddToughness$ 1".to_string()],
1258        );
1259        add_enchantment(
1260            &mut game,
1261            alice,
1262            vec!["S$ Mode$ Continuous | Affected$ Creature.YouControl | AddPower$ 1 | AddToughness$ 1".to_string()],
1263        );
1264
1265        apply_continuous_effects(&mut game);
1266        assert_eq!(game.card(creature).power(), 3, "Two anthems should give +2");
1267        assert_eq!(game.card(creature).toughness(), 3);
1268    }
1269
1270    // ── Keyword granting ──────────────────────────────────────────────────
1271
1272    #[test]
1273    fn grant_flying_to_your_creatures() {
1274        let mut game = new_game();
1275        let alice = PlayerId(0);
1276        let bob = PlayerId(1);
1277
1278        let a1 = add_creature(&mut game, alice, 2, 2, vec![], vec![]);
1279        let b1 = add_creature(&mut game, bob, 2, 2, vec![], vec![]);
1280
1281        add_enchantment(
1282            &mut game,
1283            alice,
1284            vec!["S$ Mode$ Continuous | Affected$ Creature.YouControl | AddKeyword$ Flying | Description$ Creatures you control have flying.".to_string()],
1285        );
1286
1287        apply_continuous_effects(&mut game);
1288
1289        assert!(
1290            game.card(a1).has_flying(),
1291            "Alice's creature should have flying"
1292        );
1293        assert!(
1294            !game.card(b1).has_flying(),
1295            "Bob's creature should not have flying"
1296        );
1297    }
1298
1299    #[test]
1300    fn grant_multiple_keywords() {
1301        let mut game = new_game();
1302        let alice = PlayerId(0);
1303
1304        let creature = add_creature(&mut game, alice, 2, 2, vec![], vec![]);
1305        add_enchantment(
1306            &mut game,
1307            alice,
1308            vec!["S$ Mode$ Continuous | Affected$ Creature.YouControl | AddKeyword$ Flying & First Strike".to_string()],
1309        );
1310
1311        apply_continuous_effects(&mut game);
1312
1313        assert!(game.card(creature).has_flying());
1314        assert!(game.card(creature).has_first_strike());
1315    }
1316
1317    // ── SetPT (Layer 7b) ──────────────────────────────────────────────────
1318
1319    #[test]
1320    fn set_pt_overrides_base() {
1321        let mut game = new_game();
1322        let alice = PlayerId(0);
1323
1324        let creature = add_creature(&mut game, alice, 5, 5, vec![], vec![]);
1325        // Effect: set all your creatures to 0/1 (e.g. Humility).
1326        add_enchantment(
1327            &mut game,
1328            alice,
1329            vec!["S$ Mode$ Continuous | Affected$ Creature.YouControl | SetPower$ 0 | SetToughness$ 1".to_string()],
1330        );
1331
1332        apply_continuous_effects(&mut game);
1333        assert_eq!(game.card(creature).power(), 0);
1334        assert_eq!(game.card(creature).toughness(), 1);
1335    }
1336
1337    #[test]
1338    fn modify_pt_adds_on_top_of_set_pt() {
1339        // CR 613.7c: ModifyPT applies after SetPT within the same turn.
1340        let mut game = new_game();
1341        let alice = PlayerId(0);
1342
1343        let creature = add_creature(&mut game, alice, 5, 5, vec![], vec![]);
1344        // Layer 7b: set to 0/1.
1345        add_enchantment(
1346            &mut game,
1347            alice,
1348            vec!["S$ Mode$ Continuous | Affected$ Creature.YouControl | SetPower$ 0 | SetToughness$ 1".to_string()],
1349        );
1350        // Layer 7c: +1/+1 anthem on top.
1351        add_enchantment(
1352            &mut game,
1353            alice,
1354            vec!["S$ Mode$ Continuous | Affected$ Creature.YouControl | AddPower$ 1 | AddToughness$ 1".to_string()],
1355        );
1356
1357        apply_continuous_effects(&mut game);
1358        // 0 + 1 = 1 power, 1 + 1 = 2 toughness.
1359        assert_eq!(game.card(creature).power(), 1);
1360        assert_eq!(game.card(creature).toughness(), 2);
1361    }
1362
1363    // ── CantAttack / CantBlock ────────────────────────────────────────────
1364
1365    #[test]
1366    fn cant_attack_flag_set() {
1367        let mut game = new_game();
1368        let alice = PlayerId(0);
1369
1370        let creature = add_creature(&mut game, alice, 2, 2, vec![], vec![]);
1371        // Pacifism-like effect.
1372        add_enchantment(
1373            &mut game,
1374            alice,
1375            vec!["S$ Mode$ CantAttack | Affected$ Creature.YouControl | Description$ Creatures you control can't attack.".to_string()],
1376        );
1377
1378        apply_continuous_effects(&mut game);
1379        assert!(game.card(creature).cant_attack_static);
1380    }
1381
1382    #[test]
1383    fn cant_block_flag_set() {
1384        let mut game = new_game();
1385        let alice = PlayerId(0);
1386
1387        let creature = add_creature(&mut game, alice, 2, 2, vec![], vec![]);
1388        add_enchantment(
1389            &mut game,
1390            alice,
1391            vec!["S$ Mode$ CantBlock | Affected$ Creature.YouControl".to_string()],
1392        );
1393
1394        apply_continuous_effects(&mut game);
1395        assert!(game.card(creature).cant_block_static);
1396    }
1397
1398    #[test]
1399    fn flags_reset_on_reapplication() {
1400        let mut game = new_game();
1401        let alice = PlayerId(0);
1402
1403        let creature = add_creature(&mut game, alice, 2, 2, vec![], vec![]);
1404        let restrictor = add_enchantment(
1405            &mut game,
1406            alice,
1407            vec!["S$ Mode$ CantAttack | Affected$ Creature.YouControl".to_string()],
1408        );
1409
1410        apply_continuous_effects(&mut game);
1411        assert!(game.card(creature).cant_attack_static);
1412
1413        game.move_card(restrictor, ZoneType::Graveyard, alice);
1414        apply_continuous_effects(&mut game);
1415        assert!(
1416            !game.card(creature).cant_attack_static,
1417            "Flag should clear after enchantment leaves"
1418        );
1419    }
1420
1421    #[test]
1422    fn lands_gain_swamp_mana_ability_from_urborg_style_effect() {
1423        let mut game = new_game();
1424        let alice = PlayerId(0);
1425
1426        let urborg = add_land(
1427            &mut game,
1428            alice,
1429            "Urborg, Tomb of Yawgmoth",
1430            "Legendary Land",
1431            vec!["S$ Mode$ Continuous | Affected$ Land | AddType$ Swamp | Description$ Each land is a Swamp in addition to its other land types.".to_string()],
1432        );
1433        let black_gate = add_land(
1434            &mut game,
1435            alice,
1436            "The Black Gate",
1437            "Legendary Land Gate",
1438            vec![],
1439        );
1440
1441        apply_continuous_effects(&mut game);
1442
1443        for land_id in [urborg, black_gate] {
1444            let land = game.card(land_id);
1445            assert!(
1446                land.type_line.has_subtype("Swamp"),
1447                "{} should gain the Swamp subtype",
1448                land.card_name
1449            );
1450            assert!(
1451                land.activated_abilities.iter().any(|ab| {
1452                    ab.is_mana_ability
1453                        && ab
1454                            .produced_ir
1455                            .as_ref()
1456                            .is_some_and(|ir| ir.as_script_text() == "B")
1457                }),
1458                "{} should gain an intrinsic black mana ability from Swamp",
1459                land.card_name
1460            );
1461        }
1462    }
1463
1464    // ── ETB Tapped ────────────────────────────────────────────────────────
1465
1466    #[test]
1467    fn self_etb_tapped() {
1468        let mut game = new_game();
1469        let alice = PlayerId(0);
1470
1471        // A permanent with ETBTapped on itself.
1472        let card = Card::new(
1473            CardId(0),
1474            "TappedLand".to_string(),
1475            alice,
1476            CardTypeLine::parse("Land"),
1477            ManaCost::parse(""),
1478            ColorSet::from_mask(0),
1479            None,
1480            None,
1481            vec![],
1482            vec!["S$ Mode$ ETBTapped | Description$ Enters tapped.".to_string()],
1483        );
1484        let id = game.create_card(card);
1485        game.move_card(id, ZoneType::Battlefield, alice);
1486        apply_etb_tapped(&mut game, id);
1487
1488        assert!(
1489            game.card(id).tapped,
1490            "Card with ETBTapped should enter tapped"
1491        );
1492    }
1493
1494    #[test]
1495    fn no_etb_tapped_without_ability() {
1496        let mut game = new_game();
1497        let alice = PlayerId(0);
1498
1499        let id = add_creature(&mut game, alice, 2, 2, vec![], vec![]);
1500        // Fresh ETB, no static — should not be tapped.
1501        assert!(
1502            !game.card(id).tapped,
1503            "Normal creature should not enter tapped"
1504        );
1505    }
1506
1507    #[test]
1508    fn etb_tapped_via_replacement_effect() {
1509        let mut game = new_game();
1510        let alice = PlayerId(0);
1511
1512        // A land with R:Event$ Moved replacement effect (like Path of Ancestry).
1513        let card = Card::new(
1514            CardId(0),
1515            "PathOfAncestry".to_string(),
1516            alice,
1517            CardTypeLine::parse("Land"),
1518            ManaCost::parse(""),
1519            ColorSet::from_mask(0),
1520            None,
1521            None,
1522            vec![],
1523            vec!["R:Event$ Moved | Destination$ Battlefield | ValidCard$ Card.Self | ReplaceWith$ ETBTapped | Description$ ~ enters tapped.".to_string()],
1524        );
1525        let id = game.create_card(card);
1526        game.move_card(id, ZoneType::Battlefield, alice);
1527        apply_etb_tapped(&mut game, id);
1528
1529        assert!(
1530            game.card(id).tapped,
1531            "Card with ReplaceWith$ ETBTapped replacement should enter tapped"
1532        );
1533    }
1534}