Files
galaxy-game/ui/frontend/tests/inspector-ship-group-modernize-cost.test.ts
T
Ilia Denisov 9ae7b88b89
Tests · UI / test (push) Successful in 2m14s
Tests · Go / test (push) Successful in 2m25s
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>
2026-05-21 20:04:07 +02:00

184 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Vitest coverage for the Phase 20 modernize cost preview. The
// preview line in the inspector calls `core.blockUpgradeCost` once
// per ship block and multiplies the per-ship total by the number of
// targeted ships. The preview hides when `Core` is unavailable; when
// `tech === "ALL"` the targets are the player's race tech levels;
// otherwise only the picked block contributes to the cost.
import "@testing-library/jest-dom/vitest";
import "fake-indexeddb/auto";
import { fireEvent, render } from "@testing-library/svelte";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { i18n } from "../src/lib/i18n/index.svelte";
import type {
ReportLocalShipGroup,
ReportPlanet,
ShipClassSummary,
} from "../src/api/game-state";
import ShipGroup, {
type ShipGroupSelection,
} from "../src/lib/inspectors/ship-group.svelte";
import {
ORDER_DRAFT_CONTEXT_KEY,
OrderDraftStore,
} from "../src/sync/order-draft.svelte";
import { CORE_CONTEXT_KEY, CoreHolder } from "../src/lib/core-context.svelte";
import type { Core } from "../src/platform/core/index";
import { makeFakeCore } from "./fake-core";
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";
import type { IDBPDatabase } from "idb";
const GAME_ID = "11111111-2222-3333-4444-555555555555";
let db: IDBPDatabase<GalaxyDB>;
let dbName: string;
let cache: Cache;
let draft: OrderDraftStore;
const PLANETS: ReportPlanet[] = [
{
number: 17,
name: "Castle",
x: 100,
y: 100,
kind: "local",
owner: null,
size: 1000,
resources: 5,
industryStockpile: 0,
materialsStockpile: 0,
industry: 1000,
population: 1000,
colonists: 0,
production: "Capital",
freeIndustry: 1000,
},
];
const SHIP_CLASS_CRUISER: ShipClassSummary = {
name: "Cruiser",
drive: 5,
armament: 0,
weapons: 0,
shields: 5,
cargo: 5,
};
beforeEach(async () => {
dbName = `galaxy-ship-group-modernize-${crypto.randomUUID()}`;
db = await openGalaxyDB(dbName);
cache = new IDBCache(db);
draft = new OrderDraftStore();
await draft.init({ cache, gameId: GAME_ID });
i18n.resetForTests("en");
});
afterEach(async () => {
draft.dispose();
db.close();
await new Promise<void>((resolve) => {
const req = indexedDB.deleteDatabase(dbName);
req.onsuccess = () => resolve();
req.onerror = () => resolve();
req.onblocked = () => resolve();
});
});
function group(
overrides: Partial<ReportLocalShipGroup> = {},
): ReportLocalShipGroup {
return {
id: "cccccccc-cccc-cccc-cccc-cccccccccccc",
count: 4,
class: "Cruiser",
tech: { drive: 1, weapons: 0, shields: 1, cargo: 1 },
cargo: "NONE",
load: 0,
destination: 17,
origin: null,
range: null,
speed: 0,
mass: 25,
state: "In_Orbit",
fleet: null,
...overrides,
};
}
// stubCore mirrors `pkg/calc` exactly (via the shared makeFakeCore) so
// the preview line shows the same number the WASM bridge would produce.
// The modernize preview only consults `weaponsBlockMass` (returns null
// when armament is zero) and `blockUpgradeCost`.
function stubCore(): Core {
return makeFakeCore();
}
function mount(
g: ReportLocalShipGroup,
options: { core?: Core | null } = {},
) {
const selection: ShipGroupSelection = { variant: "local", group: g };
const holder = new CoreHolder();
if (options.core !== undefined) holder.set(options.core);
const context = new Map<unknown, unknown>([
[ORDER_DRAFT_CONTEXT_KEY, draft],
[CORE_CONTEXT_KEY, holder],
]);
return render(ShipGroup, {
props: {
selection,
planets: PLANETS,
localShipClass: [SHIP_CLASS_CRUISER],
localFleets: [],
otherRaces: [],
mapWidth: 1000,
mapHeight: 1000,
localPlayerDrive: 2,
localPlayerWeapons: 2,
localPlayerShields: 2,
localPlayerCargo: 2,
},
context,
});
}
describe("ship-group inspector — modernize cost preview", () => {
test("ALL upgrade preview matches the BlockUpgradeCost formula × ship count", async () => {
// drive: mass=5 current=1 target=2 → (1 - 0.5) * 10 * 5 = 25
// shields: mass=5 current=1 target=2 → 25
// cargo: mass=5 current=1 target=2 → 25
// weapons: armament=0 weapons=0 → block mass 0 → 0
// per-ship = 75; group of 4 → 300
const ui = mount(group(), { core: stubCore() });
await fireEvent.click(ui.getByTestId("inspector-ship-group-action-modernize"));
const preview = ui.getByTestId("inspector-ship-group-form-modernize-cost");
expect(preview).toHaveTextContent("300");
});
test("per-block tech with custom level uses only that block", async () => {
// DRIVE only, target=2: 25 per ship × 4 = 100.
const ui = mount(group(), { core: stubCore() });
await fireEvent.click(ui.getByTestId("inspector-ship-group-action-modernize"));
await fireEvent.change(
ui.getByTestId("inspector-ship-group-form-modernize-tech"),
{ target: { value: "DRIVE" } },
);
await fireEvent.input(
ui.getByTestId("inspector-ship-group-form-modernize-level"),
{ target: { value: "2" } },
);
const preview = ui.getByTestId("inspector-ship-group-form-modernize-cost");
expect(preview).toHaveTextContent("100");
});
test("preview is unavailable when Core is not loaded", async () => {
const ui = mount(group(), { core: null });
await fireEvent.click(ui.getByTestId("inspector-ship-group-action-modernize"));
const preview = ui.getByTestId("inspector-ship-group-form-modernize-cost");
expect(preview).toHaveTextContent(/preview unavailable/i);
});
});