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) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-18 20:04:34 +02:00
parent e5a5916679
commit 91086adbac
4 changed files with 115 additions and 33 deletions

View file

@ -11,11 +11,11 @@ public sealed class CameraController
public RetailChaseCamera? RetailChase { get; private set; } public RetailChaseCamera? RetailChase { get; private set; }
/// <summary> /// <summary>
/// The renderer-facing active camera. In chase mode, returns /// The renderer-facing active camera. Both the legacy and retail
/// <see cref="RetailChase"/> when /// chase cameras are held simultaneously so that flipping
/// <see cref="CameraDiagnostics.UseRetailChaseCamera"/> is true, /// <see cref="CameraDiagnostics.UseRetailChaseCamera"/> takes effect
/// otherwise <see cref="Chase"/>. In fly mode returns /// on the very next access to this property — no re-entry required,
/// <see cref="Fly"/>; default is <see cref="Orbit"/>. /// no notification mechanism, no stale state.
/// </summary> /// </summary>
public ICamera Active public ICamera Active
{ {
@ -53,9 +53,8 @@ public sealed class CameraController
} }
/// <summary> /// <summary>
/// Enter chase mode with both candidate cameras. Both are held; /// Store both cameras simultaneously; <see cref="Active"/> picks
/// <see cref="Active"/> picks based on /// between them per-read via the flag — no re-entry needed on flip.
/// <see cref="CameraDiagnostics.UseRetailChaseCamera"/>.
/// </summary> /// </summary>
public void EnterChaseMode(ChaseCamera legacy, RetailChaseCamera retail) public void EnterChaseMode(ChaseCamera legacy, RetailChaseCamera retail)
{ {

View file

@ -262,4 +262,14 @@ public enum InputAction
/// <summary>Fly-camera descend (Ctrl) — only meaningful while fly camera /// <summary>Fly-camera descend (Ctrl) — only meaningful while fly camera
/// is active. K.1b binds it to ControlLeft; K.1c may rebind.</summary> /// is active. K.1b binds it to ControlLeft; K.1c may rebind.</summary>
AcdreamFlyDown, AcdreamFlyDown,
// ── AcdreamCameraCommands ─────────────────────────────
/// <summary>Camera zoom in (held key, integrates Distance= adjSpeed·dt). Default unbound.</summary>
CameraZoomIn,
/// <summary>Camera zoom out (held key, integrates Distance+= adjSpeed·dt). Default unbound.</summary>
CameraZoomOut,
/// <summary>Camera raise (held key, integrates Pitch+= adjSpeed·dt·0.02). Default unbound.</summary>
CameraRaise,
/// <summary>Camera lower (held key, integrates Pitch= adjSpeed·dt·0.02). Default unbound.</summary>
CameraLower,
} }

View file

@ -1,6 +1,7 @@
using System.Numerics; using System.Numerics;
using AcDream.Core.Combat; using AcDream.Core.Combat;
using AcDream.Core.Physics; using AcDream.Core.Physics;
using AcDream.Core.Rendering;
namespace AcDream.UI.Abstractions.Panels.Debug; namespace AcDream.UI.Abstractions.Panels.Debug;
@ -290,6 +291,50 @@ public sealed class DebugVM
set => PhysicsDiagnostics.ProbeAutoWalkEnabled = value; set => PhysicsDiagnostics.ProbeAutoWalkEnabled = value;
} }
// ── Chase camera tunables (forward to CameraDiagnostics) ──────────
/// <summary>Runtime mirror of <see cref="CameraDiagnostics.UseRetailChaseCamera"/>.</summary>
public bool UseRetailChaseCamera
{
get => CameraDiagnostics.UseRetailChaseCamera;
set => CameraDiagnostics.UseRetailChaseCamera = value;
}
/// <summary>Runtime mirror of <see cref="CameraDiagnostics.AlignToSlope"/>.</summary>
public bool CameraAlignToSlope
{
get => CameraDiagnostics.AlignToSlope;
set => CameraDiagnostics.AlignToSlope = value;
}
/// <summary>Runtime mirror of <see cref="CameraDiagnostics.TranslationStiffness"/>.</summary>
public float CameraTranslationStiffness
{
get => CameraDiagnostics.TranslationStiffness;
set => CameraDiagnostics.TranslationStiffness = value;
}
/// <summary>Runtime mirror of <see cref="CameraDiagnostics.RotationStiffness"/>.</summary>
public float CameraRotationStiffness
{
get => CameraDiagnostics.RotationStiffness;
set => CameraDiagnostics.RotationStiffness = value;
}
/// <summary>Runtime mirror of <see cref="CameraDiagnostics.MouseLowPassWindowSec"/>.</summary>
public float CameraMouseLowPassWindowSec
{
get => CameraDiagnostics.MouseLowPassWindowSec;
set => CameraDiagnostics.MouseLowPassWindowSec = value;
}
/// <summary>Runtime mirror of <see cref="CameraDiagnostics.CameraAdjustmentSpeed"/>.</summary>
public float CameraAdjustmentSpeed
{
get => CameraDiagnostics.CameraAdjustmentSpeed;
set => CameraDiagnostics.CameraAdjustmentSpeed = value;
}
// ── Action hooks invoked by panel buttons ────────────────────────── // ── Action hooks invoked by panel buttons ──────────────────────────
/// <summary> /// <summary>

View file

@ -1,4 +1,3 @@
using System.Numerics;
using AcDream.App.Rendering; using AcDream.App.Rendering;
using AcDream.Core.Rendering; using AcDream.Core.Rendering;
using Xunit; using Xunit;
@ -21,47 +20,76 @@ public class CameraControllerTests
[Fact] [Fact]
public void ChaseMode_WhenFlagOff_ActiveIsLegacy() public void ChaseMode_WhenFlagOff_ActiveIsLegacy()
{ {
CameraDiagnostics.UseRetailChaseCamera = false; bool saved = CameraDiagnostics.UseRetailChaseCamera;
var (ctl, legacy, _) = MakeChaseFixture(); try
Assert.Same(legacy, ctl.Active); {
Assert.True(ctl.IsChaseMode); CameraDiagnostics.UseRetailChaseCamera = false;
var (ctl, legacy, _) = MakeChaseFixture();
Assert.Same(legacy, ctl.Active);
Assert.True(ctl.IsChaseMode);
}
finally
{
CameraDiagnostics.UseRetailChaseCamera = saved;
}
} }
[Fact] [Fact]
public void ChaseMode_WhenFlagOn_ActiveIsRetail() public void ChaseMode_WhenFlagOn_ActiveIsRetail()
{ {
CameraDiagnostics.UseRetailChaseCamera = true; bool saved = CameraDiagnostics.UseRetailChaseCamera;
var (ctl, _, retail) = MakeChaseFixture(); try
Assert.Same(retail, ctl.Active); {
Assert.True(ctl.IsChaseMode); CameraDiagnostics.UseRetailChaseCamera = true;
var (ctl, _, retail) = MakeChaseFixture();
// Reset. Assert.Same(retail, ctl.Active);
CameraDiagnostics.UseRetailChaseCamera = false; Assert.True(ctl.IsChaseMode);
}
finally
{
CameraDiagnostics.UseRetailChaseCamera = saved;
}
} }
[Fact] [Fact]
public void ChaseMode_FlagFlipped_ActiveSwaps() public void ChaseMode_FlagFlipped_ActiveSwaps()
{ {
CameraDiagnostics.UseRetailChaseCamera = false; bool saved = CameraDiagnostics.UseRetailChaseCamera;
var (ctl, legacy, retail) = MakeChaseFixture(); try
Assert.Same(legacy, ctl.Active); {
CameraDiagnostics.UseRetailChaseCamera = false;
var (ctl, legacy, retail) = MakeChaseFixture();
Assert.Same(legacy, ctl.Active);
CameraDiagnostics.UseRetailChaseCamera = true; CameraDiagnostics.UseRetailChaseCamera = true;
Assert.Same(retail, ctl.Active); Assert.Same(retail, ctl.Active);
CameraDiagnostics.UseRetailChaseCamera = false; CameraDiagnostics.UseRetailChaseCamera = false;
Assert.Same(legacy, ctl.Active); Assert.Same(legacy, ctl.Active);
}
finally
{
CameraDiagnostics.UseRetailChaseCamera = saved;
}
} }
[Fact] [Fact]
public void ExitChaseMode_ClearsBothCameras() public void ExitChaseMode_ClearsBothCameras()
{ {
CameraDiagnostics.UseRetailChaseCamera = false; bool saved = CameraDiagnostics.UseRetailChaseCamera;
var (ctl, _, _) = MakeChaseFixture(); try
ctl.ExitChaseMode(); {
CameraDiagnostics.UseRetailChaseCamera = false;
var (ctl, _, _) = MakeChaseFixture();
ctl.ExitChaseMode();
Assert.Null(ctl.Chase); Assert.Null(ctl.Chase);
Assert.Null(ctl.RetailChase); Assert.Null(ctl.RetailChase);
Assert.False(ctl.IsChaseMode); Assert.False(ctl.IsChaseMode);
}
finally
{
CameraDiagnostics.UseRetailChaseCamera = saved;
}
} }
} }