HardYards/web/world/dev_skyfx.html
type-two b70699189c 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>
2026-07-18 11:51:26 +10:00

109 lines
4.8 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<!--
dev_skyfx — Lane C's storm viewer. [SPRINT12]
The skyfx bench: one storm, a bare ground plane, and the whole sky stack
(dome, rain, hail, lightning, change front, audio) with a scrubbable clock.
Exists so "does the change ANNOUNCE itself" (gate 3.4) can be judged by eye
in ten seconds instead of playing to night 3 — D, this is for you: load the
early buster, watch the southern sky from t=0, and say whether you'd have
known the change was coming without reading a number.
dev_skyfx.html ← storm_03b_earlybuster
dev_skyfx.html?storm=storm_03_southerly&t=12 ← night 2's slower version
keys: space pause · ] +5 s · [ 5 s (forward-clean; rewinds may re-flash)
←/→ turn · click once to unlock audio
The VIEW runs on rAF (it's a viewer); the sim is stepped at FIXED_DT
accumulated, same as the game, so what you see is what ships.
-->
<meta charset="utf-8">
<title>dev_skyfx</title>
<style>
body { margin:0; background:#000; overflow:hidden; }
#hud { position:fixed; top:10px; left:12px; color:#cde; font:12px/1.5 ui-monospace,Menlo,monospace;
text-shadow:0 1px 2px #000; white-space:pre; pointer-events:none; }
</style>
<div id="hud">loading…</div>
<script type="importmap">
{ "imports": { "three": "./vendor/three.module.js",
"three/addons/": "./vendor/addons/" } }
</script>
<script type="module">
import * as THREE from './vendor/three.module.js';
import { FIXED_DT } from './js/contracts.js';
import { loadStorm, createWind } from './js/weather.js';
import { createSkyFx } from './js/skyfx.js';
import { createDebris } from './js/debris.js';
const q = new URLSearchParams(location.search);
const stormName = q.get('storm') || 'storm_03b_earlybuster';
const def = await loadStorm(stormName);
const wind = createWind(def);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x9fc4e8);
const camera = new THREE.PerspectiveCamera(70, innerWidth / innerHeight, 0.1, 400);
camera.position.set(0, 1.7, -4);
let yaw = Math.PI; // start looking toward +Z — the south
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
const sun = new THREE.DirectionalLight(0xfff4e0, 2.0);
sun.position.set(30, 50, -20);
scene.add(sun);
const hemi = new THREE.HemisphereLight(0xbfd8ff, 0x3a4a2a, 1.2);
scene.add(hemi);
const ground = new THREE.Mesh(
new THREE.CircleGeometry(120, 48).rotateX(-Math.PI / 2),
new THREE.MeshLambertMaterial({ color: 0x4a5d3a }),
);
scene.add(ground);
// a fence-height slab to the north, so there's a yard-ish thing in frame
const slab = new THREE.Mesh(new THREE.BoxGeometry(14, 3, 1), new THREE.MeshLambertMaterial({ color: 0x8a7f70 }));
slab.position.set(0, 1.5, -11);
scene.add(slab);
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 });
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) => {
if (e.code === 'Space') paused = !paused;
if (e.key === ']') t = Math.min(def.duration, t + 5);
if (e.key === '[') t = Math.max(0, t - 5);
if (e.key === 'ArrowLeft') yaw += 0.15;
if (e.key === 'ArrowRight') yaw -= 0.15;
});
addEventListener('resize', () => {
camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
const hud = document.getElementById('hud');
function frame(now) {
requestAnimationFrame(frame);
const wall = Math.min(0.1, (now - last) / 1000); last = now;
if (!paused && t < def.duration) {
acc += wall;
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');
const ch = (def.events || []).find((e) => e.type === 'windchange');
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)} `
+ `front ${sky.changeFront.toFixed(2)} leaves ${debris.leafCount}${ch ? ` (change at t=${ch.t})` : ' (no change)'}\n`
+ `space pause · [ ] scrub · ←→ turn · click for audio`;
renderer.render(scene, camera);
}
requestAnimationFrame(frame);
</script>