Skip to main content

wowlab_engine_combat/systems/damage_pipeline/
api.rs

1//! Damage request construction and public dispatch helpers.
2
3use 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    /// Semantics of a damage impact. Empty is magical, player-sourced, direct, and crit-capable.
180    #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
181    pub struct DamageFlags: u16 {
182        const PHYSICAL = 1 << 0;
183        /// Companion-owned impact; guardians carry it for pet scaling, telemetry, and proc suppression.
184        const PET = 1 << 1;
185        const PERIODIC = 1 << 2;
186        const NO_CRIT = 1 << 3;
187        /// Temporary-guardian subtype, set with `PET` so target modifiers can distinguish guardians.
188        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        /// Attribute telemetry to the action while retaining the payload for runtime mechanics.
194        const REPORT_AS_PROFILE = 1 << 9;
195        /// Area damage, including DBC spells explicitly tagged to use area-effect semantics.
196        const AOE = 1 << 10;
197        /// The target's reflect/deflect decision has already been resolved for this impact.
198        const REFLECTION_RESOLVED = 1 << 11;
199        /// Re-execute this impact against its caster without another reflection check.
200        const REFLECTED = 1 << 12;
201        /// Preserve crit chance while suppressing bonuses beyond the baseline 2x crit damage.
202        const IGNORE_CRIT_DAMAGE_BONUSES = 1 << 13;
203    }
204}
205
206/// Spell identity, scaling coefficient, and semantic flags for one power-scaled hit.
207#[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
335/// Emit already-resolved residual damage without a second multiplier pass.
336pub(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
340/// Emit resolved damage without reapplying stats, buffs, mastery, mitigation, or crit.
341pub(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
415/// Deal attack-power-based damage with the default main-hand weapon-AP composite.
416pub 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
430/// Deal attack-power-based damage with an explicit weapon-AP composite.
431pub(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
540/// Deal damage from an already-resolved base amount.
541pub(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
553/// Deal an already-resolved amount while retaining its exact DBC effect coordinate.
554pub(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}