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