From 670443eea0c0bcd45e85a662eaf6f8db0c308aa1 Mon Sep 17 00:00:00 2001 From: m3ultra Date: Fri, 17 Jul 2026 03:32:02 +1000 Subject: [PATCH] Guard pond accessors before attach; caught driving the live game MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pondMass/pondCentroid/drainPondAt/dumpPond threw when this.water didn't exist yet — Lane A's HUD reads pondMass() every frame, including in the forecast/prep phases before the sail is rigged. Node tests always attach first so they never hit it; the live game did on the first frame. Guarded + a pre-attach assert so it can't regress. Verified in the assembled game: a flat quad ponds 780 kg (belly sagging below ground), dumps on break, and the merged browser selftest is 219/0/0. Co-Authored-By: Claude Opus 4.8 --- web/world/js/sail.js | 2 ++ web/world/js/sail.selftest.js | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/web/world/js/sail.js b/web/world/js/sail.js index a3705cc..5edec71 100644 --- a/web/world/js/sail.js +++ b/web/world/js/sail.js @@ -582,6 +582,7 @@ export class SailRig { /** Total water on the sail, kg. Lane A's "SAIL PONDING — get the broom" number. */ pondMass() { + if (!this.water) return 0; // the HUD may read this before the sail is rigged let m = 0; for (let n = 0; n < this.water.length; n++) m += this.water[n]; return m; @@ -593,6 +594,7 @@ export class SailRig { * @returns {{x:number,y:number,z:number,mass:number,node:number}|null} */ pondCentroid() { + if (!this.water) return null; let m = 0, x = 0, y = 0, z = 0, node = -1, hw = 0; for (let n = 0; n < this.water.length; n++) { const q = this.water[n]; diff --git a/web/world/js/sail.selftest.js b/web/world/js/sail.selftest.js index 880bea1..dc66050 100644 --- a/web/world/js/sail.selftest.js +++ b/web/world/js/sail.selftest.js @@ -879,6 +879,17 @@ test('ponding: the broom SAVES a flat rig that water would have killed', () => { return `kept 4/4 by sweeping the belly; unswept the same rig loses corners to water`; }); +test('ponding: pond accessors are safe before the sail is rigged', () => { + // Caught live: Lane A's HUD reads pondMass() every frame, including before the + // player has rigged anything — and this.water doesn't exist until attach(). + const bare = new SailRig({ anchors: makeAnchors(HEIGHTS_FLAT) }); + assert(bare.pondMass() === 0, 'pondMass threw / was non-zero on an unrigged sail'); + assert(bare.pondCentroid() === null, 'pondCentroid should be null on an unrigged sail'); + assert(bare.drainPondAt(0, SIM_DT) === 0, 'drainPondAt should no-op on an unrigged sail'); + assert(bare.dumpPond() === 0, 'dumpPond should no-op on an unrigged sail'); + return 'pondMass/centroid/drain/dump all safe pre-attach'; +}); + test('ponding: rain that the router swallows cannot silently pass', () => { // The integrator caught the wind router dropping the rain API this sprint, // which would have made every test above pass while ponding did nothing in the