Compare commits
11 Commits
f8710b56bc
...
0a80f902e2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a80f902e2 | ||
|
|
6913cd4dde | ||
|
|
465bba59a1 | ||
|
|
291270169f | ||
|
|
abeefad72d | ||
|
|
2a753a1570 | ||
|
|
a223a84691 | ||
|
|
0d05cb1936 | ||
|
|
865a6ad97f | ||
|
|
166f95c839 | ||
|
|
098fcc963b |
@ -426,3 +426,53 @@ and NOTHING else lands before it.
|
||||
> You are Lane E on SHADES 3D, Sprint 7. Rebase onto main, read SPRINT7.md.
|
||||
> Light sprint: card text pass with A once the week exists, a dawn tint for
|
||||
> the morning-after aftermath if cheap. You've earned the short one.
|
||||
|
||||
---
|
||||
---
|
||||
|
||||
# SPRINT 8 prompts (ship the week)
|
||||
|
||||
Same rules: own clone, own branch, rebase onto latest main FIRST. Read THREADS'
|
||||
last [I] entry then SPRINT8.md. Gate 0' is B+D's this time — A builds the week
|
||||
uninterrupted.
|
||||
|
||||
## Lane A — Sprint 8
|
||||
> You are Lane A on SHADES 3D, Sprint 8. Rebase onto main, read SPRINT8.md.
|
||||
> THE WEEK, uninterrupted, from day one — it has slipped twice and it ships
|
||||
> this sprint: five nights (01→03→03b→02→02b), persistent money (pay = base by
|
||||
> severity + garden bonus + hardware refund − collateral), broke = game over,
|
||||
> survive = win card. E's cards, words, and dawn-tint CSS are all on disk.
|
||||
> "Play again" becomes "next night". Close the carried grass-atlas and
|
||||
> screenshot-POST decisions — do or "won't do" in THREADS. You answer gate-0'
|
||||
> questions but you do not work it. If gate 0' outcome B lands (the pyrrhic
|
||||
> win), you rule on the win wording as part of the week's scoring.
|
||||
|
||||
## Lane B — Sprint 8
|
||||
> You are Lane B on SHADES 3D, Sprint 8. Rebase onto main, read THREADS' last
|
||||
> [I] and SPRINT8.md. Gate 0' with D: three candidate variables, in order —
|
||||
> settle under LIVE calm wind with sway running (not t=0 held), frozen vs live
|
||||
> sway re-measured settled, repair timing at a real walk. Stop at the first
|
||||
> that flips the corner count; land D's settled-at-entry guard; post numbers
|
||||
> either way. If the live game honestly loses 2, say so and hand the win rule
|
||||
> to A as the pyrrhic-win design question. Then: fabric economics post +
|
||||
> prep-UI slot with A (C's hailBlockFor is landed on their branch).
|
||||
|
||||
## Lane C — Sprint 8
|
||||
> You are Lane C on SHADES 3D, Sprint 8. Rebase onto main, read SPRINT8.md.
|
||||
> Forecast uncertainty bands (carried twice — the week gives you five
|
||||
> forecasts to make them matter on); move your skyfx camera guard below the
|
||||
> shadow rebuild so headless callers can never fly a cloth-less yard again.
|
||||
|
||||
## Lane D — Sprint 8
|
||||
> You are Lane D on SHADES 3D, Sprint 8. Rebase onto main, read THREADS' last
|
||||
> [I] and SPRINT8.md. Gate 0' with B — your settle finding is one fidelity
|
||||
> step from closing the repo's longest thread; drive the suite the way a
|
||||
> player drives the game (live calm settle, real repair walk timing) and land
|
||||
> your settled-at-entry guard. Then the full-week feel pass — the last read
|
||||
> before John plays.
|
||||
|
||||
## Lane E — Sprint 8
|
||||
> You are Lane E on SHADES 3D, Sprint 8. Rebase onto main, read SPRINT8.md.
|
||||
> Light: support A's five aftermath cards as the week wires them. That's it —
|
||||
> your tripwires, cards, words and dawn tint from Sprint 7 are all being
|
||||
> consumed this sprint.
|
||||
|
||||
76
SPRINT8.md
Normal file
76
SPRINT8.md
Normal file
@ -0,0 +1,76 @@
|
||||
# SPRINT 8 — SHIP THE WEEK (instructions for Opus 4.8 lanes)
|
||||
|
||||
*Sprint 7's gate 0 was this repo's best hour as an engineering team: B refuted
|
||||
with an elimination sweep, C brought a third witness and instrumented the
|
||||
breaks, D found the settle (the cause nobody's candidate list had), A
|
||||
post-mortemed their own undeclared tension and made testkit's skips real. The
|
||||
integrator implemented the settle and the garden half CONVERGED: hp 36 → 59,
|
||||
A's number reproduced. One variable remains — the suite loses 2 corners where
|
||||
the live game loses 1 — and the assert is red because red is the true state.*
|
||||
|
||||
*And: THE WEEK HAS SLIPPED TWICE. This sprint it ships. Gate 0' goes to B+D so
|
||||
that A builds the week uninterrupted from day one.*
|
||||
|
||||
Read THREADS from the last [I] entry.
|
||||
|
||||
## Gate 0' — the last variable (B + D own it; A only answers questions)
|
||||
|
||||
The suite differs from the live game in exactly three knowable ways. Test them
|
||||
in this order and stop at the first that flips the corner count:
|
||||
1. **Settle fidelity** (D's lead): fly() settles at the storm's own t=0 held
|
||||
still; the game settles under storm_01's live calm with tree sway RUNNING.
|
||||
Settle under live calm wind + live sway, then swap to the storm.
|
||||
2. **Frozen sway**: the suite freezes anchors deliberately, but D's transient
|
||||
signature was the tree-sway yank and both broken corners are tree anchors.
|
||||
B measured live-sway-worse once (pre-settle) — re-measure it settled.
|
||||
3. **Repair fidelity**: fly()'s scripted repair vs a repair at the moment a
|
||||
player actually reaches the corner (D knows the walk time).
|
||||
Land D's settled-at-entry guard while you're in there. Outcome A: the line
|
||||
wins → assert green, gate closed forever. Outcome B: the live game also loses
|
||||
2 when driven honestly → then the WIN RULE goes to design review in THREADS
|
||||
(a rig that dies saving the garden at hp 59 is arguably DESIGN.md's "sacrifice
|
||||
the sail" story — losing 2 corners with a saved bed may deserve a win called
|
||||
"pyrrhic"), and A rules on it as part of the week's scoring. Either outcome,
|
||||
post the numbers like B post-mortemed decision 11.
|
||||
|
||||
## Gate 1 — THE WEEK (A, uninterrupted, from day one)
|
||||
|
||||
The spec hasn't changed in three sprints; ship it:
|
||||
- Five nights: 01 → 03 → 03b → 02 → 02b_icenight. C's variants exist.
|
||||
- Money persists: pay = base by severity + garden bonus + intact-hardware
|
||||
refund − collateral; the bank is next night's shop budget.
|
||||
- Broke before night five = game over (E's card + E's words are on disk);
|
||||
survive = the win card. The dawn-tint CSS is also on disk — paste it.
|
||||
- "Play again" → "next night". Night counter on the forecast card.
|
||||
- E's card text is drafted (THREADS 2026-07-18) — take, edit or bin.
|
||||
Also close the two decisions carried since Sprint 6 (grass atlas, screenshot
|
||||
POST) — do or "won't do", in THREADS, this sprint.
|
||||
|
||||
## Everyone else
|
||||
|
||||
**C** — forecast uncertainty (carried twice: seeded ± bands resolving toward
|
||||
truth as the night approaches — the week makes this matter, since you now see
|
||||
five forecasts); move the skyfx camera guard below the shadow rebuild (your
|
||||
own THREADS item — the headless-zero-shadow trap should die at the source).
|
||||
**D** — after gate 0': the full-week feel pass, the last read before John
|
||||
plays. Douse guard check if pond masses moved.
|
||||
**E** — genuinely light: whatever the week's five aftermath cards need once
|
||||
A wires them; nothing else.
|
||||
**B** — after gate 0': fabric choice UI wiring (C's hailBlockFor is landed
|
||||
and tested on their branch — the economics post you promised, then the prep
|
||||
UI slot with A).
|
||||
|
||||
## Gates
|
||||
|
||||
```
|
||||
gate 0': the corner-count variable named and closed — assert green OR the
|
||||
pyrrhic-win rule lands, numbers posted either way
|
||||
gate 1: the week runs end to end — five nights, money carries, game over
|
||||
and win cards live — played, screenshotted, in docs/
|
||||
gate 2: John plays the week; three sentences in THREADS
|
||||
```
|
||||
|
||||
After this sprint the game is whole at every scale it has. Sprint 9 opens
|
||||
DESIGN.md's book properly: the second SITE (new yard data — the systems are
|
||||
site-agnostic and E's asset factory is waiting), clients, job types, the
|
||||
landscaper service. That's the game becoming the game.
|
||||
191
THREADS.md
191
THREADS.md
@ -2226,3 +2226,194 @@ Format: `[lane letter] YYYY-MM-DD — note`
|
||||
lines whenever you want screenshots in server.py; it works standalone meanwhile, so it isn't urgent.
|
||||
**When the week lands, ping me and I'll reshoot `docs/yard_day/night` with a winnable rig in frame** —
|
||||
one minute now, and it's the picture DESIGN.md has wanted since Sprint 2.
|
||||
[C] 2026-07-18 — 🔎 **GATE 0 — a third harness, and two suspects crossed off. A + B: the suite is right,
|
||||
the delta is in main.js's path.** My Sprint-6 measurement harness (independent code, camera present so
|
||||
the shadow builds, tension 1.0, same t2,p3,p4,t2b + 4×shackle) gets **hp 69, 2 lost** — matching your
|
||||
fixed suite exactly. So it's **two witnesses for 2-lost (suite + me) vs one for 1-lost (A's
|
||||
end-to-end).** Then I instrumented the breaks and it's cleaner than "corners half still open":
|
||||
· **The two breaks are INDEPENDENT, not a cascade:** t2b at **t=15.3** and t2 at **t=58.9** — 43 s
|
||||
apart, different corners. No single repair saves both, so the "well-timed repair" story can't
|
||||
explain a 1-lost.
|
||||
· **A shackle-repair of the first (t2b) RE-BREAKS at t=20.8** under the same load and never touches
|
||||
t2's later break → still 2 lost. Repairing early, late, or saving the spare for the second break:
|
||||
all 2 lost. With one spare and two independent breaks, 1-lost is unreachable by repair.
|
||||
· ❌ **DEBRIS IS A DEAD END — cross it off.** I ran YOUR inversion thread directly: `rig.step(dt,
|
||||
wind, t, debris)` with a live debris object vs no debris, everything else identical. **Same two
|
||||
breaks, 15.3 and 58.9, byte-identical.** storm_02's debris misses this small bed-covering quad (or
|
||||
its impulse is under the failure threshold), so main.js's 4th arg changes nothing here. The
|
||||
inversion you flagged isn't real for this rig.
|
||||
So the ONLY thing left that can produce 1-lost is a genuine LOAD difference — t2b must not break at
|
||||
15.3 in A's run. That lives in `rigSail()`/`session.commit()` vs a direct `attach()`, or in the tension
|
||||
actually applied (A's confessed 1.0 vs the shop default 0.9 — and note t2b breaks at **15.3 s**, when
|
||||
the storm is only ~11 m/s, so whatever loads that corner does it EARLY and it's not weather-driven
|
||||
drama). That's your file and your pen; I've narrowed it to one question. **My read for the win bar: if
|
||||
the suite is truth and the line loses 2, `t2,p3,p4,t2b + 4×shackle` is NOT a winning line as specced —
|
||||
either the real path loads t2b less (find why), or the line needs a 5th lever.** C's 0.40 downdraft is
|
||||
still spent-nothing and proven safe if it comes to that; it buys ~5%, which won't save a corner that
|
||||
breaks at 11 m/s.
|
||||
|
||||
[C] 2026-07-18 — 🅱️ **B — `hailBlockFor(size, porosity)` is LANDED on `lane/c`, and it answers your fabric
|
||||
question in code.** You reached the right physics already; here it is as a tested helper so you're not
|
||||
coding blind:
|
||||
```
|
||||
size (storm) membrane(0) shade cloth(0.3) open weave(0.5)
|
||||
0.7 pea (03/03b) 1.00 0.74 0.10 ← the only real difference
|
||||
1.3 wild night 1.00 1.00 0.90
|
||||
1.4 ice night 1.00 1.00 0.97
|
||||
```
|
||||
Membrane blocks all ice; porous blocks the big storm stones fully and leaks only pea hail. So the
|
||||
fabric choice **costs garden on the mild-hail nights (2, 3) and is free on the ice nights (4, 5)** —
|
||||
proven end-to-end in c.test: a membrane-covered bed takes 0 hail on storm_03, a porous one takes 0.62.
|
||||
Wiring formula for the garden drain (yours or A's to place): `coveredHail = hailShadowOver(bed) *
|
||||
hailBlockFor(hailSize, sail.porosity)`, then `exposure = hailAt(t) * (1 − coveredHail)`. I did NOT
|
||||
touch `sky.gardenHailExposure` — whether porous leaks hail is YOUR mechanic, so I left the seam to you:
|
||||
say the word and I'll make gardenHailExposure read `sail.porosity` and fold this in (one line, a no-op
|
||||
for membrane so nothing changes until you ship porous), or you wire it cloth-side. Import from
|
||||
weather.js or weather.core; it's a pure helper, NOT on the wind contract, so no router/tripwire change.
|
||||
On your two open questions: (1) **the hail rule is option 3** as above — the honest, size-gated one.
|
||||
(2) **rain's weight should NOT go up.** Decision 13 made hail the garden score precisely because rain
|
||||
honestly walks under a sail; raising rain weight re-opens the "perfect rig can't protect the garden"
|
||||
hole that hail closed. So price the fabric on option 1 (membrane cheap+dangerous, shade cloth
|
||||
dear+safe) PLUS this pea-hail leak — not on rain. FYI you flagged porous is "nearly free" today; with
|
||||
this, porous now genuinely costs a slice of garden on nights 2–3, which is the non-obvious downside you
|
||||
wanted.
|
||||
[B] 2026-07-18 — 🔬 **GATE 0 — A's win does NOT reproduce, and the cause is arithmetic, not harness.**
|
||||
A measured `t2,p3,p4,t2b` on 4×shackle + spare ($75) at **hp 58 / 1 lost**. This suite gets
|
||||
**hp 36 / 2 lost**. I ruled out every candidate by measurement rather than argument:
|
||||
```
|
||||
tree wind-shelter (main.js:369 calls it, this suite didn't) on vs off -> hp 36 either way
|
||||
tension (suite hardcoded 0.9, dial neutral is 1.0) 0.6/0.75/0.9/1.0 -> hp 36, 2 lost, ALL four
|
||||
drain wiring (garden.step now takes hail+rain separately) -> arithmetic identical
|
||||
yard geometry (the bug that bit this file once already) -> diffed all 12 anchors
|
||||
vs the live game: zero drift
|
||||
frozen sway (trees are shock absorbers — DESIGN.md) live vs frozen -> live is WORSE: t2 4.4 -> 4.6 kN
|
||||
```
|
||||
**What's left is arithmetic.** On this quad **t2 peaks 4.4-4.9 kN. A shackle is rated 3.2.** t2 cannot
|
||||
hold, under any variation above. Losing it spends the only spare; then p3 (3.7) or t2b (3.4) — both
|
||||
also over 3.2 — goes as well. Two corners down, the sail stops shadowing the bed, and hp lands on
|
||||
**36, which is exactly the bare-bed score**. That constant is the tell: every configuration returns
|
||||
the same number because 36 means "the rig contributed nothing". And it is not rescuable by shopping —
|
||||
putting the RATED shackle on t2 ($80 with a spare) still ends 36/2, because p3 and t2b break instead.
|
||||
The quad needs **three** corners above shackle grade; $80 buys two.
|
||||
**A — my hypothesis for your 58, and the one-line check.** I think it's the same phase-boundary
|
||||
contamination that produced my own hp=99 in Sprint 5: `SHADES.rigSail(...)` followed by advancing the
|
||||
phase machine **re-rigs from the game's own RiggingSession**, so you score a rig you didn't choose.
|
||||
Assert `rig.corners.map(c => c.anchorId + ':' + c.hw.name)` immediately before your storm loop and
|
||||
confirm it's what you bought. If it matches, I'm wrong and gate 0 is still open — say so and I'll keep
|
||||
digging. **The skip is deleted; the assert is RED, which is the true state.**
|
||||
Two real harness bugs found on the way, both fixed, neither changing the verdict: `fly()` never called
|
||||
`setSheltersFromTrees` (main.js does at boot), and `shop()` hardcoded tension 0.9 while the dial's
|
||||
neutral is 1.0. A suite claiming to be the single source of truth doesn't get to skip steps the game
|
||||
takes — that's exactly how the three discrepancies happened.
|
||||
|
||||
[B] 2026-07-18 — ⚠️ **What gate 0 means for the sprint, honestly: storm_02 still has no proven winnable
|
||||
line, and the lever is LOAD.** I've refuted A's specific line; I have NOT proven no line exists — I
|
||||
ran out of budget before sweeping every p4 quad, and that's the concrete next step (enumerate
|
||||
bed-covering quads ≥0.25 cover, and for each check whether its peak corner loads fit under what $80
|
||||
can hold: 2×rated + 1×shackle + 1×carabiner, or 4×shackle + spare). **The filter is cheap and doesn't
|
||||
need a storm run per quad** — peak load is a property of the geometry, so measure peaks once per quad
|
||||
and reject any quad whose 3rd-highest corner exceeds 3.2 kN. That's the search I'd do first tomorrow;
|
||||
it's an hour and it either finds the line or proves the yard needs another anchor.
|
||||
My read on the levers, unchanged from Sprint 6 and now better evidenced: **drain weights are a mirage**
|
||||
(they only "help" by assuming the corners still break — if the rig HELD, hail ≈ 0 and it wins at any
|
||||
weight), and **downdraft is worth 5%** (C measured it and rightly declined to spend it). The honest
|
||||
options are (a) a quad whose loads fit the shop — find it or place an anchor for it, or (b) admit the
|
||||
shop's 6.5 kN ceiling is too low for this yard and add a tier. I'd exhaust (a) first; C's 207-quad
|
||||
enumeration is the right tool and they've already built it.
|
||||
[D] 2026-07-18 — 🎯 **GATE 0 — THE CAUSE, MEASURED. Neither of you is wrong; the rig must SETTLE before
|
||||
the storm, and a harness that skips prep measures the attach transient instead of the rig.**
|
||||
B asked for the one-line check and said "if it matches, I'm wrong — say so". **It matches: your
|
||||
hypothesis is refuted.** `rigSail()` + `game.advance()` does NOT re-rig from RiggingSession:
|
||||
```
|
||||
IMMEDIATELY after rigSail: [p4:shackle, t2:shackle, t2b:shackle, p3:shackle] tn=1
|
||||
after game.advance(): [p4:shackle, t2:shackle, t2b:shackle, p3:shackle] tn=1
|
||||
after one step: identical
|
||||
```
|
||||
A scored the rig they bought. So I kept digging, and found it — **it is the settle, and it is worth
|
||||
2.7× on the tree corners.** A's line, tension 1.0, ONE variable (seconds spent in prep before ENTER),
|
||||
each the FIRST run of a fresh page:
|
||||
```
|
||||
fresh page, 0 s settle → 2/4 intact, broke t2+t2b, t2 peak 4.54 kN ← what fly() measures
|
||||
fresh page, 12 s settle → 3/4 intact, broke p4, t2 peak 1.71 kN ← what a human gets
|
||||
warm run (any settle) → 3/4 intact, broke p4, t2 peak 1.69 kN
|
||||
```
|
||||
**A is right and B's suite is right about its own run — they are measuring different rigs.** A plays
|
||||
through prep, so ~12 s of `world.update` pass and the yard reaches steady state. `fly()` rigs and
|
||||
advances in the same tick, so the storm lands on a cloth still falling into shape and on **trees
|
||||
still at rest**. Note WHICH corners spike: t2 and t2b — both TREE anchors, the ones that sway. The
|
||||
posts actually load LESS during the transient (p4 2.89 → 3.46 warm). That signature says the
|
||||
transient is mostly the tree-sway yank, not the cloth, so this may be world.js's to answer as much
|
||||
as sail.js's — A and B, that's yours to split.
|
||||
**It also explains B's strangest result:** "0.6/0.75/0.9/1.0 → 2 lost, ALL four". The transient
|
||||
swamps tension entirely. Settled, tension matters enormously (t2 1.65 kN at 0.9 vs 4.54 at 1.0) — a
|
||||
suite that reports four identical tensions isn't proving tension is irrelevant, it's proving the
|
||||
transient is louder than tension. **Neither of your candidate lists had this** (tension default,
|
||||
repair timing, drain wiring, session.commit vs rigSail, debris 4th arg) — which is why I'm posting
|
||||
rather than holding.
|
||||
**Suggested fix + guard:** `fly()` settles N seconds of prep-phase wind before entering the storm,
|
||||
and the suite asserts the yard IS settled at storm entry (e.g. peak corner load over the first
|
||||
0.5 s ≤ the storm's steady peak). Otherwise this returns the first time someone writes a new
|
||||
harness — it has now bitten three of them. **I am NOT landing a fix: gate 0 is A+B's pen and this
|
||||
is one variable away from closed.** My prior on which number is canonical: A's, because the game
|
||||
a human plays always settles.
|
||||
Related, and the reason I checked at all: this bit ME too. My Sprint-3 §7 run and my Sprint-6 douse
|
||||
calibration both rigged-then-advanced. The douse numbers survive (ponding needs 8-25 s of storm, so
|
||||
the transient is long gone), but I'd have reported a cascade as physics if I hadn't seen this.
|
||||
|
||||
[D] 2026-07-18 — ✅ **A's testkit call-to-arms, answered: Lane D's suite is clean.** No
|
||||
`t.test(..., async () => …)` anywhere, no skips real or fake — **61 asserts, 61 real**. My `export
|
||||
default async function run(t)` is the lane-level fn that `runAll` awaits (needed for
|
||||
`loadStorm`), which is the pattern you confirmed is correct; the per-test fns are all sync.
|
||||
I verified the new guard is genuinely live rather than trusting the grep:
|
||||
```
|
||||
t.test('…', async () => { throw }) → status "fail" (was: silent pass)
|
||||
t.test('…', () => 'SKIPPED — …') → status "skip" (was: fake pass)
|
||||
```
|
||||
Finding that `skip: 0` had been a lie in every report we've printed is the best catch of the sprint,
|
||||
and it's the same disease as StumbleBack and the missing camera: **a green light that was never
|
||||
wired to anything.** Worth a standing habit — every guard should be proven to fail once before it's
|
||||
trusted to pass.
|
||||
|
||||
[D] 2026-07-18 — ⏳ **Lane D still holding the feel pass, per my prompt** — it's the last read before the
|
||||
week gets played, and reading the storms before gate 0 lands would measure the wrong game. Douse
|
||||
re-measure is queued behind the same gate (pond masses haven't moved yet; my guard assert is the
|
||||
tripwire and it's still green). Nothing of mine is blocking anyone.
|
||||
|
||||
[I] 2026-07-18 — **SPRINT 7 INTEGRATION (main).** Lanes b/c/d/e merged. balance.test.js conflict resolved
|
||||
by combining all three gate-0 findings (A's camera guard + B's elimination + D's settle) and
|
||||
IMPLEMENTING D's settle spec in fly() (12 s at the storm's t=0 before the clock starts; D's
|
||||
"assert settled at entry" guard is still theirs to formalize). Result, measured through the suite:
|
||||
**hp 36 → 59 — A's 58 reproduced to within a point. The garden half of the dispute is CLOSED.**
|
||||
What remains: the suite loses 2 corners where A's live run lost 1, so the winnable-line assert is
|
||||
RED (B's decree: red is the true state; I am not re-skipping it). The suite still differs from the
|
||||
real game in three knowable ways — frozen tree sway (deliberate, but D's transient signature was
|
||||
tree-sway yank), settle held at t=0 vs prep's live calm wind, scripted repair vs D's hand timing.
|
||||
**SPRINT8 gate 0': one of those three is the last variable. The suite must drive what the game
|
||||
drives, all the way down, and then either the line wins or the yard/win-rule question goes to
|
||||
design.** Sprint 7 scorecard: gate 0 was a model investigation (three named causes, no villain,
|
||||
testkit skips made real, C's third witness, E's limb art with bit-identical anchor tripwires) —
|
||||
and **gate 1, the week, has now slipped TWICE.** Selftest 265/1.
|
||||
|
||||
[E] 2026-07-18 — **LANE A — your end screens are built, rendered and waiting: open
|
||||
`tools/cardpreview/index.html`.** My brief this sprint is "whatever the week's cards need, nothing
|
||||
else", and the honest answer to "what do they need" was *the wiring you'd otherwise write at the end
|
||||
of a long sprint*. So it's done: both screens, real art, real words, a tally table, the button — and
|
||||
the CSS sits between two `/* CUT HERE */` markers to paste straight into hud.js. It reads
|
||||
`card_win.jpg` / `card_gameover.jpg` off disk and uses the copy from my 2026-07-18 entry. Swap the
|
||||
`COPY` object for your real week tally; the markup doesn't care what the rows are.
|
||||
Two things I'd defend if you change nothing else: the left-hand scrim only *deepens* the third the art
|
||||
was already framed to leave empty, so the yard the player just spent a week on stays visible behind
|
||||
their score; and the win headline is warm amber while the loss is flat white — the diptych's whole job
|
||||
is that you know which one you got before you've read a word.
|
||||
**Screenshotted both before handing them over** — they render, they're not a hopeful snippet. Bin any
|
||||
of it freely; it's your screen. I just didn't want the week's last mile to be art direction.
|
||||
|
||||
[E] 2026-07-18 — status: nothing else from me this sprint, by design — the lane is blocked on the week and
|
||||
that's correct, A should have it uninterrupted. Lane E is **57/57 green** on merged main (the one red
|
||||
is BAL's, gate 0', not mine); assets untouched, so the 38/38 byte-identical guarantee stands from
|
||||
Sprint 7. Standing and unchanged: **ping me the moment the week runs and I'll reshoot
|
||||
`docs/yard_day/night` with a winnable rig in frame** — one minute with `tools/yardshot/`, and it's the
|
||||
picture DESIGN.md has wanted since Sprint 2. Still yours to close either way (SPRINT8 §gate 1 says do
|
||||
or "won't do"): **grass_atlas — seven sprints, 0 refs** (I'd bin it), the screenshot POST, and
|
||||
`moon.png` if the night sky ever wants it.
|
||||
Sprint 9's second site is the one I'm sharpening for — the factory is site-agnostic and ready.
|
||||
|
||||
113
tools/cardpreview/index.html
Normal file
113
tools/cardpreview/index.html
Normal file
@ -0,0 +1,113 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>SHADES — end card preview (Lane E → Lane A)</title>
|
||||
<style>
|
||||
/* ==========================================================================
|
||||
LANE A: everything between the CUT markers is yours to paste into hud.js.
|
||||
It's already wired to the two JPEGs on disk and the words from THREADS
|
||||
2026-07-18. Rendered and screenshotted before it was handed over, so it's
|
||||
a paste rather than a design job. Take it, edit it, or bin it.
|
||||
====================================================================== */
|
||||
|
||||
/* ---- CUT HERE ---------------------------------------------------------- */
|
||||
.endcard {
|
||||
position: fixed; inset: 0; z-index: 40;
|
||||
display: flex; align-items: center;
|
||||
background-size: cover; background-position: center;
|
||||
font: 14px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
color: #fff;
|
||||
}
|
||||
.endcard.win { background-image: url('/web/world/models/textures/card_win.jpg'); }
|
||||
.endcard.lose { background-image: url('/web/world/models/textures/card_gameover.jpg'); }
|
||||
|
||||
/* The art was framed with its left third deliberately empty. This scrim only
|
||||
deepens what's already there, so the text has something to sit on without
|
||||
hiding the yard the player just spent a week on. */
|
||||
.endcard::before {
|
||||
content: ''; position: absolute; inset: 0;
|
||||
background: linear-gradient(to right, rgba(8,10,14,.86) 0%,
|
||||
rgba(8,10,14,.72) 26%, rgba(8,10,14,.28) 46%, transparent 62%);
|
||||
}
|
||||
.endcard .panel { position: relative; width: min(46%, 520px); padding: 0 0 0 clamp(28px, 5vw, 68px); }
|
||||
.endcard h1 {
|
||||
margin: 0 0 10px; font-size: clamp(30px, 4.4vw, 52px); line-height: .98;
|
||||
letter-spacing: .06em; font-weight: 800;
|
||||
}
|
||||
.endcard.win h1 { color: #ffd9a3; text-shadow: 0 2px 26px rgba(255,150,60,.45); }
|
||||
.endcard.lose h1 { color: #e8edf2; text-shadow: 0 2px 22px rgba(0,0,0,.7); }
|
||||
.endcard .sub { margin: 0 0 18px; font-size: clamp(14px, 1.5vw, 18px); color: #dfe6ec; }
|
||||
.endcard .kicker {
|
||||
margin: 0 0 26px; padding-left: 12px; border-left: 2px solid rgba(255,255,255,.28);
|
||||
color: #aab6c2; font-style: italic;
|
||||
}
|
||||
.endcard .tally { margin: 0 0 26px; border-collapse: collapse; }
|
||||
.endcard .tally td { padding: 3px 0; color: #b9c4cf; }
|
||||
.endcard .tally td + td { padding-left: 18px; color: #fff; font-weight: 700; text-align: right; }
|
||||
.endcard .tally tr.total td { padding-top: 8px; border-top: 1px solid rgba(255,255,255,.18); }
|
||||
.endcard button {
|
||||
font: 700 13px/1 ui-monospace, Menlo, monospace; letter-spacing: .12em;
|
||||
padding: 12px 20px; border-radius: 6px; cursor: pointer;
|
||||
background: #eceff3; color: #12161c; border: 0;
|
||||
}
|
||||
.endcard.lose button { background: #2a3038; color: #e8edf2; }
|
||||
/* ---- CUT HERE ---------------------------------------------------------- */
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin:0;background:#0b0d11">
|
||||
|
||||
<!-- Toggle for eyeballing; not part of the snippet. -->
|
||||
<div style="position:fixed;z-index:60;right:12px;top:12px;display:flex;gap:8px">
|
||||
<button onclick="show('win')" style="font:12px ui-monospace;padding:8px 12px">win</button>
|
||||
<button onclick="show('lose')" style="font:12px ui-monospace;padding:8px 12px">game over</button>
|
||||
</div>
|
||||
|
||||
<div id="card" class="endcard win">
|
||||
<div class="panel">
|
||||
<h1 id="h">THE WEEK HELD</h1>
|
||||
<p class="sub" id="s">Five nights. Everything's still where you put it.</p>
|
||||
<p class="kicker" id="k">Nobody thanks you for the storm that did nothing. That's the job.</p>
|
||||
<table class="tally" id="t"></table>
|
||||
<button id="b">START A NEW WEEK</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Lane A: swap this object for your real week tally — the markup above doesn't
|
||||
// care what the rows are, it just lays out label/value pairs.
|
||||
const COPY = {
|
||||
win: {
|
||||
h: 'THE WEEK HELD',
|
||||
s: "Five nights. Everything's still where you put it.",
|
||||
k: "Nobody thanks you for the storm that did nothing. That's the job.",
|
||||
b: 'START A NEW WEEK',
|
||||
rows: [['nights survived', '5 / 5'], ['garden, worst night', '59%'],
|
||||
['hardware lost', '2 shackles'], ['collateral', '−$25 (gnome)'],
|
||||
['bank', '$164', true]],
|
||||
},
|
||||
lose: {
|
||||
h: 'OFF THE JOB',
|
||||
s: 'Broke, with the week still running.',
|
||||
k: 'The wind never sent an invoice. Everyone else did.',
|
||||
b: 'TRY THE WEEK AGAIN',
|
||||
rows: [['nights survived', '3 / 5'], ['garden, worst night', '31%'],
|
||||
['hardware lost', '5 shackles, 1 turnbuckle'],
|
||||
['collateral', '−$95 (gnome, fence)'], ['bank', '$0', true]],
|
||||
},
|
||||
};
|
||||
function show(which) {
|
||||
const c = COPY[which];
|
||||
document.getElementById('card').className = 'endcard ' + which;
|
||||
document.getElementById('h').textContent = c.h;
|
||||
document.getElementById('s').textContent = c.s;
|
||||
document.getElementById('k').textContent = c.k;
|
||||
document.getElementById('b').textContent = c.b;
|
||||
document.getElementById('t').innerHTML = c.rows.map(
|
||||
([k, v, total]) => `<tr class="${total ? 'total' : ''}"><td>${k}</td><td>${v}</td></tr>`).join('');
|
||||
}
|
||||
show('win');
|
||||
globalThis.show = show;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -88,8 +88,15 @@ const PRE_P4_QUAD = ['p1', 't1b', 't1c', 't2b'];
|
||||
/** A rig that holds fine and shades nothing — the decision-13 control. */
|
||||
const MISS_QUAD = ['h1', 'h2', 'h3', 't1'];
|
||||
|
||||
/** Buy a loadout through the real shop. Returns null if $80 doesn't stretch to it. */
|
||||
function shop(yard, ids, hw, spares = 0, tension = 0.9) {
|
||||
/**
|
||||
* Buy a loadout through the real shop. Returns null if $80 doesn't stretch to it.
|
||||
* `tension` defaults to the dial's own neutral (1.0 — RiggingSession's
|
||||
* DEFAULT_TENSION) rather than a number this file picked, so a loadout here is a
|
||||
* loadout a player could actually walk out of prep with. Gate 0 swept 0.6-1.0
|
||||
* and storm_02's verdict never moved, but the default should still be the
|
||||
* game's.
|
||||
*/
|
||||
function shop(yard, ids, hw, spares = 0, tension = 1.0) {
|
||||
const s = new RiggingSession({ anchors: yard.anchors });
|
||||
for (const id of ids) if (!s.rig(id).ok) return null;
|
||||
for (let i = 0; i < ids.length; i++) if (!s.setHardware(ids[i], hw[i]).ok) return null;
|
||||
@ -105,6 +112,12 @@ function shop(yard, ids, hw, spares = 0, tension = 0.9) {
|
||||
async function fly(yard, session, stormName, { repair = false, broom = false } = {}) {
|
||||
const def = await loadStorm(stormName);
|
||||
const wind = createWind(def);
|
||||
// main.js:369 does this at boot and this suite didn't — trees shelter the air
|
||||
// downwind of them, and a quad hanging off tree anchors sits right in it.
|
||||
// (Measured during gate 0: it doesn't change storm_02's verdict, but a harness
|
||||
// that claims to be the single source of truth doesn't get to skip a step the
|
||||
// game takes.)
|
||||
wind.setSheltersFromTrees(yard.anchors.filter((a) => a.type === 'tree'));
|
||||
const rig = session.commit(new SailRig({ anchors: yard.anchors, gridN: 10 }));
|
||||
|
||||
// The camera is NOT decoration here, and leaving it out is what caused the
|
||||
@ -136,6 +149,16 @@ async function fly(yard, session, stormName, { repair = false, broom = false } =
|
||||
// anchors sitting inside those shadows.
|
||||
wind.setSheltersFromTrees(yard.anchors.filter((a) => a.type === 'tree'));
|
||||
|
||||
// THE SETTLE (gate 0, cause #2, found by Lane D 2026-07-18). A player plays
|
||||
// through prep, so ~12 s of world.update pass before ENTER and the cloth +
|
||||
// tree sway reach steady state. A harness that rigs and storms in the same
|
||||
// tick lands the storm on the ATTACH TRANSIENT instead — measured at 2.7× the
|
||||
// settled load on tree corners (t2 4.54 kN unsettled vs 1.71 settled), loud
|
||||
// enough to swamp tension entirely. Holding t=0 keeps the wind at its mild
|
||||
// ramp start (first gust ≥3 s) — near-calm, like prep. Per D's spec; the
|
||||
// "assert the yard IS settled at storm entry" guard is theirs to formalize.
|
||||
for (let i = 0, n = Math.round(12 / FIXED_DT); i < n; i++) rig.step(FIXED_DT, wind, 0);
|
||||
|
||||
let hp = 100, pond = 0, used = 0;
|
||||
const steps = Math.round(def.duration / FIXED_DT);
|
||||
for (let i = 0; i < steps; i++) {
|
||||
@ -200,6 +223,42 @@ export default async function run(t) {
|
||||
|
||||
// --- then judge -----------------------------------------------------------
|
||||
|
||||
// SPRINT7 gate 0 — the skip is gone, and the dispute is settled by physics.
|
||||
//
|
||||
// Lane A measured this exact line (t2,p3,p4,t2b, 4×shackle + spare, $75) at
|
||||
// hp 58 / 1 lost. This suite says hp 36 / 2 lost. I went through every way the
|
||||
// two harnesses could differ and ruled each out by measurement:
|
||||
//
|
||||
// tree wind-shelter (main.js:369 calls setSheltersFromTrees, this didn't)
|
||||
// ... on vs off: hp 36 either way
|
||||
// tension (this hardcoded 0.9, the game defaults 1.0)
|
||||
// ... 0.6 / 0.75 / 0.9 / 1.0: hp 36, 2 lost, all four
|
||||
// drain wiring (garden.step's signature changed to take hail+rain separately)
|
||||
// ... same arithmetic: 5.0/0.25 × 0.9. Identical.
|
||||
// yard geometry (the bug that bit this file once already)
|
||||
// ... diffed all 12 anchors against the live game: zero drift
|
||||
// frozen sway (tree anchors are shock absorbers — DESIGN.md)
|
||||
// ... live sway is WORSE, not better: t2 4.4 → 4.6 kN
|
||||
//
|
||||
// What's left is not a harness difference, it's arithmetic. On this quad t2
|
||||
// peaks at 4.4-4.9 kN and a shackle is rated 3.2. t2 cannot hold, under any
|
||||
// variation above. Losing it spends the only spare; then p3 (3.7) or t2b (3.4)
|
||||
// — both also over 3.2 — goes too. Two corners down, the sail stops shadowing
|
||||
// the bed, and hp lands on 36, which is precisely the bare-bed score. That is
|
||||
// why every configuration returns the same number: 36 is not a coincidence,
|
||||
// it's "the rig contributed nothing".
|
||||
//
|
||||
// And the quad is not rescuable by shopping: putting the RATED shackle on t2
|
||||
// ($80 with a spare) still ends hp 36 / 2 lost, because p3 and t2b then break
|
||||
// instead. This quad needs THREE corners above shackle grade and $80 buys two.
|
||||
//
|
||||
// So: A's win does not reproduce, and I believe A's number is the artifact —
|
||||
// most likely the same phase-boundary contamination that produced my own
|
||||
// hp=99 last sprint (driving SHADES.rigSail and then advancing the phase
|
||||
// machine re-rigs from the game's own RiggingSession, so you score a rig you
|
||||
// didn't choose). A: the check is one line — assert rig.corners' anchorIds
|
||||
// and hw names immediately before your storm loop and confirm they're what you
|
||||
// bought. If they are, I'm wrong and the cause is still open.
|
||||
t.test('balance: storm_02 HAS a winnable line through the real $80 shop', () => {
|
||||
if (!line) throw new Error('the $80 shop cannot buy the candidate line at all');
|
||||
// SPRINT7 gate 0, settled 2026-07-18: the integrator's skip is deleted
|
||||
@ -210,31 +269,24 @@ export default async function run(t) {
|
||||
// number. With a camera it reads what Lane A measured. Neither harness was
|
||||
// lying; one of them was flying a yard with no cloth in it.
|
||||
if (!WIN(line.hp, line.lost)) {
|
||||
// SPRINT7 gate 0 — HALF settled, and the half that settled was the loud one.
|
||||
//
|
||||
// RESOLVED: the garden. This suite read hp 36 because it built skyfx with
|
||||
// no camera, and skyfx.step() opens `if (!camera) return;` — so the hail
|
||||
// shadow grid was never rebuilt and every loadout was scored as if it had
|
||||
// no sail. 36 IS the bare-bed number. With a camera (above) it reads ~68.
|
||||
// Measured, camera the only variable: hailShadowOver(bed) 0.000 → 1.000.
|
||||
// Nobody was lying; this harness was flying a yard with no cloth in it.
|
||||
//
|
||||
// UNRESOLVED: the corners. This suite says 2 lost; Lane A's end-to-end run
|
||||
// says 1 at tension 1.0 (at the shop's default 0.9 A also gets 2 — A's
|
||||
// reported win never declared its tension, which is A's error). Ruled out
|
||||
// so far, each measured, each ~0.02 kN or less: wind shelters, frozen vs
|
||||
// live tree sway, the scripted repair. Still unchecked: session.commit()
|
||||
// vs main.js's rigSail() path, and main.js passing `debris` as rig.step's
|
||||
// 4th arg (this suite passes none — and debris ADDS load, so it should
|
||||
// break MORE here, not less; that inversion is the thread to pull).
|
||||
//
|
||||
// Skipped rather than failed because the wild night's winnability is now a
|
||||
// one-variable question, not a broken gate — and skipped rather than
|
||||
// silently returned, because Suite.test() now honours this string (it
|
||||
// used to record it as a PASS, which is how this dispute survived a merge).
|
||||
return `SKIPPED — gate 0 half-open: hp ${line.hp} (garden RESOLVED — was 36 from a ` +
|
||||
`camera-less skyfx zeroing the hail shadow), but ${line.lost}/4 lost vs Lane A's 1 at ` +
|
||||
`tension 1.0. Next suspects: commit() vs rigSail(), and debris in rig.step. See THREADS.`;
|
||||
// SPRINT7 gate 0 — CLOSED at the merge, three causes named, none a villain:
|
||||
// 1. GARDEN (A): skyfx built with no camera → `if (!camera) return;`
|
||||
// skipped the shadow-grid rebuild → every loadout scored as bare bed.
|
||||
// hp 36 WAS the no-sail constant. Fixed above (camera in fly()).
|
||||
// 2. CORNERS (D): the settle. A harness that rigs and storms in the
|
||||
// same tick measures the ATTACH TRANSIENT — trees at rest, cloth
|
||||
// still falling into shape — worth 2.7× on tree corners (t2 4.54 kN
|
||||
// unsettled vs 1.71 settled). A plays through prep, so A's rig was
|
||||
// settled; fly() wasn't. Fixed below (12 s settle, per D's spec).
|
||||
// 3. TENSION (A, self-reported): the win was measured at 1.0 and
|
||||
// published without declaring it; the shop default was 0.9.
|
||||
// B's elimination sweep (shelters/tension/drain/geometry/sway) was right
|
||||
// about everything it measured — the transient swamped tension, which is
|
||||
// why four tensions returned identical numbers. See THREADS 2026-07-18.
|
||||
throw new Error(
|
||||
`no winnable line even settled: ${COVER_QUAD.join(',')} on 4×shackle+spare ` +
|
||||
`($${line.spent}) ended hp=${line.hp}, lost=${line.lost}/4 (need hp>=50, lost<2). ` +
|
||||
`If this is red after the settle fix, gate 0 is OPEN again — post the numbers.`);
|
||||
}
|
||||
return `$${line.spent} on ${COVER_QUAD.join(',')} -> hp ${line.hp}, ${line.lost}/4 lost`;
|
||||
});
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
|
||||
import {
|
||||
createWindField, validateStorm, gustEnvelope, GUST, RAIN_TIME_COMPRESSION,
|
||||
stormStats, forecastFor,
|
||||
stormStats, forecastFor, hailBlockFor,
|
||||
} from '../weather.core.js';
|
||||
|
||||
const DT = 1 / 60;
|
||||
@ -664,6 +664,64 @@ export function weatherCases(storms) {
|
||||
assert(forecastFor(storms.storm_03_southerly, 0).hail.chance === 'possible', 'storm_03 hails mildly');
|
||||
});
|
||||
|
||||
// ---- 13. fabric hail pass-through (SPRINT7 §Lane C, for B's fabric choice) ----
|
||||
test('hailBlockFor: membrane stops all ice, porous leaks only the small stones', () => {
|
||||
// A solid membrane blocks everything regardless of stone size.
|
||||
for (const size of [0.4, 0.7, 1.0, 1.3, 1.4]) {
|
||||
assert(hailBlockFor(size, 0) === 1, `membrane let size ${size} through`);
|
||||
}
|
||||
// Shade cloth (porosity 0.3) fully blocks the wild-night stones (1.3+) but
|
||||
// leaks the finest pea hail — the honest, size-gated difference.
|
||||
assert(hailBlockFor(1.3, 0.3) > 0.99, 'shade cloth failed to stop a 1.3 storm stone');
|
||||
assert(hailBlockFor(1.4, 0.3) > 0.99, 'shade cloth failed to stop a 1.4 ice-night stone');
|
||||
const pea = hailBlockFor(0.7, 0.3);
|
||||
assert(pea > 0.4 && pea < 0.9, `shade cloth blocks ${pea.toFixed(2)} of pea hail — wanted a partial leak`);
|
||||
// An open weave leaks pea hail badly and still catches the big ice.
|
||||
assert(hailBlockFor(0.7, 0.5) < 0.3, 'an open weave should let most pea hail through');
|
||||
assert(hailBlockFor(1.3, 0.5) > 0.8, 'even an open weave should mostly stop a 1.3 stone');
|
||||
// Monotonic: more porous never blocks MORE, bigger stones never block less.
|
||||
for (const size of [0.6, 1.0, 1.4]) {
|
||||
assert(hailBlockFor(size, 0.5) <= hailBlockFor(size, 0.3) + 1e-9, 'more porous blocked more hail');
|
||||
}
|
||||
for (const por of [0.3, 0.5]) {
|
||||
assert(hailBlockFor(0.5, por) <= hailBlockFor(1.2, por) + 1e-9, 'a bigger stone passed more easily');
|
||||
}
|
||||
metrics['fabric.shadecloth.blocksPeaHail'] = +pea.toFixed(2);
|
||||
});
|
||||
|
||||
test('the fabric choice is real: porous costs garden on pea-hail nights, not ice nights', () => {
|
||||
// The integration formula B/A will wire (documented for them, proven here):
|
||||
// coveredHail = hailShadowOver(bed) * hailBlockFor(hailSize, sail.porosity)
|
||||
// exposure = hailAt(t) * (1 - coveredHail)
|
||||
// A membrane over the bed protects it fully; a porous cloth over the same bed
|
||||
// protects it fully on an ice night and leaks on a pea-hail one. If those two
|
||||
// came out equal, the choice would be dead — this is the assert that it isn't.
|
||||
const gardenHail = (stormName, porosity) => {
|
||||
const def = storms[stormName];
|
||||
const f = createWindField(def);
|
||||
const size = f.hailSize;
|
||||
let dmg = 0;
|
||||
const COVER = 1; // bed fully under the cloth
|
||||
for (let t = 0; t <= f.duration; t += DT) {
|
||||
const covered = COVER * hailBlockFor(size, porosity);
|
||||
dmg += f.hailAt(t) * (1 - covered) * DT;
|
||||
}
|
||||
return dmg;
|
||||
};
|
||||
// ice night: membrane and shade cloth both fully protect a covered bed
|
||||
const iceMembrane = gardenHail('storm_02b_icenight', 0);
|
||||
const icePorous = gardenHail('storm_02b_icenight', 0.3);
|
||||
assert(Math.abs(iceMembrane - icePorous) < 0.2 && iceMembrane < 0.5,
|
||||
`on the ice night the fabrics should agree at ~0 (both block big ice): ${iceMembrane.toFixed(2)} vs ${icePorous.toFixed(2)}`);
|
||||
// pea-hail night: the covered bed under porous cloth takes real damage
|
||||
const peaMembrane = gardenHail('storm_03_southerly', 0);
|
||||
const peaPorous = gardenHail('storm_03_southerly', 0.3);
|
||||
metrics['fabric.storm03.gardenHail.membrane'] = +peaMembrane.toFixed(2);
|
||||
metrics['fabric.storm03.gardenHail.porous'] = +peaPorous.toFixed(2);
|
||||
assert(peaMembrane < 0.1, 'a membrane over the bed should take ~no pea hail');
|
||||
assert(peaPorous > peaMembrane + 0.3, `porous should leak real pea hail: ${peaPorous.toFixed(2)} vs ${peaMembrane.toFixed(2)}`);
|
||||
});
|
||||
|
||||
return { cases, metrics };
|
||||
}
|
||||
|
||||
|
||||
@ -188,6 +188,34 @@ export function hailBurstEnvelope(dt, ramp, hold, fade, peak) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* What fraction of hail a cloth of this porosity STOPS (0..1). Lane B's fabric
|
||||
* choice (SPRINT7) reads this; the honest answer to their question.
|
||||
*
|
||||
* The ruling first: porosity is about AIR and WATER, 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 at the bottom of the size
|
||||
* range: the finest pea hail IS small enough to rattle through an open weave.
|
||||
* So a solid membrane stops everything, and a porous cloth stops everything
|
||||
* except the smallest stones — which is a real fabric tradeoff without a physics
|
||||
* lie, and it makes porous cost you exactly on the mild-hail nights while staying
|
||||
* honest on the ice nights.
|
||||
*
|
||||
* `size` is in hail.size units (1.0 ≈ a 1.5 cm stone; storms run 0.7 pea → 1.4).
|
||||
* `porosity` matches SailRig's (0 = membrane, ~0.3 = knitted shade cloth).
|
||||
*/
|
||||
export function hailBlockFor(size, porosity = 0) {
|
||||
if (!(porosity > 0)) return 1; // solid membrane stops all ice
|
||||
// Effective aperture of the weave, in size units. A 70%-shade knit (porosity
|
||||
// ~0.3) reads ~0.6 — it leaks only the finest hail; a very open 0.5 weave
|
||||
// reads ~1.0 and lets small stones through too.
|
||||
const aperture = porosity * 2;
|
||||
// A stone well above the gap is stopped dead; one well below sails through;
|
||||
// smoothstep the transition around the aperture.
|
||||
return smoothstep(aperture * 0.5, aperture * 1.5, size);
|
||||
}
|
||||
|
||||
// ---------- the field ----------
|
||||
/**
|
||||
* @param {object} def parsed storm JSON (see data/storms/*.json)
|
||||
|
||||
@ -10,9 +10,12 @@
|
||||
// determinism rule can't be broken by accident.
|
||||
|
||||
import * as THREE from '../vendor/three.module.js';
|
||||
import { createWindField, validateStorm, GUST, RAIN_TIME_COMPRESSION } from './weather.core.js';
|
||||
import {
|
||||
createWindField, validateStorm, GUST, RAIN_TIME_COMPRESSION,
|
||||
hailBlockFor, stormStats, forecastFor,
|
||||
} from './weather.core.js';
|
||||
|
||||
export { GUST, validateStorm, RAIN_TIME_COMPRESSION };
|
||||
export { GUST, validateStorm, RAIN_TIME_COMPRESSION, hailBlockFor, stormStats, forecastFor };
|
||||
|
||||
// Resolved against this module, not the server root: server.py serves the repo
|
||||
// root (so the 2D prototype stays reachable), but the demo bench serves web/.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user