diff --git a/public/index.html b/public/index.html index 3a38cdb..79149e6 100644 --- a/public/index.html +++ b/public/index.html @@ -7,7 +7,7 @@ - +
@@ -61,10 +61,10 @@
- - - - - + + + + + diff --git a/public/js/game.js b/public/js/game.js index 99ac2c3..3885664 100644 --- a/public/js/game.js +++ b/public/js/game.js @@ -280,6 +280,16 @@ class TetrisGame { this.dropInterval = Math.max(100, 1000 - (this.level - 1) * 100); } + getGhostY() { + if (!this.currentPiece) return null; + + let ghostY = this.currentPiece.y; + while (this.isValidPosition(this.currentPiece.x, ghostY + 1, this.currentPiece.shape)) { + ghostY++; + } + return ghostY; + } + receiveGarbage(rows) { if (this.gameOver) return; @@ -328,10 +338,12 @@ class TetrisGame { } getState() { + const ghostY = this.currentPiece ? this.getGhostY() : null; return { playerId: this.playerId, board: JSON.parse(JSON.stringify(this.board)), currentPiece: this.currentPiece ? JSON.parse(JSON.stringify(this.currentPiece)) : null, + ghostY: ghostY, nextPiece: this.nextPiece ? JSON.parse(JSON.stringify(this.nextPiece)) : null, holdPiece: this.holdPiece ? JSON.parse(JSON.stringify(this.holdPiece)) : null, canHold: this.canHold, diff --git a/public/js/renderer.js b/public/js/renderer.js index 0b70784..b5cd111 100644 --- a/public/js/renderer.js +++ b/public/js/renderer.js @@ -98,6 +98,11 @@ class TetrisRenderer { this.drawBoard(ctx, gameState.board); } + // Draw ghost piece (landing preview) + if (gameState && gameState.currentPiece && !gameState.gameOver && gameState.ghostY !== null) { + this.drawGhostPiece(ctx, gameState.currentPiece, gameState.ghostY); + } + // Draw current piece if (gameState && gameState.currentPiece && !gameState.gameOver) { this.drawPiece(ctx, gameState.currentPiece); @@ -210,6 +215,34 @@ class TetrisRenderer { ctx.strokeRect(px, py, CELL_SIZE, CELL_SIZE); } + drawGhostPiece(ctx, piece, ghostY) { + for (let row = 0; row < piece.shape.length; row++) { + for (let col = 0; col < piece.shape[row].length; col++) { + if (piece.shape[row][col]) { + const x = piece.x + col; + const y = ghostY + row; + if (y >= 0) { + this.drawGhostCell(ctx, x, y, piece.color); + } + } + } + } + } + + drawGhostCell(ctx, x, y, color) { + const px = x * CELL_SIZE; + const py = y * CELL_SIZE; + + // Semi-transparent fill with outlined border effect + ctx.strokeStyle = color + '80'; // 50% opacity border + ctx.lineWidth = 2; + ctx.strokeRect(px + 2, py + 2, CELL_SIZE - 4, CELL_SIZE - 4); + + // Light fill inside + ctx.fillStyle = color + '20'; // 12.5% opacity + ctx.fillRect(px + 3, py + 3, CELL_SIZE - 6, CELL_SIZE - 6); + } + drawNextPiece(ctx, piece) { // Clear ctx.fillStyle = '#000'; diff --git a/server/index.js b/server/index.js index 85a8a8c..abc61a2 100644 --- a/server/index.js +++ b/server/index.js @@ -338,6 +338,16 @@ function addGarbageToPlayer(player) { // Elimination is handled by spawnPiece when a new piece can't fit } +function getGhostY(piece, board) { + if (!piece) return null; + + let ghostY = piece.y; + while (isValidPosition(piece, piece.x, ghostY + 1, board)) { + ghostY++; + } + return ghostY; +} + function startGame() { lobby.gameStarted = true; @@ -391,6 +401,7 @@ function getStates() { playerId: p.id, board: JSON.parse(JSON.stringify(p.board)), currentPiece: p.currentPiece ? JSON.parse(JSON.stringify(p.currentPiece)) : null, + ghostY: p.currentPiece ? getGhostY(p.currentPiece, p.board) : null, nextPiece: p.nextPiece ? JSON.parse(JSON.stringify(p.nextPiece)) : null, holdPiece: p.holdPiece ? JSON.parse(JSON.stringify(p.holdPiece)) : null, canHold: p.canHold,