Skip to main content

wowlab_types/types/combat/
resource.rs

1#[derive(
2    Clone,
3    Copy,
4    Debug,
5    Eq,
6    Hash,
7    PartialEq,
8    derive_more::Display,
9    num_enum::IntoPrimitive,
10    num_enum::TryFromPrimitive,
11    serde::Deserialize,
12    serde::Serialize,
13    strum::EnumCount,
14    strum::EnumIter,
15    strum::EnumString,
16)]
17#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
18#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
19#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
20#[repr(u8)]
21// #t(rust_non_exhaustive_on_public) WoW resource types are game-defined
22pub enum ResourceType {
23    Mana = 0,
24    Rage = 1,
25    Focus = 2,
26    Energy = 3,
27    #[strum(serialize = "combo_points", serialize = "combopoints")]
28    ComboPoints = 4,
29    Runes = 5,
30    #[strum(serialize = "runic_power", serialize = "runicpower")]
31    RunicPower = 6,
32    #[strum(serialize = "soul_shards", serialize = "soulshards")]
33    SoulShards = 7,
34    #[serde(alias = "AstralPower")]
35    #[strum(
36        serialize = "lunar_power",
37        serialize = "lunarpower",
38        serialize = "astral_power",
39        serialize = "astralpower"
40    )]
41    LunarPower = 8,
42    #[strum(serialize = "holy_power", serialize = "holypower")]
43    HolyPower = 9,
44    // Discriminants preserve the gapped `PowerType.power_type_enum` ABI.
45    Alternate = 10,
46    Maelstrom = 11,
47    Chi = 12,
48    Insanity = 13,
49    #[strum(serialize = "arcane_charges", serialize = "arcanecharges")]
50    ArcaneCharges = 16,
51    Fury = 17,
52    Pain = 18,
53    Essence = 19,
54    AlternateQuest = 23,
55    AlternateEncounter = 24,
56    AlternateMount = 25,
57}
58
59impl ResourceType {
60    pub const COUNT: usize = <Self as strum::EnumCount>::COUNT;
61
62    const BASE_MAX_MANA: u32 = 50_000;
63    const BASE_MAX_PERCENTAGE: u32 = 100;
64    const BASE_MAX_COMBO: u32 = 5;
65    const BASE_MAX_RUNES: u32 = 6;
66    const BASE_MAX_ARCANE_CHARGES: u32 = 4;
67
68    const ENERGY_REGEN_PER_SEC: f32 = 10.0;
69    const FOCUS_REGEN_PER_SEC: f32 = 5.0;
70    const MANA_REGEN_PCT_PER_SEC: f32 = 0.01;
71
72    #[must_use]
73    pub const fn base_max(self) -> u32 {
74        match self {
75            Self::Mana => Self::BASE_MAX_MANA,
76            Self::ComboPoints | Self::SoulShards | Self::HolyPower | Self::Chi | Self::Essence => {
77                Self::BASE_MAX_COMBO
78            }
79            Self::Runes => Self::BASE_MAX_RUNES,
80            Self::Rage
81            | Self::Focus
82            | Self::Energy
83            | Self::RunicPower
84            | Self::LunarPower
85            | Self::Maelstrom
86            | Self::Insanity
87            | Self::Fury
88            | Self::Pain
89            | Self::Alternate
90            | Self::AlternateQuest
91            | Self::AlternateEncounter
92            | Self::AlternateMount => Self::BASE_MAX_PERCENTAGE,
93            Self::ArcaneCharges => Self::BASE_MAX_ARCANE_CHARGES,
94        }
95    }
96
97    #[must_use]
98    pub const fn has_passive_regen(self) -> bool {
99        matches!(self, Self::Mana | Self::Focus | Self::Energy)
100    }
101
102    #[must_use]
103    pub const fn base_regen_per_sec(self) -> f32 {
104        match self {
105            Self::Energy => Self::ENERGY_REGEN_PER_SEC,
106            Self::Focus => Self::FOCUS_REGEN_PER_SEC,
107            Self::Mana => Self::MANA_REGEN_PCT_PER_SEC,
108            _ => 0.0,
109        }
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use googletest::prelude::*;
116    use rstest::rstest;
117
118    use super::*;
119
120    const DISPLAY_NAMES: &[(ResourceType, &str)] = &[
121        (ResourceType::Mana, "Mana"),
122        (ResourceType::Rage, "Rage"),
123        (ResourceType::Focus, "Focus"),
124        (ResourceType::Energy, "Energy"),
125        (ResourceType::ComboPoints, "ComboPoints"),
126        (ResourceType::Runes, "Runes"),
127        (ResourceType::RunicPower, "RunicPower"),
128        (ResourceType::SoulShards, "SoulShards"),
129        (ResourceType::LunarPower, "LunarPower"),
130        (ResourceType::HolyPower, "HolyPower"),
131        (ResourceType::Alternate, "Alternate"),
132        (ResourceType::Maelstrom, "Maelstrom"),
133        (ResourceType::Chi, "Chi"),
134        (ResourceType::Insanity, "Insanity"),
135        (ResourceType::ArcaneCharges, "ArcaneCharges"),
136        (ResourceType::Fury, "Fury"),
137        (ResourceType::Pain, "Pain"),
138        (ResourceType::Essence, "Essence"),
139        (ResourceType::AlternateQuest, "AlternateQuest"),
140        (ResourceType::AlternateEncounter, "AlternateEncounter"),
141        (ResourceType::AlternateMount, "AlternateMount"),
142    ];
143
144    #[gtest]
145    #[rstest]
146    #[case::mana(ResourceType::Mana, 50_000)]
147    #[case::rage(ResourceType::Rage, 100)]
148    #[case::focus(ResourceType::Focus, 100)]
149    #[case::energy(ResourceType::Energy, 100)]
150    #[case::combo_points(ResourceType::ComboPoints, 5)]
151    #[case::soul_shards(ResourceType::SoulShards, 5)]
152    #[case::holy_power(ResourceType::HolyPower, 5)]
153    #[case::chi(ResourceType::Chi, 5)]
154    #[case::essence(ResourceType::Essence, 5)]
155    #[case::runes(ResourceType::Runes, 6)]
156    #[case::arcane_charges(ResourceType::ArcaneCharges, 4)]
157    #[case::runic_power(ResourceType::RunicPower, 100)]
158    #[case::lunar_power(ResourceType::LunarPower, 100)]
159    #[case::maelstrom(ResourceType::Maelstrom, 100)]
160    #[case::insanity(ResourceType::Insanity, 100)]
161    #[case::fury(ResourceType::Fury, 100)]
162    #[case::pain(ResourceType::Pain, 100)]
163    #[case::alternate(ResourceType::Alternate, 100)]
164    #[case::alternate_quest(ResourceType::AlternateQuest, 100)]
165    #[case::alternate_encounter(ResourceType::AlternateEncounter, 100)]
166    #[case::alternate_mount(ResourceType::AlternateMount, 100)]
167    fn base_max(#[case] resource: ResourceType, #[case] expected: u32) -> Result<()> {
168        verify_that!(resource.base_max(), eq(expected))
169    }
170
171    #[gtest]
172    #[rstest]
173    #[case::mana(ResourceType::Mana, true)]
174    #[case::focus(ResourceType::Focus, true)]
175    #[case::energy(ResourceType::Energy, true)]
176    #[case::rage(ResourceType::Rage, false)]
177    #[case::runes(ResourceType::Runes, false)]
178    #[case::combo_points(ResourceType::ComboPoints, false)]
179    fn has_passive_regen(#[case] resource: ResourceType, #[case] expected: bool) -> Result<()> {
180        verify_that!(resource.has_passive_regen(), eq(expected))
181    }
182
183    #[gtest]
184    #[rstest]
185    #[case::energy(ResourceType::Energy, 10.0)]
186    #[case::focus(ResourceType::Focus, 5.0)]
187    #[case::mana(ResourceType::Mana, 0.01)]
188    #[case::rage(ResourceType::Rage, 0.0)]
189    fn base_regen_per_sec(#[case] resource: ResourceType, #[case] expected: f32) -> Result<()> {
190        verify_that!(resource.base_regen_per_sec(), near(expected, 1e-9))
191    }
192
193    #[gtest]
194    #[rstest]
195    #[case::mana(0, ResourceType::Mana)]
196    #[case::lunar_power(8, ResourceType::LunarPower)]
197    #[case::arcane_charges(16, ResourceType::ArcaneCharges)]
198    #[case::alternate_mount(25, ResourceType::AlternateMount)]
199    fn try_from_primitive_ok(#[case] input: u8, #[case] expected: ResourceType) -> Result<()> {
200        verify_that!(ResourceType::try_from(input).ok(), some(eq(expected)))
201    }
202
203    #[gtest]
204    #[rstest]
205    #[case::gap_15(15)]
206    #[case::gap_255(255)]
207    fn try_from_primitive_gap(#[case] input: u8) -> Result<()> {
208        verify_that!(ResourceType::try_from(input).ok(), none())
209    }
210
211    #[gtest]
212    #[rstest]
213    #[case::astral_power_alias("\"AstralPower\"")]
214    #[case::canonical("\"LunarPower\"")]
215    fn serde_lunar_power_alias(#[case] input: &str) -> Result<()> {
216        verify_that!(
217            serde_json::from_str::<ResourceType>(input).ok(),
218            some(eq(ResourceType::LunarPower))
219        )
220    }
221
222    #[gtest]
223    #[rstest]
224    #[case::mana("mana", ResourceType::Mana)]
225    #[case::rage("rage", ResourceType::Rage)]
226    #[case::focus("focus", ResourceType::Focus)]
227    #[case::energy("energy", ResourceType::Energy)]
228    #[case::combo_points("combo_points", ResourceType::ComboPoints)]
229    #[case::combo_points_compact("combopoints", ResourceType::ComboPoints)]
230    #[case::runes("runes", ResourceType::Runes)]
231    #[case::runic_power("runic_power", ResourceType::RunicPower)]
232    #[case::runic_power_compact("runicpower", ResourceType::RunicPower)]
233    #[case::soul_shards("soul_shards", ResourceType::SoulShards)]
234    #[case::soul_shards_compact("soulshards", ResourceType::SoulShards)]
235    #[case::lunar_power("lunar_power", ResourceType::LunarPower)]
236    #[case::lunar_power_compact("lunarpower", ResourceType::LunarPower)]
237    #[case::astral_power("astral_power", ResourceType::LunarPower)]
238    #[case::astral_power_compact("astralpower", ResourceType::LunarPower)]
239    #[case::holy_power("holy_power", ResourceType::HolyPower)]
240    #[case::holy_power_compact("holypower", ResourceType::HolyPower)]
241    #[case::maelstrom("maelstrom", ResourceType::Maelstrom)]
242    #[case::chi("chi", ResourceType::Chi)]
243    #[case::insanity("insanity", ResourceType::Insanity)]
244    #[case::arcane_charges("arcane_charges", ResourceType::ArcaneCharges)]
245    #[case::arcane_charges_compact("arcanecharges", ResourceType::ArcaneCharges)]
246    #[case::fury("fury", ResourceType::Fury)]
247    #[case::pain("pain", ResourceType::Pain)]
248    #[case::essence("essence", ResourceType::Essence)]
249    fn parse_rotation_resource_alias(
250        #[case] input: &str,
251        #[case] expected: ResourceType,
252    ) -> Result<()> {
253        verify_that!(input.parse::<ResourceType>().ok(), some(eq(expected)))
254    }
255
256    #[gtest]
257    #[rstest]
258    #[case::single_word("MaNa", ResourceType::Mana)]
259    #[case::snake_case("CoMbO_PoInTs", ResourceType::ComboPoints)]
260    #[case::compact("RuNiCpOwEr", ResourceType::RunicPower)]
261    #[case::astral_alias("AsTrAlPoWeR", ResourceType::LunarPower)]
262    fn parse_rotation_resource_is_ascii_case_insensitive(
263        #[case] input: &str,
264        #[case] expected: ResourceType,
265    ) -> Result<()> {
266        verify_that!(input.parse::<ResourceType>().ok(), some(eq(expected)))
267    }
268
269    #[gtest]
270    #[rstest]
271    #[case::alternate("alternate", ResourceType::Alternate)]
272    #[case::alternate_quest("alternate_quest", ResourceType::AlternateQuest)]
273    #[case::alternate_encounter("alternate_encounter", ResourceType::AlternateEncounter)]
274    #[case::alternate_mount("alternate_mount", ResourceType::AlternateMount)]
275    fn parse_current_resource_variant(
276        #[case] input: &str,
277        #[case] expected: ResourceType,
278    ) -> Result<()> {
279        verify_that!(input.parse::<ResourceType>().ok(), some(eq(expected)))
280    }
281
282    #[gtest]
283    fn display_contract_remains_pascal_case() -> Result<()> {
284        verify_that!(DISPLAY_NAMES.len(), eq(ResourceType::COUNT))?;
285
286        for &(resource, expected) in DISPLAY_NAMES {
287            verify_that!(resource.to_string(), eq(expected))?;
288        }
289
290        Ok(())
291    }
292
293    #[gtest]
294    fn serde_contract_remains_pascal_case() -> Result<()> {
295        verify_that!(DISPLAY_NAMES.len(), eq(ResourceType::COUNT))?;
296
297        for &(resource, name) in DISPLAY_NAMES {
298            let encoded = serde_json::to_string(&resource).or_fail()?;
299
300            verify_that!(encoded.as_str(), eq(format!("\"{name}\"")))?;
301
302            let decoded = serde_json::from_str::<ResourceType>(&encoded).or_fail()?;
303
304            verify_that!(decoded, eq(resource))?;
305        }
306
307        verify_that!(
308            serde_json::from_str::<ResourceType>("\"lunar_power\"").ok(),
309            none()
310        )
311    }
312}