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
91952b1745
157 changed files with 92600 additions and 0 deletions
60
src/ui/Board.tsx
Normal file
60
src/ui/Board.tsx
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/**
|
||||
* A character's console grid.
|
||||
*
|
||||
* Blocked cells are drawn as holes, free cells as the piece covering them, and
|
||||
* borders only where a piece meets something else - so a build reads as six to
|
||||
* eight objects rather than twenty squares.
|
||||
*/
|
||||
import { pieceColour, renderBoard } from "../domain/render.ts";
|
||||
|
||||
export interface BoardProps {
|
||||
cells: readonly number[];
|
||||
placement: readonly number[];
|
||||
pieces: number;
|
||||
/** Called with the piece index under the pointer, or null. */
|
||||
onHover?: (piece: number | null) => void;
|
||||
labelFor?: (piece: number) => string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export function Board({
|
||||
cells,
|
||||
placement,
|
||||
pieces,
|
||||
onHover,
|
||||
labelFor,
|
||||
size = 34,
|
||||
}: BoardProps) {
|
||||
const render = renderBoard(cells, placement);
|
||||
const free = new Set(cells);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="board"
|
||||
style={{ gridTemplateColumns: `repeat(${render.cols}, ${size}px)` }}
|
||||
onPointerLeave={() => onHover?.(null)}
|
||||
>
|
||||
{Array.from({ length: render.rows * render.cols }, (_, index) => {
|
||||
if (!free.has(index)) {
|
||||
return <div key={index} className="board-cell board-cell--blocked" />;
|
||||
}
|
||||
const cell = render.cells.find((entry) => entry.cell === index)!;
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="board-cell"
|
||||
title={labelFor?.(cell.piece)}
|
||||
onPointerEnter={() => onHover?.(cell.piece)}
|
||||
style={{
|
||||
background: pieceColour(cell.piece, pieces),
|
||||
borderTopWidth: cell.top ? 2 : 0,
|
||||
borderRightWidth: cell.right ? 2 : 0,
|
||||
borderBottomWidth: cell.bottom ? 2 : 0,
|
||||
borderLeftWidth: cell.left ? 2 : 0,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue