using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Intersect.Enums; using Intersect.GameObjects; using Intersect.Logging; using Intersect.Network.Packets.Server; using Intersect.Server.Database; using Intersect.Server.Database.PlayerData.Players; using Intersect.Server.Entities.Combat; using Intersect.Server.Entities.Events; using Intersect.Server.Entities.Pathfinding; using Intersect.Server.General; using Intersect.Server.Maps; using Intersect.Server.Networking; using Intersect.Utilities; namespace Intersect.Server.Entities { public partial class Npc : Entity { //Spell casting public long CastFreq; /// /// Damage Map - Keep track of who is doing the most damage to this npc and focus accordingly /// public ConcurrentDictionary DamageMap = new ConcurrentDictionary(); public ConcurrentDictionary LootMap = new ConcurrentDictionary(); public Guid[] LootMapCache = Array.Empty(); /// /// Returns the entity that ranks the highest on this NPC's damage map. /// public Entity DamageMapHighest { get { long damage = 0; Entity top = null; foreach (var pair in DamageMap) { if (pair.Value > damage) { top = pair.Key; damage = pair.Value; } } return top; } } public bool Despawnable; //Moving public long LastRandomMove; //Pathfinding private Pathfinder mPathFinder; private Task mPathfindingTask; public byte Range; //Respawn/Despawn public long RespawnTime; public long FindTargetWaitTime; public int FindTargetDelay = 500; private int mTargetFailCounter = 0; private int mTargetFailMax = 10; private int mResetDistance = 0; private int mResetCounter = 0; private int mResetMax = 100; private bool mResetting = false; /// /// The map on which this NPC was "aggro'd" and started chasing a target. /// public MapInstance AggroCenterMap; /// /// The X value on which this NPC was "aggro'd" and started chasing a target. /// public int AggroCenterX; /// /// The Y value on which this NPC was "aggro'd" and started chasing a target. /// public int AggroCenterY; /// /// The Z value on which this NPC was "aggro'd" and started chasing a target. /// public int AggroCenterZ; public Npc(NpcBase myBase, bool despawnable = false) : base() { Name = myBase.Name; Sprite = myBase.Sprite; Color = myBase.Color; Level = myBase.Level; Base = myBase; Despawnable = despawnable; for (var i = 0; i < (int)Stats.StatCount; i++) { BaseStats[i] = myBase.Stats[i]; Stat[i] = new Stat((Stats)i, this); } var spellSlot = 0; for (var I = 0; I < Base.Spells.Count; I++) { var slot = new SpellSlot(spellSlot); slot.Set(new Spell(Base.Spells[I])); Spells.Add(slot); spellSlot++; } //Give NPC Drops var itemSlot = 0; foreach (var drop in myBase.Drops) { var slot = new InventorySlot(itemSlot); slot.Set(new Item(drop.ItemId, drop.Quantity)); slot.DropChance = drop.Chance; Items.Add(slot); itemSlot++; } for (var i = 0; i < (int)Vitals.VitalCount; i++) { SetMaxVital(i, myBase.MaxVital[i]); SetVital(i, myBase.MaxVital[i]); } Range = (byte)myBase.SightRange; mPathFinder = new Pathfinder(this); } public NpcBase Base { get; private set; } private bool IsStunnedOrSleeping => CachedStatuses.Any(PredicateStunnedOrSleeping); private bool IsUnableToCastSpells => CachedStatuses.Any(PredicateUnableToCastSpells); public override EntityTypes GetEntityType() { return EntityTypes.GlobalEntity; } public override void Die(bool generateLoot = true, Entity killer = null) { lock (EntityLock) { base.Die(generateLoot, killer); AggroCenterMap = null; AggroCenterX = 0; AggroCenterY = 0; AggroCenterZ = 0; MapInstance.Get(MapId).RemoveEntity(this); PacketSender.SendEntityDie(this); PacketSender.SendEntityLeave(this); } } public bool TargetHasStealth(Entity target) { return target == null || target.CachedStatuses.Any(s => s.Type == StatusTypes.Stealth); } //Targeting public void AssignTarget(Entity en) { var oldTarget = Target; // Are we resetting? If so, do not allow for a new target. var pathTarget = mPathFinder?.GetTarget(); if (AggroCenterMap != null && pathTarget != null && pathTarget.TargetMapId == AggroCenterMap.Id && pathTarget.TargetX == AggroCenterX && pathTarget.TargetY == AggroCenterY) { if (en == null) { return; } else { return; } } //Why are we doing all of this logic if we are assigning a target that we already have? if (en != null && en != Target) { // Can't assign a new target if taunted, unless we're resetting their target somehow. // Also make sure the taunter is in range.. If they're dead or gone, we go for someone else! if ((pathTarget != null && AggroCenterMap != null && (pathTarget.TargetMapId != AggroCenterMap.Id || pathTarget.TargetX != AggroCenterX || pathTarget.TargetY != AggroCenterY))) { foreach (var status in CachedStatuses) { if (status.Type == StatusTypes.Taunt && en != status.Attacker && GetDistanceTo(status.Attacker) != 9999) { return; } } } if (en.GetType() == typeof(Projectile)) { if (((Projectile)en).Owner != this && !TargetHasStealth((Projectile)en)) { Target = ((Projectile)en).Owner; } } else { if (en.GetType() == typeof(Npc)) { if (((Npc)en).Base == Base) { if (Base.AttackAllies == false) { return; } } } if (en.GetType() == typeof(Player)) { //TODO Make sure that the npc can target the player if (this != en && !TargetHasStealth(en)) { Target = en; } } else { if (this != en && !TargetHasStealth(en)) { Target = en; } } } // Are we configured to handle resetting NPCs after they chase a target for a specified amount of tiles? if (Options.Npc.AllowResetRadius) { // Are we configured to allow new reset locations before they move to their original location, or do we simply not have an original location yet? if (Options.Npc.AllowNewResetLocationBeforeFinish || AggroCenterMap == null) { AggroCenterMap = Map; AggroCenterX = X; AggroCenterY = Y; AggroCenterZ = Z; } } } else { Target = en; } if (Target != oldTarget) { CombatTimer = Timing.Global.Milliseconds + Options.CombatTime; PacketSender.SendNpcAggressionToProximity(this); } mTargetFailCounter = 0; } public void RemoveFromDamageMap(Entity en) { DamageMap.TryRemove(en, out _); } public void RemoveTarget() { AssignTarget(null); } public override int CalculateAttackTime() { if (Base.AttackSpeedModifier == 1) //Static { return Base.AttackSpeedValue; } return base.CalculateAttackTime(); } public override bool CanAttack(Entity entity, SpellBase spell) { if (!base.CanAttack(entity, spell)) { return false; } if (entity.GetType() == typeof(EventPageInstance)) { return false; } //Check if the attacker is stunned or blinded. foreach (var status in CachedStatuses) { if (status.Type == StatusTypes.Stun || status.Type == StatusTypes.Sleep) { return false; } } if (TargetHasStealth(entity)) { return false; } if (entity.GetType() == typeof(Resource)) { if (!entity.Passable) { return false; } } else if (entity.GetType() == typeof(Npc)) { return CanNpcCombat(entity, spell != null && spell.Combat.Friendly) || entity == this; } else if (entity.GetType() == typeof(Player)) { var player = (Player) entity; var friendly = spell != null && spell.Combat.Friendly; if (friendly && IsAllyOf(player)) { return true; } if (!friendly && !IsAllyOf(player)) { return true; } return false; } return true; } public override void TryAttack(Entity target) { if (target.IsDisposed) { return; } if (!CanAttack(target, null)) { return; } if (!IsOneBlockAway(target)) { return; } if (!IsFacingTarget(target)) { return; } var deadAnimations = new List>(); var aliveAnimations = new List>(); //We were forcing at LEAST 1hp base damage.. but then you can't have guards that won't hurt the player. //https://www.ascensiongamedev.com/community/bug_tracker/intersect/npc-set-at-0-attack-damage-still-damages-player-by-1-initially-r915/ if (AttackTimer < Globals.Timing.Milliseconds) { if (Base.AttackAnimation != null) { PacketSender.SendAnimationToProximity( Base.AttackAnimationId, -1, Guid.Empty, target.MapId, (byte) target.X, (byte) target.Y, (sbyte) Dir ); } base.TryAttack( target, Base.Damage, (DamageType) Base.DamageType, (Stats) Base.ScalingStat, Base.Scaling, Base.Stats[(int)Stats.Chimang], Base.Stats[(int)Stats.STChimang], Base.Stats[(int)Stats.Xuyengiap], Base.Stats[(int)Stats.STXuyengiap], deadAnimations, aliveAnimations ); PacketSender.SendEntityAttack(this, CalculateAttackTime()); } } public bool CanNpcCombat(Entity enemy, bool friendly = false) { //Check for NpcVsNpc Combat, both must be enabled and the attacker must have it as an enemy or attack all types of npc. if (!friendly) { if (enemy != null && enemy.GetType() == typeof(Npc) && Base != null) { if (((Npc) enemy).Base.NpcVsNpcEnabled == false) { return false; } if (Base.AttackAllies && ((Npc) enemy).Base == Base) { return true; } for (var i = 0; i < Base.AggroList.Count; i++) { if (NpcBase.Get(Base.AggroList[i]) == ((Npc) enemy).Base) { return true; } } return false; } if (enemy != null && enemy.GetType() == typeof(Player)) { return true; } } else { if (enemy != null && enemy.GetType() == typeof(Npc) && Base != null && ((Npc) enemy).Base == Base && Base.AttackAllies == false) { return true; } else if (enemy != null && enemy.GetType() == typeof(Player)) { return false; } } return false; } private static bool PredicateStunnedOrSleeping(Status status) { switch (status?.Type) { case StatusTypes.Sleep: case StatusTypes.Stun: return true; case StatusTypes.Silence: case StatusTypes.None: case StatusTypes.Snare: case StatusTypes.Blind: case StatusTypes.Stealth: case StatusTypes.Transform: case StatusTypes.Cleanse: case StatusTypes.Invulnerable: case StatusTypes.Shield: case StatusTypes.OnHit: case StatusTypes.Taunt: case null: return false; default: throw new ArgumentOutOfRangeException(); } } private static bool PredicateUnableToCastSpells(Status status) { switch (status?.Type) { case StatusTypes.Silence: case StatusTypes.Sleep: case StatusTypes.Stun: return true; case StatusTypes.None: case StatusTypes.Snare: case StatusTypes.Blind: case StatusTypes.Stealth: case StatusTypes.Transform: case StatusTypes.Cleanse: case StatusTypes.Invulnerable: case StatusTypes.Shield: case StatusTypes.OnHit: case StatusTypes.Taunt: case null: return false; default: throw new ArgumentOutOfRangeException(); } } public override int CanMove(int moveDir) { var canMove = base.CanMove(moveDir); if ((canMove == -1 || canMove == -4) && IsFleeing() && Options.Instance.NpcOpts.AllowResetRadius) { var yOffset = 0; var xOffset = 0; var tile = new TileHelper(MapId, X, Y); switch (moveDir) { case 0: //Up yOffset--; break; case 1: //Down yOffset++; break; case 2: //Left xOffset--; break; case 3: //Right xOffset++; break; case 4: //NW yOffset--; xOffset--; break; case 5: //NE yOffset--; xOffset++; break; case 6: //SW yOffset++; xOffset--; break; case 7: //SE yOffset++; xOffset++; break; } if (tile.Translate(xOffset, yOffset)) { //If this would move us past our reset radius then we cannot move. var dist = GetDistanceBetween(AggroCenterMap, tile.GetMap(), AggroCenterX, tile.GetX(), AggroCenterY, tile.GetY()); if (dist > Math.Max(Options.Npc.ResetRadius, Base.ResetRadius)) { return -2; } } } return canMove; } private void TryCastSpells() { var target = Target; if (target == null || mPathFinder.GetTarget() == null) { return; } // Check if NPC is stunned/sleeping if (IsStunnedOrSleeping) { return; } //Check if NPC is casting a spell if (CastTime > Globals.Timing.Milliseconds) { return; //can't move while casting } if (CastFreq >= Globals.Timing.Milliseconds) { return; } // Check if the NPC is able to cast spells if (IsUnableToCastSpells) { return; } if (Base.Spells == null || Base.Spells.Count <= 0) { return; } // Pick a random spell var spellIndex = Randomization.Next(0, Spells.Count); var spellId = Base.Spells[spellIndex]; var spellBase = SpellBase.Get(spellId); if (spellBase == null) { return; } if (spellBase.Combat == null) { Log.Warn($"Combat data missing for {spellBase.Id}."); } var range = spellBase.Combat?.CastRange ?? 0; var targetType = spellBase.Combat?.TargetType ?? SpellTargetTypes.Single; var projectileBase = spellBase.Combat?.Projectile; if (spellBase.SpellType == SpellTypes.CombatSpell && targetType == SpellTargetTypes.Projectile && projectileBase != null && InRangeOf(target, projectileBase.Range)) { range = projectileBase.Range; var dirToEnemy = DirToEnemy(target); if (dirToEnemy != Dir) { if (LastRandomMove >= Globals.Timing.Milliseconds) { return; } //Face the target -- next frame fire -- then go on with life ChangeDir(dirToEnemy); // Gotta get dir to enemy LastRandomMove = Globals.Timing.Milliseconds + Randomization.Next(1000, 3000); return; } } if (spellBase.VitalCost == null) { return; } if (spellBase.VitalCost[(int) Vitals.Mana] > GetVital(Vitals.Mana)) { return; } if (spellBase.VitalCost[(int) Vitals.Health] > GetVital(Vitals.Health)) { return; } var spell = Spells[spellIndex]; if (spell == null) { return; } if (SpellCooldowns.ContainsKey(spell.SpellId) && SpellCooldowns[spell.SpellId] >= Globals.Timing.MillisecondsUTC) { return; } if (!InRangeOf(target, range) && targetType == SpellTargetTypes.Single) { // ReSharper disable once SwitchStatementMissingSomeCases return; } CastTime = Globals.Timing.Milliseconds + spellBase.CastDuration; if (spellBase.VitalCost[(int) Vitals.Mana] > 0) { SubVital(Vitals.Mana, spellBase.VitalCost[(int) Vitals.Mana]); } else { AddVital(Vitals.Mana, -spellBase.VitalCost[(int) Vitals.Mana]); } if (spellBase.VitalCost[(int) Vitals.Health] > 0) { SubVital(Vitals.Health, spellBase.VitalCost[(int) Vitals.Health]); } else { AddVital(Vitals.Health, -spellBase.VitalCost[(int) Vitals.Health]); } if ((spellBase.Combat?.Friendly ?? false) && spellBase.SpellType != SpellTypes.WarpTo) { CastTarget = this; } else { CastTarget = target; } switch (Base.SpellFrequency) { case 0: CastFreq = Globals.Timing.Milliseconds + 30000; break; case 1: CastFreq = Globals.Timing.Milliseconds + 15000; break; case 2: CastFreq = Globals.Timing.Milliseconds + 8000; break; case 3: CastFreq = Globals.Timing.Milliseconds + 4000; break; case 4: CastFreq = Globals.Timing.Milliseconds + 2000; break; } SpellCastSlot = spellIndex; if (spellBase.CastAnimationId != Guid.Empty) { PacketSender.SendAnimationToProximity(spellBase.CastAnimationId, 1, Id, MapId, 0, 0, (sbyte) Dir); //Target Type 1 will be global entity } PacketSender.SendEntityCastTime(this, spellId); } public bool IsFleeing() { if (Base.FleeHealthPercentage > 0) { var fleeHpCutoff = GetMaxVital(Vitals.Health) * ((float)Base.FleeHealthPercentage / 100f); if (GetVital(Vitals.Health) < fleeHpCutoff) { return true; } } return false; } // TODO: Improve NPC movement to be more fluid like a player //General Updating public override void Update(long timeMs) { var lockObtained = false; try { Monitor.TryEnter(EntityLock, ref lockObtained); if (lockObtained) { var curMapLink = MapId; base.Update(timeMs); var tempTarget = Target; foreach (var status in CachedStatuses) { if (status.Type == StatusTypes.Stun || status.Type == StatusTypes.Sleep) { return; } } var fleeing = IsFleeing(); if (MoveTimer < Globals.Timing.Milliseconds) { var targetMap = Guid.Empty; var targetX = 0; var targetY = 0; var targetZ = 0; //TODO Clear Damage Map if out of combat (target is null and combat timer is to the point that regen has started) if (tempTarget != null && Timing.Global.Milliseconds > CombatTimer) { if (CheckForResetLocation(true)) { if (Target != tempTarget) { PacketSender.SendNpcAggressionToProximity(this); } return; } } // Are we resetting? If so, regenerate completely! if (mResetting) { var distance = GetDistanceTo(AggroCenterMap, AggroCenterX, AggroCenterY); // Have we reached our destination? If so, clear it. if (distance < 1) { targetMap = Guid.Empty; // Reset our aggro center so we can get "pulled" again. AggroCenterMap = null; AggroCenterX = 0; AggroCenterY = 0; AggroCenterZ = 0; mPathFinder?.SetTarget(null); mResetting = false; } ResetNpc(Options.Instance.NpcOpts.ContinuouslyResetVitalsAndStatuses); tempTarget = Target; if (distance != mResetDistance) { mResetDistance = distance; } else { // Something is fishy here.. We appear to be stuck in a reset loop? // Give it a few more attempts and kill the Npc is it keeps going! mResetCounter++; if (mResetCounter > mResetMax) { // Kill the Npc, and simply do not drop any loot or give any credit. Die(false, null); mResetCounter = 0; mResetDistance = 0; } } } if (tempTarget != null && (tempTarget.IsDead() || !InRangeOf(tempTarget, Options.MapWidth * 2))) { TryFindNewTarget(Timing.Global.Milliseconds, tempTarget.Id); tempTarget = Target; } //Check if there is a target, if so, run their ass down. if (tempTarget != null) { if (!tempTarget.IsDead() && CanAttack(tempTarget, null)) { targetMap = tempTarget.MapId; targetX = tempTarget.X; targetY = tempTarget.Y; targetZ = tempTarget.Z; foreach (var targetStatus in tempTarget.CachedStatuses) { if (targetStatus.Type == StatusTypes.Stealth) { targetMap = Guid.Empty; targetX = 0; targetY = 0; targetZ = 0; } } } } else //Find a target if able { // Check if attack on sight or have other npc's to target TryFindNewTarget(timeMs); tempTarget = Target; } if (targetMap != Guid.Empty) { //Check if target map is on one of the surrounding maps, if not then we are not even going to look. if (targetMap != MapId) { var found = false; foreach (var map in MapInstance.Get(MapId).SurroundingMaps) { if (map.Id == targetMap) { found = true; break; } } if (!found) { targetMap = Guid.Empty; } } } if (targetMap != Guid.Empty) { if (mPathFinder.GetTarget() != null) { if (targetMap != mPathFinder.GetTarget().TargetMapId || targetX != mPathFinder.GetTarget().TargetX || targetY != mPathFinder.GetTarget().TargetY) { mPathFinder.SetTarget(null); } } if (mPathFinder.GetTarget() == null) { mPathFinder.SetTarget(new PathfinderTarget(targetMap, targetX, targetY, targetZ)); if (tempTarget != Target) { tempTarget = Target; } } } if (mPathFinder.GetTarget() != null && Base.Movement != (int)NpcMovement.Static) { TryCastSpells(); // TODO: Make resetting mobs actually return to their starting location. if ((!mResetting && !IsOneBlockAway( mPathFinder.GetTarget().TargetMapId, mPathFinder.GetTarget().TargetX, mPathFinder.GetTarget().TargetY, mPathFinder.GetTarget().TargetZ )) || (mResetting && GetDistanceTo(AggroCenterMap, AggroCenterX, AggroCenterY) != 0) ) { switch (mPathFinder.Update(timeMs)) { case PathfinderResult.Success: var dir = mPathFinder.GetMove(); if (dir > -1) { if (fleeing) { switch (dir) { case 0: dir = 1; break; case 1: dir = 0; break; case 2: dir = 3; break; case 3: dir = 2; break; } } if (CanMove(dir) == -1 || CanMove(dir) == -4) { //check if NPC is snared or stunned foreach (var status in CachedStatuses) { if (status.Type == StatusTypes.Stun || status.Type == StatusTypes.Snare || status.Type == StatusTypes.Sleep) { return; } } Move((byte)dir, null); } else { mPathFinder.PathFailed(timeMs); } // Are we resetting? if (mResetting) { // Have we reached our destination? If so, clear it. if (GetDistanceTo(AggroCenterMap, AggroCenterX, AggroCenterY) == 0) { targetMap = Guid.Empty; // Reset our aggro center so we can get "pulled" again. AggroCenterMap = null; AggroCenterX = 0; AggroCenterY = 0; AggroCenterZ = 0; mPathFinder?.SetTarget(null); mResetting = false; } } } break; case PathfinderResult.OutOfRange: TryFindNewTarget(timeMs, tempTarget?.Id ?? Guid.Empty, true); tempTarget = Target; targetMap = Guid.Empty; break; case PathfinderResult.NoPathToTarget: TryFindNewTarget(timeMs, tempTarget?.Id ?? Guid.Empty, true); tempTarget = Target; targetMap = Guid.Empty; break; case PathfinderResult.Failure: targetMap = Guid.Empty; TryFindNewTarget(timeMs, tempTarget?.Id ?? Guid.Empty, true); tempTarget = Target; break; case PathfinderResult.Wait: targetMap = Guid.Empty; break; default: throw new ArgumentOutOfRangeException(); } } else { var fleed = false; if (tempTarget != null && fleeing) { var dir = DirToEnemy(tempTarget); switch (dir) { case 0: dir = 1; break; case 1: dir = 0; break; case 2: dir = 3; break; case 3: dir = 2; break; } if (CanMove(dir) == -1 || CanMove(dir) == -4) { //check if NPC is snared or stunned foreach (var status in CachedStatuses) { if (status.Type == StatusTypes.Stun || status.Type == StatusTypes.Snare || status.Type == StatusTypes.Sleep) { return; } } Move(dir, null); fleed = true; } } if (!fleed) { if (tempTarget != null) { if (Dir != DirToEnemy(tempTarget) && DirToEnemy(tempTarget) != -1) { ChangeDir(DirToEnemy(tempTarget)); } else { if (tempTarget.IsDisposed) { TryFindNewTarget(timeMs); tempTarget = Target; } else { if (CanAttack(tempTarget, null)) { TryAttack(tempTarget); } } } } } } } CheckForResetLocation(); //Move randomly if (targetMap != Guid.Empty) { return; } if (LastRandomMove >= Globals.Timing.Milliseconds || CastTime > 0) { return; } if (Base.Movement == (int)NpcMovement.StandStill) { LastRandomMove = Globals.Timing.Milliseconds + Randomization.Next(1000, 3000); return; } else if (Base.Movement == (int)NpcMovement.TurnRandomly) { ChangeDir((byte)Randomization.Next(0, 4)); LastRandomMove = Globals.Timing.Milliseconds + Randomization.Next(1000, 3000); return; } var i = Randomization.Next(0, 1); if (i == 0) { i = Randomization.Next(0, 4); if (CanMove(i) == -1) { //check if NPC is snared or stunned foreach (var status in CachedStatuses) { if (status.Type == StatusTypes.Stun || status.Type == StatusTypes.Snare || status.Type == StatusTypes.Sleep) { return; } } Move((byte)i, null); } } LastRandomMove = Globals.Timing.Milliseconds + Randomization.Next(1000, 3000); if (fleeing) { LastRandomMove = Globals.Timing.Milliseconds + (long) GetMovementTime(); } } //If we switched maps, lets update the maps if (curMapLink != MapId) { if (curMapLink == Guid.Empty) { MapInstance.Get(curMapLink).RemoveEntity(this); } if (MapId != Guid.Empty) { MapInstance.Get(MapId).AddEntity(this); } } } } finally { if (lockObtained) { Monitor.Exit(EntityLock); } } } private bool CheckForResetLocation(bool forceDistance = false) { // Check if we've moved out of our range we're allowed to move from after being "aggro'd" by something. // If so, remove target and move back to the origin point. if (Options.Npc.AllowResetRadius && AggroCenterMap != null && (GetDistanceTo(AggroCenterMap, AggroCenterX, AggroCenterY) > Math.Max(Options.Npc.ResetRadius, Base.ResetRadius) || forceDistance)) { ResetNpc(Options.Npc.ResetVitalsAndStatusses); mResetCounter = 0; mResetDistance = 0; // Try and move back to where we came from before we started chasing something. mResetting = true; mPathFinder.SetTarget(new PathfinderTarget(AggroCenterMap.Id, AggroCenterX, AggroCenterY, AggroCenterZ)); return true; } return false; } private void ResetNpc(bool resetVitals = true, bool clearLocation = false) { // Remove our target. RemoveTarget(); DamageMap.Clear(); LootMap.Clear(); LootMapCache = Array.Empty(); if (clearLocation) { mPathFinder.SetTarget(null); AggroCenterMap = null; AggroCenterX = 0; AggroCenterY = 0; AggroCenterZ = 0; } // Reset our vitals and statusses when configured. if (resetVitals) { Statuses.Clear(); CachedStatuses = Statuses.Values.ToArray(); DoT.Clear(); CachedDots = DoT.Values.ToArray(); for (var v = 0; v < (int)Vitals.VitalCount; v++) { RestoreVital((Vitals)v); } } } public override void NotifySwarm(Entity attacker) { var mapEntities = MapInstance.Get(MapId).GetEntities(true); foreach (var en in mapEntities) { if (en.GetType() == typeof(Npc)) { var npc = (Npc) en; if (npc.Target == null & npc.Base.Swarm && npc.Base == Base) { if (npc.InRangeOf(attacker, npc.Base.SightRange)) { npc.AssignTarget(attacker); } } } } } public bool CanPlayerAttack(Player en) { //Check to see if the npc is a friend/protector... if (IsAllyOf(en)) { return false; } //If not then check and see if player meets the conditions to attack the npc... if (Base.PlayerCanAttackConditions.Lists.Count == 0 || Conditions.MeetsConditionLists(Base.PlayerCanAttackConditions, en, null)) { return true; } return false; } public override bool IsAllyOf(Entity otherEntity) { switch (otherEntity) { case Npc otherNpc: return Base == otherNpc.Base; case Player otherPlayer: var conditionLists = Base.PlayerFriendConditions; if ((conditionLists?.Count ?? 0) == 0) { return false; } return Conditions.MeetsConditionLists(conditionLists, otherPlayer, null); default: return base.IsAllyOf(otherEntity); } } public bool ShouldAttackPlayerOnSight(Player en) { if (IsAllyOf(en)) { return false; } if (Base.Aggressive) { if (Base.AttackOnSightConditions.Lists.Count > 0 && Conditions.MeetsConditionLists(Base.AttackOnSightConditions, en, null)) { return false; } return true; } else { if (Base.AttackOnSightConditions.Lists.Count > 0 && Conditions.MeetsConditionLists(Base.AttackOnSightConditions, en, null)) { return true; } } return false; } public void TryFindNewTarget(long timeMs, Guid avoidId = new Guid(), bool ignoreTimer = false, Entity attackedBy = null) { if (!ignoreTimer && FindTargetWaitTime > timeMs) { return; } // Are we resetting? If so, do not allow for a new target. var pathTarget = mPathFinder?.GetTarget(); if (AggroCenterMap != null && pathTarget != null && pathTarget.TargetMapId == AggroCenterMap.Id && pathTarget.TargetX == AggroCenterX && pathTarget.TargetY == AggroCenterY) { if (!Options.Instance.NpcOpts.AllowEngagingWhileResetting || attackedBy == null || attackedBy.GetDistanceTo(AggroCenterMap, AggroCenterX, AggroCenterY) > Math.Max(Options.Instance.NpcOpts.ResetRadius, Base.ResetRadius)) { return; } else { //We're resetting and just got attacked, and we allow reengagement.. let's stop resetting and fight! mPathFinder?.SetTarget(null); mResetting = false; AssignTarget(attackedBy); return; } } var maps = MapInstance.Get(MapId).GetSurroundingMaps(true); var possibleTargets = new List(); var closestRange = Range + 1; //If the range is out of range we didn't find anything. var closestIndex = -1; var highestDmgIndex = -1; if (DamageMap.Count > 0) { // Go through all of our potential targets in order of damage done as instructed and select the first matching one. long highestDamage = 0; foreach (var en in DamageMap.ToArray()) { // Are we supposed to avoid this one? if (en.Key.Id == avoidId) { continue; } // Is this entry dead?, if so skip it. if (en.Key.IsDead()) { continue; } // Are we at a valid distance? (9999 means not on this map or somehow null!) if (GetDistanceTo(en.Key) != 9999) { possibleTargets.Add(en.Key); // Do we have the highest damage? if (en.Value > highestDamage) { highestDmgIndex = possibleTargets.Count - 1; highestDamage = en.Value; } } } } // Scan for nearby targets foreach (var map in maps) { foreach (var entity in map.GetCachedEntities()) { if (entity != null && !entity.IsDead() && entity != this && entity.Id != avoidId) { //TODO Check if NPC is allowed to attack player with new conditions if (entity.GetType() == typeof(Player)) { // Are we aggressive towards this player or have they hit us? if (ShouldAttackPlayerOnSight((Player)entity) || DamageMap.ContainsKey(entity)) { var dist = GetDistanceTo(entity); if (dist <= Range && dist < closestRange) { possibleTargets.Add(entity); closestIndex = possibleTargets.Count - 1; closestRange = dist; } } } else if (entity.GetType() == typeof(Npc)) { if (Base.Aggressive && Base.AggroList.Contains(((Npc)entity).Base.Id)) { var dist = GetDistanceTo(entity); if (dist <= Range && dist < closestRange) { possibleTargets.Add(entity); closestIndex = possibleTargets.Count - 1; closestRange = dist; } } } } } } // Assign our target if we've found one! if (Base.FocusHighestDamageDealer && highestDmgIndex != -1) { // We're focussed on whoever has the most threat! o7 AssignTarget(possibleTargets[highestDmgIndex]); } else if (Target != null && possibleTargets.Count > 0) { // Time to randomize who we target.. Since we don't actively care who we attack! // 10% chance to just go for someone else. if (Randomization.Next(1, 101) > 90) { if (possibleTargets.Count > 1) { var target = Randomization.Next(0, possibleTargets.Count - 1); AssignTarget(possibleTargets[target]); } else { AssignTarget(possibleTargets[0]); } } } else if (Target == null && Base.Aggressive && closestIndex != -1) { // Aggressively attack closest person! AssignTarget(possibleTargets[closestIndex]); } else if (possibleTargets.Count > 0) { // Not aggressive but no target, so just try and attack SOMEONE on the damage table! if (possibleTargets.Count > 1) { var target = Randomization.Next(0, possibleTargets.Count - 1); AssignTarget(possibleTargets[target]); } else { AssignTarget(possibleTargets[0]); } } else { // ??? What the frick is going on here? // We can't find a valid target somehow, keep it up a few times and reset if this keeps failing! mTargetFailCounter += 1; if (mTargetFailCounter > mTargetFailMax) { CheckForResetLocation(true); } } FindTargetWaitTime = timeMs + FindTargetDelay; } public override void ProcessRegen() { if (Base == null) { return; } foreach (Vitals vital in Enum.GetValues(typeof(Vitals))) { if (vital >= Vitals.VitalCount) { continue; } var vitalId = (int) vital; var vitalValue = GetVital(vital); var maxVitalValue = GetMaxVital(vital); if (vitalValue >= maxVitalValue) { continue; } var vitalRegenRate = Base.VitalRegen[vitalId] / 100f; var regenValue = (int) Math.Max(1, maxVitalValue * vitalRegenRate) * Math.Abs(Math.Sign(vitalRegenRate)); AddVital(vital, regenValue); } } public override void Warp( Guid newMapId, float newX, float newY, byte newDir, bool adminWarp = false, byte zOverride = 0, bool mapSave = false ) { var map = MapInstance.Get(newMapId); if (map == null) { return; } X = (int)newX; Y = (int)newY; Z = zOverride; Dir = newDir; if (newMapId != MapId) { var oldMap = MapInstance.Get(MapId); if (oldMap != null) { oldMap.RemoveEntity(this); } PacketSender.SendEntityLeave(this); MapId = newMapId; PacketSender.SendEntityDataToProximity(this); PacketSender.SendEntityPositionToAll(this); } else { PacketSender.SendEntityPositionToAll(this); PacketSender.SendEntityStats(this); } } public int GetAggression(Player player) { //Determines the aggression level of this npc to send to the player if (this.Target != null) { return -1; } else { //Guard = 3 //Will attack on sight = 1 //Will attack if attacked = 0 //Can't attack nor can attack = 2 var ally = IsAllyOf(player); var attackOnSight = ShouldAttackPlayerOnSight(player); var canPlayerAttack = CanPlayerAttack(player); if (ally && !canPlayerAttack) { return 3; } if (attackOnSight) { return 1; } if (!ally && !attackOnSight && canPlayerAttack) { return 0; } if (!ally && !attackOnSight && !canPlayerAttack) { return 2; } } return 2; } public override EntityPacket EntityPacket(EntityPacket packet = null, Player forPlayer = null) { if (packet == null) { packet = new NpcEntityPacket(); } packet = base.EntityPacket(packet, forPlayer); var pkt = (NpcEntityPacket) packet; pkt.Aggression = GetAggression(forPlayer); return pkt; } } }