added shield
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -139,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);
|
||||||
@@ -161,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) {
|
||||||
@@ -176,6 +214,19 @@ 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
|
// Restore saved theme
|
||||||
|
|||||||
@@ -651,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>
|
||||||
@@ -106,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>
|
||||||
|
|
||||||
@@ -120,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;">🛡 SHIELD ACTIF</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="opponent-wrapper">
|
<div id="opponent-wrapper">
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -213,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);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user