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