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.
111 lines
5.2 KiB
Markdown
111 lines
5.2 KiB
Markdown
# 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.
|