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)]
21pub 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 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}