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]
98pub fn modified_effect_coefficient(
99    query: PassiveQuery<'_>,
100    effect_index: i32,
101    coefficient: f64,
102) -> f64 {
103    modified_effect_value(query, effect_index, coefficient, false)
104}
105
106/// Return an effect's value multiplier after property-27 flat and percentage modifiers.
107#[must_use]
108pub fn modified_effect_amplitude(query: PassiveQuery<'_>, initial: f64) -> f64 {
109    let PassiveQuery {
110        passives,
111        spell: target,
112    } = query;
113
114    fold_flat_pct_modifiers(
115        passives,
116        initial,
117        ModifierFold::FlatAndPercent,
118        |source, ranks, modifier| {
119            if modifier_property_kind(modifier) != Some(ModifierPropertyKind::Amplitude)
120                || !effect_targets_spell(modifier, source, target)
121            {
122                return None;
123            }
124
125            Some(
126                modified_effect_base_points(
127                    PassiveQuery {
128                        passives,
129                        spell: source,
130                    },
131                    modifier.index,
132                    modifier.base_points,
133                ) * ranks,
134            )
135        },
136    )
137}
138
139/// Spell-power and attack-power coefficients after property-24 modifiers.
140#[derive(Clone, Copy, Debug, PartialEq)]
141pub struct ModifiedPowerCoefficients {
142    pub spell_power: f64,
143    pub attack_power: f64,
144    /// Additive coefficient per live mastery point from attribute-285 flat modifiers.
145    pub mastery_add: f64,
146}
147
148/// Resolve property 24 against the first populated coefficient field, preserving flat-before-percent order.
149#[must_use]
150pub fn modified_power_coefficients(
151    query: PassiveQuery<'_>,
152    effect_index: i32,
153    spell_power: f64,
154    attack_power: f64,
155) -> ModifiedPowerCoefficients {
156    let PassiveQuery {
157        passives,
158        spell: target,
159    } = query;
160    let spell_power = modified_effect_coefficient(query, effect_index, spell_power);
161    let attack_power = modified_effect_coefficient(query, effect_index, attack_power);
162    let (initial, modifies_spell_power) = if spell_power.abs() > f64::EPSILON {
163        (spell_power, true)
164    } else if attack_power.abs() > f64::EPSILON {
165        (attack_power, false)
166    } else {
167        return ModifiedPowerCoefficients {
168            spell_power,
169            attack_power,
170            mastery_add: 0.0,
171        };
172    };
173    let mut flat = 0.0;
174    let mut flat_mastery = 0.0;
175    let mut percent = 1.0;
176
177    for (source, ranks) in passives {
178        for modifier in &source.effects {
179            if modifier_property_kind(modifier) != Some(ModifierPropertyKind::Coefficient)
180                || !effect_targets_spell(modifier, source, target)
181            {
182                continue;
183            }
184
185            let amount = modified_effect_base_points(
186                PassiveQuery {
187                    passives,
188                    spell: source,
189                },
190                modifier.index,
191                modifier.base_points,
192            ) * ranks;
193
194            match aura_subtype_semantic(modifier.aura)
195                .and_then(|semantic| semantic.modifier)
196                .map(|(operation, _)| operation)
197            {
198                Some(ModifierOperation::Flat) => {
199                    flat += amount / HUNDRED;
200
201                    if spell_attribute_is(
202                        &source.attributes,
203                        SpellAttributeKind::MasteryAffectsPoints,
204                    ) {
205                        flat_mastery += modified_effect_coefficient(
206                            PassiveQuery {
207                                passives,
208                                spell: source,
209                            },
210                            modifier.index,
211                            modifier.bonus_coefficient,
212                        ) * ranks
213                            / HUNDRED;
214                    }
215                }
216                Some(ModifierOperation::Percent) => percent *= pct_to_mult(amount),
217                None => {}
218            }
219        }
220    }
221
222    let modified = (initial + flat) * percent;
223
224    ModifiedPowerCoefficients {
225        spell_power: if modifies_spell_power {
226            modified
227        } else {
228            spell_power
229        },
230        attack_power: if modifies_spell_power {
231            attack_power
232        } else {
233            modified
234        },
235        mastery_add: flat_mastery * percent,
236    }
237}
238
239/// Returns compound damage and critical-strike modifiers.
240#[must_use]
241pub fn passive_damage_mods(
242    passives: &[RankedPassive],
243    target_class_set: i32,
244    target_class_masks: [i32; CLASS_FAMILY_FLAGS],
245    target_labels: &[i32],
246) -> PassiveDamageMods {
247    let mut crit_damage_flat = 0.0;
248    let mut crit_damage_mult = 1.0;
249    let mut mods = PassiveDamageMods {
250        direct_flat: 0.0,
251        direct_flat_mastery_coefficient: 0.0,
252        direct: 1.0,
253        periodic: 1.0,
254        crit_chance: 0.0,
255        crit_chance_mult: 1.0,
256        crit_damage_bonus: 0.0,
257    };
258
259    for (aura, ranks) in passives {
260        for effect in &aura.effects {
261            let base_points = modified_effect_base_points(
262                PassiveQuery {
263                    passives,
264                    spell: aura,
265                },
266                effect.index,
267                effect.base_points,
268            );
269            let matched = match aura_subtype_semantic(effect.aura)
270                .and_then(|semantic| semantic.modifier)
271                .map(|(_, filter)| filter)
272            {
273                Some(ModifierFilter::ClassFamilyMask) => {
274                    aura.spell_class_set == target_class_set
275                        && masks_overlap(effect, target_class_masks)
276                }
277                Some(ModifierFilter::Label) => target_labels.contains(&effect.misc_value_1),
278                _ => false,
279            };
280
281            if !matched {
282                continue;
283            }
284
285            let operation = aura_subtype_semantic(effect.aura)
286                .and_then(|semantic| semantic.modifier)
287                .map(|(operation, _)| operation);
288
289            match (operation, modifier_property_kind(effect)) {
290                (Some(ModifierOperation::Flat), Some(ModifierPropertyKind::GenericDamage)) => {
291                    mods.direct_flat += base_points * ranks;
292
293                    if spell_attribute_is(
294                        &aura.attributes,
295                        SpellAttributeKind::MasteryAffectsPoints,
296                    ) {
297                        mods.direct_flat_mastery_coefficient += modified_effect_coefficient(
298                            PassiveQuery {
299                                passives,
300                                spell: aura,
301                            },
302                            effect.index,
303                            effect.bonus_coefficient,
304                        ) * ranks;
305                    }
306                }
307                (Some(ModifierOperation::Percent), Some(ModifierPropertyKind::GenericDamage)) => {
308                    mods.direct *= pct_to_mult(base_points * ranks);
309                    // #t(rust_log_in_loop) each contributing passive is a distinct identity needed in trace diagnostics.
310                    tracing::trace!(
311                        source_spell_id = aura.id,
312                        source_spell_name = aura.name.as_str(),
313                        effect_index = effect.index,
314                        percent = base_points * ranks,
315                        running_direct = mods.direct,
316                        "static passive direct-damage contribution"
317                    );
318                }
319                (Some(ModifierOperation::Percent), Some(ModifierPropertyKind::PeriodicAmount)) => {
320                    mods.periodic *= pct_to_mult(base_points * ranks);
321                    // #t(rust_log_in_loop) each contributing passive is a distinct identity needed in trace diagnostics.
322                    tracing::trace!(
323                        source_spell_id = aura.id,
324                        source_spell_name = aura.name.as_str(),
325                        effect_index = effect.index,
326                        percent = base_points * ranks,
327                        running_periodic = mods.periodic,
328                        "static passive periodic-damage contribution"
329                    );
330                }
331                (Some(ModifierOperation::Percent), Some(ModifierPropertyKind::CritDamage)) => {
332                    crit_damage_mult *= pct_to_mult(base_points * ranks);
333                }
334                (Some(ModifierOperation::Flat), Some(ModifierPropertyKind::CritDamage)) => {
335                    crit_damage_flat += base_points * ranks / HUNDRED;
336                }
337                (Some(ModifierOperation::Flat), Some(ModifierPropertyKind::CritChance)) => {
338                    mods.crit_chance += base_points * ranks;
339                }
340                (Some(ModifierOperation::Percent), Some(ModifierPropertyKind::CritChance)) => {
341                    mods.crit_chance_mult *= pct_to_mult(base_points * ranks);
342                }
343                _ => {}
344            }
345        }
346    }
347
348    mods.crit_damage_bonus = ((1.0 + crit_damage_flat) * crit_damage_mult - 1.0) * HUNDRED;
349
350    mods
351}
352
353/// Whether `spell` is a damage-taken debuff the caster maintains on its own target.
354///
355/// Mystic Touch 113746 and Chaos Brand 1490 are the live shapes.
356/// Each is a school-filtered aura-87 (`ModDamageTakenPercent`) aimed at the caster's own enemy.
357/// Their parent class passives (8647, 255260) carry only a dummy effect and name them in text.
358/// This predicate therefore qualifies the *referenced* spell, never the passive that names it.
359#[must_use]
360pub fn is_target_damage_taken_debuff(spell: &SpellDataFlat) -> bool {
361    spell.effects.iter().any(|effect| {
362        aura_kind(effect) == Some(AuraSubtypeKind::ModDamageTakenPercent)
363            && effect_targets_current_enemy(effect)
364    })
365}
366
367fn effect_targets_current_enemy(effect: &wowlab_types::data::SpellEffect) -> bool {
368    crate::dbc::implicit_target_semantic(effect.implicit_target_a).is_some_and(|semantic| {
369        semantic.object == crate::dbc::ImplicitTargetObject::Unit
370            && semantic.reference == crate::dbc::ImplicitTargetReference::Target
371            && semantic.check == crate::dbc::ImplicitTargetCheck::Enemy
372    })
373}