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:
@@ -173,11 +173,41 @@ export class OrderDraftStore {
|
||||
* triggers an auto-sync to keep the server in lock-step.
|
||||
* Mutations made before `init` resolves are ignored — the layout
|
||||
* always awaits `init` before exposing the store.
|
||||
*
|
||||
* `setProductionType` carries a collapse-by-`planetNumber` rule:
|
||||
* a new entry supersedes any prior `setProductionType` for the
|
||||
* same planet, so the draft holds at most one production choice
|
||||
* per planet at any time. Other variants append unconditionally —
|
||||
* `planetRename` keeps its append-only behaviour because each
|
||||
* rename is a distinct user-visible action.
|
||||
*/
|
||||
async add(command: OrderCommand): Promise<void> {
|
||||
if (this.status !== "ready") return;
|
||||
this.commands = [...this.commands, command];
|
||||
this.statuses = { ...this.statuses, [command.id]: validateCommand(command) };
|
||||
const removed: string[] = [];
|
||||
let nextCommands: OrderCommand[];
|
||||
if (command.kind === "setProductionType") {
|
||||
nextCommands = [];
|
||||
for (const existing of this.commands) {
|
||||
if (
|
||||
existing.kind === "setProductionType" &&
|
||||
existing.planetNumber === command.planetNumber
|
||||
) {
|
||||
removed.push(existing.id);
|
||||
continue;
|
||||
}
|
||||
nextCommands.push(existing);
|
||||
}
|
||||
nextCommands.push(command);
|
||||
} else {
|
||||
nextCommands = [...this.commands, command];
|
||||
}
|
||||
this.commands = nextCommands;
|
||||
const nextStatuses = { ...this.statuses };
|
||||
for (const id of removed) {
|
||||
delete nextStatuses[id];
|
||||
}
|
||||
nextStatuses[command.id] = validateCommand(command);
|
||||
this.statuses = nextStatuses;
|
||||
await this.persist();
|
||||
this.scheduleSync();
|
||||
}
|
||||
@@ -400,6 +430,20 @@ function validateCommand(cmd: OrderCommand): CommandStatus {
|
||||
switch (cmd.kind) {
|
||||
case "planetRename":
|
||||
return validateEntityName(cmd.name).ok ? "valid" : "invalid";
|
||||
case "setProductionType":
|
||||
// Mirrors the engine's `subject=Production` validator
|
||||
// (`game/internal/router/validator.go`): SCIENCE and SHIP
|
||||
// require a non-empty entity-name-valid subject; the other
|
||||
// six production types accept any subject (typically empty)
|
||||
// because the engine only consults the subject for those
|
||||
// two cases.
|
||||
if (
|
||||
cmd.productionType === "SCIENCE" ||
|
||||
cmd.productionType === "SHIP"
|
||||
) {
|
||||
return validateEntityName(cmd.subject).ok ? "valid" : "invalid";
|
||||
}
|
||||
return "valid";
|
||||
case "placeholder":
|
||||
// Phase 12 placeholder entries are content-free and never
|
||||
// transition out of `draft` — they are not submittable.
|
||||
|
||||
Reference in New Issue
Block a user