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