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 | "destroy-undead-cr-1-or-below"
423 | "destroy-undead-cr-2-or-below"
424 | "destroy-undead-cr-3-or-below"
425 | "destroy-undead-cr-4-or-below"
426 | "destroy-undead-cr-1-2-or-below"
427 | "disciple-of-life"
428 | "divine-health"
429 | "draconic-resilience"
430 | "dragon-wings"
431 | "draconic-presence"
432 | "font-of-magic"
433 | "dragon-ancestor-black---acid-damage"
434 | "dragon-ancestor-blue---lightning-damage"
435 | "dragon-ancestor-brass---fire-damage"
436 | "dragon-ancestor-bronze---lightning-damage"
437 | "dragon-ancestor-copper---acid-damage"
438 | "dragon-ancestor-gold---fire-damage"
439 | "dragon-ancestor-green---poison-damage"
440 | "dragon-ancestor-red---fire-damage"
441 | "dragon-ancestor-silver---cold-damage"
442 | "dragon-ancestor-white---cold-damage"
443 | "druid-lands-stride"
444 | "druid-timeless-body"
445 | "druidic"
446 | "elusive"
447 | "empowered-evocation"
448 | "elemental-affinity"
449 | "fast-movement"
450 | "favored-enemy-1-type"
451 | "favored-enemy-2-types"
452 | "favored-enemy-3-enemies"
453 | "feral-instinct"
454 | "feral-senses"
455 | "fighter-fighting-style-archery"
456 | "fighter-fighting-style-protection"
457 | "fighter-fighting-style-defense"
458 | "fighter-fighting-style-dueling"
459 | "fighter-fighting-style-great-weapon-fighting"
460 | "fighter-fighting-style-two-weapon-fighting"
461 | "fighting-style-defense"
462 | "fighting-style-dueling"
463 | "fighting-style-great-weapon-fighting"
464 | "foe-slayer"
465 | "hurl-through-hell"
466 | "improved-critical"
467 | "improved-divine-smite"
468 | "indomitable-1-use"
469 | "indomitable-2-uses"
470 | "indomitable-3-uses"
471 | "indomitable-might"
472 | "ki-empowered-strikes"
473 | "jack-of-all-trades"
474 | "martial-arts"
475 | "monk-evasion"
476 | "monk-timeless-body"
477 | "natural-explorer-1-terrain-type"
478 | "natural-explorer-2-terrain-types"
479 | "natural-explorer-3-terrain-types"
480 | "purity-of-body"
481 | "purity-of-spirit"
482 | "natures-sanctuary"
483 | "natures-ward"
484 | "sculpt-spells"
485 | "ranger-lands-stride"
486 | "relentless-rage"
487 | "reliable-talent"
488 | "remarkable-athlete"
489 | "rogue-evasion"
490 | "superior-critical"
491 | "superior-inspiration"
492 | "supreme-healing"
493 | "supreme-sneak"
494 | "survivor"
495 | "thiefs-reflexes"
496 | "thieves-cant"
497 | "tongue-of-the-sun-and-moon"
498 | "tranquility"
499 | "unarmored-movement-1"
500 | "unarmored-movement-2"
501 | "use-magic-device"
502 | "wild-shape-cr-1-2-or-below-no-flying-speed"
503 | "wild-shape-cr-1-4-or-below-no-flying-or-swim-speed"
504 | "wild-shape-cr-1-or-below"
505 | "ki"
506 | "monk-unarmored-defense"
507 | "perfect-self"
508 | "slippery-mind"
509 | "mindless-rage"
510 | "barbarian-unarmored-defense"
511 | "divine-intervention-improvement"
512 | "persistent-rage"
513 | "evocation-savant"
514 | "overchannel"
515 | "potent-cantrip"
516 | "second-story-work"
517 | "primeval-awareness"
518 | "beast-spells" => Some(Passive),
519 "oath-spells" => Some(Ignored),
521 x if x.starts_with("bard-expertise-") || x.starts_with("rogue-expertise-") => {
523 Some(Ignored)
524 } x if x.starts_with("spellcasting-") => Some(Ignored),
526 x if x.starts_with("eldritch-invocation-") => Some(Ignored),
528 x if x.starts_with("circle-spells-") => Some(Ignored),
530 x if x.starts_with("circle-of-the-land-") => Some(Ignored),
532 x if x.starts_with("domain-spells-") => Some(Ignored),
534 x if x.starts_with("hunters-prey") => Some(Choosable(HuntersPrey)),
535 x if x.starts_with("defensive-tactics") => Some(Choosable(DefensiveTactics)),
536 x if x.starts_with("multiattack") => Some(Choosable(Multiattack)),
537 x if x.starts_with("ranger-fighting-style") => Some(Choosable(RangerFightingStyle)),
538 x if x.starts_with("superior-hunters-defense") => {
539 Some(Choosable(SuperiorHuntersDefense))
540 }
541 x if x.contains("ability-score-improvement") => {
542 Some(Choosable(AbilityScoreImprovement))
543 }
544 _ => None,
545 }
546 }
547}
548
549impl Classes {
550 pub(super) async fn new_day(&mut self) {
551 futures::stream::iter(self.0.values_mut())
552 .for_each_concurrent(None, |class| class.new_day())
553 .await;
554 }
555}
556
557impl Class {
558 pub(super) async fn new_day(&mut self) {
559 use crate::classes::ClassSpellCasting::*;
560
561 let index = self.index().to_string();
562
563 if let Some(spell_casting) = &mut self.1.spell_casting {
564 match spell_casting {
565 KnowledgePrepared {
566 pending_preparation,
567 spells_prepared_index,
568 ..
569 }
570 | AlreadyKnowPrepared {
571 pending_preparation,
572 spells_prepared_index,
573 ..
574 } => {
575 *pending_preparation = true;
576 spells_prepared_index.clear();
577 }
578 KnowledgeAlreadyPrepared { usable_slots, .. } => {
579 if let Ok(Some(spellcasting_slots)) =
580 get_spellcasting_slots(index.as_str(), self.1.level).await
581 {
582 *usable_slots = spellcasting_slots.into();
583 }
584 }
585 }
586 }
587 }
588
589 pub async fn get_spellcasting_ability_index(&self) -> Result<String, ApiError> {
590 let op = SpellcastingAbilityQuery::build(SpellcastingAbilityQueryVariables {
591 index: Some(self.index().to_string()),
592 });
593
594 let ability_index = Client::new()
595 .post(GRAPHQL_API_URL.as_str())
596 .run_graphql(op)
597 .await?
598 .data
599 .ok_or(ApiError::Schema)?
600 .class
601 .ok_or(ApiError::Schema)?
602 .spellcasting
603 .ok_or(ApiError::Schema)?
604 .spellcasting_ability
605 .index;
606
607 Ok(ability_index)
608 }
609
610 pub async fn get_spellcasting_slots(&self) -> Result<Option<LevelSpellcasting>, ApiError> {
611 get_spellcasting_slots(self.index(), self.1.level).await
612 }
613
614 pub async fn set_level(
615 &mut self,
616 new_level: u8,
617 ) -> Result<Vec<ChoosableCustomLevelFeature>, ApiError> {
618 let op = LevelFeaturesQuery::build(LevelFeaturesQueryVariables {
619 class: Some(StringFilter(self.index().to_string())),
620 level: Some(LevelFilter {
621 gt: Some(self.1.level),
622 lte: Some(new_level),
623 gte: None,
624 }),
625 });
626
627 let features = Client::new()
628 .post(GRAPHQL_API_URL.as_str())
629 .run_graphql(op)
630 .await?
631 .data
632 .ok_or(ApiError::Schema)?
633 .features
634 .ok_or(ApiError::Schema)?;
635
636 let mut pending_features = vec![];
637
638 features
639 .iter()
640 .filter_map(|feature| CustomLevelFeatureType::identify(feature.index.clone()))
641 .for_each(|feature| match feature {
642 CustomLevelFeatureType::Passive => {}
643 CustomLevelFeatureType::Choosable(feature) => {
644 pending_features.push(feature);
645 }
646 CustomLevelFeatureType::Sheet(feature) => match feature {
647 SheetLevelFeatureType::PrimalChampion => {
648 self.1.abilities_modifiers.strength.score += 4;
649 self.1.abilities_modifiers.dexterity.score += 4;
650 }
651 },
652 Ignored => {}
653 });
654
655 self.1.level = new_level;
656
657 Ok(pending_features)
658 }
659
660 pub async fn get_levels_features(
661 &self,
662 from_level: Option<u8>,
663 passive: bool,
664 ) -> Result<Vec<String>, ApiError> {
665 let op = LevelFeaturesQuery::build(LevelFeaturesQueryVariables {
666 class: Some(StringFilter(self.index().to_string())),
667 level: Some(LevelFilter {
668 gte: Some(from_level.unwrap_or(0)),
669 lte: Some(self.1.level),
670 gt: None,
671 }),
672 });
673
674 let features = Client::new()
675 .post(GRAPHQL_API_URL.as_str())
676 .run_graphql(op)
677 .await?
678 .data
679 .ok_or(ApiError::Schema)?
680 .features
681 .ok_or(ApiError::Schema)?;
682
683 let mut features: Vec<String> = features
685 .into_iter()
686 .filter(
687 |feature| match CustomLevelFeatureType::identify(feature.index.clone()) {
688 None => true,
689 Some(custom_type) => match custom_type {
690 CustomLevelFeatureType::Passive => passive,
691 _ => false,
692 },
693 },
694 )
695 .map(|feature| feature.index)
696 .collect();
697
698 let features: Vec<String> = {
699 lazy_static! {
700 static ref CR_REGEX: regex::Regex =
701 regex::Regex::new(r"destroy-undead-cr-([0-9]+(?:-[0-9]+)?)\-or-below").unwrap();
702 }
703
704 let mut found = false;
705
706 features
707 .iter_mut()
708 .rev()
709 .filter(|feature| {
710 if CR_REGEX.is_match(feature) {
711 if found {
712 false
713 } else {
714 found = true;
715 true
716 }
717 } else {
718 true
719 }
720 })
721 .map(|feature| feature.clone())
722 .collect()
723 };
724
725 lazy_static! {
726 static ref DICE_REGEX: regex::Regex = regex::Regex::new(r"^(.+)-d(\d+)$").unwrap();
727 }
728
729 let mut grouped_features: HashMap<String, u32> = HashMap::new();
730 for feature in &features {
731 if let Some(caps) = DICE_REGEX.captures(feature) {
732 if caps.len() == 3 {
733 let prefix = caps.get(1).unwrap().as_str().to_string();
734 let dice_value = caps.get(2).unwrap().as_str().parse::<u32>().unwrap();
735
736 let current_max = grouped_features.entry(prefix).or_insert(0);
737 if dice_value > *current_max {
738 *current_max = dice_value;
739 }
740 }
741 }
742 }
743
744 let mut features: Vec<String> = features
745 .into_iter()
746 .filter(|feature| {
747 if let Some(caps) = DICE_REGEX.captures(feature) {
748 let prefix = caps.get(1).unwrap().as_str();
749 let dice_value = caps
750 .get(2)
751 .unwrap()
752 .as_str()
753 .parse::<u32>()
754 .expect("Parsing dice value");
755
756 if let Some(&max_dice) = grouped_features.get(prefix) {
757 return dice_value == max_dice;
758 }
759 }
760 true
761 })
762 .collect();
763
764 if !passive {
766 if let Some(multiattack) = &self.1.multiattack {
767 features.push(multiattack.clone());
768 }
769 if let Some(hunters_prey) = &self.1.hunters_prey {
770 features.push(hunters_prey.clone());
771 }
772 }
773
774 Ok(features)
775 }
776
777 pub fn apply_option(&mut self, option: ChoosableCustomLevelFeatureOption) {
778 use ChoosableCustomLevelFeatureOption::*;
779
780 match option {
781 StrengthPlusOne | DexterityPlusOne | ConstitutionPlusOne | IntelligencePlusOne
782 | WisdomPlusOne | CharismaPlusOne => self.increase_score(option),
783 BardProficiencyStrength
784 | BardProficiencyDexterity
785 | BardProficiencyConstitution
786 | BardProficiencyIntelligence
787 | BardProficiencyWisdom
788 | BardProficiencyCharisma => self.set_proficiency(option),
789 PactOfTheChain | PactOfTheBlade | PactOfTheTome => {
790 println!("Pact of the Chain, Blade or Tome not yet implemented");
791 }
792 HuntersPreyGiantKiller | HuntersPreyHordeBreaker | HuntersPreyColossusSlayer => {
793 self.1
794 .hunters_prey
795 .replace(option.as_index_str().to_string());
796 }
797 DefensiveTacticsSteelWill
798 | DefensiveTacticsEscapeTheHorde
799 | DefensiveTacticsMultiattackDefense => {
800 self.1
801 .defensive_tactics
802 .replace(option.as_index_str().to_string());
803 }
804 FighterFightingStyleArchery
805 | FighterFightingStyleDefense
806 | FighterFightingStyleDueling
807 | FighterFightingStyleGreatWeaponFighting
808 | FighterFightingStyleProtection
809 | FighterFightingStyleTwoWeaponFighting
810 | RangerFightingStyleArchery
811 | RangerFightingStyleDefense
812 | RangerFightingStyleDueling
813 | RangerFightingStyleTwoWeaponFighting
814 | FightingStyleDefense
815 | FightingStyleDueling
816 | FightingStyleGreatWeaponFighting
817 | FightingStyleProtection => {
818 if self.1.fighting_style.is_none() {
819 self.1
820 .fighting_style
821 .replace(option.as_index_str().to_string());
822 } else {
823 self.1
824 .additional_fighting_style
825 .replace(option.as_index_str().to_string());
826 }
827 }
828 MultiattackVolley | MultiattackWhirlwindAttack => {
829 self.1
830 .multiattack
831 .replace(option.as_index_str().to_string());
832 }
833 SuperiorHuntersDefenseEvasion
834 | SuperiorHuntersDefenseStandAgainstTheTide
835 | SuperiorHuntersDefenseUncannyDodge => {
836 self.1
837 .superior_hunters_defense
838 .replace(option.as_index_str().to_string());
839 }
840 }
841 }
842
843 fn increase_score(&mut self, option: ChoosableCustomLevelFeatureOption) {
844 match option {
845 ChoosableCustomLevelFeatureOption::StrengthPlusOne => {
846 self.1.abilities_modifiers.strength.score += 1;
847 }
848 ChoosableCustomLevelFeatureOption::DexterityPlusOne => {
849 self.1.abilities_modifiers.dexterity.score += 1;
850 }
851 ChoosableCustomLevelFeatureOption::ConstitutionPlusOne => {
852 self.1.abilities_modifiers.constitution.score += 1;
853 }
854 ChoosableCustomLevelFeatureOption::IntelligencePlusOne => {
855 self.1.abilities_modifiers.intelligence.score += 1;
856 }
857 ChoosableCustomLevelFeatureOption::WisdomPlusOne => {
858 self.1.abilities_modifiers.wisdom.score += 1;
859 }
860 ChoosableCustomLevelFeatureOption::CharismaPlusOne => {
861 self.1.abilities_modifiers.charisma.score += 1;
862 }
863 _ => {}
864 }
865 }
866
867 fn set_proficiency(&mut self, option: ChoosableCustomLevelFeatureOption) {
868 match option {
869 ChoosableCustomLevelFeatureOption::BardProficiencyStrength => {
870 self.1.abilities_modifiers.strength.proficiency = true;
871 }
872 ChoosableCustomLevelFeatureOption::BardProficiencyDexterity => {
873 self.1.abilities_modifiers.dexterity.proficiency = true;
874 }
875 ChoosableCustomLevelFeatureOption::BardProficiencyConstitution => {
876 self.1.abilities_modifiers.constitution.proficiency = true;
877 }
878 ChoosableCustomLevelFeatureOption::BardProficiencyIntelligence => {
879 self.1.abilities_modifiers.intelligence.proficiency = true;
880 }
881 ChoosableCustomLevelFeatureOption::BardProficiencyWisdom => {
882 self.1.abilities_modifiers.wisdom.proficiency = true;
883 }
884 ChoosableCustomLevelFeatureOption::BardProficiencyCharisma => {
885 self.1.abilities_modifiers.charisma.proficiency = true;
886 }
887 _ => {}
888 }
889 }
890}
891
892pub async fn get_spellcasting_slots(
893 index: &str,
894 level: u8,
895) -> Result<Option<LevelSpellcasting>, ApiError> {
896 let op = SpellcastingQuery::build(SpellcastingQueryVariables {
897 index: Some(format!("{}-{}", index, level)),
898 });
899
900 let spellcasting_slots = Client::new()
901 .post(GRAPHQL_API_URL.as_str())
902 .run_graphql(op)
903 .await?
904 .data
905 .ok_or(ApiError::Schema)?
906 .level
907 .ok_or(ApiError::Schema)?
908 .spellcasting;
909
910 Ok(spellcasting_slots)
911}