fix(client): restore retail interaction parity
Harden keyboard and camera routing, inventory and vendor interactions, chat/emotes, relog portal flow, and paperdoll rendering. Add retail research, connected gate coverage, and release-gate validation.
This commit is contained in:
parent
0c699240e0
commit
f6fe0f2a4f
151 changed files with 10162 additions and 1211 deletions
|
|
@ -29,6 +29,12 @@ namespace AcDream.App.Rendering;
|
|||
/// </summary>
|
||||
public sealed class RetailChaseCamera : ICamera
|
||||
{
|
||||
private const float RetailDefaultBack = 2.5f;
|
||||
private const float RetailDefaultUp = 0.75f;
|
||||
private const float RetailLookDownBack = 2f;
|
||||
private const float RetailMapBack = 450f;
|
||||
private const float RetailFirstPersonForward = 0.18f;
|
||||
|
||||
// ICamera surface.
|
||||
public Vector3 Position { get; private set; }
|
||||
|
||||
|
|
@ -75,6 +81,20 @@ public sealed class RetailChaseCamera : ICamera
|
|||
/// <summary>Height of look-at anchor above the player's feet (m). Retail default 1.5.</summary>
|
||||
public float PivotHeight { get; set; } = 1.5f;
|
||||
|
||||
private bool _lookingDown;
|
||||
private bool _mapMode;
|
||||
private bool _inHead;
|
||||
private bool _savedInHead;
|
||||
private float _savedDistance;
|
||||
private float _savedPitch;
|
||||
private float _savedYawOffset;
|
||||
private Vector3? _targetDirectionLocal;
|
||||
private Vector3? _savedTargetDirectionLocal;
|
||||
|
||||
public bool IsLookingDown => _lookingDown;
|
||||
public bool IsMapMode => _mapMode;
|
||||
public bool IsInHead => _inHead;
|
||||
|
||||
/// <summary>
|
||||
/// Optional spring-arm collision probe. When set (and
|
||||
/// <see cref="CameraDiagnostics.CollideCamera"/> is true), the damped eye
|
||||
|
|
@ -172,8 +192,13 @@ public sealed class RetailChaseCamera : ICamera
|
|||
// target supplies the frame heading. Without this local rotation, enabling
|
||||
// Keep in View snaps the camera behind the target and disables RMB orbit.
|
||||
float viewerYawOffset = trackedHeading.HasValue ? YawOffset : 0f;
|
||||
(Vector3 targetEye, Vector3 targetForward) = ComputeDesiredPose(
|
||||
pivotWorld, heading, Distance, Pitch, viewerYawOffset);
|
||||
(Vector3 targetEye, Vector3 targetForward) = _inHead
|
||||
? ComputeInHeadPose(pivotWorld, heading)
|
||||
: _targetDirectionLocal is { } localDirection
|
||||
? ComputeTargetDirectionPose(
|
||||
pivotWorld, heading, Distance, Pitch, localDirection)
|
||||
: ComputeDesiredPose(
|
||||
pivotWorld, heading, Distance, Pitch, viewerYawOffset);
|
||||
|
||||
// 5. Stateful sought position (#180). Retail CameraManager::UpdateCamera
|
||||
// (0x00456660) interpolates FROM THE CURRENT SWEPT VIEWER toward the
|
||||
|
|
@ -279,16 +304,120 @@ public sealed class RetailChaseCamera : ICamera
|
|||
/// <see cref="DistanceMin"/>..<see cref="DistanceMax"/>. Mirrors
|
||||
/// legacy <c>ChaseCamera.AdjustDistance</c>.
|
||||
/// </summary>
|
||||
public void AdjustDistance(float delta) =>
|
||||
public void AdjustDistance(float delta)
|
||||
{
|
||||
ExitLookDownForAdjustment();
|
||||
ExitInHeadForAdjustment();
|
||||
Distance = Math.Clamp(Distance + delta, DistanceMin, DistanceMax);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adjust the camera pitch by a delta (radians), clamped to
|
||||
/// <see cref="PitchMin"/>..<see cref="PitchMax"/>. Mirrors legacy
|
||||
/// <c>ChaseCamera.AdjustPitch</c>.
|
||||
/// </summary>
|
||||
public void AdjustPitch(float delta) =>
|
||||
public void AdjustPitch(float delta)
|
||||
{
|
||||
ExitLookDownForAdjustment();
|
||||
ExitInHeadForAdjustment();
|
||||
Pitch = Math.Clamp(Pitch + delta, PitchMin, PitchMax);
|
||||
}
|
||||
|
||||
public void SetRetailDefaultView()
|
||||
{
|
||||
_lookingDown = false;
|
||||
_mapMode = false;
|
||||
_inHead = false;
|
||||
_targetDirectionLocal = null;
|
||||
YawOffset = 0f;
|
||||
PivotHeight = 1.5f;
|
||||
SetViewerOffset(RetailDefaultBack, RetailDefaultUp);
|
||||
}
|
||||
|
||||
public void SetRetailFirstPersonView()
|
||||
{
|
||||
_lookingDown = false;
|
||||
_mapMode = false;
|
||||
_inHead = true;
|
||||
_targetDirectionLocal = null;
|
||||
YawOffset = 0f;
|
||||
Distance = RetailFirstPersonForward;
|
||||
Pitch = 0f;
|
||||
// Do not spend a transition frame inside the head/neck. Retail's
|
||||
// SetInHead installs the new viewer offset as one camera preset.
|
||||
_initialised = false;
|
||||
}
|
||||
|
||||
public void ToggleRetailLookDownView()
|
||||
{
|
||||
if (_lookingDown)
|
||||
{
|
||||
RestoreLookDownView();
|
||||
return;
|
||||
}
|
||||
SaveLookDownView();
|
||||
_lookingDown = true;
|
||||
_mapMode = false;
|
||||
_inHead = false;
|
||||
_targetDirectionLocal = new Vector3(0f, 0.5f, -1.8f);
|
||||
SetViewerOffset(RetailLookDownBack, RetailDefaultUp);
|
||||
}
|
||||
|
||||
public void ToggleRetailMapModeView()
|
||||
{
|
||||
if (_mapMode)
|
||||
{
|
||||
RestoreLookDownView();
|
||||
return;
|
||||
}
|
||||
if (!_lookingDown)
|
||||
SaveLookDownView();
|
||||
_lookingDown = true;
|
||||
_mapMode = true;
|
||||
_inHead = false;
|
||||
_targetDirectionLocal = new Vector3(0f, 0.5f, -1.8f);
|
||||
SetViewerOffset(RetailMapBack, RetailDefaultUp);
|
||||
}
|
||||
|
||||
private void SaveLookDownView()
|
||||
{
|
||||
_savedDistance = Distance;
|
||||
_savedPitch = Pitch;
|
||||
_savedYawOffset = YawOffset;
|
||||
_savedTargetDirectionLocal = _targetDirectionLocal;
|
||||
_savedInHead = _inHead;
|
||||
}
|
||||
|
||||
private void RestoreLookDownView()
|
||||
{
|
||||
Distance = _savedDistance;
|
||||
Pitch = _savedPitch;
|
||||
YawOffset = _savedYawOffset;
|
||||
_targetDirectionLocal = _savedTargetDirectionLocal;
|
||||
_inHead = _savedInHead;
|
||||
_lookingDown = false;
|
||||
_mapMode = false;
|
||||
}
|
||||
|
||||
private void ExitLookDownForAdjustment()
|
||||
{
|
||||
if (_lookingDown)
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Public entry point for the mouse-input low-pass filter. Calls
|
||||
|
|
@ -436,6 +565,47 @@ public sealed class RetailChaseCamera : ICamera
|
|||
return (eye, forward);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>CameraSet::SetInHead @ 0x00458CE0</c>: the target direction
|
||||
/// is local +Y and the viewer offset is local +Y * 0.18. It is not a
|
||||
/// negative chase boom looking back toward the player's neck.
|
||||
/// </summary>
|
||||
internal static (Vector3 eye, Vector3 forward) ComputeInHeadPose(
|
||||
Vector3 pivotWorld,
|
||||
Vector3 heading)
|
||||
{
|
||||
Vector3 forward = Vector3.Normalize(heading);
|
||||
return (pivotWorld + forward * RetailFirstPersonForward, forward);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transform both retail <c>viewer_offset</c> and <c>target_direction</c>
|
||||
/// through the target frame. LookDown/MapMode do not merely point a
|
||||
/// horizontally-positioned camera toward the ground: the downward target
|
||||
/// direction pitches the frame whose -Y/+Z offset places the viewer. For
|
||||
/// MapMode's (0,-450,0.75) offset this puts the viewer high above and only
|
||||
/// slightly behind the character, matching CameraSet::SetMapMode.
|
||||
/// </summary>
|
||||
internal static (Vector3 eye, Vector3 forward) ComputeTargetDirectionPose(
|
||||
Vector3 pivotWorld,
|
||||
Vector3 heading,
|
||||
float distance,
|
||||
float pitch,
|
||||
Vector3 targetDirectionLocal)
|
||||
{
|
||||
var (frameForward, frameRight, frameUp) = BuildBasis(heading);
|
||||
Vector3 targetForward = Vector3.Normalize(
|
||||
frameForward * targetDirectionLocal.Y
|
||||
- frameRight * targetDirectionLocal.X
|
||||
+ frameUp * targetDirectionLocal.Z);
|
||||
var (_, _, targetUp) = BuildBasis(targetForward);
|
||||
|
||||
float back = distance * MathF.Cos(pitch);
|
||||
float up = distance * MathF.Sin(pitch);
|
||||
Vector3 eye = pivotWorld - targetForward * back + targetUp * up;
|
||||
return (eye, targetForward);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build an orthonormal basis with <c>forward = heading</c>. World
|
||||
/// up is <c>(0, 0, 1)</c>; if <c>heading</c> is near-parallel to it
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue