diff --git a/Cargo.toml b/Cargo.toml index b8994a6..cda1ddb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ counter = "0.6.0" ordered-float = { version = "4", features = ["serde"]} num-traits = "0.2.19" maplit = "1.0.2" +itertools = "0.13.0" regex = "1.10.5" diff --git a/Dockerfile b/Dockerfile index 8b9623a..a09ca87 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Stage 1: Build the Rust project -FROM rust:1-alpine as builder +FROM rust:1-alpine AS builder # Set the working directory in the container WORKDIR /app diff --git a/build/creature_core_db_init.rs b/build/creature_core_db_init.rs index 8de4ee1..c10204c 100644 --- a/build/creature_core_db_init.rs +++ b/build/creature_core_db_init.rs @@ -47,19 +47,18 @@ async fn create_temporary_table(conn: &Pool) -> Result<()> { ct.source, ct.remaster, CASE WHEN ct.id IN ( - SELECT creature_id FROM ( - SELECT wcat.creature_id, base_item_id FROM ITEM_CREATURE_ASSOCIATION_TABLE wcat LEFT JOIN ( - SELECT * FROM WEAPON_TABLE w1 WHERE UPPER(w1.weapon_type) = 'MELEE' - ) wt ON base_item_id = wcat.item_id - ) + SELECT wcat.creature_id + FROM WEAPON_CREATURE_ASSOCIATION_TABLE wcat LEFT JOIN ( + SELECT * FROM WEAPON_TABLE w1 WHERE UPPER(w1.weapon_type) = 'MELEE' + ) wt ON base_item_id = wcat.weapon_id ) THEN TRUE ELSE FALSE END AS is_melee, - CASE WHEN ct.id IN ( - SELECT creature_id FROM ( - SELECT wcat.creature_id, base_item_id FROM ITEM_CREATURE_ASSOCIATION_TABLE wcat LEFT JOIN ( - SELECT * FROM WEAPON_TABLE w1 WHERE UPPER(w1.weapon_type) = 'RANGED' - ) wt ON base_item_id = wcat.item_id - ) - ) THEN TRUE ELSE FALSE END AS is_ranged, + CASE WHEN ct.id IN ( + SELECT wcat.creature_id + FROM WEAPON_CREATURE_ASSOCIATION_TABLE wcat LEFT JOIN ( + SELECT * FROM WEAPON_TABLE w1 WHERE UPPER(w1.weapon_type) = 'MELEE' + ) wt ON base_item_id = wcat.weapon_id + ) + 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, diff --git a/src/db/bestiary_proxy.rs b/src/db/bestiary_proxy.rs index 080fbe9..a6df2a7 100644 --- a/src/db/bestiary_proxy.rs +++ b/src/db/bestiary_proxy.rs @@ -11,7 +11,7 @@ use crate::models::creature::creature_metadata::creature_role::CreatureRoleEnum; use crate::models::creature::creature_metadata::type_enum::CreatureTypeEnum; use crate::models::creature::creature_metadata::variant_enum::CreatureVariant; use crate::models::pf_version_enum::PathfinderVersionEnum; -use crate::models::response_data::OptionalData; +use crate::models::response_data::ResponseDataModifiers; use crate::models::routers_validator_structs::{CreatureFieldFilters, OrderEnum}; use crate::AppState; use anyhow::Result; @@ -21,28 +21,27 @@ use strum::IntoEnumIterator; pub async fn get_creature_by_id( app_state: &AppState, id: i64, - variant: &CreatureVariant, - optional_data: &OptionalData, + variant: CreatureVariant, + response_data_mods: &ResponseDataModifiers, ) -> Option { - let creature = creature_fetcher::fetch_creature_by_id(&app_state.conn, optional_data, id) + creature_fetcher::fetch_creature_by_id(&app_state.conn, variant, response_data_mods, id) .await - .ok()?; - Some(creature.convert_creature_to_variant(variant)) + .ok() } pub async fn get_weak_creature_by_id( app_state: &AppState, id: i64, - optional_data: &OptionalData, + optional_data: &ResponseDataModifiers, ) -> Option { - get_creature_by_id(app_state, id, &CreatureVariant::Weak, optional_data).await + 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, + optional_data: &ResponseDataModifiers, ) -> Option { - get_creature_by_id(app_state, id, &CreatureVariant::Elite, optional_data).await + get_creature_by_id(app_state, id, CreatureVariant::Elite, optional_data).await } pub async fn get_paginated_creatures( @@ -142,13 +141,13 @@ pub async fn get_creatures_passing_all_filters( if fetch_weak && level_vec.contains(&(core.essential.level - 1).to_string()) { creature_vec.push(Creature::from_core_with_variant( core.clone(), - &CreatureVariant::Weak, + 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, + CreatureVariant::Elite, )); } creature_vec.push(Creature::from_core(core)); @@ -241,7 +240,7 @@ async fn get_list(app_state: &AppState, variant: CreatureVariant) -> Vec creatures.into_iter().map(Creature::from_core).collect(), _ => creatures .into_iter() - .map(|cr| Creature::from_core_with_variant(cr, &variant)) + .map(|cr| Creature::from_core_with_variant(cr, variant.clone())) .collect(), }; } diff --git a/src/db/data_providers/creature_fetcher.rs b/src/db/data_providers/creature_fetcher.rs index 592f22a..becb783 100644 --- a/src/db/data_providers/creature_fetcher.rs +++ b/src/db/data_providers/creature_fetcher.rs @@ -1,5 +1,6 @@ use crate::db::data_providers::generic_fetcher::{ - fetch_armor_runes, fetch_item_traits, fetch_weapon_runes, MyString, + fetch_armor_runes, fetch_armor_traits, fetch_item_traits, fetch_shield_traits, + fetch_weapon_damage_data, fetch_weapon_runes, fetch_weapon_traits, MyString, }; use crate::db::data_providers::raw_query_builder::prepare_filtered_get_creatures_core; use crate::models::creature::creature_component::creature_combat::{ @@ -29,7 +30,7 @@ use crate::models::item::armor_struct::Armor; use crate::models::item::item_struct::Item; use crate::models::item::shield_struct::Shield; use crate::models::item::weapon_struct::Weapon; -use crate::models::response_data::OptionalData; +use crate::models::response_data::ResponseDataModifiers; 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; @@ -210,15 +211,14 @@ pub async fn fetch_creature_traits(conn: &Pool, creature_id: i64) -> Res async fn fetch_creature_weapons(conn: &Pool, creature_id: i64) -> Result> { let weapons: Vec = sqlx::query_as( " - SELECT wt.id AS weapon_id, wt.bonus_dmg, wt.to_hit_bonus, wt.dmg_type, - wt.number_of_dice, wt.die_size, wt.splash_dmg, wt.n_of_potency_runes, + SELECT wt.id AS weapon_id, wt.to_hit_bonus, wt.splash_dmg, wt.n_of_potency_runes, wt.n_of_striking_runes, wt.range, wt.reload, wt.weapon_type, wt.base_item_id, it.* - FROM WEAPON_TABLE wt LEFT JOIN ITEM_TABLE it ON it.id = wt.base_item_id - WHERE base_item_id IN ( - SELECT item_id FROM ITEM_CREATURE_ASSOCIATION_TABLE WHERE creature_id == ($1) - ) - GROUP BY it.id + FROM WEAPON_CREATURE_ASSOCIATION_TABLE ica + LEFT JOIN WEAPON_TABLE wt ON wt.id = ica.weapon_id + LEFT JOIN ITEM_TABLE it ON it.id = wt.base_item_id + WHERE ica.creature_id = ($1) + GROUP BY ica.weapon_id ORDER BY name ", ) @@ -227,13 +227,16 @@ async fn fetch_creature_weapons(conn: &Pool, creature_id: i64) -> Result .await?; let mut result_vec = Vec::new(); for mut el in weapons { - el.item_core.traits = fetch_item_traits(conn, el.item_core.id) + el.item_core.traits = fetch_weapon_traits(conn, el.weapon_data.id) .await .unwrap_or(vec![]); - el.item_core.quantity = fetch_item_quantity(conn, creature_id, el.item_core.id).await; + el.item_core.quantity = fetch_weapon_quantity(conn, creature_id, el.weapon_data.id).await; el.weapon_data.property_runes = fetch_weapon_runes(conn, el.weapon_data.id) .await .unwrap_or(vec![]); + el.weapon_data.damage_data = fetch_weapon_damage_data(conn, el.weapon_data.id) + .await + .unwrap_or(vec![]); result_vec.push(el) } Ok(result_vec) @@ -245,11 +248,11 @@ async fn fetch_creature_armors(conn: &Pool, creature_id: i64) -> Result< SELECT at.id AS armor_id, at.bonus_ac, at.check_penalty, at.dex_cap, at.n_of_potency_runes, at.n_of_resilient_runes, at.speed_penalty, at.strength_required, at.base_item_id, it.* - FROM ARMOR_TABLE at LEFT JOIN ITEM_TABLE it ON it.id = at.base_item_id - WHERE base_item_id IN ( - SELECT item_id FROM ITEM_CREATURE_ASSOCIATION_TABLE WHERE creature_id == ($1) - ) - GROUP BY it.id + FROM ARMOR_CREATURE_ASSOCIATION_TABLE aca + LEFT JOIN ARMOR_TABLE at ON at.id = aca.armor_id + LEFT JOIN ITEM_TABLE it ON it.id = at.base_item_id + WHERE aca.creature_id = ($1) + GROUP BY aca.armor_id ORDER BY name ", ) @@ -258,10 +261,10 @@ async fn fetch_creature_armors(conn: &Pool, creature_id: i64) -> Result< .await?; let mut result_vec = Vec::new(); for mut el in armors { - el.item_core.traits = fetch_item_traits(conn, el.item_core.id) + el.item_core.traits = fetch_armor_traits(conn, el.armor_data.id) .await .unwrap_or(vec![]); - el.item_core.quantity = fetch_item_quantity(conn, creature_id, el.item_core.id).await; + el.item_core.quantity = fetch_armor_quantity(conn, creature_id, el.armor_data.id).await; el.armor_data.property_runes = fetch_armor_runes(conn, el.armor_data.id) .await .unwrap_or(vec![]); @@ -275,11 +278,11 @@ async fn fetch_creature_shields(conn: &Pool, creature_id: i64) -> Result " SELECT st.id AS shield_id, st.bonus_ac, st.n_of_reinforcing_runes, st.speed_penalty, it.* - FROM SHIELD_TABLE st LEFT JOIN ITEM_TABLE it ON it.id = st.base_item_id - WHERE base_item_id IN ( - SELECT item_id FROM ITEM_CREATURE_ASSOCIATION_TABLE WHERE creature_id == ($1) - ) - GROUP BY it.id + FROM SHIELD_CREATURE_ASSOCIATION_TABLE sca + LEFT JOIN SHIELD_TABLE st ON st.id = sca.shield_id + LEFT JOIN ITEM_TABLE it ON it.id = st.base_item_id + WHERE sca.creature_id = ($1) + GROUP BY sca.shield_id ORDER BY name ", ) @@ -288,10 +291,10 @@ async fn fetch_creature_shields(conn: &Pool, creature_id: i64) -> Result .await?; let mut result_vec = Vec::new(); for mut el in shields { - el.item_core.traits = fetch_item_traits(conn, el.item_core.id) + el.item_core.traits = fetch_shield_traits(conn, el.shield_data.id) .await .unwrap_or(vec![]); - el.item_core.quantity = fetch_item_quantity(conn, creature_id, el.item_core.id).await; + el.item_core.quantity = fetch_shield_quantity(conn, creature_id, el.shield_data.id).await; result_vec.push(el) } Ok(result_vec) @@ -336,6 +339,60 @@ async fn fetch_item_quantity(conn: &Pool, creature_id: i64, item_id: i64 } } +/// Quantities are present ONLY for creature's weapons. +/// It needs to be fetched from the association table. +/// It defaults to 1 if error are found +async fn fetch_weapon_quantity(conn: &Pool, creature_id: i64, weapon_id: i64) -> i64 { + match sqlx::query!( + "SELECT quantity FROM WEAPON_CREATURE_ASSOCIATION_TABLE WHERE + creature_id == ($1) AND weapon_id == ($2)", + creature_id, + weapon_id + ) + .fetch_one(conn) + .await + { + Ok(r) => r.quantity, + Err(_) => 1, + } +} + +/// Quantities are present ONLY for creature's shields. +/// It needs to be fetched from the association table. +/// It defaults to 1 if error are found +async fn fetch_shield_quantity(conn: &Pool, creature_id: i64, shield_id: i64) -> i64 { + match sqlx::query!( + "SELECT quantity FROM SHIELD_CREATURE_ASSOCIATION_TABLE WHERE + creature_id == ($1) AND shield_id == ($2)", + creature_id, + shield_id + ) + .fetch_one(conn) + .await + { + Ok(r) => r.quantity, + Err(_) => 1, + } +} + +/// Quantities are present ONLY for creature's armors. +/// It needs to be fetched from the association table. +/// It defaults to 1 if error are found +async fn fetch_armor_quantity(conn: &Pool, creature_id: i64, armor_id: i64) -> i64 { + match sqlx::query!( + "SELECT quantity FROM ARMOR_CREATURE_ASSOCIATION_TABLE WHERE + creature_id == ($1) AND armor_id == ($2)", + creature_id, + armor_id + ) + .fetch_one(conn) + .await + { + Ok(r) => r.quantity, + Err(_) => 1, + } +} + async fn fetch_creature_actions(conn: &Pool, creature_id: i64) -> Result> { Ok(sqlx::query_as!( Action, @@ -430,34 +487,41 @@ pub async fn fetch_traits_associated_with_creatures(conn: &Pool) -> Resu pub async fn fetch_creature_by_id( conn: &Pool, - optional_data: &OptionalData, + variant: CreatureVariant, + response_data_mods: &ResponseDataModifiers, 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 { + let cr = Creature { core_data, variant_data: CreatureVariantData { variant: CreatureVariant::Base, level, archive_link, }, - extra_data: if optional_data.extra_data.is_some_and(|x| x) { + extra_data: if response_data_mods.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) { + combat_data: if response_data_mods.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) { + spell_caster_data: if response_data_mods.spell_casting_data.is_some_and(|x| x) { Some(fetch_creature_spell_caster_data(conn, id).await?) } else { None }, + } + .convert_creature_to_variant(variant); + Ok(if response_data_mods.is_pwl_on.unwrap_or(false) { + cr.convert_creature_to_pwl() + } else { + cr }) } diff --git a/src/db/data_providers/generic_fetcher.rs b/src/db/data_providers/generic_fetcher.rs index ad57d85..4075ade 100644 --- a/src/db/data_providers/generic_fetcher.rs +++ b/src/db/data_providers/generic_fetcher.rs @@ -1,3 +1,4 @@ +use crate::models::item::weapon_struct::DamageData; use anyhow::Result; use sqlx::{FromRow, Pool, Sqlite}; @@ -33,6 +34,51 @@ pub async fn fetch_item_traits(conn: &Pool, item_id: i64) -> Result, weapon_id: i64) -> Result> { + Ok(sqlx::query_as!( + MyString, + "SELECT name AS my_str + FROM TRAIT_TABLE INTERSECT + SELECT trait_id FROM TRAIT_WEAPON_ASSOCIATION_TABLE WHERE weapon_id == ($1)", + weapon_id + ) + .fetch_all(conn) + .await? + .into_iter() + .map(|x| x.my_str) + .collect()) +} + +pub async fn fetch_shield_traits(conn: &Pool, shield_id: i64) -> Result> { + Ok(sqlx::query_as!( + MyString, + "SELECT name AS my_str + FROM TRAIT_TABLE INTERSECT + SELECT trait_id FROM TRAIT_SHIELD_ASSOCIATION_TABLE WHERE shield_id == ($1)", + shield_id + ) + .fetch_all(conn) + .await? + .into_iter() + .map(|x| x.my_str) + .collect()) +} + +pub async fn fetch_armor_traits(conn: &Pool, armor_id: i64) -> Result> { + Ok(sqlx::query_as!( + MyString, + "SELECT name AS my_str + FROM TRAIT_TABLE INTERSECT + SELECT trait_id FROM TRAIT_ARMOR_ASSOCIATION_TABLE WHERE armor_id == ($1)", + armor_id + ) + .fetch_all(conn) + .await? + .into_iter() + .map(|x| x.my_str) + .collect()) +} + pub async fn fetch_weapon_runes(conn: &Pool, wp_id: i64) -> Result> { Ok(sqlx::query_as!( MyString, @@ -48,6 +94,18 @@ pub async fn fetch_weapon_runes(conn: &Pool, wp_id: i64) -> Result, wp_id: i64) -> Result> { + Ok(sqlx::query_as( + "SELECT id, bonus_dmg, dmg_type, number_of_dice, die_size + FROM WEAPON_DAMAGE_TABLE dm RIGHT JOIN ( + SELECT id AS wp_id FROM WEAPON_TABLE WHERE wp_id == ($1) + ) ON wp_id == dm.weapon_id", + ) + .bind(wp_id) + .fetch_all(conn) + .await?) +} + pub async fn fetch_armor_runes(conn: &Pool, wp_id: i64) -> Result> { Ok(sqlx::query_as!( MyString, diff --git a/src/db/data_providers/raw_query_builder.rs b/src/db/data_providers/raw_query_builder.rs index 90af231..31ac795 100644 --- a/src/db/data_providers/raw_query_builder.rs +++ b/src/db/data_providers/raw_query_builder.rs @@ -211,10 +211,28 @@ fn prepare_item_subquery( } fn prepare_get_id_matching_item_type_query(item_type: &ItemTypeEnum) -> String { + let (item_id_field, type_query) = match item_type { + ItemTypeEnum::Consumable | ItemTypeEnum::Equipment => { + ("id", format!("AND UPPER(item_type) = UPPER('{item_type}')")) + } + // There is no need for an and statement here, we already fetch from the "private" table. + // Item instead contains a lot of item_type (it's the base for weapon/shield/etc) + ItemTypeEnum::Weapon | ItemTypeEnum::Armor | ItemTypeEnum::Shield => { + ("base_item_id", "".to_string()) + } + }; + let tass_item_id_field = match item_type { + ItemTypeEnum::Consumable | ItemTypeEnum::Equipment => "item_id", + ItemTypeEnum::Weapon => "weapon_id", + ItemTypeEnum::Armor => "armor_id", + ItemTypeEnum::Shield => "shield_id", + }; format!( - "SELECT id FROM ITEM_TABLE it - LEFT OUTER JOIN ITEM_CREATURE_ASSOCIATION_TABLE icat ON it.id = icat.item_id - WHERE icat.item_id IS NULL - AND UPPER(item_type) = UPPER('{item_type}')" + " + SELECT {item_id_field} FROM {} tmain + LEFT OUTER JOIN {} tass ON tmain.id = tass.{tass_item_id_field} + WHERE tass.{tass_item_id_field} IS NULL {type_query}", + item_type.to_db_main_table_name(), + item_type.to_db_association_table_name(), ) } diff --git a/src/db/data_providers/shop_fetcher.rs b/src/db/data_providers/shop_fetcher.rs index 5b0d1fc..d4c9c8f 100644 --- a/src/db/data_providers/shop_fetcher.rs +++ b/src/db/data_providers/shop_fetcher.rs @@ -1,5 +1,5 @@ use crate::db::data_providers::generic_fetcher::{ - fetch_armor_runes, fetch_item_traits, fetch_weapon_runes, MyString, + fetch_armor_runes, fetch_item_traits, fetch_weapon_damage_data, fetch_weapon_runes, }; use crate::db::data_providers::raw_query_builder::prepare_filtered_get_items; use crate::models::item::armor_struct::{Armor, ArmorData}; @@ -61,6 +61,7 @@ async fn fetch_weapon_by_item_id(conn: &Pool, item_id: i64) -> Result, item_id: i64) -> Result, cursor: u32, page_size: i16) -> Re LEFT OUTER JOIN ITEM_CREATURE_ASSOCIATION_TABLE icat ON it.id = icat.item_id WHERE icat.item_id IS NULL AND UPPER(item_type) == 'EQUIPMENT' OR UPPER(item_type) == 'CONSUMABLE' + GROUP BY it.id ORDER BY name LIMIT ?,?", ) .bind(cursor) @@ -135,14 +137,14 @@ pub async fn fetch_weapons( ) -> Result> { let x: Vec = sqlx::query_as( " - SELECT wt.id AS weapon_id, wt.bonus_dmg, wt.to_hit_bonus, wt.dmg_type, wt.number_of_dice, wt.die_size, wt.splash_dmg, - wt.n_of_potency_runes, wt.n_of_striking_runes, wt.range, wt.reload, wt.weapon_type, wt.base_item_id, + SELECT wt.id AS weapon_id, wt.to_hit_bonus, wt.splash_dmg, wt.n_of_potency_runes, + wt.n_of_striking_runes, wt.range, wt.reload, wt.weapon_type, wt.base_item_id, it.* FROM WEAPON_TABLE wt - LEFT OUTER JOIN ITEM_CREATURE_ASSOCIATION_TABLE icat - ON wt.base_item_id = icat.item_id + LEFT OUTER JOIN WEAPON_CREATURE_ASSOCIATION_TABLE wcat + ON wt.id = wcat.weapon_id LEFT JOIN ITEM_TABLE it ON wt.base_item_id = it.id - WHERE icat.item_id IS NULL + WHERE wcat.weapon_id IS NULL GROUP BY it.id ORDER BY name LIMIT ?,? ", @@ -155,6 +157,7 @@ pub async fn fetch_weapons( for mut el in x { el.item_core.traits = fetch_item_traits(conn, el.item_core.id).await?; el.weapon_data.property_runes = fetch_weapon_runes(conn, el.weapon_data.id).await?; + el.weapon_data.damage_data = fetch_weapon_damage_data(conn, el.weapon_data.id).await?; result_vec.push(Weapon { item_core: el.item_core, weapon_data: el.weapon_data, @@ -167,13 +170,12 @@ pub async fn fetch_armors(conn: &Pool, cursor: u32, page_size: i16) -> R let x: Vec = sqlx::query_as( " SELECT at.id AS armor_id, at.bonus_ac, at.check_penalty, at.dex_cap, at.n_of_potency_runes, - at.n_of_resilient_runes, at.speed_penalty, at.strength_required, at.base_item_id, - it.* + at.n_of_resilient_runes, at.speed_penalty, at.strength_required, at.base_item_id, it.* FROM ARMOR_TABLE at - LEFT OUTER JOIN ITEM_CREATURE_ASSOCIATION_TABLE icat - ON at.base_item_id = icat.item_id + LEFT OUTER JOIN ARMOR_CREATURE_ASSOCIATION_TABLE acat + ON at.id = acat.armor_id LEFT JOIN ITEM_TABLE it ON at.base_item_id = it.id - WHERE icat.item_id IS NULL + WHERE acat.armor_id IS NULL GROUP BY it.id ORDER BY name LIMIT ?,? ", @@ -201,13 +203,12 @@ pub async fn fetch_shields( ) -> Result> { let x: Vec = sqlx::query_as( " - SELECT st.id AS shield_id, st.bonus_ac, st.n_of_reinforcing_runes, st.speed_penalty, - it.* + SELECT st.id AS shield_id, st.bonus_ac, st.n_of_reinforcing_runes, st.speed_penalty, it.* FROM SHIELD_TABLE st - LEFT OUTER JOIN ITEM_CREATURE_ASSOCIATION_TABLE icat - ON st.base_item_id = icat.item_id + LEFT OUTER JOIN SHIELD_CREATURE_ASSOCIATION_TABLE scat + ON st.id = scat.shield_id LEFT JOIN ITEM_TABLE it ON st.base_item_id = it.id - WHERE icat.item_id IS NULL + WHERE scat.shield_id IS NULL GROUP BY it.id ORDER BY name LIMIT ?,? ", @@ -227,19 +228,6 @@ pub async fn fetch_shields( Ok(result_vec) } -pub async fn fetch_traits_associated_with_items(conn: &Pool) -> Result> { - let x: Vec = sqlx::query_as( - " - SELECT - tt.name AS my_str - FROM TRAIT_ITEM_ASSOCIATION_TABLE tiat - LEFT JOIN TRAIT_TABLE tt ON tiat.trait_id = tt.name GROUP BY tt.name", - ) - .fetch_all(conn) - .await?; - Ok(x.iter().map(|x| x.my_str.clone()).collect()) -} - async fn update_items_with_traits(conn: &Pool, mut items: Vec) -> Vec { for item in &mut items { item.traits = fetch_item_traits(conn, item.id).await.unwrap_or(vec![]); diff --git a/src/db/shop_proxy.rs b/src/db/shop_proxy.rs index 66454e8..ba2ff37 100644 --- a/src/db/shop_proxy.rs +++ b/src/db/shop_proxy.rs @@ -1,7 +1,5 @@ -use crate::db::data_providers::{generic_fetcher, shop_fetcher}; -use crate::models::creature::creature_metadata::type_enum::CreatureTypeEnum; +use crate::db::data_providers::shop_fetcher; use crate::models::item::armor_struct::Armor; -use crate::models::item::item_fields_enum::{FieldsUniqueValuesStruct, ItemField}; use crate::models::item::item_struct::Item; use crate::models::item::shield_struct::Shield; use crate::models::item::weapon_struct::Weapon; @@ -11,7 +9,7 @@ use crate::models::shop_structs::{ItemSortEnum, ShopFilterQuery, ShopPaginatedRe use crate::AppState; use anyhow::Result; use cached::proc_macro::once; -use strum::IntoEnumIterator; +use itertools::Itertools; pub async fn get_item_by_id(app_state: &AppState, id: i64) -> Option { shop_fetcher::fetch_item_by_id(&app_state.conn, id) @@ -52,6 +50,7 @@ pub async fn get_paginated_items( ItemSortEnum::Level => a.core_item.level.cmp(&b.core_item.level), ItemSortEnum::Type => a.core_item.item_type.cmp(&b.core_item.item_type), ItemSortEnum::Rarity => a.core_item.rarity.cmp(&b.core_item.rarity), + ItemSortEnum::Source => a.core_item.source.cmp(&b.core_item.source), }; match pagination .shop_sort_data @@ -133,67 +132,18 @@ async fn get_list(app_state: &AppState) -> Vec { response_vec } -pub async fn get_all_possible_values_of_filter( - app_state: &AppState, - field: ItemField, -) -> Vec { - let runtime_fields_values = get_all_keys(app_state).await; - let mut x = match field { - ItemField::Category => runtime_fields_values.list_of_categories, - - ItemField::Size => runtime_fields_values.list_of_sizes, - ItemField::Rarity => runtime_fields_values.list_of_rarities, - ItemField::Traits => runtime_fields_values.list_of_traits, - ItemField::Sources => runtime_fields_values.list_of_sources, - ItemField::Level => runtime_fields_values.list_of_levels, - ItemField::ItemType => 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 +/// Gets all the runtime sources. It will cache the result #[once(sync_writes = true)] -async fn get_all_keys(app_state: &AppState) -> FieldsUniqueValuesStruct { - FieldsUniqueValuesStruct { - list_of_levels: generic_fetcher::fetch_unique_values_of_field( - &app_state.conn, - "CREATURE_CORE", - "level", - ) - .await - .unwrap_or_default(), - list_of_categories: generic_fetcher::fetch_unique_values_of_field( - &app_state.conn, - "CREATURE_CORE", - "family", - ) - .await - .unwrap(), - list_of_traits: shop_fetcher::fetch_traits_associated_with_items(&app_state.conn) - .await - .unwrap_or_default(), - list_of_sources: generic_fetcher::fetch_unique_values_of_field( - &app_state.conn, - "CREATURE_CORE", - "source", - ) - .await - .unwrap_or_default(), - list_of_sizes: generic_fetcher::fetch_unique_values_of_field( - &app_state.conn, - "CREATURE_CORE", - "size", - ) - .await - .unwrap_or_default(), - list_of_rarities: generic_fetcher::fetch_unique_values_of_field( - &app_state.conn, - "CREATURE_CORE", - "rarity", - ) - .await - .unwrap_or_default(), +pub async fn get_all_sources(app_state: &AppState) -> Vec { + match get_all_items_from_db(app_state).await { + Ok(v) => v + .into_iter() + .map(|x| x.source) + .unique() + .filter(|x| !x.is_empty()) + .collect(), + Err(_) => { + vec![] + } } } diff --git a/src/models/creature/creature_component/creature_combat.rs b/src/models/creature/creature_component/creature_combat.rs index 3783cc0..ec50a1f 100644 --- a/src/models/creature/creature_component/creature_combat.rs +++ b/src/models/creature/creature_component/creature_combat.rs @@ -1,6 +1,7 @@ +use crate::models::creature::creature_metadata::variant_enum::CreatureVariant; use crate::models::item::armor_struct::Armor; use crate::models::item::shield_struct::Shield; -use crate::models::item::weapon_struct::Weapon; +use crate::models::item::weapon_struct::{DamageData, Weapon}; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use utoipa::ToSchema; @@ -26,3 +27,64 @@ pub struct CreatureCombatData { pub saving_throws: SavingThrows, pub ac: i8, } + +impl CreatureCombatData { + fn add_mod_to_saving_throws_and_ac_and_wp_to_hit(self, modifier: i64) -> CreatureCombatData { + let mut com_data = self; + let weapons: Vec = com_data + .weapons + .into_iter() + .map(|mut wp| { + wp.weapon_data.to_hit_bonus = + wp.weapon_data.to_hit_bonus.map(|to_hit| to_hit + modifier); + wp + }) + .collect(); + com_data.ac = (com_data.ac as i64 + modifier) as i8; + com_data.saving_throws.fortitude += modifier; + com_data.saving_throws.reflex += modifier; + com_data.saving_throws.will += modifier; + com_data.weapons = weapons; + com_data + } + + fn add_mod_to_dmg(self, modifier: i64) -> CreatureCombatData { + let mut com_data = self; + let weapons: Vec = com_data + .weapons + .into_iter() + .map(|mut wp| { + wp.weapon_data.splash_dmg = wp.weapon_data.splash_dmg.map(|dmg| dmg + modifier); + wp.weapon_data.damage_data = wp + .weapon_data + .damage_data + .iter() + .map(|x| DamageData { + id: x.id, + bonus_dmg: x.bonus_dmg + modifier, + dmg_type: x.dmg_type.clone(), + dice: x.dice.clone(), + }) + .collect(); + wp + }) + .collect(); + com_data.weapons = weapons; + com_data + } + + /// Lowers saving throws, weapon to hit bonus, and ac by the given pwl_mod + pub fn convert_from_base_to_pwl(self, pwl_mod: u64) -> CreatureCombatData { + self.add_mod_to_saving_throws_and_ac_and_wp_to_hit(-(pwl_mod as i64)) + } + + /// Increase/Decrease the damage of its Strikes and other offensive abilities by 2. + /// If the creature has limits on how many times or how often it can use an ability + /// (such as a spellcaster’s spells or a dragon’s breath), decrease the damage by 4 instead. + /// Increase/Decrease the creature’s AC, attack modifiers, DCs, saving throws by 2. + pub fn convert_from_base_to_variant(self, variant: &CreatureVariant) -> CreatureCombatData { + let modifier = variant.to_adjustment_modifier(); + self.add_mod_to_saving_throws_and_ac_and_wp_to_hit(modifier) + .add_mod_to_dmg(modifier) + } +} diff --git a/src/models/creature/creature_component/creature_extra.rs b/src/models/creature/creature_component/creature_extra.rs index aedcc27..122ed95 100644 --- a/src/models/creature/creature_component/creature_extra.rs +++ b/src/models/creature/creature_component/creature_extra.rs @@ -1,3 +1,4 @@ +use crate::models::creature::creature_metadata::variant_enum::CreatureVariant; use crate::models::creature::items::action::Action; use crate::models::creature::items::skill::Skill; use crate::models::item::item_struct::Item; @@ -30,3 +31,31 @@ pub struct CreatureExtraData { pub perception: i8, pub perception_detail: Option, } + +impl CreatureExtraData { + fn add_mod_to_perception_and_skill_mods(self, modifier: i64) -> CreatureExtraData { + let mut ex_data = self; + // we should never have a pwl much greater than perception (pwl=lvl) + ex_data.perception = (ex_data.perception as i64 + modifier) as i8; + + ex_data.skills = ex_data + .skills + .into_iter() + .map(|mut skill| { + skill.modifier += modifier; + skill + }) + .collect(); + + ex_data + } + /// Lowers skill and perception by the given pwl_mod + pub fn convert_from_base_to_pwl(self, pwl_mod: u64) -> CreatureExtraData { + self.add_mod_to_perception_and_skill_mods(-(pwl_mod as i64)) + } + + /// Increase/Decrease Perception, and skill modifiers by 2. + pub fn convert_from_base_to_variant(self, variant: &CreatureVariant) -> CreatureExtraData { + self.add_mod_to_perception_and_skill_mods(variant.to_adjustment_modifier()) + } +} diff --git a/src/models/creature/creature_component/creature_spell_caster.rs b/src/models/creature/creature_component/creature_spell_caster.rs index 75eec30..3adfa65 100644 --- a/src/models/creature/creature_component/creature_spell_caster.rs +++ b/src/models/creature/creature_component/creature_spell_caster.rs @@ -1,3 +1,4 @@ +use crate::models::creature::creature_metadata::variant_enum::CreatureVariant; use crate::models::creature::items::spell::Spell; use crate::models::creature::items::spell_caster_entry::SpellCasterEntry; use serde::{Deserialize, Serialize}; @@ -8,3 +9,36 @@ pub struct CreatureSpellCasterData { pub spells: Vec, pub spell_caster_entry: SpellCasterEntry, } + +impl CreatureSpellCasterData { + pub fn add_mod_to_spellcaster_atk_and_dc(self, modifier: i64) -> CreatureSpellCasterData { + let mut spell_data = self; + + spell_data.spell_caster_entry.spell_casting_atk_mod = spell_data + .spell_caster_entry + .spell_casting_atk_mod + .map(|x| x + modifier); + + spell_data.spell_caster_entry.spell_casting_dc_mod = spell_data + .spell_caster_entry + .spell_casting_dc_mod + .map(|x| x + modifier); + + spell_data + } + + /// Lowers spell caster atk and dc + pub fn convert_from_base_to_pwl(self, pwl_mod: u64) -> CreatureSpellCasterData { + self.add_mod_to_spellcaster_atk_and_dc(-(pwl_mod as i64)) + } + + /// Increase/Decrease the damage of its Strikes and other offensive abilities by 2. + /// If the creature has limits on how many times or how often it can use an ability + /// (such as a spellcaster’s spells or a dragon’s breath), decrease the damage by 4 instead. + pub fn convert_from_base_to_variant( + self, + variant: &CreatureVariant, + ) -> CreatureSpellCasterData { + self.add_mod_to_spellcaster_atk_and_dc(variant.to_adjustment_modifier()) + } +} diff --git a/src/models/creature/creature_metadata/creature_role.rs b/src/models/creature/creature_metadata/creature_role.rs index 7f33b51..4803cf5 100644 --- a/src/models/creature/creature_metadata/creature_role.rs +++ b/src/models/creature/creature_metadata/creature_role.rs @@ -156,9 +156,8 @@ fn is_brute( let wp_distance = cr_combat .weapons .iter() - .filter(|wp| wp.get_avg_dmg().is_some()) .map(|wp| { - let avg_dmg = wp.get_avg_dmg().unwrap(); + let avg_dmg = wp.get_avg_dmg(); let x = calculate_lb_distance( atk_bonus_scales.high, wp.weapon_data.to_hit_bonus.unwrap_or(0), @@ -208,11 +207,9 @@ fn is_sniper( let wp_distance = cr_combat .weapons .iter() - .filter(|wp| { - wp.get_avg_dmg().is_some() && wp.weapon_data.weapon_type == WeaponTypeEnum::Ranged - }) + .filter(|wp| wp.weapon_data.weapon_type == WeaponTypeEnum::Ranged) .map(|wp| { - let avg_dmg = wp.get_avg_dmg().unwrap(); + let avg_dmg = wp.get_avg_dmg(); calculate_lb_distance( atk_bonus_scales.high, wp.weapon_data.to_hit_bonus.unwrap_or(0), @@ -276,12 +273,11 @@ pub fn is_soldier( let wp_distance = cr_combat .weapons .iter() - .filter(|wp| wp.get_avg_dmg().is_some()) .map(|wp| { calculate_lb_distance( atk_bonus_scales.high, wp.weapon_data.to_hit_bonus.unwrap_or(0), - ) + calculate_lb_distance(scales_high_avg, wp.get_avg_dmg().unwrap()) + ) + calculate_lb_distance(scales_high_avg, wp.get_avg_dmg()) }) .min(); @@ -318,12 +314,11 @@ pub fn is_magical_striker( let wp_distance = cr_combat .weapons .iter() - .filter(|wp| wp.get_avg_dmg().is_some()) .map(|wp| { calculate_lb_distance( atk_bonus_scales.high, wp.weapon_data.to_hit_bonus.unwrap_or(0), - ) + calculate_lb_distance(scales_high_avg, wp.get_avg_dmg().unwrap()) + ) + calculate_lb_distance(scales_high_avg, wp.get_avg_dmg()) }) .min(); score += wp_distance.unwrap_or(MISSING_FIELD_DISTANCE); diff --git a/src/models/creature/creature_metadata/variant_enum.rs b/src/models/creature/creature_metadata/variant_enum.rs index 436c9ab..2d9f491 100644 --- a/src/models/creature/creature_metadata/variant_enum.rs +++ b/src/models/creature/creature_metadata/variant_enum.rs @@ -1,4 +1,3 @@ -use crate::models::creature::creature_component::creature_core::CreatureCoreData; use crate::services::url_calculator::add_boolean_query; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -26,38 +25,62 @@ impl Clone for CreatureVariant { } impl CreatureVariant { - pub fn to_level_delta(&self) -> i64 { + pub fn to_adjustment_modifier(&self) -> i64 { match self { - CreatureVariant::Weak => -1, - CreatureVariant::Elite => 1, + CreatureVariant::Weak => -2, + CreatureVariant::Elite => 2, CreatureVariant::Base => 0, } } - pub fn get_variant_hp_and_link(&self, core: &CreatureCoreData) -> (i64, Option) { - let mut core_hp = core.essential.hp; + pub fn get_variant_level(&self, base_lvl: i64) -> i64 { match self { - 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); + //Decrease the creature’s level by 1; if the creature is level 1, + // instead decrease its level by 2. + CreatureVariant::Weak => { + if base_lvl == 1 { + base_lvl - 2 + } else { + base_lvl - 1 + } + } + //Increase the creature’s level by 1; if the creature is level –1 or 0, + // instead increase its level by 2. + CreatureVariant::Elite => { + if base_lvl == -1 || base_lvl == 0 { + base_lvl + 2 + } else { + base_lvl + 1 + } + } + CreatureVariant::Base => base_lvl, + } + } - (core_hp, variant_archive_link) + pub fn get_variant_hp(&self, base_hp: i64, starting_lvl: i64) -> i64 { + let hp_mod_map = match self { + CreatureVariant::Weak => hp_decrease_by_level(), + CreatureVariant::Elite => hp_increase_by_level(), + CreatureVariant::Base => { + hashmap! {} } + }; + // get the lowest possible key, + // it must still be higher than the given starting level + // ex: {1=>2, 3=>4} w start_lvl = 2 => 3 + let desired_key = hp_mod_map + .keys() + .filter(|lvl| starting_lvl >= **lvl) + .max() + .unwrap_or(hp_mod_map.keys().next().unwrap_or(&0)); + let hp_mod = *hp_mod_map.get(desired_key).unwrap_or(&0); + (base_hp + hp_mod).max(1) + } + + pub fn get_variant_archive_link(&self, archive_link: Option) -> Option { + match self { + CreatureVariant::Base => archive_link, + _ => add_boolean_query(&archive_link, &self.to_string(), true), } } } @@ -65,3 +88,12 @@ impl CreatureVariant { fn hp_increase_by_level() -> HashMap { hashmap! { 1 => 10, 2=> 15, 5=> 20, 20=> 30 } } + +fn hp_decrease_by_level() -> HashMap { + hashmap! { + 1 => -10, + 3 => -15, + 6 => -20, + 21 => -30 + } +} diff --git a/src/models/creature/creature_struct.rs b/src/models/creature/creature_struct.rs index 81ae191..ef8ba25 100644 --- a/src/models/creature/creature_struct.rs +++ b/src/models/creature/creature_struct.rs @@ -19,13 +19,43 @@ pub struct Creature { } 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; + /// Decrease the creature’s level by 1; if the creature is level 1, instead decrease its level by 2. + /// Decrease the creature’s HP based on its starting level. + /// Decrease the creature’s AC, attack modifiers, DCs, saving throws, Perception, and skill modifiers by 2. + /// Decrease the damage of its Strikes and other offensive abilities by 2. If the creature has limits on how many times or how often it can use an ability (such as a spellcaster’s spells or a dragon’s breath), decrease the damage by 4 instead. + pub fn convert_creature_to_variant(self, variant: CreatureVariant) -> Creature { + let mut cr = Self::from_core_with_variant(self.core_data, variant.clone()); + cr.extra_data = self + .extra_data + .map(|x| x.convert_from_base_to_variant(&variant)); + cr.combat_data = self + .combat_data + .map(|x| x.convert_from_base_to_variant(&variant)); + cr.spell_caster_data = self + .spell_caster_data + .map(|x| x.convert_from_base_to_variant(&variant)); cr } + + pub fn convert_creature_to_pwl(self) -> Creature { + let pwl_mod = if self.core_data.essential.level >= 0 { + self.core_data.essential.level as u64 + } else { + 0 + }; + + Creature { + core_data: self.core_data, + variant_data: self.variant_data, + extra_data: self.extra_data.map(|x| x.convert_from_base_to_pwl(pwl_mod)), + combat_data: self + .combat_data + .map(|x| x.convert_from_base_to_pwl(pwl_mod)), + spell_caster_data: self + .spell_caster_data + .map(|x| x.convert_from_base_to_pwl(pwl_mod)), + } + } pub fn from_core(core: CreatureCoreData) -> Creature { let level = core.essential.level; let archive_link = core.derived.archive_link.clone(); @@ -44,17 +74,19 @@ impl Creature { pub fn from_core_with_variant( mut core: CreatureCoreData, - creature_variant: &CreatureVariant, + 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; + let variant_hp = creature_variant.get_variant_hp(core.essential.hp, core.essential.level); + let variant_archive_link = + creature_variant.get_variant_archive_link(core.derived.archive_link.clone()); + let variant_level = creature_variant.get_variant_level(core.essential.level); + core.essential.hp = variant_hp; + core.essential.level = variant_level; Self { core_data: core, variant_data: CreatureVariantData { - variant: creature_variant.clone(), - level: base_level + level_delta, + variant: creature_variant, + level: variant_level, archive_link: variant_archive_link, }, extra_data: None, diff --git a/src/models/item/item_metadata/type_enum.rs b/src/models/item/item_metadata/type_enum.rs index 0397327..3b19242 100644 --- a/src/models/item/item_metadata/type_enum.rs +++ b/src/models/item/item_metadata/type_enum.rs @@ -21,6 +21,27 @@ pub enum ItemTypeEnum { Shield, } +impl ItemTypeEnum { + pub fn to_db_main_table_name(&self) -> String { + format!("{}_TABLE", self.to_db_table_name()) + } + + pub fn to_db_association_table_name(&self) -> String { + format!("{}_CREATURE_ASSOCIATION_TABLE", self.to_db_table_name()) + } + + /// Utility method to reduce code redundancy. + /// It returns the generic table name of the given item type. + fn to_db_table_name(&self) -> String { + String::from(match self { + ItemTypeEnum::Consumable | ItemTypeEnum::Equipment => "ITEM", + ItemTypeEnum::Weapon => "WEAPON", + ItemTypeEnum::Armor => "ARMOR", + ItemTypeEnum::Shield => "SHIELD", + }) + } +} + impl Clone for ItemTypeEnum { fn clone(&self) -> ItemTypeEnum { match self { diff --git a/src/models/item/item_struct.rs b/src/models/item/item_struct.rs index 3be03de..e7a5c68 100644 --- a/src/models/item/item_struct.rs +++ b/src/models/item/item_struct.rs @@ -138,15 +138,15 @@ impl Item { filters .rarity_filter .as_ref() - .map_or(true, |rarity| self.rarity == *rarity) + .map_or(true, |x| x.iter().any(|rarity| self.rarity == *rarity)) && filters .size_filter .as_ref() - .map_or(true, |size| self.size == *size) + .map_or(true, |x| x.iter().any(|size| self.size == *size)) && filters .type_filter .as_ref() - .map_or(true, |t_filt| self.item_type == *t_filt) + .map_or(true, |x| x.iter().any(|t_filt| self.item_type == *t_filt)) } fn check_item_pass_string_filters(&self, filters: &ItemFieldFilters) -> bool { @@ -154,20 +154,24 @@ impl Item { self.name .to_lowercase() .contains(name.to_lowercase().as_str()) - }) && filters.category_filter.as_ref().map_or(true, |cat| { - if self.category.is_none() { - true - } else { + }) && filters.category_filter.as_ref().map_or(true, |x| { + x.iter().any(|cat| { self.category .clone() - .unwrap() + .unwrap_or_default() .to_lowercase() .contains(cat.to_lowercase().as_str()) - } + }) }) && match filters.pathfinder_version.clone().unwrap_or_default() { PathfinderVersionEnum::Legacy => !self.remaster, PathfinderVersionEnum::Remaster => self.remaster, PathfinderVersionEnum::Any => true, - } + } && filters.source_filter.as_ref().map_or(true, |x| { + x.iter().any(|source| { + self.source + .to_lowercase() + .contains(source.to_lowercase().as_str()) + }) + }) } } diff --git a/src/models/item/weapon_struct.rs b/src/models/item/weapon_struct.rs index 1d1535f..948b6f7 100644 --- a/src/models/item/weapon_struct.rs +++ b/src/models/item/weapon_struct.rs @@ -1,5 +1,6 @@ use crate::models::item::item_metadata::type_enum::WeaponTypeEnum; use crate::models::item::item_struct::Item; +use crate::models::routers_validator_structs::Dice; use serde::{Deserialize, Serialize}; use sqlx::sqlite::SqliteRow; use sqlx::{Error, FromRow, Row}; @@ -15,18 +16,15 @@ pub struct Weapon { #[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] pub struct WeaponData { pub id: i64, - pub bonus_dmg: i64, pub to_hit_bonus: Option, - pub dmg_type: Option, - pub number_of_dice: Option, - pub die_size: Option, - pub splash_dmg: Option, + pub damage_data: Vec, pub n_of_potency_runes: i64, pub n_of_striking_runes: i64, pub property_runes: Vec, pub range: Option, pub reload: Option, pub weapon_type: WeaponTypeEnum, + pub splash_dmg: Option, } impl<'r> FromRow<'r, SqliteRow> for Weapon { @@ -37,43 +35,55 @@ impl<'r> FromRow<'r, SqliteRow> for Weapon { item_core, weapon_data: WeaponData { id: row.try_get("weapon_id")?, - bonus_dmg: row.try_get("bonus_dmg")?, to_hit_bonus: row.try_get("to_hit_bonus")?, - dmg_type: row.try_get("dmg_type")?, - number_of_dice: row.try_get("number_of_dice")?, - die_size: row.try_get("die_size")?, - splash_dmg: row.try_get("splash_dmg")?, n_of_potency_runes: row.try_get("n_of_potency_runes")?, n_of_striking_runes: row.try_get("n_of_striking_runes")?, property_runes: vec![], range: row.try_get("range")?, reload: row.try_get("reload")?, weapon_type: wp_type.unwrap_or(WeaponTypeEnum::Melee), + damage_data: vec![], + splash_dmg: row.try_get("splash_dmg").ok(), }, }) } } 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 - .weapon_data - .die_size - .clone()? - .split_once('d')? - .1 - .parse::() - .ok()?; - let n = self.weapon_data.number_of_dice? as f64; - let b = self.weapon_data.bonus_dmg as f64; + pub fn get_avg_dmg(&self) -> i64 { + self.weapon_data + .damage_data + .iter() + .map(|x| { + let b = x.bonus_dmg as f64; + if let Some(dice) = x.clone().dice { + dice.get_avg_dmg(b) + } else { + 0 + } + }) + .sum() + } +} + +#[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] +pub struct DamageData { + pub id: i64, + pub bonus_dmg: i64, + pub dmg_type: Option, + pub dice: Option, +} - let avg: f64 = (((m + 1.) / 2.) * n) + b; - Some(avg.floor() as i64) +impl<'r> FromRow<'r, SqliteRow> for DamageData { + fn from_row(row: &'r SqliteRow) -> Result { + Ok(DamageData { + id: row.try_get("id")?, + bonus_dmg: row.try_get("bonus_dmg")?, + dmg_type: row.try_get("dmg_type").ok(), + dice: Dice::from_optional_dice_number_and_size( + row.try_get("number_of_dice").ok(), + row.try_get("die_size").ok(), + ), + }) } } diff --git a/src/models/response_data.rs b/src/models/response_data.rs index 4f7c744..6341b07 100644 --- a/src/models/response_data.rs +++ b/src/models/response_data.rs @@ -13,7 +13,8 @@ use utoipa::{IntoParams, ToSchema}; use validator::Validate; #[derive(Serialize, Deserialize, IntoParams, Default, Eq, PartialEq, Hash, Clone, Validate)] -pub struct OptionalData { +pub struct ResponseDataModifiers { + pub is_pwl_on: Option, pub extra_data: Option, pub combat_data: Option, pub spell_casting_data: Option, diff --git a/src/models/routers_validator_structs.rs b/src/models/routers_validator_structs.rs index ea01311..aacff49 100644 --- a/src/models/routers_validator_structs.rs +++ b/src/models/routers_validator_structs.rs @@ -14,6 +14,7 @@ use validator::Validate; #[derive(Serialize, Deserialize, IntoParams, Validate)] pub struct CreatureFieldFilters { pub name_filter: Option, + pub source_filter: Option, pub family_filter: Option, pub rarity_filter: Option, pub size_filter: Option, @@ -36,10 +37,11 @@ pub struct CreatureFieldFilters { pub pathfinder_version: Option, } -#[derive(Serialize, Deserialize, IntoParams, Validate)] +#[derive(Serialize, Deserialize, IntoParams, ToSchema, Validate)] pub struct ItemFieldFilters { pub name_filter: Option, - pub category_filter: Option, + pub category_filter: Option>, + pub source_filter: Option>, #[validate(range(min = 0.))] pub min_bulk_filter: Option, @@ -66,9 +68,9 @@ pub struct ItemFieldFilters { #[validate(range(min = 0))] pub max_n_of_uses_filter: Option, - pub type_filter: Option, - pub rarity_filter: Option, - pub size_filter: Option, + pub type_filter: Option>, + pub rarity_filter: Option>, + pub size_filter: Option>, pub pathfinder_version: Option, } @@ -124,4 +126,39 @@ impl Dice { } roll_result } + + pub fn get_avg_dmg(&self, bonus_dmg: f64) -> i64 { + // 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.dice_size as f64; + let n = self.n_of_dices as f64; + let b = bonus_dmg; + let avg: f64 = (((m + 1.) / 2.) * n) + b; + avg.floor() as i64 + } + + pub fn from_optional_dice_number_and_size( + n_of_dices: Option, + dice_size: Option, + ) -> Option { + match (n_of_dices, dice_size) { + (Some(n), Some(s)) => Some(Dice { + n_of_dices: n, + dice_size: s, + }), + (None, Some(s)) => Some(Dice { + n_of_dices: 1, + dice_size: s, + }), + (Some(n), None) => Some(Dice { + n_of_dices: n, + dice_size: 1, + }), + (_, _) => None, + } + } } diff --git a/src/models/shop_structs.rs b/src/models/shop_structs.rs index 17ebf2a..407917f 100644 --- a/src/models/shop_structs.rs +++ b/src/models/shop_structs.rs @@ -8,7 +8,7 @@ use validator::Validate; #[derive( Serialize, Deserialize, ToSchema, Default, EnumIter, Eq, PartialEq, Hash, Ord, PartialOrd, Clone, )] -pub enum ShopTypeEnum { +pub enum ShopTemplateEnum { Blacksmith, Alchemist, #[default] @@ -25,7 +25,7 @@ pub struct RandomShopData { pub equipment_dices: Vec, #[validate(length(min = 1))] pub consumable_dices: Vec, - pub shop_type: Option, + pub shop_template: Option, pub pathfinder_version: Option, } @@ -54,6 +54,8 @@ pub enum ItemSortEnum { Type, #[serde(alias = "rarity", alias = "RARITY")] Rarity, + #[serde(alias = "source", alias = "SOURCE")] + Source, } #[derive(Serialize, Deserialize, IntoParams, Validate, Eq, PartialEq, Hash, Default)] diff --git a/src/routes/bestiary.rs b/src/routes/bestiary.rs index eba5e18..9aa98a6 100644 --- a/src/routes/bestiary.rs +++ b/src/routes/bestiary.rs @@ -2,8 +2,9 @@ use crate::models::creature::creature_metadata::alignment_enum::AlignmentEnum; use crate::models::creature::creature_metadata::creature_role::CreatureRoleEnum; use crate::models::creature::creature_metadata::type_enum::CreatureTypeEnum; use crate::models::creature::creature_metadata::variant_enum::CreatureVariant; -use crate::models::response_data::OptionalData; +use crate::models::item::shield_struct::Shield; use crate::models::response_data::ResponseCreature; +use crate::models::response_data::ResponseDataModifiers; use crate::models::routers_validator_structs::OrderEnum; use crate::models::shared::rarity_enum::RarityEnum; use crate::models::shared::size_enum::SizeEnum; @@ -87,6 +88,7 @@ pub fn init_docs(doc: &mut utoipa::openapi::OpenApi) { CreatureCombatData, CreatureSpellCasterData, Spell, + Shield, Weapon, Armor, SavingThrows, @@ -283,7 +285,7 @@ pub async fn get_creature_roles_list() -> Result { tag = "bestiary", params( ("creature_id" = String, Path, description = "id of the creature to fetch"), - OptionalData, + ResponseDataModifiers, ), responses( (status=200, description = "Successful Response", body = ResponseCreature), @@ -294,10 +296,11 @@ pub async fn get_creature_roles_list() -> Result { pub async fn get_creature( data: web::Data, creature_id: web::Path, - optional_data: Query, + response_data_mods: Query, ) -> Result { Ok(web::Json( - bestiary_service::get_creature(&data, sanitize_id(&creature_id)?, &optional_data.0).await, + bestiary_service::get_creature(&data, sanitize_id(&creature_id)?, &response_data_mods.0) + .await, )) } @@ -307,7 +310,7 @@ pub async fn get_creature( tag = "bestiary", params( ("creature_id" = String, Path, description = "id of the creature to fetch"), - OptionalData + ResponseDataModifiers ), responses( (status=200, description = "Successful Response", body = ResponseCreature), @@ -318,11 +321,15 @@ pub async fn get_creature( pub async fn get_elite_creature( data: web::Data, creature_id: web::Path, - response_data: Query, + response_data_mods: Query, ) -> Result { Ok(web::Json( - bestiary_service::get_elite_creature(&data, sanitize_id(&creature_id)?, &response_data.0) - .await, + bestiary_service::get_elite_creature( + &data, + sanitize_id(&creature_id)?, + &response_data_mods.0, + ) + .await, )) } @@ -332,7 +339,7 @@ pub async fn get_elite_creature( tag = "bestiary", params( ("creature_id" = String, Path, description = "id of the creature to fetch"), - OptionalData, + ResponseDataModifiers, ), responses( (status=200, description = "Successful Response", body = ResponseCreature), @@ -343,11 +350,15 @@ pub async fn get_elite_creature( pub async fn get_weak_creature( data: web::Data, creature_id: web::Path, - response_data: Query, + response_data_mods: Query, ) -> Result { Ok(web::Json( - bestiary_service::get_weak_creature(&data, sanitize_id(&creature_id)?, &response_data.0) - .await, + bestiary_service::get_weak_creature( + &data, + sanitize_id(&creature_id)?, + &response_data_mods.0, + ) + .await, )) } diff --git a/src/routes/shop.rs b/src/routes/shop.rs index 682610b..477d140 100644 --- a/src/routes/shop.rs +++ b/src/routes/shop.rs @@ -3,11 +3,12 @@ use crate::models::item::item_metadata::type_enum::ItemTypeEnum; use crate::models::item::item_metadata::type_enum::WeaponTypeEnum; use crate::models::item::item_struct::Item; use crate::models::item::shield_struct::ShieldData; +use crate::models::item::weapon_struct::DamageData; use crate::models::item::weapon_struct::WeaponData; use crate::models::response_data::ResponseItem; use crate::models::routers_validator_structs::ItemFieldFilters; use crate::models::routers_validator_structs::{Dice, PaginatedRequest}; -use crate::models::shop_structs::ShopTypeEnum; +use crate::models::shop_structs::ShopTemplateEnum; use crate::models::shop_structs::{ItemSortEnum, ShopPaginatedRequest}; use crate::models::shop_structs::{RandomShopData, ShopSortData}; use crate::services::shop_service; @@ -22,6 +23,7 @@ pub fn init_endpoints(cfg: &mut web::ServiceConfig) { web::scope("/shop") .service(get_item) .service(get_shop_listing) + .service(get_sources_list) .service(get_random_shop_listing), ); } @@ -29,7 +31,7 @@ pub fn init_endpoints(cfg: &mut web::ServiceConfig) { pub fn init_docs(doc: &mut utoipa::openapi::OpenApi) { #[derive(OpenApi)] #[openapi( - paths(get_shop_listing, get_item, get_random_shop_listing), + paths(get_shop_listing, get_item, get_random_shop_listing, get_sources_list), components(schemas( ResponseItem, ItemTypeEnum, @@ -37,8 +39,10 @@ pub fn init_docs(doc: &mut utoipa::openapi::OpenApi) { Item, RandomShopData, Dice, - ShopTypeEnum, + ShopTemplateEnum, + ItemFieldFilters, ItemSortEnum, + DamageData, WeaponData, ArmorData, ShieldData, @@ -51,28 +55,32 @@ pub fn init_docs(doc: &mut utoipa::openapi::OpenApi) { } #[utoipa::path( - get, + post, path = "/shop/list", tag = "shop", + request_body( + content = ItemFieldFilters, + content_type = "application/json", + ), params( - ItemFieldFilters, PaginatedRequest, ShopSortData + PaginatedRequest, ShopSortData ), responses( (status=200, description = "Successful Response", body = ShopListingResponse), (status=400, description = "Bad request.") ), )] -#[get("/list")] +#[post("/list")] pub async fn get_shop_listing( data: web::Data, - filters: Query, + web::Json(body): web::Json, pagination: Query, sort_data: Query, ) -> actix_web::Result { Ok(web::Json( shop_service::get_shop_listing( &data, - &filters.0, + &body, &ShopPaginatedRequest { paginated_request: pagination.0, shop_sort_data: sort_data.0, @@ -132,7 +140,7 @@ pub async fn get_item( #[utoipa::path( get, - path = "/shop/traits", + path = "/shop/sources", tag = "shop", params( @@ -142,9 +150,9 @@ pub async fn get_item( (status=400, description = "Bad request.") ), )] -#[get("/traits")] -pub async fn get_traits_list(data: web::Data) -> actix_web::Result { - Ok(web::Json(shop_service::get_traits_list(&data).await)) +#[get("/sources")] +pub async fn get_sources_list(data: web::Data) -> actix_web::Result { + Ok(web::Json(shop_service::get_sources_list(&data).await)) } fn sanitize_id(creature_id: &str) -> actix_web::Result { diff --git a/src/services/bestiary_service.rs b/src/services/bestiary_service.rs index 7f9fe6b..1eabff1 100644 --- a/src/services/bestiary_service.rs +++ b/src/services/bestiary_service.rs @@ -4,7 +4,7 @@ use crate::models::creature::creature_filter_enum::CreatureFilter; use crate::models::creature::creature_metadata::creature_role::CreatureRoleEnum; use crate::models::creature::creature_metadata::variant_enum::CreatureVariant; use crate::models::creature::creature_struct::Creature; -use crate::models::response_data::{OptionalData, ResponseCreature}; +use crate::models::response_data::{ResponseCreature, ResponseDataModifiers}; use crate::models::routers_validator_structs::CreatureFieldFilters; use crate::services::url_calculator::bestiary_next_url_calculator; use crate::AppState; @@ -24,33 +24,33 @@ pub struct BestiaryResponse { pub async fn get_creature( app_state: &AppState, id: i64, - optional_data: &OptionalData, + response_data_mods: &ResponseDataModifiers, ) -> HashMap> { hashmap! { String::from("results") => - bestiary_proxy::get_creature_by_id(app_state, id, &CreatureVariant::Base, optional_data).await.map(ResponseCreature::from) + bestiary_proxy::get_creature_by_id(app_state, id, CreatureVariant::Base, response_data_mods).await.map(ResponseCreature::from) } } pub async fn get_elite_creature( app_state: &AppState, id: i64, - optional_data: &OptionalData, + response_data_mods: &ResponseDataModifiers, ) -> HashMap> { hashmap! { String::from("results") => - bestiary_proxy::get_elite_creature_by_id(app_state, id, optional_data).await.map(ResponseCreature::from) + bestiary_proxy::get_elite_creature_by_id(app_state, id, response_data_mods).await.map(ResponseCreature::from) } } pub async fn get_weak_creature( app_state: &AppState, id: i64, - optional_data: &OptionalData, + response_data_mods: &ResponseDataModifiers, ) -> HashMap> { hashmap! { String::from("results") => - bestiary_proxy::get_weak_creature_by_id(app_state, id, optional_data).await.map(ResponseCreature::from) + bestiary_proxy::get_weak_creature_by_id(app_state, id, response_data_mods).await.map(ResponseCreature::from) } } diff --git a/src/services/shop_service.rs b/src/services/shop_service.rs index e8c6337..9aa5331 100644 --- a/src/services/shop_service.rs +++ b/src/services/shop_service.rs @@ -1,9 +1,8 @@ use crate::db::shop_proxy; -use crate::models::item::item_fields_enum::ItemField; use crate::models::response_data::ResponseItem; use crate::models::routers_validator_structs::ItemFieldFilters; use crate::models::shop_structs::{ - RandomShopData, ShopFilterQuery, ShopPaginatedRequest, ShopTypeEnum, + RandomShopData, ShopFilterQuery, ShopPaginatedRequest, ShopTemplateEnum, }; use crate::services::url_calculator::shop_next_url_calculator; use crate::AppState; @@ -46,11 +45,11 @@ pub async fn generate_random_shop_listing( let min_level = shop_data.min_level.unwrap_or(0); let max_level = shop_data.max_level.unwrap_or(30); - let shop_type = shop_data.shop_type.clone().unwrap_or_default(); + let shop_type = shop_data.shop_template.clone().unwrap_or_default(); let n_of_consumables: i64 = shop_data.consumable_dices.iter().map(|x| x.roll()).sum(); let n_of_equipables: i64 = shop_data.equipment_dices.iter().map(|x| x.roll()).sum(); - let (n_of_equipment, n_of_armors, n_of_weapons, n_of_shields) = match shop_type { - ShopTypeEnum::Blacksmith => { + let (n_of_equipment, n_of_weapons, n_of_armors, n_of_shields) = match shop_type { + ShopTemplateEnum::Blacksmith => { // This will never panic if n_of_equipables >= 1 and dice sum should always be at least 1. // if 1<=n<2 => n/2 = 0..n // TLDR we know that it will never panic. @@ -63,8 +62,8 @@ pub async fn generate_random_shop_listing( forged_items_tuple.2, ) } - ShopTypeEnum::Alchemist => (n_of_equipables, 0, 0, 0), - ShopTypeEnum::General => { + ShopTemplateEnum::Alchemist => (n_of_equipables, 0, 0, 0), + ShopTemplateEnum::General => { // This can panic if n_of_equipables is <=1, // n=1 => n/2 = 0, 0..0 panic! // we manually set it as 1 in that case @@ -120,8 +119,8 @@ pub async fn generate_random_shop_listing( } } -pub async fn get_traits_list(app_state: &AppState) -> Vec { - shop_proxy::get_all_possible_values_of_filter(app_state, ItemField::Traits).await +pub async fn get_sources_list(app_state: &AppState) -> Vec { + shop_proxy::get_all_sources(app_state).await } /// Gets the n of: weapons, armors, shields (in this order). diff --git a/src/services/url_calculator.rs b/src/services/url_calculator.rs index ee008b9..5ae554e 100644 --- a/src/services/url_calculator.rs +++ b/src/services/url_calculator.rs @@ -7,7 +7,7 @@ pub fn shop_next_url_calculator( pagination: &ShopPaginatedRequest, next_cursor: u32, ) -> String { - let base_url = "https://backbybe.fly.dev/bestiary/list/"; + let base_url = "https://backbybe.fly.dev/shop/list/"; let filter_query = shop_filter_query_calculator(field_filters); let pagination_query = format!( @@ -125,20 +125,12 @@ fn creature_filter_query_calculator(field_filters: &CreatureFieldFilters) -> Str fn shop_filter_query_calculator(field_filters: &ItemFieldFilters) -> String { let queries: Vec = [ - field_filters - .name_filter - .clone() - .map(|name| format!("name_filter={}", name)), field_filters .min_bulk_filter .map(|bulk| format!("min_bulk_filter={}", bulk)), field_filters .max_bulk_filter .map(|bulk| format!("max_bulk_filter={}", bulk)), - field_filters - .category_filter - .clone() - .map(|cat| format!("category_filter={}", cat)), field_filters .min_hardness_filter .map(|hn| format!("min_hardness_filter={}", hn)), @@ -169,18 +161,6 @@ fn shop_filter_query_calculator(field_filters: &ItemFieldFilters) -> String { field_filters .max_n_of_uses_filter .map(|uses| format!("max_n_of_uses_filter={}", uses)), - field_filters - .type_filter - .clone() - .map(|x| format!("type_filter={}", x)), - field_filters - .rarity_filter - .clone() - .map(|rar| format!("rarity_filter={}", rar)), - field_filters - .size_filter - .clone() - .map(|size| format!("size_filter={}", size)), ] .iter() .filter_map(|opt| opt.clone())