From 72bc9ea62843eb94338d459698160175f63aef8e Mon Sep 17 00:00:00 2001 From: H3XploR <73852348+H3XploR@users.noreply.github.com> Date: Thu, 19 Mar 2026 14:38:56 +0100 Subject: [PATCH] added shield --- Transcendence/srcs/backend/services/socket.js | 10 ++++ Transcendence/srcs/frontend/src/duel.js | 36 +++++++++-- Transcendence/srcs/frontend/src/renderer.js | 51 ++++++++++++++++ Transcendence/srcs/frontend/src/tetris.css | 33 ++++++++++ Transcendence/srcs/frontend/src/tetris.html | 8 +++ Transcendence/srcs/frontend/src/tetris.js | 60 ++++++++++++++++++- Transcendence/srcs/frontend/src/ui.js | 4 ++ 7 files changed, 194 insertions(+), 8 deletions(-) diff --git a/Transcendence/srcs/backend/services/socket.js b/Transcendence/srcs/backend/services/socket.js index 6fe5e7c..39a0780 100644 --- a/Transcendence/srcs/backend/services/socket.js +++ b/Transcendence/srcs/backend/services/socket.js @@ -730,6 +730,16 @@ function setupSocketIO(io) _tetrisRelayToOpponent(socket, 'tetris:lines-cleared', data); }); + // Relay pur : shield-activated → adversaire uniquement + socket.on('tetris:shield-activated', () => { + _tetrisRelayToOpponent(socket, 'tetris:shield-activated', {}); + }); + + // Relay pur : shield-deactivated → adversaire uniquement + socket.on('tetris:shield-deactivated', () => { + _tetrisRelayToOpponent(socket, 'tetris:shield-deactivated', {}); + }); + // start-duel → relayé aux DEUX joueurs de la room (inclut l'émetteur) socket.on('tetris:start-duel', () => { const code = socket.tetrisRoomCode; diff --git a/Transcendence/srcs/frontend/src/duel.js b/Transcendence/srcs/frontend/src/duel.js index 70acee1..745f4e7 100644 --- a/Transcendence/srcs/frontend/src/duel.js +++ b/Transcendence/srcs/frontend/src/duel.js @@ -9,11 +9,12 @@ class Duel { this.onStatusChange = onStatusChange; // (status, opponentName) => void this.onStart = onStart; // () => void — déclenche le début du jeu local - this.action_queue = []; - this.opponentGrid = this._emptyGrid(); - this.opponentScore = 0; - this.roomCode = null; - this.isReady = false; + this.action_queue = []; + this.opponentGrid = this._emptyGrid(); + this.opponentScore = 0; + this.opponentShieldActive = false; + this.roomCode = null; + this.isReady = false; this._bindSocketEvents(); } @@ -60,6 +61,15 @@ class Duel { this.endDuel(); } + onLocalShieldChanged(event) { + if (!this.isReady) return; + if (event === 'activated') { + this.socket.emit('tetris:shield-activated'); + } else if (event === 'deactivated') { + this.socket.emit('tetris:shield-deactivated'); + } + } + endDuel() { this.isReady = false; this.action_queue = []; @@ -92,6 +102,14 @@ class Duel { showOverlay('YOU WIN', action.score); this.endDuel(); break; + + case 'OPPONENT_SHIELD_ACTIVATED': + this.opponentShieldActive = true; + break; + + case 'OPPONENT_SHIELD_DEACTIVATED': + this.opponentShieldActive = false; + break; } } @@ -127,6 +145,14 @@ class Duel { this.action_queue.push({ type: 'OPPONENT_GAME_OVER', score: data.score, validBlock: data.validBlock }); }); + this.socket.on('tetris:shield-activated', () => { + this.action_queue.push({ type: 'OPPONENT_SHIELD_ACTIVATED' }); + }); + + this.socket.on('tetris:shield-deactivated', () => { + this.action_queue.push({ type: 'OPPONENT_SHIELD_DEACTIVATED' }); + }); + this.socket.on('tetris:start-duel', () => { if (this.onStart) this.onStart(); }); diff --git a/Transcendence/srcs/frontend/src/renderer.js b/Transcendence/srcs/frontend/src/renderer.js index 66a4df6..b3efcfc 100644 --- a/Transcendence/srcs/frontend/src/renderer.js +++ b/Transcendence/srcs/frontend/src/renderer.js @@ -139,6 +139,17 @@ function drawMiniPiece(ctx, piece, canvasW, canvasH) { 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() { // Grille principale clearCanvas(ctxMain, 300, 600); @@ -161,12 +172,39 @@ 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) { + 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) { @@ -176,6 +214,19 @@ function renderOpponent(opponentGrid) { for (let x = 0; x < opponentGrid[y].length; x++) if (opponentGrid[y][x] !== 0) 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 diff --git a/Transcendence/srcs/frontend/src/tetris.css b/Transcendence/srcs/frontend/src/tetris.css index 6e574a9..36de98c 100644 --- a/Transcendence/srcs/frontend/src/tetris.css +++ b/Transcendence/srcs/frontend/src/tetris.css @@ -651,3 +651,36 @@ button:disabled { opacity: 0.3; cursor: not-allowed; } } body { overflow: hidden; } + + +/* ── Shield ───────────────────────────────── */ +.shield-bar-bg { + width: 100%; + height: 4px; + background: rgba(0,212,255,0.15); + border-radius: 2px; + margin-top: 4px; + overflow: hidden; +} + +.shield-bar { + height: 100%; + background: #00d4ff; + border-radius: 2px; + transition: width 0.1s linear; + box-shadow: 0 0 6px #00d4ff; +} + +.shield-ready { color: #00d4ff !important; } +.shield-active { color: #00ffff !important; text-shadow: 0 0 8px #00ffff; } +.shield-cooldown { color: var(--dim) !important; } + +kbd { + display: inline-block; + padding: 0 3px; + border: 1px solid var(--border); + border-radius: 2px; + font-size: 0.6rem; + font-family: inherit; + color: var(--dim); +} diff --git a/Transcendence/srcs/frontend/src/tetris.html b/Transcendence/srcs/frontend/src/tetris.html index ec678a5..970ce88 100644 --- a/Transcendence/srcs/frontend/src/tetris.html +++ b/Transcendence/srcs/frontend/src/tetris.html @@ -51,6 +51,12 @@
0
+
+
Shield E
+
PRÊT
+
+
+
@@ -106,6 +112,7 @@
W Rot. droite
Espace Drop
C Hold
+
E Shield
@@ -120,6 +127,7 @@
Score
+
diff --git a/Transcendence/srcs/frontend/src/tetris.js b/Transcendence/srcs/frontend/src/tetris.js index cf6962b..ec40e7f 100644 --- a/Transcendence/srcs/frontend/src/tetris.js +++ b/Transcendence/srcs/frontend/src/tetris.js @@ -3,11 +3,12 @@ // ─────────────────────────────────────────── class Tetris { - constructor(onRender, onGameOver, onBlockPlaced = null, onLinesCleared = null) { + constructor(onRender, onGameOver, onBlockPlaced = null, onLinesCleared = null, onShieldChanged = null) { this.onRender = onRender; this.onGameOver = onGameOver; this.onBlockPlaced = onBlockPlaced; this.onLinesCleared = onLinesCleared; + this.onShieldChanged = onShieldChanged; this.grid = this._createGrid(10, 20); this.bufferGrid = this._createGrid(10, 5); @@ -28,6 +29,12 @@ class Tetris { this.isPaused = false; 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.lastTime = 0; this.accumulator = 0; @@ -55,6 +62,10 @@ class Tetris { this.timeToDown = this.initialTimeToDown; this.storedPiece = null; this.canStore = true; + this.shieldActive = false; + this.shieldActiveMs = 0; + this.shieldCooldownMs = 0; + this.shieldReady = true; this._spawnNewPiece(); document.addEventListener('keydown', this._keyHandler); this._startGameLoop(); @@ -108,6 +119,8 @@ class Tetris { this.lastTime = currentTime; this.accumulator += deltaTime; + this._updateShield(deltaTime); + while (this.isRunning && this.accumulator >= this.timeToDown) { this._tick(); this.accumulator -= this.timeToDown; @@ -174,11 +187,42 @@ class Tetris { e.preventDefault(); if (!this.isPaused) this._storePiece(); break; + case 'e': case 'E': + e.preventDefault(); + if (!this.isPaused) this._activateShield(); + break; } 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() { if (!this.currentPiece) return; let dist = 0; @@ -275,8 +319,17 @@ class Tetris { const points = [0, 100, 300, 500, 800]; this.score += points[cleared]; this.count += points[cleared]; - if (this.onLinesCleared && cleared > 0) - this.onLinesCleared(cleared, this.lastLandingCol); + if (cleared > 0) { + // 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() { @@ -361,6 +414,7 @@ class Tetris { } addGarbageLines(lines) { + if (this.shieldActive) return; // shield bloque les lignes garbage if (!this.isRunning || !lines.length) return; this.grid.splice(0, lines.length); for (const line of lines) this.grid.push([...line]); // ...line pour faire une copie independante diff --git a/Transcendence/srcs/frontend/src/ui.js b/Transcendence/srcs/frontend/src/ui.js index af7db65..280b07c 100644 --- a/Transcendence/srcs/frontend/src/ui.js +++ b/Transcendence/srcs/frontend/src/ui.js @@ -213,6 +213,10 @@ const game = new Tetris( // onLinesCleared — relay duel (count, holeCol) => { if (duel) duel.onLocalLinesCleared(count, holeCol); + }, + // onShieldChanged — relay duel + (event) => { + if (duel) duel.onLocalShieldChanged(event); } );