diff --git a/Intersect (Core)/Enums/NpcMovement.cs b/Intersect (Core)/Enums/NpcMovement.cs index b001c35567..37e4ef4515 100644 --- a/Intersect (Core)/Enums/NpcMovement.cs +++ b/Intersect (Core)/Enums/NpcMovement.cs @@ -9,4 +9,12 @@ public enum NpcMovement StandStill, Static, + + HorizontalPatrol, + + VerticalPatrol, + + BackslashPatrol, + + ForwardslashPatrol, } diff --git a/Intersect.Client.Core/Entities/Critter.cs b/Intersect.Client.Core/Entities/Critter.cs index 55a0619e7e..11aa138976 100644 --- a/Intersect.Client.Core/Entities/Critter.cs +++ b/Intersect.Client.Core/Entities/Critter.cs @@ -15,7 +15,10 @@ namespace Intersect.Client.Entities; public partial class Critter : Entity { private readonly MapCritterAttribute mAttribute; - private long mLastMove = -1; + + // Critter's Movement + private long _lastMove = -1; + private byte _randomMoveRange; public Critter(MapInstance map, byte x, byte y, MapCritterAttribute att) : base(Guid.NewGuid(), null, EntityType.GlobalEntity) { @@ -50,65 +53,65 @@ public Critter(MapInstance map, byte x, byte y, MapCritterAttribute att) : base( public override bool Update() { - if (base.Update()) + if (!base.Update()) { - if (mLastMove < Timing.Global.MillisecondsUtc) - { - switch (mAttribute.Movement) - { - case 0: //Move Randomly - MoveRandomly(); - break; - case 1: //Turn? - DirectionFacing = Randomization.NextDirection(); - break; - - } - - mLastMove = Timing.Global.MillisecondsUtc + mAttribute.Frequency + Globals.Random.Next((int)(mAttribute.Frequency * .5f)); - } + return false; + } + // Only skip if we are NOT in the middle of a range-walk AND the frequency timer is active + if (_randomMoveRange <= 0 && _lastMove >= Timing.Global.MillisecondsUtc) + { return true; } - return false; + switch (mAttribute.Movement) + { + case 0: // Move Randomly + MoveRandomly(); + break; + case 1: // Turn Randomly + DirectionFacing = Randomization.NextDirection(); + // Set pause after turning + _lastMove = Timing.Global.MillisecondsUtc + mAttribute.Frequency + Globals.Random.Next((int)(mAttribute.Frequency * .5f)); + break; + } + + return true; } private void MoveRandomly() { - DirectionMoving = Randomization.NextDirection(); - var tmpX = (sbyte)X; - var tmpY = (sbyte)Y; - IEntity? blockedBy = null; - + // Don't start a new step if currently moving between tiles if (IsMoving || MoveTimer >= Timing.Global.MillisecondsUtc) { return; } + // No range left: pick a new direction and range + if (_randomMoveRange <= 0) + { + DirectionFacing = Randomization.NextDirection(); + _randomMoveRange = (byte)Randomization.Next(1, 5); + } + var deltaX = 0; var deltaY = 0; - - switch (DirectionMoving) + switch (DirectionFacing) { case Direction.Up: - deltaX = 0; deltaY = -1; break; case Direction.Down: - deltaX = 0; deltaY = 1; break; case Direction.Left: deltaX = -1; - deltaY = 0; break; case Direction.Right: deltaX = 1; - deltaY = 0; break; case Direction.UpLeft: @@ -132,59 +135,37 @@ private void MoveRandomly() break; } - if (deltaX != 0 || deltaY != 0) - { - var newX = tmpX + deltaX; - var newY = tmpY + deltaY; - var isBlocked = -1 == - IsTileBlocked( - new Point(newX, newY), - Z, - MapId, - ref blockedBy, - true, - true, - mAttribute.IgnoreNpcAvoids - ); - var playerOnTile = PlayerOnTile(MapId, newX, newY); - - if (isBlocked && newX >= 0 && newX < Options.Instance.Map.MapWidth && newY >= 0 && newY < Options.Instance.Map.MapHeight && - (!mAttribute.BlockPlayers || !playerOnTile)) - { - tmpX += (sbyte)deltaX; - tmpY += (sbyte)deltaY; - IsMoving = true; - DirectionFacing = DirectionMoving; + var newX = (sbyte)X + deltaX; + var newY = (sbyte)Y + deltaY; + IEntity? blockedBy = null; - if (deltaX == 0) - { - OffsetX = 0; - } - else - { - OffsetX = deltaX > 0 ? -Options.Instance.Map.TileWidth : Options.Instance.Map.TileWidth; - } + // Boundary checks + var isBlocked = -1 == IsTileBlocked(new Point(newX, newY), Z, MapId, ref blockedBy, true, true, mAttribute.IgnoreNpcAvoids); + var playerOnTile = PlayerOnTile(MapId, newX, newY); - if (deltaY == 0) - { - OffsetY = 0; - } - else - { - OffsetY = deltaY > 0 ? -Options.Instance.Map.TileHeight : Options.Instance.Map.TileHeight; - } - } - } - - if (IsMoving) + if (isBlocked && !playerOnTile && + newX >= 0 && newX < Options.Instance.Map.MapWidth && + newY >= 0 && newY < Options.Instance.Map.MapHeight) { - X = (byte)tmpX; - Y = (byte)tmpY; + X = (byte)newX; + Y = (byte)newY; + IsMoving = true; + OffsetX = deltaX == 0 ? 0 : (deltaX > 0 ? -Options.Instance.Map.TileWidth : Options.Instance.Map.TileWidth); + OffsetY = deltaY == 0 ? 0 : (deltaY > 0 ? -Options.Instance.Map.TileHeight : Options.Instance.Map.TileHeight); MoveTimer = Timing.Global.MillisecondsUtc + (long)GetMovementTime(); + _randomMoveRange--; + + // Critter's last step: set an idle pause timer + if (_randomMoveRange <= 0) + { + _lastMove = Timing.Global.MillisecondsUtc + mAttribute.Frequency + Globals.Random.Next((int)(mAttribute.Frequency * .5f)); + } } - else if (DirectionMoving != DirectionFacing) + else { - DirectionFacing = DirectionMoving; + // Blocked by something: end range early and trigger pause + _randomMoveRange = 0; + _lastMove = Timing.Global.MillisecondsUtc + mAttribute.Frequency; } } diff --git a/Intersect.Editor/Localization/Strings.cs b/Intersect.Editor/Localization/Strings.cs index 9321a0d29d..9c83dddd14 100644 --- a/Intersect.Editor/Localization/Strings.cs +++ b/Intersect.Editor/Localization/Strings.cs @@ -4582,6 +4582,10 @@ public partial struct NpcEditor {1, @"Turn Randomly"}, {2, @"Stand Still"}, {3, @"Static"}, + {4, @"Horizontal Patrol"}, + {5, @"Vertical Patrol"}, + {6, @"Backslash Patrol (\)"}, + {7, @"Forwardslash Patrol (/)"}, }; public static LocalizedString mpregen = @"MP (%):"; diff --git a/Intersect.Server.Core/Entities/Npc.cs b/Intersect.Server.Core/Entities/Npc.cs index 118855cdbb..c215337cdc 100644 --- a/Intersect.Server.Core/Entities/Npc.cs +++ b/Intersect.Server.Core/Entities/Npc.cs @@ -64,25 +64,23 @@ public Entity DamageMapHighest public bool Despawnable; //Moving - public long LastRandomMove; - private byte _randomMoveRange; + private long _lastMovement; + private byte _movementRange; + private int _patrolOriginX = -1; + private int _patrolOriginY = -1; + private bool _movingAwayFromPatrolOrigin; + private readonly Direction[] _lastFleeDirs = new Direction[3]; //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; @@ -107,6 +105,21 @@ public Entity DamageMapHighest /// The Z value on which this NPC was "aggro'd" and started chasing a target. /// public int AggroCenterZ; + + /// + /// The map on which this NPC originally spawned. + /// + private MapController? _spawnMap; + + /// + /// The X coordinate where this NPC originally spawned. + /// + private int _spawnX; + + /// + /// The Y coordinate where this NPC originally spawned. + /// + private int _spawnY; public Npc(NPCDescriptor npcDescriptor, bool despawnable = false) : base() { @@ -159,6 +172,8 @@ public Npc(NPCDescriptor npcDescriptor, bool despawnable = false) : base() private bool IsStunnedOrSleeping => CachedStatuses.Any(PredicateStunnedOrSleeping); private bool IsUnableToCastSpells => CachedStatuses.Any(PredicateUnableToCastSpells); + + private bool IsUnableToMove => CachedStatuses.Any(PredicateUnableToMove); public override EntityType GetEntityType() { @@ -170,11 +185,7 @@ public override void Die(bool generateLoot = true, Entity killer = null) lock (EntityLock) { base.Die(generateLoot, killer); - - AggroCenterMap = null; - AggroCenterX = 0; - AggroCenterY = 0; - AggroCenterZ = 0; + ResetAggroCenter(); if (MapController.TryGetInstanceFromMap(MapId, MapInstanceId, out var instance)) { @@ -335,13 +346,9 @@ public override bool CanAttack(Entity entity, SpellDescriptor spell) return false; } - //Check if the attacker is stunned or blinded. - foreach (var status in CachedStatuses) + if (IsStunnedOrSleeping) { - if (status.Type == SpellEffect.Stun || status.Type == SpellEffect.Sleep) - { - return false; - } + return false; } if (entity.HasStatusEffect(SpellEffect.Stealth)) @@ -538,6 +545,31 @@ private static bool PredicateUnableToCastSpells(Status status) } } + private static bool PredicateUnableToMove(Status status) + { + switch (status?.Type) + { + case SpellEffect.Stun: + case SpellEffect.Sleep: + case SpellEffect.Snare: + return true; + case SpellEffect.Silence: + case SpellEffect.None: + case SpellEffect.Blind: + case SpellEffect.Stealth: + case SpellEffect.Transform: + case SpellEffect.Cleanse: + case SpellEffect.Invulnerable: + case SpellEffect.Shield: + case SpellEffect.OnHit: + case SpellEffect.Taunt: + case null: + return false; + default: + throw new ArgumentOutOfRangeException(); + } + } + protected override bool IgnoresNpcAvoid => false; /// @@ -717,14 +749,14 @@ private void TryCastSpells() var dirToEnemy = DirectionToTarget(target); if (dirToEnemy != Dir) { - if (LastRandomMove >= Timing.Global.Milliseconds) + if (_lastMovement >= Timing.Global.Milliseconds) { return; } //Face the target -- next frame fire -- then go on with life ChangeDir(dirToEnemy); // Gotta get dir to enemy - LastRandomMove = Timing.Global.Milliseconds + Randomization.Next(1000, 3000); + _lastMovement = Timing.Global.Milliseconds + Randomization.Next(1000, 3000); return; } @@ -805,7 +837,110 @@ public bool IsFleeing() return false; } - // TODO: Improve NPC movement to be more fluid like a player + private bool TrySmartFlee(Entity? target) + { + if (target == null || mResetting || IsUnableToMove || IsStunnedOrSleeping || IsCasting) + { + return false; + } + + var bestDir = GetBestFleeDirection(target); + if (bestDir == Direction.None) + { + return false; + } + + ChangeDir(bestDir); + if (CanMoveInDirection(bestDir, out var blockerType, out _, out _) || blockerType == MovementBlockerType.Slide) + { + Move(bestDir, null); + ShiftFleeHistory(bestDir); + return true; + } + + return false; + } + + private Direction GetBestFleeDirection(Entity target) + { + var bestDirs = new List<(Direction dir, int score)>(); + var dirs = new[] + { + Direction.Up, Direction.Down, + Direction.Left, Direction.Right, + Direction.UpLeft, Direction.UpRight, + Direction.DownLeft, Direction.DownRight + }; + var tx = target.X; + var ty = target.Y; + var curDistSq = (X - tx) * (X - tx) + (Y - ty) * (Y - ty); + + foreach (var dir in dirs) + { + if (CanMoveInDirection(dir, out var blockerType2, out _, out _) || blockerType2 == MovementBlockerType.Slide) + { + var (dx, dy) = GetDirOffset(dir); + var nx = X + dx; + var ny = Y + dy; + var newDistSq = (nx - tx) * (nx - tx) + (ny - ty) * (ny - ty); + var delta = newDistSq - curDistSq; + if (delta <= 0) + { + continue; + } + + var score = delta * delta; + if (!IsDiagonal(dir)) + { + score += delta / 2; + } + + score += GetAntiLoopMultiplier(dir) * delta; + bestDirs.Add((dir, score)); + } + } + + if (bestDirs.Count == 0) + { + return Direction.None; + } + + var maxScore = bestDirs.Max(d => d.score); + var topDirs = bestDirs.Where(d => d.score >= maxScore - (maxScore / 20)).Select(d => d.dir).ToArray(); + return topDirs[Randomization.Next(topDirs.Length)]; + } + + private static (int dx, int dy) GetDirOffset(Direction dir) => dir switch + { + Direction.Up => (0, -1), + Direction.Down => (0, 1), + Direction.Left => (-1, 0), + Direction.Right => (1, 0), + Direction.UpLeft => (-1, -1), + Direction.UpRight => (1, -1), + Direction.DownLeft => (-1, 1), + Direction.DownRight => (1, 1), + _ => (0, 0) + }; + + private static bool IsDiagonal(Direction dir) => dir is Direction.UpLeft or Direction.UpRight or Direction.DownLeft or Direction.DownRight; + + private int GetAntiLoopMultiplier(Direction dir) + { + var mult = _lastFleeDirs.Where(t => t == dir).Aggregate(1, (current, t) => current - 1); + return Math.Max(mult, -2); + } + + private void ShiftFleeHistory(Direction dir) + { + for (var i = _lastFleeDirs.Length - 1; i > 0; i--) + { + _lastFleeDirs[i] = _lastFleeDirs[i - 1]; + } + + _lastFleeDirs[0] = dir; + } + //General Updating public override void Update(long timeMs) { @@ -815,17 +950,20 @@ public override void Update(long timeMs) Monitor.TryEnter(EntityLock, ref lockObtained); if (lockObtained) { + if (_spawnMap == null && MapController.TryGet(MapId, out var currentMap)) + { + _spawnMap = currentMap; + _spawnX = X; + _spawnY = Y; + } + var curMapLink = MapId; base.Update(timeMs); - var tempTarget = Target; - foreach (var status in CachedStatuses) + if (IsStunnedOrSleeping) { - if (status.Type is SpellEffect.Stun or SpellEffect.Sleep) - { - return; - } + return; } var fleeing = IsFleeing(); @@ -860,7 +998,7 @@ public override void Update(long timeMs) // Are we resetting? If so, regenerate completely! if (mResetting) { - var distance = GetDistanceTo(AggroCenterMap, AggroCenterX, AggroCenterY); + var distance = GetDistanceTo(_spawnMap, _spawnX, _spawnY); // Have we reached our destination? If so, clear it. if (distance < 1) { @@ -881,6 +1019,7 @@ public override void Update(long timeMs) mResetCounter++; if (mResetCounter > mResetMax) { + Warp(_spawnMap.Id, _spawnX, _spawnY); ResetAggroCenter(out targetMap); mResetCounter = 0; mResetDistance = 0; @@ -941,11 +1080,33 @@ public override void Update(long timeMs) if (mPathFinder.GetTarget() == null) { - mPathFinder.SetTarget(new PathfinderTarget(targetMap, targetX, targetY, targetZ)); + if (fleeing && tempTarget != null && !mResetting) + { + // Safe far flee spot + var fx = X; var fy = Y; + if (tempTarget.X < X) + { + fx = Math.Min(X + 5, Options.Instance.Map.MapWidth - 1); + } + else if (tempTarget.X > X) + { + fx = Math.Max(X - 5, 0); + } + + if (tempTarget.Y < Y) + { + fy = Math.Min(Y + 5, Options.Instance.Map.MapHeight - 1); + } + else if (tempTarget.Y > Y) + { + fy = Math.Max(Y - 5, 0); + } - if (tempTarget != null && tempTarget != Target) + mPathFinder.SetTarget(new PathfinderTarget(MapId, fx, fy, Z)); + } + else { - tempTarget = Target; + mPathFinder.SetTarget(new PathfinderTarget(targetMap, targetX, targetY, targetZ)); } } @@ -954,13 +1115,10 @@ public override void Update(long timeMs) if (mPathFinder.GetTarget() != null && Descriptor.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) - ) + )) || (mResetting && GetDistanceTo(_spawnMap, _spawnX, _spawnY) != 0)) { var pathFinderResult = mPathFinder.Update(timeMs); switch (pathFinderResult.Type) @@ -969,35 +1127,11 @@ public override void Update(long timeMs) var nextPathDirection = mPathFinder.GetMove(); if (nextPathDirection > Direction.None) { - if (fleeing) - { - nextPathDirection = nextPathDirection switch - { - Direction.Up => Direction.Down, - Direction.Down => Direction.Up, - Direction.Left => Direction.Right, - Direction.Right => Direction.Left, - Direction.UpLeft => Direction.UpRight, - Direction.UpRight => Direction.UpLeft, - Direction.DownRight => Direction.DownLeft, - Direction.DownLeft => Direction.DownRight, - _ => nextPathDirection, - }; - } - if (CanMoveInDirection(nextPathDirection, out var blockerType, out var blockingEntityType, out var blockingEntity) || blockerType == MovementBlockerType.Slide) { - //check if NPC is snared or stunned - // ReSharper disable once LoopCanBeConvertedToQuery - foreach (var status in CachedStatuses) + if (IsUnableToMove) { - // ReSharper disable once MergeIntoLogicalPattern - if (status.Type == SpellEffect.Stun || - status.Type == SpellEffect.Snare || - status.Type == SpellEffect.Sleep) - { - return; - } + return; } Move(nextPathDirection, null); @@ -1031,18 +1165,11 @@ public override void Update(long timeMs) // Are we resetting? if (mResetting) { - // Have we reached our destination? If so, clear it. - if (GetDistanceTo(AggroCenterMap, AggroCenterX, AggroCenterY) == 0) + // Have we reached our spawn destination? If so, clear it. + if (GetDistanceTo(_spawnMap, _spawnX, _spawnY) == 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; + ResetAggroCenter(); } } } @@ -1068,65 +1195,18 @@ public override void Update(long timeMs) default: throw new ArgumentOutOfRangeException(); } + + if (tempTarget == null) + { + SoftReset(); + } } else { var fleed = false; if (tempTarget != null && fleeing) { - var dir = DirectionToTarget(tempTarget); - switch (dir) - { - case Direction.Up: - dir = Direction.Down; - - break; - case Direction.Down: - dir = Direction.Up; - - break; - case Direction.Left: - dir = Direction.Right; - - break; - case Direction.Right: - dir = Direction.Left; - - break; - case Direction.UpLeft: - dir = Direction.UpRight; - - break; - case Direction.UpRight: - dir = Direction.UpLeft; - break; - - case Direction.DownRight: - dir = Direction.DownLeft; - - break; - case Direction.DownLeft: - dir = Direction.DownRight; - - break; - } - - if (CanMoveInDirection(dir, out var blockerType, out _) || blockerType == MovementBlockerType.Slide) - { - //check if NPC is snared or stunned - foreach (var status in CachedStatuses) - { - if (status.Type == SpellEffect.Stun || - status.Type == SpellEffect.Snare || - status.Type == SpellEffect.Sleep) - { - return; - } - } - - Move(dir, null); - fleed = true; - } + fleed = TrySmartFlee(tempTarget); } if (!fleed) @@ -1159,28 +1239,40 @@ public override void Update(long timeMs) CheckForResetLocation(); - if (targetMap != Guid.Empty || LastRandomMove >= Timing.Global.Milliseconds || IsCasting) + if (IsUnableToMove || targetMap != Guid.Empty || _lastMovement >= Timing.Global.Milliseconds || IsCasting) + { + return; + } + + if (Target != null || mResetting || fleeing) { return; } switch (Descriptor.Movement) { - case (int)NpcMovement.StandStill: - LastRandomMove = Timing.Global.Milliseconds + Randomization.Next(1000, 3000); + case (byte)NpcMovement.StandStill: + _lastMovement = Timing.Global.Milliseconds + Randomization.Next(1000, 3000); return; - case (int)NpcMovement.TurnRandomly: + case (byte)NpcMovement.TurnRandomly: ChangeDir(Randomization.NextDirection()); - LastRandomMove = Timing.Global.Milliseconds + Randomization.Next(1000, 3000); + _lastMovement = Timing.Global.Milliseconds + Randomization.Next(1000, 3000); return; - case (int)NpcMovement.MoveRandomly: + case (byte)NpcMovement.MoveRandomly: MoveRandomly(); break; - } - - if (fleeing) - { - LastRandomMove = Timing.Global.Milliseconds + (long)GetMovementTime(); + case (byte)NpcMovement.HorizontalPatrol: + HorizontalPatrol(); + break; + case (byte)NpcMovement.VerticalPatrol: + VerticalPatrol(); + break; + case (byte)NpcMovement.BackslashPatrol: + BackslashPatrol(); + break; + case (byte)NpcMovement.ForwardslashPatrol: + ForwardslashPatrol(); + break; } } @@ -1216,35 +1308,238 @@ public override void Update(long timeMs) private void MoveRandomly() { - if (_randomMoveRange <= 0) + var currentTime = Timing.Global.Milliseconds; + + // Pick a valid random direction and range + if (_movementRange <= 0) { - Dir = Randomization.NextDirection(); - LastRandomMove = Timing.Global.Milliseconds + Randomization.Next(1000, 2000); - _randomMoveRange = (byte)Randomization.Next(0, Descriptor.SightRange + Randomization.Next(0, 3)); + ChangeDir(FindValidRandomPath()); + _movementRange = (byte)Randomization.Next(0, Descriptor.SightRange + 1); } - else if (CanMoveInDirection(Dir)) + + // chance to change behavior while walking + if (_movementRange >= 1 && Randomization.Next(0, 100) < 35) { - foreach (var status in CachedStatuses) + if (Randomization.Next(0, 100) < 50) { - if (status.Type is SpellEffect.Stun or SpellEffect.Snare or SpellEffect.Sleep) - { - return; - } + // change to a random valid direction + ChangeDir(FindValidRandomPath()); + } + else + { + // stop and think: abandon path and trigger an idle pause + _movementRange = 0; + _lastMovement = currentTime + Randomization.Next(840, 1000); + return; } + } + // try to move in current direction + if (CanMoveInDirection(Dir) && !IsUnableToMove) + { Move(Dir, null); - LastRandomMove = Timing.Global.Milliseconds + (long)GetMovementTime(); + _movementRange--; + _lastMovement = _movementRange > 0 ? currentTime + (long)GetMovementTime() : currentTime + Randomization.Next(420, 840); + } + else + { + // Blocked: try valid alternative directions + var alternativeDir = FindValidRandomPath(); + if (alternativeDir != Direction.None) + { + Move(alternativeDir, null); + _movementRange--; + _lastMovement = currentTime + (long)GetMovementTime(); + } + else + { + // Completely stuck: pick new random direction and wait + ChangeDir(Randomization.NextDirection()); + _movementRange = 0; + _lastMovement = currentTime + 420; + } + } + } + + private Direction FindValidRandomPath() + { + var allDirections = Enum.GetValues(); + var maxDirs = Options.Instance.Map.EnableDiagonalMovement ? 8 : 4; + var start = Randomization.Next(maxDirs); - if (_randomMoveRange <= Randomization.Next(0, 3)) + for (int i = 0; i < maxDirs; i++) + { + var dir = allDirections[(start + i) % maxDirs]; + if (dir != Direction.None && CanMoveInDirection(dir)) { - Dir = Randomization.NextDirection(); + return dir; } + } + + return Direction.None; + } + + // Horizontal patrol (left/right from spawn) + private void HorizontalPatrol() + { + var currentTime = Timing.Global.Milliseconds; + + if (_patrolOriginX == -1) + { + _patrolOriginX = X; + _movingAwayFromPatrolOrigin = true; + } + + // Initialize direction + if (Dir != Direction.Left && Dir != Direction.Right) + { + Dir = Randomization.Next(0, 2) == 0 ? Direction.Left : Direction.Right; + } + + var distanceFromSpawn = Math.Abs(X - _patrolOriginX); + + switch (_movingAwayFromPatrolOrigin) + { + // Only reverse at endpoints + case true when distanceFromSpawn >= Descriptor.SightRange: + Dir = Dir == Direction.Left ? Direction.Right : Direction.Left; + _movingAwayFromPatrolOrigin = false; + break; + case false when distanceFromSpawn == 0: + _movingAwayFromPatrolOrigin = true; + break; + } + + if (CanMoveInDirection(Dir) && !IsUnableToMove) + { + Move(Dir, null); + _lastMovement = currentTime + (long)GetMovementTime(); + } + else + { + _lastMovement = currentTime + 420; + } + } + + // Vertical patrol (up/down from spawn) + private void VerticalPatrol() + { + var currentTime = Timing.Global.Milliseconds; + + if (_patrolOriginY == -1) + { + _patrolOriginY = Y; + _movingAwayFromPatrolOrigin = true; + } + + if (Dir != Direction.Up && Dir != Direction.Down) + { + Dir = Randomization.Next(0, 2) == 0 ? Direction.Up : Direction.Down; + } + + var distanceFromSpawn = Math.Abs(Y - _patrolOriginY); + + switch (_movingAwayFromPatrolOrigin) + { + case true when distanceFromSpawn >= Descriptor.SightRange: + Dir = Dir == Direction.Up ? Direction.Down : Direction.Up; + _movingAwayFromPatrolOrigin = false; + break; + case false when distanceFromSpawn == 0: + _movingAwayFromPatrolOrigin = true; + break; + } + + if (CanMoveInDirection(Dir) && !IsUnableToMove) + { + Move(Dir, null); + _lastMovement = currentTime + (long)GetMovementTime(); + } + else + { + _lastMovement = currentTime + 420; + } + } + + // Diagonal patrol (Backslash pattern: \) + private void BackslashPatrol() + { + var currentTime = Timing.Global.Milliseconds; + + if (_patrolOriginX == -1) + { + _patrolOriginX = X; + _patrolOriginY = Y; + _movingAwayFromPatrolOrigin = true; + } + + if (Dir != Direction.UpLeft && Dir != Direction.DownRight) + { + Dir = Randomization.Next(0, 2) == 0 ? Direction.UpLeft : Direction.DownRight; + } + + var distanceFromSpawn = Math.Max(Math.Abs(X - _patrolOriginX), Math.Abs(Y - _patrolOriginY)); + + switch (_movingAwayFromPatrolOrigin) + { + case true when distanceFromSpawn >= Descriptor.SightRange: + Dir = Dir == Direction.UpLeft ? Direction.DownRight : Direction.UpLeft; + _movingAwayFromPatrolOrigin = false; + break; + case false when X == _patrolOriginX && Y == _patrolOriginY: + _movingAwayFromPatrolOrigin = true; + break; + } + + if (CanMoveInDirection(Dir) && !IsUnableToMove) + { + Move(Dir, null); + _lastMovement = currentTime + (long)GetMovementTime(); + } + else + { + _lastMovement = currentTime + 420; + } + } + + // Diagonal patrol (Forwardslash pattern: /) + private void ForwardslashPatrol() + { + var currentTime = Timing.Global.Milliseconds; + + if (_patrolOriginX == -1) + { + _patrolOriginX = X; + _patrolOriginY = Y; + _movingAwayFromPatrolOrigin = true; + } + + if (Dir != Direction.UpRight && Dir != Direction.DownLeft) + { + Dir = Randomization.Next(0, 2) == 0 ? Direction.UpRight : Direction.DownLeft; + } + + var distanceFromSpawn = Math.Max(Math.Abs(X - _patrolOriginX), Math.Abs(Y - _patrolOriginY)); + + switch (_movingAwayFromPatrolOrigin) + { + case true when distanceFromSpawn >= Descriptor.SightRange: + Dir = Dir == Direction.UpRight ? Direction.DownLeft : Direction.UpRight; + _movingAwayFromPatrolOrigin = false; + break; + case false when X == _patrolOriginX && Y == _patrolOriginY: + _movingAwayFromPatrolOrigin = true; + break; + } - _randomMoveRange--; + if (CanMoveInDirection(Dir) && !IsUnableToMove) + { + Move(Dir, null); + _lastMovement = currentTime + (long)GetMovementTime(); } else { - Dir = Randomization.NextDirection(); + _lastMovement = currentTime + 420; } } @@ -1261,45 +1556,98 @@ private void ResetAggroCenter(out Guid targetMap) AggroCenterX = 0; AggroCenterY = 0; AggroCenterZ = 0; - mPathFinder?.SetTarget(null); + mPathFinder.SetTarget(null); + + // Reset patrol origin + _patrolOriginX = -1; + _patrolOriginY = -1; + _movingAwayFromPatrolOrigin = false; + mResetting = false; } + private void ResetAggroCenter() + { + _ = mPathFinder.GetTarget()?.TargetMapId ?? Guid.Empty; + ResetAggroCenter(out _); + } + 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.Instance.Npc.AllowResetRadius && AggroCenterMap != null && (GetDistanceTo(AggroCenterMap, AggroCenterX, AggroCenterY) > Math.Max(Options.Instance.Npc.ResetRadius, Math.Min(Descriptor.ResetRadius, Math.Max(Options.Instance.Map.MapWidth, Options.Instance.Map.MapHeight))) || forceDistance)) + if (Options.Instance.Npc.AllowResetRadius && _spawnMap != null && AggroCenterMap != null && (GetDistanceTo(AggroCenterMap, AggroCenterX, AggroCenterY) > Math.Max(Options.Instance.Npc.ResetRadius, Math.Min(Descriptor.ResetRadius, Math.Max(Options.Instance.Map.MapWidth, Options.Instance.Map.MapHeight))) || forceDistance)) { - Reset(Options.Instance.Npc.ResetVitalsAndStatuses); - + Reset(IsFleeing() || Options.Instance.Npc.ResetVitalsAndStatuses); mResetCounter = 0; mResetDistance = 0; + mPathFinder.SetTarget(null); - // 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)); + // If on different map when resetting: warp immediately + if (_spawnMap.Id != MapId) + { + mResetting = true; + Warp(_spawnMap.Id, _spawnX, _spawnY); + ResetAggroCenter(); + } + else + { + // Same map - set pathfinder to spawn point + mResetting = true; + mPathFinder.SetTarget(new PathfinderTarget(_spawnMap.Id, _spawnX, _spawnY, Z)); + } return true; } return false; } + private void SoftReset() + { + if (_spawnMap == null) + { + return; + } + + Reset(IsFleeing(), true); + mResetCounter = 0; + mResetDistance = 0; + mPathFinder.SetTarget(null); + + // Move back to where we SPAWNED (only for patrol movement patterns) + switch (Descriptor.Movement) + { + case (byte)NpcMovement.HorizontalPatrol: + case (byte)NpcMovement.VerticalPatrol: + case (byte)NpcMovement.BackslashPatrol: + case (byte)NpcMovement.ForwardslashPatrol: + // If on different map when resetting: warp immediately + if (_spawnMap.Id != MapId) + { + mResetting = true; + Warp(_spawnMap.Id, _spawnX, _spawnY); + ResetAggroCenter(); + } + else + { + // Same map - set pathfinder to spawn point + mResetting = true; + mPathFinder.SetTarget(new PathfinderTarget(_spawnMap.Id, _spawnX, _spawnY, Z)); + } + break; + } + } + private void Reset(bool resetVitals, 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; + ResetAggroCenter(); } // Reset our vitals and statusses when configured. @@ -1319,9 +1667,9 @@ private void Reset(bool resetVitals, bool clearLocation = false) // Completely resets an Npc to full health and its spawnpoint if it's current chasing something. public override void Reset() { - if (AggroCenterMap != null) + if (_spawnMap != null) { - Warp(AggroCenterMap.Id, AggroCenterX, AggroCenterY); + Warp(_spawnMap.Id, _spawnX, _spawnY); } Reset(true, true); diff --git a/Intersect.Server.Core/Entities/Player.cs b/Intersect.Server.Core/Entities/Player.cs index 54c789b863..0a3453fdda 100644 --- a/Intersect.Server.Core/Entities/Player.cs +++ b/Intersect.Server.Core/Entities/Player.cs @@ -1118,16 +1118,7 @@ public override void Die(bool dropItems = true, Entity killer = null) { CastTime = 0; CastTarget = null; - - //Flag death to the client - PacketSender.SendPlayerDeath(this); - - //Event trigger - foreach (var evt in EventLookup) - { - evt.Value.PlayerHasDied = true; - } - + // Remove player from ALL threat lists. foreach (var instance in MapController.GetSurroundingMapInstances(Map.Id, MapInstanceId, true)) { @@ -1135,17 +1126,26 @@ public override void Die(bool dropItems = true, Entity killer = null) { if (entity is Npc npc) { + npc.RemoveTarget(); npc.RemoveFromDamageMap(this); } } } + //Flag death to the client + PacketSender.SendPlayerDeath(this); + + //Event trigger + foreach (var evt in EventLookup) + { + evt.Value.PlayerHasDied = true; + } + lock (EntityLock) { base.Die(dropItems, killer); } - // EXP Loss - don't lose in shared instance, or in an Arena zone if (InstanceType != MapInstanceType.Shared || Options.Instance.Instancing.LoseExpOnInstanceDeath) {