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