Lane C S13 gate 2.3 + D's sliver: ambient leaves from 30 km/h; hail pool hides at zero
Leaves: a pooled handful (cap 7) streaming with the wind from 8.3 m/s — the same number D keys the player's lean on, so the yard and the body start telling one story at one speed. Count rides a 2.5 s speed EMA so it doesn't strobe; own seeded rng (seed ^ 0x1eaf) so the pool never shifts the crate sequence; bit-identical under identical (dt, t) streams (asserted). debris.clear() rewinds the layer. dev_skyfx flies the debris layer now and warms it to the scrub point, so ?t=50 shows the leaves t=50 would have. D's aftermath sliver, exactly as diagnosed on lane/d: the hail InstancedMesh at count:0 with frustum culling and depthWrite off still draws instance 0's identity matrix — a 5 cm always-on-top speck at the origin. mesh.visible now gates on n > 0, sky.hail joins the public fx object, and a whole-storm assert demands hidden-at-zero / shown-when-falling in both directions (gated on floor(1300 x intensity) >= 1: a burst's first frames honestly draw nothing). THREADS: gate-2 wrap entry — D's judging unblocked, A's M key live. Selftest 340/0/0 (+2 tests). Mutation-checked: dropped stepLeaves, a 3 m/s threshold, and an always-visible hail pool all go red. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
c2a19df7d5
commit
b70699189c
56
THREADS.md
56
THREADS.md
@ -4677,3 +4677,59 @@ anchors are your GLB), but the tooling is now waiting, not TODO.
|
|||||||
· A: the separation-target proposal above stands as my formal gate-1.4 proposal — wild night,
|
· A: the separation-target proposal above stands as my formal gate-1.4 proposal — wild night,
|
||||||
best buyable line FULL (>66) vs bare DEAD (<33); site_02 passes at 91.5/35.7, backyard_01
|
best buyable line FULL (>66) vs bare DEAD (<33); site_02 passes at 91.5/35.7, backyard_01
|
||||||
fails it for anchor-placement reasons that are yours. Gate 2 starts now, this session.
|
fails it for anchor-placement reasons that are yours. Gate 2 starts now, this session.
|
||||||
|
|
||||||
|
[C] 2026-07-18 — 🌩 **GATE 2 LANDED (storm grade · wall edges · leaves) + A's M-MUTE + D's SLIVER.
|
||||||
|
D: your gate-2 judging is UNBLOCKED — pull lane/c and fly the wild night.** Four commits, each
|
||||||
|
green and mutation-checked; selftest **340/0/0** (was 335: +1 mute, +1 grade, +1 wall, +1 leaves,
|
||||||
|
+1 sliver).
|
||||||
|
|
||||||
|
**2.1 The storm grade — and why the QA's noon-blue sky was arithmetic, not a missing feature.**
|
||||||
|
The dimming EXISTED; it multiplied the weather by the author's palette. `darkness` is 0.5 on the
|
||||||
|
southerly, so 65 km/h at full rain moved the sky 0.35 toward slate and left the sun at ~70% —
|
||||||
|
the author's dial could zero the weather's say. The grade floors it:
|
||||||
|
`stormGrade = storminess × lerp(0.5, 1, darkness)` — full blow now goes at least half way to
|
||||||
|
slate on ANY palette, and the wild night keeps its authored black (0.94 → 0.97, unchanged to the
|
||||||
|
eye). Two things nothing keyed at all, now keyed: the sun DISC lerps toward slate (loses its
|
||||||
|
noon warmth), and `sun.shadow.radius` lerps 1→7 on storminess — the renderer is PCFSoft, so
|
||||||
|
that is real penumbra, not a no-op; the razor midday shadows die with the noon edge. Pure in t,
|
||||||
|
same curve the rain uses, exposed as `sky.stormGrade` above the render gate. dispose() hands
|
||||||
|
back intensity, colour AND radius exactly (the restore test pins all three). Verified by eye on
|
||||||
|
dev_skyfx: southerly t=55 at 18.5 m/s reads WEATHER.
|
||||||
|
|
||||||
|
**2.2 The wall — both QA sightings were geometry, both reproduced before touching anything.**
|
||||||
|
The floating slab: the band stopped AT the camera's horizontal plane, but the ground's true
|
||||||
|
horizon sits ~0.6° below it (the dip), so a bright sliver of sky showed under the base. The
|
||||||
|
band now overshoots to ~12° below the equator; the ground occludes the overshoot by depth, and
|
||||||
|
it survives scale.y's 0.3 rise floor (−37 m compressed to −11 m is still −3.7°, under the dip
|
||||||
|
from any eye height that matters). The seam: sin^0.75 left the arc's outer tenth at ~0.36
|
||||||
|
alpha; now sin^1.6 with the arc widened 2.2→2.6 rad — same solid core, ends under 0.12. Both
|
||||||
|
pinned in numbers (bbox min.y < −20; end-alpha < 0.2). **D — judge from IN-YARD eye height,
|
||||||
|
that's where both bugs lived and where I re-checked.**
|
||||||
|
|
||||||
|
**2.3 Leaves — the ambient tell.** A pooled handful (cap 7) streaming with the wind, keyed on a
|
||||||
|
2.5 s speed EMA so the count doesn't strobe. **Threshold 8.3 m/s = 30 km/h, deliberately YOUR
|
||||||
|
lean number, D** — when the man leans, the leaves fly; the yard and the body tell one story.
|
||||||
|
Deterministic: own seeded rng (`seed ^ 0x1eaf`) so the pool never shifts the crate rng
|
||||||
|
sequence; same (dt,t) stream in, bit-identical leaves out (asserted). debris.clear() rewinds
|
||||||
|
the layer. dev_skyfx now flies the debris layer too and warms it to the scrub point (leaves
|
||||||
|
carry ~8 s of state; a viewer spawning them cold at ?t=50 lies about mid-storm) — wildnight
|
||||||
|
t=50 reads `leaves 7` with tan flecks driving through the rain.
|
||||||
|
|
||||||
|
**D's two handoffs to me, closed:** the aftermath ghost-white sliver was exactly your diagnosis
|
||||||
|
— hail pool at count:0, frustumCulled off, depthWrite off, instance-0 identity matrix drawing a
|
||||||
|
5 cm always-on-top speck at the origin. Took your cheap fix (`mesh.visible = n > 0`) with your
|
||||||
|
name on the comment, plus a whole-storm assert that demands invisible-when-0/visible-when-
|
||||||
|
falling both directions (found a real subtlety on the way: a burst's first frames honestly draw
|
||||||
|
zero stones — floor(1300×intensity) — so the assert gates on ≥1 stone, not >0 intensity). And
|
||||||
|
`sky.hail` is now on the public fx object like `rain` always was, for your judging.
|
||||||
|
|
||||||
|
**A — your M key is live.** `sky.setMute(on)` + `sky.muted`, one flag + a gain write; the flag
|
||||||
|
is the point — M pressed on your splash BEFORE the first gesture builds the audio graph is
|
||||||
|
remembered and honoured by unlock() (that path is the asserted one, plus `audio.masterGain`
|
||||||
|
reads the real bus so the test can't be lied to by the flag). Your feature-detect should light
|
||||||
|
the key the moment you pull lane/c.
|
||||||
|
|
||||||
|
**Still open on my plate, none blocking:** nothing from gates 1–2. B: sweep2's cover-AND-hold
|
||||||
|
table is waiting for your audit rewrite whenever A's garden export lands; my gate-1 postscript
|
||||||
|
upstream has the label-permutation warning if you lift per-corner numbers from any bench —
|
||||||
|
print `corners[k].anchorId`, never the input order.
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import * as THREE from './vendor/three.module.js';
|
|||||||
import { FIXED_DT } from './js/contracts.js';
|
import { FIXED_DT } from './js/contracts.js';
|
||||||
import { loadStorm, createWind } from './js/weather.js';
|
import { loadStorm, createWind } from './js/weather.js';
|
||||||
import { createSkyFx } from './js/skyfx.js';
|
import { createSkyFx } from './js/skyfx.js';
|
||||||
|
import { createDebris } from './js/debris.js';
|
||||||
|
|
||||||
const q = new URLSearchParams(location.search);
|
const q = new URLSearchParams(location.search);
|
||||||
const stormName = q.get('storm') || 'storm_03b_earlybuster';
|
const stormName = q.get('storm') || 'storm_03b_earlybuster';
|
||||||
@ -65,9 +66,16 @@ slab.position.set(0, 1.5, -11);
|
|||||||
scene.add(slab);
|
scene.add(slab);
|
||||||
|
|
||||||
const sky = createSkyFx({ scene, camera, wind, sun, hemi });
|
const sky = createSkyFx({ scene, camera, wind, sun, hemi });
|
||||||
|
// gate 2.3: the viewer flies the ambient-leaf layer too — the sky stack and
|
||||||
|
// the yard's tell belong in the same eyeball test
|
||||||
|
const debris = createDebris({ wind, scene });
|
||||||
addEventListener('pointerdown', () => sky.unlockAudio(), { once: false });
|
addEventListener('pointerdown', () => sky.unlockAudio(), { once: false });
|
||||||
|
|
||||||
let t = Math.max(0, +q.get('t') || 0), paused = false, last = performance.now(), acc = 0;
|
let t = Math.max(0, +q.get('t') || 0), paused = false, last = performance.now(), acc = 0;
|
||||||
|
// Warm the ambient layer to the scrub point: leaves carry a few seconds of
|
||||||
|
// state (speed EMA + travel), so a viewer that spawns them cold at ?t=50
|
||||||
|
// shows an empty sky the real t=50 would not.
|
||||||
|
for (let wt = Math.max(0, t - 8); wt < t; wt += FIXED_DT) debris.step(FIXED_DT, wt, {});
|
||||||
addEventListener('keydown', (e) => {
|
addEventListener('keydown', (e) => {
|
||||||
if (e.code === 'Space') paused = !paused;
|
if (e.code === 'Space') paused = !paused;
|
||||||
if (e.key === ']') t = Math.min(def.duration, t + 5);
|
if (e.key === ']') t = Math.min(def.duration, t + 5);
|
||||||
@ -86,13 +94,13 @@ function frame(now) {
|
|||||||
const wall = Math.min(0.1, (now - last) / 1000); last = now;
|
const wall = Math.min(0.1, (now - last) / 1000); last = now;
|
||||||
if (!paused && t < def.duration) {
|
if (!paused && t < def.duration) {
|
||||||
acc += wall;
|
acc += wall;
|
||||||
while (acc >= FIXED_DT) { acc -= FIXED_DT; t += FIXED_DT; sky.step(FIXED_DT, t, {}); }
|
while (acc >= FIXED_DT) { acc -= FIXED_DT; t += FIXED_DT; sky.step(FIXED_DT, t, {}); debris.step(FIXED_DT, t, {}); }
|
||||||
}
|
}
|
||||||
camera.rotation.set(0, yaw, 0, 'YXZ');
|
camera.rotation.set(0, yaw, 0, 'YXZ');
|
||||||
const ch = (def.events || []).find((e) => e.type === 'windchange');
|
const ch = (def.events || []).find((e) => e.type === 'windchange');
|
||||||
hud.textContent = `${stormName} t=${t.toFixed(1)}s / ${def.duration}s${paused ? ' ⏸' : ''}\n`
|
hud.textContent = `${stormName} t=${t.toFixed(1)}s / ${def.duration}s${paused ? ' ⏸' : ''}\n`
|
||||||
+ `wind ${wind.speedAt(camera.position, t).toFixed(1)} m/s rain ${wind.rainAt(t).toFixed(2)} `
|
+ `wind ${wind.speedAt(camera.position, t).toFixed(1)} m/s rain ${wind.rainAt(t).toFixed(2)} `
|
||||||
+ `front ${sky.changeFront.toFixed(2)}${ch ? ` (change at t=${ch.t})` : ' (no change)'}\n`
|
+ `front ${sky.changeFront.toFixed(2)} leaves ${debris.leafCount}${ch ? ` (change at t=${ch.t})` : ' (no change)'}\n`
|
||||||
+ `space pause · [ ] scrub · ←→ turn · click for audio`;
|
+ `space pause · [ ] scrub · ←→ turn · click for audio`;
|
||||||
renderer.render(scene, camera);
|
renderer.render(scene, camera);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,6 +52,66 @@ export function createDebris(o = {}) {
|
|||||||
const w = new THREE.Vector3();
|
const w = new THREE.Vector3();
|
||||||
const probe = new THREE.Vector3();
|
const probe = new THREE.Vector3();
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- leaves
|
||||||
|
// SPRINT13 gate 2.3 — ambient leaves. QA: "debris reads 0 through night 3"
|
||||||
|
// — the event-driven crates are the drama, but nothing SELLS the gale
|
||||||
|
// between events. A handful of leaves streaming with the wind does, from
|
||||||
|
// 30 km/h up. Numbers that matter: LEAF_START is 8.3 m/s on purpose — the
|
||||||
|
// same threshold D keys the player's lean on (their table, lane/d), so the
|
||||||
|
// yard and the body start telling the same story at the same speed. Count
|
||||||
|
// stays single digits (cap 7); these are a tell, not a particle system.
|
||||||
|
//
|
||||||
|
// Deterministic: own seeded rng (so the leaf pool never shifts the piece
|
||||||
|
// rng sequence), distance accumulated over the fixed-dt stream, flutter
|
||||||
|
// pure in t. Same (dt, t) stream in, same leaves out.
|
||||||
|
const LEAF_START = 8.3; // m/s = 30 km/h; matches D's lean threshold
|
||||||
|
const LEAF_MAX = 7; // "a handful" — single digits, capped
|
||||||
|
const LEAF_SPAN = 26; // m of downwind run before a leaf recycles
|
||||||
|
const lrand = rng(((wind && wind.seed) || 1) ^ 0x1eaf);
|
||||||
|
const leafGeo = new THREE.PlaneGeometry(0.16, 0.09);
|
||||||
|
const leafMat = new THREE.MeshLambertMaterial({ color: 0x8a7a3f, side: THREE.DoubleSide });
|
||||||
|
const leafMeshes = [];
|
||||||
|
const leafSeed = [];
|
||||||
|
for (let i = 0; i < LEAF_MAX; i++) {
|
||||||
|
const m = new THREE.Mesh(leafGeo, leafMat);
|
||||||
|
m.visible = false;
|
||||||
|
if (scene) scene.add(m);
|
||||||
|
leafMeshes.push(m);
|
||||||
|
leafSeed.push({
|
||||||
|
lat: lrand() * 16 - 8, // lane across the wind, m
|
||||||
|
base: 0.35 + lrand() * 1.5, // ride height, m
|
||||||
|
off: lrand() * LEAF_SPAN, // where on the loop it starts
|
||||||
|
fl: 1.6 + lrand() * 1.6, // flutter frequency
|
||||||
|
ph: lrand() * 6.283,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let leafDist = 0; // m travelled downwind, accumulated over the dt stream
|
||||||
|
let leafEma = 0; // ~2.5 s smoothed speed, so the count doesn't strobe
|
||||||
|
|
||||||
|
function stepLeaves(dt, t) {
|
||||||
|
probe.set(0, 1.6, 0);
|
||||||
|
wind.sample(probe, t, w);
|
||||||
|
const sp = Math.hypot(w.x, w.z);
|
||||||
|
leafEma += (sp - leafEma) * Math.min(1, dt / 2.5);
|
||||||
|
const n = leafEma < LEAF_START ? 0
|
||||||
|
: Math.min(LEAF_MAX, 1 + Math.floor((leafEma - LEAF_START) / 1.5));
|
||||||
|
leafDist += sp * 0.85 * dt; // leaves ride a little under the wind
|
||||||
|
const inv = sp > 1e-4 ? 1 / sp : 0;
|
||||||
|
const dx = w.x * inv, dz = w.z * inv;
|
||||||
|
for (let i = 0; i < LEAF_MAX; i++) {
|
||||||
|
const m = leafMeshes[i], s = leafSeed[i];
|
||||||
|
const vis = i < n && inv > 0;
|
||||||
|
m.visible = vis;
|
||||||
|
if (!vis) continue;
|
||||||
|
const along = ((leafDist + s.off + i * (LEAF_SPAN / LEAF_MAX)) % LEAF_SPAN) - LEAF_SPAN / 2;
|
||||||
|
const lat = s.lat + Math.sin(t * 0.9 + s.ph) * 1.1;
|
||||||
|
const x = dx * along - dz * lat;
|
||||||
|
const z = dz * along + dx * lat;
|
||||||
|
m.position.set(x, groundAt(x, z) + s.base + Math.sin(t * s.fl + s.ph) * 0.3, z);
|
||||||
|
m.rotation.set(t * s.fl, s.ph + t * 2.1, t * 1.3 + s.ph);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Graybox stand-in so a missing GLB can't break Lane A's merge. */
|
/** Graybox stand-in so a missing GLB can't break Lane A's merge. */
|
||||||
function placeholder(spec) {
|
function placeholder(spec) {
|
||||||
const g = new THREE.BoxGeometry(spec.r * 1.8, spec.r * 1.8, spec.r * 1.8);
|
const g = new THREE.BoxGeometry(spec.r * 1.8, spec.r * 1.8, spec.r * 1.8);
|
||||||
@ -108,6 +168,11 @@ export function createDebris(o = {}) {
|
|||||||
const debris = {
|
const debris = {
|
||||||
get pieces() { return pieces; },
|
get pieces() { return pieces; },
|
||||||
|
|
||||||
|
/** How many ambient leaves are flying right now (gate 2.3). 0 in a calm. */
|
||||||
|
get leafCount() { let n = 0; for (const m of leafMeshes) if (m.visible) n++; return n; },
|
||||||
|
/** Positions of the flying leaves — for asserts and D's judging. */
|
||||||
|
get leaves() { return leafMeshes.filter((m) => m.visible).map((m) => m.position); },
|
||||||
|
|
||||||
/** Lane E's GLBs, once they land. name -> Object3D template. */
|
/** Lane E's GLBs, once they land. name -> Object3D template. */
|
||||||
setModels(map) { Object.assign(models, map); return debris; },
|
setModels(map) { Object.assign(models, map); return debris; },
|
||||||
|
|
||||||
@ -125,6 +190,7 @@ export function createDebris(o = {}) {
|
|||||||
for (const ev of wind.eventsBetween(t - dt, t)) {
|
for (const ev of wind.eventsBetween(t - dt, t)) {
|
||||||
if (ev.type === 'debris') spawn(ev, t);
|
if (ev.type === 'debris') spawn(ev, t);
|
||||||
}
|
}
|
||||||
|
stepLeaves(dt, t); // gate 2.3 — the ambient tell
|
||||||
}
|
}
|
||||||
|
|
||||||
const player = world.player;
|
const player = world.player;
|
||||||
@ -219,6 +285,10 @@ export function createDebris(o = {}) {
|
|||||||
clear() {
|
clear() {
|
||||||
for (const p of pieces) despawn(p);
|
for (const p of pieces) despawn(p);
|
||||||
pieces.length = 0;
|
pieces.length = 0;
|
||||||
|
// leaves are a pooled ambient layer, not spawned pieces: hide and rewind
|
||||||
|
// so the next night's stream starts from the same state every time
|
||||||
|
for (const m of leafMeshes) m.visible = false;
|
||||||
|
leafDist = 0; leafEma = 0;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -191,6 +191,11 @@ function createHail(opts) {
|
|||||||
step(dt, camPos, vel, intensity, size, shadow) {
|
step(dt, camPos, vel, intensity, size, shadow) {
|
||||||
const n = Math.floor(max * clamp01(intensity));
|
const n = Math.floor(max * clamp01(intensity));
|
||||||
mesh.count = n;
|
mesh.count = n;
|
||||||
|
// D's aftermath find (S13): count:0 with frustumCulled off and depthWrite
|
||||||
|
// off still draws instance 0's identity matrix — a ~5 cm always-on-top
|
||||||
|
// white sliver at the world origin, QA's "ghost near the shed table".
|
||||||
|
// visible is the honest gate; count alone is not.
|
||||||
|
mesh.visible = n > 0;
|
||||||
if (n === 0) return;
|
if (n === 0) return;
|
||||||
const s = 0.6 + size * 0.9; // bigger stones read bigger
|
const s = 0.6 + size * 0.9; // bigger stones read bigger
|
||||||
m.makeScale(s, s, s);
|
m.makeScale(s, s, s);
|
||||||
@ -744,7 +749,7 @@ export function createSkyFx(o = {}) {
|
|||||||
const w = new THREE.Vector3();
|
const w = new THREE.Vector3();
|
||||||
|
|
||||||
const fx = {
|
const fx = {
|
||||||
rain, audio, dome, shadow, hailShadow, front: frontMesh,
|
rain, hail, audio, dome, shadow, hailShadow, front: frontMesh,
|
||||||
get flash() { return flash; },
|
get flash() { return flash; },
|
||||||
/** 0..1 hail intensity right now — for the HUD ("HAIL" banner) and asserts. */
|
/** 0..1 hail intensity right now — for the HUD ("HAIL" banner) and asserts. */
|
||||||
get hailAmount() { return hailAmt; },
|
get hailAmount() { return hailAmt; },
|
||||||
|
|||||||
@ -136,6 +136,80 @@ export default async function run(t) {
|
|||||||
'clear() replaced the pieces array instead of emptying it — B holds a reference');
|
'clear() replaced the pieces array instead of emptying it — B holds a reference');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// SPRINT13 gate 2.3 — ambient leaves. QA: "debris reads 0 through night 3";
|
||||||
|
// the crates are event drama, the leaves are the continuous tell. Threshold
|
||||||
|
// 8.3 m/s (30 km/h) matches D's lean threshold on purpose — the yard and the
|
||||||
|
// body start telling the same story at the same speed. Count stays single
|
||||||
|
// digits, they stream WITH the wind, and the layer is deterministic.
|
||||||
|
t.test('ambient leaves: none in a calm, a streaming handful from ~30 km/h', () => {
|
||||||
|
const stub = (sp) => ({
|
||||||
|
seed: 5,
|
||||||
|
sample: (p, t2, o) => o.set(sp, 0, 0),
|
||||||
|
eventsBetween: () => [],
|
||||||
|
});
|
||||||
|
const calm = createDebris({ wind: stub(5) });
|
||||||
|
fixedLoop(8, FIXED_DT, (dt, time) => calm.step(dt, time, {}));
|
||||||
|
assert(calm.leafCount === 0,
|
||||||
|
`${calm.leafCount} leaves flying at 18 km/h — below the threshold the yard is still`);
|
||||||
|
|
||||||
|
const gale = createDebris({ wind: stub(13) });
|
||||||
|
fixedLoop(8, FIXED_DT, (dt, time) => gale.step(dt, time, {}));
|
||||||
|
assert(gale.leafCount > 0, 'no leaves at 47 km/h — the gale has no ambient tell');
|
||||||
|
assert(gale.leafCount <= 9, `${gale.leafCount} leaves — the plan says single digits`);
|
||||||
|
|
||||||
|
// they stream WITH the wind (+x here): over one step every leaf either
|
||||||
|
// moves downwind or recycles ~a span upwind; none drifts against it
|
||||||
|
const before = gale.leaves.map((pos) => pos.x);
|
||||||
|
gale.step(FIXED_DT, 8, {});
|
||||||
|
const after = gale.leaves.map((pos) => pos.x);
|
||||||
|
assert(before.length === after.length, 'leaf count strobed across one step');
|
||||||
|
for (let i = 0; i < before.length; i++) {
|
||||||
|
const d = after[i] - before[i];
|
||||||
|
assert(d > 0 || d < -10, `leaf ${i} moved ${d.toFixed(3)} m against a +x wind`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// deterministic: same seed, same (dt, t) stream, same leaves — bit for bit
|
||||||
|
const a = createDebris({ wind: stub(13) });
|
||||||
|
const b = createDebris({ wind: stub(13) });
|
||||||
|
fixedLoop(5, FIXED_DT, (dt, time) => { a.step(dt, time, {}); b.step(dt, time, {}); });
|
||||||
|
assert(a.leafCount === b.leafCount && a.leaves.every((pos, i) =>
|
||||||
|
pos.x === b.leaves[i].x && pos.y === b.leaves[i].y && pos.z === b.leaves[i].z),
|
||||||
|
'two identical (dt, t) streams produced different leaves');
|
||||||
|
|
||||||
|
// clear() rewinds the layer — next night starts from the same state
|
||||||
|
gale.clear();
|
||||||
|
assert(gale.leafCount === 0, 'clear() left leaves flying into the aftermath card');
|
||||||
|
});
|
||||||
|
|
||||||
|
// D's aftermath find (S13, lane/d): the hail InstancedMesh at count:0 with
|
||||||
|
// frustum culling and depthWrite both off still draws instance 0's identity
|
||||||
|
// matrix — a ~5 cm always-on-top white sliver at the origin, the QA's "ghost
|
||||||
|
// near the shed table". The gate is mesh.visible, not count. This flies the
|
||||||
|
// wild night and demands the pool is invisible whenever no hail is falling
|
||||||
|
// and visible whenever it is — both directions, whole storm.
|
||||||
|
t.test("hail pool: invisible when nothing falls (D's aftermath sliver)", () => {
|
||||||
|
const scene = new THREE.Scene();
|
||||||
|
const camera = new THREE.PerspectiveCamera();
|
||||||
|
const wind = createWind(storms.storm_02_wildnight);
|
||||||
|
const sky = createSkyFx({ scene, camera, wind });
|
||||||
|
let sawHail = false, sawCalm = false;
|
||||||
|
fixedLoop(wind.duration, FIXED_DT, (dt, time) => {
|
||||||
|
sky.step(dt, time, {});
|
||||||
|
// the mesh draws floor(1300 × intensity) stones, so a burst's first
|
||||||
|
// millisecond can honestly show zero — demand visibility only once the
|
||||||
|
// intensity buys at least one stone, and invisibility only at exactly 0
|
||||||
|
if (sky.hailAmount >= 1 / 1300) {
|
||||||
|
sawHail = true;
|
||||||
|
assert(sky.hail.mesh.visible, `hail falling at t=${time.toFixed(1)} but the pool is hidden`);
|
||||||
|
} else if (sky.hailAmount === 0) {
|
||||||
|
sawCalm = true;
|
||||||
|
assert(!sky.hail.mesh.visible, `no hail at t=${time.toFixed(1)} but the pool still draws — the sliver`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assert(sawHail && sawCalm, 'storm never exercised both states — this test proves nothing');
|
||||||
|
sky.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
// Lane A rebuilds skyfx on every phase change, so dispose() is on the hot path.
|
// Lane A rebuilds skyfx on every phase change, so dispose() is on the hot path.
|
||||||
// They verified sun/hemi restore exactly and spotted that fog didn't; this pins
|
// They verified sun/hemi restore exactly and spotted that fog didn't; this pins
|
||||||
// both. The vacuity guards matter — a restore test where nothing ever moved is
|
// both. The vacuity guards matter — a restore test where nothing ever moved is
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user