diff --git a/Cargo.toml b/Cargo.toml index 5a10aa4..e097691 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ authors = ["RakuJa"] # Compiler info edition = "2021" -rust-version = "1.77.1" +rust-version = "1.75.0" description = "Beyond Your Bestiary Explorer (BYBE) is a web service that provides tools to help Pathfinder 2e Game Masters." readme = "README.md" @@ -15,7 +15,7 @@ license = "MIT" keywords = ["webservice", "pathfinder"] publish = false -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +build = "build/main.rs" [lints.rust] unsafe_code = "forbid" @@ -24,19 +24,20 @@ unsafe_code = "forbid" actix-web = "4.5.1" actix-cors = "0.7.0" actix-web-validator = "5.0.1" +# Cannot be updated until actix-web updates validator dependency 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"] } +utoipa = { version = "4.2.3", features = ["actix_extras"] } +utoipa-swagger-ui = { version = "7.0.1", features = ["actix-web"] } sqlx = { version = "0.7.4", features = ["runtime-async-std", "sqlite"] } -mini-moka = "0.10.3" +cached = { version = "0.51.3", features = ["async"] } -anyhow = "1.0.81" -serde = { version = "1.0.197", features = ["derive"] } -serde_json = "1.0.115" +anyhow = "1.0.86" +serde = { version = "1.0.202", features = ["derive"] } +serde_json = "1.0.117" strum = {version="0.26.2", features = ["derive"]} -rand = "0.9.0-alpha.0" +rand = "0.9.0-alpha.1" counter = "0.5.7" dotenvy = "0.15.7" regex = "1.10.4" @@ -44,4 +45,10 @@ regex = "1.10.4" env_logger = "0.11.3" log = "0.4.21" maplit = "1.0.2" -num-traits = "0.2.18" +num-traits = "0.2.19" + +[build-dependencies] +tokio = { version = "1", features = ["macros", "rt-multi-thread", "rt"] } +anyhow = "1.0.86" +sqlx = {version = "0.7.4", features = ["runtime-async-std", "sqlite"]} +dotenvy = "0.15.7" diff --git a/build/creature_core_db_init.rs b/build/creature_core_db_init.rs new file mode 100644 index 0000000..700588d --- /dev/null +++ b/build/creature_core_db_init.rs @@ -0,0 +1,113 @@ +use anyhow::Result; +use sqlx::{Pool, Sqlite}; + +pub async fn create_creature_core_table(conn: &Pool) -> Result<()> { + delete_core_table(conn).await?; + create_temporary_table(conn).await?; + sqlx::query!( + " + CREATE TABLE IF NOT EXISTS CREATURE_CORE( + id INTEGER PRIMARY KEY NOT NULL, + aon_id INTEGER, + name TEXT NOT NULL DEFAULT '', + hp INTEGER NOT NULL DEFAULT -1, + level INTEGER NOT NULL DEFAULT -99, + size TEXT NOT NULL DEFAULT 'MEDIUM', + rarity TEXT NOT NULL DEFAULT 'COMMON', + is_melee BOOL NOT NULL DEFAULT 0, + is_ranged BOOL NOT NULL DEFAULT 0, + is_spell_caster BOOL NOT NULL DEFAULT 0, + archive_link TEXT, + cr_type TEXT NOT NULL DEFAULT 'MONSTER', + family TEXT NOT NULL DEFAULT '-', + license TEXT NOT NULL DEFAULT '', + source TEXT NOT NULL DEFAULT '', + remaster BOOL NOT NULL DEFAULT 0 + )" + ) + .execute(conn) + .await?; + insert_role_columns_in_core_table(conn).await?; + Ok(()) +} + +async fn create_temporary_table(conn: &Pool) -> Result<()> { + sqlx::query!(" + CREATE TABLE IF NOT EXISTS TMP_CREATURE_CORE AS + SELECT + ct.id, + ct.aon_id, + ct.name, + ct.hp, + ct.LEVEL, + ct.SIZE, + ct.rarity, + ct.license, + ct.source, + ct.remaster, + CASE WHEN wt.creature_id IS NOT NULL AND UPPER(wt.wp_type)='MELEE' THEN TRUE ELSE FALSE END AS is_melee, + CASE WHEN wt.creature_id IS NOT NULL AND UPPER(wt.wp_type)='RANGED' THEN TRUE ELSE FALSE END AS is_ranged, + CASE WHEN st.creature_id IS NOT NULL THEN TRUE ELSE FALSE END AS is_spell_caster, + CASE WHEN ct.aon_id IS NOT NULL THEN CONCAT('https://2e.aonprd.com/', CAST(UPPER(COALESCE(UPPER(ct.cr_type) , 'MONSTER')) AS TEXT), 's' , '.aspx?ID=', CAST(ct.aon_id AS TEXT)) ELSE NULL END AS archive_link, + COALESCE(ct.cr_type , 'Monster') AS cr_type, + COALESCE(ct.family , '-') AS family + FROM CREATURE_TABLE ct + LEFT JOIN WEAPON_TABLE wt ON ct.id = wt.creature_id + LEFT JOIN SPELL_TABLE st ON ct.id = st.creature_id + GROUP BY ct.id; + " + // Be careful, cr_type must be either Monster or NPC or we have runtime error + ).execute(conn).await?; + Ok(()) +} + +pub async fn initialize_data(conn: &Pool) -> Result<()> { + sqlx::query( + " + INSERT INTO CREATURE_CORE ( + id, aon_id, name, hp, level, size, rarity, + license, source, remaster, is_melee, is_ranged, + is_spell_caster, archive_link, cr_type, family + ) SELECT + id, aon_id, name, hp, level, size, rarity, + license, source, remaster, is_melee, is_ranged, + is_spell_caster, archive_link, cr_type, family + FROM TMP_CREATURE_CORE; + ", + ) + .execute(conn) + .await?; + Ok(()) +} + +async fn insert_role_columns_in_core_table(conn: &Pool) -> Result<()> { + sqlx::query( + " + ALTER TABLE CREATURE_CORE ADD brute_percentage INTEGER NOT NULL DEFAULT 0; + ALTER TABLE CREATURE_CORE ADD magical_striker_percentage INTEGER NOT NULL DEFAULT 0; + ALTER TABLE CREATURE_CORE ADD skill_paragon_percentage INTEGER NOT NULL DEFAULT 0; + ALTER TABLE CREATURE_CORE ADD skirmisher_percentage INTEGER NOT NULL DEFAULT 0; + ALTER TABLE CREATURE_CORE ADD sniper_percentage INTEGER NOT NULL DEFAULT 0; + ALTER TABLE CREATURE_CORE ADD soldier_percentage INTEGER NOT NULL DEFAULT 0; + ALTER TABLE CREATURE_CORE ADD spell_caster_percentage INTEGER NOT NULL DEFAULT 0; + ", + ) + .execute(conn) + .await?; + Ok(()) +} + +/// Removes temporary tables created during execution of init +pub async fn cleanup_db(conn: &Pool) -> Result<()> { + sqlx::query("DROP TABLE TMP_CREATURE_CORE") + .execute(conn) + .await?; + Ok(()) +} + +async fn delete_core_table(conn: &Pool) -> Result<()> { + sqlx::query!("DROP TABLE IF EXISTS CREATURE_CORE") + .execute(conn) + .await?; + Ok(()) +} diff --git a/build/main.rs b/build/main.rs new file mode 100644 index 0000000..c40d67c --- /dev/null +++ b/build/main.rs @@ -0,0 +1,31 @@ +mod creature_core_db_init; + +use dotenvy::dotenv; +use sqlx::sqlite::SqliteConnectOptions; +use sqlx::SqlitePool; +use std::env; +use std::str::FromStr; + +#[tokio::main] +async fn main() { + dotenv().ok(); + let db_url = &env::var("DATABASE_URL") + .expect("DB URL IS NOT SET.. Aborting. Hint: set DATABASE_URL environmental variable"); + + let conn = SqlitePool::connect_with( + SqliteConnectOptions::from_str(db_url) + .expect("Could not find a valid db in the given path") + .create_if_missing(true), + ) + .await + .expect("Could not connect to the given db url, something went wrong.."); + creature_core_db_init::create_creature_core_table(&conn) + .await + .expect("Could not initialize tables inside the db, something went wrong.."); + creature_core_db_init::initialize_data(&conn) + .await + .expect("Could not populate the db, something went wrong.."); + creature_core_db_init::cleanup_db(&conn) + .await + .expect("Could not clean up the db. Dirty state detected, closing..") +} diff --git a/src/db/cr_core_initializer.rs b/src/db/cr_core_initializer.rs new file mode 100644 index 0000000..495d911 --- /dev/null +++ b/src/db/cr_core_initializer.rs @@ -0,0 +1,151 @@ +use crate::db::data_providers::fetcher::{ + fetch_creature_combat_data, fetch_creature_extra_data, fetch_creature_scales, + fetch_creature_spell_caster_data, +}; +use crate::models::creature_component::creature_core::EssentialData; +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 crate::models::routers_validator_structs::PaginatedRequest; +use anyhow::{bail, Result}; +use serde::{Deserialize, Serialize}; +use sqlx::{FromRow, Pool, Sqlite}; + +/// Handler for startup, first creature_core initialization. Then it shouldn't be used + +pub async fn update_creature_core_table(conn: &Pool) -> Result<()> { + let pagination = PaginatedRequest { + cursor: 0, + page_size: -1, + }; + let scales = fetch_creature_scales(conn).await?; + for cr in get_creatures_raw_essential_data(conn, &pagination).await? { + let essential_data = EssentialData { + id: cr.id, + aon_id: cr.aon_id, + name: cr.name, + hp: cr.hp, + level: cr.level, + size: cr.size, + family: cr.family.unwrap_or(String::from("-")), + rarity: cr.rarity, + license: cr.license, + remaster: cr.remaster, + source: cr.source, + cr_type: CreatureTypeEnum::from(cr.cr_type), + }; + let extra_data = fetch_creature_extra_data(conn, essential_data.id).await?; + let combat_data = fetch_creature_combat_data(conn, essential_data.id).await?; + let spell_caster_data = fetch_creature_spell_caster_data(conn, essential_data.id).await?; + let roles = CreatureRoleEnum::from_creature_with_given_scales( + &essential_data, + &extra_data, + &combat_data, + &spell_caster_data, + &scales, + ); + for (curr_role, curr_percentage) in roles { + update_role_column_value(conn, curr_role, curr_percentage, essential_data.id).await?; + } + } + Ok(()) +} + +async fn update_role_column_value( + conn: &Pool, + role: CreatureRoleEnum, + value: i64, + creature_id: i64, +) -> Result<()> { + let x = match role { + CreatureRoleEnum::Brute => { + sqlx::query!( + "UPDATE CREATURE_CORE SET brute_percentage = ? WHERE id = ?", + value, + creature_id + ) + } + CreatureRoleEnum::MagicalStriker => { + sqlx::query!( + "UPDATE CREATURE_CORE SET magical_striker_percentage = ? WHERE id = ?", + value, + creature_id + ) + } + CreatureRoleEnum::SkillParagon => { + sqlx::query!( + "UPDATE CREATURE_CORE SET skill_paragon_percentage = ? WHERE id = ?", + value, + creature_id + ) + } + CreatureRoleEnum::Skirmisher => { + sqlx::query!( + "UPDATE CREATURE_CORE SET skirmisher_percentage = ? WHERE id = ?", + value, + creature_id + ) + } + CreatureRoleEnum::Sniper => { + sqlx::query!( + "UPDATE CREATURE_CORE SET sniper_percentage = ? WHERE id = ?", + value, + creature_id + ) + } + CreatureRoleEnum::Soldier => { + sqlx::query!( + "UPDATE CREATURE_CORE SET soldier_percentage = ? WHERE id = ?", + value, + creature_id + ) + } + CreatureRoleEnum::SpellCaster => { + sqlx::query!( + "UPDATE CREATURE_CORE SET spell_caster_percentage = ? WHERE id = ?", + value, + creature_id + ) + } + } + .execute(conn) + .await?; + if x.rows_affected() < 1 { + bail!("Error encountered with creature id: {creature_id}. Could not update role: {role}") + } + Ok(()) +} + +async fn get_creatures_raw_essential_data( + conn: &Pool, + paginated_request: &PaginatedRequest, +) -> Result> { + Ok(sqlx::query_as!( + RawEssentialData, + "SELECT + id, aon_id, name, hp, level, size, family, rarity, + license, remaster, source, cr_type + FROM CREATURE_TABLE ORDER BY name LIMIT ?,?", + paginated_request.cursor, + paginated_request.page_size + ) + .fetch_all(conn) + .await?) +} + +#[derive(Serialize, Deserialize, FromRow, Clone)] +pub struct RawEssentialData { + pub id: i64, + pub aon_id: Option, + pub name: String, + pub hp: i64, + pub level: i64, + pub size: SizeEnum, + pub family: Option, + pub rarity: RarityEnum, + pub license: String, + pub remaster: bool, + pub source: String, + pub cr_type: Option, +} diff --git a/src/db/data_providers/fetcher.rs b/src/db/data_providers/fetcher.rs new file mode 100644 index 0000000..1905cbd --- /dev/null +++ b/src/db/data_providers/fetcher.rs @@ -0,0 +1,591 @@ +use crate::db::data_providers::raw_query_builder::prepare_filtered_get_creatures_core; +use crate::models::creature::Creature; +use crate::models::creature_component::creature_combat::{CreatureCombatData, SavingThrows}; +use crate::models::creature_component::creature_core::{ + CreatureCoreData, DerivedData, EssentialData, +}; +use crate::models::creature_component::creature_extra::{AbilityScores, CreatureExtraData}; +use crate::models::creature_component::creature_spell_caster::CreatureSpellCasterData; +use crate::models::creature_component::creature_variant::CreatureVariantData; +use crate::models::creature_filter_enum::CreatureFilter; +use crate::models::creature_metadata::alignment_enum::{AlignmentEnum, ALIGNMENT_TRAITS}; +use crate::models::creature_metadata::variant_enum::CreatureVariant; +use crate::models::db::raw_immunity::RawImmunity; +use crate::models::db::raw_language::RawLanguage; +use crate::models::db::raw_resistance::RawResistance; +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::spell_caster_entry::SpellCasterEntry; +use crate::models::items::weapon::Weapon; +use crate::models::response_data::OptionalData; +use crate::models::routers_validator_structs::PaginatedRequest; +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 sqlx::{FromRow, Pool, Sqlite}; +use std::collections::{HashMap, HashSet}; + +async fn fetch_creature_immunities( + conn: &Pool, + creature_id: i64, +) -> Result> { + Ok(sqlx::query_as!( + RawImmunity, + "SELECT * FROM IMMUNITY_TABLE INTERSECT SELECT immunity_id FROM IMMUNITY_CREATURE_ASSOCIATION_TABLE WHERE creature_id == ($1)", + creature_id + ).fetch_all(conn).await?) +} + +async fn fetch_creature_languages( + conn: &Pool, + creature_id: i64, +) -> Result> { + Ok(sqlx::query_as!( + RawLanguage, + "SELECT * FROM LANGUAGE_TABLE INTERSECT SELECT language_id FROM LANGUAGE_CREATURE_ASSOCIATION_TABLE WHERE creature_id == ($1)", + creature_id + ).fetch_all(conn).await?) +} + +async fn fetch_creature_resistances( + conn: &Pool, + creature_id: i64, +) -> Result> { + Ok(sqlx::query_as!( + RawResistance, + "SELECT name, value FROM RESISTANCE_TABLE WHERE creature_id == ($1)", + creature_id + ) + .fetch_all(conn) + .await?) +} + +async fn fetch_creature_senses(conn: &Pool, creature_id: i64) -> Result> { + Ok(sqlx::query_as!( + RawSense, + "SELECT * FROM SENSE_TABLE INTERSECT SELECT sense_id FROM SENSE_CREATURE_ASSOCIATION_TABLE WHERE creature_id == ($1)", + creature_id + ).fetch_all(conn).await?) +} + +async fn fetch_creature_speeds(conn: &Pool, creature_id: i64) -> Result> { + Ok(sqlx::query_as!( + RawSpeed, + "SELECT name, value FROM SPEED_TABLE WHERE creature_id == ($1)", + creature_id + ) + .fetch_all(conn) + .await?) +} + +async fn fetch_creature_weaknesses( + conn: &Pool, + creature_id: i64, +) -> Result> { + Ok(sqlx::query_as!( + RawWeakness, + "SELECT name, value FROM WEAKNESS_TABLE WHERE creature_id == ($1)", + creature_id + ) + .fetch_all(conn) + .await?) +} + +async fn fetch_creature_saving_throws( + conn: &Pool, + creature_id: i64, +) -> Result { + Ok( + sqlx::query_as!( + SavingThrows, + "SELECT fortitude, reflex, will, fortitude_detail, reflex_detail, will_detail FROM CREATURE_TABLE WHERE id == ($1)", + creature_id + ).fetch_one(conn).await? + ) +} + +async fn fetch_creature_ability_scores( + conn: &Pool, + creature_id: i64, +) -> Result { + Ok( + sqlx::query_as!( + AbilityScores, + "SELECT charisma, constitution, dexterity, intelligence, strength, wisdom FROM CREATURE_TABLE WHERE id == ($1)", + creature_id + ).fetch_one(conn).await? + ) +} + +async fn fetch_creature_ac(conn: &Pool, creature_id: i64) -> Result { + Ok( + sqlx::query_scalar("SELECT ac FROM CREATURE_TABLE WHERE id = $1") + .bind(creature_id) + .fetch_one(conn) + .await?, + ) +} + +async fn fetch_creature_ac_detail(conn: &Pool, creature_id: i64) -> Result> { + Ok( + sqlx::query_scalar("SELECT ac_detail FROM CREATURE_TABLE WHERE id = $1 LIMIT 1") + .bind(creature_id) + .fetch_optional(conn) + .await?, + ) +} + +async fn fetch_creature_hp_detail(conn: &Pool, creature_id: i64) -> Result> { + Ok( + sqlx::query_scalar("SELECT hp_detail FROM CREATURE_TABLE WHERE id = $1 LIMIT 1") + .bind(creature_id) + .fetch_optional(conn) + .await?, + ) +} + +async fn fetch_creature_language_detail( + conn: &Pool, + creature_id: i64, +) -> Result> { + Ok( + sqlx::query_scalar("SELECT language_detail FROM CREATURE_TABLE WHERE id = $1 LIMIT 1") + .bind(creature_id) + .fetch_optional(conn) + .await?, + ) +} + +async fn fetch_creature_perception(conn: &Pool, creature_id: i64) -> Result { + Ok( + sqlx::query_scalar("SELECT perception FROM CREATURE_TABLE WHERE id = $1 LIMIT 1") + .bind(creature_id) + .fetch_one(conn) + .await?, + ) +} + +async fn fetch_creature_perception_detail( + conn: &Pool, + creature_id: i64, +) -> Result> { + Ok( + sqlx::query_scalar("SELECT perception_detail FROM CREATURE_TABLE WHERE id = $1 LIMIT 1") + .bind(creature_id) + .fetch_optional(conn) + .await?, + ) +} + +async fn fetch_creature_traits(conn: &Pool, creature_id: i64) -> Result> { + Ok(sqlx::query_as!( + RawTrait, + "SELECT * FROM TRAIT_TABLE INTERSECT SELECT trait_id FROM TRAIT_CREATURE_ASSOCIATION_TABLE WHERE creature_id == ($1)", + creature_id + ).fetch_all(conn).await?) +} + +async fn fetch_creature_weapons(conn: &Pool, creature_id: i64) -> Result> { + Ok(sqlx::query_as!( + Weapon, + "SELECT * FROM WEAPON_TABLE WHERE creature_id == ($1)", + creature_id + ) + .fetch_all(conn) + .await?) +} + +async fn fetch_creature_actions(conn: &Pool, 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 fetch_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 fetch_creature_spells(conn: &Pool, creature_id: i64) -> Result> { + Ok(sqlx::query_as!( + Spell, + "SELECT * FROM SPELL_TABLE WHERE creature_id == ($1)", + creature_id + ) + .fetch_all(conn) + .await?) +} + +async fn fetch_creature_spell_caster_entry( + conn: &Pool, + creature_id: i64, +) -> Result { + Ok(sqlx::query_as!( + SpellCasterEntry, + "SELECT spell_casting_name, is_spell_casting_flexible, type_of_spell_caster, spell_casting_dc_mod, spell_casting_atk_mod, spell_casting_tradition FROM CREATURE_TABLE WHERE id == ($1) LIMIT 1", + creature_id + ).fetch_one(conn).await?) +} + +async fn fetch_creature_core_data( + conn: &Pool, + creature_id: i64, +) -> Result { + let essential = fetch_creature_essential_data(conn, creature_id).await?; + let derived = fetch_creature_derived_data(conn, creature_id).await?; + let traits = fetch_creature_traits(conn, creature_id) + .await + .unwrap_or_default(); + let is_remaster = essential.remaster; + Ok(CreatureCoreData { + essential, + derived, + traits: traits.iter().map(|x| x.name.clone()).collect(), + alignment: AlignmentEnum::from((&traits, is_remaster)), + }) +} + +async fn fetch_creature_essential_data( + conn: &Pool, + creature_id: i64, +) -> Result { + Ok(sqlx::query_as!( + EssentialData, + "SELECT id, aon_id, name, hp, level, size, family, rarity, + license, remaster, source, cr_type + FROM CREATURE_CORE WHERE id = ? ORDER BY name LIMIT 1", + creature_id, + ) + .fetch_one(conn) + .await?) +} + +async fn fetch_creature_derived_data(conn: &Pool, creature_id: i64) -> Result { + Ok(sqlx::query_as!( + DerivedData, + "SELECT + archive_link, is_melee, is_ranged, is_spell_caster, brute_percentage, + magical_striker_percentage, skill_paragon_percentage, skirmisher_percentage, + sniper_percentage, soldier_percentage, spell_caster_percentage + FROM CREATURE_CORE WHERE id = ? ORDER BY name LIMIT 1", + creature_id, + ) + .fetch_one(conn) + .await?) +} + +async fn update_creatures_core_with_traits( + conn: &Pool, + mut creature_core_data: Vec, +) -> Vec { + for core in &mut creature_core_data { + let traits = fetch_creature_traits(conn, core.essential.id) + .await + .unwrap_or_default(); + let is_remaster = core.essential.remaster; + core.traits = traits + .iter() + .filter(|x| !ALIGNMENT_TRAITS.contains(&&*x.name.as_str().to_uppercase())) + .map(|x| x.name.clone()) + .collect(); + core.alignment = AlignmentEnum::from((&traits, is_remaster)); + } + creature_core_data +} + +#[derive(FromRow)] +struct MyString { + my_str: String, +} + +pub async fn fetch_unique_values_of_field( + conn: &Pool, + table: &str, + field: &str, +) -> Result> { + let query = format!( + "SELECT CAST(t1.{field} AS TEXT) AS my_str FROM ((SELECT DISTINCT ({field}) FROM {table})) t1" + ); + let x: Vec = sqlx::query_as(query.as_str()).fetch_all(conn).await?; + Ok(x.iter().map(|x| x.my_str.clone()).collect()) +} + +pub async fn fetch_traits_associated_with_creatures(conn: &Pool) -> Result> { + let x: Vec = sqlx::query_as( + " + SELECT + tt.name AS my_str + FROM TRAIT_CREATURE_ASSOCIATION_TABLE tcat + LEFT JOIN TRAIT_TABLE tt ON tcat.trait_id = tt.name GROUP BY tt.name", + ) + .fetch_all(conn) + .await?; + Ok(x.iter() + .filter(|x| !ALIGNMENT_TRAITS.contains(&&*x.my_str.as_str().to_uppercase())) + .map(|x| x.my_str.clone()) + .collect()) +} + +pub async fn fetch_creature_by_id( + conn: &Pool, + optional_data: &OptionalData, + id: i64, +) -> Result { + let core_data = fetch_creature_core_data(conn, id).await?; + let level = core_data.essential.level; + let archive_link = core_data.derived.archive_link.clone(); + Ok(Creature { + core_data, + variant_data: CreatureVariantData { + variant: CreatureVariant::Base, + level, + archive_link, + }, + extra_data: if optional_data.extra_data.is_some_and(|x| x) { + Some(fetch_creature_extra_data(conn, id).await?) + } else { + None + }, + combat_data: if optional_data.combat_data.is_some_and(|x| x) { + Some(fetch_creature_combat_data(conn, id).await?) + } else { + None + }, + spell_caster_data: if optional_data.spell_casting_data.is_some_and(|x| x) { + Some(fetch_creature_spell_caster_data(conn, id).await?) + } else { + None + }, + }) +} + +pub async fn fetch_creatures_core_data_with_filters( + conn: &Pool, + key_value_filters: &HashMap>, +) -> Result> { + let query = prepare_filtered_get_creatures_core(key_value_filters); + let core_data: Vec = sqlx::query_as(query.as_str()).fetch_all(conn).await?; + Ok(update_creatures_core_with_traits(conn, core_data).await) +} + +/// Gets all the creatures core it can find with the given pagination as boundaries +/// for the search. +pub async fn fetch_creatures_core_data( + conn: &Pool, + paginated_request: &PaginatedRequest, +) -> Result> { + let cr_core: Vec = + sqlx::query_as("SELECT * FROM CREATURE_CORE ORDER BY name LIMIT ?,?") + .bind(paginated_request.cursor) + .bind(paginated_request.page_size) + .fetch_all(conn) + .await?; + Ok(update_creatures_core_with_traits(conn, cr_core).await) +} + +pub async fn fetch_creature_extra_data( + conn: &Pool, + creature_id: i64, +) -> Result { + let actions = fetch_creature_actions(conn, creature_id) + .await + .unwrap_or_default(); + let skills = fetch_creature_skills(conn, creature_id) + .await + .unwrap_or_default(); + let languages = fetch_creature_languages(conn, creature_id) + .await + .unwrap_or_default(); + let senses = fetch_creature_senses(conn, creature_id) + .await + .unwrap_or_default(); + let speeds = fetch_creature_speeds(conn, creature_id) + .await + .unwrap_or_default(); + let ability_scores = fetch_creature_ability_scores(conn, creature_id).await?; + let hp_detail = fetch_creature_hp_detail(conn, creature_id).await?; + let ac_detail = fetch_creature_ac_detail(conn, creature_id).await?; + let language_detail = fetch_creature_language_detail(conn, creature_id).await?; + let perception = fetch_creature_perception(conn, creature_id).await?; + let perception_detail = fetch_creature_perception_detail(conn, creature_id).await?; + + Ok(CreatureExtraData { + actions, + skills, + languages: languages.iter().map(|x| x.name.clone()).collect(), + senses: senses.iter().map(|x| x.name.clone()).collect(), + speeds: speeds + .iter() + .map(|x| (x.name.clone(), x.value as i16)) + .collect(), + ability_scores, + hp_detail, + ac_detail, + language_detail, + perception, + perception_detail, + }) +} + +pub async fn fetch_creature_combat_data( + conn: &Pool, + creature_id: i64, +) -> Result { + let weapons = fetch_creature_weapons(conn, creature_id) + .await + .unwrap_or_default(); + let resistances = fetch_creature_resistances(conn, creature_id) + .await + .unwrap_or_default(); + let immunities = fetch_creature_immunities(conn, creature_id) + .await + .unwrap_or_default(); + let weaknesses = fetch_creature_weaknesses(conn, creature_id) + .await + .unwrap_or_default(); + let saving_throws = fetch_creature_saving_throws(conn, creature_id).await?; + let creature_ac = fetch_creature_ac(conn, creature_id).await?; + Ok(CreatureCombatData { + weapons, + resistances: resistances + .iter() + .map(|x| (x.name.clone(), x.value as i16)) + .collect(), + immunities: immunities.iter().map(|x| x.name.clone()).collect(), + weaknesses: weaknesses + .iter() + .map(|x| (x.name.clone(), x.value as i16)) + .collect(), + saving_throws, + ac: creature_ac, + }) +} + +pub async fn fetch_creature_spell_caster_data( + conn: &Pool, + creature_id: i64, +) -> Result { + let spells = fetch_creature_spells(conn, creature_id).await?; + let spell_caster_entry = fetch_creature_spell_caster_entry(conn, creature_id).await?; + Ok(CreatureSpellCasterData { + spells, + spell_caster_entry, + }) +} + +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, n)) + .collect(), + ac_scales: sqlx::query_as!(AcScales, "SELECT * FROM AC_SCALES_TABLE",) + .fetch_all(conn) + .await? + .into_iter() + .map(|n| (n.level, 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, n)) + .collect(), + hp_scales: sqlx::query_as!(HpScales, "SELECT * FROM HP_SCALES_TABLE",) + .fetch_all(conn) + .await? + .into_iter() + .map(|n| (n.level, 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, 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, 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, n)) + .collect(), + skill_scales: sqlx::query_as!(SkillScales, "SELECT * FROM SKILL_SCALES_TABLE",) + .fetch_all(conn) + .await? + .into_iter() + .map(|n| (n.level, 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, 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, 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, n)) + .collect(), + }) +} diff --git a/src/db/data_providers/mod.rs b/src/db/data_providers/mod.rs new file mode 100644 index 0000000..b4c8257 --- /dev/null +++ b/src/db/data_providers/mod.rs @@ -0,0 +1,2 @@ +pub mod fetcher; +mod raw_query_builder; diff --git a/src/db/data_providers/raw_query_builder.rs b/src/db/data_providers/raw_query_builder.rs new file mode 100644 index 0000000..ccf5ae0 --- /dev/null +++ b/src/db/data_providers/raw_query_builder.rs @@ -0,0 +1,134 @@ +use crate::models::creature_filter_enum::CreatureFilter; +use log::debug; +use std::collections::{HashMap, HashSet}; + +const ACCURACY_THRESHOLD: i64 = 50; +pub fn prepare_filtered_get_creatures_core( + key_value_filters: &HashMap>, +) -> String { + let mut simple_core_query = String::new(); + let mut trait_query = String::new(); + for (key, value) in key_value_filters { + match key { + CreatureFilter::Level + | CreatureFilter::Melee + | CreatureFilter::Ranged + | CreatureFilter::SpellCaster => { + if !simple_core_query.is_empty() { + simple_core_query.push_str(" AND ") + } + simple_core_query.push_str( + prepare_in_statement_for_generic_type(key.to_string().as_str(), value).as_str(), + ) + } + CreatureFilter::Family + | CreatureFilter::Size + | CreatureFilter::Rarity + | CreatureFilter::CreatureTypes => { + if !simple_core_query.is_empty() { + simple_core_query.push_str(" AND ") + } + simple_core_query.push_str( + prepare_in_statement_for_string_type(key.to_string().as_str(), value).as_str(), + ) + } + CreatureFilter::Traits => trait_query.push_str(prepare_trait_filter(value).as_str()), + CreatureFilter::CreatureRoles => simple_core_query + .push_str(prepare_bounded_check(value, 0, ACCURACY_THRESHOLD).as_str()), + } + } + let mut where_query = simple_core_query.to_string(); + if !trait_query.is_empty() { + where_query.push_str(format!("AND id IN ({trait_query})").as_str()); + }; + if !where_query.is_empty() { + where_query = format!("WHERE {where_query}"); + } + let query = format!("SELECT * FROM CREATURE_CORE {where_query} ORDER BY RANDOM() LIMIT 20"); + debug!("{}", query); + query +} + +/// Prepares a 'bounded AND statement' aka checks if all the columns are in the bound given +/// (brute_percentage >= 0 AND brute_percentage <= 0) AND (sniper_percentage >= 0 ...) ... +fn prepare_bounded_check( + column_names: &HashSet, + lower_bound: i64, + upper_bound: i64, +) -> String { + let mut bounded_query = String::new(); + if column_names.is_empty() { + return bounded_query; + } + for column in column_names { + if !bounded_query.is_empty() { + bounded_query.push_str(" AND "); + } + bounded_query.push_str( + format!("({column} >= {lower_bound} AND {column} <= {upper_bound})").as_str(), + ); + } + bounded_query +} + +/// Prepares a query that gets all the ids linked with a given list of traits, example +/// SELECT tcat.creature_id +/// FROM TRAIT_CREATURE_ASSOCIATION_TABLE tcat +/// RIGHT JOIN +/// (SELECT * FROM TRAIT_TABLE WHERE name IN ('good')) tt +/// ON tcat.trait_id = tt.name GROUP BY tcat.creature_id +/// +fn prepare_trait_filter(column_values: &HashSet) -> String { + let mut in_string = String::new(); + in_string.push_str(prepare_in_statement_for_string_type("tt.name", column_values).as_str()); + if !in_string.is_empty() { + let select_query = "SELECT tcat.creature_id FROM TRAIT_CREATURE_ASSOCIATION_TABLE"; + let inner_query = format!("SELECT * FROM TRAIT_TABLE tt WHERE {in_string}"); + return format!("{select_query} tcat RIGHT JOIN ({inner_query}) jt ON tcat.trait_id = jt.name GROUP BY tcat.creature_id"); + } + in_string +} + +/// Prepares an 'in' statement in the following format. Assuming a string value +/// "field in ('el1', 'el2', 'el3')" +fn prepare_in_statement_for_string_type( + column_name: &str, + column_values: &HashSet, +) -> String { + let mut result_string = String::new(); + if !column_values.is_empty() { + result_string.push_str(format!("UPPER({column_name})").as_str()); + result_string.push_str(" IN ("); + column_values.iter().for_each(|x| { + result_string.push_str(format!("UPPER('{x}')").as_str()); + result_string.push(','); + }); + if result_string.ends_with(',') { + result_string.remove(result_string.len() - 1); + } + result_string.push(')') + } + result_string +} + +/// Prepares an 'in' statement in the following format +/// 'field in (el1, el2, el3)' +fn prepare_in_statement_for_generic_type( + column_name: &str, + column_values: &HashSet, +) -> String { + let mut result_string = String::new(); + if !column_values.is_empty() { + result_string.push_str(column_name); + result_string.push_str(" IN ("); + column_values.iter().for_each(|x| { + result_string.push_str(x); + result_string.push(','); + }); + if result_string.ends_with(',') { + result_string.remove(result_string.len() - 1); + } + result_string.push(')') + } + result_string +} diff --git a/src/db/db_cache.rs b/src/db/db_cache.rs deleted file mode 100644 index a9e1fe9..0000000 --- a/src/db/db_cache.rs +++ /dev/null @@ -1,101 +0,0 @@ -use crate::models::creature::Creature; -use crate::AppState; - -#[derive(Default, Eq, PartialEq, Clone)] -pub struct RuntimeFieldsValues { - pub list_of_ids: Vec, - pub list_of_levels: Vec, - pub list_of_families: Vec, - pub list_of_traits: Vec, - pub list_of_sources: Vec, - pub list_of_alignments: Vec, - pub list_of_sizes: Vec, - pub list_of_rarities: Vec, - pub list_of_creature_types: Vec, -} - -pub fn from_db_data_to_filter_cache( - app_state: &AppState, - data: Vec, -) -> RuntimeFieldsValues { - let mut fields_values_cache = RuntimeFieldsValues::default(); - let cache = &app_state.runtime_fields_cache.clone(); - if let Some(runtime_fields) = cache.get(&0) { - return RuntimeFieldsValues { - list_of_ids: runtime_fields.list_of_ids.clone(), - list_of_levels: runtime_fields.list_of_levels.clone(), - list_of_families: runtime_fields.list_of_families.clone(), - list_of_traits: runtime_fields.list_of_traits.clone(), - list_of_sources: runtime_fields.list_of_sources.clone(), - list_of_alignments: runtime_fields.list_of_alignments.clone(), - list_of_sizes: runtime_fields.list_of_sizes.clone(), - list_of_rarities: runtime_fields.list_of_rarities.clone(), - list_of_creature_types: runtime_fields.list_of_creature_types.clone(), - }; - } - for curr_creature in data { - let id = curr_creature.core_data.id.to_string(); - let lvl = curr_creature.variant_data.level.to_string(); - let family = if curr_creature.core_data.family.is_some() { - curr_creature.core_data.family.unwrap() - } else { - "-".to_string() - }; - let alignment = curr_creature.core_data.alignment.to_string(); - let size = curr_creature.core_data.size.to_string(); - let rarity = curr_creature.core_data.rarity.to_string(); - let creature_type = curr_creature.core_data.creature_type.to_string(); - - if !fields_values_cache.list_of_ids.contains(&id) { - fields_values_cache.list_of_ids.push(id); - } - if !fields_values_cache.list_of_levels.contains(&lvl) { - fields_values_cache.list_of_levels.push(lvl); - } - if !fields_values_cache.list_of_families.contains(&family) { - fields_values_cache.list_of_families.push(family); - } - - curr_creature - .core_data - .traits - .iter() - .for_each(|single_trait| { - if !fields_values_cache.list_of_traits.contains(single_trait) { - fields_values_cache - .list_of_traits - .push(single_trait.to_string()) - } - }); - - if !fields_values_cache - .list_of_sources - .contains(&curr_creature.core_data.publication_info.source) - { - fields_values_cache - .list_of_sources - .push(curr_creature.core_data.publication_info.source.clone()); - } - - if !fields_values_cache.list_of_alignments.contains(&alignment) { - fields_values_cache.list_of_alignments.push(alignment); - } - if !fields_values_cache.list_of_sizes.contains(&size) { - fields_values_cache.list_of_sizes.push(size); - } - if !fields_values_cache.list_of_rarities.contains(&rarity) { - fields_values_cache.list_of_rarities.push(rarity); - } - if !fields_values_cache - .list_of_creature_types - .contains(&creature_type) - { - fields_values_cache - .list_of_creature_types - .push(creature_type); - } - } - cache.insert(0, fields_values_cache.clone()); - - fields_values_cache -} diff --git a/src/db/db_communicator.rs b/src/db/db_communicator.rs deleted file mode 100644 index e087164..0000000 --- a/src/db/db_communicator.rs +++ /dev/null @@ -1,295 +0,0 @@ -use crate::models::creature::Creature; -use crate::models::db::raw_creature::RawCreature; -use crate::models::db::raw_immunity::RawImmunity; -use crate::models::db::raw_language::RawLanguage; -use crate::models::db::raw_resistance::RawResistance; -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, - 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 - .unwrap_or_default(); - let languages = get_creature_languages(conn, el.id) - .await - .unwrap_or_default(); - let resistances = get_creature_resistances(conn, el.id) - .await - .unwrap_or_default(); - let senses = get_creature_senses(conn, el.id).await.unwrap_or_default(); - let speeds = get_creature_speeds(conn, el.id).await.unwrap_or_default(); - let traits = get_creature_traits(conn, el.id).await.unwrap_or_default(); - let weaknesses = get_creature_weaknesses(conn, el.id) - .await - .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 -} - -async fn get_creature_immunities( - conn: &Pool, - creature_id: i64, -) -> Result> { - Ok(sqlx::query_as!( - RawImmunity, - "SELECT * FROM IMMUNITY_TABLE INTERSECT SELECT immunity_id FROM IMMUNITY_CREATURE_ASSOCIATION_TABLE WHERE creature_id == ($1)", - creature_id - ).fetch_all(conn).await?) -} - -async fn get_creature_languages(conn: &Pool, creature_id: i64) -> Result> { - Ok(sqlx::query_as!( - RawLanguage, - "SELECT * FROM LANGUAGE_TABLE INTERSECT SELECT language_id FROM LANGUAGE_CREATURE_ASSOCIATION_TABLE WHERE creature_id == ($1)", - creature_id - ).fetch_all(conn).await?) -} - -async fn get_creature_resistances( - conn: &Pool, - creature_id: i64, -) -> Result> { - Ok(sqlx::query_as!( - RawResistance, - "SELECT name, value FROM RESISTANCE_TABLE WHERE creature_id == ($1)", - creature_id - ) - .fetch_all(conn) - .await?) -} - -async fn get_creature_senses(conn: &Pool, creature_id: i64) -> Result> { - Ok(sqlx::query_as!( - RawSense, - "SELECT * FROM SENSE_TABLE INTERSECT SELECT sense_id FROM SENSE_CREATURE_ASSOCIATION_TABLE WHERE creature_id == ($1)", - creature_id - ).fetch_all(conn).await?) -} - -async fn get_creature_speeds(conn: &Pool, creature_id: i64) -> Result> { - Ok(sqlx::query_as!( - RawSpeed, - "SELECT name, value FROM SPEED_TABLE WHERE creature_id == ($1)", - creature_id - ) - .fetch_all(conn) - .await?) -} - -async fn get_creature_weaknesses( - conn: &Pool, - creature_id: i64, -) -> Result> { - Ok(sqlx::query_as!( - RawWeakness, - "SELECT name, value FROM WEAKNESS_TABLE WHERE creature_id == ($1)", - creature_id - ) - .fetch_all(conn) - .await?) -} - -async fn get_creature_traits(conn: &Pool, creature_id: i64) -> Result> { - Ok(sqlx::query_as!( - RawTrait, - "SELECT * FROM TRAIT_TABLE INTERSECT SELECT trait_id FROM TRAIT_CREATURE_ASSOCIATION_TABLE WHERE creature_id == ($1)", - creature_id - ).fetch_all(conn).await?) -} - -async fn get_creature_weapons(conn: &Pool, creature_id: i64) -> Result> { - Ok(sqlx::query_as!( - Weapon, - "SELECT * FROM WEAPON_TABLE WHERE creature_id == ($1)", - creature_id - ) - .fetch_all(conn) - .await?) -} - -async fn get_creature_actions(conn: &Pool, 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, - "SELECT * FROM SPELL_TABLE WHERE creature_id == ($1)", - creature_id - ) - .fetch_all(conn) - .await?) -} - -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 deleted file mode 100644 index a0d72bd..0000000 --- a/src/db/db_proxy.rs +++ /dev/null @@ -1,275 +0,0 @@ -use crate::db::db_communicator; -use crate::models::creature::Creature; -use std::collections::{HashMap, HashSet}; - -use crate::db::db_cache::from_db_data_to_filter_cache; -use crate::models::creature_fields_enum::CreatureField; -use crate::models::creature_filter_enum::CreatureFilter; -use crate::models::creature_metadata::variant_enum::CreatureVariant; -use crate::models::routers_validator_structs::{FieldFilters, PaginatedRequest}; -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 } -} - -pub async fn get_creature_by_id( - app_state: &AppState, - id: i32, - variant: CreatureVariant, -) -> Option { - let list = get_list(app_state, variant).await; - list.iter() - .find(|creature| creature.core_data.id == id) - .cloned() -} - -fn convert_creature_to_variant(creature: &Creature, level_delta: i8) -> Creature { - let mut cr = creature.clone(); - let hp_increase = hp_increase_by_level(); - let desired_key = hp_increase - .keys() - .filter(|&&lvl| cr.variant_data.level >= lvl) - .max() - .unwrap_or(hp_increase.keys().next().unwrap_or(&0)); - cr.core_data.hp += *hp_increase.get(desired_key).unwrap_or(&0) as i16 * level_delta as i16; - cr.core_data.hp = cr.core_data.hp.max(1); - - cr.variant_data.level += level_delta; - - if level_delta >= 1 { - cr.core_data.variant = CreatureVariant::Elite - } else if level_delta <= -1 { - cr.core_data.variant = CreatureVariant::Weak - } else { - cr.core_data.variant = CreatureVariant::Base - } - if cr.core_data.variant != CreatureVariant::Base { - cr.variant_data.archive_link = add_boolean_query( - creature.core_data.archive_link.clone(), - &cr.core_data.variant.to_string(), - true, - ); - } - cr -} - -pub async fn get_weak_creature_by_id(app_state: &AppState, id: i32) -> Option { - get_creature_by_id(app_state, id, CreatureVariant::Weak).await -} -pub async fn get_elite_creature_by_id(app_state: &AppState, id: i32) -> Option { - get_creature_by_id(app_state, id, CreatureVariant::Elite).await -} - -pub async fn get_paginated_creatures( - app_state: &AppState, - filters: &FieldFilters, - pagination: &PaginatedRequest, -) -> Result<(u32, Vec)> { - let list = get_list(app_state, CreatureVariant::Base).await; - - let filtered_list: Vec = list - .into_iter() - .filter(|x| Creature::is_passing_filters(x, filters)) - .collect(); - - let curr_slice: Vec = filtered_list - .iter() - .skip(pagination.cursor as usize) - .take(pagination.page_size as usize) - .cloned() - .collect(); - - Ok((curr_slice.len() as u32, curr_slice)) -} - -pub async fn fetch_creatures_passing_all_filters( - app_state: &AppState, - key_value_filters: &HashMap>, - variant: CreatureVariant, -) -> Result> { - let creature_list = get_list(app_state, variant).await; - let mut intersection = HashSet::from_iter(creature_list.iter().cloned()); - key_value_filters - .iter() - .map(|(curr_field_filter, curr_value_filter)| { - fetch_creatures_passing_single_filter( - &creature_list, - curr_field_filter, - curr_value_filter, - ) - }) - .for_each(|curr| intersection = intersection.intersection(&curr).cloned().collect()); - Ok(intersection) -} - -fn fetch_creatures_passing_single_filter( - creature_list: &[Creature], - field_in_which_to_filter: &CreatureFilter, - filter_vec: &HashSet, -) -> HashSet { - let cr_iterator = creature_list.iter().cloned(); - match field_in_which_to_filter { - CreatureFilter::Id => cr_iterator - .filter(|creature| filter_vec.contains(creature.core_data.id.to_string().as_str())) - .collect(), - CreatureFilter::Level => cr_iterator - .filter(|creature| { - filter_vec.contains(creature.variant_data.level.to_string().as_str()) - }) - .collect(), - CreatureFilter::Family => cr_iterator - .filter(|creature| { - filter_vec.contains( - creature - .core_data - .family - .clone() - .unwrap_or_default() - .as_str(), - ) - }) - .collect(), - CreatureFilter::Traits => cr_iterator - .filter(|creature| { - filter_vec - .iter() - .any(|curr_trait| creature.clone().core_data.traits.contains(curr_trait)) - }) - .collect(), - CreatureFilter::CreatureTypes => cr_iterator - .filter(|creature| { - filter_vec.contains(creature.core_data.creature_type.to_string().as_str()) - }) - .collect(), - CreatureFilter::Alignment => cr_iterator - .filter(|creature| { - filter_vec.contains(creature.core_data.alignment.to_string().as_str()) - }) - .collect(), - CreatureFilter::Size => cr_iterator - .filter(|creature| filter_vec.contains(creature.core_data.size.to_string().as_str())) - .collect(), - CreatureFilter::Rarity => cr_iterator - .filter(|creature| filter_vec.contains(creature.core_data.rarity.to_string().as_str())) - .collect(), - CreatureFilter::Melee => cr_iterator - .filter(|creature| { - filter_vec.contains(creature.core_data.is_melee.to_string().as_str()) - }) - .collect(), - CreatureFilter::Ranged => cr_iterator - .filter(|creature| { - filter_vec.contains(creature.core_data.is_ranged.to_string().as_str()) - }) - .collect(), - CreatureFilter::SpellCaster => cr_iterator - .filter(|creature| { - 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(), - } -} - -pub async fn get_keys(app_state: &AppState, field: CreatureField) -> Vec { - if let Some(db_data) = fetch_data_from_database(app_state, CreatureVariant::Base).await { - let runtime_fields_values = from_db_data_to_filter_cache(app_state, db_data); - let mut x = match field { - CreatureField::Id => runtime_fields_values.list_of_ids, - CreatureField::Size => runtime_fields_values.list_of_sizes, - CreatureField::Rarity => runtime_fields_values.list_of_rarities, - CreatureField::Ranged => vec![true.to_string(), false.to_string()], - CreatureField::Melee => vec![true.to_string(), false.to_string()], - CreatureField::SpellCaster => vec![true.to_string(), false.to_string()], - CreatureField::Family => runtime_fields_values.list_of_families, - CreatureField::Traits => runtime_fields_values.list_of_traits, - CreatureField::Sources => runtime_fields_values.list_of_sources, - 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(); - return x; - } - vec![] -} - -async fn fetch_data_from_database( - app_state: &AppState, - variant: CreatureVariant, -) -> Option> { - if let Some(creature_vec) = fetch_creatures(app_state, variant).await { - return Some(creature_vec); - } - None -} - -async fn fetch_creatures(app_state: &AppState, variant: CreatureVariant) -> Option> { - let cache = &app_state.creature_cache.clone(); - 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, &app_state.creature_scales).await - { - cache.insert(0, creatures.clone()); - let mut weak_creatures = Vec::new(); - let mut elite_creatures = Vec::new(); - - creatures.iter().for_each(|cr| { - weak_creatures.push(convert_creature_to_variant( - cr, - CreatureVariant::to_level_delta(&CreatureVariant::Weak), - )); - elite_creatures.push(convert_creature_to_variant( - cr, - CreatureVariant::to_level_delta(&CreatureVariant::Elite), - )); - }); - cache.insert( - CreatureVariant::to_cache_index(&CreatureVariant::Weak), - weak_creatures.clone(), - ); - cache.insert( - CreatureVariant::to_cache_index(&CreatureVariant::Elite), - elite_creatures.clone(), - ); - return match variant { - CreatureVariant::Weak => Some(weak_creatures), - CreatureVariant::Elite => Some(elite_creatures), - _ => Some(creatures), - }; - } - None -} - -async fn get_list(app_state: &AppState, variant: CreatureVariant) -> Vec { - if let Some(db_data) = fetch_data_from_database(app_state, variant).await { - return db_data; - } - vec![] -} - -pub fn order_list_by_level(creature_list: HashSet) -> HashMap> { - let mut ordered_by_level = HashMap::new(); - creature_list.iter().for_each(|creature| { - ordered_by_level - .entry(creature.variant_data.level as i16) - .or_insert_with(Vec::new) - .push(creature.clone()); - }); - ordered_by_level -} diff --git a/src/db/mod.rs b/src/db/mod.rs index 6b48a43..5a7f6b7 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,3 +1,3 @@ -pub mod db_cache; -pub mod db_communicator; -pub mod db_proxy; +pub mod cr_core_initializer; +pub mod data_providers; +pub mod proxy; diff --git a/src/db/proxy.rs b/src/db/proxy.rs new file mode 100644 index 0000000..35689a8 --- /dev/null +++ b/src/db/proxy.rs @@ -0,0 +1,223 @@ +use crate::models::creature::Creature; +use std::collections::{HashMap, HashSet}; + +use crate::db::data_providers::fetcher; +use crate::db::data_providers::fetcher::{ + fetch_traits_associated_with_creatures, fetch_unique_values_of_field, +}; +use crate::models::creature_component::creature_core::CreatureCoreData; +use crate::models::creature_component::fields_unique_values_struct::FieldsUniqueValuesStruct; +use crate::models::creature_fields_enum::CreatureField; +use crate::models::creature_filter_enum::CreatureFilter; +use crate::models::creature_metadata::alignment_enum::AlignmentEnum; +use crate::models::creature_metadata::type_enum::CreatureTypeEnum; +use crate::models::creature_metadata::variant_enum::CreatureVariant; +use crate::models::response_data::OptionalData; +use crate::models::routers_validator_structs::{FieldFilters, PaginatedRequest}; +use crate::AppState; +use anyhow::Result; +use cached::proc_macro::once; +use strum::IntoEnumIterator; + +pub async fn get_creature_by_id( + app_state: &AppState, + id: i64, + variant: &CreatureVariant, + optional_data: &OptionalData, +) -> Option { + let creature = fetcher::fetch_creature_by_id(&app_state.conn, optional_data, id) + .await + .ok()?; + Some(creature.convert_creature_to_variant(variant)) +} + +pub async fn get_weak_creature_by_id( + app_state: &AppState, + id: i64, + optional_data: &OptionalData, +) -> Option { + get_creature_by_id(app_state, id, &CreatureVariant::Weak, optional_data).await +} +pub async fn get_elite_creature_by_id( + app_state: &AppState, + id: i64, + optional_data: &OptionalData, +) -> Option { + get_creature_by_id(app_state, id, &CreatureVariant::Elite, optional_data).await +} + +pub async fn get_paginated_creatures( + app_state: &AppState, + filters: &FieldFilters, + pagination: &PaginatedRequest, +) -> Result<(u32, Vec)> { + let list = get_list(app_state, CreatureVariant::Base).await; + + let filtered_list: Vec = list + .into_iter() + .filter(|x| Creature::is_passing_filters(x, filters)) + .collect(); + + let curr_slice: Vec = filtered_list + .iter() + .skip(pagination.cursor as usize) + .take(pagination.page_size as usize) + .cloned() + .collect(); + + Ok((curr_slice.len() as u32, curr_slice)) +} + +pub async fn get_creatures_passing_all_filters( + app_state: &AppState, + key_value_filters: HashMap>, + fetch_weak: bool, + fetch_elite: bool, +) -> Result> { + let mut creature_vec = Vec::new(); + let empty_set = HashSet::new(); + let level_vec = key_value_filters + .get(&CreatureFilter::Level) + .unwrap_or(&empty_set) + .clone(); + let modified_filters = + prepare_filters_for_db_communication(key_value_filters, fetch_weak, fetch_elite); + for core in + fetcher::fetch_creatures_core_data_with_filters(&app_state.conn, &modified_filters).await? + { + // We have fetched creature with level +1 if weak is allowed or level-1 if elite is allowed + // (or both). Now we catalogue correctly giving them the elite or weak variant, this does not + // mean that if we have [0,1,2,3] in the filter and allow_elite => [-1,0,1,2,3] then + // a creature of level 1 will always be considered the elite variant of level 0. We'll + // duplicate the data and will have a base 0 for level 0 and elite 0 for level 1 + if fetch_weak && level_vec.contains(&(core.essential.level - 1).to_string()) { + creature_vec.push(Creature::from_core_with_variant( + core.clone(), + &CreatureVariant::Weak, + )); + } + if fetch_elite && level_vec.contains(&(core.essential.level + 1).to_string()) { + creature_vec.push(Creature::from_core_with_variant( + core.clone(), + &CreatureVariant::Elite, + )); + } + creature_vec.push(Creature::from_core(core)); + } + Ok(creature_vec) +} + +pub async fn get_keys(app_state: &AppState, field: CreatureField) -> Vec { + let runtime_fields_values = get_all_keys(app_state).await; + let mut x = match field { + CreatureField::Size => runtime_fields_values.list_of_sizes, + CreatureField::Rarity => runtime_fields_values.list_of_rarities, + CreatureField::Ranged => vec![true.to_string(), false.to_string()], + CreatureField::Melee => vec![true.to_string(), false.to_string()], + CreatureField::SpellCaster => vec![true.to_string(), false.to_string()], + CreatureField::Family => runtime_fields_values.list_of_families, + CreatureField::Traits => runtime_fields_values.list_of_traits, + CreatureField::Sources => runtime_fields_values.list_of_sources, + CreatureField::Alignment => AlignmentEnum::iter().map(|x| x.to_string()).collect(), + CreatureField::Level => runtime_fields_values.list_of_levels, + CreatureField::CreatureTypes => CreatureTypeEnum::iter().map(|x| x.to_string()).collect(), + _ => vec![], + }; + x.sort(); + x +} + +/// Gets all the runtime keys (each table column unique values). It will cache the result +#[once(sync_writes = true)] +async fn get_all_keys(app_state: &AppState) -> FieldsUniqueValuesStruct { + FieldsUniqueValuesStruct { + list_of_levels: fetch_unique_values_of_field(&app_state.conn, "CREATURE_CORE", "level") + .await + .unwrap_or_default(), + list_of_families: fetch_unique_values_of_field(&app_state.conn, "CREATURE_CORE", "family") + .await + .unwrap(), + list_of_traits: fetch_traits_associated_with_creatures(&app_state.conn) + .await + .unwrap_or_default(), + list_of_sources: fetch_unique_values_of_field(&app_state.conn, "CREATURE_CORE", "source") + .await + .unwrap_or_default(), + list_of_sizes: fetch_unique_values_of_field(&app_state.conn, "CREATURE_CORE", "size") + .await + .unwrap_or_default(), + list_of_rarities: fetch_unique_values_of_field(&app_state.conn, "CREATURE_CORE", "rarity") + .await + .unwrap_or_default(), + } +} + +/// Gets all the creature core data from the DB. It will not fetch data outside of variant and core. +/// It will cache the result. +#[once(sync_writes = true, result = true)] +async fn get_all_creatures_from_db(app_state: &AppState) -> Result> { + fetcher::fetch_creatures_core_data( + &app_state.conn, + &PaginatedRequest { + cursor: 0, + page_size: -1, + }, + ) + .await +} + +/// Infallible method, it will expose a vector representing the values fetched from db or empty vec +async fn get_list(app_state: &AppState, variant: CreatureVariant) -> Vec { + if let Ok(creatures) = get_all_creatures_from_db(app_state).await { + return match variant { + CreatureVariant::Base => creatures.into_iter().map(Creature::from_core).collect(), + _ => creatures + .into_iter() + .map(|cr| Creature::from_core_with_variant(cr, &variant)) + .collect(), + }; + } + vec![] +} + +pub fn order_list_by_level(creature_list: Vec) -> HashMap> { + let mut ordered_by_level = HashMap::new(); + creature_list.iter().for_each(|creature| { + ordered_by_level + .entry(creature.variant_data.level) + .or_insert_with(Vec::new) + .push(creature.clone()); + }); + ordered_by_level +} + +/// Used to prepare the filters for db communication. +/// The level must be adjusted if elite/weak must be fetched. +///Example if we allow weak then we can fetch creature with level +1 => weak = level +fn prepare_filters_for_db_communication( + key_value_filters: HashMap>, + fetch_weak: bool, + fetch_elite: bool, +) -> HashMap> { + key_value_filters + .into_iter() + .map(|(key, values)| match key { + CreatureFilter::Level => { + let mut new_values = HashSet::new(); + for str_level in values { + if let Ok(level) = str_level.parse::() { + if fetch_weak { + new_values.insert((level + 1).to_string()); + } + if fetch_elite { + new_values.insert((level - 1).to_string()); + } + new_values.insert(level.to_string()); + } + } + (key, new_values) + } + _ => (key, values), + }) + .collect() +} diff --git a/src/main.rs b/src/main.rs index 9f3235e..5e0a4c2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,18 +3,13 @@ extern crate maplit; 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}; use actix_web::{get, middleware, web, App, HttpResponse, HttpServer, Responder}; use dotenvy::dotenv; -use mini_moka::sync::Cache; use sqlx::{sqlite::SqlitePoolOptions, Pool, Sqlite}; use std::env; -use std::time::Duration; use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; @@ -25,9 +20,6 @@ mod services; #[derive(Clone)] pub struct AppState { conn: Pool, - creature_cache: Cache>, - runtime_fields_cache: Cache, - creature_scales: CreatureScales, } #[utoipa::path(get, path = "/")] @@ -66,6 +58,8 @@ async fn main() -> std::io::Result<()> { let service_ip = get_service_ip(); let service_port = get_service_port(); + log::info!("Starting DB connection & creation of required tables",); + // establish connection to database let pool = SqlitePoolOptions::new() .max_connections(5) @@ -73,26 +67,9 @@ async fn main() -> std::io::Result<()> { .await .expect("Error building connection pool"); - let cr_cache = Cache::builder() - // Time to live (TTL): 1 week - .time_to_live(Duration::from_secs(604800)) - // Time to idle (TTI): 1 week - // .time_to_idle(Duration::from_secs( 5 * 60)) - // Create the cache. - .build(); - - let fields_cache = Cache::builder() - // Time to live (TTL): 1 week - .time_to_live(Duration::from_secs(604800)) - // Time to idle (TTI): 1 week - // .time_to_idle(Duration::from_secs( 5 * 60)) - // Create the cache. - .build(); - - let creature_scales = db::db_communicator::fetch_creature_scales(&pool) + db::cr_core_initializer::update_creature_core_table(&pool) .await - .expect("Could not establish valid connection with the database.. Startup failed"); - + .expect("Could not initialize correctly core creature table.. Startup failed"); log::info!( "starting HTTP server at http://{}:{}", service_ip.as_str(), @@ -129,12 +106,7 @@ async fn main() -> std::io::Result<()> { .service( SwaggerUi::new("/swagger-ui/{_:.*}").url("/api-docs/openapi.json", openapi.clone()), ) - .app_data(web::Data::new(AppState { - conn: pool.clone(), - creature_cache: cr_cache.clone(), - runtime_fields_cache: fields_cache.clone(), - creature_scales: creature_scales.clone(), - })) + .app_data(web::Data::new(AppState { conn: pool.clone() })) }) .bind((get_service_ip(), get_service_port()))? .run() diff --git a/src/models/creature.rs b/src/models/creature.rs index 01e0f2d..17a7fbb 100644 --- a/src/models/creature.rs +++ b/src/models/creature.rs @@ -1,46 +1,66 @@ 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; -use crate::models::db::raw_resistance::RawResistance; -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::creature_metadata::creature_role::CreatureRoleEnum; +use crate::models::creature_metadata::variant_enum::CreatureVariant; 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 PublicationInfo { - pub license: String, - pub remaster: bool, - pub source: String, -} #[derive(Serialize, Deserialize, Clone, Eq, Hash, PartialEq)] pub struct Creature { pub core_data: CreatureCoreData, pub variant_data: CreatureVariantData, - pub extra_data: CreatureExtraData, - pub combat_data: CreatureCombatData, - pub spell_caster_data: CreatureSpellCasterData, - pub info: CreatureInfo, + pub extra_data: Option, + pub combat_data: Option, + pub spell_caster_data: Option, } impl Creature { + pub fn convert_creature_to_variant(self, variant: &CreatureVariant) -> Creature { + let mut cr = Self::from_core_with_variant(self.core_data, variant); + cr.extra_data = self.extra_data; + cr.combat_data = self.combat_data; + cr.spell_caster_data = self.spell_caster_data; + cr + } + pub fn from_core(core: CreatureCoreData) -> Creature { + let level = core.essential.level; + let archive_link = core.derived.archive_link.clone(); + Self { + core_data: core, + variant_data: CreatureVariantData { + variant: CreatureVariant::Base, + level, + archive_link, + }, + extra_data: None, + combat_data: None, + spell_caster_data: None, + } + } + + pub fn from_core_with_variant( + mut core: CreatureCoreData, + creature_variant: &CreatureVariant, + ) -> Creature { + let (hp, variant_archive_link) = creature_variant.get_variant_hp_and_link(&core); + let base_level = core.essential.level; + let level_delta = creature_variant.to_level_delta(); + core.essential.hp = hp; + Self { + core_data: core, + variant_data: CreatureVariantData { + variant: creature_variant.clone(), + level: base_level + level_delta, + archive_link: variant_archive_link, + }, + extra_data: None, + combat_data: None, + spell_caster_data: None, + } + } pub fn is_passing_filters(&self, filters: &FieldFilters) -> bool { Creature::check_creature_pass_equality_filters(self, filters) && Creature::check_creature_pass_greater_filters(self, filters) @@ -51,7 +71,7 @@ impl Creature { fn check_creature_pass_greater_filters(&self, filters: &FieldFilters) -> bool { let hp_pass = filters .max_hp_filter - .map_or(true, |max_hp| self.core_data.hp <= max_hp); + .map_or(true, |max_hp| self.core_data.essential.hp <= max_hp); let level_pass = filters .max_level_filter @@ -63,7 +83,7 @@ impl Creature { fn check_creature_pass_lesser_filters(&self, filters: &FieldFilters) -> bool { let hp_pass = filters .min_hp_filter - .map_or(true, |min_hp| self.core_data.hp >= min_hp); + .map_or(true, |min_hp| self.core_data.essential.hp >= min_hp); let level_pass = filters .min_level_filter @@ -76,37 +96,52 @@ impl Creature { let rarity_pass = filters .rarity_filter .as_ref() - .map_or(true, |rarity| self.core_data.rarity == *rarity); + .map_or(true, |rarity| self.core_data.essential.rarity == *rarity); let size_pass = filters .size_filter .as_ref() - .map_or(true, |size| self.core_data.size == *size); + .map_or(true, |size| self.core_data.essential.size == *size); let alignment_pass = filters .alignment_filter .as_ref() .map_or(true, |alignment| self.core_data.alignment == *alignment); let is_melee_pass = filters .is_melee_filter - .map_or(true, |is_melee| self.core_data.is_melee == is_melee); - let is_ranged_pass = filters - .is_ranged_filter - .map_or(true, |is_ranged| self.core_data.is_ranged == is_ranged); + .map_or(true, |is_melee| self.core_data.derived.is_melee == is_melee); + let is_ranged_pass = filters.is_ranged_filter.map_or(true, |is_ranged| { + self.core_data.derived.is_ranged == is_ranged + }); let is_spell_caster_pass = filters .is_spell_caster_filter .map_or(true, |is_spell_caster| { - self.core_data.is_spell_caster == is_spell_caster + self.core_data.derived.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)) - }) - }); + .map_or(true, |cr_type| self.core_data.essential.cr_type == *cr_type); + + let role_pass = filters.role_threshold.is_none() + || filters.role_filter.as_ref().map_or(true, |cr_role| { + let t = filters.role_threshold.unwrap_or(0); + match cr_role { + CreatureRoleEnum::Brute => self.core_data.derived.brute_percentage >= t, + CreatureRoleEnum::MagicalStriker => { + self.core_data.derived.magical_striker_percentage >= t + } + CreatureRoleEnum::SkillParagon => { + self.core_data.derived.skill_paragon_percentage >= t + } + CreatureRoleEnum::Skirmisher => { + self.core_data.derived.skirmisher_percentage >= t + } + CreatureRoleEnum::Sniper => self.core_data.derived.sniper_percentage >= t, + CreatureRoleEnum::Soldier => self.core_data.derived.soldier_percentage >= t, + CreatureRoleEnum::SpellCaster => { + self.core_data.derived.spell_caster_percentage >= t + } + } + }); rarity_pass && size_pass @@ -120,126 +155,16 @@ impl Creature { fn check_creature_pass_string_filters(&self, filters: &FieldFilters) -> bool { let name_pass: bool = filters.name_filter.as_ref().map_or(true, |name| { - self.core_data.name.to_lowercase().contains(name) + self.core_data.essential.name.to_lowercase().contains(name) }); let family_pass: bool = filters.family_filter.as_ref().map_or(true, |name| { self.core_data + .essential .family - .as_ref() - .unwrap_or(&"".to_string()) .to_lowercase() .contains(name) }); name_pass && family_pass } } - -impl - From<( - RawCreature, - Vec, - Vec, - Vec, - Vec, - Vec, - Vec, - Vec, - Vec, - Vec, - Vec, - Vec, - &CreatureScales, - &Regex, - )> for Creature -{ - fn from( - tuple: ( - RawCreature, - 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 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 = 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: 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 index db41597..36553be 100644 --- a/src/models/creature_component/creature_combat.rs +++ b/src/models/creature_component/creature_combat.rs @@ -1,7 +1,3 @@ -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; @@ -9,9 +5,9 @@ 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: i64, + pub reflex: i64, + pub will: i64, pub fortitude_detail: Option, pub reflex_detail: Option, pub will_detail: Option, @@ -26,64 +22,3 @@ pub struct CreatureCombatData { 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 index 8377d33..ee54d8a 100644 --- a/src/models/creature_component/creature_core.rs +++ b/src/models/creature_component/creature_core.rs @@ -1,70 +1,86 @@ -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 sqlx::sqlite::SqliteRow; +use sqlx::{Error, FromRow, Row}; 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 essential: EssentialData, + pub derived: DerivedData, + pub traits: Vec, pub alignment: AlignmentEnum, +} +#[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq, FromRow)] +pub struct EssentialData { + pub id: i64, + pub aon_id: Option, + pub name: String, + pub hp: i64, + pub level: i64, pub size: SizeEnum, - pub family: Option, + pub family: String, pub rarity: RarityEnum, + pub license: String, + pub remaster: bool, + pub source: String, + pub cr_type: CreatureTypeEnum, +} + +#[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq, FromRow)] +pub struct DerivedData { + pub archive_link: Option, + 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; + pub brute_percentage: i64, + pub magical_striker_percentage: i64, + pub skill_paragon_percentage: i64, + pub skirmisher_percentage: i64, + pub sniper_percentage: i64, + pub soldier_percentage: i64, + pub spell_caster_percentage: i64, +} - 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, +impl<'r> FromRow<'r, SqliteRow> for CreatureCoreData { + fn from_row(row: &'r SqliteRow) -> Result { + let rarity: String = row.try_get("rarity")?; + let size: String = row.try_get("size")?; + Ok(CreatureCoreData { + essential: EssentialData { + id: row.try_get("id")?, + aon_id: row.try_get("aon_id").ok(), + name: row.try_get("name")?, + hp: row.try_get("hp")?, + level: row.try_get("level")?, + size: SizeEnum::from(size), + family: row.try_get("family").unwrap_or(String::from("-")), + rarity: RarityEnum::from(rarity), + license: row.try_get("license")?, + remaster: row.try_get("remaster")?, + source: row.try_get("source")?, + cr_type: CreatureTypeEnum::from(row.try_get("cr_type").ok()), + }, + derived: DerivedData { + archive_link: row.try_get("archive_link").ok(), + is_melee: row.try_get("is_melee")?, + is_ranged: row.try_get("is_ranged")?, + is_spell_caster: row.try_get("is_spell_caster")?, + brute_percentage: row.try_get("brute_percentage")?, + magical_striker_percentage: row.try_get("magical_striker_percentage")?, + skill_paragon_percentage: row.try_get("skill_paragon_percentage")?, + skirmisher_percentage: row.try_get("skirmisher_percentage")?, + sniper_percentage: row.try_get("sniper_percentage")?, + soldier_percentage: row.try_get("soldier_percentage")?, + spell_caster_percentage: row.try_get("spell_caster_percentage")?, }, - 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, - } + traits: vec![], + alignment: Default::default(), + }) } } diff --git a/src/models/creature_component/creature_extra.rs b/src/models/creature_component/creature_extra.rs index 3d2b059..11f393f 100644 --- a/src/models/creature_component/creature_extra.rs +++ b/src/models/creature_component/creature_extra.rs @@ -1,7 +1,3 @@ -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}; @@ -10,12 +6,12 @@ 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, + pub charisma: i64, + pub constitution: i64, + pub dexterity: i64, + pub intelligence: i64, + pub strength: i64, + pub wisdom: i64, } #[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] @@ -32,71 +28,3 @@ pub struct CreatureExtraData { 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 deleted file mode 100644 index 6c3397a..0000000 --- a/src/models/creature_component/creature_info.rs +++ /dev/null @@ -1,45 +0,0 @@ -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 index bca6ebc..fa19b87 100644 --- a/src/models/creature_component/creature_spell_caster.rs +++ b/src/models/creature_component/creature_spell_caster.rs @@ -1,4 +1,3 @@ -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}; @@ -9,20 +8,3 @@ 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 index 88af048..e44ca4c 100644 --- a/src/models/creature_component/creature_variant.rs +++ b/src/models/creature_component/creature_variant.rs @@ -1,17 +1,10 @@ +use crate::models::creature_metadata::variant_enum::CreatureVariant; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; #[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] pub struct CreatureVariantData { - pub level: i8, + pub variant: CreatureVariant, + pub level: i64, 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/fields_unique_values_struct.rs b/src/models/creature_component/fields_unique_values_struct.rs new file mode 100644 index 0000000..68f3466 --- /dev/null +++ b/src/models/creature_component/fields_unique_values_struct.rs @@ -0,0 +1,9 @@ +#[derive(Default, Eq, PartialEq, Clone)] +pub struct FieldsUniqueValuesStruct { + pub list_of_levels: Vec, + pub list_of_families: Vec, + pub list_of_traits: Vec, + pub list_of_sources: Vec, + pub list_of_sizes: Vec, + pub list_of_rarities: Vec, +} diff --git a/src/models/creature_component/mod.rs b/src/models/creature_component/mod.rs index 9d842de..b3b1284 100644 --- a/src/models/creature_component/mod.rs +++ b/src/models/creature_component/mod.rs @@ -1,7 +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 fields_unique_values_struct; pub mod filter_struct; diff --git a/src/models/creature_filter_enum.rs b/src/models/creature_filter_enum.rs index 2c5c773..9370348 100644 --- a/src/models/creature_filter_enum.rs +++ b/src/models/creature_filter_enum.rs @@ -1,11 +1,10 @@ use serde::{Deserialize, Serialize}; +use std::fmt; #[derive(Serialize, Deserialize, Eq, PartialEq, Hash)] pub enum CreatureFilter { - Id, Level, Family, - Alignment, Size, Rarity, Melee, @@ -15,3 +14,40 @@ pub enum CreatureFilter { CreatureTypes, CreatureRoles, } + +impl fmt::Display for CreatureFilter { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + CreatureFilter::Level => { + write!(f, "level") + } + CreatureFilter::Family => { + write!(f, "family") + } + CreatureFilter::Size => { + write!(f, "size") + } + CreatureFilter::Rarity => { + write!(f, "rarity") + } + CreatureFilter::Melee => { + write!(f, "is_melee") + } + CreatureFilter::Ranged => { + write!(f, "is_ranged") + } + CreatureFilter::SpellCaster => { + write!(f, "is_spell_caster") + } + CreatureFilter::Traits => { + write!(f, "traits") + } + CreatureFilter::CreatureTypes => { + write!(f, "cr_type") + } + CreatureFilter::CreatureRoles => { + write!(f, "CREATUREROLES") + } + } + } +} diff --git a/src/models/creature_metadata/alignment_enum.rs b/src/models/creature_metadata/alignment_enum.rs index afaf784..8495492 100644 --- a/src/models/creature_metadata/alignment_enum.rs +++ b/src/models/creature_metadata/alignment_enum.rs @@ -1,11 +1,21 @@ use crate::models::db::raw_trait::RawTrait; use serde::{Deserialize, Serialize}; use std::str::FromStr; -use strum::Display; +use strum::{Display, EnumIter}; use utoipa::ToSchema; #[derive( - Serialize, Deserialize, ToSchema, Display, Eq, Hash, PartialEq, Ord, PartialOrd, Default, + Serialize, + Deserialize, + ToSchema, + Display, + Eq, + Hash, + PartialEq, + Ord, + PartialOrd, + Default, + EnumIter, )] pub enum AlignmentEnum { #[strum(to_string = "CE")] @@ -44,16 +54,23 @@ pub enum AlignmentEnum { Any, // can be every alignment } +pub const ALIGNMENT_TRAITS: [&str; 4] = ["GOOD", "EVIL", "CHAOTIC", "LAWFUL"]; + impl From<(&Vec, bool)> for AlignmentEnum { fn from(tuple: (&Vec, bool)) -> AlignmentEnum { + // If remaster then it's always no alignment if tuple.1 { return AlignmentEnum::No; } - let string_traits: Vec = tuple.0.iter().map(|x| x.name.clone()).collect(); - let is_good = string_traits.contains(&"good".to_string()); - let is_evil = string_traits.contains(&"evil".to_string()); - let is_chaos = string_traits.contains(&"chaotic".to_string()); - let is_lawful = string_traits.contains(&"lawful".to_string()); + let string_traits: Vec = tuple + .0 + .iter() + .map(|x| x.name.clone().to_uppercase()) + .collect(); + let is_good = string_traits.contains(&"GOOD".to_string()); + let is_evil = string_traits.contains(&"EVIL".to_string()); + let is_chaos = string_traits.contains(&"CHAOTIC".to_string()); + let is_lawful = string_traits.contains(&"LAWFUL".to_string()); if is_good { if is_chaos { return AlignmentEnum::Cg; @@ -101,6 +118,43 @@ impl FromStr for AlignmentEnum { } } +impl AlignmentEnum { + pub fn to_traits(&self) -> Vec { + match self { + AlignmentEnum::Ce => { + vec![String::from("CHAOTIC"), String::from("EVIL")] + } + AlignmentEnum::Cn => { + vec![String::from("CHAOTIC"), String::from("NEUTRAL")] + } + AlignmentEnum::Cg => { + vec![String::from("CHAOTIC"), String::from("GOOD")] + } + AlignmentEnum::Ne => { + vec![String::from("NEUTRAL"), String::from("EVIL")] + } + AlignmentEnum::N => { + vec![String::from("NEUTRAL")] + } + AlignmentEnum::Ng => { + vec![String::from("NEUTRAL"), String::from("GOOD")] + } + AlignmentEnum::Le => { + vec![String::from("LAWFUL"), String::from("EVIL")] + } + AlignmentEnum::Ln => { + vec![String::from("LAWFUL"), String::from("NEUTRAL")] + } + AlignmentEnum::Lg => { + vec![String::from("LAWFUL"), String::from("GOOD")] + } + AlignmentEnum::No | AlignmentEnum::Any => { + vec![] + } + } + } +} + impl Clone for AlignmentEnum { fn clone(&self) -> AlignmentEnum { match self { diff --git a/src/models/creature_metadata/creature_role.rs b/src/models/creature_metadata/creature_role.rs index a896ee4..9aeea26 100644 --- a/src/models/creature_metadata/creature_role.rs +++ b/src/models/creature_metadata/creature_role.rs @@ -1,5 +1,5 @@ use crate::models::creature_component::creature_combat::CreatureCombatData; -use crate::models::creature_component::creature_core::CreatureCoreData; +use crate::models::creature_component::creature_core::EssentialData; 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; @@ -8,9 +8,9 @@ use regex::Regex; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::fmt; +use std::str::FromStr; use strum::{EnumIter, IntoEnumIterator}; use utoipa::ToSchema; -use validator::HasLen; const MISSING_FIELD_DISTANCE: u16 = 20; @@ -30,24 +30,35 @@ pub enum CreatureRoleEnum { } impl CreatureRoleEnum { + pub fn to_db_column(&self) -> String { + match self { + CreatureRoleEnum::Brute => String::from("brute_percentage"), + CreatureRoleEnum::MagicalStriker => String::from("magical_striker_percentage"), + CreatureRoleEnum::SkillParagon => String::from("skill_paragon_percentage"), + CreatureRoleEnum::Skirmisher => String::from("skirmisher_percentage"), + CreatureRoleEnum::Sniper => String::from("sniper_percentage"), + CreatureRoleEnum::Soldier => String::from("soldier_percentage"), + CreatureRoleEnum::SpellCaster => String::from("spell_caster_percentage"), + } + } pub fn from_creature_with_given_scales( - cr_core: &CreatureCoreData, + cr_core: &EssentialData, cr_extra: &CreatureExtraData, cr_combat: &CreatureCombatData, cr_spells: &CreatureSpellCasterData, scales: &CreatureScales, - dmg_scales_regex: &Regex, ) -> BTreeMap { + let dmg_scales_regex = Regex::new(r"\((\d+)\)").unwrap(); let mut roles = BTreeMap::new(); roles.insert( Self::Brute, - is_brute(cr_core, cr_extra, cr_combat, scales, dmg_scales_regex) + 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) + is_magical_striker(cr_core, cr_spells, cr_combat, scales, &dmg_scales_regex) .map(|x| (x * 100.).round() as i64) .unwrap_or(0), ); @@ -65,13 +76,13 @@ impl CreatureRoleEnum { ); roles.insert( Self::Sniper, - is_sniper(cr_core, cr_extra, cr_combat, scales, dmg_scales_regex) + 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) + is_soldier(cr_core, cr_extra, cr_combat, scales, &dmg_scales_regex) .map(|x| (x * 100.).round() as i64) .unwrap_or(0), ); @@ -90,55 +101,46 @@ impl CreatureRoleEnum { } // Brute fn is_brute( - cr_core: &CreatureCoreData, + cr_core: &EssentialData, cr_extra: &CreatureExtraData, cr_combat: &CreatureCombatData, scales: &CreatureScales, re: &Regex, ) -> Option { let mut score: u16 = 0; - let lvl = cr_core.base_level; + let lvl = cr_core.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); + score += calculate_lb_distance(ability_scales.high, cr_extra.ability_scores.strength); // high to moderate Con modifier, - let constitution = cr_extra.ability_scores.constitution as i64; + let constitution = cr_extra.ability_scores.constitution; 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, + cr_extra.ability_scores.intelligence + 1, ); + score += calculate_ub_distance(ability_scales.moderate, cr_extra.ability_scores.wisdom + 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, + cr_extra.ability_scores.charisma + 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, - ); + score += calculate_ub_distance(saving_scales.moderate, cr_combat.saving_throws.reflex + 1); // high Fortitude, - score += calculate_lb_distance(saving_scales.high, cr_combat.saving_throws.fortitude as i64); + score += calculate_lb_distance(saving_scales.high, cr_combat.saving_throws.fortitude); // Low will, - score += calculate_ub_distance( - saving_scales.moderate, - cr_combat.saving_throws.will as i64 + 1, - ); + score += calculate_ub_distance(saving_scales.moderate, cr_combat.saving_throws.will + 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); + score += calculate_lb_distance(hp_scales.high_lb, cr_core.hp); let atk_bonus_scales = scales.strike_bonus_scales.get(&lvl)?; let dmg_scales = scales.strike_dmg_scales.get(&lvl)?; @@ -178,14 +180,14 @@ fn is_brute( // Sniper fn is_sniper( - cr_core: &CreatureCoreData, + cr_core: &EssentialData, cr_extra: &CreatureExtraData, cr_combat: &CreatureCombatData, scales: &CreatureScales, re: &Regex, ) -> Option { let mut score: u16 = 0; - let lvl = cr_core.base_level; + let lvl = cr_core.level; let per_scales = scales.perception_scales.get(&lvl)?; // high Perception (chosen moderate // !!!This is a critical stat, upping it will half creature result!!! @@ -193,18 +195,12 @@ fn is_sniper( 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, - ); + score += calculate_lb_distance(ability_scales.moderate, cr_extra.ability_scores.dexterity); 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, - ); + score += calculate_lb_distance(saving_scales.moderate, cr_combat.saving_throws.reflex); // moderate to low HP; skipped let atk_bonus_scales = scales.strike_bonus_scales.get(&lvl)?; @@ -232,27 +228,24 @@ fn is_sniper( } // Skirmisher fn is_skirmisher( - cr_core: &CreatureCoreData, + cr_core: &EssentialData, cr_extra: &CreatureExtraData, cr_combat: &CreatureCombatData, scales: &CreatureScales, ) -> Option { let mut score: u16 = 0; - let lvl = cr_core.base_level; + let lvl = cr_core.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, - ); + score += calculate_lb_distance(ability_scales.high, cr_extra.ability_scores.dexterity); 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, + cr_combat.saving_throws.fortitude + 1, ); // high Reflex; - score += calculate_lb_distance(saving_scales.high, cr_combat.saving_throws.reflex as i64); + score += calculate_lb_distance(saving_scales.high, cr_combat.saving_throws.reflex); // Higher than avg speed (avg ~= 25) score += cr_extra .speeds @@ -264,23 +257,23 @@ fn is_skirmisher( } // Soldier pub fn is_soldier( - cr_core: &CreatureCoreData, + cr_core: &EssentialData, cr_extra: &CreatureExtraData, cr_combat: &CreatureCombatData, scales: &CreatureScales, re: &Regex, ) -> Option { let mut score: u16 = 0; - let lvl = cr_core.base_level; + let lvl = cr_core.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); + score += calculate_lb_distance(ability_scales.high, cr_extra.ability_scores.strength); 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); + score += calculate_lb_distance(saving_scales.high, cr_combat.saving_throws.fortitude); let atk_bonus_scales = scales.strike_bonus_scales.get(&lvl)?; let dmg_scales = scales.strike_dmg_scales.get(&lvl)?; let scales_high_avg = re @@ -319,14 +312,14 @@ pub fn is_soldier( // Magical Striker pub fn is_magical_striker( - cr_core: &CreatureCoreData, + cr_core: &EssentialData, cr_spell: &CreatureSpellCasterData, cr_combat: &CreatureCombatData, scales: &CreatureScales, re: &Regex, ) -> Option { let mut score: u16 = 0; - let lvl = cr_core.base_level; + let lvl = cr_core.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 @@ -351,28 +344,27 @@ pub fn is_magical_striker( 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, + cr_spell.spell_caster_entry.spell_casting_dc_mod.unwrap(), ); } 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; + if (cr_spell.spells.len() as f64) < (cr_core.level as f64 / 2.).ceil() - 1. { + score += (((cr_core.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_core: &EssentialData, cr_extra: &CreatureExtraData, cr_combat: &CreatureCombatData, scales: &CreatureScales, ) -> Option { let mut score: u16 = 0; - let lvl = cr_core.base_level; + let lvl = cr_core.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()?; @@ -382,11 +374,11 @@ fn is_skill_paragon( // typically high Reflex or Will and low Fortitude; score += calculate_ub_distance( saving_scales.moderate, - cr_combat.saving_throws.fortitude as i64 + 1, + cr_combat.saving_throws.fortitude + 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); + let ref_dist = calculate_lb_distance(saving_scales.high, cr_combat.saving_throws.reflex); + let will_dist = calculate_lb_distance(saving_scales.high, cr_combat.saving_throws.will); score += if ref_dist > will_dist { will_dist } else { @@ -395,14 +387,14 @@ fn is_skill_paragon( // 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; + let cr_skill_amount = cr_extra.skills.len() as i64 * 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) + - cr_skill_amount) .unsigned_abs() as u16; // at least two special ability to use the creature's skills in combat if cr_extra @@ -421,47 +413,65 @@ fn is_skill_paragon( } // Spellcaster fn is_spellcaster( - cr_core: &CreatureCoreData, + cr_core: &EssentialData, cr_spell: &CreatureSpellCasterData, cr_combat: &CreatureCombatData, cr_extra: &CreatureExtraData, scales: &CreatureScales, ) -> Option { let mut score: u16 = 0; - let lvl = cr_core.base_level; + let lvl = cr_core.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, + cr_combat.saving_throws.fortitude + 1, ); // high Will; - score += calculate_lb_distance(saving_scales.high, cr_combat.saving_throws.will as i64); + score += calculate_lb_distance(saving_scales.high, cr_combat.saving_throws.will); // low HP; let hp_scales = scales.hp_scales.get(&lvl)?; - score += calculate_ub_distance(hp_scales.high_lb, cr_core.hp as i64 + 1); + score += calculate_ub_distance(hp_scales.high_lb, cr_core.hp + 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, + cr_spell.spell_caster_entry.spell_casting_dc_mod?, ); // 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)) + if (cr_spell.spells.len() as f64) < (cr_core.level as f64 / 2.).ceil() { + score += (((cr_core.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); + let best_mental_ability = cr_extra + .ability_scores + .wisdom + .max(cr_extra.ability_scores.intelligence) + .max(cr_extra.ability_scores.charisma); score += calculate_lb_distance(ability_scales.high, best_mental_ability); Some(f64::E().powf(-0.2 * (score as f64))) } +impl FromStr for CreatureRoleEnum { + type Err = (); + fn from_str(s: &str) -> Result { + match s.to_uppercase().as_str() { + "BRUTE" => Ok(CreatureRoleEnum::Brute), + "MAGICAL STRIKER" | "MAGICALSTRIKER" => Ok(CreatureRoleEnum::MagicalStriker), + "SKILL PARAGON" | "SKILLPARAGON" => Ok(CreatureRoleEnum::SkillParagon), + "SKIRMISHER" => Ok(CreatureRoleEnum::Skirmisher), + "SNIPER" => Ok(CreatureRoleEnum::Sniper), + "SOLDIER" => Ok(CreatureRoleEnum::Soldier), + "SPELLCASTER" | "SPELL CASTER" => Ok(CreatureRoleEnum::SpellCaster), + _ => Err(()), + } + } +} + impl fmt::Display for CreatureRoleEnum { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { diff --git a/src/models/creature_metadata/rarity_enum.rs b/src/models/creature_metadata/rarity_enum.rs index be71863..0841261 100644 --- a/src/models/creature_metadata/rarity_enum.rs +++ b/src/models/creature_metadata/rarity_enum.rs @@ -1,10 +1,11 @@ use serde::{Deserialize, Serialize}; +use sqlx::Type; use std::str::FromStr; use strum::Display; use utoipa::ToSchema; #[derive( - Serialize, Deserialize, ToSchema, Display, Eq, Hash, PartialEq, Ord, PartialOrd, Default, + Serialize, Deserialize, ToSchema, Display, Eq, Hash, PartialEq, Ord, PartialOrd, Default, Type, )] pub enum RarityEnum { #[default] diff --git a/src/models/creature_metadata/size_enum.rs b/src/models/creature_metadata/size_enum.rs index 830c24d..f8a255a 100644 --- a/src/models/creature_metadata/size_enum.rs +++ b/src/models/creature_metadata/size_enum.rs @@ -1,10 +1,11 @@ use serde::{Deserialize, Serialize}; +use sqlx::Type; use std::str::FromStr; use strum::Display; use utoipa::ToSchema; #[derive( - Serialize, Deserialize, ToSchema, Display, Eq, Hash, PartialEq, Ord, PartialOrd, Default, + Serialize, Deserialize, ToSchema, Display, Eq, Hash, PartialEq, Ord, PartialOrd, Default, Type, )] pub enum SizeEnum { #[serde(alias = "tiny", alias = "TINY")] diff --git a/src/models/creature_metadata/type_enum.rs b/src/models/creature_metadata/type_enum.rs index caeb3af..f778fed 100644 --- a/src/models/creature_metadata/type_enum.rs +++ b/src/models/creature_metadata/type_enum.rs @@ -1,16 +1,26 @@ use serde::{Deserialize, Serialize}; +use sqlx::Type; use std::str::FromStr; -use strum::Display; +use strum::{Display, EnumIter}; use utoipa::ToSchema; #[derive( - Serialize, Deserialize, ToSchema, Display, Eq, Hash, PartialEq, Ord, PartialOrd, Default, + Serialize, + Deserialize, + ToSchema, + Display, + Eq, + Hash, + PartialEq, + Ord, + PartialOrd, + Default, + Type, + EnumIter, )] pub enum CreatureTypeEnum { #[default] #[serde(alias = "monster", alias = "MONSTER")] - #[strum(to_string = "Monster")] - #[serde(rename = "Monster")] Monster, #[serde(alias = "npc", alias = "NPC")] #[strum(to_string = "NPC")] @@ -18,12 +28,24 @@ pub enum CreatureTypeEnum { Npc, } +impl From for CreatureTypeEnum { + fn from(value: String) -> Self { + CreatureTypeEnum::from_str(value.as_str()).unwrap_or_default() + } +} + impl From> for CreatureTypeEnum { fn from(value: Option) -> Self { CreatureTypeEnum::from_str(value.unwrap_or_default().as_str()).unwrap_or_default() } } +impl From<&Option> for CreatureTypeEnum { + fn from(value: &Option) -> Self { + CreatureTypeEnum::from_str(value.clone().unwrap_or_default().as_str()).unwrap_or_default() + } +} + impl FromStr for CreatureTypeEnum { type Err = (); fn from_str(s: &str) -> Result { @@ -43,12 +65,3 @@ impl Clone for CreatureTypeEnum { } } } - -impl CreatureTypeEnum { - pub fn to_url_string(&self) -> &str { - match self { - CreatureTypeEnum::Monster => "Monsters", - CreatureTypeEnum::Npc => "NPCs", - } - } -} diff --git a/src/models/creature_metadata/variant_enum.rs b/src/models/creature_metadata/variant_enum.rs index 8f94a2f..de8bf7a 100644 --- a/src/models/creature_metadata/variant_enum.rs +++ b/src/models/creature_metadata/variant_enum.rs @@ -1,4 +1,7 @@ +use crate::models::creature_component::creature_core::CreatureCoreData; +use crate::services::url_calculator::add_boolean_query; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use strum::Display; use utoipa::ToSchema; @@ -23,7 +26,7 @@ impl Clone for CreatureVariant { } impl CreatureVariant { - pub fn to_level_delta(&self) -> i8 { + pub fn to_level_delta(&self) -> i64 { match self { CreatureVariant::Weak => -1, CreatureVariant::Elite => 1, @@ -31,11 +34,34 @@ impl CreatureVariant { } } - pub fn to_cache_index(&self) -> i32 { + pub fn get_variant_hp_and_link(&self, core: &CreatureCoreData) -> (i64, Option) { + let mut core_hp = core.essential.hp; match self { - CreatureVariant::Base => 0, - CreatureVariant::Weak => 1, - CreatureVariant::Elite => 2, + CreatureVariant::Base => (core_hp, core.derived.archive_link.clone()), + _ => { + let level = core.essential.level; + let level_delta = self.to_level_delta(); + let archive_link = core.derived.archive_link.clone(); + let variant_archive_link = match self { + CreatureVariant::Base => archive_link, + _ => add_boolean_query(&archive_link, &self.to_string(), true), + }; + + let hp_increase = hp_increase_by_level(); + let desired_key = hp_increase + .keys() + .filter(|lvl| level >= **lvl) + .max() + .unwrap_or(hp_increase.keys().next().unwrap_or(&0)); + core_hp += *hp_increase.get(desired_key).unwrap_or(&0) * level_delta; + core_hp = core_hp.max(1); + + (core_hp, variant_archive_link) + } } } } + +fn hp_increase_by_level() -> HashMap { + hashmap! { 1 => 10, 2=> 15, 5=> 20, 20=> 30 } +} diff --git a/src/models/db/mod.rs b/src/models/db/mod.rs index 6ce16de..a763587 100644 --- a/src/models/db/mod.rs +++ b/src/models/db/mod.rs @@ -1,4 +1,3 @@ -pub mod raw_creature; pub mod raw_immunity; pub mod raw_language; pub mod raw_resistance; diff --git a/src/models/db/raw_creature.rs b/src/models/db/raw_creature.rs deleted file mode 100644 index 681045f..0000000 --- a/src/models/db/raw_creature.rs +++ /dev/null @@ -1,47 +0,0 @@ -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 sqlx::FromRow; - -#[derive(Serialize, Deserialize, FromRow, Clone)] -pub struct RawCreature { - pub id: i64, - pub aon_id: Option, - pub name: String, - pub charisma: i64, - pub constitution: i64, - pub dexterity: i64, - pub intelligence: i64, - pub strength: i64, - pub wisdom: i64, - pub ac: i64, - pub hp: i64, - pub hp_detail: String, - pub ac_detail: String, - pub language_detail: Option, - pub level: i64, - pub license: String, - pub remaster: bool, - pub source: String, - pub initiative_ability: String, - pub perception: i64, - pub perception_detail: String, - pub fortitude: i64, - pub reflex: i64, - pub will: i64, - pub fortitude_detail: String, - pub reflex_detail: String, - pub will_detail: String, - pub rarity: RarityEnum, - pub size: SizeEnum, - pub cr_type: CreatureTypeEnum, - pub family: Option, - - 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 a39cb76..7bcb0ba 100644 --- a/src/models/encounter_structs.rs +++ b/src/models/encounter_structs.rs @@ -3,7 +3,6 @@ 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 crate::models::response_data::ResponseData; use rand::distributions::{Distribution, Standard}; use rand::Rng; use serde::{Deserialize, Serialize}; @@ -14,9 +13,9 @@ use validator::Validate; #[derive(Serialize, Deserialize, ToSchema, Validate)] pub struct EncounterParams { #[validate(length(min = 1))] - pub party_levels: Vec, + pub party_levels: Vec, #[validate(length(min = 1))] - pub enemy_levels: Vec, + pub enemy_levels: Vec, pub is_pwl_on: bool, } @@ -33,11 +32,10 @@ pub struct RandomEncounterData { pub min_creatures: Option, pub max_creatures: Option, #[validate(length(min = 1))] - pub party_levels: Vec, + pub party_levels: Vec, pub allow_elite_variants: Option, pub allow_weak_variants: Option, pub is_pwl_on: bool, - pub response_data: ResponseData, } #[derive( @@ -70,6 +68,6 @@ impl Distribution for Standard { } pub struct ExpRange { - pub lower_bound: i16, - pub upper_bound: i16, + pub lower_bound: i64, + pub upper_bound: i64, } diff --git a/src/models/items/spell_caster_entry.rs b/src/models/items/spell_caster_entry.rs index d17fdbc..a9abf21 100644 --- a/src/models/items/spell_caster_entry.rs +++ b/src/models/items/spell_caster_entry.rs @@ -6,7 +6,7 @@ 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_dc_mod: Option, + pub spell_casting_atk_mod: Option, pub spell_casting_tradition: Option, } diff --git a/src/models/response_data.rs b/src/models/response_data.rs index f76b8bf..deff73d 100644 --- a/src/models/response_data.rs +++ b/src/models/response_data.rs @@ -2,7 +2,6 @@ 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}; @@ -10,9 +9,7 @@ use utoipa::{IntoParams, ToSchema}; use validator::Validate; #[derive(Serialize, Deserialize, IntoParams, Default, Eq, PartialEq, Hash, Clone, Validate)] -pub struct ResponseData { - pub core_data: Option, - pub variant_data: Option, +pub struct OptionalData { pub extra_data: Option, pub combat_data: Option, pub spell_casting_data: Option, @@ -20,50 +17,22 @@ pub struct ResponseData { #[derive(Serialize, Deserialize, Clone, ToSchema, Hash, PartialEq)] pub struct ResponseCreature { - pub core_data: Option, - pub variant_data: Option, + pub core_data: CreatureCoreData, + pub variant_data: CreatureVariantData, pub extra_data: Option, pub combat_data: Option, pub spell_caster_data: Option, - pub info: Option, } -impl From<(Creature, &ResponseData)> for ResponseCreature { - fn from(value: (Creature, &ResponseData)) -> Self { - let cr = value.0; - let rd = value.1; +impl From for ResponseCreature { + fn from(value: Creature) -> Self { + let cr = value; Self { - core_data: if rd.core_data.is_none() || !rd.core_data.unwrap() { - None - } else { - Some(cr.core_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) - }, - 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) - }, + core_data: cr.core_data, + variant_data: cr.variant_data, + extra_data: cr.extra_data, + spell_caster_data: cr.spell_caster_data, + combat_data: cr.combat_data, } } } diff --git a/src/models/routers_validator_structs.rs b/src/models/routers_validator_structs.rs index 7ad2594..c89712d 100644 --- a/src/models/routers_validator_structs.rs +++ b/src/models/routers_validator_structs.rs @@ -16,17 +16,17 @@ pub struct FieldFilters { 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, - pub max_level_filter: Option, + pub role_threshold: Option, + pub min_hp_filter: Option, + pub max_hp_filter: Option, + pub min_level_filter: Option, + pub max_level_filter: Option, pub is_melee_filter: Option, pub is_ranged_filter: Option, pub is_spell_caster_filter: Option, } -#[derive(Serialize, Deserialize, IntoParams, Validate)] +#[derive(Serialize, Deserialize, IntoParams, Validate, Eq, PartialEq, Hash)] pub struct PaginatedRequest { pub cursor: u32, #[validate(range(min = -1, max = 100))] diff --git a/src/models/scales_struct/creature_scales.rs b/src/models/scales_struct/creature_scales.rs index 344b31a..70f6010 100644 --- a/src/models/scales_struct/creature_scales.rs +++ b/src/models/scales_struct/creature_scales.rs @@ -14,16 +14,16 @@ 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 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, + 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/routes/bestiary.rs b/src/routes/bestiary.rs index fd6f8e4..bf40d19 100644 --- a/src/routes/bestiary.rs +++ b/src/routes/bestiary.rs @@ -4,15 +4,16 @@ 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::response_data::OptionalData; use crate::models::response_data::ResponseCreature; -use crate::models::response_data::ResponseData; 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_core::DerivedData; +use crate::models::creature_component::creature_core::EssentialData; 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; @@ -22,8 +23,6 @@ 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; @@ -76,15 +75,15 @@ pub fn init_docs(doc: &mut utoipa::openapi::OpenApi) { CreatureTypeEnum, CreatureVariant, CreatureCoreData, + EssentialData, + DerivedData, CreatureVariantData, CreatureExtraData, CreatureCombatData, CreatureSpellCasterData, - CreatureInfo, Spell, Weapon, SavingThrows, - PublicationInfo, AbilityScores, Action, Skill, @@ -102,7 +101,7 @@ pub fn init_docs(doc: &mut utoipa::openapi::OpenApi) { path = "/bestiary/list", tag = "bestiary", params( - FieldFilters, PaginatedRequest, ResponseData + FieldFilters, PaginatedRequest ), responses( (status=200, description = "Successful Response", body = BestiaryResponse), @@ -114,10 +113,9 @@ pub async fn get_bestiary( data: web::Data, filters: Query, pagination: Query, - response_data: Query, ) -> Result { Ok(web::Json( - bestiary_service::get_bestiary(&data, &filters.0, &pagination.0, &response_data.0).await, + bestiary_service::get_bestiary(&data, &filters.0, &pagination.0).await, )) } @@ -267,7 +265,7 @@ pub async fn get_creature_roles_list() -> Result { tag = "bestiary", params( ("creature_id" = String, Path, description = "id of the creature to fetch"), - ResponseData, + OptionalData, ), responses( (status=200, description = "Successful Response", body = ResponseCreature), @@ -278,10 +276,10 @@ pub async fn get_creature_roles_list() -> Result { pub async fn get_creature( data: web::Data, creature_id: web::Path, - response_data: Query, + optional_data: Query, ) -> Result { Ok(web::Json( - bestiary_service::get_creature(&data, sanitize_id(&creature_id)?, &response_data.0).await, + bestiary_service::get_creature(&data, sanitize_id(&creature_id)?, &optional_data.0).await, )) } @@ -291,7 +289,7 @@ pub async fn get_creature( tag = "bestiary", params( ("creature_id" = String, Path, description = "id of the creature to fetch"), - ResponseData + OptionalData ), responses( (status=200, description = "Successful Response", body = ResponseCreature), @@ -302,7 +300,7 @@ pub async fn get_creature( pub async fn get_elite_creature( data: web::Data, creature_id: web::Path, - response_data: Query, + response_data: Query, ) -> Result { Ok(web::Json( bestiary_service::get_elite_creature(&data, sanitize_id(&creature_id)?, &response_data.0) @@ -316,7 +314,7 @@ pub async fn get_elite_creature( tag = "bestiary", params( ("creature_id" = String, Path, description = "id of the creature to fetch"), - ResponseData, + OptionalData, ), responses( (status=200, description = "Successful Response", body = ResponseCreature), @@ -327,7 +325,7 @@ pub async fn get_elite_creature( pub async fn get_weak_creature( data: web::Data, creature_id: web::Path, - response_data: Query, + response_data: Query, ) -> Result { Ok(web::Json( bestiary_service::get_weak_creature(&data, sanitize_id(&creature_id)?, &response_data.0) @@ -335,8 +333,8 @@ pub async fn get_weak_creature( )) } -fn sanitize_id(creature_id: &str) -> Result { - let id = creature_id.parse::(); +fn sanitize_id(creature_id: &str) -> Result { + let id = creature_id.parse::(); match id { Ok(s) => Ok(s), Err(e) => Err(error::ErrorNotFound(e)), diff --git a/src/services/bestiary_service.rs b/src/services/bestiary_service.rs index 6d981f8..90b49a7 100644 --- a/src/services/bestiary_service.rs +++ b/src/services/bestiary_service.rs @@ -1,9 +1,9 @@ -use crate::db::db_proxy; +use crate::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::response_data::{OptionalData, ResponseCreature}; use crate::models::routers_validator_structs::{FieldFilters, PaginatedRequest}; use crate::services::url_calculator::next_url_calculator; use crate::AppState; @@ -21,34 +21,34 @@ pub struct BestiaryResponse { pub async fn get_creature( app_state: &AppState, - id: i32, - response_data: &ResponseData, + id: i64, + optional_data: &OptionalData, ) -> HashMap> { hashmap! { String::from("results") => - db_proxy::get_creature_by_id(app_state, id, CreatureVariant::Base).await.map(|x| ResponseCreature::from((x, response_data))) + proxy::get_creature_by_id(app_state, id, &CreatureVariant::Base, optional_data).await.map(ResponseCreature::from) } } pub async fn get_elite_creature( app_state: &AppState, - id: i32, - response_data: &ResponseData, + id: i64, + optional_data: &OptionalData, ) -> HashMap> { hashmap! { String::from("results") => - db_proxy::get_elite_creature_by_id(app_state, id).await.map(|x| ResponseCreature::from((x, response_data))) + proxy::get_elite_creature_by_id(app_state, id, optional_data).await.map(ResponseCreature::from) } } pub async fn get_weak_creature( app_state: &AppState, - id: i32, - response_data: &ResponseData, + id: i64, + optional_data: &OptionalData, ) -> HashMap> { hashmap! { String::from("results") => - db_proxy::get_weak_creature_by_id(app_state, id).await.map(|x| ResponseCreature::from((x, response_data))) + proxy::get_weak_creature_by_id(app_state, id, optional_data).await.map(ResponseCreature::from) } } @@ -56,42 +56,40 @@ pub async fn get_bestiary( app_state: &AppState, field_filter: &FieldFilters, pagination: &PaginatedRequest, - response_data: &ResponseData, ) -> BestiaryResponse { convert_result_to_bestiary_response( field_filter, pagination, - db_proxy::get_paginated_creatures(app_state, field_filter, pagination).await, - response_data, + proxy::get_paginated_creatures(app_state, field_filter, pagination).await, ) } pub async fn get_families_list(app_state: &AppState) -> Vec { - db_proxy::get_keys(app_state, CreatureField::Family).await + proxy::get_keys(app_state, CreatureField::Family).await } pub async fn get_traits_list(app_state: &AppState) -> Vec { - db_proxy::get_keys(app_state, CreatureField::Traits).await + proxy::get_keys(app_state, CreatureField::Traits).await } pub async fn get_sources_list(app_state: &AppState) -> Vec { - db_proxy::get_keys(app_state, CreatureField::Sources).await + proxy::get_keys(app_state, CreatureField::Sources).await } pub async fn get_rarities_list(app_state: &AppState) -> Vec { - db_proxy::get_keys(app_state, CreatureField::Rarity).await + proxy::get_keys(app_state, CreatureField::Rarity).await } pub async fn get_sizes_list(app_state: &AppState) -> Vec { - db_proxy::get_keys(app_state, CreatureField::Size).await + proxy::get_keys(app_state, CreatureField::Size).await } pub async fn get_alignments_list(app_state: &AppState) -> Vec { - db_proxy::get_keys(app_state, CreatureField::Alignment).await + proxy::get_keys(app_state, CreatureField::Alignment).await } pub async fn get_creature_types_list(app_state: &AppState) -> Vec { - db_proxy::get_keys(app_state, CreatureField::CreatureTypes).await + proxy::get_keys(app_state, CreatureField::CreatureTypes).await } pub async fn get_creature_roles_list() -> Vec { @@ -101,19 +99,13 @@ fn convert_result_to_bestiary_response( field_filters: &FieldFilters, pagination: &PaginatedRequest, result: Result<(u32, Vec)>, - response_data: &ResponseData, ) -> BestiaryResponse { match result { Ok(res) => { let cr: Vec = res.1; let cr_length = cr.len(); BestiaryResponse { - results: Some( - cr.into_iter() - .clone() - .map(|x| ResponseCreature::from((x, response_data))) - .collect(), - ), + results: Some(cr.into_iter().map(ResponseCreature::from).collect()), count: cr_length, next: if cr_length >= pagination.page_size as usize { Some(next_url_calculator(field_filters, pagination, res.0)) diff --git a/src/services/encounter_handler/difficulty_utilities.rs b/src/services/encounter_handler/difficulty_utilities.rs index 78f7c08..dab2df6 100644 --- a/src/services/encounter_handler/difficulty_utilities.rs +++ b/src/services/encounter_handler/difficulty_utilities.rs @@ -1,6 +1,6 @@ use crate::models::encounter_structs::{EncounterChallengeEnum, ExpRange}; -pub fn scale_difficulty_exp(base_difficulty: &EncounterChallengeEnum, party_size: i16) -> ExpRange { +pub fn scale_difficulty_exp(base_difficulty: &EncounterChallengeEnum, party_size: i64) -> ExpRange { // Given the base difficulty and the party size, it scales the base difficulty. // Useful when a party is not the canon 4 party member. let party_deviation = party_size - 4; @@ -17,7 +17,7 @@ pub fn scale_difficulty_exp(base_difficulty: &EncounterChallengeEnum, party_size } } -fn convert_difficulty_enum_to_base_xp_budget(diff: &EncounterChallengeEnum) -> i16 { +fn convert_difficulty_enum_to_base_xp_budget(diff: &EncounterChallengeEnum) -> i64 { match diff { EncounterChallengeEnum::Trivial => 40, EncounterChallengeEnum::Low => 60, @@ -28,7 +28,7 @@ fn convert_difficulty_enum_to_base_xp_budget(diff: &EncounterChallengeEnum) -> i } } -fn convert_difficulty_enum_to_xp_adjustment(diff: &EncounterChallengeEnum) -> i16 { +fn convert_difficulty_enum_to_xp_adjustment(diff: &EncounterChallengeEnum) -> i64 { match diff { EncounterChallengeEnum::Trivial => 10, EncounterChallengeEnum::Low => 15, diff --git a/src/services/encounter_handler/encounter_calculator.rs b/src/services/encounter_handler/encounter_calculator.rs index cc6007f..da256d9 100644 --- a/src/services/encounter_handler/encounter_calculator.rs +++ b/src/services/encounter_handler/encounter_calculator.rs @@ -4,16 +4,15 @@ use std::collections::{HashMap, HashSet}; use std::ops::Neg; // Used to explicitly tell about the iter trait use strum::IntoEnumIterator; -use validator::HasLen; -fn calculate_max_lvl_diff(lvl_and_exp_map: &HashMap) -> i16 { +fn calculate_max_lvl_diff(lvl_and_exp_map: &HashMap) -> i64 { match lvl_and_exp_map.keys().min() { None => panic!("No valid lvl and exp map was passed. Abort"), Some(max_lvl_diff) => *max_lvl_diff, } } -fn calculate_lvl_and_exp_map(is_pwl_on: bool) -> HashMap { +fn calculate_lvl_and_exp_map(is_pwl_on: bool) -> HashMap { // PWL stands for proficiency without level if is_pwl_on { hashmap! { @@ -48,16 +47,16 @@ fn calculate_lvl_and_exp_map(is_pwl_on: bool) -> HashMap { } } -pub fn calculate_encounter_exp(party_levels: &[i16], enemy_levels: &[i16], is_pwl_on: bool) -> i16 { +pub fn calculate_encounter_exp(party_levels: &[i64], enemy_levels: &[i64], is_pwl_on: bool) -> i64 { // Given a party and enemy party, it calculates the exp that the // party will get from defeating the enemy - let party_avg = party_levels.iter().sum::() as f32 / party_levels.len() as f32; + let party_avg = party_levels.iter().sum::() as f64 / party_levels.len() as f64; let exp_sum = enemy_levels .iter() .map(|&curr_enemy_lvl| { - let enemy_lvl = curr_enemy_lvl as f32; - let lvl_diff = if enemy_lvl < 0f32 && enemy_lvl < party_avg { - ((enemy_lvl - party_avg).abs()).neg() + let enemy_lvl = curr_enemy_lvl as f64; + let lvl_diff = if enemy_lvl < 0. && enemy_lvl < party_avg { + (enemy_lvl - party_avg).abs().neg() } else { enemy_lvl - party_avg }; @@ -73,21 +72,21 @@ pub fn calculate_encounter_exp(party_levels: &[i16], enemy_levels: &[i16], is_pw pub fn calculate_encounter_scaling_difficulty( party_size: usize, -) -> HashMap { +) -> HashMap { // Given the party size, it scales and calculates the threshold for the various difficulty levels let mut diff_scaled_exp_map = HashMap::new(); for curr_diff in EncounterChallengeEnum::iter() { diff_scaled_exp_map.insert( curr_diff.clone(), - scale_difficulty_exp(&curr_diff, party_size as i16).lower_bound, + scale_difficulty_exp(&curr_diff, party_size as i64).lower_bound, ); } diff_scaled_exp_map } pub fn calculate_encounter_difficulty( - encounter_exp: i16, - scaled_exp_levels: &HashMap, + encounter_exp: i64, + scaled_exp_levels: &HashMap, ) -> EncounterChallengeEnum { // This method is ugly, it's 1:1 from python and as such needs refactor if &encounter_exp < scaled_exp_levels.get(&EncounterChallengeEnum::Low).unwrap() { @@ -122,26 +121,26 @@ pub fn calculate_encounter_difficulty( pub fn calculate_lvl_combination_for_encounter( difficulty: &EncounterChallengeEnum, - party_levels: &[i16], + party_levels: &[i64], is_pwl_on: bool, -) -> HashSet> { +) -> HashSet> { // Given an encounter difficulty it calculates all possible encounter permutations - let exp_range = scale_difficulty_exp(difficulty, party_levels.len() as i16); - let party_avg = party_levels.iter().sum::() as f32 / party_levels.len() as f32; + let exp_range = scale_difficulty_exp(difficulty, party_levels.len() as i64); + let party_avg: f64 = party_levels.iter().sum::() as f64 / party_levels.len() as f64; calculate_lvl_combinations_for_given_exp( exp_range, - party_avg.floor(), + party_avg.floor() as i64, calculate_lvl_and_exp_map(is_pwl_on), ) } pub fn filter_combinations_outside_range( - combinations: HashSet>, + combinations: HashSet>, lower_bound: Option, upper_bound: Option, -) -> HashSet> { - let mut lower = lower_bound.unwrap_or(0); - let mut upper = upper_bound.unwrap_or(0); +) -> HashSet> { + let mut lower = lower_bound.unwrap_or(0) as i64; + let mut upper = upper_bound.unwrap_or(0) as i64; if lower != 0 && upper == 0 { upper = lower; } else if lower == 0 && upper != 0 { @@ -151,7 +150,7 @@ pub fn filter_combinations_outside_range( } let mut filtered_combo = HashSet::new(); combinations.iter().for_each(|curr_combo| { - if curr_combo.length() >= lower as u64 && curr_combo.length() <= upper as u64 { + if curr_combo.len() >= lower as usize && curr_combo.len() <= upper as usize { filtered_combo.insert(curr_combo.clone()); } }); @@ -159,20 +158,20 @@ pub fn filter_combinations_outside_range( } fn convert_lvl_diff_into_exp( - lvl_diff: f32, + lvl_diff: f64, party_size: usize, - lvl_and_exp_map: &HashMap, -) -> i16 { - let lvl_diff_rounded_down = lvl_diff.floor() as i16; + lvl_and_exp_map: &HashMap, +) -> i64 { + let lvl_diff_rounded_down = lvl_diff.floor() as i64; lvl_and_exp_map .get(&lvl_diff_rounded_down) .map(|value| value.abs()) .unwrap_or( if lvl_diff_rounded_down < calculate_max_lvl_diff(lvl_and_exp_map) { - 0i16 + 0 } else { // To avoid the party of 50 level 1 pg destroying a lvl 20 - scale_difficulty_exp(&EncounterChallengeEnum::Impossible, party_size as i16) + scale_difficulty_exp(&EncounterChallengeEnum::Impossible, party_size as i64) .lower_bound }, ) @@ -180,11 +179,11 @@ fn convert_lvl_diff_into_exp( fn calculate_lvl_combinations_for_given_exp( experience_range: ExpRange, - party_lvl: f32, - lvl_and_exp_map: HashMap, -) -> HashSet> { + party_lvl: i64, + lvl_and_exp_map: HashMap, +) -> HashSet> { // Given an encounter experience it calculates all possible encounter permutations - let exp_list = lvl_and_exp_map.values().cloned().collect::>(); + let exp_list = lvl_and_exp_map.values().copied().collect(); find_combinations(exp_list, experience_range) .iter() .map(|curr_combination| { @@ -192,31 +191,31 @@ fn calculate_lvl_combinations_for_given_exp( .iter() .map(|curr_exp| convert_exp_to_lvl_diff(*curr_exp, &lvl_and_exp_map)) .filter(|a| a.is_some()) - .map(|lvl_diff| party_lvl as i16 + lvl_diff.unwrap()) - .collect::>() + .map(|lvl_diff| party_lvl + lvl_diff.unwrap()) + .collect() }) - .filter(|x| !x.is_empty()) + .filter(|x: &Vec| !x.is_empty()) // there are no creature with level<-1 .filter(|x| x.iter().all(|curr_lvl| *curr_lvl >= -1)) - .collect::>>() + .collect::>>() } -fn convert_exp_to_lvl_diff(experience: i16, lvl_and_exp_map: &HashMap) -> Option { +fn convert_exp_to_lvl_diff(experience: i64, lvl_and_exp_map: &HashMap) -> Option { lvl_and_exp_map .iter() .find_map(|(key, &exp)| if exp == experience { Some(*key) } else { None }) } -fn find_combinations(candidates: Vec, target_range: ExpRange) -> Vec> { +fn find_combinations(candidates: Vec, target_range: ExpRange) -> Vec> { // Find all the combination of numbers in the candidates vector // that sums up to the target. I.e coin changing problem fn backtrack( - candidates: &Vec, - lb_target: i16, - ub_target: i16, + candidates: &Vec, + lb_target: i64, + ub_target: i64, start: usize, - path: &mut Vec, - result: &mut Vec>, + path: &mut Vec, + result: &mut Vec>, ) { if lb_target == 0 || (lb_target < 0 && ub_target > 0) { // If target is reached OR we exceeded lower bound but still not have reached upper bound, diff --git a/src/services/encounter_service.rs b/src/services/encounter_service.rs index 39ba9ca..bdda2c7 100644 --- a/src/services/encounter_service.rs +++ b/src/services/encounter_service.rs @@ -1,8 +1,7 @@ -use crate::db::db_proxy::{fetch_creatures_passing_all_filters, order_list_by_level}; +use crate::db::proxy::{get_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::variant_enum::CreatureVariant; use crate::models::encounter_structs::{ EncounterChallengeEnum, EncounterParams, RandomEncounterData, }; @@ -21,9 +20,9 @@ use utoipa::ToSchema; #[derive(Serialize, Deserialize, ToSchema)] pub struct EncounterInfoResponse { - experience: i16, + experience: i64, challenge: EncounterChallengeEnum, - encounter_exp_levels: BTreeMap, + encounter_exp_levels: BTreeMap, } #[derive(Serialize, Deserialize, ToSchema)] @@ -73,11 +72,11 @@ pub async fn generate_random_encounter( }) } -// Private method, does not handle failure. For that we use a public method +/// Private method, does not handle failure. For that we use a public method async fn calculate_random_encounter( app_state: &AppState, enc_data: RandomEncounterData, - party_levels: Vec, + party_levels: Vec, ) -> Result { let enc_diff = enc_data.challenge.unwrap_or(rand::random()); let is_pwl_on = enc_data.is_pwl_on; @@ -115,9 +114,9 @@ async fn calculate_random_encounter( let filtered_creatures = get_filtered_creatures( app_state, - &filter_map, - enc_data.allow_weak_variants, - enc_data.allow_elite_variants, + filter_map, + enc_data.allow_weak_variants.is_some_and(|x| x), + enc_data.allow_elite_variants.is_some_and(|x| x), ) .await?; @@ -134,14 +133,14 @@ async fn calculate_random_encounter( chosen_encounter .clone() .into_iter() - .map(|x| ResponseCreature::from((x, &enc_data.response_data))) + .map(ResponseCreature::from) .collect(), ), encounter_info: get_encounter_info(EncounterParams { party_levels, enemy_levels: chosen_encounter .iter() - .map(|cr| cr.variant_data.level as i16) + .map(|cr| cr.variant_data.level) .collect(), is_pwl_on, }), @@ -149,12 +148,12 @@ async fn calculate_random_encounter( } fn choose_random_creatures_combination( - filtered_creatures: HashSet, - lvl_combinations: HashSet>, + filtered_creatures: Vec, + lvl_combinations: HashSet>, ) -> Result> { // Chooses an id combination, could be (1, 1, 2). Admits duplicates let creatures_ordered_by_level = order_list_by_level(filtered_creatures); - let mut list_of_levels: Vec = Vec::new(); + let mut list_of_levels = Vec::new(); creatures_ordered_by_level .keys() .for_each(|key| list_of_levels.push(*key)); @@ -207,9 +206,9 @@ fn fill_vector_if_it_does_not_contain_enough_elements( } fn filter_non_existing_levels( - creatures_levels: Vec, - level_combinations: HashSet>, -) -> HashSet> { + creatures_levels: Vec, + level_combinations: HashSet>, +) -> HashSet> { // Removes the vec with levels that are not found in any creature let mut result_vec = HashSet::new(); for curr_combo in level_combinations { @@ -246,8 +245,8 @@ fn build_filter_map(filter_enum: FilterStruct) -> HashMap HashMap HashMap>, - allow_weak: Option, - allow_elite: Option, -) -> Result> { - let mut filtered_variants = HashSet::new(); - - if allow_weak.is_some() && allow_weak.unwrap() { - filtered_variants.extend( - fetch_creatures_passing_all_filters(app_state, filter_map, CreatureVariant::Weak) - .await?, - ); - } - if allow_elite.is_some() && allow_elite.unwrap() { - filtered_variants.extend( - fetch_creatures_passing_all_filters(app_state, filter_map, CreatureVariant::Elite) - .await?, - ); - } - - let mut filtered_creatures = - fetch_creatures_passing_all_filters(app_state, filter_map, CreatureVariant::Base).await?; - - filtered_creatures.extend(filtered_variants); - Ok(filtered_creatures) + filter_map: HashMap>, + allow_weak: bool, + allow_elite: bool, +) -> Result> { + get_creatures_passing_all_filters(app_state, filter_map, allow_weak, allow_elite).await } diff --git a/src/services/url_calculator.rs b/src/services/url_calculator.rs index a669032..f565c97 100644 --- a/src/services/url_calculator.rs +++ b/src/services/url_calculator.rs @@ -1,4 +1,3 @@ -use crate::models::creature_metadata::type_enum::CreatureTypeEnum; use crate::models::routers_validator_structs::{FieldFilters, PaginatedRequest}; pub fn next_url_calculator( @@ -13,24 +12,9 @@ pub fn next_url_calculator( format!("{}{}{}", base_url, filter_query, pagination_query) } -pub fn generate_archive_link(id: Option, creature_type: &CreatureTypeEnum) -> Option { - id.map(|x| { - format!( - "https://2e.aonprd.com/{}.aspx?ID={}", - CreatureTypeEnum::to_url_string(creature_type), - x - ) - }) -} - -pub fn add_boolean_query(url: Option, key: &String, value: bool) -> Option { - match url { - Some(mut x) => { - x.push_str(&format!("&{}={}", key, value)); - Some(x) - } - None => None, - } +pub fn add_boolean_query(url: &Option, key: &String, value: bool) -> Option { + url.as_ref() + .map(|base_url| format!("{}&{}={}", base_url, key, value)) } fn filter_query_calculator(field_filters: &FieldFilters) -> String {