Skip to content

Commit 46a13da

Browse files
committed
enhancement: smoother NPC and Critter random movement
This update refactors how both, NPCs and Critters randomly move around, replacing the boring single-tile shuffling-around with a more natural, range-based pathing system.
1 parent dcbd209 commit 46a13da

2 files changed

Lines changed: 155 additions & 92 deletions

File tree

Intersect.Client.Core/Entities/Critter.cs

Lines changed: 57 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ namespace Intersect.Client.Entities;
1515
public partial class Critter : Entity
1616
{
1717
private readonly MapCritterAttribute mAttribute;
18-
private long mLastMove = -1;
18+
19+
// Critter's Movement
20+
private long _lastMove = -1;
21+
private byte _randomMoveRange;
1922

2023
public Critter(MapInstance map, byte x, byte y, MapCritterAttribute att) : base(Guid.NewGuid(), null, EntityType.GlobalEntity)
2124
{
@@ -50,65 +53,65 @@ public Critter(MapInstance map, byte x, byte y, MapCritterAttribute att) : base(
5053

5154
public override bool Update()
5255
{
53-
if (base.Update())
56+
if (!base.Update())
5457
{
55-
if (mLastMove < Timing.Global.MillisecondsUtc)
56-
{
57-
switch (mAttribute.Movement)
58-
{
59-
case 0: //Move Randomly
60-
MoveRandomly();
61-
break;
62-
case 1: //Turn?
63-
DirectionFacing = Randomization.NextDirection();
64-
break;
65-
66-
}
67-
68-
mLastMove = Timing.Global.MillisecondsUtc + mAttribute.Frequency + Globals.Random.Next((int)(mAttribute.Frequency * .5f));
69-
}
58+
return false;
59+
}
7060

61+
// Only skip if we are NOT in the middle of a range-walk AND the frequency timer is active
62+
if (_randomMoveRange <= 0 && _lastMove >= Timing.Global.MillisecondsUtc)
63+
{
7164
return true;
7265
}
7366

74-
return false;
67+
switch (mAttribute.Movement)
68+
{
69+
case 0: // Move Randomly
70+
MoveRandomly();
71+
break;
72+
case 1: // Turn Randomly
73+
DirectionFacing = Randomization.NextDirection();
74+
// Set pause after turning
75+
_lastMove = Timing.Global.MillisecondsUtc + mAttribute.Frequency + Globals.Random.Next((int)(mAttribute.Frequency * .5f));
76+
break;
77+
}
78+
79+
return true;
7580
}
7681

7782
private void MoveRandomly()
7883
{
79-
DirectionMoving = Randomization.NextDirection();
80-
var tmpX = (sbyte)X;
81-
var tmpY = (sbyte)Y;
82-
IEntity? blockedBy = null;
83-
84+
// Don't start a new step if currently moving between tiles
8485
if (IsMoving || MoveTimer >= Timing.Global.MillisecondsUtc)
8586
{
8687
return;
8788
}
8889

90+
// No range left: pick a new direction and range
91+
if (_randomMoveRange <= 0)
92+
{
93+
DirectionFacing = Randomization.NextDirection();
94+
_randomMoveRange = (byte)Randomization.Next(1, 5);
95+
}
96+
8997
var deltaX = 0;
9098
var deltaY = 0;
91-
92-
switch (DirectionMoving)
99+
switch (DirectionFacing)
93100
{
94101
case Direction.Up:
95-
deltaX = 0;
96102
deltaY = -1;
97103
break;
98104

99105
case Direction.Down:
100-
deltaX = 0;
101106
deltaY = 1;
102107
break;
103108

104109
case Direction.Left:
105110
deltaX = -1;
106-
deltaY = 0;
107111
break;
108112

109113
case Direction.Right:
110114
deltaX = 1;
111-
deltaY = 0;
112115
break;
113116

114117
case Direction.UpLeft:
@@ -132,59 +135,37 @@ private void MoveRandomly()
132135
break;
133136
}
134137

135-
if (deltaX != 0 || deltaY != 0)
136-
{
137-
var newX = tmpX + deltaX;
138-
var newY = tmpY + deltaY;
139-
var isBlocked = -1 ==
140-
IsTileBlocked(
141-
new Point(newX, newY),
142-
Z,
143-
MapId,
144-
ref blockedBy,
145-
true,
146-
true,
147-
mAttribute.IgnoreNpcAvoids
148-
);
149-
var playerOnTile = PlayerOnTile(MapId, newX, newY);
150-
151-
if (isBlocked && newX >= 0 && newX < Options.Instance.Map.MapWidth && newY >= 0 && newY < Options.Instance.Map.MapHeight &&
152-
(!mAttribute.BlockPlayers || !playerOnTile))
153-
{
154-
tmpX += (sbyte)deltaX;
155-
tmpY += (sbyte)deltaY;
156-
IsMoving = true;
157-
DirectionFacing = DirectionMoving;
138+
var newX = (sbyte)X + deltaX;
139+
var newY = (sbyte)Y + deltaY;
140+
IEntity? blockedBy = null;
158141

159-
if (deltaX == 0)
160-
{
161-
OffsetX = 0;
162-
}
163-
else
164-
{
165-
OffsetX = deltaX > 0 ? -Options.Instance.Map.TileWidth : Options.Instance.Map.TileWidth;
166-
}
142+
// Boundary checks
143+
var isBlocked = -1 == IsTileBlocked(new Point(newX, newY), Z, MapId, ref blockedBy, true, true, mAttribute.IgnoreNpcAvoids);
144+
var playerOnTile = PlayerOnTile(MapId, newX, newY);
167145

168-
if (deltaY == 0)
169-
{
170-
OffsetY = 0;
171-
}
172-
else
173-
{
174-
OffsetY = deltaY > 0 ? -Options.Instance.Map.TileHeight : Options.Instance.Map.TileHeight;
175-
}
176-
}
177-
}
178-
179-
if (IsMoving)
146+
if (isBlocked && !playerOnTile &&
147+
newX >= 0 && newX < Options.Instance.Map.MapWidth &&
148+
newY >= 0 && newY < Options.Instance.Map.MapHeight)
180149
{
181-
X = (byte)tmpX;
182-
Y = (byte)tmpY;
150+
X = (byte)newX;
151+
Y = (byte)newY;
152+
IsMoving = true;
153+
OffsetX = deltaX == 0 ? 0 : (deltaX > 0 ? -Options.Instance.Map.TileWidth : Options.Instance.Map.TileWidth);
154+
OffsetY = deltaY == 0 ? 0 : (deltaY > 0 ? -Options.Instance.Map.TileHeight : Options.Instance.Map.TileHeight);
183155
MoveTimer = Timing.Global.MillisecondsUtc + (long)GetMovementTime();
156+
_randomMoveRange--;
157+
158+
// Critter's last step: set an idle pause timer
159+
if (_randomMoveRange <= 0)
160+
{
161+
_lastMove = Timing.Global.MillisecondsUtc + mAttribute.Frequency + Globals.Random.Next((int)(mAttribute.Frequency * .5f));
162+
}
184163
}
185-
else if (DirectionMoving != DirectionFacing)
164+
else
186165
{
187-
DirectionFacing = DirectionMoving;
166+
// Blocked by something: end range early and trigger pause
167+
_randomMoveRange = 0;
168+
_lastMove = Timing.Global.MillisecondsUtc + mAttribute.Frequency;
188169
}
189170
}
190171

Intersect.Server.Core/Entities/Npc.cs

Lines changed: 98 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,8 @@ public Npc(NPCDescriptor npcDescriptor, bool despawnable = false) : base()
159159
private bool IsStunnedOrSleeping => CachedStatuses.Any(PredicateStunnedOrSleeping);
160160

161161
private bool IsUnableToCastSpells => CachedStatuses.Any(PredicateUnableToCastSpells);
162+
163+
private bool IsUnableToMove => CachedStatuses.Any(PredicateUnableToMove);
162164

163165
public override EntityType GetEntityType()
164166
{
@@ -538,6 +540,31 @@ private static bool PredicateUnableToCastSpells(Status status)
538540
}
539541
}
540542

543+
private static bool PredicateUnableToMove(Status status)
544+
{
545+
switch (status?.Type)
546+
{
547+
case SpellEffect.Stun:
548+
case SpellEffect.Sleep:
549+
case SpellEffect.Snare:
550+
return true;
551+
case SpellEffect.Silence:
552+
case SpellEffect.None:
553+
case SpellEffect.Blind:
554+
case SpellEffect.Stealth:
555+
case SpellEffect.Transform:
556+
case SpellEffect.Cleanse:
557+
case SpellEffect.Invulnerable:
558+
case SpellEffect.Shield:
559+
case SpellEffect.OnHit:
560+
case SpellEffect.Taunt:
561+
case null:
562+
return false;
563+
default:
564+
throw new ArgumentOutOfRangeException();
565+
}
566+
}
567+
541568
protected override bool IgnoresNpcAvoid => false;
542569

543570
/// <inheritdoc />
@@ -1159,7 +1186,7 @@ public override void Update(long timeMs)
11591186

11601187
CheckForResetLocation();
11611188

1162-
if (targetMap != Guid.Empty || LastRandomMove >= Timing.Global.Milliseconds || IsCasting)
1189+
if (IsUnableToMove || targetMap != Guid.Empty || LastRandomMove >= Timing.Global.Milliseconds || IsCasting)
11631190
{
11641191
return;
11651192
}
@@ -1216,36 +1243,91 @@ public override void Update(long timeMs)
12161243

12171244
private void MoveRandomly()
12181245
{
1246+
var currentTime = Timing.Global.Milliseconds;
1247+
1248+
// Pick new direction and range
12191249
if (_randomMoveRange <= 0)
12201250
{
1221-
Dir = Randomization.NextDirection();
1222-
LastRandomMove = Timing.Global.Milliseconds + Randomization.Next(1000, 2000);
1223-
_randomMoveRange = (byte)Randomization.Next(0, Descriptor.SightRange + Randomization.Next(0, 3));
1251+
Dir = FindValidDirectionOrRandom();
1252+
_randomMoveRange = (byte)Randomization.Next(0, Descriptor.SightRange + 1);
12241253
}
1225-
else if (CanMoveInDirection(Dir))
1254+
1255+
// Mid-path deviation: 35% chance to change behavior while walking
1256+
if (_randomMoveRange > 1 && Randomization.Next(0, 100) < 35)
12261257
{
1227-
foreach (var status in CachedStatuses)
1258+
if (Randomization.Next(0, 100) < 50)
12281259
{
1229-
if (status.Type is SpellEffect.Stun or SpellEffect.Snare or SpellEffect.Sleep)
1230-
{
1231-
return;
1232-
}
1260+
// Pivot: change to a valid direction
1261+
Dir = FindValidDirectionOrRandom();
12331262
}
1263+
else
1264+
{
1265+
// Stop and think: abandon path and trigger an idle pause
1266+
_randomMoveRange = 0;
1267+
LastRandomMove = currentTime + Randomization.Next(840, 1000);
1268+
return;
1269+
}
1270+
}
12341271

1272+
// Check if path is clear
1273+
if (CanMoveInDirection(Dir) && !IsUnableToMove)
1274+
{
12351275
Move(Dir, null);
1236-
LastRandomMove = Timing.Global.Milliseconds + (long)GetMovementTime();
1276+
_randomMoveRange--;
12371277

1238-
if (_randomMoveRange <= Randomization.Next(0, 3))
1278+
LastRandomMove = _randomMoveRange > 0
1279+
? currentTime + (long)GetMovementTime()
1280+
: currentTime + Randomization.Next(420, 840);
1281+
}
1282+
else
1283+
{
1284+
// Blocked: try to find alternative direction immediately
1285+
var alternativeDir = FindValidDirection();
1286+
if (alternativeDir != Direction.None)
1287+
{
1288+
Dir = alternativeDir;
1289+
Move(Dir, null);
1290+
_randomMoveRange--;
1291+
LastRandomMove = currentTime + (long)GetMovementTime();
1292+
}
1293+
else
12391294
{
1240-
Dir = Randomization.NextDirection();
1295+
// Completely blocked: clear range and wait
1296+
_randomMoveRange = 0;
1297+
LastRandomMove = currentTime + 420;
12411298
}
1299+
}
1300+
}
12421301

1243-
_randomMoveRange--;
1302+
// Finds a valid unblocked direction, or returns Direction.None if all blocked
1303+
private Direction FindValidDirection()
1304+
{
1305+
// Get all directions from Intersect's Direction enum
1306+
var directions = Enum.GetValues<Direction>().Where(d => d != Direction.None).ToArray();
1307+
1308+
// Shuffle for randomization
1309+
for (int i = directions.Length - 1; i > 0; i--)
1310+
{
1311+
int j = Randomization.Next(0, i + 1);
1312+
(directions[i], directions[j]) = (directions[j], directions[i]);
12441313
}
1245-
else
1314+
1315+
foreach (var dir in directions)
12461316
{
1247-
Dir = Randomization.NextDirection();
1317+
if (CanMoveInDirection(dir))
1318+
{
1319+
return dir;
1320+
}
12481321
}
1322+
1323+
return Direction.None;
1324+
}
1325+
1326+
// Finds a valid direction, falls back to random if all blocked
1327+
private Direction FindValidDirectionOrRandom()
1328+
{
1329+
var validDir = FindValidDirection();
1330+
return validDir != Direction.None ? validDir : Randomization.NextDirection();
12491331
}
12501332

12511333
/// <summary>

0 commit comments

Comments
 (0)