# 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`). The Go-side counterpart is `pkg/calc/race.go.FligthDistance`. The engine accepts a route from a player-owned planet to **any** planet inside that distance — own, foreign-race, uninhabited, or unidentified all qualify (`game/internal/controller/route.go.PlanetRouteSet` only enforces ownership of the *origin*). The picker mirrors that contract: the `reachableSet()` in `cargo-routes.svelte` filters out only the source planet itself. 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.FligthDistance` 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.