diff --git a/docs/superpowers/specs/2026-05-29-a8f-camera-collision-design.md b/docs/superpowers/specs/2026-05-29-a8f-camera-collision-design.md index 0cc1a21..269aa55 100644 --- a/docs/superpowers/specs/2026-05-29-a8f-camera-collision-design.md +++ b/docs/superpowers/specs/2026-05-29-a8f-camera-collision-design.md @@ -144,9 +144,13 @@ public Vector3 SweepEye(Vector3 pivot, Vector3 desiredEye, uint cellId, uint sel stepDownHeight: 0f, // no step-down / ground snap isOnGround: false, // no contact-plane / walkable semantics body: null, // no cross-frame contact-plane persistence - moverFlags: ObjectInfoState.None, // all targets collide; also keeps - // camera sweeps out of the #98 - // IsPlayer capture filter + // Retail init_object(player, 0x5c) = IsViewer|PathClipped|FreeRotate| + // PerfectClip (pseudo-C :92864). PathClipped = hard-stop at first contact + // (the spring arm, not edge-slide); IsViewer = eye passes through creatures, + // colliding only with world geometry. Not IsPlayer → stays out of the #98 + // capture filter. + moverFlags: ObjectInfoState.IsViewer | ObjectInfoState.PathClipped + | ObjectInfoState.FreeRotate | ObjectInfoState.PerfectClip, movingEntityId: selfEntityId); // skip the player's own ShadowEntry return r.Position + zoff; // r.Position = sp.CheckPos (path pt); + zoff = eye } @@ -264,11 +268,13 @@ Compare `ACDREAM_CAMERA_COLLIDE=0` vs default to confirm the flag isolates the f ## 11. Open implementation questions (decide during the plan, not now) -1. **Slide vs hard-stop.** Reusing `ResolveWithTransition` gives the player - path's edge-slide (the eye glides along a wall rather than jittering). Read - retail's `find_valid_position` during implementation and confirm whether it - slides or hard-stops; match it. Both keep the eye out of walls, so this does - not change the architecture. +1. **Slide vs hard-stop — RESOLVED (hard-stop).** Retail's `update_viewer` calls + `init_object(player, 0x5c)`, and `0x5c` includes `PathClipped`, which makes the + transition HARD-STOP at first contact (`TransitionTypes.cs:811`) rather than + edge-slide. The probe therefore passes + `IsViewer | PathClipped | FreeRotate | PerfectClip` (see §5.1). Hard-stop also + avoids the eye sliding around a corner into the next room, which would re-trigger + the A8.F camera-cell instability this fix targets. 2. **`sphereHeight: 0f` — RESOLVED.** `SpherePath.InitPath` with height 0 yields a single sphere (`NumSphere = 1`, `TransitionTypes.cs:534-537`). It also offsets sphere0's center to `pathPos + (0,0,radius)` (foot-capsule convention) whereas diff --git a/src/AcDream.App/Rendering/PhysicsCameraCollisionProbe.cs b/src/AcDream.App/Rendering/PhysicsCameraCollisionProbe.cs index 09ce5c1..a5e9e89 100644 --- a/src/AcDream.App/Rendering/PhysicsCameraCollisionProbe.cs +++ b/src/AcDream.App/Rendering/PhysicsCameraCollisionProbe.cs @@ -43,9 +43,17 @@ public sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe stepDownHeight: 0f, // no step-down / ground snap isOnGround: false, // no contact-plane / walkable semantics body: null, // no cross-frame persistence - moverFlags: ObjectInfoState.None, // all targets collide; also keeps - // camera sweeps out of the #98 - // IsPlayer capture filter + // Retail SmartBox::update_viewer calls init_object(player, 0x5c) = + // IsViewer | PathClipped | FreeRotate | PerfectClip (acclient + // pseudo-C :92864; enum TransitionTypes.cs:24-33). PathClipped makes + // the sweep HARD-STOP at first contact (TransitionTypes.cs:811) — the + // spring-arm pull-in, not the player's edge-slide. IsViewer lets the + // eye pass through creatures, colliding only with world geometry + // (CollisionExemption.cs:83-85). FreeRotate/PerfectClip are no-ops in + // acdream today but set to match retail's exact value. NOT IsPlayer + // (0x100), so camera sweeps stay out of the #98 capture filter. + moverFlags: ObjectInfoState.IsViewer | ObjectInfoState.PathClipped + | ObjectInfoState.FreeRotate | ObjectInfoState.PerfectClip, movingEntityId: selfEntityId); // skip the player's own ShadowEntry return FromSpherePath(r.Position, ViewerSphereRadius); diff --git a/tests/AcDream.Core.Tests/Rendering/CameraDiagnosticsTests.cs b/tests/AcDream.Core.Tests/Rendering/CameraDiagnosticsTests.cs index d89d1ad..b27c81c 100644 --- a/tests/AcDream.Core.Tests/Rendering/CameraDiagnosticsTests.cs +++ b/tests/AcDream.Core.Tests/Rendering/CameraDiagnosticsTests.cs @@ -21,6 +21,7 @@ public class CameraDiagnosticsTests CameraDiagnostics.CameraAdjustmentSpeed = 40.0f; CameraDiagnostics.AlignToSlope = true; CameraDiagnostics.UseRetailChaseCamera = true; + CameraDiagnostics.CollideCamera = true; Assert.Equal(0.45f, CameraDiagnostics.TranslationStiffness); Assert.Equal(0.45f, CameraDiagnostics.RotationStiffness); @@ -31,6 +32,7 @@ public class CameraDiagnosticsTests // legacy camera remains opt-in via the DebugPanel toggle until // the follow-up deletion commit. Assert.True(CameraDiagnostics.UseRetailChaseCamera); + Assert.True(CameraDiagnostics.CollideCamera); } [Fact]