feat(ui): Phase 30 ship-class calculator with goal-seek and reach circles
Tests · UI / test (push) Successful in 2m14s
Tests · Go / test (push) Successful in 2m25s

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>
This commit is contained in:
Ilia Denisov
2026-05-21 19:52:08 +02:00
parent 00159ddf7c
commit 9ae7b88b89
53 changed files with 3748 additions and 1298 deletions
+31 -1
View File
@@ -31,6 +31,8 @@ preference the store already manages.
} from "../../map/index";
import { buildCargoRouteLines } from "../../map/cargo-routes";
import { buildPendingSendLines } from "../../map/pending-send-routes";
import { computeReachCircles } from "../../map/reach-circles";
import { reachStore } from "$lib/calculator/reach.svelte";
import {
reportToWorld,
type HitTarget,
@@ -196,6 +198,11 @@ preference the store already manages.
void toggles.bombingMarkers;
void toggles.visibleHyperspace;
// Subscribe to the calculator's published reach so the rings
// redraw as the design or the selected planet changes.
void reachStore.origin;
void reachStore.speedPerTurn;
// Phase 29 visibility derivation. Cargo routes and pending-
// Send overlay are extras (no Pixi remount on flip); the
// cascade-filtering happens here so the extras list shrinks
@@ -219,8 +226,14 @@ preference the store already manages.
// the visible set reliably triggers a push.
const draftCommands = orderDraft?.commands ?? [];
const draftStatuses = orderDraft?.statuses ?? {};
const reachOrigin = reachStore.origin;
const reachFingerprint =
reachOrigin === null
? ""
: `${reachOrigin.x},${reachOrigin.y},${reachStore.speedPerTurn}`;
const extrasFingerprint =
`cr=${toggles.cargoRoutes ? "1" : "0"}|hp=${hiddenPlanetFingerprint}|` +
`reach=${reachFingerprint}|` +
computeRoutesFingerprint(report.routes) +
"|" +
computePendingSendFingerprint(draftCommands, draftStatuses);
@@ -256,6 +269,7 @@ preference the store already manages.
draftStatuses,
toggles,
hiddenPlanetNumbers,
mode,
),
);
});
@@ -289,6 +303,7 @@ preference the store already manages.
draftStatuses: Readonly<Record<string, string>>,
toggles: MapToggles,
hiddenPlanetNumbers: ReadonlySet<number>,
mode: "torus" | "no-wrap",
): import("../../map/world").Primitive[] {
const skip = hiddenPlanetNumbers.size > 0 ? hiddenPlanetNumbers : undefined;
const cargo = toggles.cargoRoutes
@@ -300,7 +315,21 @@ preference the store already manages.
draftStatuses,
skip ? { skipPlanets: skip } : undefined,
);
return [...cargo, ...pending];
// Reach circles published by the ship-class calculator. Empty
// when no own planet is selected or the design is invalid, so
// this is a no-op for the rest of the map.
const reachOrigin = reachStore.origin;
const reach =
reachOrigin !== null && reachStore.speedPerTurn > 0
? computeReachCircles(
reachOrigin,
reachStore.speedPerTurn,
report.mapWidth,
report.mapHeight,
mode,
)
: [];
return [...cargo, ...pending, ...reach];
}
function applyVisibilityState(
@@ -342,6 +371,7 @@ preference the store already manages.
draftStatuses,
toggles,
hiddenPlanetNumbers,
mode,
),
);
lastExtrasFingerprint = extrasFingerprint;