fix(p2): port retail slide_sphere for head near-miss (cellar wall-slide)

Replace the A6.P4 Collided shortcut in transitional_insert's neg_step_up==0
branch with the faithful CSphere::slide_sphere. Retail
(acclient_2013_pseudo_c.txt:273350-273351, CTransition::transitional_insert):
neg_step_up==0 (HEAD-sphere near-miss) -> slide_sphere -> continue the insert
loop; neg_step_up==1 (foot) -> step_up_slide. The acdream foot branch already
did that; only the head branch took the shortcut (SetCollisionNormal + return
Collided = dead hard-stop). The slide itself is the existing
SlideSphereInternal (Sphere.SlideSphere port): it strips the into-wall
component and keeps the tangential crease (collisionNormal x contactPlane.N).

Surfaced by the B1 near-miss-gate fix (abbd761): once the grounded mover climbs
onto the cottage floor, its head sphere brushes the cellar stairwell walls and
the old hard-stop wedged it (2026-06-04 live capture: 274 (0,-1,0) + 78
(1,0,0) hits, out==current, dead oscillation). Post-fix capture shows 96
hit-and-advanced frames (the body slides along the walls).

Visual-verified 2026-06-04: closed cottage door still BLOCKS (no walkthrough --
drifts sideways along it, retail-faithful); cellar ascent now works (was always
stuck). An intermittent corner-wedge (slide into the -Y/+X wall corner) remains
-- separate finer issue, under investigation.

Core 1310 pass / 4 fail (pre-existing: 3 door documents-the-bug + D4 airborne).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-04 10:28:59 +02:00
parent f984e92e37
commit 0935a315bf

View file

@ -1070,18 +1070,33 @@ public sealed class Transition
} }
else else
{ {
// Retail CSphere::slide_sphere — the full retail version // Retail neg_step_up==0 (HEAD-sphere near-miss) → CSphere::slide_sphere,
// adjusts sphere position via add_offset_to_check_pos + // then the insert loop continues (re-tests at the slid position):
// returns Adjusted (on success) or Collided (degenerate). // CTransition::transitional_insert, acclient_2013_pseudo_c.txt:273350-273351
// Our simpler response: record the collision normal + // if (sphere_path.neg_step_up == 0)
// return Collided. The outer engine sees Collided and // edi = CSphere::slide_sphere(global_sphere, sphere_path,
// does NOT advance the sphere position — block achieved. // collision_info, neg_collision_normal,
// global_curr_center);
// //
// A6.P4 door inside-out fix (2026-05-25): user-visible // Earlier (A6.P4, 2026-05-25) this branch returned Collided as a
// blocking is the goal (retail behavior); the full // simplification so closed doors would block. But that hard stop
// slide-position adjustment can be a later iteration. // ALSO wedged a grounded mover whose HEAD sphere brushed a wall
ci.SetCollisionNormal(sp.NegCollisionNormal); // while moving along it — e.g. exiting the Holtburg cottage cellar:
return TransitionState.Collided; // 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;
} }
} }