// 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 1–3 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; }