ui/phase-18: ship-class calc bridge with live designer preview

Wires pkg/calc/ship.go into the WASM Core boundary as seven thin
wrappers (DriveEffective, EmptyMass, WeaponsBlockMass, FullMass,
Speed, CargoCapacity, CarryingMass). The ship-class designer reads
Core through a new CORE_CONTEXT_KEY populated by the in-game layout
and renders a five-row preview pane (mass, full-load mass, max
speed, range at full load, cargo capacity) that updates reactively
on every form edit and on the player's localPlayer{Drive,Weapons,
Shields,Cargo} tech levels — three of which are now decoded from
the report's Player block alongside the existing localPlayerDrive.

CarryingMass is the seventh wrapper added to the original six-function
list so that "full-load mass" composes through pkg/calc/ functions
without putting math in TypeScript.
This commit is contained in:
Ilia Denisov
2026-05-09 23:14:40 +02:00
parent 721fa2172d
commit e4dc0ce029
25 changed files with 1056 additions and 64 deletions
+71 -19
View File
@@ -4,12 +4,58 @@ 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
in Go under `pkg/calc/` and are surfaced to the UI through a
Go → WASM → TypeScript bridge mounted under `ui/core/calc/` and a
matching TS adapter in `ui/frontend/src/`.
matching TS adapter in `ui/frontend/src/platform/core/`.
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.
Phase 18 lands the **ship-math slice** of the bridge — everything
the ship-class designer needs to render its preview pane. Other
slices (production forecast, science research, ship build progress)
remain deferred to dedicated future phases. This document is the
running audit trail of what is live, what is missing, and how each
function maps to its `pkg/calc/` source.
## Live bridge surface (Phase 18)
The Go module `galaxy/core/calc` (`ui/core/calc/ship.go`) exposes
seven thin wrappers around `pkg/calc/ship.go`. Each is a one-line
passthrough — the bridge contains zero math. The same seven names
appear on the JS-side `globalThis.galaxyCore` (registered in
`ui/wasm/main.go`) and on the typed `Core` interface
(`ui/frontend/src/platform/core/index.ts`).
| Bridge function | `pkg/calc/` source | JS return shape | Used by |
| ------------------ | --------------------------------------------------- | --------------- | -------------------------------- |
| `driveEffective` | `calc.DriveEffective(drive, driveTech)` | `number` | designer preview (`Speed` input) |
| `emptyMass` | `calc.EmptyMass(drive, weapons, armament, …)` | `number\|null` | designer preview (mass row) |
| `weaponsBlockMass` | `calc.WeaponsBlockMass(weapons, armament)` | `number\|null` | reserved for future stages |
| `fullMass` | `calc.FullMass(emptyMass, carryingMass)` | `number` | designer preview (full-load row) |
| `speed` | `calc.Speed(driveEffective, fullMass)` | `number` | designer preview (speed + range) |
| `cargoCapacity` | `calc.CargoCapacity(cargo, cargoTech)` | `number` | designer preview (cargo row) |
| `carryingMass` | `calc.CarryingMass(load, cargoTech)` | `number` | designer preview (full-load mass)|
`number|null` returns mirror the Go `(float64, bool)` signature: the
upstream validator rejects weapons/armament pairings with one zero
side and the other non-zero, so the bridge returns `null` instead of
silently zeroing. The ship-class form gates the preview behind its
own `validateShipClass` so a user-visible `null` is the safety net,
not the common path.
Composition (e.g., "full-load mass = `fullMass(emptyMass,
carryingMass(cargoCapacity, cargoTech))`") happens in the TS preview
component, not in the bridge — the Go side stays purely a marshalling
adapter. Parity is exercised by `ui/core/calc/ship_test.go`, which
calls each wrapper alongside the direct `pkg/calc/` function on the
same inputs and asserts byte-equal outputs.
## Still-deferred slices
Phase 18's Go-side bridge is intentionally narrow: it covers ship
math and nothing else. Production forecasts, science, ship-build
progress, and reach (`FligthDistance`) still depend on either
inline TS arithmetic or the engine-shipped fields on `GameReport`.
See the table further down for what is missing and the per-feature
waivers below for the rationale on each deferral.
## Current `pkg/calc/` exports
@@ -32,6 +78,7 @@ whether the underlying Go function exists.
| UI feature | Go formula | In `pkg/calc/`? | Surfaced to TS? |
| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | :-------------: | :-------------: |
| Ship-class designer preview (Phase 18) | `EmptyMass`, `FullMass`, `Speed`, `DriveEffective`, `CargoCapacity`, `CarryingMass`, `WeaponsBlockMass` (`pkg/calc/ship.go`) | yes | yes |
| 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 |
@@ -100,20 +147,25 @@ cargo-route auto-removal at turn cutoff. Until then, the UI
duplicates `flightDistance` knowingly — same precedent as the
production forecast deferral above.
## Planned bridge shape (follow-up phase)
## Planned bridge growth (follow-up phases)
When the bridge phase lands, the contract should be:
Phase 18 set up the canonical bridge layout (Go subpackage + WASM
registration + typed `Core` interface + parity tests). Future calc
work follows the same shape:
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.
1. Promote any still-engine-only formula from the table above into
`pkg/calc/` so the engine and the UI share one Go-side
implementation. The engine continues to call them through its
`game/internal/...` wrappers.
2. Add a thin one-line wrapper in `ui/core/calc/` (new file per
topic, e.g. `ui/core/calc/planet.go` for production forecasts).
No math in the bridge.
3. Register the function in `ui/wasm/main.go` under
`globalThis.galaxyCore`.
4. Extend the `Core` interface in
`ui/frontend/src/platform/core/index.ts` with a typed signature
and add the passthrough in `wasm.ts.adaptBridge`.
5. Add a parity test in `ui/core/calc/<topic>_test.go` and a
feature-level test under `ui/frontend/tests/`.
6. Update this document — move the row from "missing" to the live
surface table and link the test files.