feat(camera): CameraController carries both legacy + retail chase cams
EnterChaseMode now takes (ChaseCamera, RetailChaseCamera); Active consults CameraDiagnostics.UseRetailChaseCamera to pick which to expose. Flag flip at runtime swaps cameras instantly (both are kept warm). GameWindow's two EnterChaseMode call sites get a temporary stub RetailChaseCamera; Task 7 wires proper construction + per-frame updates. Also folds in two minor cleanups from the Task 3 code review: - Update() discards the unused `right` axis from BuildBasis (no caller in the chase-cam math; viewer_offset.X is always 0) - The three CameraDiagnostics-mutating integration tests now save and restore the static state in try/finally to avoid ordering-dependent contamination Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0c1403f2e6
commit
e5a5916679
5 changed files with 187 additions and 58 deletions
67
tests/AcDream.App.Tests/Rendering/CameraControllerTests.cs
Normal file
67
tests/AcDream.App.Tests/Rendering/CameraControllerTests.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.Core.Rendering;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
||||
public class CameraControllerTests
|
||||
{
|
||||
private static (CameraController ctl, ChaseCamera legacy, RetailChaseCamera retail) MakeChaseFixture()
|
||||
{
|
||||
var orbit = new OrbitCamera();
|
||||
var fly = new FlyCamera();
|
||||
var ctl = new CameraController(orbit, fly);
|
||||
var legacy = new ChaseCamera();
|
||||
var retail = new RetailChaseCamera();
|
||||
ctl.EnterChaseMode(legacy, retail);
|
||||
return (ctl, legacy, retail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChaseMode_WhenFlagOff_ActiveIsLegacy()
|
||||
{
|
||||
CameraDiagnostics.UseRetailChaseCamera = false;
|
||||
var (ctl, legacy, _) = MakeChaseFixture();
|
||||
Assert.Same(legacy, ctl.Active);
|
||||
Assert.True(ctl.IsChaseMode);
|
||||
}
|
||||
|
||||
[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;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChaseMode_FlagFlipped_ActiveSwaps()
|
||||
{
|
||||
CameraDiagnostics.UseRetailChaseCamera = false;
|
||||
var (ctl, legacy, retail) = MakeChaseFixture();
|
||||
Assert.Same(legacy, ctl.Active);
|
||||
|
||||
CameraDiagnostics.UseRetailChaseCamera = true;
|
||||
Assert.Same(retail, ctl.Active);
|
||||
|
||||
CameraDiagnostics.UseRetailChaseCamera = false;
|
||||
Assert.Same(legacy, ctl.Active);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExitChaseMode_ClearsBothCameras()
|
||||
{
|
||||
CameraDiagnostics.UseRetailChaseCamera = false;
|
||||
var (ctl, _, _) = MakeChaseFixture();
|
||||
ctl.ExitChaseMode();
|
||||
|
||||
Assert.Null(ctl.Chase);
|
||||
Assert.Null(ctl.RetailChase);
|
||||
Assert.False(ctl.IsChaseMode);
|
||||
}
|
||||
}
|
||||
|
|
@ -273,62 +273,90 @@ public class RetailChaseCameraTests
|
|||
[Fact]
|
||||
public void FirstUpdate_SnapsToTarget()
|
||||
{
|
||||
var cam = new RetailChaseCamera { Distance = 5f, Pitch = 0f };
|
||||
CameraDiagnostics.AlignToSlope = false; // deterministic: heading = yaw vec
|
||||
bool savedAlign = CameraDiagnostics.AlignToSlope;
|
||||
try
|
||||
{
|
||||
var cam = new RetailChaseCamera { Distance = 5f, Pitch = 0f };
|
||||
CameraDiagnostics.AlignToSlope = false; // deterministic: heading = yaw vec
|
||||
|
||||
cam.Update(
|
||||
playerPosition: new Vector3(10f, 20f, 30f),
|
||||
playerYaw: 0f, // forward = +X
|
||||
playerVelocity: Vector3.Zero,
|
||||
dt: 1f / 60f);
|
||||
cam.Update(
|
||||
playerPosition: new Vector3(10f, 20f, 30f),
|
||||
playerYaw: 0f, // forward = +X
|
||||
playerVelocity: Vector3.Zero,
|
||||
dt: 1f / 60f);
|
||||
|
||||
// Expected target eye:
|
||||
// pivot = (10, 20, 30+1.5=31.5)
|
||||
// forward (yaw=0)= (1, 0, 0)
|
||||
// right = (0, -1, 0) since (1,0,0) × (0,0,1) = (0, -1, 0)
|
||||
// up = right × forward = (0,-1,0) × (1,0,0) = (0,0,1)
|
||||
// viewer_offset = (0, -5, 0) (Distance=5, Pitch=0 → -Distance*cos = -5, sin = 0)
|
||||
// eye = pivot + right*0 + forward*-5 + up*0
|
||||
// = (10 - 5, 20, 31.5) = (5, 20, 31.5)
|
||||
Assert.Equal(5f, cam.Position.X, 4);
|
||||
Assert.Equal(20f, cam.Position.Y, 4);
|
||||
Assert.Equal(31.5f, cam.Position.Z, 4);
|
||||
// Expected target eye:
|
||||
// pivot = (10, 20, 30+1.5=31.5)
|
||||
// forward (yaw=0)= (1, 0, 0)
|
||||
// right = (0, -1, 0) since (1,0,0) × (0,0,1) = (0, -1, 0)
|
||||
// up = right × forward = (0,-1,0) × (1,0,0) = (0,0,1)
|
||||
// viewer_offset = (0, -5, 0) (Distance=5, Pitch=0 → -Distance*cos = -5, sin = 0)
|
||||
// eye = pivot + right*0 + forward*-5 + up*0
|
||||
// = (10 - 5, 20, 31.5) = (5, 20, 31.5)
|
||||
Assert.Equal(5f, cam.Position.X, 4);
|
||||
Assert.Equal(20f, cam.Position.Y, 4);
|
||||
Assert.Equal(31.5f, cam.Position.Z, 4);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CameraDiagnostics.AlignToSlope = savedAlign;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SecondUpdate_LerpsTowardTarget()
|
||||
{
|
||||
var cam = new RetailChaseCamera { Distance = 5f, Pitch = 0f };
|
||||
CameraDiagnostics.AlignToSlope = false;
|
||||
CameraDiagnostics.TranslationStiffness = 0.45f;
|
||||
CameraDiagnostics.RotationStiffness = 0.45f;
|
||||
bool savedAlign = CameraDiagnostics.AlignToSlope;
|
||||
float savedTranslation = CameraDiagnostics.TranslationStiffness;
|
||||
float savedRotation = CameraDiagnostics.RotationStiffness;
|
||||
try
|
||||
{
|
||||
var cam = new RetailChaseCamera { Distance = 5f, Pitch = 0f };
|
||||
CameraDiagnostics.AlignToSlope = false;
|
||||
CameraDiagnostics.TranslationStiffness = 0.45f;
|
||||
CameraDiagnostics.RotationStiffness = 0.45f;
|
||||
|
||||
// First update at origin: dampedEye = (-5, 0, 1.5).
|
||||
cam.Update(Vector3.Zero, playerYaw: 0f, playerVelocity: Vector3.Zero, dt: 1f / 60f);
|
||||
var firstEye = cam.Position;
|
||||
// First update at origin: dampedEye = (-5, 0, 1.5).
|
||||
cam.Update(Vector3.Zero, playerYaw: 0f, playerVelocity: Vector3.Zero, dt: 1f / 60f);
|
||||
var firstEye = cam.Position;
|
||||
|
||||
// Teleport the player one frame later. Target eye now at (10-5, 0, 1.5) = (5, 0, 1.5).
|
||||
// alpha = 0.45 * (1/60) * 10 = 0.075.
|
||||
// New eye = firstEye + 0.075 * (target - firstEye)
|
||||
// = (-5,0,1.5) + 0.075 * ((5,0,1.5) - (-5,0,1.5))
|
||||
// = (-5,0,1.5) + 0.075 * (10,0,0)
|
||||
// = (-4.25, 0, 1.5)
|
||||
cam.Update(new Vector3(10f, 0f, 0f), playerYaw: 0f, playerVelocity: Vector3.Zero, dt: 1f / 60f);
|
||||
// Teleport the player one frame later. Target eye now at (10-5, 0, 1.5) = (5, 0, 1.5).
|
||||
// alpha = 0.45 * (1/60) * 10 = 0.075.
|
||||
// New eye = firstEye + 0.075 * (target - firstEye)
|
||||
// = (-5,0,1.5) + 0.075 * ((5,0,1.5) - (-5,0,1.5))
|
||||
// = (-5,0,1.5) + 0.075 * (10,0,0)
|
||||
// = (-4.25, 0, 1.5)
|
||||
cam.Update(new Vector3(10f, 0f, 0f), playerYaw: 0f, playerVelocity: Vector3.Zero, dt: 1f / 60f);
|
||||
|
||||
Assert.Equal(-4.25f, cam.Position.X, 3);
|
||||
Assert.Equal(0f, cam.Position.Y, 4);
|
||||
Assert.Equal(1.5f, cam.Position.Z, 4);
|
||||
Assert.Equal(-4.25f, cam.Position.X, 3);
|
||||
Assert.Equal(0f, cam.Position.Y, 4);
|
||||
Assert.Equal(1.5f, cam.Position.Z, 4);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CameraDiagnostics.AlignToSlope = savedAlign;
|
||||
CameraDiagnostics.TranslationStiffness = savedTranslation;
|
||||
CameraDiagnostics.RotationStiffness = savedRotation;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Translucency_PropertyReflectsCurrentDampedDistance()
|
||||
{
|
||||
var cam = new RetailChaseCamera { Distance = 5f, Pitch = 0f, PivotHeight = 1.5f };
|
||||
CameraDiagnostics.AlignToSlope = false;
|
||||
bool savedAlign = CameraDiagnostics.AlignToSlope;
|
||||
try
|
||||
{
|
||||
var cam = new RetailChaseCamera { Distance = 5f, Pitch = 0f, PivotHeight = 1.5f };
|
||||
CameraDiagnostics.AlignToSlope = false;
|
||||
|
||||
// Far from pivot — translucency should be 0.
|
||||
cam.Update(Vector3.Zero, playerYaw: 0f, playerVelocity: Vector3.Zero, dt: 1f / 60f);
|
||||
Assert.Equal(0f, cam.PlayerTranslucency, 5);
|
||||
// Far from pivot — translucency should be 0.
|
||||
cam.Update(Vector3.Zero, playerYaw: 0f, playerVelocity: Vector3.Zero, dt: 1f / 60f);
|
||||
Assert.Equal(0f, cam.PlayerTranslucency, 5);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CameraDiagnostics.AlignToSlope = savedAlign;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue