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