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