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