using System; using System.Collections.Generic; using System.Linq; using System.Numerics; using ACE.Common; using ACE.Common.Extensions; using ACE.Entity.Enum; using ACE.Entity.Enum.Properties; using ACE.Server.Entity; using ACE.Server.Entity.Actions; using ACE.Server.Managers; namespace ACE.Server.WorldObjects { /// /// Determines when a monster wakes up from idle state /// partial class Creature { /// /// Monsters wake up when players are in visual range /// public bool IsAwake = false; /// /// Transitions a monster from idle to awake state /// public void WakeUp(bool alertNearby = true) { MonsterState = State.Awake; IsAwake = true; //DoAttackStance(); EmoteManager.OnWakeUp(AttackTarget as Creature); EmoteManager.OnNewEnemy(AttackTarget as Creature); //SelectTargetingTactic(); if (alertNearby) AlertFriendly(); } /// /// Transitions a monster from awake to idle state /// public virtual void Sleep() { if (DebugMove) Console.WriteLine($"{Name} ({Guid}).Sleep()"); SetCombatMode(CombatMode.NonCombat); CurrentAttack = null; firstUpdate = true; AttackTarget = null; IsAwake = false; IsMoving = false; MonsterState = State.Idle; PhysicsObj.CachedVelocity = Vector3.Zero; ClearRetaliateTargets(); } public Tolerance Tolerance { get => (Tolerance)(GetProperty(PropertyInt.Tolerance) ?? 0); set { if (value == 0) RemoveProperty(PropertyInt.Tolerance); else SetProperty(PropertyInt.Tolerance, (int)value); } } /// /// This list of possible targeting tactics for this monster /// public TargetingTactic TargetingTactic { get => (TargetingTactic)(GetProperty(PropertyInt.TargetingTactic) ?? 0); set { if (value == 0) RemoveProperty(PropertyInt.TargetingTactic); else SetProperty(PropertyInt.TargetingTactic, (int)TargetingTactic); } } /// /// The current targeting tactic for this monster /// public TargetingTactic CurrentTargetingTactic; public void SelectTargetingTactic() { // monsters have multiple targeting tactics, ex. Focused | Random // when should this function be called? // when a monster spawns in, does it choose 1 TargetingTactic? // or do they randomly select a TargetingTactic from their list of possible tactics, // each time they go to find a new target? //Console.WriteLine($"{Name}.TargetingTactics: {TargetingTactic}"); // if targeting tactic is none, // use the most common targeting tactic // TODO: ensure all monsters in the db have a targeting tactic var targetingTactic = TargetingTactic; if (targetingTactic == TargetingTactic.None) targetingTactic = TargetingTactic.Random | TargetingTactic.TopDamager; var possibleTactics = EnumHelper.GetFlags(targetingTactic); var rng = ThreadSafeRandom.Next(1, possibleTactics.Count - 1); if (targetingTactic == 0) rng = 0; CurrentTargetingTactic = (TargetingTactic)possibleTactics[rng]; //Console.WriteLine($"{Name}.TargetingTactic: {CurrentTargetingTactic}"); } public double NextFindTarget; public virtual void HandleFindTarget() { if (Timers.RunningTime < NextFindTarget) return; FindNextTarget(); } public void SetNextTargetTime() { // use rng? //var rng = ThreadSafeRandom.Next(5.0f, 10.0f); var rng = 5.0f; NextFindTarget = Timers.RunningTime + rng; } public virtual bool FindNextTarget() { //stopwatch.Restart(); try { SelectTargetingTactic(); SetNextTargetTime(); var visibleTargets = GetAttackTargets(); if (visibleTargets.Count == 0) { if (MonsterState != State.Return) MoveToHome(); return false; } // Generally, a creature chooses whom to attack based on: // - who it was last attacking, // - who attacked it last, // - or who caused it damage last. // When players first enter the creature's detection radius, however, none of these things are useful yet, // so the creature chooses a target randomly, weighted by distance. // Players within the creature's detection sphere are weighted by how close they are to the creature -- // the closer you are, the more chance you have to be selected to be attacked. var prevAttackTarget = AttackTarget; switch (CurrentTargetingTactic) { case TargetingTactic.None: Console.WriteLine($"{Name}.FindNextTarget(): TargetingTactic.None"); break; // same as focused? case TargetingTactic.Random: // this is a very common tactic with monsters, // although it is not truly random, it is weighted by distance var targetDistances = BuildTargetDistance(visibleTargets); AttackTarget = SelectWeightedDistance(targetDistances); break; case TargetingTactic.Focused: break; // always stick with original target? case TargetingTactic.LastDamager: var lastDamager = DamageHistory.LastDamager?.TryGetAttacker() as Creature; if (lastDamager != null) AttackTarget = lastDamager; break; case TargetingTactic.TopDamager: var topDamager = DamageHistory.TopDamager?.TryGetAttacker() as Creature; if (topDamager != null) AttackTarget = topDamager; break; // these below don't seem to be used in PY16 yet... case TargetingTactic.Weakest: // should probably shuffle the list beforehand, // in case a bunch of levels of same level are in a group, // so the same player isn't always selected var lowestLevel = visibleTargets.OrderBy(p => p.Level).FirstOrDefault(); AttackTarget = lowestLevel; break; case TargetingTactic.Strongest: var highestLevel = visibleTargets.OrderByDescending(p => p.Level).FirstOrDefault(); AttackTarget = highestLevel; break; case TargetingTactic.Nearest: var nearest = BuildTargetDistance(visibleTargets); AttackTarget = nearest[0].Target; break; } //Console.WriteLine($"{Name}.FindNextTarget = {AttackTarget.Name}"); if (AttackTarget != null && AttackTarget != prevAttackTarget) EmoteManager.OnNewEnemy(AttackTarget); return AttackTarget != null; } finally { //ServerPerformanceMonitor.AddToCumulativeEvent(ServerPerformanceMonitor.CumulativeEventHistoryType.Monster_Awareness_FindNextTarget, stopwatch.Elapsed.TotalSeconds); } } /// /// Returns a list of attackable targets currently visible to this monster /// public List GetAttackTargets() { var visibleTargets = new List(); foreach (var creature in PhysicsObj.ObjMaint.GetVisibleTargetsValuesOfTypeCreature()) { // ensure attackable if (!creature.Attackable && creature.TargetingTactic == TargetingTactic.None || creature.Teleporting) continue; // ensure within 'detection radius' ? var chaseDistSq = creature == AttackTarget ? MaxChaseRangeSq : VisualAwarenessRangeSq; /*if (Location.SquaredDistanceTo(creature.Location) > chaseDistSq) continue;*/ if (PhysicsObj.get_distance_sq_to_object(creature.PhysicsObj, true) > chaseDistSq) continue; // if this monster belongs to a faction, // ensure target does not belong to the same faction if (SameFaction(creature)) { // unless they have been provoked if (!PhysicsObj.ObjMaint.RetaliateTargetsContainsKey(creature.Guid.Full)) continue; } // cannot switch AttackTargets with Tolerance.Target if (Tolerance.HasFlag(Tolerance.Target) && creature != AttackTarget) continue; // can only target other monsters with Tolerance.Monster -- cannot target players or combat pets if (Tolerance.HasFlag(Tolerance.Monster) && (creature is Player || creature is CombatPet)) continue; visibleTargets.Add(creature); } return visibleTargets; } /// /// Returns the list of potential attack targets, sorted by closest distance /// public List BuildTargetDistance(List targets, bool distSq = false) { var targetDistance = new List(); foreach (var target in targets) //targetDistance.Add(new TargetDistance(target, distSq ? Location.SquaredDistanceTo(target.Location) : Location.DistanceTo(target.Location))); targetDistance.Add(new TargetDistance(target, distSq ? (float)PhysicsObj.get_distance_sq_to_object(target.PhysicsObj, true) : (float)PhysicsObj.get_distance_to_object(target.PhysicsObj, true))); return targetDistance.OrderBy(i => i.Distance).ToList(); } /// /// Uses weighted RNG selection by distance to select a target /// public Creature SelectWeightedDistance(List targetDistances) { if (targetDistances.Count == 1) return targetDistances[0].Target; // http://asheron.wikia.com/wiki/Wi_Flag var distSum = targetDistances.Select(i => i.Distance).Sum(); // get the sum of the inverted ratios var invRatioSum = targetDistances.Count - 1; // roll between 0 - invRatioSum here, // instead of 0-1 (the source of the original wi bug) var rng = ThreadSafeRandom.Next(0.0f, invRatioSum); // walk the list var invRatio = 0.0f; foreach (var targetDistance in targetDistances) { invRatio += 1.0f - (targetDistance.Distance / distSum); if (rng < invRatio) return targetDistance.Target; } // precision error? Console.WriteLine($"{Name}.SelectWeightedDistance: couldn't find target: {string.Join(",", targetDistances.Select(i => i.Distance))}"); return targetDistances[0].Target; } /// /// If one of these fields is set, monster scanning for targets when it first spawns in /// is terminated immediately /// private static readonly Tolerance ExcludeSpawnScan = Tolerance.NoAttack | Tolerance.Appraise | Tolerance.Provoke | Tolerance.Retaliate; /// /// Called when a monster is first spawning in /// public void CheckTargets() { if (!Attackable && TargetingTactic == TargetingTactic.None || (Tolerance & ExcludeSpawnScan) != 0) return; var actionChain = new ActionChain(); actionChain.AddDelaySeconds(0.75f); actionChain.AddAction(this, CheckTargets_Inner); actionChain.EnqueueChain(); } public void CheckTargets_Inner() { Creature closestTarget = null; var closestDistSq = float.MaxValue; foreach (var creature in PhysicsObj.ObjMaint.GetVisibleTargetsValuesOfTypeCreature()) { if (creature is Player player && (!player.Attackable || player.Teleporting || (player.Hidden ?? false))) continue; if (Tolerance.HasFlag(Tolerance.Monster) && (creature is Player || creature is CombatPet)) continue; //var distSq = Location.SquaredDistanceTo(creature.Location); var distSq = PhysicsObj.get_distance_sq_to_object(creature.PhysicsObj, true); if (distSq < closestDistSq) { closestDistSq = (float)distSq; closestTarget = creature; } } if (closestTarget == null || closestDistSq > VisualAwarenessRangeSq) return; closestTarget.AlertMonster(this); } /// /// The most common value from retail /// Some other common values are in the range of 12-25 /// public const float VisualAwarenessRange_Default = 18.0f; /// /// The highest value found in the current database /// public const float VisualAwarenessRange_Highest = 75.0f; public double? VisualAwarenessRange { get => GetProperty(PropertyFloat.VisualAwarenessRange); set { if (!value.HasValue) RemoveProperty(PropertyFloat.VisualAwarenessRange); else SetProperty(PropertyFloat.VisualAwarenessRange, value.Value); } } public double? AuralAwarenessRange { get => GetProperty(PropertyFloat.AuralAwarenessRange); set { if (!value.HasValue) RemoveProperty(PropertyFloat.AuralAwarenessRange); else SetProperty(PropertyFloat.AuralAwarenessRange, value.Value); } } private float? _visualAwarenessRangeSq; public float VisualAwarenessRangeSq { get { if (_visualAwarenessRangeSq == null) { var visualAwarenessRange = (float)((VisualAwarenessRange ?? VisualAwarenessRange_Default) * PropertyManager.GetDouble("mob_awareness_range").Item); _visualAwarenessRangeSq = visualAwarenessRange * visualAwarenessRange; } return _visualAwarenessRangeSq.Value; } } private float? _auralAwarenessRangeSq; public float AuralAwarenessRangeSq { get { if (_auralAwarenessRangeSq == null) { var auralAwarenessRange = (float)((AuralAwarenessRange ?? VisualAwarenessRange ?? VisualAwarenessRange_Default) * PropertyManager.GetDouble("mob_awareness_range").Item); _auralAwarenessRangeSq = auralAwarenessRange * auralAwarenessRange; } return _auralAwarenessRangeSq.Value; } } /// /// A monster can only alert friendly mobs to the presence of each attack target /// once every AlertThreshold /// private static readonly TimeSpan AlertThreshold = TimeSpan.FromMinutes(2); /// /// AttackTarget => last alerted time /// private Dictionary Alerted; public void AlertFriendly() { // if current attacker has already alerted this monster recently, // don't re-alert friendlies if (Alerted != null && Alerted.TryGetValue(AttackTarget.Guid.Full, out var lastAlertTime) && DateTime.UtcNow - lastAlertTime < AlertThreshold) return; var visibleObjs = PhysicsObj.ObjMaint.GetVisibleObjects(PhysicsObj.CurCell); var targetCreature = AttackTarget as Creature; var alerted = false; foreach (var obj in visibleObjs) { var nearbyCreature = obj.WeenieObj.WorldObject as Creature; if (nearbyCreature == null || nearbyCreature.IsAwake || !nearbyCreature.Attackable && nearbyCreature.TargetingTactic == TargetingTactic.None) continue; if ((nearbyCreature.Tolerance & AlertExclude) != 0) continue; if (CreatureType != null && CreatureType == nearbyCreature.CreatureType || FriendType != null && FriendType == nearbyCreature.CreatureType) { //var distSq = Location.SquaredDistanceTo(nearbyCreature.Location); var distSq = PhysicsObj.get_distance_sq_to_object(nearbyCreature.PhysicsObj, true); if (distSq > nearbyCreature.AuralAwarenessRangeSq) continue; // scenario: spawn a faction mob, and then spawn a non-faction mob next to it, of the same CreatureType // the spawning mob will become alerted by the faction mob, and will then go to alert its friendly types // the faction mob happens to be a friendly type, so it in effect becomes alerted to itself // this is to prevent the faction mob from adding itself to its retaliate targets / visible targets, // and setting itself to its AttackTarget if (nearbyCreature == AttackTarget) continue; if (nearbyCreature.SameFaction(targetCreature)) nearbyCreature.AddRetaliateTarget(AttackTarget); if (PotentialFoe(targetCreature)) { if (nearbyCreature.PotentialFoe(targetCreature)) nearbyCreature.AddRetaliateTarget(AttackTarget); else continue; } alerted = true; nearbyCreature.AttackTarget = AttackTarget; nearbyCreature.WakeUp(false); } } // only set alerted if monsters were actually alerted if (alerted) { if (Alerted == null) Alerted = new Dictionary(); Alerted[AttackTarget.Guid.Full] = DateTime.UtcNow; } } /// /// Wakes up a faction monster from any non-faction monsters wandering within range /// public void FactionMob_CheckMonsters() { if (MonsterState != State.Idle) return; var creatures = PhysicsObj.ObjMaint.GetVisibleTargetsValuesOfTypeCreature(); foreach (var creature in creatures) { // ensure type isn't already handled elsewhere if (creature is Player || creature is CombatPet) continue; // ensure attackable if (creature.IsDead || !creature.Attackable && creature.TargetingTactic == TargetingTactic.None || creature.Teleporting) continue; // ensure another faction if (SameFaction(creature) && !PotentialFoe(creature)) continue; // ensure within detection range if (PhysicsObj.get_distance_sq_to_object(creature.PhysicsObj, true) > VisualAwarenessRangeSq) continue; creature.AlertMonster(this); break; } } } }