feat: production chain

This commit is contained in:
2026-03-15 22:46:47 -04:00
parent 651556c916
commit 5ba1287f85
65 changed files with 3718 additions and 687 deletions

View File

@@ -1,5 +1,5 @@
import { formatInventory, formatVector } from "./viewerMath";
import { describeOrbitalParent, describeSelectable, getSelectionGroup, renderSystemDetails } from "./viewerSelection";
import { formatInventory, formatVector, inventoryAmount } from "./viewerMath";
import { describeOrbitalParent, describeSelectable, describeShipCurrentAction, describeShipState, describeSpatialNodePathWithinSystem, getSelectionGroup, renderSystemDetails } from "./viewerSelection";
import type {
CameraMode,
HistoryWindowState,
@@ -31,6 +31,29 @@ interface SystemPanelParams {
cameraTargetShipId?: string;
}
function renderSystemOwnership(world: WorldState, systemId: string): string {
const claims = [...world.claims.values()].filter((claim) =>
claim.systemId === systemId && claim.state !== "destroyed");
if (claims.length === 0) {
return "Ownership none";
}
const ownershipByFaction = new Map<string, number>();
for (const claim of claims) {
ownershipByFaction.set(claim.factionId, (ownershipByFaction.get(claim.factionId) ?? 0) + 1);
}
return [...ownershipByFaction.entries()]
.sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
.map(([factionId, count]) => {
const faction = world.factions.get(factionId);
const label = faction?.label ?? factionId;
const share = Math.round((count / claims.length) * 100);
return `${label} ${count}/${claims.length} (${share}%)`;
})
.join("<br>");
}
export function updateDetailPanel(
detailTitleEl: HTMLHeadingElement,
detailBodyEl: HTMLDivElement,
@@ -79,12 +102,30 @@ export function updateDetailPanel(
return;
}
const parent = describeSelectionParent(selected);
const cargoUsed = ship.inventory.reduce((sum, entry) => sum + entry.amount, 0);
const fuelStored = inventoryAmount(ship.inventory, "fuel");
const cargoUsed = ship.cargoItemId
? inventoryAmount(ship.inventory, ship.cargoItemId)
: 0;
const cargoLabel = ship.cargoItemId ?? "none";
const shipState = describeShipState(world, ship);
const shipAction = describeShipCurrentAction(ship);
detailTitleEl.textContent = ship.label;
detailBodyEl.innerHTML = `
<p>Parent ${parent}</p>
<p>State ${ship.state}</p>
<p>Energy ${ship.energyStored.toFixed(0)}<br>Cargo ${cargoUsed.toFixed(0)} / ${ship.cargoCapacity.toFixed(0)}</p>
<p>State ${shipState}</p>
${shipAction ? `
<div class="detail-progress">
<div class="detail-progress-label">
<span>${shipAction.label}</span>
<span>${Math.round(shipAction.progress * 100)}%</span>
</div>
<div class="detail-progress-track">
<div class="detail-progress-fill" style="width: ${(shipAction.progress * 100).toFixed(1)}%"></div>
</div>
</div>
` : ""}
<p>Fuel ${fuelStored.toFixed(1)}<br>Capacitor ${ship.energyStored.toFixed(1)}</p>
<p>Cargo ${cargoLabel} ${cargoUsed.toFixed(0)} / ${ship.cargoCapacity.toFixed(0)}</p>
<p>Inventory ${formatInventory(ship.inventory)}</p>
<p>Velocity ${formatVector(ship.localVelocity)}</p>
<p>Camera ${cameraMode === "follow" && cameraTargetShipId === ship.id ? "camera-follow" : "tactical"}<br>Press C to toggle follow</p>
@@ -98,12 +139,43 @@ export function updateDetailPanel(
return;
}
const parent = describeSelectionParent(selected);
const installedModules = station.installedModules.length > 0
? station.installedModules.join("<br>")
: "none";
const activeConstruction = [...world.constructionSites.values()]
.filter((site) => site.stationId === station.id && site.state !== "completed")
.map((site) => `${site.blueprintId ?? site.targetDefinitionId} (${site.state})`)
.join("<br>") || "none";
const dockedShipLabels = station.dockedShipIds.length > 0
? station.dockedShipIds.map((shipId) => world.ships.get(shipId)?.label ?? shipId).join("<br>")
: "none";
const stationInventory = station.inventory.filter((entry) => entry.itemId !== "fuel");
const stationProcesses = station.currentProcesses;
const stationProcessingHtml = stationProcesses.length > 0
? stationProcesses.map((process) => `
<div class="detail-progress">
<div class="detail-progress-label">
<span>${process.label}</span>
<span>${Math.round(process.progress * 100)}%</span>
</div>
<div class="detail-progress-track">
<div class="detail-progress-fill" style="width: ${(process.progress * 100).toFixed(1)}%"></div>
</div>
</div>
`).join("")
: "";
detailTitleEl.textContent = station.label;
detailBodyEl.innerHTML = `
<p>${station.category} · ${station.systemId}</p>
<p>Parent ${parent}</p>
<p>Energy ${station.energyStored.toFixed(0)}<br>Docked ${station.dockedShips} / ${station.dockingPads}</p>
<p>Inventory ${formatInventory(station.inventory)}</p>
${stationProcessingHtml}
<p>Fuel ${station.fuelStored.toFixed(1)} / ${station.fuelCapacity.toFixed(1)}<br>Capacitor ${station.energyStored.toFixed(1)} / ${station.energyCapacity.toFixed(1)}</p>
<p>Docked ${station.dockedShips} / ${station.dockingPads}
<br>
${dockedShipLabels}</p>
<p>Modules ${installedModules}</p>
<p>Constructing ${activeConstruction}</p>
<p>Inventory ${formatInventory(stationInventory)}</p>
<p>History available in the separate history window.</p>
`;
return;
@@ -115,11 +187,23 @@ export function updateDetailPanel(
return;
}
const parent = describeSelectionParent(selected);
const nodeLevel = node.maxOre > 0
? Math.max(0, Math.min(node.oreRemaining / node.maxOre, 1))
: 0;
detailTitleEl.textContent = `Node ${node.id}`;
detailBodyEl.innerHTML = `
<p>${node.systemId}</p>
<p>Parent ${parent}</p>
<p>Source ${node.sourceKind}<br>Resource ${node.itemId}</p>
<div class="detail-progress">
<div class="detail-progress-label">
<span>Level</span>
<span>${Math.round(nodeLevel * 100)}%</span>
</div>
<div class="detail-progress-track">
<div class="detail-progress-fill" style="width: ${(nodeLevel * 100).toFixed(1)}%"></div>
</div>
</div>
<p>Stock ${node.oreRemaining.toFixed(0)} / ${node.maxOre.toFixed(0)}</p>
`;
return;
@@ -240,7 +324,9 @@ export function updateSystemPanel(params: SystemPanelParams) {
}
systemTitleEl.textContent = activeSystem.label;
systemBodyEl.innerHTML = renderSystemDetails(world, activeSystem, true, cameraMode, cameraTargetShipId);
systemBodyEl.innerHTML = `
<p>${renderSystemOwnership(world, activeSystem.id)}</p>
`;
}
export function describeSelectionParent(
@@ -270,8 +356,13 @@ export function describeSelectionParent(
}
if (selection.kind === "station") {
const station = world.stations.get(selection.id);
const visual = station ? stationVisuals.get(selection.id) : undefined;
return describeOrbitalParent(world, station?.systemId, visual?.anchor);
if (!station) {
return "unknown";
}
return station.anchorNodeId
? describeSpatialNodePathWithinSystem(world, station.systemId, station.anchorNodeId) ?? `${station.systemId} network`
: "unknown";
}
if (selection.kind === "node") {
const node = world.nodes.get(selection.id);