dnd_character/api/
classes.rs

1use super::shared::schema;
2use crate::GRAPHQL_API_URL;
3use crate::api::classes::CustomLevelFeatureType::Ignored;
4use crate::api::shared::ApiError;
5use crate::classes::{Class, Classes, UsableSlots};
6use cynic::http::ReqwestExt;
7use cynic::{QueryBuilder, impl_scalar};
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            cantrip_slots: self.cantrips_known.unwrap_or(0) as u8,
92            level_1: self.spell_slots_level_1.unwrap_or(0) as u8,
93            level_2: self.spell_slots_level_2.unwrap_or(0) as u8,
94            level_3: self.spell_slots_level_3.unwrap_or(0) as u8,
95            level_4: self.spell_slots_level_4.unwrap_or(0) as u8,
96            level_5: self.spell_slots_level_5.unwrap_or(0) as u8,
97            level_6: self.spell_slots_level_6.unwrap_or(0) as u8,
98            level_7: self.spell_slots_level_7.unwrap_or(0) as u8,
99            level_8: self.spell_slots_level_8.unwrap_or(0) as u8,
100            level_9: self.spell_slots_level_9.unwrap_or(0) as u8,
101        }
102    }
103}
104
105#[derive(cynic::QueryVariables, Debug)]
106pub struct LevelFeaturesQueryVariables {
107    pub class: Option<StringFilter>,
108    pub level: Option<LevelFilter>,
109}
110
111#[derive(serde::Serialize, Debug)]
112pub struct LevelFilter {
113    pub gt: Option<u8>,
114    pub gte: Option<u8>,
115    pub lte: Option<u8>,
116}
117
118impl_scalar!(LevelFilter, schema::IntFilter);
119
120#[derive(cynic::QueryFragment, Debug)]
121#[cynic(graphql_type = "Query", variables = "LevelFeaturesQueryVariables")]
122pub struct LevelFeaturesQuery {
123    #[arguments(class: $ class, level: $level )]
124    pub features: Option<Vec<Feature>>,
125}
126
127#[derive(cynic::QueryFragment, Debug)]
128pub struct Feature {
129    pub index: String,
130}
131
132#[derive(cynic::Scalar, Debug, Clone)]
133pub struct StringFilter(pub String);
134
135#[derive(Clone)]
136#[cfg_attr(feature = "serde", derive(serde::Serialize))]
137#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
138pub enum ChoosableCustomLevelFeature {
139    /// Ask the user to spend 2 points in any ability score
140    AbilityScoreImprovement,
141    /// https://dnd5eapi.rpgmaster.ai/api/2014/features/hunters-prey
142    HuntersPrey,
143    /// https://dnd5eapi.rpgmaster.ai/api/2014/features/defensive-tactics
144    DefensiveTactics,
145    /// https://www.dnd5eapi.co/api/features/pact-boon
146    WarlockPact,
147    /// https://www.dnd5eapi.co/api/features/additional-fighting-style
148    AdditionalFighterFightingStyle,
149    /// https://www.dnd5eapi.co/api/features/fighter-fighting-style
150    FighterFightingStyle,
151    /// https://www.dnd5eapi.co/api/features/ranger-fighting-style
152    RangerFightingStyle,
153    /// Used for
154    /// https://www.dnd5eapi.co/api/features/bard-expertise-1
155    /// https://www.dnd5eapi.co/api/features/bard-expertise-2
156    /// https://www.dnd5eapi.co/api/features/rogue-expertise-1
157    /// https://www.dnd5eapi.co/api/features/rogue-expertise-2
158    MultiplyTwoSkillProficiency,
159    /// https://www.dnd5eapi.co/api/features/magical-secrets-1
160    /// https://www.dnd5eapi.co/api/features/magical-secrets-2
161    /// https://www.dnd5eapi.co/api/features/magical-secrets-3
162    ChooseTwoSpellForAnyClass,
163    /// https://www.dnd5eapi.co/api/features/mystic-arcanum-6th-level
164    /// https://www.dnd5eapi.co/api/features/mystic-arcanum-7th-level
165    /// https://www.dnd5eapi.co/api/features/mystic-arcanum-8th-level
166    /// https://www.dnd5eapi.co/api/features/mystic-arcanum-9th-level
167    ChooseOne6thLevelSpellFromWarlockList,
168    /// https://www.dnd5eapi.co/api/features/paladin-fighting-style
169    PaladinFightingStyle,
170    /// https://dnd5eapi.rpgmaster.ai/api/2014/features/multiattack
171    Multiattack,
172    /// https://dnd5eapi.rpgmaster.ai/api/2014/features/superior-hunters-defense
173    SuperiorHuntersDefense,
174    /// https://dnd5eapi.rpgmaster.ai/api/2014/features/favored-enemy-1-type
175    /// https://dnd5eapi.rpgmaster.ai/api/2014/features/favored-enemy-2-types
176    /// https://dnd5eapi.rpgmaster.ai/api/2014/features/favored-enemy-3-enemies
177    RangerFavoredEnemyType,
178    /// https://dnd5eapi.rpgmaster.ai/api/2014/features/natural-explorer-1-terrain-type
179    /// https://dnd5eapi.rpgmaster.ai/api/2014/features/natural-explorer-2-terrain-types
180    /// https://dnd5eapi.rpgmaster.ai/api/2014/features/natural-explorer-3-terrain-types
181    RangerTerrainType,
182    /// https://dnd5eapi.rpgmaster.ai/api/2014/features/metamagic-1
183    /// https://dnd5eapi.rpgmaster.ai/api/2014/features/metamagic-2
184    /// https://dnd5eapi.rpgmaster.ai/api/2014/features/metamagic-3
185    Metamagic,
186    /// https://dnd5eapi.rpgmaster.ai/api/2014/features/eldritch-invocations
187    EldritchInvocations,
188    /// https://dnd5eapi.rpgmaster.ai/api/2014/features/dragon-ancestor
189    DragonAncestor,
190}
191
192#[derive(Clone, Debug)]
193#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
194#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
195pub enum ChoosableCustomLevelFeatureOption {
196    StrengthPlusOne,
197    DexterityPlusOne,
198    ConstitutionPlusOne,
199    IntelligencePlusOne,
200    WisdomPlusOne,
201    CharismaPlusOne,
202
203    PactOfTheChain,
204    PactOfTheBlade,
205    PactOfTheTome,
206
207    FighterFightingStyleArchery,
208    FighterFightingStyleDefense,
209    FighterFightingStyleDueling,
210    FighterFightingStyleGreatWeaponFighting,
211    FighterFightingStyleProtection,
212    FighterFightingStyleTwoWeaponFighting,
213
214    RangerFightingStyleArchery,
215    RangerFightingStyleDefense,
216    RangerFightingStyleDueling,
217    RangerFightingStyleTwoWeaponFighting,
218
219    RangerTerrainTypeArctic,
220    RangerTerrainTypeCoast,
221    RangerTerrainTypeDesert,
222    RangerTerrainTypeForest,
223    RangerTerrainTypeGrassland,
224    RangerTerrainTypeMountain,
225    RangerTerrainTypeSwamp,
226
227    RangerFavoredEnemyTypeAberrations,
228    RangerFavoredEnemyTypeBeasts,
229    RangerFavoredEnemyTypeCelestials,
230    RangerFavoredEnemyTypeConstructs,
231    RangerFavoredEnemyTypeDragons,
232    RangerFavoredEnemyTypeElementals,
233    RangerFavoredEnemyTypeFey,
234    RangerFavoredEnemyTypeFiends,
235    RangerFavoredEnemyTypeGiants,
236    RangerFavoredEnemyTypeMonstrosities,
237    RangerFavoredEnemyTypeOozes,
238    RangerFavoredEnemyTypePlants,
239    RangerFavoredEnemyTypeUndead,
240    RangerFavoredEnemyTypeHumanoids,
241
242    FightingStyleDefense,
243    FightingStyleDueling,
244    FightingStyleGreatWeaponFighting,
245    FightingStyleProtection,
246
247    HuntersPreyGiantKiller,
248    HuntersPreyHordeBreaker,
249    HuntersPreyColossusSlayer,
250
251    DefensiveTacticsSteelWill,
252    DefensiveTacticsEscapeTheHorde,
253    DefensiveTacticsMultiattackDefense,
254
255    MultiattackVolley,
256    MultiattackWhirlwindAttack,
257
258    SuperiorHuntersDefenseEvasion,
259    SuperiorHuntersDefenseStandAgainstTheTide,
260    SuperiorHuntersDefenseUncannyDodge,
261
262    MetamagicCarefulSpell,
263    MetamagicDistantSpell,
264    MetamagicEmpoweredSpell,
265    MetamagicExtendedSpell,
266    MetamagicHeightenedSpell,
267    MetamagicQuickenedSpell,
268    MetamagicSubtleSpell,
269    MetamagicTwinnedSpell,
270
271    EldritchInvocationAgonizingBlast,
272    EldritchInvocationArmorOfShadows,
273    EldritchInvocationAscendantStep,
274    EldritchInvocationBeastSpeech,
275    EldritchInvocationBeguilingInfluence,
276    EldritchInvocationBewitchingWhispers,
277    EldritchInvocationBookOfAncientSecrets,
278    EldritchInvocationChainsOfCarceri,
279    EldritchInvocationDevilsSight,
280    EldritchInvocationDreadfulWord,
281    EldritchInvocationEldritchSight,
282    EldritchInvocationEldritchSpear,
283    EldritchInvocationEyesOfTheRuneKeeper,
284    EldritchInvocationFiendishVigor,
285    EldritchInvocationGazeOfTwoMinds,
286    EldritchInvocationLifedrinker,
287    EldritchInvocationMaskOfManyFaces,
288    EldritchInvocationMasterOfMyriadForms,
289    EldritchInvocationMinionsOfChaos,
290    EldritchInvocationMireTheMind,
291    EldritchInvocationMistyVisions,
292    EldritchInvocationOneWithShadows,
293    EldritchInvocationOtherworldlyLeap,
294    EldritchInvocationRepellingBlast,
295    EldritchInvocationSculptorOfFlesh,
296    EldritchInvocationSignOfIllOmen,
297    EldritchInvocationThiefOfFiveFates,
298    EldritchInvocationThirstingBlade,
299    EldritchInvocationVisionsOfDistantRealms,
300    EldritchInvocationVoiceOfTheChainMaster,
301    EldritchInvocationWhispersOfTheGrave,
302    EldritchInvocationWitchSight,
303
304    #[serde(rename = "dragon-ancestor-black---acid-damage")]
305    DragonAncestorBlackAcidDamage,
306    #[serde(rename = "dragon-ancestor-blue---lightning-damage")]
307    DragonAncestorBlueLightningDamage,
308    #[serde(rename = "dragon-ancestor-brass---fire-damage")]
309    DragonAncestorBrassFireDamage,
310    #[serde(rename = "dragon-ancestor-bronze---lightning-damage")]
311    DragonAncestorBronzeLightningDamage,
312    #[serde(rename = "dragon-ancestor-copper---acid-damage")]
313    DragonAncestorCopperAcidDamage,
314    #[serde(rename = "dragon-ancestor-gold---fire-damage")]
315    DragonAncestorGoldFireDamage,
316    #[serde(rename = "dragon-ancestor-green---poison-damage")]
317    DragonAncestorGreenPoisonDamage,
318    #[serde(rename = "dragon-ancestor-red---fire-damage")]
319    DragonAncestorRedFireDamage,
320    #[serde(rename = "dragon-ancestor-silver---cold-damage")]
321    DragonAncestorSilverColdDamage,
322    #[serde(rename = "dragon-ancestor-white---cold-damage")]
323    DragonAncestorWhiteColdDamage,
324}
325
326impl ChoosableCustomLevelFeatureOption {
327    #[cfg(feature = "serde")]
328    pub fn as_index_str(&self) -> &str {
329        serde_variant::to_variant_name(self).unwrap()
330    }
331
332    #[cfg(feature = "serde")]
333    pub fn from_index_str(index: &str) -> Option<ChoosableCustomLevelFeatureOption> {
334        #[derive(serde::Deserialize)]
335        struct Helper {
336            value: ChoosableCustomLevelFeatureOption,
337        }
338
339        let json = json!({
340            "value": index
341        });
342
343        serde_json::from_value::<Helper>(json)
344            .map(|helper| helper.value)
345            .ok()
346    }
347}
348
349impl ChoosableCustomLevelFeature {
350    #[cfg(feature = "serde")]
351    pub fn as_index_str(&self) -> &str {
352        serde_variant::to_variant_name(self).unwrap()
353    }
354
355    pub fn to_options(&self) -> Vec<Vec<ChoosableCustomLevelFeatureOption>> {
356        use ChoosableCustomLevelFeatureOption::*;
357
358        match self {
359            ChoosableCustomLevelFeature::AbilityScoreImprovement => {
360                let ability_names = vec![
361                    StrengthPlusOne,
362                    DexterityPlusOne,
363                    ConstitutionPlusOne,
364                    IntelligencePlusOne,
365                    WisdomPlusOne,
366                    CharismaPlusOne,
367                ];
368
369                vec![ability_names.clone(), ability_names]
370            }
371            ChoosableCustomLevelFeature::WarlockPact => {
372                vec![vec![PactOfTheChain, PactOfTheBlade, PactOfTheTome]]
373            }
374            ChoosableCustomLevelFeature::AdditionalFighterFightingStyle
375            | ChoosableCustomLevelFeature::FighterFightingStyle => {
376                vec![vec![
377                    FighterFightingStyleArchery,
378                    FighterFightingStyleDefense,
379                    FighterFightingStyleDueling,
380                    FighterFightingStyleGreatWeaponFighting,
381                    FighterFightingStyleProtection,
382                    FighterFightingStyleTwoWeaponFighting,
383                ]]
384            }
385            ChoosableCustomLevelFeature::RangerFightingStyle => {
386                vec![vec![
387                    RangerFightingStyleArchery,
388                    RangerFightingStyleDefense,
389                    RangerFightingStyleDueling,
390                    RangerFightingStyleTwoWeaponFighting,
391                ]]
392            }
393            ChoosableCustomLevelFeature::MultiplyTwoSkillProficiency => {
394                // TODO: Implement this
395                vec![vec![]]
396            }
397            ChoosableCustomLevelFeature::ChooseTwoSpellForAnyClass => {
398                // TODO: Implement this
399                vec![vec![]]
400            }
401            ChoosableCustomLevelFeature::ChooseOne6thLevelSpellFromWarlockList => {
402                // TODO: Implement this when other warlock features are implemented
403                vec![vec![]]
404            }
405            ChoosableCustomLevelFeature::PaladinFightingStyle => {
406                vec![vec![
407                    FightingStyleDefense,
408                    FightingStyleDueling,
409                    FightingStyleGreatWeaponFighting,
410                    FightingStyleProtection,
411                ]]
412            }
413            ChoosableCustomLevelFeature::HuntersPrey => {
414                vec![vec![
415                    HuntersPreyGiantKiller,
416                    HuntersPreyHordeBreaker,
417                    HuntersPreyColossusSlayer,
418                ]]
419            }
420            ChoosableCustomLevelFeature::DefensiveTactics => {
421                vec![vec![
422                    DefensiveTacticsSteelWill,
423                    DefensiveTacticsEscapeTheHorde,
424                    DefensiveTacticsMultiattackDefense,
425                ]]
426            }
427            ChoosableCustomLevelFeature::Multiattack => {
428                vec![vec![MultiattackWhirlwindAttack, MultiattackVolley]]
429            }
430            ChoosableCustomLevelFeature::SuperiorHuntersDefense => {
431                vec![vec![
432                    SuperiorHuntersDefenseEvasion,
433                    SuperiorHuntersDefenseStandAgainstTheTide,
434                    SuperiorHuntersDefenseUncannyDodge,
435                ]]
436            }
437            ChoosableCustomLevelFeature::RangerFavoredEnemyType => {
438                vec![vec![
439                    RangerFavoredEnemyTypeAberrations,
440                    RangerFavoredEnemyTypeBeasts,
441                    RangerFavoredEnemyTypeCelestials,
442                    RangerFavoredEnemyTypeConstructs,
443                    RangerFavoredEnemyTypeDragons,
444                    RangerFavoredEnemyTypeElementals,
445                    RangerFavoredEnemyTypeFey,
446                    RangerFavoredEnemyTypeFiends,
447                    RangerFavoredEnemyTypeGiants,
448                    RangerFavoredEnemyTypeMonstrosities,
449                    RangerFavoredEnemyTypeOozes,
450                    RangerFavoredEnemyTypePlants,
451                    RangerFavoredEnemyTypeUndead,
452                    // RangerFavoredEnemyTypeHumanoids,
453                ]]
454            }
455            ChoosableCustomLevelFeature::RangerTerrainType => {
456                vec![vec![
457                    RangerTerrainTypeArctic,
458                    RangerTerrainTypeCoast,
459                    RangerTerrainTypeDesert,
460                    RangerTerrainTypeForest,
461                    RangerTerrainTypeGrassland,
462                    RangerTerrainTypeMountain,
463                    RangerTerrainTypeSwamp,
464                ]]
465            }
466            ChoosableCustomLevelFeature::Metamagic => {
467                let all_metamagics = vec![
468                    MetamagicCarefulSpell,
469                    MetamagicDistantSpell,
470                    MetamagicEmpoweredSpell,
471                    MetamagicExtendedSpell,
472                    MetamagicHeightenedSpell,
473                    MetamagicQuickenedSpell,
474                    MetamagicSubtleSpell,
475                    MetamagicTwinnedSpell,
476                ];
477
478                vec![all_metamagics.clone(), all_metamagics]
479            }
480            ChoosableCustomLevelFeature::EldritchInvocations => {
481                let all_eldritch_invocations = vec![
482                    EldritchInvocationAgonizingBlast,
483                    EldritchInvocationArmorOfShadows,
484                    EldritchInvocationAscendantStep,
485                    EldritchInvocationBeastSpeech,
486                    EldritchInvocationBeguilingInfluence,
487                    EldritchInvocationBewitchingWhispers,
488                    EldritchInvocationBookOfAncientSecrets,
489                    EldritchInvocationChainsOfCarceri,
490                    EldritchInvocationDevilsSight,
491                    EldritchInvocationDreadfulWord,
492                    EldritchInvocationEldritchSight,
493                    EldritchInvocationEldritchSpear,
494                    EldritchInvocationEyesOfTheRuneKeeper,
495                    EldritchInvocationFiendishVigor,
496                    EldritchInvocationGazeOfTwoMinds,
497                    EldritchInvocationLifedrinker,
498                    EldritchInvocationMaskOfManyFaces,
499                    EldritchInvocationMasterOfMyriadForms,
500                    EldritchInvocationMinionsOfChaos,
501                    EldritchInvocationMireTheMind,
502                    EldritchInvocationMistyVisions,
503                    EldritchInvocationOneWithShadows,
504                    EldritchInvocationOtherworldlyLeap,
505                    EldritchInvocationRepellingBlast,
506                    EldritchInvocationSculptorOfFlesh,
507                    EldritchInvocationSignOfIllOmen,
508                    EldritchInvocationThiefOfFiveFates,
509                    EldritchInvocationThirstingBlade,
510                    EldritchInvocationVisionsOfDistantRealms,
511                    EldritchInvocationVoiceOfTheChainMaster,
512                    EldritchInvocationWhispersOfTheGrave,
513                    EldritchInvocationWitchSight,
514                ];
515
516                vec![all_eldritch_invocations.clone(), all_eldritch_invocations]
517            }
518            ChoosableCustomLevelFeature::DragonAncestor => {
519                vec![vec![
520                    DragonAncestorBlackAcidDamage,
521                    DragonAncestorBlueLightningDamage,
522                    DragonAncestorBrassFireDamage,
523                    DragonAncestorBronzeLightningDamage,
524                    DragonAncestorCopperAcidDamage,
525                    DragonAncestorGoldFireDamage,
526                    DragonAncestorGreenPoisonDamage,
527                    DragonAncestorRedFireDamage,
528                    DragonAncestorSilverColdDamage,
529                    DragonAncestorWhiteColdDamage,
530                ]]
531            }
532        }
533    }
534}
535
536pub enum SheetLevelFeatureType {
537    /// https://www.dnd5eapi.co/api/features/primal-champion
538    PrimalChampion,
539}
540
541pub enum CustomLevelFeatureType {
542    Choosable(ChoosableCustomLevelFeature),
543    Sheet(SheetLevelFeatureType),
544    Passive,
545    Ignored,
546}
547
548impl CustomLevelFeatureType {
549    pub fn identify(index: String) -> Option<CustomLevelFeatureType> {
550        use ChoosableCustomLevelFeature::*;
551        use CustomLevelFeatureType::*;
552        use SheetLevelFeatureType::*;
553        match index.as_str() {
554            // Ignore all subclass choices since we have only one subclass per class
555            "bard-college"
556            | "divine-domain"
557            | "monastic-tradition"
558            | "sacred-oath"
559            | "ranger-archetype"
560            | "sorcerous-origin"
561            | "druid-circle"
562            | "primal-path"
563            | "martial-archetype"
564            | "roguish-archetype"
565            | "otherworldly-patron" => Some(Ignored),
566            "pact-boon" => Some(Choosable(WarlockPact)),
567            "additional-fighting-style" => Some(Choosable(AdditionalFighterFightingStyle)),
568            "fighter-fighting-style" => Some(Choosable(FighterFightingStyle)),
569            "bonus-proficiency" => Some(Passive),
570            // TODO: Ignored until skill implementation
571            "bonus-proficiencies" => Some(Ignored),
572            "additional-magical-secrets" | "bonus-cantrip" => Some(Ignored),
573            "channel-divinity-1-rest" | "channel-divinity-2-rest" | "channel-divinity-3-rest" => {
574                Some(Ignored)
575            }
576            //"magical-secrets-1" | "magical-secrets-2" | "magical-secrets-3" => Some(Choosable(ChooseTwoSpellForAnyClass)), TODO: Implement this
577            "magical-secrets-1" | "magical-secrets-2" | "magical-secrets-3" => Some(Ignored),
578            "mystic-arcanum-6th-level"
579            | "mystic-arcanum-7th-level"
580            | "mystic-arcanum-8th-level"
581            | "mystic-arcanum-9th-level" => Some(Choosable(ChooseOne6thLevelSpellFromWarlockList)),
582            "paladin-fighting-style" => Some(Choosable(PaladinFightingStyle)),
583            "primal-champion" => Some(Sheet(PrimalChampion)),
584            // TODO: Implement https://www.dnd5eapi.co/api/features/diamond-soul
585            "diamond-soul" => Some(Passive),
586            "arcane-recovery"
587            | "arcane-tradition"
588            | "archdruid"
589            | "aura-improvements"
590            | "aura-of-courage"
591            | "aura-of-devotion"
592            | "aura-of-protection"
593            | "blessed-healer"
594            | "blindsense"
595            | "brutal-critical-1-dice"
596            | "brutal-critical-2-dice"
597            | "brutal-critical-3-dice"
598            | "danger-sense"
599            | "dark-ones-blessing"
600            | "dark-ones-own-luck"
601            | "destroy-undead-cr-1-or-below"
602            | "destroy-undead-cr-2-or-below"
603            | "destroy-undead-cr-3-or-below"
604            | "destroy-undead-cr-4-or-below"
605            | "destroy-undead-cr-1-2-or-below"
606            | "disciple-of-life"
607            | "divine-health"
608            | "draconic-resilience"
609            | "font-of-magic"
610            | "druid-lands-stride"
611            | "druid-timeless-body"
612            | "druidic"
613            | "elusive"
614            | "empowered-evocation"
615            | "fast-movement"
616            | "feral-instinct"
617            | "feral-senses"
618            | "foe-slayer"
619            | "hurl-through-hell"
620            | "improved-critical"
621            | "improved-divine-smite"
622            | "indomitable-1-use"
623            | "indomitable-2-uses"
624            | "indomitable-3-uses"
625            | "indomitable-might"
626            | "ki-empowered-strikes"
627            | "jack-of-all-trades"
628            | "martial-arts"
629            | "monk-evasion"
630            | "monk-timeless-body"
631            | "purity-of-body"
632            | "purity-of-spirit"
633            | "natures-sanctuary"
634            | "natures-ward"
635            | "sculpt-spells"
636            | "ranger-lands-stride"
637            | "relentless-rage"
638            | "reliable-talent"
639            | "remarkable-athlete"
640            | "rogue-evasion"
641            | "superior-critical"
642            | "superior-inspiration"
643            | "supreme-healing"
644            | "supreme-sneak"
645            | "survivor"
646            | "thiefs-reflexes"
647            | "thieves-cant"
648            | "tongue-of-the-sun-and-moon"
649            | "tranquility"
650            | "unarmored-movement-1"
651            | "unarmored-movement-2"
652            | "use-magic-device"
653            | "ki"
654            | "monk-unarmored-defense"
655            | "perfect-self"
656            | "slippery-mind"
657            | "mindless-rage"
658            | "barbarian-unarmored-defense"
659            | "divine-intervention-improvement"
660            | "persistent-rage"
661            | "evocation-savant"
662            | "overchannel"
663            | "potent-cantrip"
664            | "font-of-inspiration"
665            | "second-story-work"
666            | "primeval-awareness"
667            | "beast-spells" => Some(Passive),
668            // ignored until implementation?
669            "oath-spells" => Some(Ignored),
670            "natural-recovery" => Some(Ignored),
671            "eldritch-invocations" => Some(Choosable(EldritchInvocations)),
672            x if x.starts_with("metamagic-") => {
673                if x.len() == 11 {
674                    Some(Choosable(Metamagic))
675                } else {
676                    Some(Ignored)
677                }
678            }
679            "hunters-prey" => Some(Choosable(HuntersPrey)),
680            x if x.starts_with("hunters-prey-") => Some(Ignored),
681            "superior-hunters-defense" => Some(Choosable(SuperiorHuntersDefense)),
682            x if x.starts_with("superior-hunters-defenese-") => Some(Ignored),
683            //x if x.starts_with("bard-expertise-")|| x.starts_with("rogue-expertise-") => Some(Choosable(MultiplyTwoSkillProficiency)),
684            x if x.starts_with("bard-expertise-") || x.starts_with("rogue-expertise-") => {
685                Some(Ignored)
686            } // TODO: Implement this
687            x if x.starts_with("spellcasting-") => Some(Ignored),
688            // Ignore all eldritch invocations since they are unlocked using invocation known table
689            x if x.starts_with("eldritch-invocation-") => Some(Ignored),
690            // Ignore all circle-spells until implementation
691            x if x.starts_with("circle-spells-") => Some(Ignored),
692            // Ignore all circle of the land until implementation
693            x if x.starts_with("circle-of-the-land") => Some(Ignored),
694            // Ignore all domain spells until implementation
695            x if x.starts_with("domain-spells-") => Some(Ignored),
696            // sorcery points not yet implemented
697            x if x.starts_with("flexible-casting-") => Some(Ignored),
698            "dragon-ancestor" => Some(Choosable(DragonAncestor)),
699            x if x.starts_with("dragon-ancestor-") => Some(Ignored),
700            "defensive-tactics" => Some(Choosable(DefensiveTactics)),
701            x if x.starts_with("defensive-tactics-") => Some(Ignored),
702            "multiattack" => Some(Choosable(Multiattack)),
703            x if x.starts_with("multiattack-") => Some(Ignored),
704            "ranger-fighting-style" => Some(Choosable(RangerFightingStyle)),
705            x if x.starts_with("favored-enemy-") => Some(Choosable(RangerFavoredEnemyType)),
706            x if x.starts_with("natural-explorer-") => Some(Choosable(RangerTerrainType)),
707            // Ignore pacts from patc-boon
708            x if x.starts_with("pact-of-the-") => Some(Ignored),
709            x if x.contains("ability-score-improvement") => {
710                Some(Choosable(AbilityScoreImprovement))
711            }
712            x if x.starts_with("fighting-style-") => Some(Ignored),
713            x if x.starts_with("fighter-fighting-style-") => Some(Ignored),
714            x if x.starts_with("ranger-fighting-style-") => Some(Ignored),
715            _ => None,
716        }
717    }
718}
719
720impl Classes {
721    pub(super) async fn new_day(&mut self) {
722        futures::stream::iter(self.0.values_mut())
723            .for_each_concurrent(None, |class| class.new_day())
724            .await;
725    }
726}
727
728impl Class {
729    pub(super) async fn new_day(&mut self) {
730        use crate::classes::ClassSpellCasting::*;
731
732        let index = self.index().to_string();
733
734        if let Some(spell_casting) = &mut self.1.spell_casting {
735            match spell_casting {
736                KnowledgePrepared {
737                    pending_preparation,
738                    spells_prepared_index,
739                    ..
740                }
741                | AlreadyKnowPrepared {
742                    pending_preparation,
743                    spells_prepared_index,
744                    ..
745                } => {
746                    *pending_preparation = true;
747                    spells_prepared_index.clear();
748                }
749                KnowledgeAlreadyPrepared { usable_slots, .. } => {
750                    if let Ok(Some(spellcasting_slots)) =
751                        get_spellcasting_slots(index.as_str(), self.1.level).await
752                    {
753                        *usable_slots = spellcasting_slots.into();
754                    }
755                }
756            }
757        }
758    }
759
760    pub async fn get_spellcasting_ability_index(&self) -> Result<String, ApiError> {
761        let op = SpellcastingAbilityQuery::build(SpellcastingAbilityQueryVariables {
762            index: Some(self.index().to_string()),
763        });
764
765        let ability_index = Client::new()
766            .post(GRAPHQL_API_URL.as_str())
767            .run_graphql(op)
768            .await?
769            .data
770            .ok_or(ApiError::Schema)?
771            .class
772            .ok_or(ApiError::Schema)?
773            .spellcasting
774            .ok_or(ApiError::Schema)?
775            .spellcasting_ability
776            .index;
777
778        Ok(ability_index)
779    }
780
781    pub async fn get_spellcasting_slots(&self) -> Result<Option<LevelSpellcasting>, ApiError> {
782        get_spellcasting_slots(self.index(), self.1.level).await
783    }
784
785    pub async fn set_level(
786        &mut self,
787        new_level: u8,
788    ) -> Result<Vec<ChoosableCustomLevelFeature>, ApiError> {
789        let op = LevelFeaturesQuery::build(LevelFeaturesQueryVariables {
790            class: Some(StringFilter(self.index().to_string())),
791            level: Some(LevelFilter {
792                gt: Some(self.1.level),
793                lte: Some(new_level),
794                gte: None,
795            }),
796        });
797
798        let features = Client::new()
799            .post(GRAPHQL_API_URL.as_str())
800            .run_graphql(op)
801            .await?
802            .data
803            .ok_or(ApiError::Schema)?
804            .features
805            .ok_or(ApiError::Schema)?;
806
807        let mut pending_features = vec![];
808
809        features
810            .iter()
811            .filter_map(|feature| CustomLevelFeatureType::identify(feature.index.clone()))
812            .for_each(|feature| match feature {
813                CustomLevelFeatureType::Passive => {}
814                CustomLevelFeatureType::Choosable(feature) => {
815                    pending_features.push(feature);
816                }
817                CustomLevelFeatureType::Sheet(feature) => match feature {
818                    SheetLevelFeatureType::PrimalChampion => {
819                        let mut abilities = self.1.abilities.lock().unwrap();
820                        abilities.strength.score += 4;
821                        abilities.constitution.score += 4;
822                    }
823                },
824                Ignored => {}
825            });
826
827        self.1.level = new_level;
828
829        Ok(pending_features)
830    }
831
832    pub async fn get_levels_features(
833        &self,
834        from_level: Option<u8>,
835        passive: bool,
836    ) -> Result<Vec<String>, ApiError> {
837        let op = LevelFeaturesQuery::build(LevelFeaturesQueryVariables {
838            class: Some(StringFilter(self.index().to_string())),
839            level: Some(LevelFilter {
840                gte: Some(from_level.unwrap_or(0)),
841                lte: Some(self.1.level),
842                gt: None,
843            }),
844        });
845
846        let features = Client::new()
847            .post(GRAPHQL_API_URL.as_str())
848            .run_graphql(op)
849            .await?
850            .data
851            .ok_or(ApiError::Schema)?
852            .features
853            .ok_or(ApiError::Schema)?;
854
855        // First convert features to String objects and filter out non-matching features
856        let features: Vec<String> = features
857            .into_iter()
858            .filter_map(
859                |feature| match CustomLevelFeatureType::identify(feature.index.clone()) {
860                    None => Some(feature.index),
861                    Some(custom_type) => match custom_type {
862                        CustomLevelFeatureType::Passive if passive => Some(feature.index),
863                        _ => None,
864                    },
865                },
866            )
867            .collect();
868
869        // Define all regexes at once
870        lazy_static! {
871            static ref CR_REGEX: regex::Regex =
872                regex::Regex::new(r"(.*)-cr-([0-9]+(?:-[0-9]+)?)-or-below.*").unwrap();
873            static ref DICE_REGEX: regex::Regex = regex::Regex::new(r"^(.+)-d(\d+)$").unwrap();
874            static ref DIE_DICE_REGEX: regex::Regex =
875                regex::Regex::new(r"^(.+)-(\d+)-(die|dice)$").unwrap();
876            static ref UNARMORED_MOVEMENT_REGEX: regex::Regex =
877                regex::Regex::new(r"^(unarmored-movement)-(\d+)$").unwrap();
878        }
879
880        // Track the highest values for each pattern type
881        let mut cr_features: HashMap<String, (f32, String)> = HashMap::new();
882        let mut dice_features: HashMap<String, u32> = HashMap::new();
883        let mut die_dice_features: HashMap<String, u32> = HashMap::new();
884        let mut unarmored_movement_features: HashMap<String, (u32, String)> = HashMap::new();
885
886        // First pass to collect all the pattern information
887        for feature in &features {
888            // Process CR pattern
889            if let Some(caps) = CR_REGEX.captures(feature) {
890                let prefix = caps.get(1).unwrap().as_str().to_string();
891                let cr_str = caps.get(2).unwrap().as_str();
892
893                // Parse CR value (handling fractions like "1-2" for 1/2)
894                let cr_value = if cr_str.contains('-') {
895                    let parts: Vec<&str> = cr_str.split('-').collect();
896                    if parts.len() == 2 {
897                        parts[0].parse::<f32>().unwrap_or(0.0)
898                            / parts[1].parse::<f32>().unwrap_or(1.0)
899                    } else {
900                        0.0
901                    }
902                } else {
903                    cr_str.parse::<f32>().unwrap_or(0.0)
904                };
905
906                // Update if this is higher CR for this prefix
907                if let Some((existing_cr, _)) = cr_features.get(&prefix) {
908                    if cr_value > *existing_cr {
909                        cr_features.insert(prefix, (cr_value, feature.clone()));
910                    }
911                } else {
912                    cr_features.insert(prefix, (cr_value, feature.clone()));
913                }
914                continue;
915            }
916
917            // Process dice-N pattern
918            if let Some(caps) = DICE_REGEX.captures(feature) {
919                let prefix = caps.get(1).unwrap().as_str().to_string();
920                let dice_value = caps.get(2).unwrap().as_str().parse::<u32>().unwrap_or(0);
921
922                let current_max = dice_features.entry(prefix).or_insert(0);
923                if dice_value > *current_max {
924                    *current_max = dice_value;
925                }
926                continue;
927            }
928
929            // Process N-die/dice pattern
930            if let Some(caps) = DIE_DICE_REGEX.captures(feature) {
931                let prefix = caps.get(1).unwrap().as_str().to_string();
932                let dice_value = caps.get(2).unwrap().as_str().parse::<u32>().unwrap_or(0);
933
934                let current_max = die_dice_features.entry(prefix).or_insert(0);
935                if dice_value > *current_max {
936                    *current_max = dice_value;
937                }
938            }
939
940            // Process unarmored-movement-N pattern
941            if let Some(caps) = UNARMORED_MOVEMENT_REGEX.captures(feature) {
942                let prefix = caps.get(1).unwrap().as_str().to_string();
943                let movement_value = caps.get(2).unwrap().as_str().parse::<u32>().unwrap_or(0);
944
945                // Update if this is a higher value for unarmored movement
946                if let Some((existing_value, _)) = unarmored_movement_features.get(&prefix) {
947                    if movement_value > *existing_value {
948                        unarmored_movement_features
949                            .insert(prefix, (movement_value, feature.clone()));
950                    }
951                } else {
952                    unarmored_movement_features.insert(prefix, (movement_value, feature.clone()));
953                }
954            }
955        }
956
957        // Second pass: Filter to keep only the highest value patterns
958        let mut filtered_features = Vec::new();
959        let mut has_improved_divine_smite = false;
960
961        // First check if improved-divine-smite exists
962        for feature in &features {
963            if feature == "improved-divine-smite" {
964                has_improved_divine_smite = true;
965                break;
966            }
967        }
968
969        for feature in features {
970            // Skip divine-smite if improved-divine-smite is present
971            if feature == "divine-smite" && has_improved_divine_smite {
972                continue;
973            }
974
975            // Handle CR pattern
976            if let Some(caps) = CR_REGEX.captures(&feature) {
977                let prefix = caps.get(1).unwrap().as_str().to_string();
978
979                if let Some((_, highest_feature)) = cr_features.get(&prefix) {
980                    if &feature == highest_feature {
981                        filtered_features.push(feature);
982                    }
983                }
984                continue;
985            }
986
987            // Handle dice pattern
988            if let Some(caps) = DICE_REGEX.captures(&feature) {
989                let prefix = caps.get(1).unwrap().as_str().to_string();
990                let dice_value = caps
991                    .get(2)
992                    .unwrap()
993                    .as_str()
994                    .parse::<u32>()
995                    .expect("Parsing dice value");
996
997                if let Some(&max_dice) = dice_features.get(&prefix) {
998                    if dice_value == max_dice {
999                        filtered_features.push(feature);
1000                    }
1001                }
1002                continue;
1003            }
1004
1005            // Handle die/dice pattern
1006            if let Some(caps) = DIE_DICE_REGEX.captures(&feature) {
1007                let prefix = caps.get(1).unwrap().as_str().to_string();
1008                let dice_value = caps
1009                    .get(2)
1010                    .unwrap()
1011                    .as_str()
1012                    .parse::<u32>()
1013                    .expect("Parsing die/dice value");
1014
1015                if let Some(&max_dice) = die_dice_features.get(&prefix) {
1016                    if dice_value == max_dice {
1017                        filtered_features.push(feature);
1018                    }
1019                }
1020                continue;
1021            }
1022
1023            // Handle unarmored-movement-N pattern
1024            if let Some(caps) = UNARMORED_MOVEMENT_REGEX.captures(&feature) {
1025                let prefix = caps.get(1).unwrap().as_str().to_string();
1026
1027                if let Some((_, highest_feature)) = unarmored_movement_features.get(&prefix) {
1028                    if &feature == highest_feature {
1029                        filtered_features.push(feature);
1030                    }
1031                }
1032                continue;
1033            }
1034
1035            // Regular feature, keep it
1036            filtered_features.push(feature);
1037        }
1038
1039        let mut features = filtered_features;
1040
1041        // Add the selected multiattack feature if it exists and we're not requesting passive features
1042        if !passive {
1043            if let Some(multiattack) = &self.1.multiattack {
1044                features.push(multiattack.clone());
1045            }
1046            if let Some(hunters_prey) = &self.1.hunters_prey {
1047                features.push(hunters_prey.clone());
1048            }
1049            if let Some(metamagic) = &self.1.sorcerer_metamagic {
1050                features.append(&mut metamagic.clone());
1051            }
1052            if let Some(eldritch_invocation) = &self.1.warlock_eldritch_invocation {
1053                features.append(&mut eldritch_invocation.clone());
1054            }
1055        }
1056
1057        Ok(features)
1058    }
1059
1060    pub fn apply_option(&mut self, option: ChoosableCustomLevelFeatureOption) {
1061        use ChoosableCustomLevelFeatureOption::*;
1062
1063        match option {
1064            StrengthPlusOne | DexterityPlusOne | ConstitutionPlusOne | IntelligencePlusOne
1065            | WisdomPlusOne | CharismaPlusOne => self.increase_score(option),
1066            PactOfTheChain | PactOfTheBlade | PactOfTheTome => {
1067                println!("Pact of the Chain, Blade or Tome not yet implemented");
1068            }
1069            HuntersPreyGiantKiller | HuntersPreyHordeBreaker | HuntersPreyColossusSlayer => {
1070                self.1
1071                    .hunters_prey
1072                    .replace(option.as_index_str().to_string());
1073            }
1074            DefensiveTacticsSteelWill
1075            | DefensiveTacticsEscapeTheHorde
1076            | DefensiveTacticsMultiattackDefense => {
1077                self.1
1078                    .defensive_tactics
1079                    .replace(option.as_index_str().to_string());
1080            }
1081            FighterFightingStyleArchery
1082            | FighterFightingStyleDefense
1083            | FighterFightingStyleDueling
1084            | FighterFightingStyleGreatWeaponFighting
1085            | FighterFightingStyleProtection
1086            | FighterFightingStyleTwoWeaponFighting
1087            | RangerFightingStyleArchery
1088            | RangerFightingStyleDefense
1089            | RangerFightingStyleDueling
1090            | RangerFightingStyleTwoWeaponFighting
1091            | FightingStyleDefense
1092            | FightingStyleDueling
1093            | FightingStyleGreatWeaponFighting
1094            | FightingStyleProtection => {
1095                if self.1.fighting_style.is_none() {
1096                    self.1
1097                        .fighting_style
1098                        .replace(option.as_index_str().to_string());
1099                } else {
1100                    self.1
1101                        .additional_fighting_style
1102                        .replace(option.as_index_str().to_string());
1103                }
1104            }
1105            MultiattackVolley | MultiattackWhirlwindAttack => {
1106                self.1
1107                    .multiattack
1108                    .replace(option.as_index_str().to_string());
1109            }
1110            SuperiorHuntersDefenseEvasion
1111            | SuperiorHuntersDefenseStandAgainstTheTide
1112            | SuperiorHuntersDefenseUncannyDodge => {
1113                self.1
1114                    .superior_hunters_defense
1115                    .replace(option.as_index_str().to_string());
1116            }
1117            RangerTerrainTypeArctic
1118            | RangerTerrainTypeCoast
1119            | RangerTerrainTypeDesert
1120            | RangerTerrainTypeForest
1121            | RangerTerrainTypeGrassland
1122            | RangerTerrainTypeMountain
1123            | RangerTerrainTypeSwamp => {
1124                self.1
1125                    .natural_explorer_terrain_type
1126                    .get_or_insert_with(Vec::new)
1127                    .push(option.as_index_str().to_string());
1128            }
1129            RangerFavoredEnemyTypeAberrations
1130            | RangerFavoredEnemyTypeBeasts
1131            | RangerFavoredEnemyTypeCelestials
1132            | RangerFavoredEnemyTypeConstructs
1133            | RangerFavoredEnemyTypeDragons
1134            | RangerFavoredEnemyTypeElementals
1135            | RangerFavoredEnemyTypeFey
1136            | RangerFavoredEnemyTypeFiends
1137            | RangerFavoredEnemyTypeGiants
1138            | RangerFavoredEnemyTypeMonstrosities
1139            | RangerFavoredEnemyTypeOozes
1140            | RangerFavoredEnemyTypePlants
1141            | RangerFavoredEnemyTypeUndead
1142            | RangerFavoredEnemyTypeHumanoids => {
1143                self.1
1144                    .ranger_favored_enemy_type
1145                    .get_or_insert_with(Vec::new)
1146                    .push(option.as_index_str().to_string());
1147            }
1148            MetamagicCarefulSpell
1149            | MetamagicDistantSpell
1150            | MetamagicEmpoweredSpell
1151            | MetamagicExtendedSpell
1152            | MetamagicHeightenedSpell
1153            | MetamagicQuickenedSpell
1154            | MetamagicSubtleSpell
1155            | MetamagicTwinnedSpell => {
1156                self.1
1157                    .sorcerer_metamagic
1158                    .get_or_insert_with(Vec::new)
1159                    .push(option.as_index_str().to_string());
1160            }
1161            EldritchInvocationAgonizingBlast
1162            | EldritchInvocationArmorOfShadows
1163            | EldritchInvocationAscendantStep
1164            | EldritchInvocationBeastSpeech
1165            | EldritchInvocationBeguilingInfluence
1166            | EldritchInvocationBewitchingWhispers
1167            | EldritchInvocationBookOfAncientSecrets
1168            | EldritchInvocationChainsOfCarceri
1169            | EldritchInvocationDevilsSight
1170            | EldritchInvocationDreadfulWord
1171            | EldritchInvocationEldritchSight
1172            | EldritchInvocationEldritchSpear
1173            | EldritchInvocationEyesOfTheRuneKeeper
1174            | EldritchInvocationFiendishVigor
1175            | EldritchInvocationGazeOfTwoMinds
1176            | EldritchInvocationLifedrinker
1177            | EldritchInvocationMaskOfManyFaces
1178            | EldritchInvocationMasterOfMyriadForms
1179            | EldritchInvocationMinionsOfChaos
1180            | EldritchInvocationMireTheMind
1181            | EldritchInvocationMistyVisions
1182            | EldritchInvocationOneWithShadows
1183            | EldritchInvocationOtherworldlyLeap
1184            | EldritchInvocationRepellingBlast
1185            | EldritchInvocationSculptorOfFlesh
1186            | EldritchInvocationSignOfIllOmen
1187            | EldritchInvocationThiefOfFiveFates
1188            | EldritchInvocationThirstingBlade
1189            | EldritchInvocationVisionsOfDistantRealms
1190            | EldritchInvocationVoiceOfTheChainMaster
1191            | EldritchInvocationWhispersOfTheGrave
1192            | EldritchInvocationWitchSight => {
1193                self.1
1194                    .warlock_eldritch_invocation
1195                    .get_or_insert_with(Vec::new)
1196                    .push(option.as_index_str().to_string());
1197            }
1198            DragonAncestorBlackAcidDamage
1199            | DragonAncestorBlueLightningDamage
1200            | DragonAncestorBrassFireDamage
1201            | DragonAncestorBronzeLightningDamage
1202            | DragonAncestorCopperAcidDamage
1203            | DragonAncestorGoldFireDamage
1204            | DragonAncestorGreenPoisonDamage
1205            | DragonAncestorRedFireDamage
1206            | DragonAncestorSilverColdDamage
1207            | DragonAncestorWhiteColdDamage => {
1208                self.1
1209                    .sorcerer_dragon_ancestor
1210                    .replace(option.as_index_str().to_string());
1211            }
1212        }
1213    }
1214
1215    fn increase_score(&mut self, option: ChoosableCustomLevelFeatureOption) {
1216        let mut abilities = self.1.abilities.lock().unwrap();
1217        match option {
1218            ChoosableCustomLevelFeatureOption::StrengthPlusOne => {
1219                abilities.strength.score += 1;
1220            }
1221            ChoosableCustomLevelFeatureOption::DexterityPlusOne => {
1222                abilities.dexterity.score += 1;
1223            }
1224            ChoosableCustomLevelFeatureOption::ConstitutionPlusOne => {
1225                abilities.constitution.score += 1;
1226            }
1227            ChoosableCustomLevelFeatureOption::IntelligencePlusOne => {
1228                abilities.intelligence.score += 1;
1229            }
1230            ChoosableCustomLevelFeatureOption::WisdomPlusOne => {
1231                abilities.wisdom.score += 1;
1232            }
1233            ChoosableCustomLevelFeatureOption::CharismaPlusOne => {
1234                abilities.charisma.score += 1;
1235            }
1236            _ => {}
1237        }
1238    }
1239}
1240
1241pub async fn get_spellcasting_slots(
1242    index: &str,
1243    level: u8,
1244) -> Result<Option<LevelSpellcasting>, ApiError> {
1245    let op = SpellcastingQuery::build(SpellcastingQueryVariables {
1246        index: Some(format!("{}-{}", index, level)),
1247    });
1248
1249    let spellcasting_slots = Client::new()
1250        .post(GRAPHQL_API_URL.as_str())
1251        .run_graphql(op)
1252        .await?
1253        .data
1254        .ok_or(ApiError::Schema)?
1255        .level
1256        .ok_or(ApiError::Schema)?
1257        .spellcasting;
1258
1259    Ok(spellcasting_slots)
1260}