7c8b5aeb23
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>
162 lines
8.4 KiB
Markdown
162 lines
8.4 KiB
Markdown
# 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.
|