dnd_character/api/
classes.rs

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