iPuzzle/js/net/NetworkManager.js

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; },
};
})();