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 ctx.game
109 .card_mut(cid)
110 .add_counter(&crate::card::CounterType::M1M1, damage);
111 }
112 } else if use_damage_map {
113 if let Some(src_id) = sa.source {
114 if let Some(map) = ctx.game.pending_damage_map.as_mut() {
115 map.put(src_id, DamageTarget::Card(cid), damage);
116 }
117 }
118 } else {
119 ctx.game
120 .deal_damage_to_card_from(cid, damage, sa.source, false);
121 }
122 if !use_damage_map {
123 ctx.trigger_handler.run_trigger(
124 crate::trigger::TriggerType::DamageDone,
125 crate::event::RunParams {
126 damage_source: sa.source,
127 damage_target_card: Some(cid),
128 damage_amount: Some(damage),
129 is_combat_damage: Some(false),
130 ..Default::default()
131 },
132 false,
133 );
134 }
135 }
136 let _ = crate::ability::spell_ability_effect::replace_dying(ctx.game, sa);
137 return;
138 }
139
140 for target_player in target_players {
141 let source_has_infect = if let Some(src_id) = sa.source {
142 let src = ctx.game.card(src_id);
143 source_has_infect_keyword
144 || crate::staticability::static_ability_infect_damage::is_infect_damage(
145 ctx.game,
146 &ctx.game.cards,
147 target_player,
148 src.controller,
149 )
150 } else {
151 false
152 };
153 if source_has_infect {
154 if use_damage_map {
156 if let Some(src_id) = sa.source {
157 if let Some(map) = ctx.game.pending_damage_map.as_mut() {
158 map.put(src_id, DamageTarget::Player(target_player), damage);
159 }
160 }
161 } else if !crate::staticability::static_ability_cant_put_counter::any_cant_put_counter_on_player(
162 &ctx.game.cards,
163 target_player,
164 &crate::card::CounterType::Poison,
165 ) {
166 ctx.game.player_add_poison(target_player, damage);
167 }
168 } else if use_damage_map {
169 if let Some(src_id) = sa.source {
170 if let Some(map) = ctx.game.pending_damage_map.as_mut() {
171 map.put(src_id, DamageTarget::Player(target_player), damage);
172 }
173 }
174 } else {
175 let dealt =
176 ctx.game
177 .deal_damage_to_player_from(target_player, damage, sa.source, false);
178 ctx.game
179 .record_player_damage_assignment(sa.source, Some(target_player), dealt, false);
180 }
181
182 if !use_damage_map {
184 if let Some(src_id) = sa.source {
185 if damage > 0 {
186 ctx.game.card_mut(src_id).total_damage_done_this_turn += damage;
187 ctx.game
188 .card_mut(src_id)
189 .damage_history
190 .record_damage(damage, false);
191 }
192 }
193 }
194
195 if !use_damage_map {
197 ctx.trigger_handler.run_trigger(
198 crate::trigger::TriggerType::DamageDone,
199 crate::event::RunParams {
200 damage_source: sa.source,
201 damage_target_player: Some(target_player),
202 damage_amount: Some(damage),
203 is_combat_damage: Some(false),
204 ..Default::default()
205 },
206 false,
207 );
208 ctx.trigger_handler.run_trigger(
209 crate::trigger::TriggerType::DamageDoneOnce,
210 crate::event::RunParams {
211 damage_target_player: Some(target_player),
212 damage_amount: Some(damage),
213 is_combat_damage: Some(false),
214 ..Default::default()
215 },
216 false,
217 );
218 ctx.trigger_handler.flush_waiting_triggers(ctx.game);
219 }
220 }
221 let mut target_cards = sa.target_chosen.target_card.into_iter().collect::<Vec<_>>();
222 target_cards.extend(card_util::get_radiance(ctx.game, sa).iter().copied());
223 target_cards.sort_unstable_by_key(|cid| cid.0);
224 target_cards.dedup();
225 for target_card in target_cards {
226 if ctx.game.card(target_card).zone == ZoneType::Battlefield {
227 if let Some(src_id) = sa.source {
229 if crate::staticability::static_ability_colorless_damage_source::target_is_protected_from_source(
230 &ctx.game.cards,
231 ctx.game.card(target_card),
232 ctx.game.card(src_id),
233 ) {
234 return;
235 }
236 }
237
238 if let Some(src_id) = sa.source {
240 if !ctx
241 .game
242 .card(target_card)
243 .damage_sources_this_turn
244 .contains(&src_id)
245 {
246 ctx.game
247 .card_mut(target_card)
248 .damage_sources_this_turn
249 .push(src_id);
250 }
251 }
252 if source_has_infect_keyword || source_has_wither {
253 if use_damage_map {
255 if let Some(src_id) = sa.source {
256 if let Some(map) = ctx.game.pending_damage_map.as_mut() {
257 map.put(src_id, DamageTarget::Card(target_card), damage);
258 }
259 }
260 } else if !crate::staticability::static_ability_cant_put_counter::any_cant_put_counter_on_card(
261 &ctx.game.cards,
262 ctx.game.card(target_card),
263 &crate::card::CounterType::M1M1,
264 ) {
265 ctx.game
266 .card_mut(target_card)
267 .add_counter(&crate::card::CounterType::M1M1, damage);
268 }
269 } else if use_damage_map {
270 if let Some(src_id) = sa.source {
271 if let Some(map) = ctx.game.pending_damage_map.as_mut() {
272 map.put(src_id, DamageTarget::Card(target_card), damage);
273 }
274 }
275 } else {
276 ctx.game
277 .deal_damage_to_card_from(target_card, damage, sa.source, false);
278 }
279
280 if !use_damage_map {
282 if let Some(src_id) = sa.source {
283 if damage > 0 {
284 ctx.game.card_mut(src_id).total_damage_done_this_turn += damage;
285 ctx.game
286 .card_mut(src_id)
287 .damage_history
288 .record_damage(damage, false);
289 }
290 }
291 }
292
293 if !use_damage_map {
295 ctx.trigger_handler.run_trigger(
296 crate::trigger::TriggerType::DamageDone,
297 crate::event::RunParams {
298 damage_source: sa.source,
299 damage_target_card: Some(target_card),
300 damage_amount: Some(damage),
301 is_combat_damage: Some(false),
302 ..Default::default()
303 },
304 false,
305 );
306 ctx.trigger_handler.run_trigger(
312 crate::trigger::TriggerType::DamageDoneOnce,
313 crate::event::RunParams {
314 damage_target_card: Some(target_card),
315 damage_amount: Some(damage),
316 is_combat_damage: Some(false),
317 ..Default::default()
318 },
319 false,
320 );
321 ctx.trigger_handler.flush_waiting_triggers(ctx.game);
326 }
327
328 if sa.ir.remember_damaged_creature {
329 if let Some(src_id) = sa.source {
330 let src = ctx.game.card_mut(src_id);
331 src.add_remembered_card(target_card);
332 }
333 }
334 }
335 }
336
337 let _ = crate::ability::spell_ability_effect::replace_dying(ctx.game, sa);
338}
339
340fn resolve_damage_amount(ctx: &EffectContext, sa: &SpellAbility) -> i32 {
344 if let Some(EffectIr::DealDamage(ir)) = &sa.ir.effect {
345 if let Some(amount) = &ir.amount {
346 if let Some(value) = resolve_amount_expr(ctx, sa, amount) {
347 #[cfg(debug_assertions)]
348 debug_assert_eq!(
349 value,
350 resolve_damage_amount_from_params(ctx, sa),
351 "compiled DealDamage amount diverged from string params"
352 );
353 return value;
354 }
355 }
356 }
357
358 resolve_damage_amount_from_params(ctx, sa)
359}
360
361fn resolve_amount_expr(ctx: &EffectContext, sa: &SpellAbility, amount: &AmountExpr) -> Option<i32> {
362 match amount {
363 AmountExpr::Literal(value) => Some(*value),
364 AmountExpr::X => Some(resolve_x_amount(ctx, sa)),
365 AmountExpr::SVar(name) => Some(resolve_svar_amount(ctx, sa, name)),
366 AmountExpr::Raw(_) => None,
367 }
368}
369
370fn resolve_damage_amount_from_params(ctx: &EffectContext, sa: &SpellAbility) -> i32 {
371 resolve_numeric_svar(ctx.game, sa, keys::NUM_DMG, 0)
372}
373
374fn resolve_x_amount(ctx: &EffectContext, sa: &SpellAbility) -> i32 {
375 if let Some(source_id) = sa.source {
376 if let Some(svar_expr) = ctx.game.card(source_id).get_s_var("X") {
377 return evaluate_svar_expr(ctx, sa, svar_expr);
378 }
379 }
380 sa.x_mana_cost_paid as i32
381}
382
383fn resolve_svar_amount(ctx: &EffectContext, sa: &SpellAbility, var_name: &str) -> i32 {
384 if let Some(source_id) = sa.source {
385 let svar_val = ctx
386 .game
387 .card(source_id)
388 .get_s_var(var_name)
389 .map(str::to_string);
390 if let Some(expr) = svar_val {
391 return evaluate_svar_expr(ctx, sa, &expr);
392 }
393 }
394
395 0
396}
397
398fn evaluate_svar_expr(ctx: &EffectContext, sa: &SpellAbility, expr: &str) -> i32 {
401 if expr.starts_with("Count$Kicked.") {
403 return super::evaluate_svar(expr, sa);
404 }
405 if expr.starts_with("Count$") {
407 if let Some(source_id) = sa.source {
408 return crate::svar::resolve_count_svar_for_sa(
409 expr,
410 ctx.game,
411 source_id,
412 sa.activating_player,
413 sa,
414 );
415 }
416 }
417 if expr == "Sacrificed$CardPower" || expr == "Sacrificed$CardToughness" {
420 if let Some(sac_id) = ctx.game.last_sacrificed_card {
421 let sac_card = ctx.game.card(sac_id);
422 let val = if expr.ends_with("Power") {
423 sac_card
424 .lki_power
425 .unwrap_or(sac_card.base_power.unwrap_or(0))
426 } else {
427 sac_card
428 .lki_toughness
429 .unwrap_or(sac_card.base_toughness.unwrap_or(0))
430 };
431 return val;
432 }
433 return 0;
434 }
435 if expr == "TriggeredCard$CardPower" || expr == "TriggeredCard$CardToughness" {
436 let trigger_value_key = if expr.ends_with("CardPower") {
437 "TriggeredCardPower"
438 } else {
439 "TriggeredCardToughness"
440 };
441 if let Some(value) = crate::ability::ability_key::from_string(trigger_value_key)
442 .and_then(|key| sa.get_triggering_value(key))
443 .and_then(|value| value.trim().parse::<i32>().ok())
444 {
445 return value;
446 }
447
448 let triggered_card = sa
449 .get_triggering_card(crate::ability::AbilityKey::Card)
450 .or(sa.trigger_source);
451 if let Some(card_id) = triggered_card {
452 return if expr.ends_with("CardPower") {
453 crate::lki::resolve_lki_power(ctx.game, card_id)
454 } else {
455 crate::lki::resolve_lki_toughness(ctx.game, card_id)
456 };
457 }
458 return 0;
459 }
460 match expr {
461 "Count$xPaid" | "Count$XPaid" => sa.x_mana_cost_paid as i32,
463 "ParentTargeted$CardPower" => ctx
466 .parent_target_card
467 .map(|id| ctx.game.card(id).power())
468 .unwrap_or(0),
469 "ParentTargeted$CardToughness" => ctx
470 .parent_target_card
471 .map(|id| ctx.game.card(id).toughness())
472 .unwrap_or(0),
473 _ => 0,
474 }
475}