Files
galaxy-game/ui/docs/calculator-ux.md
T
Ilia Denisov 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
docs(site): edit rules for clarity + cross-links; migrate off rules.txt
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.
2026-05-31 15:56:00 +02:00

10 KiB
Raw Blame History

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 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.