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>
43 lines
1.4 KiB
TypeScript
43 lines
1.4 KiB
TypeScript
/**
|
|
* Hash routing.
|
|
*
|
|
* About forty lines instead of a router dependency, and it is what makes the
|
|
* single-file `file://` build work without a second code path - a history
|
|
* router would need a server rewrite that a local file has no way to provide.
|
|
*/
|
|
import { useEffect, useState } from "react";
|
|
|
|
export const TABS = ["cartridges", "modules", "characters", "team"] as const;
|
|
export type Tab = (typeof TABS)[number];
|
|
|
|
export interface Route {
|
|
tab: Tab;
|
|
/** The character a detail view is open on, if any. */
|
|
id: string | null;
|
|
}
|
|
|
|
export function parseHash(hash: string): Route {
|
|
const parts = hash.replace(/^#\/?/, "").split("/").filter(Boolean);
|
|
const tab = (TABS as readonly string[]).includes(parts[0] ?? "")
|
|
? (parts[0] as Tab)
|
|
: "characters";
|
|
return { tab, id: parts[1] ? decodeURIComponent(parts[1]) : null };
|
|
}
|
|
|
|
export function hashFor(tab: Tab, id?: string | null): string {
|
|
return id ? `#/${tab}/${encodeURIComponent(id)}` : `#/${tab}`;
|
|
}
|
|
|
|
export function navigate(tab: Tab, id?: string | null): void {
|
|
window.location.hash = hashFor(tab, id);
|
|
}
|
|
|
|
export function useRoute(): Route {
|
|
const [route, setRoute] = useState<Route>(() => parseHash(window.location.hash));
|
|
useEffect(() => {
|
|
const onChange = () => setRoute(parseHash(window.location.hash));
|
|
window.addEventListener("hashchange", onChange);
|
|
return () => window.removeEventListener("hashchange", onChange);
|
|
}, []);
|
|
return route;
|
|
}
|