From d03fe84845f386e9a87c5a736a47d0b88e15d7d9 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 3 Jun 2026 12:34:07 +0200 Subject: [PATCH] =?UTF-8?q?feat(render):=20RetailChaseCamera.ViewerCellId?= =?UTF-8?q?=20=E2=80=94=20the=20swept=20viewer=20cell=20(retail=20viewer?= =?UTF-8?q?=5Fcell)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update() now always sets ViewerCellId: the camera-collision sweep's swept cell when collision is on (retail viewer_cell = sphere_path.curr_cell), else the passed player cell. This is the robust, per-frame, graph-tracked 'which cell is the camera in?' answer that V1 roots the render on — no AABB, no grace frames (the U.4c flap source). 176 App tests green (2 new). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Rendering/RetailChaseCamera.cs | 18 ++++++++++- .../Rendering/RetailChaseCameraTests.cs | 32 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/AcDream.App/Rendering/RetailChaseCamera.cs b/src/AcDream.App/Rendering/RetailChaseCamera.cs index 993999a..d05b907 100644 --- a/src/AcDream.App/Rendering/RetailChaseCamera.cs +++ b/src/AcDream.App/Rendering/RetailChaseCamera.cs @@ -27,6 +27,14 @@ public sealed class RetailChaseCamera : ICamera { // ICamera surface. public Vector3 Position { get; private set; } + + /// + /// The cell the collided viewer-sphere ended in (retail viewer_cell = + /// sphere_path.curr_cell). Roots the render mode + indoor visibility + the portal + /// side-test in (Phase W single-viewpoint V1) — the ONE viewpoint. + /// Equals the passed player cell when camera collision is off / the probe is null. + /// + public uint ViewerCellId { get; private set; } public float Aspect { get; set; } = 16f / 9f; public float FovY { get; set; } = MathF.PI / 3f; public Matrix4x4 View { get; private set; } = Matrix4x4.Identity; @@ -152,8 +160,16 @@ public sealed class RetailChaseCamera : ICamera // eye is pressed against a wall. So collide into a separate local and // leave _dampedEye as the clean, uncollided sought position. Vector3 publishedEye = _dampedEye; + // The viewer cell defaults to the player cell (collision off / null probe); the sweep + // overwrites it with the swept cell (retail viewer_cell). Always set so GameWindow has a + // robust per-frame "which cell is the camera in?" answer. + ViewerCellId = cellId; if (CameraDiagnostics.CollideCamera && CollisionProbe is not null) - publishedEye = CollisionProbe.SweepEye(pivotWorld, _dampedEye, cellId, selfEntityId).Eye; + { + var swept = CollisionProbe.SweepEye(pivotWorld, _dampedEye, cellId, selfEntityId); + publishedEye = swept.Eye; + ViewerCellId = swept.ViewerCellId; + } // 6. Publish renderer surface (from the collided eye; rotation stays the // smoothly-damped look direction toward the pivot). diff --git a/tests/AcDream.App.Tests/Rendering/RetailChaseCameraTests.cs b/tests/AcDream.App.Tests/Rendering/RetailChaseCameraTests.cs index 0e58afb..989dcb2 100644 --- a/tests/AcDream.App.Tests/Rendering/RetailChaseCameraTests.cs +++ b/tests/AcDream.App.Tests/Rendering/RetailChaseCameraTests.cs @@ -477,6 +477,38 @@ public class RetailChaseCameraTests Assert.Equal(collided, cam.Position); } + [Fact] + public void Update_WithProbeAndFlagOn_ExposesSweptViewerCell() + { + CameraDiagnostics.CollideCamera = true; + var probe = new FakeProbe { ReturnEye = new Vector3(1f, 2f, 3f), ReturnCell = 0xA9B40170u }; + var cam = new RetailChaseCamera { CollisionProbe = probe }; + + cam.Update( + playerPosition: Vector3.Zero, playerYaw: 0f, playerVelocity: Vector3.Zero, + isOnGround: true, contactPlaneNormal: Vector3.UnitZ, dt: 1f / 60f, + cellId: 0xA9B40171u, selfEntityId: 0x5); + + Assert.Equal(0xA9B40170u, cam.ViewerCellId); // the swept cell, not the player cell + } + + [Fact] + public void Update_FlagOff_ViewerCellFallsBackToPlayerCell() + { + CameraDiagnostics.CollideCamera = false; + try + { + var cam = new RetailChaseCamera { CollisionProbe = new FakeProbe { ReturnCell = 0xDEADu } }; + cam.Update( + playerPosition: Vector3.Zero, playerYaw: 0f, playerVelocity: Vector3.Zero, + isOnGround: true, contactPlaneNormal: Vector3.UnitZ, dt: 1f / 60f, + cellId: 0xA9B40171u, selfEntityId: 0x5); + + Assert.Equal(0xA9B40171u, cam.ViewerCellId); // collision off → the passed player cell + } + finally { CameraDiagnostics.CollideCamera = true; } + } + [Fact] public void Update_FlagOff_DoesNotConsultProbe() {