// Phase 27 battle and bombing markers on the map. // // Two visual markers per planet: // // * 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. // * Bombing marker — a thin stroke-only circle slightly larger // than the planet circle. Yellow on damaged planets, red on // wiped planets. Clicking it deep-links to the bombings row in // the Reports view for the planet number. // // Both markers are wired into `state-binding.ts` so they live in the // same `world` / `hitLookup` plumbing as planets and ship groups. import type { GameReport, ReportPlanet } from "../api/game-state"; import type { CirclePrim, LinePrim, Primitive, PrimitiveID, Style, } from "./world"; export const BATTLE_MARKER_COLOR = 0xffd400; export const BOMBING_MARKER_COLOR_DAMAGED = 0xffd400; export const BOMBING_MARKER_COLOR_WIPED = 0xff3030; /** Battle and bombing 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; export const BOMBING_MARKER_ID_PREFIX = 0xc0000000; const PLANET_RADIUS_WORLD = 6; const BOMBING_RING_RADIUS = PLANET_RADIUS_WORLD + 3; 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 BOMBING_MARKER_PRIORITY = 10; const BATTLE_LINE_INDEX_A = 0; const BATTLE_LINE_INDEX_B = 1; export interface BattleMarkerTarget { kind: "battle"; battleId: string; planet: number; } export interface BombingMarkerTarget { kind: "bombing"; planet: number; } export type MarkerTarget = BattleMarkerTarget | BombingMarkerTarget; export interface BuildMarkersResult { primitives: Primitive[]; lookup: 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 and bombing marker * 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. */ export function buildBattleAndBombingMarkers( report: GameReport, ): BuildMarkersResult { const planetByNumber = new Map(); for (const planet of report.planets) { planetByNumber.set(planet.number, planet); } const primitives: Primitive[] = []; const lookup = new Map(); 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: BATTLE_MARKER_COLOR, 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); } for (let i = 0; i < report.bombings.length; i++) { const bombing = report.bombings[i]; const planet = planetByNumber.get(bombing.planetNumber); if (planet === undefined) continue; const color = bombing.wiped ? BOMBING_MARKER_COLOR_WIPED : BOMBING_MARKER_COLOR_DAMAGED; const style: Style = { strokeColor: color, strokeAlpha: 0.9, strokeWidthPx: 1.5, }; const id = BOMBING_MARKER_ID_PREFIX | i; const ring: CirclePrim = { kind: "circle", id, priority: BOMBING_MARKER_PRIORITY, style, hitSlopPx: 0, x: planet.x, y: planet.y, radius: BOMBING_RING_RADIUS, }; primitives.push(ring); lookup.set(id, { kind: "bombing", planet: bombing.planetNumber }); } return { primitives, lookup }; }