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>
This commit is contained in:
@@ -23,6 +23,7 @@ import {
|
||||
OrderDraftStore,
|
||||
} from "../src/sync/order-draft.svelte";
|
||||
import { RENDERED_REPORT_CONTEXT_KEY } from "../src/lib/rendered-report.svelte";
|
||||
import { calculatorLoadRequest } from "../src/lib/calculator/load-request.svelte";
|
||||
import { IDBCache } from "../src/platform/store/idb-cache";
|
||||
import { openGalaxyDB, type GalaxyDB } from "../src/platform/store/idb";
|
||||
import type { Cache } from "../src/platform/store/index";
|
||||
@@ -188,12 +189,14 @@ describe("ship-classes table", () => {
|
||||
expect(names).toEqual(["Battleship", "Cruiser", "Drone"]);
|
||||
});
|
||||
|
||||
test("dblclick on a row navigates to the designer for that class", async () => {
|
||||
test("dblclick on a row requests the calculator for that class", async () => {
|
||||
const ui = mountTable(
|
||||
makeReport([shipClass({ name: "Drone", drive: 1 })]),
|
||||
);
|
||||
const before = calculatorLoadRequest.token;
|
||||
await fireEvent.dblClick(ui.getByTestId("ship-classes-row"));
|
||||
expect(gotoMock).toHaveBeenCalledWith("/games/g1/designer/ship-class/Drone");
|
||||
expect(calculatorLoadRequest.token).toBe(before + 1);
|
||||
expect(calculatorLoadRequest.name).toBe("Drone");
|
||||
});
|
||||
|
||||
test("delete button adds a removeShipClass to the draft", async () => {
|
||||
@@ -207,9 +210,11 @@ describe("ship-classes table", () => {
|
||||
expect(cmd.name).toBe("Drone");
|
||||
});
|
||||
|
||||
test("new button navigates to the empty designer", async () => {
|
||||
test("new button requests a fresh calculator design", async () => {
|
||||
const ui = mountTable(makeReport([]));
|
||||
const before = calculatorLoadRequest.token;
|
||||
await fireEvent.click(ui.getByTestId("ship-classes-new"));
|
||||
expect(gotoMock).toHaveBeenCalledWith("/games/g1/designer/ship-class");
|
||||
expect(calculatorLoadRequest.token).toBe(before + 1);
|
||||
expect(calculatorLoadRequest.name).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user