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:
@@ -17,15 +17,20 @@
|
||||
|
||||
import { Builder } from "flatbuffers";
|
||||
|
||||
import { UUID } from "../../../src/proto/galaxy/fbs/common";
|
||||
import {
|
||||
Bombing,
|
||||
LocalPlanet,
|
||||
OtherPlanet,
|
||||
OtherScience,
|
||||
OthersShipClass,
|
||||
Player,
|
||||
Report,
|
||||
Route,
|
||||
RouteEntry,
|
||||
Science,
|
||||
ShipClass,
|
||||
ShipProduction,
|
||||
UnidentifiedPlanet,
|
||||
UninhabitedPlanet,
|
||||
} from "../../../src/proto/galaxy/fbs/report";
|
||||
@@ -94,6 +99,39 @@ export interface RouteFixture {
|
||||
entries: RouteEntryFixture[];
|
||||
}
|
||||
|
||||
export interface OtherScienceFixture extends ScienceFixture {
|
||||
race: string;
|
||||
}
|
||||
|
||||
export interface OtherShipClassFixture extends ShipClassFixture {
|
||||
race: string;
|
||||
mass?: number;
|
||||
}
|
||||
|
||||
export interface BombingFixture {
|
||||
planetNumber: number;
|
||||
planet: string;
|
||||
owner: string;
|
||||
attacker: string;
|
||||
production?: string;
|
||||
industry?: number;
|
||||
population?: number;
|
||||
colonists?: number;
|
||||
capital?: number;
|
||||
material?: number;
|
||||
attackPower?: number;
|
||||
wiped?: boolean;
|
||||
}
|
||||
|
||||
export interface ShipProductionFixture {
|
||||
planet: number;
|
||||
class: string;
|
||||
cost?: number;
|
||||
prodUsed?: number;
|
||||
percent?: number;
|
||||
free?: number;
|
||||
}
|
||||
|
||||
export interface ReportFixture {
|
||||
turn: number;
|
||||
mapWidth?: number;
|
||||
@@ -109,6 +147,11 @@ export interface ReportFixture {
|
||||
routes?: RouteFixture[];
|
||||
myVotes?: number;
|
||||
myVoteFor?: string;
|
||||
otherScience?: OtherScienceFixture[];
|
||||
otherShipClass?: OtherShipClassFixture[];
|
||||
battles?: string[];
|
||||
bombings?: BombingFixture[];
|
||||
shipProductions?: ShipProductionFixture[];
|
||||
}
|
||||
|
||||
export function buildReportPayload(fixture: ReportFixture): Uint8Array {
|
||||
@@ -245,6 +288,67 @@ export function buildReportPayload(fixture: ReportFixture): Uint8Array {
|
||||
return Route.endRoute(builder);
|
||||
});
|
||||
|
||||
const otherScienceOffsets = (fixture.otherScience ?? []).map((sci) => {
|
||||
const race = builder.createString(sci.race);
|
||||
const name = builder.createString(sci.name);
|
||||
OtherScience.startOtherScience(builder);
|
||||
OtherScience.addRace(builder, race);
|
||||
OtherScience.addName(builder, name);
|
||||
OtherScience.addDrive(builder, sci.drive ?? 0);
|
||||
OtherScience.addWeapons(builder, sci.weapons ?? 0);
|
||||
OtherScience.addShields(builder, sci.shields ?? 0);
|
||||
OtherScience.addCargo(builder, sci.cargo ?? 0);
|
||||
return OtherScience.endOtherScience(builder);
|
||||
});
|
||||
|
||||
const otherShipClassOffsets = (fixture.otherShipClass ?? []).map((cls) => {
|
||||
const race = builder.createString(cls.race);
|
||||
const name = builder.createString(cls.name);
|
||||
OthersShipClass.startOthersShipClass(builder);
|
||||
OthersShipClass.addRace(builder, race);
|
||||
OthersShipClass.addName(builder, name);
|
||||
OthersShipClass.addDrive(builder, cls.drive ?? 0);
|
||||
OthersShipClass.addArmament(builder, BigInt(cls.armament ?? 0));
|
||||
OthersShipClass.addWeapons(builder, cls.weapons ?? 0);
|
||||
OthersShipClass.addShields(builder, cls.shields ?? 0);
|
||||
OthersShipClass.addCargo(builder, cls.cargo ?? 0);
|
||||
OthersShipClass.addMass(builder, cls.mass ?? 0);
|
||||
return OthersShipClass.endOthersShipClass(builder);
|
||||
});
|
||||
|
||||
const bombingOffsets = (fixture.bombings ?? []).map((b) => {
|
||||
const planet = builder.createString(b.planet);
|
||||
const owner = builder.createString(b.owner);
|
||||
const attacker = builder.createString(b.attacker);
|
||||
const production = builder.createString(b.production ?? "");
|
||||
Bombing.startBombing(builder);
|
||||
Bombing.addNumber(builder, BigInt(b.planetNumber));
|
||||
Bombing.addPlanet(builder, planet);
|
||||
Bombing.addOwner(builder, owner);
|
||||
Bombing.addAttacker(builder, attacker);
|
||||
Bombing.addProduction(builder, production);
|
||||
Bombing.addIndustry(builder, b.industry ?? 0);
|
||||
Bombing.addPopulation(builder, b.population ?? 0);
|
||||
Bombing.addColonists(builder, b.colonists ?? 0);
|
||||
Bombing.addCapital(builder, b.capital ?? 0);
|
||||
Bombing.addMaterial(builder, b.material ?? 0);
|
||||
Bombing.addAttackPower(builder, b.attackPower ?? 0);
|
||||
Bombing.addWiped(builder, b.wiped ?? false);
|
||||
return Bombing.endBombing(builder);
|
||||
});
|
||||
|
||||
const shipProductionOffsets = (fixture.shipProductions ?? []).map((sp) => {
|
||||
const className = builder.createString(sp.class);
|
||||
ShipProduction.startShipProduction(builder);
|
||||
ShipProduction.addPlanet(builder, BigInt(sp.planet));
|
||||
ShipProduction.addClass(builder, className);
|
||||
ShipProduction.addCost(builder, sp.cost ?? 0);
|
||||
ShipProduction.addProdUsed(builder, sp.prodUsed ?? 0);
|
||||
ShipProduction.addPercent(builder, sp.percent ?? 0);
|
||||
ShipProduction.addFree(builder, sp.free ?? 0);
|
||||
return ShipProduction.endShipProduction(builder);
|
||||
});
|
||||
|
||||
const localVec =
|
||||
localOffsets.length === 0
|
||||
? null
|
||||
@@ -277,6 +381,36 @@ export function buildReportPayload(fixture: ReportFixture): Uint8Array {
|
||||
routeOffsets.length === 0
|
||||
? null
|
||||
: Report.createRouteVector(builder, routeOffsets);
|
||||
const otherScienceVec =
|
||||
otherScienceOffsets.length === 0
|
||||
? null
|
||||
: Report.createOtherScienceVector(builder, otherScienceOffsets);
|
||||
const otherShipClassVec =
|
||||
otherShipClassOffsets.length === 0
|
||||
? null
|
||||
: Report.createOtherShipClassVector(builder, otherShipClassOffsets);
|
||||
const bombingVec =
|
||||
bombingOffsets.length === 0
|
||||
? null
|
||||
: Report.createBombingVector(builder, bombingOffsets);
|
||||
const shipProductionVec =
|
||||
shipProductionOffsets.length === 0
|
||||
? null
|
||||
: Report.createShipProductionVector(builder, shipProductionOffsets);
|
||||
// `battle` is a struct vector (16 bytes per UUID, alignment 8), so
|
||||
// it uses the start/inline-write/end pattern rather than a typical
|
||||
// offset-list helper. Iterating in reverse matches the FlatBuffers
|
||||
// convention that the vector is built end-to-start.
|
||||
const battleVec = (() => {
|
||||
const ids = fixture.battles ?? [];
|
||||
if (ids.length === 0) return null;
|
||||
Report.startBattleVector(builder, ids.length);
|
||||
for (let i = ids.length - 1; i >= 0; i--) {
|
||||
const [hi, lo] = uuidToHiLo(ids[i]!);
|
||||
UUID.createUUID(builder, hi, lo);
|
||||
}
|
||||
return builder.endVector();
|
||||
})();
|
||||
const raceOffset =
|
||||
fixture.race === undefined ? null : builder.createString(fixture.race);
|
||||
const voteForOffset =
|
||||
@@ -308,7 +442,25 @@ export function buildReportPayload(fixture: ReportFixture): Uint8Array {
|
||||
if (localScienceVec !== null)
|
||||
Report.addLocalScience(builder, localScienceVec);
|
||||
if (routeVec !== null) Report.addRoute(builder, routeVec);
|
||||
if (otherScienceVec !== null)
|
||||
Report.addOtherScience(builder, otherScienceVec);
|
||||
if (otherShipClassVec !== null)
|
||||
Report.addOtherShipClass(builder, otherShipClassVec);
|
||||
if (battleVec !== null) Report.addBattle(builder, battleVec);
|
||||
if (bombingVec !== null) Report.addBombing(builder, bombingVec);
|
||||
if (shipProductionVec !== null)
|
||||
Report.addShipProduction(builder, shipProductionVec);
|
||||
const reportOff = Report.endReport(builder);
|
||||
builder.finish(reportOff);
|
||||
return builder.asUint8Array();
|
||||
}
|
||||
|
||||
function uuidToHiLo(value: string): [bigint, bigint] {
|
||||
const hex = value.replace(/-/g, "").toLowerCase();
|
||||
if (hex.length !== 32 || /[^0-9a-f]/.test(hex)) {
|
||||
throw new Error(`buildReportPayload: invalid battle uuid ${value}`);
|
||||
}
|
||||
const hi = BigInt(`0x${hex.slice(0, 16)}`);
|
||||
const lo = BigInt(`0x${hex.slice(16, 32)}`);
|
||||
return [hi, lo];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user