Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>NEON-TRIS | Cyberpunk Tetris</title> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); | |
| :root { | |
| --neon-pink: #ff2a6d; | |
| --neon-blue: #05d9e8; | |
| --neon-purple: #d300c5; | |
| --neon-green: #00ff9d; | |
| --dark-bg: #0d0221; | |
| --grid-bg: rgba(13, 2, 33, 0.7); | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| body { | |
| font-family: 'Press Start 2P', cursive; | |
| background-color: var(--dark-bg); | |
| color: var(--neon-blue); | |
| overflow: hidden; | |
| height: 100vh; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| position: relative; | |
| } | |
| .cyber-grid { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: | |
| linear-gradient(rgba(5, 217, 232, 0.1) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(5, 217, 232, 0.1) 1px, transparent 1px); | |
| background-size: 30px 30px; | |
| z-index: -1; | |
| opacity: 0.3; | |
| } | |
| .cyber-lines { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: | |
| linear-gradient(135deg, var(--neon-purple) 0%, transparent 50%), | |
| linear-gradient(-135deg, var(--neon-green) 0%, transparent 50%); | |
| z-index: -2; | |
| opacity: 0.2; | |
| } | |
| .game-container { | |
| display: flex; | |
| gap: 30px; | |
| align-items: flex-start; | |
| position: relative; | |
| } | |
| #game-board { | |
| border: 5px solid var(--neon-purple); | |
| border-radius: 3px; | |
| box-shadow: 0 0 20px var(--neon-purple), | |
| 0 0 40px rgba(211, 0, 197, 0.3); | |
| background-color: var(--grid-bg); | |
| } | |
| .side-panel { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 30px; | |
| min-width: 200px; | |
| } | |
| .panel { | |
| border: 3px solid var(--neon-blue); | |
| border-radius: 3px; | |
| padding: 15px; | |
| box-shadow: 0 0 15px var(--neon-blue), | |
| 0 0 30px rgba(5, 217, 232, 0.3); | |
| background-color: var(--grid-bg); | |
| text-align: center; | |
| } | |
| h1 { | |
| font-size: 24px; | |
| margin-bottom: 15px; | |
| color: var(--neon-pink); | |
| text-shadow: 0 0 10px var(--neon-pink); | |
| letter-spacing: 2px; | |
| } | |
| .score-display { | |
| font-size: 22px; | |
| margin: 15px 0; | |
| color: var(--neon-green); | |
| text-shadow: 0 0 8px var(--neon-green); | |
| } | |
| .level-display { | |
| font-size: 18px; | |
| margin: 10px 0; | |
| color: var(--neon-blue); | |
| text-shadow: 0 0 8px var(--neon-blue); | |
| } | |
| #next-piece { | |
| width: 120px; | |
| height: 120px; | |
| margin: 0 auto; | |
| position: relative; | |
| } | |
| button { | |
| background-color: transparent; | |
| border: 2px solid var(--neon-green); | |
| color: var(--neon-green); | |
| font-family: 'Press Start 2P', cursive; | |
| padding: 10px; | |
| margin: 5px 0; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| text-shadow: 0 0 5px var(--neon-green); | |
| } | |
| button:hover { | |
| background-color: var(--neon-green); | |
| color: var(--dark-bg); | |
| box-shadow: 0 0 15px var(--neon-green); | |
| } | |
| .controls { | |
| margin-top: 20px; | |
| } | |
| .controls p { | |
| font-size: 12px; | |
| margin: 10px 0; | |
| color: var(--neon-blue); | |
| line-height: 1.6; | |
| } | |
| .neon-text { | |
| animation: flicker 1.5s infinite alternate; | |
| } | |
| .game-over { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: rgba(13, 2, 33, 0.9); | |
| display: none; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 10; | |
| } | |
| .game-over h1 { | |
| font-size: 36px; | |
| margin-bottom: 30px; | |
| color: var(--neon-pink); | |
| text-shadow: 0 0 15px var(--neon-pink); | |
| } | |
| .scanline { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient( | |
| to bottom, | |
| transparent 0%, | |
| rgba(0, 255, 157, 0.1) 50%, | |
| transparent 100% | |
| ); | |
| background-size: 100% 8px; | |
| pointer-events: none; | |
| z-index: 0; | |
| animation: scanline 8s linear infinite; | |
| } | |
| @keyframes flicker { | |
| 0%, 19%, 21%, 23%, 25%, 54%, 56%, 100% { | |
| text-shadow: 0 0 10px var(--neon-pink), | |
| 0 0 20px var(--neon-pink), | |
| 0 0 30px var(--neon-purple), | |
| 0 0 40px rgba(255, 42, 109, 0.5); | |
| } | |
| 20%, 24%, 55% { | |
| text-shadow: none; | |
| } | |
| } | |
| @keyframes scanline { | |
| from { | |
| transform: translateY(-100%); | |
| } | |
| to { | |
| transform: translateY(100%); | |
| } | |
| } | |
| .glitch { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2 2"><rect width="1" height="1" fill="%2305d9e8" opacity="0.05"/></svg>') repeat; | |
| z-index: -1; | |
| opacity: 0.2; | |
| animation: glitch 10s infinite; | |
| pointer-events: none; | |
| mix-blend-mode: overlay; | |
| } | |
| @keyframes glitch { | |
| 0% { transform: translate(0); } | |
| 20% { transform: translate(-1px, 1px); } | |
| 40% { transform: translate(-1px, -1px); } | |
| 60% { transform: translate(1px, 1px); } | |
| 80% { transform: translate(1px, -1px); } | |
| 100% { transform: translate(0); } | |
| } | |
| /* Responsive adjustments */ | |
| @media (max-width: 768px) { | |
| .game-container { | |
| flex-direction: column; | |
| align-items: center; | |
| } | |
| .side-panel { | |
| min-width: 300px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="cyber-grid"></div> | |
| <div class="cyber-lines"></div> | |
| <div class="scanline"></div> | |
| <div class="glitch"></div> | |
| <div class="game-container"> | |
| <canvas id="game-board" width="300" height="600"></canvas> | |
| <div class="side-panel"> | |
| <div class="panel"> | |
| <h1 class="neon-text">NEON-TRIS</h1> | |
| <div class="score-display" id="score">00000</div> | |
| <div class="level-display">LEVEL: <span id="level">1</span></div> | |
| <div class="level-display">LINES: <span id="lines">0</span></div> | |
| </div> | |
| <div class="panel"> | |
| <h1>NEXT</h1> | |
| <canvas id="next-piece" width="120" height="120"></canvas> | |
| </div> | |
| <div class="panel"> | |
| <button id="start-btn">NEW GAME</button> | |
| <button id="pause-btn">PAUSE</button> | |
| <div class="controls"> | |
| <p>← → : MOVE</p> | |
| <p>↑ : ROTATE</p> | |
| <p>↓ : SOFT DROP</p> | |
| <p>SPACE : HARD DROP</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="game-over" id="game-over"> | |
| <h1 class="neon-text">GAME OVER</h1> | |
| <button id="restart-btn">RETRY</button> | |
| </div> | |
| <audio id="move-sound" src="data:audio/wav;base64,UklGRl9vT19XQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YU..."></audio> | |
| <audio id="rotate-sound" src="data:audio/wav;base64,UklGRl9vT19XQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0Y..."></audio> | |
| <audio id="drop-sound" src="data:audio/wav;base64,UklGRl9vT19XQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0Y..."></audio> | |
| <audio id="clear-sound" src="data:audio/wav;base64,UklGRl9vT19XQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0Y..."></audio> | |
| <audio id="gameover-sound" src="data:audio/wav;base64,UklGRl9vT19XQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0Y..."></audio> | |
| <script> | |
| // 8-bit sound effects as base64 encoded strings | |
| function createSound(src) { | |
| const audio = new Audio(); | |
| audio.src = src; | |
| return audio; | |
| } | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Game variables | |
| const canvas = document.getElementById('game-board'); | |
| const ctx = canvas.getContext('2d'); | |
| const nextCanvas = document.getElementById('next-piece'); | |
| const nextCtx = nextCanvas.getContext('2d'); | |
| // Sound effects (simplified base64 strings - in a real app these would be full sound files) | |
| const sounds = { | |
| move: document.getElementById('move-sound'), | |
| rotate: document.getElementById('rotate-sound'), | |
| drop: document.getElementById('drop-sound'), | |
| clear: document.getElementById('clear-sound'), | |
| gameover: document.getElementById('gameover-sound'), | |
| }; | |
| // For demonstration, we'll just play short beeps | |
| function playSound(type) { | |
| try { | |
| sounds[type].currentTime = 0; | |
| sounds[type].play(); | |
| } catch(e) { | |
| console.log("Sound error:", e); | |
| // Fallback to simple beep | |
| const oscillator = new (window.AudioContext || window.webkitAudioContext)().createOscillator(); | |
| const gainNode = new (window.AudioContext || window.webkitAudioContext)().createGain(); | |
| oscillator.connect(gainNode); | |
| gainNode.connect((window.AudioContext || window.webkitAudioContext)().destination); | |
| if (type === 'move') { | |
| oscillator.frequency.value = 200; | |
| gainNode.gain.value = 0.1; | |
| } else if (type === 'rotate') { | |
| oscillator.frequency.value = 300; | |
| gainNode.gain.value = 0.1; | |
| } else if (type === 'drop') { | |
| oscillator.frequency.value = 100; | |
| gainNode.gain.value = 0.2; | |
| } else if (type === 'clear') { | |
| oscillator.frequency.value = 500; | |
| gainNode.gain.value = 0.3; | |
| } else if (type === 'gameover') { | |
| oscillator.frequency.value = 150; | |
| gainNode.gain.value = 0.3; | |
| } | |
| oscillator.start(); | |
| oscillator.stop((window.AudioContext || window.webkitAudioContext)().currentTime + 0.1); | |
| } | |
| } | |
| // Game constants | |
| const COLS = 10; | |
| const ROWS = 20; | |
| const BLOCK_SIZE = 30; | |
| const COLORS = [ | |
| 'rgba(255, 42, 109, 0)', | |
| 'rgba(255, 42, 109, 1)', // I | |
| 'rgba(5, 217, 232, 1)', // J | |
| 'rgba(211, 0, 197, 1)', // L | |
| 'rgba(0, 255, 157, 1)', // O | |
| 'rgba(255, 204, 0, 1)', // S | |
| 'rgba(255, 128, 0, 1)', // T | |
| 'rgba(180, 0, 255, 1)' // Z | |
| ]; | |
| const SHAPES = [ | |
| [], | |
| [[0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0]], // I | |
| [[2, 0, 0], [2, 2, 2], [0, 0, 0]], // J | |
| [[0, 0, 3], [3, 3, 3], [0, 0, 0]], // L | |
| [[0, 4, 4], [0, 4, 4], [0, 0, 0]], // O | |
| [[0, 5, 5], [5, 5, 0], [0, 0, 0]], // S | |
| [[0, 6, 0], [6, 6, 6], [0, 0, 0]], // T | |
| [[7, 7, 0], [0, 7, 7], [0, 0, 0]] // Z | |
| ]; | |
| // Game state | |
| let board = Array(ROWS).fill().map(() => Array(COLS).fill(0)); | |
| let piece = null; | |
| let nextPiece = null; | |
| let score = 0; | |
| let lines = 0; | |
| let level = 1; | |
| let gameOver = false; | |
| let isPaused = false; | |
| let dropCounter = 0; | |
| let dropInterval = 1000; | |
| let lastTime = 0; | |
| // UI elements | |
| const scoreElement = document.getElementById('score'); | |
| const levelElement = document.getElementById('level'); | |
| const linesElement = document.getElementById('lines'); | |
| const startButton = document.getElementById('start-btn'); | |
| const pauseButton = document.getElementById('pause-btn'); | |
| const restartButton = document.getElementById('restart-btn'); | |
| const gameOverScreen = document.getElementById('game-over'); | |
| // Initialize the game | |
| function init() { | |
| board = Array(ROWS).fill().map(() => Array(COLS).fill(0)); | |
| score = 0; | |
| lines = 0; | |
| level = 1; | |
| gameOver = false; | |
| isPaused = false; | |
| dropInterval = 1000; | |
| updateScore(); | |
| createNextPiece(); | |
| spawnPiece(); | |
| draw(); | |
| } | |
| // Create a new random piece | |
| function createNextPiece() { | |
| const shapeId = Math.floor(Math.random() * 7) + 1; | |
| const shape = SHAPES[shapeId]; | |
| nextPiece = { | |
| shapeId, | |
| shape, | |
| pos: {x: 0, y: 0}, | |
| width: shape[0].length, | |
| height: shape.length | |
| }; | |
| drawNextPiece(); | |
| } | |
| // Draw the next piece preview | |
| function drawNextPiece() { | |
| nextCtx.clearRect(0, 0, nextCanvas.width, nextCanvas.height); | |
| if (!nextPiece) return; | |
| nextCtx.fillStyle = COLORS[nextPiece.shapeId]; | |
| nextCtx.strokeStyle = '#fff'; | |
| nextCtx.lineWidth = 1; | |
| // Center the piece in the preview | |
| const offsetX = (nextCanvas.width - nextPiece.width * (BLOCK_SIZE / 2)) / 2; | |
| const offsetY = (nextCanvas.height - nextPiece.height * (BLOCK_SIZE / 2)) / 2; | |
| for (let y = 0; y < nextPiece.height; y++) { | |
| for (let x = 0; x < nextPiece.width; x++) { | |
| if (nextPiece.shape[y][x]) { | |
| drawBlock( | |
| nextCtx, | |
| x * (BLOCK_SIZE / 2) + offsetX, | |
| y * (BLOCK_SIZE / 2) + offsetY, | |
| BLOCK_SIZE / 2, | |
| COLORS[nextPiece.shapeId], | |
| true | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| // Spawn the next piece | |
| function spawnPiece() { | |
| piece = { | |
| shapeId: nextPiece.shapeId, | |
| shape: nextPiece.shape, | |
| pos: { | |
| x: Math.floor(COLS / 2) - Math.floor(nextPiece.width / 2), | |
| y: 0 | |
| }, | |
| width: nextPiece.width, | |
| height: nextPiece.height | |
| }; | |
| createNextPiece(); | |
| // Check if game over | |
| if (collision()) { | |
| gameOver = true; | |
| playSound('gameover'); | |
| gameOverScreen.style.display = 'flex'; | |
| } | |
| } | |
| // Rotate the current piece | |
| function rotatePiece() { | |
| if (isPaused || gameOver) return; | |
| const rotated = Array(piece.width).fill().map(() => Array(piece.height).fill(0)); | |
| for (let y = 0; y < piece.height; y++) { | |
| for (let x = 0; x < piece.width; x++) { | |
| rotated[x][piece.height - 1 - y] = piece.shape[y][x]; | |
| } | |
| } | |
| const originalShape = piece.shape; | |
| const originalWidth = piece.width; | |
| const originalHeight = piece.height; | |
| piece.shape = rotated; | |
| piece.width = rotated[0].length; | |
| piece.height = rotated.length; | |
| // Wall kick - adjust position if rotation causes collision | |
| if (collision()) { | |
| // Try moving left | |
| piece.pos.x -= 1; | |
| if (collision()) { | |
| // Try moving right | |
| piece.pos.x += 2; | |
| if (collision()) { | |
| // Revert rotation | |
| piece.shape = originalShape; | |
| piece.width = originalWidth; | |
| piece.height = originalHeight; | |
| return; | |
| } | |
| } | |
| } | |
| playSound('rotate'); | |
| } | |
| // Move the current piece | |
| function movePiece(dir) { | |
| if (isPaused || gameOver) return; | |
| piece.pos.x += dir; | |
| if (collision()) { | |
| piece.pos.x -= dir; | |
| return false; | |
| } | |
| playSound('move'); | |
| return true; | |
| } | |
| // Hard drop the current piece | |
| function hardDrop() { | |
| if (isPaused || gameOver) return; | |
| while (!collision()) { | |
| piece.pos.y++; | |
| } | |
| piece.pos.y--; | |
| dropCounter = dropInterval; | |
| merge(); | |
| spawnPiece(); | |
| playSound('drop'); | |
| } | |
| // Merge the current piece with the board | |
| function merge() { | |
| for (let y = 0; y < piece.height; y++) { | |
| for (let x = 0; x < piece.width; x++) { | |
| if (piece.shape[y][x]) { | |
| board[y + piece.pos.y][x + piece.pos.x] = piece.shapeId; | |
| } | |
| } | |
| } | |
| // Check for cleared lines | |
| clearLines(); | |
| updateScore(); | |
| } | |
| // Clear completed lines | |
| function clearLines() { | |
| let linesCleared = 0; | |
| for (let y = ROWS - 1; y >= 0; y--) { | |
| if (board[y].every(cell => cell > 0)) { | |
| // Remove the line | |
| board.splice(y, 1); | |
| // Add a new empty line at the top | |
| board.unshift(Array(COLS).fill(0)); | |
| linesCleared++; | |
| y++; // Check this row again since rows shift down | |
| } | |
| } | |
| if (linesCleared > 0) { | |
| lines += linesCleared; | |
| playSound('clear'); | |
| // Level up every 10 lines | |
| level = Math.floor(lines / 10) + 1; | |
| dropInterval = Math.max(100, 1000 - (level - 1) * 100); | |
| } | |
| } | |
| // Check if the current piece collides with the board boundaries or other pieces | |
| function collision() { | |
| for (let y = 0; y < piece.height; y++) { | |
| for (let x = 0; x < piece.width; x++) { | |
| if (piece.shape[y][x]) { | |
| const boardX = piece.pos.x + x; | |
| const boardY = piece.pos.y + y; | |
| if ( | |
| boardX < 0 || | |
| boardX >= COLS || | |
| boardY >= ROWS || | |
| (boardY >= 0 && board[boardY][boardX]) | |
| ) { | |
| return true; | |
| } | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| // Update game score and level display | |
| function updateScore() { | |
| // Simple scoring system | |
| const newScore = lines * 100 * level; | |
| scoreElement.textContent = String(newScore).padStart(5, '0'); | |
| levelElement.textContent = level; | |
| linesElement.textContent = lines; | |
| } | |
| // Draw a single block with neon glow effect | |
| function drawBlock(ctx, x, y, size, color, isGhost = false) { | |
| const realX = x * size; | |
| const realY = y * size; | |
| // Inner block | |
| if (isGhost) { | |
| ctx.fillStyle = color.replace('1)', '0.3)'); | |
| } else { | |
| ctx.fillStyle = color; | |
| } | |
| ctx.fillRect(realX, realY, size, size); | |
| // Neon border effect | |
| ctx.strokeStyle = isGhost ? color.replace('1)', '0.5)') : color; | |
| ctx.lineWidth = isGhost ? 1 : 2; | |
| ctx.strokeRect(realX, realY, size, size); | |
| // Glow effect | |
| if (!isGhost) { | |
| ctx.shadowColor = color; | |
| ctx.shadowBlur = 15; | |
| ctx.fillRect(realX, realY, size, size); | |
| ctx.shadowBlur = 0; | |
| // Inner highlight | |
| ctx.fillStyle = color.replace('1)', '0.7)'); | |
| ctx.fillRect(realX + size * 0.2, realY + size * 0.2, size * 0.6, size * 0.6); | |
| } | |
| } | |
| // Draw the game board and current piece | |
| function draw() { | |
| // Clear the canvas | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // Draw the board | |
| for (let y = 0; y < ROWS; y++) { | |
| for (let x = 0; x < COLS; x++) { | |
| if (board[y][x]) { | |
| drawBlock(ctx, x, y, BLOCK_SIZE, COLORS[board[y][x]]); | |
| } | |
| } | |
| } | |
| // Draw the current piece | |
| if (piece) { | |
| // Draw ghost piece | |
| const ghostY = piece.pos.y; | |
| while (!collision()) { | |
| piece.pos.y++; | |
| } | |
| piece.pos.y--; | |
| const currentY = piece.pos.y; | |
| piece.pos.y = ghostY; | |
| for (let y = 0; y < piece.height; y++) { | |
| for (let x = 0; x < piece.width; x++) { | |
| if (piece.shape[y][x]) { | |
| drawBlock(ctx, | |
| x + piece.pos.x, | |
| y + currentY, | |
| BLOCK_SIZE, | |
| COLORS[piece.shapeId], | |
| true | |
| ); | |
| } | |
| } | |
| } | |
| // Draw actual piece | |
| for (let y = 0; y < piece.height; y++) { | |
| for (let x = 0; x < piece.width; x++) { | |
| if (piece.shape[y][x]) { | |
| drawBlock(ctx, | |
| x + piece.pos.x, | |
| y + piece.pos.y, | |
| BLOCK_SIZE, | |
| COLORS[piece.shapeId] | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // Game loop | |
| function update(time = 0) { | |
| if (gameOver) return; | |
| if (isPaused) { | |
| window.requestAnimationFrame(update); | |
| return; | |
| } | |
| const deltaTime = time - lastTime; | |
| lastTime = time; | |
| dropCounter += deltaTime; | |
| if (dropCounter > dropInterval) { | |
| piece.pos.y++; | |
| if (collision()) { | |
| piece.pos.y--; | |
| merge(); | |
| spawnPiece(); | |
| } | |
| dropCounter = 0; | |
| } | |
| draw(); | |
| window.requestAnimationFrame(update); | |
| } | |
| // Event listeners | |
| document.addEventListener('keydown', event => { | |
| if (gameOver) return; | |
| switch (event.key) { | |
| case 'ArrowLeft': | |
| movePiece(-1); | |
| break; | |
| case 'ArrowRight': | |
| movePiece(1); | |
| break; | |
| case 'ArrowDown': | |
| piece.pos.y++; | |
| if (collision()) { | |
| piece.pos.y--; | |
| merge(); | |
| spawnPiece(); | |
| } | |
| playSound('move'); | |
| dropCounter = 0; | |
| break; | |
| case 'ArrowUp': | |
| rotatePiece(); | |
| break; | |
| case ' ': | |
| hardDrop(); | |
| break; | |
| case 'p': | |
| togglePause(); | |
| break; | |
| } | |
| }); | |
| function togglePause() { | |
| if (gameOver) return; | |
| isPaused = !isPaused; | |
| pauseButton.textContent = isPaused ? 'RESUME' : 'PAUSE'; | |
| if (!isPaused) { | |
| lastTime = performance.now(); | |
| update(); | |
| } | |
| } | |
| // Button event listeners | |
| startButton.addEventListener('click', () => { | |
| init(); | |
| lastTime = performance.now(); | |
| update(); | |
| }); | |
| pauseButton.addEventListener('click', togglePause); | |
| restartButton.addEventListener('click', () => { | |
| init(); | |
| gameOverScreen.style.display = 'none'; | |
| lastTime = performance.now(); | |
| update(); | |
| }); | |
| // Start the game | |
| init(); | |
| draw(); | |
| }); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <a href="https://enzostvs-deepsite.hf.space" style="color: #fff;" target="_blank" >DeepSite</a> <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;"></p></body> | |
| </html> |