merge done
This commit is contained in:
@@ -33,7 +33,6 @@ router.get('/playing', authenticateToken, async(req, res) =>
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// IMPORTANT: This route must be before /:roomId to avoid "current" being interpreted as a roomId
|
||||
router.get('/current', authenticateToken, async(req, res) =>
|
||||
{
|
||||
@@ -150,7 +149,6 @@ router.post('/:roomId/leave', authenticateToken, async(req, res) =>
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Join a room as spectator
|
||||
router.post('/:roomId/spectate', authenticateToken, async(req, res) =>
|
||||
{
|
||||
@@ -184,5 +182,4 @@ router.post('/:roomId/leave-spectate', authenticateToken, async(req, res) =>
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export default router;
|
||||
export default router;
|
||||
@@ -108,7 +108,6 @@ async function leaveSpectateRoom(roomId, userId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function joinRoom(roomId, userId)
|
||||
{
|
||||
const room = await getRoomById(roomId);
|
||||
@@ -181,7 +180,7 @@ async function getCurrentRoom(userId)
|
||||
`SELECT r.*
|
||||
FROM game_rooms r
|
||||
JOIN game_players gp ON r.id = gp.room_id
|
||||
WHERE gp.user_id = $1 AND r.status = 'waiting'
|
||||
WHERE gp.user_id = $1 AND r.status IN ('waiting', 'playing')
|
||||
LIMIT 1`,
|
||||
[userId]
|
||||
);
|
||||
@@ -252,4 +251,4 @@ export default
|
||||
updateRoomStatus,
|
||||
resetRoomScores,
|
||||
cleanupEndedRooms
|
||||
};
|
||||
};
|
||||
@@ -600,10 +600,36 @@ function setupSocketIO(io)
|
||||
const gameState = gameRooms.get(roomId);
|
||||
if (gameState)
|
||||
{
|
||||
const wasDrawer = gameState.drawer === username;
|
||||
|
||||
gameState.players = gameState.players.filter(p => p !== username);
|
||||
delete gameState.scores[username];
|
||||
|
||||
io.to(roomId).emit('scores-updated', gameState.scores);
|
||||
|
||||
// If the drawer left and there are still enough players, choose a new drawer
|
||||
if (wasDrawer && gameState.players.length >= 1)
|
||||
{
|
||||
// Pick the next player as the new drawer
|
||||
gameState.currentPlayerIndex = gameState.currentPlayerIndex % gameState.players.length;
|
||||
const newDrawer = gameState.players[gameState.currentPlayerIndex];
|
||||
gameState.drawer = newDrawer;
|
||||
|
||||
// Reset the word state for the new round
|
||||
gameState.currentWord = '';
|
||||
gameState.revealedLetters = [];
|
||||
gameState.revealedWord = [];
|
||||
gameState.guessedLetters = [];
|
||||
gameState.wrongGuesses = 0;
|
||||
|
||||
console.log(`Drawer ${username} left, new drawer is ${newDrawer}`);
|
||||
|
||||
io.to(roomId).emit('game-drawer-changed', {
|
||||
newDrawer: newDrawer,
|
||||
reason: 'drawer_left',
|
||||
message: `${username} (dessinateur) a quitté, ${newDrawer} devient le nouveau dessinateur`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await checkAndStopSinglePlayerGame(io, roomId, dbRoomId);
|
||||
@@ -769,7 +795,9 @@ function setupSocketIO(io)
|
||||
username: socket.user.username
|
||||
});
|
||||
console.log(`Spectator ${socket.user.username} disconnected from ${roomId}`);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
// Regular player disconnect
|
||||
socket.to(roomId).emit('game-player-left', {
|
||||
username: socket.user.username,
|
||||
@@ -789,25 +817,9 @@ function setupSocketIO(io)
|
||||
// Check if game should auto-stop due to single player
|
||||
await checkAndStopSinglePlayerGame(io, roomId, dbRoomId);
|
||||
|
||||
|
||||
|
||||
socket.to(roomId).emit('game-player-left', {
|
||||
username: socket.user.username,
|
||||
userId: socket.user.userId
|
||||
});
|
||||
|
||||
// Get updated player list and broadcast
|
||||
if (dbRoomId) {
|
||||
try {
|
||||
const players = await gameRoomService.getRoomPlayers(dbRoomId);
|
||||
io.to(roomId).emit('game-players-updated', { players });
|
||||
} catch (err) {
|
||||
console.log('Room may have been deleted on disconnect:', err.message);
|
||||
}
|
||||
// Broadcast updated rooms list
|
||||
broadcastRoomsList(io);
|
||||
}
|
||||
|
||||
// Broadcast updated rooms list
|
||||
broadcastRoomsList(io);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -815,7 +827,8 @@ function setupSocketIO(io)
|
||||
|
||||
// ── Helpers tetris duel ──────────────────────────────────────────────────
|
||||
|
||||
function _tetrisLeave(socket) {
|
||||
function _tetrisLeave(socket)
|
||||
{
|
||||
const code = socket.tetrisRoomCode;
|
||||
if (!code) return;
|
||||
const room = tetrisRooms.get(code);
|
||||
@@ -842,4 +855,4 @@ function _tetrisRelayToOpponent(socket, event, data) {
|
||||
}
|
||||
|
||||
export { broadcastRoomsList };
|
||||
export default setupSocketIO;
|
||||
export default setupSocketIO;
|
||||
@@ -69,7 +69,6 @@ class App {
|
||||
initPage() {
|
||||
const page = document.querySelector('.page');
|
||||
if (!page) {
|
||||
console.warn('Page not found');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,9 @@ export class AvatarWindow extends Window {
|
||||
|
||||
this.buildUI();
|
||||
this.bindEvents();
|
||||
this.loadAvatar();
|
||||
if (localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN)) {
|
||||
this.loadAvatar();
|
||||
}
|
||||
|
||||
// Listen for login events
|
||||
eventBus.on(Events.USER_LOGGED_IN, () => this.loadAvatar());
|
||||
|
||||
@@ -23,11 +23,14 @@ export const API = {
|
||||
},
|
||||
ROOMS: {
|
||||
LIST: '/api/rooms',
|
||||
PLAYING: '/api/rooms/playing',
|
||||
CREATE: '/api/rooms',
|
||||
GET: (id) => `/api/rooms/${id}`,
|
||||
PLAYERS: (id) => `/api/rooms/${id}/players`,
|
||||
JOIN: (id) => `/api/rooms/${id}/join`,
|
||||
LEAVE: (id) => `/api/rooms/${id}/leave`,
|
||||
SPECTATE: (id) => `/api/rooms/${id}/spectate`,
|
||||
LEAVE_SPECTATE: (id) => `/api/rooms/${id}/leave-spectate`,
|
||||
CURRENT: '/api/rooms/current'
|
||||
},
|
||||
STATS: {
|
||||
|
||||
@@ -53,11 +53,13 @@ class EventBus {
|
||||
*/
|
||||
emit(event, data) {
|
||||
if (this.listeners.has(event)) {
|
||||
const listeners = this.listeners.get(event);
|
||||
this.listeners.get(event).forEach(callback => {
|
||||
try {
|
||||
callback(data);
|
||||
} catch (error) {
|
||||
console.error(`Error in listener for "${event}":`, error);
|
||||
}
|
||||
catch (err) {
|
||||
// Show that some events are not fully handled, but don't break the app
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -36,9 +36,9 @@ export class GameRoomWindow extends Window {
|
||||
this.updateTabsAccess();
|
||||
|
||||
// Verifier si l'utilisateur est deja dans un salon au chargement
|
||||
if (this.isLoggedIn()) {
|
||||
const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
|
||||
if (token)
|
||||
this.checkCurrentRoom();
|
||||
}
|
||||
}
|
||||
|
||||
buildUI() {
|
||||
@@ -49,6 +49,11 @@ export class GameRoomWindow extends Window {
|
||||
});
|
||||
this.browseTab.dataset.tab = 'browse';
|
||||
|
||||
this.spectatorTab = this.createElement('button', CSS.GAMEROOM_TAB, {
|
||||
text: 'Spectateur'
|
||||
});
|
||||
this.spectatorTab.dataset.tab = 'spectator';
|
||||
|
||||
this.createTab = this.createElement('button', CSS.GAMEROOM_TAB, {
|
||||
text: 'Creer'
|
||||
});
|
||||
@@ -60,7 +65,7 @@ export class GameRoomWindow extends Window {
|
||||
this.lobbyTab.dataset.tab = 'lobby';
|
||||
this.lobbyTab.style.display = 'none';
|
||||
|
||||
this.tabs.append(this.browseTab, this.createTab, this.lobbyTab);
|
||||
this.tabs.append(this.browseTab, this.spectatorTab, this.createTab, this.lobbyTab);
|
||||
|
||||
this.content = this.createElement('div', CSS.GAMEROOM_CONTENT);
|
||||
|
||||
@@ -99,9 +104,12 @@ export class GameRoomWindow extends Window {
|
||||
|
||||
this.list = this.createElement('div', CSS.GAMEROOM_LIST);
|
||||
|
||||
this.spectatorList = this.createElement('div', CSS.GAMEROOM_LIST);
|
||||
this.spectatorList.style.display = 'none';
|
||||
|
||||
this.message = this.createElement('div', CSS.MESSAGE);
|
||||
|
||||
this.content.append(this.createContainer, this.lobbyContainer, this.list, this.message);
|
||||
this.content.append(this.createContainer, this.lobbyContainer, this.list, this.spectatorList, this.message);
|
||||
|
||||
this.body.append(this.tabs, this.content);
|
||||
}
|
||||
@@ -160,7 +168,7 @@ export class GameRoomWindow extends Window {
|
||||
|
||||
// Boutons du jeu
|
||||
this.gameButtons = this.createElement('div', 'gameroom__game-buttons');
|
||||
this.backToLobbyBtn = this.createElement('button', [CSS.BTN, CSS.BTN_SECONDARY], { text: 'Retour au lobby' });
|
||||
this.backToLobbyBtn = this.createElement('button', [CSS.BTN, CSS.BTN_SECONDARY], { text: 'Quitter la partie' });
|
||||
this.endRoundBtn = this.createElement('button', [CSS.BTN, CSS.BTN_DANGER], { text: 'Terminer le jeu' });
|
||||
this.gameButtons.append(this.backToLobbyBtn, this.endRoundBtn);
|
||||
|
||||
@@ -198,7 +206,7 @@ export class GameRoomWindow extends Window {
|
||||
this.lastY = 0;
|
||||
|
||||
this.canvas.addEventListener('mousedown', (e) => {
|
||||
if (!this.gameState.isPlaying || !this.isCurrentUserDrawer()) return;
|
||||
if (!this.gameState.isPlaying || !this.isCurrentUserDrawer() || this.isSpectating) return;
|
||||
this.isDrawing = true;
|
||||
[this.lastX, this.lastY] = [e.offsetX, e.offsetY];
|
||||
});
|
||||
@@ -365,7 +373,25 @@ export class GameRoomWindow extends Window {
|
||||
});
|
||||
|
||||
this.socket.on('game-player-left', (data) => {
|
||||
console.log(`${data.username} left the room`);
|
||||
this.showMessage(`${data.username} a quitté le salon`, 'info');
|
||||
|
||||
if (this.gameState.isPlaying)
|
||||
{
|
||||
if (this.gameState.players)
|
||||
this.gameState.players = this.gameState.players.filter(p => p !== data.username);
|
||||
}
|
||||
|
||||
if (this.gameState.scores)
|
||||
{
|
||||
delete this.gameState.scores[data.username];
|
||||
this.updateScoresDisplay(this.gameState.scores);
|
||||
}
|
||||
|
||||
// Note: If the drawer left, the server will emit 'game-drawer-changed'
|
||||
// with the new drawer, so we don't need to handle it here
|
||||
|
||||
if (this.currentRoom && !this.gameState.isPlaying)
|
||||
this.loadLobby();
|
||||
});
|
||||
|
||||
// Game started
|
||||
@@ -384,6 +410,12 @@ export class GameRoomWindow extends Window {
|
||||
this.setupRound();
|
||||
});
|
||||
|
||||
// Game start error
|
||||
this.socket.on('game-start-error', (data) => {
|
||||
console.error('Game start error:', data.error);
|
||||
this.showMessage(data.error || 'Impossible de démarrer la partie', 'error');
|
||||
});
|
||||
|
||||
// Word was set by drawer
|
||||
this.socket.on('game-word-set', (data) => {
|
||||
console.log(`Word set by ${data.drawer}, length: ${data.wordLength}`);
|
||||
@@ -396,6 +428,13 @@ export class GameRoomWindow extends Window {
|
||||
}
|
||||
|
||||
this.updateWordDisplay();
|
||||
|
||||
// Don't change UI for spectators
|
||||
if (this.isSpectating) {
|
||||
this.currentDrawerInfo.textContent = '👁️ MODE SPECTATEUR - Vous regardez la partie';
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentDrawerInfo.textContent = `${data.drawer} dessine (${data.wordLength} lettres)`;
|
||||
|
||||
// Enable guess input for non-drawers
|
||||
@@ -451,7 +490,22 @@ export class GameRoomWindow extends Window {
|
||||
|
||||
// Game ended
|
||||
this.socket.on('game-ended', () => {
|
||||
this.resetGameUI();
|
||||
// If spectating, return to spectator list
|
||||
if (this.isSpectating) {
|
||||
this.resetGameUI();
|
||||
this.currentRoom = null;
|
||||
this.isSpectating = false;
|
||||
this.switchTab('spectator');
|
||||
this.showMessage('La partie est terminée', 'info');
|
||||
} else {
|
||||
this.resetGameUI();
|
||||
this.loadLobby();
|
||||
}
|
||||
});
|
||||
|
||||
// Game message from server
|
||||
this.socket.on('game-message', (data) => {
|
||||
this.showMessage(data.message, data.type || 'info');
|
||||
});
|
||||
|
||||
// Sync state for late joiners
|
||||
@@ -463,12 +517,25 @@ export class GameRoomWindow extends Window {
|
||||
this.gameState.revealedLetters = data.revealedLetters || [];
|
||||
this.gameState.revealedWord = data.revealedWord || new Array(data.wordLength).fill('_');
|
||||
this.gameState.players = data.players;
|
||||
this.gameState.scores = data.scores || {};
|
||||
|
||||
this.showGameUI();
|
||||
this.updateWordDisplay();
|
||||
|
||||
// Update scores display
|
||||
if (data.scores) {
|
||||
this.updateScoresDisplay(data.scores);
|
||||
}
|
||||
|
||||
this.currentDrawerInfo.textContent = `${data.drawer} dessine (${data.wordLength} lettres)`;
|
||||
|
||||
if (!this.isCurrentUserDrawer()) {
|
||||
// Don't enable input for spectators
|
||||
if (this.isSpectating) {
|
||||
this.guessContainer.style.display = 'none';
|
||||
this.wordInputContainer.style.display = 'none';
|
||||
this.drawTools.style.display = 'none';
|
||||
this.currentDrawerInfo.textContent = '👁️ MODE SPECTATEUR - Vous regardez la partie';
|
||||
} else if (!this.isCurrentUserDrawer()) {
|
||||
this.guessContainer.style.display = 'flex';
|
||||
if (data.wordLength > 0) {
|
||||
this.letterInput.disabled = false;
|
||||
@@ -482,11 +549,73 @@ export class GameRoomWindow extends Window {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Spectator events
|
||||
this.socket.on('game-spectate-joined', (data) => {
|
||||
console.log('Successfully joined as spectator:', data.roomId);
|
||||
this.isSpectating = true;
|
||||
|
||||
// Prepare UI for spectating
|
||||
this.spectatorList.style.display = 'none';
|
||||
this.list.style.display = 'none';
|
||||
this.createContainer.style.display = 'none';
|
||||
this.lobbyContainer.style.display = 'flex';
|
||||
|
||||
// Hide lobby elements, keep game container for when state syncs
|
||||
this.playerList.style.display = 'none';
|
||||
this.lobbyButtons.style.display = 'none';
|
||||
this.lobbyTitle.textContent = 'Mode Spectateur';
|
||||
|
||||
this.showMessage('Vous regardez la partie...', 'success');
|
||||
// The game state will be synced via game-state-sync event
|
||||
});
|
||||
|
||||
this.socket.on('game-spectate-error', (data) => {
|
||||
console.error('Spectate error:', data.error);
|
||||
this.showMessage(data.error || 'Impossible de regarder cette partie', 'error');
|
||||
});
|
||||
|
||||
this.socket.on('game-spectator-joined', (data) => {
|
||||
console.log(`Spectator ${data.username} joined`);
|
||||
});
|
||||
|
||||
this.socket.on('game-spectator-left', (data) => {
|
||||
console.log(`Spectator ${data.username} left`);
|
||||
});
|
||||
|
||||
// Drawer changed (when drawer leaves during game)
|
||||
this.socket.on('game-drawer-changed', (data) => {
|
||||
console.log('Drawer changed:', data);
|
||||
this.showMessage(data.message, 'info');
|
||||
|
||||
// Update game state with new drawer
|
||||
this.gameState.drawer = data.newDrawer;
|
||||
this.gameState.currentPlayerIndex = this.gameState.players.indexOf(data.newDrawer);
|
||||
|
||||
// Reset round state
|
||||
this.gameState.currentWord = '';
|
||||
this.gameState.wordLength = 0;
|
||||
this.gameState.revealedLetters = [];
|
||||
this.gameState.revealedWord = [];
|
||||
this.gameState.guessedLetters = [];
|
||||
|
||||
// Clear canvas and history
|
||||
this.clearCanvas();
|
||||
this.guessHistory.innerHTML = '';
|
||||
this.wordDisplay.textContent = '';
|
||||
|
||||
// Setup UI for new round with new drawer
|
||||
this.setupRound();
|
||||
});
|
||||
}
|
||||
|
||||
disconnectGameSocket() {
|
||||
if (this.socket) {
|
||||
this.socket.emit('game-leave-room');
|
||||
if (this.isSpectating) {
|
||||
this.socket.emit('game-leave-spectate');
|
||||
} else {
|
||||
this.socket.emit('game-leave-room');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -533,13 +662,14 @@ export class GameRoomWindow extends Window {
|
||||
|
||||
this.currentTab = tabName;
|
||||
|
||||
[this.browseTab, this.createTab, this.lobbyTab].forEach(tab => {
|
||||
[this.browseTab, this.spectatorTab, this.createTab, this.lobbyTab].forEach(tab => {
|
||||
tab.classList.toggle(CSS.GAMEROOM_TAB_ACTIVE, tab.dataset.tab === tabName);
|
||||
});
|
||||
|
||||
this.createContainer.style.display = tabName === 'create' ? 'flex' : 'none';
|
||||
this.lobbyContainer.style.display = tabName === 'lobby' ? 'flex' : 'none';
|
||||
this.list.style.display = tabName === 'browse' ? 'flex' : 'none';
|
||||
this.spectatorList.style.display = tabName === 'spectator' ? 'flex' : 'none';
|
||||
|
||||
this.loadCurrentTab();
|
||||
}
|
||||
@@ -551,6 +681,10 @@ export class GameRoomWindow extends Window {
|
||||
// Connect to socket to receive real-time room updates
|
||||
this.ensureSocketConnected();
|
||||
break;
|
||||
case 'spectator':
|
||||
this.loadPlayingRooms();
|
||||
this.ensureSocketConnected();
|
||||
break;
|
||||
case 'create':
|
||||
this.message.textContent = '';
|
||||
this.ensureSocketConnected();
|
||||
@@ -776,6 +910,123 @@ export class GameRoomWindow extends Window {
|
||||
}
|
||||
}
|
||||
|
||||
createSpectatorRoomItem(room) {
|
||||
const item = this.createElement('div', CSS.GAMEROOM_ITEM);
|
||||
|
||||
const name = this.createElement('span', CSS.GAMEROOM_NAME, {
|
||||
text: room.name
|
||||
});
|
||||
|
||||
const players = this.createElement('span', CSS.GAMEROOM_PLAYERS, {
|
||||
text: `${room.player_count || 0}/${room.max_players || 8}`
|
||||
});
|
||||
|
||||
const status = this.createElement('span', 'gameroom__status', {
|
||||
text: '🎮 En cours'
|
||||
});
|
||||
status.style.color = '#4CAF50';
|
||||
status.style.fontWeight = 'bold';
|
||||
|
||||
const actions = this.createElement('div', CSS.GAMEROOM_ACTIONS);
|
||||
|
||||
const spectateBtn = this.createElement('button', [CSS.BTN, CSS.BTN_PRIMARY], {
|
||||
text: 'Regarder'
|
||||
});
|
||||
spectateBtn.addEventListener('click', () => this.spectateRoom(room.id));
|
||||
actions.appendChild(spectateBtn);
|
||||
|
||||
item.append(name, players, status, actions);
|
||||
return item;
|
||||
}
|
||||
|
||||
async loadPlayingRooms() {
|
||||
const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
|
||||
if (!token) {
|
||||
this.showMessage('Connectez-vous pour voir les parties en cours', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(API.ROOMS.PLAYING, {
|
||||
headers: this.getHeaders()
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
this.showMessage(data.error || 'Erreur', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.renderPlayingRoomsList(data || []);
|
||||
} catch (error) {
|
||||
console.error('Load playing rooms error:', error);
|
||||
this.showMessage('Erreur de connexion', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
renderPlayingRoomsList(rooms) {
|
||||
this.spectatorList.innerHTML = '';
|
||||
this.message.textContent = '';
|
||||
|
||||
if (rooms.length === 0) {
|
||||
this.showMessage('Aucune partie en cours', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
rooms.forEach(room => {
|
||||
const item = this.createSpectatorRoomItem(room);
|
||||
this.spectatorList.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
async spectateRoom(roomId) {
|
||||
const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
|
||||
if (!token) {
|
||||
this.showMessage('Connectez-vous pour regarder', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if user is already in a room as a player
|
||||
if (this.currentRoom && !this.isSpectating) {
|
||||
this.showMessage('Vous êtes déjà dans un salon. Quittez-le d\'abord.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if already spectating another game
|
||||
if (this.isSpectating && this.currentRoom && this.currentRoom.id !== roomId) {
|
||||
this.showMessage('Vous regardez déjà une autre partie', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(API.ROOMS.SPECTATE(roomId), {
|
||||
method: 'POST',
|
||||
headers: this.getHeaders()
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
this.showMessage(data.error || 'Impossible de regarder cette partie', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Store room info and mark as spectating
|
||||
this.currentRoom = data;
|
||||
this.isSpectating = true;
|
||||
|
||||
// Join as spectator via socket
|
||||
await this.ensureSocketConnected();
|
||||
if (this.socket?.connected) {
|
||||
this.socket.emit('game-spectate-room', { roomId: roomId });
|
||||
}
|
||||
|
||||
this.showMessage('Connexion à la partie...', 'info');
|
||||
} catch (error) {
|
||||
console.error('Spectate room error:', error);
|
||||
this.showMessage('Erreur de connexion', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async joinRoom(roomId) {
|
||||
const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
|
||||
if (!token) {
|
||||
@@ -844,6 +1095,8 @@ export class GameRoomWindow extends Window {
|
||||
async loadLobby() {
|
||||
if (!this.currentRoom) return;
|
||||
|
||||
this.gameState.scores = {};
|
||||
|
||||
try {
|
||||
const response = await fetch(API.ROOMS.PLAYERS(this.currentRoom.id), {
|
||||
headers: this.getHeaders()
|
||||
@@ -870,6 +1123,10 @@ export class GameRoomWindow extends Window {
|
||||
text: 'Aucun joueur'
|
||||
});
|
||||
this.playerList.appendChild(empty);
|
||||
// Disable start button if no players
|
||||
this.startGameBtn.disabled = true;
|
||||
this.startGameBtn.style.opacity = '0.5';
|
||||
this.startGameBtn.title = 'Il faut au moins 2 joueurs';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -899,6 +1156,17 @@ export class GameRoomWindow extends Window {
|
||||
item.append(avatar, name, statsContainer);
|
||||
this.playerList.appendChild(item);
|
||||
});
|
||||
|
||||
// Enable/disable start button based on player count
|
||||
if (players.length < 2) {
|
||||
this.startGameBtn.disabled = true;
|
||||
this.startGameBtn.style.opacity = '0.5';
|
||||
this.startGameBtn.title = 'Il faut au moins 2 joueurs';
|
||||
} else {
|
||||
this.startGameBtn.disabled = false;
|
||||
this.startGameBtn.style.opacity = '1';
|
||||
this.startGameBtn.title = '';
|
||||
}
|
||||
}
|
||||
|
||||
async leaveRoom() {
|
||||
@@ -941,6 +1209,11 @@ export class GameRoomWindow extends Window {
|
||||
}
|
||||
|
||||
showMessage(text, type = 'info') {
|
||||
// Clear any existing timeout
|
||||
if (this.messageTimeout) {
|
||||
clearTimeout(this.messageTimeout);
|
||||
}
|
||||
|
||||
this.message.textContent = text;
|
||||
this.message.className = CSS.MESSAGE;
|
||||
|
||||
@@ -951,6 +1224,12 @@ export class GameRoomWindow extends Window {
|
||||
} else {
|
||||
this.message.classList.add(CSS.MESSAGE_INFO);
|
||||
}
|
||||
|
||||
// Auto-clear message after 5 seconds
|
||||
this.messageTimeout = setTimeout(() => {
|
||||
this.message.textContent = '';
|
||||
this.message.className = CSS.MESSAGE;
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -978,6 +1257,23 @@ export class GameRoomWindow extends Window {
|
||||
this.lobbyButtons.style.display = 'none';
|
||||
this.clearCanvas();
|
||||
this.guessHistory.innerHTML = '';
|
||||
|
||||
// If spectating, show indicator and disable interactions
|
||||
if (this.isSpectating) {
|
||||
this.currentDrawerInfo.textContent = '👁️ MODE SPECTATEUR - Vous regardez la partie';
|
||||
this.currentDrawerInfo.style.backgroundColor = '#2196F3';
|
||||
this.currentDrawerInfo.style.color = 'white';
|
||||
this.currentDrawerInfo.style.padding = '8px';
|
||||
this.currentDrawerInfo.style.borderRadius = '4px';
|
||||
this.currentDrawerInfo.style.textAlign = 'center';
|
||||
|
||||
// Change button text for spectators
|
||||
this.backToLobbyBtn.textContent = 'Arrêter de regarder';
|
||||
this.endRoundBtn.style.display = 'none'; // Hide end game button for spectators
|
||||
} else {
|
||||
this.backToLobbyBtn.textContent = 'Quitter la partie';
|
||||
this.endRoundBtn.style.display = 'inline-block';
|
||||
}
|
||||
}
|
||||
|
||||
resetGameUI() {
|
||||
@@ -987,6 +1283,21 @@ export class GameRoomWindow extends Window {
|
||||
this.gameState.revealedLetters = [];
|
||||
this.gameState.revealedWord = [];
|
||||
this.gameState.drawer = null;
|
||||
this.isSpectating = false;
|
||||
|
||||
this.gameState.scores = {};
|
||||
this.gameState.players = [];
|
||||
this.gameState.currentPlayerIndex = 0;
|
||||
this.gameState.guessedLetters = [];
|
||||
|
||||
// Clear scores display
|
||||
if (this.scoresDisplay)
|
||||
this.scoresDisplay.textContent = '';
|
||||
|
||||
if (this.guessHistory)
|
||||
this.guessHistory.innerHTML = '';
|
||||
|
||||
this.clearCanvas();
|
||||
|
||||
this.gameContainer.style.display = 'none';
|
||||
this.playerList.style.display = 'flex';
|
||||
@@ -996,6 +1307,12 @@ export class GameRoomWindow extends Window {
|
||||
this.guessContainer.style.display = 'none';
|
||||
this.drawTools.style.display = 'none';
|
||||
|
||||
// Reset spectator styling
|
||||
this.currentDrawerInfo.style.backgroundColor = '';
|
||||
this.currentDrawerInfo.style.color = '';
|
||||
this.currentDrawerInfo.style.padding = '';
|
||||
this.currentDrawerInfo.style.borderRadius = '';
|
||||
this.currentDrawerInfo.style.textAlign = '';
|
||||
this.currentDrawerInfo.classList.remove('gameroom__drawer-info--winner');
|
||||
}
|
||||
|
||||
@@ -1010,8 +1327,8 @@ export class GameRoomWindow extends Window {
|
||||
|
||||
console.log('Players found:', players);
|
||||
|
||||
if (players.length < 1) {
|
||||
this.showMessage('Il faut au moins 1 joueur pour jouer', 'error');
|
||||
if (players.length < 2) {
|
||||
this.showMessage('Il faut au moins 2 joueurs pour commencer', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1048,6 +1365,15 @@ export class GameRoomWindow extends Window {
|
||||
this.guessHistory.innerHTML = '';
|
||||
this.clearCanvas();
|
||||
|
||||
// Spectators cannot interact
|
||||
if (this.isSpectating) {
|
||||
this.wordInputContainer.style.display = 'none';
|
||||
this.guessContainer.style.display = 'none';
|
||||
this.drawTools.style.display = 'none';
|
||||
this.currentDrawerInfo.textContent = '👁️ MODE SPECTATEUR - Vous regardez la partie';
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isCurrentUserDrawer()) {
|
||||
// Drawer chooses a word
|
||||
this.wordInputContainer.style.display = 'flex';
|
||||
@@ -1254,9 +1580,14 @@ export class GameRoomWindow extends Window {
|
||||
}
|
||||
|
||||
backToLobby() {
|
||||
if (this.socket?.connected) {
|
||||
this.socket.emit('leave-room-during-game');
|
||||
}
|
||||
|
||||
// Return to lobby without ending game for others
|
||||
this.resetGameUI();
|
||||
this.loadLobby();
|
||||
this.exitLobby();
|
||||
this.showMessage('Vous avez quitté la partie', 'info');
|
||||
}
|
||||
|
||||
endGame() {
|
||||
|
||||
@@ -17,6 +17,8 @@ export class GlobalChat extends Window {
|
||||
this.socket = null;
|
||||
this.connected = false;
|
||||
this.friendIds = new Set();
|
||||
this.currentUserId = null;
|
||||
this.currentUsername = null;
|
||||
|
||||
this.buildUI();
|
||||
this.bindEvents();
|
||||
@@ -169,6 +171,19 @@ export class GlobalChat extends Window {
|
||||
await this.connect();
|
||||
}
|
||||
|
||||
decodeToken(token)
|
||||
{
|
||||
try
|
||||
{
|
||||
const payload = token.split('.')[1];
|
||||
return (JSON.parse(atob(payload)));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects to the Socket.IO server
|
||||
*/
|
||||
@@ -180,6 +195,13 @@ export class GlobalChat extends Window {
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenData = this.decodeToken(token);
|
||||
|
||||
if (tokenData) {
|
||||
this.currentUserId = tokenData.id || tokenData.userId || tokenData.user_id || tokenData.sub || null;
|
||||
this.currentUsername = tokenData.username || tokenData.name || null;
|
||||
}
|
||||
|
||||
if (this.socket?.connected) {
|
||||
this.addSystemMessage('Already connected to global chat');
|
||||
return;
|
||||
@@ -239,6 +261,7 @@ export class GlobalChat extends Window {
|
||||
this.socket.on('connect', () => {
|
||||
console.log('Socket connected, ID:', this.socket.id);
|
||||
this.connected = true;
|
||||
this.output.innerHTML = '';
|
||||
this.addSystemMessage('Connected to global chat', 'success');
|
||||
eventBus.emit(Events.CHAT_CONNECTED, { socketId: this.socket.id });
|
||||
});
|
||||
@@ -262,15 +285,38 @@ export class GlobalChat extends Window {
|
||||
|
||||
// Display recent messages
|
||||
data.messages.forEach(msg => {
|
||||
const isFriend = this.friendIds.has(msg.sender_id);
|
||||
this.addChatMessage(msg.username, msg.content, false, isFriend);
|
||||
const isOwn = this.isOwnMessage(msg);
|
||||
const isFriend = !isOwn && this.friendIds.has(msg.sender_id);
|
||||
const displayUsername = isOwn ? 'Me' : msg.username;
|
||||
this.addChatMessage(displayUsername, msg.content, isOwn, isFriend);
|
||||
});
|
||||
});
|
||||
|
||||
this.socket.on('chat-message', (msg) => {
|
||||
const isOwn = this.isOwnMessage(msg);
|
||||
if (isOwn)
|
||||
return;
|
||||
|
||||
const isFriend = this.friendIds.has(msg.sender_id);
|
||||
this.addChatMessage(msg.username, msg.content, false, isFriend);
|
||||
eventBus.emit(Events.CHAT_MESSAGE_RECEIVED, msg);
|
||||
});
|
||||
}
|
||||
|
||||
isOwnMessage(msg)
|
||||
{
|
||||
if (this.currentUserId !== null && msg.sender_id !== undefined && msg.sender_id !== null)
|
||||
{
|
||||
if (String(this.currentUserId) === String(msg.sender_id))
|
||||
return (true);
|
||||
}
|
||||
|
||||
if (this.currentUsername && msg.username)
|
||||
{
|
||||
if (this.currentUsername.toLowerCase() === msg.username.toLowerCase())
|
||||
return (true);
|
||||
}
|
||||
|
||||
return (false);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user