dnd_character/
classes.rs

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