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.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 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}