import type { ShipSnapshot, SpatialNodeSnapshot, SystemSnapshot } from "./contracts"; import type { CameraMode, OrbitalAnchor, Selectable, SelectionGroup, WorldState, } from "./viewerTypes"; import { formatGalaxyDistance } from "./viewerMath"; export function describeSelectable(world: WorldState | undefined, item: Selectable): string { if (!world) { return item.kind; } if (item.kind === "ship") { return world.ships.get(item.id)?.label ?? item.id; } if (item.kind === "station") { return world.stations.get(item.id)?.label ?? item.id; } if (item.kind === "node") { return item.id; } if (item.kind === "spatial-node") { return `${world.spatialNodes.get(item.id)?.kind ?? "node"} ${item.id}`; } if (item.kind === "bubble") { return `bubble ${item.id}`; } 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}`; } 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") { return world.ships.get(item.id)?.label ?? item.id; } if (item.kind === "station") { return world.stations.get(item.id)?.label ?? item.id; } if (item.kind === "system") { return world.systems.get(item.id)?.label ?? item.id; } 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 === "node") { const node = world.nodes.get(item.id); if (!node) { return item.id; } const anchorPath = node.anchorNodeId ? describeSpatialNodePathWithinSystem(world, node.systemId, node.anchorNodeId) : undefined; return anchorPath ? `${anchorPath} / ${node.itemId}` : `${node.systemId} / ${node.itemId}`; } if (item.kind === "spatial-node") { const node = world.spatialNodes.get(item.id); if (!node) { return item.id; } if (node.kind === "star") { const system = world.systems.get(node.systemId); return system ? `${system.label} star` : `${node.systemId} star`; } return describeSpatialNodePathWithinSystem(world, node.systemId, node.id) ?? `${node.systemId} / ${node.kind}`; } if (item.kind === "bubble") { const bubble = world.localBubbles.get(item.id); const anchorPath = bubble?.nodeId ? describeSpatialNodePathWithinSystem(world, bubble.systemId, bubble.nodeId) : undefined; return anchorPath ? `${anchorPath} bubble` : `Bubble ${item.id}`; } if (item.kind === "claim") { const claim = world.claims.get(item.id); const anchorPath = claim?.nodeId ? describeSpatialNodePathWithinSystem(world, claim.systemId, claim.nodeId) : undefined; return anchorPath ? `${anchorPath} claim` : `Claim ${item.id}`; } if (item.kind === "construction-site") { const site = world.constructionSites.get(item.id); const anchorPath = site?.nodeId ? describeSpatialNodePathWithinSystem(world, site.systemId, site.nodeId) : 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 === "spatial-node" || item.kind === "bubble" || 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 === "spatial-node") { return world.spatialNodes.get(selection.id)?.systemId; } if (selection.kind === "bubble") { return world.localBubbles.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; } return selection.id; } export function resolveFocusedBubbleId(world: WorldState | undefined, selectedItems: Selectable[]): string | undefined { if (!world || selectedItems.length !== 1) { return undefined; } const selected = selectedItems[0]; if (selected.kind === "bubble") { return selected.id; } if (selected.kind === "ship") { return world.ships.get(selected.id)?.bubbleId ?? world.ships.get(selected.id)?.spatialState.currentBubbleId ?? undefined; } if (selected.kind === "station") { return world.stations.get(selected.id)?.bubbleId ?? undefined; } if (selected.kind === "spatial-node") { return world.spatialNodes.get(selected.id)?.bubbleId ?? undefined; } if (selected.kind === "claim") { return world.claims.get(selected.id)?.bubbleId ?? undefined; } if (selected.kind === "construction-site") { return world.constructionSites.get(selected.id)?.bubbleId ?? undefined; } 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 spatialNodeCount = 0; let bubbleCount = 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 node of world.spatialNodes.values()) { if (node.systemId === system.id) { spatialNodeCount += 1; } } for (const bubble of world.localBubbles.values()) { if (bubble.systemId === system.id) { bubbleCount += 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.moonCount; } const followText = activeContext && cameraMode === "follow" && cameraTargetShipId ? `

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

` : ""; return `

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

${system.starKind} · ${system.starCount} star${system.starCount > 1 ? "s" : ""}

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

Spatial nodes ${spatialNodeCount}
Resource nodes ${nodeCount}
Bubbles ${bubbleCount}

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; if (baseState === "capacitor-starved") { return `${baseState} while ${describeControllerTask(ship.controllerTaskKind)}`; } if (!world || (baseState !== "ftl" && baseState !== "spooling-ftl" && baseState !== "warping" && baseState !== "spooling-warp")) { return baseState; } const destinationNodeId = ship.spatialState.destinationNodeId ?? ship.spatialState.transit?.destinationNodeId; if (!destinationNodeId) { return baseState; } const destinationNode = world.spatialNodes.get(destinationNodeId); if (!destinationNode) { return `${baseState} -> ${destinationNodeId}`; } if (baseState === "warping" || baseState === "spooling-warp") { const destinationPath = describeSpatialNodePathWithinSystem(world, destinationNode.systemId, destinationNodeId); return `${baseState} -> ${destinationPath ?? destinationNodeId}`; } const destinationSystem = world.systems.get(destinationNode.systemId); return `${baseState} -> ${destinationSystem?.label ?? destinationNode.systemId}`; } function describeControllerTask(taskKind: string): string { switch (taskKind) { case "travel": return "travel"; case "extract": return "mining"; case "dock": return "docking"; case "unload": return "transfer"; case "refuel": return "refuel"; case "deliver-construction": return "material delivery"; case "build-construction-site": return "site construction"; case "construct-module": return "module construction"; case "undock": return "undocking"; case "load-workers": return "worker loading"; case "unload-workers": return "worker unloading"; default: return taskKind; } } export function describeShipCurrentAction(ship: ShipSnapshot): { label: string; progress: number } | undefined { if (!ship.currentAction) { return undefined; } return { label: ship.currentAction.label, progress: Math.max(0, Math.min(ship.currentAction.progress, 1)), }; } 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.anchorNodeId ? describeSpatialNodePathWithinSystem(world, station.systemId, station.anchorNodeId) : undefined; return { system: systemLabel, local: anchorPath ? `${anchorPath}/${station.label}` : station.label, }; } } const currentNodeId = ship.spatialState.currentNodeId ?? ship.nodeId; if (currentNodeId) { const nodePath = describeSpatialNodePathWithinSystem(world, systemId, currentNodeId); if (nodePath) { return { system: systemLabel, local: nodePath }; } } const currentBubbleId = ship.spatialState.currentBubbleId ?? ship.bubbleId; if (currentBubbleId) { const bubble = world.localBubbles.get(currentBubbleId); if (bubble?.nodeId) { const nodePath = describeSpatialNodePathWithinSystem(world, systemId, bubble.nodeId); if (nodePath) { return { system: systemLabel, local: nodePath }; } } } return { system: systemLabel }; } export function describeActiveSpace( world: WorldState | undefined, zoomLevel: "local" | "system" | "universe", activeSystemId: string | undefined, selectedItems: Selectable[], ): string { if (!world || zoomLevel === "universe") { return "deep-space"; } const activeSystem = activeSystemId ? world.systems.get(activeSystemId) : undefined; if (!activeSystem) { return "deep-space"; } if (zoomLevel !== "local") { return activeSystem.label; } const bubbleId = resolveFocusedBubbleId(world, selectedItems); if (bubbleId) { const bubble = world.localBubbles.get(bubbleId); const localPath = bubble?.nodeId ? describeSpatialNodePathWithinSystem(world, activeSystem.id, bubble.nodeId) : undefined; 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 describeSpatialNodePathWithinSystem(world: WorldState, systemId: string, nodeId: string): string | undefined { const node = world.spatialNodes.get(nodeId); const system = world.systems.get(systemId); if (!node || !system) { return undefined; } if (node.parentNodeId) { const parentPath = describeSpatialNodePathWithinSystem(world, systemId, node.parentNodeId); const segment = describeSpatialNodeSegment(world, system, node); return parentPath ? `${parentPath}/${segment}` : segment; } if (node.kind === "star") { return undefined; } return describeSpatialNodeSegment(world, system, node); } function describeSpatialNodeSegment(world: WorldState, system: SystemSnapshot, node: SpatialNodeSnapshot): string { const moonMatch = node.id.match(/-planet-(\d+)-moon-(\d+)$/); if (moonMatch) { const moonIndex = Number.parseInt(moonMatch[2], 10); return `Moon ${moonIndex}`; } const lagrangeMatch = node.id.match(/-planet-\d+-(l[1-5])$/); if (lagrangeMatch) { return lagrangeMatch[1].toUpperCase(); } const planetMatch = node.id.match(/-planet-(\d+)$/); if (planetMatch) { const planetIndex = Number.parseInt(planetMatch[1], 10) - 1; return system.planets[planetIndex]?.label ?? `Planet ${planetMatch[1]}`; } if (node.kind === "station" && node.occupyingStructureId) { return world.stations.get(node.occupyingStructureId)?.label ?? node.occupyingStructureId; } if (node.kind === "resource-site") { return node.orbitReferenceId ?? "Resource Site"; } return node.orbitReferenceId ?? node.kind; }