205 lines
9.5 KiB
TypeScript
205 lines
9.5 KiB
TypeScript
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<string, ShipInstance>,
|
|
activeFleetId: string | undefined,
|
|
selection: ShipInstance[],
|
|
) {
|
|
if (fleets.length === 0) {
|
|
return `<div class="fleet-card"><span class="fleet-card-line">No fleets initialized.</span></div>`;
|
|
}
|
|
|
|
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 `
|
|
<article class="fleet-card" data-active="${fleet.id === activeFleetId}">
|
|
<button type="button" class="fleet-select" data-select-kind="fleet" data-select-id="${fleet.id}">${fleet.label}</button>
|
|
<div class="fleet-tree">
|
|
<div class="fleet-tree-root" data-select-kind="fleet" data-select-id="${fleet.id}" data-selected="${fleetSelected}">
|
|
<span class="fleet-card-title">${commander?.definition.label ?? fleet.commanderShipId}</span>
|
|
<span class="fleet-card-line">Commander • ${fleet.shipIds.length} ships • ${fleet.systemId}</span>
|
|
<span class="fleet-card-line">Fleet Order: ${describeFleetOrder(fleet)}</span>
|
|
<span class="fleet-card-line">Stance: ${fleet.stance}</span>
|
|
</div>
|
|
<div class="fleet-tree-children">${tree}</div>
|
|
</div>
|
|
</article>
|
|
`;
|
|
})
|
|
.join("");
|
|
}
|
|
|
|
function renderWingNode(
|
|
fleet: FleetInstance,
|
|
wingId: string,
|
|
shipsById: Map<string, ShipInstance>,
|
|
selectedShipIds: Set<string>,
|
|
): 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 `
|
|
<div class="fleet-tree-node wing-node">
|
|
<div class="fleet-node-card" data-select-kind="wing" data-select-id="${wing.id}" data-selected="${wingSelected}">
|
|
<span class="fleet-node-title">${wing.label}</span>
|
|
<span class="fleet-node-meta">${wing.behavior} • ${wing.shipIds.length} ships</span>
|
|
<span class="fleet-node-meta">Wing Lead: ${leader ? describeShipNode(leader) : wing.leaderShipId}</span>
|
|
</div>
|
|
<div class="fleet-tree-children">
|
|
${childWings.map((childWing) => renderWingNode(fleet, childWing.id, shipsById, selectedShipIds)).join("")}
|
|
${nonLeaderShips
|
|
.map(
|
|
(ship) => `
|
|
<div class="fleet-tree-node ship-node">
|
|
<div class="fleet-node-card" data-select-kind="ship" data-select-id="${ship.id}" data-selected="${selectedShipIds.has(ship.id)}">
|
|
<span class="fleet-node-title">${ship.definition.label}</span>
|
|
<span class="fleet-node-meta">${describeShipNode(ship)}</span>
|
|
</div>
|
|
</div>
|
|
`,
|
|
)
|
|
.join("")}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
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<string>([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;
|
|
}
|