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#[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#[derive(Clone, Copy, Debug, PartialEq)]
23pub struct PassiveProcChanceMods {
24 pub flat_pct: f64,
25 pub multiplier: f64,
26}
27
28#[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#[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#[must_use]
98pub fn modified_effect_coefficient(
100 query: PassiveQuery<'_>,
101 effect_index: i32,
102 coefficient: f64,
103) -> f64 {
104 if coefficient == 0.0 {
105 return 0.0;
106 }
107
108 modified_effect_value(query, effect_index, coefficient, false)
109}
110
111#[must_use]
113pub fn modified_effect_amplitude(query: PassiveQuery<'_>, initial: f64) -> f64 {
114 let PassiveQuery {
115 passives,
116 spell: target,
117 } = query;
118
119 fold_flat_pct_modifiers(
120 passives,
121 initial,
122 ModifierFold::FlatAndPercent,
123 |source, ranks, modifier| {
124 if modifier_property_kind(modifier) != Some(ModifierPropertyKind::Amplitude)
125 || !effect_targets_spell(modifier, source, target)
126 {
127 return None;
128 }
129
130 Some(
131 modified_effect_base_points(
132 PassiveQuery {
133 passives,
134 spell: source,
135 },
136 modifier.index,
137 modifier.base_points,
138 ) * ranks,
139 )
140 },
141 )
142}
143
144#[derive(Clone, Copy, Debug, PartialEq)]
146pub struct ModifiedPowerCoefficients {
147 pub spell_power: f64,
148 pub attack_power: f64,
149 pub mastery_add: f64,
151}
152
153#[must_use]
155pub fn modified_power_coefficients(
157 query: PassiveQuery<'_>,
158 effect_index: i32,
159 spell_power: f64,
160 attack_power: f64,
161) -> ModifiedPowerCoefficients {
162 let PassiveQuery {
163 passives,
164 spell: target,
165 } = query;
166 let spell_power = if spell_power == 0.0 {
167 0.0
168 } else {
169 modified_effect_coefficient(query, effect_index, spell_power)
170 };
171 let attack_power = if attack_power == 0.0 {
172 0.0
173 } else {
174 modified_effect_coefficient(query, effect_index, attack_power)
175 };
176 let (initial, modifies_spell_power) = if spell_power.abs() > f64::EPSILON {
177 (spell_power, true)
178 } else if attack_power.abs() > f64::EPSILON {
179 (attack_power, false)
180 } else {
181 return ModifiedPowerCoefficients {
182 spell_power,
183 attack_power,
184 mastery_add: 0.0,
185 };
186 };
187 let mut flat = 0.0;
188 let mut flat_mastery = 0.0;
189 let mut percent = 1.0;
190
191 for (source, ranks) in passives {
192 for modifier in &source.effects {
193 if modifier_property_kind(modifier) != Some(ModifierPropertyKind::Coefficient)
194 || !effect_targets_spell(modifier, source, target)
195 {
196 continue;
197 }
198
199 let amount = modified_effect_base_points(
200 PassiveQuery {
201 passives,
202 spell: source,
203 },
204 modifier.index,
205 modifier.base_points,
206 ) * ranks;
207
208 match aura_subtype_semantic(modifier.aura)
209 .and_then(|semantic| semantic.modifier)
210 .map(|(operation, _)| operation)
211 {
212 Some(ModifierOperation::Flat) => {
213 flat += amount / HUNDRED;
214
215 if spell_attribute_is(
216 &source.attributes,
217 SpellAttributeKind::MasteryAffectsPoints,
218 ) {
219 flat_mastery += modified_effect_coefficient(
220 PassiveQuery {
221 passives,
222 spell: source,
223 },
224 modifier.index,
225 modifier.bonus_coefficient,
226 ) * ranks
227 / HUNDRED;
228 }
229 }
230 Some(ModifierOperation::Percent) => percent *= pct_to_mult(amount),
231 None => {}
232 }
233 }
234 }
235
236 let modified = (initial + flat) * percent;
237
238 ModifiedPowerCoefficients {
239 spell_power: if modifies_spell_power {
240 modified
241 } else {
242 spell_power
243 },
244 attack_power: if modifies_spell_power {
245 attack_power
246 } else {
247 modified
248 },
249 mastery_add: flat_mastery * percent,
250 }
251}
252
253#[must_use]
255pub fn passive_damage_mods(
256 passives: &[RankedPassive],
257 target_class_set: i32,
258 target_class_masks: [i32; CLASS_FAMILY_FLAGS],
259 target_labels: &[i32],
260) -> PassiveDamageMods {
261 let mut crit_damage_flat = 0.0;
262 let mut crit_damage_mult = 1.0;
263 let mut mods = PassiveDamageMods {
264 direct_flat: 0.0,
265 direct_flat_mastery_coefficient: 0.0,
266 direct: 1.0,
267 periodic: 1.0,
268 crit_chance: 0.0,
269 crit_chance_mult: 1.0,
270 crit_damage_bonus: 0.0,
271 };
272
273 for (aura, ranks) in passives {
274 for effect in &aura.effects {
275 let base_points = modified_effect_base_points(
276 PassiveQuery {
277 passives,
278 spell: aura,
279 },
280 effect.index,
281 effect.base_points,
282 );
283 let matched = match aura_subtype_semantic(effect.aura)
284 .and_then(|semantic| semantic.modifier)
285 .map(|(_, filter)| filter)
286 {
287 Some(ModifierFilter::ClassFamilyMask) => {
288 aura.spell_class_set == target_class_set
289 && masks_overlap(effect, target_class_masks)
290 }
291 Some(ModifierFilter::Label) => target_labels.contains(&effect.misc_value_1),
292 _ => false,
293 };
294
295 if !matched {
296 continue;
297 }
298
299 let operation = aura_subtype_semantic(effect.aura)
300 .and_then(|semantic| semantic.modifier)
301 .map(|(operation, _)| operation);
302
303 match (operation, modifier_property_kind(effect)) {
304 (Some(ModifierOperation::Flat), Some(ModifierPropertyKind::GenericDamage)) => {
305 mods.direct_flat += base_points * ranks;
306
307 if spell_attribute_is(
308 &aura.attributes,
309 SpellAttributeKind::MasteryAffectsPoints,
310 ) {
311 mods.direct_flat_mastery_coefficient += modified_effect_coefficient(
312 PassiveQuery {
313 passives,
314 spell: aura,
315 },
316 effect.index,
317 effect.bonus_coefficient,
318 ) * ranks;
319 }
320 }
321 (Some(ModifierOperation::Percent), Some(ModifierPropertyKind::GenericDamage)) => {
322 mods.direct *= pct_to_mult(base_points * ranks);
323 tracing::trace!(
325 source_spell_id = aura.id,
326 source_spell_name = aura.name.as_str(),
327 effect_index = effect.index,
328 percent = base_points * ranks,
329 running_direct = mods.direct,
330 "static passive direct-damage contribution"
331 );
332 }
333 (Some(ModifierOperation::Percent), Some(ModifierPropertyKind::PeriodicAmount)) => {
334 mods.periodic *= pct_to_mult(base_points * ranks);
335 tracing::trace!(
337 source_spell_id = aura.id,
338 source_spell_name = aura.name.as_str(),
339 effect_index = effect.index,
340 percent = base_points * ranks,
341 running_periodic = mods.periodic,
342 "static passive periodic-damage contribution"
343 );
344 }
345 (Some(ModifierOperation::Percent), Some(ModifierPropertyKind::CritDamage)) => {
346 crit_damage_mult *= pct_to_mult(base_points * ranks);
347 }
348 (Some(ModifierOperation::Flat), Some(ModifierPropertyKind::CritDamage)) => {
349 crit_damage_flat += base_points * ranks / HUNDRED;
350 }
351 (Some(ModifierOperation::Flat), Some(ModifierPropertyKind::CritChance)) => {
352 mods.crit_chance += base_points * ranks;
353 }
354 (Some(ModifierOperation::Percent), Some(ModifierPropertyKind::CritChance)) => {
355 mods.crit_chance_mult *= pct_to_mult(base_points * ranks);
356 }
357 _ => {}
358 }
359 }
360 }
361
362 mods.crit_damage_bonus = ((1.0 + crit_damage_flat) * crit_damage_mult - 1.0) * HUNDRED;
363
364 mods
365}
366
367#[must_use]
374pub fn is_target_damage_taken_debuff(spell: &SpellDataFlat) -> bool {
375 spell.effects.iter().any(|effect| {
376 aura_kind(effect) == Some(AuraSubtypeKind::ModDamageTakenPercent)
377 && effect_targets_current_enemy(effect)
378 })
379}
380
381fn effect_targets_current_enemy(effect: &wowlab_types::data::SpellEffect) -> bool {
382 crate::dbc::implicit_target_semantic(effect.implicit_target_a).is_some_and(|semantic| {
383 semantic.object == crate::dbc::ImplicitTargetObject::Unit
384 && semantic.reference == crate::dbc::ImplicitTargetReference::Target
385 && semantic.check == crate::dbc::ImplicitTargetCheck::Enemy
386 })
387}