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:
@@ -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