dnd_character/api/
classes.rs

1use super::shared::schema;
2use crate::api::classes::CustomLevelFeatureType::Ignored;
3use crate::api::shared::ApiError;
4use crate::classes::{Class, Classes, UsableSlots};
5use crate::GRAPHQL_API_URL;
6use cynic::http::ReqwestExt;
7use cynic::{impl_scalar, QueryBuilder};
8use futures::StreamExt;
9use lazy_static::lazy_static;
10use reqwest::Client;
11use serde_json::json;
12use std::collections::HashMap;
13
14#[derive(cynic::QueryVariables, Debug)]
15struct SpellcastingAbilityQueryVariables {
16    pub index: Option<String>,
17}
18
19#[derive(cynic::QueryFragment, Debug)]
20#[cynic(
21    graphql_type = "Query",
22    variables = "SpellcastingAbilityQueryVariables"
23)]
24struct SpellcastingAbilityQuery {
25    #[arguments(index: $ index)]
26    pub class: Option<ClassSpellCasting>,
27}
28
29#[derive(cynic::QueryFragment, Debug)]
30#[cynic(graphql_type = "Class")]
31struct ClassSpellCasting {
32    pub spellcasting: Option<ClassSpellcasting>,
33}
34
35#[derive(cynic::QueryFragment, Debug)]
36struct ClassSpellcasting {
37    #[cynic(rename = "spellcasting_ability")]
38    pub spellcasting_ability: AbilityScore,
39}
40
41#[derive(cynic::QueryFragment, Debug)]
42struct AbilityScore {
43    pub index: String,
44}
45
46#[derive(cynic::QueryVariables, Debug)]
47pub struct SpellcastingQueryVariables {
48    pub index: Option<String>,
49}
50
51#[derive(cynic::QueryFragment, Debug)]
52#[cynic(graphql_type = "Query", variables = "SpellcastingQueryVariables")]
53pub struct SpellcastingQuery {
54    #[arguments(index: $ index)]
55    pub level: Option<Level>,
56}
57
58#[derive(cynic::QueryFragment, Debug)]
59pub struct Level {
60    pub spellcasting: Option<LevelSpellcasting>,
61}
62
63#[derive(cynic::QueryFragment, Debug, Copy, Clone)]
64#[cfg_attr(feature = "serde", derive(serde::Serialize))]
65pub struct LevelSpellcasting {
66    #[cynic(rename = "cantrips_known")]
67    pub cantrips_known: Option<i32>,
68    #[cynic(rename = "spell_slots_level_1")]
69    pub spell_slots_level_1: Option<i32>,
70    #[cynic(rename = "spell_slots_level_2")]
71    pub spell_slots_level_2: Option<i32>,
72    #[cynic(rename = "spell_slots_level_3")]
73    pub spell_slots_level_3: Option<i32>,
74    #[cynic(rename = "spell_slots_level_4")]
75    pub spell_slots_level_4: Option<i32>,
76    #[cynic(rename = "spell_slots_level_5")]
77    pub spell_slots_level_5: Option<i32>,
78    #[cynic(rename = "spell_slots_level_6")]
79    pub spell_slots_level_6: Option<i32>,
80    #[cynic(rename = "spell_slots_level_7")]
81    pub spell_slots_level_7: Option<i32>,
82    #[cynic(rename = "spell_slots_level_8")]
83    pub spell_slots_level_8: Option<i32>,
84    #[cynic(rename = "spell_slots_level_9")]
85    pub spell_slots_level_9: Option<i32>,
86}
87
88impl Into<UsableSlots> for LevelSpellcasting {
89    fn into(self) -> UsableSlots {
90        UsableSlots {
91            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    /// https://www.dnd5eapi.co/api/features/bonus-proficiencies
154    BonusBardProficiency,
155    /// Used for
156    /// https://www.dnd5eapi.co/api/features/bard-expertise-1
157    /// https://www.dnd5eapi.co/api/features/bard-expertise-2
158    /// https://www.dnd5eapi.co/api/features/rogue-expertise-1
159    /// https://www.dnd5eapi.co/api/features/rogue-expertise-2
160    MultiplyTwoSkillProficiency,
161    /// https://www.dnd5eapi.co/api/features/magical-secrets-1
162    /// https://www.dnd5eapi.co/api/features/magical-secrets-2
163    /// https://www.dnd5eapi.co/api/features/magical-secrets-3
164    ChooseTwoSpellForAnyClass,
165    /// https://www.dnd5eapi.co/api/features/mystic-arcanum-6th-level
166    /// https://www.dnd5eapi.co/api/features/mystic-arcanum-7th-level
167    /// https://www.dnd5eapi.co/api/features/mystic-arcanum-8th-level
168    /// https://www.dnd5eapi.co/api/features/mystic-arcanum-9th-level
169    ChooseOne6thLevelSpellFromWarlockList,
170    /// https://www.dnd5eapi.co/api/features/paladin-fighting-style
171    PaladinFightingStyle,
172    /// https://dnd5eapi.rpgmaster.ai/api/2014/features/multiattack
173    Multiattack,
174    /// https://dnd5eapi.rpgmaster.ai/api/2014/features/superior-hunters-defense
175    SuperiorHuntersDefense,
176    /// https://dnd5eapi.rpgmaster.ai/api/2014/features/favored-enemy-1-type
177    /// https://dnd5eapi.rpgmaster.ai/api/2014/features/favored-enemy-2-types
178    /// https://dnd5eapi.rpgmaster.ai/api/2014/features/favored-enemy-3-enemies
179    RangerFavoredEnemyType,
180    /// https://dnd5eapi.rpgmaster.ai/api/2014/features/natural-explorer-1-terrain-type
181    /// https://dnd5eapi.rpgmaster.ai/api/2014/features/natural-explorer-2-terrain-types
182    /// https://dnd5eapi.rpgmaster.ai/api/2014/features/natural-explorer-3-terrain-types
183    RangerTerrainType,
184}
185
186#[derive(Clone, Debug)]
187#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
188#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
189pub enum ChoosableCustomLevelFeatureOption {
190    StrengthPlusOne,
191    DexterityPlusOne,
192    ConstitutionPlusOne,
193    IntelligencePlusOne,
194    WisdomPlusOne,
195    CharismaPlusOne,
196
197    PactOfTheChain,
198    PactOfTheBlade,
199    PactOfTheTome,
200
201    FighterFightingStyleArchery,
202    FighterFightingStyleDefense,
203    FighterFightingStyleDueling,
204    FighterFightingStyleGreatWeaponFighting,
205    FighterFightingStyleProtection,
206    FighterFightingStyleTwoWeaponFighting,
207
208    RangerFightingStyleArchery,
209    RangerFightingStyleDefense,
210    RangerFightingStyleDueling,
211    RangerFightingStyleTwoWeaponFighting,
212
213    RangerTerrainTypeArctic,
214    RangerTerrainTypeCoast,
215    RangerTerrainTypeDesert,
216    RangerTerrainTypeForest,
217    RangerTerrainTypeGrassland,
218    RangerTerrainTypeMountain,
219    RangerTerrainTypeSwamp,
220
221    RangerFavoredEnemyTypeAberrations,
222    RangerFavoredEnemyTypeBeasts,
223    RangerFavoredEnemyTypeCelestials,
224    RangerFavoredEnemyTypeConstructs,
225    RangerFavoredEnemyTypeDragons,
226    RangerFavoredEnemyTypeElementals,
227    RangerFavoredEnemyTypeFey,
228    RangerFavoredEnemyTypeFiends,
229    RangerFavoredEnemyTypeGiants,
230    RangerFavoredEnemyTypeMonstrosities,
231    RangerFavoredEnemyTypeOozes,
232    RangerFavoredEnemyTypePlants,
233    RangerFavoredEnemyTypeUndead,
234    RangerFavoredEnemyTypeHumanoids,
235
236    BardProficiencyStrength,
237    BardProficiencyDexterity,
238    BardProficiencyConstitution,
239    BardProficiencyIntelligence,
240    BardProficiencyWisdom,
241    BardProficiencyCharisma,
242
243    FightingStyleDefense,
244    FightingStyleDueling,
245    FightingStyleGreatWeaponFighting,
246    FightingStyleProtection,
247
248    HuntersPreyGiantKiller,
249    HuntersPreyHordeBreaker,
250    HuntersPreyColossusSlayer,
251
252    DefensiveTacticsSteelWill,
253    DefensiveTacticsEscapeTheHorde,
254    DefensiveTacticsMultiattackDefense,
255
256    MultiattackVolley,
257    MultiattackWhirlwindAttack,
258
259    SuperiorHuntersDefenseEvasion,
260    SuperiorHuntersDefenseStandAgainstTheTide,
261    SuperiorHuntersDefenseUncannyDodge,
262}
263
264impl ChoosableCustomLevelFeatureOption {
265    #[cfg(feature = "serde")]
266    pub fn as_index_str(&self) -> &str {
267        serde_variant::to_variant_name(self).unwrap()
268    }
269
270    #[cfg(feature = "serde")]
271    pub fn from_index_str(index: &str) -> Option<ChoosableCustomLevelFeatureOption> {
272        #[derive(serde::Deserialize)]
273        struct Helper {
274            value: ChoosableCustomLevelFeatureOption,
275        }
276
277        let json = json!({
278            "value": index
279        });
280
281        serde_json::from_value::<Helper>(json)
282            .map(|helper| helper.value)
283            .ok()
284    }
285}
286
287impl ChoosableCustomLevelFeature {
288    #[cfg(feature = "serde")]
289    pub fn as_index_str(&self) -> &str {
290        serde_variant::to_variant_name(self).unwrap()
291    }
292
293    pub fn to_options(&self) -> Vec<Vec<ChoosableCustomLevelFeatureOption>> {
294        use ChoosableCustomLevelFeatureOption::*;
295
296        match self {
297            ChoosableCustomLevelFeature::AbilityScoreImprovement => {
298                let ability_names = vec![
299                    StrengthPlusOne,
300                    DexterityPlusOne,
301                    ConstitutionPlusOne,
302                    IntelligencePlusOne,
303                    WisdomPlusOne,
304                    CharismaPlusOne,
305                ];
306
307                vec![ability_names.clone(), ability_names]
308            }
309            ChoosableCustomLevelFeature::WarlockPact => {
310                vec![vec![PactOfTheChain, PactOfTheBlade, PactOfTheTome]]
311            }
312            ChoosableCustomLevelFeature::AdditionalFighterFightingStyle
313            | ChoosableCustomLevelFeature::FighterFightingStyle => {
314                vec![vec![
315                    FighterFightingStyleArchery,
316                    FighterFightingStyleDefense,
317                    FighterFightingStyleDueling,
318                    FighterFightingStyleGreatWeaponFighting,
319                    FighterFightingStyleProtection,
320                    FighterFightingStyleTwoWeaponFighting,
321                ]]
322            }
323            ChoosableCustomLevelFeature::RangerFightingStyle => {
324                vec![vec![
325                    RangerFightingStyleArchery,
326                    RangerFightingStyleDefense,
327                    RangerFightingStyleDueling,
328                    RangerFightingStyleTwoWeaponFighting,
329                ]]
330            }
331            ChoosableCustomLevelFeature::BonusBardProficiency => {
332                let ability_names = vec![
333                    BardProficiencyStrength,
334                    BardProficiencyDexterity,
335                    BardProficiencyConstitution,
336                    BardProficiencyIntelligence,
337                    BardProficiencyWisdom,
338                    BardProficiencyCharisma,
339                ];
340
341                vec![ability_names.clone(), ability_names.clone(), ability_names]
342            }
343            ChoosableCustomLevelFeature::MultiplyTwoSkillProficiency => {
344                // TODO: Implement this
345                vec![vec![]]
346            }
347            ChoosableCustomLevelFeature::ChooseTwoSpellForAnyClass => {
348                // TODO: Implement this
349                vec![vec![]]
350            }
351            ChoosableCustomLevelFeature::ChooseOne6thLevelSpellFromWarlockList => {
352                // TODO: Implement this when other warlock features are implemented
353                vec![vec![]]
354            }
355            ChoosableCustomLevelFeature::PaladinFightingStyle => {
356                vec![vec![
357                    FightingStyleDefense,
358                    FightingStyleDueling,
359                    FightingStyleGreatWeaponFighting,
360                    FightingStyleProtection,
361                ]]
362            }
363            ChoosableCustomLevelFeature::HuntersPrey => {
364                vec![vec![
365                    HuntersPreyGiantKiller,
366                    HuntersPreyHordeBreaker,
367                    HuntersPreyColossusSlayer,
368                ]]
369            }
370            ChoosableCustomLevelFeature::DefensiveTactics => {
371                vec![vec![
372                    DefensiveTacticsSteelWill,
373                    DefensiveTacticsEscapeTheHorde,
374                    DefensiveTacticsMultiattackDefense,
375                ]]
376            }
377            ChoosableCustomLevelFeature::Multiattack => {
378                vec![vec![MultiattackWhirlwindAttack, MultiattackVolley]]
379            }
380            ChoosableCustomLevelFeature::SuperiorHuntersDefense => {
381                vec![vec![
382                    SuperiorHuntersDefenseEvasion,
383                    SuperiorHuntersDefenseStandAgainstTheTide,
384                    SuperiorHuntersDefenseUncannyDodge,
385                ]]
386            }
387            ChoosableCustomLevelFeature::RangerFavoredEnemyType => {
388                vec![vec![
389                    RangerFavoredEnemyTypeAberrations,
390                    RangerFavoredEnemyTypeBeasts,
391                    RangerFavoredEnemyTypeCelestials,
392                    RangerFavoredEnemyTypeConstructs,
393                    RangerFavoredEnemyTypeDragons,
394                    RangerFavoredEnemyTypeElementals,
395                    RangerFavoredEnemyTypeFey,
396                    RangerFavoredEnemyTypeFiends,
397                    RangerFavoredEnemyTypeGiants,
398                    RangerFavoredEnemyTypeMonstrosities,
399                    RangerFavoredEnemyTypeOozes,
400                    RangerFavoredEnemyTypePlants,
401                    RangerFavoredEnemyTypeUndead,
402                    RangerFavoredEnemyTypeHumanoids,
403                ]]
404            }
405            ChoosableCustomLevelFeature::RangerTerrainType => {
406                vec![vec![
407                    RangerTerrainTypeArctic,
408                    RangerTerrainTypeCoast,
409                    RangerTerrainTypeDesert,
410                    RangerTerrainTypeForest,
411                    RangerTerrainTypeGrassland,
412                    RangerTerrainTypeMountain,
413                    RangerTerrainTypeSwamp,
414                ]]
415            }
416        }
417    }
418}
419
420pub enum SheetLevelFeatureType {
421    /// https://www.dnd5eapi.co/api/features/primal-champion
422    PrimalChampion,
423}
424
425pub enum CustomLevelFeatureType {
426    Choosable(ChoosableCustomLevelFeature),
427    Sheet(SheetLevelFeatureType),
428    Passive,
429    Ignored,
430}
431
432impl CustomLevelFeatureType {
433    pub fn identify(index: String) -> Option<CustomLevelFeatureType> {
434        use ChoosableCustomLevelFeature::*;
435        use CustomLevelFeatureType::*;
436        use SheetLevelFeatureType::*;
437        match index.as_str() {
438            // Ignore all subclass choices since we have only one subclass per class
439            "bard-college"
440            | "divine-domain"
441            | "monastic-tradition"
442            | "sacred-oath"
443            | "ranger-archetype"
444            | "sorcerous-origin"
445            | "druid-circle"
446            | "primal-path"
447            | "martial-archetype"
448            | "otherworldly-patron" => Some(Ignored),
449            "pact-boon" => Some(Choosable(WarlockPact)),
450            "additional-fighting-style" => Some(Choosable(AdditionalFighterFightingStyle)),
451            "fighter-fighting-style" => Some(Choosable(FighterFightingStyle)),
452            "bonus-proficiencies" => Some(Choosable(BonusBardProficiency)),
453            "bonus-proficiency" => Some(Passive),
454            "additional-magical-secrets" | "bonus-cantrip" => Some(Ignored),
455            "channel-divinity-1-rest" | "channel-divinity-2-rest" | "channel-divinity-3-rest" => {
456                Some(Ignored)
457            }
458            //"magical-secrets-1" | "magical-secrets-2" | "magical-secrets-3" => Some(Choosable(ChooseTwoSpellForAnyClass)), TODO: Implement this
459            "magical-secrets-1" | "magical-secrets-2" | "magical-secrets-3" => Some(Ignored),
460            "mystic-arcanum-6th-level"
461            | "mystic-arcanum-7th-level"
462            | "mystic-arcanum-8th-level"
463            | "mystic-arcanum-9th-level" => Some(Choosable(ChooseOne6thLevelSpellFromWarlockList)),
464            "paladin-fighting-style" => Some(Choosable(PaladinFightingStyle)),
465            "primal-champion" => Some(Sheet(PrimalChampion)),
466            // TODO: Implement https://www.dnd5eapi.co/api/features/diamond-soul
467            "diamond-soul" => Some(Passive),
468            "arcane-recovery"
469            | "arcane-tradition"
470            | "archdruid"
471            | "aura-improvements"
472            | "aura-of-courage"
473            | "aura-of-devotion"
474            | "aura-of-protection"
475            | "blessed-healer"
476            | "blindsense"
477            | "brutal-critical-1-dice"
478            | "brutal-critical-2-dice"
479            | "brutal-critical-3-dice"
480            | "danger-sense"
481            | "dark-ones-blessing"
482            | "dark-ones-own-luck"
483            | "destroy-undead-cr-1-or-below"
484            | "destroy-undead-cr-2-or-below"
485            | "destroy-undead-cr-3-or-below"
486            | "destroy-undead-cr-4-or-below"
487            | "destroy-undead-cr-1-2-or-below"
488            | "disciple-of-life"
489            | "divine-health"
490            | "draconic-resilience"
491            | "dragon-wings"
492            | "draconic-presence"
493            | "font-of-magic"
494            | "dragon-ancestor-black---acid-damage"
495            | "dragon-ancestor-blue---lightning-damage"
496            | "dragon-ancestor-brass---fire-damage"
497            | "dragon-ancestor-bronze---lightning-damage"
498            | "dragon-ancestor-copper---acid-damage"
499            | "dragon-ancestor-gold---fire-damage"
500            | "dragon-ancestor-green---poison-damage"
501            | "dragon-ancestor-red---fire-damage"
502            | "dragon-ancestor-silver---cold-damage"
503            | "dragon-ancestor-white---cold-damage"
504            | "druid-lands-stride"
505            | "druid-timeless-body"
506            | "druidic"
507            | "elusive"
508            | "empowered-evocation"
509            | "elemental-affinity"
510            | "fast-movement"
511            | "feral-instinct"
512            | "feral-senses"
513            | "fighter-fighting-style-archery"
514            | "fighter-fighting-style-protection"
515            | "fighter-fighting-style-defense"
516            | "fighter-fighting-style-dueling"
517            | "fighter-fighting-style-great-weapon-fighting"
518            | "fighter-fighting-style-two-weapon-fighting"
519            | "fighting-style-defense"
520            | "fighting-style-dueling"
521            | "fighting-style-great-weapon-fighting"
522            | "foe-slayer"
523            | "hurl-through-hell"
524            | "improved-critical"
525            | "improved-divine-smite"
526            | "indomitable-1-use"
527            | "indomitable-2-uses"
528            | "indomitable-3-uses"
529            | "indomitable-might"
530            | "ki-empowered-strikes"
531            | "jack-of-all-trades"
532            | "martial-arts"
533            | "monk-evasion"
534            | "monk-timeless-body"
535            | "purity-of-body"
536            | "purity-of-spirit"
537            | "natures-sanctuary"
538            | "natures-ward"
539            | "sculpt-spells"
540            | "ranger-lands-stride"
541            | "relentless-rage"
542            | "reliable-talent"
543            | "remarkable-athlete"
544            | "rogue-evasion"
545            | "superior-critical"
546            | "superior-inspiration"
547            | "supreme-healing"
548            | "supreme-sneak"
549            | "survivor"
550            | "thiefs-reflexes"
551            | "thieves-cant"
552            | "tongue-of-the-sun-and-moon"
553            | "tranquility"
554            | "unarmored-movement-1"
555            | "unarmored-movement-2"
556            | "use-magic-device"
557            | "wild-shape-cr-1-2-or-below-no-flying-speed"
558            | "wild-shape-cr-1-4-or-below-no-flying-or-swim-speed"
559            | "wild-shape-cr-1-or-below"
560            | "ki"
561            | "monk-unarmored-defense"
562            | "perfect-self"
563            | "slippery-mind"
564            | "mindless-rage"
565            | "barbarian-unarmored-defense"
566            | "divine-intervention-improvement"
567            | "persistent-rage"
568            | "evocation-savant"
569            | "overchannel"
570            | "potent-cantrip"
571            | "second-story-work"
572            | "primeval-awareness"
573            | "beast-spells" => Some(Passive),
574            // ignored until implementation?
575            "oath-spells" => Some(Ignored),
576            "hunters-prey" => Some(Choosable(HuntersPrey)),
577            "superior-hunters-defense" => Some(Choosable(SuperiorHuntersDefense)),
578            //x if x.starts_with("bard-expertise-") || x.starts_with("rogue-expertise-") => Some(Choosable(MultiplyTwoSkillProficiency)),
579            x if x.starts_with("bard-expertise-") || x.starts_with("rogue-expertise-") => {
580                Some(Ignored)
581            } // TODO: Implement this
582            x if x.starts_with("spellcasting-") => Some(Ignored),
583            // Ignore all eldritch invocations since they are unlocked using invocation known table
584            x if x.starts_with("eldritch-invocation-") => Some(Ignored),
585            // Ignore all circle-spells until implementation
586            x if x.starts_with("circle-spells-") => Some(Ignored),
587            // Ignore all circle of the land until implementation
588            x if x.starts_with("circle-of-the-land-") => Some(Ignored),
589            // Ignore all domain spells until implementation
590            x if x.starts_with("domain-spells-") => Some(Ignored),
591            x if x.starts_with("defensive-tactics") => Some(Choosable(DefensiveTactics)),
592            x if x.starts_with("multiattack") => Some(Choosable(Multiattack)),
593            x if x.starts_with("ranger-fighting-style") => Some(Choosable(RangerFightingStyle)),
594            x if x.starts_with("favored-enemy-") => Some(Choosable(RangerFavoredEnemyType)),
595            x if x.starts_with("natural-explorer-") => Some(Choosable(RangerTerrainType)),
596            x if x.contains("ability-score-improvement") => {
597                Some(Choosable(AbilityScoreImprovement))
598            }
599            _ => None,
600        }
601    }
602}
603
604impl Classes {
605    pub(super) async fn new_day(&mut self) {
606        futures::stream::iter(self.0.values_mut())
607            .for_each_concurrent(None, |class| class.new_day())
608            .await;
609    }
610}
611
612impl Class {
613    pub(super) async fn new_day(&mut self) {
614        use crate::classes::ClassSpellCasting::*;
615
616        let index = self.index().to_string();
617
618        if let Some(spell_casting) = &mut self.1.spell_casting {
619            match spell_casting {
620                KnowledgePrepared {
621                    pending_preparation,
622                    spells_prepared_index,
623                    ..
624                }
625                | AlreadyKnowPrepared {
626                    pending_preparation,
627                    spells_prepared_index,
628                    ..
629                } => {
630                    *pending_preparation = true;
631                    spells_prepared_index.clear();
632                }
633                KnowledgeAlreadyPrepared { usable_slots, .. } => {
634                    if let Ok(Some(spellcasting_slots)) =
635                        get_spellcasting_slots(index.as_str(), self.1.level).await
636                    {
637                        *usable_slots = spellcasting_slots.into();
638                    }
639                }
640            }
641        }
642    }
643
644    pub async fn get_spellcasting_ability_index(&self) -> Result<String, ApiError> {
645        let op = SpellcastingAbilityQuery::build(SpellcastingAbilityQueryVariables {
646            index: Some(self.index().to_string()),
647        });
648
649        let ability_index = Client::new()
650            .post(GRAPHQL_API_URL.as_str())
651            .run_graphql(op)
652            .await?
653            .data
654            .ok_or(ApiError::Schema)?
655            .class
656            .ok_or(ApiError::Schema)?
657            .spellcasting
658            .ok_or(ApiError::Schema)?
659            .spellcasting_ability
660            .index;
661
662        Ok(ability_index)
663    }
664
665    pub async fn get_spellcasting_slots(&self) -> Result<Option<LevelSpellcasting>, ApiError> {
666        get_spellcasting_slots(self.index(), self.1.level).await
667    }
668
669    pub async fn set_level(
670        &mut self,
671        new_level: u8,
672    ) -> Result<Vec<ChoosableCustomLevelFeature>, ApiError> {
673        let op = LevelFeaturesQuery::build(LevelFeaturesQueryVariables {
674            class: Some(StringFilter(self.index().to_string())),
675            level: Some(LevelFilter {
676                gt: Some(self.1.level),
677                lte: Some(new_level),
678                gte: None,
679            }),
680        });
681
682        let features = Client::new()
683            .post(GRAPHQL_API_URL.as_str())
684            .run_graphql(op)
685            .await?
686            .data
687            .ok_or(ApiError::Schema)?
688            .features
689            .ok_or(ApiError::Schema)?;
690
691        let mut pending_features = vec![];
692
693        features
694            .iter()
695            .filter_map(|feature| CustomLevelFeatureType::identify(feature.index.clone()))
696            .for_each(|feature| match feature {
697                CustomLevelFeatureType::Passive => {}
698                CustomLevelFeatureType::Choosable(feature) => {
699                    pending_features.push(feature);
700                }
701                CustomLevelFeatureType::Sheet(feature) => match feature {
702                    SheetLevelFeatureType::PrimalChampion => {
703                        self.1.abilities_modifiers.strength.score += 4;
704                        self.1.abilities_modifiers.dexterity.score += 4;
705                    }
706                },
707                Ignored => {}
708            });
709
710        self.1.level = new_level;
711
712        Ok(pending_features)
713    }
714
715    pub async fn get_levels_features(
716        &self,
717        from_level: Option<u8>,
718        passive: bool,
719    ) -> Result<Vec<String>, ApiError> {
720        let op = LevelFeaturesQuery::build(LevelFeaturesQueryVariables {
721            class: Some(StringFilter(self.index().to_string())),
722            level: Some(LevelFilter {
723                gte: Some(from_level.unwrap_or(0)),
724                lte: Some(self.1.level),
725                gt: None,
726            }),
727        });
728
729        let features = Client::new()
730            .post(GRAPHQL_API_URL.as_str())
731            .run_graphql(op)
732            .await?
733            .data
734            .ok_or(ApiError::Schema)?
735            .features
736            .ok_or(ApiError::Schema)?;
737
738        // Remove all identifiable features
739        let mut features: Vec<String> = features
740            .into_iter()
741            .filter(
742                |feature| match CustomLevelFeatureType::identify(feature.index.clone()) {
743                    None => true,
744                    Some(custom_type) => match custom_type {
745                        CustomLevelFeatureType::Passive => passive,
746                        _ => false,
747                    },
748                },
749            )
750            .map(|feature| feature.index)
751            .collect();
752
753        let features: Vec<String> = {
754            lazy_static! {
755                static ref CR_REGEX: regex::Regex =
756                    regex::Regex::new(r"destroy-undead-cr-([0-9]+(?:-[0-9]+)?)\-or-below").unwrap();
757            }
758
759            let mut found = false;
760
761            features
762                .iter_mut()
763                .rev()
764                .filter(|feature| {
765                    if CR_REGEX.is_match(feature) {
766                        if found {
767                            false
768                        } else {
769                            found = true;
770                            true
771                        }
772                    } else {
773                        true
774                    }
775                })
776                .map(|feature| feature.clone())
777                .collect()
778        };
779
780        lazy_static! {
781            static ref DICE_REGEX: regex::Regex = regex::Regex::new(r"^(.+)-d(\d+)$").unwrap();
782        }
783
784        let mut grouped_features: HashMap<String, u32> = HashMap::new();
785        for feature in &features {
786            if let Some(caps) = DICE_REGEX.captures(feature) {
787                if caps.len() == 3 {
788                    let prefix = caps.get(1).unwrap().as_str().to_string();
789                    let dice_value = caps.get(2).unwrap().as_str().parse::<u32>().unwrap();
790
791                    let current_max = grouped_features.entry(prefix).or_insert(0);
792                    if dice_value > *current_max {
793                        *current_max = dice_value;
794                    }
795                }
796            }
797        }
798
799        let mut features: Vec<String> = features
800            .into_iter()
801            .filter(|feature| {
802                if let Some(caps) = DICE_REGEX.captures(feature) {
803                    let prefix = caps.get(1).unwrap().as_str();
804                    let dice_value = caps
805                        .get(2)
806                        .unwrap()
807                        .as_str()
808                        .parse::<u32>()
809                        .expect("Parsing dice value");
810
811                    if let Some(&max_dice) = grouped_features.get(prefix) {
812                        return dice_value == max_dice;
813                    }
814                }
815                true
816            })
817            .collect();
818
819        // Add the selected multiattack feature if it exists and we're not requesting passive features
820        if !passive {
821            if let Some(multiattack) = &self.1.multiattack {
822                features.push(multiattack.clone());
823            }
824            if let Some(hunters_prey) = &self.1.hunters_prey {
825                features.push(hunters_prey.clone());
826            }
827        }
828
829        Ok(features)
830    }
831
832    pub fn apply_option(&mut self, option: ChoosableCustomLevelFeatureOption) {
833        use ChoosableCustomLevelFeatureOption::*;
834
835        match option {
836            StrengthPlusOne | DexterityPlusOne | ConstitutionPlusOne | IntelligencePlusOne
837            | WisdomPlusOne | CharismaPlusOne => self.increase_score(option),
838            BardProficiencyStrength
839            | BardProficiencyDexterity
840            | BardProficiencyConstitution
841            | BardProficiencyIntelligence
842            | BardProficiencyWisdom
843            | BardProficiencyCharisma => self.set_proficiency(option),
844            PactOfTheChain | PactOfTheBlade | PactOfTheTome => {
845                println!("Pact of the Chain, Blade or Tome not yet implemented");
846            }
847            HuntersPreyGiantKiller | HuntersPreyHordeBreaker | HuntersPreyColossusSlayer => {
848                self.1
849                    .hunters_prey
850                    .replace(option.as_index_str().to_string());
851            }
852            DefensiveTacticsSteelWill
853            | DefensiveTacticsEscapeTheHorde
854            | DefensiveTacticsMultiattackDefense => {
855                self.1
856                    .defensive_tactics
857                    .replace(option.as_index_str().to_string());
858            }
859            FighterFightingStyleArchery
860            | FighterFightingStyleDefense
861            | FighterFightingStyleDueling
862            | FighterFightingStyleGreatWeaponFighting
863            | FighterFightingStyleProtection
864            | FighterFightingStyleTwoWeaponFighting
865            | RangerFightingStyleArchery
866            | RangerFightingStyleDefense
867            | RangerFightingStyleDueling
868            | RangerFightingStyleTwoWeaponFighting
869            | FightingStyleDefense
870            | FightingStyleDueling
871            | FightingStyleGreatWeaponFighting
872            | FightingStyleProtection => {
873                if self.1.fighting_style.is_none() {
874                    self.1
875                        .fighting_style
876                        .replace(option.as_index_str().to_string());
877                } else {
878                    self.1
879                        .additional_fighting_style
880                        .replace(option.as_index_str().to_string());
881                }
882            }
883            MultiattackVolley | MultiattackWhirlwindAttack => {
884                self.1
885                    .multiattack
886                    .replace(option.as_index_str().to_string());
887            }
888            SuperiorHuntersDefenseEvasion
889            | SuperiorHuntersDefenseStandAgainstTheTide
890            | SuperiorHuntersDefenseUncannyDodge => {
891                self.1
892                    .superior_hunters_defense
893                    .replace(option.as_index_str().to_string());
894            }
895            RangerTerrainTypeArctic
896            | RangerTerrainTypeCoast
897            | RangerTerrainTypeDesert
898            | RangerTerrainTypeForest
899            | RangerTerrainTypeGrassland
900            | RangerTerrainTypeMountain
901            | RangerTerrainTypeSwamp => {
902                self.1
903                    .natural_explorer_terrain_type
904                    .get_or_insert_with(Vec::new)
905                    .push(option.as_index_str().to_string());
906            }
907            RangerFavoredEnemyTypeAberrations
908            | RangerFavoredEnemyTypeBeasts
909            | RangerFavoredEnemyTypeCelestials
910            | RangerFavoredEnemyTypeConstructs
911            | RangerFavoredEnemyTypeDragons
912            | RangerFavoredEnemyTypeElementals
913            | RangerFavoredEnemyTypeFey
914            | RangerFavoredEnemyTypeFiends
915            | RangerFavoredEnemyTypeGiants
916            | RangerFavoredEnemyTypeMonstrosities
917            | RangerFavoredEnemyTypeOozes
918            | RangerFavoredEnemyTypePlants
919            | RangerFavoredEnemyTypeUndead
920            | RangerFavoredEnemyTypeHumanoids => {
921                self.1
922                    .ranger_favored_enemy_type
923                    .get_or_insert_with(Vec::new)
924                    .push(option.as_index_str().to_string());
925            }
926        }
927    }
928
929    fn increase_score(&mut self, option: ChoosableCustomLevelFeatureOption) {
930        match option {
931            ChoosableCustomLevelFeatureOption::StrengthPlusOne => {
932                self.1.abilities_modifiers.strength.score += 1;
933            }
934            ChoosableCustomLevelFeatureOption::DexterityPlusOne => {
935                self.1.abilities_modifiers.dexterity.score += 1;
936            }
937            ChoosableCustomLevelFeatureOption::ConstitutionPlusOne => {
938                self.1.abilities_modifiers.constitution.score += 1;
939            }
940            ChoosableCustomLevelFeatureOption::IntelligencePlusOne => {
941                self.1.abilities_modifiers.intelligence.score += 1;
942            }
943            ChoosableCustomLevelFeatureOption::WisdomPlusOne => {
944                self.1.abilities_modifiers.wisdom.score += 1;
945            }
946            ChoosableCustomLevelFeatureOption::CharismaPlusOne => {
947                self.1.abilities_modifiers.charisma.score += 1;
948            }
949            _ => {}
950        }
951    }
952
953    fn set_proficiency(&mut self, option: ChoosableCustomLevelFeatureOption) {
954        match option {
955            ChoosableCustomLevelFeatureOption::BardProficiencyStrength => {
956                self.1.abilities_modifiers.strength.proficiency = true;
957            }
958            ChoosableCustomLevelFeatureOption::BardProficiencyDexterity => {
959                self.1.abilities_modifiers.dexterity.proficiency = true;
960            }
961            ChoosableCustomLevelFeatureOption::BardProficiencyConstitution => {
962                self.1.abilities_modifiers.constitution.proficiency = true;
963            }
964            ChoosableCustomLevelFeatureOption::BardProficiencyIntelligence => {
965                self.1.abilities_modifiers.intelligence.proficiency = true;
966            }
967            ChoosableCustomLevelFeatureOption::BardProficiencyWisdom => {
968                self.1.abilities_modifiers.wisdom.proficiency = true;
969            }
970            ChoosableCustomLevelFeatureOption::BardProficiencyCharisma => {
971                self.1.abilities_modifiers.charisma.proficiency = true;
972            }
973            _ => {}
974        }
975    }
976}
977
978pub async fn get_spellcasting_slots(
979    index: &str,
980    level: u8,
981) -> Result<Option<LevelSpellcasting>, ApiError> {
982    let op = SpellcastingQuery::build(SpellcastingQueryVariables {
983        index: Some(format!("{}-{}", index, level)),
984    });
985
986    let spellcasting_slots = Client::new()
987        .post(GRAPHQL_API_URL.as_str())
988        .run_graphql(op)
989        .await?
990        .data
991        .ok_or(ApiError::Schema)?
992        .level
993        .ok_or(ApiError::Schema)?
994        .spellcasting;
995
996    Ok(spellcasting_slots)
997}