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 | "draconic-resilience"
386 | "dragon-wings"
387 | "draconic-presence"
388 | "font-of-magic"
389 | "dragon-ancestor-black---acid-damage"
390 | "dragon-ancestor-blue---lightning-damage"
391 | "dragon-ancestor-brass---fire-damage"
392 | "dragon-ancestor-bronze---lightning-damage"
393 | "dragon-ancestor-copper---acid-damage"
394 | "dragon-ancestor-gold---fire-damage"
395 | "dragon-ancestor-green---poison-damage"
396 | "dragon-ancestor-red---fire-damage"
397 | "dragon-ancestor-silver---cold-damage"
398 | "dragon-ancestor-white---cold-damage"
399 | "druid-lands-stride"
400 | "druid-timeless-body"
401 | "druidic"
402 | "elusive"
403 | "empowered-evocation"
404 | "elemental-affinity"
405 | "fast-movement"
406 | "favored-enemy-1-type"
407 | "favored-enemy-2-types"
408 | "favored-enemy-3-enemies"
409 | "feral-instinct"
410 | "feral-senses"
411 | "fighter-fighting-style-archery"
412 | "fighter-fighting-style-protection"
413 | "fighter-fighting-style-defense"
414 | "fighter-fighting-style-dueling"
415 | "fighter-fighting-style-great-weapon-fighting"
416 | "fighter-fighting-style-two-weapon-fighting"
417 | "fighting-style-defense"
418 | "fighting-style-dueling"
419 | "fighting-style-great-weapon-fighting"
420 | "foe-slayer"
421 | "hurl-through-hell"
422 | "improved-critical"
423 | "improved-divine-smite"
424 | "indomitable-1-use"
425 | "indomitable-2-uses"
426 | "indomitable-3-uses"
427 | "indomitable-might"
428 | "ki-empowered-strikes"
429 | "jack-of-all-trades"
430 | "martial-arts"
431 | "monk-evasion"
432 | "monk-timeless-body"
433 | "natural-explorer-1-terrain-type"
434 | "natural-explorer-2-terrain-types"
435 | "natural-explorer-3-terrain-types"
436 | "purity-of-body"
437 | "purity-of-spirit"
438 | "natures-sanctuary"
439 | "natures-ward"
440 | "sculpt-spells"
441 | "ranger-lands-stride"
442 | "relentless-rage"
443 | "reliable-talent"
444 | "remarkable-athlete"
445 | "rogue-evasion"
446 | "superior-critical"
447 | "superior-inspiration"
448 | "supreme-healing"
449 | "supreme-sneak"
450 | "survivor"
451 | "thiefs-reflexes"
452 | "thieves-cant"
453 | "tongue-of-the-sun-and-moon"
454 | "tranquility"
455 | "unarmored-movement-1"
456 | "unarmored-movement-2"
457 | "use-magic-device"
458 | "superior-hunters-defense"
459 | "superior-hunters-defense-evasion"
460 | "wild-shape-cr-1-2-or-below-no-flying-speed"
461 | "wild-shape-cr-1-4-or-below-no-flying-or-swim-speed"
462 | "wild-shape-cr-1-or-below"
463 | "ki"
464 | "monk-unarmored-defense"
465 | "perfect-self"
466 | "slippery-mind"
467 | "mindless-rage"
468 | "barbarian-unarmored-defense"
469 | "divine-intervention-improvement"
470 | "persistent-rage"
471 | "evocation-savant"
472 | "overchannel"
473 | "potent-cantrip"
474 | "second-story-work"
475 | "primeval-awareness"
476 | "vanish"
477 | "hunters-prey-colossus-slayer"
478 | "hunters-prey-giant-killer"
479 | "beast-spells" => Some(Passive),
480 "oath-spells" => Some(Ignored),
482 x if x.starts_with("bard-expertise-") || x.starts_with("rogue-expertise-") => {
484 Some(Ignored)
485 } x if x.starts_with("spellcasting-") => Some(Ignored),
487 x if x.starts_with("eldritch-invocation-") => Some(Ignored),
489 x if x.starts_with("circle-spells-") => Some(Ignored),
491 x if x.starts_with("circle-of-the-land-") => Some(Ignored),
493 x if x.starts_with("domain-spells-") => Some(Ignored),
495 x if x.contains("ability-score-improvement") => {
496 Some(Choosable(AbilityScoreImprovement))
497 }
498 _ => None,
499 }
500 }
501}
502
503impl Classes {
504 pub(super) async fn new_day(&mut self) {
505 futures::stream::iter(self.0.values_mut())
506 .for_each_concurrent(None, |class| class.new_day())
507 .await;
508 }
509}
510
511impl Class {
512 pub(super) async fn new_day(&mut self) {
513 use crate::classes::ClassSpellCasting::*;
514
515 let index = self.index().to_string();
516
517 if let Some(spell_casting) = &mut self.1.spell_casting {
518 match spell_casting {
519 KnowledgePrepared {
520 pending_preparation,
521 spells_prepared_index,
522 ..
523 }
524 | AlreadyKnowPrepared {
525 pending_preparation,
526 spells_prepared_index,
527 ..
528 } => {
529 *pending_preparation = true;
530 spells_prepared_index.clear();
531 }
532 KnowledgeAlreadyPrepared { usable_slots, .. } => {
533 if let Ok(Some(spellcasting_slots)) =
534 get_spellcasting_slots(index.as_str(), self.1.level).await
535 {
536 *usable_slots = spellcasting_slots.into();
537 }
538 }
539 }
540 }
541 }
542
543 pub async fn get_spellcasting_ability_index(&self) -> Result<String, ApiError> {
544 let op = SpellcastingAbilityQuery::build(SpellcastingAbilityQueryVariables {
545 index: Some(self.index().to_string()),
546 });
547
548 let ability_index = Client::new()
549 .post(GRAPHQL_API_URL.as_str())
550 .run_graphql(op)
551 .await?
552 .data
553 .ok_or(ApiError::Schema)?
554 .class
555 .ok_or(ApiError::Schema)?
556 .spellcasting
557 .ok_or(ApiError::Schema)?
558 .spellcasting_ability
559 .index;
560
561 Ok(ability_index)
562 }
563
564 pub async fn get_spellcasting_slots(&self) -> Result<Option<LevelSpellcasting>, ApiError> {
565 get_spellcasting_slots(self.index(), self.1.level).await
566 }
567
568 pub async fn set_level(
569 &mut self,
570 new_level: u8,
571 ) -> Result<Vec<ChoosableCustomLevelFeature>, ApiError> {
572 let op = LevelFeaturesQuery::build(LevelFeaturesQueryVariables {
573 class: Some(StringFilter(self.index().to_string())),
574 level: Some(LevelFilter {
575 gte: Some(self.1.level),
576 lte: Some(new_level),
577 }),
578 });
579
580 let features = Client::new()
581 .post(GRAPHQL_API_URL.as_str())
582 .run_graphql(op)
583 .await?
584 .data
585 .ok_or(ApiError::Schema)?
586 .features
587 .ok_or(ApiError::Schema)?;
588
589 let mut pending_features = vec![];
590
591 features
592 .iter()
593 .filter_map(|feature| CustomLevelFeatureType::identify(feature.index.clone()))
594 .for_each(|feature| match feature {
595 CustomLevelFeatureType::Passive => {}
596 CustomLevelFeatureType::Choosable(feature) => {
597 pending_features.push(feature);
598 }
599 CustomLevelFeatureType::Sheet(feature) => match feature {
600 SheetLevelFeatureType::PrimalChampion => {
601 self.1.abilities_modifiers.strength.score += 4;
602 self.1.abilities_modifiers.dexterity.score += 4;
603 }
604 },
605 Ignored => {}
606 });
607
608 self.1.level = new_level;
609
610 Ok(pending_features)
611 }
612
613 pub async fn get_levels_features(
614 &self,
615 from_level: Option<u8>,
616 passive: bool,
617 ) -> Result<Vec<String>, ApiError> {
618 let op = LevelFeaturesQuery::build(LevelFeaturesQueryVariables {
619 class: Some(StringFilter(self.index().to_string())),
620 level: Some(LevelFilter {
621 gte: Some(from_level.unwrap_or(0)),
622 lte: Some(self.1.level),
623 }),
624 });
625
626 let features = Client::new()
627 .post(GRAPHQL_API_URL.as_str())
628 .run_graphql(op)
629 .await?
630 .data
631 .ok_or(ApiError::Schema)?
632 .features
633 .ok_or(ApiError::Schema)?;
634
635 let mut features: Vec<String> = features
637 .into_iter()
638 .filter(
639 |feature| match CustomLevelFeatureType::identify(feature.index.clone()) {
640 None => true,
641 Some(custom_type) => match custom_type {
642 CustomLevelFeatureType::Passive => passive,
643 _ => false,
644 },
645 },
646 )
647 .map(|feature| feature.index)
648 .collect();
649
650 let features: Vec<String> = {
651 lazy_static! {
652 static ref CR_REGEX: regex::Regex =
653 regex::Regex::new(r"destroy-undead-cr-([0-9]+(?:-[0-9]+)?)\-or-below").unwrap();
654 }
655
656 let mut found = false;
657
658 features
659 .iter_mut()
660 .rev()
661 .filter(|feature| {
662 if CR_REGEX.is_match(feature) {
663 if found {
664 false
665 } else {
666 found = true;
667 true
668 }
669 } else {
670 true
671 }
672 })
673 .map(|feature| feature.clone())
674 .collect()
675 };
676
677 lazy_static! {
678 static ref DICE_REGEX: regex::Regex = regex::Regex::new(r"^(.+)-d(\d+)$").unwrap();
679 }
680
681 let mut grouped_features: HashMap<String, u32> = HashMap::new();
682 for feature in &features {
683 if let Some(caps) = DICE_REGEX.captures(feature) {
684 if caps.len() == 3 {
685 let prefix = caps.get(1).unwrap().as_str().to_string();
686 let dice_value = caps.get(2).unwrap().as_str().parse::<u32>().unwrap();
687
688 let current_max = grouped_features.entry(prefix).or_insert(0);
689 if dice_value > *current_max {
690 *current_max = dice_value;
691 }
692 }
693 }
694 }
695
696 let features = features
697 .into_iter()
698 .filter(|feature| {
699 if let Some(caps) = DICE_REGEX.captures(feature) {
700 let prefix = caps.get(1).unwrap().as_str();
701 let dice_value = caps
702 .get(2)
703 .unwrap()
704 .as_str()
705 .parse::<u32>()
706 .expect("Parsing dice value");
707
708 if let Some(&max_dice) = grouped_features.get(prefix) {
709 return dice_value == max_dice;
710 }
711 }
712 true
713 })
714 .collect();
715
716 Ok(features)
717 }
718
719 pub fn apply_option(&mut self, option: ChoosableCustomLevelFeatureOption) {
720 use ChoosableCustomLevelFeatureOption::*;
721
722 match option {
723 StrengthPlusOne | DexterityPlusOne | ConstitutionPlusOne | IntelligencePlusOne
724 | WisdomPlusOne | CharismaPlusOne => self.increase_score(option),
725 BardProficiencyStrength
726 | BardProficiencyDexterity
727 | BardProficiencyConstitution
728 | BardProficiencyIntelligence
729 | BardProficiencyWisdom
730 | BardProficiencyCharisma => self.set_proficiency(option),
731 PactOfTheChain | PactOfTheBlade | PactOfTheTome => {
732 println!("Pact of the Chain, Blade or Tome not yet implemented");
733 }
734 FighterFightingStyleArchery
735 | FighterFightingStyleDefense
736 | FighterFightingStyleDueling
737 | FighterFightingStyleGreatWeaponFighting
738 | FighterFightingStyleProtection
739 | FighterFightingStyleTwoWeaponFighting
740 | RangerFightingStyleArchery
741 | RangerFightingStyleDefense
742 | RangerFightingStyleDueling
743 | RangerFightingStyleTwoWeaponFighting
744 | FightingStyleDefense
745 | FightingStyleDueling
746 | FightingStyleGreatWeaponFighting
747 | FightingStyleProtection => {
748 if self.1.fighting_style.is_none() {
749 self.1
750 .fighting_style
751 .replace(option.as_index_str().to_string());
752 } else {
753 self.1
754 .additional_fighting_style
755 .replace(option.as_index_str().to_string());
756 }
757 }
758 }
759 }
760
761 fn increase_score(&mut self, option: ChoosableCustomLevelFeatureOption) {
762 match option {
763 ChoosableCustomLevelFeatureOption::StrengthPlusOne => {
764 self.1.abilities_modifiers.strength.score += 1;
765 }
766 ChoosableCustomLevelFeatureOption::DexterityPlusOne => {
767 self.1.abilities_modifiers.dexterity.score += 1;
768 }
769 ChoosableCustomLevelFeatureOption::ConstitutionPlusOne => {
770 self.1.abilities_modifiers.constitution.score += 1;
771 }
772 ChoosableCustomLevelFeatureOption::IntelligencePlusOne => {
773 self.1.abilities_modifiers.intelligence.score += 1;
774 }
775 ChoosableCustomLevelFeatureOption::WisdomPlusOne => {
776 self.1.abilities_modifiers.wisdom.score += 1;
777 }
778 ChoosableCustomLevelFeatureOption::CharismaPlusOne => {
779 self.1.abilities_modifiers.charisma.score += 1;
780 }
781 _ => {}
782 }
783 }
784
785 fn set_proficiency(&mut self, option: ChoosableCustomLevelFeatureOption) {
786 match option {
787 ChoosableCustomLevelFeatureOption::BardProficiencyStrength => {
788 self.1.abilities_modifiers.strength.proficiency = true;
789 }
790 ChoosableCustomLevelFeatureOption::BardProficiencyDexterity => {
791 self.1.abilities_modifiers.dexterity.proficiency = true;
792 }
793 ChoosableCustomLevelFeatureOption::BardProficiencyConstitution => {
794 self.1.abilities_modifiers.constitution.proficiency = true;
795 }
796 ChoosableCustomLevelFeatureOption::BardProficiencyIntelligence => {
797 self.1.abilities_modifiers.intelligence.proficiency = true;
798 }
799 ChoosableCustomLevelFeatureOption::BardProficiencyWisdom => {
800 self.1.abilities_modifiers.wisdom.proficiency = true;
801 }
802 ChoosableCustomLevelFeatureOption::BardProficiencyCharisma => {
803 self.1.abilities_modifiers.charisma.proficiency = true;
804 }
805 _ => {}
806 }
807 }
808}
809
810pub async fn get_spellcasting_slots(
811 index: &str,
812 level: u8,
813) -> Result<Option<LevelSpellcasting>, ApiError> {
814 let op = SpellcastingQuery::build(SpellcastingQueryVariables {
815 index: Some(format!("{}-{}", index, level)),
816 });
817
818 let spellcasting_slots = Client::new()
819 .post(GRAPHQL_API_URL.as_str())
820 .run_graphql(op)
821 .await?
822 .data
823 .ok_or(ApiError::Schema)?
824 .level
825 .ok_or(ApiError::Schema)?
826 .spellcasting;
827
828 Ok(spellcasting_slots)
829}