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() {