Skip to main content

wowlab_engine_domain/dbc/
weapon.rs

1//! Weapon classification into the `game.item_damage_scaling.weapon_type` key.
2
3use 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/// Weapon classification bucket keyed by `game.item_damage_scaling.weapon_type`.
13#[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    /// The `weapon_type` string stored in `game.item_damage_scaling`.
24    #[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/// Classify a weapon by `inventory_type`, `item_subclass`, and caster-weapon flag; `None` for non-weapons.
36#[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/// Normalized weapon speed (seconds) for AP-contribution.
74#[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/// Resolve an equipped weapon's damage range from its DBC item and scaling rows.
83#[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}