Skip to main content

manabrew_engine/ability/effects/
move_counter_effect.rs

1use forge_foundation::ZoneType;
2
3use super::{resolve_numeric_svar, EffectContext};
4use crate::ability::ability_ir::DefinedRef;
5use crate::card::CounterType;
6use crate::event::RunParams;
7use crate::parsing::keys;
8use crate::trigger::TriggerType;
9
10/// `SP$ MoveCounter` — move counters from one permanent to another.
11///
12/// Mirrors Java's `CountersMoveEffect.java`.
13/// - `CounterType$` — type of counter to move (default P1P1).
14/// - `CounterNum$` — number of counters to move (default 1).
15/// - `Source$` — where to take counters from (default Self/source card).
16/// - `Defined$` — where to put counters (default Targeted).
17///
18/// # Card script examples
19/// ```text
20/// A:SP$ MoveCounter | CounterType$ P1P1 | CounterNum$ 1 | Source$ Self | Defined$ Targeted
21/// A:SP$ MoveCounter | CounterType$ CHARGE | CounterNum$ 2
22/// ```
23/// Struct form of this effect so it can participate in the
24/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
25/// `MoveCounterEffect` class extending `SpellAbilityEffect`.
26#[manabrew_engine_macros::spell_effect(MoveCounterEffect)]
27fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
28    let counter_type = sa.ir.counter_type.clone().unwrap_or(CounterType::P1P1);
29    let count = resolve_numeric_svar(ctx.game, sa, keys::COUNTER_NUM, 1);
30    if count <= 0 {
31        return;
32    }
33
34    // Determine source of counters
35    let from_id = match sa.ir.source_text.as_deref() {
36        Some(source) if matches!(DefinedRef::parse(source), DefinedRef::Targeted) => {
37            sa.target_chosen.target_card
38        }
39        Some(source) if matches!(DefinedRef::parse(source), DefinedRef::ParentTarget) => {
40            ctx.parent_target_card
41        }
42        _ => sa.source, // Default: Self
43    };
44
45    // Determine destination for counters
46    let to_id = sa
47        .target_chosen
48        .target_card
49        .or_else(|| match sa.defined_ref() {
50            Some(DefinedRef::SelfCard) => sa.source,
51            Some(DefinedRef::ParentTarget) => ctx.parent_target_card,
52            _ => None,
53        });
54
55    let from = match from_id {
56        Some(id) if ctx.game.card(id).zone == ZoneType::Battlefield => id,
57        _ => return,
58    };
59    let to = match to_id {
60        Some(id) if ctx.game.card(id).zone == ZoneType::Battlefield => id,
61        _ => return,
62    };
63
64    if from == to {
65        return;
66    }
67
68    // Calculate how many we can actually move
69    let available = ctx.game.card(from).counter_count(&counter_type);
70    let actual = count.min(available);
71    if actual <= 0 {
72        return;
73    }
74
75    if ctx.game.card(to).phased_out
76        || crate::staticability::static_ability_cant_put_counter::any_cant_put_counter_on_card(
77            &ctx.game.cards,
78            ctx.game.card(to),
79            &counter_type,
80        )
81    {
82        return;
83    }
84    ctx.game
85        .card_mut(from)
86        .remove_counter(&counter_type, actual);
87    ctx.trigger_handler.run_trigger(
88        TriggerType::CounterRemoved,
89        RunParams {
90            card: Some(from),
91            counter_type: Some(format!("{counter_type:?}")),
92            counter_amount: Some(actual),
93            ..Default::default()
94        },
95        false,
96    );
97    ctx.add_counter(to, &counter_type, actual, sa, RunParams::default());
98}