Skip to main content

manabrew_engine/ability/effects/
damage_all_effect.rs

1use forge_foundation::ZoneType;
2
3use super::{resolve_numeric_svar, EffectContext};
4use crate::ability::ability_ir::EffectIr;
5use crate::ability::AbilityKey;
6use crate::card::card_damage_map::DamageTarget;
7use crate::card::valid_filter::{self, MatchContext};
8use crate::ids::CardId;
9use crate::parsing::keys;
10use crate::spellability::SpellAbility;
11
12/// `SP$ DamageAll` — deal N damage to all matching permanents and/or players.
13///
14/// Mirrors Java's `DamageAllEffect.java`:
15/// - `ValidCards$` selects which battlefield permanents receive damage.
16/// - `ValidPlayers$` set to "Player" also deals damage to every player.
17/// - `NumDmg$` specifies the amount (integer or SVar reference).
18///
19/// # Card script examples
20/// ```text
21/// A:SP$ DamageAll | NumDmg$ 2 | ValidCards$ Creature
22/// A:SP$ DamageAll | ValidCards$ Creature.withFlying | ValidPlayers$ Player | NumDmg$ X
23/// ```
24/// Struct form of this effect so it can participate in the
25/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
26/// `DamageAllEffect` class extending `SpellAbilityEffect`.
27#[manabrew_engine_macros::spell_effect(DamageAllEffect)]
28fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
29    let num_dmg = resolve_damage_all_amount(ctx, sa);
30    if num_dmg <= 0 {
31        return;
32    }
33
34    let activating_player = sa.activating_player;
35    let valid_cards_filter = sa.ir.valid_cards_selector.as_ref();
36    let valid_players = sa.ir.valid_players_selector.as_ref();
37    let use_damage_map = ctx.game.pending_damage_map.is_some() || sa.ir.damage_map;
38    if sa.ir.damage_map {
39        ctx.game.ensure_pending_damage_maps();
40    }
41
42    let player_ids = ctx.game.player_order.clone();
43
44    // Pass 1 — collect matching battlefield permanents
45    let mut to_damage: Vec<CardId> = Vec::new();
46    if let Some(valid_cards_filter) = valid_cards_filter {
47        for &pid in &player_ids {
48            let zone_cards = ctx.game.cards_in_zone(ZoneType::Battlefield, pid).to_vec();
49            for cid in zone_cards {
50                if damage_all_matches_valid_card(ctx, sa, valid_cards_filter, cid) {
51                    to_damage.push(cid);
52                }
53            }
54        }
55    }
56
57    // Check source card for Infect/Wither keywords
58    let source = sa.source;
59    let (source_has_infect_keyword, source_has_wither) = if let Some(src_id) = source {
60        let src = ctx.game.card(src_id);
61        (
62            src.has_infect(),
63            src.has_wither()
64                || crate::staticability::static_ability_wither_damage::is_wither_damage(
65                    &ctx.game.cards,
66                    src,
67                ),
68        )
69    } else {
70        (false, false)
71    };
72
73    // Pass 2 — apply damage to collected permanents
74    let source = sa.source;
75    for card_id in to_damage {
76        if ctx.game.card(card_id).zone == ZoneType::Battlefield {
77            // Protection: prevents all damage from matching sources
78            if let Some(src_id) = source {
79                if crate::staticability::static_ability_colorless_damage_source::target_is_protected_from_source(
80                    &ctx.game.cards,
81                    ctx.game.card(card_id),
82                    ctx.game.card(src_id),
83                ) {
84                    continue;
85                }
86            }
87
88            // Track damage source for DamagedBy trigger filters
89            if let Some(src_id) = source {
90                if !ctx
91                    .game
92                    .card(card_id)
93                    .damage_sources_this_turn
94                    .contains(&src_id)
95                {
96                    ctx.game
97                        .card_mut(card_id)
98                        .damage_sources_this_turn
99                        .push(src_id);
100                }
101            }
102            if source_has_infect_keyword || source_has_wither {
103                // Infect/Wither: damage to creatures as -1/-1 counters
104                if use_damage_map {
105                    if let Some(src_id) = source {
106                        if let Some(map) = ctx.game.pending_damage_map.as_mut() {
107                            map.put(src_id, DamageTarget::Card(card_id), num_dmg);
108                        }
109                    }
110                } else if !crate::staticability::static_ability_cant_put_counter::any_cant_put_counter_on_card(
111                    &ctx.game.cards,
112                    ctx.game.card(card_id),
113                    &crate::card::CounterType::M1M1,
114                ) {
115                    crate::ability::effects::effect_context::add_counter_with_context(
116                        ctx.game,
117                        Some(ctx.trigger_handler),
118                        Some(ctx.agents),
119                        card_id,
120                        &crate::card::CounterType::M1M1,
121                        num_dmg,
122                        crate::event::RunParams {
123                            source_player: sa.source.map(|src_id| ctx.game.card(src_id).controller),
124                            cause: Some(sa.clone()),
125                            ..Default::default()
126                        },
127                        true,
128                    );
129                }
130            } else if use_damage_map {
131                if let Some(src_id) = source {
132                    if let Some(map) = ctx.game.pending_damage_map.as_mut() {
133                        map.put(src_id, DamageTarget::Card(card_id), num_dmg);
134                    }
135                }
136            } else {
137                ctx.game.deal_damage_to_card(card_id, num_dmg);
138            }
139
140            // Fire DamageDone trigger per card
141            if !use_damage_map {
142                ctx.trigger_handler.run_trigger(
143                    crate::trigger::TriggerType::DamageDone,
144                    crate::event::RunParams {
145                        damage_source: source,
146                        damage_target_card: Some(card_id),
147                        damage_amount: Some(num_dmg),
148                        is_combat_damage: Some(false),
149                        ..Default::default()
150                    },
151                    false,
152                );
153            }
154        }
155    }
156
157    // Deal damage to each matching player if ValidPlayers$ is set
158    if let Some(valid_players) = valid_players {
159        for pid in player_ids {
160            if !valid_filter::matches_valid_player_selector(valid_players, pid, activating_player) {
161                continue;
162            }
163            let source_has_infect = if let Some(src_id) = source {
164                let src = ctx.game.card(src_id);
165                source_has_infect_keyword
166                    || crate::staticability::static_ability_infect_damage::is_infect_damage(
167                        ctx.game,
168                        &ctx.game.cards,
169                        pid,
170                        src.controller,
171                    )
172            } else {
173                false
174            };
175            if source_has_infect {
176                if use_damage_map {
177                    if let Some(src_id) = source {
178                        if let Some(map) = ctx.game.pending_damage_map.as_mut() {
179                            map.put(src_id, DamageTarget::Player(pid), num_dmg);
180                        }
181                    }
182                } else {
183                    ctx.add_player_counter(
184                        pid,
185                        &crate::card::CounterType::Poison,
186                        num_dmg,
187                        sa,
188                        crate::event::RunParams {
189                            source_player: source.map(|source| ctx.game.card(source).controller),
190                            ..Default::default()
191                        },
192                    );
193                }
194            } else if use_damage_map {
195                if let Some(src_id) = source {
196                    if let Some(map) = ctx.game.pending_damage_map.as_mut() {
197                        map.put(src_id, DamageTarget::Player(pid), num_dmg);
198                    }
199                }
200            } else {
201                let dealt = ctx.game.deal_damage_to_player(pid, num_dmg);
202                ctx.game
203                    .record_player_damage_assignment(source, Some(pid), dealt, false);
204            }
205
206            // Fire DamageDone trigger per player
207            if !use_damage_map {
208                ctx.trigger_handler.run_trigger(
209                    crate::trigger::TriggerType::DamageDone,
210                    crate::event::RunParams {
211                        damage_source: source,
212                        damage_target_player: Some(pid),
213                        damage_amount: Some(num_dmg),
214                        is_combat_damage: Some(false),
215                        ..Default::default()
216                    },
217                    false,
218                );
219            }
220        }
221    }
222}
223
224fn resolve_damage_all_amount(ctx: &EffectContext, sa: &SpellAbility) -> i32 {
225    if let Some(EffectIr::DamageAll(ir)) = &sa.ir.effect {
226        if let Some(amount) = &ir.amount {
227            let resolved = amount.resolve_for_spell_ability(ctx.game, sa, 0);
228            #[cfg(debug_assertions)]
229            debug_assert_eq!(
230                resolved,
231                resolve_numeric_svar(ctx.game, sa, keys::NUM_DMG, 0),
232                "compiled DamageAll amount diverged from string params"
233            );
234            return resolved;
235        }
236    }
237
238    resolve_numeric_svar(ctx.game, sa, keys::NUM_DMG, 0)
239}
240
241fn damage_all_matches_valid_card(
242    ctx: &EffectContext,
243    sa: &SpellAbility,
244    selector: &crate::parsing::CompiledSelector,
245    card_id: CardId,
246) -> bool {
247    let Some(source_id) = sa.source else {
248        return crate::ability::ability_utils::matches_valid_cards_selector_opt(
249            Some(selector),
250            ctx.game.card(card_id),
251            sa.activating_player,
252        );
253    };
254    let source = ctx.game.card(source_id);
255    let targeted_cards = sa.target_chosen.all_target_cards();
256    let targeted_players = sa.target_chosen.all_target_players();
257    let triggering_card = sa.get_triggering_card(AbilityKey::Target);
258    let triggering_player = sa.get_triggering_player(AbilityKey::Target);
259    valid_filter::matches_valid_card_selector_with_context(
260        selector,
261        ctx.game.card(card_id),
262        MatchContext::from_source(source)
263            .with_game(ctx.game)
264            .with_targets(&targeted_cards, &targeted_players)
265            .with_triggering(triggering_card, triggering_player),
266    )
267}
268
269#[cfg(test)]
270mod tests {
271    use crate::ability::spell_ability_effect::SpellAbilityEffect;
272    use forge_foundation::{CardTypeLine, ColorSet, ManaCost, ZoneType};
273    use std::collections::HashMap;
274
275    use crate::ability::effects::EffectContext;
276    use crate::agent::PassAgent;
277    use crate::card::Card;
278    use crate::game::GameState;
279    use crate::ids::{CardId, PlayerId};
280    use crate::mana::ManaPool;
281    use crate::spellability::SpellAbility;
282    use crate::trigger::handler::TriggerHandler;
283
284    fn make_creature(game: &mut GameState, owner: PlayerId) -> CardId {
285        let c = Card::new(
286            CardId(0),
287            "Bear".into(),
288            owner,
289            CardTypeLine::parse("Creature - Bear"),
290            ManaCost::parse("1 G"),
291            ColorSet::GREEN,
292            Some(2),
293            Some(2),
294            vec![],
295            vec![],
296        );
297        game.create_card(c)
298    }
299
300    #[test]
301    fn damage_all_deals_to_each_creature() {
302        let mut game = GameState::new(&["Alice", "Bob"], 20);
303        let p0 = PlayerId(0);
304        let p1 = PlayerId(1);
305        let c1 = make_creature(&mut game, p0);
306        let c2 = make_creature(&mut game, p1);
307        game.move_card(c1, ZoneType::Battlefield, p0);
308        game.move_card(c2, ZoneType::Battlefield, p1);
309
310        let sa = SpellAbility::new_simple(
311            None,
312            p0,
313            "A:SP$ DamageAll | NumDmg$ 2 | ValidCards$ Creature",
314        );
315        let mut th = TriggerHandler::new();
316        let mut agents: Vec<Box<dyn crate::agent::PlayerAgent>> =
317            vec![Box::new(PassAgent), Box::new(PassAgent)];
318        let mut mp = vec![ManaPool::default(), ManaPool::default()];
319        let templates = HashMap::new();
320        let templates_variants = HashMap::new();
321        let token_fallback = HashMap::new();
322        let edition_dates: HashMap<String, String> = HashMap::new();
323        let mut rng_adapter = crate::game_rng::ThreadRngAdapter;
324        let mut ctx = EffectContext {
325            game: &mut game,
326            combat: None,
327            agents: &mut agents,
328            trigger_handler: &mut th,
329            token_templates: &templates,
330            token_art_variants: &templates_variants,
331            token_fallback: &token_fallback,
332            edition_dates: &edition_dates,
333            mana_pools: &mut mp,
334            parent_target_card: None,
335            rng: &mut rng_adapter,
336        };
337        super::DamageAllEffect::resolve(&mut ctx, &sa);
338
339        assert_eq!(ctx.game.card(c1).damage, 2);
340        assert_eq!(ctx.game.card(c2).damage, 2);
341        // Players not affected (no ValidPlayers$)
342        assert_eq!(ctx.game.player(p0).life, 20);
343        assert_eq!(ctx.game.player(p1).life, 20);
344    }
345
346    #[test]
347    fn damage_all_with_valid_players() {
348        let mut game = GameState::new(&["Alice", "Bob"], 20);
349        let p0 = PlayerId(0);
350        let p1 = PlayerId(1);
351
352        let sa = SpellAbility::new_simple(
353            None,
354            p0,
355            "A:SP$ DamageAll | NumDmg$ 3 | ValidCards$ | ValidPlayers$ Player",
356        );
357        let mut th = TriggerHandler::new();
358        let mut agents: Vec<Box<dyn crate::agent::PlayerAgent>> =
359            vec![Box::new(PassAgent), Box::new(PassAgent)];
360        let mut mp = vec![ManaPool::default(), ManaPool::default()];
361        let templates = HashMap::new();
362        let templates_variants = HashMap::new();
363        let token_fallback = HashMap::new();
364        let edition_dates: HashMap<String, String> = HashMap::new();
365        let mut rng_adapter = crate::game_rng::ThreadRngAdapter;
366        let mut ctx = EffectContext {
367            game: &mut game,
368            combat: None,
369            agents: &mut agents,
370            trigger_handler: &mut th,
371            token_templates: &templates,
372            token_art_variants: &templates_variants,
373            token_fallback: &token_fallback,
374            edition_dates: &edition_dates,
375            mana_pools: &mut mp,
376            parent_target_card: None,
377            rng: &mut rng_adapter,
378        };
379        super::DamageAllEffect::resolve(&mut ctx, &sa);
380
381        assert_eq!(ctx.game.player(p0).life, 17);
382        assert_eq!(ctx.game.player(p1).life, 17);
383    }
384}