Refactor simulation and viewer architecture
This commit is contained in:
193
apps/viewer/src/viewerMath.ts
Normal file
193
apps/viewer/src/viewerMath.ts
Normal 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));
|
||||
}
|
||||
Reference in New Issue
Block a user