- custom load capped at cargo capacity (error when exceeded); full load shows the cargo capacity; zero cargo pins load to empty and disables the toggle - per-input red border + tooltip for every invalid value (blocks, techs, load, MAT, modernization target); no value may be negative; locking a speed is disabled when drive is zero - display every computed number (results + goal-seek back-solved input) rounded up to 3 decimals via a shared pkg/calc Ceil3 bridged to wasm; engine keeps its own round-to-nearest util.Fixed* - modernization total upgrade cost spans two columns (single line) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6.3 KiB
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
BlockUpgradeCostfrom 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
- 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.
- 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 (in cargo units) that the loaded-column results use. At full the toggle shows the ship's cargo capacity; a custom load over that capacity is flagged as an error. With a zero cargo block there is no hold, so the load is pinned to empty and the toggle is disabled.
- 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 inpkg/calc/solve.go; the bisection for defence → shields is the only non-analytic case. Locking a speed is disabled when the drive block is zero (a deliberately immobile ship has no speed to back-solve).
Validation and display
Every numeric input is validated independently and an offending one gets
a red border and a hover/tap tooltip with the reason: no value may be
negative, the five blocks follow the engine value rules
(pkg/calc/validator.go, surfaced per-field by
shipClassFieldErrors), and a custom load may not exceed cargo capacity.
Every displayed number — the derived results and the goal-seek
back-solved input — is rounded up to three decimals through the
shared pkg/calc/number.go.Ceil3 (bridged as core.ceil3), so a value
is never shown lower than it is (a speed of 5.0003 reads 5.001). The
engine keeps its own round-to-nearest util.Fixed*; Ceil3 is a
display-only helper that lives in pkg/calc so the UI and Go share one
implementation.
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.