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 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 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 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 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 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 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 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}