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