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
{