diff --git a/Transcendence/.gitignore b/Transcendence/.gitignore
index e909572..5e74a24 100644
--- a/Transcendence/.gitignore
+++ b/Transcendence/.gitignore
@@ -1,2 +1,3 @@
srcs/.DS_Store
-*.DS_Store
\ No newline at end of file
+*.DS_Store
+srcs/backend/avatar/*
\ No newline at end of file
diff --git a/Transcendence/srcs/backend/db.js b/Transcendence/srcs/backend/db.js
index 5bf4ff6..35ec2a2 100644
--- a/Transcendence/srcs/backend/db.js
+++ b/Transcendence/srcs/backend/db.js
@@ -28,6 +28,33 @@ async function waitForDb(retries = 10, delay = 2000)
throw new Error('Could not connect to database after multiple attempts');
}
+async function runMigrations()
+{
+ try
+ {
+ // Add total_points column if it doesn't exist
+ await pool.query(`
+ DO $$
+ BEGIN
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='total_points') THEN
+ ALTER TABLE users ADD COLUMN total_points INT DEFAULT 0;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='games_played') THEN
+ ALTER TABLE users ADD COLUMN games_played INT DEFAULT 0;
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='games_won') THEN
+ ALTER TABLE users ADD COLUMN games_won INT DEFAULT 0;
+ END IF;
+ END $$;
+ `);
+ console.log('Migrations completed!');
+ }
+ catch (err)
+ {
+ console.error('Error running migrations:', err);
+ }
+}
+
async function createTables()
{
try
@@ -39,6 +66,9 @@ async function createTables()
password_hash TEXT NOT NULL,
email VARCHAR(100),
avatar_url TEXT DEFAULT '/avatar/default.png',
+ total_points INT DEFAULT 0,
+ games_played INT DEFAULT 0,
+ games_won INT DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW()
);
@@ -148,6 +178,7 @@ export
{
waitForDb,
createTables,
+ runMigrations,
query,
ensureOauthClient
};
diff --git a/Transcendence/srcs/backend/index.js b/Transcendence/srcs/backend/index.js
index f9e7961..f098e36 100644
--- a/Transcendence/srcs/backend/index.js
+++ b/Transcendence/srcs/backend/index.js
@@ -7,7 +7,8 @@ import chatRouter from './routes/global_chat.js';
import gameRoomRouter from './routes/game_room.js';
import avatarRouter from './routes/avatar.js';
import friendsRouter from './routes/friends.js';
-import {waitForDb, createTables, ensureOauthClient} from './db.js';
+import playerStatsRouter from './routes/player_stats.js';
+import {waitForDb, createTables, runMigrations, ensureOauthClient} from './db.js';
import setupSocketIO from './services/socket.js';
import avatarService from './services/avatar.js';
@@ -31,6 +32,7 @@ async function startServer()
{
await waitForDb();
await createTables();
+ await runMigrations();
// Ensure GitHub OAuth client is registered in DB
try {
@@ -45,6 +47,7 @@ async function startServer()
app.use('/api/rooms', gameRoomRouter);
app.use('/api/avatar', avatarRouter);
app.use('/api/friends', friendsRouter);
+ app.use('/api/stats', playerStatsRouter);
app.get('/api', (req, res) => res.send('Backend running'));
server.listen(3001, () =>
diff --git a/Transcendence/srcs/backend/routes/game_room.js b/Transcendence/srcs/backend/routes/game_room.js
index 0a98719..0d19d24 100644
--- a/Transcendence/srcs/backend/routes/game_room.js
+++ b/Transcendence/srcs/backend/routes/game_room.js
@@ -1,6 +1,7 @@
import express from 'express';
import gameRoomService from '../services/game_room.js';
import authenticateToken from '../middleware/auth.js';
+import { getIO, broadcastRoomsList } from '../services/socket.js';
const router = express.Router();
router.get('/', authenticateToken, async(req, res) =>
@@ -17,6 +18,23 @@ router.get('/', 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) =>
+{
+ try
+ {
+ const room = await gameRoomService.getCurrentRoom(req.user.userId);
+ if (!room)
+ return res.status(204).send(); // No content - user is not in any room
+ res.json(room);
+ }
+ catch(err)
+ {
+ console.error(err);
+ res.status(500).json({error: 'Server error'});
+ }
+});
+
router.get('/:roomId', authenticateToken, async(req, res) =>
{
try
@@ -55,6 +73,13 @@ router.post('/', authenticateToken, async(req, res) =>
if (!name)
return (res.status(400).json({error: 'Room name required'}));
const room = await gameRoomService.createRoom(name, req.user.userId);
+
+ // Broadcast updated rooms list to all clients
+ const io = getIO();
+ if (io) {
+ broadcastRoomsList(io);
+ }
+
res.status(201).json(room);
}
catch(err)
@@ -69,6 +94,13 @@ router.post('/:roomId/join', authenticateToken, async(req, res) =>
try
{
const player = await gameRoomService.joinRoom(req.params.roomId, req.user.userId);
+
+ // Broadcast updated rooms list to all clients
+ const io = getIO();
+ if (io) {
+ broadcastRoomsList(io);
+ }
+
res.json(player);
}
catch(err)
@@ -86,6 +118,13 @@ router.post('/:roomId/leave', authenticateToken, async(req, res) =>
try
{
await gameRoomService.leaveRoom(req.params.roomId, req.user.userId);
+
+ // Broadcast updated rooms list to all clients
+ const io = getIO();
+ if (io) {
+ broadcastRoomsList(io);
+ }
+
res.json({message: 'Left room successfully'});
}
catch(err)
diff --git a/Transcendence/srcs/backend/routes/player_stats.js b/Transcendence/srcs/backend/routes/player_stats.js
new file mode 100644
index 0000000..db9a06e
--- /dev/null
+++ b/Transcendence/srcs/backend/routes/player_stats.js
@@ -0,0 +1,46 @@
+import express from 'express';
+import playerStatsService from '../services/player_stats.js';
+import authenticateToken from '../middleware/auth.js';
+const router = express.Router();
+
+// Get current user's stats
+router.get('/me', authenticateToken, async (req, res) => {
+ try {
+ const stats = await playerStatsService.getStatsByUserId(req.user.userId);
+ if (!stats) {
+ return res.status(404).json({ error: 'User not found' });
+ }
+ res.json(stats);
+ } catch (err) {
+ console.error('Error getting user stats:', err);
+ res.status(500).json({ error: 'Server error' });
+ }
+});
+
+// Get stats by username
+router.get('/user/:username', authenticateToken, async (req, res) => {
+ try {
+ const stats = await playerStatsService.getStatsByUsername(req.params.username);
+ if (!stats) {
+ return res.status(404).json({ error: 'User not found' });
+ }
+ res.json(stats);
+ } catch (err) {
+ console.error('Error getting user stats:', err);
+ res.status(500).json({ error: 'Server error' });
+ }
+});
+
+// Get leaderboard
+router.get('/leaderboard', authenticateToken, async (req, res) => {
+ try {
+ const limit = Math.min(parseInt(req.query.limit) || 10, 50);
+ const leaderboard = await playerStatsService.getLeaderboard(limit);
+ res.json(leaderboard);
+ } catch (err) {
+ console.error('Error getting leaderboard:', err);
+ res.status(500).json({ error: 'Server error' });
+ }
+});
+
+export default router;
diff --git a/Transcendence/srcs/backend/services/friends.js b/Transcendence/srcs/backend/services/friends.js
index a9de380..9220dd1 100644
--- a/Transcendence/srcs/backend/services/friends.js
+++ b/Transcendence/srcs/backend/services/friends.js
@@ -6,7 +6,7 @@ import { query } from '../db.js';
async function getFriends(userId) {
try {
const result = await query(
- `SELECT u.id, u.username, u.avatar_url
+ `SELECT u.id, u.username, u.avatar_url, u.total_points, u.games_played, u.games_won
FROM friendship f
JOIN users u ON (
CASE
diff --git a/Transcendence/srcs/backend/services/game_room.js b/Transcendence/srcs/backend/services/game_room.js
index 393858d..e2fd1f6 100644
--- a/Transcendence/srcs/backend/services/game_room.js
+++ b/Transcendence/srcs/backend/services/game_room.js
@@ -98,7 +98,7 @@ async function getRoomPlayers(roomId)
{
const result = await query
(
- `SELECT gp.*, u.username
+ `SELECT gp.*, u.username, u.avatar_url, u.total_points, u.games_played, u.games_won
FROM game_players gp
JOIN users u ON gp.user_id = u.id
WHERE gp.room_id = $1
@@ -108,6 +108,21 @@ async function getRoomPlayers(roomId)
return (result.rows);
}
+// Get the current room of a user (if any)
+async function getCurrentRoom(userId)
+{
+ const result = await query
+ (
+ `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'
+ LIMIT 1`,
+ [userId]
+ );
+ return (result.rows[0] || null);
+}
+
export default
{
createRoom,
@@ -115,5 +130,6 @@ export default
listActiveRooms,
joinRoom,
leaveRoom,
- getRoomPlayers
+ getRoomPlayers,
+ getCurrentRoom
};
\ No newline at end of file
diff --git a/Transcendence/srcs/backend/services/player_stats.js b/Transcendence/srcs/backend/services/player_stats.js
new file mode 100644
index 0000000..c6c7e71
--- /dev/null
+++ b/Transcendence/srcs/backend/services/player_stats.js
@@ -0,0 +1,88 @@
+import { query } from '../db.js';
+
+// Get player stats by user ID
+async function getStatsByUserId(userId) {
+ const result = await query(
+ `SELECT id, username, avatar_url, total_points, games_played, games_won, created_at
+ FROM users WHERE id = $1`,
+ [userId]
+ );
+ return result.rows[0] || null;
+}
+
+// Get player stats by username
+async function getStatsByUsername(username) {
+ const result = await query(
+ `SELECT id, username, avatar_url, total_points, games_played, games_won, created_at
+ FROM users WHERE username = $1`,
+ [username]
+ );
+ return result.rows[0] || null;
+}
+
+// Update player points (add points to total)
+async function addPoints(userId, points) {
+ const result = await query(
+ `UPDATE users SET total_points = COALESCE(total_points, 0) + $1 WHERE id = $2 RETURNING total_points`,
+ [points, userId]
+ );
+ return result.rows[0]?.total_points || 0;
+}
+
+// Update player points by username
+async function addPointsByUsername(username, points) {
+ const result = await query(
+ `UPDATE users SET total_points = COALESCE(total_points, 0) + $1 WHERE username = $2 RETURNING total_points`,
+ [points, username]
+ );
+ return result.rows[0]?.total_points || 0;
+}
+
+// Increment games played
+async function incrementGamesPlayed(userId) {
+ await query(
+ `UPDATE users SET games_played = COALESCE(games_played, 0) + 1 WHERE id = $1`,
+ [userId]
+ );
+}
+
+// Increment games won
+async function incrementGamesWon(userId) {
+ await query(
+ `UPDATE users SET games_won = COALESCE(games_won, 0) + 1 WHERE id = $1`,
+ [userId]
+ );
+}
+
+// Get leaderboard (top players by points)
+async function getLeaderboard(limit = 10) {
+ const result = await query(
+ `SELECT id, username, avatar_url, total_points, games_played, games_won
+ FROM users
+ WHERE total_points > 0
+ ORDER BY total_points DESC
+ LIMIT $1`,
+ [limit]
+ );
+ return result.rows;
+}
+
+// Get user ID by username
+async function getUserIdByUsername(username) {
+ const result = await query(
+ `SELECT id FROM users WHERE username = $1`,
+ [username]
+ );
+ return result.rows[0]?.id || null;
+}
+
+export default {
+ getStatsByUserId,
+ getStatsByUsername,
+ addPoints,
+ addPointsByUsername,
+ incrementGamesPlayed,
+ incrementGamesWon,
+ getLeaderboard,
+ getUserIdByUsername
+};
diff --git a/Transcendence/srcs/backend/services/socket.js b/Transcendence/srcs/backend/services/socket.js
index 6f5cea1..ac2cf65 100644
--- a/Transcendence/srcs/backend/services/socket.js
+++ b/Transcendence/srcs/backend/services/socket.js
@@ -1,9 +1,48 @@
import jwt from 'jsonwebtoken';
import chatService from './global_chat.js';
import friendsService from './friends.js';
+import gameRoomService from './game_room.js';
+import playerStatsService from './player_stats.js';
+
+// Store game state per room
+const gameRooms = new Map();
+
+// Store io instance globally for use in routes
+let ioInstance = null;
+
+export function getIO() {
+ return ioInstance;
+}
+
+// Broadcast rooms list to all connected clients
+async function broadcastRoomsList(io) {
+ try {
+ const rooms = await gameRoomService.listActiveRooms();
+ io.emit('game-rooms-updated', { rooms });
+ } catch (err) {
+ console.error('Error broadcasting rooms list:', err);
+ }
+}
+
+// Save round points to database (only the difference from round start)
+async function saveRoundPoints(currentScores, roundStartScores) {
+ for (const [username, currentPoints] of Object.entries(currentScores)) {
+ const startPoints = roundStartScores[username] || 0;
+ const pointsEarned = currentPoints - startPoints;
+ if (pointsEarned !== 0) {
+ try {
+ await playerStatsService.addPointsByUsername(username, pointsEarned);
+ console.log(`Saved ${pointsEarned} points for ${username}`);
+ } catch (err) {
+ console.error(`Error saving points for ${username}:`, err);
+ }
+ }
+ }
+}
function setupSocketIO(io)
{
+ ioInstance = io;
io.use((socket, next) =>
{
const token = socket.handshake.auth.token;
@@ -63,11 +102,356 @@ function setupSocketIO(io)
socket.emit('error', {message: 'Failed to send message'});
}
});
- socket.on('disconnect', () =>
+
+ // ============================================
+ // GAME ROOM EVENTS
+ // ============================================
+
+ // Join a game room
+ socket.on('game-join-room', async (data) => {
+ console.log('Received game-join-room from', socket.user.username, 'data:', data);
+ const roomId = `game-room-${data.roomId}`;
+ socket.join(roomId);
+ socket.gameRoomId = roomId;
+ socket.gameRoomDbId = data.roomId;
+ console.log(`${socket.user.username} joined ${roomId}, socket.gameRoomId set to:`, socket.gameRoomId);
+
+ // Send confirmation to the socket that joined
+ socket.emit('game-room-joined', {
+ roomId: data.roomId,
+ success: true
+ });
+
+ // Get updated player list from DB
+ try {
+ const players = await gameRoomService.getRoomPlayers(data.roomId);
+ // Notify ALL players in the room (including the one who joined) with updated player list
+ io.to(roomId).emit('game-players-updated', { players });
+ } catch (err) {
+ console.error('Error getting room players:', err);
+ }
+
+ // Notify others in the room that someone joined
+ socket.to(roomId).emit('game-player-joined', {
+ username: socket.user.username,
+ userId: socket.user.userId
+ });
+
+ // Broadcast rooms list update to everyone
+ broadcastRoomsList(io);
+
+ // Send current game state if game is in progress
+ const gameState = gameRooms.get(roomId);
+ if (gameState && gameState.isPlaying) {
+ socket.emit('game-state-sync', {
+ isPlaying: gameState.isPlaying,
+ drawer: gameState.drawer,
+ wordLength: gameState.currentWord ? gameState.currentWord.length : 0,
+ revealedLetters: gameState.revealedLetters,
+ revealedWord: gameState.revealedWord || [],
+ guessedLetters: gameState.guessedLetters,
+ players: gameState.players
+ });
+ }
+ });
+
+ // Leave a game room
+ socket.on('game-leave-room', async () => {
+ if (socket.gameRoomId) {
+ const roomId = socket.gameRoomId;
+ const dbRoomId = socket.gameRoomDbId;
+
+ socket.to(roomId).emit('game-player-left', {
+ username: socket.user.username,
+ userId: socket.user.userId
+ });
+ socket.leave(roomId);
+ console.log(`${socket.user.username} left ${roomId}`);
+
+ // Get updated player list and broadcast to remaining players
+ if (dbRoomId) {
+ try {
+ const players = await gameRoomService.getRoomPlayers(dbRoomId);
+ io.to(roomId).emit('game-players-updated', { players });
+ } catch (err) {
+ // Room may have been deleted
+ console.log('Room may have been deleted:', err.message);
+ }
+ }
+
+ socket.gameRoomId = null;
+ socket.gameRoomDbId = null;
+
+ // Broadcast updated rooms list
+ broadcastRoomsList(io);
+ }
+ });
+
+ // Start the game
+ socket.on('game-start', (data) => {
+ console.log('Received game-start event from', socket.user.username);
+ console.log('socket.gameRoomId:', socket.gameRoomId);
+
+ const gameStartedData = {
+ drawer: data.drawer,
+ players: data.players
+ };
+
+ const roomId = socket.gameRoomId;
+
+ // If no roomId, still start the game for this socket only
+ if (!roomId) {
+ console.log('WARNING: No roomId for socket, starting game for this socket only');
+ socket.emit('game-started', gameStartedData);
+ return;
+ }
+
+ // Initialize scores for all players
+ const scores = {};
+ data.players.forEach(p => scores[p] = 0);
+
+ const gameState = {
+ isPlaying: true,
+ currentWord: '',
+ revealedLetters: [],
+ drawer: data.drawer,
+ players: data.players,
+ currentPlayerIndex: 0,
+ guessedLetters: [],
+ scores: scores,
+ roundStartScores: { ...scores }
+ };
+ gameRooms.set(roomId, gameState);
+
+ // Emit to OTHER players in the room
+ socket.to(roomId).emit('game-started', gameStartedData);
+
+ // Emit directly to this socket (the one who started the game)
+ socket.emit('game-started', gameStartedData);
+
+ console.log(`Game started in ${roomId} by ${socket.user.username}`);
+ });
+
+ // Drawer sets the word
+ socket.on('game-set-word', (data) => {
+ const roomId = socket.gameRoomId;
+ if (!roomId) return;
+
+ const gameState = gameRooms.get(roomId);
+ if (!gameState) return;
+
+ gameState.currentWord = data.word.toLowerCase();
+ gameState.revealedLetters = new Array(data.word.length).fill(false);
+ gameState.revealedWord = new Array(data.word.length).fill('_');
+ gameState.guessedLetters = [];
+ gameState.wrongGuesses = 0;
+
+ // Initialize scores if not already done
+ if (!gameState.scores) {
+ gameState.scores = {};
+ gameState.players.forEach(p => gameState.scores[p] = 0);
+ }
+
+ // Notify all players (without revealing the word)
+ io.to(roomId).emit('game-word-set', {
+ wordLength: data.word.length,
+ drawer: socket.user.username,
+ revealedWord: gameState.revealedWord,
+ scores: gameState.scores
+ });
+ });
+
+ // Drawing data (real-time)
+ socket.on('game-draw', (data) => {
+ const roomId = socket.gameRoomId;
+ if (!roomId) return;
+
+ // Broadcast drawing to all other players in the room
+ socket.to(roomId).emit('game-draw', {
+ x1: data.x1,
+ y1: data.y1,
+ x2: data.x2,
+ y2: data.y2,
+ color: data.color,
+ lineWidth: data.lineWidth
+ });
+ });
+
+ // Clear canvas
+ socket.on('game-clear-canvas', () => {
+ const roomId = socket.gameRoomId;
+ if (!roomId) return;
+
+ socket.to(roomId).emit('game-clear-canvas');
+ });
+
+ // Player makes a guess
+ socket.on('game-guess', (data) => {
+ const roomId = socket.gameRoomId;
+ if (!roomId) return;
+
+ const gameState = gameRooms.get(roomId);
+ if (!gameState || !gameState.currentWord) return;
+
+ const guess = data.guess.toLowerCase();
+ const isLetter = guess.length === 1;
+ let success = false;
+ let points = 0;
+ const username = socket.user.username;
+
+ // Initialize scores if needed
+ if (!gameState.scores) {
+ gameState.scores = {};
+ gameState.players.forEach(p => gameState.scores[p] = 0);
+ }
+ if (!gameState.scores[username]) {
+ gameState.scores[username] = 0;
+ }
+
+ if (isLetter) {
+ // Check if letter was already guessed
+ if (gameState.guessedLetters.includes(guess)) {
+ socket.emit('game-guess-result', {
+ guess,
+ success: false,
+ type: 'letter',
+ message: 'Lettre deja proposee',
+ username: username,
+ scores: gameState.scores
+ });
+ return;
+ }
+
+ gameState.guessedLetters.push(guess);
+
+ // Check each position and reveal the actual letter
+ let lettersFound = 0;
+ for (let i = 0; i < gameState.currentWord.length; i++) {
+ if (gameState.currentWord[i] === guess) {
+ gameState.revealedLetters[i] = true;
+ gameState.revealedWord[i] = guess;
+ success = true;
+ lettersFound++;
+ }
+ }
+
+ // Points: 10 per letter found, -5 for wrong guess
+ if (success) {
+ points = lettersFound * 10;
+ gameState.scores[username] += points;
+ } else {
+ points = -5;
+ gameState.scores[username] += points;
+ gameState.wrongGuesses++;
+ }
+ } else {
+ // Full word guess
+ success = guess === gameState.currentWord;
+ if (success) {
+ gameState.revealedLetters = gameState.revealedLetters.map(() => true);
+ gameState.revealedWord = gameState.currentWord.split('');
+ // Bonus points for guessing the whole word
+ const remainingLetters = gameState.revealedLetters.filter(r => !r).length;
+ points = 50 + (remainingLetters * 5);
+ gameState.scores[username] += points;
+ } else {
+ points = -10;
+ gameState.scores[username] += points;
+ gameState.wrongGuesses++;
+ }
+ }
+
+ // Broadcast result to all players with the revealed word (actual letters)
+ io.to(roomId).emit('game-guess-result', {
+ guess,
+ success,
+ type: isLetter ? 'letter' : 'word',
+ username: username,
+ revealedLetters: gameState.revealedLetters,
+ revealedWord: gameState.revealedWord,
+ points: points,
+ scores: gameState.scores
+ });
+
+ // Check if word is complete
+ if (gameState.revealedLetters.every(r => r)) {
+ // Bonus points for the drawer
+ const drawerBonus = Math.max(0, 30 - (gameState.wrongGuesses * 5));
+ if (gameState.scores[gameState.drawer]) {
+ gameState.scores[gameState.drawer] += drawerBonus;
+ }
+
+ // Save points to database for all players
+ saveRoundPoints(gameState.scores, gameState.roundStartScores || {});
+ // Update round start scores for next round
+ gameState.roundStartScores = { ...gameState.scores };
+
+ io.to(roomId).emit('game-word-found', {
+ word: gameState.currentWord,
+ winner: username,
+ scores: gameState.scores,
+ drawerBonus: drawerBonus
+ });
+ }
+ });
+
+ // Next round
+ socket.on('game-next-round', (data) => {
+ const roomId = socket.gameRoomId;
+ if (!roomId) return;
+
+ const gameState = gameRooms.get(roomId);
+ if (!gameState) return;
+
+ gameState.currentWord = '';
+ gameState.revealedLetters = [];
+ gameState.guessedLetters = [];
+ gameState.drawer = data.drawer;
+
+ io.to(roomId).emit('game-new-round', {
+ drawer: data.drawer
+ });
+ });
+
+ // End game
+ socket.on('game-end', () => {
+ const roomId = socket.gameRoomId;
+ if (!roomId) return;
+
+ gameRooms.delete(roomId);
+ io.to(roomId).emit('game-ended');
+ });
+
+ socket.on('disconnect', async () =>
{
console.log(`User disconnected: ${socket.user.username}`);
+
+ // Notify game room if player was in one
+ if (socket.gameRoomId) {
+ const roomId = socket.gameRoomId;
+ const dbRoomId = socket.gameRoomDbId;
+
+ 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);
+ }
});
});
}
+export { broadcastRoomsList };
export default setupSocketIO;
\ No newline at end of file
diff --git a/Transcendence/srcs/frontend/src/app.js b/Transcendence/srcs/frontend/src/app.js
index ec84433..563844c 100644
--- a/Transcendence/srcs/frontend/src/app.js
+++ b/Transcendence/srcs/frontend/src/app.js
@@ -7,6 +7,7 @@ import { LoginWindow } from './login.js';
import { GlobalChat } from './global_chat.js';
import { AvatarWindow } from './avatar.js';
import { FriendsWindow } from './friends.js';
+import { GameRoomWindow } from './game_room.js';
/**
* Main application class
@@ -23,11 +24,11 @@ class App {
* Initializes all windows
*/
initWindows() {
- // Windows automatically register themselves in the registry
new LoginWindow();
new GlobalChat();
new AvatarWindow();
new FriendsWindow();
+ new GameRoomWindow();
}
/**
@@ -41,12 +42,12 @@ class App {
return;
}
- // Action to window name mapping
const actionMap = {
'login': 'login',
'chat': 'chat',
'avatar': 'avatar',
- 'friends': 'friends'
+ 'friends': 'friends',
+ 'gameroom': 'gameroom'
};
// Event delegation on the menu
@@ -72,7 +73,7 @@ class App {
const easterEgg = document.querySelector('.easter-egg');
if (easterEgg) {
easterEgg.addEventListener('click', () => {
- alert('You clicked when we told you not to!');
+ alert('DONT CLICK!');
});
}
}
diff --git a/Transcendence/srcs/frontend/src/avatar.js b/Transcendence/srcs/frontend/src/avatar.js
index 3201dad..be7c939 100644
--- a/Transcendence/srcs/frontend/src/avatar.js
+++ b/Transcendence/srcs/frontend/src/avatar.js
@@ -34,6 +34,13 @@ export class AvatarWindow extends Window {
// Username display
this.username = this.createElement('div', CSS.AVATAR_USERNAME);
+ // Stats display
+ this.statsContainer = this.createElement('div', 'avatar__stats');
+ this.pointsDisplay = this.createElement('div', 'avatar__stat');
+ this.gamesPlayedDisplay = this.createElement('div', 'avatar__stat');
+ this.gamesWonDisplay = this.createElement('div', 'avatar__stat');
+ this.statsContainer.append(this.pointsDisplay, this.gamesPlayedDisplay, this.gamesWonDisplay);
+
// Hidden file input
this.fileInput = this.createElement('input', 'avatar__file-input', {
type: 'file',
@@ -64,6 +71,7 @@ export class AvatarWindow extends Window {
this.body.append(
this.preview,
this.username,
+ this.statsContainer,
this.fileInput,
this.controls,
this.message
@@ -148,6 +156,46 @@ export class AvatarWindow extends Window {
} catch (error) {
console.error('Error loading avatar:', error);
}
+
+ // Load stats
+ await this.loadStats();
+ }
+
+ /**
+ * Loads player stats from the server
+ */
+ async loadStats() {
+ const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
+ if (!token) return;
+
+ try {
+ const response = await fetch(API.STATS.ME, {
+ method: 'GET',
+ headers: {
+ 'Authorization': `Bearer ${token}`
+ }
+ });
+
+ if (!response.ok) {
+ console.warn('Failed to load stats, status:', response.status);
+ return;
+ }
+
+ const data = await response.json();
+ this.updateStatsDisplay(data);
+ } catch (error) {
+ console.error('Error loading stats:', error);
+ }
+ }
+
+ /**
+ * Updates the stats display
+ * @param {object} stats
+ */
+ updateStatsDisplay(stats) {
+ this.pointsDisplay.innerHTML = `Points: ${stats.total_points || 0}`;
+ this.gamesPlayedDisplay.innerHTML = `Parties: ${stats.games_played || 0}`;
+ this.gamesWonDisplay.innerHTML = `Victoires: ${stats.games_won || 0}`;
}
/**
diff --git a/Transcendence/srcs/frontend/src/config.js b/Transcendence/srcs/frontend/src/config.js
index 646575f..9fae344 100644
--- a/Transcendence/srcs/frontend/src/config.js
+++ b/Transcendence/srcs/frontend/src/config.js
@@ -20,6 +20,20 @@ export const API = {
REQUEST: '/api/friends/request',
ACCEPT: '/api/friends/accept',
DECLINE: '/api/friends/decline'
+ },
+ ROOMS: {
+ LIST: '/api/rooms',
+ 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`,
+ CURRENT: '/api/rooms/current'
+ },
+ STATS: {
+ ME: '/api/stats/me',
+ USER: (username) => `/api/stats/user/${username}`,
+ LEADERBOARD: '/api/stats/leaderboard'
}
};
@@ -94,7 +108,26 @@ export const CSS = {
FRIENDS_NAME: 'friends__name',
FRIENDS_ACTIONS: 'friends__actions',
FRIENDS_SEARCH: 'friends__search',
- FRIENDS_EMPTY: 'friends__empty'
+ FRIENDS_EMPTY: 'friends__empty',
+
+ // Game Rooms
+ GAMEROOM: 'gameroom',
+ GAMEROOM_TABS: 'gameroom__tabs',
+ GAMEROOM_TAB: 'gameroom__tab',
+ GAMEROOM_TAB_ACTIVE: 'gameroom__tab--active',
+ GAMEROOM_CONTENT: 'gameroom__content',
+ GAMEROOM_LIST: 'gameroom__list',
+ GAMEROOM_ITEM: 'gameroom__item',
+ GAMEROOM_NAME: 'gameroom__name',
+ GAMEROOM_PLAYERS: 'gameroom__players',
+ GAMEROOM_ACTIONS: 'gameroom__actions',
+ GAMEROOM_CREATE: 'gameroom__create',
+ GAMEROOM_LOBBY: 'gameroom__lobby',
+ GAMEROOM_PLAYER_LIST: 'gameroom__player-list',
+ GAMEROOM_PLAYER: 'gameroom__player',
+ GAMEROOM_PLAYER_AVATAR: 'gameroom__player-avatar',
+ GAMEROOM_PLAYER_NAME: 'gameroom__player-name',
+ GAMEROOM_PLAYER_SCORE: 'gameroom__player-score'
};
// Colors (for reference, mainly used in CSS)
diff --git a/Transcendence/srcs/frontend/src/events.js b/Transcendence/srcs/frontend/src/events.js
index 5bb0aa6..632a64c 100644
--- a/Transcendence/srcs/frontend/src/events.js
+++ b/Transcendence/srcs/frontend/src/events.js
@@ -84,5 +84,10 @@ export const Events = {
// Chat
CHAT_CONNECTED: 'chat:connected',
CHAT_DISCONNECTED: 'chat:disconnected',
- CHAT_MESSAGE_RECEIVED: 'chat:message-received'
+ CHAT_MESSAGE_RECEIVED: 'chat:message-received',
+
+ // Game Rooms
+ ROOM_JOINED: 'room:joined',
+ ROOM_LEFT: 'room:left',
+ ROOM_CREATED: 'room:created'
};
diff --git a/Transcendence/srcs/frontend/src/friends.js b/Transcendence/srcs/frontend/src/friends.js
index e262af5..cedb40f 100644
--- a/Transcendence/srcs/frontend/src/friends.js
+++ b/Transcendence/srcs/frontend/src/friends.js
@@ -290,10 +290,22 @@ export class FriendsWindow extends Window {
});
avatar.src = user.avatar_url || '/avatar/default.png';
+ const infoContainer = this.createElement('div', 'friends__info');
+
const name = this.createElement('span', CSS.FRIENDS_NAME, {
text: user.username
});
+ infoContainer.appendChild(name);
+
+ // Show stats for friends
+ if (type === 'friend' && user.total_points !== undefined) {
+ const stats = this.createElement('span', 'friends__stats', {
+ text: `${user.total_points || 0} pts`
+ });
+ infoContainer.appendChild(stats);
+ }
+
const actions = this.createElement('div', CSS.FRIENDS_ACTIONS);
if (type === 'friend') {
@@ -322,7 +334,7 @@ export class FriendsWindow extends Window {
actions.appendChild(addBtn);
}
- item.append(avatar, name, actions);
+ item.append(avatar, infoContainer, actions);
return item;
}
diff --git a/Transcendence/srcs/frontend/src/game_room.js b/Transcendence/srcs/frontend/src/game_room.js
new file mode 100644
index 0000000..4a1ed59
--- /dev/null
+++ b/Transcendence/srcs/frontend/src/game_room.js
@@ -0,0 +1,1261 @@
+import { Window } from './windows.js';
+import { API, STORAGE_KEYS, CSS } from './config.js';
+import { eventBus, Events } from './events.js';
+
+export class GameRoomWindow extends Window {
+ constructor() {
+ super({
+ name: 'gameroom',
+ title: 'Game Rooms',
+ cssClasses: ['gameroom-window']
+ });
+
+ this.currentTab = 'browse';
+ this.currentRoom = null;
+ this.roomsList = [];
+ this.socket = null;
+ this.buildUI();
+ this.bindEvents();
+
+ eventBus.on(Events.USER_LOGGED_IN, () => {
+ this.updateTabsAccess();
+ this.checkCurrentRoom();
+ });
+ eventBus.on(Events.USER_LOGGED_OUT, () => {
+ this.handleLogout();
+ });
+
+ this.updateTabsAccess();
+
+ // Verifier si l'utilisateur est deja dans un salon au chargement
+ if (this.isLoggedIn()) {
+ this.checkCurrentRoom();
+ }
+ }
+
+ buildUI() {
+ this.tabs = this.createElement('div', CSS.GAMEROOM_TABS);
+
+ this.browseTab = this.createElement('button', [CSS.GAMEROOM_TAB, CSS.GAMEROOM_TAB_ACTIVE], {
+ text: 'Salons'
+ });
+ this.browseTab.dataset.tab = 'browse';
+
+ this.createTab = this.createElement('button', CSS.GAMEROOM_TAB, {
+ text: 'Creer'
+ });
+ this.createTab.dataset.tab = 'create';
+
+ this.lobbyTab = this.createElement('button', CSS.GAMEROOM_TAB, {
+ text: 'Lobby'
+ });
+ this.lobbyTab.dataset.tab = 'lobby';
+ this.lobbyTab.style.display = 'none';
+
+ this.tabs.append(this.browseTab, this.createTab, this.lobbyTab);
+
+ this.content = this.createElement('div', CSS.GAMEROOM_CONTENT);
+
+ this.createContainer = this.createElement('div', CSS.GAMEROOM_CREATE);
+ this.roomNameInput = this.createElement('input', CSS.INPUT, {
+ type: 'text',
+ placeholder: 'Nom du salon...'
+ });
+ this.createBtn = this.createElement('button', [CSS.BTN, CSS.BTN_PRIMARY], {
+ text: 'Creer le salon'
+ });
+ this.createContainer.append(this.roomNameInput, this.createBtn);
+ this.createContainer.style.display = 'none';
+
+ this.lobbyContainer = this.createElement('div', CSS.GAMEROOM_LOBBY);
+ this.lobbyTitle = this.createElement('h3', 'gameroom__lobby-title', { text: '' });
+ this.playerList = this.createElement('div', CSS.GAMEROOM_PLAYER_LIST);
+
+ // Boutons du lobby
+ this.lobbyButtons = this.createElement('div', 'gameroom__lobby-buttons');
+ this.startGameBtn = this.createElement('button', [CSS.BTN, CSS.BTN_SUCCESS], {
+ text: 'Lancer le jeu'
+ });
+ this.leaveBtn = this.createElement('button', [CSS.BTN, CSS.BTN_DANGER], {
+ text: 'Quitter'
+ });
+ this.lobbyButtons.append(this.startGameBtn, this.leaveBtn);
+
+ // Container du jeu (caché par défaut)
+ this.gameContainer = this.createElement('div', 'gameroom__game');
+ this.gameContainer.style.display = 'none';
+ this.buildGameUI();
+
+ this.lobbyContainer.append(this.lobbyTitle, this.playerList, this.lobbyButtons, this.gameContainer);
+ this.lobbyContainer.style.display = 'none';
+
+ this.list = this.createElement('div', CSS.GAMEROOM_LIST);
+
+ this.message = this.createElement('div', CSS.MESSAGE);
+
+ this.content.append(this.createContainer, this.lobbyContainer, this.list, this.message);
+
+ this.body.append(this.tabs, this.content);
+ }
+
+ buildGameUI() {
+ // Zone d'info du jeu
+ this.gameInfo = this.createElement('div', 'gameroom__game-info');
+ this.currentDrawerInfo = this.createElement('div', 'gameroom__drawer-info', { text: '' });
+ this.scoresDisplay = this.createElement('div', 'gameroom__scores-display');
+ this.gameInfo.append(this.currentDrawerInfo, this.scoresDisplay);
+
+ // Affichage du mot caché
+ this.wordDisplay = this.createElement('div', 'gameroom__word-display');
+
+ // Canvas de dessin
+ this.canvasContainer = this.createElement('div', 'gameroom__canvas-container');
+ this.canvas = document.createElement('canvas');
+ this.canvas.className = 'gameroom__canvas';
+ this.canvas.width = 380;
+ this.canvas.height = 200;
+ this.ctx = this.canvas.getContext('2d');
+ this.canvasContainer.appendChild(this.canvas);
+
+ // Outils de dessin
+ this.drawTools = this.createElement('div', 'gameroom__draw-tools');
+ this.colorPicker = this.createElement('input', 'gameroom__color-picker');
+ this.colorPicker.type = 'color';
+ this.colorPicker.value = '#ffffff';
+ this.clearCanvasBtn = this.createElement('button', [CSS.BTN, CSS.BTN_SECONDARY], { text: 'Effacer' });
+ this.drawTools.append(this.colorPicker, this.clearCanvasBtn);
+ this.drawTools.style.display = 'none';
+
+ // Zone pour choisir le mot (pour le dessinateur)
+ this.wordInputContainer = this.createElement('div', 'gameroom__word-input-container');
+ this.wordInput = this.createElement('input', CSS.INPUT, {
+ type: 'text',
+ placeholder: 'Entrez le mot a faire deviner...'
+ });
+ this.confirmWordBtn = this.createElement('button', [CSS.BTN, CSS.BTN_PRIMARY], { text: 'OK' });
+ this.wordInputContainer.append(this.wordInput, this.confirmWordBtn);
+ this.wordInputContainer.style.display = 'none';
+
+ // Zone pour deviner (pour les autres joueurs)
+ this.guessContainer = this.createElement('div', 'gameroom__guess-container');
+ this.letterInput = this.createElement('input', CSS.INPUT, {
+ type: 'text',
+ placeholder: 'Proposez une lettre ou le mot...',
+ maxLength: '50'
+ });
+ this.guessBtn = this.createElement('button', [CSS.BTN, CSS.BTN_PRIMARY], { text: 'Deviner' });
+ this.guessContainer.append(this.letterInput, this.guessBtn);
+ this.guessContainer.style.display = 'none';
+
+ // Historique des tentatives
+ this.guessHistory = this.createElement('div', 'gameroom__guess-history');
+
+ // 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.endRoundBtn = this.createElement('button', [CSS.BTN, CSS.BTN_DANGER], { text: 'Terminer le jeu' });
+ this.gameButtons.append(this.backToLobbyBtn, this.endRoundBtn);
+
+ this.gameContainer.append(
+ this.gameInfo,
+ this.wordDisplay,
+ this.canvasContainer,
+ this.drawTools,
+ this.wordInputContainer,
+ this.guessContainer,
+ this.guessHistory,
+ this.gameButtons
+ );
+
+ // Initialiser les variables du jeu
+ this.gameState = {
+ isPlaying: false,
+ currentWord: '',
+ wordLength: 0,
+ revealedLetters: [],
+ revealedWord: [],
+ drawer: null,
+ players: [],
+ currentPlayerIndex: 0,
+ guessedLetters: [],
+ scores: {}
+ };
+
+ this.initDrawing();
+ }
+
+ initDrawing() {
+ this.isDrawing = false;
+ this.lastX = 0;
+ this.lastY = 0;
+
+ this.canvas.addEventListener('mousedown', (e) => {
+ if (!this.gameState.isPlaying || !this.isCurrentUserDrawer()) return;
+ this.isDrawing = true;
+ [this.lastX, this.lastY] = [e.offsetX, e.offsetY];
+ });
+
+ this.canvas.addEventListener('mousemove', (e) => {
+ if (!this.isDrawing) return;
+
+ const x1 = this.lastX;
+ const y1 = this.lastY;
+ const x2 = e.offsetX;
+ const y2 = e.offsetY;
+ const color = this.colorPicker.value;
+ const lineWidth = 3;
+
+ // Dessiner localement
+ this.drawLine(x1, y1, x2, y2, color, lineWidth);
+
+ // Envoyer aux autres joueurs via WebSocket
+ if (this.socket?.connected) {
+ this.socket.emit('game-draw', { x1, y1, x2, y2, color, lineWidth });
+ }
+
+ [this.lastX, this.lastY] = [x2, y2];
+ });
+
+ this.canvas.addEventListener('mouseup', () => this.isDrawing = false);
+ this.canvas.addEventListener('mouseout', () => this.isDrawing = false);
+
+ this.clearCanvasBtn.addEventListener('click', () => {
+ this.clearCanvas();
+ // Notifier les autres
+ if (this.socket?.connected) {
+ this.socket.emit('game-clear-canvas');
+ }
+ });
+ }
+
+ drawLine(x1, y1, x2, y2, color, lineWidth) {
+ this.ctx.beginPath();
+ this.ctx.strokeStyle = color;
+ this.ctx.lineWidth = lineWidth;
+ this.ctx.lineCap = 'round';
+ this.ctx.moveTo(x1, y1);
+ this.ctx.lineTo(x2, y2);
+ this.ctx.stroke();
+ }
+
+ clearCanvas() {
+ this.ctx.fillStyle = '#333';
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
+ }
+
+ bindEvents() {
+ this.tabs.addEventListener('click', (e) => {
+ const tab = e.target.closest(`.${CSS.GAMEROOM_TAB}`);
+ if (tab) {
+ this.switchTab(tab.dataset.tab);
+ }
+ });
+
+ this.createBtn.addEventListener('click', () => this.createRoom());
+ this.roomNameInput.addEventListener('keypress', (e) => {
+ if (e.key === 'Enter') this.createRoom();
+ });
+
+ this.leaveBtn.addEventListener('click', () => this.leaveRoom());
+ this.startGameBtn.addEventListener('click', () => this.startGame());
+
+ // Events du jeu
+ this.confirmWordBtn.addEventListener('click', () => this.confirmWord());
+ this.wordInput.addEventListener('keypress', (e) => {
+ if (e.key === 'Enter') this.confirmWord();
+ });
+
+ this.guessBtn.addEventListener('click', () => this.makeGuess());
+ this.letterInput.addEventListener('keypress', (e) => {
+ if (e.key === 'Enter') this.makeGuess();
+ });
+
+ this.backToLobbyBtn.addEventListener('click', () => this.backToLobby());
+ this.endRoundBtn.addEventListener('click', () => this.endGame());
+ }
+
+ // ============================================
+ // SOCKET.IO CONNECTION
+ // ============================================
+
+ async connectToGameSocket() {
+ const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
+ if (!token || !this.currentRoom) return;
+
+ // Ensure socket is connected
+ await this.ensureSocketConnected();
+
+ // Join the room
+ if (this.socket?.connected) {
+ console.log('Socket connected, joining room:', this.currentRoom.id);
+ this.socket.emit('game-join-room', { roomId: this.currentRoom.id });
+ }
+ }
+
+ async loadSocketIO() {
+ if (window.io) return;
+
+ return new Promise((resolve, reject) => {
+ const script = document.createElement('script');
+ script.src = '/socket.io/socket.io.js';
+
+ script.onload = () => {
+ console.log('Socket.IO loaded for game');
+ resolve();
+ };
+
+ script.onerror = () => {
+ console.error('Failed to load Socket.IO');
+ reject(new Error('Socket.IO load failed'));
+ };
+
+ document.head.appendChild(script);
+ });
+ }
+
+ setupGameSocketListeners() {
+ this.socketReady = false;
+
+ this.socket.on('connect', () => {
+ console.log('Game socket connected, id:', this.socket.id);
+ if (this.currentRoom) {
+ console.log('Joining room:', this.currentRoom.id);
+ this.socket.emit('game-join-room', { roomId: this.currentRoom.id });
+ }
+ });
+
+ this.socket.on('connect_error', (err) => {
+ console.error('Game socket connection error:', err.message);
+ });
+
+ // Confirmation that we joined the room
+ this.socket.on('game-room-joined', (data) => {
+ console.log('Successfully joined game room:', data.roomId);
+ this.socketReady = true;
+ });
+
+ // Real-time rooms list update
+ this.socket.on('game-rooms-updated', (data) => {
+ console.log('Rooms list updated:', data.rooms?.length, 'rooms');
+ if (this.currentTab === 'browse') {
+ this.roomsList = data.rooms || [];
+ this.renderRoomsList(this.roomsList);
+ }
+ });
+
+ // Real-time player list update in lobby
+ this.socket.on('game-players-updated', (data) => {
+ console.log('Players list updated:', data.players?.length, 'players');
+ if (this.currentRoom) {
+ this.renderPlayersList(data.players || []);
+ }
+ });
+
+ // Player joined/left
+ this.socket.on('game-player-joined', (data) => {
+ console.log(`${data.username} joined the room`);
+ });
+
+ this.socket.on('game-player-left', (data) => {
+ console.log(`${data.username} left the room`);
+ });
+
+ // Game started
+ this.socket.on('game-started', (data) => {
+ console.log('Received game-started event:', data);
+ this.gameState.isPlaying = true;
+ this.gameState.drawer = data.drawer;
+ this.gameState.players = data.players;
+ this.gameState.currentPlayerIndex = data.players.indexOf(data.drawer);
+
+ // Initialize scores
+ this.gameState.scores = {};
+ data.players.forEach(p => this.gameState.scores[p] = 0);
+
+ this.showGameUI();
+ this.setupRound();
+ });
+
+ // Word was set by drawer
+ this.socket.on('game-word-set', (data) => {
+ console.log(`Word set by ${data.drawer}, length: ${data.wordLength}`);
+ this.gameState.wordLength = data.wordLength;
+ this.gameState.revealedLetters = new Array(data.wordLength).fill(false);
+ this.gameState.revealedWord = data.revealedWord || new Array(data.wordLength).fill('_');
+
+ if (data.scores) {
+ this.updateScoresDisplay(data.scores);
+ }
+
+ this.updateWordDisplay();
+ this.currentDrawerInfo.textContent = `${data.drawer} dessine (${data.wordLength} lettres)`;
+
+ // Enable guess input for non-drawers
+ if (!this.isCurrentUserDrawer()) {
+ this.guessContainer.style.display = 'flex';
+ this.letterInput.disabled = false;
+ this.guessBtn.disabled = false;
+ this.letterInput.placeholder = 'Proposez une lettre ou le mot...';
+ this.letterInput.focus();
+ }
+ });
+
+ // Drawing received
+ this.socket.on('game-draw', (data) => {
+ this.drawLine(data.x1, data.y1, data.x2, data.y2, data.color, data.lineWidth);
+ });
+
+ // Clear canvas
+ this.socket.on('game-clear-canvas', () => {
+ this.clearCanvas();
+ });
+
+ // Guess result
+ this.socket.on('game-guess-result', (data) => {
+ this.addGuessToHistory(data.guess, data.success, data.type, data.username, data.points || 0);
+
+ if (data.revealedLetters) {
+ this.gameState.revealedLetters = data.revealedLetters;
+ }
+ if (data.revealedWord) {
+ this.gameState.revealedWord = data.revealedWord;
+ }
+ if (data.scores) {
+ this.updateScoresDisplay(data.scores);
+ }
+ this.updateWordDisplay();
+ });
+
+ // Word found
+ this.socket.on('game-word-found', (data) => {
+ if (data.scores) {
+ this.updateScoresDisplay(data.scores);
+ }
+ this.wordFound(data.word, data.winner, data.drawerBonus || 0);
+ });
+
+ // New round
+ this.socket.on('game-new-round', (data) => {
+ this.gameState.drawer = data.drawer;
+ this.gameState.currentPlayerIndex = this.gameState.players.indexOf(data.drawer);
+ this.setupRound();
+ });
+
+ // Game ended
+ this.socket.on('game-ended', () => {
+ this.resetGameUI();
+ });
+
+ // Sync state for late joiners
+ this.socket.on('game-state-sync', (data) => {
+ if (data.isPlaying) {
+ this.gameState.isPlaying = true;
+ this.gameState.drawer = data.drawer;
+ this.gameState.wordLength = data.wordLength;
+ this.gameState.revealedLetters = data.revealedLetters || [];
+ this.gameState.revealedWord = data.revealedWord || new Array(data.wordLength).fill('_');
+ this.gameState.players = data.players;
+
+ this.showGameUI();
+ this.updateWordDisplay();
+ this.currentDrawerInfo.textContent = `${data.drawer} dessine (${data.wordLength} lettres)`;
+
+ if (!this.isCurrentUserDrawer()) {
+ this.guessContainer.style.display = 'flex';
+ if (data.wordLength > 0) {
+ this.letterInput.disabled = false;
+ this.guessBtn.disabled = false;
+ this.letterInput.placeholder = 'Proposez une lettre ou le mot...';
+ } else {
+ this.letterInput.disabled = true;
+ this.guessBtn.disabled = true;
+ this.letterInput.placeholder = 'En attente du mot...';
+ }
+ }
+ }
+ });
+ }
+
+ disconnectGameSocket() {
+ if (this.socket) {
+ this.socket.emit('game-leave-room');
+ }
+ }
+
+ // ============================================
+ // UI HELPERS
+ // ============================================
+
+ isLoggedIn() {
+ return !!localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
+ }
+
+ updateTabsAccess() {
+ const loggedIn = this.isLoggedIn();
+
+ this.createTab.disabled = !loggedIn;
+ this.createTab.style.opacity = loggedIn ? '1' : '0.5';
+ this.createTab.title = loggedIn ? '' : 'Connectez-vous pour creer un salon';
+
+ if (!loggedIn && this.currentTab === 'create') {
+ this.switchTab('browse');
+ }
+ }
+
+ handleLogout() {
+ this.disconnectGameSocket();
+ if (this.currentRoom) {
+ this.exitLobby();
+ }
+ this.updateTabsAccess();
+ if (this.currentTab !== 'browse') {
+ this.switchTab('browse');
+ }
+ }
+
+ switchTab(tabName) {
+ if (tabName === 'lobby' && !this.currentRoom) {
+ return;
+ }
+
+ if (tabName === 'create' && !this.isLoggedIn()) {
+ this.showMessage('Connectez-vous pour creer un salon', 'info');
+ return;
+ }
+
+ this.currentTab = tabName;
+
+ [this.browseTab, 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.loadCurrentTab();
+ }
+
+ loadCurrentTab() {
+ switch (this.currentTab) {
+ case 'browse':
+ this.loadRooms();
+ // Connect to socket to receive real-time room updates
+ this.ensureSocketConnected();
+ break;
+ case 'create':
+ this.message.textContent = '';
+ this.ensureSocketConnected();
+ break;
+ case 'lobby':
+ if (this.currentRoom) {
+ this.loadLobby();
+ }
+ break;
+ }
+ }
+
+ async ensureSocketConnected() {
+ if (!this.isLoggedIn()) return;
+ if (this.socket?.connected) return;
+
+ const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
+ if (!token) return;
+
+ await this.loadSocketIO();
+
+ const ioConfig = {
+ auth: { token },
+ reconnection: true,
+ reconnectionAttempts: 5,
+ reconnectionDelay: 1000,
+ transports: ['websocket', 'polling']
+ };
+
+ const altPort = window.GLOBAL_CHAT_ALT_PORT;
+ if (altPort) {
+ const host = location.hostname || 'localhost';
+ this.socket = io(`http://${host}:${altPort}`, ioConfig);
+ } else {
+ this.socket = io(ioConfig);
+ }
+
+ this.setupGameSocketListeners();
+ }
+
+ getHeaders() {
+ const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
+ return {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json'
+ };
+ }
+
+ async loadRooms() {
+ const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
+ if (!token) {
+ this.showMessage('Connectez-vous pour voir les salons', 'info');
+ return;
+ }
+
+ try {
+ const response = await fetch(API.ROOMS.LIST, {
+ headers: this.getHeaders()
+ });
+ const data = await response.json();
+
+ if (!response.ok) {
+ this.showMessage(data.error || 'Erreur', 'error');
+ return;
+ }
+
+ this.roomsList = data || [];
+ this.renderRoomsList(this.roomsList);
+ } catch (error) {
+ console.error('Load rooms error:', error);
+ this.showMessage('Erreur de connexion', 'error');
+ }
+ }
+
+ async checkCurrentRoom() {
+ const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
+ if (!token) {
+ return null;
+ }
+
+ try {
+ const response = await fetch(API.ROOMS.CURRENT, {
+ headers: this.getHeaders()
+ });
+
+ // 204 No Content means user is not in any room
+ if (response.status === 204) {
+ return null;
+ }
+
+ if (!response.ok) {
+ return null;
+ }
+
+ const data = await response.json();
+ if (data && data.id) {
+ this.currentRoom = data;
+ this.enterLobby(data);
+ return data;
+ }
+ return null;
+ } catch (error) {
+ console.error('Check current room error:', error);
+ return null;
+ }
+ }
+
+ roomNameExists(name) {
+ const normalizedName = name.toLowerCase().trim();
+ return this.roomsList.some(room => room.name.toLowerCase().trim() === normalizedName);
+ }
+
+ renderRoomsList(rooms) {
+ this.list.innerHTML = '';
+ this.message.textContent = '';
+
+ if (rooms.length === 0) {
+ this.showMessage('Aucun salon disponible', 'info');
+ return;
+ }
+
+ rooms.forEach(room => {
+ const item = this.createRoomItem(room);
+ this.list.appendChild(item);
+ });
+ }
+
+ createRoomItem(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 actions = this.createElement('div', CSS.GAMEROOM_ACTIONS);
+
+ const joinBtn = this.createElement('button', [CSS.BTN, CSS.BTN_SUCCESS], {
+ text: 'Rejoindre'
+ });
+ joinBtn.addEventListener('click', () => this.joinRoom(room.id));
+ actions.appendChild(joinBtn);
+
+ item.append(name, players, actions);
+ return item;
+ }
+
+ async createRoom() {
+ const name = this.roomNameInput.value.trim();
+ if (!name) {
+ this.showMessage('Entrez un nom pour le salon', 'error');
+ return;
+ }
+
+ const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
+ if (!token) {
+ this.showMessage('Connectez-vous pour creer un salon', 'info');
+ return;
+ }
+
+ if (this.currentRoom) {
+ this.showMessage('Vous etes deja dans un salon. Quittez-le d\'abord.', 'error');
+ return;
+ }
+
+ try {
+ const currentResponse = await fetch(API.ROOMS.CURRENT, {
+ headers: this.getHeaders()
+ });
+ if (currentResponse.ok && currentResponse.status !== 204) {
+ const currentData = await currentResponse.json();
+ if (currentData && currentData.id) {
+ this.currentRoom = currentData;
+ this.enterLobby(currentData);
+ this.showMessage('Vous etes deja dans un salon', 'error');
+ return;
+ }
+ }
+ } catch (e) {
+ // Continue
+ }
+
+ try {
+ const listResponse = await fetch(API.ROOMS.LIST, {
+ headers: this.getHeaders()
+ });
+ if (listResponse.ok) {
+ this.roomsList = await listResponse.json() || [];
+ }
+ } catch (e) {
+ // Continue
+ }
+
+ if (this.roomNameExists(name)) {
+ this.showMessage('Un salon avec ce nom existe deja', 'error');
+ return;
+ }
+
+ try {
+ const response = await fetch(API.ROOMS.CREATE, {
+ method: 'POST',
+ headers: this.getHeaders(),
+ body: JSON.stringify({ name })
+ });
+ const data = await response.json();
+
+ if (!response.ok) {
+ this.showMessage(data.error || 'Erreur', 'error');
+ return;
+ }
+
+ this.roomNameInput.value = '';
+ this.currentRoom = data;
+ this.showMessage('Salon cree', 'success');
+ eventBus.emit(Events.ROOM_CREATED, data);
+ this.enterLobby(data);
+ } catch (error) {
+ console.error('Create room error:', error);
+ this.showMessage('Erreur de connexion', 'error');
+ }
+ }
+
+ async joinRoom(roomId) {
+ const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
+ if (!token) {
+ this.showMessage('Connectez-vous pour rejoindre', 'info');
+ return;
+ }
+
+ if (this.currentRoom) {
+ this.showMessage('Vous etes deja dans un salon. Quittez-le d\'abord.', 'error');
+ return;
+ }
+
+ try {
+ const currentResponse = await fetch(API.ROOMS.CURRENT, {
+ headers: this.getHeaders()
+ });
+ if (currentResponse.ok && currentResponse.status !== 204) {
+ const currentData = await currentResponse.json();
+ if (currentData && currentData.id) {
+ this.currentRoom = currentData;
+ this.enterLobby(currentData);
+ this.showMessage('Vous etes deja dans un salon', 'error');
+ return;
+ }
+ }
+ } catch (e) {
+ // Continue
+ }
+
+ try {
+ const response = await fetch(API.ROOMS.JOIN(roomId), {
+ method: 'POST',
+ headers: this.getHeaders()
+ });
+ const data = await response.json();
+
+ if (!response.ok) {
+ this.showMessage(data.error || 'Erreur', 'error');
+ return;
+ }
+
+ const roomResponse = await fetch(API.ROOMS.GET(roomId), {
+ headers: this.getHeaders()
+ });
+ const roomData = await roomResponse.json();
+
+ this.currentRoom = roomData;
+ eventBus.emit(Events.ROOM_JOINED, roomData);
+ this.enterLobby(roomData);
+ } catch (error) {
+ console.error('Join room error:', error);
+ this.showMessage('Erreur de connexion', 'error');
+ }
+ }
+
+ enterLobby(room) {
+ this.currentRoom = room;
+ this.lobbyTab.style.display = 'block';
+ this.lobbyTitle.textContent = room.name;
+ this.switchTab('lobby');
+
+ // Connect to WebSocket for real-time sync
+ this.connectToGameSocket();
+ }
+
+ async loadLobby() {
+ if (!this.currentRoom) return;
+
+ try {
+ const response = await fetch(API.ROOMS.PLAYERS(this.currentRoom.id), {
+ headers: this.getHeaders()
+ });
+ const data = await response.json();
+
+ if (!response.ok) {
+ this.showMessage(data.error || 'Erreur', 'error');
+ return;
+ }
+
+ this.renderPlayersList(data || []);
+ } catch (error) {
+ console.error('Load lobby error:', error);
+ this.showMessage('Erreur de connexion', 'error');
+ }
+ }
+
+ renderPlayersList(players) {
+ this.playerList.innerHTML = '';
+
+ if (players.length === 0) {
+ const empty = this.createElement('div', 'gameroom__empty', {
+ text: 'Aucun joueur'
+ });
+ this.playerList.appendChild(empty);
+ return;
+ }
+
+ players.forEach(player => {
+ const item = this.createElement('div', CSS.GAMEROOM_PLAYER);
+
+ const avatar = this.createElement('img', CSS.GAMEROOM_PLAYER_AVATAR, {
+ alt: player.username
+ });
+ avatar.src = player.avatar_url || '/avatar/default.png';
+
+ const name = this.createElement('span', CSS.GAMEROOM_PLAYER_NAME, {
+ text: player.username
+ });
+
+ const statsContainer = this.createElement('div', 'gameroom__player-stats');
+
+ const score = this.createElement('span', CSS.GAMEROOM_PLAYER_SCORE, {
+ text: `${player.score || 0} pts`
+ });
+
+ const totalPoints = this.createElement('span', 'gameroom__player-total', {
+ text: `Total: ${player.total_points || 0}`
+ });
+
+ statsContainer.append(score, totalPoints);
+ item.append(avatar, name, statsContainer);
+ this.playerList.appendChild(item);
+ });
+ }
+
+ async leaveRoom() {
+ if (!this.currentRoom) return;
+
+ // End game if playing
+ if (this.gameState.isPlaying) {
+ this.endGame();
+ }
+
+ this.disconnectGameSocket();
+
+ try {
+ const response = await fetch(API.ROOMS.LEAVE(this.currentRoom.id), {
+ method: 'POST',
+ headers: this.getHeaders()
+ });
+
+ if (!response.ok) {
+ const data = await response.json();
+ this.showMessage(data.error || 'Erreur', 'error');
+ return;
+ }
+
+ eventBus.emit(Events.ROOM_LEFT, this.currentRoom);
+ this.exitLobby();
+ } catch (error) {
+ console.error('Leave room error:', error);
+ this.showMessage('Erreur de connexion', 'error');
+ }
+ }
+
+ exitLobby() {
+ this.currentRoom = null;
+ this.lobbyTab.style.display = 'none';
+ this.playerList.innerHTML = '';
+ this.lobbyTitle.textContent = '';
+ this.resetGameUI();
+ this.switchTab('browse');
+ }
+
+ showMessage(text, type = 'info') {
+ this.message.textContent = text;
+ this.message.className = CSS.MESSAGE;
+
+ if (type === 'success') {
+ this.message.classList.add(CSS.MESSAGE_SUCCESS);
+ } else if (type === 'error') {
+ this.message.classList.add(CSS.MESSAGE_ERROR);
+ } else {
+ this.message.classList.add(CSS.MESSAGE_INFO);
+ }
+ }
+
+ // ============================================
+ // LOGIQUE DU JEU
+ // ============================================
+
+ getCurrentUsername() {
+ const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
+ if (!token) return null;
+ try {
+ const payload = JSON.parse(atob(token.split('.')[1]));
+ return payload.username || payload.sub || 'Joueur';
+ } catch {
+ return 'Joueur';
+ }
+ }
+
+ isCurrentUserDrawer() {
+ return this.gameState.drawer === this.getCurrentUsername();
+ }
+
+ showGameUI() {
+ this.gameContainer.style.display = 'flex';
+ this.playerList.style.display = 'none';
+ this.lobbyButtons.style.display = 'none';
+ this.clearCanvas();
+ this.guessHistory.innerHTML = '';
+ }
+
+ resetGameUI() {
+ this.gameState.isPlaying = false;
+ this.gameState.currentWord = '';
+ this.gameState.wordLength = 0;
+ this.gameState.revealedLetters = [];
+ this.gameState.revealedWord = [];
+ this.gameState.drawer = null;
+
+ this.gameContainer.style.display = 'none';
+ this.playerList.style.display = 'flex';
+ this.lobbyButtons.style.display = 'flex';
+
+ this.wordInputContainer.style.display = 'none';
+ this.guessContainer.style.display = 'none';
+ this.drawTools.style.display = 'none';
+
+ this.currentDrawerInfo.classList.remove('gameroom__drawer-info--winner');
+ }
+
+ async startGame() {
+ console.log('startGame called');
+
+ // Load player list
+ await this.loadLobby();
+
+ const playerElements = this.playerList.querySelectorAll('.gameroom__player-name');
+ const players = Array.from(playerElements).map(el => el.textContent);
+
+ console.log('Players found:', players);
+
+ if (players.length < 1) {
+ this.showMessage('Il faut au moins 1 joueur pour jouer', 'error');
+ return;
+ }
+
+ const drawer = players[0];
+
+ console.log('Socket connected:', this.socket?.connected, 'Socket ready:', this.socketReady);
+
+ // Send start game event via WebSocket
+ if (this.socket?.connected) {
+ console.log('Emitting game-start event');
+ this.socket.emit('game-start', { drawer, players });
+ } else {
+ console.log('No socket, using local fallback');
+ // Fallback local - start immediately
+ this.gameState.isPlaying = true;
+ this.gameState.players = players;
+ this.gameState.drawer = drawer;
+ this.gameState.currentPlayerIndex = 0;
+ this.showGameUI();
+ this.setupRound();
+ }
+ }
+
+ setupRound() {
+ this.gameState.currentWord = '';
+ this.gameState.wordLength = 0;
+ this.gameState.revealedLetters = [];
+ this.gameState.revealedWord = [];
+ this.gameState.guessedLetters = [];
+
+ this.currentDrawerInfo.textContent = `C'est au tour de ${this.gameState.drawer} de dessiner`;
+ this.currentDrawerInfo.classList.remove('gameroom__drawer-info--winner');
+ this.wordDisplay.textContent = '';
+ this.guessHistory.innerHTML = '';
+ this.clearCanvas();
+
+ if (this.isCurrentUserDrawer()) {
+ // Drawer chooses a word
+ this.wordInputContainer.style.display = 'flex';
+ this.guessContainer.style.display = 'none';
+ this.drawTools.style.display = 'none';
+ this.currentDrawerInfo.textContent = 'Choisissez un mot a faire deviner';
+ } else {
+ // Others see the guess input (disabled while waiting for word)
+ this.wordInputContainer.style.display = 'none';
+ this.guessContainer.style.display = 'flex';
+ this.drawTools.style.display = 'none';
+ this.letterInput.disabled = true;
+ this.guessBtn.disabled = true;
+ this.letterInput.placeholder = 'En attente du mot...';
+ this.currentDrawerInfo.textContent = `${this.gameState.drawer} choisit un mot...`;
+ }
+ }
+
+ confirmWord() {
+ const word = this.wordInput.value.trim().toLowerCase();
+ if (!word || word.length < 2) {
+ this.showMessage('Le mot doit faire au moins 2 lettres', 'error');
+ return;
+ }
+
+ if (!/^[a-z]+$/.test(word)) {
+ this.showMessage('Le mot ne doit contenir que des lettres', 'error');
+ return;
+ }
+
+ this.gameState.currentWord = word;
+ this.gameState.wordLength = word.length;
+ this.gameState.revealedLetters = new Array(word.length).fill(false);
+ this.gameState.revealedWord = new Array(word.length).fill('_');
+
+ this.wordInput.value = '';
+ this.wordInputContainer.style.display = 'none';
+ this.drawTools.style.display = 'flex';
+
+ // Send word to server via WebSocket
+ if (this.socket?.connected) {
+ this.socket.emit('game-set-word', { word });
+ }
+
+ this.updateWordDisplay();
+ this.currentDrawerInfo.textContent = `Dessinez pour faire deviner le mot (${word.length} lettres)`;
+ }
+
+ updateWordDisplay() {
+ // If drawer, show from currentWord
+ if (this.isCurrentUserDrawer() && this.gameState.currentWord) {
+ let display = '';
+ for (let i = 0; i < this.gameState.currentWord.length; i++) {
+ if (this.gameState.revealedLetters && this.gameState.revealedLetters[i]) {
+ display += this.gameState.currentWord[i] + ' ';
+ } else {
+ display += '_ ';
+ }
+ }
+ this.wordDisplay.textContent = display.trim();
+ return;
+ }
+
+ // For guessers, use revealedWord from server
+ if (this.gameState.revealedWord && this.gameState.revealedWord.length > 0) {
+ this.wordDisplay.textContent = this.gameState.revealedWord.join(' ');
+ return;
+ }
+
+ // Fallback: show underscores based on wordLength
+ if (this.gameState.wordLength > 0) {
+ this.wordDisplay.textContent = '_ '.repeat(this.gameState.wordLength).trim();
+ }
+ }
+
+ makeGuess() {
+ const guess = this.letterInput.value.trim().toLowerCase();
+ if (!guess) return;
+
+ this.letterInput.value = '';
+
+ // Send guess via WebSocket
+ if (this.socket?.connected) {
+ this.socket.emit('game-guess', { guess });
+ } else {
+ // Fallback local (for testing)
+ this.processGuessLocally(guess);
+ }
+ }
+
+ processGuessLocally(guess) {
+ const username = this.getCurrentUsername();
+
+ if (guess.length > 1) {
+ const success = guess === this.gameState.currentWord;
+ this.addGuessToHistory(guess, success, 'word', username);
+ if (success) {
+ this.gameState.revealedWord = this.gameState.currentWord.split('');
+ this.wordFound(this.gameState.currentWord, username);
+ }
+ return;
+ }
+
+ if (this.gameState.guessedLetters.includes(guess)) {
+ this.showMessage('Lettre deja proposee', 'info');
+ return;
+ }
+
+ this.gameState.guessedLetters.push(guess);
+
+ let found = false;
+ for (let i = 0; i < this.gameState.currentWord.length; i++) {
+ if (this.gameState.currentWord[i] === guess) {
+ this.gameState.revealedLetters[i] = true;
+ this.gameState.revealedWord[i] = guess;
+ found = true;
+ }
+ }
+
+ this.addGuessToHistory(guess, found, 'letter', username);
+ this.updateWordDisplay();
+
+ if (this.gameState.revealedLetters.every(r => r)) {
+ this.wordFound(this.gameState.currentWord, username);
+ }
+ }
+
+ addGuessToHistory(guess, success, type, username, points = 0) {
+ const item = this.createElement('div', 'gameroom__guess-item');
+ item.classList.add(success ? 'gameroom__guess-item--success' : 'gameroom__guess-item--fail');
+
+ const typeText = type === 'letter' ? 'lettre' : 'mot';
+ const pointsText = points !== 0 ? ` (${points > 0 ? '+' : ''}${points} pts)` : '';
+
+ if (success) {
+ item.textContent = `${username}: "${guess}" - Bonne ${typeText}!${pointsText}`;
+ } else {
+ item.textContent = `${username}: "${guess}" - Mauvais ${typeText}${pointsText}`;
+ }
+
+ this.guessHistory.appendChild(item);
+ this.guessHistory.scrollTop = this.guessHistory.scrollHeight;
+ }
+
+ updateScoresDisplay(scores) {
+ if (!scores) return;
+ this.gameState.scores = scores;
+
+ // Update scores display in game UI
+ if (this.scoresDisplay) {
+ const sortedScores = Object.entries(scores)
+ .sort((a, b) => b[1] - a[1])
+ .map(([name, score]) => `${name}: ${score}`)
+ .join(' | ');
+ this.scoresDisplay.textContent = sortedScores;
+ }
+
+ // Update player list with scores if visible
+ const playerItems = this.playerList.querySelectorAll('.gameroom__player');
+ playerItems.forEach(item => {
+ const nameEl = item.querySelector('.gameroom__player-name');
+ const scoreEl = item.querySelector('.gameroom__player-score');
+ if (nameEl && scoreEl) {
+ const playerName = nameEl.textContent;
+ const score = scores[playerName] || 0;
+ scoreEl.textContent = `${score} pts`;
+ }
+ });
+ }
+
+ wordFound(word, winner, drawerBonus = 0) {
+ let message = `${winner} a trouve le mot: ${word}!`;
+ if (drawerBonus > 0 && this.gameState.drawer) {
+ message += ` (${this.gameState.drawer} +${drawerBonus} pts)`;
+ }
+ this.currentDrawerInfo.textContent = message;
+ this.currentDrawerInfo.classList.add('gameroom__drawer-info--winner');
+
+ this.guessContainer.style.display = 'none';
+ this.drawTools.style.display = 'none';
+
+ // Reveal full word
+ this.wordDisplay.textContent = word.split('').join(' ');
+
+ // Auto next round after delay
+ setTimeout(() => {
+ if (this.gameState.isPlaying) {
+ this.nextRound();
+ }
+ }, 3000);
+ }
+
+ nextRound() {
+ // Move to next player
+ this.gameState.currentPlayerIndex = (this.gameState.currentPlayerIndex + 1) % this.gameState.players.length;
+ const nextDrawer = this.gameState.players[this.gameState.currentPlayerIndex];
+
+ if (this.socket?.connected) {
+ this.socket.emit('game-next-round', { drawer: nextDrawer });
+ } else {
+ this.gameState.drawer = nextDrawer;
+ this.setupRound();
+ }
+ }
+
+ backToLobby() {
+ // Return to lobby without ending game for others
+ this.resetGameUI();
+ this.loadLobby();
+ }
+
+ endGame() {
+ if (this.socket?.connected) {
+ this.socket.emit('game-end');
+ }
+ this.resetGameUI();
+ this.showMessage('Jeu termine', 'info');
+ }
+}
diff --git a/Transcendence/srcs/frontend/src/index.html b/Transcendence/srcs/frontend/src/index.html
index 0003109..771cbcc 100644
--- a/Transcendence/srcs/frontend/src/index.html
+++ b/Transcendence/srcs/frontend/src/index.html
@@ -17,6 +17,7 @@
+
diff --git a/Transcendence/srcs/frontend/src/style.css b/Transcendence/srcs/frontend/src/style.css
index 4714622..3809dcf 100644
--- a/Transcendence/srcs/frontend/src/style.css
+++ b/Transcendence/srcs/frontend/src/style.css
@@ -460,6 +460,34 @@ body {
display: none;
}
+.avatar__stats {
+ display: flex;
+ justify-content: center;
+ gap: var(--spacing-lg);
+ margin: var(--spacing-md) 0;
+ padding: var(--spacing-sm);
+ background: var(--color-surface);
+ border-radius: var(--radius-md);
+}
+
+.avatar__stat {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: var(--spacing-xs);
+}
+
+.avatar__stat-label {
+ font-size: var(--font-size-sm);
+ color: var(--color-text-muted);
+}
+
+.avatar__stat-value {
+ font-size: var(--font-size-lg);
+ font-weight: 600;
+ color: var(--color-success);
+}
+
/* ============================================
EASTER EGG BUTTON
============================================ */
@@ -588,12 +616,23 @@ body {
border: 2px solid var(--color-surface-light);
}
-.friends__name {
+.friends__info {
flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.friends__name {
font-size: var(--font-size-md);
font-weight: 500;
}
+.friends__stats {
+ font-size: var(--font-size-sm);
+ color: var(--color-success);
+}
+
.friends__actions {
display: flex;
gap: var(--spacing-xs);
@@ -609,3 +648,316 @@ body {
color: var(--color-text-muted);
padding: var(--spacing-lg);
}
+
+/* ============================================
+ GAME ROOM WINDOW
+ ============================================ */
+.gameroom-window {
+ width: 420px;
+ height: 480px;
+}
+
+.gameroom__tabs {
+ display: flex;
+ gap: var(--spacing-xs);
+ margin-bottom: var(--spacing-sm);
+}
+
+.gameroom__tab {
+ flex: 1;
+ padding: var(--spacing-sm);
+ background: var(--color-surface);
+ border: 1px solid var(--color-surface-light);
+ color: var(--color-text);
+ cursor: pointer;
+ font-size: var(--font-size-sm);
+ transition: all var(--transition-fast);
+}
+
+.gameroom__tab:hover {
+ background: var(--color-surface-light);
+}
+
+.gameroom__tab--active {
+ background: var(--color-primary);
+ border-color: var(--color-primary);
+}
+
+.gameroom__content {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ overflow: hidden;
+}
+
+.gameroom__create {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-sm);
+ margin-bottom: var(--spacing-sm);
+}
+
+.gameroom__list {
+ flex: 1;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-sm);
+}
+
+.gameroom__item {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-sm);
+ padding: var(--spacing-sm);
+ background: var(--color-surface);
+ border-radius: var(--radius-md);
+}
+
+.gameroom__name {
+ flex: 1;
+ font-size: var(--font-size-md);
+ font-weight: 500;
+}
+
+.gameroom__players {
+ font-size: var(--font-size-sm);
+ color: var(--color-text-muted);
+ padding: var(--spacing-xs) var(--spacing-sm);
+ background: var(--color-surface-light);
+ border-radius: var(--radius-sm);
+}
+
+.gameroom__actions {
+ display: flex;
+ gap: var(--spacing-xs);
+}
+
+.gameroom__actions .btn {
+ padding: var(--spacing-xs) var(--spacing-sm);
+ font-size: var(--font-size-sm);
+}
+
+.gameroom__lobby {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ gap: var(--spacing-sm);
+}
+
+.gameroom__lobby-title {
+ margin: 0;
+ font-size: var(--font-size-lg);
+ text-align: center;
+ color: var(--color-success);
+}
+
+.gameroom__player-list {
+ flex: 1;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-sm);
+ background: var(--color-surface);
+ border-radius: var(--radius-md);
+ padding: var(--spacing-sm);
+}
+
+.gameroom__player {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-sm);
+ padding: var(--spacing-xs) var(--spacing-sm);
+ background: var(--color-surface-light);
+ border-radius: var(--radius-sm);
+}
+
+.gameroom__player-avatar {
+ width: 32px;
+ height: 32px;
+ border-radius: var(--radius-full);
+ object-fit: cover;
+ border: 2px solid var(--color-surface-light);
+}
+
+.gameroom__player-name {
+ flex: 1;
+ font-size: var(--font-size-md);
+}
+
+.gameroom__player-stats {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ gap: 2px;
+}
+
+.gameroom__player-score {
+ font-size: var(--font-size-sm);
+ color: var(--color-success);
+ font-weight: 500;
+}
+
+.gameroom__player-total {
+ font-size: var(--font-size-sm);
+ color: var(--color-text-muted);
+}
+
+.gameroom__empty {
+ text-align: center;
+ color: var(--color-text-muted);
+ padding: var(--spacing-lg);
+}
+
+/* ============================================
+ GAME - JEU DU PENDU/DESSIN
+ ============================================ */
+.gameroom__lobby-buttons {
+ display: flex;
+ gap: var(--spacing-sm);
+ margin-top: auto;
+}
+
+.gameroom__lobby-buttons .btn {
+ flex: 1;
+}
+
+.gameroom__game {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-sm);
+ flex: 1;
+}
+
+.gameroom__game-info {
+ text-align: center;
+}
+
+.gameroom__drawer-info {
+ font-size: var(--font-size-md);
+ color: var(--color-text-muted);
+ padding: var(--spacing-xs);
+}
+
+.gameroom__scores-display {
+ font-size: var(--font-size-sm);
+ color: var(--color-success);
+ padding: var(--spacing-xs);
+ background: var(--color-surface);
+ border-radius: var(--radius-sm);
+ margin-top: var(--spacing-xs);
+}
+
+.gameroom__drawer-info--winner {
+ color: var(--color-success);
+ font-weight: bold;
+ animation: pulse 0.5s ease-in-out 3;
+}
+
+@keyframes pulse {
+ 0%, 100% { transform: scale(1); }
+ 50% { transform: scale(1.05); }
+}
+
+.gameroom__word-display {
+ font-size: var(--font-size-xl);
+ font-family: monospace;
+ text-align: center;
+ letter-spacing: 8px;
+ padding: var(--spacing-md);
+ background: var(--color-surface);
+ border-radius: var(--radius-md);
+ min-height: 60px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--color-success);
+}
+
+.gameroom__canvas-container {
+ display: flex;
+ justify-content: center;
+}
+
+.gameroom__canvas {
+ background: var(--color-surface-light);
+ border-radius: var(--radius-md);
+ cursor: crosshair;
+ border: 2px solid var(--color-surface-light);
+}
+
+.gameroom__draw-tools {
+ display: flex;
+ gap: var(--spacing-sm);
+ justify-content: center;
+ align-items: center;
+}
+
+.gameroom__color-picker {
+ width: 40px;
+ height: 32px;
+ border: none;
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ background: transparent;
+}
+
+.gameroom__word-input-container,
+.gameroom__guess-container {
+ display: flex;
+ gap: var(--spacing-sm);
+}
+
+.gameroom__word-input-container .input,
+.gameroom__guess-container .input {
+ flex: 1;
+}
+
+.gameroom__guess-container .input:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.gameroom__guess-container .btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.gameroom__guess-history {
+ flex: 1;
+ min-height: 60px;
+ max-height: 100px;
+ overflow-y: auto;
+ background: var(--color-surface);
+ border-radius: var(--radius-md);
+ padding: var(--spacing-sm);
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-xs);
+}
+
+.gameroom__guess-item {
+ font-size: var(--font-size-sm);
+ padding: var(--spacing-xs) var(--spacing-sm);
+ border-radius: var(--radius-sm);
+}
+
+.gameroom__guess-item--success {
+ background: rgba(60, 255, 1, 0.2);
+ color: var(--color-success);
+}
+
+.gameroom__guess-item--fail {
+ background: rgba(255, 77, 77, 0.2);
+ color: var(--color-error);
+}
+
+.gameroom__game-buttons {
+ display: flex;
+ gap: var(--spacing-sm);
+ margin-top: var(--spacing-sm);
+}
+
+.gameroom__game-buttons .btn {
+ flex: 1;
+}