Skip to content

Commit 73b4af5

Browse files
committed
update: NPC and Critter movement
NPC and Critter movement: - Refactors how both, NPCs and Critters randomly move around, replacing the boring single-tile-paused movement with a more natural, range-based path logic. - NPCs now intelligently navigate around obstacles and other entities - Collision detection overhead with optimized valid direction finding - Prevents movement patterns from messing with current path finder's target - Code chore: cached status check consolidation - Code chore: updated naming conventions for movement variables - Code chore: TODOs - soft reset (walk) to spawn point and hard reset (warp) when pathfinder is unable to lead back to spawnpoint (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) - Patrolling NPCs return to their spawn/patrol point when there's no target left to smack or out of former map All patrol modes use NPC sight range as patrol distance and keep track of their spawn position for reseting purposes.
1 parent 4891f3c commit 73b4af5

5 files changed

Lines changed: 594 additions & 253 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)