Skip to main content

wowlab_engine_combat/systems/damage_pipeline/
api.rs

1//! Damage request construction and public dispatch helpers.
2
3use wowlab_engine_gamedata::ResolvedGameData;
4use wowlab_engine_telemetry::{DamageEvent, DamageEventFlags, TelemetrySink};
5use wowlab_types::sim::{ActorId, EnemyIdx, SpellIdx};
6
7use super::{
8    current_spell_power,
9    dispatch::{deal_damage, deal_damage_to_target},
10    resolve_outgoing_absorbs, typed_attack_power,
11    weapon::{WeaponDamageInput, weapon_damage_input},
12};
13use crate::{
14    context::CombatCtx,
15    state::{CombatInstance, CombatProgram},
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(crate) fn damage_flags_from_local(
67    program: &CombatProgram,
68    action: crate::state::program::LocalDamageActionIdx,
69    physical: bool,
70) -> DamageFlags {
71    let mut flags = program.damage.action_at(action).damage_attribute_flags;
72
73    if physical {
74        flags |= DamageFlags::PHYSICAL;
75    }
76
77    flags
78}
79
80pub(crate) fn damage_flags_from_binding(
81    program: &CombatProgram,
82    action: crate::DamageActionId,
83    physical: bool,
84) -> DamageFlags {
85    let mut flags = program
86        .damage
87        .action_id_plan(action)
88        .map_or_else(DamageFlags::empty, |plan| plan.damage_attribute_flags);
89
90    if physical {
91        flags |= DamageFlags::PHYSICAL;
92    }
93
94    flags
95}
96
97pub(in crate::systems) fn finalize_damage(
98    ctx: &mut CombatCtx<'_>,
99    target: EnemyIdx,
100    binding: crate::state::program::DamageRequestBinding,
101    event: &DamageEvent,
102) -> bool {
103    finalize_damage_with(ctx, target, binding, event, emit_damage_telemetry);
104
105    !ctx.instance.has_runtime_error()
106}
107
108pub(in crate::systems) fn emit_damage_telemetry(
109    _source: ActorId,
110    _target: EnemyIdx,
111    event: &DamageEvent,
112    _program: &CombatProgram,
113    _state: &CombatInstance,
114    sink: &mut TelemetrySink,
115) {
116    sink.emit_damage(event);
117}
118
119pub(in crate::systems) fn finalize_damage_with(
120    ctx: &mut CombatCtx<'_>,
121    target: EnemyIdx,
122    binding: crate::state::program::DamageRequestBinding,
123    event: &DamageEvent,
124    emit: impl FnOnce(
125        ActorId,
126        EnemyIdx,
127        &DamageEvent,
128        &CombatProgram,
129        &CombatInstance,
130        &mut TelemetrySink,
131    ),
132) {
133    if !ctx.instance.is_valid_target(target) {
134        return;
135    }
136
137    let amount = event.amount;
138
139    emit(
140        ctx.source,
141        target,
142        event,
143        ctx.program,
144        ctx.instance,
145        ctx.sink,
146    );
147
148    ctx.instance.runtime.total_damage += amount;
149    ctx.instance.refresh_enemy_health(target, ctx.now);
150    let before_health = ctx
151        .instance
152        .enemy_health_fraction(ctx.program, target, ctx.now)
153        .unwrap_or(1.0);
154    let death = match ctx.instance.apply_enemy_damage(target, amount, ctx.now) {
155        Ok(death) => death,
156        Err(error) => {
157            ctx.instance.record_runtime_error(error);
158
159            return;
160        }
161    };
162    let after_health = ctx
163        .instance
164        .enemy_health_fraction(ctx.program, target, ctx.now)
165        .unwrap_or(before_health);
166    let actual_damage = ctx
167        .instance
168        .enemy_max_health(ctx.program, target)
169        .map_or(amount, |maximum| {
170            ((before_health - after_health).max(0.0) * maximum).min(amount)
171        });
172
173    crate::systems::process_actor_health_change(
174        ctx.program,
175        ctx.instance,
176        ActorId::Enemy(target),
177        before_health,
178        after_health,
179        ctx.now,
180    );
181    super::leech::apply_leech_healing(ctx, binding, event, actual_damage);
182
183    if let Some(dead) = death {
184        ctx.instance.schedule(wowlab_engine_ports::Event::Death {
185            t: dead.at,
186            target: dead.enemy,
187            scope: wowlab_engine_ports::DeathEventScope::Unscoped,
188        });
189
190        if matches!(ctx.source, ActorId::Player | ActorId::Pet(_)) {
191            crate::systems::procs::fire_kill_procs(ctx, binding.effect_action(), dead.enemy);
192        }
193    }
194}
195
196pub(crate) fn resolve_spell_impact_flags(
197    ctx: &mut CombatCtx<'_>,
198    profile_action: crate::state::program::LocalDamageActionIdx,
199    flags: DamageFlags,
200) -> Option<DamageFlags> {
201    if flags.contains(DamageFlags::REFLECTION_RESOLVED) {
202        return Some(flags);
203    }
204
205    if !ctx.program.damage.has_reflection && !ctx.program.damage.has_deflection {
206        return Some(flags | DamageFlags::REFLECTION_RESOLVED);
207    }
208
209    let school = ctx.program.damage.action_at(profile_action).school;
210
211    match crate::systems::buffs::resolve_incoming_spell_defense(
212        ctx.program,
213        ctx.instance,
214        ctx.target,
215        profile_action,
216        school,
217        ctx.rng,
218    ) {
219        crate::systems::buffs::IncomingSpellDefense::None => {
220            Some(flags | DamageFlags::REFLECTION_RESOLVED)
221        }
222        crate::systems::buffs::IncomingSpellDefense::Reflected => {
223            Some(flags | DamageFlags::REFLECTION_RESOLVED | DamageFlags::REFLECTED)
224        }
225        crate::systems::buffs::IncomingSpellDefense::Deflected => None,
226    }
227}
228bitflags::bitflags! {
229    /// Semantics of a damage impact. Empty is magical, player-sourced, direct, and crit-capable.
230    #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
231    pub struct DamageFlags: u16 {
232        const PHYSICAL = 1 << 0;
233        /// Companion-owned impact; guardians carry it for pet scaling, telemetry, and proc suppression.
234        const PET = 1 << 1;
235        const PERIODIC = 1 << 2;
236        const NO_CRIT = 1 << 3;
237        /// Temporary-guardian subtype, set with `PET` so target modifiers can distinguish guardians.
238        const GUARDIAN = 1 << 4;
239        const IGNORE_PLAYER_MULTIPLIERS = 1 << 5;
240        const IGNORE_TARGET_MULTIPLIERS = 1 << 6;
241        const IGNORE_POSITIVE_TARGET_MULTIPLIERS = 1 << 7;
242        const PROC = 1 << 8;
243        /// Attribute telemetry to the action while retaining the payload for runtime mechanics.
244        const REPORT_AS_PROFILE = 1 << 9;
245        /// Area damage, including DBC spells explicitly tagged to use area-effect semantics.
246        const AOE = 1 << 10;
247        /// The target's reflect/deflect decision has already been resolved for this impact.
248        const REFLECTION_RESOLVED = 1 << 11;
249        /// Re-execute this impact against its caster without another reflection check.
250        const REFLECTED = 1 << 12;
251        /// Preserve crit chance while suppressing bonuses beyond the baseline 2x crit damage.
252        const IGNORE_CRIT_DAMAGE_BONUSES = 1 << 13;
253    }
254}
255
256/// Spell identity, scaling coefficient, and semantic flags for one power-scaled hit.
257#[derive(Clone, Copy, Debug)]
258pub struct DamagePayload {
259    pub spell_id: u32,
260    pub profile_spell_id: Option<u32>,
261    pub coef: f64,
262    pub flags: DamageFlags,
263}
264
265impl DamagePayload {
266    #[must_use]
267    pub const fn new(spell_id: u32, coef: f64, flags: DamageFlags) -> Self {
268        Self {
269            spell_id,
270            profile_spell_id: None,
271            coef,
272            flags,
273        }
274    }
275
276    #[must_use]
277    pub const fn profiled(
278        spell_id: u32,
279        profile_spell_id: u32,
280        coef: f64,
281        flags: DamageFlags,
282    ) -> Self {
283        Self {
284            spell_id,
285            profile_spell_id: Some(profile_spell_id),
286            coef,
287            flags,
288        }
289    }
290
291    pub(super) const fn resolved_profile_spell_id(self) -> u32 {
292        match self.profile_spell_id {
293            Some(profile_spell_id) => profile_spell_id,
294            None => self.spell_id,
295        }
296    }
297}
298
299#[inline]
300pub(in crate::systems) const fn reported_damage_spell_id(request: &DamageRequest) -> u32 {
301    if request.flags.contains(DamageFlags::REPORT_AS_PROFILE) {
302        request.profile_spell_id
303    } else {
304        request.effect.spell_id
305    }
306}
307
308#[derive(Clone, Copy, Debug)]
309pub(in crate::systems) struct DamageRequest {
310    pub effect: crate::state::DamageEffectRef,
311    pub geometry_effect: crate::state::DamageEffectRef,
312    pub source_position: Option<wowlab_types::sim::SpatialTransform>,
313    pub destination: Option<wowlab_types::sim::SpatialTransform>,
314    pub profile_spell_id: u32,
315    pub base_points: Option<f64>,
316    pub coef: f64,
317    pub power: f64,
318    pub weapon: WeaponDamageInput,
319    pub flags: DamageFlags,
320}
321
322impl DamageRequest {
323    const fn new(payload: DamagePayload, power: f64) -> Self {
324        let effect = crate::state::DamageEffectRef::new(payload.spell_id, 1);
325
326        Self {
327            effect,
328            geometry_effect: effect,
329            source_position: None,
330            destination: None,
331            profile_spell_id: payload.resolved_profile_spell_id(),
332            base_points: None,
333            coef: payload.coef,
334            power,
335            weapon: WeaponDamageInput::none(),
336            flags: payload.flags,
337        }
338    }
339
340    const fn profile(mut self, spell_id: u32) -> Self {
341        self.profile_spell_id = spell_id;
342
343        self
344    }
345
346    const fn effect(mut self, effect: crate::state::DamageEffectRef) -> Self {
347        self.effect = effect;
348        self.geometry_effect = effect;
349
350        self
351    }
352
353    const fn geometry(
354        mut self,
355        effect: crate::state::DamageEffectRef,
356        source_position: Option<wowlab_types::sim::SpatialTransform>,
357        destination: Option<wowlab_types::sim::SpatialTransform>,
358    ) -> Self {
359        self.geometry_effect = effect;
360        self.source_position = source_position;
361        self.destination = destination;
362
363        self
364    }
365
366    const fn weapon(mut self, weapon: WeaponDamageInput) -> Self {
367        self.weapon = weapon;
368
369        self
370    }
371
372    const fn base_points(mut self, base_points: f64) -> Self {
373        self.base_points = Some(base_points);
374
375        self
376    }
377
378    const fn optional_base_points(mut self, base_points: Option<f64>) -> Self {
379        self.base_points = base_points;
380
381        self
382    }
383}
384
385/// Executable damage input after all external identities have been resolved once.
386#[derive(Clone, Copy, Debug)]
387pub(in crate::systems) struct BoundDamageRequest {
388    pub binding: crate::state::program::DamageRequestBinding,
389    input: DamageRequest,
390}
391
392impl BoundDamageRequest {
393    pub(super) const fn new(
394        input: DamageRequest,
395        binding: crate::state::program::DamageRequestBinding,
396    ) -> Self {
397        Self { binding, input }
398    }
399}
400
401impl std::ops::Deref for BoundDamageRequest {
402    type Target = DamageRequest;
403
404    fn deref(&self) -> &Self::Target {
405        &self.input
406    }
407}
408
409impl std::ops::DerefMut for BoundDamageRequest {
410    fn deref_mut(&mut self) -> &mut Self::Target {
411        &mut self.input
412    }
413}
414
415/// Emit already-resolved residual damage without a second multiplier pass.
416pub(crate) fn deal_residual_damage(
417    ctx: &mut CombatCtx<'_>,
418    binding: crate::state::program::DamageRequestBinding,
419    spell_id: u32,
420    amount: f64,
421) {
422    deal_bound_resolved_damage(ctx, binding, spell_id, amount, DamageFlags::PERIODIC);
423}
424
425/// Emit resolved damage without reapplying stats, buffs, mastery, mitigation, or crit.
426pub(crate) fn deal_bound_resolved_damage(
427    ctx: &mut CombatCtx<'_>,
428    binding: crate::state::program::DamageRequestBinding,
429    spell_id: u32,
430    amount: f64,
431    flags: DamageFlags,
432) {
433    if amount <= 0.0 {
434        return;
435    }
436
437    if !ctx.instance.is_valid_target(ctx.target) {
438        return;
439    }
440
441    let Some(scope) = ctx.program.telemetry_scope(ctx.source, ctx.target) else {
442        return;
443    };
444
445    let resolved = resolve_outgoing_absorbs(
446        ctx.instance,
447        ctx.target,
448        ctx.program.damage.action_at(binding.effect_action()).school,
449        amount,
450        wowlab_types::combat::HitResult::Hit,
451    );
452
453    if resolved.amount > f64::EPSILON
454        && !finalize_damage(
455            ctx,
456            ctx.target,
457            binding,
458            &DamageEvent::new(
459                wowlab_engine_telemetry::DamageObservation {
460                    spell_id,
461                    amount: resolved.amount,
462                    flags: DamageEventFlags::default()
463                        .with_periodic(flags.contains(DamageFlags::PERIODIC))
464                        .with_pet(flags.contains(DamageFlags::PET)),
465                },
466                ctx.now.as_millis(),
467                scope,
468            ),
469        )
470    {
471        return;
472    }
473
474    fire_impact_procs(
475        ctx,
476        crate::ImpactEvent {
477            kind: crate::state::ProcEventKind::Spell,
478            phase: crate::state::ProcPhaseMask::HIT,
479            hit_mask: resolved.hit_mask,
480            spell_id,
481            action: Some(binding.effect_action()),
482            amount: resolved.amount,
483            is_periodic: flags.contains(DamageFlags::PERIODIC),
484            is_physical: ctx.program.damage.action_at(binding.effect_action()).school
485                == wowlab_types::combat::DamageSchool::Physical,
486            swing_hand: None,
487            swing_speed_ms: 0,
488            source: ctx.source,
489            target: ctx.target,
490            provenance: crate::state::ProcProvenance {
491                trigger_spell_id: spell_id,
492                policy: crate::ProcPolicy::from_attributes(
493                    ctx.program
494                        .damage
495                        .action_at(binding.effect_action())
496                        .attributes
497                        .as_ref(),
498                ),
499                kind: if flags.contains(DamageFlags::PROC) {
500                    crate::state::ProcSourceKind::Proc
501                } else {
502                    crate::state::ProcSourceKind::Action
503                },
504            },
505        },
506    );
507}
508
509/// Deal attack-power-based damage with the default main-hand weapon-AP composite.
510pub(crate) fn deal_bound_damage_ap(
511    ctx: &mut CombatCtx<'_>,
512    binding: crate::state::program::DamageRequestBinding,
513    payload: DamagePayload,
514) -> (f64, bool) {
515    deal_damage_ap_typed(ctx, binding, payload, crate::state::WeaponApType::MainHand)
516}
517
518pub(crate) fn deal_damage_ap_at(
519    ctx: &mut CombatCtx<'_>,
520    target: EnemyIdx,
521    binding: crate::state::program::DamageRequestBinding,
522    payload: DamagePayload,
523) -> (f64, bool) {
524    let power = typed_attack_power(
525        ctx.program,
526        ctx.instance,
527        crate::state::WeaponApType::MainHand,
528    );
529
530    deal_damage_to_target(
531        ctx,
532        target,
533        BoundDamageRequest::new(DamageRequest::new(payload, power), binding),
534    )
535}
536
537/// Deal attack-power-based damage with an explicit weapon-AP composite.
538pub(super) fn deal_damage_ap_typed(
539    ctx: &mut CombatCtx<'_>,
540    binding: crate::state::program::DamageRequestBinding,
541    payload: DamagePayload,
542    ap_type: crate::state::WeaponApType,
543) -> (f64, bool) {
544    let power = typed_attack_power(ctx.program, ctx.instance, ap_type);
545
546    deal_damage(
547        ctx,
548        BoundDamageRequest::new(DamageRequest::new(payload, power), binding),
549    )
550}
551
552pub(crate) fn deal_effect_damage_ap(
553    ctx: &mut CombatCtx<'_>,
554    binding: crate::state::program::DamageRequestBinding,
555    effect: crate::state::DamageEffectRef,
556    coef: f64,
557    flags: DamageFlags,
558) -> (f64, bool) {
559    let power = typed_attack_power(
560        ctx.program,
561        ctx.instance,
562        crate::state::WeaponApType::MainHand,
563    );
564
565    deal_damage(
566        ctx,
567        BoundDamageRequest::new(
568            DamageRequest::new(DamagePayload::new(effect.spell_id, coef, flags), power)
569                .effect(effect),
570            binding,
571        ),
572    )
573}
574
575pub(crate) fn deal_bound_effect_damage_ap_with_geometry(
576    ctx: &mut CombatCtx<'_>,
577    binding: crate::state::program::DamageRequestBinding,
578    effect: crate::state::DamageEffectRef,
579    geometry_effect: crate::state::DamageEffectRef,
580    coef: f64,
581    flags: DamageFlags,
582) -> (f64, bool) {
583    let destination = ctx.instance.actor_transform(ActorId::Enemy(ctx.target));
584    let power = typed_attack_power(
585        ctx.program,
586        ctx.instance,
587        crate::state::WeaponApType::MainHand,
588    );
589
590    deal_damage(
591        ctx,
592        BoundDamageRequest::new(
593            DamageRequest::new(DamagePayload::new(effect.spell_id, coef, flags), power)
594                .effect(effect)
595                .geometry(geometry_effect, None, destination),
596            binding,
597        ),
598    )
599}
600
601pub(crate) fn deal_effect_damage_sp_with_geometry(
602    ctx: &mut CombatCtx<'_>,
603    binding: crate::state::program::DamageRequestBinding,
604    effect: crate::state::DamageEffectRef,
605    geometry_effect: crate::state::DamageEffectRef,
606    coef: f64,
607    flags: DamageFlags,
608) -> (f64, bool) {
609    let destination = ctx.instance.actor_transform(ActorId::Enemy(ctx.target));
610
611    deal_damage(
612        ctx,
613        BoundDamageRequest::new(
614            DamageRequest::new(
615                DamagePayload::new(effect.spell_id, coef, flags),
616                current_spell_power(ctx.program, ctx.instance),
617            )
618            .effect(effect)
619            .geometry(geometry_effect, None, destination),
620            binding,
621        ),
622    )
623}
624
625pub(crate) fn deal_effect_damage_sp(
626    ctx: &mut CombatCtx<'_>,
627    binding: crate::state::program::DamageRequestBinding,
628    effect: crate::state::DamageEffectRef,
629    coef: f64,
630    flags: DamageFlags,
631) -> (f64, bool) {
632    deal_damage(
633        ctx,
634        BoundDamageRequest::new(
635            DamageRequest::new(
636                DamagePayload::new(effect.spell_id, coef, flags),
637                current_spell_power(ctx.program, ctx.instance),
638            )
639            .effect(effect),
640            binding,
641        ),
642    )
643}
644
645pub(in crate::systems) fn deal_damage_ap_with_weapon(
646    ctx: &mut CombatCtx<'_>,
647    binding: crate::state::program::DamageRequestBinding,
648    payload: DamagePayload,
649    weapon: WeaponDamageInput,
650) -> (f64, bool) {
651    let ap_type = if weapon.max > 0.0 {
652        crate::state::WeaponApType::None
653    } else {
654        crate::state::WeaponApType::MainHand
655    };
656    let power = typed_attack_power(ctx.program, ctx.instance, ap_type);
657
658    deal_damage(
659        ctx,
660        BoundDamageRequest::new(DamageRequest::new(payload, power).weapon(weapon), binding),
661    )
662}
663
664pub(crate) fn deal_bound_damage_sp(
665    ctx: &mut CombatCtx<'_>,
666    binding: crate::state::program::DamageRequestBinding,
667    payload: DamagePayload,
668) -> (f64, bool) {
669    let power = current_spell_power(ctx.program, ctx.instance);
670
671    deal_damage(
672        ctx,
673        BoundDamageRequest::new(DamageRequest::new(payload, power), binding),
674    )
675}
676
677pub(crate) fn deal_damage_sp_at(
678    ctx: &mut CombatCtx<'_>,
679    target: EnemyIdx,
680    binding: crate::state::program::DamageRequestBinding,
681    payload: DamagePayload,
682) -> (f64, bool) {
683    let power = current_spell_power(ctx.program, ctx.instance);
684
685    deal_damage_to_target(
686        ctx,
687        target,
688        BoundDamageRequest::new(DamageRequest::new(payload, power), binding),
689    )
690}
691
692/// Deal damage from an already-resolved base amount.
693pub(crate) fn deal_damage_base(
694    ctx: &mut CombatCtx<'_>,
695    binding: crate::state::program::DamageRequestBinding,
696    spell_id: u32,
697    amount: f64,
698    flags: DamageFlags,
699) -> (f64, bool) {
700    deal_damage(
701        ctx,
702        BoundDamageRequest::new(
703            DamageRequest::new(DamagePayload::new(spell_id, 0.0, flags), 0.0).base_points(amount),
704            binding,
705        ),
706    )
707}
708
709/// Deal an already-resolved amount while retaining its exact DBC effect coordinate.
710pub(crate) fn deal_effect_damage_base(
711    ctx: &mut CombatCtx<'_>,
712    binding: crate::state::program::DamageRequestBinding,
713    effect: crate::state::DamageEffectRef,
714    profile_spell_id: u32,
715    amount: f64,
716    flags: DamageFlags,
717) -> (f64, bool) {
718    deal_damage(
719        ctx,
720        BoundDamageRequest::new(
721            DamageRequest::new(DamagePayload::new(effect.spell_id, 0.0, flags), 0.0)
722                .profile(profile_spell_id)
723                .effect(effect)
724                .base_points(amount),
725            binding,
726        ),
727    )
728}
729
730pub(crate) fn deal_damage_def(
731    ctx: &mut CombatCtx<'_>,
732    binding: crate::state::program::DamageRequestBinding,
733    spell_id: u32,
734    def: crate::state::RuntimeDamageDef,
735    flags: DamageFlags,
736) {
737    deal_bound_profiled_damage_def(
738        ctx,
739        ProfiledDamageDef {
740            binding,
741            effect: crate::state::DamageEffectRef::new(spell_id, 1),
742            geometry_effect: crate::state::DamageEffectRef::new(spell_id, 1),
743            profile_spell_id: spell_id,
744            base_points: None,
745            def,
746            flags,
747            source_position: None,
748            destination: None,
749        },
750    );
751}
752
753pub(crate) fn deal_bound_profiled_damage_def(
754    ctx: &mut CombatCtx<'_>,
755    request: ProfiledDamageDef,
756) -> (f64, bool) {
757    deal_profiled_damage_def_at(ctx, &request)
758}
759
760#[derive(Clone, Copy)]
761pub(crate) struct ProfiledDamageDef {
762    pub(crate) binding: crate::state::program::DamageRequestBinding,
763    pub(crate) effect: crate::state::DamageEffectRef,
764    pub(crate) geometry_effect: crate::state::DamageEffectRef,
765    pub(crate) profile_spell_id: u32,
766    pub(crate) base_points: Option<f64>,
767    pub(crate) def: crate::state::RuntimeDamageDef,
768    pub(crate) flags: DamageFlags,
769    pub(crate) source_position: Option<wowlab_types::sim::SpatialTransform>,
770    pub(crate) destination: Option<wowlab_types::sim::SpatialTransform>,
771}
772
773pub(crate) fn deal_profiled_damage_def_at(
774    ctx: &mut CombatCtx<'_>,
775    request: &ProfiledDamageDef,
776) -> (f64, bool) {
777    let ProfiledDamageDef {
778        binding,
779        effect,
780        mut geometry_effect,
781        profile_spell_id,
782        base_points,
783        def,
784        flags,
785        source_position,
786        destination,
787    } = *request;
788
789    if geometry_effect == effect {
790        geometry_effect = ctx.program.damage.effect_ref(binding.geometry_effect);
791    }
792
793    match def {
794        crate::state::RuntimeDamageDef::None => {
795            tracing::trace!(
796                spell_id = effect.spell_id,
797                is_periodic = flags.contains(DamageFlags::PERIODIC),
798                "damage dispatch with no damage defined"
799            );
800
801            (0.0, false)
802        }
803        crate::state::RuntimeDamageDef::Flat(amount) => deal_damage(
804            ctx,
805            BoundDamageRequest::new(
806                DamageRequest::new(
807                    DamagePayload::profiled(effect.spell_id, profile_spell_id, 0.0, flags),
808                    0.0,
809                )
810                .effect(effect)
811                .geometry(geometry_effect, source_position, destination)
812                .base_points(amount),
813                binding,
814            ),
815        ),
816        crate::state::RuntimeDamageDef::ApCoefficient {
817            coef,
818            is_physical,
819            ap_type,
820        } => {
821            let power = typed_attack_power(ctx.program, ctx.instance, ap_type);
822
823            deal_damage(
824                ctx,
825                BoundDamageRequest::new(
826                    DamageRequest::new(
827                        DamagePayload::profiled(
828                            effect.spell_id,
829                            profile_spell_id,
830                            coef,
831                            if is_physical {
832                                flags | DamageFlags::PHYSICAL
833                            } else {
834                                flags
835                            },
836                        ),
837                        power,
838                    )
839                    .effect(effect)
840                    .geometry(geometry_effect, source_position, destination)
841                    .optional_base_points(base_points),
842                    binding,
843                ),
844            )
845        }
846        crate::state::RuntimeDamageDef::SpCoefficient { coef, is_physical } => {
847            let power = current_spell_power(ctx.program, ctx.instance);
848
849            deal_damage(
850                ctx,
851                BoundDamageRequest::new(
852                    DamageRequest::new(
853                        DamagePayload::profiled(
854                            effect.spell_id,
855                            profile_spell_id,
856                            coef,
857                            if is_physical {
858                                flags | DamageFlags::PHYSICAL
859                            } else {
860                                flags
861                            },
862                        ),
863                        power,
864                    )
865                    .effect(effect)
866                    .geometry(geometry_effect, source_position, destination)
867                    .optional_base_points(base_points),
868                    binding,
869                ),
870            )
871        }
872        crate::state::RuntimeDamageDef::Weapon {
873            multiplier,
874            flat_bonus,
875            normalized,
876            is_physical,
877            hand,
878        } => {
879            let power =
880                typed_attack_power(ctx.program, ctx.instance, crate::state::WeaponApType::None);
881            let weapon =
882                weapon_damage_input(ctx.program, ctx.instance, hand, multiplier, normalized);
883
884            deal_damage(
885                ctx,
886                BoundDamageRequest::new(
887                    DamageRequest::new(
888                        DamagePayload::profiled(
889                            effect.spell_id,
890                            profile_spell_id,
891                            0.0,
892                            if is_physical {
893                                flags | DamageFlags::PHYSICAL
894                            } else {
895                                flags
896                            },
897                        ),
898                        power,
899                    )
900                    .effect(effect)
901                    .geometry(geometry_effect, source_position, destination)
902                    .weapon(weapon)
903                    .base_points(flat_bonus),
904                    binding,
905                ),
906            )
907        }
908    }
909}