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:
commit
5ce255fa6f
157 changed files with 92600 additions and 0 deletions
87
src/state/gamedata.ts
Normal file
87
src/state/gamedata.ts
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue