manabrew_engine/ability/effects/
damage_all_effect.rs1use 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#[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 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 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 let source = sa.source;
75 for card_id in to_damage {
76 if ctx.game.card(card_id).zone == ZoneType::Battlefield {
77 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 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 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 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 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 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 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}