Лабиринт Теней
by LuckyGlider651155 lines49.5 KB
<!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> движение • <kbd>Shift</kbd> бег • <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.