e2aba856b59719a443092afe7267292b767960be
35 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e2aba856b5 |
ui/phase-27: viewer layout pass + static cluster + duel layout
Layout reshuffle so the scene captures the maximum viewer area: - Header collapses three rows into one: `back to map` / `back to report` on the left, the centred title `Battle on planet <name> (#<number>)` (new i18n key `game.battle.header_title`), and the frame counter on the right. The wrapper `.active-view` no longer renders its own back-row; routes flow through props. - Viewer drops the `max-width: 880px` cap so on a wide monitor the scene scales up across the full active-view-host. - A drag-seek `<input type="range">` sits between the scene and the controls; dragging pauses playback and lands `frameIndex` on the chosen shot. - Speed control is one cycling button: `1x → 2x → 4x → 6x → 1x`. The label shows the current speed; the new 6x adds a 67 ms frame interval for skimming a long timeline. - The text protocol log is now collapsible behind a `Log ▲▼` toggle in the controls bar. The toggle is its own button; the default state stays expanded. Collapsing the log hands the remaining height to the scene. - Numerical list markers (`1. 2. 3.`) are dropped from the log; `list-style: none` keeps each row visually clean. Static cluster + visibility filter: - `staticBucketsByRace` now locks bucket order, mass, radius and local Vogel-spiral positions for the lifetime of the viewer; it only re-derives when `report` or the wasm `core` change. - `renderedByRace` overlays the per-frame `remaining` map and drops buckets whose `numLeft` hits zero. The surviving buckets keep their slots, so a class emptying never reshuffles the cluster — the empty bucket simply disappears. - A shot whose attacker or defender bucket is no longer visible draws no line (phantom shots into already-empty buckets are silently skipped, matching the user expectation that pup at 0 should stop attracting fire visually). - Race label clamps to a minimum y inside the SVG viewport so three-or-more-race layouts with a north anchor never clip the top race name off-canvas. Duel layout (user suggestion): - `layoutRaces` rotates the radial start angle by 90° when only two participants remain, so race 0 lands at 9 o'clock and race 1 at 3 o'clock. The pair faces off horizontally; neither label pushes against the SVG top edge. The existing test for two-race positions is updated accordingly. Tests: the existing `layoutRaces` two-race case is rewritten for the horizontal duel; the `game-shell-stubs` battle case checks the loading placeholder (back buttons now live in the loaded viewer, not the wrapper). 644 Vitest cases stay green; 4 Playwright battle-viewer cases stay green. Docs: `ui/docs/battle-viewer-ux.md` documents the static cluster / visibility filter, the duel layout, the scrubber, the cycling speed button and the collapsible log. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
17a3afd5e9 |
ui/phase-27: viewer polish + phantom-destroy clamp
Nine BattleViewer refinements from the latest review pass: 1. Mass radii were uniform in synthetic mode because `+layout.svelte` skipped `loadCore()` on the synthetic branch. The wasm bridge to `pkg/calc/ship.go` now boots in both modes so `computeBattleGroupMass` resolves a real FullMass and `radiusForMass` produces a per-battle scale. 2. Phantom-destroy clamp in `buildFrames`. Legacy emitters (KNNTS041 planet #7) log many more `Destroyed` lines against a group than the group's initial population — at frame 406 of 2317 the race totals previously hit zero on phantom shots and the scene blanked while playback continued silently. We now only shrink the per-group remaining count and the race totals when the group still has ships. The line still draws on phantom frames; only the counters stay sane. 3. Vogel sunflower positions are now reassigned by inward dot product before being handed to ranks: the rank-0 bucket — the one with the largest initial ship count — always lands at the most-inward spiral slot. The previous quarter-step anchor bias was too weak; ranks r ≥ 2 routinely overtook rank-0 toward the planet. The anchor offset is gone. 4. Bucket order inside a cluster is locked at battle start by each bucket's *initial* ship count (`num`), not its live `numLeft`. The position of every class circle stays put for the whole battle; only the label number changes as ships die. 5. Shot line + defender flash blink on a per-frame timer during play. The line stays on for the first 90 % of frame duration, off for the last 10 %, so two consecutive shots from the same attacker on the same defender look like two distinct pulses. On pause the line and flash stay drawn for inspection. 6. The defender's class circle now flashes red (destroyed) or green (shielded) in sync with the shot line, so the eye catches *who* was hit, not just where the line lands. 7. Battle log rows are buttons. Click / Enter / Space pauses playback and seeks to that shot. The list also auto-scrolls the current row into view so the highlight does not race off the bottom on long battles. 8. Race labels now sit above the cloud's bounding top instead of a fixed offset, so a dense cluster does not swallow its own race name. 9. Planet glyph + label switch to neutral grey (`#2a2f40` / `#4a5066` / `#6d7388`), keeping the planet "in the background" rather than competing with the combatants. Step-back icon switched to `◀︎◀︎` to mirror step-forward. Tests: two new Vitest cases cover the phantom-destroy clamp (single-race wipe, mixed-class race survives a class wipe). The existing 642 Vitest tests stay green; all four `battle-viewer` Playwright cases pass. Docs: `ui/docs/battle-viewer-ux.md` rewrites the cluster section (locked order + Vogel reassignment), adds Playback Details (blink + flash semantics), and a Phantom Destroys section explaining the clamp. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8c260f8715 |
ui/phase-27: mass-based circles + cloud cluster + height fit
Three Phase-27 BattleViewer refinements on top of the radial scene:
1. Height fit. The viewer is pinned to `calc(100dvh − 80px)` so it
never pushes the in-game shell past the viewport. `.active-view`
gains `overflow: hidden` + flex column; `.viewer` becomes a
`flex: 1` child; the always-visible text log shrinks to a 30 dvh
ceiling with its own scroll. A global `body { margin: 0 }`
reset (added to `app.html`) plugs the 16 px the browser's
default body margin used to leak.
2. Mass-based ship-class circles. New `lib/battle-player/mass.ts`
carries the radius formula and the per-battle FullMass compute:
`MIN_RADIUS + (MAX_RADIUS − MIN_RADIUS) * sqrt(mass / max)`,
clamped to `[6, 24] px`. FullMass goes through the existing
wasm bridge (`emptyMass` → `carryingMass` → `fullMass`) — no
new wire fields. The viewer page resolves a
`(race, className) → ShipClassRef` lookup from the parent
GameReport's `localShipClass` + `otherShipClass` tables and
passes it to the viewer via context. Unknown class or
degenerate (weapons/armament) params fall back to MAX_RADIUS
so the bucket stays visible.
3. Cloud cluster layout. Cluster key shifts from per-group
`g.key` to `(raceId, className)` so tech-variants of the same
hull collapse into one visual bucket. The horizontal
classCircleX row is replaced by a Vogel sunflower spiral in
the local `(u, v)` basis — `u` points from the race anchor to
the planet, `v` is `u` rotated 90° clockwise. Buckets are
sorted by NumberLeft desc; the cluster anchor is pushed inward
by a quarter step so rank-0 sits closest to the planet. The
step is adaptive (`min(baseStep, MAX_CLUSTER_RADIUS / sqrt(N))`)
so clusters with many classes do not spill into neighbours.
Tests:
- Vitest: `radiusForMass` covering zero / max / quarter-mass /
out-of-range cases (6 cases).
- Playwright: new `battle-viewer.spec.ts` case asserts
`document.documentElement.scrollHeight - window.innerHeight ≤ 4`
at a 1280×720 desktop viewport. The existing fixture gains
`localShipClass` + `otherShipClass` so the lookup has data to
render proportional circles.
Docs: `ui/docs/battle-viewer-ux.md` rewrites the "Radial scene"
section (cloud layout, mass-based radius, height fit) and adds
a "Height fit" subsection. `docs/FUNCTIONAL.md` §6.5 (+ ru
mirror) get the one-line story about per-mass sizing, cluster
aggregation, and the viewport-locked layout.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
969c0480ba |
ui/phase-27: battle viewer (radial scene, playback, map markers)
Engine wire change: Report.battle switched from []uuid.UUID to
[]BattleSummary{id, planet, shots} so the map can place battle
markers without N extra fetches. FBS schema + generated Go/TS
regenerated; transcoder + report controller updated; openapi
adds the BattleSummary schema with a freeze test.
Backend gateway forwards engine GET /api/v1/battle/:turn/:uuid as
/api/v1/user/games/{game_id}/battles/{turn}/{battle_id} (handler
plus engineclient.FetchBattle, contract test stub, openapi spec).
UI:
- BattleViewer (lib/battle-player/) is a logically isolated SVG
radial scene that consumes a BattleReport prop. Planet at the
centre, races on the outer ring at equal angular spacing, race
clusters by (race, className) with <class>:<numLeft> labels;
observer groups (inBattle: false) are not drawn; eliminated
races drop out and survivors re-distribute on the next frame.
- Shot line per frame: red on destroyed, green otherwise; erased
on the next frame. Playback controls: play/pause + step ± +
rewind + 1x/2x/4x speed (400/200/100 ms per frame).
- Page wrapper (lib/active-view/battle.svelte) loads BattleReport
via api/battle-fetch.ts; synthetic-gameId prefix routes to a
fixture loader, otherwise REST through the gateway. Always-
visible <ol> text protocol satisfies the accessibility ask.
- section-battles.svelte links every battle UUID into the viewer.
- map/battle-markers.ts: yellow X cross of 2 LinePrim through the
corners of the planet's circumscribed square (stroke width
clamps from 1 px at 1 shot to 5 px at 100+ shots); bombing
marker is a stroke-only ring (yellow when damaged, red when
wiped). Wired into state-binding.ts; click handler dispatches
battle clicks to the viewer and bombing clicks to the matching
Reports row.
- i18n keys for the viewer in en + ru.
Docs: ui/docs/battle-viewer-ux.md, FUNCTIONAL.md §6.5 + ru
mirror, ui/PLAN.md Phase 27 decisions + deferred TODOs (push
event, richer class visuals, animated re-distribution).
Tests: Vitest unit (radial layout + timeline frame builder +
marker stroke formula + marker primitives), Playwright e2e for
the viewer (Reports link → viewer, playback step, not-found),
backend engineclient FetchBattle (200 / 404 / bad input), engine
openapi freezes (BattleReport, BattleReportGroup,
BattleActionReport, BattleSummary, Report.battle items).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
2d17760a5e |
ui/phase-26: history mode (turn navigator + read-only banner)
Split GameStateStore into currentTurn (server's latest) and viewedTurn (displayed snapshot) so history excursions don't corrupt the resume bookmark or the live-turn bound. Add viewTurn / returnToCurrent / historyMode rune, plus a game-history cache namespace that stores past-turn reports for fast re-entry. OrderDraftStore.bindClient takes a getHistoryMode getter and short-circuits add / remove / move while the user is viewing a past turn; RenderedReportSource skips the order overlay in the same case. Header replaces the static "turn N" with a clickable triplet (TurnNavigator), the layout mounts HistoryBanner under the header, and visibility-refresh is a no-op while history is active. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
2ca47eb4df |
ui/phase-25: backend turn-cutoff guard + auto-pause + UI sync protocol
Backend now owns the turn-cutoff and pause guards the order tab relies on: the scheduler flips runtime_status between generation_in_progress and running around every engine tick, a failed tick auto-pauses the game through OnRuntimeSnapshot, and a new game.paused notification kind fans out alongside game.turn.ready. The user-games handlers reject submits with HTTP 409 turn_already_closed or game_paused depending on the runtime state. UI delegates auto-sync to a new OrderQueue: offline detection, single retry on reconnect, conflict / paused classification. OrderDraftStore surfaces conflictBanner / pausedBanner runes, clears them on local mutation or on a game.turn.ready push via resetForNewTurn. The order tab renders the matching banners and the new conflict per-row badge; i18n bundles cover en + ru. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5b07bb4e14 |
ui/phase-24: push events, turn-ready toast, single SubscribeEvents consumer
Wires the gateway's signed SubscribeEvents stream end-to-end:
- backend: emit game.turn.ready from lobby.OnRuntimeSnapshot on every
current_turn advance, addressed to every active membership, push-only
channel, idempotency key turn-ready:<game_id>:<turn>;
- ui: single EventStream singleton replaces revocation-watcher.ts and
carries both per-event dispatch and revocation detection; toast
primitive (store + host) lives in lib/; GameStateStore gains
pendingTurn/markPendingTurn/advanceToPending and a persisted
lastViewedTurn so a return after multiple turns surfaces the same
"view now" affordance as a live push event;
- mandatory event-signature verification through ui/core
(verifyPayloadHash + verifyEvent), full-jitter exponential backoff
1s -> 30s on transient failure, signOut("revoked") on
Unauthenticated or clean end-of-stream;
- catalog and migration accept the new kind; tests cover producer
(testcontainers + capturing publisher), consumer (Vitest event
stream, toast, game-state extensions), and a Playwright e2e
delivering a signed frame to the live UI.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
c58027c034 |
ui/phase-23: turn-report view with twenty sections and TOC
Replaces the Phase 10 report stub with a scrollable orchestrator that renders every FBS array as a dedicated section (galaxy summary, votes, player status, my/foreign sciences, my/foreign ship classes, battles, bombings, approaching groups, my/foreign/uninhabited/unknown planets, ships in production, cargo routes, my fleets, my/foreign/unidentified ship groups). A sticky table of contents (a <select> on mobile), "back to map" affordance, IntersectionObserver-driven active-section highlight, and SvelteKit Snapshot-based scroll save/restore round out the view. GameReport gains six new fields (players, otherScience, otherShipClass, battleIds, bombings, shipProductions); decodeReport, the synthetic- report loader, the e2e fixture builder, and EMPTY_SHIP_GROUPS extend in lockstep. ~90 new i18n keys land in en + ru together. The legacy-report parser is extended to populate the new sections from the dg/gplus text formats (Your Sciences, <Race> Sciences, <Race> Ship Types, Bombings, Ships In Production). Ships-in-production prod_used is derived through a new pkg/calc.ShipBuildCost helper; the engine's controller.ProduceShip refactors to call the same helper without any behaviour change (engine tests stay unchanged and green). Battles remain in the parser's Skipped list — the legacy text carries no stable per-battle UUID. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
e2a4790f6c |
ui/phase-22: skip the no-op stance click in the races table
Clicking the already-active WAR/PEACE button still appended a \`setDiplomaticStance\` whose \`relation\` matched the row's current value. The engine would accept the duplicate harmlessly, but the order tab inflates with rows that say nothing and every auto-sync re-ships the redundant payload. Compare against the overlayed stance (so a queued-but-not-applied change suppresses a re-click that matches the *intended* state, not just the server snapshot) and short-circuit when they agree. Mirrors the vote picker, which already had the same guard. vitest.config.ts: \`mergeConfig\` refuses callback-form base configs, so resolve \`vite.config.ts\`'s callback with the test context first and merge the plain object. Surfaced after the \`loadEnv\` migration switched the root config to the callback form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
9111dd955a |
ui/phase-22: races table with stance toggle and vote slot
Adds the Races View in the in-game shell. The table lists every non-extinct other race with tech levels (percent), totals, planets, votes received, and a per-row WAR | PEACE segmented control. A single vote-recipient slot above the table queues a `CommandRaceVote`; per-row buttons queue `CommandRaceRelation`. Both commands flow through the existing order draft store with collapse-by-acceptor (stance) and singleton (vote) rules. `GameReport` widens with `races`, `myVotes`, `myVoteFor`; the decoder walks `report.player[]` once for the richer projection. The optimistic overlay flips stance and vote target immediately; `votesReceived`, `myVotes`, and the alliance summary stay server-authoritative — alliance grouping and the 2/3 victory check are tallied on the server at turn cutoff and explicitly not surfaced client-side (`rules.txt` keeps foreign races' outgoing vote targets private). Includes Vitest component coverage of stance and vote collapse rules + a Playwright e2e that drives both commands through the dispatcher route and verifies the gateway saw the expected `CommandRaceRelation` / `CommandRaceVote` payloads. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
9c29f03d66 |
ui/phase-21: make MapView's mounted flag reactive
The renderer-mount effect in `lib/active-view/map.svelte` reads
`mounted` to gate the runSerializedMount call, but the variable was
declared as a plain `let`, not `$state`. On the first navigation to
/map this is benign: the effect's first pass returns early (gameState
still hydrating, `report` null), and once `report` arrives the
effect re-fires — by which point `onMount` has already flipped
`mounted = true`.
On every subsequent return to /map the report is already loaded by
the long-lived gameState in the layout. The effect therefore makes
exactly one pass on the freshly-mounted component, gates on
`mounted === false` (the brand-new instance has not run `onMount`
yet), and never wakes up again because no tracked state changes
afterwards. Symptom: black canvas — fresh DOM, no mount-error
overlay, but Pixi never rebuilt the world on the new canvas.
Convert `mounted` to `$state(false)` so flipping it true inside
`onMount` triggers the effect's second pass, which now finds all
preconditions satisfied and proceeds to `runSerializedMount`. The
detailed lifecycle reasoning is preserved as a code comment so the
next reader can see why this one variable must be reactive.
Add tests/e2e/map-roundtrip.spec.ts: navigates /map → {report,
ship-class designer, science designer, mail} → /map for each
non-map view, then asserts the renderer republished primitives onto
the DEV `__galaxyDebug.getMapPrimitives()` surface. The pre-fix
build failed every variant; the patch lands all four green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
7bea22b0b5 |
ui/phase-21: sciences CRUD list, designer, and production-picker integration
Lights up the player-defined sciences feature: a table view with sort and filter, a designer with four percent inputs and a strict sum-equals-100 gate, and a Research-sub-row integration so the planet production picker lists the user's sciences alongside the four tech buttons. Phase 21 decisions are baked back into ui/PLAN.md (no UpdateScience on the wire — write-once via createScience + removeScience; percentages instead of fractions; sciences live under the existing Research segment). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
54733bfb14 |
ui/phase-20: lock after Send + dashed tracks for in-flight & pending sends
Send joins Modernize / Dismantle / Transfer as a lockable command: once any of the four lands in the draft for a group, every action button on its inspector is disabled with a "command pending" tooltip and the banner names the queued kind. Load / Unload / Split / Join Fleet stay non-locking — they stack legitimately on the engine side. Two dashed overlays now run alongside the cargo-route arrows: - Yellow dashed track for own in-space groups, drawn from the origin planet to the destination (matches the in-space point colour so eye reads both as one entity). - Green dashed track for every wire-valid sendShipGroup command in the order draft, drawn from the source group's orbit planet to the chosen destination. Disappears when the command is removed from the order tab, when the engine rejects it, or when the group has left orbit (in-space track replaces it). Both tracks are wrap-aware via torusShortestDelta and never participate in hit-test. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
ac14eaff10 |
ui/phase-20: pick-first Send + lock after Modernize/Dismantle/Transfer
Send no longer carries a destination control inside the form: a click on the action drops the inspector straight into map-pick mode, and the form (ship count + confirm) only mounts after the player chooses a destination. Cancelling the picker leaves no form behind. A queued Modernize / Dismantle / Transfer for a given group locks every action button on its inspector and surfaces a banner that points the player at the order list. Cancelling the queued entry from the order tab releases the lock on the next render — the derivation watches draft.commands directly. Send / Load / Unload / Split / Join Fleet do not lock; Send is naturally followed by an out-of-orbit state at turn cutoff, the rest can stack legitimately. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
3626998a33 |
ui/phase-20: ship-group inspector actions
Eight ship-group operations land on the inspector behind a single inline-form panel: split, send, load, unload, modernize, dismantle, transfer, join fleet. Each action either appends a typed command to the local order draft or surfaces a tooltip explaining the disabled state. Partial-ship operations emit an implicit breakShipGroup command before the targeted action so the engine sees a clean (Break, Action) pair on the wire. `pkg/calc.BlockUpgradeCost` migrates from `game/internal/controller/ship_group_upgrade.go` so the calc bridge can wrap a pure pkg/calc formula; the controller now imports it. The bridge surfaces the function as `core.blockUpgradeCost`, which the inspector calls once per ship block to render the modernize cost preview. `GameReport.otherRaces` is decoded from the report's player block (non-extinct, ≠ self) and feeds the transfer-to-race picker. The planet inspector's stationed-ship rows become clickable for own groups so the actions panel is reachable from the standard click flow (the renderer continues to hide on-planet groups). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
f7109af55c |
ui/phase-19: torus-aware incoming track + on-planet groups in inspector
Two follow-up fixes after the initial Phase 19 landing:
1. The IncomingGroup dashed trajectory was drawn between raw
(x1, y1) and (x2, y2) world coordinates. On torus wrap mode
this took the long way around when origin and destination
sat near opposite seams. The line now picks endpoints via
`torusShortestDelta` so the segment crosses the seam when
that's the shorter visual path. The interpolated clickable
point follows the same unwrapped vector. The same helper
fixes the in-hyperspace position for local / foreign groups.
2. On-planet local and foreign groups previously rendered as
small offset points next to every populated planet, which
turned the canvas into noise as soon as a player held more
than a handful of planets. The map no longer renders any
in-orbit group; the planet inspector grows a compact
"stationed ship groups" subsection
(`lib/inspectors/planet/ship-groups.svelte`) that lists
each in-orbit group as a row of `<race> · <class> · <count>
ships · <mass>`. Race attribution: LocalGroup → the player's
race, OtherGroup on a foreign-owned planet → the planet's
owner, OtherGroup elsewhere → "foreign" placeholder. Rows
are non-interactive in Phase 19; Phase 21+ will deep-link
into the ship-groups table view with a (planet, race) filter.
Tests:
- `state-binding-groups.test.ts` swaps the on-planet rendering
expectation for the new "no map primitive" rule, and adds a
regression that asserts the incoming line crosses the torus
seam via `torusShortestDelta`.
- new `inspector-planet-ship-groups.test.ts` covers row
composition, the destination-mismatch filter, the
in-hyperspace exclusion, the foreign-planet owner fallback,
and the empty-state collapse.
- `inspector-planet.test.ts` and `inspector-ship-group.spec.ts`
pick up the new prop chain (`localShipGroups`,
`otherShipGroups`, `localRace`).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
86e77efe39 |
ui/phase-19: read-only ship-group inspector + sheet + tab dispatch
Closes Phase 19's UI surface. The inspector dispatches on the
selection variant: local / other groups render class, count, the
four tech levels, mass, cargo (type + amount when loaded),
location (planet name on-orbit, from/to/distance in hyperspace),
and — for local groups only — fleet membership + state. Incoming
groups surface origin / destination / distance / speed and the
inline ETA = ceil(distance / speed); zero speed collapses to the
designer's existing "—" placeholder. Unidentified groups render
just the (x, y) coordinates and the no-data hint, mirroring the
unidentified planet treatment.
Layout / inspector-tab plumbing:
- inspector-tab.svelte derives selectedShipGroup against the
rendered report and mounts <ShipGroup /> when the planet
branch doesn't match. Stale refs (an index that no longer
resolves after a turn refresh) collapse cleanly to the empty
state.
- +layout.svelte mounts <ShipGroupSheet /> alongside the
existing planet sheet on mobile; both share the
`effectiveTool === "map"` guard and clear-on-close.
i18n: en + ru both grow ~30 keys under
`game.inspector.ship_group.*`. Adding a key to one without the
other is a TS error (TranslationKey is `keyof typeof en`), so the
Russian mirror stays mandatory.
Tests:
- inspector-ship-group.test.ts exercises every variant —
on-planet local, in-hyperspace local, cargo-loaded local,
foreign, incoming with ETA, incoming with zero speed,
unidentified, plus the missing-planet `#NN` fallback.
- tests/e2e/inspector-ship-group.spec.ts is a smoke spec that
drives the DEV-only synthetic-report loader from /lobby
through navigation to /games/synthetic-XXX/map.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
676556db4e |
ui/phase-19: ship-group decoder + map binding + selection store
Wires Phase 19's data and rendering layers without yet adding the
inspector UI:
- game-state.ts grows ReportLocalShipGroup / ReportOtherShipGroup
/ ReportIncomingShipGroup / ReportUnidentifiedShipGroup /
ReportLocalFleet types and walks the matching FlatBuffers
vectors (LocalGroup, OtherGroup, IncomingGroup,
UnidentifiedGroup, LocalFleet) inside decodeReport. The Tech
map is folded into the fixed-shape ShipGroupTech struct;
cargo strings normalise to the closed CargoLoadType | "NONE"
union; UUIDs come back as canonical 36-char strings.
- synthetic-report.ts mirrors the new fields so the DEV-only
lobby loader can feed JSON produced by legacy-report-to-json
straight into the live UI surface.
- selection.svelte.ts widens its discriminated union with a
`kind: "shipGroup"` branch carrying a ShipGroupRef
(local UUID / other / incoming / unidentified by index).
- world.ts adds Style.strokeDashPx and render.ts.drawLine
honours it via manual segmentation (PixiJS v8 has no native
dash API). Ignored on points and circles.
- state-binding.ts now returns { world, hitLookup }: the
hit-lookup map keys every primitive id back to a concrete
HitTarget so the click handler can dispatch to selectPlanet
or selectShipGroup. Ship-group primitives live in a separate
ship-groups.ts that emits one point per local / other /
unidentified group, plus a dashed origin→destination line +
clickable point per incoming group. Position is interpolated
along the trajectory for in-hyperspace groups.
- map.svelte threads the hitLookup into handleMapClick.
Vitest:
- tests/helpers/empty-ship-groups.ts exposes EMPTY_SHIP_GROUPS
so existing fixtures can spread the new five empty arrays
without enumerating every field.
- state-binding-groups.test.ts covers each group variant's
primitive geometry and lookup correctness.
- All previously-existing fixture builders pick up the spread
so GameReport stays a complete object.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
8f320010c6 |
ui/synthetic-report: dev-only legacy report loader on lobby
Adds api/synthetic-report.ts, an in-memory registry + JSON->GameReport decoder for synthetic-mode game sessions. The lobby grows a import.meta.env.DEV-gated "Synthetic test reports" section with a JSON file picker; loading a file registers the decoded report under a synthetic-<uuid> id and navigates to /games/<id>/map. The in-game shell layout detects the synthetic id range, takes the report straight from the registry via gameState.initSynthetic, and deliberately skips both galaxyClient.set and orderDraft.bindClient. Order auto-sync stays silent: scheduleSync already short-circuits on non-UUID game ids, and without a bound client the network path is unreachable. applyOrderOverlay continues to project locally-valid draft commands onto the rendered report so renames / production choices / route edits are visible immediately. A page reload loses the in-memory entry and redirects to /lobby — synthetic mode is a debug affordance, not a session. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
e4dc0ce029 |
ui/phase-18: ship-class calc bridge with live designer preview
Wires pkg/calc/ship.go into the WASM Core boundary as seven thin
wrappers (DriveEffective, EmptyMass, WeaponsBlockMass, FullMass,
Speed, CargoCapacity, CarryingMass). The ship-class designer reads
Core through a new CORE_CONTEXT_KEY populated by the in-game layout
and renders a five-row preview pane (mass, full-load mass, max
speed, range at full load, cargo capacity) that updates reactively
on every form edit and on the player's localPlayer{Drive,Weapons,
Shields,Cargo} tech levels — three of which are now decoded from
the report's Player block alongside the existing localPlayerDrive.
CarryingMass is the seventh wrapper added to the original six-function
list so that "full-load mass" composes through pkg/calc/ functions
without putting math in TypeScript.
|
||
|
|
c8332bb122 |
ui/phase-17: clamp ship-class name input to validator's 30-rune limit
Mirrors the validateEntityName MAX_LENGTH on the form input so the field stops accepting characters once the limit is hit. The validator still runs and surfaces the localised reason if a paste overshoots; the maxlength is purely a typing-time guardrail. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
785c3483f8 |
ui/phase-17: ship-class CRUD without calc
Phase 17 lights up the ship-class table and designer active views, extends the order-draft pipeline with createShipClass and removeShipClass commands, and projects pending Save/Delete actions through applyOrderOverlay so the table reflects the player's intent before auto-sync lands. The plan is corrected in the same patch: per game/rules.txt, ship classes are designed once and cannot be edited — the engine has no Update command, so the UI exposes only Create + Delete. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
8a236bef14 |
ui/phase-16: pick any planet in reach + stronger pick-mode dim
The cargo-route picker filtered out unidentified planets, so an early-game player who had spotted but not surveyed a destination could not configure a route to it — the engine has no such restriction (`game/internal/controller/route.go.PlanetRouteSet` only checks ownership of the origin and `util.ShortDistance(...) <= FligthDistance`). Drop the unidentified guard and document the contract in `cargo-routes-ux.md` plus a comment over `reachableSet()`. Pick-mode dim now drops both alpha and tint on out-of-reach planets so bright shapes (`STYLE_LOCAL` is `0x6dd2ff`) collapse into a single muted gray. The single-channel `dimAlpha=0.3` was too gentle against the dark theme — the user reported the dim wasn't visible. Tighten to `dimAlpha=0.35 + dimTint=0x303841`; restore both on tear-down. Also threads through the user's `pkg/calc/race.go.FligthDistance` addition: `calc-bridge.md` records the new Go-side reference (the engine's `Race.FlightDistance()` already wraps it), and the picker comment points at the canonical formula location. Tests: - `inspector-planet-cargo-routes.test.ts` adds two cases — a reach-spans-every-kind case (own + foreign + uninhabited + unidentified all picked when in range) and a successful pick to an unidentified destination. - All 356 vitest cases + chromium-desktop / webkit-desktop e2e cargo-routes pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
7c8b5aeb23 |
ui/phase-16: cargo routes inspector + map pick foundation
Add per-planet cargo routes (COL/CAP/MAT/EMP) to the inspector with a renderer-driven destination picker (faded out-of-reach planets, cursor-line anchor, hover-highlight) and per-route arrows on the map. The pick-mode primitives are exposed via `MapPickService` so ship-group dispatch in Phase 19/20 can reuse the same surface. Pass A — generic map foundation: - hit-test now sizes the click zone to `pointRadiusPx + slopPx` so the visible disc is always part of the target. - `RendererHandle` gains `onPointerMove`, `onHoverChange`, `setPickMode`, `getPickState`, `getPrimitiveAlpha`, `setExtraPrimitives`, `getPrimitives`. The click dispatcher is centralised: pick-mode swallows clicks atomically so the standard selection consumers do not race against teardown. - `MapPickService` (`lib/map-pick.svelte.ts`) wraps the renderer contract in a promise-shaped `pick(...)`. The in-game shell layout owns the service so sidebar and bottom-sheet inspectors see the same instance. - Debug-surface registry exposes `getMapPrimitives`, `getMapPickState`, `getMapCamera` to e2e specs without spawning a separate debug page after navigation. Pass B — cargo-route feature: - `CargoLoadType`, `setCargoRoute`, `removeCargoRoute` typed variants with `(source, loadType)` collapse rule on the order draft; round-trip through the FBS encoder/decoder. - `GameReport` decodes `routes` and the local player's drive tech for the inline reach formula (40 × drive). `applyOrderOverlay` upserts/drops route entries for valid/submitting/applied commands. - `lib/inspectors/planet/cargo-routes.svelte` renders the four-slot section. `Add` / `Edit` call `MapPickService.pick`, `Remove` emits `removeCargoRoute`. - `map/cargo-routes.ts` builds shaft + arrowhead primitives per cargo type; the map view pushes them through `setExtraPrimitives` so the renderer never re-inits Pixi on route mutations (Pixi 8 doesn't support that on a reused canvas). Docs: - `docs/cargo-routes-ux.md` covers engine semantics + UI map. - `docs/renderer.md` documents pick mode and the debug surface. - `docs/calc-bridge.md` records the Phase 16 reach waiver. - `PLAN.md` rewrites Phase 16 to reflect the foundation + feature split and the decisions baked in (map-driven picker, inline reach, optimistic overlay via `setExtraPrimitives`). Tests: - `tests/map-pick-mode.test.ts` — pure overlay-spec helper. - `tests/map-cargo-routes.test.ts` — `buildCargoRouteLines`. - `tests/inspector-planet-cargo-routes.test.ts` — slot rendering, picker invocation, collapse, cancel, remove. - Extensions to `order-draft`, `submit`, `order-load`, `order-overlay`, `state-binding`, `inspector-planet`, `inspector-overlay`, `game-shell-sidebar`, `game-shell-header`. - `tests/e2e/cargo-routes.spec.ts` — Playwright happy path: add COL, add CAP, remove COL, asserting both the inspector and the arrow count via `__galaxyDebug.getMapPrimitives()`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
915b4372dd |
ui/phase-15: planet inspector production controls + order-draft collapse
Adds the second end-to-end command (`setProductionType`) with a collapse-by-`planetNumber` rule on the order draft, the segmented production-controls component on the planet inspector, the FBS encoder/decoder pair for `CommandPlanetProduce`, and the `localShipClass` projection on `GameReport`. Forecast number is deferred and tracked in the new `ui/docs/calc-bridge.md`. |
||
|
|
229c43beb5 |
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> |
||
|
|
f80c623a74 |
ui/phase-14: rename planet end-to-end + order read-back
Wires the first end-to-end command through the full pipeline:
inspector rename action → local order draft → user.games.order
submit → optimistic overlay on map / inspector → server hydration
on cache miss via the new user.games.order.get message type.
Backend: GET /api/v1/user/games/{id}/orders forwards to engine
GET /api/v1/order. Gateway parses the engine PUT response into the
extended UserGamesOrderResponse FBS envelope and adds
executeUserGamesOrderGet for the read-back path. Frontend ports
ValidateTypeName to TS, lands the inline rename editor + Submit
button, and exposes a renderedReport context so consumers see the
overlay-applied snapshot.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
6364bba6fd |
ui/phase-13: planet inspector — read-only
Plumbs the map → inspector pathway: a click on a planet selects it through the new SelectionStore, the sidebar Inspector tab swaps its empty-state copy for a per-kind read-only field set, and a mobile-only bottom-sheet mirrors the same content over the map. Field projection in api/game-state.ts now surfaces every documented planet field. |
||
|
|
460591c159 |
ui/phase-12: order composer skeleton
OrderDraftStore persists per-game command drafts in Cache; the sidebar Order tab renders the list with a per-row delete control. The layout passes a `historyMode` prop through Sidebar / BottomTabs as a constant `false`, so Phase 26 only flips the source. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
ce7a66b3e6 |
ui/phase-11: map wired to live game state
Replaces the Phase 10 map stub with live planet rendering driven by `user.games.report`, and wires the header turn counter to the same data. Phase 11's frontend sits on a per-game `GameStateStore` that lives in `lib/game-state.svelte.ts`: the in-game shell layout instantiates one per game, exposes it through Svelte context, and disposes it on remount. The store discovers the game's current turn through `lobby.my.games.list`, fetches the matching report, and exposes a TS-friendly snapshot to the header turn counter, the map view, and the inspector / order / calculator tabs that later phases will plug onto the same instance. The pipeline forced one cross-stage decision: the user surface needs the current turn number to know which report to fetch, but `GameSummary` did not expose it. Phase 11 extends the lobby catalogue (FB schema, transcoder, Go model, backend gameSummaryWire, gateway decoders, openapi, TS bindings, api/lobby.ts) with `current_turn:int32`. The data was already tracked in backend's `RuntimeSnapshot.CurrentTurn`; surfacing it is a wire change only. Two alternatives were rejected: a brand-new `user.games.state` message (full wire-flow for one field) and hard-coding `turn=0` (works for the dev sandbox, which never advances past zero, but renders the initial state for any real game). The change crosses Phase 8's already-shipped catalogue per the project's "decisions baked back into the live plan" rule — existing tests and fixtures are updated in the same patch. The state binding lives in `map/state-binding.ts::reportToWorld`: one Point primitive per planet across all four kinds (local / other / uninhabited / unidentified) with distinct fill colours, fill alphas, and point radii so the user can tell them apart at a glance. The planet engine number is reused as the primitive id so a hit-test result resolves directly to a planet without an extra lookup table. Zero-planet reports yield a well-formed empty world; malformed dimensions fall back to 1×1 so a bad report cannot crash the renderer. The map view's mount effect creates the renderer once and skips re-mount on no-op refreshes (same turn, same wrap mode); a turn change or wrap-mode flip disposes and recreates it. The renderer's external API does not yet expose `setWorld`; Phase 24 / 34 will extract it once high-frequency updates land. The store installs a `visibilitychange` listener that calls `refresh()` when the tab regains focus. Wrap-mode preference uses `Cache` namespace `game-prefs`, key `<gameId>/wrap-mode`, default `torus`. Phase 11 reads through `store.wrapMode`; Phase 29 wires the toggle UI on top of `setWrapMode`. Tests: Vitest unit coverage for `reportToWorld` (every kind, ids, styling, empty / zero-dimension edges, priority order) and for the store lifecycle (init success, missing-membership error, forbidden-result error, `setTurn`, wrap-mode persistence across instances, `failBootstrap`). Playwright e2e mocks the gateway for `lobby.my.games.list` and `user.games.report` and asserts the live data path: turn counter shows the reported turn, `active-view-map` flips to `data-status="ready"`, and `data-planet-count` matches the fixture count. The zero-planet regression and the missing-membership error path are covered. Phase 11 status stays `pending` in `ui/PLAN.md` until the local-ci run lands green; flipping to `done` follows in the next commit per the per-stage CI gate in `CLAUDE.md`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
fc371c7fe1 |
ui/phase-10: in-game shell with view-replacement skeleton
Wraps every in-game route under `/games/:id/*` in a responsive shell
with a header (race / turn placeholders, view-menu dropdown or mobile
hamburger, account menu), a three-tab sidebar (Calculator, Inspector,
Order), an active-view slot, and a mobile-only bottom-tabs row
`[Map, Calc, Order, More]`. Every view in the IA section
(`map`, `table/:entity`, `report`, `battle/:battleId?`, `mail`,
`designer/{ship-class,science}/:id?`) ships as a thin SvelteKit route
that mounts a `lib/active-view/<name>.svelte` stub rendering a
localised `coming soon` body. The lobby's `gotoGame` path now actually
lands on a rendered shell instead of a 404.
The "view router" mentioned in the plan is implemented as the file
system plus two-line route wrappers — no separate dispatch component.
Sidebar tab state lives as a `$state` rune inside `sidebar.svelte`,
which sits in the layout that SvelteKit keeps mounted across child
route swaps, so tab choice survives every active-view navigation for
free. A `?sidebar=calc|inspector|order` URL param seeds the initial
tab on first mount; the mobile bottom-tabs use a layout-owned
`mobileTool` rune with a URL-gated `effectiveTool` derivation so the
Calc / Order tool overlay only applies on `/map` and naturally drops
when the user navigates elsewhere.
Tablet ships with a click-toggle drawer for the sidebar rather than
the IA section's swipe-from-right gesture; the structural breakpoint
satisfies Phase 10's acceptance criterion and Phase 35 polish lands
the swipe. The mobile More drawer mirrors the header view-menu
content; the IA's narrower More list (Mail, Battle, Tables, History,
Settings, Logout) is also a Phase 35 polish target once History
exists.
Topic doc `ui/docs/navigation.md` captures the active-view model, the
sidebar state-preservation rule, the `?sidebar=` and `mobileTool`
conventions, and the transient map-overlay back-stack concept (with
the implementation deferred to Phase 34 alongside its first user).
i18n catalogues for `en` and `ru` add the full `game.shell.*`,
`game.view.*`, `game.sidebar.*`, `game.bottom_tabs.*` namespaces.
Tests: Vitest covers the header view-menu (every IA destination
including the Tables sub-list), the account-menu Logout / Language
wiring, the sidebar default tab / switching / `?sidebar=` seed /
close button, and every active-view stub. Playwright e2e boots an
authenticated session via `__galaxyDebug.setDeviceSessionId` (no
gateway calls — the shell makes none in Phase 10), exercises every
view through both the desktop dropdown and the mobile More drawer,
verifies sidebar tab survival across navigation, and uses
`setViewportSize` to validate the breakpoint switches at 768 px and
1024 px.
Phase 10 status stays `pending` in `ui/PLAN.md` until the local-ci
run lands green; flipping to `done` follows in the next commit per
the per-stage CI gate in `CLAUDE.md`.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
f57a290432 |
phase 8: lobby UI + cross-stack lobby command catalog + TS FlatBuffers
- Extend pkg/model/lobby and pkg/schema/fbs/lobby.fbs with public-games
list, my-applications/invites lists, game-create, application-submit,
invite-redeem/decline. Mirror the matching transcoder pairs and Go
fixture round-trip tests.
- Wire the seven new lobby message types through
gateway/internal/backendclient/{routes,lobby_commands}.go with
per-command REST helpers, JSON-tolerant decoding of backend wire
shapes, and httptest-based unit coverage for success / 4xx / 5xx /
503 across each command.
- Introduce TS-side FlatBuffers via the `flatbuffers` runtime dep, a
`make fbs-ts` target driving flatc, and the generated bindings under
ui/frontend/src/proto/galaxy/fbs. Phase 7's `user.account.get` decode
now uses these bindings as well, closing the JSON.parse vs
FlatBuffers gap that would have failed against a real local stack.
- Replace the placeholder lobby with five sections (my games, pending
invitations, my applications, public games, create new game) and the
/lobby/create form. Submit-application uses an inline race-name
form on the public-game card; create-game keeps name / description /
turn_schedule / enrollment_ends_at always visible and the rest under
an Advanced toggle with TS-side defaults.
- Update lobby/+page.svelte to throw LobbyError on non-ok result codes;
GalaxyClient.executeCommand now returns { resultCode, payloadBytes }.
- Vitest binding round-trips, lobby.ts wrapper unit tests, lobby-page
+ lobby-create component tests, Playwright lobby-flow.spec covering
create / submit / accept across all four projects. Phase 7 e2e was
migrated to the FlatBuffers fixtures and to click+fill against the
Safari-autofill readonly inputs.
- Mark Phase 8 done in ui/PLAN.md, mirror the wire-format note into
Phase 7, append the new lobby commands to gateway/README.md and
docs/ARCHITECTURE.md, add ui/docs/lobby.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
9101aba816 |
phase 7+: i18n primitive + login language picker + autocomplete-off
Adds a minimal Svelte 5 i18n primitive (`src/lib/i18n/`) backing the
login form, the layout blocker page, and the lobby placeholder.
SUPPORTED_LOCALES drives both the picker and the runtime lookup;
adding a language is a two-step change inside `src/lib/i18n/`.
Login form gains a globe-icon language dropdown (English / Русский
in their native names), defaulting to navigator.languages with `en`
as the fallback. Switching the locale re-renders the form in place;
on submit, the locale rides in the JSON body of `send-email-code`
because Safari/WebKit silently drops JS-set Accept-Language. Gateway
gains a body `locale` field that takes priority over the request
header for preferred-language resolution.
Email and code inputs disable browser autofill / suggestions
(`autocomplete=off` + `autocorrect=off` + `autocapitalize=off` +
`spellcheck=false`) so Keychain / address-book pickers and
remembered-value dropdowns no longer fire on focus.
Cross-cuts:
- backend & gateway openapi: clarify that body `locale` is honored.
- docs/FUNCTIONAL{,_ru}.md §1.2: document body-vs-header priority.
- gateway tests: body `locale` overrides Accept-Language; blank
body `locale` falls back to header.
- new ui/docs/i18n.md; cross-links from auth-flow.md and ui/README.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
22b0710d04 |
phase 7: auth flow UI (email-code login + session resume + revocation)
Implements ui/PLAN.md Phase 7 end-to-end: - /login two-step form (email -> code) over the gateway public REST surface; /lobby placeholder issues the first authenticated user.account.get and renders the decoded display name. - SessionStore (Svelte 5 runes) with loading / unsupported / anonymous / authenticated states; layout-level route guard, browser-not-supported blocker, and a minimal SubscribeEvents revocation watcher that closes the active client within 1s on a clean stream end or Unauthenticated. - VITE_GATEWAY_BASE_URL + VITE_GATEWAY_RESPONSE_PUBLIC_KEY config plus AuthError taxonomy in api/auth.ts. - Vitest (auth-api, session-store, login-page) and Playwright e2e (auth-flow.spec.ts) on the four configured projects, with a fixture Ed25519 keypair forging Connect-Web JSON responses. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
7cc18159e9 | phase 1 |