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