EchoQuest v0.27.0: NPCs Come Alive
This update is one we’ve been looking forward to for a while. NPCs in EchoQuest no longer stand in one spot like mannequins waiting for you to click on them. They have places to be.
The Problem
Until now, NPCs in EchoQuest were stationary. They stood in one spot forever, waiting for you to talk to them. We wanted something better — NPCs that feel like they have lives. A town guard who patrols the streets in the morning, takes a lunch break at the tavern, then returns to duty at night.
Building the Schedule System
The challenge was architectural. Our old system was map-centric: each map managed its own NPCs. Load a map, get its NPCs. Unload it, they’re gone.
That works fine until an NPC needs to move between maps. If the guard walks to the tavern but no player follows him there, the tavern map never loads. The guard effectively vanishes into the void. When his shift ends and he’s supposed to walk back to his post, there’s nothing to walk back from.
The solution: a global NPC schedule registry that tracks every scheduled NPC regardless of which maps are loaded. When a player enters a map, the registry tells the behavior service exactly who should be there and what they should be doing.
Each NPC can have multiple schedule entries — time ranges with a location, behavior override, and optional dialogue changes. The registry evaluates every game-clock hour and produces transitions: spawn here, despawn there, change behavior from “static” to “patrol.”
The Invisible Bug
After building all of this, we tested it. NPCs still wouldn’t patrol. They’d show up in the right place at the right time (schedule transitions worked), but they’d just stand there instead of walking their patrol routes.
We spent hours tracing through schedule logic, waypoint loading, state management, and transition handling. Everything looked correct. The schedule set behavior to “patrol.” The waypoints loaded from the database. The patrol function was ready to cycle through them.
The bug was in the tick loop — the 2Hz heartbeat that updates every NPC’s position:
const playerCount = mapPresenceService.getPlayerCountOnMap?.(mapKey) || 0;
if (playerCount === 0) continue;
getPlayerCountOnMap doesn’t exist. It was never a function on mapPresenceService. JavaScript’s optional chaining (?.()) silently returned undefined, which || 0 turned into zero. Every map was skipped on every tick. No NPC ever moved — not just patrols, but wander behavior and scheduled movement too.
The fix was two lines:
const players = mapPresenceService.getPlayersOnMapDirect(mapKey);
if (players.length === 0) continue;
Sometimes the hardest bugs are the simplest ones hiding behind silent failures.
What Else
- Sentry error tracking across client, server, and Electron — so we catch crashes before players report them
- Smooth clock display — the in-game clock now ticks every second instead of jumping in 60-second chunks
- No more client-side enemy spawning — all enemies are now 100% server-authoritative
- NPC dialogue overrides — NPCs can say different things depending on the time of day
There’s more to come. Continue reading on the devlog →
— Bruno