Files
galaxy-game/ui/docs/calculator-ux.md
T
Ilia Denisov e9b904332e
Tests · Go / test (push) Successful in 2m31s
Tests · UI / test (push) Waiting to run
Tests · Integration / integration (pull_request) Successful in 1m41s
Tests · Go / test (pull_request) Successful in 3m14s
Tests · UI / test (pull_request) Successful in 2m32s
fix(ui): calculator polish — smart input steps, unified tech/MAT lock idiom, tech floor, speed-lock ceiling fix
- pkg/calc: DriveForSpeed treats restMass==0 as a valid ceiling-only
  case (every positive drive solves it), so locking the displayed
  speed of a D=1, W=A=S=C=0 ship is no longer a phantom "infeasible".
- ship-design-area: drive/weapons/shields/cargo inputs use a JS-driven
  smart step on ArrowUp/ArrowDown (0↔1 jump, otherwise ±0.1) and hide
  the native spinner so it cannot produce invalid (0, 1) values;
  armament keeps its native step 1.
- Tech and planet MAT cells follow the same lock idiom as goal-seek
  locks: open padlock (🔓) over the inherited value → click to open
  an input with a closed padlock (🔒). The padlock slot is always
  reserved, so the column width is stable.
- Tech overrides (design area and modernization target) are floored
  at the player's current tech on this turn — a lower value is
  flagged as invalid.
2026-05-26 14:30:43 +02:00

160 lines
7.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Ship Class Calculator — UX
The ship-class designer and calculator are fused into one sidebar
tool (`lib/sidebar/calculator-tab.svelte`). The standalone designer
view/route was replaced by this combined tool. 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: the cell renders
the inherited number with an open padlock; clicking the open lock
activates an input (closed padlock), where the player may type an
override at or above their current tech. Clicking the closed
padlock resets to the default. The padlock slot is always reserved,
so the column width does not shift as the lock state toggles. The
four ship-class blocks (drive, weapons, shields, cargo) use a smart
keyboard step that respects the engine value rule (`0` or `≥ 1`):
ArrowUp from 0 jumps straight to 1, otherwise +0.1; ArrowDown from
1 collapses to 0, otherwise 0.1, never producing an invalid value
in `(0, 1)`. The native spinner is hidden on these inputs (it would
produce invalid intermediates); armament keeps its native step 1.
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 (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.
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 MAT follows the same lock idiom as the tech
cells: the planet number renders with an open padlock, clicking
opens an input with a closed padlock, and the closed padlock resets
to the planet value. The realistic multi-turn forecast with CAP/COL
supply is planned (see ../ROADMAP.md).
## Locks and goal-seek
Two distinct lock semantics share one padlock affordance. Both follow
the same idiom — an open padlock (🔓) means *value is inherited /
derived, click to override*; a closed padlock (🔒) means *value is
pinned by the player, click to reset*:
- **Override locks** on inputs that have a default — the four techs and
the planet MAT. By default the cell shows the inherited number plus
an open padlock; clicking it switches to an input plus a closed
padlock for typing the override. Closing (clicking the closed
padlock) resets to the default. Any number may be overridden at once.
Tech overrides are floored at the player's current tech on this
turn — a lower value is flagged as invalid. The same floor applies
to the modernization target tech.
- **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 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. Locking a speed is disabled when the drive block is
zero (a deliberately immobile ship has no speed to back-solve). With
the drive block as the only non-zero mass the displayed speed equals
the ceiling exactly (every positive drive gives the same speed), so
the solver accepts that ceiling target as a feasible lock and any
positive drive solves it.
## 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 13 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.