using AcDream.UI.Abstractions.Input; namespace AcDream.UI.Abstractions.Tests.Input; /// /// Phase K.2 — MMB-hold instant mouse-look state machine. While /// active, mouse-X drives the character's heading (and the chase /// camera follows automatically because ChaseCamera.Update /// reads the player yaw). The state machine guards activation on /// ImGui's WantCaptureMouse so a panel-hovered MMB doesn't /// hijack the cursor. /// public sealed class MmbMouseLookTests { private sealed class YawSink { public float Total; public int ApplyCount; public void Apply(float d) { Total += d; ApplyCount++; } } [Fact] public void Press_ActivatesAndCapturesCursorPosition() { var sink = new YawSink(); var ml = new MouseLookState(sink.Apply); ml.Press(cursorX: 320f, cursorY: 240f, wantCaptureMouse: false); Assert.True(ml.Active); Assert.Equal(320f, ml.CapturedCursorX); Assert.Equal(240f, ml.CapturedCursorY); } [Fact] public void Release_DeactivatesWhenActive() { var sink = new YawSink(); var ml = new MouseLookState(sink.Apply); ml.Press(0f, 0f, wantCaptureMouse: false); ml.Release(); Assert.False(ml.Active); } [Fact] public void Press_WhileImGuiCapturesMouse_DoesNotActivate() { // Defense in depth — the dispatcher already filters on // WantCaptureMouse, but if a binding ever fires through the // state machine itself must not turn on. var sink = new YawSink(); var ml = new MouseLookState(sink.Apply); ml.Press(0f, 0f, wantCaptureMouse: true); Assert.False(ml.Active); } [Fact] public void OnWantCaptureMouseChanged_TrueWhileActive_Deactivates() { var sink = new YawSink(); var ml = new MouseLookState(sink.Apply); ml.Press(0f, 0f, wantCaptureMouse: false); Assert.True(ml.Active); // ImGui takes mouse focus mid-hold (e.g. tooltip pop-up over // the cursor) → suspend mouse-look so the panel gets the // cursor. ml.OnWantCaptureMouseChanged(wantCaptureMouse: true); Assert.False(ml.Active); } [Fact] public void OnWantCaptureMouseChanged_FalseWhileInactive_NoOp() { // Going from "ImGui captures" to "ImGui doesn't capture" must // NOT auto-reactivate — only an explicit Press does. var sink = new YawSink(); var ml = new MouseLookState(sink.Apply); ml.OnWantCaptureMouseChanged(wantCaptureMouse: false); Assert.False(ml.Active); } [Fact] public void ApplyDelta_WhileActive_DrivesYawSink() { var sink = new YawSink(); var ml = new MouseLookState(sink.Apply) { SensitivityRadiansPerPixel = 0.01f }; ml.Press(0f, 0f, wantCaptureMouse: false); ml.ApplyDelta(dx: 10f, extraSensitivity: 1.0f); // Sign: dragging right (positive dx) yaws the character right // — by the acdream Yaw convention that's negative yaw delta. // Magnitude: 10 * 0.01 * 1.0 = 0.1 rad. Assert.Equal(1, sink.ApplyCount); Assert.Equal(-0.1f, sink.Total, 5); } [Fact] public void ApplyDelta_WhileInactive_DoesNothing() { var sink = new YawSink(); var ml = new MouseLookState(sink.Apply); // Never pressed. ml.ApplyDelta(dx: 100f, extraSensitivity: 1.0f); Assert.Equal(0, sink.ApplyCount); Assert.Equal(0f, sink.Total); } [Fact] public void ApplyDelta_AfterRelease_DoesNothing() { var sink = new YawSink(); var ml = new MouseLookState(sink.Apply); ml.Press(0f, 0f, wantCaptureMouse: false); ml.Release(); ml.ApplyDelta(dx: 50f, extraSensitivity: 1.0f); Assert.Equal(0, sink.ApplyCount); } [Fact] public void Press_WhileAlreadyActive_IsIdempotent() { // Repeated Press calls must not blow away the originally // captured cursor position — the cursor-restore-on-release // path needs the original anchor. var sink = new YawSink(); var ml = new MouseLookState(sink.Apply); ml.Press(100f, 200f, wantCaptureMouse: false); ml.Press(999f, 888f, wantCaptureMouse: false); Assert.Equal(100f, ml.CapturedCursorX); Assert.Equal(200f, ml.CapturedCursorY); } [Fact] public void ApplyDelta_DriverDrivesCharacterAndCameraInOneSink() { // Combined drive: the test only exposes a single yaw mutator, // and that's the design — the yaw mutator GameWindow passes // updates _playerController.Yaw, and the chase camera reads // the same yaw via ChaseCamera.Update(pos, playerYaw). So a // single sink in this test models both behaviors. var sink = new YawSink(); var ml = new MouseLookState(sink.Apply) { SensitivityRadiansPerPixel = 0.005f }; ml.Press(0f, 0f, wantCaptureMouse: false); ml.ApplyDelta(dx: 4f, extraSensitivity: 0.5f); // -0.01 ml.ApplyDelta(dx: -2f, extraSensitivity: 0.5f); // +0.005 Assert.Equal(2, sink.ApplyCount); Assert.Equal(-0.005f, sink.Total, 5); } }