// Phase 27 battle markers on the map. Bombing markers used to live // here as a separate ring primitive but F8-12 / #30 turned them into // a planet-outline overlay drawn by `render.ts.setPlanetOutlines`, // driven from `map.svelte`. The remaining surface here is the battle // X-cross: // // * Battle marker — an X cross drawn through the corners of the // square that circumscribes the planet circle. Two yellow // LinePrim, stroke width scales linearly with the number of // shots: 1 shot → 1px, 100+ shots → 5px (capped). Clicking // either line opens the Battle Viewer for the corresponding // UUID. import type { GameReport, ReportPlanet } from "../api/game-state"; import { DARK_THEME, type LinePrim, type Primitive, type PrimitiveID, type Style, type Theme, } from "./world"; /** Battle marker primitive ids use a high-bit prefix to avoid * colliding with planet numbers or cargo-route line ids. */ export const BATTLE_MARKER_ID_PREFIX = 0xa0000000; const PLANET_RADIUS_WORLD = 6; const BATTLE_CROSS_HALF = PLANET_RADIUS_WORLD + 2; /** Battle marker priority sits between planets (1..4) and cargo * routes; the cross is over the planet but loses clicks against the * planet glyph itself. */ const BATTLE_MARKER_PRIORITY = 9; const BATTLE_LINE_INDEX_A = 0; const BATTLE_LINE_INDEX_B = 1; export interface BattleMarkerTarget { kind: "battle"; battleId: string; planet: number; } export type MarkerTarget = BattleMarkerTarget; /** * MarkerCategory tags every emitted primitive with the toggleable * surface it belongs to so the Phase 29 hide-set machinery can flip * each independently. Battle markers are the only category left here; * the `bombingMarker` toggle now hides the planet-outline overlay * built in `map.svelte.applyPlanetOutlines` (F8-12 / #30). */ export type MarkerCategory = "battleMarker"; export interface BuildMarkersResult { primitives: Primitive[]; lookup: Map; categories: Map; /** * planetDependents maps the anchor planet number to the ids of * markers drawn on it; the Phase 29 cascade hides the markers * together with the planet when the planet itself is filtered out * (kind toggle off or unreachable filter on). */ planetDependents: Map>; } /** * battleMarkerStrokeWidth maps a battle's `shots` count to a stroke * width in pixels. 1 shot → 1 px (the thinnest visible), 100+ shots * → 5 px (the cap). Linearly interpolated between those bounds. */ export function battleMarkerStrokeWidth(shots: number): number { if (shots <= 1) return 1; if (shots >= 100) return 5; return 1 + ((shots - 1) * 4) / 99; } /** * buildBattleAndBombingMarkers emits battle X-cross primitives plus a * hit-lookup mapping for the current-turn report. Battles whose * planet is not visible (e.g. observer-only without a report.planets * entry) are skipped — they have no on-map location to anchor * against. Bombing visuals are no longer produced here (F8-12 / #30); * the renderer paints them as a planet-outline overlay driven from * `map.svelte.applyPlanetOutlines`. */ export function buildBattleAndBombingMarkers( report: GameReport, theme: Theme = DARK_THEME, ): BuildMarkersResult { const planetByNumber = new Map(); for (const planet of report.planets) { planetByNumber.set(planet.number, planet); } const primitives: Primitive[] = []; const lookup = new Map(); const categories = new Map(); const planetDependents = new Map>(); const addDependent = (planetNumber: number, id: PrimitiveID): void => { let set = planetDependents.get(planetNumber); if (set === undefined) { set = new Set(); planetDependents.set(planetNumber, set); } set.add(id); }; for (let i = 0; i < report.battles.length; i++) { const battle = report.battles[i]; const planet = planetByNumber.get(battle.planet); if (planet === undefined) continue; const strokeWidthPx = battleMarkerStrokeWidth(battle.shots); const style: Style = { strokeColor: theme.battleMarker, strokeAlpha: 0.95, strokeWidthPx, }; const baseId = BATTLE_MARKER_ID_PREFIX | (i << 4); const lineA: LinePrim = { kind: "line", id: baseId | BATTLE_LINE_INDEX_A, priority: BATTLE_MARKER_PRIORITY, style, hitSlopPx: 0, x1: planet.x - BATTLE_CROSS_HALF, y1: planet.y - BATTLE_CROSS_HALF, x2: planet.x + BATTLE_CROSS_HALF, y2: planet.y + BATTLE_CROSS_HALF, }; const lineB: LinePrim = { kind: "line", id: baseId | BATTLE_LINE_INDEX_B, priority: BATTLE_MARKER_PRIORITY, style, hitSlopPx: 0, x1: planet.x - BATTLE_CROSS_HALF, y1: planet.y + BATTLE_CROSS_HALF, x2: planet.x + BATTLE_CROSS_HALF, y2: planet.y - BATTLE_CROSS_HALF, }; const target: BattleMarkerTarget = { kind: "battle", battleId: battle.id, planet: battle.planet, }; primitives.push(lineA, lineB); lookup.set(lineA.id, target); lookup.set(lineB.id, target); categories.set(lineA.id, "battleMarker"); categories.set(lineB.id, "battleMarker"); addDependent(battle.planet, lineA.id); addDependent(battle.planet, lineB.id); } // Bombing visuals are produced by `setPlanetOutlines` in the // renderer (F8-12 / #30); the data still lives on // `report.bombings`, but no primitive is emitted here. return { primitives, lookup, categories, planetDependents }; }