<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Шутер (а-ля Standoff 2)</title>
<style>
body { margin: 0; overflow: hidden; background: #000; }
#info {
position: absolute;
top: 20px;
left: 20px;
color: white;
font-family: Arial, sans-serif;
font-size: 18px;
background: rgba(0,0,0,0.6);
padding: 10px 20px;
border-radius: 8px;
pointer-events: none;
user-select: none;
}
#reticle {
position: absolute;
top: 50%;
left: 50%;
width: 4px;
height: 4px;
background: red;
border-radius: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
}
#hint {
position: absolute;
bottom: 30px;
left: 0;
width: 100%;
text-align: center;
color: #aaa;
font-family: Arial, sans-serif;
font-size: 16px;
pointer-events: none;
background: rgba(0,0,0,0.5);
padding: 8px;
}
</style>
</head>
<body>
<div id="info">🎯 Счёт: <span id="score">0</span> | Врагов: <span id="enemies">0</span></div>
<div id="reticle"></div>
<div id="hint">Кликните по экрану → управление WASD + мышь (прицел) / ЛКМ — огонь</div>
<!-- Подключаем Three.js и вспомогательные библиотеки -->
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.128.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.128.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';
// --- Сцена, камера, рендер ---
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87CEEB); // небо
scene.fog = new THREE.Fog(0x87CEEB, 30, 80);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 1.6, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);
// --- Управление (захват мыши) ---
const controls = new PointerLockControls(camera, document.body);
// --- Освещение ---
const ambientLight = new THREE.AmbientLight(0x404060);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.position.set(20, 30, 10);
dirLight.castShadow = true;
scene.add(dirLight);
const backLight = new THREE.DirectionalLight(0xccddff, 0.5);
backLight.position.set(-20, 10, -20);
scene.add(backLight);
// --- Пол ---
const groundGeometry = new THREE.PlaneGeometry(100, 100);
const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x2c3e50, roughness: 0.8 });
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -0.01;
ground.receiveShadow = true;
scene.add(ground);
// Сетка для ориентира
const grid = new THREE.GridHelper(100, 20, 0xaaaaaa, 0x444444);
grid.position.y = 0;
scene.add(grid);
// --- Стены по краям (для ощущения пространства) ---
const wallMat = new THREE.MeshStandardMaterial({ color: 0x7f8c8d, roughness: 0.7 });
const createWall = (x, z, w, h, d) => {
const wall = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), wallMat);
wall.position.set(x, h/2, z);
wall.castShadow = true;
wall.receiveShadow = true;
scene.add(wall);
return wall;
};
createWall(0, -48, 100, 4, 2); // задняя
createWall(0, 48, 100, 4, 2); // передняя
createWall(-48, 0, 2, 4, 100); // левая
createWall(48, 0, 2, 4, 100); // правая
// --- Создание врагов (красные кубы) ---
const enemies = [];
const enemyMaterial = new THREE.MeshStandardMaterial({ color: 0xe74c3c, emissive: 0x330000 });
const enemyGeometry = new THREE.BoxGeometry(0.8, 1.8, 0.8);
function spawnEnemy() {
let x, z, valid;
let attempts = 0;
do {
x = (Math.random() - 0.5) * 70;
z = (Math.random() - 0.5) * 70;
valid = true;
// Не спавнить слишком близко к игроку (0,0)
if (Math.sqrt(x*x + z*z) < 5) valid = false;
// Не спавнить внутри других врагов
for (let e of enemies) {
if (e.position.distanceTo(new THREE.Vector3(x, 0, z)) < 1.5) {
valid = false;
break;
}
}
attempts++;
if (attempts > 100) break;
} while (!valid);
const enemy = new THREE.Mesh(enemyGeometry, enemyMaterial);
enemy.position.set(x, 1.0, z);
enemy.castShadow = true;
enemy.receiveShadow = true;
scene.add(enemy);
enemies.push(enemy);
updateUI();
}
// Изначально 10 врагов
for (let i = 0; i < 10; i++) spawnEnemy();
// --- Счёт и UI ---
let score = 0;
const scoreSpan = document.getElementById('score');
const enemiesSpan = document.getElementById('enemies');
function updateUI() {
scoreSpan.textContent = score;
enemiesSpan.textContent = enemies.length;
}
updateUI();
// --- Выстрел (Raycaster) ---
const raycaster = new THREE.Raycaster();
function shoot() {
if (!controls.isLocked) return; // стрельба только когда захвачена мышь
raycaster.setFromCamera({ x: 0, y: 0 }, camera); // центр экрана
const intersects = raycaster.intersectObjects(enemies);
if (intersects.length > 0) {
const hit = intersects[0].object;
// Удаляем врага
scene.remove(hit);
const idx = enemies.indexOf(hit);
if (idx !== -1) enemies.splice(idx, 1);
score++;
updateUI();
// Через 2 секунды появится новый враг (чтобы не было пусто)
setTimeout(() => {
if (enemies.length < 20) spawnEnemy();
}, 1000);
}
}
// --- Обработка событий ---
// Клик по экрану для захвата мыши
document.addEventListener('click', () => {
if (!controls.isLocked) {
controls.lock();
} else {
shoot(); // если уже захвачено, стреляем
}
});
// Клавиша Escape разблокирует мышь
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && controls.isLocked) {
controls.unlock();
}
});
// Событие выстрела также по пробелу (для удобства)
document.addEventListener('keydown', (e) => {
if (e.key === ' ' && controls.isLocked) {
e.preventDefault();
shoot();
}
});
// --- Перемещение (клавиши WASD) ---
const keys = { w: false, a: false, s: false, d: false };
document.addEventListener('keydown', (e) => {
switch(e.key.toLowerCase()) {
case 'w': keys.w = true; e.preventDefault(); break;
case 'a': keys.a = true; e.preventDefault(); break;
case 's': keys.s = true; e.preventDefault(); break;
case 'd': keys.d = true; e.preventDefault(); break;
}
});
document.addEventListener('keyup', (e) => {
switch(e.key.toLowerCase()) {
case 'w': keys.w = false; e.preventDefault(); break;
case 'a': keys.a = false; e.preventDefault(); break;
case 's': keys.s = false; e.preventDefault(); break;
case 'd': keys.d = false; e.preventDefault(); break;
}
});
// --- Адаптация размера окна ---
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// --- Анимация цикла ---
function animate() {
requestAnimationFrame(animate);
// Перемещение (только если мышь захвачена)
if (controls.isLocked) {
const speed = 0.12;
const forward = new THREE.Vector3(0, 0, -1);
forward.applyQuaternion(camera.quaternion);
forward.y = 0;
forward.normalize();
const right = new THREE.Vector3(1, 0, 0);
right.applyQuaternion(camera.quaternion);
right.y = 0;
right.normalize();
const moveVec = new THREE.Vector3(0, 0, 0);
if (keys.w) moveVec.add(forward);
if (keys.s) moveVec.sub(forward);
if (keys.d) moveVec.add(right);
if (keys.a) moveVec.sub(right);
if (moveVec.length() > 0) {
moveVec.normalize().multiplyScalar(speed);
camera.position.add(moveVec);
// Простая коллизия со стенами (приблизительная)
if (Math.abs(camera.position.x) > 46) camera.position.x = Math.sign(camera.position.x) * 46;
if (Math.abs(camera.position.z) > 46) camera.position.z = Math.sign(camera.position.z) * 46;
}
}
renderer.render(scene, camera);
}
animate();
// Периодическое появление новых врагов, если их мало
setInterval(() => {
if (enemies.length < 8) {
spawnEnemy();
}
}, 5000);
console.log('Игра запущена! Кликните, чтобы захватить мышь, WASD — ходьба, клик/пробел — стрельба.');
</script>
</body>
</html>