import * as THREE from "three"; import { MOON_RENDER_SCALE } from "./viewerConstants"; import type { ShipSnapshot, PlanetSnapshot, MoonSnapshot, Vector3Dto, WorldSnapshot, } from "./contracts"; import type { OrbitalAnchor, WorldState, PovLevel, } from "./viewerTypes"; import type { ZoomBlend } from "./viewerConstants"; export const KILOMETERS_PER_AU = 149_597_870.7; export const METERS_PER_KILOMETER = 1000; export const DISPLAY_UNITS_PER_KILOMETER = 0.0000015; export const DISPLAY_UNITS_PER_LIGHT_YEAR = 2600; export function formatInventory(entries: { itemId: string; amount: number }[]): string { if (entries.length === 0) { return "empty"; } return entries .map((entry) => `${entry.itemId} ${entry.amount.toFixed(0)}`) .join("
"); } export function inventoryAmount(entries: { itemId: string; amount: number }[], itemId: string): number { return entries.find((entry) => entry.itemId === itemId)?.amount ?? 0; } export function formatVector(vector: Vector3Dto): string { return `${vector.x.toFixed(1)}, ${vector.y.toFixed(1)}, ${vector.z.toFixed(1)}`; } function formatNumber(value: number, fractionDigits: number) { return new Intl.NumberFormat("en-US", { minimumFractionDigits: fractionDigits, maximumFractionDigits: fractionDigits, }).format(value); } export function formatLocalDistance(value: number): string { return `${formatNumber(value, value >= 100 ? 0 : 1)} m`; } export function formatSystemDistance(value: number): string { return `${formatNumber(value, 2)} AU`; } export function formatGalaxyDistance(value: number): string { return `${formatNumber(value, 2)} ly`; } export function formatAdaptiveDistanceFromKilometers(kilometers: number): string { const absoluteKilometers = Math.max(0, kilometers); const meters = absoluteKilometers * 1000; const astronomicalUnits = absoluteKilometers / KILOMETERS_PER_AU; const lightYears = absoluteKilometers / (KILOMETERS_PER_AU * 63_241.077); if (lightYears >= 0.1) { return `${formatNumber(lightYears, 2)} ly`; } if (astronomicalUnits >= 0.1) { return `${formatNumber(astronomicalUnits, astronomicalUnits >= 10 ? 1 : 3)} AU`; } if (absoluteKilometers >= 1) { return `${formatNumber(absoluteKilometers, absoluteKilometers >= 100 ? 0 : 2)} km`; } return `${formatNumber(meters, meters >= 100 ? 0 : 1)} m`; } export function formatAdaptiveDistanceFromMeters(meters: number): string { const absoluteMeters = Math.max(0, meters); if (absoluteMeters >= METERS_PER_KILOMETER) { const kilometers = absoluteMeters / METERS_PER_KILOMETER; return `${formatNumber(kilometers, kilometers >= 100 ? 0 : 2)} km`; } return `${formatNumber(absoluteMeters, absoluteMeters >= 100 ? 0 : 1)} m`; } export function formatShipSpeed(ship: ShipSnapshot): string { const speed = Math.max(0, ship.travelSpeed); const unit = ship.travelSpeedUnit; if (unit === "ly/s") { return `${formatNumber(speed, 3)} ly/s`; } if (unit === "AU/s") { return `${formatNumber(speed, 4)} AU/s`; } if (speed >= 1000) { return `${formatNumber(speed / 1000, 2)} km/s`; } return `${formatNumber(speed, 0)} m/s`; } export function formatBytes(bytes: number): string { if (bytes >= 1024 * 1024) { return `${(bytes / 1024 / 1024).toFixed(2)} MB`; } if (bytes >= 1024) { return `${(bytes / 1024).toFixed(1)} KB`; } return `${Math.round(bytes)} B`; } export function smoothBand(value: number, start: number, end: number): number { const t = THREE.MathUtils.clamp((value - start) / Math.max(end - start, 1), 0, 1); return t * t * (3 - (2 * t)); } export function computeZoomBlend(distance: number): ZoomBlend { const localToSystem = smoothBand(distance, 120, 650); const systemToUniverse = smoothBand(distance, 9000, 22000); return { localWeight: 1 - localToSystem, systemWeight: Math.min(localToSystem, 1 - systemToUniverse), galaxyWeight: systemToUniverse, }; } export function classifyPovLevel(distance: number): PovLevel { const blend = computeZoomBlend(distance); if (blend.localWeight >= blend.systemWeight && blend.localWeight >= blend.galaxyWeight) { return "local"; } if (blend.systemWeight >= blend.galaxyWeight) { return "system"; } return "galaxy"; } export function toThreeVector(vector: Vector3Dto): THREE.Vector3 { return new THREE.Vector3(vector.x, vector.y, vector.z); } export function scaleGalaxyScalar(lightYears: number): number { return lightYears * DISPLAY_UNITS_PER_LIGHT_YEAR; } export function scaleLocalScalar(kilometers: number): number { return kilometers * DISPLAY_UNITS_PER_KILOMETER; } export function scaleGalaxyVector(vector: THREE.Vector3): THREE.Vector3 { return vector.clone().multiplyScalar(DISPLAY_UNITS_PER_LIGHT_YEAR); } export function toDisplayGalaxyVector(vector: Vector3Dto): THREE.Vector3 { return scaleGalaxyVector(toThreeVector(vector)); } export function scaleLocalVector(vector: THREE.Vector3): THREE.Vector3 { return vector.clone().multiplyScalar(DISPLAY_UNITS_PER_KILOMETER); } export function currentWorldTimeSeconds(world: WorldState | undefined, worldTimeSyncMs: number): number { if (!world) { return 0; } const elapsedMs = performance.now() - worldTimeSyncMs; return world.orbitalTimeSeconds + ((elapsedMs / 1000) * world.orbitalSimulation.simulatedSecondsPerRealSecond); } export function hashUnit(seed: number, value: string): number { let hash = seed; for (let index = 0; index < value.length; index += 1) { hash = ((hash << 5) - hash) + value.charCodeAt(index); hash |= 0; } return (hash >>> 0) / 0xffffffff; } export function computePlanetLocalPosition(planet: PlanetSnapshot, timeSeconds: number, phaseOverrideDegrees?: number): THREE.Vector3 { const eccentricity = THREE.MathUtils.clamp(planet.orbitEccentricity, 0, 0.85); const meanAnomaly = THREE.MathUtils.degToRad(phaseOverrideDegrees ?? planet.orbitPhaseAtEpoch) + (timeSeconds * planet.orbitSpeed); const eccentricAnomaly = meanAnomaly + (eccentricity * Math.sin(meanAnomaly)) + (0.5 * eccentricity * eccentricity * Math.sin(2 * meanAnomaly)); const semiMajorAxis = planet.orbitRadius * KILOMETERS_PER_AU; const semiMinorAxis = semiMajorAxis * Math.sqrt(Math.max(1 - (eccentricity * eccentricity), 0.05)); const local = new THREE.Vector3( semiMajorAxis * (Math.cos(eccentricAnomaly) - eccentricity), 0, semiMinorAxis * Math.sin(eccentricAnomaly), ); local.applyAxisAngle(new THREE.Vector3(0, 1, 0), THREE.MathUtils.degToRad(planet.orbitArgumentOfPeriapsis)); local.applyAxisAngle(new THREE.Vector3(1, 0, 0), THREE.MathUtils.degToRad(planet.orbitInclination)); local.applyAxisAngle(new THREE.Vector3(0, 1, 0), THREE.MathUtils.degToRad(planet.orbitLongitudeOfAscendingNode)); return local; } export function computeMoonLocalPosition(moon: MoonSnapshot, timeSeconds: number): THREE.Vector3 { const angle = THREE.MathUtils.degToRad(moon.orbitPhaseAtEpoch) + (timeSeconds * moon.orbitSpeed); const local = new THREE.Vector3( Math.cos(angle) * moon.orbitRadius, 0, Math.sin(angle) * moon.orbitRadius, ); local.applyAxisAngle(new THREE.Vector3(1, 0, 0), THREE.MathUtils.degToRad(moon.orbitInclination)); local.applyAxisAngle(new THREE.Vector3(0, 1, 0), THREE.MathUtils.degToRad(moon.orbitLongitudeOfAscendingNode)); return local; } export function celestialRenderRadius(size: number, scale: number, minRadius: number, exponent = 1): number { return Math.max(minRadius, Math.pow(Math.max(size, 0.1), exponent) * scale); } export function computeMoonRenderRadius(moon: MoonSnapshot): number { return celestialRenderRadius(moon.size, 0.00011, 0.025, 0.62); } export function starHaloOpacity(starKind: string): number { if (starKind.includes("neutron")) { return 0.22; } if (starKind.includes("white-dwarf")) { return 0.18; } if (starKind.includes("brown-dwarf")) { return 0.1; } return 0.14; } export function resolveOrbitalAnchorPosition( world: WorldState | undefined, systemId: string, anchor: OrbitalAnchor, timeSeconds: number, ): THREE.Vector3 { if (!world || anchor.kind === "star") { return new THREE.Vector3(); } const system = world.systems.get(systemId); const planet = system?.planets[anchor.planetIndex]; if (!system || !planet) { return new THREE.Vector3(); } const planetPosition = computePlanetLocalPosition(planet, timeSeconds); if (anchor.kind === "planet") { return planetPosition; } const moon = planet.moons[anchor.moonIndex]; return planetPosition.add(computeMoonLocalPosition(moon, timeSeconds)); }