wowlab_engine_domain/dbc/semantics/
schools.rs1use wowlab_types::combat::DamageSchool;
2
3bitflags::bitflags! {
4 #[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 #[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}