MVP web client (Phases 1-30) is complete; reorganize planning + living docs around that. - PLAN.md kept as the staged MVP record (1-30) with a status block + pointers; removed the 31-36 stages, regression scenarios, and deferred-TODO section (moved out); fixed a stale cross-machine plan path. - ui/PLAN-finalize.md (new): active web-finalization plan in 8 stages (visual system, a11y, i18n, error UX, PWA, build hygiene, docs, owner manual-QA loop); absorbs former Phases 33 and 35. - ui/ROADMAP.md (new): post-MVP (Wails, Capacitor, realistic projection, acceptance + regression scenarios) and triaged deferred follow-ups. - ui/docs/README.md (new): grouped topic-doc index. - De-archaeologized all 20 ui/docs topic docs + ui/README.md + ui/core/README.md: stripped Phase-N build history, rewritten as current-state; deferred work now points at ROADMAP.md / PLAN-finalize.md. Docs-only; no code change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5.0 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 game/rules.txt describes
sciences (game/rules.txt:345-362: "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 Research sub-row
(lib/inspectors/planet/production.svelte) renders the four tech
buttons and one extra button per defined science from the player's
localScience overlay. A click on a science button dispatches
setProductionType("SCIENCE", "<scienceName>"), mirroring the
wire-level CommandPlanetProduce shape
(pkg/schema/fbs/order.fbs.CommandPlanetProduce).
The active highlight 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
button), 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 dispatchesremoveScience, navigation to the designer.tests/designer-science.test.ts— empty form Save disabled, live sum readout, valid Save dispatchescreateSciencewith fractions, view-mode Delete dispatchesremoveScience, duplicate-name guard against the overlay.tests/e2e/sciences.spec.ts— full Playwright walkthrough: create → list → set planet production via the Research sub-row → delete.