🎮ArcadeLab

Geometry Dash Style - бег с препятствиями

by PrismBolt10
548 lines20.6 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, viewport-fit=cover">
    <title>Geometry Dash Style - бег с препятствиями</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            user-select: none;
            -webkit-tap-highlight-color: transparent;
        }

        body {
            background: #0a0f1e;
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            font-family: 'Courier New', 'Monaco', monospace;
            touch-action: pan-x pan-y; /* разрешаем жесты, но canvas сам перехватит тапы */
        }

        .game-container {
            position: relative;
            box-shadow: 0 20px 35px rgba(0,0,0,0.5);
            border-radius: 20px;
            overflow: hidden;
            border: 2px solid #2affb6;
        }

        canvas {
            display: block;
            width: 100%;
            height: auto;
            background: #0b1120;
            cursor: pointer;
            touch-action: manipulation;
        }

        .info {
            position: absolute;
            top: 12px;
            left: 0;
            right: 0;
            text-align: center;
            pointer-events: none;
            z-index: 10;
            font-weight: bold;
            text-shadow: 0 0 5px #00ffff;
        }

        .score-board {
            display: inline-block;
            background: rgba(0,0,0,0.7);
            backdrop-filter: blur(4px);
            padding: 6px 20px;
            border-radius: 40px;
            font-size: 1.7rem;
            font-weight: bold;
            letter-spacing: 2px;
            color: #ffe484;
            font-family: monospace;
            border: 1px solid #ffcc44;
        }

        .restart-btn {
            position: absolute;
            bottom: 25px;
            left: 50%;
            transform: translateX(-50%);
            background: #ff4757;
            color: white;
            border: none;
            padding: 10px 24px;
            border-radius: 50px;
            font-weight: bold;
            font-size: 1.1rem;
            font-family: inherit;
            cursor: pointer;
            box-shadow: 0 5px 0 #8b1e2a;
            transition: 0.05s linear;
            z-index: 20;
            pointer-events: auto;
            letter-spacing: 1px;
        }

        .restart-btn:active {
            transform: translateX(-50%) translateY(3px);
            box-shadow: 0 2px 0 #8b1e2a;
        }

        .instruction {
            position: absolute;
            bottom: 20px;
            right: 12px;
            background: rgba(0,0,0,0.5);
            padding: 4px 12px;
            border-radius: 50px;
            font-size: 0.7rem;
            color: #bbffdd;
            pointer-events: none;
            font-weight: bold;
        }

        @media (max-width: 600px) {
            .score-board { font-size: 1.3rem; padding: 4px 16px; }
            .restart-btn { padding: 8px 20px; font-size: 1rem; bottom: 18px; }
        }
    </style>
</head>
<body>
<div class="game-container">
    <canvas id="gameCanvas" width="450" height="650"></canvas>
    <div class="info">
        <div class="score-board" id="scoreDisplay">0</div>
    </div>
    <button class="restart-btn" id="restartButton">⟳ СТАРТ / ЗАНОВО</button>
    <div class="instruction">⬆️ ТАПНИ, ЧТОБЫ ПРЫГНУТЬ</div>
</div>

<script>
    (function() {
        // ------ НАСТРОЙКИ ИГРЫ (Geometry Dash стиль) ------
        const canvas = document.getElementById('gameCanvas');
        const ctx = canvas.getContext('2d');
        const scoreSpan = document.getElementById('scoreDisplay');

        // ЛОГИЧЕСКИЕ РАЗМЕРЫ (фиксированное поле для удобства физики)
        const W = 450;
        const H = 650;
        canvas.width = W;
        canvas.height = H;

        // Параметры мира
        const GROUND_Y = H - 60;        // уровень земли (верхняя граница земли)
        const PLAYER_SIZE = 32;         // размер квадратного игрока
        const PLAYER_FIXED_X = 90;       // игрок статичен по X, мир движется на него
        
        // Физика (используем секундные величины, delta time)
        const GRAVITY = 1600;            // пикселей/сек²
        const JUMP_POWER = -400;         // начальная скорость прыжка (вверх)
        
        // Препятствия
        const OBSTACLE_WIDTH = 28;
        const OBSTACLE_HEIGHT = 32;
        const BASE_SPEED = 230;           // скорость движения препятствий вправо (пикс/сек)
        
        // Генерация препятствий
        const MIN_GAP_SEC = 0.9;           // мин интервал между препятствиями (сек)
        const MAX_GAP_SEC = 1.5;           // макс интервал (сек)
        
        // состояние игры
        let gameRunning = true;
        let score = 0;
        let frameRequest;
        let lastTimestamp = 0;
        
        // динамические объекты
        let player = {
            x: PLAYER_FIXED_X,
            y: GROUND_Y - PLAYER_SIZE,    // стоя на земле
            vy: 0,
            width: PLAYER_SIZE,
            height: PLAYER_SIZE,
            isOnGround: true
        };
        
        let obstacles = [];   // каждый объект: { x, width, height, counted }
        
        // таймер для генерации
        let timeToNextObstacle = 0;    // секунд до следующего препятствия
        
        // анимационные эффекты (вспышка при смерти)
        let deathFlash = 0;
        
        // ---------- вспомогательные функции ----------
        function updateScoreUI() {
            scoreSpan.innerText = Math.floor(score);
        }
        
        // полный сброс игры
        function resetGame() {
            gameRunning = true;
            score = 0;
            updateScoreUI();
            obstacles = [];
            // сброс игрока
            player.y = GROUND_Y - PLAYER_SIZE;
            player.vy = 0;
            player.isOnGround = true;
            deathFlash = 0;
            
            // генерируем первое препятствие через небольшую задержку
            timeToNextObstacle = 0.6;   // первый блок появится быстро
        }
        
        // добавить новое препятствие
        function addObstacle() {
            obstacles.push({
                x: W,                     // появляется справа за границей
                width: OBSTACLE_WIDTH,
                height: OBSTACLE_HEIGHT,
                counted: false
            });
        }
        
        // обработка столкновений (прямоугольник с прямоугольником)
        function checkCollision() {
            // границы игрока
            const playerLeft = player.x;
            const playerRight = player.x + PLAYER_SIZE;
            const playerTop = player.y;
            const playerBottom = player.y + PLAYER_SIZE;
            
            for (let i = 0; i < obstacles.length; i++) {
                const obs = obstacles[i];
                const obsLeft = obs.x;
                const obsRight = obs.x + obs.width;
                const obsTop = GROUND_Y - OBSTACLE_HEIGHT;   // препятствие стоит на земле
                const obsBottom = GROUND_Y;
                
                if (playerRight > obsLeft && playerLeft < obsRight &&
                    playerBottom > obsTop && playerTop < obsBottom) {
                    return true;  // столкновение
                }
            }
            return false;
        }
        
        // обновление очков: когда препятствие проходит левее игрока и не посчитано
        function updateScoreFromObstacles() {
            for (let i = 0; i < obstacles.length; i++) {
                const obs = obstacles[i];
                if (!obs.counted && (obs.x + obs.width) < player.x) {
                    obs.counted = true;
                    score += 10;
                    updateScoreUI();
                }
            }
        }
        
        // прыжок (вызывается по тапу или клику)
        function jump() {
            if (!gameRunning) return;
            // проверяем, стоит ли игрок на земле (маленький допуск)
            const epsilon = 3;
            if (player.y + PLAYER_SIZE >= GROUND_Y - epsilon && player.vy >= -50) {
                player.vy = JUMP_POWER;
                player.isOnGround = false;
                // эффект (звука нет, но для отдачи)
            }
        }
        
        // обновление физики и положения (dt - секунды)
        function updatePhysics(dtSec) {
            if (dtSec > 0.03) dtSec = 0.03; // ограничение
            
            // вертикальное движение
            player.vy += GRAVITY * dtSec;
            player.y += player.vy * dtSec;
            
            // проверка земли
            if (player.y + PLAYER_SIZE >= GROUND_Y) {
                player.y = GROUND_Y - PLAYER_SIZE;
                player.vy = 0;
                player.isOnGround = true;
            }
            // верхняя граница (не вылетаем в небо)
            if (player.y < 0) {
                player.y = 0;
                if (player.vy < 0) player.vy = 0;
            }
        }
        
        // обновление движения препятствий и генерация
        function updateObstacles(dtSec) {
            // 1) двигаем все препятствия влево
            for (let i = 0; i < obstacles.length; i++) {
                obstacles[i].x -= BASE_SPEED * dtSec;
            }
            // удаляем вышедшие за левую границу
            obstacles = obstacles.filter(obs => obs.x + obs.width > 0);
            
            // 2) генерация новых препятствий
            if (gameRunning) {
                if (timeToNextObstacle <= 0) {
                    // добавляем новый блок
                    addObstacle();
                    // случайная задержка до следующего
                    const gap = MIN_GAP_SEC + Math.random() * (MAX_GAP_SEC - MIN_GAP_SEC);
                    timeToNextObstacle = gap;
                } else {
                    timeToNextObstacle -= dtSec;
                }
            }
        }
        
        // ОСНОВНОЙ ЦИКЛ ОБНОВЛЕНИЯ (логика)
        function updateGame(dtSec) {
            if (!gameRunning) return;
            
            // запоминаем предыдущее состояние для проверки смерти
            updatePhysics(dtSec);
            updateObstacles(dtSec);
            updateScoreFromObstacles();
            
            // проверка столкновения после всех изменений
            const collided = checkCollision();
            if (collided) {
                gameRunning = false;
                deathFlash = 1.0;   // эффект красной вспышки
                return;
            }
            
            // Дополнительная проверка: если игрок упал ниже земли (по багу) - тоже смерть
            if (player.y + PLAYER_SIZE > GROUND_Y + 10) {
                gameRunning = false;
                deathFlash = 1.0;
            }
        }
        
        // ---------- ОТРИСОВКА (стиль Geometry Dash неон) ----------
        function drawBackground() {
            // градиентное небо
            const grad = ctx.createLinearGradient(0, 0, 0, H);
            grad.addColorStop(0, "#0a0f2a");
            grad.addColorStop(1, "#131b3c");
            ctx.fillStyle = grad;
            ctx.fillRect(0, 0, W, H);
            
            // декоративные линии (как рельсы)
            ctx.beginPath();
            ctx.strokeStyle = "#2effc6";
            ctx.lineWidth = 2;
            ctx.setLineDash([12, 16]);
            for (let i = 0; i < 4; i++) {
                const yLine = GROUND_Y - 15 + i * 12;
                ctx.beginPath();
                ctx.moveTo(0, yLine);
                ctx.lineTo(W, yLine);
                ctx.stroke();
            }
            ctx.setLineDash([]);
            
            // частицы/звезды
            ctx.fillStyle = "#fffbd0";
            for (let i = 0; i < 120; i++) {
                if (i%2 === 0) continue;
                let sx = (i * 131) % W;
                let sy = (i * 253) % (GROUND_Y-40);
                ctx.globalAlpha = 0.5 + Math.sin(Date.now() * 0.002 + i) * 0.3;
                ctx.fillRect(sx, sy, 2, 2);
            }
            ctx.globalAlpha = 1;
        }
        
        function drawGround() {
            // земля с текстурой "кирпичи / металл"
            ctx.fillStyle = "#252b4a";
            ctx.fillRect(0, GROUND_Y, W, H - GROUND_Y + 5);
            ctx.fillStyle = "#ffbe4d";
            for (let i = 0; i < W; i += 25) {
                ctx.fillRect(i, GROUND_Y - 4, 12, 5);
            }
            ctx.fillStyle = "#fbff47";
            for (let i = 6; i < W; i += 30) {
                ctx.fillRect(i, GROUND_Y - 2, 8, 4);
            }
            // блестящая кайма
            ctx.strokeStyle = "#0effd0";
            ctx.lineWidth = 3;
            ctx.beginPath();
            ctx.moveTo(0, GROUND_Y);
            ctx.lineTo(W, GROUND_Y);
            ctx.stroke();
        }
        
        function drawPlayer() {
            // игрок - глэйдиаторский куб с глазами (Geometry dash стиль)
            let yPos = player.y;
            // тень
            ctx.shadowBlur = 8;
            ctx.shadowColor = "#00ffe0";
            ctx.fillStyle = "#fee440";
            ctx.fillRect(player.x, yPos, PLAYER_SIZE, PLAYER_SIZE);
            ctx.fillStyle = "#f15b2b";
            ctx.fillRect(player.x+5, yPos+6, 6, 8);
            ctx.fillRect(player.x+PLAYER_SIZE-11, yPos+6, 6, 8);
            ctx.fillStyle = "#000000";
            ctx.fillRect(player.x+8, yPos+18, 6, 8);
            ctx.fillRect(player.x+18, yPos+18, 6, 8);
            // динамическая усмешка при прыжке
            ctx.beginPath();
            if (player.vy < -50) {
                ctx.arc(player.x+16, yPos+26, 8, 0.1, Math.PI - 0.1);
            } else {
                ctx.arc(player.x+16, yPos+26, 8, 0, Math.PI);
            }
            ctx.strokeStyle = "#1e2a3e";
            ctx.lineWidth = 2;
            ctx.stroke();
            // "рожки"
            ctx.fillStyle = "#ffaa33";
            ctx.fillRect(player.x+4, yPos-5, 6, 8);
            ctx.fillRect(player.x+22, yPos-5, 6, 8);
            ctx.shadowBlur = 0;
        }
        
        function drawObstacles() {
            for (let obs of obstacles) {
                const obsX = obs.x;
                const obsY = GROUND_Y - OBSTACLE_HEIGHT;
                // градиент препятствия (шипованный стиль)
                const grad = ctx.createLinearGradient(obsX, obsY, obsX+OBSTACLE_WIDTH, obsY+OBSTACLE_HEIGHT);
                grad.addColorStop(0, "#e74c3c");
                grad.addColorStop(1, "#c0392b");
                ctx.fillStyle = grad;
                ctx.fillRect(obsX, obsY, OBSTACLE_WIDTH, OBSTACLE_HEIGHT);
                ctx.fillStyle = "#f1c40f";
                ctx.fillRect(obsX+4, obsY-4, OBSTACLE_WIDTH-8, 6);
                ctx.fillStyle = "#2ecc71";
                for (let i = 0; i < 3; i++) {
                    ctx.fillRect(obsX+4 + i*7, obsY+8, 4, 12);
                }
                // шипы наверху
                ctx.fillStyle = "#f39c12";
                ctx.beginPath();
                ctx.moveTo(obsX+5, obsY-4);
                ctx.lineTo(obsX+10, obsY-10);
                ctx.lineTo(obsX+15, obsY-4);
                ctx.fill();
                ctx.beginPath();
                ctx.moveTo(obsX+15, obsY-4);
                ctx.lineTo(obsX+20, obsY-10);
                ctx.lineTo(obsX+25, obsY-4);
                ctx.fill();
            }
        }
        
        function drawEffectsAndUI() {
            // вспышка при смерти
            if (deathFlash > 0) {
                ctx.fillStyle = `rgba(255, 50, 50, ${deathFlash * 0.7})`;
                ctx.fillRect(0, 0, W, H);
                deathFlash -= 0.05;
                if (deathFlash < 0) deathFlash = 0;
            }
            
            if (!gameRunning) {
                ctx.font = "bold 28 monospace";
                ctx.shadowBlur = 0;
                ctx.fillStyle = "#ffd966";
                ctx.shadowColor = "black";
                ctx.fillText("GAME OVER", W/2-100, H/2-50);
                ctx.font = "18px monospace";
                ctx.fillStyle = "#aaffdd";
                ctx.fillText("нажми кнопку внизу", W/2-110, H/2+20);
            }
            
            // счет на экране дополнительно
            ctx.font = "bold 28 'Courier New'";
            ctx.fillStyle = "#ffe484";
            ctx.shadowBlur = 0;
            ctx.fillText("⚡"+Math.floor(score), 20, 58);
        }
        
        function draw() {
            drawBackground();
            drawGround();
            drawObstacles();
            drawPlayer();
            drawEffectsAndUI();
        }
        
        // ----- главный игровой цикл с delta time -----
        let lastFrameTime = 0;
        
        function gameLoop(nowMs) {
            if (!lastFrameTime) {
                lastFrameTime = nowMs;
                frameRequest = requestAnimationFrame(gameLoop);
                draw();
                return;
            }
            
            let dt = Math.min(0.033, (nowMs - lastFrameTime) / 1000);
            if (dt <= 0) {
                lastFrameTime = nowMs;
                frameRequest = requestAnimationFrame(gameLoop);
                draw();
                return;
            }
            
            if (gameRunning) {
                updateGame(dt);
            }
            
            draw();
            lastFrameTime = nowMs;
            frameRequest = requestAnimationFrame(gameLoop);
        }
        
        // ----- обработка управления (тап / клик) -----
        function handleJumpStart(e) {
            e.preventDefault();
            jump();
        }
        
        function attachEvents() {
            // для мобильных и пк
            canvas.addEventListener('touchstart', handleJumpStart, { passive: false });
            canvas.addEventListener('mousedown', handleJumpStart);
            // также клик по кнопке рестарта не должен вызывать прыжок
            const restartBtn = document.getElementById('restartButton');
            restartBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                resetGame();
                gameRunning = true;
                deathFlash = 0;
                // обнуляем счетчик генерации, чтобы резко не спамило препятствиями
                timeToNextObstacle = 0.5;
                obstacles = [];
                player.y = GROUND_Y - PLAYER_SIZE;
                player.vy = 0;
                player.isOnGround = true;
                score = 0;
                updateScoreUI();
            });
        }
        
        // предотвращаем зуминг двойным тапом на canvas
        canvas.addEventListener('touchstart', (e) => {
            if (e.touches.length > 1) e.preventDefault();
        }, { passive: false });
        
        // ИНИЦИАЛИЗАЦИЯ
        resetGame();
        attachEvents();
        // старт цикла
        lastFrameTime = 0;
        frameRequest = requestAnimationFrame(gameLoop);
    })();
</script>
</body>
</html>

Game Source: Geometry Dash Style - бег с препятствиями

Creator: PrismBolt10

Libraries: none

Complexity: complex (548 lines, 20.6 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: geometry-dash-style-prismbolt10" to link back to the original. Then publish at arcadelab.ai/publish.