From 91086adbac2c6ab6f4f3ddd48d2f42af4c3a2c98 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 18 May 2026 20:04:34 +0200 Subject: [PATCH] feat(camera): InputAction + DebugVM surface for retail chase camera Four new InputAction entries for held-key offset integration (CameraZoomIn/Out, CameraRaise/Lower; default unbound). Six new DebugVM mirror properties forwarding to CameraDiagnostics so the upcoming "Chase camera" DebugPanel section can drive them live. Also folds in four small cleanups from the Task 4 code review: - Both CameraDiagnostics-mutating tests in CameraControllerTests now use try/finally save/restore (consistency with Task-3 follow-up B) - Drop unused `using System.Numerics` from CameraControllerTests - Reword the XML doc on CameraController.Active to explain WHY both cameras are held simultaneously (flag flip takes effect on the next Active access without re-entry) rather than restating the getter logic Co-Authored-By: Claude Opus 4.7 (1M context) --- src/AcDream.App/Rendering/CameraController.cs | 15 ++-- .../Input/InputAction.cs | 10 +++ .../Panels/Debug/DebugVM.cs | 45 +++++++++++ .../Rendering/CameraControllerTests.cs | 78 +++++++++++++------ 4 files changed, 115 insertions(+), 33 deletions(-) diff --git a/src/AcDream.App/Rendering/CameraController.cs b/src/AcDream.App/Rendering/CameraController.cs index 1673ba0..c855a9a 100644 --- a/src/AcDream.App/Rendering/CameraController.cs +++ b/src/AcDream.App/Rendering/CameraController.cs @@ -11,11 +11,11 @@ public sealed class CameraController public RetailChaseCamera? RetailChase { get; private set; } /// - /// The renderer-facing active camera. In chase mode, returns - /// when - /// is true, - /// otherwise . In fly mode returns - /// ; default is . + /// The renderer-facing active camera. Both the legacy and retail + /// chase cameras are held simultaneously so that flipping + /// takes effect + /// on the very next access to this property — no re-entry required, + /// no notification mechanism, no stale state. /// public ICamera Active { @@ -53,9 +53,8 @@ public sealed class CameraController } /// - /// Enter chase mode with both candidate cameras. Both are held; - /// picks based on - /// . + /// Store both cameras simultaneously; picks + /// between them per-read via the flag — no re-entry needed on flip. /// public void EnterChaseMode(ChaseCamera legacy, RetailChaseCamera retail) { diff --git a/src/AcDream.UI.Abstractions/Input/InputAction.cs b/src/AcDream.UI.Abstractions/Input/InputAction.cs index a922353..9beb39f 100644 --- a/src/AcDream.UI.Abstractions/Input/InputAction.cs +++ b/src/AcDream.UI.Abstractions/Input/InputAction.cs @@ -262,4 +262,14 @@ public enum InputAction /// Fly-camera descend (Ctrl) — only meaningful while fly camera /// is active. K.1b binds it to ControlLeft; K.1c may rebind. AcdreamFlyDown, + + // ── AcdreamCameraCommands ───────────────────────────── + /// Camera zoom in (held key, integrates Distance−= adjSpeed·dt). Default unbound. + CameraZoomIn, + /// Camera zoom out (held key, integrates Distance+= adjSpeed·dt). Default unbound. + CameraZoomOut, + /// Camera raise (held key, integrates Pitch+= adjSpeed·dt·0.02). Default unbound. + CameraRaise, + /// Camera lower (held key, integrates Pitch−= adjSpeed·dt·0.02). Default unbound. + CameraLower, } diff --git a/src/AcDream.UI.Abstractions/Panels/Debug/DebugVM.cs b/src/AcDream.UI.Abstractions/Panels/Debug/DebugVM.cs index ef99a25..baf1055 100644 --- a/src/AcDream.UI.Abstractions/Panels/Debug/DebugVM.cs +++ b/src/AcDream.UI.Abstractions/Panels/Debug/DebugVM.cs @@ -1,6 +1,7 @@ using System.Numerics; using AcDream.Core.Combat; using AcDream.Core.Physics; +using AcDream.Core.Rendering; namespace AcDream.UI.Abstractions.Panels.Debug; @@ -290,6 +291,50 @@ public sealed class DebugVM set => PhysicsDiagnostics.ProbeAutoWalkEnabled = value; } + // ── Chase camera tunables (forward to CameraDiagnostics) ────────── + + /// Runtime mirror of . + public bool UseRetailChaseCamera + { + get => CameraDiagnostics.UseRetailChaseCamera; + set => CameraDiagnostics.UseRetailChaseCamera = value; + } + + /// Runtime mirror of . + public bool CameraAlignToSlope + { + get => CameraDiagnostics.AlignToSlope; + set => CameraDiagnostics.AlignToSlope = value; + } + + /// Runtime mirror of . + public float CameraTranslationStiffness + { + get => CameraDiagnostics.TranslationStiffness; + set => CameraDiagnostics.TranslationStiffness = value; + } + + /// Runtime mirror of . + public float CameraRotationStiffness + { + get => CameraDiagnostics.RotationStiffness; + set => CameraDiagnostics.RotationStiffness = value; + } + + /// Runtime mirror of . + public float CameraMouseLowPassWindowSec + { + get => CameraDiagnostics.MouseLowPassWindowSec; + set => CameraDiagnostics.MouseLowPassWindowSec = value; + } + + /// Runtime mirror of . + public float CameraAdjustmentSpeed + { + get => CameraDiagnostics.CameraAdjustmentSpeed; + set => CameraDiagnostics.CameraAdjustmentSpeed = value; + } + // ── Action hooks invoked by panel buttons ────────────────────────── /// diff --git a/tests/AcDream.App.Tests/Rendering/CameraControllerTests.cs b/tests/AcDream.App.Tests/Rendering/CameraControllerTests.cs index d16b879..7bddfe5 100644 --- a/tests/AcDream.App.Tests/Rendering/CameraControllerTests.cs +++ b/tests/AcDream.App.Tests/Rendering/CameraControllerTests.cs @@ -1,4 +1,3 @@ -using System.Numerics; using AcDream.App.Rendering; using AcDream.Core.Rendering; using Xunit; @@ -21,47 +20,76 @@ public class CameraControllerTests [Fact] public void ChaseMode_WhenFlagOff_ActiveIsLegacy() { - CameraDiagnostics.UseRetailChaseCamera = false; - var (ctl, legacy, _) = MakeChaseFixture(); - Assert.Same(legacy, ctl.Active); - Assert.True(ctl.IsChaseMode); + bool saved = CameraDiagnostics.UseRetailChaseCamera; + try + { + CameraDiagnostics.UseRetailChaseCamera = false; + var (ctl, legacy, _) = MakeChaseFixture(); + Assert.Same(legacy, ctl.Active); + Assert.True(ctl.IsChaseMode); + } + finally + { + CameraDiagnostics.UseRetailChaseCamera = saved; + } } [Fact] public void ChaseMode_WhenFlagOn_ActiveIsRetail() { - CameraDiagnostics.UseRetailChaseCamera = true; - var (ctl, _, retail) = MakeChaseFixture(); - Assert.Same(retail, ctl.Active); - Assert.True(ctl.IsChaseMode); - - // Reset. - CameraDiagnostics.UseRetailChaseCamera = false; + bool saved = CameraDiagnostics.UseRetailChaseCamera; + try + { + CameraDiagnostics.UseRetailChaseCamera = true; + var (ctl, _, retail) = MakeChaseFixture(); + Assert.Same(retail, ctl.Active); + Assert.True(ctl.IsChaseMode); + } + finally + { + CameraDiagnostics.UseRetailChaseCamera = saved; + } } [Fact] public void ChaseMode_FlagFlipped_ActiveSwaps() { - CameraDiagnostics.UseRetailChaseCamera = false; - var (ctl, legacy, retail) = MakeChaseFixture(); - Assert.Same(legacy, ctl.Active); + bool saved = CameraDiagnostics.UseRetailChaseCamera; + try + { + CameraDiagnostics.UseRetailChaseCamera = false; + var (ctl, legacy, retail) = MakeChaseFixture(); + Assert.Same(legacy, ctl.Active); - CameraDiagnostics.UseRetailChaseCamera = true; - Assert.Same(retail, ctl.Active); + CameraDiagnostics.UseRetailChaseCamera = true; + Assert.Same(retail, ctl.Active); - CameraDiagnostics.UseRetailChaseCamera = false; - Assert.Same(legacy, ctl.Active); + CameraDiagnostics.UseRetailChaseCamera = false; + Assert.Same(legacy, ctl.Active); + } + finally + { + CameraDiagnostics.UseRetailChaseCamera = saved; + } } [Fact] public void ExitChaseMode_ClearsBothCameras() { - CameraDiagnostics.UseRetailChaseCamera = false; - var (ctl, _, _) = MakeChaseFixture(); - ctl.ExitChaseMode(); + bool saved = CameraDiagnostics.UseRetailChaseCamera; + try + { + CameraDiagnostics.UseRetailChaseCamera = false; + var (ctl, _, _) = MakeChaseFixture(); + ctl.ExitChaseMode(); - Assert.Null(ctl.Chase); - Assert.Null(ctl.RetailChase); - Assert.False(ctl.IsChaseMode); + Assert.Null(ctl.Chase); + Assert.Null(ctl.RetailChase); + Assert.False(ctl.IsChaseMode); + } + finally + { + CameraDiagnostics.UseRetailChaseCamera = saved; + } } }