Files
galaxy-game/ui/frontend/src/map/reach-circles.ts
T
Ilia Denisov f6e4a4f6bd
Tests · UI / test (push) Waiting to run
Tests · UI / test (pull_request) Successful in 2m45s
feat(ui): map canvas follows light/dark theme; fix invisible gear control
The map view now selects a DARK_THEME or LIGHT_THEME palette from the
resolved app theme and threads it through every primitive builder, so
the canvas, planets, ship groups, cargo routes, battle/bombing markers,
fog, reach + selection rings, pending-Send tracks, and the pick overlay
all switch with the rest of the chrome. A theme flip remounts the
renderer preserving the camera — Pixi bakes the background at init and
every primitive bakes its colour at build, so a live re-tint is not
possible on the same instance.

This also fixes the reported bug: the gear-popover trigger and the
loading overlay hardcoded a dark navy background, so in light theme the
gear was invisible (dark icon on dark chip) until hover flipped it to a
white chip. Both now use the --color-surface-overlay token and read
correctly in both themes.

The light palette mirrors the dark one role-for-role, darkened /
saturated for contrast on a light background while keeping the incoming,
battle, and bombing accents vivid. The values are a first pass meant to
be refined during the F8 manual-QA loop.

Removes the now-dead "Phase 35" references from the code and lifts the
map-recoloring prohibition from the design-system / renderer docs; the
battle scene stays a fixed-palette data-viz surface.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 08:49:37 +02:00

83 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Phase 30 reach circles. When the ship-class calculator has a planet
// selected and a valid design, it publishes the design's loaded speed and
// the planet origin to `lib/calculator/reach.svelte`; the map view reads
// that store and feeds it through `computeReachCircles` to draw 13 thin
// concentric rings showing how far the ship reaches in 1, 2, and 3 turns.
//
// The ring count is bounded by how soon a ring reaches the meaningful
// extent of the map: half the shorter side on a torus (beyond that a
// ring wraps onto itself), or the farthest corner on a bounded no-wrap
// plane (beyond that the ring is entirely off-map). A fast ship that
// clears the map in one turn therefore shows a single ring; a slow ship
// shows all three.
import { DARK_THEME, type CirclePrim, type Theme } from "./world";
/** High-bit prefix so reach-circle ids never collide with planet
* numbers, cargo-route lines, or battle/bombing markers. */
export const REACH_CIRCLE_ID_PREFIX = 0xb0000000;
const MAX_TURNS = 3;
/** Reach rings sit below every interactive primitive so they never win
* a click against a planet or ship group. */
const REACH_CIRCLE_PRIORITY = 0;
/**
* reachBound returns the largest ring radius worth drawing for the map.
* On a torus it is half the shorter side (a larger ring overlaps itself);
* on a bounded plane it is the distance from the origin to the farthest
* corner (a larger ring is wholly off-map).
*/
export function reachBound(
origin: { x: number; y: number },
mapWidth: number,
mapHeight: number,
mode: "torus" | "no-wrap",
): number {
if (mode === "torus") {
return Math.min(mapWidth, mapHeight) / 2;
}
const dx = Math.max(origin.x, mapWidth - origin.x);
const dy = Math.max(origin.y, mapHeight - origin.y);
return Math.hypot(dx, dy);
}
/**
* computeReachCircles produces up to three concentric ring primitives
* centred on `origin`, with radii speedPerTurn × {1, 2, 3}. A ring for
* turn `t` is included only when the previous ring still fits inside the
* map's reach bound, so the count shrinks as the per-turn speed grows.
* Returns an empty list when the speed is non-positive. `theme`
* supplies the ring colour and defaults to `DARK_THEME`.
*/
export function computeReachCircles(
origin: { x: number; y: number },
speedPerTurn: number,
mapWidth: number,
mapHeight: number,
mode: "torus" | "no-wrap",
theme: Theme = DARK_THEME,
): CirclePrim[] {
if (speedPerTurn <= 0) return [];
const bound = reachBound(origin, mapWidth, mapHeight, mode);
const circles: CirclePrim[] = [];
for (let turn = 1; turn <= MAX_TURNS; turn++) {
// Stop once the previous ring already reached the bound.
if (turn > 1 && speedPerTurn * (turn - 1) >= bound) break;
circles.push({
kind: "circle",
id: REACH_CIRCLE_ID_PREFIX + turn,
priority: REACH_CIRCLE_PRIORITY,
hitSlopPx: 0,
x: origin.x,
y: origin.y,
radius: speedPerTurn * turn,
style: {
strokeColor: theme.reachCircle,
strokeAlpha: 0.55 - (turn - 1) * 0.12,
strokeWidthPx: 0.5,
},
});
}
return circles;
}