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 valid_cards_filter.is_some() {
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.unwrap(), 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                    ctx.game
116                        .card_mut(card_id)
117                        .add_counter(&crate::card::CounterType::M1M1, num_dmg);
118                }
119            } else if use_damage_map {
120                if let Some(src_id) = source {
121                    if let Some(map) = ctx.game.pending_damage_map.as_mut() {
122                        map.put(src_id, DamageTarget::Card(card_id), num_dmg);
123                    }
124                }
125            } else {
126                ctx.game.deal_damage_to_card(card_id, num_dmg);
127            }
128
129            // Fire DamageDone trigger per card
130            if !use_damage_map {
131                ctx.trigger_handler.run_trigger(
132                    crate::trigger::TriggerType::DamageDone,
133                    crate::event::RunParams {
134                        damage_source: source,
135                        damage_target_card: Some(card_id),
136                        damage_amount: Some(num_dmg),
137                        is_combat_damage: Some(false),
138                        ..Default::default()
139                    },
140                    false,
141                );
142            }
143        }
144    }
145
146    // Deal damage to each matching player if ValidPlayers$ is set
147    if let Some(valid_players) = valid_players {
148        for pid in player_ids {
149            if !valid_filter::matches_valid_player_selector(valid_players, pid, activating_player) {
150                continue;
151            }
152            let source_has_infect = if let Some(src_id) = source {
153                let src = ctx.game.card(src_id);
154                source_has_infect_keyword
155                    || crate::staticability::static_ability_infect_damage::is_infect_damage(
156                        ctx.game,
157                        &ctx.game.cards,
158                        pid,
159                        src.controller,
160                    )
161            } else {
162                false
163            };
164            if source_has_infect {
165                if use_damage_map {
166                    if let Some(src_id) = source {
167                        if let Some(map) = ctx.game.pending_damage_map.as_mut() {
168                            map.put(src_id, DamageTarget::Player(pid), num_dmg);
169                        }
170                    }
171                } else if !crate::staticability::static_ability_cant_put_counter::any_cant_put_counter_on_player(
172                    &ctx.game.cards,
173                    pid,
174                    &crate::card::CounterType::Poison,
175                ) {
176                    ctx.game.player_add_poison(pid, num_dmg);
177                }
178            } else if use_damage_map {
179                if let Some(src_id) = source {
180                    if let Some(map) = ctx.game.pending_damage_map.as_mut() {
181                        map.put(src_id, DamageTarget::Player(pid), num_dmg);
182                    }
183                }
184            } else {
185                let dealt = ctx.game.deal_damage_to_player(pid, num_dmg);
186                ctx.game
187                    .record_player_damage_assignment(source, Some(pid), dealt, false);
188            }
189
190            // Fire DamageDone trigger per player
191            if !use_damage_map {
192                ctx.trigger_handler.run_trigger(
193                    crate::trigger::TriggerType::DamageDone,
194                    crate::event::RunParams {
195                        damage_source: source,
196                        damage_target_player: Some(pid),
197                        damage_amount: Some(num_dmg),
198                        is_combat_damage: Some(false),
199                        ..Default::default()
200                    },
201                    false,
202                );
203            }
204        }
205    }
206}
207
208fn resolve_damage_all_amount(ctx: &EffectContext, sa: &SpellAbility) -> i32 {
209    if let Some(EffectIr::DamageAll(ir)) = &sa.ir.effect {
210        if let Some(amount) = &ir.amount {
211            let resolved = amount.resolve_for_spell_ability(ctx.game, sa, 0);
212            #[cfg(debug_assertions)]
213            debug_assert_eq!(
214                resolved,
215                resolve_numeric_svar(ctx.game, sa, keys::NUM_DMG, 0),
216                "compiled DamageAll amount diverged from string params"
217            );
218            return resolved;
219        }
220    }
221
222    resolve_numeric_svar(ctx.game, sa, keys::NUM_DMG, 0)
223}
224
225fn damage_all_matches_valid_card(
226    ctx: &EffectContext,
227    sa: &SpellAbility,
228    selector: &crate::parsing::CompiledSelector,
229    card_id: CardId,
230) -> bool {
231    let Some(source_id) = sa.source else {
232        return crate::ability::ability_utils::matches_valid_cards_selector_opt(
233            Some(selector),
234            ctx.game.card(card_id),
235            sa.activating_player,
236        );
237    };
238    let source = ctx.game.card(source_id);
239    let targeted_cards = sa.target_chosen.all_target_cards();
240    let targeted_players = sa.target_chosen.all_target_players();
241    let triggering_card = sa.get_triggering_card(AbilityKey::Target);
242    let triggering_player = sa.get_triggering_player(AbilityKey::Target);
243    valid_filter::matches_valid_card_selector_with_context(
244        selector,
245        ctx.game.card(card_id),
246        MatchContext::from_source(source)
247            .with_game(ctx.game)
248            .with_targets(&targeted_cards, &targeted_players)
249            .with_triggering(triggering_card, triggering_player),
250    )
251}
252
253#[cfg(test)]
254mod tests {
255    use crate::ability::spell_ability_effect::SpellAbilityEffect;
256    use forge_foundation::{CardTypeLine, ColorSet, ManaCost, ZoneType};
257    use std::collections::HashMap;
258
259    use crate::ability::effects::EffectContext;
260    use crate::agent::PassAgent;
261    use crate::card::Card;
262    use crate::game::GameState;
263    use crate::ids::{CardId, PlayerId};
264    use crate::mana::ManaPool;
265    use crate::spellability::SpellAbility;
266    use crate::trigger::handler::TriggerHandler;
267
268    fn make_creature(game: &mut GameState, owner: PlayerId) -> CardId {
269        let c = Card::new(
270            CardId(0),
271            "Bear".into(),
272            owner,
273            CardTypeLine::parse("Creature - Bear"),
274            ManaCost::parse("1 G"),
275            ColorSet::GREEN,
276            Some(2),
277            Some(2),
278            vec![],
279            vec![],
280        );
281        game.create_card(c)
282    }
283
284    #[test]
285    fn damage_all_deals_to_each_creature() {
286        let mut game = GameState::new(&["Alice", "Bob"], 20);
287        let p0 = PlayerId(0);
288        let p1 = PlayerId(1);
289        let c1 = make_creature(&mut game, p0);
290        let c2 = make_creature(&mut game, p1);
291        game.move_card(c1, ZoneType::Battlefield, p0);
292        game.move_card(c2, ZoneType::Battlefield, p1);
293
294        let sa = SpellAbility::new_simple(
295            None,
296            p0,
297            "A:SP$ DamageAll | NumDmg$ 2 | ValidCards$ Creature",
298        );
299        let mut th = TriggerHandler::new();
300        let mut agents: Vec<Box<dyn crate::agent::PlayerAgent>> =
301            vec![Box::new(PassAgent), Box::new(PassAgent)];
302        let mut mp = vec![ManaPool::default(), ManaPool::default()];
303        let templates = HashMap::new();
304        let templates_variants = HashMap::new();
305        let token_fallback = HashMap::new();
306        let edition_dates: HashMap<String, String> = HashMap::new();
307        let mut rng_adapter = crate::game_rng::ThreadRngAdapter;
308        let mut ctx = EffectContext {
309            game: &mut game,
310            combat: None,
311            agents: &mut agents,
312            trigger_handler: &mut th,
313            token_templates: &templates,
314            token_art_variants: &templates_variants,
315            token_fallback: &token_fallback,
316            edition_dates: &edition_dates,
317            mana_pools: &mut mp,
318            parent_target_card: None,
319            rng: &mut rng_adapter,
320        };
321        super::DamageAllEffect::resolve(&mut ctx, &sa);
322
323        assert_eq!(ctx.game.card(c1).damage, 2);
324        assert_eq!(ctx.game.card(c2).damage, 2);
325        // Players not affected (no ValidPlayers$)
326        assert_eq!(ctx.game.player(p0).life, 20);
327        assert_eq!(ctx.game.player(p1).life, 20);
328    }
329
330    #[test]
331    fn damage_all_with_valid_players() {
332        let mut game = GameState::new(&["Alice", "Bob"], 20);
333        let p0 = PlayerId(0);
334        let p1 = PlayerId(1);
335
336        let sa = SpellAbility::new_simple(
337            None,
338            p0,
339            "A:SP$ DamageAll | NumDmg$ 3 | ValidCards$ | ValidPlayers$ Player",
340        );
341        let mut th = TriggerHandler::new();
342        let mut agents: Vec<Box<dyn crate::agent::PlayerAgent>> =
343            vec![Box::new(PassAgent), Box::new(PassAgent)];
344        let mut mp = vec![ManaPool::default(), ManaPool::default()];
345        let templates = HashMap::new();
346        let templates_variants = HashMap::new();
347        let token_fallback = HashMap::new();
348        let edition_dates: HashMap<String, String> = HashMap::new();
349        let mut rng_adapter = crate::game_rng::ThreadRngAdapter;
350        let mut ctx = EffectContext {
351            game: &mut game,
352            combat: None,
353            agents: &mut agents,
354            trigger_handler: &mut th,
355            token_templates: &templates,
356            token_art_variants: &templates_variants,
357            token_fallback: &token_fallback,
358            edition_dates: &edition_dates,
359            mana_pools: &mut mp,
360            parent_target_card: None,
361            rng: &mut rng_adapter,
362        };
363        super::DamageAllEffect::resolve(&mut ctx, &sa);
364
365        assert_eq!(ctx.game.player(p0).life, 17);
366        assert_eq!(ctx.game.player(p1).life, 17);
367    }
368}