use std::{fmt::Display, panic, path::PathBuf}; use itertools::Itertools; use rinja::Template; use serde::{Deserialize, Deserializer}; use crate::create_file; #[derive(Debug, Deserialize, PartialEq, Eq)] struct Board { order: u32, spirit: String, routes: Vec, abilities: Vec, #[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(""), } } } #[derive(Template)] #[template(path = "pages/ddd/boards.html")] 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![]; // Loading multiple files into one vector due to the size of each board let paths = std::fs::read_dir(ABILITIES_PATH) .unwrap() .filter_map(|f| f.ok()) .map(|f| f.path()) .filter_map(|p| match p.extension().map_or(false, |e| e == "toml") { true => Some(p), false => None, }) .collect::>(); for path in paths { let board_str = std::fs::read_to_string(path).unwrap(); let mut board = toml::from_str::(&board_str).unwrap(); board.init_routes(); board.init_total_lp(); board.init_max_level(); board.init_stats(); board.init_ability_help(); // dbg!(&board); boards.push(board); } boards.sort_by(|a, b| a.order.cmp(&b.order)); tracing::info!("Generating the DDD ability boards template"); let boards_template = AbilitiesTemplate { boards }; create_file("./out/ddd", "boards", boards_template.render().unwrap()).unwrap(); }