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,362 @@
import * as THREE from "three";
import {
MAX_CAMERA_DISTANCE,
MIN_CAMERA_DISTANCE,
ZOOM_DISTANCE,
} from "./viewerConstants";
import { createViewerHud } from "./viewerHud";
import {
classifyZoomLevel,
computeZoomBlend,
formatBytes,
inventoryAmount,
smoothBand,
} from "./viewerMath";
import { updatePanFromKeyboard } from "./viewerCamera";
import {
createCirclePoints,
shipLength,
shipPresentationColor,
shipSize,
spatialNodeColor,
} from "./viewerSceneAppearance";
import {
createBackdropStars,
createNebulaClouds,
createNebulaTexture,
} from "./viewerSceneFactory";
import {
setShellReticleOpacity,
} from "./viewerControls";
import {
recordPerformanceStats,
updateNetworkPanel as renderNetworkPanel,
updatePerformancePanel as renderPerformancePanel,
} from "./viewerTelemetry";
import { renderFrame, resizeViewer, stepCamera } from "./viewerRenderLoop";
import { updatePlanetPresentation } from "./viewerPresentation";
import {
renderRecentEvents,
updateGameStatus,
updateSystemSummaries,
updateWorldPresentation,
} from "./viewerWorldPresentation";
import {
resolveFocusedBubbleId,
} from "./viewerSelection";
import { describeSelectionParent, updateSystemPanel } from "./viewerPanels";
import {
createInitialNetworkStats,
createInitialPerformanceStats,
} from "./viewerState";
import { ViewerWorldLifecycle } from "./viewerWorldLifecycle";
import { ViewerInteractionController } from "./viewerInteractionController";
import { ViewerNavigationController } from "./viewerNavigationController";
import { ViewerSceneDataController } from "./viewerSceneDataController";
import { ViewerPresentationController } from "./viewerPresentationController";
import { createViewerControllers, wireViewerEvents } from "./viewerControllerFactory";
import type { FactionSnapshot, ShipSnapshot } from "./contracts";
import type {
BubbleVisual,
CameraMode,
ClaimVisual,
ConstructionSiteVisual,
DragMode,
HistoryWindowState,
MoonVisual,
NetworkStats,
NodeVisual,
OrbitalAnchor,
PerformanceStats,
PlanetVisual,
PresentationEntry,
Selectable,
ShipVisual,
SpatialNodeVisual,
StructureVisual,
SystemSummaryVisual,
SystemVisual,
WorldState,
ZoomLevel,
} from "./viewerTypes";
export class ViewerAppController {
private readonly container: HTMLElement;
private readonly renderer = new THREE.WebGLRenderer({ antialias: true });
private readonly scene = new THREE.Scene();
private readonly camera = new THREE.PerspectiveCamera(50, 1, 0.1, 160000);
private readonly clock = new THREE.Clock();
private readonly raycaster = new THREE.Raycaster();
private readonly mouse = new THREE.Vector2();
private readonly galaxyFocus = new THREE.Vector3(2200, 0, 300);
private readonly systemFocusLocal = new THREE.Vector3();
private readonly cameraOffset = new THREE.Vector3();
private readonly keyState = new Set<string>();
private readonly systemGroup = new THREE.Group();
private readonly spatialNodeGroup = new THREE.Group();
private readonly bubbleGroup = new THREE.Group();
private readonly nodeGroup = new THREE.Group();
private readonly stationGroup = new THREE.Group();
private readonly claimGroup = new THREE.Group();
private readonly constructionSiteGroup = new THREE.Group();
private readonly shipGroup = new THREE.Group();
private readonly ambienceGroup = new THREE.Group();
private readonly selectableTargets = new Map<THREE.Object3D, Selectable>();
private readonly presentationEntries: PresentationEntry[] = [];
private readonly nodeVisuals = new Map<string, NodeVisual>();
private readonly spatialNodeVisuals = new Map<string, SpatialNodeVisual>();
private readonly bubbleVisuals = new Map<string, BubbleVisual>();
private readonly stationVisuals = new Map<string, StructureVisual>();
private readonly claimVisuals = new Map<string, ClaimVisual>();
private readonly constructionSiteVisuals = new Map<string, ConstructionSiteVisual>();
private readonly shipVisuals = new Map<string, ShipVisual>();
private readonly systemVisuals = new Map<string, SystemVisual>();
private readonly systemSummaryVisuals = new Map<string, SystemSummaryVisual>();
private readonly planetVisuals: PlanetVisual[] = [];
private readonly orbitLines: THREE.Object3D[] = [];
private readonly statusEl: HTMLDivElement;
private readonly systemPanelEl: HTMLDivElement;
private readonly systemTitleEl: HTMLHeadingElement;
private readonly systemBodyEl: HTMLDivElement;
private readonly detailTitleEl: HTMLHeadingElement;
private readonly detailBodyEl: HTMLDivElement;
private readonly factionStripEl: HTMLDivElement;
private readonly networkPanelEl: HTMLDivElement;
private readonly performancePanelEl: HTMLDivElement;
private readonly errorEl: HTMLDivElement;
private readonly historyLayerEl: HTMLDivElement;
private readonly marqueeEl: HTMLDivElement;
private readonly hoverLabelEl: HTMLDivElement;
private world?: WorldState;
private worldTimeSyncMs = performance.now();
private stream?: EventSource;
private currentStreamScopeKey = "";
private readonly networkStats: NetworkStats = createInitialNetworkStats();
private readonly performanceStats: PerformanceStats = createInitialPerformanceStats();
private selectedItems: Selectable[] = [];
private worldSignature = "";
private zoomLevel: ZoomLevel = "system";
private currentDistance = ZOOM_DISTANCE.system;
private desiredDistance = ZOOM_DISTANCE.system;
private orbitYaw = -2.3;
private orbitPitch = 0.62;
private cameraMode: CameraMode = "tactical";
private dragMode?: DragMode;
private dragPointerId?: number;
private dragStart = new THREE.Vector2();
private dragLast = new THREE.Vector2();
private marqueeActive = false;
private suppressClickSelection = false;
private activeSystemId?: string;
private cameraTargetShipId?: string;
private readonly followCameraPosition = new THREE.Vector3();
private readonly followCameraFocus = new THREE.Vector3();
private readonly followCameraDirection = new THREE.Vector3(0, 0.16, 1);
private readonly followCameraDesiredDirection = new THREE.Vector3(0, 0.16, 1);
private readonly followCameraOffset = new THREE.Vector3();
private readonly historyWindows: HistoryWindowState[] = [];
private historyWindowCounter = 0;
private historyWindowZCounter = 10;
private historyWindowDragId?: string;
private historyWindowDragPointerId?: number;
private historyWindowDragOffset = new THREE.Vector2();
private readonly worldLifecycle: ViewerWorldLifecycle;
private readonly interactionController: ViewerInteractionController;
private readonly navigationController: ViewerNavigationController;
private readonly sceneDataController: ViewerSceneDataController;
private readonly presentationController: ViewerPresentationController;
constructor(container: HTMLElement) {
this.container = container;
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.renderer.outputColorSpace = THREE.SRGBColorSpace;
this.scene.background = new THREE.Color(0x040912);
this.scene.fog = new THREE.FogExp2(0x040912, 0.00011);
this.scene.add(new THREE.AmbientLight(0x90a6c0, 0.55));
const keyLight = new THREE.DirectionalLight(0xdcecff, 1.3);
keyLight.position.set(1000, 1200, 800);
this.scene.add(keyLight);
const hud = createViewerHud(document);
this.statusEl = hud.statusEl;
this.systemPanelEl = hud.systemPanelEl;
this.systemTitleEl = hud.systemTitleEl;
this.systemBodyEl = hud.systemBodyEl;
this.detailTitleEl = hud.detailTitleEl;
this.detailBodyEl = hud.detailBodyEl;
this.factionStripEl = hud.factionStripEl;
this.networkPanelEl = hud.networkPanelEl;
this.performancePanelEl = hud.performancePanelEl;
this.errorEl = hud.errorEl;
this.historyLayerEl = hud.historyLayerEl;
this.marqueeEl = hud.marqueeEl;
this.hoverLabelEl = hud.hoverLabelEl;
({
sceneDataController: this.sceneDataController,
navigationController: this.navigationController,
presentationController: this.presentationController,
worldLifecycle: this.worldLifecycle,
interactionController: this.interactionController,
} = createViewerControllers(this));
this.container.append(this.renderer.domElement, hud.root);
wireViewerEvents(this);
this.onResize();
this.updateCamera(0);
}
async start() {
await this.worldLifecycle.bootstrapWorld();
this.renderer.setAnimationLoop(() => this.render());
}
private refreshStreamScopeIfNeeded() {
this.worldLifecycle.refreshStreamScopeIfNeeded();
}
private createWorldPresentationContext() {
return this.sceneDataController.createWorldPresentationContext({
world: this.world,
activeSystemId: this.activeSystemId,
orbitYaw: this.orbitYaw,
camera: this.camera,
systemFocusLocal: this.systemFocusLocal,
toDisplayLocalPosition: this.toDisplayLocalPosition.bind(this),
updateSystemDetailVisibility: () => this.navigationController.updateSystemDetailVisibility(),
setShellReticleOpacity: (sprite, opacity) => this.setShellReticleOpacity(sprite, opacity),
});
}
private rebuildFactions(_factions: FactionSnapshot[]) {
this.worldLifecycle.rebuildFactions(_factions);
}
private updatePanels() {
this.worldLifecycle.updatePanels();
}
private render() {
renderFrame({
clock: this.clock,
renderer: this.renderer,
scene: this.scene,
camera: this.camera,
updateCamera: (delta) => this.updateCamera(delta),
updateAmbience: (delta) => this.presentationController.updateAmbience(delta),
updatePlanetPresentation: () => this.presentationController.updatePlanetPresentation(),
updateShipPresentation: () => this.presentationController.updateShipPresentation(),
updateNetworkPanel: () => this.presentationController.updateNetworkPanel(),
applyZoomPresentation: () => this.presentationController.applyZoomPresentation(),
recordPerformanceStats: (frameMs) => this.presentationController.recordPerformanceStats(frameMs),
updatePerformancePanel: () => this.presentationController.updatePerformancePanel(),
});
}
private updateAmbience(delta: number) {
this.ambienceGroup.position.copy(this.camera.position);
this.ambienceGroup.rotation.y += delta * 0.005;
this.ambienceGroup.rotation.x = Math.sin(performance.now() * 0.00003) * 0.015;
}
private updateCamera(delta: number) {
const nextState = stepCamera({
currentDistance: this.currentDistance,
desiredDistance: this.desiredDistance,
orbitPitch: this.orbitPitch,
delta,
});
this.currentDistance = nextState.currentDistance;
this.zoomLevel = nextState.zoomLevel;
this.orbitPitch = nextState.orbitPitch;
this.navigationController.updateActiveSystem();
if (this.cameraMode === "follow" && this.navigationController.updateFollowCamera(delta)) {
return;
}
this.updatePanFromKeyboard(delta);
this.orbitPitch = THREE.MathUtils.clamp(this.orbitPitch, 0.18, 1.3);
const horizontalDistance = this.currentDistance * Math.cos(this.orbitPitch);
const focus = this.navigationController.getCameraFocusWorldPosition();
this.cameraOffset.set(
Math.cos(this.orbitYaw) * horizontalDistance,
this.currentDistance * Math.sin(this.orbitPitch),
Math.sin(this.orbitYaw) * horizontalDistance,
);
this.camera.position.copy(focus).add(this.cameraOffset);
this.camera.lookAt(focus);
}
private updatePanFromKeyboard(delta: number) {
updatePanFromKeyboard(
this.keyState,
this.orbitYaw,
this.currentDistance,
this.activeSystemId,
this.systemFocusLocal,
this.galaxyFocus,
delta,
MIN_CAMERA_DISTANCE,
MAX_CAMERA_DISTANCE,
);
}
private updateSystemSummaries() {
this.presentationController.updateSystemSummaries();
}
private registerPresentation(
detail: THREE.Object3D,
icon: THREE.Sprite,
hideDetailInUniverse: boolean,
hideIconInUniverse = false,
systemId?: string,
) {
this.presentationEntries.push({ detail, icon, systemId, hideDetailInUniverse, hideIconInUniverse });
}
private renderRecentEvents(entityKind: string, entityId: string) {
return this.presentationController.renderRecentEvents(entityKind, entityId);
}
private updateGamePanel(mode: string) {
this.presentationController.updateGamePanel(mode);
}
private screenPointFromClient(clientX: number, clientY: number) {
return this.presentationController.screenPointFromClient(clientX, clientY);
}
private refreshHistoryWindows() {
this.interactionController.refreshHistoryWindows();
}
private resolveFocusedBubbleId() {
return resolveFocusedBubbleId(this.world, this.selectedItems);
}
private onResize = () => {
resizeViewer({
renderer: this.renderer,
camera: this.camera,
});
};
private setShellReticleOpacity(sprite: THREE.Sprite, opacity: number) {
setShellReticleOpacity(sprite, opacity);
}
private describeSelectionParent(selection: Selectable) {
return describeSelectionParent(this.world, selection, this.stationVisuals, this.nodeVisuals);
}
private toDisplayLocalPosition(localPosition: THREE.Vector3, systemId?: string) {
return this.navigationController.toDisplayLocalPosition(localPosition, systemId);
}
private updateSystemPanel() {
this.presentationController.updateSystemPanel();
}
}