Skip to main content

edda_types/
spellcasting.rs

1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::{ValidationError, resources::Pool};
6
7/// Everything about a character's spellcasting. Empty for non-casters.
8#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
9#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
10#[serde(default)]
11pub struct Spellcasting {
12    pub spells: Vec<Spell>,
13    /// Regular spell slots keyed by slot level (1-9). Recover on a long rest.
14    #[serde(deserialize_with = "level_keys::deserialize")]
15    // JSON keys are strings: "1" to "9".
16    #[cfg_attr(feature = "openapi", schema(value_type = BTreeMap<String, Pool>))]
17    pub slots: BTreeMap<u8, Pool>,
18    /// Warlock Pact Magic slots. Recover on a short or long rest.
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub pact_magic: Option<PactMagic>,
21}
22
23/// A spell the character knows or has prepared.
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
26pub struct Spell {
27    pub name: String,
28    /// 0 for cantrips.
29    #[serde(default)]
30    pub level: u8,
31    /// Which class grants this spell, for multiclass characters.
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub class: Option<String>,
34    /// Whether the spell can currently be cast. Defaults to true so "known"
35    /// casters (Sorcerer, Bard, ...) needn't care; Wizards and Clerics toggle it.
36    #[serde(default = "yes")]
37    pub prepared: bool,
38    #[serde(default)]
39    pub concentration: bool,
40    #[serde(default)]
41    pub ritual: bool,
42    #[serde(default, skip_serializing_if = "String::is_empty")]
43    pub notes: String,
44}
45
46fn yes() -> bool {
47    true
48}
49
50/// Warlock Pact Magic: slots all of one level.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
52#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
53pub struct PactMagic {
54    /// All pact slots are this level (1-5).
55    pub slot_level: u8,
56    #[serde(flatten)]
57    pub slots: Pool,
58}
59
60impl Spellcasting {
61    /// The highest level of a spell slot.
62    pub const MAX_SPELL_LEVEL: u8 = 9;
63    /// The highest level of a pact slot.
64    pub const MAX_PACT_LEVEL: u8 = 5;
65
66    /// The spell called `name` (any case), if the character has it.
67    pub fn find_spell(&self, name: &str) -> Option<&Spell> {
68        self.spells
69            .iter()
70            .find(|s| s.name.eq_ignore_ascii_case(name))
71    }
72
73    /// Checks the spells' names and levels, and the slots' levels and
74    /// counts.
75    pub fn validate(&self) -> Result<(), ValidationError> {
76        for spell in &self.spells {
77            if spell.name.trim().is_empty() {
78                return Err(ValidationError::EmptySpellName);
79            }
80            if spell.level > Self::MAX_SPELL_LEVEL {
81                return Err(ValidationError::InvalidSpellLevel(
82                    spell.name.clone(),
83                    spell.level,
84                ));
85            }
86        }
87        for (&level, pool) in &self.slots {
88            if !(1..=Self::MAX_SPELL_LEVEL).contains(&level) {
89                return Err(ValidationError::InvalidSlotLevel(level));
90            }
91            if !pool.is_valid() {
92                return Err(ValidationError::Overspent(format!(
93                    "level {level} spell slots"
94                )));
95            }
96        }
97        if let Some(pact) = &self.pact_magic {
98            if !(1..=Self::MAX_PACT_LEVEL).contains(&pact.slot_level) {
99                return Err(ValidationError::InvalidSlotLevel(pact.slot_level));
100            }
101            if !pact.slots.is_valid() {
102                return Err(ValidationError::Overspent("pact magic slots".into()));
103            }
104        }
105        Ok(())
106    }
107}
108
109/// JSON map keys are always strings, and inside `#[serde(flatten)]` (as in
110/// `Character`) serde can't convert `"1"` back to an integer key on its own.
111/// Accept slot levels as either integers or numeric strings.
112mod level_keys {
113    use std::{collections::BTreeMap, fmt};
114
115    use serde::{Deserialize, Deserializer, de};
116
117    use crate::resources::Pool;
118
119    #[derive(PartialEq, Eq, PartialOrd, Ord)]
120    struct Level(u8);
121
122    impl<'de> Deserialize<'de> for Level {
123        fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
124            struct Visitor;
125            impl de::Visitor<'_> for Visitor {
126                type Value = Level;
127                fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
128                    f.write_str("a spell slot level")
129                }
130                fn visit_u64<E: de::Error>(self, v: u64) -> Result<Level, E> {
131                    u8::try_from(v).map(Level).map_err(E::custom)
132                }
133                fn visit_str<E: de::Error>(self, v: &str) -> Result<Level, E> {
134                    v.parse().map(Level).map_err(E::custom)
135                }
136            }
137            d.deserialize_any(Visitor)
138        }
139    }
140
141    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<BTreeMap<u8, Pool>, D::Error> {
142        let map = BTreeMap::<Level, Pool>::deserialize(d)?;
143        Ok(map.into_iter().map(|(Level(l), p)| (l, p)).collect())
144    }
145}