// P2P networking via PeerJS (WebRTC data channels). The PeerJS public cloud // broker is used only for signaling/room discovery — once connected, all game // traffic flows peer-to-peer. No game server anywhere. // // The room key IS the host's peer id (prefixed). Share the 5 letters, done. const PRE = 'barkour-w1-'; const KEY_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; export function randomKey() { let s = ''; for (let i = 0; i < 5; i++) s += KEY_ALPHABET[Math.floor(Math.random() * KEY_ALPHABET.length)]; return s; } export function peerAvailable() { return typeof window !== 'undefined' && typeof window.Peer !== 'undefined'; } function errMsg(e) { const t = e && e.type; if (t === 'peer-unavailable') return 'No room with that key. Check the code and try again.'; if (t === 'unavailable-id') return 'Room key collision — host again for a fresh key.'; if (t === 'network' || t === 'server-error' || t === 'socket-error') return 'Could not reach the signaling broker (offline?).'; return 'Connection error: ' + (e && e.message || t || 'unknown'); } // WebRTC close events are unreliable (a vanished tab often never fires // 'close'), so both sides run a heartbeat + watchdog over the data channel. const HB_INTERVAL = 2000, HB_TIMEOUT = 8000; function wireConn(c, cb, die, who) { let lastRecv = performance.now(); c.on('data', (d) => { lastRecv = performance.now(); if (d && d.t === '_hb') return; cb.data(d); }); c.on('close', () => die(`${who} disconnected.`)); c.on('error', () => die('Connection lost.')); const timer = setInterval(() => { if (c.open) { try { c.send({ t: '_hb' }); } catch (_) {} if (performance.now() - lastRecv > HB_TIMEOUT) die(`${who} stopped responding.`); } }, HB_INTERVAL); return () => clearInterval(timer); } // cb: { open(key), conn(), data(msg), close(reason) } export function hostRoom(key, cb) { if (!peerAvailable()) { cb.close('PeerJS failed to load — check your internet connection.'); return null; } const peer = new window.Peer(PRE + key); let conn = null; let dead = false; let linked = false; // data channel established — broker hiccups no longer matter let unwire = null; const die = (msg) => { if (!dead) { dead = true; if (unwire) unwire(); cb.close(msg); } }; peer.on('open', () => cb.open(key)); peer.on('error', (e) => { if (linked && e && e.type === 'network') return; die(errMsg(e)); }); peer.on('disconnected', () => { try { peer.reconnect(); } catch (_) { /* broker gone; game traffic is p2p anyway */ } }); peer.on('connection', (c) => { if (conn) { try { c.close(); } catch (_) {} return; } // room is full (2 players) conn = c; c.on('open', () => { linked = true; unwire = wireConn(c, cb, die, 'Other player'); cb.conn(); }); }); return { send: (o) => { if (conn && conn.open) conn.send(o); }, destroy: () => { dead = true; if (unwire) unwire(); try { peer.destroy(); } catch (_) {} }, }; } export function joinRoom(key, cb) { if (!peerAvailable()) { cb.close('PeerJS failed to load — check your internet connection.'); return null; } const peer = new window.Peer(); let dead = false; let linked = false; let unwire = null; const die = (msg) => { if (!dead) { dead = true; if (unwire) unwire(); cb.close(msg); } }; const api = { send: () => {}, destroy: () => { dead = true; if (unwire) unwire(); try { peer.destroy(); } catch (_) {} }, }; peer.on('error', (e) => { if (linked && e && e.type === 'network') return; die(errMsg(e)); }); peer.on('disconnected', () => { try { peer.reconnect(); } catch (_) { /* broker gone; game traffic is p2p anyway */ } }); peer.on('open', () => { const c = peer.connect(PRE + key.toUpperCase().trim(), { reliable: true }); c.on('open', () => { linked = true; api.send = (o) => { if (c.open) c.send(o); }; unwire = wireConn(c, cb, die, 'Host'); cb.conn(); }); }); return api; }