// GODSEED — shared math + color helpers. import * as THREE from "three"; export const PLANET_R = 2.0; export const DEG = Math.PI / 180; export const clamp = (v, a, b) => Math.min(b, Math.max(a, v)); export const lerp = (a, b, t) => a + (b - a) * t; export const easeOutCubic = (x) => 1 - Math.pow(1 - clamp(x, 0, 1), 3); export const easeInOutCubic = (x) => { x = clamp(x, 0, 1); return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2; }; export const easeOutBack = (x) => { x = clamp(x, 0, 1); const c1 = 1.70158, c3 = c1 + 1; return 1 + c3 * Math.pow(x - 1, 3) + c1 * Math.pow(x - 1, 2); }; /** lat/lon (degrees) → unit direction. Single source of truth for the mapping. */ export function latLonToDir(lat, lon, out = new THREE.Vector3()) { const la = clamp(lat, -90, 90) * DEG; const lo = lon * DEG; return out.set(Math.cos(la) * Math.cos(lo), Math.sin(la), Math.cos(la) * Math.sin(lo)); } export function dirToLatLon(dir) { return { lat: Math.asin(clamp(dir.y, -1, 1)) / DEG, lon: Math.atan2(dir.z, dir.x) / DEG }; } /** Angle (radians) between two unit vectors, numerically safe. */ export function angleBetween(a, b) { return Math.acos(clamp(a.dot(b), -1, 1)); } const _t1 = new THREE.Vector3(); const _t2 = new THREE.Vector3(); const _q = new THREE.Quaternion(); const _Y = new THREE.Vector3(0, 1, 0); /** * Random unit direction inside a spherical cap of `radiusRad` around `center`. * Density falls toward the rim (sqrt disc sampling → uniform-ish over the cap). */ export function randomDirInCap(center, radiusRad, rng, out = new THREE.Vector3()) { const ang = Math.sqrt(rng()) * radiusRad; const rot = rng() * Math.PI * 2; // tangent basis around center _t1.set(0, 1, 0); if (Math.abs(center.y) > 0.93) _t1.set(1, 0, 0); _t2.crossVectors(center, _t1).normalize(); _t1.crossVectors(_t2, center).normalize(); out.copy(center).multiplyScalar(Math.cos(ang)) .addScaledVector(_t2, Math.sin(ang) * Math.cos(rot)) .addScaledVector(_t1, Math.sin(ang) * Math.sin(rot)); return out.normalize(); } /** Quaternion aligning local +Y with `dir`, then spun by `yaw` around it. */ export function uprightQuat(dir, yaw, out = new THREE.Quaternion()) { out.setFromUnitVectors(_Y, dir); _q.setFromAxisAngle(dir, yaw); return out.premultiply(_q); } /** Spherical interpolation between two unit vectors. */ export function slerpDirs(a, b, t, out = new THREE.Vector3()) { const omega = angleBetween(a, b); if (omega < 1e-5) return out.copy(a); const so = Math.sin(omega); return out.copy(a).multiplyScalar(Math.sin((1 - t) * omega) / so) .addScaledVector(b, Math.sin(t * omega) / so); } /** hue (deg, any range) + s,l in [0,1] → THREE.Color. */ export function hsl(h, s, l, out = new THREE.Color()) { return out.setHSL((((h % 360) + 360) % 360) / 360, clamp(s, 0, 1), clamp(l, 0, 1)); } export function ordinal(n) { const s = ["th", "st", "nd", "rd"]; const v = n % 100; return n + (s[(v - 20) % 10] || s[v] || s[0]); } // --- the town anchor (engine + renderer compute IDENTICALLY) ----------------- // The genesis monolith — the fallback heart of the world before any building. export const GENESIS_MONOLITH = { lat: 14, lon: 38 }; /** * townCenter(features) → { dir: THREE.Vector3 (unit), lat, lon }. * * The seeded centroid of the town: the unit-vector mean (renormalized) of the * directions of every non-genesis `place_structure` + `build_district` feature. * This is the heart the camera frames and the HUD names. When no town has been * built yet, it falls back to the genesis monolith at lat 14, lon 38. * * Must match Agent A's engine algorithm exactly — the live shared world and the * client agree on where "the town" is, so the default view is deterministic. */ export function townCenter(features, out = new THREE.Vector3()) { const sum = out.set(0, 0, 0); const d = _t1; let n = 0; for (const f of features || []) { if (!f || f.wish_id === "genesis") continue; if (f.tool !== "place_structure" && f.tool !== "build_district") continue; const a = f.args || {}; if (a.lat == null || a.lon == null) continue; latLonToDir(a.lat, a.lon, d); sum.add(d); n++; } if (n === 0 || sum.lengthSq() < 1e-9) { latLonToDir(GENESIS_MONOLITH.lat, GENESIS_MONOLITH.lon, sum); } else { sum.normalize(); } const { lat, lon } = dirToLatLon(sum); return { dir: sum, lat, lon }; }