Drawaria Tetris Full Game

Un modo de juego de Tetris completo para Drawaria.online con gráficos, sonido y partículas generados proceduralmente, ocupando gran parte de la pantalla.

// ==UserScript==
// @name         Drawaria Tetris Full Game
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Un modo de juego de Tetris completo para Drawaria.online con gráficos, sonido y partículas generados proceduralmente, ocupando gran parte de la pantalla.
// @author       YouTubeDrawaria
// @match        https://drawaria.online/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=drawaria.online
// @grant        none
// @license      MIT
// @icon         https://www.google.com/s2/favicons?sz=64&domain=drawaria.online
// ==/UserScript==

(function() {
    'use strict';

    // --- 1. CSS Injection for Styling, Animations, and Particles ---
    const style = document.createElement('style');
    style.innerHTML = `
        @keyframes glowing-border {
            0% { box-shadow: 0 0 5px #00FFFF, 0 0 10px #00FFFF, 0 0 15px #00FFFF, inset 0 0 5px rgba(0, 255, 255, 0.3); }
            50% { box-shadow: 0 0 10px #00FFFF, 0 0 20px #00FFFF, 0 0 30px #00FFFF, inset 0 0 8px rgba(0, 255, 255, 0.5); }
            100% { box-shadow: 0 0 5px #00FFFF, 0 0 10px #00FFFF, 0 0 15px #00FFFF, inset 0 0 5px rgba(0, 255, 255, 0.3); }
        }

        @keyframes title-pulse {
            0% { transform: scale(1); text-shadow: 0 0 5px #00FFFF; }
            50% { transform: scale(1.03); text-shadow: 0 0 10px #00FFFF, 0 0 20px #00FFFF; }
            100% { transform: scale(1); text-shadow: 0 0 5px #00FFFF; }
        }

        /* Added for potential hiding of Drawaria elements - EXPERIMENTAL */
        #main-game-container, #canvas-container { /* These are common IDs, adjust if Drawaria uses different ones */
             display: none !important; /* THIS IS EXPERIMENTAL and might break Drawaria */
        }

        .tetris-container {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%); /* Center the container */
            background: #1a1a2e; /* Dark theme */
            border: 3px solid #00FFFF; /* Brighter and thicker border */
            border-radius: 15px;
            box-shadow: 0 0 20px #00FFFF, 0 0 30px #00FFFF; /* More intense glow */
            animation: glowing-border 2s infinite alternate; /* Pulsating glow */
            font-family: 'Press Start 2P', cursive, monospace; /* Pixelated font or similar */
            color: #E0E0E0;
            display: flex;
            flex-direction: column;
            overflow: hidden; /* For particles */
            z-index: 10000; /* Ensure it's on top */
            width: 80%; /* Occupy 80% of viewport width */
            max-width: 960px; /* Max width in pixels */
            height: 85vh; /* Occupy 85% of viewport height */
            max-height: 720px; /* Max height in pixels */
            padding: 15px;
            box-sizing: border-box; /* Include padding and border in element's total width and height */
            position: relative; /* For particle positioning */
        }

        .tetris-title {
            color: #00FFFF;
            font-size: clamp(2em, 5vw, 3.5em); /* Responsive font size */
            text-align: center;
            margin-bottom: 15px;
            text-shadow: 0 0 8px #00FFFF, 0 0 15px #00FFFF;
            animation: title-pulse 3s infinite ease-in-out;
        }

        .tetris-game-area {
            display: flex;
            justify-content: center; /* Center the game and sidebar */
            align-items: flex-start;
            flex-grow: 1; /* Allow game area to take available space */
            gap: 15px; /* Space between canvas and sidebar */
        }

        .tetris-canvas {
            background: #0d0d1a; /* Even darker background for the play area */
            border: 2px solid #00BFFF; /* Slightly different blue border */
            box-shadow: inset 0 0 10px rgba(0, 255, 255, 0.5), 0 0 8px rgba(0, 255, 255, 0.3);
            display: block; /* Remove extra space below canvas */
            flex-shrink: 0; /* Prevent canvas from shrinking */
            /* Canvas will scale based on BLOCK_SIZE and COLS/ROWS */
        }

        .tetris-sidebar {
            display: flex;
            flex-direction: column;
            align-items: center;
            padding: 10px;
            background: #2a2a4a;
            border-radius: 8px;
            box-shadow: 0 0 12px rgba(0, 255, 255, 0.4);
            height: fit-content; /* Sidebar fits its content */
        }

        .tetris-info-box {
            background: #1d1d3d;
            border: 1px solid #00BFFF;
            padding: 10px;
            margin-bottom: 12px;
            border-radius: 6px;
            width: 120px; /* Adjusted width */
            text-align: center;
            box-sizing: border-box;
            font-size: 0.9em;
            text-shadow: 0 0 3px rgba(0, 255, 255, 0.5);
        }

        .tetris-info-box label {
            display: block;
            margin-bottom: 4px;
            color: #00FFFF;
            font-size: 0.85em;
        }

        .tetris-info-box span {
            font-weight: bold;
            color: #F0F0F0;
            font-size: 1.2em; /* Slightly larger text */
        }

        .tetris-next-piece-display {
            background: #1d1d3d;
            border: 1px solid #00BFFF;
            padding: 8px;
            margin-bottom: 12px;
            border-radius: 6px;
            width: 100px; /* Small fixed width for next piece */
            height: 100px; /* Adjust based on block size */
            display: flex;
            justify-content: center;
            align-items: center;
            overflow: hidden;
            box-shadow: 0 0 10px rgba(0, 255, 255, 0.3);
        }

        .tetris-next-piece-display canvas {
            display: block;
        }

        .tetris-button {
            background: linear-gradient(145deg, #00FFFF, #00BFFF);
            color: #1a1a2e;
            border: none;
            padding: 12px 20px; /* Larger buttons */
            font-size: 1.1em;
            font-weight: bold;
            border-radius: 7px;
            cursor: pointer;
            transition: background 0.3s ease, transform 0.1s ease;
            box-shadow: 0 0 12px rgba(0, 255, 255, 0.7);
            text-transform: uppercase;
            letter-spacing: 1px;
            margin-top: 5px; /* Add some space above buttons */
        }

        .tetris-button:hover {
            background: linear-gradient(145deg, #00BFFF, #00FFFF);
            transform: translateY(-3px); /* More pronounced hover effect */
            box-shadow: 0 0 18px rgba(0, 255, 255, 0.9);
        }

        .tetris-button:active {
            transform: translateY(0);
        }

        .game-over-overlay {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.85); /* Slightly more opaque */
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            color: #FF00FF; /* Neon magenta */
            font-size: 2.5em; /* Larger game over text */
            text-shadow: 0 0 10px #FF00FF, 0 0 20px #FF00FF;
            text-align: center;
            border-radius: 15px; /* Match container radius */
            z-index: 1000;
            box-sizing: border-box;
        }

        .game-over-overlay h2 {
            font-size: 1.5em; /* Scale down h2 a bit */
            margin-bottom: 15px;
        }

        .game-over-overlay p {
            margin: 10px 0;
            color: #FFC0CB; /* Pink for score/level text */
            font-size: 0.8em;
            text-shadow: 0 0 5px rgba(255, 192, 203, 0.5);
        }

        /* Particle animations */
        @keyframes float {
            0% { transform: translateY(0px) rotate(0deg) scale(1); opacity: 0.7; }
            50% { transform: translateY(-15px) rotate(10deg) scale(1.2); opacity: 1; }
            100% { transform: translateY(0px) rotate(0deg) scale(1); opacity: 0.7; }
        }

        .tetris-particle {
            position: absolute;
            background: rgba(255, 255, 255, 0.5); /* Semi-transparent white */
            border-radius: 50%;
            box-shadow: 0 0 5px rgba(0, 255, 255, 0.7); /* Cyan glow */
            opacity: 0; /* Start invisible */
            animation-name: float;
            animation-iteration-count: infinite;
            animation-timing-function: ease-in-out;
            pointer-events: none; /* Do not block clicks */
            z-index: 0; /* Behind other content */
        }
    `;
    document.head.appendChild(style);

    // --- 2. HTML Structure Creation ---
    const tetrisContainer = document.createElement('div');
    tetrisContainer.className = 'tetris-container';

    tetrisContainer.innerHTML = `
        <h1 class="tetris-title">TETRIS</h1>
        <div class="tetris-game-area">
            <canvas id="tetrisCanvas" class="tetris-canvas"></canvas>
            <div class="tetris-sidebar">
                <div class="tetris-info-box">
                    <label>SCORE</label>
                    <span id="tetrisScore">0</span>
                </div>
                <div class="tetris-info-box">
                    <label>LEVEL</label>
                    <span id="tetrisLevel">1</span>
                </div>
                <div class="tetris-info-box">
                    <label>LINES</label>
                    <span id="tetrisLines">0</span>
                </div>
                <div class="tetris-info-box">
                    <label>NEXT</label>
                    <div class="tetris-next-piece-display">
                        <canvas id="nextPieceCanvas"></canvas>
                    </div>
                </div>
                <button id="tetrisStartButton" class="tetris-button">Start Game</button>
            </div>
        </div>
        <div id="gameOverOverlay" class="game-over-overlay" style="display: none;">
            <h2>GAME OVER!</h2>
            <p>Score: <span id="finalScore">0</span></p>
            <p>Level: <span id="finalLevel">1</span></p>
            <button id="restartButton" class="tetris-button">Play Again?</button>
        </div>
    `;

    document.body.appendChild(tetrisContainer);

    // Get elements
    const canvas = document.getElementById('tetrisCanvas');
    const ctx = canvas.getContext('2d');
    const nextPieceCanvas = document.getElementById('nextPieceCanvas');
    const nextCtx = nextPieceCanvas.getContext('2d');
    const scoreDisplay = document.getElementById('tetrisScore');
    const levelDisplay = document.getElementById('tetrisLevel');
    const linesDisplay = document.getElementById('tetrisLines');
    const startButton = document.getElementById('tetrisStartButton');
    const gameOverOverlay = document.getElementById('gameOverOverlay');
    const finalScoreDisplay = document.getElementById('finalScore');
    const finalLevelDisplay = document.getElementById('finalLevel');
    const restartButton = document.getElementById('restartButton');

    // --- 3. Web Audio API for Procedural Sound Generation ---
    const audioCtx = new (window.AudioContext || window.webkitAudioContext)();

    function createOscillator(frequency, type = 'sine', duration = 0.1, volume = 0.5) {
        if (!audioCtx) return;
        const oscillator = audioCtx.createOscillator();
        const gainNode = audioCtx.createGain();

        oscillator.type = type;
        oscillator.frequency.setValueAtTime(frequency, audioCtx.currentTime);

        gainNode.gain.setValueAtTime(volume, audioCtx.currentTime);
        gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + duration);

        oscillator.connect(gainNode);
        gainNode.connect(audioCtx.destination);

        oscillator.start();
        oscillator.stop(audioCtx.currentTime + duration);
    }

    function playMoveSound() { createOscillator(260, 'triangle', 0.05, 0.2); }
    function playRotateSound() { createOscillator(440, 'square', 0.08, 0.3); createOscillator(550, 'square', 0.08, 0.3); }
    function playHardDropSound() { createOscillator(180, 'sawtooth', 0.1, 0.5); createOscillator(90, 'square', 0.1, 0.6); }
    function playLineClearSound() {
        const baseFreq = 600;
        for (let i = 0; i < 4; i++) { createOscillator(baseFreq + (i * 100), 'sine', 0.08, 0.4); }
    }
    function playGameOverSound() {
        createOscillator(100, 'square', 0.5, 0.7);
        createOscillator(50, 'sawtooth', 0.5, 0.8);
        setTimeout(() => { createOscillator(200, 'triangle', 0.3, 0.5); }, 300);
    }
    function playStartupMelody() {
        const notes = [
            { freq: 440, dur: 0.15 }, { freq: 523, dur: 0.15 }, { freq: 659, dur: 0.15 }, { freq: 880, dur: 0.3 }
        ];
        let currentTime = audioCtx.currentTime;
        notes.forEach(note => {
            const oscillator = audioCtx.createOscillator();
            const gainNode = audioCtx.createGain();
            oscillator.type = 'sine';
            oscillator.frequency.setValueAtTime(note.freq, currentTime);
            gainNode.gain.setValueAtTime(0.3, currentTime);
            gainNode.gain.exponentialRampToValueAtTime(0.001, currentTime + note.dur);
            oscillator.connect(gainNode);
            gainNode.connect(audioCtx.destination);
            oscillator.start(currentTime);
            oscillator.stop(currentTime + note.dur);
            currentTime += note.dur;
        });
    }

    // --- 4. Tetris Game Logic ---
    const COLS = 10;
    const ROWS = 20;
    let BLOCK_SIZE; // Will be determined by canvas size and aspect ratio

    // Function to resize canvas and adjust block size dynamically
    function resizeGame() {
        const containerWidth = tetrisContainer.offsetWidth;
        const containerHeight = tetrisContainer.offsetHeight;

        // Calculate available space for game area excluding sidebar and title
        const sidebarWidth = 250; // Approximate sidebar width
        const titleHeight = 70; // Approximate title height

        const gameAreaMaxWidth = containerWidth * 0.75 - sidebarWidth; // Max width for canvas
        const gameAreaMaxHeight = containerHeight - titleHeight - 30; // Max height for canvas

        // Determine block size based on available space and aspect ratio
        const aspectRatio = COLS / ROWS;
        let calculatedBlockSizeWidth = gameAreaMaxWidth / COLS;
        let calculatedBlockSizeHeight = gameAreaMaxHeight / ROWS;

        BLOCK_SIZE = Math.min(calculatedBlockSizeWidth, calculatedBlockSizeHeight);

        // Ensure BLOCK_SIZE is at least 10px and not too large
        BLOCK_SIZE = Math.max(10, Math.min(BLOCK_SIZE, 30)); // Min 10px, Max 30px

        canvas.width = COLS * BLOCK_SIZE;
        canvas.height = ROWS * BLOCK_SIZE;
        nextPieceCanvas.width = 4 * BLOCK_SIZE; // For next piece display
        nextPieceCanvas.height = 4 * BLOCK_SIZE;

        // Adjust sidebar to align with canvas height
        const sidebar = tetrisContainer.querySelector('.tetris-sidebar');
        if (sidebar) {
             // sidebar.style.height = `${canvas.height}px`; // This might make sidebar too short if game area is constrained by width
        }

        // Redraw everything if game is running
        if (gameStarted && !gameOver) {
            drawBoard();
            drawCurrentPiece();
            drawNextPiece();
        } else if (!gameStarted) {
            drawBoard(); // Draw initial empty board if not started
            drawNextPiece(); // Draw initial empty next piece
        }
    }

    // Event listener for window resize
    window.addEventListener('resize', resizeGame);

    let board = [];
    let currentPiece;
    let nextPiece;
    let score = 0;
    let level = 1;
    let lines = 0;
    let gameOver = false;
    let dropInterval = 1000;
    let lastDropTime = 0;
    let gameStarted = false;

    const TETROMINOS = {
        'I': { shape: [[0,0,0,0], [1,1,1,1], [0,0,0,0], [0,0,0,0]], color: '#00FFFF' },
        'J': { shape: [[1,0,0], [1,1,1], [0,0,0]], color: '#0000FF' },
        'L': { shape: [[0,0,1], [1,1,1], [0,0,0]], color: '#FFA500' },
        'O': { shape: [[1,1], [1,1]], color: '#FFFF00' },
        'S': { shape: [[0,1,1], [1,1,0], [0,0,0]], color: '#00FF00' },
        'T': { shape: [[0,1,0], [1,1,1], [0,0,0]], color: '#800080' },
        'Z': { shape: [[1,1,0], [0,1,1], [0,0,0]], color: '#FF0000' }
    };

    function initBoard() {
        board = Array(ROWS).fill(null).map(() => Array(COLS).fill(0));
        score = 0; level = 1; lines = 0; gameOver = false;
        dropInterval = 1000;
        updateUI();
        generateNewPiece();
        generateNewPiece(true); // Generate next piece
        gameOverOverlay.style.display = 'none';
        startButton.textContent = 'Restart Game';
        resizeGame(); // Initialize canvas size
    }

    function updateUI() {
        scoreDisplay.textContent = score;
        levelDisplay.textContent = level;
        linesDisplay.textContent = lines;
    }

    function generateNewPiece(isNext = false) {
        const pieceNames = Object.keys(TETROMINOS);
        const randomName = pieceNames[Math.floor(Math.random() * pieceNames.length)];
        const newPiece = JSON.parse(JSON.stringify(TETROMINOS[randomName]));
        newPiece.x = Math.floor(COLS / 2) - Math.floor(newPiece.shape[0].length / 2);
        newPiece.y = 0;

        if (isNext) {
            nextPiece = newPiece;
            drawNextPiece();
        } else {
            currentPiece = newPiece;
            if (checkCollision(currentPiece.x, currentPiece.y, currentPiece.shape)) {
                endGame();
            }
        }
    }

    function drawBlock(context, x, y, color, blockSize) {
        if (color === 0) return;

        context.fillStyle = color;
        context.fillRect(x * blockSize, y * blockSize, blockSize, blockSize);

        // Add procedural highlights and shadows for depth
        const highlightColor = 'rgba(255, 255, 255, 0.6)';
        const shadowColor = 'rgba(0, 0, 0, 0.4)';

        context.strokeStyle = highlightColor;
        context.lineWidth = 1;
        context.beginPath();
        context.moveTo(x * blockSize, y * blockSize);
        context.lineTo((x + 1) * blockSize -1, y * blockSize); // Slightly offset to avoid drawing over next block's border
        context.lineTo((x + 1) * blockSize -1, (y + 1) * blockSize -1);
        context.stroke();

        context.strokeStyle = shadowColor;
        context.beginPath();
        context.moveTo(x * blockSize, (y + 1) * blockSize -1);
        context.lineTo((x + 1) * blockSize -1, (y + 1) * blockSize -1);
        context.lineTo((x + 1) * blockSize -1, y * blockSize);
        context.stroke();
    }

    function drawBoard() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        for (let row = 0; row < ROWS; row++) {
            for (let col = 0; col < COLS; col++) {
                drawBlock(ctx, col, row, board[row][col], BLOCK_SIZE);
            }
        }
    }

    function drawCurrentPiece() {
        if (!currentPiece) return;
        for (let row = 0; row < currentPiece.shape.length; row++) {
            for (let col = 0; col < currentPiece.shape[row].length; col++) {
                if (currentPiece.shape[row][col]) {
                    drawBlock(ctx, currentPiece.x + col, currentPiece.y + row, currentPiece.color, BLOCK_SIZE);
                }
            }
        }
    }

    function drawNextPiece() {
        nextCtx.clearRect(0, 0, nextPieceCanvas.width, nextPieceCanvas.height);
        if (!nextPiece) return;

        const pieceWidth = nextPiece.shape[0].length;
        const pieceHeight = nextPiece.shape.length;
        const startX = (4 - pieceWidth) / 2; // Center in 4x4 grid
        const startY = (4 - pieceHeight) / 2; // Center in 4x4 grid

        for (let row = 0; row < pieceHeight; row++) {
            for (let col = 0; col < pieceWidth; col++) {
                if (nextPiece.shape[row][col]) {
                    drawBlock(nextCtx, startX + col, startY + row, nextPiece.color, BLOCK_SIZE);
                }
            }
        }
    }

    function checkCollision(x, y, shape) {
        for (let row = 0; row < shape.length; row++) {
            for (let col = 0; col < shape[row].length; col++) {
                if (shape[row][col]) {
                    const boardX = x + col;
                    const boardY = y + row;

                    if (boardX < 0 || boardX >= COLS || boardY >= ROWS) {
                        return true;
                    }
                    if (boardY >= 0 && board[boardY][boardX] !== 0) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    function mergePieceToBoard() {
        for (let row = 0; row < currentPiece.shape.length; row++) {
            for (let col = 0; col < currentPiece.shape[row].length; col++) {
                if (currentPiece.shape[row][col]) {
                    const boardX = currentPiece.x + col;
                    const boardY = currentPiece.y + row;
                    if (boardY >= 0) {
                        board[boardY][boardX] = currentPiece.color;
                    }
                }
            }
        }
    }

    function clearLines() {
        let linesClearedThisTurn = 0;
        for (let row = ROWS - 1; row >= 0; row--) {
            if (board[row].every(cell => cell !== 0)) {
                board.splice(row, 1);
                board.unshift(Array(COLS).fill(0));
                linesClearedThisTurn++;
                playLineClearSound();
                row++; // Check the new line at this row index again
            }
        }

        if (linesClearedThisTurn > 0) {
            lines += linesClearedThisTurn;
            if (linesClearedThisTurn === 1) score += 100 * level;
            else if (linesClearedThisTurn === 2) score += 300 * level;
            else if (linesClearedThisTurn === 3) score += 500 * level;
            else if (linesClearedThisTurn === 4) score += 800 * level;

            const oldLevel = level;
            level = Math.floor(lines / 10) + 1;
            if (level > oldLevel) {
                dropInterval *= 0.8;
                if (dropInterval < 100) dropInterval = 100;
            }
            updateUI();
        }
    }

    function rotatePiece(piece) {
        const originalShape = piece.shape;
        const newShape = originalShape[0].map((val, index) => originalShape.map(row => row[index]).reverse());

        if (!checkCollision(piece.x, piece.y, newShape)) {
            piece.shape = newShape;
            playRotateSound();
            return true;
        }

        const kicks = [[-1, 0], [1, 0], [-2, 0], [2, 0], [0, -1]]; // Basic wall kicks
        for (const [dx, dy] of kicks) {
            if (!checkCollision(piece.x + dx, piece.y + dy, newShape)) {
                piece.x += dx; piece.y += dy;
                piece.shape = newShape;
                playRotateSound();
                return true;
            }
        }
        return false;
    }

    function dropPiece() {
        if (gameOver) return;
        if (!checkCollision(currentPiece.x, currentPiece.y + 1, currentPiece.shape)) {
            currentPiece.y++;
        } else {
            mergePieceToBoard();
            clearLines();
            currentPiece = nextPiece;
            generateNewPiece(true);
            if (checkCollision(currentPiece.x, currentPiece.y, currentPiece.shape)) {
                endGame();
            }
        }
    }

    function hardDrop() {
        if (gameOver) return;
        while (!checkCollision(currentPiece.x, currentPiece.y + 1, currentPiece.shape)) {
            currentPiece.y++;
        }
        mergePieceToBoard();
        playHardDropSound();
        clearLines();
        currentPiece = nextPiece;
        generateNewPiece(true);
        if (checkCollision(currentPiece.x, currentPiece.y, currentPiece.shape)) {
            endGame();
        }
    }

    function endGame() {
        gameOver = true;
        playGameOverSound();
        finalScoreDisplay.textContent = score;
        finalLevelDisplay.textContent = level;
        gameOverOverlay.style.display = 'flex';
        gameStarted = false;
    }

    function gameLoop(currentTime) {
        if (gameOver || !gameStarted) {
            requestAnimationFrame(gameLoop);
            return;
        }

        const deltaTime = currentTime - lastDropTime;
        if (deltaTime > dropInterval) {
            dropPiece();
            lastDropTime = currentTime;
        }

        drawBoard();
        drawCurrentPiece();

        requestAnimationFrame(gameLoop);
    }

    // --- 5. Input Handling ---
    document.addEventListener('keydown', (e) => {
        // Only process input if the Tetris container is focused or if it's a game-critical key
        // This is tricky. For now, let's assume we want Tetris input to always work if gameStarted.
        if (gameOver || !gameStarted) return;

        switch (e.key) {
            case 'ArrowLeft': e.preventDefault(); currentPiece.x--; if (checkCollision(currentPiece.x, currentPiece.y, currentPiece.shape)) currentPiece.x++; else playMoveSound(); break;
            case 'ArrowRight': e.preventDefault(); currentPiece.x++; if (checkCollision(currentPiece.x, currentPiece.y, currentPiece.shape)) currentPiece.x--; else playMoveSound(); break;
            case 'ArrowDown': e.preventDefault(); currentPiece.y++; if (checkCollision(currentPiece.x, currentPiece.y, currentPiece.shape)) { currentPiece.y--; dropPiece(); } else { playMoveSound(); score += 1; } lastDropTime = performance.now(); break;
            case 'ArrowUp': e.preventDefault(); rotatePiece(currentPiece); break;
            case 'x': e.preventDefault(); rotatePiece(currentPiece); break;
            case ' ': e.preventDefault(); hardDrop(); break;
        }
        drawBoard();
        drawCurrentPiece();
    });

    // --- 6. Particle System ---
    const particles = [];
    const NUM_PARTICLES = 20; // Increased number of particles

    function createParticle() {
        const particle = document.createElement('div');
        particle.className = 'tetris-particle';
        tetrisContainer.appendChild(particle);

        const size = Math.random() * 4 + 3; // 3-7px
        particle.style.width = `${size}px`;
        particle.style.height = `${size}px`;
        particle.style.left = `${Math.random() * 100}%`;
        particle.style.top = `${Math.random() * 100}%`;
        particle.style.animationDuration = `${Math.random() * 6 + 6}s`; // 6-12s animation
        particle.style.animationDelay = `${Math.random() * -6}s`;
        particle.style.opacity = Math.random() * 0.3 + 0.6; // 0.6 - 0.9 opacity

        particles.push(particle);
    }

    for (let i = 0; i < NUM_PARTICLES; i++) {
        createParticle();
    }

    // --- 7. Game Initialization and Start Button Logic ---
    function startGame() {
        if (!gameStarted) {
            playStartupMelody();
            initBoard();
            gameStarted = true;
            lastDropTime = performance.now();
            requestAnimationFrame(gameLoop);
        }
    }

    startButton.addEventListener('click', startGame);
    restartButton.addEventListener('click', startGame);

    // Initial setup
    initBoard(); // Sets up board and UI elements initially
    resizeGame(); // Adjust canvas size on load
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址