171 lines
4.4 KiB
JavaScript
171 lines
4.4 KiB
JavaScript
/* global */
|
|
|
|
/**
|
|
* NetworkManager — singleton WebSocket client for multiplayer.
|
|
*
|
|
* Usage:
|
|
* NetworkManager.connect();
|
|
* NetworkManager.on('room_created', (msg) => { ... });
|
|
* NetworkManager.createRoom(roomCode, stateObj);
|
|
*/
|
|
const NetworkManager = (() => {
|
|
let _ws = null;
|
|
let _playerId = null;
|
|
let _playerName = null;
|
|
let _roomCode = null;
|
|
let _connected = false;
|
|
|
|
// Simple event emitter
|
|
const _listeners = {};
|
|
|
|
// Move throttle
|
|
let _lastMoveTime = 0;
|
|
const MOVE_INTERVAL = 50; // ms
|
|
|
|
// Reconnection
|
|
let _reconnectAttempts = 0;
|
|
const MAX_RECONNECT = 5;
|
|
let _reconnectTimer = null;
|
|
let _pendingRoomCode = null;
|
|
let _wasConnected = false;
|
|
|
|
function on(event, callback) {
|
|
if (!_listeners[event]) _listeners[event] = [];
|
|
_listeners[event].push(callback);
|
|
}
|
|
|
|
function off(event, callback) {
|
|
if (!_listeners[event]) return;
|
|
_listeners[event] = _listeners[event].filter(cb => cb !== callback);
|
|
}
|
|
|
|
function _emit(event, data) {
|
|
if (!_listeners[event]) return;
|
|
_listeners[event].forEach(cb => cb(data));
|
|
}
|
|
|
|
function connect(url) {
|
|
if (_ws && (_ws.readyState === WebSocket.OPEN || _ws.readyState === WebSocket.CONNECTING)) {
|
|
return;
|
|
}
|
|
|
|
if (!url) {
|
|
const loc = window.location;
|
|
const proto = loc.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
// 0.0.0.0 is not a valid WebSocket target — use localhost instead
|
|
const host = loc.hostname === '0.0.0.0' ? 'localhost' : loc.hostname;
|
|
const port = loc.port ? ':' + loc.port : '';
|
|
url = `${proto}//${host}${port}`;
|
|
}
|
|
|
|
_ws = new WebSocket(url);
|
|
|
|
_ws.onopen = () => {
|
|
_connected = true;
|
|
_reconnectAttempts = 0;
|
|
_wasConnected = true;
|
|
_emit('connected');
|
|
console.log('NetworkManager: connected');
|
|
};
|
|
|
|
_ws.onmessage = (event) => {
|
|
let msg;
|
|
try {
|
|
msg = JSON.parse(event.data);
|
|
} catch {
|
|
return;
|
|
}
|
|
|
|
// Track our player ID from room lifecycle messages
|
|
if (msg.type === 'room_created' || msg.type === 'room_joined') {
|
|
_playerId = msg.playerId;
|
|
_roomCode = msg.roomCode;
|
|
}
|
|
|
|
_emit(msg.type, msg);
|
|
};
|
|
|
|
_ws.onclose = () => {
|
|
_connected = false;
|
|
_emit('disconnected');
|
|
console.log('NetworkManager: disconnected');
|
|
_tryReconnect();
|
|
};
|
|
|
|
_ws.onerror = (err) => {
|
|
console.warn('NetworkManager: error', err);
|
|
};
|
|
}
|
|
|
|
function _tryReconnect() {
|
|
if (_reconnectAttempts >= MAX_RECONNECT) return;
|
|
_reconnectAttempts++;
|
|
const delay = Math.min(1000 * Math.pow(2, _reconnectAttempts - 1), 10000);
|
|
console.log(`NetworkManager: reconnecting in ${delay}ms (attempt ${_reconnectAttempts})`);
|
|
_reconnectTimer = setTimeout(() => connect(), delay);
|
|
}
|
|
|
|
function disconnect() {
|
|
_wasConnected = false;
|
|
if (_reconnectTimer) clearTimeout(_reconnectTimer);
|
|
if (_ws) {
|
|
_ws.onclose = null; // prevent reconnection
|
|
_ws.close();
|
|
_ws = null;
|
|
}
|
|
_connected = false;
|
|
_playerId = null;
|
|
_roomCode = null;
|
|
}
|
|
|
|
function _send(obj) {
|
|
if (_ws && _ws.readyState === WebSocket.OPEN) {
|
|
_ws.send(JSON.stringify(obj));
|
|
}
|
|
}
|
|
|
|
// ─── Public API ─────────────────────────────────────────────────────
|
|
|
|
function createRoom(roomCode, state, playerName) {
|
|
_playerName = playerName;
|
|
_send({ type: 'create_room', roomCode, state, playerName });
|
|
}
|
|
|
|
function joinRoom(roomCode, playerName) {
|
|
_playerName = playerName;
|
|
_pendingRoomCode = roomCode;
|
|
_send({ type: 'join_room', roomCode, playerName });
|
|
}
|
|
|
|
function claimPiece(pieceId) {
|
|
_send({ type: 'claim', pieceId });
|
|
}
|
|
|
|
function sendMove(pieceId, dx, dy) {
|
|
const now = performance.now();
|
|
if (now - _lastMoveTime < MOVE_INTERVAL) return;
|
|
_lastMoveTime = now;
|
|
_send({ type: 'move', pieceId, dx, dy });
|
|
}
|
|
|
|
function sendRelease(pieceId, positions, groups) {
|
|
_send({ type: 'release', pieceId, positions, groups });
|
|
}
|
|
|
|
return {
|
|
connect,
|
|
disconnect,
|
|
on,
|
|
off,
|
|
createRoom,
|
|
joinRoom,
|
|
claimPiece,
|
|
sendMove,
|
|
sendRelease,
|
|
get playerId() { return _playerId; },
|
|
get playerName() { return _playerName; },
|
|
get roomCode() { return _roomCode; },
|
|
get connected() { return _connected; },
|
|
};
|
|
})();
|