fix(combat): track targets with retail camera

Port ClientCombatSystem's Keep in View consumer through CameraSet's pivot-to-target boom heading, including the transformed half-metre target offset and melee/missile validity gates. Preserve the existing stateful chase damping and collision architecture, with conformance coverage and named-retail pseudocode.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-11 20:36:58 +02:00
parent 927fa7881a
commit 33cc9aa16a
5 changed files with 159 additions and 14 deletions

View file

@ -8415,13 +8415,15 @@ public sealed class GameWindow : IDisposable
// player visibly rises in frame without the camera
// swinging vertically (was the symptom of using raw
// velocity-vector heading).
System.Numerics.Vector3? trackedCombatTarget = GetCombatCameraTargetPoint();
_retailChaseCamera!.Update(result.RenderPosition, _playerController.Yaw,
playerVelocity: _playerController.BodyVelocity,
isOnGround: result.IsOnGround,
contactPlaneNormal: _playerController.ContactPlane.Normal,
dt: (float)dt,
cellId: _playerController.CellId,
selfEntityId: _playerController.LocalEntityId);
selfEntityId: _playerController.LocalEntityId,
trackedTargetPoint: trackedCombatTarget);
// Send outbound movement messages to the live server.
if (_liveSession is not null)
@ -12021,6 +12023,26 @@ public sealed class GameWindow : IDisposable
return SelectClosestCombatTarget(showToast: false);
}
/// <summary>
/// Resolves retail's combat-camera target. ClientCombatSystem::
/// UpdateTargetTracking (0x0056A950) enables CameraSet::TrackTarget only
/// for melee/missile combat with the option enabled and a valid attack
/// target. TrackTarget applies target-local offset (0,0,0.5).
/// </summary>
private System.Numerics.Vector3? GetCombatCameraTargetPoint()
{
if (!_persistedGameplay.ViewCombatTarget
|| !AcDream.Core.Combat.CombatInputPlanner.SupportsTargetedAttack(Combat.CurrentMode)
|| _selection.SelectedObjectId is not uint selected
|| !IsLiveCreatureTarget(selected)
|| !_entitiesByServerGuid.TryGetValue(selected, out var target))
return null;
return target.Position
+ System.Numerics.Vector3.Transform(
new System.Numerics.Vector3(0f, 0f, 0.5f), target.Rotation);
}
// ============================================================
// Phase B.4b — outbound Use handler. Wires three input actions
// (LMB click select, LMB-double-click select+use, R hotkey

View file

@ -143,28 +143,31 @@ public sealed class RetailChaseCamera : ICamera
Vector3 contactPlaneNormal,
float dt,
uint cellId = 0,
uint selfEntityId = 0)
uint selfEntityId = 0,
Vector3? trackedTargetPoint = null)
{
// 1. Push velocity into 5-frame ring, get average.
PushVelocity(_velocityRing, ref _velocityCount, playerVelocity);
Vector3 avgVel = AverageVelocity(_velocityRing, _velocityCount);
// 2. Heading vector — player's facing projected onto the contact
// plane (grounded) or world XY (airborne). See ComputeHeading
// doc + retail decomp :95644-95795 for why this is facing-based
// rather than velocity-based.
Vector3 heading = ComputeHeading(
avgVel,
playerYaw + YawOffset,
isOnGround,
contactPlaneNormal,
CameraDiagnostics.AlignToSlope);
// 2. Heading vector. TrackTarget uses the pivot-to-target direction;
// otherwise this is the player's facing projected onto the contact
// plane (grounded) or world XY (airborne). See named retail
// CameraSet::TrackTarget 0x00458280 and CameraManager::UpdateCamera
// 0x00456826-0x00456915.
Vector3 pivotWorld = playerPosition + new Vector3(0f, 0f, PivotHeight);
Vector3 heading = ComputeTrackedHeading(pivotWorld, trackedTargetPoint)
?? ComputeHeading(
avgVel,
playerYaw + YawOffset,
isOnGround,
contactPlaneNormal,
CameraDiagnostics.AlignToSlope);
// 3. Orthonormal heading-frame basis.
var (forward, _, up) = BuildBasis(heading);
// 4. Target pose.
Vector3 pivotWorld = playerPosition + new Vector3(0f, 0f, PivotHeight);
float horizontal = Distance * MathF.Cos(Pitch);
float vertical = Distance * MathF.Sin(Pitch);
// viewer_offset = -horizontal along forward + vertical along up.
@ -359,6 +362,24 @@ public sealed class RetailChaseCamera : ICamera
return Vector3.Normalize(projected);
}
/// <summary>
/// Port of the LOOK_AT_OBJECT direction contribution in retail
/// CameraManager::UpdateCamera (0x00456826-0x00456915). The caller
/// supplies the transformed world-space target point, including retail's
/// local (0,0,0.5) target offset. A coincident point cannot define a
/// heading, so the ordinary player-facing path remains active.
/// </summary>
internal static Vector3? ComputeTrackedHeading(Vector3 pivotWorld, Vector3? trackedTargetPoint)
{
if (trackedTargetPoint is not Vector3 targetPoint)
return null;
Vector3 towardTarget = targetPoint - pivotWorld;
return towardTarget.LengthSquared() < 1e-8f
? null
: Vector3.Normalize(towardTarget);
}
/// <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