fix(npc): roll while walking, and refuse an out-of-box destination #238

Merged
sickday merged 3 commits from fix/npc-wander-ratchet into main 2026-08-19 01:47:29 +00:00
Owner

NPC wander composed two correct rules into a ratchet. hold/1 keeps the
destination when a creature is in the way, and roam/1 refused to roll
while it had one -- the client's own pathLength == 0 guard. Together,
two NPCs whose next steps cross can neither move nor choose anywhere
else; nothing clears it, so every jam is permanent and the population
accumulates into knots rather than settling into a wrong distribution.

Measured over the whole spawn table, 16,942 roamers against real
collision and real entity flags, the share of NPC ticks lost to a
creature in the way ran 0.6% -> 8.2% -> 37.2% -> 72.3% at ticks 1, 10,
100 and 1,000, and was still climbing. Mean other NPCs within three
tiles went 1.43 -> 2.44 over the same run, with the share of NPCs in a
knot of four or more going 20% -> 38.7%. That is the clustering an
observer sees, and it has nothing to do with pathfinding.

The roll now fires whether or not the NPC is already walking. The source
states the client guard and then contradicts it for the server, from the
game: NPCs move in one direction and then suddenly the opposite one, so
wandering may occur even with movement already queued. A roll that names
nowhere legal leaves the journey alone -- it named nowhere, which is not
the same as naming here.

The leash now refuses an out-of-box destination where it clamped one to
the box edge. The clamp had no source: the client's clamp is against the
map edge, and the wander box is a server concept the client does not
have. It loaded the boundary -- over 200,000 ticks a clamped walk spent
9.6% of its life on the four corner tiles, which are 3.3% of the box.
Refusing is the rule the module already applied to a zero offset.

Refusal needs one companion, because a stray can no longer be clamped
home: an NPC outside its box walks back to its spawn instead of rolling.
Only a fight can put it there, and World.NpcCombat already does this
for a creature that breaks its leash.

After: held flat at ~5% from tick 25 onward, crowding flat at 1.33 and
17.9% -- below what the spawn table itself starts at -- corner occupancy
0.68% -> 0.27% per tile, and the median NPC's distinct tiles visited
29 -> 63.

Three new tests, each checked to fail against the old behaviour.

NPC wander composed two correct rules into a ratchet. `hold/1` keeps the destination when a creature is in the way, and `roam/1` refused to roll while it had one -- the client's own `pathLength == 0` guard. Together, two NPCs whose next steps cross can neither move nor choose anywhere else; nothing clears it, so every jam is permanent and the population accumulates into knots rather than settling into a wrong distribution. Measured over the whole spawn table, 16,942 roamers against real collision and real entity flags, the share of NPC ticks lost to a creature in the way ran 0.6% -> 8.2% -> 37.2% -> 72.3% at ticks 1, 10, 100 and 1,000, and was still climbing. Mean other NPCs within three tiles went 1.43 -> 2.44 over the same run, with the share of NPCs in a knot of four or more going 20% -> 38.7%. That is the clustering an observer sees, and it has nothing to do with pathfinding. The roll now fires whether or not the NPC is already walking. The source states the client guard and then contradicts it for the server, from the game: NPCs move in one direction and then suddenly the opposite one, so wandering may occur even with movement already queued. A roll that names nowhere legal leaves the journey alone -- it named nowhere, which is not the same as naming here. The leash now refuses an out-of-box destination where it clamped one to the box edge. The clamp had no source: the client's clamp is against the map edge, and the wander box is a server concept the client does not have. It loaded the boundary -- over 200,000 ticks a clamped walk spent 9.6% of its life on the four corner tiles, which are 3.3% of the box. Refusing is the rule the module already applied to a zero offset. Refusal needs one companion, because a stray can no longer be clamped home: an NPC outside its box walks back to its spawn instead of rolling. Only a fight can put it there, and `World.NpcCombat` already does this for a creature that breaks its leash. After: held flat at ~5% from tick 25 onward, crowding flat at 1.33 and 17.9% -- below what the spawn table itself starts at -- corner occupancy 0.68% -> 0.27% per tile, and the median NPC's distinct tiles visited 29 -> 63. Three new tests, each checked to fail against the old behaviour.
fix(npc): roll while walking, and refuse an out-of-box destination
All checks were successful
ci / gates (pull_request) Successful in 4m1s
951b8ebb7f
NPC wander composed two correct rules into a ratchet. `hold/1` keeps the
destination when a creature is in the way, and `roam/1` refused to roll
while it had one -- the client's own `pathLength == 0` guard. Together,
two NPCs whose next steps cross can neither move nor choose anywhere
else; nothing clears it, so every jam is permanent and the population
accumulates into knots rather than settling into a wrong distribution.

Measured over the whole spawn table, 16,942 roamers against real
collision and real entity flags, the share of NPC ticks lost to a
creature in the way ran 0.6% -> 8.2% -> 37.2% -> 72.3% at ticks 1, 10,
100 and 1,000, and was still climbing. Mean other NPCs within three
tiles went 1.43 -> 2.44 over the same run, with the share of NPCs in a
knot of four or more going 20% -> 38.7%. That is the clustering an
observer sees, and it has nothing to do with pathfinding.

The roll now fires whether or not the NPC is already walking. The source
states the client guard and then contradicts it for the server, from the
game: NPCs move in one direction and then suddenly the opposite one, so
wandering may occur even with movement already queued. A roll that names
nowhere legal leaves the journey alone -- it named nowhere, which is not
the same as naming here.

The leash now refuses an out-of-box destination where it clamped one to
the box edge. The clamp had no source: the client's clamp is against the
map edge, and the wander box is a server concept the client does not
have. It loaded the boundary -- over 200,000 ticks a clamped walk spent
9.6% of its life on the four corner tiles, which are 3.3% of the box.
Refusing is the rule the module already applied to a zero offset.

Refusal needs one companion, because a stray can no longer be clamped
home: an NPC outside its box walks back to its spawn instead of rolling.
Only a fight can put it there, and `World.NpcCombat` already does this
for a creature that breaks its leash.

After: held flat at ~5% from tick 25 onward, crowding flat at 1.33 and
17.9% -- below what the spawn table itself starts at -- corner occupancy
0.68% -> 0.27% per tile, and the median NPC's distinct tiles visited
29 -> 63.

Three new tests, each checked to fail against the old behaviour.
Merge branch 'main' into fix/npc-wander-ratchet
All checks were successful
ci / gates (pull_request) Successful in 3m49s
192bbb7b82
Merge branch 'main' into fix/npc-wander-ratchet
All checks were successful
ci / gates (pull_request) Successful in 3m46s
build / image (push) Successful in 33s
ci / gates (push) Successful in 3m44s
0f921e1975
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Revenant/Server!238
No description provided.