feat: NTE gear optimizer web app

Client-side optimizer for Neverness to Everness console builds. Imports a
gear export from nte-history-exporter, keeps it in IndexedDB, and solves for
the best module placement per character or across a team.

- domain/: pure ports of the Python research scripts (shapes, cartridges,
  board tiling, stats, scoring, set bonuses, Arc effects, sheet prediction).
  No DOM, no storage imports, so they stay worker-safe and Node-testable.
- solver/: single-character solve with branch and bound, plus a three-phase
  team solve (portfolio, leximin, column generation) over disjoint item sets.
  Runs in a Worker when one can be constructed, inline otherwise.
- db/: three storage tiers selected by use rather than feature detection,
  fail-closed import validation, and undo that restores both sides of a steal.
- ui/: React 19 views for items, characters and teams; the board is drawn as
  the deliverable rather than a score.
- Two build targets: a hosted bundle and a single self-contained nte.html
  with no external requests.

Values the model cannot measure are reported as unavailable rather than
guessed: cartridge set bonuses, the level 80 base-stat multiplier, and the
distinction between "optimal for this packing" and globally optimal.

145 tests, tsc clean, both build targets clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
goober 2026-08-21 11:38:17 +03:00
commit 5ce255fa6f
157 changed files with 92600 additions and 0 deletions

87
src/state/gamedata.ts Normal file
View file

@ -0,0 +1,87 @@
/**
* The generated game data.
*
* Loaded through a dynamic import so Vite emits it as a content-hashed chunk -
* immutable-cacheable, and off the first-paint path. It stays a plain module
* rather than a `fetch` precisely so the single-file build can inline it;
* `fetch` could not be.
*/
import type { RawTilings } from "../domain/tilings.ts";
import type { SetBonusTable } from "../domain/setbonus.ts";
import type { GuideTable } from "../domain/guides.ts";
export interface Esper {
id: number;
abilityKey: string;
name: string;
element: string | null;
rarity: number;
icon: string;
stats: Array<{ id_stats: string; name: string; bShowPercent: boolean; values: number[] }>;
slots: number[][];
ownerGridCount: number;
trait: Array<{ id_stats: string; name: string; bShowPercent: boolean; value: number }>;
breakthroughLevels: number[];
}
export interface Arc {
id: string;
name: string;
icon: string;
quality: number;
desc: string;
stats: unknown;
values: unknown;
}
export type ArcEffectMode = "always" | "stacks" | "toggle" | "duration" | "unmodellable";
export interface ArcEffect {
placeholder: number;
stat: string | null;
percent?: boolean;
mode: ArcEffectMode;
default?: boolean;
why: string;
}
export interface GameData {
format: string;
format_version: number;
generated: string;
espers: Esper[];
arcs: Arc[];
arcEffects: Record<string, { name: string; effects: ArcEffect[] }>;
}
export interface LoadedData {
gamedata: GameData;
tilings: RawTilings;
setBonuses: SetBonusTable;
guides: GuideTable;
}
let cached: Promise<LoadedData> | null = null;
export function loadGameData(): Promise<LoadedData> {
cached ??= (async () => {
const [gamedata, tilings, setBonuses, guides] = await Promise.all([
import("../generated/gamedata.json"),
import("../generated/tilings.json"),
import("../../data-src/set-bonuses.json"),
import("../../data-src/guides.json"),
]);
return {
gamedata: (gamedata.default ?? gamedata) as unknown as GameData,
tilings: (tilings.default ?? tilings) as unknown as RawTilings,
setBonuses: (setBonuses.default ?? setBonuses) as unknown as SetBonusTable,
guides: (guides.default ?? guides) as unknown as GuideTable,
};
})();
return cached;
}
/** The esper whose ability key a capture used, or null for a codename with no record. */
export function esperFor(data: GameData, characterId: string): Esper | null {
return data.espers.find((esper) => esper.abilityKey === characterId) ?? null;
}