Files
galaxy-game/ui/docs/calc-bridge.md
T
Ilia Denisov a89048f6c5 docs(ui): finalize MVP plan structure and de-archaeologize topic docs
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>
2026-05-21 23:17:51 +02:00

13 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/.

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.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 / 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:

  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.