wowlab_engine_domain/
damage.rs1use wowlab_engine_rng::proc_chance;
4use wowlab_types::{
5 combat::{DamageSchool, HitResult},
6 constants::{HUNDRED, MS_PER_SECOND},
7};
8
9pub const WEAPON_POWER_COEFFICIENT: f64 = 6.0;
11
12#[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#[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#[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#[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 {
62 let roll = rng();
63
64 if self.miss_chance == 0.0
65 && self.dodge_chance == 0.0
66 && self.parry_chance == 0.0
67 && self.glance_chance == 0.0
68 && self.block_chance == 0.0
69 {
70 return if roll < crit_chance.clamp(0.0, 1.0) {
71 HitResult::Crit
72 } else {
73 HitResult::Hit
74 };
75 }
76
77 let mut threshold = self.miss_chance.clamp(0.0, 1.0);
78
79 if roll < threshold {
80 return HitResult::Miss;
81 }
82
83 threshold = (threshold + self.dodge_chance.max(0.0)).min(1.0);
84
85 if roll < threshold {
86 return HitResult::Dodge;
87 }
88
89 threshold = (threshold + self.parry_chance.max(0.0)).min(1.0);
90
91 if roll < threshold {
92 return HitResult::Parry;
93 }
94
95 threshold = (threshold + self.glance_chance.max(0.0)).min(1.0);
96
97 if roll < threshold {
98 return HitResult::Glance;
99 }
100
101 threshold = (threshold + self.block_chance.max(0.0)).min(1.0);
102
103 if roll < threshold {
104 let mut critical_block_roll = || rng();
105
106 return if proc_chance(&mut critical_block_roll, self.critical_block_chance) {
107 HitResult::CritBlock
108 } else {
109 HitResult::Block
110 };
111 }
112
113 threshold = (threshold + crit_chance.clamp(0.0, 1.0)).min(1.0);
114
115 if roll < threshold {
116 HitResult::Crit
117 } else {
118 HitResult::Hit
119 }
120 }
121
122 const fn multiplier(self, result: HitResult, crit_multiplier: f64) -> f64 {
123 match result {
124 HitResult::Miss | HitResult::Dodge | HitResult::Parry => 0.0,
125 HitResult::Glance => self.glance_multiplier,
126 HitResult::Block => self.block_multiplier,
127 HitResult::CritBlock => self.critical_block_multiplier,
128 HitResult::Crit => crit_multiplier,
129 HitResult::Hit => 1.0,
130 }
131 }
132}
133
134impl Default for AttackTable {
135 fn default() -> Self {
136 Self {
137 miss_chance: 0.0,
138 dodge_chance: 0.0,
139 parry_chance: 0.0,
140 glance_chance: 0.0,
141 block_chance: 0.0,
142 critical_block_chance: 0.0,
143 glance_multiplier: Self::DEFAULT_GLANCE_MULTIPLIER,
144 block_multiplier: Self::DEFAULT_BLOCK_MULTIPLIER,
145 critical_block_multiplier: Self::DEFAULT_CRITICAL_BLOCK_MULTIPLIER,
146 }
147 }
148}
149
150#[derive(Clone, Debug)]
153#[must_use]
154pub struct DamageCalc {
155 pub base: f64,
156 pub coefficient: f64,
157 pub attack_power: f64,
158 pub crit_chance: f64,
159 pub crit_multiplier: f64,
160 pub attack_table: AttackTable,
161 pub versatility: f64,
162 pub target_armor: f64,
163 pub armor_k: f64,
164 pub damage_multiplier: f64,
165 pub mastery_mult: f64,
166 pub weapon_min: f64,
167 pub weapon_max: f64,
168 pub weapon_multiplier: f64,
169 pub school: DamageSchool,
170}
171impl DamageCalc {
174 pub fn calculate(&self, rng: &mut (impl FnMut() -> f64 + ?Sized)) -> DamageResult {
176 let weapon_roll = if self.weapon_max > 0.0 {
177 self.weapon_min + rng() * (self.weapon_max - self.weapon_min)
178 } else {
179 0.0
180 };
181 let raw =
182 self.base + weapon_roll * self.weapon_multiplier + self.coefficient * self.attack_power;
183
184 let hit_result = self.attack_table.resolve(self.crit_chance, rng);
185 let is_crit = hit_result.is_crit();
186 let crit_factor = self
187 .attack_table
188 .multiplier(hit_result, self.crit_multiplier);
189
190 let after_crit = raw * crit_factor;
191 let after_vers = after_crit * (1.0 + self.versatility / HUNDRED);
192 let mit = armor_mitigation(self.target_armor, self.armor_k);
193 let after_armor = after_vers * mit;
194 let final_amount = (after_armor * self.damage_multiplier * self.mastery_mult).max(0.0);
195
196 DamageResult {
197 raw,
198 is_crit,
199 hit_result,
200 final_amount,
201 }
202 }
203}
204
205#[must_use]
208pub fn armor_mitigation(armor: f64, armor_k: f64) -> f64 {
209 if armor <= 0.0 {
210 return 1.0;
211 }
212 let mitigated = armor / (armor + armor_k);
213 (1.0 - mitigated.clamp(0.0, 1.0)).max(0.0)
214}
215#[cfg(test)]
218#[path = "damage/tests.rs"]
219mod tests;