Skip to main content

manabrew_engine/ability/effects/
counters_remove_effect.rs

1use forge_foundation::ZoneType;
2
3use super::{parse_counter_type, EffectContext};
4use crate::event::RunParams;
5use crate::ids::CardId;
6use crate::replacement::replacement_handler::{apply_replacements, ReplacementEvent};
7use crate::replacement::ReplacementResult;
8use crate::spellability::SpellAbility;
9use crate::trigger::TriggerType;
10
11/// Resolve `DB$ RemoveCounter` / `AB$ RemoveCounter` / `SP$ RemoveCounter`.
12///
13/// Mirrors Java `CountersRemoveEffect.java` (~280 lines). This is an MVP
14/// covering the most common modes (~80% of real card usage):
15///
16/// - `Defined$ Self` or targeted card
17/// - Specific `CounterType$` (not "Any" / "All" type modes)
18/// - `CounterNum$` as integer or `"All"`
19/// - Fires `TriggerType::CounterRemoved`
20///
21/// Deferred (would need UI interaction loops):
22/// - `CounterType$ Any` / `CounterType$ All` (interactive counter type selection)
23/// - `Choices$` / `ChoiceZone$` (card selection)
24/// - `Optional$` (confirmation dialog)
25/// - `UpTo$` (number selection)
26/// - `CounterNumShared$` (shared budget across targets)
27/// - Player counter removal
28///
29/// Real card patterns (MVP):
30/// - `DB$ RemoveCounter | Defined$ Self | CounterType$ P1P1 | CounterNum$ 1`
31/// - `DB$ RemoveCounter | Defined$ Self | CounterType$ TIME | CounterNum$ 1`
32/// - `DB$ RemoveCounter | Defined$ Self | CounterType$ FADE | CounterNum$ 1`
33/// - `AB$ RemoveCounter | ValidTgts$ Creature | CounterType$ M1M1 | CounterNum$ 1`
34/// - `SP$ RemoveCounter | ValidTgts$ Creature.YouCtrl | CounterType$ P1P1 | CounterNum$ All`
35/// Struct form of this effect so it can participate in the
36/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
37/// `CountersRemoveEffect` class extending `SpellAbilityEffect`.
38#[manabrew_engine_macros::spell_effect(CountersRemoveEffect)]
39fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
40    // Parse counter type — skip unsupported "Any" / "All" type modes.
41    let counter_type_str = sa.ir.counter_type_text.as_deref().unwrap_or("P1P1");
42    if counter_type_str.eq_ignore_ascii_case("Any") || counter_type_str.eq_ignore_ascii_case("All")
43    {
44        // Interactive counter type selection — not yet supported.
45        return;
46    }
47    let counter_type = parse_counter_type(counter_type_str);
48
49    // Parse amount: integer or "All" (resolve per-card).
50    let remove_all = sa
51        .ir
52        .counter_num_text
53        .as_deref()
54        .is_some_and(|s| s.eq_ignore_ascii_case("All"));
55    let requested_count = if remove_all {
56        i32::MAX // will be clamped per-card
57    } else {
58        sa.ir
59            .counter_num_text
60            .as_deref()
61            .and_then(|s| s.parse::<i32>().ok())
62            .unwrap_or(1)
63    };
64
65    // Resolve target card.
66    let target_card = resolve_target_card(ctx, sa);
67    let Some(card_id) = target_card else {
68        return;
69    };
70
71    // Verify card is on the battlefield.
72    if ctx.game.card(card_id).zone != ZoneType::Battlefield {
73        return;
74    }
75
76    // Run RemoveCounter replacement effects before removing.
77    let mut event = ReplacementEvent::RemoveCounter {
78        target: card_id,
79        counter_type: counter_type.clone(),
80        count: requested_count,
81    };
82    let result = apply_replacements(ctx.game, &mut event);
83    if result == ReplacementResult::Skipped || result == ReplacementResult::Replaced {
84        return;
85    }
86    let requested_count = if let ReplacementEvent::RemoveCounter { count, .. } = event {
87        count
88    } else {
89        requested_count
90    };
91
92    // Compute actual removal count (can't remove more than present).
93    let current = ctx.game.card(card_id).counter_count(&counter_type);
94    let actual = requested_count.min(current);
95    if actual <= 0 {
96        return;
97    }
98
99    // Remove counters.
100    ctx.game
101        .card_mut(card_id)
102        .remove_counter(&counter_type, actual);
103
104    // Fire CounterRemoved trigger.
105    ctx.trigger_handler.run_trigger(
106        TriggerType::CounterRemoved,
107        RunParams {
108            card: Some(card_id),
109            counter_type: Some(format!("{counter_type:?}")),
110            counter_amount: Some(actual),
111            ..Default::default()
112        },
113        false,
114    );
115}
116
117/// Resolve the target card for counter removal.
118/// Checks `Defined$ Self`, `Defined$ Targeted` / `Defined$ ParentTarget`,
119/// and targeting (target_chosen.target_card).
120fn resolve_target_card(ctx: &EffectContext, sa: &SpellAbility) -> Option<CardId> {
121    // Explicit targeting takes priority.
122    if let Some(card_id) = sa.target_chosen.target_card {
123        return Some(card_id);
124    }
125
126    let defined = sa.defined().unwrap_or("Self");
127
128    match defined {
129        "Self" => sa.source,
130        "ParentTarget" => ctx.parent_target_card,
131        "Targeted" => sa.target_chosen.target_card,
132        _ => sa.source, // fallback to source
133    }
134}