diff --git a/src/AcDream.Core/Physics/BSPQuery.cs b/src/AcDream.Core/Physics/BSPQuery.cs index 7d7a37b..fceddb6 100644 --- a/src/AcDream.Core/Physics/BSPQuery.cs +++ b/src/AcDream.Core/Physics/BSPQuery.cs @@ -1036,8 +1036,11 @@ public static class BSPQuery Transition transition, CollisionSphere checkPos, Vector3 up, - float scale) + float scale, + Quaternion localToWorld = default) { + if (localToWorld == default) localToWorld = Quaternion.Identity; + var path = transition.SpherePath; var collisions = transition.CollisionInfo; @@ -1053,16 +1056,18 @@ public static class BSPQuery if (changed && polyHit is not null) { + // ACE: path.LocalSpacePos.LocalToGlobalVec(adjusted) * scale var adjusted = validPos.Center - checkPos.Center; - var offset = adjusted * scale; + var offset = Vector3.Transform(adjusted, localToWorld) * scale; path.AddOffsetToCheckPos(offset); + var worldNormal = Vector3.Transform(polyHit.Plane.Normal, localToWorld); collisions.SetContactPlane( - new Plane(polyHit.Plane.Normal, polyHit.Plane.D * scale), + new Plane(worldNormal, polyHit.Plane.D * scale), path.CheckCellId, false); path.WalkableValid = true; - path.WalkablePlane = new Plane(polyHit.Plane.Normal, polyHit.Plane.D * scale); + path.WalkablePlane = new Plane(worldNormal, polyHit.Plane.D * scale); path.WalkableAllowance = PhysicsGlobals.FloorZ; return TransitionState.Adjusted; @@ -1157,13 +1162,17 @@ public static class BSPQuery Vector3 curPos, ResolvedPolygon hitPoly, Vector3 contactPoint, - float scale) + float scale, + Quaternion localToWorld = default) { + if (localToWorld == default) localToWorld = Quaternion.Identity; + var obj = transition.ObjectInfo; var path = transition.SpherePath; var collisions = transition.CollisionInfo; - var collisionNormal = hitPoly.Plane.Normal; + // ACE: path.LocalSpacePos.LocalToGlobalVec(hitPoly.Plane.Normal) + var collisionNormal = Vector3.Transform(hitPoly.Plane.Normal, localToWorld); if (!obj.State.HasFlag(ObjectInfoState.PerfectClip)) { @@ -1179,7 +1188,8 @@ public static class BSPQuery collisions.SetCollisionNormal(collisionNormal); var adjusted = validPos.Center - checkPos.Center; - var offset = adjusted * scale; + // ACE: path.LocalSpacePos.LocalToGlobalVec(adjusted) * scale + var offset = Vector3.Transform(adjusted, localToWorld) * scale; path.AddOffsetToCheckPos(offset); return TransitionState.Adjusted; @@ -1197,11 +1207,14 @@ public static class BSPQuery private static TransitionState NegPolyHitDispatch( SpherePath path, ResolvedPolygon hitPoly, - bool stepUp) + bool stepUp, + Quaternion localToWorld = default) { + if (localToWorld == default) localToWorld = Quaternion.Identity; path.NegPolyHit = true; path.NegStepUp = stepUp; - path.NegCollisionNormal = hitPoly.Plane.Normal; + // ACE: path.LocalSpacePos.LocalToGlobalVec(hitPoly.Plane.Normal) + path.NegCollisionNormal = Vector3.Transform(hitPoly.Plane.Normal, localToWorld); return TransitionState.OK; } @@ -1336,6 +1349,12 @@ public static class BSPQuery /// Previous center of the primary sphere in local space. /// Up vector in object-local space (usually Vector3.UnitZ). /// Scale factor for the collision object. + /// + /// Rotation that transforms vectors from object-local space back to world space. + /// ACE: path.LocalSpacePos.LocalToGlobalVec(). For indoor cells with identity + /// transform, pass Quaternion.Identity. For rotated objects, pass the object's + /// rotation quaternion. + /// public static TransitionState FindCollisions( PhysicsBSPNode? root, Dictionary resolved, @@ -1344,9 +1363,12 @@ public static class BSPQuery DatReaderWriter.Types.Sphere? localSphere1, Vector3 localCurrCenter, Vector3 localSpaceZ, - float scale) + float scale, + Quaternion localToWorld = default) { if (root is null) return TransitionState.OK; + // Default quaternion (0,0,0,0) → treat as identity + if (localToWorld == default) localToWorld = Quaternion.Identity; var path = transition.SpherePath; var collisions = transition.CollisionInfo; @@ -1359,13 +1381,15 @@ public static class BSPQuery var movement = sphere0.Center - localCurrCenter; + // Helper: transform a local-space vector to world space. + // ACE: path.LocalSpacePos.LocalToGlobalVec(v) + Vector3 L2W(Vector3 v) => Vector3.Transform(v, localToWorld); + // ---------------------------------------------------------------- // Path 1: Placement or Ethereal → sphere_intersects_solid - // ACE: if (path.InsertType == InsertType.Placement || path.ObstructionEthereal) // ---------------------------------------------------------------- if (path.InsertType == InsertType.Placement || obj.Ethereal) { - // clearCell=true unless BuildingCheck && HitsInteriorCell (not exposed here). const bool clearCell = true; if (SphereIntersectsSolidInternal(root, resolved, sphere0, clearCell)) @@ -1380,7 +1404,6 @@ public static class BSPQuery // ---------------------------------------------------------------- // Path 2: CheckWalkable → hits_walkable - // ACE: if (path.CheckWalkable) return check_walkable(path, localSphere, scale); // ---------------------------------------------------------------- if (path.CheckWalkable) { @@ -1389,16 +1412,15 @@ public static class BSPQuery // ---------------------------------------------------------------- // Path 3: StepDown → step_sphere_down - // ACE: if (path.StepDown) return step_sphere_down(transition, localSphere, scale); // ---------------------------------------------------------------- if (path.StepDown) { - return StepSphereDown(root, resolved, transition, sphere0, localSpaceZ, scale); + return StepSphereDown(root, resolved, transition, sphere0, localSpaceZ, scale, localToWorld); } // ---------------------------------------------------------------- // Path 4: Collide → find_walkable (land on surface) - // ACE: RootNode.find_walkable(path, validPos, ref hitPoly, movement, Z, ref changed) + // ACE transforms offset and plane normal from local→global // ---------------------------------------------------------------- if (path.Collide) { @@ -1411,15 +1433,18 @@ public static class BSPQuery if (changed && hitPoly is not null) { - var offset = (validPos.Center - sphere0.Center) * scale; - path.AddOffsetToCheckPos(offset); + // ACE: var offset = LocalToGlobalVec(validPos.Center - localSphere.Center) * scale + var localOffset = validPos.Center - sphere0.Center; + var worldOffset = L2W(localOffset) * scale; + path.AddOffsetToCheckPos(worldOffset); + var worldNormal = L2W(hitPoly.Plane.Normal); collisions.SetContactPlane( - new Plane(hitPoly.Plane.Normal, hitPoly.Plane.D * scale), + new Plane(worldNormal, hitPoly.Plane.D * scale), path.CheckCellId, false); path.WalkableValid = true; - path.WalkablePlane = new Plane(hitPoly.Plane.Normal, hitPoly.Plane.D * scale); + path.WalkablePlane = new Plane(worldNormal, hitPoly.Plane.D * scale); path.WalkableAllowance = PhysicsGlobals.FloorZ; return TransitionState.Adjusted; @@ -1429,7 +1454,7 @@ public static class BSPQuery // ---------------------------------------------------------------- // Path 5: Contact — sphere_intersects_poly + step_sphere_up / slide - // ACE: if (obj.State.HasFlag(ObjectInfoState.Contact)) + // ACE transforms collision normal from local→global before step_up/slide // ---------------------------------------------------------------- if (obj.State.HasFlag(ObjectInfoState.Contact)) { @@ -1439,7 +1464,8 @@ public static class BSPQuery if (SphereIntersectsPolyInternal(root, resolved, sphere0, movement, ref hitPoly0, ref contact0)) { - return StepSphereUp(transition, hitPoly0!.Plane.Normal); + var worldNormal = L2W(hitPoly0!.Plane.Normal); + return StepSphereUp(transition, worldNormal); } if (sphere1 is not null) @@ -1450,16 +1476,14 @@ public static class BSPQuery if (SphereIntersectsPolyInternal(root, resolved, sphere1, movement, ref hitPoly1, ref contact1)) { - return SlideSphere(transition, hitPoly1!.Plane.Normal); + var worldNormal = L2W(hitPoly1!.Plane.Normal); + return SlideSphere(transition, worldNormal); } - // ACE checks hitPoly1 != null and hitPoly0 != null after the calls - // (the traversal may record a hitPoly even when pos_hits_sphere - // returned false, because the movement dot filtered it out). if (hitPoly1 is not null) - return NegPolyHitDispatch(path, hitPoly1, false); + return NegPolyHitDispatch(path, hitPoly1, false, localToWorld); if (hitPoly0 is not null) - return NegPolyHitDispatch(path, hitPoly0, true); + return NegPolyHitDispatch(path, hitPoly0, true, localToWorld); } return TransitionState.OK; @@ -1467,7 +1491,7 @@ public static class BSPQuery // ---------------------------------------------------------------- // Path 6: Default — sphere_intersects_poly → collide_with_pt / land - // ACE: RootNode.sphere_intersects_poly(localSphere, movement, ref hitPoly, ref cp) || hitPoly != null + // ACE transforms normals from local→global // ---------------------------------------------------------------- { ResolvedPolygon? hitPoly0 = null; @@ -1482,14 +1506,13 @@ public static class BSPQuery { return CollideWithPt(root, resolved, transition, sphere0, localCurrCenter, - hitPoly0!, contact0, scale); + hitPoly0!, contact0, scale, localToWorld); } - var normal = hitPoly0!.Plane.Normal; + var worldNormal = L2W(hitPoly0!.Plane.Normal); path.WalkableAllowance = PhysicsGlobals.LandingZ; - // ACE: path.SetCollide(collisionNormal) — sets Collide=true + stores normal. path.Collide = true; - collisions.SetCollisionNormal(normal); + collisions.SetCollisionNormal(worldNormal); return TransitionState.Adjusted; } @@ -1503,7 +1526,8 @@ public static class BSPQuery if (hit1 || hitPoly1 is not null) { - collisions.SetCollisionNormal(hitPoly1!.Plane.Normal); + var worldNormal = L2W(hitPoly1!.Plane.Normal); + collisions.SetCollisionNormal(worldNormal); return TransitionState.Collided; } } diff --git a/src/AcDream.Core/Physics/TransitionTypes.cs b/src/AcDream.Core/Physics/TransitionTypes.cs index b6a500f..2b30b34 100644 --- a/src/AcDream.Core/Physics/TransitionTypes.cs +++ b/src/AcDream.Core/Physics/TransitionTypes.cs @@ -745,6 +745,9 @@ public sealed class Transition var localSpaceZ = Vector3.Transform(Vector3.UnitZ, invRot); // Use the retail 6-path dispatcher with pre-resolved polygons. + // Pass the object's rotation so collision responses (normals, + // offsets) are transformed from object-local back to world space. + // ACE: path.LocalSpacePos.LocalToGlobalVec() result = BSPQuery.FindCollisions( physics.BSP.Root, physics.Resolved, @@ -753,7 +756,8 @@ public sealed class Transition localSphere1, localCurrCenter, localSpaceZ, - 1.0f); // scale = 1.0 for object geometry + 1.0f, // scale = 1.0 for object geometry + obj.Rotation); // local→world rotation } else {