fix(camera): smooth chase-camera Z follow so the jump arc is visible on screen

Diagnostic from K-fix10 confirmed our local jump physics is
mathematically perfect — every full-charge jump produces
formulaPeak = actualPeakDz = vz²/19.6 to four-digit precision
(3.11 m for Jump skill 208). Yet the user observed retail
clients seeing the SAME character jump much higher than ACdream
sees of itself.

Root cause: ChaseCamera tracked player.Z 1:1. When the player
rises 3 m the camera rises 3 m too — the player's screen
position never changes during the arc, so the jump is visually
invisible. Retail's chase camera lags the Z follow, so an
observer sees the player visibly rise on screen.

Fix: low-pass filter the camera's Z target.
ChaseCamera.Update gains a dt parameter and an exponential
smoother:
    alpha = 1 - exp(-dt / ZFollowTimeConstant)
    smoothedZ += (player.Z - smoothedZ) * alpha
ZFollowTimeConstant defaults to 0.15 s — slow enough that a
~1 s jump arc shows up clearly on screen, fast enough that
slope walking still feels glued. The look-at point still uses
the raw player Z so the camera tilts up to keep the airborne
character in frame.

Drive-by: stripped K-fix10 jump diagnostic logging now that the
math has been confirmed correct.

Tests stay 1222 green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-26 18:00:58 +02:00
parent 13cc08e506
commit 05ce090346
3 changed files with 43 additions and 33 deletions

View file

@ -130,12 +130,6 @@ public sealed class PlayerMovementController
// Jump charge state.
private bool _jumpCharging;
private float _jumpExtent;
// K-fix10 diag (2026-04-26): track airborne arc height for the
// jump-too-low investigation. Strip after fix.
private bool _jumpDiagSampling;
private float _jumpDiagStartZ;
private float _jumpDiagPeakZ;
private float _jumpDiagSentVz;
// K-fix6 (2026-04-26): retail's PowerBar charge constant for jump is
// not legible in the named decomp (the divisor was clobbered in
// GetPowerBarLevel's FPU stack reordering at FUN_0056ade0). 2.0/s
@ -397,16 +391,6 @@ public sealed class PlayerMovementController
_motion.LeaveGround();
outJumpExtent = _jumpExtent;
outJumpVelocity = _body.Velocity; // capture after LeaveGround applies it
_jumpDiagSampling = true;
_jumpDiagStartZ = _body.Position.Z;
_jumpDiagPeakZ = _body.Position.Z;
_jumpDiagSentVz = _body.Velocity.Z;
Console.WriteLine(
$"[jump.send] extent={_jumpExtent:F3} sentVz={_body.Velocity.Z:F3} " +
$"sentVel=({_body.Velocity.X:F2},{_body.Velocity.Y:F2},{_body.Velocity.Z:F2}) " +
$"formulaPeak={_body.Velocity.Z * _body.Velocity.Z / 19.6f:F2}m " +
$"startZ={_body.Position.Z:F2}");
}
_jumpCharging = false;
_jumpExtent = 0f;
@ -451,14 +435,6 @@ public sealed class PlayerMovementController
{
_motion.HitGround();
justLanded = true;
if (_jumpDiagSampling)
{
Console.WriteLine(
$"[jump.peak] sentVz={_jumpDiagSentVz:F3} formulaPeak={_jumpDiagSentVz * _jumpDiagSentVz / 19.6f:F2}m " +
$"actualPeakDz={(_jumpDiagPeakZ - _jumpDiagStartZ):F2}m " +
$"startZ={_jumpDiagStartZ:F2} peakZ={_jumpDiagPeakZ:F2} landZ={_body.Position.Z:F2}");
_jumpDiagSampling = false;
}
}
}
else
@ -475,10 +451,6 @@ public sealed class PlayerMovementController
_body.calc_acceleration();
}
// K-fix10 diag: peak Z tracking — placed AFTER the resolve branch
// so it doesn't disrupt control flow.
if (_jumpDiagSampling && _body.Position.Z > _jumpDiagPeakZ)
_jumpDiagPeakZ = _body.Position.Z;
_wasAirborneLastFrame = !_body.OnWalkable;
CellId = resolveResult.CellId;