From 484a28911249c0bf036638dee663d313db5d0636 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 13 Jun 2026 19:16:38 -0400 Subject: [PATCH] ugh --- scripts/import-newserv-bestiary-tables.py | 87 + site/bestiary-tables.js | 367 + site/bestiary.html | 53 +- site/generated/bestiary/bb/bb.json | 77186 ++++++++++ site/generated/bestiary/bb/index.json | 12 + site/style.css | 43 + source-bestiary/.gitignore | 2 + .../system/tables/battle-params.json | 115358 +++++++++++++++ ...u-hardcore-candidates-20260613T220132Z.txt | 38 + ...al-newserv-candidates-20260613T220132Z.txt | 22 + .../system/tables/battle-params.json | 115358 +++++++++++++++ 11 files changed, 308516 insertions(+), 10 deletions(-) create mode 100755 scripts/import-newserv-bestiary-tables.py create mode 100644 site/bestiary-tables.js create mode 100644 site/generated/bestiary/bb/bb.json create mode 100644 site/generated/bestiary/bb/index.json create mode 100644 source-bestiary/.gitignore create mode 100644 source-bestiary/eu-hardcore/opt/newserv-hardcore/system/tables/battle-params.json create mode 100644 source-bestiary/inventory/eu-hardcore-candidates-20260613T220132Z.txt create mode 100644 source-bestiary/inventory/local-newserv-candidates-20260613T220132Z.txt create mode 100644 source-bestiary/local-newserv/system/tables/battle-params.json diff --git a/scripts/import-newserv-bestiary-tables.py b/scripts/import-newserv-bestiary-tables.py new file mode 100755 index 0000000..9d890b1 --- /dev/null +++ b/scripts/import-newserv-bestiary-tables.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +import json +from pathlib import Path + +SOURCE = Path("source-bestiary/local-newserv/system/tables/battle-params.json") +OUT = Path("site/generated/bestiary/bb") + +DIFFICULTY_ORDER = { + "Normal": 0, + "Hard": 1, + "Very Hard": 2, + "Ultimate": 3, +} + +def label_episode(table_name): + ep, mode = table_name.split("-", 1) + return ep.replace("Episode", "Episode "), mode + +def enemy_label(value): + return str(value).replace("_", " ").title() + +def main(): + data = json.loads(SOURCE.read_text()) + OUT.mkdir(parents=True, exist_ok=True) + + rows = [] + + for table_name, difficulties in data.items(): + episode, mode = label_episode(table_name) + + for difficulty, entries in difficulties.items(): + for entry in entries: + stats = entry.get("Stats", {}) + resist = entry.get("ResistData", {}) + enemies = stats.get("Enemies") or [] + + for enemy in enemies: + rows.append({ + "version": "bb", + "episode": episode, + "mode": mode, + "difficulty": difficulty, + "enemy": enemy_label(enemy), + "enemy_key": enemy, + "bp_index": entry.get("BPIndex"), + "hp": stats.get("HP", 0), + "atp": stats.get("ATP", 0), + "dfp": stats.get("DFP", 0), + "mst": stats.get("MST", 0), + "ata": stats.get("ATA", 0), + "evp": stats.get("EVP", 0), + "lck": stats.get("LCK", 0), + "esp": stats.get("ESP", 0), + "exp": stats.get("EXP", 0), + "meseta": stats.get("Meseta", 0), + "efr": resist.get("EFR", 0), + "eic": resist.get("EIC", 0), + "eth": resist.get("ETH", 0), + "elt": resist.get("ELT", 0), + "edk": resist.get("EDK", 0), + }) + + rows.sort(key=lambda row: ( + row["episode"], + row["mode"], + DIFFICULTY_ORDER.get(row["difficulty"], 999), + row["enemy"], + row["bp_index"] if row["bp_index"] is not None else 9999, + )) + + (OUT / "bb.json").write_text(json.dumps(rows, indent=2, sort_keys=True) + "\n") + (OUT / "index.json").write_text(json.dumps({ + "mode": "bestiary", + "label": "BB", + "tables": [{ + "version": "bb", + "label": "BB", + "path": "bb.json", + "rows": len(rows), + }], + }, indent=2, sort_keys=True) + "\n") + + print(f"bb: {len(rows)} rows -> {OUT / 'bb.json'}") + print(f"index -> {OUT / 'index.json'}") + +if __name__ == "__main__": + main() diff --git a/site/bestiary-tables.js b/site/bestiary-tables.js new file mode 100644 index 0000000..64dd920 --- /dev/null +++ b/site/bestiary-tables.js @@ -0,0 +1,367 @@ +(() => { + "use strict"; + + const qs = (sel) => document.querySelector(sel); + + const DIFFICULTY_ORDER = { + Normal: 0, + Hard: 1, + "Very Hard": 2, + Ultimate: 3, + }; + + const NUMERIC_COLUMNS = new Set([ + "hp", "atp", "dfp", "mst", "ata", "evp", "lck", "esp", "exp", + "efr", "eic", "eth", "elt", "edk", + ]); + + const BP_TIERS = { + 0: { hp: 1.00, atp: 1.00, exp: 1.00 }, + 1: { hp: 1.10, atp: 1.01, exp: 1.10 }, + 2: { hp: 1.15, atp: 1.02, exp: 1.15 }, + 3: { hp: 1.20, atp: 1.03, exp: 1.20 }, + 4: { hp: 1.30, atp: 1.04, exp: 1.30 }, + 5: { hp: 1.40, atp: 1.05, exp: 1.40 }, + 6: { hp: 1.50, atp: 1.06, exp: 1.50 }, + 7: { hp: 1.75, atp: 1.07, exp: 1.75 }, + 8: { hp: 2.00, atp: 1.08, exp: 2.00 }, + 9: { hp: 2.50, atp: 1.09, exp: 2.50 }, + 10: { hp: 3.00, atp: 1.10, exp: 3.00 }, + 11: { hp: 4.00, atp: 1.10, exp: null }, + }; + + const COLUMN_GROUPS = { + stats: [ + ["enemy", "Enemy"], + ["mode", "Mode"], + ["episode", "Episode"], + ["difficulty", "Difficulty"], + ["hp", "HP"], + ["atp", "ATP"], + ["dfp", "DFP"], + ["ata", "ATA"], + ["evp", "EVP"], + ["lck", "LCK"], + ["exp", "EXP"], + ], + resists: [ + ["enemy", "Enemy"], + ["mode", "Mode"], + ["episode", "Episode"], + ["difficulty", "Difficulty"], + ["efr", "EFR"], + ["eic", "EIC"], + ["eth", "ETH"], + ["elt", "ELT"], + ["edk", "EDK"], + ["esp", "ESP"], + ], + all: [ + ["enemy", "Enemy"], + ["mode", "Mode"], + ["episode", "Episode"], + ["difficulty", "Difficulty"], + ["hp", "HP"], + ["atp", "ATP"], + ["dfp", "DFP"], + ["mst", "MST"], + ["ata", "ATA"], + ["evp", "EVP"], + ["lck", "LCK"], + ["esp", "ESP"], + ["exp", "EXP"], + ["efr", "EFR"], + ["eic", "EIC"], + ["eth", "ETH"], + ["elt", "ELT"], + ["edk", "EDK"], + ], + }; + + function currentColumns() { + const view = qs("#bestiary-view")?.value || "stats"; + return COLUMN_GROUPS[view] || COLUMN_GROUPS.stats; + } + + const state = { + rows: [], + index: null, + table: null, + page: 1, + pageSize: 100, + sort: { + key: "enemy", + dir: "asc", + }, + filters: { + mode: "", + episode: "", + difficulty: "", + search: "", + }, + }; + + function esc(value) { + return String(value ?? "") + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); + } + + async function fetchJson(path) { + const res = await fetch(path, { cache: "no-store" }); + if (!res.ok) throw new Error(`${path}: HTTP ${res.status}`); + return res.json(); + } + + function setStatus(message, kind = "") { + const box = qs("#bestiary-placeholder"); + if (!box) return; + box.innerHTML = `
${esc(message)}
`; + } + + function uniqueSorted(rows, key) { + return [...new Set(rows.map((row) => row[key]).filter(Boolean))] + .sort((a, b) => String(a).localeCompare(String(b), undefined, { numeric: true })); + } + + function fillSelect(select, values, allLabel) { + if (!select) return; + const previous = select.value; + select.innerHTML = ``; + for (const value of values) { + const opt = document.createElement("option"); + opt.value = value; + opt.textContent = value; + select.appendChild(opt); + } + if ([...select.options].some((opt) => opt.value === previous)) { + select.value = previous; + } + } + + function orderedDifficulties(rows) { + const present = new Set(rows.map((row) => row.difficulty).filter(Boolean)); + return Object.keys(DIFFICULTY_ORDER).filter((value) => present.has(value)); + } + + function bpTier() { + const tier = Number(qs("#bestiary-bp-tier")?.value || 0); + return BP_TIERS[tier] || BP_TIERS[0]; + } + + function displayValue(row, key) { + const value = row[key]; + + if (row.difficulty !== "Ultimate") { + return value ?? ""; + } + + const tier = bpTier(); + + if (key === "hp") { + return Math.round(Number(value || 0) * tier.hp); + } + + if (key === "atp") { + return Math.round(Number(value || 0) * tier.atp); + } + + if (key === "exp" && tier.exp !== null) { + return Math.round(Number(value || 0) * tier.exp); + } + + return value ?? ""; + } + + function visibleRows() { + const search = state.filters.search.trim().toLowerCase(); + + return state.rows.filter((row) => { + if (state.filters.mode && row.mode !== state.filters.mode) return false; + if (state.filters.episode && row.episode !== state.filters.episode) return false; + if (state.filters.difficulty && row.difficulty !== state.filters.difficulty) return false; + + if (search) { + const haystack = [row.enemy, row.enemy_key, row.mode, row.episode, row.difficulty] + .join(" ") + .toLowerCase(); + if (!haystack.includes(search)) return false; + } + + return true; + }); + } + + function sortValue(row, key) { + if (key === "difficulty") return DIFFICULTY_ORDER[row.difficulty] ?? 999; + if (NUMERIC_COLUMNS.has(key)) return Number(displayValue(row, key) || 0); + return row[key] || ""; + } + + function sortedRows(rows) { + const { key, dir } = state.sort; + const factor = dir === "desc" ? -1 : 1; + + return [...rows].sort((a, b) => { + const av = sortValue(a, key); + const bv = sortValue(b, key); + + if (typeof av === "number" && typeof bv === "number") { + return (av - bv) * factor; + } + + return String(av).localeCompare(String(bv), undefined, { + numeric: true, + sensitivity: "base", + }) * factor; + }); + } + + function sortHeader(key, label) { + const active = state.sort.key === key; + const arrow = active ? (state.sort.dir === "asc" ? "▲" : "▼") : ""; + const ariaSort = active ? (state.sort.dir === "asc" ? "ascending" : "descending") : "none"; + + return ` + + `; + } + + function renderTable() { + const box = qs("#bestiary-placeholder"); + if (!box) return; + + const rows = sortedRows(visibleRows()); + const totalPages = Math.max(1, Math.ceil(rows.length / state.pageSize)); + state.page = Math.min(Math.max(1, state.page), totalPages); + + const start = (state.page - 1) * state.pageSize; + const shown = rows.slice(start, start + state.pageSize); + const end = start + shown.length; + + const columns = currentColumns(); + if (!columns.some(([key]) => key === state.sort.key)) { + state.sort.key = "enemy"; + state.sort.dir = "asc"; + } + + const body = shown.map((row) => ` + + ${columns.map(([key]) => `${esc(displayValue(row, key))}`).join("")} + + `).join(""); + + const rangeText = rows.length + ? ` Showing ${Number(start + 1).toLocaleString()}-${Number(end).toLocaleString()}.` + : ""; + + box.innerHTML = ` +
+
+ BB Bestiary + ${rows.length.toLocaleString()} matching rows.${rangeText} +
+ ${state.rows.length.toLocaleString()} total rows +
+
+ + + ${columns.map(([key, label]) => sortHeader(key, label)).join("")} + + ${body || ``} +
No enemies match these filters.
+
+
+ + Page ${state.page} of ${totalPages} + +
+ `; + } + + function populateFilters() { + fillSelect(qs("#bestiary-mode"), uniqueSorted(state.rows, "mode"), "All modes"); + fillSelect(qs("#bestiary-episode"), uniqueSorted(state.rows, "episode"), "All episodes"); + fillSelect(qs("#bestiary-difficulty"), orderedDifficulties(state.rows), "All difficulties"); + } + + async function loadBestiary() { + setStatus("Loading BB bestiary..."); + + state.index = await fetchJson("generated/bestiary/bb/index.json"); + state.table = state.index.tables?.[0] || null; + state.rows = await fetchJson(`generated/bestiary/bb/${state.table.path}`); + + populateFilters(); + renderTable(); + } + + document.addEventListener("DOMContentLoaded", () => { + qs("#bestiary-mode")?.addEventListener("change", (event) => { + state.filters.mode = event.target.value; + state.page = 1; + renderTable(); + }); + + qs("#bestiary-episode")?.addEventListener("change", (event) => { + state.filters.episode = event.target.value; + state.page = 1; + renderTable(); + }); + + qs("#bestiary-difficulty")?.addEventListener("change", (event) => { + state.filters.difficulty = event.target.value; + state.page = 1; + renderTable(); + }); + + qs("#bestiary-view")?.addEventListener("change", () => { + state.page = 1; + renderTable(); + }); + + qs("#bestiary-search")?.addEventListener("input", (event) => { + state.filters.search = event.target.value; + state.page = 1; + renderTable(); + }); + + qs("#bestiary-bp-tier")?.addEventListener("change", () => { + state.page = 1; + renderTable(); + }); + + qs("#bestiary-placeholder")?.addEventListener("click", (event) => { + const pageButton = event.target.closest("[data-bestiary-page]"); + if (pageButton) { + state.page += pageButton.dataset.bestiaryPage === "next" ? 1 : -1; + renderTable(); + return; + } + + const button = event.target.closest("[data-bestiary-sort]"); + if (!button) return; + + const key = button.dataset.bestiarySort; + if (state.sort.key === key) { + state.sort.dir = state.sort.dir === "asc" ? "desc" : "asc"; + } else { + state.sort.key = key; + state.sort.dir = "asc"; + } + + state.page = 1; + renderTable(); + }); + + loadBestiary().catch((err) => { + setStatus(err?.message || "Unable to load bestiary.", "error"); + }); + }); +})(); diff --git a/site/bestiary.html b/site/bestiary.html index dd5a9bb..60ec441 100644 --- a/site/bestiary.html +++ b/site/bestiary.html @@ -8,7 +8,7 @@ - + @@ -33,18 +33,51 @@
- - + + + + + + + + + +

Applies HP, ATP, and EXP modifiers to Ultimate rows only.

+ + + + + + +