1use 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#[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 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 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 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 fn haste_multiplier(&self) -> f64 {
137 systems::current_haste_mult(&self.view().combat())
138 }
139
140 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 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 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 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 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 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}