From e175cecf75d57242eff038ba9dd9c1aac6b129de Mon Sep 17 00:00:00 2001 From: RakuJa Date: Sun, 5 May 2024 18:34:39 +0200 Subject: [PATCH] feat: Introduce Creature Role (#49) * Query the new db fields (actions, skills, scales) * Return percentage associated with each role * Filter by role * Random gen by role * Divide response data (and Creature) in fields to have a modular and lightweight response * Update dependencies --- Cargo.toml | 14 +- Dockerfile | 2 +- src/db/db_cache.rs | 4 +- src/db/db_communicator.rs | 149 ++++- src/db/db_proxy.rs | 15 +- src/main.rs | 7 + src/models/creature.rs | 291 ++++------ .../creature_component/creature_combat.rs | 89 +++ .../creature_component/creature_core.rs | 70 +++ .../creature_component/creature_extra.rs | 102 ++++ .../creature_component/creature_info.rs | 45 ++ .../creature_spell_caster.rs | 28 + .../creature_component/creature_variant.rs | 17 + .../creature_component/filter_struct.rs | 17 + src/models/creature_component/mod.rs | 7 + src/models/creature_filter_enum.rs | 1 + src/models/creature_metadata/creature_role.rs | 523 ++++++++++++++++++ src/models/creature_metadata/mod.rs | 1 + src/models/db/raw_creature.rs | 8 +- src/models/encounter_structs.rs | 6 +- src/models/items/action.rs | 21 + src/models/items/mod.rs | 3 + src/models/items/skill.rs | 12 + src/models/items/spell_caster_entry.rs | 12 + src/models/items/weapon.rs | 23 + src/models/mod.rs | 2 + src/models/response_data.rs | 55 +- src/models/routers_validator_structs.rs | 5 + src/models/scales_struct/ability_scales.rs | 9 + src/models/scales_struct/ac_scales.rs | 9 + src/models/scales_struct/area_dmg_scales.rs | 7 + src/models/scales_struct/creature_scales.rs | 29 + src/models/scales_struct/hp_scales.rs | 11 + src/models/scales_struct/item_scales.rs | 6 + src/models/scales_struct/mod.rs | 13 + src/models/scales_struct/perception_scales.rs | 10 + src/models/scales_struct/res_weak_scales.rs | 7 + .../scales_struct/saving_throw_scales.rs | 10 + src/models/scales_struct/skill_scales.rs | 10 + .../scales_struct/spell_dc_and_atk_scales.rs | 11 + .../scales_struct/strike_bonus_scales.rs | 9 + src/models/scales_struct/strike_dmg_scales.rs | 9 + src/routes/bestiary.rs | 50 +- src/services/bestiary_service.rs | 5 + src/services/encounter_service.rs | 83 +-- 45 files changed, 1555 insertions(+), 262 deletions(-) create mode 100644 src/models/creature_component/creature_combat.rs create mode 100644 src/models/creature_component/creature_core.rs create mode 100644 src/models/creature_component/creature_extra.rs create mode 100644 src/models/creature_component/creature_info.rs create mode 100644 src/models/creature_component/creature_spell_caster.rs create mode 100644 src/models/creature_component/creature_variant.rs create mode 100644 src/models/creature_component/filter_struct.rs create mode 100644 src/models/creature_component/mod.rs create mode 100644 src/models/creature_metadata/creature_role.rs create mode 100644 src/models/items/action.rs create mode 100644 src/models/items/skill.rs create mode 100644 src/models/items/spell_caster_entry.rs create mode 100644 src/models/scales_struct/ability_scales.rs create mode 100644 src/models/scales_struct/ac_scales.rs create mode 100644 src/models/scales_struct/area_dmg_scales.rs create mode 100644 src/models/scales_struct/creature_scales.rs create mode 100644 src/models/scales_struct/hp_scales.rs create mode 100644 src/models/scales_struct/item_scales.rs create mode 100644 src/models/scales_struct/mod.rs create mode 100644 src/models/scales_struct/perception_scales.rs create mode 100644 src/models/scales_struct/res_weak_scales.rs create mode 100644 src/models/scales_struct/saving_throw_scales.rs create mode 100644 src/models/scales_struct/skill_scales.rs create mode 100644 src/models/scales_struct/spell_dc_and_atk_scales.rs create mode 100644 src/models/scales_struct/strike_bonus_scales.rs create mode 100644 src/models/scales_struct/strike_dmg_scales.rs diff --git a/Cargo.toml b/Cargo.toml index f5a8502..5a10aa4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ authors = ["RakuJa"] # Compiler info edition = "2021" -rust-version = "1.75.0" +rust-version = "1.77.1" description = "Beyond Your Bestiary Explorer (BYBE) is a web service that provides tools to help Pathfinder 2e Game Masters." readme = "README.md" @@ -29,17 +29,19 @@ validator = {version="0.16.1", features = ["derive"]} utoipa = { version = "4.2.0", features = ["actix_extras"] } utoipa-swagger-ui = { version = "6.0.0", features = ["actix-web"] } -sqlx = { version = "0.7.3", features = ["runtime-async-std", "sqlite"] } +sqlx = { version = "0.7.4", features = ["runtime-async-std", "sqlite"] } mini-moka = "0.10.3" -anyhow = "1.0.80" +anyhow = "1.0.81" serde = { version = "1.0.197", features = ["derive"] } -serde_json = "1.0.114" -strum = {version="0.26.1", features = ["derive"]} +serde_json = "1.0.115" +strum = {version="0.26.2", features = ["derive"]} rand = "0.9.0-alpha.0" counter = "0.5.7" dotenvy = "0.15.7" +regex = "1.10.4" -env_logger = "0.11.2" +env_logger = "0.11.3" log = "0.4.21" maplit = "1.0.2" +num-traits = "0.2.18" diff --git a/Dockerfile b/Dockerfile index 3d1bba6..3603dc9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Stage 1: Build the Rust project -FROM rust:1.75-alpine as builder +FROM rust:1-alpine as builder # Set the working directory in the container WORKDIR /app diff --git a/src/db/db_cache.rs b/src/db/db_cache.rs index f034f68..a9e1fe9 100644 --- a/src/db/db_cache.rs +++ b/src/db/db_cache.rs @@ -70,11 +70,11 @@ pub fn from_db_data_to_filter_cache( if !fields_values_cache .list_of_sources - .contains(&curr_creature.core_data.source) + .contains(&curr_creature.core_data.publication_info.source) { fields_values_cache .list_of_sources - .push(curr_creature.core_data.source.clone()); + .push(curr_creature.core_data.publication_info.source.clone()); } if !fields_values_cache.list_of_alignments.contains(&alignment) { diff --git a/src/db/db_communicator.rs b/src/db/db_communicator.rs index 91e33fd..e087164 100644 --- a/src/db/db_communicator.rs +++ b/src/db/db_communicator.rs @@ -7,13 +7,34 @@ use crate::models::db::raw_sense::RawSense; use crate::models::db::raw_speed::RawSpeed; use crate::models::db::raw_trait::RawTrait; use crate::models::db::raw_weakness::RawWeakness; +use crate::models::items::action::Action; +use crate::models::items::skill::Skill; use crate::models::items::spell::Spell; use crate::models::items::weapon::Weapon; +use crate::models::scales_struct::ability_scales::AbilityScales; +use crate::models::scales_struct::ac_scales::AcScales; +use crate::models::scales_struct::area_dmg_scales::AreaDmgScales; +use crate::models::scales_struct::creature_scales::CreatureScales; +use crate::models::scales_struct::hp_scales::HpScales; +use crate::models::scales_struct::item_scales::ItemScales; +use crate::models::scales_struct::perception_scales::PerceptionScales; +use crate::models::scales_struct::res_weak_scales::ResWeakScales; +use crate::models::scales_struct::saving_throw_scales::SavingThrowScales; +use crate::models::scales_struct::skill_scales::SkillScales; +use crate::models::scales_struct::spell_dc_and_atk_scales::SpellDcAndAtkScales; +use crate::models::scales_struct::strike_bonus_scales::StrikeBonusScales; +use crate::models::scales_struct::strike_dmg_scales::StrikeDmgScales; use anyhow::Result; +use regex::Regex; use sqlx::{Error, Pool, Sqlite}; -async fn from_raw_vec_to_creature(conn: &Pool, raw_vec: Vec) -> Vec { +async fn from_raw_vec_to_creature( + conn: &Pool, + scales: &CreatureScales, + raw_vec: Vec, +) -> Vec { let mut creature_list = Vec::new(); + let scales_dmg_regex = Regex::new(r"\((\d+)\)").ok().unwrap(); for el in raw_vec { let immunities = get_creature_immunities(conn, el.id) .await @@ -32,17 +53,23 @@ async fn from_raw_vec_to_creature(conn: &Pool, raw_vec: Vec .unwrap_or_default(); let spells = get_creature_spells(conn, el.id).await.unwrap_or_default(); let weapons = get_creature_weapons(conn, el.id).await.unwrap_or_default(); + let actions = get_creature_actions(conn, el.id).await.unwrap_or_default(); + let skills = get_creature_skills(conn, el.id).await.unwrap_or_default(); creature_list.push(Creature::from(( el, traits, weapons, spells, + actions, + skills, immunities, languages, resistances, senses, speeds, weaknesses, + scales, + &scales_dmg_regex, ))); } creature_list @@ -129,6 +156,26 @@ async fn get_creature_weapons(conn: &Pool, creature_id: i64) -> Result, creature_id: i64) -> Result> { + Ok(sqlx::query_as!( + Action, + "SELECT * FROM ACTION_TABLE WHERE creature_id == ($1)", + creature_id + ) + .fetch_all(conn) + .await?) +} + +async fn get_creature_skills(conn: &Pool, creature_id: i64) -> Result> { + Ok(sqlx::query_as!( + Skill, + "SELECT name, description, modifier, proficiency FROM SKILL_TABLE WHERE creature_id == ($1)", + creature_id + ) + .fetch_all(conn) + .await?) +} + async fn get_creature_spells(conn: &Pool, creature_id: i64) -> Result> { Ok(sqlx::query_as!( Spell, @@ -139,12 +186,110 @@ async fn get_creature_spells(conn: &Pool, creature_id: i64) -> Result) -> Result, Error> { +pub async fn fetch_creatures( + conn: &Pool, + creature_scales: &CreatureScales, +) -> Result, Error> { Ok(from_raw_vec_to_creature( conn, + creature_scales, sqlx::query_as!(RawCreature, "SELECT * FROM CREATURE_TABLE ORDER BY name") .fetch_all(conn) .await?, ) .await) } + +pub async fn fetch_creature_scales(conn: &Pool) -> Result { + Ok(CreatureScales { + ability_scales: sqlx::query_as!(AbilityScales, "SELECT * FROM ABILITY_SCALES_TABLE",) + .fetch_all(conn) + .await? + .into_iter() + .map(|n| (n.level as i8, n)) + .collect(), + ac_scales: sqlx::query_as!(AcScales, "SELECT * FROM AC_SCALES_TABLE",) + .fetch_all(conn) + .await? + .into_iter() + .map(|n| (n.level as i8, n)) + .collect(), + area_dmg_scales: sqlx::query_as!(AreaDmgScales, "SELECT * FROM AREA_DAMAGE_SCALES_TABLE",) + .fetch_all(conn) + .await? + .into_iter() + .map(|n| (n.level as i8, n)) + .collect(), + hp_scales: sqlx::query_as!(HpScales, "SELECT * FROM HP_SCALES_TABLE",) + .fetch_all(conn) + .await? + .into_iter() + .map(|n| (n.level as i8, n)) + .collect(), + item_scales: sqlx::query_as!(ItemScales, "SELECT * FROM ITEM_SCALES_TABLE",) + .fetch_all(conn) + .await? + .into_iter() + .map(|n| (n.cr_level.clone(), n)) + .collect(), + perception_scales: sqlx::query_as!( + PerceptionScales, + "SELECT * FROM PERCEPTION_SCALES_TABLE", + ) + .fetch_all(conn) + .await? + .into_iter() + .map(|n| (n.level as i8, n)) + .collect(), + res_weak_scales: sqlx::query_as!(ResWeakScales, "SELECT * FROM RES_WEAK_SCALES_TABLE",) + .fetch_all(conn) + .await? + .into_iter() + .map(|n| (n.level as i8, n)) + .collect(), + saving_throw_scales: sqlx::query_as!( + SavingThrowScales, + "SELECT * FROM SAVING_THROW_SCALES_TABLE", + ) + .fetch_all(conn) + .await? + .into_iter() + .map(|n| (n.level as i8, n)) + .collect(), + skill_scales: sqlx::query_as!(SkillScales, "SELECT * FROM SKILL_SCALES_TABLE",) + .fetch_all(conn) + .await? + .into_iter() + .map(|n| (n.level as i8, n)) + .collect(), + spell_dc_and_atk_scales: sqlx::query_as!( + SpellDcAndAtkScales, + "SELECT * FROM SPELL_DC_AND_ATTACK_SCALES_TABLE", + ) + .fetch_all(conn) + .await? + .into_iter() + .map(|n| (n.level as i8, n)) + .collect(), + strike_bonus_scales: sqlx::query_as!( + StrikeBonusScales, + "SELECT * FROM STRIKE_BONUS_SCALES_TABLE", + ) + .fetch_all(conn) + .await? + .into_iter() + .map(|n| (n.level as i8, n)) + .collect(), + strike_dmg_scales: sqlx::query_as!( + StrikeDmgScales, + "SELECT * FROM STRIKE_DAMAGE_SCALES_TABLE", + ) + .fetch_all(conn) + .await? + .into_iter() + .map(|n| (n.level as i8, n)) + .collect(), + }) +} + +// TODO: Restructure creature fetch to use transaction diff --git a/src/db/db_proxy.rs b/src/db/db_proxy.rs index 3c6b3b9..a0d72bd 100644 --- a/src/db/db_proxy.rs +++ b/src/db/db_proxy.rs @@ -11,6 +11,8 @@ use crate::services::url_calculator::add_boolean_query; use crate::AppState; use anyhow::Result; +const ACCURACY_THRESHOLD: i64 = 50; + fn hp_increase_by_level() -> HashMap { hashmap! { 1 => 10, 2=> 15, 5=> 20, 20=> 30 } } @@ -170,6 +172,14 @@ fn fetch_creatures_passing_single_filter( filter_vec.contains(creature.core_data.is_spell_caster.to_string().as_str()) }) .collect(), + CreatureFilter::CreatureRoles => cr_iterator + .filter(|creature| { + creature.info.roles.iter().any(|(role, accuracy)| { + accuracy >= &ACCURACY_THRESHOLD + && filter_vec.contains(role.to_string().as_str()) + }) + }) + .collect(), } } @@ -189,7 +199,6 @@ pub async fn get_keys(app_state: &AppState, field: CreatureField) -> Vec CreatureField::Alignment => runtime_fields_values.list_of_alignments, CreatureField::Level => runtime_fields_values.list_of_levels, CreatureField::CreatureTypes => runtime_fields_values.list_of_creature_types, - _ => vec![], }; x.sort(); @@ -213,7 +222,9 @@ async fn fetch_creatures(app_state: &AppState, variant: CreatureVariant) -> Opti let index = &CreatureVariant::to_cache_index(&variant); if let Some(creatures) = cache.get(index) { return Some(creatures); - } else if let Ok(creatures) = db_communicator::fetch_creatures(&app_state.conn).await { + } else if let Ok(creatures) = + db_communicator::fetch_creatures(&app_state.conn, &app_state.creature_scales).await + { cache.insert(0, creatures.clone()); let mut weak_creatures = Vec::new(); let mut elite_creatures = Vec::new(); diff --git a/src/main.rs b/src/main.rs index 2709257..9f3235e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ mod routes; use crate::db::db_cache::RuntimeFieldsValues; use crate::models::creature::Creature; +use crate::models::scales_struct::creature_scales::CreatureScales; use crate::routes::{bestiary, encounter, health}; use actix_cors::Cors; use actix_web::http::header::{CacheControl, CacheDirective}; @@ -26,6 +27,7 @@ pub struct AppState { conn: Pool, creature_cache: Cache>, runtime_fields_cache: Cache, + creature_scales: CreatureScales, } #[utoipa::path(get, path = "/")] @@ -87,6 +89,10 @@ async fn main() -> std::io::Result<()> { // Create the cache. .build(); + let creature_scales = db::db_communicator::fetch_creature_scales(&pool) + .await + .expect("Could not establish valid connection with the database.. Startup failed"); + log::info!( "starting HTTP server at http://{}:{}", service_ip.as_str(), @@ -127,6 +133,7 @@ async fn main() -> std::io::Result<()> { conn: pool.clone(), creature_cache: cr_cache.clone(), runtime_fields_cache: fields_cache.clone(), + creature_scales: creature_scales.clone(), })) }) .bind((get_service_ip(), get_service_port()))? diff --git a/src/models/creature.rs b/src/models/creature.rs index 7d03096..01e0f2d 100644 --- a/src/models/creature.rs +++ b/src/models/creature.rs @@ -1,8 +1,9 @@ -use crate::models::creature_metadata::alignment_enum::AlignmentEnum; -use crate::models::creature_metadata::rarity_enum::RarityEnum; -use crate::models::creature_metadata::size_enum::SizeEnum; -use crate::models::creature_metadata::type_enum::CreatureTypeEnum; -use crate::models::creature_metadata::variant_enum::CreatureVariant; +use crate::models::creature_component::creature_combat::CreatureCombatData; +use crate::models::creature_component::creature_core::CreatureCoreData; +use crate::models::creature_component::creature_extra::CreatureExtraData; +use crate::models::creature_component::creature_info::CreatureInfo; +use crate::models::creature_component::creature_spell_caster::CreatureSpellCasterData; +use crate::models::creature_component::creature_variant::CreatureVariantData; use crate::models::db::raw_creature::RawCreature; use crate::models::db::raw_immunity::RawImmunity; use crate::models::db::raw_language::RawLanguage; @@ -11,72 +12,32 @@ use crate::models::db::raw_sense::RawSense; use crate::models::db::raw_speed::RawSpeed; use crate::models::db::raw_trait::RawTrait; use crate::models::db::raw_weakness::RawWeakness; +use crate::models::items::action::Action; +use crate::models::items::skill::Skill; use crate::models::items::spell::Spell; use crate::models::items::weapon::Weapon; use crate::models::routers_validator_structs::FieldFilters; +use crate::models::scales_struct::creature_scales::CreatureScales; use crate::services::url_calculator::generate_archive_link; +use regex::Regex; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; #[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] -pub struct CoreCreatureData { - pub id: i32, - pub aon_id: Option, - pub name: String, - pub hp: i16, - // constant value, it will never change - pub base_level: i8, - pub alignment: AlignmentEnum, - pub size: SizeEnum, - pub family: Option, - pub rarity: RarityEnum, - pub is_spell_caster: bool, - pub is_melee: bool, - pub is_ranged: bool, +pub struct PublicationInfo { + pub license: String, + pub remaster: bool, pub source: String, - pub traits: Vec, - pub archive_link: Option, - pub creature_type: CreatureTypeEnum, - pub variant: CreatureVariant, -} - -#[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] -pub struct VariantCreatureData { - pub level: i8, - pub archive_link: Option, -} - -#[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] -pub struct ExtraCreatureData { - pub weapons: Vec, - pub spells: Vec, - pub immunities: Vec, - pub languages: Vec, - pub resistances: Vec<(String, i16)>, - pub senses: Vec, - pub speeds: Vec<(String, i16)>, - pub weaknesses: Vec<(String, i16)>, } #[derive(Serialize, Deserialize, Clone, Eq, Hash, PartialEq)] pub struct Creature { - pub core_data: CoreCreatureData, - pub variant_data: VariantCreatureData, - pub extra_data: ExtraCreatureData, -} - -impl CoreCreatureData { - pub fn is_melee(weapons: &[Weapon]) -> bool { - weapons - .iter() - .any(|el| el.wp_type.to_uppercase() == "MELEE") - } - - pub fn is_ranged(weapons: &[Weapon]) -> bool { - weapons - .iter() - .any(|el| el.wp_type.to_uppercase() == "RANGED") - } + pub core_data: CreatureCoreData, + pub variant_data: CreatureVariantData, + pub extra_data: CreatureExtraData, + pub combat_data: CreatureCombatData, + pub spell_caster_data: CreatureSpellCasterData, + pub info: CreatureInfo, } impl Creature { @@ -135,6 +96,17 @@ impl Creature { .map_or(true, |is_spell_caster| { self.core_data.is_spell_caster == is_spell_caster }); + let type_pass = filters + .type_filter + .as_ref() + .map_or(true, |cr_type| self.core_data.creature_type == *cr_type); + let role_pass = filters.role_filter.as_ref().map_or(true, |cr_role| { + self.info.roles.iter().any(|(r, a)| { + r == cr_role + && (filters.role_threshold.is_none() + || a >= &(filters.role_threshold.unwrap() as i64)) + }) + }); rarity_pass && size_pass @@ -142,6 +114,8 @@ impl Creature { && is_melee_pass && is_ranged_pass && is_spell_caster_pass + && type_pass + && role_pass } fn check_creature_pass_string_filters(&self, filters: &FieldFilters) -> bool { @@ -161,122 +135,22 @@ impl Creature { } } -impl From<(RawCreature, Vec, bool, bool, Option)> for CoreCreatureData { - fn from(tuple: (RawCreature, Vec, bool, bool, Option)) -> Self { - let raw = tuple.0; - let traits = tuple.1; - let is_ranged = tuple.2; - let is_melee = tuple.3; - let archive_link = tuple.4; - - let alignment_enum = AlignmentEnum::from((&traits, raw.remaster)); - CoreCreatureData { - id: raw.id as i32, - aon_id: raw.aon_id.map(|x| x as i32), - name: raw.name.clone(), - hp: raw.hp as i16, - base_level: raw.level as i8, - alignment: alignment_enum, - size: raw.size.clone(), - family: raw.family.clone(), - rarity: raw.rarity.clone(), - is_spell_caster: raw.is_spell_caster, - source: raw.source.clone(), - traits: traits - .into_iter() - .map(|curr_trait| curr_trait.name) - .collect(), - creature_type: raw.cr_type.clone(), - archive_link: archive_link.clone(), - variant: CreatureVariant::Base, - is_ranged, - is_melee, - } - } -} - -impl From<(i64, Option)> for VariantCreatureData { - fn from(value: (i64, Option)) -> Self { - Self { - level: value.0 as i8, - archive_link: value.1, - } - } -} - -impl - From<( - Vec, - Vec, - Vec, - Vec, - Vec, - Vec, - Vec, - Vec, - )> for ExtraCreatureData -{ - fn from( - tuple: ( - Vec, - Vec, - Vec, - Vec, - Vec, - Vec, - Vec, - Vec, - ), - ) -> Self { - Self { - weapons: tuple.0, - spells: tuple.1, - immunities: tuple - .2 - .into_iter() - .map(|curr_trait| curr_trait.name) - .collect(), - languages: tuple - .3 - .into_iter() - .map(|curr_trait| curr_trait.name) - .collect(), - resistances: tuple - .4 - .into_iter() - .map(|curr_res| (curr_res.name, curr_res.value as i16)) - .collect(), - senses: tuple - .5 - .into_iter() - .map(|curr_trait| curr_trait.name) - .collect(), - speeds: tuple - .6 - .into_iter() - .map(|curr_speed| (curr_speed.name, curr_speed.value as i16)) - .collect(), - weaknesses: tuple - .7 - .into_iter() - .map(|curr_weak| (curr_weak.name, curr_weak.value as i16)) - .collect(), - } - } -} - impl From<( RawCreature, Vec, Vec, Vec, + Vec, + Vec, Vec, Vec, Vec, Vec, Vec, Vec, + &CreatureScales, + &Regex, )> for Creature { fn from( @@ -285,48 +159,87 @@ impl Vec, Vec, Vec, + Vec, + Vec, Vec, Vec, Vec, Vec, Vec, Vec, + &CreatureScales, + &Regex, ), ) -> Self { let raw_creature = tuple.0; + let traits = tuple.1; let weapons = tuple.2; let spells = tuple.3; - let traits = tuple.1; - let immunities = tuple.4; - let languages = tuple.5; - let resistances = tuple.6; - let senses = tuple.7; - let speeds = tuple.8; - let weaknesses = tuple.9; + let actions = tuple.4; + let skills = tuple.5; + let immunities = tuple.6; + let languages = tuple.7; + let resistances = tuple.8; + let senses = tuple.9; + let speeds = tuple.10; + let weaknesses = tuple.11; let archive_link = generate_archive_link(raw_creature.aon_id, &raw_creature.cr_type); - let is_ranged = CoreCreatureData::is_ranged(&weapons); - let is_melee = CoreCreatureData::is_melee(&weapons); + let is_ranged = is_ranged(&weapons); + let is_melee = is_melee(&weapons); + let core_data = CreatureCoreData::from(( + raw_creature.clone(), + traits, + is_ranged, + is_melee, + archive_link.clone(), + )); + let extra_data = CreatureExtraData::from(( + raw_creature.clone(), + actions, + skills, + languages, + senses, + speeds, + )); + + let combat_data = CreatureCombatData::from(( + raw_creature.clone(), + weapons, + immunities, + resistances, + weaknesses, + )); + let scales = tuple.12; + let spell_caster_data = CreatureSpellCasterData::from((raw_creature.clone(), spells)); + let info = CreatureInfo::from(( + &core_data, + &extra_data, + &combat_data, + &spell_caster_data, + scales, + tuple.13, + )); Self { - variant_data: VariantCreatureData::from((raw_creature.level, archive_link.clone())), - core_data: CoreCreatureData::from(( - raw_creature, - traits, - is_ranged, - is_melee, - archive_link, - )), - extra_data: ExtraCreatureData::from(( - weapons, - spells, - immunities, - languages, - resistances, - senses, - speeds, - weaknesses, - )), + variant_data: CreatureVariantData::from((raw_creature.level, archive_link)), + core_data, + extra_data, + spell_caster_data, + combat_data, + info, } } } + +fn is_melee(weapons: &[Weapon]) -> bool { + weapons + .iter() + .any(|el| el.wp_type.to_uppercase() == "MELEE") +} + +fn is_ranged(weapons: &[Weapon]) -> bool { + weapons + .iter() + .any(|el| el.wp_type.to_uppercase() == "RANGED") +} diff --git a/src/models/creature_component/creature_combat.rs b/src/models/creature_component/creature_combat.rs new file mode 100644 index 0000000..db41597 --- /dev/null +++ b/src/models/creature_component/creature_combat.rs @@ -0,0 +1,89 @@ +use crate::models::db::raw_creature::RawCreature; +use crate::models::db::raw_immunity::RawImmunity; +use crate::models::db::raw_resistance::RawResistance; +use crate::models::db::raw_weakness::RawWeakness; +use crate::models::items::weapon::Weapon; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use utoipa::ToSchema; + +#[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] +pub struct SavingThrows { + pub fortitude: i8, + pub reflex: i8, + pub will: i8, + pub fortitude_detail: Option, + pub reflex_detail: Option, + pub will_detail: Option, +} + +#[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] +pub struct CreatureCombatData { + pub weapons: Vec, + pub resistances: BTreeMap, + pub immunities: Vec, + pub weaknesses: BTreeMap, + pub saving_throws: SavingThrows, + pub ac: i8, +} + +impl + From<( + RawCreature, + Vec, + Vec, + Vec, + Vec, + )> for CreatureCombatData +{ + fn from( + tuple: ( + RawCreature, + Vec, + Vec, + Vec, + Vec, + ), + ) -> Self { + let raw_cr = tuple.0; + Self { + weapons: tuple.1, + immunities: tuple + .2 + .into_iter() + .map(|curr_trait| curr_trait.name) + .collect(), + resistances: tuple + .3 + .into_iter() + .map(|curr_res| (curr_res.name, curr_res.value as i16)) + .collect(), + weaknesses: tuple + .4 + .into_iter() + .map(|curr_weak| (curr_weak.name, curr_weak.value as i16)) + .collect(), + ac: raw_cr.ac as i8, + saving_throws: SavingThrows { + fortitude: raw_cr.fortitude as i8, + reflex: raw_cr.reflex as i8, + will: raw_cr.will as i8, + fortitude_detail: if raw_cr.fortitude_detail.is_empty() { + None + } else { + Some(raw_cr.fortitude_detail) + }, + reflex_detail: if raw_cr.reflex_detail.is_empty() { + None + } else { + Some(raw_cr.reflex_detail) + }, + will_detail: if raw_cr.will_detail.is_empty() { + None + } else { + Some(raw_cr.will_detail) + }, + }, + } + } +} diff --git a/src/models/creature_component/creature_core.rs b/src/models/creature_component/creature_core.rs new file mode 100644 index 0000000..8377d33 --- /dev/null +++ b/src/models/creature_component/creature_core.rs @@ -0,0 +1,70 @@ +use crate::models::creature::PublicationInfo; +use crate::models::creature_metadata::alignment_enum::AlignmentEnum; +use crate::models::creature_metadata::rarity_enum::RarityEnum; +use crate::models::creature_metadata::size_enum::SizeEnum; +use crate::models::creature_metadata::type_enum::CreatureTypeEnum; +use crate::models::creature_metadata::variant_enum::CreatureVariant; +use crate::models::db::raw_creature::RawCreature; +use crate::models::db::raw_trait::RawTrait; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] +pub struct CreatureCoreData { + pub id: i32, + pub aon_id: Option, + pub name: String, + pub hp: i16, + // constant value, it will never change + pub base_level: i8, + pub alignment: AlignmentEnum, + pub size: SizeEnum, + pub family: Option, + pub rarity: RarityEnum, + pub is_melee: bool, + pub is_ranged: bool, + pub is_spell_caster: bool, + pub publication_info: PublicationInfo, + pub traits: Vec, + pub archive_link: Option, + pub creature_type: CreatureTypeEnum, + pub variant: CreatureVariant, +} + +impl From<(RawCreature, Vec, bool, bool, Option)> for CreatureCoreData { + fn from(tuple: (RawCreature, Vec, bool, bool, Option)) -> Self { + let raw = tuple.0; + let traits = tuple.1; + let is_ranged = tuple.2; + let is_melee = tuple.3; + let archive_link = tuple.4; + + let alignment_enum = AlignmentEnum::from((&traits, raw.remaster)); + CreatureCoreData { + id: raw.id as i32, + aon_id: raw.aon_id.map(|x| x as i32), + name: raw.name.clone(), + hp: raw.hp as i16, + base_level: raw.level as i8, + alignment: alignment_enum, + size: raw.size.clone(), + family: raw.family.clone(), + rarity: raw.rarity.clone(), + is_spell_caster: raw.spell_casting_name.is_some(), + publication_info: PublicationInfo { + remaster: raw.remaster, + source: raw.source, + license: raw.license, + }, + traits: traits + .into_iter() + .map(|curr_trait| curr_trait.name) + .collect(), + creature_type: raw.cr_type.clone(), + archive_link: archive_link.clone(), + variant: CreatureVariant::Base, + is_ranged, + is_melee, + } + } +} diff --git a/src/models/creature_component/creature_extra.rs b/src/models/creature_component/creature_extra.rs new file mode 100644 index 0000000..3d2b059 --- /dev/null +++ b/src/models/creature_component/creature_extra.rs @@ -0,0 +1,102 @@ +use crate::models::db::raw_creature::RawCreature; +use crate::models::db::raw_language::RawLanguage; +use crate::models::db::raw_sense::RawSense; +use crate::models::db::raw_speed::RawSpeed; +use crate::models::items::action::Action; +use crate::models::items::skill::Skill; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use utoipa::ToSchema; + +#[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] +pub struct AbilityScores { + pub charisma: i8, + pub constitution: i8, + pub dexterity: i8, + pub intelligence: i8, + pub strength: i8, + pub wisdom: i8, +} + +#[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] +pub struct CreatureExtraData { + pub actions: Vec, + pub skills: Vec, + pub languages: Vec, + pub senses: Vec, + pub speeds: BTreeMap, + pub ability_scores: AbilityScores, + pub hp_detail: Option, + pub ac_detail: Option, + pub language_detail: Option, + pub perception: i8, + pub perception_detail: Option, +} + +impl + From<( + RawCreature, + Vec, + Vec, + Vec, + Vec, + Vec, + )> for CreatureExtraData +{ + fn from( + tuple: ( + RawCreature, + Vec, + Vec, + Vec, + Vec, + Vec, + ), + ) -> Self { + let raw_cr = tuple.0; + Self { + actions: tuple.1, + skills: tuple.2, + languages: tuple + .3 + .into_iter() + .map(|curr_trait| curr_trait.name) + .collect(), + senses: tuple + .4 + .into_iter() + .map(|curr_trait| curr_trait.name) + .collect(), + speeds: tuple + .5 + .into_iter() + .map(|curr_speed| (curr_speed.name, curr_speed.value as i16)) + .collect(), + ability_scores: AbilityScores { + charisma: raw_cr.charisma as i8, + constitution: raw_cr.constitution as i8, + dexterity: raw_cr.dexterity as i8, + intelligence: raw_cr.intelligence as i8, + strength: raw_cr.strength as i8, + wisdom: raw_cr.wisdom as i8, + }, + hp_detail: if raw_cr.hp_detail.is_empty() { + None + } else { + Some(raw_cr.hp_detail) + }, + ac_detail: if raw_cr.ac_detail.is_empty() { + None + } else { + Some(raw_cr.ac_detail) + }, + language_detail: raw_cr.language_detail, + perception: raw_cr.perception as i8, + perception_detail: if raw_cr.perception_detail.is_empty() { + None + } else { + Some(raw_cr.perception_detail) + }, + } + } +} diff --git a/src/models/creature_component/creature_info.rs b/src/models/creature_component/creature_info.rs new file mode 100644 index 0000000..6c3397a --- /dev/null +++ b/src/models/creature_component/creature_info.rs @@ -0,0 +1,45 @@ +use crate::models::creature_component::creature_combat::CreatureCombatData; +use crate::models::creature_component::creature_core::CreatureCoreData; +use crate::models::creature_component::creature_extra::CreatureExtraData; +use crate::models::creature_component::creature_spell_caster::CreatureSpellCasterData; +use crate::models::creature_metadata::creature_role::CreatureRoleEnum; +use crate::models::scales_struct::creature_scales::CreatureScales; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use utoipa::ToSchema; + +#[derive(Serialize, Deserialize, Clone, ToSchema, Hash, PartialEq, Eq)] +pub struct CreatureInfo { + pub roles: BTreeMap, + pub locale: Vec, +} + +impl + From<( + &CreatureCoreData, + &CreatureExtraData, + &CreatureCombatData, + &CreatureSpellCasterData, + &CreatureScales, + &Regex, + )> for CreatureInfo +{ + fn from( + tuple: ( + &CreatureCoreData, + &CreatureExtraData, + &CreatureCombatData, + &CreatureSpellCasterData, + &CreatureScales, + &Regex, + ), + ) -> Self { + Self { + roles: CreatureRoleEnum::from_creature_with_given_scales( + tuple.0, tuple.1, tuple.2, tuple.3, tuple.4, tuple.5, + ), + locale: vec![], + } + } +} diff --git a/src/models/creature_component/creature_spell_caster.rs b/src/models/creature_component/creature_spell_caster.rs new file mode 100644 index 0000000..bca6ebc --- /dev/null +++ b/src/models/creature_component/creature_spell_caster.rs @@ -0,0 +1,28 @@ +use crate::models::db::raw_creature::RawCreature; +use crate::models::items::spell::Spell; +use crate::models::items::spell_caster_entry::SpellCasterEntry; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] +pub struct CreatureSpellCasterData { + pub spells: Vec, + pub spell_caster_entry: SpellCasterEntry, +} + +impl From<(RawCreature, Vec)> for CreatureSpellCasterData { + fn from(tuple: (RawCreature, Vec)) -> Self { + let raw_cr = tuple.0; + Self { + spells: tuple.1, + spell_caster_entry: SpellCasterEntry { + spell_casting_name: raw_cr.spell_casting_name, + is_spell_casting_flexible: raw_cr.is_spell_casting_flexible, + type_of_spell_caster: raw_cr.type_of_spell_caster, + spell_casting_dc_mod: raw_cr.spell_casting_dc_mod.map(|x| x as i8), + spell_casting_atk_mod: raw_cr.spell_casting_atk_mod.map(|x| x as i8), + spell_casting_tradition: raw_cr.spell_casting_tradition, + }, + } + } +} diff --git a/src/models/creature_component/creature_variant.rs b/src/models/creature_component/creature_variant.rs new file mode 100644 index 0000000..88af048 --- /dev/null +++ b/src/models/creature_component/creature_variant.rs @@ -0,0 +1,17 @@ +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] +pub struct CreatureVariantData { + pub level: i8, + pub archive_link: Option, +} + +impl From<(i64, Option)> for CreatureVariantData { + fn from(value: (i64, Option)) -> Self { + Self { + level: value.0 as i8, + archive_link: value.1, + } + } +} diff --git a/src/models/creature_component/filter_struct.rs b/src/models/creature_component/filter_struct.rs new file mode 100644 index 0000000..9a9e8b5 --- /dev/null +++ b/src/models/creature_component/filter_struct.rs @@ -0,0 +1,17 @@ +use crate::models::creature_metadata::alignment_enum::AlignmentEnum; +use crate::models::creature_metadata::creature_role::CreatureRoleEnum; +use crate::models::creature_metadata::rarity_enum::RarityEnum; +use crate::models::creature_metadata::size_enum::SizeEnum; +use crate::models::creature_metadata::type_enum::CreatureTypeEnum; +use std::collections::HashSet; + +pub struct FilterStruct { + pub families: Option>, + pub traits: Option>, + pub rarities: Option>, + pub sizes: Option>, + pub alignments: Option>, + pub creature_types: Option>, + pub creature_roles: Option>, + pub lvl_combinations: HashSet, +} diff --git a/src/models/creature_component/mod.rs b/src/models/creature_component/mod.rs new file mode 100644 index 0000000..9d842de --- /dev/null +++ b/src/models/creature_component/mod.rs @@ -0,0 +1,7 @@ +pub mod creature_combat; +pub mod creature_core; +pub mod creature_extra; +pub mod creature_info; +pub mod creature_spell_caster; +pub mod creature_variant; +pub mod filter_struct; diff --git a/src/models/creature_filter_enum.rs b/src/models/creature_filter_enum.rs index 015ca8f..2c5c773 100644 --- a/src/models/creature_filter_enum.rs +++ b/src/models/creature_filter_enum.rs @@ -13,4 +13,5 @@ pub enum CreatureFilter { SpellCaster, Traits, CreatureTypes, + CreatureRoles, } diff --git a/src/models/creature_metadata/creature_role.rs b/src/models/creature_metadata/creature_role.rs new file mode 100644 index 0000000..a896ee4 --- /dev/null +++ b/src/models/creature_metadata/creature_role.rs @@ -0,0 +1,523 @@ +use crate::models::creature_component::creature_combat::CreatureCombatData; +use crate::models::creature_component::creature_core::CreatureCoreData; +use crate::models::creature_component::creature_extra::CreatureExtraData; +use crate::models::creature_component::creature_spell_caster::CreatureSpellCasterData; +use crate::models::scales_struct::creature_scales::CreatureScales; +use num_traits::float::FloatConst; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use std::fmt; +use strum::{EnumIter, IntoEnumIterator}; +use utoipa::ToSchema; +use validator::HasLen; + +const MISSING_FIELD_DISTANCE: u16 = 20; + +#[derive( + Serialize, Deserialize, EnumIter, Clone, ToSchema, Eq, Hash, PartialEq, Ord, PartialOrd, +)] +pub enum CreatureRoleEnum { + Brute, + #[serde(rename = "Magical Striker")] + MagicalStriker, + #[serde(rename = "Skill Paragon")] + SkillParagon, + Skirmisher, + Sniper, + Soldier, + SpellCaster, +} + +impl CreatureRoleEnum { + pub fn from_creature_with_given_scales( + cr_core: &CreatureCoreData, + cr_extra: &CreatureExtraData, + cr_combat: &CreatureCombatData, + cr_spells: &CreatureSpellCasterData, + scales: &CreatureScales, + dmg_scales_regex: &Regex, + ) -> BTreeMap { + let mut roles = BTreeMap::new(); + roles.insert( + Self::Brute, + is_brute(cr_core, cr_extra, cr_combat, scales, dmg_scales_regex) + .map(|x| (x * 100.).round() as i64) + .unwrap_or(0), + ); + roles.insert( + Self::MagicalStriker, + is_magical_striker(cr_core, cr_spells, cr_combat, scales, dmg_scales_regex) + .map(|x| (x * 100.).round() as i64) + .unwrap_or(0), + ); + roles.insert( + Self::SkillParagon, + is_skill_paragon(cr_core, cr_extra, cr_combat, scales) + .map(|x| (x * 100.).round() as i64) + .unwrap_or(0), + ); + roles.insert( + Self::Skirmisher, + is_skirmisher(cr_core, cr_extra, cr_combat, scales) + .map(|x| (x * 100.).round() as i64) + .unwrap_or(0), + ); + roles.insert( + Self::Sniper, + is_sniper(cr_core, cr_extra, cr_combat, scales, dmg_scales_regex) + .map(|x| (x * 100.).round() as i64) + .unwrap_or(0), + ); + roles.insert( + Self::Soldier, + is_soldier(cr_core, cr_extra, cr_combat, scales, dmg_scales_regex) + .map(|x| (x * 100.).round() as i64) + .unwrap_or(0), + ); + roles.insert( + Self::SpellCaster, + is_spellcaster(cr_core, cr_spells, cr_combat, cr_extra, scales) + .map(|x| (x * 100.).round() as i64) + .unwrap_or(0), + ); + roles + } + + pub fn list() -> Vec { + CreatureRoleEnum::iter().map(|x| x.to_string()).collect() + } +} +// Brute +fn is_brute( + cr_core: &CreatureCoreData, + cr_extra: &CreatureExtraData, + cr_combat: &CreatureCombatData, + scales: &CreatureScales, + re: &Regex, +) -> Option { + let mut score: u16 = 0; + let lvl = cr_core.base_level; + let per_scales = scales.perception_scales.get(&lvl)?; + // low Perception; + score += calculate_ub_distance(per_scales.moderate, cr_extra.perception as i64 + 1); + let ability_scales = scales.ability_scales.get(&lvl)?; + // high or extreme Str modifier, + score += calculate_lb_distance(ability_scales.high, cr_extra.ability_scores.strength as i64); + // high to moderate Con modifier, + let constitution = cr_extra.ability_scores.constitution as i64; + score += calculate_lb_distance(ability_scales.moderate, constitution); + // low or lower mental modifiers; + score += calculate_ub_distance( + ability_scales.moderate, + cr_extra.ability_scores.intelligence as i64 + 1, + ); + score += calculate_ub_distance( + ability_scales.moderate, + cr_extra.ability_scores.wisdom as i64 + 1, + ); + score += calculate_ub_distance( + ability_scales.moderate, + cr_extra.ability_scores.charisma as i64 + 1, + ); + let saving_scales = scales.saving_throw_scales.get(&lvl)?; + // Low or lower Reflex + score += calculate_ub_distance( + saving_scales.moderate, + cr_combat.saving_throws.reflex as i64 + 1, + ); + // high Fortitude, + score += calculate_lb_distance(saving_scales.high, cr_combat.saving_throws.fortitude as i64); + // Low will, + score += calculate_ub_distance( + saving_scales.moderate, + cr_combat.saving_throws.will as i64 + 1, + ); + let ac_scales = scales.ac_scales.get(&lvl)?; + // moderate or low AC; + score += calculate_ub_distance(ac_scales.high, cr_combat.ac as i64 + 1); + // high HP; + let hp_scales = scales.hp_scales.get(&lvl)?; + score += calculate_lb_distance(hp_scales.high_lb, cr_core.hp as i64); + let atk_bonus_scales = scales.strike_bonus_scales.get(&lvl)?; + let dmg_scales = scales.strike_dmg_scales.get(&lvl)?; + + let scales_extreme_avg = re + .captures(dmg_scales.extreme.as_str())? + .get(1)? + .as_str() + .parse::() + .ok()?; + let scales_high_avg = re + .captures(dmg_scales.high.as_str())? + .get(1)? + .as_str() + .parse::() + .ok()?; + // high attack bonus and high damage OR moderate attack bonus and extreme damage + let wp_distance = cr_combat + .weapons + .iter() + .filter(|wp| wp.get_avg_dmg().is_some()) + .map(|wp| { + let avg_dmg = wp.get_avg_dmg().unwrap(); + let x = calculate_lb_distance(atk_bonus_scales.high, wp.to_hit_bonus) + + calculate_lb_distance(scales_high_avg, avg_dmg); + let y = calculate_dist( + atk_bonus_scales.moderate, + atk_bonus_scales.high, + wp.to_hit_bonus, + ) + calculate_lb_distance(scales_extreme_avg, avg_dmg); + x.min(y) + }) + .min(); + score += wp_distance.unwrap_or(MISSING_FIELD_DISTANCE); + + Some(f64::E().powf(-0.2 * (score as f64))) +} + +// Sniper +fn is_sniper( + cr_core: &CreatureCoreData, + cr_extra: &CreatureExtraData, + cr_combat: &CreatureCombatData, + scales: &CreatureScales, + re: &Regex, +) -> Option { + let mut score: u16 = 0; + let lvl = cr_core.base_level; + let per_scales = scales.perception_scales.get(&lvl)?; + // high Perception (chosen moderate + // !!!This is a critical stat, upping it will half creature result!!! + // ); + score += calculate_lb_distance(per_scales.moderate, cr_extra.perception as i64); + let ability_scales = scales.ability_scales.get(&lvl)?; + // high Dex modifier (chosen moderate); + score += calculate_lb_distance( + ability_scales.moderate, + cr_extra.ability_scores.dexterity as i64, + ); + let saving_scales = scales.saving_throw_scales.get(&lvl)?; + // low Fortitude + // skipped + // high Reflex (chosen moderate); + score += calculate_lb_distance( + saving_scales.moderate, + cr_combat.saving_throws.reflex as i64, + ); + + // moderate to low HP; skipped + let atk_bonus_scales = scales.strike_bonus_scales.get(&lvl)?; + let dmg_scales = scales.strike_dmg_scales.get(&lvl)?; + let scales_mod_avg = re + .captures(dmg_scales.moderate.as_str())? + .get(1)? + .as_str() + .parse::() + .ok()?; + // ranged Strikes have high attack bonus and damage or + // moderate attack bonus and extreme damage (melee Strikes are weaker) + let wp_distance = cr_combat + .weapons + .iter() + .filter(|wp| wp.get_avg_dmg().is_some() && wp.wp_type.to_uppercase() == "RANGED") + .map(|wp| { + let avg_dmg = wp.get_avg_dmg().unwrap(); + calculate_lb_distance(atk_bonus_scales.high, wp.to_hit_bonus) + + calculate_lb_distance(scales_mod_avg, avg_dmg) + }) + .min(); + score += wp_distance.unwrap_or(MISSING_FIELD_DISTANCE); + Some(f64::E().powf(-0.2 * (score as f64))) +} +// Skirmisher +fn is_skirmisher( + cr_core: &CreatureCoreData, + cr_extra: &CreatureExtraData, + cr_combat: &CreatureCombatData, + scales: &CreatureScales, +) -> Option { + let mut score: u16 = 0; + let lvl = cr_core.base_level; + let ability_scales = scales.ability_scales.get(&lvl)?; + // high Dex modifier; + score += calculate_lb_distance( + ability_scales.high, + cr_extra.ability_scores.dexterity as i64, + ); + let saving_scales = scales.saving_throw_scales.get(&lvl)?; + // low Fortitude + score += calculate_ub_distance( + saving_scales.moderate, + cr_combat.saving_throws.fortitude as i64 + 1, + ); + // high Reflex; + score += calculate_lb_distance(saving_scales.high, cr_combat.saving_throws.reflex as i64); + // Higher than avg speed (avg ~= 25) + score += cr_extra + .speeds + .values() + .map(|speed_value| calculate_lb_distance(30, *speed_value as i64)) + .min() + .unwrap_or(MISSING_FIELD_DISTANCE); + Some(f64::E().powf(-0.2 * (score as f64))) +} +// Soldier +pub fn is_soldier( + cr_core: &CreatureCoreData, + cr_extra: &CreatureExtraData, + cr_combat: &CreatureCombatData, + scales: &CreatureScales, + re: &Regex, +) -> Option { + let mut score: u16 = 0; + let lvl = cr_core.base_level; + let ability_scales = scales.ability_scales.get(&lvl)?; + // high Str modifier; + score += calculate_lb_distance(ability_scales.high, cr_extra.ability_scores.strength as i64); + let ac_scales = scales.ac_scales.get(&lvl)?; + // high to extreme AC; + score += calculate_lb_distance(ac_scales.high, cr_combat.ac as i64); + let saving_scales = scales.saving_throw_scales.get(&lvl)?; + // high Fortitude; + score += calculate_lb_distance(saving_scales.high, cr_combat.saving_throws.fortitude as i64); + let atk_bonus_scales = scales.strike_bonus_scales.get(&lvl)?; + let dmg_scales = scales.strike_dmg_scales.get(&lvl)?; + let scales_high_avg = re + .captures(dmg_scales.high.as_str())? + .get(1)? + .as_str() + .parse::() + .ok()?; + // high attack bonus and high damage; + + let wp_distance = cr_combat + .weapons + .iter() + .filter(|wp| wp.get_avg_dmg().is_some()) + .map(|wp| { + calculate_lb_distance(atk_bonus_scales.high, wp.to_hit_bonus) + + calculate_lb_distance(scales_high_avg, wp.get_avg_dmg().unwrap()) + }) + .min(); + + score += wp_distance.unwrap_or(MISSING_FIELD_DISTANCE); + if !cr_extra.actions.iter().any(|x| { + x.category.as_str().to_uppercase() == "OFFENSIVE" + && x.action_type.as_str().to_uppercase() == "ACTION" + }) { + score += MISSING_FIELD_DISTANCE; + } else if !cr_extra.actions.iter().any(|curr_act| { + curr_act.name.to_uppercase() == "ATTACK OF OPPORTUNITY" + || (curr_act.slug.is_none() + || curr_act.slug.clone().unwrap().to_uppercase() == "ATTACK-OF-OPPORTUNITY") + }) { + score += 3; + } + Some(f64::E().powf(-0.2 * (score as f64))) +} + +// Magical Striker +pub fn is_magical_striker( + cr_core: &CreatureCoreData, + cr_spell: &CreatureSpellCasterData, + cr_combat: &CreatureCombatData, + scales: &CreatureScales, + re: &Regex, +) -> Option { + let mut score: u16 = 0; + let lvl = cr_core.base_level; + let atk_bonus_scales = scales.strike_bonus_scales.get(&lvl)?; + let dmg_scales = scales.strike_dmg_scales.get(&lvl)?; + let scales_high_avg = re + .captures(dmg_scales.high.as_str())? + .get(1)? + .as_str() + .parse::() + .ok()?; + // high attack bonus and high damage; + let wp_distance = cr_combat + .weapons + .iter() + .filter(|wp| wp.get_avg_dmg().is_some()) + .map(|wp| { + calculate_lb_distance(atk_bonus_scales.high, wp.to_hit_bonus) + + calculate_lb_distance(scales_high_avg, wp.get_avg_dmg().unwrap()) + }) + .min(); + score += wp_distance.unwrap_or(MISSING_FIELD_DISTANCE); + let spell_dc = scales.spell_dc_and_atk_scales.get(&lvl)?; + // moderate to high spell DCs; + if cr_spell.spell_caster_entry.spell_casting_dc_mod.is_some() { + score += calculate_lb_distance( + spell_dc.moderate_dc, + cr_spell.spell_caster_entry.spell_casting_dc_mod.unwrap() as i64, + ); + } else { + score += MISSING_FIELD_DISTANCE; + } + if (cr_spell.spells.len() as f64) < (cr_core.base_level as f64 / 2.).ceil() - 1. { + score += + (((cr_core.base_level as f64 / 2.).ceil() as i64) - 1 - (cr_spell.spells.len() as i64)) + .unsigned_abs() as u16; + } + Some(f64::E().powf(-0.2 * (score as f64))) +} + +// Skill Paragon +fn is_skill_paragon( + cr_core: &CreatureCoreData, + cr_extra: &CreatureExtraData, + cr_combat: &CreatureCombatData, + scales: &CreatureScales, +) -> Option { + let mut score: u16 = 0; + let lvl = cr_core.base_level; + let ability_scales = scales.ability_scales.get(&lvl)?; + scales.skill_scales.get(&lvl)?; + let best_skill = cr_extra.skills.iter().map(|x| x.modifier).max()?; + // high or extreme attribute modifier matching its best skills; + score += calculate_lb_distance(ability_scales.high, best_skill); + let saving_scales = scales.saving_throw_scales.get(&lvl)?; + // typically high Reflex or Will and low Fortitude; + score += calculate_ub_distance( + saving_scales.moderate, + cr_combat.saving_throws.fortitude as i64 + 1, + ); + + let ref_dist = calculate_lb_distance(saving_scales.high, cr_combat.saving_throws.reflex as i64); + let will_dist = calculate_lb_distance(saving_scales.high, cr_combat.saving_throws.will as i64); + score += if ref_dist > will_dist { + will_dist + } else { + ref_dist + }; + + // many skills at moderate or high and potentially one or two extreme skills; + // Many is kinda up in the air, I'll set 70% + let cr_skill_amount = cr_extra.skills.length() * 70 / 100; + // if there aren't at least 70% of skill in the moderate-high range + score += (cr_extra + .skills + .iter() + .filter(|x| x.modifier >= saving_scales.moderate) + .count() as i64 + - cr_skill_amount as i64) + .unsigned_abs() as u16; + // at least two special ability to use the creature's skills in combat + if cr_extra + .actions + .iter() + .filter(|x| { + x.category.as_str().to_uppercase() == "OFFENSIVE" + && x.action_type.as_str().to_uppercase() == "ACTION" + }) + .count() + < 2 + { + score += MISSING_FIELD_DISTANCE; + } + Some(f64::E().powf(-0.2 * (score as f64))) +} +// Spellcaster +fn is_spellcaster( + cr_core: &CreatureCoreData, + cr_spell: &CreatureSpellCasterData, + cr_combat: &CreatureCombatData, + cr_extra: &CreatureExtraData, + scales: &CreatureScales, +) -> Option { + let mut score: u16 = 0; + let lvl = cr_core.base_level; + let saving_scales = scales.saving_throw_scales.get(&lvl)?; + // low Fortitude, + score += calculate_ub_distance( + saving_scales.moderate, + cr_combat.saving_throws.fortitude as i64 + 1, + ); + // high Will; + score += calculate_lb_distance(saving_scales.high, cr_combat.saving_throws.will as i64); + // low HP; + let hp_scales = scales.hp_scales.get(&lvl)?; + score += calculate_ub_distance(hp_scales.high_lb, cr_core.hp as i64 + 1); + // low attack bonus and moderate or low damage; + // skipped + // high or extreme spell DCs; + let spells_dc_and_atk_scales = scales.spell_dc_and_atk_scales.get(&lvl)?; + score += calculate_lb_distance( + spells_dc_and_atk_scales.high_dc, + cr_spell.spell_caster_entry.spell_casting_dc_mod? as i64, + ); + // prepared or spontaneous spells up to half the creature’s level (rounded up) + if (cr_spell.spells.len() as f64) < (cr_core.base_level as f64 / 2.).ceil() { + score += (((cr_core.base_level as f64 / 2.).ceil() as i64) - (cr_spell.spells.len() as i64)) + .unsigned_abs() as u16; + } + let ability_scales = scales.ability_scales.get(&lvl)?; + // high or extreme modifier for the corresponding mental ability; + let best_mental_ability = (cr_extra.ability_scores.wisdom as i64) + .max(cr_extra.ability_scores.intelligence as i64) + .max(cr_extra.ability_scores.charisma as i64); + score += calculate_lb_distance(ability_scales.high, best_mental_ability); + Some(f64::E().powf(-0.2 * (score as f64))) +} + +impl fmt::Display for CreatureRoleEnum { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + CreatureRoleEnum::Brute => { + write!(f, "Brute") + } + CreatureRoleEnum::MagicalStriker => { + write!(f, "Magical Striker") + } + CreatureRoleEnum::SkillParagon => { + write!(f, "Skill Paragon") + } + CreatureRoleEnum::Skirmisher => { + write!(f, "Skirmisher") + } + CreatureRoleEnum::Sniper => { + write!(f, "Sniper") + } + CreatureRoleEnum::Soldier => { + write!(f, "Soldier") + } + CreatureRoleEnum::SpellCaster => { + write!(f, "SpellCaster") + } + } + } +} + +/// Calculate value distance from upper bound, lower than ub value will +/// yield 0 +fn calculate_ub_distance(ub: i64, value: i64) -> u16 { + if value > ub { + (value - ub).unsigned_abs() as u16 + } else { + 0 + } +} + +/// Calculate value distance from lower bound, higher than lb value will +/// yield 0 +fn calculate_lb_distance(lb: i64, value: i64) -> u16 { + if value < lb { + (lb - value).unsigned_abs() as u16 + } else { + 0 + } +} + +/// Calculates value distance from bounds, it will exclude upper bound +fn calculate_dist(lb: i64, ub: i64, value: i64) -> u16 { + if value < lb { + (lb - value).unsigned_abs() as u16 + } else if value >= ub { + // if value = 30 and ub=30 you are 1 from the range, not 0 + (value + 1 - ub).unsigned_abs() as u16 + } else { + 0 + } +} diff --git a/src/models/creature_metadata/mod.rs b/src/models/creature_metadata/mod.rs index e80c979..57a4d9d 100644 --- a/src/models/creature_metadata/mod.rs +++ b/src/models/creature_metadata/mod.rs @@ -1,4 +1,5 @@ pub mod alignment_enum; +pub mod creature_role; pub mod rarity_enum; pub mod size_enum; pub mod type_enum; diff --git a/src/models/db/raw_creature.rs b/src/models/db/raw_creature.rs index da25ce4..681045f 100644 --- a/src/models/db/raw_creature.rs +++ b/src/models/db/raw_creature.rs @@ -37,5 +37,11 @@ pub struct RawCreature { pub size: SizeEnum, pub cr_type: CreatureTypeEnum, pub family: Option, - pub is_spell_caster: bool, + + pub spell_casting_name: Option, + pub is_spell_casting_flexible: Option, + pub type_of_spell_caster: Option, + pub spell_casting_dc_mod: Option, + pub spell_casting_atk_mod: Option, + pub spell_casting_tradition: Option, } diff --git a/src/models/encounter_structs.rs b/src/models/encounter_structs.rs index a81fdee..a39cb76 100644 --- a/src/models/encounter_structs.rs +++ b/src/models/encounter_structs.rs @@ -1,4 +1,5 @@ use crate::models::creature_metadata::alignment_enum::AlignmentEnum; +use crate::models::creature_metadata::creature_role::CreatureRoleEnum; use crate::models::creature_metadata::rarity_enum::RarityEnum; use crate::models::creature_metadata::size_enum::SizeEnum; use crate::models::creature_metadata::type_enum::CreatureTypeEnum; @@ -27,6 +28,7 @@ pub struct RandomEncounterData { pub sizes: Option>, pub alignments: Option>, pub creature_types: Option>, + pub creature_roles: Option>, pub challenge: Option, pub min_creatures: Option, pub max_creatures: Option, @@ -38,7 +40,9 @@ pub struct RandomEncounterData { pub response_data: ResponseData, } -#[derive(Serialize, Deserialize, ToSchema, Default, EnumIter, Eq, PartialEq, Hash, Clone)] +#[derive( + Serialize, Deserialize, ToSchema, Default, EnumIter, Eq, PartialEq, Hash, Ord, PartialOrd, Clone, +)] pub enum EncounterChallengeEnum { Trivial, Low, diff --git a/src/models/items/action.rs b/src/models/items/action.rs new file mode 100644 index 0000000..53c873e --- /dev/null +++ b/src/models/items/action.rs @@ -0,0 +1,21 @@ +use crate::models::creature_metadata::rarity_enum::RarityEnum; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] +pub struct Action { + pub id: i64, + pub name: String, + pub action_type: String, + pub n_of_actions: Option, + pub category: String, + pub description: String, + + pub license: String, + pub remaster: bool, + pub source: String, + + pub slug: Option, + pub rarity: RarityEnum, + pub creature_id: i64, +} diff --git a/src/models/items/mod.rs b/src/models/items/mod.rs index 16c2fd2..7cfe576 100644 --- a/src/models/items/mod.rs +++ b/src/models/items/mod.rs @@ -1,2 +1,5 @@ +pub mod action; +pub mod skill; pub mod spell; +pub mod spell_caster_entry; pub mod weapon; diff --git a/src/models/items/skill.rs b/src/models/items/skill.rs new file mode 100644 index 0000000..dd847c6 --- /dev/null +++ b/src/models/items/skill.rs @@ -0,0 +1,12 @@ +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] +pub struct Skill { + pub name: String, + pub description: Option, + pub modifier: i64, + pub proficiency: i64, + // pub publication_info: PublicationInfo, + // pub variant_label: Vec, +} diff --git a/src/models/items/spell_caster_entry.rs b/src/models/items/spell_caster_entry.rs new file mode 100644 index 0000000..d17fdbc --- /dev/null +++ b/src/models/items/spell_caster_entry.rs @@ -0,0 +1,12 @@ +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] +pub struct SpellCasterEntry { + pub spell_casting_name: Option, + pub is_spell_casting_flexible: Option, + pub type_of_spell_caster: Option, + pub spell_casting_dc_mod: Option, + pub spell_casting_atk_mod: Option, + pub spell_casting_tradition: Option, +} diff --git a/src/models/items/weapon.rs b/src/models/items/weapon.rs index ec6e965..7addfbf 100644 --- a/src/models/items/weapon.rs +++ b/src/models/items/weapon.rs @@ -39,3 +39,26 @@ pub struct Weapon { pub wp_type: String, pub creature_id: i64, } + +impl Weapon { + pub fn get_avg_dmg(&self) -> Option { + // avg dice value is + // AVG = (((M+1)/2)∗N)+B + // + // M = max value of the dice + // N = number of dices + // B = bonus dmg + let m = self + .die_size + .clone()? + .split_once('d')? + .1 + .parse::() + .ok()?; + let n = self.n_of_dices? as f64; + let b = self.bonus_dmg? as f64; + + let avg: f64 = (((m + 1.) / 2.) * n) + b; + Some(avg.floor() as i64) + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs index 1216550..d14456b 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,4 +1,5 @@ pub mod creature; +pub mod creature_component; pub mod creature_fields_enum; pub mod creature_filter_enum; pub mod creature_metadata; @@ -7,3 +8,4 @@ pub mod encounter_structs; pub mod items; pub mod response_data; pub mod routers_validator_structs; +pub mod scales_struct; diff --git a/src/models/response_data.rs b/src/models/response_data.rs index 1a1b429..f76b8bf 100644 --- a/src/models/response_data.rs +++ b/src/models/response_data.rs @@ -1,20 +1,31 @@ -use crate::models::creature::{CoreCreatureData, Creature, ExtraCreatureData, VariantCreatureData}; +use crate::models::creature::Creature; +use crate::models::creature_component::creature_combat::CreatureCombatData; +use crate::models::creature_component::creature_core::CreatureCoreData; +use crate::models::creature_component::creature_extra::CreatureExtraData; +use crate::models::creature_component::creature_info::CreatureInfo; +use crate::models::creature_component::creature_spell_caster::CreatureSpellCasterData; +use crate::models::creature_component::creature_variant::CreatureVariantData; use serde::{Deserialize, Serialize}; use utoipa::{IntoParams, ToSchema}; use validator::Validate; #[derive(Serialize, Deserialize, IntoParams, Default, Eq, PartialEq, Hash, Clone, Validate)] pub struct ResponseData { - pub essential_data: bool, - pub variant_data: bool, - pub extra_data: bool, + pub core_data: Option, + pub variant_data: Option, + pub extra_data: Option, + pub combat_data: Option, + pub spell_casting_data: Option, } -#[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] +#[derive(Serialize, Deserialize, Clone, ToSchema, Hash, PartialEq)] pub struct ResponseCreature { - pub core_data: Option, - pub variant_data: Option, - pub extra_data: Option, + pub core_data: Option, + pub variant_data: Option, + pub extra_data: Option, + pub combat_data: Option, + pub spell_caster_data: Option, + pub info: Option, } impl From<(Creature, &ResponseData)> for ResponseCreature { @@ -22,20 +33,36 @@ impl From<(Creature, &ResponseData)> for ResponseCreature { let cr = value.0; let rd = value.1; Self { - core_data: if rd.essential_data { - Some(cr.core_data) - } else { + core_data: if rd.core_data.is_none() || !rd.core_data.unwrap() { None + } else { + Some(cr.core_data) }, - variant_data: if rd.variant_data { + variant_data: if rd.variant_data.is_none() || !rd.variant_data.unwrap() { + None + } else { Some(cr.variant_data) + }, + extra_data: if rd.extra_data.is_none() || !rd.extra_data.unwrap() { + None } else { + Some(cr.extra_data) + }, + info: if rd.extra_data.is_none() || !rd.extra_data.unwrap() { None + } else { + Some(cr.info) }, - extra_data: if rd.extra_data { - Some(cr.extra_data) + spell_caster_data: if rd.spell_casting_data.is_none() || !rd.spell_casting_data.unwrap() + { + None } else { + Some(cr.spell_caster_data) + }, + combat_data: if rd.combat_data.is_none() || !rd.combat_data.unwrap() { None + } else { + Some(cr.combat_data) }, } } diff --git a/src/models/routers_validator_structs.rs b/src/models/routers_validator_structs.rs index ca2af8c..7ad2594 100644 --- a/src/models/routers_validator_structs.rs +++ b/src/models/routers_validator_structs.rs @@ -1,6 +1,8 @@ use crate::models::creature_metadata::alignment_enum::AlignmentEnum; +use crate::models::creature_metadata::creature_role::CreatureRoleEnum; use crate::models::creature_metadata::rarity_enum::RarityEnum; use crate::models::creature_metadata::size_enum::SizeEnum; +use crate::models::creature_metadata::type_enum::CreatureTypeEnum; use serde::{Deserialize, Serialize}; use utoipa::IntoParams; use validator::Validate; @@ -12,6 +14,9 @@ pub struct FieldFilters { pub rarity_filter: Option, pub size_filter: Option, pub alignment_filter: Option, + pub role_filter: Option, + pub type_filter: Option, + pub role_threshold: Option, pub min_hp_filter: Option, pub max_hp_filter: Option, pub min_level_filter: Option, diff --git a/src/models/scales_struct/ability_scales.rs b/src/models/scales_struct/ability_scales.rs new file mode 100644 index 0000000..e09baf8 --- /dev/null +++ b/src/models/scales_struct/ability_scales.rs @@ -0,0 +1,9 @@ +#[derive(Default, Eq, PartialEq, Clone)] +pub struct AbilityScales { + pub id: i64, + pub level: i64, + pub extreme: Option, + pub high: i64, + pub moderate: i64, + pub low: i64, +} diff --git a/src/models/scales_struct/ac_scales.rs b/src/models/scales_struct/ac_scales.rs new file mode 100644 index 0000000..3049656 --- /dev/null +++ b/src/models/scales_struct/ac_scales.rs @@ -0,0 +1,9 @@ +#[derive(Default, Eq, PartialEq, Clone)] +pub struct AcScales { + pub id: i64, + pub level: i64, + pub extreme: i64, + pub high: i64, + pub moderate: i64, + pub low: i64, +} diff --git a/src/models/scales_struct/area_dmg_scales.rs b/src/models/scales_struct/area_dmg_scales.rs new file mode 100644 index 0000000..51c828a --- /dev/null +++ b/src/models/scales_struct/area_dmg_scales.rs @@ -0,0 +1,7 @@ +#[derive(Default, Eq, PartialEq, Clone)] +pub struct AreaDmgScales { + pub id: i64, + pub level: i64, + pub unlimited_use: String, + pub limited_use: String, +} diff --git a/src/models/scales_struct/creature_scales.rs b/src/models/scales_struct/creature_scales.rs new file mode 100644 index 0000000..344b31a --- /dev/null +++ b/src/models/scales_struct/creature_scales.rs @@ -0,0 +1,29 @@ +use crate::models::scales_struct::ability_scales::AbilityScales; +use crate::models::scales_struct::ac_scales::AcScales; +use crate::models::scales_struct::area_dmg_scales::AreaDmgScales; +use crate::models::scales_struct::hp_scales::HpScales; +use crate::models::scales_struct::item_scales::ItemScales; +use crate::models::scales_struct::perception_scales::PerceptionScales; +use crate::models::scales_struct::res_weak_scales::ResWeakScales; +use crate::models::scales_struct::saving_throw_scales::SavingThrowScales; +use crate::models::scales_struct::skill_scales::SkillScales; +use crate::models::scales_struct::spell_dc_and_atk_scales::SpellDcAndAtkScales; +use crate::models::scales_struct::strike_bonus_scales::StrikeBonusScales; +use crate::models::scales_struct::strike_dmg_scales::StrikeDmgScales; +use std::collections::HashMap; + +#[derive(Default, Eq, PartialEq, Clone)] +pub struct CreatureScales { + pub ability_scales: HashMap, + pub ac_scales: HashMap, + pub area_dmg_scales: HashMap, + pub hp_scales: HashMap, + pub item_scales: HashMap, + pub perception_scales: HashMap, + pub res_weak_scales: HashMap, + pub saving_throw_scales: HashMap, + pub skill_scales: HashMap, + pub spell_dc_and_atk_scales: HashMap, + pub strike_bonus_scales: HashMap, + pub strike_dmg_scales: HashMap, +} diff --git a/src/models/scales_struct/hp_scales.rs b/src/models/scales_struct/hp_scales.rs new file mode 100644 index 0000000..0d76f11 --- /dev/null +++ b/src/models/scales_struct/hp_scales.rs @@ -0,0 +1,11 @@ +#[derive(Default, Eq, PartialEq, Clone)] +pub struct HpScales { + pub id: i64, + pub level: i64, + pub high_ub: i64, + pub high_lb: i64, + pub moderate_ub: i64, + pub moderate_lb: i64, + pub low_ub: i64, + pub low_lb: i64, +} diff --git a/src/models/scales_struct/item_scales.rs b/src/models/scales_struct/item_scales.rs new file mode 100644 index 0000000..2271e28 --- /dev/null +++ b/src/models/scales_struct/item_scales.rs @@ -0,0 +1,6 @@ +#[derive(Default, Eq, PartialEq, Clone)] +pub struct ItemScales { + pub id: i64, + pub cr_level: String, + pub safe_item_level: String, +} diff --git a/src/models/scales_struct/mod.rs b/src/models/scales_struct/mod.rs new file mode 100644 index 0000000..27738a3 --- /dev/null +++ b/src/models/scales_struct/mod.rs @@ -0,0 +1,13 @@ +pub mod ability_scales; +pub mod ac_scales; +pub mod area_dmg_scales; +pub mod creature_scales; +pub mod hp_scales; +pub mod item_scales; +pub mod perception_scales; +pub mod res_weak_scales; +pub mod saving_throw_scales; +pub mod skill_scales; +pub mod spell_dc_and_atk_scales; +pub mod strike_bonus_scales; +pub mod strike_dmg_scales; diff --git a/src/models/scales_struct/perception_scales.rs b/src/models/scales_struct/perception_scales.rs new file mode 100644 index 0000000..a2b1904 --- /dev/null +++ b/src/models/scales_struct/perception_scales.rs @@ -0,0 +1,10 @@ +#[derive(Default, Eq, PartialEq, Clone)] +pub struct PerceptionScales { + pub id: i64, + pub level: i64, + pub extreme: i64, + pub high: i64, + pub moderate: i64, + pub low: i64, + pub terrible: i64, +} diff --git a/src/models/scales_struct/res_weak_scales.rs b/src/models/scales_struct/res_weak_scales.rs new file mode 100644 index 0000000..8cbf6c3 --- /dev/null +++ b/src/models/scales_struct/res_weak_scales.rs @@ -0,0 +1,7 @@ +#[derive(Default, Eq, PartialEq, Clone)] +pub struct ResWeakScales { + pub id: i64, + pub level: i64, + pub max: i64, + pub min: i64, +} diff --git a/src/models/scales_struct/saving_throw_scales.rs b/src/models/scales_struct/saving_throw_scales.rs new file mode 100644 index 0000000..ca7d159 --- /dev/null +++ b/src/models/scales_struct/saving_throw_scales.rs @@ -0,0 +1,10 @@ +#[derive(Default, Eq, PartialEq, Clone)] +pub struct SavingThrowScales { + pub id: i64, + pub level: i64, + pub extreme: i64, + pub high: i64, + pub moderate: i64, + pub low: i64, + pub terrible: i64, +} diff --git a/src/models/scales_struct/skill_scales.rs b/src/models/scales_struct/skill_scales.rs new file mode 100644 index 0000000..66f0990 --- /dev/null +++ b/src/models/scales_struct/skill_scales.rs @@ -0,0 +1,10 @@ +#[derive(Default, Eq, PartialEq, Clone)] +pub struct SkillScales { + pub id: i64, + pub level: i64, + pub extreme: i64, + pub high: i64, + pub moderate: i64, + pub low_ub: i64, + pub low_lb: i64, +} diff --git a/src/models/scales_struct/spell_dc_and_atk_scales.rs b/src/models/scales_struct/spell_dc_and_atk_scales.rs new file mode 100644 index 0000000..7999064 --- /dev/null +++ b/src/models/scales_struct/spell_dc_and_atk_scales.rs @@ -0,0 +1,11 @@ +#[derive(Default, Eq, PartialEq, Clone)] +pub struct SpellDcAndAtkScales { + pub id: i64, + pub level: i64, + pub extreme_dc: i64, + pub extreme_atk_bonus: i64, + pub high_dc: i64, + pub high_atk_bonus: i64, + pub moderate_dc: i64, + pub moderate_atk_bonus: i64, +} diff --git a/src/models/scales_struct/strike_bonus_scales.rs b/src/models/scales_struct/strike_bonus_scales.rs new file mode 100644 index 0000000..cb5542e --- /dev/null +++ b/src/models/scales_struct/strike_bonus_scales.rs @@ -0,0 +1,9 @@ +#[derive(Default, Eq, PartialEq, Clone)] +pub struct StrikeBonusScales { + pub id: i64, + pub level: i64, + pub extreme: i64, + pub high: i64, + pub moderate: i64, + pub low: i64, +} diff --git a/src/models/scales_struct/strike_dmg_scales.rs b/src/models/scales_struct/strike_dmg_scales.rs new file mode 100644 index 0000000..3cf98e4 --- /dev/null +++ b/src/models/scales_struct/strike_dmg_scales.rs @@ -0,0 +1,9 @@ +#[derive(Default, Eq, PartialEq, Clone)] +pub struct StrikeDmgScales { + pub id: i64, + pub level: i64, + pub extreme: String, + pub high: String, + pub moderate: String, + pub low: String, +} diff --git a/src/routes/bestiary.rs b/src/routes/bestiary.rs index bd86048..fd6f8e4 100644 --- a/src/routes/bestiary.rs +++ b/src/routes/bestiary.rs @@ -1,4 +1,5 @@ use crate::models::creature_metadata::alignment_enum::AlignmentEnum; +use crate::models::creature_metadata::creature_role::CreatureRoleEnum; use crate::models::creature_metadata::rarity_enum::RarityEnum; use crate::models::creature_metadata::size_enum::SizeEnum; use crate::models::creature_metadata::type_enum::CreatureTypeEnum; @@ -6,11 +7,23 @@ use crate::models::creature_metadata::variant_enum::CreatureVariant; use crate::models::response_data::ResponseCreature; use crate::models::response_data::ResponseData; -use crate::models::creature::{CoreCreatureData, ExtraCreatureData, VariantCreatureData}; +use crate::models::creature_component::creature_combat::CreatureCombatData; +use crate::models::creature_component::creature_combat::SavingThrows; +use crate::models::creature_component::creature_core::CreatureCoreData; +use crate::models::creature_component::creature_extra::AbilityScores; +use crate::models::creature_component::creature_extra::CreatureExtraData; +use crate::models::creature_component::creature_info::CreatureInfo; +use crate::models::creature_component::creature_spell_caster::CreatureSpellCasterData; +use crate::models::creature_component::creature_variant::CreatureVariantData; +use crate::models::items::action::Action; +use crate::models::items::skill::Skill; use crate::models::items::spell::Spell; +use crate::models::items::spell_caster_entry::SpellCasterEntry; use crate::models::items::weapon::Weapon; +use crate::models::creature::PublicationInfo; + use crate::models::routers_validator_structs::{FieldFilters, PaginatedRequest}; use crate::services::bestiary_service; use crate::services::bestiary_service::BestiaryResponse; @@ -31,6 +44,7 @@ pub fn init_endpoints(cfg: &mut web::ServiceConfig) { .service(get_sources_list) .service(get_rarities_list) .service(get_creature_types_list) + .service(get_creature_roles_list) .service(get_sizes_list) .service(get_alignments_list), ); @@ -48,6 +62,7 @@ pub fn init_docs(doc: &mut utoipa::openapi::OpenApi) { get_sizes_list, get_alignments_list, get_creature_types_list, + get_creature_roles_list, get_creature, get_elite_creature, get_weak_creature, @@ -60,11 +75,21 @@ pub fn init_docs(doc: &mut utoipa::openapi::OpenApi) { SizeEnum, CreatureTypeEnum, CreatureVariant, - CoreCreatureData, - VariantCreatureData, - ExtraCreatureData, + CreatureCoreData, + CreatureVariantData, + CreatureExtraData, + CreatureCombatData, + CreatureSpellCasterData, + CreatureInfo, Spell, Weapon, + SavingThrows, + PublicationInfo, + AbilityScores, + Action, + Skill, + CreatureRoleEnum, + SpellCasterEntry )) )] struct ApiDoc; @@ -219,6 +244,23 @@ pub async fn get_creature_types_list(data: web::Data) -> Result Result { + Ok(web::Json(bestiary_service::get_creature_roles_list().await)) +} + #[utoipa::path( get, path = "/bestiary/base/{creature_id}", diff --git a/src/services/bestiary_service.rs b/src/services/bestiary_service.rs index e75d066..6d981f8 100644 --- a/src/services/bestiary_service.rs +++ b/src/services/bestiary_service.rs @@ -1,6 +1,7 @@ use crate::db::db_proxy; use crate::models::creature::Creature; use crate::models::creature_fields_enum::CreatureField; +use crate::models::creature_metadata::creature_role::CreatureRoleEnum; use crate::models::creature_metadata::variant_enum::CreatureVariant; use crate::models::response_data::{ResponseCreature, ResponseData}; use crate::models::routers_validator_structs::{FieldFilters, PaginatedRequest}; @@ -92,6 +93,10 @@ pub async fn get_alignments_list(app_state: &AppState) -> Vec { pub async fn get_creature_types_list(app_state: &AppState) -> Vec { db_proxy::get_keys(app_state, CreatureField::CreatureTypes).await } + +pub async fn get_creature_roles_list() -> Vec { + CreatureRoleEnum::list() +} fn convert_result_to_bestiary_response( field_filters: &FieldFilters, pagination: &PaginatedRequest, diff --git a/src/services/encounter_service.rs b/src/services/encounter_service.rs index 1c97c88..39ba9ca 100644 --- a/src/services/encounter_service.rs +++ b/src/services/encounter_service.rs @@ -1,10 +1,7 @@ use crate::db::db_proxy::{fetch_creatures_passing_all_filters, order_list_by_level}; use crate::models::creature::Creature; +use crate::models::creature_component::filter_struct::FilterStruct; use crate::models::creature_filter_enum::CreatureFilter; -use crate::models::creature_metadata::alignment_enum::AlignmentEnum; -use crate::models::creature_metadata::rarity_enum::RarityEnum; -use crate::models::creature_metadata::size_enum::SizeEnum; -use crate::models::creature_metadata::type_enum::CreatureTypeEnum; use crate::models::creature_metadata::variant_enum::CreatureVariant; use crate::models::encounter_structs::{ EncounterChallengeEnum, EncounterParams, RandomEncounterData, @@ -19,14 +16,14 @@ use log::warn; use rand::seq::IndexedRandom; use rand::Rng; use serde::{Deserialize, Serialize}; -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeMap, HashMap, HashSet}; use utoipa::ToSchema; #[derive(Serialize, Deserialize, ToSchema)] pub struct EncounterInfoResponse { experience: i16, challenge: EncounterChallengeEnum, - encounter_exp_levels: HashMap, + encounter_exp_levels: BTreeMap, } #[derive(Serialize, Deserialize, ToSchema)] @@ -49,7 +46,7 @@ pub fn get_encounter_info(enc_params: EncounterParams) -> EncounterInfoResponse EncounterInfoResponse { experience: enc_exp, challenge: enc_diff, - encounter_exp_levels: scaled_exp, + encounter_exp_levels: scaled_exp.into_iter().collect(), } } @@ -105,15 +102,16 @@ async fn calculate_random_encounter( !unique_levels.is_empty(), "There are no valid levels to chose from. Encounter could not be built" ); - let filter_map = build_filter_map( - enc_data.families, - enc_data.traits, - enc_data.rarities, - enc_data.sizes, - enc_data.alignments, - enc_data.creature_types, - unique_levels, - ); + let filter_map = build_filter_map(FilterStruct { + families: enc_data.families, + traits: enc_data.traits, + rarities: enc_data.rarities, + sizes: enc_data.sizes, + alignments: enc_data.alignments, + creature_types: enc_data.creature_types, + creature_roles: enc_data.creature_roles, + lvl_combinations: unique_levels, + }); let filtered_creatures = get_filtered_creatures( app_state, @@ -223,43 +221,48 @@ fn filter_non_existing_levels( result_vec } -fn build_filter_map( - families: Option>, - traits: Option>, - rarities: Option>, - sizes: Option>, - alignments: Option>, - creature_types: Option>, - lvl_combinations: HashSet, -) -> HashMap> { +fn build_filter_map(filter_enum: FilterStruct) -> HashMap> { let mut filter_map = HashMap::new(); - families.map(|el| filter_map.insert(CreatureFilter::Family, HashSet::from_iter(el))); - traits.map(|el| filter_map.insert(CreatureFilter::Traits, HashSet::from_iter(el))); - rarities.map(|vec| { + + filter_enum + .families + .map(|el| filter_map.insert(CreatureFilter::Family, HashSet::from_iter(el))); + filter_enum + .traits + .map(|el| filter_map.insert(CreatureFilter::Traits, HashSet::from_iter(el))); + // What no generic enum does to a mf (mother function) + // it could also prob be bad programming by me + if let Some(vec) = filter_enum.rarities { filter_map.insert( CreatureFilter::Rarity, HashSet::from_iter(vec.iter().map(|el| el.to_string())), - ) - }); - sizes.map(|vec| { + ); + }; + if let Some(vec) = filter_enum.sizes { filter_map.insert( CreatureFilter::Size, HashSet::from_iter(vec.iter().map(|el| el.to_string())), - ) - }); - alignments.map(|vec| { + ); + }; + if let Some(vec) = filter_enum.alignments { filter_map.insert( CreatureFilter::Alignment, HashSet::from_iter(vec.iter().map(|el| el.to_string())), - ) - }); - creature_types.map(|vec| { + ); + }; + if let Some(vec) = filter_enum.creature_types { filter_map.insert( CreatureFilter::CreatureTypes, HashSet::from_iter(vec.iter().map(|el| el.to_string())), - ) - }); - filter_map.insert(CreatureFilter::Level, lvl_combinations); + ); + }; + if let Some(vec) = filter_enum.creature_roles { + filter_map.insert( + CreatureFilter::CreatureRoles, + HashSet::from_iter(vec.iter().map(|el| el.to_string())), + ); + }; + filter_map.insert(CreatureFilter::Level, filter_enum.lvl_combinations); filter_map }