manabrew_engine/ability/effects/
damage_deal_effect.rs1use forge_foundation::ZoneType;
2
3use super::{resolve_numeric_svar, EffectContext};
4use crate::ability::ability_ir::EffectIr;
5use crate::card::card_damage_map::DamageTarget;
6use crate::card::card_util;
7use crate::parsing::amount::AmountExpr;
8use crate::parsing::keys;
9use crate::spellability::SpellAbility;
10
11#[manabrew_engine_macros::spell_effect(DamageDealEffect)]
15fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
16 let damage = resolve_damage_amount(ctx, sa);
17 let use_damage_map = ctx.game.pending_damage_map.is_some() || sa.ir.damage_map;
18 if sa.ir.damage_map {
19 ctx.game.ensure_pending_damage_maps();
20 }
21
22 let target_players: Vec<_> = if let Some(target_player) = sa.target_chosen.target_player {
26 vec![target_player]
27 } else if let Some(defined) = sa.defined() {
28 crate::ability::ability_utils::resolve_defined_players_with_sa(
29 defined,
30 sa,
31 sa.activating_player,
32 ctx.game,
33 )
34 } else {
35 Vec::new()
36 };
37
38 let (source_has_infect_keyword, source_has_wither) = if let Some(src_id) = sa.source {
40 let src = ctx.game.card(src_id);
41 (
42 src.has_infect(),
43 src.has_wither()
44 || crate::staticability::static_ability_wither_damage::is_wither_damage(
45 &ctx.game.cards,
46 src,
47 ),
48 )
49 } else {
50 (false, false)
51 };
52
53 if sa.overloaded {
55 let valid_tgts = sa
56 .target_restrictions
57 .as_ref()
58 .and_then(|restrictions| restrictions.valid_tgts.first())
59 .map(String::as_str)
60 .unwrap_or_default();
61 let valid_tgts_selector = sa
62 .target_restrictions
63 .as_ref()
64 .map(|restrictions| &restrictions.valid_tgts_selector);
65 let all_bf: Vec<crate::ids::CardId> = ctx
66 .game
67 .player_order
68 .clone()
69 .iter()
70 .flat_map(|&pid| ctx.game.cards_in_zone(ZoneType::Battlefield, pid).to_vec())
71 .collect();
72 for cid in all_bf {
73 if ctx.game.card(cid).zone != ZoneType::Battlefield {
74 continue;
75 }
76 if !super::matches_valid_cards_for_sa(
77 ctx.game,
78 sa,
79 ctx.game.card(cid),
80 valid_tgts_selector,
81 valid_tgts,
82 ) {
83 continue;
84 }
85 if let Some(src_id) = sa.source {
87 if !ctx
88 .game
89 .card(cid)
90 .damage_sources_this_turn
91 .contains(&src_id)
92 {
93 ctx.game.card_mut(cid).add_damage_source_this_turn(src_id);
94 }
95 }
96 if source_has_infect_keyword || source_has_wither {
97 if use_damage_map {
98 if let Some(src_id) = sa.source {
99 if let Some(map) = ctx.game.pending_damage_map.as_mut() {
100 map.put(src_id, DamageTarget::Card(cid), damage);
101 }
102 }
103 } else if !crate::staticability::static_ability_cant_put_counter::any_cant_put_counter_on_card(
104 &ctx.game.cards,
105 ctx.game.card(cid),
106 &crate::card::CounterType::M1M1,
107 ) {
108 crate::ability::effects::effect_context::add_counter_with_context(
109 ctx.game,
110 Some(ctx.trigger_handler),
111 Some(ctx.agents),
112 cid,
113 &crate::card::CounterType::M1M1,
114 damage,
115 crate::event::RunParams {
116 source_player: sa.source.map(|src_id| ctx.game.card(src_id).controller),
117 cause: Some(sa.clone()),
118 ..Default::default()
119 },
120 true,
121 );
122 }
123 } else if use_damage_map {
124 if let Some(src_id) = sa.source {
125 if let Some(map) = ctx.game.pending_damage_map.as_mut() {
126 map.put(src_id, DamageTarget::Card(cid), damage);
127 }
128 }
129 } else {
130 ctx.game
131 .deal_damage_to_card_from(cid, damage, sa.source, false);
132 }
133 if !use_damage_map {
134 ctx.trigger_handler.run_trigger(
135 crate::trigger::TriggerType::DamageDone,
136 crate::event::RunParams {
137 damage_source: sa.source,
138 damage_target_card: Some(cid),
139 damage_amount: Some(damage),
140 is_combat_damage: Some(false),
141 ..Default::default()
142 },
143 false,
144 );
145 }
146 }
147 let _ = crate::ability::spell_ability_effect::replace_dying(ctx.game, sa);
148 return;
149 }
150
151 for target_player in target_players {
152 let source_has_infect = if let Some(src_id) = sa.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 target_player,
159 src.controller,
160 )
161 } else {
162 false
163 };
164 if source_has_infect {
165 if use_damage_map {
167 if let Some(src_id) = sa.source {
168 if let Some(map) = ctx.game.pending_damage_map.as_mut() {
169 map.put(src_id, DamageTarget::Player(target_player), damage);
170 }
171 }
172 } else {
173 ctx.add_player_counter(
174 target_player,
175 &crate::card::CounterType::Poison,
176 damage,
177 sa,
178 crate::event::RunParams {
179 source_player: sa.source.map(|source| ctx.game.card(source).controller),
180 ..Default::default()
181 },
182 );
183 }
184 } else if use_damage_map {
185 if let Some(src_id) = sa.source {
186 if let Some(map) = ctx.game.pending_damage_map.as_mut() {
187 map.put(src_id, DamageTarget::Player(target_player), damage);
188 }
189 }
190 } else {
191 let dealt =
192 ctx.game
193 .deal_damage_to_player_from(target_player, damage, sa.source, false);
194 ctx.game
195 .record_player_damage_assignment(sa.source, Some(target_player), dealt, false);
196 }
197
198 if !use_damage_map {
200 if let Some(src_id) = sa.source {
201 if damage > 0 {
202 ctx.game.card_mut(src_id).total_damage_done_this_turn += damage;
203 ctx.game
204 .card_mut(src_id)
205 .damage_history
206 .record_damage(damage, false);
207 }
208 }
209 }
210
211 if !use_damage_map {
213 ctx.trigger_handler.run_trigger(
214 crate::trigger::TriggerType::DamageDone,
215 crate::event::RunParams {
216 damage_source: sa.source,
217 damage_target_player: Some(target_player),
218 damage_amount: Some(damage),
219 is_combat_damage: Some(false),
220 ..Default::default()
221 },
222 false,
223 );
224 ctx.trigger_handler.run_trigger(
225 crate::trigger::TriggerType::DamageDoneOnce,
226 crate::event::RunParams {
227 damage_target_player: Some(target_player),
228 damage_amount: Some(damage),
229 is_combat_damage: Some(false),
230 ..Default::default()
231 },
232 false,
233 );
234 ctx.trigger_handler.flush_waiting_triggers(ctx.game);
235 }
236 }
237 let mut target_cards = sa.target_chosen.target_card.into_iter().collect::<Vec<_>>();
238 target_cards.extend(card_util::get_radiance(ctx.game, sa).iter().copied());
239 target_cards.sort_unstable_by_key(|cid| cid.0);
240 target_cards.dedup();
241 for target_card in target_cards {
242 if ctx.game.card(target_card).zone == ZoneType::Battlefield {
243 if let Some(src_id) = sa.source {
245 if crate::staticability::static_ability_colorless_damage_source::target_is_protected_from_source(
246 &ctx.game.cards,
247 ctx.game.card(target_card),
248 ctx.game.card(src_id),
249 ) {
250 return;
251 }
252 }
253
254 if let Some(src_id) = sa.source {
256 if !ctx
257 .game
258 .card(target_card)
259 .damage_sources_this_turn
260 .contains(&src_id)
261 {
262 ctx.game
263 .card_mut(target_card)
264 .damage_sources_this_turn
265 .push(src_id);
266 }
267 }
268 if source_has_infect_keyword || source_has_wither {
269 if use_damage_map {
271 if let Some(src_id) = sa.source {
272 if let Some(map) = ctx.game.pending_damage_map.as_mut() {
273 map.put(src_id, DamageTarget::Card(target_card), damage);
274 }
275 }
276 } else if !crate::staticability::static_ability_cant_put_counter::any_cant_put_counter_on_card(
277 &ctx.game.cards,
278 ctx.game.card(target_card),
279 &crate::card::CounterType::M1M1,
280 ) {
281 crate::ability::effects::effect_context::add_counter_with_context(
282 ctx.game,
283 Some(ctx.trigger_handler),
284 Some(ctx.agents),
285 target_card,
286 &crate::card::CounterType::M1M1,
287 damage,
288 crate::event::RunParams {
289 source_player: sa.source.map(|src_id| ctx.game.card(src_id).controller),
290 cause: Some(sa.clone()),
291 ..Default::default()
292 },
293 true,
294 );
295 }
296 } else if use_damage_map {
297 if let Some(src_id) = sa.source {
298 if let Some(map) = ctx.game.pending_damage_map.as_mut() {
299 map.put(src_id, DamageTarget::Card(target_card), damage);
300 }
301 }
302 } else {
303 ctx.game
304 .deal_damage_to_card_from(target_card, damage, sa.source, false);
305 }
306
307 if !use_damage_map {
309 if let Some(src_id) = sa.source {
310 if damage > 0 {
311 ctx.game.card_mut(src_id).total_damage_done_this_turn += damage;
312 ctx.game
313 .card_mut(src_id)
314 .damage_history
315 .record_damage(damage, false);
316 }
317 }
318 }
319
320 if !use_damage_map {
322 ctx.trigger_handler.run_trigger(
323 crate::trigger::TriggerType::DamageDone,
324 crate::event::RunParams {
325 damage_source: sa.source,
326 damage_target_card: Some(target_card),
327 damage_amount: Some(damage),
328 is_combat_damage: Some(false),
329 ..Default::default()
330 },
331 false,
332 );
333 ctx.trigger_handler.run_trigger(
339 crate::trigger::TriggerType::DamageDoneOnce,
340 crate::event::RunParams {
341 damage_target_card: Some(target_card),
342 damage_amount: Some(damage),
343 is_combat_damage: Some(false),
344 ..Default::default()
345 },
346 false,
347 );
348 ctx.trigger_handler.flush_waiting_triggers(ctx.game);
353 }
354
355 if sa.ir.remember_damaged_creature {
356 if let Some(src_id) = sa.source {
357 let src = ctx.game.card_mut(src_id);
358 src.add_remembered_card(target_card);
359 }
360 }
361 }
362 }
363
364 let _ = crate::ability::spell_ability_effect::replace_dying(ctx.game, sa);
365}
366
367fn resolve_damage_amount(ctx: &EffectContext, sa: &SpellAbility) -> i32 {
371 if let Some(EffectIr::DealDamage(ir)) = &sa.ir.effect {
372 if let Some(amount) = &ir.amount {
373 if let Some(value) = resolve_amount_expr(ctx, sa, amount) {
374 #[cfg(debug_assertions)]
375 debug_assert_eq!(
376 value,
377 resolve_damage_amount_from_params(ctx, sa),
378 "compiled DealDamage amount diverged from string params"
379 );
380 return value;
381 }
382 }
383 }
384
385 resolve_damage_amount_from_params(ctx, sa)
386}
387
388fn resolve_amount_expr(ctx: &EffectContext, sa: &SpellAbility, amount: &AmountExpr) -> Option<i32> {
389 match amount {
390 AmountExpr::Literal(value) => Some(*value),
391 AmountExpr::X => Some(resolve_x_amount(ctx, sa)),
392 AmountExpr::SVar(name) => Some(resolve_svar_amount(ctx, sa, name)),
393 AmountExpr::Raw(_) => None,
394 }
395}
396
397fn resolve_damage_amount_from_params(ctx: &EffectContext, sa: &SpellAbility) -> i32 {
398 resolve_numeric_svar(ctx.game, sa, keys::NUM_DMG, 0)
399}
400
401fn resolve_x_amount(ctx: &EffectContext, sa: &SpellAbility) -> i32 {
402 if let Some(source_id) = sa.source {
403 if let Some(svar_expr) = ctx.game.card(source_id).get_s_var("X") {
404 return evaluate_svar_expr(ctx, sa, svar_expr);
405 }
406 }
407 sa.x_mana_cost_paid as i32
408}
409
410fn resolve_svar_amount(ctx: &EffectContext, sa: &SpellAbility, var_name: &str) -> i32 {
411 if let Some(source_id) = sa.source {
412 let svar_val = ctx
413 .game
414 .card(source_id)
415 .get_s_var(var_name)
416 .map(str::to_string);
417 if let Some(expr) = svar_val {
418 return evaluate_svar_expr(ctx, sa, &expr);
419 }
420 }
421
422 0
423}
424
425fn evaluate_svar_expr(ctx: &EffectContext, sa: &SpellAbility, expr: &str) -> i32 {
428 if expr.starts_with("Count$Kicked.") {
430 return super::evaluate_svar(expr, sa);
431 }
432 if expr.starts_with("Count$") {
434 if let Some(source_id) = sa.source {
435 return crate::svar::resolve_count_svar_for_sa(
436 expr,
437 ctx.game,
438 source_id,
439 sa.activating_player,
440 sa,
441 );
442 }
443 }
444 if expr == "Sacrificed$CardPower" || expr == "Sacrificed$CardToughness" {
447 if let Some(sac_id) = ctx.game.last_sacrificed_card {
448 let sac_card = ctx.game.card(sac_id);
449 let val = if expr.ends_with("Power") {
450 sac_card
451 .lki_power
452 .unwrap_or(sac_card.base_power.unwrap_or(0))
453 } else {
454 sac_card
455 .lki_toughness
456 .unwrap_or(sac_card.base_toughness.unwrap_or(0))
457 };
458 return val;
459 }
460 return 0;
461 }
462 if expr == "TriggeredCard$CardPower" || expr == "TriggeredCard$CardToughness" {
463 let trigger_value_key = if expr.ends_with("CardPower") {
464 "TriggeredCardPower"
465 } else {
466 "TriggeredCardToughness"
467 };
468 if let Some(value) = crate::ability::ability_key::from_string(trigger_value_key)
469 .and_then(|key| sa.get_triggering_value(key))
470 .and_then(|value| value.trim().parse::<i32>().ok())
471 {
472 return value;
473 }
474
475 let triggered_card = sa
476 .get_triggering_card(crate::ability::AbilityKey::Card)
477 .or(sa.trigger_source);
478 if let Some(card_id) = triggered_card {
479 return if expr.ends_with("CardPower") {
480 crate::lki::resolve_lki_power(ctx.game, card_id)
481 } else {
482 crate::lki::resolve_lki_toughness(ctx.game, card_id)
483 };
484 }
485 return 0;
486 }
487 match expr {
488 "Count$xPaid" | "Count$XPaid" => sa.x_mana_cost_paid as i32,
490 "ParentTargeted$CardPower" => ctx
493 .parent_target_card
494 .map(|id| ctx.game.card(id).power())
495 .unwrap_or(0),
496 "ParentTargeted$CardToughness" => ctx
497 .parent_target_card
498 .map(|id| ctx.game.card(id).toughness())
499 .unwrap_or(0),
500 _ => 0,
501 }
502}