Commit Graph

5 Commits

Author SHA1 Message Date
m3ultra
1ace50da0f Merge Sprint 2 lanes; wire debris into sail step; settle downdraft dispute as documented skip
Selftest on merged main: 169 pass / 0 fail.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 01:09:51 +10:00
m3ultra
2034593a30 Re-run the §7 gate against real storm wind; stop allocating per face
B-4. The gate used to run on my own stub wind — uniform, horizontal,
tuned by nobody — so it proved the cloth was self-consistent, not that
the game works. It now loads the shipped storm JSON and drives the cloth
through weather.core.js (pure and import-free, so the same path runs in
node and in selftest.html; weather.js's own loadStorm can't help because
its STORM_DIR is a file:// URL that node's fetch won't open).

All three halves of §7 now run on real storm_02 and the real yard:
a flat drum-tight carabiner rig cascades 4/4; a well-twisted mixed rig
holds all four; and a twisted rig with one dodgy corner blows it and
finishes 4/4 intact after a single repair — the DoD scenario, in an
assert.

Perf: Lane C added an `out` param to wind.sample specifically so sail.js
wouldn't allocate, and I wasn't passing it — 162 faces at 60 Hz is ~9.7k
throwaway Vector3s a second. Now passing a scratch vector. Stub winds
that ignore `out` still work; we read the return value.

Two tests skip rather than pass vacuously while Lane C's downdraft is
unmerged: a "nothing broke" repair scenario would go green forever and
check nothing. Both light up by themselves on merge, and the repair one
hard-fails if a downdraft IS present and still can't threaten the rig.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 00:25:25 +10:00
m3ultra
7e8ffa307c Land decisions 4 and 5: Lane D's rig seam, debris impulses
Decision 4 — conform to Lane D's call sites rather than the reverse (D
landed first and duck-typed them): repair(i), trim(i, delta) and
cornerPos(i). repair takes no hardware argument because prep sells
exactly one kind of spare, so it re-rigs at shackle grade — an upgrade
on a blown carabiner, a downgrade on a blown rated shackle, which is the
prototype's behaviour and a real choice about which corner you run to.
cornerPos returns a fresh vector at the live node, so a blown corner's
prompt chases the flogging corner instead of sitting on a dead anchor
(measured: 13 m off). All three are contract entries now, not PROPOSED
comments, so the merge tripwire enforces the seam.

Decision 5 — sail.step() takes an optional debris and applies sphere-vs-
cloth impulses. The exchange is symmetric: every newton-second the cloth
takes out of a crate, the crate loses. Asserted, and it conserves to
0.000% on an interior hit. Pinned corners are the deliberate exception —
invMass 0 means a crate off a corner dumps its momentum into the house,
which is correct, the anchor is bolted to a wall.

Note this leaves debris.js's applyToSail dead: it guards on `sail.nodes`,
which never existed on the rig — the cloth stores Float64Arrays. So the
debris-vs-sail impulse has been silently doing nothing in the assembled
game. Decision 5 puts it on this side; flagged for Lane C in THREADS.

The contact radius is swept by the piece's travel because main.js steps
the sail before the debris (so piece positions are a frame stale) and a
0.3 m crate at 25 m/s covers 0.42 m per frame — enough to pass clean
between cloth nodes.

Also: coverageOver() rays now start at heightAt(x,z) rather than y=0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 23:58:14 +10:00
m3ultra
18099c8e6f Align sail lane to contracts.js; free blown corners so they flog
Rebased onto M0 and reconciled against the real spine. checkContract
('sailRig') now conforms and js/tests/b.test.js runs 28 asserts green.

Contract fixes:
  - anchor.sway(t) is the ABSOLUTE position, not an offset (thanks A —
    I had it adding sway to pos, which would have flung every
    tree-anchored corner to double its coordinates).
  - events is an Emitter emitting {type, corner}, not a drained array.
  - coverageOver() rects are centre+size, matching world.gardenBed. It
    consumes world.sunDir directly: a hit along sunDir means shaded.
  - START_BUDGET/SPARE_COST/HARDWARE/FIXED_DT now come from contracts.js
    rather than being redeclared here.

Bug: a corner that blew was marked broken but never had its mass
returned, so invMass stayed 0 and the "blown" corner sat welded in
mid-air — no flogging, and the sail silently went dead. PLAN3D §5-B
wants flogging emergent from the freed node, so _checkFailure now frees
it. The cascade test missed this because it called _repin() by hand;
the new test drives a real overload failure instead and asserts the
corner tears 2 m off its anchor and keeps moving.

Tension dial remapped from the prototype's rest/tension to a real
pre-strain. rest/tension asks for 17% strain at dial 1.2 and 29% at 1.4
— stretching an 18 m sail by three metres — and put 68 kN on a corner of
the yard's biggest quad with no wind blowing. At 0.10 strain-per-dial it
swings a 5x5 rig's peak load 2.1x loose-to-tight and redlines a 192 m2
quad at 8.3 kN drum-tight, which is punishing and correct.

HARDWARE ratings retuned in contracts.js to real newtons per the
standing note there that Lane B owns these numbers. Costs and tier shape
untouched; $80 still buys rated hardware on at most 2 of 4 corners.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 21:55:37 +10:00
m3ultra
06ec4cbea2 Add 3D sail cloth sim with per-face wind and XPBD corner loads
Verlet cloth on a bilinear patch between 4 anchors, N=10 grid,
structural/shear/bend constraints, 5 relaxation iterations at a fixed
1/60 substep. Wind is applied per FACE so hypar twist genuinely sheds
load rather than being cosmetic.

Two deviations from PLAN3D worth flagging:

- Load is read from each constraint's XPBD Lagrange multiplier, not from
  FABRIC_K * leftover-stretch. After a fixed iteration count the leftover
  stretch is solver error, not fabric strain, so the naive reading came
  out ~50x hot (60 kN peaks on a 5x5 m sail). The multiplier is the real
  constraint impulse, which the statics assert confirms by balancing the
  corner reactions against the applied wind to 8%.

- Wind uses a signed square (d*|d|) rather than clamp(d)^2, so the
  leeward face is pushed too. A sail is double-sided.

The sim core deliberately does not import three.js: it runs headless
under node today, stays allocation-free in the hot loop, and replays
bit-for-bit. createSailView() pulls three in lazily for rendering.

Loads land in real newtons (~1-4 kN on a 5x5 m sail in a 34 m/s storm),
so hardware ratings are real working load limits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 21:55:37 +10:00