wowlab_engine_domain/dbc/passives/
timing.rs1use 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#[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#[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
74pub 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#[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#[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 passive_cast_time_ms => CastTime,
211 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}