Skip to main content

wowlab_engine_domain/dbc/semantics/
schools.rs

1use wowlab_types::combat::DamageSchool;
2
3bitflags::bitflags! {
4    /// DBC `SpellSchoolMask`: one bit per damage school.
5    #[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq)]
6    pub struct SpellSchoolMask: u8 {
7        const PHYSICAL = 1 << 0;
8        const HOLY = 1 << 1;
9        const FIRE = 1 << 2;
10        const NATURE = 1 << 3;
11        const FROST = 1 << 4;
12        const SHADOW = 1 << 5;
13        const ARCANE = 1 << 6;
14        const CHAOS = Self::PHYSICAL.bits()
15            | Self::HOLY.bits()
16            | Self::FIRE.bits()
17            | Self::NATURE.bits()
18            | Self::FROST.bits()
19            | Self::SHADOW.bits()
20            | Self::ARCANE.bits();
21    }
22}
23
24impl SpellSchoolMask {
25    #[must_use]
26    pub const fn from_dbc(raw: i32) -> Self {
27        Self::from_bits_truncate(raw.to_le_bytes()[0])
28    }
29
30    /// Resolve the engine's single-school damage channel from a possibly multi-school mask.
31    #[must_use]
32    pub fn primary_school(self) -> DamageSchool {
33        if self.bits() == Self::CHAOS.bits() {
34            return DamageSchool::Chaos;
35        }
36
37        match self.bits().trailing_zeros() {
38            1 => DamageSchool::Holy,
39            2 => DamageSchool::Fire,
40            3 => DamageSchool::Nature,
41            4 => DamageSchool::Frost,
42            5 => DamageSchool::Shadow,
43            6 => DamageSchool::Arcane,
44            _ => DamageSchool::Physical,
45        }
46    }
47}
48
49impl From<DamageSchool> for SpellSchoolMask {
50    fn from(school: DamageSchool) -> Self {
51        match school {
52            DamageSchool::Physical => Self::PHYSICAL,
53            DamageSchool::Holy => Self::HOLY,
54            DamageSchool::Fire => Self::FIRE,
55            DamageSchool::Nature => Self::NATURE,
56            DamageSchool::Frost => Self::FROST,
57            DamageSchool::Shadow => Self::SHADOW,
58            DamageSchool::Arcane => Self::ARCANE,
59            DamageSchool::Chaos => Self::CHAOS,
60        }
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use googletest::prelude::*;
67
68    use super::*;
69
70    fn reference_primary_school(mask: SpellSchoolMask) -> DamageSchool {
71        if mask.bits() == SpellSchoolMask::CHAOS.bits() {
72            return DamageSchool::Chaos;
73        }
74
75        [
76            (SpellSchoolMask::PHYSICAL, DamageSchool::Physical),
77            (SpellSchoolMask::HOLY, DamageSchool::Holy),
78            (SpellSchoolMask::FIRE, DamageSchool::Fire),
79            (SpellSchoolMask::NATURE, DamageSchool::Nature),
80            (SpellSchoolMask::FROST, DamageSchool::Frost),
81            (SpellSchoolMask::SHADOW, DamageSchool::Shadow),
82            (SpellSchoolMask::ARCANE, DamageSchool::Arcane),
83        ]
84        .into_iter()
85        .find_map(|(candidate, school)| mask.contains(candidate).then_some(school))
86        .unwrap_or(DamageSchool::Physical)
87    }
88
89    #[gtest]
90    fn chaos_is_the_complete_school_mask_but_not_physical_resolution() {
91        let chaos = SpellSchoolMask::CHAOS;
92
93        expect_that!(chaos.contains(SpellSchoolMask::PHYSICAL), is_true());
94        expect_that!(chaos.primary_school(), eq(DamageSchool::Chaos));
95    }
96
97    #[gtest]
98    fn multi_school_resolution_uses_lowest_set_school() {
99        let mask = SpellSchoolMask::FIRE | SpellSchoolMask::SHADOW;
100
101        expect_that!(mask.primary_school(), eq(DamageSchool::Fire));
102    }
103
104    #[gtest]
105    fn primary_school_matches_reference_for_every_low_byte_mask() -> Result<()> {
106        for bits in u8::MIN..=u8::MAX {
107            let mask = SpellSchoolMask::from_bits_truncate(bits);
108
109            verify_that!(mask.primary_school(), eq(reference_primary_school(mask)))?;
110        }
111
112        Ok(())
113    }
114}