🎮ArcadeLab

Лабиринт Теней

by LuckyGlider65
1155 lines49.5 KB
▶ Play
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Лабиринт Теней</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            overflow: hidden;
            background: #0a0a0a;
            font-family: 'Segoe UI', Arial, sans-serif;
            user-select: none;
        }
        canvas {
            display: block;
        }
        
        #ui {
            position: absolute;
            top: 20px;
            left: 0;
            right: 0;
            display: flex;
            justify-content: center;
            gap: 30px;
            color: #fff;
            font-size: 18px;
            font-weight: bold;
            text-shadow: 0 0 20px rgba(255,0,0,0.3);
            pointer-events: none;
            z-index: 10;
            flex-wrap: wrap;
            padding: 0 15px;
        }
        #ui span {
            background: rgba(0,0,0,0.8);
            padding: 8px 20px;
            border-radius: 10px;
            border: 1px solid rgba(255,255,255,0.06);
            backdrop-filter: blur(10px);
            display: flex;
            align-items: center;
            gap: 8px;
        }
        #ui .keys { color: #f7d44a; }
        #ui .status { color: #ff6b6b; }
        #ui .timer { color: #4ecdc4; }
        
        #gameOver {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            color: #fff;
            text-align: center;
            z-index: 20;
            display: none;
            background: rgba(0,0,0,0.9);
            padding: 40px 60px;
            border-radius: 20px;
            border: 2px solid rgba(255,0,0,0.3);
            backdrop-filter: blur(20px);
            min-width: 300px;
        }
        #gameOver h1 {
            font-size: 48px;
            margin-bottom: 10px;
        }
        #gameOver h1.win { color: #4ecdc4; }
        #gameOver h1.lose { color: #ff6b6b; }
        #gameOver p {
            font-size: 18px;
            color: rgba(255,255,255,0.7);
            margin: 10px 0 20px;
        }
        #gameOver button {
            padding: 12px 40px;
            background: linear-gradient(135deg, #e94560, #c73652);
            color: white;
            border: none;
            border-radius: 10px;
            font-size: 18px;
            font-weight: bold;
            cursor: pointer;
            transition: 0.3s;
        }
        #gameOver button:hover {
            transform: scale(1.05);
            box-shadow: 0 0 30px rgba(233,69,96,0.4);
        }
        
        #controls-hint {
            position: absolute;
            bottom: 25px;
            left: 50%;
            transform: translateX(-50%);
            color: rgba(255,255,255,0.2);
            font-size: 12px;
            z-index: 10;
            background: rgba(0,0,0,0.5);
            padding: 6px 16px;
            border-radius: 20px;
            border: 1px solid rgba(255,255,255,0.05);
            pointer-events: none;
            white-space: nowrap;
        }
        #controls-hint kbd {
            background: rgba(255,255,255,0.06);
            padding: 1px 8px;
            border-radius: 3px;
            border: 1px solid rgba(255,255,255,0.05);
        }
        
        #minimap {
            position: absolute;
            bottom: 70px;
            right: 20px;
            width: 150px;
            height: 150px;
            background: rgba(0,0,0,0.7);
            border-radius: 12px;
            border: 1px solid rgba(255,255,255,0.06);
            z-index: 10;
            overflow: hidden;
        }
        #minimap canvas {
            width: 100%;
            height: 100%;
            image-rendering: pixelated;
        }
        
        .heartbeat {
            animation: heartbeat 1.5s ease-in-out infinite;
        }
        @keyframes heartbeat {
            0%, 100% { transform: scale(1); }
            14% { transform: scale(1.1); }
            28% { transform: scale(1); }
            42% { transform: scale(1.05); }
            70% { transform: scale(1); }
        }
        
        @media (max-width: 768px) {
            #ui { font-size: 13px; gap: 10px; top: 10px; }
            #ui span { padding: 5px 12px; }
            #gameOver { padding: 25px 30px; min-width: auto; width: 90%; }
            #gameOver h1 { font-size: 32px; }
            #minimap { width: 100px; height: 100px; bottom: 60px; right: 10px; }
            #controls-hint { font-size: 10px; bottom: 15px; padding: 4px 12px; }
        }
    </style>
</head>
<body>
    <div id="ui">
        <span>🗝️ <span class="keys" id="keysDisplay">0 / 5</span></span>
        <span>👻 <span class="status" id="statusDisplay">В безопасности</span></span>
        <span>⏱️ <span class="timer" id="timerDisplay">0:00</span></span>
    </div>
    
    <div id="gameOver">
        <h1 id="resultTitle">💀</h1>
        <p id="resultDesc">Вы погибли в лабиринте...</p>
        <button id="restartBtn">🔄 Играть снова</button>
    </div>
    
    <div id="minimap"><canvas id="minimapCanvas" width="150" height="150"></canvas></div>
    
    <div id="controls-hint">
        <kbd>W A S D</kbd> движение &bull; <kbd>Shift</kbd> бег &bull; <kbd>R</kbd> рестарт
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <script>
        (function(){
            // ---------- ПАРАМЕТРЫ ----------
            const MAZE_SIZE = 21; // должно быть нечётным для генерации
            const CELL_SIZE = 0.8;
            const PLAYER_SPEED = 3.0;
            const PLAYER_RUN_SPEED = 5.0;
            const MONSTER_SPEED_BASE = 1.2;
            const MONSTER_SPEED_FAST = 2.8;
            const TOTAL_KEYS = 5;
            const MONSTER_DETECTION_RANGE = 6.0;
            const MONSTER_ATTACK_RANGE = 0.8;
            
            // ---------- СОСТОЯНИЕ ----------
            let maze = [];
            let playerPos = { x: 1, z: 1 };
            let monsterPos = { x: 1, z: 1 };
            let keys = [];
            let exitPos = { x: 1, z: 1 };
            let collectedKeys = 0;
            let gameActive = true;
            let gameWon = false;
            let monsterChasing = false;
            let timeElapsed = 0;
            let timerInterval = null;
            
            // Управление
            const keysPressed = { w: false, a: false, s: false, d: false, shift: false };
            
            // DOM
            const keysDisplay = document.getElementById('keysDisplay');
            const statusDisplay = document.getElementById('statusDisplay');
            const timerDisplay = document.getElementById('timerDisplay');
            const gameOverDiv = document.getElementById('gameOver');
            const resultTitle = document.getElementById('resultTitle');
            const resultDesc = document.getElementById('resultDesc');
            const restartBtn = document.getElementById('restartBtn');
            const minimapCanvas = document.getElementById('minimapCanvas');
            const minimapCtx = minimapCanvas.getContext('2d');

            // ---------- THREE.JS ----------
            const scene = new THREE.Scene();
            scene.background = new THREE.Color(0x0a0a0a);
            scene.fog = new THREE.Fog(0x0a0a0a, 8, 20);

            const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 30);
            camera.position.set(0, 12, 0);
            camera.lookAt(0, 0, 0);

            const renderer = new THREE.WebGLRenderer({ antialias: true });
            renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
            renderer.shadowMap.enabled = true;
            renderer.shadowMap.type = THREE.PCFSoftShadowMap;
            renderer.toneMapping = THREE.ACESFilmicToneMapping;
            renderer.toneMappingExposure = 0.8;
            document.body.prepend(renderer.domElement);

            // ---------- СВЕТ ----------
            const ambientLight = new THREE.AmbientLight(0x111122, 0.3);
            scene.add(ambientLight);

            // Красноватый свет для атмосферы ужаса
            const redLight = new THREE.DirectionalLight(0xff4444, 0.15);
            redLight.position.set(-5, 8, 5);
            scene.add(redLight);

            const blueLight = new THREE.DirectionalLight(0x4444ff, 0.1);
            blueLight.position.set(5, 8, -5);
            scene.add(blueLight);

            // Свет от игрока (фонарик)
            const playerLight = new THREE.PointLight(0xffeedd, 0.8, 8);
            playerLight.position.set(0, 1.5, 0);
            scene.add(playerLight);

            const playerLight2 = new THREE.PointLight(0x4444ff, 0.2, 5);
            playerLight2.position.set(0, 0.5, 0);
            scene.add(playerLight2);

            // ---------- ГЕНЕРАЦИЯ ЛАБИРИНТА (алгоритм DFS) ----------
            function generateMaze(size) {
                // Инициализация стен
                const grid = [];
                for (let i = 0; i < size; i++) {
                    grid[i] = [];
                    for (let j = 0; j < size; j++) {
                        grid[i][j] = 1; // 1 = стена
                    }
                }

                // Функция для проверки границ
                function isInBounds(x, z) {
                    return x > 0 && x < size - 1 && z > 0 && z < size - 1;
                }

                // Рекурсивный DFS для создания лабиринта
                function carve(x, z) {
                    grid[x][z] = 0; // 0 = проход
                    
                    const dirs = [
                        [0, -2], [0, 2], [-2, 0], [2, 0]
                    ];
                    // Перемешиваем направления
                    for (let i = dirs.length - 1; i > 0; i--) {
                        const j = Math.floor(Math.random() * (i + 1));
                        [dirs[i], dirs[j]] = [dirs[j], dirs[i]];
                    }
                    
                    for (const [dx, dz] of dirs) {
                        const nx = x + dx;
                        const nz = z + dz;
                        if (isInBounds(nx, nz) && grid[nx][nz] === 1) {
                            grid[x + dx/2][z + dz/2] = 0; // убираем стену между
                            carve(nx, nz);
                        }
                    }
                }

                // Начинаем с (1, 1)
                carve(1, 1);

                // Убеждаемся, что выход доступен (делаем проход на границе)
                // Находим случайный выход на правой или нижней границе
                let exitX, exitZ;
                const side = Math.floor(Math.random() * 4);
                switch(side) {
                    case 0: // правый край
                        exitX = size - 2;
                        exitZ = 1 + Math.floor(Math.random() * (size - 3)) * 2 + 1;
                        break;
                    case 1: // левый край
                        exitX = 1;
                        exitZ = 1 + Math.floor(Math.random() * (size - 3)) * 2 + 1;
                        break;
                    case 2: // нижний край
                        exitX = 1 + Math.floor(Math.random() * (size - 3)) * 2 + 1;
                        exitZ = size - 2;
                        break;
                    case 3: // верхний край
                        exitX = 1 + Math.floor(Math.random() * (size - 3)) * 2 + 1;
                        exitZ = 1;
                        break;
                }
                
                // Убираем стену на выходе
                grid[exitX][exitZ] = 0;
                
                // Сохраняем выход
                exitPos = { x: exitX, z: exitZ };
                
                return grid;
            }

            // ---------- СОЗДАНИЕ СЦЕНЫ ----------
            function createMazeScene() {
                // Удаляем старые объекты лабиринта
                const toRemove = [];
                scene.children.forEach(child => {
                    if (child.userData && child.userData.isMaze) {
                        toRemove.push(child);
                    }
                });
                toRemove.forEach(child => scene.remove(child));

                maze = generateMaze(MAZE_SIZE);
                
                // Создаём стены
                const wallMaterial = new THREE.MeshStandardMaterial({
                    color: 0x1a1a2e,
                    roughness: 0.9,
                    metalness: 0.1,
                    emissive: 0x0a0a1a,
                    emissiveIntensity: 0.2
                });
                
                const halfSize = MAZE_SIZE / 2 * CELL_SIZE;
                
                for (let i = 0; i < MAZE_SIZE; i++) {
                    for (let j = 0; j < MAZE_SIZE; j++) {
                        if (maze[i][j] === 1) {
                            const x = (i - MAZE_SIZE/2 + 0.5) * CELL_SIZE;
                            const z = (j - MAZE_SIZE/2 + 0.5) * CELL_SIZE;
                            
                            const height = 0.8 + Math.random() * 0.3;
                            const geo = new THREE.BoxGeometry(CELL_SIZE, height, CELL_SIZE);
                            const mesh = new THREE.Mesh(geo, wallMaterial);
                            mesh.position.set(x, height/2 - 0.1, z);
                            mesh.castShadow = true;
                            mesh.receiveShadow = true;
                            mesh.userData.isMaze = true;
                            scene.add(mesh);
                        }
                    }
                }

                // Пол
                const floorGeo = new THREE.PlaneGeometry(MAZE_SIZE * CELL_SIZE + 2, MAZE_SIZE * CELL_SIZE + 2);
                const floorMat = new THREE.MeshStandardMaterial({
                    color: 0x0a0a15,
                    roughness: 0.9,
                    metalness: 0.0
                });
                const floor = new THREE.Mesh(floorGeo, floorMat);
                floor.rotation.x = -Math.PI / 2;
                floor.position.y = -0.1;
                floor.receiveShadow = true;
                floor.userData.isMaze = true;
                scene.add(floor);

                // Размещаем ключи
                keys = [];
                const keyMaterial = new THREE.MeshStandardMaterial({
                    color: 0xf7d44a,
                    emissive: 0xf7d44a,
                    emissiveIntensity: 0.5,
                    metalness: 0.8,
                    roughness: 0.2
                });
                
                let placedKeys = 0;
                let attempts = 0;
                while (placedKeys < TOTAL_KEYS && attempts < 1000) {
                    attempts++;
                    const x = 1 + Math.floor(Math.random() * (MAZE_SIZE - 2));
                    const z = 1 + Math.floor(Math.random() * (MAZE_SIZE - 2));
                    // Не ставим ключи на старт и выход
                    if ((x === 1 && z === 1) || (x === exitPos.x && z === exitPos.z)) continue;
                    if (maze[x][z] === 0 && !keys.some(k => k.x === x && k.z === z)) {
                        const key = { x, z, collected: false };
                        keys.push(key);
                        
                        const keyGroup = new THREE.Group();
                        const keyGeo = new THREE.TorusGeometry(0.15, 0.05, 8, 16);
                        const keyMesh = new THREE.Mesh(keyGeo, keyMaterial);
                        keyMesh.rotation.x = Math.PI / 2;
                        keyMesh.position.y = 0.3;
                        keyGroup.add(keyMesh);
                        
                        const keyGlow = new THREE.Mesh(
                            new THREE.SphereGeometry(0.1, 8, 8),
                            new THREE.MeshBasicMaterial({
                                color: 0xf7d44a,
                                transparent: true,
                                opacity: 0.2
                            })
                        );
                        keyGlow.position.y = 0.3;
                        keyGroup.add(keyGlow);
                        
                        const posX = (x - MAZE_SIZE/2 + 0.5) * CELL_SIZE;
                        const posZ = (z - MAZE_SIZE/2 + 0.5) * CELL_SIZE;
                        keyGroup.position.set(posX, 0, posZ);
                        keyGroup.userData.isMaze = true;
                        keyGroup.userData.isKey = true;
                        keyGroup.userData.keyIndex = placedKeys;
                        scene.add(keyGroup);
                        placedKeys++;
                    }
                }

                // Размещаем монстра
                let monsterPlaced = false;
                attempts = 0;
                while (!monsterPlaced && attempts < 500) {
                    attempts++;
                    const x = 1 + Math.floor(Math.random() * (MAZE_SIZE - 2));
                    const z = 1 + Math.floor(Math.random() * (MAZE_SIZE - 2));
                    if (maze[x][z] === 0 && !(x === 1 && z === 1) && 
                        !keys.some(k => k.x === x && k.z === z) &&
                        !(x === exitPos.x && z === exitPos.z)) {
                        monsterPos = { x, z };
                        monsterPlaced = true;
                    }
                }

                // Если монстр не разместился, ставим его вручную
                if (!monsterPlaced) {
                    monsterPos = { x: MAZE_SIZE - 2, z: MAZE_SIZE - 2 };
                }
                createMonster();

                // Выход
                createExit();

                // Ставим игрока
                playerPos = { x: 1, z: 1 };
                createPlayer();

                updateMinimap();
            }

            // ---------- СОЗДАНИЕ ИГРОКА ----------
            let playerMesh = null;
            function createPlayer() {
                if (playerMesh) {
                    scene.remove(playerMesh);
                }
                
                const group = new THREE.Group();
                
                // Тело (фонарик)
                const bodyGeo = new THREE.SphereGeometry(0.2, 12, 12);
                const bodyMat = new THREE.MeshStandardMaterial({
                    color: 0x4ecdc4,
                    emissive: 0x4ecdc4,
                    emissiveIntensity: 0.2
                });
                const body = new THREE.Mesh(bodyGeo, bodyMat);
                body.position.y = 0.2;
                group.add(body);
                
                // Свет вокруг игрока
                const glowGeo = new THREE.SphereGeometry(0.4, 8, 8);
                const glowMat = new THREE.MeshBasicMaterial({
                    color: 0x4ecdc4,
                    transparent: true,
                    opacity: 0.1
                });
                const glow = new THREE.Mesh(glowGeo, glowMat);
                glow.position.y = 0.2;
                group.add(glow);
                
                // Индикатор направления (маленькая стрелка)
                const arrowGeo = new THREE.ConeGeometry(0.1, 0.2, 6);
                const arrowMat = new THREE.MeshBasicMaterial({ color: 0x4ecdc4 });
                const arrow = new THREE.Mesh(arrowGeo, arrowMat);
                arrow.position.set(0, 0.4, -0.25);
                arrow.rotation.x = 0.3;
                group.add(arrow);
                
                const posX = (playerPos.x - MAZE_SIZE/2 + 0.5) * CELL_SIZE;
                const posZ = (playerPos.z - MAZE_SIZE/2 + 0.5) * CELL_SIZE;
                group.position.set(posX, 0, posZ);
                group.userData.isMaze = true;
                group.userData.isPlayer = true;
                scene.add(group);
                playerMesh = group;
            }

            // ---------- СОЗДАНИЕ МОНСТРА ----------
            let monsterMesh = null;
            let monsterPulse = 0;
            function createMonster() {
                if (monsterMesh) {
                    scene.remove(monsterMesh);
                }
                
                const group = new THREE.Group();
                
                // Тело (красное, пульсирующее)
                const bodyGeo = new THREE.SphereGeometry(0.3, 12, 12);
                const bodyMat = new THREE.MeshStandardMaterial({
                    color: 0xff0044,
                    emissive: 0xff0044,
                    emissiveIntensity: 0.5,
                    transparent: true,
                    opacity: 0.9
                });
                const body = new THREE.Mesh(bodyGeo, bodyMat);
                body.position.y = 0.3;
                group.add(body);
                
                // Глаза
                const eyeMat = new THREE.MeshBasicMaterial({ color: 0xff0000 });
                const eyeGeo = new THREE.SphereGeometry(0.05, 6, 6);
                const eye1 = new THREE.Mesh(eyeGeo, eyeMat);
                eye1.position.set(-0.12, 0.38, -0.2);
                group.add(eye1);
                const eye2 = new THREE.Mesh(eyeGeo, eyeMat);
                eye2.position.set(0.12, 0.38, -0.2);
                group.add(eye2);
                
                // Тени вокруг (дым)
                const smokeMat = new THREE.MeshBasicMaterial({
                    color: 0x660022,
                    transparent: true,
                    opacity: 0.2
                });
                for (let i = 0; i < 5; i++) {
                    const smoke = new THREE.Mesh(
                        new THREE.SphereGeometry(0.1 + Math.random() * 0.15, 6, 6),
                        smokeMat
                    );
                    smoke.position.set(
                        (Math.random() - 0.5) * 0.5,
                        0.1 + Math.random() * 0.3,
                        (Math.random() - 0.5) * 0.5
                    );
                    group.add(smoke);
                }
                
                // Аура
                const auraMat = new THREE.MeshBasicMaterial({
                    color: 0xff0044,
                    transparent: true,
                    opacity: 0.05
                });
                const aura = new THREE.Mesh(
                    new THREE.SphereGeometry(0.7, 8, 8),
                    auraMat
                );
                aura.position.y = 0.3;
                group.add(aura);
                
                const posX = (monsterPos.x - MAZE_SIZE/2 + 0.5) * CELL_SIZE;
                const posZ = (monsterPos.z - MAZE_SIZE/2 + 0.5) * CELL_SIZE;
                group.position.set(posX, 0, posZ);
                group.userData.isMaze = true;
                group.userData.isMonster = true;
                scene.add(group);
                monsterMesh = group;
            }

            // ---------- СОЗДАНИЕ ВЫХОДА ----------
            function createExit() {
                const x = (exitPos.x - MAZE_SIZE/2 + 0.5) * CELL_SIZE;
                const z = (exitPos.z - MAZE_SIZE/2 + 0.5) * CELL_SIZE;
                
                // Врата
                const gateMat = new THREE.MeshStandardMaterial({
                    color: 0x4ecdc4,
                    emissive: 0x4ecdc4,
                    emissiveIntensity: 0.3,
                    transparent: true,
                    opacity: 0.6
                });
                
                for (let i = -1; i <= 1; i += 2) {
                    const pillar = new THREE.Mesh(
                        new THREE.BoxGeometry(0.08, 0.8, 0.08),
                        gateMat
                    );
                    pillar.position.set(x + i * 0.25, 0.4, z);
                    pillar.userData.isMaze = true;
                    scene.add(pillar);
                }
                
                // Арка сверху
                const arch = new THREE.Mesh(
                    new THREE.BoxGeometry(0.6, 0.08, 0.08),
                    gateMat
                );
                arch.position.set(x, 0.8, z);
                arch.userData.isMaze = true;
                scene.add(arch);
                
                // Световой столб
                const beamMat = new THREE.MeshBasicMaterial({
                    color: 0x4ecdc4,
                    transparent: true,
                    opacity: 0.05
                });
                const beam = new THREE.Mesh(
                    new THREE.CylinderGeometry(0.1, 0.5, 3, 8),
                    beamMat
                );
                beam.position.set(x, 1.5, z);
                beam.userData.isMaze = true;
                scene.add(beam);
                
                // Частицы вокруг выхода
                const particleCount = 30;
                const particleGeo = new THREE.BufferGeometry();
                const positions = new Float32Array(particleCount * 3);
                for (let i = 0; i < particleCount; i++) {
                    const angle = Math.random() * Math.PI * 2;
                    const radius = 0.2 + Math.random() * 0.4;
                    positions[i*3] = x + Math.cos(angle) * radius;
                    positions[i*3+1] = Math.random() * 1.5;
                    positions[i*3+2] = z + Math.sin(angle) * radius;
                }
                particleGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
                const particleMat = new THREE.PointsMaterial({
                    color: 0x4ecdc4,
                    size: 0.02,
                    transparent: true,
                    opacity: 0.5,
                    blending: THREE.AdditiveBlending
                });
                const particles = new THREE.Points(particleGeo, particleMat);
                particles.userData.isMaze = true;
                scene.add(particles);
            }

            // ---------- МИНИ-КАРТА ----------
            function updateMinimap() {
                const ctx = minimapCtx;
                const size = 150;
                ctx.fillStyle = '#0a0a0a';
                ctx.fillRect(0, 0, size, size);
                
                const cellSize = size / MAZE_SIZE;
                
                // Стены
                for (let i = 0; i < MAZE_SIZE; i++) {
                    for (let j = 0; j < MAZE_SIZE; j++) {
                        if (maze[i][j] === 1) {
                            ctx.fillStyle = '#1a1a3a';
                            ctx.fillRect(i * cellSize, j * cellSize, cellSize, cellSize);
                        }
                    }
                }
                
                // Ключи
                for (const key of keys) {
                    if (!key.collected) {
                        ctx.fillStyle = '#f7d44a';
                        ctx.beginPath();
                        ctx.arc(key.x * cellSize + cellSize/2, key.z * cellSize + cellSize/2, 3, 0, Math.PI * 2);
                        ctx.fill();
                    }
                }
                
                // Выход
                ctx.fillStyle = '#4ecdc4';
                ctx.fillRect(exitPos.x * cellSize + 2, exitPos.z * cellSize + 2, cellSize - 4, cellSize - 4);
                
                // Монстр
                ctx.fillStyle = '#ff0044';
                ctx.shadowColor = '#ff0044';
                ctx.shadowBlur = 5;
                ctx.beginPath();
                ctx.arc(monsterPos.x * cellSize + cellSize/2, monsterPos.z * cellSize + cellSize/2, 4, 0, Math.PI * 2);
                ctx.fill();
                ctx.shadowBlur = 0;
                
                // Игрок
                ctx.fillStyle = '#4ecdc4';
                ctx.shadowColor = '#4ecdc4';
                ctx.shadowBlur = 5;
                ctx.beginPath();
                ctx.arc(playerPos.x * cellSize + cellSize/2, playerPos.z * cellSize + cellSize/2, 4, 0, Math.PI * 2);
                ctx.fill();
                ctx.shadowBlur = 0;
            }

            // ---------- ПОИСК ПУТИ (BFS) ДЛЯ МОНСТРА ----------
            function findPath(startX, startZ, targetX, targetZ) {
                // Простой поиск в ширину с ограничением
                const queue = [{ x: startX, z: startZ, path: [] }];
                const visited = new Set();
                visited.add(`${startX},${startZ}`);
                
                const directions = [
                    [0, -1], [0, 1], [-1, 0], [1, 0]
                ];
                
                let steps = 0;
                while (queue.length > 0 && steps < 100) {
                    steps++;
                    const current = queue.shift();
                    
                    if (current.x === targetX && current.z === targetZ) {
                        return current.path;
                    }
                    
                    for (const [dx, dz] of directions) {
                        const nx = current.x + dx;
                        const nz = current.z + dz;
                        const key = `${nx},${nz}`;
                        
                        if (!visited.has(key) && 
                            nx >= 0 && nx < MAZE_SIZE && 
                            nz >= 0 && nz < MAZE_SIZE &&
                            maze[nx][nz] === 0) {
                            visited.add(key);
                            const newPath = [...current.path, { x: nx, z: nz }];
                            queue.push({ x: nx, z: nz, path: newPath });
                        }
                    }
                }
                return null;
            }

            // ---------- ДВИЖЕНИЕ МОНСТРА ----------
            function moveMonster(deltaTime) {
                if (!gameActive) return;
                
                const dist = Math.sqrt(
                    Math.pow(playerPos.x - monsterPos.x, 2) + 
                    Math.pow(playerPos.z - monsterPos.z, 2)
                );
                
                monsterChasing = dist < MONSTER_DETECTION_RANGE;
                
                if (monsterChasing) {
                    statusDisplay.textContent = '👁️ Монстр видит тебя!';
                    statusDisplay.style.color = '#ff0044';
                    statusDisplay.style.animation = 'heartbeat 0.5s ease-in-out infinite';
                } else {
                    statusDisplay.textContent = '🌙 В безопасности';
                    statusDisplay.style.color = '#4ecdc4';
                    statusDisplay.style.animation = 'none';
                }
                
                // Если монстр близко к игроку — атака
                if (dist < MONSTER_ATTACK_RANGE) {
                    gameOver(false);
                    return;
                }
                
                // Движение монстра
                const speed = monsterChasing ? MONSTER_SPEED_FAST : MONSTER_SPEED_BASE;
                const moveAmount = speed * deltaTime;
                
                if (monsterChasing) {
                    // Преследование с использованием BFS
                    const path = findPath(
                        Math.round(monsterPos.x), Math.round(monsterPos.z),
                        Math.round(playerPos.x), Math.round(playerPos.z)
                    );
                    
                    if (path && path.length > 0) {
                        const next = path[0];
                        // Проверяем, что следующая клетка не занята стеной
                        if (maze[Math.round(next.x)] && maze[Math.round(next.x)][Math.round(next.z)] === 0) {
                            // Плавное движение
                            const dx = next.x - monsterPos.x;
                            const dz = next.z - monsterPos.z;
                            const distToNext = Math.sqrt(dx*dx + dz*dz);
                            if (distToNext > 0.01) {
                                const step = Math.min(moveAmount / CELL_SIZE, distToNext);
                                monsterPos.x += (dx / distToNext) * step;
                                monsterPos.z += (dz / distToNext) * step;
                            }
                        }
                    }
                } else {
                    // Случайное блуждание
                    if (Math.random() < 0.01) {
                        // Меняем направление
                        const dirs = [
                            [0, -1], [0, 1], [-1, 0], [1, 0]
                        ];
                        const dir = dirs[Math.floor(Math.random() * dirs.length)];
                        const newX = Math.round(monsterPos.x + dir[0]);
                        const newZ = Math.round(monsterPos.z + dir[1]);
                        if (newX >= 0 && newX < MAZE_SIZE && newZ >= 0 && newZ < MAZE_SIZE && 
                            maze[newX][newZ] === 0) {
                            monsterPos.x += dir[0] * moveAmount / CELL_SIZE;
                            monsterPos.z += dir[1] * moveAmount / CELL_SIZE;
                        }
                    }
                }
                
                // Ограничиваем позицию монстра
                monsterPos.x = Math.max(0.1, Math.min(MAZE_SIZE - 0.1, monsterPos.x));
                monsterPos.z = Math.max(0.1, Math.min(MAZE_SIZE - 0.1, monsterPos.z));
                
                // Обновляем позицию монстра в сцене
                if (monsterMesh) {
                    const posX = (monsterPos.x - MAZE_SIZE/2 + 0.5) * CELL_SIZE;
                    const posZ = (monsterPos.z - MAZE_SIZE/2 + 0.5) * CELL_SIZE;
                    monsterMesh.position.set(posX, 0, posZ);
                    
                    // Пульсация
                    monsterPulse += deltaTime * 3;
                    const pulse = 1 + 0.05 * Math.sin(monsterPulse);
                    monsterMesh.scale.set(pulse, pulse, pulse);
                    
                    // Вращение в сторону игрока при преследовании
                    if (monsterChasing) {
                        const dx = playerPos.x - monsterPos.x;
                        const dz = playerPos.z - monsterPos.z;
                        monsterMesh.rotation.y = Math.atan2(dx, dz);
                    }
                }
            }

            // ---------- ДВИЖЕНИЕ ИГРОКА ----------
            function movePlayer(deltaTime) {
                if (!gameActive) return;
                
                const speed = keysPressed.shift ? PLAYER_RUN_SPEED : PLAYER_SPEED;
                const moveAmount = speed * deltaTime / CELL_SIZE;
                
                let dx = 0, dz = 0;
                if (keysPressed.w) dz -= moveAmount;
                if (keysPressed.s) dz += moveAmount;
                if (keysPressed.a) dx -= moveAmount;
                if (keysPressed.d) dx += moveAmount;
                
                // Нормализуем диагональ
                if (dx !== 0 && dz !== 0) {
                    dx *= 0.707;
                    dz *= 0.707;
                }
                
                // Проверяем столкновения по отдельности
                const newX = playerPos.x + dx;
                const newZ = playerPos.z + dz;
                
                // Проверка X
                const xCheck = Math.round(newX);
                const zCheck = Math.round(playerPos.z);
                if (xCheck >= 0 && xCheck < MAZE_SIZE && zCheck >= 0 && zCheck < MAZE_SIZE && 
                    maze[xCheck][zCheck] === 0) {
                    playerPos.x = newX;
                }
                
                // Проверка Z
                const xCheck2 = Math.round(playerPos.x);
                const zCheck2 = Math.round(newZ);
                if (xCheck2 >= 0 && xCheck2 < MAZE_SIZE && zCheck2 >= 0 && zCheck2 < MAZE_SIZE && 
                    maze[xCheck2][zCheck2] === 0) {
                    playerPos.z = newZ;
                }
                
                // Ограничиваем позицию
                playerPos.x = Math.max(0.1, Math.min(MAZE_SIZE - 0.1, playerPos.x));
                playerPos.z = Math.max(0.1, Math.min(MAZE_SIZE - 0.1, playerPos.z));
                
                // Обновляем позицию игрока в сцене
                if (playerMesh) {
                    const posX = (playerPos.x - MAZE_SIZE/2 + 0.5) * CELL_SIZE;
                    const posZ = (playerPos.z - MAZE_SIZE/2 + 0.5) * CELL_SIZE;
                    playerMesh.position.set(posX, 0, posZ);
                    
                    // Поворот в сторону движения
                    if (dx !== 0 || dz !== 0) {
                        playerMesh.rotation.y = Math.atan2(dx, dz);
                    }
                }
                
                // Обновляем свет игрока
                const lightX = (playerPos.x - MAZE_SIZE/2 + 0.5) * CELL_SIZE;
                const lightZ = (playerPos.z - MAZE_SIZE/2 + 0.5) * CELL_SIZE;
                playerLight.position.set(lightX, 1.5, lightZ);
                playerLight2.position.set(lightX, 0.5, lightZ);
                
                // Проверка сбора ключей
                for (const key of keys) {
                    if (!key.collected) {
                        const dist = Math.sqrt(
                            Math.pow(playerPos.x - key.x, 2) + 
                            Math.pow(playerPos.z - key.z, 2)
                        );
                        if (dist < 0.5) {
                            key.collected = true;
                            collectedKeys++;
                            keysDisplay.textContent = `${collectedKeys} / ${TOTAL_KEYS}`;
                            
                            // Удаляем ключ из сцены
                            scene.children.forEach(child => {
                                if (child.userData && child.userData.isKey && 
                                    child.userData.keyIndex === keys.indexOf(key)) {
                                    scene.remove(child);
                                }
                            });
                            
                            // Эффект сбора
                            const posX2 = (key.x - MAZE_SIZE/2 + 0.5) * CELL_SIZE;
                            const posZ2 = (key.z - MAZE_SIZE/2 + 0.5) * CELL_SIZE;
                            createCollectionEffect(posX2, posZ2);
                            
                            // Если собраны все ключи — активируем выход
                            if (collectedKeys === TOTAL_KEYS) {
                                statusDisplay.textContent = '🚪 Выход открыт!';
                                statusDisplay.style.color = '#4ecdc4';
                            }
                        }
                    }
                }
                
                // Проверка выхода (если все ключи собраны)
                if (collectedKeys === TOTAL_KEYS) {
                    const distToExit = Math.sqrt(
                        Math.pow(playerPos.x - exitPos.x, 2) + 
                        Math.pow(playerPos.z - exitPos.z, 2)
                    );
                    if (distToExit < 0.5) {
                        gameOver(true);
                        return;
                    }
                }
                
                // Проверка столкновения с монстром
                const distToMonster = Math.sqrt(
                    Math.pow(playerPos.x - monsterPos.x, 2) + 
                    Math.pow(playerPos.z - monsterPos.z, 2)
                );
                if (distToMonster < MONSTER_ATTACK_RANGE) {
                    gameOver(false);
                }
                
                updateMinimap();
            }

            // ---------- ЭФФЕКТ СБОРА КЛЮЧА ----------
            function createCollectionEffect(x, z) {
                const count = 30;
                const geo = new THREE.BufferGeometry();
                const positions = new Float32Array(count * 3);
                const velocities = [];
                
                for (let i = 0; i < count; i++) {
                    positions[i*3] = x;
                    positions[i*3+1] = 0.3;
                    positions[i*3+2] = z;
                    velocities.push({
                        x: (Math.random() - 0.5) * 3,
                        y: 1 + Math.random() * 3,
                        z: (Math.random() - 0.5) * 3
                    });
                }
                
                geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
                const mat = new THREE.PointsMaterial({
                    color: 0xf7d44a,
                    size: 0.05,
                    transparent: true,
                    opacity: 1,
                    blending: THREE.AdditiveBlending
                });
                const points = new THREE.Points(geo, mat);
                scene.add(points);
                
                let life = 1.0;
                const interval = setInterval(() => {
                    life -= 0.02;
                    if (life <= 0) {
                        clearInterval(interval);
                        scene.remove(points);
                        return;
                    }
                    const pos = points.geometry.attributes.position.array;
                    for (let i = 0; i < count; i++) {
                        pos[i*3] += velocities[i].x * 0.02;
                        pos[i*3+1] += velocities[i].y * 0.02;
                        pos[i*3+2] += velocities[i].z * 0.02;
                        velocities[i].y -= 0.02;
                    }
                    points.geometry.attributes.position.needsUpdate = true;
                    points.material.opacity = life;
                }, 16);
            }

            // ---------- КОНЕЦ ИГРЫ ----------
            function gameOver(won) {
                if (!gameActive) return;
                gameActive = false;
                gameWon = won;
                
                if (timerInterval) {
                    clearInterval(timerInterval);
                    timerInterval = null;
                }
                
                gameOverDiv.style.display = 'block';
                if (won) {
                    resultTitle.textContent = '🎉 ПОБЕДА!';
                    resultTitle.className = 'win';
                    resultDesc.textContent = `Вы выбрались из лабиринта за ${formatTime(timeElapsed)}!`;
                } else {
                    resultTitle.textContent = '💀 ПОГИБЕЛЬ';
                    resultTitle.className = 'lose';
                    resultDesc.textContent = 'Монстр добрался до вас... Попробуйте снова!';
                }
            }

            // ---------- ФОРМАТИРОВАНИЕ ВРЕМЕНИ ----------
            function formatTime(seconds) {
                const m = Math.floor(seconds / 60);
                const s = Math.floor(seconds % 60);
                return `${m}:${s.toString().padStart(2, '0')}`;
            }

            // ---------- ТАЙМЕР ----------
            function startTimer() {
                if (timerInterval) clearInterval(timerInterval);
                timeElapsed = 0;
                timerInterval = setInterval(() => {
                    if (gameActive) {
                        timeElapsed++;
                        timerDisplay.textContent = formatTime(timeElapsed);
                    }
                }, 1000);
            }

            // ---------- РЕСТАРТ ----------
            function restartGame() {
                gameActive = true;
                gameWon = false;
                collectedKeys = 0;
                monsterChasing = false;
                gameOverDiv.style.display = 'none';
                keysDisplay.textContent = `0 / ${TOTAL_KEYS}`;
                statusDisplay.textContent = '🌙 В безопасности';
                statusDisplay.style.color = '#4ecdc4';
                statusDisplay.style.animation = 'none';
                
                // Очищаем сцену от объектов лабиринта
                const toRemove = [];
                scene.children.forEach(child => {
                    if (child.userData && child.userData.isMaze) {
                        toRemove.push(child);
                    }
                });
                toRemove.forEach(child => scene.remove(child));
                
                // Пересоздаём лабиринт
                createMazeScene();
                startTimer();
                updateMinimap();
            }

            // ---------- ОБНОВЛЕНИЕ ----------
            function update(deltaTime) {
                if (!gameActive) return;
                movePlayer(deltaTime);
                moveMonster(deltaTime);
                
                // Анимация ключей (вращение)
                scene.children.forEach(child => {
                    if (child.userData && child.userData.isKey) {
                        child.rotation.y += deltaTime * 2;
                        child.position.y = Math.sin(Date.now() / 500 + child.userData.keyIndex) * 0.1 + 0.2;
                    }
                });
            }

            // ---------- АНИМАЦИЯ ----------
            let lastTime = 0;
            function animate(time) {
                requestAnimationFrame(animate);
                const delta = Math.min((time - lastTime) / 1000, 0.05);
                lastTime = time;
                update(delta);
                renderer.render(scene, camera);
            }

            // ---------- СОБЫТИЯ КЛАВИШ ----------
            document.addEventListener('keydown', (e) => {
                const key = e.key.toLowerCase();
                if (key === 'w' || key === 'ц') keysPressed.w = true;
                if (key === 'a' || key === 'ф') keysPressed.a = true;
                if (key === 's' || key === 'ы') keysPressed.s = true;
                if (key === 'd' || key === 'в') keysPressed.d = true;
                if (key === 'shift') keysPressed.shift = true;
                if (key === 'r') restartGame();
                e.preventDefault();
            });
            
            document.addEventListener('keyup', (e) => {
                const key = e.key.toLowerCase();
                if (key === 'w' || key === 'ц') keysPressed.w = false;
                if (key === 'a' || key === 'ф') keysPressed.a = false;
                if (key === 's' || key === 'ы') keysPressed.s = false;
                if (key === 'd' || key === 'в') keysPressed.d = false;
                if (key === 'shift') keysPressed.shift = false;
            });

            // ---------- РЕСТАРТ КНОПКА ----------
            restartBtn.addEventListener('click', restartGame);

            // ---------- РЕСАЙЗ ----------
            window.addEventListener('resize', () => {
                const w = window.innerWidth;
                const h = window.innerHeight;
                camera.aspect = w / h;
                camera.updateProjectionMatrix();
                renderer.setSize(w, h);
            });

            // ---------- ЗАПУСК ----------
            createMazeScene();
            startTimer();
            animate(0);

            console.log('👻 Лабиринт Теней загружен!');
            console.log('Собери все ключи и найди выход, убегая от монстра!');
            console.log('Управление: WASD — движение, Shift — бег, R — рестарт');

        })();
    </script>
</body>
</html>

Game Source: Лабиринт Теней

Creator: LuckyGlider65

Libraries: none

Complexity: complex (1155 lines, 49.5 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: game-luckyglider65-mruym7y3" to link back to the original. Then publish at arcadelab.ai/publish.