ui/phase-23: turn-report view with twenty sections and TOC
Replaces the Phase 10 report stub with a scrollable orchestrator that renders every FBS array as a dedicated section (galaxy summary, votes, player status, my/foreign sciences, my/foreign ship classes, battles, bombings, approaching groups, my/foreign/uninhabited/unknown planets, ships in production, cargo routes, my fleets, my/foreign/unidentified ship groups). A sticky table of contents (a <select> on mobile), "back to map" affordance, IntersectionObserver-driven active-section highlight, and SvelteKit Snapshot-based scroll save/restore round out the view. GameReport gains six new fields (players, otherScience, otherShipClass, battleIds, bombings, shipProductions); decodeReport, the synthetic- report loader, the e2e fixture builder, and EMPTY_SHIP_GROUPS extend in lockstep. ~90 new i18n keys land in en + ru together. The legacy-report parser is extended to populate the new sections from the dg/gplus text formats (Your Sciences, <Race> Sciences, <Race> Ship Types, Bombings, Ships In Production). Ships-in-production prod_used is derived through a new pkg/calc.ShipBuildCost helper; the engine's controller.ProduceShip refactors to call the same helper without any behaviour change (engine tests stay unchanged and green). Battles remain in the parser's Skipped list — the legacy text carries no stable per-battle UUID. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -38,12 +38,16 @@ import { Builder, ByteBuffer } from "flatbuffers";
|
||||
import type { GalaxyClient } from "./galaxy-client";
|
||||
import { UUID } from "../proto/galaxy/fbs/common";
|
||||
import {
|
||||
Bombing,
|
||||
GameReportRequest,
|
||||
IncomingGroup,
|
||||
LocalFleet,
|
||||
LocalGroup,
|
||||
OtherGroup,
|
||||
OtherScience,
|
||||
OthersShipClass,
|
||||
Report,
|
||||
ShipProduction,
|
||||
UnidentifiedGroup,
|
||||
} from "../proto/galaxy/fbs/report";
|
||||
import type {
|
||||
@@ -280,6 +284,113 @@ export interface ReportOtherRace {
|
||||
votesReceived: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* ReportPlayer is the per-player projection consumed by the Phase 23
|
||||
* Report View's Player Status section. Unlike `ReportOtherRace`, this
|
||||
* row carries the local player and extinct rows too: the section is a
|
||||
* status overview, not a diplomacy surface. Sorted alphabetically by
|
||||
* name (case-insensitive); `isLocal` flags the calling player's row so
|
||||
* the section can highlight it. The wire `relation` field is
|
||||
* intentionally omitted — the self row carries the engine's "-"
|
||||
* sentinel and the other-race rows already expose it via
|
||||
* `GameReport.races`.
|
||||
*/
|
||||
export interface ReportPlayer {
|
||||
name: string;
|
||||
drive: number;
|
||||
weapons: number;
|
||||
shields: number;
|
||||
cargo: number;
|
||||
population: number;
|
||||
industry: number;
|
||||
planets: number;
|
||||
votesReceived: number;
|
||||
extinct: boolean;
|
||||
isLocal: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* ReportOtherScience is a single row in the Phase 23 Report View's
|
||||
* Foreign Sciences section. Mirrors the wire `OtherScience` (carries
|
||||
* the owning `race` alongside the four tech proportions). Stable
|
||||
* order: sorted by `(race, name)` so the report's per-race sub-tables
|
||||
* render deterministically.
|
||||
*/
|
||||
export interface ReportOtherScience {
|
||||
race: string;
|
||||
name: string;
|
||||
drive: number;
|
||||
weapons: number;
|
||||
shields: number;
|
||||
cargo: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* ReportOtherShipClass is a single row in the Phase 23 Report View's
|
||||
* Foreign Ship Classes section. Mirrors the wire `OthersShipClass`
|
||||
* (carries the owning `race`, the five tech-derived numbers, plus the
|
||||
* `mass` the local ship-classes table does not surface — useful for
|
||||
* fleet-mass comparison against incoming groups). Stable order:
|
||||
* sorted by `(race, name)`.
|
||||
*/
|
||||
export interface ReportOtherShipClass {
|
||||
race: string;
|
||||
name: string;
|
||||
drive: number;
|
||||
armament: number;
|
||||
weapons: number;
|
||||
shields: number;
|
||||
cargo: number;
|
||||
mass: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* ReportBombing is a single row in the Phase 23 Report View's
|
||||
* Bombings section. Mirrors the wire `Bombing` (post-bombing planet
|
||||
* snapshot, attacker/owner identity, attack power, and the boolean
|
||||
* `wiped` flag that drives a visually-distinct row state). Sorted by
|
||||
* `planetNumber` for deterministic rendering.
|
||||
*
|
||||
* Field naming follows the existing `ReportPlanet` convention:
|
||||
* `capital → industryStockpile`, `material → materialsStockpile`,
|
||||
* `number → planetNumber`.
|
||||
*/
|
||||
export interface ReportBombing {
|
||||
planetNumber: number;
|
||||
planet: string;
|
||||
owner: string;
|
||||
attacker: string;
|
||||
production: string;
|
||||
industry: number;
|
||||
population: number;
|
||||
colonists: number;
|
||||
industryStockpile: number;
|
||||
materialsStockpile: number;
|
||||
attackPower: number;
|
||||
wiped: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* ReportShipProduction is a single row in the Phase 23 Report View's
|
||||
* Ships In Production section. Mirrors the wire `ShipProduction`.
|
||||
* `planetNumber` resolves against `GameReport.planets` so the section
|
||||
* can render the producing planet's name; `cost` is the per-ship
|
||||
* production cost (`ShipProductionCost(shipMass)`, not including the
|
||||
* per-turn material-farming term); `prodUsed` is the engine's residual
|
||||
* production poured into the partial ship this turn; `percent` is the
|
||||
* cumulative build progress as a fraction in [0, 1]; `freeIndustry`
|
||||
* mirrors the producing planet's free industry. Stable order: sorted
|
||||
* by `(planetNumber, class)`.
|
||||
*/
|
||||
export interface ReportShipProduction {
|
||||
planetNumber: number;
|
||||
class: string;
|
||||
cost: number;
|
||||
prodUsed: number;
|
||||
percent: number;
|
||||
freeIndustry: number;
|
||||
}
|
||||
|
||||
export interface GameReport {
|
||||
turn: number;
|
||||
mapWidth: number;
|
||||
@@ -389,6 +500,50 @@ export interface GameReport {
|
||||
* report always carries a non-empty value.
|
||||
*/
|
||||
myVoteFor: string;
|
||||
/**
|
||||
* players is the richer per-player projection Phase 23 added for
|
||||
* the Report View's Player Status section. Same data source as
|
||||
* `races[]` (`report.player[]`) but with the local player and
|
||||
* extinct rows included, sorted alphabetically by name and tagged
|
||||
* with `isLocal`. `races[]` stays Phase 22's view (other,
|
||||
* non-extinct) so diplomatic-stance code paths do not churn.
|
||||
*/
|
||||
players: ReportPlayer[];
|
||||
/**
|
||||
* otherScience is the per-race foreign-sciences projection Phase
|
||||
* 23 added for the Report View's Foreign Sciences section. Sorted
|
||||
* by `(race, name)`. Empty when the report has no foreign science
|
||||
* data (boot state, single-race game, legacy synthetic data).
|
||||
*/
|
||||
otherScience: ReportOtherScience[];
|
||||
/**
|
||||
* otherShipClass is the per-race foreign-ship-classes projection
|
||||
* Phase 23 added for the Report View's Foreign Ship Classes
|
||||
* section. Sorted by `(race, name)`. Empty when the report has no
|
||||
* foreign ship-class data.
|
||||
*/
|
||||
otherShipClass: ReportOtherShipClass[];
|
||||
/**
|
||||
* battleIds is the list of battle UUIDs the engine recorded for
|
||||
* the current turn. Phase 23 renders them as inactive
|
||||
* monospace identifiers; Phase 27 will turn them into navigation
|
||||
* targets once the battle viewer lands. Empty when no battles
|
||||
* occurred last turn.
|
||||
*/
|
||||
battleIds: string[];
|
||||
/**
|
||||
* bombings is the per-bombing projection Phase 23 added for the
|
||||
* Report View's Bombings section. Sorted by `planetNumber`. Empty
|
||||
* when no planets were bombed last turn.
|
||||
*/
|
||||
bombings: ReportBombing[];
|
||||
/**
|
||||
* shipProductions is the per-ship-production projection Phase 23
|
||||
* added for the Report View's Ships In Production section.
|
||||
* Sorted by `(planetNumber, class)`. Empty when no planet is
|
||||
* currently producing a ship.
|
||||
*/
|
||||
shipProductions: ReportShipProduction[];
|
||||
}
|
||||
|
||||
export async function fetchGameReport(
|
||||
@@ -537,11 +692,17 @@ function decodeReport(report: Report): GameReport {
|
||||
const localTech = findLocalPlayerTech(report, raceName);
|
||||
const otherRaces = collectOtherRaces(report, raceName);
|
||||
const races = collectOtherRaceRows(report, raceName);
|
||||
const players = decodePlayers(report, raceName);
|
||||
const localShipGroups = decodeLocalShipGroups(report);
|
||||
const otherShipGroups = decodeOtherShipGroups(report);
|
||||
const incomingShipGroups = decodeIncomingShipGroups(report);
|
||||
const unidentifiedShipGroups = decodeUnidentifiedShipGroups(report);
|
||||
const localFleets = decodeLocalFleets(report);
|
||||
const otherScience = decodeOtherScience(report);
|
||||
const otherShipClass = decodeOtherShipClass(report);
|
||||
const battleIds = decodeBattleIds(report);
|
||||
const bombings = decodeBombings(report);
|
||||
const shipProductions = decodeShipProductions(report);
|
||||
|
||||
return {
|
||||
turn: Number(report.turn()),
|
||||
@@ -566,6 +727,12 @@ function decodeReport(report: Report): GameReport {
|
||||
races,
|
||||
myVotes: report.votes(),
|
||||
myVoteFor: report.voteFor() ?? "",
|
||||
players,
|
||||
otherScience,
|
||||
otherShipClass,
|
||||
battleIds,
|
||||
bombings,
|
||||
shipProductions,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -903,6 +1070,146 @@ function collectOtherRaceRows(
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* decodePlayers walks `report.player[]` and emits the full status
|
||||
* roster the Phase 23 Report View's Player Status section renders:
|
||||
* every named row including the local player and extinct races,
|
||||
* sorted alphabetically (case-insensitive). The local row carries
|
||||
* `isLocal: true` so the section can highlight it; the wire
|
||||
* `relation` field is intentionally dropped (self carries the engine
|
||||
* "-" sentinel, other rows already surface relation through
|
||||
* `GameReport.races`).
|
||||
*/
|
||||
function decodePlayers(report: Report, raceName: string): ReportPlayer[] {
|
||||
const out: ReportPlayer[] = [];
|
||||
for (let i = 0; i < report.playerLength(); i++) {
|
||||
const player = report.player(i);
|
||||
if (player === null) continue;
|
||||
const name = player.name() ?? "";
|
||||
if (name === "") continue;
|
||||
out.push({
|
||||
name,
|
||||
drive: player.drive(),
|
||||
weapons: player.weapons(),
|
||||
shields: player.shields(),
|
||||
cargo: player.cargo(),
|
||||
population: player.population(),
|
||||
industry: player.industry(),
|
||||
planets: player.planets(),
|
||||
votesReceived: player.votes(),
|
||||
extinct: player.extinct(),
|
||||
isLocal: name === raceName,
|
||||
});
|
||||
}
|
||||
out.sort((a, b) =>
|
||||
a.name.toLowerCase().localeCompare(b.name.toLowerCase()),
|
||||
);
|
||||
return out;
|
||||
}
|
||||
|
||||
function decodeOtherScience(report: Report): ReportOtherScience[] {
|
||||
const out: ReportOtherScience[] = [];
|
||||
for (let i = 0; i < report.otherScienceLength(); i++) {
|
||||
const s = report.otherScience(i);
|
||||
if (s === null) continue;
|
||||
out.push({
|
||||
race: s.race() ?? "",
|
||||
name: s.name() ?? "",
|
||||
drive: s.drive(),
|
||||
weapons: s.weapons(),
|
||||
shields: s.shields(),
|
||||
cargo: s.cargo(),
|
||||
});
|
||||
}
|
||||
out.sort((a, b) => {
|
||||
const byRace = a.race.localeCompare(b.race);
|
||||
if (byRace !== 0) return byRace;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
function decodeOtherShipClass(report: Report): ReportOtherShipClass[] {
|
||||
const out: ReportOtherShipClass[] = [];
|
||||
for (let i = 0; i < report.otherShipClassLength(); i++) {
|
||||
const sc = report.otherShipClass(i);
|
||||
if (sc === null) continue;
|
||||
out.push({
|
||||
race: sc.race() ?? "",
|
||||
name: sc.name() ?? "",
|
||||
drive: sc.drive(),
|
||||
armament: Number(sc.armament()),
|
||||
weapons: sc.weapons(),
|
||||
shields: sc.shields(),
|
||||
cargo: sc.cargo(),
|
||||
mass: sc.mass(),
|
||||
});
|
||||
}
|
||||
out.sort((a, b) => {
|
||||
const byRace = a.race.localeCompare(b.race);
|
||||
if (byRace !== 0) return byRace;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
function decodeBattleIds(report: Report): string[] {
|
||||
const out: string[] = [];
|
||||
for (let i = 0; i < report.battleLength(); i++) {
|
||||
const uuid = report.battle(i);
|
||||
const value = uuidStringFromFB(uuid);
|
||||
if (value === null) continue;
|
||||
out.push(value);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function decodeBombings(report: Report): ReportBombing[] {
|
||||
const out: ReportBombing[] = [];
|
||||
for (let i = 0; i < report.bombingLength(); i++) {
|
||||
const b = report.bombing(i);
|
||||
if (b === null) continue;
|
||||
out.push({
|
||||
planetNumber: Number(b.number()),
|
||||
planet: b.planet() ?? "",
|
||||
owner: b.owner() ?? "",
|
||||
attacker: b.attacker() ?? "",
|
||||
production: b.production() ?? "",
|
||||
industry: b.industry(),
|
||||
population: b.population(),
|
||||
colonists: b.colonists(),
|
||||
industryStockpile: b.capital(),
|
||||
materialsStockpile: b.material(),
|
||||
attackPower: b.attackPower(),
|
||||
wiped: b.wiped(),
|
||||
});
|
||||
}
|
||||
out.sort((a, b) => a.planetNumber - b.planetNumber);
|
||||
return out;
|
||||
}
|
||||
|
||||
function decodeShipProductions(report: Report): ReportShipProduction[] {
|
||||
const out: ReportShipProduction[] = [];
|
||||
for (let i = 0; i < report.shipProductionLength(); i++) {
|
||||
const sp = report.shipProduction(i);
|
||||
if (sp === null) continue;
|
||||
out.push({
|
||||
planetNumber: Number(sp.planet()),
|
||||
class: sp.class_() ?? "",
|
||||
cost: sp.cost(),
|
||||
prodUsed: sp.prodUsed(),
|
||||
percent: sp.percent(),
|
||||
freeIndustry: sp.free(),
|
||||
});
|
||||
}
|
||||
out.sort((a, b) => {
|
||||
const byPlanet = a.planetNumber - b.planetNumber;
|
||||
if (byPlanet !== 0) return byPlanet;
|
||||
return a.class.localeCompare(b.class);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* uuidToHiLo splits the canonical 36-character UUID string
|
||||
* (`xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`) into the two big-endian
|
||||
@@ -1125,6 +1432,16 @@ export function applyOrderOverlay(
|
||||
localScience: mutatedScience ?? report.localScience ?? [],
|
||||
races: mutatedRaces ?? report.races ?? [],
|
||||
myVoteFor: mutatedVoteFor ?? report.myVoteFor,
|
||||
// Phase 23 read-only fields. No overlay branches touch them
|
||||
// today; the `?? []` keeps a stale HMR-instance of `report`
|
||||
// (loaded before the shape bump) from blanking the Report
|
||||
// View when its section components iterate.
|
||||
players: report.players ?? [],
|
||||
otherScience: report.otherScience ?? [],
|
||||
otherShipClass: report.otherShipClass ?? [],
|
||||
battleIds: report.battleIds ?? [],
|
||||
bombings: report.bombings ?? [],
|
||||
shipProductions: report.shipProductions ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -20,13 +20,18 @@
|
||||
|
||||
import type {
|
||||
GameReport,
|
||||
ReportBombing,
|
||||
ReportIncomingShipGroup,
|
||||
ReportLocalFleet,
|
||||
ReportLocalShipGroup,
|
||||
ReportOtherRace,
|
||||
ReportOtherScience,
|
||||
ReportOtherShipClass,
|
||||
ReportOtherShipGroup,
|
||||
ReportPlanet,
|
||||
ReportPlayer,
|
||||
ReportRoute,
|
||||
ReportShipProduction,
|
||||
ReportUnidentifiedShipGroup,
|
||||
ScienceSummary,
|
||||
ShipClassSummary,
|
||||
@@ -159,6 +164,39 @@ interface SyntheticScience {
|
||||
cargo?: number;
|
||||
}
|
||||
|
||||
interface SyntheticOtherScience extends SyntheticScience {
|
||||
race?: string;
|
||||
}
|
||||
|
||||
interface SyntheticOtherShipClass extends SyntheticShipClass {
|
||||
race?: string;
|
||||
mass?: number;
|
||||
}
|
||||
|
||||
interface SyntheticBombing {
|
||||
planet?: number; // wire field "number"
|
||||
planetName?: string; // wire field "planetName"
|
||||
owner?: string;
|
||||
attacker?: string;
|
||||
production?: string;
|
||||
industry?: number;
|
||||
population?: number;
|
||||
colonists?: number;
|
||||
capital?: number;
|
||||
material?: number;
|
||||
attack?: number;
|
||||
wiped?: boolean;
|
||||
}
|
||||
|
||||
interface SyntheticShipProductionRow {
|
||||
planet?: number;
|
||||
class?: string;
|
||||
cost?: number;
|
||||
prodUsed?: number;
|
||||
percent?: number;
|
||||
free?: number;
|
||||
}
|
||||
|
||||
interface SyntheticReportRoot {
|
||||
turn?: number;
|
||||
mapWidth?: number;
|
||||
@@ -173,12 +211,17 @@ interface SyntheticReportRoot {
|
||||
uninhabitedPlanet?: SyntheticPlanet[];
|
||||
unidentifiedPlanet?: SyntheticPlanet[];
|
||||
localShipClass?: SyntheticShipClass[];
|
||||
otherShipClass?: SyntheticOtherShipClass[];
|
||||
localScience?: SyntheticScience[];
|
||||
otherScience?: SyntheticOtherScience[];
|
||||
localGroup?: SyntheticShipGroup[];
|
||||
otherGroup?: SyntheticShipGroup[];
|
||||
incomingGroup?: SyntheticIncomingGroup[];
|
||||
unidentifiedGroup?: SyntheticUnidentifiedGroup[];
|
||||
localFleet?: SyntheticLocalFleet[];
|
||||
battle?: string[];
|
||||
bombing?: SyntheticBombing[];
|
||||
shipProduction?: SyntheticShipProductionRow[];
|
||||
}
|
||||
|
||||
function decodeSyntheticReport(json: unknown): GameReport {
|
||||
@@ -278,6 +321,78 @@ function decodeSyntheticReport(json: unknown): GameReport {
|
||||
state: typeof f.state === "string" ? f.state : "",
|
||||
}));
|
||||
|
||||
const otherScience: ReportOtherScience[] = (root.otherScience ?? []).map(
|
||||
(sc) => ({
|
||||
race: typeof sc.race === "string" ? sc.race : "",
|
||||
name: typeof sc.name === "string" ? sc.name : "",
|
||||
drive: numOr0(sc.drive),
|
||||
weapons: numOr0(sc.weapons),
|
||||
shields: numOr0(sc.shields),
|
||||
cargo: numOr0(sc.cargo),
|
||||
}),
|
||||
);
|
||||
otherScience.sort((a, b) => {
|
||||
const byRace = a.race.localeCompare(b.race);
|
||||
if (byRace !== 0) return byRace;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
const otherShipClass: ReportOtherShipClass[] = (root.otherShipClass ?? []).map(
|
||||
(sc) => ({
|
||||
race: typeof sc.race === "string" ? sc.race : "",
|
||||
name: typeof sc.name === "string" ? sc.name : "",
|
||||
drive: numOr0(sc.drive),
|
||||
armament: Math.trunc(numOr0(sc.armament)),
|
||||
weapons: numOr0(sc.weapons),
|
||||
shields: numOr0(sc.shields),
|
||||
cargo: numOr0(sc.cargo),
|
||||
// `mass` is on the wire but synthetic fixtures may omit
|
||||
// it; fall back to 0 rather than reject the row.
|
||||
mass: typeof sc.mass === "number" ? sc.mass : 0,
|
||||
}),
|
||||
);
|
||||
otherShipClass.sort((a, b) => {
|
||||
const byRace = a.race.localeCompare(b.race);
|
||||
if (byRace !== 0) return byRace;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
const battleIds: string[] = (root.battle ?? []).filter(
|
||||
(v): v is string => typeof v === "string" && v !== "",
|
||||
);
|
||||
|
||||
const bombings: ReportBombing[] = (root.bombing ?? []).map((b) => ({
|
||||
planetNumber: numOr0(b.planet),
|
||||
planet: typeof b.planetName === "string" ? b.planetName : "",
|
||||
owner: typeof b.owner === "string" ? b.owner : "",
|
||||
attacker: typeof b.attacker === "string" ? b.attacker : "",
|
||||
production: typeof b.production === "string" ? b.production : "",
|
||||
industry: numOr0(b.industry),
|
||||
population: numOr0(b.population),
|
||||
colonists: numOr0(b.colonists),
|
||||
industryStockpile: numOr0(b.capital),
|
||||
materialsStockpile: numOr0(b.material),
|
||||
attackPower: numOr0(b.attack),
|
||||
wiped: b.wiped === true,
|
||||
}));
|
||||
bombings.sort((a, b) => a.planetNumber - b.planetNumber);
|
||||
|
||||
const shipProductions: ReportShipProduction[] = (root.shipProduction ?? []).map(
|
||||
(sp) => ({
|
||||
planetNumber: numOr0(sp.planet),
|
||||
class: typeof sp.class === "string" ? sp.class : "",
|
||||
cost: numOr0(sp.cost),
|
||||
prodUsed: numOr0(sp.prodUsed),
|
||||
percent: numOr0(sp.percent),
|
||||
freeIndustry: numOr0(sp.free),
|
||||
}),
|
||||
);
|
||||
shipProductions.sort((a, b) => {
|
||||
const byPlanet = a.planetNumber - b.planetNumber;
|
||||
if (byPlanet !== 0) return byPlanet;
|
||||
return a.class.localeCompare(b.class);
|
||||
});
|
||||
|
||||
return {
|
||||
turn: numOr0(root.turn),
|
||||
mapWidth: numOr0(root.mapWidth),
|
||||
@@ -301,9 +416,43 @@ function decodeSyntheticReport(json: unknown): GameReport {
|
||||
races: collectOtherRaceRowsFromSynthetic(root, race),
|
||||
myVotes: numOr0(root.votes),
|
||||
myVoteFor: typeof root.voteFor === "string" ? root.voteFor : "",
|
||||
players: collectPlayersFromSynthetic(root, race),
|
||||
otherScience,
|
||||
otherShipClass,
|
||||
battleIds,
|
||||
bombings,
|
||||
shipProductions,
|
||||
};
|
||||
}
|
||||
|
||||
function collectPlayersFromSynthetic(
|
||||
root: SyntheticReportRoot,
|
||||
raceName: string,
|
||||
): ReportPlayer[] {
|
||||
const out: ReportPlayer[] = [];
|
||||
for (const player of root.player ?? []) {
|
||||
const name = typeof player.name === "string" ? player.name : "";
|
||||
if (name === "") continue;
|
||||
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)),
|
||||
votesReceived: numOr0(player.votes),
|
||||
extinct: player.extinct === true,
|
||||
isLocal: name === raceName,
|
||||
});
|
||||
}
|
||||
out.sort((a, b) =>
|
||||
a.name.toLowerCase().localeCompare(b.name.toLowerCase()),
|
||||
);
|
||||
return out;
|
||||
}
|
||||
|
||||
function collectOtherRacesFromSynthetic(
|
||||
root: SyntheticReportRoot,
|
||||
raceName: string,
|
||||
|
||||
Reference in New Issue
Block a user