fix: reachable undo, nameable groups, and no favicon request
Found by running the app in a browser for the first time. - Undo was only reachable from the team tab, and only after a team solve had finished, so an equip made from the character view had no way back. Both views now show it whenever there is something to undo. - The owner-group naming dropdown listed all 23 espers from the game database rather than the characters the capture says you own. Two espers are both published as "Zero", so the list held two identical entries and picking the wrong one misattributed a whole loadout; conversely a character the game database has never heard of had no entry at all and its group could not be named. Options now come from the captured roster, labelled with the esper name and disambiguated by codename when two owned characters share one. - An inline SVG favicon, so the page stops requesting one that does not exist. Inline rather than a file because the single-file build must make no requests at all. Verified headless against the real 819-item export: import writes 819 items, 20 characters and 99 equipment rows to IndexedDB; a solve runs in a real Worker and scores 0.6690 for Zankou, matching the Node bench; equip writes 8 app rows and displaces 7; undo restores all 99 imported rows; state survives a reload. The file:// build renders, imports, solves inline and issues no network request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
91952b1745
commit
f6466881f4
4 changed files with 37 additions and 4 deletions
|
|
@ -3,6 +3,7 @@
|
|||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='6' fill='%230d1117'/><rect x='6' y='6' width='9' height='9' fill='%23e5534b'/><rect x='17' y='6' width='9' height='9' fill='%23d9b125'/><rect x='6' y='17' width='9' height='9' fill='%234c8ed9'/><rect x='17' y='17' width='9' height='9' fill='%232ea44f'/></svg>" />
|
||||
<title>NTE gear optimizer</title>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -351,6 +351,9 @@ export function CharacterView({
|
|||
Equip this build
|
||||
</button>
|
||||
)}
|
||||
{state.data.undo.length > 0 && (
|
||||
<button onClick={() => void store.undo()}>Undo last equip</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{running && progress && (
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { esperFor, type LoadedData } from "../state/gamedata.ts";
|
|||
import { CharacterView } from "./CharacterView.tsx";
|
||||
import { Icon } from "./Icon.tsx";
|
||||
import { equipmentOf } from "../db/store.ts";
|
||||
import type { CharacterRow } from "../db/schema.ts";
|
||||
import { hashFor, type Route } from "./router.ts";
|
||||
import { useAppState, useStore } from "./useStore.ts";
|
||||
|
||||
|
|
@ -60,6 +61,31 @@ export function CharactersTab({
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* What to call a character in the naming dropdown.
|
||||
*
|
||||
* Two espers are both published as "Zero", so a bare display name can send a
|
||||
* whole loadout to the wrong character; where a name is shared the codename
|
||||
* disambiguates it. Seven of the captured roster have no everness record at
|
||||
* all, so their codename is all there is - which is also why the options come
|
||||
* from the capture rather than from the esper list: a character the game says
|
||||
* you own must be nameable even when the game database has never heard of it.
|
||||
*/
|
||||
function ownerLabel(
|
||||
data: LoadedData,
|
||||
characters: CharacterRow[],
|
||||
characterId: string,
|
||||
): string {
|
||||
const esper = esperFor(data.gamedata, characterId);
|
||||
if (!esper) return characterId;
|
||||
const shared = characters.some(
|
||||
(other) =>
|
||||
other.characterId !== characterId &&
|
||||
esperFor(data.gamedata, other.characterId)?.name === esper.name,
|
||||
);
|
||||
return shared ? `${esper.name} (${characterId})` : esper.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* The owner groups nobody has named.
|
||||
*
|
||||
|
|
@ -97,9 +123,9 @@ function UnidentifiedGroups({ groups, data }: { groups: string[]; data: LoadedDa
|
|||
}}
|
||||
>
|
||||
<option value="">name this group…</option>
|
||||
{data.gamedata.espers.map((esper) => (
|
||||
<option key={esper.abilityKey} value={esper.abilityKey}>
|
||||
{esper.name}
|
||||
{db.characters.map((character) => (
|
||||
<option key={character.characterId} value={character.characterId}>
|
||||
{ownerLabel(data, db.characters, character.characterId)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
|
|
|||
|
|
@ -223,6 +223,7 @@ function Result({
|
|||
data: LoadedData;
|
||||
store: ReturnType<typeof useStore>;
|
||||
}) {
|
||||
const state = useAppState();
|
||||
if (result.infeasible) {
|
||||
return <p className="warning">No conflict-free team could be built from this pool.</p>;
|
||||
}
|
||||
|
|
@ -243,7 +244,9 @@ function Result({
|
|||
</div>
|
||||
<p className="dim">
|
||||
Equipping writes an “app” row you still have to apply in game.{" "}
|
||||
<button onClick={() => void store.undo()}>Undo last equip</button>
|
||||
{state.data.undo.length > 0 && (
|
||||
<button onClick={() => void store.undo()}>Undo last equip</button>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue