mirror of
https://github.com/DeNNiiInc/Connect-5.git
synced 2026-04-17 18:26:01 +00:00
737 lines
28 KiB
JavaScript
737 lines
28 KiB
JavaScript
// Multiplayer Client Manager
|
||
class MultiplayerClient {
|
||
constructor(game) {
|
||
this.game = game;
|
||
this.socket = null;
|
||
this.playerId = null;
|
||
this.username = null;
|
||
this.currentGameId = null;
|
||
this.isMultiplayer = false;
|
||
this.activePlayers = [];
|
||
this.pendingChallenges = new Map();
|
||
this.selectedBoardSize = 15; // Default board size for multiplayer
|
||
this.opponent = null; // Track opponent for rematch
|
||
this.opponentId = null;
|
||
this.lastGameResult = null;
|
||
}
|
||
|
||
|
||
// Connect to server
|
||
async connect() {
|
||
if (typeof io === 'undefined') {
|
||
this.showMessage('Socket.io library not loaded. Please check your internet connection.', 'error');
|
||
return;
|
||
}
|
||
|
||
// Show username modal immediately if not saved
|
||
const savedUsername = await window.gameStorage.getItem('connect5_username');
|
||
if (!savedUsername) {
|
||
this.showUsernameModal();
|
||
} else {
|
||
this.username = savedUsername; // Pre-load username
|
||
}
|
||
|
||
// Dynamically construct proxy URLs based on current origin
|
||
const targetUrl = window.location.origin;
|
||
const servers = [
|
||
targetUrl, // Primary (Production/Local)
|
||
'http://localhost:3000' // Failover for local dev
|
||
];
|
||
|
||
let connected = false;
|
||
const loadingEl = document.querySelector('.loading');
|
||
|
||
for (const serverUrl of servers) {
|
||
if (connected) break;
|
||
|
||
try {
|
||
if (loadingEl) loadingEl.textContent = `Connecting to ${serverUrl}...`;
|
||
console.log(`Attempting connection to: ${serverUrl}`);
|
||
await this.tryConnect(serverUrl);
|
||
connected = true;
|
||
console.log(`✅ Successfully connected to: ${serverUrl}`);
|
||
if (loadingEl) loadingEl.textContent = 'Connected! verifying functionality...';
|
||
} catch (error) {
|
||
console.warn(`❌ Failed to connect to ${serverUrl}:`, error);
|
||
}
|
||
}
|
||
|
||
|
||
|
||
if (!connected) {
|
||
this.showMessage('Failed to connect to any multiplayer server. Please try again later.', 'error');
|
||
const loading = document.querySelector('.loading');
|
||
if (loading) loading.textContent = 'Connection failed.';
|
||
}
|
||
|
||
// Setup board size selector buttons
|
||
document.querySelectorAll('.size-btn-mp').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
document.querySelectorAll('.size-btn-mp').forEach(b => b.classList.remove('active'));
|
||
e.target.classList.add('active');
|
||
this.selectedBoardSize = parseInt(e.target.dataset.size);
|
||
});
|
||
});
|
||
}
|
||
|
||
tryConnect(url) {
|
||
return new Promise((resolve, reject) => {
|
||
const socketOptions = {
|
||
reconnection: false, // We handle reconnection manually for failover
|
||
timeout: 5000,
|
||
transports: ['websocket', 'polling']
|
||
};
|
||
|
||
const tempSocket = io(url, socketOptions);
|
||
|
||
// Set up ALL listeners BEFORE the connection completes
|
||
// This prevents race conditions
|
||
this.setupSocketListeners(tempSocket);
|
||
|
||
const timeout = setTimeout(() => {
|
||
if (!tempSocket.connected) {
|
||
tempSocket.close();
|
||
reject(new Error('Connection timed out'));
|
||
}
|
||
}, 5000);
|
||
|
||
tempSocket.on('connect', async () => {
|
||
clearTimeout(timeout);
|
||
this.socket = tempSocket;
|
||
console.log('✅ Socket connected, listeners ready');
|
||
|
||
// Now that listeners are set up, handle auto-registration
|
||
const savedUsername = await window.gameStorage.getItem('connect5_username') || this.username;
|
||
if (savedUsername) {
|
||
console.log('Auto-registering with saved username:', savedUsername);
|
||
this.registerPlayer(savedUsername);
|
||
}
|
||
|
||
resolve();
|
||
});
|
||
|
||
tempSocket.on('connect_error', (err) => {
|
||
clearTimeout(timeout);
|
||
tempSocket.close();
|
||
reject(err);
|
||
});
|
||
});
|
||
}
|
||
|
||
setupSocketListeners(socket) {
|
||
if (!socket) return;
|
||
|
||
// Safety timeout: If we are connected but don't get a player list or login prompt within 5 seconds, warn the user.
|
||
setTimeout(() => {
|
||
const loading = document.querySelector('.loading');
|
||
if (loading && loading.textContent.includes('Connecting')) {
|
||
loading.textContent = 'Connection successful, but server response is slow...';
|
||
} else if (loading && loading.textContent === 'Loading players...') {
|
||
loading.innerHTML = 'Server not responding. <a href="#" onclick="window.multiplayerClient.connect()">Retry</a>';
|
||
}
|
||
}, 5000);
|
||
|
||
socket.on('disconnect', () => {
|
||
console.log('❌ Disconnected from server');
|
||
this.handleDisconnect();
|
||
});
|
||
|
||
socket.on('registration_result', (data) => {
|
||
console.log('📥 Received registration_result:', data);
|
||
this.handleRegistration(data);
|
||
});
|
||
|
||
socket.on('active_players_update', (players) => {
|
||
this.updateActivePlayers(players);
|
||
});
|
||
|
||
socket.on('challenge_received', (data) => {
|
||
this.showChallengeNotification(data);
|
||
});
|
||
|
||
socket.on('challenge_result', (data) => {
|
||
this.handleChallengeResult(data);
|
||
});
|
||
|
||
socket.on('challenge_declined', (data) => {
|
||
this.showMessage(`${data.by} declined your challenge`, 'error');
|
||
});
|
||
|
||
socket.on('game_started', (data) => {
|
||
this.startMultiplayerGame(data);
|
||
});
|
||
|
||
socket.on('opponent_move', (data) => {
|
||
this.handleOpponentMove(data);
|
||
});
|
||
|
||
socket.on('move_result', (data) => {
|
||
this.handleMoveResult(data);
|
||
});
|
||
|
||
socket.on('game_ended', (data) => {
|
||
this.handleGameEnded(data);
|
||
});
|
||
|
||
socket.on('rematch_request', (data) => {
|
||
this.handleRematchRequest(data);
|
||
});
|
||
|
||
socket.on('rematch_accepted', (data) => {
|
||
this.startMultiplayerGame(data);
|
||
});
|
||
|
||
socket.on('rematch_declined', (data) => {
|
||
this.showMessage(`${data.by} declined the rematch`, 'error');
|
||
document.getElementById('gameOverModal').classList.remove('active');
|
||
this.returnToLobby();
|
||
});
|
||
|
||
socket.on('opponent_disconnected', (data) => {
|
||
this.showMessage(data.message + '. Waiting for reconnection...', 'warning');
|
||
});
|
||
|
||
socket.on('opponent_reconnected', (data) => {
|
||
this.showMessage(data.message, 'success');
|
||
});
|
||
|
||
// Send heartbeat every 30 seconds
|
||
setInterval(() => {
|
||
if (this.socket && this.socket.connected) {
|
||
this.socket.emit('heartbeat');
|
||
}
|
||
}, 30000);
|
||
}
|
||
|
||
// Show username input modal
|
||
showUsernameModal() {
|
||
const modal = document.getElementById('usernameModal');
|
||
if (modal) {
|
||
modal.classList.add('active');
|
||
}
|
||
}
|
||
|
||
// Register player
|
||
async registerPlayer(username) {
|
||
this.username = username;
|
||
await window.gameStorage.setItem('connect5_username', username);
|
||
|
||
// Hide username modal immediately for better UX
|
||
const modal = document.getElementById('usernameModal');
|
||
if (modal) {
|
||
modal.classList.remove('active');
|
||
}
|
||
|
||
// Show loading state
|
||
const loading = document.querySelector('.loading');
|
||
if (loading) {
|
||
loading.textContent = 'Registering...';
|
||
}
|
||
|
||
if (!this.socket || !this.socket.connected) {
|
||
console.log('Socket not ready yet, username saved and will be sent on connect.');
|
||
if (loading) {
|
||
loading.textContent = 'Connecting to server...';
|
||
}
|
||
return;
|
||
}
|
||
|
||
console.log('Emitting register_player for:', username);
|
||
this.socket.emit('register_player', { username });
|
||
}
|
||
|
||
// Handle registration response
|
||
async handleRegistration(data) {
|
||
if (data.success) {
|
||
this.playerId = data.player.id;
|
||
this.username = data.player.username;
|
||
await window.gameStorage.setItem('connect5_username', this.username);
|
||
console.log('Username saved to IndexedDB');
|
||
|
||
// Hide username modal (if visible)
|
||
const modal = document.getElementById('usernameModal');
|
||
if (modal) {
|
||
modal.classList.remove('active');
|
||
}
|
||
|
||
// Show multiplayer panel
|
||
const multiplayerPanel = document.getElementById('multiplayerPanel');
|
||
if (multiplayerPanel) {
|
||
multiplayerPanel.style.display = 'block';
|
||
}
|
||
|
||
// Update player stats display
|
||
document.getElementById('playerUsername').textContent = this.username;
|
||
document.getElementById('playerWins').textContent = data.player.stats.wins;
|
||
document.getElementById('playerLosses').textContent = data.player.stats.losses;
|
||
document.getElementById('playerDraws').textContent = data.player.stats.draws;
|
||
|
||
this.showMessage(`Welcome back, ${this.username}!`, 'success');
|
||
|
||
// Request active players
|
||
this.socket.emit('request_active_players');
|
||
|
||
// Check for active game restoration
|
||
if (data.activeGame) {
|
||
console.log(' restoring active game:', data.activeGame);
|
||
this.startMultiplayerGame(data.activeGame);
|
||
}
|
||
} else {
|
||
// Registration failed - clear saved username and show modal
|
||
localStorage.removeItem('connect5_username');
|
||
this.showMessage(data.error, 'error');
|
||
this.showUsernameModal();
|
||
}
|
||
}
|
||
|
||
// Update active players list
|
||
updateActivePlayers(players) {
|
||
this.activePlayers = players.filter(p => p.username !== this.username);
|
||
|
||
const container = document.getElementById('activePlayersList');
|
||
if (!container) return;
|
||
|
||
if (this.activePlayers.length === 0) {
|
||
container.innerHTML = '<div class="no-players">No other players online</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = this.activePlayers.map(player => `
|
||
<div class="player-item">
|
||
<div class="player-info">
|
||
<span class="player-name">${player.username}</span>
|
||
<span class="player-stats">${player.total_wins}W - ${player.total_losses}L - ${player.total_draws}D</span>
|
||
</div>
|
||
<button class="challenge-btn" onclick="multiplayerClient.sendChallenge('${player.username}')">
|
||
Challenge
|
||
</button>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
// Send challenge
|
||
sendChallenge(targetUsername) {
|
||
this.socket.emit('send_challenge', {
|
||
targetUsername,
|
||
boardSize: this.selectedBoardSize
|
||
});
|
||
this.showMessage(`Challenge sent to ${targetUsername} (${this.selectedBoardSize}×${this.selectedBoardSize})`, 'info');
|
||
}
|
||
|
||
// Handle challenge result
|
||
handleChallengeResult(data) {
|
||
if (!data.success) {
|
||
this.showMessage(data.error, 'error');
|
||
}
|
||
}
|
||
|
||
// Show challenge notification
|
||
showChallengeNotification(data) {
|
||
this.pendingChallenges.set(data.challengeId, data);
|
||
|
||
const notification = document.createElement('div');
|
||
notification.className = 'challenge-notification';
|
||
notification.innerHTML = `
|
||
<div class="challenge-content">
|
||
<h3>Challenge Received!</h3>
|
||
<p><strong>${data.from}</strong> wants to play</p>
|
||
<p>Board size: ${data.boardSize}×${data.boardSize}</p>
|
||
<div class="challenge-actions">
|
||
<button class="accept-btn" onclick="multiplayerClient.acceptChallenge('${data.challengeId}')">
|
||
Accept
|
||
</button>
|
||
<button class="decline-btn" onclick="multiplayerClient.declineChallenge('${data.challengeId}')">
|
||
Decline
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
document.body.appendChild(notification);
|
||
setTimeout(() => notification.classList.add('active'), 10);
|
||
}
|
||
|
||
// Accept challenge
|
||
acceptChallenge(challengeId) {
|
||
this.socket.emit('accept_challenge', { challengeId });
|
||
this.pendingChallenges.delete(challengeId);
|
||
|
||
// Remove notification
|
||
const notifications = document.querySelectorAll('.challenge-notification');
|
||
notifications.forEach(n => n.remove());
|
||
}
|
||
|
||
// Decline challenge
|
||
declineChallenge(challengeId) {
|
||
this.socket.emit('decline_challenge', { challengeId });
|
||
this.pendingChallenges.delete(challengeId);
|
||
|
||
// Remove notification
|
||
const notifications = document.querySelectorAll('.challenge-notification');
|
||
notifications.forEach(n => n.remove());
|
||
}
|
||
|
||
// Start multiplayer game
|
||
startMultiplayerGame(data) {
|
||
console.log('🎮 Starting multiplayer game:', data);
|
||
this.isMultiplayer = true;
|
||
this.currentGameId = data.gameId;
|
||
this.mySymbol = data.yourSymbol;
|
||
this.opponent = data.opponent;
|
||
this.opponentId = data.opponentId; // Store for rematch
|
||
this.myTurn = data.yourTurn;
|
||
|
||
// Show surrender button
|
||
const surrenderBtn = document.getElementById('surrenderBtn');
|
||
if (surrenderBtn) {
|
||
surrenderBtn.style.display = 'flex';
|
||
}
|
||
|
||
// Update UI - hide multiplayer lobby, show game board
|
||
document.getElementById('multiplayerPanel').style.display = 'none';
|
||
document.getElementById('gameControls').style.display = 'grid';
|
||
document.querySelector('.board-wrapper').style.display = 'flex';
|
||
// Status bar is part of gameControls
|
||
|
||
// Set board size
|
||
document.querySelectorAll('.size-btn').forEach(btn => {
|
||
btn.classList.toggle('active', parseInt(btn.dataset.size) === data.boardSize);
|
||
});
|
||
|
||
// Reset game board
|
||
this.game.boardSize = data.boardSize;
|
||
this.game.currentPlayer = this.mySymbol; // Set to our symbol
|
||
this.game.gameActive = true;
|
||
|
||
// Reset scores if this is a new opponent, otherwise keep them for rematch
|
||
if (this.currentOpponentId !== data.opponentId) {
|
||
this.game.scores = { X: 0, O: 0 };
|
||
this.game.updateScores();
|
||
this.currentOpponentId = data.opponentId;
|
||
}
|
||
|
||
this.game.initializeBoard();
|
||
|
||
// Update player identity display (Right Side)
|
||
const identitySection = document.getElementById('playerIdentitySection');
|
||
const identityMarker = document.getElementById('playerIdentity');
|
||
|
||
if (identitySection && identityMarker) {
|
||
identitySection.style.display = 'flex';
|
||
identityMarker.textContent = this.mySymbol;
|
||
|
||
// Set color for identity marker
|
||
if (this.mySymbol === 'O') {
|
||
identityMarker.style.color = 'hsl(195, 70%, 55%)';
|
||
identityMarker.parentElement.style.borderColor = 'hsl(195, 70%, 55%)';
|
||
} else {
|
||
identityMarker.style.color = 'hsl(270, 70%, 60%)';
|
||
identityMarker.parentElement.style.borderColor = 'hsl(270, 70%, 60%)';
|
||
}
|
||
}
|
||
|
||
// Ensure Turn label is correct (Left Side)
|
||
const turnLabel = document.getElementById('turnLabel');
|
||
if (turnLabel) turnLabel.textContent = 'Current Turn:';
|
||
|
||
// Set initial color for Current Turn display (will be updated by game.js but good to init)
|
||
const currentPlayerDisplay = document.getElementById('currentPlayer');
|
||
|
||
// Update status text (Left Side generic message)
|
||
const statusMessage = document.getElementById('statusMessage');
|
||
|
||
if (this.myTurn) {
|
||
statusMessage.textContent = `VS ${this.opponent} | YOUR TURN`;
|
||
statusMessage.className = 'status-text-small success';
|
||
} else {
|
||
statusMessage.textContent = `VS ${this.opponent} | Waiting for opponent`;
|
||
statusMessage.className = 'status-text-small info';
|
||
}
|
||
|
||
console.log(`✅ Game started! You are ${this.mySymbol}, ${this.myTurn ? 'your turn' : 'waiting'}`);
|
||
|
||
// Restore board state if provided (reconnection)
|
||
if (data.board) {
|
||
this.game.board = data.board;
|
||
// Re-render board
|
||
const cells = this.game.boardElement.children;
|
||
for (let r = 0; r < data.boardSize; r++) {
|
||
for (let c = 0; c < data.boardSize; c++) {
|
||
const symbol = data.board[r][c];
|
||
if (symbol) {
|
||
const index = r * data.boardSize + c;
|
||
const cell = cells[index];
|
||
if (cell) {
|
||
cell.classList.add('occupied', symbol.toLowerCase());
|
||
cell.textContent = ''; // CSS handles the X/O appearance
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Update current player for game logic
|
||
if (data.currentTurnSymbol) {
|
||
this.game.currentPlayer = data.currentTurnSymbol;
|
||
const currentPlayerDisplay = document.getElementById('currentPlayer');
|
||
if (currentPlayerDisplay) {
|
||
currentPlayerDisplay.textContent = this.game.currentPlayer;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Make move in multiplayer game
|
||
makeMove(row, col) {
|
||
if (!this.isMultiplayer || !this.myTurn) return false;
|
||
|
||
this.socket.emit('make_move', {
|
||
gameId: this.currentGameId,
|
||
row: row,
|
||
col: col
|
||
});
|
||
|
||
this.lastMyMove = { row, col };
|
||
return true;
|
||
}
|
||
|
||
// Handle move result from server
|
||
handleMoveResult(data) {
|
||
if (data.success) {
|
||
this.myTurn = false;
|
||
|
||
if (!data.gameOver) {
|
||
const statusMessage = document.getElementById('statusMessage');
|
||
if (statusMessage) {
|
||
statusMessage.textContent = `Playing against ${this.opponent} - Waiting for opponent...`;
|
||
statusMessage.className = 'status-text-small info';
|
||
}
|
||
}
|
||
} else {
|
||
this.showMessage(data.error, 'error');
|
||
}
|
||
}
|
||
|
||
// Handle opponent move
|
||
handleOpponentMove(data) {
|
||
this.lastOpponentMove = { row: data.row, col: data.col };
|
||
// Place opponent's piece on board
|
||
this.game.currentPlayer = data.symbol;
|
||
|
||
// Update turn indicator immediately to show X or O
|
||
const currentPlayerDisplay = document.getElementById('currentPlayer');
|
||
if (currentPlayerDisplay) {
|
||
currentPlayerDisplay.textContent = data.symbol;
|
||
}
|
||
|
||
const cellIndex = data.row * this.game.boardSize + data.col;
|
||
const cell = this.game.boardElement.children[cellIndex];
|
||
|
||
if (cell) {
|
||
cell.classList.add('occupied', data.symbol.toLowerCase(), 'latest-move');
|
||
this.game.board[data.row][data.col] = data.symbol;
|
||
|
||
// Remove brightness boost after 2 seconds
|
||
setTimeout(() => {
|
||
if (cell) cell.classList.remove('latest-move');
|
||
}, 2000);
|
||
}
|
||
|
||
// It's now my turn
|
||
this.myTurn = true;
|
||
this.game.currentPlayer = this.mySymbol;
|
||
|
||
const statusMessage = document.getElementById('statusMessage');
|
||
if (statusMessage) {
|
||
statusMessage.textContent = `Playing against ${this.opponent} - Your turn`;
|
||
statusMessage.className = 'status-text-small success';
|
||
}
|
||
}
|
||
|
||
// Handle game ended
|
||
handleGameEnded(data) {
|
||
this.isMultiplayer = false;
|
||
this.currentGameId = null;
|
||
|
||
let message = '';
|
||
if (data.reason === 'win') {
|
||
message = '🎉 You won!';
|
||
} else if (data.reason === 'loss') {
|
||
message = '😔 You lost!';
|
||
} else if (data.reason === 'draw') {
|
||
message = '🤝 It\'s a draw!';
|
||
} else if (data.reason === 'opponent_abandoned') {
|
||
message = '🏆 You won! Opponent disconnected';
|
||
}
|
||
|
||
// Update session scores
|
||
if (this.game) {
|
||
let winner = null;
|
||
if (data.reason === 'win' || data.reason === 'opponent_abandoned') {
|
||
winner = this.mySymbol;
|
||
} else if (data.reason === 'loss') {
|
||
winner = this.mySymbol === 'X' ? 'O' : 'X';
|
||
}
|
||
|
||
if (winner) {
|
||
this.game.scores[winner]++;
|
||
this.game.updateScores();
|
||
}
|
||
}
|
||
|
||
// Update stats
|
||
if (data.stats) {
|
||
document.getElementById('playerWins').textContent = data.stats.wins;
|
||
document.getElementById('playerLosses').textContent = data.stats.losses;
|
||
document.getElementById('playerDraws').textContent = data.stats.draws;
|
||
}
|
||
|
||
// Hide surrender button
|
||
const surrenderBtn = document.getElementById('surrenderBtn');
|
||
if (surrenderBtn) {
|
||
surrenderBtn.style.display = 'none';
|
||
}
|
||
|
||
// Show game-over modal with stats
|
||
const modal = document.getElementById('gameOverModal');
|
||
const icon = document.getElementById('gameOverIcon');
|
||
const title = document.getElementById('gameOverTitle');
|
||
const subtitle = document.getElementById('gameOverMessage');
|
||
|
||
if (modal && icon && title && subtitle) {
|
||
|
||
// Highlight winning move if applicable
|
||
if (this.game && (data.reason === 'win' || data.reason === 'loss')) {
|
||
try {
|
||
const move = data.reason === 'win' ? this.lastMyMove : this.lastOpponentMove;
|
||
if (move) {
|
||
// We temporarily set gameActive to true to allow checkWin to run purely for highlighting
|
||
// (checkWin doesn't check gameActive, but just in case)
|
||
// Actually checkWin calculates and calls highlightWinningCells
|
||
this.game.checkWin(move.row, move.col);
|
||
}
|
||
} catch (e) {
|
||
console.error("Error highlighting winning move:", e);
|
||
}
|
||
}
|
||
|
||
// Delay showing the modal for 5 seconds
|
||
setTimeout(() => {
|
||
// Set icon and title based on result
|
||
if (data.reason === 'win' || data.reason === 'opponent_abandoned') {
|
||
icon.textContent = '🏆';
|
||
title.textContent = 'Victory!';
|
||
subtitle.textContent = data.reason === 'opponent_abandoned' ? 'Opponent disconnected' : 'Great game!';
|
||
} else if (data.reason === 'loss' || data.reason === 'surrender') {
|
||
icon.textContent = '😔';
|
||
title.textContent = 'Defeat';
|
||
subtitle.textContent = data.reason === 'surrender' ? 'You surrendered' : 'Better luck next time!';
|
||
} else {
|
||
icon.textContent = '🤝';
|
||
title.textContent = "It's a Draw!";
|
||
subtitle.textContent = 'Well played!';
|
||
}
|
||
|
||
// Update stats in modal
|
||
if (data.stats) {
|
||
document.getElementById('gameOverWins').textContent = data.stats.wins;
|
||
document.getElementById('gameOverLosses').textContent = data.stats.losses;
|
||
document.getElementById('gameOverDraws').textContent = data.stats.draws;
|
||
}
|
||
|
||
// Show modal
|
||
modal.classList.add('active');
|
||
}, 5000);
|
||
}
|
||
}
|
||
|
||
// Handle disconnect
|
||
handleDisconnect() {
|
||
this.showMessage('Disconnected from server. Reconnecting...', 'error');
|
||
|
||
// Try to reconnect
|
||
setTimeout(() => {
|
||
if (!this.socket || !this.socket.connected) {
|
||
this.connect();
|
||
}
|
||
}, 2000);
|
||
}
|
||
|
||
// Return to lobby
|
||
returnToLobby() {
|
||
this.isMultiplayer = false;
|
||
this.currentGameId = null;
|
||
document.getElementById('multiplayerPanel').style.display = 'block';
|
||
document.getElementById('gameControls').style.display = 'block'; // Or grid/none depending on logic, but block/grid usually toggled by logic
|
||
|
||
// Hide identity section in local play
|
||
const identitySection = document.getElementById('playerIdentitySection');
|
||
if (identitySection) identitySection.style.display = 'none';
|
||
|
||
this.socket.emit('request_active_players');
|
||
}
|
||
|
||
// Show message to user
|
||
showMessage(text, type = 'info') {
|
||
const messageEl = document.getElementById('statusMessage');
|
||
if (messageEl) {
|
||
messageEl.textContent = text;
|
||
messageEl.className = `status-text-small ${type}`;
|
||
}
|
||
}
|
||
|
||
// Handle rematch request from opponent
|
||
handleRematchRequest(data) {
|
||
const notification = document.createElement('div');
|
||
notification.className = 'challenge-notification';
|
||
notification.innerHTML = `
|
||
<div class="challenge-content">
|
||
<h3>Rematch Request!</h3>
|
||
<p><strong>${data.from}</strong> wants a rematch</p>
|
||
<p>Board size: ${data.boardSize}×${data.boardSize}</p>
|
||
<div class="challenge-actions">
|
||
<button class="accept-btn" onclick="acceptRematchFromNotification('${data.rematchId}')">
|
||
Accept
|
||
</button>
|
||
<button class="decline-btn" onclick="declineRematchFromNotification('${data.rematchId}')">
|
||
Decline
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
document.body.appendChild(notification);
|
||
setTimeout(() => notification.classList.add('active'), 10);
|
||
}
|
||
}
|
||
|
||
// Global helper functions for rematch from notifications
|
||
function acceptRematchFromNotification(rematchId) {
|
||
if (window.multiplayerClient && window.multiplayerClient.socket) {
|
||
window.multiplayerClient.socket.emit('accept_rematch', { rematchId });
|
||
|
||
// Hide game over modal if visible
|
||
const modal = document.getElementById('gameOverModal');
|
||
if (modal) {
|
||
modal.classList.remove('active');
|
||
}
|
||
|
||
// Remove notification
|
||
const notifications = document.querySelectorAll('.challenge-notification');
|
||
notifications.forEach(n => n.remove());
|
||
}
|
||
}
|
||
|
||
function declineRematchFromNotification(rematchId) {
|
||
if (window.multiplayerClient && window.multiplayerClient.socket) {
|
||
window.multiplayerClient.socket.emit('decline_rematch', { rematchId });
|
||
|
||
// Remove notification
|
||
const notifications = document.querySelectorAll('.challenge-notification');
|
||
notifications.forEach(n => n.remove());
|
||
}
|
||
}
|
||
|
||
// Initialize multiplayer client (will be used by game.js)
|
||
// Initialize multiplayer client (will be used by game.js)
|
||
window.multiplayerClient = null;
|