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            | "archdruid"
470            | "aura-improvements"
471            | "aura-of-courage"
472            | "aura-of-devotion"
473            | "aura-of-protection"
474            | "blessed-healer"
475            | "blindsense"
476            | "brutal-critical-1-dice"
477            | "brutal-critical-2-dice"
478            | "brutal-critical-3-dice"
479            | "danger-sense"
480            | "dark-ones-blessing"
481            | "dark-ones-own-luck"
482            | "destroy-undead-cr-1-or-below"
483            | "destroy-undead-cr-2-or-below"
484            | "destroy-undead-cr-3-or-below"
485            | "destroy-undead-cr-4-or-below"
486            | "destroy-undead-cr-1-2-or-below"
487            | "disciple-of-life"
488            | "divine-health"
489            | "draconic-resilience"
490            | "dragon-wings"
491            | "draconic-presence"
492            | "font-of-magic"
493            | "dragon-ancestor-black---acid-damage"
494            | "dragon-ancestor-blue---lightning-damage"
495            | "dragon-ancestor-brass---fire-damage"
496            | "dragon-ancestor-bronze---lightning-damage"
497            | "dragon-ancestor-copper---acid-damage"
498            | "dragon-ancestor-gold---fire-damage"
499            | "dragon-ancestor-green---poison-damage"
500            | "dragon-ancestor-red---fire-damage"
501            | "dragon-ancestor-silver---cold-damage"
502            | "dragon-ancestor-white---cold-damage"
503            | "druid-lands-stride"
504            | "druid-timeless-body"
505            | "druidic"
506            | "elusive"
507            | "empowered-evocation"
508            | "elemental-affinity"
509            | "fast-movement"
510            | "favored-enemy-1-type"
511            | "favored-enemy-2-types"
512            | "favored-enemy-3-enemies"
513            | "feral-instinct"
514            | "feral-senses"
515            | "fighter-fighting-style-archery"
516            | "fighter-fighting-style-protection"
517            | "fighter-fighting-style-defense"
518            | "fighter-fighting-style-dueling"
519            | "fighter-fighting-style-great-weapon-fighting"
520            | "fighter-fighting-style-two-weapon-fighting"
521            | "fighting-style-defense"
522            | "fighting-style-dueling"
523            | "fighting-style-great-weapon-fighting"
524            | "foe-slayer"
525            | "hurl-through-hell"
526            | "improved-critical"
527            | "improved-divine-smite"
528            | "indomitable-1-use"
529            | "indomitable-2-uses"
530            | "indomitable-3-uses"
531            | "indomitable-might"
532            | "ki-empowered-strikes"
533            | "jack-of-all-trades"
534            | "martial-arts"
535            | "monk-evasion"
536            | "monk-timeless-body"
537            | "natural-explorer-1-terrain-type"
538            | "natural-explorer-2-terrain-types"
539            | "natural-explorer-3-terrain-types"
540            | "purity-of-body"
541            | "purity-of-spirit"
542            | "natures-sanctuary"
543            | "natures-ward"
544            | "sculpt-spells"
545            | "ranger-lands-stride"
546            | "relentless-rage"
547            | "reliable-talent"
548            | "remarkable-athlete"
549            | "rogue-evasion"
550            | "superior-critical"
551            | "superior-inspiration"
552            | "supreme-healing"
553            | "supreme-sneak"
554            | "survivor"
555            | "thiefs-reflexes"
556            | "thieves-cant"
557            | "tongue-of-the-sun-and-moon"
558            | "tranquility"
559            | "unarmored-movement-1"
560            | "unarmored-movement-2"
561            | "use-magic-device"
562            | "wild-shape-cr-1-2-or-below-no-flying-speed"
563            | "wild-shape-cr-1-4-or-below-no-flying-or-swim-speed"
564            | "wild-shape-cr-1-or-below"
565            | "ki"
566            | "monk-unarmored-defense"
567            | "perfect-self"
568            | "slippery-mind"
569            | "mindless-rage"
570            | "barbarian-unarmored-defense"
571            | "divine-intervention-improvement"
572            | "persistent-rage"
573            | "evocation-savant"
574            | "overchannel"
575            | "potent-cantrip"
576            | "second-story-work"
577            | "primeval-awareness"
578            | "beast-spells" => Some(Passive),
579            // ignored until implementation?
580            "oath-spells" => Some(Ignored),
581            //x if x.starts_with("bard-expertise-") || x.starts_with("rogue-expertise-") => Some(Choosable(MultiplyTwoSkillProficiency)),
582            x if x.starts_with("bard-expertise-") || x.starts_with("rogue-expertise-") => {
583                Some(Ignored)
584            } // TODO: Implement this
585            x if x.starts_with("spellcasting-") => Some(Ignored),
586            // Ignore all eldritch invocations since they are unlocked using invocation known table
587            x if x.starts_with("eldritch-invocation-") => Some(Ignored),
588            // Ignore all circle-spells until implementation
589            x if x.starts_with("circle-spells-") => Some(Ignored),
590            // Ignore all circle of the land until implementation
591            x if x.starts_with("circle-of-the-land-") => Some(Ignored),
592            // Ignore all domain spells until implementation
593            x if x.starts_with("domain-spells-") => Some(Ignored),
594            x if x.starts_with("hunters-prey") => Some(Choosable(HuntersPrey)),
595            x if x.starts_with("defensive-tactics") => Some(Choosable(DefensiveTactics)),
596            x if x.starts_with("multiattack") => Some(Choosable(Multiattack)),
597            x if x.starts_with("ranger-fighting-style") => Some(Choosable(RangerFightingStyle)),
598            x if x.starts_with("favored-enemy-") => Some(Choosable(RangerFavoredEnemyType)),
599            x if x.starts_with("natural-explorer-") => Some(Choosable(RangerTerrainType)),
600            x if x.starts_with("superior-hunters-defense") => {
601                Some(Choosable(SuperiorHuntersDefense))
602            }
603            x if x.contains("ability-score-improvement") => {
604                Some(Choosable(AbilityScoreImprovement))
605            }
606            _ => None,
607        }
608    }
609}
610
611impl Classes {
612    pub(super) async fn new_day(&mut self) {
613        futures::stream::iter(self.0.values_mut())
614            .for_each_concurrent(None, |class| class.new_day())
615            .await;
616    }
617}
618
619impl Class {
620    pub(super) async fn new_day(&mut self) {
621        use crate::classes::ClassSpellCasting::*;
622
623        let index = self.index().to_string();
624
625        if let Some(spell_casting) = &mut self.1.spell_casting {
626            match spell_casting {
627                KnowledgePrepared {
628                    pending_preparation,
629                    spells_prepared_index,
630                    ..
631                }
632                | AlreadyKnowPrepared {
633                    pending_preparation,
634                    spells_prepared_index,
635                    ..
636                } => {
637                    *pending_preparation = true;
638                    spells_prepared_index.clear();
639                }
640                KnowledgeAlreadyPrepared { usable_slots, .. } => {
641                    if let Ok(Some(spellcasting_slots)) =
642                        get_spellcasting_slots(index.as_str(), self.1.level).await
643                    {
644                        *usable_slots = spellcasting_slots.into();
645                    }
646                }
647            }
648        }
649    }
650
651    pub async fn get_spellcasting_ability_index(&self) -> Result<String, ApiError> {
652        let op = SpellcastingAbilityQuery::build(SpellcastingAbilityQueryVariables {
653            index: Some(self.index().to_string()),
654        });
655
656        let ability_index = Client::new()
657            .post(GRAPHQL_API_URL.as_str())
658            .run_graphql(op)
659            .await?
660            .data
661            .ok_or(ApiError::Schema)?
662            .class
663            .ok_or(ApiError::Schema)?
664            .spellcasting
665            .ok_or(ApiError::Schema)?
666            .spellcasting_ability
667            .index;
668
669        Ok(ability_index)
670    }
671
672    pub async fn get_spellcasting_slots(&self) -> Result<Option<LevelSpellcasting>, ApiError> {
673        get_spellcasting_slots(self.index(), self.1.level).await
674    }
675
676    pub async fn set_level(
677        &mut self,
678        new_level: u8,
679    ) -> Result<Vec<ChoosableCustomLevelFeature>, ApiError> {
680        let op = LevelFeaturesQuery::build(LevelFeaturesQueryVariables {
681            class: Some(StringFilter(self.index().to_string())),
682            level: Some(LevelFilter {
683                gt: Some(self.1.level),
684                lte: Some(new_level),
685                gte: None,
686            }),
687        });
688
689        let features = Client::new()
690            .post(GRAPHQL_API_URL.as_str())
691            .run_graphql(op)
692            .await?
693            .data
694            .ok_or(ApiError::Schema)?
695            .features
696            .ok_or(ApiError::Schema)?;
697
698        let mut pending_features = vec![];
699
700        features
701            .iter()
702            .filter_map(|feature| CustomLevelFeatureType::identify(feature.index.clone()))
703            .for_each(|feature| match feature {
704                CustomLevelFeatureType::Passive => {}
705                CustomLevelFeatureType::Choosable(feature) => {
706                    pending_features.push(feature);
707                }
708                CustomLevelFeatureType::Sheet(feature) => match feature {
709                    SheetLevelFeatureType::PrimalChampion => {
710                        self.1.abilities_modifiers.strength.score += 4;
711                        self.1.abilities_modifiers.dexterity.score += 4;
712                    }
713                },
714                Ignored => {}
715            });
716
717        self.1.level = new_level;
718
719        Ok(pending_features)
720    }
721
722    pub async fn get_levels_features(
723        &self,
724        from_level: Option<u8>,
725        passive: bool,
726    ) -> Result<Vec<String>, ApiError> {
727        let op = LevelFeaturesQuery::build(LevelFeaturesQueryVariables {
728            class: Some(StringFilter(self.index().to_string())),
729            level: Some(LevelFilter {
730                gte: Some(from_level.unwrap_or(0)),
731                lte: Some(self.1.level),
732                gt: None,
733            }),
734        });
735
736        let features = Client::new()
737            .post(GRAPHQL_API_URL.as_str())
738            .run_graphql(op)
739            .await?
740            .data
741            .ok_or(ApiError::Schema)?
742            .features
743            .ok_or(ApiError::Schema)?;
744
745        // Remove all identifiable features
746        let mut features: Vec<String> = features
747            .into_iter()
748            .filter(
749                |feature| match CustomLevelFeatureType::identify(feature.index.clone()) {
750                    None => true,
751                    Some(custom_type) => match custom_type {
752                        CustomLevelFeatureType::Passive => passive,
753                        _ => false,
754                    },
755                },
756            )
757            .map(|feature| feature.index)
758            .collect();
759
760        let features: Vec<String> = {
761            lazy_static! {
762                static ref CR_REGEX: regex::Regex =
763                    regex::Regex::new(r"destroy-undead-cr-([0-9]+(?:-[0-9]+)?)\-or-below").unwrap();
764            }
765
766            let mut found = false;
767
768            features
769                .iter_mut()
770                .rev()
771                .filter(|feature| {
772                    if CR_REGEX.is_match(feature) {
773                        if found {
774                            false
775                        } else {
776                            found = true;
777                            true
778                        }
779                    } else {
780                        true
781                    }
782                })
783                .map(|feature| feature.clone())
784                .collect()
785        };
786
787        lazy_static! {
788            static ref DICE_REGEX: regex::Regex = regex::Regex::new(r"^(.+)-d(\d+)$").unwrap();
789        }
790
791        let mut grouped_features: HashMap<String, u32> = HashMap::new();
792        for feature in &features {
793            if let Some(caps) = DICE_REGEX.captures(feature) {
794                if caps.len() == 3 {
795                    let prefix = caps.get(1).unwrap().as_str().to_string();
796                    let dice_value = caps.get(2).unwrap().as_str().parse::<u32>().unwrap();
797
798                    let current_max = grouped_features.entry(prefix).or_insert(0);
799                    if dice_value > *current_max {
800                        *current_max = dice_value;
801                    }
802                }
803            }
804        }
805
806        let mut features: Vec<String> = features
807            .into_iter()
808            .filter(|feature| {
809                if let Some(caps) = DICE_REGEX.captures(feature) {
810                    let prefix = caps.get(1).unwrap().as_str();
811                    let dice_value = caps
812                        .get(2)
813                        .unwrap()
814                        .as_str()
815                        .parse::<u32>()
816                        .expect("Parsing dice value");
817
818                    if let Some(&max_dice) = grouped_features.get(prefix) {
819                        return dice_value == max_dice;
820                    }
821                }
822                true
823            })
824            .collect();
825
826        // Add the selected multiattack feature if it exists and we're not requesting passive features
827        if !passive {
828            if let Some(multiattack) = &self.1.multiattack {
829                features.push(multiattack.clone());
830            }
831            if let Some(hunters_prey) = &self.1.hunters_prey {
832                features.push(hunters_prey.clone());
833            }
834        }
835
836        Ok(features)
837    }
838
839    pub fn apply_option(&mut self, option: ChoosableCustomLevelFeatureOption) {
840        use ChoosableCustomLevelFeatureOption::*;
841
842        match option {
843            StrengthPlusOne | DexterityPlusOne | ConstitutionPlusOne | IntelligencePlusOne
844            | WisdomPlusOne | CharismaPlusOne => self.increase_score(option),
845            BardProficiencyStrength
846            | BardProficiencyDexterity
847            | BardProficiencyConstitution
848            | BardProficiencyIntelligence
849            | BardProficiencyWisdom
850            | BardProficiencyCharisma => self.set_proficiency(option),
851            PactOfTheChain | PactOfTheBlade | PactOfTheTome => {
852                println!("Pact of the Chain, Blade or Tome not yet implemented");
853            }
854            HuntersPreyGiantKiller | HuntersPreyHordeBreaker | HuntersPreyColossusSlayer => {
855                self.1
856                    .hunters_prey
857                    .replace(option.as_index_str().to_string());
858            }
859            DefensiveTacticsSteelWill
860            | DefensiveTacticsEscapeTheHorde
861            | DefensiveTacticsMultiattackDefense => {
862                self.1
863                    .defensive_tactics
864                    .replace(option.as_index_str().to_string());
865            }
866            FighterFightingStyleArchery
867            | FighterFightingStyleDefense
868            | FighterFightingStyleDueling
869            | FighterFightingStyleGreatWeaponFighting
870            | FighterFightingStyleProtection
871            | FighterFightingStyleTwoWeaponFighting
872            | RangerFightingStyleArchery
873            | RangerFightingStyleDefense
874            | RangerFightingStyleDueling
875            | RangerFightingStyleTwoWeaponFighting
876            | FightingStyleDefense
877            | FightingStyleDueling
878            | FightingStyleGreatWeaponFighting
879            | FightingStyleProtection => {
880                if self.1.fighting_style.is_none() {
881                    self.1
882                        .fighting_style
883                        .replace(option.as_index_str().to_string());
884                } else {
885                    self.1
886                        .additional_fighting_style
887                        .replace(option.as_index_str().to_string());
888                }
889            }
890            MultiattackVolley | MultiattackWhirlwindAttack => {
891                self.1
892                    .multiattack
893                    .replace(option.as_index_str().to_string());
894            }
895            SuperiorHuntersDefenseEvasion
896            | SuperiorHuntersDefenseStandAgainstTheTide
897            | SuperiorHuntersDefenseUncannyDodge => {
898                self.1
899                    .superior_hunters_defense
900                    .replace(option.as_index_str().to_string());
901            }
902            RangerTerrainTypeArctic
903            | RangerTerrainTypeCoast
904            | RangerTerrainTypeDesert
905            | RangerTerrainTypeForest
906            | RangerTerrainTypeGrassland
907            | RangerTerrainTypeMountain
908            | RangerTerrainTypeSwamp => {
909                self.1
910                    .natural_explorer_terrain_type
911                    .get_or_insert_with(Vec::new)
912                    .push(option.as_index_str().to_string());
913            }
914            RangerFavoredEnemyTypeAberrations
915            | RangerFavoredEnemyTypeBeasts
916            | RangerFavoredEnemyTypeCelestials
917            | RangerFavoredEnemyTypeConstructs
918            | RangerFavoredEnemyTypeDragons
919            | RangerFavoredEnemyTypeElementals
920            | RangerFavoredEnemyTypeFey
921            | RangerFavoredEnemyTypeFiends
922            | RangerFavoredEnemyTypeGiants
923            | RangerFavoredEnemyTypeMonstrosities
924            | RangerFavoredEnemyTypeOozes
925            | RangerFavoredEnemyTypePlants
926            | RangerFavoredEnemyTypeUndead
927            | RangerFavoredEnemyTypeHumanoids => {
928                self.1
929                    .ranger_favored_enemy_type
930                    .get_or_insert_with(Vec::new)
931                    .push(option.as_index_str().to_string());
932            }
933        }
934    }
935
936    fn increase_score(&mut self, option: ChoosableCustomLevelFeatureOption) {
937        match option {
938            ChoosableCustomLevelFeatureOption::StrengthPlusOne => {
939                self.1.abilities_modifiers.strength.score += 1;
940            }
941            ChoosableCustomLevelFeatureOption::DexterityPlusOne => {
942                self.1.abilities_modifiers.dexterity.score += 1;
943            }
944            ChoosableCustomLevelFeatureOption::ConstitutionPlusOne => {
945                self.1.abilities_modifiers.constitution.score += 1;
946            }
947            ChoosableCustomLevelFeatureOption::IntelligencePlusOne => {
948                self.1.abilities_modifiers.intelligence.score += 1;
949            }
950            ChoosableCustomLevelFeatureOption::WisdomPlusOne => {
951                self.1.abilities_modifiers.wisdom.score += 1;
952            }
953            ChoosableCustomLevelFeatureOption::CharismaPlusOne => {
954                self.1.abilities_modifiers.charisma.score += 1;
955            }
956            _ => {}
957        }
958    }
959
960    fn set_proficiency(&mut self, option: ChoosableCustomLevelFeatureOption) {
961        match option {
962            ChoosableCustomLevelFeatureOption::BardProficiencyStrength => {
963                self.1.abilities_modifiers.strength.proficiency = true;
964            }
965            ChoosableCustomLevelFeatureOption::BardProficiencyDexterity => {
966                self.1.abilities_modifiers.dexterity.proficiency = true;
967            }
968            ChoosableCustomLevelFeatureOption::BardProficiencyConstitution => {
969                self.1.abilities_modifiers.constitution.proficiency = true;
970            }
971            ChoosableCustomLevelFeatureOption::BardProficiencyIntelligence => {
972                self.1.abilities_modifiers.intelligence.proficiency = true;
973            }
974            ChoosableCustomLevelFeatureOption::BardProficiencyWisdom => {
975                self.1.abilities_modifiers.wisdom.proficiency = true;
976            }
977            ChoosableCustomLevelFeatureOption::BardProficiencyCharisma => {
978                self.1.abilities_modifiers.charisma.proficiency = true;
979            }
980            _ => {}
981        }
982    }
983}
984
985pub async fn get_spellcasting_slots(
986    index: &str,
987    level: u8,
988) -> Result<Option<LevelSpellcasting>, ApiError> {
989    let op = SpellcastingQuery::build(SpellcastingQueryVariables {
990        index: Some(format!("{}-{}", index, level)),
991    });
992
993    let spellcasting_slots = Client::new()
994        .post(GRAPHQL_API_URL.as_str())
995        .run_graphql(op)
996        .await?
997        .data
998        .ok_or(ApiError::Schema)?
999        .level
1000        .ok_or(ApiError::Schema)?
1001        .spellcasting;
1002
1003    Ok(spellcasting_slots)
1004}