e8fa8a736e
9 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0d05cb1936 |
Land hailBlockFor(size, porosity) — the honest fabric-hail rule for B
B asked for the hail ruling before coding fabric choice; this is it as code. The physics: porosity is about AIR (blows through → less wind load) and WATER (drains → no ponding), NOT ice. A knitted shade cloth's gaps are ~1-3 mm; a damaging hailstone is 6-45 mm, so it can't pass a mesh an order of magnitude finer than itself — porous and membrane block the big stones identically. The ONE true difference is the finest pea hail, which IS small enough to rattle through an open weave. So: membrane stops everything; porous stops everything except the smallest stones. Verified across the storm sizes: shade cloth (0.3) fully blocks the wild-night 1.3/1.4 stones and leaks 26% of storm_03's 0.7 pea hail; an open 0.5 weave leaks 90% of pea hail but still catches the big ice. That makes the fabric choice cost you exactly on the mild-hail nights and stay honest on the ice nights — a real tradeoff without a physics lie. The assert also proves the integration end to end so nobody wires it blind: a membrane-covered bed takes ~0 hail on storm_03, a porous-covered one takes 0.62 — the choice is genuinely non-trivial. Formula for whoever wires the garden drain: coveredHail = hailShadowOver(bed) * hailBlockFor(hailSize, sail.porosity). Pure helper (no THREE), exported through weather.js alongside stormStats/ forecastFor; NOT on the wind contract, so no router change. Selftest 263/0/0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
40be89c238 |
The week's two storm variants + forecast uncertainty
Variants for A's five-night week. Each gets ONE new trick, and neither is "the same storm but harder": - storm_03b_earlybuster (night 3): same force as storm_03 (21 m/s gust, 13 sustained) but the change lands at t=18 instead of t=30. You rig for a hot NW'er and get a southerly a third of the way in. Tests timing, not budget. - storm_02b_icenight (night 5): LESS wind than the wild night (28 vs 32 gust, 19 vs 20 sustained) and 2.8× the hail — 32 hail-seconds vs 11.4, held 26 s straight across the change, size 1.4 stones, ice on every gust ≥9. Hail is the score (decision 13), so this asks the one question storm_02 only asks for nine seconds: is the bed actually covered, and does the cover hold? A rig that survives storm_02 by hiding in a small quad off the bed loses here. Week escalation now reads 11.3 / 21.4 / 21.2 / 32.3 / 28.3 m/s peak gust with 0 / 2.4 / 2.4 / 11.4 / 32.0 hail-seconds — force ramps then plateaus while the question changes underneath it. Forecast uncertainty (DESIGN.md partial-information canon): `forecastFor(def, lead)` + `stormStats(def)`, pure functions on a def rather than methods on the wind — the card holds defs, doesn't want a field, and this keeps forecasting off the wind contract and out of A's router. Bands widen with lead and resolve to the exact truth at lead 0; deterministic per storm, so re-reading the card can't reroll it. The invariant, asserted across every storm at five leads: a band ALWAYS contains the truth. That's the line between partial information and a lie — a player who rigs for the top of the stated range must never be ambushed. stormStats also measures what the card was estimating: storm_02 gusts to 32.3 m/s, not the 30 that `baseCurve peak + powBase + powRamp` implied. Selftest 253/0/0; node 49/0/0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
f1d51d6b87 |
Hail — the system that makes the garden score respond to the rig (decision 13)
Rain honestly walks under a sail (a droplet's terminal velocity is ~9 m/s, so a 30 m/s crosswind blows it in at ~73° off vertical), which is why a perfect rig scored 54% garden vs 48% for no rig at all. Hail is dense: terminal ~22 m/s, and a dense stone couples weakly to the crosswind, so even a gale leans it ≤20°. Steep hail is blocked by overhead cloth, so the garden score becomes rig-responsive without faking the rain physics — and it was always DESIGN.md canon (hail shreds gardens; drainage answers rain, later). weather.core: `hail` block in storm JSON — authored bursts (envelopes) plus one synced to every gust at/above `withGustsAbove`, so the biggest gusts arrive WITH ice. `hailAt(t)` (max over live bursts), `hailSize`, validator. Gust-synced bursts key off the deterministic gust timeline and draw ZERO randomness, so tuning hail can't re-time the storm — asserted, same guarantee as the downdraft. storm_02 bursts on the southerly change (peak 1.0 at t=56.5, 11.4 hail-seconds); storm_03 a mild 0.5; storm_01 none. skyfx: `hailVelocity` (steep, ≤20° lean, terminal-velocity reasoning cited so nobody re-opens the rain-angle argument), a second RainShadow fed the steep vector, `gardenHailExposure(bed, t)` in the gardenExposure mold (A wires the drain), instanced falling stones (hidden under the cloth so you SEE the sail working), and hail audio: ground clatter that fades as the sail intercepts, plus the cloth DRUM that rises exactly as the sail catches hail — the "my sail is earning its money" sound. Decision-13 gate proven: no-sail garden takes 4.4× the hail of a bed under a good rig over a full storm_02 (bar is ≥2×), through the real gardenHailExposure and B's SailRig. Verified live too (router-patched): stones fall visibly steeper than the rain beside them, and a bed-covering rig cuts hail exposure roughly in half at the burst. Selftest 214/0/0 (was 207); node 36/0/0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
1f99cd9bca |
Rain gets physical units for ponding; night pass on wildnight
SPRINT4 §Lane C 2/3/4. PONDING DATA (decision 10). rainAt() was a dimensionless 0..1 — fine for drop count and opacity, useless for water mass. Lane B would have had to invent the mm/hr scale, which is exactly the "default-off code tuned by a constant I invented" they rightly reverted. So the scale is storm data now: rain.peakMmPerHour (validated 0..300; a rain curve without one is a hard error, since ponding would silently use the default instead of what the author meant), plus wind.rainMmPerHour(t) and rainDepthMm(t0,t1). RAIN_TIME_COMPRESSION=40 is exported from weather.core so B applies it cloth-side rather than either of us hardcoding 40 twice: how hard it rains is mine, how much water a sail holds is theirs. Calibrated to B's own arithmetic, and the numbers land on it: storm_02 80 mm/hr severe -> 50.9 mm = 3.12 kN/corner (B predicted 3.1) storm_03 30 mm/hr moderate -> 8.3 mm = 0.51 kN/corner (teases a carabiner) storm_01 8 mm/hr shower -> 0.9 mm = 0.06 kN/corner (harmless, as designed) against a storm_02 wind load of 0.2-1.1 kN. A flat rig should drown in the wild night; a hypar pools nothing and won't notice. Asserted with B's arithmetic so the storms are provably fit for their water before their cloth lands. DECISION 7 helper for Lane A: sky.gardenExposure(bed, t) = rainAt x (1-shadow), the whole drain term in one call. Note it moves on its own — the rain shadow follows the wind, so the southerly change walks the dry patch off the bed and the drain climbs with no corner having failed. NIGHT PASS. sky.night is now the author's call (was: inferred from a darkness threshold), and darkening scene.background did nothing anyway — the cloud dome covers it at 0.85 opacity, so an overcast-grey texture was what you actually saw. The dome now tints AND crushes (a lerp alone lands #717273: it runs in linear space and the texture is baked near-white). Stops at 0.78 because the yard has no lights and a storm you can't see is a black screen. Lightning now lights the cloud it's inside (#3e3e3f -> #d4ddf2), and fires on the biggest gusts via sky.lightningGustPow, not just the three authored strikes — driven off the telegraph so the flash lands with the gust that earned it. Also: c.test's storm list was hardcoded to two storms while the node runner globs the directory, so storm_03 was untested in the browser half. Its own ponding assert caught it. Selftest 195/0/0 (was 184). Verified live: night reads as night, flash lights the cloud, exposure responds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
576422e1f0 |
Downdraft as fraction of total wind (decision 8) + storm_03
The gust-only downdraft could not satisfy B's 60% no-free-lunch bar and the §7
twisted-survives gate together: the downdraft peaked at the gust peak, where
the horizontal peaked too, so a flat sail never reached 60% of a pitched one's
load without a spike so violent it also broke the twisted rig. The integrator
measured the pincer (0.58 -> 48% and still breaks twisted).
Fix: the downdraft is now a fraction of the LOCAL total wind speed, not of gust
power (weather.core verticalAt = -frac * localHoriz). It presses a flat roof
steadily across the whole storm — peak total 32.6 m/s dwarfs peak gust power
12.6 — so the ratio clears 60% at a gentle fraction, with no gust-peak spike.
It rides the local speed, so a tree's wind shadow shelters from falling air too.
speedAt() stays horizontal (a wind meter doesn't read falling air).
Field renamed downdraft -> downdraftOfTotal; the validator rejects the old name
rather than silently re-meaning it. The vertical now carries NO rng draws at
all, so the determinism guarantee (tuning can't re-time gusts) is structural,
not just separate-stream.
Measured both gates myself with B's SailRig (8-direction flat-vs-pitched sweep +
§7 legs). Target for storm_02 is 0.45: 69% of-max / 60% worst-heading on the
bar, twisted rated rig survives with ~21% margin. HELD at 0.12 this commit —
the current yard's only twisted quad ('h1,t2,p1,t1', ~190 m2) starts losing a
corner near 0.15 in the exact solver, so 0.45 would red B's §7. 0.12 ~= the old
gust-only 0.3 in peak downdraft (-4.2 vs -4.5 m/s). Bump to 0.45 is a one-number
joint step once A lands decision-2 anchors (18-45 m2 quads) and B re-points §7.
storm_03_southerly: campaign ramp between gentle and wildnight — peak gust 21
m/s, sustained 13, one moderate southerly change. Auto-swept by the suite.
Selftest 170/0/0 (all three §7 legs green). Verified live: downdraft/horizontal
ratio is exactly 0.12 in-game, rain occlusion still covers the bed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
135511fb05 |
Add vertical gusts, freeze debris.pieces, fix fog restore
SPRINT2 decisions 3 and 5, plus Lane A's fog nit. Decision 3 — gusts now descend. Cloth pressure goes with dot(wind, normal); a flat panel's normal points at the sky, so in a perfectly horizontal wind the dot is ~0 and "lie it flat and ignore the storm" was the cheapest winning rig. A gust front is descending air, not just faster air. Per-gust downdraft fraction in storm JSON (storm_02 0.3, storm_01 0.18, default 0.25, validated 0..1), each gust varying 0.6-1.4x. Peak downdraft in storm_02 is 4.4 m/s, 17% of the horizontal. Lane B: the cloth-side assert is yours. The vertical draws from its OWN rng stream, and there's an assert pinning that: pulling it from the main stream would shift every subsequent (t0, pow) and silently re-time storms Lane A has already hand-verified. Their carabiner still blows at t=45.4 and cascades at t=56. speedAt() stays horizontal — an anemometer doesn't read falling air, and a wind meter that spikes because a gust is descending reads as a bug. Decision 5 — debris.pieces frozen and documented in contracts.js as the seam Lane B reads in sail.step(), with the sphere/SI/mutated-in-place semantics spelled out and an assert tying the live shape to the contract table. Fog: dispose() captured scene.fog by reference and step() mutates that object in place, so restoring it restored nothing (Lane A caught it). Now captured by value, and fog we created ourselves is removed rather than left behind. Selftest 130/0/0 (was 121); Lane C 28 asserts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
11f27c493c |
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> |
||
|
|
383471d0f5 |
Add debris, skyfx and the Lane C bench; align to landed contracts
debris.js: hand-rolled kinematic tumble, drag ∝ speed² so the gust that spikes a corner is the one that launches the neighbour's bin. Ground bounce via world.heightAt (contracts.js documents it as ours), sphere-vs-player knockdown reported to Lane D, sphere-vs-sail-node impulse duck-typed so it lights up when Lane B exposes nodes and stays silent until then. skyfx.js: instanced rain that wraps around the camera rather than respawning, storm sky + procedural cloud dome, lightning, and synthesized WebAudio layers (wind bed, howl, rain, gust whoosh on the telegraph, rope creak off the worst corner, flog when one blows). It modulates Lane A's lights and hands them back on dispose() rather than owning them. weather_demo.html: graybox bench to drive all three before M0 — mock sail, storm scrub, 4x, throw-a-crate. Aligned to contracts.js now that it has landed: relative three imports (there is no importmap), contracts' rng() instead of a local copy, and storm paths resolved off import.meta.url — server.py serves the repo root, so an absolute /world/... would have 404'd at integration. storm_02: fix the southerly change. contracts.js puts north at -Z and the wind vector blows toward (cos d, sin d), so the old swing to +2.6 blew toward due south — a northerly wearing a southerly's name. Now slews to -1.35: a SSW buster off the open side of the yard, into the house. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
84f647a90c |
Add wind field, storm timelines and weather selftest
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> |