import * as THREE from "three"; import { ACTIVE_SYSTEM_CAPTURE_RADIUS, ACTIVE_SYSTEM_DETAIL_SCALE } from "./viewerConstants"; import { DISPLAY_UNITS_PER_KILOMETER, KILOMETERS_PER_AU, computePlanetLocalPosition, currentWorldTimeSeconds, scaleGalaxyVector, scaleLocalVector, toThreeVector } from "./viewerMath"; import { resolveSelectableSystemId } from "./viewerSelection"; import type { NodeVisual, PlanetVisual, Selectable, ShipVisual, WorldState, PovLevel, } from "./viewerTypes"; interface ResolveSelectionPositionParams { world: WorldState | undefined; selection: Selectable; worldTimeSyncMs: number; nodeVisuals: Map; planetVisuals: PlanetVisual[]; computeNodeLocalPosition: (visual: NodeVisual, timeSeconds: number) => THREE.Vector3; resolvePointPosition: (systemId: string, celestialId?: string | null) => THREE.Vector3; } interface FocusOnSelectionParams extends ResolveSelectionPositionParams { activeSystemId?: string; galaxyAnchor: THREE.Vector3; systemAnchor: THREE.Vector3; } interface DetermineActiveSystemParams { world: WorldState | undefined; cameraMode: "tactical" | "follow"; cameraTargetShipId?: string; currentDistance: number; selectedItems: Selectable[]; galaxyAnchor: THREE.Vector3; } interface SeedSystemFocusParams { world: WorldState | undefined; systemId: string; cameraMode: "tactical" | "follow"; cameraTargetShipId?: string; selectedItems: Selectable[]; systemAnchor: THREE.Vector3; worldTimeSyncMs: number; nodeVisuals: Map; planetVisuals: PlanetVisual[]; computeNodeLocalPosition: (visual: NodeVisual, timeSeconds: number) => THREE.Vector3; resolvePointPosition: (systemId: string, celestialId?: string | null) => THREE.Vector3; } interface CameraFocusParams { galaxyAnchor: THREE.Vector3; } export function getSystemCameraFocus(systemAnchor: THREE.Vector3): THREE.Vector3 { return systemAnchor.clone().multiplyScalar(DISPLAY_UNITS_PER_KILOMETER * ACTIVE_SYSTEM_DETAIL_SCALE); } export function updatePanFromKeyboard( keyState: Set, orbitYaw: number, currentDistance: number, povLevel: PovLevel, activeSystemId: string | undefined, systemAnchor: THREE.Vector3, galaxyAnchor: 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)); if (activeSystemId) { const speedKilometers = povLevel === "system" ? THREE.MathUtils.mapLinear(currentDistance, 80, 4000, KILOMETERS_PER_AU * 0.002, KILOMETERS_PER_AU * 0.35) : THREE.MathUtils.mapLinear(currentDistance, minimumDistance, 120, 40, 180000); systemAnchor.addScaledVector(pan, speedKilometers * delta); return; } const speed = THREE.MathUtils.mapLinear(currentDistance, minimumDistance, maximumDistance, 320, 6800); galaxyAnchor.addScaledVector(pan, speed * delta); } export function determineActiveSystemId(params: DetermineActiveSystemParams): string | undefined { const { world, cameraMode, cameraTargetShipId, currentDistance, selectedItems, galaxyAnchor, } = params; if (!world) { return undefined; } if (cameraMode === "follow" && cameraTargetShipId) { const followedShip = world.ships.get(cameraTargetShipId); if (!followedShip) { return undefined; } return followedShip.spatialState.movementRegime === "ftl-transit" ? undefined : followedShip.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 = scaleGalaxyVector(toThreeVector(system.galaxyPosition)); const distance = center.distanceTo(galaxyAnchor); 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, 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 === "celestial") { const celestial = world.celestials.get(selection.id); return celestial ? toThreeVector(celestial.orbitalAnchor) : undefined; } if (selection.kind === "claim") { const claim = world.claims.get(selection.id); return claim ? resolvePointPosition(claim.systemId, claim.celestialId) : undefined; } if (selection.kind === "construction-site") { const site = world.constructionSites.get(selection.id); return site ? resolvePointPosition(site.systemId, site.celestialId) : undefined; } if (selection.kind === "planet") { const system = world.systems.get(selection.systemId); const planet = system?.planets[selection.planetIndex]; if (!system || !planet) { return undefined; } return computePlanetLocalPosition(planet, currentWorldTimeSeconds(world, worldTimeSyncMs)); } if (selection.kind === "moon") { const system = world.systems.get(selection.systemId); const planet = system?.planets[selection.planetIndex]; return planet ? computePlanetLocalPosition(planet, currentWorldTimeSeconds(world, worldTimeSyncMs)) : undefined; } const system = world.systems.get(selection.id); return system ? scaleGalaxyVector(toThreeVector(system.galaxyPosition)) : undefined; } export function focusOnSelection(params: FocusOnSelectionParams) { const { world, selection, activeSystemId, galaxyAnchor, systemAnchor, } = params; const nextFocus = resolveSelectionPosition(params); if (!nextFocus) { return; } if (selection.kind === "system") { galaxyAnchor.copy(nextFocus); systemAnchor.set(0, 0, 0); return; } const selectionSystemId = resolveSelectableSystemId(world, selection); if (selectionSystemId && world) { const system = world.systems.get(selectionSystemId); if (system) { galaxyAnchor.copy(scaleGalaxyVector(toThreeVector(system.galaxyPosition))); systemAnchor.copy(nextFocus); return; } } if (activeSystemId && resolveSelectableSystemId(world, selection) === activeSystemId) { systemAnchor.copy(nextFocus); return; } galaxyAnchor.copy(nextFocus); } export function seedSystemFocusLocal(params: SeedSystemFocusParams) { const { world, systemId, cameraMode, cameraTargetShipId, selectedItems, systemAnchor, } = params; if (!world) { return; } if (cameraMode === "follow" && cameraTargetShipId) { const followedShip = world.ships.get(cameraTargetShipId); if (followedShip?.systemId === systemId) { systemAnchor.copy(toThreeVector(followedShip.localPosition)); return; } } const selected = selectedItems[0]; if (selected && resolveSelectableSystemId(world, selected) === systemId) { if (selected.kind === "system") { systemAnchor.set(0, 0, 0); return; } const selectedPosition = resolveSelectionPosition({ world, selection: selected, worldTimeSyncMs: params.worldTimeSyncMs, nodeVisuals: params.nodeVisuals, planetVisuals: params.planetVisuals, computeNodeLocalPosition: params.computeNodeLocalPosition, resolvePointPosition: params.resolvePointPosition, }); if (selectedPosition) { systemAnchor.copy(selectedPosition); return; } } systemAnchor.set(0, 0, 0); } export function getCameraFocusWorldPosition(params: CameraFocusParams): THREE.Vector3 { return params.galaxyAnchor; } /** * Convert a local km position to system-scene display coordinates. * System scene coordinate system: star at origin, all positions scaled by * DISPLAY_UNITS_PER_KILOMETER * ACTIVE_SYSTEM_DETAIL_SCALE. */ export function toDisplayLocalPosition(localPosition: THREE.Vector3): THREE.Vector3 { return localPosition.clone().multiplyScalar(DISPLAY_UNITS_PER_KILOMETER * ACTIVE_SYSTEM_DETAIL_SCALE); }