Skip to main content

wowlab_engine_domain/
damage.rs

1//! Damage calculation pipeline: base + coef * power, then crit, vers, armor, and multipliers.
2
3use wowlab_engine_rng::proc_chance;
4use wowlab_types::{
5    combat::{DamageSchool, HitResult},
6    constants::{HUNDRED, MS_PER_SECOND},
7};
8
9/// Converts weapon damage per second into attack power.
10pub const WEAPON_POWER_COEFFICIENT: f64 = 6.0;
11
12/// Returns the owner-power coefficient for a pet weapon swing.
13#[must_use]
14pub fn pet_swing_power_coefficient(
15    power_from_owner: f64,
16    weapon_speed_ms: u32,
17    weapon_multiplier: f64,
18) -> f64 {
19    power_from_owner * f64::from(weapon_speed_ms) / MS_PER_SECOND / WEAPON_POWER_COEFFICIENT
20        * weapon_multiplier
21}
22
23/// Attack-power coefficient for a normalized pet auto-attack swing.
24#[must_use]
25pub fn pet_swing_ap(swing_ms: u32) -> f64 {
26    pet_swing_power_coefficient(1.0, swing_ms, 1.0)
27}
28
29/// Result of a single damage calculation.
30#[derive(Clone, Debug)]
31#[must_use]
32pub struct DamageResult {
33    pub raw: f64,
34    pub is_crit: bool,
35    pub hit_result: HitResult,
36    pub final_amount: f64,
37}
38
39/// Ordered one-roll attack-table probabilities and landed-hit reductions.
40#[derive(Clone, Copy, Debug)]
41#[must_use]
42pub struct AttackTable {
43    pub miss_chance: f64,
44    pub dodge_chance: f64,
45    pub parry_chance: f64,
46    pub glance_chance: f64,
47    pub block_chance: f64,
48    pub critical_block_chance: f64,
49    pub glance_multiplier: f64,
50    pub block_multiplier: f64,
51    pub critical_block_multiplier: f64,
52}
53
54impl AttackTable {
55    const DEFAULT_BLOCK_MULTIPLIER: f64 = 0.7;
56    const DEFAULT_CRITICAL_BLOCK_MULTIPLIER: f64 = 0.4;
57    const DEFAULT_GLANCE_MULTIPLIER: f64 = 0.75;
58
59    fn resolve(self, crit_chance: f64, rng: &mut (impl FnMut() -> f64 + ?Sized)) -> HitResult {
60        let roll = rng();
61        let mut threshold = self.miss_chance.clamp(0.0, 1.0);
62
63        if roll < threshold {
64            return HitResult::Miss;
65        }
66
67        threshold = (threshold + self.dodge_chance.max(0.0)).min(1.0);
68
69        if roll < threshold {
70            return HitResult::Dodge;
71        }
72
73        threshold = (threshold + self.parry_chance.max(0.0)).min(1.0);
74
75        if roll < threshold {
76            return HitResult::Parry;
77        }
78
79        threshold = (threshold + self.glance_chance.max(0.0)).min(1.0);
80
81        if roll < threshold {
82            return HitResult::Glance;
83        }
84
85        threshold = (threshold + self.block_chance.max(0.0)).min(1.0);
86
87        if roll < threshold {
88            let mut critical_block_roll = || rng();
89
90            return if proc_chance(&mut critical_block_roll, self.critical_block_chance) {
91                HitResult::CritBlock
92            } else {
93                HitResult::Block
94            };
95        }
96
97        threshold = (threshold + crit_chance.clamp(0.0, 1.0)).min(1.0);
98
99        if roll < threshold {
100            HitResult::Crit
101        } else {
102            HitResult::Hit
103        }
104    }
105
106    const fn multiplier(self, result: HitResult, crit_multiplier: f64) -> f64 {
107        match result {
108            HitResult::Miss | HitResult::Dodge | HitResult::Parry => 0.0,
109            HitResult::Glance => self.glance_multiplier,
110            HitResult::Block => self.block_multiplier,
111            HitResult::CritBlock => self.critical_block_multiplier,
112            HitResult::Crit => crit_multiplier,
113            HitResult::Hit => 1.0,
114        }
115    }
116}
117
118impl Default for AttackTable {
119    fn default() -> Self {
120        Self {
121            miss_chance: 0.0,
122            dodge_chance: 0.0,
123            parry_chance: 0.0,
124            glance_chance: 0.0,
125            block_chance: 0.0,
126            critical_block_chance: 0.0,
127            glance_multiplier: Self::DEFAULT_GLANCE_MULTIPLIER,
128            block_multiplier: Self::DEFAULT_BLOCK_MULTIPLIER,
129            critical_block_multiplier: Self::DEFAULT_CRITICAL_BLOCK_MULTIPLIER,
130        }
131    }
132}
133
134/// All inputs needed to compute a damage hit.
135// docref:start combat-formulas-damage-calc-struct
136#[derive(Clone, Debug)]
137#[must_use]
138pub struct DamageCalc {
139    pub base: f64,
140    pub coefficient: f64,
141    pub attack_power: f64,
142    pub crit_chance: f64,
143    pub crit_multiplier: f64,
144    pub attack_table: AttackTable,
145    pub versatility: f64,
146    pub target_armor: f64,
147    pub armor_k: f64,
148    pub damage_multiplier: f64,
149    pub mastery_mult: f64,
150    pub weapon_min: f64,
151    pub weapon_max: f64,
152    pub weapon_multiplier: f64,
153    pub school: DamageSchool,
154}
155// docref:end combat-formulas-damage-calc-struct
156
157impl DamageCalc {
158    /// Compute the damage result, calling `rng` once for the weapon roll (if any) and once for crit.
159    pub fn calculate(&self, rng: &mut (impl FnMut() -> f64 + ?Sized)) -> DamageResult {
160        let weapon_roll = if self.weapon_max > 0.0 {
161            self.weapon_min + rng() * (self.weapon_max - self.weapon_min)
162        } else {
163            0.0
164        };
165        let raw =
166            self.base + weapon_roll * self.weapon_multiplier + self.coefficient * self.attack_power;
167
168        let hit_result = self.attack_table.resolve(self.crit_chance, rng);
169        let is_crit = hit_result.is_crit();
170        let crit_factor = self
171            .attack_table
172            .multiplier(hit_result, self.crit_multiplier);
173
174        let after_crit = raw * crit_factor;
175        let after_vers = after_crit * (1.0 + self.versatility / HUNDRED);
176        let mit = armor_mitigation(self.target_armor, self.armor_k);
177        let after_armor = after_vers * mit;
178        let final_amount = (after_armor * self.damage_multiplier * self.mastery_mult).max(0.0);
179
180        DamageResult {
181            raw,
182            is_crit,
183            hit_result,
184            final_amount,
185        }
186    }
187}
188
189/// Fraction of damage that bypasses armor (`1 - armor / (armor + K)`), in `[0, 1]`.
190// docref:start combat-formulas-armor-mitigation
191#[must_use]
192pub fn armor_mitigation(armor: f64, armor_k: f64) -> f64 {
193    if armor <= 0.0 {
194        return 1.0;
195    }
196    let mitigated = armor / (armor + armor_k);
197    (1.0 - mitigated.clamp(0.0, 1.0)).max(0.0)
198}
199// docref:end combat-formulas-armor-mitigation
200
201#[cfg(test)]
202#[path = "damage/tests.rs"]
203mod tests;