godseed / web /planet /util.js
AndresCarreon's picture
Town Mode: close town view by default, build_district + place_road, bank/market/house, grow-one-town steering, behold-the-world reveal, tamed needle
b0d758d verified
Raw
History Blame Contribute Delete
4.44 kB
// 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 };
}