325 lines
9.5 KiB
TypeScript
325 lines
9.5 KiB
TypeScript
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<string, NodeVisual>;
|
|
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<string, NodeVisual>;
|
|
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<string>,
|
|
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);
|
|
}
|