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 AbilityScoreImprovement,
140 HuntersPrey,
142 DefensiveTactics,
144 WarlockPact,
146 AdditionalFighterFightingStyle,
148 FighterFightingStyle,
150 RangerFightingStyle,
152 BonusBardProficiency,
154 MultiplyTwoSkillProficiency,
160 ChooseTwoSpellForAnyClass,
164 ChooseOne6thLevelSpellFromWarlockList,
169 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 vec![vec![]]
303 }
304 ChoosableCustomLevelFeature::ChooseTwoSpellForAnyClass => {
305 vec![vec![]]
307 }
308 ChoosableCustomLevelFeature::ChooseOne6thLevelSpellFromWarlockList => {
309 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 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 "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(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 "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 "oath-spells" => Some(Ignored),
507 x if x.starts_with("bard-expertise-") || x.starts_with("rogue-expertise-") => {
509 Some(Ignored)
510 } x if x.starts_with("spellcasting-") => Some(Ignored),
512 x if x.starts_with("eldritch-invocation-") => Some(Ignored),
514 x if x.starts_with("circle-spells-") => Some(Ignored),
516 x if x.starts_with("circle-of-the-land-") => Some(Ignored),
518 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 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}