From de2193940b69fbc662902d6139bd7a45ea00b060 Mon Sep 17 00:00:00 2001 From: rakuja Date: Sun, 17 Mar 2024 17:03:35 +0100 Subject: [PATCH] feat: introduce creature roles. Right now they are bugged as scales do not seem coherent --- Cargo.toml | 1 + src/db/db_communicator.rs | 149 ++++- src/db/db_proxy.rs | 4 +- src/main.rs | 7 + src/models/creature.rs | 359 +++--------- .../creature_component/creature_combat.rs | 88 +++ .../creature_component/creature_core.rs | 70 +++ .../creature_component/creature_extra.rs | 101 ++++ .../creature_component/creature_info.rs | 44 ++ .../creature_spell_caster.rs | 28 + .../creature_component/creature_variant.rs | 17 + src/models/creature_component/mod.rs | 6 + src/models/creature_metadata/creature_role.rs | 510 ++++++++++++++++++ src/models/creature_metadata/mod.rs | 1 + src/models/db/raw_creature.rs | 8 +- src/models/items/action.rs | 21 + src/models/items/mod.rs | 3 + src/models/items/skill.rs | 12 + src/models/items/spell_caster_entry.rs | 12 + src/models/items/weapon.rs | 23 + src/models/mod.rs | 2 + src/models/response_data.rs | 30 +- src/models/scales_struct/ability_scales.rs | 9 + src/models/scales_struct/ac_scales.rs | 9 + src/models/scales_struct/area_dmg_scales.rs | 7 + src/models/scales_struct/creature_scales.rs | 29 + src/models/scales_struct/hp_scales.rs | 11 + src/models/scales_struct/item_scales.rs | 6 + src/models/scales_struct/mod.rs | 13 + src/models/scales_struct/perception_scales.rs | 10 + src/models/scales_struct/res_weak_scales.rs | 7 + .../scales_struct/saving_throw_scales.rs | 10 + src/models/scales_struct/skill_scales.rs | 10 + .../scales_struct/spell_dc_and_atk_scales.rs | 11 + .../scales_struct/strike_bonus_scales.rs | 9 + src/models/scales_struct/strike_dmg_scales.rs | 9 + src/routes/bestiary.rs | 31 +- 37 files changed, 1392 insertions(+), 285 deletions(-) create mode 100644 src/models/creature_component/creature_combat.rs create mode 100644 src/models/creature_component/creature_core.rs create mode 100644 src/models/creature_component/creature_extra.rs create mode 100644 src/models/creature_component/creature_info.rs create mode 100644 src/models/creature_component/creature_spell_caster.rs create mode 100644 src/models/creature_component/creature_variant.rs create mode 100644 src/models/creature_component/mod.rs create mode 100644 src/models/creature_metadata/creature_role.rs create mode 100644 src/models/items/action.rs create mode 100644 src/models/items/skill.rs create mode 100644 src/models/items/spell_caster_entry.rs create mode 100644 src/models/scales_struct/ability_scales.rs create mode 100644 src/models/scales_struct/ac_scales.rs create mode 100644 src/models/scales_struct/area_dmg_scales.rs create mode 100644 src/models/scales_struct/creature_scales.rs create mode 100644 src/models/scales_struct/hp_scales.rs create mode 100644 src/models/scales_struct/item_scales.rs create mode 100644 src/models/scales_struct/mod.rs create mode 100644 src/models/scales_struct/perception_scales.rs create mode 100644 src/models/scales_struct/res_weak_scales.rs create mode 100644 src/models/scales_struct/saving_throw_scales.rs create mode 100644 src/models/scales_struct/skill_scales.rs create mode 100644 src/models/scales_struct/spell_dc_and_atk_scales.rs create mode 100644 src/models/scales_struct/strike_bonus_scales.rs create mode 100644 src/models/scales_struct/strike_dmg_scales.rs diff --git a/Cargo.toml b/Cargo.toml index f5a8502..49cdbef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ strum = {version="0.26.1", features = ["derive"]} rand = "0.9.0-alpha.0" counter = "0.5.7" dotenvy = "0.15.7" +regex = "1.10.3" env_logger = "0.11.2" log = "0.4.21" diff --git a/src/db/db_communicator.rs b/src/db/db_communicator.rs index 91e33fd..e087164 100644 --- a/src/db/db_communicator.rs +++ b/src/db/db_communicator.rs @@ -7,13 +7,34 @@ use crate::models::db::raw_sense::RawSense; use crate::models::db::raw_speed::RawSpeed; use crate::models::db::raw_trait::RawTrait; use crate::models::db::raw_weakness::RawWeakness; +use crate::models::items::action::Action; +use crate::models::items::skill::Skill; use crate::models::items::spell::Spell; use crate::models::items::weapon::Weapon; +use crate::models::scales_struct::ability_scales::AbilityScales; +use crate::models::scales_struct::ac_scales::AcScales; +use crate::models::scales_struct::area_dmg_scales::AreaDmgScales; +use crate::models::scales_struct::creature_scales::CreatureScales; +use crate::models::scales_struct::hp_scales::HpScales; +use crate::models::scales_struct::item_scales::ItemScales; +use crate::models::scales_struct::perception_scales::PerceptionScales; +use crate::models::scales_struct::res_weak_scales::ResWeakScales; +use crate::models::scales_struct::saving_throw_scales::SavingThrowScales; +use crate::models::scales_struct::skill_scales::SkillScales; +use crate::models::scales_struct::spell_dc_and_atk_scales::SpellDcAndAtkScales; +use crate::models::scales_struct::strike_bonus_scales::StrikeBonusScales; +use crate::models::scales_struct::strike_dmg_scales::StrikeDmgScales; use anyhow::Result; +use regex::Regex; use sqlx::{Error, Pool, Sqlite}; -async fn from_raw_vec_to_creature(conn: &Pool, raw_vec: Vec) -> Vec { +async fn from_raw_vec_to_creature( + conn: &Pool, + scales: &CreatureScales, + raw_vec: Vec, +) -> Vec { let mut creature_list = Vec::new(); + let scales_dmg_regex = Regex::new(r"\((\d+)\)").ok().unwrap(); for el in raw_vec { let immunities = get_creature_immunities(conn, el.id) .await @@ -32,17 +53,23 @@ async fn from_raw_vec_to_creature(conn: &Pool, raw_vec: Vec .unwrap_or_default(); let spells = get_creature_spells(conn, el.id).await.unwrap_or_default(); let weapons = get_creature_weapons(conn, el.id).await.unwrap_or_default(); + let actions = get_creature_actions(conn, el.id).await.unwrap_or_default(); + let skills = get_creature_skills(conn, el.id).await.unwrap_or_default(); creature_list.push(Creature::from(( el, traits, weapons, spells, + actions, + skills, immunities, languages, resistances, senses, speeds, weaknesses, + scales, + &scales_dmg_regex, ))); } creature_list @@ -129,6 +156,26 @@ async fn get_creature_weapons(conn: &Pool, creature_id: i64) -> Result, creature_id: i64) -> Result> { + Ok(sqlx::query_as!( + Action, + "SELECT * FROM ACTION_TABLE WHERE creature_id == ($1)", + creature_id + ) + .fetch_all(conn) + .await?) +} + +async fn get_creature_skills(conn: &Pool, creature_id: i64) -> Result> { + Ok(sqlx::query_as!( + Skill, + "SELECT name, description, modifier, proficiency FROM SKILL_TABLE WHERE creature_id == ($1)", + creature_id + ) + .fetch_all(conn) + .await?) +} + async fn get_creature_spells(conn: &Pool, creature_id: i64) -> Result> { Ok(sqlx::query_as!( Spell, @@ -139,12 +186,110 @@ async fn get_creature_spells(conn: &Pool, creature_id: i64) -> Result) -> Result, Error> { +pub async fn fetch_creatures( + conn: &Pool, + creature_scales: &CreatureScales, +) -> Result, Error> { Ok(from_raw_vec_to_creature( conn, + creature_scales, sqlx::query_as!(RawCreature, "SELECT * FROM CREATURE_TABLE ORDER BY name") .fetch_all(conn) .await?, ) .await) } + +pub async fn fetch_creature_scales(conn: &Pool) -> Result { + Ok(CreatureScales { + ability_scales: sqlx::query_as!(AbilityScales, "SELECT * FROM ABILITY_SCALES_TABLE",) + .fetch_all(conn) + .await? + .into_iter() + .map(|n| (n.level as i8, n)) + .collect(), + ac_scales: sqlx::query_as!(AcScales, "SELECT * FROM AC_SCALES_TABLE",) + .fetch_all(conn) + .await? + .into_iter() + .map(|n| (n.level as i8, n)) + .collect(), + area_dmg_scales: sqlx::query_as!(AreaDmgScales, "SELECT * FROM AREA_DAMAGE_SCALES_TABLE",) + .fetch_all(conn) + .await? + .into_iter() + .map(|n| (n.level as i8, n)) + .collect(), + hp_scales: sqlx::query_as!(HpScales, "SELECT * FROM HP_SCALES_TABLE",) + .fetch_all(conn) + .await? + .into_iter() + .map(|n| (n.level as i8, n)) + .collect(), + item_scales: sqlx::query_as!(ItemScales, "SELECT * FROM ITEM_SCALES_TABLE",) + .fetch_all(conn) + .await? + .into_iter() + .map(|n| (n.cr_level.clone(), n)) + .collect(), + perception_scales: sqlx::query_as!( + PerceptionScales, + "SELECT * FROM PERCEPTION_SCALES_TABLE", + ) + .fetch_all(conn) + .await? + .into_iter() + .map(|n| (n.level as i8, n)) + .collect(), + res_weak_scales: sqlx::query_as!(ResWeakScales, "SELECT * FROM RES_WEAK_SCALES_TABLE",) + .fetch_all(conn) + .await? + .into_iter() + .map(|n| (n.level as i8, n)) + .collect(), + saving_throw_scales: sqlx::query_as!( + SavingThrowScales, + "SELECT * FROM SAVING_THROW_SCALES_TABLE", + ) + .fetch_all(conn) + .await? + .into_iter() + .map(|n| (n.level as i8, n)) + .collect(), + skill_scales: sqlx::query_as!(SkillScales, "SELECT * FROM SKILL_SCALES_TABLE",) + .fetch_all(conn) + .await? + .into_iter() + .map(|n| (n.level as i8, n)) + .collect(), + spell_dc_and_atk_scales: sqlx::query_as!( + SpellDcAndAtkScales, + "SELECT * FROM SPELL_DC_AND_ATTACK_SCALES_TABLE", + ) + .fetch_all(conn) + .await? + .into_iter() + .map(|n| (n.level as i8, n)) + .collect(), + strike_bonus_scales: sqlx::query_as!( + StrikeBonusScales, + "SELECT * FROM STRIKE_BONUS_SCALES_TABLE", + ) + .fetch_all(conn) + .await? + .into_iter() + .map(|n| (n.level as i8, n)) + .collect(), + strike_dmg_scales: sqlx::query_as!( + StrikeDmgScales, + "SELECT * FROM STRIKE_DAMAGE_SCALES_TABLE", + ) + .fetch_all(conn) + .await? + .into_iter() + .map(|n| (n.level as i8, n)) + .collect(), + }) +} + +// TODO: Restructure creature fetch to use transaction diff --git a/src/db/db_proxy.rs b/src/db/db_proxy.rs index 3c6b3b9..3eb0c73 100644 --- a/src/db/db_proxy.rs +++ b/src/db/db_proxy.rs @@ -213,7 +213,9 @@ async fn fetch_creatures(app_state: &AppState, variant: CreatureVariant) -> Opti let index = &CreatureVariant::to_cache_index(&variant); if let Some(creatures) = cache.get(index) { return Some(creatures); - } else if let Ok(creatures) = db_communicator::fetch_creatures(&app_state.conn).await { + } else if let Ok(creatures) = + db_communicator::fetch_creatures(&app_state.conn, &app_state.creature_scales).await + { cache.insert(0, creatures.clone()); let mut weak_creatures = Vec::new(); let mut elite_creatures = Vec::new(); diff --git a/src/main.rs b/src/main.rs index 2709257..9f3235e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ mod routes; use crate::db::db_cache::RuntimeFieldsValues; use crate::models::creature::Creature; +use crate::models::scales_struct::creature_scales::CreatureScales; use crate::routes::{bestiary, encounter, health}; use actix_cors::Cors; use actix_web::http::header::{CacheControl, CacheDirective}; @@ -26,6 +27,7 @@ pub struct AppState { conn: Pool, creature_cache: Cache>, runtime_fields_cache: Cache, + creature_scales: CreatureScales, } #[utoipa::path(get, path = "/")] @@ -87,6 +89,10 @@ async fn main() -> std::io::Result<()> { // Create the cache. .build(); + let creature_scales = db::db_communicator::fetch_creature_scales(&pool) + .await + .expect("Could not establish valid connection with the database.. Startup failed"); + log::info!( "starting HTTP server at http://{}:{}", service_ip.as_str(), @@ -127,6 +133,7 @@ async fn main() -> std::io::Result<()> { conn: pool.clone(), creature_cache: cr_cache.clone(), runtime_fields_cache: fields_cache.clone(), + creature_scales: creature_scales.clone(), })) }) .bind((get_service_ip(), get_service_port()))? diff --git a/src/models/creature.rs b/src/models/creature.rs index a4fed43..40840db 100644 --- a/src/models/creature.rs +++ b/src/models/creature.rs @@ -1,8 +1,9 @@ -use crate::models::creature_metadata::alignment_enum::AlignmentEnum; -use crate::models::creature_metadata::rarity_enum::RarityEnum; -use crate::models::creature_metadata::size_enum::SizeEnum; -use crate::models::creature_metadata::type_enum::CreatureTypeEnum; -use crate::models::creature_metadata::variant_enum::CreatureVariant; +use crate::models::creature_component::creature_combat::CreatureCombatData; +use crate::models::creature_component::creature_core::CreatureCoreData; +use crate::models::creature_component::creature_extra::CreatureExtraData; +use crate::models::creature_component::creature_info::CreatureInfo; +use crate::models::creature_component::creature_spell_caster::CreatureSpellCasterData; +use crate::models::creature_component::creature_variant::CreatureVariantData; use crate::models::db::raw_creature::RawCreature; use crate::models::db::raw_immunity::RawImmunity; use crate::models::db::raw_language::RawLanguage; @@ -11,106 +12,32 @@ use crate::models::db::raw_sense::RawSense; use crate::models::db::raw_speed::RawSpeed; use crate::models::db::raw_trait::RawTrait; use crate::models::db::raw_weakness::RawWeakness; +use crate::models::items::action::Action; +use crate::models::items::skill::Skill; use crate::models::items::spell::Spell; use crate::models::items::weapon::Weapon; use crate::models::routers_validator_structs::FieldFilters; +use crate::models::scales_struct::creature_scales::CreatureScales; use crate::services::url_calculator::generate_archive_link; +use regex::Regex; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; #[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] -pub struct CoreCreatureData { - pub id: i32, - pub aon_id: Option, - pub name: String, - pub hp: i16, - // constant value, it will never change - pub base_level: i8, - pub alignment: AlignmentEnum, - pub size: SizeEnum, - pub family: Option, - pub rarity: RarityEnum, - pub is_spell_caster: bool, - pub is_melee: bool, - pub is_ranged: bool, - pub publication_info: PublicationInfo, - pub traits: Vec, - pub archive_link: Option, - pub creature_type: CreatureTypeEnum, - pub variant: CreatureVariant, -} - -#[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] -pub struct VariantCreatureData { - pub level: i8, - pub archive_link: Option, -} - -#[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] -pub struct ExtraCreatureData { - pub weapons: Vec, - pub spells: Vec, - pub immunities: Vec, - pub languages: Vec, - pub resistances: Vec<(String, i16)>, - pub senses: Vec, - pub speeds: Vec<(String, i16)>, - pub weaknesses: Vec<(String, i16)>, - pub ability_scores: AbilityScores, - pub hp_detail: Option, - pub ac_detail: Option, - pub language_detail: Option, - pub perception: i8, - pub perception_detail: Option, - pub saving_throws: SavingThrows, -} - -#[derive(Serialize, Deserialize, Clone, Eq, Hash, PartialEq)] -pub struct AbilityScores { - pub charisma: i8, - pub constitution: i8, - pub dexterity: i8, - pub intelligence: i8, - pub strength: i8, - pub wisdom: i8, -} - -#[derive(Serialize, Deserialize, Clone, Eq, Hash, PartialEq)] pub struct PublicationInfo { pub license: String, pub remaster: bool, pub source: String, } -#[derive(Serialize, Deserialize, Clone, Eq, Hash, PartialEq)] -pub struct SavingThrows { - fortitude: i8, - reflex: i8, - will: i8, - fortitude_detail: Option, - reflex_detail: Option, - will_detail: Option, -} - #[derive(Serialize, Deserialize, Clone, Eq, Hash, PartialEq)] pub struct Creature { - pub core_data: CoreCreatureData, - pub variant_data: VariantCreatureData, - pub extra_data: ExtraCreatureData, -} - -impl CoreCreatureData { - pub fn is_melee(weapons: &[Weapon]) -> bool { - weapons - .iter() - .any(|el| el.wp_type.to_uppercase() == "MELEE") - } - - pub fn is_ranged(weapons: &[Weapon]) -> bool { - weapons - .iter() - .any(|el| el.wp_type.to_uppercase() == "RANGED") - } + pub core_data: CreatureCoreData, + pub variant_data: CreatureVariantData, + pub extra_data: CreatureExtraData, + pub combat_data: CreatureCombatData, + pub spell_caster_data: CreatureSpellCasterData, + pub info: CreatureInfo, } impl Creature { @@ -195,174 +122,22 @@ impl Creature { } } -impl From<(RawCreature, Vec, bool, bool, Option)> for CoreCreatureData { - fn from(tuple: (RawCreature, Vec, bool, bool, Option)) -> Self { - let raw = tuple.0; - let traits = tuple.1; - let is_ranged = tuple.2; - let is_melee = tuple.3; - let archive_link = tuple.4; - - let alignment_enum = AlignmentEnum::from((&traits, raw.remaster)); - CoreCreatureData { - id: raw.id as i32, - aon_id: raw.aon_id.map(|x| x as i32), - name: raw.name.clone(), - hp: raw.hp as i16, - base_level: raw.level as i8, - alignment: alignment_enum, - size: raw.size.clone(), - family: raw.family.clone(), - rarity: raw.rarity.clone(), - is_spell_caster: raw.is_spell_caster, - publication_info: PublicationInfo { - remaster: raw.remaster, - source: raw.source, - license: raw.license, - }, - traits: traits - .into_iter() - .map(|curr_trait| curr_trait.name) - .collect(), - creature_type: raw.cr_type.clone(), - archive_link: archive_link.clone(), - variant: CreatureVariant::Base, - is_ranged, - is_melee, - } - } -} - -impl From<(i64, Option)> for VariantCreatureData { - fn from(value: (i64, Option)) -> Self { - Self { - level: value.0 as i8, - archive_link: value.1, - } - } -} - -impl - From<( - RawCreature, - Vec, - Vec, - Vec, - Vec, - Vec, - Vec, - Vec, - Vec, - )> for ExtraCreatureData -{ - fn from( - tuple: ( - RawCreature, - Vec, - Vec, - Vec, - Vec, - Vec, - Vec, - Vec, - Vec, - ), - ) -> Self { - let raw_cr = tuple.0; - Self { - weapons: tuple.1, - spells: tuple.2, - immunities: tuple - .3 - .into_iter() - .map(|curr_trait| curr_trait.name) - .collect(), - languages: tuple - .4 - .into_iter() - .map(|curr_trait| curr_trait.name) - .collect(), - resistances: tuple - .5 - .into_iter() - .map(|curr_res| (curr_res.name, curr_res.value as i16)) - .collect(), - senses: tuple - .6 - .into_iter() - .map(|curr_trait| curr_trait.name) - .collect(), - speeds: tuple - .7 - .into_iter() - .map(|curr_speed| (curr_speed.name, curr_speed.value as i16)) - .collect(), - weaknesses: tuple - .8 - .into_iter() - .map(|curr_weak| (curr_weak.name, curr_weak.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) - }, - 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) - }, - }, - } - } -} - impl From<( RawCreature, Vec, Vec, Vec, + Vec, + Vec, Vec, Vec, Vec, Vec, Vec, Vec, + &CreatureScales, + &Regex, )> for Creature { fn from( @@ -371,49 +146,87 @@ impl Vec, Vec, Vec, + Vec, + Vec, Vec, Vec, Vec, Vec, Vec, Vec, + &CreatureScales, + &Regex, ), ) -> Self { let raw_creature = tuple.0; + let traits = tuple.1; let weapons = tuple.2; let spells = tuple.3; - let traits = tuple.1; - let immunities = tuple.4; - let languages = tuple.5; - let resistances = tuple.6; - let senses = tuple.7; - let speeds = tuple.8; - let weaknesses = tuple.9; + let actions = tuple.4; + let skills = tuple.5; + let immunities = tuple.6; + let languages = tuple.7; + let resistances = tuple.8; + let senses = tuple.9; + let speeds = tuple.10; + let weaknesses = tuple.11; let archive_link = generate_archive_link(raw_creature.aon_id, &raw_creature.cr_type); - let is_ranged = CoreCreatureData::is_ranged(&weapons); - let is_melee = CoreCreatureData::is_melee(&weapons); + let is_ranged = is_ranged(&weapons); + let is_melee = is_melee(&weapons); + let core_data = CreatureCoreData::from(( + raw_creature.clone(), + traits, + is_ranged, + is_melee, + archive_link.clone(), + )); + let extra_data = CreatureExtraData::from(( + raw_creature.clone(), + actions, + skills, + languages, + senses, + speeds, + )); + + let combat_data = CreatureCombatData::from(( + raw_creature.clone(), + weapons, + immunities, + resistances, + weaknesses, + )); + let scales = tuple.12; + let spell_caster_data = CreatureSpellCasterData::from((raw_creature.clone(), spells)); + let info = CreatureInfo::from(( + &core_data, + &extra_data, + &combat_data, + &spell_caster_data, + scales, + tuple.13, + )); Self { - variant_data: VariantCreatureData::from((raw_creature.level, archive_link.clone())), - core_data: CoreCreatureData::from(( - raw_creature.clone(), - traits, - is_ranged, - is_melee, - archive_link, - )), - extra_data: ExtraCreatureData::from(( - raw_creature, - weapons, - spells, - immunities, - languages, - resistances, - senses, - speeds, - weaknesses, - )), + variant_data: CreatureVariantData::from((raw_creature.level, archive_link)), + core_data, + extra_data, + spell_caster_data, + combat_data, + info, } } } + +fn is_melee(weapons: &[Weapon]) -> bool { + weapons + .iter() + .any(|el| el.wp_type.to_uppercase() == "MELEE") +} + +fn is_ranged(weapons: &[Weapon]) -> bool { + weapons + .iter() + .any(|el| el.wp_type.to_uppercase() == "RANGED") +} diff --git a/src/models/creature_component/creature_combat.rs b/src/models/creature_component/creature_combat.rs new file mode 100644 index 0000000..c5c91a6 --- /dev/null +++ b/src/models/creature_component/creature_combat.rs @@ -0,0 +1,88 @@ +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 utoipa::ToSchema; + +#[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] +pub struct SavingThrows { + pub fortitude: i8, + pub reflex: i8, + pub will: i8, + pub fortitude_detail: Option, + pub reflex_detail: Option, + pub will_detail: Option, +} + +#[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] +pub struct CreatureCombatData { + pub weapons: Vec, + pub resistances: Vec<(String, i16)>, + pub immunities: Vec, + pub weaknesses: Vec<(String, i16)>, + pub saving_throws: SavingThrows, + pub ac: i8, +} + +impl + From<( + RawCreature, + Vec, + Vec, + Vec, + Vec, + )> for CreatureCombatData +{ + fn from( + tuple: ( + RawCreature, + Vec, + Vec, + Vec, + Vec, + ), + ) -> Self { + let raw_cr = tuple.0; + Self { + weapons: tuple.1, + immunities: tuple + .2 + .into_iter() + .map(|curr_trait| curr_trait.name) + .collect(), + resistances: tuple + .3 + .into_iter() + .map(|curr_res| (curr_res.name, curr_res.value as i16)) + .collect(), + weaknesses: tuple + .4 + .into_iter() + .map(|curr_weak| (curr_weak.name, curr_weak.value as i16)) + .collect(), + ac: raw_cr.ac as i8, + saving_throws: SavingThrows { + fortitude: raw_cr.fortitude as i8, + reflex: raw_cr.reflex as i8, + will: raw_cr.will as i8, + fortitude_detail: if raw_cr.fortitude_detail.is_empty() { + None + } else { + Some(raw_cr.fortitude_detail) + }, + reflex_detail: if raw_cr.reflex_detail.is_empty() { + None + } else { + Some(raw_cr.reflex_detail) + }, + will_detail: if raw_cr.will_detail.is_empty() { + None + } else { + Some(raw_cr.will_detail) + }, + }, + } + } +} diff --git a/src/models/creature_component/creature_core.rs b/src/models/creature_component/creature_core.rs new file mode 100644 index 0000000..8377d33 --- /dev/null +++ b/src/models/creature_component/creature_core.rs @@ -0,0 +1,70 @@ +use crate::models::creature::PublicationInfo; +use crate::models::creature_metadata::alignment_enum::AlignmentEnum; +use crate::models::creature_metadata::rarity_enum::RarityEnum; +use crate::models::creature_metadata::size_enum::SizeEnum; +use crate::models::creature_metadata::type_enum::CreatureTypeEnum; +use crate::models::creature_metadata::variant_enum::CreatureVariant; +use crate::models::db::raw_creature::RawCreature; +use crate::models::db::raw_trait::RawTrait; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] +pub struct CreatureCoreData { + pub id: i32, + pub aon_id: Option, + pub name: String, + pub hp: i16, + // constant value, it will never change + pub base_level: i8, + pub alignment: AlignmentEnum, + pub size: SizeEnum, + pub family: Option, + pub rarity: RarityEnum, + pub is_melee: bool, + pub is_ranged: bool, + pub is_spell_caster: bool, + pub publication_info: PublicationInfo, + pub traits: Vec, + pub archive_link: Option, + pub creature_type: CreatureTypeEnum, + pub variant: CreatureVariant, +} + +impl From<(RawCreature, Vec, bool, bool, Option)> for CreatureCoreData { + fn from(tuple: (RawCreature, Vec, bool, bool, Option)) -> Self { + let raw = tuple.0; + let traits = tuple.1; + let is_ranged = tuple.2; + let is_melee = tuple.3; + let archive_link = tuple.4; + + let alignment_enum = AlignmentEnum::from((&traits, raw.remaster)); + CreatureCoreData { + id: raw.id as i32, + aon_id: raw.aon_id.map(|x| x as i32), + name: raw.name.clone(), + hp: raw.hp as i16, + base_level: raw.level as i8, + alignment: alignment_enum, + size: raw.size.clone(), + family: raw.family.clone(), + rarity: raw.rarity.clone(), + is_spell_caster: raw.spell_casting_name.is_some(), + publication_info: PublicationInfo { + remaster: raw.remaster, + source: raw.source, + license: raw.license, + }, + traits: traits + .into_iter() + .map(|curr_trait| curr_trait.name) + .collect(), + creature_type: raw.cr_type.clone(), + archive_link: archive_link.clone(), + variant: CreatureVariant::Base, + is_ranged, + is_melee, + } + } +} diff --git a/src/models/creature_component/creature_extra.rs b/src/models/creature_component/creature_extra.rs new file mode 100644 index 0000000..78ecd87 --- /dev/null +++ b/src/models/creature_component/creature_extra.rs @@ -0,0 +1,101 @@ +use crate::models::db::raw_creature::RawCreature; +use crate::models::db::raw_language::RawLanguage; +use crate::models::db::raw_sense::RawSense; +use crate::models::db::raw_speed::RawSpeed; +use crate::models::items::action::Action; +use crate::models::items::skill::Skill; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] +pub struct AbilityScores { + pub charisma: i8, + pub constitution: i8, + pub dexterity: i8, + pub intelligence: i8, + pub strength: i8, + pub wisdom: i8, +} + +#[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] +pub struct CreatureExtraData { + pub actions: Vec, + pub skills: Vec, + pub languages: Vec, + pub senses: Vec, + pub speeds: Vec<(String, i16)>, + pub ability_scores: AbilityScores, + pub hp_detail: Option, + pub ac_detail: Option, + pub language_detail: Option, + pub perception: i8, + pub perception_detail: Option, +} + +impl + From<( + RawCreature, + Vec, + Vec, + Vec, + Vec, + Vec, + )> for CreatureExtraData +{ + fn from( + tuple: ( + RawCreature, + Vec, + Vec, + Vec, + Vec, + Vec, + ), + ) -> Self { + let raw_cr = tuple.0; + Self { + actions: tuple.1, + skills: tuple.2, + languages: tuple + .3 + .into_iter() + .map(|curr_trait| curr_trait.name) + .collect(), + senses: tuple + .4 + .into_iter() + .map(|curr_trait| curr_trait.name) + .collect(), + speeds: tuple + .5 + .into_iter() + .map(|curr_speed| (curr_speed.name, curr_speed.value as i16)) + .collect(), + ability_scores: AbilityScores { + charisma: raw_cr.charisma as i8, + constitution: raw_cr.constitution as i8, + dexterity: raw_cr.dexterity as i8, + intelligence: raw_cr.intelligence as i8, + strength: raw_cr.strength as i8, + wisdom: raw_cr.wisdom as i8, + }, + hp_detail: if raw_cr.hp_detail.is_empty() { + None + } else { + Some(raw_cr.hp_detail) + }, + ac_detail: if raw_cr.ac_detail.is_empty() { + None + } else { + Some(raw_cr.ac_detail) + }, + language_detail: raw_cr.language_detail, + perception: raw_cr.perception as i8, + perception_detail: if raw_cr.perception_detail.is_empty() { + None + } else { + Some(raw_cr.perception_detail) + }, + } + } +} diff --git a/src/models/creature_component/creature_info.rs b/src/models/creature_component/creature_info.rs new file mode 100644 index 0000000..fbbf408 --- /dev/null +++ b/src/models/creature_component/creature_info.rs @@ -0,0 +1,44 @@ +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::CreatureRole; +use crate::models::scales_struct::creature_scales::CreatureScales; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] +pub struct CreatureInfo { + pub roles: Vec, + 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: CreatureRole::from_creature_with_given_scales( + tuple.0, tuple.1, tuple.2, tuple.3, tuple.4, tuple.5, + ), + locale: vec![], + } + } +} diff --git a/src/models/creature_component/creature_spell_caster.rs b/src/models/creature_component/creature_spell_caster.rs new file mode 100644 index 0000000..bca6ebc --- /dev/null +++ b/src/models/creature_component/creature_spell_caster.rs @@ -0,0 +1,28 @@ +use crate::models::db::raw_creature::RawCreature; +use crate::models::items::spell::Spell; +use crate::models::items::spell_caster_entry::SpellCasterEntry; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] +pub struct CreatureSpellCasterData { + pub spells: Vec, + pub spell_caster_entry: SpellCasterEntry, +} + +impl From<(RawCreature, Vec)> for CreatureSpellCasterData { + fn from(tuple: (RawCreature, Vec)) -> Self { + let raw_cr = tuple.0; + Self { + spells: tuple.1, + spell_caster_entry: SpellCasterEntry { + spell_casting_name: raw_cr.spell_casting_name, + is_spell_casting_flexible: raw_cr.is_spell_casting_flexible, + type_of_spell_caster: raw_cr.type_of_spell_caster, + spell_casting_dc_mod: raw_cr.spell_casting_dc_mod.map(|x| x as i8), + spell_casting_atk_mod: raw_cr.spell_casting_atk_mod.map(|x| x as i8), + spell_casting_tradition: raw_cr.spell_casting_tradition, + }, + } + } +} diff --git a/src/models/creature_component/creature_variant.rs b/src/models/creature_component/creature_variant.rs new file mode 100644 index 0000000..88af048 --- /dev/null +++ b/src/models/creature_component/creature_variant.rs @@ -0,0 +1,17 @@ +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] +pub struct CreatureVariantData { + pub level: i8, + pub archive_link: Option, +} + +impl From<(i64, Option)> for CreatureVariantData { + fn from(value: (i64, Option)) -> Self { + Self { + level: value.0 as i8, + archive_link: value.1, + } + } +} diff --git a/src/models/creature_component/mod.rs b/src/models/creature_component/mod.rs new file mode 100644 index 0000000..31682c2 --- /dev/null +++ b/src/models/creature_component/mod.rs @@ -0,0 +1,6 @@ +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; diff --git a/src/models/creature_metadata/creature_role.rs b/src/models/creature_metadata/creature_role.rs new file mode 100644 index 0000000..bf7f711 --- /dev/null +++ b/src/models/creature_metadata/creature_role.rs @@ -0,0 +1,510 @@ +use crate::models::creature_component::creature_combat::CreatureCombatData; +use crate::models::creature_component::creature_core::CreatureCoreData; +use crate::models::creature_component::creature_extra::CreatureExtraData; +use crate::models::creature_component::creature_spell_caster::CreatureSpellCasterData; +use crate::models::scales_struct::creature_scales::CreatureScales; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use std::fmt; +use utoipa::ToSchema; +use validator::HasLen; + +#[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] +pub enum CreatureRole { + Brute, + MagicalStriker, + SkillParagon, + Skirmisher, + Sniper, + Soldier, + SpellCaster, +} + +impl CreatureRole { + pub fn from_creature_with_given_scales( + cr_core: &CreatureCoreData, + cr_extra: &CreatureExtraData, + cr_combat: &CreatureCombatData, + cr_spells: &CreatureSpellCasterData, + scales: &CreatureScales, + dmg_scales_regex: &Regex, + ) -> Vec { + let mut roles = Vec::new(); + if is_brute(cr_core, cr_extra, cr_combat, scales, dmg_scales_regex).is_some_and(|x| x) { + roles.push(Self::Brute); + } + if is_magical_striker(cr_core, cr_spells, cr_combat, scales, dmg_scales_regex) + .is_some_and(|x| x) + { + roles.push(Self::MagicalStriker) + } + if is_skill_paragon(cr_core, cr_extra, cr_combat, scales).is_some_and(|x| x) { + roles.push(Self::SkillParagon) + } + if is_skirmisher(cr_core, cr_extra, cr_combat, scales).is_some_and(|x| x) { + roles.push(Self::Skirmisher); + } + if is_sniper(cr_core, cr_extra, cr_combat, scales, dmg_scales_regex).is_some_and(|x| x) { + roles.push(Self::Sniper) + } + if is_soldier(cr_core, cr_extra, cr_combat, scales, dmg_scales_regex).is_some_and(|x| x) { + roles.push(Self::Soldier); + } + if is_spellcaster( + cr_core, + cr_spells, + cr_combat, + cr_extra, + scales, + dmg_scales_regex, + ) + .is_some_and(|x| x) + { + roles.push(Self::SpellCaster) + } + + roles + } +} +// Brute +fn is_brute( + cr_core: &CreatureCoreData, + cr_extra: &CreatureExtraData, + cr_combat: &CreatureCombatData, + scales: &CreatureScales, + re: &Regex, +) -> Option { + let lvl = cr_core.base_level; + let per_scales = scales.perception_scales.get(&lvl)?; + // low Perception; + if !(per_scales.low..per_scales.moderate).contains(&(cr_extra.perception as i64)) { + return Some(false); + } + let ability_scales = scales.ability_scales.get(&lvl)?; + // high or extreme Str modifier, + if cr_extra.ability_scores.strength < ability_scales.high as i8 { + return Some(false); + } + // high to moderate Con modifier, TODO check with the shortcut of extreme + if !(ability_scales.moderate..ability_scales.extreme?) + .contains(&(cr_extra.ability_scores.constitution as i64)) + { + return Some(false); + } + // low or lower mental modifiers; + if cr_extra.ability_scores.intelligence >= ability_scales.moderate as i8 + && cr_extra.ability_scores.wisdom >= ability_scales.moderate as i8 + && cr_extra.ability_scores.charisma >= ability_scales.moderate as i8 + { + return Some(false); + } + let saving_scales = scales.saving_throw_scales.get(&lvl)?; + // Low or lower Reflex + if cr_combat.saving_throws.reflex >= saving_scales.moderate as i8 { + return Some(false); + } + // high Fortitude, + if !(saving_scales.high..saving_scales.extreme) + .contains(&(cr_combat.saving_throws.fortitude as i64)) + { + return Some(false); + } + // Low will, + if !(saving_scales.low..saving_scales.moderate).contains(&(cr_combat.saving_throws.will as i64)) + { + return Some(false); + } + let ac_scales = scales.ac_scales.get(&lvl)?; + // moderate or low AC; + if cr_combat.ac >= ac_scales.high as i8 { + return Some(false); + } + // high HP; + let hp_scales = scales.hp_scales.get(&lvl)?; + if cr_core.hp < hp_scales.high_lb as i16 || cr_core.hp > hp_scales.high_ub as i16 { + return Some(false); + } + let atk_bonus_scales = scales.strike_bonus_scales.get(&lvl)?; + let dmg_scales = scales.strike_dmg_scales.get(&lvl)?; + + let scales_extreme_avg = re + .captures(dmg_scales.extreme.as_str())? + .get(1)? + .as_str() + .parse::() + .ok()?; + let scales_high_avg = re + .captures(dmg_scales.high.as_str())? + .get(1)? + .as_str() + .parse::() + .ok()?; + // high attack bonus and high damage OR moderate attack bonus and extreme damage + if cr_combat.weapons.iter().any(|curr_wp| { + !((atk_bonus_scales.high..atk_bonus_scales.extreme).contains(&curr_wp.to_hit_bonus) + && curr_wp + .get_avg_dmg() + .is_some_and(|x| (scales_high_avg..scales_extreme_avg).contains(&x)) + || (atk_bonus_scales.moderate..atk_bonus_scales.high).contains(&curr_wp.to_hit_bonus) + && curr_wp + .get_avg_dmg() + .is_some_and(|x| x >= scales_extreme_avg)) + }) { + return Some(false); + } + Some(true) +} + +// Sniper +fn is_sniper( + cr_core: &CreatureCoreData, + cr_extra: &CreatureExtraData, + cr_combat: &CreatureCombatData, + scales: &CreatureScales, + re: &Regex, +) -> Option { + let lvl = cr_core.base_level; + let per_scales = scales.perception_scales.get(&lvl)?; + // high Perception; + if !(per_scales.high..per_scales.extreme).contains(&(cr_extra.perception as i64)) { + return Some(false); + } + let ability_scales = scales.ability_scales.get(&lvl)?; + // high Dex modifier; + if !(ability_scales.high..ability_scales.extreme?) + .contains(&(cr_extra.ability_scores.dexterity as i64)) + { + return Some(false); + } + let saving_scales = scales.saving_throw_scales.get(&lvl)?; + // low Fortitude + if !(saving_scales.low..saving_scales.moderate) + .contains(&(cr_combat.saving_throws.fortitude as i64)) + { + return Some(false); + } + // high Reflex; + if !(saving_scales.high..saving_scales.extreme) + .contains(&(cr_combat.saving_throws.reflex as i64)) + { + return Some(false); + } + + let hp_scales = scales.hp_scales.get(&lvl)?; + // moderate to low HP; + if !(hp_scales.low_lb..hp_scales.moderate_ub + 1).contains(&(cr_core.hp as i64)) { + return Some(false); + } + let atk_bonus_scales = scales.strike_bonus_scales.get(&lvl)?; + let dmg_scales = scales.strike_dmg_scales.get(&lvl)?; + let scales_extreme_avg = re + .captures(dmg_scales.extreme.as_str())? + .get(1)? + .as_str() + .parse::() + .ok()?; + let scales_high_avg = re + .captures(dmg_scales.high.as_str())? + .get(1)? + .as_str() + .parse::() + .ok()?; + // ranged Strikes have high attack bonus and damage or + // moderate attack bonus and extreme damage (melee Strikes are weaker) + if !cr_combat.weapons.iter().any(|curr_wp| { + curr_wp.wp_type.to_uppercase() == *"RANGED" + && ((atk_bonus_scales.high..atk_bonus_scales.extreme).contains(&curr_wp.to_hit_bonus) + && curr_wp + .get_avg_dmg() + .is_some_and(|x| (scales_high_avg..scales_extreme_avg).contains(&x)) + || (atk_bonus_scales.moderate..atk_bonus_scales.high) + .contains(&curr_wp.to_hit_bonus) + && curr_wp + .get_avg_dmg() + .is_some_and(|x| x >= scales_extreme_avg)) + }) { + return Some(false); + } + Some(true) +} +// Skirmisher +fn is_skirmisher( + cr_core: &CreatureCoreData, + cr_extra: &CreatureExtraData, + cr_combat: &CreatureCombatData, + scales: &CreatureScales, +) -> Option { + let lvl = cr_core.base_level; + let ability_scales = scales.ability_scales.get(&lvl)?; + // high Dex modifier; + if !(ability_scales.high..ability_scales.extreme?) + .contains(&(cr_extra.ability_scores.dexterity as i64)) + { + return Some(false); + } + let saving_scales = scales.saving_throw_scales.get(&lvl)?; + // low Fortitude + if !(saving_scales.low..saving_scales.moderate) + .contains(&(cr_combat.saving_throws.fortitude as i64)) + { + return Some(false); + } + // high Reflex; + if !(saving_scales.high..saving_scales.extreme) + .contains(&(cr_combat.saving_throws.reflex as i64)) + { + return Some(false); + } + // TODO: higher Speed than typical + + Some(true) +} +// Soldier +pub fn is_soldier( + cr_core: &CreatureCoreData, + cr_extra: &CreatureExtraData, + cr_combat: &CreatureCombatData, + scales: &CreatureScales, + re: &Regex, +) -> Option { + let lvl = cr_core.base_level; + let ability_scales = scales.ability_scales.get(&lvl)?; + // high Str modifier; + if !(ability_scales.high..ability_scales.extreme?) + .contains(&(cr_extra.ability_scores.strength as i64)) + { + return Some(false); + } + let ac_scales = scales.ac_scales.get(&lvl)?; + // high to extreme AC; + if !(ac_scales.high..ac_scales.extreme).contains(&(cr_combat.ac as i64)) { + return Some(false); + } + let saving_scales = scales.saving_throw_scales.get(&lvl)?; + // high Fortitude; + if !(saving_scales.high..saving_scales.extreme) + .contains(&(cr_combat.saving_throws.fortitude as i64)) + { + return Some(false); + } + let atk_bonus_scales = scales.strike_bonus_scales.get(&lvl)?; + let dmg_scales = scales.strike_dmg_scales.get(&lvl)?; + let scales_extreme_avg = re + .captures(dmg_scales.extreme.as_str())? + .get(1)? + .as_str() + .parse::() + .ok()?; + let scales_high_avg = re + .captures(dmg_scales.high.as_str())? + .get(1)? + .as_str() + .parse::() + .ok()?; + // high attack bonus and high damage; + if !cr_combat.weapons.iter().any(|curr_wp| { + (atk_bonus_scales.high..atk_bonus_scales.extreme).contains(&curr_wp.to_hit_bonus) + && curr_wp + .get_avg_dmg() + .is_some_and(|x| (scales_high_avg..scales_extreme_avg).contains(&x)) + }) { + return Some(false); + } + if !cr_extra.actions.iter().any(|curr_act| { + curr_act.name.to_uppercase() == "ATTACK OF OPPORTUNITY" + || (curr_act.slug.is_none() + || curr_act.slug.clone().unwrap().to_uppercase() == "ATTACK-OF-OPPORTUNITY") + }) { + return Some(false); + } + // TODO: Attack of Opportunity **OR** other tactical abilities + Some(true) +} + +// Magical Striker +pub fn is_magical_striker( + cr_core: &CreatureCoreData, + cr_spell: &CreatureSpellCasterData, + cr_combat: &CreatureCombatData, + scales: &CreatureScales, + re: &Regex, +) -> Option { + let lvl = cr_core.base_level; + let atk_bonus_scales = scales.strike_bonus_scales.get(&lvl)?; + let dmg_scales = scales.strike_dmg_scales.get(&lvl)?; + let scales_extreme_avg = re + .captures(dmg_scales.extreme.as_str())? + .get(1)? + .as_str() + .parse::() + .ok()?; + let scales_high_avg = re + .captures(dmg_scales.high.as_str())? + .get(1)? + .as_str() + .parse::() + .ok()?; + // high attack bonus and high damage; + if !cr_combat.weapons.iter().any(|curr_wp| { + (atk_bonus_scales.high..atk_bonus_scales.extreme).contains(&curr_wp.to_hit_bonus) + && curr_wp + .get_avg_dmg() + .is_some_and(|x| (scales_high_avg..scales_extreme_avg).contains(&x)) + }) { + return Some(false); + } + let spell_dc = scales.spell_dc_and_atk_scales.get(&lvl)?; + // moderate to high spell DCs; + if !cr_spell + .spell_caster_entry + .spell_casting_dc_mod + .is_some_and(|x| (spell_dc.moderate_dc..spell_dc.high_dc).contains(&(x as i64))) + { + return Some(false); + } + // either a scattering of innate spells or prepared or spontaneous spells up to half the creature’s level (rounded up) minus 1 + Some(true) +} + +// Skill Paragon +fn is_skill_paragon( + cr_core: &CreatureCoreData, + cr_extra: &CreatureExtraData, + cr_combat: &CreatureCombatData, + scales: &CreatureScales, +) -> Option { + let lvl = cr_core.base_level; + let ability_scales = scales.ability_scales.get(&lvl)?; + scales.skill_scales.get(&lvl)?; + let best_skill = cr_extra.skills.iter().map(|x| x.modifier).max()?; + // high or extreme attribute modifier matching its best skills; + if !(ability_scales.high..ability_scales.extreme?).contains(&best_skill) { + return Some(false); + }; + let saving_scales = scales.saving_throw_scales.get(&lvl)?; + // typically high Reflex or Will and low Fortitude; + if !(((saving_scales.high..saving_scales.extreme) + .contains(&(cr_combat.saving_throws.reflex as i64)) + || (saving_scales.high..saving_scales.extreme) + .contains(&(cr_combat.saving_throws.will as i64))) + && (saving_scales.low..saving_scales.moderate) + .contains(&(cr_combat.saving_throws.fortitude as i64))) + { + return Some(false); + } + // many skills at moderate or high and potentially one or two extreme skills; + // Many is kinda up in the air, I'll set 70% + let cr_skill_amount = cr_extra.skills.length() * 70 / 100; + // if there aren't at least 70% of skill in the moderate-high range, exit + if !cr_extra + .skills + .iter() + .filter(|x| (saving_scales.moderate..saving_scales.high).contains(&x.modifier)) + .count() as u64 + >= cr_skill_amount + { + return Some(false); + } + // TODO: at least one special ability to use the creature's skills in combat + Some(true) +} +// Spellcaster +fn is_spellcaster( + cr_core: &CreatureCoreData, + cr_spell: &CreatureSpellCasterData, + cr_combat: &CreatureCombatData, + cr_extra: &CreatureExtraData, + scales: &CreatureScales, + re: &Regex, +) -> Option { + let lvl = cr_core.base_level; + let saving_scales = scales.saving_throw_scales.get(&lvl)?; + // low Fortitude, + if !(saving_scales.low..saving_scales.moderate) + .contains(&(cr_combat.saving_throws.fortitude as i64)) + { + return Some(false); + } + // high Will; + if !(saving_scales.high..saving_scales.extreme).contains(&(cr_combat.saving_throws.will as i64)) + { + return Some(false); + } + // low HP; + let hp_scales = scales.hp_scales.get(&lvl)?; + if cr_core.hp < hp_scales.low_lb as i16 || cr_core.hp > hp_scales.low_ub as i16 { + return Some(false); + } + let atk_bonus_scales = scales.strike_bonus_scales.get(&lvl)?; + let dmg_scales = scales.strike_dmg_scales.get(&lvl)?; + let scales_high_avg = re + .captures(dmg_scales.high.as_str())? + .get(1)? + .as_str() + .parse::() + .ok()?; + let scales_low_avg = re + .captures(dmg_scales.low.as_str())? + .get(1)? + .as_str() + .parse::() + .ok()?; + + // low attack bonus and moderate or low damage; + if cr_combat.weapons.iter().any(|curr_wp| { + (atk_bonus_scales.low..atk_bonus_scales.moderate).contains(&curr_wp.to_hit_bonus) + && curr_wp + .get_avg_dmg() + .is_some_and(|x| (scales_low_avg..scales_high_avg).contains(&x)) + }) { + return Some(false); + } + // high or extreme spell DCs; + let spells_dc_and_atk_scales = scales.spell_dc_and_atk_scales.get(&lvl)?; + if !(spells_dc_and_atk_scales.high_dc..spells_dc_and_atk_scales.extreme_dc) + .contains(&(cr_spell.spell_caster_entry.spell_casting_dc_mod? as i64)) + { + return Some(false); + } + // 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() { + return Some(false); + } + let ability_scales = scales.ability_scales.get(&lvl)?; + // high or extreme modifier for the corresponding mental ability; + if !(cr_extra.ability_scores.wisdom as i64 > ability_scales.high + || cr_extra.ability_scores.intelligence as i64 > ability_scales.high + || cr_extra.ability_scores.charisma as i64 > ability_scales.high) + { + return Some(false); + } + Some(true) +} + +impl fmt::Display for CreatureRole { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + CreatureRole::Brute => { + write!(f, "{:?}", "Brute") + } + CreatureRole::MagicalStriker => { + write!(f, "{:?}", "Magical Striker") + } + CreatureRole::SkillParagon => { + write!(f, "{:?}", "Skill Paragon") + } + CreatureRole::Skirmisher => { + write!(f, "{:?}", "Skirmisher") + } + CreatureRole::Sniper => { + write!(f, "{:?}", "Sniper") + } + CreatureRole::Soldier => { + write!(f, "{:?}", "Soldier") + } + CreatureRole::SpellCaster => { + write!(f, "{:?}", "Spellcaster") + } + } + } +} diff --git a/src/models/creature_metadata/mod.rs b/src/models/creature_metadata/mod.rs index e80c979..57a4d9d 100644 --- a/src/models/creature_metadata/mod.rs +++ b/src/models/creature_metadata/mod.rs @@ -1,4 +1,5 @@ pub mod alignment_enum; +pub mod creature_role; pub mod rarity_enum; pub mod size_enum; pub mod type_enum; diff --git a/src/models/db/raw_creature.rs b/src/models/db/raw_creature.rs index da25ce4..681045f 100644 --- a/src/models/db/raw_creature.rs +++ b/src/models/db/raw_creature.rs @@ -37,5 +37,11 @@ pub struct RawCreature { pub size: SizeEnum, pub cr_type: CreatureTypeEnum, pub family: Option, - pub is_spell_caster: bool, + + pub spell_casting_name: Option, + pub is_spell_casting_flexible: Option, + pub type_of_spell_caster: Option, + pub spell_casting_dc_mod: Option, + pub spell_casting_atk_mod: Option, + pub spell_casting_tradition: Option, } diff --git a/src/models/items/action.rs b/src/models/items/action.rs new file mode 100644 index 0000000..53c873e --- /dev/null +++ b/src/models/items/action.rs @@ -0,0 +1,21 @@ +use crate::models::creature_metadata::rarity_enum::RarityEnum; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] +pub struct Action { + pub id: i64, + pub name: String, + pub action_type: String, + pub n_of_actions: Option, + pub category: String, + pub description: String, + + pub license: String, + pub remaster: bool, + pub source: String, + + pub slug: Option, + pub rarity: RarityEnum, + pub creature_id: i64, +} diff --git a/src/models/items/mod.rs b/src/models/items/mod.rs index 16c2fd2..7cfe576 100644 --- a/src/models/items/mod.rs +++ b/src/models/items/mod.rs @@ -1,2 +1,5 @@ +pub mod action; +pub mod skill; pub mod spell; +pub mod spell_caster_entry; pub mod weapon; diff --git a/src/models/items/skill.rs b/src/models/items/skill.rs new file mode 100644 index 0000000..dd847c6 --- /dev/null +++ b/src/models/items/skill.rs @@ -0,0 +1,12 @@ +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] +pub struct Skill { + pub name: String, + pub description: Option, + pub modifier: i64, + pub proficiency: i64, + // pub publication_info: PublicationInfo, + // pub variant_label: Vec, +} diff --git a/src/models/items/spell_caster_entry.rs b/src/models/items/spell_caster_entry.rs new file mode 100644 index 0000000..d17fdbc --- /dev/null +++ b/src/models/items/spell_caster_entry.rs @@ -0,0 +1,12 @@ +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] +pub struct SpellCasterEntry { + pub spell_casting_name: Option, + pub is_spell_casting_flexible: Option, + pub type_of_spell_caster: Option, + pub spell_casting_dc_mod: Option, + pub spell_casting_atk_mod: Option, + pub spell_casting_tradition: Option, +} diff --git a/src/models/items/weapon.rs b/src/models/items/weapon.rs index ec6e965..7addfbf 100644 --- a/src/models/items/weapon.rs +++ b/src/models/items/weapon.rs @@ -39,3 +39,26 @@ pub struct Weapon { pub wp_type: String, pub creature_id: i64, } + +impl Weapon { + pub fn get_avg_dmg(&self) -> Option { + // avg dice value is + // AVG = (((M+1)/2)∗N)+B + // + // M = max value of the dice + // N = number of dices + // B = bonus dmg + let m = self + .die_size + .clone()? + .split_once('d')? + .1 + .parse::() + .ok()?; + let n = self.n_of_dices? as f64; + let b = self.bonus_dmg? as f64; + + let avg: f64 = (((m + 1.) / 2.) * n) + b; + Some(avg.floor() as i64) + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs index 1216550..d14456b 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,4 +1,5 @@ pub mod creature; +pub mod creature_component; pub mod creature_fields_enum; pub mod creature_filter_enum; pub mod creature_metadata; @@ -7,3 +8,4 @@ pub mod encounter_structs; pub mod items; pub mod response_data; pub mod routers_validator_structs; +pub mod scales_struct; diff --git a/src/models/response_data.rs b/src/models/response_data.rs index 1a1b429..ddb36e0 100644 --- a/src/models/response_data.rs +++ b/src/models/response_data.rs @@ -1,4 +1,10 @@ -use crate::models::creature::{CoreCreatureData, Creature, ExtraCreatureData, VariantCreatureData}; +use crate::models::creature::Creature; +use crate::models::creature_component::creature_combat::CreatureCombatData; +use crate::models::creature_component::creature_core::CreatureCoreData; +use crate::models::creature_component::creature_extra::CreatureExtraData; +use crate::models::creature_component::creature_info::CreatureInfo; +use crate::models::creature_component::creature_spell_caster::CreatureSpellCasterData; +use crate::models::creature_component::creature_variant::CreatureVariantData; use serde::{Deserialize, Serialize}; use utoipa::{IntoParams, ToSchema}; use validator::Validate; @@ -8,13 +14,18 @@ pub struct ResponseData { pub essential_data: bool, pub variant_data: bool, pub extra_data: bool, + pub combat_data: bool, + pub spell_casting_data: bool, } #[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] pub struct ResponseCreature { - pub core_data: Option, - pub variant_data: Option, - pub extra_data: Option, + pub core_data: Option, + pub variant_data: Option, + pub extra_data: Option, + pub combat_data: Option, + pub spell_caster_data: Option, + pub info: Option, } impl From<(Creature, &ResponseData)> for ResponseCreature { @@ -37,6 +48,17 @@ impl From<(Creature, &ResponseData)> for ResponseCreature { } else { None }, + info: if rd.extra_data { Some(cr.info) } else { None }, + spell_caster_data: if rd.spell_casting_data { + Some(cr.spell_caster_data) + } else { + None + }, + combat_data: if rd.combat_data { + Some(cr.combat_data) + } else { + None + }, } } } diff --git a/src/models/scales_struct/ability_scales.rs b/src/models/scales_struct/ability_scales.rs new file mode 100644 index 0000000..e09baf8 --- /dev/null +++ b/src/models/scales_struct/ability_scales.rs @@ -0,0 +1,9 @@ +#[derive(Default, Eq, PartialEq, Clone)] +pub struct AbilityScales { + pub id: i64, + pub level: i64, + pub extreme: Option, + pub high: i64, + pub moderate: i64, + pub low: i64, +} diff --git a/src/models/scales_struct/ac_scales.rs b/src/models/scales_struct/ac_scales.rs new file mode 100644 index 0000000..3049656 --- /dev/null +++ b/src/models/scales_struct/ac_scales.rs @@ -0,0 +1,9 @@ +#[derive(Default, Eq, PartialEq, Clone)] +pub struct AcScales { + pub id: i64, + pub level: i64, + pub extreme: i64, + pub high: i64, + pub moderate: i64, + pub low: i64, +} diff --git a/src/models/scales_struct/area_dmg_scales.rs b/src/models/scales_struct/area_dmg_scales.rs new file mode 100644 index 0000000..51c828a --- /dev/null +++ b/src/models/scales_struct/area_dmg_scales.rs @@ -0,0 +1,7 @@ +#[derive(Default, Eq, PartialEq, Clone)] +pub struct AreaDmgScales { + pub id: i64, + pub level: i64, + pub unlimited_use: String, + pub limited_use: String, +} diff --git a/src/models/scales_struct/creature_scales.rs b/src/models/scales_struct/creature_scales.rs new file mode 100644 index 0000000..344b31a --- /dev/null +++ b/src/models/scales_struct/creature_scales.rs @@ -0,0 +1,29 @@ +use crate::models::scales_struct::ability_scales::AbilityScales; +use crate::models::scales_struct::ac_scales::AcScales; +use crate::models::scales_struct::area_dmg_scales::AreaDmgScales; +use crate::models::scales_struct::hp_scales::HpScales; +use crate::models::scales_struct::item_scales::ItemScales; +use crate::models::scales_struct::perception_scales::PerceptionScales; +use crate::models::scales_struct::res_weak_scales::ResWeakScales; +use crate::models::scales_struct::saving_throw_scales::SavingThrowScales; +use crate::models::scales_struct::skill_scales::SkillScales; +use crate::models::scales_struct::spell_dc_and_atk_scales::SpellDcAndAtkScales; +use crate::models::scales_struct::strike_bonus_scales::StrikeBonusScales; +use crate::models::scales_struct::strike_dmg_scales::StrikeDmgScales; +use std::collections::HashMap; + +#[derive(Default, Eq, PartialEq, Clone)] +pub struct CreatureScales { + pub ability_scales: HashMap, + pub ac_scales: HashMap, + pub area_dmg_scales: HashMap, + pub hp_scales: HashMap, + pub item_scales: HashMap, + pub perception_scales: HashMap, + pub res_weak_scales: HashMap, + pub saving_throw_scales: HashMap, + pub skill_scales: HashMap, + pub spell_dc_and_atk_scales: HashMap, + pub strike_bonus_scales: HashMap, + pub strike_dmg_scales: HashMap, +} diff --git a/src/models/scales_struct/hp_scales.rs b/src/models/scales_struct/hp_scales.rs new file mode 100644 index 0000000..0d76f11 --- /dev/null +++ b/src/models/scales_struct/hp_scales.rs @@ -0,0 +1,11 @@ +#[derive(Default, Eq, PartialEq, Clone)] +pub struct HpScales { + pub id: i64, + pub level: i64, + pub high_ub: i64, + pub high_lb: i64, + pub moderate_ub: i64, + pub moderate_lb: i64, + pub low_ub: i64, + pub low_lb: i64, +} diff --git a/src/models/scales_struct/item_scales.rs b/src/models/scales_struct/item_scales.rs new file mode 100644 index 0000000..2271e28 --- /dev/null +++ b/src/models/scales_struct/item_scales.rs @@ -0,0 +1,6 @@ +#[derive(Default, Eq, PartialEq, Clone)] +pub struct ItemScales { + pub id: i64, + pub cr_level: String, + pub safe_item_level: String, +} diff --git a/src/models/scales_struct/mod.rs b/src/models/scales_struct/mod.rs new file mode 100644 index 0000000..27738a3 --- /dev/null +++ b/src/models/scales_struct/mod.rs @@ -0,0 +1,13 @@ +pub mod ability_scales; +pub mod ac_scales; +pub mod area_dmg_scales; +pub mod creature_scales; +pub mod hp_scales; +pub mod item_scales; +pub mod perception_scales; +pub mod res_weak_scales; +pub mod saving_throw_scales; +pub mod skill_scales; +pub mod spell_dc_and_atk_scales; +pub mod strike_bonus_scales; +pub mod strike_dmg_scales; diff --git a/src/models/scales_struct/perception_scales.rs b/src/models/scales_struct/perception_scales.rs new file mode 100644 index 0000000..a2b1904 --- /dev/null +++ b/src/models/scales_struct/perception_scales.rs @@ -0,0 +1,10 @@ +#[derive(Default, Eq, PartialEq, Clone)] +pub struct PerceptionScales { + pub id: i64, + pub level: i64, + pub extreme: i64, + pub high: i64, + pub moderate: i64, + pub low: i64, + pub terrible: i64, +} diff --git a/src/models/scales_struct/res_weak_scales.rs b/src/models/scales_struct/res_weak_scales.rs new file mode 100644 index 0000000..8cbf6c3 --- /dev/null +++ b/src/models/scales_struct/res_weak_scales.rs @@ -0,0 +1,7 @@ +#[derive(Default, Eq, PartialEq, Clone)] +pub struct ResWeakScales { + pub id: i64, + pub level: i64, + pub max: i64, + pub min: i64, +} diff --git a/src/models/scales_struct/saving_throw_scales.rs b/src/models/scales_struct/saving_throw_scales.rs new file mode 100644 index 0000000..ca7d159 --- /dev/null +++ b/src/models/scales_struct/saving_throw_scales.rs @@ -0,0 +1,10 @@ +#[derive(Default, Eq, PartialEq, Clone)] +pub struct SavingThrowScales { + pub id: i64, + pub level: i64, + pub extreme: i64, + pub high: i64, + pub moderate: i64, + pub low: i64, + pub terrible: i64, +} diff --git a/src/models/scales_struct/skill_scales.rs b/src/models/scales_struct/skill_scales.rs new file mode 100644 index 0000000..66f0990 --- /dev/null +++ b/src/models/scales_struct/skill_scales.rs @@ -0,0 +1,10 @@ +#[derive(Default, Eq, PartialEq, Clone)] +pub struct SkillScales { + pub id: i64, + pub level: i64, + pub extreme: i64, + pub high: i64, + pub moderate: i64, + pub low_ub: i64, + pub low_lb: i64, +} diff --git a/src/models/scales_struct/spell_dc_and_atk_scales.rs b/src/models/scales_struct/spell_dc_and_atk_scales.rs new file mode 100644 index 0000000..7999064 --- /dev/null +++ b/src/models/scales_struct/spell_dc_and_atk_scales.rs @@ -0,0 +1,11 @@ +#[derive(Default, Eq, PartialEq, Clone)] +pub struct SpellDcAndAtkScales { + pub id: i64, + pub level: i64, + pub extreme_dc: i64, + pub extreme_atk_bonus: i64, + pub high_dc: i64, + pub high_atk_bonus: i64, + pub moderate_dc: i64, + pub moderate_atk_bonus: i64, +} diff --git a/src/models/scales_struct/strike_bonus_scales.rs b/src/models/scales_struct/strike_bonus_scales.rs new file mode 100644 index 0000000..cb5542e --- /dev/null +++ b/src/models/scales_struct/strike_bonus_scales.rs @@ -0,0 +1,9 @@ +#[derive(Default, Eq, PartialEq, Clone)] +pub struct StrikeBonusScales { + pub id: i64, + pub level: i64, + pub extreme: i64, + pub high: i64, + pub moderate: i64, + pub low: i64, +} diff --git a/src/models/scales_struct/strike_dmg_scales.rs b/src/models/scales_struct/strike_dmg_scales.rs new file mode 100644 index 0000000..3cf98e4 --- /dev/null +++ b/src/models/scales_struct/strike_dmg_scales.rs @@ -0,0 +1,9 @@ +#[derive(Default, Eq, PartialEq, Clone)] +pub struct StrikeDmgScales { + pub id: i64, + pub level: i64, + pub extreme: String, + pub high: String, + pub moderate: String, + pub low: String, +} diff --git a/src/routes/bestiary.rs b/src/routes/bestiary.rs index bd86048..1c13a19 100644 --- a/src/routes/bestiary.rs +++ b/src/routes/bestiary.rs @@ -1,4 +1,5 @@ use crate::models::creature_metadata::alignment_enum::AlignmentEnum; +use crate::models::creature_metadata::creature_role::CreatureRole; use crate::models::creature_metadata::rarity_enum::RarityEnum; use crate::models::creature_metadata::size_enum::SizeEnum; use crate::models::creature_metadata::type_enum::CreatureTypeEnum; @@ -6,11 +7,23 @@ use crate::models::creature_metadata::variant_enum::CreatureVariant; use crate::models::response_data::ResponseCreature; use crate::models::response_data::ResponseData; -use crate::models::creature::{CoreCreatureData, ExtraCreatureData, VariantCreatureData}; +use crate::models::creature_component::creature_combat::CreatureCombatData; +use crate::models::creature_component::creature_combat::SavingThrows; +use crate::models::creature_component::creature_core::CreatureCoreData; +use crate::models::creature_component::creature_extra::AbilityScores; +use crate::models::creature_component::creature_extra::CreatureExtraData; +use crate::models::creature_component::creature_info::CreatureInfo; +use crate::models::creature_component::creature_spell_caster::CreatureSpellCasterData; +use crate::models::creature_component::creature_variant::CreatureVariantData; +use crate::models::items::action::Action; +use crate::models::items::skill::Skill; use crate::models::items::spell::Spell; +use crate::models::items::spell_caster_entry::SpellCasterEntry; use crate::models::items::weapon::Weapon; +use crate::models::creature::PublicationInfo; + use crate::models::routers_validator_structs::{FieldFilters, PaginatedRequest}; use crate::services::bestiary_service; use crate::services::bestiary_service::BestiaryResponse; @@ -60,11 +73,21 @@ pub fn init_docs(doc: &mut utoipa::openapi::OpenApi) { SizeEnum, CreatureTypeEnum, CreatureVariant, - CoreCreatureData, - VariantCreatureData, - ExtraCreatureData, + CreatureCoreData, + CreatureVariantData, + CreatureExtraData, + CreatureCombatData, + CreatureSpellCasterData, + CreatureInfo, Spell, Weapon, + SavingThrows, + PublicationInfo, + AbilityScores, + Action, + Skill, + CreatureRole, + SpellCasterEntry )) )] struct ApiDoc;