tf2_enum/
spell.rs

1use crate::error::TryFromSpellError;
2use crate::{Attribute, Attributes};
3use std::fmt;
4use std::str::FromStr;
5use strum_macros::{Display, EnumString, EnumIter, EnumCount};
6use num_enum::{TryFromPrimitive, IntoPrimitive};
7use serde_repr::{Serialize_repr, Deserialize_repr};
8use serde::{Serialize, Deserialize, Serializer};
9use serde::de::{self, Visitor, Deserializer};
10
11/// Spell. In the schema, spells aren’t grouped together because they have different attributes,
12/// though in practice they’re often treated as if they are.
13#[derive(
14    Debug,
15    Hash,
16    Eq,
17    PartialEq,
18    Ord,
19    PartialOrd,
20    Display,
21    EnumString,
22    EnumIter,
23    EnumCount,
24    Clone,
25    Copy,
26)]
27pub enum Spell {
28    #[strum(serialize = "Team Spirit Footprints")]
29    TeamSpiritFootprints,
30    #[strum(serialize = "Gangreen Footprints")]
31    GangreenFootprints,
32    #[strum(serialize = "Corpse Gray Footprints")]
33    CorpseGrayFootprints,
34    #[strum(serialize = "Violent Violet Footprints")]
35    ViolentVioletFootprints,
36    #[strum(serialize = "Rotten Orange Footprints")]
37    RottenOrangeFootprints,
38    #[strum(serialize = "Bruised Purple Footprints")]
39    BruisedPurpleFootprints,
40    #[strum(serialize = "Headless Horseshoes")]
41    HeadlessHorseshoes,
42    #[strum(serialize = "Die Job")]
43    DieJob,
44    #[strum(serialize = "Chromatic Corruption")]
45    ChromaticCorruption,
46    #[strum(serialize = "Putrescent Pigmentation")]
47    PutrescentPigmentation,
48    #[strum(serialize = "Spectral Spectrum")]
49    SpectralSpectrum,
50    #[strum(serialize = "Sinister Staining")]
51    SinisterStaining,
52    // Allow conversion from "Voices From Below" but serialize as "Voices from Below".
53    #[strum(serialize = "Voices From Below", serialize = "Voices from Below")]
54    VoicesFromBelow,
55    #[strum(serialize = "Pumpkin Bombs")]
56    PumpkinBombs,
57    #[strum(serialize = "Halloween Fire")]
58    HalloweenFire,
59    #[strum(serialize = "Exorcism")]
60    Exorcism,
61}
62
63impl Spell {
64    /// The attribute `defindex` for paint spells.
65    pub const DEFINDEX_PAINT: u32 = 1004;
66    /// The attribute `defindex` for footprints spells.
67    pub const DEFINDEX_FOOTPRINTS: u32 = 1005;
68    /// The attribute `defindex` for voices from below spell.
69    pub const DEFINDEX_VOICES_FROM_BELOW: u32 = 1006;
70    /// The attribute `defindex` for pumpkin bombs spell.
71    pub const DEFINDEX_PUMPKIN_BOMBS: u32 = 1007;
72    /// The attribute `defindex` for halloween fire spell.
73    pub const DEFINDEX_HALLOWEEN_FIRE: u32 = 1008;
74    /// The attribute `defindex` for exorcism spell.
75    pub const DEFINDEX_EXORCISM: u32 = 1009;
76    
77    /// Gets the attribute `defindex` of this spell.
78    pub fn attribute_defindex(&self) -> u32 {
79        match self {
80            Self::DieJob |
81            Self::ChromaticCorruption |
82            Self::PutrescentPigmentation |
83            Self::SpectralSpectrum |
84            Self::SinisterStaining => Self::DEFINDEX_PAINT,
85            Self::TeamSpiritFootprints |
86            Self::GangreenFootprints |
87            Self::CorpseGrayFootprints |
88            Self::ViolentVioletFootprints |
89            Self::RottenOrangeFootprints |
90            Self::BruisedPurpleFootprints |
91            Self::HeadlessHorseshoes => Self::DEFINDEX_FOOTPRINTS,
92            Self::VoicesFromBelow => Self::DEFINDEX_VOICES_FROM_BELOW,
93            Self::PumpkinBombs => Self::DEFINDEX_PUMPKIN_BOMBS,
94            Self::HalloweenFire => Self::DEFINDEX_HALLOWEEN_FIRE,
95            Self::Exorcism => Self::DEFINDEX_EXORCISM,
96        }
97    }
98    
99    /// Gets the value of an attribute belonging to a group of spells.
100    /// 
101    /// Footprints and paint spells share a common attribute but have specific values that
102    /// correspond to which spell is being referenced that can be used to identify the spell.
103    /// 
104    /// # Examples
105    /// ```
106    /// use tf2_enum::Spell;
107    /// 
108    /// assert_eq!(Spell::DieJob.attribute_value(), Some(0));
109    /// assert_eq!(Spell::HeadlessHorseshoes.attribute_value(), Some(2));
110    /// assert_eq!(Spell::Exorcism.attribute_value(), None);
111    /// ```
112    pub fn attribute_value(&self) -> Option<u32> {
113        match self {
114            Self::DieJob => Some(0),
115            Self::ChromaticCorruption => Some(1),
116            Self::PutrescentPigmentation => Some(2),
117            Self::SpectralSpectrum => Some(3),
118            Self::SinisterStaining => Some(4),
119            Self::TeamSpiritFootprints => Some(1),
120            Self::GangreenFootprints => Some(8421376),
121            Self::CorpseGrayFootprints => Some(3100495),
122            Self::ViolentVioletFootprints => Some(5322826),
123            Self::RottenOrangeFootprints => Some(13595446),
124            Self::BruisedPurpleFootprints => Some(8208497),
125            Self::HeadlessHorseshoes => Some(2),
126            _ => None,
127        }
128    }
129}
130
131/// set_item_tint_rgb_override
132/// halloween_footstep_type
133/// halloween_voice_modulation
134/// halloween_pumpkin_explosions
135/// halloween_green_flames
136/// halloween_death_ghosts
137impl Attributes for Spell {
138    const DEFINDEX: &[u32] = &[
139        1004,
140        1005,
141        1006,
142        1007,
143        1008,
144        1009,
145    ];
146    const NAME: &[&str] = &[
147        "SPELL: set item tint RGB",
148        "SPELL: set Halloween footstep type",
149        "SPELL: Halloween voice modulation",
150        "SPELL: Halloween pumpkin explosions",
151        "SPELL: Halloween green flames",
152        "SPELL: Halloween death ghosts",
153    ];
154    const ATTRIBUTE_CLASS: &[&str] = &[
155        "set_item_tint_rgb_override",
156        "halloween_footstep_type",
157        "halloween_voice_modulation",
158        "halloween_pumpkin_explosions",
159        "halloween_green_flames",
160        "halloween_death_ghosts",
161    ];
162    const DESCRIPTION_STRING: &[Option<&str>] = &[
163        Some("%s1"),
164        Some("%s1"),
165        Some("Voices from Below"),
166        Some("Pumpkin Bombs"),
167        Some("Halloween Fire"),
168        Some("Exorcism"),
169    ];
170    const DESCRIPTION_FORMAT: &[Option<&str>] = &[
171        Some("value_is_from_lookup_table"),
172        Some("value_is_from_lookup_table"),
173        Some("value_is_additive"),
174        Some("value_is_additive"),
175        Some("value_is_additive"),
176        Some("value_is_additive"),
177    ];
178    const EFFECT_TYPE: &[&str] = &[
179        "positive",
180        "positive",
181        "positive",
182        "positive",
183        "positive",
184        "positive",
185    ];
186    const HIDDEN: &[bool] = &[
187        false,
188        false,
189        false,
190        false,
191        false,
192        false,
193    ];
194    const STORED_AS_INTEGER: &[bool] = &[
195        false,
196        false,
197        false,
198        false,
199        false,
200        false,
201    ];
202}
203
204struct SpellVisitor;
205
206impl<'de> Visitor<'de> for SpellVisitor {
207    type Value = Spell;
208    
209    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
210        formatter.write_str("a string")
211    }
212    
213    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
214    where
215        E: de::Error,
216    {
217        Spell::from_str(value).map_err(serde::de::Error::custom)
218    }
219}
220
221impl<'de> Deserialize<'de> for Spell {
222    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
223    where
224        D: Deserializer<'de>,
225    {
226        deserializer.deserialize_any(SpellVisitor)
227    }
228}
229
230impl Serialize for Spell {
231    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
232    where
233        S: Serializer,
234    {
235        serializer.serialize_str(&self.to_string())
236    }
237}
238
239impl TryInto<u32> for Spell {
240    type Error = ();
241    
242    fn try_into(self) -> Result<u32, Self::Error> {
243        self.attribute_value().ok_or(())
244    }
245}
246
247/// Paint spell.
248#[derive(
249    Serialize_repr,
250    Deserialize_repr,
251    Debug,
252    Hash,
253    Eq,
254    PartialEq,
255    Ord,
256    PartialOrd,
257    Display,
258    EnumString,
259    EnumIter,
260    EnumCount,
261    TryFromPrimitive,
262    IntoPrimitive,
263    Clone,
264    Copy,
265)]
266#[repr(u32)]
267pub enum PaintSpell {
268    #[strum(serialize = "Die Job")]
269    DieJob = 0,
270    #[strum(serialize = "Chromatic Corruption")]
271    ChromaticCorruption = 1,
272    #[strum(serialize = "Putrescent Pigmentation")]
273    PutrescentPigmentation = 2,
274    #[strum(serialize = "Spectral Spectrum")]
275    SpectralSpectrum = 3,
276    #[strum(serialize = "Sinister Staining")]
277    SinisterStaining = 4,
278}
279
280/// set_item_tint_rgb_override
281impl Attribute for PaintSpell {
282    const DEFINDEX: u32 = 1004;
283    const NAME: &str = "SPELL: set item tint RGB";
284    const ATTRIBUTE_CLASS: &str = "set_item_tint_rgb_override";
285    const DESCRIPTION_STRING: Option<&str> = Some("%s1");
286    const DESCRIPTION_FORMAT: Option<&str> = Some("value_is_from_lookup_table");
287    const EFFECT_TYPE: &str = "positive";
288    const HIDDEN: bool = false;
289    const STORED_AS_INTEGER: bool = false;
290}
291
292impl From<PaintSpell> for Spell {
293    fn from(val: PaintSpell) -> Self {
294        match val {
295            PaintSpell::DieJob => Spell::DieJob,
296            PaintSpell::ChromaticCorruption => Spell::ChromaticCorruption,
297            PaintSpell::PutrescentPigmentation => Spell::PutrescentPigmentation,
298            PaintSpell::SpectralSpectrum => Spell::SpectralSpectrum,
299            PaintSpell::SinisterStaining => Spell::SinisterStaining,
300        }
301    }
302}
303
304impl TryFrom<Spell> for PaintSpell {
305    type Error = TryFromSpellError;
306    
307    fn try_from(value: Spell) -> Result<Self, Self::Error> {
308        match value {
309            Spell::DieJob => Ok(PaintSpell::DieJob),
310            Spell::ChromaticCorruption => Ok(PaintSpell::ChromaticCorruption),
311            Spell::PutrescentPigmentation => Ok(PaintSpell::PutrescentPigmentation),
312            Spell::SpectralSpectrum => Ok(PaintSpell::SpectralSpectrum),
313            Spell::SinisterStaining => Ok(PaintSpell::SinisterStaining),
314            _ => Err(TryFromSpellError {
315                defindex: Self::DEFINDEX,
316                value
317            }),
318        }
319    }
320}
321
322/// Footprints spell.
323#[derive(
324    Serialize_repr,
325    Deserialize_repr,
326    Debug,
327    Hash,
328    Eq,
329    PartialEq,
330    Ord,
331    PartialOrd,
332    Display,
333    EnumString,
334    EnumIter,
335    EnumCount,
336    TryFromPrimitive,
337    IntoPrimitive,
338    Clone,
339    Copy,
340)]
341#[repr(u32)]
342pub enum FootprintsSpell {
343    #[strum(serialize = "Team Spirit Footprints")]
344    TeamSpiritFootprints = 1,
345    #[strum(serialize = "Headless Horseshoes")]
346    HeadlessHorseshoes = 2,
347    #[strum(serialize = "Rotten Orange Footprints")]
348    RottenOrangeFootprints = 13595446,
349    #[strum(serialize = "Corpse Gray Footprints")]
350    CorpseGrayFootprints = 3100495,
351    #[strum(serialize = "Violent Violet Footprints")]
352    ViolentVioletFootprints = 5322826,
353    #[strum(serialize = "Bruised Purple Footprints")]
354    BruisedPurpleFootprints = 8208497,
355    #[strum(serialize = "Gangreen Footprints")]
356    GangreenFootprints = 8421376,
357}
358
359impl Attribute for FootprintsSpell {
360    const DEFINDEX: u32 = 1005;
361    const NAME: &str = "SPELL: set Halloween footstep type";
362    const ATTRIBUTE_CLASS: &str = "halloween_footstep_type";
363    const DESCRIPTION_STRING: Option<&str> = Some("%s1");
364    const DESCRIPTION_FORMAT: Option<&str> = Some("value_is_from_lookup_table");
365    const EFFECT_TYPE: &str = "positive";
366    const HIDDEN: bool = false;
367    const STORED_AS_INTEGER: bool = false;
368}
369
370impl From<FootprintsSpell> for Spell {
371    fn from(val: FootprintsSpell) -> Self {
372        match val {
373            FootprintsSpell::TeamSpiritFootprints => Spell::TeamSpiritFootprints,
374            FootprintsSpell::GangreenFootprints => Spell::GangreenFootprints,
375            FootprintsSpell::CorpseGrayFootprints => Spell::CorpseGrayFootprints,
376            FootprintsSpell::ViolentVioletFootprints => Spell::ViolentVioletFootprints,
377            FootprintsSpell::RottenOrangeFootprints => Spell::RottenOrangeFootprints,
378            FootprintsSpell::BruisedPurpleFootprints => Spell::BruisedPurpleFootprints,
379            FootprintsSpell::HeadlessHorseshoes => Spell::HeadlessHorseshoes,
380        }
381    }
382}
383
384#[cfg(test)]
385mod tests {
386    use super::*;
387    use std::str::FromStr;
388    
389    #[test]
390    fn from_str() {
391        assert_eq!(Spell::from_str("Headless Horseshoes").unwrap(), Spell::HeadlessHorseshoes);
392    }
393    
394    #[test]
395    fn serialize_spell() {
396        #[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
397        struct SpellAttribute {
398            spell: Spell,
399        }
400        
401        let attribute = SpellAttribute {
402            spell: Spell::HeadlessHorseshoes,
403        };
404        let json = serde_json::to_string(&attribute).unwrap();
405        
406        assert_eq!(json, "{\"spell\":\"Headless Horseshoes\"}");
407        assert_eq!(serde_json::from_str::<SpellAttribute>(&json).unwrap(), attribute);
408    }
409    
410    #[test]
411    fn to_string() {
412        assert_eq!(Spell::HeadlessHorseshoes.to_string(), "Headless Horseshoes");
413    }
414    
415    #[test]
416    fn from_repr() {
417        assert_eq!(FootprintsSpell::try_from(2).unwrap(), FootprintsSpell::HeadlessHorseshoes);
418    }
419    
420    #[test]
421    fn voices_from_below_from_str() {
422        assert_eq!(Spell::VoicesFromBelow.to_string(), "Voices from Below");
423        assert_eq!(Spell::from_str("Voices from Below").unwrap(), Spell::VoicesFromBelow);
424        assert_eq!(Spell::from_str("Voices From Below").unwrap(), Spell::VoicesFromBelow);
425    }
426}