a89048f6c5
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>
190 lines
13 KiB
Markdown
190 lines
13 KiB
Markdown
# 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.
|