feat: improving ui

This commit is contained in:
2026-03-19 02:19:41 -04:00
parent aa4a6930ba
commit 710addf1f5
8 changed files with 137 additions and 69 deletions

View File

@@ -5,6 +5,15 @@ import {
formatSystemDistance,
inventoryAmount,
} from "./viewerMath";
import modulesData from "../../../shared/data/modules.json";
import itemsData from "../../../shared/data/items.json";
const moduleNameById = new Map<string, string>(
(modulesData as { id: string; name: string }[]).map((m) => [m.id, m.name]),
);
const itemTransportById = new Map<string, string>(
(itemsData as { id: string; transport: string }[]).map((item) => [item.id, item.transport]),
);
import { describeCelestialPathWithinSystem, describeOrbitalParent, describeSelectable, describeShipBehavior, describeShipCurrentAction, describeShipOrder, describeShipState, getSelectionGroup, renderSystemDetails } from "./viewerSelection";
import type {
CameraMode,
@@ -37,72 +46,74 @@ interface SystemPanelParams {
cameraTargetShipId?: string;
}
function laneModuleId(lane: string): string | undefined {
switch (lane) {
case "refinery":
return "refinery-stack";
case "fabrication":
return "fabricator-array";
case "components":
return "component-factory";
case "shipyard":
return "ship-factory";
default:
return undefined;
}
function formatDuration(seconds: number): string {
if (seconds < 60) return `${Math.ceil(seconds)}s`;
const m = Math.floor(seconds / 60);
const s = Math.ceil(seconds % 60);
return s > 0 ? `${m}m ${s}s` : `${m}m`;
}
function escapeAttr(str: string): string {
return str
.replaceAll("&", "&amp;")
.replaceAll("\"", "&quot;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;");
}
function renderProgressBar(progress: number): string {
return `<div class="detail-progress"><div class="detail-progress-track"><div class="detail-progress-fill" style="width: ${(progress * 100).toFixed(1)}%"></div></div></div>`;
}
function formatModuleListWithConstruction(
world: WorldState,
stationId: string,
installedModules: string[],
currentProcesses: { lane: string; label: string; progress: number }[],
currentProcesses: { lane: string; label: string; progress: number; timeRemainingSeconds: number }[],
): string {
const processByModule = new Map<string, { label: string; progress: number }[]>();
const processByModule = new Map<string, { label: string; progress: number; timeRemainingSeconds: number; cycleSeconds: number; inputs: { itemId: string; amount: number }[]; outputs: { itemId: string; amount: number }[] }[]>();
for (const process of currentProcesses) {
const moduleId = laneModuleId(process.lane);
if (!moduleId) {
continue;
}
const existing = processByModule.get(moduleId) ?? [];
existing.push({ label: process.label, progress: process.progress });
processByModule.set(moduleId, existing);
const existing = processByModule.get(process.lane) ?? [];
existing.push({ label: process.label, progress: process.progress, timeRemainingSeconds: process.timeRemainingSeconds, cycleSeconds: process.cycleSeconds, inputs: process.inputs, outputs: process.outputs });
processByModule.set(process.lane, existing);
}
const renderedProcessCount = new Map<string, number>();
const moduleLines = installedModules.map((moduleId) => {
const moduleHtmlParts = installedModules.map((moduleId) => {
const processIndex = renderedProcessCount.get(moduleId) ?? 0;
const processes = processByModule.get(moduleId) ?? [];
const process = processes[processIndex];
renderedProcessCount.set(moduleId, processIndex + 1);
const moduleName = moduleNameById.get(moduleId) ?? moduleId;
if (!process) {
return moduleId;
return moduleName;
}
return `${moduleId} -> ${process.label} (${Math.round(process.progress * 100)}%)`;
const inputLines = process.inputs.map((e) => ` ${e.itemId}: ${e.amount.toFixed(0)}`).join("\n");
const outputLines = process.outputs.map((e) => ` ${e.itemId}: ${e.amount.toFixed(0)}`).join("\n");
const tooltip = `Cycle: ${formatDuration(process.cycleSeconds)}\nInputs:\n${inputLines || " none"}\nOutputs:\n${outputLines || " none"}`;
const rightLabel = `${Math.round(process.progress * 100)}% · ${formatDuration(process.timeRemainingSeconds)}`;
return `<div class="detail-progress-label" title="${escapeAttr(tooltip)}"><span>${moduleName}</span><span>${rightLabel}</span></div>${renderProgressBar(process.progress)}`;
});
const activeSites = [...world.constructionSites.values()]
.filter((site) => site.stationId === stationId && site.state !== "completed")
.sort((left, right) => left.targetDefinitionId.localeCompare(right.targetDefinitionId));
for (const site of activeSites) {
const moduleId = site.blueprintId ?? site.targetDefinitionId;
const progress = Math.round(site.progress * 100);
const tooltip = site.requiredItems.length > 0
const moduleName = moduleNameById.get(moduleId) ?? moduleId;
const progress = site.progress;
const constructionTooltip = site.requiredItems.length > 0
? site.requiredItems
.map((entry) => `${entry.itemId}: ${entry.amount.toFixed(0)} required / ${inventoryAmount(site.stationId ? (world.stations.get(site.stationId)?.inventory ?? []) : site.deliveredItems, entry.itemId).toFixed(0)} available`)
.join("\n")
: "No material requirements";
const escapedTooltip = tooltip
.replaceAll("&", "&amp;")
.replaceAll("\"", "&quot;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;");
moduleLines.push(`<span title="${escapedTooltip}">${moduleId} (${progress}% constructing)</span>`);
moduleHtmlParts.push(`<div class="detail-progress-label" title="${escapeAttr(constructionTooltip)}"><span>${moduleName}</span><span>${Math.round(progress * 100)}% constructing</span></div>${renderProgressBar(progress)}`);
}
return moduleLines.length > 0 ? moduleLines.join("<br>") : "none";
return moduleHtmlParts.length > 0 ? moduleHtmlParts.join("<br>") : "none";
}
function formatStorageClassLabel(storageClass: string): string {
@@ -112,15 +123,31 @@ function formatStorageClassLabel(storageClass: string): string {
.join(" ");
}
function formatStorageUsage(storageUsage: { storageClass: string; used: number; capacity: number }[]): string {
function formatStorageWithInventory(
storageUsage: { storageClass: string; used: number; capacity: number }[],
inventory: { itemId: string; amount: number }[],
): string {
if (storageUsage.length === 0) {
return "none";
return inventory.length === 0 ? "none" : inventory.map((e) => `${e.itemId} ${e.amount.toFixed(0)}`).join("<br>");
}
const itemsByClass = new Map<string, { itemId: string; amount: number }[]>();
for (const entry of inventory) {
const cls = itemTransportById.get(entry.itemId) ?? "unknown";
const list = itemsByClass.get(cls) ?? [];
list.push(entry);
itemsByClass.set(cls, list);
}
return storageUsage
.map((entry) => {
const percentUsed = entry.capacity > 0 ? Math.round((entry.used / entry.capacity) * 100) : 0;
return `${formatStorageClassLabel(entry.storageClass)} ${percentUsed}% used (${entry.used.toFixed(0)} / ${entry.capacity.toFixed(0)})`;
const header = `${formatStorageClassLabel(entry.storageClass)} ${percentUsed}% (${entry.used.toFixed(0)} / ${entry.capacity.toFixed(0)})`;
const items = itemsByClass.get(entry.storageClass) ?? [];
if (items.length === 0) {
return `${header}<br><span style="padding-left:1em">empty</span>`;
}
return `${header}<br>${items.map((e) => `<span style="padding-left:1em">${e.itemId} ${e.amount.toFixed(0)}</span>`).join("<br>")}`;
})
.join("<br>");
}
@@ -236,33 +263,16 @@ export function updateDetailPanel(
const dockedShipLabels = station.dockedShipIds.length > 0
? station.dockedShipIds.map((shipId) => world.ships.get(shipId)?.label ?? shipId).join("<br>")
: "none";
const stationInventory = station.inventory;
const stationStorageUsage = formatStorageUsage(station.storageUsage);
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("")
: "";
const stationStorage = formatStorageWithInventory(station.storageUsage, station.inventory);
detailTitleEl.textContent = station.label;
detailBodyEl.innerHTML = `
<p>${station.category} · ${station.systemId}</p>
<p>Parent ${parent}</p>
${stationProcessingHtml}
<p>Docked ${station.dockedShips} / ${station.dockingPads}
<br>
${dockedShipLabels}</p>
<p>Modules ${moduleList}</p>
<p>Storage ${stationStorageUsage}</p>
<p>Inventory ${formatInventory(stationInventory)}</p>
<p>Storage ${stationStorage}</p>
`;
return;
}