7c8b5aeb23
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>
249 lines
8.6 KiB
TypeScript
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();
|
|
}
|