Files
space-game/src/game/ui/presenters.ts

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;
}