Skip to main content

wowlab_engine_domain/dbc/passives/
combat.rs

1use std::collections::BTreeMap;
2
3use wowlab_types::constants::HUNDRED;
4#[cfg(test)]
5use wowlab_types::data::SpellDataFlat;
6
7use super::{
8    AuraSubtypeKind, PassiveQuery, PetStatKind, RankedPassive, SpellEffectSemanticExt, aura_kind,
9    modified_effect_base_points, pct_to_mult, pet_stat_kind,
10};
11
12/// Passive aura-382 inheritance multipliers for one companion NPC.
13#[derive(Clone, Copy, Debug, PartialEq)]
14pub struct PassivePetStatMods {
15    pub npc_id: u32,
16    pub attack_power_mult: f64,
17    pub spell_power_mult: f64,
18}
19
20fn compound_flat_aura_mult(passives: &[RankedPassive], expected: AuraSubtypeKind) -> f64 {
21    let effects = passives.iter().flat_map(|(aura, ranks)| {
22        aura.effects
23            .iter()
24            .map(move |effect| (aura, effect, *ranks))
25    });
26
27    effects
28        .filter(|(_, effect, _)| aura_kind(effect) == Some(expected))
29        .fold(1.0, |mult, (aura, effect, ranks)| {
30            let percent = modified_effect_base_points(
31                PassiveQuery {
32                    passives,
33                    spell: aura,
34                },
35                effect.index,
36                effect.base_points,
37            );
38
39            mult * pct_to_mult(percent * ranks)
40        })
41}
42
43/// Compound auto-attack damage multiplier across passive aura 344 effects.
44#[must_use]
45pub fn passive_auto_attack_mult(passives: &[RankedPassive]) -> f64 {
46    compound_flat_aura_mult(passives, AuraSubtypeKind::ModAutoAttackDamage)
47}
48
49/// Compound pet damage multiplier across passive aura 429 effects.
50#[must_use]
51pub fn passive_pet_damage_mult(passives: &[RankedPassive]) -> f64 {
52    compound_flat_aura_mult(passives, AuraSubtypeKind::ModPetDamageDone)
53}
54
55/// Compound guardian damage multiplier across passive aura 531 effects.
56#[must_use]
57pub fn passive_guardian_damage_mult(passives: &[RankedPassive]) -> f64 {
58    compound_flat_aura_mult(passives, AuraSubtypeKind::ModGuardianDamageDonePercent)
59}
60
61/// Resolve NPC-filtered attack/spell-power inheritance from passive aura 382 effects.
62#[must_use]
63pub fn passive_pet_stat_mods(passives: &[RankedPassive]) -> Vec<PassivePetStatMods> {
64    let mut by_npc = BTreeMap::<u32, (f64, f64)>::new();
65
66    for (aura, ranks) in passives {
67        for effect in &aura.effects {
68            if !effect.aura_is(AuraSubtypeKind::ModPetStat) || effect.misc_value_1 <= 0 {
69                continue;
70            }
71
72            let Ok(npc_id) = u32::try_from(effect.misc_value_1) else {
73                continue;
74            };
75            let entry = by_npc.entry(npc_id).or_insert((1.0, 1.0));
76            let mult = pct_to_mult(effect.base_points * ranks);
77
78            match pet_stat_kind(effect.misc_value_0) {
79                Some(PetStatKind::AttackPowerInheritance) => entry.0 *= mult,
80                Some(PetStatKind::SpellPowerInheritance) => entry.1 *= mult,
81                _ => {}
82            }
83        }
84    }
85
86    by_npc
87        .into_iter()
88        .map(
89            |(npc_id, (attack_power_mult, spell_power_mult))| PassivePetStatMods {
90                npc_id,
91                attack_power_mult,
92                spell_power_mult,
93            },
94        )
95        .collect()
96}
97
98/// Target-armor multiplier after additive passive armor-penetration percentages.
99#[must_use]
100pub fn passive_target_armor_mult(passives: &[RankedPassive]) -> f64 {
101    let effects = passives
102        .iter()
103        .flat_map(|(aura, ranks)| aura.effects.iter().map(move |effect| (effect, *ranks)));
104    let penetration_pct = effects
105        .filter(|(effect, _)| effect.aura_is(AuraSubtypeKind::ModTargetArmorPercent))
106        .map(|(effect, ranks)| effect.base_points * ranks)
107        .sum::<f64>();
108
109    (1.0 - penetration_pct / HUNDRED).max(0.0)
110}
111
112#[cfg(test)]
113mod tests {
114    use googletest::prelude::*;
115    use wowlab_types::data::{SpellEffect, SpellLabel};
116
117    use super::*;
118
119    #[gtest]
120    fn pet_damage_passive_uses_label_modifier_on_its_effect_value() {
121        let pet_damage = SpellDataFlat {
122            labels: vec![SpellLabel(6324)],
123            is_passive: true,
124            effects: vec![SpellEffect {
125                index: 0,
126                aura: AuraSubtypeKind::ModPetDamageDone as i32,
127                base_points: 0.0,
128                ..Default::default()
129            }],
130            ..Default::default()
131        };
132        let specialization = SpellDataFlat {
133            is_passive: true,
134            effects: vec![SpellEffect {
135                aura: AuraSubtypeKind::AddFlatLabelModifier as i32,
136                base_points: 15.0,
137                misc_value_0: super::super::ModifierPropertyKind::Effect1 as i32,
138                misc_value_1: 6324,
139                ..Default::default()
140            }],
141            ..Default::default()
142        };
143
144        expect_that!(
145            passive_pet_damage_mult(&[(pet_damage, 1.0), (specialization, 1.0)]),
146            eq(1.15)
147        );
148    }
149
150    #[gtest]
151    fn pet_and_guardian_damage_passives_are_resolved_independently() {
152        let passive = SpellDataFlat {
153            is_passive: true,
154            effects: vec![
155                SpellEffect {
156                    aura: AuraSubtypeKind::ModPetDamageDone as i32,
157                    base_points: 20.0,
158                    ..Default::default()
159                },
160                SpellEffect {
161                    aura: AuraSubtypeKind::ModGuardianDamageDonePercent as i32,
162                    base_points: -10.0,
163                    ..Default::default()
164                },
165            ],
166            ..Default::default()
167        };
168        let passives = [(passive, 1.0)];
169
170        expect_that!(passive_pet_damage_mult(&passives), eq(1.2));
171        expect_that!(passive_guardian_damage_mult(&passives), eq(0.9));
172    }
173
174    #[gtest]
175    fn pet_stat_mods_group_ap_and_sp_inheritance_by_npc() {
176        let passive = SpellDataFlat {
177            is_passive: true,
178            effects: vec![
179                SpellEffect {
180                    aura: AuraSubtypeKind::ModPetStat as i32,
181                    base_points: 10.0,
182                    misc_value_0: PetStatKind::AttackPowerInheritance as i32,
183                    misc_value_1: 26_125,
184                    ..Default::default()
185                },
186                SpellEffect {
187                    aura: AuraSubtypeKind::ModPetStat as i32,
188                    base_points: 20.0,
189                    misc_value_0: PetStatKind::SpellPowerInheritance as i32,
190                    misc_value_1: 26_125,
191                    ..Default::default()
192                },
193            ],
194            ..Default::default()
195        };
196
197        expect_that!(
198            passive_pet_stat_mods(&[(passive, 1.0)]),
199            eq(&vec![PassivePetStatMods {
200                npc_id: 26_125,
201                attack_power_mult: 1.1,
202                spell_power_mult: 1.2,
203            }])
204        );
205    }
206}