322 lines
14 KiB
TypeScript
322 lines
14 KiB
TypeScript
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 `<span class="selection-strip-empty">No active selection</span>`;
|
|
}
|
|
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<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 renderCard(title: string, lines: string[]) {
|
|
return `
|
|
<article class="selection-strip-card">
|
|
<span class="selection-strip-card-title">${title}</span>
|
|
${lines.map((line) => `<span class="selection-strip-card-line">${line}</span>`).join("")}
|
|
</article>
|
|
`;
|
|
}
|
|
|
|
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;
|
|
}
|