1use wowlab_engine_domain::rotation::DenseBuffer;
4use wowlab_engine_gamedata::ResolvedGameData;
5use wowlab_engine_telemetry::{DamageEvent, DamageEventFlags, TelemetrySink};
6use wowlab_types::sim::{ActorId, EnemyIdx, SpellIdx};
7
8use super::{
9 current_spell_power,
10 dispatch::{deal_damage, deal_damage_to_target},
11 resolve_outgoing_absorbs, school_of, typed_attack_power,
12 weapon::{WeaponDamageInput, weapon_damage_input},
13};
14use crate::{
15 builder::buffer_init::project_encounter, context::CombatCtx, state::CombatState,
16 systems::procs::fire_impact_procs,
17};
18
19pub(crate) fn damage_flags_from_data(
20 data: &ResolvedGameData,
21 spell_id: SpellIdx,
22 physical: bool,
23) -> DamageFlags {
24 let mut flags = if physical {
25 DamageFlags::PHYSICAL
26 } else {
27 DamageFlags::empty()
28 };
29
30 if data.cannot_crit(spell_id).unwrap_or(false) {
31 flags |= DamageFlags::NO_CRIT;
32 }
33
34 if data.treat_as_periodic(spell_id).unwrap_or(false) {
35 flags |= DamageFlags::PERIODIC;
36 }
37
38 if data.treat_as_area_effect(spell_id).unwrap_or(false) {
39 flags |= DamageFlags::AOE;
40 }
41
42 if data
43 .disable_player_damage_multiplier(spell_id)
44 .unwrap_or(false)
45 {
46 flags |= DamageFlags::IGNORE_PLAYER_MULTIPLIERS;
47 }
48
49 if data
50 .disable_target_damage_multiplier(spell_id)
51 .unwrap_or(false)
52 {
53 flags |= DamageFlags::IGNORE_TARGET_MULTIPLIERS;
54 }
55
56 if data
57 .disable_positive_target_damage_multiplier(spell_id)
58 .unwrap_or(false)
59 {
60 flags |= DamageFlags::IGNORE_POSITIVE_TARGET_MULTIPLIERS;
61 }
62
63 flags
64}
65
66pub(in crate::systems) fn finalize_damage(
67 ctx: &mut CombatCtx<'_>,
68 target: EnemyIdx,
69 event: &DamageEvent,
70) -> bool {
71 finalize_damage_with(ctx, target, event, emit_damage_telemetry);
72
73 !ctx.state.has_runtime_error()
74}
75
76pub(in crate::systems) fn emit_damage_telemetry(
77 _source: ActorId,
78 _target: EnemyIdx,
79 event: &DamageEvent,
80 _state: &CombatState,
81 _buf: &DenseBuffer,
82 sink: &mut TelemetrySink,
83) {
84 sink.emit_damage(event);
85}
86
87pub(in crate::systems) fn finalize_damage_with(
88 ctx: &mut CombatCtx<'_>,
89 target: EnemyIdx,
90 event: &DamageEvent,
91 emit: impl FnOnce(ActorId, EnemyIdx, &DamageEvent, &CombatState, &DenseBuffer, &mut TelemetrySink),
92) {
93 if !ctx.state.is_valid_target(target) {
94 return;
95 }
96
97 let amount = event.amount;
98
99 emit(ctx.source, target, event, ctx.state, ctx.buf, ctx.sink);
100
101 ctx.state.runtime.total_damage += amount;
102 ctx.state.refresh_enemy_health(target, ctx.now);
103 let before_health = ctx
104 .state
105 .enemy_health_fraction(target, ctx.now)
106 .unwrap_or(1.0);
107 let death = match ctx.state.apply_enemy_damage(target, amount, ctx.now) {
108 Ok(death) => death,
109 Err(error) => {
110 ctx.state.record_runtime_error(error);
111
112 return;
113 }
114 };
115 let after_health = ctx
116 .state
117 .enemy_health_fraction(target, ctx.now)
118 .unwrap_or(before_health);
119 let actual_damage = ctx
120 .state
121 .enemy_max_health(target)
122 .map_or(amount, |maximum| {
123 ((before_health - after_health).max(0.0) * maximum).min(amount)
124 });
125
126 crate::systems::process_actor_health_change(
127 ctx.state,
128 ctx.buf,
129 ActorId::Enemy(target),
130 before_health,
131 after_health,
132 ctx.now,
133 );
134 project_encounter(ctx.state, ctx.buf, ctx.now);
135 super::leech::apply_leech_healing(ctx, event, actual_damage);
136
137 if let Some(dead) = death {
138 ctx.state.schedule(wowlab_engine_ports::Event::Death {
139 t: dead.at,
140 target: dead.enemy,
141 scope: wowlab_engine_ports::DeathEventScope::Unscoped,
142 });
143
144 if matches!(ctx.source, ActorId::Player | ActorId::Pet(_)) {
145 crate::systems::procs::fire_kill_procs(ctx, event.spell_id, dead.enemy);
146 }
147 }
148}
149
150pub(crate) fn resolve_spell_impact_flags(
151 ctx: &mut CombatCtx<'_>,
152 profile_spell_id: u32,
153 flags: DamageFlags,
154) -> Option<DamageFlags> {
155 if flags.contains(DamageFlags::REFLECTION_RESOLVED) {
156 return Some(flags);
157 }
158
159 let school = school_of(ctx.state, profile_spell_id);
160
161 match crate::systems::buffs::resolve_incoming_spell_defense(
162 ctx.state,
163 ctx.buf,
164 ctx.target,
165 profile_spell_id,
166 school,
167 ctx.rng,
168 ) {
169 crate::systems::buffs::IncomingSpellDefense::None => {
170 Some(flags | DamageFlags::REFLECTION_RESOLVED)
171 }
172 crate::systems::buffs::IncomingSpellDefense::Reflected => {
173 Some(flags | DamageFlags::REFLECTION_RESOLVED | DamageFlags::REFLECTED)
174 }
175 crate::systems::buffs::IncomingSpellDefense::Deflected => None,
176 }
177}
178bitflags::bitflags! {
179 #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
181 pub struct DamageFlags: u16 {
182 const PHYSICAL = 1 << 0;
183 const PET = 1 << 1;
185 const PERIODIC = 1 << 2;
186 const NO_CRIT = 1 << 3;
187 const GUARDIAN = 1 << 4;
189 const IGNORE_PLAYER_MULTIPLIERS = 1 << 5;
190 const IGNORE_TARGET_MULTIPLIERS = 1 << 6;
191 const IGNORE_POSITIVE_TARGET_MULTIPLIERS = 1 << 7;
192 const PROC = 1 << 8;
193 const REPORT_AS_PROFILE = 1 << 9;
195 const AOE = 1 << 10;
197 const REFLECTION_RESOLVED = 1 << 11;
199 const REFLECTED = 1 << 12;
201 const IGNORE_CRIT_DAMAGE_BONUSES = 1 << 13;
203 }
204}
205
206#[derive(Clone, Copy, Debug)]
208pub struct DamagePayload {
209 pub spell_id: u32,
210 pub profile_spell_id: Option<u32>,
211 pub coef: f64,
212 pub flags: DamageFlags,
213}
214
215impl DamagePayload {
216 #[must_use]
217 pub const fn new(spell_id: u32, coef: f64, flags: DamageFlags) -> Self {
218 Self {
219 spell_id,
220 profile_spell_id: None,
221 coef,
222 flags,
223 }
224 }
225
226 #[must_use]
227 pub const fn profiled(
228 spell_id: u32,
229 profile_spell_id: u32,
230 coef: f64,
231 flags: DamageFlags,
232 ) -> Self {
233 Self {
234 spell_id,
235 profile_spell_id: Some(profile_spell_id),
236 coef,
237 flags,
238 }
239 }
240
241 const fn resolved_profile_spell_id(self) -> u32 {
242 match self.profile_spell_id {
243 Some(profile_spell_id) => profile_spell_id,
244 None => self.spell_id,
245 }
246 }
247}
248
249#[inline]
250pub(in crate::systems) const fn reported_damage_spell_id(request: &DamageRequest) -> u32 {
251 if request.flags.contains(DamageFlags::REPORT_AS_PROFILE) {
252 request.profile_spell_id
253 } else {
254 request.effect.spell_id
255 }
256}
257
258#[derive(Clone, Copy, Debug)]
259pub(in crate::systems) struct DamageRequest {
260 pub effect: crate::state::DamageEffectRef,
261 pub geometry_effect: crate::state::DamageEffectRef,
262 pub source_position: Option<wowlab_types::sim::SpatialTransform>,
263 pub destination: Option<wowlab_types::sim::SpatialTransform>,
264 pub profile_spell_id: u32,
265 pub base_points: Option<f64>,
266 pub coef: f64,
267 pub power: f64,
268 pub weapon: WeaponDamageInput,
269 pub flags: DamageFlags,
270}
271
272impl DamageRequest {
273 const fn new(payload: DamagePayload, power: f64) -> Self {
274 let effect = crate::state::DamageEffectRef::new(payload.spell_id, 1);
275
276 Self {
277 effect,
278 geometry_effect: effect,
279 source_position: None,
280 destination: None,
281 profile_spell_id: payload.resolved_profile_spell_id(),
282 base_points: None,
283 coef: payload.coef,
284 power,
285 weapon: WeaponDamageInput::none(),
286 flags: payload.flags,
287 }
288 }
289
290 const fn profile(mut self, spell_id: u32) -> Self {
291 self.profile_spell_id = spell_id;
292
293 self
294 }
295
296 const fn effect(mut self, effect: crate::state::DamageEffectRef) -> Self {
297 self.effect = effect;
298 self.geometry_effect = effect;
299
300 self
301 }
302
303 const fn geometry(
304 mut self,
305 effect: crate::state::DamageEffectRef,
306 source_position: Option<wowlab_types::sim::SpatialTransform>,
307 destination: Option<wowlab_types::sim::SpatialTransform>,
308 ) -> Self {
309 self.geometry_effect = effect;
310 self.source_position = source_position;
311 self.destination = destination;
312
313 self
314 }
315
316 const fn weapon(mut self, weapon: WeaponDamageInput) -> Self {
317 self.weapon = weapon;
318
319 self
320 }
321
322 const fn base_points(mut self, base_points: f64) -> Self {
323 self.base_points = Some(base_points);
324
325 self
326 }
327
328 const fn optional_base_points(mut self, base_points: Option<f64>) -> Self {
329 self.base_points = base_points;
330
331 self
332 }
333}
334
335pub(crate) fn deal_residual_damage(ctx: &mut CombatCtx<'_>, spell_id: u32, amount: f64) {
337 deal_resolved_damage(ctx, spell_id, amount, DamageFlags::PERIODIC);
338}
339
340pub(crate) fn deal_resolved_damage(
342 ctx: &mut CombatCtx<'_>,
343 spell_id: u32,
344 amount: f64,
345 flags: DamageFlags,
346) {
347 if amount <= 0.0 {
348 return;
349 }
350
351 if !ctx.state.is_valid_target(ctx.target) {
352 return;
353 }
354
355 let Some(scope) = ctx.state.telemetry_scope(ctx.source, ctx.target) else {
356 return;
357 };
358
359 let resolved = resolve_outgoing_absorbs(
360 ctx.state,
361 ctx.target,
362 school_of(ctx.state, spell_id),
363 amount,
364 wowlab_types::combat::HitResult::Hit,
365 );
366
367 if resolved.amount > f64::EPSILON
368 && !finalize_damage(
369 ctx,
370 ctx.target,
371 &DamageEvent::new(
372 wowlab_engine_telemetry::DamageObservation {
373 spell_id,
374 amount: resolved.amount,
375 flags: DamageEventFlags::default()
376 .with_periodic(flags.contains(DamageFlags::PERIODIC))
377 .with_pet(flags.contains(DamageFlags::PET)),
378 },
379 ctx.now.as_millis(),
380 scope,
381 ),
382 )
383 {
384 return;
385 }
386
387 fire_impact_procs(
388 ctx,
389 crate::ImpactEvent {
390 kind: crate::state::ProcEventKind::Spell,
391 phase: crate::state::ProcPhaseMask::HIT,
392 hit_mask: resolved.hit_mask,
393 spell_id,
394 amount: resolved.amount,
395 is_periodic: flags.contains(DamageFlags::PERIODIC),
396 is_physical: school_of(ctx.state, spell_id)
397 == wowlab_types::combat::DamageSchool::Physical,
398 swing_hand: None,
399 swing_speed_ms: 0,
400 source: ctx.source,
401 target: ctx.target,
402 provenance: crate::state::ProcProvenance::from_spell(
403 &ctx.state.config.game_data,
404 spell_id,
405 if flags.contains(DamageFlags::PROC) {
406 crate::state::ProcSourceKind::Proc
407 } else {
408 crate::state::ProcSourceKind::Action
409 },
410 ),
411 },
412 );
413}
414
415pub fn deal_damage_ap(ctx: &mut CombatCtx<'_>, payload: DamagePayload) -> (f64, bool) {
417 deal_damage_ap_typed(ctx, payload, crate::state::WeaponApType::MainHand)
418}
419
420pub(crate) fn deal_damage_ap_at(
421 ctx: &mut CombatCtx<'_>,
422 target: EnemyIdx,
423 payload: DamagePayload,
424) -> (f64, bool) {
425 let power = typed_attack_power(ctx.state, ctx.buf, crate::state::WeaponApType::MainHand);
426
427 deal_damage_to_target(ctx, target, DamageRequest::new(payload, power))
428}
429
430pub(super) fn deal_damage_ap_typed(
432 ctx: &mut CombatCtx<'_>,
433 payload: DamagePayload,
434 ap_type: crate::state::WeaponApType,
435) -> (f64, bool) {
436 let power = typed_attack_power(ctx.state, ctx.buf, ap_type);
437
438 deal_damage(ctx, DamageRequest::new(payload, power))
439}
440
441pub(crate) fn deal_effect_damage_ap(
442 ctx: &mut CombatCtx<'_>,
443 effect: crate::state::DamageEffectRef,
444 coef: f64,
445 flags: DamageFlags,
446) -> (f64, bool) {
447 let power = typed_attack_power(ctx.state, ctx.buf, crate::state::WeaponApType::MainHand);
448
449 deal_damage(
450 ctx,
451 DamageRequest::new(DamagePayload::new(effect.spell_id, coef, flags), power).effect(effect),
452 )
453}
454
455pub(crate) fn deal_effect_damage_ap_with_geometry(
456 ctx: &mut CombatCtx<'_>,
457 effect: crate::state::DamageEffectRef,
458 geometry_effect: crate::state::DamageEffectRef,
459 coef: f64,
460 flags: DamageFlags,
461) -> (f64, bool) {
462 let destination = ctx.state.actor_transform(ActorId::Enemy(ctx.target));
463 let power = typed_attack_power(ctx.state, ctx.buf, crate::state::WeaponApType::MainHand);
464
465 deal_damage(
466 ctx,
467 DamageRequest::new(DamagePayload::new(effect.spell_id, coef, flags), power)
468 .effect(effect)
469 .geometry(geometry_effect, None, destination),
470 )
471}
472
473pub(crate) fn deal_effect_damage_sp_with_geometry(
474 ctx: &mut CombatCtx<'_>,
475 effect: crate::state::DamageEffectRef,
476 geometry_effect: crate::state::DamageEffectRef,
477 coef: f64,
478 flags: DamageFlags,
479) -> (f64, bool) {
480 let destination = ctx.state.actor_transform(ActorId::Enemy(ctx.target));
481
482 deal_damage(
483 ctx,
484 DamageRequest::new(
485 DamagePayload::new(effect.spell_id, coef, flags),
486 current_spell_power(ctx.state, ctx.buf),
487 )
488 .effect(effect)
489 .geometry(geometry_effect, None, destination),
490 )
491}
492
493pub(crate) fn deal_effect_damage_sp(
494 ctx: &mut CombatCtx<'_>,
495 effect: crate::state::DamageEffectRef,
496 coef: f64,
497 flags: DamageFlags,
498) -> (f64, bool) {
499 deal_damage(
500 ctx,
501 DamageRequest::new(
502 DamagePayload::new(effect.spell_id, coef, flags),
503 current_spell_power(ctx.state, ctx.buf),
504 )
505 .effect(effect),
506 )
507}
508
509pub(in crate::systems) fn deal_damage_ap_with_weapon(
510 ctx: &mut CombatCtx<'_>,
511 payload: DamagePayload,
512 weapon: WeaponDamageInput,
513) -> (f64, bool) {
514 let ap_type = if weapon.max > 0.0 {
515 crate::state::WeaponApType::None
516 } else {
517 crate::state::WeaponApType::MainHand
518 };
519 let power = typed_attack_power(ctx.state, ctx.buf, ap_type);
520
521 deal_damage(ctx, DamageRequest::new(payload, power).weapon(weapon))
522}
523
524pub(crate) fn deal_damage_sp(ctx: &mut CombatCtx<'_>, payload: DamagePayload) -> (f64, bool) {
525 let power = current_spell_power(ctx.state, ctx.buf);
526
527 deal_damage(ctx, DamageRequest::new(payload, power))
528}
529
530pub(crate) fn deal_damage_sp_at(
531 ctx: &mut CombatCtx<'_>,
532 target: EnemyIdx,
533 payload: DamagePayload,
534) -> (f64, bool) {
535 let power = current_spell_power(ctx.state, ctx.buf);
536
537 deal_damage_to_target(ctx, target, DamageRequest::new(payload, power))
538}
539
540pub(crate) fn deal_damage_base(
542 ctx: &mut CombatCtx<'_>,
543 spell_id: u32,
544 amount: f64,
545 flags: DamageFlags,
546) -> (f64, bool) {
547 deal_damage(
548 ctx,
549 DamageRequest::new(DamagePayload::new(spell_id, 0.0, flags), 0.0).base_points(amount),
550 )
551}
552
553pub(crate) fn deal_effect_damage_base(
555 ctx: &mut CombatCtx<'_>,
556 effect: crate::state::DamageEffectRef,
557 profile_spell_id: u32,
558 amount: f64,
559 flags: DamageFlags,
560) -> (f64, bool) {
561 deal_damage(
562 ctx,
563 DamageRequest::new(DamagePayload::new(effect.spell_id, 0.0, flags), 0.0)
564 .profile(profile_spell_id)
565 .effect(effect)
566 .base_points(amount),
567 )
568}
569
570pub(crate) fn deal_damage_def(
571 ctx: &mut CombatCtx<'_>,
572 spell_id: u32,
573 def: crate::state::RuntimeDamageDef,
574 flags: DamageFlags,
575) {
576 deal_profiled_damage_def(
577 ctx,
578 crate::state::DamageEffectRef::new(spell_id, 1),
579 spell_id,
580 None,
581 def,
582 flags,
583 );
584}
585
586pub(crate) fn deal_profiled_damage_def(
587 ctx: &mut CombatCtx<'_>,
588 effect: crate::state::DamageEffectRef,
589 profile_spell_id: u32,
590 base_points: Option<f64>,
591 def: crate::state::RuntimeDamageDef,
592 flags: DamageFlags,
593) -> (f64, bool) {
594 deal_profiled_damage_def_at(
595 ctx,
596 &ProfiledDamageDef {
597 effect,
598 geometry_effect: effect,
599 profile_spell_id,
600 base_points,
601 def,
602 flags,
603 source_position: None,
604 destination: None,
605 },
606 )
607}
608
609#[derive(Clone, Copy)]
610pub(crate) struct ProfiledDamageDef {
611 pub(crate) effect: crate::state::DamageEffectRef,
612 pub(crate) geometry_effect: crate::state::DamageEffectRef,
613 pub(crate) profile_spell_id: u32,
614 pub(crate) base_points: Option<f64>,
615 pub(crate) def: crate::state::RuntimeDamageDef,
616 pub(crate) flags: DamageFlags,
617 pub(crate) source_position: Option<wowlab_types::sim::SpatialTransform>,
618 pub(crate) destination: Option<wowlab_types::sim::SpatialTransform>,
619}
620
621pub(in crate::systems) fn resolved_damage_geometry_effect(
622 data: &ResolvedGameData,
623 effect: crate::state::DamageEffectRef,
624) -> crate::state::DamageEffectRef {
625 let spell_id = SpellIdx::from_raw(effect.spell_id);
626 let effect_type = data.effect_type(spell_id, effect.effect_index);
627
628 if wowlab_engine_domain::dbc::spell_effect_is_any(
629 effect_type,
630 &[
631 wowlab_engine_domain::dbc::SpellEffectKind::TriggerMissile,
632 wowlab_engine_domain::dbc::SpellEffectKind::TriggerSpell,
633 wowlab_engine_domain::dbc::SpellEffectKind::TriggerSpellWithValue,
634 wowlab_engine_domain::dbc::SpellEffectKind::TriggerSpell2,
635 ],
636 ) && let Ok(triggered) = wowlab_engine_domain::dbc::resolve_triggered_damage(
637 wowlab_engine_domain::dbc::EffectLookup::new(
638 data,
639 wowlab_types::sim::EffectRef::new(spell_id, effect.effect_index),
640 ),
641 ) {
642 return crate::state::DamageEffectRef::new(
643 triggered.spell_id.as_u32(),
644 triggered.damage.effect_index,
645 );
646 }
647
648 effect
649}
650
651pub(crate) fn deal_profiled_damage_def_at(
652 ctx: &mut CombatCtx<'_>,
653 request: &ProfiledDamageDef,
654) -> (f64, bool) {
655 let ProfiledDamageDef {
656 effect,
657 mut geometry_effect,
658 profile_spell_id,
659 base_points,
660 def,
661 flags,
662 source_position,
663 destination,
664 } = *request;
665
666 if geometry_effect == effect {
667 geometry_effect = resolved_damage_geometry_effect(&ctx.state.config.game_data, effect);
668 }
669
670 match def {
671 crate::state::RuntimeDamageDef::None => {
672 tracing::trace!(
673 spell_id = effect.spell_id,
674 is_periodic = flags.contains(DamageFlags::PERIODIC),
675 "damage dispatch with no damage defined"
676 );
677
678 (0.0, false)
679 }
680 crate::state::RuntimeDamageDef::Flat(amount) => deal_damage(
681 ctx,
682 DamageRequest::new(
683 DamagePayload::profiled(effect.spell_id, profile_spell_id, 0.0, flags),
684 0.0,
685 )
686 .effect(effect)
687 .geometry(geometry_effect, source_position, destination)
688 .base_points(amount),
689 ),
690 crate::state::RuntimeDamageDef::ApCoefficient {
691 coef,
692 is_physical,
693 ap_type,
694 } => {
695 let power = typed_attack_power(ctx.state, ctx.buf, ap_type);
696
697 deal_damage(
698 ctx,
699 DamageRequest::new(
700 DamagePayload::profiled(
701 effect.spell_id,
702 profile_spell_id,
703 coef,
704 if is_physical {
705 flags | DamageFlags::PHYSICAL
706 } else {
707 flags
708 },
709 ),
710 power,
711 )
712 .effect(effect)
713 .geometry(geometry_effect, source_position, destination)
714 .optional_base_points(base_points),
715 )
716 }
717 crate::state::RuntimeDamageDef::SpCoefficient { coef, is_physical } => {
718 let power = current_spell_power(ctx.state, ctx.buf);
719
720 deal_damage(
721 ctx,
722 DamageRequest::new(
723 DamagePayload::profiled(
724 effect.spell_id,
725 profile_spell_id,
726 coef,
727 if is_physical {
728 flags | DamageFlags::PHYSICAL
729 } else {
730 flags
731 },
732 ),
733 power,
734 )
735 .effect(effect)
736 .geometry(geometry_effect, source_position, destination)
737 .optional_base_points(base_points),
738 )
739 }
740 crate::state::RuntimeDamageDef::Weapon {
741 multiplier,
742 flat_bonus,
743 normalized,
744 is_physical,
745 hand,
746 } => {
747 let power = typed_attack_power(ctx.state, ctx.buf, crate::state::WeaponApType::None);
748 let weapon = weapon_damage_input(ctx.state, hand, multiplier, normalized);
749
750 deal_damage(
751 ctx,
752 DamageRequest::new(
753 DamagePayload::profiled(
754 effect.spell_id,
755 profile_spell_id,
756 0.0,
757 if is_physical {
758 flags | DamageFlags::PHYSICAL
759 } else {
760 flags
761 },
762 ),
763 power,
764 )
765 .effect(effect)
766 .geometry(geometry_effect, source_position, destination)
767 .weapon(weapon)
768 .base_points(flat_bonus),
769 )
770 }
771 }
772}