262 lines
8.5 KiB
TypeScript
262 lines
8.5 KiB
TypeScript
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("<br>");
|
|
}
|
|
|
|
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));
|
|
}
|