Sim: clamp knockback under tile size (wall tunneling), bomb/ball terminal velocity, grapple constraint no longer drags players into terrain, bite window ends on getting hit, shield/boost cleared on death, spawn invuln preserved through countdown, spike/saw events only on real damage. Mapgen: spawn footprint validation (was embedding dogs in 58% of seeds), saw paths rejected if they grind terrain or sweep spawns, towers no longer hang slabs over pits. Net: heartbeat+watchdog for dead peers, broker-error tolerance once P2P link is up, join/host reentrancy guards. UI: text-field keystrokes no longer hit game hotkeys, audio unlocks on mouse, textBaseline leak, snapshot carries onGround/landT so client dogs animate.
96 lines
3.9 KiB
JavaScript
96 lines
3.9 KiB
JavaScript
// 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;
|
|
}
|