From 5145938d06727ee8b97018a535c7ac867d0b4a70 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 26 Apr 2026 17:17:13 +0200 Subject: [PATCH] =?UTF-8?q?fix(physics):=20jump=20arc=20was=20zero=20?= =?UTF-8?q?=E2=80=94=20stop=20pre-seeding=20ContactPlane=20while=20airborn?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live diagnostic (extent=1.000, vz=9.09 — formula peak 4.21m) showed the body's Velocity.Z stayed at ~9 m/s but Position.Z never advanced past 66.000 even after 575 frames airborne. The collision resolver was snapping the player back to ground every step. Root cause: PhysicsEngine.ResolveWithTransition unconditionally pre-seeded the Transition's CollisionInfo from body.ContactPlane before each resolve (a slope-walking continuity hack). Once airborne, that pre-seed makes Transition.CollisionInfo's ContactPlaneValid stay true. Then in AdjustOffset's "Have a contact plane" path, when collisionAngle > 0 (offset moving AWAY from the plane = jumping up), the code calls Plane::snap_to_plane on the offset which ZEROES the Z component for flat ground (Normal.Z=1, plane.D=0 → snap_to_plane sets vec.z = 0). The horizontal X/Y parts of the offset survived; vertical Z was destroyed every step. Position.Z only ever got the gravity drift back down, so the "jump" was literally a sub-frame upward blip followed by 575 frames of stuck-at-ground while gravity ate vz. Retail's CTransition::init at retail address 0x509dd0 (named-retail line 271954) explicitly sets contact_plane_valid = 0 at the start of every transition resolve. ValidateWalkable then re-establishes it during the sweep when the foot sphere bottom is within EPSILON of the terrain plane — so for grounded motion the plane is set fresh per frame, and for airborne motion no plane interferes. Fix: only seed the contact plane when isOnGround is true. Airborne resolves now start with no plane, so AdjustOffset preserves the upward Z and the integrator's positional update actually lands. Slope-walking continuity is preserved because the seed still fires whenever the body is grounded. Diagnostic logging stripped after the fix. Tests stay 1222 green. Live verification pending. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/AcDream.Core/Physics/PhysicsEngine.cs | 27 ++++++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) 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,