dnd_character/
lib.rs

1#[cfg(feature = "api")]
2pub mod api;
3
4pub mod abilities;
5pub mod classes;
6
7use std::cmp::Ordering;
8use std::collections::HashMap;
9use std::fmt;
10use anyhow::{anyhow, bail};
11use lazy_static::lazy_static;
12#[cfg(feature = "serde")]
13use serde::{Deserialize, Serialize};
14
15use crate::abilities::{Abilities};
16use crate::classes::Classes;
17
18lazy_static! {
19    pub static ref GRAPHQL_API_URL: String = std::env::var("DND_GRAPHQL_API_URL").unwrap_or_else(|_| "https://www.dnd5eapi.co/graphql".to_string());
20}
21
22#[derive(Debug)]
23pub struct UnexpectedAbility;
24
25impl fmt::Display for UnexpectedAbility {
26    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
27        write!(f, "The ability isn't present in the character's abilities")
28    }
29}
30
31impl std::error::Error for UnexpectedAbility {}
32
33#[derive(Debug)]
34#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
35#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
36#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
37pub struct Character {
38    /// Indexes from https://www.dnd5eapi.co/api/classes/
39    pub classes: Classes,
40    pub name: String,
41    pub age: u16,
42    /// Index from https://www.dnd5eapi.co/api/races/
43    pub race_index: String,
44    /// Index from https://www.dnd5eapi.co/api/subraces/
45    pub subrace_index: String,
46    /// Index from https://www.dnd5eapi.co/api/alignments/
47    pub alignment_index: String,
48    /// Physical description
49    pub description: String,
50    /// Index from https://www.dnd5eapi.co/api/backgrounds/
51    pub background_index: String,
52    /// Background description
53    pub background_description: String,
54
55    experience_points: u32,
56
57    pub money: u32,
58
59    pub abilities_score: Abilities,
60
61    //Health related stuff
62    pub hp: u16,
63    pub max_hp: u16,
64
65    pub inventory: HashMap<String, u16>,
66
67    pub other: Vec<String>,
68}
69
70#[cfg(feature = "utoipa")]
71pub mod utoipa_addon {
72    use utoipa::{Modify, PartialSchema, ToSchema};
73    use utoipa::openapi::OpenApi;
74
75    pub struct ApiDocDndCharacterAddon;
76
77    impl Modify for ApiDocDndCharacterAddon {
78        fn modify(&self, openapi: &mut OpenApi) {
79            if let Some(components) = openapi.components.as_mut() {
80                components.schemas.insert(super::classes::ClassProperties::name().to_string(), super::classes::ClassProperties::schema());
81                components.schemas.insert(super::classes::ClassSpellCasting::name().to_string(), super::classes::ClassSpellCasting::schema());
82                components.schemas.insert(super::classes::Class::name().to_string(), super::classes::Class::schema());
83                components.schemas.insert(super::Classes::name().to_string(), super::Classes::schema());
84                components.schemas.insert(super::classes::UsableSlots::name().to_string(), super::classes::UsableSlots::schema());
85                components.schemas.insert(super::Abilities::name().to_string(), super::Abilities::schema());
86                components.schemas.insert(super::abilities::AbilityScore::name().to_string(), super::abilities::AbilityScore::schema());
87                components.schemas.insert(super::Character::name().to_string(), super::Character::schema());
88            }
89        }
90    }
91}
92
93const LEVELS: [u32; 19] = [300, 900, 2_700, 6_500, 14_000, 23_000, 34_000, 48_000, 64_000, 85_000, 100_000, 120_000, 140_000, 165_000, 195_000, 225_000, 265_000, 305_000, 355_000];
94
95impl Character {
96    pub fn new(main_class: String, name: String, age: u16, race_index: String, subrace_index: String, alignment_index: String, description: String, background_index: String, background_description: String) -> Self {
97        Self {
98            classes: Classes::new(main_class),
99            name,
100            age,
101            race_index,
102            subrace_index,
103            alignment_index,
104            description,
105            background_index,
106            background_description,
107            experience_points: 0,
108            money: 0,
109            inventory: HashMap::new(),
110
111            abilities_score: Abilities::default(),
112            hp: 0,
113            max_hp: 0,
114            other: vec![],
115        }
116    }
117
118    pub fn class_armor(&self) -> i8 {
119        match self.classes.0.iter().next().unwrap().0.as_str() {
120            "monk" => {
121                10 + self.abilities_score.dexterity.modifier(0) + self.abilities_score.wisdom.modifier(0)
122            }
123            _ => {
124                10 + self.abilities_score.dexterity.modifier(0)
125            }
126        }
127    }
128
129    /// Return current level of the character
130    pub fn level(&self) -> u8 {
131        LEVELS.iter().filter(|&&x| x <= self.experience_points).count() as u8 + 1
132    }
133
134    /// Returns the experience points of the character
135    pub fn experience_points(&self) -> u32 {
136        self.experience_points
137    }
138
139    /// Returns the number of levels the character has earned
140    /// this means that you should add the returned value to a class level (this must be done manually to permit multiclassing)
141    /// # Arguments
142    /// * `experience` - The experience points to add to the character
143    pub fn add_experience(&mut self, experience: u32) -> u8 {
144        //Save the level before adding experience
145        let previous_level = self.level();
146
147        // Limit the experience gotten to the experience needed to reach the next level
148        let experience_to_add = LEVELS.get(self.level() as usize - 1)
149            .map_or(experience, |&next_level_points| {
150                (next_level_points - self.experience_points).min(experience)
151            });
152
153        //Add the experience
154        self.experience_points += experience_to_add;
155
156        //Save the level after adding experience
157        let current_level = self.level();
158
159        //Return the number of levels earned
160        current_level - previous_level
161    }
162
163    pub fn remove_item(&mut self, item: &str, amount: Option<u16>) -> anyhow::Result<(), anyhow::Error> {
164        if let Some(quantity) = self.inventory.get_mut(item) {
165            let quantity_to_remove = amount.unwrap_or(*quantity);
166
167            if *quantity <= quantity_to_remove {
168                self.inventory.remove(item);
169            } else {
170                *quantity -= quantity_to_remove;
171            }
172        } else {
173            bail!("Item not found")
174        }
175
176        Ok(())
177    }
178
179    pub fn add_item(&mut self, item: &str, amount: u16) {
180        if let Some(quantity) = self.inventory.get_mut(item) {
181            *quantity += amount;
182        } else {
183            self.inventory.insert(item.to_string(), amount);
184        }
185    }
186
187    pub fn alter_item_quantity(&mut self, item: &str, amount: i32) -> anyhow::Result<()> {
188        match amount.cmp(&0) {
189            Ordering::Greater => {
190                self.add_item(item, amount as u16);
191                Ok(())
192            }
193            Ordering::Less => {
194                self.remove_item(item, Some(amount.unsigned_abs() as u16))
195            }
196            Ordering::Equal => {
197                bail!("Cannot alter quantity to 0")
198            }
199        }
200    }
201}