17 Commits

Author SHA1 Message Date
Thomas Fauve-Piot aae651aa8b Front done 2026-03-24 21:02:48 +01:00
Kali Gallon ec560f3447 a faire 2026-03-24 19:38:31 +01:00
Kali Gallon 6df0f24ef6 ^^._, work in progress, small changes 2026-03-24 19:35:13 +01:00
Kali Gallon d8b97ebe17 Merge branch 'LosGringos' into kali 2026-03-24 19:00:32 +01:00
Kali Gallon 98d30c85b2 Merge branch 'LosGringos' of github.com:OlaketalAmigo/Transcendence into LosGringos 2026-03-24 19:00:04 +01:00
Kali Gallon ec36271886 ^^._, work in progress, small changes 2026-03-24 18:58:14 +01:00
Kali Gallon 029c8a6650 ^^._, work in progress, small changes 2026-03-24 18:56:09 +01:00
Thomas Fauve-Piot 0a6e9a25ed Added doodles.png 2026-03-24 18:52:30 +01:00
Kali Gallon e764d565c1 fix merge confict? 2026-03-24 17:02:11 +01:00
Kali Gallon b0fc705d26 ^^._, work in progress, small changes 2026-03-24 15:36:43 +01:00
Thomas Fauve-Piot cb1fc01ad6 Animated but not smooth enough 2026-03-23 23:01:25 +01:00
Kali Gallon 5299f3d1af ^^._, work in progress, small changes 2026-03-20 19:44:08 +01:00
Thomas Fauve-Piot 27704b97f8 Home page's front good 2026-03-20 17:57:00 +01:00
Kali Gallon 2eaae81f28 first impression, i did nothing 2026-03-20 15:16:10 +01:00
Kali Gallon 6f5d27f6a2 ^^._, work in progress, small changes 2026-03-20 15:13:10 +01:00
Thomas Fauve-Piot 938d4cf3b5 45env45 2026-03-20 15:01:39 +01:00
Thomas Fauve-Piot 167896aedd Front started 2026-03-19 23:08:45 +01:00
111 changed files with 1145 additions and 2516 deletions
+10
View File
@@ -0,0 +1,10 @@
POSTGRES_PASSWORD=coucou
JWT_SECRET=superlongsecretkeyatleast32characterspleasenevercommitthis
POSTGRES_DB=database
POSTGRES_HOST=database
POSTGRES_USER=user
GITHUB_CLIENT_ID=Ov23li6ovg3fzec5IO5D
GITHUB_CLIENT_SECRET=0345e959e8f0e9f784061c5c90ee227ddb2ef9ab
GITHUB_CALLBACK_URL=http://localhost:8080/api/auth/github/callback
pogpog
-142
View File
@@ -1,142 +0,0 @@
*This project has been created as part of the 42 curriculum by agallon, gprunet, yantoine and tfauve-p*
**DESCRIPTION**
For starters, ft_transcendence is a wonderful project based on building a web-application running from Docker containers where the goal is, for the first time to do whatever we want, yet still we need to follow multiples criteria based on a number of points to grind to set the project as finished.
For such a project our group thought about the "CATETRIBBL.IO". We chose to make a web application featuring multiples games such as Tetris one of the very first game ever developed and Skkribl.io the amazing drawing game !
But beware ! A mysterious noble cat named Wiskas The Third is gone ... It is said that he's been lurking around trapping 42's students into infinite conversation known as "tunnel". If you see him please report to us as soon as possible !
**INSTRUCTIONS**
Like every 42 project you will need to git clone it into a valid repository, then add our functional .env file at the root of the repository. After all that, make use of the "make" command and watch our fabulous containers building themselves ! Look for https://localhost:8443/ once it's built, remember that you need to login in order to play on our web app !
Outside of 42 environment you will obviously need Docker and Make installed.
**RESOURCES**
- https://www.geeksforgeeks.org
- https://developer.mozilla.org/fr/docs/Web/JavaScript
- https://www.w3schools.com/js/
- https://www.tigerdata.com/learn/postgres-cheat-sheet
- https://www.programiz.com/css/button-styling
- https://developer.mozilla.org/fr/docs/Web/CSS
- https://chatgpt.com/
- https://www.gimp.org/tutorials/
AI was mostly used to ask questions and deepen understanding, it was also used to generate multiple samples of what we could do front-end wise.
**FEATURES:**
- Login
- Avatar
- Global Chat
- Skribbl.io + Spectator mode
- Tetris + Duels
- Wiskas the Third
Use of the framework Express for the back-end because its compatible with jsonwebtoken(JWT) and contains solid and well tested features.
**DEPENDENCIES**
- "express": "^4.18.2",
- "pg": "^8.11.3",
- "bcrypt": "^5.1.0",
- "jsonwebtoken": "^9.0.2",
- "dotenv": "^17.2.3",
- "socket.io": "^4.6.1",
- "cors": "^2.8.5",
- "passport": "0.7.0",
- "passport-github2": "0.1.12",
- "express-session": "1.18.0",
- "multer": "^1.4.5-lts.1",
- "file-type": "^19.0.0"
**TEAM INFORMATION**
Tfauve-p : The project manager, is in lead of organizing all the meeting with the team which changed over time, including then some recruitment. There was some adjustments to make over our vision of the project while coding it on GitHub.
Yantoine : The project owner, is in lead of both games, Tetris and Skkribl.io, made core decisions on features about these and got the work completed. His communication skills were very important due to the front-end / back-end relationship needed in order to achieve this glorious project.
Gprunet : The technical lead, is in charge of the back-end, made some strong decisions on the architectures of the project in terms of technology used. Created the entire database and most of the foundation of this project such as the builder files.
Agallon : Developer in the front-end action, he joined the team after the project was done but managed to innovate and brought to life the marvelous Wiskas the Third. Furthermore he also cared about the integrity of the web application and greatly improved the user experience through logical decisions.
**PROJECT MANAGEMENT**
The task's sharing was based on our own advance of the 42 cursus, meaning that Gprunet and Yantoine started coding the app sooner than Tfauve-p and Agallon.
Gprunet: Back-end + spectator mode
Yantoine: Github auth, games and Front/Back sockets
Tfauve-p: Front-end Designer
Agallon: Adjustements on Front-end and some new features.
**TECHNICAL STACK**
Front-end: JavaScript, HTML, CSS, NGINX
Back-end: JavaScript, Express, JWT, multer, etc...
Database: PostgreSQL because it uses a permissibe open-source licence and is feature-rich and powerful for the scale of our project
Since python doesn't have many front-end framework we opted to use JavaScript for both front and back.
After learning about JWT and learning that Express had a great synergy with it the choice was natural.
**DATABASES SCHEMA**
![Database Schema required ... ](./postgresql-architecture.jpg)
**FEATURES LIST**
- 2 Games
- One talking Cat
- Friends chat
-
**MODULES**
Total : 23pts ( 14pts for 100% 19pts for 125% )
- WEB
Minor : Use a back end framework
Major : Implement real-time features
Major : Allow users to interact with others
Major : A public API to interact with the database
Minor : A complete notification system for all creation, update and deletion account
- ACCESSIBILITY
Minor : Support for additional browsers
- USER MANAGEMENT
Major : Standard user management and authentication
Minor : Game statistics and match history ???
Minor : Implement remote authentication
- GAMING AND USER EXPERIENCE
Major : Implement a complete web-based game where users can play against each other
Major : Remote players, Enable two players on separate computers to play the same game
Major : Multiplayer game
Major : Add another game with user history and matchmaking
Minor : Advanced chat features ????
Minor : Game customization options
Minor : Spectator mode for games
+10 -3
View File
@@ -1,17 +1,24 @@
all : up all :
@$(call random_shmol_cat, "hELLO", "nice human corrector", $(CLS), )
@docker compose -f ./docker-compose.yml up -d
up : no_cache :
@docker compose -f ./docker-compose.yml build --no-cache @docker compose -f ./docker-compose.yml build --no-cache
@docker compose -f ./docker-compose.yml up -d @docker compose -f ./docker-compose.yml up -d
clean : clean :
@$(call print_cat, $(CLEAR), $(C_225), $(C_320), $(C_450), $(call pad_word, 10, "Objects"), $(call pad_word, 12, "Exterminated"));
@docker compose -f ./docker-compose.yml down -t 1 @docker compose -f ./docker-compose.yml down -t 1
fclean : fclean :
@$(call print_cat, $(CLEAR), $(C_120), $(C_300), $(C_210), $(call pad_word, 10, "Allclean"), $(call pad_word, 12, "Miaster"));
@docker compose -f ./docker-compose.yml down -v -t 1 @docker compose -f ./docker-compose.yml down -v -t 1
@docker system prune -af --volumes @docker system prune -af --volumes
re : fclean up re : fclean no_cache
@$(call print_cat, $(CLEAR), $(C_120), $(C_300), $(C_210), $(call pad_word, 10, "Re-Doing"), $(call pad_word, 12, "Miaster"));
.PHONY : all no_cache clean fclean re
-3
View File
@@ -1,3 +0,0 @@
INTRA_CLIENT_ID=u-s4t2ud-c226cd35cd1ac08a4c6668deee1c64d7d67a13a766aee672acafd4a1522d483c
INTRA_CLIENT_SECRET=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
+5 -3
View File
@@ -1,5 +1,5 @@
volumes: volumes:
pgdata: data:
networks: networks:
transcendence: transcendence:
@@ -12,7 +12,7 @@ services:
ports: ports:
- "5432:5432" - "5432:5432"
volumes: volumes:
- pgdata:/var/lib/postgresql - data:/var/lib/postgresql/data/pg15/
env_file: env_file:
- ../.env - ../.env
networks: networks:
@@ -24,6 +24,8 @@ services:
build: ./srcs/backend build: ./srcs/backend
expose: expose:
- "3001" - "3001"
# ports:
# - "3001:3001"
depends_on: depends_on:
- database - database
volumes: volumes:
@@ -38,7 +40,7 @@ services:
container_name: frontend container_name: frontend
build: ./srcs/frontend/ build: ./srcs/frontend/
ports: ports:
- "8443:443" - "8080:80"
depends_on: depends_on:
- backend - backend
networks: networks:
+1 -1
View File
@@ -127,7 +127,7 @@ async function createTables()
status VARCHAR(20) DEFAULT 'waiting', status VARCHAR(20) DEFAULT 'waiting',
max_players INT DEFAULT 8, max_players INT DEFAULT 8,
current_round INT DEFAULT 0, current_round INT DEFAULT 0,
max_rounds INT DEFAULT 5, max_rounds INT DEFAULT 3,
round_duration INT DEFAULT 90, round_duration INT DEFAULT 90,
created_at TIMESTAMP DEFAULT NOW(), created_at TIMESTAMP DEFAULT NOW(),
started_at TIMESTAMP, started_at TIMESTAMP,
-8
View File
@@ -1,13 +1,5 @@
FROM node:20-alpine FROM node:20-alpine
RUN apk add --no-cache openssl
RUN mkdir -p /etc/backend/.ssl
RUN openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /etc/backend/.ssl/key.pem \
-out /etc/backend/.ssl/cert.pem \
-subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
WORKDIR /app WORKDIR /app
COPY package*.json ./ COPY package*.json ./
+2 -9
View File
@@ -1,6 +1,5 @@
import express from 'express'; import express from 'express';
import https from 'https'; import http from 'http';
import fs from 'fs';
import cors from 'cors'; import cors from 'cors';
import {Server} from 'socket.io'; import {Server} from 'socket.io';
import authRouter from './routes/auth.js'; import authRouter from './routes/auth.js';
@@ -12,14 +11,9 @@ import playerStatsRouter from './routes/player_stats.js';
import {waitForDb, createTables, runMigrations, ensureOauthClient} from './db.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';
import intraRouter from './routes/intra.js';
const app = express(); const app = express();
const httpsOptions = { const server = http.createServer(app);
key: fs.readFileSync('/etc/backend/.ssl/key.pem'),
cert: fs.readFileSync('/etc/backend/.ssl/cert.pem')
};
const server = https.createServer(httpsOptions, app);
const io = new Server(server, const io = new Server(server,
{ {
cors: cors:
@@ -54,7 +48,6 @@ async function startServer()
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.use('/api/stats', playerStatsRouter);
app.use('/api/intra', intraRouter);
app.get('/api', (req, res) => res.send('Backend running')); app.get('/api', (req, res) => res.send('Backend running'));
server.listen(3001, () => server.listen(3001, () =>
-11
View File
@@ -26,17 +26,6 @@ router.post('/login', async(req, res) =>
res.status(result.status).json(result.data); res.status(result.status).json(result.data);
}); });
router.post('/logout', async(req, res) =>
{
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token)
return (res.status(401).json({error: 'Missing token'}));
const result = await authService.logout(token);
res.status(result.status).json(result.data);
});
router.get('/github', (req, res) => { router.get('/github', (req, res) => {
const githubAuthUrl = `https://github.com/login/oauth/authorize?` + const githubAuthUrl = `https://github.com/login/oauth/authorize?` +
`client_id=${process.env.GITHUB_CLIENT_ID}&` + `client_id=${process.env.GITHUB_CLIENT_ID}&` +
+1 -1
View File
@@ -25,7 +25,7 @@ router.post('/upload', authenticateToken, upload.single('avatar'), async(req, re
res.status(result.status).json(result.data); res.status(result.status).json(result.data);
}); });
router.delete('/delete', authenticateToken, async(req, res) => router.delete('/', authenticateToken, async(req, res) =>
{ {
const result = await avatarService.deleteAvatar(req.user.userId); const result = await avatarService.deleteAvatar(req.user.userId);
res.status(result.status).json(result.data); res.status(result.status).json(result.data);
@@ -1,53 +0,0 @@
// routes/intra.js
import express from 'express';
const router = express.Router();
let token;
async function set_token() {
try {
const response = await fetch("https://api.intra.42.fr/oauth/token", {
method: "POST",
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: process.env.INTRA_CLIENT_ID,
client_secret: process.env.INTRA_CLIENT_SECRET
}),
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
});
token = await response.json();
setTimeout(set_token, (token.expires_in - 60) * 1000);
} catch (e) {
console.error("Token error:", e);
}
}
set_token();
router.get('/profile/:login', async (req, res) => {
try {
const response = await fetch(
`https://api.intra.42.fr/v2/users/${req.params.login}`,
{
headers: {
Authorization: `Bearer ${token.access_token}`
}
}
);
if (!response.ok) {
return res.status(response.status).json({ error: 'User not found' });
}
res.json(await response.json());
} catch (e) {
res.status(500).json({ error: 'Failed to fetch profile' });
}
});
export default router;
+1 -25
View File
@@ -2,30 +2,6 @@ import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import {query} from '../db.js'; import {query} from '../db.js';
async function logout(token)
{
try
{
if (!token)
return ({status: 400, data: {error: 'Missing token'}});
try
{
jwt.verify(token, process.env.JWT_SECRET);
}
catch
{
return ({status: 401, data: {error: 'Invalid token'}});
}
return ({status: 200, data: {message: 'Logged out'}});
}
catch (err)
{
console.error(err);
return ({status: 500, data: {error: 'Server error'}});
}
}
async function login(username, password) async function login(username, password)
{ {
try try
@@ -84,4 +60,4 @@ async function register(username, password)
} }
}; };
export default {register, login, logout}; export default {register, login};
@@ -69,9 +69,6 @@ async function deleteAvatar(userId) {
if (currentAvatar === null) if (currentAvatar === null)
return ({status: 404, data: {error: 'User not found'}}); return ({status: 404, data: {error: 'User not found'}});
if (currentAvatar === DEFAULT_AVATAR)
return ({status: 400, data: {error: 'Cannot delete default avatar'}});
// Reset the avatar to the default one // Reset the avatar to the default one
await setAvatar(DEFAULT_AVATAR, userId); await setAvatar(DEFAULT_AVATAR, userId);
+37 -139
View File
@@ -30,63 +30,6 @@ async function broadcastRoomsList(io) {
} }
} }
function startRoomTimer(io, roomId, seconds)
{
const gameState = gameRooms.get(roomId);
if (!gameState) return;
if (gameState.timerInterval)
clearInterval(gameState.timerInterval);
gameState.timerSeconds = seconds;
gameState.timerInterval = setInterval(() => {
gameState.timerSeconds--;
if (gameState.timerSeconds < 0)
gameState.timerSeconds = 0;
if (gameState.timerSeconds <= 0)
{
io.to(roomId).emit('game-timer-sync', {
remaining: 0
});
clearInterval(gameState.timerInterval);
gameState.timerInterval = null;
io.to(roomId).emit('game-timer-ended', { message: 'Temps écoulé !' });
gameState.currentPlayerIndex = (gameState.currentPlayerIndex + 1) % gameState.players.length;
const nextDrawer = gameState.players[gameState.currentPlayerIndex];
gameState.drawer = nextDrawer;
gameState.currentWord = '';
gameState.revealedLetters = [];
gameState.revealedWord = [];
gameState.guessedLetters = [];
gameState.wrongGuesses = 0;
io.to(roomId).emit('game-new-round', {
drawer: nextDrawer
});
}
else
{
io.to(roomId).emit('game-timer-sync', {
remaining: gameState.timerSeconds
});
}
}, 1000);
}
function stopRoomTimer(roomId)
{
const gameState = gameRooms.get(roomId);
if (!gameState || !gameState.timerInterval) return;
clearInterval(gameState.timerInterval);
gameState.timerInterval = null;
}
// Check if a playing game has only 1 player left and auto-stop it // Check if a playing game has only 1 player left and auto-stop it
async function checkAndStopSinglePlayerGame(io, roomId, dbRoomId) { async function checkAndStopSinglePlayerGame(io, roomId, dbRoomId) {
if (!dbRoomId) return; if (!dbRoomId) return;
@@ -100,7 +43,6 @@ async function checkAndStopSinglePlayerGame(io, roomId, dbRoomId) {
const players = await gameRoomService.getRoomPlayers(dbRoomId); const players = await gameRoomService.getRoomPlayers(dbRoomId);
if (players.length <= 1) { if (players.length <= 1) {
console.log(`Room ${dbRoomId} has only ${players.length} player(s) left, ending game`); console.log(`Room ${dbRoomId} has only ${players.length} player(s) left, ending game`);
stopRoomTimer(roomId);
// Update room status to 'ended' // Update room status to 'ended'
await gameRoomService.updateRoomStatus(dbRoomId, 'waiting'); await gameRoomService.updateRoomStatus(dbRoomId, 'waiting');
@@ -140,47 +82,6 @@ async function saveRoundPoints(currentScores, roundStartScores) {
} }
} }
function handlePlayerDeparture(io, roomId, username) {
const gameState = gameRooms.get(roomId);
if (!gameState || !gameState.isPlaying) return;
if (!Array.isArray(gameState.players)) return;
const leavingIndex = gameState.players.indexOf(username);
if (leavingIndex === -1) return;
const wasDrawer = gameState.drawer === username;
gameState.players = gameState.players.filter(p => p !== username);
if (gameState.scores) {
delete gameState.scores[username];
}
if (gameState.currentPlayerIndex >= leavingIndex) {
gameState.currentPlayerIndex = Math.max(0, gameState.currentPlayerIndex - 1);
}
if (gameState.currentPlayerIndex >= gameState.players.length) {
gameState.currentPlayerIndex = 0;
}
if (wasDrawer && gameState.players.length > 0) {
stopRoomTimer(roomId);
const newDrawer = gameState.players[gameState.currentPlayerIndex];
gameState.drawer = newDrawer;
gameState.currentWord = '';
gameState.revealedLetters = [];
gameState.revealedWord = [];
gameState.guessedLetters = [];
gameState.wrongGuesses = 0;
io.to(roomId).emit('game-drawer-changed', {
newDrawer: newDrawer,
reason: 'drawer_left',
message: `${username} (dessinateur) a quitte, ${newDrawer} devient le nouveau dessinateur`
});
startRoomTimer(io, roomId, 60);
}
}
function setupSocketIO(io) function setupSocketIO(io)
{ {
ioInstance = io; ioInstance = io;
@@ -291,9 +192,7 @@ function setupSocketIO(io)
revealedLetters: gameState.revealedLetters, revealedLetters: gameState.revealedLetters,
revealedWord: gameState.revealedWord || [], revealedWord: gameState.revealedWord || [],
guessedLetters: gameState.guessedLetters, guessedLetters: gameState.guessedLetters,
players: gameState.players, players: gameState.players
scores: gameState.scores || {},
timer: gameState.timerSeconds || 0
}); });
} }
}); });
@@ -303,21 +202,11 @@ function setupSocketIO(io)
if (socket.gameRoomId) { if (socket.gameRoomId) {
const roomId = socket.gameRoomId; const roomId = socket.gameRoomId;
const dbRoomId = socket.gameRoomDbId; const dbRoomId = socket.gameRoomDbId;
const userId = socket.user.userId;
if (dbRoomId && userId) {
try {
await gameRoomService.leaveRoom(dbRoomId, userId);
} catch (err) {
console.error('Error removing player from room on socket leave:', err.message);
}
}
socket.to(roomId).emit('game-player-left', { socket.to(roomId).emit('game-player-left', {
username: socket.user.username, username: socket.user.username,
userId: socket.user.userId userId: socket.user.userId
}); });
handlePlayerDeparture(io, roomId, socket.user.username);
socket.leave(roomId); socket.leave(roomId);
console.log(`${socket.user.username} left ${roomId}`); console.log(`${socket.user.username} left ${roomId}`);
@@ -379,8 +268,7 @@ function setupSocketIO(io)
revealedWord: gameState.revealedWord || [], revealedWord: gameState.revealedWord || [],
guessedLetters: gameState.guessedLetters, guessedLetters: gameState.guessedLetters,
players: gameState.players, players: gameState.players,
scores: gameState.scores || {}, scores: gameState.scores || {}
timer: gameState.timerSeconds || 0
}); });
} }
} catch (err) { } catch (err) {
@@ -502,7 +390,6 @@ function setupSocketIO(io)
const gameState = gameRooms.get(roomId); const gameState = gameRooms.get(roomId);
if (!gameState) return; if (!gameState) return;
startRoomTimer(io, roomId, 60);
gameState.currentWord = data.word.toLowerCase(); gameState.currentWord = data.word.toLowerCase();
gameState.revealedLetters = new Array(data.word.length).fill(false); gameState.revealedLetters = new Array(data.word.length).fill(false);
gameState.revealedWord = new Array(data.word.length).fill('_'); gameState.revealedWord = new Array(data.word.length).fill('_');
@@ -616,7 +503,7 @@ function setupSocketIO(io)
// Points: 10 per letter found, -5 for wrong guess // Points: 10 per letter found, -5 for wrong guess
if (success) { if (success) {
points = lettersFound * 5; points = lettersFound * 10;
gameState.scores[username] += points; gameState.scores[username] += points;
} else { } else {
points = -5; points = -5;
@@ -665,8 +552,6 @@ function setupSocketIO(io)
// Update round start scores for next round // Update round start scores for next round
gameState.roundStartScores = { ...gameState.scores }; gameState.roundStartScores = { ...gameState.scores };
stopRoomTimer(roomId);
io.to(roomId).emit('game-word-found', { io.to(roomId).emit('game-word-found', {
word: gameState.currentWord, word: gameState.currentWord,
winner: username, winner: username,
@@ -715,7 +600,40 @@ function setupSocketIO(io)
message: `${username} a quitté la partie` message: `${username} a quitté la partie`
}); });
handlePlayerDeparture(io, roomId, username); const gameState = gameRooms.get(roomId);
if (gameState)
{
const wasDrawer = gameState.drawer === username;
gameState.players = gameState.players.filter(p => p !== username);
delete gameState.scores[username];
io.to(roomId).emit('scores-updated', gameState.scores);
// If the drawer left and there are still enough players, choose a new drawer
if (wasDrawer && gameState.players.length >= 1)
{
// Pick the next player as the new drawer
gameState.currentPlayerIndex = gameState.currentPlayerIndex % gameState.players.length;
const newDrawer = gameState.players[gameState.currentPlayerIndex];
gameState.drawer = newDrawer;
// Reset the word state for the new round
gameState.currentWord = '';
gameState.revealedLetters = [];
gameState.revealedWord = [];
gameState.guessedLetters = [];
gameState.wrongGuesses = 0;
console.log(`Drawer ${username} left, new drawer is ${newDrawer}`);
io.to(roomId).emit('game-drawer-changed', {
newDrawer: newDrawer,
reason: 'drawer_left',
message: `${username} (dessinateur) a quitté, ${newDrawer} devient le nouveau dessinateur`
});
}
}
await checkAndStopSinglePlayerGame(io, roomId, dbRoomId); await checkAndStopSinglePlayerGame(io, roomId, dbRoomId);
@@ -734,7 +652,6 @@ function setupSocketIO(io)
socket.on('game-end', async () => { socket.on('game-end', async () => {
const roomId = socket.gameRoomId; const roomId = socket.gameRoomId;
if (!roomId) return; if (!roomId) return;
stopRoomTimer(roomId);
// Update room status to 'waiting' in database // Update room status to 'waiting' in database
const dbRoomId = socket.gameRoomDbId; const dbRoomId = socket.gameRoomDbId;
@@ -813,16 +730,6 @@ function setupSocketIO(io)
_tetrisRelayToOpponent(socket, 'tetris:lines-cleared', data); _tetrisRelayToOpponent(socket, 'tetris:lines-cleared', data);
}); });
// Relay pur : shield-activated → adversaire uniquement
socket.on('tetris:shield-activated', () => {
_tetrisRelayToOpponent(socket, 'tetris:shield-activated', {});
});
// Relay pur : shield-deactivated → adversaire uniquement
socket.on('tetris:shield-deactivated', () => {
_tetrisRelayToOpponent(socket, 'tetris:shield-deactivated', {});
});
// start-duel → relayé aux DEUX joueurs de la room (inclut l'émetteur) // start-duel → relayé aux DEUX joueurs de la room (inclut l'émetteur)
socket.on('tetris:start-duel', () => { socket.on('tetris:start-duel', () => {
const code = socket.tetrisRoomCode; const code = socket.tetrisRoomCode;
@@ -950,20 +857,11 @@ function setupSocketIO(io)
} }
else else
{ {
if (dbRoomId && socket.user.userId) {
try {
await gameRoomService.leaveRoom(dbRoomId, socket.user.userId);
} catch (err) {
console.error('Error removing disconnected player from room:', err.message);
}
}
// Regular player disconnect // Regular player disconnect
socket.to(roomId).emit('game-player-left', { socket.to(roomId).emit('game-player-left', {
username: socket.user.username, username: socket.user.username,
userId: socket.user.userId userId: socket.user.userId
}); });
handlePlayerDeparture(io, roomId, socket.user.username);
// Get updated player list and broadcast // Get updated player list and broadcast
if (dbRoomId) { if (dbRoomId) {
+1 -16
View File
@@ -1,20 +1,5 @@
FROM nginx:alpine FROM nginx:alpine
RUN apk add --no-cache openssl && \
mkdir -p /etc/nginx/ssl && \
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /etc/nginx/ssl/key.pem \
-out /etc/nginx/ssl/cert.pem \
-subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
#ADDED
RUN rm -f /etc/nginx/conf.d/default.conf
COPY src /usr/share/nginx/html COPY src /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
EXPOSE 443
CMD ["nginx", "-g", "daemon off;"] CMD ["nginx", "-g", "daemon off;"]
+5 -15
View File
@@ -1,9 +1,5 @@
server { server {
listen 443 ssl; listen 80;
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
error_page 497 =301 https://$host:8443$request_uri;
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html; index index.html;
@@ -15,33 +11,27 @@ server {
# Backend API # Backend API
location /api/ { location /api/ {
proxy_pass https://backend:3001; proxy_pass http://backend:3001;
proxy_ssl_verify off;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto https;
} }
# Socket.IO WebSocket proxying # Socket.IO WebSocket proxying
location /socket.io/ { location /socket.io/ {
proxy_pass https://backend:3001; proxy_pass http://backend:3001;
proxy_ssl_verify off;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
} }
location /avatar/ { location /avatar/ {
proxy_pass https://backend:3001/avatar/; proxy_pass http://backend:3001/avatar/;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_ssl_verify off;
proxy_hide_header Content-Type; proxy_hide_header Content-Type;
add_header Cache-Control "public, max-age=3600"; add_header Cache-Control "public, max-age=3600";
} }
+35 -86
View File
@@ -2,16 +2,13 @@
* Application entry point * Application entry point
* Initializes windows and handles menu interactions * Initializes windows and handles menu interactions
*/ */
import { windowRegistry } from './core/windows.js'; import { windowRegistry } from './windows.js';
import { API, STORAGE_KEYS } from './core/config.js'; import { LoginWindow } from './login.js';
import { eventBus, Events } from './core/events.js'; import { GlobalChat } from './global_chat.js';
import { LoginWindow } from './windows/login.js'; import { AvatarWindow } from './avatar.js';
import { LogoutWindow } from './windows/logout.js'; import { FriendsWindow } from './friends.js';
import { GlobalChat } from './windows/global_chat.js'; import { GameRoomWindow } from './game_room.js';
import { AvatarWindow } from './windows/avatar.js'; import { StatsWindow } from './stats.js';
import { FriendsWindow } from './windows/friends.js';
import { GameRoomWindow } from './windows/game_room.js';
import { StatsWindow } from './windows/stats.js';
/** /**
* Main application class * Main application class
@@ -19,7 +16,7 @@ import { StatsWindow } from './windows/stats.js';
*/ */
class App { class App {
constructor() { constructor() {
this.invalidateStaleToken(); console.log("APP STARTED");
this.initWindows(); this.initWindows();
this.initMenu(); this.initMenu();
this.initPage(); this.initPage();
@@ -27,51 +24,6 @@ class App {
this.colorizeUI(); this.colorizeUI();
} }
async invalidateStaleToken() {
const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
if (!token) return;
if (this.isJwtExpired(token)) {
localStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN);
eventBus.emit(Events.USER_LOGGED_OUT);
return;
}
try {
const response = await fetch(API.STATS.ME, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (response.status === 401) {
localStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN);
eventBus.emit(Events.USER_LOGGED_OUT);
setTimeout(() => window.location.reload(), 500);
}
} catch (error) {
console.warn('Token validation skipped:', error);
}
}
isJwtExpired(token) {
try {
const payload = this.decodeJwtPayload(token);
if (!payload || !payload.exp) return false;
const now = Math.floor(Date.now() / 1000);
return payload.exp <= now;
} catch (error) {
return false;
}
}
decodeJwtPayload(token) {
const parts = token.split('.');
if (parts.length < 2) return null;
const base64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=');
return JSON.parse(atob(padded));
}
/** /**
* Initializes all windows * Initializes all windows
*/ */
@@ -82,7 +34,6 @@ class App {
new FriendsWindow(); new FriendsWindow();
new GameRoomWindow(); new GameRoomWindow();
new StatsWindow(); new StatsWindow();
new LogoutWindow();
} }
/** /**
@@ -100,8 +51,7 @@ class App {
'login': 'login', 'login': 'login',
'chat': 'chat', 'chat': 'chat',
'avatar': 'avatar', 'avatar': 'avatar',
'friends': 'friends', 'friends': 'friends'
'logout': 'logout'
}; };
// Event delegation on the menu // Event delegation on the menu
@@ -126,6 +76,10 @@ class App {
return; return;
} }
const actionMap = {
'gameroom': 'gameroom'
};
// Event delegation on the menu // Event delegation on the menu
page.addEventListener('click', (e) => { page.addEventListener('click', (e) => {
const button = e.target.closest('.page__item'); const button = e.target.closest('.page__item');
@@ -133,14 +87,9 @@ class App {
const action = button.dataset.action; const action = button.dataset.action;
if (action === 'gameroom') { // Actions with associated windows
const gameRoomWindow = windowRegistry.get('gameroom'); if (actionMap[action]) {
windowRegistry.toggle('gameroom'); windowRegistry.toggle(actionMap[action]);
gameRoomWindow.loadRooms();
if (gameRoomWindow?.currentTab === 'browse') {
gameRoomWindow.loadRooms();
}
return; return;
} }
@@ -161,34 +110,34 @@ class App {
colorizeUI() { colorizeUI() {
const elements = document.querySelectorAll(".title, .menu__item, .game__item, .page__item"); const elements = document.querySelectorAll(".title, .menu__item, .game__item, .page__item");
const colorizeText = (el) => { const colorizeText = (el) => {
const text = el.textContent; const text = el.textContent;
el.innerHTML = ""; el.innerHTML = "";
const baseHue = Math.random() * 360; const baseHue = Math.random() * 360;
// 🎲 random step = makes rainbow "scrambled" // 🎲 random step = makes rainbow "scrambled"
const step = (Math.random() * 60) + 10; // 10 → 70 const step = (Math.random() * 60) + 10; // 10 → 70
// 🎲 random direction (left or right rainbow) // 🎲 random direction (left or right rainbow)
const direction = Math.random() < 0.5 ? 1 : -1; const direction = Math.random() < 0.5 ? 1 : -1;
[...text].forEach((char, i) => { [...text].forEach((char, i) => {
const span = document.createElement("span"); const span = document.createElement("span");
span.textContent = char; span.textContent = char;
const hue = baseHue + (i * step * direction); const hue = baseHue + (i * step * direction);
span.style.color = `hsl(${hue}, 90%, 60%)`; span.style.color = `hsl(${hue}, 90%, 60%)`;
span.style.textShadow = `1px 1px 0 rgba(0,0,0,0.3)`; span.style.textShadow = `1px 1px 0 rgba(0,0,0,0.3)`;
el.appendChild(span); el.appendChild(span);
}); });
}; };
elements.forEach(colorizeText); elements.forEach(colorizeText);
} }
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before

Width:  |  Height:  |  Size: 994 B

After

Width:  |  Height:  |  Size: 994 B

Before

Width:  |  Height:  |  Size: 1018 B

After

Width:  |  Height:  |  Size: 1018 B

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before

Width:  |  Height:  |  Size: 955 B

After

Width:  |  Height:  |  Size: 955 B

Before

Width:  |  Height:  |  Size: 1022 B

After

Width:  |  Height:  |  Size: 1022 B

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before

Width:  |  Height:  |  Size: 887 B

After

Width:  |  Height:  |  Size: 887 B

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before

Width:  |  Height:  |  Size: 1000 B

After

Width:  |  Height:  |  Size: 1000 B

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

@@ -1,6 +1,6 @@
import { Window, windowRegistry } from '../core/windows.js'; import { Window, windowRegistry } from './windows.js';
import { API, STORAGE_KEYS, CSS } from '../core/config.js'; import { API, STORAGE_KEYS, CSS } from './config.js';
import { eventBus, Events } from '../core/events.js'; import { eventBus, Events } from './events.js';
/** /**
* Avatar management window * Avatar management window
@@ -68,11 +68,7 @@ export class AvatarWindow extends Window {
text: 'Refresh' text: 'Refresh'
}); });
this.deleteBtn = this.createElement('button', [CSS.BTN, CSS.BTN_SECONDARY], { this.controls.append(this.statsBtn, this.chooseBtn, this.saveBtn, this.refreshBtn);
text: 'Delete avatar'
});
this.controls.append(this.statsBtn, this.chooseBtn, this.saveBtn, this.refreshBtn, this.deleteBtn);
// Feedback message // Feedback message
this.message = this.createElement('div', CSS.MESSAGE); this.message = this.createElement('div', CSS.MESSAGE);
@@ -97,7 +93,6 @@ export class AvatarWindow extends Window {
this.chooseBtn.addEventListener('click', () => this.fileInput.click()); this.chooseBtn.addEventListener('click', () => this.fileInput.click());
this.saveBtn.addEventListener('click', () => this.uploadAvatar()); this.saveBtn.addEventListener('click', () => this.uploadAvatar());
this.refreshBtn.addEventListener('click', () => this.loadAvatar()); this.refreshBtn.addEventListener('click', () => this.loadAvatar());
this.deleteBtn.addEventListener('click', () => this.deleteAvatar());
} }
/** /**
@@ -217,14 +212,12 @@ export class AvatarWindow extends Window {
const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
if (!token) { if (!token) {
this.showMessage('You must be logged in', 'error'); this.showMessage('You must be logged in', 'error');
this.showNotification('You must be logged in to change your avatar', 'red');
return; return;
} }
const file = this.fileInput.files?.[0]; const file = this.fileInput.files?.[0];
if (!file) { if (!file) {
this.showMessage('Select an image first', 'error'); this.showMessage('Select an image first', 'error');
this.showNotification('Please select an image to upload', 'red');
return; return;
} }
@@ -247,7 +240,6 @@ export class AvatarWindow extends Window {
if (!response.ok) { if (!response.ok) {
const errorMsg = data?.error || data?.message || 'Upload failed'; const errorMsg = data?.error || data?.message || 'Upload failed';
this.showMessage(errorMsg, 'error'); this.showMessage(errorMsg, 'error');
this.showNotification('Failed to upload avatar.', 'red');
return; return;
} }
@@ -256,47 +248,11 @@ export class AvatarWindow extends Window {
} }
this.showMessage('Avatar saved!', 'success'); this.showMessage('Avatar saved!', 'success');
this.showNotification('Avatar updated successfully!', 'green');
eventBus.emit(Events.AVATAR_UPDATED, { url: data?.avatar_url }); eventBus.emit(Events.AVATAR_UPDATED, { url: data?.avatar_url });
} catch (error) { } catch (error) {
console.error('Avatar upload error:', error); console.error('Avatar upload error:', error);
this.showMessage('Upload error', 'error'); this.showMessage('Upload error', 'error');
this.showNotification('Failed to upload avatar.', 'red');
}
}
async deleteAvatar() {
const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
if (!token) {
this.showMessage('You must be logged in', 'error');
this.showNotification('You must be logged in to delete your avatar', 'red');
return;
}
try {
const response = await fetch(API.AVATAR.DELETE, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
this.showMessage('Failed to delete avatar', 'error');
this.showNotification('Failed to delete avatar.', 'red');
return;
}
this.preview.src = '';
this.showMessage('Avatar deleted!', 'success');
this.showNotification('Avatar deleted successfully!', 'green');
eventBus.emit(Events.AVATAR_DELETED);
} catch (error) {
console.error('Avatar delete error:', error);
this.showMessage('Delete error', 'error');
this.showNotification('Failed to delete avatar.', 'red');
} }
} }
@@ -6,14 +6,12 @@
export const API = { export const API = {
AUTH: { AUTH: {
LOGIN: '/api/auth/login', LOGIN: '/api/auth/login',
LOGOUT: '/api/auth/logout',
REGISTER: '/api/auth/register', REGISTER: '/api/auth/register',
GITHUB: '/api/auth/github' GITHUB: '/api/auth/github'
}, },
AVATAR: { AVATAR: {
GET: '/api/avatar/me', GET: '/api/avatar/me',
UPLOAD: '/api/avatar/upload', UPLOAD: '/api/avatar/upload'
DELETE: '/api/avatar/delete'
}, },
FRIENDS: { FRIENDS: {
LIST: '/api/friends', LIST: '/api/friends',
@@ -0,0 +1,87 @@
.shape {
/* The "Physical" properties */
position: fixed;
/* transform: translate(-50%, -50%); Optional: This makes 'left/top' refer to the CENTER of the doodle */
width: 142px;
height: 142px;
/* The "Stenciling" instructions (but no image yet!) */
-webkit-mask-size: contain;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
/* The default "Paint" color */
background-color: white;
}
.shape:hover {
transform: scale(1.2); /* Grow by 20% when you hover the mouse over it */
transition: transform 0.3s ease; /* Make it a smooth grow */
}
/* Individual Doodle Definitions */
.doodle-1 { -webkit-mask-image: url('assets/doodles/ball.png'); mask-image: url('assets/doodles/ball.png'); left: 10vw; top: 10vh; }
.doodle-2 { -webkit-mask-image: url('assets/doodles/batman.png'); mask-image: url('assets/doodles/batman.png'); left: 20vw; top: 15vh; }
.doodle-3 { -webkit-mask-image: url('assets/doodles/building.png'); mask-image: url('assets/doodles/building.png'); left: 30vw; top: 20vh; }
.doodle-4 { -webkit-mask-image: url('assets/doodles/butterfly.png'); mask-image: url('assets/doodles/butterfly.png'); left: 40vw; top: 25vh; }
.doodle-5 { -webkit-mask-image: url('assets/doodles/car.png'); mask-image: url('assets/doodles/car.png'); left: 50vw; top: 30vh; }
.doodle-6 { -webkit-mask-image: url('assets/doodles/cat.png'); mask-image: url('assets/doodles/cat.png'); left: 60vw; top: 35vh; }
.doodle-7 { -webkit-mask-image: url('assets/doodles/clouds.png'); mask-image: url('assets/doodles/clouds.png'); left: 70vw; top: 40vh; }
.doodle-8 { -webkit-mask-image: url('assets/doodles/controls.png'); mask-image: url('assets/doodles/controls.png'); left: 80vw; top: 45vh; }
.doodle-9 { -webkit-mask-image: url('assets/doodles/dead.png'); mask-image: url('assets/doodles/dead.png'); left: 90vw; top: 50vh; }
.doodle-10 { -webkit-mask-image: url('assets/doodles/diamant.png'); mask-image: url('assets/doodles/diamant.png'); left: 15vw; top: 55vh; }
.doodle-11 { -webkit-mask-image: url('assets/doodles/dice.png'); mask-image: url('assets/doodles/dice.png'); left: 25vw; top: 60vh; }
.doodle-12 { -webkit-mask-image: url('assets/doodles/earth.png'); mask-image: url('assets/doodles/earth.png'); left: 35vw; top: 65vh; }
.doodle-13 { -webkit-mask-image: url('assets/doodles/egypt.png'); mask-image: url('assets/doodles/egypt.png'); left: 45vw; top: 70vh; }
.doodle-14 { -webkit-mask-image: url('assets/doodles/fire.png'); mask-image: url('assets/doodles/fire.png'); left: 55vw; top: 75vh; }
.doodle-15 { -webkit-mask-image: url('assets/doodles/fish.png'); mask-image: url('assets/doodles/fish.png'); left: 65vw; top: 80vh; }
.doodle-16 { -webkit-mask-image: url('assets/doodles/flag.png'); mask-image: url('assets/doodles/flag.png'); left: 75vw; top: 85vh; }
.doodle-17 { -webkit-mask-image: url('assets/doodles/hearts.png'); mask-image: url('assets/doodles/hearts.png'); left: 85vw; top: 90vh; }
.doodle-18 { -webkit-mask-image: url('assets/doodles/house.png'); mask-image: url('assets/doodles/house.png'); left: 5vw; top: 45vh; }
.doodle-19 { -webkit-mask-image: url('assets/doodles/idol.png'); mask-image: url('assets/doodles/idol.png'); left: 12vw; top: 22vh; }
.doodle-20 { -webkit-mask-image: url('assets/doodles/lotus.png'); mask-image: url('assets/doodles/lotus.png'); left: 22vw; top: 32vh; }
.doodle-21 { -webkit-mask-image: url('assets/doodles/mail.png'); mask-image: url('assets/doodles/mail.png'); left: 32vw; top: 42vh; }
.doodle-22 { -webkit-mask-image: url('assets/doodles/moon.png'); mask-image: url('assets/doodles/moon.png'); left: 42vw; top: 52vh; }
.doodle-23 { -webkit-mask-image: url('assets/doodles/pokeball.png'); mask-image: url('assets/doodles/pokeball.png'); left: 52vw; top: 62vh; }
.doodle-24 { -webkit-mask-image: url('assets/doodles/runes.png'); mask-image: url('assets/doodles/runes.png'); left: 62vw; top: 72vh; }
.doodle-25 { -webkit-mask-image: url('assets/doodles/shield.png'); mask-image: url('assets/doodles/shield.png'); left: 72vw; top: 82vh; }
.doodle-26 { -webkit-mask-image: url('assets/doodles/shiny.png'); mask-image: url('assets/doodles/shiny.png'); left: 82vw; top: 12vh; }
.doodle-27 { -webkit-mask-image: url('assets/doodles/snail.png'); mask-image: url('assets/doodles/snail.png'); left: 92vw; top: 22vh; }
.doodle-28 { -webkit-mask-image: url('assets/doodles/sound.png'); mask-image: url('assets/doodles/sound.png'); left: 18vw; top: 82vh; }
.doodle-29 { -webkit-mask-image: url('assets/doodles/spiral.png'); mask-image: url('assets/doodles/spiral.png'); left: 28vw; top: 72vh; }
.doodle-30 { -webkit-mask-image: url('assets/doodles/star.png'); mask-image: url('assets/doodles/star.png'); left: 38vw; top: 62vh; }
.doodle-31 { -webkit-mask-image: url('assets/doodles/stop.png'); mask-image: url('assets/doodles/stop.png'); left: 48vw; top: 52vh; }
.doodle-32 { -webkit-mask-image: url('assets/doodles/sun.png'); mask-image: url('assets/doodles/sun.png'); left: 58vw; top: 42vh; }
.doodle-33 { -webkit-mask-image: url('assets/doodles/tree.png'); mask-image: url('assets/doodles/tree.png'); left: 68vw; top: 32vh; }
.doodle-34 { -webkit-mask-image: url('assets/doodles/triskel.png'); mask-image: url('assets/doodles/triskel.png'); left: 78vw; top: 22vh; }
.doodle-35 { -webkit-mask-image: url('assets/doodles/yin_yang.png'); mask-image: url('assets/doodles/yin_yang.png'); left: 88vw; top: 12vh; }
/* 3. A quick animation for the color loop */
.loop-color {
animation: colorShift 12s infinite alternate ease-in-out;
}
@keyframes colorShift {
/* 0% and 100% are identical to create the "Infinite Circle" effect */
0% { background-color: #3075ff; } /* Royal Blue (Start) */
8% { background-color: #24a1ff; } /* Sky Blue */
17% { background-color: #1ad8ff; } /* Cyan */
25% { background-color: #1bffa7; } /* Seafoam Green */
33% { background-color: #1fff4d; } /* Bright Green */
42% { background-color: #8bff32; } /* Lime Green */
50% { background-color: #dcff38; } /* Electric Yellow */
58% { background-color: #ffbc29; } /* Golden Yellow */
67% { background-color: #ff8c4a; } /* Coral Orange */
75% { background-color: #ff1d1d; } /* Hot Red */
83% { background-color: #ff2bf3; } /* Magenta Pink */
92% { background-color: #ac37ff; } /* Electric Purple */
100% { background-color: #3075ff; } /* Royal Blue (Seamless Loop) */
}
+117
View File
@@ -0,0 +1,117 @@
// Function to update a specific shape's color and position
function updateShape(id, x, y, color) {
const element = document.getElementById(id);
if (element) {
element.style.left = x + "px";
element.style.top = y + "px";
element.style.backgroundColor = color;
}
}
// Example usage: Move shape1 to (100, 100) and make it red
// updateShape('shape1', 100, 100, '#ff0000');
function moveRandomly(id) {
const element = document.getElementById(id);
if (!element) return;
// Calculate random coordinates
// We subtract 300 so the shape doesn't go partially off-screen (since your width is 300px)
const maxX = window.innerWidth - 300;
const maxY = window.innerHeight - 300;
const randomX = Math.floor(Math.random() * maxX);
const randomY = Math.floor(Math.random() * maxY);
// Generate a random HEX color
const randomColor = "#" + Math.floor(Math.random()*16777215).toString(16);
// Apply the changes
element.style.left = randomX + "px";
element.style.top = randomY + "px";
element.style.backgroundColor = randomColor;
}
// To make it move every 2 seconds automatically:
// setInterval(() => moveRandomly('shape1'), 2000);
// setInterval(() => moveRandomly('shape2'), 2000);
function startSmoothRandomMove(id, speed = 2) {
const el = document.getElementById(id);
if (!el) return;
// 1. Get initial pixel position or pick random if CSS isn't loaded yet
const rect = el.getBoundingClientRect();
const state = {
x: rect.left || Math.random() * (window.innerWidth - 142),
y: rect.top || Math.random() * (window.innerHeight - 142),
angle: Math.random() * Math.PI * 2,
speed: speed
};
function update() {
// 2. Refresh screen boundaries every frame
const screenW = window.innerWidth;
const screenH = window.innerHeight;
const shapeSize = 142; // Matches your CSS width/height
// 3. Calculate next step
state.x += Math.cos(state.angle) * state.speed;
state.y += Math.sin(state.angle) * state.speed;
// 4. BOUNCE LOGIC (Corrected)
// Horizontal check
if (state.x <= 0) {
state.x = 0;
state.angle = Math.PI - state.angle;
} else if (state.x + shapeSize >= screenW) {
state.x = screenW - shapeSize;
state.angle = Math.PI - state.angle;
}
// Vertical check
if (state.y <= 0) {
state.y = 0;
state.angle = -state.angle;
} else if (state.y + shapeSize >= screenH) {
state.y = screenH - shapeSize;
state.angle = -state.angle;
}
// 5. Apply position using pixels for precision
el.style.left = state.x + "px";
el.style.top = state.y + "px";
requestAnimationFrame(update);
}
requestAnimationFrame(update);
}
// This loop runs 35 times, once for each shape ID
for (let i = 1; i <= 35; i++) {
// Generate a random speed between 1 and 4 for each shape
// so they don't all move at the exact same pace
const randomSpeed = 1 + Math.random() * 3;
// Call your function using the ID 'shape1', 'shape2', etc.
startSmoothRandomMove(`shape${i}`, randomSpeed);
}
function randomizeAnimationStarts() {
const shapes = document.querySelectorAll('.loop-color');
shapes.forEach(shape => {
// Pick a random number between 0 and 10 (since your loop is 10s)
const randomDelay = Math.random() * - 12;
// Apply it directly to the element's style
shape.style.animationDelay = randomDelay + "s";
});
}
// Call this once when the script loads
randomizeAnimationStarts();
@@ -3,20 +3,17 @@
// ───────────────────────────────────────────── // ─────────────────────────────────────────────
class Duel { class Duel {
// ui : { showOverlay, hideOverlay, render, renderOpponent, updateButtons } constructor(socket, tetrisGame, onStatusChange, onStart) {
constructor(socket, tetrisGame, onStatusChange, onStart, ui) {
this.socket = socket; this.socket = socket;
this.tetrisGame = tetrisGame; this.tetrisGame = tetrisGame;
this.onStatusChange = onStatusChange; this.onStatusChange = onStatusChange; // (status, opponentName) => void
this.onStart = onStart; this.onStart = onStart; // () => void — déclenche le début du jeu local
this.ui = ui;
this.action_queue = []; this.action_queue = [];
this.opponentGrid = this._emptyGrid(); this.opponentGrid = this._emptyGrid();
this.opponentScore = 0; this.opponentScore = 0;
this.opponentShieldActive = false; this.roomCode = null;
this.roomCode = null; this.isReady = false;
this.isReady = false;
this._bindSocketEvents(); this._bindSocketEvents();
} }
@@ -36,11 +33,10 @@ class Duel {
leave() { leave() {
if (!this.roomCode) return; if (!this.roomCode) return;
this.socket.emit('tetris:leave'); this.socket.emit('tetris:leave');
this.roomCode = null; this.roomCode = null;
this.isReady = false; this.isReady = false;
this.opponentGrid = this._emptyGrid(); this.opponentGrid = this._emptyGrid();
this.opponentScore = 0; this.opponentScore = 0;
this.opponentShieldActive = false;
} }
// ─── Hooks appelés par tetris.js ────────── // ─── Hooks appelés par tetris.js ──────────
@@ -52,7 +48,9 @@ class Duel {
onLocalLinesCleared(count, holeCol) { onLocalLinesCleared(count, holeCol) {
if (!this.isReady) return; if (!this.isReady) return;
const garbageLines = Array.from({ length: count }, () => this._buildGarbageLine(holeCol)); const garbageLines = [];
for (let i = 0; i < count; i++)
garbageLines.push(this._buildGarbageLine(holeCol));
this.socket.emit('tetris:lines-cleared', { count, holeCol, garbageLines }); this.socket.emit('tetris:lines-cleared', { count, holeCol, garbageLines });
} }
@@ -62,12 +60,6 @@ class Duel {
this.endDuel(); this.endDuel();
} }
onLocalShieldChanged(event) {
if (!this.isReady) return;
if (event === 'activated') this.socket.emit('tetris:shield-activated');
else if (event === 'deactivated') this.socket.emit('tetris:shield-deactivated');
}
endDuel() { endDuel() {
this.isReady = false; this.isReady = false;
this.action_queue = []; this.action_queue = [];
@@ -78,7 +70,8 @@ class Duel {
synchronize_game() { synchronize_game() {
while (this.action_queue.length > 0) { while (this.action_queue.length > 0) {
this._processAction(this.action_queue.shift()); const action = this.action_queue.shift();
this._processAction(action);
} }
} }
@@ -88,7 +81,7 @@ class Duel {
this.opponentGrid = action.grid; this.opponentGrid = action.grid;
this.opponentScore = action.score; this.opponentScore = action.score;
document.getElementById('opponent-score').textContent = action.score; document.getElementById('opponent-score').textContent = action.score;
this.ui.renderOpponent(this.opponentGrid, this.opponentShieldActive); renderOpponent(this.opponentGrid);
break; break;
case 'LINES_CLEARED': case 'LINES_CLEARED':
@@ -96,17 +89,9 @@ class Duel {
break; break;
case 'OPPONENT_GAME_OVER': case 'OPPONENT_GAME_OVER':
this.ui.showOverlay('YOU WIN', action.score); showOverlay('YOU WIN', action.score);
this.endDuel(); this.endDuel();
break; break;
case 'OPPONENT_SHIELD_ACTIVATED':
this.opponentShieldActive = true;
break;
case 'OPPONENT_SHIELD_DEACTIVATED':
this.opponentShieldActive = false;
break;
} }
} }
@@ -142,36 +127,28 @@ class Duel {
this.action_queue.push({ type: 'OPPONENT_GAME_OVER', score: data.score, validBlock: data.validBlock }); this.action_queue.push({ type: 'OPPONENT_GAME_OVER', score: data.score, validBlock: data.validBlock });
}); });
this.socket.on('tetris:shield-activated', () => {
this.action_queue.push({ type: 'OPPONENT_SHIELD_ACTIVATED' });
});
this.socket.on('tetris:shield-deactivated', () => {
this.action_queue.push({ type: 'OPPONENT_SHIELD_DEACTIVATED' });
});
this.socket.on('tetris:start-duel', () => { this.socket.on('tetris:start-duel', () => {
if (this.onStart) this.onStart(); if (this.onStart) this.onStart();
}); });
this.socket.on('tetris:pause', () => { this.socket.on('tetris:pause', () => {
this.tetrisGame.pause(); this.tetrisGame.pause();
this.ui.updateButtons(); updateButtons();
if (this.tetrisGame.isPaused) this.ui.showOverlay('PAUSE'); if (this.tetrisGame.isPaused) showOverlay('PAUSE');
else this.ui.hideOverlay(); else hideOverlay();
}); });
this.socket.on('tetris:stop', () => { this.socket.on('tetris:stop', () => {
this.tetrisGame.stop(); this.tetrisGame.stop();
this.ui.updateButtons(); updateButtons();
this.ui.render(); render();
this.ui.showOverlay('STOPPED'); showOverlay('STOPPED');
}); });
this.socket.on('tetris:settings', (data) => { this.socket.on('tetris:settings', (data) => {
document.getElementById('input-ttd').value = data.timeToDown; document.getElementById('input-ttd').value = data.timeToDown;
document.getElementById('input-hardening').value = data.hardening; document.getElementById('input-hardening').value = data.hardening;
document.getElementById('input-decrement').value = data.decrementTTD; document.getElementById('input-decrement').value = data.decrementTTD;
this.tetrisGame.configure(data); this.tetrisGame.configure(data);
}); });
} }
@@ -82,7 +82,6 @@ export const Events = {
// Avatar // Avatar
AVATAR_UPDATED: 'avatar:updated', AVATAR_UPDATED: 'avatar:updated',
AVATAR_DELETED: 'avatar:deleted',
// Chat // Chat
CHAT_CONNECTED: 'chat:connected', CHAT_CONNECTED: 'chat:connected',
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

@@ -1,6 +1,6 @@
import { Window, windowRegistry } from '../core/windows.js'; import { Window, windowRegistry } from './windows.js';
import { API, STORAGE_KEYS, CSS } from '../core/config.js'; import { API, STORAGE_KEYS, CSS } from './config.js';
import { eventBus, Events } from '../core/events.js'; import { eventBus, Events } from './events.js';
/** /**
* Friends management window * Friends management window
@@ -82,7 +82,6 @@ html {
body { body {
margin: 0; margin: 0;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
color: var(--color-text); color: var(--color-text);
line-height: 1.5; line-height: 1.5;
@@ -121,7 +120,7 @@ body {
.title { .title {
position: absolute; position: absolute;
z-index: 1; z-index: 999;
top: 20px; top: 20px;
left: 50%; left: 50%;
translate: -50% 0; translate: -50% 0;
+79
View File
@@ -0,0 +1,79 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Lobby</title>
<link rel="stylesheet" href="doodle.css">
<link rel="stylesheet" href="game.css" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Cinzel+Decorative:wght@400;700&display=swap" rel="stylesheet" />
<script src="doodle.js" defer></script>
</head>
<script type="module" src="app.js"></script>
<body>
<h1 class="title">
<span>L</span>
<span>o</span>
<span>b</span>
<span>b</span>
<span>y</span>
</h1>
<nav class="menu" aria-label="Menu principal">
<button class="menu__item" data-action="login" aria-label="Login">Login</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="friends" aria-label="Amis">Amis</button>
</nav>
<nav class="game" aria-label="Game">
<button class="game__item" data-action="Home page" aria-label="Home Page"
onclick="window.location.href='index.html'">Home Page</button>
</nav>
<div class="page" aria-label="Page">
<button class="page__item" data-action="gameroom" aria-label="Game Rooms">Game Rooms</button>
</div>
<div class="shape doodle-1 loop-color" id="shape1"></div>
<div class="shape doodle-2 loop-color" id="shape2"></div>
<div class="shape doodle-3 loop-color" id="shape3"></div>
<div class="shape doodle-4 loop-color" id="shape4"></div>
<div class="shape doodle-5 loop-color" id="shape5"></div>
<div class="shape doodle-6 loop-color" id="shape6"></div>
<div class="shape doodle-7 loop-color" id="shape7"></div>
<div class="shape doodle-8 loop-color" id="shape8"></div>
<div class="shape doodle-9 loop-color" id="shape9"></div>
<div class="shape doodle-10 loop-color" id="shape10"></div>
<div class="shape doodle-11 loop-color" id="shape11"></div>
<div class="shape doodle-12 loop-color" id="shape12"></div>
<div class="shape doodle-13 loop-color" id="shape13"></div>
<div class="shape doodle-14 loop-color" id="shape14"></div>
<div class="shape doodle-15 loop-color" id="shape15"></div>
<div class="shape doodle-16 loop-color" id="shape16"></div>
<div class="shape doodle-17 loop-color" id="shape17"></div>
<div class="shape doodle-18 loop-color" id="shape18"></div>
<div class="shape doodle-19 loop-color" id="shape19"></div>
<div class="shape doodle-20 loop-color" id="shape20"></div>
<div class="shape doodle-21 loop-color" id="shape21"></div>
<div class="shape doodle-22 loop-color" id="shape22"></div>
<div class="shape doodle-23 loop-color" id="shape23"></div>
<div class="shape doodle-24 loop-color" id="shape24"></div>
<div class="shape doodle-25 loop-color" id="shape25"></div>
<div class="shape doodle-26 loop-color" id="shape26"></div>
<div class="shape doodle-27 loop-color" id="shape27"></div>
<div class="shape doodle-28 loop-color" id="shape28"></div>
<div class="shape doodle-29 loop-color" id="shape29"></div>
<div class="shape doodle-30 loop-color" id="shape30"></div>
<div class="shape doodle-31 loop-color" id="shape31"></div>
<div class="shape doodle-32 loop-color" id="shape32"></div>
<div class="shape doodle-33 loop-color" id="shape33"></div>
<div class="shape doodle-34 loop-color" id="shape34"></div>
<div class="shape doodle-35 loop-color" id="shape35"></div>
</body>
</html>
@@ -1,87 +0,0 @@
.shape {
/* The "Physical" properties */
position: fixed;
/* transform: translate(-50%, -50%); Optional: This makes 'left/top' refer to the CENTER of the doodle */
width: 142px;
height: 142px;
/* The "Stenciling" instructions (but no image yet!) */
-webkit-mask-size: contain;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
/* The default "Paint" color */
background-color: white;
}
.shape:hover {
transform: scale(1.2); /* Grow by 20% when you hover the mouse over it */
transition: transform 0.3s ease; /* Make it a smooth grow */
}
/* Individual Doodle Definitions */
.doodle-0 { -webkit-mask-image: url('doodles/cat.png'); mask-image: url('doodles/cat.png'); left: 60vw; top: 35vh; }
.doodle-1 { -webkit-mask-image: url('doodles/ball.png'); mask-image: url('doodles/ball.png'); left: 10vw; top: 10vh; }
.doodle-2 { -webkit-mask-image: url('doodles/batman.png'); mask-image: url('doodles/batman.png'); left: 20vw; top: 15vh; }
.doodle-3 { -webkit-mask-image: url('doodles/building.png'); mask-image: url('doodles/building.png'); left: 30vw; top: 20vh; }
.doodle-4 { -webkit-mask-image: url('doodles/butterfly.png'); mask-image: url('doodles/butterfly.png'); left: 40vw; top: 25vh; }
.doodle-5 { -webkit-mask-image: url('doodles/car.png'); mask-image: url('doodles/car.png'); left: 50vw; top: 30vh; }
.doodle-6 { -webkit-mask-image: url('doodles/yin_yang.png'); mask-image: url('doodles/yin_yang.png'); left: 88vw; top: 12vh; }
.doodle-7 { -webkit-mask-image: url('doodles/clouds.png'); mask-image: url('doodles/clouds.png'); left: 70vw; top: 40vh; }
.doodle-8 { -webkit-mask-image: url('doodles/controls.png'); mask-image: url('doodles/controls.png'); left: 80vw; top: 45vh; }
.doodle-9 { -webkit-mask-image: url('doodles/dead.png'); mask-image: url('doodles/dead.png'); left: 90vw; top: 50vh; }
.doodle-10 { -webkit-mask-image: url('doodles/diamant.png'); mask-image: url('doodles/diamant.png'); left: 15vw; top: 55vh; }
.doodle-11 { -webkit-mask-image: url('doodles/dice.png'); mask-image: url('doodles/dice.png'); left: 25vw; top: 60vh; }
.doodle-12 { -webkit-mask-image: url('doodles/earth.png'); mask-image: url('doodles/earth.png'); left: 35vw; top: 65vh; }
.doodle-13 { -webkit-mask-image: url('doodles/egypt.png'); mask-image: url('doodles/egypt.png'); left: 45vw; top: 70vh; }
.doodle-14 { -webkit-mask-image: url('doodles/fire.png'); mask-image: url('doodles/fire.png'); left: 55vw; top: 75vh; }
.doodle-15 { -webkit-mask-image: url('doodles/fish.png'); mask-image: url('doodles/fish.png'); left: 65vw; top: 80vh; }
.doodle-16 { -webkit-mask-image: url('doodles/flag.png'); mask-image: url('doodles/flag.png'); left: 75vw; top: 85vh; }
.doodle-17 { -webkit-mask-image: url('doodles/hearts.png'); mask-image: url('doodles/hearts.png'); left: 85vw; top: 90vh; }
.doodle-18 { -webkit-mask-image: url('doodles/house.png'); mask-image: url('doodles/house.png'); left: 5vw; top: 45vh; }
.doodle-19 { -webkit-mask-image: url('doodles/idol.png'); mask-image: url('doodles/idol.png'); left: 12vw; top: 22vh; }
.doodle-20 { -webkit-mask-image: url('doodles/lotus.png'); mask-image: url('doodles/lotus.png'); left: 22vw; top: 32vh; }
.doodle-21 { -webkit-mask-image: url('doodles/mail.png'); mask-image: url('doodles/mail.png'); left: 32vw; top: 42vh; }
.doodle-22 { -webkit-mask-image: url('doodles/moon.png'); mask-image: url('doodles/moon.png'); left: 42vw; top: 52vh; }
.doodle-23 { -webkit-mask-image: url('doodles/pokeball.png'); mask-image: url('doodles/pokeball.png'); left: 52vw; top: 62vh; }
.doodle-24 { -webkit-mask-image: url('doodles/runes.png'); mask-image: url('doodles/runes.png'); left: 62vw; top: 72vh; }
.doodle-25 { -webkit-mask-image: url('doodles/shield.png'); mask-image: url('doodles/shield.png'); left: 72vw; top: 82vh; }
.doodle-26 { -webkit-mask-image: url('doodles/shiny.png'); mask-image: url('doodles/shiny.png'); left: 82vw; top: 12vh; }
.doodle-27 { -webkit-mask-image: url('doodles/snail.png'); mask-image: url('doodles/snail.png'); left: 92vw; top: 22vh; }
.doodle-28 { -webkit-mask-image: url('doodles/sound.png'); mask-image: url('doodles/sound.png'); left: 18vw; top: 82vh; }
.doodle-29 { -webkit-mask-image: url('doodles/spiral.png'); mask-image: url('doodles/spiral.png'); left: 28vw; top: 72vh; }
.doodle-30 { -webkit-mask-image: url('doodles/star.png'); mask-image: url('doodles/star.png'); left: 38vw; top: 62vh; }
.doodle-31 { -webkit-mask-image: url('doodles/stop.png'); mask-image: url('doodles/stop.png'); left: 48vw; top: 52vh; }
.doodle-32 { -webkit-mask-image: url('doodles/sun.png'); mask-image: url('doodles/sun.png'); left: 58vw; top: 42vh; }
.doodle-33 { -webkit-mask-image: url('doodles/tree.png'); mask-image: url('doodles/tree.png'); left: 68vw; top: 32vh; }
.doodle-34 { -webkit-mask-image: url('doodles/triskel.png'); mask-image: url('doodles/triskel.png'); left: 78vw; top: 22vh; }
/* 3. A quick animation for the color loop */
.loop-color {
animation: colorShift 12s infinite alternate ease-in-out;
}
@keyframes colorShift {
/* 0% and 100% are identical to create the "Infinite Circle" effect */
0% { background-color: #3075ff; } /* Royal Blue (Start) */
8% { background-color: #24a1ff; } /* Sky Blue */
17% { background-color: #1ad8ff; } /* Cyan */
25% { background-color: #1bffa7; } /* Seafoam Green */
33% { background-color: #1fff4d; } /* Bright Green */
42% { background-color: #8bff32; } /* Lime Green */
50% { background-color: #dcff38; } /* Electric Yellow */
58% { background-color: #ffbc29; } /* Golden Yellow */
67% { background-color: #ff8c4a; } /* Coral Orange */
75% { background-color: #ff1d1d; } /* Hot Red */
83% { background-color: #ff2bf3; } /* Magenta Pink */
92% { background-color: #ac37ff; } /* Electric Purple */
100% { background-color: #3075ff; } /* Royal Blue (Seamless Loop) */
}
@@ -1,113 +0,0 @@
const maxdoodles = 34;
// /////////////////////////////////////////////////////////////////////////////////////////>\
// container for all doodles, create them
class DoodleContainer {
constructor(parent) {
this.parent = parent;
this.obj = document.createElement('div');
Object.assign(this.obj.style, {
width: '100vw',
height: '100vw',
});
this.createAllDoodles();
parent.append(this.obj);
this.randomizeAnimationStarts();
}
createAllDoodles() {
for (let i = 0; i <= maxdoodles; i++) {
let d = document.createElement('div');
d.classList.add('shape', 'doodle-' + i, 'loop-color');
d.id = 'shape' + i;
this.obj.append(d);
d.addEventListener('click', () => {
console.log(`hi from ${d.id}!`);
})
}
}
startSmoothRandomMove(id, speed = 2) {
const el = document.getElementById(id);
if (!el)
return;
// 1. Get initial pixel position or pick random if CSS isn't loaded yet
const rect = el.getBoundingClientRect();
const state = {
x: rect.left || Math.random() * (window.innerWidth - 142),
y: rect.top || Math.random() * (window.innerHeight - 142),
angle: Math.random() * Math.PI * 2,
speed: speed
};
function update() {
// 2. Refresh screen boundaries every frame
const screenW = window.innerWidth;
const screenH = window.innerHeight;
const shapeSize = 142; // Matches your CSS width/height
// 3. Calculate next step
state.x += Math.cos(state.angle) * state.speed;
state.y += Math.sin(state.angle) * state.speed;
// 4. BOUNCE LOGIC
// Horizontal check
if (state.x <= 0) {
state.x = 0;
state.angle = Math.PI - state.angle;
} else if (state.x + shapeSize >= screenW) {
state.x = screenW - shapeSize;
state.angle = Math.PI - state.angle;
}
// Vertical check
if (state.y <= 0) {
state.y = 0;
state.angle = -state.angle;
} else if (state.y + shapeSize >= screenH) {
state.y = screenH - shapeSize;
state.angle = -state.angle;
}
// 5. Apply position using pixels for precision
el.style.left = state.x + "px";
el.style.top = state.y + "px";
requestAnimationFrame(update);
}
requestAnimationFrame(update);
}
randomizeAnimationStarts() {
for (let i = 0; i <= maxdoodles; i++) {
const randomSpeed = 1 + Math.random() * 3;
this.startSmoothRandomMove(`shape${i}`, randomSpeed);
}
}
}
// /////////////////////////////////////////////////////////////////////////////////////////>
// all loop-color have the same @colorShift animation cycle, this disynchronize them
function randomizeColorsStarts() {
const shapes = document.querySelectorAll('.loop-color');
shapes.forEach(shape => {
// Pick a random number between 0 and 10 (since your loop is 10s)
const randomDelay = Math.random() * - 15;
// Apply it directly to the element's style
shape.style.animationDelay = randomDelay + "s";
});
}
const a = new DoodleContainer(document.body);
// Call this once when the script loads
randomizeColorsStarts();
@@ -1,43 +0,0 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Skkribl.io</title>
<link rel="stylesheet" href="doodle.css">
<link rel="stylesheet" href="game.css" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Cinzel+Decorative:wght@400;700&display=swap" rel="stylesheet" />
<script src="doodle.js" defer></script>
<script type="module" src="../app.js"></script>
</head>
<body>
<h1 class="title">
<span>L</span>
<span>o</span>
<span>b</span>
<span>b</span>
<span>y</span>
</h1>
<nav class="menu" aria-label="Menu principal">
<button class="menu__item" data-action="login" aria-label="Login">Login</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="friends" aria-label="Amis">Amis</button>
</nav>
<nav class="game" aria-label="Game">
<button class="game__item" data-action="Home page" aria-label="Home Page"
onclick="window.location.href='../index.html'">Home Page</button>
</nav>
<div class="page" aria-label="Page">
<button class="page__item" data-action="gameroom" aria-label="Game Rooms">Game Rooms</button>
</div>
</body>
</html>
@@ -1,6 +1,6 @@
import { Window } from '../core/windows.js'; import { Window } from './windows.js';
import { API, STORAGE_KEYS, CSS } from '../core/config.js'; import { API, STORAGE_KEYS, CSS } from './config.js';
import { eventBus, Events } from '../core/events.js'; import { eventBus, Events } from './events.js';
export class GameRoomWindow extends Window { export class GameRoomWindow extends Window {
constructor() { constructor() {
@@ -34,7 +34,6 @@ export class GameRoomWindow extends Window {
}); });
this.updateTabsAccess(); this.updateTabsAccess();
this.loadCurrentTab();
// Verifier si l'utilisateur est deja dans un salon au chargement // Verifier si l'utilisateur est deja dans un salon au chargement
const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
@@ -120,8 +119,7 @@ export class GameRoomWindow extends Window {
this.gameInfo = this.createElement('div', 'gameroom__game-info'); this.gameInfo = this.createElement('div', 'gameroom__game-info');
this.currentDrawerInfo = this.createElement('div', 'gameroom__drawer-info', { text: '' }); this.currentDrawerInfo = this.createElement('div', 'gameroom__drawer-info', { text: '' });
this.scoresDisplay = this.createElement('div', 'gameroom__scores-display'); this.scoresDisplay = this.createElement('div', 'gameroom__scores-display');
this.timerDisplay = this.createElement('div', 'gameroom__timer-display'); this.gameInfo.append(this.currentDrawerInfo, this.scoresDisplay);
this.gameInfo.append(this.currentDrawerInfo, this.scoresDisplay, this.timerDisplay);
// Affichage du mot caché // Affichage du mot caché
this.wordDisplay = this.createElement('div', 'gameroom__word-display'); this.wordDisplay = this.createElement('div', 'gameroom__word-display');
@@ -196,10 +194,7 @@ export class GameRoomWindow extends Window {
players: [], players: [],
currentPlayerIndex: 0, currentPlayerIndex: 0,
guessedLetters: [], guessedLetters: [],
scores: {}, scores: {}
counter: 0,
counterRound: 0,
timer: 0
}; };
this.initDrawing(); this.initDrawing();
@@ -379,11 +374,10 @@ export class GameRoomWindow extends Window {
this.socket.on('game-player-left', (data) => { this.socket.on('game-player-left', (data) => {
this.showMessage(`${data.username} a quitté le salon`, 'info'); this.showMessage(`${data.username} a quitté le salon`, 'info');
console.log(`${data.username} left the room`);
if (this.gameState.isPlaying) if (this.gameState.isPlaying)
{ {
if (Array.isArray(this.gameState.players)) if (this.gameState.players)
this.gameState.players = this.gameState.players.filter(p => p !== data.username); this.gameState.players = this.gameState.players.filter(p => p !== data.username);
} }
@@ -499,7 +493,7 @@ export class GameRoomWindow extends Window {
// If spectating, return to spectator list // If spectating, return to spectator list
if (this.isSpectating) { if (this.isSpectating) {
this.resetGameUI(); this.resetGameUI();
// this.currentRoom = null; this.currentRoom = null;
this.isSpectating = false; this.isSpectating = false;
this.switchTab('spectator'); this.switchTab('spectator');
this.showMessage('La partie est terminée', 'info'); this.showMessage('La partie est terminée', 'info');
@@ -524,8 +518,6 @@ export class GameRoomWindow extends Window {
this.gameState.revealedWord = data.revealedWord || new Array(data.wordLength).fill('_'); this.gameState.revealedWord = data.revealedWord || new Array(data.wordLength).fill('_');
this.gameState.players = data.players; this.gameState.players = data.players;
this.gameState.scores = data.scores || {}; this.gameState.scores = data.scores || {};
this.gameState.timer = data.timer || 0;
this.updateTimerUI();
this.showGameUI(); this.showGameUI();
this.updateWordDisplay(); this.updateWordDisplay();
@@ -615,15 +607,6 @@ export class GameRoomWindow extends Window {
// Setup UI for new round with new drawer // Setup UI for new round with new drawer
this.setupRound(); this.setupRound();
}); });
this.socket.on('game-timer-sync', (data) => {
this.gameState.timer = data.remaining;
this.updateTimerUI();
});
this.socket.on('game-timer-ended', (data) => {
this.showMessage(data.message || 'Temps écoulé !', 'info');
})
} }
disconnectGameSocket() { disconnectGameSocket() {
@@ -734,7 +717,7 @@ export class GameRoomWindow extends Window {
const altPort = window.GLOBAL_CHAT_ALT_PORT; const altPort = window.GLOBAL_CHAT_ALT_PORT;
if (altPort) { if (altPort) {
const host = location.hostname || 'localhost'; const host = location.hostname || 'localhost';
this.socket = io(`${location.protocol}//${host}:${altPort}`, ioConfig); this.socket = io(`http://${host}:${altPort}`, ioConfig);
} else { } else {
this.socket = io(ioConfig); this.socket = io(ioConfig);
} }
@@ -768,7 +751,8 @@ export class GameRoomWindow extends Window {
return; return;
} }
this.renderRoomsList(data || []); this.roomsList = data || [];
this.renderRoomsList(this.roomsList);
} catch (error) { } catch (error) {
console.error('Load rooms error:', error); console.error('Load rooms error:', error);
this.showMessage('Erreur de connexion', 'error'); this.showMessage('Erreur de connexion', 'error');
@@ -855,20 +839,17 @@ export class GameRoomWindow extends Window {
const name = this.roomNameInput.value.trim(); const name = this.roomNameInput.value.trim();
if (!name) { if (!name) {
this.showMessage('Entrez un nom pour le salon', 'error'); this.showMessage('Entrez un nom pour le salon', 'error');
this.showNotification('Entrez un nom pour le salon', 'red');
return; return;
} }
const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN); const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
if (!token) { if (!token) {
this.showMessage('Connectez-vous pour creer un salon', 'info'); this.showMessage('Connectez-vous pour creer un salon', 'info');
this.showNotification('Connectez-vous pour créer un salon', 'red');
return; return;
} }
if (this.currentRoom) { if (this.currentRoom) {
this.showMessage('Vous etes deja dans un salon. Quittez-le d\'abord.', 'error'); this.showMessage('Vous etes deja dans un salon. Quittez-le d\'abord.', 'error');
this.showNotification('Vous êtes déjà dans un salon. Quittez-le d\'abord.', 'red');
return; return;
} }
@@ -882,7 +863,6 @@ export class GameRoomWindow extends Window {
this.currentRoom = currentData; this.currentRoom = currentData;
this.enterLobby(currentData); this.enterLobby(currentData);
this.showMessage('Vous etes deja dans un salon', 'error'); this.showMessage('Vous etes deja dans un salon', 'error');
this.showNotification('Vous êtes déjà dans un salon. Quittez-le d\'abord.', 'red');
return; return;
} }
} }
@@ -903,7 +883,6 @@ export class GameRoomWindow extends Window {
if (this.roomNameExists(name)) { if (this.roomNameExists(name)) {
this.showMessage('Un salon avec ce nom existe deja', 'error'); this.showMessage('Un salon avec ce nom existe deja', 'error');
this.showNotification('Un salon avec ce nom existe deja', 'red');
return; return;
} }
@@ -925,7 +904,6 @@ export class GameRoomWindow extends Window {
this.showMessage('Salon cree', 'success'); this.showMessage('Salon cree', 'success');
eventBus.emit(Events.ROOM_CREATED, data); eventBus.emit(Events.ROOM_CREATED, data);
this.enterLobby(data); this.enterLobby(data);
this.showNotification(`Vous avez créé le salon "${data.name}"`, 'green');
} catch (error) { } catch (error) {
console.error('Create room error:', error); console.error('Create room error:', error);
this.showMessage('Erreur de connexion', 'error'); this.showMessage('Erreur de connexion', 'error');
@@ -1058,7 +1036,6 @@ export class GameRoomWindow extends Window {
if (this.currentRoom) { if (this.currentRoom) {
this.showMessage('Vous etes deja dans un salon. Quittez-le d\'abord.', 'error'); this.showMessage('Vous etes deja dans un salon. Quittez-le d\'abord.', 'error');
this.showNotification('Vous êtes déjà dans un salon. Quittez-le d\'abord.', 'red');
return; return;
} }
@@ -1072,7 +1049,6 @@ export class GameRoomWindow extends Window {
this.currentRoom = currentData; this.currentRoom = currentData;
this.enterLobby(currentData); this.enterLobby(currentData);
this.showMessage('Vous etes deja dans un salon', 'error'); this.showMessage('Vous etes deja dans un salon', 'error');
this.showNotification('Vous êtes déjà dans un salon. Quittez-le d\'abord.', 'red');
return; return;
} }
} }
@@ -1117,9 +1093,7 @@ export class GameRoomWindow extends Window {
} }
async loadLobby() { async loadLobby() {
console.log('Loading lobby for room:', this.currentRoom);
if (!this.currentRoom) return; if (!this.currentRoom) return;
console.log('Managed to load lobby, current room:', this.currentRoom);
this.gameState.scores = {}; this.gameState.scores = {};
@@ -1258,12 +1232,6 @@ export class GameRoomWindow extends Window {
}, 5000); }, 5000);
} }
updateTimerUI()
{
if (this.timerDisplay)
this.timerDisplay.textContent = `Temps restant : ${this.gameState.timer}s`;
}
// ============================================ // ============================================
// LOGIQUE DU JEU // LOGIQUE DU JEU
// ============================================ // ============================================
@@ -1318,9 +1286,6 @@ export class GameRoomWindow extends Window {
this.isSpectating = false; this.isSpectating = false;
this.gameState.scores = {}; this.gameState.scores = {};
this.gameState.counter = 0;
this.gameState.counterRound = 0;
this.gameState.timer = 0;
this.gameState.players = []; this.gameState.players = [];
this.gameState.currentPlayerIndex = 0; this.gameState.currentPlayerIndex = 0;
this.gameState.guessedLetters = []; this.gameState.guessedLetters = [];
@@ -1544,7 +1509,7 @@ export class GameRoomWindow extends Window {
const pointsText = points !== 0 ? ` (${points > 0 ? '+' : ''}${points} pts)` : ''; const pointsText = points !== 0 ? ` (${points > 0 ? '+' : ''}${points} pts)` : '';
if (success) { if (success) {
item.textContent = `${username}: "${guess}" - Bon ${typeText}!${pointsText}`; item.textContent = `${username}: "${guess}" - Bonne ${typeText}!${pointsText}`;
} else { } else {
item.textContent = `${username}: "${guess}" - Mauvais ${typeText}${pointsText}`; item.textContent = `${username}: "${guess}" - Mauvais ${typeText}${pointsText}`;
} }
@@ -1594,10 +1559,7 @@ export class GameRoomWindow extends Window {
this.wordDisplay.textContent = word.split('').join(' '); this.wordDisplay.textContent = word.split('').join(' ');
// Auto next round after delay // Auto next round after delay
this.gameState.counterRound++;
setTimeout(() => { setTimeout(() => {
if (this.gameState.counterRound >= (this.gameState.players.length * 4))
this.endGame();
if (this.gameState.isPlaying) { if (this.gameState.isPlaying) {
this.nextRound(); this.nextRound();
} }
@@ -1606,11 +1568,8 @@ export class GameRoomWindow extends Window {
nextRound() { nextRound() {
// Move to next player // Move to next player
this.gameState.counter++; this.gameState.currentPlayerIndex = (this.gameState.currentPlayerIndex + 1) % this.gameState.players.length;
if (this.gameState.counter >= this.gameState.players.length) { const nextDrawer = this.gameState.players[this.gameState.currentPlayerIndex];
this.gameState.counter = 0;
}
const nextDrawer = this.gameState.players[this.gameState.counter];
if (this.socket?.connected) { if (this.socket?.connected) {
this.socket.emit('game-next-round', { drawer: nextDrawer }); this.socket.emit('game-next-round', { drawer: nextDrawer });
@@ -1,6 +1,6 @@
import { Window } from '../core/windows.js'; import { Window } from './windows.js';
import { STORAGE_KEYS, CSS } from '../core/config.js'; import { STORAGE_KEYS, CSS } from './config.js';
import { eventBus, Events } from '../core/events.js'; import { eventBus, Events } from './events.js';
/** /**
* Global chat window * Global chat window
@@ -222,7 +222,7 @@ export class GlobalChat extends Window {
const altPort = window.GLOBAL_CHAT_ALT_PORT; const altPort = window.GLOBAL_CHAT_ALT_PORT;
if (altPort) { if (altPort) {
const host = location.hostname || 'localhost'; const host = location.hostname || 'localhost';
this.socket = io(`${location.protocol}//${host}:${altPort}`, ioConfig); this.socket = io(`http://${host}:${altPort}`, ioConfig);
} else { } else {
this.socket = io(ioConfig); this.socket = io(ioConfig);
} }
+14 -136
View File
@@ -69,45 +69,32 @@
} }
html { html {
height: 100%;
background-image: var(--app-background-image), var(--app-background-base);
/* Make background image responsive but limited in size */
background-size: clamp(400px, 100%, 800px) auto, cover;
/* Align image to the top, gradient stays centered */
background-position: center 35%, center;
background-repeat: no-repeat, no-repeat;
}
/* html {
height: 100%; height: 100%;
background-image: background-image:
var(--app-background-image), var(--app-background-image),
var(--app-background-base); var(--app-background-base);
background-size: background-size:
80%, contain,
cover; cover;
background-position: background-position:
50% 20%, center,
center; center;
background-repeat: background-repeat:
no-repeat, no-repeat,
no-repeat; no-repeat;
} */ }
body { body {
margin: 0 auto; margin: 0;
width: 70%; width: 70%;
min-width: 800px; min-width: 800px;
margin: 0 auto;
display: flex; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
flex-direction: column; color: var(--color-text);
min-height: 100vh; /* KEY */ line-height: 1.5;
} }
/* ============================================ /* ============================================
@@ -233,57 +220,6 @@ body {
border-color: var(--color-primary); border-color: var(--color-primary);
} }
/* ///////////////////////////////////////////////////////// */
footer {
margin-top: auto; /* pushes footer to bottom */
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.footer_div {
display: flex;
justify-content: center;
gap: 30%; /* space between items */
padding: 10px 0;
}
.footer_grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
justify-items: center; /* center each column */
align-items: center;
gap: 10px 60px; /* row gap / column gap */
}
.ico_footer {
width: 25%;
}
a {
text-decoration: none;
color: #000000;
}
a:hover {
color: rgb(218, 145, 12);
}
.legal {
margin-top: 5%;
padding-bottom: 5%;
text-align: center;
display: block;
color: #000000;
}
.legal:hover {
color: rgb(218, 145, 12);
}
/* ///////////////////////////////////////////////////////// */
/* ============================================ /* ============================================
BUTTONS BUTTONS
============================================ */ ============================================ */
@@ -473,6 +409,11 @@ a:hover {
============================================ */ ============================================ */
.login { .login {
width: 320px; width: 320px;
border-radius: 5px;
border-color: #aa1f1f;
border: 6px solid #faac37;
background: #ffffff;
color: #000;
} }
.login__form { .login__form {
@@ -684,30 +625,6 @@ a:hover {
padding: var(--spacing-sm) 0; padding: var(--spacing-sm) 0;
} }
/* ============================================
EASTER EGG BUTTON
============================================ */
/* .easter-egg {
position: absolute;
top: 20%;
left: 50%;
transform: translateX(-50%);
z-index: 1;
background: var(--color-surface);
color: var(--color-text);
border: 1px solid var(--color-surface-light);
padding: var(--spacing-sm) var(--spacing-md);
cursor: pointer;
font-size: var(--font-size-md);
border-radius: var(--radius-md);
transition: all var(--transition-fast);
}
.easter-egg:hover {
background: var(--color-error);
border-color: var(--color-error);
} */
/* ============================================ /* ============================================
UTILITIES UTILITIES
============================================ */ ============================================ */
@@ -753,7 +670,7 @@ a:hover {
.friends__tab { .friends__tab {
flex: 1; flex: 1;
padding: var(--spacing-sm); padding: var(--spacing-sm);
background: var(--color-surface); background: var(--color-surface-light);
border: 1px solid var(--color-surface-light); border: 1px solid var(--color-surface-light);
color: var(--color-text); color: var(--color-text);
cursor: pointer; cursor: pointer;
@@ -833,42 +750,3 @@ a:hover {
color: var(--color-text-muted); color: var(--color-text-muted);
padding: var(--spacing-lg); padding: var(--spacing-lg);
} }
.container-1 {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
margin: 5px;
position: relative;
min-height: 200px;
}
/* ///////////////////////////////////////////////////////// */
.button {
color: red;
margin: 5px 50px;
padding: 5px 50px;
}
.button-1 {
display: inline-block;
padding: 10px 20px;
background-color: #000000;
color: #8e8e8e;
text-align: center;
text-decoration: none;
font-size: 16px;
cursor: pointer;
border: 3px solid #363636;
border-radius: 6px;
transition: background-color 0.3s;
}
.button-1:hover {
background-color: rgb(202, 135, 10);
color: black;
}
+8 -22
View File
@@ -17,34 +17,20 @@
<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="logout" aria-label="Logout">Logout</button> <button class="menu__item" data-action="test" aria-label="Test Page"
onclick="window.location.href='test.html'">Test Page</button>
</nav> </nav>
<nav class="game" aria-label="Game"> <nav class="game" aria-label="Game">
<button class="game__item" data-action="new_game" aria-label="Skkrrribl.io" <button class="game__item" data-action="new_game" aria-label="Skkrrribl.io"
onclick="window.location.href='./game/game.html'">Skkrrribl.io</button> onclick="window.location.href='game.html'">Skkrrribl.io</button>
<button class="game__item" data-action="tetris" aria-label="Tetris" <button class="game__item" data-action="tetris" aria-label="Tetris"
onclick="window.location.href='./tetris/tetris.html'">Tetris</button> onclick="window.location.href='tetris.html'">Tetris</button>
<button class="game__item" data-action="Wiskas" aria-label="Wiskas"
onclick="window.location.href='./wiskas/wiskas.html'">Wiskas</button>
</nav> </nav>
<script type="module" src="./app.js"></script> <script type="module" src="app.js"></script>
<script type="module" src="./script.js"></script>
<footer>
<div class="footer_grid">
<img class="ico_footer" src="./assets/facebook_logo.png">
<img class="ico_footer" src="./assets/insta_logo.png">
<img class="ico_footer" src="./assets/twitter_logo.png">
<a href="https://www.facebook.com/">MIAOUBOOK</a>
<a href="https://www.instagram.com/">INSTAMIAOU</a>
<a href="https://twitter.com/">BLUE-SNACK</a>
</div>
<a href="./mentions/mentions_legales.html" class="legal">LEGAL NOTICES</a>
</footer>
</body> </body>
</html> </html>
@@ -1,6 +1,6 @@
import { Window } from '../core/windows.js'; import { Window } from './windows.js';
import { API, STORAGE_KEYS, CSS } from '../core/config.js'; import { API, STORAGE_KEYS, CSS } from './config.js';
import { eventBus, Events } from '../core/events.js'; import { eventBus, Events } from './events.js';
/** /**
* Login and registration window * Login and registration window
@@ -83,8 +83,7 @@ export class LoginWindow extends Window {
bindEvents() { bindEvents() {
this.loginBtn.addEventListener('click', () => this.handleLogin()); this.loginBtn.addEventListener('click', () => this.handleLogin());
this.registerBtn.addEventListener('click', () => this.handleRegister()); this.registerBtn.addEventListener('click', () => this.handleRegister());
this.githubBtn.addEventListener('click', () => this.handleGitHubLogin());
this.githubBtn.addEventListener('click', () => {console.log(API.AUTH.GITHUB); this.handleGitHubLogin();});
// Login with Enter // Login with Enter
this.passwordInput.addEventListener('keypress', (e) => { this.passwordInput.addEventListener('keypress', (e) => {
@@ -130,7 +129,6 @@ export class LoginWindow extends Window {
if (response.ok && data.token) { if (response.ok && data.token) {
localStorage.setItem(STORAGE_KEYS.AUTH_TOKEN, data.token); localStorage.setItem(STORAGE_KEYS.AUTH_TOKEN, data.token);
this.showMessage('Login successful! Welcome.', 'success'); this.showMessage('Login successful! Welcome.', 'success');
this.showNotification('Login successful', 'green');
// Emit login event // Emit login event
eventBus.emit(Events.USER_LOGGED_IN, { username, token: data.token }); eventBus.emit(Events.USER_LOGGED_IN, { username, token: data.token });
@@ -140,7 +138,6 @@ export class LoginWindow extends Window {
} else { } else {
const errorMsg = data?.message || 'Login failed'; const errorMsg = data?.message || 'Login failed';
this.showMessage(errorMsg, 'error'); this.showMessage(errorMsg, 'error');
this.showNotification(errorMsg, 'red');
} }
} catch (error) { } catch (error) {
console.error('Login error:', error); console.error('Login error:', error);
@@ -173,12 +170,10 @@ export class LoginWindow extends Window {
if (response.ok) { if (response.ok) {
this.showMessage('Registration successful! You can now sign in.', 'success'); this.showMessage('Registration successful! You can now sign in.', 'success');
this.showNotification('Registration successful', 'green');
eventBus.emit(Events.USER_REGISTERED, { username }); eventBus.emit(Events.USER_REGISTERED, { username });
} else { } else {
const errorMsg = data?.message || 'Registration failed'; const errorMsg = data?.message || 'Registration failed';
this.showMessage(errorMsg, 'error'); this.showMessage(errorMsg, 'error');
this.showNotification(errorMsg, 'red');
} }
} catch (error) { } catch (error) {
console.error('Registration error:', error); console.error('Registration error:', error);
@@ -195,8 +190,6 @@ export class LoginWindow extends Window {
const left = (screen.width - width) / 2; const left = (screen.width - width) / 2;
const top = (screen.height - height) / 2; const top = (screen.height - height) / 2;
console.log(API.AUTH.GITHUB);
const popup = window.open( const popup = window.open(
API.AUTH.GITHUB, API.AUTH.GITHUB,
'githubOAuth', 'githubOAuth',
@@ -207,7 +200,6 @@ export class LoginWindow extends Window {
if (event.data?.token) { if (event.data?.token) {
localStorage.setItem(STORAGE_KEYS.AUTH_TOKEN, event.data.token); localStorage.setItem(STORAGE_KEYS.AUTH_TOKEN, event.data.token);
this.showMessage('GitHub login successful! Welcome.', 'success'); this.showMessage('GitHub login successful! Welcome.', 'success');
this.showNotification('GitHub login successful', 'green');
// Emit login event // Emit login event
eventBus.emit(Events.USER_LOGGED_IN, { eventBus.emit(Events.USER_LOGGED_IN, {
@@ -1,55 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Legal Notices</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #fff8e1;
color: #333;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
padding: 20px;
text-align: center;
}
.legal {
max-width: 600px;
font-size: 0.9rem;
line-height: 1.5;
border: 1px solid #ccc;
padding: 20px;
border-radius: 8px;
background-color: #fff;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.btn-home {
padding: 10px 20px;
background-color: #ffc75e;
color: #000;
border: none;
border-radius: 6px;
font-size: 1rem;
cursor: pointer;
text-decoration: none;
transition: all 0.3s ease;
}
.btn-home:hover {
background-color: #ffb347;
}
</style>
</head>
<body>
<div class="legal">
<strong>Legal Notices</strong><br><br>
All user credentials are securely stored using hashed passwords. We respect your privacy and do not share your personal information with third parties. By using this site, you agree to our data handling practices.
</div>
<a href="../index.html" class="btn-home">Back to Home</a>
</body>
</html>
+133
View File
@@ -0,0 +1,133 @@
// ─────────────────────────────────────────────
// RENDU
// ─────────────────────────────────────────────
const CELL = 30;
const COLORS = ['#000500','#00ff41','#39ff14','#00e676','#76ff03','#b2ff59','#00ffaa','#ccff00','#2d5a2d'];
const ctxMain = document.getElementById('canvas-main').getContext('2d');
const ctxNext = document.getElementById('canvas-next').getContext('2d');
const ctxHold = document.getElementById('canvas-hold').getContext('2d');
const ctxOpponent = document.getElementById('canvas-opponent').getContext('2d');
function drawCell(ctx, x, y, colorIndex, size) {
const p = 1;
const color = COLORS[colorIndex];
ctx.fillStyle = color;
ctx.fillRect(x * size + p, y * size + p, size - p * 2, size - p * 2);
// Glow inner
ctx.shadowColor = color;
ctx.shadowBlur = 6;
ctx.fillStyle = color;
ctx.fillRect(x * size + p + 2, y * size + p + 2, size - p * 2 - 4, size - p * 2 - 4);
ctx.shadowBlur = 0;
// Highlight top/left
ctx.fillStyle = 'rgba(200,255,200,0.2)';
ctx.fillRect(x * size + p, y * size + p, size - p * 2, 2);
ctx.fillRect(x * size + p, y * size + p, 2, size - p * 2);
// Shadow bottom/right
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(x * size + p, (y + 1) * size - p - 2, size - p * 2, 2);
ctx.fillRect((x + 1) * size - p - 2, y * size + p, 2, size - p * 2);
}
function clearCanvas(ctx, w, h) {
ctx.fillStyle = '#000500';
ctx.fillRect(0, 0, w, h);
}
function drawGridLines(ctx, cols, rows, size) {
ctx.strokeStyle = 'rgba(0,255,65,0.06)';
ctx.lineWidth = 1;
for (let x = 0; x <= cols; x++) {
ctx.beginPath(); ctx.moveTo(x * size, 0); ctx.lineTo(x * size, rows * size); ctx.stroke();
}
for (let y = 0; y <= rows; y++) {
ctx.beginPath(); ctx.moveTo(0, y * size); ctx.lineTo(cols * size, y * size); ctx.stroke();
}
}
function drawGhost(ctx, piece, grid) {
if (!piece) return;
const ghost = { x: piece.getPosition().x, y: piece.getPosition().y };
const shape = piece.getShape();
while (true) {
ghost.y++;
let valid = true;
for (let row = 0; row < shape.length && valid; row++)
for (let col = 0; col < shape[row].length && valid; col++)
if (shape[row][col] !== 0) {
const ny = ghost.y + row;
const nx = ghost.x + col;
if (ny < 0 || ny >= grid.length || nx < 0 || nx >= grid[ny].length || grid[ny][nx] !== 0) valid = false;
}
if (!valid) { ghost.y--; break; }
}
if (ghost.y === piece.getPosition().y) return;
ctx.strokeStyle = 'rgba(0,255,65,0.25)';
ctx.lineWidth = 1;
for (let row = 0; row < shape.length; row++)
for (let col = 0; col < shape[row].length; col++)
if (shape[row][col] !== 0)
ctx.strokeRect(
(ghost.x + col) * CELL + 2,
(ghost.y + row) * CELL + 2,
CELL - 4, CELL - 4
);
}
function drawMiniPiece(ctx, piece, canvasW, canvasH) {
clearCanvas(ctx, canvasW, canvasH);
if (!piece) return;
const shape = piece.getShape();
const color = piece.getColor();
const s = 20;
const offsetX = Math.floor((canvasW / s - shape[0].length) / 2);
const offsetY = Math.floor((canvasH / s - shape.length) / 2);
for (let row = 0; row < shape.length; row++)
for (let col = 0; col < shape[row].length; col++)
if (shape[row][col] !== 0)
drawCell(ctx, offsetX + col, offsetY + row, color, s);
}
function render() {
// Grille principale
clearCanvas(ctxMain, 300, 600);
drawGridLines(ctxMain, 10, 20, CELL);
for (let y = 0; y < game.grid.length; y++)
for (let x = 0; x < game.grid[y].length; x++)
if (game.grid[y][x] !== 0)
drawCell(ctxMain, x, y, game.grid[y][x], CELL);
// Ghost + pièce courante
if (game.currentPiece) {
drawGhost(ctxMain, game.currentPiece, game.grid);
const { x, y } = game.currentPiece.getPosition();
const shape = game.currentPiece.getShape();
const color = game.currentPiece.getColor();
for (let row = 0; row < shape.length; row++)
for (let col = 0; col < shape[row].length; col++)
if (shape[row][col] !== 0)
drawCell(ctxMain, x + col, y + row, color, CELL);
}
// Panneaux miniatures
drawMiniPiece(ctxNext, game.nextPiece, 100, 80);
drawMiniPiece(ctxHold, game.storedPiece, 100, 80);
// Score
document.getElementById('score-display').textContent = game.score;
}
function renderOpponent(opponentGrid) {
clearCanvas(ctxOpponent, 300, 600);
drawGridLines(ctxOpponent, 10, 20, CELL);
for (let y = 0; y < opponentGrid.length; y++)
for (let x = 0; x < opponentGrid[y].length; x++)
if (opponentGrid[y][x] !== 0)
drawCell(ctxOpponent, x, y, opponentGrid[y][x], CELL);
}
-57
View File
@@ -1,57 +0,0 @@
const container = document.querySelector('.container-gamelinks');
const buttons = document.querySelectorAll('.game-button');
function initButtons() {
const rect = container.getBoundingClientRect();
buttons.forEach(btn => {
// Ensure size is known
const bw = btn.offsetWidth;
const bh = btn.offsetHeight;
// Random start position INSIDE container
btn.x = Math.random() * (rect.width - bw);
btn.y = Math.random() * (rect.height - bh);
// Better velocity (avoid super slow)
btn.vx = (Math.random() * 2 + 1) * (Math.random() < 0.5 ? -1 : 1);
btn.vy = (Math.random() * 2 + 1) * (Math.random() < 0.5 ? -1 : 1);
btn.style.left = btn.x + 'px';
btn.style.top = btn.y + 'px';
});
}
function animateButtons() {
const rect = container.getBoundingClientRect();
buttons.forEach(btn => {
btn.x += btn.vx;
btn.y += btn.vy;
const bw = btn.offsetWidth;
const bh = btn.offsetHeight;
// Bounce inside container
if (btn.x <= 0 || btn.x + bw >= rect.width) {
btn.vx *= -1;
btn.x = Math.max(0, Math.min(btn.x, rect.width - bw)); // clamp
}
if (btn.y <= 0 || btn.y + bh >= rect.height) {
btn.vy *= -1;
btn.y = Math.max(0, Math.min(btn.y, rect.height - bh)); // clamp
}
btn.style.left = btn.x + 'px';
btn.style.top = btn.y + 'px';
});
requestAnimationFrame(animateButtons);
}
// 🔥 IMPORTANT: wait for layout to be ready
window.addEventListener('load', () => {
initButtons();
animateButtons();
});
@@ -1,5 +1,5 @@
import { Window } from '../core/windows.js'; import { Window } from './windows.js';
import { API, STORAGE_KEYS } from '../core/config.js'; import { API, STORAGE_KEYS } from './config.js';
/** /**
* Stats window displays Scribble + Tetris stats for any user * Stats window displays Scribble + Tetris stats for any user
+47
View File
@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dynamic Hand-Drawn Shapes</title>
<link rel="stylesheet" href="doodle.css">
<script src="doodle.js" defer></script>
</head>
<body>
<div class="shape doodle-1 loop-color" id="shape1"></div>
<div class="shape doodle-2 loop-color" id="shape2"></div>
<div class="shape doodle-3 loop-color" id="shape3"></div>
<div class="shape doodle-4 loop-color" id="shape4"></div>
<div class="shape doodle-5 loop-color" id="shape5"></div>
<div class="shape doodle-6 loop-color" id="shape6"></div>
<div class="shape doodle-7 loop-color" id="shape7"></div>
<div class="shape doodle-8 loop-color" id="shape8"></div>
<div class="shape doodle-9 loop-color" id="shape9"></div>
<div class="shape doodle-10 loop-color" id="shape10"></div>
<div class="shape doodle-11 loop-color" id="shape11"></div>
<div class="shape doodle-12 loop-color" id="shape12"></div>
<div class="shape doodle-13 loop-color" id="shape13"></div>
<div class="shape doodle-14 loop-color" id="shape14"></div>
<div class="shape doodle-15 loop-color" id="shape15"></div>
<div class="shape doodle-16 loop-color" id="shape16"></div>
<div class="shape doodle-17 loop-color" id="shape17"></div>
<div class="shape doodle-18 loop-color" id="shape18"></div>
<div class="shape doodle-19 loop-color" id="shape19"></div>
<div class="shape doodle-20 loop-color" id="shape20"></div>
<div class="shape doodle-21 loop-color" id="shape21"></div>
<div class="shape doodle-22 loop-color" id="shape22"></div>
<div class="shape doodle-23 loop-color" id="shape23"></div>
<div class="shape doodle-24 loop-color" id="shape24"></div>
<div class="shape doodle-25 loop-color" id="shape25"></div>
<div class="shape doodle-26 loop-color" id="shape26"></div>
<div class="shape doodle-27 loop-color" id="shape27"></div>
<div class="shape doodle-28 loop-color" id="shape28"></div>
<div class="shape doodle-29 loop-color" id="shape29"></div>
<div class="shape doodle-30 loop-color" id="shape30"></div>
<div class="shape doodle-31 loop-color" id="shape31"></div>
<div class="shape doodle-32 loop-color" id="shape32"></div>
<div class="shape doodle-33 loop-color" id="shape33"></div>
<div class="shape doodle-34 loop-color" id="shape34"></div>
<div class="shape doodle-35 loop-color" id="shape35"></div>
</body>
</html>
@@ -445,37 +445,6 @@ button:disabled { opacity: 0.3; cursor: not-allowed; }
letter-spacing: 0.05em; letter-spacing: 0.05em;
} }
/* ── Theme color picker ── */
.theme-btns {
display: flex;
gap: 6px;
margin-top: 2px;
}
.theme-btn {
width: 22px;
height: 22px;
min-width: 22px;
padding: 0;
border-radius: 50%;
border: 2px solid transparent;
cursor: pointer;
transition: transform 0.15s, box-shadow 0.15s;
}
.theme-btn[data-theme="green"] { background: #00ff41; }
.theme-btn[data-theme="red"] { background: #ff1744; }
.theme-btn[data-theme="yellow"] { background: #ffd600; }
.theme-btn[data-theme="blue"] { background: #00b0ff; }
.theme-btn:hover { transform: scale(1.2); }
.theme-btn.active {
border-color: #ffffff;
box-shadow: 0 0 8px currentColor;
transform: scale(1.15);
}
#settings-panel input[type="number"] { #settings-panel input[type="number"] {
background: var(--bg); background: var(--bg);
border: 1px solid var(--border); border: 1px solid var(--border);
@@ -651,36 +620,3 @@ button:disabled { opacity: 0.3; cursor: not-allowed; }
} }
body { overflow: hidden; } body { overflow: hidden; }
/* ── Shield ───────────────────────────────── */
.shield-bar-bg {
width: 100%;
height: 4px;
background: rgba(0,212,255,0.15);
border-radius: 2px;
margin-top: 4px;
overflow: hidden;
}
.shield-bar {
height: 100%;
background: #00d4ff;
border-radius: 2px;
transition: width 0.1s linear;
box-shadow: 0 0 6px #00d4ff;
}
.shield-ready { color: #00d4ff !important; }
.shield-active { color: #00ffff !important; text-shadow: 0 0 8px #00ffff; }
.shield-cooldown { color: var(--dim) !important; }
kbd {
display: inline-block;
padding: 0 3px;
border: 1px solid var(--border);
border-radius: 2px;
font-size: 0.6rem;
font-family: inherit;
color: var(--dim);
}
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tetris</title> <title>TETRIS</title>
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Orbitron:wght@400;700;900&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Orbitron:wght@400;700;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="tetris.css"> <link rel="stylesheet" href="tetris.css">
</head> </head>
@@ -15,9 +15,10 @@
<h1 data-text="TETRIS">TETRIS<span class="cursor">_</span></h1> <h1 data-text="TETRIS">TETRIS<span class="cursor">_</span></h1>
<a id="btn-home" href="../index.html">Home</a> <!-- Bouton home -->
<a id="btn-home" href="/">Home</a>
<!-- Panneau duel --> <!-- Panneau de connexion duel -->
<div id="duel-panel"> <div id="duel-panel">
<span class="settings-title">Duel</span> <span class="settings-title">Duel</span>
<div class="duel-row"> <div class="duel-row">
@@ -39,7 +40,7 @@
<div id="local-section"> <div id="local-section">
<div id="app"> <div id="app">
<!-- Colonne gauche : Hold + Score + Boutons + Paramètres --> <!-- Colonne gauche : Hold + Score + Boutons + Settings -->
<div id="left-column"> <div id="left-column">
<div class="panel"> <div class="panel">
<div class="panel-title">Hold</div> <div class="panel-title">Hold</div>
@@ -50,12 +51,6 @@
<div class="score-value" id="score-display">0</div> <div class="score-value" id="score-display">0</div>
</div> </div>
<div class="score-block">
<div class="score-label">Shield <kbd>E</kbd></div>
<div class="score-value shield-ready" id="shield-status-display">PRÊT</div>
<div class="shield-bar-bg"><div class="shield-bar" id="shield-bar"></div></div>
</div>
<div class="btn-group"> <div class="btn-group">
<button id="btn-start">Start</button> <button id="btn-start">Start</button>
<button id="btn-pause" disabled>Pause</button> <button id="btn-pause" disabled>Pause</button>
@@ -63,18 +58,9 @@
</div> </div>
</div> </div>
<!-- Paramètres --> <!-- Panneau de configuration -->
<div id="settings-panel"> <div id="settings-panel">
<div class="settings-title">Paramètres</div> <div class="settings-title">Paramètres</div>
<div class="settings-row">
<label>Couleur</label>
<div class="theme-btns">
<button class="theme-btn active" data-theme="green" title="Vert"></button>
<button class="theme-btn" data-theme="red" title="Rouge"></button>
<button class="theme-btn" data-theme="yellow" title="Jaune"></button>
<button class="theme-btn" data-theme="blue" title="Bleu"></button>
</div>
</div>
<div class="settings-row"> <div class="settings-row">
<label for="input-ttd">Vitesse initiale (ms)</label> <label for="input-ttd">Vitesse initiale (ms)</label>
<input type="number" id="input-ttd" min="100" max="3000" step="50" value="1000"> <input type="number" id="input-ttd" min="100" max="3000" step="50" value="1000">
@@ -111,7 +97,6 @@
<div><span>W</span> Rot. droite</div> <div><span>W</span> Rot. droite</div>
<div><span>Espace</span> Drop</div> <div><span>Espace</span> Drop</div>
<div><span>C</span> Hold</div> <div><span>C</span> Hold</div>
<div><span>E</span> Shield</div>
</div> </div>
</div> </div>
@@ -126,7 +111,6 @@
<div class="score-label">Score</div> <div class="score-label">Score</div>
<div class="score-value" id="opponent-score"></div> <div class="score-value" id="opponent-score"></div>
</div> </div>
<div id="opponent-shield-indicator" style="display:none;color:#00d4ff;font-size:0.75rem;text-align:center;letter-spacing:1px;margin-top:4px;">&#x1F6E1; SHIELD ACTIF</div>
</div> </div>
<div id="opponent-wrapper"> <div id="opponent-wrapper">
@@ -150,22 +134,34 @@
<div id="lb-scores" class="lb-content lb-content--active"> <div id="lb-scores" class="lb-content lb-content--active">
<table class="lb-table"> <table class="lb-table">
<thead><tr><th>#</th><th>Joueur</th><th>Meilleur score</th><th>Parties</th></tr></thead> <thead>
<tbody id="lb-scores-body"><tr><td colspan="4">Chargement…</td></tr></tbody> <tr><th>#</th><th>Joueur</th><th>Meilleur score</th><th>Parties</th></tr>
</thead>
<tbody id="lb-scores-body">
<tr><td colspan="4">Chargement…</td></tr>
</tbody>
</table> </table>
</div> </div>
<div id="lb-wins" class="lb-content"> <div id="lb-wins" class="lb-content">
<table class="lb-table"> <table class="lb-table">
<thead><tr><th>#</th><th>Joueur</th><th>Victoires</th><th>Parties</th></tr></thead> <thead>
<tbody id="lb-wins-body"><tr><td colspan="4">Chargement…</td></tr></tbody> <tr><th>#</th><th>Joueur</th><th>Victoires</th><th>Parties</th></tr>
</thead>
<tbody id="lb-wins-body">
<tr><td colspan="4">Chargement…</td></tr>
</tbody>
</table> </table>
</div> </div>
<div id="lb-history" class="lb-content"> <div id="lb-history" class="lb-content">
<table class="lb-table"> <table class="lb-table">
<thead><tr><th>#</th><th>Date</th><th>Type</th><th>Score</th><th>Résultat</th></tr></thead> <thead>
<tbody id="lb-history-body"><tr><td colspan="5">Chargement…</td></tr></tbody> <tr><th>#</th><th>Date</th><th>Type</th><th>Score</th><th>Résultat</th></tr>
</thead>
<tbody id="lb-history-body">
<tr><td colspan="5">Chargement…</td></tr>
</tbody>
</table> </table>
</div> </div>
</div> </div>
@@ -177,9 +173,59 @@
<script src="tetris.js"></script> <script src="tetris.js"></script>
<script src="renderer.js"></script> <script src="renderer.js"></script>
<script src="duel.js"></script> <script src="duel.js"></script>
<script src="leaderboard.js"></script>
<script src="ui.js"></script> <script src="ui.js"></script>
<script src="effects.js"></script>
<script>
// ── Responsive scaling ──────────────────────────
(function() {
const container = document.getElementById('scale-container');
// Dimensions naturelles du contenu (single-player)
const NAT_W = 640;
const NAT_H = 1020;
function resize() {
const s = Math.min(
window.innerWidth / NAT_W,
window.innerHeight / NAT_H
);
container.style.transform = 'scale(' + s + ')';
container.style.transformOrigin = 'top center';
// Compense l'espace de layout non affecté par transform
container.style.marginBottom = ((s - 1) * NAT_H) + 'px';
}
resize();
window.addEventListener('resize', resize);
})();
</script>
<script>
// ── Matrix rain ──────────────────────────────────
(function() {
const canvas = document.getElementById('matrix-bg');
const ctx = canvas.getContext('2d');
function resize() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; }
resize();
window.addEventListener('resize', resize);
const chars = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン0123456789ABCDEF>_{}[]|\\/#@$%^&*01';
const fs = 14;
let drops = [];
function initDrops() { drops = Array(Math.floor(canvas.width / fs)).fill(1); }
initDrops();
window.addEventListener('resize', initDrops);
setInterval(function() {
ctx.fillStyle = 'rgba(0,5,0,0.05)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.font = fs + 'px monospace';
for (let i = 0; i < drops.length; i++) {
const ch = chars[Math.floor(Math.random() * chars.length)];
ctx.fillStyle = drops[i] * fs < 50 ? '#aaffaa' : '#00ff41';
ctx.fillText(ch, i * fs, drops[i] * fs);
if (drops[i] * fs > canvas.height && Math.random() > 0.975) drops[i] = 0;
drops[i]++;
}
}, 40);
})();
</script>
</body> </body>
</html> </html>
@@ -3,12 +3,11 @@
// ─────────────────────────────────────────── // ───────────────────────────────────────────
class Tetris { class Tetris {
constructor(onRender, onGameOver, onBlockPlaced = null, onLinesCleared = null, onShieldChanged = null) { constructor(onRender, onGameOver, onBlockPlaced = null, onLinesCleared = null) {
this.onRender = onRender; this.onRender = onRender;
this.onGameOver = onGameOver; this.onGameOver = onGameOver;
this.onBlockPlaced = onBlockPlaced; this.onBlockPlaced = onBlockPlaced;
this.onLinesCleared = onLinesCleared; this.onLinesCleared = onLinesCleared;
this.onShieldChanged = onShieldChanged;
this.grid = this._createGrid(10, 20); this.grid = this._createGrid(10, 20);
this.bufferGrid = this._createGrid(10, 5); this.bufferGrid = this._createGrid(10, 5);
@@ -29,12 +28,6 @@ class Tetris {
this.isPaused = false; this.isPaused = false;
this.canStore = true; this.canStore = true;
// Shield
this.shieldActive = false;
this.shieldActiveMs = 0;
this.shieldCooldownMs = 0;
this.shieldReady = true; // prêt dès le début
this.animationFrameId = null; this.animationFrameId = null;
this.lastTime = 0; this.lastTime = 0;
this.accumulator = 0; this.accumulator = 0;
@@ -62,10 +55,6 @@ class Tetris {
this.timeToDown = this.initialTimeToDown; this.timeToDown = this.initialTimeToDown;
this.storedPiece = null; this.storedPiece = null;
this.canStore = true; this.canStore = true;
this.shieldActive = false;
this.shieldActiveMs = 0;
this.shieldCooldownMs = 0;
this.shieldReady = true;
this._spawnNewPiece(); this._spawnNewPiece();
document.addEventListener('keydown', this._keyHandler); document.addEventListener('keydown', this._keyHandler);
this._startGameLoop(); this._startGameLoop();
@@ -119,8 +108,6 @@ class Tetris {
this.lastTime = currentTime; this.lastTime = currentTime;
this.accumulator += deltaTime; this.accumulator += deltaTime;
this._updateShield(deltaTime);
while (this.isRunning && this.accumulator >= this.timeToDown) { while (this.isRunning && this.accumulator >= this.timeToDown) {
this._tick(); this._tick();
this.accumulator -= this.timeToDown; this.accumulator -= this.timeToDown;
@@ -187,42 +174,11 @@ class Tetris {
e.preventDefault(); e.preventDefault();
if (!this.isPaused) this._storePiece(); if (!this.isPaused) this._storePiece();
break; break;
case 'e': case 'E':
e.preventDefault();
if (!this.isPaused) this._activateShield();
break;
} }
this.onRender(); this.onRender();
} }
_activateShield() {
if (!this.shieldReady || this.shieldActive) return;
this.shieldActive = true;
this.shieldActiveMs = 3000;
this.shieldReady = false;
if (this.onShieldChanged) this.onShieldChanged('activated');
}
_updateShield(deltaTime) {
if (this.shieldActive) {
this.shieldActiveMs -= deltaTime;
if (this.shieldActiveMs <= 0) {
this.shieldActive = false;
this.shieldActiveMs = 0;
this.shieldCooldownMs = 60000;
if (this.onShieldChanged) this.onShieldChanged('deactivated');
}
} else if (!this.shieldReady) {
this.shieldCooldownMs -= deltaTime;
if (this.shieldCooldownMs <= 0) {
this.shieldCooldownMs = 0;
this.shieldReady = true;
if (this.onShieldChanged) this.onShieldChanged('ready');
}
}
}
_hardDrop() { _hardDrop() {
if (!this.currentPiece) return; if (!this.currentPiece) return;
let dist = 0; let dist = 0;
@@ -319,17 +275,8 @@ class Tetris {
const points = [0, 100, 300, 500, 800]; const points = [0, 100, 300, 500, 800];
this.score += points[cleared]; this.score += points[cleared];
this.count += points[cleared]; this.count += points[cleared];
if (cleared > 0) { if (this.onLinesCleared && cleared > 0)
// Chaque ligne remplie réduit le cooldown du shield de 10s this.onLinesCleared(cleared, this.lastLandingCol);
if (!this.shieldActive && !this.shieldReady) {
this.shieldCooldownMs = Math.max(0, this.shieldCooldownMs - cleared * 10000);
if (this.shieldCooldownMs === 0) {
this.shieldReady = true;
if (this.onShieldChanged) this.onShieldChanged('ready');
}
}
if (this.onLinesCleared) this.onLinesCleared(cleared, this.lastLandingCol);
}
} }
_makeHarder() { _makeHarder() {
@@ -414,7 +361,6 @@ class Tetris {
} }
addGarbageLines(lines) { addGarbageLines(lines) {
if (this.shieldActive) return; // shield bloque les lignes garbage
if (!this.isRunning || !lines.length) return; if (!this.isRunning || !lines.length) return;
this.grid.splice(0, lines.length); this.grid.splice(0, lines.length);
for (const line of lines) this.grid.push([...line]); // ...line pour faire une copie independante for (const line of lines) this.grid.push([...line]); // ...line pour faire une copie independante
-49
View File
@@ -1,49 +0,0 @@
// ─────────────────────────────────────────────
// EFFETS VISUELS : SCALING RESPONSIVE + MATRIX RAIN
// ─────────────────────────────────────────────
// ── Responsive scaling ──
(function() {
const container = document.getElementById('scale-container');
const NAT_W = 640;
const NAT_H = 1020;
function resize() {
const s = Math.min(window.innerWidth / NAT_W, window.innerHeight / NAT_H);
container.style.transform = 'scale(' + s + ')';
container.style.transformOrigin = 'top center';
container.style.marginBottom = ((s - 1) * NAT_H) + 'px';
}
resize();
window.addEventListener('resize', resize);
})();
// ── Matrix rain ──
(function() {
const canvas = document.getElementById('matrix-bg');
const ctx = canvas.getContext('2d');
const chars = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン0123456789ABCDEF>_{}[]|\\/#@$%^&*01';
const fs = 14;
let drops = [];
function resize() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; }
function initDrops() { drops = Array(Math.floor(canvas.width / fs)).fill(1); }
resize();
initDrops();
window.addEventListener('resize', () => { resize(); initDrops(); });
setInterval(function() {
ctx.fillStyle = 'rgba(0,5,0,0.05)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.font = fs + 'px monospace';
for (let i = 0; i < drops.length; i++) {
const ch = chars[Math.floor(Math.random() * chars.length)];
ctx.fillStyle = drops[i] * fs < 50 ? '#aaffaa' : '#00ff41';
ctx.fillText(ch, i * fs, drops[i] * fs);
if (drops[i] * fs > canvas.height && Math.random() > 0.975) drops[i] = 0;
drops[i]++;
}
}, 40);
})();
@@ -1,124 +0,0 @@
// ─────────────────────────────────────────────
// LEADERBOARDS & HISTORIQUE
// ─────────────────────────────────────────────
function escapeHtml(str) {
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
// ── Historique ───────────────────────────────
async function loadGameHistory() {
const token = localStorage.getItem('auth_token');
if (!token) return;
try {
const res = await fetch('/api/stats/tetris/history', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!res.ok) return;
renderGameHistory(await res.json());
} catch (err) {
console.error('Erreur chargement historique:', err);
}
}
function renderGameHistory(history) {
const tbody = document.getElementById('lb-history-body');
if (!tbody) return;
if (!history.length) {
tbody.innerHTML = '<tr><td colspan="5">Aucune partie jouée</td></tr>';
return;
}
tbody.innerHTML = history.map((entry, i) => {
const date = new Date(entry.played_at).toLocaleDateString('fr-FR', {
day: '2-digit', month: '2-digit', year: '2-digit',
hour: '2-digit', minute: '2-digit'
});
const type = entry.game_type === 'duel' ? 'Duel' : 'Solo';
let resultHtml = '—';
if (entry.result === 'win') resultHtml = '<span class="hist-win">Victoire</span>';
if (entry.result === 'loss') resultHtml = '<span class="hist-loss">Défaite</span>';
return `<tr>
<td>${i + 1}</td>
<td>${date}</td>
<td>${type}</td>
<td>${entry.score}</td>
<td>${resultHtml}</td>
</tr>`;
}).join('');
}
// ── Classements ──────────────────────────────
async function loadLeaderboards() {
const token = localStorage.getItem('auth_token');
if (!token) return;
const headers = { 'Authorization': `Bearer ${token}` };
try {
const [scoresRes, winsRes, meRes, rankScoreRes, rankWinsRes] = await Promise.all([
fetch('/api/stats/tetris/leaderboard/score', { headers }),
fetch('/api/stats/tetris/leaderboard/wins', { headers }),
fetch('/api/stats/me', { headers }),
fetch('/api/stats/tetris/rank/score', { headers }),
fetch('/api/stats/tetris/rank/wins', { headers })
]);
const me = meRes.ok ? await meRes.json() : null;
const rankScore = rankScoreRes.ok ? (await rankScoreRes.json()).rank : null;
const rankWins = rankWinsRes.ok ? (await rankWinsRes.json()).rank : null;
if (scoresRes.ok) renderLeaderboard('lb-scores-body', await scoresRes.json(), ['tetris_best_score', 'tetris_games_played'], me, rankScore);
if (winsRes.ok) renderLeaderboard('lb-wins-body', await winsRes.json(), ['tetris_wins', 'tetris_games_played'], me, rankWins);
} catch (err) {
console.error('Erreur chargement leaderboards:', err);
}
}
function renderLeaderboard(tbodyId, rows, [col1, col2], me, myRank) {
const tbody = document.getElementById(tbodyId);
if (!tbody) return;
if (!rows.length && !me) {
tbody.innerHTML = '<tr><td colspan="4">Aucun résultat</td></tr>';
return;
}
const myUsername = me?.username;
const inTop = rows.some(r => r.username === myUsername);
let html = rows.map((r, i) => {
const isMe = r.username === myUsername;
return `<tr class="${isMe ? 'lb-me' : ''}">
<td>${i + 1}</td>
<td>${escapeHtml(r.username)}${isMe ? ' <span class="lb-you">(vous)</span>' : ''}</td>
<td>${r[col1] ?? 0}</td>
<td>${r[col2] ?? 0}</td>
</tr>`;
}).join('');
if (!inTop && me && myRank !== null) {
html += `<tr class="lb-separator"><td colspan="4">· · ·</td></tr>`;
html += `<tr class="lb-me">
<td>${myRank}</td>
<td>${escapeHtml(myUsername)} <span class="lb-you">(vous)</span></td>
<td>${me[col1] ?? 0}</td>
<td>${me[col2] ?? 0}</td>
</tr>`;
}
tbody.innerHTML = html || '<tr><td colspan="4">Aucun résultat</td></tr>';
}
// ── Tabs ─────────────────────────────────────
document.querySelectorAll('.lb-tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.lb-tab').forEach(t => t.classList.remove('lb-tab--active'));
document.querySelectorAll('.lb-content').forEach(c => c.classList.remove('lb-content--active'));
tab.classList.add('lb-tab--active');
document.getElementById(`lb-${tab.dataset.tab}`).classList.add('lb-content--active');
if (tab.dataset.tab === 'history') loadGameHistory();
});
});
loadLeaderboards();
loadGameHistory();

Some files were not shown because too many files have changed in this diff Show More