diff --git a/Cargo.toml b/Cargo.toml index 7d68c8f..50b5407 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,20 @@ [package] name = "khguide" -version = "1.2.0" -edition = "2021" +version = "1.3.0" +edition = "2024" [dependencies] -rinja = "0.3.5" -serde = { version = "1.0.203", features = ["derive"] } -serde_json = "1.0.118" -toml = "0.8.19" -tracing = "0.1.40" -tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } -itertools = "0.13.0" +askama = "0.14" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +toml = "0.8" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +itertools = "0.14" [features] -default = ["bbs", "ddd", "kh3", "kh2"] +default = ["bbs", "ddd", "kh3", "kh2", "kh1"] +kh1 = [] kh2 = [] kh3 = [] bbs = [] diff --git a/rinja.toml b/askama.toml similarity index 100% rename from rinja.toml rename to askama.toml diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 292fe49..c5794a6 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "stable" +channel = "1.85" diff --git a/src/bbs.rs b/src/bbs.rs index 5ab2b79..cb64afa 100644 --- a/src/bbs.rs +++ b/src/bbs.rs @@ -1,11 +1,23 @@ use std::collections::HashMap; +use ability::Ability; +use askama::Template; +use command::Command; +use finisher::Finisher; use itertools::Itertools; -use rinja::Template; use serde::Deserialize; use crate::create_file; +mod ability; +mod command; +mod finisher; +mod melding; + +const ABILITIES_PATH: &str = "./input/bbs/abilities.json"; +const FINISHERS_PATH: &str = "./input/bbs/finish-commands.json"; +const COMMANDS_PATH: &str = "./input/bbs/commands.json"; + #[derive(Debug, Deserialize, PartialEq, Eq)] enum Character { #[serde(alias = "A")] @@ -16,110 +28,6 @@ enum Character { Terra, } -#[derive(Debug, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord)] -struct Ability { - name: String, - from: String, - category: String, - max: u8, - types: Vec, -} - -#[derive(Debug, Deserialize)] -struct Command { - name: String, - category: String, - char: Vec, - #[serde(default)] - starting: Option, - #[serde(default)] - info: Option, - #[serde(default)] - recipes: Vec, -} - -impl Command {} - -#[derive(Debug, Deserialize)] -struct CommandRecipe { - char: Vec, - r#type: char, - ingredients: (String, String), - chance: u8, - #[serde(skip_deserializing)] - abilities: Vec<(String, Ability)>, -} - -impl CommandRecipe { - pub fn get_ability(&self, crystal: &str) -> Option { - if self.r#type == '-' { - return None; - } - - for ability in self.abilities.iter() { - if ability.0 == crystal { - return Some(ability.1.clone()); - } - } - - // This should never happen unless the json files are wrong - panic!( - "No ability found for {} + {} and {}", - self.ingredients.0, self.ingredients.1, crystal - ); - } - - pub fn can_unlock(&self, char: Character) -> bool { - self.char.contains(&char) - } - - pub fn get_unlock_chars(&self) -> String { - let mut id = String::new(); - if self.can_unlock(Character::Terra) { - id += "T" - } - - if self.can_unlock(Character::Ventus) { - if !id.is_empty() { - id += " "; - } - id += "V"; - } - - if self.can_unlock(Character::Aqua) { - if !id.is_empty() { - id += " "; - } - id += "A"; - } - - id - } - - pub fn set_abilities(&mut self, abilities: &[Ability]) { - let mut vec: Vec<(String, Ability)> = vec![]; - - for ability in abilities.iter() { - if ability.types.contains(&self.r#type) { - vec.push((ability.from.clone(), ability.clone())); - } - } - vec.sort(); - self.abilities = vec; - } -} - -#[derive(Debug, Deserialize)] -struct Finisher { - name: String, - char: Vec, - level: u8, - #[serde(default)] - follows: Vec, - goal: String, - color: String, -} - #[derive(Template)] #[template(path = "pages/bbs/melding.html")] struct CommandsTemplate { @@ -127,10 +35,6 @@ struct CommandsTemplate { pub crystals: Vec, } -const ABILITIES_PATH: &str = "./input/bbs/abilities.json"; -const FINISHERS_PATH: &str = "./input/bbs/finish-commands.json"; -const COMMANDS_PATH: &str = "./input/bbs/commands.json"; - pub fn init() { tracing::info!("Loading abilities data from {}", ABILITIES_PATH); let abilities_str = std::fs::read_to_string(ABILITIES_PATH).unwrap(); diff --git a/src/bbs/ability.rs b/src/bbs/ability.rs new file mode 100644 index 0000000..1a15318 --- /dev/null +++ b/src/bbs/ability.rs @@ -0,0 +1,10 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct Ability { + pub name: String, + pub from: String, + pub category: String, + pub max: u8, + pub types: Vec, +} diff --git a/src/bbs/command.rs b/src/bbs/command.rs new file mode 100644 index 0000000..6e56372 --- /dev/null +++ b/src/bbs/command.rs @@ -0,0 +1,16 @@ +use serde::Deserialize; + +use super::melding::CommandRecipe; + +#[derive(Debug, Deserialize)] +pub struct Command { + pub name: String, + pub category: String, + pub char: Vec, + #[serde(default)] + pub starting: Option, + #[serde(default)] + pub info: Option, + #[serde(default)] + pub recipes: Vec, +} diff --git a/src/bbs/finisher.rs b/src/bbs/finisher.rs new file mode 100644 index 0000000..c2567e0 --- /dev/null +++ b/src/bbs/finisher.rs @@ -0,0 +1,14 @@ +use serde::Deserialize; + +use super::Character; + +#[derive(Debug, Deserialize)] +pub struct Finisher { + pub name: String, + pub char: Vec, + pub level: u8, + #[serde(default)] + pub follows: Vec, + pub goal: String, + pub color: String, +} diff --git a/src/bbs/melding.rs b/src/bbs/melding.rs new file mode 100644 index 0000000..d466e23 --- /dev/null +++ b/src/bbs/melding.rs @@ -0,0 +1,72 @@ +use serde::Deserialize; + +use super::{ability::Ability, Character}; + +#[derive(Debug, Deserialize)] +pub struct CommandRecipe { + pub char: Vec, + pub r#type: char, + pub ingredients: (String, String), + pub chance: u8, + #[serde(skip_deserializing)] + pub abilities: Vec<(String, Ability)>, +} + +impl CommandRecipe { + pub fn get_ability(&self, crystal: &str) -> Option { + if self.r#type == '-' { + return None; + } + + for ability in self.abilities.iter() { + if ability.0 == crystal { + return Some(ability.1.clone()); + } + } + + // This should never happen unless the json files are wrong + panic!( + "No ability found for {} + {} and {}", + self.ingredients.0, self.ingredients.1, crystal + ); + } + + pub fn can_unlock(&self, char: Character) -> bool { + self.char.contains(&char) + } + + pub fn get_unlock_chars(&self) -> String { + let mut id = String::new(); + if self.can_unlock(Character::Terra) { + id += "T" + } + + if self.can_unlock(Character::Ventus) { + if !id.is_empty() { + id += " "; + } + id += "V"; + } + + if self.can_unlock(Character::Aqua) { + if !id.is_empty() { + id += " "; + } + id += "A"; + } + + id + } + + pub fn set_abilities(&mut self, abilities: &[Ability]) { + let mut vec: Vec<(String, Ability)> = vec![]; + + for ability in abilities.iter() { + if ability.types.contains(&self.r#type) { + vec.push((ability.from.clone(), ability.clone())); + } + } + vec.sort(); + self.abilities = vec; + } +} diff --git a/src/common.rs b/src/common.rs new file mode 100644 index 0000000..d777b82 --- /dev/null +++ b/src/common.rs @@ -0,0 +1,2 @@ +pub mod direction; +pub mod materials; diff --git a/src/common/direction.rs b/src/common/direction.rs new file mode 100644 index 0000000..08cc3b9 --- /dev/null +++ b/src/common/direction.rs @@ -0,0 +1,13 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize, PartialEq, Eq)] +pub enum Direction { + #[serde(alias = "N")] + North, + #[serde(alias = "S")] + South, + #[serde(alias = "E")] + East, + #[serde(alias = "W")] + West, +} diff --git a/src/common/materials.rs b/src/common/materials.rs new file mode 100644 index 0000000..c49575b --- /dev/null +++ b/src/common/materials.rs @@ -0,0 +1,37 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize, PartialEq, Eq)] +pub struct EnemyDrop { + pub from: String, + pub chance: u8, + + #[serde(default)] + pub info: Option, +} + +impl EnemyDrop { + pub fn texture(&self) -> String { + self.from.replace(" ", "_").to_lowercase() + } +} + +#[derive(Debug, Deserialize, PartialEq, Eq)] +pub struct MaterialDrops { + pub kind: String, + pub shard: Vec, + pub stone: Vec, + pub gem: Vec, + pub crystal: Vec, +} + +impl MaterialDrops { + pub fn drops(&self, kind: &str) -> &[EnemyDrop] { + match kind { + "shard" => &self.shard, + "stone" => &self.stone, + "gem" => &self.gem, + "crystal" => &self.crystal, + _ => &self.shard, + } + } +} diff --git a/src/ddd.rs b/src/ddd.rs index 679212f..4363252 100644 --- a/src/ddd.rs +++ b/src/ddd.rs @@ -1,353 +1,16 @@ -use std::{fmt::Display, panic, path::PathBuf}; - -use itertools::Itertools; -use rinja::Template; -use serde::{Deserialize, Deserializer}; +use std::path::PathBuf; use crate::create_file; +use crate::ddd::ability::AbilityType; +use askama::Template; +use board::Board; -#[derive(Debug, Deserialize, PartialEq, Eq)] -struct Board { - order: u32, - spirit: String, - routes: Vec, - abilities: Vec, +mod ability; +mod board; +mod board_position; +mod route; - #[serde(skip_deserializing)] - total_lp: u32, - #[serde(skip_deserializing)] - max_level: u32, - - #[serde(skip_deserializing)] - stats: Vec<(usize, String)>, -} - -impl Board { - pub fn init_ability_help(&mut self) { - for abl in &mut self.abilities { - if abl.tip.is_none() && abl.route.is_some() { - let route_id = abl.route.unwrap(); - - let mut route = None; - for r in &self.routes { - if r.id == route_id { - route = Some(r); - break; - } - } - - if route_id < 10 && route.is_some() { - abl.tip = Some(format!("Unlocks with disposition {}", route.unwrap().name)); - } - } - } - } - - pub fn init_routes(&mut self) { - self.routes.iter_mut().for_each(|r| { - r.interaction = match r.color { - Color::Blue => Interaction::Poke, - Color::Purple => Interaction::Poke, - Color::Yellow => Interaction::Rub, - Color::Green => Interaction::Rub, - - _ => Interaction::None, - } - }); - - let routes = self - .routes - .clone() - .into_iter() - .filter(|r| r.id < 100) - .collect_vec(); - - self.routes.iter_mut().filter(|r| r.id < 100).for_each(|r| { - r.tips.iter_mut().for_each(|t| { - let route = routes.iter().find(|r| r.name == t.to); - if let Some(route) = route { - t.to_color = route.color.clone(); - t.tip = format!("{} {}", route.interaction, t.tip); - } - }); - }); - } - - pub fn init_stats(&mut self) { - let v = self - .abilities - .iter() - .filter(|&ability| ability.r#type == AbilityType::Stat) - .sorted_by(|a, b| Ord::cmp(&a.name, &b.name)) - .collect_vec(); - - let mut stats: Vec<(usize, String)> = vec![]; - for (key, chunk) in &v.into_iter().chunk_by(|k| k.name.clone()) { - let grouped = chunk.collect_vec(); - stats.push((grouped.len(), key)); - } - - self.stats = stats; - } - - pub fn init_total_lp(&mut self) { - self.total_lp = self - .abilities - .iter() - .filter(|&ability| ability.price.contains("LP")) - .map(|ability| { - let mut split = ability.price.split_whitespace(); - - split.next().unwrap_or("0").parse::().unwrap_or(0) - }) - .sum::(); - } - - pub fn init_max_level(&mut self) { - self.max_level = self - .abilities - .iter() - .filter(|&ability| ability.price.starts_with("Level")) - .map(|ability| { - let mut split = ability.price.split_whitespace(); - - split.nth(1).unwrap_or("0").parse::().unwrap_or(0) - }) - .sorted() - .last() - .unwrap_or(0); - } - - pub fn get_size(&self) -> BoardPosition { - let mut x = 1; - let mut y = 1; - for ability in &self.abilities { - if ability.pos.0 > x { - x = ability.pos.0; - } - - if ability.pos.1 > y { - y = ability.pos.1; - } - } - - (x, y) - } - - pub fn get_ability_at(&self, x: &u32, y: &u32) -> Option<&Ability> { - self.abilities - .iter() - .find(|&ability| ability.pos == (*x, *y)) - } - - pub fn get_char(&self, i: &u32) -> char { - char::from_u32(64 + *i).unwrap_or('0') - } - - pub fn get_dispositions(&self) -> Vec<&Route> { - self.routes.iter().filter(|r| r.id < 100).collect_vec() - } - - pub fn get_supports(&self) -> Vec<&Ability> { - self.abilities - .iter() - .filter(|&ability| ability.r#type == AbilityType::Support) - .collect_vec() - } - - pub fn get_commands(&self) -> Vec<&Ability> { - self.abilities - .iter() - .filter(|&ability| { - ability.r#type == AbilityType::Magic - || ability.r#type == AbilityType::Attack - || ability.r#type == AbilityType::Reprisal - || ability.r#type == AbilityType::Defense - }) - .collect_vec() - } - - pub fn get_spirits(&self) -> Vec<&Ability> { - self.abilities - .iter() - .filter(|&ability| ability.r#type == AbilityType::Spirit) - .collect_vec() - } -} - -#[derive(Debug, Deserialize, PartialEq, Eq, Clone)] -struct Route { - id: u32, - name: String, - color: Color, - tips: Vec, - #[serde(skip_deserializing)] - interaction: Interaction, -} - -#[derive(Debug, Deserialize, PartialEq, Eq, Clone)] -enum Color { - #[serde(alias = "blue")] - Blue, - #[serde(alias = "purple")] - Purple, - #[serde(alias = "yellow")] - Yellow, - #[serde(alias = "green")] - Green, - - #[serde(alias = "secret1")] - SecretGreen, - #[serde(alias = "secret2")] - SecretRed, -} - -impl Display for Color { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Color::Blue => f.write_str("blue"), - Color::Purple => f.write_str("purple"), - Color::Yellow => f.write_str("yellow"), - Color::Green => f.write_str("green"), - - Color::SecretGreen => f.write_str("secret1"), - Color::SecretRed => f.write_str("secret2"), - } - } -} - -#[derive(Debug, Deserialize, PartialEq, Eq, Clone)] -struct Tip { - to: String, - #[serde(skip_deserializing, default = "default_disposition_color")] - to_color: Color, - tip: String, -} - -fn default_disposition_color() -> Color { - Color::Blue -} - -#[derive(Debug, Deserialize, PartialEq, Eq)] -enum AbilityType { - Start, - Checkpoint, - Secret, - Stat, - Spirit, - Support, - Attack, - Magic, - Reprisal, - Defense, -} - -type BoardPosition = (u32, u32); - -#[derive(Debug, Deserialize, PartialEq, Eq)] -struct ParseBoardPositionError; - -fn deserialize_position<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - let str = match String::deserialize(deserializer) { - Ok(v) => v, - Err(e) => { - panic!("Tried deserializing a non-string type\nerror: {}", e); - } - }; - let str = str.to_uppercase(); - - let mut chars = str.chars(); - - let a = chars.next().unwrap_or('A'); - let b = chars.next().unwrap_or('1'); - - let a = (a.to_ascii_uppercase() as u32).saturating_sub(64); - if a == 0 { - panic!("Second position parameter {} is 0 or lower!", str); - } - - let b = b.to_digit(10).unwrap_or(0); - - Ok((a, b)) -} - -#[derive(Debug, Deserialize, PartialEq, Eq)] -struct Ability { - name: String, - #[serde(deserialize_with = "deserialize_position")] - pos: BoardPosition, - r#type: AbilityType, - price: String, - route: Option, - path: Vec, - tip: Option, -} - -impl Ability { - pub fn tip(&self) -> String { - self.tip.clone().unwrap_or_default() - } - - pub fn get_slot_details(&self, board: &Board) -> String { - let mut details = String::new(); - if let Some(route) = self.route { - for broute in &board.routes { - if broute.id == route { - details += &format!("{} ", broute.color); - break; - } - } - } - - for path in &self.path { - match path { - Direction::North => details += "north ", - Direction::South => details += "south ", - Direction::East => details += "east ", - Direction::West => details += "west ", - } - } - - if self.r#type == AbilityType::Start { - details += "start "; - } - - details - } -} - -#[derive(Debug, Deserialize, PartialEq, Eq)] -enum Direction { - #[serde(alias = "N")] - North, - #[serde(alias = "S")] - South, - #[serde(alias = "E")] - East, - #[serde(alias = "W")] - West, -} - -#[derive(Debug, Default, Deserialize, PartialEq, Eq, Clone)] -enum Interaction { - #[default] - None, - Poke, - Rub, -} - -impl Display for Interaction { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Interaction::Poke => f.write_str("Poke"), - Interaction::Rub => f.write_str("Rub"), - _ => f.write_str(""), - } - } -} +const ABILITIES_PATH: &str = "./input/ddd/abilities"; #[derive(Template)] #[template(path = "pages/ddd/boards.html")] @@ -355,8 +18,6 @@ struct AbilitiesTemplate { pub boards: Vec, } -const ABILITIES_PATH: &str = "./input/ddd/abilities"; - pub fn init() { tracing::info!("Loading ability boards data from {}", ABILITIES_PATH); let mut boards: Vec = vec![]; @@ -365,7 +26,7 @@ pub fn init() { .unwrap() .filter_map(|f| f.ok()) .map(|f| f.path()) - .filter_map(|p| match p.extension().map_or(false, |e| e == "toml") { + .filter_map(|p| match p.extension().is_some_and(|e| e == "toml") { true => Some(p), false => None, }) diff --git a/src/ddd/ability.rs b/src/ddd/ability.rs new file mode 100644 index 0000000..e3f2859 --- /dev/null +++ b/src/ddd/ability.rs @@ -0,0 +1,64 @@ +use serde::Deserialize; + +use crate::common::direction::Direction; + +use super::{board::Board, board_position::BoardPosition}; + +#[derive(Debug, Deserialize, PartialEq, Eq)] +pub enum AbilityType { + Start, + Checkpoint, + Secret, + Stat, + Spirit, + Support, + Attack, + Magic, + Reprisal, + Defense, +} + +#[derive(Debug, Deserialize, PartialEq, Eq)] +pub struct Ability { + pub name: String, + #[serde(deserialize_with = "crate::ddd::board_position::deserialize_position")] + pub pos: BoardPosition, + pub r#type: AbilityType, + pub price: String, + pub route: Option, + pub path: Vec, + pub tip: Option, +} + +impl Ability { + pub fn tip(&self) -> String { + self.tip.clone().unwrap_or_default() + } + + pub fn get_slot_details(&self, board: &Board) -> String { + let mut details = String::new(); + if let Some(route) = self.route { + for broute in &board.routes { + if broute.id == route { + details += &format!("{} ", broute.color); + break; + } + } + } + + for path in &self.path { + match path { + Direction::North => details += "north ", + Direction::South => details += "south ", + Direction::East => details += "east ", + Direction::West => details += "west ", + } + } + + if self.r#type == AbilityType::Start { + details += "start "; + } + + details + } +} diff --git a/src/ddd/board.rs b/src/ddd/board.rs new file mode 100644 index 0000000..efce1ab --- /dev/null +++ b/src/ddd/board.rs @@ -0,0 +1,177 @@ +use itertools::Itertools; +use serde::Deserialize; + +use super::{ + ability::{Ability, AbilityType}, + board_position::BoardPosition, + route::{Interaction, Route, RouteColor}, +}; + +#[derive(Debug, Deserialize, PartialEq, Eq)] +pub struct Board { + pub order: u32, + pub spirit: String, + pub routes: Vec, + pub abilities: Vec, + + #[serde(skip_deserializing)] + pub total_lp: u32, + #[serde(skip_deserializing)] + pub max_level: u32, + + #[serde(skip_deserializing)] + pub stats: Vec<(usize, String)>, +} + +impl Board { + pub fn init_ability_help(&mut self) { + for abl in &mut self.abilities { + if abl.tip.is_none() && abl.route.is_some() { + let route_id = abl.route.unwrap(); + + let mut route = None; + for r in &self.routes { + if r.id == route_id { + route = Some(r); + break; + } + } + + if route_id < 10 && route.is_some() { + abl.tip = Some(format!("Unlocks with disposition {}", route.unwrap().name)); + } + } + } + } + + pub fn init_routes(&mut self) { + self.routes.iter_mut().for_each(|r| { + r.interaction = match r.color { + RouteColor::Blue => Interaction::Poke, + RouteColor::Purple => Interaction::Poke, + RouteColor::Yellow => Interaction::Rub, + RouteColor::Green => Interaction::Rub, + + _ => Interaction::None, + } + }); + + let routes = self + .routes + .clone() + .into_iter() + .filter(|r| r.id < 100) + .collect_vec(); + + self.routes.iter_mut().filter(|r| r.id < 100).for_each(|r| { + r.tips.iter_mut().for_each(|t| { + let route = routes.iter().find(|r| r.name == t.to); + if let Some(route) = route { + t.to_color = route.color.clone(); + t.tip = format!("{} {}", route.interaction, t.tip); + } + }); + }); + } + + pub fn init_stats(&mut self) { + let v = self + .abilities + .iter() + .filter(|&ability| ability.r#type == AbilityType::Stat) + .sorted_by(|a, b| Ord::cmp(&a.name, &b.name)) + .collect_vec(); + + let mut stats: Vec<(usize, String)> = vec![]; + for (key, chunk) in &v.into_iter().chunk_by(|k| k.name.clone()) { + let grouped = chunk.collect_vec(); + stats.push((grouped.len(), key)); + } + + self.stats = stats; + } + + pub fn init_total_lp(&mut self) { + self.total_lp = self + .abilities + .iter() + .filter(|&ability| ability.price.contains("LP")) + .map(|ability| { + let mut split = ability.price.split_whitespace(); + + split.next().unwrap_or("0").parse::().unwrap_or(0) + }) + .sum::(); + } + + pub fn init_max_level(&mut self) { + self.max_level = self + .abilities + .iter() + .filter(|&ability| ability.price.starts_with("Level")) + .map(|ability| { + let mut split = ability.price.split_whitespace(); + + split.nth(1).unwrap_or("0").parse::().unwrap_or(0) + }) + .sorted() + .last() + .unwrap_or(0); + } + + pub fn get_size(&self) -> BoardPosition { + let mut x = 1; + let mut y = 1; + for ability in &self.abilities { + if ability.pos.0 > x { + x = ability.pos.0; + } + + if ability.pos.1 > y { + y = ability.pos.1; + } + } + + (x, y) + } + + pub fn get_ability_at(&self, x: &u32, y: &u32) -> Option<&Ability> { + self.abilities + .iter() + .find(|&ability| ability.pos == (*x, *y)) + } + + pub fn get_char(&self, i: &u32) -> char { + char::from_u32(64 + *i).unwrap_or('0') + } + + pub fn get_dispositions(&self) -> Vec<&Route> { + self.routes.iter().filter(|r| r.id < 100).collect_vec() + } + + pub fn get_supports(&self) -> Vec<&Ability> { + self.abilities + .iter() + .filter(|&ability| ability.r#type == AbilityType::Support) + .collect_vec() + } + + pub fn get_commands(&self) -> Vec<&Ability> { + self.abilities + .iter() + .filter(|&ability| { + ability.r#type == AbilityType::Magic + || ability.r#type == AbilityType::Attack + || ability.r#type == AbilityType::Reprisal + || ability.r#type == AbilityType::Defense + }) + .collect_vec() + } + + pub fn get_spirits(&self) -> Vec<&Ability> { + self.abilities + .iter() + .filter(|&ability| ability.r#type == AbilityType::Spirit) + .collect_vec() + } +} diff --git a/src/ddd/board_position.rs b/src/ddd/board_position.rs new file mode 100644 index 0000000..144625a --- /dev/null +++ b/src/ddd/board_position.rs @@ -0,0 +1,33 @@ +use serde::{Deserialize, Deserializer}; + +pub type BoardPosition = (u32, u32); + +#[derive(Debug, Deserialize, PartialEq, Eq)] +struct ParseBoardPositionError; + +pub fn deserialize_position<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let str = match String::deserialize(deserializer) { + Ok(v) => v, + Err(e) => { + panic!("Tried deserializing a non-string type\nerror: {}", e); + } + }; + let str = str.to_uppercase(); + + let mut chars = str.chars(); + + let a = chars.next().unwrap_or('A'); + let b = chars.next().unwrap_or('1'); + + let a = (a.to_ascii_uppercase() as u32).saturating_sub(64); + if a == 0 { + panic!("Second position parameter {} is 0 or lower!", str); + } + + let b = b.to_digit(10).unwrap_or(0); + + Ok((a, b)) +} diff --git a/src/ddd/route.rs b/src/ddd/route.rs new file mode 100644 index 0000000..39b2c20 --- /dev/null +++ b/src/ddd/route.rs @@ -0,0 +1,71 @@ +use std::fmt::Display; + +use serde::Deserialize; + +#[derive(Debug, Deserialize, PartialEq, Eq, Clone)] +pub struct Route { + pub id: u32, + pub name: String, + pub color: RouteColor, + pub tips: Vec, + #[serde(skip_deserializing)] + pub interaction: Interaction, +} + +#[derive(Debug, Default, Deserialize, PartialEq, Eq, Clone)] +pub enum Interaction { + #[default] + None, + Poke, + Rub, +} + +impl Display for Interaction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Interaction::Poke => f.write_str("Poke"), + Interaction::Rub => f.write_str("Rub"), + _ => f.write_str(""), + } + } +} + +#[derive(Debug, Default, Deserialize, PartialEq, Eq, Clone)] +pub enum RouteColor { + #[default] + #[serde(alias = "blue")] + Blue, + #[serde(alias = "purple")] + Purple, + #[serde(alias = "yellow")] + Yellow, + #[serde(alias = "green")] + Green, + + #[serde(alias = "secret1")] + SecretGreen, + #[serde(alias = "secret2")] + SecretRed, +} + +impl Display for RouteColor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RouteColor::Blue => f.write_str("blue"), + RouteColor::Purple => f.write_str("purple"), + RouteColor::Yellow => f.write_str("yellow"), + RouteColor::Green => f.write_str("green"), + + RouteColor::SecretGreen => f.write_str("secret1"), + RouteColor::SecretRed => f.write_str("secret2"), + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Clone)] +pub struct Tip { + pub to: String, + #[serde(skip_deserializing)] + pub to_color: RouteColor, + pub tip: String, +} diff --git a/src/kh2.rs b/src/kh2.rs index 08af02f..7284f65 100644 --- a/src/kh2.rs +++ b/src/kh2.rs @@ -1,11 +1,10 @@ use std::path::PathBuf; -use rinja::Template; -use serde::Deserialize; +use askama::Template; -use crate::create_file; +use crate::{common::materials::MaterialDrops, create_file}; -pub const MATERIAL_KINDS: &[&str] = &[ +const MATERIAL_KINDS: &[&str] = &[ "blazing", "bright", "dark", @@ -19,39 +18,7 @@ pub const MATERIAL_KINDS: &[&str] = &[ "serenity", "twilight", ]; - -#[derive(Debug, Deserialize, PartialEq, Eq)] -pub struct MaterialDrops { - kind: String, - shard: Vec, - stone: Vec, - gem: Vec, - crystal: Vec, -} - -impl MaterialDrops { - fn drops(&self, kind: &str) -> &[EnemyDrop] { - match kind { - "shard" => &self.shard, - "stone" => &self.stone, - "gem" => &self.gem, - "crystal" => &self.crystal, - _ => &self.shard, - } - } -} - -#[derive(Debug, Deserialize, PartialEq, Eq)] -pub struct EnemyDrop { - from: String, - chance: u8, -} - -impl EnemyDrop { - fn texture(&self) -> String { - self.from.replace(" ", "_").to_lowercase() - } -} +const DROPS_PATH: &str = "./input/kh2/drops"; #[derive(Template)] #[template(path = "pages/kh2/drops.html")] @@ -59,8 +26,6 @@ struct DropsTemplate { pub drops: Vec, } -const DROPS_PATH: &str = "./input/kh2/drops"; - pub fn init() { tracing::info!("Loading enemy drops data from {}", DROPS_PATH); let mut drops: Vec = vec![]; @@ -69,7 +34,7 @@ pub fn init() { .unwrap() .filter_map(|f| f.ok()) .map(|f| f.path()) - .filter_map(|p| match p.extension().map_or(false, |e| e == "toml") { + .filter_map(|p| match p.extension().is_some_and(|e| e == "toml") { true => Some(p), false => None, }) diff --git a/src/kh3.rs b/src/kh3.rs index 66a1c8f..106758a 100644 --- a/src/kh3.rs +++ b/src/kh3.rs @@ -1,60 +1,11 @@ -use std::fmt::Display; - -use rinja::Template; -use serde::Deserialize; +use askama::Template; +use food::Recipes; use crate::create_file; -#[derive(Debug, Deserialize, PartialEq, Eq)] -struct Recipes { - starters: Vec, - soups: Vec, - fish: Vec, - meat: Vec, - deserts: Vec, -} +mod food; -#[derive(Debug, Deserialize, PartialEq, Eq)] -struct Recipe { - name: String, - is_special: bool, - ingredients: Vec, - normal: FoodStats, - plus: FoodStats, -} - -impl Recipe { - fn stats(&self, is_plus: &bool) -> &FoodStats { - if *is_plus { - &self.plus - } else { - &self.normal - } - } -} - -#[derive(Debug, Deserialize, PartialEq, Eq)] -struct FoodStats { - str: u8, - mag: u8, - def: u8, - hp: u8, - mp: u8, -} - -impl Display for FoodStats { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let val = serde_json::json!({ - "str": self.str, - "mag": self.mag, - "def": self.def, - "hp": self.hp, - "mp": self.mp - }); - f.write_str(&val.to_string())?; - Ok(()) - } -} +const RECIPES_PATH: &str = "./input/kh3/recipes.toml"; #[derive(Template)] #[template(path = "pages/kh3/food-sim.html")] @@ -62,8 +13,6 @@ struct RecipesTemplate { pub recipes: Recipes, } -const RECIPES_PATH: &str = "./input/kh3/recipes.toml"; - pub fn init() { tracing::info!("Loading recipes data from {}", RECIPES_PATH); let recipes_str = std::fs::read_to_string(RECIPES_PATH).unwrap(); diff --git a/src/kh3/food.rs b/src/kh3/food.rs new file mode 100644 index 0000000..1064898 --- /dev/null +++ b/src/kh3/food.rs @@ -0,0 +1,50 @@ +use std::fmt::Display; + +use serde::Deserialize; + +#[derive(Debug, Deserialize, PartialEq, Eq)] +pub struct Recipes { + pub starters: Vec, + pub soups: Vec, + pub fish: Vec, + pub meat: Vec, + pub deserts: Vec, +} + +#[derive(Debug, Deserialize, PartialEq, Eq)] +pub struct Recipe { + pub name: String, + pub is_special: bool, + pub ingredients: Vec, + pub normal: FoodStats, + pub plus: FoodStats, +} + +impl Recipe { + pub fn stats(&self, is_plus: bool) -> &FoodStats { + if is_plus { &self.plus } else { &self.normal } + } +} + +#[derive(Debug, Deserialize, PartialEq, Eq)] +pub struct FoodStats { + pub str: u8, + pub mag: u8, + pub def: u8, + pub hp: u8, + pub mp: u8, +} + +impl Display for FoodStats { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let val = serde_json::json!({ + "str": self.str, + "mag": self.mag, + "def": self.def, + "hp": self.hp, + "mp": self.mp + }); + f.write_str(&val.to_string())?; + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index a908817..e1c4d47 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,10 @@ #![allow(dead_code)] -use rinja::Template; +use askama::Template; use tracing_subscriber::EnvFilter; +mod common; + #[cfg(feature = "bbs")] mod bbs;