// core/bus.js (Lane F) — event bus. Event names are a contract: TECH.md §Bus. export function createBus() { const map = new Map(); return { on(name, fn) { if (!map.has(name)) map.set(name, new Set()); map.get(name).add(fn); return () => map.get(name)?.delete(fn); }, off(name, fn) { map.get(name)?.delete(fn); }, emit(name, payload) { const set = map.get(name); if (!set) return; // ISOLATED. Listeners run bare here until now, so one throw in (say) the HUD aborted the // rest of the emit — and since player:state is emitted every frame from inside step(), // a single bad frame in one subscriber took down combat, audio and the run loop with it. // A UI module failing should cost its own readout and nothing else, which is the same // principle boot already applies when CONSTRUCTING them. for (const fn of [...set]) { try { fn(payload); } catch (e) { console.error(`[bus] listener for "${name}" threw —`, e); } } }, }; }