// Phase 29 coverage for the categories + planetDependents maps that // `reportToWorld` now returns. The map view consumes both to feed // `RendererHandle.setHiddenPrimitiveIds`: categories drive the // per-category toggle, planetDependents drive the cascade (planet // hidden → markers + in-space + incoming groups flying to it hide // together). import { describe, expect, test } from "vitest"; import type { GameReport, ReportBattle, ReportBombing, ReportIncomingShipGroup, ReportLocalShipGroup, ReportOtherShipGroup, ReportPlanet, ReportUnidentifiedShipGroup, } from "../src/api/game-state"; import { BATTLE_MARKER_ID_PREFIX } from "../src/map/battle-markers"; import { SHIP_GROUP_ID_OFFSETS } from "../src/map/ship-groups"; import { reportToWorld } from "../src/map/state-binding"; import { EMPTY_SHIP_GROUPS } from "./helpers/empty-ship-groups"; function makeReport(overrides: Partial = {}): GameReport { return { turn: 1, mapWidth: 4000, mapHeight: 4000, planetCount: 0, planets: [], race: "", localShipClass: [], routes: [], localPlayerDrive: 0, localPlayerWeapons: 0, localPlayerShields: 0, localPlayerCargo: 0, ...EMPTY_SHIP_GROUPS, ...overrides, }; } function makePlanet(overrides: Partial): ReportPlanet { return { number: 0, name: "", x: 0, y: 0, kind: "local", owner: null, size: null, resources: null, industryStockpile: null, materialsStockpile: null, industry: null, population: null, colonists: null, production: null, freeIndustry: null, ...overrides, }; } function makeLocalShipGroup( overrides: Partial, ): ReportLocalShipGroup { return { id: "00000000-0000-0000-0000-000000000000", count: 1, class: "Scout", tech: { drive: 1, weapons: 0, shields: 0, cargo: 0 }, cargo: "NONE", load: 0, destination: 0, origin: null, range: null, speed: 1, mass: 0, state: "InOrbit", fleet: null, race: "Earthlings", ...overrides, }; } function makeOtherShipGroup( overrides: Partial, ): ReportOtherShipGroup { return { count: 1, class: "Cruiser", tech: { drive: 1, weapons: 0, shields: 0, cargo: 0 }, cargo: "NONE", load: 0, destination: 0, origin: null, range: null, speed: 1, mass: 0, race: "Klingons", ...overrides, }; } function makeIncoming( overrides: Partial, ): ReportIncomingShipGroup { return { origin: 0, destination: 0, distance: 0, speed: 1, mass: 1, ...overrides, }; } function makeUnidentified( overrides: Partial, ): ReportUnidentifiedShipGroup { return { x: 0, y: 0, ...overrides }; } function makeBattle(overrides: Partial): ReportBattle { return { id: "battle", planet: 0, shots: 1, ...overrides, }; } function makeBombing(overrides: Partial): ReportBombing { return { planetNumber: 0, planet: "", owner: "", attacker: "", production: "", industry: 0, population: 0, colonists: 0, industryStockpile: 0, materialsStockpile: 0, attackPower: 0, wiped: false, ...overrides, }; } describe("reportToWorld — categories", () => { test("planet primitives carry their kind-flavoured category", () => { const { categories } = reportToWorld( makeReport({ planets: [ makePlanet({ number: 1, kind: "local", x: 100, y: 100 }), makePlanet({ number: 2, kind: "other", x: 200, y: 200 }), makePlanet({ number: 3, kind: "uninhabited", x: 300, y: 300 }), makePlanet({ number: 4, kind: "unidentified", x: 400, y: 400 }), ], }), ); expect(categories.get(1)).toBe("planet-local"); expect(categories.get(2)).toBe("planet-foreign"); expect(categories.get(3)).toBe("planet-uninhabited"); expect(categories.get(4)).toBe("planet-unidentified"); }); test("ship-group sub-builder tags every primitive id", () => { const localGroupPrimId = SHIP_GROUP_ID_OFFSETS.local + 0; const localLineId = SHIP_GROUP_ID_OFFSETS.localLine + 0; const otherId = SHIP_GROUP_ID_OFFSETS.other + 0; const incomingPointId = SHIP_GROUP_ID_OFFSETS.incoming + 0; const incomingLineId = SHIP_GROUP_ID_OFFSETS.incomingLine + 0; const unidentifiedId = SHIP_GROUP_ID_OFFSETS.unidentified + 0; const { categories } = reportToWorld( makeReport({ planets: [ makePlanet({ number: 1, kind: "local", x: 100, y: 100 }), makePlanet({ number: 2, kind: "other", x: 200, y: 200 }), ], localShipGroups: [ makeLocalShipGroup({ origin: 1, range: 10, destination: 2 }), ], otherShipGroups: [ makeOtherShipGroup({ origin: 1, range: 10, destination: 2 }), ], incomingShipGroups: [ makeIncoming({ origin: 1, destination: 2, distance: 5 }), ], unidentifiedShipGroups: [makeUnidentified({ x: 500, y: 500 })], }), ); expect(categories.get(localGroupPrimId)).toBe("hyperspaceGroup"); expect(categories.get(localLineId)).toBe("hyperspaceGroup"); expect(categories.get(otherId)).toBe("hyperspaceGroup"); expect(categories.get(incomingPointId)).toBe("incomingGroup"); expect(categories.get(incomingLineId)).toBe("incomingGroup"); expect(categories.get(unidentifiedId)).toBe("unidentifiedGroup"); }); test("battle markers carry the battleMarker category", () => { const { categories } = reportToWorld( makeReport({ planets: [ makePlanet({ number: 1, kind: "local", x: 100, y: 100 }), makePlanet({ number: 2, kind: "other", x: 200, y: 200 }), ], battles: [makeBattle({ id: "b1", planet: 2 })], // F8-12 / #30: bombings no longer emit their own // primitives — the planet outline is drawn by // `setPlanetOutlines` from the map view. bombings: [makeBombing({ planetNumber: 2 })], }), ); // Battle marker emits two LinePrims at `BATTLE_MARKER_ID_PREFIX | (i << 4) | (A|B)`. const battleA = BATTLE_MARKER_ID_PREFIX | (0 << 4) | 0; const battleB = BATTLE_MARKER_ID_PREFIX | (0 << 4) | 1; expect(categories.get(battleA)).toBe("battleMarker"); expect(categories.get(battleB)).toBe("battleMarker"); }); }); describe("reportToWorld — planetDependents", () => { test("every planet seeds its own dependents entry with its own id", () => { const { planetDependents } = reportToWorld( makeReport({ planets: [ makePlanet({ number: 1, kind: "local", x: 100, y: 100 }), makePlanet({ number: 7, kind: "other", x: 200, y: 200 }), ], }), ); expect(planetDependents.get(1)?.has(1)).toBe(true); expect(planetDependents.get(7)?.has(7)).toBe(true); }); test("battle markers cascade onto their anchor planet", () => { const { planetDependents } = reportToWorld( makeReport({ planets: [ makePlanet({ number: 1, kind: "local", x: 100, y: 100 }), makePlanet({ number: 2, kind: "other", x: 200, y: 200 }), ], battles: [makeBattle({ planet: 2 })], // Bombings are still in the report but no primitive // rides the cascade now — they paint a planet outline // straight from `map.svelte`. bombings: [makeBombing({ planetNumber: 2 })], }), ); const battleA = BATTLE_MARKER_ID_PREFIX | (0 << 4) | 0; const battleB = BATTLE_MARKER_ID_PREFIX | (0 << 4) | 1; const deps = planetDependents.get(2) ?? new Set(); expect(deps.has(2)).toBe(true); expect(deps.has(battleA)).toBe(true); expect(deps.has(battleB)).toBe(true); }); test("in-space groups cascade onto their destination planet", () => { const { planetDependents } = reportToWorld( makeReport({ planets: [ makePlanet({ number: 1, kind: "local", x: 100, y: 100 }), makePlanet({ number: 2, kind: "other", x: 200, y: 200 }), ], localShipGroups: [ makeLocalShipGroup({ origin: 1, range: 10, destination: 2 }), ], otherShipGroups: [ makeOtherShipGroup({ origin: 1, range: 10, destination: 2 }), ], }), ); const localPointId = SHIP_GROUP_ID_OFFSETS.local + 0; const localLineId = SHIP_GROUP_ID_OFFSETS.localLine + 0; const otherId = SHIP_GROUP_ID_OFFSETS.other + 0; const deps = planetDependents.get(2) ?? new Set(); expect(deps.has(localPointId)).toBe(true); expect(deps.has(localLineId)).toBe(true); expect(deps.has(otherId)).toBe(true); }); test("incoming groups cascade onto their destination planet", () => { const { planetDependents } = reportToWorld( makeReport({ planets: [ makePlanet({ number: 1, kind: "local", x: 100, y: 100 }), makePlanet({ number: 2, kind: "other", x: 200, y: 200 }), ], incomingShipGroups: [ makeIncoming({ origin: 1, destination: 2, distance: 5 }), ], }), ); const incomingPointId = SHIP_GROUP_ID_OFFSETS.incoming + 0; const incomingLineId = SHIP_GROUP_ID_OFFSETS.incomingLine + 0; const deps = planetDependents.get(2) ?? new Set(); expect(deps.has(incomingPointId)).toBe(true); expect(deps.has(incomingLineId)).toBe(true); }); test("unidentified groups do not contribute to any planet's dependents", () => { const { planetDependents } = reportToWorld( makeReport({ planets: [makePlanet({ number: 1, kind: "local", x: 100, y: 100 })], unidentifiedShipGroups: [makeUnidentified({ x: 500, y: 500 })], }), ); // Only the local planet seeds its own entry; no other entries. expect(planetDependents.size).toBe(1); expect(planetDependents.get(1)?.size).toBe(1); }); });