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:
@@ -197,6 +197,137 @@ describe("OrderDraftStore", () => {
|
||||
store.dispose();
|
||||
});
|
||||
|
||||
test("setProductionType validates locally per the engine's subject rule", async () => {
|
||||
const store = new OrderDraftStore();
|
||||
await store.init({ cache, gameId: GAME_ID });
|
||||
await store.add({
|
||||
kind: "setProductionType",
|
||||
id: "cap",
|
||||
planetNumber: 1,
|
||||
productionType: "CAP",
|
||||
subject: "",
|
||||
});
|
||||
await store.add({
|
||||
kind: "setProductionType",
|
||||
id: "drive",
|
||||
planetNumber: 2,
|
||||
productionType: "DRIVE",
|
||||
subject: "",
|
||||
});
|
||||
await store.add({
|
||||
kind: "setProductionType",
|
||||
id: "ship-ok",
|
||||
planetNumber: 3,
|
||||
productionType: "SHIP",
|
||||
subject: "Scout",
|
||||
});
|
||||
await store.add({
|
||||
kind: "setProductionType",
|
||||
id: "ship-empty",
|
||||
planetNumber: 4,
|
||||
productionType: "SHIP",
|
||||
subject: "",
|
||||
});
|
||||
await store.add({
|
||||
kind: "setProductionType",
|
||||
id: "science-bad",
|
||||
planetNumber: 5,
|
||||
productionType: "SCIENCE",
|
||||
subject: "Bad Name",
|
||||
});
|
||||
expect(store.statuses["cap"]).toBe("valid");
|
||||
expect(store.statuses["drive"]).toBe("valid");
|
||||
expect(store.statuses["ship-ok"]).toBe("valid");
|
||||
expect(store.statuses["ship-empty"]).toBe("invalid");
|
||||
expect(store.statuses["science-bad"]).toBe("invalid");
|
||||
store.dispose();
|
||||
});
|
||||
|
||||
test("setProductionType collapses to the latest entry per planet", async () => {
|
||||
const store = new OrderDraftStore();
|
||||
await store.init({ cache, gameId: GAME_ID });
|
||||
await store.add({
|
||||
kind: "setProductionType",
|
||||
id: "first",
|
||||
planetNumber: 7,
|
||||
productionType: "CAP",
|
||||
subject: "",
|
||||
});
|
||||
await store.add({
|
||||
kind: "setProductionType",
|
||||
id: "second",
|
||||
planetNumber: 7,
|
||||
productionType: "MAT",
|
||||
subject: "",
|
||||
});
|
||||
await store.add({
|
||||
kind: "setProductionType",
|
||||
id: "third",
|
||||
planetNumber: 7,
|
||||
productionType: "DRIVE",
|
||||
subject: "",
|
||||
});
|
||||
expect(store.commands).toHaveLength(1);
|
||||
const only = store.commands[0]!;
|
||||
expect(only.id).toBe("third");
|
||||
if (only.kind !== "setProductionType") {
|
||||
throw new Error("expected setProductionType");
|
||||
}
|
||||
expect(only.productionType).toBe("DRIVE");
|
||||
// Old ids are scrubbed from statuses so the order tab does not
|
||||
// keep ghost rows.
|
||||
expect(store.statuses["first"]).toBeUndefined();
|
||||
expect(store.statuses["second"]).toBeUndefined();
|
||||
expect(store.statuses["third"]).toBe("valid");
|
||||
store.dispose();
|
||||
});
|
||||
|
||||
test("setProductionType for different planets stay independent", async () => {
|
||||
const store = new OrderDraftStore();
|
||||
await store.init({ cache, gameId: GAME_ID });
|
||||
await store.add({
|
||||
kind: "setProductionType",
|
||||
id: "p7-cap",
|
||||
planetNumber: 7,
|
||||
productionType: "CAP",
|
||||
subject: "",
|
||||
});
|
||||
await store.add({
|
||||
kind: "setProductionType",
|
||||
id: "p9-mat",
|
||||
planetNumber: 9,
|
||||
productionType: "MAT",
|
||||
subject: "",
|
||||
});
|
||||
expect(store.commands.map((c) => c.id)).toEqual([
|
||||
"p7-cap",
|
||||
"p9-mat",
|
||||
]);
|
||||
store.dispose();
|
||||
});
|
||||
|
||||
test("planetRename and setProductionType on the same planet keep both", async () => {
|
||||
const store = new OrderDraftStore();
|
||||
await store.init({ cache, gameId: GAME_ID });
|
||||
await store.add({
|
||||
kind: "planetRename",
|
||||
id: "ren",
|
||||
planetNumber: 7,
|
||||
name: "Earth",
|
||||
});
|
||||
await store.add({
|
||||
kind: "setProductionType",
|
||||
id: "prod",
|
||||
planetNumber: 7,
|
||||
productionType: "CAP",
|
||||
subject: "",
|
||||
});
|
||||
expect(store.commands.map((c) => c.id)).toEqual(["ren", "prod"]);
|
||||
expect(store.statuses["ren"]).toBe("valid");
|
||||
expect(store.statuses["prod"]).toBe("valid");
|
||||
store.dispose();
|
||||
});
|
||||
|
||||
test("hydrateFromServer overwrites the local cache with the server snapshot", async () => {
|
||||
const { fakeFetchClient } = await import("./helpers/fake-order-client");
|
||||
const { client } = fakeFetchClient(GAME_ID, [
|
||||
|
||||
Reference in New Issue
Block a user