5 Commits

Author SHA1 Message Date
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
65 changed files with 797 additions and 352 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 -6
View File
@@ -1,8 +1,6 @@
all : all : up
@docker compose -f ./docker-compose.yml up -d
no_cache : up :
@docker compose -f ./docker-compose.yml build --no-cache
@docker compose -f ./docker-compose.yml up -d @docker compose -f ./docker-compose.yml up -d
clean : clean :
@@ -12,6 +10,5 @@ fclean :
@docker compose -f ./docker-compose.yml down -v -t 1 @docker compose -f ./docker-compose.yml down -v -t 1
@docker system prune -af --volumes @docker system prune -af --volumes
re : fclean no_cache re : fclean up
.PHONY : all no_cache clean fclean re
-2
View File
@@ -24,8 +24,6 @@ services:
build: ./srcs/backend build: ./srcs/backend
expose: expose:
- "3001" - "3001"
# ports:
# - "3001:3001"
depends_on: depends_on:
- database - database
volumes: volumes:
@@ -730,6 +730,16 @@ function setupSocketIO(io)
_tetrisRelayToOpponent(socket, 'tetris:lines-cleared', data); _tetrisRelayToOpponent(socket, 'tetris:lines-cleared', data);
}); });
// Relay pur : shield-activated → adversaire uniquement
socket.on('tetris:shield-activated', () => {
_tetrisRelayToOpponent(socket, 'tetris:shield-activated', {});
});
// Relay pur : shield-deactivated → adversaire uniquement
socket.on('tetris:shield-deactivated', () => {
_tetrisRelayToOpponent(socket, 'tetris:shield-deactivated', {});
});
// start-duel → relayé aux DEUX joueurs de la room (inclut l'émetteur) // start-duel → relayé aux DEUX joueurs de la room (inclut l'émetteur)
socket.on('tetris:start-duel', () => { socket.on('tetris:start-duel', () => {
const code = socket.tetrisRoomCode; const code = socket.tetrisRoomCode;
-35
View File
@@ -16,12 +16,10 @@ import { StatsWindow } from './stats.js';
*/ */
class App { class App {
constructor() { constructor() {
console.log("APP STARTED");
this.initWindows(); this.initWindows();
this.initMenu(); this.initMenu();
this.initPage(); this.initPage();
this.initEasterEgg(); this.initEasterEgg();
this.colorizeUI();
} }
/** /**
@@ -107,39 +105,6 @@ class App {
}); });
} }
} }
colorizeUI() {
const elements = document.querySelectorAll(".title, .menu__item, .game__item, .page__item");
const colorizeText = (el) => {
const text = el.textContent;
el.innerHTML = "";
const baseHue = Math.random() * 360;
// 🎲 random step = makes rainbow "scrambled"
const step = (Math.random() * 60) + 10; // 10 → 70
// 🎲 random direction (left or right rainbow)
const direction = Math.random() < 0.5 ? 1 : -1;
[...text].forEach((char, i) => {
const span = document.createElement("span");
span.textContent = char;
const hue = baseHue + (i * step * direction);
span.style.color = `hsl(${hue}, 90%, 60%)`;
span.style.textShadow = `1px 1px 0 rgba(0,0,0,0.3)`;
el.appendChild(span);
});
};
elements.forEach(colorizeText);
}
} }
// Start the application when DOM is ready // Start the application when DOM is ready
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.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 994 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1018 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 955 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1022 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 887 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1000 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

+31 -5
View File
@@ -9,11 +9,12 @@ class Duel {
this.onStatusChange = onStatusChange; // (status, opponentName) => void this.onStatusChange = onStatusChange; // (status, opponentName) => void
this.onStart = onStart; // () => void — déclenche le début du jeu local this.onStart = onStart; // () => void — déclenche le début du jeu local
this.action_queue = []; this.action_queue = [];
this.opponentGrid = this._emptyGrid(); this.opponentGrid = this._emptyGrid();
this.opponentScore = 0; this.opponentScore = 0;
this.roomCode = null; this.opponentShieldActive = false;
this.isReady = false; this.roomCode = null;
this.isReady = false;
this._bindSocketEvents(); this._bindSocketEvents();
} }
@@ -60,6 +61,15 @@ class Duel {
this.endDuel(); this.endDuel();
} }
onLocalShieldChanged(event) {
if (!this.isReady) return;
if (event === 'activated') {
this.socket.emit('tetris:shield-activated');
} else if (event === 'deactivated') {
this.socket.emit('tetris:shield-deactivated');
}
}
endDuel() { endDuel() {
this.isReady = false; this.isReady = false;
this.action_queue = []; this.action_queue = [];
@@ -92,6 +102,14 @@ class Duel {
showOverlay('YOU WIN', action.score); showOverlay('YOU WIN', action.score);
this.endDuel(); this.endDuel();
break; break;
case 'OPPONENT_SHIELD_ACTIVATED':
this.opponentShieldActive = true;
break;
case 'OPPONENT_SHIELD_DEACTIVATED':
this.opponentShieldActive = false;
break;
} }
} }
@@ -127,6 +145,14 @@ class Duel {
this.action_queue.push({ type: 'OPPONENT_GAME_OVER', score: data.score, validBlock: data.validBlock }); this.action_queue.push({ type: 'OPPONENT_GAME_OVER', score: data.score, validBlock: data.validBlock });
}); });
this.socket.on('tetris:shield-activated', () => {
this.action_queue.push({ type: 'OPPONENT_SHIELD_ACTIVATED' });
});
this.socket.on('tetris:shield-deactivated', () => {
this.action_queue.push({ type: 'OPPONENT_SHIELD_DEACTIVATED' });
});
this.socket.on('tetris:start-duel', () => { this.socket.on('tetris:start-duel', () => {
if (this.onStart) this.onStart(); if (this.onStart) this.onStart();
}); });
+381 -209
View File
@@ -1,27 +1,26 @@
:root { :root {
--color-primary: #ffc75e; --color-primary: #0066cc;
--color-primary-hover: #ffc75e; --color-primary-hover: #0052a3;
--color-success: #3cff01; --color-success: #3cff01;
--color-success-dark: #ffc75e; --color-success-dark: #28a745;
--color-error: #ff4d4d; --color-error: #ff4d4d;
--color-warning: #ffc75e; --color-warning: #ffc107;
--color-github: #ffc75e; --color-github: #24292e;
--color-bg: #ffe5b5; --color-bg: #000;
--app-background-base: radial-gradient( --app-background-base: radial-gradient(
circle at top, circle at top,
#3fc9ff, #1b2735,
#21fcc5 #090a0f
); );
--app-background-image: url("./assets/Frame1.png"); /* --app-background-image: url("./assets/background.png"); */
--color-surface: #ffcc00; --color-surface: #222;
--color-surface-light: #feffa6; --color-surface-light: #333;
--color-text: #000000; --color-text: #fff;
--color-text-muted: #353535; --color-text-muted: #aaa;
--font-size-base: 10px; --font-size-base: 10px;
--font-size-sm: 1.2rem; --font-size-sm: 1.2rem;
@@ -64,24 +63,18 @@
html { html {
height: 100%; height: 100%;
background-image: background-image:
var(--app-background-image),
var(--app-background-base); var(--app-background-base);
animation: bg-animation 12s steps(1) infinite;
background-size: contain, cover;
background-position: center, center;
background-repeat: no-repeat, no-repeat;
background-size: background-size:
contain, contain,
cover; cover;
background-position: background-position:
center,
center; center;
background-repeat: background-repeat:
no-repeat,
no-repeat; no-repeat;
} }
@@ -100,136 +93,54 @@ body {
} }
/* ============================================
ANIMATIONS
============================================ */
@keyframes wobble {
0% { transform: translate(0%, 0) rotate(0deg); }
25% { transform: translate(-5%, -1px) rotate(-0.5deg); }
50% { transform: translate(0%, 1px) rotate(0.5deg); }
75% { transform: translate(+5%, -1px) rotate(0.5deg); }
100% { transform: translate(0%, 0) rotate(0deg); }
}
@keyframes bounce {
0% { transform: translateY(0) rotate(var(--rot)); }
33% { transform: translateY(-6px) rotate(var(--rot)); }
66% { transform: translateY(-8px) rotate(var(--rot)); }
100% { transform: translateY(0) rotate(var(--rot)); }
}
@keyframes bg-animation {
0% {
background-image: url("./assets/Frame1.png"), var(--app-background-base);
}
8.33% {
background-image: url("./assets/Frame2.png"), var(--app-background-base);
}
16.66% {
background-image: url("./assets/Frame3.png"), var(--app-background-base);
}
25% {
background-image: url("./assets/Frame4.png"), var(--app-background-base);
}
33.33% {
background-image: url("./assets/Frame5.png"), var(--app-background-base);
}
41.66% {
background-image: url("./assets/Frame6.png"), var(--app-background-base);
}
50% {
background-image: url("./assets/Frame7.png"), var(--app-background-base);
}
58.33% {
background-image: url("./assets/Frame8.png"), var(--app-background-base);
}
66.66% {
background-image: url("./assets/Frame9.png"), var(--app-background-base);
}
75% {
background-image: url("./assets/Frame10.png"), var(--app-background-base);
}
83.33% {
background-image: url("./assets/Frame11.png"), var(--app-background-base);
}
91.66% {
background-image: url("./assets/Frame12.png"), var(--app-background-base);
}
100% {
background-image: url("./assets/Frame1.png"), var(--app-background-base);
}
}
/* ============================================ /* ============================================
TYPOGRAPHY TYPOGRAPHY
============================================ */ ============================================ */
.title { .title {
position: absolute; position: absolute;
top: 20px; top: 0;
left: 50%; left: 50%;
translate: -50% 0; transform: translateX(-50%);
background: #ffcc00; text-transform: uppercase;
color: #000; display: flex;
align-items: center;
border: 4px solid #feffa6; justify-content: center;
border-radius: 18px; gap: 20px;
font-size: var(--font-size-xl);
padding: 0.6rem 1.2rem; text-align: center;
text-shadow: 2px 2px 10px black;
animation: wobble 2s infinite ease-in-out; z-index: 1;
font-family: "Cinzel Decorative", cursive;
color: var(--color-success);
margin: 0;
padding: var(--spacing-md);
} }
.title span {
display: inline-block;
transform-origin: center;
font-size: 4rem;
font-weight: bold;
text-shadow: 2px 2px 6px rgba(0, 0, 0, 0.5);
animation: bounce 1.2s infinite alternate;
animation-timing-function: ease-in-out;
}
.title span:nth-child(1) { --rot: -5deg; color: #ff4d4d; }
.title span:nth-child(2) { --rot: 3deg; color: #5beb67; }
.title span:nth-child(3) { --rot: -3deg; color: #ca8dfc; }
.title span:nth-child(4) { --rot: 2deg; color: #6698f5; }
.title span:nth-child(5) { --rot: -4deg; color: #ff66cc; }
.title span:nth-child(2) { animation-delay: 0.2s; }
.title span:nth-child(3) { animation-delay: 0.4s; }
.title span:nth-child(4) { animation-delay: 0.6s; }
.title span:nth-child(5) { animation-delay: 0.8s; }
.title span { will-change: transform; }
/* ============================================ /* ============================================
MENU MENU
============================================ */ ============================================ */
.menu { .menu {
position: fixed; position: fixed;
top: var(--spacing-lg); top: 0;
left: 50px; left: 50px;
padding: 0;
margin: 0;
z-index: var(--z-menu);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--spacing-lg); gap: var(--spacing-xs);
z-index: var(--z-menu);
} }
.menu__item { .menu__item {
background: var(--color-surface); background: var(--color-surface);
border-radius: var(--radius-lg); color: var(--color-text);
border: 1px solid var(--color-surface-light); border: 1px solid var(--color-surface-light);
padding: var(--spacing-sm) var(--spacing-md); padding: var(--spacing-sm) var(--spacing-md);
font-size: var(--font-size-md); font-size: var(--font-size-md);
cursor: pointer; cursor: pointer;
transition: all var(--transition-fast); transition: all var(--transition-fast);
text-align: center; text-align: left;
} }
.menu__item:hover { .menu__item:hover {
@@ -248,22 +159,25 @@ body {
.game { .game {
position: fixed; position: fixed;
top: var(--spacing-lg); top: 0;
right: 50px; right: 50px;
padding: 0;
margin: 0;
z-index: var(--z-menu);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--spacing-xs);
} }
.game__item { .game__item {
background: var(--color-surface); background: var(--color-surface);
color: var(--color-text); color: var(--color-text);
border-radius: var(--radius-lg);
border: 1px solid var(--color-surface-light); border: 1px solid var(--color-surface-light);
padding: var(--spacing-sm) var(--spacing-md); padding: var(--spacing-sm) var(--spacing-md);
font-size: var(--font-size-md); font-size: var(--font-size-md);
cursor: pointer; cursor: pointer;
transition: all var(--transition-fast); transition: all var(--transition-fast);
text-align: center; text-align: right;
} }
.game__item:hover { .game__item:hover {
@@ -294,8 +208,6 @@ body {
} }
.page__item { .page__item {
border-radius: var(--radius-lg);
background: var(--color-surface); background: var(--color-surface);
color: var(--color-text); color: var(--color-text);
border: 1px solid var(--color-surface-light); border: 1px solid var(--color-surface-light);
@@ -303,7 +215,7 @@ body {
font-size: var(--font-size-md); font-size: var(--font-size-md);
cursor: pointer; cursor: pointer;
transition: all var(--transition-fast); transition: all var(--transition-fast);
text-align: center; text-align: right;
} }
.page__item:hover { .page__item:hover {
@@ -316,10 +228,10 @@ body {
border-color: var(--color-primary); border-color: var(--color-primary);
} }
/* ============================================ /* ============================================
BUTTONS BUTTONS
============================================ */ ============================================ */
.btn { .btn {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -416,15 +328,13 @@ body {
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
background: var(--color-bg); background: var(--color-bg);
border: 2px ridge var(--color-text);
color: var(--color-text); color: var(--color-text);
z-index: var(--z-window); z-index: var(--z-window);
display: none; display: none;
flex-direction: column; flex-direction: column;
min-width: 280px; min-width: 280px;
box-shadow: var(--shadow-lg); box-shadow: var(--shadow-lg);
border-radius: 5px;
border-color: #aa1f1f;
border: 6px solid #faac37;
} }
.window--visible { .window--visible {
@@ -485,8 +395,7 @@ body {
.message { .message {
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
padding: var(--spacing-xs); padding: var(--spacing-xs);
border-radius: var(--radius-lg); border-radius: var(--radius-sm);
border-color: #000;
} }
.message--success { .message--success {
@@ -506,11 +415,6 @@ body {
============================================ */ ============================================ */
.login { .login {
width: 320px; width: 320px;
border-radius: 5px;
border-color: #aa1f1f;
border: 6px solid #faac37;
background: #ffffff;
color: #000;
} }
.login__form { .login__form {
@@ -653,74 +557,28 @@ body {
} }
/* ============================================ /* ============================================
STATS WINDOW EASTER EGG BUTTON
============================================ */ ============================================ */
.stats-window { /* .easter-egg {
width: 320px; position: absolute;
top: 20%;
left: 50%;
transform: translateX(-50%);
z-index: 1;
background: var(--color-surface);
color: var(--color-text);
border: 1px solid var(--color-surface-light);
padding: var(--spacing-sm) var(--spacing-md);
cursor: pointer;
font-size: var(--font-size-md);
border-radius: var(--radius-md);
transition: all var(--transition-fast);
} }
.stats__avatar { .easter-egg:hover {
width: 72px; background: var(--color-error);
height: 72px; border-color: var(--color-error);
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 UTILITIES
@@ -767,7 +625,7 @@ body {
.friends__tab { .friends__tab {
flex: 1; flex: 1;
padding: var(--spacing-sm); padding: var(--spacing-sm);
background: var(--color-surface-light); background: var(--color-surface);
border: 1px solid var(--color-surface-light); border: 1px solid var(--color-surface-light);
color: var(--color-text); color: var(--color-text);
cursor: pointer; cursor: pointer;
@@ -847,3 +705,317 @@ body {
color: var(--color-text-muted); color: var(--color-text-muted);
padding: var(--spacing-lg); padding: var(--spacing-lg);
} }
/* ============================================
GAME ROOM WINDOW
============================================ */
.gameroom-window {
width: 600px;
height: 800px;
}
.gameroom__tabs {
display: flex;
gap: var(--spacing-xs);
margin-bottom: var(--spacing-sm);
}
.gameroom__tab {
flex: 1;
padding: var(--spacing-sm);
background: var(--color-surface);
border: 1px solid var(--color-surface-light);
color: var(--color-text);
cursor: pointer;
font-size: var(--font-size-sm);
transition: all var(--transition-fast);
}
.gameroom__tab:hover {
background: var(--color-surface-light);
}
.gameroom__tab--active {
background: var(--color-primary);
border-color: var(--color-primary);
}
.gameroom__content {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}
.gameroom__create {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-sm);
}
.gameroom__list {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.gameroom__item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm);
background: var(--color-surface);
border-radius: var(--radius-md);
}
.gameroom__name {
flex: 1;
font-size: var(--font-size-md);
font-weight: 500;
}
.gameroom__players {
font-size: var(--font-size-sm);
color: var(--color-text-muted);
padding: var(--spacing-xs) var(--spacing-sm);
background: var(--color-surface-light);
border-radius: var(--radius-sm);
}
.gameroom__actions {
display: flex;
gap: var(--spacing-xs);
}
.gameroom__actions .btn {
padding: var(--spacing-xs) var(--spacing-sm);
font-size: var(--font-size-sm);
}
.gameroom__lobby {
display: flex;
flex-direction: column;
flex: 1;
gap: var(--spacing-sm);
}
.gameroom__lobby-title {
margin: 0;
font-size: var(--font-size-lg);
text-align: center;
color: var(--color-success);
}
.gameroom__player-list {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
background: var(--color-surface);
border-radius: var(--radius-md);
padding: var(--spacing-sm);
}
.gameroom__player {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-xs) var(--spacing-sm);
background: var(--color-surface-light);
border-radius: var(--radius-sm);
}
.gameroom__player-avatar {
width: 32px;
height: 32px;
border-radius: var(--radius-full);
object-fit: cover;
border: 2px solid var(--color-surface-light);
}
.gameroom__player-name {
flex: 1;
font-size: var(--font-size-md);
}
.gameroom__player-stats {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 2px;
}
.gameroom__player-score {
font-size: var(--font-size-sm);
color: var(--color-success);
font-weight: 500;
}
.gameroom__player-total {
font-size: var(--font-size-sm);
color: var(--color-text-muted);
}
.gameroom__empty {
text-align: center;
color: var(--color-text-muted);
padding: var(--spacing-lg);
}
/* ============================================
GAME - JEU DU PENDU/DESSIN
============================================ */
.gameroom__lobby-buttons {
display: flex;
gap: var(--spacing-sm);
margin-top: auto;
}
.gameroom__lobby-buttons .btn {
flex: 1;
}
.gameroom__game {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
flex: 1;
}
.gameroom__game-info {
text-align: center;
}
.gameroom__drawer-info {
font-size: var(--font-size-md);
color: var(--color-text-muted);
padding: var(--spacing-xs);
}
.gameroom__scores-display {
font-size: var(--font-size-sm);
color: var(--color-success);
padding: var(--spacing-xs);
background: var(--color-surface);
border-radius: var(--radius-sm);
margin-top: var(--spacing-xs);
}
.gameroom__drawer-info--winner {
color: var(--color-success);
font-weight: bold;
animation: pulse 0.5s ease-in-out 3;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.gameroom__word-display {
font-size: var(--font-size-xl);
font-family: monospace;
text-align: center;
letter-spacing: 8px;
padding: var(--spacing-md);
background: var(--color-surface);
border-radius: var(--radius-md);
min-height: 60px;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-success);
}
.gameroom__canvas-container {
display: flex;
justify-content: center;
}
.gameroom__canvas {
background: var(--color-surface-light);
border-radius: var(--radius-md);
cursor: crosshair;
border: 2px solid var(--color-surface-light);
}
.gameroom__draw-tools {
display: flex;
gap: var(--spacing-sm);
justify-content: center;
align-items: center;
}
.gameroom__color-picker {
width: 40px;
height: 32px;
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
background: transparent;
}
.gameroom__word-input-container,
.gameroom__guess-container {
display: flex;
gap: var(--spacing-sm);
}
.gameroom__word-input-container .input,
.gameroom__guess-container .input {
flex: 1;
}
.gameroom__guess-container .input:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.gameroom__guess-container .btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.gameroom__guess-history {
flex: 1;
min-height: 60px;
max-height: 100px;
overflow-y: auto;
background: var(--color-surface);
border-radius: var(--radius-md);
padding: var(--spacing-sm);
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.gameroom__guess-item {
font-size: var(--font-size-sm);
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius-sm);
}
.gameroom__guess-item--success {
background: rgba(60, 255, 1, 0.2);
color: var(--color-success);
}
.gameroom__guess-item--fail {
background: rgba(255, 77, 77, 0.2);
color: var(--color-error);
}
.gameroom__game-buttons {
display: flex;
gap: var(--spacing-sm);
margin-top: var(--spacing-sm);
}
.gameroom__game-buttons .btn {
flex: 1;
}
+4 -8
View File
@@ -9,15 +9,8 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <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" /> <link href="https://fonts.googleapis.com/css2?family=Cinzel+Decorative:wght@400;700&display=swap" rel="stylesheet" />
</head> </head>
<script type="module" src="app.js"></script>
<body> <body>
<h1 class="title"> <h1 class="title">Lobby</h1>
<span>L</span>
<span>o</span>
<span>b</span>
<span>b</span>
<span>y</span>
</h1>
<nav class="menu" aria-label="Menu principal"> <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="login" aria-label="Login">Login</button>
@@ -34,5 +27,8 @@
<div class="page" aria-label="Page"> <div class="page" aria-label="Page">
<button class="page__item" data-action="gameroom" aria-label="Game Rooms">Game Rooms</button> <button class="page__item" data-action="gameroom" aria-label="Game Rooms">Game Rooms</button>
</div> </div>
<script type="module" src="app.js"></script>
</body> </body>
</html> </html>
+7 -3
View File
@@ -194,7 +194,8 @@ export class GameRoomWindow extends Window {
players: [], players: [],
currentPlayerIndex: 0, currentPlayerIndex: 0,
guessedLetters: [], guessedLetters: [],
scores: {} scores: {},
counter: 0
}; };
this.initDrawing(); this.initDrawing();
@@ -1568,8 +1569,11 @@ export class GameRoomWindow extends Window {
nextRound() { nextRound() {
// Move to next player // Move to next player
this.gameState.currentPlayerIndex = (this.gameState.currentPlayerIndex + 1) % this.gameState.players.length; this.gameState.counter++;
const nextDrawer = this.gameState.players[this.gameState.currentPlayerIndex]; if (this.gameState.counter >= this.gameState.players.length) {
this.gameState.counter = 0;
}
const nextDrawer = this.gameState.players[this.gameState.counter];
if (this.socket?.connected) { if (this.socket?.connected) {
this.socket.emit('game-next-round', { drawer: nextDrawer }); this.socket.emit('game-next-round', { drawer: nextDrawer });
+48 -60
View File
@@ -7,28 +7,28 @@
CSS VARIABLES CSS VARIABLES
============================================ */ ============================================ */
:root { :root {
--color-primary: #ffc75e; --color-primary: #0066cc;
--color-primary-hover: #ffc75e; --color-primary-hover: #0052a3;
--color-success: #3cff01; --color-success: #3cff01;
--color-success-dark: #ffc75e; --color-success-dark: #28a745;
--color-error: #ff4d4d; --color-error: #ff4d4d;
--color-warning: #ffc75e; --color-warning: #ffc107;
--color-github: #ffc75e; --color-github: #24292e;
--color-bg: #ffe5b5; --color-bg: #a3a3a3;
--app-background-base: radial-gradient( --app-background-base: radial-gradient(
circle at top, circle at top,
#fff787, #000000,
#ff8080 #4d4d4d
); );
--app-background-image: url("./assets/background.png"); --app-background-image: url("./assets/background.png");
--color-surface: #ffefce; --color-surface: #222;
--color-surface-light: #ffc75e; --color-surface-light: #333;
--color-text: #000000; --color-text: #fff;
--color-text-muted: #000000; --color-text-muted: #aaa;
--font-size-base: 10px; --font-size-base: 10px;
--font-size-sm: 1.2rem; --font-size-sm: 1.2rem;
@@ -117,16 +117,16 @@ body {
text-align: center; text-align: center;
text-shadow: 2px 2px 10px black; text-shadow: 2px 2px 10px black;
z-index: 1; z-index: 1;
font-family: "Roboto"; font-family: "Cinzel Decorative", cursive;
letter-spacing: -10px;
color: rgba(248, 252, 2, 0.6); color: rgba(248, 252, 2, 0.6);
margin: 0; margin: 0;
padding: 0.6rem 1.2rem; padding: var(--spacing-md);
background-color: #ffefce; /* Rectangle + rounded corners */
background-color: rgba(247, 7, 67, 0.6);
border: 2px solid rgba(0, 0, 0, 0.6); border: 2px solid rgba(0, 0, 0, 0.6);
border-radius: var(--radius-lg); border-radius: 15px;
} }
@@ -134,7 +134,7 @@ body {
MENU MENU
============================================ */ ============================================ */
/* .menu { .menu {
position: fixed; position: fixed;
top: 0; top: 0;
left: 50px; left: 50px;
@@ -144,31 +144,17 @@ body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--spacing-xs); gap: var(--spacing-xs);
} */
.menu {
position: fixed;
top: var(--spacing-lg);
left: 50px;
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
z-index: var(--z-menu);
} }
.menu__item { .menu__item {
background: var(--color-surface); background: var(--color-surface);
color: var(--color-text); color: var(--color-text);
border: 1px solid var(--color-surface-light); border: 1px solid var(--color-surface-light);
border-radius: var(--radius-lg);
border-color: #000;
padding: var(--spacing-sm) var(--spacing-md); padding: var(--spacing-sm) var(--spacing-md);
font-size: var(--font-size-md); font-size: var(--font-size-md);
cursor: pointer; cursor: pointer;
transition: all var(--transition-fast); transition: all var(--transition-fast);
text-align: center; text-align: left;
} }
.menu__item:hover { .menu__item:hover {
@@ -185,7 +171,7 @@ body {
GAME GAME
============================================ */ ============================================ */
/* .game { .game {
position: fixed; position: fixed;
top: 0; top: 0;
right: 50px; right: 50px;
@@ -195,31 +181,17 @@ body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--spacing-xs); 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 { .game__item {
background: var(--color-surface); background: var(--color-surface);
color: var(--color-text); color: var(--color-text);
border: 1px solid var(--color-surface-light); border: 1px solid var(--color-surface-light);
border-radius: var(--radius-lg);
border-color: #000;
padding: var(--spacing-sm) var(--spacing-md); padding: var(--spacing-sm) var(--spacing-md);
font-size: var(--font-size-md); font-size: var(--font-size-md);
cursor: pointer; cursor: pointer;
transition: all var(--transition-fast); transition: all var(--transition-fast);
text-align: center; text-align: right;
} }
.game__item:hover { .game__item:hover {
@@ -331,15 +303,13 @@ body {
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
background: var(--color-bg); background: var(--color-bg);
border: 2px ridge var(--color-text);
color: var(--color-text); color: var(--color-text);
z-index: var(--z-window); z-index: var(--z-window);
display: none; display: none;
flex-direction: column; flex-direction: column;
min-width: 280px; min-width: 280px;
box-shadow: var(--shadow-lg); box-shadow: var(--shadow-lg);
border-radius: 5px;
border-color: #aa1f1f;
border: 6px solid #faac37;
} }
.window--visible { .window--visible {
@@ -400,8 +370,7 @@ body {
.message { .message {
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
padding: var(--spacing-xs); padding: var(--spacing-xs);
border-radius: var(--radius-lg); border-radius: var(--radius-sm);
border-color: #000;
} }
.message--success { .message--success {
@@ -421,11 +390,6 @@ body {
============================================ */ ============================================ */
.login { .login {
width: 320px; width: 320px;
border-radius: 5px;
border-color: #aa1f1f;
border: 6px solid #faac37;
background: #ffffff;
color: #000;
} }
.login__form { .login__form {
@@ -637,6 +601,30 @@ body {
padding: var(--spacing-sm) 0; padding: var(--spacing-sm) 0;
} }
/* ============================================
EASTER EGG BUTTON
============================================ */
/* .easter-egg {
position: absolute;
top: 20%;
left: 50%;
transform: translateX(-50%);
z-index: 1;
background: var(--color-surface);
color: var(--color-text);
border: 1px solid var(--color-surface-light);
padding: var(--spacing-sm) var(--spacing-md);
cursor: pointer;
font-size: var(--font-size-md);
border-radius: var(--radius-md);
transition: all var(--transition-fast);
}
.easter-egg:hover {
background: var(--color-error);
border-color: var(--color-error);
} */
/* ============================================ /* ============================================
UTILITIES UTILITIES
============================================ */ ============================================ */
@@ -682,7 +670,7 @@ body {
.friends__tab { .friends__tab {
flex: 1; flex: 1;
padding: var(--spacing-sm); padding: var(--spacing-sm);
background: var(--color-surface-light); background: var(--color-surface);
border: 1px solid var(--color-surface-light); border: 1px solid var(--color-surface-light);
color: var(--color-text); color: var(--color-text);
cursor: pointer; cursor: pointer;
+4 -4
View File
@@ -3,14 +3,14 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Transcendence</title> <title>Transcendence.io</title>
<link rel="stylesheet" href="index.css" /> <link rel="stylesheet" href="index.css" />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <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" /> <link href="https://fonts.googleapis.com/css2?family=Cinzel+Decorative:wght@400;700&display=swap" rel="stylesheet" />
</head> </head>
<body> <body>
<h1 class="title">Transcendence</h1> <h1 class="title">Transcendence.io</h1>
<nav class="menu" aria-label="Menu principal"> <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="login" aria-label="Login">Login</button>
@@ -20,8 +20,8 @@
</nav> </nav>
<nav class="game" aria-label="Game"> <nav class="game" aria-label="Game">
<button class="game__item" data-action="new_game" aria-label="Skkrrribl.io" <button class="game__item" data-action="new_game" aria-label="Start new game"
onclick="window.location.href='game.html'">Skkrrribl.io</button> onclick="window.location.href='game.html'">Start new game</button>
<button class="game__item" data-action="tetris" aria-label="Tetris" <button class="game__item" data-action="tetris" aria-label="Tetris"
onclick="window.location.href='tetris.html'">Tetris</button> onclick="window.location.href='tetris.html'">Tetris</button>
</nav> </nav>
+55
View File
@@ -17,6 +17,7 @@ export class LoginWindow extends Window {
this.buildUI(); this.buildUI();
this.bindEvents(); this.bindEvents();
this.checkIfAlreadyLoggedIn(); this.checkIfAlreadyLoggedIn();
this.NotficationContainer();
} }
/** /**
@@ -129,6 +130,7 @@ export class LoginWindow extends Window {
if (response.ok && data.token) { if (response.ok && data.token) {
localStorage.setItem(STORAGE_KEYS.AUTH_TOKEN, data.token); localStorage.setItem(STORAGE_KEYS.AUTH_TOKEN, data.token);
this.showMessage('Login successful! Welcome.', 'success'); this.showMessage('Login successful! Welcome.', 'success');
this.showNotification('Login successful', 'green');
// Emit login event // Emit login event
eventBus.emit(Events.USER_LOGGED_IN, { username, token: data.token }); eventBus.emit(Events.USER_LOGGED_IN, { username, token: data.token });
@@ -138,6 +140,7 @@ export class LoginWindow extends Window {
} else { } else {
const errorMsg = data?.message || 'Login failed'; const errorMsg = data?.message || 'Login failed';
this.showMessage(errorMsg, 'error'); this.showMessage(errorMsg, 'error');
this.showNotification(errorMsg, 'red');
} }
} catch (error) { } catch (error) {
console.error('Login error:', error); console.error('Login error:', error);
@@ -170,10 +173,12 @@ export class LoginWindow extends Window {
if (response.ok) { if (response.ok) {
this.showMessage('Registration successful! You can now sign in.', 'success'); this.showMessage('Registration successful! You can now sign in.', 'success');
this.showNotification('Registration successful', 'green');
eventBus.emit(Events.USER_REGISTERED, { username }); eventBus.emit(Events.USER_REGISTERED, { username });
} else { } else {
const errorMsg = data?.message || 'Registration failed'; const errorMsg = data?.message || 'Registration failed';
this.showMessage(errorMsg, 'error'); this.showMessage(errorMsg, 'error');
this.showNotification(errorMsg, 'red');
} }
} catch (error) { } catch (error) {
console.error('Registration error:', error); console.error('Registration error:', error);
@@ -200,6 +205,7 @@ export class LoginWindow extends Window {
if (event.data?.token) { if (event.data?.token) {
localStorage.setItem(STORAGE_KEYS.AUTH_TOKEN, event.data.token); localStorage.setItem(STORAGE_KEYS.AUTH_TOKEN, event.data.token);
this.showMessage('GitHub login successful! Welcome.', 'success'); this.showMessage('GitHub login successful! Welcome.', 'success');
this.showNotification('GitHub login successful', 'green');
// Emit login event // Emit login event
eventBus.emit(Events.USER_LOGGED_IN, { eventBus.emit(Events.USER_LOGGED_IN, {
@@ -215,6 +221,55 @@ export class LoginWindow extends Window {
window.addEventListener('message', handleMessage, { once: true }); window.addEventListener('message', handleMessage, { once: true });
} }
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) {
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);
}
/** /**
* Displays a feedback message * Displays a feedback message
* @param {string} text - Message text * @param {string} text - Message text
+109 -6
View File
@@ -2,8 +2,54 @@
// RENDU // RENDU
// ───────────────────────────────────────────── // ─────────────────────────────────────────────
const CELL = 30; const CELL = 30;
const COLORS = ['#000500','#00ff41','#39ff14','#00e676','#76ff03','#b2ff59','#00ffaa','#ccff00','#2d5a2d'];
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 ctxMain = document.getElementById('canvas-main').getContext('2d');
const ctxNext = document.getElementById('canvas-next').getContext('2d'); const ctxNext = document.getElementById('canvas-next').getContext('2d');
@@ -22,7 +68,7 @@ function drawCell(ctx, x, y, colorIndex, size) {
ctx.fillRect(x * size + p + 2, y * size + p + 2, size - p * 2 - 4, size - p * 2 - 4); ctx.fillRect(x * size + p + 2, y * size + p + 2, size - p * 2 - 4, size - p * 2 - 4);
ctx.shadowBlur = 0; ctx.shadowBlur = 0;
// Highlight top/left // Highlight top/left
ctx.fillStyle = 'rgba(200,255,200,0.2)'; ctx.fillStyle = currentTheme.highlight;
ctx.fillRect(x * size + p, y * size + p, size - p * 2, 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); ctx.fillRect(x * size + p, y * size + p, 2, size - p * 2);
// Shadow bottom/right // Shadow bottom/right
@@ -32,12 +78,12 @@ function drawCell(ctx, x, y, colorIndex, size) {
} }
function clearCanvas(ctx, w, h) { function clearCanvas(ctx, w, h) {
ctx.fillStyle = '#000500'; ctx.fillStyle = currentTheme.bg;
ctx.fillRect(0, 0, w, h); ctx.fillRect(0, 0, w, h);
} }
function drawGridLines(ctx, cols, rows, size) { function drawGridLines(ctx, cols, rows, size) {
ctx.strokeStyle = 'rgba(0,255,65,0.06)'; ctx.strokeStyle = currentTheme.grid;
ctx.lineWidth = 1; ctx.lineWidth = 1;
for (let x = 0; x <= cols; x++) { for (let x = 0; x <= cols; x++) {
ctx.beginPath(); ctx.moveTo(x * size, 0); ctx.lineTo(x * size, rows * size); ctx.stroke(); ctx.beginPath(); ctx.moveTo(x * size, 0); ctx.lineTo(x * size, rows * size); ctx.stroke();
@@ -67,7 +113,7 @@ function drawGhost(ctx, piece, grid) {
if (ghost.y === piece.getPosition().y) return; if (ghost.y === piece.getPosition().y) return;
ctx.strokeStyle = 'rgba(0,255,65,0.25)'; ctx.strokeStyle = currentTheme.ghost;
ctx.lineWidth = 1; ctx.lineWidth = 1;
for (let row = 0; row < shape.length; row++) for (let row = 0; row < shape.length; row++)
for (let col = 0; col < shape[row].length; col++) for (let col = 0; col < shape[row].length; col++)
@@ -93,6 +139,17 @@ function drawMiniPiece(ctx, piece, canvasW, canvasH) {
drawCell(ctx, offsetX + col, offsetY + row, color, s); 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();
}
function render() { function render() {
// Grille principale // Grille principale
clearCanvas(ctxMain, 300, 600); clearCanvas(ctxMain, 300, 600);
@@ -115,12 +172,39 @@ function render() {
drawCell(ctxMain, x + col, y + row, color, CELL); drawCell(ctxMain, x + col, y + row, color, CELL);
} }
// Shield overlay (bordure cyan pulsée)
if (game.shieldActive) {
const pulse = 0.6 + 0.4 * Math.sin(Date.now() / 150);
_drawShieldOverlay(ctxMain, 300, 600, pulse);
}
// Panneaux miniatures // Panneaux miniatures
drawMiniPiece(ctxNext, game.nextPiece, 100, 80); drawMiniPiece(ctxNext, game.nextPiece, 100, 80);
drawMiniPiece(ctxHold, game.storedPiece, 100, 80); drawMiniPiece(ctxHold, game.storedPiece, 100, 80);
// Score // Score
document.getElementById('score-display').textContent = game.score; document.getElementById('score-display').textContent = game.score;
// Shield status UI
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) + '%';
}
}
} }
function renderOpponent(opponentGrid) { function renderOpponent(opponentGrid) {
@@ -130,4 +214,23 @@ function renderOpponent(opponentGrid) {
for (let x = 0; x < opponentGrid[y].length; x++) for (let x = 0; x < opponentGrid[y].length; x++)
if (opponentGrid[y][x] !== 0) if (opponentGrid[y][x] !== 0)
drawCell(ctxOpponent, x, y, opponentGrid[y][x], CELL); drawCell(ctxOpponent, x, y, opponentGrid[y][x], CELL);
// Shield overlay adversaire
if (typeof duel !== 'undefined' && duel && duel.opponentShieldActive) {
const pulse = 0.6 + 0.4 * Math.sin(Date.now() / 150);
_drawShieldOverlay(ctxOpponent, 300, 600, pulse);
}
// Indicateur HTML adversaire
const oppShieldEl = document.getElementById('opponent-shield-indicator');
if (oppShieldEl) {
const active = typeof duel !== 'undefined' && duel && duel.opponentShieldActive;
oppShieldEl.style.display = active ? 'block' : 'none';
}
} }
// Restore saved theme
(function() {
const saved = localStorage.getItem('tetris-theme');
if (saved && THEMES[saved]) setColorTheme(saved);
})();
@@ -445,6 +445,37 @@ button:disabled { opacity: 0.3; cursor: not-allowed; }
letter-spacing: 0.05em; letter-spacing: 0.05em;
} }
/* ── Theme color picker ── */
.theme-btns {
display: flex;
gap: 6px;
margin-top: 2px;
}
.theme-btn {
width: 22px;
height: 22px;
min-width: 22px;
padding: 0;
border-radius: 50%;
border: 2px solid transparent;
cursor: pointer;
transition: transform 0.15s, box-shadow 0.15s;
}
.theme-btn[data-theme="green"] { background: #00ff41; }
.theme-btn[data-theme="red"] { background: #ff1744; }
.theme-btn[data-theme="yellow"] { background: #ffd600; }
.theme-btn[data-theme="blue"] { background: #00b0ff; }
.theme-btn:hover { transform: scale(1.2); }
.theme-btn.active {
border-color: #ffffff;
box-shadow: 0 0 8px currentColor;
transform: scale(1.15);
}
#settings-panel input[type="number"] { #settings-panel input[type="number"] {
background: var(--bg); background: var(--bg);
border: 1px solid var(--border); border: 1px solid var(--border);
@@ -620,3 +651,36 @@ button:disabled { opacity: 0.3; cursor: not-allowed; }
} }
body { overflow: hidden; } body { overflow: hidden; }
/* ── Shield ───────────────────────────────── */
.shield-bar-bg {
width: 100%;
height: 4px;
background: rgba(0,212,255,0.15);
border-radius: 2px;
margin-top: 4px;
overflow: hidden;
}
.shield-bar {
height: 100%;
background: #00d4ff;
border-radius: 2px;
transition: width 0.1s linear;
box-shadow: 0 0 6px #00d4ff;
}
.shield-ready { color: #00d4ff !important; }
.shield-active { color: #00ffff !important; text-shadow: 0 0 8px #00ffff; }
.shield-cooldown { color: var(--dim) !important; }
kbd {
display: inline-block;
padding: 0 3px;
border: 1px solid var(--border);
border-radius: 2px;
font-size: 0.6rem;
font-family: inherit;
color: var(--dim);
}
@@ -51,6 +51,12 @@
<div class="score-value" id="score-display">0</div> <div class="score-value" id="score-display">0</div>
</div> </div>
<div class="score-block">
<div class="score-label">Shield <kbd>E</kbd></div>
<div class="score-value shield-ready" id="shield-status-display">PRÊT</div>
<div class="shield-bar-bg"><div class="shield-bar" id="shield-bar"></div></div>
</div>
<div class="btn-group"> <div class="btn-group">
<button id="btn-start">Start</button> <button id="btn-start">Start</button>
<button id="btn-pause" disabled>Pause</button> <button id="btn-pause" disabled>Pause</button>
@@ -61,6 +67,15 @@
<!-- Panneau de configuration --> <!-- Panneau de configuration -->
<div id="settings-panel"> <div id="settings-panel">
<div class="settings-title">Paramètres</div> <div class="settings-title">Paramètres</div>
<div class="settings-row">
<label>Couleur</label>
<div class="theme-btns">
<button class="theme-btn active" data-theme="green" onclick="setColorTheme('green')" title="Vert"></button>
<button class="theme-btn" data-theme="red" onclick="setColorTheme('red')" title="Rouge"></button>
<button class="theme-btn" data-theme="yellow" onclick="setColorTheme('yellow')" title="Jaune"></button>
<button class="theme-btn" data-theme="blue" onclick="setColorTheme('blue')" title="Bleu"></button>
</div>
</div>
<div class="settings-row"> <div class="settings-row">
<label for="input-ttd">Vitesse initiale (ms)</label> <label for="input-ttd">Vitesse initiale (ms)</label>
<input type="number" id="input-ttd" min="100" max="3000" step="50" value="1000"> <input type="number" id="input-ttd" min="100" max="3000" step="50" value="1000">
@@ -97,6 +112,7 @@
<div><span>W</span> Rot. droite</div> <div><span>W</span> Rot. droite</div>
<div><span>Espace</span> Drop</div> <div><span>Espace</span> Drop</div>
<div><span>C</span> Hold</div> <div><span>C</span> Hold</div>
<div><span>E</span> Shield</div>
</div> </div>
</div> </div>
@@ -111,6 +127,7 @@
<div class="score-label">Score</div> <div class="score-label">Score</div>
<div class="score-value" id="opponent-score"></div> <div class="score-value" id="opponent-score"></div>
</div> </div>
<div id="opponent-shield-indicator" style="display:none;color:#00d4ff;font-size:0.75rem;text-align:center;letter-spacing:1px;margin-top:4px;">&#x1F6E1; SHIELD ACTIF</div>
</div> </div>
<div id="opponent-wrapper"> <div id="opponent-wrapper">
+57 -3
View File
@@ -3,11 +3,12 @@
// ─────────────────────────────────────────── // ───────────────────────────────────────────
class Tetris { class Tetris {
constructor(onRender, onGameOver, onBlockPlaced = null, onLinesCleared = null) { constructor(onRender, onGameOver, onBlockPlaced = null, onLinesCleared = null, onShieldChanged = null) {
this.onRender = onRender; this.onRender = onRender;
this.onGameOver = onGameOver; this.onGameOver = onGameOver;
this.onBlockPlaced = onBlockPlaced; this.onBlockPlaced = onBlockPlaced;
this.onLinesCleared = onLinesCleared; this.onLinesCleared = onLinesCleared;
this.onShieldChanged = onShieldChanged;
this.grid = this._createGrid(10, 20); this.grid = this._createGrid(10, 20);
this.bufferGrid = this._createGrid(10, 5); this.bufferGrid = this._createGrid(10, 5);
@@ -28,6 +29,12 @@ class Tetris {
this.isPaused = false; this.isPaused = false;
this.canStore = true; this.canStore = true;
// Shield
this.shieldActive = false;
this.shieldActiveMs = 0;
this.shieldCooldownMs = 0;
this.shieldReady = true; // prêt dès le début
this.animationFrameId = null; this.animationFrameId = null;
this.lastTime = 0; this.lastTime = 0;
this.accumulator = 0; this.accumulator = 0;
@@ -55,6 +62,10 @@ class Tetris {
this.timeToDown = this.initialTimeToDown; this.timeToDown = this.initialTimeToDown;
this.storedPiece = null; this.storedPiece = null;
this.canStore = true; this.canStore = true;
this.shieldActive = false;
this.shieldActiveMs = 0;
this.shieldCooldownMs = 0;
this.shieldReady = true;
this._spawnNewPiece(); this._spawnNewPiece();
document.addEventListener('keydown', this._keyHandler); document.addEventListener('keydown', this._keyHandler);
this._startGameLoop(); this._startGameLoop();
@@ -108,6 +119,8 @@ class Tetris {
this.lastTime = currentTime; this.lastTime = currentTime;
this.accumulator += deltaTime; this.accumulator += deltaTime;
this._updateShield(deltaTime);
while (this.isRunning && this.accumulator >= this.timeToDown) { while (this.isRunning && this.accumulator >= this.timeToDown) {
this._tick(); this._tick();
this.accumulator -= this.timeToDown; this.accumulator -= this.timeToDown;
@@ -174,11 +187,42 @@ class Tetris {
e.preventDefault(); e.preventDefault();
if (!this.isPaused) this._storePiece(); if (!this.isPaused) this._storePiece();
break; break;
case 'e': case 'E':
e.preventDefault();
if (!this.isPaused) this._activateShield();
break;
} }
this.onRender(); this.onRender();
} }
_activateShield() {
if (!this.shieldReady || this.shieldActive) return;
this.shieldActive = true;
this.shieldActiveMs = 3000;
this.shieldReady = false;
if (this.onShieldChanged) this.onShieldChanged('activated');
}
_updateShield(deltaTime) {
if (this.shieldActive) {
this.shieldActiveMs -= deltaTime;
if (this.shieldActiveMs <= 0) {
this.shieldActive = false;
this.shieldActiveMs = 0;
this.shieldCooldownMs = 60000;
if (this.onShieldChanged) this.onShieldChanged('deactivated');
}
} else if (!this.shieldReady) {
this.shieldCooldownMs -= deltaTime;
if (this.shieldCooldownMs <= 0) {
this.shieldCooldownMs = 0;
this.shieldReady = true;
if (this.onShieldChanged) this.onShieldChanged('ready');
}
}
}
_hardDrop() { _hardDrop() {
if (!this.currentPiece) return; if (!this.currentPiece) return;
let dist = 0; let dist = 0;
@@ -275,8 +319,17 @@ class Tetris {
const points = [0, 100, 300, 500, 800]; const points = [0, 100, 300, 500, 800];
this.score += points[cleared]; this.score += points[cleared];
this.count += points[cleared]; this.count += points[cleared];
if (this.onLinesCleared && cleared > 0) if (cleared > 0) {
this.onLinesCleared(cleared, this.lastLandingCol); // Chaque ligne remplie réduit le cooldown du shield de 10s
if (!this.shieldActive && !this.shieldReady) {
this.shieldCooldownMs = Math.max(0, this.shieldCooldownMs - cleared * 10000);
if (this.shieldCooldownMs === 0) {
this.shieldReady = true;
if (this.onShieldChanged) this.onShieldChanged('ready');
}
}
if (this.onLinesCleared) this.onLinesCleared(cleared, this.lastLandingCol);
}
} }
_makeHarder() { _makeHarder() {
@@ -361,6 +414,7 @@ class Tetris {
} }
addGarbageLines(lines) { addGarbageLines(lines) {
if (this.shieldActive) return; // shield bloque les lignes garbage
if (!this.isRunning || !lines.length) return; if (!this.isRunning || !lines.length) return;
this.grid.splice(0, lines.length); this.grid.splice(0, lines.length);
for (const line of lines) this.grid.push([...line]); // ...line pour faire une copie independante for (const line of lines) this.grid.push([...line]); // ...line pour faire une copie independante
+6
View File
@@ -113,6 +113,7 @@ btnJoinDuel.addEventListener('click', () => {
const code = inputRoomCode.value.trim().toUpperCase(); const code = inputRoomCode.value.trim().toUpperCase();
if (!code) return; if (!code) return;
if (duel) { duel.leave(); } if (duel) { duel.leave(); }
if (game.isRunning) { game.stop(); hideOverlay(); render(); updateButtons(); }
duel = new Duel(socket, game, updateDuelStatus, startLocalGame); duel = new Duel(socket, game, updateDuelStatus, startLocalGame);
duel.join(code); duel.join(code);
btnJoinDuel.disabled = true; btnJoinDuel.disabled = true;
@@ -170,6 +171,7 @@ socket.on('tetris:matched', (data) => {
// Auto-rejoindre la salle générée // Auto-rejoindre la salle générée
if (duel) { duel.leave(); } if (duel) { duel.leave(); }
if (game.isRunning) { game.stop(); hideOverlay(); render(); updateButtons(); }
duel = new Duel(socket, game, updateDuelStatus, startLocalGame); duel = new Duel(socket, game, updateDuelStatus, startLocalGame);
duel.join(data.roomCode); duel.join(data.roomCode);
inputRoomCode.value = data.roomCode; inputRoomCode.value = data.roomCode;
@@ -211,6 +213,10 @@ const game = new Tetris(
// onLinesCleared — relay duel // onLinesCleared — relay duel
(count, holeCol) => { (count, holeCol) => {
if (duel) duel.onLocalLinesCleared(count, holeCol); if (duel) duel.onLocalLinesCleared(count, holeCol);
},
// onShieldChanged — relay duel
(event) => {
if (duel) duel.onLocalShieldChanged(event);
} }
); );