From 80a33ba52c328ebc687467c67ac9f08d83ec65f7 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sun, 8 Nov 2026 13:48:55 -0800 Subject: [PATCH] fix(cockpit): a coarse read never takes a river bridge back to the sea Before the first full-detail water read lands (it can take many seconds from cold tiles), a refresh's loaded-tile read with one low probe still dropped a river height to the geoid. Before that landing, loaded-tile reads may now find raised water but only a full-detail read lowers it. CURRENT-STATE also says what the earlier fixes changed: C stays the CCTV shortcut when the bridge cannot be entered, voice entry is refused on a bridge, and the ridden ship's trail is hidden with its sprite. --- docs/CURRENT-STATE.md | 15 ++++++++++----- src/ui/cockpitBridge.test.mjs | 24 ++++++++++++++++++++++++ src/ui/cockpitCamera.js | 14 ++++++++++---- 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 372fd4dc9..b5174c081 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1530,16 +1530,20 @@ With no aircraft tracked, Cockpit can ride the selected AIS vessel instead. Entry needs only the layer of the vessel it rides, Live AIS Vessels, and a selected vessel; Contacts and every other layer may be off, before or during the bridge (`bridgeEntryAllowed` in `src/contextModePolicy.js`). A Context mode -change still in flight blocks entry until it settles. `C` enters it, and the +change still in flight blocks entry until it settles. `C` enters it (when +the bridge cannot be entered, `C` stays the CCTV shortcut), and the entry button reads BRIDGE with a ship icon both in Contacts (`#cockpit-entry`) and under the selected vessel's Intel HUD readout in the bottom-right corner (`#cockpit-entry-readout`, shown with the HUD on, in its Tactical and Cyber layouts). Turning Live AIS Vessels off ends the bridge: the layer then has no subject (`getCockpitSubject` returns null). The bridge picture uses AIS alone, so no other feed being off makes it partial. -Voice (`control_cockpit`) and the selection entry transaction stay aircraft-only. +Voice (`control_cockpit`) and the selection entry transaction stay aircraft-only; +voice `enter` on a bridge is refused before it touches tracking, with the +reason to exit the bridge first. The vessel layer supplies the subject (`getCockpitSubject`), hides that ship's -sprite, card and detection bracket while it is held (`setCockpitSubject`), and +sprite, card, detection bracket and trail while it is held +(`setCockpitSubject`), and ignores scene clicks so open water cannot deselect it. The eye sits on the AIS position at a representative bridge height for the AIS type family (`src/layers/vessels/cockpitSubject.js`: cargo and tanker 30 m, passenger 25 m, @@ -1569,8 +1573,9 @@ eight probes 30 m out, the lowest probe taken as the water when it is 2 m to the full-detail read replaces it; it repeats every 5 s. Only one full-detail read runs at a time: while one is still running, a refresh reads the loaded tiles instead, and the slow read applies whenever it lands. Once a -full-detail read has answered, loaded-tile reads no longer change the height, -and a read that sampled nothing keeps the height it had. A failed read keeps +full-detail read has answered, loaded-tile reads no longer change the height; +before then they may find raised water but never take it back to the sea. +A read that sampled nothing keeps the height it had. A failed read keeps the sea surface. The HUD keeps its frame, vision styles, briefing, weather and Display/Radio diff --git a/src/ui/cockpitBridge.test.mjs b/src/ui/cockpitBridge.test.mjs index 86bd639f0..13a0ced35 100644 --- a/src/ui/cockpitBridge.test.mjs +++ b/src/ui/cockpitBridge.test.mjs @@ -1350,6 +1350,30 @@ test('a river height stands through reads that sample nothing or one coarse prob ); }); +test('before any full read lands, a coarse loaded read keeps the river it found', async (t) => { + const clock = manualClock(t, START_MS); + const sea = await bridgeAt(t, clock); + frames(sea.cockpit, clock, sea.shots, 60); + const seaEyeM = eyeHeightM(sea.shots.at(-1)); + sea.cockpit.exit(); + + clock.set(START_MS); + const record = { ...RECORD, lastPositionEpoch: START_MS / 1000 }; + const ship = fixture(t, { record }); + // The full-detail read is cold and never lands in this test. + ship.viewer.scene.sampleHeightMostDetailed = () => new Promise(() => {}); + ship.viewer.scene.sampleHeight = () => 27; + ship.cockpit.enter(); + await settle(); + ship.cockpit.update(); + frames(ship.cockpit, clock, ship.shots, 60); + near(eyeHeightM(ship.shots.at(-1)) - seaEyeM, 27, 'on the river', 0.5); + // A refresh meets one low probe on coarse tiles. + ship.viewer.scene.sampleHeight = () => 0.5; + frames(ship.cockpit, clock, ship.shots, 240); + near(eyeHeightM(ship.shots.at(-1)) - seaEyeM, 27, 'still on the river', 0.5); +}); + test('a client clock minutes fast does not age the bridge into a stale feed', async (t) => { // The feed's newest fix is this ship's; the client clock runs 200 s // ahead of the feed and received the refresh just now. diff --git a/src/ui/cockpitCamera.js b/src/ui/cockpitCamera.js index 2a70ef280..d5c268b35 100644 --- a/src/ui/cockpitCamera.js +++ b/src/ui/cockpitCamera.js @@ -110,9 +110,10 @@ export function update() { * every frame until it settles): while one is still running, a refresh * reads the loaded tiles instead, and the slow read applies whenever it * lands. Once a full-detail read has answered, only full-detail reads - * change the height: a loaded-tile read stands in only until the first - * lands, so one coarse probe cannot drop a river bridge to the sea, and a - * read that sampled nothing keeps the height it had. + * change the height; before then a loaded-tile read may find raised water + * but never takes a raised height back to the sea. One coarse probe cannot + * drop a river bridge to the sea, and a read that sampled nothing keeps + * the height it had. * @param {{latitude: number, longitude: number}} [at] Where to read the * water: the dead-reckoned position the camera rides. * @returns {number} Ellipsoidal metres. @@ -145,7 +146,12 @@ function vesselSurfaceM(info, nowMs, at = info) { read.sampledMs = nowMs; if (read.inFlight && !read.landed) { const loadedM = loadedWaterlineM(scene, probes, seaM); - if (Number.isFinite(loadedM)) read.heightM = loadedM; + // Taking a river back to the sea is the full-detail read's to say. + const lowers = + Number.isFinite(read.heightM) && + read.heightM > seaM && + !(loadedM > seaM); + if (Number.isFinite(loadedM) && !lowers) read.heightM = loadedM; } } if (!read.inFlight) readWaterline.call(this, read, scene, probes, seaM);