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>
This commit is contained in:
Ilia Denisov
2026-05-09 20:01:34 +02:00
parent 5fd67ed958
commit 7c8b5aeb23
43 changed files with 4559 additions and 98 deletions
+161
View File
@@ -0,0 +1,161 @@
# Cargo routes UX
This document covers the cargo-route surface added in Phase 16: the
four-slot inspector subsection, the map-driven destination pick, and
the optimistic overlay that keeps the inspector and the map in lock-
step with the local order draft. The user-visible spec lives in
[`../PLAN.md`](../PLAN.md) Phase 16; the engine semantics are quoted
from [`game/rules.txt`](../../game/rules.txt) section "Грузовые
маршруты" (lines 808843); this file is the source of truth for how
the UI surfaces those rules.
## Engine semantics in one paragraph
A cargo route on a planet you own pairs a load-type slot
(`COL`/`CAP`/`MAT`/`EMP`) with a destination planet. Once set, the
engine loads transport ships at the source on every turn cutoff and
sends them to the destination, draining the load-type stockpile
("colonists" → population pool, "capital" → industry crates, "mat" →
raw materials, "empty" → ships returning unloaded). When several
slots are configured the engine processes them in
`COL > CAP > MAT > EMP` priority order
(`game/internal/controller/route.go.SendRoutedGroups`, line 101).
Routes are constrained by reach: the destination must be no further
than `40 × driveTech` world units along the torus-shortest path
(`util.ShortDistance``Race.FlightDistance()`), and a route whose
destination becomes unreachable at the next turn is auto-removed
(`RemoveUnreachableRoutes`).
## Four-slot inspector subsection
The cargo-routes subsection renders below the production controls
on every owned planet inspector. Slots appear in
`CARGO_LOAD_TYPE_VALUES` order (COL, CAP, MAT, EMP) so visual order
matches the engine's load priority — players who scan top-down see
the highest-priority cargo first.
Slot states:
- **Empty** — `(no route)` text plus a single `Add` button.
- **Filled** — `→ {destination name}` plus `Edit` and `Remove`.
`Add` and `Edit` open a renderer-driven destination pick (see next
section). `Remove` emits a `removeCargoRoute` command. The collapse
rule on the order draft store ensures only one entry per
`(source, loadType)` slot survives in the draft at any time, so a
sequence of `Add → Edit → Remove` collapses to the latest verb only
(matching the production-controls pattern from Phase 15).
Disabled state: every button is disabled when the
`OrderDraftStore` or `MapPickService` context is missing (the
component is mounted outside the in-game shell, in tests, etc.).
## Map-driven destination pick
The picker is renderer-side: the inspector calls
`MapPickService.pick({sourcePlanetNumber, reachableIds})` and awaits a
planet number (or `null` on cancel). Reach is computed inline against
`localPlayerDrive` — see the calc-bridge waiver in
[`calc-bridge.md`](./calc-bridge.md).
While a session is active:
- All planets whose ids are not in `reachableIds` and are not the
source render at `alpha = 0.3`. The visual fade signals "not a
valid destination" without removing the planet from the map.
- The source planet keeps full alpha and gains an outline ring so
the player sees where the route originates.
- A line is drawn from the source to the cursor. On touch devices
the line follows the finger during drag.
- Hover over a reachable planet adds an outline highlight at
`pointRadiusPx + 4` world units so the player can confirm which
planet they are about to pick.
Resolution paths:
- Click on a reachable planet → `setCargoRoute` enters the draft;
the inspector slot fills; the arrow appears immediately on the
map (overlay route applied on the next render).
- Click on empty space or a non-reachable planet → no-op (a
forgiving rule for accidental taps mid-pan).
- Press Escape, click the inspector's `Cancel pick` button, or
unmount the active map view (e.g. switch tools, navigate away)
→ session cancels; the slot stays as it was.
The inspector renders an inline status line `pick a destination on
the map (Esc to cancel)` while the session is active so the player
knows the click target and the cancel hotkey. The line vanishes as
soon as the picker resolves.
## Optimistic map overlay
`applyOrderOverlay` projects every locally-valid, in-flight, or
applied `setCargoRoute` / `removeCargoRoute` onto `report.routes`,
re-runs `reportToWorld` (which appends `LinePrim` arrows from
`buildCargoRouteLines`), and remounts the renderer when the routes-
content fingerprint changes. The map active view captures camera
centre + zoom before each remount and restores them when the game
id is unchanged, so adding a route mid-pan does not jolt the view.
Arrows are drawn as a shaft plus two short arrowhead wings. Per-type
styling (placeholder Phase 16 colours; final values land in Phase
35 polish):
| Load type | Stroke colour | Notes |
| --------- | ------------- | ------------------------ |
| COL | `#4FC3F7` | brightest blue, highest priority |
| CAP | `#FFB74D` | warm orange |
| MAT | `#81C784` | green |
| EMP | `#90A4AE` | dim grey, thinner stroke |
Arrow priority (COL=8, CAP=7, MAT=6, EMP=5) sits above all planet
priorities (1..4), so two arrows that overlap exactly resolve to the
higher-priority load type during hit-test. Planet primitives still
win over arrows because the line ids carry a high-bit prefix
(`0x80000000`) and the renderer keeps points at the kind tie-break
position 0.
## Reach computation
Implementation: inline TypeScript using `torusShortestDelta` for
each axis and `Math.hypot` for the distance, compared against `40 ×
localPlayerDrive`. The local player's drive comes from the report's
`Player` block, looked up by `name === report.race`
(`api/game-state.ts.findLocalPlayerDrive`).
Why inline rather than via a Go calc bridge? See the Phase 15 / 16
deferral note in [`calc-bridge.md`](./calc-bridge.md). The formula
is trivial (`tech × 40`) and the WASM glue would be premature
infrastructure; when the calc bridge phase lands the shared
`pkg/calc.Reach` will replace this implementation.
## Tests
| Layer | File | What it covers |
| ----------------------------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
| Order draft collapse | `tests/order-draft.test.ts` | `(source, loadType)` collapse rules across `set` and `remove`. |
| Encode / decode round-trip | `tests/submit.test.ts`, `tests/order-load.test.ts` | `setCargoRoute` and `removeCargoRoute` ↔ FBS payloads; UNKNOWN load-type drops with a warn. |
| Overlay | `tests/order-overlay.test.ts` | `applyOrderOverlay` upserts / drops route entries for valid / submitting / applied statuses. |
| Inspector subsection | `tests/inspector-planet-cargo-routes.test.ts` | Slot rendering, pick invocation, emit, cancel, edit, remove, per-type independence. |
| Map arrows | `tests/map-cargo-routes.test.ts` | `buildCargoRouteLines` shape on torus + no-wrap fixtures, per-type style, priority ordering. |
| End-to-end | `tests/e2e/cargo-routes.spec.ts` | Mocked gateway: open inspector, dim outside reach, pick destination, arrow appears, reload. |
## File index
- `ui/frontend/src/lib/inspectors/planet/cargo-routes.svelte`
inspector subsection.
- `ui/frontend/src/sync/order-types.ts``CargoLoadType`,
`SetCargoRouteCommand`, `RemoveCargoRouteCommand`.
- `ui/frontend/src/sync/order-draft.svelte.ts` — collapse rule.
- `ui/frontend/src/sync/submit.ts` — encoder.
- `ui/frontend/src/sync/order-load.ts` — decoder.
- `ui/frontend/src/api/game-state.ts``routes` and
`localPlayerDrive` decoding plus overlay extension.
- `ui/frontend/src/map/cargo-routes.ts` — arrow geometry.
- `ui/frontend/src/map/state-binding.ts` — appends route lines.
- `ui/frontend/src/lib/active-view/map.svelte` — fingerprint guard
+ camera preservation.
- `ui/frontend/src/map/pick-mode.ts` and
`ui/frontend/src/map/render.ts` — pick-mode foundation.
- `ui/frontend/src/lib/map-pick.svelte.ts` — Svelte adapter.
- `ui/docs/renderer.md` — pick-mode and debug-surface contract.