Refactor simulation and viewer architecture
This commit is contained in:
296
apps/viewer/src/viewerPanels.ts
Normal file
296
apps/viewer/src/viewerPanels.ts
Normal 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";
|
||||
}
|
||||
Reference in New Issue
Block a user