Fix debris ground friction, cloud seam and yard coords
Three bugs the bench found once it was actually running a storm: - Debris was glued to the floor. The scrape `v *= 0.86` ran every frame while grounded, which is 0.86^60 per second — it pinned a 9 kg crate at 0.7 m/s in a 19 m/s wind. Friction now applies on impact, with dt-scaled rolling friction while resting. A crate crosses the whole yard at ~6 m/s. - Debris slid instead of tumbling: wind is horizontal, so once down there was no vertical force at all and it skated at constant height. Added tumbling lift that flips sign as it rolls — bins hop now. - The cloud dome had a dead straight seam across the sky. The fbm claimed to tile and didn't; now each octave wraps at its own integer period. Also: shelters and the bench now use Lane A's landed yard coords (t1 -9,2 / t2 8,-2, gardenBed 1,2) instead of my guesses, so shelter tuning means something. Verified in-browser against a real storm: crate crosses the yard and the t=74 bin spawns from storm JSON; sail node shoved 0.32 m; a 14 kg bin at 20 m/s knocks the player down and a tub drifting at 0.3 m/s correctly does not; lightning peaks 0.88. Lane A's selftest: 37 pass / 3 skip. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
383471d0f5
commit
11f27c493c
@ -13,6 +13,9 @@ import { rng } from './contracts.js';
|
||||
|
||||
const RHO = 1.2; // air density, kg/m³
|
||||
const GRAVITY = -9.81;
|
||||
// Deceleration while resting on the ground, m/s². Drag in a 19 m/s wind gives a
|
||||
// 9 kg crate ~7 m/s², so it still skitters downwind — which is the whole point.
|
||||
const GROUND_FRICTION = 3.5;
|
||||
|
||||
// Fallback specs for Lane E's debris set (3D-STORE crates/tubs). Radius is the
|
||||
// collision sphere, not the render bounds — a crate is boxy, but a sphere is
|
||||
@ -85,6 +88,7 @@ export function createDebris(o = {}) {
|
||||
// spin axis is arbitrary but seeded; rate scales with airspeed in step()
|
||||
sx: rand() * 2 - 1, sy: rand() * 2 - 1, sz: rand() * 2 - 1,
|
||||
spin: 0,
|
||||
phase: rand() * 6.283, // so two crates don't hop in lockstep
|
||||
r: spec.r, mass: spec.mass, cd: spec.cd,
|
||||
area: Math.PI * spec.r * spec.r,
|
||||
hitPlayer: false,
|
||||
@ -140,17 +144,37 @@ export function createDebris(o = {}) {
|
||||
p.vy += ry * k * dt + GRAVITY * dt;
|
||||
p.vz += rz * k * dt;
|
||||
|
||||
// A tumbling bluff body doesn't just get shoved, it gets picked up: lift
|
||||
// flips sign as it rolls, which is why a bin HOPS across a yard instead
|
||||
// of sliding. Wind is horizontal (weather.js keeps y=0), so without this
|
||||
// there is no vertical force at all once it's down and it just skates.
|
||||
p.vy += (0.5 * rel * rel * Math.sin(p.spin * 1.7 + p.phase) / p.mass) * dt;
|
||||
|
||||
p.x += p.vx * dt;
|
||||
p.y += p.vy * dt;
|
||||
p.z += p.vz * dt;
|
||||
|
||||
// ground: bounce, shed sideways speed, keep skittering
|
||||
// ground
|
||||
const floor = groundAt(p.x, p.z) + p.r;
|
||||
if (p.y <= floor) {
|
||||
p.y = floor;
|
||||
if (p.vy < 0) p.vy = -p.vy * 0.32; // dead-ish bounce, it's a plastic tub
|
||||
if (Math.abs(p.vy) < 0.35) p.vy = 0;
|
||||
p.vx *= 0.86; p.vz *= 0.86; // scrape
|
||||
if (p.vy < -0.5) {
|
||||
// a real impact: bounce, and lose some tangential speed to the hit
|
||||
p.vy = -p.vy * 0.32; // dead-ish, it's a plastic tub
|
||||
p.vx *= 0.72; p.vz *= 0.72;
|
||||
} else {
|
||||
if (p.vy < 0) p.vy = 0;
|
||||
// Resting: rolling friction as a dt-scaled DECELERATION, not a
|
||||
// per-frame multiplier. `v *= 0.86` every frame is 0.86^60 per
|
||||
// second — that isn't scrape, it's glue, and it pinned a 9 kg crate
|
||||
// at 0.7 m/s in a 19 m/s wind.
|
||||
const sp = Math.hypot(p.vx, p.vz);
|
||||
if (sp > 1e-4) {
|
||||
const drop = Math.min(sp, GROUND_FRICTION * dt);
|
||||
p.vx -= (p.vx / sp) * drop;
|
||||
p.vz -= (p.vz / sp) * drop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tumble rate follows airspeed — becalmed debris shouldn't keep spinning
|
||||
|
||||
@ -110,11 +110,13 @@ function cloudTexture(size = 256, seed = 7) {
|
||||
const img = ctx.createImageData(size, size);
|
||||
for (let y = 0; y < size; y++) {
|
||||
for (let x = 0; x < size; x++) {
|
||||
// fbm, tiled by sampling a torus-ish domain so the scroll never seams
|
||||
// fbm at integer frequencies, each octave wrapped at its own period, so
|
||||
// the texture tiles: it's set to repeat(3,2) and it scrolls forever, and
|
||||
// an unwrapped octave puts a dead straight seam across the sky.
|
||||
let n = 0, amp = 0.5, f = 4;
|
||||
for (let o = 0; o < 4; o++) {
|
||||
n += amp * valueNoise2((x / size) * f, (y / size) * f, seed + o * 977);
|
||||
amp *= 0.5; f *= 2.07;
|
||||
n += amp * valueNoise2((x / size) * f, (y / size) * f, seed + o * 977, f);
|
||||
amp *= 0.5; f *= 2;
|
||||
}
|
||||
const v = clamp01((n - 0.28) * 2.2);
|
||||
const i = (y * size + x) * 4;
|
||||
|
||||
@ -22,9 +22,10 @@ const PROBES = [
|
||||
{ x: -14, z: 9 }, { x: 14, z: 9 }, { x: 5, z: -3 },
|
||||
];
|
||||
|
||||
// t1 and t2 from Lane A's landed yard (THREADS: "yard layout is now FACT")
|
||||
const TREE_SHELTERS = [
|
||||
{ x: -9, z: 4, radius: 3, strength: 0.45, length: 14 },
|
||||
{ x: 8, z: -5, radius: 2.5, strength: 0.4, length: 12 },
|
||||
{ x: -9, z: 2, radius: 3, strength: 0.45, length: 14 },
|
||||
{ x: 8, z: -2, radius: 2.5, strength: 0.4, length: 12 },
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@ -58,12 +58,28 @@ function hash2(ix, iz, seed) {
|
||||
|
||||
const smooth = (f) => f * f * (3 - 2 * f);
|
||||
|
||||
/** Value noise, 0..1, C1-continuous (smoothstep interp) so wind never steps. */
|
||||
export function valueNoise2(x, z, seed) {
|
||||
/**
|
||||
* Value noise, 0..1, C1-continuous (smoothstep interp) so wind never steps.
|
||||
*
|
||||
* @param {number} [period] Wrap the lattice at this many cells, making the noise
|
||||
* tile seamlessly over [0, period). The wind doesn't want this (the yard would
|
||||
* repeat); a scrolling cloud texture does, or every wrap boundary is a visible
|
||||
* straight edge in the sky. Pass an integer that matches your frequency.
|
||||
*/
|
||||
export function valueNoise2(x, z, seed, period = 0) {
|
||||
const ix = Math.floor(x), iz = Math.floor(z);
|
||||
const ux = smooth(x - ix), uz = smooth(z - iz);
|
||||
const a = hash2(ix, iz, seed), b = hash2(ix + 1, iz, seed);
|
||||
const c = hash2(ix, iz + 1, seed), d = hash2(ix + 1, iz + 1, seed);
|
||||
// branch, not a closure: this is the wind's hot path (the cloth alone samples
|
||||
// it thousands of times a second) and a per-call allocation would show up.
|
||||
let x0 = ix, x1 = ix + 1, z0 = iz, z1 = iz + 1;
|
||||
if (period > 0) {
|
||||
x0 = ((x0 % period) + period) % period;
|
||||
x1 = ((x1 % period) + period) % period;
|
||||
z0 = ((z0 % period) + period) % period;
|
||||
z1 = ((z1 % period) + period) % period;
|
||||
}
|
||||
const a = hash2(x0, z0, seed), b = hash2(x1, z0, seed);
|
||||
const c = hash2(x0, z1, seed), d = hash2(x1, z1, seed);
|
||||
return (a + (b - a) * ux) * (1 - uz) + (c + (d - c) * ux) * uz;
|
||||
}
|
||||
|
||||
|
||||
@ -60,7 +60,8 @@ ground.rotation.x = -Math.PI / 2;
|
||||
ground.receiveShadow = true;
|
||||
scene.add(ground);
|
||||
|
||||
const TREES = [{ x: -9, z: 4 }, { x: 8, z: -5 }];
|
||||
// Lane A's landed yard (THREADS): t1 (-9,2), t2 (8,-2), house edge at z=-9.9
|
||||
const TREES = [{ x: -9, z: 2 }, { x: 8, z: -2 }];
|
||||
for (const tr of TREES) {
|
||||
const trunk = new THREE.Mesh(
|
||||
new THREE.CylinderGeometry(0.2, 0.28, 4, 8),
|
||||
@ -77,13 +78,20 @@ for (const tr of TREES) {
|
||||
canopy.castShadow = true;
|
||||
scene.add(canopy);
|
||||
}
|
||||
// house edge along north, for scale
|
||||
// house edge along north (-Z), for scale
|
||||
const house = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(30, 3.2, 1),
|
||||
new THREE.MeshStandardMaterial({ color: 0x8a8f96 }),
|
||||
);
|
||||
house.position.set(0, 1.6, -10.5);
|
||||
house.position.set(0, 1.6, -10.4);
|
||||
scene.add(house);
|
||||
// the thing you're protecting — Lane A's gardenBed rect
|
||||
const bed = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(6, 0.25, 4),
|
||||
new THREE.MeshStandardMaterial({ color: 0x6b4a2f }),
|
||||
);
|
||||
bed.position.set(1, 0.12, 2);
|
||||
scene.add(bed);
|
||||
// 1.7 m reference person
|
||||
const ref = new THREE.Mesh(
|
||||
new THREE.CapsuleGeometry(0.25, 1.2, 4, 8),
|
||||
@ -174,7 +182,7 @@ addEventListener('wheel', (e) => { dist = Math.min(60, Math.max(8, dist + e.delt
|
||||
|
||||
function resize() {
|
||||
const w = innerWidth, h = innerHeight;
|
||||
renderer.setSize(w, h, false);
|
||||
renderer.setSize(w, h);
|
||||
camera.aspect = w / h;
|
||||
camera.updateProjectionMatrix();
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user