Skip to main content

wowlab_types/types/game/
class.rs

1use serde::{Deserialize, Serialize};
2
3use super::super::combat::ResourceType;
4
5#[derive(
6    Clone,
7    Copy,
8    Debug,
9    Eq,
10    Hash,
11    PartialEq,
12    num_enum::IntoPrimitive,
13    num_enum::TryFromPrimitive,
14    serde::Deserialize,
15    serde::Serialize,
16    strum::Display,
17    strum::EnumCount,
18    strum::EnumIter,
19    strum::EnumString,
20)]
21#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
22#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
23#[repr(u8)]
24// #t(rust_non_exhaustive_on_public) WoW class IDs are game-defined
25pub enum ClassId {
26    Warrior = 1,
27    Paladin = 2,
28    Hunter = 3,
29    Rogue = 4,
30    Priest = 5,
31    #[strum(serialize = "Death Knight", serialize = "DeathKnight")]
32    DeathKnight = 6,
33    Shaman = 7,
34    Mage = 8,
35    Warlock = 9,
36    Monk = 10,
37    Druid = 11,
38    #[strum(serialize = "Demon Hunter", serialize = "DemonHunter")]
39    DemonHunter = 12,
40    Evoker = 13,
41}
42
43macro_rules! spec_table {
44    (
45        $(
46            $variant:ident => wow: $wow:expr, toml: $toml:literal, slug: $slug:literal,
47                class: $class:ident, resource: $resource:ident, role: $role:ident
48        ),+ $(,)?
49    ) => {
50        impl SpecId {
51            pub const fn wow_spec_id(self) -> u32 {
52                match self { $(Self::$variant => $wow),+ }
53            }
54
55            pub const fn from_wow_spec_id(wow_id: u32) -> Option<Self> {
56                Some(match wow_id {
57                    $($wow => Self::$variant,)+
58                    _ => return None,
59                })
60            }
61
62            pub const fn toml_key(self) -> &'static str {
63                match self { $(Self::$variant => $toml),+ }
64            }
65
66            pub fn from_toml_key(value: &str) -> Option<Self> {
67                Some(match value {
68                    $($toml => Self::$variant,)+
69                    _ => return None,
70                })
71            }
72
73            pub fn from_manifest_slug(slug: &str) -> Option<Self> {
74                Some(match slug {
75                    $($slug => Self::$variant,)+
76                    _ => return None,
77                })
78            }
79
80            pub const fn class(self) -> ClassId {
81                match self { $(Self::$variant => ClassId::$class),+ }
82            }
83
84            /// `{spec}_{class}` slug.
85            pub const fn slug(self) -> &'static str {
86                match self { $(Self::$variant => $slug),+ }
87            }
88
89            pub const fn primary_resource(self) -> ResourceType {
90                match self { $(Self::$variant => ResourceType::$resource),+ }
91            }
92
93            pub const fn is_dps(self) -> bool {
94                match self { $(Self::$variant => spec_table!(@is_role dps $role)),+ }
95            }
96
97            pub const fn is_tank(self) -> bool {
98                match self { $(Self::$variant => spec_table!(@is_role tank $role)),+ }
99            }
100
101            pub const fn is_healer(self) -> bool {
102                match self { $(Self::$variant => spec_table!(@is_role healer $role)),+ }
103            }
104        }
105    };
106
107    (@is_role dps dps) => { true };
108    (@is_role tank tank) => { true };
109    (@is_role healer healer) => { true };
110    (@is_role $want:ident $have:ident) => { false };
111}
112
113#[derive(
114    Clone,
115    Copy,
116    Debug,
117    Eq,
118    Hash,
119    PartialEq,
120    num_enum::IntoPrimitive,
121    num_enum::TryFromPrimitive,
122    serde::Deserialize,
123    serde::Serialize,
124    strum::Display,
125    strum::EnumCount,
126    strum::EnumIter,
127    strum::EnumString,
128)]
129#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
130#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
131#[repr(u8)]
132// #t(rust_non_exhaustive_on_public) WoW spec IDs are game-defined
133pub enum SpecId {
134    Arms = 1,
135    Fury = 2,
136    #[strum(serialize = "Protection Warrior", serialize = "ProtWarrior")]
137    ProtWarrior = 3,
138    #[strum(serialize = "Holy Paladin", serialize = "HolyPaladin")]
139    HolyPaladin = 4,
140    #[strum(serialize = "Protection Paladin", serialize = "ProtPaladin")]
141    ProtPaladin = 5,
142    Retribution = 6,
143    #[strum(
144        serialize = "Beast Mastery",
145        serialize = "BeastMastery",
146        serialize = "bm"
147    )]
148    BeastMastery = 7,
149    #[strum(serialize = "Marksmanship", serialize = "mm")]
150    Marksmanship = 8,
151    Survival = 9,
152    Assassination = 10,
153    Outlaw = 11,
154    Subtlety = 12,
155    Discipline = 13,
156    #[strum(serialize = "Holy Priest", serialize = "HolyPriest")]
157    HolyPriest = 14,
158    Shadow = 15,
159    Blood = 16,
160    #[strum(serialize = "Frost DK", serialize = "FrostDK")]
161    FrostDK = 17,
162    Unholy = 18,
163    Elemental = 19,
164    Enhancement = 20,
165    #[strum(serialize = "Restoration Shaman", serialize = "RestoShaman")]
166    RestoShaman = 21,
167    Arcane = 22,
168    Fire = 23,
169    #[strum(serialize = "Frost Mage", serialize = "FrostMage")]
170    FrostMage = 24,
171    Affliction = 25,
172    Demonology = 26,
173    Destruction = 27,
174    Brewmaster = 28,
175    Mistweaver = 29,
176    Windwalker = 30,
177    Balance = 31,
178    Feral = 32,
179    Guardian = 33,
180    #[strum(serialize = "Restoration Druid", serialize = "RestoDruid")]
181    RestoDruid = 34,
182    Havoc = 35,
183    Vengeance = 36,
184    Devourer = 40,
185    Devastation = 37,
186    Preservation = 38,
187    Augmentation = 39,
188}
189
190#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
191#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
192#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
193pub struct ImplementedSpecInfo {
194    pub spec_id: u32,
195    pub class_id: u32,
196    pub class_name: String,
197    pub spec_name: String,
198    pub display_name: String,
199    pub slug: String,
200    pub spell_count: usize,
201    pub aura_count: usize,
202    pub talent_count: usize,
203}
204
205wowlab_engine_macros::define_error! {
206/// Failure to parse a specialization identifier.
207#[derive(Debug)]
208pub struct SpecIdParseError {
209    kind: SpecIdParseErrorKind,
210}
211
212#[derive(Debug, thiserror::Error)]
213enum SpecIdParseErrorKind {
214    #[error("unknown wow spec id: {wow_id}")]
215    UnknownWowId { wow_id: u32 },
216    #[error("unknown spec key: '{value}'")]
217    UnknownTomlKey { value: String },
218}
219}
220
221impl SpecIdParseError {
222    const fn unknown_wow_id(wow_id: u32) -> Self {
223        Self {
224            kind: SpecIdParseErrorKind::UnknownWowId { wow_id },
225        }
226    }
227
228    fn unknown_toml_key(value: impl Into<String>) -> Self {
229        Self {
230            kind: SpecIdParseErrorKind::UnknownTomlKey {
231                value: value.into(),
232            },
233        }
234    }
235}
236
237impl SpecId {
238    pub const COUNT: usize = <Self as strum::EnumCount>::COUNT;
239
240    /// Parse a game specialization ID.
241    ///
242    /// # Errors
243    ///
244    /// Returns an error when the ID is not in the specialization table.
245    pub fn parse_wow_spec_id(wow_id: u32) -> Result<Self, SpecIdParseError> {
246        Self::from_wow_spec_id(wow_id).ok_or_else(|| SpecIdParseError::unknown_wow_id(wow_id))
247    }
248
249    /// Parse a specialization TOML key.
250    ///
251    /// # Errors
252    ///
253    /// Returns an error when the key is not in the specialization table.
254    pub fn parse_toml_key(value: &str) -> Result<Self, SpecIdParseError> {
255        Self::from_toml_key(value).ok_or_else(|| SpecIdParseError::unknown_toml_key(value))
256    }
257}
258
259spec_table! {
260    Arms         => wow: 71,   toml: "arms",          slug: "arms_warrior",              class: Warrior,     resource: Rage,        role: dps,
261    Fury         => wow: 72,   toml: "fury",          slug: "fury_warrior",              class: Warrior,     resource: Rage,        role: dps,
262    ProtWarrior  => wow: 73,   toml: "prot_warrior",  slug: "protection_warrior",        class: Warrior,     resource: Rage,        role: tank,
263    HolyPaladin  => wow: 65,   toml: "holy_paladin",  slug: "holy_paladin",              class: Paladin,     resource: HolyPower,   role: healer,
264    ProtPaladin  => wow: 66,   toml: "prot_paladin",  slug: "protection_paladin",        class: Paladin,     resource: HolyPower,   role: tank,
265    Retribution  => wow: 70,   toml: "retribution",   slug: "retribution_paladin",       class: Paladin,     resource: HolyPower,   role: dps,
266    BeastMastery => wow: 253,  toml: "beast_mastery", slug: "beast_mastery_hunter",      class: Hunter,      resource: Focus,       role: dps,
267    Marksmanship => wow: 254,  toml: "marksmanship",  slug: "marksmanship_hunter",       class: Hunter,      resource: Focus,       role: dps,
268    Survival     => wow: 255,  toml: "survival",      slug: "survival_hunter",           class: Hunter,      resource: Focus,       role: dps,
269    Assassination => wow: 259, toml: "assassination", slug: "assassination_rogue",       class: Rogue,       resource: Energy,      role: dps,
270    Outlaw       => wow: 260,  toml: "outlaw",        slug: "outlaw_rogue",              class: Rogue,       resource: Energy,      role: dps,
271    Subtlety     => wow: 261,  toml: "subtlety",      slug: "subtlety_rogue",            class: Rogue,       resource: Energy,      role: dps,
272    Discipline   => wow: 256,  toml: "discipline",    slug: "discipline_priest",         class: Priest,      resource: Mana,        role: healer,
273    HolyPriest   => wow: 257,  toml: "holy_priest",   slug: "holy_priest",               class: Priest,      resource: Mana,        role: healer,
274    Shadow       => wow: 258,  toml: "shadow",        slug: "shadow_priest",             class: Priest,      resource: Mana,        role: dps,
275    Blood        => wow: 250,  toml: "blood",         slug: "blood_death_knight",        class: DeathKnight, resource: RunicPower,  role: tank,
276    FrostDK      => wow: 251,  toml: "frost_dk",      slug: "frost_death_knight",        class: DeathKnight, resource: RunicPower,  role: dps,
277    Unholy       => wow: 252,  toml: "unholy",        slug: "unholy_death_knight",       class: DeathKnight, resource: RunicPower,  role: dps,
278    Elemental    => wow: 262,  toml: "elemental",     slug: "elemental_shaman",          class: Shaman,      resource: Maelstrom,   role: dps,
279    Enhancement  => wow: 263,  toml: "enhancement",   slug: "enhancement_shaman",        class: Shaman,      resource: Maelstrom,   role: dps,
280    RestoShaman  => wow: 264,  toml: "resto_shaman",  slug: "restoration_shaman",        class: Shaman,      resource: Maelstrom,   role: healer,
281    Arcane       => wow: 62,   toml: "arcane",        slug: "arcane_mage",               class: Mage,        resource: Mana,        role: dps,
282    Fire         => wow: 63,   toml: "fire",          slug: "fire_mage",                 class: Mage,        resource: Mana,        role: dps,
283    FrostMage    => wow: 64,   toml: "frost_mage",    slug: "frost_mage",                class: Mage,        resource: Mana,        role: dps,
284    Affliction   => wow: 265,  toml: "affliction",    slug: "affliction_warlock",        class: Warlock,     resource: SoulShards,  role: dps,
285    Demonology   => wow: 266,  toml: "demonology",    slug: "demonology_warlock",        class: Warlock,     resource: SoulShards,  role: dps,
286    Destruction  => wow: 267,  toml: "destruction",   slug: "destruction_warlock",        class: Warlock,     resource: SoulShards,  role: dps,
287    Brewmaster   => wow: 268,  toml: "brewmaster",    slug: "brewmaster_monk",           class: Monk,        resource: Chi,         role: tank,
288    Mistweaver   => wow: 270,  toml: "mistweaver",    slug: "mistweaver_monk",           class: Monk,        resource: Chi,         role: healer,
289    Windwalker   => wow: 269,  toml: "windwalker",    slug: "windwalker_monk",           class: Monk,        resource: Chi,         role: dps,
290    Balance      => wow: 102,  toml: "balance",       slug: "balance_druid",             class: Druid,       resource: LunarPower,  role: dps,
291    Feral        => wow: 103,  toml: "feral",         slug: "feral_druid",               class: Druid,       resource: Energy,      role: dps,
292    Guardian     => wow: 104,  toml: "guardian",       slug: "guardian_druid",            class: Druid,       resource: Energy,      role: tank,
293    RestoDruid   => wow: 105,  toml: "resto_druid",   slug: "restoration_druid",         class: Druid,       resource: Mana,        role: healer,
294    Havoc        => wow: 577,  toml: "havoc",         slug: "havoc_demon_hunter",        class: DemonHunter, resource: Fury,        role: dps,
295    Vengeance    => wow: 581,  toml: "vengeance",     slug: "vengeance_demon_hunter",    class: DemonHunter, resource: Fury,        role: tank,
296    Devourer     => wow: 1480, toml: "devourer",      slug: "devourer_demon_hunter",     class: DemonHunter, resource: Fury,        role: dps,
297    Devastation  => wow: 1467, toml: "devastation",   slug: "devastation_evoker",        class: Evoker,      resource: Essence,     role: dps,
298    Preservation => wow: 1468, toml: "preservation",  slug: "preservation_evoker",       class: Evoker,      resource: Essence,     role: healer,
299    Augmentation => wow: 1473, toml: "augmentation",  slug: "augmentation_evoker",       class: Evoker,      resource: Essence,     role: dps,
300}
301
302impl ClassId {
303    #[must_use]
304    pub const fn slug(self) -> &'static str {
305        match self {
306            Self::Warrior => "warrior",
307            Self::Paladin => "paladin",
308            Self::Hunter => "hunter",
309            Self::Rogue => "rogue",
310            Self::Priest => "priest",
311            Self::DeathKnight => "death_knight",
312            Self::Shaman => "shaman",
313            Self::Mage => "mage",
314            Self::Warlock => "warlock",
315            Self::Monk => "monk",
316            Self::Druid => "druid",
317            Self::DemonHunter => "demon_hunter",
318            Self::Evoker => "evoker",
319        }
320    }
321}
322
323#[derive(
324    Clone,
325    Copy,
326    Debug,
327    Eq,
328    Hash,
329    PartialEq,
330    num_enum::IntoPrimitive,
331    num_enum::TryFromPrimitive,
332    serde::Deserialize,
333    serde::Serialize,
334    strum::Display,
335    strum::EnumCount,
336    strum::EnumIter,
337    strum::EnumString,
338)]
339#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
340#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
341#[repr(u8)]
342// #t(rust_non_exhaustive_on_public) WoW race IDs are game-defined
343pub enum RaceId {
344    #[strum(serialize = "Human", serialize = "human")]
345    Human = 1,
346    #[strum(serialize = "Orc", serialize = "orc")]
347    Orc = 2,
348    #[strum(serialize = "Dwarf", serialize = "dwarf")]
349    Dwarf = 3,
350    #[strum(
351        serialize = "Night Elf",
352        serialize = "NightElf",
353        serialize = "night_elf"
354    )]
355    NightElf = 4,
356    #[strum(serialize = "Undead", serialize = "undead")]
357    Undead = 5,
358    #[strum(serialize = "Tauren", serialize = "tauren")]
359    Tauren = 6,
360    #[strum(serialize = "Gnome", serialize = "gnome")]
361    Gnome = 7,
362    #[strum(serialize = "Troll", serialize = "troll")]
363    Troll = 8,
364    #[strum(serialize = "Goblin", serialize = "goblin")]
365    Goblin = 9,
366    #[strum(
367        serialize = "Blood Elf",
368        serialize = "BloodElf",
369        serialize = "blood_elf"
370    )]
371    BloodElf = 10,
372    #[strum(serialize = "Draenei", serialize = "draenei")]
373    Draenei = 11,
374    #[strum(serialize = "Worgen", serialize = "worgen")]
375    Worgen = 12,
376    #[strum(
377        serialize = "Pandaren (Alliance)",
378        serialize = "PandarenA",
379        serialize = "pandaren_alliance",
380        serialize = "Pandaren",
381        serialize = "pandaren"
382    )]
383    PandarenA = 13,
384    #[strum(
385        serialize = "Pandaren (Horde)",
386        serialize = "PandarenH",
387        serialize = "pandaren_horde"
388    )]
389    PandarenH = 14,
390    #[strum(serialize = "Nightborne", serialize = "nightborne")]
391    Nightborne = 15,
392    #[strum(
393        serialize = "Highmountain Tauren",
394        serialize = "HighmountainTauren",
395        serialize = "highmountain_tauren"
396    )]
397    HighmountainTauren = 16,
398    #[strum(serialize = "Void Elf", serialize = "VoidElf", serialize = "void_elf")]
399    VoidElf = 17,
400    #[strum(
401        serialize = "Lightforged Draenei",
402        serialize = "LightforgedDraenei",
403        serialize = "lightforged_draenei"
404    )]
405    LightforgedDraenei = 18,
406    #[strum(
407        serialize = "Zandalari Troll",
408        serialize = "ZandalariTroll",
409        serialize = "zandalari_troll"
410    )]
411    ZandalariTroll = 19,
412    #[strum(
413        serialize = "Kul Tiran",
414        serialize = "KulTiran",
415        serialize = "kul_tiran"
416    )]
417    KulTiran = 20,
418    #[strum(
419        serialize = "Dark Iron Dwarf",
420        serialize = "DarkIronDwarf",
421        serialize = "dark_iron_dwarf"
422    )]
423    DarkIronDwarf = 21,
424    #[strum(serialize = "Vulpera", serialize = "vulpera")]
425    Vulpera = 22,
426    #[strum(
427        serialize = "Mag'har Orc",
428        serialize = "MagharOrc",
429        serialize = "mag_har_orc",
430        serialize = "maghar_orc"
431    )]
432    MagharOrc = 23,
433    #[strum(serialize = "Mechagnome", serialize = "mechagnome")]
434    Mechagnome = 24,
435    #[strum(serialize = "Dracthyr", serialize = "dracthyr")]
436    Dracthyr = 25,
437    #[strum(
438        serialize = "Earthen (Alliance)",
439        serialize = "EarthenA",
440        serialize = "earthen_alliance"
441    )]
442    EarthenA = 26,
443    #[strum(
444        serialize = "Earthen (Horde)",
445        serialize = "EarthenH",
446        serialize = "earthen_horde"
447    )]
448    EarthenH = 27,
449}
450
451impl RaceId {
452    /// Returns the matching `ChrRaces.ID`, which is not this enum's own discriminant.
453    #[must_use]
454    // #t(fn: rust_magic_numbers) one-to-one identity map onto the client's ChrRaces row ids
455    // #t(fn: rust_cyclomatic_complexity) one arm per race is the map, not branching logic
456    pub const fn wow_race_id(self) -> i32 {
457        match self {
458            Self::Human => 1,
459            Self::Orc => 2,
460            Self::Dwarf => 3,
461            Self::NightElf => 4,
462            Self::Undead => 5,
463            Self::Tauren => 6,
464            Self::Gnome => 7,
465            Self::Troll => 8,
466            Self::Goblin => 9,
467            Self::BloodElf => 10,
468            Self::Draenei => 11,
469            Self::Worgen => 22,
470            Self::PandarenA => 25,
471            Self::PandarenH => 26,
472            Self::Nightborne => 27,
473            Self::HighmountainTauren => 28,
474            Self::VoidElf => 29,
475            Self::LightforgedDraenei => 30,
476            Self::ZandalariTroll => 31,
477            Self::KulTiran => 32,
478            Self::DarkIronDwarf => 34,
479            Self::Vulpera => 35,
480            Self::MagharOrc => 36,
481            Self::Mechagnome => 37,
482            Self::Dracthyr => 52,
483            Self::EarthenA => 85,
484            Self::EarthenH => 84,
485        }
486    }
487}
488
489#[cfg(test)]
490mod tests {
491    use googletest::prelude::*;
492    use rstest::rstest;
493    use strum::IntoEnumIterator;
494
495    use super::*;
496
497    #[gtest]
498    fn wow_race_id_is_distinct_for_every_race() -> Result<()> {
499        let mut seen = std::collections::BTreeSet::new();
500
501        for race in RaceId::iter() {
502            scoped_trace!("race {race:?} maps to {}", race.wow_race_id());
503            verify_true!(seen.insert(race.wow_race_id()))?;
504        }
505
506        verify_that!(seen.len(), eq(<RaceId as strum::EnumCount>::COUNT))
507    }
508
509    #[gtest]
510    #[rstest]
511    #[case::orc(RaceId::Orc, 2)]
512    #[case::worgen(RaceId::Worgen, 22)]
513    #[case::pandaren_alliance(RaceId::PandarenA, 25)]
514    #[case::dark_iron_dwarf(RaceId::DarkIronDwarf, 34)]
515    #[case::dracthyr(RaceId::Dracthyr, 52)]
516    #[case::earthen_alliance(RaceId::EarthenA, 85)]
517    fn wow_race_id_matches_chr_races(#[case] race: RaceId, #[case] expected: i32) -> Result<()> {
518        verify_that!(race.wow_race_id(), eq(expected))
519    }
520
521    #[gtest]
522    fn from_wow_spec_id_round_trips_for_all_specs() -> Result<()> {
523        for spec in SpecId::iter() {
524            let wow_id = spec.wow_spec_id();
525
526            scoped_trace!("round-trip for {spec:?} (wow_id={wow_id})");
527            verify_that!(SpecId::from_wow_spec_id(wow_id), some(eq(spec)))?;
528        }
529
530        Ok(())
531    }
532
533    #[gtest]
534    fn from_wow_spec_id_returns_none_for_unknown() -> Result<()> {
535        verify_that!(SpecId::from_wow_spec_id(0), none())?;
536
537        verify_that!(SpecId::from_wow_spec_id(99_999), none())
538    }
539
540    #[gtest]
541    #[rstest]
542    #[case::outlaw(SpecId::Outlaw)]
543    #[case::arms(SpecId::Arms)]
544    #[case::resto_druid(SpecId::RestoDruid)]
545    fn spec_toml_key_round_trips(#[case] spec: SpecId) -> Result<()> {
546        verify_that!(SpecId::from_toml_key(spec.toml_key()), some(eq(spec)))
547    }
548
549    #[gtest]
550    fn spec_from_toml_key_unknown_is_none() -> Result<()> {
551        verify_that!(SpecId::from_toml_key("nonsense"), none())
552    }
553
554    #[gtest]
555    fn spec_parse_toml_key_ok() -> Result<()> {
556        verify_that!(
557            SpecId::parse_toml_key("outlaw").ok(),
558            some(eq(SpecId::Outlaw))
559        )
560    }
561
562    #[gtest]
563    fn spec_parse_toml_key_err_message() -> Result<()> {
564        let err = SpecId::parse_toml_key("bogus").unwrap_err();
565
566        verify_that!(err, displays_as(contains_substring("unknown spec key")))
567    }
568
569    #[gtest]
570    fn spec_parse_wow_spec_id_ok() -> Result<()> {
571        verify_that!(
572            SpecId::parse_wow_spec_id(260).ok(),
573            some(eq(SpecId::Outlaw))
574        )
575    }
576
577    #[gtest]
578    fn spec_parse_wow_spec_id_err_message() -> Result<()> {
579        let err = SpecId::parse_wow_spec_id(0).unwrap_err();
580
581        verify_that!(err, displays_as(contains_substring("unknown wow spec id")))
582    }
583
584    #[gtest]
585    #[rstest]
586    #[case::outlaw(SpecId::Outlaw, "outlaw_rogue")]
587    #[case::prot_warrior(SpecId::ProtWarrior, "protection_warrior")]
588    fn spec_manifest_slug_round_trips(#[case] spec: SpecId, #[case] slug: &str) -> Result<()> {
589        verify_that!(spec.slug(), eq(slug))?;
590
591        verify_that!(SpecId::from_manifest_slug(slug), some(eq(spec)))
592    }
593
594    #[gtest]
595    fn spec_from_manifest_slug_unknown_is_none() -> Result<()> {
596        verify_that!(SpecId::from_manifest_slug("x"), none())
597    }
598
599    #[gtest]
600    #[rstest]
601    #[case::outlaw(SpecId::Outlaw, ClassId::Rogue, ResourceType::Energy)]
602    #[case::blood(SpecId::Blood, ClassId::DeathKnight, ResourceType::RunicPower)]
603    #[case::augmentation(SpecId::Augmentation, ClassId::Evoker, ResourceType::Essence)]
604    fn spec_class_and_resource(
605        #[case] spec: SpecId,
606        #[case] class: ClassId,
607        #[case] resource: ResourceType,
608    ) -> Result<()> {
609        verify_that!(spec.class(), eq(class))?;
610
611        verify_that!(spec.primary_resource(), eq(resource))
612    }
613
614    #[gtest]
615    #[rstest]
616    #[case::outlaw_dps(SpecId::Outlaw, true, false, false)]
617    #[case::blood_tank(SpecId::Blood, false, true, false)]
618    #[case::holy_paladin_healer(SpecId::HolyPaladin, false, false, true)]
619    fn spec_role_predicates(
620        #[case] spec: SpecId,
621        #[case] is_dps: bool,
622        #[case] is_tank: bool,
623        #[case] is_healer: bool,
624    ) -> Result<()> {
625        verify_that!(spec.is_dps(), eq(is_dps))?;
626        verify_that!(spec.is_tank(), eq(is_tank))?;
627
628        verify_that!(spec.is_healer(), eq(is_healer))
629    }
630
631    #[gtest]
632    #[rstest]
633    #[case::warrior(ClassId::Warrior, "warrior")]
634    #[case::death_knight(ClassId::DeathKnight, "death_knight")]
635    #[case::demon_hunter(ClassId::DemonHunter, "demon_hunter")]
636    #[case::evoker(ClassId::Evoker, "evoker")]
637    fn class_slug(#[case] class: ClassId, #[case] slug: &str) -> Result<()> {
638        verify_that!(class.slug(), eq(slug))
639    }
640
641    #[gtest]
642    fn class_enum_string_aliases() -> Result<()> {
643        verify_that!(
644            "DeathKnight".parse::<ClassId>().ok(),
645            some(eq(ClassId::DeathKnight))
646        )?;
647
648        verify_that!(
649            "Death Knight".parse::<ClassId>().ok(),
650            some(eq(ClassId::DeathKnight))
651        )
652    }
653
654    #[gtest]
655    fn spec_enum_string_aliases() -> Result<()> {
656        verify_that!("bm".parse::<SpecId>().ok(), some(eq(SpecId::BeastMastery)))?;
657
658        verify_that!("FrostDK".parse::<SpecId>().ok(), some(eq(SpecId::FrostDK)))
659    }
660
661    #[gtest]
662    fn race_enum_string_aliases() -> Result<()> {
663        verify_that!(
664            "MagharOrc".parse::<RaceId>().ok(),
665            some(eq(RaceId::MagharOrc))
666        )?;
667
668        verify_that!(
669            "night_elf".parse::<RaceId>().ok(),
670            some(eq(RaceId::NightElf))
671        )
672    }
673}
674
675/// Hunter pet spec.
676#[derive(
677    Clone,
678    Copy,
679    Debug,
680    Eq,
681    Hash,
682    PartialEq,
683    num_enum::IntoPrimitive,
684    num_enum::TryFromPrimitive,
685    serde::Deserialize,
686    serde::Serialize,
687    strum::Display,
688    strum::EnumCount,
689    strum::EnumIter,
690    strum::EnumString,
691)]
692#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
693#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
694#[repr(u8)]
695// #t(rust_non_exhaustive_on_public) WoW pet types are game-defined
696pub enum PetType {
697    Ferocity = 0,
698    Tenacity = 1,
699    Cunning = 2,
700}
701
702#[derive(
703    Clone,
704    Copy,
705    Debug,
706    Eq,
707    Hash,
708    PartialEq,
709    num_enum::IntoPrimitive,
710    num_enum::TryFromPrimitive,
711    serde::Deserialize,
712    serde::Serialize,
713    strum::Display,
714    strum::EnumCount,
715    strum::EnumIter,
716    strum::EnumString,
717)]
718#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
719#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
720#[repr(u8)]
721// #t(rust_non_exhaustive_on_public) WoW pet kinds are game-defined
722pub enum PetKind {
723    Permanent = 0,
724    Guardian = 1,
725    Summon = 2,
726}