fix(camera): port retail offset adjustment laws
This commit is contained in:
parent
cb22691beb
commit
90eba0ec3c
7 changed files with 212 additions and 61 deletions
|
|
@ -489,8 +489,9 @@ internal sealed class CameraPointerInputController
|
|||
rawY: dy,
|
||||
weight: 0.5f,
|
||||
nowSec: _clock.NowSeconds);
|
||||
_chase.Retail.YawOffset -= filteredDx * 0.004f * sensitivity;
|
||||
_chase.Retail.AdjustPitch(filteredDy * 0.003f * sensitivity);
|
||||
const float retailMouseScale = 0.0666666701f;
|
||||
_chase.Retail.AdjustYaw(-filteredDx * sensitivity * retailMouseScale);
|
||||
_chase.Retail.AdjustPitch(filteredDy * sensitivity * retailMouseScale);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ internal sealed class MouseLookController : IMouseLookInputFrameController
|
|||
: (rawX, rawY);
|
||||
_state.ApplyDelta(filteredX, _chase.Sensitivity);
|
||||
if (_chase.Retail is { } retailCamera)
|
||||
retailCamera.AdjustPitch(filteredY * 0.003f * _chase.Sensitivity);
|
||||
retailCamera.AdjustPitch(filteredY * 0.0666666701f * _chase.Sensitivity);
|
||||
else
|
||||
_chase.Legacy?.AdjustPitch(filteredY * 0.003f * _chase.Sensitivity);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,13 +82,13 @@ internal sealed class CameraFrameController : ICameraFramePhase
|
|||
if (input.ZoomOut)
|
||||
retail.AdjustDistance(+adjustment);
|
||||
if (input.Raise)
|
||||
retail.AdjustPitch(+adjustment * 0.02f);
|
||||
retail.AdjustPitch(+adjustment);
|
||||
if (input.Lower)
|
||||
retail.AdjustPitch(-adjustment * 0.02f);
|
||||
retail.AdjustPitch(-adjustment);
|
||||
if (input.RotateLeft)
|
||||
retail.YawOffset += adjustment * 0.02f;
|
||||
retail.AdjustYaw(+adjustment);
|
||||
if (input.RotateRight)
|
||||
retail.YawOffset -= adjustment * 0.02f;
|
||||
retail.AdjustYaw(-adjustment);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -34,6 +34,18 @@ public sealed class RetailChaseCamera : ICamera
|
|||
private const float RetailLookDownBack = 2f;
|
||||
private const float RetailMapBack = 450f;
|
||||
private const float RetailFirstPersonForward = 0.18f;
|
||||
private const float RetailLeaveHeadBack = 0.6f;
|
||||
private const float RetailLeaveHeadUp = 0.5f;
|
||||
|
||||
// CameraSet::{Farther,Closer,Raise,Lower,Rotate} constants. The named
|
||||
// retail decomp carries CAMERA_MOUSELOOK_INC at 0x0079BC04 and the
|
||||
// eight-degree offset angle initialised at 0x006EABF0/0x0083D034.
|
||||
internal const float OffsetScalePerAdjustment = 0.200000003f;
|
||||
internal const float OffsetAngleRadians = 8f * MathF.PI / 180f;
|
||||
internal const float MinimumOffsetLength = 0.5f;
|
||||
internal const float MaximumHorizontalComponent = 10f;
|
||||
internal const float MaximumVerticalComponent = 450f;
|
||||
internal const float MinimumVerticalComponent = -1.8f;
|
||||
|
||||
// ICamera surface.
|
||||
public Vector3 Position { get; private set; }
|
||||
|
|
@ -106,12 +118,6 @@ public sealed class RetailChaseCamera : ICamera
|
|||
/// <summary>Computed translucency for the player mesh (0 = opaque, 1 = invisible). Read by GameWindow.</summary>
|
||||
public float PlayerTranslucency { get; private set; }
|
||||
|
||||
/// <summary>Clamp bounds carried over from legacy ChaseCamera.</summary>
|
||||
public const float DistanceMin = 2f;
|
||||
public const float DistanceMax = 40f;
|
||||
public const float PitchMin = -0.7f;
|
||||
public const float PitchMax = 1.4f;
|
||||
|
||||
// Retail CameraManager::UpdateCamera convergence-snap thresholds (decomp
|
||||
// acclient_2013_pseudo_c.txt, 0x00456fcd–0x00457035). SnapEpsilon = 2 ×
|
||||
// 0.000199999995 m ≈ 0.0004 m — the per-frame translation step below which retail
|
||||
|
|
@ -311,27 +317,80 @@ public sealed class RetailChaseCamera : ICamera
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adjust the camera distance (zoom) by a delta, clamped to
|
||||
/// <see cref="DistanceMin"/>..<see cref="DistanceMax"/>. Mirrors
|
||||
/// legacy <c>ChaseCamera.AdjustDistance</c>.
|
||||
/// Apply one retail camera-adjustment unit to the viewer offset.
|
||||
/// <c>CameraSet::Farther</c>/<c>Closer</c> (0x00458890/0x004586D0)
|
||||
/// scale all three offset components multiplicatively by
|
||||
/// <c>1 + adjustment * 0.2</c>. Positive values move farther and
|
||||
/// negative values move closer. Retail refuses the complete write when
|
||||
/// any component or the 0.5 m near limit would be crossed; it does not
|
||||
/// clamp an invented scalar distance.
|
||||
/// </summary>
|
||||
public void AdjustDistance(float delta)
|
||||
public void AdjustDistance(float adjustment)
|
||||
{
|
||||
ExitLookDownForAdjustment();
|
||||
ExitInHeadForAdjustment();
|
||||
Distance = Math.Clamp(Distance + delta, DistanceMin, DistanceMax);
|
||||
if (!float.IsFinite(adjustment) || adjustment == 0f)
|
||||
return;
|
||||
|
||||
// Farther's exact in-head branch replaces (0,+0.18,0) with
|
||||
// (0,-0.6,+0.5) and returns without applying the ordinary scale.
|
||||
if (_inHead)
|
||||
{
|
||||
_inHead = false;
|
||||
SetViewerOffset(RetailLeaveHeadBack, RetailLeaveHeadUp);
|
||||
return;
|
||||
}
|
||||
|
||||
float scale = 1f + adjustment * OffsetScalePerAdjustment;
|
||||
if (!(scale > 0f) || !float.IsFinite(scale))
|
||||
return;
|
||||
|
||||
float candidateDistance = Distance * scale;
|
||||
if (adjustment < 0f && !(candidateDistance > MinimumOffsetLength))
|
||||
return;
|
||||
|
||||
TryWriteViewerOffset(candidateDistance, Pitch, preserveHorizontalSign: false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adjust the camera pitch by a delta (radians), clamped to
|
||||
/// <see cref="PitchMin"/>..<see cref="PitchMax"/>. Mirrors legacy
|
||||
/// <c>ChaseCamera.AdjustPitch</c>.
|
||||
/// Apply one retail camera-adjustment unit to the viewer-offset pitch.
|
||||
/// <c>CameraSet::Raise</c>/<c>Lower</c> (0x00457980/0x00457CF0)
|
||||
/// rotate the offset by eight degrees per unit while preserving its
|
||||
/// length. A write that would cross the vertical axis or a component
|
||||
/// limit is refused as a whole.
|
||||
/// </summary>
|
||||
public void AdjustPitch(float delta)
|
||||
public void AdjustPitch(float adjustment)
|
||||
{
|
||||
ExitLookDownForAdjustment();
|
||||
ExitInHeadForAdjustment();
|
||||
Pitch = Math.Clamp(Pitch + delta, PitchMin, PitchMax);
|
||||
if (!float.IsFinite(adjustment) || adjustment == 0f)
|
||||
return;
|
||||
|
||||
if (_inHead)
|
||||
{
|
||||
_inHead = false;
|
||||
SetViewerOffset(RetailLeaveHeadBack, RetailLeaveHeadUp);
|
||||
}
|
||||
|
||||
float candidatePitch = Pitch + adjustment * OffsetAngleRadians;
|
||||
TryWriteViewerOffset(Distance, candidatePitch, preserveHorizontalSign: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rotate the viewer offset around local Z using retail's eight-degree
|
||||
/// adjustment unit (<c>CameraSet::Rotate @ 0x00458310</c>).
|
||||
/// </summary>
|
||||
public void AdjustYaw(float adjustment)
|
||||
{
|
||||
ExitLookDownForAdjustment();
|
||||
if (!float.IsFinite(adjustment) || adjustment == 0f)
|
||||
return;
|
||||
|
||||
if (_inHead)
|
||||
{
|
||||
_inHead = false;
|
||||
SetViewerOffset(RetailLeaveHeadBack, RetailLeaveHeadUp);
|
||||
}
|
||||
|
||||
YawOffset += adjustment * OffsetAngleRadians;
|
||||
}
|
||||
|
||||
public void SetRetailDefaultView()
|
||||
|
|
@ -416,20 +475,52 @@ public sealed class RetailChaseCamera : ICamera
|
|||
RestoreLookDownView();
|
||||
}
|
||||
|
||||
private void ExitInHeadForAdjustment()
|
||||
{
|
||||
if (!_inHead)
|
||||
return;
|
||||
_inHead = false;
|
||||
Distance = DistanceMin;
|
||||
}
|
||||
|
||||
private void SetViewerOffset(float back, float up)
|
||||
{
|
||||
Distance = MathF.Sqrt(back * back + up * up);
|
||||
Pitch = MathF.Atan2(up, back);
|
||||
}
|
||||
|
||||
private bool TryWriteViewerOffset(
|
||||
float candidateDistance,
|
||||
float candidatePitch,
|
||||
bool preserveHorizontalSign)
|
||||
{
|
||||
if (!float.IsFinite(candidateDistance)
|
||||
|| !float.IsFinite(candidatePitch)
|
||||
|| !(candidateDistance > 0f))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
float currentHorizontal = Distance * MathF.Cos(Pitch);
|
||||
float candidateHorizontal = candidateDistance * MathF.Cos(candidatePitch);
|
||||
if (preserveHorizontalSign
|
||||
&& ((currentHorizontal > 0f && candidateHorizontal < 0f)
|
||||
|| (currentHorizontal < 0f && candidateHorizontal > 0f)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// AC frame-local viewer_offset. Only magnitudes matter for Farther's
|
||||
// X/Y limits, but including YawOffset is essential: at diagonal orbit
|
||||
// angles retail can reach a longer boom before either component is 10.
|
||||
float x = candidateHorizontal * MathF.Sin(YawOffset);
|
||||
float y = -candidateHorizontal * MathF.Cos(YawOffset);
|
||||
float z = candidateDistance * MathF.Sin(candidatePitch);
|
||||
if (!(MathF.Abs(x) < MaximumHorizontalComponent)
|
||||
|| !(MathF.Abs(y) < MaximumHorizontalComponent)
|
||||
|| !(z < MaximumVerticalComponent)
|
||||
|| !(z > MinimumVerticalComponent))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Distance = candidateDistance;
|
||||
Pitch = candidatePitch;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Public entry point for the mouse-input low-pass filter. Calls
|
||||
/// <see cref="FilterMouseAxis"/> on each axis with shared state.
|
||||
|
|
|
|||
|
|
@ -73,8 +73,9 @@ public static class CameraDiagnostics
|
|||
|
||||
/// <summary>
|
||||
/// Per-second rate that held-key offset adjustments
|
||||
/// (CameraZoomIn/Out, CameraRaise/Lower) integrate into the
|
||||
/// camera's Distance / Pitch. Retail default 40.0.
|
||||
/// (CameraZoomIn/Out, CameraRaise/Lower) feed into retail's
|
||||
/// multiplicative zoom / eight-degree offset rotation. Retail
|
||||
/// <c>CameraManager::CameraManager @ 0x0045710A</c> sets 40.0.
|
||||
/// </summary>
|
||||
public static float CameraAdjustmentSpeed { get; set; } = 40.0f;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ public sealed class CameraPointerInputControllerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void ChaseMouseWheel_RetainsExtendedZoomOutRange()
|
||||
public void ChaseMouseWheel_UsesRetailMultiplicativeZoomAndComponentLimit()
|
||||
{
|
||||
var fixture = Create([new RawSurface()]);
|
||||
var legacy = new ChaseCamera();
|
||||
|
|
@ -187,21 +187,24 @@ public sealed class CameraPointerInputControllerTests
|
|||
fixture.Chase.Retail = retail;
|
||||
fixture.Camera.EnterChaseMode(legacy, retail);
|
||||
|
||||
float before = CameraDiagnostics.UseRetailChaseCamera
|
||||
? retail.Distance
|
||||
: legacy.Distance;
|
||||
fixture.Owner.HandleScroll(InputAction.ScrollDown);
|
||||
float afterOne = CameraDiagnostics.UseRetailChaseCamera
|
||||
? retail.Distance
|
||||
: legacy.Distance;
|
||||
for (int i = 0; i < 100; i++)
|
||||
bool savedRetail = CameraDiagnostics.UseRetailChaseCamera;
|
||||
try
|
||||
{
|
||||
CameraDiagnostics.UseRetailChaseCamera = true;
|
||||
float before = retail.Distance;
|
||||
fixture.Owner.HandleScroll(InputAction.ScrollDown);
|
||||
float afterMany = CameraDiagnostics.UseRetailChaseCamera
|
||||
? retail.Distance
|
||||
: legacy.Distance;
|
||||
float afterOne = retail.Distance;
|
||||
for (int i = 0; i < 100; i++)
|
||||
fixture.Owner.HandleScroll(InputAction.ScrollDown);
|
||||
|
||||
Assert.Equal(before + 0.8f, afterOne, 5);
|
||||
Assert.Equal(40f, afterMany, 5);
|
||||
Assert.Equal(before * (1f + 0.8f * RetailChaseCamera.OffsetScalePerAdjustment), afterOne, 5);
|
||||
Assert.True(retail.Distance < 10.5f);
|
||||
Assert.True(retail.Distance > 8f);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CameraDiagnostics.UseRetailChaseCamera = savedRetail;
|
||||
}
|
||||
}
|
||||
|
||||
private static Fixture Create(IReadOnlyList<RawSurface> surfaces)
|
||||
|
|
|
|||
|
|
@ -537,14 +537,50 @@ public class RetailChaseCameraTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void AdjustDistance_ClampsToRange()
|
||||
public void AdjustDistance_UsesRetailMultiplicativeScale()
|
||||
{
|
||||
var cam = new RetailChaseCamera { Distance = 5f };
|
||||
cam.AdjustDistance(-100f);
|
||||
Assert.Equal(RetailChaseCamera.DistanceMin, cam.Distance);
|
||||
cam.AdjustDistance(+1f);
|
||||
Assert.Equal(6f, cam.Distance, 5);
|
||||
|
||||
cam.AdjustDistance(+200f);
|
||||
Assert.Equal(RetailChaseCamera.DistanceMax, cam.Distance);
|
||||
cam.AdjustDistance(-1f);
|
||||
Assert.Equal(4.8f, cam.Distance, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdjustDistance_RefusesWholeWriteAtRetailComponentAndNearLimits()
|
||||
{
|
||||
var far = new RetailChaseCamera
|
||||
{
|
||||
Distance = 9.9f,
|
||||
Pitch = 0f,
|
||||
YawOffset = 0f,
|
||||
};
|
||||
far.AdjustDistance(+1f);
|
||||
Assert.Equal(9.9f, far.Distance, 5);
|
||||
|
||||
var near = new RetailChaseCamera
|
||||
{
|
||||
Distance = 0.55f,
|
||||
Pitch = 0f,
|
||||
};
|
||||
near.AdjustDistance(-1f);
|
||||
Assert.Equal(0.55f, near.Distance, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdjustDistance_DiagonalOrbitUsesIndependentRetailXAndYLimits()
|
||||
{
|
||||
var cam = new RetailChaseCamera
|
||||
{
|
||||
Distance = 12f,
|
||||
Pitch = 0f,
|
||||
YawOffset = MathF.PI / 4f,
|
||||
};
|
||||
|
||||
cam.AdjustDistance(+0.5f);
|
||||
|
||||
Assert.Equal(13.2f, cam.Distance, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -579,18 +615,37 @@ public class RetailChaseCameraTests
|
|||
cam.AdjustDistance(1f);
|
||||
|
||||
Assert.False(cam.IsInHead);
|
||||
Assert.Equal(RetailChaseCamera.DistanceMin + 1f, cam.Distance);
|
||||
Assert.Equal(MathF.Sqrt(0.6f * 0.6f + 0.5f * 0.5f), cam.Distance, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdjustPitch_ClampsToRange()
|
||||
public void AdjustPitch_UsesRetailEightDegreeRotationAndPreservesLength()
|
||||
{
|
||||
var cam = new RetailChaseCamera { Pitch = 0f };
|
||||
cam.AdjustPitch(-10f);
|
||||
Assert.Equal(RetailChaseCamera.PitchMin, cam.Pitch);
|
||||
var cam = new RetailChaseCamera { Distance = 5f, Pitch = 0f };
|
||||
cam.AdjustPitch(+1f);
|
||||
|
||||
cam.AdjustPitch(+10f);
|
||||
Assert.Equal(RetailChaseCamera.PitchMax, cam.Pitch);
|
||||
Assert.Equal(RetailChaseCamera.OffsetAngleRadians, cam.Pitch, 6);
|
||||
Assert.Equal(5f, cam.Distance, 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdjustPitch_RefusesWholeWriteBelowRetailVerticalLimit()
|
||||
{
|
||||
var cam = new RetailChaseCamera { Distance = 3f, Pitch = -0.55f };
|
||||
float before = cam.Pitch;
|
||||
|
||||
cam.AdjustPitch(-1f);
|
||||
|
||||
Assert.Equal(before, cam.Pitch);
|
||||
Assert.Equal(3f, cam.Distance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdjustYaw_UsesRetailEightDegreeRotationUnit()
|
||||
{
|
||||
var cam = new RetailChaseCamera();
|
||||
cam.AdjustYaw(+1f);
|
||||
Assert.Equal(RetailChaseCamera.OffsetAngleRadians, cam.YawOffset, 6);
|
||||
}
|
||||
|
||||
// ── Camera collision (A8.F) ───────────────────────────────────────
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue