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