manabrew_engine/ability/effects/
damage_resolve_effect.rs1use super::EffectContext;
5use crate::card::card_damage_map::DamageTarget;
6use crate::card::CounterType;
7
8#[manabrew_engine_macros::spell_effect(DamageResolveEffect)]
12fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
13 let from_pending = sa.damage_map.is_none();
14 let damage_map_owned = sa
15 .damage_map
16 .clone()
17 .or_else(|| ctx.game.pending_damage_map.clone());
18 let Some(damage_map) = damage_map_owned.as_ref() else {
19 return;
20 };
21
22 let prevent_map_owned = sa
23 .prevent_map
24 .clone()
25 .or_else(|| ctx.game.pending_prevent_map.clone());
26 if let Some(prevent_map) = prevent_map_owned.as_ref() {
27 prevent_map.trigger_prevent_damage(ctx.trigger_handler, false);
28 }
29
30 for (source, target, amount) in damage_map.entries() {
31 if amount <= 0 {
32 continue;
33 }
34 match target {
35 DamageTarget::Card(cid) => {
36 if ctx.game.card(cid).zone == forge_foundation::ZoneType::Battlefield {
37 if crate::staticability::static_ability_colorless_damage_source::target_is_protected_from_source(
39 &ctx.game.cards,
40 ctx.game.card(cid),
41 ctx.game.card(source),
42 ) {
43 continue;
44 }
45
46 if !ctx
47 .game
48 .card(cid)
49 .damage_sources_this_turn
50 .contains(&source)
51 {
52 ctx.game.card_mut(cid).add_damage_source_this_turn(source);
53 }
54
55 let source_has_infect = ctx.game.card(source).has_infect();
56 let source_has_wither = ctx.game.card(source).has_wither()
57 || crate::staticability::static_ability_wither_damage::is_wither_damage(
58 &ctx.game.cards,
59 ctx.game.card(source),
60 );
61
62 if source_has_infect || source_has_wither {
63 if !crate::staticability::static_ability_cant_put_counter::any_cant_put_counter_on_card(
64 &ctx.game.cards,
65 ctx.game.card(cid),
66 &CounterType::M1M1,
67 ) {
68 ctx.game.card_mut(cid).add_counter(&CounterType::M1M1, amount);
69 }
70 } else {
71 ctx.game.deal_damage_to_card(cid, amount);
72 }
73 }
74 }
75 DamageTarget::Player(pid) => {
76 let source_has_infect = ctx.game.card(source).has_infect()
77 || crate::staticability::static_ability_infect_damage::is_infect_damage(
78 ctx.game,
79 &ctx.game.cards,
80 pid,
81 ctx.game.card(source).controller,
82 );
83 if source_has_infect {
84 if !crate::staticability::static_ability_cant_put_counter::any_cant_put_counter_on_player(
85 &ctx.game.cards,
86 pid,
87 &CounterType::Poison,
88 ) {
89 ctx.game.player_add_poison(pid, amount);
90 }
91 } else {
92 let dealt = ctx.game.deal_damage_to_player(pid, amount);
93 ctx.game
94 .record_player_damage_assignment(Some(source), Some(pid), dealt, false);
95 }
96 }
97 }
98
99 ctx.game.card_mut(source).total_damage_done_this_turn += amount;
101 ctx.game
102 .card_mut(source)
103 .damage_history
104 .record_damage(amount, false);
105 }
106
107 damage_map.trigger_damage_done_once(ctx.game, ctx.trigger_handler, false);
108
109 ctx.trigger_handler.flush_waiting_triggers(ctx.game);
115
116 let _ = crate::ability::spell_ability_effect::replace_dying(ctx.game, sa);
118
119 if from_pending {
120 ctx.game.clear_pending_damage_maps();
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use crate::ability::spell_ability_effect::SpellAbilityEffect;
127 use std::collections::HashMap;
128
129 use forge_foundation::{CardTypeLine, ColorSet, ManaCost, ZoneType};
130
131 use crate::ability::effects::EffectContext;
132 use crate::agent::{PassAgent, PlayerAgent};
133 use crate::card::card_damage_map::{CardDamageMap, DamageTarget};
134 use crate::card::Card;
135 use crate::game::GameState;
136 use crate::ids::{CardId, PlayerId};
137 use crate::mana::ManaPool;
138 use crate::spellability::SpellAbility;
139 use crate::trigger::handler::TriggerHandler;
140
141 fn creature(game: &mut GameState, owner: PlayerId, name: &str) -> CardId {
142 let c = Card::new(
143 CardId(0),
144 name.to_string(),
145 owner,
146 CardTypeLine::parse("Creature"),
147 ManaCost::parse(""),
148 ColorSet::COLORLESS,
149 Some(2),
150 Some(2),
151 vec![],
152 vec![],
153 );
154 let id = game.create_card(c);
155 game.move_card(id, ZoneType::Battlefield, owner);
156 id
157 }
158
159 #[test]
160 fn damage_resolve_consumes_pending_damage_map() {
161 let mut game = GameState::new(&["A", "B"], 20);
162 let p0 = PlayerId(0);
163 let p1 = PlayerId(1);
164 let src = creature(&mut game, p0, "Src");
165 let tgt = creature(&mut game, p1, "Tgt");
166
167 let mut map = CardDamageMap::default();
168 map.put(src, DamageTarget::Card(tgt), 2);
169 map.put(src, DamageTarget::Player(p1), 1);
170 game.pending_damage_map = Some(map);
171 game.pending_prevent_map = Some(CardDamageMap::default());
172
173 let sa = SpellAbility::new_simple(Some(src), p0, "DB$ DamageResolve");
174 let mut th = TriggerHandler::new();
175 let mut agents: Vec<Box<dyn PlayerAgent>> = vec![Box::new(PassAgent), Box::new(PassAgent)];
176 let mut pools = vec![ManaPool::default(), ManaPool::default()];
177 let templates = HashMap::new();
178 let templates_variants = HashMap::new();
179 let token_fallback = HashMap::new();
180 let edition_dates: HashMap<String, String> = HashMap::new();
181 let mut rng = crate::game_rng::ThreadRngAdapter;
182 let mut ctx = EffectContext {
183 game: &mut game,
184 combat: None,
185 agents: &mut agents,
186 trigger_handler: &mut th,
187 token_templates: &templates,
188 token_art_variants: &templates_variants,
189 token_fallback: &token_fallback,
190 edition_dates: &edition_dates,
191 mana_pools: &mut pools,
192 parent_target_card: None,
193 rng: &mut rng,
194 };
195
196 super::DamageResolveEffect::resolve(&mut ctx, &sa);
197
198 assert_eq!(ctx.game.card(tgt).damage, 2);
199 assert_eq!(ctx.game.player(p1).life, 19);
200 assert!(ctx.game.pending_damage_map.is_none());
201 assert!(ctx.game.pending_prevent_map.is_none());
202 }
203
204 #[test]
205 fn damage_resolve_replace_dying_exiles_lethal_target() {
206 let mut game = GameState::new(&["A", "B"], 20);
207 let p0 = PlayerId(0);
208 let p1 = PlayerId(1);
209 let src = creature(&mut game, p0, "Scorching Dragonfire");
210 let tgt = creature(&mut game, p1, "Talruum Minotaur");
211
212 let mut map = CardDamageMap::default();
213 map.put(src, DamageTarget::Card(tgt), 3);
214 game.pending_damage_map = Some(map);
215 game.pending_prevent_map = Some(CardDamageMap::default());
216
217 let mut sa = SpellAbility::new_simple(
218 Some(src),
219 p0,
220 "DB$ DamageResolve | ReplaceDyingDefined$ Targeted",
221 );
222 sa.target_chosen.target_card = Some(tgt);
223
224 let mut th = TriggerHandler::new();
225 let mut agents: Vec<Box<dyn PlayerAgent>> = vec![Box::new(PassAgent), Box::new(PassAgent)];
226 let mut pools = vec![ManaPool::default(), ManaPool::default()];
227 let templates = HashMap::new();
228 let templates_variants = HashMap::new();
229 let token_fallback = HashMap::new();
230 let edition_dates: HashMap<String, String> = HashMap::new();
231 let mut rng = crate::game_rng::ThreadRngAdapter;
232 let mut ctx = EffectContext {
233 game: &mut game,
234 combat: None,
235 agents: &mut agents,
236 trigger_handler: &mut th,
237 token_templates: &templates,
238 token_art_variants: &templates_variants,
239 token_fallback: &token_fallback,
240 edition_dates: &edition_dates,
241 mana_pools: &mut pools,
242 parent_target_card: None,
243 rng: &mut rng,
244 };
245
246 super::DamageResolveEffect::resolve(&mut ctx, &sa);
247 assert_eq!(ctx.game.card(tgt).zone, ZoneType::Battlefield);
248 assert!(ctx.game.cards.iter().any(|c| {
249 c.zone == ZoneType::Command
250 && c.remembered_cards == vec![tgt]
251 && !c.replacement_effects.is_empty()
252 }));
253
254 assert!(ctx.game.check_state_based_actions());
255 assert_eq!(ctx.game.card(tgt).zone, ZoneType::Exile);
256 assert!(!ctx.game.cards.iter().any(|c| c.zone == ZoneType::Command));
257 }
258}