The world was frozen solid, but only if you weren't playing
TERMINAL DESTINY has a public spectator link — ?watch=1. No wallet, no signup, no hero. You click it and you're dropped into the world behind the camera of JOURNEY, an autonomous agent that roams the map around the clock. It exists for exactly one reason: to be the link you send someone who has never heard of the game, so they can watch it move for thirty seconds and decide if they care.
For some unknown stretch of time, that link showed a world where nothing moved.
The symptom
Everything rendered. Rain fell, the flock crossed the sky, fireflies did their thing, JOURNEY walked its route. But every enemy on the map stood perfectly still. The Camper sat at the fire. The Wraith hung in place. Not idling, not breathing — planted, on the exact coordinate it spawned on.
I only caught it because I was watching my own demo link like a stranger would, which is a habit I recommend and rarely keep.
Once I stopped guessing and measured it, the number was unambiguous. A real watch session, camera parked 55 units off the Campsite, sampling the Camper's position for 20 seconds:
0% of samples moving. Not sluggish. Not a low tick rate. Zero.
And the tell was in which enemies froze: everything except the ones standing near an agent. Anything within a few strides of JOURNEY behaved perfectly. So the bug wasn't in roaming. It was in whatever decides that roaming is allowed to happen.
What was actually happening
Two systems were sharing one number, and they wanted opposite things from it.
Enemies chase the nearest valid target — the local player, or the nearest live agent, whichever is closer. Spectators are deliberately excluded from that. A spectator still has a body in the world for bookkeeping reasons, a hidden placeholder parked back at the cave, and if enemies could see it they'd walk across the map to beat up an invisible object while a stranger watched. So the chase code seeds the target distance at Infinity for spectators:
let d2 = _spectator ? Infinity : dL2;
Nothing pursues a spectator, ever. That part worked exactly as designed.
But roaming was gated on the same d2:
if (e.roams && e.alive && e.hp > -9999 && d2 < 5000 * 5000) {
That gate has a real reason to exist. Zones stream in around whoever's nearby, and the roam path guard rejects moves against scene._solidCircles — the building footprints an enemy must not wander into. Those circles don't exist until the zone is built. Roam an enemy nobody has streamed in and it drifts happily through a wall, then stands inside it when you finally arrive. So: don't roam if nobody's close enough for the world to have loaded.
Sound logic. Except the number it asked for that question was the aggro distance, which for a spectator is Infinity by deliberate design. Infinity is never less than 5000. So for a spectator, the roam gate answered "nobody is close enough" for every enemy on the map, forever — unless an agent happened to be nearby and dragged the number down.
The line reads like it's asking "is anyone near this enemy?" It's actually asking "is anyone hunting this enemy?" For every player who's ever existed those are the same question. For a spectator they're opposites.
The fix
Roaming is ambience, not aggro. It shouldn't key off who the enemy wants to attack — it should key off who is looking.
const _viewAnchor = (_spectator || bScene._followMesh) ? bScene._followGamePos : null;
const _vX = _viewAnchor ? _viewAnchor.x : player.x;
const _vY = _viewAnchor ? _viewAnchor.y : player.y;
// ...
const _vdx = _vX - e.gameX, _vdy = _vY - e.gameY;
if (e.roams && e.alive && e.hp > -9999 && _vdx * _vdx + _vdy * _vdy < 5000 * 5000) {
_followGamePos is the camera anchor — the agent being followed, or the spectator's free-roam look point — falling back to the player's own position when there's no camera to follow. Fourteen lines, most of them the comment.
The part that makes it safe rather than merely working: _followGamePos is the same anchor the zone streamer uses. So the wall circles the roam guard needs are guaranteed to exist wherever it's now allowed to roam. The original constraint is still honoured; it's just being asked about the right point in space.
Chasing is untouched. A spectator is still never a target. For an ordinary player with the camera on their own hero, the anchor is the player, and nothing changes at all.
Same session, same camera position, after the change: 89% of samples moving.
What I'd do differently
The bug wasn't the gate and it wasn't the sentinel. It was that d2 carried two meanings, and I only ever wrote down one of them.
Seeding Infinity was a deliberate lie told to the chase system — a clean way to say "this entity is not a target." It just never occurred to me that a second system would read that same variable and interpret the lie as a fact about geometry. Sentinel values are a message to a specific reader, and a variable in scope has no way to know who's reading it.
Two things I'm taking from it:
A sentinel needs to be scoped to the system it lies to. If I'd computed the chase distance as its own named thing instead of overloading the general one, the roam gate would never have been able to see the Infinity at all.
Spectator mode needs its own smoke test. Every bug in this class is invisible to the person building the game, because I'm almost always logged in as a player with a hero and a body. The one path I never sit in is the one every new person sees first. That's backwards, and the frozen world was the polite version of that lesson.
The unnerving part isn't the bug. It's that the demo link was the loudest thing I own, and it was broken in a way I could only see by pretending to be a stranger.