ui/phase-22: races table with stance toggle and vote slot

Adds the Races View in the in-game shell. The table lists every
non-extinct other race with tech levels (percent), totals,
planets, votes received, and a per-row WAR | PEACE segmented
control. A single vote-recipient slot above the table queues a
`CommandRaceVote`; per-row buttons queue `CommandRaceRelation`.
Both commands flow through the existing order draft store with
collapse-by-acceptor (stance) and singleton (vote) rules.

`GameReport` widens with `races`, `myVotes`, `myVoteFor`; the
decoder walks `report.player[]` once for the richer projection.
The optimistic overlay flips stance and vote target immediately;
`votesReceived`, `myVotes`, and the alliance summary stay
server-authoritative — alliance grouping and the 2/3 victory
check are tallied on the server at turn cutoff and explicitly
not surfaced client-side (`rules.txt` keeps foreign races'
outgoing vote targets private).

Includes Vitest component coverage of stance and vote
collapse rules + a Playwright e2e that drives both commands
through the dispatcher route and verifies the gateway saw the
expected `CommandRaceRelation` / `CommandRaceVote` payloads.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-11 01:52:23 +02:00
parent 7a7f2e4b98
commit 9111dd955a
18 changed files with 1714 additions and 47 deletions
+23
View File
@@ -204,6 +204,8 @@ const en = {
"game.sidebar.order.label.ship_group_dismantle": "dismantle group {group}",
"game.sidebar.order.label.ship_group_transfer": "transfer group {group} → {acceptor}",
"game.sidebar.order.label.ship_group_join_fleet": "assign group {group} → fleet {fleet}",
"game.sidebar.order.label.race_relation": "declare {relation} on {acceptor}",
"game.sidebar.order.label.race_vote": "give my votes to {acceptor}",
"game.table.ship_classes.title": "ship classes",
"game.table.ship_classes.column.name": "name",
"game.table.ship_classes.column.drive": "drive",
@@ -297,6 +299,27 @@ const en = {
"game.designer.science.invalid.cargo_value": "cargo % must be in [0, 100]",
"game.designer.science.invalid.sum_not_hundred": "the four percentages must sum to exactly 100",
"game.table.races.title": "races",
"game.table.races.loading": "loading races…",
"game.table.races.empty": "no other races known yet",
"game.table.races.filter.placeholder": "filter by name",
"game.table.races.column.name": "name",
"game.table.races.column.drive": "drive %",
"game.table.races.column.weapons": "weapons %",
"game.table.races.column.shields": "shields %",
"game.table.races.column.cargo": "cargo %",
"game.table.races.column.population": "population",
"game.table.races.column.industry": "production",
"game.table.races.column.planets": "planets",
"game.table.races.column.votes": "votes received",
"game.table.races.column.relation": "stance",
"game.table.races.action.war": "WAR",
"game.table.races.action.peace": "PEACE",
"game.table.races.votes.mine": "my votes",
"game.table.races.votes.target": "I vote for",
"game.table.races.votes.target_placeholder": "— select a race —",
"game.table.races.note.alliance_server_side": "alliances and the 2/3 victory are tallied by the server at turn cutoff; this table shows only my outgoing vote and the votes each race received in the last tally",
"game.inspector.ship_group.kind.local": "your group",
"game.inspector.ship_group.kind.other": "other race group",
"game.inspector.ship_group.kind.incoming": "incoming group",