From 6e80064308f02f2fddbcda9a0372eff3ec1d30f6 Mon Sep 17 00:00:00 2001 From: rakuja Date: Fri, 13 Dec 2024 09:49:35 +0100 Subject: [PATCH] feat: provide sort by attacks (melee, ranged, spellcaster) and roles with threshold --- Cargo.toml | 2 +- src/db/bestiary_proxy.rs | 35 ++++--- src/models/bestiary_structs.rs | 10 +- .../creature_component/creature_core.rs | 62 +++++++------ src/models/creature/creature_struct.rs | 91 ++++++++++++------- src/models/encounter_structs.rs | 5 +- src/models/routers_validator_structs.rs | 9 +- src/services/encounter_service.rs | 21 +++-- src/services/url_calculator.rs | 9 -- 9 files changed, 136 insertions(+), 108 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 45574ab..4c75063 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,7 +55,7 @@ sqlx = { version = "0.8.2", features = ["runtime-async-std", "sqlite"] } cached = { version = "0.54.0", features = ["async"] } anyhow = "1.0.94" -serde = { version = "1.0.215", features = ["derive"] } +serde = { version = "1.0.216", features = ["derive"] } serde_json = "1.0.133" strum = {version="0.26.3", features = ["derive"]} fastrand = "2.3.0" diff --git a/src/db/bestiary_proxy.rs b/src/db/bestiary_proxy.rs index ce7a86b..dcc4fd7 100644 --- a/src/db/bestiary_proxy.rs +++ b/src/db/bestiary_proxy.rs @@ -1,5 +1,5 @@ use crate::models::creature::creature_struct::Creature; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use crate::db::data_providers::creature_fetcher::fetch_traits_associated_with_creatures; use crate::db::data_providers::{creature_fetcher, generic_fetcher}; @@ -102,21 +102,28 @@ pub async fn get_paginated_creatures( .essential .alignment .cmp(&b.core_data.essential.alignment), - CreatureSortEnum::Melee => a + CreatureSortEnum::Attacks => a .core_data .derived - .is_melee - .cmp(&b.core_data.derived.is_melee), - CreatureSortEnum::Ranged => a - .core_data - .derived - .is_ranged - .cmp(&b.core_data.derived.is_ranged), - CreatureSortEnum::SpellCaster => a - .core_data - .derived - .is_spell_caster - .cmp(&b.core_data.derived.is_spell_caster), + .attack_data + .cmp(&b.core_data.derived.attack_data), + CreatureSortEnum::Roles => { + let threshold = filters.role_threshold.unwrap_or(0); + a.core_data + .derived + .role_data + .iter() + .filter(|(_, y)| **y > threshold) + .collect::>() + .cmp( + &b.core_data + .derived + .role_data + .iter() + .filter(|(_, y)| **y > threshold) + .collect::>(), + ) + } }; match pagination .bestiary_sort_data diff --git a/src/models/bestiary_structs.rs b/src/models/bestiary_structs.rs index dc8eef2..8828a26 100644 --- a/src/models/bestiary_structs.rs +++ b/src/models/bestiary_structs.rs @@ -31,12 +31,10 @@ pub enum CreatureSortEnum { Family, #[serde(alias = "alignment", alias = "ALIGNMENT")] Alignment, - #[serde(alias = "melee", alias = "MELEE")] - Melee, - #[serde(alias = "ranged", alias = "RANGED")] - Ranged, - #[serde(alias = "SPELLCASTER", alias = "SPELLCASTER")] - SpellCaster, + #[serde(alias = "attacks", alias = "ATTACKS")] + Attacks, + #[serde(alias = "roles", alias = "ROLES")] + Roles, } #[derive(Serialize, Deserialize, IntoParams, ToSchema, Eq, PartialEq, Hash, Default)] 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_struct.rs b/src/models/creature/creature_struct.rs index d2c091a..14c3391 100644 --- a/src/models/creature/creature_struct.rs +++ b/src/models/creature/creature_struct.rs @@ -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, diff --git a/src/models/encounter_structs.rs b/src/models/encounter_structs.rs index 6236b1f..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; @@ -31,9 +32,7 @@ pub struct RandomEncounterData { pub alignment_filter: Option>, pub type_filter: Option>, pub role_filter: Option>, - pub is_melee_filter: Option, - pub is_ranged_filter: Option, - pub is_spellcaster_filter: Option, + pub attack_list: Option>, pub role_lower_threshold: Option, pub role_upper_threshold: Option, pub challenge: Option, diff --git a/src/models/routers_validator_structs.rs b/src/models/routers_validator_structs.rs index 7832fd8..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)] @@ -30,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/services/encounter_service.rs b/src/services/encounter_service.rs index 42ae611..21ef7ed 100644 --- a/src/services/encounter_service.rs +++ b/src/services/encounter_service.rs @@ -111,15 +111,18 @@ async fn calculate_random_encounter( role_upper_threshold: enc_data .role_upper_threshold .unwrap_or(CreatureTableFieldsFilter::default_upper_threshold()), - is_melee_filter: enc_data - .is_melee_filter - .map_or_else(|| vec![true, false], |x| vec![x]), - is_ranged_filter: enc_data - .is_ranged_filter - .map_or_else(|| vec![true, false], |x| vec![x]), - is_spellcaster_filter: enc_data - .is_spellcaster_filter - .map_or_else(|| vec![true, false], |x| vec![x]), + 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() 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)