1use wowlab_engine_domain::dbc::{
2 ModifierOperation, ModifierPropertyKind, aura_subtype_modifier, modifier_property_is,
3};
4use wowlab_types::constants::HUNDRED;
5
6#[cfg(feature = "rotation-optimizations")]
7use crate::systems::buffs::for_each_active_damage_modifier_for_actions;
8#[cfg(not(feature = "rotation-optimizations"))]
9use crate::{
10 state::EffectFamily,
11 systems::buffs::{
12 for_each_active_damage_modifier_for_action, for_each_planned_actor_effect_damage_binding,
13 },
14};
15use crate::{
16 state::{BuffEffect, CombatInstance, DamageEffectRef, DamageSource},
17 systems::{
18 buffs::{
19 dbc_effect_points, for_each_active_spell_modifier_for_action, spell_modifier_owner,
20 target_aura_mechanic_mask,
21 },
22 damage_pipeline::DamageFlags,
23 },
24};
25
26#[derive(Clone, Copy, Debug, PartialEq)]
27pub(crate) struct SpellScopedMods {
29 pub(crate) damage_mult: f64,
30 pub(crate) target_damage_mult: f64,
31 pub(crate) crit_chance_pct: f64,
32 pub(crate) crit_damage_mult: f64,
33}
34
35impl Default for SpellScopedMods {
36 fn default() -> Self {
37 Self {
38 damage_mult: 1.0,
39 target_damage_mult: 1.0,
40 crit_chance_pct: 0.0,
41 crit_damage_mult: 1.0,
42 }
43 }
44}
45
46#[derive(Clone, Copy, Debug)]
47pub(crate) struct SpellModifierProfile {
48 external: u32,
49 local: crate::state::program::LocalDamageActionIdx,
50}
51
52impl SpellModifierProfile {
53 pub(crate) const fn new(
54 external: u32,
55 local: crate::state::program::LocalDamageActionIdx,
56 ) -> Self {
57 Self { external, local }
58 }
59}
60
61#[derive(Clone, Copy, Debug)]
62pub(crate) struct SpellModifierEffect {
63 external: DamageEffectRef,
64 local: crate::state::program::LocalDamageEffectIdx,
65}
66
67impl SpellModifierEffect {
68 pub(crate) const fn new(
69 external: DamageEffectRef,
70 local: crate::state::program::LocalDamageEffectIdx,
71 ) -> Self {
72 Self { external, local }
73 }
74}
75
76#[derive(Clone, Copy, Debug)]
77pub(crate) struct SpellModifierQuery {
78 profile: SpellModifierProfile,
79 effect: SpellModifierEffect,
80 flags: DamageFlags,
81}
82
83impl SpellModifierQuery {
84 pub(crate) const fn new(
85 profile: SpellModifierProfile,
86 effect: SpellModifierEffect,
87 flags: DamageFlags,
88 ) -> Self {
89 Self {
90 profile,
91 effect,
92 flags,
93 }
94 }
95
96 fn listed(
97 self,
98 program: &crate::state::CombatProgram,
99 binding: &crate::state::program::FinalizedBuffDamageBinding,
100 ) -> bool {
101 let effect_action = program.damage.effect_at(self.effect.local).action;
102
103 binding.lists_action(self.profile.local) || binding.lists_action(effect_action)
104 }
105
106 fn affected(
107 self,
108 program: &crate::state::CombatProgram,
109 source: crate::state::program::LocalDamageEffectIdx,
110 ) -> bool {
111 let profile_affected = program
112 .damage
113 .effect_affects_action(source, self.profile.local);
114 let effect_action = program.damage.effect_at(self.effect.local).action;
115 let effect_affected = self.profile.external != self.effect.external.spell_id
116 && program.damage.effect_affects_action(source, effect_action);
117
118 profile_affected || effect_affected
119 }
120
121 fn mechanic_mask(
122 self,
123 program: &crate::state::CombatProgram,
124 ) -> wowlab_engine_domain::dbc::MechanicMask {
125 let effect = program.damage.effect_at(self.effect.local).mechanic_mask;
126
127 if effect.is_empty() {
128 program.damage.action_at(self.profile.local).mechanic_mask
129 } else {
130 effect
131 }
132 }
133
134 fn school_mask(
135 self,
136 program: &crate::state::CombatProgram,
137 ) -> wowlab_engine_domain::dbc::SpellSchoolMask {
138 program
139 .damage
140 .action_at(program.damage.effect_at(self.effect.local).action)
141 .school_mask
142 }
143}
144
145struct SpellModifierContext<'a> {
146 program: &'a crate::state::CombatProgram,
147 state: &'a CombatInstance,
148 query: SpellModifierQuery,
149 target: Option<wowlab_types::sim::EnemyIdx>,
150 now: wowlab_types::sim::SimTime,
151}
152
153#[derive(Clone, Copy)]
154struct ActiveEffectPointModifier {
155 stacks: f64,
156 value: f64,
157 source: crate::state::program::LocalDamageEffectIdx,
158 operation: ModifierOperation,
159}
160
161impl ActiveEffectPointModifier {
162 fn from_buff(
163 stacks: f64,
164 effect: BuffEffect,
165 binding: &crate::state::program::FinalizedBuffDamageBinding,
166 ) -> Option<Self> {
167 let BuffEffect::SpellModifierFromEffect {
168 value,
169 property: ModifierPropertyKind::Points,
170 operation,
171 ..
172 } = effect
173 else {
174 return None;
175 };
176
177 Some(Self {
178 stacks,
179 value,
180 source: binding.source_effect?,
181 operation,
182 })
183 }
184}
185
186impl SpellModifierContext<'_> {
187 fn apply(
188 &self,
189 stacks: f64,
190 effect: &BuffEffect,
191 binding: &crate::state::program::FinalizedBuffDamageBinding,
192 mods: &mut SpellScopedMods,
193 ) {
194 let before = *mods;
195
196 self.apply_damage(stacks, effect, binding, mods);
197 self.apply_target(stacks, effect, binding, mods);
198 self.apply_critical(stacks, effect, binding, mods);
199
200 if *mods != before {
201 tracing::trace!(
202 profile_spell_id = self.query.profile.external,
203 effect_spell_id = self.query.effect.external.spell_id,
204 stacks,
205 ?effect,
206 damage_mult = mods.damage_mult,
207 target_damage_mult = mods.target_damage_mult,
208 crit_chance_pct = mods.crit_chance_pct,
209 crit_damage_mult = mods.crit_damage_mult,
210 "spell-scoped aura modifier applied"
211 );
212 }
213 }
214
215 fn apply_damage(
216 &self,
217 stacks: f64,
218 effect: &BuffEffect,
219 binding: &crate::state::program::FinalizedBuffDamageBinding,
220 mods: &mut SpellScopedMods,
221 ) {
222 match effect {
223 BuffEffect::DamageMultSpells(value, _) if self.query.listed(self.program, binding) => {
224 mods.damage_mult *= value.powf(stacks);
225 }
226 BuffEffect::DamageMultSpellsActive(value, _)
227 if self.query.listed(self.program, binding) =>
228 {
229 mods.damage_mult *= value;
230 }
231 BuffEffect::SpellDamageFromEffect {
232 percent,
233 source_spell_id,
234 effect_index,
235 periodic,
236 } if *periodic == self.query.flags.contains(DamageFlags::PERIODIC)
237 && binding
238 .source_effect
239 .is_some_and(|source| self.query.affected(self.program, source)) =>
240 {
241 let points = dbc_effect_points(
242 self.program,
243 self.state,
244 binding
245 .source_effect
246 .expect("matched source binding exists"),
247 stacks,
248 );
249
250 mods.damage_mult *= 1.0 + percent * points / HUNDRED;
251 }
252 BuffEffect::DamageMultSpellsLinear(value, _)
253 if self.query.listed(self.program, binding) =>
254 {
255 mods.damage_mult *= 1.0 + value / HUNDRED * stacks;
256 }
257 BuffEffect::DynamicDamageMultSpells(resolve, _)
258 if self.query.listed(self.program, binding) =>
259 {
260 mods.damage_mult *=
261 resolve(self.program, self.state, self.target, self.now).powf(stacks);
262 }
263 BuffEffect::DamageToTargetAuraMechanicPercent {
264 percent,
265 mechanic_mask,
266 } if self.target.is_some_and(|target| {
267 target_aura_mechanic_mask(self.program, self.state, target, self.now)
268 .intersects(*mechanic_mask)
269 }) =>
270 {
271 mods.damage_mult *= (1.0 + percent / HUNDRED).powf(stacks);
272 }
273 _ => {}
274 }
275 }
276
277 fn apply_incoming_damage(&self, stacks: f64, effect: &BuffEffect, mods: &mut SpellScopedMods) {
282 if let BuffEffect::IncomingDamagePercent { percent, schools } = effect
283 && schools.intersects(self.query.school_mask(self.program))
284 {
285 mods.target_damage_mult *= (1.0 + percent / HUNDRED).max(0.0).powf(stacks);
286 }
287 }
288
289 fn apply_target(
290 &self,
291 stacks: f64,
292 effect: &BuffEffect,
293 binding: &crate::state::program::FinalizedBuffDamageBinding,
294 mods: &mut SpellScopedMods,
295 ) {
296 match effect {
297 BuffEffect::DamageTakenFromSource(percent, source)
298 if damage_source_matches(*source, self.query.flags) =>
299 {
300 mods.target_damage_mult *= 1.0 + percent / HUNDRED;
301 }
302 BuffEffect::DamageTakenFromSourceSpells {
303 percent,
304 source_spell_id,
305 effect_index,
306 source,
307 } if damage_source_matches(*source, self.query.flags)
308 && binding
309 .source_effect
310 .is_some_and(|source| self.query.affected(self.program, source)) =>
311 {
312 let points = dbc_effect_points(
313 self.program,
314 self.state,
315 binding
316 .source_effect
317 .expect("matched source binding exists"),
318 stacks,
319 );
320
321 mods.target_damage_mult *= 1.0 + percent / HUNDRED * points;
322 }
323 BuffEffect::MechanicDamageTakenPercent {
324 percent,
325 mechanic_mask,
326 } if self
327 .query
328 .mechanic_mask(self.program)
329 .intersects(*mechanic_mask) =>
330 {
331 mods.target_damage_mult *= (1.0 + percent / HUNDRED).powf(stacks);
332 }
333 BuffEffect::CreatureAoeDamageAvoidance { percent, schools }
334 if self.query.flags.contains(DamageFlags::AOE)
335 && schools.intersects(self.query.school_mask(self.program)) =>
336 {
337 mods.target_damage_mult *= (1.0 + percent / HUNDRED).powf(stacks);
338 }
339 _ => {}
340 }
341 }
342
343 fn apply_critical(
344 &self,
345 stacks: f64,
346 effect: &BuffEffect,
347 binding: &crate::state::program::FinalizedBuffDamageBinding,
348 mods: &mut SpellScopedMods,
349 ) {
350 match effect {
351 BuffEffect::SpellCritChanceFromEffect {
352 percent,
353 source_spell_id,
354 effect_index,
355 } if binding
356 .source_effect
357 .is_some_and(|source| self.query.affected(self.program, source)) =>
358 {
359 let points = dbc_effect_points(
360 self.program,
361 self.state,
362 binding
363 .source_effect
364 .expect("matched source binding exists"),
365 stacks,
366 );
367
368 mods.crit_chance_pct += percent * points;
369 }
370 BuffEffect::SpellCritDamageFromEffect {
371 percent,
372 source_spell_id,
373 effect_index,
374 } if binding
375 .source_effect
376 .is_some_and(|source| self.query.affected(self.program, source)) =>
377 {
378 let points = dbc_effect_points(
379 self.program,
380 self.state,
381 binding
382 .source_effect
383 .expect("matched source binding exists"),
384 stacks,
385 );
386
387 mods.crit_damage_mult *= 1.0 + percent * points / HUNDRED;
388 }
389 BuffEffect::CritChanceSpells(value, _) if self.query.listed(self.program, binding) => {
390 mods.crit_chance_pct += value * stacks;
391 }
392 BuffEffect::CritDamageSpells(value, _) if self.query.listed(self.program, binding) => {
393 mods.crit_damage_mult *= (1.0 + value / HUNDRED).powf(stacks);
394 }
395 _ => {}
396 }
397 }
398
399 fn apply_driver_spell_modifiers(&self, mods: &mut SpellScopedMods) {
400 #[cfg(not(feature = "rotation-optimizations"))]
401 for (index, modifier) in self.program.driver_spell_modifiers.iter().enumerate() {
402 let effect_action = self
403 .program
404 .damage
405 .effect_at(self.query.effect.local)
406 .action;
407
408 if !self
409 .program
410 .damage
411 .driver_modifier_lists_action(index, self.query.profile.local)
412 && !self
413 .program
414 .damage
415 .driver_modifier_lists_action(index, effect_action)
416 {
417 continue;
418 }
419
420 self.apply_driver_spell_modifier(modifier, mods);
421 }
422
423 #[cfg(feature = "rotation-optimizations")]
424 {
425 let effect_action = self
426 .program
427 .damage
428 .effect_at(self.query.effect.local)
429 .action;
430 let profile_modifiers = self
431 .program
432 .damage
433 .driver_modifiers_for_action(self.query.profile.local);
434 let effect_modifiers = self
435 .program
436 .damage
437 .driver_modifiers_for_action(effect_action);
438
439 for_each_sorted_union(profile_modifiers, effect_modifiers, |index| {
440 let modifier = self
441 .program
442 .driver_spell_modifiers
443 .get(index)
444 .expect("driver adjacency references a finalized spell modifier");
445
446 self.apply_driver_spell_modifier(modifier, mods);
447 });
448 }
449 }
450
451 fn apply_driver_spell_modifier(
452 &self,
453 modifier: &crate::state::DriverSpellModifier,
454 mods: &mut SpellScopedMods,
455 ) {
456 let resolved = (modifier.resolve)(crate::HookView {
457 program: self.program,
458 instance: self.state,
459 target: self.target,
460 now: self.now,
461 });
462
463 mods.damage_mult *= resolved.damage_mult;
464 mods.target_damage_mult *= resolved.target_damage_mult;
465 mods.crit_chance_pct += resolved.crit_chance_pct;
466 mods.crit_damage_mult *= resolved.crit_damage_mult;
467
468 tracing::trace!(
470 driver_spell_id = modifier.driver_spell_id,
471 profile_spell_id = self.query.profile.external,
472 effect_spell_id = self.query.effect.external.spell_id,
473 ?resolved,
474 "driver-owned spell modifier applied"
475 );
476 }
477
478 fn apply_effect_point_damage_modifiers(
479 &self,
480 owner: wowlab_types::sim::ActorId,
481 mods: &mut SpellScopedMods,
482 ) {
483 #[cfg(not(feature = "rotation-optimizations"))]
484 for (coordinate, _) in self.program.damage.folded_passive_effects() {
485 self.apply_effect_point_damage_modifier(owner, coordinate, mods);
486 }
487
488 #[cfg(feature = "rotation-optimizations")]
489 {
490 let effect_action = self
491 .program
492 .damage
493 .effect_at(self.query.effect.local)
494 .action;
495 let profile_effects = self
496 .program
497 .damage
498 .folded_passive_effects_for_action(self.query.profile.local);
499 let effect_effects = self
500 .program
501 .damage
502 .folded_passive_effects_for_action(effect_action);
503
504 for_each_sorted_union(profile_effects, effect_effects, |coordinate| {
505 self.apply_effect_point_damage_modifier(owner, coordinate, mods);
506 });
507 }
508 }
509
510 fn apply_effect_point_damage_modifier(
511 &self,
512 owner: wowlab_types::sim::ActorId,
513 coordinate: crate::state::program::LocalDamageEffectIdx,
514 mods: &mut SpellScopedMods,
515 ) {
516 let passive_effect = self.program.damage.effect_at(coordinate);
517 let hit_is_periodic = self.query.flags.contains(DamageFlags::PERIODIC);
518 let property = passive_effect.modifier_property;
519 let effect_is_periodic =
520 if modifier_property_is(property, ModifierPropertyKind::GenericDamage) {
521 false
522 } else if modifier_property_is(property, ModifierPropertyKind::PeriodicAmount) {
523 true
524 } else {
525 return;
526 };
527
528 if effect_is_periodic != hit_is_periodic
529 || !self.query.affected(self.program, coordinate)
530 || aura_subtype_modifier(passive_effect.aura_subtype)
531 .is_none_or(|(operation, _)| operation != ModifierOperation::Percent)
532 {
533 return;
534 }
535
536 let initial = passive_effect.base_points;
537 let mut flat = 0.0;
538 let mut percent = 1.0;
539 let target = passive_effect.action;
540
541 for_each_active_spell_modifier_for_action(
542 crate::context::ActorView::new(self.program, self.state, owner),
543 target,
544 ModifierPropertyKind::Points,
545 |stacks, effect, binding| {
546 let Some(modifier) = ActiveEffectPointModifier::from_buff(stacks, *effect, binding)
547 else {
548 return;
549 };
550
551 let affected = self
552 .program
553 .damage
554 .effect_affects_action(modifier.source, target);
555
556 if !affected {
557 return;
558 }
559
560 let points =
561 dbc_effect_points(self.program, self.state, modifier.source, modifier.stacks);
562
563 match modifier.operation {
564 ModifierOperation::Flat => flat += modifier.value * points,
565 ModifierOperation::Percent => {
566 percent *= 1.0 + modifier.value * points / HUNDRED;
567 }
568 _ => {}
569 }
570 },
571 );
572
573 let original_factor = 1.0 + initial / HUNDRED;
574
575 if original_factor.abs() <= f64::EPSILON {
576 return;
577 }
578
579 let modified_factor = 1.0 + (initial + flat) * percent / HUNDRED;
580
581 mods.damage_mult *= modified_factor / original_factor;
582 }
583}
584
585#[cfg(feature = "rotation-optimizations")]
586fn for_each_sorted_union<T>(left: &[T], right: &[T], mut visit: impl FnMut(T))
587where
588 T: Copy + Ord,
589{
590 let mut left_index = 0;
591 let mut right_index = 0;
592
593 while left_index < left.len() || right_index < right.len() {
594 let effect = match (left.get(left_index), right.get(right_index)) {
595 (Some(&left_effect), Some(&right_effect)) if left_effect < right_effect => {
596 left_index += 1;
597
598 left_effect
599 }
600 (Some(&left_effect), Some(&right_effect)) if right_effect < left_effect => {
601 right_index += 1;
602
603 right_effect
604 }
605 (Some(&effect), Some(_)) => {
606 left_index += 1;
607 right_index += 1;
608
609 effect
610 }
611 (Some(&effect), None) => {
612 left_index += 1;
613
614 effect
615 }
616 (None, Some(&effect)) => {
617 right_index += 1;
618
619 effect
620 }
621 (None, None) => break,
622 };
623
624 visit(effect);
625 }
626}
627
628fn damage_source_matches(source: DamageSource, flags: DamageFlags) -> bool {
629 match source {
630 DamageSource::Player => !flags.intersects(DamageFlags::PET | DamageFlags::GUARDIAN),
631 DamageSource::Pet => {
632 flags.contains(DamageFlags::PET) && !flags.contains(DamageFlags::GUARDIAN)
633 }
634 DamageSource::Guardian => flags.contains(DamageFlags::GUARDIAN),
635 }
636}
637
638fn target_aoe_damage_mult(
639 program: &crate::state::CombatProgram,
640 query: SpellModifierQuery,
641 target: Option<wowlab_types::sim::EnemyIdx>,
642) -> f64 {
643 if !query.flags.contains(DamageFlags::AOE) {
644 return 1.0;
645 }
646
647 target
648 .and_then(|target| program.enemy_definition(target))
649 .map_or(1.0, |enemy| {
650 1.0 - enemy.creature_aoe_avoidance_pct().clamp(0.0, HUNDRED) / HUNDRED
651 })
652}
653
654pub(crate) fn spell_scoped_mods_for(
655 program: &crate::state::CombatProgram,
656 state: &CombatInstance,
657 source: wowlab_types::sim::ActorId,
658 query: SpellModifierQuery,
659 target: Option<wowlab_types::sim::EnemyIdx>,
660 now: wowlab_types::sim::SimTime,
661) -> SpellScopedMods {
662 if !program.damage.has_spell_modifiers {
663 return SpellScopedMods::default();
664 }
665
666 let mut mods = SpellScopedMods::default();
667 let context = SpellModifierContext {
668 program,
669 state,
670 query,
671 target,
672 now,
673 };
674
675 mods.target_damage_mult *= target_aoe_damage_mult(program, query, target);
676
677 let owner = spell_modifier_owner(source);
678 let effect_action = program.damage.effect_at(query.effect.local).action;
679
680 if let Some(owner) = owner {
681 #[cfg(feature = "rotation-optimizations")]
682 for_each_active_damage_modifier_for_actions(
683 crate::context::ActorView::new(program, state, owner),
684 query.profile.local,
685 effect_action,
686 |stacks, effect, binding| context.apply(stacks, effect, binding, &mut mods),
687 );
688
689 #[cfg(not(feature = "rotation-optimizations"))]
690 if query.profile.local == effect_action {
691 for_each_active_damage_modifier_for_action(
692 crate::context::ActorView::new(program, state, owner),
693 query.profile.local,
694 |stacks, effect, binding| context.apply(stacks, effect, binding, &mut mods),
695 );
696 } else {
697 for_each_planned_actor_effect_damage_binding(
698 crate::context::ActorView::new(program, state, owner),
699 EffectFamily::SPELL_MODIFIER,
700 |stacks, effect, binding| context.apply(stacks, effect, binding, &mut mods),
701 );
702 }
703 }
704
705 if let Some(target) = target {
706 let target_view = crate::context::ActorView::new(
707 program,
708 state,
709 wowlab_types::sim::ActorId::Enemy(target),
710 );
711
712 #[cfg(feature = "rotation-optimizations")]
713 for_each_active_damage_modifier_for_actions(
714 target_view,
715 query.profile.local,
716 effect_action,
717 |stacks, effect, binding| {
718 context.apply(stacks, effect, binding, &mut mods);
719 context.apply_incoming_damage(stacks, effect, &mut mods);
720 },
721 );
722
723 #[cfg(not(feature = "rotation-optimizations"))]
724 if query.profile.local == effect_action {
725 for_each_active_damage_modifier_for_action(
726 target_view,
727 query.profile.local,
728 |stacks, effect, binding| {
729 context.apply(stacks, effect, binding, &mut mods);
730 context.apply_incoming_damage(stacks, effect, &mut mods);
731 },
732 );
733 } else {
734 for_each_planned_actor_effect_damage_binding(
735 target_view,
736 EffectFamily::SPELL_MODIFIER,
737 |stacks, effect, binding| {
738 context.apply(stacks, effect, binding, &mut mods);
739 context.apply_incoming_damage(stacks, effect, &mut mods);
740 },
741 );
742 }
743 }
744
745 if let Some(owner) = owner {
746 context.apply_driver_spell_modifiers(&mut mods);
747 context.apply_effect_point_damage_modifiers(owner, &mut mods);
748 }
749
750 mods
751}
752
753#[cfg(test)]
754pub(crate) fn spell_scoped_mods_fixture(
755 view: &crate::context::CombatView<'_>,
756 spell_id: u32,
757 flags: DamageFlags,
758) -> SpellScopedMods {
759 let effect = DamageEffectRef::new(spell_id, 0);
760 let profile_action = view
761 .program
762 .damage
763 .diagnostic_bind_action(spell_id)
764 .expect("spell modifier fixture profile belongs to the finalized program");
765 let effect_binding = view
766 .program
767 .damage
768 .diagnostic_bind_effect(effect)
769 .expect("spell modifier fixture effect belongs to the finalized program");
770
771 spell_scoped_mods_for(
772 view.program,
773 view.instance,
774 wowlab_types::sim::ActorId::Player,
775 SpellModifierQuery::new(
776 SpellModifierProfile::new(spell_id, profile_action),
777 SpellModifierEffect::new(effect, effect_binding),
778 flags,
779 ),
780 view.instance.current_target(),
781 wowlab_types::sim::SimTime::ZERO,
782 )
783}