import type { CelestialSnapshot, ShipSnapshot, SystemSnapshot } from "./contracts"; import type { CameraMode, OrbitalAnchor, Selectable, SelectionGroup, WorldState, } from "./viewerTypes"; import { formatGalaxyDistance } from "./viewerMath"; import { getShipBehaviorLabel, getShipOrderLabel } from "./shipAutomationPresentation"; export function describeSelectable(world: WorldState | undefined, item: Selectable): string { if (!world) { return item.kind; } if (item.kind === "ship") { return world.ships.get(item.id)?.name ?? item.id; } if (item.kind === "station") { return world.stations.get(item.id)?.label ?? item.id; } if (item.kind === "node") { const node = world.nodes.get(item.id); return node ? `${node.itemId} source` : item.id; } if (item.kind === "celestial") { const celestial = world.celestials.get(item.id); if (!celestial) { return item.id; } return describeCelestialPathWithinSystem(world, celestial.systemId, celestial.id) ?? `${world.systems.get(celestial.systemId)?.label ?? celestial.systemId} / ${celestial.kind}`; } if (item.kind === "claim") { return `claim ${item.id}`; } if (item.kind === "construction-site") { return `construction ${item.id}`; } if (item.kind === "planet") { return world.systems.get(item.systemId)?.planets[item.planetIndex]?.label ?? `${item.systemId}:${item.planetIndex}`; } if (item.kind === "moon") { const planet = world.systems.get(item.systemId)?.planets[item.planetIndex]; return planet?.moons[item.moonIndex]?.label ?? `moon ${item.moonIndex + 1}`; } return world.systems.get(item.id)?.label ?? item.id; } export function describeHoverLabel(world: WorldState | undefined, item: Selectable): string | undefined { if (!world) { return undefined; } if (item.kind === "ship") { const ship = world.ships.get(item.id); if (!ship) { return item.id; } const lines = [ ship.name, `Behavior ${describeShipBehavior(ship)}`, `State ${describeShipState(world, ship)}`, `Order ${describeShipOrder(ship)}`, ]; return lines.join("\n"); } if (item.kind === "station") { return world.stations.get(item.id)?.label ?? item.id; } if (item.kind === "system") { const system = world.systems.get(item.id); if (!system) { return item.id; } const starLabel = system.stars.length > 1 ? `${system.stars.length}× ${system.stars[0]?.kind}` : (system.stars[0]?.kind ?? "unknown"); const planetCount = system.planets.length; const shipCount = [...world.ships.values()].filter((s) => s.systemId === item.id).length; const stationCount = [...world.stations.values()].filter((s) => s.systemId === item.id).length; const lines = [ system.label, `${starLabel} · ${planetCount} planet${planetCount !== 1 ? "s" : ""}`, ]; const parts: string[] = []; if (shipCount > 0) { parts.push(`${shipCount} ship${shipCount !== 1 ? "s" : ""}`); } if (stationCount > 0) { parts.push(`${stationCount} station${stationCount !== 1 ? "s" : ""}`); } if (parts.length > 0) { lines.push(parts.join(" · ")); } return lines.join("\n"); } if (item.kind === "planet") { const system = world.systems.get(item.systemId); const planet = system?.planets[item.planetIndex]; return planet ? `${system?.label ?? item.systemId} / ${planet.label}` : `${item.systemId} / planet ${item.planetIndex + 1}`; } if (item.kind === "moon") { const system = world.systems.get(item.systemId); const planet = system?.planets[item.planetIndex]; const moon = planet?.moons[item.moonIndex]; if (moon) { return `${system?.label ?? item.systemId} / ${planet?.label ?? `planet ${item.planetIndex + 1}`} / ${moon.label}`; } return `${item.systemId} / planet ${item.planetIndex + 1} / moon ${item.moonIndex + 1}`; } if (item.kind === "node") { const node = world.nodes.get(item.id); if (!node) { return item.id; } const anchorPath = describeAnchorPathWithinSystem(world, node.systemId, node.anchorId); return anchorPath ? `${anchorPath} / ${node.itemId}` : `${node.systemId} / ${node.itemId}`; } if (item.kind === "celestial") { const celestial = world.celestials.get(item.id); if (!celestial) { return item.id; } if (celestial.kind === "star") { const system = world.systems.get(celestial.systemId); return system ? `${system.label} star` : `${celestial.systemId} star`; } return describeCelestialPathWithinSystem(world, celestial.systemId, celestial.id) ?? `${celestial.systemId} / ${celestial.kind}`; } if (item.kind === "claim") { const claim = world.claims.get(item.id); const anchorPath = claim ? describeAnchorPathWithinSystem(world, claim.systemId, claim.anchorId) : undefined; return anchorPath ? `${anchorPath} claim` : `Claim ${item.id}`; } if (item.kind === "construction-site") { const site = world.constructionSites.get(item.id); const anchorPath = site ? describeAnchorPathWithinSystem(world, site.systemId, site.anchorId) : undefined; const siteLabel = site ? (site.blueprintId ?? site.targetDefinitionId) : item.id; return anchorPath ? `${anchorPath} / ${siteLabel}` : `Construction ${siteLabel}`; } return describeSelectable(world, item); } export function getSelectionGroup(item: Selectable): SelectionGroup { if (item.kind === "ship") { return "ships"; } if ( item.kind === "station" || item.kind === "node" || item.kind === "claim" || item.kind === "construction-site" ) { return "structures"; } return "celestials"; } export function resolveSelectableSystemId(world: WorldState | undefined, selection: Selectable): string | undefined { if (!world) { return undefined; } if (selection.kind === "ship") { return world.ships.get(selection.id)?.systemId; } if (selection.kind === "station") { return world.stations.get(selection.id)?.systemId; } if (selection.kind === "node") { return world.nodes.get(selection.id)?.systemId; } if (selection.kind === "celestial") { return world.celestials.get(selection.id)?.systemId; } if (selection.kind === "claim") { return world.claims.get(selection.id)?.systemId; } if (selection.kind === "construction-site") { return world.constructionSites.get(selection.id)?.systemId; } if (selection.kind === "planet") { return selection.systemId; } if (selection.kind === "moon") { return selection.systemId; } return selection.id; } export function resolveFocusedCelestialId(world: WorldState | undefined, selectedItems: Selectable[]): string | undefined { if (!world || selectedItems.length !== 1) { return undefined; } const selected = selectedItems[0]; if (selected.kind === "celestial") { return selected.id; } if (selected.kind === "ship") { const ship = world.ships.get(selected.id); return ship?.spatialState.currentAnchorId && world.celestials.has(ship.spatialState.currentAnchorId) ? ship.spatialState.currentAnchorId : (ship?.anchorId && world.celestials.has(ship.anchorId) ? ship.anchorId : undefined); } if (selected.kind === "station") { const station = world.stations.get(selected.id); return station?.anchorId && world.celestials.has(station.anchorId) ? station.anchorId : undefined; } if (selected.kind === "claim") { const claim = world.claims.get(selected.id); return claim && world.celestials.has(claim.anchorId) ? claim.anchorId : undefined; } if (selected.kind === "construction-site") { const site = world.constructionSites.get(selected.id); return site && world.celestials.has(site.anchorId) ? site.anchorId : undefined; } return undefined; } export function resolveFocusedAnchorId(world: WorldState | undefined, selectedItems: Selectable[]): string | undefined { if (!world || selectedItems.length !== 1) { return undefined; } const selected = selectedItems[0]; if (selected.kind === "node") { return world.nodes.get(selected.id)?.anchorId; } if (selected.kind === "ship") { const ship = world.ships.get(selected.id); return ship?.spatialState.currentAnchorId ?? ship?.anchorId ?? resolveFocusedCelestialId(world, selectedItems); } if (selected.kind === "station") { const station = world.stations.get(selected.id); return station?.anchorId ?? resolveFocusedCelestialId(world, selectedItems); } if (selected.kind === "claim") { const claim = world.claims.get(selected.id); return claim?.anchorId ?? resolveFocusedCelestialId(world, selectedItems); } if (selected.kind === "construction-site") { const site = world.constructionSites.get(selected.id); return site?.anchorId ?? resolveFocusedCelestialId(world, selectedItems); } if (selected.kind === "celestial") { if (world.anchors.has(selected.id)) { return selected.id; } const orbitBackedAnchor = [...world.anchors.values()].find((anchor) => anchor.orbitReferenceId === selected.id); return orbitBackedAnchor?.id; } if (selected.kind === "planet") { return `node-${selected.systemId}-planet-${selected.planetIndex + 1}`; } if (selected.kind === "moon") { return `node-${selected.systemId}-planet-${selected.planetIndex + 1}-moon-${selected.moonIndex + 1}`; } return undefined; } export function describeOrbitalParent(world: WorldState | undefined, systemId?: string, anchor?: OrbitalAnchor): string { if (!world || !systemId) { return "unknown"; } const system = world.systems.get(systemId); if (!system) { return systemId; } if (!anchor || anchor.kind === "star") { return `${system.label} star`; } const planet = system.planets[anchor.planetIndex]; if (!planet) { return `${system.label} star`; } if (anchor.kind === "planet") { return planet.label; } return `${planet.label} moon ${anchor.moonIndex + 1}`; } export function renderSystemDetails( world: WorldState | undefined, system: SystemSnapshot, activeContext: boolean, cameraMode: CameraMode, cameraTargetShipId?: string, ): string { if (!world) { return ""; } let shipCount = 0; let stationCount = 0; let nodeCount = 0; let celestialCount = 0; let claimCount = 0; let constructionCount = 0; let moonCount = 0; for (const ship of world.ships.values()) { if (ship.systemId === system.id) { shipCount += 1; } } for (const station of world.stations.values()) { if (station.systemId === system.id) { stationCount += 1; } } for (const node of world.nodes.values()) { if (node.systemId === system.id) { nodeCount += 1; } } for (const celestial of world.celestials.values()) { if (celestial.systemId === system.id) { celestialCount += 1; } } for (const claim of world.claims.values()) { if (claim.systemId === system.id) { claimCount += 1; } } for (const site of world.constructionSites.values()) { if (site.systemId === system.id) { constructionCount += 1; } } for (const planet of system.planets) { moonCount += planet.moons.length; } const followText = activeContext && cameraMode === "follow" && cameraTargetShipId ? `

Camera locked to ${world.ships.get(cameraTargetShipId)?.name ?? cameraTargetShipId}

` : ""; return `

${system.id}${activeContext ? " · active system" : ""}

${system.stars[0]?.kind ?? "unknown"} · ${system.stars.length} star${system.stars.length > 1 ? "s" : ""}

Planets ${system.planets.length}
Moons ${moonCount}
Ships ${shipCount}
Stations ${stationCount}

Celestials ${celestialCount}
Resource nodes ${nodeCount}

Claims ${claimCount}
Construction sites ${constructionCount}

Height ${formatGalaxyDistance(system.galaxyPosition.y)}

${system.planets.slice(0, 8).map((planet) => `${planet.label} (${planet.planetType})`).join("
")}

${followText} `; } export function describeShipState(world: WorldState | undefined, ship: ShipSnapshot): string { const baseState = ship.state; const currentSubTask = ship.activeSubTasks[0]; if (baseState === "capacitor-starved") { return currentSubTask ? `${baseState} while ${titleTask(currentSubTask.kind)}` : baseState; } if (!world || (baseState !== "ftl" && baseState !== "spooling-ftl" && baseState !== "warping" && baseState !== "spooling-warp")) { return baseState; } const destinationAnchorId = ship.spatialState.destinationAnchorId ?? ship.spatialState.transit?.destinationAnchorId; if (!destinationAnchorId) { return baseState; } const destinationAnchor = destinationAnchorId ? world.anchors.get(destinationAnchorId) : undefined; if (baseState === "warping" || baseState === "spooling-warp") { const destinationSystemId = destinationAnchor?.systemId ?? ship.spatialState.currentSystemId ?? ship.systemId; const destinationPath = describeAnchorPathWithinSystem( world, destinationSystemId, destinationAnchorId, ); return `${baseState} -> ${destinationPath ?? destinationAnchorId}`; } const destinationSystemId = destinationAnchor?.systemId ?? ship.spatialState.currentSystemId ?? ship.systemId; const destinationSystem = world.systems.get(destinationSystemId); if (!destinationSystem) { return `${baseState} -> ${destinationAnchorId}`; } return `${baseState} -> ${destinationSystem.label}`; } export function describeShipObjective(objective: string): string { return objective.replace(/[-_]+/g, " "); } export function describeShipBehavior(ship: ShipSnapshot): string { const parts = [getShipBehaviorLabel(ship.defaultBehavior.kind)]; if (ship.assignment?.kind) { parts.push(ship.assignment.kind); } return parts.join(" · "); } export function describeShipOrder(ship: ShipSnapshot): string { const activeOrder = ship.orderQueue.find((order) => order.status === "queued" || order.status === "active"); if (activeOrder) { return activeOrder.label ?? getShipOrderLabel(activeOrder.kind); } if (ship.assignment?.kind) { return describeShipObjective(ship.assignment.kind); } return getShipBehaviorLabel(ship.defaultBehavior.kind); } export function describeShipCurrentAction(ship: ShipSnapshot): { label: string; progress: number } | undefined { const subTask = ship.activeSubTasks[0]; if (!subTask) { return undefined; } return { label: subTask.summary || titleTask(subTask.kind), progress: Math.max(0, Math.min(subTask.progress, 1)), }; } function titleTask(value: string): string { return value.replace(/[-_]+/g, " "); } export function describeShipLocation(world: WorldState | undefined, ship: ShipSnapshot): { system: string; local?: string } { const systemId = ship.spatialState.currentSystemId || ship.systemId; const system = world?.systems.get(systemId); const systemLabel = system?.label ?? systemId; if (!world || !system) { return { system: systemLabel }; } if (ship.dockedStationId) { const station = world.stations.get(ship.dockedStationId); if (station) { const anchorPath = station.anchorId ? describeAnchorPathWithinSystem(world, station.systemId, station.anchorId) : undefined; return { system: systemLabel, local: anchorPath ? `${anchorPath}/${station.label}` : station.label, }; } } const currentAnchorId = ship.spatialState.currentAnchorId ?? ship.anchorId; if (currentAnchorId) { const anchorPath = describeAnchorPathWithinSystem(world, systemId, currentAnchorId); if (anchorPath) { return { system: systemLabel, local: anchorPath }; } } return { system: systemLabel }; } export function describeActiveSpace( world: WorldState | undefined, povLevel: "local" | "system" | "galaxy", activeSystemId: string | undefined, selectedItems: Selectable[], ): string { if (!world || povLevel === "galaxy") { return "deep-space"; } const activeSystem = activeSystemId ? world.systems.get(activeSystemId) : undefined; if (!activeSystem) { return "deep-space"; } if (povLevel !== "local") { return activeSystem.label; } const anchorId = resolveFocusedAnchorId(world, selectedItems); if (anchorId) { const localPath = describeAnchorPathWithinSystem(world, activeSystem.id, anchorId); return localPath ? `${activeSystem.label} / ${localPath}` : activeSystem.label; } const selected = selectedItems.length === 1 ? selectedItems[0] : undefined; if (selected?.kind === "planet" && selected.systemId === activeSystem.id) { const planet = activeSystem.planets[selected.planetIndex]; return planet ? `${activeSystem.label} / ${planet.label}` : activeSystem.label; } return activeSystem.label; } export function describeCelestialPathWithinSystem(world: WorldState, systemId: string, celestialId: string): string | undefined { const celestial = world.celestials.get(celestialId); const system = world.systems.get(systemId); if (!celestial || !system) { return undefined; } const anchorId = resolveAnchorIdForCelestial(world, celestialId); if (anchorId) { return describeAnchorPathWithinSystem(world, systemId, anchorId); } if (celestial.kind === "star") { return undefined; } return describeCelestialSegment(system, celestial); } export function describeAnchorPathWithinSystem(world: WorldState, systemId: string, anchorId: string, celestialId?: string | null): string | undefined { const anchor = world.anchors.get(anchorId); if (anchor?.parentAnchorId) { const parentPath = describeAnchorPathWithinSystem(world, systemId, anchor.parentAnchorId); const segment = describeAnchorSegment(anchor); return parentPath ? `${parentPath}/${segment}` : segment; } if (celestialId) { return describeCelestialPathWithinSystem(world, systemId, celestialId); } if (!anchor) { return undefined; } return describeAnchorSegment(anchor); } function describeAnchorSegment(anchor: { kind: string; id: string; orbitReferenceId?: string | null }): string { if (anchor.orbitReferenceId) { return describeAnchorOrbitReference(anchor.orbitReferenceId); } if (anchor.kind === "resource-node") { return anchor.id; } return anchor.kind.replace(/-/g, " "); } function resolveAnchorIdForCelestial(world: WorldState, celestialId: string): string | undefined { return world.anchors.has(celestialId) ? celestialId : undefined; } function describeAnchorOrbitReference(referenceId: string): string { const lagrangeMatch = referenceId.match(/(l[1-5])$/i); if (lagrangeMatch) { return lagrangeMatch[1].toUpperCase(); } const moonMatch = referenceId.match(/moon-(\d+)$/i); if (moonMatch) { return `Moon ${moonMatch[1]}`; } const planetMatch = referenceId.match(/planet-(\d+)$/i); if (planetMatch) { return `Planet ${planetMatch[1]}`; } return referenceId; } function describeCelestialSegment(system: SystemSnapshot, celestial: CelestialSnapshot): string { const moonMatch = celestial.id.match(/-planet-(\d+)-moon-(\d+)$/); if (moonMatch) { const moonIndex = Number.parseInt(moonMatch[2], 10); return `Moon ${moonIndex}`; } const lagrangeMatch = celestial.id.match(/-planet-\d+-(l[1-5])$/); if (lagrangeMatch) { return lagrangeMatch[1].toUpperCase(); } const planetMatch = celestial.id.match(/-planet-(\d+)$/); if (planetMatch) { const planetIndex = Number.parseInt(planetMatch[1], 10) - 1; return system.planets[planetIndex]?.label ?? `Planet ${planetMatch[1]}`; } return celestial.orbitReferenceId ?? celestial.kind; }