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 {
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#[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}
155impl DamageCalc {
158 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#[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#[cfg(test)]
202#[path = "damage/tests.rs"]
203mod tests;