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:
Ilia Denisov
2026-05-09 15:54:30 +02:00
parent c4f1409329
commit 915b4372dd
31 changed files with 2200 additions and 76 deletions
+82
View File
@@ -0,0 +1,82 @@
# Calc bridge
The Galaxy frontend renders predictive numbers (free production
potential, forecast output for a chosen production type, ship build
progress, tech progress) that depend on the same formulas the engine
uses at turn cutoff. To keep one source of truth, those formulas live
in Go under `pkg/calc/` and are surfaced to the UI through a planned
Go → WASM → TypeScript bridge mounted under `ui/core/calc/` and a
matching TS adapter in `ui/frontend/src/`.
The bridge does not exist yet. This document is the audit trail for
what it must expose, what is already in place, and what is missing.
## Current `pkg/calc/` exports
| Function | Purpose |
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
| `ShipProductionCost(shipEmptyMass float64) float64` | Production units required per unit of ship empty mass (×10). |
| `PlanetProduceShipMass(L, Mat, Res float64) float64` | Ship mass produced per turn given free production `L`, material stockpile `Mat`, resources `Res`.|
| `DriveEffective`, `Speed`, `EmptyMass`, `FullMass`, … | Ship-level derivations (`pkg/calc/ship.go`). |
| `ValidateShipTypeValues`, `CheckShipTypeValueDWSC` | Ship-design validators (`pkg/calc/validator.go`). |
Nothing else lives in `pkg/calc/` today. Production-side formulas
(industry / materials / per-tech research / production capacity) sit
in `game/internal/model/game/planet.go` and `…/science.go` and have
never been exported.
## Required calc functions per UI feature
The table below tracks what UI features need from the bridge and
whether the underlying Go function exists.
| UI feature | Go formula | In `pkg/calc/`? | Surfaced to TS? |
| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | :-------------: | :-------------: |
| Free production potential (`freeIndustry`) | `Planet.ProductionCapacity``industry*0.75 + population*0.25` (`game/internal/model/game/planet.go`) | no | no |
| Industry production output per turn | `Planet.ProduceIndustry(freeProduction)` (`planet.go`); `freeProduction/5` modulo material constraint | no | no |
| Materials production output per turn | `Planet.ProduceMaterial(freeProduction)` (`planet.go`); `freeProduction * resources` | no | no |
| Per-tech research progress (DRIVE/WEAPONS/…) | `ResearchTech` (`game/internal/model/game/science.go`); `freeProduction / 5000` per tech level | no | no |
| Custom-science progress | weighted form of `ResearchTech` driven by `Race.Sciences[].(Drive\|Weapons\|Shields\|Cargo)` (`science.go`) | no | no |
| Ship build progress | `PlanetProduceShipMass(L, Mat, Res) / ShipProductionCost(class.EmptyMass)` (combination of two existing exports) | partial | no |
`partial` means the Go primitives exist in `pkg/calc/` but the
composition (and the conversion of TS-side `ReportPlanet`/
`ShipClass` to the formula inputs) is not implemented anywhere.
## Phase 15 waiver
Phase 15 ships the inspector's planet production controls
(segmented control + sub-pickers + collapse-by-`planetNumber`
order command) but **deliberately does not surface the per-type
forecast number**. The planning gate explicitly raised the gap as
a blocker per the plan's audit clause ("if any are missing in
`pkg/calc/`, raise as blocker") and the project owner approved
deferring the forecast to a dedicated future bridge phase. The
inspector still renders the existing `freeIndustry` row (free
production potential) — that number is computed engine-side and
ships in the report payload, so no calc-bridge access is required
for it today.
Acceptance criterion 3 of Phase 15 ("forecast output number
reflects the chosen production type and matches `pkg/calc/`
outputs") is therefore intentionally not satisfied; the rewritten
Phase 15 stage text records this decision and points back at this
document.
## Planned bridge shape (follow-up phase)
When the bridge phase lands, the contract should be:
1. Promote every formula in the table above into `pkg/calc/` so the
engine and the UI share one Go-side implementation. The engine
continues to call them through `game/internal/...` wrappers.
2. Mount a `ui/core/calc/` Go module that re-exports the subset the
UI needs. Keep it WASM-friendly (no `unsafe`, no goroutines,
simple in/out values).
3. Wire the WASM glue in `ui/wasm/main.go` so each calc function is
reachable from `globalThis.galaxyCore`.
4. Add a TypeScript adapter under `ui/frontend/src/platform/core/`
that wraps the WASM calls in typed helpers
(`forecastIndustry(freeProduction, …)` etc.).
5. Update this document with the live function inventory and
delete the "missing" rows above.
+44 -2
View File
@@ -95,7 +95,7 @@ stored value).
`OrderCommand` is a discriminated union on the `kind` field. Phase
12 shipped the skeleton with a single content-free variant; Phase
14 adds the first real one:
14 added the first real one and Phase 15 added the second:
```ts
interface PlaceholderCommand {
@@ -111,7 +111,20 @@ interface PlanetRenameCommand {
readonly name: string;
}
type OrderCommand = PlaceholderCommand | PlanetRenameCommand;
interface SetProductionTypeCommand {
readonly kind: "setProductionType";
readonly id: string;
readonly planetNumber: number;
readonly productionType:
| "MAT" | "CAP" | "DRIVE" | "WEAPONS"
| "SHIELDS" | "CARGO" | "SCIENCE" | "SHIP";
readonly subject: string;
}
type OrderCommand =
| PlaceholderCommand
| PlanetRenameCommand
| SetProductionTypeCommand;
```
The `id` field is the canonical identifier the store uses for
@@ -123,6 +136,35 @@ with the inline editor in `lib/inspectors/planet.svelte`, the
local validator (`lib/util/entity-name.ts`, parity with
`pkg/util/string.go.ValidateTypeName`), and the submit pipeline.
`setProductionType` is the wire-mirror of the engine's
`CommandPlanetProduce` (`pkg/model/order/order.go`). The local
validator runs the same `subject=Production` rule as
`game/internal/router/validator.go`: `subject` is required and
must satisfy `validateEntityName` when `productionType` is
`SCIENCE` or `SHIP`; otherwise it is the empty string. The
optimistic overlay rewrites `planet.production` using
`productionDisplayFromCommand` (`api/game-state.ts`), which
mirrors the engine's `Cache.PlanetProductionDisplayName` so the
overlay stays byte-equal with the next server report.
### Collapse-by-target rule (Phase 15)
`setProductionType` is the first variant to carry a
collapse-by-target rule. `OrderDraftStore.add` enforces it:
when the incoming command's `kind` is `"setProductionType"` it
drops every prior `setProductionType` entry with the same
`planetNumber` (and the matching keys from `statuses`) before
appending. Other variants keep their append-only behaviour —
each `planetRename` is a distinct user-visible action and
collapsing them would lose intent.
Net effect on the order tab: at most one `setProductionType`
row per planet, regardless of how many times the player clicks
through the inspector segments. Auto-sync still fires on every
mutation; the engine accepts repeat submits idempotently. A
`setProductionType` and a `planetRename` for the same planet
coexist — the rules apply within a `kind`, not across.
## Store
`OrderDraftStore` lives in