ui/phase-21: sciences CRUD list, designer, and production-picker integration
Lights up the player-defined sciences feature: a table view with sort and filter, a designer with four percent inputs and a strict sum-equals-100 gate, and a Research-sub-row integration so the planet production picker lists the user's sciences alongside the four tech buttons. Phase 21 decisions are baked back into ui/PLAN.md (no UpdateScience on the wire — write-once via createScience + removeScience; percentages instead of fractions; sciences live under the existing Research segment). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
# 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`).
|
||||
Phase 21 lights up the CRUD list, the designer, and the
|
||||
production-picker integration. The wire and the engine validation
|
||||
are unchanged from earlier phases — only the UI is new.
|
||||
|
||||
## 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 is a Phase 21 decision (alternatives —
|
||||
auto-rebalance, raw-parts-with-engine-normalisation — were
|
||||
considered and rejected): 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 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 sub-row
|
||||
→ delete.
|
||||
Reference in New Issue
Block a user