Files
galaxy-game/ui/docs/calc-bridge.md
T
Ilia Denisov 3626998a33 ui/phase-20: ship-group inspector actions
Eight ship-group operations land on the inspector behind a single
inline-form panel: split, send, load, unload, modernize, dismantle,
transfer, join fleet. Each action either appends a typed command to
the local order draft or surfaces a tooltip explaining the
disabled state. Partial-ship operations emit an implicit
breakShipGroup command before the targeted action so the engine
sees a clean (Break, Action) pair on the wire.

`pkg/calc.BlockUpgradeCost` migrates from
`game/internal/controller/ship_group_upgrade.go` so the calc
bridge can wrap a pure pkg/calc formula; the controller now
imports it. The bridge surfaces the function as
`core.blockUpgradeCost`, which the inspector calls once per ship
block to render the modernize cost preview.

`GameReport.otherRaces` is decoded from the report's player block
(non-extinct, ≠ self) and feeds the transfer-to-race picker. The
planet inspector's stationed-ship rows become clickable for own
groups so the actions panel is reachable from the standard click
flow (the renderer continues to hide on-planet groups).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 16:27:55 +02:00

11 KiB
Raw Blame History

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. Phase 20 extends it with BlockUpgradeCost so the ship-group inspector can preview modernize cost. 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

The Go module galaxy/core/calc (ui/core/calc/ship.go) exposes thin wrappers around pkg/calc/ship.go. Each is a one-line passthrough — the bridge contains zero math. The same 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 designer preview, modernize cost preview
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)
blockUpgradeCost calc.BlockUpgradeCost(blockMass, currentTech, target) number ship-group inspector modernize preview

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).
BlockUpgradeCost(blockMass, currentTech, target) Production cost of upgrading a single ship block (Phase 20 migrated this from controller).
FligthDistance(driveTech), VisibilityDistance(...) Race-level reach formulas (pkg/calc/race.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
Ship-group modernize cost preview (Phase 20) BlockUpgradeCost (pkg/calc/ship.go, migrated from game/internal/controller/ship_group_upgrade.go) yes yes
Free production potential (freeIndustry) Planet.ProductionCapacityindustry*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 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.