fix(G.3): pin dungeon collapse to the cell's landblock, not the position-derived one (#133)

"The dungeon is broken" — the collapse was unloading the REAL dungeon. A dungeon's
EnvCells sit at arbitrary "ocean" world coords with negative cell-local Y (snap
showed pos=(58.9,-69.6) in cell 0x00070133), so the observer landblock
_liveCenterY + floor(pp.Y/192) = 7 + floor(-69.6/192) = 7 + (-1) = 6 lands one row
off. The collapse pinned to 0x0006 and unloaded 0x0007 — the real dungeon — which
nulled CurrCell (the cell no longer existed) and left the player floating in
outdoor-lit empty space (lb 1/1 @ ~1585 fps, but the wrong landblock). This is the
Bug-A negative-local-coordinate class.

Fix: when inside a dungeon, pin the collapse to the cell's OWN landblock
(CurrCell.Id >> 16), never the position-derived observer landblock — the cell id is
the authoritative landblock for ocean-placed dungeon geometry.

Also hardened the hysteresis so a transient CurrCell flicker can't thrash:
- Re-collapse when insideDungeon at a DIFFERENT landblock (multi-landblock dungeon).
- Expand only on a DISTANT move (Chebyshev > 1) — a real exit teleports far from the
  ocean-grid block; the off-by-one flicker is always an ADJACENT (±1) landblock, so
  it now HOLDS the collapse instead of expanding.
- SweepCollapsed always preserves _collapsedCenter (the true dungeon landblock),
  never the per-frame observer landblock.

Build green; 59 streaming tests green (flicker regression test updated to the
realistic adjacent off-by-one).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-13 22:51:50 +02:00
parent d9e7dd65e9
commit 2561918a70
3 changed files with 52 additions and 18 deletions

View file

@ -110,22 +110,24 @@ public class StreamingControllerDungeonGateTests
}
[Fact]
public void Collapsed_CurrCellFlickersToNull_SameLandblock_DoesNotExpand()
public void Collapsed_CurrCellFlickersToAdjacentOffByOne_DoesNotExpand()
{
// Regression: the live run thrashed collapse↔expand because CurrCell
// momentarily resolved to null (insideDungeon=false) while the player
// stayed in the dungeon landblock — leaking lights via reload storms.
// The landblock-hysteresis must hold the collapse.
// Regression: the live run broke because a dungeon cell's negative local-Y
// makes the position-derived observer landblock land one row off (0,7→0,6).
// When CurrCell flickers null mid-frame, GameWindow stops overriding to the
// cell landblock and passes that adjacent (0,6). The Chebyshev>1 guard must
// treat that as a flicker and HOLD — never expand (which would unload the
// real dungeon and re-stream the 25×25 neighbor window).
var h = Make();
h.State.AddLandblock(MakeLb(0, 7));
h.Ctrl.Tick(0, 7, insideDungeon: true); // collapse
h.Ctrl.Tick(0, 7, insideDungeon: true); // collapse onto the dungeon (0,7)
h.Loads.Clear();
h.Unloads.Clear();
h.Ctrl.Tick(0, 7, insideDungeon: false); // CurrCell flicker, same landblock
h.Ctrl.Tick(0, 6, insideDungeon: false); // flicker → adjacent off-by-one
Assert.Empty(h.Loads); // NO full-window reload
Assert.Empty(h.Unloads); // only the center is resident → nothing to sweep
Assert.Empty(h.Unloads); // dungeon (0,7) preserved; nothing else resident
}
[Fact]