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