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 | "feral-instinct"
511 | "feral-senses"
512 | "fighter-fighting-style-archery"
513 | "fighter-fighting-style-protection"
514 | "fighter-fighting-style-defense"
515 | "fighter-fighting-style-dueling"
516 | "fighter-fighting-style-great-weapon-fighting"
517 | "fighter-fighting-style-two-weapon-fighting"
518 | "fighting-style-defense"
519 | "fighting-style-dueling"
520 | "fighting-style-great-weapon-fighting"
521 | "foe-slayer"
522 | "hurl-through-hell"
523 | "improved-critical"
524 | "improved-divine-smite"
525 | "indomitable-1-use"
526 | "indomitable-2-uses"
527 | "indomitable-3-uses"
528 | "indomitable-might"
529 | "ki-empowered-strikes"
530 | "jack-of-all-trades"
531 | "martial-arts"
532 | "monk-evasion"
533 | "monk-timeless-body"
534 | "purity-of-body"
535 | "purity-of-spirit"
536 | "natures-sanctuary"
537 | "natures-ward"
538 | "sculpt-spells"
539 | "ranger-lands-stride"
540 | "relentless-rage"
541 | "reliable-talent"
542 | "remarkable-athlete"
543 | "rogue-evasion"
544 | "superior-critical"
545 | "superior-inspiration"
546 | "supreme-healing"
547 | "supreme-sneak"
548 | "survivor"
549 | "thiefs-reflexes"
550 | "thieves-cant"
551 | "tongue-of-the-sun-and-moon"
552 | "tranquility"
553 | "unarmored-movement-1"
554 | "unarmored-movement-2"
555 | "use-magic-device"
556 | "wild-shape-cr-1-2-or-below-no-flying-speed"
557 | "wild-shape-cr-1-4-or-below-no-flying-or-swim-speed"
558 | "wild-shape-cr-1-or-below"
559 | "ki"
560 | "monk-unarmored-defense"
561 | "perfect-self"
562 | "slippery-mind"
563 | "mindless-rage"
564 | "barbarian-unarmored-defense"
565 | "divine-intervention-improvement"
566 | "persistent-rage"
567 | "evocation-savant"
568 | "overchannel"
569 | "potent-cantrip"
570 | "second-story-work"
571 | "primeval-awareness"
572 | "beast-spells" => Some(Passive),
573 "oath-spells" => Some(Ignored),
575 "hunters-prey" => Some(Choosable(HuntersPrey)),
576 "superior-hunters-defense" => Some(Choosable(SuperiorHuntersDefense)),
577 x if x.starts_with("bard-expertise-") || x.starts_with("rogue-expertise-") => {
579 Some(Ignored)
580 } x if x.starts_with("spellcasting-") => Some(Ignored),
582 x if x.starts_with("eldritch-invocation-") => Some(Ignored),
584 x if x.starts_with("circle-spells-") => Some(Ignored),
586 x if x.starts_with("circle-of-the-land-") => Some(Ignored),
588 x if x.starts_with("domain-spells-") => Some(Ignored),
590 x if x.starts_with("defensive-tactics") => Some(Choosable(DefensiveTactics)),
591 x if x.starts_with("multiattack") => Some(Choosable(Multiattack)),
592 x if x.starts_with("ranger-fighting-style") => Some(Choosable(RangerFightingStyle)),
593 x if x.starts_with("favored-enemy-") => Some(Choosable(RangerFavoredEnemyType)),
594 x if x.starts_with("natural-explorer-") => Some(Choosable(RangerTerrainType)),
595 x if x.contains("ability-score-improvement") => {
596 Some(Choosable(AbilityScoreImprovement))
597 }
598 _ => None,
599 }
600 }
601}
602
603impl Classes {
604 pub(super) async fn new_day(&mut self) {
605 futures::stream::iter(self.0.values_mut())
606 .for_each_concurrent(None, |class| class.new_day())
607 .await;
608 }
609}
610
611impl Class {
612 pub(super) async fn new_day(&mut self) {
613 use crate::classes::ClassSpellCasting::*;
614
615 let index = self.index().to_string();
616
617 if let Some(spell_casting) = &mut self.1.spell_casting {
618 match spell_casting {
619 KnowledgePrepared {
620 pending_preparation,
621 spells_prepared_index,
622 ..
623 }
624 | AlreadyKnowPrepared {
625 pending_preparation,
626 spells_prepared_index,
627 ..
628 } => {
629 *pending_preparation = true;
630 spells_prepared_index.clear();
631 }
632 KnowledgeAlreadyPrepared { usable_slots, .. } => {
633 if let Ok(Some(spellcasting_slots)) =
634 get_spellcasting_slots(index.as_str(), self.1.level).await
635 {
636 *usable_slots = spellcasting_slots.into();
637 }
638 }
639 }
640 }
641 }
642
643 pub async fn get_spellcasting_ability_index(&self) -> Result<String, ApiError> {
644 let op = SpellcastingAbilityQuery::build(SpellcastingAbilityQueryVariables {
645 index: Some(self.index().to_string()),
646 });
647
648 let ability_index = Client::new()
649 .post(GRAPHQL_API_URL.as_str())
650 .run_graphql(op)
651 .await?
652 .data
653 .ok_or(ApiError::Schema)?
654 .class
655 .ok_or(ApiError::Schema)?
656 .spellcasting
657 .ok_or(ApiError::Schema)?
658 .spellcasting_ability
659 .index;
660
661 Ok(ability_index)
662 }
663
664 pub async fn get_spellcasting_slots(&self) -> Result<Option<LevelSpellcasting>, ApiError> {
665 get_spellcasting_slots(self.index(), self.1.level).await
666 }
667
668 pub async fn set_level(
669 &mut self,
670 new_level: u8,
671 ) -> Result<Vec<ChoosableCustomLevelFeature>, ApiError> {
672 let op = LevelFeaturesQuery::build(LevelFeaturesQueryVariables {
673 class: Some(StringFilter(self.index().to_string())),
674 level: Some(LevelFilter {
675 gt: Some(self.1.level),
676 lte: Some(new_level),
677 gte: None,
678 }),
679 });
680
681 let features = Client::new()
682 .post(GRAPHQL_API_URL.as_str())
683 .run_graphql(op)
684 .await?
685 .data
686 .ok_or(ApiError::Schema)?
687 .features
688 .ok_or(ApiError::Schema)?;
689
690 let mut pending_features = vec![];
691
692 features
693 .iter()
694 .filter_map(|feature| CustomLevelFeatureType::identify(feature.index.clone()))
695 .for_each(|feature| match feature {
696 CustomLevelFeatureType::Passive => {}
697 CustomLevelFeatureType::Choosable(feature) => {
698 pending_features.push(feature);
699 }
700 CustomLevelFeatureType::Sheet(feature) => match feature {
701 SheetLevelFeatureType::PrimalChampion => {
702 self.1.abilities_modifiers.strength.score += 4;
703 self.1.abilities_modifiers.dexterity.score += 4;
704 }
705 },
706 Ignored => {}
707 });
708
709 self.1.level = new_level;
710
711 Ok(pending_features)
712 }
713
714 pub async fn get_levels_features(
715 &self,
716 from_level: Option<u8>,
717 passive: bool,
718 ) -> Result<Vec<String>, ApiError> {
719 let op = LevelFeaturesQuery::build(LevelFeaturesQueryVariables {
720 class: Some(StringFilter(self.index().to_string())),
721 level: Some(LevelFilter {
722 gte: Some(from_level.unwrap_or(0)),
723 lte: Some(self.1.level),
724 gt: None,
725 }),
726 });
727
728 let features = Client::new()
729 .post(GRAPHQL_API_URL.as_str())
730 .run_graphql(op)
731 .await?
732 .data
733 .ok_or(ApiError::Schema)?
734 .features
735 .ok_or(ApiError::Schema)?;
736
737 let mut features: Vec<String> = features
739 .into_iter()
740 .filter(
741 |feature| match CustomLevelFeatureType::identify(feature.index.clone()) {
742 None => true,
743 Some(custom_type) => match custom_type {
744 CustomLevelFeatureType::Passive => passive,
745 _ => false,
746 },
747 },
748 )
749 .map(|feature| feature.index)
750 .collect();
751
752 let features: Vec<String> = {
753 lazy_static! {
754 static ref CR_REGEX: regex::Regex =
755 regex::Regex::new(r"destroy-undead-cr-([0-9]+(?:-[0-9]+)?)\-or-below").unwrap();
756 }
757
758 let mut found = false;
759
760 features
761 .iter_mut()
762 .rev()
763 .filter(|feature| {
764 if CR_REGEX.is_match(feature) {
765 if found {
766 false
767 } else {
768 found = true;
769 true
770 }
771 } else {
772 true
773 }
774 })
775 .map(|feature| feature.clone())
776 .collect()
777 };
778
779 lazy_static! {
780 static ref DICE_REGEX: regex::Regex = regex::Regex::new(r"^(.+)-d(\d+)$").unwrap();
781 }
782
783 let mut grouped_features: HashMap<String, u32> = HashMap::new();
784 for feature in &features {
785 if let Some(caps) = DICE_REGEX.captures(feature) {
786 if caps.len() == 3 {
787 let prefix = caps.get(1).unwrap().as_str().to_string();
788 let dice_value = caps.get(2).unwrap().as_str().parse::<u32>().unwrap();
789
790 let current_max = grouped_features.entry(prefix).or_insert(0);
791 if dice_value > *current_max {
792 *current_max = dice_value;
793 }
794 }
795 }
796 }
797
798 let mut features: Vec<String> = features
799 .into_iter()
800 .filter(|feature| {
801 if let Some(caps) = DICE_REGEX.captures(feature) {
802 let prefix = caps.get(1).unwrap().as_str();
803 let dice_value = caps
804 .get(2)
805 .unwrap()
806 .as_str()
807 .parse::<u32>()
808 .expect("Parsing dice value");
809
810 if let Some(&max_dice) = grouped_features.get(prefix) {
811 return dice_value == max_dice;
812 }
813 }
814 true
815 })
816 .collect();
817
818 if !passive {
820 if let Some(multiattack) = &self.1.multiattack {
821 features.push(multiattack.clone());
822 }
823 if let Some(hunters_prey) = &self.1.hunters_prey {
824 features.push(hunters_prey.clone());
825 }
826 }
827
828 Ok(features)
829 }
830
831 pub fn apply_option(&mut self, option: ChoosableCustomLevelFeatureOption) {
832 use ChoosableCustomLevelFeatureOption::*;
833
834 match option {
835 StrengthPlusOne | DexterityPlusOne | ConstitutionPlusOne | IntelligencePlusOne
836 | WisdomPlusOne | CharismaPlusOne => self.increase_score(option),
837 BardProficiencyStrength
838 | BardProficiencyDexterity
839 | BardProficiencyConstitution
840 | BardProficiencyIntelligence
841 | BardProficiencyWisdom
842 | BardProficiencyCharisma => self.set_proficiency(option),
843 PactOfTheChain | PactOfTheBlade | PactOfTheTome => {
844 println!("Pact of the Chain, Blade or Tome not yet implemented");
845 }
846 HuntersPreyGiantKiller | HuntersPreyHordeBreaker | HuntersPreyColossusSlayer => {
847 self.1
848 .hunters_prey
849 .replace(option.as_index_str().to_string());
850 }
851 DefensiveTacticsSteelWill
852 | DefensiveTacticsEscapeTheHorde
853 | DefensiveTacticsMultiattackDefense => {
854 self.1
855 .defensive_tactics
856 .replace(option.as_index_str().to_string());
857 }
858 FighterFightingStyleArchery
859 | FighterFightingStyleDefense
860 | FighterFightingStyleDueling
861 | FighterFightingStyleGreatWeaponFighting
862 | FighterFightingStyleProtection
863 | FighterFightingStyleTwoWeaponFighting
864 | RangerFightingStyleArchery
865 | RangerFightingStyleDefense
866 | RangerFightingStyleDueling
867 | RangerFightingStyleTwoWeaponFighting
868 | FightingStyleDefense
869 | FightingStyleDueling
870 | FightingStyleGreatWeaponFighting
871 | FightingStyleProtection => {
872 if self.1.fighting_style.is_none() {
873 self.1
874 .fighting_style
875 .replace(option.as_index_str().to_string());
876 } else {
877 self.1
878 .additional_fighting_style
879 .replace(option.as_index_str().to_string());
880 }
881 }
882 MultiattackVolley | MultiattackWhirlwindAttack => {
883 self.1
884 .multiattack
885 .replace(option.as_index_str().to_string());
886 }
887 SuperiorHuntersDefenseEvasion
888 | SuperiorHuntersDefenseStandAgainstTheTide
889 | SuperiorHuntersDefenseUncannyDodge => {
890 self.1
891 .superior_hunters_defense
892 .replace(option.as_index_str().to_string());
893 }
894 RangerTerrainTypeArctic
895 | RangerTerrainTypeCoast
896 | RangerTerrainTypeDesert
897 | RangerTerrainTypeForest
898 | RangerTerrainTypeGrassland
899 | RangerTerrainTypeMountain
900 | RangerTerrainTypeSwamp => {
901 self.1
902 .natural_explorer_terrain_type
903 .get_or_insert_with(Vec::new)
904 .push(option.as_index_str().to_string());
905 }
906 RangerFavoredEnemyTypeAberrations
907 | RangerFavoredEnemyTypeBeasts
908 | RangerFavoredEnemyTypeCelestials
909 | RangerFavoredEnemyTypeConstructs
910 | RangerFavoredEnemyTypeDragons
911 | RangerFavoredEnemyTypeElementals
912 | RangerFavoredEnemyTypeFey
913 | RangerFavoredEnemyTypeFiends
914 | RangerFavoredEnemyTypeGiants
915 | RangerFavoredEnemyTypeMonstrosities
916 | RangerFavoredEnemyTypeOozes
917 | RangerFavoredEnemyTypePlants
918 | RangerFavoredEnemyTypeUndead
919 | RangerFavoredEnemyTypeHumanoids => {
920 self.1
921 .ranger_favored_enemy_type
922 .get_or_insert_with(Vec::new)
923 .push(option.as_index_str().to_string());
924 }
925 }
926 }
927
928 fn increase_score(&mut self, option: ChoosableCustomLevelFeatureOption) {
929 match option {
930 ChoosableCustomLevelFeatureOption::StrengthPlusOne => {
931 self.1.abilities_modifiers.strength.score += 1;
932 }
933 ChoosableCustomLevelFeatureOption::DexterityPlusOne => {
934 self.1.abilities_modifiers.dexterity.score += 1;
935 }
936 ChoosableCustomLevelFeatureOption::ConstitutionPlusOne => {
937 self.1.abilities_modifiers.constitution.score += 1;
938 }
939 ChoosableCustomLevelFeatureOption::IntelligencePlusOne => {
940 self.1.abilities_modifiers.intelligence.score += 1;
941 }
942 ChoosableCustomLevelFeatureOption::WisdomPlusOne => {
943 self.1.abilities_modifiers.wisdom.score += 1;
944 }
945 ChoosableCustomLevelFeatureOption::CharismaPlusOne => {
946 self.1.abilities_modifiers.charisma.score += 1;
947 }
948 _ => {}
949 }
950 }
951
952 fn set_proficiency(&mut self, option: ChoosableCustomLevelFeatureOption) {
953 match option {
954 ChoosableCustomLevelFeatureOption::BardProficiencyStrength => {
955 self.1.abilities_modifiers.strength.proficiency = true;
956 }
957 ChoosableCustomLevelFeatureOption::BardProficiencyDexterity => {
958 self.1.abilities_modifiers.dexterity.proficiency = true;
959 }
960 ChoosableCustomLevelFeatureOption::BardProficiencyConstitution => {
961 self.1.abilities_modifiers.constitution.proficiency = true;
962 }
963 ChoosableCustomLevelFeatureOption::BardProficiencyIntelligence => {
964 self.1.abilities_modifiers.intelligence.proficiency = true;
965 }
966 ChoosableCustomLevelFeatureOption::BardProficiencyWisdom => {
967 self.1.abilities_modifiers.wisdom.proficiency = true;
968 }
969 ChoosableCustomLevelFeatureOption::BardProficiencyCharisma => {
970 self.1.abilities_modifiers.charisma.proficiency = true;
971 }
972 _ => {}
973 }
974 }
975}
976
977pub async fn get_spellcasting_slots(
978 index: &str,
979 level: u8,
980) -> Result<Option<LevelSpellcasting>, ApiError> {
981 let op = SpellcastingQuery::build(SpellcastingQueryVariables {
982 index: Some(format!("{}-{}", index, level)),
983 });
984
985 let spellcasting_slots = Client::new()
986 .post(GRAPHQL_API_URL.as_str())
987 .run_graphql(op)
988 .await?
989 .data
990 .ok_or(ApiError::Schema)?
991 .level
992 .ok_or(ApiError::Schema)?
993 .spellcasting;
994
995 Ok(spellcasting_slots)
996}