19 Commits

Author SHA1 Message Date
gprunet c066fdc31c Round Timer + room creation synced with browse tab 2026-04-01 21:31:14 +02:00
Georges-Leonard Prunet c96629b704 Fix lobby player leaving 2026-03-31 15:59:02 +02:00
Georges-Leonard Prunet 41612f5d39 https + volume 2026-03-31 14:21:11 +02:00
kalips003 e1573ba9f0 ^^._, work in progress, small changes 2026-03-31 05:35:04 +02:00
kalips003 b9c4c817f8 ^^._, work in progress, small changes 2026-03-30 22:33:13 +02:00
kalips003 384363c8dd ^^._, work in progress, small changes 2026-03-30 17:19:14 +02:00
Kali Gallon def9918047 ^^._, work in progress, small changes 2026-03-30 01:36:33 +02:00
Kali Gallon cafa0cccc4 ^^._, work in progress, small changes 2026-03-27 23:17:31 +01:00
kalips003 8b907d5a86 transfer 2026-03-27 20:17:21 +01:00
H3XploR 13f93fb332 https installed 2026-03-24 14:29:04 +01:00
gprunet 801750da96 Notifs + Logout + delete Avatar 2026-03-22 18:26:50 +01:00
H3XploR 82623b0078 Merge pull request #21 from OlaketalAmigo/modular_code
cleaned
2026-03-22 13:48:56 +01:00
H3XploR d3e2d9bdf9 cleaned 2026-03-22 13:48:16 +01:00
H3XploR 9c1e8e03bb Merge pull request #20 from OlaketalAmigo/TETRIS
Tetris
2026-03-20 23:39:05 +01:00
Georges-Leonard Prunet 55c241fd61 notification for login/register 2026-03-20 17:57:17 +01:00
Georges-Leonard Prunet 592bb38c0d fixed next drawer 2026-03-20 17:29:06 +01:00
H3XploR 72bc9ea628 added shield 2026-03-19 14:38:56 +01:00
H3XploR 557cf23f71 reset before join 2026-03-19 14:14:20 +01:00
H3XploR b51b711b10 ajout de theme 2026-03-19 14:00:20 +01:00
203 changed files with 4153 additions and 1565 deletions
-10
View File
@@ -1,10 +0,0 @@
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
+3 -11
View File
@@ -1,24 +1,16 @@
all : all : up
@$(call random_shmol_cat, "hELLO", "nice human corrector", $(CLS), )
@docker compose -f ./docker-compose.yml up -d
no_cache : up :
@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 no_cache re : fclean up
@$(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 -5
View File
@@ -1,5 +1,5 @@
volumes: volumes:
data: pgdata:
networks: networks:
transcendence: transcendence:
@@ -12,7 +12,7 @@ services:
ports: ports:
- "5432:5432" - "5432:5432"
volumes: volumes:
- data:/var/lib/postgresql/data/pg15/ - pgdata:/var/lib/postgresql
env_file: env_file:
- ../.env - ../.env
networks: networks:
@@ -24,8 +24,6 @@ services:
build: ./srcs/backend build: ./srcs/backend
expose: expose:
- "3001" - "3001"
# ports:
# - "3001:3001"
depends_on: depends_on:
- database - database
volumes: volumes:
@@ -40,7 +38,7 @@ services:
container_name: frontend container_name: frontend
build: ./srcs/frontend/ build: ./srcs/frontend/
ports: ports:
- "8080:80" - "8443:443"
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 3, max_rounds INT DEFAULT 5,
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,5 +1,13 @@
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 ./
+7 -2
View File
@@ -1,5 +1,6 @@
import express from 'express'; import express from 'express';
import http from 'http'; import https from 'https';
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';
@@ -13,7 +14,11 @@ import setupSocketIO from './services/socket.js';
import avatarService from './services/avatar.js'; import avatarService from './services/avatar.js';
const app = express(); const app = express();
const server = http.createServer(app); const httpsOptions = {
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:
+11
View File
@@ -26,6 +26,17 @@ 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('/', authenticateToken, async(req, res) => router.delete('/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);
+25 -1
View File
@@ -2,6 +2,30 @@ 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
@@ -60,4 +84,4 @@ async function register(username, password)
} }
}; };
export default {register, login}; export default {register, login, logout};
@@ -69,6 +69,9 @@ 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);
+96 -2
View File
@@ -30,6 +30,63 @@ 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;
@@ -43,6 +100,7 @@ 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');
@@ -192,7 +250,9 @@ 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
}); });
} }
}); });
@@ -202,6 +262,15 @@ 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,
@@ -268,7 +337,8 @@ 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) {
@@ -390,6 +460,7 @@ 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('_');
@@ -552,6 +623,8 @@ 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,
@@ -613,6 +686,7 @@ function setupSocketIO(io)
// If the drawer left and there are still enough players, choose a new drawer // If the drawer left and there are still enough players, choose a new drawer
if (wasDrawer && gameState.players.length >= 1) if (wasDrawer && gameState.players.length >= 1)
{ {
stopRoomTimer(roomId);
// Pick the next player as the new drawer // Pick the next player as the new drawer
gameState.currentPlayerIndex = gameState.currentPlayerIndex % gameState.players.length; gameState.currentPlayerIndex = gameState.currentPlayerIndex % gameState.players.length;
const newDrawer = gameState.players[gameState.currentPlayerIndex]; const newDrawer = gameState.players[gameState.currentPlayerIndex];
@@ -632,6 +706,7 @@ function setupSocketIO(io)
reason: 'drawer_left', reason: 'drawer_left',
message: `${username} (dessinateur) a quitté, ${newDrawer} devient le nouveau dessinateur` message: `${username} (dessinateur) a quitté, ${newDrawer} devient le nouveau dessinateur`
}); });
startRoomTimer(io, roomId, 60);
} }
} }
@@ -652,6 +727,7 @@ 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;
@@ -730,6 +806,16 @@ 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;
@@ -857,6 +943,14 @@ 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,
+8 -1
View File
@@ -1,5 +1,12 @@
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"
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;"]
+15 -5
View File
@@ -1,5 +1,9 @@
server { server {
listen 80; listen 443 ssl;
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;
@@ -11,27 +15,33 @@ server {
# Backend API # Backend API
location /api/ { location /api/ {
proxy_pass http://backend:3001; proxy_pass https://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 http://backend:3001; proxy_pass https://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 $scheme; proxy_set_header X-Forwarded-Proto https;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
} }
location /avatar/ { location /avatar/ {
proxy_pass http://backend:3001/avatar/; proxy_pass https://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";
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

@@ -6,12 +6,14 @@
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',
@@ -82,6 +82,7 @@ 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',
@@ -228,6 +228,56 @@ export class Window {
return element; return element;
} }
NotficationContainer()
{
if (document.getElementById('notification-container')) return;
const container = this.createElement('div');
container.id = 'notification-container';
Object.assign(container.style, {
position: 'fixed',
top: '20px',
right: '20px',
zIndex: 1000,
display: 'flex',
flexDirection: 'column',
gap: '10px'
});
document.body.appendChild(container);
}
showNotification(message, color) {
this.NotficationContainer();
const container = document.getElementById('notification-container');
if (!container) return;
const notification = document.createElement('div');
notification.textContent = message;
Object.assign(notification.style, {
backgroundColor: color,
color: 'white',
padding: '10px 20px',
borderRadius: '5px',
boxShadow: '0 2px 6px rgba(0,0,0,0.3)',
opacity: '0',
transform: 'translateY(-8px)',
transition: 'opacity 0.5s ease, transform 0.5s ease'
});
container.appendChild(notification);
requestAnimationFrame(() => {
notification.style.opacity = '1';
notification.style.transform = 'translateY(0)';
});
setTimeout(() => {
notification.style.opacity = '0';
notification.style.transform = 'translateY(-8px)';
setTimeout(() => notification.remove(), 500);
}, 2200);
}
} }
// Export old class name for compatibility (alias) // Export old class name for compatibility (alias)
@@ -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-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) */
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,34 @@
<!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="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" />
</head>
<body>
<h1 class="title">Lobby</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>
<script type="module" src="../app.js"></script>
</body>
</html>
@@ -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-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) */
}
@@ -0,0 +1,113 @@
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();

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

@@ -120,7 +120,7 @@ body {
.title { .title {
position: absolute; position: absolute;
z-index: 999; z-index: 1;
top: 20px; top: 20px;
left: 50%; left: 50%;
translate: -50% 0; translate: -50% 0;
@@ -0,0 +1,43 @@
<!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>
<script type="module" src="../trans/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,79 +0,0 @@
<!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,47 +0,0 @@
<!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>
@@ -1,22 +0,0 @@
.dialog-bubble {
display: inline-block;
max-width: 70%;
padding: 10px 15px;
margin: 5px 0;
border-radius: 15px;
font-family: sans-serif;
font-size: 14px;
opacity: 0; /* start hidden for fade-in */
transition: opacity 0.3s ease, transform 0.3s ease;
}
.dialog-bubble.bot {
background-color: #e0e0e0;
align-self: flex-start;
}
.dialog-bubble.user {
background-color: #4caf50;
color: white;
align-self: flex-end;
}
@@ -1,102 +0,0 @@
export class DialogBubble {
constructor(toAttachTo, messageText) {
this.message = messageText; // the text to show
this.bubbleElement = null; // the actual DOM element for the bubble
this.sender = null; // "user" or "bot"
this.visible = false; // track visibility
this.toAttachTo = toAttachTo;
this.render();
}
// Create the bubble element in the DOM
render() {
// 1. Create <div> or <span> for the bubble
const bubble = document.createElement('div'); // could also use 'span'
this.bubbleElement = bubble; // store reference for later
bubble.classList.add('popup-chaberu');
bubble.textContent = this.message;
if (this.toAttachTo) {
this.toAttachTo.appendChild(bubble);
} else {
console.warn('No parent to attach bubble to');
}
}
// Animate text letter by letter
async typeText(speed = 50) {
if (!this.bubbleElement) return;
// 1️⃣ Show the bubble with a smooth fade-in
this.bubbleElement.style.opacity = '0';
this.bubbleElement.style.display = 'inline-block';
this.bubbleElement.style.transition = 'opacity 0.3s ease';
requestAnimationFrame(() => {
this.bubbleElement.style.opacity = '1';
});
// 2️⃣ Loop through message characters
this.bubbleElement.textContent = ''; // start empty
for (let i = 0; i < this.message.length; i++) {
this.bubbleElement.textContent += this.message[i];
// 3️⃣ Wait `speed` ms between letters
await new Promise(resolve => setTimeout(resolve, speed));
}
// 4️⃣ Optionally trigger a callback or just mark as done
// e.g., you could emit an event here
}
// Make the bubble visible with optional transition
show() {
if (!this.bubbleElement) return;
// Mark as visible
this.visible = true;
// Ensure its displayed (in case it was hidden)
this.bubbleElement.style.display = 'inline-block';
// Apply fade-in using opacity and transition
this.bubbleElement.style.opacity = '0'; // start hidden
this.bubbleElement.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
// Trigger the transition on the next frame
requestAnimationFrame(() => {
this.bubbleElement.style.opacity = '1';
this.bubbleElement.style.transform = 'translateY(0)'; // optional subtle move-in
});
}
// Fade out after a delay
hide(delay = 2000) {
if (!this.bubbleElement) return;
// Schedule the fade-out
setTimeout(() => {
// Start fade-out
this.bubbleElement.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
this.bubbleElement.style.opacity = '0';
this.bubbleElement.style.transform = 'translateY(-10px)'; // optional slight move up
// Optional: remove from DOM after fade-out finishes
setTimeout(() => {
if (this.bubbleElement && this.bubbleElement.parentNode) {
this.bubbleElement.parentNode.removeChild(this.bubbleElement);
}
}, 500); // match the fade duration
}, delay);
this.visible = false;
}
// Main function to “chat” this bubble
chat(speed = 50, fadeDelay = 2000) {
this.render(); // create bubble in DOM
this.show(); // fade-in
this.typeText(speed); // write text letter by letter
setTimeout(() => this.hide(fadeDelay), fadeDelay + this.message.length * speed);
}
}
@@ -1,21 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Donate Page</title>
<script src="js/script_testing.js"></script>
<link rel="stylesheet" type="text/css" href="css/donate.css">
</head>
<body>
<h1>Donate Page</h1>
<button onclick="sendHello()">Send Hello</button>
<br><br>
<input type="file" id="fileInput" multiple>
<button onclick="uploadFile()">Donate File</button>
<br><br>
<button onclick="uploadFiles()">Donate Lots of Files</button>
<br><br>
<button onclick="deleteFile()">Delete Test File</button>
<br><br>
<button onclick="callCgi()">Call CGI test</button>
</body>
</html>
@@ -1,105 +0,0 @@
:root {
/* custom CSS variables */
--color-dark: #0f172a;
--color-light: #f1f5f9;
--color-grey-1: #696969;
--color-accent: #e11d48;
--color-orange: rgb(218, 145, 12);
--font: 'Times New Roman', serif;
--font-size: 20px;
}
body {
background-color: black;
color: var( --color-grey-1);
font-family: var(--font);
font-size: var(--font-size);
line-height: 1.5;
text-align: center;
}
/**************************************************/
.button-3d {
display: inline-block;
padding: 10px 20px;
font-size: 16px;
font-weight: bold;
color: #8e8e8e;
background-color: #000000;
border: 3px solid #363636;
border-radius: 6px;
cursor: pointer;
user-select: none;
/* 3D illusion */
/* box-shadow: 0 6px #004999, 0 6px 0 #004999; /* deep shadow underneath */
transition: background-color 0.3s;
text-align: center;
text-decoration: none;
}
button-3d:hover {
background-color: rgb(202, 135, 10);
color: black;
}
button-3d:press {
color: var(--color-orange);
}
/**************************************************/
/* INDEX */
.header-1 {
display: flex;
justify-content: space-between;
text-align: justify;
align-items: center;
border: 2px solid rgb(218, 145, 12);
border-radius: 25px;
}
.button-play {
margin-left: 50px;
}
.button-login {
margin-right: 30px;
}
/*
.popup-chaberu {
position: fixed;
left: 75%;
transform: translateX(-50%);
padding: 20px;
z-index: 1000;
display: flex;
height: 400px;
width: 250px;
color: var(--color1);
font-weight: bold;
font-size: 25px;
justify-content: center;
align-items: center;
text-align: center;
} */
.popup-chaberu {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: red;
color: white;
font-size: 30px;
padding: 40px;
border-radius: 20px;
z-index: 9999;
}
@@ -1,38 +0,0 @@
import { DialogBubble } from './chaberu.js';
import { STORAGE_KEYS } from '../../js/config.js';
function chaberu(text) {
const wiskas = document.getElementById('wiskas');
const bubble = new DialogBubble(wiskas, text);
bubble.chat();
}
// THINGS TO CHANGE BASED ON LOGIN STATUS
const playButton = document.getElementById('play-button');
const title = document.getElementById('header-hello');
const loginButton = document.getElementById('login-button');
// <!--///////////////////////////////////////////////////////////////////////////////////////////-->
// looged in check
if (localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN)) {
// play button mapped to transcendnece
playButton.addEventListener('click', () => {
window.location.href = 'donate.html';
});
title.content = "hello ${user}";
loginButton = loggedmenu;
}
// <!--///////////////////////////////////////////////////////////////////////////////////////////-->
else {
playButton.addEventListener('click', () => {
chaberu('Please login before');
});
title.content = "Welcome to CAT !";
loginButton.addEventListener('click', () => {
try loggin();
});
}
// chaberu('Please log in before playing');
+157
View File
@@ -0,0 +1,157 @@
/* ///////////////////////////////////////////////////////// */
:root {
--custom-value: hello;
--app-background-base: radial-gradient(
circle at top,
#000000,
#4d4d4d
);
--app-background-image: url("./assets/background.png");
--num-value: 10px;
--black: #000000;
}
/* ///////////////////////////////////////////////////////// */
*, *::before, *::after {
}
body {
line-height: 1.5; /* inherited */
word-spacing: 1.4px; /* inherited */
font-size: 20px;
font-family: 'Times New Roman', serif; /* inherited */
color: var(--black); /* inherited */
text-align: center;
color: #696969;
margin: 0;
padding: 0;
background-color: var(--black);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.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;
}
/* ///////////////////////////////////////////////////////// */
.button-trans {
/* SIZE */
position: absolute;
left: 50%;
transform: translateX(-50%);
width: 500px;
height: 200px;
/* TEXT */
font-family: "Roboto";
font-size: 62px;
letter-spacing: -10px;
display: flex;
align-items: center;
justify-content: center;
/* Background */
background-image: url("./assets/background.png");
background-position: center;
background-repeat: no-repeat;
background-size: 150%;
/* Borders */
border-radius: 20px;
border-radius: 20px;
border: 5px solid transparent; /* keep space for the shadow */
background-clip: padding-box;
/* metallic effect */
box-shadow:
0 0 0 5px #c0c0c0 inset, /* inner shine */
0 0 0 2px rgba(255,255,255,0.3) inset; /* subtle highlight */
/* OTHER */
cursor: pointer;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.button-trans:hover {
transform: translateX(-50%) scale(1.02);
box-shadow:
0 0 20px 5px #fff inset,
0 0 20px 5px rgba(255,255,255,0.3) inset;
}
/* ///////////////////////////////////////////////////////// */
.button-test {
margin-right: auto;
margin-left: 20px;
}
/* ///////////////////////////////////////////////////////// */
.footer_div {
display: flex;
justify-content: space-around;
/* padding: 20px; */
/* margin-top: 80px;
margin-bottom: 100px; */
}
.ico_footer {
text-align: center;
width: 25px;
vertical-align: top;
/* padding-right: 5px; */
}
a {
text-decoration: none;
color: #5c5c5c;
}
a:hover {
color: rgb(218, 145, 12);
}
/* ///////////////////////////////////////////////////////// */
+54 -36
View File
@@ -1,36 +1,54 @@
<!doctype html> <!DOCTYPE html>
<html lang="fr"> <head>
<head> <link rel="stylesheet" href="./index.css" />
<meta charset="utf-8" /> <script type="module" src="./index.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1" /> </head>
<title>Transcendence</title> <body>
<link rel="stylesheet" href="index.css" />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <div id="header-1" class="container-1"
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> style="">
<link href="https://fonts.googleapis.com/css2?family=Cinzel+Decorative:wght@400;700&display=swap" rel="stylesheet" /> <div id="button-test" class="button-1 button-test multicolor" onclick="window.location.href = 'test/index.html';">TEST</div>
</head> <div id="button-trans" class="button-trans multicolor">TRANSCENDENCE</div>
<body> </div>
<h1 class="title">Transcendence</h1>
<img id="wiskas" style="margin: auto; display: block;" src="webcat/web_cat_img/wiskas-the-third.jpg">
<nav class="menu" aria-label="Menu principal">
<button class="menu__item" data-action="login" aria-label="Login">Login</button> <section style="display: flex;
<button class="menu__item" data-action="chat" aria-label="Global chat">Global chat</button> justify-content: center;
<button class="menu__item" data-action="avatar" aria-label="Avatar">Avatar</button> width: 1000px;
<button class="menu__item" data-action="friends" aria-label="Amis">Amis</button> margin: 0 auto;">
</nav> <p>I, am wiskas-the-third,
We are the cat company, we dont need to present our self for you already know
<nav class="game" aria-label="Game"> who we are, we created the internet, and we are still managing it now<br>
<button class="game__item" data-action="new_game" aria-label="Skkrrribl.io" We at CAT are the admin, creator, and workers of the internet
onclick="window.location.href='game.html'">Skkrrribl.io</button> Everytime a human goes to sleep, a cat start its shift, 1 billion pair of whiskers that are always here for you
<button class="game__item" data-action="tetris" aria-label="Tetris" Why? because we are philantropists, dont question it. Our goals are beyond your understanding
onclick="window.location.href='tetris.html'">Tetris</button> the internet was created by us, for us, and you should be glad we allow you to use it.
<button class="game__item" data-action="wiscat" aria-label="Wiscat" </p>
onclick="window.location.href='wiscat.html'">Wiscat</button> </section>
</nav>
<section style="display: flex;">
<script type="module" src="app.js"></script> <button style="margin-right: 50px;" class="button-1 multicolor" onclick="window.location.href = 'webcat/biblio.html';">
</body> Latest News</button><br>
</html> <button style="margin-left: 50px;" class="button-1 multicolor" onclick="window.location.href = 'webcat/staff/staff.html';">
meet the staff</button><br>
</section>
<footer>
<br><br><br>
<div class="footer_div" style="margin-top: 100px;">
<img class="ico_footer" src="webcat/web_cat_img/facebook_logo.png">
<img class="ico_footer" src="webcat/web_cat_img/insta_logo.png">
<img class="ico_footer" src="webcat/web_cat_img/twitter_logo.png">
</div>
<div class="footer_div" style="margin-bottom: 50px;">
<a href="https://www.facebook.com/">MIAOUBOOK</a>
<a href="https://www.instagram.com/">INSTAMIA</a>
<a href="https://twitter.com/">BLUE-SNACK</a>
</div>
<a href="./webcat/ml/mentions_legales.html">- LEGAL NOTICES -<br>(boring stuff, really, dont go look into this, i mean we are obligated to include it, but it will bore you, like, really)
<br>Dont do it! every seconds you spend in this next page, a kitten dies. so dont</a>
</footer>
</body>
</html>
+54
View File
@@ -0,0 +1,54 @@
import { updateElement } from "./test/tools.js";
import { colorizeText } from "./tools.js";
// //////////////////////////////////////////]
let div2 = document.createElement('div')
document.body.append(div2)
let button1 = document.createElement('button')
div2.append(button1)
button1.textContent = 'game-lobby'
button1.addEventListener('click', () => {
window.location.href = './game2/game.html';
})
let button2 = document.createElement('button')
div2.append(button2)
button2.textContent = 'tetris'
button2.addEventListener('click', () => {
window.location.href = './tetris/tetris.html';
})
let button4 = document.createElement('button')
div2.append(button4)
button4.textContent = 'test'
button4.addEventListener('click', () => {
window.location.href = './test/index.html';
})
let img = document.getElementById('wiskas');
img.before(div2)
// apply multicolor to .multicolor
colorizeText();
/* ///////////////////////////////////////////////////////// */
// make transcendence button move via: .button-trans
function updateButtonTranscendence(move) {
const btn = document.querySelector('.button-trans');
btn.addEventListener('mousemove', e => {
const rect = btn.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width - 0.5) * move;
const y = ((e.clientY - rect.top) / rect.height - 0.5) * move;
btn.style.backgroundPosition = `calc(50% + ${x}px) calc(50% + ${y}px)`;
});
btn.addEventListener('mouseleave', () => {
btn.style.backgroundPosition = 'center';
});
btn.addEventListener('click', () => {
window.location.href = './trans/index2.html';
});
}
/* ///////////////////////////////////////////////////////// */
updateButtonTranscendence(100);
@@ -1,117 +0,0 @@
// 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();
@@ -1,133 +0,0 @@
// ─────────────────────────────────────────────
// 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);
}
-406
View File
@@ -1,406 +0,0 @@
// ─────────────────────────────────────────────
// UI
// ─────────────────────────────────────────────
const btnStart = document.getElementById('btn-start');
const btnPause = document.getElementById('btn-pause');
const btnStop = document.getElementById('btn-stop');
const overlay = document.getElementById('overlay');
const inputTTD = document.getElementById('input-ttd');
const inputHardening = document.getElementById('input-hardening');
const inputDecrement = document.getElementById('input-decrement');
// Duel UI
const btnJoinDuel = document.getElementById('btn-join-duel');
const btnLeaveDuel = document.getElementById('btn-leave-duel');
const inputRoomCode = document.getElementById('input-room-code');
const duelStatusEl = document.getElementById('duel-status');
const opponentSection = document.getElementById('opponent-section');
// Matchmaking UI
const btnMatchmaking = document.getElementById('btn-matchmaking');
const btnMatchmakingCancel = document.getElementById('btn-matchmaking-cancel');
const matchmakingStatusEl = document.getElementById('matchmaking-status');
function updateButtons() {
btnStart.disabled = game.isRunning;
btnPause.disabled = !game.isRunning;
btnStop.disabled = !game.isRunning;
btnPause.textContent = game.isPaused ? 'Resume' : 'Pause';
inputTTD.disabled = game.isRunning;
inputHardening.disabled = game.isRunning;
inputDecrement.disabled = game.isRunning;
}
function showOverlay(title, score) {
document.getElementById('overlay-title').textContent = title;
document.getElementById('overlay-score').textContent = score !== undefined ? `Score : ${score}` : '';
overlay.classList.add('visible');
}
function hideOverlay() {
overlay.classList.remove('visible');
}
// ─────────────────────────────────────────────
// SOCKET + DUEL
// ─────────────────────────────────────────────
const socket = io({
auth: { token: localStorage.getItem('auth_token') },
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000,
transports: ['websocket', 'polling']
});
let duel = null;
function updateDuelStatus(status, opponentName) {
duelStatusEl.className = '';
if (status === 'waiting') {
duelStatusEl.textContent = 'En attente d\'un adversaire…';
duelStatusEl.classList.add('waiting');
opponentSection.classList.remove('visible');
} else if (status === 'ready') {
duelStatusEl.textContent = `Prêt — ${opponentName}`;
duelStatusEl.classList.add('ready');
opponentSection.classList.add('visible');
if (duel) duel.hideOpponentOverlay();
renderOpponent(duel ? duel.opponentGrid : Array.from({length:20}, () => Array(10).fill(0)));
} else {
duelStatusEl.textContent = '—';
opponentSection.classList.remove('visible');
}
}
function startLocalGame() {
hideOverlay();
game.start();
updateButtons();
render();
}
// ─────────────────────────────────────────────
// SCORE SAVE (solo)
// ─────────────────────────────────────────────
function saveTetrisScore(score) {
const token = localStorage.getItem('auth_token');
if (!token) return;
fetch('/api/stats/tetris/score', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ score })
})
.then(r => r.json())
.then(data => {
if (data.bestScore !== undefined) {
console.log('Meilleur score tetris:', data.bestScore);
}
})
.catch(err => console.error('Erreur sauvegarde score tetris:', err));
}
// ─────────────────────────────────────────────
// DUEL BUTTONS
// ─────────────────────────────────────────────
btnJoinDuel.addEventListener('click', () => {
const code = inputRoomCode.value.trim().toUpperCase();
if (!code) return;
if (duel) { duel.leave(); }
duel = new Duel(socket, game, updateDuelStatus, startLocalGame);
duel.join(code);
btnJoinDuel.disabled = true;
btnLeaveDuel.disabled = false;
inputRoomCode.disabled = true;
updateDuelStatus('waiting', null);
});
btnLeaveDuel.addEventListener('click', () => {
if (duel) { duel.leave(); duel = null; }
btnJoinDuel.disabled = false;
btnLeaveDuel.disabled = true;
inputRoomCode.disabled = false;
updateDuelStatus(null, null);
});
// ─────────────────────────────────────────────
// MATCHMAKING
// ─────────────────────────────────────────────
btnMatchmaking.addEventListener('click', () => {
socket.emit('tetris:matchmaking-join');
btnMatchmaking.disabled = true;
btnMatchmakingCancel.disabled = false;
btnJoinDuel.disabled = true;
matchmakingStatusEl.textContent = 'Recherche en cours…';
matchmakingStatusEl.className = 'waiting';
});
btnMatchmakingCancel.addEventListener('click', () => {
socket.emit('tetris:matchmaking-leave');
btnMatchmaking.disabled = false;
btnMatchmakingCancel.disabled = true;
btnJoinDuel.disabled = false;
matchmakingStatusEl.textContent = '';
});
socket.on('tetris:matchmaking-status', (data) => {
if (data.status === 'searching') {
matchmakingStatusEl.textContent = `Recherche… (${data.position} joueur(s) en attente)`;
} else if (data.status === 'idle') {
matchmakingStatusEl.textContent = '';
btnMatchmaking.disabled = false;
btnMatchmakingCancel.disabled = true;
btnJoinDuel.disabled = false;
}
});
socket.on('tetris:matched', (data) => {
matchmakingStatusEl.textContent = `Adversaire trouvé : ${data.opponent} !`;
matchmakingStatusEl.className = 'ready';
btnMatchmaking.disabled = false;
btnMatchmakingCancel.disabled = true;
btnJoinDuel.disabled = false;
// Auto-rejoindre la salle générée
if (duel) { duel.leave(); }
duel = new Duel(socket, game, updateDuelStatus, startLocalGame);
duel.join(data.roomCode);
inputRoomCode.value = data.roomCode;
btnJoinDuel.disabled = true;
btnLeaveDuel.disabled = false;
inputRoomCode.disabled = true;
updateDuelStatus('waiting', null);
});
// ─────────────────────────────────────────────
// INIT
// ─────────────────────────────────────────────
const game = new Tetris(
// onRender
() => {
if (duel) duel.synchronize_game();
render();
updateButtons();
},
// onGameOver
(score, validBlock) => {
const isDuel = duel && duel.isReady;
if (isDuel) {
duel.onLocalGameOver(score, validBlock);
} else {
saveTetrisScore(score);
}
render();
updateButtons();
showOverlay('GAME OVER', score);
loadLeaderboards();
loadGameHistory();
},
// onBlockPlaced — relay duel
(grid) => {
if (duel) duel.onLocalBlockPlaced(grid, game.score);
},
// onLinesCleared — relay duel
(count, holeCol) => {
if (duel) duel.onLocalLinesCleared(count, holeCol);
}
);
btnStart.addEventListener('click', () => {
if (duel && duel.isReady) {
duel.startDuel(); // déclenche les deux parties via le serveur
} else {
startLocalGame(); // solo
}
});
btnPause.addEventListener('click', () => {
if (duel && duel.isReady) {
duel.togglePause();
} else {
game.pause();
updateButtons();
if (game.isPaused) showOverlay('PAUSE');
else hideOverlay();
}
});
btnStop.addEventListener('click', () => {
if (duel && duel.isReady) {
duel.stop();
} else {
game.stop();
updateButtons();
render();
showOverlay('STOPPED');
}
});
function applySettings() {
const settings = {
timeToDown: parseInt(inputTTD.value, 10),
hardening: parseInt(inputHardening.value, 10),
decrementTTD: parseInt(inputDecrement.value, 10),
};
game.configure(settings);
if (duel && duel.isReady) duel.syncSettings(settings);
}
inputTTD.addEventListener('change', applySettings);
inputHardening.addEventListener('change', applySettings);
inputDecrement.addEventListener('change', applySettings);
const btnRestart = document.getElementById('btn-restart');
if (btnRestart) {
btnRestart.addEventListener('click', () => {
if (duel && duel.isReady) return;
game.restart();
updateButtons();
render();
});
}
// ─────────────────────────────────────────────
// GAME HISTORY
// ─────────────────────────────────────────────
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;
const history = await res.json();
renderGameHistory(history);
} 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('');
}
// ─────────────────────────────────────────────
// LEADERBOARDS
// ─────────────────────────────────────────────
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) {
const scores = await scoresRes.json();
renderLeaderboard('lb-scores-body', scores, ['tetris_best_score', 'tetris_games_played'], me, rankScore);
}
if (winsRes.ok) {
const wins = await winsRes.json();
renderLeaderboard('lb-wins-body', wins, ['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>';
}
function escapeHtml(str) {
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
// Tabs leaderboard
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();
});
});
// Chargement initial des leaderboards
loadLeaderboards();
loadGameHistory();
+188
View File
@@ -0,0 +1,188 @@
.test {/* =======================
🎨 COLORS & BACKGROUND
======================= */
color: red;
background-color: blue;
background-image: url(img.jpg);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
opacity: 0.5;
/* =======================
📏 SIZE & SPACING
======================= */
width: 200px;
height: 100px;
min-width: 100px;
max-width: 500px;
padding: 10px;
margin: 20px;
box-sizing: border-box;
/* shorthand */
margin: 10px 20px; /* top/bottom left/right */
padding: 10px 20px 5px 0; /* top right bottom left */
/* =======================
📍 POSITIONING
======================= */
position: static;
position: relative;
position: absolute;
position: fixed;
position: sticky;
top: 10px;
left: 20px;
right: 0;
bottom: 0;
z-index: 10;
/* =======================
📦 DISPLAY & LAYOUT
======================= */
display: block;
display: inline;
display: inline-block;
display: none;
display: flex; /* children can be controled with: justify-content (horizontal) / align-items (vertical) */
display: grid;
/* =======================
🔧 FLEXBOX
======================= */
display: flex;
flex-direction: row; /* row | column */
justify-content: center; /* main axis */
align-items: center; /* cross axis */
gap: 10px;
/* common */
justify-content: space-between;
justify-content: space-around;
justify-content: space-evenly;
/* =======================
🧱 GRID
======================= */
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: auto;
gap: 10px;
/* =======================
🔤 TEXT & FONT
======================= */
font-size: 16px;
font-weight: bold;
font-family: Arial, sans-serif;
text-align: center;
text-decoration: underline;
text-transform: uppercase;
line-height: 1.5;
letter-spacing: 2px;
/* =======================
🟦 BORDER & OUTLINE
======================= */
border: 1px solid black;
border-width: 2px;
border-style: dashed;
border-color: red;
border-radius: 10px;
outline: 2px solid blue;
/* =======================
👁️ VISIBILITY
======================= */
display: none;
visibility: hidden;
overflow: hidden;
overflow: scroll;
overflow: auto;
/* =======================
🎬 TRANSITIONS & EFFECTS
======================= */
transition: all 0.3s ease;
transform: translateX(50px);
transform: rotate(45deg);
transform: scale(1.1);
}
/* hover example */
:hover {
transform: scale(1.1);
}
/* =======================
🧠 SELECTORS
======================= */
/* basic */
div {} /* tag */
.class {} /* class */
#id {} /* id */
* {} /* all elements */
/* grouping */
div, p, span {} /* multiple selectors */
/* combinators */
div p {} /* any descendant */
div > p {} /* direct child */
div + p {} /* next sibling */
div ~ p {} /* all following siblings */
/* attribute selectors */
input[type="text"] {}
a[href] {}
button[class*="btn"] {} /* contains */
button[class^="btn"] {} /* starts with */
button[class$="btn"] {} /* ends with */
/* pseudo-classes (state) */
button:hover {}
input:focus {}
a:active {}
a:visited {}
input:checked {}
:nth-child(2) {}
:nth-child(odd) {}
:nth-child(even) {}
:not(.active) {}
/* pseudo-elements (virtual parts) */
::before {}
::after {}
::placeholder {}
::first-letter {}
::first-line {}
/* combined examples */
button.primary:hover {}
div#main.content {}
ul li:first-child {}
input:focus::placeholder {}
/* universal + pseudo */
*::before {}
*::after {}
/* =======================
⚡ SHORTHANDS
======================= */
.test2 {
background: red url(img.jpg) no-repeat center/cover;
border: 2px solid black;
font: bold 16px Arial;
margin: 10px 20px;
padding: 5px 10px;
}
+123
View File
@@ -0,0 +1,123 @@
// SIZE
box.style.width = "200px";
box.style.height = "100px";
box.style.minWidth = "100px";
box.style.maxWidth = "500px";
{
display: "flex" // flex | inline-flex | block | inline | none
justifyContent: "flex-start" // flex-start | flex-end | center | space-between | space-around | space-evenly
alignItems: "stretch" // stretch | flex-start | flex-end | center | baseline
}
// POSITION
box.style.position = "absolute";
box.style.top = "50px";
box.style.left = "100px";
box.style.right = "20px";
box.style.bottom = "10px";
box.style.zIndex = "10";
// SPACING
box.style.margin = "10px";
box.style.padding = "20px";
box.style.marginTop = "10px";
box.style.paddingLeft = "5px";
// BACKGROUND & COLORS
box.style.background = "red";
box.style.backgroundColor = "blue";
box.style.color = "white";
// BORDER
box.style.border = "2px solid black";
box.style.borderRadius = "10px";
// TEXT
box.style.fontSize = "20px";
box.style.fontWeight = "bold";
box.style.textAlign = "center";
// DISPLAY & VISIBILITY
box.style.display = "block";
box.style.visibility = "visible";
box.style.opacity = "0.5";
// TRANSFORM
box.style.transform = "translateX(100px)";
box.style.transform = "translate(50px, 20px)";
box.style.transform = "scale(1.5)";
box.style.transform = "rotate(45deg)";
box.style.transform = "translateX(100px) scale(2)";
// ANIMATION & TRANSITION
box.style.transition = "all 0.3s ease";
box.style.animation = "move 2s linear";
// INTERACTION
box.style.cursor = "pointer";
box.style.pointerEvents = "none";
// /////////////////////////////////////////////////////>
// /////////////////////////////////////////////////////>
// CONTENT
el.textContent = "Hello"; // plain text
el.innerHTML = "<b>Hello</b>"; // HTML content
el.innerText = "Hello"; // like textContent but respects line breaks
// ATTRIBUTES
el.id = "myDiv"; // element ID
el.className = "box highlight"; // full class string
el.classList.add("active"); // add a class
el.classList.remove("hidden"); // remove a class
el.classList.toggle("open"); // toggle a class
el.title = "Tooltip text"; // title attribute
el.value = "42"; // input value
el.src = "image.png"; // img, video, audio src
el.href = "https://example.com"; // anchor href
el.alt = "alternative text"; // img alt
// DOM STRUCTURE
el.appendChild(child); // add child
el.append(child1, child2); // add multiple children
el.prepend(child); // add at start
el.remove(); // remove self
el.replaceWith(newEl); // replace element
el.cloneNode(true); // copy element (deep if true)
// DATA & CUSTOM
el.dataset.id = "123"; // data-id attribute
el.dataset.name = "box1"; // data-name attribute
// EVENTS
el.onclick = () => {}; // direct event assignment
el.onmouseover = () => {};
el.addEventListener("click", () => {}); // preferred
el.removeEventListener("click", handler);
// VISIBILITY & FOCUS
el.hidden = true; // hides element
el.focus(); // focus element
el.blur(); // remove focus
el.tabIndex = 0; // make element focusable
// DIMENSIONS & POSITION (read-only or get)
el.clientWidth;
el.clientHeight;
el.offsetWidth;
el.offsetHeight;
el.offsetTop;
el.offsetLeft;
el.scrollWidth;
el.scrollHeight;
el.scrollTop;
el.scrollLeft;
// OTHER
el.checked = true; // checkbox / radio
el.selected = true; // option element
el.disabled = true; // input/button
el.readOnly = true; // input/textarea
el.name = "username"; // input / form element
el.type = "text"; // input type
@@ -0,0 +1,55 @@
import fetch from 'node-fetch';
import express, { response } from 'express';
import cors from 'cors';
const app = express();
const PORT = 3000//process.env.PORT || 3000;
app.use(express.json());
app.use(cors());
let token;
async function set_token()
{
fetch("https://api.intra.42.fr/oauth/token", {
method: "POST",
body: "grant_type=client_credentials&client_id=u-s4t2ud-c226cd35cd1ac08a4c6668deee1c64d7d67a13a766aee672acafd4a1522d483c&client_secret=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",
headers: {
"User-Agent": "agallon",
'Content-Type': 'application/x-www-form-urlencoded',}
})
.then(response => {
return response.json();
})
.then(data => {
token = data;
setTimeout(set_token, token.expires_in);
})
.catch(error => {
console.error('Error fetching token:', error);
});
}
set_token();
app.get('/proxy/profile/:login', async (req, res) => {
const { login } = req.params;
const profileURL = `https://api.intra.42.fr/v2/users/${login}`;
try {
const response = await fetch(profileURL, {
headers: {
"Authorization": `Bearer ${token.access_token}`}});
console.log(`response.status = ${response.status}`);
if (response.status !== 200) {
throw new Error('User not found');
}
const data = await response.json();
res.status(200).json(data);
} catch (error) {
console.error('Error fetching profile:', error);
res.status(500).json({ error: 'Failed to fetch profile' });
}
});
app.listen(PORT, () => {
console.log(`Proxy server running on port ${PORT}`);
});
@@ -0,0 +1,26 @@
import {checkIfLoggedIn} from './tools.js';
export class Header {
constructor() {
this.obj = document.createElement('div');
Object.assign(this.obj.style, {
});
let play = document.createElement('span');
let title = document.createElement('span');
let login = document.createElement('span');
play.textContent = "PLAY";
if (checkIfLoggedIn())
title.textContent = "Welcome back you!";
else
title.textContent = "Welcome to CAT !";
this.obj.append(play);
this.obj.append(title);
this.obj.append(login);
}
}
@@ -0,0 +1,44 @@
export class Popup {
constructor(msg, parent = document.body) {
this.msg = msg;
this.parent = parent;
this.obj = document.createElement('span');
this.obj.className = "popup";
this.obj.textContent = "";
this.obj.style.opacity = "0";
this.run();
}
async create() {
this.parent.appendChild(this.obj);
this.obj.style.transition = "opacity 0.5s ease";
requestAnimationFrame(() => {
this.obj.style.opacity = "1";
});
await new Promise(r => setTimeout(r, 500));
}
async write(speed = 50) {
for (let i = 0; i < this.msg.length; i++) {
this.obj.textContent += this.msg[i];
await new Promise(r => setTimeout(r, speed));
}
}
async remove() {
await new Promise(r => setTimeout(r, 2000));
this.obj.style.transition = "opacity 0.3s ease";
this.obj.style.opacity = "0";
await new Promise(r => setTimeout(r, 300));
if (this.obj.parentNode) {
this.obj.parentNode.removeChild(this.obj);
}
}
async run() {
await this.create();
await this.write();
await this.remove();
}
}
@@ -0,0 +1,9 @@
import { STORAGE_KEYS } from '../../core/config.js';
export function checkIfLoggedIn() {
const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
if (token) {
return true;
}
return false;
}
@@ -0,0 +1,43 @@
/* ////////////////////////////////////////// */
.box {
background: #142d4a;
height: 200px;
aspect-ratio: 1/1;
border-radius: 10px;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
@property --deg {
syntax: '<angle>';
inherits: true;
initial-value: 0deg;
}
.box::before,
.box::after {
content: "";
position: absolute;
height: 100%;
width: 100%;
background: conic-gradient(
from var(--deg) at center,
#00c3ff,
#4d0199,
#6300c6,
#009dcd
);
border-radius: inherit;
z-index: -2;
padding: 2px;
animation: autoRotate 2s linear infinite;
}
.box::after {
filter: blur(10px);
}
@keyframes autoRotate {
to{ --deg: 360deg; }
}
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<head>
<link rel="stylesheet" href="../game2/game.css" />
<link rel="stylesheet" href="./style.css" />
<script type="module" src="./script.js"></script>
</head>
<body style="background-color: black; display: flex; justify-content: center; align-items: center;">
<!--
<div class="container">
<div class="item item-1">Item 1</div>
<div class="item item-2">Item 2</div>
<div class="item item-3">Item 3</div>
</div> -->
<div></div>
<div class="box"></div>
</body>
</html>
@@ -0,0 +1,17 @@
// import { LoginSidebar } from "./loginSidebar.js";
import { Sidebar } from "./sidebar.js";
import { updateElement } from "./tools.js";
let b = updateElement({
classList: ['container2'],
additionalStyles: {
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center'
}
});
new Sidebar();
// new LoginSidebar();
// new Sidebar();
@@ -0,0 +1,93 @@
import { updateElement } from "./tools.js";
import { windowRegistry } from '../core/windows.js';
import { LoginWindow } from '../windows/login.js';
import { LogoutWindow } from '../windows/logout.js';
import { GlobalChat } from '../windows/global_chat.js';
import { AvatarWindow } from '../windows/avatar.js';
import { FriendsWindow } from '../windows/friends.js';
import { GameRoomWindow } from '../windows/game_room.js';
import { StatsWindow } from '../windows/stats.js';
export class Sidebar {
/* CONSTURCTOR */
constructor(parent = document.body) {
this.parent = parent;
this.stateopen = 'closed';
// this.state = this.checkIfLoggedIn() ? "loggedOut" : "loggedIn";
this.obj = updateElement({
parent: parent,
id: `login-wrapper`,
classList: [ 'login-wrapper' ],
})
this.createAllButtons();
this.handleClickOutside = (event) => {
if (this.stateopen === 'open' && !this.obj.contains(event.target)) {
this.toggle();
}
};
}
/* toogle menu open / closed */
toggle() {
this.stateopen = (this.stateopen === 'open') ? 'closed' : 'open';
console.log(this.stateopen);
if (this.stateopen === 'open') {
this.main_button.style.display = 'none';
this.menu_buttons.forEach(b => b.style.display = 'block');
// ensure only ONE listener exists
document.removeEventListener('click', this.handleClickOutside);
document.addEventListener('click', this.handleClickOutside);
}
else {
this.menu_buttons.forEach(b => b.style.display = 'none');
this.main_button.style.display = 'block';
document.removeEventListener('click', this.handleClickOutside);
}
}
/* create all element, append to div */
createAllButtons() {
// not-logged closed button
this.main_button = updateElement({
id: `button-main`,
parent: this.obj,
textContent: 'LOGIN',
classList: [ 'login-button' ],
})
this.obj.append(this.main_button);
this.main_button.addEventListener('click', (e) => {
e.stopPropagation();
this.toggle();
})
// menu buttons
const items = ['friends', 'chat', 'rooms', 'settings', 'log','logout'];
this.menu_buttons = [];
items.forEach(name => {
this[name] = updateElement({
id: `button-${name}`,
parent: this.obj,
textContent: name,
classList: ['login-button'],
additionalStyles: { display: 'none'}
})
this.menu_buttons.push(this[name]);
this.obj.append(this[name]);
})
this.loginWindow = new LoginWindow();
this.obj.append(this.loginWindow.form);
this.loginWindow.form.style.display = 'none';
this['log'].addEventListener('click', () => {
this.menu_buttons.forEach(b => b.style.display = 'none');
this.loginWindow.form.style.display = 'block';
})
// menu elements
}
}
@@ -0,0 +1,138 @@
/* BASE STYLES */
:root {
--clr-dark: #0f172a;
--clr-light: #f1f5f9;
--clr-accent: #e11d48;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
line-height: 1.6; /* inherited */
word-spacing: 1.4px; /* inherited */
font-family: "Roboto", sans-serif; /* inherited */
color: var(--clr-dark); /* inherited */
background-color: var(--clr-light);
/* display: flex; */
/* justify-content: center; */
/* align-items: center; */
height: 100vh;
position: relative;
}
.container {
width: 80%;
height: 700px;
margin: 0 auto;
border: 10px solid var(--clr-dark);
}
.item {
width: 150px;
height: 150px;
background-color: #fb7185;
padding: 1em;
font-weight: 700;
color: var(--clr-light);
text-align: center;
border: 10px solid var(--clr-accent);
border-radius: 10px;
margin-left: -50px
}
/* END OF BASE STYLES */
.item-1 {
font-size: 1.5rem;
}
.container {
display: flex;
}
.container2 {
margin: 0 auto;
border: 10px solid var(--clr-dark);
}
/*//////////////////////////////////////////////////////////*/
.button {
padding: 10px 18px;
font-size: 14px;
font-family: inherit;
color: white;
background-color: #3b82f6; /* blue */
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
}
.button:hover {
background-color: #2563eb;
transform: translateY(-1px);
}
.button:active {
transform: translateY(1px);
background-color: #1d4ed8;
}
.button:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.4);
}
/*//////////////////////////////////////////////////////////*/
.login-wrapper {
display: flex;
flex-direction: column;
gap: 7px;
background-color: #3b82f6; /* blue */
padding-right: 75px;
padding-bottom: 25px;
padding-top: 25px;
position: fixed;
top: 0;
right: 0;
}
.loggin-button {
position: relative;
display: inline-block;
border: 5px solid blue;
height: 35px;
min-width: 50px;
}
/*//////////////////////////////////////////////////////////*/
/* LOGIN */
.login-button {
width: 150px;
height: 150px;
background-color: #fb7185;
padding: 1em;
font-weight: 700;
color: var(--clr-light);
text-align: center;
border: 10px solid var(--clr-accent);
border-radius: 10px;
margin-left: -50px
}
.login-element {
}
@@ -0,0 +1,29 @@
export function updateElement({
el, // existing element or null to create new
parent = document.body,
id = null,
classList = [], // object like { css - classes to add }
textContent = "",
additionalStyles = {} // object like { color: 'red', display: 'flex' }
} = {}) {
// If no element passed, create a div by default
if (!el) {
el = document.createElement('div');
parent.appendChild(el);
}
// Set ID if provided
if (id) el.id = id;
// Manage classes
classList.forEach(cls => el.classList.add(cls));
// Set text content
if (textContent !== undefined) el.textContent = textContent;
// Apply additional styles
Object.assign(el.style, additionalStyles);
return el; // return element for further use
}
@@ -3,17 +3,20 @@
// ───────────────────────────────────────────── // ─────────────────────────────────────────────
class Duel { class Duel {
constructor(socket, tetrisGame, onStatusChange, onStart) { // ui : { showOverlay, hideOverlay, render, renderOpponent, updateButtons }
constructor(socket, tetrisGame, onStatusChange, onStart, ui) {
this.socket = socket; this.socket = socket;
this.tetrisGame = tetrisGame; this.tetrisGame = tetrisGame;
this.onStatusChange = onStatusChange; // (status, opponentName) => void this.onStatusChange = onStatusChange;
this.onStart = onStart; // () => void — déclenche le début du jeu local this.onStart = onStart;
this.ui = ui;
this.action_queue = []; this.action_queue = [];
this.opponentGrid = this._emptyGrid(); this.opponentGrid = this._emptyGrid();
this.opponentScore = 0; this.opponentScore = 0;
this.roomCode = null; this.opponentShieldActive = false;
this.isReady = false; this.roomCode = null;
this.isReady = false;
this._bindSocketEvents(); this._bindSocketEvents();
} }
@@ -33,10 +36,11 @@ 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 ──────────
@@ -48,9 +52,7 @@ class Duel {
onLocalLinesCleared(count, holeCol) { onLocalLinesCleared(count, holeCol) {
if (!this.isReady) return; if (!this.isReady) return;
const garbageLines = []; const garbageLines = Array.from({ length: count }, () => this._buildGarbageLine(holeCol));
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 });
} }
@@ -60,6 +62,12 @@ 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 = [];
@@ -70,8 +78,7 @@ class Duel {
synchronize_game() { synchronize_game() {
while (this.action_queue.length > 0) { while (this.action_queue.length > 0) {
const action = this.action_queue.shift(); this._processAction(this.action_queue.shift());
this._processAction(action);
} }
} }
@@ -81,7 +88,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;
renderOpponent(this.opponentGrid); this.ui.renderOpponent(this.opponentGrid, this.opponentShieldActive);
break; break;
case 'LINES_CLEARED': case 'LINES_CLEARED':
@@ -89,9 +96,17 @@ class Duel {
break; break;
case 'OPPONENT_GAME_OVER': case 'OPPONENT_GAME_OVER':
showOverlay('YOU WIN', action.score); this.ui.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;
} }
} }
@@ -127,28 +142,36 @@ 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();
updateButtons(); this.ui.updateButtons();
if (this.tetrisGame.isPaused) showOverlay('PAUSE'); if (this.tetrisGame.isPaused) this.ui.showOverlay('PAUSE');
else hideOverlay(); else this.ui.hideOverlay();
}); });
this.socket.on('tetris:stop', () => { this.socket.on('tetris:stop', () => {
this.tetrisGame.stop(); this.tetrisGame.stop();
updateButtons(); this.ui.updateButtons();
render(); this.ui.render();
showOverlay('STOPPED'); this.ui.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);
}); });
} }
+49
View File
@@ -0,0 +1,49 @@
// ─────────────────────────────────────────────
// 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);
})();
@@ -0,0 +1,124 @@
// ─────────────────────────────────────────────
// 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();
@@ -0,0 +1,228 @@
// ─────────────────────────────────────────────
// RENDU
// ─────────────────────────────────────────────
const CELL = 30;
const THEMES = {
green: {
bg: '#000500', panel: '#000d00', border: '#004400',
accent: '#00ff41', accent2: '#39ff14', dim: '#1a5c1a', text: '#00cc26',
grid: 'rgba(0,255,65,0.06)', ghost: 'rgba(0,255,65,0.25)', highlight: 'rgba(200,255,200,0.2)',
colors: ['#000500','#00ff41','#39ff14','#00e676','#76ff03','#b2ff59','#00ffaa','#ccff00','#2d5a2d']
},
red: {
bg: '#050000', panel: '#0d0000', border: '#440000',
accent: '#ff1744', accent2: '#ff4569', dim: '#5c1a1a', text: '#cc2626',
grid: 'rgba(255,23,68,0.06)', ghost: 'rgba(255,23,68,0.25)', highlight: 'rgba(255,200,200,0.2)',
colors: ['#050000','#ff1744','#ff4569','#e53935','#ff6d00','#ff8a65','#ff5252','#ff6e40','#5a2d2d']
},
yellow: {
bg: '#050500', panel: '#0d0d00', border: '#444400',
accent: '#ffd600', accent2: '#ffea00', dim: '#5c5c1a', text: '#ccaa00',
grid: 'rgba(255,214,0,0.06)', ghost: 'rgba(255,214,0,0.25)', highlight: 'rgba(255,255,200,0.2)',
colors: ['#050500','#ffd600','#ffea00','#ffab00','#fff176','#ffe57f','#ffff00','#ffc400','#5a5a2d']
},
blue: {
bg: '#000005', panel: '#00000d', border: '#000044',
accent: '#00b0ff', accent2: '#40c4ff', dim: '#1a1a5c', text: '#2626cc',
grid: 'rgba(0,176,255,0.06)', ghost: 'rgba(0,176,255,0.25)', highlight: 'rgba(200,200,255,0.2)',
colors: ['#000005','#00b0ff','#40c4ff','#0091ea','#448aff','#82b1ff','#00e5ff','#2979ff','#2d2d5a']
}
};
let currentTheme = THEMES.green;
let COLORS = [...currentTheme.colors];
function setColorTheme(themeName) {
currentTheme = THEMES[themeName] || THEMES.green;
COLORS = [...currentTheme.colors];
const root = document.documentElement;
root.style.setProperty('--bg', currentTheme.bg);
root.style.setProperty('--panel', currentTheme.panel);
root.style.setProperty('--border', currentTheme.border);
root.style.setProperty('--accent', currentTheme.accent);
root.style.setProperty('--accent2', currentTheme.accent2);
root.style.setProperty('--dim', currentTheme.dim);
root.style.setProperty('--text', currentTheme.text);
localStorage.setItem('tetris-theme', themeName);
document.querySelectorAll('.theme-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.theme === themeName);
});
}
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);
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;
ctx.fillStyle = currentTheme.highlight;
ctx.fillRect(x * size + p, y * size + p, size - p * 2, 2);
ctx.fillRect(x * size + p, y * size + p, 2, size - p * 2);
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 = currentTheme.bg;
ctx.fillRect(0, 0, w, h);
}
function drawGridLines(ctx, cols, rows, size) {
ctx.strokeStyle = currentTheme.grid;
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 = currentTheme.ghost;
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 _drawShieldOverlay(ctx, w, h, alpha) {
ctx.save();
ctx.strokeStyle = `rgba(0,212,255,${alpha})`;
ctx.lineWidth = 4;
ctx.shadowColor = '#00d4ff';
ctx.shadowBlur = 16;
ctx.strokeRect(2, 2, w - 4, h - 4);
ctx.shadowBlur = 0;
ctx.restore();
}
// ── Rendu joueur local ────────────────────────────────────────────────────────
// Prend l'objet game explicitement — aucun accès à des globaux externes.
function render(game) {
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);
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);
}
if (game.shieldActive) {
const pulse = 0.6 + 0.4 * Math.sin(Date.now() / 150);
_drawShieldOverlay(ctxMain, 300, 600, pulse);
}
drawMiniPiece(ctxNext, game.nextPiece, 100, 80);
drawMiniPiece(ctxHold, game.storedPiece, 100, 80);
document.getElementById('score-display').textContent = game.score;
const shieldEl = document.getElementById('shield-status-display');
const shieldBar = document.getElementById('shield-bar');
if (shieldEl) {
if (game.shieldActive) {
const secs = Math.ceil(game.shieldActiveMs / 1000);
shieldEl.textContent = `ACTIF ${secs}s`;
shieldEl.className = 'score-value shield-active';
if (shieldBar) shieldBar.style.width = (game.shieldActiveMs / 3000 * 100) + '%';
} else if (game.shieldReady) {
shieldEl.textContent = 'PRÊT';
shieldEl.className = 'score-value shield-ready';
if (shieldBar) shieldBar.style.width = '100%';
} else {
const secs = Math.ceil(game.shieldCooldownMs / 1000);
shieldEl.textContent = `${secs}s`;
shieldEl.className = 'score-value shield-cooldown';
if (shieldBar) shieldBar.style.width = ((1 - game.shieldCooldownMs / 60000) * 100) + '%';
}
}
}
// ── Rendu adversaire ─────────────────────────────────────────────────────────
// Prend grid et shieldActive explicitement — aucun accès à l'objet duel global.
function renderOpponent(grid, shieldActive) {
clearCanvas(ctxOpponent, 300, 600);
drawGridLines(ctxOpponent, 10, 20, CELL);
for (let y = 0; y < grid.length; y++)
for (let x = 0; x < grid[y].length; x++)
if (grid[y][x] !== 0)
drawCell(ctxOpponent, x, y, grid[y][x], CELL);
if (shieldActive) {
const pulse = 0.6 + 0.4 * Math.sin(Date.now() / 150);
_drawShieldOverlay(ctxOpponent, 300, 600, pulse);
}
const oppShieldEl = document.getElementById('opponent-shield-indicator');
if (oppShieldEl) oppShieldEl.style.display = shieldActive ? 'block' : 'none';
}
// Restaure le thème sauvegardé
(function() {
const saved = localStorage.getItem('tetris-theme');
if (saved && THEMES[saved]) setColorTheme(saved);
})();
@@ -445,6 +445,37 @@ 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);
@@ -620,3 +651,36 @@ 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);
}
@@ -15,10 +15,9 @@
<h1 data-text="TETRIS">TETRIS<span class="cursor">_</span></h1> <h1 data-text="TETRIS">TETRIS<span class="cursor">_</span></h1>
<!-- Bouton home --> <a id="btn-home" href="/">Home</a>
<a id="btn-home" href="/">Home</a>
<!-- Panneau de connexion duel --> <!-- Panneau 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">
@@ -40,7 +39,7 @@
<div id="local-section"> <div id="local-section">
<div id="app"> <div id="app">
<!-- Colonne gauche : Hold + Score + Boutons + Settings --> <!-- Colonne gauche : Hold + Score + Boutons + Paramètres -->
<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>
@@ -51,6 +50,12 @@
<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>
@@ -58,9 +63,18 @@
</div> </div>
</div> </div>
<!-- Panneau de configuration --> <!-- Paramètres -->
<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">
@@ -97,6 +111,7 @@
<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>
@@ -111,6 +126,7 @@
<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">
@@ -134,34 +150,22 @@
<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> <thead><tr><th>#</th><th>Joueur</th><th>Meilleur score</th><th>Parties</th></tr></thead>
<tr><th>#</th><th>Joueur</th><th>Meilleur score</th><th>Parties</th></tr> <tbody id="lb-scores-body"><tr><td colspan="4">Chargement…</td></tr></tbody>
</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> <thead><tr><th>#</th><th>Joueur</th><th>Victoires</th><th>Parties</th></tr></thead>
<tr><th>#</th><th>Joueur</th><th>Victoires</th><th>Parties</th></tr> <tbody id="lb-wins-body"><tr><td colspan="4">Chargement…</td></tr></tbody>
</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> <thead><tr><th>#</th><th>Date</th><th>Type</th><th>Score</th><th>Résultat</th></tr></thead>
<tr><th>#</th><th>Date</th><th>Type</th><th>Score</th><th>Résultat</th></tr> <tbody id="lb-history-body"><tr><td colspan="5">Chargement…</td></tr></tbody>
</thead>
<tbody id="lb-history-body">
<tr><td colspan="5">Chargement…</td></tr>
</tbody>
</table> </table>
</div> </div>
</div> </div>
@@ -173,59 +177,9 @@
<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,11 +3,12 @@
// ─────────────────────────────────────────── // ───────────────────────────────────────────
class Tetris { class Tetris {
constructor(onRender, onGameOver, onBlockPlaced = null, onLinesCleared = null) { constructor(onRender, onGameOver, onBlockPlaced = null, onLinesCleared = null, onShieldChanged = 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);
@@ -28,6 +29,12 @@ 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;
@@ -55,6 +62,10 @@ 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();
@@ -108,6 +119,8 @@ 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;
@@ -174,11 +187,42 @@ 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;
@@ -275,8 +319,17 @@ 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 (this.onLinesCleared && cleared > 0) if (cleared > 0) {
this.onLinesCleared(cleared, this.lastLandingCol); // Chaque ligne remplie réduit le cooldown du shield de 10s
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() {
@@ -361,6 +414,7 @@ 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
@@ -0,0 +1,265 @@
// ─────────────────────────────────────────────
// UI — Contrôles, socket, duel, matchmaking
// ─────────────────────────────────────────────
// ── Références DOM ───────────────────────────
const btnStart = document.getElementById('btn-start');
const btnPause = document.getElementById('btn-pause');
const btnStop = document.getElementById('btn-stop');
const btnRestart = document.getElementById('btn-restart');
const overlay = document.getElementById('overlay');
const inputTTD = document.getElementById('input-ttd');
const inputHardening = document.getElementById('input-hardening');
const inputDecrement = document.getElementById('input-decrement');
const btnJoinDuel = document.getElementById('btn-join-duel');
const btnLeaveDuel = document.getElementById('btn-leave-duel');
const inputRoomCode = document.getElementById('input-room-code');
const duelStatusEl = document.getElementById('duel-status');
const opponentSection = document.getElementById('opponent-section');
const btnMatchmaking = document.getElementById('btn-matchmaking');
const btnMatchmakingCancel = document.getElementById('btn-matchmaking-cancel');
const matchmakingStatusEl = document.getElementById('matchmaking-status');
// ── Overlay ──────────────────────────────────
function showOverlay(title, score) {
document.getElementById('overlay-title').textContent = title;
document.getElementById('overlay-score').textContent = score !== undefined ? `Score : ${score}` : '';
overlay.classList.add('visible');
}
function hideOverlay() {
overlay.classList.remove('visible');
}
// ── Boutons ──────────────────────────────────
function updateButtons() {
btnStart.disabled = game.isRunning;
btnPause.disabled = !game.isRunning;
btnStop.disabled = !game.isRunning;
btnPause.textContent = game.isPaused ? 'Resume' : 'Pause';
inputTTD.disabled = game.isRunning;
inputHardening.disabled = game.isRunning;
inputDecrement.disabled = game.isRunning;
}
// ── Socket ───────────────────────────────────
const socket = io({
auth: { token: localStorage.getItem('auth_token') },
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000,
transports: ['websocket', 'polling']
});
// ── Duel ─────────────────────────────────────
let duel = null;
// Callbacks passés au Duel pour qu'il pilote l'UI sans accéder à des globaux.
function _makeDuelUI() {
return {
showOverlay,
hideOverlay,
updateButtons,
render: () => render(game),
renderOpponent: (grid, shieldActive) => renderOpponent(grid, shieldActive),
};
}
function updateDuelStatus(status, opponentName) {
duelStatusEl.className = '';
if (status === 'waiting') {
duelStatusEl.textContent = "En attente d'un adversaire…";
duelStatusEl.classList.add('waiting');
opponentSection.classList.remove('visible');
} else if (status === 'ready') {
duelStatusEl.textContent = `Prêt — ${opponentName}`;
duelStatusEl.classList.add('ready');
opponentSection.classList.add('visible');
if (duel) duel.hideOpponentOverlay();
const grid = duel ? duel.opponentGrid : Array.from({ length: 20 }, () => Array(10).fill(0));
const shieldActive = duel ? duel.opponentShieldActive : false;
renderOpponent(grid, shieldActive);
} else {
duelStatusEl.textContent = '—';
opponentSection.classList.remove('visible');
}
}
function startLocalGame() {
hideOverlay();
game.start();
updateButtons();
render(game);
}
// Crée un Duel et rejoint la salle — mutualisé entre le bouton et le matchmaking.
function _joinDuelRoom(code) {
if (duel) duel.leave();
if (game.isRunning) { game.stop(); hideOverlay(); render(game); updateButtons(); }
duel = new Duel(socket, game, updateDuelStatus, startLocalGame, _makeDuelUI());
duel.join(code);
btnJoinDuel.disabled = true;
btnLeaveDuel.disabled = false;
inputRoomCode.disabled = true;
updateDuelStatus('waiting', null);
}
btnJoinDuel.addEventListener('click', () => {
const code = inputRoomCode.value.trim().toUpperCase();
if (!code) return;
_joinDuelRoom(code);
});
btnLeaveDuel.addEventListener('click', () => {
if (duel) { duel.leave(); duel = null; }
btnJoinDuel.disabled = false;
btnLeaveDuel.disabled = true;
inputRoomCode.disabled = false;
updateDuelStatus(null, null);
});
// ── Matchmaking ──────────────────────────────
btnMatchmaking.addEventListener('click', () => {
socket.emit('tetris:matchmaking-join');
btnMatchmaking.disabled = true;
btnMatchmakingCancel.disabled = false;
btnJoinDuel.disabled = true;
matchmakingStatusEl.textContent = 'Recherche en cours…';
matchmakingStatusEl.className = 'waiting';
});
btnMatchmakingCancel.addEventListener('click', () => {
socket.emit('tetris:matchmaking-leave');
btnMatchmaking.disabled = false;
btnMatchmakingCancel.disabled = true;
btnJoinDuel.disabled = false;
matchmakingStatusEl.textContent = '';
});
socket.on('tetris:matchmaking-status', (data) => {
if (data.status === 'searching') {
matchmakingStatusEl.textContent = `Recherche… (${data.position} joueur(s) en attente)`;
} else if (data.status === 'idle') {
matchmakingStatusEl.textContent = '';
btnMatchmaking.disabled = false;
btnMatchmakingCancel.disabled = true;
btnJoinDuel.disabled = false;
}
});
socket.on('tetris:matched', (data) => {
matchmakingStatusEl.textContent = `Adversaire trouvé : ${data.opponent} !`;
matchmakingStatusEl.className = 'ready';
btnMatchmaking.disabled = false;
btnMatchmakingCancel.disabled = true;
inputRoomCode.value = data.roomCode;
_joinDuelRoom(data.roomCode);
});
// ── Jeu ──────────────────────────────────────
function saveTetrisScore(score) {
const token = localStorage.getItem('auth_token');
if (!token) return;
fetch('/api/stats/tetris/score', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
body: JSON.stringify({ score })
})
.then(r => r.json())
.then(data => { if (data.bestScore !== undefined) console.log('Meilleur score tetris:', data.bestScore); })
.catch(err => console.error('Erreur sauvegarde score tetris:', err));
}
const game = new Tetris(
// onRender
() => {
if (duel) duel.synchronize_game();
render(game);
updateButtons();
},
// onGameOver
(score, validBlock) => {
if (duel && duel.isReady) duel.onLocalGameOver(score, validBlock);
else saveTetrisScore(score);
render(game);
updateButtons();
showOverlay('GAME OVER', score);
loadLeaderboards();
loadGameHistory();
},
// onBlockPlaced
(grid) => { if (duel) duel.onLocalBlockPlaced(grid, game.score); },
// onLinesCleared
(count, holeCol) => { if (duel) duel.onLocalLinesCleared(count, holeCol); },
// onShieldChanged
(event) => { if (duel) duel.onLocalShieldChanged(event); }
);
// ── Boutons de contrôle ──────────────────────
btnStart.addEventListener('click', () => {
if (duel && duel.isReady) duel.startDuel();
else startLocalGame();
});
btnPause.addEventListener('click', () => {
if (duel && duel.isReady) {
duel.togglePause();
} else {
game.pause();
updateButtons();
if (game.isPaused) showOverlay('PAUSE');
else hideOverlay();
}
});
btnStop.addEventListener('click', () => {
if (duel && duel.isReady) {
duel.stop();
} else {
game.stop();
updateButtons();
render(game);
showOverlay('STOPPED');
}
});
if (btnRestart) {
btnRestart.addEventListener('click', () => {
if (duel && duel.isReady) return;
game.restart();
updateButtons();
render(game);
});
}
// ── Paramètres ───────────────────────────────
function applySettings() {
const settings = {
timeToDown: parseInt(inputTTD.value, 10),
hardening: parseInt(inputHardening.value, 10),
decrementTTD: parseInt(inputDecrement.value, 10),
};
game.configure(settings);
if (duel && duel.isReady) duel.syncSettings(settings);
}
inputTTD.addEventListener('change', applySettings);
inputHardening.addEventListener('change', applySettings);
inputDecrement.addEventListener('change', applySettings);
// ── Thème ────────────────────────────────────
document.querySelectorAll('.theme-btn').forEach(btn => {
btn.addEventListener('click', () => setColorTheme(btn.dataset.theme));
});
+32
View File
@@ -0,0 +1,32 @@
// render in color the text of all .multicolor
export function colorizeText() {
const elements = document.querySelectorAll(".multicolor");
const colorizeText = (el) => {
const text = el.textContent;
el.innerHTML = "";
const baseHue = Math.random() * 360;
// 🎲 random step = makes rainbow "scrambled"
const step = (Math.random() * 60) + 10; // 10 → 70
// 🎲 random direction (left or right rainbow)
const direction = Math.random() < 0.5 ? 1 : -1;
[...text].forEach((char, i) => {
const span = document.createElement("span");
span.textContent = char;
const hue = baseHue + (i * step * direction);
span.style.color = `hsl(${hue}, 90%, 60%)`;
span.style.textShadow = `1px 1px 0 rgba(0,0,0,0.3)`;
el.appendChild(span);
});
};
elements.forEach(colorizeText);
}
@@ -2,13 +2,14 @@
* Application entry point * Application entry point
* Initializes windows and handles menu interactions * Initializes windows and handles menu interactions
*/ */
import { windowRegistry } from './windows.js'; import { windowRegistry } from '../core/windows.js';
import { LoginWindow } from './login.js'; import { LoginWindow } from '../windows/login.js';
import { GlobalChat } from './global_chat.js'; import { LogoutWindow } from '../windows/logout.js';
import { AvatarWindow } from './avatar.js'; import { GlobalChat } from '../windows/global_chat.js';
import { FriendsWindow } from './friends.js'; import { AvatarWindow } from '../windows/avatar.js';
import { GameRoomWindow } from './game_room.js'; import { FriendsWindow } from '../windows/friends.js';
import { StatsWindow } from './stats.js'; import { GameRoomWindow } from '../windows/game_room.js';
import { StatsWindow } from '../windows/stats.js';
/** /**
* Main application class * Main application class
@@ -16,7 +17,6 @@ import { StatsWindow } from './stats.js';
*/ */
class App { class App {
constructor() { constructor() {
console.log("APP STARTED");
this.initWindows(); this.initWindows();
this.initMenu(); this.initMenu();
this.initPage(); this.initPage();
@@ -34,6 +34,7 @@ class App {
new FriendsWindow(); new FriendsWindow();
new GameRoomWindow(); new GameRoomWindow();
new StatsWindow(); new StatsWindow();
new LogoutWindow();
} }
/** /**
@@ -51,7 +52,8 @@ 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
@@ -76,10 +78,6 @@ 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');
@@ -87,9 +85,14 @@ class App {
const action = button.dataset.action; const action = button.dataset.action;
// Actions with associated windows if (action === 'gameroom') {
if (actionMap[action]) { const gameRoomWindow = windowRegistry.get('gameroom');
windowRegistry.toggle(actionMap[action]); windowRegistry.toggle('gameroom');
gameRoomWindow.loadRooms();
if (gameRoomWindow?.currentTab === 'browse') {
gameRoomWindow.loadRooms();
}
return; return;
} }
@@ -110,34 +113,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);
} }
} }
@@ -147,4 +150,4 @@ if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => new App()); document.addEventListener('DOMContentLoaded', () => new App());
} else { } else {
new App(); new App();
} }
@@ -23,7 +23,7 @@
#ff8080 #ff8080
); );
--app-background-image: url("./assets/background.png"); --app-background-image: url("../assets/background.png");
--color-surface: #ffefce; --color-surface: #ffefce;
--color-surface-light: #ffc75e; --color-surface-light: #ffc75e;
@@ -409,11 +409,6 @@ body {
============================================ */ ============================================ */
.login { .login {
width: 320px; width: 320px;
border-radius: 5px;
border-color: #aa1f1f;
border: 6px solid #faac37;
background: #ffffff;
color: #000;
} }
.login__form { .login__form {
@@ -625,6 +620,30 @@ body {
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
============================================ */ ============================================ */
@@ -670,7 +689,7 @@ body {
.friends__tab { .friends__tab {
flex: 1; flex: 1;
padding: var(--spacing-sm); padding: var(--spacing-sm);
background: var(--color-surface-light); background: var(--color-surface);
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;
@@ -0,0 +1,41 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Transcendence</title>
<link rel="stylesheet" href="index.css" />
<link rel="stylesheet" href="style.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" />
</head>
<body>
<h1 class="title">Transcendence</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>
<button class="menu__item" data-action="logout" aria-label="Logout">Logout</button>
</nav>
<nav class="game" aria-label="Game">
<button class="game__item" data-action="new_game" aria-label="Skkrrribl.io"
onclick="window.location.href='../game2/game.html'">Skkrrribl.io</button>
<button class="game__item" data-action="tetris" aria-label="Tetris"
onclick="window.location.href='../tetris/tetris.html'">Tetris</button>
<button class="game__item" data-action="wiscat" aria-label="Wiscat"
onclick="window.location.href='../wiscat/wiscat.html'">Wiscat</button>
</nav>
<div class="container-gamelinks">
<button class="game-button link-game" onclick="window.location.href='../game2/game.html';">Skkrrribl.io</button>
<button class="game-button link-tetris" onclick="window.location.href='../tetris/tetris.html';">Tetris</button>
<button class="game-button link-wiscat" onclick="window.location.href='../wiscat/wiscat.html';">Wiscat</button>
</div>
<script type="module" src="app.js"></script>
<script type="module" src="script.js"></script>
</body>
</html>
@@ -0,0 +1,57 @@
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();
});

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