Refactor simulation and viewer architecture

This commit is contained in:
2026-03-14 15:08:49 -04:00
parent ddca4a16d5
commit 651556c916
71 changed files with 11472 additions and 9031 deletions

View File

@@ -0,0 +1,193 @@
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("<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)}`;
}
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));
}