Skip to main content

wowlab_engine_domain/dbc/passives/
timing.rs

1use wowlab_engine_gamedata::SpellProps;
2use wowlab_types::{data::SpellDataFlat, numeric::f64_to_i32_saturating_round};
3
4use super::{
5    AuraSubtypeKind, ModifierFold, ModifierPropertyKind, PassiveQuery, RankedPassive,
6    SpellEffectSemanticExt, aura_kind, effect_targets_spell, fold_flat_pct_modifiers,
7    masks_overlap, modified_effect_base_points, modifier_property_kind, pct_to_mult,
8};
9
10/// Maximum spell charges after category-based passive charge modifiers.
11#[must_use]
12pub fn passive_max_charges(query: PassiveQuery<'_>, initial: i32) -> i32 {
13    let PassiveQuery { passives, spell } = query;
14    let extra = passives
15        .iter()
16        .flat_map(|(source, ranks)| {
17            source
18                .effects
19                .iter()
20                .filter(|effect| {
21                    effect.aura_is(AuraSubtypeKind::ModCooldownChargeCategory)
22                        && spell.charge_category_id > 0
23                        && effect.misc_value_0 == spell.charge_category_id
24                })
25                .map(move |effect| {
26                    modified_effect_base_points(
27                        PassiveQuery {
28                            passives,
29                            spell: source,
30                        },
31                        effect.index,
32                        effect.base_points,
33                    ) * ranks
34                })
35        })
36        .sum::<f64>();
37
38    f64_to_i32_saturating_round((f64::from(initial) + extra).max(0.0))
39}
40
41/// Returns the cooldown after passive modifiers targeting `spell`.
42#[must_use]
43pub fn passive_cooldown_ms(query: PassiveQuery<'_>, base_cooldown_ms: i32) -> i32 {
44    let PassiveQuery { passives, spell } = query;
45
46    f64_to_i32_saturating_round(
47        fold_flat_pct_modifiers(
48            passives,
49            f64::from(base_cooldown_ms),
50            ModifierFold::FlatAndPercent,
51            |source, ranks, modifier| {
52                if modifier_property_kind(modifier) != Some(ModifierPropertyKind::Cooldown)
53                    || !effect_targets_spell(modifier, source, spell)
54                {
55                    return None;
56                }
57
58                Some(
59                    modified_effect_base_points(
60                        PassiveQuery {
61                            passives,
62                            spell: source,
63                        },
64                        modifier.index,
65                        modifier.base_points,
66                    ) * ranks,
67                )
68            },
69        )
70        .max(0.0),
71    )
72}
73
74/// Applies passive modifiers to every spell-owned cooldown clock.
75pub fn apply_passive_cooldowns(query: PassiveQuery<'_>, props: &mut SpellProps) {
76    let PassiveQuery { passives, spell } = query;
77
78    props.cooldown_ms = passive_cooldown_ms(query, props.cooldown_ms);
79    props.charge_cd_ms = passive_cooldown_ms(query, props.charge_cd_ms);
80    props.category_cooldown_ms = passive_cooldown_ms(query, props.category_cooldown_ms);
81
82    if spell.category_id > 0 {
83        let category_delta = passives
84            .iter()
85            .flat_map(|(source, ranks)| {
86                source.effects.iter().filter_map(move |effect| {
87                    (effect.aura_is(AuraSubtypeKind::ModCooldownTimeCategory)
88                        && effect.misc_value_0 == spell.category_id)
89                        .then_some(effect.base_points * ranks)
90                })
91            })
92            .sum::<f64>();
93
94        props.category_cooldown_ms = f64_to_i32_saturating_round(
95            (f64::from(props.category_cooldown_ms) + category_delta).max(0.0),
96        );
97    }
98
99    let mut charge_cooldown = f64::from(props.charge_cd_ms);
100
101    for (source, ranks) in passives {
102        for modifier in &source.effects {
103            if spell.charge_category_id == 0 || modifier.misc_value_0 != spell.charge_category_id {
104                continue;
105            }
106
107            let amount = modified_effect_base_points(
108                PassiveQuery {
109                    passives,
110                    spell: source,
111                },
112                modifier.index,
113                modifier.base_points,
114            ) * ranks;
115
116            match aura_kind(modifier) {
117                Some(AuraSubtypeKind::ModRechargeTimeCategory) => charge_cooldown += amount,
118                Some(AuraSubtypeKind::ModRechargeTimePercentCategory) => {
119                    charge_cooldown *= pct_to_mult(amount);
120                }
121                _ => {}
122            }
123        }
124    }
125
126    props.charge_cd_ms = f64_to_i32_saturating_round(charge_cooldown.max(0.0));
127}
128
129/// Whether a passive aura marks this spell's cooldown as haste-scaled.
130#[must_use]
131pub fn passive_hasted_cooldown(passives: &[RankedPassive], target: &SpellDataFlat) -> bool {
132    passives.iter().any(|(aura, _)| {
133        aura.is_passive
134            && aura.effects.iter().any(|effect| match aura_kind(effect) {
135                Some(AuraSubtypeKind::HastedCooldown) => {
136                    aura.spell_class_set == target.spell_class_set
137                        && masks_overlap(
138                            effect,
139                            [
140                                target.spell_class_mask_1,
141                                target.spell_class_mask_2,
142                                target.spell_class_mask_3,
143                                target.spell_class_mask_4,
144                            ],
145                        )
146                }
147                Some(AuraSubtypeKind::HastedCooldownCategory) => {
148                    target.charge_category_id != 0
149                        && effect.misc_value_0 == target.charge_category_id
150                }
151                _ => false,
152            })
153    })
154}
155
156/// Whether the spell's global cooldown scales with the actor's haste.
157#[must_use]
158pub fn passive_hasted_gcd(passives: &[RankedPassive], target: &SpellDataFlat) -> bool {
159    let is_ability = crate::dbc::spell_attribute_is_any(
160        &target.attributes,
161        &[
162            crate::dbc::SpellAttributeKind::RangedAbility,
163            crate::dbc::SpellAttributeKind::Ability,
164        ],
165    );
166
167    !is_ability
168        || passives.iter().any(|(aura, _)| {
169            aura.is_passive
170                && aura.effects.iter().any(|effect| match aura_kind(effect) {
171                    Some(AuraSubtypeKind::HastedGlobalCooldown) => {
172                        aura.spell_class_set == target.spell_class_set
173                            && masks_overlap(
174                                effect,
175                                [
176                                    target.spell_class_mask_1,
177                                    target.spell_class_mask_2,
178                                    target.spell_class_mask_3,
179                                    target.spell_class_mask_4,
180                                ],
181                            )
182                    }
183                    Some(AuraSubtypeKind::ApplyHastedGcdLabel) => target
184                        .labels
185                        .iter()
186                        .any(|label| label.0 == effect.misc_value_0),
187                    _ => false,
188                })
189        })
190}
191
192macro_rules! passive_timing_wrappers {
193    ($($(#[$meta:meta])* $method:ident => $property:ident),+ $(,)?) => {
194        $(
195            $(#[$meta])*
196            #[must_use]
197            pub fn $method(query: PassiveQuery<'_>, initial: i32) -> i32 {
198                super::passive_integral_value(
199                    query,
200                    initial,
201                    ModifierPropertyKind::$property,
202                )
203            }
204        )+
205    };
206}
207
208passive_timing_wrappers! {
209    /// Cast time after selected passive flat and percentage modifiers.
210    passive_cast_time_ms => CastTime,
211    /// Global cooldown after selected passive flat and percentage modifiers.
212    passive_gcd_ms => GlobalCooldown,
213}
214
215#[cfg(test)]
216mod tests {
217    use googletest::prelude::*;
218    use wowlab_types::data::{SpellEffect, SpellLabel};
219
220    use super::*;
221
222    const CLASS_SET: i32 = 11;
223
224    fn ability() -> SpellDataFlat {
225        let mut attributes = vec![0; 1];
226
227        attributes[crate::dbc::SpellAttributeKind::Ability.block()] |=
228            crate::dbc::SpellAttributeKind::Ability.mask();
229
230        SpellDataFlat {
231            attributes,
232            spell_class_set: CLASS_SET,
233            spell_class_mask_1: 1,
234            ..Default::default()
235        }
236    }
237
238    fn passive(effect: SpellEffect) -> SpellDataFlat {
239        SpellDataFlat {
240            is_passive: true,
241            spell_class_set: CLASS_SET,
242            effects: vec![effect],
243            ..Default::default()
244        }
245    }
246
247    #[gtest]
248    fn hasted_gcd_combines_ability_flags_with_mask_and_label_passives() -> Result<()> {
249        let mut target = ability();
250
251        verify_that!(passive_hasted_gcd(&[], &target), eq(false))?;
252
253        let masked = SpellEffect {
254            aura: AuraSubtypeKind::HastedGlobalCooldown as i32,
255            effect_class_mask_1: 1,
256            ..Default::default()
257        };
258
259        verify_that!(
260            passive_hasted_gcd(&[(passive(masked), 1.0)], &target),
261            eq(true)
262        )?;
263
264        let label = 640;
265
266        target.labels = vec![SpellLabel(label)];
267        let labeled = SpellEffect {
268            aura: AuraSubtypeKind::ApplyHastedGcdLabel as i32,
269            misc_value_0: label,
270            ..Default::default()
271        };
272
273        verify_that!(
274            passive_hasted_gcd(&[(passive(labeled), 1.0)], &target),
275            eq(true)
276        )?;
277
278        verify_that!(passive_hasted_gcd(&[], &SpellDataFlat::default()), eq(true))
279    }
280}