Reorganizing assets for game specific mods
parent
14fca5a001
commit
0968bfc157
|
@ -2,6 +2,7 @@
|
|||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
out/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
|
|
4166
index.html
4166
index.html
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,164 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use askama::Template;
|
||||
use itertools::Itertools;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq, Eq)]
|
||||
enum Character {
|
||||
#[serde(alias = "A")]
|
||||
Aqua,
|
||||
#[serde(alias = "V")]
|
||||
Ventus,
|
||||
#[serde(alias = "T")]
|
||||
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-commands.html", whitespace = "suppress")]
|
||||
struct CommandsTemplate {
|
||||
pub commands: Vec<Command>,
|
||||
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 json data from {}", ABILITIES_PATH);
|
||||
let abilities_str = std::fs::read_to_string(ABILITIES_PATH).unwrap();
|
||||
let abilities = serde_json::from_str::<Vec<Ability>>(&abilities_str).unwrap();
|
||||
|
||||
tracing::info!("Loading finishers json data from {}", ABILITIES_PATH);
|
||||
let finishers_str = std::fs::read_to_string(FINISHERS_PATH).unwrap();
|
||||
let finishers = serde_json::from_str::<HashMap<String, Finisher>>(&finishers_str).unwrap();
|
||||
|
||||
tracing::info!("Loading commands json data from {}", ABILITIES_PATH);
|
||||
let commands_str = std::fs::read_to_string(COMMANDS_PATH).unwrap();
|
||||
let mut commands = serde_json::from_str::<Vec<Command>>(&commands_str).unwrap();
|
||||
|
||||
// Create a vec with all the crystal variants found in abilities
|
||||
let crystals = abilities
|
||||
.iter()
|
||||
.map(|x| x.from.clone())
|
||||
.unique()
|
||||
.sorted()
|
||||
.collect();
|
||||
|
||||
// Create a vec of crystals and what ability they give for each recipe
|
||||
for cmd in commands.iter_mut() {
|
||||
for recipe in cmd.recipes.iter_mut() {
|
||||
recipe.set_abilities(&abilities);
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!("Generating the commands table template");
|
||||
let template = CommandsTemplate { commands, crystals };
|
||||
|
||||
std::fs::write("./out/index.html", template.render().unwrap()).unwrap();
|
||||
}
|
161
src/main.rs
161
src/main.rs
|
@ -1,136 +1,9 @@
|
|||
#![allow(dead_code)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use askama::Template;
|
||||
use itertools::Itertools;
|
||||
use serde::Deserialize;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq, Eq)]
|
||||
enum Character {
|
||||
#[serde(alias = "A")]
|
||||
Aqua,
|
||||
#[serde(alias = "V")]
|
||||
Ventus,
|
||||
#[serde(alias = "T")]
|
||||
Terra,
|
||||
}
|
||||
mod bbs;
|
||||
|
||||
#[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/commands.html", whitespace = "suppress")]
|
||||
struct CommandsTemplate {
|
||||
pub commands: Vec<Command>,
|
||||
pub crystals: Vec<String>,
|
||||
}
|
||||
|
||||
const ABILITIES_PATH: &str = "./input/abilities.json";
|
||||
const FINISHERS_PATH: &str = "./input/finish-commands.json";
|
||||
const COMMANDS_PATH: &str = "./input/commands.json";
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
fn main() {
|
||||
|
@ -140,35 +13,5 @@ fn main() {
|
|||
.event_format(tracing_subscriber::fmt::format().with_source_location(true))
|
||||
.init();
|
||||
|
||||
tracing::info!("Loading abilities json data from {}", ABILITIES_PATH);
|
||||
let abilities_str = std::fs::read_to_string(ABILITIES_PATH).unwrap();
|
||||
let abilities = serde_json::from_str::<Vec<Ability>>(&abilities_str).unwrap();
|
||||
|
||||
tracing::info!("Loading finishers json data from {}", ABILITIES_PATH);
|
||||
let finishers_str = std::fs::read_to_string(FINISHERS_PATH).unwrap();
|
||||
let finishers = serde_json::from_str::<HashMap<String, Finisher>>(&finishers_str).unwrap();
|
||||
|
||||
tracing::info!("Loading commands json data from {}", ABILITIES_PATH);
|
||||
let commands_str = std::fs::read_to_string(COMMANDS_PATH).unwrap();
|
||||
let mut commands = serde_json::from_str::<Vec<Command>>(&commands_str).unwrap();
|
||||
|
||||
// Create a vec with all the crystal variants found in abilities
|
||||
let crystals = abilities
|
||||
.iter()
|
||||
.map(|x| x.from.clone())
|
||||
.unique()
|
||||
.sorted()
|
||||
.collect();
|
||||
|
||||
// Create a vec of crystals and what ability they give for each recipe
|
||||
for cmd in commands.iter_mut() {
|
||||
for recipe in cmd.recipes.iter_mut() {
|
||||
recipe.set_abilities(&abilities);
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!("Generating the commands table template");
|
||||
let template = CommandsTemplate { commands, crystals };
|
||||
|
||||
std::fs::write("./index.html", template.render().unwrap()).unwrap();
|
||||
bbs::init();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,205 @@
|
|||
{% extends "layouts/base.html" %}
|
||||
|
||||
{% block title %}Commands{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<script>
|
||||
let charFilter = "";
|
||||
let typeFilter = "";
|
||||
let searchType = "commands";
|
||||
|
||||
document.addEventListener("DOMContentLoaded", (event) => {
|
||||
const searchFilter = document.getElementById("filter");
|
||||
let filterHandler = debounce(() => filter());
|
||||
searchFilter.addEventListener("keyup", filterHandler);
|
||||
searchFilter.placeholder = "Search commands...";
|
||||
|
||||
const searchInputs = document.querySelectorAll(
|
||||
'input[type="radio"][name="search"]',
|
||||
);
|
||||
|
||||
searchInputs.forEach(function (item, index) {
|
||||
item.addEventListener("input", function () {
|
||||
searchType = this.checked ? this.value : "";
|
||||
searchFilter.placeholder = "Search " + this.value + "...";
|
||||
filter();
|
||||
});
|
||||
});
|
||||
|
||||
const charFilters = document.querySelectorAll(
|
||||
'input[type="radio"][name="charFilter"]',
|
||||
);
|
||||
|
||||
charFilters.forEach(function (item, index) {
|
||||
item.addEventListener("input", function () {
|
||||
charFilter = this.checked ? this.value : "";
|
||||
filter();
|
||||
});
|
||||
});
|
||||
|
||||
const typeFilters = document.querySelectorAll(
|
||||
'input[type="radio"][name="typeFilter"]',
|
||||
);
|
||||
|
||||
typeFilters.forEach(function (item, index) {
|
||||
item.addEventListener("input", function () {
|
||||
typeFilter = this.checked ? this.value : "";
|
||||
filter();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function debounce(callback, wait = 300) {
|
||||
let timeoutId = null;
|
||||
return (...args) => {
|
||||
window.clearTimeout(timeoutId);
|
||||
timeoutId = window.setTimeout(() => {
|
||||
callback(...args);
|
||||
}, wait);
|
||||
};
|
||||
}
|
||||
|
||||
function filter() {
|
||||
const table = document.querySelector("table tbody");
|
||||
const search = document
|
||||
.getElementById("filter")
|
||||
.value.toLowerCase();
|
||||
|
||||
for (const child of table.children) {
|
||||
const tds = child.children;
|
||||
resetStyle(child, tds);
|
||||
|
||||
if (search.length > 0) {
|
||||
if (searchType === "commands") {
|
||||
// Check for command name
|
||||
if (!tds[1].innerText.toLowerCase().includes(search)) {
|
||||
child.style.display = "none";
|
||||
} else {
|
||||
applyStyle(tds[1]);
|
||||
}
|
||||
} else if (searchType === "ingredients") {
|
||||
// Ingredients
|
||||
if (!tds[2].innerText.toLowerCase().includes(search)) {
|
||||
if (
|
||||
!tds[3].innerText.toLowerCase().includes(search)
|
||||
) {
|
||||
child.style.display = "none";
|
||||
} else {
|
||||
applyStyle(tds[3]);
|
||||
}
|
||||
} else {
|
||||
applyStyle(tds[2]);
|
||||
if (
|
||||
tds[3].innerText.toLowerCase().includes(search)
|
||||
) {
|
||||
applyStyle(tds[3]);
|
||||
}
|
||||
}
|
||||
} else if (searchType === "abilities") {
|
||||
// Abilities
|
||||
hasLine = false;
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const id = i + 6;
|
||||
const ablName = tds[id].innerText.toLowerCase();
|
||||
if (ablName.includes(search)) {
|
||||
applyStyle(tds[id]);
|
||||
hasLine = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasLine) {
|
||||
child.style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const typeCheck =
|
||||
typeFilter === "" || tds[1].className === typeFilter;
|
||||
const charCheck =
|
||||
charFilter === "" || child.className.includes(charFilter);
|
||||
|
||||
if (child.style.display == "" && (!typeCheck || !charCheck)) {
|
||||
child.style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resetStyle(child, tds) {
|
||||
child.style.display = "";
|
||||
for (const td of tds) {
|
||||
td.style.fontWeight = "inherit";
|
||||
td.style.color = "#fff";
|
||||
}
|
||||
}
|
||||
|
||||
function applyStyle(el) {
|
||||
el.style.fontWeight = "bold";
|
||||
el.style.color = "red";
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include "components/bbs/search.html" %}
|
||||
<br />
|
||||
{% include "components/bbs/type-filters.html" %}
|
||||
<br />
|
||||
{% include "components/bbs/char-filters.html" %}
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th rowspan="2">Character</th>
|
||||
<th rowspan="2">Command</th>
|
||||
<th rowspan="2">Ingredient A</th>
|
||||
<th rowspan="2">Ingredient B</th>
|
||||
<th rowspan="2">Type</th>
|
||||
<th rowspan="2">Chance</th>
|
||||
<th colspan="7">Abilities</th>
|
||||
</tr>
|
||||
<tr>
|
||||
{% for crystal in crystals %}
|
||||
<th>{{ crystal }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for cmd in commands %}
|
||||
{% for recipe in cmd.recipes %}
|
||||
<tr class="{{ recipe.get_unlock_chars() }}">
|
||||
<td>
|
||||
<div class="charlist">
|
||||
<!-- RGB moment -->
|
||||
<span class="terra">
|
||||
{% if recipe.can_unlock(Character::Terra) %}T{% endif %}
|
||||
</span>
|
||||
<span class="ventus">
|
||||
{% if recipe.can_unlock(Character::Ventus) %}V{% endif %}
|
||||
</span>
|
||||
<span class="aqua">
|
||||
{% if recipe.can_unlock(Character::Aqua) %}A{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="{{ cmd.category }}">{{ cmd.name }}</td>
|
||||
<td>{{ recipe.ingredients.0 }}</td>
|
||||
<td>{{ recipe.ingredients.1 }}</td>
|
||||
<td>{{ recipe.type }}</td>
|
||||
<td>{{ recipe.chance }}%</td>
|
||||
{% for crystal in crystals %}
|
||||
{% let ability = recipe.get_ability(crystal) %}
|
||||
<td>
|
||||
{% if ability.is_some() %}
|
||||
{{ ability.unwrap().name }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
Loading…
Reference in New Issue