Skip to content

Commit 14ef75d

Browse files
committed
update: NPC and Critter movement
Movement System Improvements: - Refactors how both, NPCs and Critters randomly move around, replacing the boring single-tile-paused movement with a more natural, range-based pathing system. - NPCs now intelligently navigate around obstacles and other entities - Collision detection overhead with optimized valid direction finding New NPC Patrol Patterns: - Horizontal patrol (left/right from spawn) - Vertical patrol (up/down from spawn) - Diagonal backslash patrol (UpLeft ↔ DownRight) - Diagonal forwardslash patrol (UpRight ↔ DownLeft) All patrol modes use NPC sight range as patrol distance and track spawn position to prevent drift.
1 parent dcbd209 commit 14ef75d

4 files changed

Lines changed: 331 additions & 97 deletions

File tree

Intersect (Core)/Enums/NpcMovement.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,12 @@ public enum NpcMovement
99
StandStill,
1010

1111
Static,
12+
13+
HorizontalPatrol,
14+
15+
VerticalPatrol,
16+
17+
BackslashPatrol,
18+
19+
ForwardslashPatrol,
1220
}

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.Editor/Localization/Strings.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4582,6 +4582,10 @@ public partial struct NpcEditor
45824582
{1, @"Turn Randomly"},
45834583
{2, @"Stand Still"},
45844584
{3, @"Static"},
4585+
{4, @"Horizontal Patrol"},
4586+
{5, @"Vertical Patrol"},
4587+
{6, @"Backslash Patrol (\)"},
4588+
{7, @"Forwardslash Patrol (/)"},
45854589
};
45864590

45874591
public static LocalizedString mpregen = @"MP (%):";

0 commit comments

Comments
 (0)