using System; using System.Numerics; using ACE.Entity.Enum; using ACE.Server.Physics.Animation; using ACE.Server.Physics.Common; using ACE.Server.Physics.Extensions; namespace ACE.Server.Physics { /// /// Spherical collision detection /// public class Sphere: IEquatable { /// /// The center point of the sphere /// public Vector3 Center; /// /// The radius of the sphere /// public float Radius; /// /// Default constructor /// public Sphere() { } /// /// Copy constructor /// public Sphere(Sphere sphere) { Center = sphere.Center; Radius = sphere.Radius; } /// /// Constructs a sphere from a center point and radius /// /// The center point of the sphere /// The radius of the sphere public Sphere(Vector3 center, float radius) { Center = center; Radius = radius; } /// /// Constructs a sphere loaded from portal.dat /// public Sphere(DatLoader.Entity.Sphere sphere) { Center = sphere.Origin; Radius = sphere.Radius; } public const float ThresholdMed = 1.0f / 3.0f; public const float ThresholdHigh = 2.0f / 3.0f; public static Quadrant Attack(Position targetPos, float targetRadius, float targetHeight, Position attackPos, Vector2 left, Vector2 right, float attackRadius, float attackHeight) { var center = attackPos.LocalToLocal(targetPos, Vector3.Zero); if (attackHeight < 0.0f || attackHeight > targetHeight) return Quadrant.None; var radsum = targetRadius + attackRadius; var distSq = center.LengthSquared2D(); if (distSq > radsum * radsum) return Quadrant.None; var hitLoc = targetPos.LocalToLocal(attackPos, Vector3.Zero); var quadrant = hitLoc.X <= 0.0f ? Quadrant.Left : Quadrant.Right; quadrant |= hitLoc.Y > 0.0f ? Quadrant.Front : Quadrant.Back; if (attackHeight < targetHeight * ThresholdMed) quadrant |= Quadrant.Low; else if (attackHeight < targetHeight * ThresholdHigh) quadrant |= Quadrant.Medium; else quadrant |= Quadrant.High; // 2d cross product? var attack_ht = center.Y * left.X - center.X * left.Y; var right_dist = center.X * right.Y - center.Y * right.X; if (attack_ht <= 0.0f && right_dist <= 0.0f) return quadrant; if (left.X * right.Y - left.Y * right.X >= 0.0f) { if (right_dist * attack_ht <= 0.0f || attack_ht <= targetRadius || right_dist <= targetRadius) return quadrant; else return Quadrant.None; } if (attack_ht < 0.0f) { if (right_dist <= targetRadius) return quadrant; else return Quadrant.None; } if (right_dist >= 0.0f) { if (distSq <= targetRadius * targetRadius) return quadrant; else return Quadrant.None; } if (attack_ht < 0.0f) { if (right_dist <= targetRadius) return quadrant; else return Quadrant.None; } if (attack_ht <= targetRadius) return quadrant; else return Quadrant.None; } /// /// Redirects a sphere to be on collision course towards a point /// /// The transition information for the sphere /// The spherical point to redirect towards /// Currently doesn't seem to be used? /// The sum of the sphere and spherical point radii /// Used as an offset in path.GlobalCurrCenter to determine movement /// The TransitionState either collided or adjusted [Obsolete("Use CollideWithPoint without disp")] public TransitionState CollideWithPoint(Transition transition, Sphere checkPos, Vector3 disp, float radsum, int sphereNum) { return CollideWithPoint(transition, checkPos, radsum, sphereNum); } /// /// Redirects a sphere to be on collision course towards a point /// /// The transition information for the sphere /// The spherical point to redirect towards /// Currently doesn't seem to be used? /// The sum of the sphere and spherical point radii /// Used as an offset in path.GlobalCurrCenter to determine movement /// The TransitionState either collided or adjusted public TransitionState CollideWithPoint(Transition transition, Sphere checkPos, float radsum, int sphereNum) { return CollideWithPoint(Center, transition, checkPos, radsum, sphereNum); } /// /// Redirects a sphere to be on collision course towards a point /// /// The transition information for the sphere /// The spherical point to redirect towards /// Currently doesn't seem to be used? /// The sum of the sphere and spherical point radii /// Used as an offset in path.GlobalCurrCenter to determine movement /// The TransitionState either collided or adjusted public static TransitionState CollideWithPoint(Vector3 center, Transition transition, Sphere checkPos, float radsum, int sphereNum) { var obj = transition.ObjectInfo; var path = transition.SpherePath; var collisions = transition.CollisionInfo; var gCenter = path.GlobalCurrCenter[sphereNum].Center; var globalOffset = gCenter - center; // if set to PerfectClip, does a more precise check if (obj.State.HasFlag(ObjectInfoState.PerfectClip)) { var blockOffset = LandDefs.GetBlockOffset(path.CurPos.ObjCellID, path.CheckPos.ObjCellID); var checkOffset = checkPos.Center - gCenter + blockOffset; var collisionTime = FindTimeOfCollision(checkOffset, globalOffset, radsum + PhysicsGlobals.EPSILON); if (collisionTime < PhysicsGlobals.EPSILON || collisionTime > 1.0f) return TransitionState.Collided; else { var collisionOffset = checkOffset * (float)collisionTime - checkOffset; var old_disp = collisionOffset + checkPos.Center - center; var invRad = 1.0f / radsum; var collision_normal = old_disp * invRad; collisions.SetCollisionNormal(collision_normal); path.AddOffsetToCheckPos(old_disp, checkPos.Radius); return TransitionState.Adjusted; } } else { if (!Vec.NormalizeCheckSmall(ref globalOffset)) collisions.SetCollisionNormal(globalOffset); return TransitionState.Collided; } } /// /// Collision detection from legacy implementation /// public static bool CollidesWithSphere(Vector3 otherSphere, float radsum) { // original implementation with FPU flag // is touching/equal considered a collision here? return otherSphere.LengthSquared() <= radsum * radsum; } /// /// Finds the percentage along this sphere's movement path /// if/ when it collides with another sphere /// /// The movement vector for the current sphere /// The position of the other sphere /// The sum of the radii between this sphere and the other sphere /// A 0-1 interval along the movement path for the time of collision, or -1 non-collision /// Verify this could be different from original AC, which seems to return a negative interval? public static double FindTimeOfCollision(Vector3 movement, Vector3 spherePos, float radSum) { var distSq = movement.LengthSquared(); if (distSq < PhysicsGlobals.EPSILON) return -1; var nonCollide = spherePos.LengthSquared() - radSum * radSum; if (nonCollide < PhysicsGlobals.EPSILON) return -1; var similar = -Vector3.Dot(spherePos, movement); var nonCollideB = similar * similar - nonCollide * distSq; if (nonCollideB < 0) return -1; var cDist = Math.Sqrt(nonCollideB); if (similar - cDist < 0) return -1 * (cDist + similar) / distSq; else return -1 * (similar - cDist) / distSq; } /// /// Returns true if this sphere intersects with another sphere /// public bool Intersects(Sphere sphere) { var delta = sphere.Center - Center; var radSum = Radius + sphere.Radius; return delta.LengthSquared() < radSum * radSum; } /// /// Determines if this sphere collides with any other spheres during its transitions /// public TransitionState IntersectsSphere(Position position, float scale, Transition transition, bool isCreature) { var globPos = transition.SpherePath.CheckPos.LocalToGlobal(position, Center * scale); return IntersectsSphere(globPos, Radius * scale, transition, isCreature); } /// /// Determines if this sphere collides with anything during its transition /// Note: For high load scenarios, consider the static function to avoid excess heap usage: IntersectsSphere(Vector3 center, float radius, Transition transition, bool isCreature) /// /// The transition path for this sphere /// Flag indicates if this sphere is a player / monster /// The collision result for this transition path public TransitionState IntersectsSphere(Transition transition, bool isCreature) { return IntersectsSphere(Center, Radius, transition, isCreature); } /// /// Determines if this sphere collides with anything during its transition /// /// The transition path for this sphere /// Flag indicates if this sphere is a player / monster /// The collision result for this transition path public static TransitionState IntersectsSphere(Vector3 center, float radius, Transition transition, bool isCreature) { var globSphere = transition.SpherePath.GlobalSphere[0]; var disp = globSphere.Center - center; Sphere globSphere_ = null; Vector3 disp_ = Vector3.Zero; if (transition.SpherePath.NumSphere > 1) { globSphere_ = transition.SpherePath.GlobalSphere[1]; disp_ = globSphere_.Center - center; } var radsum = globSphere.Radius + radius - PhysicsGlobals.EPSILON; if (transition.SpherePath.ObstructionEthereal || transition.SpherePath.InsertType == InsertType.Placement) { if (disp.LengthSquared() <= radsum * radsum) return TransitionState.Collided; if (transition.SpherePath.NumSphere > 1) { if (CollidesWithSphere(disp_, radsum)) return TransitionState.Collided; } return TransitionState.OK; } if (transition.SpherePath.StepDown) { if (isCreature) return TransitionState.OK; return StepSphereDown(center, radius, transition, globSphere, ref disp, radsum); } if (transition.SpherePath.CheckWalkable) { if (CollidesWithSphere(disp, radsum)) return TransitionState.Collided; if (transition.SpherePath.NumSphere > 1) { if (CollidesWithSphere(disp_, radsum)) return TransitionState.Collided; } return TransitionState.OK; } if (!transition.SpherePath.Collide) { if (transition.ObjectInfo.State.HasFlag(ObjectInfoState.Contact) || transition.ObjectInfo.State.HasFlag(ObjectInfoState.OnWalkable)) { if (CollidesWithSphere(disp, radsum)) return StepSphereUp(center, transition, disp, radsum); if (transition.SpherePath.NumSphere > 1) { if (CollidesWithSphere(disp_, radsum)) return SlideSphere(center, transition, globSphere_, 1); } return TransitionState.OK; } else if (transition.ObjectInfo.State.HasFlag(ObjectInfoState.PathClipped)) { if (CollidesWithSphere(disp, radsum)) return CollideWithPoint(center, transition, globSphere, radsum, 0); } else { if (CollidesWithSphere(disp, radsum)) return LandOnSphere(center, transition); if (transition.SpherePath.NumSphere > 1) { if (CollidesWithSphere(disp_, radsum)) return CollideWithPoint(center, transition, globSphere_, radsum, 1); } } return TransitionState.OK; } if (isCreature) return TransitionState.OK; if (!CollidesWithSphere(disp, radsum)) { if (transition.SpherePath.NumSphere > 1) { if (!CollidesWithSphere(disp_, radsum)) return TransitionState.OK; } } // handles movement interpolation var blockOffset = transition.SpherePath.GetCurPosCheckPosBlockOffset(); var movement = transition.SpherePath.GlobalCurrCenter[0].Center - globSphere.Center - blockOffset; radsum += PhysicsGlobals.EPSILON; var lenSq = movement.LengthSquared(); var diff = -Vector3.Dot(movement, disp); if (Math.Abs(lenSq) < PhysicsGlobals.EPSILON) return TransitionState.Collided; var t = Math.Sqrt(diff * diff - (disp.LengthSquared() - radsum * radsum) * lenSq) + diff; // solve for t if (t > 1) t = diff * 2 - t; var time = (float)t / lenSq; var timecheck = (1 - time) * transition.SpherePath.WalkInterp; if (timecheck >= transition.SpherePath.WalkInterp || timecheck < -0.1f) return TransitionState.Collided; movement *= time; disp = (disp + movement) / radsum; if (!transition.SpherePath.IsWalkableAllowable(disp.Z)) return TransitionState.OK; var disp2 = globSphere.Center - disp * globSphere.Radius; var contactPlane = new Plane(disp, -Vector3.Dot(disp, disp2)); transition.CollisionInfo.SetContactPlane(contactPlane, true); transition.CollisionInfo.ContactPlaneCellID = transition.SpherePath.CheckPos.ObjCellID; transition.SpherePath.WalkInterp = timecheck; transition.SpherePath.AddOffsetToCheckPos(movement, globSphere.Radius); return TransitionState.Adjusted; } /// /// Handles the collision when an object lands on a sphere /// [Obsolete("Use LandOnShere without checkPos, disp, radsum")] public TransitionState LandOnSphere(Transition transition, Sphere checkPos, Vector3 disp, float radsum) { return LandOnSphere(transition); } /// /// Handles the collision when an object lands on a sphere /// public TransitionState LandOnSphere(Transition transition) { return LandOnSphere(Center, transition); } /// /// Handles the collision when an object lands on a sphere /// public static TransitionState LandOnSphere(Vector3 center, Transition transition) { var path = transition.SpherePath; var collisionNormal = path.GlobalCurrCenter[0].Center - center; if (Vec.NormalizeCheckSmall(ref collisionNormal)) return TransitionState.Collided; else { path.SetCollide(collisionNormal); path.WalkableAllowance = PhysicsGlobals.LandingZ; return TransitionState.Adjusted; } } /// /// Attempts to slide the sphere from a collision /// [Obsolete("Use SlideSphere without radsum")] public TransitionState SlideSphere(Transition transition, Vector3 disp, float radsum, int sphereNum) { return SlideSphere(transition, disp, sphereNum); } /// /// Attempts to slide the sphere from a collision /// public TransitionState SlideSphere(Transition transition, Vector3 disp, int sphereNum) { return SlideSphere(Center, transition, disp, sphereNum); } /// /// Attempts to slide the sphere from a collision /// public static TransitionState SlideSphere(Vector3 center, Transition transition, Vector3 disp, int sphereNum) { var path = transition.SpherePath; var collisions = transition.CollisionInfo; var globSphere = path.GlobalSphere[sphereNum]; var collisionNormal = path.GlobalCurrCenter[sphereNum].Center - center; if (Vec.NormalizeCheckSmall(ref collisionNormal)) return TransitionState.Collided; collisions.SetCollisionNormal(collisionNormal); var contactPlane = collisions.ContactPlaneValid ? collisions.ContactPlane : collisions.LastKnownContactPlane; var skid_dir = contactPlane.Normal; //var direction = Vector3.Cross(skid_dir, collisionNormal); var direction = Vector3.Cross(collisionNormal, skid_dir); var blockOffset = LandDefs.GetBlockOffset(path.CurPos.ObjCellID, path.CheckPos.ObjCellID); var globOffset = globSphere.Center - path.GlobalCurrCenter[sphereNum].Center + blockOffset; var dirLenSq = direction.LengthSquared(); if (dirLenSq >= PhysicsGlobals.EPSILON) { skid_dir = Vector3.Dot(globOffset, direction) * direction; var invDirLenSq = 1.0f / dirLenSq; //skid_dir *= invDirLenSq * invDirLenSq; skid_dir *= invDirLenSq; direction = skid_dir; // only x? //if (direction.X * direction.X < PhysicsGlobals.EPSILON) if (direction.LengthSquared() < PhysicsGlobals.EPSILON) return TransitionState.Collided; direction -= globOffset; path.AddOffsetToCheckPos(direction, globSphere.Radius); return TransitionState.Slid; } if (Vector3.Dot(skid_dir, disp) < 0.0f) return TransitionState.Collided; direction = -Vector3.Dot(globOffset, collisionNormal) * collisionNormal; path.AddOffsetToCheckPos(direction, globSphere.Radius); return TransitionState.Slid; } /// /// Attempts to slide a sphere from a collision /// [Obsolete("Use SlideSphere without disp and radsum")] public TransitionState SlideSphere(Transition transition, Sphere checkPos, Vector3 disp, float radsum, int sphereNum) { return SlideSphere(transition, checkPos, sphereNum); } /// /// Attempts to slide a sphere from a collision /// public TransitionState SlideSphere(Transition transition, Sphere checkPos, int sphereNum) { return SlideSphere(Center, transition, checkPos, sphereNum); } /// /// Attempts to slide a sphere from a collision /// public static TransitionState SlideSphere(Vector3 center, Transition transition, Sphere checkPos, int sphereNum) { var globalCenter = transition.SpherePath.GlobalCurrCenter[sphereNum].Center; var collisionNormal = globalCenter - center; if (Vec.NormalizeCheckSmall(ref collisionNormal)) return TransitionState.Collided; else return checkPos.SlideSphere(transition, ref collisionNormal, globalCenter); } /// /// Attempts to slide a sphere from a collision /// public TransitionState SlideSphere(Transition transition, ref Vector3 collisionNormal, Vector3 currPos) { var path = transition.SpherePath; var collisions = transition.CollisionInfo; if (collisionNormal.Equals(Vector3.Zero)) { var halfOffset = (currPos - Center) * 0.5f; path.AddOffsetToCheckPos(halfOffset, Radius); return TransitionState.Adjusted; } collisions.SetCollisionNormal(collisionNormal); var blockOffset = LandDefs.GetBlockOffset(path.CurPos.ObjCellID, path.CheckPos.ObjCellID); var gDelta = blockOffset + (Center - currPos); var contactPlane = collisions.ContactPlaneValid ? collisions.ContactPlane : collisions.LastKnownContactPlane; var direction = Vector3.Cross(collisionNormal, contactPlane.Normal); var dirLenSq = direction.LengthSquared(); if (dirLenSq >= PhysicsGlobals.EPSILON) { var diff = Vector3.Dot(direction, gDelta); var invDirLenSq = 1.0f / dirLenSq; var offset = direction * diff * invDirLenSq; if (offset.LengthSquared() < PhysicsGlobals.EPSILON) return TransitionState.Collided; offset -= gDelta; path.AddOffsetToCheckPos(offset, Radius); return TransitionState.Slid; } if (Vector3.Dot(collisionNormal, contactPlane.Normal) >= 0.0f) { var diff = Vector3.Dot(collisionNormal, gDelta); var offset = -collisionNormal * diff; path.AddOffsetToCheckPos(offset, Radius); return TransitionState.Slid; } collisionNormal = -gDelta; if (!Vec.NormalizeCheckSmall(ref collisionNormal)) collisions.SetCollisionNormal(collisionNormal); return TransitionState.OK; } /// /// Attempts to move the sphere down from a collision /// public TransitionState StepSphereDown(Transition transition, Sphere checkPos, ref Vector3 disp, float radsum) { return StepSphereDown(Center, Radius, transition, checkPos, ref disp, radsum); } /// /// Attempts to move the sphere down from a collision /// public static TransitionState StepSphereDown(Vector3 center, float radius, Transition transition, Sphere checkPos, ref Vector3 disp, float radsum) { var path = transition.SpherePath; var collisions = transition.CollisionInfo; if (!CollidesWithSphere(disp, radsum)) { if (path.NumSphere <= 1) return TransitionState.OK; var disp_ = path.GlobalSphere[1].Center - center; if (!CollidesWithSphere(disp_, radsum)) return TransitionState.OK; } var stepDown = path.StepDownAmt * path.WalkInterp; if (Math.Abs(stepDown) < PhysicsGlobals.EPSILON) return TransitionState.Collided; radsum += PhysicsGlobals.EPSILON; var val = Math.Sqrt(radsum * radsum - (disp.X * disp.X + disp.Y * disp.Y)); var scaledStep = (float)(val - disp.Z) / stepDown; var timecheck = (1.0f - scaledStep) * path.WalkInterp; if (timecheck >= path.WalkInterp || timecheck < -0.1f) return TransitionState.Collided; var interp = stepDown * scaledStep; var invRadSum = 1.0f / radsum; // modifies disp? //var _disp = new Vector3(disp.X, disp.Y, disp.Z + interp) * invRadSum; disp = new Vector3(disp.X, disp.Y, disp.Z + interp) * invRadSum; if (disp.Z <= path.WalkableAllowance) return TransitionState.OK; var scaledDisp = disp * radius + center; var restPlane = new Plane(disp, -Vector3.Dot(disp, scaledDisp)); collisions.SetContactPlane(restPlane, true); collisions.ContactPlaneCellID = path.CheckPos.ObjCellID; path.WalkInterp = timecheck; var offset = new Vector3(0, 0, interp); path.AddOffsetToCheckPos(offset, checkPos.Radius); return TransitionState.Adjusted; } /// /// Attempts to move the sphere up from a collision /// [Obsolete("Use override without checkPos")] public TransitionState StepSphereUp(Transition transition, Sphere checkPos, Vector3 disp, float radsum) { return StepSphereUp(transition, disp, radsum); } /// /// Attempts to move the sphere up from a collision /// public TransitionState StepSphereUp(Transition transition, Vector3 disp, float radsum) { return StepSphereUp(Center, transition, disp, radsum); } /// /// Attempts to move the sphere up from a collision /// public static TransitionState StepSphereUp(Vector3 center, Transition transition, Vector3 disp, float radsum) { radsum += PhysicsGlobals.EPSILON; if (transition.ObjectInfo.StepUpHeight < radsum - disp.Z) return SlideSphere(center, transition, disp, 0); else { var globCenter = transition.SpherePath.GlobalCurrCenter[0].Center; var collisionNormal = globCenter - center; if (transition.StepUp(collisionNormal)) return TransitionState.OK; else return transition.SpherePath.StepUpSlide(transition); } } /// /// Detects if a ray intersects with a sphere /// /// A ray is defined by a start point, a unit direction, and a length /// out parameter, the length of the ray when it hit the sphere /// True if ray intersected, otherwise false. /// /// - If the start point of the ray inside sphere, it is not considered an intersection. /// - If the sphere is behind the ray start point, with the ray direction pointing away from the sphere, /// it can still return true for intersection, with timeOfIntersection as a negative value. /// public bool SphereIntersectsRay(Ray ray, out double timeOfIntersection) { timeOfIntersection = 0; var distSq = ray.Dir.LengthSquared(); if (distSq < PhysicsGlobals.EPSILON) return false; // dir should be unit vector, redundant? // detect intersection var delta = ray.Point - Center; var c = delta.LengthSquared() - Radius * Radius; if (c <= 0) return false; // detect point of intersection var b = -Vector3.Dot(delta, ray.Dir); var d = b * b - c * distSq; if (d < 0) return false; var dist = Math.Sqrt(d); if (b <= dist) timeOfIntersection = (b + dist) / distSq; else timeOfIntersection = (b - dist) / distSq; return true; } public override string ToString() { return string.Format("Center: {0} Radius: {1}", Center, Radius); } public bool Equals(Sphere sphere) { return Center.X == sphere.Center.X && Center.Y == sphere.Center.Y && Center.Z == sphere.Center.Z && Radius == sphere.Radius; } public override int GetHashCode() { return HashCode.Combine(Center, Radius); } } }