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:
@@ -15,6 +15,11 @@
|
||||
// rename in the local draft swaps the planet name on the rendered
|
||||
// report so the player sees their intent reflected immediately,
|
||||
// without waiting for the next turn cutoff.
|
||||
//
|
||||
// Phase 15 extends the projection with a minimal `localShipClass`
|
||||
// summary so the planet inspector's Build-Ship sub-picker has data
|
||||
// to render. Phase 17 (ship-class CRUD) widens `ShipClassSummary`
|
||||
// when the designer ships need the full attribute set.
|
||||
|
||||
import { Builder, ByteBuffer } from "flatbuffers";
|
||||
|
||||
@@ -24,7 +29,11 @@ import {
|
||||
GameReportRequest,
|
||||
Report,
|
||||
} from "../proto/galaxy/fbs/report";
|
||||
import type { CommandStatus, OrderCommand } from "../sync/order-types";
|
||||
import type {
|
||||
CommandStatus,
|
||||
OrderCommand,
|
||||
ProductionType,
|
||||
} from "../sync/order-types";
|
||||
|
||||
const MESSAGE_TYPE = "user.games.report";
|
||||
|
||||
@@ -61,6 +70,18 @@ export interface ReportPlanet {
|
||||
freeIndustry: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* ShipClassSummary is the slim projection of `report.ShipClass` the
|
||||
* planet inspector's Build-Ship sub-picker needs in Phase 15. Only
|
||||
* the human-visible `name` is carried — the engine command shape
|
||||
* (`CommandPlanetProduce.subject`) takes the class name, not its
|
||||
* underlying tech values. Phase 17 widens this type when the ship
|
||||
* designer needs the full attribute set.
|
||||
*/
|
||||
export interface ShipClassSummary {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface GameReport {
|
||||
turn: number;
|
||||
mapWidth: number;
|
||||
@@ -73,6 +94,14 @@ export interface GameReport {
|
||||
* has not produced a report yet (boot state).
|
||||
*/
|
||||
race: string;
|
||||
/**
|
||||
* localShipClass enumerates the player's own designed ship classes
|
||||
* by name. Empty until at least one class is created
|
||||
* (`CommandShipClassCreate`, Phase 17). The Build-Ship sub-picker
|
||||
* shows a localized "no ship classes" placeholder when this is
|
||||
* empty.
|
||||
*/
|
||||
localShipClass: ShipClassSummary[];
|
||||
}
|
||||
|
||||
export async function fetchGameReport(
|
||||
@@ -189,6 +218,13 @@ function decodeReport(report: Report): GameReport {
|
||||
});
|
||||
}
|
||||
|
||||
const localShipClass: ShipClassSummary[] = [];
|
||||
for (let i = 0; i < report.localShipClassLength(); i++) {
|
||||
const sc = report.localShipClass(i);
|
||||
if (sc === null) continue;
|
||||
localShipClass.push({ name: sc.name() ?? "" });
|
||||
}
|
||||
|
||||
return {
|
||||
turn: Number(report.turn()),
|
||||
mapWidth: report.width(),
|
||||
@@ -196,6 +232,7 @@ function decodeReport(report: Report): GameReport {
|
||||
planetCount: report.planetCount(),
|
||||
planets,
|
||||
race: report.race() ?? "",
|
||||
localShipClass,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -221,10 +258,12 @@ export function uuidToHiLo(value: string): [bigint, bigint] {
|
||||
/**
|
||||
* applyOrderOverlay returns a copy of `report` with every locally-
|
||||
* valid or still-in-flight or applied command from `commands`
|
||||
* projected on top. Phase 14 understands `planetRename` only —
|
||||
* every other variant passes through. The function is pure:
|
||||
* callers re-derive the overlay whenever the draft or the report
|
||||
* change.
|
||||
* projected on top. Phase 14 introduced the overlay for
|
||||
* `planetRename`; Phase 15 extends it to `setProductionType` so the
|
||||
* inspector segment / map label reflect the chosen production target
|
||||
* before the engine confirms it. Other variants pass through. The
|
||||
* function is pure: callers re-derive the overlay whenever the draft
|
||||
* or the report change.
|
||||
*
|
||||
* `statuses` maps command id → status. Entries with `valid`,
|
||||
* `submitting`, or `applied` participate in the overlay — together
|
||||
@@ -250,18 +289,69 @@ export function applyOrderOverlay(
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (cmd.kind !== "planetRename") continue;
|
||||
const idx = report.planets.findIndex((p) => p.number === cmd.planetNumber);
|
||||
if (idx < 0) continue;
|
||||
if (mutatedPlanets === null) {
|
||||
mutatedPlanets = [...report.planets];
|
||||
if (cmd.kind === "planetRename") {
|
||||
const idx = report.planets.findIndex(
|
||||
(p) => p.number === cmd.planetNumber,
|
||||
);
|
||||
if (idx < 0) continue;
|
||||
if (mutatedPlanets === null) {
|
||||
mutatedPlanets = [...report.planets];
|
||||
}
|
||||
mutatedPlanets[idx] = { ...mutatedPlanets[idx]!, name: cmd.name };
|
||||
continue;
|
||||
}
|
||||
if (cmd.kind === "setProductionType") {
|
||||
const idx = report.planets.findIndex(
|
||||
(p) => p.number === cmd.planetNumber,
|
||||
);
|
||||
if (idx < 0) continue;
|
||||
if (mutatedPlanets === null) {
|
||||
mutatedPlanets = [...report.planets];
|
||||
}
|
||||
mutatedPlanets[idx] = {
|
||||
...mutatedPlanets[idx]!,
|
||||
production: productionDisplayFromCommand(
|
||||
cmd.productionType,
|
||||
cmd.subject,
|
||||
),
|
||||
};
|
||||
continue;
|
||||
}
|
||||
mutatedPlanets[idx] = { ...mutatedPlanets[idx]!, name: cmd.name };
|
||||
}
|
||||
if (mutatedPlanets === null) return report;
|
||||
return { ...report, planets: mutatedPlanets };
|
||||
}
|
||||
|
||||
/**
|
||||
* productionDisplayFromCommand mirrors the engine's
|
||||
* `Cache.PlanetProductionDisplayName`
|
||||
* (`game/internal/controller/planet.go`) for the optimistic overlay.
|
||||
* Keeping the strings byte-equal with the next server report avoids
|
||||
* a flicker when the overlay drops on the next turn cutoff.
|
||||
*/
|
||||
export function productionDisplayFromCommand(
|
||||
productionType: ProductionType,
|
||||
subject: string,
|
||||
): string {
|
||||
switch (productionType) {
|
||||
case "MAT":
|
||||
return "Material";
|
||||
case "CAP":
|
||||
return "Capital";
|
||||
case "DRIVE":
|
||||
return "Drive";
|
||||
case "WEAPONS":
|
||||
return "Weapons";
|
||||
case "SHIELDS":
|
||||
return "Shields";
|
||||
case "CARGO":
|
||||
return "Cargo";
|
||||
case "SCIENCE":
|
||||
case "SHIP":
|
||||
return subject;
|
||||
}
|
||||
}
|
||||
|
||||
function decodeErrorMessage(payload: Uint8Array): { code: string; message: string } {
|
||||
if (payload.length === 0) {
|
||||
return { code: "internal_error", message: "empty error payload" };
|
||||
|
||||
Reference in New Issue
Block a user