using System;
using System.Numerics;
using ACE.Common;
using ACE.Entity;
using ACE.Entity.Enum;
using ACE.Entity.Enum.Properties;
using ACE.Server.Entity;
using ACE.Server.Entity.Actions;
using ACE.Server.Managers;
using ACE.Server.Physics.Animation;
using ACE.Server.Physics.Common;
namespace ACE.Server.WorldObjects
{
partial class Creature
{
///
/// Return to home if target distance exceeds this range
///
public const float MaxChaseRange = 96.0f;
public const float MaxChaseRangeSq = MaxChaseRange * MaxChaseRange;
///
/// Determines if a monster is within melee range of target
///
//public const float MaxMeleeRange = 1.5f;
public const float MaxMeleeRange = 0.75f;
//public const float MaxMeleeRange = 1.5f + 0.6f + 0.1f; // max melee range + distance from + buffer
///
/// The maximum range for a monster missile attack
///
//public const float MaxMissileRange = 80.0f;
//public const float MaxMissileRange = 40.0f; // for testing
///
/// The distance per second from running animation
///
public float MoveSpeed;
///
/// The run skill via MovementSystem GetRunRate()
///
public float RunRate;
///
/// Flag indicates monster is turning towards target
///
public bool IsTurning = false;
///
/// Flag indicates monster is moving towards target
///
public bool IsMoving = false;
///
/// The last time a movement tick was processed
///
public double LastMoveTime;
public bool DebugMove;
public double NextMoveTime;
public double NextCancelTime;
///
/// Starts the process of monster turning towards target
///
public void StartTurn()
{
//if (Timers.RunningTime < NextMoveTime)
//return;
if (!MoveReady())
return;
if (DebugMove)
Console.WriteLine($"{Name} ({Guid}) - StartTurn, ranged={IsRanged}");
if (MoveSpeed == 0.0f)
GetMovementSpeed();
//Console.WriteLine($"[{Timers.RunningTime}] - {Name} ({Guid}) - starting turn");
IsTurning = true;
// send network actions
var targetDist = GetDistanceToTarget();
var turnTo = IsRanged || (CurrentAttack == CombatType.Magic && targetDist <= GetSpellMaxRange()) || AiImmobile;
if (turnTo)
TurnTo(AttackTarget);
else
MoveTo(AttackTarget, RunRate);
// need turning listener?
IsTurning = false;
IsMoving = true;
LastMoveTime = Timers.RunningTime;
NextCancelTime = LastMoveTime + ThreadSafeRandom.Next(2, 4);
moveBit = false;
var mvp = GetMovementParameters();
if (turnTo)
PhysicsObj.TurnToObject(AttackTarget.PhysicsObj, mvp);
else
PhysicsObj.MoveToObject(AttackTarget.PhysicsObj, mvp);
// prevent initial snap
PhysicsObj.UpdateTime = PhysicsTimer.CurrentTime;
}
///
/// Called when the TurnTo process has completed
///
public void OnTurnComplete()
{
var dir = Vector3.Normalize(AttackTarget.Location.ToGlobal() - Location.ToGlobal());
Location.Rotate(dir);
IsTurning = false;
if (!IsRanged)
StartMove();
}
///
/// Starts the process of monster moving towards target
///
public void StartMove()
{
LastMoveTime = Timers.RunningTime;
IsMoving = true;
}
///
/// Called when the MoveTo process has completed
///
public override void OnMoveComplete(WeenieError status)
{
if (DebugMove)
Console.WriteLine($"{Name} ({Guid}) - OnMoveComplete({status})");
if (status != WeenieError.None)
return;
if (AiImmobile && CurrentAttack == CombatType.Melee)
{
var targetDist = GetDistanceToTarget();
if (targetDist > MaxRange)
ResetAttack();
}
if (MonsterState == State.Return)
Sleep();
PhysicsObj.CachedVelocity = Vector3.Zero;
IsMoving = false;
}
///
/// Estimates the time it will take the monster to turn and move towards target
///
public float EstimateTargetTime()
{
return EstimateTurnTo() + EstimateMoveTo();
}
///
/// Estimates the time it will take the monster to turn towards target
///
public float EstimateTurnTo()
{
return GetRotateDelay(AttackTarget);
}
///
/// Estimates the time it will take the monster to move towards target
///
public float EstimateMoveTo()
{
return GetDistanceToTarget() / MoveSpeed;
}
///
/// Returns TRUE if monster is within target melee range
///
public bool IsMeleeRange()
{
return GetDistanceToTarget() <= MaxMeleeRange;
}
///
/// Returns TRUE if monster in range for current attack type
///
public bool IsAttackRange()
{
return GetDistanceToTarget() <= MaxRange;
}
///
/// Gets the distance to target, with radius excluded
///
public float GetDistanceToTarget()
{
if (AttackTarget == null)
return float.MaxValue;
//var matchIndoors = Location.Indoors == AttackTarget.Location.Indoors;
//var targetPos = matchIndoors ? AttackTarget.Location.ToGlobal() : AttackTarget.Location.Pos;
//var sourcePos = matchIndoors ? Location.ToGlobal() : Location.Pos;
//var dist = (targetPos - sourcePos).Length();
//var radialDist = dist - (AttackTarget.PhysicsObj.GetRadius() + PhysicsObj.GetRadius());
// always use spheres?
var cylDist = (float)Physics.Common.Position.CylinderDistance(PhysicsObj.GetRadius(), PhysicsObj.GetHeight(), PhysicsObj.Position,
AttackTarget.PhysicsObj.GetRadius(), AttackTarget.PhysicsObj.GetHeight(), AttackTarget.PhysicsObj.Position);
if (DebugMove)
Console.WriteLine($"{Name}.DistanceToTarget: {cylDist}");
//return radialDist;
return cylDist;
}
///
/// Returns the destination position the monster is attempting to move to
/// to perform a melee attack
///
public Vector3 GetDestination()
{
var dir = Vector3.Normalize(Location.ToGlobal() - AttackTarget.Location.ToGlobal());
return AttackTarget.Location.Pos + dir * (AttackTarget.PhysicsObj.GetRadius() + PhysicsObj.GetRadius());
}
///
/// Primary movement handler, determines if target in range
///
public void Movement()
{
//if (!IsRanged)
UpdatePosition();
if (MonsterState == State.Awake && GetDistanceToTarget() >= MaxChaseRange)
{
CancelMoveTo();
FindNextTarget();
return;
}
if (PhysicsObj.MovementManager.MoveToManager.FailProgressCount > 0 && Timers.RunningTime > NextCancelTime)
CancelMoveTo();
}
public void UpdatePosition(bool netsend = true)
{
//stopwatch.Restart();
PhysicsObj.update_object();
//ServerPerformanceMonitor.AddToCumulativeEvent(ServerPerformanceMonitor.CumulativeEventHistoryType.Monster_Navigation_UpdatePosition_PUO, stopwatch.Elapsed.TotalSeconds);
UpdatePosition_SyncLocation();
if (netsend)
SendUpdatePosition();
if (DebugMove)
//Console.WriteLine($"{Name} ({Guid}) - UpdatePosition (velocity: {PhysicsObj.CachedVelocity.Length()})");
Console.WriteLine($"{Name} ({Guid}) - UpdatePosition: {Location.ToLOCString()}");
if (MonsterState == State.Return && PhysicsObj.MovementManager.MoveToManager.PendingActions.Count == 0)
Sleep();
if (MonsterState == State.Awake && IsMoving && PhysicsObj.MovementManager.MoveToManager.PendingActions.Count == 0)
IsMoving = false;
}
///
/// Synchronizes the WorldObject Location with the Physics Location
///
public void UpdatePosition_SyncLocation()
{
// was the position successfully moved to?
// use the physics position as the source-of-truth?
var newPos = PhysicsObj.Position;
if (Location.LandblockId.Raw != newPos.ObjCellID)
{
var prevBlockCell = Location.LandblockId.Raw;
var prevBlock = Location.LandblockId.Raw >> 16;
var prevCell = Location.LandblockId.Raw & 0xFFFF;
var newBlockCell = newPos.ObjCellID;
var newBlock = newPos.ObjCellID >> 16;
var newCell = newPos.ObjCellID & 0xFFFF;
Location.LandblockId = new LandblockId(newPos.ObjCellID);
if (prevBlock != newBlock)
{
LandblockManager.RelocateObjectForPhysics(this, true);
//Console.WriteLine($"Relocating {Name} from {prevBlockCell:X8} to {newBlockCell:X8}");
//Console.WriteLine("Old position: " + Location.Pos);
//Console.WriteLine("New position: " + newPos.Frame.Origin);
}
//else
//Console.WriteLine("Moving " + Name + " to " + Location.LandblockId.Raw.ToString("X8"));
}
// skip ObjCellID check when updating from physics
// TODO: update to newer version of ACE.Entity.Position
Location.PositionX = newPos.Frame.Origin.X;
Location.PositionY = newPos.Frame.Origin.Y;
Location.PositionZ = newPos.Frame.Origin.Z;
Location.Rotation = newPos.Frame.Orientation;
if (DebugMove)
DebugDistance();
}
public void DebugDistance()
{
if (AttackTarget == null) return;
var dist = GetDistanceToTarget();
var angle = GetAngle(AttackTarget);
//Console.WriteLine("Dist: " + dist);
//Console.WriteLine("Angle: " + angle);
}
public void GetMovementSpeed()
{
var moveSpeed = MotionTable.GetRunSpeed(MotionTableId);
if (moveSpeed == 0) moveSpeed = 2.5f;
var scale = ObjScale ?? 1.0f;
RunRate = GetRunRate();
MoveSpeed = moveSpeed * RunRate * scale;
//Console.WriteLine(Name + " - Run: " + runSkill + " - RunRate: " + RunRate + " - Move: " + MoveSpeed + " - Scale: " + scale);
}
///
/// Returns the RunRate that is sent to the client as myRunRate
///
public float GetRunRate()
{
var burden = 0.0f;
// assuming burden only applies to players...
if (this is Player player)
{
var strength = Strength.Current;
var capacity = EncumbranceSystem.EncumbranceCapacity((int)strength, player.AugmentationIncreasedCarryingCapacity);
burden = EncumbranceSystem.GetBurden(capacity, EncumbranceVal ?? 0);
// TODO: find this exact formula in client
// technically this would be based on when the player releases / presses the movement key after stamina > 0
if (player.IsExhausted)
burden = 3.0f;
}
var runSkill = GetCreatureSkill(Skill.Run).Current;
var runRate = MovementSystem.GetRunRate(burden, (int)runSkill, 1.0f);
return (float)runRate;
}
///
/// Sets the corpse to the final position
///
public void SetFinalPosition()
{
if (AttackTarget == null) return;
var playerDir = AttackTarget.Location.GetCurrentDir();
Location.Pos = AttackTarget.Location.Pos + playerDir * (AttackTarget.PhysicsObj.GetRadius() + PhysicsObj.GetRadius());
SendUpdatePosition();
}
///
/// Returns TRUE if monster is facing towards the target
///
public bool IsFacing(WorldObject target)
{
if (target?.Location == null) return false;
var angle = GetAngle(target);
var dist = Math.Max(0, GetDistanceToTarget());
// rotation accuracy?
var threshold = 5.0f;
var minDist = 10.0f;
if (dist < minDist)
threshold += (minDist - dist) * 1.5f;
if (DebugMove)
Console.WriteLine($"{Name}.IsFacing({target.Name}): Angle={angle}, Dist={dist}, Threshold={threshold}, {angle < threshold}");
return angle < threshold;
}
public MovementParameters GetMovementParameters()
{
var mvp = new MovementParameters();
// set non-default params for monster movement
mvp.Flags &= ~MovementParamFlags.CanWalk;
var turnTo = IsRanged || (CurrentAttack == CombatType.Magic && GetDistanceToTarget() <= GetSpellMaxRange()) || AiImmobile;
if (!turnTo)
mvp.Flags |= MovementParamFlags.FailWalk | MovementParamFlags.UseFinalHeading | MovementParamFlags.Sticky | MovementParamFlags.MoveAway;
return mvp;
}
///
/// The maximum distance a monster can travel outside of its home position
///
public double? HomeRadius
{
get => GetProperty(PropertyFloat.HomeRadius);
set { if (!value.HasValue) RemoveProperty(PropertyFloat.HomeRadius); else SetProperty(PropertyFloat.HomeRadius, value.Value); }
}
public static float DefaultHomeRadius = 192.0f;
public static float DefaultHomeRadiusSq = DefaultHomeRadius * DefaultHomeRadius;
private float? homeRadiusSq;
public float HomeRadiusSq
{
get
{
if (homeRadiusSq == null)
{
var homeRadius = HomeRadius ?? DefaultHomeRadius;
homeRadiusSq = (float)(homeRadius * homeRadius);
}
return homeRadiusSq.Value;
}
}
public void CheckMissHome()
{
if (MonsterState == State.Return)
return;
var homePosition = GetPosition(PositionType.Home);
//var matchIndoors = Location.Indoors == homePosition.Indoors;
//var globalPos = matchIndoors ? Location.ToGlobal() : Location.Pos;
//var globalHomePos = matchIndoors ? homePosition.ToGlobal() : homePosition.Pos;
var globalPos = Location.ToGlobal();
var globalHomePos = homePosition.ToGlobal();
var homeDistSq = Vector3.DistanceSquared(globalHomePos, globalPos);
if (homeDistSq > HomeRadiusSq)
MoveToHome();
}
public void MoveToHome()
{
if (DebugMove)
Console.WriteLine($"{Name}.MoveToHome()");
var prevAttackTarget = AttackTarget;
MonsterState = State.Return;
AttackTarget = null;
var home = GetPosition(PositionType.Home);
if (Location.Equals(home))
{
Sleep();
return;
}
NextCancelTime = Timers.RunningTime + 5.0f;
MoveTo(home, RunRate, false, 1.0f);
var homePos = new Physics.Common.Position(home);
var mvp = GetMovementParameters();
mvp.DistanceToObject = 0.6f;
mvp.DesiredHeading = homePos.Frame.get_heading();
PhysicsObj.MoveToPosition(homePos, mvp);
IsMoving = true;
EmoteManager.OnHomeSick(prevAttackTarget);
}
public void CancelMoveTo()
{
//Console.WriteLine($"{Name}.CancelMoveTo()");
PhysicsObj.MovementManager.MoveToManager.CancelMoveTo(WeenieError.ActionCancelled);
PhysicsObj.MovementManager.MoveToManager.FailProgressCount = 0;
if (MonsterState == State.Return)
ForceHome();
EnqueueBroadcastMotion(new Motion(CurrentMotionState.Stance, MotionCommand.Ready));
IsMoving = false;
NextMoveTime = Timers.RunningTime + 1.0f;
ResetAttack();
FindNextTarget();
}
public void ForceHome()
{
var homePos = GetPosition(PositionType.Home);
if (DebugMove)
Console.WriteLine($"{Name} ({Guid}) - ForceHome({homePos.ToLOCString()})");
var setPos = new SetPosition();
setPos.Pos = new Physics.Common.Position(homePos);
setPos.Flags = SetPositionFlags.Teleport;
PhysicsObj.SetPosition(setPos);
UpdatePosition_SyncLocation();
SendUpdatePosition();
var actionChain = new ActionChain();
actionChain.AddDelaySeconds(0.5f);
actionChain.AddAction(this, Sleep);
actionChain.EnqueueChain();
}
}
}