ui/phase-14: auto-sync order draft + always GET on boot + header headline
Replaces the manual Submit button with an auto-sync pipeline driven by `OrderDraftStore`: every successful add / remove / move coalesces a `submitOrder` call so the engine always mirrors the local draft. Removing the last command sends an empty cmd[] PUT — the engine, repo, and rest model now accept that as a valid "player cleared their draft" state. `hydrateFromServer` is now invoked unconditionally on game boot so a fresh device picks up the player's stored order, and the local cache is overwritten by the server's view (server is the source of truth). Header replaces the static "race ?" + turn counter with a single headline string `<race> @ <game>, turn <n>`, sourced from the engine's Report.race + the lobby's GameSummary.gameName + the live turn number, with a `?` fallback while any piece is loading. Tests: - engine: empty PUT round-trips, repo round-trips empty Commands - order-draft: auto-sync sends full draft on every mutation, rejected response surfaces error sync status, rapid mutations coalesce, server hydration overwrites cache - order-tab: per-row status flips through the auto-sync lifecycle, remove → empty cmd[] PUT, rejected → retry button - inspector overlay: applied + valid + submitting all participate in the optimistic projection - header: live race / game / turn rendering with fall-back Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -67,6 +67,12 @@ export interface GameReport {
|
||||
mapHeight: number;
|
||||
planetCount: number;
|
||||
planets: ReportPlanet[];
|
||||
/**
|
||||
* race is the calling player's race name as resolved by the
|
||||
* engine from the runtime player mapping. Empty when the engine
|
||||
* has not produced a report yet (boot state).
|
||||
*/
|
||||
race: string;
|
||||
}
|
||||
|
||||
export async function fetchGameReport(
|
||||
@@ -189,6 +195,7 @@ function decodeReport(report: Report): GameReport {
|
||||
mapHeight: report.height(),
|
||||
planetCount: report.planetCount(),
|
||||
planets,
|
||||
race: report.race() ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -212,18 +219,20 @@ export function uuidToHiLo(value: string): [bigint, bigint] {
|
||||
}
|
||||
|
||||
/**
|
||||
* applyOrderOverlay returns a copy of `report` with every applied or
|
||||
* still-in-flight (`submitting`) command from `commands` projected on
|
||||
* top. Phase 14 understands `planetRename` only — every other variant
|
||||
* passes through. The function is pure: callers re-derive the
|
||||
* overlay whenever the draft or the report change.
|
||||
* applyOrderOverlay returns a copy of `report` with every locally-
|
||||
* valid or still-in-flight or applied command from `commands`
|
||||
* projected on top. Phase 14 understands `planetRename` only —
|
||||
* every other variant passes through. The function is pure:
|
||||
* callers re-derive the overlay whenever the draft or the report
|
||||
* change.
|
||||
*
|
||||
* `statuses` maps command id → status. Entries with `applied` or
|
||||
* `submitting` participate in the overlay; everything else (`draft`,
|
||||
* `valid`, `invalid`, `rejected`) is treated as "not yet committed
|
||||
* by the player" and skipped. This matches the order-composer model:
|
||||
* the player sees their own committed intent, not their unfinished
|
||||
* edits.
|
||||
* `statuses` maps command id → status. Entries with `valid`,
|
||||
* `submitting`, or `applied` participate in the overlay — together
|
||||
* they describe "the player's committed intent for this turn":
|
||||
* locally-valid (auto-sync about to fire), in-flight on the wire,
|
||||
* or acknowledged by the engine. Entries with `draft`, `invalid`,
|
||||
* or `rejected` skip the overlay so the player keeps the server's
|
||||
* (un-renamed) view.
|
||||
*/
|
||||
export function applyOrderOverlay(
|
||||
report: GameReport,
|
||||
@@ -234,7 +243,13 @@ export function applyOrderOverlay(
|
||||
let mutatedPlanets: ReportPlanet[] | null = null;
|
||||
for (const cmd of commands) {
|
||||
const status = statuses[cmd.id];
|
||||
if (status !== "applied" && status !== "submitting") continue;
|
||||
if (
|
||||
status !== "valid" &&
|
||||
status !== "submitting" &&
|
||||
status !== "applied"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (cmd.kind !== "planetRename") continue;
|
||||
const idx = report.planets.findIndex((p) => p.number === cmd.planetNumber);
|
||||
if (idx < 0) continue;
|
||||
|
||||
Reference in New Issue
Block a user