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,348 @@
import * as THREE from "three";
import { ACTIVE_SYSTEM_CAPTURE_RADIUS, ACTIVE_SYSTEM_DETAIL_SCALE, GALAXY_PARALLAX_FACTOR } from "./viewerConstants";
import { computePlanetLocalPosition, currentWorldTimeSeconds, toThreeVector } from "./viewerMath";
import { resolveSelectableSystemId } from "./viewerSelection";
import type {
BubbleVisual,
ClaimVisual,
ConstructionSiteVisual,
NodeVisual,
Selectable,
ShipVisual,
SpatialNodeVisual,
StructureVisual,
WorldState,
} from "./viewerTypes";
interface ResolveSelectionPositionParams {
world: WorldState | undefined;
selection: Selectable;
worldTimeSyncMs: number;
nodeVisuals: Map<string, NodeVisual>;
planetVisuals: { systemId: string; planet: { label: string }; mesh: THREE.Mesh }[];
computeNodeLocalPosition: (visual: NodeVisual, timeSeconds: number) => THREE.Vector3;
resolveBubblePosition: (bubbleId: string) => THREE.Vector3 | undefined;
resolvePointPosition: (systemId: string, nodeId?: string | null) => THREE.Vector3;
}
interface FocusOnSelectionParams extends ResolveSelectionPositionParams {
activeSystemId?: string;
galaxyFocus: THREE.Vector3;
systemFocusLocal: THREE.Vector3;
}
interface DetermineActiveSystemParams {
world: WorldState | undefined;
cameraMode: "tactical" | "follow";
cameraTargetShipId?: string;
currentDistance: number;
selectedItems: Selectable[];
galaxyFocus: THREE.Vector3;
}
interface SeedSystemFocusParams {
world: WorldState | undefined;
systemId: string;
cameraMode: "tactical" | "follow";
cameraTargetShipId?: string;
selectedItems: Selectable[];
systemFocusLocal: THREE.Vector3;
worldTimeSyncMs: number;
nodeVisuals: Map<string, NodeVisual>;
planetVisuals: { systemId: string; planet: { label: string }; mesh: THREE.Mesh }[];
computeNodeLocalPosition: (visual: NodeVisual, timeSeconds: number) => THREE.Vector3;
resolveBubblePosition: (bubbleId: string) => THREE.Vector3 | undefined;
resolvePointPosition: (systemId: string, nodeId?: string | null) => THREE.Vector3;
}
interface CameraFocusParams {
world: WorldState | undefined;
activeSystemId?: string;
galaxyFocus: THREE.Vector3;
systemFocusLocal: THREE.Vector3;
}
interface DisplayLocalPositionParams {
world: WorldState | undefined;
systemId?: string;
activeSystemId?: string;
localPosition: THREE.Vector3;
systemFocusLocal: THREE.Vector3;
}
export function updatePanFromKeyboard(
keyState: Set<string>,
orbitYaw: number,
currentDistance: number,
activeSystemId: string | undefined,
systemFocusLocal: THREE.Vector3,
galaxyFocus: THREE.Vector3,
delta: number,
minimumDistance: number,
maximumDistance: number,
) {
const move = new THREE.Vector3();
if (keyState.has("w")) {
move.z -= 1;
}
if (keyState.has("s")) {
move.z += 1;
}
if (keyState.has("a")) {
move.x += 1;
}
if (keyState.has("d")) {
move.x -= 1;
}
if (move.lengthSq() === 0) {
return;
}
move.normalize();
const forward = new THREE.Vector3(Math.cos(orbitYaw), 0, Math.sin(orbitYaw));
const right = new THREE.Vector3(-forward.z, 0, forward.x);
const pan = right.multiplyScalar(move.x).add(forward.multiplyScalar(move.z));
const speed = THREE.MathUtils.mapLinear(currentDistance, minimumDistance, maximumDistance, 320, 6800);
if (activeSystemId) {
systemFocusLocal.addScaledVector(pan, speed * delta);
return;
}
galaxyFocus.addScaledVector(pan, speed * delta);
}
export function determineActiveSystemId(params: DetermineActiveSystemParams): string | undefined {
const {
world,
cameraMode,
cameraTargetShipId,
currentDistance,
selectedItems,
galaxyFocus,
} = params;
if (!world) {
return undefined;
}
if (cameraMode === "follow" && cameraTargetShipId) {
return world.ships.get(cameraTargetShipId)?.systemId;
}
if (currentDistance >= 12000) {
return undefined;
}
const selected = selectedItems[0];
if (selected && selectedItems.length === 1) {
if (selected.kind === "system") {
return selected.id;
}
if (selected.kind === "planet") {
return selected.systemId;
}
const selectedSystemId = resolveSelectableSystemId(world, selected);
if (selectedSystemId) {
return selectedSystemId;
}
}
let nearestSystemId: string | undefined;
let nearestDistance = Number.POSITIVE_INFINITY;
for (const system of world.systems.values()) {
const center = toThreeVector(system.galaxyPosition);
const distance = center.distanceTo(galaxyFocus);
if (distance < nearestDistance) {
nearestDistance = distance;
nearestSystemId = system.id;
}
}
return nearestDistance <= Math.max(ACTIVE_SYSTEM_CAPTURE_RADIUS, currentDistance * 2.2)
? nearestSystemId
: undefined;
}
export function resolveSelectionPosition(params: ResolveSelectionPositionParams): THREE.Vector3 | undefined {
const {
world,
selection,
worldTimeSyncMs,
nodeVisuals,
planetVisuals,
computeNodeLocalPosition,
resolveBubblePosition,
resolvePointPosition,
} = params;
if (!world) {
return undefined;
}
if (selection.kind === "ship") {
const ship = world.ships.get(selection.id);
return ship ? toThreeVector(ship.localPosition) : undefined;
}
if (selection.kind === "station") {
const station = world.stations.get(selection.id);
return station ? toThreeVector(station.localPosition) : undefined;
}
if (selection.kind === "node") {
const node = world.nodes.get(selection.id);
const visual = node ? nodeVisuals.get(node.id) : undefined;
return visual
? computeNodeLocalPosition(visual, currentWorldTimeSeconds(world, worldTimeSyncMs))
: (node ? toThreeVector(node.localPosition) : undefined);
}
if (selection.kind === "spatial-node") {
const node = world.spatialNodes.get(selection.id);
return node ? toThreeVector(node.localPosition) : undefined;
}
if (selection.kind === "bubble") {
return resolveBubblePosition(selection.id);
}
if (selection.kind === "claim") {
const claim = world.claims.get(selection.id);
return claim ? resolvePointPosition(claim.systemId, claim.nodeId) : undefined;
}
if (selection.kind === "construction-site") {
const site = world.constructionSites.get(selection.id);
return site ? resolvePointPosition(site.systemId, site.nodeId) : undefined;
}
if (selection.kind === "planet") {
const system = world.systems.get(selection.systemId);
const planet = system?.planets[selection.planetIndex];
if (!system || !planet) {
return undefined;
}
const visual = planetVisuals.find((candidate) =>
candidate.systemId === selection.systemId && candidate.planet === planet);
return visual?.mesh.position.clone() ?? computePlanetLocalPosition(planet, currentWorldTimeSeconds(world, worldTimeSyncMs));
}
const system = world.systems.get(selection.id);
return system ? toThreeVector(system.galaxyPosition) : undefined;
}
export function focusOnSelection(params: FocusOnSelectionParams) {
const {
world,
selection,
activeSystemId,
galaxyFocus,
systemFocusLocal,
} = params;
const nextFocus = resolveSelectionPosition(params);
if (!nextFocus) {
return;
}
const selectionSystemId = resolveSelectableSystemId(world, selection);
if (selectionSystemId && selection.kind !== "system" && world) {
const system = world.systems.get(selectionSystemId);
if (system) {
galaxyFocus.copy(toThreeVector(system.galaxyPosition));
systemFocusLocal.copy(nextFocus);
return;
}
}
if (activeSystemId && resolveSelectableSystemId(world, selection) === activeSystemId) {
systemFocusLocal.copy(nextFocus);
return;
}
galaxyFocus.copy(nextFocus);
}
export function seedSystemFocusLocal(params: SeedSystemFocusParams) {
const {
world,
systemId,
cameraMode,
cameraTargetShipId,
selectedItems,
systemFocusLocal,
} = params;
if (!world) {
return;
}
if (cameraMode === "follow" && cameraTargetShipId) {
const followedShip = world.ships.get(cameraTargetShipId);
if (followedShip?.systemId === systemId) {
systemFocusLocal.copy(toThreeVector(followedShip.localPosition));
return;
}
}
const selected = selectedItems[0];
if (selected && resolveSelectableSystemId(world, selected) === systemId) {
const selectedPosition = resolveSelectionPosition({
world,
selection: selected,
worldTimeSyncMs: params.worldTimeSyncMs,
nodeVisuals: params.nodeVisuals,
planetVisuals: params.planetVisuals,
computeNodeLocalPosition: params.computeNodeLocalPosition,
resolveBubblePosition: params.resolveBubblePosition,
resolvePointPosition: params.resolvePointPosition,
});
if (selectedPosition) {
systemFocusLocal.copy(selectedPosition);
return;
}
}
systemFocusLocal.set(0, 0, 0);
}
export function getCameraFocusWorldPosition(params: CameraFocusParams): THREE.Vector3 {
const {
world,
activeSystemId,
galaxyFocus,
systemFocusLocal,
} = params;
if (!activeSystemId || !world) {
return galaxyFocus;
}
const system = world.systems.get(activeSystemId);
return system
? toThreeVector(system.galaxyPosition).add(
systemFocusLocal.clone().multiplyScalar(ACTIVE_SYSTEM_DETAIL_SCALE * GALAXY_PARALLAX_FACTOR),
)
: galaxyFocus;
}
export function toDisplayLocalPosition(params: DisplayLocalPositionParams): THREE.Vector3 {
const {
world,
systemId,
activeSystemId,
localPosition,
systemFocusLocal,
} = params;
if (!world || !systemId) {
return localPosition.clone();
}
const system = world.systems.get(systemId);
if (!system) {
return localPosition.clone();
}
const center = toThreeVector(system.galaxyPosition);
if (systemId !== activeSystemId) {
return center.clone().add(localPosition);
}
return center.clone().add(localPosition.clone().sub(systemFocusLocal).multiplyScalar(ACTIVE_SYSTEM_DETAIL_SCALE));
}