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:
@@ -227,3 +227,54 @@ test("clicking a planet on mobile raises the bottom-sheet, close clears it", asy
|
||||
await page.getByTestId("inspector-planet-sheet-close").click();
|
||||
await expect(page.getByTestId("inspector-planet-sheet")).toHaveCount(0);
|
||||
});
|
||||
|
||||
// Counts reach-circle primitives off the renderer debug surface. Reach
|
||||
// circles use ids in [REACH_CIRCLE_ID_PREFIX, bombing-marker prefix) —
|
||||
// 0xb0000000..0xc0000000 (see `map/reach-circles.ts`).
|
||||
async function countReachCircles(page: Page): Promise<number> {
|
||||
return page.evaluate(() => {
|
||||
const surface = (
|
||||
window as unknown as {
|
||||
__galaxyDebug?: {
|
||||
getMapPrimitives?: () => readonly { id: number; kind: string }[];
|
||||
};
|
||||
}
|
||||
).__galaxyDebug;
|
||||
const prims = surface?.getMapPrimitives?.() ?? [];
|
||||
return prims.filter(
|
||||
(p) => p.kind === "circle" && p.id >= 0xb0000000 && p.id < 0xc0000000,
|
||||
).length;
|
||||
});
|
||||
}
|
||||
|
||||
test("calculator draws reach circles for the selected planet", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
test.skip(
|
||||
testInfo.project.name.startsWith("chromium-mobile"),
|
||||
"calculator + reach circles are a desktop-sidebar flow",
|
||||
);
|
||||
await setupShell(page);
|
||||
|
||||
// No reach circles before a planet is selected and a design exists.
|
||||
expect(await countReachCircles(page)).toBe(0);
|
||||
|
||||
// Select the planet, then switch the sidebar to the calculator.
|
||||
await clickCanvasCentre(page);
|
||||
await page.getByTestId("sidebar-tab-calculator").click();
|
||||
const calc = page.getByTestId("sidebar-tool-calculator");
|
||||
await expect(calc).toBeVisible();
|
||||
|
||||
// A valid design with a positive drive tech override yields a
|
||||
// positive loaded speed, which the calculator publishes to the map.
|
||||
await calc.getByTestId("calculator-block-drive").fill("10");
|
||||
await calc.getByTestId("calculator-block-shields").fill("5");
|
||||
await calc.getByTestId("calculator-block-cargo").fill("5");
|
||||
await calc.getByTestId("calculator-tech-drive").fill("1.2");
|
||||
|
||||
await expect.poll(() => countReachCircles(page)).toBeGreaterThan(0);
|
||||
|
||||
// Leaving ship mode clears the published reach, so the rings drop.
|
||||
await calc.getByTestId("calculator-mode-modernization").click();
|
||||
await expect.poll(() => countReachCircles(page)).toBe(0);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user