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