jeu implemente HAHAHAHAHHAHAHAbUGHAHAHAHBUGHAHAHAHbugAHHAHbugAHAHAHHAHAHAHHAHA
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
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');
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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!');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user