1#[cfg(feature = "api")]
2pub mod api;
3
4pub mod abilities;
5pub mod classes;
6
7use abilities::AbilityScore;
8use anyhow::bail;
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::sync::{Arc, Mutex};
16
17use crate::abilities::Abilities;
18use crate::classes::Classes;
19
20#[cfg(feature = "serde")]
21mod abilities_score_serde {
22 use crate::abilities::Abilities;
23 use serde::{Deserialize, Deserializer, Serialize, Serializer};
24 use std::sync::{Arc, Mutex};
25
26 pub fn serialize<S>(
27 abilities: &Arc<Mutex<Abilities>>,
28 serializer: S,
29 ) -> Result<S::Ok, S::Error>
30 where
31 S: Serializer,
32 {
33 abilities.lock().unwrap().serialize(serializer)
34 }
35
36 pub fn deserialize<'de, D>(deserializer: D) -> Result<Arc<Mutex<Abilities>>, D::Error>
37 where
38 D: Deserializer<'de>,
39 {
40 let abilities = Abilities::deserialize(deserializer)?;
41 Ok(Arc::new(Mutex::new(abilities)))
42 }
43}
44
45#[cfg(feature = "serde")]
46mod classes_serde {
47 use crate::abilities::Abilities;
48 use crate::classes::Classes;
49 use serde::de::Error;
50 use serde::{Deserialize, Deserializer, Serialize, Serializer};
51 use std::sync::{Arc, Mutex};
52
53 pub fn serialize<S>(classes: &Classes, serializer: S) -> Result<S::Ok, S::Error>
54 where
55 S: Serializer,
56 {
57 classes.serialize(serializer)
58 }
59
60 pub fn deserialize<'de, D>(
61 deserializer: D,
62 abilities_ref: &Arc<Mutex<Abilities>>,
63 ) -> Result<Classes, D::Error>
64 where
65 D: Deserializer<'de>,
66 {
67 let value = serde_json::Value::deserialize(deserializer)
69 .map_err(|e| D::Error::custom(format!("Failed to deserialize classes: {}", e)))?;
70
71 Classes::deserialize_with_abilities(value, abilities_ref.clone())
73 .map_err(|e| D::Error::custom(format!("Failed to deserialize with abilities: {}", e)))
74 }
75}
76
77lazy_static! {
78 pub static ref GRAPHQL_API_URL: String = std::env::var("DND_GRAPHQL_API_URL")
79 .unwrap_or_else(|_| "https://www.dnd5eapi.co/graphql/2014".to_string());
80}
81
82#[derive(Debug)]
83pub struct UnexpectedAbility;
84
85impl fmt::Display for UnexpectedAbility {
86 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
87 write!(f, "The ability isn't present in the character's abilities")
88 }
89}
90
91impl std::error::Error for UnexpectedAbility {}
92
93#[derive(Debug)]
94#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
95#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
96#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
97#[cfg_attr(feature = "serde", serde(from = "CharacterDeserializeHelper"))]
98pub struct Character {
99 pub classes: Classes,
101 pub name: String,
102 pub age: u16,
103 pub race_index: String,
105 pub subrace_index: String,
107 pub alignment_index: String,
109 pub description: String,
111 pub background_index: String,
113 pub background_description: String,
115
116 experience_points: u32,
117
118 pub money: u32,
119
120 #[cfg_attr(feature = "serde", serde(with = "abilities_score_serde"))]
121 #[cfg_attr(feature = "utoipa", schema(value_type = Abilities))]
122 pub abilities_score: Arc<Mutex<Abilities>>,
123
124 pub hp: u16,
126 #[serde(default = "default_hit_dice")]
127 pub hit_dice_result: u16,
128
129 pub inventory: HashMap<String, u16>,
130
131 pub other: Vec<String>,
132}
133
134fn default_hit_dice() -> u16 {
136 12
137}
138
139#[cfg(feature = "serde")]
140#[derive(Deserialize)]
141#[serde(rename_all = "camelCase")]
142struct CharacterDeserializeHelper {
143 name: String,
144 age: u16,
145 race_index: String,
146 subrace_index: String,
147 alignment_index: String,
148 description: String,
149 background_index: String,
150 background_description: String,
151 experience_points: u32,
152 money: u32,
153 abilities_score: Abilities,
154 hp: u16,
155 #[serde(default = "default_hit_dice")]
156 hit_dice_result: u16,
157 inventory: HashMap<String, u16>,
158 other: Vec<String>,
159 #[serde(default)]
160 classes: serde_json::Value,
161}
162
163#[cfg(feature = "serde")]
164impl From<CharacterDeserializeHelper> for Character {
165 fn from(helper: CharacterDeserializeHelper) -> Self {
166 let abilities_score = Arc::new(Mutex::new(helper.abilities_score));
168
169 let classes =
171 match Classes::deserialize_with_abilities(helper.classes, abilities_score.clone()) {
172 Ok(classes) => classes,
173 Err(_) => Classes::default(),
174 };
175
176 Self {
177 classes,
178 name: helper.name,
179 age: helper.age,
180 race_index: helper.race_index,
181 subrace_index: helper.subrace_index,
182 alignment_index: helper.alignment_index,
183 description: helper.description,
184 background_index: helper.background_index,
185 background_description: helper.background_description,
186 experience_points: helper.experience_points,
187 money: helper.money,
188 abilities_score,
189 hp: helper.hp,
190 hit_dice_result: helper.hit_dice_result,
191 inventory: helper.inventory,
192 other: helper.other,
193 }
194 }
195}
196
197#[cfg(feature = "utoipa")]
198pub mod utoipa_addon {
199 use utoipa::openapi::OpenApi;
200 use utoipa::{Modify, PartialSchema, ToSchema};
201
202 pub struct ApiDocDndCharacterAddon;
203
204 impl Modify for ApiDocDndCharacterAddon {
205 fn modify(&self, openapi: &mut OpenApi) {
206 if let Some(components) = openapi.components.as_mut() {
207 components.schemas.insert(
208 super::classes::ClassProperties::name().to_string(),
209 super::classes::ClassProperties::schema(),
210 );
211 components.schemas.insert(
212 super::classes::ClassSpellCasting::name().to_string(),
213 super::classes::ClassSpellCasting::schema(),
214 );
215 components.schemas.insert(
216 super::classes::Class::name().to_string(),
217 super::classes::Class::schema(),
218 );
219 components
220 .schemas
221 .insert(super::Classes::name().to_string(), super::Classes::schema());
222 components.schemas.insert(
223 super::classes::UsableSlots::name().to_string(),
224 super::classes::UsableSlots::schema(),
225 );
226 components.schemas.insert(
227 super::Abilities::name().to_string(),
228 super::Abilities::schema(),
229 );
230 components.schemas.insert(
231 super::abilities::AbilityScore::name().to_string(),
232 super::abilities::AbilityScore::schema(),
233 );
234 components.schemas.insert(
235 super::Character::name().to_string(),
236 super::Character::schema(),
237 );
238 }
239 }
240 }
241}
242
243const LEVELS: [u32; 19] = [
244 300, 900, 2_700, 6_500, 14_000, 23_000, 34_000, 48_000, 64_000, 85_000, 100_000, 120_000,
245 140_000, 165_000, 195_000, 225_000, 265_000, 305_000, 355_000,
246];
247
248impl Character {
249 pub fn new(
250 main_class: String,
251 name: String,
252 age: u16,
253 race_index: String,
254 subrace_index: String,
255 alignment_index: String,
256 description: String,
257 background_index: String,
258 background_description: String,
259 ) -> Self {
260 let abilities_score = Arc::new(Mutex::new(Abilities::default()));
262
263 let mut classes = Classes::new(main_class);
265
266 for class in classes.0.values_mut() {
268 class.1.abilities_modifiers = abilities_score.clone();
269 }
270
271 Self {
272 classes,
273 name,
274 age,
275 race_index,
276 subrace_index,
277 alignment_index,
278 description,
279 background_index,
280 background_description,
281 experience_points: 0,
282 money: 0,
283 inventory: HashMap::new(),
284
285 abilities_score,
286 hp: 0,
287 hit_dice_result: 0,
288 other: vec![],
289 }
290 }
291
292 pub fn class_armor(&self) -> i8 {
293 let first_class = self.classes.0.iter().next().unwrap();
295 let class_name = first_class.0.as_str();
296
297 let abilities_score = self.abilities_score.lock().unwrap();
298
299 let mut base = match class_name {
301 "monk" => {
302 10 + abilities_score.dexterity.modifier(0) + abilities_score.wisdom.modifier(0)
303 }
304 "sorcerer" => 13 + abilities_score.dexterity.modifier(0),
305 "barbarian" => {
306 10 + abilities_score.dexterity.modifier(0)
307 + abilities_score.constitution.modifier(0)
308 }
309 _ => 10 + abilities_score.dexterity.modifier(0),
310 };
311
312 let has_defense_style = first_class
314 .1
315 .1
316 .fighting_style
317 .as_ref()
318 .map(|s| s.contains("defense"))
319 .unwrap_or(false)
320 || first_class
321 .1
322 .1
323 .additional_fighting_style
324 .as_ref()
325 .map(|s| s.contains("defense"))
326 .unwrap_or(false);
327
328 if has_defense_style {
330 base += 1;
331 }
332
333 base
334 }
335
336 pub fn level(&self) -> u8 {
338 LEVELS
339 .iter()
340 .filter(|&&x| x <= self.experience_points)
341 .count() as u8
342 + 1
343 }
344
345 pub fn experience_points(&self) -> u32 {
347 self.experience_points
348 }
349
350 pub fn add_experience(&mut self, experience: u32) -> u8 {
355 let previous_level = self.level();
357
358 let experience_to_add = LEVELS
360 .get(self.level() as usize - 1)
361 .map_or(experience, |&next_level_points| {
362 (next_level_points - self.experience_points).min(experience)
363 });
364
365 self.experience_points += experience_to_add;
367
368 let current_level = self.level();
370
371 current_level - previous_level
373 }
374
375 pub fn remove_item(
376 &mut self,
377 item: &str,
378 amount: Option<u16>,
379 ) -> anyhow::Result<(), anyhow::Error> {
380 if let Some(quantity) = self.inventory.get_mut(item) {
381 let quantity_to_remove = amount.unwrap_or(*quantity);
382
383 if *quantity <= quantity_to_remove {
384 self.inventory.remove(item);
385 } else {
386 *quantity -= quantity_to_remove;
387 }
388 } else {
389 bail!("Item not found")
390 }
391
392 Ok(())
393 }
394
395 pub fn add_item(&mut self, item: &str, amount: u16) {
396 if let Some(quantity) = self.inventory.get_mut(item) {
397 *quantity += amount;
398 } else {
399 self.inventory.insert(item.to_string(), amount);
400 }
401 }
402
403 pub fn alter_item_quantity(&mut self, item: &str, amount: i32) -> anyhow::Result<()> {
404 match amount.cmp(&0) {
405 Ordering::Greater => {
406 self.add_item(item, amount as u16);
407 Ok(())
408 }
409 Ordering::Less => self.remove_item(item, Some(amount.unsigned_abs() as u16)),
410 Ordering::Equal => {
411 bail!("Cannot alter quantity to 0")
412 }
413 }
414 }
415
416 pub fn max_hp(&self) -> u16 {
418 let constitution_modifier = self.abilities_score.lock().unwrap().constitution.modifier(0);
419
420 (constitution_modifier as i32)
421 .saturating_mul(self.level().into())
422 .saturating_add(self.hit_dice_result.into())
423 .max(0) as u16
424 }
425}