diff --git a/src/game.js b/src/game.js index 4a24ba7..c6decc9 100644 --- a/src/game.js +++ b/src/game.js @@ -28,6 +28,11 @@ export class Game { this.debugMode = false; this.lastProximityWarning = 0; + // Move tracking for replay + this.playerMoves = []; + this.gameStartTime = null; + this.currentMazeLayout = null; + this.setupControls(); this.bindEvents(); } @@ -142,6 +147,9 @@ export class Game { if (this.maze.canMoveTo(newX, newY)) { this.player.moveTo(newX, newY); + // Track player move for replay + this.trackMove(newX, newY); + // Check if player stepped on a visual danger square if (this.maze.isDangerousVisual(newX, newY)) { this.handleVisualDanger(); @@ -159,6 +167,17 @@ export class Game { } } + trackMove(x, y) { + // Track player move with timestamp + const timestamp = Date.now(); + this.playerMoves.push({ + x: x, + y: y, + timestamp: timestamp, + relativeTime: timestamp - (this.gameStartTime || timestamp) + }); + } + handleVisualDanger() { // Visual danger squares end the game this.gameOver(); @@ -179,8 +198,7 @@ export class Game { } setTimeout(() => { - alert('Game Over! You stepped on a dangerous square.\nReturning to start...'); - this.resetGame(); + this.showReplay('Game Over! You stepped on a dangerous square.'); }, 500); } @@ -194,14 +212,153 @@ export class Game { } setTimeout(() => { - alert('🎉 Congratulations! You survived the danger field!\nGenerating new challenge...'); - this.resetGame(); + this.showReplay('🎉 Congratulations! You survived the danger field!'); }, 500); } + showReplay(message) { + // Create and show replay modal + this.createReplayModal(message); + this.startReplayAnimation(); + } + + createReplayModal(message) { + // Remove existing modal if any + const existingModal = document.getElementById('replayModal'); + if (existingModal) { + existingModal.remove(); + } + + // Create modal HTML + const modal = document.createElement('div'); + modal.id = 'replayModal'; + modal.innerHTML = ` +
+
+

${message}

+
+

Moves: ${this.playerMoves.length}

+

Time: ${Math.round((Date.now() - this.gameStartTime) / 1000)}s

+
+ +
+ + +
+
+
+ `; + + document.body.appendChild(modal); + + // Add event listeners + document.getElementById('restartBtn').addEventListener('click', () => { + this.closeReplay(); + this.resetGame(); + }); + + document.getElementById('closeReplayBtn').addEventListener('click', () => { + this.closeReplay(); + }); + } + + startReplayAnimation() { + const replayCanvas = document.getElementById('replayCanvas'); + const replayCtx = replayCanvas.getContext('2d'); + + if (!replayCanvas || !replayCtx) return; + + let currentMoveIndex = 0; + const animationSpeed = 300; // ms per move + + // Draw initial state + this.drawReplayFrame(replayCtx, replayCanvas, -1); + + // Animate player moves + const animateMove = () => { + if (currentMoveIndex < this.playerMoves.length) { + this.drawReplayFrame(replayCtx, replayCanvas, currentMoveIndex); + currentMoveIndex++; + setTimeout(animateMove, animationSpeed); + } + }; + + // Start animation after a short delay + setTimeout(animateMove, 1000); + } + + drawReplayFrame(ctx, canvas, moveIndex) { + // Clear canvas + ctx.fillStyle = '#1a1a1a'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Draw maze with all audio dangers visible + this.maze.render(ctx, this.cellSize, true); // Force debug mode for replay + + // Draw path up to current move + ctx.strokeStyle = '#00ff00'; + ctx.lineWidth = 4; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + if (moveIndex >= 0 && this.playerMoves.length > 0) { + ctx.beginPath(); + // Start from initial position + ctx.moveTo(1 * this.cellSize + this.cellSize/2, 1 * this.cellSize + this.cellSize/2); + + // Draw line to each move up to current index + for (let i = 0; i <= moveIndex && i < this.playerMoves.length; i++) { + const move = this.playerMoves[i]; + ctx.lineTo(move.x * this.cellSize + this.cellSize/2, move.y * this.cellSize + this.cellSize/2); + } + ctx.stroke(); + } + + // Draw current player position + if (moveIndex >= 0 && moveIndex < this.playerMoves.length) { + const currentMove = this.playerMoves[moveIndex]; + ctx.fillStyle = '#ffff00'; + ctx.beginPath(); + ctx.arc( + currentMove.x * this.cellSize + this.cellSize/2, + currentMove.y * this.cellSize + this.cellSize/2, + this.cellSize * 0.3, + 0, + Math.PI * 2 + ); + ctx.fill(); + } else if (moveIndex === -1) { + // Draw initial position + ctx.fillStyle = '#ffff00'; + ctx.beginPath(); + ctx.arc( + 1 * this.cellSize + this.cellSize/2, + 1 * this.cellSize + this.cellSize/2, + this.cellSize * 0.3, + 0, + Math.PI * 2 + ); + ctx.fill(); + } + } + + closeReplay() { + const modal = document.getElementById('replayModal'); + if (modal) { + modal.remove(); + } + } + resetGame() { this.maze.generate(); this.player.moveTo(1, 1); + + // Reset move tracking + this.playerMoves = []; + this.gameStartTime = Date.now(); + + // Store current maze layout for replay + this.currentMazeLayout = this.maze.cloneMaze(); } update(deltaTime) { @@ -254,6 +411,12 @@ export class Game { start() { this.running = true; this.maze.generate(); + + // Initialize move tracking + this.playerMoves = []; + this.gameStartTime = Date.now(); + this.currentMazeLayout = this.maze.cloneMaze(); + this.lastTime = performance.now(); requestAnimationFrame((time) => this.gameLoop(time)); } diff --git a/src/maze.js b/src/maze.js index 01536e3..f19ec2b 100644 --- a/src/maze.js +++ b/src/maze.js @@ -372,4 +372,16 @@ export class Maze { } } } + + cloneMaze() { + // Create a deep copy of the current maze state for replay + return { + grid: this.grid.map(row => [...row]), + dangerousSquares: new Set(this.dangerousSquares), + exitX: this.exitX, + exitY: this.exitY, + cols: this.cols, + rows: this.rows + }; + } } diff --git a/src/style.css b/src/style.css index b133eb5..4ee8228 100644 --- a/src/style.css +++ b/src/style.css @@ -203,3 +203,117 @@ h1 { gap: 0.5rem; } } + +/* Replay Modal */ +#replayModal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1000; +} + +.replay-backdrop { + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + display: flex; + align-items: center; + justify-content: center; + backdrop-filter: blur(5px); +} + +.replay-content { + background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%); + border-radius: 15px; + padding: 2rem; + max-width: 90vw; + max-height: 90vh; + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.7); + border: 2px solid rgba(255, 255, 255, 0.1); + text-align: center; + overflow-y: auto; +} + +.replay-content h2 { + margin-top: 0; + margin-bottom: 1rem; + font-size: 2rem; + color: #fff; +} + +.replay-stats { + display: flex; + justify-content: center; + gap: 2rem; + margin-bottom: 1.5rem; +} + +.replay-stats p { + margin: 0; + font-size: 1.2rem; + color: #81C784; + font-weight: bold; +} + +#replayCanvas { + border: 3px solid rgba(255, 255, 255, 0.2); + border-radius: 10px; + background: #1a1a1a; + box-shadow: + 0 0 20px rgba(0, 0, 0, 0.5), + inset 0 0 20px rgba(255, 255, 255, 0.05); + margin: 1rem 0; + max-width: 100%; + height: auto; +} + +.replay-controls { + display: flex; + gap: 1rem; + justify-content: center; + margin-top: 1.5rem; +} + +.replay-controls button { + background: linear-gradient(45deg, #4CAF50, #45a049); + color: white; + border: none; + padding: 0.8rem 1.5rem; + border-radius: 25px; + font-size: 1rem; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(76, 175, 80, 0.3); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.replay-controls button:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(76, 175, 80, 0.4); + background: linear-gradient(45deg, #45a049, #4CAF50); +} + +.replay-controls button:active { + transform: translateY(0); + box-shadow: 0 2px 10px rgba(76, 175, 80, 0.3); +} + +@media (max-width: 768px) { + .replay-content { + padding: 1rem; + margin: 1rem; + } + + .replay-stats { + flex-direction: column; + gap: 0.5rem; + } + + .replay-controls { + flex-direction: column; + gap: 0.5rem; + } +} \ No newline at end of file