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 pub classes: Classes,
40 pub name: String,
41 pub age: u16,
42 pub race_index: String,
44 pub subrace_index: String,
46 pub alignment_index: String,
48 pub description: String,
50 pub background_index: String,
52 pub background_description: String,
54
55 experience_points: u32,
56
57 pub money: u32,
58
59 pub abilities_score: Abilities,
60
61 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 pub fn level(&self) -> u8 {
131 LEVELS.iter().filter(|&&x| x <= self.experience_points).count() as u8 + 1
132 }
133
134 pub fn experience_points(&self) -> u32 {
136 self.experience_points
137 }
138
139 pub fn add_experience(&mut self, experience: u32) -> u8 {
144 let previous_level = self.level();
146
147 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 self.experience_points += experience_to_add;
155
156 let current_level = self.level();
158
159 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}