diff --git a/src/AcDream.Core/Physics/PhysicsEngine.cs b/src/AcDream.Core/Physics/PhysicsEngine.cs index c938c36..bb26c54 100644 --- a/src/AcDream.Core/Physics/PhysicsEngine.cs +++ b/src/AcDream.Core/Physics/PhysicsEngine.cs @@ -398,12 +398,27 @@ public sealed class PhysicsEngine if (isOnGround) transition.ObjectInfo.State |= ObjectInfoState.Contact | ObjectInfoState.OnWalkable; - // Seed the transition's CollisionInfo with the previous frame's - // contact plane (retail PhysicsObj field). Without this, every - // ResolveWithTransition call starts with a fresh plane, AdjustOffset's - // "Have a contact plane" branch never fires, and slope projection - // never happens. - if (body is not null && body.ContactPlaneValid) + // K-fix7 (2026-04-26): only seed the contact plane when the body + // is actually grounded. Pre-seeding while AIRBORNE caused + // AdjustOffset's "Have a contact plane / Moving away from plane" + // branch to fire on every jump step — which calls + // Plane::snap_to_plane on the offset and ZEROES the Z component, + // killing all upward jump motion (the body's Z velocity stayed + // ~9 m/s but Position.Z never advanced because every step's + // offset got snapped flat). Retail's CTransition::init at + // 0x509dd0 (named-retail line 271954) explicitly clears + // contact_plane_valid = 0 at the start of every transition + // resolve, then ValidateWalkable re-establishes it during the + // sweep when the sphere bottom is within EPSILON of the terrain + // plane — so for grounded motion the plane is set fresh every + // resolve, and for airborne motion no plane interferes. + // + // We KEEP the seeding when isOnGround for slope-walking + // continuity (the original concern that motivated the seed) — + // walking up a hill needs the previous step's slope to project + // movement properly. Airborne / jumping must start with no + // plane so AdjustOffset preserves Z. + if (isOnGround && body is not null && body.ContactPlaneValid) { transition.CollisionInfo.SetContactPlane( body.ContactPlane,