🎮ArcadeLab

3D Арканоид

by LuckyGlider65
536 lines22.7 KB
▶ Play
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>3D Арканоид</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            background: linear-gradient(135deg, #0a0a1a, #1a1a3e);
            font-family: 'Arial', sans-serif;
        }
        .game-wrapper {
            background: linear-gradient(145deg, #1a1a2e, #16213e);
            padding: 20px;
            border-radius: 16px;
            box-shadow: 0 20px 60px rgba(0,0,0,0.8);
        }
        .header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 0 5px 15px 5px;
            color: #e0e0e0;
        }
        .header-left {
            display: flex;
            gap: 30px;
            font-size: 20px;
            font-weight: bold;
        }
        .header-left span {
            background: rgba(255,255,255,0.06);
            padding: 6px 16px;
            border-radius: 8px;
            border: 1px solid rgba(255,255,255,0.05);
        }
        .header-left .score-val { color: #f7d44a; }
        .header-left .lives-val { color: #ff6b6b; }
        .restart-btn {
            background: linear-gradient(135deg, #e94560, #c73652);
            color: white;
            border: none;
            padding: 8px 24px;
            border-radius: 8px;
            font-size: 18px;
            font-weight: bold;
            cursor: pointer;
            transition: 0.3s;
            box-shadow: 0 4px 15px rgba(233,69,96,0.3);
        }
        .restart-btn:hover {
            transform: scale(1.05);
            box-shadow: 0 6px 25px rgba(233,69,96,0.5);
        }
        canvas {
            display: block;
            border-radius: 10px;
            background: #0a0a1a;
            cursor: none;
            box-shadow: inset 0 0 50px rgba(78,205,196,0.05);
        }
        .footer {
            color: #6a6a8a;
            text-align: center;
            padding-top: 12px;
            font-size: 14px;
            letter-spacing: 0.5px;
        }
        .footer kbd {
            background: rgba(255,255,255,0.08);
            padding: 2px 10px;
            border-radius: 4px;
            border: 1px solid rgba(255,255,255,0.05);
            color: #b0b0d0;
        }
    </style>
</head>
<body>
    <div class="game-wrapper">
        <div class="header">
            <div class="header-left">
                <span>🏆 <span class="score-val" id="scoreDisplay">0</span></span>
                <span>❤️ <span class="lives-val" id="livesDisplay">3</span></span>
            </div>
            <button class="restart-btn" id="restartButton">🔄 Рестарт</button>
        </div>
        <canvas id="gameCanvas" width="640" height="420"></canvas>
        <div class="footer">
            <kbd>←</kbd> <kbd>→</kbd> или мышь &bull; <kbd>Пробел</kbd> или клик — запуск мяча
        </div>
    </div>

    <script>
        (function(){
            const canvas = document.getElementById('gameCanvas');
            const ctx = canvas.getContext('2d');
            const scoreSpan = document.getElementById('scoreDisplay');
            const livesSpan = document.getElementById('livesDisplay');

            // ---------- РАЗМЕРЫ ----------
            const W = 640, H = 420;

            // ---------- 3D ПАРАМЕТРЫ ----------
            // "Глубина" — насколько сильно уходят вдаль верхние ряды
            const DEPTH_FACTOR = 0.35; 
            // Смещение по Y для создания перспективы (верхние ряды выше)
            const PERSPECTIVE_OFFSET = 60;

            // ---------- ИГРОВЫЕ ПАРАМЕТРЫ ----------
            const PADDLE_WIDTH = 90;
            const PADDLE_HEIGHT = 14;
            const PADDLE_SPEED = 5.5;
            const BALL_RADIUS = 7;

            const BRICK_ROWS = 7;
            const BRICK_COLS = 9;
            const BRICK_WIDTH = 58;
            const BRICK_HEIGHT = 22;
            const BRICK_PADDING = 4;
            const BRICK_TOP_OFFSET = 35;

            // ---------- СОСТОЯНИЕ ----------
            let paddle = { x: W/2 - PADDLE_WIDTH/2, y: H - 38, w: PADDLE_WIDTH, h: PADDLE_HEIGHT };
            let ball = {
                x: W/2, y: H - 52,
                r: BALL_RADIUS,
                dx: 0, dy: 0,
                speed: 5.0
            };
            let bricks = [];
            let score = 0;
            let lives = 3;
            let gameRunning = true;
            let gameStarted = false;
            let animId = null;

            let keys = { left: false, right: false };

            // ---------- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ 3D ----------
            // Получить коэффициент масштаба для ряда (0 = низ, 1 = верх)
            function getDepthScale(row) {
                // row 0 = самый верхний (дальний), row (BRICK_ROWS-1) = нижний (ближний)
                const t = row / (BRICK_ROWS - 1); // 0..1
                // Чем выше ряд, тем сильнее сжатие
                return 1 - t * DEPTH_FACTOR;
            }

            // Получить Y-смещение для ряда (перспектива)
            function getRowOffsetY(row) {
                const t = row / (BRICK_ROWS - 1);
                return -t * PERSPECTIVE_OFFSET;
            }

            // Трансформировать координаты кирпича с учётом 3D
            function getBrickScreenCoords(brick) {
                const scale = getDepthScale(brick.row);
                const offsetY = getRowOffsetY(brick.row);
                // Центр поля смещается так, чтобы верхние ряды "уезжали" вверх-вглубь
                const cx = W/2;
                const xOff = (brick.x - cx) * scale;
                const screenX = cx + xOff;
                const screenY = brick.y + offsetY;
                const screenW = brick.w * scale;
                const screenH = brick.h * scale;
                return { x: screenX, y: screenY, w: screenW, h: screenH };
            }

            // ---------- ИНИЦИАЛИЗАЦИЯ КИРПИЧЕЙ ----------
            function initBricks() {
                const colors = [
                    ['#ff6b6b', '#ee5a24'],
                    ['#feca57', '#f9a825'],
                    ['#f7d44a', '#f5a623'],
                    ['#4ecdc4', '#0abde3'],
                    ['#45b7d1', '#0984e3'],
                    ['#a29bfe', '#6c5ce7'],
                    ['#fd79a8', '#e84393']
                ];
                bricks = [];
                const totalWidth = BRICK_COLS * (BRICK_WIDTH + BRICK_PADDING) - BRICK_PADDING;
                const startX = (W - totalWidth) / 2;

                for (let row = 0; row < BRICK_ROWS; row++) {
                    const scale = getDepthScale(row);
                    // Для верхних рядов кирпичи немного уже — создаём иллюзию перспективы
                    const effectiveW = BRICK_WIDTH * scale;
                    const effectiveH = BRICK_HEIGHT * scale;
                    const effectivePad = BRICK_PADDING * scale;
                    const rowTotal = BRICK_COLS * (effectiveW + effectivePad) - effectivePad;
                    const rowStartX = (W - rowTotal) / 2;

                    for (let col = 0; col < BRICK_COLS; col++) {
                        const x = rowStartX + col * (effectiveW + effectivePad);
                        const y = BRICK_TOP_OFFSET + row * (BRICK_HEIGHT + BRICK_PADDING);
                        const color = colors[row % colors.length];
                        bricks.push({
                            row: row,
                            col: col,
                            x: x,
                            y: y,
                            w: BRICK_WIDTH,   // храним базовый размер
                            h: BRICK_HEIGHT,
                            alive: true,
                            color: color[0],
                            glow: color[1]
                        });
                    }
                }
            }

            // ---------- СБРОС МЯЧА ----------
            function resetBall() {
                ball.x = paddle.x + paddle.w/2;
                ball.y = paddle.y - ball.r - 1;
                ball.dx = 0;
                ball.dy = 0;
                gameStarted = false;
            }

            function startBall() {
                if (gameStarted) return;
                const angle = (Math.random() * 50 + 20) * Math.PI / 180;
                const dir = Math.random() > 0.5 ? 1 : -1;
                ball.dx = ball.speed * Math.cos(angle) * dir;
                ball.dy = -ball.speed * Math.sin(angle);
                gameStarted = true;
            }

            // ---------- ОБНОВЛЕНИЕ ----------
            function update() {
                if (!gameRunning || !gameStarted) return;

                // Управление
                if (keys.left && paddle.x > 0) paddle.x -= PADDLE_SPEED;
                if (keys.right && paddle.x + paddle.w < W) paddle.x += PADDLE_SPEED;

                // Движение мяча
                ball.x += ball.dx;
                ball.y += ball.dy;

                // Стены
                if (ball.x - ball.r < 0) { ball.x = ball.r; ball.dx = -ball.dx; }
                if (ball.x + ball.r > W) { ball.x = W - ball.r; ball.dx = -ball.dx; }
                if (ball.y - ball.r < 0) { ball.y = ball.r; ball.dy = -ball.dy; }

                // Платформа (с учётом 3D — платформа плоская, без искажений)
                if (ball.dy > 0 &&
                    ball.y + ball.r >= paddle.y &&
                    ball.y + ball.r <= paddle.y + paddle.h + 4 &&
                    ball.x >= paddle.x - ball.r &&
                    ball.x <= paddle.x + paddle.w + ball.r) {
                    
                    const hitPos = (ball.x - paddle.x) / paddle.w;
                    const angle = (hitPos - 0.5) * Math.PI / 2.2;
                    const speed = Math.sqrt(ball.dx*ball.dx + ball.dy*ball.dy);
                    ball.dx = speed * Math.sin(angle);
                    ball.dy = -speed * Math.cos(angle);
                    
                    const curSpeed = Math.sqrt(ball.dx*ball.dx + ball.dy*ball.dy);
                    const newSpeed = Math.min(curSpeed * 1.01, 6.8);
                    const ratio = newSpeed / curSpeed;
                    ball.dx *= ratio;
                    ball.dy *= ratio;
                }

                // Потеря мяча
                if (ball.y + ball.r > H) {
                    lives--;
                    updateLives();
                    if (lives === 0) {
                        gameRunning = false;
                        setTimeout(() => {
                            alert('💔 Игра окончена! Попробуйте снова.');
                            resetGame();
                        }, 150);
                    } else {
                        resetBall();
                        paddle.x = (W - paddle.w) / 2;
                    }
                }

                // Столкновения с кирпичами (с использованием 3D-координат)
                for (const brick of bricks) {
                    if (!brick.alive) continue;
                    const scr = getBrickScreenCoords(brick);
                    
                    const bx = ball.x, by = ball.y, r = ball.r;
                    // Проверка пересечения с трансформированным прямоугольником
                    if (bx + r > scr.x && bx - r < scr.x + scr.w &&
                        by + r > scr.y && by - r < scr.y + scr.h) {
                        
                        brick.alive = false;
                        score += 10;
                        updateScore();

                        // Определяем сторону столкновения
                        const overlapX = Math.min(bx + r - scr.x, scr.x + scr.w - (bx - r));
                        const overlapY = Math.min(by + r - scr.y, scr.y + scr.h - (by - r));
                        if (overlapX < overlapY) {
                            ball.dx = -ball.dx;
                        } else {
                            ball.dy = -ball.dy;
                        }

                        // Проверка победы
                        if (bricks.every(b => !b.alive)) {
                            gameRunning = false;
                            setTimeout(() => {
                                alert('🎉 ПОБЕДА! Вы разбили все кирпичи в 3D! 🎉');
                                resetGame();
                            }, 150);
                        }
                        break;
                    }
                }
            }

            // ---------- ОТРИСОВКА ----------
            function draw() {
                ctx.clearRect(0, 0, W, H);

                // --- Пол с эффектом глубины ---
                const grad = ctx.createLinearGradient(0, H-60, 0, H);
                grad.addColorStop(0, 'rgba(78,205,196,0.0)');
                grad.addColorStop(0.5, 'rgba(78,205,196,0.03)');
                grad.addColorStop(1, 'rgba(78,205,196,0.08)');
                ctx.fillStyle = grad;
                ctx.fillRect(0, H-60, W, 60);

                // --- Линии перспективы на поле ---
                ctx.strokeStyle = 'rgba(78,205,196,0.04)';
                ctx.lineWidth = 1;
                for (let i = 0; i < 12; i++) {
                    const t = i / 12;
                    const y = H - 20 - t * 200;
                    const spread = 60 + t * 40;
                    ctx.beginPath();
                    ctx.moveTo(W/2 - spread, y);
                    ctx.lineTo(W/2 + spread, y);
                    ctx.stroke();
                }

                // --- Кирпичи (с 3D трансформацией) ---
                for (const brick of bricks) {
                    if (!brick.alive) continue;
                    const scr = getBrickScreenCoords(brick);
                    
                    // Основной цвет с 3D-затемнением (верхние ряды темнее)
                    const scale = getDepthScale(brick.row);
                    const brightness = 0.55 + 0.45 * scale;
                    ctx.shadowColor = brick.glow;
                    ctx.shadowBlur = 12 * scale + 4;
                    
                    // Градиент для объёма
                    const grad2 = ctx.createLinearGradient(scr.x, scr.y, scr.x, scr.y + scr.h);
                    grad2.addColorStop(0, brick.color);
                    grad2.addColorStop(0.5, brick.color);
                    grad2.addColorStop(1, brick.glow);
                    ctx.fillStyle = grad2;
                    
                    // Скруглённый прямоугольник
                    const radius = 4 * scale + 1;
                    ctx.beginPath();
                    ctx.roundRect(scr.x, scr.y, scr.w, scr.h, radius);
                    ctx.fill();
                    
                    // Блик
                    ctx.shadowBlur = 0;
                    ctx.fillStyle = `rgba(255,255,255,${0.12 * scale + 0.05})`;
                    ctx.beginPath();
                    ctx.roundRect(scr.x + 3*scale, scr.y + 2*scale, scr.w - 6*scale, scr.h * 0.3, 2);
                    ctx.fill();
                }
                ctx.shadowBlur = 0;

                // --- Платформа (с лёгким свечением) ---
                ctx.shadowColor = '#4ecdc4';
                ctx.shadowBlur = 20;
                const pGrad = ctx.createLinearGradient(paddle.x, paddle.y, paddle.x, paddle.y + paddle.h);
                pGrad.addColorStop(0, '#6ef0e8');
                pGrad.addColorStop(0.5, '#4ecdc4');
                pGrad.addColorStop(1, '#2ea89e');
                ctx.fillStyle = pGrad;
                ctx.beginPath();
                ctx.roundRect(paddle.x, paddle.y, paddle.w, paddle.h, 8);
                ctx.fill();
                
                // Блик на платформе
                ctx.shadowBlur = 0;
                ctx.fillStyle = 'rgba(255,255,255,0.2)';
                ctx.beginPath();
                ctx.roundRect(paddle.x + 8, paddle.y + 3, paddle.w - 16, 5, 3);
                ctx.fill();

                // --- Мяч (3D-шарик) ---
                ctx.shadowColor = '#f7d44a';
                ctx.shadowBlur = 25;
                const grad3 = ctx.createRadialGradient(
                    ball.x - 3, ball.y - 4, 1,
                    ball.x, ball.y, ball.r + 2
                );
                grad3.addColorStop(0, '#fff9e6');
                grad3.addColorStop(0.3, '#f7d44a');
                grad3.addColorStop(0.8, '#e8a317');
                grad3.addColorStop(1, '#b8860b');
                ctx.fillStyle = grad3;
                ctx.beginPath();
                ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2);
                ctx.fill();
                
                // Блик на мяче
                ctx.shadowBlur = 0;
                ctx.fillStyle = 'rgba(255,255,255,0.6)';
                ctx.beginPath();
                ctx.arc(ball.x - 2.5, ball.y - 3, 2.2, 0, Math.PI * 2);
                ctx.fill();
                ctx.fillStyle = 'rgba(255,255,255,0.2)';
                ctx.beginPath();
                ctx.arc(ball.x - 1, ball.y - 5, 1, 0, Math.PI * 2);
                ctx.fill();

                // --- Подсказка "Нажми пробел" ---
                if (!gameStarted && gameRunning) {
                    ctx.shadowBlur = 0;
                    ctx.fillStyle = 'rgba(255,255,255,0.15)';
                    ctx.font = '20px Arial';
                    ctx.textAlign = 'center';
                    ctx.fillText('🔄 Нажми ПРОБЕЛ или кликни', W/2, H/2 + 10);
                    ctx.fillStyle = 'rgba(255,255,255,0.06)';
                    ctx.font = '14px Arial';
                    ctx.fillText('чтобы запустить мяч', W/2, H/2 + 40);
                }
            }

            // ---------- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ----------
            function updateScore() {
                scoreSpan.textContent = score;
            }
            function updateLives() {
                livesSpan.textContent = lives;
            }

            function resetGame() {
                score = 0;
                lives = 3;
                gameRunning = true;
                gameStarted = false;
                updateScore();
                updateLives();
                initBricks();
                paddle.x = (W - paddle.w) / 2;
                resetBall();
                if (animId) cancelAnimationFrame(animId);
                gameLoop();
            }

            // ---------- ИГРОВОЙ ЦИКЛ ----------
            function gameLoop() {
                update();
                draw();
                animId = requestAnimationFrame(gameLoop);
            }

            // ---------- ПОЛИФИЛ roundRect ----------
            if (!CanvasRenderingContext2D.prototype.roundRect) {
                CanvasRenderingContext2D.prototype.roundRect = function (x, y, w, h, r) {
                    if (r > w/2) r = w/2;
                    if (r > h/2) r = h/2;
                    this.moveTo(x + r, y);
                    this.lineTo(x + w - r, y);
                    this.quadraticCurveTo(x + w, y, x + w, y + r);
                    this.lineTo(x + w, y + h - r);
                    this.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
                    this.lineTo(x + r, y + h);
                    this.quadraticCurveTo(x, y + h, x, y + h - r);
                    this.lineTo(x, y + r);
                    this.quadraticCurveTo(x, y, x + r, y);
                    this.closePath();
                    return this;
                };
            }

            // ---------- ОБРАБОТЧИКИ ----------
            document.addEventListener('keydown', (e) => {
                if (e.key === 'ArrowLeft' || e.key === 'Left' || e.key === 'a' || e.key === 'A') {
                    keys.left = true;
                    e.preventDefault();
                } else if (e.key === 'ArrowRight' || e.key === 'Right' || e.key === 'd' || e.key === 'D') {
                    keys.right = true;
                    e.preventDefault();
                } else if (e.key === ' ' || e.key === 'Space') {
                    e.preventDefault();
                    if (!gameStarted && gameRunning) startBall();
                }
            });
            document.addEventListener('keyup', (e) => {
                if (e.key === 'ArrowLeft' || e.key === 'Left' || e.key === 'a' || e.key === 'A') {
                    keys.left = false;
                    e.preventDefault();
                } else if (e.key === 'ArrowRight' || e.key === 'Right' || e.key === 'd' || e.key === 'D') {
                    keys.right = false;
                    e.preventDefault();
                }
            });

            canvas.addEventListener('mousemove', (e) => {
                const rect = canvas.getBoundingClientRect();
                const scaleX = W / rect.width;
                const mouseX = (e.clientX - rect.left) * scaleX;
                paddle.x = Math.min(Math.max(mouseX - paddle.w/2, 0), W - paddle.w);
            });

            canvas.addEventListener('click', () => {
                if (!gameStarted && gameRunning) startBall();
            });

            document.getElementById('restartButton').addEventListener('click', resetGame);

            // ---------- СТАРТ ----------
            initBricks();
            paddle.x = (W - paddle.w) / 2;
            resetBall();
            gameLoop();
        })();
    </script>
</body>
</html>

Game Source: 3D Арканоид

Creator: LuckyGlider65

Libraries: none

Complexity: complex (536 lines, 22.7 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: 3d-luckyglider65" to link back to the original. Then publish at arcadelab.ai/publish.