Обновить doom.html

This commit is contained in:
2026-06-15 18:31:30 +03:00
parent 26c2b65ad9
commit 548d09376d
+430 -199
View File
@@ -3,53 +3,55 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DOOM ETERNAL: Web 3D Prototype</title> <title>DOOM ETERNAL: Advanced Web 3D</title>
<style> <style>
body { margin: 0; overflow: hidden; background: #000; font-family: 'Impact', 'Arial Black', sans-serif; user-select: none; } body { margin: 0; overflow: hidden; background: #000; font-family: 'Impact', 'Arial Black', sans-serif; user-select: none; }
#game-container { position: relative; width: 100vw; height: 100vh; } #game-container { position: relative; width: 100vw; height: 100vh; }
/* Прицел */
#crosshair { #crosshair {
position: absolute; top: 50%; left: 50%; width: 20px; height: 20px; position: absolute; top: 50%; left: 50%; width: 24px; height: 24px;
transform: translate(-50%, -50%); pointer-events: none; z-index: 10; transform: translate(-50%, -50%); pointer-events: none; z-index: 10;
} }
#crosshair::before, #crosshair::after { #crosshair::before, #crosshair::after {
content: ''; position: absolute; background: #00ff00; box-shadow: 0 0 5px #00ff00; content: ''; position: absolute; background: #00ff00; box-shadow: 0 0 8px #00ff00;
} }
#crosshair::before { top: 9px; left: 0; width: 20px; height: 2px; } #crosshair::before { top: 11px; left: 0; width: 24px; height: 2px; }
#crosshair::after { top: 0; left: 9px; width: 2px; height: 20px; } #crosshair::after { top: 0; left: 11px; width: 2px; height: 24px; }
/* Экран старта */
#blocker { #blocker {
position: absolute; width: 100%; height: 100%; background: rgba(0,0,0,0.85); position: absolute; width: 100%; height: 100%; background: rgba(0,0,0,0.9);
display: flex; flex-direction: column; align-items: center; justify-content: center; display: flex; flex-direction: column; align-items: center; justify-content: center;
z-index: 20; color: #fff; cursor: pointer; z-index: 20; color: #fff; cursor: pointer;
} }
#instructions { font-size: 36px; text-transform: uppercase; letter-spacing: 4px; color: #ff3333; text-shadow: 0 0 15px #ff0000; text-align: center; } #instructions { font-size: 42px; text-transform: uppercase; letter-spacing: 6px; color: #ff3333; text-shadow: 0 0 20px #ff0000; text-align: center; }
#sub-instructions { font-size: 18px; color: #aaa; margin-top: 20px; font-family: monospace; } #sub-instructions { font-size: 16px; color: #888; margin-top: 20px; font-family: monospace; letter-spacing: 1px; }
/* HUD в стиле Doom Eternal */
#hud { #hud {
position: absolute; bottom: 0; left: 0; width: 100%; height: 100px; position: absolute; bottom: 0; left: 0; width: 100%; height: 110px;
background: linear-gradient(to top, #0a0a0a 80%, transparent); background: linear-gradient(to top, #050000 85%, transparent);
border-top: 3px solid #8b0000; display: flex; justify-content: space-between; border-top: 4px solid #8b0000; display: flex; justify-content: space-between;
align-items: center; padding: 0 40px; box-sizing: border-box; pointer-events: none; z-index: 10; align-items: center; padding: 0 50px; box-sizing: border-box; pointer-events: none; z-index: 10;
} }
.stat-box { text-align: center; color: #fff; } .stat-box { text-align: center; color: #fff; position: relative; }
.stat-label { font-size: 14px; color: #888; letter-spacing: 2px; text-transform: uppercase; } .stat-label { font-size: 14px; color: #666; letter-spacing: 3px; text-transform: uppercase; font-family: monospace; }
.stat-value { font-size: 48px; line-height: 48px; text-shadow: 0 0 10px currentColor; } .stat-value { font-size: 56px; line-height: 56px; text-shadow: 0 0 15px currentColor; font-family: 'Impact', sans-serif; }
.health { color: #00ff00; } .health { color: #00ff00; }
.armor { color: #00bfff; } .armor { color: #00bfff; }
.ammo { color: #ffcc00; } .ammo { color: #ffcc00; }
#damage-overlay { #damage-overlay {
position: absolute; top: 0; left: 0; width: 100%; height: 100%; position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background: radial-gradient(circle, transparent 50%, rgba(255,0,0,0.6) 100%); background: radial-gradient(circle, transparent 40%, rgba(139, 0, 0, 0.8) 100%);
opacity: 0; pointer-events: none; transition: opacity 0.2s; z-index: 5; opacity: 0; pointer-events: none; transition: opacity 0.1s; z-index: 5;
}
#ammo-warning {
position: absolute; bottom: 130px; left: 50%; transform: translateX(-50%);
color: #ff0000; font-size: 24px; font-family: monospace; opacity: 0; transition: opacity 0.3s;
text-shadow: 0 0 10px #ff0000;
} }
</style> </style>
<!-- Импорт Three.js через CDN -->
<script type="importmap"> <script type="importmap">
{ {
"imports": { "imports": {
@@ -64,10 +66,11 @@
<div id="game-container"> <div id="game-container">
<div id="damage-overlay"></div> <div id="damage-overlay"></div>
<div id="crosshair"></div> <div id="crosshair"></div>
<div id="ammo-warning">МАЛО ПАТРОНОВ!</div>
<div id="blocker"> <div id="blocker">
<div id="instructions">КЛИКНИ, ЧТОБЫ ИГРАТЬ</div> <div id="instructions">КЛИКНИ ДЛЯ СТАРТА</div>
<div id="sub-instructions">WASD - Движение | Мышь - Обзор | ЛКМ - Огонь | RIP AND TEAR</div> <div id="sub-instructions">WASD - Движение | Мышь - Обзор | ЛКМ - Огонь | R - Перезарядка (симуляция)</div>
</div> </div>
<div id="hud"> <div id="hud">
@@ -97,171 +100,319 @@
const velocity = new THREE.Vector3(); const velocity = new THREE.Vector3();
const direction = new THREE.Vector3(); const direction = new THREE.Vector3();
// Игровые параметры
let health = 100, armor = 50, ammo = 24; let health = 100, armor = 50, ammo = 24;
const MAX_AMMO = 50;
let enemies = []; let enemies = [];
let particles = []; let particles = [];
let weaponGroup; let pickups = [];
let isShooting = false; let weaponGroup, pumpGroup;
let muzzleLight; let isShooting = false, isPumping = false;
let muzzleLight, ambientLight;
let audioCtx;
// Карта: 1 = Стена, 0 = Пол, 2 = Спавн врага, 3 = Спавн патронов, 9 = Игрок
const mapLayout = [
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,9,0,0,0,1,0,0,0,0,0,0,1,0,0,3,0,0,0,0,0,0,0,0,1],
[1,0,1,1,0,1,0,1,1,1,1,0,1,0,1,1,1,1,0,1,1,1,1,0,1],
[1,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,1,0,1],
[1,0,1,0,1,1,1,1,1,0,1,1,1,1,1,1,0,1,1,1,1,0,1,0,1],
[1,0,0,0,1,3,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,1],
[1,1,1,0,1,0,1,1,1,2,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1],
[1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,1,1,1,1,1,0,1,1,1,0,1,1,1,1,0,1,1,1,1,1,1,0,1],
[1,0,0,0,0,0,0,0,1,3,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1],
[1,1,1,0,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,0,1,0,1,0,1],
[1,2,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,1],
[1,0,1,1,1,0,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1],
[1,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,1],
[1,1,1,1,1,1,1,0,1,0,1,0,1,0,1,1,1,1,1,1,1,1,1,0,1],
[1,3,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1],
[1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
];
const CELL_SIZE = 4;
// Элементы UI
const blocker = document.getElementById('blocker');
const instructions = document.getElementById('instructions');
const damageOverlay = document.getElementById('damage-overlay');
const uiHealth = document.getElementById('health-val'); const uiHealth = document.getElementById('health-val');
const uiArmor = document.getElementById('armor-val'); const uiArmor = document.getElementById('armor-val');
const uiAmmo = document.getElementById('ammo-val'); const uiAmmo = document.getElementById('ammo-val');
const ammoWarning = document.getElementById('ammo-warning');
const damageOverlay = document.getElementById('damage-overlay');
init(); init();
animate(); animate();
// --- Процедурная генерация текстур ---
function createNoiseTexture(baseColor, noiseColor, scale = 1) {
const canvas = document.createElement('canvas');
canvas.width = 256 * scale; canvas.height = 256 * scale;
const ctx = canvas.getContext('2d');
ctx.fillStyle = baseColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < 4000 * scale; i++) {
ctx.fillStyle = noiseColor;
ctx.globalAlpha = Math.random() * 0.3;
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height;
const size = Math.random() * 3 * scale;
ctx.fillRect(x, y, size, size);
}
// Добавим "швы" или линии для реалистичности
ctx.globalAlpha = 0.1;
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, canvas.height/2); ctx.lineTo(canvas.width, canvas.height/2);
ctx.moveTo(canvas.width/2, 0); ctx.lineTo(canvas.width/2, canvas.height);
ctx.stroke();
const tex = new THREE.CanvasTexture(canvas);
tex.wrapS = THREE.RepeatWrapping;
tex.wrapT = THREE.RepeatWrapping;
return tex;
}
const wallTexture = createNoiseTexture('#3a1515', '#5a2020', 2);
const floorTexture = createNoiseTexture('#1a1a1a', '#2a2a2a', 4);
floorTexture.repeat.set(10, 10);
// --- Аудио движок (Web Audio API) ---
function initAudio() {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
}
function playSound(type) {
if (!audioCtx) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
const now = audioCtx.currentTime;
if (type === 'shoot') {
// Шум выстрела (симуляция через буфер)
const bufferSize = audioCtx.sampleRate * 0.2;
const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = Math.random() * 2 - 1;
}
const noise = audioCtx.createBufferSource();
noise.buffer = buffer;
const noiseGain = audioCtx.createGain();
noise.connect(noiseGain);
noiseGain.connect(audioCtx.destination);
noiseGain.gain.setValueAtTime(0.8, now);
noiseGain.gain.exponentialRampToValueAtTime(0.01, now + 0.2);
noise.start(now);
// Низкий удар
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(150, now);
osc.frequency.exponentialRampToValueAtTime(40, now + 0.15);
gain.gain.setValueAtTime(0.5, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.15);
osc.start(now);
osc.stop(now + 0.2);
} else if (type === 'pickup') {
osc.type = 'sine';
osc.frequency.setValueAtTime(400, now);
osc.frequency.linearRampToValueAtTime(800, now + 0.1);
gain.gain.setValueAtTime(0.3, now);
gain.gain.linearRampToValueAtTime(0, now + 0.15);
osc.start(now);
osc.stop(now + 0.15);
} else if (type === 'hit') {
osc.type = 'square';
osc.frequency.setValueAtTime(100, now);
osc.frequency.exponentialRampToValueAtTime(50, now + 0.1);
gain.gain.setValueAtTime(0.3, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.1);
osc.start(now);
osc.stop(now + 0.1);
}
}
function init() { function init() {
// 1. Сцена и Камера
scene = new THREE.Scene(); scene = new THREE.Scene();
scene.background = new THREE.Color(0x050000); scene.background = new THREE.Color(0x050000);
// Адский туман scene.fog = new THREE.FogExp2(0x1a0505, 0.04);
scene.fog = new THREE.FogExp2(0x1a0505, 0.035);
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.y = 1.6; // Рост игрока
// 2. Освещение ambientLight = new THREE.AmbientLight(0x220505, 0.6);
const ambientLight = new THREE.AmbientLight(0x401010, 0.5); // Темно-красный ambient
scene.add(ambientLight); scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xff4400, 0.8); muzzleLight = new THREE.PointLight(0xffaa00, 0, 15);
dirLight.position.set(10, 20, 10); muzzleLight.castShadow = true;
dirLight.castShadow = true;
scene.add(dirLight);
// Вспышка выстрела (PointLight)
muzzleLight = new THREE.PointLight(0xffaa00, 0, 10);
scene.add(muzzleLight); scene.add(muzzleLight);
// 3. Окружение (Пол и Стены) // Генерация уровня
const floorGeometry = new THREE.PlaneGeometry(100, 100); const wallGeo = new THREE.BoxGeometry(CELL_SIZE, CELL_SIZE * 1.5, CELL_SIZE);
const floorMaterial = new THREE.MeshStandardMaterial({ const wallMat = new THREE.MeshStandardMaterial({
color: 0x221111, roughness: 0.8, metalness: 0.2 map: wallTexture, roughness: 0.7, metalness: 0.3
}); });
const floor = new THREE.Mesh(floorGeometry, floorMaterial); const floorGeo = new THREE.PlaneGeometry(mapLayout[0].length * CELL_SIZE, mapLayout.length * CELL_SIZE);
const floorMat = new THREE.MeshStandardMaterial({
map: floorTexture, roughness: 0.8, metalness: 0.1
});
const floor = new THREE.Mesh(floorGeo, floorMat);
floor.rotation.x = -Math.PI / 2; floor.rotation.x = -Math.PI / 2;
floor.position.set((mapLayout[0].length * CELL_SIZE)/2 - CELL_SIZE/2, 0, (mapLayout.length * CELL_SIZE)/2 - CELL_SIZE/2);
floor.receiveShadow = true; floor.receiveShadow = true;
scene.add(floor); scene.add(floor);
// Создаем несколько колонн/стен для укрытий // Потолок
const boxGeo = new THREE.BoxGeometry(2, 6, 2); const ceilGeo = new THREE.PlaneGeometry(mapLayout[0].length * CELL_SIZE, mapLayout.length * CELL_SIZE);
const boxMat = new THREE.MeshStandardMaterial({ color: 0x331111, roughness: 0.9 }); const ceilMat = new THREE.MeshStandardMaterial({ color: 0x110505, roughness: 1 });
for (let i = 0; i < 15; i++) { const ceil = new THREE.Mesh(ceilGeo, ceilMat);
const wall = new THREE.Mesh(boxGeo, boxMat); ceil.rotation.x = Math.PI / 2;
wall.position.x = (Math.random() - 0.5) * 60; ceil.position.set((mapLayout[0].length * CELL_SIZE)/2 - CELL_SIZE/2, CELL_SIZE * 1.5, (mapLayout.length * CELL_SIZE)/2 - CELL_SIZE/2);
wall.position.z = (Math.random() - 0.5) * 60; scene.add(ceil);
wall.position.y = 3;
wall.castShadow = true; for (let z = 0; z < mapLayout.length; z++) {
wall.receiveShadow = true; for (let x = 0; x < mapLayout[z].length; x++) {
scene.add(wall); const type = mapLayout[z][x];
const posX = x * CELL_SIZE;
const posZ = z * CELL_SIZE;
if (type === 1) {
const wall = new THREE.Mesh(wallGeo, wallMat);
wall.position.set(posX, CELL_SIZE * 0.75, posZ);
wall.castShadow = true;
wall.receiveShadow = true;
scene.add(wall);
} else if (type === 9) {
camera.position.set(posX, 1.6, posZ);
} else if (type === 2) {
spawnEnemy(posX, posZ);
} else if (type === 3) {
spawnPickup(posX, posZ);
}
}
} }
// 4. Оружие (Супердробовик - процедурная модель) // Оружие: Супердробовик
weaponGroup = new THREE.Group(); weaponGroup = new THREE.Group();
const barrelGeo = new THREE.CylinderGeometry(0.04, 0.04, 0.8, 8); // Основной корпус
const barrelMat = new THREE.MeshStandardMaterial({ color: 0x111111, metalness: 0.8, roughness: 0.3 }); const bodyGeo = new THREE.BoxGeometry(0.12, 0.15, 0.7);
const barrel1 = new THREE.Mesh(barrelGeo, barrelMat); const bodyMat = new THREE.MeshStandardMaterial({ color: 0x222222, metalness: 0.8, roughness: 0.4 });
barrel1.rotation.x = Math.PI / 2;
barrel1.position.set(-0.03, 0, -0.4);
const barrel2 = new THREE.Mesh(barrelGeo, barrelMat);
barrel2.rotation.x = Math.PI / 2;
barrel2.position.set(0.03, 0, -0.4);
const bodyGeo = new THREE.BoxGeometry(0.15, 0.2, 0.6);
const bodyMat = new THREE.MeshStandardMaterial({ color: 0x4a3c31, metalness: 0.5 });
const body = new THREE.Mesh(bodyGeo, bodyMat); const body = new THREE.Mesh(bodyGeo, bodyMat);
body.position.z = 0.1;
weaponGroup.add(barrel1, barrel2, body); // Стволы
weaponGroup.position.set(0.3, -0.3, -0.5); const barrelGeo = new THREE.CylinderGeometry(0.035, 0.035, 0.6, 8);
camera.add(weaponGroup); // Прикрепляем оружие к камере const barrelMat = new THREE.MeshStandardMaterial({ color: 0x111111, metalness: 0.9, roughness: 0.2 });
const b1 = new THREE.Mesh(barrelGeo, barrelMat);
b1.rotation.x = Math.PI / 2; b1.position.set(-0.04, 0.05, -0.5);
const b2 = new THREE.Mesh(barrelGeo, barrelMat);
b2.rotation.x = Math.PI / 2; b2.position.set(0.04, 0.05, -0.5);
// Цевье (движущаяся часть помпы)
pumpGroup = new THREE.Group();
const pumpGeo = new THREE.BoxGeometry(0.14, 0.12, 0.25);
const pumpMat = new THREE.MeshStandardMaterial({ color: 0x4a3c31, roughness: 0.9 });
const pump = new THREE.Mesh(pumpGeo, pumpMat);
pump.position.z = 0.15;
pumpGroup.add(pump);
pumpGroup.position.z = 0;
weaponGroup.add(body, b1, b2, pumpGroup);
weaponGroup.position.set(0.35, -0.35, -0.6);
camera.add(weaponGroup);
scene.add(camera); scene.add(camera);
// 5. Управление
controls = new PointerLockControls(camera, document.body); controls = new PointerLockControls(camera, document.body);
const blocker = document.getElementById('blocker');
blocker.addEventListener('click', () => { blocker.addEventListener('click', () => {
initAudio();
controls.lock(); controls.lock();
}); });
controls.addEventListener('lock', () => { controls.addEventListener('lock', () => {
instructions.style.display = 'none';
blocker.style.display = 'none'; blocker.style.display = 'none';
}); });
controls.addEventListener('unlock', () => { controls.addEventListener('unlock', () => {
blocker.style.display = 'flex'; blocker.style.display = 'flex';
instructions.style.display = 'block'; document.getElementById('instructions').innerText = "ПАУЗА. КЛИКНИ ДЛЯ ПРОДОЛЖЕНИЯ";
instructions.innerText = "ПАУЗА. КЛИКНИ, ЧТОБЫ ПРОДОЛЖИТЬ";
}); });
document.addEventListener('keydown', onKeyDown); document.addEventListener('keydown', onKeyDown);
document.addEventListener('keyup', onKeyUp); document.addEventListener('keyup', onKeyUp);
document.addEventListener('mousedown', onMouseDown); document.addEventListener('mousedown', onMouseDown);
// 6. Рендерер renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance" });
renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight); renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true; renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.getElementById('game-container').appendChild(renderer.domElement); document.getElementById('game-container').appendChild(renderer.domElement);
window.addEventListener('resize', onWindowResize); window.addEventListener('resize', onWindowResize);
// Спавн первых врагов
for(let i=0; i<5; i++) spawnEnemy();
} }
function spawnEnemy() { function spawnEnemy(x, z) {
// Враг - демоническая светящаяся сфера (Икосаэдр) // Враг: Какодемон-подобная форма
const geometry = new THREE.IcosahedronGeometry(0.8, 1); const group = new THREE.Group();
const material = new THREE.MeshStandardMaterial({
color: 0xff0000, const bodyGeo = new THREE.IcosahedronGeometry(0.7, 1);
emissive: 0x550000, const bodyMat = new THREE.MeshStandardMaterial({ color: 0x880000, roughness: 0.3, metalness: 0.6, emissive: 0x220000 });
roughness: 0.4, const body = new THREE.Mesh(bodyGeo, bodyMat);
metalness: 0.6
const eyeGeo = new THREE.SphereGeometry(0.25, 16, 16);
const eyeMat = new THREE.MeshBasicMaterial({ color: 0xffff00 });
const eye = new THREE.Mesh(eyeGeo, eyeMat);
eye.position.set(0, 0.2, 0.6);
// Свет от врага
const light = new THREE.PointLight(0xff0000, 1, 5);
light.position.y = 0.5;
group.add(body, eye, light);
group.position.set(x, 1.2, z);
group.castShadow = true;
group.userData = { health: 40, speed: 3.5, lastAttack: 0, id: Math.random() };
scene.add(group);
enemies.push(group);
}
function spawnPickup(x, z, isDrop = false) {
const geo = new THREE.BoxGeometry(0.6, 0.4, 0.4);
const mat = new THREE.MeshStandardMaterial({
color: 0xffcc00, emissive: 0xaa6600, emissiveIntensity: 0.5, metalness: 0.8
}); });
const enemy = new THREE.Mesh(geometry, material); const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(x, isDrop ? 0.5 : 1, z);
// Случайная позиция вдали от игрока const light = new THREE.PointLight(0xffcc00, 0.5, 3);
const angle = Math.random() * Math.PI * 2; mesh.add(light);
const radius = 15 + Math.random() * 20;
enemy.position.x = camera.position.x + Math.cos(angle) * radius;
enemy.position.z = camera.position.z + Math.sin(angle) * radius;
enemy.position.y = 1;
enemy.castShadow = true; scene.add(mesh);
enemy.userData = { health: 30, speed: 2 + Math.random() * 2, lastAttack: 0 }; pickups.push({ mesh, isDrop, bobOffset: Math.random() * Math.PI });
scene.add(enemy);
enemies.push(enemy);
} }
function createParticles(position, color, count) { function createParticles(pos, color, count, speed = 5) {
const geo = new THREE.BoxGeometry(0.1, 0.1, 0.1); const geo = new THREE.BoxGeometry(0.08, 0.08, 0.08);
const mat = new THREE.MeshBasicMaterial({ color: color }); const mat = new THREE.MeshBasicMaterial({ color: color });
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const mesh = new THREE.Mesh(geo, mat); const mesh = new THREE.Mesh(geo, mat);
mesh.position.copy(position); mesh.position.copy(pos);
mesh.position.x += (Math.random() - 0.5) * 0.5;
mesh.position.y += (Math.random() - 0.5) * 0.5;
mesh.position.z += (Math.random() - 0.5) * 0.5;
const vel = new THREE.Vector3( const vel = new THREE.Vector3(
(Math.random() - 0.5) * 5, (Math.random() - 0.5) * speed,
(Math.random() - 0.5) * 5 + 2, // Вверх (Math.random() - 0.5) * speed + 2,
(Math.random() - 0.5) * 5 (Math.random() - 0.5) * speed
); );
scene.add(mesh); scene.add(mesh);
particles.push({ mesh, vel, life: 1.0 }); particles.push({ mesh, vel, life: 1.0 });
} }
@@ -269,68 +420,117 @@
function onKeyDown(event) { function onKeyDown(event) {
switch (event.code) { switch (event.code) {
case 'ArrowUp': case 'KeyW': moveForward = true; break; case 'KeyW': case 'ArrowUp': moveForward = true; break;
case 'ArrowLeft': case 'KeyA': moveLeft = true; break; case 'KeyA': case 'ArrowLeft': moveLeft = true; break;
case 'ArrowDown': case 'KeyS': moveBackward = true; break; case 'KeyS': case 'ArrowDown': moveBackward = true; break;
case 'ArrowRight': case 'KeyD': moveRight = true; break; case 'KeyD': case 'ArrowRight': moveRight = true; break;
case 'KeyR':
if (ammo < MAX_AMMO) {
// Симуляция перезарядки
isPumping = true;
setTimeout(() => {
ammo = Math.min(ammo + 6, MAX_AMMO);
uiAmmo.innerText = ammo;
isPumping = false;
playSound('pickup');
}, 800);
}
break;
} }
} }
function onKeyUp(event) { function onKeyUp(event) {
switch (event.code) { switch (event.code) {
case 'ArrowUp': case 'KeyW': moveForward = false; break; case 'KeyW': case 'ArrowUp': moveForward = false; break;
case 'ArrowLeft': case 'KeyA': moveLeft = false; break; case 'KeyA': case 'ArrowLeft': moveLeft = false; break;
case 'ArrowDown': case 'KeyS': moveBackward = false; break; case 'KeyS': case 'ArrowDown': moveBackward = false; break;
case 'ArrowRight': case 'KeyD': moveRight = false; break; case 'KeyD': case 'ArrowRight': moveRight = false; break;
} }
} }
function onMouseDown(event) { function onMouseDown(event) {
if (!controls.isLocked || event.button !== 0 || ammo <= 0) return; if (!controls.isLocked || event.button !== 0 || isPumping) return;
if (ammo <= 0) {
// Щелчок пустого оружия
playSound('hit');
return;
}
// Логика выстрела
ammo--; ammo--;
uiAmmo.innerText = ammo; uiAmmo.innerText = ammo;
isShooting = true; playSound('shoot');
// Анимация отдачи // Анимация отдачи и помпы
weaponGroup.position.z += 0.2; weaponGroup.position.z += 0.15;
weaponGroup.rotation.x -= 0.1; weaponGroup.rotation.x -= 0.15;
pumpGroup.position.z = -0.2; // Откат цевья
// Вспышка света setTimeout(() => {
muzzleLight.position.copy(camera.position).add(camera.getWorldDirection(new THREE.Vector3()).multiplyScalar(1)); pumpGroup.position.z = 0; // Возврат цевья
muzzleLight.intensity = 5; }, 300);
setTimeout(() => { muzzleLight.intensity = 0; }, 50);
// Raycasting (стрельба) muzzleLight.intensity = 8;
setTimeout(() => { muzzleLight.intensity = 0; }, 60);
// Стрельба (Raycast)
const raycaster = new THREE.Raycaster(); const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(new THREE.Vector2(0, 0), camera); raycaster.setFromCamera(new THREE.Vector2(0, 0), camera);
const intersects = raycaster.intersectObjects(enemies, true); // true для рекурсивного поиска в группах
const intersects = raycaster.intersectObjects(enemies);
if (intersects.length > 0) { if (intersects.length > 0) {
const hit = intersects[0]; // Находим корневой объект врага (группу)
const enemy = hit.object; let target = intersects[0].object;
while(target.parent && target.parent.type !== 'Scene') {
if (target.userData && target.userData.health !== undefined) break;
target = target.parent;
}
// Эффект попадания if (target.userData && target.userData.health !== undefined) {
createParticles(hit.point, 0xff0000, 8); // Кровь const hitPoint = intersects[0].point;
createParticles(hit.point, 0xffaa00, 4); // Искры createParticles(hitPoint, 0xaa0000, 10, 8); // Кровь
createParticles(hitPoint, 0xffaa00, 5, 6); // Искры
enemy.userData.health -= 15; target.userData.health -= 20;
playSound('hit');
// Отталкивание врага // Отталкивание
const pushDir = hit.point.clone().sub(camera.position).normalize(); const pushDir = hitPoint.clone().sub(camera.position).normalize();
enemy.position.add(pushDir.multiplyScalar(0.5)); target.position.add(pushDir.multiplyScalar(0.8));
if (enemy.userData.health <= 0) { if (target.userData.health <= 0) {
scene.remove(enemy); createParticles(target.position, 0xaa0000, 30, 10);
enemies = enemies.filter(e => e !== enemy); scene.remove(target);
createParticles(enemy.position, 0xff0000, 20); // Взрыв крови enemies = enemies.filter(e => e !== target);
spawnEnemy(); // Спавним нового
spawnEnemy(); // Усложняем // Дроп патронов с врага
spawnPickup(target.position.x, target.position.z, true);
// Спавн нового врага в случайной точке
setTimeout(() => {
const emptySpots = [];
for(let z=0; z<mapLayout.length; z++) {
for(let x=0; x<mapLayout[z].length; x++) {
if(mapLayout[z][x] === 0 || mapLayout[z][x] === 2) emptySpots.push({x, z});
}
}
const spot = emptySpots[Math.floor(Math.random() * emptySpots.length)];
spawnEnemy(spot.x * CELL_SIZE, spot.z * CELL_SIZE);
}, 2000);
}
} }
} }
} }
function checkWallCollision(newX, newZ) {
const gridX = Math.round(newX / CELL_SIZE);
const gridZ = Math.round(newZ / CELL_SIZE);
if (gridZ >= 0 && gridZ < mapLayout.length && gridX >= 0 && gridX < mapLayout[0].length) {
return mapLayout[gridZ][gridX] === 1;
}
return true;
}
function onWindowResize() { function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight; camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix(); camera.updateProjectionMatrix();
@@ -341,94 +541,125 @@
requestAnimationFrame(animate); requestAnimationFrame(animate);
const time = performance.now(); const time = performance.now();
const delta = (time - prevTime) / 1000; const delta = Math.min((time - prevTime) / 1000, 0.1); // Ограничение delta для стабильности
prevTime = time; prevTime = time;
if (controls.isLocked) { if (controls.isLocked) {
// --- Движение игрока --- // --- Движение игрока с коллизией ---
velocity.x -= velocity.x * 10.0 * delta; velocity.x -= velocity.x * 12.0 * delta;
velocity.z -= velocity.z * 10.0 * delta; velocity.z -= velocity.z * 12.0 * delta;
direction.z = Number(moveForward) - Number(moveBackward); direction.z = Number(moveForward) - Number(moveBackward);
direction.x = Number(moveRight) - Number(moveLeft); direction.x = Number(moveRight) - Number(moveLeft);
direction.normalize(); direction.normalize();
if (moveForward || moveBackward) velocity.z -= direction.z * 100.0 * delta; if (moveForward || moveBackward) velocity.z -= direction.z * 80.0 * delta;
if (moveLeft || moveRight) velocity.x -= direction.x * 100.0 * delta; if (moveLeft || moveRight) velocity.x -= direction.x * 80.0 * delta;
controls.moveRight(-velocity.x * delta); const nextX = camera.position.x - velocity.x * delta;
controls.moveForward(-velocity.z * delta); const nextZ = camera.position.z - velocity.z * delta;
// Ограничение карты (простая коллизия с границами) // Простая коллизия по осям отдельно для скольжения вдоль стен
camera.position.x = Math.max(-45, Math.min(45, camera.position.x)); if (!checkWallCollision(nextX, camera.position.z)) {
camera.position.z = Math.max(-45, Math.min(45, camera.position.z)); camera.position.x = nextX;
}
if (!checkWallCollision(camera.position.x, nextZ)) {
camera.position.z = nextZ;
}
// --- Анимация оружия --- // --- Анимация оружия ---
// Возврат отдачи weaponGroup.position.z = THREE.MathUtils.lerp(weaponGroup.position.z, -0.6, delta * 12);
weaponGroup.position.z = THREE.MathUtils.lerp(weaponGroup.position.z, -0.5, delta * 10); weaponGroup.rotation.x = THREE.MathUtils.lerp(weaponGroup.rotation.x, 0, delta * 12);
weaponGroup.rotation.x = THREE.MathUtils.lerp(weaponGroup.rotation.x, 0, delta * 10);
// Покачивание оружия при ходьбе (Bobbing)
if (moveForward || moveBackward || moveLeft || moveRight) { if (moveForward || moveBackward || moveLeft || moveRight) {
weaponGroup.position.y = -0.3 + Math.sin(time * 0.01) * 0.01; const bobSpeed = time * 0.015;
weaponGroup.position.x = 0.3 + Math.cos(time * 0.01) * 0.01; weaponGroup.position.y = -0.35 + Math.sin(bobSpeed) * 0.015;
weaponGroup.position.x = 0.35 + Math.cos(bobSpeed * 0.5) * 0.01;
} }
// --- Логика врагов --- // --- Логика врагов ---
enemies.forEach(enemy => { enemies.forEach(enemy => {
// Движение к игроку const dist = enemy.position.distanceTo(camera.position);
const dirToPlayer = new THREE.Vector3().subVectors(camera.position, enemy.position).normalize(); const dirToPlayer = new THREE.Vector3().subVectors(camera.position, enemy.position).normalize();
dirToPlayer.y = 0; // Не летать вверх/вниз dirToPlayer.y = 0;
// Простая проверка расстояния, чтобы не застревать друг в друге // Движение к игроку, если не слишком близко
let tooClose = false; if (dist > 1.5) {
enemies.forEach(other => { // Простое избегание друг друга
if (enemy !== other && enemy.position.distanceTo(other.position) < 1.5) { let separation = new THREE.Vector3();
tooClose = true; enemies.forEach(other => {
} if (enemy !== other) {
}); const d = enemy.position.distanceTo(other.position);
if (d < 1.5) {
separation.add(new THREE.Vector3().subVectors(enemy.position, other.position).normalize().multiplyScalar(1/d));
}
}
});
if (!tooClose) { const finalDir = dirToPlayer.add(separation.multiplyScalar(0.5)).normalize();
enemy.position.add(dirToPlayer.multiplyScalar(enemy.userData.speed * delta)); enemy.position.add(finalDir.multiplyScalar(enemy.userData.speed * delta));
enemy.lookAt(camera.position.x, enemy.position.y, camera.position.z); enemy.lookAt(camera.position.x, enemy.position.y, camera.position.z);
} }
// Атака игрока // Атака
const dist = enemy.position.distanceTo(camera.position); if (dist < 2.0 && time - enemy.userData.lastAttack > 800) {
if (dist < 2.0 && time - enemy.userData.lastAttack > 1000) {
enemy.userData.lastAttack = time; enemy.userData.lastAttack = time;
health -= 10; let damage = 15;
uiHealth.innerText = health; if (armor > 0) {
armor -= damage * 0.6;
damage *= 0.4;
uiArmor.innerText = Math.max(0, Math.floor(armor));
}
health -= damage;
uiHealth.innerText = Math.max(0, Math.floor(health));
// Эффект урона damageOverlay.style.opacity = 0.6;
damageOverlay.style.opacity = 0.8; setTimeout(() => { damageOverlay.style.opacity = 0; }, 150);
setTimeout(() => { damageOverlay.style.opacity = 0; }, 200);
// Тряска камеры // Тряска камеры
camera.position.y += 0.1; camera.position.y -= 0.1;
setTimeout(() => { camera.position.y -= 0.1; }, 50); setTimeout(() => { camera.position.y += 0.1; }, 100);
if (health <= 0) { if (health <= 0) {
controls.unlock();
alert("ВЫ ПОГИБЛИ. RIP AND TEAR В СЛЕДУЮЩИЙ РАЗ."); alert("ВЫ ПОГИБЛИ. RIP AND TEAR В СЛЕДУЮЩИЙ РАЗ.");
location.reload(); location.reload();
} }
} }
}); });
// --- Подбор предметов ---
for (let i = pickups.length - 1; i >= 0; i--) {
const p = pickups[i];
p.mesh.rotation.y += delta * 2;
p.mesh.position.y = (p.isDrop ? 0.5 : 1.0) + Math.sin(time * 0.005 + p.bobOffset) * 0.15;
if (camera.position.distanceTo(p.mesh.position) < 1.5) {
ammo = Math.min(ammo + 12, MAX_AMMO);
uiAmmo.innerText = ammo;
playSound('pickup');
scene.remove(p.mesh);
pickups.splice(i, 1);
}
}
// --- Частицы --- // --- Частицы ---
for (let i = particles.length - 1; i >= 0; i--) { for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i]; const p = particles[i];
p.life -= delta * 2; p.life -= delta * 1.5;
p.vel.y -= 9.8 * delta; // Гравитация p.vel.y -= 15.0 * delta; // Гравитация
p.mesh.position.add(p.vel.clone().multiplyScalar(delta)); p.mesh.position.add(p.vel.clone().multiplyScalar(delta));
p.mesh.rotation.x += delta * 5; p.mesh.rotation.x += delta * 8;
p.mesh.scale.setScalar(p.life); p.mesh.scale.setScalar(Math.max(0, p.life));
if (p.life <= 0 || p.mesh.position.y < 0) { if (p.life <= 0 || p.mesh.position.y < 0.05) {
scene.remove(p.mesh); scene.remove(p.mesh);
particles.splice(i, 1); particles.splice(i, 1);
} }
} }
// UI предупреждения
ammoWarning.style.opacity = ammo <= 6 ? 1 : 0;
} }
renderer.render(scene, camera); renderer.render(scene, camera);