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
+73
View File
@@ -0,0 +1,73 @@
// Smoke test for the Phase 30 calculator bridge against the real
// (TinyGo-built) core.wasm. The calc-model and component suites use a
// fake Core; this file boots the actual WASM module to confirm every new
// function is registered in `ui/wasm/main.go` and marshals correctly —
// including the object return of `produceShipsInTurn` and the `null`
// infeasible result of the solvers. Requires `make wasm` to have run.
import { beforeAll, describe, expect, test } from "vitest";
import type { Core } from "../src/platform/core/index";
import { loadWasmCoreForTest } from "./setup-wasm";
let core: Core;
beforeAll(async () => {
core = await loadWasmCoreForTest();
});
describe("WasmCore calculator bridge (Phase 30)", () => {
test("combat results", () => {
expect(core.effectiveAttack({ weapons: 15, weaponsTech: 1.5 })).toBeCloseTo(
22.5,
9,
);
expect(
core.effectiveDefence({ shields: 20, shieldsTech: 1, fullMass: 45 }),
).toBeCloseTo((20 / Math.cbrt(45)) * Math.cbrt(30), 6);
expect(
core.bombingPower({ weapons: 30, weaponsTech: 1, armament: 3, number: 1 }),
).toBeCloseTo(139.29503, 3);
});
test("planet build", () => {
expect(
core.shipBuildCost({ shipMass: 10, material: 3, resources: 0.5 }),
).toBeCloseTo(114, 9);
const r = core.produceShipsInTurn({
productionAvailable: 100,
material: 100,
resources: 10,
shipMass: 1,
});
expect(r).toEqual({
ships: 10,
materialLeft: 90,
productionUsed: 0,
progress: 0,
});
});
test("goal-seek solvers, including infeasible", () => {
expect(
core.weaponsForAttack({ targetAttack: 30, weaponsTech: 1.5 }),
).toBeCloseTo(20, 9);
expect(
core.cargoForEmptyMass({ targetEmptyMass: 42, restMass: 30 }),
).toBeCloseTo(12, 9);
expect(
core.loadForFullMass({ targetFullMass: 65, emptyMass: 45, cargoTech: 1 }),
).toBeCloseTo(20, 9);
const shields = core.shieldsForDefence({
targetDefence: 5,
shieldsTech: 1,
restMass: 40,
});
expect(shields).not.toBeNull();
expect(shields as number).toBeGreaterThan(0);
// Speed at/above the stripped-hull ceiling (20 × driveTech) is
// unreachable: the bridge returns null.
expect(
core.driveForSpeed({ targetSpeed: 100, driveTech: 1.2, restMass: 35 }),
).toBeNull();
});
});