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:
@@ -23,6 +23,7 @@ import type {
|
||||
ReportIncomingShipGroup,
|
||||
ReportLocalFleet,
|
||||
ReportLocalShipGroup,
|
||||
ReportOtherRace,
|
||||
ReportOtherShipGroup,
|
||||
ReportPlanet,
|
||||
ReportRoute,
|
||||
@@ -31,8 +32,8 @@ import type {
|
||||
ShipClassSummary,
|
||||
ShipGroupTech,
|
||||
} from "./game-state";
|
||||
import type { CargoLoadType } from "../sync/order-types";
|
||||
import { isCargoLoadType } from "../sync/order-types";
|
||||
import type { CargoLoadType, Relation } from "../sync/order-types";
|
||||
import { isCargoLoadType, isRelation } from "../sync/order-types";
|
||||
|
||||
export const SYNTHETIC_GAME_ID_PREFIX = "synthetic-";
|
||||
|
||||
@@ -103,6 +104,11 @@ interface SyntheticPlayer {
|
||||
weapons: number;
|
||||
shields: number;
|
||||
cargo: number;
|
||||
population?: number;
|
||||
industry?: number;
|
||||
planets?: number;
|
||||
relation?: string;
|
||||
votes?: number;
|
||||
extinct?: boolean;
|
||||
}
|
||||
|
||||
@@ -159,6 +165,8 @@ interface SyntheticReportRoot {
|
||||
mapHeight?: number;
|
||||
mapPlanets?: number;
|
||||
race?: string;
|
||||
votes?: number;
|
||||
voteFor?: string;
|
||||
player?: SyntheticPlayer[];
|
||||
localPlanet?: SyntheticPlanet[];
|
||||
otherPlanet?: SyntheticPlanet[];
|
||||
@@ -290,6 +298,9 @@ function decodeSyntheticReport(json: unknown): GameReport {
|
||||
unidentifiedShipGroups,
|
||||
localFleets,
|
||||
otherRaces: collectOtherRacesFromSynthetic(root, race),
|
||||
races: collectOtherRaceRowsFromSynthetic(root, race),
|
||||
myVotes: numOr0(root.votes),
|
||||
myVoteFor: typeof root.voteFor === "string" ? root.voteFor : "",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -308,6 +319,34 @@ function collectOtherRacesFromSynthetic(
|
||||
return out;
|
||||
}
|
||||
|
||||
function collectOtherRaceRowsFromSynthetic(
|
||||
root: SyntheticReportRoot,
|
||||
raceName: string,
|
||||
): ReportOtherRace[] {
|
||||
const out: ReportOtherRace[] = [];
|
||||
for (const player of root.player ?? []) {
|
||||
if (player.extinct === true) continue;
|
||||
const name = typeof player.name === "string" ? player.name : "";
|
||||
if (name === "" || name === raceName) continue;
|
||||
const wire = typeof player.relation === "string" ? player.relation : "";
|
||||
const relation: Relation = isRelation(wire) ? wire : "PEACE";
|
||||
out.push({
|
||||
name,
|
||||
drive: numOr0(player.drive),
|
||||
weapons: numOr0(player.weapons),
|
||||
shields: numOr0(player.shields),
|
||||
cargo: numOr0(player.cargo),
|
||||
population: numOr0(player.population),
|
||||
industry: numOr0(player.industry),
|
||||
planets: Math.trunc(numOr0(player.planets)),
|
||||
relation,
|
||||
votesReceived: numOr0(player.votes),
|
||||
});
|
||||
}
|
||||
out.sort((a, b) => a.name.localeCompare(b.name));
|
||||
return out;
|
||||
}
|
||||
|
||||
function toShipGroupTech(raw: Record<string, number> | undefined): ShipGroupTech {
|
||||
const out: ShipGroupTech = { drive: 0, weapons: 0, shields: 0, cargo: 0 };
|
||||
if (raw === undefined || raw === null) return out;
|
||||
|
||||
Reference in New Issue
Block a user