1use std::collections::{HashMap, HashSet};
7
8use serde::{Deserialize, Serialize};
9
10use crate::card::valid_filter;
11use crate::event::RunParams;
12use crate::game::GameState;
13use crate::ids::{CardId, PlayerId};
14use crate::parsing::cached_compiled_selector;
15use crate::spellability::SpellAbility;
16use crate::trigger::TriggerHandler;
17use crate::trigger::TriggerType;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
20pub enum DamageTarget {
21 Card(CardId),
22 Player(PlayerId),
23}
24
25#[derive(Debug, Clone, Default, Serialize, Deserialize)]
26pub struct CardDamageMap {
27 data: HashMap<CardId, HashMap<DamageTarget, i32>>,
28}
29
30impl CardDamageMap {
31 pub fn put(&mut self, source: CardId, target: DamageTarget, amount: i32) -> i32 {
33 let by_target = self.data.entry(source).or_default();
34 let prev = by_target.get(&target).copied().unwrap_or(0);
35 by_target.insert(target, prev + amount);
36 prev
37 }
38
39 pub fn total_amount(&self) -> i32 {
40 self.data.values().flat_map(|m| m.values()).copied().sum()
41 }
42
43 pub fn entries(&self) -> Vec<(CardId, DamageTarget, i32)> {
44 self.data
45 .iter()
46 .flat_map(|(&source, targets)| {
47 targets
48 .iter()
49 .map(move |(&target, &amount)| (source, target, amount))
50 })
51 .collect()
52 }
53
54 pub fn filtered_map(
56 &self,
57 game: &GameState,
58 valid_source: Option<&str>,
59 valid_target: Option<&str>,
60 host: CardId,
61 ) -> CardDamageMap {
62 let host_card = game.card(host);
63 let valid_source = valid_source.map(cached_compiled_selector);
64 let valid_target = valid_target.map(cached_compiled_selector);
65 let mut out = CardDamageMap::default();
66
67 for (&source, targets) in &self.data {
68 if let Some(valid) = valid_source.as_ref() {
69 if !valid_filter::matches_valid_card_selector_in_game(
70 valid,
71 game.card(source),
72 host_card,
73 game,
74 ) {
75 continue;
76 }
77 }
78
79 for (&target, &amount) in targets {
80 let target_ok = match (valid_target.as_ref(), target) {
81 (None, _) => true,
82 (Some(valid), DamageTarget::Card(cid)) => {
83 valid_filter::matches_valid_card_selector_in_game(
84 valid,
85 game.card(cid),
86 host_card,
87 game,
88 )
89 }
90 (Some(valid), DamageTarget::Player(pid)) => {
91 valid_filter::matches_valid_player_selector(
92 valid,
93 pid,
94 host_card.controller,
95 )
96 }
97 };
98 if target_ok {
99 out.put(source, target, amount);
100 }
101 }
102 }
103 out
104 }
105
106 pub fn trigger_prevent_damage(&self, trigger_handler: &mut TriggerHandler, is_combat: bool) {
107 let mut by_target: HashMap<DamageTarget, i32> = HashMap::new();
108 for targets in self.data.values() {
109 for (&target, &amount) in targets {
110 *by_target.entry(target).or_insert(0) += amount;
111 }
112 }
113
114 for (target, amount) in by_target {
115 if amount <= 0 {
116 continue;
117 }
118 let params = match target {
119 DamageTarget::Card(cid) => RunParams {
120 damage_target_card: Some(cid),
121 damage_amount: Some(amount),
122 is_combat_damage: Some(is_combat),
123 ..Default::default()
124 },
125 DamageTarget::Player(pid) => RunParams {
126 damage_target_player: Some(pid),
127 damage_amount: Some(amount),
128 is_combat_damage: Some(is_combat),
129 ..Default::default()
130 },
131 };
132 trigger_handler.run_trigger(TriggerType::DamagePreventedOnce, params, false);
133 }
134 }
135
136 pub fn trigger_damage_done_once(
137 &self,
138 game: &GameState,
139 trigger_handler: &mut TriggerHandler,
140 is_combat: bool,
141 ) {
142 for (&source, targets) in &self.data {
144 let sum: i32 = targets.values().copied().sum();
145 if sum <= 0 {
146 continue;
147 }
148 trigger_handler.run_trigger(
149 TriggerType::DamageDealtOnce,
150 RunParams {
151 damage_source: Some(source),
152 damage_amount: Some(sum),
153 is_combat_damage: Some(is_combat),
154 ..Default::default()
155 },
156 false,
157 );
158 }
159
160 let mut by_target: HashMap<DamageTarget, i32> = HashMap::new();
162 let mut target_controllers: HashMap<DamageTarget, HashSet<PlayerId>> = HashMap::new();
163 for (&source, targets) in &self.data {
164 for (&target, &amount) in targets {
165 *by_target.entry(target).or_insert(0) += amount;
166 target_controllers
167 .entry(target)
168 .or_default()
169 .insert(game.card(source).controller);
170 }
171 }
172
173 for (target, sum) in by_target {
174 if sum <= 0 {
175 continue;
176 }
177
178 let base = match target {
179 DamageTarget::Card(cid) => RunParams {
180 damage_target_card: Some(cid),
181 damage_amount: Some(sum),
182 is_combat_damage: Some(is_combat),
183 ..Default::default()
184 },
185 DamageTarget::Player(pid) => RunParams {
186 damage_target_player: Some(pid),
187 damage_amount: Some(sum),
188 is_combat_damage: Some(is_combat),
189 ..Default::default()
190 },
191 };
192 trigger_handler.run_trigger(TriggerType::DamageDoneOnce, base, false);
193
194 if let Some(controllers) = target_controllers.get(&target) {
195 for &controller in controllers {
196 let params = match target {
197 DamageTarget::Card(cid) => RunParams {
198 damage_target_card: Some(cid),
199 cause_player: Some(controller),
200 is_combat_damage: Some(is_combat),
201 ..Default::default()
202 },
203 DamageTarget::Player(pid) => RunParams {
204 damage_target_player: Some(pid),
205 cause_player: Some(controller),
206 is_combat_damage: Some(is_combat),
207 ..Default::default()
208 },
209 };
210 trigger_handler.run_trigger(TriggerType::DamageDoneOnce, params, false);
211 }
212 }
213 }
214
215 trigger_handler.run_trigger(
216 TriggerType::DamageAll,
217 RunParams {
218 damage_amount: Some(self.total_amount()),
219 is_combat_damage: Some(is_combat),
220 ..Default::default()
221 },
222 false,
223 );
224 }
225
226 pub fn trigger_excess_damage(
228 &self,
229 game: &GameState,
230 trigger_handler: &mut TriggerHandler,
231 is_combat: bool,
232 lethal_damage: &HashMap<CardId, i32>,
233 _cause: Option<&mut SpellAbility>,
234 ) {
235 for (&target_card, &lethal) in lethal_damage {
236 let dealt: i32 = self
237 .data
238 .values()
239 .map(|m| {
240 m.get(&DamageTarget::Card(target_card))
241 .copied()
242 .unwrap_or(0)
243 })
244 .sum();
245 if dealt <= 0 {
246 continue;
247 }
248
249 let deathtouch_threshold = if game.card(target_card).has_deathtouch_damage {
250 1
251 } else {
252 lethal
253 };
254 let excess = dealt - deathtouch_threshold;
255 if excess <= 0 {
256 continue;
257 }
258
259 trigger_handler.run_trigger(
260 TriggerType::ExcessDamage,
261 RunParams {
262 damage_target_card: Some(target_card),
263 damage_amount: Some(excess),
264 is_combat_damage: Some(is_combat),
265 ..Default::default()
266 },
267 false,
268 );
269 }
270 }
271}