USER
<html>
<head>
<meta charset="UTF-8">
<title>Кибершамыга</title>
<style>
body { margin: 0; overflow: hidden; font-family: Arial, sans-serif; }
canvas { display: block; }
#menu {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: white;
}
#menu h1 {
font-size: 4em;
margin-bottom: 30px;
}
#playButton {
font-size: 1.5em;
padding: 10px 20px;
cursor: pointer;
}
#hud, #health-bar, #crosshair {
display: none;
}
#hud {
position: absolute;
top: 10px;
left: 10px;
color: white;
font-size: 18px;
}
#health-bar {
position: absolute;
bottom: 10px;
left: 10px;
width: 200px;
height: 20px;
background-color: red;
}
#health {
height: 100%;
background-color: green;
width: 100%;
text-align: center;
color: white;
font-weight: bold;
line-height: 20px;
}
#crosshair {
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 20px;
background-color: transparent;
border: 2px solid red;
border-radius: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
}
#death-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8); /* затемненный фон */
display: none; /* Скрыто по умолчанию */
justify-content: center;
align-items: center;
}
#death-modal-content {
background-color: #333;
padding: 40px;
border-radius: 10px;
text-align: center;
color: white;
}
#death-modal-content h2 {
margin-bottom: 30px;
}
#death-modal-content button {
font-size: 1.2em;
padding: 10px 30px;
margin: 10px;
cursor: pointer;
}
#skins-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
display: none;
justify-content: center;
align-items: center;
}
#skins-modal-content {
background-color: #333;
padding: 40px;
border-radius: 10px;
text-align: center;
color: white;
}
.skin-item {
margin-bottom: 10px;
}
.skin-item span {
margin-right: 10px;
}
#version {
position: fixed;
bottom: 10px;
right: 10px;
color: white;
font-size: 14px;
}
</style>
</head>
<body>
<!-- Меню -->
<div id="menu">
<h1>Кибершамыга</h1>
<button id="playButton">Играть</button>
<span id="previous-kills"> Kills record: <span id="kills-display">0</span></span>
<button id="skinsButton">Скины</button>
</div>
<div id="skins-modal">
<div id="skins-modal-content">
<h2>Выбор скинов</h2>
<div id="skins-list">
<!-- Здесь будут отображаться скины -->
</div>
<button id="closeSkinsButton">Закрыть</button>
</div>
</div>
<div id="death-modal">
<div id="death-modal-content">
<h2>Вы погибли!</h2>
<button id="restartButton">Начать заново</button>
<button id="returnToMenuButton">Вернуться в меню</button>
</div>
</div>
<!-- HUD (Heads-Up Display) -->
<div id="hud">
<div>Время: <span id="time">00:00</span></div>
<div>Киллы: <span id="kills">0</span></div>
<div>Патроны: <span id="ammo">10/∞</span></div>
</div>
<!-- Полоса здоровья -->
<div id="health-bar">
<div id="health">100%</div>
</div>
<!-- Прицел -->
<div id="crosshair"></div>
<!-- Подключаем библиотеку Three.js с CDN -->
<script src="https://cdn.jsdelivr.net/npm/three@0.148/build/three.min.js"></script>
<script>
let scene, camera, renderer, player;
let health = 100;
let bulletSpeed = 0.5;
let gameStartTime;
let kills = 0;
let killsRecord = 0;
let muzzleFlash; // Дульная вспышка
let muzzleFlashDuration = 0;
let muzzleFlashEndTime = 0;
let magazineSize = 10; // Размер обоймы
let currentAmmo = magazineSize; // Текущее количество патронов
let isReloading = false; // Флаг перезарядки
let canShoot = true;
let isGameActive = false; // Флаг состояния игры
let skins = {
default: {
name: "Стандартный",
unlocked: true,
model: null // Модель пистолета для этого скина
},
golden: {
name: "Золотой",
unlocked: false,
model: null // Модель пистолета для этого скина
}
};
let selectedSkin = 'default';
let reloadAnimationProgress = 0;
let isAnimatingReload = false;
const reloadAnimationDuration = 1.5; // Длительность анимации перезарядки
const enemies = [];
let enemySpeed = 0.05; // Начальная скорость врагов
let maxEnemies = 5; // Начальное максимальное количество врагов
let lastDifficultyIncreaseTime; // Время последнего увеличения сложности
const bullets = [];
const trees = [];
const skinsButton = document.getElementById('skinsButton');
const skinsModal = document.getElementById('skins-modal');
const closeSkinsButton = document.getElementById('closeSkinsButton');
skinsButton.addEventListener('click', function(event) {
event.stopPropagation();
skinsModal.style.display = 'flex'; // Показываем меню скинов
});
closeSkinsButton.addEventListener('click', function(event) {
event.stopPropagation();
skinsModal.style.display = 'none'; // Скрываем меню скинов
});
let gunGroup, gunBody, magazine;
function updateGunSkin() {
if (selectedSkin === 'golden') {
gunBody.material.color.set(0xFFD700); // Золотой цвет
} else {
gunBody.material.color.set(0x333333); // Стандартный цвет
}
}
function updateSkinsMenu() {
const skinsList = document.getElementById('skins-list');
skinsList.innerHTML = ''; // Очищаем список
for (let key in skins) {
let skin = skins[key];
let skinDiv = document.createElement('div');
skinDiv.className = 'skin-item';
let skinName = document.createElement('span');
skinName.textContent = skin.name;
let selectButton = document.createElement('button');
selectButton.textContent = 'Выбрать';
selectButton.disabled = !skin.unlocked;
selectButton.addEventListener('click', function () {
selectedSkin = key;
skinsModal.style.display = 'none';
updateGunSkin();
});
if (!skin.unlocked) {
let lockSpan = document.createElement('span');
lockSpan.textContent = '🔒 Требуется 1000 киллов';
skinDiv.appendChild(lockSpan);
}
skinDiv.appendChild(skinName);
skinDiv.appendChild(selectButton);
skinsList.appendChild(skinDiv);
}
}
function saveSkins() {
localStorage.setItem('skins', JSON.stringify({
goldenUnlocked: skins.golden.unlocked
}));
}
// Функция для загрузки информации о скинах из localStorage
function loadSkins() {
let storedSkins = localStorage.getItem('skins');
if (storedSkins) {
let data = JSON.parse(storedSkins);
skins.golden.unlocked = data.goldenUnlocked || false;
}
}
// Вызываем loadSkins() после объявления переменных
loadSkins();
function getRandomSpawnPosition() {
let position;
let isPositionValid = false;
while (!isPositionValid) {
position = new THREE.Vector3(
(Math.random() - 0.5) * 100,
1.6,
(Math.random() - 0.5) * 100
);
isPositionValid = true;
// Проверяем, что позиция не слишком близка к деревьям
for (let i = 0; i < trees.length; i++) {
const tree = trees[i];
const distance = position.distanceTo(tree.position);
if (distance < 5) { // 5 — минимальное расстояние до дерева
isPositionValid = false;
break;
}
}
}
return position;
}
function reload() {
if (isReloading || currentAmmo === magazineSize) {
return; // Уже выполняется перезарядка или обойма полная
}
isReloading = true;
isAnimatingReload = true;
reloadAnimationProgress = 0;
updateHUD();
}
function showDeathModal() {
const deathModal = document.getElementById('death-modal');
deathModal.style.display = 'flex';
}
function saveProgress() {
localStorage.setItem('killsRecord', killsRecord);
}
function loadProgress() {
const storedKillsRecord = localStorage.getItem('killsRecord');
if (storedKillsRecord) {
killsRecord = parseInt(storedKillsRecord, 10);
}
}
function updateMenu() {
const killsDisplay = document.getElementById('kills-display');
killsDisplay.textContent = killsRecord;
// Обновляем список скинов
updateSkinsMenu();
}
// Call this function when loading the page to show the previous kills
loadProgress();
updateMenu();
// Функция для обновления HUD
function updateHUD() {
const timeElement = document.getElementById('time');
const killsElement = document.getElementById('kills');
const ammoElement = document.getElementById('ammo');
const currentTime = new Date().getTime();
const elapsedTime = Math.floor((currentTime - gameStartTime) / 1000);
const minutes = Math.floor(elapsedTime / 60);
const seconds = elapsedTime % 60;
// Используем шаблонные строки (template literals) с обратными кавычками
timeElement.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
killsElement.textContent = kills;
// Обновляем отображение патронов
ammoElement.textContent = isReloading ? 'Перезарядка...' : `${currentAmmo}/${String.fromCharCode(8734)}`; // ∞ символ
}
function updateHealthBar() {
const healthBar = document.getElementById('health');
healthBar.style.width = health + '%';
healthBar.textContent = health + '%';
}
function takeDamage(amount) {
health -= amount;
if (health <= 0) {
health = 0;
updateHealthBar();
endGame();
showDeathModal();
} else {
updateHealthBar();
}
}
function createTree() {
const trunkGeometry = new THREE.CylinderGeometry(0.2, 0.2, 2);
const trunkMaterial = new THREE.MeshBasicMaterial({ color: 0x8B4513 });
const trunk = new THREE.Mesh(trunkGeometry, trunkMaterial);
const leavesGeometry = new THREE.ConeGeometry(1, 2, 8);
const leavesMaterial = new THREE.MeshBasicMaterial({ color: 0x228B22 });
const leaves = new THREE.Mesh(leavesGeometry, leavesMaterial);
leaves.position.y = 2;
const tree = new THREE.Group();
tree.add(trunk);
tree.add(leaves);
return tree;
}
function createEnemy() {
if (!isGameActive) return;
// Проверяем, не превышено ли максимальное количество врагов
if (enemies.length >= maxEnemies) return;
// Создаем объект врага для управления позицией
const enemy = new THREE.Object3D();
enemy.position.y = 1; // Установка высоты
// Создаем визуальную модель врага (plane)
const enemyTexture = new THREE.TextureLoader().load('https://i.postimg.cc/QC6j7xGC/photo-2024-05-28-17-49-00.jpg');
const enemyMaterial = new THREE.MeshBasicMaterial({
map: enemyTexture,
transparent: true,
side: THREE.DoubleSide
});
const enemyGeometry = new THREE.PlaneGeometry(2, 2);
const enemyMesh = new THREE.Mesh(enemyGeometry, enemyMaterial);
enemyMesh.userData.isEnemyMesh = true; // Помечаем для идентификации
enemy.add(enemyMesh);
// Создаем и добавляем меч к визуальной модели
const sword = createSword();
sword.position.set(0.5, -0.5, 0);
sword.userData.initialRotation = sword.rotation.clone();
sword.userData.isSword = true; // Помечаем меч
enemyMesh.add(sword);
// Устанавливаем начальную позицию врага
let enemyPosition;
do {
enemyPosition = new THREE.Vector3(
(Math.random() - 0.5) * 100,
1,
(Math.random() - 0.5) * 100
);
} while (enemyPosition.distanceTo(player.position) < 10);
enemy.position.copy(enemyPosition);
scene.add(enemy);
enemies.push(enemy);
}
function createSword() {
const bladeGeometry = new THREE.BoxGeometry(0.05, 1, 0.2);
const bladeMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
const blade = new THREE.Mesh(bladeGeometry, bladeMaterial);
blade.position.set(0, 0.5, 0);
const hiltGeometry = new THREE.CylinderGeometry(0.05, 0.05, 0.3);
const hiltMaterial = new THREE.MeshBasicMaterial({ color: 0x8B4513 });
const hilt = new THREE.Mesh(hiltGeometry, hiltMaterial);
hilt.position.set(0, -0.15, 0);
const sword = new THREE.Group();
sword.add(blade);
sword.add(hilt);
sword.rotation.order = 'YXZ'; // Устанавливаем порядок вращений, если необходимо
return sword;
}
function shoot() {
if (isReloading || !canShoot) {
return; // Нельзя стрелять во время перезарядки или если предыдущий выстрел еще не завершен
}
canShoot = false; // Блокируем возможность стрельбы до следующего кадра
if (currentAmmo > 0) {
// Уменьшаем количество патронов
currentAmmo--;
updateHUD();
// Создаем и добавляем пулю
const bulletGeometry = new THREE.SphereGeometry(0.1, 8, 8);
const bulletMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 });
const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
const cameraWorldPosition = new THREE.Vector3();
camera.getWorldPosition(cameraWorldPosition);
const cameraWorldDirection = new THREE.Vector3();
camera.getWorldDirection(cameraWorldDirection);
bullet.position.copy(cameraWorldPosition).add(cameraWorldDirection.multiplyScalar(1));
bullet.userData.direction = cameraWorldDirection.clone();
scene.add(bullet);
bullets.push(bullet);
// Показываем дульную вспышку
muzzleFlash.visible = true;
muzzleFlash.material.opacity = 1;
const currentTime = performance.now() / 1000; // Время в секундах
muzzleFlashEndTime = currentTime + 0.05; // Отображать вспышку в течение 0.05 секунд
} else {
// Патроны закончились, инициируем перезарядку
reload();
}
// Разрешаем стрельбу снова в следующем кадре
setTimeout(() => {
canShoot = true;
}, 0);
}
function initScene() {
// Создаем сцену, камеру и рендерер
scene = new THREE.Scene();
scene.background = new THREE.Color(0x87CEEB);
camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// Добавляем плоскость (землю)
const floorGeometry = new THREE.PlaneGeometry(200, 200);
const floorMaterial = new THREE.MeshBasicMaterial({ color: 0x228B22 });
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
floor.rotation.x = -Math.PI / 2;
scene.add(floor);
// Создаем несколько деревьев
for (let i = 0; i < 20; i++) {
const tree = createTree();
tree.position.set(
(Math.random() - 0.5) * 100,
0,
(Math.random() - 0.5) * 100
);
scene.add(tree);
trees.push(tree);
}
// Создаем игрока
player = new THREE.Object3D();
player.position.set(0, 1.6, 0);
scene.add(player);
player.add(camera);
// Создаем группу для пистолета
gunGroup = new THREE.Group();
// Создаем основную часть пистолета
const gunGeometry = new THREE.BoxGeometry(0.1, 0.1, 0.5);
const gunMaterial = new THREE.MeshBasicMaterial({ color: 0x333333 });
// Инициализируем глобальную переменную gunBody
gunBody = new THREE.Mesh(gunGeometry, gunMaterial);
gunBody.position.set(0, 0, 0);
gunGroup.add(gunBody);
// Создаем рукоятку
const handleGeometry = new THREE.BoxGeometry(0.05, 0.2, 0.1);
const handleMaterial = new THREE.MeshBasicMaterial({ color: 0x111111 });
const handle = new THREE.Mesh(handleGeometry, handleMaterial);
handle.position.set(0, -0.15, -0.2);
gunGroup.add(handle);
// Создаем обойму
const magazineGeometry = new THREE.BoxGeometry(0.04, 0.1, 0.1);
const magazineMaterial = new THREE.MeshBasicMaterial({ color: 0x555555 });
magazine = new THREE.Mesh(magazineGeometry, magazineMaterial);
magazine.position.set(0, -0.25, -0.2);
gunGroup.add(magazine);
// Добавляем дульную вспышку
const muzzleFlashGeometry = new THREE.ConeGeometry(0.1, 0.2, 4);
const muzzleFlashMaterial = new THREE.MeshBasicMaterial({
color: 0xffaa00,
transparent: true, // Позволяет изменять прозрачность
});
muzzleFlash = new THREE.Mesh(muzzleFlashGeometry, muzzleFlashMaterial);
muzzleFlash.position.set(0, 0, -0.25); // Позиционирование вспышки на конце дула
muzzleFlash.rotation.x = Math.PI / 2; // Поворот вспышки вперед
muzzleFlash.visible = false; // Скрываем вспышку по умолчанию
gunBody.add(muzzleFlash);
// Добавляем вызов функции для обновления скина пистолета
updateGunSkin();
// Добавляем группу пистолета к камере
gunGroup.position.set(0.2, -0.2, -0.5);
camera.add(gunGroup);
}
// Обработчики событий клавиатуры
let moveForward = false;
let moveBackward = false;
let moveLeft = false;
let moveRight = false;
const velocity = new THREE.Vector3();
const direction = new THREE.Vector3();
document.addEventListener('keydown', function(event) {
if (!isGameActive) return; // Если игра не активна, игнорируем нажатия клавиш
switch (event.code) {
case 'KeyW':
moveForward = true;
break;
case 'KeyS':
moveBackward = true;
break;
case 'KeyA':
moveLeft = true;
break;
case 'KeyD':
moveRight = true;
break;
case 'KeyR':
reload();
break;
}
});
document.addEventListener('keyup', function(event) {
if (!isGameActive) return; // Если игра не активна, игнорируем отпускания клавиш
switch (event.code) {
case 'KeyW':
moveForward = false;
break;
case 'KeyS':
moveBackward = false;
break;
case 'KeyA':
moveLeft = false;
break;
case 'KeyD':
moveRight = false;
break;
}
});
// Стрельба по клику мыши
document.addEventListener('mousedown', function(event) {
if (isGameActive && isLocked && event.button === 0) {
shoot();
}
});
// Управление камерой с помощью мыши
let isLocked = false;
const pointerLockChange = function () {
isLocked = document.pointerLockElement === document.body;
};
document.addEventListener('pointerlockchange', pointerLockChange, false);
document.addEventListener('click', function () {
if (!isLocked) {
document.body.requestPointerLock();
}
}, false);
document.addEventListener('mousemove', function (event) {
if (isLocked && isGameActive) {
const movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0;
const movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0;
player.rotation.y -= movementX * 0.002;
camera.rotation.x -= movementY * 0.002;
camera.rotation.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, camera.rotation.x));
}
}, false);
// Анимация
let prevTime = performance.now();
function animate() {
requestAnimationFrame(animate);
const time = performance.now();
const delta = (time - prevTime) / 1000;
// Сохраняем предыдущую позицию игрока перед перемещением
const prevPosition = player.position.clone();
// Обновляем скорость и направление движения игрока
velocity.x -= velocity.x * 10.0 * delta;
velocity.z -= velocity.z * 10.0 * delta;
direction.z = Number(moveForward) - Number(moveBackward);
direction.x = Number(moveLeft) - Number(moveRight);
direction.normalize();
if (moveForward || moveBackward) velocity.z -= direction.z * 150.0 * delta;
if (moveLeft || moveRight) velocity.x -= direction.x * 150.0 * delta;
// Перемещаем игрока
player.translateX(velocity.x * delta);
player.translateZ(velocity.z * delta);
// Обработка столкновений с деревьями
const playerCollider = new THREE.Sphere(player.position.clone(), 0.5);
let collisionDetected = false;
for (let i = 0; i < trees.length; i++) {
const tree = trees[i];
const treeBox = new THREE.Box3().setFromObject(tree);
if (treeBox.intersectsSphere(playerCollider)) {
collisionDetected = true;
break;
}
}
if (collisionDetected) {
player.position.copy(prevPosition);
}
// Обновляем положение пуль и проверяем столкновения
if (isGameActive) {
for (let i = bullets.length - 1; i >= 0; i--) {
const bullet = bullets[i];
bullet.position.add(bullet.userData.direction.clone().multiplyScalar(bulletSpeed));
// Проверяем столкновение пули с врагом
for (let j = enemies.length - 1; j >= 0; j--) {
const enemy = enemies[j];
if (bullet.position.distanceTo(enemy.position) < 1) {
scene.remove(enemy);
enemies.splice(j, 1);
scene.remove(bullet);
bullets.splice(i, 1);
kills++;
updateHUD();
if (isGameActive && enemies.length < maxEnemies) {
setTimeout(createEnemy, 2000);
}
break;
}
}
if (bullet.position.length() > 200) {
scene.remove(bullet);
bullets.splice(i, 1);
}
}
}
// Обновляем положение врагов и обрабатываем их поведение
if (isGameActive) {
const currentTime = new Date().getTime();
const elapsedTimeSinceLastIncrease = currentTime - lastDifficultyIncreaseTime;
// Проверяем, прошло ли 2 минуты с последнего увеличения сложности
if (elapsedTimeSinceLastIncrease >= 2 * 60 * 1000) { // 2 минуты в миллисекундах
// Увеличиваем скорость врагов на 20%
enemySpeed *= 1.2;
// Увеличиваем максимальное количество врагов на 1
maxEnemies += 1;
// Ограничиваем максимальную скорость и количество врагов, если необходимо
if (enemySpeed > 0.3) enemySpeed = 0.3; // Максимальная скорость
if (maxEnemies > 20) maxEnemies = 20; // Максимальное количество врагов
// Обновляем время последнего увеличения сложности
lastDifficultyIncreaseTime = currentTime;
// Создаем дополнительных врагов, если их меньше, чем maxEnemies
while (enemies.length < maxEnemies) {
createEnemy();
}
}
enemies.forEach(function(enemy) {
const prevEnemyPosition = enemy.position.clone();
// Вычисляем направление к игроку
let enemyDirection = new THREE.Vector3();
enemyDirection.subVectors(player.position.clone().setY(0), enemy.position.clone().setY(0));
enemyDirection.normalize();
let avoidDirection = new THREE.Vector3(0, 0, 0);
let isObstacleDetected = false;
const enemyCollider = new THREE.Sphere(enemy.position.clone(), 0.5);
trees.forEach(function(tree) {
const treeBox = new THREE.Box3().setFromObject(tree);
if (treeBox.intersectsSphere(enemyCollider)) {
enemy.position.copy(prevEnemyPosition);
}
const distance = enemy.position.distanceTo(tree.position);
if (distance < 3) {
let f = new THREE.Vector3();
f.subVectors(enemy.position, tree.position);
f.y = 0;
f.normalize();
f.divideScalar(distance);
avoidDirection.add(f);
isObstacleDetected = true;
}
});
if (isObstacleDetected) {
avoidDirection.normalize();
enemyDirection.add(avoidDirection);
enemyDirection.normalize();
}
// Перемещаем врага с учетом текущей скорости
enemy.position.addScaledVector(enemyDirection, enemySpeed);
enemy.position.y = 1; // Фиксируем высоту врага
// Поворачиваем визуальную модель врага к камере
enemy.traverse(function(child) {
if (child.userData.isEnemyMesh) {
const enemyPositionWorld = new THREE.Vector3();
child.getWorldPosition(enemyPositionWorld);
const cameraPositionWorld = new THREE.Vector3();
camera.getWorldPosition(cameraPositionWorld);
const dx = cameraPositionWorld.x - enemyPositionWorld.x;
const dz = cameraPositionWorld.z - enemyPositionWorld.z;
const angle = Math.atan2(dx, dz);
child.rotation.set(0, angle, 0);
}
});
// Отключаем вращение меча
enemy.traverse(function(child) {
if (child.userData.isSword) {
child.rotation.copy(child.userData.initialRotation);
}
});
// Проверка столкновения с игроком
if (enemy.position.distanceTo(player.position) < 1) {
takeDamage(10);
}
});
}
// Обновляем информацию на экране
updateHUD();
// Анимация дульной вспышки
const currentTimeSec = time / 1000; // Время в секундах
if (currentTimeSec >= muzzleFlashEndTime) {
muzzleFlash.visible = false;
} else {
muzzleFlash.visible = true;
// Вычисляем оставшееся время для эффекта затухания
const remainingTime = muzzleFlashEndTime - currentTimeSec;
muzzleFlash.material.opacity = remainingTime / 0.05;
}
// Анимация перезарядки
if (isAnimatingReload) {
reloadAnimationProgress += delta;
const halfDuration = reloadAnimationDuration / 2;
if (reloadAnimationProgress <= halfDuration) {
// Первая половина анимации: обойма выходит вниз
const t = reloadAnimationProgress / halfDuration;
magazine.position.y = THREE.MathUtils.lerp(-0.25, -0.5, t);
} else if (reloadAnimationProgress <= reloadAnimationDuration) {
// Вторая половина анимации: обойма возвращается вверх
const t = (reloadAnimationProgress - halfDuration) / halfDuration;
magazine.position.y = THREE.MathUtils.lerp(-0.5, -0.25, t);
} else {
// Анимация завершена
magazine.position.y = -0.25;
isAnimatingReload = false;
currentAmmo = magazineSize;
isReloading = false;
updateHUD();
}
}
// Рендерим сцену
renderer.render(scene, camera);
prevTime = time;
}
// Функция для начала игры
function startGame() {
const menu = document.getElementById('menu');
menu.style.display = 'none';
// Скрываем модальное окно, если оно было показано
const deathModal = document.getElementById('death-modal');
deathModal.style.display = 'none';
// Показываем HUD, health-bar и crosshair
document.getElementById('hud').style.display = 'block';
document.getElementById('health-bar').style.display = 'block';
document.getElementById('crosshair').style.display = 'block';
// Сбрасываем счетчик убийств и здоровья
kills = 0;
health = 100;
updateHealthBar();
updateHUD();
// Сбрасываем количество патронов и состояние перезарядки
currentAmmo = magazineSize;
isReloading = false;
isAnimatingReload = false;
reloadAnimationProgress = 0;
updateHUD(); // Обновляем HUD
// Сбрасываем позицию и поворот игрока и камеры
player.position.copy(getRandomSpawnPosition());
player.rotation.set(0, 0, 0);
camera.rotation.set(0, 0, 0);
// Сбрасываем скорость игрока
velocity.x = 0;
velocity.y = 0;
velocity.z = 0;
// Устанавливаем время начала игры
gameStartTime = new Date().getTime();
// Устанавливаем флаг активности игры
isGameActive = true;
// Инициализируем скорость врагов, максимальное количество и время последнего увеличения сложности
enemySpeed = 0.05; // Начальная скорость врагов
maxEnemies = 5; // Начальное максимальное количество врагов
lastDifficultyIncreaseTime = gameStartTime; // Время последнего увеличения сложности
// Создаем начальных врагов
for (let i = 0; i < maxEnemies; i++) {
createEnemy();
}
// Запрашиваем блокировку указателя мыши
document.body.requestPointerLock();
}
// Функция для завершения игры
function endGame() {
isGameActive = false; // Останавливаем обновление игры
// Очищаем сцену от врагов и пуль
enemies.forEach(enemy => scene.remove(enemy));
enemies.length = 0;
bullets.forEach(bullet => scene.remove(bullet));
bullets.length = 0;
// Освобождаем указатель мыши
document.exitPointerLock();
// Проверяем и обновляем рекорд
if (kills > killsRecord) {
killsRecord = kills;
saveProgress();
}
// Разблокировка золотого пистолета при достижении 1000 киллов
if (killsRecord >= 1000) {
skins.golden.unlocked = true;
saveSkins();
}
// Обновляем меню
updateMenu();
}
const restartButton = document.getElementById('restartButton');
const returnToMenuButton = document.getElementById('returnToMenuButton');
restartButton.addEventListener('click', function(event) {
event.stopPropagation();
const deathModal = document.getElementById('death-modal');
deathModal.style.display = 'none';
// Сбрасываем параметры игры и начинаем заново
startGame();
});
returnToMenuButton.addEventListener('click', function(event) {
event.stopPropagation();
const deathModal = document.getElementById('death-modal');
deathModal.style.display = 'none';
// Перезагружаем страницу
location.reload();
});
// Обработчик кнопки "Играть"
const playButton = document.getElementById('playButton');
playButton.addEventListener('click', function(event) {
event.stopPropagation();
startGame();
});
// Изменение размера окна
window.addEventListener('resize', function() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}, false);
// Инициализация игры
loadProgress();
loadSkins();
initScene(); // Перенесено перед updateGunSkin()
updateGunSkin(); // Теперь вызывается после initScene()
updateMenu();
updateHealthBar();
updateHUD();
// Запускаем анимацию один раз при загрузке страницы
animate();
</script>
<div id="version">Версия 0.0.0.3</div>
</body>
</html>
сделай когда заходишь за горизонт карты ну где кончается полотно начинает генерироваться новая такая же с деревьями и врагамиASSISTANT
Чтобы реализовать бесшовное расширение карты при переходе игрока за границы текущего полотна, мы можем использовать систему тайлов (плиток). Идея заключается в том, чтобы разбить мир на сетку тайлов и генерировать новые тайлы по мере приближения игрока к краю текущего тайла.
Ниже я предоставлю обновленный код с необходимыми изменениями и пояснениями к ним.
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Кибершамыга</title>
<style>
body { margin: 0; overflow: hidden; font-family: Arial, sans-serif; }
canvas { display: block; }
#menu {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: white;
}
#menu h1 {
font-size: 4em;
margin-bottom: 30px;
}
#playButton {
font-size: 1.5em;
padding: 10px 20px;
cursor: pointer;
}
#hud, #health-bar, #crosshair {
display: none;
}
#hud {
position: absolute;
top: 10px;
left: 10px;
color: white;
font-size: 18px;
}
#health-bar {
position: absolute;
bottom: 10px;
left: 10px;
width: 200px;
height: 20px;
background-color: red;
}
#health {
height: 100%;
background-color: green;
width: 100%;
text-align: center;
color: white;
font-weight: bold;
line-height: 20px;
}
#crosshair {
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 20px;
background-color: transparent;
border: 2px solid red;
border-radius: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
}
#death-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
display: none;
justify-content: center;
align-items: center;
}
#death-modal-content {
background-color: #333;
padding: 40px;
border-radius: 10px;
text-align: center;
color: white;
}
#death-modal-content h2 {
margin-bottom: 30px;
}
#death-modal-content button {
font-size: 1.2em;
padding: 10px 30px;
margin: 10px;
cursor: pointer;
}
#skins-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
display: none;
justify-content: center;
align-items: center;
}
#skins-modal-content {
background-color: #333;
padding: 40px;
border-radius: 10px;
text-align: center;
color: white;
}
.skin-item {
margin-bottom: 10px;
}
.skin-item span {
margin-right: 10px;
}
#version {
position: fixed;
bottom: 10px;
right: 10px;
color: white;
font-size: 14px;
}
</style>
</head>
<body>
<!-- Меню -->
<div id="menu">
<h1>Кибершамыга</h1>
<button id="playButton">Играть</button>
<span id="previous-kills"> Kills record: <span id="kills-display">0</span></span>
<button id="skinsButton">Скины</button>
</div>
<div id="skins-modal">
<div id="skins-modal-content">
<h2>Выбор скинов</h2>
<div id="skins-list">
<!-- Здесь будут отображаться скины -->
</div>
<button id="closeSkinsButton">Закрыть</button>
</div>
</div>
<div id="death-modal">
<div id="death-modal-content">
<h2>Вы погибли!</h2>
<button id="restartButton">Начать заново</button>
<button id="returnToMenuButton">Вернуться в меню</button>
</div>
</div>
<!-- HUD (Heads-Up Display) -->
<div id="hud">
<div>Время: <span id="time">00:00</span></div>
<div>Киллы: <span id="kills">0</span></div>
<div>Патроны: <span id="ammo">10/∞</span></div>
</div>
<!-- Полоса здоровья -->
<div id="health-bar">
<div id="health">100%</div>
</div>
<!-- Прицел -->
<div id="crosshair"></div>
<!-- Подключаем библиотеку Three.js с CDN -->
<script src="https://cdn.jsdelivr.net/npm/three@0.148/build/three.min.js"></script>
<script>
let scene, camera, renderer, player;
let health = 100;
let bulletSpeed = 0.5;
let gameStartTime;
let kills = 0;
let killsRecord = 0;
let muzzleFlash;
let muzzleFlashDuration = 0;
let muzzleFlashEndTime = 0;
let magazineSize = 10;
let currentAmmo = magazineSize;
let isReloading = false;
let canShoot = true;
let isGameActive = false;
let skins = {
default: {
name: "Стандартный",
unlocked: true,
model: null
},
golden: {
name: "Золотой",
unlocked: false,
model: null
}
};
let selectedSkin = 'default';
let reloadAnimationProgress = 0;
let isAnimatingReload = false;
const reloadAnimationDuration = 1.5;
const enemies = [];
let enemySpeed = 0.05;
let maxEnemies = 5;
let lastDifficultyIncreaseTime;
const bullets = [];
const tileSize = 200; // Размер одного тайла
const tiles = {}; // Объект для хранения созданных тайлов
const tileMeshes = {}; // Объект для хранения мешей тайлов
const trees = [];
const treeMeshes = {}; // Объект для хранения мешей деревьев для каждого тайла
const skinsButton = document.getElementById('skinsButton');
const skinsModal = document.getElementById('skins-modal');
const closeSkinsButton = document.getElementById('closeSkinsButton');
skinsButton.addEventListener('click', function(event) {
event.stopPropagation();
skinsModal.style.display = 'flex';
});
closeSkinsButton.addEventListener('click', function(event) {
event.stopPropagation();
skinsModal.style.display = 'none';
});
let gunGroup, gunBody, magazine;
function updateGunSkin() {
if (selectedSkin === 'golden') {
gunBody.material.color.set(0xFFD700);
} else {
gunBody.material.color.set(0x333333);
}
}
function updateSkinsMenu() {
const skinsList = document.getElementById('skins-list');
skinsList.innerHTML = '';
for (let key in skins) {
let skin = skins[key];
let skinDiv = document.createElement('div');
skinDiv.className = 'skin-item';
let skinName = document.createElement('span');
skinName.textContent = skin.name;
let selectButton = document.createElement('button');
selectButton.textContent = 'Выбрать';
selectButton.disabled = !skin.unlocked;
selectButton.addEventListener('click', function () {
selectedSkin = key;
skinsModal.style.display = 'none';
updateGunSkin();
});
if (!skin.unlocked) {
let lockSpan = document.createElement('span');
lockSpan.textContent = '🔒 Требуется 1000 киллов';
skinDiv.appendChild(lockSpan);
}
skinDiv.appendChild(skinName);
skinDiv.appendChild(selectButton);
skinsList.appendChild(skinDiv);
}
}
function saveSkins() {
localStorage.setItem('skins', JSON.stringify({
goldenUnlocked: skins.golden.unlocked
}));
}
function loadSkins() {
let storedSkins = localStorage.getItem('skins');
if (storedSkins) {
let data = JSON.parse(storedSkins);
skins.golden.unlocked = data.goldenUnlocked || false;
}
}
loadSkins();
function getRandomSpawnPosition() {
let position;
let isPositionValid = false;
while (!isPositionValid) {
position = new THREE.Vector3(
(Math.random() - 0.5) * tileSize,
1.6,
(Math.random() - 0.5) * tileSize
);
isPositionValid = true;
// Проверяем, что позиция не слишком близка к деревьям
for (let i = 0; i < trees.length; i++) {
const tree = trees[i];
const distance = position.distanceTo(tree.position);
if (distance < 5) {
isPositionValid = false;
break;
}
}
}
return position;
}
function reload() {
if (isReloading || currentAmmo === magazineSize) {
return;
}
isReloading = true;
isAnimatingReload = true;
reloadAnimationProgress = 0;
updateHUD();
}
function showDeathModal() {
const deathModal = document.getElementById('death-modal');
deathModal.style.display = 'flex';
}
function saveProgress() {
localStorage.setItem('killsRecord', killsRecord);
}
function loadProgress() {
const storedKillsRecord = localStorage.getItem('killsRecord');
if (storedKillsRecord) {
killsRecord = parseInt(storedKillsRecord, 10);
}
}
function updateMenu() {
const killsDisplay = document.getElementById('kills-display');
killsDisplay.textContent = killsRecord;
// Обновляем список скинов
updateSkinsMenu();
}
loadProgress();
updateMenu();
function updateHUD() {
const timeElement = document.getElementById('time');
const killsElement = document.getElementById('kills');
const ammoElement = document.getElementById('ammo');
const currentTime = new Date().getTime();
const elapsedTime = Math.floor((currentTime - gameStartTime) / 1000);
const minutes = Math.floor(elapsedTime / 60);
const seconds = elapsedTime % 60;
timeElement.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
killsElement.textContent = kills;
ammoElement.textContent = isReloading ? 'Перезарядка...' : `${currentAmmo}/${String.fromCharCode(8734)}`;
}
function updateHealthBar() {
const healthBar = document.getElementById('health');
healthBar.style.width = health + '%';
healthBar.textContent = health + '%';
}
function takeDamage(amount) {
health -= amount;
if (health <= 0) {
health = 0;
updateHealthBar();
endGame();
showDeathModal();
} else {
updateHealthBar();
}
}
function createTree(x, z) {
const trunkGeometry = new THREE.CylinderGeometry(0.2, 0.2, 2);
const trunkMaterial = new THREE.MeshBasicMaterial({ color: 0x8B4513 });
const trunk = new THREE.Mesh(trunkGeometry, trunkMaterial);
const leavesGeometry = new THREE.ConeGeometry(1, 2, 8);
const leavesMaterial = new THREE.MeshBasicMaterial({ color: 0x228B22 });
const leaves = new THREE.Mesh(leavesGeometry, leavesMaterial);
leaves.position.y = 2;
const tree = new THREE.Group();
tree.add(trunk);
tree.add(leaves);
tree.position.set(x, 0, z);
return tree;
}
function createEnemy() {
if (!isGameActive) return;
if (enemies.length >= maxEnemies) return;
const enemy = new THREE.Object3D();
enemy.position.y = 1;
const enemyTexture = new THREE.TextureLoader().load('https://i.postimg.cc/QC6j7xGC/photo-2024-05-28-17-49-00.jpg');
const enemyMaterial = new THREE.MeshBasicMaterial({
map: enemyTexture,
transparent: true,
side: THREE.DoubleSide
});
const enemyGeometry = new THREE.PlaneGeometry(2, 2);
const enemyMesh = new THREE.Mesh(enemyGeometry, enemyMaterial);
enemyMesh.userData.isEnemyMesh = true;
enemy.add(enemyMesh);
const sword = createSword();
sword.position.set(0.5, -0.5, 0);
sword.userData.initialRotation = sword.rotation.clone();
sword.userData.isSword = true;
enemyMesh.add(sword);
// Устанавливаем начальную позицию врага
let enemyPosition;
let attempts = 0;
do {
const currentTileX = Math.floor(player.position.x / tileSize);
const currentTileZ = Math.floor(player.position.z / tileSize);
const tileKey = `${currentTileX},${currentTileZ}`;
const minX = currentTileX * tileSize - tileSize;
const maxX = (currentTileX + 1) * tileSize + tileSize;
const minZ = currentTileZ * tileSize - tileSize;
const maxZ = (currentTileZ + 1) * tileSize + tileSize;
enemyPosition = new THREE.Vector3(
Math.random() * (maxX - minX) + minX,
1,
Math.random() * (maxZ - minZ) + minZ
);
attempts++;
if (attempts > 10) break; // Избегаем бесконечного цикла
} while (enemyPosition.distanceTo(player.position) < 10);
enemy.position.copy(enemyPosition);
scene.add(enemy);
enemies.push(enemy);
}
function createSword() {
const bladeGeometry = new THREE.BoxGeometry(0.05, 1, 0.2);
const bladeMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
const blade = new THREE.Mesh(bladeGeometry, bladeMaterial);
blade.position.set(0, 0.5, 0);
const hiltGeometry = new THREE.CylinderGeometry(0.05, 0.05, 0.3);
const hiltMaterial = new THREE.MeshBasicMaterial({ color: 0x8B4513 });
const hilt = new THREE.Mesh(hiltGeometry, hiltMaterial);
hilt.position.set(0, -0.15, 0);
const sword = new THREE.Group();
sword.add(blade);
sword.add(hilt);
sword.rotation.order = 'YXZ';
return sword;
}
function shoot() {
if (isReloading || !canShoot) {
return;
}
canShoot = false;
if (currentAmmo > 0) {
currentAmmo--;
updateHUD();
const bulletGeometry = new THREE.SphereGeometry(0.1, 8, 8);
const bulletMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 });
const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
const cameraWorldPosition = new THREE.Vector3();
camera.getWorldPosition(cameraWorldPosition);
const cameraWorldDirection = new THREE.Vector3();
camera.getWorldDirection(cameraWorldDirection);
bullet.position.copy(cameraWorldPosition).add(cameraWorldDirection.multiplyScalar(1));
bullet.userData.direction = cameraWorldDirection.clone();
scene.add(bullet);
bullets.push(bullet);
// Показываем дульную вспышку
muzzleFlash.visible = true;
muzzleFlash.material.opacity = 1;
const currentTime = performance.now() / 1000;
muzzleFlashEndTime = currentTime + 0.05;
} else {
reload();
}
setTimeout(() => {
canShoot = true;
}, 0);
}
function initScene() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x87CEEB);
camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// Инициализируем тайлы карты
generateTilesAroundPlayer();
// Создаем игрока
player = new THREE.Object3D();
player.position.set(0, 1.6, 0);
scene.add(player);
player.add(camera);
// Создаем группу для пистолета
gunGroup = new THREE.Group();
const gunGeometry = new THREE.BoxGeometry(0.1, 0.1, 0.5);
const gunMaterial = new THREE.MeshBasicMaterial({ color: 0x333333 });
gunBody = new THREE.Mesh(gunGeometry, gunMaterial);
gunBody.position.set(0, 0, 0);
gunGroup.add(gunBody);
const handleGeometry = new THREE.BoxGeometry(0.05, 0.2, 0.1);
const handleMaterial = new THREE.MeshBasicMaterial({ color: 0x111111 });
const handle = new THREE.Mesh(handleGeometry, handleMaterial);
handle.position.set(0, -0.15, -0.2);
gunGroup.add(handle);
const magazineGeometry = new THREE.BoxGeometry(0.04, 0.1, 0.1);
const magazineMaterial = new THREE.MeshBasicMaterial({ color: 0x555555 });
magazine = new THREE.Mesh(magazineGeometry, magazineMaterial);
magazine.position.set(0, -0.25, -0.2);
gunGroup.add(magazine);
const muzzleFlashGeometry = new THREE.ConeGeometry(0.1, 0.2, 4);
const muzzleFlashMaterial = new THREE.MeshBasicMaterial({
color: 0xffaa00,
transparent: true,
});
muzzleFlash = new THREE.Mesh(muzzleFlashGeometry, muzzleFlashMaterial);
muzzleFlash.position.set(0, 0, -0.25);
muzzleFlash.rotation.x = Math.PI / 2;
muzzleFlash.visible = false;
gunBody.add(muzzleFlash);
updateGunSkin();
gunGroup.position.set(0.2, -0.2, -0.5);
camera.add(gunGroup);
}
// Обработчики событий клавиатуры
let moveForward = false;
let moveBackward = false;
let moveLeft = false;
let moveRight = false;
const velocity = new THREE.Vector3();
const direction = new THREE.Vector3();
document.addEventListener('keydown', function(event) {
if (!isGameActive) return;
switch (event.code) {
case 'KeyW':
moveForward = true;
break;
case 'KeyS':
moveBackward = true;
break;
case 'KeyA':
moveLeft = true;
break;
case 'KeyD':
moveRight = true;
break;
case 'KeyR':
reload();
break;
}
});
document.addEventListener('keyup', function(event) {
if (!isGameActive) return;
switch (event.code) {
case 'KeyW':
moveForward = false;
break;
case 'KeyS':
moveBackward = false;
break;
case 'KeyA':
moveLeft = false;
break;
case 'KeyD':
moveRight = false;
break;
}
});
document.addEventListener('mousedown', function(event) {
if (isGameActive && isLocked && event.button === 0) {
shoot();
}
});
let isLocked = false;
const pointerLockChange = function () {
isLocked = document.pointerLockElement === document.body;
};
document.addEventListener('pointerlockchange', pointerLockChange, false);
document.addEventListener('click', function () {
if (!isLocked) {
document.body.requestPointerLock();
}
}, false);
document.addEventListener('mousemove', function (event) {
if (isLocked && isGameActive) {
const movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0;
const movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0;
player.rotation.y -= movementX * 0.002;
camera.rotation.x -= movementY * 0.002;
camera.rotation.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, camera.rotation.x));
}
}, false);
let prevTime = performance.now();
function animate() {
requestAnimationFrame(animate);
const time = performance.now();
const delta = (time - prevTime) / 1000;
const prevPosition = player.position.clone();
velocity.x -= velocity.x * 10.0 * delta;
velocity.z -= velocity.z * 10.0 * delta;
direction.z = Number(moveForward) - Number(moveBackward);
direction.x = Number(moveLeft) - Number(moveRight);
direction.normalize();
if (moveForward || moveBackward) velocity.z -= direction.z * 150.0 * delta;
if (moveLeft || moveRight) velocity.x -= direction.x * 150.0 * delta;
player.translateX(velocity.x * delta);
player.translateZ(velocity.z * delta);
// Проверяем, нужно ли генерировать новые тайлы
if (isGameActive) {
generateTilesAroundPlayer();
}
// Обработка столкновений с деревьями
const playerCollider = new THREE.Sphere(player.position.clone(), 0.5);
let collisionDetected = false;
for (let i = 0; i < trees.length; i++) {
const tree = trees[i];
const treeBox = new THREE.Box3().setFromObject(tree);
if (treeBox.intersectsSphere(playerCollider)) {
collisionDetected = true;
break;
}
}
if (collisionDetected) {
player.position.copy(prevPosition);
}
// Обновляем положение пуль и проверяем столкновения
if (isGameActive) {
for (let i = bullets.length - 1; i >= 0; i--) {
const bullet = bullets[i];
bullet.position.add(bullet.userData.direction.clone().multiplyScalar(bulletSpeed));
// Проверяем столкновение пули с врагом
for (let j = enemies.length - 1; j >= 0; j--) {
const enemy = enemies[j];
if (bullet.position.distanceTo(enemy.position) < 1) {
scene.remove(enemy);
enemies.splice(j, 1);
scene.remove(bullet);
bullets.splice(i, 1);
kills++;
updateHUD();
if (isGameActive && enemies.length < maxEnemies) {
setTimeout(createEnemy, 2000);
}
break;
}
}
if (bullet.position.length() > 2000) {
scene.remove(bullet);
bullets.splice(i, 1);
}
}
}
// Обновляем положение врагов и обрабатываем их поведение
if (isGameActive) {
const currentTime = new Date().getTime();
const elapsedTimeSinceLastIncrease = currentTime - lastDifficultyIncreaseTime;
if (elapsedTimeSinceLastIncrease >= 2 * 60 * 1000) {
enemySpeed *= 1.2;
maxEnemies += 1;
if (enemySpeed > 0.3) enemySpeed = 0.3;
if (maxEnemies > 20) maxEnemies = 20;
lastDifficultyIncreaseTime = currentTime;
while (enemies.length < maxEnemies) {
createEnemy();
}
}
enemies.forEach(function(enemy) {
const prevEnemyPosition = enemy.position.clone();
let enemyDirection = new THREE.Vector3();
enemyDirection.subVectors(player.position.clone().setY(0), enemy.position.clone().setY(0));
enemyDirection.normalize();
let avoidDirection = new THREE.Vector3(0, 0, 0);
let isObstacleDetected = false;
const enemyCollider = new THREE.Sphere(enemy.position.clone(), 0.5);
trees.forEach(function(tree) {
const treeBox = new THREE.Box3().setFromObject(tree);
if (treeBox.intersectsSphere(enemyCollider)) {
enemy.position.copy(prevEnemyPosition);
}
const distance = enemy.position.distanceTo(tree.position);
if (distance < 3) {
let f = new THREE.Vector3();
f.subVectors(enemy.position, tree.position);
f.y = 0;
f.normalize();
f.divideScalar(distance);
avoidDirection.add(f);
isObstacleDetected = true;
}
});
if (isObstacleDetected) {
avoidDirection.normalize();
enemyDirection.add(avoidDirection);
enemyDirection.normalize();
}
enemy.position.addScaledVector(enemyDirection, enemySpeed);
enemy.position.y = 1;
enemy.traverse(function(child) {
if (child.userData.isEnemyMesh) {
const enemyPositionWorld = new THREE.Vector3();
child.getWorldPosition(enemyPositionWorld);
const cameraPositionWorld = new THREE.Vector3();
camera.getWorldPosition(cameraPositionWorld);
const dx = cameraPositionWorld.x - enemyPositionWorld.x;
const dz = cameraPositionWorld.z - enemyPositionWorld.z;
const angle = Math.atan2(dx, dz);
child.rotation.set(0, angle, 0);
}
});
enemy.traverse(function(child) {
if (child.userData.isSword) {
child.rotation.copy(child.userData.initialRotation);
}
});
if (enemy.position.distanceTo(player.position) < 1) {
takeDamage(10);
}
});
}
updateHUD();
const currentTimeSec = time / 1000;
if (currentTimeSec >= muzzleFlashEndTime) {
muzzleFlash.visible = false;
} else {
muzzleFlash.visible = true;
const remainingTime = muzzleFlashEndTime - currentTimeSec;
muzzleFlash.material.opacity = remainingTime / 0.05;
}
if (isAnimatingReload) {
reloadAnimationProgress += delta;
const halfDuration = reloadAnimationDuration / 2;
if (reloadAnimationProgress <= halfDuration) {
const t = reloadAnimationProgress / halfDuration;
magazine.position.y = THREE.MathUtils.lerp(-0.25, -0.5, t);
} else if (reloadAnimationProgress <= reloadAnimationDuration) {
const t = (reloadAnimationProgress - halfDuration) / halfDuration;
magazine.position.y = THREE.MathUtils.lerp(-0.5, -0.25, t);
} else {
magazine.position.y = -0.25;
isAnimatingReload = false;
currentAmmo = magazineSize;
isReloading = false;
updateHUD();
}
}
renderer.render(scene, camera);
prevTime = time;
}
// Функция для генерации тайлов вокруг игрока
function generateTilesAroundPlayer() {
const playerTileX = Math.floor(player.position.x / tileSize);
const playerTileZ = Math.floor(player.position.z / tileSize);
const range = 1; // Радиус генерации тайлов вокруг игрока
for (let x = playerTileX - range; x <= playerTileX + range; x++) {
for (let z = playerTileZ - range; z <= playerTileZ + range; z++) {
const tileKey = `${x},${z}`;
if (!tiles[tileKey]) {
createTile(x, z);
}
}
}
// Удаляем тайлы, которые находятся слишком далеко
for (let key in tiles) {
const [tileX, tileZ] = key.split(',').map(Number);
if (Math.abs(tileX - playerTileX) > range + 1 || Math.abs(tileZ - playerTileZ) > range + 1) {
removeTile(tileX, tileZ);
}
}
}
// Функция для создания тайла
function createTile(x, z) {
const tileGeometry = new THREE.PlaneGeometry(tileSize, tileSize);
const tileMaterial = new THREE.MeshBasicMaterial({ color: 0x228B22 });
const tile = new THREE.Mesh(tileGeometry, tileMaterial);
tile.rotation.x = -Math.PI / 2;
tile.position.set(x * tileSize + tileSize / 2, 0, z * tileSize + tileSize / 2);
scene.add(tile);
tileMeshes[`${x},${z}`] = tile;
tiles[`${x},${z}`] = true;
// Создаем деревья на этом тайле
const numTrees = 20; // Количество деревьев на тайле
const treeMeshArray = [];
for (let i = 0; i < numTrees; i++) {
const treeX = x * tileSize + (Math.random() * tileSize);
const treeZ = z * tileSize + (Math.random() * tileSize);
const tree = createTree(treeX, treeZ);
scene.add(tree);
trees.push(tree);
treeMeshArray.push(tree);
}
treeMeshes[`${x},${z}`] = treeMeshArray;
}
// Функция для удаления тайла
function removeTile(x, z) {
const tileKey = `${x},${z}`;
const tile = tileMeshes[tileKey];
if (tile) {
scene.remove(tile);
delete tileMeshes[tileKey];
}
const treeMeshArray = treeMeshes[tileKey];
if (treeMeshArray) {
for (const tree of treeMeshArray) {
scene.remove(tree);
const index = trees.indexOf(tree);
if (index > -1) {
trees.splice(index, 1);
}
}
delete treeMeshes[tileKey];
}
delete tiles[tileKey];
}
function startGame() {
const menu = document.getElementById('menu');
menu.style.display = 'none';
const deathModal = document.getElementById('death-modal');
deathModal.style.display = 'none';
document.getElementById('hud').style.display = 'block';
document.getElementById('health-bar').style.display = 'block';
document.getElementById('crosshair').style.display = 'block';
kills = 0;
health = 100;
updateHealthBar();
updateHUD();
currentAmmo = magazineSize;
isReloading = false;
isAnimatingReload = false;
reloadAnimationProgress = 0;
updateHUD();
// Очищаем сцены от предыдущих объектов
clearGameObjects();
// Сбрасываем позицию и поворот игрока и камеры
player.position.copy(getRandomSpawnPosition());
player.rotation.set(0, 0, 0);
camera.rotation.set(0, 0, 0);
velocity.x = 0;
velocity.y = 0;
velocity.z = 0;
gameStartTime = new Date().getTime();
isGameActive = true;
enemySpeed = 0.05;
maxEnemies = 5;
lastDifficultyIncreaseTime = gameStartTime;
for (let i = 0; i < maxEnemies; i++) {
createEnemy();
}
document.body.requestPointerLock();
}
function endGame() {
isGameActive = false;
document.exitPointerLock();
if (kills > killsRecord) {
killsRecord = kills;
saveProgress();
}
if (killsRecord >= 1000) {
skins.golden.unlocked = true;
saveSkins();
}
updateMenu();
}
function clearGameObjects() {
// Удаляем врагов
enemies.forEach(enemy => scene.remove(enemy));
enemies.length = 0;
// Удаляем пули
bullets.forEach(bullet => scene.remove(bullet));
bullets.length = 0;
// Удаляем тайлы и деревья
for (let key in tileMeshes) {
scene.remove(tileMeshes[key]);
}
for (let key in treeMeshes) {
treeMeshes[key].forEach(tree => scene.remove(tree));
}
tiles = {};
tileMeshes = {};
treeMeshes = {};
trees.length = 0;
// Генерируем новые тайлы вокруг игрока
generateTilesAroundPlayer();
}
const restartButton = document.getElementById('restartButton');
const returnToMenuButton = document.getElementById('returnToMenuButton');
restartButton.addEventListener('click', function(event) {
event.stopPropagation();
const deathModal = document.getElementById('death-modal');
deathModal.style.display = 'none';
startGame();
});
returnToMenuButton.addEventListener('click', function(event) {
event.stopPropagation();
const deathModal = document.getElementById('death-modal');
deathModal.style.display = 'none';
location.reload();
});
const playButton = document.getElementById('playButton');
playButton.addEventListener('click', function(event) {
event.stopPropagation();
startGame();
});
window.addEventListener('resize', function() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}, false);
// Инициализация игры
loadProgress();
loadSkins();
initScene();
updateGunSkin();
updateMenu();
updateHealthBar();
updateHUD();
animate();
</script>
<div id="version">Версия 0.0.0.4</div>
</body>
</html>
```
**Пояснения к внесенным изменениям:**
1. **Создание системы тайлов:**
- **Переменные:**
- `tileSize` — размер одного тайла (в вашем случае это 200).
- `tiles` — объект для хранения информации о созданных тайлах.
- `tileMeshes` — объект для хранения самих мешей тайлов.
- `treeMeshes` — объект для хранения деревьев на каждом тайле.
- **Функции:**
- `generateTilesAroundPlayer()` — основная функция, которая генерирует тайлы вокруг игрока. Она вызывается при инициализации сцены и в каждом кадре анимации.
- `createTile(x, z)` — функция для создания отдельного тайла по координатам сетки.
- `removeTile(x, z)` — функция для удаления тайла и всех связанных с ним объектов (деревьев и т.д.), если он находится слишком далеко от игрока.
2. **Изменения в функции `initScene()`:**
- Убрана генерация единственного плана земли.
- Вместо этого при инициализации сцены вызывается `generateTilesAroundPlayer()`, которая создает необходимые тайлы вокруг начальной позиции игрока.
3. **Изменения в функции `animate()`:**
- В начале анимации проверяется, нужно ли сгенерировать новые тайлы, вызывая `generateTilesAroundPlayer()`.
- Также добавлено удаление тайлов, которые находятся слишком далеко от игрока, чтобы оптимизировать производительность.
4. **Обработка деревьев и столкновений:**
- Деревья теперь хранятся в массиве `trees`, и для каждого тайла есть свой список деревьев в `treeMeshes`.
- Столкновения с деревьями обрабатываются так же, как и раньше.
5. **Генерация врагов:**
- Враги генерируются в пределах видимой области вокруг игрока.
- В функции `createEnemy()` изменено место спавна врага, чтобы они появлялись в окружающих тайлах.
6. **Очистка объектов при перезапуске игры:**
- Добавлена функция `clearGameObjects()`, которая удаляет все существующие объекты (враги, пули, тайлы, деревья) из сцены и очищает соответствующие массивы и объекты.
- Эта функция вызывается в `startGame()` перед запуском новой игры, чтобы убедиться, что предыдущие объекты не сохраняются.
Теперь, когда игрок будет приближаться к краю текущей карты, будут генерироваться новые тайлы с землей, деревьями и врагами, создавая эффект бесконечного мира.
**Примечание по производительности:**
Поскольку мир становится потенциально бесконечным, важно контролировать количество объектов в сцене. В данном коде предусмотрено удаление тайлов, которые находятся слишком далеко от игрока, чтобы не перегружать память и не снижать производительность.
**Улучшения и возможные дальнейшие шаги:**
- **Оптимизация:** Можно добавить системы фрустум-кулинга (отсечения объектов вне поля зрения) и LOD (уровни детализации) для дальнейшей оптимизации производительности.
- **Сохранение мира:** Если вы хотите, чтобы мир оставался постоянным между сессиями, можно добавить генератор псевдослучайных чисел с фиксированным сидом и сохранять информацию о посещенных тайлах.
- **Дополнительные элементы:** Можно добавить разные типы тайлов, объектов и врагов для разнообразия игрового процесса.