Skip to main content

wowlab_engine_combat/state/
buff_effect.rs

1// #t(file: rust_too_many_lines_in_file) BuffEffect keeps its exhaustive scaling and classification contracts beside the variant vocabulary.
2
3use wowlab_engine_domain::dbc::ModifierOperation;
4use wowlab_types::{combat::DamageSchool, constants::HUNDRED};
5
6use super::{
7    AbsorbEffect, ActiveSpellGate, DamageReplication, DamageSource, DamageTransfer,
8    HealthThresholdDirection,
9};
10
11fn stacked_percent_multiplier(percent: f64, stacks: f64) -> f64 {
12    ((1.0 + percent / HUNDRED).powf(stacks) - 1.0) * HUNDRED
13}
14
15// #t(fn: rust_cyclomatic_complexity) the finalized taxonomy explicitly names every non-exhaustive modifier property supported by Engine Domain.
16fn modifier_property_family(
17    property: wowlab_engine_domain::dbc::ModifierPropertyKind,
18) -> EffectFamily {
19    use wowlab_engine_domain::dbc::ModifierPropertyKind as Property;
20
21    match property {
22        Property::GenericDamage => EffectFamily::MODIFIER_GENERIC_DAMAGE,
23        Property::Duration => EffectFamily::MODIFIER_DURATION,
24        Property::Threat => EffectFamily::MODIFIER_THREAT,
25        Property::Effect1 => EffectFamily::MODIFIER_EFFECT_1,
26        Property::ProcCharges => EffectFamily::MODIFIER_PROC_CHARGES,
27        Property::Range => EffectFamily::MODIFIER_RANGE,
28        Property::Radius => EffectFamily::MODIFIER_RADIUS,
29        Property::CritChance => EffectFamily::MODIFIER_CRITICAL_CHANCE,
30        Property::Points => EffectFamily::MODIFIER_POINTS,
31        Property::ResistPushback => EffectFamily::MODIFIER_PUSHBACK_RESISTANCE,
32        Property::CastTime => EffectFamily::MODIFIER_CAST_TIME,
33        Property::Cooldown => EffectFamily::MODIFIER_COOLDOWN,
34        Property::Effect2 => EffectFamily::MODIFIER_EFFECT_2,
35        Property::TargetResistance => EffectFamily::MODIFIER_TARGET_RESISTANCE,
36        Property::ResourceCost1 => EffectFamily::MODIFIER_RESOURCE_COST_1,
37        Property::CritDamage => EffectFamily::MODIFIER_CRITICAL_DAMAGE,
38        Property::HitChance => EffectFamily::MODIFIER_HIT_CHANCE,
39        Property::ChainTargets => EffectFamily::MODIFIER_CHAIN_TARGETS,
40        Property::ProcChance => EffectFamily::MODIFIER_PROC_CHANCE,
41        Property::TickTime => EffectFamily::MODIFIER_TICK_TIME,
42        Property::ChainMultiplier => EffectFamily::MODIFIER_CHAIN_MULTIPLIER,
43        Property::GlobalCooldown => EffectFamily::MODIFIER_GLOBAL_COOLDOWN,
44        Property::PeriodicAmount => EffectFamily::MODIFIER_PERIODIC_AMOUNT,
45        Property::Effect3 => EffectFamily::MODIFIER_EFFECT_3,
46        Property::Coefficient => EffectFamily::MODIFIER_COEFFICIENT,
47        Property::ProcFrequency => EffectFamily::MODIFIER_PROC_FREQUENCY,
48        Property::Amplitude => EffectFamily::MODIFIER_AMPLITUDE,
49        Property::DispelResistance => EffectFamily::MODIFIER_DISPEL_RESISTANCE,
50        Property::Doses => EffectFamily::MODIFIER_DOSES,
51        Property::Effect4 => EffectFamily::MODIFIER_EFFECT_4,
52        Property::Effect5 => EffectFamily::MODIFIER_EFFECT_5,
53        Property::ResourceCost2 => EffectFamily::MODIFIER_RESOURCE_COST_2,
54        Property::ChainTargetRange => EffectFamily::MODIFIER_CHAIN_TARGET_RANGE,
55        Property::MaxStacks => EffectFamily::MODIFIER_MAX_STACKS,
56        Property::ProcCooldown => EffectFamily::MODIFIER_PROC_COOLDOWN,
57        Property::ResourceCost3 => EffectFamily::MODIFIER_RESOURCE_COST_3,
58        Property::MaxTargets => EffectFamily::MODIFIER_MAX_TARGETS,
59        _ => EffectFamily::empty(),
60    }
61}
62
63fn targeting_property_family(
64    property: wowlab_engine_domain::dbc::ModifierPropertyKind,
65) -> EffectFamily {
66    use wowlab_engine_domain::dbc::ModifierPropertyKind as Property;
67
68    match property {
69        Property::Range => EffectFamily::TARGETING_RANGE,
70        Property::Radius => EffectFamily::TARGETING_RADIUS,
71        Property::ChainTargetRange => EffectFamily::TARGETING_CHAIN_RANGE,
72        Property::MaxTargets => EffectFamily::TARGETING_MAX_TARGETS,
73        _ => EffectFamily::empty(),
74    }
75}
76
77fn resource_cost_property_family(cost_entry: u8) -> EffectFamily {
78    match cost_entry {
79        0 => EffectFamily::MODIFIER_RESOURCE_COST_1,
80        1 => EffectFamily::MODIFIER_RESOURCE_COST_2,
81        2 => EffectFamily::MODIFIER_RESOURCE_COST_3,
82        _ => EffectFamily::empty(),
83    }
84}
85
86pub const MAX_AURA_BUFF_EFFECTS: usize = 16;
87
88bitflags::bitflags! {
89    /// Finalized runtime capabilities contributed by one or more buff effects.
90    #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
91    pub(crate) struct EffectFamily: u128 {
92        const AUTO_ATTACK_REPLACEMENT = 1 << 0;
93        const GENERAL_TOTALS = 1 << 1;
94        const STATIC_TOTALS = 1 << 2;
95        const DYNAMIC_TOTALS = 1 << 3;
96        const SPELL_MODIFIER = 1 << 4;
97        const SCHOOL_OVERRIDE = 1 << 5;
98        const STANCE_EXCEPTION = 1 << 6;
99        const TARGETING = 1 << 7;
100        const TARGETING_RANGE = 1 << 8;
101        const TARGETING_RADIUS = 1 << 9;
102        const TARGETING_CHAIN_RANGE = 1 << 10;
103        const TARGETING_MAX_TARGETS = 1 << 11;
104        const COOLDOWN_RECHARGE_RATE = 1 << 12;
105        const COOLDOWN_DURATION = 1 << 13;
106        const MAXIMUM_CHARGES = 1 << 14;
107        const IGNORED_CHARGE_COOLDOWN = 1 << 15;
108        const RESOURCE_COST = 1 << 16;
109        const RESOURCE_REGENERATION = 1 << 17;
110        const RESOURCE_PROJECTION = 1 << 18;
111        const AURA_TIME_RATE = 1 << 19;
112        const HEALTH_DEPENDENT_CRITICAL_CHANCE = 1 << 20;
113        const SPELL_GATE = 1 << 21;
114        const DAMAGE_REPLICATION = 1 << 22;
115        const HEALTH_THRESHOLD_TRIGGER = 1 << 23;
116        const CAST_TIME = 1 << 24;
117        const GLOBAL_COOLDOWN = 1 << 25;
118        const AURA_DURATION = 1 << 26;
119        const PERIODIC_INTERVAL = 1 << 27;
120        const PUSHBACK_RESISTANCE = 1 << 28;
121        const ARMOR_BYPASS = 1 << 29;
122        const REFLECTION = 1 << 30;
123        const DEFLECTION = 1 << 31;
124        const PLAYER_ATTACK_SPEED = 1 << 32;
125        const PET_GUARDIAN_SPEED = 1 << 33;
126        const AUTO_ATTACK_DAMAGE = 1 << 34;
127        const AUTO_ATTACK_CRITICAL_CHANCE = 1 << 35;
128        const RESOURCE_GAIN = 1 << 36;
129        const STATS = 1 << 37;
130        const OUTGOING_DAMAGE = 1 << 38;
131        const INCOMING_DAMAGE = 1 << 39;
132        const HEALING = 1 << 40;
133        const ABSORB = 1 << 41;
134        const DAMAGE_TRANSFER = 1 << 42;
135        const IMMUNITY = 1 << 43;
136        const LEECH = 1 << 44;
137
138        const MODIFIER_GENERIC_DAMAGE = 1 << 48;
139        const MODIFIER_DURATION = 1 << 49;
140        const MODIFIER_THREAT = 1 << 50;
141        const MODIFIER_EFFECT_1 = 1 << 51;
142        const MODIFIER_PROC_CHARGES = 1 << 52;
143        const MODIFIER_RANGE = 1 << 53;
144        const MODIFIER_RADIUS = 1 << 54;
145        const MODIFIER_CRITICAL_CHANCE = 1 << 55;
146        const MODIFIER_POINTS = 1 << 56;
147        const MODIFIER_PUSHBACK_RESISTANCE = 1 << 57;
148        const MODIFIER_CAST_TIME = 1 << 58;
149        const MODIFIER_COOLDOWN = 1 << 59;
150        const MODIFIER_EFFECT_2 = 1 << 60;
151        const MODIFIER_TARGET_RESISTANCE = 1 << 61;
152        const MODIFIER_RESOURCE_COST_1 = 1 << 62;
153        const MODIFIER_CRITICAL_DAMAGE = 1 << 63;
154        const MODIFIER_HIT_CHANCE = 1 << 64;
155        const MODIFIER_CHAIN_TARGETS = 1 << 65;
156        const MODIFIER_PROC_CHANCE = 1 << 66;
157        const MODIFIER_TICK_TIME = 1 << 67;
158        const MODIFIER_CHAIN_MULTIPLIER = 1 << 68;
159        const MODIFIER_GLOBAL_COOLDOWN = 1 << 69;
160        const MODIFIER_PERIODIC_AMOUNT = 1 << 70;
161        const MODIFIER_EFFECT_3 = 1 << 71;
162        const MODIFIER_COEFFICIENT = 1 << 72;
163        const MODIFIER_PROC_FREQUENCY = 1 << 73;
164        const MODIFIER_AMPLITUDE = 1 << 74;
165        const MODIFIER_DISPEL_RESISTANCE = 1 << 75;
166        const MODIFIER_DOSES = 1 << 76;
167        const MODIFIER_EFFECT_4 = 1 << 77;
168        const MODIFIER_EFFECT_5 = 1 << 78;
169        const MODIFIER_RESOURCE_COST_2 = 1 << 79;
170        const MODIFIER_CHAIN_TARGET_RANGE = 1 << 80;
171        const MODIFIER_MAX_STACKS = 1 << 81;
172        const MODIFIER_PROC_COOLDOWN = 1 << 82;
173        const MODIFIER_RESOURCE_COST_3 = 1 << 83;
174        const MODIFIER_MAX_TARGETS = 1 << 84;
175    }
176}
177
178bitflags::bitflags! {
179    #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
180    pub(crate) struct EffectConsumer: u16 {
181        const GENERAL_TOTALS = 1 << 0;
182        const DYNAMIC_TOTALS = 1 << 1;
183        const SPELL_MODIFIERS = 1 << 2;
184        const COSTS = 1 << 3;
185        const TIMING = 1 << 4;
186        const TARGETING = 1 << 5;
187        const GATES = 1 << 6;
188        const RECHARGE_CHARGES = 1 << 7;
189        const STATS = 1 << 8;
190        const SPEED_PROJECTION = 1 << 9;
191        const SNAPSHOT_TOTALS = 1 << 10;
192    }
193}
194
195/// Exhaustive semantic classification of one [`BuffEffect`] variant.
196#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
197pub(crate) struct EffectClassification {
198    families: EffectFamily,
199    consumers: EffectConsumer,
200}
201
202impl EffectClassification {
203    const fn new(families: EffectFamily, consumers: EffectConsumer) -> Self {
204        Self {
205            families,
206            consumers,
207        }
208    }
209
210    pub(crate) const fn families(self) -> EffectFamily {
211        self.families
212    }
213
214    pub(crate) const fn consumers(self) -> EffectConsumer {
215        self.consumers
216    }
217}
218
219/// Returned by [`try_push_effect`] when an aura's fixed effect array is already full.
220#[derive(Clone, Copy, Debug)]
221pub struct BuffEffectsFull;
222
223/// Push `effect` into the first empty slot of a fixed aura-effect array, erroring when full.
224///
225/// # Errors
226///
227/// Returns `BuffEffectsFull` when all effect slots are occupied.
228pub fn try_push_effect(
229    effects: &mut [Option<BuffEffect>; MAX_AURA_BUFF_EFFECTS],
230    effect: BuffEffect,
231) -> Result<(), BuffEffectsFull> {
232    for slot in effects.iter_mut() {
233        if slot.is_none() {
234            *slot = Some(effect);
235
236            return Ok(());
237        }
238    }
239
240    Err(BuffEffectsFull)
241}
242
243/// A single per-stack buff contributed by an active aura.
244#[derive(Clone, Copy, Debug)]
245#[non_exhaustive]
246pub enum BuffEffect {
247    Haste(f64),
248    /// Multiplicative haste source whose per-stack percent points add (aura 193).
249    HasteMult(f64),
250    /// DBC-derived multiplicative haste source retaining its coordinate for content masking.
251    HasteMultFromEffect {
252        percent: f64,
253        source_spell_id: u32,
254        effect_index: u8,
255    },
256    /// Multiplicative spell cast-speed percent per stack (aura 355).
257    SpellCastSpeed(f64),
258    /// Percent points of auto-attack speed per stack (auras 319/342).
259    AttackSpeed(f64),
260    /// Percent points of extra auto-attack damage per stack (dynamic aura 344).
261    AutoAttackDamage(f64),
262    /// Percent points of extra auto-attack damage taken from the caster (aura 343).
263    AutoAttackDamageTaken(f64),
264    /// Additive critical-strike chance for this actor's white auto-attacks (aura 334).
265    AutoAttackCritChance(f64),
266    /// Critical chance while the hostile target is at or above the DBC threshold (aura 183).
267    CritChanceVersusTargetHealth {
268        percent: f64,
269        threshold_pct: f64,
270    },
271    /// Main-hand white swings execute this spell while the owning aura is active (aura 361).
272    AutoAttackReplacement(u32),
273    /// Cast a spell when the affected actor crosses a health threshold (aura 468).
274    HealthThresholdTrigger {
275        threshold_pct: f64,
276        direction: HealthThresholdDirection,
277        spell_id: u32,
278        scheduled: Option<wowlab_engine_ports::ScheduledSpellBinding>,
279    },
280    /// Percent of the affected target's armor bypassed by this aura's caster (aura 345).
281    ArmorBypassFromCaster(f64),
282    /// Temporary spell-school override filtered through the aura effect's affect list (aura 220).
283    SpellSchoolFromEffect {
284        school: DamageSchool,
285        source_spell_id: u32,
286        effect_index: u8,
287    },
288    /// Wall-clock duration/cadence multiplier from aura 399 or label-scoped aura 470.
289    AuraTimeRateFromEffect {
290        percent: f64,
291        source_spell_id: u32,
292        effect_index: u8,
293    },
294    SpellReflection(wowlab_engine_domain::dbc::SpellSchoolMask),
295    SpellDeflectChance(f64),
296    /// Aura 213 resource-gain multiplier.
297    ResourceGainMult(f64),
298    /// Percent points of extra Rage gain while active (aura 213).
299    RageGainMult(f64),
300    /// Flat regeneration per five seconds for the named DBC power type (aura 85).
301    ResourceRegenPerFiveSeconds(f64, wowlab_types::combat::ResourceType),
302    /// Multiplicative incoming-damage percent filtered by DBC school mask (aura 87).
303    IncomingDamagePercent {
304        percent: f64,
305        schools: wowlab_engine_domain::dbc::SpellSchoolMask,
306    },
307    /// Multiplicative incoming area-damage percent filtered by DBC school mask (aura 229).
308    IncomingAoeDamagePercent {
309        percent: f64,
310        schools: wowlab_engine_domain::dbc::SpellSchoolMask,
311    },
312    /// Flat source-side healing bonus filtered by spell school (auras 115/135).
313    HealingDoneFlat {
314        amount: f64,
315        schools: wowlab_engine_domain::dbc::SpellSchoolMask,
316    },
317    /// Multiplicative source-side healing bonus (aura 136).
318    HealingDonePercent(f64),
319    /// Multiplicative target-side healing received bonus (aura 118).
320    HealingReceivedPercent(f64),
321    /// Healing received from this aura's caster only (aura 283).
322    HealingReceivedFromCasterPercent(f64),
323    /// Multiplicative target-side periodic-healing bonus (aura 259).
324    PeriodicHealingPercent(f64),
325    /// Damage-to-healing percentage (aura 443).
326    LeechPercent(f64),
327    /// Source-side absorb amount multiplier (aura 421).
328    AbsorbDonePercent(f64),
329    /// Target-side absorb amount multiplier (aura 422).
330    AbsorbTakenPercent(f64),
331    /// Mutable damage/healing/overkill absorb initialized when the aura is applied.
332    Absorb(AbsorbEffect),
333    /// Aura 81/300 target-side damage split/share.
334    DamageTransfer(DamageTransfer),
335    /// Complete incoming-damage immunity for the named schools (aura 39).
336    SchoolImmunity(wowlab_engine_domain::dbc::SpellSchoolMask),
337    /// Complete immunity for incoming effects carrying any named mechanic bit (auras 77/147).
338    MechanicImmunity(u64),
339    /// Marks power regeneration as interrupted (aura 94).
340    InterruptRegen,
341    /// Percent of normal regeneration retained while interrupted (aura 134).
342    InterruptedRegenPercent(f64),
343    /// DBC power-type bit mask whose regeneration is prevented (aura 294).
344    PreventResourceRegen(u64),
345    /// Additive resource-cost percent for the named DBC power type (aura 423).
346    ResourceCostPercent(f64, wowlab_types::combat::ResourceType),
347    Crit(f64),
348    /// DBC-derived critical-strike chance retaining its source coordinate for content masking.
349    CritFromEffect {
350        percent: f64,
351        source_spell_id: u32,
352        effect_index: u8,
353    },
354    /// Critical-strike chance whose magnitude is unchanged by aura charge stacks.
355    CritActive(f64),
356    Mastery(f64),
357    Versatility(f64),
358    PrimaryStat(f64),
359    PrimaryStatPercent(f64),
360    /// Aura 467 percentage applied to positive non-create stat gains.
361    BonusStatPercent {
362        percent: f64,
363        stat: wowlab_engine_domain::dbc::StatModifierKind,
364    },
365    /// Aura 366 spell-power replacement sourced from pre-override attack power.
366    OverrideSpellPowerByAttackPowerPercent(f64),
367    /// Aura 404 attack-power replacement sourced from pre-override spell power.
368    OverrideAttackPowerBySpellPowerPercent(f64),
369    AttackPowerPercent(f64),
370    DamageMult(f64),
371    PetDamageMult(f64),
372    DynamicDamageMult(
373        fn(
374            &super::CombatProgram,
375            &super::CombatInstance,
376            Option<wowlab_types::sim::EnemyIdx>,
377            wowlab_types::sim::SimTime,
378        ) -> f64,
379    ),
380    DynamicDamageMultSpells(
381        fn(
382            &super::CombatProgram,
383            &super::CombatInstance,
384            Option<wowlab_types::sim::EnemyIdx>,
385            wowlab_types::sim::SimTime,
386        ) -> f64,
387        &'static [u32],
388    ),
389    DynamicRegenPercent(fn(&super::CombatProgram, &super::CombatInstance) -> f64),
390    DamageMultSchool(f64, DamageSchool),
391    DamageMultSpells(f64, &'static [u32]),
392    /// Spell-scoped multiplier whose magnitude is unchanged by aura charge stacks.
393    DamageMultSpellsActive(f64, &'static [u32]),
394    /// Aura 249: source multiplier while the target carries an aura tagged with `mechanic_mask`.
395    DamageToTargetAuraMechanicPercent {
396        percent: f64,
397        mechanic_mask: wowlab_engine_domain::dbc::MechanicMask,
398    },
399    /// Aura 255: target multiplier for damage spells tagged with `mechanic_mask`.
400    MechanicDamageTakenPercent {
401        percent: f64,
402        mechanic_mask: wowlab_engine_domain::dbc::MechanicMask,
403    },
404    /// Aura 310: target multiplier for area damage filtered by school.
405    CreatureAoeDamageAvoidance {
406        percent: f64,
407        schools: wowlab_engine_domain::dbc::SpellSchoolMask,
408    },
409    SpellDamageFromEffect {
410        percent: f64,
411        source_spell_id: u32,
412        effect_index: u8,
413        periodic: bool,
414    },
415    /// Active affected-spell modifier for runtime values outside the damage-point family.
416    SpellModifierFromEffect {
417        value: f64,
418        source_spell_id: u32,
419        effect_index: u8,
420        property: wowlab_engine_domain::dbc::ModifierPropertyKind,
421        operation: ModifierOperation,
422    },
423    SpellCritChanceFromEffect {
424        percent: f64,
425        source_spell_id: u32,
426        effect_index: u8,
427    },
428    SpellCritDamageFromEffect {
429        percent: f64,
430        source_spell_id: u32,
431        effect_index: u8,
432    },
433    SpellGateFromEffect {
434        source_spell_id: u32,
435        effect_index: u8,
436        gate: ActiveSpellGate,
437    },
438    /// Active aura-275 stance-mask exception filtered through its DBC affect list.
439    SpellStanceMaskFromEffect {
440        source_spell_id: u32,
441        effect_index: u8,
442    },
443    DamageTakenFromSource(f64, DamageSource),
444    DamageTakenFromSourceSpells {
445        percent: f64,
446        source_spell_id: u32,
447        effect_index: u8,
448        source: DamageSource,
449    },
450    DamageMultSpellsLinear(f64, &'static [u32]),
451    /// Buff-borne flat aura-107 `P_CRIT` (misc 7), e.g. Killing Machine (51124)
452    CritChanceSpells(f64, &'static [u32]),
453    /// Buff-borne aura-108 `P_CRIT_BONUS` (misc 15): Killing Machine e4 (+25%).
454    CritDamageSpells(f64, &'static [u32]),
455    SpellCostPercent(f64, &'static [u32], super::ResourcePool),
456    SpellCostFlat(f64, &'static [u32], super::ResourcePool),
457    SpellCostFromEffect {
458        value: f64,
459        source_spell_id: u32,
460        effect_index: u8,
461        cost_entry: u8,
462        operation: ModifierOperation,
463    },
464    /// Active aura-63 flat adjustment to an affected spell's optional power cost.
465    AdditionalPowerCostFromEffect {
466        value: f64,
467        source_spell_id: u32,
468        effect_index: u8,
469        resource_type: i32,
470    },
471    RechargeRatePercent(f64, &'static [u32]),
472    /// Active DBC aura-286 cooldown-recharge speed filtered through its affect list.
473    CooldownRechargeRateFromEffect {
474        percent: f64,
475        source_spell_id: u32,
476        effect_index: u8,
477    },
478    /// Active DBC aura-148 charge-recharge speed for one charge category.
479    ChargeRechargeRateCategory {
480        percent: f64,
481        category: i32,
482        source_spell_id: u32,
483        effect_index: u8,
484    },
485    /// Extra maximum charges for a charge category while active (aura 411).
486    MaxChargesCategory(f64, i32),
487    /// Casts do not consume charges from this category while active (aura 500).
488    IgnoreChargeCooldownCategory(i32),
489    CooldownDurationPercent {
490        percent: f64,
491        source_spell_id: u32,
492        effect_index: u8,
493    },
494    SpellCastTimeFromEffect {
495        value: f64,
496        source_spell_id: u32,
497        effect_index: u8,
498        operation: ModifierOperation,
499    },
500    SpellGcdFromEffect {
501        value: f64,
502        source_spell_id: u32,
503        effect_index: u8,
504        operation: ModifierOperation,
505    },
506    SpellDurationFromEffect {
507        value: f64,
508        source_spell_id: u32,
509        effect_index: u8,
510        operation: ModifierOperation,
511    },
512    SpellTickTimeFromEffect {
513        value: f64,
514        source_spell_id: u32,
515        effect_index: u8,
516        operation: ModifierOperation,
517    },
518    CastWhileMovingFromEffect {
519        source_spell_id: u32,
520        effect_index: u8,
521    },
522    ResistPushbackFromEffect {
523        value: f64,
524        source_spell_id: u32,
525        effect_index: u8,
526        operation: ModifierOperation,
527    },
528    SpellTargetingFromEffect {
529        value: f64,
530        source_spell_id: u32,
531        effect_index: u8,
532        property: wowlab_engine_domain::dbc::ModifierPropertyKind,
533        operation: ModifierOperation,
534    },
535    /// The magnitude is aura-wide rather than per-stack.
536    CastTimePercent(f64, &'static [u32]),
537    CastTimePercentPerStack(f64, &'static [u32]),
538    /// Like [`BuffEffect::CastTimePercent`], the magnitude does not scale with stacks.
539    GcdPercent(f64, &'static [u32]),
540    DamageReplication(DamageReplication),
541    DamageMultStacking {
542        initial: f64,
543        per_stack: f64,
544    },
545    RegenPercent(f64),
546    MaxResourceFlat(f64),
547}
548
549impl BuffEffect {
550    /// The DBC spell effect this row was derived from, when the shared resolution built it from data.
551    ///
552    /// Content-declared rows carry no DBC provenance and return `None`.
553    #[inline]
554    #[must_use]
555    pub fn dbc_source(&self) -> Option<(u32, u8)> {
556        match self {
557            Self::SpellSchoolFromEffect {
558                source_spell_id,
559                effect_index,
560                ..
561            }
562            | Self::AuraTimeRateFromEffect {
563                source_spell_id,
564                effect_index,
565                ..
566            }
567            | Self::HasteMultFromEffect {
568                source_spell_id,
569                effect_index,
570                ..
571            }
572            | Self::CritFromEffect {
573                source_spell_id,
574                effect_index,
575                ..
576            }
577            | Self::SpellDamageFromEffect {
578                source_spell_id,
579                effect_index,
580                ..
581            }
582            | Self::SpellModifierFromEffect {
583                source_spell_id,
584                effect_index,
585                ..
586            }
587            | Self::SpellCritChanceFromEffect {
588                source_spell_id,
589                effect_index,
590                ..
591            }
592            | Self::SpellCritDamageFromEffect {
593                source_spell_id,
594                effect_index,
595                ..
596            }
597            | Self::SpellGateFromEffect {
598                source_spell_id,
599                effect_index,
600                ..
601            }
602            | Self::SpellStanceMaskFromEffect {
603                source_spell_id,
604                effect_index,
605                ..
606            }
607            | Self::DamageTakenFromSourceSpells {
608                source_spell_id,
609                effect_index,
610                ..
611            }
612            | Self::SpellCostFromEffect {
613                source_spell_id,
614                effect_index,
615                ..
616            }
617            | Self::AdditionalPowerCostFromEffect {
618                source_spell_id,
619                effect_index,
620                ..
621            }
622            | Self::CooldownRechargeRateFromEffect {
623                source_spell_id,
624                effect_index,
625                ..
626            }
627            | Self::ChargeRechargeRateCategory {
628                source_spell_id,
629                effect_index,
630                ..
631            }
632            | Self::CooldownDurationPercent {
633                source_spell_id,
634                effect_index,
635                ..
636            }
637            | Self::SpellCastTimeFromEffect {
638                source_spell_id,
639                effect_index,
640                ..
641            }
642            | Self::SpellGcdFromEffect {
643                source_spell_id,
644                effect_index,
645                ..
646            }
647            | Self::SpellDurationFromEffect {
648                source_spell_id,
649                effect_index,
650                ..
651            }
652            | Self::SpellTickTimeFromEffect {
653                source_spell_id,
654                effect_index,
655                ..
656            }
657            | Self::CastWhileMovingFromEffect {
658                source_spell_id,
659                effect_index,
660                ..
661            }
662            | Self::ResistPushbackFromEffect {
663                source_spell_id,
664                effect_index,
665                ..
666            }
667            | Self::SpellTargetingFromEffect {
668                source_spell_id,
669                effect_index,
670                ..
671            } => Some((*source_spell_id, *effect_index)),
672            _ => None,
673        }
674    }
675
676    /// Resolve the effect at the active aura's stack count.
677    ///
678    /// # Panics
679    ///
680    /// Panics if `unscaled_dbc_effect` omits an effect variant classified as unscaled, indicating an internal dispatch bug.
681    // docref:start combat-formulas-buff-effect-scaled
682    #[inline]
683    // #t(fn: rust_cyclomatic_complexity) flat per-variant dispatch; grows with BuffEffect variants
684    // #t(fn: rust_max_fn_lines) flat exhaustive variant dispatch stays easier to audit as one match
685    #[must_use]
686    pub fn scaled(self, stacks: f64) -> Self {
687        if let Some(scaled) = self.unscaled_dbc_effect() {
688            return scaled;
689        }
690        match self {
691            Self::Haste(v) => Self::Haste(v * stacks),
692            Self::HasteMult(v) => Self::HasteMult(v * stacks),
693            Self::HasteMultFromEffect {
694                percent,
695                source_spell_id,
696                effect_index,
697            } => Self::HasteMultFromEffect {
698                percent: percent * stacks,
699                source_spell_id,
700                effect_index,
701            },
702            Self::SpellCastSpeed(v) => Self::SpellCastSpeed(stacked_percent_multiplier(v, stacks)),
703            Self::AttackSpeed(v) => Self::AttackSpeed(v * stacks),
704            Self::AutoAttackDamage(v) => Self::AutoAttackDamage(v * stacks),
705            Self::AutoAttackDamageTaken(v) => Self::AutoAttackDamageTaken(v * stacks),
706            Self::AutoAttackCritChance(v) => Self::AutoAttackCritChance(v * stacks),
707            Self::CritChanceVersusTargetHealth {
708                percent,
709                threshold_pct,
710            } => Self::CritChanceVersusTargetHealth {
711                percent: percent * stacks,
712                threshold_pct,
713            },
714            Self::ArmorBypassFromCaster(v) => Self::ArmorBypassFromCaster(v * stacks),
715            Self::ResourceGainMult(v) => Self::ResourceGainMult(v * stacks),
716            Self::RageGainMult(v) => Self::RageGainMult(v * stacks),
717            Self::ResourceRegenPerFiveSeconds(value, resource_type) => {
718                Self::ResourceRegenPerFiveSeconds(value * stacks, resource_type)
719            }
720            Self::IncomingDamagePercent { percent, schools } => Self::IncomingDamagePercent {
721                percent: percent * stacks,
722                schools,
723            },
724            Self::IncomingAoeDamagePercent { percent, schools } => Self::IncomingAoeDamagePercent {
725                percent: percent * stacks,
726                schools,
727            },
728            Self::HealingDoneFlat { amount, schools } => Self::HealingDoneFlat {
729                amount: amount * stacks,
730                schools,
731            },
732            Self::HealingDonePercent(v) => Self::HealingDonePercent(v * stacks),
733            Self::HealingReceivedPercent(v) => Self::HealingReceivedPercent(v * stacks),
734            Self::HealingReceivedFromCasterPercent(v) => {
735                Self::HealingReceivedFromCasterPercent(v * stacks)
736            }
737            Self::PeriodicHealingPercent(v) => Self::PeriodicHealingPercent(v * stacks),
738            Self::LeechPercent(v) => Self::LeechPercent(v * stacks),
739            Self::AbsorbDonePercent(v) => Self::AbsorbDonePercent(v * stacks),
740            Self::AbsorbTakenPercent(v) => Self::AbsorbTakenPercent(v * stacks),
741            Self::Absorb(effect) => Self::Absorb(AbsorbEffect {
742                amount: effect.amount * stacks,
743                ..effect
744            }),
745            Self::DamageTransfer(transfer) => Self::DamageTransfer(transfer),
746            Self::SchoolImmunity(schools) => Self::SchoolImmunity(schools),
747            Self::MechanicImmunity(mask) => Self::MechanicImmunity(mask),
748            Self::InterruptRegen => Self::InterruptRegen,
749            Self::InterruptedRegenPercent(percent) => {
750                Self::InterruptedRegenPercent(percent * stacks)
751            }
752            Self::PreventResourceRegen(mask) => Self::PreventResourceRegen(mask),
753            Self::ResourceCostPercent(value, resource_type) => {
754                Self::ResourceCostPercent(value * stacks, resource_type)
755            }
756            Self::Crit(v) => Self::Crit(v * stacks),
757            Self::CritFromEffect {
758                percent,
759                source_spell_id,
760                effect_index,
761            } => Self::CritFromEffect {
762                percent: percent * stacks,
763                source_spell_id,
764                effect_index,
765            },
766            Self::CritActive(v) => Self::CritActive(v),
767            Self::Mastery(v) => Self::Mastery(v * stacks),
768            Self::Versatility(v) => Self::Versatility(v * stacks),
769            Self::PrimaryStat(v) => Self::PrimaryStat(v * stacks),
770            Self::PrimaryStatPercent(v) => Self::PrimaryStatPercent(v * stacks),
771            Self::BonusStatPercent { percent, stat } => Self::BonusStatPercent {
772                percent: percent * stacks,
773                stat,
774            },
775            Self::OverrideSpellPowerByAttackPowerPercent(v) => {
776                Self::OverrideSpellPowerByAttackPowerPercent(v * stacks)
777            }
778            Self::OverrideAttackPowerBySpellPowerPercent(v) => {
779                Self::OverrideAttackPowerBySpellPowerPercent(v * stacks)
780            }
781            Self::AttackPowerPercent(v) => Self::AttackPowerPercent(v * stacks),
782            Self::DamageMult(v) => Self::DamageMult(v.powf(stacks)),
783            Self::PetDamageMult(v) => Self::PetDamageMult(v.powf(stacks)),
784            Self::DynamicDamageMult(resolve) => Self::DynamicDamageMult(resolve),
785            Self::DynamicDamageMultSpells(r, s) => Self::DynamicDamageMultSpells(r, s),
786            Self::DynamicRegenPercent(resolve) => Self::DynamicRegenPercent(resolve),
787            Self::DamageMultSchool(v, school) => Self::DamageMultSchool(v.powf(stacks), school),
788            Self::DamageMultSpells(v, spells) => Self::DamageMultSpells(v.powf(stacks), spells),
789            Self::DamageMultSpellsActive(v, spells) => Self::DamageMultSpellsActive(v, spells),
790            Self::DamageToTargetAuraMechanicPercent {
791                percent,
792                mechanic_mask,
793            } => Self::DamageToTargetAuraMechanicPercent {
794                percent: percent * stacks,
795                mechanic_mask,
796            },
797            Self::MechanicDamageTakenPercent {
798                percent,
799                mechanic_mask,
800            } => Self::MechanicDamageTakenPercent {
801                percent: percent * stacks,
802                mechanic_mask,
803            },
804            Self::CreatureAoeDamageAvoidance { percent, schools } => {
805                Self::CreatureAoeDamageAvoidance {
806                    percent: percent * stacks,
807                    schools,
808                }
809            }
810            Self::DamageTakenFromSource(v, source) => {
811                Self::DamageTakenFromSource(v * stacks, source)
812            }
813            Self::DamageMultSpellsLinear(v, spells) => {
814                Self::DamageMultSpellsLinear(v * stacks, spells)
815            }
816            Self::CritChanceSpells(v, spells) => Self::CritChanceSpells(v * stacks, spells),
817            Self::CritDamageSpells(v, spells) => Self::CritDamageSpells(v * stacks, spells),
818            Self::SpellCostPercent(v, spells, pool) => {
819                Self::SpellCostPercent(v * stacks, spells, pool)
820            }
821            Self::SpellCostFlat(v, spells, pool) => Self::SpellCostFlat(v * stacks, spells, pool),
822            Self::SpellCostFromEffect {
823                value,
824                source_spell_id,
825                effect_index,
826                cost_entry,
827                operation,
828            } => Self::SpellCostFromEffect {
829                value,
830                source_spell_id,
831                effect_index,
832                cost_entry,
833                operation,
834            },
835            Self::AdditionalPowerCostFromEffect {
836                value,
837                source_spell_id,
838                effect_index,
839                resource_type,
840            } => Self::AdditionalPowerCostFromEffect {
841                value,
842                source_spell_id,
843                effect_index,
844                resource_type,
845            },
846            Self::RechargeRatePercent(v, spells) => Self::RechargeRatePercent(v * stacks, spells),
847            Self::CooldownRechargeRateFromEffect {
848                percent,
849                source_spell_id,
850                effect_index,
851            } => Self::CooldownRechargeRateFromEffect {
852                percent,
853                source_spell_id,
854                effect_index,
855            },
856            Self::ChargeRechargeRateCategory {
857                percent,
858                category,
859                source_spell_id,
860                effect_index,
861            } => Self::ChargeRechargeRateCategory {
862                percent,
863                category,
864                source_spell_id,
865                effect_index,
866            },
867            Self::MaxChargesCategory(v, category) => Self::MaxChargesCategory(v * stacks, category),
868            Self::IgnoreChargeCooldownCategory(category) => {
869                Self::IgnoreChargeCooldownCategory(category)
870            }
871            Self::CooldownDurationPercent {
872                percent,
873                source_spell_id,
874                effect_index,
875            } => Self::CooldownDurationPercent {
876                percent,
877                source_spell_id,
878                effect_index,
879            },
880            Self::SpellCastTimeFromEffect { .. }
881            | Self::SpellGcdFromEffect { .. }
882            | Self::SpellDurationFromEffect { .. }
883            | Self::SpellTickTimeFromEffect { .. }
884            | Self::SpellModifierFromEffect { .. }
885            | Self::SpellTargetingFromEffect { .. }
886            | Self::SpellSchoolFromEffect { .. }
887            | Self::AuraTimeRateFromEffect { .. }
888            | Self::SpellReflection(..)
889            | Self::AutoAttackReplacement(..)
890            | Self::HealthThresholdTrigger { .. }
891            | Self::SpellDamageFromEffect { .. }
892            | Self::SpellCritChanceFromEffect { .. }
893            | Self::SpellCritDamageFromEffect { .. }
894            | Self::DamageTakenFromSourceSpells { .. }
895            | Self::SpellGateFromEffect { .. }
896            | Self::SpellStanceMaskFromEffect { .. }
897            | Self::CastWhileMovingFromEffect { .. }
898            | Self::ResistPushbackFromEffect { .. } => unreachable!("handled above"),
899            Self::CastTimePercent(v, spells) => Self::CastTimePercent(v, spells),
900            Self::CastTimePercentPerStack(v, spells) => {
901                Self::CastTimePercentPerStack(v * stacks, spells)
902            }
903            Self::GcdPercent(v, spells) => Self::GcdPercent(v, spells),
904            Self::DamageReplication(replication) => Self::DamageReplication(replication),
905            Self::DamageMultStacking { initial, per_stack } => {
906                Self::DamageMult(1.0 + initial + per_stack * (stacks - 1.0))
907            }
908            Self::RegenPercent(v) => Self::RegenPercent(v * stacks),
909            Self::MaxResourceFlat(v) => Self::MaxResourceFlat(v * stacks),
910            Self::SpellDeflectChance(v) => Self::SpellDeflectChance(v * stacks),
911        }
912    }
913    // docref:end combat-formulas-buff-effect-scaled
914
915    /// External identities consumed into dense membership during finalization.
916    #[must_use]
917    pub(crate) const fn listed_spell_ids(&self) -> Option<&'static [u32]> {
918        match self {
919            Self::DynamicDamageMultSpells(_, spells)
920            | Self::DamageMultSpells(_, spells)
921            | Self::DamageMultSpellsActive(_, spells)
922            | Self::DamageMultSpellsLinear(_, spells)
923            | Self::CritChanceSpells(_, spells)
924            | Self::CritDamageSpells(_, spells)
925            | Self::SpellCostPercent(_, spells, _)
926            | Self::SpellCostFlat(_, spells, _)
927            | Self::RechargeRatePercent(_, spells)
928            | Self::CastTimePercent(_, spells)
929            | Self::CastTimePercentPerStack(_, spells)
930            | Self::GcdPercent(_, spells) => Some(spells),
931            _ => None,
932        }
933    }
934    /// Classify this effect for finalized capability and positional-mask plans.
935    /// This exhaustive match makes a new effect variant require an explicit runtime-consumer decision before Engine Combat compiles.
936    // #t(fn: rust_cyclomatic_complexity) exhaustive per-variant classification is the single audited source of effect meaning.
937    // #t(fn: rust_max_fn_lines) keeping the exhaustive taxonomy beside BuffEffect makes omitted variants a compile error.
938    #[must_use]
939    pub(crate) fn classification(&self) -> EffectClassification {
940        use EffectConsumer as Consumer;
941        use EffectFamily as Family;
942
943        let static_total = Consumer::GENERAL_TOTALS;
944        let stat_total = static_total | Consumer::STATS;
945        let speed_total = stat_total | Consumer::SPEED_PROJECTION;
946        let spell_modifier = Consumer::SPELL_MODIFIERS;
947        let timing = Consumer::TIMING;
948        let recharge = Consumer::RECHARGE_CHARGES;
949
950        match self {
951            Self::Haste(..) | Self::HasteMult(..) | Self::HasteMultFromEffect { .. } => {
952                EffectClassification::new(
953                    Family::GENERAL_TOTALS
954                        | Family::STATIC_TOTALS
955                        | Family::STATS
956                        | Family::PLAYER_ATTACK_SPEED
957                        | Family::PET_GUARDIAN_SPEED,
958                    speed_total,
959                )
960            }
961            Self::SpellCastSpeed(..) => EffectClassification::new(
962                Family::GENERAL_TOTALS
963                    | Family::STATIC_TOTALS
964                    | Family::STATS
965                    | Family::PET_GUARDIAN_SPEED,
966                speed_total,
967            ),
968            Self::AttackSpeed(..) => EffectClassification::new(
969                Family::GENERAL_TOTALS
970                    | Family::STATIC_TOTALS
971                    | Family::PLAYER_ATTACK_SPEED
972                    | Family::PET_GUARDIAN_SPEED,
973                static_total | Consumer::SPEED_PROJECTION,
974            ),
975            Self::AutoAttackDamage(..) => EffectClassification::new(
976                Family::GENERAL_TOTALS | Family::STATIC_TOTALS | Family::AUTO_ATTACK_DAMAGE,
977                static_total,
978            ),
979            Self::AutoAttackCritChance(..) => EffectClassification::new(
980                Family::GENERAL_TOTALS
981                    | Family::STATIC_TOTALS
982                    | Family::AUTO_ATTACK_CRITICAL_CHANCE,
983                static_total,
984            ),
985            Self::CritChanceVersusTargetHealth { .. } => EffectClassification::new(
986                Family::HEALTH_DEPENDENT_CRITICAL_CHANCE | Family::SPELL_MODIFIER,
987                spell_modifier,
988            ),
989            Self::AutoAttackReplacement(..) => {
990                EffectClassification::new(Family::AUTO_ATTACK_REPLACEMENT, EffectConsumer::empty())
991            }
992            Self::HealthThresholdTrigger { .. } => {
993                EffectClassification::new(Family::HEALTH_THRESHOLD_TRIGGER, EffectConsumer::empty())
994            }
995            Self::ArmorBypassFromCaster(..) => EffectClassification::new(
996                Family::ARMOR_BYPASS | Family::SPELL_MODIFIER,
997                spell_modifier,
998            ),
999            Self::SpellSchoolFromEffect { .. } => EffectClassification::new(
1000                Family::SCHOOL_OVERRIDE | Family::SPELL_MODIFIER,
1001                spell_modifier,
1002            ),
1003            Self::AuraTimeRateFromEffect { .. } => {
1004                EffectClassification::new(Family::AURA_TIME_RATE, timing)
1005            }
1006            Self::SpellReflection(..) => {
1007                EffectClassification::new(Family::REFLECTION, EffectConsumer::empty())
1008            }
1009            Self::SpellDeflectChance(..) => {
1010                EffectClassification::new(Family::DEFLECTION, EffectConsumer::empty())
1011            }
1012            Self::ResourceGainMult(..) | Self::RageGainMult(..) => EffectClassification::new(
1013                Family::GENERAL_TOTALS | Family::STATIC_TOTALS | Family::RESOURCE_GAIN,
1014                static_total,
1015            ),
1016            Self::ResourceRegenPerFiveSeconds(..)
1017            | Self::InterruptedRegenPercent(..)
1018            | Self::PreventResourceRegen(..)
1019            | Self::InterruptRegen => {
1020                EffectClassification::new(Family::RESOURCE_REGENERATION, EffectConsumer::empty())
1021            }
1022            Self::AutoAttackDamageTaken(..) => EffectClassification::new(
1023                Family::GENERAL_TOTALS
1024                    | Family::STATIC_TOTALS
1025                    | Family::AUTO_ATTACK_DAMAGE
1026                    | Family::INCOMING_DAMAGE,
1027                static_total,
1028            ),
1029            Self::IncomingDamagePercent { .. }
1030            | Self::IncomingAoeDamagePercent { .. }
1031            | Self::DamageTakenFromSource(..)
1032            | Self::DamageTakenFromSourceSpells { .. }
1033            | Self::MechanicDamageTakenPercent { .. }
1034            | Self::CreatureAoeDamageAvoidance { .. } => EffectClassification::new(
1035                Family::INCOMING_DAMAGE | Family::SPELL_MODIFIER,
1036                spell_modifier,
1037            ),
1038            Self::HealingDoneFlat { .. }
1039            | Self::HealingDonePercent(..)
1040            | Self::HealingReceivedPercent(..)
1041            | Self::HealingReceivedFromCasterPercent(..)
1042            | Self::PeriodicHealingPercent(..) => {
1043                EffectClassification::new(Family::HEALING, EffectConsumer::empty())
1044            }
1045            Self::LeechPercent(..) => {
1046                EffectClassification::new(Family::LEECH, EffectConsumer::empty())
1047            }
1048            Self::AbsorbDonePercent(..) | Self::AbsorbTakenPercent(..) | Self::Absorb(..) => {
1049                EffectClassification::new(Family::ABSORB, EffectConsumer::empty())
1050            }
1051            Self::DamageTransfer(..) => {
1052                EffectClassification::new(Family::DAMAGE_TRANSFER, EffectConsumer::empty())
1053            }
1054            Self::SchoolImmunity(..) | Self::MechanicImmunity(..) => {
1055                EffectClassification::new(Family::IMMUNITY, EffectConsumer::empty())
1056            }
1057            Self::ResourceCostPercent(..) => {
1058                EffectClassification::new(Family::RESOURCE_COST, Consumer::COSTS)
1059            }
1060            Self::Crit(..)
1061            | Self::CritFromEffect { .. }
1062            | Self::CritActive(..)
1063            | Self::Mastery(..)
1064            | Self::Versatility(..)
1065            | Self::PrimaryStat(..)
1066            | Self::PrimaryStatPercent(..)
1067            | Self::BonusStatPercent { .. }
1068            | Self::OverrideSpellPowerByAttackPowerPercent(..)
1069            | Self::OverrideAttackPowerBySpellPowerPercent(..)
1070            | Self::AttackPowerPercent(..) => EffectClassification::new(
1071                Family::GENERAL_TOTALS | Family::STATIC_TOTALS | Family::STATS,
1072                stat_total,
1073            ),
1074            Self::DamageMult(..) | Self::PetDamageMult(..) | Self::DamageMultSchool(..) => {
1075                EffectClassification::new(
1076                    Family::GENERAL_TOTALS | Family::STATIC_TOTALS | Family::OUTGOING_DAMAGE,
1077                    static_total,
1078                )
1079            }
1080            Self::DynamicDamageMult(..) | Self::DynamicRegenPercent(..) => {
1081                EffectClassification::new(
1082                    Family::GENERAL_TOTALS | Family::DYNAMIC_TOTALS | Family::OUTGOING_DAMAGE,
1083                    Consumer::GENERAL_TOTALS | Consumer::DYNAMIC_TOTALS | Consumer::SNAPSHOT_TOTALS,
1084                )
1085            }
1086            Self::DynamicDamageMultSpells(..)
1087            | Self::DamageMultSpells(..)
1088            | Self::DamageMultSpellsActive(..)
1089            | Self::DamageToTargetAuraMechanicPercent { .. }
1090            | Self::DamageMultSpellsLinear(..)
1091            | Self::CritChanceSpells(..)
1092            | Self::CritDamageSpells(..) => EffectClassification::new(
1093                Family::SPELL_MODIFIER | Family::OUTGOING_DAMAGE,
1094                spell_modifier,
1095            ),
1096            Self::SpellDamageFromEffect { periodic, .. } => EffectClassification::new(
1097                Family::SPELL_MODIFIER
1098                    | Family::OUTGOING_DAMAGE
1099                    | if *periodic {
1100                        Family::MODIFIER_PERIODIC_AMOUNT
1101                    } else {
1102                        Family::MODIFIER_GENERIC_DAMAGE
1103                    },
1104                spell_modifier,
1105            ),
1106            Self::SpellModifierFromEffect { property, .. } => EffectClassification::new(
1107                Family::SPELL_MODIFIER | modifier_property_family(*property),
1108                spell_modifier,
1109            ),
1110            Self::SpellCritChanceFromEffect { .. } => EffectClassification::new(
1111                Family::SPELL_MODIFIER | Family::MODIFIER_CRITICAL_CHANCE,
1112                spell_modifier,
1113            ),
1114            Self::SpellCritDamageFromEffect { .. } => EffectClassification::new(
1115                Family::SPELL_MODIFIER | Family::MODIFIER_CRITICAL_DAMAGE,
1116                spell_modifier,
1117            ),
1118            Self::SpellGateFromEffect { .. } => {
1119                EffectClassification::new(Family::SPELL_GATE, Consumer::GATES)
1120            }
1121            Self::SpellStanceMaskFromEffect { .. } => EffectClassification::new(
1122                Family::STANCE_EXCEPTION | Family::SPELL_GATE,
1123                Consumer::GATES,
1124            ),
1125            Self::SpellCostPercent(..)
1126            | Self::SpellCostFlat(..)
1127            | Self::AdditionalPowerCostFromEffect { .. } => EffectClassification::new(
1128                Family::RESOURCE_COST | Family::SPELL_MODIFIER,
1129                Consumer::COSTS,
1130            ),
1131            Self::SpellCostFromEffect { cost_entry, .. } => EffectClassification::new(
1132                Family::RESOURCE_COST
1133                    | Family::SPELL_MODIFIER
1134                    | resource_cost_property_family(*cost_entry),
1135                Consumer::COSTS,
1136            ),
1137            Self::RechargeRatePercent(..)
1138            | Self::CooldownRechargeRateFromEffect { .. }
1139            | Self::ChargeRechargeRateCategory { .. } => {
1140                EffectClassification::new(Family::COOLDOWN_RECHARGE_RATE, recharge)
1141            }
1142            Self::MaxChargesCategory(..) => {
1143                EffectClassification::new(Family::MAXIMUM_CHARGES, recharge)
1144            }
1145            Self::IgnoreChargeCooldownCategory(..) => {
1146                EffectClassification::new(Family::IGNORED_CHARGE_COOLDOWN, recharge)
1147            }
1148            Self::CooldownDurationPercent { .. } => EffectClassification::new(
1149                Family::COOLDOWN_DURATION | Family::MODIFIER_COOLDOWN,
1150                timing | recharge,
1151            ),
1152            Self::SpellCastTimeFromEffect { .. } => EffectClassification::new(
1153                Family::SPELL_MODIFIER | Family::CAST_TIME | Family::MODIFIER_CAST_TIME,
1154                timing | spell_modifier,
1155            ),
1156            Self::SpellGcdFromEffect { .. } => EffectClassification::new(
1157                Family::SPELL_MODIFIER | Family::GLOBAL_COOLDOWN | Family::MODIFIER_GLOBAL_COOLDOWN,
1158                timing | spell_modifier,
1159            ),
1160            Self::SpellDurationFromEffect { .. } => EffectClassification::new(
1161                Family::SPELL_MODIFIER | Family::AURA_DURATION | Family::MODIFIER_DURATION,
1162                timing | spell_modifier,
1163            ),
1164            Self::SpellTickTimeFromEffect { .. } => EffectClassification::new(
1165                Family::SPELL_MODIFIER | Family::PERIODIC_INTERVAL | Family::MODIFIER_TICK_TIME,
1166                timing | spell_modifier,
1167            ),
1168            Self::CastWhileMovingFromEffect { .. } => EffectClassification::new(
1169                Family::SPELL_GATE | Family::SPELL_MODIFIER,
1170                Consumer::GATES | spell_modifier,
1171            ),
1172            Self::ResistPushbackFromEffect { .. } => EffectClassification::new(
1173                Family::SPELL_MODIFIER
1174                    | Family::PUSHBACK_RESISTANCE
1175                    | Family::MODIFIER_PUSHBACK_RESISTANCE,
1176                timing | spell_modifier,
1177            ),
1178            Self::SpellTargetingFromEffect { property, .. } => EffectClassification::new(
1179                Family::SPELL_MODIFIER
1180                    | Family::TARGETING
1181                    | modifier_property_family(*property)
1182                    | targeting_property_family(*property),
1183                Consumer::TARGETING | spell_modifier,
1184            ),
1185            Self::CastTimePercent(..) | Self::CastTimePercentPerStack(..) => {
1186                EffectClassification::new(Family::CAST_TIME, timing)
1187            }
1188            Self::GcdPercent(..) => EffectClassification::new(Family::GLOBAL_COOLDOWN, timing),
1189            Self::DamageReplication(..) => {
1190                EffectClassification::new(Family::DAMAGE_REPLICATION, EffectConsumer::empty())
1191            }
1192            Self::DamageMultStacking { .. } => EffectClassification::new(
1193                Family::GENERAL_TOTALS | Family::STATIC_TOTALS | Family::OUTGOING_DAMAGE,
1194                static_total,
1195            ),
1196            Self::RegenPercent(..) => EffectClassification::new(
1197                Family::GENERAL_TOTALS | Family::STATIC_TOTALS | Family::RESOURCE_REGENERATION,
1198                static_total,
1199            ),
1200            Self::MaxResourceFlat(..) => EffectClassification::new(
1201                Family::GENERAL_TOTALS | Family::STATIC_TOTALS | Family::RESOURCE_PROJECTION,
1202                static_total,
1203            ),
1204        }
1205    }
1206
1207    // #t(fn: rust_cyclomatic_complexity) exhaustive per-variant projection preserves which DBC-derived effects can be unscaled.
1208    fn unscaled_dbc_effect(self) -> Option<Self> {
1209        let scaled = match self {
1210            Self::SpellCastTimeFromEffect {
1211                value,
1212                source_spell_id,
1213                effect_index,
1214                operation,
1215            } => Self::SpellCastTimeFromEffect {
1216                value,
1217                source_spell_id,
1218                effect_index,
1219                operation,
1220            },
1221            Self::SpellGcdFromEffect {
1222                value,
1223                source_spell_id,
1224                effect_index,
1225                operation,
1226            } => Self::SpellGcdFromEffect {
1227                value,
1228                source_spell_id,
1229                effect_index,
1230                operation,
1231            },
1232            Self::SpellDurationFromEffect {
1233                value,
1234                source_spell_id,
1235                effect_index,
1236                operation,
1237            } => Self::SpellDurationFromEffect {
1238                value,
1239                source_spell_id,
1240                effect_index,
1241                operation,
1242            },
1243            Self::SpellTickTimeFromEffect {
1244                value,
1245                source_spell_id,
1246                effect_index,
1247                operation,
1248            } => Self::SpellTickTimeFromEffect {
1249                value,
1250                source_spell_id,
1251                effect_index,
1252                operation,
1253            },
1254            Self::CastWhileMovingFromEffect {
1255                source_spell_id,
1256                effect_index,
1257            } => Self::CastWhileMovingFromEffect {
1258                source_spell_id,
1259                effect_index,
1260            },
1261            Self::ResistPushbackFromEffect {
1262                value,
1263                source_spell_id,
1264                effect_index,
1265                operation,
1266            } => Self::ResistPushbackFromEffect {
1267                value,
1268                source_spell_id,
1269                effect_index,
1270                operation,
1271            },
1272            Self::SpellGateFromEffect {
1273                source_spell_id,
1274                effect_index,
1275                gate,
1276            } => Self::SpellGateFromEffect {
1277                source_spell_id,
1278                effect_index,
1279                gate,
1280            },
1281            Self::SpellStanceMaskFromEffect {
1282                source_spell_id,
1283                effect_index,
1284            } => Self::SpellStanceMaskFromEffect {
1285                source_spell_id,
1286                effect_index,
1287            },
1288            Self::SpellModifierFromEffect {
1289                value,
1290                source_spell_id,
1291                effect_index,
1292                property,
1293                operation,
1294            } => Self::SpellModifierFromEffect {
1295                value,
1296                source_spell_id,
1297                effect_index,
1298                property,
1299                operation,
1300            },
1301            Self::SpellTargetingFromEffect {
1302                value,
1303                source_spell_id,
1304                effect_index,
1305                property,
1306                operation,
1307            } => Self::SpellTargetingFromEffect {
1308                value,
1309                source_spell_id,
1310                effect_index,
1311                property,
1312                operation,
1313            },
1314            Self::SpellSchoolFromEffect {
1315                school,
1316                source_spell_id,
1317                effect_index,
1318            } => Self::SpellSchoolFromEffect {
1319                school,
1320                source_spell_id,
1321                effect_index,
1322            },
1323            Self::AuraTimeRateFromEffect {
1324                percent,
1325                source_spell_id,
1326                effect_index,
1327            } => Self::AuraTimeRateFromEffect {
1328                percent,
1329                source_spell_id,
1330                effect_index,
1331            },
1332            Self::SpellReflection(schools) => Self::SpellReflection(schools),
1333            Self::AutoAttackReplacement(spell_id) => Self::AutoAttackReplacement(spell_id),
1334            Self::HealthThresholdTrigger {
1335                threshold_pct,
1336                direction,
1337                spell_id,
1338                scheduled,
1339            } => Self::HealthThresholdTrigger {
1340                threshold_pct,
1341                direction,
1342                spell_id,
1343                scheduled,
1344            },
1345            effect @ (Self::SpellDamageFromEffect { .. }
1346            | Self::SpellCritChanceFromEffect { .. }
1347            | Self::SpellCritDamageFromEffect { .. }
1348            | Self::DamageTakenFromSourceSpells { .. }) => effect,
1349            _ => return None,
1350        };
1351
1352        Some(scaled)
1353    }
1354}
1355
1356#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1357#[cfg(test)]
1358enum ReferenceTotalsRelevance {
1359    None,
1360    Static,
1361    Snapshot,
1362}
1363
1364/// Independent exhaustive oracle for the aura-revision relevance projection.
1365#[cfg(test)]
1366fn reference_recompute_totals_relevance(effect: &BuffEffect) -> ReferenceTotalsRelevance {
1367    use ReferenceTotalsRelevance::{None, Snapshot, Static};
1368
1369    match effect {
1370        BuffEffect::Haste(..)
1371        | BuffEffect::HasteMult(..)
1372        | BuffEffect::HasteMultFromEffect { .. }
1373        | BuffEffect::SpellCastSpeed(..)
1374        | BuffEffect::AttackSpeed(..)
1375        | BuffEffect::AutoAttackDamage(..)
1376        | BuffEffect::AutoAttackDamageTaken(..)
1377        | BuffEffect::AutoAttackCritChance(..)
1378        | BuffEffect::ResourceGainMult(..)
1379        | BuffEffect::RageGainMult(..)
1380        | BuffEffect::Crit(..)
1381        | BuffEffect::CritFromEffect { .. }
1382        | BuffEffect::CritActive(..)
1383        | BuffEffect::Mastery(..)
1384        | BuffEffect::Versatility(..)
1385        | BuffEffect::PrimaryStat(..)
1386        | BuffEffect::PrimaryStatPercent(..)
1387        | BuffEffect::BonusStatPercent { .. }
1388        | BuffEffect::OverrideSpellPowerByAttackPowerPercent(..)
1389        | BuffEffect::OverrideAttackPowerBySpellPowerPercent(..)
1390        | BuffEffect::AttackPowerPercent(..)
1391        | BuffEffect::DamageMult(..)
1392        | BuffEffect::PetDamageMult(..)
1393        | BuffEffect::DamageMultSchool(..)
1394        | BuffEffect::DamageMultStacking { .. }
1395        | BuffEffect::RegenPercent(..)
1396        | BuffEffect::MaxResourceFlat(..) => Static,
1397        BuffEffect::DynamicDamageMult(..) | BuffEffect::DynamicRegenPercent(..) => Snapshot,
1398        BuffEffect::CritChanceVersusTargetHealth { .. }
1399        | BuffEffect::AutoAttackReplacement(..)
1400        | BuffEffect::HealthThresholdTrigger { .. }
1401        | BuffEffect::ArmorBypassFromCaster(..)
1402        | BuffEffect::SpellSchoolFromEffect { .. }
1403        | BuffEffect::AuraTimeRateFromEffect { .. }
1404        | BuffEffect::SpellReflection(..)
1405        | BuffEffect::SpellDeflectChance(..)
1406        | BuffEffect::ResourceRegenPerFiveSeconds(..)
1407        | BuffEffect::IncomingDamagePercent { .. }
1408        | BuffEffect::IncomingAoeDamagePercent { .. }
1409        | BuffEffect::HealingDoneFlat { .. }
1410        | BuffEffect::HealingDonePercent(..)
1411        | BuffEffect::HealingReceivedPercent(..)
1412        | BuffEffect::HealingReceivedFromCasterPercent(..)
1413        | BuffEffect::PeriodicHealingPercent(..)
1414        | BuffEffect::LeechPercent(..)
1415        | BuffEffect::AbsorbDonePercent(..)
1416        | BuffEffect::AbsorbTakenPercent(..)
1417        | BuffEffect::Absorb(..)
1418        | BuffEffect::DamageTransfer(..)
1419        | BuffEffect::SchoolImmunity(..)
1420        | BuffEffect::MechanicImmunity(..)
1421        | BuffEffect::InterruptRegen
1422        | BuffEffect::InterruptedRegenPercent(..)
1423        | BuffEffect::PreventResourceRegen(..)
1424        | BuffEffect::ResourceCostPercent(..)
1425        | BuffEffect::DynamicDamageMultSpells(..)
1426        | BuffEffect::DamageMultSpells(..)
1427        | BuffEffect::DamageMultSpellsActive(..)
1428        | BuffEffect::DamageToTargetAuraMechanicPercent { .. }
1429        | BuffEffect::MechanicDamageTakenPercent { .. }
1430        | BuffEffect::CreatureAoeDamageAvoidance { .. }
1431        | BuffEffect::SpellDamageFromEffect { .. }
1432        | BuffEffect::SpellModifierFromEffect { .. }
1433        | BuffEffect::SpellCritChanceFromEffect { .. }
1434        | BuffEffect::SpellCritDamageFromEffect { .. }
1435        | BuffEffect::SpellGateFromEffect { .. }
1436        | BuffEffect::SpellStanceMaskFromEffect { .. }
1437        | BuffEffect::DamageTakenFromSource(..)
1438        | BuffEffect::DamageTakenFromSourceSpells { .. }
1439        | BuffEffect::DamageMultSpellsLinear(..)
1440        | BuffEffect::CritChanceSpells(..)
1441        | BuffEffect::CritDamageSpells(..)
1442        | BuffEffect::SpellCostPercent(..)
1443        | BuffEffect::SpellCostFlat(..)
1444        | BuffEffect::SpellCostFromEffect { .. }
1445        | BuffEffect::AdditionalPowerCostFromEffect { .. }
1446        | BuffEffect::RechargeRatePercent(..)
1447        | BuffEffect::CooldownRechargeRateFromEffect { .. }
1448        | BuffEffect::ChargeRechargeRateCategory { .. }
1449        | BuffEffect::MaxChargesCategory(..)
1450        | BuffEffect::IgnoreChargeCooldownCategory(..)
1451        | BuffEffect::CooldownDurationPercent { .. }
1452        | BuffEffect::SpellCastTimeFromEffect { .. }
1453        | BuffEffect::SpellGcdFromEffect { .. }
1454        | BuffEffect::SpellDurationFromEffect { .. }
1455        | BuffEffect::SpellTickTimeFromEffect { .. }
1456        | BuffEffect::CastWhileMovingFromEffect { .. }
1457        | BuffEffect::ResistPushbackFromEffect { .. }
1458        | BuffEffect::SpellTargetingFromEffect { .. }
1459        | BuffEffect::CastTimePercent(..)
1460        | BuffEffect::CastTimePercentPerStack(..)
1461        | BuffEffect::GcdPercent(..)
1462        | BuffEffect::DamageReplication(..) => None,
1463    }
1464}
1465
1466/// Number of [`DamageSchool`] variants; sized for indexing `school_damage_mult`.
1467pub(crate) const DAMAGE_SCHOOL_COUNT: usize = DamageSchool::COUNT;
1468const BONUS_STAT_COUNT: usize = 4;
1469
1470/// Accumulated buff totals from all active auras for one frame.
1471#[derive(Clone, Copy, Debug, PartialEq)]
1472pub struct BuffTotals {
1473    pub haste_pct: f64,
1474    pub haste_mult: f64,
1475    pub spell_cast_speed_mult: f64,
1476    pub attack_speed_mult: f64,
1477    pub auto_attack_damage_mult: f64,
1478    pub auto_attack_crit_pct: f64,
1479    pub resource_gain_mult: f64,
1480    pub rage_gain_mult: f64,
1481    pub crit_pct: f64,
1482    pub mastery_pct: f64,
1483    pub vers_pct: f64,
1484    pub primary_stat: f64,
1485    pub primary_stat_pct: f64,
1486    pub bonus_primary_stat_pct: f64,
1487    pub bonus_all_stat_pct: f64,
1488    pub bonus_stat_pct: [f64; BONUS_STAT_COUNT],
1489    pub override_spell_power_by_attack_power_pct: Option<f64>,
1490    pub override_attack_power_by_spell_power_pct: Option<f64>,
1491    pub attack_power_pct: f64,
1492    pub damage_mult: f64,
1493    pub pet_damage_mult: f64,
1494    pub school_damage_mult: [f64; DAMAGE_SCHOOL_COUNT],
1495    pub regen_pct: f64,
1496    pub max_resource_flat: f64,
1497}
1498
1499#[derive(Clone, Copy, Debug, Default, PartialEq)]
1500pub(crate) struct PlayerScalingBuffs {
1501    pub(crate) haste_pct: f64,
1502    pub(crate) haste_mult: f64,
1503    pub(crate) spell_cast_speed_mult: f64,
1504    pub(crate) attack_speed_mult: f64,
1505    pub(crate) crit_pct: f64,
1506}
1507
1508impl From<BuffTotals> for PlayerScalingBuffs {
1509    fn from(totals: BuffTotals) -> Self {
1510        Self {
1511            haste_pct: totals.haste_pct,
1512            haste_mult: totals.haste_mult,
1513            spell_cast_speed_mult: totals.spell_cast_speed_mult,
1514            attack_speed_mult: totals.attack_speed_mult,
1515            crit_pct: totals.crit_pct,
1516        }
1517    }
1518}
1519
1520impl BuffTotals {
1521    // #t(fn: rust_cyclomatic_complexity) flat per-variant dispatch; grows with BuffEffect variants
1522    #[expect(
1523        clippy::match_same_arms,
1524        reason = "separate no-op groups document which subsystem consumes each effect category"
1525    )]
1526    pub fn add(&mut self, effect: BuffEffect) {
1527        match effect {
1528            BuffEffect::Haste(v) => self.haste_pct += v,
1529            BuffEffect::HasteMult(v) => self.haste_mult *= 1.0 + v / HUNDRED,
1530            BuffEffect::HasteMultFromEffect { percent, .. } => {
1531                self.haste_mult *= 1.0 + percent / HUNDRED;
1532            }
1533            BuffEffect::SpellCastSpeed(v) => {
1534                self.spell_cast_speed_mult *= 1.0 + v / HUNDRED;
1535            }
1536            BuffEffect::AttackSpeed(v) => self.attack_speed_mult *= 1.0 + v / HUNDRED,
1537            BuffEffect::AutoAttackDamage(v) | BuffEffect::AutoAttackDamageTaken(v) => {
1538                self.auto_attack_damage_mult *= 1.0 + v / HUNDRED;
1539            }
1540            BuffEffect::AutoAttackCritChance(v) => self.auto_attack_crit_pct += v,
1541            BuffEffect::CritChanceVersusTargetHealth { .. } => {}
1542            BuffEffect::AutoAttackReplacement(..) => {}
1543            BuffEffect::HealthThresholdTrigger { .. } => {}
1544            BuffEffect::ArmorBypassFromCaster(..) => {}
1545            BuffEffect::SpellSchoolFromEffect { .. } => {}
1546            BuffEffect::AuraTimeRateFromEffect { .. } => {}
1547            BuffEffect::SpellReflection(..) | BuffEffect::SpellDeflectChance(..) => {}
1548            BuffEffect::ResourceGainMult(v) => self.resource_gain_mult *= 1.0 + v / HUNDRED,
1549            BuffEffect::RageGainMult(v) => self.rage_gain_mult *= 1.0 + v / HUNDRED,
1550            BuffEffect::Crit(v) => self.crit_pct += v,
1551            BuffEffect::CritFromEffect { percent, .. } => self.crit_pct += percent,
1552            BuffEffect::CritActive(v) => self.crit_pct += v,
1553            BuffEffect::Mastery(v) => self.mastery_pct += v,
1554            BuffEffect::Versatility(v) => self.vers_pct += v,
1555            BuffEffect::PrimaryStat(v) => self.primary_stat += v,
1556            BuffEffect::PrimaryStatPercent(v) => self.primary_stat_pct += v,
1557            BuffEffect::BonusStatPercent { percent, stat } => match stat {
1558                wowlab_engine_domain::dbc::StatModifierKind::Primary => {
1559                    self.bonus_primary_stat_pct += percent;
1560                }
1561                wowlab_engine_domain::dbc::StatModifierKind::All => {
1562                    self.bonus_all_stat_pct += percent;
1563                }
1564                _ => {
1565                    if let Some(index) = stat.stat_index()
1566                        && let Some(total) = self.bonus_stat_pct.get_mut(index)
1567                    {
1568                        *total += percent;
1569                    }
1570                }
1571            },
1572            BuffEffect::OverrideSpellPowerByAttackPowerPercent(v) => {
1573                self.override_spell_power_by_attack_power_pct = Some(v);
1574            }
1575            BuffEffect::OverrideAttackPowerBySpellPowerPercent(v) => {
1576                self.override_attack_power_by_spell_power_pct = Some(v);
1577            }
1578            BuffEffect::AttackPowerPercent(v) => self.attack_power_pct += v,
1579            BuffEffect::DamageMult(v) => self.damage_mult *= v,
1580            BuffEffect::PetDamageMult(v) => self.pet_damage_mult *= v,
1581            // Requires combat state and is handled by `buff_totals`.
1582            BuffEffect::DynamicDamageMult(..)
1583            | BuffEffect::DynamicDamageMultSpells(..)
1584            | BuffEffect::DynamicRegenPercent(..) => {}
1585            BuffEffect::DamageMultSchool(v, school) => {
1586                // #t(rust_unchecked_indexing) school as u8 is bounded by DAMAGE_SCHOOL_COUNT
1587                self.school_damage_mult[school as usize] *= v;
1588            }
1589            // Applied per damage event in `spell_scoped_mods`, not in spell-agnostic totals.
1590            BuffEffect::DamageMultSpells(..)
1591            | BuffEffect::DamageMultSpellsActive(..)
1592            | BuffEffect::SpellDamageFromEffect { .. }
1593            | BuffEffect::SpellModifierFromEffect { .. }
1594            | BuffEffect::SpellCritChanceFromEffect { .. }
1595            | BuffEffect::SpellCritDamageFromEffect { .. }
1596            | BuffEffect::SpellGateFromEffect { .. }
1597            | BuffEffect::SpellStanceMaskFromEffect { .. }
1598            | BuffEffect::DamageTakenFromSource(..)
1599            | BuffEffect::DamageTakenFromSourceSpells { .. }
1600            | BuffEffect::DamageToTargetAuraMechanicPercent { .. }
1601            | BuffEffect::MechanicDamageTakenPercent { .. }
1602            | BuffEffect::CreatureAoeDamageAvoidance { .. }
1603            | BuffEffect::DamageMultSpellsLinear(..)
1604            | BuffEffect::CritChanceSpells(..)
1605            | BuffEffect::CritDamageSpells(..) => {}
1606            // Applied by `effective_spell_cost`, not spell-agnostic stat totals.
1607            BuffEffect::SpellCostPercent(..)
1608            | BuffEffect::SpellCostFlat(..)
1609            | BuffEffect::SpellCostFromEffect { .. }
1610            | BuffEffect::ResourceCostPercent(..)
1611            | BuffEffect::AdditionalPowerCostFromEffect { .. }
1612            | BuffEffect::RechargeRatePercent(..)
1613            | BuffEffect::CooldownRechargeRateFromEffect { .. }
1614            | BuffEffect::ChargeRechargeRateCategory { .. }
1615            | BuffEffect::MaxChargesCategory(..)
1616            | BuffEffect::IgnoreChargeCooldownCategory(..)
1617            | BuffEffect::CooldownDurationPercent { .. }
1618            | BuffEffect::SpellCastTimeFromEffect { .. }
1619            | BuffEffect::SpellGcdFromEffect { .. }
1620            | BuffEffect::SpellDurationFromEffect { .. }
1621            | BuffEffect::SpellTickTimeFromEffect { .. }
1622            | BuffEffect::CastWhileMovingFromEffect { .. }
1623            | BuffEffect::ResistPushbackFromEffect { .. }
1624            | BuffEffect::SpellTargetingFromEffect { .. }
1625            | BuffEffect::CastTimePercent(..)
1626            | BuffEffect::CastTimePercentPerStack(..)
1627            | BuffEffect::GcdPercent(..) => {}
1628            // Applied per damage event in `deal_damage`.
1629            BuffEffect::DamageReplication(..) => {}
1630            // `scaled()` converts this to `DamageMult` before `add` is reached.
1631            BuffEffect::DamageMultStacking { .. } => {}
1632            BuffEffect::RegenPercent(v) => self.regen_pct += v,
1633            BuffEffect::ResourceRegenPerFiveSeconds(..) => {}
1634            BuffEffect::IncomingDamagePercent { .. }
1635            | BuffEffect::IncomingAoeDamagePercent { .. }
1636            | BuffEffect::SchoolImmunity(..)
1637            | BuffEffect::MechanicImmunity(..)
1638            | BuffEffect::InterruptRegen
1639            | BuffEffect::InterruptedRegenPercent(..)
1640            | BuffEffect::PreventResourceRegen(..)
1641            | BuffEffect::HealingDoneFlat { .. }
1642            | BuffEffect::HealingDonePercent(..)
1643            | BuffEffect::HealingReceivedPercent(..)
1644            | BuffEffect::HealingReceivedFromCasterPercent(..)
1645            | BuffEffect::PeriodicHealingPercent(..)
1646            | BuffEffect::LeechPercent(..)
1647            | BuffEffect::AbsorbDonePercent(..)
1648            | BuffEffect::AbsorbTakenPercent(..)
1649            | BuffEffect::Absorb(..)
1650            | BuffEffect::DamageTransfer(..) => {}
1651            BuffEffect::MaxResourceFlat(v) => self.max_resource_flat += v,
1652        }
1653    }
1654
1655    /// Fold one planned total effect directly at its active stack count.
1656    // #t(fn: rust_cyclomatic_complexity) exhaustive fused stack folding keeps every supported total field allocation-free and makes new variants auditable.
1657    pub(crate) fn add_scaled(&mut self, effect: &BuffEffect, stacks: f64) {
1658        match effect {
1659            BuffEffect::Haste(value) => self.haste_pct += value * stacks,
1660            BuffEffect::HasteMult(value) => {
1661                self.haste_mult *= 1.0 + value * stacks / HUNDRED;
1662            }
1663            BuffEffect::HasteMultFromEffect { percent, .. } => {
1664                self.haste_mult *= 1.0 + percent * stacks / HUNDRED;
1665            }
1666            BuffEffect::SpellCastSpeed(value) => {
1667                self.spell_cast_speed_mult *= (1.0 + value / HUNDRED).powf(stacks);
1668            }
1669            BuffEffect::AttackSpeed(value) => {
1670                self.attack_speed_mult *= 1.0 + value * stacks / HUNDRED;
1671            }
1672            BuffEffect::AutoAttackDamage(value) | BuffEffect::AutoAttackDamageTaken(value) => {
1673                self.auto_attack_damage_mult *= 1.0 + value * stacks / HUNDRED;
1674            }
1675            BuffEffect::AutoAttackCritChance(value) => {
1676                self.auto_attack_crit_pct += value * stacks;
1677            }
1678            BuffEffect::ResourceGainMult(value) => {
1679                self.resource_gain_mult *= 1.0 + value * stacks / HUNDRED;
1680            }
1681            BuffEffect::RageGainMult(value) => {
1682                self.rage_gain_mult *= 1.0 + value * stacks / HUNDRED;
1683            }
1684            BuffEffect::Crit(value) => self.crit_pct += value * stacks,
1685            BuffEffect::CritFromEffect { percent, .. } => self.crit_pct += percent * stacks,
1686            BuffEffect::CritActive(value) => self.crit_pct += value,
1687            BuffEffect::Mastery(value) => self.mastery_pct += value * stacks,
1688            BuffEffect::Versatility(value) => self.vers_pct += value * stacks,
1689            BuffEffect::PrimaryStat(value) => self.primary_stat += value * stacks,
1690            BuffEffect::PrimaryStatPercent(value) => self.primary_stat_pct += value * stacks,
1691            BuffEffect::BonusStatPercent { percent, stat } => {
1692                let scaled = percent * stacks;
1693
1694                match stat {
1695                    wowlab_engine_domain::dbc::StatModifierKind::Primary => {
1696                        self.bonus_primary_stat_pct += scaled;
1697                    }
1698                    wowlab_engine_domain::dbc::StatModifierKind::All => {
1699                        self.bonus_all_stat_pct += scaled;
1700                    }
1701                    _ => {
1702                        if let Some(index) = stat.stat_index()
1703                            && let Some(total) = self.bonus_stat_pct.get_mut(index)
1704                        {
1705                            *total += scaled;
1706                        }
1707                    }
1708                }
1709            }
1710            BuffEffect::OverrideSpellPowerByAttackPowerPercent(value) => {
1711                self.override_spell_power_by_attack_power_pct = Some(value * stacks);
1712            }
1713            BuffEffect::OverrideAttackPowerBySpellPowerPercent(value) => {
1714                self.override_attack_power_by_spell_power_pct = Some(value * stacks);
1715            }
1716            BuffEffect::AttackPowerPercent(value) => self.attack_power_pct += value * stacks,
1717            BuffEffect::DamageMult(value) => self.damage_mult *= value.powf(stacks),
1718            BuffEffect::PetDamageMult(value) => self.pet_damage_mult *= value.powf(stacks),
1719            BuffEffect::DamageMultSchool(value, school) => {
1720                // BOUNDS: DamageSchool is the discriminant domain used to size school_damage_mult.
1721                self.school_damage_mult[*school as usize] *= value.powf(stacks);
1722            }
1723            BuffEffect::DamageMultStacking { initial, per_stack } => {
1724                self.damage_mult *= 1.0 + initial + per_stack * (stacks - 1.0);
1725            }
1726            BuffEffect::RegenPercent(value) => self.regen_pct += value * stacks,
1727            BuffEffect::MaxResourceFlat(value) => self.max_resource_flat += value * stacks,
1728            _ => {}
1729        }
1730    }
1731}
1732
1733impl Default for BuffTotals {
1734    fn default() -> Self {
1735        Self {
1736            haste_pct: 0.0,
1737            haste_mult: 1.0,
1738            spell_cast_speed_mult: 1.0,
1739            attack_speed_mult: 1.0,
1740            auto_attack_damage_mult: 1.0,
1741            auto_attack_crit_pct: 0.0,
1742            resource_gain_mult: 1.0,
1743            rage_gain_mult: 1.0,
1744            crit_pct: 0.0,
1745            mastery_pct: 0.0,
1746            vers_pct: 0.0,
1747            primary_stat: 0.0,
1748            primary_stat_pct: 0.0,
1749            bonus_primary_stat_pct: 0.0,
1750            bonus_all_stat_pct: 0.0,
1751            bonus_stat_pct: [0.0; BONUS_STAT_COUNT],
1752            override_spell_power_by_attack_power_pct: None,
1753            override_attack_power_by_spell_power_pct: None,
1754            attack_power_pct: 0.0,
1755            damage_mult: 1.0,
1756            pet_damage_mult: 1.0,
1757            school_damage_mult: [1.0; DAMAGE_SCHOOL_COUNT],
1758            regen_pct: 0.0,
1759            max_resource_flat: 0.0,
1760        }
1761    }
1762}
1763
1764#[cfg(test)]
1765#[path = "buff_effect/tests.rs"]
1766mod tests;