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