# 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`](../../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.
