import { useState, useEffect } from 'react'; import { ArrowLeft, Phone, Play, Pause, Trophy, Users, Crown, Copy, Volume2, UserMinus } from 'lucide-react'; import Ticket from './Ticket'; import NumberBoard from './NumberBoard'; import Chat from './Chat'; import { GAME_MODES, checkTicketWin, playSound, calculatePlayerProgress } from '../utils/gameUtils'; function Game({ socket, playerData, roomData, playerTicket, onLeaveRoom, initialCalledNumbers = [], initialCurrentNumber = null, initialGameMode = null, isConnected = true, connectionHealth = 'good', reconnectAttempts = 0, onShowResults }) { const [localTicket, setLocalTicket] = useState(null); const [calledNumbers, setCalledNumbers] = useState(initialCalledNumbers); const [currentNumber, setCurrentNumber] = useState(initialCurrentNumber); const [gameMode, setGameMode] = useState(initialGameMode || roomData?.gameMode); // NEW: Multi-mode support const [activeGameModes, setActiveGameModes] = useState(roomData?.activeGameModes || null); const [winnersByMode, setWinnersByMode] = useState(roomData?.winnersByMode || {}); const [claimableWins, setClaimableWins] = useState({}); // Track which modes the player can claim const [autoCall, setAutoCall] = useState(false); const [autoCallInterval, setAutoCallInterval] = useState(10); // Default 10 seconds const [showIntervalSetting, setShowIntervalSetting] = useState(false); const [canClaimWin, setCanClaimWin] = useState(false); const [winners, setWinners] = useState([]); const [celebrationWinner, setCelebrationWinner] = useState(null); const [toastNotifications, setToastNotifications] = useState([]); const [notificationTimeouts, setNotificationTimeouts] = useState(new Map()); const [showRankingPanel, setShowRankingPanel] = useState(true); const [rankingPulse, setRankingPulse] = useState(false); const [requiredWinners, setRequiredWinners] = useState(3); const [showRoomCode, setShowRoomCode] = useState(false); const [playerProgress, setPlayerProgress] = useState(new Map()); // Store other players' progress // HOST SUPERPOWERS - New state variables const [isPaused, setIsPaused] = useState(false); const [showPlayerManagement, setShowPlayerManagement] = useState(false); const [showTransferHost, setShowTransferHost] = useState(false); const [showKickConfirm, setShowKickConfirm] = useState(false); const [selectedPlayerToKick, setSelectedPlayerToKick] = useState(null); const [selectedPlayerForHost, setSelectedPlayerForHost] = useState(null); const [hostActionLoading, setHostActionLoading] = useState(false); const isHost = roomData?.players?.find(p => p.id === playerData?.playerId)?.isHost; const currentPlayer = roomData?.players?.find(p => p.id === playerData?.playerId); const connectedPlayersCount = roomData?.players?.filter(p => p.isConnected).length || 0; const isGamePausedForPlayers = (roomData?.gameState === 'playing' && connectedPlayersCount < 2); // NEW: Check if this is a multi-mode game const isMultiMode = activeGameModes && Array.isArray(activeGameModes) && activeGameModes.length > 0; // In multi-mode games, only consider player "fully won" if all modes are claimed or game is complete // In single-mode games, hasWon means they can't compete anymore const hasWon = isMultiMode ? (currentPlayer?.hasWon && Object.keys(winnersByMode || {}).length === (activeGameModes || []).length) // All modes claimed in multi-mode : (currentPlayer?.hasWon || winners.some(w => w.player.id === playerData?.playerId)); // Traditional single-mode logic // Helper function for adding notifications with proper cleanup const addNotification = (message, type = 'info', duration = 3000) => { const toastId = Date.now(); // Clear any existing timeout for similar messages setNotificationTimeouts(prev => { const existingTimeout = prev.get(message); if (existingTimeout) { clearTimeout(existingTimeout); } return new Map(prev); }); // Remove any existing notification with same message to prevent duplicates setToastNotifications(prev => prev.filter(t => t.message !== message)); setToastNotifications(prev => [...prev, { id: toastId, message, type }]); // Auto-remove toast after specified duration const timeoutId = setTimeout(() => { setToastNotifications(prev => prev.filter(t => t.id !== toastId)); setNotificationTimeouts(prev => { const newMap = new Map(prev); newMap.delete(message); return newMap; }); }, duration); // Store timeout for potential cleanup setNotificationTimeouts(prev => new Map(prev.set(message, timeoutId))); return { toastId, timeoutId }; }; // Send initial progress when ticket is ready useEffect(() => { if (localTicket && socket && playerData) { if (isMultiMode && activeGameModes && activeGameModes.length > 0) { // Send progress for each active game mode in multi-mode activeGameModes.forEach(mode => { const progress = calculatePlayerProgress(localTicket, mode, calledNumbers); socket.emit('updatePlayerProgress', { playerId: playerData.playerId, gameMode: mode, progress }); }); } else if (gameMode) { // Single mode progress const progress = calculatePlayerProgress(localTicket, gameMode, calledNumbers); socket.emit('updatePlayerProgress', { playerId: playerData.playerId, gameMode: gameMode, progress }); } } }, [localTicket, socket, playerData, gameMode, isMultiMode, activeGameModes, calledNumbers]); // Sync prop ticket to local state useEffect(() => { if (playerTicket && !localTicket) { setLocalTicket(playerTicket); } }, [playerTicket, localTicket]); // NEW: Sync multi-mode data from roomData useEffect(() => { if (roomData?.activeGameModes) { setActiveGameModes(roomData.activeGameModes); } if (roomData?.winnersByMode) { setWinnersByMode(roomData.winnersByMode); } }, [roomData?.activeGameModes, roomData?.winnersByMode]); // Sync initial game state for rejoining players useEffect(() => { if (initialCalledNumbers.length > 0) { setCalledNumbers(initialCalledNumbers); } if (initialCurrentNumber) { setCurrentNumber(initialCurrentNumber); } if (initialGameMode) { setGameMode(initialGameMode); } }, [initialCalledNumbers, initialCurrentNumber, initialGameMode]); // Initialize auto-call interval from room data useEffect(() => { if (roomData?.autoCallInterval) { setAutoCallInterval(roomData.autoCallInterval); } }, [roomData?.autoCallInterval]); useEffect(() => { if (!socket) return; // Connection recovery - request state sync on reconnection const handleReconnection = () => { if (playerData && roomData) { socket.emit('requestFullSync', { roomCode: roomData.code, playerId: playerData.playerId }); } }; // Listen for reconnection socket.on('connect', handleReconnection); // Handle full game state sync socket.on('fullGameSync', (syncData) => { if (syncData.calledNumbers) { setCalledNumbers(syncData.calledNumbers); } if (syncData.currentNumber) { setCurrentNumber(syncData.currentNumber); } if (syncData.autoCallStatus !== undefined) { setAutoCall(syncData.autoCallStatus); } // Acknowledge the sync if (syncData.timestamp) { socket.emit('messageAck', { messageId: `sync_${syncData.timestamp}` }); } }); socket.on('numberCalled', (data) => { setCalledNumbers(data.calledNumbers); setCurrentNumber(data.number); playSound('number'); // Acknowledge critical message if required if (data.requireAck && data.messageId) { socket.emit('messageAck', { messageId: data.messageId }); } }); socket.on('autoCallToggled', (data) => { setAutoCall(data.autoCall); }); socket.on('playerWon', (data) => { // Someone won - show immediate feedback playSound('win'); // Note: We don't show toast notification here because the 'rankingUpdated' event // that follows will handle the visual celebration to prevent duplicate notifications }); socket.on('rankingUpdated', (data) => { const prevWinnersCount = winners.length; const newWinners = data.winners; setWinners(newWinners); // Update required winners count if (data.requiredWinners) setRequiredWinners(data.requiredWinners); // Show celebration for new winner if (newWinners.length > prevWinnersCount) { const newWinner = newWinners[newWinners.length - 1]; const rank = newWinners.length; // Pulse the ranking panel to draw attention setRankingPulse(true); setTimeout(() => setRankingPulse(false), 3000); // Show full-screen celebration only for the winning player if (newWinner.player.id === playerData?.playerId) { setCelebrationWinner({ ...newWinner, rank, isYou: true }); // Auto-hide celebration after 4 seconds setTimeout(() => { setCelebrationWinner(null); }, 4000); } } playSound('win'); }); socket.on('verificationVoteReceived', (data) => { // Handle verification vote updates - keeping for future use }); socket.on('gameResumed', (data) => { setIsPaused(false); // Clear any existing resume notifications first setToastNotifications(prev => prev.filter(t => !t.message.includes('resumed') && !t.message.includes('Auto-call') )); if (data.resumedBy) { addNotification(`Game resumed by ${data.resumedBy}`, 'success', 3000); if (data.autoCallResumed) { // Add a small delay to prevent notification overlap setTimeout(() => { addNotification('Auto-call has been resumed', 'info', 2000); }, 100); } } else if (data.gameState === 'playing') { // Only show notification if multiple players rejoined (important event) if (data.message && data.message.includes('reconnected')) { addNotification(data.message, 'success', 3000); } } }); socket.on('gamePaused', (data) => { setIsPaused(true); if (data.pausedBy) { addNotification(`Game paused by ${data.pausedBy}`, 'warning', 4000); } else if (data.message && !data.message.includes('waiting for') && !data.message.includes('minimum')) { // Only show notification for important pause events (not frequent disconnects) addNotification(data.message, 'warning', 4000); } }); // HOST SUPERPOWERS - New socket listeners socket.on('playerKicked', (data) => { // Check if this player was kicked if (data.playerId === playerData?.playerId) { addNotification(`You were removed from the game by ${data.kickedBy}`, 'error', 5000); // Redirect to home after a short delay setTimeout(() => { onLeaveRoom(); }, 2000); } }); socket.on('hostTransferred', (data) => { if (data.newHost.id === playerData?.playerId) { addNotification(`You are now the host!`, 'success', 4000); } else { addNotification(`${data.newHost.nickname} is now the host`, 'info', 3000); } }); socket.on('playerProgressUpdated', (data) => { // Update the stored progress for other players if (data.playerId !== playerData?.playerId) { setPlayerProgress(prev => { const newProgress = new Map(prev); const playerProgress = newProgress.get(data.playerId) || {}; playerProgress[data.gameMode] = data.progress; newProgress.set(data.playerId, playerProgress); return newProgress; }); } }); socket.on('autoCallIntervalChanged', (data) => { setAutoCallInterval(data.interval); // Only show notification if you're the host (you changed it) if (playerData?.isHost) { const toastId = Date.now(); setToastNotifications(prev => [...prev, { id: toastId, message: `Auto-call interval set to ${data.interval} seconds`, type: 'success' }]); // Auto-remove toast after 2 seconds setTimeout(() => { setToastNotifications(prev => prev.filter(t => t.id !== toastId)); }, 2000); } }); // NEW: Multi-mode specific win events - PATTERN COMPLETION socket.on('playerWonMode', (data) => { // Only show notification if it's the current player who won if (data.playerId === playerData?.playerId) { // Player themselves won a pattern playSound('win'); const toastId = Date.now(); const patternName = GAME_MODES[data.gameMode]?.name || data.gameMode; const rankDisplay = data.modeRank === 1 ? '1st' : data.modeRank === 2 ? '2nd' : '3rd'; // FIXED: Pattern completion notification (not overall ranking) setToastNotifications(prev => [...prev, { id: toastId, message: `đ¯ You completed ${patternName}!`, type: 'success', patternWin: true, gameMode: data.gameMode, modeRank: data.modeRank }]); // Auto-remove toast after 4 seconds setTimeout(() => { setToastNotifications(prev => prev.filter(t => t.id !== toastId)); }, 4000); } // Don't show notifications for other players winning patterns to reduce spam }); socket.on('multiModeRankingUpdated', (data) => { const prevWinnersCount = winners.length; const newWinners = data.winners; setWinners(newWinners); // Update winners by mode if (data.winnersByMode) { setWinnersByMode(data.winnersByMode); } // Update required winners count if (data.room?.players) { const totalPlayers = data.room.players.length; const requiredWinners = totalPlayers === 2 ? 2 : Math.min(3, totalPlayers); setRequiredWinners(requiredWinners); } // FIXED: Show celebration ONLY for new OVERALL winner (when someone achieves final rank) if (newWinners.length > prevWinnersCount) { // Find the actually NEW winner by comparing with previous winners // BUG FIX: Don't use newWinners[length-1] because the array is sorted by rank const newWinner = newWinners.find(winner => !winners.some(prevWinner => prevWinner.player.id === winner.player.id && prevWinner.gameMode === winner.gameMode ) ); if (!newWinner) { console.log('đ No new winner found in multiModeRankingUpdated'); return; } // For multi-mode games, use overallRank (based on host's mode selection order) const rank = newWinner.overallRank || newWinners.length; console.log(`đ Multi-mode celebration: Player ${newWinner.player.nickname}, overallRank: ${newWinner.overallRank}, completionOrder: ${newWinner.completionOrder}, using rank: ${rank}`); // Pulse the ranking panel to draw attention setRankingPulse(true); setTimeout(() => setRankingPulse(false), 3000); // Show full-screen celebration only for the winning player (OVERALL final rank achievement) if (newWinner.player.id === playerData?.playerId) { setCelebrationWinner({ ...newWinner, rank, isYou: true, isPatternWin: false // This is overall ranking win }); // FIXED: Also show ranking notification const rankTitle = rank === 1 ? 'Champion đ' : rank === 2 ? '2nd Place đĨ' : '3rd Place đĨ'; const toastId = Date.now(); setToastNotifications(prev => [...prev, { id: toastId, message: `đ You achieved ${rankTitle}!`, type: 'success', rank: rank, isOverallRanking: true }]); // Auto-remove toast after 5 seconds setTimeout(() => { setToastNotifications(prev => prev.filter(t => t.id !== toastId)); }, 5000); // Auto-hide celebration after 4 seconds setTimeout(() => { setCelebrationWinner(null); }, 4000); } else { // For other players' overall wins, show a simple notification const championTitle = rank === 1 ? 'Champion đ' : rank === 2 ? '2nd Place đĨ' : '3rd Place đĨ'; addNotification( `đ ${newWinner.player.nickname} achieved ${championTitle}!`, 'info', 4000 ); } } playSound('win'); }); // Handle claim errors socket.on('error', (data) => { // Show error toast notification const toastId = Date.now(); setToastNotifications(prev => [...prev, { id: toastId, message: data.message, type: 'error' }]); // Auto-remove toast after 5 seconds setTimeout(() => { setToastNotifications(prev => prev.filter(t => t.id !== toastId)); }, 5000); }); return () => { socket.off('connect'); socket.off('fullGameSync'); socket.off('numberCalled'); socket.off('autoCallToggled'); socket.off('rankingUpdated'); socket.off('playerWonMode'); socket.off('multiModeRankingUpdated'); socket.off('verificationVoteReceived'); socket.off('gameResumed'); socket.off('gamePaused'); socket.off('playerProgressUpdated'); socket.off('autoCallIntervalChanged'); socket.off('error'); // HOST SUPERPOWERS - cleanup socket.off('playerKicked'); socket.off('hostTransferred'); socket.off('numberReplayed'); }; }, [socket, gameMode]); // HOST SUPERPOWERS - Click outside handler for player management dropdown useEffect(() => { const handleClickOutside = (event) => { // Only trigger if the dropdown is actually open and the click is outside if (showPlayerManagement && !event.target.closest('.player-management-dropdown') && !event.target.closest('[data-player-management-trigger]')) { setShowPlayerManagement(false); } }; // Only add listener when dropdown is open if (showPlayerManagement) { document.addEventListener('mousedown', handleClickOutside); return () => { document.removeEventListener('mousedown', handleClickOutside); }; } }, [showPlayerManagement]); // Sync local isPaused state with room's gameState and player count useEffect(() => { if (roomData?.gameState === 'paused' || isGamePausedForPlayers) { setIsPaused(true); } else if (roomData?.gameState === 'playing' && !isGamePausedForPlayers) { setIsPaused(false); } }, [roomData?.gameState, isGamePausedForPlayers]); // Check win condition whenever ticket or called numbers change useEffect(() => { // In multi-mode games, allow checking for claims even if player has won one mode // In single-mode games, don't check if player has already won if (localTicket && calledNumbers.length > 0 && (!hasWon || isMultiMode)) { if (isMultiMode && activeGameModes && activeGameModes.length > 0) { // Check which modes the player can claim wins for const newClaimableWins = {}; let hasAnyClaimableWin = false; activeGameModes.forEach(mode => { const canWin = checkTicketWin(localTicket, mode, calledNumbers); const hasAlreadyWonThisMode = winnersByMode[mode]?.some(w => w.player.id === playerData?.playerId); const modeAlreadyClaimed = winnersByMode[mode] && winnersByMode[mode].length > 0; // Check if ANY player has claimed this mode if (canWin && !hasAlreadyWonThisMode && !modeAlreadyClaimed) { newClaimableWins[mode] = true; hasAnyClaimableWin = true; } }); setClaimableWins(newClaimableWins); setCanClaimWin(hasAnyClaimableWin); } else if (gameMode) { // Single mode win checking const canWin = checkTicketWin(localTicket, gameMode, calledNumbers); setCanClaimWin(canWin); } } else { setCanClaimWin(false); setClaimableWins({}); } }, [localTicket, calledNumbers, gameMode, hasWon, isMultiMode, activeGameModes, winnersByMode, playerData?.playerId]); // Calculate initial required winners based on room data useEffect(() => { if (roomData?.players) { const totalPlayers = roomData.players.length; const initialRequiredWinners = totalPlayers === 2 ? 2 : Math.min(3, totalPlayers); setRequiredWinners(initialRequiredWinners); } }, [roomData?.players?.length]); const handleMarkNumber = (number, rowIndex, colIndex, shouldMark) => { if (!localTicket) return; // In multi-mode games, allow marking even if player has won one mode (they can still compete for others) // In single-mode games, prevent marking if player has already won if (!isMultiMode && hasWon) return; // Only allow marking, not unmarking if (!shouldMark) return; // Create a new ticket with the updated marking for immediate UI feedback const newTicket = localTicket.map((row, rIdx) => row.map((cell, cIdx) => { if (rIdx === rowIndex && cIdx === colIndex) { if (cell === null) return null; const cellNumber = typeof cell === 'object' ? cell.number : cell; if (cellNumber === number) { return { number, marked: true }; } } return cell; }) ); setLocalTicket(newTicket); // Send the manual marking to server if (socket) { socket.emit('manualMark', { number, rowIndex, colIndex, marked: true }); } // Check win condition after manual marking if (isMultiMode && activeGameModes && activeGameModes.length > 0) { // Check each active game mode const newClaimableWins = {}; let hasAnyClaimableWin = false; activeGameModes.forEach(mode => { const canWin = checkTicketWin(newTicket, mode, calledNumbers); const hasAlreadyWonThisMode = winnersByMode[mode]?.some(w => w.player.id === playerData?.playerId); const modeAlreadyClaimed = winnersByMode[mode] && winnersByMode[mode].length > 0; // Check if ANY player has claimed this mode if (canWin && !hasAlreadyWonThisMode && !modeAlreadyClaimed) { newClaimableWins[mode] = true; hasAnyClaimableWin = true; } }); setClaimableWins(newClaimableWins); setCanClaimWin(hasAnyClaimableWin); } else if (gameMode) { const canWin = checkTicketWin(newTicket, gameMode, calledNumbers); setCanClaimWin(canWin); } // Update and broadcast player progress if (socket && playerData) { if (isMultiMode && activeGameModes && activeGameModes.length > 0) { // Send progress for each active game mode activeGameModes.forEach(mode => { const progress = calculatePlayerProgress(newTicket, mode, calledNumbers); socket.emit('updatePlayerProgress', { playerId: playerData.playerId, gameMode: mode, progress }); }); } else if (gameMode) { const progress = calculatePlayerProgress(newTicket, gameMode, calledNumbers); socket.emit('updatePlayerProgress', { playerId: playerData.playerId, gameMode: gameMode, progress }); } } }; const copyRoomCode = async () => { try { await navigator.clipboard.writeText(roomData?.code || ''); setShowRoomCode(true); setTimeout(() => setShowRoomCode(false), 2000); } catch (err) { // Fallback for older browsers const textArea = document.createElement('textarea'); textArea.value = roomData?.code || ''; document.body.appendChild(textArea); textArea.select(); document.execCommand('copy'); document.body.removeChild(textArea); setShowRoomCode(true); setTimeout(() => setShowRoomCode(false), 2000); } }; const announceCurrentNumber = () => { if (currentNumber) { playSound('number'); // Try to use speech synthesis if available if ('speechSynthesis' in window) { const utterance = new SpeechSynthesisUtterance(`${currentNumber}`); utterance.rate = 0.8; utterance.volume = 0.7; window.speechSynthesis.speak(utterance); } } }; const handleCallNumber = () => { if (socket && isHost) { socket.emit('callNumber'); } }; const handleToggleAutoCall = () => { if (socket && isHost) { socket.emit('toggleAutoCall'); } }; const handleClaimWin = (specificMode = null) => { if (socket && canClaimWin) { if (isMultiMode && specificMode) { // Claim win for specific mode in multi-mode game socket.emit('claimWinForMode', { gameMode: specificMode }); playSound('win'); } else { // Traditional single-mode claim or claim all available in multi-mode socket.emit('claimWin'); playSound('win'); } } }; // NEW: Helper function to claim win for specific mode const handleClaimModeWin = (gameMode) => { if (socket && claimableWins[gameMode]) { socket.emit('claimWinForMode', { gameMode }); playSound('win'); } }; const handleSetAutoCallInterval = (seconds) => { if (socket && isHost && seconds >= 5 && seconds <= 60) { socket.emit('setAutoCallInterval', { seconds }); setShowIntervalSetting(false); } }; // HOST SUPERPOWERS - New handler functions const handlePauseGame = () => { if (socket && isHost && !hostActionLoading) { setHostActionLoading(true); socket.emit('pauseGame'); setTimeout(() => setHostActionLoading(false), 1000); } }; const handleResumeGame = () => { if (socket && isHost && !hostActionLoading) { // Check if there are enough players to resume if (connectedPlayersCount < 2) { addNotification('Cannot resume - need at least 2 players connected', 'error', 3000); return; } setHostActionLoading(true); socket.emit('resumeGame'); setTimeout(() => setHostActionLoading(false), 1000); } }; const handleKickPlayer = (playerId) => { if (socket && isHost && playerId && !hostActionLoading) { setHostActionLoading(true); socket.emit('kickPlayer', { targetPlayerId: playerId }); setShowKickConfirm(false); setSelectedPlayerToKick(null); setTimeout(() => setHostActionLoading(false), 1000); } }; const handleTransferHost = (playerId) => { if (socket && isHost && playerId && !hostActionLoading) { setHostActionLoading(true); socket.emit('transferHost', { targetPlayerId: playerId }); setShowTransferHost(false); setSelectedPlayerForHost(null); setTimeout(() => setHostActionLoading(false), 1000); } }; const openKickConfirmation = (player) => { setSelectedPlayerToKick(player); setShowKickConfirm(true); setShowPlayerManagement(false); }; const openTransferHostDialog = () => { setShowTransferHost(true); setShowPlayerManagement(false); }; const confirmKickPlayer = () => { if (!selectedPlayerToKick || !socket) return; setHostActionLoading(true); socket.emit('kickPlayer', { targetPlayerId: selectedPlayerToKick.id }); setTimeout(() => { setHostActionLoading(false); setShowKickConfirm(false); setSelectedPlayerToKick(null); }, 1000); }; const confirmTransferHost = () => { if (!selectedPlayerForHost || !socket) return; setHostActionLoading(true); socket.emit('transferHost', { targetPlayerId: selectedPlayerForHost }); setTimeout(() => { setHostActionLoading(false); setShowTransferHost(false); setSelectedPlayerForHost(null); }, 1000); }; return (
{isMultiMode ? `Playing: ${(activeGameModes || []).map(mode => GAME_MODES[mode]?.name).join(', ')}` : GAME_MODES[gameMode]?.description }
{/* Multi-mode winner count display */} {isMultiMode ? ({celebrationWinner.isYou ? 'You completed' : 'Completed'}{' '} {celebrationWinner.patternName}
{celebrationWinner.isYou && (Great job! Continue playing for more patterns! đ
)} > ) : ( <>{celebrationWinner.isYou ? 'You achieved' : 'Achieved'}{' '} {celebrationWinner.rank === 1 ? '1st Place' : celebrationWinner.rank === 2 ? '2nd Place' : '3rd Place'}
{celebrationWinner.isYou && (Amazing job! Your victory is now secured! đ
)} > )} {!celebrationWinner.isYou && winners.length < requiredWinners && (Keep playing for remaining positions!
)}Are you sure you want to kick {selectedPlayerToKick.nickname} from the game?
Choose a player to transfer host role to: