Skip to main content

manabrew_engine/ability/effects/
counter_effect.rs

1use forge_foundation::ZoneType;
2
3use super::{emit_zone_trigger, EffectContext};
4use crate::event::RunParams;
5use crate::replacement::replacement_handler::{apply_replacements, ReplacementEvent};
6use crate::replacement::ReplacementResult;
7use crate::spellability::SpellAbility;
8use crate::trigger::TriggerType;
9
10/// Build/configure the spell ability after construction.
11/// Mirrors Java's `CounterEffect.buildSpellAbility(SpellAbility)`.
12///
13/// Counter effects target spells or abilities on the stack when they use
14/// targeting, matching Java's `TargetRestrictions.setZone(ZoneType.Stack)`.
15pub fn build_spell_ability(sa: &mut crate::spellability::SpellAbility) {
16    if sa.uses_targeting() {
17        if let Some(ref mut tr) = sa.target_restrictions {
18            tr.tgt_zone = vec![ZoneType::Stack];
19        }
20    }
21}
22
23/// Check if the "would be destroyed" condition is met for a conditional counter.
24/// Mirrors Java's `CounterEffect.checkForConditionWouldDestroy(...)`.
25///
26/// Some counter effects have conditions like "Counter target spell if it would
27/// destroy a permanent you control" (e.g. Rebuff the Wicked). This checks
28/// whether the targeted spell would cause destruction.
29pub fn check_for_condition_would_destroy(
30    game: &crate::game::GameState,
31    sa: &SpellAbility,
32    target_stack_id: u32,
33) -> bool {
34    let entry = match game.stack.find_by_id(target_stack_id) {
35        Some(e) => e,
36        None => return false,
37    };
38
39    let targeted_sa = &entry.spell_ability;
40    let controller = sa.activating_player;
41
42    // Check if the targeted spell is a Destroy effect aimed at our permanents
43    if let Some(api) = targeted_sa.api {
44        match api {
45            crate::ability::api_type::ApiType::Destroy
46            | crate::ability::api_type::ApiType::DestroyAll => {
47                // Check if any of our permanents would be affected
48                if let Some(target_card) = targeted_sa.target_chosen.target_card {
49                    return game.card(target_card).controller == controller;
50                }
51                // For DestroyAll, check the ValidCards filter
52                if let Some(valid) = targeted_sa.ir.valid_cards_selector.as_ref() {
53                    let our_permanents =
54                        game.cards_in_zone(forge_foundation::ZoneType::Battlefield, controller);
55                    return our_permanents.iter().any(|&cid| {
56                        crate::ability::ability_utils::matches_valid_cards_selector_opt(
57                            Some(valid),
58                            game.card(cid),
59                            targeted_sa.activating_player,
60                        )
61                    });
62                }
63                return true; // Assume it would destroy something
64            }
65            _ => {}
66        }
67    }
68
69    false
70}
71
72/// SP$ Counter — remove a targeted spell from the stack and put it into
73/// its owner's graveyard (or exile, per Destination$ if present).
74///
75/// Supports `UnlessCost$` — if present, the targeted spell's controller is
76/// prompted to pay; if they accept, the spell is NOT countered.
77/// Mirrors Java's `CounterEffect.resolve()`.
78/// Struct form of this effect so it can participate in the
79/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
80/// `CounterEffect` class extending `SpellAbilityEffect`.
81#[manabrew_engine_macros::spell_effect(CounterEffect)]
82fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
83    let entry_id = match counter_target_stack_entry_id(ctx, sa) {
84        Some(id) => id,
85        None => return, // no target chosen
86    };
87
88    // Determine destination (default: graveyard).
89    let dest_zone = sa.ir.destination_zone.unwrap_or(ZoneType::Graveyard);
90
91    // Check if the spell has a "can't be countered" replacement effect.
92    // Find the source card of the targeted stack entry.
93    if let Some(entry) = ctx.game.stack.find_by_id(entry_id) {
94        if let Some(source_card) = entry.spell_ability.source {
95            if sa.ir.remember_for_counter {
96                if let Some(source_id) = sa.source {
97                    ctx.game
98                        .card_mut(source_id)
99                        .add_remembered_card(source_card);
100                }
101            }
102            if sa.ir.remember_countered_cmc {
103                let cmc = ctx.game.card(source_card).mana_cost.cmc();
104                if let Some(source_id) = sa.source {
105                    ctx.game.card_mut(source_id).add_remembered_cmc(cmc);
106                }
107            }
108            let mut event = ReplacementEvent::Counter { card: source_card };
109            let result = apply_replacements(ctx.game, &mut event);
110            if result == ReplacementResult::Replaced {
111                return;
112            }
113        }
114    }
115
116    // Remove from stack
117    if let Some(entry) = ctx.game.stack.remove_by_id(entry_id) {
118        let countered_sa = &entry.spell_ability;
119        if let Some(source_card) = countered_sa.source {
120            // Only move if the card is still "virtual" (on the stack, zone = None is fine)
121            // — it was removed from hand when cast; move it to dest zone now.
122            let owner = ctx.game.card(source_card).owner;
123
124            // Remember parameters if needed
125            if sa.ir.remember_countered {
126                ctx.game
127                    .card_mut(sa.source.unwrap())
128                    .add_remembered_card(source_card);
129            }
130
131            if !countered_sa.is_activated && !countered_sa.is_trigger {
132                ctx.move_card(source_card, dest_zone, owner);
133                emit_zone_trigger(ctx.trigger_handler, source_card, ZoneType::Stack, dest_zone);
134            }
135
136            // Fire Countered trigger
137            ctx.trigger_handler.run_trigger(
138                TriggerType::Countered,
139                RunParams {
140                    card: Some(source_card),
141                    spell_ability: Some(countered_sa.clone()),
142                    cause: Some(sa.clone()),
143                    ..Default::default()
144                },
145                false,
146            );
147        }
148    }
149}
150
151fn counter_target_stack_entry_id(ctx: &EffectContext, sa: &SpellAbility) -> Option<u32> {
152    if let Some(id) = sa.target_chosen.target_stack_entry {
153        return Some(id);
154    }
155
156    let defined = sa.defined()?;
157    let defined_spells =
158        crate::ability::ability_utils::get_defined_spell_abilities(defined, sa, ctx.game);
159    let mut candidate_sources: Vec<crate::ids::CardId> = defined_spells
160        .iter()
161        .filter_map(|defined_sa| defined_sa.source)
162        .collect();
163    if defined == "TriggeredSpellAbility" || defined == "TriggeredSourceSA" {
164        if let Some(source) = sa
165            .trigger_objects
166            .get(&crate::ability::AbilityKey::Source)
167            .and_then(|value| value.parse::<u32>().ok())
168            .map(crate::ids::CardId)
169        {
170            if !candidate_sources.contains(&source) {
171                candidate_sources.push(source);
172            }
173        }
174    }
175
176    for defined_sa in defined_spells {
177        if let Some(source) = defined_sa.source {
178            let stack_entries: Vec<_> = ctx.game.stack.iter().collect();
179            for entry in stack_entries.iter().rev() {
180                if entry.spell_ability.source == Some(source)
181                    && entry.spell_ability.ability_text == defined_sa.ability_text
182                {
183                    return Some(entry.id);
184                }
185            }
186            for entry in stack_entries.iter().rev() {
187                if entry.spell_ability.source == Some(source) {
188                    return Some(entry.id);
189                }
190            }
191        }
192    }
193    for source in candidate_sources {
194        let stack_entries: Vec<_> = ctx.game.stack.iter().collect();
195        for entry in stack_entries.iter().rev() {
196            if entry.spell_ability.source == Some(source) {
197                return Some(entry.id);
198            }
199        }
200    }
201
202    None
203}