140ee8e0ee
Build · Site / build (push) Successful in 8s
Tests · Go / test (push) Successful in 2m27s
Tests · UI / test (push) Waiting to run
Tests · Integration / integration (pull_request) Successful in 1m45s
Build · Site / build (pull_request) Successful in 9s
Tests · Go / test (pull_request) Successful in 3m14s
Tests · UI / test (pull_request) Successful in 3m14s
Editorial pass over site/ru/rules.md (on top of the verbatim port): - moved the lore intro to the RU home page, rewritten in a modern voice; - fixed typos, replaced the TODO/WTF cargo-tech note and the abandoned (---ссылка---) marker with the verified mechanic and a real cross-link, dropped the report TODO row; - wove organic intra-page cross-links (#combat, #movement, #victory, ...); - documented engine nuances verified against the code: ore auto-farming and the capital / "запасы промышленности" store (industry capped at population); cargo lost with ships destroyed in battle; and that a losing race's colonists at a neutral planet are NOT lost — they stay aboard (this corrects the audit note, verified in route.go). Migration: delete game/rules.txt (its content now lives, authoritative, in site/ru/rules.md) and repoint every reference to it (ui/frontend code comments + tests, ui/docs, tools, ui/PLAN.md links). Record the RU-authoritative rule in site/README.md and CLAUDE.md. The English site/rules.md mirror follows in a separate stage.
193 lines
10 KiB
Markdown
193 lines
10 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. The bombing
|
||
and cargo-capacity rows have no goal-seek lock, but they still
|
||
reserve a hidden lock-slot placeholder so the value column stays
|
||
vertically aligned with the lockable rows above.
|
||
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, the inherited tech /
|
||
planet MAT labels, 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`) and always padded to three decimals so the
|
||
column reads the same on integers and fractions alike (a speed of 20
|
||
shows as `20.000`, of 5.0003 as `5.001`). Labels and inputs use the
|
||
monospace stack from the design tokens (`--font-mono`) with
|
||
right-aligned, tabular numerals so values line up vertically across
|
||
rows. To match the display rule, every number input also refuses a
|
||
fourth decimal as the user types: typing `1.2345` clamps the input to
|
||
`1.234` on input. 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
|
||
|
||
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`). Create reuses the existing
|
||
`createShipClass` order-draft flow, so the optimistic overlay reflects
|
||
the change immediately. Ship classes are immutable after creation (per
|
||
`site/ru/rules.md`), so there is no edit — only Create-new. Delete-class
|
||
lives in the ship-classes table (`lib/active-view/table-ship-classes.svelte`),
|
||
not the calculator.
|
||
|
||
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.
|