438 lines
18 KiB
HTML
438 lines
18 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ru">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>DOOM ETERNAL: Web 3D Prototype</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; }
|
|
|
|
/* Прицел */
|
|
#crosshair {
|
|
position: absolute; top: 50%; left: 50%; width: 20px; height: 20px;
|
|
transform: translate(-50%, -50%); pointer-events: none; z-index: 10;
|
|
}
|
|
#crosshair::before, #crosshair::after {
|
|
content: ''; position: absolute; background: #00ff00; box-shadow: 0 0 5px #00ff00;
|
|
}
|
|
#crosshair::before { top: 9px; left: 0; width: 20px; height: 2px; }
|
|
#crosshair::after { top: 0; left: 9px; width: 2px; height: 20px; }
|
|
|
|
/* Экран старта */
|
|
#blocker {
|
|
position: absolute; width: 100%; height: 100%; background: rgba(0,0,0,0.85);
|
|
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
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; }
|
|
#sub-instructions { font-size: 18px; color: #aaa; margin-top: 20px; font-family: monospace; }
|
|
|
|
/* HUD в стиле Doom Eternal */
|
|
#hud {
|
|
position: absolute; bottom: 0; left: 0; width: 100%; height: 100px;
|
|
background: linear-gradient(to top, #0a0a0a 80%, transparent);
|
|
border-top: 3px solid #8b0000; display: flex; justify-content: space-between;
|
|
align-items: center; padding: 0 40px; box-sizing: border-box; pointer-events: none; z-index: 10;
|
|
}
|
|
.stat-box { text-align: center; color: #fff; }
|
|
.stat-label { font-size: 14px; color: #888; letter-spacing: 2px; text-transform: uppercase; }
|
|
.stat-value { font-size: 48px; line-height: 48px; text-shadow: 0 0 10px currentColor; }
|
|
.health { color: #00ff00; }
|
|
.armor { color: #00bfff; }
|
|
.ammo { color: #ffcc00; }
|
|
|
|
#damage-overlay {
|
|
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
|
background: radial-gradient(circle, transparent 50%, rgba(255,0,0,0.6) 100%);
|
|
opacity: 0; pointer-events: none; transition: opacity 0.2s; z-index: 5;
|
|
}
|
|
</style>
|
|
|
|
<!-- Импорт Three.js через CDN -->
|
|
<script type="importmap">
|
|
{
|
|
"imports": {
|
|
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
|
|
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
|
|
}
|
|
}
|
|
</script>
|
|
</head>
|
|
<body>
|
|
|
|
<div id="game-container">
|
|
<div id="damage-overlay"></div>
|
|
<div id="crosshair"></div>
|
|
|
|
<div id="blocker">
|
|
<div id="instructions">КЛИКНИ, ЧТОБЫ ИГРАТЬ</div>
|
|
<div id="sub-instructions">WASD - Движение | Мышь - Обзор | ЛКМ - Огонь | RIP AND TEAR</div>
|
|
</div>
|
|
|
|
<div id="hud">
|
|
<div class="stat-box">
|
|
<div class="stat-label">Здоровье</div>
|
|
<div class="stat-value health" id="health-val">100</div>
|
|
</div>
|
|
<div class="stat-box">
|
|
<div class="stat-label">Броня</div>
|
|
<div class="stat-value armor" id="armor-val">50</div>
|
|
</div>
|
|
<div class="stat-box">
|
|
<div class="stat-label">Патроны</div>
|
|
<div class="stat-value ammo" id="ammo-val">24</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script type="module">
|
|
import * as THREE from 'three';
|
|
import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';
|
|
|
|
// --- Глобальные переменные ---
|
|
let camera, scene, renderer, controls;
|
|
let moveForward = false, moveBackward = false, moveLeft = false, moveRight = false;
|
|
let prevTime = performance.now();
|
|
const velocity = new THREE.Vector3();
|
|
const direction = new THREE.Vector3();
|
|
|
|
// Игровые параметры
|
|
let health = 100, armor = 50, ammo = 24;
|
|
let enemies = [];
|
|
let particles = [];
|
|
let weaponGroup;
|
|
let isShooting = false;
|
|
let muzzleLight;
|
|
|
|
// Элементы UI
|
|
const blocker = document.getElementById('blocker');
|
|
const instructions = document.getElementById('instructions');
|
|
const damageOverlay = document.getElementById('damage-overlay');
|
|
const uiHealth = document.getElementById('health-val');
|
|
const uiArmor = document.getElementById('armor-val');
|
|
const uiAmmo = document.getElementById('ammo-val');
|
|
|
|
init();
|
|
animate();
|
|
|
|
function init() {
|
|
// 1. Сцена и Камера
|
|
scene = new THREE.Scene();
|
|
scene.background = new THREE.Color(0x050000);
|
|
// Адский туман
|
|
scene.fog = new THREE.FogExp2(0x1a0505, 0.035);
|
|
|
|
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
|
camera.position.y = 1.6; // Рост игрока
|
|
|
|
// 2. Освещение
|
|
const ambientLight = new THREE.AmbientLight(0x401010, 0.5); // Темно-красный ambient
|
|
scene.add(ambientLight);
|
|
|
|
const dirLight = new THREE.DirectionalLight(0xff4400, 0.8);
|
|
dirLight.position.set(10, 20, 10);
|
|
dirLight.castShadow = true;
|
|
scene.add(dirLight);
|
|
|
|
// Вспышка выстрела (PointLight)
|
|
muzzleLight = new THREE.PointLight(0xffaa00, 0, 10);
|
|
scene.add(muzzleLight);
|
|
|
|
// 3. Окружение (Пол и Стены)
|
|
const floorGeometry = new THREE.PlaneGeometry(100, 100);
|
|
const floorMaterial = new THREE.MeshStandardMaterial({
|
|
color: 0x221111, roughness: 0.8, metalness: 0.2
|
|
});
|
|
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
|
|
floor.rotation.x = -Math.PI / 2;
|
|
floor.receiveShadow = true;
|
|
scene.add(floor);
|
|
|
|
// Создаем несколько колонн/стен для укрытий
|
|
const boxGeo = new THREE.BoxGeometry(2, 6, 2);
|
|
const boxMat = new THREE.MeshStandardMaterial({ color: 0x331111, roughness: 0.9 });
|
|
for (let i = 0; i < 15; i++) {
|
|
const wall = new THREE.Mesh(boxGeo, boxMat);
|
|
wall.position.x = (Math.random() - 0.5) * 60;
|
|
wall.position.z = (Math.random() - 0.5) * 60;
|
|
wall.position.y = 3;
|
|
wall.castShadow = true;
|
|
wall.receiveShadow = true;
|
|
scene.add(wall);
|
|
}
|
|
|
|
// 4. Оружие (Супердробовик - процедурная модель)
|
|
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 barrel1 = new THREE.Mesh(barrelGeo, barrelMat);
|
|
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);
|
|
body.position.z = 0.1;
|
|
|
|
weaponGroup.add(barrel1, barrel2, body);
|
|
weaponGroup.position.set(0.3, -0.3, -0.5);
|
|
camera.add(weaponGroup); // Прикрепляем оружие к камере
|
|
scene.add(camera);
|
|
|
|
// 5. Управление
|
|
controls = new PointerLockControls(camera, document.body);
|
|
|
|
blocker.addEventListener('click', () => {
|
|
controls.lock();
|
|
});
|
|
|
|
controls.addEventListener('lock', () => {
|
|
instructions.style.display = 'none';
|
|
blocker.style.display = 'none';
|
|
});
|
|
|
|
controls.addEventListener('unlock', () => {
|
|
blocker.style.display = 'flex';
|
|
instructions.style.display = 'block';
|
|
instructions.innerText = "ПАУЗА. КЛИКНИ, ЧТОБЫ ПРОДОЛЖИТЬ";
|
|
});
|
|
|
|
document.addEventListener('keydown', onKeyDown);
|
|
document.addEventListener('keyup', onKeyUp);
|
|
document.addEventListener('mousedown', onMouseDown);
|
|
|
|
// 6. Рендерер
|
|
renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
renderer.setPixelRatio(window.devicePixelRatio);
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
renderer.shadowMap.enabled = true;
|
|
document.getElementById('game-container').appendChild(renderer.domElement);
|
|
|
|
window.addEventListener('resize', onWindowResize);
|
|
|
|
// Спавн первых врагов
|
|
for(let i=0; i<5; i++) spawnEnemy();
|
|
}
|
|
|
|
function spawnEnemy() {
|
|
// Враг - демоническая светящаяся сфера (Икосаэдр)
|
|
const geometry = new THREE.IcosahedronGeometry(0.8, 1);
|
|
const material = new THREE.MeshStandardMaterial({
|
|
color: 0xff0000,
|
|
emissive: 0x550000,
|
|
roughness: 0.4,
|
|
metalness: 0.6
|
|
});
|
|
const enemy = new THREE.Mesh(geometry, material);
|
|
|
|
// Случайная позиция вдали от игрока
|
|
const angle = Math.random() * Math.PI * 2;
|
|
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;
|
|
enemy.userData = { health: 30, speed: 2 + Math.random() * 2, lastAttack: 0 };
|
|
|
|
scene.add(enemy);
|
|
enemies.push(enemy);
|
|
}
|
|
|
|
function createParticles(position, color, count) {
|
|
const geo = new THREE.BoxGeometry(0.1, 0.1, 0.1);
|
|
const mat = new THREE.MeshBasicMaterial({ color: color });
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
const mesh = new THREE.Mesh(geo, mat);
|
|
mesh.position.copy(position);
|
|
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(
|
|
(Math.random() - 0.5) * 5,
|
|
(Math.random() - 0.5) * 5 + 2, // Вверх
|
|
(Math.random() - 0.5) * 5
|
|
);
|
|
|
|
scene.add(mesh);
|
|
particles.push({ mesh, vel, life: 1.0 });
|
|
}
|
|
}
|
|
|
|
function onKeyDown(event) {
|
|
switch (event.code) {
|
|
case 'ArrowUp': case 'KeyW': moveForward = true; break;
|
|
case 'ArrowLeft': case 'KeyA': moveLeft = true; break;
|
|
case 'ArrowDown': case 'KeyS': moveBackward = true; break;
|
|
case 'ArrowRight': case 'KeyD': moveRight = true; break;
|
|
}
|
|
}
|
|
|
|
function onKeyUp(event) {
|
|
switch (event.code) {
|
|
case 'ArrowUp': case 'KeyW': moveForward = false; break;
|
|
case 'ArrowLeft': case 'KeyA': moveLeft = false; break;
|
|
case 'ArrowDown': case 'KeyS': moveBackward = false; break;
|
|
case 'ArrowRight': case 'KeyD': moveRight = false; break;
|
|
}
|
|
}
|
|
|
|
function onMouseDown(event) {
|
|
if (!controls.isLocked || event.button !== 0 || ammo <= 0) return;
|
|
|
|
// Логика выстрела
|
|
ammo--;
|
|
uiAmmo.innerText = ammo;
|
|
isShooting = true;
|
|
|
|
// Анимация отдачи
|
|
weaponGroup.position.z += 0.2;
|
|
weaponGroup.rotation.x -= 0.1;
|
|
|
|
// Вспышка света
|
|
muzzleLight.position.copy(camera.position).add(camera.getWorldDirection(new THREE.Vector3()).multiplyScalar(1));
|
|
muzzleLight.intensity = 5;
|
|
setTimeout(() => { muzzleLight.intensity = 0; }, 50);
|
|
|
|
// Raycasting (стрельба)
|
|
const raycaster = new THREE.Raycaster();
|
|
raycaster.setFromCamera(new THREE.Vector2(0, 0), camera);
|
|
|
|
const intersects = raycaster.intersectObjects(enemies);
|
|
if (intersects.length > 0) {
|
|
const hit = intersects[0];
|
|
const enemy = hit.object;
|
|
|
|
// Эффект попадания
|
|
createParticles(hit.point, 0xff0000, 8); // Кровь
|
|
createParticles(hit.point, 0xffaa00, 4); // Искры
|
|
|
|
enemy.userData.health -= 15;
|
|
|
|
// Отталкивание врага
|
|
const pushDir = hit.point.clone().sub(camera.position).normalize();
|
|
enemy.position.add(pushDir.multiplyScalar(0.5));
|
|
|
|
if (enemy.userData.health <= 0) {
|
|
scene.remove(enemy);
|
|
enemies = enemies.filter(e => e !== enemy);
|
|
createParticles(enemy.position, 0xff0000, 20); // Взрыв крови
|
|
spawnEnemy(); // Спавним нового
|
|
spawnEnemy(); // Усложняем
|
|
}
|
|
}
|
|
}
|
|
|
|
function onWindowResize() {
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
}
|
|
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
|
|
const time = performance.now();
|
|
const delta = (time - prevTime) / 1000;
|
|
prevTime = time;
|
|
|
|
if (controls.isLocked) {
|
|
// --- Движение игрока ---
|
|
velocity.x -= velocity.x * 10.0 * delta;
|
|
velocity.z -= velocity.z * 10.0 * delta;
|
|
|
|
direction.z = Number(moveForward) - Number(moveBackward);
|
|
direction.x = Number(moveRight) - Number(moveLeft);
|
|
direction.normalize();
|
|
|
|
if (moveForward || moveBackward) velocity.z -= direction.z * 100.0 * delta;
|
|
if (moveLeft || moveRight) velocity.x -= direction.x * 100.0 * delta;
|
|
|
|
controls.moveRight(-velocity.x * delta);
|
|
controls.moveForward(-velocity.z * delta);
|
|
|
|
// Ограничение карты (простая коллизия с границами)
|
|
camera.position.x = Math.max(-45, Math.min(45, camera.position.x));
|
|
camera.position.z = Math.max(-45, Math.min(45, camera.position.z));
|
|
|
|
// --- Анимация оружия ---
|
|
// Возврат отдачи
|
|
weaponGroup.position.z = THREE.MathUtils.lerp(weaponGroup.position.z, -0.5, delta * 10);
|
|
weaponGroup.rotation.x = THREE.MathUtils.lerp(weaponGroup.rotation.x, 0, delta * 10);
|
|
|
|
// Покачивание оружия при ходьбе (Bobbing)
|
|
if (moveForward || moveBackward || moveLeft || moveRight) {
|
|
weaponGroup.position.y = -0.3 + Math.sin(time * 0.01) * 0.01;
|
|
weaponGroup.position.x = 0.3 + Math.cos(time * 0.01) * 0.01;
|
|
}
|
|
|
|
// --- Логика врагов ---
|
|
enemies.forEach(enemy => {
|
|
// Движение к игроку
|
|
const dirToPlayer = new THREE.Vector3().subVectors(camera.position, enemy.position).normalize();
|
|
dirToPlayer.y = 0; // Не летать вверх/вниз
|
|
|
|
// Простая проверка расстояния, чтобы не застревать друг в друге
|
|
let tooClose = false;
|
|
enemies.forEach(other => {
|
|
if (enemy !== other && enemy.position.distanceTo(other.position) < 1.5) {
|
|
tooClose = true;
|
|
}
|
|
});
|
|
|
|
if (!tooClose) {
|
|
enemy.position.add(dirToPlayer.multiplyScalar(enemy.userData.speed * delta));
|
|
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 > 1000) {
|
|
enemy.userData.lastAttack = time;
|
|
health -= 10;
|
|
uiHealth.innerText = health;
|
|
|
|
// Эффект урона
|
|
damageOverlay.style.opacity = 0.8;
|
|
setTimeout(() => { damageOverlay.style.opacity = 0; }, 200);
|
|
|
|
// Тряска камеры
|
|
camera.position.y += 0.1;
|
|
setTimeout(() => { camera.position.y -= 0.1; }, 50);
|
|
|
|
if (health <= 0) {
|
|
alert("ВЫ ПОГИБЛИ. RIP AND TEAR В СЛЕДУЮЩИЙ РАЗ.");
|
|
location.reload();
|
|
}
|
|
}
|
|
});
|
|
|
|
// --- Частицы ---
|
|
for (let i = particles.length - 1; i >= 0; i--) {
|
|
const p = particles[i];
|
|
p.life -= delta * 2;
|
|
p.vel.y -= 9.8 * delta; // Гравитация
|
|
p.mesh.position.add(p.vel.clone().multiplyScalar(delta));
|
|
p.mesh.rotation.x += delta * 5;
|
|
p.mesh.scale.setScalar(p.life);
|
|
|
|
if (p.life <= 0 || p.mesh.position.y < 0) {
|
|
scene.remove(p.mesh);
|
|
particles.splice(i, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
renderer.render(scene, camera);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |