diff --git a/src/AcDream.Core/Physics/TransitionTypes.cs b/src/AcDream.Core/Physics/TransitionTypes.cs index f2c4f6c..1a3a12f 100644 --- a/src/AcDream.Core/Physics/TransitionTypes.cs +++ b/src/AcDream.Core/Physics/TransitionTypes.cs @@ -1389,6 +1389,7 @@ public sealed class Transition var sp = SpherePath; var oi = ObjectInfo; + var ci = CollisionInfo; // #42 diagnostic (2026-05-05): identify which static object causes // the airborne first-frame ~1m push. Capture sphere check pos at @@ -1460,6 +1461,14 @@ public sealed class Transition if (CollisionExemption.ShouldSkip(obj.State, obj.Flags, ObjectInfo.State)) continue; + // L.2a slice 3 (2026-05-12): snapshot collision-normal state so + // we can tell whether THIS object's BSP/CylSphere test produced a + // new collision (BSPQuery sets the normal but may still return OK + // for slide cases). Together with the `result != OK` check below + // this populates ci.CollideObjectGuids + LastCollidedObjectGuid so + // the [resolve] probe surfaces the responsible entity id. + bool collisionWasValidPre = ci.CollisionNormalValid; + TransitionState result; if (obj.CollisionType == ShadowCollisionType.BSP) @@ -1523,6 +1532,22 @@ public sealed class Transition result = CylinderCollision(obj, sp); } + // L.2a slice 3: attribute the collision (if any) to this entity. + // Two cases: + // - result != OK: the object stopped the transition (hard-block). + // - result == OK but the normal flipped from invalid→valid during + // this call: BSPQuery captured a slide normal without halting. + // Either way this object is responsible for the hit, so add its + // entity id. CollideObjectGuids carries the full chain; the last + // assignment to LastCollidedObjectGuid wins which matches retail's + // "most recent" semantics for the probe. + if (result != TransitionState.OK + || (!collisionWasValidPre && ci.CollisionNormalValid)) + { + ci.CollideObjectGuids.Add(obj.EntityId); + ci.LastCollidedObjectGuid = obj.EntityId; + } + if (result != TransitionState.OK) { if (airborneDiag)