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,216 @@
import type { SystemSnapshot } from "./contracts";
import type {
CameraMode,
OrbitalAnchor,
Selectable,
SelectionGroup,
WorldState,
} from "./viewerTypes";
export function describeSelectable(world: WorldState | undefined, item: Selectable): string {
if (!world) {
return item.kind;
}
if (item.kind === "ship") {
return world.ships.get(item.id)?.label ?? item.id;
}
if (item.kind === "station") {
return world.stations.get(item.id)?.label ?? item.id;
}
if (item.kind === "node") {
return item.id;
}
if (item.kind === "spatial-node") {
return `${world.spatialNodes.get(item.id)?.kind ?? "node"} ${item.id}`;
}
if (item.kind === "bubble") {
return `bubble ${item.id}`;
}
if (item.kind === "claim") {
return `claim ${item.id}`;
}
if (item.kind === "construction-site") {
return `construction ${item.id}`;
}
if (item.kind === "planet") {
return world.systems.get(item.systemId)?.planets[item.planetIndex]?.label ?? `${item.systemId}:${item.planetIndex}`;
}
return world.systems.get(item.id)?.label ?? item.id;
}
export function getSelectionGroup(item: Selectable): SelectionGroup {
if (item.kind === "ship") {
return "ships";
}
if (
item.kind === "station"
|| item.kind === "node"
|| item.kind === "spatial-node"
|| item.kind === "bubble"
|| item.kind === "claim"
|| item.kind === "construction-site"
) {
return "structures";
}
return "celestials";
}
export function resolveSelectableSystemId(world: WorldState | undefined, selection: Selectable): string | undefined {
if (!world) {
return undefined;
}
if (selection.kind === "ship") {
return world.ships.get(selection.id)?.systemId;
}
if (selection.kind === "station") {
return world.stations.get(selection.id)?.systemId;
}
if (selection.kind === "node") {
return world.nodes.get(selection.id)?.systemId;
}
if (selection.kind === "spatial-node") {
return world.spatialNodes.get(selection.id)?.systemId;
}
if (selection.kind === "bubble") {
return world.localBubbles.get(selection.id)?.systemId;
}
if (selection.kind === "claim") {
return world.claims.get(selection.id)?.systemId;
}
if (selection.kind === "construction-site") {
return world.constructionSites.get(selection.id)?.systemId;
}
if (selection.kind === "planet") {
return selection.systemId;
}
return selection.id;
}
export function resolveFocusedBubbleId(world: WorldState | undefined, selectedItems: Selectable[]): string | undefined {
if (!world || selectedItems.length !== 1) {
return undefined;
}
const selected = selectedItems[0];
if (selected.kind === "bubble") {
return selected.id;
}
if (selected.kind === "ship") {
return world.ships.get(selected.id)?.bubbleId ?? world.ships.get(selected.id)?.spatialState.currentBubbleId ?? undefined;
}
if (selected.kind === "station") {
return world.stations.get(selected.id)?.bubbleId ?? undefined;
}
if (selected.kind === "spatial-node") {
return world.spatialNodes.get(selected.id)?.bubbleId ?? undefined;
}
if (selected.kind === "claim") {
return world.claims.get(selected.id)?.bubbleId ?? undefined;
}
if (selected.kind === "construction-site") {
return world.constructionSites.get(selected.id)?.bubbleId ?? undefined;
}
return undefined;
}
export function describeOrbitalParent(world: WorldState | undefined, systemId?: string, anchor?: OrbitalAnchor): string {
if (!world || !systemId) {
return "unknown";
}
const system = world.systems.get(systemId);
if (!system) {
return systemId;
}
if (!anchor || anchor.kind === "star") {
return `${system.label} star`;
}
const planet = system.planets[anchor.planetIndex];
if (!planet) {
return `${system.label} star`;
}
if (anchor.kind === "planet") {
return planet.label;
}
return `${planet.label} moon ${anchor.moonIndex + 1}`;
}
export function renderSystemDetails(
world: WorldState | undefined,
system: SystemSnapshot,
activeContext: boolean,
cameraMode: CameraMode,
cameraTargetShipId?: string,
): string {
if (!world) {
return "";
}
let shipCount = 0;
let stationCount = 0;
let nodeCount = 0;
let spatialNodeCount = 0;
let bubbleCount = 0;
let claimCount = 0;
let constructionCount = 0;
let moonCount = 0;
for (const ship of world.ships.values()) {
if (ship.systemId === system.id) {
shipCount += 1;
}
}
for (const station of world.stations.values()) {
if (station.systemId === system.id) {
stationCount += 1;
}
}
for (const node of world.nodes.values()) {
if (node.systemId === system.id) {
nodeCount += 1;
}
}
for (const node of world.spatialNodes.values()) {
if (node.systemId === system.id) {
spatialNodeCount += 1;
}
}
for (const bubble of world.localBubbles.values()) {
if (bubble.systemId === system.id) {
bubbleCount += 1;
}
}
for (const claim of world.claims.values()) {
if (claim.systemId === system.id) {
claimCount += 1;
}
}
for (const site of world.constructionSites.values()) {
if (site.systemId === system.id) {
constructionCount += 1;
}
}
for (const planet of system.planets) {
moonCount += planet.moonCount;
}
const followText = activeContext && cameraMode === "follow" && cameraTargetShipId
? `<p>Camera locked to ${world.ships.get(cameraTargetShipId)?.label ?? cameraTargetShipId}</p>`
: "";
return `
<p>${system.id}${activeContext ? " · active system" : ""}</p>
<p>${system.starKind} · ${system.starCount} star${system.starCount > 1 ? "s" : ""}</p>
<p>Planets ${system.planets.length}<br>Moons ${moonCount}<br>Ships ${shipCount}<br>Stations ${stationCount}</p>
<p>Spatial nodes ${spatialNodeCount}<br>Resource nodes ${nodeCount}<br>Bubbles ${bubbleCount}</p>
<p>Claims ${claimCount}<br>Construction sites ${constructionCount}</p>
<p>Height ${system.galaxyPosition.y.toFixed(0)}</p>
<p>${system.planets.slice(0, 8).map((planet) => `${planet.label} (${planet.planetType})`).join("<br>")}</p>
${followText}
`;
}