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
211 changed files with 5408 additions and 1838 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 :
@$(call random_shmol_cat, "hELLO", "nice human corrector", $(CLS), )
@docker compose -f ./docker-compose.yml up -d
all : up
no_cache :
@docker compose -f ./docker-compose.yml build --no-cache
up :
@docker compose -f ./docker-compose.yml up -d
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
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 system prune -af --volumes
re : fclean no_cache
@$(call print_cat, $(CLEAR), $(C_120), $(C_300), $(C_210), $(call pad_word, 10, "Re-Doing"), $(call pad_word, 12, "Miaster"));
.PHONY : all no_cache clean fclean re
re : fclean up
+3 -5
View File
@@ -1,5 +1,5 @@
volumes:
data:
pgdata:
networks:
transcendence:
@@ -12,7 +12,7 @@ services:
ports:
- "5432:5432"
volumes:
- data:/var/lib/postgresql/data/pg15/
- pgdata:/var/lib/postgresql
env_file:
- ../.env
networks:
@@ -24,8 +24,6 @@ services:
build: ./srcs/backend
expose:
- "3001"
# ports:
# - "3001:3001"
depends_on:
- database
volumes:
@@ -40,7 +38,7 @@ services:
container_name: frontend
build: ./srcs/frontend/
ports:
- "8080:80"
- "8443:443"
depends_on:
- backend
networks:
+1 -1
View File
@@ -127,7 +127,7 @@ async function createTables()
status VARCHAR(20) DEFAULT 'waiting',
max_players INT DEFAULT 8,
current_round INT DEFAULT 0,
max_rounds INT DEFAULT 3,
max_rounds INT DEFAULT 5,
round_duration INT DEFAULT 90,
created_at TIMESTAMP DEFAULT NOW(),
started_at TIMESTAMP,
+8
View File
@@ -1,5 +1,13 @@
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
COPY package*.json ./
+7 -2
View File
@@ -1,5 +1,6 @@
import express from 'express';
import http from 'http';
import https from 'https';
import fs from 'fs';
import cors from 'cors';
import {Server} from 'socket.io';
import authRouter from './routes/auth.js';
@@ -13,7 +14,11 @@ import setupSocketIO from './services/socket.js';
import avatarService from './services/avatar.js';
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,
{
cors:
+11
View File
@@ -26,6 +26,17 @@ router.post('/login', async(req, res) =>
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) => {
const githubAuthUrl = `https://github.com/login/oauth/authorize?` +
`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);
});
router.delete('/', authenticateToken, async(req, res) =>
router.delete('/delete', authenticateToken, async(req, res) =>
{
const result = await avatarService.deleteAvatar(req.user.userId);
res.status(result.status).json(result.data);
+25 -1
View File
@@ -2,6 +2,30 @@ import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
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)
{
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)
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
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
async function checkAndStopSinglePlayerGame(io, roomId, dbRoomId) {
if (!dbRoomId) return;
@@ -43,6 +100,7 @@ async function checkAndStopSinglePlayerGame(io, roomId, dbRoomId) {
const players = await gameRoomService.getRoomPlayers(dbRoomId);
if (players.length <= 1) {
console.log(`Room ${dbRoomId} has only ${players.length} player(s) left, ending game`);
stopRoomTimer(roomId);
// Update room status to 'ended'
await gameRoomService.updateRoomStatus(dbRoomId, 'waiting');
@@ -192,7 +250,9 @@ function setupSocketIO(io)
revealedLetters: gameState.revealedLetters,
revealedWord: gameState.revealedWord || [],
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) {
const roomId = socket.gameRoomId;
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', {
username: socket.user.username,
@@ -268,7 +337,8 @@ function setupSocketIO(io)
revealedWord: gameState.revealedWord || [],
guessedLetters: gameState.guessedLetters,
players: gameState.players,
scores: gameState.scores || {}
scores: gameState.scores || {},
timer: gameState.timerSeconds || 0
});
}
} catch (err) {
@@ -390,6 +460,7 @@ function setupSocketIO(io)
const gameState = gameRooms.get(roomId);
if (!gameState) return;
startRoomTimer(io, roomId, 60);
gameState.currentWord = data.word.toLowerCase();
gameState.revealedLetters = new Array(data.word.length).fill(false);
gameState.revealedWord = new Array(data.word.length).fill('_');
@@ -552,6 +623,8 @@ function setupSocketIO(io)
// Update round start scores for next round
gameState.roundStartScores = { ...gameState.scores };
stopRoomTimer(roomId);
io.to(roomId).emit('game-word-found', {
word: gameState.currentWord,
winner: username,
@@ -613,6 +686,7 @@ function setupSocketIO(io)
// If the drawer left and there are still enough players, choose a new drawer
if (wasDrawer && gameState.players.length >= 1)
{
stopRoomTimer(roomId);
// Pick the next player as the new drawer
gameState.currentPlayerIndex = gameState.currentPlayerIndex % gameState.players.length;
const newDrawer = gameState.players[gameState.currentPlayerIndex];
@@ -632,6 +706,7 @@ function setupSocketIO(io)
reason: 'drawer_left',
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 () => {
const roomId = socket.gameRoomId;
if (!roomId) return;
stopRoomTimer(roomId);
// Update room status to 'waiting' in database
const dbRoomId = socket.gameRoomDbId;
@@ -730,6 +806,16 @@ function setupSocketIO(io)
_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)
socket.on('tetris:start-duel', () => {
const code = socket.tetrisRoomCode;
@@ -857,6 +943,14 @@ function setupSocketIO(io)
}
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
socket.to(roomId).emit('game-player-left', {
username: socket.user.username,
+8 -1
View File
@@ -1,5 +1,12 @@
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 nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
EXPOSE 443
CMD ["nginx", "-g", "daemon off;"]
+15 -5
View File
@@ -1,5 +1,9 @@
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;
index index.html;
@@ -11,27 +15,33 @@ server {
# Backend 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 X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto https;
}
# Socket.IO WebSocket proxying
location /socket.io/ {
proxy_pass http://backend:3001;
proxy_pass https://backend:3001;
proxy_ssl_verify off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
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/ {
proxy_pass http://backend:3001/avatar/;
proxy_pass https://backend:3001/avatar/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_ssl_verify off;
proxy_hide_header Content-Type;
add_header Cache-Control "public, max-age=3600";
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

@@ -6,12 +6,14 @@
export const API = {
AUTH: {
LOGIN: '/api/auth/login',
LOGOUT: '/api/auth/logout',
REGISTER: '/api/auth/register',
GITHUB: '/api/auth/github'
},
AVATAR: {
GET: '/api/avatar/me',
UPLOAD: '/api/avatar/upload'
UPLOAD: '/api/avatar/upload',
DELETE: '/api/avatar/delete'
},
FRIENDS: {
LIST: '/api/friends',
@@ -82,6 +82,7 @@ export const Events = {
// Avatar
AVATAR_UPDATED: 'avatar:updated',
AVATAR_DELETED: 'avatar:deleted',
// Chat
CHAT_CONNECTED: 'chat:connected',
@@ -228,6 +228,56 @@ export class Window {
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)
@@ -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) */
}
-117
View File
@@ -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();
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

-79
View File
@@ -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>
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 {
position: absolute;
z-index: 999;
z-index: 1;
top: 20px;
left: 50%;
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>
+119 -714
View File
@@ -1,752 +1,157 @@
/* ============================================
TRANSCENDENCE - Main Stylesheet
Convention: BEM (Block__Element--Modifier)
============================================ */
/* ============================================
CSS VARIABLES
============================================ */
/* ///////////////////////////////////////////////////////// */
:root {
--color-primary: #ffc75e;
--color-primary-hover: #ffc75e;
--color-success: #3cff01;
--color-success-dark: #ffc75e;
--color-error: #ff4d4d;
--color-warning: #ffc75e;
--color-github: #ffc75e;
--color-bg: #ffe5b5;
--custom-value: hello;
--app-background-base: radial-gradient(
circle at top,
#fff787,
#ff8080
#000000,
#4d4d4d
);
--app-background-image: url("./assets/background.png");
--color-surface: #ffefce;
--color-surface-light: #ffc75e;
--color-text: #000000;
--color-text-muted: #000000;
--font-size-base: 10px;
--font-size-sm: 1.2rem;
--font-size-md: 1.4rem;
--font-size-lg: 1.6rem;
--font-size-xl: 3rem;
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 12px;
--spacing-lg: 16px;
--spacing-xl: 24px;
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 12px;
--radius-full: 50%;
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.5);
--shadow-lg: 0 8px 16px rgba(0, 0, 0, 0.5);
--transition-fast: 150ms ease;
--transition-normal: 250ms ease;
--z-menu: 2;
--z-window: 100;
--z-modal: 200;
--num-value: 10px;
--black: #000000;
}
/* ============================================
RESET & BASE
============================================ */
*,
*::before,
*::after {
box-sizing: border-box;
}
/* ///////////////////////////////////////////////////////// */
*, *::before, *::after {
html {
height: 100%;
background-image:
var(--app-background-image),
var(--app-background-base);
background-size:
contain,
cover;
background-position:
center,
center;
background-repeat:
no-repeat,
no-repeat;
}
body {
margin: 0;
width: 70%;
min-width: 800px;
margin: 0 auto;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
color: var(--color-text);
line-height: 1.5;
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;
}
/* ============================================
TYPOGRAPHY
============================================ */
.title {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
text-transform: uppercase;
.container-1 {
display: flex;
justify-content: center;
align-items: center;
display: flex;
align-items: center;
justify-content: center;
gap: 20px;
font-size: var(--font-size-xl);
text-align: center;
text-shadow: 2px 2px 10px black;
z-index: 1;
font-family: "Roboto";
letter-spacing: -10px;
color: rgba(248, 252, 2, 0.6);
margin: 0;
padding: 0.6rem 1.2rem;
background-color: #ffefce;
border: 2px solid rgba(0, 0, 0, 0.6);
border-radius: var(--radius-lg);
width: 100%;
margin: 5px;
position: relative;
min-height: 200px;
}
/* ///////////////////////////////////////////////////////// */
/* ============================================
MENU
============================================ */
.menu {
position: fixed;
top: var(--spacing-lg);
left: 50px;
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
z-index: var(--z-menu);
.button {
color: red;
margin: 5px 50px;
padding: 5px 50px;
}
.menu__item {
background: var(--color-surface);
color: var(--color-text);
border: 1px solid var(--color-surface-light);
border-radius: var(--radius-lg);
border-color: #000;
padding: var(--spacing-sm) var(--spacing-md);
font-size: var(--font-size-md);
cursor: pointer;
transition: all var(--transition-fast);
text-align: center;
}
.menu__item:hover {
background: var(--color-surface-light);
font-size: var(--font-size-lg);
}
.menu__item--active {
background: var(--color-primary);
border-color: var(--color-primary);
}
/* ============================================
GAME
============================================ */
/* .game {
position: fixed;
top: 0;
right: 50px;
padding: 0;
margin: 0;
z-index: var(--z-menu);
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
} */
.game {
position: fixed;
top: var(--spacing-lg);
right: 50px;
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
z-index: var(--z-menu);
}
.game__item {
background: var(--color-surface);
color: var(--color-text);
border: 1px solid var(--color-surface-light);
border-radius: var(--radius-lg);
border-color: #000;
padding: var(--spacing-sm) var(--spacing-md);
font-size: var(--font-size-md);
cursor: pointer;
transition: all var(--transition-fast);
text-align: center;
}
.game__item:hover {
background: var(--color-surface-light);
font-size: var(--font-size-lg);
}
.game__item--active {
background: var(--color-primary);
border-color: var(--color-primary);
}
/* ============================================
BUTTONS
============================================ */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: var(--spacing-sm) var(--spacing-md);
font-size: var(--font-size-md);
font-weight: 500;
border: none;
border-radius: var(--radius-md);
cursor: pointer;
transition: all var(--transition-fast);
text-decoration: none;
}
.btn:hover {
opacity: 0.9;
transform: translateY(-1px);
}
.btn:active {
transform: translateY(0);
}
.btn--primary {
background: var(--color-primary);
color: var(--color-text);
}
.btn--primary:hover {
background: var(--color-primary-hover);
}
.btn--secondary {
background: var(--color-surface-light);
color: var(--color-text);
}
.btn--success {
background: var(--color-success-dark);
color: var(--color-text);
}
.btn--danger {
background: var(--color-error);
color: var(--color-text);
}
.btn--github {
background: var(--color-github);
color: var(--color-text);
}
.btn--ghost {
background: transparent;
color: var(--color-text);
border: 1px solid var(--color-surface-light);
}
/* ============================================
INPUTS
============================================ */
.input {
width: 100%;
padding: var(--spacing-sm) var(--spacing-md);
font-size: var(--font-size-md);
background: var(--color-surface);
color: var(--color-text);
border: 1px solid var(--color-surface-light);
border-radius: var(--radius-md);
transition: border-color var(--transition-fast);
}
.input:focus {
outline: none;
border-color: var(--color-primary);
}
.input::placeholder {
color: var(--color-text-muted);
}
.input-group {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
/* ============================================
WINDOWS
============================================ */
.window {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--color-bg);
color: var(--color-text);
z-index: var(--z-window);
display: none;
flex-direction: column;
min-width: 280px;
box-shadow: var(--shadow-lg);
border-radius: 5px;
border-color: #aa1f1f;
border: 6px solid #faac37;
}
.window--visible {
display: flex;
}
.window--left {
left: 25%;
}
.window--right {
left: 75%;
}
.window__header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-sm) var(--spacing-md);
background: var(--color-surface);
cursor: move;
user-select: none;
}
.window__title {
font-weight: 500;
font-size: var(--font-size-md);
}
.window__close {
cursor: pointer;
font-size: var(--font-size-lg);
opacity: 0.8;
transition: opacity var(--transition-fast);
background: none;
border: none;
color: var(--color-text);
padding: 0;
line-height: 1;
}
.window__close:hover {
opacity: 1;
}
.window__body {
padding: var(--spacing-md);
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
flex: 1;
overflow: auto;
}
/* ============================================
MESSAGES
============================================ */
.message {
font-size: var(--font-size-sm);
padding: var(--spacing-xs);
border-radius: var(--radius-lg);
border-color: #000;
}
.message--success {
color: var(--color-success);
}
.message--error {
color: var(--color-error);
}
.message--info {
color: var(--color-text-muted);
}
/* ============================================
LOGIN WINDOW
============================================ */
.login {
width: 320px;
border-radius: 5px;
border-color: #aa1f1f;
border: 6px solid #faac37;
background: #ffffff;
color: #000;
}
.login__form {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.login__actions {
display: flex;
gap: var(--spacing-sm);
margin-top: var(--spacing-xs);
}
.login__divider {
display: flex;
align-items: center;
gap: var(--spacing-sm);
color: var(--color-text-muted);
font-size: var(--font-size-sm);
margin: var(--spacing-sm) 0;
}
.login__divider::before,
.login__divider::after {
content: '';
flex: 1;
height: 1px;
background: var(--color-surface-light);
}
/* ============================================
CHAT WINDOW
============================================ */
.chat {
width: 380px;
height: 400px;
}
.chat__output {
flex: 1;
overflow-y: auto;
padding: var(--spacing-sm);
background: var(--color-surface);
border-radius: var(--radius-md);
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
min-height: 150px;
}
.chat__message {
padding: var(--spacing-xs) var(--spacing-sm);
background: var(--color-surface-light);
border-radius: var(--radius-sm);
font-size: var(--font-size-sm);
}
.chat__message--own {
background: var(--color-primary);
align-self: flex-end;
}
.chat__friend-indicator {
.button-1 {
display: inline-block;
width: 8px;
height: 8px;
background-color: var(--color-success);
border-radius: 50%;
margin-right: var(--spacing-xs);
vertical-align: middle;
}
.chat__system {
color: var(--color-text-muted);
font-size: var(--font-size-sm);
font-style: italic;
padding: 10px 20px;
background-color: #000000;
color: #8e8e8e;
text-align: center;
}
text-decoration: none;
.chat__system--error {
color: var(--color-error);
}
.chat__system--success {
color: var(--color-success);
}
.chat__input-container {
display: flex;
gap: var(--spacing-sm);
margin-top: var(--spacing-sm);
}
.chat__input {
flex: 1;
}
.chat__controls {
display: flex;
gap: var(--spacing-sm);
margin-top: var(--spacing-sm);
}
/* ============================================
AVATAR WINDOW
============================================ */
.avatar-window {
width: 360px;
}
.avatar__preview {
width: 120px;
height: 120px;
object-fit: cover;
border-radius: var(--radius-full);
border: 3px solid var(--color-text);
box-shadow: var(--shadow-md);
background: var(--color-surface);
align-self: center;
}
.avatar__username {
font-size: var(--font-size-lg);
font-weight: 600;
text-align: center;
color: var(--color-text);
margin-top: var(--spacing-sm);
}
.avatar__controls {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
align-items: center;
}
.avatar__file-input {
display: none;
}
/* ============================================
STATS WINDOW
============================================ */
.stats-window {
width: 320px;
}
.stats__avatar {
width: 72px;
height: 72px;
object-fit: cover;
border-radius: var(--radius-full);
border: 2px solid var(--color-text);
align-self: center;
display: block;
margin: 0 auto var(--spacing-xs);
}
.stats__username {
font-size: var(--font-size-lg);
font-weight: 600;
text-align: center;
color: #000;
margin-bottom: var(--spacing-md);
}
.stats__section {
margin-bottom: var(--spacing-md);
}
.stats__section-title {
font-size: var(--font-size-sm);
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--color-primary);
border-bottom: 1px solid var(--color-surface-light);
padding-bottom: var(--spacing-xs);
margin-bottom: var(--spacing-xs);
}
.stats__section-body {
display: flex;
flex-direction: column;
gap: 4px;
}
.stats__row {
display: flex;
justify-content: space-between;
font-size: var(--font-size-sm);
padding: 3px 0;
}
.stats__label {
color: #333;
}
.stats__value {
font-weight: 600;
color: #000;
}
.stats__loading {
font-size: var(--font-size-sm);
color: #333;
text-align: center;
padding: var(--spacing-sm) 0;
}
/* ============================================
UTILITIES
============================================ */
.hidden {
display: none !important;
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
.text-center {
text-align: center;
}
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
/* ============================================
FRIENDS WINDOW
============================================ */
.friends-window {
width: 400px;
height: 450px;
}
.friends__tabs {
display: flex;
gap: var(--spacing-xs);
margin-bottom: var(--spacing-sm);
}
.friends__tab {
flex: 1;
padding: var(--spacing-sm);
background: var(--color-surface-light);
border: 1px solid var(--color-surface-light);
color: var(--color-text);
font-size: 16px;
cursor: pointer;
font-size: var(--font-size-sm);
transition: all var(--transition-fast);
border: 3px solid #363636;
border-radius: 6px;
transition: background-color 0.3s;
}
.button-1:hover {
background-color: rgb(202, 135, 10);
color: black;
}
.friends__tab:hover {
background: var(--color-surface-light);
/* ///////////////////////////////////////////////////////// */
.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;
}
.friends__tab--active {
background: var(--color-primary);
border-color: var(--color-primary);
.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;
}
.friends__content {
/* ///////////////////////////////////////////////////////// */
.button-test {
margin-right: auto;
margin-left: 20px;
}
/* ///////////////////////////////////////////////////////// */
.footer_div {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
justify-content: space-around;
/* padding: 20px; */
/* margin-top: 80px;
margin-bottom: 100px; */
}
.friends__search {
display: flex;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-sm);
}
.friends__search .input {
flex: 1;
}
.friends__list {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.friends__item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm);
background: var(--color-surface);
border-radius: var(--radius-md);
}
.friends__avatar {
width: 40px;
height: 40px;
border-radius: var(--radius-full);
object-fit: cover;
border: 2px solid var(--color-surface-light);
}
.friends__name {
flex: 1;
font-size: var(--font-size-md);
font-weight: 500;
}
.friends__actions {
display: flex;
gap: var(--spacing-xs);
}
.friends__actions .btn {
padding: var(--spacing-xs) var(--spacing-sm);
font-size: var(--font-size-sm);
}
.friends__empty {
.ico_footer {
text-align: center;
color: var(--color-text-muted);
padding: var(--spacing-lg);
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>
<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="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="test" aria-label="Test Page"
onclick="window.location.href='test.html'">Test Page</button>
</nav>
<nav class="game" aria-label="Game">
<button class="game__item" data-action="new_game" aria-label="Skkrrribl.io"
onclick="window.location.href='game.html'">Skkrrribl.io</button>
<button class="game__item" data-action="tetris" aria-label="Tetris"
onclick="window.location.href='tetris.html'">Tetris</button>
</nav>
<script type="module" src="app.js"></script>
</body>
</html>
<!DOCTYPE html>
<head>
<link rel="stylesheet" href="./index.css" />
<script type="module" src="./index.js"></script>
</head>
<body>
<div id="header-1" class="container-1"
style="">
<div id="button-test" class="button-1 button-test multicolor" onclick="window.location.href = 'test/index.html';">TEST</div>
<div id="button-trans" class="button-trans multicolor">TRANSCENDENCE</div>
</div>
<img id="wiskas" style="margin: auto; display: block;" src="webcat/web_cat_img/wiskas-the-third.jpg">
<section style="display: flex;
justify-content: center;
width: 1000px;
margin: 0 auto;">
<p>I, am wiskas-the-third,
We are the cat company, we dont need to present our self for you already know
who we are, we created the internet, and we are still managing it now<br>
We at CAT are the admin, creator, and workers of the internet
Everytime a human goes to sleep, a cat start its shift, 1 billion pair of whiskers that are always here for you
Why? because we are philantropists, dont question it. Our goals are beyond your understanding
the internet was created by us, for us, and you should be glad we allow you to use it.
</p>
</section>
<section style="display: flex;">
<button style="margin-right: 50px;" class="button-1 multicolor" onclick="window.location.href = 'webcat/biblio.html';">
Latest News</button><br>
<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);
+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
-133
View File
@@ -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);
}
-47
View File
@@ -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>
@@ -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 {
constructor(socket, tetrisGame, onStatusChange, onStart) {
// ui : { showOverlay, hideOverlay, render, renderOpponent, updateButtons }
constructor(socket, tetrisGame, onStatusChange, onStart, ui) {
this.socket = socket;
this.tetrisGame = tetrisGame;
this.onStatusChange = onStatusChange; // (status, opponentName) => void
this.onStart = onStart; // () => void — déclenche le début du jeu local
this.onStatusChange = onStatusChange;
this.onStart = onStart;
this.ui = ui;
this.action_queue = [];
this.opponentGrid = this._emptyGrid();
this.opponentScore = 0;
this.roomCode = null;
this.isReady = false;
this.action_queue = [];
this.opponentGrid = this._emptyGrid();
this.opponentScore = 0;
this.opponentShieldActive = false;
this.roomCode = null;
this.isReady = false;
this._bindSocketEvents();
}
@@ -33,10 +36,11 @@ class Duel {
leave() {
if (!this.roomCode) return;
this.socket.emit('tetris:leave');
this.roomCode = null;
this.isReady = false;
this.opponentGrid = this._emptyGrid();
this.opponentScore = 0;
this.roomCode = null;
this.isReady = false;
this.opponentGrid = this._emptyGrid();
this.opponentScore = 0;
this.opponentShieldActive = false;
}
// ─── Hooks appelés par tetris.js ──────────
@@ -48,9 +52,7 @@ class Duel {
onLocalLinesCleared(count, holeCol) {
if (!this.isReady) return;
const garbageLines = [];
for (let i = 0; i < count; i++)
garbageLines.push(this._buildGarbageLine(holeCol));
const garbageLines = Array.from({ length: count }, () => this._buildGarbageLine(holeCol));
this.socket.emit('tetris:lines-cleared', { count, holeCol, garbageLines });
}
@@ -60,6 +62,12 @@ class Duel {
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() {
this.isReady = false;
this.action_queue = [];
@@ -70,8 +78,7 @@ class Duel {
synchronize_game() {
while (this.action_queue.length > 0) {
const action = this.action_queue.shift();
this._processAction(action);
this._processAction(this.action_queue.shift());
}
}
@@ -81,7 +88,7 @@ class Duel {
this.opponentGrid = action.grid;
this.opponentScore = action.score;
document.getElementById('opponent-score').textContent = action.score;
renderOpponent(this.opponentGrid);
this.ui.renderOpponent(this.opponentGrid, this.opponentShieldActive);
break;
case 'LINES_CLEARED':
@@ -89,9 +96,17 @@ class Duel {
break;
case 'OPPONENT_GAME_OVER':
showOverlay('YOU WIN', action.score);
this.ui.showOverlay('YOU WIN', action.score);
this.endDuel();
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.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', () => {
if (this.onStart) this.onStart();
});
this.socket.on('tetris:pause', () => {
this.tetrisGame.pause();
updateButtons();
if (this.tetrisGame.isPaused) showOverlay('PAUSE');
else hideOverlay();
this.ui.updateButtons();
if (this.tetrisGame.isPaused) this.ui.showOverlay('PAUSE');
else this.ui.hideOverlay();
});
this.socket.on('tetris:stop', () => {
this.tetrisGame.stop();
updateButtons();
render();
showOverlay('STOPPED');
this.ui.updateButtons();
this.ui.render();
this.ui.showOverlay('STOPPED');
});
this.socket.on('tetris:settings', (data) => {
document.getElementById('input-ttd').value = data.timeToDown;
document.getElementById('input-hardening').value = data.hardening;
document.getElementById('input-decrement').value = data.decrementTTD;
document.getElementById('input-ttd').value = data.timeToDown;
document.getElementById('input-hardening').value = data.hardening;
document.getElementById('input-decrement').value = data.decrementTTD;
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;
}
/* ── 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"] {
background: var(--bg);
border: 1px solid var(--border);
@@ -620,3 +651,36 @@ button:disabled { opacity: 0.3; cursor: not-allowed; }
}
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>
<!-- 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">
<span class="settings-title">Duel</span>
<div class="duel-row">
@@ -40,7 +39,7 @@
<div id="local-section">
<div id="app">
<!-- Colonne gauche : Hold + Score + Boutons + Settings -->
<!-- Colonne gauche : Hold + Score + Boutons + Paramètres -->
<div id="left-column">
<div class="panel">
<div class="panel-title">Hold</div>
@@ -51,6 +50,12 @@
<div class="score-value" id="score-display">0</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">
<button id="btn-start">Start</button>
<button id="btn-pause" disabled>Pause</button>
@@ -58,9 +63,18 @@
</div>
</div>
<!-- Panneau de configuration -->
<!-- Paramètres -->
<div id="settings-panel">
<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">
<label for="input-ttd">Vitesse initiale (ms)</label>
<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>Espace</span> Drop</div>
<div><span>C</span> Hold</div>
<div><span>E</span> Shield</div>
</div>
</div>
@@ -111,6 +126,7 @@
<div class="score-label">Score</div>
<div class="score-value" id="opponent-score"></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 id="opponent-wrapper">
@@ -134,34 +150,22 @@
<div id="lb-scores" class="lb-content lb-content--active">
<table class="lb-table">
<thead>
<tr><th>#</th><th>Joueur</th><th>Meilleur score</th><th>Parties</th></tr>
</thead>
<tbody id="lb-scores-body">
<tr><td colspan="4">Chargement…</td></tr>
</tbody>
<thead><tr><th>#</th><th>Joueur</th><th>Meilleur score</th><th>Parties</th></tr></thead>
<tbody id="lb-scores-body"><tr><td colspan="4">Chargement…</td></tr></tbody>
</table>
</div>
<div id="lb-wins" class="lb-content">
<table class="lb-table">
<thead>
<tr><th>#</th><th>Joueur</th><th>Victoires</th><th>Parties</th></tr>
</thead>
<tbody id="lb-wins-body">
<tr><td colspan="4">Chargement…</td></tr>
</tbody>
<thead><tr><th>#</th><th>Joueur</th><th>Victoires</th><th>Parties</th></tr></thead>
<tbody id="lb-wins-body"><tr><td colspan="4">Chargement…</td></tr></tbody>
</table>
</div>
<div id="lb-history" class="lb-content">
<table class="lb-table">
<thead>
<tr><th>#</th><th>Date</th><th>Type</th><th>Score</th><th>Résultat</th></tr>
</thead>
<tbody id="lb-history-body">
<tr><td colspan="5">Chargement…</td></tr>
</tbody>
<thead><tr><th>#</th><th>Date</th><th>Type</th><th>Score</th><th>Résultat</th></tr></thead>
<tbody id="lb-history-body"><tr><td colspan="5">Chargement…</td></tr></tbody>
</table>
</div>
</div>
@@ -173,59 +177,9 @@
<script src="tetris.js"></script>
<script src="renderer.js"></script>
<script src="duel.js"></script>
<script src="leaderboard.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>
</html>

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