edda_types/
spellcasting.rs1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::{ValidationError, resources::Pool};
6
7#[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 #[serde(deserialize_with = "level_keys::deserialize")]
15 #[cfg_attr(feature = "openapi", schema(value_type = BTreeMap<String, Pool>))]
17 pub slots: BTreeMap<u8, Pool>,
18 #[serde(skip_serializing_if = "Option::is_none")]
20 pub pact_magic: Option<PactMagic>,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
26pub struct Spell {
27 pub name: String,
28 #[serde(default)]
30 pub level: u8,
31 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub class: Option<String>,
34 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
52#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
53pub struct PactMagic {
54 pub slot_level: u8,
56 #[serde(flatten)]
57 pub slots: Pool,
58}
59
60impl Spellcasting {
61 pub const MAX_SPELL_LEVEL: u8 = 9;
63 pub const MAX_PACT_LEVEL: u8 = 5;
65
66 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 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
109mod 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}