Files
galaxy-game/ui/docs/science-designer-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

5.2 KiB

Science designer UX

A science is a named mix of four tech proportions — drive, weapons, shields, cargo — that sum to 1.0. When a planet's production is set to a science, the planet's industry output for that turn is split between the four tech research tracks in those proportions (game/internal/controller/planet/production.go.runScienceResearch). The CRUD list, the designer, and the production-picker integration are provided by the UI; the wire and engine validation are handled by the backend.

Engine semantics in one paragraph

pkg/schema/fbs/order.fbs.CommandScienceCreate carries name + drive + weapons + shields + cargo as four float64 proportions. The engine validator (pkg/calc/validator.go.ValidateScienceValues) refuses any value outside [0, 1] and any sum that drifts further than its float tolerance from 1.0. Names follow the universal entity-name rules (pkg/util/string.go.ValidateTypeName): trimmed, non-empty, ≤ 30 runes, only letters / digits / combining marks / the allowed special set !@#$%^*-_=+~()[]{}, no special at start or end, ≤ 2 specials in a row, no whitespace. There is no CommandScienceUpdate on the wire — sciences are write-once, and an "edit" is a Remove + Create sequence.

Percent input model

The designer presents the four proportions as percentages (step="0.1", range [0, 100]) so the player can type and reason about whole-number splits — closer to how site/ru/rules.md describes sciences (site/ru/rules.md #sciences: "10 parts Drive, 5 parts Weapons, 30 parts Shields, 0 parts Cargo, …"). The wire shape is still fractions; conversion happens inside validateScience only on Save (value / 100 for each of the four).

The four inputs are not auto-rebalanced. The validator refuses a draft whose sum drifts further than SUM_EPSILON_PERCENT (1e-3) from 100, and the form's Save button stays disabled until the sum matches. A live readout under the inputs displays the running total so the player can chase it down without trial-and-error guessing.

The strict-sum gate was chosen over alternatives — auto-rebalance and raw-parts-with-engine-normalisation — because keeping the input model close to "what gets sent on the wire" minimises surprises when the engine returns the science exactly as typed. See lib/util/science-validation.ts for the validator and the conversion helper.

Name validation

validateScience runs validateEntityName first and returns its invalid-reason verbatim, so the designer's aria-describedby mapping reuses the existing translation keys for empty, too_long, starts_with_special, ends_with_special, consecutive_specials, whitespace, disallowed_character. A new key duplicate_name covers the UX-only check against the optimistic-overlay localScience projection — the engine would refuse the duplicate at submit time, but catching it locally keeps the Save button disabled with a clear hint instead of letting a red-badge rejected row land in the order tab.

Read-only view mode

A scienceId-bearing URL renders the designer in view mode: a read-only table of the four percentages plus name, with Back and Delete affordances. Sciences are write-once on the wire, so there is no Save-edits affordance — to change a science, the player deletes it and creates a new one. Delete dispatches a removeScience order command; the engine refuses removals when the science is referenced by an active production target on any planet, which surfaces as rejected in the order tab.

Production-picker integration

The planet inspector's production row (lib/inspectors/planet/production.svelte) is two <select>s plus a green ✓ apply / yellow ✗ cancel pair after F8-05. With the primary picker on research, the secondary picker lists the four tech display strings and one extra option per defined science from the player's localScience overlay. Picking a science target and pressing ✓ dispatches setProductionType("SCIENCE", "<scienceName>"), mirroring the wire-level CommandPlanetProduce shape (pkg/schema/fbs/order.fbs.CommandPlanetProduce).

The active value of both selects is derived from planet.production — the display string the engine emits in the report. A science name shadows the matching tech display string when they collide (a science deliberately named Drive wins over the Drive tech option), because the wire string is ambiguous and the user clearly intended the named science. This is a pragmatic accept; a structured production tag on the wire would let us disambiguate without the shadow rule, but that is a separate backend concern.

Tests

  • tests/science-validation.test.ts — validator branches, percent → fraction conversion, sum tolerance, duplicate-name detection.
  • tests/table-sciences.test.ts — table rendering, filter, sort, Delete dispatches removeScience, navigation to the designer.
  • tests/designer-science.test.ts — empty form Save disabled, live sum readout, valid Save dispatches createScience with fractions, view-mode Delete dispatches removeScience, duplicate-name guard against the overlay.
  • tests/e2e/sciences.spec.ts — full Playwright walkthrough: create → list → set planet production via the research/target dropdown pair + ✓ apply → delete.