Skip to main content

wowlab_engine_combat/systems/damage_pipeline/
setup.rs

1use wowlab_engine_domain::rotation::BoundDenseBuffer;
2use wowlab_types::constants::HUNDRED;
3
4#[cfg(test)]
5use super::DamageRequest;
6use super::{BoundDamageRequest, DamageFlags};
7#[cfg(all(
8    feature = "rotation-optimizations",
9    any(test, feature = "test-support")
10))]
11use crate::state::DamageProjectionSheetStats;
12#[cfg(test)]
13use crate::state::DamageSource;
14#[cfg(feature = "rotation-optimizations")]
15use crate::state::{DamageBuffProjection, DamageProjectionKey};
16use crate::{
17    context::{ActorView, CombatView},
18    state::{BuffEffect, BuffTotals, CombatInstance},
19    systems::{
20        aoe::{AoeConfig, read_spell_aoe_config},
21        buffs::{
22            active_spell_school_override, actor_dynamic_damage_mult, actor_static_buff_totals,
23        },
24        mastery::MasteryCtx,
25    },
26};
27
28mod armor;
29mod passive_damage;
30pub(super) mod spell_modifiers;
31
32use passive_damage::{PassiveDamageAdjustments, passive_damage_adjustments};
33use spell_modifiers::{
34    SpellModifierEffect, SpellModifierProfile, SpellModifierQuery, spell_scoped_mods_for,
35};
36
37#[derive(Clone, Copy, Debug, Default)]
38pub(crate) struct DamageSnapshot {
39    pub ap: f64,
40    pub sp: f64,
41    pub crit_pct: f64,
42    pub vers_pct: f64,
43    pub mastery_mult: f64,
44    pub damage_mult: f64,
45}
46
47#[derive(Clone, Copy, Debug)]
48pub(super) struct DamageSetup {
49    pub(super) base: f64,
50    pub(super) armor: f64,
51    pub(super) armor_k: f64,
52    pub(super) combined_mult: f64,
53    pub(super) target_damage_mult: f64,
54    pub(super) power_coefficient_add: f64,
55    pub(super) auto_combined_mult: f64,
56    pub(super) auto_attack_buff_mult: f64,
57    pub(super) mastery_mult: f64,
58    pub(super) crit_pct_total: f64,
59    pub(super) crit_multiplier: f64,
60    pub(super) vers_pct_total: f64,
61    pub(super) school: wowlab_types::combat::DamageSchool,
62    pub(super) is_physical: bool,
63    pub(super) aoe: AoeConfig,
64}
65
66#[derive(Clone, Copy)]
67pub(super) struct SnapshotDamageContext<'a> {
68    program: &'a crate::state::CombatProgram,
69    instance: &'a CombatInstance,
70    source: wowlab_types::sim::ActorId,
71    target: wowlab_types::sim::EnemyIdx,
72    now: wowlab_types::sim::SimTime,
73}
74
75#[derive(Clone, Copy)]
76pub(super) struct SnapshotDamageEvent {
77    pub(super) source: wowlab_types::sim::ActorId,
78    pub(super) target: wowlab_types::sim::EnemyIdx,
79    pub(super) now: wowlab_types::sim::SimTime,
80}
81
82impl<'a> SnapshotDamageContext<'a> {
83    pub(super) const fn new(
84        program: &'a crate::state::CombatProgram,
85        instance: &'a CombatInstance,
86        event: SnapshotDamageEvent,
87    ) -> Self {
88        Self {
89            program,
90            instance,
91            source: event.source,
92            target: event.target,
93            now: event.now,
94        }
95    }
96}
97
98impl DamageSetup {
99    pub(super) fn from_snapshot_for(
100        context: SnapshotDamageContext<'_>,
101        binding: crate::state::program::DamageRequestBinding,
102        effect: crate::state::DamageEffectRef,
103        flags: DamageFlags,
104        snap: &DamageSnapshot,
105    ) -> Self {
106        let spell_id = effect.spell_id;
107        let SnapshotDamageContext {
108            program,
109            instance: state,
110            source,
111            target,
112            now,
113        } = context;
114        let base = program
115            .damage
116            .action_at(binding.profile_action)
117            .profile_base_points;
118        let passive = program.damage.action_at(binding.profile_action).passive;
119        let passive_mult = passive.periodic_damage_mult;
120        let effect_binding = binding.effect;
121        let profile_action = binding.profile_action;
122        let mods = spell_scoped_mods_for(
123            program,
124            state,
125            source,
126            SpellModifierQuery::new(
127                SpellModifierProfile::new(spell_id, profile_action),
128                SpellModifierEffect::new(effect, effect_binding),
129                flags,
130            ),
131            Some(target),
132            now,
133        );
134        let ignore_player = flags.contains(DamageFlags::IGNORE_PLAYER_MULTIPLIERS);
135        let ignore_target = flags.intersects(
136            DamageFlags::IGNORE_TARGET_MULTIPLIERS
137                | DamageFlags::IGNORE_POSITIVE_TARGET_MULTIPLIERS,
138        );
139        let target_mult = if ignore_target {
140            1.0
141        } else {
142            mods.target_damage_mult
143        };
144        let view = &CombatView::new(program, state);
145        let (school, mitigation_flags) = effective_school_and_flags(
146            view,
147            binding.effect_action(),
148            binding.profile_action,
149            binding.effect_action(),
150            flags,
151        );
152
153        Self {
154            base,
155            armor: if flags.contains(DamageFlags::IGNORE_TARGET_MULTIPLIERS) {
156                0.0
157            } else {
158                armor::for_target(
159                    crate::HookView {
160                        program,
161                        instance: state,
162                        target: Some(target),
163                        now,
164                    },
165                    source,
166                    mitigation_flags | DamageFlags::PERIODIC,
167                )
168            },
169            armor_k: program.enemy_definition(target).map_or(
170                0.0,
171                wowlab_engine_domain::encounter::EnemyActorDefinition::armor_constant,
172            ),
173            combined_mult: if ignore_player { 1.0 } else { snap.damage_mult }
174                * passive_mult
175                * target_mult,
176            target_damage_mult: target_mult,
177            power_coefficient_add: 0.0,
178            auto_combined_mult: if ignore_player { 1.0 } else { snap.damage_mult }
179                * passive_mult
180                * target_mult,
181            auto_attack_buff_mult: 1.0,
182            mastery_mult: snap.mastery_mult,
183            crit_pct_total: snap.crit_pct
184                + passive.crit_chance_bonus
185                + crit_chance_versus_target_health(program, state, source, target, now)
186                + guaranteed_crit_pct(program, state, binding.profile_action),
187            crit_multiplier: {
188                let crit_scaled = crit_chance_scaled_mults(
189                    program,
190                    state,
191                    binding.profile_action,
192                    school,
193                    snap.crit_pct,
194                );
195
196                resolved_crit_multiplier(
197                    flags,
198                    wowlab_engine_domain::stats::crit_multiplier(&program.config.base_stats.stats)
199                        * crit_scaled.total,
200                    0.0,
201                    passive.crit_damage_bonus,
202                    mods.crit_damage_mult * crit_scaled.bonus,
203                )
204            },
205            vers_pct_total: if ignore_player { 0.0 } else { snap.vers_pct },
206            school,
207            is_physical: mitigation_flags.contains(DamageFlags::PHYSICAL),
208            aoe: AoeConfig::default(),
209        }
210    }
211
212    #[cfg(test)]
213    pub(super) fn from_snapshot_fixture(
214        view: &CombatView<'_>,
215        spell_id: u32,
216        flags: DamageFlags,
217        snap: &DamageSnapshot,
218    ) -> Self {
219        let effect = crate::state::DamageEffectRef::new(spell_id, 1);
220        let binding = view
221            .program
222            .damage
223            .diagnostic_bind_request(effect, effect, spell_id)
224            .expect("snapshot fixture identity belongs to its finalized program");
225
226        Self::from_snapshot_for(
227            SnapshotDamageContext::new(
228                view.program,
229                view.instance,
230                SnapshotDamageEvent {
231                    source: wowlab_types::sim::ActorId::Player,
232                    target: view
233                        .instance
234                        .current_target()
235                        .expect("snapshot fixture requires a selected target"),
236                    now: wowlab_types::sim::SimTime::ZERO,
237                },
238            ),
239            binding,
240            effect,
241            flags,
242            snap,
243        )
244    }
245}
246
247/// The two crit multipliers a passive can scale by the caster's own critical strike chance.
248#[derive(Clone, Copy, Debug)]
249pub(super) struct CritChanceScaledMults {
250    /// Scales the whole crit multiplier, including its `1.0` base.
251    total: f64,
252    /// Scales only the bonus above `1.0`.
253    bonus: f64,
254}
255
256/// Reads the crit-damage terms the game states as a share of the caster's own crit chance.
257fn crit_chance_scaled_mults(
258    program: &crate::state::CombatProgram,
259    _state: &CombatInstance,
260    action: crate::state::program::LocalDamageActionIdx,
261    school: wowlab_types::combat::DamageSchool,
262    player_crit_pct: f64,
263) -> CritChanceScaledMults {
264    let crit_chance = player_crit_pct / HUNDRED;
265    let hit_schools = wowlab_engine_domain::dbc::SpellSchoolMask::from(school);
266    let total = program
267        .damage
268        .crit_damage_per_crit_chance()
269        .filter(|(schools, _)| hit_schools.intersects(*schools))
270        .fold(1.0, |mult, (_, percent)| {
271            mult * (1.0 + percent / HUNDRED * crit_chance)
272        });
273
274    CritChanceScaledMults {
275        total,
276        bonus: 1.0
277            + program
278                .damage
279                .action_at(action)
280                .passive
281                .crit_bonus_per_crit_chance
282                / HUNDRED
283                * crit_chance,
284    }
285}
286
287const BASE_CRIT_MULTIPLIER: f64 = 2.0;
288
289fn resolved_crit_multiplier(
290    flags: DamageFlags,
291    effective_base_crit: f64,
292    mastery_bonus_pct: f64,
293    passive_bonus_pct: f64,
294    buff_bonus_mult: f64,
295) -> f64 {
296    if flags.contains(DamageFlags::IGNORE_CRIT_DAMAGE_BONUSES) {
297        return BASE_CRIT_MULTIPLIER;
298    }
299
300    let bonus_mult =
301        (1.0 + mastery_bonus_pct / HUNDRED) * (1.0 + passive_bonus_pct / HUNDRED) * buff_bonus_mult;
302
303    1.0 + (effective_base_crit - 1.0) * bonus_mult
304}
305
306pub(crate) const GUARANTEED_CRIT_PCT: f64 = HUNDRED;
307
308fn guaranteed_crit_pct(
309    program: &crate::state::CombatProgram,
310    _state: &CombatInstance,
311    action: crate::state::program::LocalDamageActionIdx,
312) -> f64 {
313    let guaranteed = program.damage.action_at(action).guaranteed_crit;
314
315    if guaranteed { GUARANTEED_CRIT_PCT } else { 0.0 }
316}
317
318pub(crate) fn dynamic_base_powers(
319    program: &crate::state::CombatProgram,
320    state: &CombatInstance,
321    totals: &BuffTotals,
322) -> (f64, f64) {
323    let p = state.buffer.player();
324    let flat = totals.primary_stat * program.config.base_stats.stats.primary_stat_mult;
325    let bonus_stat = program
326        .config
327        .base_stats
328        .primary_attribute
329        .map_or(0.0, |attribute| {
330            positive_non_create_bonus(&program.config.base_stats.stats, attribute)
331                * bonus_stat_multiplier(totals, attribute, true)
332        });
333    let pct_mult = 1.0 + totals.primary_stat_pct / HUNDRED;
334    let base_attack_power = (p.attack_power + flat + bonus_stat) * pct_mult;
335    let base_spell_power = (p.spell_power + flat + bonus_stat) * pct_mult;
336    let attack_power = totals
337        .override_attack_power_by_spell_power_pct
338        .map_or(base_attack_power, |percent| {
339            base_spell_power * percent / HUNDRED
340        });
341    let spell_power = totals
342        .override_spell_power_by_attack_power_pct
343        .map_or(base_spell_power, |percent| {
344            base_attack_power * percent / HUNDRED
345        });
346
347    (attack_power, spell_power)
348}
349
350#[cfg(feature = "rotation-optimizations")]
351pub(crate) fn static_damage_buff_projection(
352    program: &crate::state::CombatProgram,
353    state: &CombatInstance,
354    actor: wowlab_types::sim::ActorId,
355) -> DamageBuffProjection {
356    let actor_only = actor != wowlab_types::sim::ActorId::Player;
357    let revision = state.actor_aura_revision(actor).unwrap_or(0);
358    let owner_revision = state
359        .actor_aura_revision(wowlab_types::sim::ActorId::Player)
360        .unwrap_or(0);
361    #[cfg(any(test, feature = "test-support"))]
362    let player = state.buffer.player();
363    #[cfg(any(test, feature = "test-support"))]
364    let key = DamageProjectionKey::new(
365        revision,
366        owner_revision,
367        DamageProjectionSheetStats {
368            attack_power: player.attack_power,
369            spell_power: player.spell_power,
370            crit: player.crit,
371            mastery: player.mastery,
372            versatility: player.versatility,
373        },
374    );
375    #[cfg(not(any(test, feature = "test-support")))]
376    let key = DamageProjectionKey::new(revision, owner_revision);
377
378    if let Some(projection) = state.cached_damage_buff_projection(actor, key) {
379        return projection;
380    }
381
382    #[cfg(not(any(test, feature = "test-support")))]
383    let player = state.buffer.player();
384    let totals = actor_static_buff_totals(ActorView::new(program, state, actor), actor_only);
385    let (attack_power, spell_power) = if actor_only {
386        (0.0, 0.0)
387    } else {
388        dynamic_base_powers(program, state, &totals)
389    };
390    let projection = DamageBuffProjection {
391        attack_power,
392        spell_power,
393        attack_power_pct: totals.attack_power_pct,
394        crit_pct: totals.crit_pct + if actor_only { 0.0 } else { player.crit },
395        mastery: totals.mastery_pct + if actor_only { 0.0 } else { player.mastery },
396        vers_pct: totals.vers_pct + if actor_only { 0.0 } else { player.versatility },
397        damage_mult: totals.damage_mult,
398        pet_damage_mult: totals.pet_damage_mult,
399        auto_attack_damage_mult: totals.auto_attack_damage_mult,
400        auto_attack_crit_pct: totals.auto_attack_crit_pct,
401        school_damage_mult: totals.school_damage_mult,
402    };
403
404    state.cache_damage_buff_projection(actor, key, projection);
405
406    projection
407}
408
409fn positive_non_create_bonus(
410    stats: &wowlab_engine_ports::CombatStats,
411    attribute: wowlab_types::game::Attribute,
412) -> f64 {
413    match attribute {
414        wowlab_types::game::Attribute::Strength => stats.positive_strength_bonus,
415        wowlab_types::game::Attribute::Agility => stats.positive_agility_bonus,
416        wowlab_types::game::Attribute::Stamina => stats.positive_stamina_bonus,
417        wowlab_types::game::Attribute::Intellect => stats.positive_intellect_bonus,
418    }
419    .max(0.0)
420}
421
422fn bonus_stat_multiplier(
423    totals: &BuffTotals,
424    attribute: wowlab_types::game::Attribute,
425    is_primary: bool,
426) -> f64 {
427    let index = wowlab_engine_domain::dbc::StatModifierKind::from_attribute(attribute)
428        .stat_index()
429        .expect("specific attributes always have a stat index");
430    let percent = totals.bonus_all_stat_pct
431        + if is_primary {
432            totals.bonus_primary_stat_pct
433        } else {
434            0.0
435        }
436        + totals.bonus_stat_pct.get(index).copied().unwrap_or(0.0);
437
438    percent / HUNDRED
439}
440
441pub(crate) fn dynamic_stamina(
442    program: &crate::state::CombatProgram,
443    _state: &CombatInstance,
444    totals: &BuffTotals,
445) -> f64 {
446    let combat_stats = &program.config.base_stats.stats;
447
448    combat_stats.stamina
449        + positive_non_create_bonus(combat_stats, wowlab_types::game::Attribute::Stamina)
450            * bonus_stat_multiplier(
451                totals,
452                wowlab_types::game::Attribute::Stamina,
453                program.config.base_stats.primary_attribute
454                    == Some(wowlab_types::game::Attribute::Stamina),
455            )
456}
457
458fn effective_school_and_flags(
459    view: &CombatView<'_>,
460    base_action: crate::state::program::LocalDamageActionIdx,
461    profile_action: crate::state::program::LocalDamageActionIdx,
462    effect_action: crate::state::program::LocalDamageActionIdx,
463    flags: DamageFlags,
464) -> (wowlab_types::combat::DamageSchool, DamageFlags) {
465    let override_school = active_spell_school_override(view, profile_action, effect_action);
466    let school =
467        override_school.unwrap_or_else(|| view.program.damage.action_at(base_action).school);
468    let flags = match override_school {
469        Some(_) if school.is_physical() => flags | DamageFlags::PHYSICAL,
470        Some(_) => flags & !DamageFlags::PHYSICAL,
471        None if !school.is_physical() => flags & !DamageFlags::PHYSICAL,
472        None => flags,
473    };
474
475    (school, flags)
476}
477
478#[cfg(feature = "rotation-optimizations")]
479fn actor_auto_attack_crit_pct(
480    request: &BoundDamageRequest,
481    source: wowlab_types::sim::ActorId,
482    owner: DamageBuffProjection,
483    actor: Option<DamageBuffProjection>,
484) -> f64 {
485    if !request.weapon.is_auto_attack {
486        return 0.0;
487    }
488
489    if source == wowlab_types::sim::ActorId::Player {
490        owner.auto_attack_crit_pct
491    } else {
492        actor.map_or(0.0, |projection| projection.auto_attack_crit_pct)
493    }
494}
495
496#[cfg(not(feature = "rotation-optimizations"))]
497fn actor_auto_attack_crit_pct(
498    request: &BoundDamageRequest,
499    source: wowlab_types::sim::ActorId,
500    owner: &BuffTotals,
501    actor: Option<&BuffTotals>,
502) -> f64 {
503    if !request.weapon.is_auto_attack {
504        return 0.0;
505    }
506
507    if source == wowlab_types::sim::ActorId::Player {
508        owner.auto_attack_crit_pct
509    } else {
510        actor.map_or(0.0, |totals| totals.auto_attack_crit_pct)
511    }
512}
513
514fn crit_chance_versus_target_health(
515    program: &crate::state::CombatProgram,
516    state: &CombatInstance,
517    source: wowlab_types::sim::ActorId,
518    target: wowlab_types::sim::EnemyIdx,
519    now: wowlab_types::sim::SimTime,
520) -> f64 {
521    if !program.damage.has_health_critical {
522        return 0.0;
523    }
524
525    let health_pct = state
526        .enemy_health_fraction(program, target, now)
527        .unwrap_or(1.0)
528        * HUNDRED;
529    let mut total = 0.0;
530
531    crate::systems::buffs::for_each_planned_actor_effect(
532        ActorView::new(program, state, source),
533        crate::state::EffectFamily::HEALTH_DEPENDENT_CRITICAL_CHANCE,
534        |stacks, effect| {
535            if let BuffEffect::CritChanceVersusTargetHealth {
536                percent,
537                threshold_pct,
538            } = effect
539                && health_pct >= *threshold_pct
540            {
541                total += percent * stacks;
542            }
543        },
544    );
545
546    total
547}
548
549#[derive(Clone, Copy)]
550#[expect(
551    clippy::struct_excessive_bools,
552    reason = "the immutable batch setup stores independent resolved damage-routing flags"
553)]
554pub(super) struct DamageBatchSetup {
555    source: wowlab_types::sim::ActorId,
556    flags: DamageFlags,
557    spell_id: u32,
558    profile_spell_id: u32,
559    profile_action: crate::state::program::LocalDamageActionIdx,
560    effect: crate::state::DamageEffectRef,
561    effect_binding: crate::state::program::LocalDamageEffectIdx,
562    reflected: bool,
563    is_pet: bool,
564    ignore_player: bool,
565    ignore_target: bool,
566    school: wowlab_types::combat::DamageSchool,
567    mitigation_flags: DamageFlags,
568    crit: f64,
569    vers: f64,
570    mastery: f64,
571    passive: PassiveDamageAdjustments,
572    #[cfg(feature = "rotation-optimizations")]
573    owner_projection: DamageBuffProjection,
574    #[cfg(feature = "rotation-optimizations")]
575    actor_projection: Option<DamageBuffProjection>,
576    #[cfg(not(feature = "rotation-optimizations"))]
577    owner_totals: BuffTotals,
578    #[cfg(not(feature = "rotation-optimizations"))]
579    actor_totals: Option<BuffTotals>,
580    school_mult: f64,
581    pet_mult: f64,
582    player_mult: f64,
583    actor_mult: f64,
584    auto_attack_crit_pct: f64,
585    base: f64,
586    aoe: AoeConfig,
587}
588
589// #t(fn: rust_max_fn_lines) Action and owner invariants are assembled once per damage batch.
590pub(super) fn prepare_damage_batch_setup(
591    program: &crate::state::CombatProgram,
592    state: &CombatInstance,
593    request: &BoundDamageRequest,
594    source: wowlab_types::sim::ActorId,
595) -> DamageBatchSetup {
596    wowlab_types::bench_metrics::record_damage_action_setup();
597    let combat_view = &CombatView::new(program, state);
598    let spell_id = request.effect.spell_id;
599    let flags = request.flags;
600    let reflected = flags.contains(DamageFlags::REFLECTED);
601    let is_pet = flags.contains(DamageFlags::PET);
602    let ignore_player = flags.contains(DamageFlags::IGNORE_PLAYER_MULTIPLIERS);
603    let ignore_target = reflected
604        || flags.intersects(
605            DamageFlags::IGNORE_TARGET_MULTIPLIERS
606                | DamageFlags::IGNORE_POSITIVE_TARGET_MULTIPLIERS,
607        );
608    let (school, mitigation_flags) = effective_school_and_flags(
609        combat_view,
610        request.binding.effect_action(),
611        request.binding.profile_action,
612        request.binding.effect_action(),
613        flags,
614    );
615
616    wowlab_types::bench_metrics::record_damage_owner_setup();
617    #[cfg(not(feature = "rotation-optimizations"))]
618    let (crit, vers) = read_player_stats_for_damage(&state.buffer);
619    #[cfg(feature = "rotation-optimizations")]
620    let owner_projection =
621        static_damage_buff_projection(program, state, wowlab_types::sim::ActorId::Player);
622    #[cfg(feature = "rotation-optimizations")]
623    let actor_projection = if source == wowlab_types::sim::ActorId::Player {
624        None
625    } else {
626        Some(static_damage_buff_projection(program, state, source))
627    };
628    #[cfg(feature = "rotation-optimizations")]
629    let owner_school_mult = owner_projection
630        .school_damage_mult
631        .get(school as usize)
632        .copied()
633        .unwrap_or(1.0);
634    #[cfg(feature = "rotation-optimizations")]
635    let actor_school_mult = actor_projection.map_or(1.0, |projection| {
636        projection
637            .school_damage_mult
638            .get(school as usize)
639            .copied()
640            .unwrap_or(1.0)
641    });
642    #[cfg(feature = "rotation-optimizations")]
643    let school_mult = owner_school_mult * actor_school_mult;
644    #[cfg(feature = "rotation-optimizations")]
645    let mastery = owner_projection.mastery;
646    #[cfg(feature = "rotation-optimizations")]
647    let pet_mult = if is_pet && !ignore_player {
648        owner_projection.pet_damage_mult
649    } else {
650        1.0
651    };
652    #[cfg(feature = "rotation-optimizations")]
653    let player_mult = if ignore_player {
654        1.0
655    } else {
656        owner_projection.damage_mult
657    };
658    #[cfg(feature = "rotation-optimizations")]
659    let actor_mult = actor_projection.map_or(1.0, |projection| projection.damage_mult);
660    #[cfg(feature = "rotation-optimizations")]
661    let auto_attack_crit_pct =
662        actor_auto_attack_crit_pct(request, source, owner_projection, actor_projection);
663
664    #[cfg(not(feature = "rotation-optimizations"))]
665    let owner_totals = actor_static_buff_totals(
666        combat_view.for_actor(wowlab_types::sim::ActorId::Player),
667        false,
668    );
669    #[cfg(not(feature = "rotation-optimizations"))]
670    let actor_totals = if source == wowlab_types::sim::ActorId::Player {
671        None
672    } else {
673        Some(actor_static_buff_totals(
674            combat_view.for_actor(source),
675            true,
676        ))
677    };
678    #[cfg(not(feature = "rotation-optimizations"))]
679    let owner_school_mult = owner_totals
680        .school_damage_mult
681        .get(school as usize)
682        .copied()
683        .unwrap_or(1.0);
684    #[cfg(not(feature = "rotation-optimizations"))]
685    let actor_school_mult = actor_totals.as_ref().map_or(1.0, |totals| {
686        totals
687            .school_damage_mult
688            .get(school as usize)
689            .copied()
690            .unwrap_or(1.0)
691    });
692    #[cfg(not(feature = "rotation-optimizations"))]
693    let school_mult = owner_school_mult * actor_school_mult;
694    #[cfg(not(feature = "rotation-optimizations"))]
695    let mastery = state.buffer.player().mastery + owner_totals.mastery_pct;
696    let passive = passive_damage_adjustments(program, state, request, mastery);
697    #[cfg(not(feature = "rotation-optimizations"))]
698    let pet_mult = if is_pet && !ignore_player {
699        owner_totals.pet_damage_mult
700    } else {
701        1.0
702    };
703    #[cfg(not(feature = "rotation-optimizations"))]
704    let player_mult = if ignore_player {
705        1.0
706    } else {
707        owner_totals.damage_mult
708    };
709    #[cfg(not(feature = "rotation-optimizations"))]
710    let actor_mult = actor_totals
711        .as_ref()
712        .map_or(1.0, |totals| totals.damage_mult);
713    #[cfg(not(feature = "rotation-optimizations"))]
714    let auto_attack_crit_pct =
715        actor_auto_attack_crit_pct(request, source, &owner_totals, actor_totals.as_ref());
716    let profile = program.damage.action_at(request.binding.profile_action);
717    let base = request.base_points.unwrap_or(profile.profile_base_points);
718    let base = crate::systems::buffs::active_effect_points(
719        ActorView::new(program, state, source),
720        request.binding.effect,
721        base,
722    ) + passive.direct_flat;
723
724    DamageBatchSetup {
725        source,
726        flags,
727        spell_id,
728        profile_spell_id: profile.spell.as_u32(),
729        profile_action: request.binding.profile_action,
730        effect: request.effect,
731        effect_binding: request.binding.effect,
732        reflected,
733        is_pet,
734        ignore_player,
735        ignore_target,
736        school,
737        mitigation_flags,
738        #[cfg(feature = "rotation-optimizations")]
739        crit: owner_projection.crit_pct,
740        #[cfg(not(feature = "rotation-optimizations"))]
741        crit,
742        #[cfg(feature = "rotation-optimizations")]
743        vers: owner_projection.vers_pct,
744        #[cfg(not(feature = "rotation-optimizations"))]
745        vers,
746        mastery,
747        passive,
748        #[cfg(feature = "rotation-optimizations")]
749        owner_projection,
750        #[cfg(feature = "rotation-optimizations")]
751        actor_projection,
752        #[cfg(not(feature = "rotation-optimizations"))]
753        owner_totals,
754        #[cfg(not(feature = "rotation-optimizations"))]
755        actor_totals,
756        school_mult,
757        pet_mult,
758        player_mult,
759        actor_mult,
760        auto_attack_crit_pct,
761        base,
762        aoe: read_spell_aoe_config(
763            program,
764            state,
765            source,
766            request.binding.profile_action,
767            request.binding.geometry_effect,
768        ),
769    }
770}
771
772// #t(fn: rust_max_fn_lines) Target-dependent damage state is the only per-hit setup stage.
773pub(super) fn prepare_damage_target_setup(
774    program: &crate::state::CombatProgram,
775    state: &CombatInstance,
776    batch: &DamageBatchSetup,
777    target: wowlab_types::sim::EnemyIdx,
778    now: wowlab_types::sim::SimTime,
779) -> DamageSetup {
780    wowlab_types::bench_metrics::record_damage_target_setup();
781    let modifier_target = (!batch.reflected).then_some(target);
782    let armor = if batch.reflected || batch.flags.contains(DamageFlags::IGNORE_TARGET_MULTIPLIERS) {
783        0.0
784    } else {
785        armor::for_target(
786            crate::HookView {
787                program,
788                instance: state,
789                target: Some(target),
790                now,
791            },
792            batch.source,
793            batch.mitigation_flags,
794        )
795    };
796    let armor = crate::systems::buffs::active_spell_modifier_value(
797        ActorView::new(program, state, batch.source),
798        batch.profile_action,
799        wowlab_engine_domain::dbc::ModifierPropertyKind::TargetResistance,
800        armor,
801    )
802    .max(0.0);
803    let mods = spell_scoped_mods_for(
804        program,
805        state,
806        batch.source,
807        SpellModifierQuery::new(
808            SpellModifierProfile::new(batch.profile_spell_id, batch.profile_action),
809            SpellModifierEffect::new(batch.effect, batch.effect_binding),
810            batch.flags,
811        ),
812        modifier_target,
813        now,
814    );
815    let target_mult = if batch.ignore_target {
816        1.0
817    } else {
818        mods.target_damage_mult
819    };
820    let owner_dynamic_mult = actor_dynamic_damage_mult(
821        ActorView::new(program, state, wowlab_types::sim::ActorId::Player),
822        modifier_target,
823        false,
824        now,
825    );
826    let actor_dynamic_mult = if batch.source == wowlab_types::sim::ActorId::Player {
827        1.0
828    } else {
829        actor_dynamic_damage_mult(
830            ActorView::new(program, state, batch.source),
831            modifier_target,
832            true,
833            now,
834        )
835    };
836    let auto_combined_mult = batch.school_mult * batch.passive.damage_mult * target_mult;
837    let combined_mult = auto_combined_mult
838        * batch.player_mult
839        * batch.actor_mult
840        * owner_dynamic_mult
841        * actor_dynamic_mult
842        * mods.damage_mult
843        * batch.pet_mult;
844    #[cfg(feature = "rotation-optimizations")]
845    let owner_crit_pct = 0.0;
846    #[cfg(not(feature = "rotation-optimizations"))]
847    let owner_crit_pct = batch.owner_totals.crit_pct;
848    let mastery_ctx = MasteryCtx {
849        program,
850        instance: state,
851        spell_id: batch.spell_id,
852        spell_action: batch.profile_action,
853        school: batch.school,
854        mastery: batch.mastery,
855        player_crit_pct: batch.crit + owner_crit_pct,
856        mastery_spell: program.config.mastery_spell,
857        mastery_action: program.damage.mastery_action(),
858        is_periodic: batch.flags.contains(DamageFlags::PERIODIC),
859        is_pet: batch.is_pet,
860        source: batch.source,
861        target: modifier_target,
862        now,
863    };
864    let mastery_mult = (program.config.mastery_hook)(&mastery_ctx);
865    let crit_scaled = crit_chance_scaled_mults(
866        program,
867        state,
868        batch.profile_action,
869        batch.school,
870        mastery_ctx.player_crit_pct,
871    );
872    let crit_multiplier = resolved_crit_multiplier(
873        batch.flags,
874        wowlab_engine_domain::stats::crit_multiplier(&program.config.base_stats.stats)
875            * crit_scaled.total,
876        (program.config.mastery_crit_damage)(&mastery_ctx),
877        program
878            .damage
879            .action_at(batch.profile_action)
880            .passive
881            .crit_damage_bonus,
882        mods.crit_damage_mult * crit_scaled.bonus,
883    );
884    let is_companion = batch
885        .flags
886        .intersects(DamageFlags::PET | DamageFlags::GUARDIAN);
887    #[cfg(feature = "rotation-optimizations")]
888    let owner_auto_attack_damage_mult = batch.owner_projection.auto_attack_damage_mult;
889    #[cfg(not(feature = "rotation-optimizations"))]
890    let owner_auto_attack_damage_mult = batch.owner_totals.auto_attack_damage_mult;
891    #[cfg(feature = "rotation-optimizations")]
892    let actor_auto_attack_damage_mult = batch
893        .actor_projection
894        .map_or(1.0, |projection| projection.auto_attack_damage_mult);
895    #[cfg(not(feature = "rotation-optimizations"))]
896    let actor_auto_attack_damage_mult = batch
897        .actor_totals
898        .as_ref()
899        .map_or(1.0, |totals| totals.auto_attack_damage_mult);
900    #[cfg(feature = "rotation-optimizations")]
901    let owner_vers_pct = 0.0;
902    #[cfg(not(feature = "rotation-optimizations"))]
903    let owner_vers_pct = batch.owner_totals.vers_pct;
904
905    DamageSetup {
906        base: batch.base,
907        armor,
908        armor_k: program.enemy_definition(target).map_or(
909            0.0,
910            wowlab_engine_domain::encounter::EnemyActorDefinition::armor_constant,
911        ),
912        combined_mult,
913        target_damage_mult: target_mult,
914        power_coefficient_add: batch.passive.power_coefficient_add,
915        auto_combined_mult,
916        auto_attack_buff_mult: if is_companion {
917            1.0
918        } else {
919            owner_auto_attack_damage_mult
920        } * actor_auto_attack_damage_mult,
921        mastery_mult,
922        crit_pct_total: (batch.crit
923            + owner_crit_pct
924            + batch.auto_attack_crit_pct
925            + batch.passive.crit_chance
926            + mods.crit_chance_pct
927            + if batch.reflected {
928                0.0
929            } else {
930                crit_chance_versus_target_health(program, state, batch.source, target, now)
931            }
932            + guaranteed_crit_pct(program, state, batch.profile_action))
933            * batch.passive.crit_chance_mult,
934        crit_multiplier,
935        vers_pct_total: if batch.ignore_player {
936            0.0
937        } else {
938            batch.vers + owner_vers_pct
939        },
940        school: batch.school,
941        is_physical: batch.mitigation_flags.contains(DamageFlags::PHYSICAL),
942        aoe: batch.aoe,
943    }
944}
945
946pub(super) fn prepare_bound_damage_setup_for(
947    program: &crate::state::CombatProgram,
948    state: &CombatInstance,
949    request: &BoundDamageRequest,
950    source: wowlab_types::sim::ActorId,
951    target: wowlab_types::sim::EnemyIdx,
952    now: wowlab_types::sim::SimTime,
953) -> DamageSetup {
954    let batch = prepare_damage_batch_setup(program, state, request, source);
955
956    prepare_damage_target_setup(program, state, &batch, target, now)
957}
958
959#[cfg(test)]
960pub(super) fn prepare_damage_setup_for(
961    program: &crate::state::CombatProgram,
962    state: &CombatInstance,
963    request: &DamageRequest,
964    source: wowlab_types::sim::ActorId,
965    target: wowlab_types::sim::EnemyIdx,
966    now: wowlab_types::sim::SimTime,
967) -> DamageSetup {
968    let binding = program
969        .damage
970        .diagnostic_bind_request(
971            request.effect,
972            request.geometry_effect,
973            request.profile_spell_id,
974        )
975        .expect("damage setup fixture identities belong to its finalized program");
976    let request = BoundDamageRequest::new(*request, binding);
977
978    prepare_bound_damage_setup_for(program, state, &request, source, target, now)
979}
980
981#[cfg(test)]
982pub(super) fn prepare_damage_setup_fixture(
983    program: &crate::state::CombatProgram,
984    state: &CombatInstance,
985    request: &DamageRequest,
986) -> DamageSetup {
987    let binding = program
988        .damage
989        .diagnostic_bind_request(
990            request.effect,
991            request.geometry_effect,
992            request.profile_spell_id,
993        )
994        .expect("damage setup fixture identities belong to its finalized program");
995    let request = BoundDamageRequest::new(*request, binding);
996
997    prepare_bound_damage_setup_for(
998        program,
999        state,
1000        &request,
1001        wowlab_types::sim::ActorId::Player,
1002        state
1003            .current_target()
1004            .expect("damage setup fixture requires a selected target"),
1005        wowlab_types::sim::SimTime::ZERO,
1006    )
1007}
1008
1009pub(crate) fn read_player_stats_for_damage(buf: &BoundDenseBuffer) -> (f64, f64) {
1010    let p = buf.player();
1011
1012    (p.crit, p.versatility)
1013}
1014
1015#[cfg(test)]
1016fn apply_test_aura_as(
1017    program: &crate::state::CombatProgram,
1018    state: &mut CombatInstance,
1019    aura_id: u32,
1020    source: wowlab_types::sim::ActorId,
1021) {
1022    const TEST_RNG_ROLL: f64 = 0.5;
1023
1024    let mut rng = || TEST_RNG_ROLL;
1025    let local = program.aura_local(aura_id).expect("aura registered");
1026    let mut sink = wowlab_engine_telemetry::TelemetrySink::default();
1027
1028    crate::systems::auras::apply_aura(
1029        &mut crate::HookCtx::new(
1030            crate::context::HookCtxServices {
1031                program,
1032                instance: state,
1033                sink: &mut sink,
1034                rng: &mut rng,
1035            },
1036            crate::context::HookCtxRequest::for_target(
1037                wowlab_types::sim::SimTime::ZERO,
1038                wowlab_types::sim::EnemyIdx::PRIMARY,
1039            )
1040            .with_source(source),
1041        ),
1042        local,
1043    );
1044}
1045
1046#[cfg(test)]
1047mod auto_attack_crit_tests;
1048#[cfg(test)]
1049mod passive_flat_tests;
1050#[cfg(test)]
1051mod school_override_tests;
1052
1053#[cfg(test)]
1054#[path = "setup/tests.rs"]
1055mod tests;