// State binding between the typed game report and the renderer's // World. Phase 11 emitted primitives only for planets; Phase 19 // extends the binding with ship-group primitives (own / foreign / in- // hyperspace / incoming / unidentified) plus a `hitLookup` map so the // click handler can dispatch a renderer-side hit back to the right // selection variant. Later phases extend with ship-class reach // circles (Phase 17 / 18 in `ui/core/calc/`), reach / visibility // zones, and battle / bombing markers (Phase 27). // // The four planet kinds in the report each map to a distinct style so // the user can tell own / other-race / uninhabited / unidentified // planets apart at a glance. The exact colours are Phase 11 defaults // chosen against the dark theme; Phase 35 polish picks final // colours and adds theme switching. import type { GameReport, ReportPlanet } from "../api/game-state"; import type { ShipGroupRef } from "../lib/selection.svelte"; import { buildBattleAndBombingMarkers } from "./battle-markers"; import { shipGroupsToPrimitives } from "./ship-groups"; import { World, type Primitive, type PrimitiveID, type Style } from "./world"; const STYLE_LOCAL: Style = { fillColor: 0x6dd2ff, fillAlpha: 1, pointRadiusPx: 6, }; const STYLE_OTHER: Style = { fillColor: 0xff8a65, fillAlpha: 1, pointRadiusPx: 5, }; const STYLE_UNINHABITED: Style = { fillColor: 0xb0bec5, fillAlpha: 0.85, pointRadiusPx: 4, }; const STYLE_UNIDENTIFIED: Style = { fillColor: 0x546e7a, fillAlpha: 0.7, pointRadiusPx: 3, }; // PlanetIDs occupy the [0, 4_000_000_000) range — well below // JavaScript's `Number.MAX_SAFE_INTEGER` — so the engine `number` // (uint64) fits in a primitive id (number) without truncation. The // binding uses the engine number directly as the primitive id so the // click handler can recover a planet by hit-test result without an // extra lookup. function styleFor(kind: ReportPlanet["kind"]): Style { switch (kind) { case "local": return STYLE_LOCAL; case "other": return STYLE_OTHER; case "uninhabited": return STYLE_UNINHABITED; case "unidentified": return STYLE_UNIDENTIFIED; } } function priorityFor(kind: ReportPlanet["kind"]): number { switch (kind) { case "local": return 4; case "other": return 3; case "uninhabited": return 2; case "unidentified": return 1; } } /** * HitTarget describes which game entity a renderer-side hit-test * resolves to. The click handler in `lib/active-view/map.svelte` * looks the hit primitive's id up in the binding's hitLookup map * and dispatches `selection.selectPlanet` or * `selection.selectShipGroup` accordingly. */ export type HitTarget = | { kind: "planet"; number: number } | { kind: "shipGroup"; ref: ShipGroupRef } | { kind: "battle"; battleId: string; planet: number } | { kind: "bombing"; planet: number }; export interface ReportToWorldResult { world: World; hitLookup: Map; } /** * reportToWorld translates a GameReport into a renderer-ready World * containing one Point primitive per planet (all four planet kinds) * plus the Phase 19 ship-group surface — own / foreign groups * (on-planet or in-hyperspace), incoming groups (dashed trajectory * line + clickable point), and unidentified-group blips. The world * rectangle matches `report.mapWidth` × `report.mapHeight`. * * If the report carries zero planets (turn-zero edge cases or seeded * tests), the World is still well-formed: the renderer mounts on an * empty primitive list without errors. */ export function reportToWorld(report: GameReport): ReportToWorldResult { const primitives: Primitive[] = []; const hitLookup = new Map(); for (const planet of report.planets) { primitives.push({ kind: "point", id: planet.number, priority: priorityFor(planet.kind), style: styleFor(planet.kind), hitSlopPx: 0, x: planet.x, y: planet.y, }); hitLookup.set(planet.number, { kind: "planet", number: planet.number }); } const groups = shipGroupsToPrimitives(report); for (const prim of groups.primitives) { primitives.push(prim); } for (const [primId, ref] of groups.lookup) { hitLookup.set(primId, { kind: "shipGroup", ref }); } const markers = buildBattleAndBombingMarkers(report); for (const prim of markers.primitives) { primitives.push(prim); } for (const [primId, target] of markers.lookup) { hitLookup.set(primId, target); } const width = report.mapWidth > 0 ? report.mapWidth : 1; const height = report.mapHeight > 0 ? report.mapHeight : 1; return { world: new World(width, height, primitives), hitLookup }; }