diff --git a/src/AcDream.App/Input/PlayerMovementController.cs b/src/AcDream.App/Input/PlayerMovementController.cs
index 4326599..d3c25c4 100644
--- a/src/AcDream.App/Input/PlayerMovementController.cs
+++ b/src/AcDream.App/Input/PlayerMovementController.cs
@@ -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;
diff --git a/src/AcDream.App/Rendering/ChaseCamera.cs b/src/AcDream.App/Rendering/ChaseCamera.cs
index c778ff6..174df3a 100644
--- a/src/AcDream.App/Rendering/ChaseCamera.cs
+++ b/src/AcDream.App/Rendering/ChaseCamera.cs
@@ -46,6 +46,22 @@ public sealed class ChaseCamera : ICamera
private float _playerYaw;
private Vector3 _lookAt;
+ // K-fix11 (2026-04-26): smoothed Z follow. Without this the camera
+ // tracks player.Z 1:1 — during a 3 m jump the camera also rises 3 m
+ // and the player's screen position never changes, making the jump
+ // visually invisible. A retail observer sees ACdream's jump arc
+ // clearly because retail's chase camera lags the Z follow. Mirror
+ // that: the camera Z target uses an exponential low-pass filter
+ // toward player.Z with a ~150 ms time constant. Initialised on
+ // first Update so spawn doesn't snap.
+ private float _smoothedPlayerZ;
+ private bool _smoothedZInitialised;
+ /// Time constant for the camera-Z smoothing filter (seconds).
+ /// Smaller = camera catches up faster (less perceptible jump arc on
+ /// screen). Larger = jumps look bigger but camera lags on slope
+ /// transitions. 0.15 = retail-feel feedback for a ~0.5–1.0 s jump.
+ public float ZFollowTimeConstant { get; set; } = 0.15f;
+
public Matrix4x4 View =>
Matrix4x4.CreateLookAt(Position, _lookAt, Vector3.UnitZ);
@@ -53,11 +69,30 @@ public sealed class ChaseCamera : ICamera
Matrix4x4.CreatePerspectiveFieldOfView(FovY, Aspect, 1f, 5000f);
///
- /// Update the camera position to follow the player.
+ /// Update the camera position to follow the player.
+ /// drives the Z-smoothing filter.
///
- public void Update(Vector3 playerPosition, float playerYaw)
+ public void Update(Vector3 playerPosition, float playerYaw, float dt = 1f / 60f)
{
_playerYaw = playerYaw;
+
+ // K-fix11: smooth the camera's Z target using an exponential
+ // low-pass filter. Camera Z lags player Z so airborne motion
+ // visibly shows up on screen.
+ if (!_smoothedZInitialised)
+ {
+ _smoothedPlayerZ = playerPosition.Z;
+ _smoothedZInitialised = true;
+ }
+ else
+ {
+ float alpha = 1f - MathF.Exp(-MathF.Max(dt, 1e-4f) / MathF.Max(ZFollowTimeConstant, 1e-4f));
+ _smoothedPlayerZ += (playerPosition.Z - _smoothedPlayerZ) * alpha;
+ }
+
+ // Look-at uses the actual player Z so the camera always points
+ // at the character — when player rises above the lagged camera
+ // the camera tilts up slightly to keep them in frame.
_lookAt = playerPosition + new Vector3(0f, 0f, EyeHeight);
// Camera offset: behind the player (-forward direction) plus any
@@ -72,7 +107,7 @@ public sealed class ChaseCamera : ICamera
Position = new Vector3(
playerPosition.X - forwardX * horizontalDist,
playerPosition.Y - forwardY * horizontalDist,
- playerPosition.Z + EyeHeight + verticalDist);
+ _smoothedPlayerZ + EyeHeight + verticalDist); // ← uses smoothed Z, not raw
}
///
diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs
index 1f6e726..a456fd9 100644
--- a/src/AcDream.App/Rendering/GameWindow.cs
+++ b/src/AcDream.App/Rendering/GameWindow.cs
@@ -3987,8 +3987,11 @@ public sealed class GameWindow : IDisposable
_worldState.RelocateEntity(pe, currentLb);
}
- // Update chase camera.
- _chaseCamera.Update(result.Position, _playerController.Yaw);
+ // Update chase camera. K-fix11 (2026-04-26): pass dt so the
+ // Z-smoothing low-pass filter tracks correctly on variable
+ // frame rates — without this the camera tracks player.Z 1:1
+ // and jumps look invisible (camera rises with player).
+ _chaseCamera.Update(result.Position, _playerController.Yaw, (float)dt);
// Send outbound movement messages to the live server.
if (_liveSession is not null)