ui/phase-16: pick any planet in reach + stronger pick-mode dim

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>
This commit is contained in:
Ilia Denisov
2026-05-09 20:48:42 +02:00
parent 3442dc94f7
commit 8a236bef14
8 changed files with 164 additions and 16 deletions
@@ -104,11 +104,18 @@ The component is purposely deferential to the existing infrastructure:
const reach = $derived(40 * localPlayerDrive);
function reachableSet(): Set<number> {
// The engine accepts a route from a player-owned planet to any
// planet inside the source's flight distance — own, foreign,
// uninhabited, and unidentified all qualify (`game/internal/
// controller/route.go.PlanetRouteSet` only checks ownership of
// the origin and `util.ShortDistance(...) <= FligthDistance`,
// see `pkg/calc/race.go`). The picker mirrors that contract;
// only the source itself is excluded so a self-route cannot be
// emitted.
const ids = new Set<number>();
if (reach <= 0) return ids;
for (const candidate of planets) {
if (candidate.number === planet.number) continue;
if (candidate.kind === "unidentified") continue;
const dx = torusShortestDelta(planet.x, candidate.x, mapWidth);
const dy = torusShortestDelta(planet.y, candidate.y, mapHeight);
if (Math.hypot(dx, dy) <= reach) {
+11 -1
View File
@@ -151,10 +151,20 @@ export function computePickOverlay(
* PICK_OVERLAY_STYLE captures the colours / widths the renderer
* applies to each spec channel. Exported so tests and future themes
* can read the same values.
*
* `dimAlpha` and `dimTint` are applied together to non-reachable
* primitives during a pick session: the alpha drops their
* brightness, and the tint multiplies their fill colour toward dark
* gray so the colour identity (planet kind) collapses into a
* single muted shade. The combination has to read as "obviously
* disabled" against the dark theme — bright planets such as
* `STYLE_LOCAL` (`0x6dd2ff`) survive a 0.3 alpha alone too
* comfortably, so the tint pulls them down too.
*/
export const PICK_OVERLAY_STYLE = {
anchor: { color: 0xffe082, alpha: 0.9, width: 2 },
line: { color: 0xffe082, alpha: 0.5, width: 1 },
hover: { color: 0xffe082, alpha: 1, width: 2 },
dimAlpha: 0.3,
dimAlpha: 0.35,
dimTint: 0x303841,
} as const;
+5
View File
@@ -380,6 +380,7 @@ export async function createRenderer(opts: RendererOptions): Promise<RendererHan
let pickOptions: PickModeOptions | null = null;
let pickOverlay: Graphics | null = null;
const dimmedAlphaBackup = new Map<Graphics, number>();
const dimmedTintBackup = new Map<Graphics, number>();
const detachPickListeners: Array<() => void> = [];
const handleViewportClicked = (e: {
@@ -462,6 +463,8 @@ export async function createRenderer(opts: RendererOptions): Promise<RendererHan
detachPickListeners.length = 0;
for (const [g, alpha] of dimmedAlphaBackup) g.alpha = alpha;
dimmedAlphaBackup.clear();
for (const [g, tint] of dimmedTintBackup) g.tint = tint;
dimmedTintBackup.clear();
if (pickOverlay !== null) {
pickOverlay.destroy();
pickOverlay = null;
@@ -484,7 +487,9 @@ export async function createRenderer(opts: RendererOptions): Promise<RendererHan
if (options.reachableIds.has(id)) continue;
for (const g of list) {
dimmedAlphaBackup.set(g, g.alpha);
dimmedTintBackup.set(g, g.tint as number);
g.alpha = PICK_OVERLAY_STYLE.dimAlpha;
g.tint = PICK_OVERLAY_STYLE.dimTint;
}
}
// Overlay graphic. Lives in the origin copy so the central