Files
space-game/apps/viewer/src/viewerMath.ts

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));
}