1#[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
45pub 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#[derive(Clone, Copy, Debug, PartialEq)]
118pub struct PassiveModifierComponents {
119 pub flat: f64,
120 pub multiplier: f64,
121}
122
123#[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#[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 (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#[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#[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#[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#[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#[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;