MVP web client (Phases 1-30) is complete; reorganize planning + living docs around that. - PLAN.md kept as the staged MVP record (1-30) with a status block + pointers; removed the 31-36 stages, regression scenarios, and deferred-TODO section (moved out); fixed a stale cross-machine plan path. - ui/PLAN-finalize.md (new): active web-finalization plan in 8 stages (visual system, a11y, i18n, error UX, PWA, build hygiene, docs, owner manual-QA loop); absorbs former Phases 33 and 35. - ui/ROADMAP.md (new): post-MVP (Wails, Capacitor, realistic projection, acceptance + regression scenarios) and triaged deferred follow-ups. - ui/docs/README.md (new): grouped topic-doc index. - De-archaeologized all 20 ui/docs topic docs + ui/README.md + ui/core/README.md: stripped Phase-N build history, rewritten as current-state; deferred work now points at ROADMAP.md / PLAN-finalize.md. Docs-only; no code change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
13 KiB
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/.
The bridge covers the ship-math slice (everything the ship-class
designer needs to render its preview pane), BlockUpgradeCost (for
the ship-group inspector's modernize-cost preview), and the combat,
planet-build, and goal-seek slice for the ship-class calculator:
EffectiveAttack, EffectiveDefence, BombingPower, ShipBuildCost,
ProduceShipsInTurn, and the inverse solvers from pkg/calc/solve.go.
Other slices (production/science forecast, the realistic multi-turn
planet projection) remain deferred. 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 + modernization mode |
effectiveAttack |
calc.EffectiveAttack(weapons, weaponsTech) |
number |
calculator (attack result) |
effectiveDefence |
calc.EffectiveDefence(shields, shieldsTech, fullMass) |
number |
calculator (defence result) |
bombingPower |
calc.BombingPower(weapons, weaponsTech, armament, n) |
number |
calculator (bombing result, n = 1) |
shipBuildCost |
calc.ShipBuildCost(shipMass, material, resources) |
number |
calculator (planet build) |
produceShipsInTurn |
calc.ProduceShipsInTurn(L, material, resources, mass) |
{ships,…} |
calculator (planet ships/turn) |
weaponsForAttack |
calc.WeaponsForAttack(targetAttack, weaponsTech) |
number|null |
calculator goal-seek (attack → weapons) |
driveForSpeed |
calc.DriveForSpeed(targetSpeed, driveTech, restMass) |
number|null |
calculator goal-seek (speed → drive) |
shieldsForDefence |
calc.ShieldsForDefence(targetDefence, sTech, restMass) |
number|null |
calculator goal-seek (defence → shields) |
cargoForEmptyMass |
calc.CargoForEmptyMass(targetEmptyMass, restMass) |
number|null |
calculator goal-seek (mass → cargo) |
loadForFullMass |
calc.LoadForFullMass(targetFullMass, emptyMass, cTech) |
number|null |
calculator goal-seek (loaded mass → load) |
ceil3 |
calc.Ceil3(value) (pkg/calc/number.go) |
number |
calculator display rounding (round up to 3 dp) |
BombingPower and the per-turn build loop are no longer engine-only:
BombingPower was extracted from game/internal/model/game/group.go
and the per-iteration build math from controller.ProduceShip into
pkg/calc (ProduceShipsInTurn); the engine now delegates to both —
a true refactor, not a mirror.
The inverse solvers (pkg/calc/solve.go) invert the forward formulas
for single-target goal-seek and return null when infeasible;
shieldsForDefence uses bisection, the rest are analytic. Parity and
round-trip tests live in ui/core/calc/{ship,planet,solve}_test.go.
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
The Go-side bridge is intentionally narrow: it covers ship math and
the combat/planet-build/goal-seek slice. Production forecasts, science,
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 (migrated 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 | EmptyMass, FullMass, Speed, DriveEffective, CargoCapacity, CarryingMass, WeaponsBlockMass (pkg/calc/ship.go) |
yes | yes |
| Ship-group modernize cost preview | BlockUpgradeCost (pkg/calc/ship.go, migrated from game/internal/controller/ship_group_upgrade.go) |
yes | yes |
| Ship calculator combat | EffectiveAttack, EffectiveDefence, BombingPower (pkg/calc/ship.go; BombingPower extracted from model/game/group.go) |
yes | yes |
| Ship calculator goal-seek | inverse solvers in pkg/calc/solve.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 / planet build rate | ProduceShipsInTurn(L, Mat, Res, mass) (pkg/calc/planet.go, extracted from controller.ProduceShip); ShipBuildCost |
yes | yes |
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.
Production forecast waiver
The inspector's planet production controls (segmented control +
sub-pickers + collapse-by-planetNumber order command) do not
surface the per-type forecast number. The inspector 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. The per-type forecast number
is deferred pending promotion of the relevant formulas into
pkg/calc/.
Reach formula waiver
Ship-reach filtering for the cargo-route destination picker uses a trivial engine formula:
flightDistance = driveTech * 40
The Go-side reference lives in
pkg/calc/race.go as
FligthDistance(driveTech) float64 (alongside the matching
VisibilityDistance for in-space group reports). 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
pending cleanup.
Implementing the WASM glue for one constant-time multiplication
would be premature scaffolding, so the picker 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 remaining bridge work ships, the inline formula will be
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
The canonical bridge layout is established (Go subpackage + WASM
registration + typed Core interface + parity tests). Future calc
work follows the same shape:
- 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 itsgame/internal/...wrappers. - Add a thin one-line wrapper in
ui/core/calc/(new file per topic, e.g.ui/core/calc/planet.gofor production forecasts). No math in the bridge. - Register the function in
ui/wasm/main.gounderglobalThis.galaxyCore. - Extend the
Coreinterface inui/frontend/src/platform/core/index.tswith a typed signature and add the passthrough inwasm.ts.adaptBridge. - Add a parity test in
ui/core/calc/<topic>_test.goand a feature-level test underui/frontend/tests/. - Update this document — move the row from "missing" to the live surface table and link the test files.