import { itemDefinitionsById, moduleDefinitionsById, recipeDefinitions, } from "../data/catalog"; import { describeFleetOrder } from "../fleet/runtime"; import { getShipCargoAmount } from "../state/inventory"; import type { FactionInstance, FleetInstance, PlanetInstance, ShipInstance, SolarSystemInstance, StationInstance, ViewLevel, } from "../types"; export function getSelectionTitle( selection: ShipInstance[], selectedStation?: StationInstance, selectedSystem?: SolarSystemInstance, selectedPlanet?: PlanetInstance, ) { if (selectedPlanet) { return selectedPlanet.definition.label; } if (selectedSystem) { return selectedSystem.definition.label; } if (selectedStation) { return selectedStation.definition.label; } if (selection.length === 0) { return "No Selection"; } if (selection.length === 1) { return selection[0].definition.label; } return `${selection.length} Ships Selected`; } export function getSelectionStripLabels( selection: ShipInstance[], selectedStation?: StationInstance, selectedSystem?: SolarSystemInstance, selectedPlanet?: PlanetInstance, ) { if (selectedPlanet) { return [selectedPlanet.definition.label]; } if (selectedSystem) { return [selectedSystem.definition.label]; } if (selectedStation) { return [selectedStation.definition.label]; } if (selection.length === 0) { return []; } return selection.map((ship) => ship.definition.label); } export function getSelectionCardsMarkup( selection: ShipInstance[], selectedStation: StationInstance | undefined, selectedSystem: SolarSystemInstance | undefined, selectedPlanet: PlanetInstance | undefined, ) { if (selectedPlanet) { return renderCard( selectedPlanet.definition.label, [ selectedPlanet.systemId, `Orbit ${Math.round(selectedPlanet.definition.orbitRadius)}`, `Size ${selectedPlanet.definition.size}`, selectedPlanet.definition.hasRing ? "Ringed" : "No ring", ], ); } if (selectedSystem) { return renderCard( selectedSystem.definition.label, [ selectedSystem.strategicValue, `${selectedSystem.planets.length} planets`, `${selectedSystem.definition.resourceNodes.length} nodes`, `${selectedSystem.controllingFactionId ?? "Contested"} ${Math.round(selectedSystem.controlProgress)}%`, ], ); } if (selectedStation) { return renderCard( selectedStation.definition.label, [ selectedStation.factionId, selectedStation.definition.category, `HP ${Math.round(selectedStation.health)}/${selectedStation.maxHealth}`, `Dock ${selectedStation.dockedShipIds.size}/${selectedStation.definition.dockingCapacity}`, ], ); } if (selection.length === 0) { return `No active selection`; } return selection .map((ship) => renderCard(ship.definition.label, [ ship.factionId, ship.state, ship.order.kind, `HP ${Math.round(ship.health)}/${ship.maxHealth}`, ]), ) .join(""); } export function getSelectionDetails( selection: ShipInstance[], selectedStation: StationInstance | undefined, selectedSystem: SolarSystemInstance | undefined, selectedPlanet: PlanetInstance | undefined, systems: SolarSystemInstance[], viewLevel: ViewLevel, ships: ShipInstance[], fleets: FleetInstance[], factions: FactionInstance[], ) { if (selectedPlanet) { return `${selectedPlanet.definition.label} • ${selectedPlanet.systemId}\nOrbit Radius: ${Math.round(selectedPlanet.definition.orbitRadius)}\nSize: ${selectedPlanet.definition.size}\nOrbit Speed: ${selectedPlanet.definition.orbitSpeed.toFixed(2)}\nTilt: ${selectedPlanet.definition.tilt.toFixed(2)}\nRing: ${selectedPlanet.definition.hasRing ? "Yes" : "No"}`; } if (selectedSystem) { return `${selectedSystem.definition.label}\nType: ${selectedSystem.strategicValue}\nControl: ${selectedSystem.controllingFactionId ?? "Contested"} ${Math.round(selectedSystem.controlProgress)}%\nPlanets: ${selectedSystem.planets.length}\nResource Nodes: ${selectedSystem.definition.resourceNodes.length}\nGravity Well: ${Math.round(selectedSystem.gravityWellRadius)}`; } if (selectedStation) { return describeStation(selectedStation, ships, fleets); } if (selection.length === 0) { const central = systems .filter((system) => system.strategicValue === "central") .map((system) => `${system.definition.label}: ${system.controllingFactionId ?? "contested"} ${Math.round(system.controlProgress)}%`) .join("\n"); const factionLines = factions .filter((faction) => faction.definition.kind === "empire") .map( (faction) => `${faction.definition.label}: systems ${faction.ownedSystemIds.size} • mined ${Math.round(faction.oreMined)} • built ${faction.shipsBuilt} ships • losses ${faction.shipsLost}`, ) .join("\n"); return `Observer Mode\nSystems online: ${systems.length}\nFleets tracked: ${fleets.length}\nView: ${viewLevel}\n\nCentral systems:\n${central}\n\nEmpires:\n${factionLines}`; } return selection .map( (ship) => { const dockedAt = ship.dockedCarrierId ?? ship.dockedStationId; const hangarStatus = ship.definition.dockingCapacity && ship.definition.dockingCapacity > 0 ? `\nHangar: ${ship.dockedShipIds.size}/${ship.definition.dockingCapacity} for ${(ship.definition.dockingClasses ?? []).join(", ")}` : ""; return `${ship.definition.label} • ${ship.systemId}\nFaction: ${ship.factionId}\nClass: ${ship.definition.shipClass}\nState: ${ship.state}${dockedAt ? ` @ ${dockedAt}` : ""}\nOrder: ${ship.order.kind}\nFleet: ${ship.fleetId ?? "Independent"}${ship.isFleetCommander ? " • Commander" : ship.isWingLeader ? " • Wing Leader" : ""}\nBehavior: ${ship.behavior}\nHealth: ${Math.round(ship.health)}/${ship.maxHealth}\nCargo: ${Math.round(getShipCargoAmount(ship))}/${ship.definition.cargoCapacity || 0} ${getItemLabel(ship.cargoItemId)}\nFuel: ${ship.fuel.toFixed(0)}/${ship.maxFuel}\nEnergy: ${ship.energy.toFixed(0)}/${ship.maxEnergy}\nHold Type: ${ship.definition.cargoKind ?? "none"}${hangarStatus}\nModules: ${ship.definition.modules.map(getModuleLabel).join(", ")}`; }, ) .join("\n\n"); } export function describeStation(station: StationInstance, ships: ShipInstance[], fleets: FleetInstance[]) { const miners = ships.filter((ship) => ship.systemId === station.systemId && ship.order.kind === "mine").length; const escorts = ships.filter((ship) => ship.systemId === station.systemId && ship.order.kind === "escort").length; const patrols = ships.filter((ship) => ship.systemId === station.systemId && ship.order.kind === "patrol").length; const localFleets = fleets.filter((fleet) => fleet.systemId === station.systemId).length; const activeRecipe = station.activeRecipeId ? recipeDefinitions.find((recipe) => recipe.id === station.activeRecipeId) : undefined; const stockSummary = Object.entries(station.itemStocks) .filter(([, amount]) => amount > 0) .sort((left, right) => right[1] - left[1]) .slice(0, 5) .map(([itemId, amount]) => `${getItemLabel(itemId)} ${Math.round(amount)}`) .join(", "); const productionStatus = station.modules.includes("fabricator-array") || station.definition.category === "refining" ? `Ore: ${Math.round(station.oreStored)}\nRefined Metals: ${Math.round(station.refinedStock)}\nBatch: ${Math.round(station.activeBatch)}\nRecipe: ${activeRecipe?.label ?? "Idle"}\nTime Remaining: ${station.activeBatch > 0 ? `${station.processTimer.toFixed(1)}s` : "Idle"}\nStocks: ${stockSummary || "None"}\n` : ""; const activity = station.definition.category === "refining" ? `Refining and fabricating for ${miners} mining ships` : station.definition.category === "shipyard" ? `Building ship parts for ${patrols} patrol craft` : station.definition.category === "farm" ? "Supplying agricultural goods and industrial consumables" : station.definition.category === "defense" ? `Coordinating ${escorts} escort wings` : station.definition.category === "gate" ? "Assembling transit infrastructure and gate components" : station.modules.includes("fabricator-array") ? "Fabricating industrial parts and equipment" : "Managing local trade traffic"; return `${station.definition.label} • ${station.systemId}\nFaction: ${station.factionId}\nRole: ${station.definition.category}\nActivity: ${activity}\nLocal Fleets: ${localFleets}\nDocking: ${station.dockedShipIds.size}/${station.definition.dockingCapacity}\nHealth: ${Math.round(station.health)}/${station.maxHealth}\nFuel: ${station.fuel.toFixed(0)}/${station.maxFuel}\nEnergy: ${station.energy.toFixed(0)}/${station.maxEnergy}\nBulk Solid: ${Math.round(station.inventory["bulk-solid"])}\nContainer: ${Math.round(station.inventory.container)}\nManufactured: ${Math.round(station.inventory.manufactured)}\nModules: ${station.modules.map(getModuleLabel).join(", ")}\n${productionStatus}Radius: ${station.definition.radius}`; } export function getFleetWindowMarkup( fleets: FleetInstance[], shipsById: Map, activeFleetId: string | undefined, selection: ShipInstance[], ) { if (fleets.length === 0) { return `
No fleets initialized.
`; } const selectedShipIds = new Set(selection.map((ship) => ship.id)); return fleets .map((fleet) => { const commander = shipsById.get(fleet.commanderShipId); const rootWings = fleet.wings.filter((wing) => !wing.parentWingId); const tree = rootWings.map((wing) => renderWingNode(fleet, wing.id, shipsById, selectedShipIds)).join(""); const fleetSelected = fleet.shipIds.length > 0 && fleet.shipIds.every((shipId) => selectedShipIds.has(shipId)); return `
${commander?.definition.label ?? fleet.commanderShipId} Commander • ${fleet.shipIds.length} ships • ${fleet.systemId} Fleet Order: ${describeFleetOrder(fleet)} Stance: ${fleet.stance}
${tree}
`; }) .join(""); } function renderWingNode( fleet: FleetInstance, wingId: string, shipsById: Map, selectedShipIds: Set, ): string { const wing = fleet.wings.find((candidate) => candidate.id === wingId); if (!wing) { return ""; } const leader = shipsById.get(wing.leaderShipId); const childWings = fleet.wings.filter((candidate) => candidate.parentWingId === wing.id); const wingTreeShipIds = collectWingShipIds(fleet, wing.id); const wingSelected = wingTreeShipIds.length > 0 && wingTreeShipIds.every((shipId) => selectedShipIds.has(shipId)); const nonLeaderShips = wing.shipIds .filter((shipId) => shipId !== wing.leaderShipId) .map((shipId) => shipsById.get(shipId)) .filter((ship): ship is ShipInstance => Boolean(ship)); return `
${wing.label} ${wing.behavior} • ${wing.shipIds.length} ships Wing Lead: ${leader ? describeShipNode(leader) : wing.leaderShipId}
${childWings.map((childWing) => renderWingNode(fleet, childWing.id, shipsById, selectedShipIds)).join("")} ${nonLeaderShips .map( (ship) => `
${ship.definition.label} ${describeShipNode(ship)}
`, ) .join("")}
`; } function describeShipNode(ship: ShipInstance): string { return `${ship.definition.shipClass} • ${ship.state} • ${ship.order.kind} • ${ship.behavior}`; } function renderCard(title: string, lines: string[]) { return `
${title} ${lines.map((line) => `${line}`).join("")}
`; } function collectWingShipIds(fleet: FleetInstance, rootWingId: string): string[] { const wingIds = new Set([rootWingId]); let changed = true; while (changed) { changed = false; fleet.wings.forEach((wing) => { if (wing.parentWingId && wingIds.has(wing.parentWingId) && !wingIds.has(wing.id)) { wingIds.add(wing.id); changed = true; } }); } return fleet.wings.filter((wing) => wingIds.has(wing.id)).flatMap((wing) => wing.shipIds); } export function getItemLabel(itemId?: string) { return itemId ? itemDefinitionsById.get(itemId)?.label ?? itemId : "None"; } export function getModuleLabel(moduleId: string) { return moduleDefinitionsById.get(moduleId)?.label ?? moduleId; }