Updated dependencies + a major split and cleanup of the code
parent
b57fa29fae
commit
9180d330d6
21
Cargo.toml
21
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 = []
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
[toolchain]
|
||||
channel = "stable"
|
||||
channel = "1.85"
|
||||
|
|
122
src/bbs.rs
122
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<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();
|
||||
|
|
|
@ -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>,
|
||||
}
|
|
@ -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>,
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
pub mod direction;
|
||||
pub mod materials;
|
|
@ -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,
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
359
src/ddd.rs
359
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<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,
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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,
|
||||
}
|
45
src/kh2.rs
45
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<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,
|
||||
})
|
||||
|
|
59
src/kh3.rs
59
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<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();
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
#![allow(dead_code)]
|
||||
|
||||
use rinja::Template;
|
||||
use askama::Template;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
mod common;
|
||||
|
||||
#[cfg(feature = "bbs")]
|
||||
mod bbs;
|
||||
|
||||
|
|
Loading…
Reference in New Issue