NPC wander fix

This commit is contained in:
dam 2026-05-09 16:11:11 +03:00
parent fad4726760
commit 4328d591f5
No known key found for this signature in database
GPG key ID: 4AF4E722399663FB
2 changed files with 152 additions and 14 deletions

View file

@ -21,8 +21,11 @@ import content.global.skill.slayer.Tasks;
import content.global.skill.summoning.familiar.Familiar;
import core.game.world.map.Direction;
import core.game.world.map.Location;
import core.game.world.map.Point;
import core.game.world.map.RegionManager;
import core.game.world.map.build.DynamicRegion;
import core.game.world.map.path.ClipMaskSupplier;
import core.game.world.map.path.Path;
import core.game.world.map.path.Pathfinder;
import core.game.world.update.flag.context.Animation;
import core.game.world.update.flag.context.Graphics;
@ -466,25 +469,22 @@ public class NPC extends Entity {
return;
if (!getLocks().isInteractionLocked()) {
if (!getLocks().isMovementLocked()) {
int effectiveWalkRadius = getWalkRadius();
if (
!pathBoundMovement
&& walkRadius > 0
&& walkRadius <= 20
&& !getLocation().withinDistance(getProperties().getSpawnLocation(), (int)(walkRadius * 1.5))
&& effectiveWalkRadius > 0
&& effectiveWalkRadius <= 20
&& !getLocation().withinDistance(getProperties().getSpawnLocation(), getSpawnReturnDistance(effectiveWalkRadius))
&& !getAttribute("no-spawn-return", false)
)
{
MovementPulse current = getAttribute("return-to-spawn-pulse");
if (current != null && current.isRunning()) return;
if(!isNeverWalks()){
if(walkRadius == 0)
walkRadius = 3;
}
if (aggressiveHandler != null) {
aggressiveHandler.setPauseTicks(walkRadius + 1);
aggressiveHandler.setPauseTicks(effectiveWalkRadius + 1);
}
nextWalk = GameWorld.getTicks() + walkRadius + 1;
nextWalk = GameWorld.getTicks() + effectiveWalkRadius + 1;
getLocks().lockMovement(100);
getImpactHandler().setDisabledTicks(100);
setAttribute("return-to-spawn", true);
@ -530,15 +530,84 @@ public class NPC extends Entity {
setNextWalk();
Location l = getMovementDestination();
if (canMove(l)) {
if((Boolean) definition.getHandlers().getOrDefault("water_npc",false)){
Pathfinder.findWater(this,l,true,Pathfinder.DUMB).walk(this);
} else {
Pathfinder.find(this, l, true, Pathfinder.DUMB).walk(this);
}
Path path = pathBoundMovement ? findMovementPath(l) : findRandomMovementPath(l);
path.walk(this);
}
return false;
}
private Path findMovementPath(Location destination) {
if (isWaterNPC()) {
return Pathfinder.findWater(this, destination, true, Pathfinder.DUMB);
}
return Pathfinder.find(this, destination, true, Pathfinder.DUMB);
}
private Path findRandomMovementPath(Location destination) {
if (isWaterNPC() || size() != 1) {
Path path = findMovementPath(destination);
return isRandomMovementPathWithinBounds(path) ? path : new Path();
}
return findDirectRandomMovementPath(destination);
}
private Path findDirectRandomMovementPath(Location destination) {
Path path = new Path();
path.setSuccesful(true);
Location current = getLocation();
int maxSteps = Math.min(14, Math.max(1, getSpawnReturnDistance(Math.max(1, getWalkRadius()))));
for (int steps = 0; !current.equals(destination) && steps < maxSteps; steps++) {
Direction direction = Direction.getDirection(current, destination);
if (direction == null || !canTakeRandomMovementStep(current, direction)) {
path.setSuccesful(false);
path.setMoveNear(!path.getPoints().isEmpty());
break;
}
Location next = current.transform(direction);
if (!isWithinRandomMovementBounds(next)) {
path.setSuccesful(false);
path.setMoveNear(!path.getPoints().isEmpty());
break;
}
path.getPoints().add(new Point(next.getX(), next.getY(), direction, direction.getStepX(), direction.getStepY()));
current = next;
}
if (!current.equals(destination) && !path.getPoints().isEmpty()) {
path.setMoveNear(true);
}
return path;
}
private boolean canTakeRandomMovementStep(Location current, Direction direction) {
ClipMaskSupplier clipMaskSupplier = behavior != null ? behavior.getClippingSupplier(this) : null;
if (clipMaskSupplier == null) {
clipMaskSupplier = RegionManager::getClippingFlag;
}
return direction.canMoveFrom(current.getZ(), current.getX(), current.getY(), clipMaskSupplier);
}
private boolean isRandomMovementPathWithinBounds(Path path) {
for (Point point : path.getPoints()) {
if (!isWithinRandomMovementBounds(Location.create(point.getX(), point.getY(), getLocation().getZ()))) {
return false;
}
}
return true;
}
private boolean isWithinRandomMovementBounds(Location location) {
int walkRadius = getWalkRadius();
return walkRadius <= 0 || location.withinDistance(getProperties().getSpawnLocation(), getSpawnReturnDistance(walkRadius));
}
private int getSpawnReturnDistance(int walkRadius) {
return (int) (walkRadius * 1.5);
}
private boolean isWaterNPC() {
return (Boolean) definition.getHandlers().getOrDefault("water_npc", false);
}
public int getNextWalk() {
return nextWalk;
}

View file

@ -584,6 +584,56 @@ class PathfinderTests {
}
}
@Test fun npcReturnToSpawnShouldUseOverriddenWalkRadius() {
TestUtils.getMockPlayer("overriddenRadiusReturn").use {
val spawn = ServerConstants.HOME_LOCATION!!
val npc = object : NPC(1, spawn.transform(5, 0, 0)) {
override fun getWalkRadius(): Int {
return 3
}
}
npc.isWalks = true
npc.isNeverWalks = false
npc.init()
npc.properties.spawnLocation = spawn
try {
npc.handleTickActions()
Assertions.assertEquals(true, npc.getAttribute("return-to-spawn", false))
} finally {
npc.clear()
}
}
}
@Test fun randomWalkingNpcShouldNotRouteAroundBlockedLocalDestination() {
val origin = Location.create(3200, 3600, 0)
val blocked = origin.transform(1, 0, 0)
val destination = origin.transform(2, 0, 0)
val npc = FixedDestinationNPC(origin, destination, 3)
npc.isWalks = true
npc.isNeverWalks = false
npc.init()
npc.properties.spawnLocation = origin
RegionManager.addClippingFlag(blocked.z, blocked.x, blocked.y, false, movementBlockFlag)
try {
npc.resetWalk()
repeat(20) {
npc.handleTickActions()
}
Assertions.assertFalse(
npc.walkingQueue.hasPath(),
"Random-walking NPCs should not take an RSMOD detour around a clipped boundary tile."
)
npc.walkingQueue.update()
Assertions.assertEquals(origin, npc.location)
} finally {
RegionManager.removeClippingFlag(blocked.z, blocked.x, blocked.y, false, movementBlockFlag)
npc.clear()
}
}
@Test fun npcShouldReliablyReturnToSpawnEvenIfRegionUnloaded() {
//spawn a player into the area just to make sure it ticks...
TestUtils.getMockPlayer("areaunloadtest").use { p ->
@ -602,4 +652,23 @@ class PathfinderTests {
Assertions.assertEquals(true, npc.location.getDistance(ServerConstants.HOME_LOCATION!!) <= 5)
}
}
private class FixedDestinationNPC(
location: Location,
private val destination: Location,
private val radius: Int
) : NPC(1, location) {
override fun getMovementDestination(): Location {
return destination
}
override fun getWalkRadius(): Int {
return radius
}
}
private val movementBlockFlag = Pathfinder.PREVENT_NORTH or
Pathfinder.PREVENT_EAST or
Pathfinder.PREVENT_SOUTH or
Pathfinder.PREVENT_WEST
}