feat: 3 scene rendering setup

This commit is contained in:
2026-03-18 08:49:51 -04:00
parent 933c6afd08
commit 358122a74a
33 changed files with 1094 additions and 1132 deletions

View File

@@ -1,4 +1,4 @@
import type { ShipSnapshot, SpatialNodeSnapshot, SystemSnapshot } from "./contracts";
import type { CelestialSnapshot, ShipSnapshot, SystemSnapshot } from "./contracts";
import type {
CameraMode,
OrbitalAnchor,
@@ -21,11 +21,8 @@ export function describeSelectable(world: WorldState | undefined, item: Selectab
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 === "celestial") {
return `${world.celestials.get(item.id)?.kind ?? "celestial"} ${item.id}`;
}
if (item.kind === "claim") {
return `claim ${item.id}`;
@@ -53,7 +50,29 @@ export function describeHoverLabel(world: WorldState | undefined, item: Selectab
}
if (item.kind === "system") {
return world.systems.get(item.id)?.label ?? item.id;
const system = world.systems.get(item.id);
if (!system) {
return item.id;
}
const starLabel = system.starCount > 1 ? `${system.starCount}× ${system.starKind}` : system.starKind;
const planetCount = system.planets.length;
const shipCount = [...world.ships.values()].filter((s) => s.systemId === item.id).length;
const stationCount = [...world.stations.values()].filter((s) => s.systemId === item.id).length;
const lines = [
system.label,
`${starLabel} · ${planetCount} planet${planetCount !== 1 ? "s" : ""}`,
];
const parts: string[] = [];
if (shipCount > 0) {
parts.push(`${shipCount} ship${shipCount !== 1 ? "s" : ""}`);
}
if (stationCount > 0) {
parts.push(`${stationCount} station${stationCount !== 1 ? "s" : ""}`);
}
if (parts.length > 0) {
lines.push(parts.join(" · "));
}
return lines.join("\n");
}
if (item.kind === "planet") {
@@ -68,46 +87,38 @@ export function describeHoverLabel(world: WorldState | undefined, item: Selectab
return item.id;
}
const anchorPath = node.anchorNodeId
? describeSpatialNodePathWithinSystem(world, node.systemId, node.anchorNodeId)
const anchorPath = node.celestialId
? describeCelestialPathWithinSystem(world, node.systemId, node.celestialId)
: undefined;
return anchorPath ? `${anchorPath} / ${node.itemId}` : `${node.systemId} / ${node.itemId}`;
}
if (item.kind === "spatial-node") {
const node = world.spatialNodes.get(item.id);
if (!node) {
if (item.kind === "celestial") {
const celestial = world.celestials.get(item.id);
if (!celestial) {
return item.id;
}
if (node.kind === "star") {
const system = world.systems.get(node.systemId);
return system ? `${system.label} star` : `${node.systemId} star`;
if (celestial.kind === "star") {
const system = world.systems.get(celestial.systemId);
return system ? `${system.label} star` : `${celestial.systemId} star`;
}
return describeSpatialNodePathWithinSystem(world, node.systemId, node.id) ?? `${node.systemId} / ${node.kind}`;
}
if (item.kind === "bubble") {
const bubble = world.localBubbles.get(item.id);
const anchorPath = bubble?.nodeId
? describeSpatialNodePathWithinSystem(world, bubble.systemId, bubble.nodeId)
: undefined;
return anchorPath ? `${anchorPath} bubble` : `Bubble ${item.id}`;
return describeCelestialPathWithinSystem(world, celestial.systemId, celestial.id) ?? `${celestial.systemId} / ${celestial.kind}`;
}
if (item.kind === "claim") {
const claim = world.claims.get(item.id);
const anchorPath = claim?.nodeId
? describeSpatialNodePathWithinSystem(world, claim.systemId, claim.nodeId)
const anchorPath = claim?.celestialId
? describeCelestialPathWithinSystem(world, claim.systemId, claim.celestialId)
: undefined;
return anchorPath ? `${anchorPath} claim` : `Claim ${item.id}`;
}
if (item.kind === "construction-site") {
const site = world.constructionSites.get(item.id);
const anchorPath = site?.nodeId
? describeSpatialNodePathWithinSystem(world, site.systemId, site.nodeId)
const anchorPath = site?.celestialId
? describeCelestialPathWithinSystem(world, site.systemId, site.celestialId)
: undefined;
const siteLabel = site ? (site.blueprintId ?? site.targetDefinitionId) : item.id;
return anchorPath ? `${anchorPath} / ${siteLabel}` : `Construction ${siteLabel}`;
@@ -123,8 +134,6 @@ export function getSelectionGroup(item: Selectable): SelectionGroup {
if (
item.kind === "station"
|| item.kind === "node"
|| item.kind === "spatial-node"
|| item.kind === "bubble"
|| item.kind === "claim"
|| item.kind === "construction-site"
) {
@@ -147,11 +156,8 @@ export function resolveSelectableSystemId(world: WorldState | undefined, selecti
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 === "celestial") {
return world.celestials.get(selection.id)?.systemId;
}
if (selection.kind === "claim") {
return world.claims.get(selection.id)?.systemId;
@@ -165,29 +171,26 @@ export function resolveSelectableSystemId(world: WorldState | undefined, selecti
return selection.id;
}
export function resolveFocusedBubbleId(world: WorldState | undefined, selectedItems: Selectable[]): string | undefined {
export function resolveFocusedCelestialId(world: WorldState | undefined, selectedItems: Selectable[]): string | undefined {
if (!world || selectedItems.length !== 1) {
return undefined;
}
const selected = selectedItems[0];
if (selected.kind === "bubble") {
if (selected.kind === "celestial") {
return selected.id;
}
if (selected.kind === "ship") {
return world.ships.get(selected.id)?.bubbleId ?? world.ships.get(selected.id)?.spatialState.currentBubbleId ?? undefined;
return world.ships.get(selected.id)?.spatialState.currentCelestialId ?? world.ships.get(selected.id)?.celestialId ?? 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;
return world.stations.get(selected.id)?.celestialId ?? undefined;
}
if (selected.kind === "claim") {
return world.claims.get(selected.id)?.bubbleId ?? undefined;
return world.claims.get(selected.id)?.celestialId ?? undefined;
}
if (selected.kind === "construction-site") {
return world.constructionSites.get(selected.id)?.bubbleId ?? undefined;
return world.constructionSites.get(selected.id)?.celestialId ?? undefined;
}
return undefined;
}
@@ -232,8 +235,7 @@ export function renderSystemDetails(
let shipCount = 0;
let stationCount = 0;
let nodeCount = 0;
let spatialNodeCount = 0;
let bubbleCount = 0;
let celestialCount = 0;
let claimCount = 0;
let constructionCount = 0;
let moonCount = 0;
@@ -253,14 +255,9 @@ export function renderSystemDetails(
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 celestial of world.celestials.values()) {
if (celestial.systemId === system.id) {
celestialCount += 1;
}
}
for (const claim of world.claims.values()) {
@@ -285,7 +282,7 @@ export function renderSystemDetails(
<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>Celestials ${celestialCount}<br>Resource nodes ${nodeCount}</p>
<p>Claims ${claimCount}<br>Construction sites ${constructionCount}</p>
<p>Height ${formatGalaxyDistance(system.galaxyPosition.y)}</p>
<p>${system.planets.slice(0, 8).map((planet) => `${planet.label} (${planet.planetType})`).join("<br>")}</p>
@@ -308,18 +305,18 @@ export function describeShipState(world: WorldState | undefined, ship: ShipSnaps
return baseState;
}
const destinationNode = world.spatialNodes.get(destinationNodeId);
if (!destinationNode) {
const destinationCelestial = world.celestials.get(destinationNodeId);
if (!destinationCelestial) {
return `${baseState} -> ${destinationNodeId}`;
}
if (baseState === "warping" || baseState === "spooling-warp") {
const destinationPath = describeSpatialNodePathWithinSystem(world, destinationNode.systemId, destinationNodeId);
const destinationPath = describeCelestialPathWithinSystem(world, destinationCelestial.systemId, destinationNodeId);
return `${baseState} -> ${destinationPath ?? destinationNodeId}`;
}
const destinationSystem = world.systems.get(destinationNode.systemId);
return `${baseState} -> ${destinationSystem?.label ?? destinationNode.systemId}`;
const destinationSystem = world.systems.get(destinationCelestial.systemId);
return `${baseState} -> ${destinationSystem?.label ?? destinationCelestial.systemId}`;
}
function describeControllerTask(taskKind: string): string {
@@ -381,8 +378,8 @@ export function describeShipLocation(world: WorldState | undefined, ship: ShipSn
if (ship.dockedStationId) {
const station = world.stations.get(ship.dockedStationId);
if (station) {
const anchorPath = station.anchorNodeId
? describeSpatialNodePathWithinSystem(world, station.systemId, station.anchorNodeId)
const anchorPath = station.celestialId
? describeCelestialPathWithinSystem(world, station.systemId, station.celestialId)
: undefined;
return {
system: systemLabel,
@@ -391,22 +388,11 @@ export function describeShipLocation(world: WorldState | undefined, ship: ShipSn
}
}
const currentNodeId = ship.spatialState.currentNodeId ?? ship.nodeId;
if (currentNodeId) {
const nodePath = describeSpatialNodePathWithinSystem(world, systemId, currentNodeId);
if (nodePath) {
return { system: systemLabel, local: nodePath };
}
}
const currentBubbleId = ship.spatialState.currentBubbleId ?? ship.bubbleId;
if (currentBubbleId) {
const bubble = world.localBubbles.get(currentBubbleId);
if (bubble?.nodeId) {
const nodePath = describeSpatialNodePathWithinSystem(world, systemId, bubble.nodeId);
if (nodePath) {
return { system: systemLabel, local: nodePath };
}
const currentCelestialId = ship.spatialState.currentCelestialId ?? ship.celestialId;
if (currentCelestialId) {
const celestialPath = describeCelestialPathWithinSystem(world, systemId, currentCelestialId);
if (celestialPath) {
return { system: systemLabel, local: celestialPath };
}
}
@@ -415,11 +401,11 @@ export function describeShipLocation(world: WorldState | undefined, ship: ShipSn
export function describeActiveSpace(
world: WorldState | undefined,
zoomLevel: "local" | "system" | "universe",
povLevel: "local" | "system" | "galaxy",
activeSystemId: string | undefined,
selectedItems: Selectable[],
): string {
if (!world || zoomLevel === "universe") {
if (!world || povLevel === "galaxy") {
return "deep-space";
}
@@ -428,16 +414,13 @@ export function describeActiveSpace(
return "deep-space";
}
if (zoomLevel !== "local") {
if (povLevel !== "local") {
return activeSystem.label;
}
const bubbleId = resolveFocusedBubbleId(world, selectedItems);
if (bubbleId) {
const bubble = world.localBubbles.get(bubbleId);
const localPath = bubble?.nodeId
? describeSpatialNodePathWithinSystem(world, activeSystem.id, bubble.nodeId)
: undefined;
const celestialId = resolveFocusedCelestialId(world, selectedItems);
if (celestialId) {
const localPath = describeCelestialPathWithinSystem(world, activeSystem.id, celestialId);
return localPath
? `${activeSystem.label} / ${localPath}`
: activeSystem.label;
@@ -454,51 +437,43 @@ export function describeActiveSpace(
return activeSystem.label;
}
export function describeSpatialNodePathWithinSystem(world: WorldState, systemId: string, nodeId: string): string | undefined {
const node = world.spatialNodes.get(nodeId);
export function describeCelestialPathWithinSystem(world: WorldState, systemId: string, celestialId: string): string | undefined {
const celestial = world.celestials.get(celestialId);
const system = world.systems.get(systemId);
if (!node || !system) {
if (!celestial || !system) {
return undefined;
}
if (node.parentNodeId) {
const parentPath = describeSpatialNodePathWithinSystem(world, systemId, node.parentNodeId);
const segment = describeSpatialNodeSegment(world, system, node);
if (celestial.parentNodeId) {
const parentPath = describeCelestialPathWithinSystem(world, systemId, celestial.parentNodeId);
const segment = describeCelestialSegment(system, celestial);
return parentPath ? `${parentPath}/${segment}` : segment;
}
if (node.kind === "star") {
if (celestial.kind === "star") {
return undefined;
}
return describeSpatialNodeSegment(world, system, node);
return describeCelestialSegment(system, celestial);
}
function describeSpatialNodeSegment(world: WorldState, system: SystemSnapshot, node: SpatialNodeSnapshot): string {
const moonMatch = node.id.match(/-planet-(\d+)-moon-(\d+)$/);
function describeCelestialSegment(system: SystemSnapshot, celestial: CelestialSnapshot): string {
const moonMatch = celestial.id.match(/-planet-(\d+)-moon-(\d+)$/);
if (moonMatch) {
const moonIndex = Number.parseInt(moonMatch[2], 10);
return `Moon ${moonIndex}`;
}
const lagrangeMatch = node.id.match(/-planet-\d+-(l[1-5])$/);
const lagrangeMatch = celestial.id.match(/-planet-\d+-(l[1-5])$/);
if (lagrangeMatch) {
return lagrangeMatch[1].toUpperCase();
}
const planetMatch = node.id.match(/-planet-(\d+)$/);
const planetMatch = celestial.id.match(/-planet-(\d+)$/);
if (planetMatch) {
const planetIndex = Number.parseInt(planetMatch[1], 10) - 1;
return system.planets[planetIndex]?.label ?? `Planet ${planetMatch[1]}`;
}
if (node.kind === "station" && node.occupyingStructureId) {
return world.stations.get(node.occupyingStructureId)?.label ?? node.occupyingStructureId;
}
if (node.kind === "resource-site") {
return node.orbitReferenceId ?? "Resource Site";
}
return node.orbitReferenceId ?? node.kind;
return celestial.orbitReferenceId ?? celestial.kind;
}