wowlab_engine_domain/dbc/
weapon.rs1use wowlab_types::{
4 constants::MS_PER_SECOND,
5 data::{ItemDamageScalingFlat, ItemQuality, ResolvedItem, WeaponStats},
6};
7
8use super::{InventoryType, WeaponSubclass};
9
10const DAMAGE_RANGE_HALF: f64 = 2.0;
11
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14#[non_exhaustive]
15pub enum WeaponClass {
16 OneHand,
17 TwoHand,
18 OneHandCaster,
19 TwoHandCaster,
20}
21
22impl WeaponClass {
23 #[must_use]
25 pub const fn weapon_type_string(self) -> &'static str {
26 match self {
27 WeaponClass::OneHand => "one_hand",
28 WeaponClass::TwoHand => "two_hand",
29 WeaponClass::OneHandCaster => "one_hand_caster",
30 WeaponClass::TwoHandCaster => "two_hand_caster",
31 }
32 }
33}
34
35#[must_use]
37pub fn classify_weapon(
38 inventory_type: i32,
39 item_subclass: i32,
40 is_caster: bool,
41) -> Option<WeaponClass> {
42 match InventoryType::try_from(inventory_type).ok()? {
43 InventoryType::Weapon | InventoryType::MainHandWeapon | InventoryType::OffHandWeapon => {
44 if is_caster {
45 Some(WeaponClass::OneHandCaster)
46 } else {
47 Some(WeaponClass::OneHand)
48 }
49 }
50 InventoryType::TwoHandWeapon => {
51 if is_caster {
52 Some(WeaponClass::TwoHandCaster)
53 } else {
54 Some(WeaponClass::TwoHand)
55 }
56 }
57 InventoryType::Ranged | InventoryType::Thrown | InventoryType::RangedRight => {
58 match WeaponSubclass::try_from(item_subclass).ok()? {
59 WeaponSubclass::Bow | WeaponSubclass::Gun | WeaponSubclass::Crossbow => {
60 Some(WeaponClass::TwoHand)
61 }
62 WeaponSubclass::Thrown => Some(WeaponClass::OneHand),
63 WeaponSubclass::Wand => Some(WeaponClass::OneHandCaster),
64 }
65 }
66 _ => None,
67 }
68}
69
70const NORMALIZED_SPEED_1H_S: f64 = 2.4;
71const NORMALIZED_SPEED_2H_S: f64 = 3.6;
72
73#[must_use]
75pub const fn normalized_speed_s(class: WeaponClass) -> f64 {
76 match class {
77 WeaponClass::OneHand | WeaponClass::OneHandCaster => NORMALIZED_SPEED_1H_S,
78 WeaponClass::TwoHand | WeaponClass::TwoHandCaster => NORMALIZED_SPEED_2H_S,
79 }
80}
81
82#[must_use]
84pub fn resolve_weapon_stats(
85 item: &ResolvedItem,
86 damage: &ItemDamageScalingFlat,
87 spec_is_caster: bool,
88) -> Option<WeaponStats> {
89 let class = classify_weapon(item.inventory_type, item.subclass_id, spec_is_caster)?;
90
91 let quality_index = match ItemQuality::from_dbc(item.quality) {
92 Some(ItemQuality::Heirloom) => {
93 tracing::warn!(
94 item_id = item.id,
95 quality_index = ItemQuality::Artifact as i32,
96 "Heirloom weapon quality uses the highest quality scaling column"
97 );
98
99 ItemQuality::Artifact as i32
100 }
101 Some(quality) => quality as i32,
102 None => {
103 tracing::warn!(
104 item_id = item.id,
105 quality = item.quality,
106 "weapon has unsupported quality; weapon DPS skipped"
107 );
108
109 return None;
110 }
111 };
112
113 let dps = match damage.quality(quality_index) {
114 Some(value) if value > 0.0 => value,
115 _ => {
116 tracing::warn!(
117 item_id = item.id,
118 item_level = item.item_level,
119 quality_index,
120 "weapon damage scaling column is missing or zero"
121 );
122
123 0.0
124 }
125 };
126
127 let variance = if item.dmg_variance > 0.0 {
128 f64::from(item.dmg_variance)
129 } else {
130 tracing::warn!(
131 item_id = item.id,
132 "weapon damage variance is missing or zero"
133 );
134
135 0.0
136 };
137
138 let speed_ms = if item.speed > 0 {
139 wowlab_types::numeric::i32_to_u32_nonnegative(item.speed)
140 } else {
141 tracing::warn!(
142 item_id = item.id,
143 "weapon speed is missing or zero; auto-attack will skip this weapon"
144 );
145
146 0
147 };
148
149 let average = dps * (f64::from(speed_ms) / MS_PER_SECOND);
150 let minimum = average * (1.0 - variance / DAMAGE_RANGE_HALF);
151 let maximum = average * (1.0 + variance / DAMAGE_RANGE_HALF);
152
153 Some(WeaponStats {
154 min_damage: minimum,
155 max_damage: maximum,
156 speed_ms,
157 normalized_speed_s: normalized_speed_s(class),
158 item_class: item.class_id,
159 subclass: item.subclass_id,
160 inventory_type: item.inventory_type,
161 })
162}
163
164#[cfg(test)]
165mod tests {
166 use googletest::prelude::*;
167
168 use super::*;
169
170 #[gtest]
171 fn test_mainhand_physical_is_one_hand() {
172 expect_that!(
173 classify_weapon(InventoryType::MainHandWeapon as i32, 7, false),
174 eq(Some(WeaponClass::OneHand))
175 );
176 }
177
178 #[gtest]
179 fn test_mainhand_caster_is_one_hand_caster() {
180 expect_that!(
181 classify_weapon(InventoryType::MainHandWeapon as i32, 15, true),
182 eq(Some(WeaponClass::OneHandCaster))
183 );
184 }
185
186 #[gtest]
187 fn test_two_hand_physical() {
188 expect_that!(
189 classify_weapon(InventoryType::TwoHandWeapon as i32, 1, false),
190 eq(Some(WeaponClass::TwoHand))
191 );
192 }
193
194 #[gtest]
195 fn test_two_hand_caster_staff() {
196 expect_that!(
197 classify_weapon(InventoryType::TwoHandWeapon as i32, 10, true),
198 eq(Some(WeaponClass::TwoHandCaster))
199 );
200 }
201
202 #[gtest]
203 fn test_bow_is_two_hand() {
204 expect_that!(
205 classify_weapon(
206 InventoryType::RangedRight as i32,
207 WeaponSubclass::Bow as i32,
208 false
209 ),
210 eq(Some(WeaponClass::TwoHand))
211 );
212 }
213
214 #[gtest]
215 fn test_wand_is_one_hand_caster() {
216 expect_that!(
217 classify_weapon(
218 InventoryType::RangedRight as i32,
219 WeaponSubclass::Wand as i32,
220 false
221 ),
222 eq(Some(WeaponClass::OneHandCaster))
223 );
224 }
225
226 #[gtest]
227 fn test_thrown_is_one_hand() {
228 expect_that!(
229 classify_weapon(
230 InventoryType::Thrown as i32,
231 WeaponSubclass::Thrown as i32,
232 false
233 ),
234 eq(Some(WeaponClass::OneHand))
235 );
236 }
237
238 #[gtest]
239 fn test_non_weapon_returns_none() {
240 expect_that!(classify_weapon(5, 0, false), eq(None));
241 }
242
243 #[gtest]
244 fn test_weapon_type_strings() {
245 expect_that!(WeaponClass::OneHand.weapon_type_string(), eq("one_hand"));
246 expect_that!(WeaponClass::TwoHand.weapon_type_string(), eq("two_hand"));
247 expect_that!(
248 WeaponClass::OneHandCaster.weapon_type_string(),
249 eq("one_hand_caster")
250 );
251 expect_that!(
252 WeaponClass::TwoHandCaster.weapon_type_string(),
253 eq("two_hand_caster")
254 );
255 }
256
257 #[gtest]
258 fn test_normalized_speeds() {
259 expect_that!(
260 normalized_speed_s(WeaponClass::OneHand),
261 near(2.4, f64::EPSILON)
262 );
263 expect_that!(
264 normalized_speed_s(WeaponClass::OneHandCaster),
265 near(2.4, f64::EPSILON)
266 );
267 expect_that!(
268 normalized_speed_s(WeaponClass::TwoHand),
269 near(3.6, f64::EPSILON)
270 );
271 expect_that!(
272 normalized_speed_s(WeaponClass::TwoHandCaster),
273 near(3.6, f64::EPSILON)
274 );
275 }
276}