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}