Files
galaxy-game/ui/docs/cargo-routes-ux.md
T
Ilia Denisov aee5f39a7e
Tests · UI / test (pull_request) Successful in 11m1s
docs(ui): F8-05 — sync inspector topic docs with the new compact rows
- order-composer.md describes the production row's apply-gate (two
  selects + ✓/✗) and the click-to-edit entry point for planetRename.
- cargo-routes-ux.md replaces the four-slot grid description with the
  new single-row dropdown + contextual actions and notes the
  "stays on the picked type" UX rule.
- science-designer-ux.md updates the production-picker integration
  description to the dropdown pair and refreshes the e2e walkthrough
  step.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 13:41:44 +02:00

9.2 KiB
Raw Blame History

Cargo routes UX

This document covers the cargo-route surface: the inspector subsection (a single-row dropdown + contextual actions after F8-05), 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 engine semantics are quoted from game/rules.txt section "Грузовые маршруты" (lines 808843); 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.ShortDistanceRace.FlightDistance()), and a route whose destination becomes unreachable at the next turn is auto-removed (RemoveUnreachableRoutes).

Single-row inspector subsection

The cargo-routes subsection renders below the production controls on every owned planet inspector. F8-05 collapsed the previous four-slot grid into a single <select> that lists the load-types in CARGO_LOAD_TYPE_VALUES order (COL, CAP, MAT, EMP) — same order as the engine's load priority — preceded by a placeholder option that absorbs the old "cargo routes" section title. Nothing else is rendered until the player picks a type; after a pick the dropdown stays on the chosen type and the row reveals one of two states:

  • Empty — 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). After a successful pick or remove the dropdown deliberately stays on the just-acted type so the player sees the result of the gesture in place.

Disabled state: the dropdown and every action button are 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.

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 (visual refinements are deferred to the finalization plan (../PLAN-finalize.md)):

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 deferral note in calc-bridge.md. The formula is trivial (tech × 40) and the WASM glue would be premature infrastructure; when the calc bridge 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.tsCargoLoadType, 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.tsroutes 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.