Refactor simulation and viewer architecture

This commit is contained in:
2026-03-14 15:08:49 -04:00
parent ddca4a16d5
commit 651556c916
71 changed files with 11472 additions and 9031 deletions

View File

@@ -0,0 +1,296 @@
import { formatInventory, formatVector } from "./viewerMath";
import { describeOrbitalParent, describeSelectable, getSelectionGroup, renderSystemDetails } from "./viewerSelection";
import type {
CameraMode,
HistoryWindowState,
NodeVisual,
OrbitalAnchor,
Selectable,
ShipVisual,
StructureVisual,
WorldState,
} from "./viewerTypes";
interface DetailPanelParams {
world: WorldState;
selectedItems: Selectable[];
zoomLevel: string;
cameraMode: CameraMode;
cameraTargetShipId?: string;
worldLabel: string;
describeSelectionParent: (selection: Selectable) => string;
}
interface SystemPanelParams {
world: WorldState;
activeSystemId?: string;
systemTitleEl: HTMLHeadingElement;
systemBodyEl: HTMLDivElement;
systemPanelEl: HTMLDivElement;
cameraMode: CameraMode;
cameraTargetShipId?: string;
}
export function updateDetailPanel(
detailTitleEl: HTMLHeadingElement,
detailBodyEl: HTMLDivElement,
params: DetailPanelParams,
) {
const {
world,
selectedItems,
zoomLevel,
cameraMode,
cameraTargetShipId,
worldLabel,
describeSelectionParent,
} = params;
if (selectedItems.length === 0) {
detailTitleEl.textContent = worldLabel;
detailBodyEl.innerHTML = `
Zoom ${zoomLevel}<br>
Systems ${world.systems.size}<br>
Spatial nodes ${world.spatialNodes.size}<br>
Bubbles ${world.localBubbles.size}<br>
Stations ${world.stations.size}<br>
Claims ${world.claims.size}<br>
Construction ${world.constructionSites.size}<br>
Ships ${world.ships.size}<br>
Recent events ${world.recentEvents.length}
`;
return;
}
if (selectedItems.length > 1) {
const group = getSelectionGroup(selectedItems[0]);
detailTitleEl.textContent = `${selectedItems.length} selected`;
detailBodyEl.innerHTML = `
Type ${group}<br>
${selectedItems.slice(0, 8).map((item) => describeSelectable(world, item)).join("<br>")}
`;
return;
}
const selected = selectedItems[0];
if (selected.kind === "ship") {
const ship = world.ships.get(selected.id);
if (!ship) {
return;
}
const parent = describeSelectionParent(selected);
const cargoUsed = ship.inventory.reduce((sum, entry) => sum + entry.amount, 0);
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>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>
`;
return;
}
if (selected.kind === "station") {
const station = world.stations.get(selected.id);
if (!station) {
return;
}
const parent = describeSelectionParent(selected);
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>
<p>History available in the separate history window.</p>
`;
return;
}
if (selected.kind === "node") {
const node = world.nodes.get(selected.id);
if (!node) {
return;
}
const parent = describeSelectionParent(selected);
detailTitleEl.textContent = `Node ${node.id}`;
detailBodyEl.innerHTML = `
<p>${node.systemId}</p>
<p>Parent ${parent}</p>
<p>Source ${node.sourceKind}<br>Resource ${node.itemId}</p>
<p>Stock ${node.oreRemaining.toFixed(0)} / ${node.maxOre.toFixed(0)}</p>
`;
return;
}
if (selected.kind === "spatial-node") {
const node = world.spatialNodes.get(selected.id);
if (!node) {
return;
}
const bubble = world.localBubbles.get(node.bubbleId);
detailTitleEl.textContent = `${node.kind} node`;
detailBodyEl.innerHTML = `
<p>${node.systemId}</p>
<p>Bubble ${node.bubbleId}</p>
<p>Parent ${node.parentNodeId ?? "none"}<br>Orbit ref ${node.orbitReferenceId ?? "none"}</p>
<p>Occupying structure ${node.occupyingStructureId ?? "none"}</p>
<p>Bubble occupants ${bubble ? bubble.occupantShipIds.length + bubble.occupantStationIds.length : 0}</p>
`;
return;
}
if (selected.kind === "bubble") {
const bubble = world.localBubbles.get(selected.id);
if (!bubble) {
return;
}
detailTitleEl.textContent = `Bubble ${bubble.id}`;
detailBodyEl.innerHTML = `
<p>${bubble.systemId}</p>
<p>Anchor node ${bubble.nodeId}<br>Radius ${bubble.radius.toFixed(0)}</p>
<p>Ships ${bubble.occupantShipIds.length}<br>Stations ${bubble.occupantStationIds.length}</p>
<p>Claims ${bubble.occupantClaimIds.length}<br>Construction sites ${bubble.occupantConstructionSiteIds.length}</p>
`;
return;
}
if (selected.kind === "claim") {
const claim = world.claims.get(selected.id);
if (!claim) {
return;
}
detailTitleEl.textContent = `Claim ${claim.id}`;
detailBodyEl.innerHTML = `
<p>${claim.systemId}</p>
<p>Node ${claim.nodeId}<br>Bubble ${claim.bubbleId}</p>
<p>State ${claim.state}<br>Health ${claim.health.toFixed(0)}</p>
<p>Activates ${new Date(claim.activatesAtUtc).toLocaleTimeString()}</p>
`;
return;
}
if (selected.kind === "construction-site") {
const site = world.constructionSites.get(selected.id);
if (!site) {
return;
}
const orderCount = [...world.marketOrders.values()].filter((order) => order.constructionSiteId === site.id).length;
detailTitleEl.textContent = `Construction ${site.id}`;
detailBodyEl.innerHTML = `
<p>${site.systemId}</p>
<p>Node ${site.nodeId}<br>Bubble ${site.bubbleId}</p>
<p>${site.targetKind} ${site.targetDefinitionId}</p>
<p>State ${site.state}<br>Progress ${(site.progress * 100).toFixed(0)}%</p>
<p>Orders ${orderCount}<br>Assigned constructors ${site.assignedConstructorShipIds.length}</p>
`;
return;
}
if (selected.kind === "planet") {
const system = world.systems.get(selected.systemId);
const planet = system?.planets[selected.planetIndex];
if (!system || !planet) {
return;
}
const parent = describeSelectionParent(selected);
detailTitleEl.textContent = planet.label;
detailBodyEl.innerHTML = `
<p>${system.label}</p>
<p>Parent ${parent}</p>
<p>${planet.planetType} · ${planet.shape} · Moons ${planet.moonCount}</p>
<p>Orbit ${planet.orbitRadius.toFixed(0)}<br>Speed ${planet.orbitSpeed.toFixed(3)}<br>Ecc ${planet.orbitEccentricity.toFixed(3)}<br>Inc ${planet.orbitInclination.toFixed(1)}°</p>
<p>Phase ${planet.orbitPhaseAtEpoch.toFixed(1)}°</p>
`;
return;
}
const system = world.systems.get(selected.id);
if (!system) {
return;
}
detailTitleEl.textContent = system.label;
detailBodyEl.innerHTML = `
<p>Parent galaxy</p>
${renderSystemDetails(world, system, false, cameraMode, cameraTargetShipId)}
`;
}
export function updateSystemPanel(params: SystemPanelParams) {
const {
world,
activeSystemId,
systemTitleEl,
systemBodyEl,
systemPanelEl,
cameraMode,
cameraTargetShipId,
} = params;
const activeSystem = activeSystemId ? world.systems.get(activeSystemId) : undefined;
systemPanelEl.hidden = !activeSystem;
if (!activeSystem) {
systemTitleEl.textContent = "Deep Space";
systemBodyEl.innerHTML = "";
return;
}
systemTitleEl.textContent = activeSystem.label;
systemBodyEl.innerHTML = renderSystemDetails(world, activeSystem, true, cameraMode, cameraTargetShipId);
}
export function describeSelectionParent(
world: WorldState | undefined,
selection: Selectable,
stationVisuals: Map<string, StructureVisual>,
nodeVisuals: Map<string, NodeVisual>,
) {
if (!world) {
return "unknown";
}
if (selection.kind === "system") {
return "galaxy";
}
if (selection.kind === "planet") {
const system = world.systems.get(selection.systemId);
return system ? `${system.label} star` : selection.systemId;
}
if (selection.kind === "ship") {
const ship = world.ships.get(selection.id);
if (!ship) {
return "unknown";
}
const system = world.systems.get(ship.systemId);
return system ? `${system.label} system` : ship.systemId;
}
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 (selection.kind === "node") {
const node = world.nodes.get(selection.id);
const visual = node ? nodeVisuals.get(selection.id) : undefined;
return describeOrbitalParent(world, node?.systemId, visual?.anchor);
}
if (selection.kind === "spatial-node") {
const node = world.spatialNodes.get(selection.id);
return node?.parentNodeId ?? `${node?.systemId ?? "unknown"} network`;
}
if (selection.kind === "bubble") {
return `${world.localBubbles.get(selection.id)?.nodeId ?? "unknown"} node`;
}
if (selection.kind === "claim") {
return world.claims.get(selection.id)?.nodeId ?? "unknown";
}
if (selection.kind === "construction-site") {
return world.constructionSites.get(selection.id)?.nodeId ?? "unknown";
}
return "unknown";
}