New system for drop tracking combined into enemy data

master
Wynd 2025-06-26 01:08:13 +03:00
parent 904a03733d
commit d8fd3d7111
28 changed files with 671 additions and 63 deletions

View File

@ -12,6 +12,7 @@ tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
itertools = "0.14" itertools = "0.14"
blake3 = "1.8" blake3 = "1.8"
ordered-float = { version = "5.0", features = ["serde"] }
[features] [features]
default = ["bbs", "ddd", "kh3", "kh2", "kh1"] default = ["bbs", "ddd", "kh3", "kh2", "kh1"]

View File

@ -0,0 +1,24 @@
name = "Air Pirate"
[[world]]
name = "Neverland"
room = ""
[[drops]]
name = "Hi-Potion"
kind = "item"
chance = 2
[[drops]]
name = "Mega-Potion"
kind = "item"
chance = 1
[[drops]]
name = "Power Gem"
kind = "material"
chance = 4
[drops.material]
category = "power"
kind = "gem"

View File

@ -0,0 +1,24 @@
name = "Air Soldier"
[[world]]
name = ""
room = ""
[[drops]]
name = "Potion"
kind = "item"
chance = 2
[[drops]]
name = "Hi-Potion"
kind = "item"
chance = 1
[[drops]]
name = "Spirit Gem"
kind = "material"
chance = 4
[drops.material]
category = "spirit"
kind = "gem"

View File

@ -0,0 +1,18 @@
name = "Angel Star"
[[world]]
name = "End of the World"
[[drops]]
name = "Ether"
kind = "item"
chance = 2
[[drops]]
name = "Gale"
kind = "material"
chance = 4
[drops.material]
category = "stormy"
kind = "crystal"

View File

@ -0,0 +1,18 @@
name = "Aquatank"
[[world]]
name = "Atlantica"
[[drops]]
name = "Mega-Potion"
kind = "item"
chance = 4
[[drops]]
name = "Thunder Gem"
kind = "material"
chance = 8
[drops.material]
category = "thunder"
kind = "gem"

View File

@ -0,0 +1,24 @@
name = "Bandit"
[[world]]
name = "Agrabah"
[[world]]
name = "Monstro"
[[world]]
name = "End of the World"
[[drops]]
name = "Potion"
kind = "item"
chance = 2
[[drops]]
name = "Blaze Gem"
kind = "material"
chance = 4
[drops.material]
category = "blaze"
kind = "gem"

View File

@ -0,0 +1,20 @@
name = "Barrel Spider"
[[world]]
name = "Monstro"
[[world]]
name = "Neverland"
[[world]]
name = "End of the World"
[[drops]]
name = "Camping Set"
kind = "item"
chance = 1
[[drops]]
name = "Cottage"
kind = "item"
chance = 0.5

View File

@ -0,0 +1,34 @@
name = "Battleship"
[[world]]
name = "Neverland"
[[drops]]
name = "Elixir"
kind = "item"
chance = 0.5
[[drops]]
name = "Elixir"
kind = "item"
chance = 1
info = "Destroy the Stern, Cannons or Mast"
[[drops]]
name = "Power Gem"
kind = "material"
chance = 4
[drops.material]
category = "power"
kind = "gem"
[[drops]]
name = "Power Gem"
kind = "material"
chance = 8
info = "Destroy the Stern, Cannons or Mast"
[drops.material]
category = "power"
kind = "gem"

View File

@ -0,0 +1,30 @@
name = "Blue Rhapsody"
[[world]]
name = "Traverse Town"
[[world]]
name = "Wonderland"
[[world]]
name = "Monstro"
[[world]]
name = "Hollow Bastion"
[[world]]
name = "End of the World"
[[drops]]
name = "Ether"
kind = "item"
chance = 1
[[drops]]
name = "Frost Shard"
kind = "material"
chance = 12
[drops.material]
category = "frost"
kind = "shard"

View File

@ -0,0 +1,43 @@
name = "Bouncywild"
[[world]]
name = "Deep Jungle"
[[world]]
name = "End of the World"
[[drops]]
name = "Hi-Potion"
kind = "item"
chance = 2
[[drops]]
name = "Power Shard"
kind = "material"
chance = 8
[drops.material]
category = "power"
kind = "shard"
[[drops]]
name = "Ether"
kind = "item"
chance = 20
info = "When it slips on the banana peel"
[[drops]]
name = "Mega-Ether"
kind = "item"
chance = 4
info = "When it slips on the banana peel"
[[drops]]
name = "Power Shard"
kind = "material"
chance = 8
info = "When it slips on the banana peel"
[drops.material]
category = "power"
kind = "shard"

View File

@ -0,0 +1,24 @@
name = "Darkball"
[[world]]
name = "Traverse Town"
[[world]]
name = "Hollow Bastion"
[[world]]
name = "End of the World"
[[drops]]
name = "Hi-Potion"
kind = "item"
chance = 1
[[drops]]
name = "Lucid Crystal"
kind = "material"
chance = 1
[drops.material]
category = "lucid"
kind = "crystal"

View File

@ -0,0 +1,29 @@
name = "Defender"
[[world]]
name = "Traverse Town"
[[world]]
name = "Hollow Bastion"
[[world]]
name = "End of the World"
[[drops]]
name = "Elixir"
kind = "item"
chance = 1
[[drops]]
name = "Bright Crystal"
kind = "material"
chance = 2
[drops.material]
category = "bright"
kind = "crystal"
[[drops]]
name = "Defender"
kind = "equipment"
chance = 0.2

View File

@ -0,0 +1,24 @@
name = "Fat Bandit"
[[world]]
name = "Agrabah"
[[world]]
name = "Monstro"
[[world]]
name = "End of the World"
[[drops]]
name = "Hi-Potion"
kind = "item"
chance = 4
[[drops]]
name = "Blaze Gem"
kind = "material"
chance = 8
[drops.material]
category = "blaze"
kind = "gem"

View File

@ -0,0 +1,26 @@
name = "Gargoyle"
[[world]]
name = "Holloween Town"
[[world]]
name = "End of the World"
[[drops]]
name = "Ether"
kind = "item"
chance = 1
[[drops]]
name = "Mega-Ether"
kind = "item"
chance = 0.5
[[drops]]
name = "Lucid Gem"
kind = "material"
chance = 2
[drops.material]
category = "lucid"
kind = "gem"

View File

@ -0,0 +1,33 @@
name = "Green Requiem"
[[world]]
name = "Traverse Town"
[[world]]
name = "Agrabah"
[[world]]
name = "Monstro"
[[world]]
name = "Deep Jungle"
[[world]]
name = "Hollow Bastion"
[[world]]
name = "End of the World"
[[drops]]
name = "Ether"
kind = "item"
chance = 4
[[drops]]
name = "Bright Shard"
kind = "material"
chance = 10
[drops.material]
category = "bright"
kind = "shard"

View File

@ -0,0 +1,18 @@
name = "Invisible"
[[world]]
name = "End of the World"
[[drops]]
name = "Hi-Potion"
kind = "item"
chance = 2
[[drops]]
name = "Gale"
kind = "material"
chance = 4
[drops.material]
category = "stormy"
kind = "crystal"

View File

@ -0,0 +1,34 @@
name = "Large Body"
[[world]]
name = "Traverse Town"
[[world]]
name = "Wonderland"
[[world]]
name = "Agrabah"
marked = true
[[world]]
name = "Monstro"
[[world]]
name = "Hollow Bastion"
[[world]]
name = "End of the World"
[[drops]]
name = "Hi-Potion"
kind = "item"
chance = 4
[[drops]]
name = "Spirit Shard"
kind = "material"
chance = 10
[drops.material]
category = "spirit"
kind = "shard"

View File

@ -0,0 +1,29 @@
name = "Pirate"
[[world]]
name = "Neverland"
[[world]]
name = "Monstro"
[[world]]
name = "End of the World"
[[drops]]
name = "Hi-Potion"
kind = "item"
chance = 2
[[drops]]
name = "Mega-Potion"
kind = "item"
chance = 1
[[drops]]
name = "Power Gem"
kind = "material"
chance = 4
[drops.material]
category = "power"
kind = "gem"

View File

@ -0,0 +1,19 @@
name = "Shadow"
[[world]]
name = "Traverse Town"
room = ""
[[drops]]
name = "Potion"
kind = "item"
chance = 6
[[drops]]
name = "Lucid Shard"
kind = "material"
chance = 3
[drops.material]
category = "lucid"
kind = "shard"

View File

@ -0,0 +1,14 @@
name = "Soldier"
[[world]]
name = "Traverse Town"
room = ""
[[drops]]
name = "Spirit Shard"
kind = "material"
chance = 6
[drops.material]
category = "spirit"
kind = "shard"

View File

@ -1,2 +1,3 @@
pub mod direction; pub mod direction;
pub mod enemy;
pub mod materials; pub mod materials;

View File

@ -0,0 +1,90 @@
use std::{fmt::Display, path::PathBuf};
use serde::Deserialize;
use super::materials::MaterialDetails;
#[derive(Debug, Deserialize, PartialEq, Eq)]
pub struct Enemy {
pub name: String,
pub icon: Option<String>,
pub world: Vec<SpawnLocation>,
pub drops: Vec<EnemyDrop>,
}
impl Enemy {
pub fn import(path: &str) -> Vec<Enemy> {
let mut enemies: Vec<Enemy> = vec![];
// Loading multiple files into one vector due to the size of each board
let paths = std::fs::read_dir(path)
.unwrap()
.filter_map(|f| f.ok())
.map(|f| f.path())
.filter_map(|p| match p.extension().is_some_and(|e| e == "toml") {
true => Some(p),
false => None,
})
.collect::<Vec<PathBuf>>();
for path in paths {
let enemy_str = std::fs::read_to_string(path).unwrap();
let mut enemy = toml::from_str::<Enemy>(&enemy_str).unwrap();
enemy
.drops
.iter_mut()
.for_each(|d| d.from = enemy.name.clone());
enemies.push(enemy);
}
enemies
}
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct EnemyDrop {
pub name: String,
#[serde(skip)]
pub from: String,
pub chance: EnemyDropChance,
pub kind: EnemyDropKind,
pub icon: Option<String>,
pub info: Option<String>,
pub material: Option<MaterialDetails>,
}
impl EnemyDrop {
pub fn texture(&self) -> String {
self.from.replace(" ", "_").to_lowercase()
}
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "lowercase")]
pub enum EnemyDropKind {
Item,
Material,
Equipment,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(untagged)]
pub enum EnemyDropChance {
Fixed(ordered_float::OrderedFloat<f32>),
Variable(String),
}
impl Display for EnemyDropChance {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EnemyDropChance::Fixed(val) => f.write_str(&format!("{val}%")),
EnemyDropChance::Variable(val) => f.write_str(val),
}
}
}
#[derive(Debug, Deserialize, PartialEq, Eq)]
pub struct SpawnLocation {
pub name: String,
pub room: Option<String>,
}

View File

@ -1,80 +1,100 @@
use std::{fmt::Display, path::PathBuf}; use std::{collections::HashMap, fmt::Display};
use serde::Deserialize; use serde::Deserialize;
#[derive(Debug, Deserialize, PartialEq, Eq)] use super::enemy::{Enemy, EnemyDrop};
pub struct EnemyDrop {
pub from: String,
pub chance: EnemyDropChance,
#[serde(default)] #[derive(Debug, Clone, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub info: Option<String>, pub struct MaterialDetails {
pub category: String,
pub kind: MaterialKind,
} }
impl EnemyDrop { #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub fn texture(&self) -> String { #[serde(rename_all = "lowercase")]
self.from.replace(" ", "_").to_lowercase() pub enum MaterialKind {
} Shard,
Stone,
Gem,
Crystal,
} }
#[derive(Debug, Deserialize, PartialEq, Eq)] impl Display for MaterialKind {
#[serde(untagged)]
pub enum EnemyDropChance {
Fixed(u8),
Variable(String),
}
impl Display for EnemyDropChance {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
EnemyDropChance::Fixed(val) => f.write_str(&format!("{val}%")), MaterialKind::Shard => f.write_str("shard"),
EnemyDropChance::Variable(val) => f.write_str(val), MaterialKind::Stone => f.write_str("stone"),
MaterialKind::Gem => f.write_str("gem"),
MaterialKind::Crystal => f.write_str("crystal"),
} }
} }
} }
#[derive(Debug, Default, Deserialize, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
#[serde(default)]
pub struct MaterialDrops { pub struct MaterialDrops {
pub kind: String, pub name: String,
pub shard: Vec<EnemyDrop>, pub icon: String,
pub stone: Vec<EnemyDrop>, pub category: String,
pub gem: Vec<EnemyDrop>, pub kind: MaterialKind,
pub crystal: Vec<EnemyDrop>, pub drops: Vec<EnemyDrop>,
}
impl PartialOrd for MaterialDrops {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
if self.category.cmp(&other.category) == std::cmp::Ordering::Equal {
return Some(self.kind.cmp(&other.kind));
}
Some(self.category.cmp(&other.category))
}
} }
impl MaterialDrops { impl MaterialDrops {
pub fn import(path: &str) -> Vec<MaterialDrops> { pub fn new(enemies: Vec<Enemy>) -> Vec<MaterialDrops> {
let mut drops: Vec<MaterialDrops> = vec![]; let mut mat_map = HashMap::<(String, MaterialKind), MaterialDrops>::new();
// Loading multiple files into one vector due to the size of each board for enemy in enemies {
let paths = std::fs::read_dir(path) for drop in &enemy.drops {
.unwrap() let Some(material) = &drop.material else {
.filter_map(|f| f.ok()) continue;
.map(|f| f.path()) };
.filter_map(|p| match p.extension().is_some_and(|e| e == "toml") {
true => Some(p),
false => None,
})
.collect::<Vec<PathBuf>>();
for path in paths { let key = (material.category.clone(), material.kind.clone());
let drops_str = std::fs::read_to_string(path).unwrap();
let enemy_drops = toml::from_str::<MaterialDrops>(&drops_str).unwrap(); mat_map
drops.push(enemy_drops); .entry(key)
.and_modify(|d| d.drops.push(drop.clone()))
.or_insert(MaterialDrops {
name: drop.name.to_string(),
icon: "".to_string(),
category: material.category.clone(),
kind: material.kind.clone(),
drops: vec![drop.clone()],
});
}
} }
drops.sort_by(|a, b| a.kind.cmp(&b.kind));
drops let mut values: Vec<MaterialDrops> = mat_map.into_values().collect();
values.sort_by(|a, b| a.partial_cmp(b).unwrap());
values
} }
pub fn drops(&self, kind: &str) -> &[EnemyDrop] { pub fn drops(&self, kind: &str) -> Vec<&EnemyDrop> {
match kind { match kind {
"shard" => &self.shard, "shard" => self.get_drop_kind(MaterialKind::Shard),
"stone" => &self.stone, "stone" => self.get_drop_kind(MaterialKind::Stone),
"gem" => &self.gem, "gem" => self.get_drop_kind(MaterialKind::Gem),
"crystal" => &self.crystal, "crystal" => self.get_drop_kind(MaterialKind::Crystal),
_ => &self.shard, _ => vec![],
} }
} }
fn get_drop_kind(&self, kind: MaterialKind) -> Vec<&EnemyDrop> {
self.drops
.iter()
.filter(|d| d.material.as_ref().map(|m| m.kind == kind).unwrap_or(false))
.collect::<Vec<&EnemyDrop>>()
}
} }

View File

@ -3,13 +3,18 @@ use std::sync::OnceLock;
use askama::Template; use askama::Template;
use blake3::Hash; use blake3::Hash;
use crate::{RuntimeModule, common::materials::MaterialDrops, create_file, create_hashes}; use crate::{
RuntimeModule,
common::{enemy::Enemy, materials::MaterialDrops},
create_file, create_hashes,
};
const MATERIAL_KINDS: &[&str] = &[ const MATERIAL_KINDS: &[&str] = &[
"lucid", "spirit", "power", "blaze", "frost", "thunder", "shiny", "bright", "mystery", "gale", "lucid", "spirit", "power", "blaze", "frost", "thunder", "shiny", "bright", "mystery", "gale",
"mythril", "mythril",
]; ];
const DROPS_PATH: &str = "./input/kh1/drops"; const DROPS_PATH: &str = "./input/kh1/drops";
const ENEMIES_PATH: &str = "./input/kh1/enemies";
static JS_HASH: OnceLock<Hash> = OnceLock::new(); static JS_HASH: OnceLock<Hash> = OnceLock::new();
#[derive(Template)] #[derive(Template)]
@ -22,8 +27,11 @@ pub struct Module;
impl RuntimeModule for Module { impl RuntimeModule for Module {
fn start_module() { fn start_module() {
tracing::info!("Loading enemy data from {}", ENEMIES_PATH);
let enemies = Enemy::import(ENEMIES_PATH);
tracing::info!("Loading enemy drops data from {}", DROPS_PATH); tracing::info!("Loading enemy drops data from {}", DROPS_PATH);
let drops = MaterialDrops::import(DROPS_PATH); let drops = MaterialDrops::new(enemies);
tracing::info!("Generating the KH1 drops template"); tracing::info!("Generating the KH1 drops template");
let drops_template = DropsTemplate { drops }; let drops_template = DropsTemplate { drops };

View File

@ -33,12 +33,12 @@ pub struct Module;
impl RuntimeModule for Module { impl RuntimeModule for Module {
fn start_module() { fn start_module() {
tracing::info!("Loading enemy drops data from {}", DROPS_PATH); tracing::info!("Loading enemy drops data from {}", DROPS_PATH);
let drops = MaterialDrops::import(DROPS_PATH); // let drops = MaterialDrops::import(DROPS_PATH);
tracing::info!("Generating the KH2 drops template"); tracing::info!("Generating the KH2 drops template");
let drops_template = DropsTemplate { drops }; // let drops_template = DropsTemplate { drops };
create_file("./out/kh2", "drops", drops_template.render().unwrap()).unwrap(); // create_file("./out/kh2", "drops", drops_template.render().unwrap()).unwrap();
} }
fn get_js_hash() -> String { fn get_js_hash() -> String {

View File

@ -1,18 +1,21 @@
{% macro drop(label) %} {% macro drop(label) %}
{% let drops = category.drops(label) %} {% let drops = drop.drops(label) %}
{% if drops.len() > 0 %} {% if drops.len() > 0 %}
<div <div
class="category-wrapper" class="category-wrapper"
data-mat-kind="{{ category.kind }}" data-mat-kind="{{ drop.category }}"
data-mat-type="{{ label }}" data-mat-type="{{ label }}"
> >
<div class="category"> <div class="category">
<img <img
src="../public/assets/materials/{{ category.kind }}/{{ label }}.webp" src="../public/assets/materials/{{ drop.category }}/{{ label }}.webp"
width="64" width="64"
height="64" height="64"
/> />
<h1>{{ category.kind|capitalize +}} {{+ label|capitalize }}</h1> <!-- <h1> -->
<!-- {{ drop.category|capitalize +}} {{+ label|capitalize }} -->
<!-- </h1> -->
<h1>{{ drop.name }}</h1>
<button onclick="track(this)">Start tracking</button> <button onclick="track(this)">Start tracking</button>
</div> </div>
<div class="enemies"> <div class="enemies">

View File

@ -14,7 +14,7 @@
{% include "components/common/kind-filters.html" %} {% include "components/common/kind-filters.html" %}
<br /> <br />
{% for category in drops %} {% for drop in drops %}
{% call macros::drop("shard") %} {% call macros::drop("shard") %}
{% call macros::drop("stone") %} {% call macros::drop("stone") %}
{% call macros::drop("gem") %} {% call macros::drop("gem") %}

View File

@ -14,7 +14,7 @@
{% include "components/common/kind-filters.html" %} {% include "components/common/kind-filters.html" %}
<br /> <br />
{% for category in drops %} {% for drop in drops %}
{% call macros::drop("shard") %} {% call macros::drop("shard") %}
{% call macros::drop("stone") %} {% call macros::drop("stone") %}
{% call macros::drop("gem") %} {% call macros::drop("gem") %}