Обновить doom.html
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
<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: Advanced Web 3D</title>
|
<title>DOOM ETERNAL: Web 3D (Real Textures)</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; }
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
}
|
}
|
||||||
#instructions { font-size: 42px; text-transform: uppercase; letter-spacing: 6px; color: #ff3333; text-shadow: 0 0 20px #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: 16px; color: #888; margin-top: 20px; font-family: monospace; letter-spacing: 1px; }
|
#sub-instructions { font-size: 16px; color: #888; margin-top: 20px; font-family: monospace; letter-spacing: 1px; }
|
||||||
|
#loading { font-size: 18px; color: #ffaa00; margin-top: 30px; display: none; }
|
||||||
|
|
||||||
#hud {
|
#hud {
|
||||||
position: absolute; bottom: 0; left: 0; width: 100%; height: 110px;
|
position: absolute; bottom: 0; left: 0; width: 100%; height: 110px;
|
||||||
@@ -70,7 +71,8 @@
|
|||||||
|
|
||||||
<div id="blocker">
|
<div id="blocker">
|
||||||
<div id="instructions">КЛИКНИ ДЛЯ СТАРТА</div>
|
<div id="instructions">КЛИКНИ ДЛЯ СТАРТА</div>
|
||||||
<div id="sub-instructions">WASD - Движение | Мышь - Обзор | ЛКМ - Огонь | R - Перезарядка (симуляция)</div>
|
<div id="sub-instructions">WASD - Движение | Мышь - Обзор | ЛКМ - Огонь | R - Перезарядка</div>
|
||||||
|
<div id="loading">Загрузка ресурсов...</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="hud">
|
<div id="hud">
|
||||||
@@ -93,7 +95,6 @@
|
|||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';
|
import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';
|
||||||
|
|
||||||
// --- Глобальные переменные ---
|
|
||||||
let camera, scene, renderer, controls;
|
let camera, scene, renderer, controls;
|
||||||
let moveForward = false, moveBackward = false, moveLeft = false, moveRight = false;
|
let moveForward = false, moveBackward = false, moveLeft = false, moveRight = false;
|
||||||
let prevTime = performance.now();
|
let prevTime = performance.now();
|
||||||
@@ -106,11 +107,10 @@
|
|||||||
let particles = [];
|
let particles = [];
|
||||||
let pickups = [];
|
let pickups = [];
|
||||||
let weaponGroup, pumpGroup;
|
let weaponGroup, pumpGroup;
|
||||||
let isShooting = false, isPumping = false;
|
let isPumping = false;
|
||||||
let muzzleLight, ambientLight;
|
let muzzleLight;
|
||||||
let audioCtx;
|
let audioCtx;
|
||||||
|
|
||||||
// Карта: 1 = Стена, 0 = Пол, 2 = Спавн врага, 3 = Спавн патронов, 9 = Игрок
|
|
||||||
const mapLayout = [
|
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,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,9,0,0,0,1,0,0,0,0,0,0,1,0,0,3,0,0,0,0,0,0,0,0,1],
|
||||||
@@ -139,46 +139,12 @@
|
|||||||
const uiAmmo = document.getElementById('ammo-val');
|
const uiAmmo = document.getElementById('ammo-val');
|
||||||
const ammoWarning = document.getElementById('ammo-warning');
|
const ammoWarning = document.getElementById('ammo-warning');
|
||||||
const damageOverlay = document.getElementById('damage-overlay');
|
const damageOverlay = document.getElementById('damage-overlay');
|
||||||
|
const blocker = document.getElementById('blocker');
|
||||||
|
const loadingText = document.getElementById('loading');
|
||||||
|
|
||||||
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() {
|
function initAudio() {
|
||||||
if (!audioCtx) {
|
if (!audioCtx) {
|
||||||
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
@@ -190,21 +156,14 @@
|
|||||||
|
|
||||||
function playSound(type) {
|
function playSound(type) {
|
||||||
if (!audioCtx) return;
|
if (!audioCtx) return;
|
||||||
const osc = audioCtx.createOscillator();
|
|
||||||
const gain = audioCtx.createGain();
|
|
||||||
osc.connect(gain);
|
|
||||||
gain.connect(audioCtx.destination);
|
|
||||||
|
|
||||||
const now = audioCtx.currentTime;
|
const now = audioCtx.currentTime;
|
||||||
|
|
||||||
if (type === 'shoot') {
|
if (type === 'shoot') {
|
||||||
// Шум выстрела (симуляция через буфер)
|
|
||||||
const bufferSize = audioCtx.sampleRate * 0.2;
|
const bufferSize = audioCtx.sampleRate * 0.2;
|
||||||
const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
|
const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
|
||||||
const data = buffer.getChannelData(0);
|
const data = buffer.getChannelData(0);
|
||||||
for (let i = 0; i < bufferSize; i++) {
|
for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
|
||||||
data[i] = Math.random() * 2 - 1;
|
|
||||||
}
|
|
||||||
const noise = audioCtx.createBufferSource();
|
const noise = audioCtx.createBufferSource();
|
||||||
noise.buffer = buffer;
|
noise.buffer = buffer;
|
||||||
const noiseGain = audioCtx.createGain();
|
const noiseGain = audioCtx.createGain();
|
||||||
@@ -214,7 +173,10 @@
|
|||||||
noiseGain.gain.exponentialRampToValueAtTime(0.01, now + 0.2);
|
noiseGain.gain.exponentialRampToValueAtTime(0.01, now + 0.2);
|
||||||
noise.start(now);
|
noise.start(now);
|
||||||
|
|
||||||
// Низкий удар
|
const osc = audioCtx.createOscillator();
|
||||||
|
const gain = audioCtx.createGain();
|
||||||
|
osc.connect(gain);
|
||||||
|
gain.connect(audioCtx.destination);
|
||||||
osc.type = 'sawtooth';
|
osc.type = 'sawtooth';
|
||||||
osc.frequency.setValueAtTime(150, now);
|
osc.frequency.setValueAtTime(150, now);
|
||||||
osc.frequency.exponentialRampToValueAtTime(40, now + 0.15);
|
osc.frequency.exponentialRampToValueAtTime(40, now + 0.15);
|
||||||
@@ -223,6 +185,10 @@
|
|||||||
osc.start(now);
|
osc.start(now);
|
||||||
osc.stop(now + 0.2);
|
osc.stop(now + 0.2);
|
||||||
} else if (type === 'pickup') {
|
} else if (type === 'pickup') {
|
||||||
|
const osc = audioCtx.createOscillator();
|
||||||
|
const gain = audioCtx.createGain();
|
||||||
|
osc.connect(gain);
|
||||||
|
gain.connect(audioCtx.destination);
|
||||||
osc.type = 'sine';
|
osc.type = 'sine';
|
||||||
osc.frequency.setValueAtTime(400, now);
|
osc.frequency.setValueAtTime(400, now);
|
||||||
osc.frequency.linearRampToValueAtTime(800, now + 0.1);
|
osc.frequency.linearRampToValueAtTime(800, now + 0.1);
|
||||||
@@ -231,6 +197,10 @@
|
|||||||
osc.start(now);
|
osc.start(now);
|
||||||
osc.stop(now + 0.15);
|
osc.stop(now + 0.15);
|
||||||
} else if (type === 'hit') {
|
} else if (type === 'hit') {
|
||||||
|
const osc = audioCtx.createOscillator();
|
||||||
|
const gain = audioCtx.createGain();
|
||||||
|
osc.connect(gain);
|
||||||
|
gain.connect(audioCtx.destination);
|
||||||
osc.type = 'square';
|
osc.type = 'square';
|
||||||
osc.frequency.setValueAtTime(100, now);
|
osc.frequency.setValueAtTime(100, now);
|
||||||
osc.frequency.exponentialRampToValueAtTime(50, now + 0.1);
|
osc.frequency.exponentialRampToValueAtTime(50, now + 0.1);
|
||||||
@@ -248,21 +218,44 @@
|
|||||||
|
|
||||||
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100);
|
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100);
|
||||||
|
|
||||||
ambientLight = new THREE.AmbientLight(0x220505, 0.6);
|
const ambientLight = new THREE.AmbientLight(0x220505, 0.6);
|
||||||
scene.add(ambientLight);
|
scene.add(ambientLight);
|
||||||
|
|
||||||
muzzleLight = new THREE.PointLight(0xffaa00, 0, 15);
|
muzzleLight = new THREE.PointLight(0xffaa00, 0, 15);
|
||||||
muzzleLight.castShadow = true;
|
muzzleLight.castShadow = true;
|
||||||
scene.add(muzzleLight);
|
scene.add(muzzleLight);
|
||||||
|
|
||||||
|
// --- ЗАГРУЗКА ТЕКСТУР ---
|
||||||
|
loadingText.style.display = 'block';
|
||||||
|
const textureLoader = new THREE.TextureLoader();
|
||||||
|
|
||||||
|
// Используем надежные URL с официального репозитория Three.js (CORS-friendly)
|
||||||
|
const wallUrl = 'https://threejs.org/examples/textures/brick_diffuse.jpg';
|
||||||
|
const floorUrl = 'https://threejs.org/examples/textures/metal.jpg';
|
||||||
|
|
||||||
|
const wallTexture = textureLoader.load(wallUrl, () => { loadingText.style.display = 'none'; });
|
||||||
|
wallTexture.wrapS = wallTexture.wrapT = THREE.RepeatWrapping;
|
||||||
|
wallTexture.repeat.set(2, 2);
|
||||||
|
|
||||||
|
const floorTexture = textureLoader.load(floorUrl);
|
||||||
|
floorTexture.wrapS = floorTexture.wrapT = THREE.RepeatWrapping;
|
||||||
|
floorTexture.repeat.set(10, 10);
|
||||||
|
|
||||||
// Генерация уровня
|
// Генерация уровня
|
||||||
const wallGeo = new THREE.BoxGeometry(CELL_SIZE, CELL_SIZE * 1.5, CELL_SIZE);
|
const wallGeo = new THREE.BoxGeometry(CELL_SIZE, CELL_SIZE * 1.5, CELL_SIZE);
|
||||||
const wallMat = new THREE.MeshStandardMaterial({
|
const wallMat = new THREE.MeshStandardMaterial({
|
||||||
map: wallTexture, roughness: 0.7, metalness: 0.3
|
map: wallTexture,
|
||||||
|
color: 0x884444, // Запасной цвет, если текстура не загрузится
|
||||||
|
roughness: 0.8,
|
||||||
|
metalness: 0.2
|
||||||
});
|
});
|
||||||
|
|
||||||
const floorGeo = new THREE.PlaneGeometry(mapLayout[0].length * CELL_SIZE, mapLayout.length * CELL_SIZE);
|
const floorGeo = new THREE.PlaneGeometry(mapLayout[0].length * CELL_SIZE, mapLayout.length * CELL_SIZE);
|
||||||
const floorMat = new THREE.MeshStandardMaterial({
|
const floorMat = new THREE.MeshStandardMaterial({
|
||||||
map: floorTexture, roughness: 0.8, metalness: 0.1
|
map: floorTexture,
|
||||||
|
color: 0x555555, // Запасной цвет
|
||||||
|
roughness: 0.7,
|
||||||
|
metalness: 0.5
|
||||||
});
|
});
|
||||||
|
|
||||||
const floor = new THREE.Mesh(floorGeo, floorMat);
|
const floor = new THREE.Mesh(floorGeo, floorMat);
|
||||||
@@ -271,7 +264,6 @@
|
|||||||
floor.receiveShadow = true;
|
floor.receiveShadow = true;
|
||||||
scene.add(floor);
|
scene.add(floor);
|
||||||
|
|
||||||
// Потолок
|
|
||||||
const ceilGeo = new THREE.PlaneGeometry(mapLayout[0].length * CELL_SIZE, mapLayout.length * CELL_SIZE);
|
const ceilGeo = new THREE.PlaneGeometry(mapLayout[0].length * CELL_SIZE, mapLayout.length * CELL_SIZE);
|
||||||
const ceilMat = new THREE.MeshStandardMaterial({ color: 0x110505, roughness: 1 });
|
const ceilMat = new THREE.MeshStandardMaterial({ color: 0x110505, roughness: 1 });
|
||||||
const ceil = new THREE.Mesh(ceilGeo, ceilMat);
|
const ceil = new THREE.Mesh(ceilGeo, ceilMat);
|
||||||
@@ -301,15 +293,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Оружие: Супердробовик
|
// Оружие
|
||||||
weaponGroup = new THREE.Group();
|
weaponGroup = new THREE.Group();
|
||||||
|
|
||||||
// Основной корпус
|
|
||||||
const bodyGeo = new THREE.BoxGeometry(0.12, 0.15, 0.7);
|
const bodyGeo = new THREE.BoxGeometry(0.12, 0.15, 0.7);
|
||||||
const bodyMat = new THREE.MeshStandardMaterial({ color: 0x222222, metalness: 0.8, roughness: 0.4 });
|
const bodyMat = new THREE.MeshStandardMaterial({ color: 0x222222, metalness: 0.8, roughness: 0.4 });
|
||||||
const body = new THREE.Mesh(bodyGeo, bodyMat);
|
const body = new THREE.Mesh(bodyGeo, bodyMat);
|
||||||
|
|
||||||
// Стволы
|
|
||||||
const barrelGeo = new THREE.CylinderGeometry(0.035, 0.035, 0.6, 8);
|
const barrelGeo = new THREE.CylinderGeometry(0.035, 0.035, 0.6, 8);
|
||||||
const barrelMat = new THREE.MeshStandardMaterial({ color: 0x111111, metalness: 0.9, roughness: 0.2 });
|
const barrelMat = new THREE.MeshStandardMaterial({ color: 0x111111, metalness: 0.9, roughness: 0.2 });
|
||||||
const b1 = new THREE.Mesh(barrelGeo, barrelMat);
|
const b1 = new THREE.Mesh(barrelGeo, barrelMat);
|
||||||
@@ -317,7 +306,6 @@
|
|||||||
const b2 = new THREE.Mesh(barrelGeo, barrelMat);
|
const b2 = new THREE.Mesh(barrelGeo, barrelMat);
|
||||||
b2.rotation.x = Math.PI / 2; b2.position.set(0.04, 0.05, -0.5);
|
b2.rotation.x = Math.PI / 2; b2.position.set(0.04, 0.05, -0.5);
|
||||||
|
|
||||||
// Цевье (движущаяся часть помпы)
|
|
||||||
pumpGroup = new THREE.Group();
|
pumpGroup = new THREE.Group();
|
||||||
const pumpGeo = new THREE.BoxGeometry(0.14, 0.12, 0.25);
|
const pumpGeo = new THREE.BoxGeometry(0.14, 0.12, 0.25);
|
||||||
const pumpMat = new THREE.MeshStandardMaterial({ color: 0x4a3c31, roughness: 0.9 });
|
const pumpMat = new THREE.MeshStandardMaterial({ color: 0x4a3c31, roughness: 0.9 });
|
||||||
@@ -331,10 +319,11 @@
|
|||||||
camera.add(weaponGroup);
|
camera.add(weaponGroup);
|
||||||
scene.add(camera);
|
scene.add(camera);
|
||||||
|
|
||||||
|
// --- УПРАВЛЕНИЕ ---
|
||||||
controls = new PointerLockControls(camera, document.body);
|
controls = new PointerLockControls(camera, document.body);
|
||||||
const blocker = document.getElementById('blocker');
|
|
||||||
|
|
||||||
blocker.addEventListener('click', () => {
|
// Надежный обработчик клика для запуска
|
||||||
|
blocker.addEventListener('click', function() {
|
||||||
initAudio();
|
initAudio();
|
||||||
controls.lock();
|
controls.lock();
|
||||||
});
|
});
|
||||||
@@ -342,9 +331,11 @@
|
|||||||
controls.addEventListener('lock', () => {
|
controls.addEventListener('lock', () => {
|
||||||
blocker.style.display = 'none';
|
blocker.style.display = 'none';
|
||||||
});
|
});
|
||||||
|
|
||||||
controls.addEventListener('unlock', () => {
|
controls.addEventListener('unlock', () => {
|
||||||
blocker.style.display = 'flex';
|
blocker.style.display = 'flex';
|
||||||
document.getElementById('instructions').innerText = "ПАУЗА. КЛИКНИ ДЛЯ ПРОДОЛЖЕНИЯ";
|
document.getElementById('instructions').innerText = "ПАУЗА. КЛИКНИ ДЛЯ ПРОДОЛЖЕНИЯ";
|
||||||
|
loadingText.style.display = 'none';
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('keydown', onKeyDown);
|
document.addEventListener('keydown', onKeyDown);
|
||||||
@@ -362,9 +353,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function spawnEnemy(x, z) {
|
function spawnEnemy(x, z) {
|
||||||
// Враг: Какодемон-подобная форма
|
|
||||||
const group = new THREE.Group();
|
const group = new THREE.Group();
|
||||||
|
|
||||||
const bodyGeo = new THREE.IcosahedronGeometry(0.7, 1);
|
const bodyGeo = new THREE.IcosahedronGeometry(0.7, 1);
|
||||||
const bodyMat = new THREE.MeshStandardMaterial({ color: 0x880000, roughness: 0.3, metalness: 0.6, emissive: 0x220000 });
|
const bodyMat = new THREE.MeshStandardMaterial({ color: 0x880000, roughness: 0.3, metalness: 0.6, emissive: 0x220000 });
|
||||||
const body = new THREE.Mesh(bodyGeo, bodyMat);
|
const body = new THREE.Mesh(bodyGeo, bodyMat);
|
||||||
@@ -374,30 +363,24 @@
|
|||||||
const eye = new THREE.Mesh(eyeGeo, eyeMat);
|
const eye = new THREE.Mesh(eyeGeo, eyeMat);
|
||||||
eye.position.set(0, 0.2, 0.6);
|
eye.position.set(0, 0.2, 0.6);
|
||||||
|
|
||||||
// Свет от врага
|
|
||||||
const light = new THREE.PointLight(0xff0000, 1, 5);
|
const light = new THREE.PointLight(0xff0000, 1, 5);
|
||||||
light.position.y = 0.5;
|
light.position.y = 0.5;
|
||||||
|
|
||||||
group.add(body, eye, light);
|
group.add(body, eye, light);
|
||||||
group.position.set(x, 1.2, z);
|
group.position.set(x, 1.2, z);
|
||||||
group.castShadow = true;
|
group.castShadow = true;
|
||||||
|
group.userData = { health: 40, speed: 3.5, lastAttack: 0 };
|
||||||
group.userData = { health: 40, speed: 3.5, lastAttack: 0, id: Math.random() };
|
|
||||||
scene.add(group);
|
scene.add(group);
|
||||||
enemies.push(group);
|
enemies.push(group);
|
||||||
}
|
}
|
||||||
|
|
||||||
function spawnPickup(x, z, isDrop = false) {
|
function spawnPickup(x, z, isDrop = false) {
|
||||||
const geo = new THREE.BoxGeometry(0.6, 0.4, 0.4);
|
const geo = new THREE.BoxGeometry(0.6, 0.4, 0.4);
|
||||||
const mat = new THREE.MeshStandardMaterial({
|
const mat = new THREE.MeshStandardMaterial({ color: 0xffcc00, emissive: 0xaa6600, emissiveIntensity: 0.5, metalness: 0.8 });
|
||||||
color: 0xffcc00, emissive: 0xaa6600, emissiveIntensity: 0.5, metalness: 0.8
|
|
||||||
});
|
|
||||||
const mesh = new THREE.Mesh(geo, mat);
|
const mesh = new THREE.Mesh(geo, mat);
|
||||||
mesh.position.set(x, isDrop ? 0.5 : 1, z);
|
mesh.position.set(x, isDrop ? 0.5 : 1, z);
|
||||||
|
|
||||||
const light = new THREE.PointLight(0xffcc00, 0.5, 3);
|
const light = new THREE.PointLight(0xffcc00, 0.5, 3);
|
||||||
mesh.add(light);
|
mesh.add(light);
|
||||||
|
|
||||||
scene.add(mesh);
|
scene.add(mesh);
|
||||||
pickups.push({ mesh, isDrop, bobOffset: Math.random() * Math.PI });
|
pickups.push({ mesh, isDrop, bobOffset: Math.random() * Math.PI });
|
||||||
}
|
}
|
||||||
@@ -408,11 +391,7 @@
|
|||||||
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(pos);
|
mesh.position.copy(pos);
|
||||||
const vel = new THREE.Vector3(
|
const vel = new THREE.Vector3((Math.random() - 0.5) * speed, (Math.random() - 0.5) * speed + 2, (Math.random() - 0.5) * speed);
|
||||||
(Math.random() - 0.5) * speed,
|
|
||||||
(Math.random() - 0.5) * speed + 2,
|
|
||||||
(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 });
|
||||||
}
|
}
|
||||||
@@ -425,13 +404,14 @@
|
|||||||
case 'KeyS': case 'ArrowDown': moveBackward = true; break;
|
case 'KeyS': case 'ArrowDown': moveBackward = true; break;
|
||||||
case 'KeyD': case 'ArrowRight': moveRight = true; break;
|
case 'KeyD': case 'ArrowRight': moveRight = true; break;
|
||||||
case 'KeyR':
|
case 'KeyR':
|
||||||
if (ammo < MAX_AMMO) {
|
if (ammo < MAX_AMMO && !isPumping) {
|
||||||
// Симуляция перезарядки
|
|
||||||
isPumping = true;
|
isPumping = true;
|
||||||
|
pumpGroup.position.z = -0.2;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
ammo = Math.min(ammo + 6, MAX_AMMO);
|
ammo = Math.min(ammo + 6, MAX_AMMO);
|
||||||
uiAmmo.innerText = ammo;
|
uiAmmo.innerText = ammo;
|
||||||
isPumping = false;
|
isPumping = false;
|
||||||
|
pumpGroup.position.z = 0;
|
||||||
playSound('pickup');
|
playSound('pickup');
|
||||||
}, 800);
|
}, 800);
|
||||||
}
|
}
|
||||||
@@ -451,7 +431,6 @@
|
|||||||
function onMouseDown(event) {
|
function onMouseDown(event) {
|
||||||
if (!controls.isLocked || event.button !== 0 || isPumping) return;
|
if (!controls.isLocked || event.button !== 0 || isPumping) return;
|
||||||
if (ammo <= 0) {
|
if (ammo <= 0) {
|
||||||
// Щелчок пустого оружия
|
|
||||||
playSound('hit');
|
playSound('hit');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -460,25 +439,20 @@
|
|||||||
uiAmmo.innerText = ammo;
|
uiAmmo.innerText = ammo;
|
||||||
playSound('shoot');
|
playSound('shoot');
|
||||||
|
|
||||||
// Анимация отдачи и помпы
|
|
||||||
weaponGroup.position.z += 0.15;
|
weaponGroup.position.z += 0.15;
|
||||||
weaponGroup.rotation.x -= 0.15;
|
weaponGroup.rotation.x -= 0.15;
|
||||||
pumpGroup.position.z = -0.2; // Откат цевья
|
pumpGroup.position.z = -0.2;
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => { pumpGroup.position.z = 0; }, 300);
|
||||||
pumpGroup.position.z = 0; // Возврат цевья
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
muzzleLight.intensity = 8;
|
muzzleLight.intensity = 8;
|
||||||
setTimeout(() => { muzzleLight.intensity = 0; }, 60);
|
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, true);
|
||||||
|
|
||||||
if (intersects.length > 0) {
|
if (intersects.length > 0) {
|
||||||
// Находим корневой объект врага (группу)
|
|
||||||
let target = intersects[0].object;
|
let target = intersects[0].object;
|
||||||
while(target.parent && target.parent.type !== 'Scene') {
|
while(target.parent && target.parent.type !== 'Scene') {
|
||||||
if (target.userData && target.userData.health !== undefined) break;
|
if (target.userData && target.userData.health !== undefined) break;
|
||||||
@@ -487,13 +461,12 @@
|
|||||||
|
|
||||||
if (target.userData && target.userData.health !== undefined) {
|
if (target.userData && target.userData.health !== undefined) {
|
||||||
const hitPoint = intersects[0].point;
|
const hitPoint = intersects[0].point;
|
||||||
createParticles(hitPoint, 0xaa0000, 10, 8); // Кровь
|
createParticles(hitPoint, 0xaa0000, 10, 8);
|
||||||
createParticles(hitPoint, 0xffaa00, 5, 6); // Искры
|
createParticles(hitPoint, 0xffaa00, 5, 6);
|
||||||
|
|
||||||
target.userData.health -= 20;
|
target.userData.health -= 20;
|
||||||
playSound('hit');
|
playSound('hit');
|
||||||
|
|
||||||
// Отталкивание
|
|
||||||
const pushDir = hitPoint.clone().sub(camera.position).normalize();
|
const pushDir = hitPoint.clone().sub(camera.position).normalize();
|
||||||
target.position.add(pushDir.multiplyScalar(0.8));
|
target.position.add(pushDir.multiplyScalar(0.8));
|
||||||
|
|
||||||
@@ -501,11 +474,8 @@
|
|||||||
createParticles(target.position, 0xaa0000, 30, 10);
|
createParticles(target.position, 0xaa0000, 30, 10);
|
||||||
scene.remove(target);
|
scene.remove(target);
|
||||||
enemies = enemies.filter(e => e !== target);
|
enemies = enemies.filter(e => e !== target);
|
||||||
|
|
||||||
// Дроп патронов с врага
|
|
||||||
spawnPickup(target.position.x, target.position.z, true);
|
spawnPickup(target.position.x, target.position.z, true);
|
||||||
|
|
||||||
// Спавн нового врага в случайной точке
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const emptySpots = [];
|
const emptySpots = [];
|
||||||
for(let z=0; z<mapLayout.length; z++) {
|
for(let z=0; z<mapLayout.length; z++) {
|
||||||
@@ -513,8 +483,10 @@
|
|||||||
if(mapLayout[z][x] === 0 || mapLayout[z][x] === 2) emptySpots.push({x, z});
|
if(mapLayout[z][x] === 0 || mapLayout[z][x] === 2) emptySpots.push({x, z});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const spot = emptySpots[Math.floor(Math.random() * emptySpots.length)];
|
if(emptySpots.length > 0) {
|
||||||
spawnEnemy(spot.x * CELL_SIZE, spot.z * CELL_SIZE);
|
const spot = emptySpots[Math.floor(Math.random() * emptySpots.length)];
|
||||||
|
spawnEnemy(spot.x * CELL_SIZE, spot.z * CELL_SIZE);
|
||||||
|
}
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -524,7 +496,6 @@
|
|||||||
function checkWallCollision(newX, newZ) {
|
function checkWallCollision(newX, newZ) {
|
||||||
const gridX = Math.round(newX / CELL_SIZE);
|
const gridX = Math.round(newX / CELL_SIZE);
|
||||||
const gridZ = Math.round(newZ / CELL_SIZE);
|
const gridZ = Math.round(newZ / CELL_SIZE);
|
||||||
|
|
||||||
if (gridZ >= 0 && gridZ < mapLayout.length && gridX >= 0 && gridX < mapLayout[0].length) {
|
if (gridZ >= 0 && gridZ < mapLayout.length && gridX >= 0 && gridX < mapLayout[0].length) {
|
||||||
return mapLayout[gridZ][gridX] === 1;
|
return mapLayout[gridZ][gridX] === 1;
|
||||||
}
|
}
|
||||||
@@ -541,11 +512,10 @@
|
|||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
|
|
||||||
const time = performance.now();
|
const time = performance.now();
|
||||||
const delta = Math.min((time - prevTime) / 1000, 0.1); // Ограничение delta для стабильности
|
const delta = Math.min((time - prevTime) / 1000, 0.1);
|
||||||
prevTime = time;
|
prevTime = time;
|
||||||
|
|
||||||
if (controls.isLocked) {
|
if (controls.isLocked) {
|
||||||
// --- Движение игрока с коллизией ---
|
|
||||||
velocity.x -= velocity.x * 12.0 * delta;
|
velocity.x -= velocity.x * 12.0 * delta;
|
||||||
velocity.z -= velocity.z * 12.0 * delta;
|
velocity.z -= velocity.z * 12.0 * delta;
|
||||||
|
|
||||||
@@ -559,15 +529,9 @@
|
|||||||
const nextX = camera.position.x - velocity.x * delta;
|
const nextX = camera.position.x - velocity.x * delta;
|
||||||
const nextZ = camera.position.z - velocity.z * delta;
|
const nextZ = camera.position.z - velocity.z * delta;
|
||||||
|
|
||||||
// Простая коллизия по осям отдельно для скольжения вдоль стен
|
if (!checkWallCollision(nextX, camera.position.z)) camera.position.x = nextX;
|
||||||
if (!checkWallCollision(nextX, camera.position.z)) {
|
if (!checkWallCollision(camera.position.x, nextZ)) camera.position.z = nextZ;
|
||||||
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.6, delta * 12);
|
||||||
weaponGroup.rotation.x = THREE.MathUtils.lerp(weaponGroup.rotation.x, 0, delta * 12);
|
weaponGroup.rotation.x = THREE.MathUtils.lerp(weaponGroup.rotation.x, 0, delta * 12);
|
||||||
|
|
||||||
@@ -577,31 +541,24 @@
|
|||||||
weaponGroup.position.x = 0.35 + Math.cos(bobSpeed * 0.5) * 0.01;
|
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 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;
|
||||||
|
|
||||||
// Движение к игроку, если не слишком близко
|
|
||||||
if (dist > 1.5) {
|
if (dist > 1.5) {
|
||||||
// Простое избегание друг друга
|
|
||||||
let separation = new THREE.Vector3();
|
let separation = new THREE.Vector3();
|
||||||
enemies.forEach(other => {
|
enemies.forEach(other => {
|
||||||
if (enemy !== other) {
|
if (enemy !== other) {
|
||||||
const d = enemy.position.distanceTo(other.position);
|
const d = enemy.position.distanceTo(other.position);
|
||||||
if (d < 1.5) {
|
if (d < 1.5) separation.add(new THREE.Vector3().subVectors(enemy.position, other.position).normalize().multiplyScalar(1/d));
|
||||||
separation.add(new THREE.Vector3().subVectors(enemy.position, other.position).normalize().multiplyScalar(1/d));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const finalDir = dirToPlayer.add(separation.multiplyScalar(0.5)).normalize();
|
const finalDir = dirToPlayer.add(separation.multiplyScalar(0.5)).normalize();
|
||||||
enemy.position.add(finalDir.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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Атака
|
|
||||||
if (dist < 2.0 && time - enemy.userData.lastAttack > 800) {
|
if (dist < 2.0 && time - enemy.userData.lastAttack > 800) {
|
||||||
enemy.userData.lastAttack = time;
|
enemy.userData.lastAttack = time;
|
||||||
let damage = 15;
|
let damage = 15;
|
||||||
@@ -616,7 +573,6 @@
|
|||||||
damageOverlay.style.opacity = 0.6;
|
damageOverlay.style.opacity = 0.6;
|
||||||
setTimeout(() => { damageOverlay.style.opacity = 0; }, 150);
|
setTimeout(() => { damageOverlay.style.opacity = 0; }, 150);
|
||||||
|
|
||||||
// Тряска камеры
|
|
||||||
camera.position.y -= 0.1;
|
camera.position.y -= 0.1;
|
||||||
setTimeout(() => { camera.position.y += 0.1; }, 100);
|
setTimeout(() => { camera.position.y += 0.1; }, 100);
|
||||||
|
|
||||||
@@ -628,7 +584,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Подбор предметов ---
|
|
||||||
for (let i = pickups.length - 1; i >= 0; i--) {
|
for (let i = pickups.length - 1; i >= 0; i--) {
|
||||||
const p = pickups[i];
|
const p = pickups[i];
|
||||||
p.mesh.rotation.y += delta * 2;
|
p.mesh.rotation.y += delta * 2;
|
||||||
@@ -643,22 +598,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Частицы ---
|
|
||||||
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 * 1.5;
|
p.life -= delta * 1.5;
|
||||||
p.vel.y -= 15.0 * 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 * 8;
|
p.mesh.rotation.x += delta * 8;
|
||||||
p.mesh.scale.setScalar(Math.max(0, p.life));
|
p.mesh.scale.setScalar(Math.max(0, p.life));
|
||||||
|
|
||||||
if (p.life <= 0 || p.mesh.position.y < 0.05) {
|
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;
|
ammoWarning.style.opacity = ammo <= 6 ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user