1use super::shared::schema;
2use crate::api::classes::CustomLevelFeatureType::Ignored;
3use crate::api::shared::ApiError;
4use crate::classes::{Class, Classes, UsableSlots};
5use crate::GRAPHQL_API_URL;
6use cynic::http::ReqwestExt;
7use cynic::{impl_scalar, QueryBuilder};
8use futures::StreamExt;
9use lazy_static::lazy_static;
10use reqwest::Client;
11use serde_json::json;
12use std::collections::HashMap;
13
14#[derive(cynic::QueryVariables, Debug)]
15struct SpellcastingAbilityQueryVariables {
16 pub index: Option<String>,
17}
18
19#[derive(cynic::QueryFragment, Debug)]
20#[cynic(
21 graphql_type = "Query",
22 variables = "SpellcastingAbilityQueryVariables"
23)]
24struct SpellcastingAbilityQuery {
25 #[arguments(index: $ index)]
26 pub class: Option<ClassSpellCasting>,
27}
28
29#[derive(cynic::QueryFragment, Debug)]
30#[cynic(graphql_type = "Class")]
31struct ClassSpellCasting {
32 pub spellcasting: Option<ClassSpellcasting>,
33}
34
35#[derive(cynic::QueryFragment, Debug)]
36struct ClassSpellcasting {
37 #[cynic(rename = "spellcasting_ability")]
38 pub spellcasting_ability: AbilityScore,
39}
40
41#[derive(cynic::QueryFragment, Debug)]
42struct AbilityScore {
43 pub index: String,
44}
45
46#[derive(cynic::QueryVariables, Debug)]
47pub struct SpellcastingQueryVariables {
48 pub index: Option<String>,
49}
50
51#[derive(cynic::QueryFragment, Debug)]
52#[cynic(graphql_type = "Query", variables = "SpellcastingQueryVariables")]
53pub struct SpellcastingQuery {
54 #[arguments(index: $ index)]
55 pub level: Option<Level>,
56}
57
58#[derive(cynic::QueryFragment, Debug)]
59pub struct Level {
60 pub spellcasting: Option<LevelSpellcasting>,
61}
62
63#[derive(cynic::QueryFragment, Debug, Copy, Clone)]
64#[cfg_attr(feature = "serde", derive(serde::Serialize))]
65pub struct LevelSpellcasting {
66 #[cynic(rename = "cantrips_known")]
67 pub cantrips_known: Option<i32>,
68 #[cynic(rename = "spell_slots_level_1")]
69 pub spell_slots_level_1: Option<i32>,
70 #[cynic(rename = "spell_slots_level_2")]
71 pub spell_slots_level_2: Option<i32>,
72 #[cynic(rename = "spell_slots_level_3")]
73 pub spell_slots_level_3: Option<i32>,
74 #[cynic(rename = "spell_slots_level_4")]
75 pub spell_slots_level_4: Option<i32>,
76 #[cynic(rename = "spell_slots_level_5")]
77 pub spell_slots_level_5: Option<i32>,
78 #[cynic(rename = "spell_slots_level_6")]
79 pub spell_slots_level_6: Option<i32>,
80 #[cynic(rename = "spell_slots_level_7")]
81 pub spell_slots_level_7: Option<i32>,
82 #[cynic(rename = "spell_slots_level_8")]
83 pub spell_slots_level_8: Option<i32>,
84 #[cynic(rename = "spell_slots_level_9")]
85 pub spell_slots_level_9: Option<i32>,
86}
87
88impl Into<UsableSlots> for LevelSpellcasting {
89 fn into(self) -> UsableSlots {
90 UsableSlots {
91 level_1: self.spell_slots_level_1.unwrap_or(0) as u8,
92 level_2: self.spell_slots_level_2.unwrap_or(0) as u8,
93 level_3: self.spell_slots_level_3.unwrap_or(0) as u8,
94 level_4: self.spell_slots_level_4.unwrap_or(0) as u8,
95 level_5: self.spell_slots_level_5.unwrap_or(0) as u8,
96 level_6: self.spell_slots_level_6.unwrap_or(0) as u8,
97 level_7: self.spell_slots_level_7.unwrap_or(0) as u8,
98 level_8: self.spell_slots_level_8.unwrap_or(0) as u8,
99 level_9: self.spell_slots_level_9.unwrap_or(0) as u8,
100 }
101 }
102}
103
104#[derive(cynic::QueryVariables, Debug)]
105pub struct LevelFeaturesQueryVariables {
106 pub class: Option<StringFilter>,
107 pub level: Option<LevelFilter>,
108}
109
110#[derive(serde::Serialize, Debug)]
111pub struct LevelFilter {
112 pub gte: Option<u8>,
113 pub lte: Option<u8>,
114}
115
116impl_scalar!(LevelFilter, schema::IntFilter);
117
118#[derive(cynic::QueryFragment, Debug)]
119#[cynic(graphql_type = "Query", variables = "LevelFeaturesQueryVariables")]
120pub struct LevelFeaturesQuery {
121 #[arguments(class: $ class, level: $level )]
122 pub features: Option<Vec<Feature>>,
123}
124
125#[derive(cynic::QueryFragment, Debug)]
126pub struct Feature {
127 pub index: String,
128}
129
130#[derive(cynic::Scalar, Debug, Clone)]
131pub struct StringFilter(pub String);
132
133#[derive(Clone)]
134#[cfg_attr(feature = "serde", derive(serde::Serialize))]
135#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
136pub enum ChoosableCustomLevelFeature {
137 AbilityScoreImprovement,
139 WarlockPact,
141 AdditionalFighterFightingStyle,
143 FighterFightingStyle,
145 RangerFightingStyle,
147 BonusBardProficiency,
149 MultiplyTwoSkillProficiency,
155 ChooseTwoSpellForAnyClass,
159 ChooseOne6thLevelSpellFromWarlockList,
164 PaladinFightingStyle,
166}
167
168#[derive(Clone, Debug)]
169#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
170#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
171pub enum ChoosableCustomLevelFeatureOption {
172 StrengthPlusOne,
173 DexterityPlusOne,
174 ConstitutionPlusOne,
175 IntelligencePlusOne,
176 WisdomPlusOne,
177 CharismaPlusOne,
178
179 PactOfTheChain,
180 PactOfTheBlade,
181 PactOfTheTome,
182
183 FighterFightingStyleArchery,
184 FighterFightingStyleDefense,
185 FighterFightingStyleDueling,
186 FighterFightingStyleGreatWeaponFighting,
187 FighterFightingStyleProtection,
188 FighterFightingStyleTwoWeaponFighting,
189
190 RangerFightingStyleArchery,
191 RangerFightingStyleDefense,
192 RangerFightingStyleDueling,
193 RangerFightingStyleTwoWeaponFighting,
194
195 BardProficiencyStrength,
196 BardProficiencyDexterity,
197 BardProficiencyConstitution,
198 BardProficiencyIntelligence,
199 BardProficiencyWisdom,
200 BardProficiencyCharisma,
201
202 FightingStyleDefense,
203 FightingStyleDueling,
204 FightingStyleGreatWeaponFighting,
205 FightingStyleProtection,
206}
207
208impl ChoosableCustomLevelFeatureOption {
209 #[cfg(feature = "serde")]
210 pub fn as_index_str(&self) -> &str {
211 serde_variant::to_variant_name(self).unwrap()
212 }
213
214 #[cfg(feature = "serde")]
215 pub fn from_index_str(index: &str) -> Option<ChoosableCustomLevelFeatureOption> {
216 #[derive(serde::Deserialize)]
217 struct Helper {
218 value: ChoosableCustomLevelFeatureOption,
219 }
220
221 let json = json!({
222 "value": index
223 });
224
225 serde_json::from_value::<Helper>(json)
226 .map(|helper| helper.value)
227 .ok()
228 }
229}
230
231impl ChoosableCustomLevelFeature {
232 #[cfg(feature = "serde")]
233 pub fn as_index_str(&self) -> &str {
234 serde_variant::to_variant_name(self).unwrap()
235 }
236
237 pub fn to_options(&self) -> Vec<Vec<ChoosableCustomLevelFeatureOption>> {
238 use ChoosableCustomLevelFeatureOption::*;
239
240 match self {
241 ChoosableCustomLevelFeature::AbilityScoreImprovement => {
242 let ability_names = vec![
243 StrengthPlusOne,
244 DexterityPlusOne,
245 ConstitutionPlusOne,
246 IntelligencePlusOne,
247 WisdomPlusOne,
248 CharismaPlusOne,
249 ];
250
251 vec![ability_names.clone(), ability_names]
252 }
253 ChoosableCustomLevelFeature::WarlockPact => {
254 vec![vec![PactOfTheChain, PactOfTheBlade, PactOfTheTome]]
255 }
256 ChoosableCustomLevelFeature::AdditionalFighterFightingStyle
257 | ChoosableCustomLevelFeature::FighterFightingStyle => {
258 vec![vec![
259 FighterFightingStyleArchery,
260 FighterFightingStyleDefense,
261 FighterFightingStyleDueling,
262 FighterFightingStyleGreatWeaponFighting,
263 FighterFightingStyleProtection,
264 FighterFightingStyleTwoWeaponFighting,
265 ]]
266 }
267 ChoosableCustomLevelFeature::RangerFightingStyle => {
268 vec![vec![
269 RangerFightingStyleArchery,
270 RangerFightingStyleDefense,
271 RangerFightingStyleDueling,
272 RangerFightingStyleTwoWeaponFighting,
273 ]]
274 }
275 ChoosableCustomLevelFeature::BonusBardProficiency => {
276 let ability_names = vec![
277 BardProficiencyStrength,
278 BardProficiencyDexterity,
279 BardProficiencyConstitution,
280 BardProficiencyIntelligence,
281 BardProficiencyWisdom,
282 BardProficiencyCharisma,
283 ];
284
285 vec![ability_names.clone(), ability_names.clone(), ability_names]
286 }
287 ChoosableCustomLevelFeature::MultiplyTwoSkillProficiency => {
288 vec![vec![]]
290 }
291 ChoosableCustomLevelFeature::ChooseTwoSpellForAnyClass => {
292 vec![vec![]]
294 }
295 ChoosableCustomLevelFeature::ChooseOne6thLevelSpellFromWarlockList => {
296 vec![vec![]]
298 }
299 ChoosableCustomLevelFeature::PaladinFightingStyle => {
300 vec![vec![
301 FightingStyleDefense,
302 FightingStyleDueling,
303 FightingStyleGreatWeaponFighting,
304 FightingStyleProtection,
305 ]]
306 }
307 }
308 }
309}
310
311pub enum SheetLevelFeatureType {
312 PrimalChampion,
314}
315
316pub enum CustomLevelFeatureType {
317 Choosable(ChoosableCustomLevelFeature),
318 Sheet(SheetLevelFeatureType),
319 Passive,
320 Ignored,
321}
322
323impl CustomLevelFeatureType {
324 pub fn identify(index: String) -> Option<CustomLevelFeatureType> {
325 use ChoosableCustomLevelFeature::*;
326 use CustomLevelFeatureType::*;
327 use SheetLevelFeatureType::*;
328 match index.as_str() {
329 "bard-college"
331 | "divine-domain"
332 | "monastic-tradition"
333 | "sacred-oath"
334 | "ranger-archetype"
335 | "sorcerous-origin"
336 | "druid-circle"
337 | "primal-path"
338 | "martial-archetype"
339 | "otherworldly-patron" => Some(Ignored),
340 "pact-boon" => Some(Choosable(WarlockPact)),
341 "additional-fighting-style" => Some(Choosable(AdditionalFighterFightingStyle)),
342 "ranger-fighting-style" => Some(Choosable(RangerFightingStyle)),
343 "fighter-fighting-style" => Some(Choosable(FighterFightingStyle)),
344 "bonus-proficiencies" => Some(Choosable(BonusBardProficiency)),
345 "bonus-proficiency" => Some(Passive),
346 "additional-magical-secrets" | "bonus-cantrip" => Some(Ignored),
347 "channel-divinity-1-rest" | "channel-divinity-2-rest" | "channel-divinity-3-rest" => {
348 Some(Ignored)
349 }
350 "magical-secrets-1" | "magical-secrets-2" | "magical-secrets-3" => Some(Ignored),
352 "mystic-arcanum-6th-level"
353 | "mystic-arcanum-7th-level"
354 | "mystic-arcanum-8th-level"
355 | "mystic-arcanum-9th-level" => Some(Choosable(ChooseOne6thLevelSpellFromWarlockList)),
356 "paladin-fighting-style" => Some(Choosable(PaladinFightingStyle)),
357 "primal-champion" => Some(Sheet(PrimalChampion)),
358 "diamond-soul" => Some(Passive),
360 "arcane-recovery"
361 | "archdruid"
362 | "aura-improvements"
363 | "aura-of-courage"
364 | "aura-of-devotion"
365 | "aura-of-protection"
366 | "blessed-healer"
367 | "blindsense"
368 | "brutal-critical-1-dice"
369 | "brutal-critical-2-dice"
370 | "brutal-critical-3-dice"
371 | "danger-sense"
372 | "dark-ones-blessing"
373 | "dark-ones-own-luck"
374 | "defensive-tactics"
375 | "defensive-tactics-steel-will"
376 | "defensive-tactics-escape-the-horde"
377 | "defensive-tactics-multiattack-defense"
378 | "destroy-undead-cr-1-or-below"
379 | "destroy-undead-cr-2-or-below"
380 | "destroy-undead-cr-3-or-below"
381 | "destroy-undead-cr-4-or-below"
382 | "destroy-undead-cr-1-2-or-below"
383 | "disciple-of-life"
384 | "divine-health"
385 | "dragon-ancestor-black---acid-damage"
386 | "dragon-ancestor-blue---lightning-damage"
387 | "dragon-ancestor-brass---fire-damage"
388 | "dragon-ancestor-bronze---lightning-damage"
389 | "dragon-ancestor-copper---acid-damage"
390 | "dragon-ancestor-gold---fire-damage"
391 | "dragon-ancestor-green---poison-damage"
392 | "dragon-ancestor-red---fire-damage"
393 | "dragon-ancestor-silver---cold-damage"
394 | "dragon-ancestor-white---cold-damage"
395 | "druid-lands-stride"
396 | "druid-timeless-body"
397 | "druidic"
398 | "elusive"
399 | "empowered-evocation"
400 | "elemental-affinity"
401 | "fast-movement"
402 | "favored-enemy-1-type"
403 | "favored-enemy-2-types"
404 | "favored-enemy-3-enemies"
405 | "feral-instinct"
406 | "feral-senses"
407 | "fighter-fighting-style-archery"
408 | "fighter-fighting-style-protection"
409 | "fighter-fighting-style-defense"
410 | "fighter-fighting-style-dueling"
411 | "fighter-fighting-style-great-weapon-fighting"
412 | "fighter-fighting-style-two-weapon-fighting"
413 | "fighting-style-defense"
414 | "fighting-style-dueling"
415 | "fighting-style-great-weapon-fighting"
416 | "foe-slayer"
417 | "hurl-through-hell"
418 | "improved-critical"
419 | "improved-divine-smite"
420 | "indomitable-1-use"
421 | "indomitable-2-uses"
422 | "indomitable-3-uses"
423 | "indomitable-might"
424 | "ki-empowered-strikes"
425 | "jack-of-all-trades"
426 | "martial-arts"
427 | "monk-evasion"
428 | "monk-timeless-body"
429 | "natural-explorer-1-terrain-type"
430 | "natural-explorer-2-terrain-types"
431 | "natural-explorer-3-terrain-types"
432 | "purity-of-body"
433 | "purity-of-spirit"
434 | "natures-sanctuary"
435 | "natures-ward"
436 | "sculpt-spells"
437 | "ranger-lands-stride"
438 | "relentless-rage"
439 | "reliable-talent"
440 | "remarkable-athlete"
441 | "rogue-evasion"
442 | "superior-critical"
443 | "superior-inspiration"
444 | "supreme-healing"
445 | "supreme-sneak"
446 | "survivor"
447 | "thiefs-reflexes"
448 | "thieves-cant"
449 | "tongue-of-the-sun-and-moon"
450 | "tranquility"
451 | "unarmored-movement-1"
452 | "unarmored-movement-2"
453 | "use-magic-device"
454 | "superior-hunters-defense"
455 | "superior-hunters-defense-evasion"
456 | "wild-shape-cr-1-2-or-below-no-flying-speed"
457 | "wild-shape-cr-1-4-or-below-no-flying-or-swim-speed"
458 | "wild-shape-cr-1-or-below"
459 | "ki"
460 | "monk-unarmored-defense"
461 | "perfect-self"
462 | "slippery-mind"
463 | "mindless-rage"
464 | "barbarian-unarmored-defense"
465 | "divine-intervention-improvement"
466 | "persistent-rage"
467 | "evocation-savant"
468 | "potent-cantrip"
469 | "second-story-work"
470 | "beast-spells" => Some(Passive),
471 x if x.starts_with("bard-expertise-") || x.starts_with("rogue-expertise-") => {
473 Some(Ignored)
474 } x if x.starts_with("spellcasting-") => Some(Ignored),
476 x if x.starts_with("eldritch-invocation-") => Some(Ignored),
478 x if x.starts_with("circle-spells-") => Some(Ignored),
480 x if x.starts_with("circle-of-the-land-") => Some(Ignored),
482 x if x.starts_with("domain-spells-") => Some(Ignored),
484 x if x.contains("ability-score-improvement") => {
485 Some(Choosable(AbilityScoreImprovement))
486 }
487 _ => None,
488 }
489 }
490}
491
492impl Classes {
493 pub(super) async fn new_day(&mut self) {
494 futures::stream::iter(self.0.values_mut())
495 .for_each_concurrent(None, |class| class.new_day())
496 .await;
497 }
498}
499
500impl Class {
501 pub(super) async fn new_day(&mut self) {
502 use crate::classes::ClassSpellCasting::*;
503
504 let index = self.index().to_string();
505
506 if let Some(spell_casting) = &mut self.1.spell_casting {
507 match spell_casting {
508 KnowledgePrepared {
509 pending_preparation,
510 spells_prepared_index,
511 ..
512 }
513 | AlreadyKnowPrepared {
514 pending_preparation,
515 spells_prepared_index,
516 ..
517 } => {
518 *pending_preparation = true;
519 spells_prepared_index.clear();
520 }
521 KnowledgeAlreadyPrepared { usable_slots, .. } => {
522 if let Ok(Some(spellcasting_slots)) =
523 get_spellcasting_slots(index.as_str(), self.1.level).await
524 {
525 *usable_slots = spellcasting_slots.into();
526 }
527 }
528 }
529 }
530 }
531
532 pub async fn get_spellcasting_ability_index(&self) -> Result<String, ApiError> {
533 let op = SpellcastingAbilityQuery::build(SpellcastingAbilityQueryVariables {
534 index: Some(self.index().to_string()),
535 });
536
537 let ability_index = Client::new()
538 .post(GRAPHQL_API_URL.as_str())
539 .run_graphql(op)
540 .await?
541 .data
542 .ok_or(ApiError::Schema)?
543 .class
544 .ok_or(ApiError::Schema)?
545 .spellcasting
546 .ok_or(ApiError::Schema)?
547 .spellcasting_ability
548 .index;
549
550 Ok(ability_index)
551 }
552
553 pub async fn get_spellcasting_slots(&self) -> Result<Option<LevelSpellcasting>, ApiError> {
554 get_spellcasting_slots(self.index(), self.1.level).await
555 }
556
557 pub async fn set_level(
558 &mut self,
559 new_level: u8,
560 ) -> Result<Vec<ChoosableCustomLevelFeature>, ApiError> {
561 let op = LevelFeaturesQuery::build(LevelFeaturesQueryVariables {
562 class: Some(StringFilter(self.index().to_string())),
563 level: Some(LevelFilter {
564 gte: Some(self.1.level),
565 lte: Some(new_level),
566 }),
567 });
568
569 let features = Client::new()
570 .post(GRAPHQL_API_URL.as_str())
571 .run_graphql(op)
572 .await?
573 .data
574 .ok_or(ApiError::Schema)?
575 .features
576 .ok_or(ApiError::Schema)?;
577
578 let mut pending_features = vec![];
579
580 features
581 .iter()
582 .filter_map(|feature| CustomLevelFeatureType::identify(feature.index.clone()))
583 .for_each(|feature| match feature {
584 CustomLevelFeatureType::Passive => {}
585 CustomLevelFeatureType::Choosable(feature) => {
586 pending_features.push(feature);
587 }
588 CustomLevelFeatureType::Sheet(feature) => match feature {
589 SheetLevelFeatureType::PrimalChampion => {
590 self.1.abilities_modifiers.strength.score += 4;
591 self.1.abilities_modifiers.dexterity.score += 4;
592 }
593 },
594 Ignored => {}
595 });
596
597 self.1.level = new_level;
598
599 Ok(pending_features)
600 }
601
602 pub async fn get_levels_features(
603 &self,
604 from_level: Option<u8>,
605 passive: bool,
606 ) -> Result<Vec<String>, ApiError> {
607 let op = LevelFeaturesQuery::build(LevelFeaturesQueryVariables {
608 class: Some(StringFilter(self.index().to_string())),
609 level: Some(LevelFilter {
610 gte: Some(from_level.unwrap_or(0)),
611 lte: Some(self.1.level),
612 }),
613 });
614
615 let features = Client::new()
616 .post(GRAPHQL_API_URL.as_str())
617 .run_graphql(op)
618 .await?
619 .data
620 .ok_or(ApiError::Schema)?
621 .features
622 .ok_or(ApiError::Schema)?;
623
624 let mut features: Vec<String> = features
626 .into_iter()
627 .filter(
628 |feature| match CustomLevelFeatureType::identify(feature.index.clone()) {
629 None => true,
630 Some(custom_type) => match custom_type {
631 CustomLevelFeatureType::Passive => passive,
632 _ => false,
633 },
634 },
635 )
636 .map(|feature| feature.index)
637 .collect();
638
639 let features: Vec<String> = {
640 lazy_static! {
641 static ref CR_REGEX: regex::Regex =
642 regex::Regex::new(r"destroy-undead-cr-([0-9]+(?:-[0-9]+)?)\-or-below").unwrap();
643 }
644
645 let mut found = false;
646
647 features
648 .iter_mut()
649 .rev()
650 .filter(|feature| {
651 if CR_REGEX.is_match(feature) {
652 if found {
653 false
654 } else {
655 found = true;
656 true
657 }
658 } else {
659 true
660 }
661 })
662 .map(|feature| feature.clone())
663 .collect()
664 };
665
666 lazy_static! {
667 static ref DICE_REGEX: regex::Regex = regex::Regex::new(r"^(.+)-d(\d+)$").unwrap();
668 }
669
670 let mut grouped_features: HashMap<String, u32> = HashMap::new();
671 for feature in &features {
672 if let Some(caps) = DICE_REGEX.captures(feature) {
673 if caps.len() == 3 {
674 let prefix = caps.get(1).unwrap().as_str().to_string();
675 let dice_value = caps.get(2).unwrap().as_str().parse::<u32>().unwrap();
676
677 let current_max = grouped_features.entry(prefix).or_insert(0);
678 if dice_value > *current_max {
679 *current_max = dice_value;
680 }
681 }
682 }
683 }
684
685 let features = features
686 .into_iter()
687 .filter(|feature| {
688 if let Some(caps) = DICE_REGEX.captures(feature) {
689 let prefix = caps.get(1).unwrap().as_str();
690 let dice_value = caps
691 .get(2)
692 .unwrap()
693 .as_str()
694 .parse::<u32>()
695 .expect("Parsing dice value");
696
697 if let Some(&max_dice) = grouped_features.get(prefix) {
698 return dice_value == max_dice;
699 }
700 }
701 true
702 })
703 .collect();
704
705 Ok(features)
706 }
707
708 pub fn apply_option(&mut self, option: ChoosableCustomLevelFeatureOption) {
709 use ChoosableCustomLevelFeatureOption::*;
710
711 match option {
712 StrengthPlusOne | DexterityPlusOne | ConstitutionPlusOne | IntelligencePlusOne
713 | WisdomPlusOne | CharismaPlusOne => self.increase_score(option),
714 BardProficiencyStrength
715 | BardProficiencyDexterity
716 | BardProficiencyConstitution
717 | BardProficiencyIntelligence
718 | BardProficiencyWisdom
719 | BardProficiencyCharisma => self.set_proficiency(option),
720 PactOfTheChain | PactOfTheBlade | PactOfTheTome => {
721 println!("Pact of the Chain, Blade or Tome not yet implemented");
722 }
723 FighterFightingStyleArchery
724 | FighterFightingStyleDefense
725 | FighterFightingStyleDueling
726 | FighterFightingStyleGreatWeaponFighting
727 | FighterFightingStyleProtection
728 | FighterFightingStyleTwoWeaponFighting
729 | RangerFightingStyleArchery
730 | RangerFightingStyleDefense
731 | RangerFightingStyleDueling
732 | RangerFightingStyleTwoWeaponFighting
733 | FightingStyleDefense
734 | FightingStyleDueling
735 | FightingStyleGreatWeaponFighting
736 | FightingStyleProtection => {
737 if self.1.fighting_style.is_none() {
738 self.1
739 .fighting_style
740 .replace(option.as_index_str().to_string());
741 } else {
742 self.1
743 .additional_fighting_style
744 .replace(option.as_index_str().to_string());
745 }
746 }
747 }
748 }
749
750 fn increase_score(&mut self, option: ChoosableCustomLevelFeatureOption) {
751 match option {
752 ChoosableCustomLevelFeatureOption::StrengthPlusOne => {
753 self.1.abilities_modifiers.strength.score += 1;
754 }
755 ChoosableCustomLevelFeatureOption::DexterityPlusOne => {
756 self.1.abilities_modifiers.dexterity.score += 1;
757 }
758 ChoosableCustomLevelFeatureOption::ConstitutionPlusOne => {
759 self.1.abilities_modifiers.constitution.score += 1;
760 }
761 ChoosableCustomLevelFeatureOption::IntelligencePlusOne => {
762 self.1.abilities_modifiers.intelligence.score += 1;
763 }
764 ChoosableCustomLevelFeatureOption::WisdomPlusOne => {
765 self.1.abilities_modifiers.wisdom.score += 1;
766 }
767 ChoosableCustomLevelFeatureOption::CharismaPlusOne => {
768 self.1.abilities_modifiers.charisma.score += 1;
769 }
770 _ => {}
771 }
772 }
773
774 fn set_proficiency(&mut self, option: ChoosableCustomLevelFeatureOption) {
775 match option {
776 ChoosableCustomLevelFeatureOption::BardProficiencyStrength => {
777 self.1.abilities_modifiers.strength.proficiency = true;
778 }
779 ChoosableCustomLevelFeatureOption::BardProficiencyDexterity => {
780 self.1.abilities_modifiers.dexterity.proficiency = true;
781 }
782 ChoosableCustomLevelFeatureOption::BardProficiencyConstitution => {
783 self.1.abilities_modifiers.constitution.proficiency = true;
784 }
785 ChoosableCustomLevelFeatureOption::BardProficiencyIntelligence => {
786 self.1.abilities_modifiers.intelligence.proficiency = true;
787 }
788 ChoosableCustomLevelFeatureOption::BardProficiencyWisdom => {
789 self.1.abilities_modifiers.wisdom.proficiency = true;
790 }
791 ChoosableCustomLevelFeatureOption::BardProficiencyCharisma => {
792 self.1.abilities_modifiers.charisma.proficiency = true;
793 }
794 _ => {}
795 }
796 }
797}
798
799pub async fn get_spellcasting_slots(
800 index: &str,
801 level: u8,
802) -> Result<Option<LevelSpellcasting>, ApiError> {
803 let op = SpellcastingQuery::build(SpellcastingQueryVariables {
804 index: Some(format!("{}-{}", index, level)),
805 });
806
807 let spellcasting_slots = Client::new()
808 .post(GRAPHQL_API_URL.as_str())
809 .run_graphql(op)
810 .await?
811 .data
812 .ok_or(ApiError::Schema)?
813 .level
814 .ok_or(ApiError::Schema)?
815 .spellcasting;
816
817 Ok(spellcasting_slots)
818}