8a236bef14
The cargo-route picker filtered out unidentified planets, so an early-game player who had spotted but not surveyed a destination could not configure a route to it — the engine has no such restriction (`game/internal/controller/route.go.PlanetRouteSet` only checks ownership of the origin and `util.ShortDistance(...) <= FligthDistance`). Drop the unidentified guard and document the contract in `cargo-routes-ux.md` plus a comment over `reachableSet()`. Pick-mode dim now drops both alpha and tint on out-of-reach planets so bright shapes (`STYLE_LOCAL` is `0x6dd2ff`) collapse into a single muted gray. The single-channel `dimAlpha=0.3` was too gentle against the dark theme — the user reported the dim wasn't visible. Tighten to `dimAlpha=0.35 + dimTint=0x303841`; restore both on tear-down. Also threads through the user's `pkg/calc/race.go.FligthDistance` addition: `calc-bridge.md` records the new Go-side reference (the engine's `Race.FlightDistance()` already wraps it), and the picker comment points at the canonical formula location. Tests: - `inspector-planet-cargo-routes.test.ts` adds two cases — a reach-spans-every-kind case (own + foreign + uninhabited + unidentified all picked when in range) and a successful pick to an unidentified destination. - All 356 vitest cases + chromium-desktop / webkit-desktop e2e cargo-routes pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
171 lines
8.9 KiB
Markdown
171 lines
8.9 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`).
|
||
|
||
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.
|