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