Skip to main content

wowlab_engine_combat/context/
damage.rs

1//! Damage operations exposed to combat hooks.
2
3use wowlab_types::sim::{ActorId, EnemyIdx, PetIdx};
4
5use super::{DamageOps, HookCtx};
6use crate::{
7    DamageActionId, DamageEffectId,
8    state::{RuntimeDamageDef, SwingHand},
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, action: DamageActionId, coef: f64, flags: systems::DamageFlags) {
28        let _ = self.deal_damage_ap_result(action, 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        action: DamageActionId,
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        let spell_id = action.spell_id();
49        let Some(binding) = ctx.program.damage.action_id_request(action) else {
50            return DamageOutcome::default();
51        };
52
53        systems::deal_bound_damage_ap(
54            &mut ctx,
55            binding,
56            systems::DamagePayload::new(spell_id, coef, flags),
57        )
58        .into()
59    }
60
61    /// Derive a non-crit child hit from a parent's final amount under the child's own modifiers.
62    ///
63    /// The parent amount already contains player-wide damage and versatility multipliers.
64    ///
65    /// The child retains its spell-scoped and target modifiers.
66    /// Those inherited terms are not applied a second time.
67    fn deal_damage_from_result(
68        &mut self,
69        action: DamageActionId,
70        parent: DamageOutcome,
71        multiplier: f64,
72        flags: systems::DamageFlags,
73    ) -> DamageOutcome {
74        if parent.amount <= 0.0 || multiplier <= 0.0 {
75            return DamageOutcome::default();
76        }
77
78        let Some(target) = self.target else {
79            return DamageOutcome::default();
80        };
81        let flags = self.source_flags(
82            flags | systems::DamageFlags::NO_CRIT | systems::DamageFlags::IGNORE_PLAYER_MULTIPLIERS,
83        );
84        let mut ctx = self.combat_ctx(target);
85        let spell_id = action.spell_id();
86        let Some(binding) = ctx.program.damage.action_id_request(action) else {
87            return DamageOutcome::default();
88        };
89
90        systems::deal_damage_base(
91            &mut ctx,
92            binding,
93            spell_id,
94            parent.amount * multiplier,
95            flags,
96        )
97        .into()
98    }
99
100    /// Deal an AP hit under one spell's formula while reporting a distinct combat-log identity.
101    fn deal_profiled_damage_ap(
102        &mut self,
103        action: DamageActionId,
104        profile: DamageActionId,
105        coef: f64,
106        flags: systems::DamageFlags,
107    ) {
108        let Some(target) = self.target else {
109            return;
110        };
111        let flags = self.source_flags(flags);
112        let coef = self.resolved_pet_coefficient(
113            flags,
114            wowlab_engine_domain::dbc::PetStatKind::AttackPowerInheritance,
115            coef,
116        );
117        let mut ctx = self.combat_ctx(target);
118        let spell_id = action.spell_id();
119        let profile_spell_id = profile.spell_id();
120        let Some(binding) = ctx
121            .program
122            .damage
123            .profiled_action_id_request(action, profile)
124        else {
125            return;
126        };
127
128        systems::deal_bound_damage_ap(
129            &mut ctx,
130            binding,
131            systems::DamagePayload::profiled(spell_id, profile_spell_id, coef, flags),
132        );
133    }
134
135    /// Current outgoing spell/melee haste multiplier (`1.0` means unmodified cadence).
136    fn haste_multiplier(&self) -> f64 {
137        systems::current_haste_mult(&self.view().combat())
138    }
139
140    /// Deal exactly one attack-power hit to a previously resolved enemy identity.
141    fn deal_damage_ap_to_target(
142        &mut self,
143        target: EnemyIdx,
144        action: DamageActionId,
145        coef: f64,
146        flags: systems::DamageFlags,
147    ) {
148        let flags = self.source_flags(flags);
149        let coef = self.resolved_pet_coefficient(
150            flags,
151            wowlab_engine_domain::dbc::PetStatKind::AttackPowerInheritance,
152            coef,
153        );
154        let mut ctx = self.combat_ctx(target);
155        let spell_id = action.spell_id();
156        let Some(binding) = ctx.program.damage.action_id_request(action) else {
157            return;
158        };
159
160        systems::deal_damage_ap_at(
161            &mut ctx,
162            target,
163            binding,
164            systems::DamagePayload::new(spell_id, coef, flags),
165        );
166    }
167
168    fn deal_damage_ap_if_positive(
169        &mut self,
170        action: DamageActionId,
171        coefficient: f64,
172        flags: systems::DamageFlags,
173    ) {
174        if coefficient > 0.0 {
175            self.deal_damage_ap(action, coefficient, flags);
176        }
177    }
178
179    fn deal_effect_damage_ap(
180        &mut self,
181        effect: DamageEffectId,
182        multiplier: f64,
183        flags: systems::DamageFlags,
184    ) {
185        let coefficient = self.effect_ap_coefficient(effect) * multiplier;
186
187        if coefficient <= 0.0 {
188            return;
189        }
190
191        let Some(target) = self.target else {
192            return;
193        };
194        let flags = self.source_flags(flags);
195        let coefficient = self.resolved_pet_coefficient(
196            flags,
197            wowlab_engine_domain::dbc::PetStatKind::AttackPowerInheritance,
198            coefficient,
199        );
200        let mut ctx = self.combat_ctx(target);
201        let effect_ref =
202            crate::state::DamageEffectRef::new(effect.action().spell_id(), effect.effect_index());
203        let Some(binding) = ctx
204            .program
205            .damage
206            .effect_id_request(effect, effect, effect.action())
207        else {
208            return;
209        };
210
211        systems::deal_effect_damage_ap(&mut ctx, binding, effect_ref, coefficient, flags);
212    }
213
214    fn deal_effect_damage_ap_with_geometry(
215        &mut self,
216        effect: DamageEffectId,
217        geometry_effect: DamageEffectId,
218        multiplier: f64,
219        flags: systems::DamageFlags,
220    ) {
221        let coefficient = self.effect_ap_coefficient(effect) * multiplier;
222
223        if coefficient <= 0.0 {
224            return;
225        }
226
227        let Some(target) = self.target else {
228            return;
229        };
230        let flags = self.source_flags(flags);
231        let coefficient = self.resolved_pet_coefficient(
232            flags,
233            wowlab_engine_domain::dbc::PetStatKind::AttackPowerInheritance,
234            coefficient,
235        );
236        let mut ctx = self.combat_ctx(target);
237        let effect_ref =
238            crate::state::DamageEffectRef::new(effect.action().spell_id(), effect.effect_index());
239        let geometry_ref = crate::state::DamageEffectRef::new(
240            geometry_effect.action().spell_id(),
241            geometry_effect.effect_index(),
242        );
243        let Some(binding) =
244            ctx.program
245                .damage
246                .effect_id_request(effect, geometry_effect, effect.action())
247        else {
248            return;
249        };
250
251        systems::deal_bound_effect_damage_ap_with_geometry(
252            &mut ctx,
253            binding,
254            effect_ref,
255            geometry_ref,
256            coefficient,
257            flags,
258        );
259    }
260
261    fn deal_effect_damage_sp_with_geometry(
262        &mut self,
263        effect: DamageEffectId,
264        geometry_effect: DamageEffectId,
265        multiplier: f64,
266        flags: systems::DamageFlags,
267    ) {
268        let coefficient = self.effect_sp_coefficient(effect) * multiplier;
269
270        if coefficient <= 0.0 {
271            return;
272        }
273
274        let Some(target) = self.target else {
275            return;
276        };
277        let flags = self.source_flags(flags);
278        let coefficient = self.resolved_pet_coefficient(
279            flags,
280            wowlab_engine_domain::dbc::PetStatKind::SpellPowerInheritance,
281            coefficient,
282        );
283        let mut ctx = self.combat_ctx(target);
284        let effect_ref =
285            crate::state::DamageEffectRef::new(effect.action().spell_id(), effect.effect_index());
286        let geometry_ref = crate::state::DamageEffectRef::new(
287            geometry_effect.action().spell_id(),
288            geometry_effect.effect_index(),
289        );
290        let Some(binding) =
291            ctx.program
292                .damage
293                .effect_id_request(effect, geometry_effect, effect.action())
294        else {
295            return;
296        };
297
298        systems::deal_effect_damage_sp_with_geometry(
299            &mut ctx,
300            binding,
301            effect_ref,
302            geometry_ref,
303            coefficient,
304            flags,
305        );
306    }
307
308    fn deal_physical_damage_ap_if_positive(&mut self, action: DamageActionId, coefficient: f64) {
309        self.deal_damage_ap_if_positive(action, coefficient, systems::DamageFlags::PHYSICAL);
310    }
311
312    fn deal_pet_damage_ap(&mut self, action: DamageActionId, coefficient: f64) {
313        self.deal_damage_ap(action, coefficient, systems::DamageFlags::PET);
314    }
315
316    /// Deal pet AP damage using a construction-resolved NPC inheritance scalar.
317    fn deal_pet_damage_ap_with_inheritance(
318        &mut self,
319        action: DamageActionId,
320        coefficient: f64,
321        inheritance_multiplier: f64,
322    ) {
323        let Some(target) = self.target else {
324            return;
325        };
326        let flags = self.source_flags(systems::DamageFlags::PET);
327        let coefficient = coefficient * inheritance_multiplier;
328        let mut ctx = self.combat_ctx_with_source(ActorId::Pet(PetIdx::PRIMARY), target);
329        let spell_id = action.spell_id();
330        let Some(binding) = ctx.program.damage.action_id_request(action) else {
331            return;
332        };
333
334        systems::deal_bound_damage_ap(
335            &mut ctx,
336            binding,
337            systems::DamagePayload::new(spell_id, coefficient, flags),
338        );
339    }
340
341    fn deal_physical_pet_damage_ap(&mut self, action: DamageActionId, coefficient: f64) {
342        self.deal_damage_ap(
343            action,
344            coefficient,
345            systems::DamageFlags::PHYSICAL | systems::DamageFlags::PET,
346        );
347    }
348
349    fn deal_damage_sp(&mut self, action: DamageActionId, coef: f64, flags: systems::DamageFlags) {
350        let Some(target) = self.target else {
351            return;
352        };
353        let flags = self.source_flags(flags);
354        let coef = self.resolved_pet_coefficient(
355            flags,
356            wowlab_engine_domain::dbc::PetStatKind::SpellPowerInheritance,
357            coef,
358        );
359        let mut ctx = self.combat_ctx(target);
360        let spell_id = action.spell_id();
361        let Some(binding) = ctx.program.damage.action_id_request(action) else {
362            return;
363        };
364
365        systems::deal_bound_damage_sp(
366            &mut ctx,
367            binding,
368            systems::DamagePayload::new(spell_id, coef, flags),
369        );
370    }
371
372    /// Deal exactly one spell-power hit to a previously resolved enemy identity.
373    fn deal_damage_sp_to_target(
374        &mut self,
375        target: EnemyIdx,
376        action: DamageActionId,
377        coef: f64,
378        flags: systems::DamageFlags,
379    ) {
380        let flags = self.source_flags(flags);
381        let coef = self.resolved_pet_coefficient(
382            flags,
383            wowlab_engine_domain::dbc::PetStatKind::SpellPowerInheritance,
384            coef,
385        );
386        let mut ctx = self.combat_ctx(target);
387        let spell_id = action.spell_id();
388        let Some(binding) = ctx.program.damage.action_id_request(action) else {
389            return;
390        };
391
392        systems::deal_damage_sp_at(
393            &mut ctx,
394            target,
395            binding,
396            systems::DamagePayload::new(spell_id, coef, flags),
397        );
398    }
399
400    fn deal_profiled_damage_sp(
401        &mut self,
402        action: DamageActionId,
403        profile: DamageActionId,
404        coef: f64,
405        flags: systems::DamageFlags,
406    ) {
407        let Some(target) = self.target else {
408            return;
409        };
410        let flags = self.source_flags(flags);
411        let coef = self.resolved_pet_coefficient(
412            flags,
413            wowlab_engine_domain::dbc::PetStatKind::SpellPowerInheritance,
414            coef,
415        );
416        let mut ctx = self.combat_ctx(target);
417        let spell_id = action.spell_id();
418        let profile_spell_id = profile.spell_id();
419        let Some(binding) = ctx
420            .program
421            .damage
422            .profiled_action_id_request(action, profile)
423        else {
424            return;
425        };
426
427        systems::deal_bound_damage_sp(
428            &mut ctx,
429            binding,
430            systems::DamagePayload::profiled(spell_id, profile_spell_id, coef, flags),
431        );
432    }
433
434    fn deal_damage_flat(&mut self, action: DamageActionId, amount: f64) {
435        let Some(target) = self.target else {
436            return;
437        };
438        let spell_id = action.spell_id();
439        let flags = systems::damage_flags_from_binding(self.program, action, false);
440        let flags = self.source_flags(flags);
441        let mut ctx = self.combat_ctx(target);
442        let Some(binding) = ctx.program.damage.action_id_request(action) else {
443            return;
444        };
445
446        systems::deal_damage_def(
447            &mut ctx,
448            binding,
449            spell_id,
450            RuntimeDamageDef::Flat(amount),
451            flags,
452        );
453    }
454
455    /// Deal a post-mitigation redirected amount without a second modifier pass.
456    fn deal_resolved_damage(&mut self, action: DamageActionId, amount: f64) {
457        let Some(target) = self.target else {
458            return;
459        };
460        let flags = self.source_flags(systems::DamageFlags::empty());
461        let mut ctx = self.combat_ctx(target);
462
463        let Some(binding) = ctx.program.damage.action_id_request(action) else {
464            return;
465        };
466
467        systems::deal_bound_resolved_damage(&mut ctx, binding, action.spell_id(), amount, flags);
468    }
469
470    /// Follow one resolved DBC trigger edge and deal its terminal child damage.
471    fn deal_triggered_damage(
472        &mut self,
473        effect: DamageEffectId,
474        multiplier: f64,
475        flags: systems::DamageFlags,
476    ) -> bool {
477        let Some(target) = self.target else {
478            return false;
479        };
480        let Some((binding, planned)) = self.program.damage.triggered_effect_id_request(effect)
481        else {
482            tracing::warn!(
483                source_spell_id = effect.action().spell_id(),
484                effect_index = effect.effect_index(),
485                "TRIGGER_DAMAGE_UNRESOLVED"
486            );
487
488            return false;
489        };
490        let Some(damage) = planned
491            .resolved_damage
492            .map(|damage| damage.scaled(multiplier))
493        else {
494            return false;
495        };
496        let base_points = Some(planned.resolved_damage_base_points * multiplier);
497        let flags = self.source_flags(flags);
498        let mut ctx = self.combat_ctx(target);
499        let geometry = crate::state::DamageEffectRef::new(
500            planned.geometry.spell.as_u32(),
501            planned.geometry.effect_index,
502        );
503
504        systems::deal_bound_profiled_damage_def(
505            &mut ctx,
506            systems::ProfiledDamageDef {
507                binding,
508                effect: geometry,
509                geometry_effect: geometry,
510                profile_spell_id: effect.action().spell_id(),
511                base_points,
512                def: damage,
513                flags,
514                source_position: None,
515                destination: None,
516            },
517        );
518
519        true
520    }
521
522    /// Deal an item's spell-power damage, reading its effect-1 SP coefficient from game data.
523    fn deal_item_damage_sp(&mut self, action: DamageActionId) {
524        let coef = self
525            .program
526            .damage
527            .action_id_request(action)
528            .map(|binding| self.program.damage.effect_at(binding.effect))
529            .map_or(0.0, |effect| effect.sp_coefficient);
530
531        self.deal_damage_sp(action, coef, systems::DamageFlags::empty());
532    }
533
534    fn deal_hook_flat_damage(&mut self, action: DamageActionId) -> bool {
535        let Some(amount) = self
536            .program
537            .damage
538            .action_id_plan(action)
539            .and_then(|planned| planned.hook_flat_damage)
540        else {
541            return false;
542        };
543
544        self.deal_damage_flat(action, amount);
545
546        true
547    }
548
549    fn player_swing_period_ms(&self, hand: SwingHand) -> Option<u32> {
550        systems::active_form_combat_round_time_ms(self.program, self.instance)
551            .or_else(|| match hand {
552                SwingHand::MainHand => Some(self.program.damage.main_hand.speed_ms),
553                SwingHand::OffHand => self.program.damage.off_hand.map(|weapon| weapon.speed_ms),
554            })
555            .filter(|period_ms| *period_ms > 0)
556    }
557
558    fn effect_base_points(&self, effect: DamageEffectId) -> f64 {
559        self.program
560            .damage
561            .effect_id_plan(effect)
562            .map(|(_, planned)| planned)
563            .map_or(0.0, |planned| planned.base_points)
564    }
565
566    fn effect_points(&self, effect: DamageEffectId) -> f64 {
567        let Some(planned) = self
568            .program
569            .damage
570            .effect_id_plan(effect)
571            .map(|(_, planned)| planned)
572        else {
573            return 0.0;
574        };
575        let mastery_affects_points = self
576            .program
577            .damage
578            .action_id_plan(effect.action())
579            .is_some_and(|action| action.mastery_affects_points);
580        let mastery = if mastery_affects_points {
581            systems::current_mastery_points(&self.view().combat())
582        } else {
583            0.0
584        };
585
586        planned.base_points + mastery * planned.sp_coefficient
587    }
588
589    fn effect_percent(&self, effect: DamageEffectId) -> f64 {
590        self.effect_base_points(effect) / wowlab_types::constants::HUNDRED
591    }
592
593    fn effect_ap_coefficient(&self, effect: DamageEffectId) -> f64 {
594        self.program
595            .damage
596            .effect_id_plan(effect)
597            .map(|(_, planned)| planned)
598            .map_or(0.0, |planned| planned.ap_coefficient)
599    }
600
601    fn effect_sp_coefficient(&self, effect: DamageEffectId) -> f64 {
602        self.program
603            .damage
604            .effect_id_plan(effect)
605            .map(|(_, planned)| planned)
606            .map_or(0.0, |planned| planned.sp_coefficient)
607    }
608
609    fn effect_period_ms(&self, effect: DamageEffectId) -> u32 {
610        self.program
611            .damage
612            .effect_id_plan(effect)
613            .map(|(_, planned)| planned.period)
614            .filter(|period| period.is_finite() && *period > 0.0)
615            .map_or(0, wowlab_types::numeric::f64_to_u32_saturating_round)
616    }
617
618    fn action_proc_chance(&self, action: DamageActionId) -> f64 {
619        self.program
620            .damage
621            .action_id_plan(action)
622            .map_or(0.0, |planned| planned.proc_chance)
623    }
624
625    fn action_gcd_ms(&self, action: DamageActionId) -> u32 {
626        self.program
627            .damage
628            .action_id_plan(action)
629            .map_or(0, |planned| planned.gcd_ms)
630    }
631
632    fn action_aura_duration_ms(&self, action: DamageActionId) -> u32 {
633        self.program
634            .damage
635            .action_id_plan(action)
636            .map_or(0, |planned| planned.aura_duration_ms)
637    }
638
639    fn action_aura_tick_ms(&self, action: DamageActionId) -> u32 {
640        self.program
641            .damage
642            .action_id_plan(action)
643            .map_or(0, |planned| planned.aura_tick_ms)
644    }
645
646    fn action_secondary_gain(&self, action: DamageActionId) -> f64 {
647        self.program
648            .damage
649            .action_id_plan(action)
650            .map_or(0.0, |planned| planned.secondary_gain)
651    }
652
653    fn action_hook_scalar(&self, action: DamageActionId) -> Option<f64> {
654        self.program
655            .damage
656            .action_id_plan(action)
657            .and_then(|planned| planned.hook_scalar)
658    }
659
660    fn action_passive_direct_damage_mult(&self, action: DamageActionId) -> f64 {
661        self.program
662            .damage
663            .action_id_plan(action)
664            .map_or(1.0, |planned| planned.passive.direct_damage_mult)
665    }
666
667    fn action_is_melee(&self, action: DamageActionId) -> bool {
668        self.program
669            .damage
670            .action_id_plan(action)
671            .is_some_and(|planned| {
672                planned.defense_type == wowlab_engine_domain::dbc::DefenseType::Melee as i32
673            })
674    }
675
676    fn action_has_direct_damage(&self, action: DamageActionId) -> bool {
677        self.program
678            .damage
679            .action_id_plan(action)
680            .is_some_and(|planned| planned.has_direct_damage)
681    }
682
683    fn action_requires_main_hand(&self, action: DamageActionId) -> bool {
684        self.program
685            .damage
686            .action_id_plan(action)
687            .is_some_and(|planned| {
688                wowlab_engine_domain::dbc::spell_attribute_is(
689                    &planned.attributes,
690                    wowlab_engine_domain::dbc::SpellAttributeKind::RequiresMainHand,
691                )
692            })
693    }
694
695    fn action_requires_off_hand(&self, action: DamageActionId) -> bool {
696        self.program
697            .damage
698            .action_id_plan(action)
699            .is_some_and(|planned| {
700                wowlab_engine_domain::dbc::spell_attribute_is(
701                    &planned.attributes,
702                    wowlab_engine_domain::dbc::SpellAttributeKind::RequiresOffHand,
703                )
704            })
705    }
706
707    fn action_suppresses_caster_procs(&self, action: DamageActionId) -> bool {
708        self.program
709            .damage
710            .action_id_plan(action)
711            .is_some_and(|planned| {
712                wowlab_engine_domain::dbc::spell_attribute_is(
713                    &planned.attributes,
714                    wowlab_engine_domain::dbc::SpellAttributeKind::SuppressCasterProcs,
715                )
716            })
717    }
718
719    fn impact_has_label(&self, impact: crate::ImpactEvent, label: i32) -> bool {
720        impact.action.is_some_and(|action| {
721            self.program
722                .damage
723                .action_at(action)
724                .labels
725                .contains(&label)
726        })
727    }
728
729    fn impact_is_physical(&self, impact: crate::ImpactEvent) -> bool {
730        impact.action.is_some_and(|action| {
731            self.program
732                .damage
733                .action_at(action)
734                .school_mask
735                .contains(wowlab_engine_domain::dbc::SpellSchoolMask::PHYSICAL)
736        })
737    }
738
739    fn impact_is_nature(&self, impact: crate::ImpactEvent) -> bool {
740        impact.action.is_some_and(|action| {
741            self.program.damage.action_at(action).school
742                == wowlab_types::combat::DamageSchool::Nature
743        })
744    }
745
746    fn impact_generates_primary_resource(&self, impact: crate::ImpactEvent) -> bool {
747        impact
748            .action
749            .is_some_and(|action| self.program.damage.action_at(action).primary_resource_gain > 0.0)
750    }
751
752    fn effect_affects_impact(&self, effect: DamageEffectId, impact: crate::ImpactEvent) -> bool {
753        let Some(action) = impact.action else {
754            return false;
755        };
756        let Some((effect, _)) = self.program.damage.effect_id_plan(effect) else {
757            return false;
758        };
759
760        self.program.damage.effect_affects_action(effect, action)
761    }
762
763    fn target_health_fraction(&self) -> f64 {
764        self.target
765            .filter(|target| self.instance.is_valid_target(*target))
766            .and_then(|target| {
767                self.instance
768                    .enemy_health_fraction(self.program, target, self.now)
769            })
770            .unwrap_or(1.0)
771    }
772}