Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 57 additions & 76 deletions Intersect.Client.Core/Entities/Critter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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:
Expand All @@ -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;
}
}

Expand Down
114 changes: 98 additions & 16 deletions Intersect.Server.Core/Entities/Npc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down Expand Up @@ -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;

/// <inheritdoc />
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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<Direction>().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();
}

/// <summary>
Expand Down
Loading