Files
galaxy-game/ui/docs/calc-bridge.md
T
Ilia Denisov e4dc0ce029 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.
2026-05-09 23:14:40 +02:00

172 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
Go → WASM → TypeScript bridge mounted under `ui/core/calc/` and a
matching TS adapter in `ui/frontend/src/platform/core/`.
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
| 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? |
| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | :-------------: | :-------------: |
| 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 |
| 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.
## Phase 16 waiver
Phase 16 introduces ship-reach filtering for the cargo-route
destination picker. The engine formula is trivial:
```
flightDistance = driveTech * 40
```
The Go-side reference now lives in
[`pkg/calc/race.go`](../../pkg/calc/race.go) as
`FligthDistance(driveTech) float64` (alongside the matching
`VisibilityDistance` for in-space group reports — used in later
phases). The engine call sites
(`game/internal/model/game/race.go.FlightDistance`,
`game/internal/controller/route.go.PlanetRouteSet`) still wrap the
Go formula directly; promoting them to call `pkg/calc/` is a
follow-up cleanup outside Phase 16's scope.
The original Phase 16 stage text described surfacing this through
`pkg/calc/` and `ui/core/calc/`; with the calc-bridge phase still
deferred, implementing the WASM glue for one constant-time
multiplication would be premature scaffolding. The picker
therefore computes reach inline in TypeScript using
`torusShortestDelta(planet.x, candidate.x, mapWidth)` and
`Math.hypot` against `40 * report.localPlayerDrive`, where
`localPlayerDrive` is decoded from the report's `Player` block by
matching `Player.name` to `report.race`
(`api/game-state.ts.findLocalPlayerDrive`).
When the calc-bridge phase ships, the inline formula is replaced
with a single call into the bridge — `calc.FligthDistance(driveTech)`
becomes the source of truth for both the picker and the
cargo-route auto-removal at turn cutoff. Until then, the UI
duplicates `flightDistance` knowingly — same precedent as the
production forecast deferral above.
## Planned bridge growth (follow-up phases)
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 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.