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![
433                    vec![
434                        MetamagicCarefullSpell,
435                        MetamagicDistantSpell,
436                        MetamagicEmpoweredSpell,
437                        MetamagicExtendedSpell,
438                        MetamagicHeightenedSpell,
439                        MetamagicQuickenedSpell,
440                        MetamagicSubtleSpell,
441                        MetamagicCarefullSpell,
442                        MetamagicTwinnedSpell,
443                    ],
444                    vec![
445                        MetamagicCarefullSpell,
446                        MetamagicDistantSpell,
447                        MetamagicEmpoweredSpell,
448                        MetamagicExtendedSpell,
449                        MetamagicHeightenedSpell,
450                        MetamagicQuickenedSpell,
451                        MetamagicSubtleSpell,
452                        MetamagicCarefullSpell,
453                        MetamagicTwinnedSpell,
454                    ],
455                ]
456            }
457            ChoosableCustomLevelFeature::DragonAncestor => {
458                vec![vec![
459                    DragonAncestorBlackAcidDamage,
460                    DragonAncestorBlueLightningDamage,
461                    DragonAncestorBrassFireDamage,
462                    DragonAncestorBronzeLightningDamage,
463                    DragonAncestorCopperAcidDamage,
464                    DragonAncestorGoldFireDamage,
465                    DragonAncestorGreenPoisonDamage,
466                    DragonAncestorRedFireDamage,
467                    DragonAncestorSilverColdDamage,
468                    DragonAncestorWhiteColdDamage,
469                ]]
470            }
471        }
472    }
473}
474
475pub enum SheetLevelFeatureType {
476    /// https://www.dnd5eapi.co/api/features/primal-champion
477    PrimalChampion,
478}
479
480pub enum CustomLevelFeatureType {
481    Choosable(ChoosableCustomLevelFeature),
482    Sheet(SheetLevelFeatureType),
483    Passive,
484    Ignored,
485}
486
487impl CustomLevelFeatureType {
488    pub fn identify(index: String) -> Option<CustomLevelFeatureType> {
489        use ChoosableCustomLevelFeature::*;
490        use CustomLevelFeatureType::*;
491        use SheetLevelFeatureType::*;
492        match index.as_str() {
493            // Ignore all subclass choices since we have only one subclass per class
494            "bard-college"
495            | "divine-domain"
496            | "monastic-tradition"
497            | "sacred-oath"
498            | "ranger-archetype"
499            | "sorcerous-origin"
500            | "druid-circle"
501            | "primal-path"
502            | "martial-archetype"
503            | "otherworldly-patron" => Some(Ignored),
504            "pact-boon" => Some(Choosable(WarlockPact)),
505            "additional-fighting-style" => Some(Choosable(AdditionalFighterFightingStyle)),
506            "fighter-fighting-style" => Some(Choosable(FighterFightingStyle)),
507            "bonus-proficiency" => Some(Passive),
508            "additional-magical-secrets" | "bonus-cantrip" => Some(Ignored),
509            "channel-divinity-1-rest" | "channel-divinity-2-rest" | "channel-divinity-3-rest" => {
510                Some(Ignored)
511            }
512            //"magical-secrets-1" | "magical-secrets-2" | "magical-secrets-3" => Some(Choosable(ChooseTwoSpellForAnyClass)), TODO: Implement this
513            "magical-secrets-1" | "magical-secrets-2" | "magical-secrets-3" => Some(Ignored),
514            "mystic-arcanum-6th-level"
515            | "mystic-arcanum-7th-level"
516            | "mystic-arcanum-8th-level"
517            | "mystic-arcanum-9th-level" => Some(Choosable(ChooseOne6thLevelSpellFromWarlockList)),
518            "paladin-fighting-style" => Some(Choosable(PaladinFightingStyle)),
519            "primal-champion" => Some(Sheet(PrimalChampion)),
520            // TODO: Implement https://www.dnd5eapi.co/api/features/diamond-soul
521            "diamond-soul" => Some(Passive),
522            "arcane-recovery"
523            | "arcane-tradition"
524            | "archdruid"
525            | "aura-improvements"
526            | "aura-of-courage"
527            | "aura-of-devotion"
528            | "aura-of-protection"
529            | "blessed-healer"
530            | "blindsense"
531            | "brutal-critical-1-dice"
532            | "brutal-critical-2-dice"
533            | "brutal-critical-3-dice"
534            | "danger-sense"
535            | "dark-ones-blessing"
536            | "dark-ones-own-luck"
537            | "destroy-undead-cr-1-or-below"
538            | "destroy-undead-cr-2-or-below"
539            | "destroy-undead-cr-3-or-below"
540            | "destroy-undead-cr-4-or-below"
541            | "destroy-undead-cr-1-2-or-below"
542            | "disciple-of-life"
543            | "divine-health"
544            | "draconic-resilience"
545            | "draconic-presence"
546            | "font-of-magic"
547            | "druid-lands-stride"
548            | "druid-timeless-body"
549            | "druidic"
550            | "elusive"
551            | "empowered-evocation"
552            | "fast-movement"
553            | "feral-instinct"
554            | "feral-senses"
555            | "fighter-fighting-style-archery"
556            | "fighter-fighting-style-protection"
557            | "fighter-fighting-style-defense"
558            | "fighter-fighting-style-dueling"
559            | "fighter-fighting-style-great-weapon-fighting"
560            | "fighter-fighting-style-two-weapon-fighting"
561            | "fighting-style-defense"
562            | "fighting-style-dueling"
563            | "fighting-style-great-weapon-fighting"
564            | "foe-slayer"
565            | "hurl-through-hell"
566            | "improved-critical"
567            | "improved-divine-smite"
568            | "indomitable-1-use"
569            | "indomitable-2-uses"
570            | "indomitable-3-uses"
571            | "indomitable-might"
572            | "ki-empowered-strikes"
573            | "jack-of-all-trades"
574            | "martial-arts"
575            | "monk-evasion"
576            | "monk-timeless-body"
577            | "purity-of-body"
578            | "purity-of-spirit"
579            | "natures-sanctuary"
580            | "natures-ward"
581            | "sculpt-spells"
582            | "ranger-lands-stride"
583            | "relentless-rage"
584            | "reliable-talent"
585            | "remarkable-athlete"
586            | "rogue-evasion"
587            | "superior-critical"
588            | "superior-inspiration"
589            | "supreme-healing"
590            | "supreme-sneak"
591            | "survivor"
592            | "thiefs-reflexes"
593            | "thieves-cant"
594            | "tongue-of-the-sun-and-moon"
595            | "tranquility"
596            | "unarmored-movement-1"
597            | "unarmored-movement-2"
598            | "use-magic-device"
599            | "wild-shape-cr-1-2-or-below-no-flying-speed"
600            | "wild-shape-cr-1-4-or-below-no-flying-or-swim-speed"
601            | "wild-shape-cr-1-or-below"
602            | "ki"
603            | "monk-unarmored-defense"
604            | "perfect-self"
605            | "slippery-mind"
606            | "mindless-rage"
607            | "barbarian-unarmored-defense"
608            | "divine-intervention-improvement"
609            | "persistent-rage"
610            | "evocation-savant"
611            | "overchannel"
612            | "potent-cantrip"
613            | "font-of-inspiration"
614            | "second-story-work"
615            | "primeval-awareness"
616            | "beast-spells" => Some(Passive),
617            // ignored until implementation?
618            "oath-spells" => Some(Ignored),
619            x if x.starts_with("metamagic-") => {
620                if x.len() == 11 {
621                    Some(Choosable(Metamagic))
622                } else {
623                    Some(Ignored)
624                }
625            }
626            "hunters-prey" => Some(Choosable(HuntersPrey)),
627            x if x.starts_with("hunters-prey-") => Some(Ignored),
628            "superior-hunters-defense" => Some(Choosable(SuperiorHuntersDefense)),
629            x if x.starts_with("superior-hunters-defenese-") => Some(Ignored),
630            //x if x.starts_with("bard-expertise-") || x.starts_with("rogue-expertise-") => Some(Choosable(MultiplyTwoSkillProficiency)),
631            x if x.starts_with("bard-expertise-") || x.starts_with("rogue-expertise-") => {
632                Some(Ignored)
633            } // TODO: Implement this
634            x if x.starts_with("spellcasting-") => Some(Ignored),
635            // Ignore all eldritch invocations since they are unlocked using invocation known table
636            x if x.starts_with("eldritch-invocation-") => Some(Ignored),
637            // Ignore all circle-spells until implementation
638            x if x.starts_with("circle-spells-") => Some(Ignored),
639            // Ignore all circle of the land until implementation
640            x if x.starts_with("circle-of-the-land-") => Some(Ignored),
641            // Ignore all domain spells until implementation
642            x if x.starts_with("domain-spells-") => Some(Ignored),
643            // ki points not yet implemented
644            x if x.starts_with("flexible-casting-") => Some(Ignored),
645            "dragon-ancestor" => Some(Choosable(DragonAncestor)),
646            x if x.starts_with("dragon-ancestor-") => Some(Ignored),
647            "defensive-tactics" => Some(Choosable(DefensiveTactics)),
648            x if x.starts_with("defensive-tactics-") => Some(Ignored),
649            "multiattack" => Some(Choosable(Multiattack)),
650            x if x.starts_with("multiattack-") => Some(Ignored),
651            "ranger-fighting-style" => Some(Choosable(RangerFightingStyle)),
652            x if x.starts_with("ranger-fighting-style-") => Some(Ignored),
653            x if x.starts_with("favored-enemy-") => Some(Choosable(RangerFavoredEnemyType)),
654            x if x.starts_with("natural-explorer-") => Some(Choosable(RangerTerrainType)),
655            x if x.contains("ability-score-improvement") => {
656                Some(Choosable(AbilityScoreImprovement))
657            }
658            _ => None,
659        }
660    }
661}
662
663impl Classes {
664    pub(super) async fn new_day(&mut self) {
665        futures::stream::iter(self.0.values_mut())
666            .for_each_concurrent(None, |class| class.new_day())
667            .await;
668    }
669}
670
671impl Class {
672    pub(super) async fn new_day(&mut self) {
673        use crate::classes::ClassSpellCasting::*;
674
675        let index = self.index().to_string();
676
677        if let Some(spell_casting) = &mut self.1.spell_casting {
678            match spell_casting {
679                KnowledgePrepared {
680                    pending_preparation,
681                    spells_prepared_index,
682                    ..
683                }
684                | AlreadyKnowPrepared {
685                    pending_preparation,
686                    spells_prepared_index,
687                    ..
688                } => {
689                    *pending_preparation = true;
690                    spells_prepared_index.clear();
691                }
692                KnowledgeAlreadyPrepared { usable_slots, .. } => {
693                    if let Ok(Some(spellcasting_slots)) =
694                        get_spellcasting_slots(index.as_str(), self.1.level).await
695                    {
696                        *usable_slots = spellcasting_slots.into();
697                    }
698                }
699            }
700        }
701    }
702
703    pub async fn get_spellcasting_ability_index(&self) -> Result<String, ApiError> {
704        let op = SpellcastingAbilityQuery::build(SpellcastingAbilityQueryVariables {
705            index: Some(self.index().to_string()),
706        });
707
708        let ability_index = Client::new()
709            .post(GRAPHQL_API_URL.as_str())
710            .run_graphql(op)
711            .await?
712            .data
713            .ok_or(ApiError::Schema)?
714            .class
715            .ok_or(ApiError::Schema)?
716            .spellcasting
717            .ok_or(ApiError::Schema)?
718            .spellcasting_ability
719            .index;
720
721        Ok(ability_index)
722    }
723
724    pub async fn get_spellcasting_slots(&self) -> Result<Option<LevelSpellcasting>, ApiError> {
725        get_spellcasting_slots(self.index(), self.1.level).await
726    }
727
728    pub async fn set_level(
729        &mut self,
730        new_level: u8,
731    ) -> Result<Vec<ChoosableCustomLevelFeature>, ApiError> {
732        let op = LevelFeaturesQuery::build(LevelFeaturesQueryVariables {
733            class: Some(StringFilter(self.index().to_string())),
734            level: Some(LevelFilter {
735                gt: Some(self.1.level),
736                lte: Some(new_level),
737                gte: None,
738            }),
739        });
740
741        let features = Client::new()
742            .post(GRAPHQL_API_URL.as_str())
743            .run_graphql(op)
744            .await?
745            .data
746            .ok_or(ApiError::Schema)?
747            .features
748            .ok_or(ApiError::Schema)?;
749
750        let mut pending_features = vec![];
751
752        features
753            .iter()
754            .filter_map(|feature| CustomLevelFeatureType::identify(feature.index.clone()))
755            .for_each(|feature| match feature {
756                CustomLevelFeatureType::Passive => {}
757                CustomLevelFeatureType::Choosable(feature) => {
758                    pending_features.push(feature);
759                }
760                CustomLevelFeatureType::Sheet(feature) => match feature {
761                    SheetLevelFeatureType::PrimalChampion => {
762                        self.1.abilities_modifiers.strength.score += 4;
763                        self.1.abilities_modifiers.dexterity.score += 4;
764                    }
765                },
766                Ignored => {}
767            });
768
769        self.1.level = new_level;
770
771        Ok(pending_features)
772    }
773
774    pub async fn get_levels_features(
775        &self,
776        from_level: Option<u8>,
777        passive: bool,
778    ) -> Result<Vec<String>, ApiError> {
779        let op = LevelFeaturesQuery::build(LevelFeaturesQueryVariables {
780            class: Some(StringFilter(self.index().to_string())),
781            level: Some(LevelFilter {
782                gte: Some(from_level.unwrap_or(0)),
783                lte: Some(self.1.level),
784                gt: None,
785            }),
786        });
787
788        let features = Client::new()
789            .post(GRAPHQL_API_URL.as_str())
790            .run_graphql(op)
791            .await?
792            .data
793            .ok_or(ApiError::Schema)?
794            .features
795            .ok_or(ApiError::Schema)?;
796
797        // Remove all identifiable features
798        let mut features: Vec<String> = features
799            .into_iter()
800            .filter(
801                |feature| match CustomLevelFeatureType::identify(feature.index.clone()) {
802                    None => true,
803                    Some(custom_type) => match custom_type {
804                        CustomLevelFeatureType::Passive => passive,
805                        _ => false,
806                    },
807                },
808            )
809            .map(|feature| feature.index)
810            .collect();
811
812        let features: Vec<String> = {
813            lazy_static! {
814                static ref CR_REGEX: regex::Regex =
815                    regex::Regex::new(r"destroy-undead-cr-([0-9]+(?:-[0-9]+)?)\-or-below").unwrap();
816            }
817
818            let mut found = false;
819
820            features
821                .iter_mut()
822                .rev()
823                .filter(|feature| {
824                    if CR_REGEX.is_match(feature) {
825                        if found {
826                            false
827                        } else {
828                            found = true;
829                            true
830                        }
831                    } else {
832                        true
833                    }
834                })
835                .map(|feature| feature.clone())
836                .collect()
837        };
838
839        lazy_static! {
840            static ref DICE_REGEX: regex::Regex = regex::Regex::new(r"^(.+)-d(\d+)$").unwrap();
841        }
842
843        let mut grouped_features: HashMap<String, u32> = HashMap::new();
844        for feature in &features {
845            if let Some(caps) = DICE_REGEX.captures(feature) {
846                if caps.len() == 3 {
847                    let prefix = caps.get(1).unwrap().as_str().to_string();
848                    let dice_value = caps.get(2).unwrap().as_str().parse::<u32>().unwrap();
849
850                    let current_max = grouped_features.entry(prefix).or_insert(0);
851                    if dice_value > *current_max {
852                        *current_max = dice_value;
853                    }
854                }
855            }
856        }
857
858        let mut features: Vec<String> = features
859            .into_iter()
860            .filter(|feature| {
861                if let Some(caps) = DICE_REGEX.captures(feature) {
862                    let prefix = caps.get(1).unwrap().as_str();
863                    let dice_value = caps
864                        .get(2)
865                        .unwrap()
866                        .as_str()
867                        .parse::<u32>()
868                        .expect("Parsing dice value");
869
870                    if let Some(&max_dice) = grouped_features.get(prefix) {
871                        return dice_value == max_dice;
872                    }
873                }
874                true
875            })
876            .collect();
877
878        // Add the selected multiattack feature if it exists and we're not requesting passive features
879        if !passive {
880            if let Some(multiattack) = &self.1.multiattack {
881                features.push(multiattack.clone());
882            }
883            if let Some(hunters_prey) = &self.1.hunters_prey {
884                features.push(hunters_prey.clone());
885            }
886            if let Some(metamagic) = &self.1.sorcerer_metamagic {
887                features.append(&mut metamagic.clone());
888            }
889        }
890
891        Ok(features)
892    }
893
894    pub fn apply_option(&mut self, option: ChoosableCustomLevelFeatureOption) {
895        use ChoosableCustomLevelFeatureOption::*;
896
897        match option {
898            StrengthPlusOne | DexterityPlusOne | ConstitutionPlusOne | IntelligencePlusOne
899            | WisdomPlusOne | CharismaPlusOne => self.increase_score(option),
900            PactOfTheChain | PactOfTheBlade | PactOfTheTome => {
901                println!("Pact of the Chain, Blade or Tome not yet implemented");
902            }
903            HuntersPreyGiantKiller | HuntersPreyHordeBreaker | HuntersPreyColossusSlayer => {
904                self.1
905                    .hunters_prey
906                    .replace(option.as_index_str().to_string());
907            }
908            DefensiveTacticsSteelWill
909            | DefensiveTacticsEscapeTheHorde
910            | DefensiveTacticsMultiattackDefense => {
911                self.1
912                    .defensive_tactics
913                    .replace(option.as_index_str().to_string());
914            }
915            FighterFightingStyleArchery
916            | FighterFightingStyleDefense
917            | FighterFightingStyleDueling
918            | FighterFightingStyleGreatWeaponFighting
919            | FighterFightingStyleProtection
920            | FighterFightingStyleTwoWeaponFighting
921            | RangerFightingStyleArchery
922            | RangerFightingStyleDefense
923            | RangerFightingStyleDueling
924            | RangerFightingStyleTwoWeaponFighting
925            | FightingStyleDefense
926            | FightingStyleDueling
927            | FightingStyleGreatWeaponFighting
928            | FightingStyleProtection => {
929                if self.1.fighting_style.is_none() {
930                    self.1
931                        .fighting_style
932                        .replace(option.as_index_str().to_string());
933                } else {
934                    self.1
935                        .additional_fighting_style
936                        .replace(option.as_index_str().to_string());
937                }
938            }
939            MultiattackVolley | MultiattackWhirlwindAttack => {
940                self.1
941                    .multiattack
942                    .replace(option.as_index_str().to_string());
943            }
944            SuperiorHuntersDefenseEvasion
945            | SuperiorHuntersDefenseStandAgainstTheTide
946            | SuperiorHuntersDefenseUncannyDodge => {
947                self.1
948                    .superior_hunters_defense
949                    .replace(option.as_index_str().to_string());
950            }
951            RangerTerrainTypeArctic
952            | RangerTerrainTypeCoast
953            | RangerTerrainTypeDesert
954            | RangerTerrainTypeForest
955            | RangerTerrainTypeGrassland
956            | RangerTerrainTypeMountain
957            | RangerTerrainTypeSwamp => {
958                self.1
959                    .natural_explorer_terrain_type
960                    .get_or_insert_with(Vec::new)
961                    .push(option.as_index_str().to_string());
962            }
963            RangerFavoredEnemyTypeAberrations
964            | RangerFavoredEnemyTypeBeasts
965            | RangerFavoredEnemyTypeCelestials
966            | RangerFavoredEnemyTypeConstructs
967            | RangerFavoredEnemyTypeDragons
968            | RangerFavoredEnemyTypeElementals
969            | RangerFavoredEnemyTypeFey
970            | RangerFavoredEnemyTypeFiends
971            | RangerFavoredEnemyTypeGiants
972            | RangerFavoredEnemyTypeMonstrosities
973            | RangerFavoredEnemyTypeOozes
974            | RangerFavoredEnemyTypePlants
975            | RangerFavoredEnemyTypeUndead
976            | RangerFavoredEnemyTypeHumanoids => {
977                self.1
978                    .ranger_favored_enemy_type
979                    .get_or_insert_with(Vec::new)
980                    .push(option.as_index_str().to_string());
981            }
982            MetamagicCarefullSpell
983            | MetamagicDistantSpell
984            | MetamagicEmpoweredSpell
985            | MetamagicExtendedSpell
986            | MetamagicHeightenedSpell
987            | MetamagicQuickenedSpell
988            | MetamagicSubtleSpell
989            | MetamagicTwinnedSpell => {
990                self.1
991                    .sorcerer_metamagic
992                    .get_or_insert_with(Vec::new)
993                    .push(option.as_index_str().to_string());
994            }
995            DragonAncestorBlackAcidDamage
996            | DragonAncestorBlueLightningDamage
997            | DragonAncestorBrassFireDamage
998            | DragonAncestorBronzeLightningDamage
999            | DragonAncestorCopperAcidDamage
1000            | DragonAncestorGoldFireDamage
1001            | DragonAncestorGreenPoisonDamage
1002            | DragonAncestorRedFireDamage
1003            | DragonAncestorSilverColdDamage
1004            | DragonAncestorWhiteColdDamage => {
1005                self.1
1006                    .sorcerer_dragon_ancestor
1007                    .replace(option.as_index_str().to_string());
1008            }
1009        }
1010    }
1011
1012    fn increase_score(&mut self, option: ChoosableCustomLevelFeatureOption) {
1013        match option {
1014            ChoosableCustomLevelFeatureOption::StrengthPlusOne => {
1015                self.1.abilities_modifiers.strength.score += 1;
1016            }
1017            ChoosableCustomLevelFeatureOption::DexterityPlusOne => {
1018                self.1.abilities_modifiers.dexterity.score += 1;
1019            }
1020            ChoosableCustomLevelFeatureOption::ConstitutionPlusOne => {
1021                self.1.abilities_modifiers.constitution.score += 1;
1022            }
1023            ChoosableCustomLevelFeatureOption::IntelligencePlusOne => {
1024                self.1.abilities_modifiers.intelligence.score += 1;
1025            }
1026            ChoosableCustomLevelFeatureOption::WisdomPlusOne => {
1027                self.1.abilities_modifiers.wisdom.score += 1;
1028            }
1029            ChoosableCustomLevelFeatureOption::CharismaPlusOne => {
1030                self.1.abilities_modifiers.charisma.score += 1;
1031            }
1032            _ => {}
1033        }
1034    }
1035}
1036
1037pub async fn get_spellcasting_slots(
1038    index: &str,
1039    level: u8,
1040) -> Result<Option<LevelSpellcasting>, ApiError> {
1041    let op = SpellcastingQuery::build(SpellcastingQueryVariables {
1042        index: Some(format!("{}-{}", index, level)),
1043    });
1044
1045    let spellcasting_slots = Client::new()
1046        .post(GRAPHQL_API_URL.as_str())
1047        .run_graphql(op)
1048        .await?
1049        .data
1050        .ok_or(ApiError::Schema)?
1051        .level
1052        .ok_or(ApiError::Schema)?
1053        .spellcasting;
1054
1055    Ok(spellcasting_slots)
1056}