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