ui/phase-15: planet inspector production controls + order-draft collapse
Adds the second end-to-end command (`setProductionType`) with a collapse-by-`planetNumber` rule on the order draft, the segmented production-controls component on the planet inspector, the FBS encoder/decoder pair for `CommandPlanetProduce`, and the `localShipClass` projection on `GameReport`. Forecast number is deferred and tracked in the new `ui/docs/calc-bridge.md`.
This commit is contained in:
@@ -1,16 +1,22 @@
|
||||
// Vitest unit coverage for the pure `applyOrderOverlay` projection.
|
||||
// Phase 14 understands `planetRename` only; future phases (set
|
||||
// production, route updates) will extend the overlay and need
|
||||
// equivalent cases here.
|
||||
// Phase 14 introduced the overlay for `planetRename`; Phase 15
|
||||
// extends it to `setProductionType` and shares the same eligibility
|
||||
// rule. Future phases (route updates, etc.) will extend the overlay
|
||||
// and need equivalent cases here.
|
||||
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import {
|
||||
applyOrderOverlay,
|
||||
productionDisplayFromCommand,
|
||||
type GameReport,
|
||||
type ReportPlanet,
|
||||
} from "../src/api/game-state";
|
||||
import type { CommandStatus, OrderCommand } from "../src/sync/order-types";
|
||||
import type {
|
||||
CommandStatus,
|
||||
OrderCommand,
|
||||
ProductionType,
|
||||
} from "../src/sync/order-types";
|
||||
|
||||
function makePlanet(overrides: Partial<ReportPlanet>): ReportPlanet {
|
||||
return {
|
||||
@@ -41,6 +47,7 @@ function makeReport(planets: ReportPlanet[]): GameReport {
|
||||
planetCount: planets.length,
|
||||
planets,
|
||||
race: "",
|
||||
localShipClass: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -153,4 +160,117 @@ describe("applyOrderOverlay", () => {
|
||||
});
|
||||
expect(out.planets[0]!.name).toBe("Final");
|
||||
});
|
||||
|
||||
test("setProductionType rewrites planet.production for valid statuses", () => {
|
||||
const report = makeReport([
|
||||
makePlanet({ number: 1, name: "Earth", production: "Capital" }),
|
||||
]);
|
||||
const cmd: OrderCommand = {
|
||||
kind: "setProductionType",
|
||||
id: "cmd-1",
|
||||
planetNumber: 1,
|
||||
productionType: "DRIVE",
|
||||
subject: "",
|
||||
};
|
||||
for (const status of ["valid", "submitting", "applied"] as const) {
|
||||
const out = applyOrderOverlay(report, [cmd], { "cmd-1": status });
|
||||
expect(out.planets[0]!.production).toBe("Drive");
|
||||
}
|
||||
});
|
||||
|
||||
test("setProductionType skips draft / invalid / rejected statuses", () => {
|
||||
const report = makeReport([
|
||||
makePlanet({ number: 1, name: "Earth", production: "Capital" }),
|
||||
]);
|
||||
const cmd: OrderCommand = {
|
||||
kind: "setProductionType",
|
||||
id: "cmd-1",
|
||||
planetNumber: 1,
|
||||
productionType: "DRIVE",
|
||||
subject: "",
|
||||
};
|
||||
for (const status of ["draft", "invalid", "rejected"] as const) {
|
||||
const out = applyOrderOverlay(report, [cmd], { "cmd-1": status });
|
||||
expect(out.planets[0]!.production).toBe("Capital");
|
||||
}
|
||||
});
|
||||
|
||||
test("setProductionType applied with subject mirrors the engine's display", () => {
|
||||
const report = makeReport([
|
||||
makePlanet({ number: 1, name: "Earth", production: "Capital" }),
|
||||
]);
|
||||
const cmd: OrderCommand = {
|
||||
kind: "setProductionType",
|
||||
id: "cmd-1",
|
||||
planetNumber: 1,
|
||||
productionType: "SHIP",
|
||||
subject: "Scout",
|
||||
};
|
||||
const out = applyOrderOverlay(report, [cmd], { "cmd-1": "applied" });
|
||||
expect(out.planets[0]!.production).toBe("Scout");
|
||||
});
|
||||
|
||||
test("setProductionType + planetRename for the same planet compose", () => {
|
||||
const report = makeReport([
|
||||
makePlanet({ number: 1, name: "Earth", production: "Capital" }),
|
||||
]);
|
||||
const rename: OrderCommand = {
|
||||
kind: "planetRename",
|
||||
id: "cmd-rename",
|
||||
planetNumber: 1,
|
||||
name: "New-Earth",
|
||||
};
|
||||
const setProd: OrderCommand = {
|
||||
kind: "setProductionType",
|
||||
id: "cmd-prod",
|
||||
planetNumber: 1,
|
||||
productionType: "DRIVE",
|
||||
subject: "",
|
||||
};
|
||||
const out = applyOrderOverlay(report, [rename, setProd], {
|
||||
"cmd-rename": "applied",
|
||||
"cmd-prod": "applied",
|
||||
});
|
||||
expect(out.planets[0]!.name).toBe("New-Earth");
|
||||
expect(out.planets[0]!.production).toBe("Drive");
|
||||
});
|
||||
|
||||
test("ignores setProductionType for missing planet (visibility lost)", () => {
|
||||
const report = makeReport([
|
||||
makePlanet({ number: 1, name: "Earth", production: "Capital" }),
|
||||
]);
|
||||
const cmd: OrderCommand = {
|
||||
kind: "setProductionType",
|
||||
id: "cmd-1",
|
||||
planetNumber: 99,
|
||||
productionType: "DRIVE",
|
||||
subject: "",
|
||||
};
|
||||
const out = applyOrderOverlay(report, [cmd], { "cmd-1": "applied" });
|
||||
expect(out).toBe(report);
|
||||
});
|
||||
});
|
||||
|
||||
describe("productionDisplayFromCommand", () => {
|
||||
const cases: ReadonlyArray<{
|
||||
productionType: ProductionType;
|
||||
subject: string;
|
||||
expected: string;
|
||||
}> = [
|
||||
{ productionType: "MAT", subject: "", expected: "Material" },
|
||||
{ productionType: "CAP", subject: "", expected: "Capital" },
|
||||
{ productionType: "DRIVE", subject: "", expected: "Drive" },
|
||||
{ productionType: "WEAPONS", subject: "", expected: "Weapons" },
|
||||
{ productionType: "SHIELDS", subject: "", expected: "Shields" },
|
||||
{ productionType: "CARGO", subject: "", expected: "Cargo" },
|
||||
{ productionType: "SCIENCE", subject: "AlphaSci", expected: "AlphaSci" },
|
||||
{ productionType: "SHIP", subject: "Scout", expected: "Scout" },
|
||||
];
|
||||
for (const tc of cases) {
|
||||
test(`${tc.productionType} → ${tc.expected}`, () => {
|
||||
expect(productionDisplayFromCommand(tc.productionType, tc.subject)).toBe(
|
||||
tc.expected,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user