import { itemDefinitionsById, moduleDefinitionsById, recipeDefinitions, } from "../data/catalog"; import { describeFleetOrder } from "../fleet/runtime"; import { getShipCargoAmount } from "../state/inventory"; import type { FleetInstance, ShipInstance, SolarSystemInstance, StationInstance, ViewLevel, } from "../types"; export function getSelectionTitle(selection: ShipInstance[], selectedStation?: StationInstance) { 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 getSelectionDetails( selection: ShipInstance[], selectedStation: StationInstance | undefined, systems: SolarSystemInstance[], viewLevel: ViewLevel, ships: ShipInstance[], fleets: FleetInstance[], ) { if (selectedStation) { return describeStation(selectedStation, ships, fleets); } if (selection.length === 0) { return `Systems online: ${systems.map((system) => system.definition.label).join(", ")}\nFleets active: ${fleets.length}\n\nOrders: Move, Patrol, Escort, Mine\nView: ${viewLevel}`; } 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}\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}\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}\nRole: ${station.definition.category}\nActivity: ${activity}\nLocal Fleets: ${localFleets}\nDocking: ${station.dockedShipIds.size}/${station.definition.dockingCapacity}\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 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; }