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