Skip to main content

wowlab_engine_combat/context/
damage.rs

1//! Damage operations exposed to combat hooks.
2
3use wowlab_engine_domain::constants::OFF_HAND_DAMAGE_MULT;
4use wowlab_types::sim::{ActorId, EnemyIdx, PetIdx, SpellIdx};
5
6use super::{DamageOps, HookCtx};
7use crate::{
8    state::{RuntimeDamageDef, SwingHand, resolved_damage_base_points},
9    systems,
10};
11
12/// Resolved outcome of one hook-owned damage impact.
13#[derive(Clone, Copy, Debug, Default, PartialEq)]
14#[must_use]
15pub struct DamageOutcome {
16    pub amount: f64,
17    pub is_crit: bool,
18}
19
20impl From<(f64, bool)> for DamageOutcome {
21    fn from((amount, is_crit): (f64, bool)) -> Self {
22        Self { amount, is_crit }
23    }
24}
25
26impl DamageOps for HookCtx<'_> {
27    fn deal_damage_ap(&mut self, spell_id: u32, coef: f64, flags: systems::DamageFlags) {
28        let _ = self.deal_damage_ap_result(spell_id, coef, flags);
29    }
30
31    /// Deal one attack-power hit and return its post-mitigation outcome.
32    fn deal_damage_ap_result(
33        &mut self,
34        spell_id: u32,
35        coef: f64,
36        flags: systems::DamageFlags,
37    ) -> DamageOutcome {
38        let Some(target) = self.target else {
39            return DamageOutcome::default();
40        };
41        let flags = self.source_flags(flags);
42        let coef = self.resolved_pet_coefficient(
43            flags,
44            wowlab_engine_domain::dbc::PetStatKind::AttackPowerInheritance,
45            coef,
46        );
47        let mut ctx = self.combat_ctx(target);
48
49        systems::deal_damage_ap(&mut ctx, systems::DamagePayload::new(spell_id, coef, flags)).into()
50    }
51
52    /// Derive a non-crit child hit from a parent's final amount under the child's own modifiers.
53    ///
54    /// The parent amount already contains player-wide damage and versatility multipliers.
55    ///
56    /// The child retains its spell-scoped and target modifiers.
57    /// Those inherited terms are not applied a second time.
58    fn deal_damage_from_result(
59        &mut self,
60        spell_id: u32,
61        parent: DamageOutcome,
62        multiplier: f64,
63        flags: systems::DamageFlags,
64    ) -> DamageOutcome {
65        if parent.amount <= 0.0 || multiplier <= 0.0 {
66            return DamageOutcome::default();
67        }
68
69        let Some(target) = self.target else {
70            return DamageOutcome::default();
71        };
72        let flags = self.source_flags(
73            flags | systems::DamageFlags::NO_CRIT | systems::DamageFlags::IGNORE_PLAYER_MULTIPLIERS,
74        );
75        let mut ctx = self.combat_ctx(target);
76
77        systems::deal_damage_base(&mut ctx, spell_id, parent.amount * multiplier, flags).into()
78    }
79
80    /// Deal an AP hit under one spell's formula while reporting a distinct combat-log identity.
81    fn deal_profiled_damage_ap(
82        &mut self,
83        spell_id: u32,
84        profile_spell_id: u32,
85        coef: f64,
86        flags: systems::DamageFlags,
87    ) {
88        let Some(target) = self.target else {
89            return;
90        };
91        let flags = self.source_flags(flags);
92        let coef = self.resolved_pet_coefficient(
93            flags,
94            wowlab_engine_domain::dbc::PetStatKind::AttackPowerInheritance,
95            coef,
96        );
97        let mut ctx = self.combat_ctx(target);
98
99        systems::deal_damage_ap(
100            &mut ctx,
101            systems::DamagePayload::profiled(spell_id, profile_spell_id, coef, flags),
102        );
103    }
104
105    /// Current outgoing spell/melee haste multiplier (`1.0` means unmodified cadence).
106    fn haste_multiplier(&self) -> f64 {
107        systems::current_haste_mult(&self.view().combat())
108    }
109
110    /// Deal exactly one attack-power hit to a previously resolved enemy identity.
111    fn deal_damage_ap_to_target(
112        &mut self,
113        target: EnemyIdx,
114        spell_id: u32,
115        coef: f64,
116        flags: systems::DamageFlags,
117    ) {
118        let flags = self.source_flags(flags);
119        let coef = self.resolved_pet_coefficient(
120            flags,
121            wowlab_engine_domain::dbc::PetStatKind::AttackPowerInheritance,
122            coef,
123        );
124        let mut ctx = self.combat_ctx(target);
125
126        systems::deal_damage_ap_at(
127            &mut ctx,
128            target,
129            systems::DamagePayload::new(spell_id, coef, flags),
130        );
131    }
132
133    fn deal_damage_ap_if_positive(
134        &mut self,
135        spell_id: u32,
136        coefficient: f64,
137        flags: systems::DamageFlags,
138    ) {
139        if coefficient > 0.0 {
140            self.deal_damage_ap(spell_id, coefficient, flags);
141        }
142    }
143
144    fn deal_effect_damage_ap(
145        &mut self,
146        effect: (u32, u8),
147        multiplier: f64,
148        flags: systems::DamageFlags,
149    ) {
150        let coefficient = self.effect_ap_coefficient(effect) * multiplier;
151
152        if coefficient <= 0.0 {
153            return;
154        }
155
156        let Some(target) = self.target else {
157            return;
158        };
159        let flags = self.source_flags(flags);
160        let coefficient = self.resolved_pet_coefficient(
161            flags,
162            wowlab_engine_domain::dbc::PetStatKind::AttackPowerInheritance,
163            coefficient,
164        );
165        let mut ctx = self.combat_ctx(target);
166
167        systems::deal_effect_damage_ap(
168            &mut ctx,
169            crate::state::DamageEffectRef::new(effect.0, effect.1),
170            coefficient,
171            flags,
172        );
173    }
174
175    fn deal_effect_damage_ap_with_geometry(
176        &mut self,
177        effect: (u32, u8),
178        geometry_effect: (u32, u8),
179        multiplier: f64,
180        flags: systems::DamageFlags,
181    ) {
182        let coefficient = self.effect_ap_coefficient(effect) * multiplier;
183
184        if coefficient <= 0.0 {
185            return;
186        }
187
188        let Some(target) = self.target else {
189            return;
190        };
191        let flags = self.source_flags(flags);
192        let coefficient = self.resolved_pet_coefficient(
193            flags,
194            wowlab_engine_domain::dbc::PetStatKind::AttackPowerInheritance,
195            coefficient,
196        );
197        let mut ctx = self.combat_ctx(target);
198
199        systems::deal_effect_damage_ap_with_geometry(
200            &mut ctx,
201            crate::state::DamageEffectRef::new(effect.0, effect.1),
202            crate::state::DamageEffectRef::new(geometry_effect.0, geometry_effect.1),
203            coefficient,
204            flags,
205        );
206    }
207
208    fn deal_effect_damage_sp_with_geometry(
209        &mut self,
210        effect: (u32, u8),
211        geometry_effect: (u32, u8),
212        multiplier: f64,
213        flags: systems::DamageFlags,
214    ) {
215        let coefficient = self.effect_sp_coefficient(effect) * multiplier;
216
217        if coefficient <= 0.0 {
218            return;
219        }
220
221        let Some(target) = self.target else {
222            return;
223        };
224        let flags = self.source_flags(flags);
225        let coefficient = self.resolved_pet_coefficient(
226            flags,
227            wowlab_engine_domain::dbc::PetStatKind::SpellPowerInheritance,
228            coefficient,
229        );
230        let mut ctx = self.combat_ctx(target);
231
232        systems::deal_effect_damage_sp_with_geometry(
233            &mut ctx,
234            crate::state::DamageEffectRef::new(effect.0, effect.1),
235            crate::state::DamageEffectRef::new(geometry_effect.0, geometry_effect.1),
236            coefficient,
237            flags,
238        );
239    }
240
241    fn deal_physical_damage_ap_if_positive(&mut self, spell_id: u32, coefficient: f64) {
242        self.deal_damage_ap_if_positive(spell_id, coefficient, systems::DamageFlags::PHYSICAL);
243    }
244
245    fn deal_pet_damage_ap(&mut self, spell_id: u32, coefficient: f64) {
246        self.deal_damage_ap(spell_id, coefficient, systems::DamageFlags::PET);
247    }
248
249    /// Deal pet AP damage for an explicitly identified NPC actor.
250    fn deal_pet_damage_ap_for_npc(&mut self, npc_id: u32, spell_id: u32, coefficient: f64) {
251        let Some(target) = self.target else {
252            return;
253        };
254        let flags = self.source_flags(systems::DamageFlags::PET);
255        let coefficient = coefficient
256            * self
257                .state
258                .config
259                .game_data
260                .pet_attack_power_inheritance_mult(npc_id);
261        let mut ctx = self.combat_ctx_with_source(ActorId::Pet(PetIdx::PRIMARY), target);
262
263        systems::deal_damage_ap(
264            &mut ctx,
265            systems::DamagePayload::new(spell_id, coefficient, flags),
266        );
267    }
268
269    fn deal_physical_pet_damage_ap(&mut self, spell_id: u32, coefficient: f64) {
270        self.deal_damage_ap(
271            spell_id,
272            coefficient,
273            systems::DamageFlags::PHYSICAL | systems::DamageFlags::PET,
274        );
275    }
276
277    fn deal_damage_sp(&mut self, spell_id: u32, coef: f64, flags: systems::DamageFlags) {
278        let Some(target) = self.target else {
279            return;
280        };
281        let flags = self.source_flags(flags);
282        let coef = self.resolved_pet_coefficient(
283            flags,
284            wowlab_engine_domain::dbc::PetStatKind::SpellPowerInheritance,
285            coef,
286        );
287        let mut ctx = self.combat_ctx(target);
288
289        systems::deal_damage_sp(&mut ctx, systems::DamagePayload::new(spell_id, coef, flags));
290    }
291
292    /// Deal exactly one spell-power hit to a previously resolved enemy identity.
293    fn deal_damage_sp_to_target(
294        &mut self,
295        target: EnemyIdx,
296        spell_id: u32,
297        coef: f64,
298        flags: systems::DamageFlags,
299    ) {
300        let flags = self.source_flags(flags);
301        let coef = self.resolved_pet_coefficient(
302            flags,
303            wowlab_engine_domain::dbc::PetStatKind::SpellPowerInheritance,
304            coef,
305        );
306        let mut ctx = self.combat_ctx(target);
307
308        systems::deal_damage_sp_at(
309            &mut ctx,
310            target,
311            systems::DamagePayload::new(spell_id, coef, flags),
312        );
313    }
314
315    fn deal_profiled_damage_sp(
316        &mut self,
317        spell_id: u32,
318        profile_spell_id: u32,
319        coef: f64,
320        flags: systems::DamageFlags,
321    ) {
322        let Some(target) = self.target else {
323            return;
324        };
325        let flags = self.source_flags(flags);
326        let coef = self.resolved_pet_coefficient(
327            flags,
328            wowlab_engine_domain::dbc::PetStatKind::SpellPowerInheritance,
329            coef,
330        );
331        let mut ctx = self.combat_ctx(target);
332
333        systems::deal_damage_sp(
334            &mut ctx,
335            systems::DamagePayload::profiled(spell_id, profile_spell_id, coef, flags),
336        );
337    }
338
339    fn deal_damage_flat(&mut self, spell_id: u32, amount: f64) {
340        let Some(target) = self.target else {
341            return;
342        };
343        let flags = systems::damage_flags_from_data(
344            &self.state.config.game_data,
345            SpellIdx::from_raw(spell_id),
346            false,
347        );
348        let flags = self.source_flags(flags);
349        let mut ctx = self.combat_ctx(target);
350
351        systems::deal_damage_def(&mut ctx, spell_id, RuntimeDamageDef::Flat(amount), flags);
352    }
353
354    /// Deal a post-mitigation redirected amount without a second modifier pass.
355    fn deal_resolved_damage(&mut self, spell_id: u32, amount: f64) {
356        let Some(target) = self.target else {
357            return;
358        };
359        let flags = self.source_flags(systems::DamageFlags::empty());
360        let mut ctx = self.combat_ctx(target);
361
362        systems::deal_resolved_damage(&mut ctx, spell_id, amount, flags);
363    }
364
365    /// Follow one resolved DBC trigger edge and deal its terminal child damage.
366    fn deal_triggered_damage(
367        &mut self,
368        source_spell_id: u32,
369        effect_index: u8,
370        multiplier: f64,
371        flags: systems::DamageFlags,
372    ) -> bool {
373        let Some(target) = self.target else {
374            return false;
375        };
376        let triggered = match wowlab_engine_domain::dbc::resolve_triggered_damage(
377            wowlab_engine_domain::dbc::EffectLookup::new(
378                &self.state.config.game_data,
379                wowlab_types::sim::EffectRef::new(
380                    SpellIdx::from_raw(source_spell_id),
381                    effect_index,
382                ),
383            ),
384        ) {
385            Ok(triggered) => triggered,
386            Err(error) => {
387                tracing::warn!(source_spell_id, effect_index, %error, "TRIGGER_DAMAGE_UNRESOLVED");
388
389                return false;
390            }
391        };
392        let Some(damage) =
393            crate::builder::lower_resolved_damage(triggered.damage, OFF_HAND_DAMAGE_MULT)
394                .map(|damage| damage.scaled(multiplier))
395        else {
396            return false;
397        };
398        let base_points = Some(resolved_damage_base_points(triggered.damage, multiplier));
399        let flags = self.source_flags(flags);
400        let mut ctx = self.combat_ctx(target);
401
402        systems::deal_profiled_damage_def(
403            &mut ctx,
404            crate::state::DamageEffectRef::new(
405                triggered.spell_id.as_u32(),
406                triggered.damage.effect_index,
407            ),
408            source_spell_id,
409            base_points,
410            damage,
411            flags,
412        );
413
414        true
415    }
416
417    /// Deal an item's spell-power damage, reading its effect-1 SP coefficient from game data.
418    fn deal_item_damage_sp(&mut self, spell_id: u32) {
419        const ITEM_DAMAGE_EFFECT: u8 = 1;
420        let coef = self
421            .state
422            .config
423            .game_data
424            .sp_coef(SpellIdx::from_raw(spell_id), ITEM_DAMAGE_EFFECT);
425
426        self.deal_damage_sp(spell_id, coef, systems::DamageFlags::empty());
427    }
428
429    fn player_swing_period_ms(&self, hand: SwingHand) -> Option<u32> {
430        systems::active_form_combat_round_time_ms(self.state, self.buf)
431            .or_else(|| match hand {
432                SwingHand::MainHand => Some(self.game_data().main_hand().speed_ms),
433                SwingHand::OffHand => self.game_data().off_hand().map(|weapon| weapon.speed_ms),
434            })
435            .filter(|period_ms| *period_ms > 0)
436    }
437
438    fn effect_base_points(&self, effect: (u32, u8)) -> f64 {
439        self.game_data()
440            .base_points(SpellIdx::from_raw(effect.0), effect.1)
441    }
442
443    fn effect_points(&self, effect: (u32, u8)) -> f64 {
444        self.game_data().effect_points(
445            SpellIdx::from_raw(effect.0),
446            effect.1,
447            systems::current_mastery_points(&self.view().combat()),
448        )
449    }
450
451    fn effect_percent(&self, effect: (u32, u8)) -> f64 {
452        self.effect_base_points(effect) / wowlab_types::constants::HUNDRED
453    }
454
455    fn effect_ap_coefficient(&self, effect: (u32, u8)) -> f64 {
456        self.game_data()
457            .ap_coef(SpellIdx::from_raw(effect.0), effect.1)
458    }
459
460    fn effect_sp_coefficient(&self, effect: (u32, u8)) -> f64 {
461        self.game_data()
462            .sp_coef(SpellIdx::from_raw(effect.0), effect.1)
463    }
464
465    fn target_health_fraction(&self) -> f64 {
466        self.target
467            .filter(|target| self.state.is_valid_target(*target))
468            .and_then(|target| self.state.enemy_health_fraction(target, self.now))
469            .unwrap_or(1.0)
470    }
471}