Define _gameUpdate(dt). The editor calls it every animation frame (multiple times per frame at higher speeds).
_getGameState()player{ x, y, hp, maxHp } in world pixels, or null before a run startsenemies[{ x, y, hp }] in the same world coordinatesxpleveltimegameOvertrue on death, victory or when no run is activecanvasW, canvasH_sendAction({ type: "move", dx, dy })Pass a normalized direction vector (length 1, or 0 to stop). Each axis component beyond ±0.3 presses that direction, so movement snaps to 8 directions at the player's normal speed. The input is held until the next action, and other action types are ignored.
dtMilliseconds since the previous tick (about 16 at 60fps, divided by the speed multiplier). Accumulate it and compare against 1000 to act once per second. Note that time in the state is in seconds.
let retarget = 0, dir = { dx: 0, dy: 0 };
function _gameUpdate(dt) {
const s = _getGameState();
if (s.gameOver || !s.player) return;
retarget += dt; // dt is in ms
if (retarget >= 1000) { // re-plan once per second
retarget = 0;
let fx = 0, fy = 0; // sum of repulsion from nearby enemies
for (const e of s.enemies) {
const ex = s.player.x - e.x, ey = s.player.y - e.y;
const d = Math.hypot(ex, ey) || 1;
if (d < 300) { fx += ex / (d * d); fy += ey / (d * d); }
}
const len = Math.hypot(fx, fy);
// Strafe perpendicular to the threat so you circle instead of flee
dir = len ? { dx: -fy / len, dy: fx / len } : { dx: 0, dy: 0 };
}
_sendAction({ type: "move", ...dir });
}