diff --git a/Cargo.toml b/Cargo.toml index 25dee57..4c75063 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bybe" -version = "2.3.0" +version = "2.4.0" authors = ["RakuJa"] # Compiler info @@ -54,11 +54,11 @@ utoipa-swagger-ui = { version = "8.0.3", features = ["actix-web", "reqwest"] } sqlx = { version = "0.8.2", features = ["runtime-async-std", "sqlite"] } cached = { version = "0.54.0", features = ["async"] } -anyhow = "1.0.93" -serde = { version = "1.0.214", features = ["derive"] } -serde_json = "1.0.132" +anyhow = "1.0.94" +serde = { version = "1.0.216", features = ["derive"] } +serde_json = "1.0.133" strum = {version="0.26.3", features = ["derive"]} -fastrand = "2.2.0" +fastrand = "2.3.0" counter = "0.6.0" ordered-float = { version = "4", features = ["serde"]} num-traits = "0.2.19" @@ -76,7 +76,7 @@ once = "0.3.4" [build-dependencies] tokio = { version = "1", features = ["macros", "rt-multi-thread", "rt"] } -anyhow = "1.0.93" +anyhow = "1.0.94" sqlx = {version = "0.8.2", features = ["runtime-async-std", "sqlite"]} dotenvy = "0.15.7" diff --git a/src/db/bestiary_proxy.rs b/src/db/bestiary_proxy.rs index dfa07b2..426f076 100644 --- a/src/db/bestiary_proxy.rs +++ b/src/db/bestiary_proxy.rs @@ -1,9 +1,11 @@ use crate::models::creature::creature_struct::Creature; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use crate::db::data_providers::creature_fetcher::fetch_traits_associated_with_creatures; use crate::db::data_providers::{creature_fetcher, generic_fetcher}; -use crate::models::bestiary_structs::{BestiaryPaginatedRequest, CreatureSortEnum}; +use crate::models::bestiary_structs::{ + BestiaryFilterQuery, BestiaryPaginatedRequest, CreatureSortEnum, +}; use crate::models::creature::creature_component::creature_core::CreatureCoreData; use crate::models::creature::creature_filter_enum::{CreatureFilter, FieldsUniqueValuesStruct}; use crate::models::creature::creature_metadata::alignment_enum::AlignmentEnum; @@ -16,6 +18,7 @@ use crate::models::routers_validator_structs::{CreatureFieldFilters, OrderEnum}; use crate::AppState; use anyhow::Result; use cached::proc_macro::once; +use itertools::Itertools; use strum::IntoEnumIterator; pub async fn get_creature_by_id( @@ -94,6 +97,35 @@ pub async fn get_paginated_creatures( .essential .family .cmp(&b.core_data.essential.family), + CreatureSortEnum::Alignment => a + .core_data + .essential + .alignment + .cmp(&b.core_data.essential.alignment), + CreatureSortEnum::Attack => a + .core_data + .derived + .attack_data + .cmp(&b.core_data.derived.attack_data), + CreatureSortEnum::Role => { + let threshold = filters.role_threshold.unwrap_or(0); + a.core_data + .derived + .role_data + .iter() + .filter(|(_, role_value)| **role_value > threshold) + .map(|(role, _)| role) + .collect::>() + .cmp( + &b.core_data + .derived + .role_data + .iter() + .filter(|(_, role_affinity)| **role_affinity > threshold) + .map(|(x, _)| x) + .collect::>(), + ) + } }; match pagination .bestiary_sort_data @@ -105,11 +137,14 @@ pub async fn get_paginated_creatures( OrderEnum::Descending => cmp.reverse(), } }); - let curr_slice: Vec = filtered_list .iter() .skip(pagination.paginated_request.cursor as usize) - .take(pagination.paginated_request.page_size.unsigned_abs() as usize) + .take(if pagination.paginated_request.page_size >= 0 { + pagination.paginated_request.page_size.unsigned_abs() as usize + } else { + usize::MAX + }) .cloned() .collect(); @@ -118,16 +153,24 @@ pub async fn get_paginated_creatures( pub async fn get_creatures_passing_all_filters( app_state: &AppState, - key_value_filters: HashMap>, + filters: &BestiaryFilterQuery, fetch_weak: bool, fetch_elite: bool, ) -> Result> { let mut creature_vec = Vec::new(); - let level_vec = key_value_filters - .get(&CreatureFilter::Level) - .map_or_else(HashSet::new, std::clone::Clone::clone); - let modified_filters = - prepare_filters_for_db_communication(key_value_filters, fetch_weak, fetch_elite); + let level_vec = filters.creature_table_fields_filter.level_filter.clone(); + let mut modified_filters = filters.clone(); + modified_filters.creature_table_fields_filter.level_filter = + prepare_level_filter_for_db_communication( + filters + .creature_table_fields_filter + .level_filter + .clone() + .into_iter(), + fetch_weak, + fetch_elite, + ); + for core in creature_fetcher::fetch_creatures_core_data_with_filters(&app_state.conn, &modified_filters) .await? @@ -137,13 +180,13 @@ pub async fn get_creatures_passing_all_filters( // mean that if we have [0,1,2,3] in the filter and allow_elite => [-1,0,1,2,3] then // a creature of level 1 will always be considered the elite variant of level 0. We'll // duplicate the data and will have a base 0 for level 0 and elite 0 for level 1 - if fetch_weak && level_vec.contains(&(core.essential.base_level - 1).to_string()) { + if fetch_weak && level_vec.contains(&(core.essential.base_level - 1)) { creature_vec.push(Creature::from_core_with_variant( core.clone(), CreatureVariant::Weak, )); } - if fetch_elite && level_vec.contains(&(core.essential.base_level + 1).to_string()) { + if fetch_elite && level_vec.contains(&(core.essential.base_level + 1)) { creature_vec.push(Creature::from_core_with_variant( core.clone(), CreatureVariant::Elite, @@ -162,7 +205,7 @@ pub async fn get_all_possible_values_of_filter( let mut x = match field { CreatureFilter::Size => runtime_fields_values.list_of_sizes, CreatureFilter::Rarity => runtime_fields_values.list_of_rarities, - CreatureFilter::Ranged | CreatureFilter::Melee | CreatureFilter::SpellCaster => { + CreatureFilter::Ranged | CreatureFilter::Melee | CreatureFilter::Spellcaster => { vec![true.to_string(), false.to_string()] } CreatureFilter::Family => runtime_fields_values.list_of_families, @@ -227,12 +270,12 @@ async fn get_all_keys(app_state: &AppState) -> FieldsUniqueValuesStruct { /// Gets all the creature core data from the DB. It will not fetch data outside of variant and core. /// It will cache the result. -#[once(sync_writes = true, result = true)] async fn get_all_creatures_from_db(app_state: &AppState) -> Result> { creature_fetcher::fetch_creatures_core_data(&app_state.conn, 0, -1).await } /// Infallible method, it will expose a vector representing the values fetched from db or empty vec +#[once(sync_writes = true)] async fn get_list(app_state: &AppState, variant: CreatureVariant) -> Vec { if let Ok(creatures) = get_all_creatures_from_db(app_state).await { return match variant { @@ -261,30 +304,27 @@ pub fn order_list_by_level(creature_list: &[Creature]) -> HashMap weak = level -fn prepare_filters_for_db_communication( - key_value_filters: HashMap>, +fn prepare_level_filter_for_db_communication( + level_filter: I, fetch_weak: bool, fetch_elite: bool, -) -> HashMap> { - key_value_filters - .into_iter() - .map(|(key, values)| match key { - CreatureFilter::Level => { - let mut new_values = HashSet::new(); - for str_level in values { - if let Ok(level) = str_level.parse::() { - if fetch_weak { - new_values.insert((level + 1).to_string()); - } - if fetch_elite { - new_values.insert((level - 1).to_string()); - } - new_values.insert(level.to_string()); - } - } - (key, new_values) - } - _ => (key, values), - }) - .collect() +) -> Vec +where + I: Iterator, +{ + // do not remove sorted, it would break contract with merge and dedup + let levels = level_filter.sorted().collect::>(); + let levels_for_elite: Vec = if fetch_elite { + levels.iter().map(|x| x - 1).collect() + } else { + vec![] + }; + let levels_for_weak: Vec = if fetch_weak { + levels.iter().map(|x| x + 1).collect() + } else { + vec![] + }; + + let x = itertools::merge(levels_for_elite, levels_for_weak).collect::>(); + itertools::merge(x, levels).dedup().collect::>() } diff --git a/src/db/cr_core_initializer.rs b/src/db/cr_core_initializer.rs index 690ddb1..fc5b758 100644 --- a/src/db/cr_core_initializer.rs +++ b/src/db/cr_core_initializer.rs @@ -104,7 +104,7 @@ async fn update_role_column_value( creature_id ) } - CreatureRoleEnum::SpellCaster => { + CreatureRoleEnum::Spellcaster => { sqlx::query!( "UPDATE CREATURE_CORE SET spell_caster_percentage = ? WHERE id = ?", value, diff --git a/src/db/data_providers/creature_fetcher.rs b/src/db/data_providers/creature_fetcher.rs index 017faa5..101bf20 100644 --- a/src/db/data_providers/creature_fetcher.rs +++ b/src/db/data_providers/creature_fetcher.rs @@ -3,6 +3,7 @@ use crate::db::data_providers::generic_fetcher::{ 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::bestiary_structs::BestiaryFilterQuery; use crate::models::creature::creature_component::creature_combat::{ CreatureCombatData, SavingThrows, }; @@ -10,16 +11,15 @@ use crate::models::creature::creature_component::creature_core::CreatureCoreData use crate::models::creature::creature_component::creature_extra::{ AbilityScores, CreatureExtraData, }; -use crate::models::creature::creature_component::creature_spell_caster::CreatureSpellCasterData; +use crate::models::creature::creature_component::creature_spell_caster::CreatureSpellcasterData; use crate::models::creature::creature_component::creature_variant::CreatureVariantData; -use crate::models::creature::creature_filter_enum::CreatureFilter; use crate::models::creature::creature_metadata::alignment_enum::ALIGNMENT_TRAITS; use crate::models::creature::creature_metadata::variant_enum::CreatureVariant; use crate::models::creature::creature_struct::Creature; use crate::models::creature::items::action::Action; use crate::models::creature::items::skill::Skill; use crate::models::creature::items::spell::Spell; -use crate::models::creature::items::spell_caster_entry::SpellCasterEntry; +use crate::models::creature::items::spell_caster_entry::SpellcasterEntry; use crate::models::db::raw_immunity::RawImmunity; use crate::models::db::raw_language::RawLanguage; use crate::models::db::raw_resistance::RawResistance; @@ -46,7 +46,6 @@ use crate::models::scales_struct::strike_bonus_scales::StrikeBonusScales; use crate::models::scales_struct::strike_dmg_scales::StrikeDmgScales; use anyhow::Result; use sqlx::{Pool, Sqlite}; -use std::collections::{HashMap, HashSet}; async fn fetch_creature_immunities( conn: &Pool, @@ -423,9 +422,9 @@ async fn fetch_creature_spells(conn: &Pool, creature_id: i64) -> Result< async fn fetch_creature_spell_caster_entry( conn: &Pool, creature_id: i64, -) -> Result { +) -> Result { Ok(sqlx::query_as!( - SpellCasterEntry, + SpellcasterEntry, "SELECT spell_casting_name, is_spell_casting_flexible, type_of_spell_caster, spell_casting_dc_mod, spell_casting_atk_mod, spell_casting_tradition FROM CREATURE_TABLE WHERE id == ($1) LIMIT 1", creature_id ).fetch_one(conn).await?) @@ -524,9 +523,9 @@ pub async fn fetch_creature_by_id( pub async fn fetch_creatures_core_data_with_filters( conn: &Pool, - key_value_filters: &HashMap>, + bestiary_filter_query: &BestiaryFilterQuery, ) -> Result> { - let query = prepare_filtered_get_creatures_core(key_value_filters); + let query = prepare_filtered_get_creatures_core(bestiary_filter_query); let core_data: Vec = sqlx::query_as(query.as_str()).fetch_all(conn).await?; Ok(update_creatures_core_with_traits(conn, core_data).await) } @@ -642,10 +641,10 @@ pub async fn fetch_creature_combat_data( pub async fn fetch_creature_spell_caster_data( conn: &Pool, creature_id: i64, -) -> Result { +) -> Result { let spells = fetch_creature_spells(conn, creature_id).await?; let spell_caster_entry = fetch_creature_spell_caster_entry(conn, creature_id).await?; - Ok(CreatureSpellCasterData { + Ok(CreatureSpellcasterData { spells, spell_caster_entry, }) diff --git a/src/db/data_providers/raw_query_builder.rs b/src/db/data_providers/raw_query_builder.rs index b6aec3a..a6131c4 100644 --- a/src/db/data_providers/raw_query_builder.rs +++ b/src/db/data_providers/raw_query_builder.rs @@ -1,10 +1,8 @@ -use crate::models::creature::creature_filter_enum::CreatureFilter; +use crate::models::bestiary_structs::{BestiaryFilterQuery, CreatureTableFieldsFilter}; +use crate::models::creature::creature_metadata::creature_role::CreatureRoleEnum; use crate::models::item::item_metadata::type_enum::ItemTypeEnum; use crate::models::shop_structs::{ItemTableFieldsFilter, ShopFilterQuery}; use log::debug; -use std::collections::{HashMap, HashSet}; - -const ACCURACY_THRESHOLD: i64 = 50; pub fn prepare_filtered_get_items(shop_filter_query: &ShopFilterQuery) -> String { let equipment_query = prepare_item_subquery( @@ -49,67 +47,26 @@ pub fn prepare_filtered_get_items(shop_filter_query: &ShopFilterQuery) -> String debug!("{}", query); query } -pub fn prepare_filtered_get_creatures_core( - key_value_filters: &HashMap>, -) -> String { - let mut simple_core_query = String::new(); - let mut trait_query = String::new(); - for (key, value) in key_value_filters { - match key { - CreatureFilter::Level - | CreatureFilter::PathfinderVersion - | CreatureFilter::Melee - | CreatureFilter::Ranged - | CreatureFilter::SpellCaster => { - if !simple_core_query.is_empty() { - simple_core_query.push_str(" AND "); - } - simple_core_query.push_str( - prepare_in_statement_for_generic_type(key.to_string().as_str(), value.iter()) - .as_str(), - ); - } - CreatureFilter::Family - | CreatureFilter::Alignment - | CreatureFilter::Size - | CreatureFilter::Rarity - | CreatureFilter::CreatureTypes => { - if !simple_core_query.is_empty() { - simple_core_query.push_str(" AND "); - } - simple_core_query.push_str( - prepare_case_insensitive_in_statement( - key.to_string().as_str(), - value.iter().cloned(), - ) - .as_str(), - ); - } - CreatureFilter::Traits => { - trait_query.push_str(prepare_creature_trait_filter(value.iter().cloned()).as_str()); - } - CreatureFilter::CreatureRoles => { - if !simple_core_query.is_empty() { - simple_core_query.push_str(" AND "); - } - simple_core_query - .push_str(prepare_bounded_or_check(value, ACCURACY_THRESHOLD, 100).as_str()); - } - CreatureFilter::Sources => (), // Never given as value to filter - } - } - let mut where_query = simple_core_query.to_string(); - if !trait_query.is_empty() { - where_query.push_str(format!(" AND id IN ({trait_query}) GROUP BY cc.id").as_str()); - } - if !where_query.is_empty() { - where_query = format!("WHERE {where_query}"); - } +pub fn prepare_filtered_get_creatures_core(bestiary_filter_query: &BestiaryFilterQuery) -> String { + let initial_statement = "SELECT id FROM CREATURE_CORE"; + let trait_query_tmp = prepare_trait_filter_statement( + &prepare_creature_trait_filter(bestiary_filter_query.trait_whitelist_filter.iter()), + &prepare_creature_trait_filter(bestiary_filter_query.trait_blacklist_filter.iter()), + ); + let trait_query = if trait_query_tmp.is_empty() { + String::new() + } else { + format!("AND {trait_query_tmp}") + }; + let creature_fields_filter_query = + prepare_creature_filter_statement(&bestiary_filter_query.creature_table_fields_filter); + let where_query = + format!("{initial_statement} WHERE {creature_fields_filter_query} {trait_query}"); let query = format!( " WITH CreatureRankedByLevel AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY level ORDER BY RANDOM()) AS rn - FROM CREATURE_CORE cc {where_query} + FROM CREATURE_CORE cc WHERE cc.id IN ({where_query}) ) SELECT * FROM CreatureRankedByLevel WHERE id IN ( SELECT id FROM CreatureRankedByLevel WHERE rn>1 ORDER BY RANDOM() LIMIT 20 @@ -122,24 +79,21 @@ pub fn prepare_filtered_get_creatures_core( query } -/// Prepares a 'bounded OR statement' aka checks if all the columns are in the bound given, ex +/// Prepares a 'bounded OR statement' aka checks if all columns values are in the bound given, ex /// ```SQL -/// (brute_percentage >= 0 AND brute_percentage <= 0) OR (sniper_percentage >= 0 ...) ... +/// (brute_percentage >= 0 AND brute_percentage <= 0) AND (sniper_percentage >= 0 ...) ... /// ``` -fn prepare_bounded_or_check( - column_names: &HashSet, - lower_bound: i64, - upper_bound: i64, -) -> String { - let mut bounded_query = String::new(); - for column in column_names { - if !bounded_query.is_empty() { - bounded_query.push_str(" OR "); - } - bounded_query - .push_str(prepare_bounded_check(column.as_str(), lower_bound, upper_bound).as_str()); - } - bounded_query +fn prepare_bounded_and_check(column_names: I, lower_bound: i64, upper_bound: i64) -> String +where + I: Iterator, + S: ToString, +{ + column_names + .map(|x| x.to_string()) + .filter(|column| !column.is_empty()) + .map(|column| prepare_bounded_check(column.as_str(), lower_bound, upper_bound)) + .collect::>() + .join(" AND ") } /// Prepares a 'bounded statement' aka (x>=lb AND x<=ub) @@ -228,6 +182,7 @@ where } result_string } + fn prepare_item_subquery( item_type: &ItemTypeEnum, n_of_item: i64, @@ -241,9 +196,9 @@ where { let item_type_query = prepare_get_id_matching_item_type_query(item_type); let initial_statement = "SELECT id FROM ITEM_TABLE"; - - let trait_query_tmp = - prepare_trait_filter_statement(trait_whitelist_filter, trait_blacklist_filter); + let whitelist_query = prepare_item_trait_filter(trait_whitelist_filter); + let blacklist_query = prepare_item_trait_filter(trait_blacklist_filter); + let trait_query_tmp = prepare_trait_filter_statement(&whitelist_query, &blacklist_query); let trait_query = if trait_query_tmp.is_empty() { String::new() } else { @@ -287,17 +242,77 @@ fn prepare_item_filter_statement(shop_filter_vectors: &ItemTableFieldsFilter) -> } } +fn prepare_creature_filter_statement( + bestiary_filter_vectors: &CreatureTableFieldsFilter, +) -> String { + let remaster_query = prepare_in_statement_for_generic_type( + "remaster", + bestiary_filter_vectors.supported_version.iter(), + ); + let filters_query = vec![ + prepare_case_insensitive_in_statement( + "source", + bestiary_filter_vectors.source_filter.iter(), + ), + prepare_case_insensitive_in_statement( + "family", + bestiary_filter_vectors.family_filter.iter(), + ), + prepare_case_insensitive_in_statement( + "alignment", + bestiary_filter_vectors.alignment_filter.iter(), + ), + prepare_case_insensitive_in_statement("size", bestiary_filter_vectors.size_filter.iter()), + prepare_case_insensitive_in_statement( + "rarity", + bestiary_filter_vectors.rarity_filter.iter(), + ), + prepare_case_insensitive_in_statement( + "cr_type", + bestiary_filter_vectors.type_filter.iter(), + ), + prepare_in_statement_for_generic_type( + "is_spell_caster", + bestiary_filter_vectors.is_spellcaster_filter.iter(), + ), + prepare_in_statement_for_generic_type( + "is_ranged", + bestiary_filter_vectors.is_ranged_filter.iter(), + ), + prepare_in_statement_for_generic_type( + "is_melee", + bestiary_filter_vectors.is_melee_filter.iter(), + ), + prepare_bounded_and_check( + bestiary_filter_vectors + .role_filter + .iter() + .map(CreatureRoleEnum::to_db_column), + i64::from(bestiary_filter_vectors.role_lower_threshold), + i64::from(bestiary_filter_vectors.role_upper_threshold), + ), + ] + .into_iter() + .filter(|query| !query.is_empty()) + .collect::>() + .join(" AND "); + if filters_query.is_empty() { + remaster_query + } else { + format!("{remaster_query} AND {filters_query}") + } +} + /// Prepares an 'in' statement, with the following logic /// ```SQL /// id NOT IN (bl_id1, bl_id2, bl_idn) AND id IN (wl_id1, wl_id2, wl_idn) /// ``` -fn prepare_trait_filter_statement(whitelist: I, blacklist: I) -> String +fn prepare_trait_filter_statement(whitelist: &S, blacklist: &S) -> String where - I: Iterator, S: ToString, { - let whitelist_query = prepare_item_trait_filter(whitelist); - let blacklist_query = prepare_item_trait_filter(blacklist); + let whitelist_query = whitelist.to_string(); + let blacklist_query = blacklist.to_string(); if whitelist_query.is_empty() && blacklist_query.is_empty() { String::new() } else if whitelist_query.is_empty() { @@ -338,36 +353,36 @@ fn prepare_get_id_matching_item_type_query(item_type: &ItemTypeEnum) -> String { /// Prepares a query that gets all the ids linked with a given list of traits, example /// ```SQL -/// SELECT tcat.creature_id -/// FROM TRAIT_CREATURE_ASSOCIATION_TABLE tcat +/// SELECT tcat.item_id +/// FROM TRAIT_ITEM_ASSOCIATION_TABLE tcat /// RIGHT JOIN /// (SELECT * FROM TRAIT_TABLE WHERE name IN ('good')) tt -/// ON tcat.trait_id = tt.name GROUP BY tcat.creature_id +/// ON tcat.trait_id = tt.name GROUP BY tcat.item_id ///``` -fn prepare_creature_trait_filter(column_values: I) -> String +fn prepare_item_trait_filter(column_values: I) -> String where I: Iterator, S: ToString, { - prepare_trait_filter( - "creature_id", - "TRAIT_CREATURE_ASSOCIATION_TABLE", - column_values, - ) + prepare_trait_filter("item_id", "TRAIT_ITEM_ASSOCIATION_TABLE", column_values) } /// Prepares a query that gets all the ids linked with a given list of traits, example /// ```SQL -/// SELECT tcat.item_id -/// FROM TRAIT_ITEM_ASSOCIATION_TABLE tcat +/// SELECT tcat.creature_id +/// FROM TRAIT_CREATURE_ASSOCIATION_TABLE tcat /// RIGHT JOIN /// (SELECT * FROM TRAIT_TABLE WHERE name IN ('good')) tt -/// ON tcat.trait_id = tt.name GROUP BY tcat.item_id +/// ON tcat.trait_id = tt.name GROUP BY tcat.creature_id ///``` -fn prepare_item_trait_filter(column_values: I) -> String +fn prepare_creature_trait_filter(column_values: I) -> String where I: Iterator, S: ToString, { - prepare_trait_filter("item_id", "TRAIT_ITEM_ASSOCIATION_TABLE", column_values) + prepare_trait_filter( + "creature_id", + "TRAIT_CREATURE_ASSOCIATION_TABLE", + column_values, + ) } diff --git a/src/db/shop_proxy.rs b/src/db/shop_proxy.rs index 2291c57..864df7d 100644 --- a/src/db/shop_proxy.rs +++ b/src/db/shop_proxy.rs @@ -67,7 +67,11 @@ pub async fn get_paginated_items( let curr_slice: Vec = filtered_list .iter() .skip(pagination.paginated_request.cursor as usize) - .take(pagination.paginated_request.page_size.unsigned_abs() as usize) + .take(if pagination.paginated_request.page_size >= 0 { + pagination.paginated_request.page_size.unsigned_abs() as usize + } else { + usize::MAX + }) .cloned() .collect(); diff --git a/src/models/bestiary_structs.rs b/src/models/bestiary_structs.rs index 4ae99b2..4566dbd 100644 --- a/src/models/bestiary_structs.rs +++ b/src/models/bestiary_structs.rs @@ -1,4 +1,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::routers_validator_structs::{OrderEnum, PaginatedRequest}; +use crate::models::shared::rarity_enum::RarityEnum; +use crate::models::shared::size_enum::SizeEnum; use serde::{Deserialize, Serialize}; use strum::Display; use utoipa::{IntoParams, ToSchema}; @@ -24,6 +29,12 @@ pub enum CreatureSortEnum { Rarity, #[serde(alias = "family", alias = "FAMILY")] Family, + #[serde(alias = "alignment", alias = "ALIGNMENT")] + Alignment, + #[serde(alias = "attack", alias = "ATTACK")] + Attack, + #[serde(alias = "role", alias = "ROLE")] + Role, } #[derive(Serialize, Deserialize, IntoParams, ToSchema, Eq, PartialEq, Hash, Default)] @@ -37,3 +48,39 @@ pub struct BestiaryPaginatedRequest { pub paginated_request: PaginatedRequest, pub bestiary_sort_data: BestiarySortData, } + +#[derive(Clone)] +pub struct CreatureTableFieldsFilter { + pub source_filter: Vec, + pub family_filter: Vec, + pub alignment_filter: Vec, + pub size_filter: Vec, + pub rarity_filter: Vec, + pub type_filter: Vec, + pub role_filter: Vec, + pub role_lower_threshold: u8, + pub role_upper_threshold: u8, + pub is_melee_filter: Vec, + pub is_ranged_filter: Vec, + pub is_spellcaster_filter: Vec, + pub supported_version: Vec, + + pub level_filter: Vec, +} + +impl CreatureTableFieldsFilter { + pub const fn default_lower_threshold() -> u8 { + 50 + } + + pub const fn default_upper_threshold() -> u8 { + 100 + } +} + +#[derive(Clone)] +pub struct BestiaryFilterQuery { + pub creature_table_fields_filter: CreatureTableFieldsFilter, + pub trait_whitelist_filter: Vec, + pub trait_blacklist_filter: Vec, +} diff --git a/src/models/creature/creature_component/creature_core.rs b/src/models/creature/creature_component/creature_core.rs index 6820b3a..8035c7d 100644 --- a/src/models/creature/creature_component/creature_core.rs +++ b/src/models/creature/creature_component/creature_core.rs @@ -3,8 +3,11 @@ use crate::models::creature::creature_metadata::type_enum::CreatureTypeEnum; use crate::models::shared::rarity_enum::RarityEnum; use crate::models::shared::size_enum::SizeEnum; use serde::{Deserialize, Serialize}; +#[allow(unused_imports)] +use serde_json::json; use sqlx::sqlite::SqliteRow; use sqlx::{Error, FromRow, Row}; +use std::collections::BTreeMap; use utoipa::ToSchema; #[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] @@ -36,24 +39,10 @@ pub struct EssentialData { pub struct DerivedData { pub archive_link: Option, - pub is_melee: bool, - pub is_ranged: bool, - pub is_spell_caster: bool, - - #[schema(example = 50)] - pub brute_percentage: i64, - #[schema(example = 50)] - pub magical_striker_percentage: i64, - #[schema(example = 50)] - pub skill_paragon_percentage: i64, - #[schema(example = 50)] - pub skirmisher_percentage: i64, - #[schema(example = 50)] - pub sniper_percentage: i64, - #[schema(example = 50)] - pub soldier_percentage: i64, - #[schema(example = 50)] - pub spell_caster_percentage: i64, + #[schema(example = json!({"melee": true, "ranged": false, "spellcaster": true}))] + pub attack_data: BTreeMap, + #[schema(example = json!({"brute": 50, "magical_striker": 30, "skill_paragon": 2, "skirmisher": 3, "sniper": 0, "soldier": 30, "spellcaster": 90}))] + pub role_data: BTreeMap, } impl<'r> FromRow<'r, SqliteRow> for EssentialData { @@ -81,18 +70,35 @@ impl<'r> FromRow<'r, SqliteRow> for EssentialData { impl<'r> FromRow<'r, SqliteRow> for DerivedData { fn from_row(row: &'r SqliteRow) -> Result { + let mut attack_list = BTreeMap::new(); + attack_list.insert(String::from("melee"), row.try_get("is_melee")?); + attack_list.insert(String::from("ranged"), row.try_get("is_ranged")?); + attack_list.insert(String::from("spellcaster"), row.try_get("is_spell_caster")?); + + let mut role_list = BTreeMap::new(); + role_list.insert(String::from("brute"), row.try_get("brute_percentage")?); + role_list.insert( + String::from("magical_striker"), + row.try_get("magical_striker_percentage")?, + ); + role_list.insert( + String::from("skill_paragon"), + row.try_get("skill_paragon_percentage")?, + ); + role_list.insert( + String::from("skirmisher"), + row.try_get("skirmisher_percentage")?, + ); + role_list.insert(String::from("sniper"), row.try_get("sniper_percentage")?); + role_list.insert(String::from("soldier"), row.try_get("soldier_percentage")?); + role_list.insert( + String::from("spellcaster"), + row.try_get("spell_caster_percentage")?, + ); Ok(Self { archive_link: row.try_get("archive_link").ok(), - is_melee: row.try_get("is_melee")?, - is_ranged: row.try_get("is_ranged")?, - is_spell_caster: row.try_get("is_spell_caster")?, - brute_percentage: row.try_get("brute_percentage")?, - magical_striker_percentage: row.try_get("magical_striker_percentage")?, - skill_paragon_percentage: row.try_get("skill_paragon_percentage")?, - skirmisher_percentage: row.try_get("skirmisher_percentage")?, - sniper_percentage: row.try_get("sniper_percentage")?, - soldier_percentage: row.try_get("soldier_percentage")?, - spell_caster_percentage: row.try_get("spell_caster_percentage")?, + attack_data: attack_list, + role_data: role_list, }) } } diff --git a/src/models/creature/creature_component/creature_spell_caster.rs b/src/models/creature/creature_component/creature_spell_caster.rs index 1f2b46e..f5932ef 100644 --- a/src/models/creature/creature_component/creature_spell_caster.rs +++ b/src/models/creature/creature_component/creature_spell_caster.rs @@ -1,16 +1,16 @@ 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 crate::models::creature::items::spell_caster_entry::SpellcasterEntry; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; #[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] -pub struct CreatureSpellCasterData { +pub struct CreatureSpellcasterData { pub spells: Vec, - pub spell_caster_entry: SpellCasterEntry, + pub spell_caster_entry: SpellcasterEntry, } -impl CreatureSpellCasterData { +impl CreatureSpellcasterData { pub fn add_mod_to_spellcaster_atk_and_dc(self, modifier: i64) -> Self { let mut spell_data = self; diff --git a/src/models/creature/creature_component/filter_struct.rs b/src/models/creature/creature_component/filter_struct.rs deleted file mode 100644 index 3508484..0000000 --- a/src/models/creature/creature_component/filter_struct.rs +++ /dev/null @@ -1,19 +0,0 @@ -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::pf_version_enum::PathfinderVersionEnum; -use crate::models::shared::rarity_enum::RarityEnum; -use crate::models::shared::size_enum::SizeEnum; -use std::collections::HashSet; - -pub struct FilterStruct { - pub families: Option>, - pub traits: Option>, - pub rarities: Option>, - pub sizes: Option>, - pub alignments: Option>, - pub creature_types: Option>, - pub creature_roles: Option>, - pub lvl_combinations: HashSet, - pub pathfinder_version: PathfinderVersionEnum, -} diff --git a/src/models/creature/creature_component/mod.rs b/src/models/creature/creature_component/mod.rs index 288e9a1..e3692a9 100644 --- a/src/models/creature/creature_component/mod.rs +++ b/src/models/creature/creature_component/mod.rs @@ -3,4 +3,3 @@ pub mod creature_core; pub mod creature_extra; pub mod creature_spell_caster; pub mod creature_variant; -pub mod filter_struct; diff --git a/src/models/creature/creature_filter_enum.rs b/src/models/creature/creature_filter_enum.rs index 6a2b21e..55eb9a5 100644 --- a/src/models/creature/creature_filter_enum.rs +++ b/src/models/creature/creature_filter_enum.rs @@ -18,7 +18,7 @@ pub enum CreatureFilter { #[serde(alias = "is_ranged", alias = "IS_RANGED")] Ranged, #[serde(alias = "is_spell_caster", alias = "IS_SPELL_CASTER")] - SpellCaster, + Spellcaster, #[serde(alias = "sources", alias = "SOURCES")] Sources, #[serde(alias = "traits", alias = "TRAITS")] @@ -51,7 +51,7 @@ impl fmt::Display for CreatureFilter { Self::Ranged => { write!(f, "is_ranged") } - Self::SpellCaster => { + Self::Spellcaster => { write!(f, "is_spell_caster") } Self::Traits => { diff --git a/src/models/creature/creature_metadata/alignment_enum.rs b/src/models/creature/creature_metadata/alignment_enum.rs index 2460a97..17829e0 100644 --- a/src/models/creature/creature_metadata/alignment_enum.rs +++ b/src/models/creature/creature_metadata/alignment_enum.rs @@ -48,9 +48,6 @@ pub enum AlignmentEnum { #[serde(rename = "No Alignment")] #[default] No, // no alignment - #[strum(to_string = "Any")] - #[serde(rename = "Any")] - Any, // can be every alignment } pub const ALIGNMENT_TRAITS: [&str; 4] = ["GOOD", "EVIL", "CHAOTIC", "LAWFUL"]; @@ -119,7 +116,6 @@ impl FromStr for AlignmentEnum { "LE" => Ok(Self::Le), "LN" => Ok(Self::Ln), "LG" => Ok(Self::Lg), - "ANY" => Ok(Self::Any), _ => Ok(Self::No), } } @@ -138,7 +134,6 @@ impl Clone for AlignmentEnum { Self::Ln => Self::Ln, Self::Lg => Self::Lg, Self::No => Self::No, - Self::Any => Self::Any, } } } diff --git a/src/models/creature/creature_metadata/creature_role.rs b/src/models/creature/creature_metadata/creature_role.rs index 9e1ffa9..d1e7836 100644 --- a/src/models/creature/creature_metadata/creature_role.rs +++ b/src/models/creature/creature_metadata/creature_role.rs @@ -1,7 +1,7 @@ use crate::models::creature::creature_component::creature_combat::CreatureCombatData; use crate::models::creature::creature_component::creature_core::EssentialData; use crate::models::creature::creature_component::creature_extra::CreatureExtraData; -use crate::models::creature::creature_component::creature_spell_caster::CreatureSpellCasterData; +use crate::models::creature::creature_component::creature_spell_caster::CreatureSpellcasterData; use crate::models::item::item_metadata::type_enum::WeaponTypeEnum; use crate::models::scales_struct::creature_scales::CreatureScales; use num_traits::float::FloatConst; @@ -28,7 +28,7 @@ pub enum CreatureRoleEnum { Skirmisher, Sniper, Soldier, - SpellCaster, + Spellcaster, } fn get_dmg_from_regex(raw_str: &str) -> Option { @@ -46,14 +46,14 @@ impl CreatureRoleEnum { Self::Skirmisher => String::from("skirmisher_percentage"), Self::Sniper => String::from("sniper_percentage"), Self::Soldier => String::from("soldier_percentage"), - Self::SpellCaster => String::from("spell_caster_percentage"), + Self::Spellcaster => String::from("spell_caster_percentage"), } } pub fn from_creature_with_given_scales( cr_core: &EssentialData, cr_extra: &CreatureExtraData, cr_combat: &CreatureCombatData, - cr_spells: &CreatureSpellCasterData, + cr_spells: &CreatureSpellcasterData, scales: &CreatureScales, ) -> BTreeMap { let mut roles = BTreeMap::new(); @@ -87,7 +87,7 @@ impl CreatureRoleEnum { .map_or(0, |x| (x * 100.).round() as i64), ); roles.insert( - Self::SpellCaster, + Self::Spellcaster, is_spellcaster(cr_core, cr_spells, cr_combat, cr_extra, scales) .map_or(0, |x| (x * 100.).round() as i64), ); @@ -293,7 +293,7 @@ pub fn is_soldier( // Magical Striker pub fn is_magical_striker( cr_core: &EssentialData, - cr_spell: &CreatureSpellCasterData, + cr_spell: &CreatureSpellcasterData, cr_combat: &CreatureCombatData, scales: &CreatureScales, ) -> Option { @@ -395,7 +395,7 @@ fn is_skill_paragon( // Spellcaster fn is_spellcaster( cr_core: &EssentialData, - cr_spell: &CreatureSpellCasterData, + cr_spell: &CreatureSpellcasterData, cr_combat: &CreatureCombatData, cr_extra: &CreatureExtraData, scales: &CreatureScales, @@ -448,7 +448,7 @@ impl FromStr for CreatureRoleEnum { "SKIRMISHER" => Ok(Self::Skirmisher), "SNIPER" => Ok(Self::Sniper), "SOLDIER" => Ok(Self::Soldier), - "SPELLCASTER" | "SPELL CASTER" => Ok(Self::SpellCaster), + "SPELLCASTER" | "SPELL CASTER" => Ok(Self::Spellcaster), _ => Err(()), } } @@ -475,8 +475,8 @@ impl fmt::Display for CreatureRoleEnum { Self::Soldier => { write!(f, "Soldier") } - Self::SpellCaster => { - write!(f, "SpellCaster") + Self::Spellcaster => { + write!(f, "Spellcaster") } } } diff --git a/src/models/creature/creature_struct.rs b/src/models/creature/creature_struct.rs index 7d5c318..db160b2 100644 --- a/src/models/creature/creature_struct.rs +++ b/src/models/creature/creature_struct.rs @@ -1,7 +1,7 @@ use crate::models::creature::creature_component::creature_combat::CreatureCombatData; use crate::models::creature::creature_component::creature_core::CreatureCoreData; use crate::models::creature::creature_component::creature_extra::CreatureExtraData; -use crate::models::creature::creature_component::creature_spell_caster::CreatureSpellCasterData; +use crate::models::creature::creature_component::creature_spell_caster::CreatureSpellcasterData; use crate::models::creature::creature_component::creature_variant::CreatureVariantData; use crate::models::creature::creature_metadata::creature_role::CreatureRoleEnum; use crate::models::creature::creature_metadata::variant_enum::CreatureVariant; @@ -15,7 +15,7 @@ pub struct Creature { pub variant_data: CreatureVariantData, pub extra_data: Option, pub combat_data: Option, - pub spell_caster_data: Option, + pub spell_caster_data: Option, } impl Creature { @@ -128,42 +128,63 @@ impl Creature { }) && filters.alignment_filter.as_ref().map_or(true, |x| { x.iter() .any(|align| self.core_data.essential.alignment == *align) - }) && filters - .is_melee_filter - .map_or(true, |is_melee| self.core_data.derived.is_melee == is_melee) - && filters.is_ranged_filter.map_or(true, |is_ranged| { - self.core_data.derived.is_ranged == is_ranged - }) - && filters - .is_spell_caster_filter - .map_or(true, |is_spell_caster| { - self.core_data.derived.is_spell_caster == is_spell_caster + }) && filters.attack_data_filter.clone().map_or(true, |attacks| { + self.core_data.derived.attack_data == attacks + }) && filters.type_filter.as_ref().map_or(true, |x| { + x.iter() + .any(|cr_type| self.core_data.essential.cr_type == *cr_type) + }) && (filters.role_threshold.is_none() + || filters.role_filter.as_ref().map_or(true, |cr_role| { + let t = filters.role_threshold.unwrap_or(0); + cr_role.iter().any(|role| match role { + CreatureRoleEnum::Brute => { + self.core_data.derived.role_data.get("brute").unwrap_or(&0) >= &t + } + CreatureRoleEnum::MagicalStriker => { + self.core_data + .derived + .role_data + .get("magical_striker") + .unwrap_or(&0) + >= &t + } + CreatureRoleEnum::SkillParagon => { + self.core_data + .derived + .role_data + .get("skill_paragon") + .unwrap_or(&0) + >= &t + } + CreatureRoleEnum::Skirmisher => { + self.core_data + .derived + .role_data + .get("skirmisher") + .unwrap_or(&0) + >= &t + } + CreatureRoleEnum::Sniper => { + self.core_data.derived.role_data.get("sniper").unwrap_or(&0) >= &t + } + CreatureRoleEnum::Soldier => { + self.core_data + .derived + .role_data + .get("soldier") + .unwrap_or(&0) + >= &t + } + CreatureRoleEnum::Spellcaster => { + self.core_data + .derived + .role_data + .get("spellcaster") + .unwrap_or(&0) + >= &t + } }) - && filters.type_filter.as_ref().map_or(true, |x| { - x.iter() - .any(|cr_type| self.core_data.essential.cr_type == *cr_type) - }) - && (filters.role_threshold.is_none() - || filters.role_filter.as_ref().map_or(true, |cr_role| { - let t = filters.role_threshold.unwrap_or(0); - cr_role.iter().any(|role| match role { - CreatureRoleEnum::Brute => self.core_data.derived.brute_percentage >= t, - CreatureRoleEnum::MagicalStriker => { - self.core_data.derived.magical_striker_percentage >= t - } - CreatureRoleEnum::SkillParagon => { - self.core_data.derived.skill_paragon_percentage >= t - } - CreatureRoleEnum::Skirmisher => { - self.core_data.derived.skirmisher_percentage >= t - } - CreatureRoleEnum::Sniper => self.core_data.derived.sniper_percentage >= t, - CreatureRoleEnum::Soldier => self.core_data.derived.soldier_percentage >= t, - CreatureRoleEnum::SpellCaster => { - self.core_data.derived.spell_caster_percentage >= t - } - }) - })) + })) && match filters.pathfinder_version.clone().unwrap_or_default() { PathfinderVersionEnum::Legacy => !self.core_data.essential.remaster, PathfinderVersionEnum::Remaster => self.core_data.essential.remaster, @@ -186,6 +207,22 @@ impl Creature { .to_lowercase() .contains(fam.to_lowercase().as_str()) }) + }) && filters.trait_whitelist_filter.as_ref().map_or(true, |x| { + x.iter().any(|filter_trait| { + self.core_data.traits.iter().any(|cr_trait| { + cr_trait + .to_lowercase() + .contains(filter_trait.to_lowercase().as_str()) + }) + }) + }) && !filters.trait_blacklist_filter.as_ref().map_or(false, |x| { + x.iter().any(|filter_trait| { + self.core_data.traits.iter().any(|cr_trait| { + cr_trait + .to_lowercase() + .eq(filter_trait.to_lowercase().as_str()) + }) + }) }) } } diff --git a/src/models/creature/items/spell_caster_entry.rs b/src/models/creature/items/spell_caster_entry.rs index 52dd38d..c719f18 100644 --- a/src/models/creature/items/spell_caster_entry.rs +++ b/src/models/creature/items/spell_caster_entry.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use utoipa::ToSchema; #[derive(Serialize, Deserialize, Clone, ToSchema, Eq, Hash, PartialEq)] -pub struct SpellCasterEntry { +pub struct SpellcasterEntry { pub spell_casting_name: Option, pub is_spell_casting_flexible: Option, pub type_of_spell_caster: Option, diff --git a/src/models/encounter_structs.rs b/src/models/encounter_structs.rs index 337696c..2a7444c 100644 --- a/src/models/encounter_structs.rs +++ b/src/models/encounter_structs.rs @@ -7,6 +7,7 @@ use crate::models::shared::size_enum::SizeEnum; use serde::{Deserialize, Serialize}; #[allow(unused_imports)] // Used in schema use serde_json::json; +use std::collections::HashMap; use strum::EnumCount; use strum::EnumIter; use utoipa::ToSchema; @@ -22,13 +23,18 @@ pub struct EncounterParams { #[derive(Serialize, Deserialize, ToSchema)] pub struct RandomEncounterData { - pub families: Option>, - pub traits: Option>, - pub rarities: Option>, - pub sizes: Option>, - pub alignments: Option>, - pub creature_types: Option>, - pub creature_roles: Option>, + pub source_filter: Option>, + pub trait_whitelist_filter: Option>, + pub trait_blacklist_filter: Option>, + pub family_filter: Option>, + pub rarity_filter: Option>, + pub size_filter: Option>, + pub alignment_filter: Option>, + pub type_filter: Option>, + pub role_filter: Option>, + pub attack_list: Option>, + pub role_lower_threshold: Option, + pub role_upper_threshold: Option, pub challenge: Option, pub adventure_group: Option, #[schema(minimum = 1, maximum = 30, example = 1)] diff --git a/src/models/item/item_fields_enum.rs b/src/models/item/item_fields_enum.rs deleted file mode 100644 index bccbb11..0000000 --- a/src/models/item/item_fields_enum.rs +++ /dev/null @@ -1,37 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, Hash, Eq, PartialEq, Clone)] -pub enum ItemField { - #[serde(alias = "category", alias = "CATEGORY")] - Category, - #[serde(alias = "level", alias = "LEVEL")] - Level, - #[serde(alias = "usage", alias = "USAGE")] - Usage, - #[serde(alias = "item_type", alias = "ITEM_TYPE")] - ItemType, - #[serde(alias = "material_grade", alias = "MATERIAL_GRADE")] - MaterialGrade, - #[serde(alias = "material_type", alias = "MATERIAL_TYPE")] - MaterialType, - #[serde(alias = "number_of_uses", alias = "NUMBER_OF_USES")] - NumberOfUses, - #[serde(alias = "size", alias = "SIZE")] - Size, - #[serde(alias = "rarity", alias = "RARITY")] - Rarity, - #[serde(alias = "sources", alias = "SOURCES")] - Sources, - #[serde(alias = "traits", alias = "TRAITS")] - Traits, -} - -#[derive(Default, Eq, PartialEq, Clone)] -pub struct FieldsUniqueValuesStruct { - pub list_of_levels: Vec, - pub list_of_categories: Vec, - pub list_of_traits: Vec, - pub list_of_sources: Vec, - pub list_of_sizes: Vec, - pub list_of_rarities: Vec, -} diff --git a/src/models/item/mod.rs b/src/models/item/mod.rs index 1896e38..d4d0352 100644 --- a/src/models/item/mod.rs +++ b/src/models/item/mod.rs @@ -1,5 +1,4 @@ pub mod armor_struct; -pub mod item_fields_enum; pub mod item_metadata; pub mod item_struct; pub mod shield_struct; diff --git a/src/models/response_data.rs b/src/models/response_data.rs index 908a87c..6abd7aa 100644 --- a/src/models/response_data.rs +++ b/src/models/response_data.rs @@ -1,7 +1,7 @@ use crate::models::creature::creature_component::creature_combat::CreatureCombatData; use crate::models::creature::creature_component::creature_core::CreatureCoreData; use crate::models::creature::creature_component::creature_extra::CreatureExtraData; -use crate::models::creature::creature_component::creature_spell_caster::CreatureSpellCasterData; +use crate::models::creature::creature_component::creature_spell_caster::CreatureSpellcasterData; use crate::models::creature::creature_component::creature_variant::CreatureVariantData; use crate::models::creature::creature_struct::Creature; use crate::models::item::armor_struct::ArmorData; @@ -25,7 +25,7 @@ pub struct ResponseCreature { pub variant_data: CreatureVariantData, pub extra_data: Option, pub combat_data: Option, - pub spell_caster_data: Option, + pub spell_caster_data: Option, } impl From for ResponseCreature { diff --git a/src/models/routers_validator_structs.rs b/src/models/routers_validator_structs.rs index 837e6a1..a40aaf9 100644 --- a/src/models/routers_validator_structs.rs +++ b/src/models/routers_validator_structs.rs @@ -6,6 +6,9 @@ use crate::models::pf_version_enum::PathfinderVersionEnum; use crate::models::shared::rarity_enum::RarityEnum; use crate::models::shared::size_enum::SizeEnum; use serde::{Deserialize, Serialize}; +#[allow(unused_imports)] +use serde_json::json; +use std::collections::BTreeMap; use strum::Display; use utoipa::{IntoParams, ToSchema}; #[derive(Serialize, Deserialize, IntoParams, ToSchema)] @@ -16,6 +19,8 @@ pub struct CreatureFieldFilters { pub rarity_filter: Option>, pub size_filter: Option>, pub alignment_filter: Option>, + pub trait_whitelist_filter: Option>, + pub trait_blacklist_filter: Option>, pub role_filter: Option>, pub type_filter: Option>, #[schema(minimum = 0, maximum = 100, example = 50)] @@ -28,9 +33,9 @@ pub struct CreatureFieldFilters { pub min_level_filter: Option, #[schema(minimum = -1, example = 5)] pub max_level_filter: Option, - pub is_melee_filter: Option, - pub is_ranged_filter: Option, - pub is_spell_caster_filter: Option, + + #[schema(example = json!({"melee": true, "ranged": false, "spellcaster": true}))] + pub attack_data_filter: Option>, pub pathfinder_version: Option, } diff --git a/src/routes/bestiary.rs b/src/routes/bestiary.rs index 2cf142a..9506cc8 100644 --- a/src/routes/bestiary.rs +++ b/src/routes/bestiary.rs @@ -16,14 +16,14 @@ use crate::models::creature::creature_component::creature_core::DerivedData; use crate::models::creature::creature_component::creature_core::EssentialData; use crate::models::creature::creature_component::creature_extra::AbilityScores; use crate::models::creature::creature_component::creature_extra::CreatureExtraData; -use crate::models::creature::creature_component::creature_spell_caster::CreatureSpellCasterData; +use crate::models::creature::creature_component::creature_spell_caster::CreatureSpellcasterData; use crate::models::creature::creature_component::creature_variant::CreatureVariantData; use crate::models::pf_version_enum::PathfinderVersionEnum; use crate::models::creature::items::action::Action; use crate::models::creature::items::skill::Skill; use crate::models::creature::items::spell::Spell; -use crate::models::creature::items::spell_caster_entry::SpellCasterEntry; +use crate::models::creature::items::spell_caster_entry::SpellcasterEntry; use crate::models::item::armor_struct::Armor; use crate::models::item::weapon_struct::Weapon; @@ -43,6 +43,7 @@ pub fn init_endpoints(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("/bestiary") .service(get_bestiary) + .service(get_bestiary_listing) .service(get_elite_creature) .service(get_weak_creature) .service(get_creature) @@ -89,7 +90,7 @@ pub fn init_docs(doc: &mut utoipa::openapi::OpenApi) { CreatureVariantData, CreatureExtraData, CreatureCombatData, - CreatureSpellCasterData, + CreatureSpellcasterData, Sense, Spell, Shield, @@ -100,7 +101,7 @@ pub fn init_docs(doc: &mut utoipa::openapi::OpenApi) { Action, Skill, CreatureRoleEnum, - SpellCasterEntry, + SpellcasterEntry, PathfinderVersionEnum, OrderEnum, CreatureSortEnum diff --git a/src/services/encounter_service.rs b/src/services/encounter_service.rs index d74cb33..21ef7ed 100644 --- a/src/services/encounter_service.rs +++ b/src/services/encounter_service.rs @@ -1,7 +1,6 @@ use crate::db::bestiary_proxy::{get_creatures_passing_all_filters, order_list_by_level}; -use crate::models::creature::creature_component::filter_struct::FilterStruct; -use crate::models::creature::creature_filter_enum::CreatureFilter; -use crate::models::creature::creature_metadata::creature_role::CreatureRoleEnum; +use crate::models::bestiary_structs::BestiaryFilterQuery; +use crate::models::bestiary_structs::CreatureTableFieldsFilter; use crate::models::creature::creature_struct::Creature; use crate::models::encounter_structs::{ AdventureGroupEnum, EncounterChallengeEnum, EncounterParams, RandomEncounterData, @@ -12,11 +11,12 @@ use crate::services::encounter_handler::encounter_calculator::calculate_encounte use crate::AppState; use anyhow::{ensure, Result}; use counter::Counter; +use itertools::Itertools; use log::warn; use serde::{Deserialize, Serialize}; #[allow(unused_imports)] // it's used for Schema use serde_json::json; -use std::collections::{BTreeMap, HashMap, HashSet}; +use std::collections::{BTreeMap, HashSet}; use utoipa::ToSchema; #[derive(Serialize, Deserialize, ToSchema)] @@ -87,27 +87,51 @@ async fn calculate_random_encounter( .clone() .into_iter() .flatten() - .map(|lvl| lvl.to_string()) - .collect::>(); + .sorted() + .dedup() + .collect::>(); ensure!( !unique_levels.is_empty(), "There are no valid levels to chose from. Encounter could not be built" ); - let filter_map = build_filter_map(FilterStruct { - families: enc_data.families, - traits: enc_data.traits, - rarities: enc_data.rarities, - sizes: enc_data.sizes, - alignments: enc_data.alignments, - creature_types: enc_data.creature_types, - creature_roles: enc_data.creature_roles, - lvl_combinations: unique_levels, - pathfinder_version: enc_data.pathfinder_version.unwrap_or_default(), - }); - let filtered_creatures = get_filtered_creatures( app_state, - filter_map, + &BestiaryFilterQuery { + creature_table_fields_filter: CreatureTableFieldsFilter { + source_filter: enc_data.source_filter.unwrap_or_default(), + family_filter: enc_data.family_filter.unwrap_or_default(), + alignment_filter: enc_data.alignment_filter.unwrap_or_default(), + size_filter: enc_data.size_filter.unwrap_or_default(), + rarity_filter: enc_data.rarity_filter.unwrap_or_default(), + type_filter: enc_data.type_filter.unwrap_or_default(), + role_filter: enc_data.role_filter.unwrap_or_default(), + role_lower_threshold: enc_data + .role_lower_threshold + .unwrap_or(CreatureTableFieldsFilter::default_lower_threshold()), + role_upper_threshold: enc_data + .role_upper_threshold + .unwrap_or(CreatureTableFieldsFilter::default_upper_threshold()), + is_melee_filter: enc_data.attack_list.as_ref().map_or_else( + || vec![true, false], + |x| vec![*x.get("melee").unwrap_or(&false)], + ), + is_ranged_filter: enc_data.attack_list.as_ref().map_or_else( + || vec![true, false], + |x| vec![*x.get("ranged").unwrap_or(&false)], + ), + is_spellcaster_filter: enc_data.attack_list.map_or_else( + || vec![true, false], + |x| vec![*x.get("spellcaster").unwrap_or(&false)], + ), + supported_version: enc_data + .pathfinder_version + .unwrap_or_default() + .to_db_value(), + level_filter: unique_levels, + }, + trait_whitelist_filter: enc_data.trait_whitelist_filter.unwrap_or_default(), + trait_blacklist_filter: enc_data.trait_blacklist_filter.unwrap_or_default(), + }, enc_data.allow_weak_variants.is_some_and(|x| x), enc_data.allow_elite_variants.is_some_and(|x| x), ) @@ -224,72 +248,13 @@ fn filter_non_existing_levels( result_vec } -fn build_filter_map(filter_enum: FilterStruct) -> HashMap> { - let mut filter_map = HashMap::new(); - - filter_enum - .families - .map(|el| filter_map.insert(CreatureFilter::Family, HashSet::from_iter(el))); - filter_enum - .traits - .map(|el| filter_map.insert(CreatureFilter::Traits, HashSet::from_iter(el))); - // What no generic enum does to a mf (mother function) - // it could also prob be bad programming by me - if let Some(vec) = filter_enum.rarities { - filter_map.insert( - CreatureFilter::Rarity, - vec.iter() - .map(std::string::ToString::to_string) - .collect::>(), - ); - }; - if let Some(vec) = filter_enum.sizes { - filter_map.insert( - CreatureFilter::Size, - vec.iter() - .map(std::string::ToString::to_string) - .collect::>(), - ); - }; - if let Some(vec) = filter_enum.alignments { - filter_map.insert( - CreatureFilter::Alignment, - vec.iter() - .map(std::string::ToString::to_string) - .collect::>(), - ); - }; - if let Some(vec) = filter_enum.creature_types { - filter_map.insert( - CreatureFilter::CreatureTypes, - vec.iter() - .map(std::string::ToString::to_string) - .collect::>(), - ); - }; - if let Some(vec) = filter_enum.creature_roles { - filter_map.insert( - CreatureFilter::CreatureRoles, - vec.iter() - .map(CreatureRoleEnum::to_db_column) - .collect::>(), - ); - }; - filter_map.insert(CreatureFilter::Level, filter_enum.lvl_combinations); - filter_map.insert( - CreatureFilter::PathfinderVersion, - HashSet::from_iter(filter_enum.pathfinder_version.to_db_value()), - ); - filter_map -} - async fn get_filtered_creatures( app_state: &AppState, - filter_map: HashMap>, + filters: &BestiaryFilterQuery, allow_weak: bool, allow_elite: bool, ) -> Result> { - get_creatures_passing_all_filters(app_state, filter_map, allow_weak, allow_elite).await + get_creatures_passing_all_filters(app_state, filters, allow_weak, allow_elite).await } fn get_lvl_combinations(enc_data: &RandomEncounterData, party_levels: &[i64]) -> HashSet> { diff --git a/src/services/url_calculator.rs b/src/services/url_calculator.rs index db46249..d6d67c2 100644 --- a/src/services/url_calculator.rs +++ b/src/services/url_calculator.rs @@ -80,15 +80,6 @@ fn creature_filter_query_calculator(field_filters: &CreatureFieldFilters) -> Str field_filters .max_level_filter .map(|lvl| format!("max_level_filter={lvl}")), - field_filters - .is_melee_filter - .map(|is| format!("is_melee_filter={is}")), - field_filters - .is_ranged_filter - .map(|is| format!("is_ranged_filter={is}")), - field_filters - .is_spell_caster_filter - .map(|is| format!("is_spell_caster_filter={is}")), ] .iter() .filter_map(std::clone::Clone::clone)