dnd_character/
classes.rs

1use crate::abilities::Abilities;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::hash::Hash;
5
6#[derive(Debug, Clone)]
7#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
8#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
9#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
10pub enum ClassSpellCasting {
11    // Wizard
12    // Ask the user to prepare spells at the start of the day
13    //
14    // TODO: Add slots and consume them instead of removing from prepared
15    // TODO: daily chosable spells = inteligence + level
16    KnowledgePrepared {
17        /// Indexes from https://www.dnd5eapi.co/api/spells/
18        spells_index: Vec<Vec<String>>,
19        /// Indexes from https://www.dnd5eapi.co/api/spells/
20        spells_prepared_index: Vec<Vec<String>>,
21        /// If the user has already prepared spells for the day
22        pending_preparation: bool,
23    },
24    // TEMP: Wizard
25    // Cleric, Paladin, Druid
26    // Ask the user to prepare spells at the start of the day
27    //
28    // TODO: Add slots and consume them instead of removing from prepared
29    // TODO: cleric/druid daily chosable spells = WISDOM + (level/2)
30    // TODO: paladin daily chosable spells = CHARISMA + (level/2)
31    AlreadyKnowPrepared {
32        /// Indexes from https://www.dnd5eapi.co/api/spells/
33        spells_prepared_index: Vec<Vec<String>>,
34        /// If the user has already prepared spells for the day
35        pending_preparation: bool,
36    },
37    // Bard, Ranger, Warlock, (Sorcerer?)
38    // No need to ask anything, at the start of the day
39    KnowledgeAlreadyPrepared {
40        /// Indexes from https://www.dnd5eapi.co/api/spells/
41        spells_index: Vec<Vec<String>>,
42        usable_slots: UsableSlots,
43    },
44}
45
46#[derive(Debug, Default, Clone)]
47#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
48#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
49#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
50pub struct UsableSlots {
51    pub cantrip_slots: u8,
52    pub level_1: u8,
53    pub level_2: u8,
54    pub level_3: u8,
55    pub level_4: u8,
56    pub level_5: u8,
57    pub level_6: u8,
58    pub level_7: u8,
59    pub level_8: u8,
60    pub level_9: u8,
61}
62
63#[derive(Debug, Default, Clone)]
64#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
65#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
66#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
67pub struct ClassProperties {
68    /// The level of the class
69    pub level: u8,
70    /// Index from https://www.dnd5eapi.co/api/subclasses/
71    pub subclass: Option<String>,
72    /// Indexes from https://www.dnd5eapi.co/api/spells/
73    pub spell_casting: Option<ClassSpellCasting>,
74    pub fighting_style: Option<String>,
75    pub hunters_prey: Option<String>,
76    pub defensive_tactics: Option<String>,
77    pub additional_fighting_style: Option<String>,
78    pub multiattack: Option<String>,
79    pub superior_hunters_defense: Option<String>,
80    pub natural_explorer_terrain_type: Option<Vec<String>>,
81    pub ranger_favored_enemy_type: Option<Vec<String>>,
82    pub sorcerer_metamagic: Option<Vec<String>>,
83    pub sorcerer_dragon_ancestor: Option<String>,
84    pub abilities_modifiers: Abilities,
85}
86
87/// The key is the index of the class from https://www.dnd5eapi.co/api/classes
88#[derive(Debug)]
89#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
90#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
91#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
92pub struct Class(String, pub ClassProperties);
93
94impl Hash for Class {
95    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
96        self.0.hash(state);
97    }
98}
99
100impl PartialEq for Class {
101    fn eq(&self, other: &Self) -> bool {
102        self.0 == other.0
103    }
104}
105
106impl Eq for Class {}
107
108impl Class {
109    pub fn index(&self) -> &str {
110        &self.0
111    }
112
113    pub fn hit_dice(&self) -> u8 {
114        match self.index() {
115            "barbarian" => 12,
116            "bard" => 8,
117            "cleric" => 8,
118            "druid" => 8,
119            "fighter" => 10,
120            "monk" => 8,
121            "paladin" => 10,
122            "ranger" => 10,
123            "rogue" => 8,
124            "sorcerer" => 6,
125            "warlock" => 8,
126            "wizard" => 6,
127            // For unknown classes we will use the minimum hit dice
128            _ => 6,
129        }
130    }
131}
132
133#[derive(Default, Debug)]
134#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
135#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
136#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
137pub struct Classes(pub HashMap<String, Class>);
138
139impl Classes {
140    pub fn new(class_index: String) -> Self {
141        let mut classes = Self::default();
142
143        let spell_casting = match class_index.as_str() {
144            "cleric" | "paladin" | "druid" | "wizard" => {
145                Some(ClassSpellCasting::AlreadyKnowPrepared {
146                    spells_prepared_index: Vec::new(),
147                    pending_preparation: true,
148                })
149            }
150            "ranger" | "bard" | "warlock" | "sorcerer" => {
151                Some(ClassSpellCasting::KnowledgeAlreadyPrepared {
152                    spells_index: Vec::new(),
153                    usable_slots: UsableSlots::default(),
154                })
155            }
156            _ => None,
157        };
158
159        let class_properties = ClassProperties {
160            spell_casting,
161            ..ClassProperties::default()
162        };
163
164        classes
165            .0
166            .insert(class_index.clone(), Class(class_index, class_properties));
167        classes
168    }
169}