jeu implemente HAHAHAHAHHAHAHAbUGHAHAHAHBUGHAHAHAHbugAHHAHbugAHAHAHHAHAHAHHAHA
This commit is contained in:
@@ -1,2 +1,3 @@
|
|||||||
srcs/.DS_Store
|
srcs/.DS_Store
|
||||||
*.DS_Store
|
*.DS_Store
|
||||||
|
srcs/backend/avatar/*
|
||||||
@@ -28,6 +28,33 @@ async function waitForDb(retries = 10, delay = 2000)
|
|||||||
throw new Error('Could not connect to database after multiple attempts');
|
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()
|
async function createTables()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -39,6 +66,9 @@ async function createTables()
|
|||||||
password_hash TEXT NOT NULL,
|
password_hash TEXT NOT NULL,
|
||||||
email VARCHAR(100),
|
email VARCHAR(100),
|
||||||
avatar_url TEXT DEFAULT '/avatar/default.png',
|
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()
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -148,6 +178,7 @@ export
|
|||||||
{
|
{
|
||||||
waitForDb,
|
waitForDb,
|
||||||
createTables,
|
createTables,
|
||||||
|
runMigrations,
|
||||||
query,
|
query,
|
||||||
ensureOauthClient
|
ensureOauthClient
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import chatRouter from './routes/global_chat.js';
|
|||||||
import gameRoomRouter from './routes/game_room.js';
|
import gameRoomRouter from './routes/game_room.js';
|
||||||
import avatarRouter from './routes/avatar.js';
|
import avatarRouter from './routes/avatar.js';
|
||||||
import friendsRouter from './routes/friends.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 setupSocketIO from './services/socket.js';
|
||||||
import avatarService from './services/avatar.js';
|
import avatarService from './services/avatar.js';
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ async function startServer()
|
|||||||
{
|
{
|
||||||
await waitForDb();
|
await waitForDb();
|
||||||
await createTables();
|
await createTables();
|
||||||
|
await runMigrations();
|
||||||
|
|
||||||
// Ensure GitHub OAuth client is registered in DB
|
// Ensure GitHub OAuth client is registered in DB
|
||||||
try {
|
try {
|
||||||
@@ -45,6 +47,7 @@ async function startServer()
|
|||||||
app.use('/api/rooms', gameRoomRouter);
|
app.use('/api/rooms', gameRoomRouter);
|
||||||
app.use('/api/avatar', avatarRouter);
|
app.use('/api/avatar', avatarRouter);
|
||||||
app.use('/api/friends', friendsRouter);
|
app.use('/api/friends', friendsRouter);
|
||||||
|
app.use('/api/stats', playerStatsRouter);
|
||||||
app.get('/api', (req, res) => res.send('Backend running'));
|
app.get('/api', (req, res) => res.send('Backend running'));
|
||||||
|
|
||||||
server.listen(3001, () =>
|
server.listen(3001, () =>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import gameRoomService from '../services/game_room.js';
|
import gameRoomService from '../services/game_room.js';
|
||||||
import authenticateToken from '../middleware/auth.js';
|
import authenticateToken from '../middleware/auth.js';
|
||||||
|
import { getIO, broadcastRoomsList } from '../services/socket.js';
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.get('/', authenticateToken, async(req, res) =>
|
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) =>
|
router.get('/:roomId', authenticateToken, async(req, res) =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -55,6 +73,13 @@ router.post('/', authenticateToken, async(req, res) =>
|
|||||||
if (!name)
|
if (!name)
|
||||||
return (res.status(400).json({error: 'Room name required'}));
|
return (res.status(400).json({error: 'Room name required'}));
|
||||||
const room = await gameRoomService.createRoom(name, req.user.userId);
|
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);
|
res.status(201).json(room);
|
||||||
}
|
}
|
||||||
catch(err)
|
catch(err)
|
||||||
@@ -69,6 +94,13 @@ router.post('/:roomId/join', authenticateToken, async(req, res) =>
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
const player = await gameRoomService.joinRoom(req.params.roomId, req.user.userId);
|
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);
|
res.json(player);
|
||||||
}
|
}
|
||||||
catch(err)
|
catch(err)
|
||||||
@@ -86,6 +118,13 @@ router.post('/:roomId/leave', authenticateToken, async(req, res) =>
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
await gameRoomService.leaveRoom(req.params.roomId, req.user.userId);
|
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'});
|
res.json({message: 'Left room successfully'});
|
||||||
}
|
}
|
||||||
catch(err)
|
catch(err)
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -6,7 +6,7 @@ import { query } from '../db.js';
|
|||||||
async function getFriends(userId) {
|
async function getFriends(userId) {
|
||||||
try {
|
try {
|
||||||
const result = await query(
|
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
|
FROM friendship f
|
||||||
JOIN users u ON (
|
JOIN users u ON (
|
||||||
CASE
|
CASE
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ async function getRoomPlayers(roomId)
|
|||||||
{
|
{
|
||||||
const result = await query
|
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
|
FROM game_players gp
|
||||||
JOIN users u ON gp.user_id = u.id
|
JOIN users u ON gp.user_id = u.id
|
||||||
WHERE gp.room_id = $1
|
WHERE gp.room_id = $1
|
||||||
@@ -108,6 +108,21 @@ async function getRoomPlayers(roomId)
|
|||||||
return (result.rows);
|
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
|
export default
|
||||||
{
|
{
|
||||||
createRoom,
|
createRoom,
|
||||||
@@ -115,5 +130,6 @@ export default
|
|||||||
listActiveRooms,
|
listActiveRooms,
|
||||||
joinRoom,
|
joinRoom,
|
||||||
leaveRoom,
|
leaveRoom,
|
||||||
getRoomPlayers
|
getRoomPlayers,
|
||||||
|
getCurrentRoom
|
||||||
};
|
};
|
||||||
@@ -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
|
||||||
|
};
|
||||||
@@ -1,9 +1,48 @@
|
|||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import chatService from './global_chat.js';
|
import chatService from './global_chat.js';
|
||||||
import friendsService from './friends.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)
|
function setupSocketIO(io)
|
||||||
{
|
{
|
||||||
|
ioInstance = io;
|
||||||
io.use((socket, next) =>
|
io.use((socket, next) =>
|
||||||
{
|
{
|
||||||
const token = socket.handshake.auth.token;
|
const token = socket.handshake.auth.token;
|
||||||
@@ -63,11 +102,356 @@ function setupSocketIO(io)
|
|||||||
socket.emit('error', {message: 'Failed to send message'});
|
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}`);
|
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;
|
export default setupSocketIO;
|
||||||
@@ -7,6 +7,7 @@ import { LoginWindow } from './login.js';
|
|||||||
import { GlobalChat } from './global_chat.js';
|
import { GlobalChat } from './global_chat.js';
|
||||||
import { AvatarWindow } from './avatar.js';
|
import { AvatarWindow } from './avatar.js';
|
||||||
import { FriendsWindow } from './friends.js';
|
import { FriendsWindow } from './friends.js';
|
||||||
|
import { GameRoomWindow } from './game_room.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main application class
|
* Main application class
|
||||||
@@ -23,11 +24,11 @@ class App {
|
|||||||
* Initializes all windows
|
* Initializes all windows
|
||||||
*/
|
*/
|
||||||
initWindows() {
|
initWindows() {
|
||||||
// Windows automatically register themselves in the registry
|
|
||||||
new LoginWindow();
|
new LoginWindow();
|
||||||
new GlobalChat();
|
new GlobalChat();
|
||||||
new AvatarWindow();
|
new AvatarWindow();
|
||||||
new FriendsWindow();
|
new FriendsWindow();
|
||||||
|
new GameRoomWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,12 +42,12 @@ class App {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Action to window name mapping
|
|
||||||
const actionMap = {
|
const actionMap = {
|
||||||
'login': 'login',
|
'login': 'login',
|
||||||
'chat': 'chat',
|
'chat': 'chat',
|
||||||
'avatar': 'avatar',
|
'avatar': 'avatar',
|
||||||
'friends': 'friends'
|
'friends': 'friends',
|
||||||
|
'gameroom': 'gameroom'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Event delegation on the menu
|
// Event delegation on the menu
|
||||||
@@ -72,7 +73,7 @@ class App {
|
|||||||
const easterEgg = document.querySelector('.easter-egg');
|
const easterEgg = document.querySelector('.easter-egg');
|
||||||
if (easterEgg) {
|
if (easterEgg) {
|
||||||
easterEgg.addEventListener('click', () => {
|
easterEgg.addEventListener('click', () => {
|
||||||
alert('You clicked when we told you not to!');
|
alert('DONT CLICK!');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,13 @@ export class AvatarWindow extends Window {
|
|||||||
// Username display
|
// Username display
|
||||||
this.username = this.createElement('div', CSS.AVATAR_USERNAME);
|
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
|
// Hidden file input
|
||||||
this.fileInput = this.createElement('input', 'avatar__file-input', {
|
this.fileInput = this.createElement('input', 'avatar__file-input', {
|
||||||
type: 'file',
|
type: 'file',
|
||||||
@@ -64,6 +71,7 @@ export class AvatarWindow extends Window {
|
|||||||
this.body.append(
|
this.body.append(
|
||||||
this.preview,
|
this.preview,
|
||||||
this.username,
|
this.username,
|
||||||
|
this.statsContainer,
|
||||||
this.fileInput,
|
this.fileInput,
|
||||||
this.controls,
|
this.controls,
|
||||||
this.message
|
this.message
|
||||||
@@ -148,6 +156,46 @@ export class AvatarWindow extends Window {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading avatar:', 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 = `<span class="avatar__stat-label">Points:</span> <span class="avatar__stat-value">${stats.total_points || 0}</span>`;
|
||||||
|
this.gamesPlayedDisplay.innerHTML = `<span class="avatar__stat-label">Parties:</span> <span class="avatar__stat-value">${stats.games_played || 0}</span>`;
|
||||||
|
this.gamesWonDisplay.innerHTML = `<span class="avatar__stat-label">Victoires:</span> <span class="avatar__stat-value">${stats.games_won || 0}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -20,6 +20,20 @@ export const API = {
|
|||||||
REQUEST: '/api/friends/request',
|
REQUEST: '/api/friends/request',
|
||||||
ACCEPT: '/api/friends/accept',
|
ACCEPT: '/api/friends/accept',
|
||||||
DECLINE: '/api/friends/decline'
|
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_NAME: 'friends__name',
|
||||||
FRIENDS_ACTIONS: 'friends__actions',
|
FRIENDS_ACTIONS: 'friends__actions',
|
||||||
FRIENDS_SEARCH: 'friends__search',
|
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)
|
// Colors (for reference, mainly used in CSS)
|
||||||
|
|||||||
@@ -84,5 +84,10 @@ export const Events = {
|
|||||||
// Chat
|
// Chat
|
||||||
CHAT_CONNECTED: 'chat:connected',
|
CHAT_CONNECTED: 'chat:connected',
|
||||||
CHAT_DISCONNECTED: 'chat:disconnected',
|
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'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -290,10 +290,22 @@ export class FriendsWindow extends Window {
|
|||||||
});
|
});
|
||||||
avatar.src = user.avatar_url || '/avatar/default.png';
|
avatar.src = user.avatar_url || '/avatar/default.png';
|
||||||
|
|
||||||
|
const infoContainer = this.createElement('div', 'friends__info');
|
||||||
|
|
||||||
const name = this.createElement('span', CSS.FRIENDS_NAME, {
|
const name = this.createElement('span', CSS.FRIENDS_NAME, {
|
||||||
text: user.username
|
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);
|
const actions = this.createElement('div', CSS.FRIENDS_ACTIONS);
|
||||||
|
|
||||||
if (type === 'friend') {
|
if (type === 'friend') {
|
||||||
@@ -322,7 +334,7 @@ export class FriendsWindow extends Window {
|
|||||||
actions.appendChild(addBtn);
|
actions.appendChild(addBtn);
|
||||||
}
|
}
|
||||||
|
|
||||||
item.append(avatar, name, actions);
|
item.append(avatar, infoContainer, actions);
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,7 @@
|
|||||||
<button class="menu__item" data-action="chat" aria-label="Global chat">Global chat</button>
|
<button class="menu__item" data-action="chat" aria-label="Global chat">Global chat</button>
|
||||||
<button class="menu__item" data-action="avatar" aria-label="Avatar">Avatar</button>
|
<button class="menu__item" data-action="avatar" aria-label="Avatar">Avatar</button>
|
||||||
<button class="menu__item" data-action="friends" aria-label="Amis">Amis</button>
|
<button class="menu__item" data-action="friends" aria-label="Amis">Amis</button>
|
||||||
|
<button class="menu__item" data-action="gameroom" aria-label="Game Rooms">Game Rooms</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<button class="easter-egg" data-action="easter-egg">Ne cliquez pas !</button>
|
<button class="easter-egg" data-action="easter-egg">Ne cliquez pas !</button>
|
||||||
|
|||||||
@@ -460,6 +460,34 @@ body {
|
|||||||
display: none;
|
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
|
EASTER EGG BUTTON
|
||||||
============================================ */
|
============================================ */
|
||||||
@@ -588,12 +616,23 @@ body {
|
|||||||
border: 2px solid var(--color-surface-light);
|
border: 2px solid var(--color-surface-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.friends__name {
|
.friends__info {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.friends__name {
|
||||||
font-size: var(--font-size-md);
|
font-size: var(--font-size-md);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.friends__stats {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--color-success);
|
||||||
|
}
|
||||||
|
|
||||||
.friends__actions {
|
.friends__actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing-xs);
|
gap: var(--spacing-xs);
|
||||||
@@ -609,3 +648,316 @@ body {
|
|||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
padding: var(--spacing-lg);
|
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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user