diff --git a/src/AcDream.Core/Physics/TransitionTypes.cs b/src/AcDream.Core/Physics/TransitionTypes.cs index a2f5a5ce..9879e624 100644 --- a/src/AcDream.Core/Physics/TransitionTypes.cs +++ b/src/AcDream.Core/Physics/TransitionTypes.cs @@ -1070,18 +1070,33 @@ public sealed class Transition } else { - // Retail CSphere::slide_sphere — the full retail version - // adjusts sphere position via add_offset_to_check_pos + - // returns Adjusted (on success) or Collided (degenerate). - // Our simpler response: record the collision normal + - // return Collided. The outer engine sees Collided and - // does NOT advance the sphere position — block achieved. + // Retail neg_step_up==0 (HEAD-sphere near-miss) → CSphere::slide_sphere, + // then the insert loop continues (re-tests at the slid position): + // CTransition::transitional_insert, acclient_2013_pseudo_c.txt:273350-273351 + // if (sphere_path.neg_step_up == 0) + // edi = CSphere::slide_sphere(global_sphere, sphere_path, + // collision_info, neg_collision_normal, + // global_curr_center); // - // A6.P4 door inside-out fix (2026-05-25): user-visible - // blocking is the goal (retail behavior); the full - // slide-position adjustment can be a later iteration. - ci.SetCollisionNormal(sp.NegCollisionNormal); - return TransitionState.Collided; + // Earlier (A6.P4, 2026-05-25) this branch returned Collided as a + // simplification so closed doors would block. But that hard stop + // ALSO wedged a grounded mover whose HEAD sphere brushed a wall + // while moving along it — e.g. exiting the Holtburg cottage cellar: + // the body reached the cottage floor (Z=94) but oscillated against + // the stairwell walls with no slide (2026-06-04 live capture, 16k + // frames: 274 (0,-1,0) + 78 (1,0,0) hits, out==current). The + // faithful slide_sphere slides tangentially along the wall (crease = + // collisionNormal × contactPlane.Normal), which un-wedges the cellar + // AND still blocks a closed door — the into-door (+Y) component is + // removed and only the tangential X slide survives, so there is no + // walkthrough. + var slideRes = SlideSphereInternal( + sp.NegCollisionNormal, sp.GlobalCurrCenter[0].Origin); + if (slideRes == TransitionState.Collided) + return TransitionState.Collided; // degenerate slide → hard stop + // Slid / Adjusted / OK → re-test at the (slid) CheckPos, mirroring + // retail's insert-loop continuation after slide_sphere. + continue; } }