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![
433 vec![
434 MetamagicCarefullSpell,
435 MetamagicDistantSpell,
436 MetamagicEmpoweredSpell,
437 MetamagicExtendedSpell,
438 MetamagicHeightenedSpell,
439 MetamagicQuickenedSpell,
440 MetamagicSubtleSpell,
441 MetamagicCarefullSpell,
442 MetamagicTwinnedSpell,
443 ],
444 vec![
445 MetamagicCarefullSpell,
446 MetamagicDistantSpell,
447 MetamagicEmpoweredSpell,
448 MetamagicExtendedSpell,
449 MetamagicHeightenedSpell,
450 MetamagicQuickenedSpell,
451 MetamagicSubtleSpell,
452 MetamagicCarefullSpell,
453 MetamagicTwinnedSpell,
454 ],
455 ]
456 }
457 ChoosableCustomLevelFeature::DragonAncestor => {
458 vec![vec![
459 DragonAncestorBlackAcidDamage,
460 DragonAncestorBlueLightningDamage,
461 DragonAncestorBrassFireDamage,
462 DragonAncestorBronzeLightningDamage,
463 DragonAncestorCopperAcidDamage,
464 DragonAncestorGoldFireDamage,
465 DragonAncestorGreenPoisonDamage,
466 DragonAncestorRedFireDamage,
467 DragonAncestorSilverColdDamage,
468 DragonAncestorWhiteColdDamage,
469 ]]
470 }
471 }
472 }
473}
474
475pub enum SheetLevelFeatureType {
476 PrimalChampion,
478}
479
480pub enum CustomLevelFeatureType {
481 Choosable(ChoosableCustomLevelFeature),
482 Sheet(SheetLevelFeatureType),
483 Passive,
484 Ignored,
485}
486
487impl CustomLevelFeatureType {
488 pub fn identify(index: String) -> Option<CustomLevelFeatureType> {
489 use ChoosableCustomLevelFeature::*;
490 use CustomLevelFeatureType::*;
491 use SheetLevelFeatureType::*;
492 match index.as_str() {
493 "bard-college"
495 | "divine-domain"
496 | "monastic-tradition"
497 | "sacred-oath"
498 | "ranger-archetype"
499 | "sorcerous-origin"
500 | "druid-circle"
501 | "primal-path"
502 | "martial-archetype"
503 | "otherworldly-patron" => Some(Ignored),
504 "pact-boon" => Some(Choosable(WarlockPact)),
505 "additional-fighting-style" => Some(Choosable(AdditionalFighterFightingStyle)),
506 "fighter-fighting-style" => Some(Choosable(FighterFightingStyle)),
507 "bonus-proficiency" => Some(Passive),
508 "additional-magical-secrets" | "bonus-cantrip" => Some(Ignored),
509 "channel-divinity-1-rest" | "channel-divinity-2-rest" | "channel-divinity-3-rest" => {
510 Some(Ignored)
511 }
512 "magical-secrets-1" | "magical-secrets-2" | "magical-secrets-3" => Some(Ignored),
514 "mystic-arcanum-6th-level"
515 | "mystic-arcanum-7th-level"
516 | "mystic-arcanum-8th-level"
517 | "mystic-arcanum-9th-level" => Some(Choosable(ChooseOne6thLevelSpellFromWarlockList)),
518 "paladin-fighting-style" => Some(Choosable(PaladinFightingStyle)),
519 "primal-champion" => Some(Sheet(PrimalChampion)),
520 "diamond-soul" => Some(Passive),
522 "arcane-recovery"
523 | "arcane-tradition"
524 | "archdruid"
525 | "aura-improvements"
526 | "aura-of-courage"
527 | "aura-of-devotion"
528 | "aura-of-protection"
529 | "blessed-healer"
530 | "blindsense"
531 | "brutal-critical-1-dice"
532 | "brutal-critical-2-dice"
533 | "brutal-critical-3-dice"
534 | "danger-sense"
535 | "dark-ones-blessing"
536 | "dark-ones-own-luck"
537 | "destroy-undead-cr-1-or-below"
538 | "destroy-undead-cr-2-or-below"
539 | "destroy-undead-cr-3-or-below"
540 | "destroy-undead-cr-4-or-below"
541 | "destroy-undead-cr-1-2-or-below"
542 | "disciple-of-life"
543 | "divine-health"
544 | "draconic-resilience"
545 | "draconic-presence"
546 | "font-of-magic"
547 | "druid-lands-stride"
548 | "druid-timeless-body"
549 | "druidic"
550 | "elusive"
551 | "empowered-evocation"
552 | "fast-movement"
553 | "feral-instinct"
554 | "feral-senses"
555 | "fighter-fighting-style-archery"
556 | "fighter-fighting-style-protection"
557 | "fighter-fighting-style-defense"
558 | "fighter-fighting-style-dueling"
559 | "fighter-fighting-style-great-weapon-fighting"
560 | "fighter-fighting-style-two-weapon-fighting"
561 | "fighting-style-defense"
562 | "fighting-style-dueling"
563 | "fighting-style-great-weapon-fighting"
564 | "foe-slayer"
565 | "hurl-through-hell"
566 | "improved-critical"
567 | "improved-divine-smite"
568 | "indomitable-1-use"
569 | "indomitable-2-uses"
570 | "indomitable-3-uses"
571 | "indomitable-might"
572 | "ki-empowered-strikes"
573 | "jack-of-all-trades"
574 | "martial-arts"
575 | "monk-evasion"
576 | "monk-timeless-body"
577 | "purity-of-body"
578 | "purity-of-spirit"
579 | "natures-sanctuary"
580 | "natures-ward"
581 | "sculpt-spells"
582 | "ranger-lands-stride"
583 | "relentless-rage"
584 | "reliable-talent"
585 | "remarkable-athlete"
586 | "rogue-evasion"
587 | "superior-critical"
588 | "superior-inspiration"
589 | "supreme-healing"
590 | "supreme-sneak"
591 | "survivor"
592 | "thiefs-reflexes"
593 | "thieves-cant"
594 | "tongue-of-the-sun-and-moon"
595 | "tranquility"
596 | "unarmored-movement-1"
597 | "unarmored-movement-2"
598 | "use-magic-device"
599 | "wild-shape-cr-1-2-or-below-no-flying-speed"
600 | "wild-shape-cr-1-4-or-below-no-flying-or-swim-speed"
601 | "wild-shape-cr-1-or-below"
602 | "ki"
603 | "monk-unarmored-defense"
604 | "perfect-self"
605 | "slippery-mind"
606 | "mindless-rage"
607 | "barbarian-unarmored-defense"
608 | "divine-intervention-improvement"
609 | "persistent-rage"
610 | "evocation-savant"
611 | "overchannel"
612 | "potent-cantrip"
613 | "font-of-inspiration"
614 | "second-story-work"
615 | "primeval-awareness"
616 | "beast-spells" => Some(Passive),
617 "oath-spells" => Some(Ignored),
619 x if x.starts_with("metamagic-") => {
620 if x.len() == 11 {
621 Some(Choosable(Metamagic))
622 } else {
623 Some(Ignored)
624 }
625 }
626 "hunters-prey" => Some(Choosable(HuntersPrey)),
627 x if x.starts_with("hunters-prey-") => Some(Ignored),
628 "superior-hunters-defense" => Some(Choosable(SuperiorHuntersDefense)),
629 x if x.starts_with("superior-hunters-defenese-") => Some(Ignored),
630 x if x.starts_with("bard-expertise-") || x.starts_with("rogue-expertise-") => {
632 Some(Ignored)
633 } x if x.starts_with("spellcasting-") => Some(Ignored),
635 x if x.starts_with("eldritch-invocation-") => Some(Ignored),
637 x if x.starts_with("circle-spells-") => Some(Ignored),
639 x if x.starts_with("circle-of-the-land-") => Some(Ignored),
641 x if x.starts_with("domain-spells-") => Some(Ignored),
643 x if x.starts_with("flexible-casting-") => Some(Ignored),
645 "dragon-ancestor" => Some(Choosable(DragonAncestor)),
646 x if x.starts_with("dragon-ancestor-") => Some(Ignored),
647 "defensive-tactics" => Some(Choosable(DefensiveTactics)),
648 x if x.starts_with("defensive-tactics-") => Some(Ignored),
649 "multiattack" => Some(Choosable(Multiattack)),
650 x if x.starts_with("multiattack-") => Some(Ignored),
651 "ranger-fighting-style" => Some(Choosable(RangerFightingStyle)),
652 x if x.starts_with("ranger-fighting-style-") => Some(Ignored),
653 x if x.starts_with("favored-enemy-") => Some(Choosable(RangerFavoredEnemyType)),
654 x if x.starts_with("natural-explorer-") => Some(Choosable(RangerTerrainType)),
655 x if x.contains("ability-score-improvement") => {
656 Some(Choosable(AbilityScoreImprovement))
657 }
658 _ => None,
659 }
660 }
661}
662
663impl Classes {
664 pub(super) async fn new_day(&mut self) {
665 futures::stream::iter(self.0.values_mut())
666 .for_each_concurrent(None, |class| class.new_day())
667 .await;
668 }
669}
670
671impl Class {
672 pub(super) async fn new_day(&mut self) {
673 use crate::classes::ClassSpellCasting::*;
674
675 let index = self.index().to_string();
676
677 if let Some(spell_casting) = &mut self.1.spell_casting {
678 match spell_casting {
679 KnowledgePrepared {
680 pending_preparation,
681 spells_prepared_index,
682 ..
683 }
684 | AlreadyKnowPrepared {
685 pending_preparation,
686 spells_prepared_index,
687 ..
688 } => {
689 *pending_preparation = true;
690 spells_prepared_index.clear();
691 }
692 KnowledgeAlreadyPrepared { usable_slots, .. } => {
693 if let Ok(Some(spellcasting_slots)) =
694 get_spellcasting_slots(index.as_str(), self.1.level).await
695 {
696 *usable_slots = spellcasting_slots.into();
697 }
698 }
699 }
700 }
701 }
702
703 pub async fn get_spellcasting_ability_index(&self) -> Result<String, ApiError> {
704 let op = SpellcastingAbilityQuery::build(SpellcastingAbilityQueryVariables {
705 index: Some(self.index().to_string()),
706 });
707
708 let ability_index = Client::new()
709 .post(GRAPHQL_API_URL.as_str())
710 .run_graphql(op)
711 .await?
712 .data
713 .ok_or(ApiError::Schema)?
714 .class
715 .ok_or(ApiError::Schema)?
716 .spellcasting
717 .ok_or(ApiError::Schema)?
718 .spellcasting_ability
719 .index;
720
721 Ok(ability_index)
722 }
723
724 pub async fn get_spellcasting_slots(&self) -> Result<Option<LevelSpellcasting>, ApiError> {
725 get_spellcasting_slots(self.index(), self.1.level).await
726 }
727
728 pub async fn set_level(
729 &mut self,
730 new_level: u8,
731 ) -> Result<Vec<ChoosableCustomLevelFeature>, ApiError> {
732 let op = LevelFeaturesQuery::build(LevelFeaturesQueryVariables {
733 class: Some(StringFilter(self.index().to_string())),
734 level: Some(LevelFilter {
735 gt: Some(self.1.level),
736 lte: Some(new_level),
737 gte: None,
738 }),
739 });
740
741 let features = Client::new()
742 .post(GRAPHQL_API_URL.as_str())
743 .run_graphql(op)
744 .await?
745 .data
746 .ok_or(ApiError::Schema)?
747 .features
748 .ok_or(ApiError::Schema)?;
749
750 let mut pending_features = vec![];
751
752 features
753 .iter()
754 .filter_map(|feature| CustomLevelFeatureType::identify(feature.index.clone()))
755 .for_each(|feature| match feature {
756 CustomLevelFeatureType::Passive => {}
757 CustomLevelFeatureType::Choosable(feature) => {
758 pending_features.push(feature);
759 }
760 CustomLevelFeatureType::Sheet(feature) => match feature {
761 SheetLevelFeatureType::PrimalChampion => {
762 self.1.abilities_modifiers.strength.score += 4;
763 self.1.abilities_modifiers.dexterity.score += 4;
764 }
765 },
766 Ignored => {}
767 });
768
769 self.1.level = new_level;
770
771 Ok(pending_features)
772 }
773
774 pub async fn get_levels_features(
775 &self,
776 from_level: Option<u8>,
777 passive: bool,
778 ) -> Result<Vec<String>, ApiError> {
779 let op = LevelFeaturesQuery::build(LevelFeaturesQueryVariables {
780 class: Some(StringFilter(self.index().to_string())),
781 level: Some(LevelFilter {
782 gte: Some(from_level.unwrap_or(0)),
783 lte: Some(self.1.level),
784 gt: None,
785 }),
786 });
787
788 let features = Client::new()
789 .post(GRAPHQL_API_URL.as_str())
790 .run_graphql(op)
791 .await?
792 .data
793 .ok_or(ApiError::Schema)?
794 .features
795 .ok_or(ApiError::Schema)?;
796
797 let mut features: Vec<String> = features
799 .into_iter()
800 .filter(
801 |feature| match CustomLevelFeatureType::identify(feature.index.clone()) {
802 None => true,
803 Some(custom_type) => match custom_type {
804 CustomLevelFeatureType::Passive => passive,
805 _ => false,
806 },
807 },
808 )
809 .map(|feature| feature.index)
810 .collect();
811
812 let features: Vec<String> = {
813 lazy_static! {
814 static ref CR_REGEX: regex::Regex =
815 regex::Regex::new(r"destroy-undead-cr-([0-9]+(?:-[0-9]+)?)\-or-below").unwrap();
816 }
817
818 let mut found = false;
819
820 features
821 .iter_mut()
822 .rev()
823 .filter(|feature| {
824 if CR_REGEX.is_match(feature) {
825 if found {
826 false
827 } else {
828 found = true;
829 true
830 }
831 } else {
832 true
833 }
834 })
835 .map(|feature| feature.clone())
836 .collect()
837 };
838
839 lazy_static! {
840 static ref DICE_REGEX: regex::Regex = regex::Regex::new(r"^(.+)-d(\d+)$").unwrap();
841 }
842
843 let mut grouped_features: HashMap<String, u32> = HashMap::new();
844 for feature in &features {
845 if let Some(caps) = DICE_REGEX.captures(feature) {
846 if caps.len() == 3 {
847 let prefix = caps.get(1).unwrap().as_str().to_string();
848 let dice_value = caps.get(2).unwrap().as_str().parse::<u32>().unwrap();
849
850 let current_max = grouped_features.entry(prefix).or_insert(0);
851 if dice_value > *current_max {
852 *current_max = dice_value;
853 }
854 }
855 }
856 }
857
858 let mut features: Vec<String> = features
859 .into_iter()
860 .filter(|feature| {
861 if let Some(caps) = DICE_REGEX.captures(feature) {
862 let prefix = caps.get(1).unwrap().as_str();
863 let dice_value = caps
864 .get(2)
865 .unwrap()
866 .as_str()
867 .parse::<u32>()
868 .expect("Parsing dice value");
869
870 if let Some(&max_dice) = grouped_features.get(prefix) {
871 return dice_value == max_dice;
872 }
873 }
874 true
875 })
876 .collect();
877
878 if !passive {
880 if let Some(multiattack) = &self.1.multiattack {
881 features.push(multiattack.clone());
882 }
883 if let Some(hunters_prey) = &self.1.hunters_prey {
884 features.push(hunters_prey.clone());
885 }
886 if let Some(metamagic) = &self.1.sorcerer_metamagic {
887 features.append(&mut metamagic.clone());
888 }
889 }
890
891 Ok(features)
892 }
893
894 pub fn apply_option(&mut self, option: ChoosableCustomLevelFeatureOption) {
895 use ChoosableCustomLevelFeatureOption::*;
896
897 match option {
898 StrengthPlusOne | DexterityPlusOne | ConstitutionPlusOne | IntelligencePlusOne
899 | WisdomPlusOne | CharismaPlusOne => self.increase_score(option),
900 PactOfTheChain | PactOfTheBlade | PactOfTheTome => {
901 println!("Pact of the Chain, Blade or Tome not yet implemented");
902 }
903 HuntersPreyGiantKiller | HuntersPreyHordeBreaker | HuntersPreyColossusSlayer => {
904 self.1
905 .hunters_prey
906 .replace(option.as_index_str().to_string());
907 }
908 DefensiveTacticsSteelWill
909 | DefensiveTacticsEscapeTheHorde
910 | DefensiveTacticsMultiattackDefense => {
911 self.1
912 .defensive_tactics
913 .replace(option.as_index_str().to_string());
914 }
915 FighterFightingStyleArchery
916 | FighterFightingStyleDefense
917 | FighterFightingStyleDueling
918 | FighterFightingStyleGreatWeaponFighting
919 | FighterFightingStyleProtection
920 | FighterFightingStyleTwoWeaponFighting
921 | RangerFightingStyleArchery
922 | RangerFightingStyleDefense
923 | RangerFightingStyleDueling
924 | RangerFightingStyleTwoWeaponFighting
925 | FightingStyleDefense
926 | FightingStyleDueling
927 | FightingStyleGreatWeaponFighting
928 | FightingStyleProtection => {
929 if self.1.fighting_style.is_none() {
930 self.1
931 .fighting_style
932 .replace(option.as_index_str().to_string());
933 } else {
934 self.1
935 .additional_fighting_style
936 .replace(option.as_index_str().to_string());
937 }
938 }
939 MultiattackVolley | MultiattackWhirlwindAttack => {
940 self.1
941 .multiattack
942 .replace(option.as_index_str().to_string());
943 }
944 SuperiorHuntersDefenseEvasion
945 | SuperiorHuntersDefenseStandAgainstTheTide
946 | SuperiorHuntersDefenseUncannyDodge => {
947 self.1
948 .superior_hunters_defense
949 .replace(option.as_index_str().to_string());
950 }
951 RangerTerrainTypeArctic
952 | RangerTerrainTypeCoast
953 | RangerTerrainTypeDesert
954 | RangerTerrainTypeForest
955 | RangerTerrainTypeGrassland
956 | RangerTerrainTypeMountain
957 | RangerTerrainTypeSwamp => {
958 self.1
959 .natural_explorer_terrain_type
960 .get_or_insert_with(Vec::new)
961 .push(option.as_index_str().to_string());
962 }
963 RangerFavoredEnemyTypeAberrations
964 | RangerFavoredEnemyTypeBeasts
965 | RangerFavoredEnemyTypeCelestials
966 | RangerFavoredEnemyTypeConstructs
967 | RangerFavoredEnemyTypeDragons
968 | RangerFavoredEnemyTypeElementals
969 | RangerFavoredEnemyTypeFey
970 | RangerFavoredEnemyTypeFiends
971 | RangerFavoredEnemyTypeGiants
972 | RangerFavoredEnemyTypeMonstrosities
973 | RangerFavoredEnemyTypeOozes
974 | RangerFavoredEnemyTypePlants
975 | RangerFavoredEnemyTypeUndead
976 | RangerFavoredEnemyTypeHumanoids => {
977 self.1
978 .ranger_favored_enemy_type
979 .get_or_insert_with(Vec::new)
980 .push(option.as_index_str().to_string());
981 }
982 MetamagicCarefullSpell
983 | MetamagicDistantSpell
984 | MetamagicEmpoweredSpell
985 | MetamagicExtendedSpell
986 | MetamagicHeightenedSpell
987 | MetamagicQuickenedSpell
988 | MetamagicSubtleSpell
989 | MetamagicTwinnedSpell => {
990 self.1
991 .sorcerer_metamagic
992 .get_or_insert_with(Vec::new)
993 .push(option.as_index_str().to_string());
994 }
995 DragonAncestorBlackAcidDamage
996 | DragonAncestorBlueLightningDamage
997 | DragonAncestorBrassFireDamage
998 | DragonAncestorBronzeLightningDamage
999 | DragonAncestorCopperAcidDamage
1000 | DragonAncestorGoldFireDamage
1001 | DragonAncestorGreenPoisonDamage
1002 | DragonAncestorRedFireDamage
1003 | DragonAncestorSilverColdDamage
1004 | DragonAncestorWhiteColdDamage => {
1005 self.1
1006 .sorcerer_dragon_ancestor
1007 .replace(option.as_index_str().to_string());
1008 }
1009 }
1010 }
1011
1012 fn increase_score(&mut self, option: ChoosableCustomLevelFeatureOption) {
1013 match option {
1014 ChoosableCustomLevelFeatureOption::StrengthPlusOne => {
1015 self.1.abilities_modifiers.strength.score += 1;
1016 }
1017 ChoosableCustomLevelFeatureOption::DexterityPlusOne => {
1018 self.1.abilities_modifiers.dexterity.score += 1;
1019 }
1020 ChoosableCustomLevelFeatureOption::ConstitutionPlusOne => {
1021 self.1.abilities_modifiers.constitution.score += 1;
1022 }
1023 ChoosableCustomLevelFeatureOption::IntelligencePlusOne => {
1024 self.1.abilities_modifiers.intelligence.score += 1;
1025 }
1026 ChoosableCustomLevelFeatureOption::WisdomPlusOne => {
1027 self.1.abilities_modifiers.wisdom.score += 1;
1028 }
1029 ChoosableCustomLevelFeatureOption::CharismaPlusOne => {
1030 self.1.abilities_modifiers.charisma.score += 1;
1031 }
1032 _ => {}
1033 }
1034 }
1035}
1036
1037pub async fn get_spellcasting_slots(
1038 index: &str,
1039 level: u8,
1040) -> Result<Option<LevelSpellcasting>, ApiError> {
1041 let op = SpellcastingQuery::build(SpellcastingQueryVariables {
1042 index: Some(format!("{}-{}", index, level)),
1043 });
1044
1045 let spellcasting_slots = Client::new()
1046 .post(GRAPHQL_API_URL.as_str())
1047 .run_graphql(op)
1048 .await?
1049 .data
1050 .ok_or(ApiError::Schema)?
1051 .level
1052 .ok_or(ApiError::Schema)?
1053 .spellcasting;
1054
1055 Ok(spellcasting_slots)
1056}