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
+46 -1
View File
@@ -16,12 +16,15 @@ import {
CommandPlanetRename,
CommandPlanetRouteRemove,
CommandPlanetRouteSet,
CommandRaceRelation,
CommandRaceVote,
CommandScienceCreate,
CommandScienceRemove,
CommandShipClassCreate,
CommandShipClassRemove,
PlanetProduction,
PlanetRouteLoadType,
Relation,
UserGamesOrder,
UserGamesOrderGetResponse,
UserGamesOrderResponse,
@@ -98,6 +101,19 @@ export interface RemoveScienceResultFixture extends CommandResultFixtureBase {
name: string;
}
export interface SetDiplomaticStanceResultFixture
extends CommandResultFixtureBase {
kind: "setDiplomaticStance";
acceptor: string;
relation: "WAR" | "PEACE";
}
export interface SetVoteRecipientResultFixture
extends CommandResultFixtureBase {
kind: "setVoteRecipient";
acceptor: string;
}
export type CommandResultFixture =
| PlanetRenameResultFixture
| SetProductionTypeResultFixture
@@ -106,7 +122,9 @@ export type CommandResultFixture =
| CreateShipClassResultFixture
| RemoveShipClassResultFixture
| CreateScienceResultFixture
| RemoveScienceResultFixture;
| RemoveScienceResultFixture
| SetDiplomaticStanceResultFixture
| SetVoteRecipientResultFixture;
export function buildOrderResponsePayload(
gameId: string,
@@ -255,6 +273,22 @@ function encodeItem(builder: Builder, c: CommandResultFixture): number {
payloadType = CommandPayload.CommandScienceRemove;
break;
}
case "setDiplomaticStance": {
const acceptorOffset = builder.createString(c.acceptor);
inner = CommandRaceRelation.createCommandRaceRelation(
builder,
acceptorOffset,
relationToFBS(c.relation),
);
payloadType = CommandPayload.CommandRaceRelation;
break;
}
case "setVoteRecipient": {
const acceptorOffset = builder.createString(c.acceptor);
inner = CommandRaceVote.createCommandRaceVote(builder, acceptorOffset);
payloadType = CommandPayload.CommandRaceVote;
break;
}
}
CommandItem.startCommandItem(builder);
CommandItem.addCmdId(builder, cmdIdOffset);
@@ -304,3 +338,14 @@ function cargoLoadTypeToFBS(
return PlanetRouteLoadType.EMP;
}
}
function relationToFBS(
value: SetDiplomaticStanceResultFixture["relation"],
): Relation {
switch (value) {
case "WAR":
return Relation.WAR;
case "PEACE":
return Relation.PEACE;
}
}