Files
galaxy-game/ui/frontend/tests/e2e/fixtures/report-fbs.ts
T
Ilia Denisov 7c8b5aeb23 ui/phase-16: cargo routes inspector + map pick foundation
Add per-planet cargo routes (COL/CAP/MAT/EMP) to the inspector with
a renderer-driven destination picker (faded out-of-reach planets,
cursor-line anchor, hover-highlight) and per-route arrows on the
map. The pick-mode primitives are exposed via `MapPickService` so
ship-group dispatch in Phase 19/20 can reuse the same surface.

Pass A — generic map foundation:
- hit-test now sizes the click zone to `pointRadiusPx + slopPx` so
  the visible disc is always part of the target.
- `RendererHandle` gains `onPointerMove`, `onHoverChange`,
  `setPickMode`, `getPickState`, `getPrimitiveAlpha`,
  `setExtraPrimitives`, `getPrimitives`. The click dispatcher is
  centralised: pick-mode swallows clicks atomically so the standard
  selection consumers do not race against teardown.
- `MapPickService` (`lib/map-pick.svelte.ts`) wraps the renderer
  contract in a promise-shaped `pick(...)`. The in-game shell
  layout owns the service so sidebar and bottom-sheet inspectors
  see the same instance.
- Debug-surface registry exposes `getMapPrimitives`,
  `getMapPickState`, `getMapCamera` to e2e specs without spawning a
  separate debug page after navigation.

Pass B — cargo-route feature:
- `CargoLoadType`, `setCargoRoute`, `removeCargoRoute` typed
  variants with `(source, loadType)` collapse rule on the order
  draft; round-trip through the FBS encoder/decoder.
- `GameReport` decodes `routes` and the local player's drive tech
  for the inline reach formula (40 × drive). `applyOrderOverlay`
  upserts/drops route entries for valid/submitting/applied
  commands.
- `lib/inspectors/planet/cargo-routes.svelte` renders the
  four-slot section. `Add` / `Edit` call `MapPickService.pick`,
  `Remove` emits `removeCargoRoute`.
- `map/cargo-routes.ts` builds shaft + arrowhead primitives per
  cargo type; the map view pushes them through
  `setExtraPrimitives` so the renderer never re-inits Pixi on
  route mutations (Pixi 8 doesn't support that on a reused
  canvas).

Docs:
- `docs/cargo-routes-ux.md` covers engine semantics + UI map.
- `docs/renderer.md` documents pick mode and the debug surface.
- `docs/calc-bridge.md` records the Phase 16 reach waiver.
- `PLAN.md` rewrites Phase 16 to reflect the foundation + feature
  split and the decisions baked in (map-driven picker, inline
  reach, optimistic overlay via `setExtraPrimitives`).

Tests:
- `tests/map-pick-mode.test.ts` — pure overlay-spec helper.
- `tests/map-cargo-routes.test.ts` — `buildCargoRouteLines`.
- `tests/inspector-planet-cargo-routes.test.ts` — slot rendering,
  picker invocation, collapse, cancel, remove.
- Extensions to `order-draft`, `submit`, `order-load`,
  `order-overlay`, `state-binding`, `inspector-planet`,
  `inspector-overlay`, `game-shell-sidebar`, `game-shell-header`.
- `tests/e2e/cargo-routes.spec.ts` — Playwright happy path: add
  COL, add CAP, remove COL, asserting both the inspector and the
  arrow count via `__galaxyDebug.getMapPrimitives()`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 20:01:34 +02:00

249 lines
8.6 KiB
TypeScript

// Phase 11 helpers for forging FlatBuffers report payloads in e2e
// tests. Mirrors the engine's `report.Report` shape so the mocked
// gateway can return realistic data without standing up the real
// engine container.
//
// Phase 11 only renders planets, so the helpers keep the report shape
// minimal (turn / dimensions / planet vectors). Phase 13 extended the
// fixture with the optional rich planet fields (size, resources,
// stockpiles, population, industry, colonists, production, free
// industry) so the inspector e2e can drive the read-only display
// against realistic values. Phase 15 adds a minimal `LocalShipClass`
// projection so the planet inspector's Build-Ship sub-picker has data
// in e2e specs (`name` only — Phase 17 widens this when ship-class
// CRUD lands). Later phases extend the helper as fleets, sciences,
// etc. land.
import { Builder } from "flatbuffers";
import {
LocalPlanet,
OtherPlanet,
Player,
Report,
Route,
RouteEntry,
ShipClass,
UnidentifiedPlanet,
UninhabitedPlanet,
} from "../../../src/proto/galaxy/fbs/report";
export interface PlanetFixture {
number: number;
name: string;
x: number;
y: number;
size?: number;
resources?: number;
capital?: number;
material?: number;
}
export interface InhabitedFixture extends PlanetFixture {
population?: number;
colonists?: number;
industry?: number;
production?: string;
freeIndustry?: number;
}
export interface OtherPlanetFixture extends InhabitedFixture {
owner: string;
}
export interface ShipClassFixture {
name: string;
}
export interface PlayerFixture {
name: string;
drive?: number;
}
export interface RouteEntryFixture {
loadType: "COL" | "CAP" | "MAT" | "EMP";
destinationPlanetNumber: number;
}
export interface RouteFixture {
sourcePlanetNumber: number;
entries: RouteEntryFixture[];
}
export interface ReportFixture {
turn: number;
mapWidth?: number;
mapHeight?: number;
localPlanets?: InhabitedFixture[];
otherPlanets?: OtherPlanetFixture[];
uninhabitedPlanets?: PlanetFixture[];
unidentifiedPlanets?: { number: number; x: number; y: number }[];
localShipClass?: ShipClassFixture[];
race?: string;
players?: PlayerFixture[];
routes?: RouteFixture[];
}
export function buildReportPayload(fixture: ReportFixture): Uint8Array {
const builder = new Builder(512);
const localOffsets = (fixture.localPlanets ?? []).map((planet) => {
const name = builder.createString(planet.name);
const production =
planet.production !== undefined
? builder.createString(planet.production)
: null;
LocalPlanet.startLocalPlanet(builder);
LocalPlanet.addNumber(builder, BigInt(planet.number));
LocalPlanet.addX(builder, planet.x);
LocalPlanet.addY(builder, planet.y);
LocalPlanet.addName(builder, name);
LocalPlanet.addSize(builder, planet.size ?? 10);
LocalPlanet.addResources(builder, planet.resources ?? 0.5);
LocalPlanet.addCapital(builder, planet.capital ?? 0);
LocalPlanet.addMaterial(builder, planet.material ?? 0);
LocalPlanet.addPopulation(builder, planet.population ?? 0);
LocalPlanet.addIndustry(builder, planet.industry ?? 0);
LocalPlanet.addColonists(builder, planet.colonists ?? 0);
if (production !== null) LocalPlanet.addProduction(builder, production);
LocalPlanet.addFreeIndustry(builder, planet.freeIndustry ?? 0);
return LocalPlanet.endLocalPlanet(builder);
});
const otherOffsets = (fixture.otherPlanets ?? []).map((planet) => {
const name = builder.createString(planet.name);
const owner = builder.createString(planet.owner);
const production =
planet.production !== undefined
? builder.createString(planet.production)
: null;
OtherPlanet.startOtherPlanet(builder);
OtherPlanet.addNumber(builder, BigInt(planet.number));
OtherPlanet.addX(builder, planet.x);
OtherPlanet.addY(builder, planet.y);
OtherPlanet.addName(builder, name);
OtherPlanet.addOwner(builder, owner);
OtherPlanet.addSize(builder, planet.size ?? 9);
OtherPlanet.addResources(builder, planet.resources ?? 0.5);
OtherPlanet.addCapital(builder, planet.capital ?? 0);
OtherPlanet.addMaterial(builder, planet.material ?? 0);
OtherPlanet.addPopulation(builder, planet.population ?? 0);
OtherPlanet.addIndustry(builder, planet.industry ?? 0);
OtherPlanet.addColonists(builder, planet.colonists ?? 0);
if (production !== null) OtherPlanet.addProduction(builder, production);
OtherPlanet.addFreeIndustry(builder, planet.freeIndustry ?? 0);
return OtherPlanet.endOtherPlanet(builder);
});
const uninhabitedOffsets = (fixture.uninhabitedPlanets ?? []).map(
(planet) => {
const name = builder.createString(planet.name);
UninhabitedPlanet.startUninhabitedPlanet(builder);
UninhabitedPlanet.addNumber(builder, BigInt(planet.number));
UninhabitedPlanet.addX(builder, planet.x);
UninhabitedPlanet.addY(builder, planet.y);
UninhabitedPlanet.addName(builder, name);
UninhabitedPlanet.addSize(builder, planet.size ?? 6);
UninhabitedPlanet.addResources(builder, planet.resources ?? 0.5);
UninhabitedPlanet.addCapital(builder, planet.capital ?? 0);
UninhabitedPlanet.addMaterial(builder, planet.material ?? 0);
return UninhabitedPlanet.endUninhabitedPlanet(builder);
},
);
const unidentifiedOffsets = (fixture.unidentifiedPlanets ?? []).map(
(planet) => {
UnidentifiedPlanet.startUnidentifiedPlanet(builder);
UnidentifiedPlanet.addNumber(builder, BigInt(planet.number));
UnidentifiedPlanet.addX(builder, planet.x);
UnidentifiedPlanet.addY(builder, planet.y);
return UnidentifiedPlanet.endUnidentifiedPlanet(builder);
},
);
const localShipClassOffsets = (fixture.localShipClass ?? []).map((cls) => {
const name = builder.createString(cls.name);
ShipClass.startShipClass(builder);
ShipClass.addName(builder, name);
return ShipClass.endShipClass(builder);
});
const playerOffsets = (fixture.players ?? []).map((p) => {
const name = builder.createString(p.name);
Player.startPlayer(builder);
Player.addName(builder, name);
Player.addDrive(builder, p.drive ?? 1);
return Player.endPlayer(builder);
});
const routeOffsets = (fixture.routes ?? []).map((route) => {
const entryOffsets = route.entries.map((entry) => {
const valueOffset = builder.createString(entry.loadType);
RouteEntry.startRouteEntry(builder);
RouteEntry.addKey(builder, BigInt(entry.destinationPlanetNumber));
RouteEntry.addValue(builder, valueOffset);
return RouteEntry.endRouteEntry(builder);
});
const entriesVec = Route.createRouteVector(builder, entryOffsets);
Route.startRoute(builder);
Route.addPlanet(builder, BigInt(route.sourcePlanetNumber));
Route.addRoute(builder, entriesVec);
return Route.endRoute(builder);
});
const localVec =
localOffsets.length === 0
? null
: Report.createLocalPlanetVector(builder, localOffsets);
const otherVec =
otherOffsets.length === 0
? null
: Report.createOtherPlanetVector(builder, otherOffsets);
const uninhabitedVec =
uninhabitedOffsets.length === 0
? null
: Report.createUninhabitedPlanetVector(builder, uninhabitedOffsets);
const unidentifiedVec =
unidentifiedOffsets.length === 0
? null
: Report.createUnidentifiedPlanetVector(builder, unidentifiedOffsets);
const localShipClassVec =
localShipClassOffsets.length === 0
? null
: Report.createLocalShipClassVector(builder, localShipClassOffsets);
const playerVec =
playerOffsets.length === 0
? null
: Report.createPlayerVector(builder, playerOffsets);
const routeVec =
routeOffsets.length === 0
? null
: Report.createRouteVector(builder, routeOffsets);
const raceOffset =
fixture.race === undefined ? null : builder.createString(fixture.race);
const totalPlanets =
(fixture.localPlanets ?? []).length +
(fixture.otherPlanets ?? []).length +
(fixture.uninhabitedPlanets ?? []).length +
(fixture.unidentifiedPlanets ?? []).length;
Report.startReport(builder);
Report.addTurn(builder, BigInt(fixture.turn));
Report.addWidth(builder, fixture.mapWidth ?? 4000);
Report.addHeight(builder, fixture.mapHeight ?? 4000);
Report.addPlanetCount(builder, totalPlanets);
if (raceOffset !== null) Report.addRace(builder, raceOffset);
if (playerVec !== null) Report.addPlayer(builder, playerVec);
if (localVec !== null) Report.addLocalPlanet(builder, localVec);
if (otherVec !== null) Report.addOtherPlanet(builder, otherVec);
if (uninhabitedVec !== null) Report.addUninhabitedPlanet(builder, uninhabitedVec);
if (unidentifiedVec !== null) Report.addUnidentifiedPlanet(builder, unidentifiedVec);
if (localShipClassVec !== null)
Report.addLocalShipClass(builder, localShipClassVec);
if (routeVec !== null) Report.addRoute(builder, routeVec);
const reportOff = Report.endReport(builder);
builder.finish(reportOff);
return builder.asUint8Array();
}