dnd_character/
classes.rs

1use crate::abilities::Abilities;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::hash::Hash;
5use std::sync::{Arc, Mutex};
6
7// Default function for abilities_modifiers during deserialization
8#[cfg(feature = "serde")]
9fn default_abilities_modifiers() -> Arc<Mutex<Abilities>> {
10    Arc::new(Mutex::new(Abilities::default()))
11}
12
13#[derive(Debug, Clone)]
14#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
15#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
16#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
17pub enum ClassSpellCasting {
18    // Wizard
19    // Ask the user to prepare spells at the start of the day
20    //
21    // TODO: Add slots and consume them instead of removing from prepared
22    // TODO: daily chosable spells = inteligence + level
23    KnowledgePrepared {
24        /// Indexes from https://www.dnd5eapi.co/api/spells/
25        spells_index: Vec<Vec<String>>,
26        /// Indexes from https://www.dnd5eapi.co/api/spells/
27        spells_prepared_index: Vec<Vec<String>>,
28        /// If the user has already prepared spells for the day
29        pending_preparation: bool,
30    },
31    // TEMP: Wizard
32    // Cleric, Paladin, Druid
33    // Ask the user to prepare spells at the start of the day
34    //
35    // TODO: Add slots and consume them instead of removing from prepared
36    // TODO: cleric/druid daily chosable spells = WISDOM + (level/2)
37    // TODO: paladin daily chosable spells = CHARISMA + (level/2)
38    AlreadyKnowPrepared {
39        /// Indexes from https://www.dnd5eapi.co/api/spells/
40        spells_prepared_index: Vec<Vec<String>>,
41        /// If the user has already prepared spells for the day
42        pending_preparation: bool,
43    },
44    // Bard, Ranger, Warlock, (Sorcerer?)
45    // No need to ask anything, at the start of the day
46    KnowledgeAlreadyPrepared {
47        /// Indexes from https://www.dnd5eapi.co/api/spells/
48        spells_index: Vec<Vec<String>>,
49        usable_slots: UsableSlots,
50    },
51}
52
53#[derive(Debug, Default, Clone)]
54#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
55#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
56#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
57pub struct UsableSlots {
58    pub cantrip_slots: u8,
59    pub level_1: u8,
60    pub level_2: u8,
61    pub level_3: u8,
62    pub level_4: u8,
63    pub level_5: u8,
64    pub level_6: u8,
65    pub level_7: u8,
66    pub level_8: u8,
67    pub level_9: u8,
68}
69
70#[derive(Debug, Default, Clone)]
71#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
72#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
73#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
74pub struct ClassProperties {
75    /// The level of the class
76    pub level: u8,
77    /// Index from https://www.dnd5eapi.co/api/subclasses/
78    pub subclass: Option<String>,
79    /// Indexes from https://www.dnd5eapi.co/api/spells/
80    pub spell_casting: Option<ClassSpellCasting>,
81    pub fighting_style: Option<String>,
82    pub hunters_prey: Option<String>,
83    pub defensive_tactics: Option<String>,
84    pub additional_fighting_style: Option<String>,
85    pub multiattack: Option<String>,
86    pub superior_hunters_defense: Option<String>,
87    pub natural_explorer_terrain_type: Option<Vec<String>>,
88    pub ranger_favored_enemy_type: Option<Vec<String>>,
89    pub sorcerer_metamagic: Option<Vec<String>>,
90    pub warlock_eldritch_invocation: Option<Vec<String>>,
91    pub sorcerer_dragon_ancestor: Option<String>,
92    #[deprecated]
93    pub abilities_modifiers: Abilities,
94    #[cfg_attr(
95        feature = "serde",
96        serde(
97            skip_serializing,
98            skip_deserializing,
99            default = "default_abilities_modifiers"
100        )
101    )]
102    #[cfg_attr(feature = "utoipa", schema(ignore))]
103    pub abilities: Arc<Mutex<Abilities>>,
104}
105
106/// The key is the index of the class from https://www.dnd5eapi.co/api/classes
107#[derive(Debug)]
108#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
109#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
110#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
111pub struct Class(String, pub ClassProperties);
112
113impl Hash for Class {
114    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
115        self.0.hash(state);
116    }
117}
118
119impl PartialEq for Class {
120    fn eq(&self, other: &Self) -> bool {
121        self.0 == other.0
122    }
123}
124
125impl Eq for Class {}
126
127impl Class {
128    pub fn index(&self) -> &str {
129        &self.0
130    }
131
132    pub fn hit_dice(&self) -> u8 {
133        match self.index() {
134            "barbarian" => 12,
135            "bard" => 8,
136            "cleric" => 8,
137            "druid" => 8,
138            "fighter" => 10,
139            "monk" => 8,
140            "paladin" => 10,
141            "ranger" => 10,
142            "rogue" => 8,
143            "sorcerer" => 6,
144            "warlock" => 8,
145            "wizard" => 6,
146            // For unknown classes we will use the minimum hit dice
147            _ => 6,
148        }
149    }
150}
151
152#[derive(Default, Debug)]
153#[cfg_attr(feature = "serde", derive(Serialize))]
154#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
155#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
156pub struct Classes(pub HashMap<String, Class>);
157
158#[cfg(feature = "serde")]
159impl<'de> Deserialize<'de> for Classes {
160    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
161    where
162        D: serde::Deserializer<'de>,
163    {
164        // This is a placeholder since we'll use the custom deserializer
165        // This won't be used directly, but helps with derive macros
166        let map = HashMap::<String, Class>::deserialize(deserializer)?;
167        Ok(Classes(map))
168    }
169}
170
171impl Classes {
172    #[cfg(feature = "serde")]
173    pub fn deserialize_with_abilities(
174        value: serde_json::Value,
175        shared_abilities: Arc<Mutex<Abilities>>,
176    ) -> Result<Self, serde_json::Error> {
177        let mut result = Classes::default();
178
179        // Parse the JSON map of classes
180        let class_map = value
181            .as_object()
182            .ok_or_else(|| serde::de::Error::custom("Expected object for Classes"))?;
183
184        for (key, value) in class_map {
185            // Deserialize the basic class properties without abilities
186            let mut class_properties: ClassProperties =
187                serde_json::from_value(value.get(1).cloned().unwrap_or(serde_json::Value::Null))?;
188
189            // Set the shared abilities reference
190            class_properties.abilities = shared_abilities.clone();
191
192            // Create the class entry with the class index
193            let index = key.clone();
194            let class = Class(index, class_properties);
195
196            // Add to the map
197            result.0.insert(key.clone(), class);
198        }
199
200        Ok(result)
201    }
202
203    pub fn new(class_index: String) -> Self {
204        let mut classes = Self::default();
205
206        let spell_casting = match class_index.as_str() {
207            "cleric" | "paladin" | "druid" | "wizard" => {
208                Some(ClassSpellCasting::AlreadyKnowPrepared {
209                    spells_prepared_index: Vec::new(),
210                    pending_preparation: true,
211                })
212            }
213            "ranger" | "bard" | "warlock" | "sorcerer" => {
214                Some(ClassSpellCasting::KnowledgeAlreadyPrepared {
215                    spells_index: Vec::new(),
216                    usable_slots: UsableSlots::default(),
217                })
218            }
219            _ => None,
220        };
221
222        let class_properties = ClassProperties {
223            spell_casting,
224            ..ClassProperties::default()
225        };
226
227        // The abilities_modifiers will be set externally when creating from Character
228
229        classes
230            .0
231            .insert(class_index.clone(), Class(class_index, class_properties));
232        classes
233    }
234}