# 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.
