1#[cfg(feature = "api")]
2pub mod api;
3
4pub mod abilities;
5pub mod classes;
6
7use anyhow::{anyhow, bail};
8use api::classes::ChoosableCustomLevelFeatureOption;
9use lazy_static::lazy_static;
10#[cfg(feature = "serde")]
11use serde::{Deserialize, Serialize};
12use std::cmp::Ordering;
13use std::collections::HashMap;
14use std::fmt;
15use std::ops::Deref;
16
17use crate::abilities::Abilities;
18use crate::classes::Classes;
19
20lazy_static! {
21 pub static ref GRAPHQL_API_URL: String = std::env::var("DND_GRAPHQL_API_URL")
22 .unwrap_or_else(|_| "https://www.dnd5eapi.co/graphql/2014".to_string());
23}
24
25#[derive(Debug)]
26pub struct UnexpectedAbility;
27
28impl fmt::Display for UnexpectedAbility {
29 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
30 write!(f, "The ability isn't present in the character's abilities")
31 }
32}
33
34impl std::error::Error for UnexpectedAbility {}
35
36#[derive(Debug)]
37#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
38#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
39#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
40pub struct Character {
41 pub classes: Classes,
43 pub name: String,
44 pub age: u16,
45 pub race_index: String,
47 pub subrace_index: String,
49 pub alignment_index: String,
51 pub description: String,
53 pub background_index: String,
55 pub background_description: String,
57
58 experience_points: u32,
59
60 pub money: u32,
61
62 pub abilities_score: Abilities,
63
64 pub hp: u16,
66 pub max_hp: u16,
67
68 pub inventory: HashMap<String, u16>,
69
70 pub other: Vec<String>,
71}
72
73#[cfg(feature = "utoipa")]
74pub mod utoipa_addon {
75 use utoipa::openapi::OpenApi;
76 use utoipa::{Modify, PartialSchema, ToSchema};
77
78 pub struct ApiDocDndCharacterAddon;
79
80 impl Modify for ApiDocDndCharacterAddon {
81 fn modify(&self, openapi: &mut OpenApi) {
82 if let Some(components) = openapi.components.as_mut() {
83 components.schemas.insert(
84 super::classes::ClassProperties::name().to_string(),
85 super::classes::ClassProperties::schema(),
86 );
87 components.schemas.insert(
88 super::classes::ClassSpellCasting::name().to_string(),
89 super::classes::ClassSpellCasting::schema(),
90 );
91 components.schemas.insert(
92 super::classes::Class::name().to_string(),
93 super::classes::Class::schema(),
94 );
95 components
96 .schemas
97 .insert(super::Classes::name().to_string(), super::Classes::schema());
98 components.schemas.insert(
99 super::classes::UsableSlots::name().to_string(),
100 super::classes::UsableSlots::schema(),
101 );
102 components.schemas.insert(
103 super::Abilities::name().to_string(),
104 super::Abilities::schema(),
105 );
106 components.schemas.insert(
107 super::abilities::AbilityScore::name().to_string(),
108 super::abilities::AbilityScore::schema(),
109 );
110 components.schemas.insert(
111 super::Character::name().to_string(),
112 super::Character::schema(),
113 );
114 }
115 }
116 }
117}
118
119const LEVELS: [u32; 19] = [
120 300, 900, 2_700, 6_500, 14_000, 23_000, 34_000, 48_000, 64_000, 85_000, 100_000, 120_000,
121 140_000, 165_000, 195_000, 225_000, 265_000, 305_000, 355_000,
122];
123
124impl Character {
125 pub fn new(
126 main_class: String,
127 name: String,
128 age: u16,
129 race_index: String,
130 subrace_index: String,
131 alignment_index: String,
132 description: String,
133 background_index: String,
134 background_description: String,
135 ) -> Self {
136 Self {
137 classes: Classes::new(main_class),
138 name,
139 age,
140 race_index,
141 subrace_index,
142 alignment_index,
143 description,
144 background_index,
145 background_description,
146 experience_points: 0,
147 money: 0,
148 inventory: HashMap::new(),
149
150 abilities_score: Abilities::default(),
151 hp: 0,
152 max_hp: 0,
153 other: vec![],
154 }
155 }
156
157 pub fn class_armor(&self) -> i8 {
158 let first_class = self.classes.0.iter().next().unwrap();
160 let class_name = first_class.0.as_str();
161
162 let mut base = match class_name {
164 "monk" => {
165 10 + self.abilities_score.dexterity.modifier(0)
166 + self.abilities_score.wisdom.modifier(0)
167 }
168 _ => 10 + self.abilities_score.dexterity.modifier(0),
169 };
170
171 let has_defense_style = first_class.1 .1.fighting_style
173 == Some(
174 ChoosableCustomLevelFeatureOption::FightingStyleDefense
175 .as_index_str()
176 .to_string(),
177 );
178
179 if has_defense_style {
181 base += 1;
182 }
183
184 base
185 }
186
187 pub fn level(&self) -> u8 {
189 LEVELS
190 .iter()
191 .filter(|&&x| x <= self.experience_points)
192 .count() as u8
193 + 1
194 }
195
196 pub fn experience_points(&self) -> u32 {
198 self.experience_points
199 }
200
201 pub fn add_experience(&mut self, experience: u32) -> u8 {
206 let previous_level = self.level();
208
209 let experience_to_add = LEVELS
211 .get(self.level() as usize - 1)
212 .map_or(experience, |&next_level_points| {
213 (next_level_points - self.experience_points).min(experience)
214 });
215
216 self.experience_points += experience_to_add;
218
219 let current_level = self.level();
221
222 current_level - previous_level
224 }
225
226 pub fn remove_item(
227 &mut self,
228 item: &str,
229 amount: Option<u16>,
230 ) -> anyhow::Result<(), anyhow::Error> {
231 if let Some(quantity) = self.inventory.get_mut(item) {
232 let quantity_to_remove = amount.unwrap_or(*quantity);
233
234 if *quantity <= quantity_to_remove {
235 self.inventory.remove(item);
236 } else {
237 *quantity -= quantity_to_remove;
238 }
239 } else {
240 bail!("Item not found")
241 }
242
243 Ok(())
244 }
245
246 pub fn add_item(&mut self, item: &str, amount: u16) {
247 if let Some(quantity) = self.inventory.get_mut(item) {
248 *quantity += amount;
249 } else {
250 self.inventory.insert(item.to_string(), amount);
251 }
252 }
253
254 pub fn alter_item_quantity(&mut self, item: &str, amount: i32) -> anyhow::Result<()> {
255 match amount.cmp(&0) {
256 Ordering::Greater => {
257 self.add_item(item, amount as u16);
258 Ok(())
259 }
260 Ordering::Less => self.remove_item(item, Some(amount.unsigned_abs() as u16)),
261 Ordering::Equal => {
262 bail!("Cannot alter quantity to 0")
263 }
264 }
265 }
266}