1use super::shared::schema;
2use crate::GRAPHQL_API_URL;
3use crate::api::classes::CustomLevelFeatureType::Ignored;
4use crate::api::shared::ApiError;
5use crate::classes::{Class, Classes, UsableSlots};
6use cynic::http::ReqwestExt;
7use cynic::{QueryBuilder, impl_scalar};
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 MultiplyTwoSkillProficiency,
159 ChooseTwoSpellForAnyClass,
163 ChooseOne6thLevelSpellFromWarlockList,
168 PaladinFightingStyle,
170 Multiattack,
172 SuperiorHuntersDefense,
174 RangerFavoredEnemyType,
178 RangerTerrainType,
182 Metamagic,
186 EldritchInvocations,
188 DragonAncestor,
190}
191
192#[derive(Clone, Debug)]
193#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
194#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
195pub enum ChoosableCustomLevelFeatureOption {
196 StrengthPlusOne,
197 DexterityPlusOne,
198 ConstitutionPlusOne,
199 IntelligencePlusOne,
200 WisdomPlusOne,
201 CharismaPlusOne,
202
203 PactOfTheChain,
204 PactOfTheBlade,
205 PactOfTheTome,
206
207 FighterFightingStyleArchery,
208 FighterFightingStyleDefense,
209 FighterFightingStyleDueling,
210 FighterFightingStyleGreatWeaponFighting,
211 FighterFightingStyleProtection,
212 FighterFightingStyleTwoWeaponFighting,
213
214 RangerFightingStyleArchery,
215 RangerFightingStyleDefense,
216 RangerFightingStyleDueling,
217 RangerFightingStyleTwoWeaponFighting,
218
219 RangerTerrainTypeArctic,
220 RangerTerrainTypeCoast,
221 RangerTerrainTypeDesert,
222 RangerTerrainTypeForest,
223 RangerTerrainTypeGrassland,
224 RangerTerrainTypeMountain,
225 RangerTerrainTypeSwamp,
226
227 RangerFavoredEnemyTypeAberrations,
228 RangerFavoredEnemyTypeBeasts,
229 RangerFavoredEnemyTypeCelestials,
230 RangerFavoredEnemyTypeConstructs,
231 RangerFavoredEnemyTypeDragons,
232 RangerFavoredEnemyTypeElementals,
233 RangerFavoredEnemyTypeFey,
234 RangerFavoredEnemyTypeFiends,
235 RangerFavoredEnemyTypeGiants,
236 RangerFavoredEnemyTypeMonstrosities,
237 RangerFavoredEnemyTypeOozes,
238 RangerFavoredEnemyTypePlants,
239 RangerFavoredEnemyTypeUndead,
240 RangerFavoredEnemyTypeHumanoids,
241
242 FightingStyleDefense,
243 FightingStyleDueling,
244 FightingStyleGreatWeaponFighting,
245 FightingStyleProtection,
246
247 HuntersPreyGiantKiller,
248 HuntersPreyHordeBreaker,
249 HuntersPreyColossusSlayer,
250
251 DefensiveTacticsSteelWill,
252 DefensiveTacticsEscapeTheHorde,
253 DefensiveTacticsMultiattackDefense,
254
255 MultiattackVolley,
256 MultiattackWhirlwindAttack,
257
258 SuperiorHuntersDefenseEvasion,
259 SuperiorHuntersDefenseStandAgainstTheTide,
260 SuperiorHuntersDefenseUncannyDodge,
261
262 MetamagicCarefulSpell,
263 MetamagicDistantSpell,
264 MetamagicEmpoweredSpell,
265 MetamagicExtendedSpell,
266 MetamagicHeightenedSpell,
267 MetamagicQuickenedSpell,
268 MetamagicSubtleSpell,
269 MetamagicTwinnedSpell,
270
271 EldritchInvocationAgonizingBlast,
272 EldritchInvocationArmorOfShadows,
273 EldritchInvocationAscendantStep,
274 EldritchInvocationBeastSpeech,
275 EldritchInvocationBeguilingInfluence,
276 EldritchInvocationBewitchingWhispers,
277 EldritchInvocationBookOfAncientSecrets,
278 EldritchInvocationChainsOfCarceri,
279 EldritchInvocationDevilsSight,
280 EldritchInvocationDreadfulWord,
281 EldritchInvocationEldritchSight,
282 EldritchInvocationEldritchSpear,
283 EldritchInvocationEyesOfTheRuneKeeper,
284 EldritchInvocationFiendishVigor,
285 EldritchInvocationGazeOfTwoMinds,
286 EldritchInvocationLifedrinker,
287 EldritchInvocationMaskOfManyFaces,
288 EldritchInvocationMasterOfMyriadForms,
289 EldritchInvocationMinionsOfChaos,
290 EldritchInvocationMireTheMind,
291 EldritchInvocationMistyVisions,
292 EldritchInvocationOneWithShadows,
293 EldritchInvocationOtherworldlyLeap,
294 EldritchInvocationRepellingBlast,
295 EldritchInvocationSculptorOfFlesh,
296 EldritchInvocationSignOfIllOmen,
297 EldritchInvocationThiefOfFiveFates,
298 EldritchInvocationThirstingBlade,
299 EldritchInvocationVisionsOfDistantRealms,
300 EldritchInvocationVoiceOfTheChainMaster,
301 EldritchInvocationWhispersOfTheGrave,
302 EldritchInvocationWitchSight,
303
304 #[serde(rename = "dragon-ancestor-black---acid-damage")]
305 DragonAncestorBlackAcidDamage,
306 #[serde(rename = "dragon-ancestor-blue---lightning-damage")]
307 DragonAncestorBlueLightningDamage,
308 #[serde(rename = "dragon-ancestor-brass---fire-damage")]
309 DragonAncestorBrassFireDamage,
310 #[serde(rename = "dragon-ancestor-bronze---lightning-damage")]
311 DragonAncestorBronzeLightningDamage,
312 #[serde(rename = "dragon-ancestor-copper---acid-damage")]
313 DragonAncestorCopperAcidDamage,
314 #[serde(rename = "dragon-ancestor-gold---fire-damage")]
315 DragonAncestorGoldFireDamage,
316 #[serde(rename = "dragon-ancestor-green---poison-damage")]
317 DragonAncestorGreenPoisonDamage,
318 #[serde(rename = "dragon-ancestor-red---fire-damage")]
319 DragonAncestorRedFireDamage,
320 #[serde(rename = "dragon-ancestor-silver---cold-damage")]
321 DragonAncestorSilverColdDamage,
322 #[serde(rename = "dragon-ancestor-white---cold-damage")]
323 DragonAncestorWhiteColdDamage,
324}
325
326impl ChoosableCustomLevelFeatureOption {
327 #[cfg(feature = "serde")]
328 pub fn as_index_str(&self) -> &str {
329 serde_variant::to_variant_name(self).unwrap()
330 }
331
332 #[cfg(feature = "serde")]
333 pub fn from_index_str(index: &str) -> Option<ChoosableCustomLevelFeatureOption> {
334 #[derive(serde::Deserialize)]
335 struct Helper {
336 value: ChoosableCustomLevelFeatureOption,
337 }
338
339 let json = json!({
340 "value": index
341 });
342
343 serde_json::from_value::<Helper>(json)
344 .map(|helper| helper.value)
345 .ok()
346 }
347}
348
349impl ChoosableCustomLevelFeature {
350 #[cfg(feature = "serde")]
351 pub fn as_index_str(&self) -> &str {
352 serde_variant::to_variant_name(self).unwrap()
353 }
354
355 pub fn to_options(&self) -> Vec<Vec<ChoosableCustomLevelFeatureOption>> {
356 use ChoosableCustomLevelFeatureOption::*;
357
358 match self {
359 ChoosableCustomLevelFeature::AbilityScoreImprovement => {
360 let ability_names = vec![
361 StrengthPlusOne,
362 DexterityPlusOne,
363 ConstitutionPlusOne,
364 IntelligencePlusOne,
365 WisdomPlusOne,
366 CharismaPlusOne,
367 ];
368
369 vec![ability_names.clone(), ability_names]
370 }
371 ChoosableCustomLevelFeature::WarlockPact => {
372 vec![vec![PactOfTheChain, PactOfTheBlade, PactOfTheTome]]
373 }
374 ChoosableCustomLevelFeature::AdditionalFighterFightingStyle
375 | ChoosableCustomLevelFeature::FighterFightingStyle => {
376 vec![vec![
377 FighterFightingStyleArchery,
378 FighterFightingStyleDefense,
379 FighterFightingStyleDueling,
380 FighterFightingStyleGreatWeaponFighting,
381 FighterFightingStyleProtection,
382 FighterFightingStyleTwoWeaponFighting,
383 ]]
384 }
385 ChoosableCustomLevelFeature::RangerFightingStyle => {
386 vec![vec![
387 RangerFightingStyleArchery,
388 RangerFightingStyleDefense,
389 RangerFightingStyleDueling,
390 RangerFightingStyleTwoWeaponFighting,
391 ]]
392 }
393 ChoosableCustomLevelFeature::MultiplyTwoSkillProficiency => {
394 vec![vec![]]
396 }
397 ChoosableCustomLevelFeature::ChooseTwoSpellForAnyClass => {
398 vec![vec![]]
400 }
401 ChoosableCustomLevelFeature::ChooseOne6thLevelSpellFromWarlockList => {
402 vec![vec![]]
404 }
405 ChoosableCustomLevelFeature::PaladinFightingStyle => {
406 vec![vec![
407 FightingStyleDefense,
408 FightingStyleDueling,
409 FightingStyleGreatWeaponFighting,
410 FightingStyleProtection,
411 ]]
412 }
413 ChoosableCustomLevelFeature::HuntersPrey => {
414 vec![vec![
415 HuntersPreyGiantKiller,
416 HuntersPreyHordeBreaker,
417 HuntersPreyColossusSlayer,
418 ]]
419 }
420 ChoosableCustomLevelFeature::DefensiveTactics => {
421 vec![vec![
422 DefensiveTacticsSteelWill,
423 DefensiveTacticsEscapeTheHorde,
424 DefensiveTacticsMultiattackDefense,
425 ]]
426 }
427 ChoosableCustomLevelFeature::Multiattack => {
428 vec![vec![MultiattackWhirlwindAttack, MultiattackVolley]]
429 }
430 ChoosableCustomLevelFeature::SuperiorHuntersDefense => {
431 vec![vec![
432 SuperiorHuntersDefenseEvasion,
433 SuperiorHuntersDefenseStandAgainstTheTide,
434 SuperiorHuntersDefenseUncannyDodge,
435 ]]
436 }
437 ChoosableCustomLevelFeature::RangerFavoredEnemyType => {
438 vec![vec![
439 RangerFavoredEnemyTypeAberrations,
440 RangerFavoredEnemyTypeBeasts,
441 RangerFavoredEnemyTypeCelestials,
442 RangerFavoredEnemyTypeConstructs,
443 RangerFavoredEnemyTypeDragons,
444 RangerFavoredEnemyTypeElementals,
445 RangerFavoredEnemyTypeFey,
446 RangerFavoredEnemyTypeFiends,
447 RangerFavoredEnemyTypeGiants,
448 RangerFavoredEnemyTypeMonstrosities,
449 RangerFavoredEnemyTypeOozes,
450 RangerFavoredEnemyTypePlants,
451 RangerFavoredEnemyTypeUndead,
452 ]]
454 }
455 ChoosableCustomLevelFeature::RangerTerrainType => {
456 vec![vec![
457 RangerTerrainTypeArctic,
458 RangerTerrainTypeCoast,
459 RangerTerrainTypeDesert,
460 RangerTerrainTypeForest,
461 RangerTerrainTypeGrassland,
462 RangerTerrainTypeMountain,
463 RangerTerrainTypeSwamp,
464 ]]
465 }
466 ChoosableCustomLevelFeature::Metamagic => {
467 let all_metamagics = vec![
468 MetamagicCarefulSpell,
469 MetamagicDistantSpell,
470 MetamagicEmpoweredSpell,
471 MetamagicExtendedSpell,
472 MetamagicHeightenedSpell,
473 MetamagicQuickenedSpell,
474 MetamagicSubtleSpell,
475 MetamagicTwinnedSpell,
476 ];
477
478 vec![all_metamagics.clone(), all_metamagics]
479 }
480 ChoosableCustomLevelFeature::EldritchInvocations => {
481 let all_eldritch_invocations = vec![
482 EldritchInvocationAgonizingBlast,
483 EldritchInvocationArmorOfShadows,
484 EldritchInvocationAscendantStep,
485 EldritchInvocationBeastSpeech,
486 EldritchInvocationBeguilingInfluence,
487 EldritchInvocationBewitchingWhispers,
488 EldritchInvocationBookOfAncientSecrets,
489 EldritchInvocationChainsOfCarceri,
490 EldritchInvocationDevilsSight,
491 EldritchInvocationDreadfulWord,
492 EldritchInvocationEldritchSight,
493 EldritchInvocationEldritchSpear,
494 EldritchInvocationEyesOfTheRuneKeeper,
495 EldritchInvocationFiendishVigor,
496 EldritchInvocationGazeOfTwoMinds,
497 EldritchInvocationLifedrinker,
498 EldritchInvocationMaskOfManyFaces,
499 EldritchInvocationMasterOfMyriadForms,
500 EldritchInvocationMinionsOfChaos,
501 EldritchInvocationMireTheMind,
502 EldritchInvocationMistyVisions,
503 EldritchInvocationOneWithShadows,
504 EldritchInvocationOtherworldlyLeap,
505 EldritchInvocationRepellingBlast,
506 EldritchInvocationSculptorOfFlesh,
507 EldritchInvocationSignOfIllOmen,
508 EldritchInvocationThiefOfFiveFates,
509 EldritchInvocationThirstingBlade,
510 EldritchInvocationVisionsOfDistantRealms,
511 EldritchInvocationVoiceOfTheChainMaster,
512 EldritchInvocationWhispersOfTheGrave,
513 EldritchInvocationWitchSight,
514 ];
515
516 vec![all_eldritch_invocations.clone(), all_eldritch_invocations]
517 }
518 ChoosableCustomLevelFeature::DragonAncestor => {
519 vec![vec![
520 DragonAncestorBlackAcidDamage,
521 DragonAncestorBlueLightningDamage,
522 DragonAncestorBrassFireDamage,
523 DragonAncestorBronzeLightningDamage,
524 DragonAncestorCopperAcidDamage,
525 DragonAncestorGoldFireDamage,
526 DragonAncestorGreenPoisonDamage,
527 DragonAncestorRedFireDamage,
528 DragonAncestorSilverColdDamage,
529 DragonAncestorWhiteColdDamage,
530 ]]
531 }
532 }
533 }
534}
535
536pub enum SheetLevelFeatureType {
537 PrimalChampion,
539}
540
541pub enum CustomLevelFeatureType {
542 Choosable(ChoosableCustomLevelFeature),
543 Sheet(SheetLevelFeatureType),
544 Passive,
545 Ignored,
546}
547
548impl CustomLevelFeatureType {
549 pub fn identify(index: String) -> Option<CustomLevelFeatureType> {
550 use ChoosableCustomLevelFeature::*;
551 use CustomLevelFeatureType::*;
552 use SheetLevelFeatureType::*;
553 match index.as_str() {
554 "bard-college"
556 | "divine-domain"
557 | "monastic-tradition"
558 | "sacred-oath"
559 | "ranger-archetype"
560 | "sorcerous-origin"
561 | "druid-circle"
562 | "primal-path"
563 | "martial-archetype"
564 | "roguish-archetype"
565 | "otherworldly-patron" => Some(Ignored),
566 "pact-boon" => Some(Choosable(WarlockPact)),
567 "additional-fighting-style" => Some(Choosable(AdditionalFighterFightingStyle)),
568 "fighter-fighting-style" => Some(Choosable(FighterFightingStyle)),
569 "bonus-proficiency" => Some(Passive),
570 "bonus-proficiencies" => Some(Ignored),
572 "additional-magical-secrets" | "bonus-cantrip" => Some(Ignored),
573 "channel-divinity-1-rest" | "channel-divinity-2-rest" | "channel-divinity-3-rest" => {
574 Some(Ignored)
575 }
576 "magical-secrets-1" | "magical-secrets-2" | "magical-secrets-3" => Some(Ignored),
578 "mystic-arcanum-6th-level"
579 | "mystic-arcanum-7th-level"
580 | "mystic-arcanum-8th-level"
581 | "mystic-arcanum-9th-level" => Some(Choosable(ChooseOne6thLevelSpellFromWarlockList)),
582 "paladin-fighting-style" => Some(Choosable(PaladinFightingStyle)),
583 "primal-champion" => Some(Sheet(PrimalChampion)),
584 "diamond-soul" => Some(Passive),
586 "arcane-recovery"
587 | "arcane-tradition"
588 | "archdruid"
589 | "aura-improvements"
590 | "aura-of-courage"
591 | "aura-of-devotion"
592 | "aura-of-protection"
593 | "blessed-healer"
594 | "blindsense"
595 | "brutal-critical-1-dice"
596 | "brutal-critical-2-dice"
597 | "brutal-critical-3-dice"
598 | "danger-sense"
599 | "dark-ones-blessing"
600 | "dark-ones-own-luck"
601 | "destroy-undead-cr-1-or-below"
602 | "destroy-undead-cr-2-or-below"
603 | "destroy-undead-cr-3-or-below"
604 | "destroy-undead-cr-4-or-below"
605 | "destroy-undead-cr-1-2-or-below"
606 | "disciple-of-life"
607 | "divine-health"
608 | "draconic-resilience"
609 | "font-of-magic"
610 | "druid-lands-stride"
611 | "druid-timeless-body"
612 | "druidic"
613 | "elusive"
614 | "empowered-evocation"
615 | "fast-movement"
616 | "feral-instinct"
617 | "feral-senses"
618 | "foe-slayer"
619 | "hurl-through-hell"
620 | "improved-critical"
621 | "improved-divine-smite"
622 | "indomitable-1-use"
623 | "indomitable-2-uses"
624 | "indomitable-3-uses"
625 | "indomitable-might"
626 | "ki-empowered-strikes"
627 | "jack-of-all-trades"
628 | "martial-arts"
629 | "monk-evasion"
630 | "monk-timeless-body"
631 | "purity-of-body"
632 | "purity-of-spirit"
633 | "natures-sanctuary"
634 | "natures-ward"
635 | "sculpt-spells"
636 | "ranger-lands-stride"
637 | "relentless-rage"
638 | "reliable-talent"
639 | "remarkable-athlete"
640 | "rogue-evasion"
641 | "superior-critical"
642 | "superior-inspiration"
643 | "supreme-healing"
644 | "supreme-sneak"
645 | "survivor"
646 | "thiefs-reflexes"
647 | "thieves-cant"
648 | "tongue-of-the-sun-and-moon"
649 | "tranquility"
650 | "unarmored-movement-1"
651 | "unarmored-movement-2"
652 | "use-magic-device"
653 | "ki"
654 | "monk-unarmored-defense"
655 | "perfect-self"
656 | "slippery-mind"
657 | "mindless-rage"
658 | "barbarian-unarmored-defense"
659 | "divine-intervention-improvement"
660 | "persistent-rage"
661 | "evocation-savant"
662 | "overchannel"
663 | "potent-cantrip"
664 | "font-of-inspiration"
665 | "second-story-work"
666 | "primeval-awareness"
667 | "beast-spells" => Some(Passive),
668 "oath-spells" => Some(Ignored),
670 "natural-recovery" => Some(Ignored),
671 "eldritch-invocations" => Some(Choosable(EldritchInvocations)),
672 x if x.starts_with("metamagic-") => {
673 if x.len() == 11 {
674 Some(Choosable(Metamagic))
675 } else {
676 Some(Ignored)
677 }
678 }
679 "hunters-prey" => Some(Choosable(HuntersPrey)),
680 x if x.starts_with("hunters-prey-") => Some(Ignored),
681 "superior-hunters-defense" => Some(Choosable(SuperiorHuntersDefense)),
682 x if x.starts_with("superior-hunters-defenese-") => Some(Ignored),
683 x if x.starts_with("bard-expertise-") || x.starts_with("rogue-expertise-") => {
685 Some(Ignored)
686 } x if x.starts_with("spellcasting-") => Some(Ignored),
688 x if x.starts_with("eldritch-invocation-") => Some(Ignored),
690 x if x.starts_with("circle-spells-") => Some(Ignored),
692 x if x.starts_with("circle-of-the-land") => Some(Ignored),
694 x if x.starts_with("domain-spells-") => Some(Ignored),
696 x if x.starts_with("flexible-casting-") => Some(Ignored),
698 "dragon-ancestor" => Some(Choosable(DragonAncestor)),
699 x if x.starts_with("dragon-ancestor-") => Some(Ignored),
700 "defensive-tactics" => Some(Choosable(DefensiveTactics)),
701 x if x.starts_with("defensive-tactics-") => Some(Ignored),
702 "multiattack" => Some(Choosable(Multiattack)),
703 x if x.starts_with("multiattack-") => Some(Ignored),
704 "ranger-fighting-style" => Some(Choosable(RangerFightingStyle)),
705 x if x.starts_with("favored-enemy-") => Some(Choosable(RangerFavoredEnemyType)),
706 x if x.starts_with("natural-explorer-") => Some(Choosable(RangerTerrainType)),
707 x if x.starts_with("pact-of-the-") => Some(Ignored),
709 x if x.contains("ability-score-improvement") => {
710 Some(Choosable(AbilityScoreImprovement))
711 }
712 x if x.starts_with("fighting-style-") => Some(Ignored),
713 x if x.starts_with("fighter-fighting-style-") => Some(Ignored),
714 x if x.starts_with("ranger-fighting-style-") => Some(Ignored),
715 _ => None,
716 }
717 }
718}
719
720impl Classes {
721 pub(super) async fn new_day(&mut self) {
722 futures::stream::iter(self.0.values_mut())
723 .for_each_concurrent(None, |class| class.new_day())
724 .await;
725 }
726}
727
728impl Class {
729 pub(super) async fn new_day(&mut self) {
730 use crate::classes::ClassSpellCasting::*;
731
732 let index = self.index().to_string();
733
734 if let Some(spell_casting) = &mut self.1.spell_casting {
735 match spell_casting {
736 KnowledgePrepared {
737 pending_preparation,
738 spells_prepared_index,
739 ..
740 }
741 | AlreadyKnowPrepared {
742 pending_preparation,
743 spells_prepared_index,
744 ..
745 } => {
746 *pending_preparation = true;
747 spells_prepared_index.clear();
748 }
749 KnowledgeAlreadyPrepared { usable_slots, .. } => {
750 if let Ok(Some(spellcasting_slots)) =
751 get_spellcasting_slots(index.as_str(), self.1.level).await
752 {
753 *usable_slots = spellcasting_slots.into();
754 }
755 }
756 }
757 }
758 }
759
760 pub async fn get_spellcasting_ability_index(&self) -> Result<String, ApiError> {
761 let op = SpellcastingAbilityQuery::build(SpellcastingAbilityQueryVariables {
762 index: Some(self.index().to_string()),
763 });
764
765 let ability_index = Client::new()
766 .post(GRAPHQL_API_URL.as_str())
767 .run_graphql(op)
768 .await?
769 .data
770 .ok_or(ApiError::Schema)?
771 .class
772 .ok_or(ApiError::Schema)?
773 .spellcasting
774 .ok_or(ApiError::Schema)?
775 .spellcasting_ability
776 .index;
777
778 Ok(ability_index)
779 }
780
781 pub async fn get_spellcasting_slots(&self) -> Result<Option<LevelSpellcasting>, ApiError> {
782 get_spellcasting_slots(self.index(), self.1.level).await
783 }
784
785 pub async fn set_level(
786 &mut self,
787 new_level: u8,
788 ) -> Result<Vec<ChoosableCustomLevelFeature>, ApiError> {
789 let op = LevelFeaturesQuery::build(LevelFeaturesQueryVariables {
790 class: Some(StringFilter(self.index().to_string())),
791 level: Some(LevelFilter {
792 gt: Some(self.1.level),
793 lte: Some(new_level),
794 gte: None,
795 }),
796 });
797
798 let features = Client::new()
799 .post(GRAPHQL_API_URL.as_str())
800 .run_graphql(op)
801 .await?
802 .data
803 .ok_or(ApiError::Schema)?
804 .features
805 .ok_or(ApiError::Schema)?;
806
807 let mut pending_features = vec![];
808
809 features
810 .iter()
811 .filter_map(|feature| CustomLevelFeatureType::identify(feature.index.clone()))
812 .for_each(|feature| match feature {
813 CustomLevelFeatureType::Passive => {}
814 CustomLevelFeatureType::Choosable(feature) => {
815 pending_features.push(feature);
816 }
817 CustomLevelFeatureType::Sheet(feature) => match feature {
818 SheetLevelFeatureType::PrimalChampion => {
819 let mut abilities = self.1.abilities.lock().unwrap();
820 abilities.strength.score += 4;
821 abilities.constitution.score += 4;
822 }
823 },
824 Ignored => {}
825 });
826
827 self.1.level = new_level;
828
829 Ok(pending_features)
830 }
831
832 pub async fn get_levels_features(
833 &self,
834 from_level: Option<u8>,
835 passive: bool,
836 ) -> Result<Vec<String>, ApiError> {
837 let op = LevelFeaturesQuery::build(LevelFeaturesQueryVariables {
838 class: Some(StringFilter(self.index().to_string())),
839 level: Some(LevelFilter {
840 gte: Some(from_level.unwrap_or(0)),
841 lte: Some(self.1.level),
842 gt: None,
843 }),
844 });
845
846 let features = Client::new()
847 .post(GRAPHQL_API_URL.as_str())
848 .run_graphql(op)
849 .await?
850 .data
851 .ok_or(ApiError::Schema)?
852 .features
853 .ok_or(ApiError::Schema)?;
854
855 let features: Vec<String> = features
857 .into_iter()
858 .filter_map(
859 |feature| match CustomLevelFeatureType::identify(feature.index.clone()) {
860 None => Some(feature.index),
861 Some(custom_type) => match custom_type {
862 CustomLevelFeatureType::Passive if passive => Some(feature.index),
863 _ => None,
864 },
865 },
866 )
867 .collect();
868
869 lazy_static! {
871 static ref CR_REGEX: regex::Regex =
872 regex::Regex::new(r"(.*)-cr-([0-9]+(?:-[0-9]+)?)-or-below.*").unwrap();
873 static ref DICE_REGEX: regex::Regex = regex::Regex::new(r"^(.+)-d(\d+)$").unwrap();
874 static ref DIE_DICE_REGEX: regex::Regex =
875 regex::Regex::new(r"^(.+)-(\d+)-(die|dice)$").unwrap();
876 static ref UNARMORED_MOVEMENT_REGEX: regex::Regex =
877 regex::Regex::new(r"^(unarmored-movement)-(\d+)$").unwrap();
878 }
879
880 let mut cr_features: HashMap<String, (f32, String)> = HashMap::new();
882 let mut dice_features: HashMap<String, u32> = HashMap::new();
883 let mut die_dice_features: HashMap<String, u32> = HashMap::new();
884 let mut unarmored_movement_features: HashMap<String, (u32, String)> = HashMap::new();
885
886 for feature in &features {
888 if let Some(caps) = CR_REGEX.captures(feature) {
890 let prefix = caps.get(1).unwrap().as_str().to_string();
891 let cr_str = caps.get(2).unwrap().as_str();
892
893 let cr_value = if cr_str.contains('-') {
895 let parts: Vec<&str> = cr_str.split('-').collect();
896 if parts.len() == 2 {
897 parts[0].parse::<f32>().unwrap_or(0.0)
898 / parts[1].parse::<f32>().unwrap_or(1.0)
899 } else {
900 0.0
901 }
902 } else {
903 cr_str.parse::<f32>().unwrap_or(0.0)
904 };
905
906 if let Some((existing_cr, _)) = cr_features.get(&prefix) {
908 if cr_value > *existing_cr {
909 cr_features.insert(prefix, (cr_value, feature.clone()));
910 }
911 } else {
912 cr_features.insert(prefix, (cr_value, feature.clone()));
913 }
914 continue;
915 }
916
917 if let Some(caps) = DICE_REGEX.captures(feature) {
919 let prefix = caps.get(1).unwrap().as_str().to_string();
920 let dice_value = caps.get(2).unwrap().as_str().parse::<u32>().unwrap_or(0);
921
922 let current_max = dice_features.entry(prefix).or_insert(0);
923 if dice_value > *current_max {
924 *current_max = dice_value;
925 }
926 continue;
927 }
928
929 if let Some(caps) = DIE_DICE_REGEX.captures(feature) {
931 let prefix = caps.get(1).unwrap().as_str().to_string();
932 let dice_value = caps.get(2).unwrap().as_str().parse::<u32>().unwrap_or(0);
933
934 let current_max = die_dice_features.entry(prefix).or_insert(0);
935 if dice_value > *current_max {
936 *current_max = dice_value;
937 }
938 }
939
940 if let Some(caps) = UNARMORED_MOVEMENT_REGEX.captures(feature) {
942 let prefix = caps.get(1).unwrap().as_str().to_string();
943 let movement_value = caps.get(2).unwrap().as_str().parse::<u32>().unwrap_or(0);
944
945 if let Some((existing_value, _)) = unarmored_movement_features.get(&prefix) {
947 if movement_value > *existing_value {
948 unarmored_movement_features
949 .insert(prefix, (movement_value, feature.clone()));
950 }
951 } else {
952 unarmored_movement_features.insert(prefix, (movement_value, feature.clone()));
953 }
954 }
955 }
956
957 let mut filtered_features = Vec::new();
959 let mut has_improved_divine_smite = false;
960
961 for feature in &features {
963 if feature == "improved-divine-smite" {
964 has_improved_divine_smite = true;
965 break;
966 }
967 }
968
969 for feature in features {
970 if feature == "divine-smite" && has_improved_divine_smite {
972 continue;
973 }
974
975 if let Some(caps) = CR_REGEX.captures(&feature) {
977 let prefix = caps.get(1).unwrap().as_str().to_string();
978
979 if let Some((_, highest_feature)) = cr_features.get(&prefix) {
980 if &feature == highest_feature {
981 filtered_features.push(feature);
982 }
983 }
984 continue;
985 }
986
987 if let Some(caps) = DICE_REGEX.captures(&feature) {
989 let prefix = caps.get(1).unwrap().as_str().to_string();
990 let dice_value = caps
991 .get(2)
992 .unwrap()
993 .as_str()
994 .parse::<u32>()
995 .expect("Parsing dice value");
996
997 if let Some(&max_dice) = dice_features.get(&prefix) {
998 if dice_value == max_dice {
999 filtered_features.push(feature);
1000 }
1001 }
1002 continue;
1003 }
1004
1005 if let Some(caps) = DIE_DICE_REGEX.captures(&feature) {
1007 let prefix = caps.get(1).unwrap().as_str().to_string();
1008 let dice_value = caps
1009 .get(2)
1010 .unwrap()
1011 .as_str()
1012 .parse::<u32>()
1013 .expect("Parsing die/dice value");
1014
1015 if let Some(&max_dice) = die_dice_features.get(&prefix) {
1016 if dice_value == max_dice {
1017 filtered_features.push(feature);
1018 }
1019 }
1020 continue;
1021 }
1022
1023 if let Some(caps) = UNARMORED_MOVEMENT_REGEX.captures(&feature) {
1025 let prefix = caps.get(1).unwrap().as_str().to_string();
1026
1027 if let Some((_, highest_feature)) = unarmored_movement_features.get(&prefix) {
1028 if &feature == highest_feature {
1029 filtered_features.push(feature);
1030 }
1031 }
1032 continue;
1033 }
1034
1035 filtered_features.push(feature);
1037 }
1038
1039 let mut features = filtered_features;
1040
1041 if !passive {
1043 if let Some(multiattack) = &self.1.multiattack {
1044 features.push(multiattack.clone());
1045 }
1046 if let Some(hunters_prey) = &self.1.hunters_prey {
1047 features.push(hunters_prey.clone());
1048 }
1049 if let Some(metamagic) = &self.1.sorcerer_metamagic {
1050 features.append(&mut metamagic.clone());
1051 }
1052 if let Some(eldritch_invocation) = &self.1.warlock_eldritch_invocation {
1053 features.append(&mut eldritch_invocation.clone());
1054 }
1055 }
1056
1057 Ok(features)
1058 }
1059
1060 pub fn apply_option(&mut self, option: ChoosableCustomLevelFeatureOption) {
1061 use ChoosableCustomLevelFeatureOption::*;
1062
1063 match option {
1064 StrengthPlusOne | DexterityPlusOne | ConstitutionPlusOne | IntelligencePlusOne
1065 | WisdomPlusOne | CharismaPlusOne => self.increase_score(option),
1066 PactOfTheChain | PactOfTheBlade | PactOfTheTome => {
1067 println!("Pact of the Chain, Blade or Tome not yet implemented");
1068 }
1069 HuntersPreyGiantKiller | HuntersPreyHordeBreaker | HuntersPreyColossusSlayer => {
1070 self.1
1071 .hunters_prey
1072 .replace(option.as_index_str().to_string());
1073 }
1074 DefensiveTacticsSteelWill
1075 | DefensiveTacticsEscapeTheHorde
1076 | DefensiveTacticsMultiattackDefense => {
1077 self.1
1078 .defensive_tactics
1079 .replace(option.as_index_str().to_string());
1080 }
1081 FighterFightingStyleArchery
1082 | FighterFightingStyleDefense
1083 | FighterFightingStyleDueling
1084 | FighterFightingStyleGreatWeaponFighting
1085 | FighterFightingStyleProtection
1086 | FighterFightingStyleTwoWeaponFighting
1087 | RangerFightingStyleArchery
1088 | RangerFightingStyleDefense
1089 | RangerFightingStyleDueling
1090 | RangerFightingStyleTwoWeaponFighting
1091 | FightingStyleDefense
1092 | FightingStyleDueling
1093 | FightingStyleGreatWeaponFighting
1094 | FightingStyleProtection => {
1095 if self.1.fighting_style.is_none() {
1096 self.1
1097 .fighting_style
1098 .replace(option.as_index_str().to_string());
1099 } else {
1100 self.1
1101 .additional_fighting_style
1102 .replace(option.as_index_str().to_string());
1103 }
1104 }
1105 MultiattackVolley | MultiattackWhirlwindAttack => {
1106 self.1
1107 .multiattack
1108 .replace(option.as_index_str().to_string());
1109 }
1110 SuperiorHuntersDefenseEvasion
1111 | SuperiorHuntersDefenseStandAgainstTheTide
1112 | SuperiorHuntersDefenseUncannyDodge => {
1113 self.1
1114 .superior_hunters_defense
1115 .replace(option.as_index_str().to_string());
1116 }
1117 RangerTerrainTypeArctic
1118 | RangerTerrainTypeCoast
1119 | RangerTerrainTypeDesert
1120 | RangerTerrainTypeForest
1121 | RangerTerrainTypeGrassland
1122 | RangerTerrainTypeMountain
1123 | RangerTerrainTypeSwamp => {
1124 self.1
1125 .natural_explorer_terrain_type
1126 .get_or_insert_with(Vec::new)
1127 .push(option.as_index_str().to_string());
1128 }
1129 RangerFavoredEnemyTypeAberrations
1130 | RangerFavoredEnemyTypeBeasts
1131 | RangerFavoredEnemyTypeCelestials
1132 | RangerFavoredEnemyTypeConstructs
1133 | RangerFavoredEnemyTypeDragons
1134 | RangerFavoredEnemyTypeElementals
1135 | RangerFavoredEnemyTypeFey
1136 | RangerFavoredEnemyTypeFiends
1137 | RangerFavoredEnemyTypeGiants
1138 | RangerFavoredEnemyTypeMonstrosities
1139 | RangerFavoredEnemyTypeOozes
1140 | RangerFavoredEnemyTypePlants
1141 | RangerFavoredEnemyTypeUndead
1142 | RangerFavoredEnemyTypeHumanoids => {
1143 self.1
1144 .ranger_favored_enemy_type
1145 .get_or_insert_with(Vec::new)
1146 .push(option.as_index_str().to_string());
1147 }
1148 MetamagicCarefulSpell
1149 | MetamagicDistantSpell
1150 | MetamagicEmpoweredSpell
1151 | MetamagicExtendedSpell
1152 | MetamagicHeightenedSpell
1153 | MetamagicQuickenedSpell
1154 | MetamagicSubtleSpell
1155 | MetamagicTwinnedSpell => {
1156 self.1
1157 .sorcerer_metamagic
1158 .get_or_insert_with(Vec::new)
1159 .push(option.as_index_str().to_string());
1160 }
1161 EldritchInvocationAgonizingBlast
1162 | EldritchInvocationArmorOfShadows
1163 | EldritchInvocationAscendantStep
1164 | EldritchInvocationBeastSpeech
1165 | EldritchInvocationBeguilingInfluence
1166 | EldritchInvocationBewitchingWhispers
1167 | EldritchInvocationBookOfAncientSecrets
1168 | EldritchInvocationChainsOfCarceri
1169 | EldritchInvocationDevilsSight
1170 | EldritchInvocationDreadfulWord
1171 | EldritchInvocationEldritchSight
1172 | EldritchInvocationEldritchSpear
1173 | EldritchInvocationEyesOfTheRuneKeeper
1174 | EldritchInvocationFiendishVigor
1175 | EldritchInvocationGazeOfTwoMinds
1176 | EldritchInvocationLifedrinker
1177 | EldritchInvocationMaskOfManyFaces
1178 | EldritchInvocationMasterOfMyriadForms
1179 | EldritchInvocationMinionsOfChaos
1180 | EldritchInvocationMireTheMind
1181 | EldritchInvocationMistyVisions
1182 | EldritchInvocationOneWithShadows
1183 | EldritchInvocationOtherworldlyLeap
1184 | EldritchInvocationRepellingBlast
1185 | EldritchInvocationSculptorOfFlesh
1186 | EldritchInvocationSignOfIllOmen
1187 | EldritchInvocationThiefOfFiveFates
1188 | EldritchInvocationThirstingBlade
1189 | EldritchInvocationVisionsOfDistantRealms
1190 | EldritchInvocationVoiceOfTheChainMaster
1191 | EldritchInvocationWhispersOfTheGrave
1192 | EldritchInvocationWitchSight => {
1193 self.1
1194 .warlock_eldritch_invocation
1195 .get_or_insert_with(Vec::new)
1196 .push(option.as_index_str().to_string());
1197 }
1198 DragonAncestorBlackAcidDamage
1199 | DragonAncestorBlueLightningDamage
1200 | DragonAncestorBrassFireDamage
1201 | DragonAncestorBronzeLightningDamage
1202 | DragonAncestorCopperAcidDamage
1203 | DragonAncestorGoldFireDamage
1204 | DragonAncestorGreenPoisonDamage
1205 | DragonAncestorRedFireDamage
1206 | DragonAncestorSilverColdDamage
1207 | DragonAncestorWhiteColdDamage => {
1208 self.1
1209 .sorcerer_dragon_ancestor
1210 .replace(option.as_index_str().to_string());
1211 }
1212 }
1213 }
1214
1215 fn increase_score(&mut self, option: ChoosableCustomLevelFeatureOption) {
1216 let mut abilities = self.1.abilities.lock().unwrap();
1217 match option {
1218 ChoosableCustomLevelFeatureOption::StrengthPlusOne => {
1219 abilities.strength.score += 1;
1220 }
1221 ChoosableCustomLevelFeatureOption::DexterityPlusOne => {
1222 abilities.dexterity.score += 1;
1223 }
1224 ChoosableCustomLevelFeatureOption::ConstitutionPlusOne => {
1225 abilities.constitution.score += 1;
1226 }
1227 ChoosableCustomLevelFeatureOption::IntelligencePlusOne => {
1228 abilities.intelligence.score += 1;
1229 }
1230 ChoosableCustomLevelFeatureOption::WisdomPlusOne => {
1231 abilities.wisdom.score += 1;
1232 }
1233 ChoosableCustomLevelFeatureOption::CharismaPlusOne => {
1234 abilities.charisma.score += 1;
1235 }
1236 _ => {}
1237 }
1238 }
1239}
1240
1241pub async fn get_spellcasting_slots(
1242 index: &str,
1243 level: u8,
1244) -> Result<Option<LevelSpellcasting>, ApiError> {
1245 let op = SpellcastingQuery::build(SpellcastingQueryVariables {
1246 index: Some(format!("{}-{}", index, level)),
1247 });
1248
1249 let spellcasting_slots = Client::new()
1250 .post(GRAPHQL_API_URL.as_str())
1251 .run_graphql(op)
1252 .await?
1253 .data
1254 .ok_or(ApiError::Schema)?
1255 .level
1256 .ok_or(ApiError::Schema)?
1257 .spellcasting;
1258
1259 Ok(spellcasting_slots)
1260}