Commit Graph

11 Commits

Author SHA1 Message Date
type-two
688e821fcb Sprint 12 integration: merge all lanes (335/0/0), skyfx effective-rating creak, [I] entry 2026-07-17 21:17:45 +10:00
type-two
e5c0e3ffee skyfx: the change announces itself — a front wall in the sky (gate 3.4)
Every windchange event gets an in-world tell: a dark bank standing on the
horizon in the quarter the new wind comes FROM, rising across the 12 s
before the change and clearing after the swing, plus a low distant rumble
layer in the audio. Soft-edged via vertex alpha (the hard band read as a
floating rectangle on the bench). Deterministic off (dt, t): progress is a
pure function of t, azimuth comes from dirAt at a fixed post-change
instant. Night 2 teaches the tell at ~18 s; night 3 shows the same wall at
t=6, which is 'looks like night 2 and isn't' made visible.

dev_skyfx.html: the skyfx bench — one storm, bare ground, scrubbable
clock, so the tell can be judged by eye without playing to night 3.
Two new asserts in c.test.js (window, monotonic rise, direction, mesh
orientation, changeless storm stays clear, bit-exact determinism).
Selftest 322/0/0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 20:24:51 +10:00
m3ultra
e576f5cb67 Fabric + 0.40 as one landing: the wild night has a clean $80 win
Selftest 275/0/0. balance.test: **$80 · shade cloth · p1,p2,p3,p4 ->
hp 63, 0/4 lost — CLEAN WIN.** First time the wild night has been
winnable without sacrificing the rig. C found the line and the $10 gap;
this spends both levers together, as they only work together.

Fabric is priced at $0 BOTH WAYS, and that is a finding, not laziness.
SPRINT6 asked me to price the difference; I measured it and there is no
price at which membrane is a sane buy — it is dominated:
  buys:  +26% hail, but only on the PEA-hail nights (hailBlockFor(0.7,
         0.3)=0.74), which are the easy ones — on storm_02 (1.3) and the
         ice night (1.4) a knit already blocks 100%, because a 2 cm stone
         can't pass a 2 mm weave (C's ruling). Plus rain, at 0.25 of the
         drain vs hail's 5.0 — ~5% of the damage.
  costs: double the wind load, and it PONDS.
Charge for that and you've shipped a trap. Free, it's DESIGN.md's actual
words — "fabric choice is a forecast bet" — and the bet is real: cloth on
the windy nights, membrane when the hail is small and the wind is mild.
If A ever raises rain's weight, membrane earns a price.

Wired C's hailBlockFor into skyfx.gardenHailExposure, which nothing
consumed — without it the fabric choice would have been cosmetic, which
is the bug class this repo keeps finding.

I did NOT reproduce C's numbers, and the comment says so rather than
repeating them. C reported p1 dropping to 1.08 kN (under a carabiner's
1.2) with cloth + 0.40. I measure 1.27, and p1 never crosses under 1.2 at
any combo. The line still wins on a $5 carabiner — but because 1.27 vs
1.2 is a 6% overshoot and breaking needs 0.4 s SUSTAINED, not because the
corner got under its rating. The margin is TIME, not headroom, and it's
thinner than the table implies: anything that lengthens storm_02's gust
HOLDS could take it. Flagged for C in THREADS.

The old t2 quad stays as the sacrifice-play control (hp 56, 2/4 lost) —
now a worse bet the shop will happily sell you, rather than the wild
night's only outcome.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 14:25:59 +10:00
m3ultra
807c02fb9e Kill the headless-zero-shadow trap at its source (skyfx)
step() opened `if (!camera) return`, which read as an innocent null-guard and
was in fact a physics gate: a harness with no camera skipped the shadow-grid
rebuild, so gardenExposure/gardenHailExposure reported full exposure no matter
what the cloth was doing, and every rig scored as if the sail did not exist.
hp 36 was the bare-bed constant all along. It cost gate 0 two sprints of four
lanes' time before A found it, and the fix belongs in my file, not in each
harness that trips over it.

A camera means "there is a view to place things in", not "the weather is real".
So the grids — what the sail actually DOES — now rebuild above the render gate,
with the yard centre as the wind-sample fallback; only drops, dome, lights and
audio sit below it. Headless callers get correct numbers and skip the 3k drop
matrices they were never going to draw.

Asserted both halves so it cannot come back: a camera-less skyfx shelters the
bed (hail shadow > 0.9, exposure < 0.1 under a panel), and the camera does not
change the physics — headless and viewed shadows agree to 1e-9. If anyone
re-adds an early return, that test goes red.

Selftest 264/0/0 + the 2 gate-0 skips.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 13:10:53 +10:00
m3ultra
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>
2026-07-17 03:14:27 +10:00
m3ultra
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>
2026-07-17 02:28:09 +10:00
m3ultra
6971f31984 Rain stops at the cloth (SPRINT2 Lane C.3)
The garden visibly stays dry under the sail. Rain arrives along the wind, so
the dry patch sits downwind of the cloth and slides across the yard as the
wind swings — at the southerly change it walks right off the bed, free drama
and honest physics.

Cheap on purpose. Ray-testing 3k drops against 162 triangles every frame is
~486k intersections for an effect nobody inspects closely. Instead project the
sail's triangles ALONG the rain onto the ground into a coarse height grid, a
few times a second (the cloth moves slowly next to the rain); per-drop cost is
one grid read, and occluded drops get a zero-scale matrix rather than a
raycast. Measured 0.041 ms/rebuild, 10x/s = 0.41 ms/s against A's 0.63 ms
frame — negligible. Reads rig.pos/rig.tris, so nothing new needed from Lane B.

Verified in the real game with a surviving twisted rated rig: 497 grid cells
covered, 96% of the garden bed under cover, live drops culled under the cloth
and falling in the open yard either side. Screenshot for DESIGN.md.

skyfx exposes rainShadowOver(rect) — NOT the same as rig.coverageOver(bed,
sunDir). That one is the SUN shadow; this is the RAIN shadow (down the wind).
Which drives garden HP is a design call — flagged for A in THREADS.

Selftest 134/0/0 (8 new rain-shadow asserts).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 00:10:29 +10:00
m3ultra
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>
2026-07-16 23:58:31 +10:00
m3ultra
6c11368202 Expose audio state and levels
`ready` only meant the graph got built — a suspended AudioContext is still
silent, so there was no way to tell whether the storm was actually audible.
The HUD now reports the real context state.

Verified through it: context runs on first gesture; 7.5 -> 17.8 m/s takes the
wind bed 0.16 -> 0.36 gain while the howl layer goes 0.016 -> 0.104 and the
cutoff opens 428 -> 767 Hz, so a gale reads as a gale and not just a louder
breeze. Rain tracks its curve 0.03 -> 0.32.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 21:56:22 +10:00
m3ultra
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>
2026-07-16 21:50:41 +10:00
m3ultra
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>
2026-07-16 21:38:21 +10:00