tf2_enum/
spell.rs

1use crate::error::TryFromSpellError;
2use crate::{
3    Attribute,
4    Attributes,
5    AttributeDef,
6    AttributeValue,
7    DescriptionFormat,
8    EffectType,
9    ItemAttribute,
10    TryFromIntAttributeValue,
11};
12use crate::econ_attributes::{
13    HalloweenDeathGhosts,
14    HalloweenGreenFlames,
15    HalloweenPumpkinExplosions,
16    HalloweenVoiceModulation,
17};
18use std::fmt;
19use std::str::FromStr;
20use num_enum::{IntoPrimitive, TryFromPrimitive};
21use serde::de::{self, Deserializer, Visitor};
22use serde::{Deserialize, Serialize, Serializer};
23use serde_repr::{Deserialize_repr, Serialize_repr};
24use strum::{Display, EnumCount, EnumIter, EnumString};
25
26/// Spell.
27/// 
28/// As defined by the schema these wouldn't normally be grouped together as different types of
29/// spells fall under different attributes, but in practice they are often treated as if they are.
30#[derive(
31    Debug,
32    Hash,
33    Eq,
34    PartialEq,
35    Ord,
36    PartialOrd,
37    Display,
38    EnumString,
39    EnumIter,
40    EnumCount,
41    Clone,
42    Copy,
43)]
44#[strum(serialize_all = "title_case")]
45#[allow(missing_docs)]
46pub enum Spell {
47    TeamSpiritFootprints,
48    HeadlessHorseshoes,
49    CorpseGrayFootprints,
50    ViolentVioletFootprints,
51    BruisedPurpleFootprints,
52    GangreenFootprints,
53    RottenOrangeFootprints,
54    DieJob,
55    ChromaticCorruption,
56    PutrescentPigmentation,
57    SpectralSpectrum,
58    SinisterStaining,
59    // Allow conversion from "Voices From Below" but serialize as "Voices from Below".
60    #[strum(serialize = "Voices From Below", serialize = "Voices from Below")]
61    VoicesFromBelow,
62    PumpkinBombs,
63    HalloweenFire,
64    Exorcism,
65}
66
67impl Spell {
68    /// The attribute `defindex` for paint spells.
69    pub const DEFINDEX_PAINT: u32 = 1004;
70    /// The attribute `defindex` for footprints spells.
71    pub const DEFINDEX_FOOTPRINTS: u32 = 1005;
72    /// The attribute `defindex` for voices from below spell.
73    pub const DEFINDEX_VOICES_FROM_BELOW: u32 = 1006;
74    /// The attribute `defindex` for pumpkin bombs spell.
75    pub const DEFINDEX_PUMPKIN_BOMBS: u32 = 1007;
76    /// The attribute `defindex` for halloween fire spell.
77    pub const DEFINDEX_HALLOWEEN_FIRE: u32 = 1008;
78    /// The attribute `defindex` for exorcism spell.
79    pub const DEFINDEX_EXORCISM: u32 = 1009;
80    
81    /// Gets the attribute `defindex` of this spell.
82    pub fn attribute_defindex(&self) -> u32 {
83        match self {
84            Self::DieJob |
85            Self::ChromaticCorruption |
86            Self::PutrescentPigmentation |
87            Self::SpectralSpectrum |
88            Self::SinisterStaining => Self::DEFINDEX_PAINT,
89            Self::TeamSpiritFootprints |
90            Self::GangreenFootprints |
91            Self::CorpseGrayFootprints |
92            Self::ViolentVioletFootprints |
93            Self::RottenOrangeFootprints |
94            Self::BruisedPurpleFootprints |
95            Self::HeadlessHorseshoes => Self::DEFINDEX_FOOTPRINTS,
96            Self::VoicesFromBelow => Self::DEFINDEX_VOICES_FROM_BELOW,
97            Self::PumpkinBombs => Self::DEFINDEX_PUMPKIN_BOMBS,
98            Self::HalloweenFire => Self::DEFINDEX_HALLOWEEN_FIRE,
99            Self::Exorcism => Self::DEFINDEX_EXORCISM,
100        }
101    }
102    
103    /// Gets the attribute ID used to identify this spell. This will only return a value for
104    /// footprints spells and paint spells.
105    pub fn attribute_id(&self) -> Option<u32> {
106        match self {
107            Self::DieJob => Some(0),
108            Self::ChromaticCorruption => Some(1),
109            Self::PutrescentPigmentation => Some(2),
110            Self::SpectralSpectrum => Some(3),
111            Self::SinisterStaining => Some(4),
112            Self::TeamSpiritFootprints => Some(1),
113            Self::HeadlessHorseshoes => Some(2),
114            Self::CorpseGrayFootprints => Some(3100495),
115            Self::ViolentVioletFootprints => Some(5322826),
116            Self::BruisedPurpleFootprints => Some(8208497),
117            Self::GangreenFootprints => Some(8421376),
118            Self::RottenOrangeFootprints => Some(13595446),
119            // bool - "has a spell"
120            _ => None,
121        }
122    }
123    
124    /// Checks if this spell is a paint spell.
125    pub fn is_paint_spell(&self) -> bool {
126        matches!(
127            self,
128            Self::DieJob |
129            Self::ChromaticCorruption |
130            Self::PutrescentPigmentation |
131            Self::SpectralSpectrum |
132            Self::SinisterStaining,
133        )
134    }
135
136    /// Checks if this spell is a footprints spell.
137    pub fn is_footprints_spell(&self) -> bool {
138        matches!(
139            self,
140            Self::TeamSpiritFootprints |
141            Self::HeadlessHorseshoes |
142            Self::CorpseGrayFootprints |
143            Self::ViolentVioletFootprints |
144            Self::BruisedPurpleFootprints |
145            Self::GangreenFootprints |
146            Self::RottenOrangeFootprints,
147        )
148    }
149}
150
151impl Attributes for Spell {
152    const DEFINDEX: &'static [u32] = &[
153        1004,
154        1005,
155        1006,
156        1007,
157        1008,
158        1009,
159    ];
160    const USES_FLOAT_VALUE: bool = true;
161    /// Represents the "set_item_tint_rgb_override", "halloween_footstep_type",
162    /// "halloween_voice_modulation", "halloween_pumpkin_explosions", "halloween_green_flames",
163    /// and "halloween_death_ghosts" attributes.
164    const ATTRIBUTES: &'static [AttributeDef] = &[
165        AttributeDef {
166            defindex: 1004,
167            name: "SPELL: set item tint RGB",
168            attribute_class: Some("set_item_tint_rgb_override"),
169            description_string: Some("%s1"),
170            description_format: Some(DescriptionFormat::ValueIsFromLookupTable),
171            effect_type: EffectType::Positive,
172            hidden: false,
173            stored_as_integer: false,
174        },
175        AttributeDef {
176            defindex: 1005,
177            name: "SPELL: set Halloween footstep type",
178            attribute_class: Some("halloween_footstep_type"),
179            description_string: Some("%s1"),
180            description_format: Some(DescriptionFormat::ValueIsFromLookupTable),
181            effect_type: EffectType::Positive,
182            hidden: false,
183            stored_as_integer: false,
184        },
185        AttributeDef {
186            defindex: 1006,
187            name: "SPELL: Halloween voice modulation",
188            attribute_class: Some("halloween_voice_modulation"),
189            description_string: Some("Voices from Below"),
190            description_format: Some(DescriptionFormat::ValueIsAdditive),
191            effect_type: EffectType::Positive,
192            hidden: false,
193            stored_as_integer: false,
194        },
195        AttributeDef {
196            defindex: 1007,
197            name: "SPELL: Halloween pumpkin explosions",
198            attribute_class: Some("halloween_pumpkin_explosions"),
199            description_string: Some("Pumpkin Bombs"),
200            description_format: Some(DescriptionFormat::ValueIsAdditive),
201            effect_type: EffectType::Positive,
202            hidden: false,
203            stored_as_integer: false,
204        },
205        AttributeDef {
206            defindex: 1008,
207            name: "SPELL: Halloween green flames",
208            attribute_class: Some("halloween_green_flames"),
209            description_string: Some("Halloween Fire"),
210            description_format: Some(DescriptionFormat::ValueIsAdditive),
211            effect_type: EffectType::Positive,
212            hidden: false,
213            stored_as_integer: false,
214        },
215        AttributeDef {
216            defindex: 1009,
217            name: "SPELL: Halloween death ghosts",
218            attribute_class: Some("halloween_death_ghosts"),
219            description_string: Some("Exorcism"),
220            description_format: Some(DescriptionFormat::ValueIsAdditive),
221            effect_type: EffectType::Positive,
222            hidden: false,
223            stored_as_integer: false,
224        },
225    ];
226    
227    /// Gets the value of an attribute belonging to a group of spells.
228    /// 
229    /// Footprints and paint spells share a common attribute but have specific values that
230    /// correspond to which spell is being referenced that can be used to identify the spell.
231    /// 
232    /// # Examples
233    /// ```
234    /// use tf2_enum::{Spell, Attributes};
235    /// 
236    /// assert_eq!(Spell::DieJob.attribute_float_value(), Some(0.));
237    /// assert_eq!(Spell::HeadlessHorseshoes.attribute_float_value(), Some(2.));
238    /// // 1 means true in this context
239    /// assert_eq!(Spell::Exorcism.attribute_float_value(), Some(1.));
240    /// ```
241    fn attribute_float_value(&self) -> Option<f32> {
242        Some(match self {
243            Self::DieJob => 0.0,
244            Self::ChromaticCorruption => 1.0,
245            Self::PutrescentPigmentation => 2.0,
246            Self::SpectralSpectrum => 3.0,
247            Self::SinisterStaining => 4.0,
248            Self::TeamSpiritFootprints => 1.0,
249            Self::HeadlessHorseshoes => 2.0,
250            Self::CorpseGrayFootprints => 3100495.0,
251            Self::ViolentVioletFootprints => 5322826.0,
252            Self::BruisedPurpleFootprints => 8208497.0,
253            Self::GangreenFootprints => 8421376.0,
254            Self::RottenOrangeFootprints => 13595446.0,
255            // bool - "has a spell"
256            _ => 1.0,
257        })
258    }
259}
260
261impl From<PaintSpell> for Spell {
262    fn from(val: PaintSpell) -> Self {
263        match val {
264            PaintSpell::DieJob => Spell::DieJob,
265            PaintSpell::ChromaticCorruption => Spell::ChromaticCorruption,
266            PaintSpell::PutrescentPigmentation => Spell::PutrescentPigmentation,
267            PaintSpell::SpectralSpectrum => Spell::SpectralSpectrum,
268            PaintSpell::SinisterStaining => Spell::SinisterStaining,
269        }
270    }
271}
272
273impl From<&PaintSpell> for Spell {
274    fn from(val: &PaintSpell) -> Self {
275        Self::from(*val)
276    }
277}
278
279impl From<FootprintsSpell> for Spell {
280    fn from(val: FootprintsSpell) -> Self {
281        match val {
282            FootprintsSpell::TeamSpiritFootprints => Self::TeamSpiritFootprints,
283            FootprintsSpell::HeadlessHorseshoes => Self::HeadlessHorseshoes,
284            FootprintsSpell::CorpseGrayFootprints => Self::CorpseGrayFootprints,
285            FootprintsSpell::ViolentVioletFootprints => Self::ViolentVioletFootprints,
286            FootprintsSpell::BruisedPurpleFootprints => Self::BruisedPurpleFootprints,
287            FootprintsSpell::GangreenFootprints => Self::GangreenFootprints,
288            FootprintsSpell::RottenOrangeFootprints => Self::RottenOrangeFootprints,
289        }
290    }
291}
292
293impl From<&FootprintsSpell> for Spell {
294    fn from(val: &FootprintsSpell) -> Self {
295        Self::from(*val)
296    }
297}
298
299impl From<HalloweenVoiceModulation> for Spell {
300    fn from(_: HalloweenVoiceModulation) -> Self {
301        Spell::VoicesFromBelow
302    }
303}
304
305impl From<&HalloweenVoiceModulation> for Spell {
306    fn from(_: &HalloweenVoiceModulation) -> Self {
307        Spell::VoicesFromBelow
308    }
309}
310
311impl From<HalloweenPumpkinExplosions> for Spell {
312    fn from(_: HalloweenPumpkinExplosions) -> Self {
313        Spell::PumpkinBombs
314    }
315}
316
317impl From<&HalloweenPumpkinExplosions> for Spell {
318    fn from(_: &HalloweenPumpkinExplosions) -> Self {
319        Spell::PumpkinBombs
320    }
321}
322
323impl From<HalloweenGreenFlames> for Spell {
324    fn from(_: HalloweenGreenFlames) -> Self {
325        Spell::HalloweenFire
326    }
327}
328
329impl From<&HalloweenGreenFlames> for Spell {
330    fn from(_: &HalloweenGreenFlames) -> Self {
331        Spell::HalloweenFire
332    }
333}
334
335impl From<HalloweenDeathGhosts> for Spell {
336    fn from(_: HalloweenDeathGhosts) -> Self {
337        Spell::Exorcism
338    }
339}
340
341impl From<&HalloweenDeathGhosts> for Spell {
342    fn from(_: &HalloweenDeathGhosts) -> Self {
343        Spell::Exorcism
344    }
345}
346
347impl From<Spell> for ItemAttribute {
348    fn from(val: Spell) -> Self {
349        ItemAttribute {
350            defindex: val.attribute_defindex(),
351            value: val.attribute_value(),
352            float_value: val.attribute_float_value(),
353        }
354    }
355}
356
357struct SpellVisitor;
358
359impl<'de> Visitor<'de> for SpellVisitor {
360    type Value = Spell;
361    
362    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
363        formatter.write_str("a string")
364    }
365    
366    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
367    where
368        E: de::Error,
369    {
370        Spell::from_str(value).map_err(serde::de::Error::custom)
371    }
372}
373
374impl<'de> Deserialize<'de> for Spell {
375    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
376    where
377        D: Deserializer<'de>,
378    {
379        deserializer.deserialize_any(SpellVisitor)
380    }
381}
382
383impl Serialize for Spell {
384    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
385    where
386        S: Serializer,
387    {
388        serializer.serialize_str(&self.to_string())
389    }
390}
391
392/// Paint spell.
393#[derive(
394    Debug,
395    Clone,
396    Copy,
397    Eq,
398    PartialEq,
399    Ord,
400    PartialOrd,
401    Hash,
402    Display,
403    Serialize_repr,
404    Deserialize_repr,
405    EnumString,
406    EnumIter,
407    EnumCount,
408    TryFromPrimitive,
409    IntoPrimitive,
410)]
411#[repr(u32)]
412#[strum(serialize_all = "title_case")]
413#[allow(missing_docs)]
414pub enum PaintSpell {
415    DieJob = 0,
416    ChromaticCorruption = 1,
417    PutrescentPigmentation = 2,
418    SpectralSpectrum = 3,
419    SinisterStaining = 4,
420}
421
422impl Attribute for PaintSpell {
423    const DEFINDEX: u32 = 1004;
424    const USES_FLOAT_VALUE: bool = true;
425    /// Represents the "set_item_tint_rgb_override" attribute.
426    const ATTRIBUTE: AttributeDef = AttributeDef {
427        defindex: 1004,
428        name: "SPELL: set item tint RGB",
429        attribute_class: Some("set_item_tint_rgb_override"),
430        description_string: Some("%s1"),
431        description_format: Some(DescriptionFormat::ValueIsFromLookupTable),
432        effect_type: EffectType::Positive,
433        hidden: false,
434        stored_as_integer: false,
435    };
436    
437    fn attribute_float_value(&self) -> Option<f32> {
438        Some((*self as u32) as f32)
439    }
440}
441
442impl TryFromIntAttributeValue for PaintSpell {
443    fn try_from_attribute_value(_v: AttributeValue) -> Option<Self> {
444        None
445    }
446}
447
448impl TryFrom<Spell> for PaintSpell {
449    type Error = TryFromSpellError;
450    
451    fn try_from(value: Spell) -> Result<Self, Self::Error> {
452        match value {
453            Spell::DieJob => Ok(Self ::DieJob),
454            Spell::ChromaticCorruption => Ok(Self::ChromaticCorruption),
455            Spell::PutrescentPigmentation => Ok(Self::PutrescentPigmentation),
456            Spell::SpectralSpectrum => Ok(Self::SpectralSpectrum),
457            Spell::SinisterStaining => Ok(Self::SinisterStaining),
458            _ => Err(TryFromSpellError {
459                defindex: Self::DEFINDEX,
460                value
461            }),
462        }
463    }
464}
465
466impl TryFrom<&Spell> for PaintSpell {
467    type Error = TryFromSpellError;
468    
469    fn try_from(value: &Spell) -> Result<Self, Self::Error> {
470        Self::try_from(*value)
471    }
472}
473
474impl From<PaintSpell> for ItemAttribute {
475    fn from(val: PaintSpell) -> Self {
476        ItemAttribute {
477            defindex: PaintSpell::DEFINDEX,
478            value: val.attribute_value(),
479            float_value: val.attribute_float_value(),
480        }
481    }
482}
483
484/// Footprints spell.
485#[derive(
486    Debug,
487    Clone,
488    Copy,
489    Eq,
490    PartialEq,
491    Ord,
492    PartialOrd,
493    Hash,
494    Display,
495    Serialize_repr,
496    Deserialize_repr,
497    EnumString,
498    EnumIter,
499    EnumCount,
500    TryFromPrimitive,
501    IntoPrimitive,
502)]
503#[repr(u32)]
504#[strum(serialize_all = "title_case")]
505#[allow(missing_docs)]
506pub enum FootprintsSpell {
507    TeamSpiritFootprints = 1,
508    HeadlessHorseshoes = 2,
509    CorpseGrayFootprints = 3100495,
510    ViolentVioletFootprints = 5322826,
511    BruisedPurpleFootprints = 8208497,
512    GangreenFootprints = 8421376,
513    RottenOrangeFootprints = 13595446,
514}
515
516impl Attribute for FootprintsSpell {
517    const DEFINDEX: u32 = 1005;
518    const USES_FLOAT_VALUE: bool = true;
519    /// Represents the "halloween_footstep_type" attribute.
520    const ATTRIBUTE: AttributeDef = AttributeDef {
521        defindex: 1005,
522        name: "SPELL: set Halloween footstep type",
523        attribute_class: Some("halloween_footstep_type"),
524        description_string: Some("%s1"),
525        description_format: Some(DescriptionFormat::ValueIsFromLookupTable),
526        effect_type: EffectType::Positive,
527        hidden: false,
528        stored_as_integer: false,
529    };
530    
531    fn attribute_float_value(&self) -> Option<f32> {
532        Some((*self as u32) as f32)
533    }
534}
535
536impl TryFromIntAttributeValue for FootprintsSpell {}
537
538impl TryFrom<Spell> for FootprintsSpell {
539    type Error = TryFromSpellError;
540    
541    fn try_from(value: Spell) -> Result<Self, Self::Error> {
542        match value {
543            Spell::TeamSpiritFootprints => Ok(Self::TeamSpiritFootprints),
544            Spell::HeadlessHorseshoes => Ok(Self::HeadlessHorseshoes),
545            Spell::CorpseGrayFootprints => Ok(Self::CorpseGrayFootprints),
546            Spell::ViolentVioletFootprints => Ok(Self::ViolentVioletFootprints),
547            Spell::BruisedPurpleFootprints => Ok(Self::BruisedPurpleFootprints),
548            Spell::GangreenFootprints => Ok(Self::GangreenFootprints),
549            Spell::RottenOrangeFootprints => Ok(Self::RottenOrangeFootprints),
550            _ => Err(TryFromSpellError {
551                defindex: Self::DEFINDEX,
552                value
553            }),
554        }
555    }
556}
557
558impl TryFrom<&Spell> for FootprintsSpell {
559    type Error = TryFromSpellError;
560    
561    fn try_from(value: &Spell) -> Result<Self, Self::Error> {
562        Self::try_from(*value)
563    }
564}
565
566impl From<FootprintsSpell> for ItemAttribute {
567    fn from(val: FootprintsSpell) -> Self {
568        ItemAttribute {
569            defindex: PaintSpell::DEFINDEX,
570            value: val.attribute_value(),
571            float_value: val.attribute_float_value(),
572        }
573    }
574}
575
576#[cfg(test)]
577mod tests {
578    use super::*;
579    use std::str::FromStr;
580    
581    #[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
582    struct SpellAttribute {
583        spell: Spell,
584    }
585    
586    #[test]
587    fn from_str() {
588        assert_eq!(Spell::from_str("Headless Horseshoes").unwrap(), Spell::HeadlessHorseshoes);
589    }
590    
591    #[test]
592    fn serialize_spell() {
593        let attribute = SpellAttribute {
594            spell: Spell::HeadlessHorseshoes,
595        };
596        let json = serde_json::to_string(&attribute).unwrap();
597        
598        assert_eq!(json, "{\"spell\":\"Headless Horseshoes\"}");
599        assert_eq!(serde_json::from_str::<SpellAttribute>(&json).unwrap(), attribute);
600        assert_eq!(serde_json::to_string(&Spell::HeadlessHorseshoes).unwrap(), "\"Headless Horseshoes\"");
601    }
602    
603    #[test]
604    fn deserializes_spell() {
605        let json = "{\"spell\":\"Headless Horseshoes\"}";
606        let attribute: SpellAttribute = serde_json::from_str(json).unwrap();
607        
608        assert_eq!(attribute.spell, Spell::HeadlessHorseshoes);
609        assert_eq!(serde_json::from_str::<Spell>("\"Headless Horseshoes\"").unwrap(), Spell::HeadlessHorseshoes);
610    }
611    
612    #[test]
613    fn to_string() {
614        assert_eq!(Spell::HeadlessHorseshoes.to_string(), "Headless Horseshoes");
615    }
616    
617    #[test]
618    fn from_repr() {
619        assert_eq!(FootprintsSpell::try_from(2).unwrap(), FootprintsSpell::HeadlessHorseshoes);
620    }
621    
622    #[test]
623    fn voices_from_below_from_str() {
624        assert_eq!(Spell::VoicesFromBelow.to_string(), "Voices from Below");
625        assert_eq!(Spell::from_str("Voices from Below").unwrap(), Spell::VoicesFromBelow);
626        assert_eq!(Spell::from_str("Voices From Below").unwrap(), Spell::VoicesFromBelow);
627    }
628    
629    #[test]
630    fn attribute_slices_are_equal_length() {
631        assert_eq!(Spell::DEFINDEX.len(), Spell::ATTRIBUTES.len());
632    }
633}