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:
@@ -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 808–843); 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.
|
||||
Reference in New Issue
Block a user