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.Server.Core/Entities/Npc.cs b/Intersect.Server.Core/Entities/Npc.cs
index 118855cdbb..55e121e5f5 100644
--- a/Intersect.Server.Core/Entities/Npc.cs
+++ b/Intersect.Server.Core/Entities/Npc.cs
@@ -159,6 +159,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()
{
@@ -538,6 +540,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;
///
@@ -1159,7 +1186,7 @@ public override void Update(long timeMs)
CheckForResetLocation();
- if (targetMap != Guid.Empty || LastRandomMove >= Timing.Global.Milliseconds || IsCasting)
+ if (IsUnableToMove || targetMap != Guid.Empty || LastRandomMove >= Timing.Global.Milliseconds || IsCasting)
{
return;
}
@@ -1216,36 +1243,91 @@ public override void Update(long timeMs)
private void MoveRandomly()
{
+ var currentTime = Timing.Global.Milliseconds;
+
+ // Pick new direction and range
if (_randomMoveRange <= 0)
{
- Dir = Randomization.NextDirection();
- LastRandomMove = Timing.Global.Milliseconds + Randomization.Next(1000, 2000);
- _randomMoveRange = (byte)Randomization.Next(0, Descriptor.SightRange + Randomization.Next(0, 3));
+ Dir = FindValidDirectionOrRandom();
+ _randomMoveRange = (byte)Randomization.Next(0, Descriptor.SightRange + 1);
}
- else if (CanMoveInDirection(Dir))
+
+ // Mid-path deviation: 35% chance to change behavior while walking
+ if (_randomMoveRange > 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;
- }
+ // Pivot: change to a valid direction
+ Dir = FindValidDirectionOrRandom();
}
+ else
+ {
+ // Stop and think: abandon path and trigger an idle pause
+ _randomMoveRange = 0;
+ LastRandomMove = currentTime + Randomization.Next(840, 1000);
+ return;
+ }
+ }
+ // Check if path is clear
+ if (CanMoveInDirection(Dir) && !IsUnableToMove)
+ {
Move(Dir, null);
- LastRandomMove = Timing.Global.Milliseconds + (long)GetMovementTime();
+ _randomMoveRange--;
- if (_randomMoveRange <= Randomization.Next(0, 3))
+ LastRandomMove = _randomMoveRange > 0
+ ? currentTime + (long)GetMovementTime()
+ : currentTime + Randomization.Next(420, 840);
+ }
+ else
+ {
+ // Blocked: try to find alternative direction immediately
+ var alternativeDir = FindValidDirection();
+ if (alternativeDir != Direction.None)
+ {
+ Dir = alternativeDir;
+ Move(Dir, null);
+ _randomMoveRange--;
+ LastRandomMove = currentTime + (long)GetMovementTime();
+ }
+ else
{
- Dir = Randomization.NextDirection();
+ // Completely blocked: clear range and wait
+ _randomMoveRange = 0;
+ LastRandomMove = currentTime + 420;
}
+ }
+ }
- _randomMoveRange--;
+ // Finds a valid unblocked direction, or returns Direction.None if all blocked
+ private Direction FindValidDirection()
+ {
+ // Get all directions from Intersect's Direction enum
+ var directions = Enum.GetValues().Where(d => d != Direction.None).ToArray();
+
+ // Shuffle for randomization
+ for (int i = directions.Length - 1; i > 0; i--)
+ {
+ int j = Randomization.Next(0, i + 1);
+ (directions[i], directions[j]) = (directions[j], directions[i]);
}
- else
+
+ foreach (var dir in directions)
{
- Dir = Randomization.NextDirection();
+ if (CanMoveInDirection(dir))
+ {
+ return dir;
+ }
}
+
+ return Direction.None;
+ }
+
+ // Finds a valid direction, falls back to random if all blocked
+ private Direction FindValidDirectionOrRandom()
+ {
+ var validDir = FindValidDirection();
+ return validDir != Direction.None ? validDir : Randomization.NextDirection();
}
///