jeu implemente HAHAHAHAHHAHAHAbUGHAHAHAHBUGHAHAHAHbugAHHAHbugAHAHAHHAHAHAHHAHA

This commit is contained in:
2026-01-30 20:43:51 +01:00
parent ee73bcc35a
commit eca550b2ae
17 changed files with 2335 additions and 14 deletions
+2 -1
View File
@@ -1,2 +1,3 @@
srcs/.DS_Store
*.DS_Store
*.DS_Store
srcs/backend/avatar/*
+31
View File
@@ -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
};
+4 -1
View File
@@ -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, () =>
@@ -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)
@@ -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) {
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
@@ -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
};
@@ -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
};
+385 -1
View File
@@ -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;
+5 -4
View File
@@ -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!');
});
}
}
+48
View File
@@ -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 = `<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>`;
}
/**
+34 -1
View File
@@ -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)
+6 -1
View File
@@ -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'
};
+13 -1
View File
@@ -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;
}
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="avatar" aria-label="Avatar">Avatar</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>
<button class="easter-egg" data-action="easter-egg">Ne cliquez pas !</button>
+353 -1
View File
@@ -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;
}