Implements the contracts.js wind surface (PLAN3D §4): sample(pos,t) and gustTelegraph(t), plus storm defs as data. The prototype scheduled gusts by integrating (wind.gustT += dt). We can't: sample(pos,t) is called by sail/player/debris/rain at arbitrary t, so gusts are precomputed into a timeline from a seeded PRNG at load and read from t. Envelope shape is a faithful port — telegraph 1.5s / ramp 0.8s / hold 1.7s / fade 1.0s. Maths lives in weather.core.js with zero imports (no THREE, no DOM, no Date.now), so the determinism rule is structural and the suite runs in node without waiting on Lane A's M0. storm_01_gentle peaks at 11 m/s; storm_02_wildnight sustains 20 m/s and gusts to 32 (BOM 'destructive'), with the southerly change landing just before the worst of it — the corners that were slack all storm are the ones that cop it. 15 asserts green: telegraph lead >=1.2s, wind continuity in time and space, storm JSON validator (incl. 14 deliberately-broken defs it must reject), determinism, sample-order independence, tree wind shadow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
99 lines
3.4 KiB
JavaScript
99 lines
3.4 KiB
JavaScript
'use strict';
|
|
// SHADES — Lane C — weather: the wind field everyone samples.
|
|
//
|
|
// Implements the contracts.js wind surface (PLAN3D §4):
|
|
// wind.sample(pos, t) -> Vector3 m/s, includes gusts & local effects
|
|
// wind.gustTelegraph(t) -> {eta, dir, power} | null
|
|
//
|
|
// All the maths lives in weather.core.js (pure, no imports). This file is just
|
|
// the THREE adapter + storm loading, so the sim stays node-testable and the
|
|
// determinism rule can't be broken by accident.
|
|
|
|
import * as THREE from 'three';
|
|
import { createWindField, validateStorm, GUST } from './weather.core.js';
|
|
|
|
export { GUST, validateStorm };
|
|
|
|
const STORM_DIR = '/world/data/storms';
|
|
|
|
/** Fetch + validate a storm def. Throws loud on bad data — storms are content. */
|
|
export async function loadStorm(name, dir = STORM_DIR) {
|
|
const url = `${dir}/${name}.json`;
|
|
const res = await fetch(url);
|
|
if (!res.ok) throw new Error(`weather: cannot load ${url} (${res.status})`);
|
|
const def = await res.json();
|
|
const { ok, errors } = validateStorm(def, name);
|
|
if (!ok) throw new Error(`weather: ${url} is invalid:\n ${errors.join('\n ')}`);
|
|
return def;
|
|
}
|
|
|
|
/**
|
|
* @param {object} def parsed storm JSON
|
|
* @param {object} [opts] {seed} — same seed + same def = same storm, every run
|
|
* @returns the `wind` object from contracts.js
|
|
*/
|
|
export function createWind(def, opts = {}) {
|
|
const field = createWindField(def, opts);
|
|
const scratch = { x: 0, y: 0, z: 0 };
|
|
|
|
const wind = {
|
|
/**
|
|
* Wind velocity at a world position, m/s.
|
|
* @param {THREE.Vector3} pos
|
|
* @param {number} t storm time, seconds
|
|
* @param {THREE.Vector3} [out] pass one to avoid allocating — sail.js
|
|
* samples per-face per-frame, so this matters
|
|
*/
|
|
sample(pos, t, out) {
|
|
const v = out || new THREE.Vector3();
|
|
field.vecAt(pos.x, pos.z, t, scratch);
|
|
return v.set(scratch.x, scratch.y, scratch.z);
|
|
},
|
|
|
|
/** Scalar speed — for HUD, rain, grass. Cheaper than sample(); no allocation. */
|
|
speedAt(pos, t) {
|
|
return field.speedAt(pos.x, pos.z, t);
|
|
},
|
|
|
|
/** {eta, dir, power} while a gust is inbound but hasn't risen yet, else null. */
|
|
gustTelegraph(t) {
|
|
return field.telegraph(t);
|
|
},
|
|
|
|
/**
|
|
* Register wind shadows (trees, house). Lane A: call after the yard is built.
|
|
* Until then there are simply no shadows — nothing breaks.
|
|
* @param {Array<{x,z,radius,strength,length}>} list
|
|
*/
|
|
setShelters(list) { field.setShelters(list); return wind; },
|
|
|
|
/** Convenience: take shadows straight off world.anchors' tree entries. */
|
|
setSheltersFromTrees(trees, o = {}) {
|
|
return wind.setShelters(trees.map((tr) => ({
|
|
x: tr.pos ? tr.pos.x : tr.x,
|
|
z: tr.pos ? tr.pos.z : tr.z,
|
|
radius: o.radius ?? tr.radius ?? 3,
|
|
strength: o.strength ?? 0.45,
|
|
length: o.length ?? 14,
|
|
})));
|
|
},
|
|
|
|
/** Storm events fired in (a,b] — poll with (t-dt, t). Deterministic. */
|
|
eventsBetween(a, b) { return field.eventsBetween(a, b); },
|
|
|
|
/** 0..1 rain intensity. */
|
|
rainAt(t) { return field.rainAt(t); },
|
|
|
|
/** Direction (radians, XZ plane from +X toward +Z) ignoring local effects. */
|
|
dirAt(t) { return field.dirAt(t); },
|
|
|
|
get duration() { return field.duration; },
|
|
get gusts() { return field.gusts; },
|
|
get def() { return field.def; },
|
|
get seed() { return field.seed; },
|
|
core: field,
|
|
};
|
|
|
|
return wind;
|
|
}
|