use super::shared::schema;
use crate::GRAPHQL_API_URL;
use crate::api::classes::CustomLevelFeatureType::Ignored;
use crate::api::shared::ApiError;
use crate::classes::{Class, Classes, UsableSlots};
use cynic::http::ReqwestExt;
use cynic::{QueryBuilder, impl_scalar};
use futures::StreamExt;
use lazy_static::lazy_static;
use reqwest::Client;
use serde_json::json;
use std::collections::HashMap;
#[derive(cynic::QueryVariables, Debug)]
struct SpellcastingAbilityQueryVariables {
pub index: Option<String>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(
graphql_type = "Query",
variables = "SpellcastingAbilityQueryVariables"
)]
struct SpellcastingAbilityQuery {
#[arguments(index: $ index)]
pub class: Option<ClassSpellCasting>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Class")]
struct ClassSpellCasting {
pub spellcasting: Option<ClassSpellcasting>,
}
#[derive(cynic::QueryFragment, Debug)]
struct ClassSpellcasting {
#[cynic(rename = "spellcasting_ability")]
pub spellcasting_ability: AbilityScore,
}
#[derive(cynic::QueryFragment, Debug)]
struct AbilityScore {
pub index: String,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct SpellcastingQueryVariables {
pub index: Option<String>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query", variables = "SpellcastingQueryVariables")]
pub struct SpellcastingQuery {
#[arguments(index: $ index)]
pub level: Option<Level>,
}
#[derive(cynic::QueryFragment, Debug)]
pub struct Level {
pub spellcasting: Option<LevelSpellcasting>,
}
#[derive(cynic::QueryFragment, Debug, Copy, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct LevelSpellcasting {
#[cynic(rename = "cantrips_known")]
pub cantrips_known: Option<i32>,
#[cynic(rename = "spell_slots_level_1")]
pub spell_slots_level_1: Option<i32>,
#[cynic(rename = "spell_slots_level_2")]
pub spell_slots_level_2: Option<i32>,
#[cynic(rename = "spell_slots_level_3")]
pub spell_slots_level_3: Option<i32>,
#[cynic(rename = "spell_slots_level_4")]
pub spell_slots_level_4: Option<i32>,
#[cynic(rename = "spell_slots_level_5")]
pub spell_slots_level_5: Option<i32>,
#[cynic(rename = "spell_slots_level_6")]
pub spell_slots_level_6: Option<i32>,
#[cynic(rename = "spell_slots_level_7")]
pub spell_slots_level_7: Option<i32>,
#[cynic(rename = "spell_slots_level_8")]
pub spell_slots_level_8: Option<i32>,
#[cynic(rename = "spell_slots_level_9")]
pub spell_slots_level_9: Option<i32>,
}
impl Into<UsableSlots> for LevelSpellcasting {
fn into(self) -> UsableSlots {
UsableSlots {
cantrip_slots: self.cantrips_known.unwrap_or(0) as u8,
level_1: self.spell_slots_level_1.unwrap_or(0) as u8,
level_2: self.spell_slots_level_2.unwrap_or(0) as u8,
level_3: self.spell_slots_level_3.unwrap_or(0) as u8,
level_4: self.spell_slots_level_4.unwrap_or(0) as u8,
level_5: self.spell_slots_level_5.unwrap_or(0) as u8,
level_6: self.spell_slots_level_6.unwrap_or(0) as u8,
level_7: self.spell_slots_level_7.unwrap_or(0) as u8,
level_8: self.spell_slots_level_8.unwrap_or(0) as u8,
level_9: self.spell_slots_level_9.unwrap_or(0) as u8,
}
}
}
#[derive(cynic::QueryVariables, Debug)]
pub struct LevelFeaturesQueryVariables {
pub class: Option<StringFilter>,
pub level: Option<LevelFilter>,
}
#[derive(serde::Serialize, Debug)]
pub struct LevelFilter {
pub gt: Option<u8>,
pub gte: Option<u8>,
pub lte: Option<u8>,
}
impl_scalar!(LevelFilter, schema::IntFilter);
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query", variables = "LevelFeaturesQueryVariables")]
pub struct LevelFeaturesQuery {
#[arguments(class: $ class, level: $level )]
pub features: Option<Vec<Feature>>,
}
#[derive(cynic::QueryFragment, Debug)]
pub struct Feature {
pub index: String,
}
#[derive(cynic::Scalar, Debug, Clone)]
pub struct StringFilter(pub String);
#[derive(Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub enum ChoosableCustomLevelFeature {
AbilityScoreImprovement,
HuntersPrey,
DefensiveTactics,
WarlockPact,
AdditionalFighterFightingStyle,
FighterFightingStyle,
RangerFightingStyle,
MultiplyTwoSkillProficiency,
ChooseTwoSpellForAnyClass,
ChooseOne6thLevelSpellFromWarlockList,
PaladinFightingStyle,
Multiattack,
SuperiorHuntersDefense,
RangerFavoredEnemyType,
RangerTerrainType,
Metamagic,
EldritchInvocations,
DragonAncestor,
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
pub enum ChoosableCustomLevelFeatureOption {
StrengthPlusOne,
DexterityPlusOne,
ConstitutionPlusOne,
IntelligencePlusOne,
WisdomPlusOne,
CharismaPlusOne,
PactOfTheChain,
PactOfTheBlade,
PactOfTheTome,
FighterFightingStyleArchery,
FighterFightingStyleDefense,
FighterFightingStyleDueling,
FighterFightingStyleGreatWeaponFighting,
FighterFightingStyleProtection,
FighterFightingStyleTwoWeaponFighting,
RangerFightingStyleArchery,
RangerFightingStyleDefense,
RangerFightingStyleDueling,
RangerFightingStyleTwoWeaponFighting,
RangerTerrainTypeArctic,
RangerTerrainTypeCoast,
RangerTerrainTypeDesert,
RangerTerrainTypeForest,
RangerTerrainTypeGrassland,
RangerTerrainTypeMountain,
RangerTerrainTypeSwamp,
RangerFavoredEnemyTypeAberrations,
RangerFavoredEnemyTypeBeasts,
RangerFavoredEnemyTypeCelestials,
RangerFavoredEnemyTypeConstructs,
RangerFavoredEnemyTypeDragons,
RangerFavoredEnemyTypeElementals,
RangerFavoredEnemyTypeFey,
RangerFavoredEnemyTypeFiends,
RangerFavoredEnemyTypeGiants,
RangerFavoredEnemyTypeMonstrosities,
RangerFavoredEnemyTypeOozes,
RangerFavoredEnemyTypePlants,
RangerFavoredEnemyTypeUndead,
RangerFavoredEnemyTypeHumanoids,
FightingStyleDefense,
FightingStyleDueling,
FightingStyleGreatWeaponFighting,
FightingStyleProtection,
HuntersPreyGiantKiller,
HuntersPreyHordeBreaker,
HuntersPreyColossusSlayer,
DefensiveTacticsSteelWill,
DefensiveTacticsEscapeTheHorde,
DefensiveTacticsMultiattackDefense,
MultiattackVolley,
MultiattackWhirlwindAttack,
SuperiorHuntersDefenseEvasion,
SuperiorHuntersDefenseStandAgainstTheTide,
SuperiorHuntersDefenseUncannyDodge,
MetamagicCarefulSpell,
MetamagicDistantSpell,
MetamagicEmpoweredSpell,
MetamagicExtendedSpell,
MetamagicHeightenedSpell,
MetamagicQuickenedSpell,
MetamagicSubtleSpell,
MetamagicTwinnedSpell,
EldritchInvocationAgonizingBlast,
EldritchInvocationArmorOfShadows,
EldritchInvocationAscendantStep,
EldritchInvocationBeastSpeech,
EldritchInvocationBeguilingInfluence,
EldritchInvocationBewitchingWhispers,
EldritchInvocationBookOfAncientSecrets,
EldritchInvocationChainsOfCarceri,
EldritchInvocationDevilsSight,
EldritchInvocationDreadfulWord,
EldritchInvocationEldritchSight,
EldritchInvocationEldritchSpear,
EldritchInvocationEyesOfTheRuneKeeper,
EldritchInvocationFiendishVigor,
EldritchInvocationGazeOfTwoMinds,
EldritchInvocationLifedrinker,
EldritchInvocationMaskOfManyFaces,
EldritchInvocationMasterOfMyriadForms,
EldritchInvocationMinionsOfChaos,
EldritchInvocationMireTheMind,
EldritchInvocationMistyVisions,
EldritchInvocationOneWithShadows,
EldritchInvocationOtherworldlyLeap,
EldritchInvocationRepellingBlast,
EldritchInvocationSculptorOfFlesh,
EldritchInvocationSignOfIllOmen,
EldritchInvocationThiefOfFiveFates,
EldritchInvocationThirstingBlade,
EldritchInvocationVisionsOfDistantRealms,
EldritchInvocationVoiceOfTheChainMaster,
EldritchInvocationWhispersOfTheGrave,
EldritchInvocationWitchSight,
#[serde(rename = "dragon-ancestor-black---acid-damage")]
DragonAncestorBlackAcidDamage,
#[serde(rename = "dragon-ancestor-blue---lightning-damage")]
DragonAncestorBlueLightningDamage,
#[serde(rename = "dragon-ancestor-brass---fire-damage")]
DragonAncestorBrassFireDamage,
#[serde(rename = "dragon-ancestor-bronze---lightning-damage")]
DragonAncestorBronzeLightningDamage,
#[serde(rename = "dragon-ancestor-copper---acid-damage")]
DragonAncestorCopperAcidDamage,
#[serde(rename = "dragon-ancestor-gold---fire-damage")]
DragonAncestorGoldFireDamage,
#[serde(rename = "dragon-ancestor-green---poison-damage")]
DragonAncestorGreenPoisonDamage,
#[serde(rename = "dragon-ancestor-red---fire-damage")]
DragonAncestorRedFireDamage,
#[serde(rename = "dragon-ancestor-silver---cold-damage")]
DragonAncestorSilverColdDamage,
#[serde(rename = "dragon-ancestor-white---cold-damage")]
DragonAncestorWhiteColdDamage,
}
impl ChoosableCustomLevelFeatureOption {
#[cfg(feature = "serde")]
pub fn as_index_str(&self) -> &str {
serde_variant::to_variant_name(self).unwrap()
}
#[cfg(feature = "serde")]
pub fn from_index_str(index: &str) -> Option<ChoosableCustomLevelFeatureOption> {
#[derive(serde::Deserialize)]
struct Helper {
value: ChoosableCustomLevelFeatureOption,
}
let json = json!({
"value": index
});
serde_json::from_value::<Helper>(json)
.map(|helper| helper.value)
.ok()
}
}
impl ChoosableCustomLevelFeature {
#[cfg(feature = "serde")]
pub fn as_index_str(&self) -> &str {
serde_variant::to_variant_name(self).unwrap()
}
pub fn to_options(&self) -> Vec<Vec<ChoosableCustomLevelFeatureOption>> {
use ChoosableCustomLevelFeatureOption::*;
match self {
ChoosableCustomLevelFeature::AbilityScoreImprovement => {
let ability_names = vec![
StrengthPlusOne,
DexterityPlusOne,
ConstitutionPlusOne,
IntelligencePlusOne,
WisdomPlusOne,
CharismaPlusOne,
];
vec![ability_names.clone(), ability_names]
}
ChoosableCustomLevelFeature::WarlockPact => {
vec![vec![PactOfTheChain, PactOfTheBlade, PactOfTheTome]]
}
ChoosableCustomLevelFeature::AdditionalFighterFightingStyle
| ChoosableCustomLevelFeature::FighterFightingStyle => {
vec![vec![
FighterFightingStyleArchery,
FighterFightingStyleDefense,
FighterFightingStyleDueling,
FighterFightingStyleGreatWeaponFighting,
FighterFightingStyleProtection,
FighterFightingStyleTwoWeaponFighting,
]]
}
ChoosableCustomLevelFeature::RangerFightingStyle => {
vec![vec![
RangerFightingStyleArchery,
RangerFightingStyleDefense,
RangerFightingStyleDueling,
RangerFightingStyleTwoWeaponFighting,
]]
}
ChoosableCustomLevelFeature::MultiplyTwoSkillProficiency => {
vec![vec![]]
}
ChoosableCustomLevelFeature::ChooseTwoSpellForAnyClass => {
vec![vec![]]
}
ChoosableCustomLevelFeature::ChooseOne6thLevelSpellFromWarlockList => {
vec![vec![]]
}
ChoosableCustomLevelFeature::PaladinFightingStyle => {
vec![vec![
FightingStyleDefense,
FightingStyleDueling,
FightingStyleGreatWeaponFighting,
FightingStyleProtection,
]]
}
ChoosableCustomLevelFeature::HuntersPrey => {
vec![vec![
HuntersPreyGiantKiller,
HuntersPreyHordeBreaker,
HuntersPreyColossusSlayer,
]]
}
ChoosableCustomLevelFeature::DefensiveTactics => {
vec![vec![
DefensiveTacticsSteelWill,
DefensiveTacticsEscapeTheHorde,
DefensiveTacticsMultiattackDefense,
]]
}
ChoosableCustomLevelFeature::Multiattack => {
vec![vec![MultiattackWhirlwindAttack, MultiattackVolley]]
}
ChoosableCustomLevelFeature::SuperiorHuntersDefense => {
vec![vec![
SuperiorHuntersDefenseEvasion,
SuperiorHuntersDefenseStandAgainstTheTide,
SuperiorHuntersDefenseUncannyDodge,
]]
}
ChoosableCustomLevelFeature::RangerFavoredEnemyType => {
vec![vec![
RangerFavoredEnemyTypeAberrations,
RangerFavoredEnemyTypeBeasts,
RangerFavoredEnemyTypeCelestials,
RangerFavoredEnemyTypeConstructs,
RangerFavoredEnemyTypeDragons,
RangerFavoredEnemyTypeElementals,
RangerFavoredEnemyTypeFey,
RangerFavoredEnemyTypeFiends,
RangerFavoredEnemyTypeGiants,
RangerFavoredEnemyTypeMonstrosities,
RangerFavoredEnemyTypeOozes,
RangerFavoredEnemyTypePlants,
RangerFavoredEnemyTypeUndead,
]]
}
ChoosableCustomLevelFeature::RangerTerrainType => {
vec![vec![
RangerTerrainTypeArctic,
RangerTerrainTypeCoast,
RangerTerrainTypeDesert,
RangerTerrainTypeForest,
RangerTerrainTypeGrassland,
RangerTerrainTypeMountain,
RangerTerrainTypeSwamp,
]]
}
ChoosableCustomLevelFeature::Metamagic => {
let all_metamagics = vec![
MetamagicCarefulSpell,
MetamagicDistantSpell,
MetamagicEmpoweredSpell,
MetamagicExtendedSpell,
MetamagicHeightenedSpell,
MetamagicQuickenedSpell,
MetamagicSubtleSpell,
MetamagicTwinnedSpell,
];
vec![all_metamagics.clone(), all_metamagics]
}
ChoosableCustomLevelFeature::EldritchInvocations => {
let all_eldritch_invocations = vec![
EldritchInvocationAgonizingBlast,
EldritchInvocationArmorOfShadows,
EldritchInvocationAscendantStep,
EldritchInvocationBeastSpeech,
EldritchInvocationBeguilingInfluence,
EldritchInvocationBewitchingWhispers,
EldritchInvocationBookOfAncientSecrets,
EldritchInvocationChainsOfCarceri,
EldritchInvocationDevilsSight,
EldritchInvocationDreadfulWord,
EldritchInvocationEldritchSight,
EldritchInvocationEldritchSpear,
EldritchInvocationEyesOfTheRuneKeeper,
EldritchInvocationFiendishVigor,
EldritchInvocationGazeOfTwoMinds,
EldritchInvocationLifedrinker,
EldritchInvocationMaskOfManyFaces,
EldritchInvocationMasterOfMyriadForms,
EldritchInvocationMinionsOfChaos,
EldritchInvocationMireTheMind,
EldritchInvocationMistyVisions,
EldritchInvocationOneWithShadows,
EldritchInvocationOtherworldlyLeap,
EldritchInvocationRepellingBlast,
EldritchInvocationSculptorOfFlesh,
EldritchInvocationSignOfIllOmen,
EldritchInvocationThiefOfFiveFates,
EldritchInvocationThirstingBlade,
EldritchInvocationVisionsOfDistantRealms,
EldritchInvocationVoiceOfTheChainMaster,
EldritchInvocationWhispersOfTheGrave,
EldritchInvocationWitchSight,
];
vec![all_eldritch_invocations.clone(), all_eldritch_invocations]
}
ChoosableCustomLevelFeature::DragonAncestor => {
vec![vec![
DragonAncestorBlackAcidDamage,
DragonAncestorBlueLightningDamage,
DragonAncestorBrassFireDamage,
DragonAncestorBronzeLightningDamage,
DragonAncestorCopperAcidDamage,
DragonAncestorGoldFireDamage,
DragonAncestorGreenPoisonDamage,
DragonAncestorRedFireDamage,
DragonAncestorSilverColdDamage,
DragonAncestorWhiteColdDamage,
]]
}
}
}
}
pub enum SheetLevelFeatureType {
PrimalChampion,
}
pub enum CustomLevelFeatureType {
Choosable(ChoosableCustomLevelFeature),
Sheet(SheetLevelFeatureType),
Passive,
Ignored,
}
impl CustomLevelFeatureType {
pub fn identify(index: String) -> Option<CustomLevelFeatureType> {
use ChoosableCustomLevelFeature::*;
use CustomLevelFeatureType::*;
use SheetLevelFeatureType::*;
match index.as_str() {
"bard-college"
| "divine-domain"
| "monastic-tradition"
| "sacred-oath"
| "ranger-archetype"
| "sorcerous-origin"
| "druid-circle"
| "primal-path"
| "martial-archetype"
| "roguish-archetype"
| "otherworldly-patron" => Some(Ignored),
"pact-boon" => Some(Choosable(WarlockPact)),
"additional-fighting-style" => Some(Choosable(AdditionalFighterFightingStyle)),
"fighter-fighting-style" => Some(Choosable(FighterFightingStyle)),
"bonus-proficiency" => Some(Passive),
"bonus-proficiencies" => Some(Ignored),
"additional-magical-secrets" | "bonus-cantrip" => Some(Ignored),
"channel-divinity-1-rest" | "channel-divinity-2-rest" | "channel-divinity-3-rest" => {
Some(Ignored)
}
"magical-secrets-1" | "magical-secrets-2" | "magical-secrets-3" => Some(Ignored),
"mystic-arcanum-6th-level"
| "mystic-arcanum-7th-level"
| "mystic-arcanum-8th-level"
| "mystic-arcanum-9th-level" => Some(Choosable(ChooseOne6thLevelSpellFromWarlockList)),
"paladin-fighting-style" => Some(Choosable(PaladinFightingStyle)),
"primal-champion" => Some(Sheet(PrimalChampion)),
"diamond-soul" => Some(Passive),
"arcane-recovery"
| "arcane-tradition"
| "archdruid"
| "aura-improvements"
| "aura-of-courage"
| "aura-of-devotion"
| "aura-of-protection"
| "blessed-healer"
| "blindsense"
| "brutal-critical-1-dice"
| "brutal-critical-2-dice"
| "brutal-critical-3-dice"
| "danger-sense"
| "dark-ones-blessing"
| "dark-ones-own-luck"
| "destroy-undead-cr-1-or-below"
| "destroy-undead-cr-2-or-below"
| "destroy-undead-cr-3-or-below"
| "destroy-undead-cr-4-or-below"
| "destroy-undead-cr-1-2-or-below"
| "disciple-of-life"
| "divine-health"
| "draconic-resilience"
| "font-of-magic"
| "druid-lands-stride"
| "druid-timeless-body"
| "druidic"
| "elusive"
| "empowered-evocation"
| "fast-movement"
| "feral-instinct"
| "feral-senses"
| "foe-slayer"
| "hurl-through-hell"
| "improved-critical"
| "improved-divine-smite"
| "indomitable-1-use"
| "indomitable-2-uses"
| "indomitable-3-uses"
| "indomitable-might"
| "ki-empowered-strikes"
| "jack-of-all-trades"
| "martial-arts"
| "monk-evasion"
| "monk-timeless-body"
| "purity-of-body"
| "purity-of-spirit"
| "natures-sanctuary"
| "natures-ward"
| "sculpt-spells"
| "ranger-lands-stride"
| "relentless-rage"
| "reliable-talent"
| "remarkable-athlete"
| "rogue-evasion"
| "superior-critical"
| "superior-inspiration"
| "supreme-healing"
| "supreme-sneak"
| "survivor"
| "thiefs-reflexes"
| "thieves-cant"
| "tongue-of-the-sun-and-moon"
| "tranquility"
| "unarmored-movement-1"
| "unarmored-movement-2"
| "use-magic-device"
| "ki"
| "monk-unarmored-defense"
| "perfect-self"
| "slippery-mind"
| "mindless-rage"
| "barbarian-unarmored-defense"
| "divine-intervention-improvement"
| "persistent-rage"
| "evocation-savant"
| "overchannel"
| "potent-cantrip"
| "font-of-inspiration"
| "second-story-work"
| "primeval-awareness"
| "beast-spells" => Some(Passive),
"oath-spells" => Some(Ignored),
"natural-recovery" => Some(Ignored),
"eldritch-invocations" => Some(Choosable(EldritchInvocations)),
x if x.starts_with("metamagic-") => {
if x.len() == 11 {
Some(Choosable(Metamagic))
} else {
Some(Ignored)
}
}
"hunters-prey" => Some(Choosable(HuntersPrey)),
x if x.starts_with("hunters-prey-") => Some(Ignored),
"superior-hunters-defense" => Some(Choosable(SuperiorHuntersDefense)),
x if x.starts_with("superior-hunters-defenese-") => Some(Ignored),
x if x.starts_with("bard-expertise-") || x.starts_with("rogue-expertise-") => {
Some(Ignored)
} x if x.starts_with("spellcasting-") => Some(Ignored),
x if x.starts_with("eldritch-invocation-") => Some(Ignored),
x if x.starts_with("circle-spells-") => Some(Ignored),
x if x.starts_with("circle-of-the-land") => Some(Ignored),
x if x.starts_with("domain-spells-") => Some(Ignored),
x if x.starts_with("flexible-casting-") => Some(Ignored),
"dragon-ancestor" => Some(Choosable(DragonAncestor)),
x if x.starts_with("dragon-ancestor-") => Some(Ignored),
"defensive-tactics" => Some(Choosable(DefensiveTactics)),
x if x.starts_with("defensive-tactics-") => Some(Ignored),
"multiattack" => Some(Choosable(Multiattack)),
x if x.starts_with("multiattack-") => Some(Ignored),
"ranger-fighting-style" => Some(Choosable(RangerFightingStyle)),
x if x.starts_with("favored-enemy-") => Some(Choosable(RangerFavoredEnemyType)),
x if x.starts_with("natural-explorer-") => Some(Choosable(RangerTerrainType)),
x if x.starts_with("pact-of-the-") => Some(Ignored),
x if x.contains("ability-score-improvement") => {
Some(Choosable(AbilityScoreImprovement))
}
x if x.starts_with("fighting-style-") => Some(Ignored),
x if x.starts_with("fighter-fighting-style-") => Some(Ignored),
x if x.starts_with("ranger-fighting-style-") => Some(Ignored),
_ => None,
}
}
}
impl Classes {
pub(super) async fn new_day(&mut self) {
futures::stream::iter(self.0.values_mut())
.for_each_concurrent(None, |class| class.new_day())
.await;
}
}
impl Class {
pub(super) async fn new_day(&mut self) {
use crate::classes::ClassSpellCasting::*;
let index = self.index().to_string();
if let Some(spell_casting) = &mut self.1.spell_casting {
match spell_casting {
KnowledgePrepared {
pending_preparation,
spells_prepared_index,
..
}
| AlreadyKnowPrepared {
pending_preparation,
spells_prepared_index,
..
} => {
*pending_preparation = true;
spells_prepared_index.clear();
}
KnowledgeAlreadyPrepared { usable_slots, .. } => {
if let Ok(Some(spellcasting_slots)) =
get_spellcasting_slots(index.as_str(), self.1.level).await
{
*usable_slots = spellcasting_slots.into();
}
}
}
}
}
pub async fn get_spellcasting_ability_index(&self) -> Result<String, ApiError> {
let op = SpellcastingAbilityQuery::build(SpellcastingAbilityQueryVariables {
index: Some(self.index().to_string()),
});
let ability_index = Client::new()
.post(GRAPHQL_API_URL.as_str())
.run_graphql(op)
.await?
.data
.ok_or(ApiError::Schema)?
.class
.ok_or(ApiError::Schema)?
.spellcasting
.ok_or(ApiError::Schema)?
.spellcasting_ability
.index;
Ok(ability_index)
}
pub async fn get_spellcasting_slots(&self) -> Result<Option<LevelSpellcasting>, ApiError> {
get_spellcasting_slots(self.index(), self.1.level).await
}
pub async fn set_level(
&mut self,
new_level: u8,
) -> Result<Vec<ChoosableCustomLevelFeature>, ApiError> {
let op = LevelFeaturesQuery::build(LevelFeaturesQueryVariables {
class: Some(StringFilter(self.index().to_string())),
level: Some(LevelFilter {
gt: Some(self.1.level),
lte: Some(new_level),
gte: None,
}),
});
let features = Client::new()
.post(GRAPHQL_API_URL.as_str())
.run_graphql(op)
.await?
.data
.ok_or(ApiError::Schema)?
.features
.ok_or(ApiError::Schema)?;
let mut pending_features = vec![];
features
.iter()
.filter_map(|feature| CustomLevelFeatureType::identify(feature.index.clone()))
.for_each(|feature| match feature {
CustomLevelFeatureType::Passive => {}
CustomLevelFeatureType::Choosable(feature) => {
pending_features.push(feature);
}
CustomLevelFeatureType::Sheet(feature) => match feature {
SheetLevelFeatureType::PrimalChampion => {
let mut abilities = self.1.abilities.lock().unwrap();
abilities.strength.score += 4;
abilities.constitution.score += 4;
}
},
Ignored => {}
});
self.1.level = new_level;
Ok(pending_features)
}
pub async fn get_levels_features(
&self,
from_level: Option<u8>,
passive: bool,
) -> Result<Vec<String>, ApiError> {
let op = LevelFeaturesQuery::build(LevelFeaturesQueryVariables {
class: Some(StringFilter(self.index().to_string())),
level: Some(LevelFilter {
gte: Some(from_level.unwrap_or(0)),
lte: Some(self.1.level),
gt: None,
}),
});
let features = Client::new()
.post(GRAPHQL_API_URL.as_str())
.run_graphql(op)
.await?
.data
.ok_or(ApiError::Schema)?
.features
.ok_or(ApiError::Schema)?;
let features: Vec<String> = features
.into_iter()
.filter_map(
|feature| match CustomLevelFeatureType::identify(feature.index.clone()) {
None => Some(feature.index),
Some(custom_type) => match custom_type {
CustomLevelFeatureType::Passive if passive => Some(feature.index),
_ => None,
},
},
)
.collect();
lazy_static! {
static ref CR_REGEX: regex::Regex =
regex::Regex::new(r"(.*)-cr-([0-9]+(?:-[0-9]+)?)-or-below.*").unwrap();
static ref DICE_REGEX: regex::Regex = regex::Regex::new(r"^(.+)-d(\d+)$").unwrap();
static ref DIE_DICE_REGEX: regex::Regex =
regex::Regex::new(r"^(.+)-(\d+)-(die|dice)$").unwrap();
static ref UNARMORED_MOVEMENT_REGEX: regex::Regex =
regex::Regex::new(r"^(unarmored-movement)-(\d+)$").unwrap();
}
let mut cr_features: HashMap<String, (f32, String)> = HashMap::new();
let mut dice_features: HashMap<String, u32> = HashMap::new();
let mut die_dice_features: HashMap<String, u32> = HashMap::new();
let mut unarmored_movement_features: HashMap<String, (u32, String)> = HashMap::new();
for feature in &features {
if let Some(caps) = CR_REGEX.captures(feature) {
let prefix = caps.get(1).unwrap().as_str().to_string();
let cr_str = caps.get(2).unwrap().as_str();
let cr_value = if cr_str.contains('-') {
let parts: Vec<&str> = cr_str.split('-').collect();
if parts.len() == 2 {
parts[0].parse::<f32>().unwrap_or(0.0)
/ parts[1].parse::<f32>().unwrap_or(1.0)
} else {
0.0
}
} else {
cr_str.parse::<f32>().unwrap_or(0.0)
};
if let Some((existing_cr, _)) = cr_features.get(&prefix) {
if cr_value > *existing_cr {
cr_features.insert(prefix, (cr_value, feature.clone()));
}
} else {
cr_features.insert(prefix, (cr_value, feature.clone()));
}
continue;
}
if let Some(caps) = DICE_REGEX.captures(feature) {
let prefix = caps.get(1).unwrap().as_str().to_string();
let dice_value = caps.get(2).unwrap().as_str().parse::<u32>().unwrap_or(0);
let current_max = dice_features.entry(prefix).or_insert(0);
if dice_value > *current_max {
*current_max = dice_value;
}
continue;
}
if let Some(caps) = DIE_DICE_REGEX.captures(feature) {
let prefix = caps.get(1).unwrap().as_str().to_string();
let dice_value = caps.get(2).unwrap().as_str().parse::<u32>().unwrap_or(0);
let current_max = die_dice_features.entry(prefix).or_insert(0);
if dice_value > *current_max {
*current_max = dice_value;
}
}
if let Some(caps) = UNARMORED_MOVEMENT_REGEX.captures(feature) {
let prefix = caps.get(1).unwrap().as_str().to_string();
let movement_value = caps.get(2).unwrap().as_str().parse::<u32>().unwrap_or(0);
if let Some((existing_value, _)) = unarmored_movement_features.get(&prefix) {
if movement_value > *existing_value {
unarmored_movement_features
.insert(prefix, (movement_value, feature.clone()));
}
} else {
unarmored_movement_features.insert(prefix, (movement_value, feature.clone()));
}
}
}
let mut filtered_features = Vec::new();
let mut has_improved_divine_smite = false;
for feature in &features {
if feature == "improved-divine-smite" {
has_improved_divine_smite = true;
break;
}
}
for feature in features {
if feature == "divine-smite" && has_improved_divine_smite {
continue;
}
if let Some(caps) = CR_REGEX.captures(&feature) {
let prefix = caps.get(1).unwrap().as_str().to_string();
if let Some((_, highest_feature)) = cr_features.get(&prefix) {
if &feature == highest_feature {
filtered_features.push(feature);
}
}
continue;
}
if let Some(caps) = DICE_REGEX.captures(&feature) {
let prefix = caps.get(1).unwrap().as_str().to_string();
let dice_value = caps
.get(2)
.unwrap()
.as_str()
.parse::<u32>()
.expect("Parsing dice value");
if let Some(&max_dice) = dice_features.get(&prefix) {
if dice_value == max_dice {
filtered_features.push(feature);
}
}
continue;
}
if let Some(caps) = DIE_DICE_REGEX.captures(&feature) {
let prefix = caps.get(1).unwrap().as_str().to_string();
let dice_value = caps
.get(2)
.unwrap()
.as_str()
.parse::<u32>()
.expect("Parsing die/dice value");
if let Some(&max_dice) = die_dice_features.get(&prefix) {
if dice_value == max_dice {
filtered_features.push(feature);
}
}
continue;
}
if let Some(caps) = UNARMORED_MOVEMENT_REGEX.captures(&feature) {
let prefix = caps.get(1).unwrap().as_str().to_string();
if let Some((_, highest_feature)) = unarmored_movement_features.get(&prefix) {
if &feature == highest_feature {
filtered_features.push(feature);
}
}
continue;
}
filtered_features.push(feature);
}
let mut features = filtered_features;
if !passive {
if let Some(multiattack) = &self.1.multiattack {
features.push(multiattack.clone());
}
if let Some(hunters_prey) = &self.1.hunters_prey {
features.push(hunters_prey.clone());
}
if let Some(metamagic) = &self.1.sorcerer_metamagic {
features.append(&mut metamagic.clone());
}
if let Some(eldritch_invocation) = &self.1.warlock_eldritch_invocation {
features.append(&mut eldritch_invocation.clone());
}
}
Ok(features)
}
pub fn apply_option(&mut self, option: ChoosableCustomLevelFeatureOption) {
use ChoosableCustomLevelFeatureOption::*;
match option {
StrengthPlusOne | DexterityPlusOne | ConstitutionPlusOne | IntelligencePlusOne
| WisdomPlusOne | CharismaPlusOne => self.increase_score(option),
PactOfTheChain | PactOfTheBlade | PactOfTheTome => {
println!("Pact of the Chain, Blade or Tome not yet implemented");
}
HuntersPreyGiantKiller | HuntersPreyHordeBreaker | HuntersPreyColossusSlayer => {
self.1
.hunters_prey
.replace(option.as_index_str().to_string());
}
DefensiveTacticsSteelWill
| DefensiveTacticsEscapeTheHorde
| DefensiveTacticsMultiattackDefense => {
self.1
.defensive_tactics
.replace(option.as_index_str().to_string());
}
FighterFightingStyleArchery
| FighterFightingStyleDefense
| FighterFightingStyleDueling
| FighterFightingStyleGreatWeaponFighting
| FighterFightingStyleProtection
| FighterFightingStyleTwoWeaponFighting
| RangerFightingStyleArchery
| RangerFightingStyleDefense
| RangerFightingStyleDueling
| RangerFightingStyleTwoWeaponFighting
| FightingStyleDefense
| FightingStyleDueling
| FightingStyleGreatWeaponFighting
| FightingStyleProtection => {
if self.1.fighting_style.is_none() {
self.1
.fighting_style
.replace(option.as_index_str().to_string());
} else {
self.1
.additional_fighting_style
.replace(option.as_index_str().to_string());
}
}
MultiattackVolley | MultiattackWhirlwindAttack => {
self.1
.multiattack
.replace(option.as_index_str().to_string());
}
SuperiorHuntersDefenseEvasion
| SuperiorHuntersDefenseStandAgainstTheTide
| SuperiorHuntersDefenseUncannyDodge => {
self.1
.superior_hunters_defense
.replace(option.as_index_str().to_string());
}
RangerTerrainTypeArctic
| RangerTerrainTypeCoast
| RangerTerrainTypeDesert
| RangerTerrainTypeForest
| RangerTerrainTypeGrassland
| RangerTerrainTypeMountain
| RangerTerrainTypeSwamp => {
self.1
.natural_explorer_terrain_type
.get_or_insert_with(Vec::new)
.push(option.as_index_str().to_string());
}
RangerFavoredEnemyTypeAberrations
| RangerFavoredEnemyTypeBeasts
| RangerFavoredEnemyTypeCelestials
| RangerFavoredEnemyTypeConstructs
| RangerFavoredEnemyTypeDragons
| RangerFavoredEnemyTypeElementals
| RangerFavoredEnemyTypeFey
| RangerFavoredEnemyTypeFiends
| RangerFavoredEnemyTypeGiants
| RangerFavoredEnemyTypeMonstrosities
| RangerFavoredEnemyTypeOozes
| RangerFavoredEnemyTypePlants
| RangerFavoredEnemyTypeUndead
| RangerFavoredEnemyTypeHumanoids => {
self.1
.ranger_favored_enemy_type
.get_or_insert_with(Vec::new)
.push(option.as_index_str().to_string());
}
MetamagicCarefulSpell
| MetamagicDistantSpell
| MetamagicEmpoweredSpell
| MetamagicExtendedSpell
| MetamagicHeightenedSpell
| MetamagicQuickenedSpell
| MetamagicSubtleSpell
| MetamagicTwinnedSpell => {
self.1
.sorcerer_metamagic
.get_or_insert_with(Vec::new)
.push(option.as_index_str().to_string());
}
EldritchInvocationAgonizingBlast
| EldritchInvocationArmorOfShadows
| EldritchInvocationAscendantStep
| EldritchInvocationBeastSpeech
| EldritchInvocationBeguilingInfluence
| EldritchInvocationBewitchingWhispers
| EldritchInvocationBookOfAncientSecrets
| EldritchInvocationChainsOfCarceri
| EldritchInvocationDevilsSight
| EldritchInvocationDreadfulWord
| EldritchInvocationEldritchSight
| EldritchInvocationEldritchSpear
| EldritchInvocationEyesOfTheRuneKeeper
| EldritchInvocationFiendishVigor
| EldritchInvocationGazeOfTwoMinds
| EldritchInvocationLifedrinker
| EldritchInvocationMaskOfManyFaces
| EldritchInvocationMasterOfMyriadForms
| EldritchInvocationMinionsOfChaos
| EldritchInvocationMireTheMind
| EldritchInvocationMistyVisions
| EldritchInvocationOneWithShadows
| EldritchInvocationOtherworldlyLeap
| EldritchInvocationRepellingBlast
| EldritchInvocationSculptorOfFlesh
| EldritchInvocationSignOfIllOmen
| EldritchInvocationThiefOfFiveFates
| EldritchInvocationThirstingBlade
| EldritchInvocationVisionsOfDistantRealms
| EldritchInvocationVoiceOfTheChainMaster
| EldritchInvocationWhispersOfTheGrave
| EldritchInvocationWitchSight => {
self.1
.warlock_eldritch_invocation
.get_or_insert_with(Vec::new)
.push(option.as_index_str().to_string());
}
DragonAncestorBlackAcidDamage
| DragonAncestorBlueLightningDamage
| DragonAncestorBrassFireDamage
| DragonAncestorBronzeLightningDamage
| DragonAncestorCopperAcidDamage
| DragonAncestorGoldFireDamage
| DragonAncestorGreenPoisonDamage
| DragonAncestorRedFireDamage
| DragonAncestorSilverColdDamage
| DragonAncestorWhiteColdDamage => {
self.1
.sorcerer_dragon_ancestor
.replace(option.as_index_str().to_string());
}
}
}
fn increase_score(&mut self, option: ChoosableCustomLevelFeatureOption) {
let mut abilities = self.1.abilities.lock().unwrap();
match option {
ChoosableCustomLevelFeatureOption::StrengthPlusOne => {
abilities.strength.score += 1;
}
ChoosableCustomLevelFeatureOption::DexterityPlusOne => {
abilities.dexterity.score += 1;
}
ChoosableCustomLevelFeatureOption::ConstitutionPlusOne => {
abilities.constitution.score += 1;
}
ChoosableCustomLevelFeatureOption::IntelligencePlusOne => {
abilities.intelligence.score += 1;
}
ChoosableCustomLevelFeatureOption::WisdomPlusOne => {
abilities.wisdom.score += 1;
}
ChoosableCustomLevelFeatureOption::CharismaPlusOne => {
abilities.charisma.score += 1;
}
_ => {}
}
}
}
pub async fn get_spellcasting_slots(
index: &str,
level: u8,
) -> Result<Option<LevelSpellcasting>, ApiError> {
let op = SpellcastingQuery::build(SpellcastingQueryVariables {
index: Some(format!("{}-{}", index, level)),
});
let spellcasting_slots = Client::new()
.post(GRAPHQL_API_URL.as_str())
.run_graphql(op)
.await?
.data
.ok_or(ApiError::Schema)?
.level
.ok_or(ApiError::Schema)?
.spellcasting;
Ok(spellcasting_slots)
}