Files
galaxy-game/ui/frontend/tests/reach-circles.test.ts
T
Ilia Denisov 9ae7b88b89
Tests · UI / test (push) Successful in 2m14s
Tests · Go / test (push) Successful in 2m25s
feat(ui): Phase 30 ship-class calculator with goal-seek and reach circles
Fuse the standalone ship-class designer (Phases 17/18) into a sidebar calculator: live mass/speed/attack/defence/bombing results, a planet build-rate readout, single-target goal-seek, a modernization-cost mode, and auto reach circles on the map for the selected planet.

pkg/calc becomes the single source for the new math (no mirroring): extract BombingPower from the engine model and the per-turn ship-production loop from controller.ProduceShip into pkg/calc (engine now delegates), and add inverse goal-seek solvers in pkg/calc/solve.go. Thin-bridge the combat, planet-build, and solver functions through ui/core/calc + ui/wasm and rebuild core.wasm.

Remove the standalone designer view/route; the ship-classes table and the view/bottom menus open the calculator via a shared request store.

Docs: rewrite ui/PLAN.md Phase 30, adjust Phase 34 (realistic forecast + CAP/COL ownership), add ui/docs/calculator-ux.md, extend calc-bridge.md, fix navigation.md; remove ui/CALCULATOR.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 20:04:07 +02:00

56 lines
1.8 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.
import { describe, expect, test } from "vitest";
import {
computeReachCircles,
reachBound,
REACH_CIRCLE_ID_PREFIX,
} from "../src/map/reach-circles";
const CENTER = { x: 500, y: 500 };
describe("computeReachCircles", () => {
test("no circles for a non-positive speed", () => {
expect(computeReachCircles(CENTER, 0, 1000, 1000, "torus")).toEqual([]);
expect(computeReachCircles(CENTER, -5, 1000, 1000, "torus")).toEqual([]);
});
test("torus: a slow ship shows all three rings", () => {
// bound = min(1000,1000)/2 = 500; speed 100 keeps every ring inside.
const circles = computeReachCircles(CENTER, 100, 1000, 1000, "torus");
expect(circles.map((c) => c.radius)).toEqual([100, 200, 300]);
expect(circles[0].id).toBe(REACH_CIRCLE_ID_PREFIX + 1);
expect(circles[0].style.strokeColor).toBeDefined();
});
test("torus: a ship reaching the wrap midpoint shows one ring", () => {
// speed 500 hits the bound on turn 1, so turn 2 is dropped.
const circles = computeReachCircles(CENTER, 500, 1000, 1000, "torus");
expect(circles).toHaveLength(1);
expect(circles[0].radius).toBe(500);
});
test("torus: a mid-speed ship shows two rings", () => {
// speed 300: ring 1 = 300 (< 500), ring 2 = 600; ring 3 dropped
// because 2 × 300 = 600 ≥ 500.
const circles = computeReachCircles(CENTER, 300, 1000, 1000, "torus");
expect(circles.map((c) => c.radius)).toEqual([300, 600]);
});
test("no-wrap: the bound is the farthest corner", () => {
// origin at a corner → farthest corner is the diagonal.
expect(reachBound({ x: 0, y: 0 }, 1000, 1000, "no-wrap")).toBeCloseTo(
Math.hypot(1000, 1000),
6,
);
const circles = computeReachCircles(
{ x: 0, y: 0 },
500,
1000,
1000,
"no-wrap",
);
// bound ≈ 1414, so all three rings fit.
expect(circles.map((c) => c.radius)).toEqual([500, 1000, 1500]);
});
});