import * as THREE from "three"; import { MOON_RENDER_SCALE } from "./viewerConstants"; import type { PlanetSnapshot, Vector3Dto, WorldSnapshot, } from "./contracts"; import type { OrbitalAnchor, WorldState, ZoomLevel, } from "./viewerTypes"; import type { ZoomBlend } from "./viewerConstants"; 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)}`; } 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, 1200, 5200); const systemToUniverse = smoothBand(distance, 9000, 22000); return { localWeight: 1 - localToSystem, systemWeight: Math.min(localToSystem, 1 - systemToUniverse), universeWeight: systemToUniverse, }; } export function classifyZoomLevel(distance: number): ZoomLevel { const blend = computeZoomBlend(distance); if (blend.localWeight >= blend.systemWeight && blend.localWeight >= blend.universeWeight) { return "local"; } if (blend.systemWeight >= blend.universeWeight) { return "system"; } return "universe"; } export function toThreeVector(vector: Vector3Dto): THREE.Vector3 { return new THREE.Vector3(vector.x, vector.y, vector.z); } export function currentWorldTimeSeconds(world: WorldState | undefined, worldTimeSyncMs: number): number { if (!world) { return 0; } const baseUtcMs = Date.parse(world.generatedAtUtc); const elapsedMs = performance.now() - worldTimeSyncMs; return ((baseUtcMs + elapsedMs) / 1000) + (world.seed * 97); } 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; 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 computeMoonOrbitRadius(planet: PlanetSnapshot, moonIndex: number, seed: number): number { const spacing = planet.size * 1.4; const variance = hashUnit(seed, `${planet.label}:${moonIndex}:radius`) * planet.size * 0.9; return (planet.size * 1.8) + (moonIndex * spacing) + variance; } export function computeMoonOrbitSpeed(planet: PlanetSnapshot, moonIndex: number, seed: number): number { const radius = computeMoonOrbitRadius(planet, moonIndex, seed); return 0.9 / Math.sqrt(Math.max(radius, 1)) + (moonIndex * 0.003); } export function computeMoonLocalPosition(planet: PlanetSnapshot, moonIndex: number, timeSeconds: number, seed: number): THREE.Vector3 { const orbitRadius = computeMoonOrbitRadius(planet, moonIndex, seed); const speed = computeMoonOrbitSpeed(planet, moonIndex, seed); const phase = hashUnit(seed, `${planet.label}:${moonIndex}:phase`) * Math.PI * 2; const inclination = THREE.MathUtils.degToRad((hashUnit(seed, `${planet.label}:${moonIndex}:inclination`) - 0.5) * 28); const node = THREE.MathUtils.degToRad(hashUnit(seed, `${planet.label}:${moonIndex}:node`) * 360); const angle = phase + (timeSeconds * speed); const local = new THREE.Vector3( Math.cos(angle) * orbitRadius, 0, Math.sin(angle) * orbitRadius, ); local.applyAxisAngle(new THREE.Vector3(1, 0, 0), inclination); local.applyAxisAngle(new THREE.Vector3(0, 1, 0), node); return local; } export function computeMoonSize(planet: PlanetSnapshot, moonIndex: number, seed: number): number { const base = Math.max(2.2, planet.size * 0.11); const variance = hashUnit(seed, `${planet.label}:${moonIndex}:size`) * Math.max(planet.size * 0.16, 2.5); return Math.min(base + variance, planet.size * 0.42); } 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(planet: PlanetSnapshot, moonIndex: number, seed: number): number { return celestialRenderRadius(computeMoonSize(planet, moonIndex, seed), MOON_RENDER_SCALE, 2.5, 1.04); } 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, seed: 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; } return planetPosition.add(computeMoonLocalPosition(planet, anchor.moonIndex, timeSeconds, seed)); }