Add connection status component and improve socket reconnection handling
All checks were successful
Publish Docker image / Push Docker image to Docker Hub (push) Successful in 2m20s
All checks were successful
Publish Docker image / Push Docker image to Docker Hub (push) Successful in 2m20s
This commit is contained in:
parent
f2712bdcec
commit
301e08b6e6
@ -7,6 +7,7 @@ import HomeScreen from "./components/HomeScreen";
|
||||
import SongSubmissionScreen from "./components/SongSubmissionScreen";
|
||||
import VotingScreen from "./components/VotingScreen";
|
||||
import ResultsScreen from "./components/ResultsScreen";
|
||||
import ConnectionStatus from "./components/ConnectionStatus";
|
||||
|
||||
const App = () => {
|
||||
const [cursorPos, setCursorPos] = useState({x: 0, y: 0});
|
||||
@ -84,12 +85,6 @@ const App = () => {
|
||||
</div>
|
||||
|
||||
<div className="content-container">
|
||||
{!isConnected && (
|
||||
<div className="connection-status">
|
||||
<p>Connecting to server...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="error-message">
|
||||
<p>{error}</p>
|
||||
@ -97,6 +92,9 @@ const App = () => {
|
||||
)}
|
||||
|
||||
{renderGameScreen()}
|
||||
|
||||
{/* Show connection status component */}
|
||||
<ConnectionStatus />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
55
client/src/common/styles/components/connection-status.sass
Normal file
55
client/src/common/styles/components/connection-status.sass
Normal file
@ -0,0 +1,55 @@
|
||||
// connection-status.sass - Component for displaying network connection status
|
||||
|
||||
.connection-status
|
||||
position: fixed
|
||||
bottom: 0
|
||||
left: 0
|
||||
right: 0
|
||||
padding: 0.5rem 1rem
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: center
|
||||
gap: 0.5rem
|
||||
z-index: 1000
|
||||
transition: all 0.3s ease
|
||||
animation: slideUp 0.3s forwards
|
||||
font-family: 'Press Start 2P', monospace
|
||||
font-size: 0.7rem
|
||||
|
||||
&.connected
|
||||
background-color: rgba($success, 0.9)
|
||||
color: #fff
|
||||
animation: slideUp 0.3s forwards, fadeOut 0.5s 2.5s forwards
|
||||
|
||||
&.disconnected
|
||||
background-color: rgba(#f44336, 0.9)
|
||||
color: #fff
|
||||
|
||||
&.reconnecting
|
||||
background-color: rgba(#ff9800, 0.9)
|
||||
color: #fff
|
||||
|
||||
&.offline
|
||||
background-color: rgba(#f44336, 0.9)
|
||||
color: #fff
|
||||
|
||||
.connection-icon
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: center
|
||||
|
||||
.connection-message
|
||||
text-align: center
|
||||
|
||||
@keyframes slideUp
|
||||
from
|
||||
transform: translateY(100%)
|
||||
to
|
||||
transform: translateY(0)
|
||||
|
||||
@keyframes fadeOut
|
||||
from
|
||||
opacity: 1
|
||||
to
|
||||
opacity: 0
|
||||
transform: translateY(100%)
|
@ -253,11 +253,46 @@
|
||||
.voting-actions
|
||||
display: flex
|
||||
justify-content: center
|
||||
flex-direction: column
|
||||
align-items: center
|
||||
margin: 2rem 0
|
||||
|
||||
.btn
|
||||
min-width: 180px
|
||||
font-size: 1rem
|
||||
|
||||
&.offline
|
||||
background-color: $secondary
|
||||
position: relative
|
||||
|
||||
.offline-notice
|
||||
margin-top: 1rem
|
||||
padding: 0.5rem
|
||||
background-color: rgba($secondary, 0.2)
|
||||
border: 2px solid $secondary
|
||||
max-width: 400px
|
||||
text-align: center
|
||||
font-size: 0.7rem
|
||||
|
||||
svg
|
||||
margin-right: 0.5rem
|
||||
color: $secondary
|
||||
|
||||
.offline-vote-status
|
||||
margin-top: 1rem
|
||||
padding: 0.5rem
|
||||
background-color: rgba($success, 0.2)
|
||||
border: 2px solid $success
|
||||
max-width: 400px
|
||||
text-align: center
|
||||
font-size: 0.7rem
|
||||
color: $success
|
||||
animation: pulse-opacity 2s infinite
|
||||
|
||||
&.error
|
||||
background-color: rgba(#f44336, 0.2)
|
||||
border-color: #f44336
|
||||
color: #f44336
|
||||
|
||||
// Voting status and information
|
||||
.voting-status
|
||||
@ -274,6 +309,17 @@
|
||||
font-size: 0.9rem
|
||||
color: $text
|
||||
|
||||
.reconnecting-notice
|
||||
margin: 0.5rem auto 1rem auto
|
||||
padding: 0.5rem
|
||||
background-color: rgba($secondary, 0.2)
|
||||
border: 2px solid $secondary
|
||||
max-width: 400px
|
||||
text-align: center
|
||||
font-size: 0.7rem
|
||||
color: $secondary
|
||||
animation: pulse-opacity 2s infinite
|
||||
|
||||
.auto-advance-notice
|
||||
margin: 1rem auto
|
||||
max-width: 400px
|
||||
@ -297,6 +343,11 @@
|
||||
color: $primary
|
||||
font-weight: bold
|
||||
|
||||
.offline-badge
|
||||
color: $secondary
|
||||
margin-left: 0.5rem
|
||||
font-size: 0.7rem
|
||||
|
||||
// Player votes list styling
|
||||
.player-votes
|
||||
background-color: rgba(0, 0, 0, 0.2)
|
||||
@ -368,6 +419,21 @@
|
||||
background-color: rgba(255, 255, 255, 0.05)
|
||||
border-color: rgba(255, 255, 255, 0.1)
|
||||
opacity: 0.7
|
||||
|
||||
&.offline-voted
|
||||
background-color: rgba($secondary, 0.1)
|
||||
border-color: rgba($secondary, 0.5)
|
||||
|
||||
.offline-icon
|
||||
color: $secondary
|
||||
margin-left: 0.5rem
|
||||
animation: pixel-pulse 1.5s infinite
|
||||
|
||||
@keyframes pulse-opacity
|
||||
0%, 100%
|
||||
opacity: 1
|
||||
50%
|
||||
opacity: 0.5
|
||||
|
||||
@keyframes pixel-blink
|
||||
0%, 100%
|
||||
|
@ -12,6 +12,7 @@
|
||||
@import './components/youtube-embed'
|
||||
@import './components/youtube-search'
|
||||
@import './components/song-form-overlay'
|
||||
@import './components/connection-status'
|
||||
|
||||
// Global styles
|
||||
html, body
|
||||
|
70
client/src/components/ConnectionStatus.jsx
Normal file
70
client/src/components/ConnectionStatus.jsx
Normal file
@ -0,0 +1,70 @@
|
||||
// ConnectionStatus.jsx
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useSocket } from '../context/GameContext';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faWifi, faExclamationTriangle, faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
function ConnectionStatus() {
|
||||
const { socket, isConnected, isReconnecting, reconnectionAttempts } = useSocket();
|
||||
const [showStatus, setShowStatus] = useState(false);
|
||||
const [offline, setOffline] = useState(!navigator.onLine);
|
||||
|
||||
// Monitor browser's online/offline status
|
||||
useEffect(() => {
|
||||
const handleOnline = () => setOffline(false);
|
||||
const handleOffline = () => setOffline(true);
|
||||
|
||||
window.addEventListener('online', handleOnline);
|
||||
window.addEventListener('offline', handleOffline);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('online', handleOnline);
|
||||
window.removeEventListener('offline', handleOffline);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Always show status when disconnected, hide after successful connection
|
||||
useEffect(() => {
|
||||
if (!isConnected || isReconnecting || offline) {
|
||||
setShowStatus(true);
|
||||
} else {
|
||||
// Hide the status indicator after a delay
|
||||
const timer = setTimeout(() => {
|
||||
setShowStatus(false);
|
||||
}, 3000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isConnected, isReconnecting, offline]);
|
||||
|
||||
if (!showStatus) return null;
|
||||
|
||||
let statusClass = 'connection-status';
|
||||
let icon, message;
|
||||
|
||||
if (offline) {
|
||||
statusClass += ' offline';
|
||||
icon = <FontAwesomeIcon icon={faExclamationTriangle} />;
|
||||
message = 'Keine Internetverbindung. Spielstand wird lokal gespeichert.';
|
||||
} else if (!isConnected) {
|
||||
statusClass += ' disconnected';
|
||||
icon = <FontAwesomeIcon icon={faExclamationTriangle} />;
|
||||
message = 'Verbindung zum Server verloren. Versuche neu zu verbinden...';
|
||||
} else if (isReconnecting) {
|
||||
statusClass += ' reconnecting';
|
||||
icon = <FontAwesomeIcon icon={faSpinner} spin />;
|
||||
message = `Verbindungsversuch ${reconnectionAttempts}...`;
|
||||
} else {
|
||||
statusClass += ' connected';
|
||||
icon = <FontAwesomeIcon icon={faWifi} />;
|
||||
message = 'Verbunden mit dem Server';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={statusClass}>
|
||||
<div className="connection-icon">{icon}</div>
|
||||
<div className="connection-message">{message}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConnectionStatus;
|
@ -2,15 +2,16 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useGame } from '../context/GameContext';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faVoteYea, faTrophy, faMusic, faCheck, faMedal, faCrown } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faVoteYea, faTrophy, faMusic, faCheck, faMedal, faCrown, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
|
||||
import YouTubeEmbed from './YouTubeEmbed';
|
||||
|
||||
function VotingScreen() {
|
||||
const { lobby, currentPlayer, submitVote, isHost } = useGame();
|
||||
const { lobby, currentPlayer, submitVote, isHost, isOffline, isReconnecting } = useGame();
|
||||
const [hasVoted, setHasVoted] = useState(false);
|
||||
const [selectedSong, setSelectedSong] = useState(null);
|
||||
const [countdown, setCountdown] = useState(null);
|
||||
const [processingByeAdvance, setProcessingByeAdvance] = useState(false);
|
||||
const [offlineVoteStatus, setOfflineVoteStatus] = useState(null);
|
||||
|
||||
// Hole aktuellen Kampf
|
||||
const battle = lobby?.currentBattle || null;
|
||||
@ -39,10 +40,10 @@ function VotingScreen() {
|
||||
useEffect(() => {
|
||||
if (battle && battle.votes && currentPlayer) {
|
||||
// Prüfe, ob die ID des Spielers im Stimmen-Objekt existiert
|
||||
// Da Stimmen als Objekt, nicht als Map gesendet werden
|
||||
const votesObj = battle.votes || {};
|
||||
setHasVoted(Object.prototype.hasOwnProperty.call(votesObj, currentPlayer.id) ||
|
||||
Object.keys(votesObj).includes(currentPlayer.id));
|
||||
setHasVoted(
|
||||
Object.prototype.hasOwnProperty.call(battle.votes, currentPlayer.id) ||
|
||||
battle.votes[currentPlayer.id] !== undefined
|
||||
);
|
||||
} else {
|
||||
setHasVoted(false);
|
||||
}
|
||||
@ -59,8 +60,41 @@ function VotingScreen() {
|
||||
const handleSubmitVote = async () => {
|
||||
if (!selectedSong || hasVoted) return;
|
||||
|
||||
await submitVote(selectedSong);
|
||||
// setHasVoted wird jetzt durch den useEffect behandelt, der die Stimmen prüft
|
||||
try {
|
||||
// If offline, show status but still try to submit (it will be queued)
|
||||
if (isOffline) {
|
||||
setOfflineVoteStatus('pending');
|
||||
}
|
||||
|
||||
await submitVote(selectedSong);
|
||||
|
||||
if (isOffline) {
|
||||
// In offline mode, optimistically update UI
|
||||
setHasVoted(true);
|
||||
setOfflineVoteStatus('queued');
|
||||
|
||||
// Store vote locally for later sync
|
||||
try {
|
||||
const savedVotes = JSON.parse(localStorage.getItem('pendingVotes') || '{}');
|
||||
|
||||
if (battle) {
|
||||
savedVotes[`battle_${battle.round}_${battle.song1.id}`] = {
|
||||
songId: selectedSong,
|
||||
battleRound: battle.round,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
localStorage.setItem('pendingVotes', JSON.stringify(savedVotes));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Fehler beim Speichern der Offline-Stimme:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// setHasVoted wird jetzt durch den useEffect behandelt, der die Stimmen prüft
|
||||
} catch (error) {
|
||||
console.error("Fehler bei der Stimmabgabe:", error);
|
||||
setOfflineVoteStatus('error');
|
||||
}
|
||||
};
|
||||
|
||||
// Handle bye round advancement - für automatisches Weiterkommen
|
||||
@ -69,12 +103,18 @@ function VotingScreen() {
|
||||
|
||||
setProcessingByeAdvance(true);
|
||||
try {
|
||||
// Nur der Host kann im Bye-Modus weiterschaltenNNULL
|
||||
// If offline, show notification that the action will be queued
|
||||
if (isOffline) {
|
||||
setOfflineVoteStatus('byeQueued');
|
||||
}
|
||||
|
||||
// Nur der Host kann im Bye-Modus weiterschalten
|
||||
if (battle && battle.song1 && !battle.song2 && battle.song1.id) {
|
||||
await submitVote(battle.song1.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Fortfahren:", error);
|
||||
setOfflineVoteStatus('error');
|
||||
} finally {
|
||||
// Verzögerung, um mehrere Klicks zu verhindern
|
||||
setTimeout(() => setProcessingByeAdvance(false), 1000);
|
||||
@ -258,19 +298,46 @@ function VotingScreen() {
|
||||
{!hasVoted && (
|
||||
<div className="voting-actions">
|
||||
<button
|
||||
className="btn primary"
|
||||
className={`btn primary ${isOffline ? 'offline' : ''}`}
|
||||
onClick={handleSubmitVote}
|
||||
disabled={!selectedSong}
|
||||
>
|
||||
Abstimmen
|
||||
{isOffline ? 'Offline Abstimmen' : 'Abstimmen'}
|
||||
</button>
|
||||
|
||||
{isOffline && (
|
||||
<div className="offline-notice">
|
||||
<FontAwesomeIcon icon={faExclamationTriangle} />
|
||||
<span>Deine Stimme wird gespeichert und gesendet, sobald die Verbindung wiederhergestellt ist.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{offlineVoteStatus === 'queued' && (
|
||||
<div className="offline-vote-status">
|
||||
<span>Stimme gespeichert! Wird gesendet, wenn online.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{offlineVoteStatus === 'error' && (
|
||||
<div className="offline-vote-status error">
|
||||
<span>Fehler beim Speichern der Stimme.</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="voting-status">
|
||||
<p>{hasVoted ? 'Warte auf andere Spieler...' : 'Wähle deinen Favoriten!'}</p>
|
||||
|
||||
{isReconnecting && (
|
||||
<div className="reconnecting-notice">
|
||||
<span>Versuche die Verbindung wiederherzustellen...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="votes-count">
|
||||
<span>{battle.voteCount || 0}</span> von <span>{lobby?.players?.filter(p => p.isConnected).length || 0}</span> Stimmen
|
||||
{isOffline && <span className="offline-badge"> (Offline-Modus)</span>}
|
||||
</div>
|
||||
|
||||
{/* Liste der Spielerstimmen */}
|
||||
@ -282,12 +349,20 @@ function VotingScreen() {
|
||||
const hasPlayerVoted = battle.votes &&
|
||||
Object.keys(battle.votes).includes(player.id);
|
||||
|
||||
// Highlight current player to show they've voted offline
|
||||
const isCurrentPlayerOfflineVoted = player.id === currentPlayer.id &&
|
||||
isOffline &&
|
||||
hasVoted;
|
||||
|
||||
return (
|
||||
<li key={player.id} className={hasPlayerVoted ? 'voted' : 'not-voted'}>
|
||||
<li key={player.id} className={`${hasPlayerVoted ? 'voted' : 'not-voted'} ${isCurrentPlayerOfflineVoted ? 'offline-voted' : ''}`}>
|
||||
{player.name} {player.id === currentPlayer.id && '(Du)'}
|
||||
{hasPlayerVoted &&
|
||||
<FontAwesomeIcon icon={faCheck} className="vote-icon" />
|
||||
}
|
||||
{isCurrentPlayerOfflineVoted &&
|
||||
<FontAwesomeIcon icon={faExclamationTriangle} className="offline-icon" />
|
||||
}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
@ -11,35 +11,61 @@ const GameContext = createContext(null);
|
||||
export function SocketProvider({ children }) {
|
||||
const [socket, setSocket] = useState(null);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [isReconnecting, setIsReconnecting] = useState(false);
|
||||
const [reconnectionAttempts, setReconnectionAttempts] = useState(0);
|
||||
const [offlineActions, setOfflineActions] = useState([]);
|
||||
const [lastActivityTime, setLastActivityTime] = useState(Date.now());
|
||||
|
||||
useEffect(() => {
|
||||
// Create socket connection on component mount
|
||||
// Create socket connection on component mount with improved reconnection settings
|
||||
const socketInstance = io(import.meta.env.DEV ? 'http://localhost:5237' : window.location.origin, {
|
||||
autoConnect: true,
|
||||
reconnection: true,
|
||||
reconnectionDelay: 1000,
|
||||
reconnectionDelayMax: 5000,
|
||||
reconnectionAttempts: 5
|
||||
reconnectionDelayMax: 10000,
|
||||
reconnectionAttempts: Infinity, // Keep trying to reconnect indefinitely
|
||||
timeout: 20000, // Longer timeout for initial connection
|
||||
randomizationFactor: 0.5 // Add some randomization to reconnection attempts to prevent thundering herd
|
||||
});
|
||||
|
||||
// Socket event listeners
|
||||
socketInstance.on('connect', () => {
|
||||
setIsConnected(true);
|
||||
setIsReconnecting(false);
|
||||
setReconnectionAttempts(0);
|
||||
console.log('Connected to server');
|
||||
|
||||
// Process any actions that were queued while offline
|
||||
if (offlineActions.length > 0) {
|
||||
console.log(`Processing ${offlineActions.length} queued actions from offline mode`);
|
||||
// Wait a moment to ensure connection is stable
|
||||
setTimeout(() => {
|
||||
processOfflineActions(socketInstance);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Try to reconnect to game if we have saved data
|
||||
const savedGameData = localStorage.getItem('songBattleGame');
|
||||
if (savedGameData) {
|
||||
try {
|
||||
const { lobbyId, playerName } = JSON.parse(savedGameData);
|
||||
const { lobbyId, playerName, lastKnownState } = JSON.parse(savedGameData);
|
||||
if (lobbyId && playerName) {
|
||||
console.log(`Attempting to reconnect to lobby: ${lobbyId}`);
|
||||
socketInstance.emit('reconnect_to_lobby', { lobbyId, playerName }, (response) => {
|
||||
socketInstance.emit('reconnect_to_lobby', {
|
||||
lobbyId,
|
||||
playerName,
|
||||
lastKnownState // Send last known game state for server reconciliation
|
||||
}, (response) => {
|
||||
if (response.error) {
|
||||
console.error('Reconnection failed:', response.error);
|
||||
localStorage.removeItem('songBattleGame');
|
||||
// Don't remove data immediately, we might try again
|
||||
if (response.error === 'Lobby not found') {
|
||||
localStorage.removeItem('songBattleGame');
|
||||
}
|
||||
} else {
|
||||
console.log('Successfully reconnected to lobby');
|
||||
// Update the saved game data with latest state
|
||||
updateSavedGameData(response.lobby);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -50,25 +76,200 @@ export function SocketProvider({ children }) {
|
||||
}
|
||||
});
|
||||
|
||||
socketInstance.on('disconnect', () => {
|
||||
socketInstance.on('disconnect', (reason) => {
|
||||
setIsConnected(false);
|
||||
console.log('Disconnected from server');
|
||||
console.log(`Disconnected from server: ${reason}`);
|
||||
|
||||
// If the disconnection is expected, don't try to reconnect
|
||||
if (reason === 'io client disconnect') {
|
||||
console.log('Disconnection was initiated by the client');
|
||||
} else {
|
||||
// Start reconnection process for unexpected disconnections
|
||||
setIsReconnecting(true);
|
||||
}
|
||||
});
|
||||
|
||||
socketInstance.on('connect_error', (error) => {
|
||||
console.error('Connection error:', error);
|
||||
setIsReconnecting(true);
|
||||
});
|
||||
|
||||
socketInstance.on('reconnect_attempt', (attemptNumber) => {
|
||||
console.log(`Reconnection attempt #${attemptNumber}`);
|
||||
setReconnectionAttempts(attemptNumber);
|
||||
});
|
||||
|
||||
socketInstance.on('reconnect', () => {
|
||||
console.log('Reconnected to server after interruption');
|
||||
setIsReconnecting(false);
|
||||
setReconnectionAttempts(0);
|
||||
});
|
||||
|
||||
socketInstance.on('reconnect_error', (error) => {
|
||||
console.error('Reconnection error:', error);
|
||||
});
|
||||
|
||||
socketInstance.on('reconnect_failed', () => {
|
||||
console.error('Failed to reconnect to server after maximum attempts');
|
||||
setIsReconnecting(false);
|
||||
});
|
||||
|
||||
// Setup a heartbeat to detect silent disconnections
|
||||
const heartbeatInterval = setInterval(() => {
|
||||
if (socketInstance && isConnected) {
|
||||
// Check if we've seen activity recently (within 15 seconds)
|
||||
const timeSinceLastActivity = Date.now() - lastActivityTime;
|
||||
if (timeSinceLastActivity > 15000) {
|
||||
console.log('No recent activity, sending ping to verify connection');
|
||||
socketInstance.emit('ping', () => {
|
||||
// Update activity time when we get a response
|
||||
setLastActivityTime(Date.now());
|
||||
});
|
||||
|
||||
// Set a timeout to check if the ping was successful
|
||||
setTimeout(() => {
|
||||
const newTimeSinceLastActivity = Date.now() - lastActivityTime;
|
||||
if (newTimeSinceLastActivity > 15000) {
|
||||
console.warn('No response to ping, connection may be dead');
|
||||
// Force a reconnection attempt
|
||||
socketInstance.disconnect().connect();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
}, 30000); // Check every 30 seconds
|
||||
|
||||
setSocket(socketInstance);
|
||||
|
||||
// Clean up socket connection on unmount
|
||||
// Function to process queued offline actions
|
||||
const processOfflineActions = (socket) => {
|
||||
if (!socket || !offlineActions.length) return;
|
||||
|
||||
// Process each action in sequence
|
||||
const processAction = (index) => {
|
||||
if (index >= offlineActions.length) {
|
||||
// All actions processed, clear the queue
|
||||
setOfflineActions([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const { action, payload, callback } = offlineActions[index];
|
||||
console.log(`Processing offline action: ${action}`, payload);
|
||||
|
||||
// Emit the action to the server
|
||||
socket.emit(action, payload, (response) => {
|
||||
if (response.error) {
|
||||
console.error(`Error processing offline action ${action}:`, response.error);
|
||||
} else {
|
||||
console.log(`Successfully processed offline action: ${action}`);
|
||||
if (callback) callback(response);
|
||||
}
|
||||
|
||||
// Process the next action
|
||||
processAction(index + 1);
|
||||
});
|
||||
};
|
||||
|
||||
// Start processing from the first action
|
||||
processAction(0);
|
||||
};
|
||||
|
||||
// Update activity timestamp on any received message
|
||||
const originalOnEvent = socketInstance.onEvent;
|
||||
socketInstance.onEvent = (packet) => {
|
||||
setLastActivityTime(Date.now());
|
||||
originalOnEvent.call(socketInstance, packet);
|
||||
};
|
||||
|
||||
// Function to update saved game data with latest state
|
||||
const updateSavedGameData = (lobby) => {
|
||||
if (!lobby) return;
|
||||
|
||||
const currentData = localStorage.getItem('songBattleGame');
|
||||
if (currentData) {
|
||||
try {
|
||||
const data = JSON.parse(currentData);
|
||||
data.lastKnownState = {
|
||||
gameState: lobby.state,
|
||||
currentBattle: lobby.currentBattle,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
localStorage.setItem('songBattleGame', JSON.stringify(data));
|
||||
} catch (e) {
|
||||
console.error('Error updating saved game data:', e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Clean up on unmount
|
||||
return () => {
|
||||
clearInterval(heartbeatInterval);
|
||||
socketInstance.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Offline mode detection
|
||||
const [isOffline, setIsOffline] = useState(!navigator.onLine);
|
||||
|
||||
// Monitor browser's online/offline status
|
||||
useEffect(() => {
|
||||
const handleOnline = () => setIsOffline(false);
|
||||
const handleOffline = () => setIsOffline(true);
|
||||
|
||||
window.addEventListener('online', handleOnline);
|
||||
window.addEventListener('offline', handleOffline);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('online', handleOnline);
|
||||
window.removeEventListener('offline', handleOffline);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Add an action to the offline queue
|
||||
const queueOfflineAction = (action, payload, callback) => {
|
||||
console.log(`Queuing offline action: ${action}`, payload);
|
||||
setOfflineActions(prev => [...prev, { action, payload, callback }]);
|
||||
|
||||
// Store in localStorage as fallback
|
||||
try {
|
||||
const offlineQueue = JSON.parse(localStorage.getItem('offlineActionQueue') || '[]');
|
||||
offlineQueue.push({ action, payload, timestamp: Date.now() });
|
||||
localStorage.setItem('offlineActionQueue', JSON.stringify(offlineQueue));
|
||||
} catch (e) {
|
||||
console.error('Error storing offline action in localStorage:', e);
|
||||
}
|
||||
};
|
||||
|
||||
// Emit an event, queue it if offline
|
||||
const safeEmit = (eventName, data, callback) => {
|
||||
// Update activity timestamp
|
||||
setLastActivityTime(Date.now());
|
||||
|
||||
if (socket && isConnected && !isOffline) {
|
||||
// Online - send immediately
|
||||
socket.emit(eventName, data, callback);
|
||||
} else {
|
||||
// Offline or disconnected - queue for later
|
||||
queueOfflineAction(eventName, data, callback);
|
||||
|
||||
// For specific actions, provide optimistic UI updates
|
||||
if (eventName === 'submit_vote' && data.songId) {
|
||||
// Optimistically show the vote locally
|
||||
// Implementation depends on your UI update mechanism
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SocketContext.Provider value={{ socket, isConnected }}>
|
||||
<SocketContext.Provider value={{
|
||||
socket,
|
||||
isConnected,
|
||||
isReconnecting,
|
||||
reconnectionAttempts,
|
||||
isOffline,
|
||||
safeEmit,
|
||||
queueOfflineAction
|
||||
}}>
|
||||
{children}
|
||||
</SocketContext.Provider>
|
||||
);
|
||||
@ -78,7 +279,7 @@ export function SocketProvider({ children }) {
|
||||
* Game state provider - manages game state and provides methods for game actions
|
||||
*/
|
||||
export function GameProvider({ children }) {
|
||||
const { socket, isConnected } = useContext(SocketContext);
|
||||
const { socket, isConnected, isOffline, isReconnecting, safeEmit } = useContext(SocketContext);
|
||||
const [lobby, setLobby] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [currentPlayer, setCurrentPlayer] = useState(null);
|
||||
@ -86,12 +287,38 @@ export function GameProvider({ children }) {
|
||||
// Save game info when lobby is joined
|
||||
useEffect(() => {
|
||||
if (lobby && currentPlayer) {
|
||||
sessionStorage.setItem('songBattleGame', JSON.stringify({
|
||||
const savedData = {
|
||||
lobbyId: lobby.id,
|
||||
playerName: currentPlayer.name
|
||||
}));
|
||||
playerName: currentPlayer.name,
|
||||
lastKnownState: {
|
||||
gameState: lobby.state,
|
||||
currentBattle: lobby.currentBattle,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
};
|
||||
localStorage.setItem('songBattleGame', JSON.stringify(savedData));
|
||||
}
|
||||
}, [lobby, currentPlayer]);
|
||||
|
||||
// Helper function to update saved game data
|
||||
const updateSavedGameData = (updatedLobby) => {
|
||||
if (!updatedLobby || !currentPlayer) return;
|
||||
|
||||
const savedData = {
|
||||
lobbyId: updatedLobby.id,
|
||||
playerName: currentPlayer.name,
|
||||
lastKnownState: {
|
||||
gameState: updatedLobby.state,
|
||||
currentBattle: updatedLobby.currentBattle,
|
||||
playerState: {
|
||||
isReady: updatedLobby.players?.find(p => p.id === currentPlayer.id)?.isReady,
|
||||
hasVoted: updatedLobby.currentBattle?.votes?.[currentPlayer.id] !== undefined
|
||||
},
|
||||
timestamp: Date.now()
|
||||
}
|
||||
};
|
||||
localStorage.setItem('songBattleGame', JSON.stringify(savedData));
|
||||
};
|
||||
|
||||
// Clear error after 5 seconds
|
||||
useEffect(() => {
|
||||
@ -263,7 +490,7 @@ export function GameProvider({ children }) {
|
||||
return;
|
||||
}
|
||||
|
||||
socket.emit('create_lobby', { playerName }, (response) => {
|
||||
safeEmit('create_lobby', { playerName }, (response) => {
|
||||
if (response.error) {
|
||||
setError(response.error);
|
||||
} else {
|
||||
@ -284,7 +511,7 @@ export function GameProvider({ children }) {
|
||||
return;
|
||||
}
|
||||
|
||||
socket.emit('join_lobby', { lobbyId, playerName }, (response) => {
|
||||
safeEmit('join_lobby', { lobbyId, playerName }, (response) => {
|
||||
if (response.error) {
|
||||
setError(response.error);
|
||||
} else {
|
||||
@ -305,7 +532,7 @@ export function GameProvider({ children }) {
|
||||
return;
|
||||
}
|
||||
|
||||
socket.emit('update_settings', { settings }, (response) => {
|
||||
safeEmit('update_settings', { settings }, (response) => {
|
||||
if (response.error) {
|
||||
setError(response.error);
|
||||
}
|
||||
@ -319,7 +546,7 @@ export function GameProvider({ children }) {
|
||||
return;
|
||||
}
|
||||
|
||||
socket.emit('start_game', {}, (response) => {
|
||||
safeEmit('start_game', {}, (response) => {
|
||||
if (response.error) {
|
||||
setError(response.error);
|
||||
}
|
||||
@ -338,7 +565,7 @@ export function GameProvider({ children }) {
|
||||
console.log('Current lobby state before adding song:', lobby);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
socket.emit('add_song', { song }, (response) => {
|
||||
safeEmit('add_song', { song }, (response) => {
|
||||
console.log('Song addition response:', response);
|
||||
if (response.error) {
|
||||
console.error('Error adding song:', response.error);
|
||||
@ -368,6 +595,9 @@ export function GameProvider({ children }) {
|
||||
console.log('Updated lobby that was set:', updatedLobby);
|
||||
}, 0);
|
||||
|
||||
// Store the latest state for offline reconciliation
|
||||
updateSavedGameData(updatedLobby);
|
||||
|
||||
// Resolve with the updated lobby
|
||||
resolve(updatedLobby);
|
||||
} else {
|
||||
@ -378,8 +608,7 @@ export function GameProvider({ children }) {
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Search for songs on YouTube
|
||||
// Search for songs on YouTube
|
||||
const searchYouTube = (query) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!socket || !isConnected) {
|
||||
@ -388,7 +617,7 @@ export function GameProvider({ children }) {
|
||||
return;
|
||||
}
|
||||
|
||||
socket.emit('search_youtube', { query }, (response) => {
|
||||
safeEmit('search_youtube', { query }, (response) => {
|
||||
if (response.error) {
|
||||
setError(response.error);
|
||||
reject(response.error);
|
||||
@ -398,7 +627,7 @@ export function GameProvider({ children }) {
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// Get metadata for a YouTube video by ID
|
||||
const getYouTubeMetadata = (videoId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -408,7 +637,7 @@ export function GameProvider({ children }) {
|
||||
return;
|
||||
}
|
||||
|
||||
socket.emit('get_video_metadata', { videoId }, (response) => {
|
||||
safeEmit('get_video_metadata', { videoId }, (response) => {
|
||||
if (response.error) {
|
||||
setError(response.error);
|
||||
reject(response.error);
|
||||
@ -426,9 +655,12 @@ export function GameProvider({ children }) {
|
||||
return;
|
||||
}
|
||||
|
||||
socket.emit('remove_song', { songId }, (response) => {
|
||||
safeEmit('remove_song', { songId }, (response) => {
|
||||
if (response.error) {
|
||||
setError(response.error);
|
||||
} else if (response.lobby) {
|
||||
setLobby(response.lobby);
|
||||
updateSavedGameData(response.lobby);
|
||||
}
|
||||
});
|
||||
};
|
||||
@ -440,9 +672,12 @@ export function GameProvider({ children }) {
|
||||
return;
|
||||
}
|
||||
|
||||
socket.emit('player_ready', {}, (response) => {
|
||||
safeEmit('player_ready', {}, (response) => {
|
||||
if (response.error) {
|
||||
setError(response.error);
|
||||
} else if (response.lobby) {
|
||||
setLobby(response.lobby);
|
||||
updateSavedGameData(response.lobby);
|
||||
}
|
||||
});
|
||||
};
|
||||
@ -454,9 +689,12 @@ export function GameProvider({ children }) {
|
||||
return;
|
||||
}
|
||||
|
||||
socket.emit('submit_vote', { songId }, (response) => {
|
||||
safeEmit('submit_vote', { songId }, (response) => {
|
||||
if (response.error) {
|
||||
setError(response.error);
|
||||
} else if (response.lobby) {
|
||||
setLobby(response.lobby);
|
||||
updateSavedGameData(response.lobby);
|
||||
}
|
||||
});
|
||||
};
|
||||
@ -474,6 +712,8 @@ export function GameProvider({ children }) {
|
||||
error,
|
||||
currentPlayer,
|
||||
isHost: currentPlayer && lobby && currentPlayer.id === lobby.hostId,
|
||||
isOffline: isOffline,
|
||||
isReconnecting,
|
||||
createLobby,
|
||||
joinLobby,
|
||||
updateSettings,
|
||||
|
149
server/game.js
149
server/game.js
@ -112,9 +112,10 @@ class GameManager {
|
||||
* @param {string} playerId - New ID of the reconnecting player
|
||||
* @param {string} lobbyId - ID of the lobby
|
||||
* @param {string} playerName - Name of the player
|
||||
* @param {Object} lastKnownState - Last known game state from client
|
||||
* @returns {Object} Lobby data or error
|
||||
*/
|
||||
handleReconnect(playerId, lobbyId, playerName) {
|
||||
handleReconnect(playerId, lobbyId, playerName, lastKnownState = null) {
|
||||
// Check if lobby exists
|
||||
if (!this.lobbies.has(lobbyId)) {
|
||||
return { error: 'Lobby not found' };
|
||||
@ -135,6 +136,65 @@ class GameManager {
|
||||
// Update map
|
||||
this.playerToLobby.set(playerId, lobbyId);
|
||||
|
||||
// Handle state reconciliation if lastKnownState is provided
|
||||
if (lastKnownState && lastKnownState.gameState && lastKnownState.timestamp) {
|
||||
console.log(`Reconciling player state based on last known state: ${lastKnownState.gameState} from ${new Date(lastKnownState.timestamp).toISOString()}`);
|
||||
|
||||
// If the client's last known state was VOTING and the current battle has votes
|
||||
if (lastKnownState.gameState === 'VOTING' &&
|
||||
lastKnownState.currentBattle &&
|
||||
lastKnownState.playerState &&
|
||||
lastKnownState.playerState.hasVoted) {
|
||||
|
||||
// The player already voted in this battle according to their local state
|
||||
// If we still have the same battle, ensure their vote is counted
|
||||
if (lobby.currentBattle &&
|
||||
lastKnownState.currentBattle.round === lobby.currentBattle.round) {
|
||||
|
||||
console.log(`Player ${playerName} had voted in battle round ${lastKnownState.currentBattle.round}, checking if vote needs restoration`);
|
||||
|
||||
// Check if their vote is missing
|
||||
const hasVote = lobby.currentBattle.votes &&
|
||||
(lobby.currentBattle.votes.has(playerId) ||
|
||||
Object.keys(lobby.currentBattle.votes).includes(playerId));
|
||||
|
||||
// If their vote is missing, we need to determine which song they voted for
|
||||
if (!hasVote && lastKnownState.currentBattle.votes) {
|
||||
// Find which song they voted for
|
||||
const votedSongId = lastKnownState.currentBattle.votes[lobby.players[playerIndex].id];
|
||||
if (votedSongId) {
|
||||
console.log(`Restoring player ${playerName}'s vote for song ${votedSongId}`);
|
||||
// Recreate their vote
|
||||
this.submitVote(playerId, votedSongId, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the client's last known state was SONG_SUBMISSION and they were ready
|
||||
if (lastKnownState.gameState === 'SONG_SUBMISSION' &&
|
||||
lastKnownState.playerState &&
|
||||
lastKnownState.playerState.isReady &&
|
||||
lobby.state === 'SONG_SUBMISSION' &&
|
||||
!lobby.players[playerIndex].isReady) {
|
||||
|
||||
console.log(`Restoring ready status for player ${playerName}`);
|
||||
lobby.players[playerIndex].isReady = true;
|
||||
|
||||
// Check if all players are now ready
|
||||
const allReady = lobby.players.every(p => p.isReady || !p.isConnected);
|
||||
if (allReady) {
|
||||
console.log('All players ready after reconnection, starting tournament...');
|
||||
this._startTournament(lobby);
|
||||
return {
|
||||
lobby,
|
||||
lobbyId,
|
||||
tournamentStarted: true
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { lobby, lobbyId };
|
||||
}
|
||||
|
||||
@ -220,7 +280,6 @@ class GameManager {
|
||||
// Check if player is in a lobby
|
||||
let lobbyId = this.playerToLobby.get(playerId);
|
||||
if (!lobbyId) {
|
||||
// If no mapping exists, try to find the player in any lobby
|
||||
console.log(`[DEBUG] No lobby mapping found for player ID: ${playerId}, trying to locate player...`);
|
||||
|
||||
// Search all lobbies for this player
|
||||
@ -266,31 +325,6 @@ class GameManager {
|
||||
const playerIndex = lobby.players.findIndex(p => p.id === playerId);
|
||||
if (playerIndex === -1) {
|
||||
console.log(`[DEBUG] Player ID ${playerId} not found in lobby ${lobbyId}.`);
|
||||
|
||||
// First try: Find the host if this is a host request
|
||||
if (playerId === lobby.hostId || lobby.players.some(p => p.id === lobby.hostId)) {
|
||||
console.log('[DEBUG] This appears to be the host. Looking for host player...');
|
||||
const hostIndex = lobby.players.findIndex(p => p.id === lobby.hostId);
|
||||
if (hostIndex !== -1) {
|
||||
console.log(`[DEBUG] Found host at index ${hostIndex}`);
|
||||
return this._addSongToPlayer(hostIndex, lobby, lobbyId, song, playerId);
|
||||
}
|
||||
}
|
||||
|
||||
// Second try: Match any connected player as fallback
|
||||
console.log('[DEBUG] Trying to find any connected player...');
|
||||
const connectedIndex = lobby.players.findIndex(p => p.isConnected);
|
||||
if (connectedIndex !== -1) {
|
||||
console.log(`[DEBUG] Found connected player at index ${connectedIndex}`);
|
||||
return this._addSongToPlayer(connectedIndex, lobby, lobbyId, song, playerId);
|
||||
}
|
||||
|
||||
// If we still can't find a match, use the first player as last resort
|
||||
if (lobby.players.length > 0) {
|
||||
console.log('[DEBUG] Using first player as last resort');
|
||||
return this._addSongToPlayer(0, lobby, lobbyId, song, playerId);
|
||||
}
|
||||
|
||||
return { error: 'Player not found in lobby' };
|
||||
}
|
||||
|
||||
@ -519,9 +553,10 @@ class GameManager {
|
||||
* Submit a vote for a song in a battle
|
||||
* @param {string} playerId - ID of the voting player
|
||||
* @param {string} songId - ID of the voted song
|
||||
* @param {boolean} isRestoredVote - Whether this is a restored vote during reconnection
|
||||
* @returns {Object} Updated lobby data or error
|
||||
*/
|
||||
submitVote(playerId, songId) {
|
||||
submitVote(playerId, songId, isRestoredVote = false) {
|
||||
const lobbyId = this.playerToLobby.get(playerId);
|
||||
if (!lobbyId) {
|
||||
return { error: 'Player not in a lobby' };
|
||||
@ -545,7 +580,7 @@ class GameManager {
|
||||
}
|
||||
|
||||
// Check if player has already voted in this battle
|
||||
if (lobby.currentBattle.votes && lobby.currentBattle.votes.has(playerId)) {
|
||||
if (lobby.currentBattle.votes && Object.prototype.hasOwnProperty.call(lobby.currentBattle.votes, playerId) && !isRestoredVote) {
|
||||
return { error: 'Already voted in this battle' };
|
||||
}
|
||||
|
||||
@ -559,16 +594,16 @@ class GameManager {
|
||||
const player = lobby.players.find(p => p.id === playerId);
|
||||
const playerName = player ? player.name : 'Unknown Player';
|
||||
|
||||
// Initialize votes Map if it doesn't exist
|
||||
// Initialize votes Object if it doesn't exist
|
||||
if (!lobby.currentBattle.votes) {
|
||||
lobby.currentBattle.votes = new Map();
|
||||
lobby.currentBattle.votes = {};
|
||||
}
|
||||
|
||||
// Record the vote with player name for UI display
|
||||
lobby.currentBattle.votes.set(playerId, {
|
||||
lobby.currentBattle.votes[playerId] = {
|
||||
songId,
|
||||
playerName
|
||||
});
|
||||
};
|
||||
|
||||
// Update vote counts
|
||||
if (songId === lobby.currentBattle.song1.id) {
|
||||
@ -578,7 +613,7 @@ class GameManager {
|
||||
}
|
||||
|
||||
// Add a voteCount attribute for easier UI rendering
|
||||
lobby.currentBattle.voteCount = lobby.currentBattle.votes.size;
|
||||
lobby.currentBattle.voteCount = Object.keys(lobby.currentBattle.votes).length;
|
||||
|
||||
// For bye battles, the host's vote is all that's needed
|
||||
if (lobby.currentBattle.bye === true && playerId === lobby.hostId) {
|
||||
@ -605,7 +640,7 @@ class GameManager {
|
||||
|
||||
// For regular battles, check if all connected players have voted
|
||||
const connectedPlayers = lobby.players.filter(p => p.isConnected).length;
|
||||
const voteCount = lobby.currentBattle.votes.size;
|
||||
const voteCount = Object.keys(lobby.currentBattle.votes).length;
|
||||
|
||||
if (voteCount >= connectedPlayers) {
|
||||
// Determine winner
|
||||
@ -680,7 +715,7 @@ class GameManager {
|
||||
// For voting state, check if we need to progress
|
||||
if (lobby.state === 'VOTING' && lobby.currentBattle) {
|
||||
// Add null check for votes property
|
||||
const totalVotes = lobby.currentBattle.votes?.size || 0;
|
||||
const totalVotes = lobby.currentBattle.votes ? Object.keys(lobby.currentBattle.votes).length : 0;
|
||||
const remainingPlayers = lobby.players.filter(p => p.isConnected).length;
|
||||
|
||||
if (totalVotes >= remainingPlayers && totalVotes > 0) {
|
||||
@ -765,7 +800,7 @@ class GameManager {
|
||||
winner: null, // Set to null until host advances it
|
||||
song1Votes: 0,
|
||||
song2Votes: 0,
|
||||
votes: new Map() // Initialize votes as a Map
|
||||
votes: {} // Initialize votes as an empty object
|
||||
});
|
||||
}
|
||||
|
||||
@ -779,7 +814,7 @@ class GameManager {
|
||||
song1Votes: 0,
|
||||
song2Votes: 0,
|
||||
winner: null,
|
||||
votes: new Map()
|
||||
votes: {}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -795,8 +830,8 @@ class GameManager {
|
||||
// Ensure we create a proper battle object with all required fields
|
||||
lobby.currentBattle = {
|
||||
...brackets[0],
|
||||
// Make sure votes is a Map (it might be serialized incorrectly)
|
||||
votes: brackets[0].votes || new Map(),
|
||||
// Make sure votes is an object (not a Map)
|
||||
votes: brackets[0].votes || {},
|
||||
voteCount: 0
|
||||
};
|
||||
|
||||
@ -863,24 +898,22 @@ class GameManager {
|
||||
winner: null, // Set to null until host advances it
|
||||
song1Votes: 0,
|
||||
song2Votes: 0,
|
||||
votes: new Map() // Initialize votes as a Map
|
||||
votes: {} // Initialize votes as an object
|
||||
});
|
||||
}
|
||||
|
||||
// Create pairs for next round
|
||||
for (let i = 0; i < nextRoundSongs.length; i += 2) {
|
||||
if (i + 1 < nextRoundSongs.length) {
|
||||
nextRound.push({
|
||||
round,
|
||||
song1: nextRoundSongs[i],
|
||||
song2: nextRoundSongs[i + 1],
|
||||
song1Votes: 0,
|
||||
song2Votes: 0,
|
||||
winner: null,
|
||||
votes: new Map()
|
||||
});
|
||||
} // Create pairs for next round
|
||||
for (let i = 0; i < nextRoundSongs.length; i += 2) {
|
||||
if (i + 1 < nextRoundSongs.length) {
|
||||
nextRound.push({
|
||||
round,
|
||||
song1: nextRoundSongs[i],
|
||||
song2: nextRoundSongs[i + 1],
|
||||
song1Votes: 0,
|
||||
song2Votes: 0,
|
||||
winner: null,
|
||||
votes: {}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update brackets and reset index
|
||||
lobby.brackets = nextRound;
|
||||
@ -890,9 +923,9 @@ class GameManager {
|
||||
if (nextRound.length > 0) {
|
||||
lobby.currentBattle = nextRound[0];
|
||||
|
||||
// Ensure votes is initialized as a Map
|
||||
// Ensure votes is initialized as an object
|
||||
if (!lobby.currentBattle.votes) {
|
||||
lobby.currentBattle.votes = new Map();
|
||||
lobby.currentBattle.votes = {};
|
||||
}
|
||||
|
||||
// Initialize vote count for UI
|
||||
|
@ -73,9 +73,9 @@ io.on('connection', (socket) => {
|
||||
});
|
||||
|
||||
// Attempt to reconnect to a lobby
|
||||
socket.on('reconnect_to_lobby', ({ lobbyId, playerName }, callback) => {
|
||||
socket.on('reconnect_to_lobby', ({ lobbyId, playerName, lastKnownState }, callback) => {
|
||||
try {
|
||||
const result = gameManager.handleReconnect(socket.id, lobbyId, playerName);
|
||||
const result = gameManager.handleReconnect(socket.id, lobbyId, playerName, lastKnownState);
|
||||
|
||||
if (result.error) {
|
||||
if (callback) callback(result);
|
||||
@ -337,7 +337,7 @@ io.on('connection', (socket) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Handle disconnection
|
||||
// Handle player disconnection
|
||||
socket.on('disconnect', () => {
|
||||
try {
|
||||
const result = gameManager.handleDisconnect(socket.id);
|
||||
@ -355,6 +355,13 @@ io.on('connection', (socket) => {
|
||||
console.error('Error handling disconnect:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Simple ping handler to help with connection testing
|
||||
socket.on('ping', (callback) => {
|
||||
if (callback && typeof callback === 'function') {
|
||||
callback({ success: true, timestamp: Date.now() });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
server.on('error', (error) => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user