Compare commits
6 Commits
6f68fbf618
...
3dee2a7c83
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3dee2a7c83 | ||
|
|
4a5e5bc037 | ||
|
|
f179d6c25e | ||
|
|
b74dfd80db | ||
|
|
b7a069069d | ||
|
|
9d6db356d7 |
@ -913,3 +913,102 @@ loss.** Read docs/MANUAL.md, THREADS' last [I], then SPRINT15.md. Ports: A
|
||||
> posts. With the sprint's spare room: your call on palette gaps D hit while
|
||||
> authoring cold — read their gate-3.2 entry and propose what the next yard
|
||||
> needs, with reasoning baked beside the numbers as always.
|
||||
|
||||
---
|
||||
|
||||
# SPRINT 16 prompts (teeth — your work follows you) — ⚠️ FIRE ONLY AFTER SPRINT 15 INTEGRATES
|
||||
|
||||
Same rules: own clone, own branch, rebase onto latest main FIRST
|
||||
(`git fetch origin && git reset --hard origin/lane/<x>` on a clean tree).
|
||||
Read **ROADMAP.md** (the growth canon — John resolved the S14/S15 branch
|
||||
question: all three, sequenced, teeth first), THREADS from the last [I],
|
||||
then SPRINT16.md. Ports: A 8824, B 8825, C 8826, D 8827, E 8828. Baseline:
|
||||
whatever Sprint 15 landed (409/0/0 pre-15 + S15's additions — read the [I]).
|
||||
The one-variable rule is law: the new site and the new storm never debut on
|
||||
the same night. Numbers S15's post ruling moved get RE-MEASURED before they
|
||||
get pinned — no pre-ruling number survives into a pin.
|
||||
|
||||
## Lane A — Sprint 16
|
||||
> You are Lane A on HARD YARDS, Sprint 16. Rebase onto main, read ROADMAP.md,
|
||||
> THREADS' last [I], then SPRINT16.md. Gate 1 is yours: THE LEDGER. The week
|
||||
> records every committed rig (corners, hardware, what broke, what was
|
||||
> repaired); a corner still broken at dawn books a WARRANTY line on the next
|
||||
> morning's job sheet — hardware's shop price, off the fee AT QUOTE TIME, so
|
||||
> week.js's quote==settle invariant holds by construction. Reputation is one
|
||||
> number on the letterhead (job sheet AND invoice, E's contract): clean
|
||||
> nights up, warranty down, collateral down harder, and it moves the callout
|
||||
> fee multiplier ON THE SHEET so trust becomes visible money. Night 5's
|
||||
> gardenBeyondSaving must not cost rep — a designed loss the ledger punishes
|
||||
> is the verdict lying again. Aftermath and end-of-week card speak rep.
|
||||
> a.test: warranty only on unrepaired breaks; quote==settle with a warranty
|
||||
> line; rep untouched on designed losses; clean-week negative control. You
|
||||
> also print C's hail-size and rain lines on the forecast block (their gate
|
||||
> 3.3 exposes them). Do NOT build the job board — arc 2, and gate 6 aims it.
|
||||
|
||||
## Lane B — Sprint 16
|
||||
> You are Lane B on HARD YARDS, Sprint 16. Rebase onto main, read ROADMAP.md,
|
||||
> THREADS' last [I], then SPRINT16.md. Gate 2 is yours. (a) THE HEAL: after a
|
||||
> divergence break (landed in the fresh-eyes review — read that [I]), non-
|
||||
> finite node positions reset from the nearest finite neighbour (prev matched
|
||||
> — verlet must not invent velocity) and non-finite WATER zeroes (a NaN pond
|
||||
> re-poisons the heal through the weight term); assert finite within 1 s,
|
||||
> mutation-proof, and the existing divergence test stays untouched. (b) THE
|
||||
> RECORD: the chosen fabric prints in prep summary, invoice, and the verdict
|
||||
> when it mattered — the F key exists, now the paperwork remembers the bet.
|
||||
> (c) THE PIN, post-ruling numbers only: run gardenfly on site_02 (and the
|
||||
> swing lawn if unpinned), pin the honest held-vs-bare separation, assert it
|
||||
> like backyard_01's; if a yard can't separate at $80 that's a FINDING for
|
||||
> THREADS, not a fudged pin. Pool: the >83 ms determinism bound gets a
|
||||
> pinning test or a documented waiver.
|
||||
|
||||
## Lane C — Sprint 16
|
||||
> You are Lane C on HARD YARDS, Sprint 16. Rebase onto main, read ROADMAP.md,
|
||||
> THREADS' last [I], then SPRINT16.md. Gate 3 is yours. (1) STORM_06, the
|
||||
> pea-hail soaker: stones under your own 2 mm-weave blocking ruling, heavy
|
||||
> rain, mild wind — the first night membrane is the RIGHT answer. Cloth must
|
||||
> leak fine hail all night; membrane must pond (the broom matters); the
|
||||
> membrane line must be affordable at $80 and MEASURABLY better — gardenfly
|
||||
> and site_audit numbers pinned in your selftest, funnel state in the header,
|
||||
> post-ruling tree only. If cloth still wins, retune until the bet is real: a
|
||||
> decoration night is worse than no night. (2) makeStorm(seed): a seeded
|
||||
> generator inside authored envelopes, deterministic (same seed → byte-equal
|
||||
> def), schema-valid across 100 seeds, storm_envelope-tested — arc 4's
|
||||
> roguelite eats this; no shipped night uses it yet. (3) Expose hail stone
|
||||
> size and rain rate for the forecast block (A prints them) — bands, not
|
||||
> promises. The one-variable rule binds you to D: storm_06 flies over a yard
|
||||
> the player already knows.
|
||||
|
||||
## Lane D — Sprint 16
|
||||
> You are Lane D on HARD YARDS, Sprint 16. Rebase onto main, read ROADMAP.md,
|
||||
> THREADS' last [I], then SPRINT16.md. Gate 4 is yours. (1) Your swing lawn
|
||||
> enters the rotation: re-score site_03_swing_lawn on the post-ruling tree
|
||||
> with S15's faster loop (the numbers that made it winnable-but-mean may have
|
||||
> moved), fix what moved, wire its NIGHTS entry. (2) The week becomes SIX
|
||||
> nights: you own the order and the two new briefs (client flavour is data —
|
||||
> the corner block got a name and a reason; the swing set's kid deserves one
|
||||
> too). One-variable rule: the swing lawn debuts under a storm the player has
|
||||
> survived; storm_06 flies over a yard they know; never both new at once.
|
||||
> Night 5's beyond-saving arc and going-broke-never-on-the-final-night must
|
||||
> survive your re-ordering — a.test will tell you. (3) Cold-play all six on
|
||||
> one wallet and file the verdict: is a warranty line FAIR when it lands?
|
||||
> Does rep-moves-pay feel earned? Is night 6's bet legible from the forecast
|
||||
> alone? Your play is gates 1–3's acceptance. Pool: the phantom-sail seam if
|
||||
> S15 didn't close it.
|
||||
|
||||
## Lane E — Sprint 16
|
||||
> You are Lane E on HARD YARDS, Sprint 16. Rebase onto main, read ROADMAP.md,
|
||||
> THREADS' last [I], then SPRINT16.md. Gate 5 is yours: arc 2 needs yards
|
||||
> this palette cannot dress. Build TWO of three, factory-built, byte-stable,
|
||||
> negative-control-proven, and every anchor-bearing node passes S15's gate-1
|
||||
> mechanism (a baked hint that can't reach a JSON-declared anchor is the bug
|
||||
> class that cost the posts their truth): the GLASSHOUSE (collateral apex —
|
||||
> priced, wrecked variant, collateral_key wired so a SECOND one placed in
|
||||
> the editor still bills; nothing about it adopts as an anchor), the POOL
|
||||
> KIT (pool + compliant fence ring where every fence post is honest
|
||||
> tie_off:false — adoptAnchor's `?? 1` makes a silent node the best steel in
|
||||
> the game, and the pool yard's whole puzzle is that the legal tie-offs are
|
||||
> in the wrong places), the TRAMPOLINE + tie-down kit (typed honestly in the
|
||||
> checked enum; the tie-down is prep's counter to the storm's opening
|
||||
> projectile, priced like it matters). Flag D in THREADS as each lands. Axis
|
||||
> trap stands: measure exported GLBs in three.js coords, look at the render
|
||||
> in assetcheck, never trust a docstring about orientation.
|
||||
|
||||
11
README.md
11
README.md
@ -1,4 +1,8 @@
|
||||
# SHADES — rig it, then survive the night
|
||||
# HARD YARDS — rig it, then survive the night
|
||||
|
||||
*(repo/product history: the project was codenamed SHADES through Sprint ~8;
|
||||
the shipped game is HARD YARDS. Internal docs and the gitea repo name still
|
||||
say shades — that's the same thing, not a second game.)*
|
||||
|
||||
Geometry/tower-defense: you're a small person rigging shade sails against
|
||||
Australian storms. three.js r175 (vendored, no build step) + a zero-dep
|
||||
@ -20,9 +24,12 @@ No pip, no npm, no build. Python 3.8+ and a browser.
|
||||
1. `DESIGN.md` — the design canon (what the game is)
|
||||
2. `PLAN3D.md` — the build canon (stack, contracts, lane model)
|
||||
· `docs/MANUAL.md` — the HOW: tool map, asset/site cookbooks, earned gotchas
|
||||
· `ROADMAP.md` — the growth canon (the arcs from the week to the business)
|
||||
3. `THREADS.md` — the append-only lane log. **The project's memory.** Read the
|
||||
last few `[I]` integrator entries to know the current state.
|
||||
4. `SPRINT12.md` — the current sprint (highest-numbered SPRINTn.md wins)
|
||||
4. `SPRINT15.md` — the current sprint (highest-numbered *fired* sprint wins:
|
||||
SPRINT16 is pre-speced TEETH and fires only when 15 integrates — its own
|
||||
header says so)
|
||||
5. `LANE_PROMPTS.md` — copy-paste prompts per lane, per sprint (bottom = newest)
|
||||
|
||||
## The lane model (how this repo is built)
|
||||
|
||||
175
ROADMAP.md
Normal file
175
ROADMAP.md
Normal file
@ -0,0 +1,175 @@
|
||||
# HARD YARDS — the growth canon (where the game goes after the week)
|
||||
|
||||
*Written 2026-07-18 at John's direction, following the fresh-eyes review (THREADS
|
||||
[I] same date). DESIGN.md says what the game IS — it remains the design canon and
|
||||
nothing here overrides it. PLAN3D.md said how to BUILD the first game — done, 14
|
||||
sprints, live at partly.party/hardyards. This file says how the shipped week
|
||||
becomes the landscaping business DESIGN.md describes, in arcs that each end with
|
||||
something playable on the public URL. Sprints draw gates from here; when a sprint
|
||||
and this file disagree, the sprint is newer — fix this file.*
|
||||
|
||||
**John's ruling, 2026-07-18 (this is gate 4 of SPRINT14, resolved):** all three
|
||||
deferred branches — reputation/warranty, more sites, heatwaves — are wanted. Not
|
||||
picked between: SEQUENCED. Teeth first, clients and yards second, slow pressure
|
||||
third. "It doesn't just have to be one night… it's clearly much more than that
|
||||
now."
|
||||
|
||||
---
|
||||
|
||||
## Where we are (the honest baseline)
|
||||
|
||||
Five 90-second nights, two shipped yards (a third — the swing lawn — authored
|
||||
and inert), five storms, one wallet, ~15 minutes of campaign. Selftest 409/0/0. An editor that makes new yards cheap and scores them
|
||||
through the real engines. An economy whose own comments say the balance is
|
||||
unmeasured, and a scorecard that says a bare bed reads 82.6 where the best clean
|
||||
rig reads 91.3 — **doing nothing scores 90% of doing everything, and that is the
|
||||
single number this roadmap exists to fix first.** The sim is the strongest thing
|
||||
in the repo; the thinnest thing is consequence.
|
||||
|
||||
## The principles (each one was earned, none is negotiable)
|
||||
|
||||
1. **Every arc ends live.** partly.party/hardyards after every integration, a
|
||||
stranger can play it. No arc is "infrastructure for the next arc" — if it
|
||||
doesn't change a night a player feels, it isn't an arc, it's a gate inside one.
|
||||
2. **No mechanic without a surface.** The fabric bet shipped in SPRINT6 and no
|
||||
key reached it until SPRINT15. A sim feature the player cannot reach and the
|
||||
job sheet does not argue about does not exist. Every new mechanic lands with
|
||||
its input, its HUD read, and its line on the paperwork, in the same sprint.
|
||||
3. **Balance is measured, not reasoned.** The audit gauntlet (site_audit,
|
||||
gardenfly, storm_envelope, SCORE IT) runs before a night ships, and the pinned
|
||||
numbers go in THREADS. The bare-beds number is what "reasoned" balance looks
|
||||
like after eight sprints of everyone being careful.
|
||||
4. **Sites are data; storms are data; clients are data** (week.js proved the
|
||||
third). Content arcs must not add code paths per night — they add JSON and
|
||||
surfaces that read it.
|
||||
5. **One new variable per night.** A new site, a new storm, and a new mechanic
|
||||
never debut together — a player who loses must be able to name what beat them.
|
||||
6. **The full water sim stays a trap until it isn't** (DESIGN.md's own warning).
|
||||
Slices of water that reuse the pond (runoff aim, clogged inlets) are fair
|
||||
game; the heightmap flow layer waits for its own PLAN document and its own
|
||||
proof of fun in a bench, like the cloth had.
|
||||
|
||||
---
|
||||
|
||||
## ARC 0 — THE TRUE BASELINE (SPRINT15, already speced — the fork's gift)
|
||||
|
||||
*This roadmap and SPRINT15 were written in parallel forks the same day and
|
||||
reconciled: S15's gates (the post rating_hint ruling, the iterative editor
|
||||
score, the honest edges) re-measure the very numbers arc 1 pins, so S15 runs
|
||||
FIRST and TEETH fires as SPRINT16. The fork was the process working — D's
|
||||
cold-authoring pass found a shipped balance bug nobody reasoning from the
|
||||
code would have caught.*
|
||||
|
||||
## ARC 1 — TEETH (SPRINT16, speced in full in SPRINT16.md)
|
||||
|
||||
*The week gets consequences. Your work follows you.*
|
||||
|
||||
Reputation and warranty hang off the invoice exactly as DESIGN.md drew it: a
|
||||
corner of yours that blew and stayed blown books an unpaid warranty line on the
|
||||
next morning's job sheet; a clean night builds the number on the letterhead; the
|
||||
number moves what clients pay. The fabric bet gets its paper trail (forecast
|
||||
argues hail size and rain, invoice records the cloth you chose). D's swing lawn
|
||||
enters the rotation, the week becomes six nights, and one of them — C's pea-hail
|
||||
soaker — is the first night where the default fabric is the wrong answer. The
|
||||
bare-beds separation fight starts here and does not end until held-vs-bare is a
|
||||
pinned, teeth-bearing number on every shipped site.
|
||||
|
||||
## ARC 2 — THE BOOK OF CLIENTS (≈ two sprints)
|
||||
|
||||
*From a scripted week to a job board. Variety without new engines.*
|
||||
|
||||
- **The job board**: mornings offer N callouts, you take one. Pay, risk, client,
|
||||
and site visible before you commit — income becomes a choice, replay becomes
|
||||
real, and BROKE_BELOW starts meaning something because you can chase money
|
||||
into jobs you shouldn't take. The week structure (five-to-seven nights, one
|
||||
wallet) survives; what changes is that no two weeks need be the same list.
|
||||
- **Job types beyond "rig for tonight"** — each is a variation on engines that
|
||||
already exist, none needs new sim:
|
||||
- *Emergency callout* — you arrive MID-STORM at double rates to someone
|
||||
else's failing rig: triage, repair, cut-away. The storm phase already does
|
||||
everything this needs except spawn you at t=30 with a rig you didn't build.
|
||||
- *The cowboy audit* — storm-proofing someone else's work 36 hours out: walk
|
||||
the yard, find what's about to fail (frayed wire, unmoused pins, a carabiner
|
||||
where a shackle should be — the inspection tells exist as rating hints and
|
||||
hardware states), fix the worst of it on a budget that cannot fix all of it.
|
||||
- *Warranty callout* — arc 1's ledger, now as a full night: your own past
|
||||
work, revisited in weather, unpaid.
|
||||
- **Client personalities as constraints** (DESIGN.md's soft-constraint idea):
|
||||
"nothing attached to the house", "the pool fence is off limits", "cheapest
|
||||
option and I'll sue" — data on the night, enforced by the rigging session,
|
||||
argued on the job sheet. Cheap to build, huge puzzle variation.
|
||||
- **Yards 4–6 out of the editor**: the pool yard (fence compliance + corroded
|
||||
cheap hardware), the granny-flat alley (the venturi as the whole level), the
|
||||
glasshouse yard (maximum collateral, E's arc-1 asset). D authors them cold,
|
||||
the gauntlet prices them, separation pinned before they ship.
|
||||
|
||||
## ARC 3 — SLOW PRESSURE (≈ two sprints)
|
||||
|
||||
*DESIGN.md's other half: days, sun, and water you can see coming.*
|
||||
|
||||
- **Heatwave days**: the sun arcs, the shadow moves, shade-hours-delivered is
|
||||
the score (coverageOver() already ground-projects; it needs a sun path and an
|
||||
hourly integral). No storm — the tension is coverage over TIME, a different
|
||||
brain. Slow days between violent nights is the Overcooked rhythm DESIGN.md
|
||||
named, and it makes the storms land harder.
|
||||
- **The seasonal sun**: the same yard needs a different rig in February than
|
||||
July (sun elev/azim already live in site JSON — make them a season, not a
|
||||
constant). Seasonal-adjustment callouts fall out for free.
|
||||
- **Water, the honest slice**: sail runoff has to GO somewhere — one corner low
|
||||
dumps the pond's drainage where you aimed it (the veggie bed: bad; the path:
|
||||
fine; the neighbour's side: a bill). Reuses the pond machinery wholesale, adds
|
||||
aim. Second slice: leaves clog the one drain inlet and the puddle drowns the
|
||||
bed corner — the leaf layer already exists in debris.js as an ambient tell.
|
||||
Both slices are legible without a flow sim, and both make yards read as
|
||||
plumbing, which is the door arc 5 walks through.
|
||||
- **Heat affects you**: work slows in the blaze, water breaks are real, dawn
|
||||
and dusk are the smart hours — player.sim already has the shove/stagger
|
||||
scaffolding for one more environmental pressure.
|
||||
|
||||
## ARC 4 — THE SEASON (≈ two sprints)
|
||||
|
||||
*The campaign clock, persistence, and the roguelite.*
|
||||
|
||||
- **Save the business**: localStorage persistence — wallet, rep, gear, week
|
||||
number. The week loop closes into a season of weeks with a job-board economy.
|
||||
- **Storm season (roguelite)**: fixed starting budget, procedurally generated
|
||||
storm sequences inside authored envelopes (C's makeStorm(seed) groundwork
|
||||
lands in SPRINT15), survive the season solvent. The seeded weather core was
|
||||
built for this from day one.
|
||||
- **Gear that persists**: hardware tiers into TOOL tiers — the petrol auger vs
|
||||
the shovel (post placement time), the laser level (free grading intel), the
|
||||
second trailer (carry more spares). DESIGN.md's life-changing-upgrade list.
|
||||
- **Sandbox**: the storm designer sliders (dev_skyfx.html is 80% of it) plus
|
||||
the editor, public. Break your own yard for fun; the chaos physics carries it.
|
||||
|
||||
## ARC 5 — THE WATER TABLE (the big swing — gated, not scheduled)
|
||||
|
||||
*Retaining walls, drainage, the heightmap that flows. DESIGN.md's second game.*
|
||||
|
||||
Not scheduled until it passes the same bar the cloth passed: a bench that proves
|
||||
the fun before a lane builds the feature. The gate to open this arc is a
|
||||
tools/water_bench/ prototype — coarse-grid flow on a heightmap, one swale tool,
|
||||
one wall that can blow out, played in a probe page and felt to be a game. Arc 3's
|
||||
runoff/clog slices will have taught players (and us) how much water legibility
|
||||
buys at what sim cost. Everything DESIGN.md says about walls, weep holes,
|
||||
surcharge and the mudslide entering the debris system waits here, intact.
|
||||
|
||||
---
|
||||
|
||||
## The standing rails (every arc, no gate needed)
|
||||
|
||||
- **Perf floor**: the game must hold 60 fps on the M1 Ultra and stay playable on
|
||||
an Intel MacBook's integrated GPU. The review's 12-fps embedded-pane datum was
|
||||
the reviewer's harness, not the game — but nobody has MEASURED a low-end
|
||||
machine, and "measure, don't reason" applies to frame rate too.
|
||||
- **Sound**: every new mechanic that can kill a corner gets a tell by ear
|
||||
(DESIGN.md: players learn to defend by ear). The tink of a backing-out pin is
|
||||
still unbuilt and is the single highest-value audio asset in the design.
|
||||
- **Touch**: the notice locks phones out honestly. A touch rigging UI is a real
|
||||
future (the editor's raycast placement is most of one) — parked until arc 4,
|
||||
named here so nobody half-builds it sooner.
|
||||
- **Deploy**: tools/deploy_hardyards.sh after every integration; the live site
|
||||
is currently one sprint behind main and that gap should never exceed one.
|
||||
- **The sim's debt, tracked**: node-position heal after divergence (SPRINT16
|
||||
gate 2), the >83 ms frame determinism bound (documented, un-tested), membrane
|
||||
pricing the day rain's drain weight moves (rigging.js's comment names it).
|
||||
13
SPRINT15.md
13
SPRINT15.md
@ -79,8 +79,17 @@ D's friction list, by owner, all seen in anger:
|
||||
|
||||
## Gate 4 — JOHN PLAYS, THEN CHOOSES (nine sprints)
|
||||
|
||||
partly.party/hardyards — five nights, three sentences in THREADS. Then the
|
||||
branch, which the editor has now repriced:
|
||||
**⚡ THE BRANCH HALF OF THIS GATE IS RESOLVED (2026-07-18, integration checkout):**
|
||||
this sprint plan and a TEETH sprint plan were written in parallel forks the
|
||||
same day, and John ruled while they were being reconciled — **all three
|
||||
branches, sequenced: teeth → sites/clients → heatwaves.** ROADMAP.md (new,
|
||||
the growth canon) carries the arcs; SPRINT16.md carries TEETH pre-speced,
|
||||
firing when this sprint integrates. The gates above stand exactly as written
|
||||
— they re-measure the numbers SPRINT16 pins, which is why 15 runs first.
|
||||
|
||||
**The PLAY half stands:** partly.party/hardyards — five nights, three
|
||||
sentences in THREADS. The fabric key (F) is newly live; night 3's yard has a
|
||||
sibling now (`site_03_swing_lawn`, inert until SPRINT16 wires it).
|
||||
- **More sites** — D's `site_03_swing_lawn` is authored, scored and played,
|
||||
sitting inert waiting for a NIGHTS entry. This branch is now nearly free.
|
||||
- **Reputation / warranty** — DESIGN.md's consequence loop; the invoice exists
|
||||
|
||||
168
SPRINT16.md
Normal file
168
SPRINT16.md
Normal file
@ -0,0 +1,168 @@
|
||||
# SPRINT 16 — TEETH: YOUR WORK FOLLOWS YOU
|
||||
|
||||
*⚠️ PRE-SPECED, FIRES AFTER SPRINT 15 INTEGRATES. Sprint 15 (the post ruling,
|
||||
the iterative editor, the honest edges) re-measures numbers this sprint pins —
|
||||
do not start these gates on a pre-15 tree, and treat every number below that
|
||||
S15's gate-1 re-measure moves as "re-measure first". Written 2026-07-18 at
|
||||
John's direction (ROADMAP.md — gate 4 of S14 resolved: all three branches,
|
||||
sequenced, teeth first).*
|
||||
|
||||
*The week grows consequences — reputation on the letterhead, warranty on the
|
||||
job sheet, a sixth night, the swing lawn in the rotation, and the first night
|
||||
where the default fabric is the wrong answer. Baseline selftest: whatever
|
||||
Sprint 15 lands (409/0/0 pre-15 plus S15's additions). Keep it green;
|
||||
mutation-check anything you'd call coverage.*
|
||||
|
||||
**The one-variable rule is LAW this sprint (ROADMAP principle 5):** six nights,
|
||||
exactly one flies the new storm, exactly one is the new site, and they are NOT
|
||||
the same night. A player who loses night 6 must be able to say whether the yard
|
||||
beat them or the weather did.
|
||||
|
||||
## Gate 1 — THE LEDGER (A owns: week.js, hud.js surfaces, main.js wiring)
|
||||
|
||||
Reputation and warranty, hung off the paperwork that already exists. No new
|
||||
phase, no new sim — this is week-state plus three surfaces telling the truth.
|
||||
|
||||
1. **The work history.** Every committed rig is recorded on the week: corners,
|
||||
hardware, effective ratings, what broke, what was repaired by dawn. This is
|
||||
data the night already computes — the ledger just keeps it.
|
||||
2. **Warranty.** A corner of YOURS that broke and was still broken at dawn books
|
||||
a warranty item: the NEXT morning's job sheet carries it as a line —
|
||||
"warranty — q3, shackle, no charge to the client" — and its cost (the
|
||||
hardware's shop price, unpaid labour is the point) comes off that night's
|
||||
fee AT QUOTE TIME. week.js's own invariant is non-negotiable: the sheet and
|
||||
the invoice never diverge — the quote already carries the deduction, so
|
||||
settle pays exactly what was quoted. A repair you made mid-storm is NOT a
|
||||
warranty item; riding it out broken is.
|
||||
3. **Reputation.** One number on the letterhead (pick the unit — stars read as
|
||||
client-speak; whatever it is, it appears under the ABN on job sheet AND
|
||||
invoice, E's letterhead contract). Clean night (no breaks, no collateral):
|
||||
up. Warranty item: down. Collateral event: down harder. It MOVES PAY at
|
||||
quote time — a trusted outfit's callout fee multiplier, printed on the
|
||||
sheet, so the player watches their number become money. Start neutral;
|
||||
night 5's beyond-saving garden must NOT cost rep (the flag exists — read it;
|
||||
a designed loss the ledger punishes is the verdict lying again).
|
||||
4. **Aftermath teaches it.** The invoice's verdict line and the end-of-week
|
||||
card both speak rep: a week that ends broke-but-clean and a week that ends
|
||||
rich-with-warranties are different endings and should read as such.
|
||||
5. Tests, a.test: warranty fires only on unrepaired breaks (repaired = no
|
||||
item); quote==settle holds with a warranty line on the sheet; rep never
|
||||
moves on gardenBeyondSaving nights for garden reasons; the multiplier is on
|
||||
the sheet the night it applies. Negative control: a clean week books zero
|
||||
warranty items and ends rep-positive.
|
||||
|
||||
## Gate 2 — THE HEAL + THE PAPER TRAIL + THE PIN (B owns)
|
||||
|
||||
1. **Divergence heal.** The review landed the break (a NaN corner now lets go,
|
||||
reason:'divergence', production path) — but the freed node then integrates
|
||||
from NaN forces and the cloth renders ugly after one. Sanitize on the break:
|
||||
any non-finite node position resets from its nearest finite neighbour (or
|
||||
the corner's anchor), prev matched so verlet doesn't invent velocity, and
|
||||
non-finite water zeroed (a NaN pond re-poisons the heal through the weight
|
||||
term). Assert: poison → break → every node finite within 1 s of sim.
|
||||
Mutation: heal off, assert red. The existing divergence test must keep
|
||||
passing untouched — the heal is IN ADDITION to the break, not instead.
|
||||
2. **Fabric on the paperwork.** The chosen fabric prints in the prep summary,
|
||||
on the invoice, and in the aftermath verdict when it mattered (a membrane
|
||||
night survived in cloth should say what the leak cost). The FORECAST half is
|
||||
C's (gate 3.3) — yours is that the choice the player made is on the record.
|
||||
3. **Pin site_02's separation — AFTER S15's re-measure.** The SCORE IT card
|
||||
says NONE PINNED and calls it out. Once the post ruling has settled the
|
||||
numbers, run gardenfly, pick the honest target, pin the block in site_02's
|
||||
JSON, assert it like backyard_01's. If the honest answer is "this yard
|
||||
cannot separate held from bare at $80", that is a FINDING about the yard —
|
||||
post it in THREADS; the balance fight starts there, not at a fudged pin.
|
||||
Same pass, same rule for `site_03_swing_lawn` if S15 left it unpinned.
|
||||
|
||||
## Gate 3 — THE MEMBRANE NIGHT + THE SEEDED STORM (C owns)
|
||||
|
||||
1. **storm_06 — the pea-hail soaker.** Small stones (under the 2 mm-weave
|
||||
ruling's blocking threshold), heavy rain, mild wind. The night the bet
|
||||
flips: cloth leaks fine hail onto the bed all night; membrane stops
|
||||
everything and the mild wind means its doubled load is affordable — but it
|
||||
PONDS, so the broom matters. Author it so both lines exist and the membrane
|
||||
line is genuinely better: measure with gardenfly/site_audit (funnel state in
|
||||
the header, per the discipline; post-ruling numbers only), pin
|
||||
membrane-vs-cloth garden separation in the storm's own selftest, and make
|
||||
sure a $80 wallet affords the membrane line on the night's site. If the
|
||||
measured numbers say cloth still wins, retune the hail spectrum until the
|
||||
bet is real — the storm exists to make the F key matter, and a decoration
|
||||
night is worse than no night.
|
||||
2. **makeStorm(seed) — groundwork, not a shipped night.** A seeded generator
|
||||
inside authored envelopes (base curve, gust budget, direction change window,
|
||||
rain/hail ranges as data), producing storm defs that pass the schema
|
||||
validator and the storm_envelope harness. Deterministic from seed —
|
||||
ROADMAP arc 4's roguelite eats this. Selftest: same seed → byte-equal def;
|
||||
100 seeds → all schema-valid, all inside the envelope; a seed that violates
|
||||
the envelope is a red test, not a shrug.
|
||||
3. **The forecast argues the bet.** Hail stone size and rain rate join wind on
|
||||
the job sheet's forecast block (the data is in the storm JSON; A prints
|
||||
what you expose). The band-not-a-promise rule holds — forecast bands, real
|
||||
values inside them.
|
||||
|
||||
## Gate 4 — THE SIX-NIGHT WEEK (D owns)
|
||||
|
||||
1. **The swing lawn enters the rotation.** Your site_03_swing_lawn landed in
|
||||
S14 — authored cold, scored, played, inert. Re-score it on the post-ruling
|
||||
tree with S15's faster loop (the numbers that made it winnable-but-mean may
|
||||
have moved), fix what moved, then wire it into NIGHTS. If S15's edges work
|
||||
added a separation field or B pinned one for it, carry it; otherwise gate
|
||||
2.3 covers it.
|
||||
2. **The week becomes six nights.** Wire the swing lawn and storm_06 into
|
||||
NIGHTS under the one-variable rule: the new site debuts on a storm the
|
||||
player has already survived; storm_06 flies over a yard they already know.
|
||||
You own the order and the two new briefs (client flavour is data — the
|
||||
corner block got a name and a reason; the swing set's kid deserves one
|
||||
too). Night 5's beyond-saving flag and going-broke-never-on-the-final-night
|
||||
must both survive re-ordering — the a.test week asserts will tell you.
|
||||
3. **Cold-play the whole six**, wallet from night 1, and file the verdict:
|
||||
does a warranty callout read as FAIR when it lands on your sheet? Does rep
|
||||
moving pay feel earned or arbitrary? Is night 6's bet legible from the
|
||||
forecast alone? Your play IS the acceptance test for gates 1–3.
|
||||
4. Pool (yours): the phantom-sail seam if S15 didn't close it.
|
||||
|
||||
## Gate 5 — THE NEXT TEMPTATIONS (E owns)
|
||||
|
||||
Arc 2 needs yards this palette cannot dress yet. Two of these three, factory-
|
||||
built, byte-stable, negative-control-proven (build the wrong one on purpose,
|
||||
watch red) — and every anchor-bearing node passes the S15 gate-1 mechanism
|
||||
(a baked hint that cannot reach a JSON-declared anchor is the bug class that
|
||||
cost the posts their truth):
|
||||
1. **The glasshouse** — the collateral apex. Priced high, wreck variant,
|
||||
collateral_key wired so a SECOND one placed in the editor still bills (the
|
||||
carport's five-sprint lesson). Panes that read as fragile at a glance;
|
||||
nothing about it may adopt as an anchor.
|
||||
2. **The pool kit** — pool + compliant fence ring. The fence posts are the
|
||||
temptation: honest `tie_off:false` on every one (silence is not neutral —
|
||||
adoptAnchor's `?? 1` makes an unrated node the best steel in the game),
|
||||
because the pool yard's whole puzzle is that the legal tie-offs are all in
|
||||
the wrong places.
|
||||
3. **The trampoline + tie-down kit** — typed honestly in the checked enum;
|
||||
the tie-down is the prep-phase counter to the storm's opening projectile,
|
||||
priced like it matters.
|
||||
Flag D in THREADS as each lands.
|
||||
|
||||
## Gate 6 — JOHN: PLAY, THEN AIM ARC 2
|
||||
|
||||
Six nights at partly.party/hardyards (deploy after integration — ROADMAP: the
|
||||
live gap never exceeds one sprint). Three sentences in THREADS. Then one
|
||||
ruling for arc 2's first sprint: **emergency mid-storm callout or the cowboy
|
||||
audit** — which job type leads THE BOOK OF CLIENTS?
|
||||
|
||||
Pool, spread by ownership: whatever S15's pool leaves standing, plus:
|
||||
membrane pricing the day rain's drain weight moves (B, rigging.js's comment
|
||||
names the start); the >83 ms determinism bound gets a pinning test or a
|
||||
documented waiver (B).
|
||||
|
||||
```
|
||||
gate 1: ledger — warranty on the sheet, rep on the letterhead, quote==settle
|
||||
holds, a designed loss costs nothing
|
||||
gate 2: divergence heal (finite in 1s, mutation-proof); fabric on the record;
|
||||
separations pinned post-ruling or honestly refused
|
||||
gate 3: storm_06 makes membrane the right answer, measured not asserted;
|
||||
makeStorm(seed) deterministic inside envelopes; forecast argues the bet
|
||||
gate 4: swing lawn re-scored and wired; six nights, one-variable rule;
|
||||
the whole week cold-played and judged
|
||||
gate 5: two temptation assets, honest extras, negative controls, S15-clean
|
||||
gate 6: John's three sentences + the arc-2 job-type ruling
|
||||
```
|
||||
46
THREADS.md
46
THREADS.md
@ -6803,3 +6803,49 @@ anchors are your GLB), but the tooling is now waiting, not TODO.
|
||||
direction: John plays the week.** Nine sprints. The editor has made "more sites" nearly free and
|
||||
D has a candidate yard sitting ready; reputation/warranty and heatwave are unchanged in cost.
|
||||
That choice is his and the game is the best argument for making it that it has ever been.
|
||||
|
||||
[I] 2026-07-18 — 🔍 **FRESH-EYES REVIEW (John's ask, run by Claude in the integration
|
||||
checkout) + SIX FIXES LANDED ON MAIN.** Full read of canon + code + a played night 1 +
|
||||
the whole suite. Selftest **409/0/0** (406 baseline + 3 new, every new assert
|
||||
mutation-checked red-then-green in one sitting). The findings, fixed:
|
||||
|
||||
| # | finding | fix |
|
||||
|---|---|---|
|
||||
| 1 | `sail.selftest.js` dumpPond assert was a TAUTOLOGY — `r_prev` returned its own argument, so `|dumped − dumped| < 1e-9` could not fail no matter what dumpPond returned | asserts `dumped === pondMass-before` exactly, vacuity-guarded (`before > 1`); `r_prev` deleted. Third instance of the looks-wired-isn't disease, this time in Lane B's own suite |
|
||||
| 2 | **divergence never broke on the production path** — `load > rating` is false for NaN, so a diverged corner held FOREVER while NaN ate the spring network; `watchDivergence` (the throwing tripwire) is test-only and main.js never sets it | `_checkFailure` now breaks a non-finite corner instantly, `reason:'divergence'` on the event, load zeroed so the HUD reads a break, not NaN. New assert runs with watchDivergence OFF — the shipped path, poisoned node, 4/4 corners let go. Mutation: guard off → 0 breaks, red |
|
||||
| 3 | pond per-node cap only enforced on the rain-add — once rain stopped, downhill consolidation could stack a basin node past `POND_MAX_KG_M2` with nothing left to trim it | clamp moved to POST-FLOW (runs every step, rain or not; over-cap water sheets off, not conserved). New assert: 2000 kg injected on the settled belly → 280 kg one step later, no tear (the no-pondDump guard proves it was the CLAMP, not the belly tear). Mutation: clamp off → 1999 kg, red |
|
||||
| 4 | `debris.js` still carried the SPRINT2-era duck-typed `sail.nodes` seam + `applyToSail` — dead since decision 5 chose option b (`sail._applyDebris` reads `debris.pieces`), and a live DOUBLE-APPLY the day anyone exposed `.nodes` | deleted, with a comment saying where the real contact model lives. main.js may keep passing `sail:` — accepted and ignored, documented |
|
||||
| 5 | **the fabric bet was unreachable** — `FABRIC`/`setFabric()` shipped SPRINT6, the sim honoured porosity all along, rigging.js:276 calls cloth "half of the wild night's only winnable line"… and no key, click or HUD line ever reached it. Every rig ever played was DEFAULT_FABRIC | **F cycles cloth/membrane in prep.** Panel line 2 shows the fabric, help line advertises F, splash teaches it ("cloth breathes, membrane stops the rain"), blurb announced on switch. Session-level assert: cycle round-trips, $0 both ways, summary carries it, garbage refused. Verified in the running game |
|
||||
| 6 | main.js said `panel:false` in the comment and passed `panel:true` in the call, two sprints running | comment now tells the truth (B's panel IS the only prep UI; hud.js never took it over). README also retitled — the game shipped as HARD YARDS, the repo still said SHADES |
|
||||
|
||||
**Follow-ups pushed to SPRINT15, not snuck in here:** node-position sanitation after a
|
||||
divergence break (the freed corner integrates from NaN forces — break is detectable now,
|
||||
cloth render after one is still ugly; B should own the heal), a job-sheet surface for the
|
||||
fabric bet (the key exists, the FORECAST should argue with it — A), and membrane's
|
||||
balance once rain's drain weight moves (rigging.js's own comment says where to start).
|
||||
|
||||
[I] 2026-07-18 — 🔀 **TWO INTEGRATIONS, ONE DAY — THE FORK, RECONCILED.** The fresh-eyes
|
||||
review (my entry above) and the S14 lane integration (the [I] above it) happened in
|
||||
PARALLEL checkouts the same day: origin took D's swing lawn + the palette-_v1 fix + a
|
||||
SPRINT15 (posts/editor); this checkout took the six review fixes + a TEETH sprint plan,
|
||||
also numbered 15. Merged with the house rules — code was disjoint (their side: editor.js;
|
||||
my side: sail/rigging/debris/hud/main + suites), docs were the fork:
|
||||
|
||||
· **Their SPRINT15 STANDS as Sprint 15.** Its gate 1 (the post rating_hint ruling)
|
||||
re-measures the exact numbers TEETH wants to pin — running TEETH first would have
|
||||
pinned pre-ruling numbers, which is the S13 lesson at sprint scale.
|
||||
· **TEETH is SPRINT16, pre-speced** with a fires-after-15 banner; its D gate now WIRES
|
||||
the swing lawn instead of authoring it (it exists — 0b858ab), and every pin says
|
||||
"post-ruling numbers only". LANE_PROMPTS carries both sets, 16 below 15, marked.
|
||||
· **S15 gate 4's branch half is RESOLVED by John** (all three, sequenced — teeth →
|
||||
sites/clients → heatwaves; ROADMAP.md is the new growth canon holding the arcs). The
|
||||
PLAY half stands, and is better now: the F key is live and the review fixes ride along.
|
||||
· THREADS conflict resolved keep-both-chronological (their S14 entries, then the review
|
||||
entry — the review's "406 baseline" and "pushed to SPRINT15" read as that fork's truth;
|
||||
the follow-ups it names are SPRINT16 gates now).
|
||||
|
||||
Selftest on the merged tree: run below this entry — the review's 3 asserts join whatever
|
||||
D's landing carried. My review entry's fix #4 (debris seam deletion) and D's inert-props
|
||||
finding touched DIFFERENT halves of debris/editor and merged clean; the palette fix means
|
||||
the swing set now DRAWS, which makes E's S14 negative-control discipline the next thing
|
||||
the editor exercises.
|
||||
|
||||
@ -182,7 +182,8 @@ export function createDebris(o = {}) {
|
||||
/**
|
||||
* @param {number} dt fixed step
|
||||
* @param {number} t storm time
|
||||
* @param {object} [world] {player, sail} — both optional, both duck-typed
|
||||
* @param {object} [world] {player} — optional, duck-typed. (A `sail` key is
|
||||
* accepted and ignored: debris-vs-sail is sail.js's `_applyDebris`.)
|
||||
*/
|
||||
step(dt, t, world = {}) {
|
||||
// storm JSON drives the spawns; poll the window so nothing is missed
|
||||
@ -194,7 +195,6 @@ export function createDebris(o = {}) {
|
||||
}
|
||||
|
||||
const player = world.player;
|
||||
const sail = world.sail;
|
||||
|
||||
for (let i = pieces.length - 1; i >= 0; i--) {
|
||||
const p = pieces[i];
|
||||
@ -269,10 +269,11 @@ export function createDebris(o = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
// --- sphere vs sail nodes: impulse ---
|
||||
// Duck-typed: lights up the moment Lane B exposes nodes, silent until
|
||||
// then. See THREADS — B owns sail.js, so this is the seam we agreed on.
|
||||
if (sail && sail.nodes) applyToSail(p, sail);
|
||||
// debris-vs-sail lives in sail.js (`_applyDebris` reads `debris.pieces`
|
||||
// each step — SPRINT2 decision 5, option b). The old duck-typed seam
|
||||
// here (`if (sail.nodes) applyToSail(...)`) is deleted, not dormant:
|
||||
// had Lane B ever exposed `.nodes`, every hit would have been applied
|
||||
// TWICE through two different contact models.
|
||||
|
||||
if (p.y < groundAt(p.x, p.z) - 5 || Math.abs(p.x) > bounds.x || Math.abs(p.z) > bounds.z) {
|
||||
despawn(p);
|
||||
@ -292,33 +293,5 @@ export function createDebris(o = {}) {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Shove any cloth node the piece is intersecting, and lose some of the piece's
|
||||
* own momentum doing it. Expects sail.nodes: [{x,y,z,px,py,pz}] (verlet, so we
|
||||
* move position and let the integrator turn it into velocity).
|
||||
*/
|
||||
function applyToSail(p, sail) {
|
||||
const nodes = sail.nodes;
|
||||
const reach = p.r + 0.15;
|
||||
const reachSq = reach * reach;
|
||||
let hits = 0;
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const n = nodes[i];
|
||||
const dx = n.x - p.x, dy = n.y - p.y, dz = n.z - p.z;
|
||||
const dsq = dx * dx + dy * dy + dz * dz;
|
||||
if (dsq > reachSq || dsq < 1e-9) continue;
|
||||
const d = Math.sqrt(dsq);
|
||||
// push the node out to the sphere surface along the contact normal
|
||||
const push = (reach - d) / d;
|
||||
n.x += dx * push; n.y += dy * push; n.z += dz * push;
|
||||
hits++;
|
||||
}
|
||||
if (hits) {
|
||||
const drag = Math.min(0.5, (hits * p.mass) / 400);
|
||||
p.vx *= 1 - drag; p.vy *= 1 - drag; p.vz *= 1 - drag;
|
||||
if (sail.onDebrisHit) sail.onDebrisHit(p, hits);
|
||||
}
|
||||
}
|
||||
|
||||
return debris;
|
||||
}
|
||||
|
||||
@ -516,6 +516,7 @@ export function createHud(d) {
|
||||
['click', 'pick an anchor · again to unpick'],
|
||||
['[ ]', 'tension the rig — tighter holds shape, and breaks harder'],
|
||||
['S', 'buy a spare shackle'],
|
||||
['F', 'choose the fabric — cloth breathes, membrane stops the rain'],
|
||||
['E', 'repair a blown corner (ladder if it is up high)'],
|
||||
['C', 'brace against the wind'],
|
||||
['Enter', 'commit the rig and start the night'],
|
||||
|
||||
@ -740,8 +740,13 @@ export async function boot(opts = {}) {
|
||||
getSky: () => sky,
|
||||
});
|
||||
|
||||
// Lane B's picking adapter. `panel:false` — their built-in panel is a fine
|
||||
// bench, but hud.js owns the screen now, so the two shouldn't both draw.
|
||||
// Lane B's picking adapter. `panel:true` is DELIBERATE and the old comment
|
||||
// here ("panel:false — hud.js owns the screen") described an intent that
|
||||
// never happened: hud.js never grew a prep table, so B's panel is the only
|
||||
// prep UI there is. If hud.js ever takes prep over, flip this to false in
|
||||
// the same commit — two panels drawing at once is the bug the old comment
|
||||
// was worried about, and it was half right: the flag and the comment
|
||||
// disagreed for two sprints and nobody could tell which one was the plan.
|
||||
rigging = await createRiggingUI({
|
||||
scene,
|
||||
camera: cameraRig.object,
|
||||
|
||||
@ -514,6 +514,15 @@ export async function createRiggingUI({
|
||||
if (ev.key === '[') session.setTension(session.tension - 0.05);
|
||||
else if (ev.key === ']') session.setTension(session.tension + 0.05);
|
||||
else if (ev.key.toLowerCase() === 's') say(session.setSpares(session.spares ? 0 : 1));
|
||||
else if (ev.key.toLowerCase() === 'f') {
|
||||
// The forecast bet, finally reachable. FABRIC and setFabric() shipped in
|
||||
// SPRINT6 and the sim honoured porosity all along — but no key, click or
|
||||
// HUD line ever reached them, so every rig ever played was DEFAULT_FABRIC.
|
||||
// Two entries, so F is a toggle; announce the blurb because the panel
|
||||
// line alone doesn't say what the bet IS.
|
||||
const next = FABRIC[(FABRIC.indexOf(session.fabric) + 1) % FABRIC.length];
|
||||
if (say(session.setFabric(next.id)).ok) onMessage(`${next.name} — ${next.blurb}`);
|
||||
}
|
||||
else return;
|
||||
ev.preventDefault();
|
||||
refresh();
|
||||
@ -585,14 +594,14 @@ export async function createRiggingUI({
|
||||
: null;
|
||||
el.textContent = [
|
||||
`PREP — rig four corners $${s.budget} left`,
|
||||
`tension ${s.tension.toFixed(2)} spare x${s.spares}${area ? ` sail ${area.toFixed(0)} m2` : ''}`,
|
||||
`tension ${s.tension.toFixed(2)} spare x${s.spares} ${s.fabric.name}${area ? ` sail ${area.toFixed(0)} m2` : ''}`,
|
||||
'',
|
||||
...rows,
|
||||
...(standingLine ? ['', standingLine] : []),
|
||||
'',
|
||||
s.canStart ? 'ENTER to start the storm' : `pick ${MAX_CORNERS - session.picks.length} more corner(s)`,
|
||||
'click anchor: rig / cycle hw shift-click: remove',
|
||||
'[ ] tension S spare RMB orbit',
|
||||
'[ ] tension S spare F fabric RMB orbit',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
* js/tests/b.test.js) and node.
|
||||
*/
|
||||
|
||||
import { RiggingSession } from './rigging.js';
|
||||
import { RiggingSession, FABRIC } from './rigging.js';
|
||||
import { SailRig, TENSION_MIN, TENSION_MAX } from './sail.js';
|
||||
import { HARDWARE, START_BUDGET, SPARE_COST } from './contracts.js';
|
||||
|
||||
@ -344,6 +344,21 @@ test('reset() returns a used session to a fresh prep phase', () => {
|
||||
return 'budget, picks, tension, spares all fresh; rig-able again';
|
||||
});
|
||||
|
||||
test('fabric is a bet the F key can actually place (SPRINT15 — the dead mechanic)', () => {
|
||||
const s = session();
|
||||
assert(s.fabric.id === 'cloth', `default fabric should be cloth, got ${s.fabric.id}`);
|
||||
// the exact call the F key makes: cycle to the other entry by id
|
||||
const next = FABRIC[(FABRIC.indexOf(s.fabric) + 1) % FABRIC.length];
|
||||
assert(s.setFabric(next.id).ok, 'cycling to membrane refused');
|
||||
assert(s.fabric.id === 'membrane' && s.fabric.porosity === 0, 'membrane not applied');
|
||||
assert(s.budget === START_BUDGET, 'fabric is a bet, not a purchase — budget must not move');
|
||||
assert(s.summary.fabric.id === 'membrane', 'the panel reads fabric off summary and must see the change');
|
||||
// wrap-around brings it home, and garbage is refused loudly
|
||||
assert(s.setFabric(FABRIC[(FABRIC.indexOf(s.fabric) + 1) % FABRIC.length].id).ok && s.fabric.id === 'cloth', 'cycle should wrap back to cloth');
|
||||
assert(s.setFabric('hessian').ok === false, 'unknown fabric must refuse, not default');
|
||||
return 'cloth -> membrane -> cloth, $0 both ways, hessian refused';
|
||||
});
|
||||
|
||||
export const RIGGING_TESTS = TESTS;
|
||||
|
||||
export function runRiggingSelftest() {
|
||||
|
||||
@ -542,8 +542,6 @@ export class SailRig {
|
||||
const a = this._nodeArea[n];
|
||||
if (a > 1e-9 && kgPerM2PerSec > 0) {
|
||||
w[n] += kgPerM2PerSec * this._nodeFlat[n] * dt; // rain lands on the horizontal projection
|
||||
const cap = POND_MAX_KG_M2 * a;
|
||||
if (w[n] > cap) w[n] = cap;
|
||||
}
|
||||
flow[n] = 0;
|
||||
}
|
||||
@ -569,7 +567,17 @@ export class SailRig {
|
||||
const moved = Math.min(w[n], w[n] * POND_FLOW * bestGrad * dt);
|
||||
flow[n] -= moved; flow[best] += moved;
|
||||
}
|
||||
for (let n = 0; n < N2; n++) w[n] += flow[n];
|
||||
// The per-node ceiling is enforced AFTER the flow step, not on the rain-add:
|
||||
// the old rain-add clamp only ran while it was raining, so once the rain
|
||||
// stopped, downhill consolidation could quietly stack a basin node past
|
||||
// POND_MAX_KG_M2 with nothing left to trim it (the belly-tear guard below
|
||||
// only bounds the gross runaway). Water over the cap sheets off the cloth —
|
||||
// it leaves the sail, same as the rim spill; it is not conserved.
|
||||
for (let n = 0; n < N2; n++) {
|
||||
w[n] += flow[n];
|
||||
const a = this._nodeArea[n];
|
||||
if (a > 1e-9 && w[n] > POND_MAX_KG_M2 * a) w[n] = POND_MAX_KG_M2 * a;
|
||||
}
|
||||
|
||||
// A rim node spills over the edge ONLY when the edge is downhill — i.e. it
|
||||
// has no lower interior neighbour to send water to. A hypar's low corners
|
||||
@ -916,6 +924,22 @@ export class SailRig {
|
||||
for (let k = 0; k < 4; k++) {
|
||||
const c = this.corners[k];
|
||||
if (c.broken) continue;
|
||||
// A non-finite load can never trip the rating check below — NaN compares
|
||||
// false to everything — so a diverged corner would otherwise hold FOREVER
|
||||
// while NaN spreads node-to-node through the spring network. Divergence
|
||||
// is a failure, not weather: break the corner now, on the production
|
||||
// path, not just under the test-only watchDivergence tripwire. The event
|
||||
// carries reason:'divergence' so a log can tell it from an honest blow.
|
||||
if (!Number.isFinite(c.load)) {
|
||||
c.broken = true;
|
||||
c.overload = 0;
|
||||
c.load = 0;
|
||||
c.loadVec.x = c.loadVec.y = c.loadVec.z = 0;
|
||||
this.invMass[this.cornerIdx[k]] = 1 / this.nodeMass;
|
||||
this.dumpPond('corner blew');
|
||||
this.events.emit('break', { type: 'break', corner: c, anchorId: c.anchorId, hw: c.hw.name, t: this.t, reason: 'divergence' });
|
||||
continue;
|
||||
}
|
||||
if (c.load > c.hw.rating * (c.anchor.ratingHint ?? 1)) c.overload += dt;
|
||||
else c.overload = Math.max(0, c.overload - dt * OVERLOAD_RECOVER);
|
||||
if (c.overload > OVERLOAD_SECS) {
|
||||
|
||||
@ -197,6 +197,48 @@ test('sim stays finite through a full storm', () => {
|
||||
return `peak ${kN(r.corners.reduce((m, c) => Math.max(m, c.peakLoad), 0))}`;
|
||||
});
|
||||
|
||||
test('divergence BREAKS on the production path — a NaN corner cannot hold forever', () => {
|
||||
// watchDivergence stays OFF here on purpose: that tripwire is test-only, and
|
||||
// this test is about the SHIPPED path. Before the guard in _checkFailure,
|
||||
// `load > rating` was false for NaN — a diverged corner never broke, never
|
||||
// recovered, and the sail froze silently while NaN ate the spring network.
|
||||
const r = new SailRig({ anchors: makeAnchors(HEIGHTS_FLAT), gridN: 10 });
|
||||
r.attach(ALL_IDS, Array(4).fill(HARDWARE[2]), 1.0);
|
||||
const broke = [];
|
||||
r.events.on('break', (e) => broke.push(e));
|
||||
const w = makeStubWind({ stormLen: 90 });
|
||||
runStorm(r, w, 2);
|
||||
assert(broke.length === 0, 'rig should be healthy before the poison');
|
||||
// Poison one interior node — the shape a solver blow-up leaves behind.
|
||||
r.pos[(r.N + 1) * 3 + 1] = NaN;
|
||||
runStorm(r, w, 2);
|
||||
assert(broke.some((e) => e.reason === 'divergence'),
|
||||
`NaN spread through the cloth and no corner broke by divergence — the silent freeze is back (${broke.length} breaks)`);
|
||||
const b = r.corners.find((c) => c.broken);
|
||||
assert(b && b.load === 0, 'a corner broken by divergence must read load 0, not NaN, for the HUD');
|
||||
return `${broke.filter((e) => e.reason === 'divergence').length}/4 corners let go by divergence, HUD reads 0`;
|
||||
});
|
||||
|
||||
test('ponding: consolidation cannot stack one node past the cap (post-flow clamp)', () => {
|
||||
// The rain-add clamp only ran WHILE raining; once rain stopped, downhill
|
||||
// flow could pile a basin node arbitrarily high and nothing trimmed it.
|
||||
// Inject an absurd pond onto the settled belly and take one step: the
|
||||
// clamp must shed it — and it must be the CLAMP doing it, not a belly
|
||||
// tear, or the assert is measuring the wrong mechanism.
|
||||
const r = carportRig(UNBREAKABLE);
|
||||
const still = { sample: (_p, _t, out) => (out ? out.set(0, 0, 0) : { x: 0, y: 0, z: 0 }), rainMmPerHour: () => 0 };
|
||||
runStorm(r, still, 5); // settle: the belly is the basin
|
||||
const dumps = [];
|
||||
r.events.on('pondDump', (e) => dumps.push(e));
|
||||
const belly = Math.floor(r.N / 2) * r.N + Math.floor(r.N / 2);
|
||||
r.water[belly] = 2000; // ~7x any sane per-node cap
|
||||
r.step(SIM_DT, still, 5 + SIM_DT);
|
||||
assert(dumps.length === 0, `the belly tore (${dumps[0]?.reason}) — reduce the injection, this test is about the clamp`);
|
||||
assert(r.water[belly] < 400,
|
||||
`node ${belly} still holds ${r.water[belly].toFixed(0)} kg after a step — the per-node cap is not enforced once rain stops`);
|
||||
return `injected 2000 kg, one step later the node holds ${r.water[belly].toFixed(0)} kg, no tear`;
|
||||
});
|
||||
|
||||
test('sail sags under gravity when calm', () => {
|
||||
const r = rig(HEIGHTS_FLAT);
|
||||
runStorm(r, makeStubWind({ calm: true }), 6);
|
||||
@ -846,12 +888,17 @@ test('ponding: mass conserves until something dumps it', () => {
|
||||
const dry = { ...w, rainAt: () => 0, rainMmPerHour: () => 0 };
|
||||
runStorm(r, dry, 5);
|
||||
assert(r.pondMass() <= held + 1e-6, `pond GREW from ${held.toFixed(0)} to ${r.pondMass().toFixed(0)} kg with no rain`);
|
||||
// The old assert here compared `dumped` to itself through a helper that
|
||||
// returned its argument — a tautology that could not fail no matter what
|
||||
// dumpPond returned. Say the real thing: what it reports dropping is what
|
||||
// was on the cloth.
|
||||
const before = r.pondMass();
|
||||
assert(before > 1, `only ${before.toFixed(1)} kg left to dump — test is vacuous`);
|
||||
const dumped = r.dumpPond('test');
|
||||
assert(Math.abs(dumped - r_prev(r, dumped)) < 1e-9 || dumped > 0, 'dumpPond should report what it dropped');
|
||||
assert(Math.abs(dumped - before) < 1e-9, `dumpPond reported ${dumped.toFixed(1)} kg but ${before.toFixed(1)} kg was on the cloth`);
|
||||
assert(r.pondMass() === 0, 'dumpPond left water behind');
|
||||
return `held ${held.toFixed(0)} kg, dumped ${dumped.toFixed(0)} kg, sail now dry`;
|
||||
});
|
||||
function r_prev(_r, d) { return d; }
|
||||
|
||||
test('ponding: a blown corner tips the pond off (DESIGN.md sudden dump)', () => {
|
||||
const r = rig(HEIGHTS_FLAT, { hw: HARDWARE[1] });
|
||||
|
||||
Loading…
Reference in New Issue
Block a user