From 31cd5480dcb7c63d167e4fec37c2d0b60548f459 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 14 Apr 2026 10:13:27 +0200 Subject: [PATCH] fix(physics): jump apex velocity zeroing bug SmallVelocity threshold (0.25 m/s) in UpdatePhysicsInternal was zeroing velocity every frame while airborne at the jump apex. With vel~0.01 m/s and gravity adding only 0.012/frame, the zeroing won every frame and the character got stuck at peak height forever. Fix: only apply small-velocity zeroing when OnWalkable (grounded). While airborne, gravity must accumulate freely through the zero-crossing. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Input/PlayerMovementController.cs | 20 +++++++++++++++++++ src/AcDream.App/Rendering/GameWindow.cs | 15 +++++++------- src/AcDream.Core/Physics/PhysicsBody.cs | 5 ++++- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/AcDream.App/Input/PlayerMovementController.cs b/src/AcDream.App/Input/PlayerMovementController.cs index 28ff407..97e8226 100644 --- a/src/AcDream.App/Input/PlayerMovementController.cs +++ b/src/AcDream.App/Input/PlayerMovementController.cs @@ -97,6 +97,8 @@ public sealed class PlayerMovementController // Jump charge state. private bool _jumpCharging; + private int _debugSpeedLogCounter; + private int _debugJumpFrames; private float _jumpExtent; private const float JumpChargeRate = 1.0f; // 0→1 over 1 second @@ -244,6 +246,8 @@ public sealed class PlayerMovementController else if (input.StrafeLeft) localX = -MotionInterpreter.SidestepAnimSpeed * 0.5f; + if (input.Forward && _debugSpeedLogCounter++ % 60 == 0) + Console.WriteLine($"DEBUG speed: stateVel.Y={stateVel.Y:F2} MyRunRate={_motion.MyRunRate:F3} localY={localY:F2}"); _body.set_local_velocity(new Vector3(localX, localY, savedWorldVz)); } @@ -271,6 +275,12 @@ public sealed class PlayerMovementController { _motion.LeaveGround(); outJumpExtent = _jumpExtent; + _debugJumpFrames = 500; // log until landing + Console.WriteLine($"DEBUG jump FIRED: extent={_jumpExtent:F2} vel={_body.Velocity} onWalk={_body.OnWalkable}"); + } + else + { + Console.WriteLine($"DEBUG jump FAILED: extent={_jumpExtent:F2} result={jumpResult} onWalk={_body.OnWalkable} contact={_body.InContact}"); } _jumpCharging = false; _jumpExtent = 0f; @@ -285,6 +295,12 @@ public sealed class PlayerMovementController _body.calc_acceleration(); _body.UpdatePhysicsInternal(dt); + if (_debugJumpFrames > 0) + { + _debugJumpFrames--; + Console.WriteLine($"DEBUG jump frame: pos.Z={_body.Position.Z:F3} vel.Z={_body.Velocity.Z:F3} onWalk={_body.OnWalkable} accel.Z={_body.Acceleration.Z:F1} dt={dt:F4}"); + } + // ── 5. Terrain/cell Z snap and ground-contact detection ─────────────── // Use PhysicsEngine.Resolve to find the ground surface Z under the player. // We pass a zero delta because PhysicsBody already moved the position. @@ -310,7 +326,11 @@ public sealed class PlayerMovementController _body.Velocity = new Vector3(_body.Velocity.X, _body.Velocity.Y, 0f); if (wasAirborne) + { + Console.WriteLine($"DEBUG LANDED: bodyZ={bodyZ:F3} groundZ={groundZ:F3} vel.Z={_body.Velocity.Z:F3}"); _motion.HitGround(); + _debugJumpFrames = 0; + } } else { diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index bd878c1..2e279f6 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -960,21 +960,22 @@ public sealed class GameWindow : IDisposable && newCycle.HighFrame > newCycle.LowFrame && newCycle.Animation.PartFrames.Count > 1; - if (!newCycleIsGood) - return; - - // Wire server-echoed RunRate into the player's MotionInterpreter. - // The server broadcasts the character's real Run-skill-derived ForwardSpeed - // in UpdateMotion; without this the player would always move at 4.0 m/s - // (ForwardSpeed = 1.0 hardcoded in MotionInterpreter defaults). + // Wire server-echoed RunRate BEFORE the animation early-return. + // If the cycle can't resolve (bad stance), we still need the speed. if (_playerController is not null && update.Guid == _playerServerGuid && update.MotionState.ForwardSpeed.HasValue && update.MotionState.ForwardSpeed.Value > 0f) { + Console.WriteLine($"DEBUG RunRate: guid={update.Guid:X8} fwd={update.MotionState.ForwardSpeed.Value:F3}"); _playerController.ApplyServerRunRate(update.MotionState.ForwardSpeed.Value); } + if (!newCycleIsGood) + return; + + // (RunRate wiring moved above the early-return) + // Sequencer path if (ae.Sequencer is not null) { diff --git a/src/AcDream.Core/Physics/PhysicsBody.cs b/src/AcDream.Core/Physics/PhysicsBody.cs index 06b6781..d3fc908 100644 --- a/src/AcDream.Core/Physics/PhysicsBody.cs +++ b/src/AcDream.Core/Physics/PhysicsBody.cs @@ -298,7 +298,10 @@ public sealed class PhysicsBody calc_friction(dt, velocityMag2); // If velocity fell below the "small" threshold after friction, stop. - if (velocityMag2 - SmallVelocitySquared < 0.0002f) + // Only apply when grounded — while airborne, gravity must accumulate + // even when velocity is near zero (e.g., at jump apex). + if (velocityMag2 - SmallVelocitySquared < 0.0002f + && TransientState.HasFlag(TransientStateFlags.OnWalkable)) Velocity = Vector3.Zero; // Euler integration: position += v*dt + 0.5*a*dt²