Skip to main content

manabrew_engine/ability/effects/
destroy_all_effect.rs

1use forge_foundation::ZoneType;
2
3use super::{emit_zone_trigger_with_lki_counters, matches_valid_cards_for_sa, EffectContext};
4use crate::event::RunParams;
5use crate::ids::CardId;
6use crate::replacement::replacement_handler::{apply_replacements, ReplacementEvent};
7use crate::replacement::ReplacementResult;
8use crate::trigger::TriggerType;
9
10/// `SP$ DestroyAll` — destroy all permanents matching `ValidCards$`.
11///
12/// Mirrors Java's `DestroyAllEffect.java`:
13/// - Collects all matching battlefield cards (two-pass to avoid borrow issues).
14/// - Respects `Indestructible` (keyword or R$-based replacement effect).
15/// - `NoRegen$ True` is noted but regeneration is not yet implemented, so it
16///   has no runtime effect.
17///
18/// # Card script examples
19/// ```text
20/// A:SP$ DestroyAll | ValidCards$ Creature | NoRegen$ True
21/// A:SP$ DestroyAll | ValidCards$ Permanent.nonArtifact
22/// ```
23/// Struct form of this effect so it can participate in the
24/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
25/// `DestroyAllEffect` class extending `SpellAbilityEffect`.
26#[manabrew_engine_macros::spell_effect(DestroyAllEffect)]
27fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
28    let valid_cards = sa.ir.valid_cards_selector.as_ref();
29
30    // Pass 1 — collect matching battlefield cards
31    let player_ids = ctx.game.player_order.clone();
32    let mut to_destroy: Vec<CardId> = Vec::new();
33    for &pid in &player_ids {
34        let zone_cards = ctx.game.cards_in_zone(ZoneType::Battlefield, pid).to_vec();
35        for cid in zone_cards {
36            if matches_valid_cards_for_sa(ctx.game, sa, ctx.game.card(cid), valid_cards, "Creature")
37            {
38                to_destroy.push(cid);
39            }
40        }
41    }
42
43    // Pass 2 — destroy each card, respecting Indestructible
44    for card_id in to_destroy {
45        if ctx.game.card(card_id).zone != ZoneType::Battlefield {
46            continue; // May have already left (e.g. legendary rule, previous step)
47        }
48        // K:Indestructible keyword fast path (CR 702.12)
49        if ctx.game.card(card_id).has_keyword("Indestructible") {
50            continue;
51        }
52        // R$-based Destroy replacement (e.g. Darksteel Myr's replacement effect)
53        let mut destroy_event = ReplacementEvent::Destroy { target: card_id };
54        let result = apply_replacements(ctx.game, &mut destroy_event);
55        if result == ReplacementResult::Replaced {
56            continue;
57        }
58        let owner = ctx.game.card(card_id).owner;
59        // Capture +1/+1 counter count before move (for Modular death triggers)
60        let lki_p1p1 = *ctx
61            .game
62            .card(card_id)
63            .counters
64            .get(&crate::card::CounterType::P1P1)
65            .unwrap_or(&0);
66        // Capture LKI counters for death triggers
67        {
68            let lki_counters = ctx.game.card(card_id).counters.clone();
69            let lki_power = ctx.game.card(card_id).power();
70            let lki_toughness = ctx.game.card(card_id).toughness();
71            ctx.game.card_mut(card_id).lki_counters = Some(lki_counters);
72            ctx.game
73                .card_mut(card_id)
74                .set_lki_power_toughness(Some(lki_power), Some(lki_toughness));
75        }
76        ctx.move_card(card_id, ZoneType::Graveyard, owner);
77        ctx.trigger_handler.run_trigger(
78            TriggerType::Destroyed,
79            RunParams {
80                card: Some(card_id),
81                causer: sa.source,
82                cause_card: sa.source,
83                cause_player: Some(sa.activating_player),
84                ..Default::default()
85            },
86            false,
87        );
88        emit_zone_trigger_with_lki_counters(
89            ctx.trigger_handler,
90            card_id,
91            ZoneType::Battlefield,
92            ZoneType::Graveyard,
93            lki_p1p1,
94            ctx.game
95                .card(card_id)
96                .lki_power
97                .unwrap_or_else(|| ctx.game.card(card_id).power()),
98            ctx.game
99                .card(card_id)
100                .lki_toughness
101                .unwrap_or_else(|| ctx.game.card(card_id).toughness()),
102        );
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use crate::ability::spell_ability_effect::SpellAbilityEffect;
109    use forge_foundation::{CardTypeLine, ColorSet, ManaCost, ZoneType};
110    use std::collections::HashMap;
111
112    use crate::ability::effects::EffectContext;
113    use crate::agent::PassAgent;
114    use crate::card::Card;
115    use crate::game::GameState;
116    use crate::ids::{CardId, PlayerId};
117    use crate::mana::ManaPool;
118    use crate::spellability::SpellAbility;
119    use crate::trigger::handler::TriggerHandler;
120
121    fn make_creature(game: &mut GameState, owner: PlayerId, keywords: Vec<String>) -> CardId {
122        let c = Card::new(
123            CardId(0),
124            "Bear".into(),
125            owner,
126            CardTypeLine::parse("Creature - Bear"),
127            ManaCost::parse("1 G"),
128            ColorSet::GREEN,
129            Some(2),
130            Some(2),
131            keywords,
132            vec![],
133        );
134        game.create_card(c)
135    }
136
137    fn make_ctx<'a>(
138        game: &'a mut GameState,
139        agents: &'a mut Vec<Box<dyn crate::agent::PlayerAgent>>,
140        trigger_handler: &'a mut TriggerHandler,
141        mana_pools: &'a mut Vec<ManaPool>,
142        token_templates: &'a HashMap<String, Card>,
143        token_art_variants: &'a HashMap<(String, String), usize>,
144        token_fallback: &'a HashMap<String, String>,
145        edition_dates: &'a HashMap<String, String>,
146        rng: &'a mut dyn crate::game_rng::GameRng,
147    ) -> EffectContext<'a> {
148        EffectContext {
149            game,
150            combat: None,
151            agents,
152            trigger_handler,
153            token_templates,
154            token_art_variants,
155            token_fallback,
156            edition_dates,
157            mana_pools,
158            parent_target_card: None,
159            rng,
160        }
161    }
162
163    #[test]
164    fn destroy_all_wipes_creatures() {
165        let mut game = GameState::new(&["Alice", "Bob"], 20);
166        let p0 = PlayerId(0);
167        let p1 = PlayerId(1);
168        let c1 = make_creature(&mut game, p0, vec![]);
169        let c2 = make_creature(&mut game, p1, vec![]);
170        game.move_card(c1, ZoneType::Battlefield, p0);
171        game.move_card(c2, ZoneType::Battlefield, p1);
172
173        let sa = SpellAbility::new_simple(
174            None,
175            p0,
176            "A:SP$ DestroyAll | ValidCards$ Creature | NoRegen$ True",
177        );
178        let mut th = TriggerHandler::new();
179        let mut agents: Vec<Box<dyn crate::agent::PlayerAgent>> =
180            vec![Box::new(PassAgent), Box::new(PassAgent)];
181        let mut mp = vec![ManaPool::default(), ManaPool::default()];
182        let templates = HashMap::new();
183        let templates_variants: HashMap<(String, String), usize> = HashMap::new();
184        let token_fallback: HashMap<String, String> = HashMap::new();
185        let edition_dates: HashMap<String, String> = HashMap::new();
186        let mut rng_adapter = crate::game_rng::ThreadRngAdapter;
187        let mut ctx = make_ctx(
188            &mut game,
189            &mut agents,
190            &mut th,
191            &mut mp,
192            &templates,
193            &templates_variants,
194            &token_fallback,
195            &edition_dates,
196            &mut rng_adapter,
197        );
198        super::DestroyAllEffect::resolve(&mut ctx, &sa);
199
200        assert_eq!(ctx.game.cards_in_zone(ZoneType::Battlefield, p0).len(), 0);
201        assert_eq!(ctx.game.cards_in_zone(ZoneType::Battlefield, p1).len(), 0);
202        assert_eq!(ctx.game.cards_in_zone(ZoneType::Graveyard, p0).len(), 1);
203        assert_eq!(ctx.game.cards_in_zone(ZoneType::Graveyard, p1).len(), 1);
204    }
205
206    #[test]
207    fn destroy_all_indestructible_survives() {
208        let mut game = GameState::new(&["Alice", "Bob"], 20);
209        let p0 = PlayerId(0);
210        let mortal = make_creature(&mut game, p0, vec![]);
211        let immortal = make_creature(&mut game, p0, vec!["Indestructible".to_string()]);
212        game.move_card(mortal, ZoneType::Battlefield, p0);
213        game.move_card(immortal, ZoneType::Battlefield, p0);
214
215        let sa = SpellAbility::new_simple(
216            None,
217            p0,
218            "A:SP$ DestroyAll | ValidCards$ Creature | NoRegen$ True",
219        );
220        let mut th = TriggerHandler::new();
221        let mut agents: Vec<Box<dyn crate::agent::PlayerAgent>> =
222            vec![Box::new(PassAgent), Box::new(PassAgent)];
223        let mut mp = vec![ManaPool::default(), ManaPool::default()];
224        let templates = HashMap::new();
225        let templates_variants: HashMap<(String, String), usize> = HashMap::new();
226        let token_fallback: HashMap<String, String> = HashMap::new();
227        let edition_dates: HashMap<String, String> = HashMap::new();
228        let mut rng_adapter = crate::game_rng::ThreadRngAdapter;
229        let mut ctx = make_ctx(
230            &mut game,
231            &mut agents,
232            &mut th,
233            &mut mp,
234            &templates,
235            &templates_variants,
236            &token_fallback,
237            &edition_dates,
238            &mut rng_adapter,
239        );
240        super::DestroyAllEffect::resolve(&mut ctx, &sa);
241
242        // One creature destroyed, indestructible one stays
243        assert_eq!(ctx.game.cards_in_zone(ZoneType::Battlefield, p0).len(), 1);
244        assert_eq!(ctx.game.cards_in_zone(ZoneType::Graveyard, p0).len(), 1);
245        assert!(ctx.game.card(immortal).has_keyword("Indestructible"));
246    }
247}