Timing (no setTimeout)
Why game code never calls setTimeout — and what it calls instead.
Principle #2: game code never calls setTimeout or setInterval.
All scheduled work goes through Ticker.schedule(ms, fn) from
src/infrastructure/timing.ts.
The bug
The user switches tabs. Pixi's ticker pauses — requestAnimationFrame is
throttled or suspended by the browser. Good: the game freezes in place.
Except setTimeout doesn't pause. That win-show callback you queued with
setTimeout(..., 1500) fires while the reels haven't moved. Now the balance
updates before the animation plays. When the user switches back, you get a ghost
animation and (on a cascade game) a phase mismatch that crashes the FSM.
The fix
gsap.delayedCall runs on GSAP's ticker. slotplate syncs that ticker to
app.ticker at boot:
// src/infrastructure/timing.ts
export function syncGsapToPixi(pixiTicker) {
gsap.ticker.remove(gsap.updateRoot);
pixiTicker.add(() => gsap.updateRoot(performance.now() / 1000));
}
Now GSAP pauses when Pixi pauses. Ticker.schedule wraps
gsap.delayedCall and returns a Disposable:
schedule(delayMs: number, fn: () => void): Disposable {
const tween = gsap.delayedCall(delayMs / 1000, fn);
return { dispose: () => tween.kill() };
} Usage
// In a phase's enter():
this.cancel = ctx.ticker.schedule(1500, () => {
ctx.fsm.transition('idle');
});
// In the phase's exit():
this.cancel?.dispose(); Enforcement
biome.json declares setTimeout and setInterval
as restricted globals with a message pointing at timing.ts.
Lint will fail any PR that adds them.
What about Promises?
- Promises that resolve on ticker events (reel landed, animation done) — fine.
- Promises that resolve on wall-clock delay via
setTimeout— same bug as raw setTimeout. Use the ticker.
What about network timeouts?
AbortController + fetch's built-in timeout are infrastructure-layer
concerns. They deliberately run on wall-clock time — you want the request to fail
after N seconds of real time even if the tab is hidden. The rule is about game
timing (sequences, animations, debounces), not network.