fix(combat): match retail live attack feedback

Keep AttackDone control statuses out of chat, preserve dead motion across zero-velocity position updates, render the authored power meter from right to left, and retain the rotatable viewer offset during target tracking.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-11 21:10:01 +02:00
parent 33cc9aa16a
commit 5276a83087
16 changed files with 231 additions and 67 deletions

View file

@ -156,7 +156,8 @@ public sealed class RetailChaseCamera : ICamera
// CameraSet::TrackTarget 0x00458280 and CameraManager::UpdateCamera
// 0x00456826-0x00456915.
Vector3 pivotWorld = playerPosition + new Vector3(0f, 0f, PivotHeight);
Vector3 heading = ComputeTrackedHeading(pivotWorld, trackedTargetPoint)
Vector3? trackedHeading = ComputeTrackedHeading(pivotWorld, trackedTargetPoint);
Vector3 heading = trackedHeading
?? ComputeHeading(
avgVel,
playerYaw + YawOffset,
@ -164,15 +165,14 @@ public sealed class RetailChaseCamera : ICamera
contactPlaneNormal,
CameraDiagnostics.AlignToSlope);
// 3. Orthonormal heading-frame basis.
var (forward, _, up) = BuildBasis(heading);
// 4. Target pose.
float horizontal = Distance * MathF.Cos(Pitch);
float vertical = Distance * MathF.Sin(Pitch);
// viewer_offset = -horizontal along forward + vertical along up.
Vector3 targetEye = pivotWorld + forward * (-horizontal) + up * vertical;
Vector3 targetForward = Vector3.Normalize(pivotWorld - targetEye);
// 3-4. Target pose. Retail TrackTarget changes the frame that
// transforms viewer_offset; it does not replace that offset. CameraSet::Rotate
// (0x00458310) therefore continues to orbit viewer_offset.x/y while the
// 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);
// 5. Stateful sought position (#180). Retail CameraManager::UpdateCamera
// (0x00456660) interpolates FROM THE CURRENT SWEPT VIEWER toward the
@ -380,6 +380,35 @@ public sealed class RetailChaseCamera : ICamera
: Vector3.Normalize(towardTarget);
}
/// <summary>
/// Ports retail's heading-frame transform of <c>viewer_offset</c> in
/// CameraManager::UpdateCamera (0x00456660), including the X/Y rotation
/// retained by CameraSet::Rotate (0x00458310) while target tracking is active.
/// The returned forward always looks back at the player pivot, matching
/// LOOK_AT_PIVOT.
/// </summary>
internal static (Vector3 eye, Vector3 forward) ComputeDesiredPose(
Vector3 pivotWorld,
Vector3 heading,
float distance,
float pitch,
float viewerYawOffset)
{
var (frameForward, frameRight, frameUp) = BuildBasis(heading);
// AC's frame-local +Y points opposite our screen-right basis. Rotating
// viewer_offset.x/y by +yaw therefore produces this boom direction.
Vector3 boomForward = Vector3.Normalize(
frameForward * MathF.Cos(viewerYawOffset)
- frameRight * MathF.Sin(viewerYawOffset));
float horizontal = distance * MathF.Cos(pitch);
float vertical = distance * MathF.Sin(pitch);
Vector3 eye = pivotWorld - boomForward * horizontal + frameUp * vertical;
Vector3 forward = Vector3.Normalize(pivotWorld - eye);
return (eye, forward);
}
/// <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