Skip to main content

wowlab_engine_domain/dbc/passives/
damage_proc.rs

1use super::{
2    AuraSubtypeKind, CLASS_FAMILY_FLAGS, HUNDRED, ModifierFilter, ModifierFold, ModifierOperation,
3    ModifierPropertyKind, PassiveQuery, RankedPassive, SpellAttributeKind, SpellDataFlat,
4    aura_kind, aura_subtype_semantic, effect_targets_spell, fold_flat_pct_modifiers, masks_overlap,
5    modified_effect_base_points, modified_effect_value, modifier_property_kind, pct_to_mult,
6    spell_attribute_is,
7};
8
9/// Static damage multipliers a set of passive auras applies to one spell.
10#[derive(Clone, Copy, Debug)]
11pub struct PassiveDamageMods {
12    pub direct_flat: f64,
13    pub direct_flat_mastery_coefficient: f64,
14    pub direct: f64,
15    pub periodic: f64,
16    pub crit_chance: f64,
17    pub crit_chance_mult: f64,
18    pub crit_damage_bonus: f64,
19}
20
21/// Flat percent points and a final multiplier applied to a spell's proc chance.
22#[derive(Clone, Copy, Debug, PartialEq)]
23pub struct PassiveProcChanceMods {
24    pub flat_pct: f64,
25    pub multiplier: f64,
26}
27
28/// Resolve passive spell-modifier property 18 while preserving flat-before-percent order.
29#[must_use]
30pub fn passive_proc_chance_mods(
31    passives: &[RankedPassive],
32    spell: &SpellDataFlat,
33) -> PassiveProcChanceMods {
34    let mut mods = PassiveProcChanceMods {
35        flat_pct: 0.0,
36        multiplier: 1.0,
37    };
38
39    for (source, ranks) in passives {
40        for modifier in &source.effects {
41            if modifier_property_kind(modifier) != Some(ModifierPropertyKind::ProcChance)
42                || !effect_targets_spell(modifier, source, spell)
43            {
44                continue;
45            }
46
47            let amount = modified_effect_base_points(
48                PassiveQuery {
49                    passives,
50                    spell: source,
51                },
52                modifier.index,
53                modifier.base_points,
54            ) * ranks;
55
56            match aura_subtype_semantic(modifier.aura)
57                .and_then(|semantic| semantic.modifier)
58                .map(|(operation, _)| operation)
59            {
60                Some(ModifierOperation::Flat) => mods.flat_pct += amount,
61                Some(ModifierOperation::Percent) => mods.multiplier *= pct_to_mult(amount),
62                _ => {}
63            }
64        }
65    }
66
67    mods
68}
69
70/// Whether `spell` carries any damage-done modifier effect this module models.
71#[must_use]
72pub fn is_damage_modifier_passive(spell: &SpellDataFlat) -> bool {
73    spell.is_passive
74        && spell.effects.iter().any(|effect| match aura_kind(effect) {
75            Some(
76                AuraSubtypeKind::AddPercentModifier
77                | AuraSubtypeKind::AddPercentLabelModifier
78                | AuraSubtypeKind::ModAutoAttackDamage
79                | AuraSubtypeKind::ModPetDamageDone,
80            ) => true,
81            Some(AuraSubtypeKind::AddFlatModifier | AuraSubtypeKind::AddFlatLabelModifier) => {
82                matches!(
83                    modifier_property_kind(effect),
84                    Some(
85                        ModifierPropertyKind::CritChance
86                            | ModifierPropertyKind::CritDamage
87                            | ModifierPropertyKind::GenericDamage
88                            | ModifierPropertyKind::ChainTargets
89                    )
90                )
91            }
92            _ => false,
93        })
94}
95
96/// Returns an effect coefficient after percentage `P_EFFECT_N` modifiers.
97#[must_use]
98// #t(fn: rust_floating_point_eq) DBC zero means the coefficient field is absent and percentage-only folds cannot make it nonzero.
99pub fn modified_effect_coefficient(
100    query: PassiveQuery<'_>,
101    effect_index: i32,
102    coefficient: f64,
103) -> f64 {
104    if coefficient == 0.0 {
105        return 0.0;
106    }
107
108    modified_effect_value(query, effect_index, coefficient, false)
109}
110
111/// Return an effect's value multiplier after property-27 flat and percentage modifiers.
112#[must_use]
113pub fn modified_effect_amplitude(query: PassiveQuery<'_>, initial: f64) -> f64 {
114    let PassiveQuery {
115        passives,
116        spell: target,
117    } = query;
118
119    fold_flat_pct_modifiers(
120        passives,
121        initial,
122        ModifierFold::FlatAndPercent,
123        |source, ranks, modifier| {
124            if modifier_property_kind(modifier) != Some(ModifierPropertyKind::Amplitude)
125                || !effect_targets_spell(modifier, source, target)
126            {
127                return None;
128            }
129
130            Some(
131                modified_effect_base_points(
132                    PassiveQuery {
133                        passives,
134                        spell: source,
135                    },
136                    modifier.index,
137                    modifier.base_points,
138                ) * ranks,
139            )
140        },
141    )
142}
143
144/// Spell-power and attack-power coefficients after property-24 modifiers.
145#[derive(Clone, Copy, Debug, PartialEq)]
146pub struct ModifiedPowerCoefficients {
147    pub spell_power: f64,
148    pub attack_power: f64,
149    /// Additive coefficient per live mastery point from attribute-285 flat modifiers.
150    pub mastery_add: f64,
151}
152
153/// Resolve property 24 against the first populated coefficient field, preserving flat-before-percent order.
154#[must_use]
155// #t(fn: rust_floating_point_eq) DBC zero marks an absent AP/SP coefficient field; only populated fields require a passive fold.
156pub fn modified_power_coefficients(
157    query: PassiveQuery<'_>,
158    effect_index: i32,
159    spell_power: f64,
160    attack_power: f64,
161) -> ModifiedPowerCoefficients {
162    let PassiveQuery {
163        passives,
164        spell: target,
165    } = query;
166    let spell_power = if spell_power == 0.0 {
167        0.0
168    } else {
169        modified_effect_coefficient(query, effect_index, spell_power)
170    };
171    let attack_power = if attack_power == 0.0 {
172        0.0
173    } else {
174        modified_effect_coefficient(query, effect_index, attack_power)
175    };
176    let (initial, modifies_spell_power) = if spell_power.abs() > f64::EPSILON {
177        (spell_power, true)
178    } else if attack_power.abs() > f64::EPSILON {
179        (attack_power, false)
180    } else {
181        return ModifiedPowerCoefficients {
182            spell_power,
183            attack_power,
184            mastery_add: 0.0,
185        };
186    };
187    let mut flat = 0.0;
188    let mut flat_mastery = 0.0;
189    let mut percent = 1.0;
190
191    for (source, ranks) in passives {
192        for modifier in &source.effects {
193            if modifier_property_kind(modifier) != Some(ModifierPropertyKind::Coefficient)
194                || !effect_targets_spell(modifier, source, target)
195            {
196                continue;
197            }
198
199            let amount = modified_effect_base_points(
200                PassiveQuery {
201                    passives,
202                    spell: source,
203                },
204                modifier.index,
205                modifier.base_points,
206            ) * ranks;
207
208            match aura_subtype_semantic(modifier.aura)
209                .and_then(|semantic| semantic.modifier)
210                .map(|(operation, _)| operation)
211            {
212                Some(ModifierOperation::Flat) => {
213                    flat += amount / HUNDRED;
214
215                    if spell_attribute_is(
216                        &source.attributes,
217                        SpellAttributeKind::MasteryAffectsPoints,
218                    ) {
219                        flat_mastery += modified_effect_coefficient(
220                            PassiveQuery {
221                                passives,
222                                spell: source,
223                            },
224                            modifier.index,
225                            modifier.bonus_coefficient,
226                        ) * ranks
227                            / HUNDRED;
228                    }
229                }
230                Some(ModifierOperation::Percent) => percent *= pct_to_mult(amount),
231                None => {}
232            }
233        }
234    }
235
236    let modified = (initial + flat) * percent;
237
238    ModifiedPowerCoefficients {
239        spell_power: if modifies_spell_power {
240            modified
241        } else {
242            spell_power
243        },
244        attack_power: if modifies_spell_power {
245            attack_power
246        } else {
247            modified
248        },
249        mastery_add: flat_mastery * percent,
250    }
251}
252
253/// Returns compound damage and critical-strike modifiers.
254#[must_use]
255pub fn passive_damage_mods(
256    passives: &[RankedPassive],
257    target_class_set: i32,
258    target_class_masks: [i32; CLASS_FAMILY_FLAGS],
259    target_labels: &[i32],
260) -> PassiveDamageMods {
261    let mut crit_damage_flat = 0.0;
262    let mut crit_damage_mult = 1.0;
263    let mut mods = PassiveDamageMods {
264        direct_flat: 0.0,
265        direct_flat_mastery_coefficient: 0.0,
266        direct: 1.0,
267        periodic: 1.0,
268        crit_chance: 0.0,
269        crit_chance_mult: 1.0,
270        crit_damage_bonus: 0.0,
271    };
272
273    for (aura, ranks) in passives {
274        for effect in &aura.effects {
275            let base_points = modified_effect_base_points(
276                PassiveQuery {
277                    passives,
278                    spell: aura,
279                },
280                effect.index,
281                effect.base_points,
282            );
283            let matched = match aura_subtype_semantic(effect.aura)
284                .and_then(|semantic| semantic.modifier)
285                .map(|(_, filter)| filter)
286            {
287                Some(ModifierFilter::ClassFamilyMask) => {
288                    aura.spell_class_set == target_class_set
289                        && masks_overlap(effect, target_class_masks)
290                }
291                Some(ModifierFilter::Label) => target_labels.contains(&effect.misc_value_1),
292                _ => false,
293            };
294
295            if !matched {
296                continue;
297            }
298
299            let operation = aura_subtype_semantic(effect.aura)
300                .and_then(|semantic| semantic.modifier)
301                .map(|(operation, _)| operation);
302
303            match (operation, modifier_property_kind(effect)) {
304                (Some(ModifierOperation::Flat), Some(ModifierPropertyKind::GenericDamage)) => {
305                    mods.direct_flat += base_points * ranks;
306
307                    if spell_attribute_is(
308                        &aura.attributes,
309                        SpellAttributeKind::MasteryAffectsPoints,
310                    ) {
311                        mods.direct_flat_mastery_coefficient += modified_effect_coefficient(
312                            PassiveQuery {
313                                passives,
314                                spell: aura,
315                            },
316                            effect.index,
317                            effect.bonus_coefficient,
318                        ) * ranks;
319                    }
320                }
321                (Some(ModifierOperation::Percent), Some(ModifierPropertyKind::GenericDamage)) => {
322                    mods.direct *= pct_to_mult(base_points * ranks);
323                    // #t(rust_log_in_loop) each contributing passive is a distinct identity needed in trace diagnostics.
324                    tracing::trace!(
325                        source_spell_id = aura.id,
326                        source_spell_name = aura.name.as_str(),
327                        effect_index = effect.index,
328                        percent = base_points * ranks,
329                        running_direct = mods.direct,
330                        "static passive direct-damage contribution"
331                    );
332                }
333                (Some(ModifierOperation::Percent), Some(ModifierPropertyKind::PeriodicAmount)) => {
334                    mods.periodic *= pct_to_mult(base_points * ranks);
335                    // #t(rust_log_in_loop) each contributing passive is a distinct identity needed in trace diagnostics.
336                    tracing::trace!(
337                        source_spell_id = aura.id,
338                        source_spell_name = aura.name.as_str(),
339                        effect_index = effect.index,
340                        percent = base_points * ranks,
341                        running_periodic = mods.periodic,
342                        "static passive periodic-damage contribution"
343                    );
344                }
345                (Some(ModifierOperation::Percent), Some(ModifierPropertyKind::CritDamage)) => {
346                    crit_damage_mult *= pct_to_mult(base_points * ranks);
347                }
348                (Some(ModifierOperation::Flat), Some(ModifierPropertyKind::CritDamage)) => {
349                    crit_damage_flat += base_points * ranks / HUNDRED;
350                }
351                (Some(ModifierOperation::Flat), Some(ModifierPropertyKind::CritChance)) => {
352                    mods.crit_chance += base_points * ranks;
353                }
354                (Some(ModifierOperation::Percent), Some(ModifierPropertyKind::CritChance)) => {
355                    mods.crit_chance_mult *= pct_to_mult(base_points * ranks);
356                }
357                _ => {}
358            }
359        }
360    }
361
362    mods.crit_damage_bonus = ((1.0 + crit_damage_flat) * crit_damage_mult - 1.0) * HUNDRED;
363
364    mods
365}
366
367/// Whether `spell` is a damage-taken debuff the caster maintains on its own target.
368///
369/// Mystic Touch 113746 and Chaos Brand 1490 are the live shapes.
370/// Each is a school-filtered aura-87 (`ModDamageTakenPercent`) aimed at the caster's own enemy.
371/// Their parent class passives (8647, 255260) carry only a dummy effect and name them in text.
372/// This predicate therefore qualifies the *referenced* spell, never the passive that names it.
373#[must_use]
374pub fn is_target_damage_taken_debuff(spell: &SpellDataFlat) -> bool {
375    spell.effects.iter().any(|effect| {
376        aura_kind(effect) == Some(AuraSubtypeKind::ModDamageTakenPercent)
377            && effect_targets_current_enemy(effect)
378    })
379}
380
381fn effect_targets_current_enemy(effect: &wowlab_types::data::SpellEffect) -> bool {
382    crate::dbc::implicit_target_semantic(effect.implicit_target_a).is_some_and(|semantic| {
383        semantic.object == crate::dbc::ImplicitTargetObject::Unit
384            && semantic.reference == crate::dbc::ImplicitTargetReference::Target
385            && semantic.check == crate::dbc::ImplicitTargetCheck::Enemy
386    })
387}