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
@@ -14,8 +14,6 @@ data fetching is performed here — the layout is responsible.
-->
<script lang="ts">
import { getContext } from "svelte";
import { goto } from "$app/navigation";
import { page } from "$app/state";
import type { ShipClassSummary } from "../../api/game-state";
import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
@@ -27,6 +25,7 @@ data fetching is performed here — the layout is responsible.
ORDER_DRAFT_CONTEXT_KEY,
OrderDraftStore,
} from "../../sync/order-draft.svelte";
import { calculatorLoadRequest } from "$lib/calculator/load-request.svelte";
type SortColumn =
| "name"
@@ -62,7 +61,6 @@ data fetching is performed here — the layout is responsible.
ORDER_DRAFT_CONTEXT_KEY,
);
const gameId = $derived(page.params.id ?? "");
let sortColumn: SortColumn = $state("name");
let sortDirection: SortDirection = $state("asc");
@@ -111,14 +109,12 @@ data fetching is performed here — the layout is responsible.
return value.toLocaleString(undefined, { maximumFractionDigits: 2 });
}
function openDesigner(name: string): void {
void goto(
`/games/${gameId}/designer/ship-class/${encodeURIComponent(name)}`,
);
function openInCalculator(name: string): void {
calculatorLoadRequest.request(name);
}
function newShipClass(): void {
void goto(`/games/${gameId}/designer/ship-class`);
calculatorLoadRequest.request(null);
}
async function deleteShipClass(name: string): Promise<void> {
@@ -194,7 +190,7 @@ data fetching is performed here — the layout is responsible.
<tr
data-testid="ship-classes-row"
data-name={cls.name}
ondblclick={() => openDesigner(cls.name)}
ondblclick={() => openInCalculator(cls.name)}
>
<td data-testid="ship-classes-cell-name">{cls.name}</td>
<td data-testid="ship-classes-cell-drive">{formatNumber(cls.drive)}</td>