/** * Lane C selftests — wind field, storm timelines, rain, debris. * * The asserts live in weather.selftest.js as a plain case list, because they * import nothing but weather.core.js (no THREE, no DOM) and so also run under * `node web/world/js/tests/run-node.mjs` — a one-second loop for tuning a storm * curve, instead of a browser round trip. This file is the browser half: it * feeds them the fetched storms and adds the checks that need the real * weather.js adapter (contract shape, THREE.Vector3 out). * * Lane A: a.test.js's 'gust telegraph always gives at least 1.2 s of warning' * is lifted onto the real wind below, per your note — the stub's claim to it is * now redundant and yours to drop whenever suits. Logged in THREADS. */ import * as THREE from '../../vendor/three.module.js'; import { assert, fixedLoop } from '../testkit.js'; import { FIXED_DT, checkContract } from '../contracts.js'; import { loadStorm, createWind } from '../weather.js'; import { weatherCases } from './weather.selftest.js'; const STORMS = ['storm_01_gentle', 'storm_02_wildnight']; /** @param {import('../testkit.js').Suite} t */ export default async function run(t) { const storms = {}; for (const name of STORMS) storms[name] = await loadStorm(name); // --- the shared case list (also runs headless in node) --- const { cases } = weatherCases(storms); for (const c of cases) t.test(c.name, c.fn); // --- the bits that need the THREE adapter, not just the core --- t.test('weather.js satisfies the wind contract', () => { const wind = createWind(storms.storm_02_wildnight); const problems = checkContract('wind', wind); assert(problems.length === 0, problems.join('; ')); }); t.test('sample() returns a Vector3 and honours an out param', () => { const wind = createWind(storms.storm_02_wildnight); const pos = new THREE.Vector3(3, 0, -2); const a = wind.sample(pos, 12.5); assert(a instanceof THREE.Vector3, 'sample did not return a THREE.Vector3'); assert(a.y === 0, `wind should be horizontal, got y=${a.y}`); // out param must not change the answer, only where it lands const out = new THREE.Vector3(); const b = wind.sample(pos, 12.5, out); assert(b === out, 'out param was ignored'); assert(a.x === b.x && a.z === b.z, 'out param changed the result'); }); // Lifted from a.test.js onto the real wind (Lane A's note in this file's // header). This is the promise the whole storm rests on: the player can // always react. Deliberately walks the public surface, not the core. t.test('gust telegraph always gives at least 1.2 s of warning', () => { const wind = createWind(storms.storm_02_wildnight); let had = false, edges = 0; fixedLoop(wind.duration, FIXED_DT, (dt, time) => { const tel = wind.gustTelegraph(time); if (tel && !had) { edges++; assert(tel.eta >= 1.2, `telegraph appeared with only ${tel.eta.toFixed(2)}s of warning`); assert(Number.isFinite(tel.power) && tel.power > 0, 'telegraph carried no power'); assert(Number.isFinite(tel.dir), 'telegraph carried no direction'); } had = !!tel; }); assert(edges >= 5, `only ${edges} gusts telegraphed in a ${wind.duration}s storm — too quiet to test`); }); t.test('every storm in data/storms/ loads and validates', () => { // loadStorm throws on invalid, so reaching here with all of them is the pass assert(Object.keys(storms).length === STORMS.length, 'a storm failed to load'); for (const [name, def] of Object.entries(storms)) { assert(def.duration > 0, `${name} has no duration`); assert(Array.isArray(def.baseCurve), `${name} has no baseCurve`); } }); }