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