Обновить doom.html
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DOOM ETERNAL: Web 3D (Fixed)</title>
|
||||
<title>DOOM ETERNAL: Web 3D</title>
|
||||
<style>
|
||||
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; }
|
||||
@@ -24,7 +24,7 @@
|
||||
z-index: 20; color: #fff; cursor: pointer;
|
||||
}
|
||||
#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: #ccc; margin-top: 20px; font-family: monospace; letter-spacing: 1px; }
|
||||
#sub-instructions { font-size: 16px; color: #ccc; margin-top: 20px; font-family: monospace; letter-spacing: 1px; text-align: center; line-height: 1.8; }
|
||||
#loading { font-size: 18px; color: #ffaa00; margin-top: 30px; display: none; }
|
||||
|
||||
#hud {
|
||||
@@ -40,6 +40,13 @@
|
||||
.armor { color: #00bfff; }
|
||||
.ammo { color: #ffcc00; }
|
||||
|
||||
#weapon-name {
|
||||
position: absolute; bottom: 120px; right: 50px;
|
||||
color: #ffcc00; font-size: 20px; font-family: monospace; letter-spacing: 2px;
|
||||
text-shadow: 0 0 10px #ffaa00; pointer-events: none; z-index: 10;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
#damage-overlay {
|
||||
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
||||
background: radial-gradient(circle, transparent 40%, rgba(139, 0, 0, 0.8) 100%);
|
||||
@@ -51,6 +58,21 @@
|
||||
color: #ff0000; font-size: 24px; font-family: monospace; opacity: 0; transition: opacity 0.3s;
|
||||
text-shadow: 0 0 10px #ff0000;
|
||||
}
|
||||
|
||||
#weapon-slots {
|
||||
position: absolute; top: 20px; right: 20px;
|
||||
display: flex; flex-direction: column; gap: 8px;
|
||||
pointer-events: none; z-index: 10;
|
||||
}
|
||||
.weapon-slot {
|
||||
padding: 8px 16px; background: rgba(0,0,0,0.7); border: 2px solid #444;
|
||||
color: #888; font-family: monospace; font-size: 14px; letter-spacing: 1px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.weapon-slot.active {
|
||||
border-color: #ffcc00; color: #ffcc00; background: rgba(50,30,0,0.8);
|
||||
box-shadow: 0 0 10px rgba(255,200,0,0.5);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="importmap">
|
||||
@@ -68,10 +90,21 @@
|
||||
<div id="damage-overlay"></div>
|
||||
<div id="crosshair"></div>
|
||||
<div id="ammo-warning">МАЛО ПАТРОНОВ!</div>
|
||||
<div id="weapon-name">ДРОБОВИК</div>
|
||||
|
||||
<div id="weapon-slots">
|
||||
<div class="weapon-slot active" data-slot="0">[1] ДРОБОВИК</div>
|
||||
<div class="weapon-slot" data-slot="1">[2] ПИСТОЛЕТ</div>
|
||||
<div class="weapon-slot" data-slot="2">[3] ПЛАЗМАГАН</div>
|
||||
</div>
|
||||
|
||||
<div id="blocker">
|
||||
<div id="instructions">КЛИКНИ ДЛЯ СТАРТА</div>
|
||||
<div id="sub-instructions">WASD - Движение | Мышь - Обзор | ЛКМ - Огонь | R - Перезарядка</div>
|
||||
<div id="sub-instructions">
|
||||
WASD - Движение | Мышь - Обзор<br>
|
||||
ЛКМ - Огонь | R - Перезарядка<br>
|
||||
1, 2, 3 или Колесо мыши - Смена оружия
|
||||
</div>
|
||||
<div id="loading">Загрузка ресурсов...</div>
|
||||
</div>
|
||||
|
||||
@@ -86,7 +119,7 @@
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="stat-label">Патроны</div>
|
||||
<div class="stat-value ammo" id="ammo-val">24</div>
|
||||
<div class="stat-value ammo" id="ammo-val">8</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -101,15 +134,53 @@
|
||||
const velocity = new THREE.Vector3();
|
||||
const direction = new THREE.Vector3();
|
||||
|
||||
let health = 100, armor = 50, ammo = 24;
|
||||
const MAX_AMMO = 50;
|
||||
let health = 100, armor = 50;
|
||||
let enemies = [];
|
||||
let particles = [];
|
||||
let pickups = [];
|
||||
let weaponGroup, pumpGroup;
|
||||
let isPumping = false;
|
||||
let projectiles = [];
|
||||
let weaponGroup;
|
||||
let muzzleLight, playerLight;
|
||||
let audioCtx;
|
||||
let isReloading = false;
|
||||
let lastShotTime = 0;
|
||||
let currentWeaponIndex = 0;
|
||||
let isSwitchingWeapon = false;
|
||||
|
||||
// --- СИСТЕМА ОРУЖИЯ ---
|
||||
const weapons = [
|
||||
{
|
||||
name: 'ДРОБОВИК',
|
||||
ammo: 8, maxAmmo: 8, reserve: 32,
|
||||
damage: 25, fireRate: 600, // мс между выстрелами
|
||||
reloadTime: 1200,
|
||||
pellets: 6, spread: 0.08, // для дробовика - несколько дробинок
|
||||
isAutomatic: false,
|
||||
isProjectile: false,
|
||||
color: 0xffaa00
|
||||
},
|
||||
{
|
||||
name: 'ПИСТОЛЕТ',
|
||||
ammo: 15, maxAmmo: 15, reserve: 60,
|
||||
damage: 18, fireRate: 200,
|
||||
reloadTime: 800,
|
||||
pellets: 1, spread: 0.02,
|
||||
isAutomatic: false,
|
||||
isProjectile: false,
|
||||
color: 0xffff00
|
||||
},
|
||||
{
|
||||
name: 'ПЛАЗМАГАН',
|
||||
ammo: 30, maxAmmo: 30, reserve: 120,
|
||||
damage: 12, fireRate: 80,
|
||||
reloadTime: 1500,
|
||||
pellets: 1, spread: 0.01,
|
||||
isAutomatic: true,
|
||||
isProjectile: true,
|
||||
projectileSpeed: 40,
|
||||
color: 0x00ccff
|
||||
}
|
||||
];
|
||||
|
||||
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],
|
||||
@@ -133,15 +204,17 @@
|
||||
[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;
|
||||
const PLAYER_RADIUS = 0.6; // Радиус игрока для коллизий
|
||||
const PLAYER_RADIUS = 0.6;
|
||||
|
||||
const uiHealth = document.getElementById('health-val');
|
||||
const uiArmor = document.getElementById('armor-val');
|
||||
const uiAmmo = document.getElementById('ammo-val');
|
||||
const uiWeaponName = document.getElementById('weapon-name');
|
||||
const ammoWarning = document.getElementById('ammo-warning');
|
||||
const damageOverlay = document.getElementById('damage-overlay');
|
||||
const blocker = document.getElementById('blocker');
|
||||
const loadingText = document.getElementById('loading');
|
||||
const weaponSlots = document.querySelectorAll('.weapon-slot');
|
||||
|
||||
init();
|
||||
animate();
|
||||
@@ -159,19 +232,18 @@
|
||||
if (!audioCtx) return;
|
||||
const now = audioCtx.currentTime;
|
||||
|
||||
if (type === 'shoot') {
|
||||
const bufferSize = audioCtx.sampleRate * 0.2;
|
||||
if (type === 'shotgun') {
|
||||
const bufferSize = audioCtx.sampleRate * 0.3;
|
||||
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);
|
||||
noiseGain.gain.setValueAtTime(1.0, now);
|
||||
noiseGain.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
|
||||
noise.start(now);
|
||||
|
||||
const osc = audioCtx.createOscillator();
|
||||
@@ -179,12 +251,49 @@
|
||||
osc.connect(gain);
|
||||
gain.connect(audioCtx.destination);
|
||||
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.frequency.setValueAtTime(120, now);
|
||||
osc.frequency.exponentialRampToValueAtTime(30, now + 0.2);
|
||||
gain.gain.setValueAtTime(0.7, now);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.2);
|
||||
osc.start(now);
|
||||
osc.stop(now + 0.2);
|
||||
osc.stop(now + 0.3);
|
||||
} else if (type === 'pistol') {
|
||||
const bufferSize = audioCtx.sampleRate * 0.15;
|
||||
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.6, now);
|
||||
noiseGain.gain.exponentialRampToValueAtTime(0.01, now + 0.15);
|
||||
noise.start(now);
|
||||
|
||||
const osc = audioCtx.createOscillator();
|
||||
const gain = audioCtx.createGain();
|
||||
osc.connect(gain);
|
||||
gain.connect(audioCtx.destination);
|
||||
osc.type = 'square';
|
||||
osc.frequency.setValueAtTime(200, now);
|
||||
osc.frequency.exponentialRampToValueAtTime(80, now + 0.1);
|
||||
gain.gain.setValueAtTime(0.4, now);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.1);
|
||||
osc.start(now);
|
||||
osc.stop(now + 0.15);
|
||||
} else if (type === 'plasma') {
|
||||
const osc = audioCtx.createOscillator();
|
||||
const gain = audioCtx.createGain();
|
||||
osc.connect(gain);
|
||||
gain.connect(audioCtx.destination);
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(800, now);
|
||||
osc.frequency.exponentialRampToValueAtTime(200, 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);
|
||||
} else if (type === 'pickup') {
|
||||
const osc = audioCtx.createOscillator();
|
||||
const gain = audioCtx.createGain();
|
||||
@@ -209,24 +318,187 @@
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.1);
|
||||
osc.start(now);
|
||||
osc.stop(now + 0.1);
|
||||
} else if (type === 'switch') {
|
||||
const osc = audioCtx.createOscillator();
|
||||
const gain = audioCtx.createGain();
|
||||
osc.connect(gain);
|
||||
gain.connect(audioCtx.destination);
|
||||
osc.type = 'triangle';
|
||||
osc.frequency.setValueAtTime(300, now);
|
||||
osc.frequency.linearRampToValueAtTime(500, now + 0.08);
|
||||
gain.gain.setValueAtTime(0.2, now);
|
||||
gain.gain.linearRampToValueAtTime(0, now + 0.1);
|
||||
osc.start(now);
|
||||
osc.stop(now + 0.1);
|
||||
} else if (type === 'empty') {
|
||||
const osc = audioCtx.createOscillator();
|
||||
const gain = audioCtx.createGain();
|
||||
osc.connect(gain);
|
||||
gain.connect(audioCtx.destination);
|
||||
osc.type = 'square';
|
||||
osc.frequency.setValueAtTime(150, now);
|
||||
gain.gain.setValueAtTime(0.2, now);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.05);
|
||||
osc.start(now);
|
||||
osc.stop(now + 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentWeapon() { return weapons[currentWeaponIndex]; }
|
||||
|
||||
function updateHUD() {
|
||||
uiHealth.innerText = Math.max(0, Math.floor(health));
|
||||
uiArmor.innerText = Math.max(0, Math.floor(armor));
|
||||
uiAmmo.innerText = getCurrentWeapon().ammo;
|
||||
uiWeaponName.innerText = getCurrentWeapon().name;
|
||||
ammoWarning.style.opacity = getCurrentWeapon().ammo <= Math.floor(getCurrentWeapon().maxAmmo * 0.25) ? 1 : 0;
|
||||
|
||||
weaponSlots.forEach((slot, i) => {
|
||||
slot.classList.toggle('active', i === currentWeaponIndex);
|
||||
});
|
||||
}
|
||||
|
||||
function switchWeapon(index) {
|
||||
if (index === currentWeaponIndex || isSwitchingWeapon || isReloading) return;
|
||||
isSwitchingWeapon = true;
|
||||
playSound('switch');
|
||||
|
||||
// Анимация ухода оружия
|
||||
const targetY = weaponGroup.position.y - 0.5;
|
||||
const startY = weaponGroup.position.y;
|
||||
const startTime = performance.now();
|
||||
|
||||
function animateSwitch() {
|
||||
const elapsed = performance.now() - startTime;
|
||||
const progress = Math.min(elapsed / 200, 1);
|
||||
weaponGroup.position.y = startY + (targetY - startY) * progress;
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animateSwitch);
|
||||
} else {
|
||||
// Смена оружия
|
||||
currentWeaponIndex = index;
|
||||
buildWeaponModel();
|
||||
updateHUD();
|
||||
|
||||
// Анимация появления
|
||||
const finalY = -0.35;
|
||||
const startY2 = targetY;
|
||||
const startTime2 = performance.now();
|
||||
|
||||
function animateAppear() {
|
||||
const elapsed2 = performance.now() - startTime2;
|
||||
const progress2 = Math.min(elapsed2 / 200, 1);
|
||||
weaponGroup.position.y = startY2 + (finalY - startY2) * progress2;
|
||||
|
||||
if (progress2 < 1) {
|
||||
requestAnimationFrame(animateAppear);
|
||||
} else {
|
||||
isSwitchingWeapon = false;
|
||||
}
|
||||
}
|
||||
animateAppear();
|
||||
}
|
||||
}
|
||||
animateSwitch();
|
||||
}
|
||||
|
||||
function buildWeaponModel() {
|
||||
// Удаляем старое оружие
|
||||
while(weaponGroup.children.length > 0) {
|
||||
weaponGroup.remove(weaponGroup.children[0]);
|
||||
}
|
||||
|
||||
const weapon = getCurrentWeapon();
|
||||
|
||||
if (currentWeaponIndex === 0) {
|
||||
// ДРОБОВИК
|
||||
const bodyGeo = new THREE.BoxGeometry(0.12, 0.15, 0.7);
|
||||
const bodyMat = new THREE.MeshStandardMaterial({ color: 0x333333, metalness: 0.8, roughness: 0.4 });
|
||||
const body = new THREE.Mesh(bodyGeo, bodyMat);
|
||||
|
||||
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 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);
|
||||
|
||||
const pumpGeo = new THREE.BoxGeometry(0.14, 0.12, 0.25);
|
||||
const pumpMat = new THREE.MeshStandardMaterial({ color: 0x5c4a3d, roughness: 0.9 });
|
||||
const pump = new THREE.Mesh(pumpGeo, pumpMat);
|
||||
pump.position.z = 0.15;
|
||||
|
||||
const stockGeo = new THREE.BoxGeometry(0.1, 0.12, 0.3);
|
||||
const stockMat = new THREE.MeshStandardMaterial({ color: 0x4a3a2d, roughness: 0.9 });
|
||||
const stock = new THREE.Mesh(stockGeo, stockMat);
|
||||
stock.position.z = 0.4;
|
||||
|
||||
weaponGroup.add(body, b1, b2, pump, stock);
|
||||
} else if (currentWeaponIndex === 1) {
|
||||
// ПИСТОЛЕТ
|
||||
const bodyGeo = new THREE.BoxGeometry(0.08, 0.12, 0.35);
|
||||
const bodyMat = new THREE.MeshStandardMaterial({ color: 0x222222, metalness: 0.9, roughness: 0.3 });
|
||||
const body = new THREE.Mesh(bodyGeo, bodyMat);
|
||||
|
||||
const barrelGeo = new THREE.CylinderGeometry(0.02, 0.02, 0.25, 8);
|
||||
const barrelMat = new THREE.MeshStandardMaterial({ color: 0x111111, metalness: 0.95, roughness: 0.1 });
|
||||
const barrel = new THREE.Mesh(barrelGeo, barrelMat);
|
||||
barrel.rotation.x = Math.PI / 2;
|
||||
barrel.position.set(0, 0.04, -0.3);
|
||||
|
||||
const gripGeo = new THREE.BoxGeometry(0.07, 0.15, 0.08);
|
||||
const gripMat = new THREE.MeshStandardMaterial({ color: 0x3d2d1d, roughness: 0.9 });
|
||||
const grip = new THREE.Mesh(gripGeo, gripMat);
|
||||
grip.position.set(0, -0.1, 0.1);
|
||||
grip.rotation.x = 0.2;
|
||||
|
||||
const slideGeo = new THREE.BoxGeometry(0.09, 0.05, 0.3);
|
||||
const slideMat = new THREE.MeshStandardMaterial({ color: 0x1a1a1a, metalness: 0.9, roughness: 0.2 });
|
||||
const slide = new THREE.Mesh(slideGeo, slideMat);
|
||||
slide.position.set(0, 0.07, -0.05);
|
||||
|
||||
weaponGroup.add(body, barrel, grip, slide);
|
||||
} else if (currentWeaponIndex === 2) {
|
||||
// ПЛАЗМАГАН
|
||||
const bodyGeo = new THREE.BoxGeometry(0.15, 0.18, 0.6);
|
||||
const bodyMat = new THREE.MeshStandardMaterial({ color: 0x1a3a5a, metalness: 0.7, roughness: 0.3, emissive: 0x001122 });
|
||||
const body = new THREE.Mesh(bodyGeo, bodyMat);
|
||||
|
||||
const barrelGeo = new THREE.CylinderGeometry(0.05, 0.04, 0.4, 12);
|
||||
const barrelMat = new THREE.MeshStandardMaterial({ color: 0x00aaff, metalness: 0.9, roughness: 0.1, emissive: 0x004466 });
|
||||
const barrel = new THREE.Mesh(barrelGeo, barrelMat);
|
||||
barrel.rotation.x = Math.PI / 2;
|
||||
barrel.position.set(0, 0.02, -0.45);
|
||||
|
||||
const coilGeo = new THREE.TorusGeometry(0.06, 0.015, 8, 16);
|
||||
const coilMat = new THREE.MeshStandardMaterial({ color: 0x00ccff, emissive: 0x0088aa, metalness: 0.8 });
|
||||
const coil1 = new THREE.Mesh(coilGeo, coilMat);
|
||||
coil1.position.set(0, 0.02, -0.3);
|
||||
const coil2 = new THREE.Mesh(coilGeo, coilMat);
|
||||
coil2.position.set(0, 0.02, -0.15);
|
||||
|
||||
const gripGeo = new THREE.BoxGeometry(0.08, 0.14, 0.1);
|
||||
const gripMat = new THREE.MeshStandardMaterial({ color: 0x2a2a3a, roughness: 0.8 });
|
||||
const grip = new THREE.Mesh(gripGeo, gripMat);
|
||||
grip.position.set(0, -0.12, 0.15);
|
||||
|
||||
weaponGroup.add(body, barrel, coil1, coil2, grip);
|
||||
}
|
||||
|
||||
weaponGroup.position.set(0.35, -0.35, -0.6);
|
||||
}
|
||||
|
||||
function init() {
|
||||
scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0x1a0a0a); // Светлее фон
|
||||
|
||||
// УМЕНЬШЕНА плотность тумана и сделан светлее для лучшей видимости
|
||||
scene.background = new THREE.Color(0x1a0a0a);
|
||||
scene.fog = new THREE.FogExp2(0x2a0a0a, 0.02);
|
||||
|
||||
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100);
|
||||
|
||||
// --- УЛУЧШЕННОЕ ОСВЕЩЕНИЕ ---
|
||||
// 1. Яркий общий теплый свет
|
||||
const ambientLight = new THREE.AmbientLight(0xffaaaa, 1.0);
|
||||
scene.add(ambientLight);
|
||||
|
||||
// 2. Направленный свет сверху (имитация адского светила)
|
||||
const dirLight = new THREE.DirectionalLight(0xffccaa, 1.2);
|
||||
dirLight.position.set(30, 50, 30);
|
||||
dirLight.castShadow = true;
|
||||
@@ -234,7 +506,6 @@
|
||||
dirLight.shadow.mapSize.height = 1024;
|
||||
scene.add(dirLight);
|
||||
|
||||
// 3. Свет от игрока (всегда освещает пространство перед ним)
|
||||
playerLight = new THREE.PointLight(0xffaa55, 1.5, 18);
|
||||
playerLight.position.set(0, 0, -1);
|
||||
camera.add(playerLight);
|
||||
@@ -243,7 +514,6 @@
|
||||
muzzleLight.castShadow = true;
|
||||
scene.add(muzzleLight);
|
||||
|
||||
// --- ЗАГРУЗКА ТЕКСТУР ---
|
||||
loadingText.style.display = 'block';
|
||||
const textureLoader = new THREE.TextureLoader();
|
||||
|
||||
@@ -258,19 +528,12 @@
|
||||
floorTexture.wrapS = floorTexture.wrapT = THREE.RepeatWrapping;
|
||||
floorTexture.repeat.set(10, 10);
|
||||
|
||||
// Материалы сделаны светлее (запасной цвет), чтобы даже без текстур было видно
|
||||
const wallMat = new THREE.MeshStandardMaterial({
|
||||
map: wallTexture,
|
||||
color: 0x996666, // Светло-красный запасной цвет
|
||||
roughness: 0.9,
|
||||
metalness: 0.1
|
||||
map: wallTexture, color: 0x996666, roughness: 0.9, metalness: 0.1
|
||||
});
|
||||
|
||||
const floorMat = new THREE.MeshStandardMaterial({
|
||||
map: floorTexture,
|
||||
color: 0x777777, // Светло-серый запасной цвет
|
||||
roughness: 0.8,
|
||||
metalness: 0.2
|
||||
map: floorTexture, color: 0x777777, roughness: 0.8, metalness: 0.2
|
||||
});
|
||||
|
||||
const floorGeo = new THREE.PlaneGeometry(mapLayout[0].length * CELL_SIZE, mapLayout.length * CELL_SIZE);
|
||||
@@ -301,7 +564,6 @@
|
||||
wall.receiveShadow = true;
|
||||
scene.add(wall);
|
||||
} else if (type === 9) {
|
||||
// Спавн игрока строго в центре клетки
|
||||
camera.position.set(posX + CELL_SIZE/2, 1.6, posZ + CELL_SIZE/2);
|
||||
} else if (type === 2) {
|
||||
spawnEnemy(posX + CELL_SIZE/2, posZ + CELL_SIZE/2);
|
||||
@@ -313,31 +575,10 @@
|
||||
|
||||
// Оружие
|
||||
weaponGroup = new THREE.Group();
|
||||
const bodyGeo = new THREE.BoxGeometry(0.12, 0.15, 0.7);
|
||||
const bodyMat = new THREE.MeshStandardMaterial({ color: 0x333333, metalness: 0.8, roughness: 0.4 });
|
||||
const body = new THREE.Mesh(bodyGeo, bodyMat);
|
||||
|
||||
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 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: 0x5c4a3d, 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);
|
||||
buildWeaponModel();
|
||||
camera.add(weaponGroup);
|
||||
scene.add(camera);
|
||||
|
||||
// --- УПРАВЛЕНИЕ ---
|
||||
controls = new PointerLockControls(camera, document.body);
|
||||
|
||||
blocker.addEventListener('click', function() {
|
||||
@@ -358,6 +599,7 @@
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
document.addEventListener('keyup', onKeyUp);
|
||||
document.addEventListener('mousedown', onMouseDown);
|
||||
document.addEventListener('wheel', onWheel);
|
||||
|
||||
renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance" });
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
@@ -367,11 +609,10 @@
|
||||
document.getElementById('game-container').appendChild(renderer.domElement);
|
||||
|
||||
window.addEventListener('resize', onWindowResize);
|
||||
updateHUD();
|
||||
}
|
||||
|
||||
// --- ИСПРАВЛЕННАЯ КОЛЛИЗИЯ ---
|
||||
function checkWallCollision(x, z) {
|
||||
// Проверяем 4 угла вокруг игрока (квадрат коллизии)
|
||||
const checks = [
|
||||
{ x: x - PLAYER_RADIUS, z: z - PLAYER_RADIUS },
|
||||
{ x: x + PLAYER_RADIUS, z: z - PLAYER_RADIUS },
|
||||
@@ -383,7 +624,6 @@
|
||||
const gridX = Math.floor(p.x / CELL_SIZE);
|
||||
const gridZ = Math.floor(p.z / CELL_SIZE);
|
||||
|
||||
// Если вышли за пределы карты или попали в стену (1)
|
||||
if (gridZ < 0 || gridZ >= mapLayout.length || gridX < 0 || gridX >= mapLayout[0].length) {
|
||||
return true;
|
||||
}
|
||||
@@ -411,7 +651,7 @@
|
||||
group.add(body, eye, light);
|
||||
group.position.set(x, 1.2, z);
|
||||
group.castShadow = true;
|
||||
group.userData = { health: 40, speed: 3.5, lastAttack: 0 };
|
||||
group.userData = { health: 40, maxHealth: 40, speed: 3.5, lastAttack: 0 };
|
||||
scene.add(group);
|
||||
enemies.push(group);
|
||||
}
|
||||
@@ -439,25 +679,144 @@
|
||||
}
|
||||
}
|
||||
|
||||
function fireWeapon() {
|
||||
const weapon = getCurrentWeapon();
|
||||
const now = performance.now();
|
||||
|
||||
if (now - lastShotTime < weapon.fireRate || isReloading || isSwitchingWeapon) return;
|
||||
|
||||
if (weapon.ammo <= 0) {
|
||||
playSound('empty');
|
||||
// Автоматическая перезарядка
|
||||
if (weapon.reserve > 0) startReload();
|
||||
return;
|
||||
}
|
||||
|
||||
lastShotTime = now;
|
||||
weapon.ammo--;
|
||||
updateHUD();
|
||||
|
||||
// Звук
|
||||
if (currentWeaponIndex === 0) playSound('shotgun');
|
||||
else if (currentWeaponIndex === 1) playSound('pistol');
|
||||
else if (currentWeaponIndex === 2) playSound('plasma');
|
||||
|
||||
// Отдача - анимация
|
||||
weaponGroup.position.z += 0.1;
|
||||
weaponGroup.rotation.x -= 0.1;
|
||||
|
||||
// Вспышка
|
||||
const dir = new THREE.Vector3();
|
||||
camera.getWorldDirection(dir);
|
||||
muzzleLight.position.copy(camera.position).add(dir.clone().multiplyScalar(1));
|
||||
muzzleLight.intensity = 8;
|
||||
muzzleLight.color.setHex(weapon.color);
|
||||
setTimeout(() => { muzzleLight.intensity = 0; }, 60);
|
||||
|
||||
if (weapon.isProjectile) {
|
||||
// Плазмаган - создаём снаряд
|
||||
const projGeo = new THREE.SphereGeometry(0.15, 8, 8);
|
||||
const projMat = new THREE.MeshBasicMaterial({ color: weapon.color });
|
||||
const proj = new THREE.Mesh(projGeo, projMat);
|
||||
proj.position.copy(camera.position).add(dir.clone().multiplyScalar(1));
|
||||
|
||||
const projLight = new THREE.PointLight(weapon.color, 2, 5);
|
||||
proj.add(projLight);
|
||||
|
||||
scene.add(proj);
|
||||
projectiles.push({
|
||||
mesh: proj,
|
||||
velocity: dir.clone().multiplyScalar(weapon.projectileSpeed),
|
||||
damage: weapon.damage,
|
||||
life: 3.0,
|
||||
color: weapon.color
|
||||
});
|
||||
} else {
|
||||
// Hitscan оружие (дробовик, пистолет)
|
||||
const raycaster = new THREE.Raycaster();
|
||||
|
||||
for (let p = 0; p < weapon.pellets; p++) {
|
||||
const spread = new THREE.Vector2(
|
||||
(Math.random() - 0.5) * weapon.spread,
|
||||
(Math.random() - 0.5) * weapon.spread
|
||||
);
|
||||
raycaster.setFromCamera(spread, camera);
|
||||
|
||||
const intersects = raycaster.intersectObjects(enemies, true);
|
||||
|
||||
if (intersects.length > 0) {
|
||||
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) {
|
||||
const hitPoint = intersects[0].point;
|
||||
createParticles(hitPoint, 0xaa0000, 5, 6);
|
||||
createParticles(hitPoint, weapon.color, 3, 4);
|
||||
|
||||
target.userData.health -= weapon.damage;
|
||||
playSound('hit');
|
||||
|
||||
const pushDir = hitPoint.clone().sub(camera.position).normalize();
|
||||
target.position.add(pushDir.multiplyScalar(0.5));
|
||||
|
||||
if (target.userData.health <= 0) {
|
||||
killEnemy(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function killEnemy(target) {
|
||||
createParticles(target.position, 0xaa0000, 30, 10);
|
||||
scene.remove(target);
|
||||
enemies = enemies.filter(e => e !== target);
|
||||
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});
|
||||
}
|
||||
}
|
||||
if(emptySpots.length > 0) {
|
||||
const spot = emptySpots[Math.floor(Math.random() * emptySpots.length)];
|
||||
spawnEnemy(spot.x * CELL_SIZE + CELL_SIZE/2, spot.z * CELL_SIZE + CELL_SIZE/2);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function startReload() {
|
||||
const weapon = getCurrentWeapon();
|
||||
if (isReloading || weapon.ammo >= weapon.maxAmmo || weapon.reserve <= 0) return;
|
||||
|
||||
isReloading = true;
|
||||
setTimeout(() => {
|
||||
const needed = weapon.maxAmmo - weapon.ammo;
|
||||
const available = Math.min(needed, weapon.reserve);
|
||||
weapon.ammo += available;
|
||||
weapon.reserve -= available;
|
||||
isReloading = false;
|
||||
playSound('pickup');
|
||||
updateHUD();
|
||||
}, weapon.reloadTime);
|
||||
}
|
||||
|
||||
function onKeyDown(event) {
|
||||
switch (event.code) {
|
||||
case 'KeyW': case 'ArrowUp': moveForward = true; break;
|
||||
case 'KeyA': case 'ArrowLeft': moveLeft = true; break;
|
||||
case 'KeyS': case 'ArrowDown': moveBackward = true; break;
|
||||
case 'KeyD': case 'ArrowRight': moveRight = true; break;
|
||||
case 'KeyR':
|
||||
if (ammo < MAX_AMMO && !isPumping) {
|
||||
isPumping = true;
|
||||
pumpGroup.position.z = -0.2;
|
||||
setTimeout(() => {
|
||||
ammo = Math.min(ammo + 6, MAX_AMMO);
|
||||
uiAmmo.innerText = ammo;
|
||||
isPumping = false;
|
||||
pumpGroup.position.z = 0;
|
||||
playSound('pickup');
|
||||
}, 800);
|
||||
}
|
||||
break;
|
||||
case 'KeyR': startReload(); break;
|
||||
case 'Digit1': switchWeapon(0); break;
|
||||
case 'Digit2': switchWeapon(1); break;
|
||||
case 'Digit3': switchWeapon(2); break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -470,69 +829,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
let mouseDown = false;
|
||||
function onMouseDown(event) {
|
||||
if (!controls.isLocked || event.button !== 0 || isPumping) return;
|
||||
if (ammo <= 0) {
|
||||
playSound('hit');
|
||||
return;
|
||||
}
|
||||
|
||||
ammo--;
|
||||
uiAmmo.innerText = ammo;
|
||||
playSound('shoot');
|
||||
|
||||
weaponGroup.position.z += 0.15;
|
||||
weaponGroup.rotation.x -= 0.15;
|
||||
pumpGroup.position.z = -0.2;
|
||||
|
||||
setTimeout(() => { pumpGroup.position.z = 0; }, 300);
|
||||
if (!controls.isLocked || event.button !== 0) return;
|
||||
mouseDown = true;
|
||||
fireWeapon();
|
||||
}
|
||||
|
||||
muzzleLight.position.copy(camera.position).add(camera.getWorldDirection(new THREE.Vector3()).multiplyScalar(1));
|
||||
muzzleLight.intensity = 10;
|
||||
setTimeout(() => { muzzleLight.intensity = 0; }, 60);
|
||||
document.addEventListener('mouseup', (e) => {
|
||||
if (e.button === 0) mouseDown = false;
|
||||
});
|
||||
|
||||
const raycaster = new THREE.Raycaster();
|
||||
raycaster.setFromCamera(new THREE.Vector2(0, 0), camera);
|
||||
const intersects = raycaster.intersectObjects(enemies, true);
|
||||
|
||||
if (intersects.length > 0) {
|
||||
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) {
|
||||
const hitPoint = intersects[0].point;
|
||||
createParticles(hitPoint, 0xaa0000, 10, 8);
|
||||
createParticles(hitPoint, 0xffaa00, 5, 6);
|
||||
|
||||
target.userData.health -= 20;
|
||||
playSound('hit');
|
||||
|
||||
const pushDir = hitPoint.clone().sub(camera.position).normalize();
|
||||
target.position.add(pushDir.multiplyScalar(0.8));
|
||||
|
||||
if (target.userData.health <= 0) {
|
||||
createParticles(target.position, 0xaa0000, 30, 10);
|
||||
scene.remove(target);
|
||||
enemies = enemies.filter(e => e !== target);
|
||||
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});
|
||||
}
|
||||
}
|
||||
if(emptySpots.length > 0) {
|
||||
const spot = emptySpots[Math.floor(Math.random() * emptySpots.length)];
|
||||
spawnEnemy(spot.x * CELL_SIZE + CELL_SIZE/2, spot.z * CELL_SIZE + CELL_SIZE/2);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
function onWheel(event) {
|
||||
if (!controls.isLocked) return;
|
||||
if (event.deltaY > 0) {
|
||||
switchWeapon((currentWeaponIndex + 1) % weapons.length);
|
||||
} else {
|
||||
switchWeapon((currentWeaponIndex - 1 + weapons.length) % weapons.length);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -549,8 +862,12 @@
|
||||
const delta = Math.min((time - prevTime) / 1000, 0.1);
|
||||
prevTime = time;
|
||||
|
||||
if (controls.isLocked) {
|
||||
// Физика движения
|
||||
// Автоогонь для плазмагана
|
||||
if (mouseDown && controls.isLocked && getCurrentWeapon().isAutomatic) {
|
||||
fireWeapon();
|
||||
}
|
||||
|
||||
if (controls.isLocked && !isSwitchingWeapon) {
|
||||
velocity.x -= velocity.x * 10.0 * delta;
|
||||
velocity.z -= velocity.z * 10.0 * delta;
|
||||
|
||||
@@ -564,11 +881,10 @@
|
||||
const nextX = camera.position.x - velocity.x * delta;
|
||||
const nextZ = camera.position.z - velocity.z * delta;
|
||||
|
||||
// ИСПРАВЛЕННОЕ СКОЛЬЖЕНИЕ ВДОЛЬ СТЕН: проверяем оси независимо
|
||||
if (!checkWallCollision(nextX, camera.position.z)) {
|
||||
camera.position.x = nextX;
|
||||
} else {
|
||||
velocity.x = 0; // Гасим инерцию при ударе о стену
|
||||
velocity.x = 0;
|
||||
}
|
||||
|
||||
if (!checkWallCollision(camera.position.x, nextZ)) {
|
||||
@@ -601,8 +917,19 @@
|
||||
if (d < 1.5) separation.add(new THREE.Vector3().subVectors(enemy.position, other.position).normalize().multiplyScalar(1/d));
|
||||
}
|
||||
});
|
||||
const finalDir = dirToPlayer.add(separation.multiplyScalar(0.5)).normalize();
|
||||
enemy.position.add(finalDir.multiplyScalar(enemy.userData.speed * delta));
|
||||
const finalDir = dirToPlayer.clone().add(separation.multiplyScalar(0.5)).normalize();
|
||||
|
||||
const newX = enemy.position.x + finalDir.x * enemy.userData.speed * delta;
|
||||
const newZ = enemy.position.z + finalDir.z * enemy.userData.speed * delta;
|
||||
|
||||
// Проверка коллизий врагов со стенами
|
||||
const gridX = Math.floor(newX / CELL_SIZE);
|
||||
const gridZ = Math.floor(newZ / CELL_SIZE);
|
||||
if (gridZ >= 0 && gridZ < mapLayout.length && gridX >= 0 && gridX < mapLayout[0].length && mapLayout[gridZ][gridX] !== 1) {
|
||||
enemy.position.x = newX;
|
||||
enemy.position.z = newZ;
|
||||
}
|
||||
|
||||
enemy.lookAt(camera.position.x, enemy.position.y, camera.position.z);
|
||||
}
|
||||
|
||||
@@ -612,10 +939,9 @@
|
||||
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));
|
||||
updateHUD();
|
||||
|
||||
damageOverlay.style.opacity = 0.6;
|
||||
setTimeout(() => { damageOverlay.style.opacity = 0; }, 150);
|
||||
@@ -638,11 +964,54 @@
|
||||
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;
|
||||
// Даём патроны для текущего оружия
|
||||
const weapon = getCurrentWeapon();
|
||||
weapon.reserve = Math.min(weapon.reserve + 12, weapon.maxAmmo * 4);
|
||||
playSound('pickup');
|
||||
scene.remove(p.mesh);
|
||||
pickups.splice(i, 1);
|
||||
updateHUD();
|
||||
}
|
||||
}
|
||||
|
||||
// Снаряды (плазмаган)
|
||||
for (let i = projectiles.length - 1; i >= 0; i--) {
|
||||
const p = projectiles[i];
|
||||
p.life -= delta;
|
||||
p.mesh.position.add(p.velocity.clone().multiplyScalar(delta));
|
||||
|
||||
// Проверка столкновения со стенами
|
||||
const gridX = Math.floor(p.mesh.position.x / CELL_SIZE);
|
||||
const gridZ = Math.floor(p.mesh.position.z / CELL_SIZE);
|
||||
let hitWall = false;
|
||||
if (gridZ < 0 || gridZ >= mapLayout.length || gridX < 0 || gridX >= mapLayout[0].length || mapLayout[gridZ][gridX] === 1) {
|
||||
hitWall = true;
|
||||
}
|
||||
|
||||
// Проверка столкновения с врагами
|
||||
let hitEnemy = false;
|
||||
for (let enemy of enemies) {
|
||||
if (p.mesh.position.distanceTo(enemy.position) < 1.0) {
|
||||
createParticles(p.mesh.position, p.color, 15, 8);
|
||||
createParticles(p.mesh.position, 0xaa0000, 10, 6);
|
||||
enemy.userData.health -= p.damage;
|
||||
playSound('hit');
|
||||
|
||||
const pushDir = p.velocity.clone().normalize().multiplyScalar(0.8);
|
||||
enemy.position.add(pushDir);
|
||||
|
||||
if (enemy.userData.health <= 0) {
|
||||
killEnemy(enemy);
|
||||
}
|
||||
hitEnemy = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hitWall || hitEnemy || p.life <= 0) {
|
||||
if (hitWall) createParticles(p.mesh.position, p.color, 10, 5);
|
||||
scene.remove(p.mesh);
|
||||
projectiles.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -659,8 +1028,6 @@
|
||||
particles.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
ammoWarning.style.opacity = ammo <= 6 ? 1 : 0;
|
||||
}
|
||||
|
||||
renderer.render(scene, camera);
|
||||
|
||||
Reference in New Issue
Block a user