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