feat: improving ui
This commit is contained in:
@@ -1,9 +1,18 @@
|
||||
import type { InventoryEntry, Vector3Dto } from "./contractsCommon";
|
||||
|
||||
export interface RecipeEntrySnapshot {
|
||||
itemId: string;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export interface StationActionProgressSnapshot {
|
||||
lane: string;
|
||||
label: string;
|
||||
progress: number;
|
||||
timeRemainingSeconds: number;
|
||||
cycleSeconds: number;
|
||||
inputs: RecipeEntrySnapshot[];
|
||||
outputs: RecipeEntrySnapshot[];
|
||||
}
|
||||
|
||||
export interface StationStorageUsageSnapshot {
|
||||
|
||||
@@ -284,7 +284,7 @@ canvas {
|
||||
|
||||
.detail-progress,
|
||||
.ship-action-progress {
|
||||
margin: 0 0 12px;
|
||||
margin: 0 0 3px;
|
||||
}
|
||||
|
||||
.detail-progress-label,
|
||||
@@ -300,6 +300,15 @@ canvas {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.module-item {
|
||||
display: block;
|
||||
padding-left: 1em;
|
||||
color: var(--muted);
|
||||
font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
|
||||
font-size: 0.72rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.detail-progress-track,
|
||||
.ship-action-progress-track {
|
||||
height: 6px;
|
||||
|
||||
@@ -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("&", "&")
|
||||
.replaceAll("\"", """)
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">");
|
||||
}
|
||||
|
||||
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("&", "&")
|
||||
.replaceAll("\"", """)
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">");
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user