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