Updated dependencies + a major split and cleanup of the code

master
Wynd 2025-06-24 19:56:47 +03:00
parent b57fa29fae
commit 9180d330d6
20 changed files with 606 additions and 565 deletions

View File

@ -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 = []

View File

@ -1,2 +1,2 @@
[toolchain]
channel = "stable"
channel = "1.85"

View File

@ -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<char>,
}
#[derive(Debug, Deserialize)]
struct Command {
name: String,
category: String,
char: Vec<char>,
#[serde(default)]
starting: Option<bool>,
#[serde(default)]
info: Option<String>,
#[serde(default)]
recipes: Vec<CommandRecipe>,
}
impl Command {}
#[derive(Debug, Deserialize)]
struct CommandRecipe {
char: Vec<Character>,
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<Ability> {
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<Character>,
level: u8,
#[serde(default)]
follows: Vec<String>,
goal: String,
color: String,
}
#[derive(Template)]
#[template(path = "pages/bbs/melding.html")]
struct CommandsTemplate {
@ -127,10 +35,6 @@ struct CommandsTemplate {
pub crystals: Vec<String>,
}
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();

10
src/bbs/ability.rs 100644
View File

@ -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<char>,
}

16
src/bbs/command.rs 100644
View File

@ -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<char>,
#[serde(default)]
pub starting: Option<bool>,
#[serde(default)]
pub info: Option<String>,
#[serde(default)]
pub recipes: Vec<CommandRecipe>,
}

View File

@ -0,0 +1,14 @@
use serde::Deserialize;
use super::Character;
#[derive(Debug, Deserialize)]
pub struct Finisher {
pub name: String,
pub char: Vec<Character>,
pub level: u8,
#[serde(default)]
pub follows: Vec<String>,
pub goal: String,
pub color: String,
}

72
src/bbs/melding.rs 100644
View File

@ -0,0 +1,72 @@
use serde::Deserialize;
use super::{ability::Ability, Character};
#[derive(Debug, Deserialize)]
pub struct CommandRecipe {
pub char: Vec<Character>,
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<Ability> {
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;
}
}

2
src/common.rs 100644
View File

@ -0,0 +1,2 @@
pub mod direction;
pub mod materials;

View File

@ -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,
}

View File

@ -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<String>,
}
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<EnemyDrop>,
pub stone: Vec<EnemyDrop>,
pub gem: Vec<EnemyDrop>,
pub crystal: Vec<EnemyDrop>,
}
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,
}
}
}

View File

@ -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<Route>,
abilities: Vec<Ability>,
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::<u32>().unwrap_or(0)
})
.sum::<u32>();
}
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::<u32>().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<Tip>,
#[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<BoardPosition, D::Error>
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<u32>,
path: Vec<Direction>,
tip: Option<String>,
}
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<Board>,
}
const ABILITIES_PATH: &str = "./input/ddd/abilities";
pub fn init() {
tracing::info!("Loading ability boards data from {}", ABILITIES_PATH);
let mut boards: Vec<Board> = 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,
})

64
src/ddd/ability.rs 100644
View File

@ -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<u32>,
pub path: Vec<Direction>,
pub tip: Option<String>,
}
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
}
}

177
src/ddd/board.rs 100644
View File

@ -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<Route>,
pub abilities: Vec<Ability>,
#[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::<u32>().unwrap_or(0)
})
.sum::<u32>();
}
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::<u32>().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()
}
}

View File

@ -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<BoardPosition, D::Error>
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))
}

71
src/ddd/route.rs 100644
View File

@ -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<Tip>,
#[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,
}

View File

@ -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<EnemyDrop>,
stone: Vec<EnemyDrop>,
gem: Vec<EnemyDrop>,
crystal: Vec<EnemyDrop>,
}
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<MaterialDrops>,
}
const DROPS_PATH: &str = "./input/kh2/drops";
pub fn init() {
tracing::info!("Loading enemy drops data from {}", DROPS_PATH);
let mut drops: Vec<MaterialDrops> = 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,
})

View File

@ -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<Recipe>,
soups: Vec<Recipe>,
fish: Vec<Recipe>,
meat: Vec<Recipe>,
deserts: Vec<Recipe>,
}
mod food;
#[derive(Debug, Deserialize, PartialEq, Eq)]
struct Recipe {
name: String,
is_special: bool,
ingredients: Vec<String>,
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();

50
src/kh3/food.rs 100644
View File

@ -0,0 +1,50 @@
use std::fmt::Display;
use serde::Deserialize;
#[derive(Debug, Deserialize, PartialEq, Eq)]
pub struct Recipes {
pub starters: Vec<Recipe>,
pub soups: Vec<Recipe>,
pub fish: Vec<Recipe>,
pub meat: Vec<Recipe>,
pub deserts: Vec<Recipe>,
}
#[derive(Debug, Deserialize, PartialEq, Eq)]
pub struct Recipe {
pub name: String,
pub is_special: bool,
pub ingredients: Vec<String>,
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(())
}
}

View File

@ -1,8 +1,10 @@
#![allow(dead_code)]
use rinja::Template;
use askama::Template;
use tracing_subscriber::EnvFilter;
mod common;
#[cfg(feature = "bbs")]
mod bbs;