🎮ArcadeLab

CUBE 31 | Бесконечный раннер

by MegaCaptain34
512 lines20.3 KB
▶ Play
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>CUBE 31 | Бесконечный раннер</title>
    <style>
        * {
            user-select: none;
            -webkit-tap-highlight-color: transparent;
        }

        body {
            margin: 0;
            min-height: 100vh;
            background: linear-gradient(145deg, #0a0f1e 0%, #0c1222 100%);
            display: flex;
            justify-content: center;
            align-items: center;
            font-family: 'Segoe UI', 'Courier New', 'Monaco', monospace;
            touch-action: manipulation;
        }

        .game-container {
            background: #03060c;
            padding: 20px 20px 24px;
            border-radius: 64px 64px 48px 48px;
            box-shadow: 0 25px 40px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.08);
        }

        canvas {
            display: block;
            margin: 0 auto;
            border-radius: 28px;
            box-shadow: 0 10px 25px rgba(0,0,0,0.5), inset 0 0 0 2px rgba(255,255,255,0.05);
            cursor: pointer;
            background-color: #101624;
        }

        .info-panel {
            display: flex;
            justify-content: space-between;
            align-items: baseline;
            margin-top: 18px;
            margin-bottom: 12px;
            padding: 0 20px;
        }

        .score-box {
            background: #010101aa;
            backdrop-filter: blur(4px);
            padding: 8px 18px;
            border-radius: 60px;
            font-weight: bold;
            font-size: 1.7rem;
            color: #f5e56b;
            text-shadow: 0 0 5px #ffb347;
            letter-spacing: 1px;
            font-family: monospace;
        }

        .best-box {
            background: #1e1f2caa;
            backdrop-filter: blur(4px);
            padding: 8px 18px;
            border-radius: 60px;
            font-weight: bold;
            font-size: 1.2rem;
            color: #b9c6db;
        }

        .controls {
            display: flex;
            justify-content: center;
            gap: 35px;
            margin-top: 20px;
        }

        button {
            background: #1e293b;
            border: none;
            font-size: 2rem;
            font-weight: bold;
            padding: 10px 28px;
            border-radius: 60px;
            color: white;
            font-family: monospace;
            cursor: pointer;
            box-shadow: 0 5px 0 #0f111a;
            transition: 0.07s linear;
            touch-action: manipulation;
        }

        button:active {
            transform: translateY(3px);
            box-shadow: 0 2px 0 #0f111a;
        }

        .restart-btn {
            background: #4a2f2f;
            font-size: 1.2rem;
            padding: 8px 24px;
            letter-spacing: 1px;
        }

        .status {
            background: #00000066;
            border-radius: 40px;
            padding: 6px 18px;
            font-size: 0.9rem;
            font-weight: bold;
            text-align: center;
            color: #ffd966;
        }

        @media (max-width: 650px) {
            .game-container {
                padding: 12px 12px 18px;
            }
            button {
                padding: 6px 22px;
                font-size: 1.6rem;
            }
            .score-box {
                font-size: 1.3rem;
                padding: 4px 14px;
            }
        }
    </style>
</head>
<body>
<div>
    <div class="game-container">
        <canvas id="gameCanvas" width="800" height="500"></canvas>
        <div class="info-panel">
            <div class="score-box">СЧЕТ: <span id="scoreValue">0</span></div>
            <div class="best-box">🏆 РЕКОРД: <span id="bestValue">0</span></div>
            <div class="status" id="gameStatusText">⚡ В ПУТИ</div>
        </div>
        <div class="controls">
            <button id="leftBtn">◀ ЛЕВО</button>
            <button id="rightBtn">ПРАВО ▶</button>
        </div>
        <div style="text-align: center; margin-top: 15px;">
            <button id="restartButton" class="restart-btn">🔄 НОВАЯ ИГРА</button>
        </div>
        <div style="text-align: center; margin-top: 12px; font-size: 12px; color: #6c7a91;">
            ← → или A/D /  пальцем по кнопкам | Уклоняйся от красных, бери ЖЕЛТЫЕ кубы
        </div>
    </div>
</div>

<script>
    (function(){
        // ---------- CANVAS ----------
        const canvas = document.getElementById('gameCanvas');
        const ctx = canvas.getContext('2d');
        
        // ---------- ИГРОВЫЕ ПАРАМЕТРЫ ----------
        const LANE_COUNT = 5;
        const LANE_WIDTH = canvas.width / LANE_COUNT;  // 800/5 = 160
        const PLAYER_SIZE = 48;   // стильный куб 48x48
        
        // игрок (изначально центральная полоса 2 индекс)
        let playerLane = 2;      // 0..4
        let gameRunning = true;
        let score = 0;
        let bestScore = 0;
        
        // загружаем рекорд из localStorage
        try {
            const saved = localStorage.getItem('cube31_best');
            if(saved && !isNaN(parseInt(saved))) bestScore = parseInt(saved);
        } catch(e) { /* тишина */ }
        document.getElementById('bestValue').innerText = bestScore;
        
        // ---------- ОБЪЕКТЫ МИРА (препятствия + монеты) ----------
        let obstacles = [];    // красные враги: { x, y, width, height, lane }
        let coins = [];        // жёлтые монеты: { x, y, width, height, lane }
        
        // Базовые настройки генерации
        let frameCounter = 0;
        let baseSpeed = 3.8;      // пикселей за кадр
        let speed = baseSpeed;
        let generationGap = 38;    // через сколько фреймов спавнить новый объект
        let lastGenFrame = 0;
        
        // параметры сложности (скорость растёт со счётом)
        let speedMultiplier = 1.0;
        
        // размер препятствий/монет
        const OBST_SIZE = 44;
        const COIN_SIZE = 36;
        
        // ---------- HELPERS ----------
        function updateUI(){
            document.getElementById('scoreValue').innerText = Math.floor(score);
            if(score > bestScore){
                bestScore = Math.floor(score);
                document.getElementById('bestValue').innerText = bestScore;
                try { localStorage.setItem('cube31_best', bestScore); } catch(e) {}
            }
        }
        
        // спавн нового объекта (либо монета либо препятствие)
        function spawnObject(){
            if(!gameRunning) return;
            // выбираем случайную полосу 0..4
            const lane = Math.floor(Math.random() * LANE_COUNT);
            const objType = Math.random();
            // чем выше счёт, тем больше препятствий (но сохраняем баланс ~ 45% монет)
            let obstacleChance = 0.48 + (Math.min(score, 400) / 1500);
            if(obstacleChance > 0.72) obstacleChance = 0.72;
            
            const isObstacle = objType < obstacleChance;
            
            const x = lane * LANE_WIDTH + (LANE_WIDTH/2) - (isObstacle ? OBST_SIZE/2 : COIN_SIZE/2);
            const y = - (isObstacle ? OBST_SIZE : COIN_SIZE);
            
            if(isObstacle){
                obstacles.push({
                    x: x,
                    y: y,
                    width: OBST_SIZE,
                    height: OBST_SIZE,
                    lane: lane
                });
            } else {
                coins.push({
                    x: x,
                    y: y,
                    width: COIN_SIZE,
                    height: COIN_SIZE,
                    lane: lane
                });
            }
        }
        
        // обновление позиций и коллизий
        function updateGame(){
            if(!gameRunning) return;
            
            // динамическая скорость (каждые 50 очков +5% скорости)
            speedMultiplier = 1.0 + (Math.floor(score / 55) * 0.025);
            if(speedMultiplier > 1.85) speedMultiplier = 1.85;
            let currentSpeed = baseSpeed * speedMultiplier;
            
            // ----- перемещаем препятствия -----
            for(let i=0; i<obstacles.length; i++){
                obstacles[i].y += currentSpeed;
            }
            // ----- перемещаем монеты -----
            for(let i=0; i<coins.length; i++){
                coins[i].y += currentSpeed;
            }
            
            // удаляем ушедшие за нижнюю границу объекты
            obstacles = obstacles.filter(obs => obs.y < canvas.height + 100);
            coins = coins.filter(coin => coin.y < canvas.height + 100);
            
            // ----- СПАВН объектов (динамический интервал) -----
            // чем выше скорость, тем чаще спавн, но не чаще чем 22 кадра
            let dynamicGap = Math.max(24, Math.floor(generationGap - (Math.min(score,350)/45)));
            if(dynamicGap < 20) dynamicGap = 20;
            if(frameCounter - lastGenFrame >= dynamicGap){
                spawnObject();
                // иногда двойной спавн для насыщенности (эффект "куб в движении")
                if(score > 80 && Math.random() < 0.33 && gameRunning){
                    spawnObject();
                }
                lastGenFrame = frameCounter;
            }
            
            // ----- КОЛЛИЗИЯ ИГРОКА (прямоугольник куба)-----
            const playerX = playerLane * LANE_WIDTH + (LANE_WIDTH/2) - PLAYER_SIZE/2;
            const playerY = canvas.height - 70;  // низкая позиция, классика раннера
            const playerRect = {
                x: playerX,
                y: playerY,
                w: PLAYER_SIZE,
                h: PLAYER_SIZE
            };
            
            // проверка препятствий (столкновение = смерть)
            for(let i=0; i<obstacles.length; i++){
                const obs = obstacles[i];
                const obsRect = { x: obs.x, y: obs.y, w: obs.width, h: obs.height };
                if(playerRect.x < obsRect.x + obsRect.w &&
                    playerRect.x + playerRect.w > obsRect.x &&
                    playerRect.y < obsRect.y + obsRect.h &&
                    playerRect.y + playerRect.h > obsRect.y){
                    gameRunning = false;
                    document.getElementById('gameStatusText').innerHTML = '💀 ГЕЙМ ОВЕР 💀';
                    document.getElementById('gameStatusText').style.color = "#ff8866";
                    return;
                }
            }
            
            // сбор монет (увеличение счета)
            for(let i=0; i<coins.length; i++){
                const coin = coins[i];
                const coinRect = { x: coin.x, y: coin.y, w: coin.width, h: coin.height };
                if(playerRect.x < coinRect.x + coinRect.w &&
                    playerRect.x + playerRect.w > coinRect.x &&
                    playerRect.y < coinRect.y + coinRect.h &&
                    playerRect.y + playerRect.h > coinRect.y){
                    // собрали монету
                    coins.splice(i,1);
                    score += 10;
                    updateUI();
                    i--; // скорректировать индекс
                }
            }
            
            // обновляем UI счета каждые кадры
            updateUI();
            
            // добавим пассивный прирост за выживание (каждый 70 кадров +1)
            if(frameCounter % 70 === 0 && gameRunning){
                score += 1;
                updateUI();
            }
        }
        
        // ---------- ОТРИСОВКА СТИЛЬНОГО КУБА И МИРА ----------
        function draw(){
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            
            // ---- СЕТКА дорожек (кибер-стиль) ----
            for(let i=0; i<=LANE_COUNT; i++){
                ctx.beginPath();
                ctx.strokeStyle = "#2a3850";
                ctx.lineWidth = 2.5;
                ctx.moveTo(i*LANE_WIDTH, 0);
                ctx.lineTo(i*LANE_WIDTH, canvas.height);
                ctx.stroke();
            }
            
            // ---- рисуем препятствия (КРАСНЫЙ АГРЕССИВНЫЙ КУБ) ----
            for(let obs of obstacles){
                // градиент красный
                const grad = ctx.createLinearGradient(obs.x, obs.y, obs.x+obs.width, obs.y+obs.height);
                grad.addColorStop(0, '#e34d4d');
                grad.addColorStop(1, '#b91c1c');
                ctx.fillStyle = grad;
                ctx.shadowBlur = 12;
                ctx.shadowColor = "#ff0000aa";
                ctx.fillRect(obs.x, obs.y, obs.width, obs.height);
                ctx.fillStyle = "#ffffff";
                ctx.font = `bold ${Math.floor(obs.width*0.5)}px monospace`;
                ctx.shadowBlur = 2;
                ctx.fillStyle = "black";
                ctx.fillText("⚠️", obs.x+obs.width*0.25, obs.y+obs.height*0.7);
            }
            
            // ---- рисуем монеты (ЗОЛОТОЙ КУБ 31 стиль)----
            for(let coin of coins){
                ctx.shadowBlur = 12;
                ctx.shadowColor = "#ffcc44";
                ctx.fillStyle = "#f5b642";
                ctx.fillRect(coin.x, coin.y, coin.width, coin.height);
                ctx.fillStyle = "#ffea80";
                ctx.fillRect(coin.x+6, coin.y+6, coin.width-12, coin.height-12);
                ctx.fillStyle = "#d48f2b";
                ctx.font = `bold ${Math.floor(coin.width*0.6)}px monospace`;
                ctx.fillStyle = "#ffd966";
                ctx.shadowBlur = 3;
                ctx.fillText("◆", coin.x+coin.width*0.25, coin.y+coin.height*0.75);
            }
            
            // ---- ИГРОК: сияющий КУБ, который катится (эффект движения)----
            const playerX = playerLane * LANE_WIDTH + (LANE_WIDTH/2) - PLAYER_SIZE/2;
            const playerY = canvas.height - 70;
            // куб с неоновой обводкой
            ctx.shadowBlur = 14;
            ctx.shadowColor = "#3b82f6";
            const gradPlayer = ctx.createLinearGradient(playerX, playerY, playerX+PLAYER_SIZE, playerY+PLAYER_SIZE);
            gradPlayer.addColorStop(0, "#4e9eff");
            gradPlayer.addColorStop(1, "#1e40af");
            ctx.fillStyle = gradPlayer;
            ctx.fillRect(playerX, playerY, PLAYER_SIZE, PLAYER_SIZE);
            ctx.strokeStyle = "#caf0ff";
            ctx.lineWidth = 3;
            ctx.strokeRect(playerX+2, playerY+2, PLAYER_SIZE-4, PLAYER_SIZE-4);
            // лицо куба (дерзкие глаза)
            ctx.fillStyle = "white";
            ctx.fillRect(playerX+12, playerY+14, 8, 8);
            ctx.fillRect(playerX+28, playerY+14, 8, 8);
            ctx.fillStyle = "#020617";
            ctx.fillRect(playerX+14, playerY+16, 5, 5);
            ctx.fillRect(playerX+30, playerY+16, 5, 5);
            ctx.beginPath();
            ctx.arc(playerX+24, playerY+32, 8, 0, Math.PI, false);
            ctx.strokeStyle = "#ffffff";
            ctx.lineWidth = 2;
            ctx.stroke();
            
            // ---- динамический текст скорости/счета на поле----
            ctx.font = "bold 15monospace";
            ctx.fillStyle = "#b9d0ff";
            ctx.shadowBlur = 0;
            ctx.fillText(`⚡ SPEED: ${(speedMultiplier*100).toFixed(0)}%`, 20, 45);
            ctx.font = "bold 18monospace";
            ctx.fillStyle = "#fcda5b";
            ctx.fillText(`SCORE: ${Math.floor(score)}`, canvas.width-130, 45);
            
            // если игра окончена, затемнение
            if(!gameRunning){
                ctx.fillStyle = "rgba(0,0,0,0.75)";
                ctx.fillRect(0,0,canvas.width,canvas.height);
                ctx.font = "900 36monospace";
                ctx.fillStyle = "#ffaa77";
                ctx.shadowBlur = 6;
                ctx.fillText("☠️ GAME OVER ☠️", canvas.width/2-150, canvas.height/2-30);
                ctx.font = "18monospace";
                ctx.fillStyle = "#ccccdd";
                ctx.fillText("НАЖМИ → НОВАЯ ИГРА", canvas.width/2-110, canvas.height/2+40);
            }
            ctx.shadowBlur = 0;
        }
        
        // ---------- УПРАВЛЕНИЕ (клавиши + кнопки) ----------
        function moveLeft(){
            if(!gameRunning) return;
            if(playerLane > 0){
                playerLane--;
            }
        }
        function moveRight(){
            if(!gameRunning) return;
            if(playerLane < LANE_COUNT-1){
                playerLane++;
            }
        }
        
        // рестарт игры
        function restartGame(){
            gameRunning = true;
            score = 0;
            speedMultiplier = 1.0;
            obstacles = [];
            coins = [];
            playerLane = 2;
            frameCounter = 0;
            lastGenFrame = 0;
            updateUI();
            document.getElementById('gameStatusText').innerHTML = '⚡ В ПУТИ';
            document.getElementById('gameStatusText').style.color = "#ffd966";
            // начальный спавн нескольких монет для динамики
            for(let i=0;i<2;i++) spawnObject();
            // небольшой тайминг, чтобы не было мгновенной смерти
        }
        
        // ---------- АНИМАЦИОННЫЙ ЦИКЛ ----------
        let animationId = null;
        function gameLoop(){
            if(gameRunning){
                updateGame();
            }
            draw();
            frameCounter++;
            animationId = requestAnimationFrame(gameLoop);
        }
        
        // ----- ПОДКЛЮЧЕНИЕ СОБЫТИЙ -----
        function initControls(){
            const leftBtn = document.getElementById('leftBtn');
            const rightBtn = document.getElementById('rightBtn');
            const restartBtn = document.getElementById('restartButton');
            
            leftBtn.addEventListener('click', (e) => {
                e.preventDefault();
                moveLeft();
            });
            rightBtn.addEventListener('click', (e) => {
                e.preventDefault();
                moveRight();
            });
            restartBtn.addEventListener('click', (e) => {
                restartGame();
            });
            
            window.addEventListener('keydown', (e) => {
                const key = e.key;
                if(key === 'ArrowLeft' || key === 'a' || key === 'A'){
                    e.preventDefault();
                    moveLeft();
                } else if(key === 'ArrowRight' || key === 'd' || key === 'D'){
                    e.preventDefault();
                    moveRight();
                } else if(key === ' ' || key === 'Space' || key === 'r' || key === 'R'){
                    if(!gameRunning){
                        e.preventDefault();
                        restartGame();
                    }
                }
            });
            // для мобильных тач-событий на кнопках - уже работает
        }
        
        // Инициализация игры + спавн стартовых монет
        function startGame(){
            restartGame(); // начальное состояние живое, с обнулением
            initControls();
            gameLoop();
        }
        
        startGame();
    })();
</script>
</body>
</html>

Game Source: CUBE 31 | Бесконечный раннер

Creator: MegaCaptain34

Libraries: none

Complexity: complex (512 lines, 20.3 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: cube-31-megacaptain34" to link back to the original. Then publish at arcadelab.ai/publish.