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