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