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:
@@ -0,0 +1,453 @@
|
||||
<!--
|
||||
Phase 22 races table. Lists every non-extinct other race with the
|
||||
local player's per-row stance toggle (WAR / PEACE — two segmented
|
||||
buttons, the active stance highlighted) and a single vote slot
|
||||
above the table. Both controls dispatch through the per-game
|
||||
`OrderDraftStore` (context), so the optimistic overlay flips
|
||||
immediately and the auto-sync pipeline drives the server in the
|
||||
background.
|
||||
|
||||
The alliance graph and the 2/3 victory check are NOT computed
|
||||
here: `rules.txt` keeps each race's outgoing vote target private
|
||||
(only the votes a race RECEIVED in the last tally and the local
|
||||
player's own pick are observable), and the acceptance criterion
|
||||
"vote counts match server state byte-for-byte" rules out
|
||||
client-side recomputation. The sub-header explains this explicitly
|
||||
so the player knows where the win condition lives.
|
||||
|
||||
The component sits inside the active-view slot owned by
|
||||
`routes/games/[id]/+layout.svelte`, so it inherits the per-game
|
||||
`OrderDraftStore` and `RenderedReportSource` through context. No
|
||||
data fetching is performed here — the layout is responsible.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { getContext } from "svelte";
|
||||
|
||||
import type { ReportOtherRace } from "../../api/game-state";
|
||||
import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
|
||||
import {
|
||||
RENDERED_REPORT_CONTEXT_KEY,
|
||||
type RenderedReportSource,
|
||||
} from "$lib/rendered-report.svelte";
|
||||
import {
|
||||
ORDER_DRAFT_CONTEXT_KEY,
|
||||
OrderDraftStore,
|
||||
} from "../../sync/order-draft.svelte";
|
||||
import type { Relation } from "../../sync/order-types";
|
||||
|
||||
type SortColumn =
|
||||
| "name"
|
||||
| "drive"
|
||||
| "weapons"
|
||||
| "shields"
|
||||
| "cargo"
|
||||
| "population"
|
||||
| "industry"
|
||||
| "planets"
|
||||
| "votesReceived";
|
||||
type SortDirection = "asc" | "desc";
|
||||
|
||||
const COLUMN_LABELS: Record<SortColumn, TranslationKey> = {
|
||||
name: "game.table.races.column.name",
|
||||
drive: "game.table.races.column.drive",
|
||||
weapons: "game.table.races.column.weapons",
|
||||
shields: "game.table.races.column.shields",
|
||||
cargo: "game.table.races.column.cargo",
|
||||
population: "game.table.races.column.population",
|
||||
industry: "game.table.races.column.industry",
|
||||
planets: "game.table.races.column.planets",
|
||||
votesReceived: "game.table.races.column.votes",
|
||||
};
|
||||
|
||||
const COLUMNS: readonly SortColumn[] = [
|
||||
"name",
|
||||
"drive",
|
||||
"weapons",
|
||||
"shields",
|
||||
"cargo",
|
||||
"population",
|
||||
"industry",
|
||||
"planets",
|
||||
"votesReceived",
|
||||
];
|
||||
|
||||
const rendered = getContext<RenderedReportSource | undefined>(
|
||||
RENDERED_REPORT_CONTEXT_KEY,
|
||||
);
|
||||
const draft = getContext<OrderDraftStore | undefined>(
|
||||
ORDER_DRAFT_CONTEXT_KEY,
|
||||
);
|
||||
|
||||
let sortColumn: SortColumn = $state("name");
|
||||
let sortDirection: SortDirection = $state("asc");
|
||||
let filter: string = $state("");
|
||||
|
||||
const races = $derived<ReportOtherRace[]>(rendered?.report?.races ?? []);
|
||||
const myVotes = $derived<number>(rendered?.report?.myVotes ?? 0);
|
||||
const myVoteFor = $derived<string>(rendered?.report?.myVoteFor ?? "");
|
||||
const reportLoaded = $derived(
|
||||
rendered?.report !== null && rendered?.report !== undefined,
|
||||
);
|
||||
|
||||
const filtered = $derived.by(() => {
|
||||
const needle = filter.trim().toLowerCase();
|
||||
if (needle === "") return races;
|
||||
return races.filter((r) => r.name.toLowerCase().includes(needle));
|
||||
});
|
||||
|
||||
const sorted = $derived.by(() => {
|
||||
const list = [...filtered];
|
||||
const dir = sortDirection === "asc" ? 1 : -1;
|
||||
list.sort((a, b) => {
|
||||
if (sortColumn === "name") {
|
||||
return a.name.localeCompare(b.name) * dir;
|
||||
}
|
||||
return (a[sortColumn] - b[sortColumn]) * dir;
|
||||
});
|
||||
return list;
|
||||
});
|
||||
|
||||
function toggleSort(column: SortColumn): void {
|
||||
if (sortColumn === column) {
|
||||
sortDirection = sortDirection === "asc" ? "desc" : "asc";
|
||||
return;
|
||||
}
|
||||
sortColumn = column;
|
||||
sortDirection = "asc";
|
||||
}
|
||||
|
||||
function ariaSort(column: SortColumn): "ascending" | "descending" | "none" {
|
||||
if (sortColumn !== column) return "none";
|
||||
return sortDirection === "asc" ? "ascending" : "descending";
|
||||
}
|
||||
|
||||
// Render a fraction in `[0, 1]` as a one-decimal percent
|
||||
// (`0.225` → `"22.5"`). The conversion is value-only — no `%`
|
||||
// suffix — so the column header carries the unit. Matches the
|
||||
// sciences-table convention.
|
||||
function formatPercent(fraction: number): string {
|
||||
return (fraction * 100).toLocaleString(undefined, {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 1,
|
||||
});
|
||||
}
|
||||
|
||||
function formatCount(value: number): string {
|
||||
return value.toLocaleString(undefined, {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
}
|
||||
|
||||
function formatVotes(value: number): string {
|
||||
return value.toLocaleString(undefined, {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
}
|
||||
|
||||
async function setStance(acceptor: string, relation: Relation): Promise<void> {
|
||||
if (draft === undefined) return;
|
||||
await draft.add({
|
||||
kind: "setDiplomaticStance",
|
||||
id: crypto.randomUUID(),
|
||||
acceptor,
|
||||
relation,
|
||||
});
|
||||
}
|
||||
|
||||
async function pickVote(event: Event): Promise<void> {
|
||||
if (draft === undefined) return;
|
||||
const select = event.currentTarget as HTMLSelectElement;
|
||||
const acceptor = select.value;
|
||||
if (acceptor === "") return;
|
||||
if (acceptor === myVoteFor) return;
|
||||
await draft.add({
|
||||
kind: "setVoteRecipient",
|
||||
id: crypto.randomUUID(),
|
||||
acceptor,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<section
|
||||
class="active-view"
|
||||
data-testid="active-view-table"
|
||||
data-entity="races"
|
||||
>
|
||||
<header>
|
||||
<h2>{i18n.t("game.table.races.title")}</h2>
|
||||
<div class="summary">
|
||||
<span class="summary-cell" data-testid="races-my-votes">
|
||||
<span class="summary-label">
|
||||
{i18n.t("game.table.races.votes.mine")}:
|
||||
</span>
|
||||
<span class="summary-value">{formatVotes(myVotes)}</span>
|
||||
</span>
|
||||
<label class="summary-cell vote-picker">
|
||||
<span class="summary-label">
|
||||
{i18n.t("game.table.races.votes.target")}:
|
||||
</span>
|
||||
<select
|
||||
data-testid="races-vote-target"
|
||||
value={myVoteFor}
|
||||
disabled={!reportLoaded || races.length === 0}
|
||||
onchange={pickVote}
|
||||
>
|
||||
<option value="" disabled>
|
||||
{i18n.t("game.table.races.votes.target_placeholder")}
|
||||
</option>
|
||||
{#each races as r (r.name)}
|
||||
<option value={r.name}>{r.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<p class="note" data-testid="races-alliance-note">
|
||||
{i18n.t("game.table.races.note.alliance_server_side")}
|
||||
</p>
|
||||
<div class="controls">
|
||||
<input
|
||||
type="search"
|
||||
class="filter"
|
||||
data-testid="races-filter"
|
||||
placeholder={i18n.t("game.table.races.filter.placeholder")}
|
||||
bind:value={filter}
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{#if !reportLoaded}
|
||||
<p class="status" data-testid="races-loading">
|
||||
{i18n.t("game.table.races.loading")}
|
||||
</p>
|
||||
{:else if races.length === 0}
|
||||
<p class="status" data-testid="races-empty">
|
||||
{i18n.t("game.table.races.empty")}
|
||||
</p>
|
||||
{:else}
|
||||
<table class="grid" data-testid="races-table">
|
||||
<thead>
|
||||
<tr>
|
||||
{#each COLUMNS as column (column)}
|
||||
<th aria-sort={ariaSort(column)}>
|
||||
<button
|
||||
type="button"
|
||||
class="sort"
|
||||
data-testid="races-column-{column}"
|
||||
onclick={() => toggleSort(column)}
|
||||
>
|
||||
{i18n.t(COLUMN_LABELS[column])}
|
||||
{#if sortColumn === column}
|
||||
<span class="sort-indicator" aria-hidden="true">
|
||||
{sortDirection === "asc" ? "▲" : "▼"}
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
</th>
|
||||
{/each}
|
||||
<th>{i18n.t("game.table.races.column.relation")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each sorted as r (r.name)}
|
||||
<tr data-testid="races-row" data-name={r.name}>
|
||||
<td data-testid="races-cell-name">{r.name}</td>
|
||||
<td data-testid="races-cell-drive">{formatPercent(r.drive)}</td>
|
||||
<td data-testid="races-cell-weapons">
|
||||
{formatPercent(r.weapons)}
|
||||
</td>
|
||||
<td data-testid="races-cell-shields">
|
||||
{formatPercent(r.shields)}
|
||||
</td>
|
||||
<td data-testid="races-cell-cargo">{formatPercent(r.cargo)}</td>
|
||||
<td data-testid="races-cell-population">
|
||||
{formatCount(r.population)}
|
||||
</td>
|
||||
<td data-testid="races-cell-industry">
|
||||
{formatCount(r.industry)}
|
||||
</td>
|
||||
<td data-testid="races-cell-planets">{formatCount(r.planets)}</td>
|
||||
<td data-testid="races-cell-votes">
|
||||
{formatVotes(r.votesReceived)}
|
||||
</td>
|
||||
<td>
|
||||
<div
|
||||
class="stance"
|
||||
role="group"
|
||||
aria-label={i18n.t("game.table.races.column.relation")}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="stance-button war"
|
||||
class:active={r.relation === "WAR"}
|
||||
aria-pressed={r.relation === "WAR"}
|
||||
data-testid="races-stance-war"
|
||||
onclick={() => void setStance(r.name, "WAR")}
|
||||
>
|
||||
{i18n.t("game.table.races.action.war")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="stance-button peace"
|
||||
class:active={r.relation === "PEACE"}
|
||||
aria-pressed={r.relation === "PEACE"}
|
||||
data-testid="races-stance-peace"
|
||||
onclick={() => void setStance(r.name, "PEACE")}
|
||||
>
|
||||
{i18n.t("game.table.races.action.peace")}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.active-view {
|
||||
padding: 1.5rem;
|
||||
font-family: system-ui, sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.summary {
|
||||
display: flex;
|
||||
gap: 1.25rem;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.summary-cell {
|
||||
display: inline-flex;
|
||||
gap: 0.4rem;
|
||||
align-items: baseline;
|
||||
}
|
||||
.summary-label {
|
||||
color: #aab;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.summary-value {
|
||||
color: #e8eaf6;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.vote-picker select {
|
||||
font: inherit;
|
||||
padding: 0.2rem 0.4rem;
|
||||
background: #0a0e1a;
|
||||
color: #e8eaf6;
|
||||
border: 1px solid #2a3150;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.vote-picker select:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.note {
|
||||
margin: 0;
|
||||
color: #889;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.filter {
|
||||
font: inherit;
|
||||
padding: 0.3rem 0.5rem;
|
||||
background: #0a0e1a;
|
||||
color: #e8eaf6;
|
||||
border: 1px solid #2a3150;
|
||||
border-radius: 3px;
|
||||
flex: 1 1 12rem;
|
||||
min-width: 8rem;
|
||||
}
|
||||
.status {
|
||||
margin: 0;
|
||||
color: #888;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.grid {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.grid th,
|
||||
.grid td {
|
||||
padding: 0.4rem 0.6rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #1c2240;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.grid th {
|
||||
color: #aab;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.grid tbody tr:hover {
|
||||
background: #11172a;
|
||||
}
|
||||
.sort {
|
||||
font: inherit;
|
||||
font-size: inherit;
|
||||
text-transform: inherit;
|
||||
letter-spacing: inherit;
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
gap: 0.3rem;
|
||||
align-items: baseline;
|
||||
}
|
||||
.sort-indicator {
|
||||
font-size: 0.7em;
|
||||
}
|
||||
.stance {
|
||||
display: inline-flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.stance-button {
|
||||
font: inherit;
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.05em;
|
||||
padding: 0.2rem 0.55rem;
|
||||
background: transparent;
|
||||
color: #aab;
|
||||
border: 1px solid #2a3150;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.stance-button:hover {
|
||||
color: #e8eaf6;
|
||||
}
|
||||
.stance-button.war.active {
|
||||
background: #4a1010;
|
||||
color: #ffcaca;
|
||||
border-color: #8a3030;
|
||||
}
|
||||
.stance-button.peace.active {
|
||||
background: #103a1a;
|
||||
color: #c8f2cf;
|
||||
border-color: #2f7a45;
|
||||
}
|
||||
</style>
|
||||
@@ -1,17 +1,18 @@
|
||||
<!--
|
||||
Active-view router for the per-entity tables. Phase 17 lights up
|
||||
the ship-classes table; Phase 21 lights up the sciences table; the
|
||||
remaining slugs (planets, ship-groups, fleets, races) keep the
|
||||
Phase 10 stub copy until their respective phases land. The wrapper
|
||||
preserves `data-testid="active-view-table"` and
|
||||
`data-entity={entity}` for every branch (each leaf component
|
||||
mirrors them) so the navigation e2e specs (`game-shell.spec.ts`,
|
||||
`view-menu`) keep matching.
|
||||
the ship-classes table; Phase 21 lights up the sciences table;
|
||||
Phase 22 lights up the races table; the remaining slugs (planets,
|
||||
ship-groups, fleets) keep the Phase 10 stub copy until their
|
||||
respective phases land. The wrapper preserves
|
||||
`data-testid="active-view-table"` and `data-entity={entity}` for
|
||||
every branch (each leaf component mirrors them) so the navigation
|
||||
e2e specs (`game-shell.spec.ts`, `view-menu`) keep matching.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
|
||||
import TableShipClasses from "./table-ship-classes.svelte";
|
||||
import TableSciences from "./table-sciences.svelte";
|
||||
import TableRaces from "./table-races.svelte";
|
||||
|
||||
type Props = { entity: string };
|
||||
let { entity }: Props = $props();
|
||||
@@ -26,6 +27,8 @@ mirrors them) so the navigation e2e specs (`game-shell.spec.ts`,
|
||||
<TableShipClasses />
|
||||
{:else if entity === "sciences"}
|
||||
<TableSciences />
|
||||
{:else if entity === "races"}
|
||||
<TableRaces />
|
||||
{:else}
|
||||
<section
|
||||
class="active-view"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -205,6 +205,8 @@ const ru: Record<keyof typeof en, string> = {
|
||||
"game.sidebar.order.label.ship_group_dismantle": "разобрать группу {group}",
|
||||
"game.sidebar.order.label.ship_group_transfer": "передать группу {group} → {acceptor}",
|
||||
"game.sidebar.order.label.ship_group_join_fleet": "включить группу {group} → флот {fleet}",
|
||||
"game.sidebar.order.label.race_relation": "объявить {relation} расе {acceptor}",
|
||||
"game.sidebar.order.label.race_vote": "отдать голоса расе {acceptor}",
|
||||
"game.table.ship_classes.title": "классы кораблей",
|
||||
"game.table.ship_classes.column.name": "название",
|
||||
"game.table.ship_classes.column.drive": "двигатель",
|
||||
@@ -298,6 +300,27 @@ const ru: Record<keyof typeof en, string> = {
|
||||
"game.designer.science.invalid.cargo_value": "трюм % должен быть в [0, 100]",
|
||||
"game.designer.science.invalid.sum_not_hundred": "сумма четырёх процентов должна быть ровно 100",
|
||||
|
||||
"game.table.races.title": "расы",
|
||||
"game.table.races.loading": "загрузка рас…",
|
||||
"game.table.races.empty": "других рас пока не видно",
|
||||
"game.table.races.filter.placeholder": "фильтр по имени",
|
||||
"game.table.races.column.name": "имя",
|
||||
"game.table.races.column.drive": "двигатель %",
|
||||
"game.table.races.column.weapons": "оружие %",
|
||||
"game.table.races.column.shields": "защита %",
|
||||
"game.table.races.column.cargo": "трюм %",
|
||||
"game.table.races.column.population": "население",
|
||||
"game.table.races.column.industry": "производство",
|
||||
"game.table.races.column.planets": "планет",
|
||||
"game.table.races.column.votes": "получено голосов",
|
||||
"game.table.races.column.relation": "отношение",
|
||||
"game.table.races.action.war": "ВОЙНА",
|
||||
"game.table.races.action.peace": "МИР",
|
||||
"game.table.races.votes.mine": "мои голоса",
|
||||
"game.table.races.votes.target": "голосую за",
|
||||
"game.table.races.votes.target_placeholder": "— выберите расу —",
|
||||
"game.table.races.note.alliance_server_side": "альянсы и победу 2/3 подсчитывает сервер при просчёте хода; в этой таблице видно лишь мой исходящий голос и количество голосов, полученных каждой расой в прошлой раздаче",
|
||||
|
||||
"game.inspector.ship_group.kind.local": "ваша группа",
|
||||
"game.inspector.ship_group.kind.other": "группа другой расы",
|
||||
"game.inspector.ship_group.kind.incoming": "входящая группа",
|
||||
|
||||
@@ -126,6 +126,15 @@ Tests exercise the tab through `__galaxyDebug.seedOrderDraft`
|
||||
group: shortGroupId(cmd.groupId),
|
||||
fleet: cmd.name,
|
||||
});
|
||||
case "setDiplomaticStance":
|
||||
return i18n.t("game.sidebar.order.label.race_relation", {
|
||||
relation: cmd.relation,
|
||||
acceptor: cmd.acceptor,
|
||||
});
|
||||
case "setVoteRecipient":
|
||||
return i18n.t("game.sidebar.order.label.race_vote", {
|
||||
acceptor: cmd.acceptor,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user