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 x if x.starts_with("bard-expertise-") || x.starts_with("rogue-expertise-") => {
482 Some(Ignored)
483 } x if x.starts_with("spellcasting-") => Some(Ignored),
485 x if x.starts_with("eldritch-invocation-") => Some(Ignored),
487 x if x.starts_with("circle-spells-") => Some(Ignored),
489 x if x.starts_with("circle-of-the-land-") => Some(Ignored),
491 x if x.starts_with("domain-spells-") => Some(Ignored),
493 x if x.contains("ability-score-improvement") => {
494 Some(Choosable(AbilityScoreImprovement))
495 }
496 _ => None,
497 }
498 }
499}
500
501impl Classes {
502 pub(super) async fn new_day(&mut self) {
503 futures::stream::iter(self.0.values_mut())
504 .for_each_concurrent(None, |class| class.new_day())
505 .await;
506 }
507}
508
509impl Class {
510 pub(super) async fn new_day(&mut self) {
511 use crate::classes::ClassSpellCasting::*;
512
513 let index = self.index().to_string();
514
515 if let Some(spell_casting) = &mut self.1.spell_casting {
516 match spell_casting {
517 KnowledgePrepared {
518 pending_preparation,
519 spells_prepared_index,
520 ..
521 }
522 | AlreadyKnowPrepared {
523 pending_preparation,
524 spells_prepared_index,
525 ..
526 } => {
527 *pending_preparation = true;
528 spells_prepared_index.clear();
529 }
530 KnowledgeAlreadyPrepared { usable_slots, .. } => {
531 if let Ok(Some(spellcasting_slots)) =
532 get_spellcasting_slots(index.as_str(), self.1.level).await
533 {
534 *usable_slots = spellcasting_slots.into();
535 }
536 }
537 }
538 }
539 }
540
541 pub async fn get_spellcasting_ability_index(&self) -> Result<String, ApiError> {
542 let op = SpellcastingAbilityQuery::build(SpellcastingAbilityQueryVariables {
543 index: Some(self.index().to_string()),
544 });
545
546 let ability_index = Client::new()
547 .post(GRAPHQL_API_URL.as_str())
548 .run_graphql(op)
549 .await?
550 .data
551 .ok_or(ApiError::Schema)?
552 .class
553 .ok_or(ApiError::Schema)?
554 .spellcasting
555 .ok_or(ApiError::Schema)?
556 .spellcasting_ability
557 .index;
558
559 Ok(ability_index)
560 }
561
562 pub async fn get_spellcasting_slots(&self) -> Result<Option<LevelSpellcasting>, ApiError> {
563 get_spellcasting_slots(self.index(), self.1.level).await
564 }
565
566 pub async fn set_level(
567 &mut self,
568 new_level: u8,
569 ) -> Result<Vec<ChoosableCustomLevelFeature>, ApiError> {
570 let op = LevelFeaturesQuery::build(LevelFeaturesQueryVariables {
571 class: Some(StringFilter(self.index().to_string())),
572 level: Some(LevelFilter {
573 gte: Some(self.1.level),
574 lte: Some(new_level),
575 }),
576 });
577
578 let features = Client::new()
579 .post(GRAPHQL_API_URL.as_str())
580 .run_graphql(op)
581 .await?
582 .data
583 .ok_or(ApiError::Schema)?
584 .features
585 .ok_or(ApiError::Schema)?;
586
587 let mut pending_features = vec![];
588
589 features
590 .iter()
591 .filter_map(|feature| CustomLevelFeatureType::identify(feature.index.clone()))
592 .for_each(|feature| match feature {
593 CustomLevelFeatureType::Passive => {}
594 CustomLevelFeatureType::Choosable(feature) => {
595 pending_features.push(feature);
596 }
597 CustomLevelFeatureType::Sheet(feature) => match feature {
598 SheetLevelFeatureType::PrimalChampion => {
599 self.1.abilities_modifiers.strength.score += 4;
600 self.1.abilities_modifiers.dexterity.score += 4;
601 }
602 },
603 Ignored => {}
604 });
605
606 self.1.level = new_level;
607
608 Ok(pending_features)
609 }
610
611 pub async fn get_levels_features(
612 &self,
613 from_level: Option<u8>,
614 passive: bool,
615 ) -> Result<Vec<String>, ApiError> {
616 let op = LevelFeaturesQuery::build(LevelFeaturesQueryVariables {
617 class: Some(StringFilter(self.index().to_string())),
618 level: Some(LevelFilter {
619 gte: Some(from_level.unwrap_or(0)),
620 lte: Some(self.1.level),
621 }),
622 });
623
624 let features = Client::new()
625 .post(GRAPHQL_API_URL.as_str())
626 .run_graphql(op)
627 .await?
628 .data
629 .ok_or(ApiError::Schema)?
630 .features
631 .ok_or(ApiError::Schema)?;
632
633 let mut features: Vec<String> = features
635 .into_iter()
636 .filter(
637 |feature| match CustomLevelFeatureType::identify(feature.index.clone()) {
638 None => true,
639 Some(custom_type) => match custom_type {
640 CustomLevelFeatureType::Passive => passive,
641 _ => false,
642 },
643 },
644 )
645 .map(|feature| feature.index)
646 .collect();
647
648 let features: Vec<String> = {
649 lazy_static! {
650 static ref CR_REGEX: regex::Regex =
651 regex::Regex::new(r"destroy-undead-cr-([0-9]+(?:-[0-9]+)?)\-or-below").unwrap();
652 }
653
654 let mut found = false;
655
656 features
657 .iter_mut()
658 .rev()
659 .filter(|feature| {
660 if CR_REGEX.is_match(feature) {
661 if found {
662 false
663 } else {
664 found = true;
665 true
666 }
667 } else {
668 true
669 }
670 })
671 .map(|feature| feature.clone())
672 .collect()
673 };
674
675 lazy_static! {
676 static ref DICE_REGEX: regex::Regex = regex::Regex::new(r"^(.+)-d(\d+)$").unwrap();
677 }
678
679 let mut grouped_features: HashMap<String, u32> = HashMap::new();
680 for feature in &features {
681 if let Some(caps) = DICE_REGEX.captures(feature) {
682 if caps.len() == 3 {
683 let prefix = caps.get(1).unwrap().as_str().to_string();
684 let dice_value = caps.get(2).unwrap().as_str().parse::<u32>().unwrap();
685
686 let current_max = grouped_features.entry(prefix).or_insert(0);
687 if dice_value > *current_max {
688 *current_max = dice_value;
689 }
690 }
691 }
692 }
693
694 let features = features
695 .into_iter()
696 .filter(|feature| {
697 if let Some(caps) = DICE_REGEX.captures(feature) {
698 let prefix = caps.get(1).unwrap().as_str();
699 let dice_value = caps
700 .get(2)
701 .unwrap()
702 .as_str()
703 .parse::<u32>()
704 .expect("Parsing dice value");
705
706 if let Some(&max_dice) = grouped_features.get(prefix) {
707 return dice_value == max_dice;
708 }
709 }
710 true
711 })
712 .collect();
713
714 Ok(features)
715 }
716
717 pub fn apply_option(&mut self, option: ChoosableCustomLevelFeatureOption) {
718 use ChoosableCustomLevelFeatureOption::*;
719
720 match option {
721 StrengthPlusOne | DexterityPlusOne | ConstitutionPlusOne | IntelligencePlusOne
722 | WisdomPlusOne | CharismaPlusOne => self.increase_score(option),
723 BardProficiencyStrength
724 | BardProficiencyDexterity
725 | BardProficiencyConstitution
726 | BardProficiencyIntelligence
727 | BardProficiencyWisdom
728 | BardProficiencyCharisma => self.set_proficiency(option),
729 PactOfTheChain | PactOfTheBlade | PactOfTheTome => {
730 println!("Pact of the Chain, Blade or Tome not yet implemented");
731 }
732 FighterFightingStyleArchery
733 | FighterFightingStyleDefense
734 | FighterFightingStyleDueling
735 | FighterFightingStyleGreatWeaponFighting
736 | FighterFightingStyleProtection
737 | FighterFightingStyleTwoWeaponFighting
738 | RangerFightingStyleArchery
739 | RangerFightingStyleDefense
740 | RangerFightingStyleDueling
741 | RangerFightingStyleTwoWeaponFighting
742 | FightingStyleDefense
743 | FightingStyleDueling
744 | FightingStyleGreatWeaponFighting
745 | FightingStyleProtection => {
746 if self.1.fighting_style.is_none() {
747 self.1
748 .fighting_style
749 .replace(option.as_index_str().to_string());
750 } else {
751 self.1
752 .additional_fighting_style
753 .replace(option.as_index_str().to_string());
754 }
755 }
756 }
757 }
758
759 fn increase_score(&mut self, option: ChoosableCustomLevelFeatureOption) {
760 match option {
761 ChoosableCustomLevelFeatureOption::StrengthPlusOne => {
762 self.1.abilities_modifiers.strength.score += 1;
763 }
764 ChoosableCustomLevelFeatureOption::DexterityPlusOne => {
765 self.1.abilities_modifiers.dexterity.score += 1;
766 }
767 ChoosableCustomLevelFeatureOption::ConstitutionPlusOne => {
768 self.1.abilities_modifiers.constitution.score += 1;
769 }
770 ChoosableCustomLevelFeatureOption::IntelligencePlusOne => {
771 self.1.abilities_modifiers.intelligence.score += 1;
772 }
773 ChoosableCustomLevelFeatureOption::WisdomPlusOne => {
774 self.1.abilities_modifiers.wisdom.score += 1;
775 }
776 ChoosableCustomLevelFeatureOption::CharismaPlusOne => {
777 self.1.abilities_modifiers.charisma.score += 1;
778 }
779 _ => {}
780 }
781 }
782
783 fn set_proficiency(&mut self, option: ChoosableCustomLevelFeatureOption) {
784 match option {
785 ChoosableCustomLevelFeatureOption::BardProficiencyStrength => {
786 self.1.abilities_modifiers.strength.proficiency = true;
787 }
788 ChoosableCustomLevelFeatureOption::BardProficiencyDexterity => {
789 self.1.abilities_modifiers.dexterity.proficiency = true;
790 }
791 ChoosableCustomLevelFeatureOption::BardProficiencyConstitution => {
792 self.1.abilities_modifiers.constitution.proficiency = true;
793 }
794 ChoosableCustomLevelFeatureOption::BardProficiencyIntelligence => {
795 self.1.abilities_modifiers.intelligence.proficiency = true;
796 }
797 ChoosableCustomLevelFeatureOption::BardProficiencyWisdom => {
798 self.1.abilities_modifiers.wisdom.proficiency = true;
799 }
800 ChoosableCustomLevelFeatureOption::BardProficiencyCharisma => {
801 self.1.abilities_modifiers.charisma.proficiency = true;
802 }
803 _ => {}
804 }
805 }
806}
807
808pub async fn get_spellcasting_slots(
809 index: &str,
810 level: u8,
811) -> Result<Option<LevelSpellcasting>, ApiError> {
812 let op = SpellcastingQuery::build(SpellcastingQueryVariables {
813 index: Some(format!("{}-{}", index, level)),
814 });
815
816 let spellcasting_slots = Client::new()
817 .post(GRAPHQL_API_URL.as_str())
818 .run_graphql(op)
819 .await?
820 .data
821 .ok_or(ApiError::Schema)?
822 .level
823 .ok_or(ApiError::Schema)?
824 .spellcasting;
825
826 Ok(spellcasting_slots)
827}