Skip to main content

wowlab_engine_domain/dbc/
passives.rs

1//! Passive aura modifiers resolved from DBC effects.
2
3#[cfg(test)]
4use wowlab_engine_gamedata::SpellProps;
5#[cfg(test)]
6use wowlab_types::sim::FastSet;
7use wowlab_types::{
8    constants::HUNDRED,
9    data::{SpellDataFlat, SpellEffect},
10    game::ClassId,
11    numeric::f64_to_i32_saturating_round,
12};
13
14use super::decode::power_cost_is_available;
15mod combat;
16mod crit_scaling;
17mod damage_proc;
18mod resource;
19mod stats;
20mod talent;
21mod targeting;
22mod timing;
23
24pub use combat::{
25    PassivePetStatMods, passive_auto_attack_mult, passive_guardian_damage_mult,
26    passive_pet_damage_mult, passive_pet_stat_mods, passive_target_armor_mult,
27};
28pub use crit_scaling::{
29    CritChanceScaledAxis, CritChanceScaledCritDamage, crit_chance_scaled_crit_damage,
30    crit_chance_scaled_spell_percent,
31};
32pub use stats::{PassiveStatMods, is_stat_modifier_passive, passive_stat_mods};
33pub use timing::{
34    apply_passive_cooldowns, passive_cast_time_ms, passive_cooldown_ms, passive_gcd_ms,
35    passive_hasted_cooldown, passive_hasted_gcd, passive_max_charges,
36};
37
38use super::{
39    AttributeMask, AuraSubtypeKind, ModifierFilter, ModifierOperation, ModifierPropertyKind,
40    PetStatKind, RatingMultiplierMask, SpellAttributeKind, SpellEffectSemanticExt,
41    aura_subtype_kind, aura_subtype_semantic, modifier_property_kind as registered_property_kind,
42    pet_stat_kind, spell_attribute_is,
43};
44
45/// The fixed cadence used by DBC aura 85 resource-regeneration amounts.
46pub const POWER_REGEN_PERIOD_SECONDS: f64 = 5.0;
47
48const CLASS_FAMILY_FLAGS: usize = 4;
49pub use damage_proc::{
50    ModifiedPowerCoefficients, PassiveDamageMods, PassiveProcChanceMods,
51    is_damage_modifier_passive, is_target_damage_taken_debuff, modified_effect_amplitude,
52    modified_effect_coefficient, modified_power_coefficients, passive_damage_mods,
53    passive_proc_chance_mods,
54};
55
56fn aura_kind(effect: &SpellEffect) -> Option<AuraSubtypeKind> {
57    aura_subtype_kind(effect.aura)
58}
59
60fn modifier_property_kind(effect: &SpellEffect) -> Option<ModifierPropertyKind> {
61    registered_property_kind(effect.misc_value_0)
62}
63
64const fn masks_overlap(effect: &SpellEffect, target_masks: [i32; CLASS_FAMILY_FLAGS]) -> bool {
65    let [m1, m2, m3, m4] = target_masks;
66
67    (effect.effect_class_mask_1 & m1) != 0
68        || (effect.effect_class_mask_2 & m2) != 0
69        || (effect.effect_class_mask_3 & m3) != 0
70        || (effect.effect_class_mask_4 & m4) != 0
71}
72
73const fn pct_to_mult(base_points: f64) -> f64 {
74    1.0 + base_points / HUNDRED
75}
76
77const fn property_effect_index(property: ModifierPropertyKind) -> Option<i32> {
78    const EFFECT_INDEX_1: i32 = 0;
79    const EFFECT_INDEX_2: i32 = 1;
80    const EFFECT_INDEX_3: i32 = 2;
81    const EFFECT_INDEX_4: i32 = 3;
82    const EFFECT_INDEX_5: i32 = 4;
83
84    match property {
85        ModifierPropertyKind::Effect1 => Some(EFFECT_INDEX_1),
86        ModifierPropertyKind::Effect2 => Some(EFFECT_INDEX_2),
87        ModifierPropertyKind::Effect3 => Some(EFFECT_INDEX_3),
88        ModifierPropertyKind::Effect4 => Some(EFFECT_INDEX_4),
89        ModifierPropertyKind::Effect5 => Some(EFFECT_INDEX_5),
90        _ => None,
91    }
92}
93
94const fn property_targets_effect(property: ModifierPropertyKind, effect_index: i32) -> bool {
95    match property_effect_index(property) {
96        Some(index) => index == effect_index,
97        None => matches!(property, ModifierPropertyKind::Points),
98    }
99}
100
101#[derive(Clone, Copy)]
102enum ModifierFold {
103    PercentOnly,
104    FlatAndPercent,
105}
106
107impl ModifierFold {
108    const fn includes(self, operation: ModifierOperation) -> bool {
109        match (self, operation) {
110            (Self::FlatAndPercent, _) | (Self::PercentOnly, ModifierOperation::Percent) => true,
111            (Self::PercentOnly, ModifierOperation::Flat) => false,
112        }
113    }
114}
115
116/// Flat and multiplicative components of one affected-spell modifier property.
117#[derive(Clone, Copy, Debug, PartialEq)]
118pub struct PassiveModifierComponents {
119    pub flat: f64,
120    pub multiplier: f64,
121}
122
123/// Passive selection and the spell whose values are being resolved.
124#[derive(Clone, Copy, Debug)]
125pub struct PassiveQuery<'a> {
126    pub passives: &'a [RankedPassive],
127    pub spell: &'a SpellDataFlat,
128}
129
130impl PassiveModifierComponents {
131    #[must_use]
132    pub const fn apply(self, initial: f64) -> f64 {
133        (initial + self.flat) * self.multiplier
134    }
135}
136
137/// Resolve one passive spell-modifier property while retaining flat-before-percent ordering.
138#[must_use]
139pub fn passive_modifier_components(
140    query: PassiveQuery<'_>,
141    property: ModifierPropertyKind,
142) -> PassiveModifierComponents {
143    let PassiveQuery {
144        passives,
145        spell: target,
146    } = query;
147    let mut components = PassiveModifierComponents {
148        flat: 0.0,
149        multiplier: 1.0,
150    };
151
152    for (source, ranks) in passives {
153        for modifier in &source.effects {
154            if modifier_property_kind(modifier) != Some(property)
155                || !effect_targets_spell(modifier, source, target)
156            {
157                continue;
158            }
159
160            let amount = modified_effect_base_points(
161                PassiveQuery {
162                    passives,
163                    spell: source,
164                },
165                modifier.index,
166                modifier.base_points,
167            ) * ranks;
168
169            match aura_subtype_semantic(modifier.aura)
170                .and_then(|semantic| semantic.modifier)
171                .map(|(operation, _)| operation)
172            {
173                Some(ModifierOperation::Flat) => components.flat += amount,
174                Some(ModifierOperation::Percent) => {
175                    components.multiplier *= pct_to_mult(amount);
176                }
177                _ => {}
178            }
179        }
180    }
181
182    components
183}
184
185fn effect_targets_spell(
186    effect: &SpellEffect,
187    source: &SpellDataFlat,
188    target: &SpellDataFlat,
189) -> bool {
190    match aura_subtype_semantic(effect.aura).and_then(|semantic| semantic.modifier) {
191        Some((_, ModifierFilter::ClassFamilyMask)) => {
192            source.spell_class_set == target.spell_class_set
193                && masks_overlap(
194                    effect,
195                    [
196                        target.spell_class_mask_1,
197                        target.spell_class_mask_2,
198                        target.spell_class_mask_3,
199                        target.spell_class_mask_4,
200                    ],
201                )
202        }
203        Some((_, ModifierFilter::Label)) => target
204            .labels
205            .iter()
206            .any(|label| label.0 == effect.misc_value_1),
207        _ => false,
208    }
209}
210
211fn fold_flat_pct_modifiers(
212    passives: &[RankedPassive],
213    initial: f64,
214    operations: ModifierFold,
215    mut amount_for: impl FnMut(&SpellDataFlat, f64, &SpellEffect) -> Option<f64>,
216) -> f64 {
217    let mut flat = 0.0;
218    let mut pct_mult = 1.0;
219
220    for (source, ranks) in passives {
221        for modifier in &source.effects {
222            let Some(amount) = amount_for(source, *ranks, modifier) else {
223                continue;
224            };
225            let operation = aura_subtype_semantic(modifier.aura)
226                .and_then(|semantic| semantic.modifier)
227                .map(|(operation, _)| operation);
228
229            match operation.filter(|operation| operations.includes(*operation)) {
230                Some(ModifierOperation::Flat) => flat += amount,
231                Some(ModifierOperation::Percent) => pct_mult *= pct_to_mult(amount),
232                _ => {}
233            }
234        }
235    }
236    // Flat adjusts the base before percent scales it, independent of passive iteration order.
237
238    (initial + flat) * pct_mult
239}
240
241fn passive_integral_value(
242    query: PassiveQuery<'_>,
243    initial: i32,
244    property: ModifierPropertyKind,
245) -> i32 {
246    let PassiveQuery {
247        passives,
248        spell: target,
249    } = query;
250
251    f64_to_i32_saturating_round(
252        fold_flat_pct_modifiers(
253            passives,
254            f64::from(initial),
255            ModifierFold::FlatAndPercent,
256            |source, ranks, modifier| {
257                if modifier_property_kind(modifier) != Some(property)
258                    || !effect_targets_spell(modifier, source, target)
259                {
260                    return None;
261                }
262
263                Some(modifier.base_points * ranks)
264            },
265        )
266        .max(0.0),
267    )
268}
269
270fn modified_effect_value(
271    query: PassiveQuery<'_>,
272    effect_index: i32,
273    initial: f64,
274    include_flat: bool,
275) -> f64 {
276    let PassiveQuery {
277        passives,
278        spell: target,
279    } = query;
280
281    fold_flat_pct_modifiers(
282        passives,
283        initial,
284        if include_flat {
285            ModifierFold::FlatAndPercent
286        } else {
287            ModifierFold::PercentOnly
288        },
289        |source, ranks, modifier| {
290            if !modifier_property_kind(modifier)
291                .is_some_and(|property| property_targets_effect(property, effect_index))
292                || !effect_targets_spell(modifier, source, target)
293            {
294                return None;
295            }
296
297            tracing::trace!(
298                source_spell_id = source.id,
299                source_spell_name = %source.name,
300                modifier_index = modifier.index,
301                target_spell_id = target.id,
302                effect_index,
303                ranks,
304                value = modifier.base_points * ranks,
305                "passive effect-value modifier applied"
306            );
307
308            Some(modifier.base_points * ranks)
309        },
310    )
311}
312
313/// Return effective effect base points after selected `P_EFFECT_N` passives apply.
314#[must_use]
315pub fn modified_effect_base_points(
316    query: PassiveQuery<'_>,
317    effect_index: i32,
318    base_points: f64,
319) -> f64 {
320    modified_effect_value(query, effect_index, base_points, true)
321}
322
323/// Resolves an aura's maximum stack count after passive modifiers.
324#[must_use]
325pub fn passive_max_stacks(query: PassiveQuery<'_>, initial: i32) -> i32 {
326    let PassiveQuery {
327        passives,
328        spell: target,
329    } = query;
330
331    f64_to_i32_saturating_round(
332        fold_flat_pct_modifiers(
333            passives,
334            f64::from(initial),
335            ModifierFold::FlatAndPercent,
336            |source, ranks, modifier| {
337                if modifier_property_kind(modifier) != Some(ModifierPropertyKind::MaxStacks)
338                    || !effect_targets_spell(modifier, source, target)
339                {
340                    return None;
341                }
342
343                Some(modifier.base_points * ranks)
344            },
345        )
346        .max(1.0),
347    )
348}
349
350/// Resolves `SpellAuraOptions.ProcCharges` after passive property-31 modifiers.
351#[must_use]
352pub fn passive_doses(passives: &[RankedPassive], target: &SpellDataFlat) -> i32 {
353    f64_to_i32_saturating_round(fold_flat_pct_modifiers(
354        passives,
355        f64::from(target.proc_charges),
356        ModifierFold::FlatAndPercent,
357        |source, ranks, modifier| {
358            if modifier_property_kind(modifier) != Some(ModifierPropertyKind::Doses)
359                || !effect_targets_spell(modifier, source, target)
360            {
361                return None;
362            }
363
364            Some(modifier.base_points * ranks)
365        },
366    ))
367}
368
369/// Returns a periodic effect's interval after flat and percentage tick-time modifiers.
370#[must_use]
371pub fn passive_period_ms(query: PassiveQuery<'_>, initial: i32) -> i32 {
372    let PassiveQuery {
373        passives,
374        spell: target,
375    } = query;
376
377    if initial <= 0 {
378        return 0;
379    }
380
381    f64_to_i32_saturating_round(
382        fold_flat_pct_modifiers(
383            passives,
384            f64::from(initial),
385            ModifierFold::FlatAndPercent,
386            |source, ranks, modifier| {
387                if modifier_property_kind(modifier) != Some(ModifierPropertyKind::TickTime)
388                    || !effect_targets_spell(modifier, source, target)
389                {
390                    return None;
391                }
392
393                Some(
394                    modified_effect_base_points(
395                        PassiveQuery {
396                            passives,
397                            spell: source,
398                        },
399                        modifier.index,
400                        modifier.base_points,
401                    ) * ranks,
402                )
403            },
404        )
405        .max(1.0),
406    )
407}
408
409pub use targeting::{
410    passive_chain_multiplier, passive_chain_target_range, passive_chain_targets,
411    passive_hostile_max_range, passive_max_targets, passive_radius,
412};
413
414/// Aura duration after selected passive flat/percent modifiers targeting `spell`.
415#[must_use]
416pub fn passive_duration_ms(query: PassiveQuery<'_>, base_duration_ms: i32) -> i32 {
417    let PassiveQuery { passives, spell } = query;
418
419    if spell.is_passive && base_duration_ms == 0 {
420        return 0;
421    }
422
423    f64_to_i32_saturating_round(
424        fold_flat_pct_modifiers(
425            passives,
426            f64::from(base_duration_ms),
427            ModifierFold::FlatAndPercent,
428            |source, ranks, modifier| {
429                if modifier_property_kind(modifier) != Some(ModifierPropertyKind::Duration)
430                    || !effect_targets_spell(modifier, source, spell)
431                {
432                    return None;
433                }
434
435                Some(
436                    modified_effect_base_points(
437                        PassiveQuery {
438                            passives,
439                            spell: source,
440                        },
441                        modifier.index,
442                        modifier.base_points,
443                    ) * ranks,
444                )
445            },
446        )
447        .max(0.0),
448    )
449}
450
451pub use resource::{RankedPassive, passive_mana_regen_multiplier, passive_resource_cost};
452pub use talent::{ResourcePassive, class_passive_spell_ids, talent_resource_passives};
453
454#[cfg(test)]
455#[path = "passives_tests.rs"]
456mod tests;