diff --git a/Transcendence/srcs/frontend/src/app.js b/Transcendence/srcs/frontend/src/app.js index b260673..0affa10 100644 --- a/Transcendence/srcs/frontend/src/app.js +++ b/Transcendence/srcs/frontend/src/app.js @@ -2,13 +2,13 @@ * Application entry point * Initializes windows and handles menu interactions */ -import { windowRegistry } from './windows.js'; -import { LoginWindow } from './login.js'; -import { GlobalChat } from './global_chat.js'; -import { AvatarWindow } from './avatar.js'; -import { FriendsWindow } from './friends.js'; -import { GameRoomWindow } from './game_room.js'; -import { StatsWindow } from './stats.js'; +import { windowRegistry } from './core/windows.js'; +import { LoginWindow } from './windows/login.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'; /** * Main application class diff --git a/Transcendence/srcs/frontend/src/config.js b/Transcendence/srcs/frontend/src/core/config.js similarity index 100% rename from Transcendence/srcs/frontend/src/config.js rename to Transcendence/srcs/frontend/src/core/config.js diff --git a/Transcendence/srcs/frontend/src/element.js b/Transcendence/srcs/frontend/src/core/element.js similarity index 100% rename from Transcendence/srcs/frontend/src/element.js rename to Transcendence/srcs/frontend/src/core/element.js diff --git a/Transcendence/srcs/frontend/src/events.js b/Transcendence/srcs/frontend/src/core/events.js similarity index 100% rename from Transcendence/srcs/frontend/src/events.js rename to Transcendence/srcs/frontend/src/core/events.js diff --git a/Transcendence/srcs/frontend/src/windows.js b/Transcendence/srcs/frontend/src/core/windows.js similarity index 100% rename from Transcendence/srcs/frontend/src/windows.js rename to Transcendence/srcs/frontend/src/core/windows.js diff --git a/Transcendence/srcs/frontend/src/game.css b/Transcendence/srcs/frontend/src/game/game.css similarity index 100% rename from Transcendence/srcs/frontend/src/game.css rename to Transcendence/srcs/frontend/src/game/game.css diff --git a/Transcendence/srcs/frontend/src/game.html b/Transcendence/srcs/frontend/src/game/game.html similarity index 91% rename from Transcendence/srcs/frontend/src/game.html rename to Transcendence/srcs/frontend/src/game/game.html index c836d29..6738dee 100644 --- a/Transcendence/srcs/frontend/src/game.html +++ b/Transcendence/srcs/frontend/src/game/game.html @@ -21,7 +21,7 @@
@@ -29,6 +29,6 @@
- + \ No newline at end of file diff --git a/Transcendence/srcs/frontend/src/index.html b/Transcendence/srcs/frontend/src/index.html index ee45125..9a0e5c3 100644 --- a/Transcendence/srcs/frontend/src/index.html +++ b/Transcendence/srcs/frontend/src/index.html @@ -21,9 +21,9 @@ diff --git a/Transcendence/srcs/frontend/src/duel.js b/Transcendence/srcs/frontend/src/tetris/duel.js similarity index 77% rename from Transcendence/srcs/frontend/src/duel.js rename to Transcendence/srcs/frontend/src/tetris/duel.js index 745f4e7..b7629a4 100644 --- a/Transcendence/srcs/frontend/src/duel.js +++ b/Transcendence/srcs/frontend/src/tetris/duel.js @@ -3,18 +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.action_queue = []; + this.opponentGrid = this._emptyGrid(); + this.opponentScore = 0; this.opponentShieldActive = false; - this.roomCode = null; - this.isReady = false; + this.roomCode = null; + this.isReady = false; this._bindSocketEvents(); } @@ -34,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 ────────── @@ -49,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 }); } @@ -63,11 +64,8 @@ class Duel { 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'); - } + if (event === 'activated') this.socket.emit('tetris:shield-activated'); + else if (event === 'deactivated') this.socket.emit('tetris:shield-deactivated'); } endDuel() { @@ -80,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()); } } @@ -91,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': @@ -99,7 +96,7 @@ class Duel { break; case 'OPPONENT_GAME_OVER': - showOverlay('YOU WIN', action.score); + this.ui.showOverlay('YOU WIN', action.score); this.endDuel(); break; @@ -159,22 +156,22 @@ class Duel { 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); }); } diff --git a/Transcendence/srcs/frontend/src/tetris/effects.js b/Transcendence/srcs/frontend/src/tetris/effects.js new file mode 100644 index 0000000..09e23f9 --- /dev/null +++ b/Transcendence/srcs/frontend/src/tetris/effects.js @@ -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); +})(); diff --git a/Transcendence/srcs/frontend/src/tetris/leaderboard.js b/Transcendence/srcs/frontend/src/tetris/leaderboard.js new file mode 100644 index 0000000..9285279 --- /dev/null +++ b/Transcendence/srcs/frontend/src/tetris/leaderboard.js @@ -0,0 +1,124 @@ +// ───────────────────────────────────────────── +// LEADERBOARDS & HISTORIQUE +// ───────────────────────────────────────────── + +function escapeHtml(str) { + return String(str).replace(/&/g,'&').replace(//g,'>'); +} + +// ── 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 = 'Aucune partie jouée'; + 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 = 'Victoire'; + if (entry.result === 'loss') resultHtml = 'Défaite'; + return ` + ${i + 1} + ${date} + ${type} + ${entry.score} + ${resultHtml} + `; + }).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 = 'Aucun résultat'; + 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 ` + ${i + 1} + ${escapeHtml(r.username)}${isMe ? ' (vous)' : ''} + ${r[col1] ?? 0} + ${r[col2] ?? 0} + `; + }).join(''); + + if (!inTop && me && myRank !== null) { + html += `· · ·`; + html += ` + ${myRank} + ${escapeHtml(myUsername)} (vous) + ${me[col1] ?? 0} + ${me[col2] ?? 0} + `; + } + + tbody.innerHTML = html || 'Aucun résultat'; +} + +// ── 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(); diff --git a/Transcendence/srcs/frontend/src/pieces.js b/Transcendence/srcs/frontend/src/tetris/pieces.js similarity index 100% rename from Transcendence/srcs/frontend/src/pieces.js rename to Transcendence/srcs/frontend/src/tetris/pieces.js diff --git a/Transcendence/srcs/frontend/src/renderer.js b/Transcendence/srcs/frontend/src/tetris/renderer.js similarity index 90% rename from Transcendence/srcs/frontend/src/renderer.js rename to Transcendence/srcs/frontend/src/tetris/renderer.js index b3efcfc..77b5e57 100644 --- a/Transcendence/srcs/frontend/src/renderer.js +++ b/Transcendence/srcs/frontend/src/tetris/renderer.js @@ -61,17 +61,14 @@ function drawCell(ctx, x, y, colorIndex, size) { 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 = 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); - // 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); @@ -150,8 +147,10 @@ function _drawShieldOverlay(ctx, w, h, alpha) { ctx.restore(); } -function render() { - // Grille principale +// ── 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); @@ -160,7 +159,6 @@ function render() { 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(); @@ -172,20 +170,16 @@ function render() { 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 drawMiniPiece(ctxNext, game.nextPiece, 100, 80); drawMiniPiece(ctxHold, game.storedPiece, 100, 80); - // 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) { @@ -207,29 +201,27 @@ function render() { } } -function renderOpponent(opponentGrid) { +// ── 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 < 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); + 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); - // Shield overlay adversaire - if (typeof duel !== 'undefined' && duel && duel.opponentShieldActive) { + if (shieldActive) { 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'; - } + if (oppShieldEl) oppShieldEl.style.display = shieldActive ? 'block' : 'none'; } -// Restore saved theme +// Restaure le thème sauvegardé (function() { const saved = localStorage.getItem('tetris-theme'); if (saved && THEMES[saved]) setColorTheme(saved); diff --git a/Transcendence/srcs/frontend/src/tetris.css b/Transcendence/srcs/frontend/src/tetris/tetris.css similarity index 100% rename from Transcendence/srcs/frontend/src/tetris.css rename to Transcendence/srcs/frontend/src/tetris/tetris.css diff --git a/Transcendence/srcs/frontend/src/tetris.html b/Transcendence/srcs/frontend/src/tetris/tetris.html similarity index 66% rename from Transcendence/srcs/frontend/src/tetris.html rename to Transcendence/srcs/frontend/src/tetris/tetris.html index 970ce88..45ca2c5 100644 --- a/Transcendence/srcs/frontend/src/tetris.html +++ b/Transcendence/srcs/frontend/src/tetris/tetris.html @@ -15,10 +15,9 @@

TETRIS_

- - Home +Home - +
Duel
@@ -40,7 +39,7 @@
- +
Hold
@@ -64,16 +63,16 @@
- +
Paramètres
- - - - + + + +
@@ -151,34 +150,22 @@
- - - - - - + +
#JoueurMeilleur scoreParties
Chargement…
#JoueurMeilleur scoreParties
Chargement…
- - - - - - + +
#JoueurVictoiresParties
Chargement…
#JoueurVictoiresParties
Chargement…
- - - - - - + +
#DateTypeScoreRésultat
Chargement…
#DateTypeScoreRésultat
Chargement…
@@ -190,59 +177,9 @@ + + - - - diff --git a/Transcendence/srcs/frontend/src/tetris.js b/Transcendence/srcs/frontend/src/tetris/tetris.js similarity index 100% rename from Transcendence/srcs/frontend/src/tetris.js rename to Transcendence/srcs/frontend/src/tetris/tetris.js diff --git a/Transcendence/srcs/frontend/src/tetris/ui.js b/Transcendence/srcs/frontend/src/tetris/ui.js new file mode 100644 index 0000000..0aaf796 --- /dev/null +++ b/Transcendence/srcs/frontend/src/tetris/ui.js @@ -0,0 +1,265 @@ +// ───────────────────────────────────────────── +// UI — Contrôles, socket, duel, matchmaking +// ───────────────────────────────────────────── + +// ── Références DOM ─────────────────────────── + +const btnStart = document.getElementById('btn-start'); +const btnPause = document.getElementById('btn-pause'); +const btnStop = document.getElementById('btn-stop'); +const btnRestart = document.getElementById('btn-restart'); +const overlay = document.getElementById('overlay'); +const inputTTD = document.getElementById('input-ttd'); +const inputHardening = document.getElementById('input-hardening'); +const inputDecrement = document.getElementById('input-decrement'); + +const btnJoinDuel = document.getElementById('btn-join-duel'); +const btnLeaveDuel = document.getElementById('btn-leave-duel'); +const inputRoomCode = document.getElementById('input-room-code'); +const duelStatusEl = document.getElementById('duel-status'); +const opponentSection = document.getElementById('opponent-section'); + +const btnMatchmaking = document.getElementById('btn-matchmaking'); +const btnMatchmakingCancel = document.getElementById('btn-matchmaking-cancel'); +const matchmakingStatusEl = document.getElementById('matchmaking-status'); + +// ── Overlay ────────────────────────────────── + +function showOverlay(title, score) { + document.getElementById('overlay-title').textContent = title; + document.getElementById('overlay-score').textContent = score !== undefined ? `Score : ${score}` : ''; + overlay.classList.add('visible'); +} + +function hideOverlay() { + overlay.classList.remove('visible'); +} + +// ── Boutons ────────────────────────────────── + +function updateButtons() { + btnStart.disabled = game.isRunning; + btnPause.disabled = !game.isRunning; + btnStop.disabled = !game.isRunning; + btnPause.textContent = game.isPaused ? 'Resume' : 'Pause'; + inputTTD.disabled = game.isRunning; + inputHardening.disabled = game.isRunning; + inputDecrement.disabled = game.isRunning; +} + +// ── Socket ─────────────────────────────────── + +const socket = io({ + auth: { token: localStorage.getItem('auth_token') }, + reconnection: true, + reconnectionAttempts: 5, + reconnectionDelay: 1000, + transports: ['websocket', 'polling'] +}); + +// ── Duel ───────────────────────────────────── + +let duel = null; + +// Callbacks passés au Duel pour qu'il pilote l'UI sans accéder à des globaux. +function _makeDuelUI() { + return { + showOverlay, + hideOverlay, + updateButtons, + render: () => render(game), + renderOpponent: (grid, shieldActive) => renderOpponent(grid, shieldActive), + }; +} + +function updateDuelStatus(status, opponentName) { + duelStatusEl.className = ''; + if (status === 'waiting') { + duelStatusEl.textContent = "En attente d'un adversaire…"; + duelStatusEl.classList.add('waiting'); + opponentSection.classList.remove('visible'); + } else if (status === 'ready') { + duelStatusEl.textContent = `Prêt — ${opponentName}`; + duelStatusEl.classList.add('ready'); + opponentSection.classList.add('visible'); + if (duel) duel.hideOpponentOverlay(); + const grid = duel ? duel.opponentGrid : Array.from({ length: 20 }, () => Array(10).fill(0)); + const shieldActive = duel ? duel.opponentShieldActive : false; + renderOpponent(grid, shieldActive); + } else { + duelStatusEl.textContent = '—'; + opponentSection.classList.remove('visible'); + } +} + +function startLocalGame() { + hideOverlay(); + game.start(); + updateButtons(); + render(game); +} + +// Crée un Duel et rejoint la salle — mutualisé entre le bouton et le matchmaking. +function _joinDuelRoom(code) { + if (duel) duel.leave(); + if (game.isRunning) { game.stop(); hideOverlay(); render(game); updateButtons(); } + duel = new Duel(socket, game, updateDuelStatus, startLocalGame, _makeDuelUI()); + duel.join(code); + btnJoinDuel.disabled = true; + btnLeaveDuel.disabled = false; + inputRoomCode.disabled = true; + updateDuelStatus('waiting', null); +} + +btnJoinDuel.addEventListener('click', () => { + const code = inputRoomCode.value.trim().toUpperCase(); + if (!code) return; + _joinDuelRoom(code); +}); + +btnLeaveDuel.addEventListener('click', () => { + if (duel) { duel.leave(); duel = null; } + btnJoinDuel.disabled = false; + btnLeaveDuel.disabled = true; + inputRoomCode.disabled = false; + updateDuelStatus(null, null); +}); + +// ── Matchmaking ────────────────────────────── + +btnMatchmaking.addEventListener('click', () => { + socket.emit('tetris:matchmaking-join'); + btnMatchmaking.disabled = true; + btnMatchmakingCancel.disabled = false; + btnJoinDuel.disabled = true; + matchmakingStatusEl.textContent = 'Recherche en cours…'; + matchmakingStatusEl.className = 'waiting'; +}); + +btnMatchmakingCancel.addEventListener('click', () => { + socket.emit('tetris:matchmaking-leave'); + btnMatchmaking.disabled = false; + btnMatchmakingCancel.disabled = true; + btnJoinDuel.disabled = false; + matchmakingStatusEl.textContent = ''; +}); + +socket.on('tetris:matchmaking-status', (data) => { + if (data.status === 'searching') { + matchmakingStatusEl.textContent = `Recherche… (${data.position} joueur(s) en attente)`; + } else if (data.status === 'idle') { + matchmakingStatusEl.textContent = ''; + btnMatchmaking.disabled = false; + btnMatchmakingCancel.disabled = true; + btnJoinDuel.disabled = false; + } +}); + +socket.on('tetris:matched', (data) => { + matchmakingStatusEl.textContent = `Adversaire trouvé : ${data.opponent} !`; + matchmakingStatusEl.className = 'ready'; + btnMatchmaking.disabled = false; + btnMatchmakingCancel.disabled = true; + inputRoomCode.value = data.roomCode; + _joinDuelRoom(data.roomCode); +}); + +// ── Jeu ────────────────────────────────────── + +function saveTetrisScore(score) { + const token = localStorage.getItem('auth_token'); + if (!token) return; + fetch('/api/stats/tetris/score', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, + body: JSON.stringify({ score }) + }) + .then(r => r.json()) + .then(data => { if (data.bestScore !== undefined) console.log('Meilleur score tetris:', data.bestScore); }) + .catch(err => console.error('Erreur sauvegarde score tetris:', err)); +} + +const game = new Tetris( + // onRender + () => { + if (duel) duel.synchronize_game(); + render(game); + updateButtons(); + }, + // onGameOver + (score, validBlock) => { + if (duel && duel.isReady) duel.onLocalGameOver(score, validBlock); + else saveTetrisScore(score); + render(game); + updateButtons(); + showOverlay('GAME OVER', score); + loadLeaderboards(); + loadGameHistory(); + }, + // onBlockPlaced + (grid) => { if (duel) duel.onLocalBlockPlaced(grid, game.score); }, + // onLinesCleared + (count, holeCol) => { if (duel) duel.onLocalLinesCleared(count, holeCol); }, + // onShieldChanged + (event) => { if (duel) duel.onLocalShieldChanged(event); } +); + +// ── Boutons de contrôle ────────────────────── + +btnStart.addEventListener('click', () => { + if (duel && duel.isReady) duel.startDuel(); + else startLocalGame(); +}); + +btnPause.addEventListener('click', () => { + if (duel && duel.isReady) { + duel.togglePause(); + } else { + game.pause(); + updateButtons(); + if (game.isPaused) showOverlay('PAUSE'); + else hideOverlay(); + } +}); + +btnStop.addEventListener('click', () => { + if (duel && duel.isReady) { + duel.stop(); + } else { + game.stop(); + updateButtons(); + render(game); + showOverlay('STOPPED'); + } +}); + +if (btnRestart) { + btnRestart.addEventListener('click', () => { + if (duel && duel.isReady) return; + game.restart(); + updateButtons(); + render(game); + }); +} + +// ── Paramètres ─────────────────────────────── + +function applySettings() { + const settings = { + timeToDown: parseInt(inputTTD.value, 10), + hardening: parseInt(inputHardening.value, 10), + decrementTTD: parseInt(inputDecrement.value, 10), + }; + game.configure(settings); + if (duel && duel.isReady) duel.syncSettings(settings); +} + +inputTTD.addEventListener('change', applySettings); +inputHardening.addEventListener('change', applySettings); +inputDecrement.addEventListener('change', applySettings); + +// ── Thème ──────────────────────────────────── + +document.querySelectorAll('.theme-btn').forEach(btn => { + btn.addEventListener('click', () => setColorTheme(btn.dataset.theme)); +}); diff --git a/Transcendence/srcs/frontend/src/ui.js b/Transcendence/srcs/frontend/src/ui.js deleted file mode 100644 index 280b07c..0000000 --- a/Transcendence/srcs/frontend/src/ui.js +++ /dev/null @@ -1,412 +0,0 @@ -// ───────────────────────────────────────────── -// UI -// ───────────────────────────────────────────── - -const btnStart = document.getElementById('btn-start'); -const btnPause = document.getElementById('btn-pause'); -const btnStop = document.getElementById('btn-stop'); -const overlay = document.getElementById('overlay'); -const inputTTD = document.getElementById('input-ttd'); -const inputHardening = document.getElementById('input-hardening'); -const inputDecrement = document.getElementById('input-decrement'); - -// Duel UI -const btnJoinDuel = document.getElementById('btn-join-duel'); -const btnLeaveDuel = document.getElementById('btn-leave-duel'); -const inputRoomCode = document.getElementById('input-room-code'); -const duelStatusEl = document.getElementById('duel-status'); -const opponentSection = document.getElementById('opponent-section'); - -// Matchmaking UI -const btnMatchmaking = document.getElementById('btn-matchmaking'); -const btnMatchmakingCancel = document.getElementById('btn-matchmaking-cancel'); -const matchmakingStatusEl = document.getElementById('matchmaking-status'); - -function updateButtons() { - btnStart.disabled = game.isRunning; - btnPause.disabled = !game.isRunning; - btnStop.disabled = !game.isRunning; - btnPause.textContent = game.isPaused ? 'Resume' : 'Pause'; - inputTTD.disabled = game.isRunning; - inputHardening.disabled = game.isRunning; - inputDecrement.disabled = game.isRunning; -} - -function showOverlay(title, score) { - document.getElementById('overlay-title').textContent = title; - document.getElementById('overlay-score').textContent = score !== undefined ? `Score : ${score}` : ''; - overlay.classList.add('visible'); -} - -function hideOverlay() { - overlay.classList.remove('visible'); -} - -// ───────────────────────────────────────────── -// SOCKET + DUEL -// ───────────────────────────────────────────── - -const socket = io({ - auth: { token: localStorage.getItem('auth_token') }, - reconnection: true, - reconnectionAttempts: 5, - reconnectionDelay: 1000, - transports: ['websocket', 'polling'] -}); - -let duel = null; - -function updateDuelStatus(status, opponentName) { - duelStatusEl.className = ''; - if (status === 'waiting') { - duelStatusEl.textContent = 'En attente d\'un adversaire…'; - duelStatusEl.classList.add('waiting'); - opponentSection.classList.remove('visible'); - } else if (status === 'ready') { - duelStatusEl.textContent = `Prêt — ${opponentName}`; - duelStatusEl.classList.add('ready'); - opponentSection.classList.add('visible'); - if (duel) duel.hideOpponentOverlay(); - renderOpponent(duel ? duel.opponentGrid : Array.from({length:20}, () => Array(10).fill(0))); - } else { - duelStatusEl.textContent = '—'; - opponentSection.classList.remove('visible'); - } -} - -function startLocalGame() { - hideOverlay(); - game.start(); - updateButtons(); - render(); -} - -// ───────────────────────────────────────────── -// SCORE SAVE (solo) -// ───────────────────────────────────────────── - -function saveTetrisScore(score) { - const token = localStorage.getItem('auth_token'); - if (!token) return; - fetch('/api/stats/tetris/score', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - }, - body: JSON.stringify({ score }) - }) - .then(r => r.json()) - .then(data => { - if (data.bestScore !== undefined) { - console.log('Meilleur score tetris:', data.bestScore); - } - }) - .catch(err => console.error('Erreur sauvegarde score tetris:', err)); -} - -// ───────────────────────────────────────────── -// DUEL BUTTONS -// ───────────────────────────────────────────── - -btnJoinDuel.addEventListener('click', () => { - const code = inputRoomCode.value.trim().toUpperCase(); - if (!code) return; - if (duel) { duel.leave(); } - if (game.isRunning) { game.stop(); hideOverlay(); render(); updateButtons(); } - duel = new Duel(socket, game, updateDuelStatus, startLocalGame); - duel.join(code); - btnJoinDuel.disabled = true; - btnLeaveDuel.disabled = false; - inputRoomCode.disabled = true; - updateDuelStatus('waiting', null); -}); - -btnLeaveDuel.addEventListener('click', () => { - if (duel) { duel.leave(); duel = null; } - btnJoinDuel.disabled = false; - btnLeaveDuel.disabled = true; - inputRoomCode.disabled = false; - updateDuelStatus(null, null); -}); - -// ───────────────────────────────────────────── -// MATCHMAKING -// ───────────────────────────────────────────── - -btnMatchmaking.addEventListener('click', () => { - socket.emit('tetris:matchmaking-join'); - btnMatchmaking.disabled = true; - btnMatchmakingCancel.disabled = false; - btnJoinDuel.disabled = true; - matchmakingStatusEl.textContent = 'Recherche en cours…'; - matchmakingStatusEl.className = 'waiting'; -}); - -btnMatchmakingCancel.addEventListener('click', () => { - socket.emit('tetris:matchmaking-leave'); - btnMatchmaking.disabled = false; - btnMatchmakingCancel.disabled = true; - btnJoinDuel.disabled = false; - matchmakingStatusEl.textContent = ''; -}); - -socket.on('tetris:matchmaking-status', (data) => { - if (data.status === 'searching') { - matchmakingStatusEl.textContent = `Recherche… (${data.position} joueur(s) en attente)`; - } else if (data.status === 'idle') { - matchmakingStatusEl.textContent = ''; - btnMatchmaking.disabled = false; - btnMatchmakingCancel.disabled = true; - btnJoinDuel.disabled = false; - } -}); - -socket.on('tetris:matched', (data) => { - matchmakingStatusEl.textContent = `Adversaire trouvé : ${data.opponent} !`; - matchmakingStatusEl.className = 'ready'; - btnMatchmaking.disabled = false; - btnMatchmakingCancel.disabled = true; - btnJoinDuel.disabled = false; - - // Auto-rejoindre la salle générée - if (duel) { duel.leave(); } - if (game.isRunning) { game.stop(); hideOverlay(); render(); updateButtons(); } - duel = new Duel(socket, game, updateDuelStatus, startLocalGame); - duel.join(data.roomCode); - inputRoomCode.value = data.roomCode; - btnJoinDuel.disabled = true; - btnLeaveDuel.disabled = false; - inputRoomCode.disabled = true; - updateDuelStatus('waiting', null); -}); - -// ───────────────────────────────────────────── -// INIT -// ───────────────────────────────────────────── - -const game = new Tetris( - // onRender - () => { - if (duel) duel.synchronize_game(); - render(); - updateButtons(); - }, - // onGameOver - (score, validBlock) => { - const isDuel = duel && duel.isReady; - if (isDuel) { - duel.onLocalGameOver(score, validBlock); - } else { - saveTetrisScore(score); - } - render(); - updateButtons(); - showOverlay('GAME OVER', score); - loadLeaderboards(); - loadGameHistory(); - }, - // onBlockPlaced — relay duel - (grid) => { - if (duel) duel.onLocalBlockPlaced(grid, game.score); - }, - // onLinesCleared — relay duel - (count, holeCol) => { - if (duel) duel.onLocalLinesCleared(count, holeCol); - }, - // onShieldChanged — relay duel - (event) => { - if (duel) duel.onLocalShieldChanged(event); - } -); - -btnStart.addEventListener('click', () => { - if (duel && duel.isReady) { - duel.startDuel(); // déclenche les deux parties via le serveur - } else { - startLocalGame(); // solo - } -}); - -btnPause.addEventListener('click', () => { - if (duel && duel.isReady) { - duel.togglePause(); - } else { - game.pause(); - updateButtons(); - if (game.isPaused) showOverlay('PAUSE'); - else hideOverlay(); - } -}); - -btnStop.addEventListener('click', () => { - if (duel && duel.isReady) { - duel.stop(); - } else { - game.stop(); - updateButtons(); - render(); - showOverlay('STOPPED'); - } -}); - -function applySettings() { - const settings = { - timeToDown: parseInt(inputTTD.value, 10), - hardening: parseInt(inputHardening.value, 10), - decrementTTD: parseInt(inputDecrement.value, 10), - }; - game.configure(settings); - if (duel && duel.isReady) duel.syncSettings(settings); -} - -inputTTD.addEventListener('change', applySettings); -inputHardening.addEventListener('change', applySettings); -inputDecrement.addEventListener('change', applySettings); - -const btnRestart = document.getElementById('btn-restart'); -if (btnRestart) { - btnRestart.addEventListener('click', () => { - if (duel && duel.isReady) return; - game.restart(); - updateButtons(); - render(); - }); -} - -// ───────────────────────────────────────────── -// GAME HISTORY -// ───────────────────────────────────────────── - -async function loadGameHistory() { - const token = localStorage.getItem('auth_token'); - if (!token) return; - - try { - const res = await fetch('/api/stats/tetris/history', { - headers: { 'Authorization': `Bearer ${token}` } - }); - if (!res.ok) return; - const history = await res.json(); - renderGameHistory(history); - } catch (err) { - console.error('Erreur chargement historique:', err); - } -} - -function renderGameHistory(history) { - const tbody = document.getElementById('lb-history-body'); - if (!tbody) return; - if (!history.length) { - tbody.innerHTML = 'Aucune partie jouée'; - 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 = 'Victoire'; - if (entry.result === 'loss') resultHtml = 'Défaite'; - return ` - ${i + 1} - ${date} - ${type} - ${entry.score} - ${resultHtml} - `; - }).join(''); -} - -// ───────────────────────────────────────────── -// LEADERBOARDS -// ───────────────────────────────────────────── - -async function loadLeaderboards() { - const token = localStorage.getItem('auth_token'); - if (!token) return; - - const headers = { 'Authorization': `Bearer ${token}` }; - - try { - const [scoresRes, winsRes, meRes, rankScoreRes, rankWinsRes] = await Promise.all([ - fetch('/api/stats/tetris/leaderboard/score', { headers }), - fetch('/api/stats/tetris/leaderboard/wins', { headers }), - fetch('/api/stats/me', { headers }), - fetch('/api/stats/tetris/rank/score', { headers }), - fetch('/api/stats/tetris/rank/wins', { headers }) - ]); - - const me = meRes.ok ? await meRes.json() : null; - const rankScore = rankScoreRes.ok ? (await rankScoreRes.json()).rank : null; - const rankWins = rankWinsRes.ok ? (await rankWinsRes.json()).rank : null; - - if (scoresRes.ok) { - const scores = await scoresRes.json(); - renderLeaderboard('lb-scores-body', scores, ['tetris_best_score', 'tetris_games_played'], me, rankScore); - } - - if (winsRes.ok) { - const wins = await winsRes.json(); - renderLeaderboard('lb-wins-body', wins, ['tetris_wins', 'tetris_games_played'], me, rankWins); - } - } catch (err) { - console.error('Erreur chargement leaderboards:', err); - } -} - -function renderLeaderboard(tbodyId, rows, [col1, col2], me, myRank) { - const tbody = document.getElementById(tbodyId); - if (!tbody) return; - if (!rows.length && !me) { - tbody.innerHTML = 'Aucun résultat'; - 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 ` - ${i + 1} - ${escapeHtml(r.username)}${isMe ? ' (vous)' : ''} - ${r[col1] ?? 0} - ${r[col2] ?? 0} - `; - }).join(''); - - if (!inTop && me && myRank !== null) { - html += `· · ·`; - html += ` - ${myRank} - ${escapeHtml(myUsername)} (vous) - ${me[col1] ?? 0} - ${me[col2] ?? 0} - `; - } - - tbody.innerHTML = html || 'Aucun résultat'; -} - -function escapeHtml(str) { - return String(str).replace(/&/g,'&').replace(//g,'>'); -} - -// Tabs leaderboard -document.querySelectorAll('.lb-tab').forEach(tab => { - tab.addEventListener('click', () => { - document.querySelectorAll('.lb-tab').forEach(t => t.classList.remove('lb-tab--active')); - document.querySelectorAll('.lb-content').forEach(c => c.classList.remove('lb-content--active')); - tab.classList.add('lb-tab--active'); - document.getElementById(`lb-${tab.dataset.tab}`).classList.add('lb-content--active'); - if (tab.dataset.tab === 'history') loadGameHistory(); - }); -}); - -// Chargement initial des leaderboards -loadLeaderboards(); -loadGameHistory(); diff --git a/Transcendence/srcs/frontend/src/avatar.js b/Transcendence/srcs/frontend/src/windows/avatar.js similarity index 97% rename from Transcendence/srcs/frontend/src/avatar.js rename to Transcendence/srcs/frontend/src/windows/avatar.js index 45e3103..4fed6ee 100644 --- a/Transcendence/srcs/frontend/src/avatar.js +++ b/Transcendence/srcs/frontend/src/windows/avatar.js @@ -1,6 +1,6 @@ -import { Window, windowRegistry } from './windows.js'; -import { API, STORAGE_KEYS, CSS } from './config.js'; -import { eventBus, Events } from './events.js'; +import { Window, windowRegistry } from '../core/windows.js'; +import { API, STORAGE_KEYS, CSS } from '../core/config.js'; +import { eventBus, Events } from '../core/events.js'; /** * Avatar management window diff --git a/Transcendence/srcs/frontend/src/friends.js b/Transcendence/srcs/frontend/src/windows/friends.js similarity index 98% rename from Transcendence/srcs/frontend/src/friends.js rename to Transcendence/srcs/frontend/src/windows/friends.js index fc8ca24..fc0f911 100644 --- a/Transcendence/srcs/frontend/src/friends.js +++ b/Transcendence/srcs/frontend/src/windows/friends.js @@ -1,6 +1,6 @@ -import { Window, windowRegistry } from './windows.js'; -import { API, STORAGE_KEYS, CSS } from './config.js'; -import { eventBus, Events } from './events.js'; +import { Window, windowRegistry } from '../core/windows.js'; +import { API, STORAGE_KEYS, CSS } from '../core/config.js'; +import { eventBus, Events } from '../core/events.js'; /** * Friends management window diff --git a/Transcendence/srcs/frontend/src/game_room.js b/Transcendence/srcs/frontend/src/windows/game_room.js similarity index 99% rename from Transcendence/srcs/frontend/src/game_room.js rename to Transcendence/srcs/frontend/src/windows/game_room.js index 362cdae..e40745a 100644 --- a/Transcendence/srcs/frontend/src/game_room.js +++ b/Transcendence/srcs/frontend/src/windows/game_room.js @@ -1,6 +1,6 @@ -import { Window } from './windows.js'; -import { API, STORAGE_KEYS, CSS } from './config.js'; -import { eventBus, Events } from './events.js'; +import { Window } from '../core/windows.js'; +import { API, STORAGE_KEYS, CSS } from '../core/config.js'; +import { eventBus, Events } from '../core/events.js'; export class GameRoomWindow extends Window { constructor() { diff --git a/Transcendence/srcs/frontend/src/global_chat.js b/Transcendence/srcs/frontend/src/windows/global_chat.js similarity index 98% rename from Transcendence/srcs/frontend/src/global_chat.js rename to Transcendence/srcs/frontend/src/windows/global_chat.js index 6e64598..3a15679 100644 --- a/Transcendence/srcs/frontend/src/global_chat.js +++ b/Transcendence/srcs/frontend/src/windows/global_chat.js @@ -1,6 +1,6 @@ -import { Window } from './windows.js'; -import { STORAGE_KEYS, CSS } from './config.js'; -import { eventBus, Events } from './events.js'; +import { Window } from '../core/windows.js'; +import { STORAGE_KEYS, CSS } from '../core/config.js'; +import { eventBus, Events } from '../core/events.js'; /** * Global chat window diff --git a/Transcendence/srcs/frontend/src/login.js b/Transcendence/srcs/frontend/src/windows/login.js similarity index 98% rename from Transcendence/srcs/frontend/src/login.js rename to Transcendence/srcs/frontend/src/windows/login.js index 5708dbd..f601579 100644 --- a/Transcendence/srcs/frontend/src/login.js +++ b/Transcendence/srcs/frontend/src/windows/login.js @@ -1,6 +1,6 @@ -import { Window } from './windows.js'; -import { API, STORAGE_KEYS, CSS } from './config.js'; -import { eventBus, Events } from './events.js'; +import { Window } from '../core/windows.js'; +import { API, STORAGE_KEYS, CSS } from '../core/config.js'; +import { eventBus, Events } from '../core/events.js'; /** * Login and registration window diff --git a/Transcendence/srcs/frontend/src/stats.js b/Transcendence/srcs/frontend/src/windows/stats.js similarity index 97% rename from Transcendence/srcs/frontend/src/stats.js rename to Transcendence/srcs/frontend/src/windows/stats.js index 0a55cbe..e490a1f 100644 --- a/Transcendence/srcs/frontend/src/stats.js +++ b/Transcendence/srcs/frontend/src/windows/stats.js @@ -1,5 +1,5 @@ -import { Window } from './windows.js'; -import { API, STORAGE_KEYS } from './config.js'; +import { Window } from '../core/windows.js'; +import { API, STORAGE_KEYS } from '../core/config.js'; /** * Stats window — displays Scribble + Tetris stats for any user