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                        self.1.abilities_modifiers.strength.score += 4;
820                        self.1.abilities_modifiers.constitution.score += 4;
821                    }
822                },
823                Ignored => {}
824            });
825
826        self.1.level = new_level;
827
828        Ok(pending_features)
829    }
830
831    pub async fn get_levels_features(
832        &self,
833        from_level: Option<u8>,
834        passive: bool,
835    ) -> Result<Vec<String>, ApiError> {
836        let op = LevelFeaturesQuery::build(LevelFeaturesQueryVariables {
837            class: Some(StringFilter(self.index().to_string())),
838            level: Some(LevelFilter {
839                gte: Some(from_level.unwrap_or(0)),
840                lte: Some(self.1.level),
841                gt: None,
842            }),
843        });
844
845        let features = Client::new()
846            .post(GRAPHQL_API_URL.as_str())
847            .run_graphql(op)
848            .await?
849            .data
850            .ok_or(ApiError::Schema)?
851            .features
852            .ok_or(ApiError::Schema)?;
853
854        // First convert features to String objects and filter out non-matching features
855        let features: Vec<String> = features
856            .into_iter()
857            .filter_map(
858                |feature| match CustomLevelFeatureType::identify(feature.index.clone()) {
859                    None => Some(feature.index),
860                    Some(custom_type) => match custom_type {
861                        CustomLevelFeatureType::Passive if passive => Some(feature.index),
862                        _ => None,
863                    },
864                },
865            )
866            .collect();
867
868        // Define all regexes at once
869        lazy_static! {
870            static ref CR_REGEX: regex::Regex =
871                regex::Regex::new(r"(.*)-cr-([0-9]+(?:-[0-9]+)?)-or-below.*").unwrap();
872            static ref DICE_REGEX: regex::Regex = regex::Regex::new(r"^(.+)-d(\d+)$").unwrap();
873            static ref DIE_DICE_REGEX: regex::Regex =
874                regex::Regex::new(r"^(.+)-(\d+)-(die|dice)$").unwrap();
875            static ref UNARMORED_MOVEMENT_REGEX: regex::Regex =
876                regex::Regex::new(r"^(unarmored-movement)-(\d+)$").unwrap();
877        }
878
879        // Track the highest values for each pattern type
880        let mut cr_features: HashMap<String, (f32, String)> = HashMap::new();
881        let mut dice_features: HashMap<String, u32> = HashMap::new();
882        let mut die_dice_features: HashMap<String, u32> = HashMap::new();
883        let mut unarmored_movement_features: HashMap<String, (u32, String)> = HashMap::new();
884
885        // First pass to collect all the pattern information
886        for feature in &features {
887            // Process CR pattern
888            if let Some(caps) = CR_REGEX.captures(feature) {
889                let prefix = caps.get(1).unwrap().as_str().to_string();
890                let cr_str = caps.get(2).unwrap().as_str();
891
892                // Parse CR value (handling fractions like "1-2" for 1/2)
893                let cr_value = if cr_str.contains('-') {
894                    let parts: Vec<&str> = cr_str.split('-').collect();
895                    if parts.len() == 2 {
896                        parts[0].parse::<f32>().unwrap_or(0.0)
897                            / parts[1].parse::<f32>().unwrap_or(1.0)
898                    } else {
899                        0.0
900                    }
901                } else {
902                    cr_str.parse::<f32>().unwrap_or(0.0)
903                };
904
905                // Update if this is higher CR for this prefix
906                if let Some((existing_cr, _)) = cr_features.get(&prefix) {
907                    if cr_value > *existing_cr {
908                        cr_features.insert(prefix, (cr_value, feature.clone()));
909                    }
910                } else {
911                    cr_features.insert(prefix, (cr_value, feature.clone()));
912                }
913                continue;
914            }
915
916            // Process dice-N pattern
917            if let Some(caps) = DICE_REGEX.captures(feature) {
918                let prefix = caps.get(1).unwrap().as_str().to_string();
919                let dice_value = caps.get(2).unwrap().as_str().parse::<u32>().unwrap_or(0);
920
921                let current_max = dice_features.entry(prefix).or_insert(0);
922                if dice_value > *current_max {
923                    *current_max = dice_value;
924                }
925                continue;
926            }
927
928            // Process N-die/dice pattern
929            if let Some(caps) = DIE_DICE_REGEX.captures(feature) {
930                let prefix = caps.get(1).unwrap().as_str().to_string();
931                let dice_value = caps.get(2).unwrap().as_str().parse::<u32>().unwrap_or(0);
932
933                let current_max = die_dice_features.entry(prefix).or_insert(0);
934                if dice_value > *current_max {
935                    *current_max = dice_value;
936                }
937            }
938
939            // Process unarmored-movement-N pattern
940            if let Some(caps) = UNARMORED_MOVEMENT_REGEX.captures(feature) {
941                let prefix = caps.get(1).unwrap().as_str().to_string();
942                let movement_value = caps.get(2).unwrap().as_str().parse::<u32>().unwrap_or(0);
943
944                // Update if this is a higher value for unarmored movement
945                if let Some((existing_value, _)) = unarmored_movement_features.get(&prefix) {
946                    if movement_value > *existing_value {
947                        unarmored_movement_features
948                            .insert(prefix, (movement_value, feature.clone()));
949                    }
950                } else {
951                    unarmored_movement_features.insert(prefix, (movement_value, feature.clone()));
952                }
953            }
954        }
955
956        // Second pass: Filter to keep only the highest value patterns
957        let mut filtered_features = Vec::new();
958        let mut has_improved_divine_smite = false;
959
960        // First check if improved-divine-smite exists
961        for feature in &features {
962            if feature == "improved-divine-smite" {
963                has_improved_divine_smite = true;
964                break;
965            }
966        }
967
968        for feature in features {
969            // Skip divine-smite if improved-divine-smite is present
970            if feature == "divine-smite" && has_improved_divine_smite {
971                continue;
972            }
973
974            // Handle CR pattern
975            if let Some(caps) = CR_REGEX.captures(&feature) {
976                let prefix = caps.get(1).unwrap().as_str().to_string();
977
978                if let Some((_, highest_feature)) = cr_features.get(&prefix) {
979                    if &feature == highest_feature {
980                        filtered_features.push(feature);
981                    }
982                }
983                continue;
984            }
985
986            // Handle dice pattern
987            if let Some(caps) = DICE_REGEX.captures(&feature) {
988                let prefix = caps.get(1).unwrap().as_str().to_string();
989                let dice_value = caps
990                    .get(2)
991                    .unwrap()
992                    .as_str()
993                    .parse::<u32>()
994                    .expect("Parsing dice value");
995
996                if let Some(&max_dice) = dice_features.get(&prefix) {
997                    if dice_value == max_dice {
998                        filtered_features.push(feature);
999                    }
1000                }
1001                continue;
1002            }
1003
1004            // Handle die/dice pattern
1005            if let Some(caps) = DIE_DICE_REGEX.captures(&feature) {
1006                let prefix = caps.get(1).unwrap().as_str().to_string();
1007                let dice_value = caps
1008                    .get(2)
1009                    .unwrap()
1010                    .as_str()
1011                    .parse::<u32>()
1012                    .expect("Parsing die/dice value");
1013
1014                if let Some(&max_dice) = die_dice_features.get(&prefix) {
1015                    if dice_value == max_dice {
1016                        filtered_features.push(feature);
1017                    }
1018                }
1019                continue;
1020            }
1021
1022            // Handle unarmored-movement-N pattern
1023            if let Some(caps) = UNARMORED_MOVEMENT_REGEX.captures(&feature) {
1024                let prefix = caps.get(1).unwrap().as_str().to_string();
1025
1026                if let Some((_, highest_feature)) = unarmored_movement_features.get(&prefix) {
1027                    if &feature == highest_feature {
1028                        filtered_features.push(feature);
1029                    }
1030                }
1031                continue;
1032            }
1033
1034            // Regular feature, keep it
1035            filtered_features.push(feature);
1036        }
1037
1038        let mut features = filtered_features;
1039
1040        // Add the selected multiattack feature if it exists and we're not requesting passive features
1041        if !passive {
1042            if let Some(multiattack) = &self.1.multiattack {
1043                features.push(multiattack.clone());
1044            }
1045            if let Some(hunters_prey) = &self.1.hunters_prey {
1046                features.push(hunters_prey.clone());
1047            }
1048            if let Some(metamagic) = &self.1.sorcerer_metamagic {
1049                features.append(&mut metamagic.clone());
1050            }
1051            if let Some(eldritch_invocation) = &self.1.warlock_eldritch_invocation {
1052                features.append(&mut eldritch_invocation.clone());
1053            }
1054        }
1055
1056        Ok(features)
1057    }
1058
1059    pub fn apply_option(&mut self, option: ChoosableCustomLevelFeatureOption) {
1060        use ChoosableCustomLevelFeatureOption::*;
1061
1062        match option {
1063            StrengthPlusOne | DexterityPlusOne | ConstitutionPlusOne | IntelligencePlusOne
1064            | WisdomPlusOne | CharismaPlusOne => self.increase_score(option),
1065            PactOfTheChain | PactOfTheBlade | PactOfTheTome => {
1066                println!("Pact of the Chain, Blade or Tome not yet implemented");
1067            }
1068            HuntersPreyGiantKiller | HuntersPreyHordeBreaker | HuntersPreyColossusSlayer => {
1069                self.1
1070                    .hunters_prey
1071                    .replace(option.as_index_str().to_string());
1072            }
1073            DefensiveTacticsSteelWill
1074            | DefensiveTacticsEscapeTheHorde
1075            | DefensiveTacticsMultiattackDefense => {
1076                self.1
1077                    .defensive_tactics
1078                    .replace(option.as_index_str().to_string());
1079            }
1080            FighterFightingStyleArchery
1081            | FighterFightingStyleDefense
1082            | FighterFightingStyleDueling
1083            | FighterFightingStyleGreatWeaponFighting
1084            | FighterFightingStyleProtection
1085            | FighterFightingStyleTwoWeaponFighting
1086            | RangerFightingStyleArchery
1087            | RangerFightingStyleDefense
1088            | RangerFightingStyleDueling
1089            | RangerFightingStyleTwoWeaponFighting
1090            | FightingStyleDefense
1091            | FightingStyleDueling
1092            | FightingStyleGreatWeaponFighting
1093            | FightingStyleProtection => {
1094                if self.1.fighting_style.is_none() {
1095                    self.1
1096                        .fighting_style
1097                        .replace(option.as_index_str().to_string());
1098                } else {
1099                    self.1
1100                        .additional_fighting_style
1101                        .replace(option.as_index_str().to_string());
1102                }
1103            }
1104            MultiattackVolley | MultiattackWhirlwindAttack => {
1105                self.1
1106                    .multiattack
1107                    .replace(option.as_index_str().to_string());
1108            }
1109            SuperiorHuntersDefenseEvasion
1110            | SuperiorHuntersDefenseStandAgainstTheTide
1111            | SuperiorHuntersDefenseUncannyDodge => {
1112                self.1
1113                    .superior_hunters_defense
1114                    .replace(option.as_index_str().to_string());
1115            }
1116            RangerTerrainTypeArctic
1117            | RangerTerrainTypeCoast
1118            | RangerTerrainTypeDesert
1119            | RangerTerrainTypeForest
1120            | RangerTerrainTypeGrassland
1121            | RangerTerrainTypeMountain
1122            | RangerTerrainTypeSwamp => {
1123                self.1
1124                    .natural_explorer_terrain_type
1125                    .get_or_insert_with(Vec::new)
1126                    .push(option.as_index_str().to_string());
1127            }
1128            RangerFavoredEnemyTypeAberrations
1129            | RangerFavoredEnemyTypeBeasts
1130            | RangerFavoredEnemyTypeCelestials
1131            | RangerFavoredEnemyTypeConstructs
1132            | RangerFavoredEnemyTypeDragons
1133            | RangerFavoredEnemyTypeElementals
1134            | RangerFavoredEnemyTypeFey
1135            | RangerFavoredEnemyTypeFiends
1136            | RangerFavoredEnemyTypeGiants
1137            | RangerFavoredEnemyTypeMonstrosities
1138            | RangerFavoredEnemyTypeOozes
1139            | RangerFavoredEnemyTypePlants
1140            | RangerFavoredEnemyTypeUndead
1141            | RangerFavoredEnemyTypeHumanoids => {
1142                self.1
1143                    .ranger_favored_enemy_type
1144                    .get_or_insert_with(Vec::new)
1145                    .push(option.as_index_str().to_string());
1146            }
1147            MetamagicCarefulSpell
1148            | MetamagicDistantSpell
1149            | MetamagicEmpoweredSpell
1150            | MetamagicExtendedSpell
1151            | MetamagicHeightenedSpell
1152            | MetamagicQuickenedSpell
1153            | MetamagicSubtleSpell
1154            | MetamagicTwinnedSpell => {
1155                self.1
1156                    .sorcerer_metamagic
1157                    .get_or_insert_with(Vec::new)
1158                    .push(option.as_index_str().to_string());
1159            }
1160            EldritchInvocationAgonizingBlast
1161            | EldritchInvocationArmorOfShadows
1162            | EldritchInvocationAscendantStep
1163            | EldritchInvocationBeastSpeech
1164            | EldritchInvocationBeguilingInfluence
1165            | EldritchInvocationBewitchingWhispers
1166            | EldritchInvocationBookOfAncientSecrets
1167            | EldritchInvocationChainsOfCarceri
1168            | EldritchInvocationDevilsSight
1169            | EldritchInvocationDreadfulWord
1170            | EldritchInvocationEldritchSight
1171            | EldritchInvocationEldritchSpear
1172            | EldritchInvocationEyesOfTheRuneKeeper
1173            | EldritchInvocationFiendishVigor
1174            | EldritchInvocationGazeOfTwoMinds
1175            | EldritchInvocationLifedrinker
1176            | EldritchInvocationMaskOfManyFaces
1177            | EldritchInvocationMasterOfMyriadForms
1178            | EldritchInvocationMinionsOfChaos
1179            | EldritchInvocationMireTheMind
1180            | EldritchInvocationMistyVisions
1181            | EldritchInvocationOneWithShadows
1182            | EldritchInvocationOtherworldlyLeap
1183            | EldritchInvocationRepellingBlast
1184            | EldritchInvocationSculptorOfFlesh
1185            | EldritchInvocationSignOfIllOmen
1186            | EldritchInvocationThiefOfFiveFates
1187            | EldritchInvocationThirstingBlade
1188            | EldritchInvocationVisionsOfDistantRealms
1189            | EldritchInvocationVoiceOfTheChainMaster
1190            | EldritchInvocationWhispersOfTheGrave
1191            | EldritchInvocationWitchSight => {
1192                self.1
1193                    .warlock_eldritch_invocation
1194                    .get_or_insert_with(Vec::new)
1195                    .push(option.as_index_str().to_string());
1196            }
1197            DragonAncestorBlackAcidDamage
1198            | DragonAncestorBlueLightningDamage
1199            | DragonAncestorBrassFireDamage
1200            | DragonAncestorBronzeLightningDamage
1201            | DragonAncestorCopperAcidDamage
1202            | DragonAncestorGoldFireDamage
1203            | DragonAncestorGreenPoisonDamage
1204            | DragonAncestorRedFireDamage
1205            | DragonAncestorSilverColdDamage
1206            | DragonAncestorWhiteColdDamage => {
1207                self.1
1208                    .sorcerer_dragon_ancestor
1209                    .replace(option.as_index_str().to_string());
1210            }
1211        }
1212    }
1213
1214    fn increase_score(&mut self, option: ChoosableCustomLevelFeatureOption) {
1215        match option {
1216            ChoosableCustomLevelFeatureOption::StrengthPlusOne => {
1217                self.1.abilities_modifiers.strength.score += 1;
1218            }
1219            ChoosableCustomLevelFeatureOption::DexterityPlusOne => {
1220                self.1.abilities_modifiers.dexterity.score += 1;
1221            }
1222            ChoosableCustomLevelFeatureOption::ConstitutionPlusOne => {
1223                self.1.abilities_modifiers.constitution.score += 1;
1224            }
1225            ChoosableCustomLevelFeatureOption::IntelligencePlusOne => {
1226                self.1.abilities_modifiers.intelligence.score += 1;
1227            }
1228            ChoosableCustomLevelFeatureOption::WisdomPlusOne => {
1229                self.1.abilities_modifiers.wisdom.score += 1;
1230            }
1231            ChoosableCustomLevelFeatureOption::CharismaPlusOne => {
1232                self.1.abilities_modifiers.charisma.score += 1;
1233            }
1234            _ => {}
1235        }
1236    }
1237}
1238
1239pub async fn get_spellcasting_slots(
1240    index: &str,
1241    level: u8,
1242) -> Result<Option<LevelSpellcasting>, ApiError> {
1243    let op = SpellcastingQuery::build(SpellcastingQueryVariables {
1244        index: Some(format!("{}-{}", index, level)),
1245    });
1246
1247    let spellcasting_slots = Client::new()
1248        .post(GRAPHQL_API_URL.as_str())
1249        .run_graphql(op)
1250        .await?
1251        .data
1252        .ok_or(ApiError::Schema)?
1253        .level
1254        .ok_or(ApiError::Schema)?
1255        .spellcasting;
1256
1257    Ok(spellcasting_slots)
1258}