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.contains("ability-score-improvement") => {
522                Some(Choosable(AbilityScoreImprovement))
523            }
524            _ => None,
525        }
526    }
527}
528
529impl Classes {
530    pub(super) async fn new_day(&mut self) {
531        futures::stream::iter(self.0.values_mut())
532            .for_each_concurrent(None, |class| class.new_day())
533            .await;
534    }
535}
536
537impl Class {
538    pub(super) async fn new_day(&mut self) {
539        use crate::classes::ClassSpellCasting::*;
540
541        let index = self.index().to_string();
542
543        if let Some(spell_casting) = &mut self.1.spell_casting {
544            match spell_casting {
545                KnowledgePrepared {
546                    pending_preparation,
547                    spells_prepared_index,
548                    ..
549                }
550                | AlreadyKnowPrepared {
551                    pending_preparation,
552                    spells_prepared_index,
553                    ..
554                } => {
555                    *pending_preparation = true;
556                    spells_prepared_index.clear();
557                }
558                KnowledgeAlreadyPrepared { usable_slots, .. } => {
559                    if let Ok(Some(spellcasting_slots)) =
560                        get_spellcasting_slots(index.as_str(), self.1.level).await
561                    {
562                        *usable_slots = spellcasting_slots.into();
563                    }
564                }
565            }
566        }
567    }
568
569    pub async fn get_spellcasting_ability_index(&self) -> Result<String, ApiError> {
570        let op = SpellcastingAbilityQuery::build(SpellcastingAbilityQueryVariables {
571            index: Some(self.index().to_string()),
572        });
573
574        let ability_index = Client::new()
575            .post(GRAPHQL_API_URL.as_str())
576            .run_graphql(op)
577            .await?
578            .data
579            .ok_or(ApiError::Schema)?
580            .class
581            .ok_or(ApiError::Schema)?
582            .spellcasting
583            .ok_or(ApiError::Schema)?
584            .spellcasting_ability
585            .index;
586
587        Ok(ability_index)
588    }
589
590    pub async fn get_spellcasting_slots(&self) -> Result<Option<LevelSpellcasting>, ApiError> {
591        get_spellcasting_slots(self.index(), self.1.level).await
592    }
593
594    pub async fn set_level(
595        &mut self,
596        new_level: u8,
597    ) -> Result<Vec<ChoosableCustomLevelFeature>, ApiError> {
598        let op = LevelFeaturesQuery::build(LevelFeaturesQueryVariables {
599            class: Some(StringFilter(self.index().to_string())),
600            level: Some(LevelFilter {
601                gt: Some(self.1.level),
602                lte: Some(new_level),
603                gte: None,
604            }),
605        });
606
607        let features = Client::new()
608            .post(GRAPHQL_API_URL.as_str())
609            .run_graphql(op)
610            .await?
611            .data
612            .ok_or(ApiError::Schema)?
613            .features
614            .ok_or(ApiError::Schema)?;
615
616        let mut pending_features = vec![];
617
618        features
619            .iter()
620            .filter_map(|feature| CustomLevelFeatureType::identify(feature.index.clone()))
621            .for_each(|feature| match feature {
622                CustomLevelFeatureType::Passive => {}
623                CustomLevelFeatureType::Choosable(feature) => {
624                    pending_features.push(feature);
625                }
626                CustomLevelFeatureType::Sheet(feature) => match feature {
627                    SheetLevelFeatureType::PrimalChampion => {
628                        self.1.abilities_modifiers.strength.score += 4;
629                        self.1.abilities_modifiers.dexterity.score += 4;
630                    }
631                },
632                Ignored => {}
633            });
634
635        self.1.level = new_level;
636
637        Ok(pending_features)
638    }
639
640    pub async fn get_levels_features(
641        &self,
642        from_level: Option<u8>,
643        passive: bool,
644    ) -> Result<Vec<String>, ApiError> {
645        let op = LevelFeaturesQuery::build(LevelFeaturesQueryVariables {
646            class: Some(StringFilter(self.index().to_string())),
647            level: Some(LevelFilter {
648                gte: Some(from_level.unwrap_or(0)),
649                lte: Some(self.1.level),
650                gt: None,
651            }),
652        });
653
654        let features = Client::new()
655            .post(GRAPHQL_API_URL.as_str())
656            .run_graphql(op)
657            .await?
658            .data
659            .ok_or(ApiError::Schema)?
660            .features
661            .ok_or(ApiError::Schema)?;
662
663        // Remove all identifiable features
664        let mut features: Vec<String> = features
665            .into_iter()
666            .filter(
667                |feature| match CustomLevelFeatureType::identify(feature.index.clone()) {
668                    None => true,
669                    Some(custom_type) => match custom_type {
670                        CustomLevelFeatureType::Passive => passive,
671                        _ => false,
672                    },
673                },
674            )
675            .map(|feature| feature.index)
676            .collect();
677
678        let features: Vec<String> = {
679            lazy_static! {
680                static ref CR_REGEX: regex::Regex =
681                    regex::Regex::new(r"destroy-undead-cr-([0-9]+(?:-[0-9]+)?)\-or-below").unwrap();
682            }
683
684            let mut found = false;
685
686            features
687                .iter_mut()
688                .rev()
689                .filter(|feature| {
690                    if CR_REGEX.is_match(feature) {
691                        if found {
692                            false
693                        } else {
694                            found = true;
695                            true
696                        }
697                    } else {
698                        true
699                    }
700                })
701                .map(|feature| feature.clone())
702                .collect()
703        };
704
705        lazy_static! {
706            static ref DICE_REGEX: regex::Regex = regex::Regex::new(r"^(.+)-d(\d+)$").unwrap();
707        }
708
709        let mut grouped_features: HashMap<String, u32> = HashMap::new();
710        for feature in &features {
711            if let Some(caps) = DICE_REGEX.captures(feature) {
712                if caps.len() == 3 {
713                    let prefix = caps.get(1).unwrap().as_str().to_string();
714                    let dice_value = caps.get(2).unwrap().as_str().parse::<u32>().unwrap();
715
716                    let current_max = grouped_features.entry(prefix).or_insert(0);
717                    if dice_value > *current_max {
718                        *current_max = dice_value;
719                    }
720                }
721            }
722        }
723
724        let features = features
725            .into_iter()
726            .filter(|feature| {
727                if let Some(caps) = DICE_REGEX.captures(feature) {
728                    let prefix = caps.get(1).unwrap().as_str();
729                    let dice_value = caps
730                        .get(2)
731                        .unwrap()
732                        .as_str()
733                        .parse::<u32>()
734                        .expect("Parsing dice value");
735
736                    if let Some(&max_dice) = grouped_features.get(prefix) {
737                        return dice_value == max_dice;
738                    }
739                }
740                true
741            })
742            .collect();
743
744        Ok(features)
745    }
746
747    pub fn apply_option(&mut self, option: ChoosableCustomLevelFeatureOption) {
748        use ChoosableCustomLevelFeatureOption::*;
749
750        match option {
751            StrengthPlusOne | DexterityPlusOne | ConstitutionPlusOne | IntelligencePlusOne
752            | WisdomPlusOne | CharismaPlusOne => self.increase_score(option),
753            BardProficiencyStrength
754            | BardProficiencyDexterity
755            | BardProficiencyConstitution
756            | BardProficiencyIntelligence
757            | BardProficiencyWisdom
758            | BardProficiencyCharisma => self.set_proficiency(option),
759            PactOfTheChain | PactOfTheBlade | PactOfTheTome => {
760                println!("Pact of the Chain, Blade or Tome not yet implemented");
761            }
762            HuntersPreyGiantKiller | HuntersPreyHordeBreaker | HuntersPreyColossusSlayer => {
763                self.1
764                    .hunters_prey
765                    .replace(option.as_index_str().to_string());
766            }
767            DefensiveTacticsSteelWill
768            | DefensiveTacticsEscapeTheHorde
769            | DefensiveTacticsMultiattackDefense => {
770                self.1
771                    .defensive_tactics
772                    .replace(option.as_index_str().to_string());
773            }
774            FighterFightingStyleArchery
775            | FighterFightingStyleDefense
776            | FighterFightingStyleDueling
777            | FighterFightingStyleGreatWeaponFighting
778            | FighterFightingStyleProtection
779            | FighterFightingStyleTwoWeaponFighting
780            | RangerFightingStyleArchery
781            | RangerFightingStyleDefense
782            | RangerFightingStyleDueling
783            | RangerFightingStyleTwoWeaponFighting
784            | FightingStyleDefense
785            | FightingStyleDueling
786            | FightingStyleGreatWeaponFighting
787            | FightingStyleProtection => {
788                if self.1.fighting_style.is_none() {
789                    self.1
790                        .fighting_style
791                        .replace(option.as_index_str().to_string());
792                } else {
793                    self.1
794                        .additional_fighting_style
795                        .replace(option.as_index_str().to_string());
796                }
797            }
798        }
799    }
800
801    fn increase_score(&mut self, option: ChoosableCustomLevelFeatureOption) {
802        match option {
803            ChoosableCustomLevelFeatureOption::StrengthPlusOne => {
804                self.1.abilities_modifiers.strength.score += 1;
805            }
806            ChoosableCustomLevelFeatureOption::DexterityPlusOne => {
807                self.1.abilities_modifiers.dexterity.score += 1;
808            }
809            ChoosableCustomLevelFeatureOption::ConstitutionPlusOne => {
810                self.1.abilities_modifiers.constitution.score += 1;
811            }
812            ChoosableCustomLevelFeatureOption::IntelligencePlusOne => {
813                self.1.abilities_modifiers.intelligence.score += 1;
814            }
815            ChoosableCustomLevelFeatureOption::WisdomPlusOne => {
816                self.1.abilities_modifiers.wisdom.score += 1;
817            }
818            ChoosableCustomLevelFeatureOption::CharismaPlusOne => {
819                self.1.abilities_modifiers.charisma.score += 1;
820            }
821            _ => {}
822        }
823    }
824
825    fn set_proficiency(&mut self, option: ChoosableCustomLevelFeatureOption) {
826        match option {
827            ChoosableCustomLevelFeatureOption::BardProficiencyStrength => {
828                self.1.abilities_modifiers.strength.proficiency = true;
829            }
830            ChoosableCustomLevelFeatureOption::BardProficiencyDexterity => {
831                self.1.abilities_modifiers.dexterity.proficiency = true;
832            }
833            ChoosableCustomLevelFeatureOption::BardProficiencyConstitution => {
834                self.1.abilities_modifiers.constitution.proficiency = true;
835            }
836            ChoosableCustomLevelFeatureOption::BardProficiencyIntelligence => {
837                self.1.abilities_modifiers.intelligence.proficiency = true;
838            }
839            ChoosableCustomLevelFeatureOption::BardProficiencyWisdom => {
840                self.1.abilities_modifiers.wisdom.proficiency = true;
841            }
842            ChoosableCustomLevelFeatureOption::BardProficiencyCharisma => {
843                self.1.abilities_modifiers.charisma.proficiency = true;
844            }
845            _ => {}
846        }
847    }
848}
849
850pub async fn get_spellcasting_slots(
851    index: &str,
852    level: u8,
853) -> Result<Option<LevelSpellcasting>, ApiError> {
854    let op = SpellcastingQuery::build(SpellcastingQueryVariables {
855        index: Some(format!("{}-{}", index, level)),
856    });
857
858    let spellcasting_slots = Client::new()
859        .post(GRAPHQL_API_URL.as_str())
860        .run_graphql(op)
861        .await?
862        .data
863        .ok_or(ApiError::Schema)?
864        .level
865        .ok_or(ApiError::Schema)?
866        .spellcasting;
867
868    Ok(spellcasting_slots)
869}