cbf7f65916
Owner review on PR #61: - п.9 (option B). Hide the native spinner on EVERY numeric input in the calculator (DWSC blocks, armament, tech, planet MAT, custom load, lock value, modernization target tech) and drive every step through ArrowUp / ArrowDown. The column widths stay stable and the inputs read consistently across the whole row. The ship blocks keep the smart (0 ↔ 1) jump on ArrowUp/ArrowDown; armament steps ±1 with a JS handler instead of relying on the native spinner. Other inputs step by their natural grain (±0.001 for tech / lock, ±0.01 for MAT / load). - п.10. Tech-level labels (`tech-val`) and the planet MAT label (`mat-val`) now read through the same `Ceil3` formatter as the derived results, so plain-text numeric values share the report's 3-decimal tabular formatting. The design-area component receives `formatNumber` as a prop; the resolved (goal-seek) cell uses the same formatter, so the read-only computed value matches the rest of the row. - п.12. `computeCalculator` now validates the back-solved block against the same DWSC rule the live validator enforces (`0` or `≥ 1`). When the solver lands in the `(0, 1)` gap (e.g. attack 0.5 / weaponsTech 1.5 → weapons 0.333…) the lock is flagged infeasible — the lock input flips red and the claimed block is NOT back-solved into the invalid range, so the design preview keeps reading the user's own typed values instead of silently showing a sub-1 block. - new. Selecting an existing ship class from the name datalist now loads it immediately. `change` fires only on blur in Firefox, which is why the previous behaviour looked delayed; switching the load to `oninput` with an `InputEvent.inputType` check makes the load synchronous everywhere (datalist replacement carries `"insertReplacementText"` in Chromium / WebKit, `undefined` in Firefox; keyboard typing always carries a typing `inputType`). Before loading we compare the live blocks to the previously loaded class (or to the empty defaults) and, if they differ, ask through a `window.confirm`. On decline we revert the name field and leave the design untouched. Tests: calculator-tab and calc-model gain six cases (armament step, tech/MAT formatter labels, lock infeasible on (0, 1) for both attack→weapons and emptyMass→cargo, lock-value Arrow step, dropdown immediate load + confirm-blocks-load + confirm-allows-load), all 779 vitest tests green. docs/calculator-ux.md follows the new behaviour. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
184 lines
9.4 KiB
Markdown
184 lines
9.4 KiB
Markdown
# 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
|
||
inherited tech value reads through the same 3-decimal `Ceil3`
|
||
formatter the report uses, so the column lines up with derived
|
||
values. **Every numeric input in the calculator hides the native
|
||
spinner and drives stepping through ArrowUp / ArrowDown.** This keeps
|
||
the column widths stable, makes the inputs read consistently, and
|
||
gives each row a step that matches its purpose. The four ship-class
|
||
blocks (drive, weapons, shields, cargo) use a smart 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)`.
|
||
Armament steps ±1 (clamped at 0). Tech, planet MAT, custom load,
|
||
lock value, and modernization target tech each step by their natural
|
||
grain (±0.001 for tech and lock values, ±0.01 for MAT and load).
|
||
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 MAT label reads through the same 3-decimal
|
||
`Ceil3` formatter, matching the rest of the calculator's label
|
||
values. 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 (a DWSC value in the `(0, 1)` gap)
|
||
— leaves the locked cell in a red error state and does not apply.
|
||
When that happens the claimed block is **not** back-solved into the
|
||
invalid range; the design preview keeps reading the user's typed
|
||
values, so the row never silently shows a sub-1 block. 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.
|
||
|
||
Selecting a class from the dropdown loads it **immediately**, the
|
||
moment the option is clicked. (Native `change` only fires on blur in
|
||
Firefox; switching the load trigger to `input` makes the load
|
||
synchronous everywhere, since the `InputEvent.inputType` flags a
|
||
datalist replacement as `"insertReplacementText"` in Chromium / WebKit
|
||
or `undefined` in Firefox — keyboard typing always carries a typing
|
||
`inputType`.) If the live blocks differ from the previously loaded
|
||
class (or, when nothing is loaded, from the empty defaults), the
|
||
calculator first asks `Discard unsaved changes and load class «…»?`
|
||
through a `window.confirm`; declining reverts the name field and
|
||
leaves the current blocks untouched.
|
||
|
||
## 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.
|