7c8b5aeb23
Add per-planet cargo routes (COL/CAP/MAT/EMP) to the inspector with a renderer-driven destination picker (faded out-of-reach planets, cursor-line anchor, hover-highlight) and per-route arrows on the map. The pick-mode primitives are exposed via `MapPickService` so ship-group dispatch in Phase 19/20 can reuse the same surface. Pass A — generic map foundation: - hit-test now sizes the click zone to `pointRadiusPx + slopPx` so the visible disc is always part of the target. - `RendererHandle` gains `onPointerMove`, `onHoverChange`, `setPickMode`, `getPickState`, `getPrimitiveAlpha`, `setExtraPrimitives`, `getPrimitives`. The click dispatcher is centralised: pick-mode swallows clicks atomically so the standard selection consumers do not race against teardown. - `MapPickService` (`lib/map-pick.svelte.ts`) wraps the renderer contract in a promise-shaped `pick(...)`. The in-game shell layout owns the service so sidebar and bottom-sheet inspectors see the same instance. - Debug-surface registry exposes `getMapPrimitives`, `getMapPickState`, `getMapCamera` to e2e specs without spawning a separate debug page after navigation. Pass B — cargo-route feature: - `CargoLoadType`, `setCargoRoute`, `removeCargoRoute` typed variants with `(source, loadType)` collapse rule on the order draft; round-trip through the FBS encoder/decoder. - `GameReport` decodes `routes` and the local player's drive tech for the inline reach formula (40 × drive). `applyOrderOverlay` upserts/drops route entries for valid/submitting/applied commands. - `lib/inspectors/planet/cargo-routes.svelte` renders the four-slot section. `Add` / `Edit` call `MapPickService.pick`, `Remove` emits `removeCargoRoute`. - `map/cargo-routes.ts` builds shaft + arrowhead primitives per cargo type; the map view pushes them through `setExtraPrimitives` so the renderer never re-inits Pixi on route mutations (Pixi 8 doesn't support that on a reused canvas). Docs: - `docs/cargo-routes-ux.md` covers engine semantics + UI map. - `docs/renderer.md` documents pick mode and the debug surface. - `docs/calc-bridge.md` records the Phase 16 reach waiver. - `PLAN.md` rewrites Phase 16 to reflect the foundation + feature split and the decisions baked in (map-driven picker, inline reach, optimistic overlay via `setExtraPrimitives`). Tests: - `tests/map-pick-mode.test.ts` — pure overlay-spec helper. - `tests/map-cargo-routes.test.ts` — `buildCargoRouteLines`. - `tests/inspector-planet-cargo-routes.test.ts` — slot rendering, picker invocation, collapse, cancel, remove. - Extensions to `order-draft`, `submit`, `order-load`, `order-overlay`, `state-binding`, `inspector-planet`, `inspector-overlay`, `game-shell-sidebar`, `game-shell-header`. - `tests/e2e/cargo-routes.spec.ts` — Playwright happy path: add COL, add CAP, remove COL, asserting both the inspector and the arrow count via `__galaxyDebug.getMapPrimitives()`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
111 lines
6.6 KiB
Markdown
111 lines
6.6 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 planned
|
||
Go → WASM → TypeScript bridge mounted under `ui/core/calc/` and a
|
||
matching TS adapter in `ui/frontend/src/`.
|
||
|
||
The bridge does not exist yet. This document is the audit trail for
|
||
what it must expose, what is already in place, and what is missing.
|
||
|
||
## 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`). |
|
||
| `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? |
|
||
| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | :-------------: | :-------------: |
|
||
| 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
|
||
```
|
||
|
||
(`game/internal/model/game/race.go.FlightDistance`). 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 bridge 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.Reach(driveTech)` becomes
|
||
the source of truth for both the picker and the cargo-route arrow
|
||
auto-removal at turn cutoff. Until then, the UI duplicates
|
||
`flightDistance` knowingly — same precedent as the production
|
||
forecast deferral above.
|
||
|
||
## Planned bridge shape (follow-up phase)
|
||
|
||
When the bridge phase lands, the contract should be:
|
||
|
||
1. Promote every formula in the table above into `pkg/calc/` so the
|
||
engine and the UI share one Go-side implementation. The engine
|
||
continues to call them through `game/internal/...` wrappers.
|
||
2. Mount a `ui/core/calc/` Go module that re-exports the subset the
|
||
UI needs. Keep it WASM-friendly (no `unsafe`, no goroutines,
|
||
simple in/out values).
|
||
3. Wire the WASM glue in `ui/wasm/main.go` so each calc function is
|
||
reachable from `globalThis.galaxyCore`.
|
||
4. Add a TypeScript adapter under `ui/frontend/src/platform/core/`
|
||
that wraps the WASM calls in typed helpers
|
||
(`forecastIndustry(freeProduction, …)` etc.).
|
||
5. Update this document with the live function inventory and
|
||
delete the "missing" rows above.
|