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
+111 -22
View File
@@ -1814,22 +1814,77 @@ Verified on local-ci run 16 (`success`, 4273102).
Status: pending.
Goal: configure up to four cargo routes per planet (colonists,
industry, materials, empty) through the inspector.
industry, materials, empty) through the inspector, with the
destination picked directly on the map. Phase 16 also lands the
generic map-pick foundation (Pass A) the inspector consumes; Phase
19/20 (ship-group dispatch) reuses the same renderer surface.
Artifacts:
Artifacts (Pass A — renderer foundation):
- `ui/frontend/src/map/pick-mode.ts` carries the `PickModeOptions` /
`PickModeHandle` types and the pure `computePickOverlay` helper.
- `ui/frontend/src/map/render.ts` extends `RendererHandle` with
`setPickMode` / `isPickModeActive` / `getPickState`,
`onPointerMove` / `onHoverChange`, and the
`getPrimitiveAlpha(id)` debug accessor. The standard `onClick`
consumers are gated on the `pickModeActive` flag so the
destination click does not also trigger planet selection.
- `ui/frontend/src/map/hit-test.ts` widens point matching to
`(pointRadiusPx + slopPx) / camera.scale` so hover and click
zones match the visible disc; default radius shared via
`DEFAULT_POINT_RADIUS_PX = 3`.
- `ui/frontend/src/lib/map-pick.svelte.ts` defines the Svelte
`MapPickService` (promise-shaped `pick(...)` plus reactive
`active`); `lib/active-view/map.svelte` constructs the service
and binds a renderer-side resolver that resolves
`sourcePlanetNumber` against the current report.
- `ui/frontend/src/lib/debug-surface.svelte.ts` registers
`getMapPrimitives()` and `getMapPickState()` providers; the
DEV-only `__galaxyDebug` surface in
`routes/__debug/store/+page.svelte` exposes them so e2e specs
can assert the renderer's state without scraping pixels.
Artifacts (Pass B — feature):
- `ui/frontend/src/lib/inspectors/planet/cargo-routes.svelte`
four-slot UI listing existing routes and offering add / edit /
remove
- `ui/frontend/src/sync/order-types.ts` extends with
`SetCargoRoute` and `RemoveCargoRoute` command variants
- destination-planet picker filtered by reach (uses `pkg/calc/` reach
function via `ui/core/calc/`)
- `ui/frontend/src/map/cargo-routes.ts` renders route arrows on the
map between source and destination planet, styled per cargo type
- topic doc `ui/docs/cargo-routes-ux.md` capturing the priority
semantics from [`rules.txt`](../game/rules.txt) (`colonists → industry → materials →
empty`)
`CargoLoadType`, `SetCargoRouteCommand`, and
`RemoveCargoRouteCommand`. `CARGO_LOAD_TYPE_VALUES` is the
priority order (`COL`, `CAP`, `MAT`, `EMP`).
- `ui/frontend/src/sync/order-draft.svelte.ts` collapses both
variants by `(sourcePlanetNumber, loadType)`; the newer entry
supersedes any prior `set` or `remove` for the same slot.
- `ui/frontend/src/sync/submit.ts` and
`ui/frontend/src/sync/order-load.ts` round-trip the two new
variants through `CommandPlanetRouteSet` and
`CommandPlanetRouteRemove`. UNKNOWN load-type values drop with
a `console.warn`.
- `ui/frontend/src/api/game-state.ts` extends `GameReport` with
`routes: ReportRoute[]` (decoded from `report.route()` in
`CARGO_LOAD_TYPE_VALUES` order) and `localPlayerDrive: number`
(looked up via `findLocalPlayerDrive`). `applyOrderOverlay`
upserts / drops route entries for valid / submitting / applied
cargo-route commands.
- `ui/frontend/src/lib/inspectors/planet/cargo-routes.svelte` is
the four-slot subsection. `Add` / `Edit` call
`MapPickService.pick(...)`; `Remove` emits
`removeCargoRoute`.
- `ui/frontend/src/map/cargo-routes.ts` builds the `LinePrim`
arrows (shaft + two arrowhead wings) per
`(source, loadType, destination)` triple. Per-type style and
priority (`COL=8` … `EMP=5`); ids prefixed with `0x80000000`
to avoid colliding with planet numbers.
- `ui/frontend/src/map/state-binding.ts` appends
`buildCargoRouteLines(report)` to the world primitives.
- `ui/frontend/src/lib/active-view/map.svelte` adds a
routes-content fingerprint to the same-snapshot guard and
preserves camera centre + zoom across route-driven remounts
inside the same game id.
- Topic doc `ui/docs/cargo-routes-ux.md` quotes
[`rules.txt`](../game/rules.txt) (lines 808843) and maps
semantics to UI; `ui/docs/renderer.md` documents the pick-mode
contract; `ui/docs/calc-bridge.md` records the Phase 16 reach
waiver (inline TS rather than a calc bridge for one
constant-time multiplication).
Dependencies: Phase 15.
@@ -1837,20 +1892,54 @@ Acceptance criteria:
- the user can add, edit, and remove cargo routes through the
inspector;
- destination picker disables planets outside reach with a tooltip
explaining the constraint;
- the destination picker happens on the map: out-of-reach planets
fade to `α=0.3`, the source gains an anchor ring, the cursor
draws a live line to the source, and hover over a reachable
planet outlines it. Clicks on non-reachable space are no-ops; a
click on a reachable planet emits `setCargoRoute`; Escape
cancels;
- the four route types are mutually exclusive — only one route per
type per source planet;
- configured routes are rendered as arrows on the map between source
and destination planets, distinguishable per cargo type.
- configured routes are rendered as arrows on the map between
source and destination planets, distinguishable per cargo type
(placeholder colour palette; final values land in Phase 35
polish);
- the optimistic overlay surfaces draft routes immediately on the
map; the camera survives the routes-fingerprint remount so the
view does not jolt mid-edit.
Targeted tests:
- Vitest unit tests for slot-conflict detection;
- Vitest unit tests for cargo-route arrow rendering on torus and
no-wrap fixtures;
- Playwright e2e: add a route end-to-end, confirm server applies it
on next turn and the arrow is visible on the map.
- Vitest: `tests/map-hit-test.test.ts` (regenerated for the
visible-radius formula), `tests/map-pick-mode.test.ts`
(`computePickOverlay` lifecycle),
`tests/map-cargo-routes.test.ts`,
`tests/inspector-planet-cargo-routes.test.ts`,
`tests/state-binding.test.ts` extension,
`tests/order-draft.test.ts` extension,
`tests/submit.test.ts` and `tests/order-load.test.ts`
extensions, `tests/order-overlay.test.ts` extension.
- Playwright e2e `tests/e2e/cargo-routes.spec.ts`: open
inspector, trigger `Add`, assert dim state via
`__galaxyDebug.getMapPickState()`, click a reachable planet,
assert `setCargoRoute` shipped + arrow visible via
`__galaxyDebug.getMapPrimitives()`. Add a CAP route to confirm
slots coexist; Remove COL → arrow gone; reload → restored from
`user.games.order.get`.
Decisions baked into Phase 16 (vs. the original stage description):
- The destination picker is map-driven, not list-based. The
acceptance criterion "disables planets outside reach with a
tooltip" is replaced by "fades planets outside reach to
`α=0.3` and forbids picking them"; the rendered map is the
player's spatial reference, so a list duplicates information
the planet already conveys.
- Reach is computed inline in TypeScript, not via a `pkg/calc/`
Go bridge (`ui/docs/calc-bridge.md` Phase 16 waiver).
- Wrap-mode is treated as a per-game property set at map load;
the camera-preservation refactor only fires when the
routes-fingerprint changes inside the same game id.
## Phase 17. Ship Classes — CRUD Without Calc