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 self.1.abilities_modifiers.strength.score += 4;
820 self.1.abilities_modifiers.constitution.score += 4;
821 }
822 },
823 Ignored => {}
824 });
825
826 self.1.level = new_level;
827
828 Ok(pending_features)
829 }
830
831 pub async fn get_levels_features(
832 &self,
833 from_level: Option<u8>,
834 passive: bool,
835 ) -> Result<Vec<String>, ApiError> {
836 let op = LevelFeaturesQuery::build(LevelFeaturesQueryVariables {
837 class: Some(StringFilter(self.index().to_string())),
838 level: Some(LevelFilter {
839 gte: Some(from_level.unwrap_or(0)),
840 lte: Some(self.1.level),
841 gt: None,
842 }),
843 });
844
845 let features = Client::new()
846 .post(GRAPHQL_API_URL.as_str())
847 .run_graphql(op)
848 .await?
849 .data
850 .ok_or(ApiError::Schema)?
851 .features
852 .ok_or(ApiError::Schema)?;
853
854 let features: Vec<String> = features
856 .into_iter()
857 .filter_map(
858 |feature| match CustomLevelFeatureType::identify(feature.index.clone()) {
859 None => Some(feature.index),
860 Some(custom_type) => match custom_type {
861 CustomLevelFeatureType::Passive if passive => Some(feature.index),
862 _ => None,
863 },
864 },
865 )
866 .collect();
867
868 lazy_static! {
870 static ref CR_REGEX: regex::Regex =
871 regex::Regex::new(r"(.*)-cr-([0-9]+(?:-[0-9]+)?)-or-below.*").unwrap();
872 static ref DICE_REGEX: regex::Regex = regex::Regex::new(r"^(.+)-d(\d+)$").unwrap();
873 static ref DIE_DICE_REGEX: regex::Regex =
874 regex::Regex::new(r"^(.+)-(\d+)-(die|dice)$").unwrap();
875 static ref UNARMORED_MOVEMENT_REGEX: regex::Regex =
876 regex::Regex::new(r"^(unarmored-movement)-(\d+)$").unwrap();
877 }
878
879 let mut cr_features: HashMap<String, (f32, String)> = HashMap::new();
881 let mut dice_features: HashMap<String, u32> = HashMap::new();
882 let mut die_dice_features: HashMap<String, u32> = HashMap::new();
883 let mut unarmored_movement_features: HashMap<String, (u32, String)> = HashMap::new();
884
885 for feature in &features {
887 if let Some(caps) = CR_REGEX.captures(feature) {
889 let prefix = caps.get(1).unwrap().as_str().to_string();
890 let cr_str = caps.get(2).unwrap().as_str();
891
892 let cr_value = if cr_str.contains('-') {
894 let parts: Vec<&str> = cr_str.split('-').collect();
895 if parts.len() == 2 {
896 parts[0].parse::<f32>().unwrap_or(0.0)
897 / parts[1].parse::<f32>().unwrap_or(1.0)
898 } else {
899 0.0
900 }
901 } else {
902 cr_str.parse::<f32>().unwrap_or(0.0)
903 };
904
905 if let Some((existing_cr, _)) = cr_features.get(&prefix) {
907 if cr_value > *existing_cr {
908 cr_features.insert(prefix, (cr_value, feature.clone()));
909 }
910 } else {
911 cr_features.insert(prefix, (cr_value, feature.clone()));
912 }
913 continue;
914 }
915
916 if let Some(caps) = DICE_REGEX.captures(feature) {
918 let prefix = caps.get(1).unwrap().as_str().to_string();
919 let dice_value = caps.get(2).unwrap().as_str().parse::<u32>().unwrap_or(0);
920
921 let current_max = dice_features.entry(prefix).or_insert(0);
922 if dice_value > *current_max {
923 *current_max = dice_value;
924 }
925 continue;
926 }
927
928 if let Some(caps) = DIE_DICE_REGEX.captures(feature) {
930 let prefix = caps.get(1).unwrap().as_str().to_string();
931 let dice_value = caps.get(2).unwrap().as_str().parse::<u32>().unwrap_or(0);
932
933 let current_max = die_dice_features.entry(prefix).or_insert(0);
934 if dice_value > *current_max {
935 *current_max = dice_value;
936 }
937 }
938
939 if let Some(caps) = UNARMORED_MOVEMENT_REGEX.captures(feature) {
941 let prefix = caps.get(1).unwrap().as_str().to_string();
942 let movement_value = caps.get(2).unwrap().as_str().parse::<u32>().unwrap_or(0);
943
944 if let Some((existing_value, _)) = unarmored_movement_features.get(&prefix) {
946 if movement_value > *existing_value {
947 unarmored_movement_features
948 .insert(prefix, (movement_value, feature.clone()));
949 }
950 } else {
951 unarmored_movement_features.insert(prefix, (movement_value, feature.clone()));
952 }
953 }
954 }
955
956 let mut filtered_features = Vec::new();
958 let mut has_improved_divine_smite = false;
959
960 for feature in &features {
962 if feature == "improved-divine-smite" {
963 has_improved_divine_smite = true;
964 break;
965 }
966 }
967
968 for feature in features {
969 if feature == "divine-smite" && has_improved_divine_smite {
971 continue;
972 }
973
974 if let Some(caps) = CR_REGEX.captures(&feature) {
976 let prefix = caps.get(1).unwrap().as_str().to_string();
977
978 if let Some((_, highest_feature)) = cr_features.get(&prefix) {
979 if &feature == highest_feature {
980 filtered_features.push(feature);
981 }
982 }
983 continue;
984 }
985
986 if let Some(caps) = DICE_REGEX.captures(&feature) {
988 let prefix = caps.get(1).unwrap().as_str().to_string();
989 let dice_value = caps
990 .get(2)
991 .unwrap()
992 .as_str()
993 .parse::<u32>()
994 .expect("Parsing dice value");
995
996 if let Some(&max_dice) = dice_features.get(&prefix) {
997 if dice_value == max_dice {
998 filtered_features.push(feature);
999 }
1000 }
1001 continue;
1002 }
1003
1004 if let Some(caps) = DIE_DICE_REGEX.captures(&feature) {
1006 let prefix = caps.get(1).unwrap().as_str().to_string();
1007 let dice_value = caps
1008 .get(2)
1009 .unwrap()
1010 .as_str()
1011 .parse::<u32>()
1012 .expect("Parsing die/dice value");
1013
1014 if let Some(&max_dice) = die_dice_features.get(&prefix) {
1015 if dice_value == max_dice {
1016 filtered_features.push(feature);
1017 }
1018 }
1019 continue;
1020 }
1021
1022 if let Some(caps) = UNARMORED_MOVEMENT_REGEX.captures(&feature) {
1024 let prefix = caps.get(1).unwrap().as_str().to_string();
1025
1026 if let Some((_, highest_feature)) = unarmored_movement_features.get(&prefix) {
1027 if &feature == highest_feature {
1028 filtered_features.push(feature);
1029 }
1030 }
1031 continue;
1032 }
1033
1034 filtered_features.push(feature);
1036 }
1037
1038 let mut features = filtered_features;
1039
1040 if !passive {
1042 if let Some(multiattack) = &self.1.multiattack {
1043 features.push(multiattack.clone());
1044 }
1045 if let Some(hunters_prey) = &self.1.hunters_prey {
1046 features.push(hunters_prey.clone());
1047 }
1048 if let Some(metamagic) = &self.1.sorcerer_metamagic {
1049 features.append(&mut metamagic.clone());
1050 }
1051 if let Some(eldritch_invocation) = &self.1.warlock_eldritch_invocation {
1052 features.append(&mut eldritch_invocation.clone());
1053 }
1054 }
1055
1056 Ok(features)
1057 }
1058
1059 pub fn apply_option(&mut self, option: ChoosableCustomLevelFeatureOption) {
1060 use ChoosableCustomLevelFeatureOption::*;
1061
1062 match option {
1063 StrengthPlusOne | DexterityPlusOne | ConstitutionPlusOne | IntelligencePlusOne
1064 | WisdomPlusOne | CharismaPlusOne => self.increase_score(option),
1065 PactOfTheChain | PactOfTheBlade | PactOfTheTome => {
1066 println!("Pact of the Chain, Blade or Tome not yet implemented");
1067 }
1068 HuntersPreyGiantKiller | HuntersPreyHordeBreaker | HuntersPreyColossusSlayer => {
1069 self.1
1070 .hunters_prey
1071 .replace(option.as_index_str().to_string());
1072 }
1073 DefensiveTacticsSteelWill
1074 | DefensiveTacticsEscapeTheHorde
1075 | DefensiveTacticsMultiattackDefense => {
1076 self.1
1077 .defensive_tactics
1078 .replace(option.as_index_str().to_string());
1079 }
1080 FighterFightingStyleArchery
1081 | FighterFightingStyleDefense
1082 | FighterFightingStyleDueling
1083 | FighterFightingStyleGreatWeaponFighting
1084 | FighterFightingStyleProtection
1085 | FighterFightingStyleTwoWeaponFighting
1086 | RangerFightingStyleArchery
1087 | RangerFightingStyleDefense
1088 | RangerFightingStyleDueling
1089 | RangerFightingStyleTwoWeaponFighting
1090 | FightingStyleDefense
1091 | FightingStyleDueling
1092 | FightingStyleGreatWeaponFighting
1093 | FightingStyleProtection => {
1094 if self.1.fighting_style.is_none() {
1095 self.1
1096 .fighting_style
1097 .replace(option.as_index_str().to_string());
1098 } else {
1099 self.1
1100 .additional_fighting_style
1101 .replace(option.as_index_str().to_string());
1102 }
1103 }
1104 MultiattackVolley | MultiattackWhirlwindAttack => {
1105 self.1
1106 .multiattack
1107 .replace(option.as_index_str().to_string());
1108 }
1109 SuperiorHuntersDefenseEvasion
1110 | SuperiorHuntersDefenseStandAgainstTheTide
1111 | SuperiorHuntersDefenseUncannyDodge => {
1112 self.1
1113 .superior_hunters_defense
1114 .replace(option.as_index_str().to_string());
1115 }
1116 RangerTerrainTypeArctic
1117 | RangerTerrainTypeCoast
1118 | RangerTerrainTypeDesert
1119 | RangerTerrainTypeForest
1120 | RangerTerrainTypeGrassland
1121 | RangerTerrainTypeMountain
1122 | RangerTerrainTypeSwamp => {
1123 self.1
1124 .natural_explorer_terrain_type
1125 .get_or_insert_with(Vec::new)
1126 .push(option.as_index_str().to_string());
1127 }
1128 RangerFavoredEnemyTypeAberrations
1129 | RangerFavoredEnemyTypeBeasts
1130 | RangerFavoredEnemyTypeCelestials
1131 | RangerFavoredEnemyTypeConstructs
1132 | RangerFavoredEnemyTypeDragons
1133 | RangerFavoredEnemyTypeElementals
1134 | RangerFavoredEnemyTypeFey
1135 | RangerFavoredEnemyTypeFiends
1136 | RangerFavoredEnemyTypeGiants
1137 | RangerFavoredEnemyTypeMonstrosities
1138 | RangerFavoredEnemyTypeOozes
1139 | RangerFavoredEnemyTypePlants
1140 | RangerFavoredEnemyTypeUndead
1141 | RangerFavoredEnemyTypeHumanoids => {
1142 self.1
1143 .ranger_favored_enemy_type
1144 .get_or_insert_with(Vec::new)
1145 .push(option.as_index_str().to_string());
1146 }
1147 MetamagicCarefulSpell
1148 | MetamagicDistantSpell
1149 | MetamagicEmpoweredSpell
1150 | MetamagicExtendedSpell
1151 | MetamagicHeightenedSpell
1152 | MetamagicQuickenedSpell
1153 | MetamagicSubtleSpell
1154 | MetamagicTwinnedSpell => {
1155 self.1
1156 .sorcerer_metamagic
1157 .get_or_insert_with(Vec::new)
1158 .push(option.as_index_str().to_string());
1159 }
1160 EldritchInvocationAgonizingBlast
1161 | EldritchInvocationArmorOfShadows
1162 | EldritchInvocationAscendantStep
1163 | EldritchInvocationBeastSpeech
1164 | EldritchInvocationBeguilingInfluence
1165 | EldritchInvocationBewitchingWhispers
1166 | EldritchInvocationBookOfAncientSecrets
1167 | EldritchInvocationChainsOfCarceri
1168 | EldritchInvocationDevilsSight
1169 | EldritchInvocationDreadfulWord
1170 | EldritchInvocationEldritchSight
1171 | EldritchInvocationEldritchSpear
1172 | EldritchInvocationEyesOfTheRuneKeeper
1173 | EldritchInvocationFiendishVigor
1174 | EldritchInvocationGazeOfTwoMinds
1175 | EldritchInvocationLifedrinker
1176 | EldritchInvocationMaskOfManyFaces
1177 | EldritchInvocationMasterOfMyriadForms
1178 | EldritchInvocationMinionsOfChaos
1179 | EldritchInvocationMireTheMind
1180 | EldritchInvocationMistyVisions
1181 | EldritchInvocationOneWithShadows
1182 | EldritchInvocationOtherworldlyLeap
1183 | EldritchInvocationRepellingBlast
1184 | EldritchInvocationSculptorOfFlesh
1185 | EldritchInvocationSignOfIllOmen
1186 | EldritchInvocationThiefOfFiveFates
1187 | EldritchInvocationThirstingBlade
1188 | EldritchInvocationVisionsOfDistantRealms
1189 | EldritchInvocationVoiceOfTheChainMaster
1190 | EldritchInvocationWhispersOfTheGrave
1191 | EldritchInvocationWitchSight => {
1192 self.1
1193 .warlock_eldritch_invocation
1194 .get_or_insert_with(Vec::new)
1195 .push(option.as_index_str().to_string());
1196 }
1197 DragonAncestorBlackAcidDamage
1198 | DragonAncestorBlueLightningDamage
1199 | DragonAncestorBrassFireDamage
1200 | DragonAncestorBronzeLightningDamage
1201 | DragonAncestorCopperAcidDamage
1202 | DragonAncestorGoldFireDamage
1203 | DragonAncestorGreenPoisonDamage
1204 | DragonAncestorRedFireDamage
1205 | DragonAncestorSilverColdDamage
1206 | DragonAncestorWhiteColdDamage => {
1207 self.1
1208 .sorcerer_dragon_ancestor
1209 .replace(option.as_index_str().to_string());
1210 }
1211 }
1212 }
1213
1214 fn increase_score(&mut self, option: ChoosableCustomLevelFeatureOption) {
1215 match option {
1216 ChoosableCustomLevelFeatureOption::StrengthPlusOne => {
1217 self.1.abilities_modifiers.strength.score += 1;
1218 }
1219 ChoosableCustomLevelFeatureOption::DexterityPlusOne => {
1220 self.1.abilities_modifiers.dexterity.score += 1;
1221 }
1222 ChoosableCustomLevelFeatureOption::ConstitutionPlusOne => {
1223 self.1.abilities_modifiers.constitution.score += 1;
1224 }
1225 ChoosableCustomLevelFeatureOption::IntelligencePlusOne => {
1226 self.1.abilities_modifiers.intelligence.score += 1;
1227 }
1228 ChoosableCustomLevelFeatureOption::WisdomPlusOne => {
1229 self.1.abilities_modifiers.wisdom.score += 1;
1230 }
1231 ChoosableCustomLevelFeatureOption::CharismaPlusOne => {
1232 self.1.abilities_modifiers.charisma.score += 1;
1233 }
1234 _ => {}
1235 }
1236 }
1237}
1238
1239pub async fn get_spellcasting_slots(
1240 index: &str,
1241 level: u8,
1242) -> Result<Option<LevelSpellcasting>, ApiError> {
1243 let op = SpellcastingQuery::build(SpellcastingQueryVariables {
1244 index: Some(format!("{}-{}", index, level)),
1245 });
1246
1247 let spellcasting_slots = Client::new()
1248 .post(GRAPHQL_API_URL.as_str())
1249 .run_graphql(op)
1250 .await?
1251 .data
1252 .ok_or(ApiError::Schema)?
1253 .level
1254 .ok_or(ApiError::Schema)?
1255 .spellcasting;
1256
1257 Ok(spellcasting_slots)
1258}