Compare commits

...

4 Commits

16 changed files with 676 additions and 114 deletions

View File

@ -0,0 +1,51 @@
[[codes]]
name = "Default Status"
stars = 5
[[codes]]
name = "Zero Defense"
stars = 4
[[codes]]
name = "HP Slip"
stars = 3
[[codes]]
name = "MP Slip"
stars = 4
[[codes]]
name = "No Shotlocks"
stars = 2
[[codes]]
name = "No Cure"
stars = 5
[[codes]]
name = "No Battle Items"
stars = 3
[[codes]]
name = "No Links"
stars = 2
[[codes]]
name = "No Formchanges/Grand Magic"
stars = 4
[[codes]]
name = "No Attractions"
stars = 1
[[codes]]
name = "No Team Attacks"
stars = 1
[[codes]]
name = "No Kupo Coin"
stars = 2
[[codes]]
name = "Ability Limit"
stars = 4

View File

@ -0,0 +1,169 @@
[[fights]]
world = "Olympus"
enemy = "Rock Titan"
merit = 100
[[fights]]
world = "Olympus"
enemy = "Ice Titan, Lava Titan and Tornado Titan"
merit = 200
[[fights]]
world = "Twilight Town"
enemy = "Demon Tide"
merit = 100
[[fights]]
world = "Toy Box"
enemy = "Angelic Amber"
merit = 100
[[fights]]
world = "Toy Box"
enemy = "King of Toys"
merit = 200
[[fights]]
world = "Kingdom of Corona"
enemy = "Chaos Carriage"
merit = 100
[[fights]]
world = "Kingdom of Corona"
enemy = "Grim Guarianess"
merit = 200
[[fights]]
world = "Monstropolis"
enemy = "Lump of Horror"
merit = 200
[[fights]]
world = "Arendelle"
enemy = "Marshmallow"
merit = 100
[[fights]]
world = "Arendelle"
enemy = "Skoll"
merit = 200
[[fights]]
world = "The Caribbean"
enemy = "Lightning Angler"
merit = 100
[[fights]]
world = "The Caribbean"
enemy = "Davy Jones"
merit = 200
[[fights]]
world = "San Fransokyo"
enemy = "Darkubes"
merit = 100
[[fights]]
world = "San Fransokyo"
enemy = "Dark Baymax"
merit = 200
[[fights]]
world = "Keyblade Graveyard"
enemy = "Demon Tide"
merit = 200
[[fights]]
world = "Keyblade Graveyard"
enemy = "Young Xehanort, Ansem, Xemnas"
merit = 200
[[fights]]
world = "Keyblade Graveyard"
enemy = "Dark Inferno"
merit = 400
[[fights]]
world = "Scala ad Caelum"
enemy = "Armored Xehanort"
merit = 200
[[fights]]
world = "Scala ad Caelum"
enemy = "Master Xehanort"
merit = 200
[[fights]]
world = "Re Mind"
enemy = "Armored Xehanort"
merit = 200
[[fights]]
world = "Limitcut"
enemy = "Ansem"
merit = 500
[[fights]]
world = "Limitcut"
enemy = "Xemnas"
merit = 500
[[fights]]
world = "Limitcut"
enemy = "Xigbar"
merit = 500
[[fights]]
world = "Limitcut"
enemy = "Luxord"
merit = 500
[[fights]]
world = "Limitcut"
enemy = "Larxene"
merit = 500
[[fights]]
world = "Limitcut"
enemy = "Marluxia"
merit = 500
[[fights]]
world = "Limitcut"
enemy = "Saix"
merit = 500
[[fights]]
world = "Limitcut"
enemy = "Terra-Xehanort"
merit = 500
[[fights]]
world = "Limitcut"
enemy = "Dark Riku"
merit = 500
[[fights]]
world = "Limitcut"
enemy = "Vanitas"
merit = 500
[[fights]]
world = "Limitcut"
enemy = "Young Xehanort"
merit = 500
[[fights]]
world = "Limitcut"
enemy = "Xion"
merit = 500
[[fights]]
world = "Limitcut"
enemy = "Master Xehanort"
merit = 500
[[fights]]
world = "Secret"
enemy = "Yozora"
merit = 600

View File

@ -0,0 +1,139 @@
const CODES_STORAGE_NAME = "kh3/pro-codes-sim/codes/";
const MERIT_STORAGE_NAME = "kh3/pro-codes-sim/merit/";
const STAR_MULTIPLIER = 1.25;
let selectionAllState = false;
let stars = 0;
let totalMerit = 0;
let fightMerit = [];
document.addEventListener("DOMContentLoaded", (event) => {
const codes = document.querySelectorAll("#codes .slot input");
const fights = document.querySelectorAll("#fights .slot button");
// Loading enabled codes
for (let code of codes) {
const hasCodeToggled =
localStorage.getItem(CODES_STORAGE_NAME + code.id) === "true"
? true
: false;
if (hasCodeToggled) {
// Normally the toggleCode gets called after the checkbox is checked,
// since we don't interact with it at this point we mark it as checked manually here
code.checked = true;
toggleCode(code);
}
}
// Loading marked fights and their merit
for (let fight of fights) {
const source = fight.dataset["meritSource"];
const merit =
Number(localStorage.getItem(MERIT_STORAGE_NAME + source)) ?? 0;
if (merit > 0) {
markFight(fight, merit);
}
}
});
function markFight(data, loadedMerit = 0) {
const source = data.dataset["meritSource"];
const enemyName = data.dataset["enemy"];
const baseMerit = data.dataset["baseMerit"];
const isMarked = data.dataset["marked"] === "true" ? true : false;
const fightLabel = data.parentNode.querySelector("span");
const isLoading = loadedMerit > 0;
if (isMarked) {
data.dataset["marked"] = false;
totalMerit -= fightMerit[source];
updateMeritCounter();
fightMerit[source] = 0;
localStorage.removeItem(MERIT_STORAGE_NAME + source);
data.innerText = "Mark";
data.classList.remove("danger");
fightLabel.innerText = enemyName;
} else {
let merit = isLoading
? loadedMerit
: baseMerit * (stars * STAR_MULTIPLIER);
data.dataset["marked"] = true;
totalMerit += merit;
updateMeritCounter();
fightMerit[source] = merit;
localStorage.setItem(MERIT_STORAGE_NAME + source, merit);
data.innerText = "Unmark";
data.classList.add("danger");
fightLabel.innerText = enemyName + " | " + merit + " Merit";
}
}
function toggleCode(code) {
let codeStars = Number(code.dataset["stars"]);
if (code.checked) {
stars += codeStars;
localStorage.setItem(CODES_STORAGE_NAME + code.id, true);
} else {
stars -= codeStars;
selectionAllState = false;
updateAllButton();
localStorage.removeItem(CODES_STORAGE_NAME + code.id);
}
updateStarsCounter();
}
function toggleAllCodes() {
const codes = document.querySelectorAll("#codes .slot input");
if (!selectionAllState) {
selectionAllState = true;
updateAllButton();
for (let code of codes) {
if (code.checked) {
continue;
}
code.checked = true;
stars += Number(code.dataset["stars"]);
localStorage.setItem(CODES_STORAGE_NAME + code.id, true);
}
} else {
selectionAllState = false;
updateAllButton();
for (let code of codes) {
if (!code.checked) {
continue;
}
code.checked = false;
stars -= Number(code.dataset["stars"]);
localStorage.removeItem(CODES_STORAGE_NAME + code.id);
}
}
updateStarsCounter();
}
function updateStarsCounter() {
let counter = document.getElementById("totalStars");
counter.innerText = "Stars: " + stars;
}
function updateMeritCounter() {
let counter = document.getElementById("totalMerit");
counter.innerText = "Merit: " + totalMerit;
}
function updateAllButton() {
const btn = document.querySelector("#codes .slot button");
if (selectionAllState) {
btn.classList.add("danger");
} else {
btn.classList.remove("danger");
}
}
Object.assign(window, { markFight, toggleCode, toggleAllCodes });

View File

@ -17,3 +17,19 @@ table {
} }
} }
} }
.filters {
display: flex;
flex-direction: column;
.row {
display: flex;
div {
display: flex;
align-items: center;
gap: 0.5rem;
flex: 0 1 100px;
}
}
}

View File

@ -67,8 +67,8 @@ button {
background: var(--button-bg-color); background: var(--button-bg-color);
color: var(--text-color); color: var(--text-color);
padding: 8px; padding: 8px;
border-color: var(--button-border-color);
border-style: groove; border-style: groove;
border-color: var(--button-border-color);
border-bottom-color: var(--primary-color); border-bottom-color: var(--primary-color);
transition-duration: 0.1s; transition-duration: 0.1s;
@ -85,6 +85,10 @@ button {
opacity: 0.6; opacity: 0.6;
cursor: not-allowed; cursor: not-allowed;
} }
&.danger {
border-bottom-color: var(--error-color);
}
} }
input[type="text"] { input[type="text"] {
@ -108,7 +112,6 @@ input[type="radio"] {
cursor: pointer; cursor: pointer;
width: 24px; width: 24px;
height: 24px; height: 24px;
padding-top: 8px;
& + label { & + label {
cursor: pointer; cursor: pointer;
@ -133,7 +136,7 @@ input[type="radio"] {
content: "\2713"; content: "\2713";
color: var(--primary-color); color: var(--primary-color);
font-size: 48px; font-size: 48px;
top: -10px; top: -16px;
left: -4px; left: -4px;
align-content: center; align-content: center;
text-align: center; text-align: center;
@ -152,7 +155,7 @@ input[type="radio"] {
&:checked:after { &:checked:after {
font-size: 16px; font-size: 16px;
top: 10px; top: 2px;
left: 6px; left: 6px;
} }

View File

@ -15,4 +15,7 @@
--primary-color: #00aa00; --primary-color: #00aa00;
--primary-light-color: #60de60; --primary-light-color: #60de60;
--error-color: hsl(2, 68%, 53%);
--error-color-lighter: hsl(2, 68%, 63%);
} }

View File

@ -90,10 +90,19 @@
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
width: 40%; width: 40%;
row-gap: 20px; row-gap: 1rem;
position: relative; position: relative;
div {
align-items: center;
display: flex;
gap: 0.5rem;
}
} }
.tracked-filter { .tracked-filter {
margin-bottom: 16px; margin-bottom: 16px;
align-items: center;
display: flex;
gap: 0.5rem;
} }

View File

@ -0,0 +1,39 @@
#content {
display: flex;
margin: 0 4rem;
}
#score,
#codes,
#fights {
display: flex;
flex-direction: column;
max-height: 80vh;
margin-top: 2rem;
width: 33%;
position: relative;
overflow: hidden;
.slot {
display: flex;
align-items: center;
margin-bottom: 0.5rem;
gap: 0.5rem;
}
.inset-box-shadow {
box-shadow: inset 0px 0px 40px 40px var(--bg-color);
width: 200%;
height: 100%;
position: absolute;
margin-left: -200px;
pointer-events: none;
}
.scrollable {
width: 100%;
height: 100%;
overflow: scroll;
padding: 60px 0px;
}
}

View File

@ -1,6 +1,7 @@
use askama::Template; use askama::Template;
use food::Recipes; use food::Recipes;
use ingredient::Ingredient; use ingredient::Ingredient;
use pro_codes::{ProCode, ProCodeFight, ProCodeFights, ProCodes};
use crate::{ use crate::{
RuntimeModule, RuntimeModule,
@ -10,10 +11,13 @@ use crate::{
mod food; mod food;
mod ingredient; mod ingredient;
mod pro_codes;
const ENEMIES_PATH: &str = "./input/kh3/enemies"; const ENEMIES_PATH: &str = "./input/kh3/enemies";
const RECIPES_PATH: &str = "./input/kh3/recipes.toml"; const RECIPES_PATH: &str = "./input/kh3/recipes.toml";
const INGREDIENTS_PATH: &str = "./input/kh3/ingredients"; const INGREDIENTS_PATH: &str = "./input/kh3/ingredients";
const PRO_CODES_PATH: &str = "./input/kh3/pro-codes/codes.toml";
const PRO_CODE_FIGHTS_PATH: &str = "./input/kh3/pro-codes/fights.toml";
#[derive(Template)] #[derive(Template)]
#[template(path = "pages/kh3/drops.html")] #[template(path = "pages/kh3/drops.html")]
@ -33,6 +37,13 @@ struct IngredientsTemplate {
pub ingredients: Vec<Ingredient>, pub ingredients: Vec<Ingredient>,
} }
#[derive(Template)]
#[template(path = "pages/kh3/pro-codes-sim.html")]
struct ProCodesTemplate {
pub pro_codes: Vec<ProCode>,
pub fights: Vec<ProCodeFight>,
}
pub struct Module; pub struct Module;
impl RuntimeModule for Module { impl RuntimeModule for Module {
@ -48,19 +59,29 @@ impl RuntimeModule for Module {
let recipes_str = std::fs::read_to_string(RECIPES_PATH).unwrap(); let recipes_str = std::fs::read_to_string(RECIPES_PATH).unwrap();
let recipes = toml::from_str::<Recipes>(&recipes_str).unwrap(); let recipes = toml::from_str::<Recipes>(&recipes_str).unwrap();
tracing::info!("Loading pro codes data from {}", PRO_CODES_PATH);
let pro_codes_str = std::fs::read_to_string(PRO_CODES_PATH).unwrap();
let pro_codes = toml::from_str::<ProCodes>(&pro_codes_str).unwrap();
let pro_code_fights_str = std::fs::read_to_string(PRO_CODE_FIGHTS_PATH).unwrap();
let pro_code_fights = toml::from_str::<ProCodeFights>(&pro_code_fights_str).unwrap();
tracing::info!("Generating the KH3 drops template"); tracing::info!("Generating the KH3 drops template");
let drops_template = DropsTemplate { data: drops }; let drops_template = DropsTemplate { data: drops };
create_file("./out/kh3", "drops", drops_template).unwrap(); create_file("./out/kh3", "drops", drops_template).unwrap();
tracing::info!("Generating the KH3 ingredients template"); tracing::info!("Generating the KH3 ingredients template");
let ingredients_template = IngredientsTemplate { ingredients }; let ingredients_template = IngredientsTemplate { ingredients };
create_file("./out/kh3", "ingredients", ingredients_template).unwrap(); create_file("./out/kh3", "ingredients", ingredients_template).unwrap();
tracing::info!("Generating the KH3 recipes template"); tracing::info!("Generating the KH3 recipes template");
let food_template = RecipesTemplate { recipes }; let food_template = RecipesTemplate { recipes };
create_file("./out/kh3", "food-sim", food_template).unwrap(); create_file("./out/kh3", "food-sim", food_template).unwrap();
tracing::info!("Generating the KH3 pro codes template");
let pro_codes_template = ProCodesTemplate {
pro_codes: pro_codes.codes,
fights: pro_code_fights.fights,
};
create_file("./out/kh3", "pro-codes-sim", pro_codes_template).unwrap();
} }
} }

View File

@ -0,0 +1,38 @@
use serde::Deserialize;
#[derive(Debug, Deserialize, PartialEq, Eq)]
pub struct ProCodes {
pub codes: Vec<ProCode>,
}
#[derive(Debug, Deserialize, PartialEq, Eq)]
pub struct ProCode {
pub name: String,
pub stars: u8,
}
impl ProCode {
pub fn id(&self) -> String {
self.name.replace(" ", "_").to_lowercase()
}
}
#[derive(Debug, Deserialize, PartialEq, Eq)]
pub struct ProCodeFights {
pub fights: Vec<ProCodeFight>,
}
#[derive(Debug, Deserialize, PartialEq, Eq)]
pub struct ProCodeFight {
pub world: String,
pub enemy: String,
pub merit: u16,
}
impl ProCodeFight {
pub fn source(&self) -> String {
let world = self.world.replace(" ", "_").to_lowercase();
let enemy = self.enemy.replace(" ", "_").to_lowercase();
format!("{world}/{enemy}")
}
}

View File

@ -1,3 +1,4 @@
<div>
<input <input
type="radio" type="radio"
id="hasAny" id="hasAny"
@ -7,7 +8,9 @@
checked checked
/> />
<label for="hasAny">Any</label> <label for="hasAny">Any</label>
</div>
<div>
<input <input
type="radio" type="radio"
id="hasTerra" id="hasTerra"
@ -16,7 +19,9 @@
name="charFilter" name="charFilter"
/> />
<label for="hasTerra">Terra</label> <label for="hasTerra">Terra</label>
</div>
<div>
<input <input
type="radio" type="radio"
id="hasVentus" id="hasVentus"
@ -25,7 +30,9 @@
name="charFilter" name="charFilter"
/> />
<label for="hasVentus">Ventus</label> <label for="hasVentus">Ventus</label>
</div>
<div>
<input <input
type="radio" type="radio"
id="hasAqua" id="hasAqua"
@ -34,3 +41,4 @@
name="charFilter" name="charFilter"
/> />
<label for="hasAqua">Aqua</label> <label for="hasAqua">Aqua</label>
</div>

View File

@ -1,5 +1,9 @@
<div class="row">
<input type="text" id="filter" autocomplete="off" /> <input type="text" id="filter" autocomplete="off" />
<br /> </div>
<div class="row">
<div>
<input <input
type="radio" type="radio"
id="searchResult" id="searchResult"
@ -9,7 +13,9 @@
checked checked
/> />
<label for="searchResult">Result</label> <label for="searchResult">Result</label>
</div>
<div>
<input <input
type="radio" type="radio"
id="searchIngredients" id="searchIngredients"
@ -18,7 +24,9 @@
name="search" name="search"
/> />
<label for="searchIngredients">Ingredients</label> <label for="searchIngredients">Ingredients</label>
</div>
<div>
<input <input
type="radio" type="radio"
id="searchAbilities" id="searchAbilities"
@ -27,3 +35,5 @@
name="search" name="search"
/> />
<label for="searchAbilities">Abilities</label> <label for="searchAbilities">Abilities</label>
</div>
</div>

View File

@ -1,3 +1,4 @@
<div>
<input <input
type="radio" type="radio"
id="isAny" id="isAny"
@ -7,7 +8,9 @@
checked checked
/> />
<label for="isAny">Any</label> <label for="isAny">Any</label>
</div>
<div>
<input <input
type="radio" type="radio"
id="isAttack" id="isAttack"
@ -16,7 +19,9 @@
value="attack" value="attack"
/> />
<label for="isAttack">Attack</label> <label for="isAttack">Attack</label>
</div>
<div>
<input <input
type="radio" type="radio"
id="isMagic" id="isMagic"
@ -25,7 +30,9 @@
value="magic" value="magic"
/> />
<label for="isMagic">Magic</label> <label for="isMagic">Magic</label>
</div>
<div>
<input <input
type="radio" type="radio"
id="isAction" id="isAction"
@ -34,7 +41,9 @@
value="action" value="action"
/> />
<label for="isAction">Action</label> <label for="isAction">Action</label>
</div>
<div>
<input <input
type="radio" type="radio"
id="isShotlock" id="isShotlock"
@ -43,3 +52,4 @@
value="shotlock" value="shotlock"
/> />
<label for="isShotlock">Shotlock</label> <label for="isShotlock">Shotlock</label>
</div>

View File

@ -11,11 +11,11 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="filters">
{% include "components/bbs/search.html" %} {% include "components/bbs/search.html" %}
<br /> <div class="row">{% include "components/bbs/type-filters.html" %}</div>
{% include "components/bbs/type-filters.html" %} <div class="row">{% include "components/bbs/char-filters.html" %}</div>
<br /> </div>
{% include "components/bbs/char-filters.html" %}
<table> <table>
<thead> <thead>

View File

@ -26,6 +26,7 @@
<li><a href="./kh3/drops.html">Material Drops</a></li> <li><a href="./kh3/drops.html">Material Drops</a></li>
<li><a href="./kh3/ingredients.html">Ingredients</a></li> <li><a href="./kh3/ingredients.html">Ingredients</a></li>
<li><a href="./kh3/food-sim.html">Food Simulator</a></li> <li><a href="./kh3/food-sim.html">Food Simulator</a></li>
<li><a href="./kh3/pro-codes-sim.html">Pro Codes Simulator</a></li>
</ul> </ul>
{% endif %} {% endif %}

View File

@ -0,0 +1,45 @@
{% extends "layouts/base.html" %}
{% block title %}KH3 - Pro Codes Simulator{% endblock %}
{% block head %}
<link rel="stylesheet" href="{{ crate::find_hash("/public/styles/kh3/pro-codes-sim.css") }}"></link>
<script
type="module"
src="{{ crate::find_hash("/public/scripts/kh3/pro-codes-sim.js") }}"
></script>
{% endblock %}
{% block content %}
<div id="score">
<h1 id="totalMerit">Merit: 0</h1>
<h2 id="totalStars">Stars: 0</h2>
</div>
<div id="codes">
<div class="slot">
<button onclick="toggleAllCodes()">Toggle All</button>
</div>
{% for code in pro_codes %}
<div class="slot">
<input type="checkbox" id="{{code.id()}}" autocomplete="off" data-stars="{{code.stars}}" onclick="toggleCode(this)">
<label for="{{code.id()}}">{{code.name}}</label>
<span>
{% for star in 0..5 %}
{% if star < code.stars %}{% else %}{% endif %}
{% endfor %}
</span>
</div>
{% endfor %}
</div>
<div id="fights">
<div class="inset-box-shadow"></div>
<div class="scrollable">
{% for fight in fights %}
<div class="slot">
<button onclick="markFight(this)" data-enemy="{{fight.enemy}}" data-merit-source="{{fight.source()}}" data-base-merit="{{fight.merit}}">Mark</button>
<span>{{fight.enemy}}</span>
</div>
{% endfor %}
</div>
</div>
{% endblock %}