using System; using System.Numerics; using ACE.Entity; using ACE.Entity.Enum; using ACE.Entity.Enum.Properties; using ACE.Server.Entity; using ACE.Server.Entity.Actions; using ACE.Server.Physics.Animation; using ACE.Server.Physics.Extensions; using ACE.Server.Network.GameMessages.Messages; namespace ACE.Server.WorldObjects { /// /// Creature navigation / position / rotation /// partial class Creature { /// /// Returns the 3D distance between this creature and target /// public float GetDistance(WorldObject target) { return Location.DistanceTo(target.Location); } /// /// Returns the 2D angle between current direction /// and position from an input target /// public float GetAngle(WorldObject target) { var currentDir = Location.GetCurrentDir(); var targetDir = Vector3.Zero; if (Location.Indoors == target.Location.Indoors) targetDir = GetDirection(Location.ToGlobal(), target.Location.ToGlobal()); else targetDir = GetDirection(Location.Pos, target.Location.Pos); targetDir.Z = 0.0f; targetDir = Vector3.Normalize(targetDir); // get the 2D angle between these vectors return GetAngle(currentDir, targetDir); } public float GetAngle_Physics(WorldObject target) { var currentDir = GetCurrentDir_Physics(); var targetDir = Vector3.Zero; if (Location.Indoors == target.Location.Indoors) targetDir = GetDirection(Location.ToGlobal(), target.Location.ToGlobal()); else targetDir = GetDirection(Location.Pos, target.Location.Pos); targetDir.Z = 0.0f; targetDir = Vector3.Normalize(targetDir); // get the 2D angle between these vectors return GetAngle(currentDir, targetDir); } public Vector3 GetCurrentDir_Physics() { return Vector3.Normalize(Vector3.Transform(Vector3.UnitY, PhysicsObj.Position.Frame.Orientation)); } public float GetAngle_Physics2(WorldObject target) { return PhysicsObj.Position.heading_diff(target.PhysicsObj.Position); } /// /// Returns the 2D angle between current direction /// and rotation from an input position /// public float GetAngle(Position position) { var currentDir = Location.GetCurrentDir(); var targetDir = position.GetCurrentDir(); // get the 2D angle between these vectors return GetAngle(currentDir, targetDir); } /// /// Returns the 2D angle of the input vector /// public static float GetAngle(Vector3 dir) { var rads = Math.Atan2(dir.Y, dir.X); if (double.IsNaN(rads)) return 0.0f; var angle = rads * 57.2958f; return (float)angle; } /// /// Returns the 2D angle between 2 vectors /// public static float GetAngle(Vector3 a, Vector3 b) { var cosTheta = a.Dot2D(b); var rads = Math.Acos(cosTheta); if (double.IsNaN(rads)) return 0.0f; var angle = rads * (180.0f / Math.PI); return (float)angle; } /// /// Returns a normalized 2D vector from self to target /// public Vector3 GetDirection(Vector3 self, Vector3 target) { var target2D = new Vector3(self.X, self.Y, 0); var self2D = new Vector3(target.X, target.Y, 0); return Vector3.Normalize(target - self); } /// /// Sends a TurnToObject command to the client /// public void TurnToObject(WorldObject target, bool stopCompletely = true) { var turnToMotion = new Motion(this, target, MovementType.TurnToObject); if (!stopCompletely) turnToMotion.MoveToParameters.MovementParameters &= ~MovementParams.StopCompletely; EnqueueBroadcastMotion(turnToMotion); } /// /// Starts rotating a creature from its current direction /// so that it eventually is facing the target position /// /// The amount of time in seconds for the rotation to complete public virtual float Rotate(WorldObject target) { if (target == null || target.Location == null) return 0.0f; // send network message to start turning creature TurnToObject(target); var angle = GetAngle(target); //Console.WriteLine("Angle: " + angle); // estimate time to rotate to target var rotateDelay = GetRotateDelay(angle); //Console.WriteLine("RotateTime: " + rotateTime); // update server object rotation on completion // TODO: proper incremental rotation var actionChain = new ActionChain(); actionChain.AddDelaySeconds(rotateDelay); actionChain.AddAction(this, () => { if (target == null || target.Location == null) return; //var matchIndoors = Location.Indoors == target.Location.Indoors; //var globalLoc = matchIndoors ? Location.ToGlobal() : Location.Pos; //var targetLoc = matchIndoors ? target.Location.ToGlobal() : target.Location.Pos; var globalLoc = Location.ToGlobal(); var targetLoc = target.Location.ToGlobal(); var targetDir = GetDirection(globalLoc, targetLoc); Location.Rotate(targetDir); }); actionChain.EnqueueChain(); return rotateDelay; } /// /// Returns the amount of time for this creature to rotate by the # of degrees /// from the input angle, using the omega speed from its MotionTable /// public virtual float GetRotateDelay(float angle) { var turnSpeed = MotionTable.GetTurnSpeed(MotionTableId); if (turnSpeed == 0.0f) return 0.0f; var rotateTime = Math.PI / turnSpeed / 180.0f * angle; return (float)rotateTime; } /// /// Returns the amount of time for this creature to rotate /// towards its target, based on the omega speed from its MotionTable /// public float GetRotateDelay(WorldObject target) { var angle = GetAngle(target); return GetRotateDelay(angle); } /// /// Starts rotating a creature from its current direction /// so that it eventually is facing the rotation from the input position /// Used by the emote system, which has the target rotation stored in positions /// /// The amount of time in seconds for the rotation to complete public float TurnTo(Position position) { var frame = new AFrame(position.Pos, position.Rotation); var heading = frame.get_heading(); // send network message to start turning creature var turnToMotion = new Motion(this, position, heading); EnqueueBroadcastMotion(turnToMotion); var angle = GetAngle(position); //Console.WriteLine("Angle: " + angle); // estimate time to rotate to target var rotateDelay = GetRotateDelay(angle); //Console.WriteLine("RotateTime: " + rotateTime); // update server object rotation on completion // TODO: proper incremental rotation var actionChain = new ActionChain(); actionChain.AddDelaySeconds(rotateDelay); actionChain.AddAction(this, () => { var targetDir = position.GetCurrentDir(); Location.Rotate(targetDir); PhysicsObj.Position.Frame.Orientation = Location.Rotation; }); actionChain.EnqueueChain(); return rotateDelay; } /// /// Returns the amount of time for this creature to rotate /// towards the rotation from the input position, based on the omega speed from its MotionTable /// Used by the emote system, which has the target rotation stored in positions /// /// Only the rotation information from this position is used here public float GetRotateDelay(Position position) { var angle = GetAngle(position); return GetRotateDelay(angle); } /// /// This is called by the monster AI system for ranged attacks /// It is mostly a duplicate of Rotate(), and should be refactored eventually... /// It sets CurrentMotionState and AttackTarget here /// public float TurnTo(WorldObject target, bool debug = false) { if (DebugMove) Console.WriteLine($"{Name}.TurnTo({target.Name})"); if (this is Player) return 0.0f; var turnToMotion = new Motion(this, target, MovementType.TurnToObject); EnqueueBroadcastMotion(turnToMotion); CurrentMotionState = turnToMotion; AttackTarget = target; var rotateDelay = EstimateTurnTo(); if (debug) Console.WriteLine("TurnTime = " + rotateDelay); var actionChain = new ActionChain(); actionChain.AddDelaySeconds(rotateDelay); actionChain.AddAction(this, () => { // fix me: in progress turn //var targetDir = GetDirection(Location.ToGlobal(), target.Location.ToGlobal()); //Location.Rotate(targetDir); if (debug) Console.WriteLine("Finished turning - " + rotateDelay + "s"); }); actionChain.EnqueueChain(); return rotateDelay; } /// /// Used by the monster AI system to start turning / running towards a target /// public virtual void MoveTo(WorldObject target, float runRate = 1.0f) { if (DebugMove) Console.WriteLine($"{Name}.MoveTo({target.Name}, {runRate}) - CurPos: {Location.ToLOCString()} - DestPos: {AttackTarget.Location.ToLOCString()} - TargetDist: {Vector3.Distance(Location.ToGlobal(), AttackTarget.Location.ToGlobal())}"); var motion = GetMoveToMotion(target, runRate); CurrentMotionState = motion; EnqueueBroadcastMotion(motion); } public Motion GetMoveToMotion(WorldObject target, float runRate) { var motion = new Motion(this, target, MovementType.MoveToObject); motion.MoveToParameters.MovementParameters |= MovementParams.CanCharge | MovementParams.FailWalk | MovementParams.UseFinalHeading | MovementParams.Sticky | MovementParams.MoveAway; motion.MoveToParameters.WalkRunThreshold = 1.0f; if (runRate > 0) motion.RunRate = runRate; else motion.MoveToParameters.MovementParameters &= ~MovementParams.CanRun; return motion; } public virtual void BroadcastMoveTo(Player player) { Motion motion = null; if (AttackTarget != null) { // move to object motion = GetMoveToMotion(AttackTarget, RunRate); } else { // move to position var home = GetPosition(PositionType.Home); motion = GetMoveToPosition(home, RunRate, 1.0f); } player.Session.Network.EnqueueSend(new GameMessageUpdateMotion(this, motion)); } /// /// Sends a network message for moving a creature to a new position /// public void MoveTo(Position position, float runRate = 1.0f, bool setLoc = true, float? walkRunThreshold = null, float? speed = null) { // build and send MoveToPosition message to client var motion = GetMoveToPosition(position, runRate, walkRunThreshold, speed); EnqueueBroadcastMotion(motion); if (!setLoc) return; // start executing MoveTo iterator on server if (!PhysicsObj.IsMovingOrAnimating) PhysicsObj.UpdateTime = Physics.Common.PhysicsTimer.CurrentTime; var mvp = new MovementParameters(motion.MoveToParameters); PhysicsObj.MoveToPosition(new Physics.Common.Position(position), mvp); AddMoveToTick(); } private void AddMoveToTick() { var actionChain = new ActionChain(); actionChain.AddDelaySeconds(monsterTickInterval); actionChain.AddAction(this, () => { if (!IsDead && PhysicsObj?.MovementManager?.MoveToManager != null && PhysicsObj.IsMovingTo()) { PhysicsObj.update_object(); UpdatePosition_SyncLocation(); SendUpdatePosition(); if (PhysicsObj?.MovementManager?.MoveToManager?.FailProgressCount < 5) { AddMoveToTick(); } else { if (PhysicsObj?.MovementManager?.MoveToManager != null) { PhysicsObj.MovementManager.MoveToManager.CancelMoveTo(WeenieError.ActionCancelled); PhysicsObj.MovementManager.MoveToManager.FailProgressCount = 0; } EnqueueBroadcastMotion(new Motion(CurrentMotionState.Stance, MotionCommand.Ready)); } //Console.WriteLine($"{Name}.Position: {Location}"); } }); actionChain.EnqueueChain(); } public Motion GetMoveToPosition(Position position, float runRate = 1.0f, float? walkRunThreshold = null, float? speed = null) { // TODO: change parameters to accept an optional MoveToParameters var motion = new Motion(this, position); motion.MovementType = MovementType.MoveToPosition; //motion.Flag |= MovementParams.CanCharge | MovementParams.FailWalk | MovementParams.UseFinalHeading | MovementParams.MoveAway; if (walkRunThreshold != null) motion.MoveToParameters.WalkRunThreshold = walkRunThreshold.Value; if (speed != null) motion.MoveToParameters.Speed = speed.Value; // always use final heading? var frame = new AFrame(position.Pos, position.Rotation); motion.MoveToParameters.DesiredHeading = frame.get_heading(); motion.MoveToParameters.MovementParameters |= MovementParams.UseFinalHeading; motion.MoveToParameters.DistanceToObject = 0.6f; if (runRate > 0) motion.RunRate = runRate; else motion.MoveToParameters.MovementParameters &= ~MovementParams.CanRun; return motion; } /// /// For monsters only -- blips to a new position within the same landblock /// public void FakeTeleport(Position _newPosition) { var newPosition = new Position(_newPosition); newPosition.PositionZ += 0.005f * (ObjScale ?? 1.0f); if (Location.Landblock != newPosition.Landblock) { log.Error($"{Name} tried to teleport from {Location} to a different landblock {newPosition}"); return; } // force out of hotspots PhysicsObj.report_collision_end(true); //HandlePreTeleportVisibility(newPosition); // do the physics teleport var setPosition = new Physics.Common.SetPosition(); setPosition.Pos = new Physics.Common.Position(newPosition); setPosition.Flags = Physics.Common.SetPositionFlags.SendPositionEvent | Physics.Common.SetPositionFlags.Slide | Physics.Common.SetPositionFlags.Placement | Physics.Common.SetPositionFlags.Teleport; PhysicsObj.SetPosition(setPosition); // update ace location SyncLocation(); // broadcast blip to new position SendUpdatePosition(true); } } }