ui/phase-17: ship-class CRUD without calc
Phase 17 lights up the ship-class table and designer active views, extends the order-draft pipeline with createShipClass and removeShipClass commands, and projects pending Save/Delete actions through applyOrderOverlay so the table reflects the player's intent before auto-sync lands. The plan is corrected in the same patch: per game/rules.txt, ship classes are designed once and cannot be edited — the engine has no Update command, so the UI exposes only Create + Delete. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -16,10 +16,15 @@
|
||||
// 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.
|
||||
// Phase 15 added a name-only `localShipClass` projection so the
|
||||
// planet inspector's Build-Ship sub-picker had data to render.
|
||||
// Phase 17 widens `ShipClassSummary` to the full attribute set
|
||||
// (drive / armament / weapons / shields / cargo) so the ship-class
|
||||
// table and designer can render every documented field, and
|
||||
// extends `applyOrderOverlay` with the `createShipClass` /
|
||||
// `removeShipClass` variants — pending Save / Delete actions are
|
||||
// reflected in the table immediately, without waiting for the
|
||||
// auto-sync round-trip.
|
||||
|
||||
import { Builder, ByteBuffer } from "flatbuffers";
|
||||
|
||||
@@ -73,15 +78,22 @@ export interface ReportPlanet {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* ShipClassSummary is the projection of `report.ShipClass` the
|
||||
* ship-class table and designer render. Phase 15 carried just the
|
||||
* `name` for the Build-Ship sub-picker; Phase 17 added the five
|
||||
* tech-derived numbers so the table can sort / filter on them and
|
||||
* the designer can populate read-only previews. The numeric ranges
|
||||
* mirror `pkg/calc/validator.go.ValidateShipTypeValues` exactly:
|
||||
* each of `drive`, `weapons`, `shields`, `cargo` is either zero or
|
||||
* ≥ 1, and `armament` is a non-negative integer.
|
||||
*/
|
||||
export interface ShipClassSummary {
|
||||
name: string;
|
||||
drive: number;
|
||||
armament: number;
|
||||
weapons: number;
|
||||
shields: number;
|
||||
cargo: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -266,7 +278,14 @@ function decodeReport(report: Report): GameReport {
|
||||
for (let i = 0; i < report.localShipClassLength(); i++) {
|
||||
const sc = report.localShipClass(i);
|
||||
if (sc === null) continue;
|
||||
localShipClass.push({ name: sc.name() ?? "" });
|
||||
localShipClass.push({
|
||||
name: sc.name() ?? "",
|
||||
drive: sc.drive(),
|
||||
armament: Number(sc.armament()),
|
||||
weapons: sc.weapons(),
|
||||
shields: sc.shields(),
|
||||
cargo: sc.cargo(),
|
||||
});
|
||||
}
|
||||
|
||||
const raceName = report.race() ?? "";
|
||||
@@ -380,11 +399,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 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.
|
||||
* `planetRename`; Phase 15 extended it to `setProductionType`;
|
||||
* Phase 16 to `setCargoRoute` / `removeCargoRoute`; Phase 17 to
|
||||
* `createShipClass` / `removeShipClass` so the ship-class table
|
||||
* shows pending Save / Delete actions immediately. 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
|
||||
@@ -402,6 +422,7 @@ export function applyOrderOverlay(
|
||||
if (commands.length === 0) return report;
|
||||
let mutatedPlanets: ReportPlanet[] | null = null;
|
||||
let mutatedRoutes: ReportRoute[] | null = null;
|
||||
let mutatedShipClass: ShipClassSummary[] | null = null;
|
||||
for (const cmd of commands) {
|
||||
const status = statuses[cmd.id];
|
||||
if (
|
||||
@@ -456,12 +477,48 @@ export function applyOrderOverlay(
|
||||
deleteRouteEntry(mutatedRoutes, cmd.sourcePlanetNumber, cmd.loadType);
|
||||
continue;
|
||||
}
|
||||
if (cmd.kind === "createShipClass") {
|
||||
if (mutatedShipClass === null) {
|
||||
mutatedShipClass = [...report.localShipClass];
|
||||
}
|
||||
// Skip duplicates: the engine refuses them server-side and
|
||||
// the designer's local validator prevents them client-side,
|
||||
// but a stale draft could still carry a row whose name now
|
||||
// collides with the server snapshot. Keeping the projection
|
||||
// unique avoids two rows in the table for the same name.
|
||||
if (mutatedShipClass.some((cls) => cls.name === cmd.name)) continue;
|
||||
mutatedShipClass.push({
|
||||
name: cmd.name,
|
||||
drive: cmd.drive,
|
||||
armament: cmd.armament,
|
||||
weapons: cmd.weapons,
|
||||
shields: cmd.shields,
|
||||
cargo: cmd.cargo,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (cmd.kind === "removeShipClass") {
|
||||
if (mutatedShipClass === null) {
|
||||
mutatedShipClass = [...report.localShipClass];
|
||||
}
|
||||
const idx = mutatedShipClass.findIndex((cls) => cls.name === cmd.name);
|
||||
if (idx < 0) continue;
|
||||
mutatedShipClass.splice(idx, 1);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (
|
||||
mutatedPlanets === null &&
|
||||
mutatedRoutes === null &&
|
||||
mutatedShipClass === null
|
||||
) {
|
||||
return report;
|
||||
}
|
||||
if (mutatedPlanets === null && mutatedRoutes === null) return report;
|
||||
return {
|
||||
...report,
|
||||
planets: mutatedPlanets ?? report.planets,
|
||||
routes: mutatedRoutes ?? report.routes,
|
||||
localShipClass: mutatedShipClass ?? report.localShipClass,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user