3626998a33
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>
178 lines
11 KiB
Markdown
178 lines
11 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/`.
|
||
|
||
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.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 | `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`](../../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.
|