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#[derive(Clone, Copy, Debug)]
249pub(super) struct CritChanceScaledMults {
250 total: f64,
252 bonus: f64,
254}
255
256fn 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
589pub(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
772pub(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;