feat(ui): Phase 30 ship-class calculator with goal-seek and reach circles
Fuse the standalone ship-class designer (Phases 17/18) into a sidebar calculator: live mass/speed/attack/defence/bombing results, a planet build-rate readout, single-target goal-seek, a modernization-cost mode, and auto reach circles on the map for the selected planet. pkg/calc becomes the single source for the new math (no mirroring): extract BombingPower from the engine model and the per-turn ship-production loop from controller.ProduceShip into pkg/calc (engine now delegates), and add inverse goal-seek solvers in pkg/calc/solve.go. Thin-bridge the combat, planet-build, and solver functions through ui/core/calc + ui/wasm and rebuild core.wasm. Remove the standalone designer view/route; the ship-classes table and the view/bottom menus open the calculator via a shared request store. Docs: rewrite ui/PLAN.md Phase 30, adjust Phase 34 (realistic forecast + CAP/COL ownership), add ui/docs/calculator-ux.md, extend calc-bridge.md, fix navigation.md; remove ui/CALCULATOR.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+32
-7
@@ -11,11 +11,14 @@ 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.
|
||||
preview modernize cost. Phase 30 extends it with 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 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
|
||||
|
||||
@@ -35,7 +38,27 @@ on the JS-side `globalThis.galaxyCore` (registered in
|
||||
| `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 |
|
||||
| `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)|
|
||||
|
||||
`BombingPower` and the per-turn build loop are no longer engine-only:
|
||||
Phase 30 extracted `BombingPower` from
|
||||
`game/internal/model/game/group.go` and the per-iteration build math
|
||||
from `controller.ProduceShip` into `pkg/calc` (`ProduceShipsInTurn`),
|
||||
and 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
|
||||
@@ -85,12 +108,14 @@ whether the underlying Go function exists.
|
||||
| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | :-------------: | :-------------: |
|
||||
| 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 |
|
||||
| Ship calculator combat (Phase 30) | `EffectiveAttack`, `EffectiveDefence`, `BombingPower` (`pkg/calc/ship.go`; `BombingPower` extracted from `model/game/group.go`) | yes | yes |
|
||||
| Ship calculator goal-seek (Phase 30) | 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 | `PlanetProduceShipMass(L, Mat, Res) / ShipProductionCost(class.EmptyMass)` (combination of two existing exports) | partial | no |
|
||||
| Ship build progress / planet build rate (Phase 30)| `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`/
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
# Ship Class Calculator — UX
|
||||
|
||||
Phase 30 fuses the ship-class designer and a calculator into one sidebar
|
||||
tool (`lib/sidebar/calculator-tab.svelte`). It replaced the standalone
|
||||
designer view/route from Phases 17/18. All numeric math lives in
|
||||
`pkg/calc` and is reached through the `Core` WASM bridge; the calculator
|
||||
holds input state and orchestrates, it never computes.
|
||||
|
||||
## Modes
|
||||
|
||||
- **Calculator** (`ship`): the full tool — design area, derived results,
|
||||
planet build, goal-seek.
|
||||
- **Modernization**: reuses the design area and shows per-block and
|
||||
total `BlockUpgradeCost` from the current tech to an editable target
|
||||
tech. The design-area component is extracted
|
||||
(`lib/calculator/ship-design-area.svelte`) so the future ship-group
|
||||
upgrade flow can reuse it.
|
||||
|
||||
The `path` mode from the original plan was dropped (MVP path-finding is
|
||||
brute force); reach circles on the map replace it. `bombing` is folded
|
||||
in as a per-ship result rather than a separate mode.
|
||||
|
||||
## Areas
|
||||
|
||||
1. **Ship Class design area** — five blocks (drive, armament, weapons,
|
||||
shields, cargo) and four tech levels (drive, weapons, shields,
|
||||
cargo). Tech defaults to the player's current tech and shows a lock
|
||||
icon once overridden; clicking it resets to the default.
|
||||
2. **Calculator area** — derived results: empty/loaded mass, empty/
|
||||
loaded speed, attack, defence, bombing (per ship), cargo capacity.
|
||||
A load toggle (empty / full / custom) sets the cargo load that the
|
||||
loaded-column results use.
|
||||
3. **Planet area** — when an own planet is selected on the map, shows
|
||||
its MAT (overridable) and the single-turn build rate (ships per turn,
|
||||
turns per ship). The realistic multi-turn forecast with CAP/COL
|
||||
supply is Phase 34.
|
||||
|
||||
## Locks and goal-seek
|
||||
|
||||
Two distinct lock semantics share one icon (a closed padlock; it only
|
||||
appears once a value is pinned, click to release):
|
||||
|
||||
- **Override locks** on inputs that have a default — the four techs and
|
||||
the planet MAT. Editing one overrides the default; the lock resets it.
|
||||
Any number may be overridden at once.
|
||||
- **Goal-seek locks** on derived results. Pinning a result back-solves
|
||||
the single input it claims, which then renders read-only (computed):
|
||||
|
||||
| result | claims |
|
||||
| ------------- | ------------- |
|
||||
| attack | weapons block |
|
||||
| defence | shields block |
|
||||
| empty speed | drive block |
|
||||
| loaded speed | drive block |
|
||||
| empty mass | cargo block |
|
||||
| loaded mass | cargo load |
|
||||
|
||||
Only **one** result may be locked at a time (the others' lock
|
||||
affordances disable with a tooltip). An unreachable target — e.g. a
|
||||
speed at or above the stripped-hull ceiling `20 × driveTech`, or a
|
||||
solved block that fails the value rules — leaves the locked cell in a
|
||||
red error state and does not apply. Inverse solving lives in
|
||||
`pkg/calc/solve.go`; the bisection for defence → shields is the only
|
||||
non-analytic case.
|
||||
|
||||
## Create / load / delete
|
||||
|
||||
The name field is a combobox over the player's existing classes. Picking
|
||||
an existing class loads it as a template (so you can tweak and Create a
|
||||
new one); Create is disabled while the name is invalid or duplicate
|
||||
(reusing `lib/util/ship-class-validation.ts`). When a saved class is
|
||||
loaded, a Delete affordance appears. Create / Delete reuse the existing
|
||||
`createShipClass` / `removeShipClass` order-draft flow, so the optimistic
|
||||
overlay reflects the change immediately. Ship classes are immutable after
|
||||
creation (per `game/rules.txt`), so there is no edit — only Create-new
|
||||
and Delete.
|
||||
|
||||
## Reach circles
|
||||
|
||||
When an own planet is selected in calculator mode, the calculator
|
||||
publishes the planet origin and the design's loaded speed to a shared
|
||||
store (`lib/calculator/reach.svelte`). The map view
|
||||
(`lib/active-view/map.svelte`) reads it and draws 1–3 thin concentric
|
||||
reach circles (`map/reach-circles.ts`) for 1/2/3 turns. The ring count
|
||||
shrinks as speed grows: a ring is dropped once the previous one reaches
|
||||
the torus wrap-midpoint (half the shorter side) or the no-wrap map edge
|
||||
(farthest corner). The circles clear when the selection clears or the
|
||||
design is invalid.
|
||||
|
||||
## State preservation and history
|
||||
|
||||
Calculator inputs are component-local state. The sidebar keeps the tab
|
||||
mounted while the player navigates between active views, so inputs
|
||||
persist across view switches per the global state-preservation rule
|
||||
(`ui/docs/navigation.md`). Tech levels track the rendered report, so in
|
||||
history mode the calculator computes against the viewed snapshot's tech.
|
||||
|
||||
The ship-classes table and the view/bottom menus open the calculator via
|
||||
a shared request store (`lib/calculator/load-request.svelte`): the
|
||||
in-game layout flips the sidebar to the calculator tab and the
|
||||
calculator loads the requested class (or starts a fresh design).
|
||||
|
||||
## Layout and mobile
|
||||
|
||||
Everything stacks vertically to fit the 18 rem sidebar; the design and
|
||||
result rows use compact two-column (ship/tech, empty/loaded) grids. On
|
||||
mobile the sidebar is the existing bottom-sheet/overlay; the calc bottom
|
||||
tab opens it.
|
||||
|
||||
## Runtime note
|
||||
|
||||
The new bridge functions are only present after `make wasm` rebuilds
|
||||
`ui/frontend/static/core.wasm` (needs TinyGo). Vitest injects a fake
|
||||
`Core` (`tests/fake-core.ts`) mirroring `pkg/calc`, so unit/component
|
||||
tests do not need the rebuild; the Playwright suite and the live app do.
|
||||
+13
-11
@@ -27,16 +27,17 @@ separate dispatch component.
|
||||
| `/games/:id/report` | `lib/active-view/report.svelte` (see [report-view.md](report-view.md)) | Phase 23 |
|
||||
| `/games/:id/battle/:battleId?` | `lib/active-view/battle.svelte` | Phase 27 |
|
||||
| `/games/:id/mail` | `lib/active-view/mail.svelte` | Phase 28 |
|
||||
| `/games/:id/designer/ship-class/:classId?` | `lib/active-view/designer-ship-class.svelte` | Phase 17 (CRUD) / 18 (calc preview) |
|
||||
| `/games/:id/designer/science/:scienceId?` | `lib/active-view/designer-science.svelte` | Phase 21 |
|
||||
|
||||
`/games/:id` (no trailing view) redirects to `/games/:id/map`. The
|
||||
optional `:classId?` / `:scienceId?` segments on the designer
|
||||
routes match SvelteKit's `[[classId]]` syntax — `/designer/ship-class`
|
||||
opens the empty new-class form, `/designer/ship-class/{name}`
|
||||
opens the read-only view of the named class with the Delete
|
||||
affordance. Phase 17 lights up the ship-class CRUD path; Phase 18
|
||||
adds the live `pkg/calc/`-backed preview pane on top.
|
||||
optional `:scienceId?` segment on the science designer route matches
|
||||
SvelteKit's `[[scienceId]]` syntax — `/designer/science` opens the
|
||||
empty new-science form, `/designer/science/{name}` opens the named
|
||||
science. Phase 17/18 originally added a parallel ship-class designer
|
||||
route; Phase 30 removed it and folded ship-class design into the
|
||||
sidebar ship-class calculator (`lib/sidebar/calculator-tab.svelte`,
|
||||
see [calculator-ux.md](calculator-ux.md)), reached from the
|
||||
ship-classes table and the view/bottom menus.
|
||||
|
||||
The `entity` slug on the table route is kebab-case (`planets`,
|
||||
`ship-classes`, `ship-groups`, `fleets`, `sciences`, `races`).
|
||||
@@ -163,10 +164,11 @@ Tables, History, Settings, Logout) is the polish target for Phase 35
|
||||
## Transient map overlays
|
||||
|
||||
Some views can push a transient overlay onto `/map` with a back
|
||||
affordance — for example, the ship-class designer pushes a
|
||||
range-preview overlay onto the map. The transient overlay clears
|
||||
when the user navigates to any other view via the header or the
|
||||
bottom-tabs.
|
||||
affordance. (Phase 30's calculator reach circles are a simpler,
|
||||
always-on map extra rather than a back-stacked overlay; the transient
|
||||
back-stack mechanism itself is still a Phase 34 concept.) A transient
|
||||
overlay clears when the user navigates to any other view via the header
|
||||
or the bottom-tabs.
|
||||
|
||||
Phase 10 documents this concept but does not implement the
|
||||
back-stack mechanism. Phase 34 lands the back-stack alongside its
|
||||
|
||||
Reference in New Issue
Block a user