Skip to main content

manabrew_engine/card/
card_damage_map.rs

1//! Card damage aggregation map.
2//!
3//! Mirrors the Java `CardDamageMap` behavior for accumulating damage from
4//! source cards to card/player targets and emitting one-shot damage triggers.
5
6use 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    /// Java parity: sum damage when the same source-target pair is inserted repeatedly.
32    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    /// Java parity subset of `filteredMap`.
55    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        // Source -> aggregate damage
143        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        // Target -> aggregate damage
161        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    /// Java parity subset of excess-damage trigger aggregation.
227    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}