игра
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>3D Игра — точная атака с прицелом</title>
<style>
* {
margin: 0;
padding: 0;
overflow: hidden;
}
body {
background: #1a1a2e;
font-family: Arial, sans-serif;
}
canvas {
display: block;
}
#ui {
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
background: rgba(10, 10, 30, 0.85);
backdrop-filter: blur(8px);
padding: 12px 24px;
border-radius: 40px;
border: 1px solid #3f4f8a;
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
justify-content: center;
pointer-events: auto;
box-shadow: 0 8px 30px rgba(0,0,0,0.7);
z-index: 10;
}
#ui button {
background: #2a3a6a;
border: none;
color: #eee;
padding: 6px 18px;
border-radius: 30px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: 0.15s;
border: 1px solid #4a5a8a;
letter-spacing: 0.3px;
white-space: nowrap;
}
#ui button:hover {
background: #3a4a8a;
transform: scale(1.04);
border-color: #7a9aff;
}
#ui input[type="file"] {
display: none;
}
#ui .sep {
width: 2px;
height: 30px;
background: #3f4f7a;
border-radius: 4px;
}
#info {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
color: rgba(255, 255, 255, 0.8);
background: rgba(0, 0, 0, 0.6);
padding: 8px 24px;
border-radius: 30px;
font-size: 14px;
border: 1px solid #2a3a6a;
backdrop-filter: blur(4px);
pointer-events: none;
user-select: none;
text-align: center;
z-index: 10;
white-space: nowrap;
}
#info span {
color: #aaccff;
font-weight: bold;
}
#status {
position: fixed;
bottom: 100px;
left: 50%;
transform: translateX(-50%);
color: #88aaff;
background: rgba(0, 0, 20, 0.7);
padding: 6px 18px;
border-radius: 30px;
font-size: 13px;
border: 1px solid #3f4f7a;
backdrop-filter: blur(4px);
pointer-events: none;
z-index: 10;
transition: 0.2s;
white-space: nowrap;
}
#controls-hint {
position: fixed;
bottom: 170px;
left: 50%;
transform: translateX(-50%);
color: rgba(255, 255, 255, 0.4);
background: rgba(0, 0, 0, 0.4);
padding: 4px 16px;
border-radius: 20px;
font-size: 12px;
backdrop-filter: blur(2px);
pointer-events: none;
z-index: 10;
text-align: center;
border: 1px solid rgba(255,255,255,0.05);
}
#coords {
position: fixed;
top: 80px;
left: 50%;
transform: translateX(-50%);
color: rgba(255, 255, 255, 0.5);
font-size: 13px;
background: rgba(0, 0, 0, 0.4);
padding: 4px 16px;
border-radius: 20px;
pointer-events: none;
z-index: 10;
font-family: monospace;
}
#hp-panel {
position: fixed;
right: 20px;
top: 50%;
transform: translateY(-50%);
background: rgba(10, 10, 30, 0.85);
backdrop-filter: blur(8px);
padding: 12px 16px;
border-radius: 12px;
border: 1px solid #3f4f8a;
color: #b0c0ee;
font-size: 12px;
z-index: 10;
max-height: 70vh;
overflow-y: auto;
min-width: 140px;
pointer-events: none;
}
#hp-panel::-webkit-scrollbar {
width: 4px;
}
#hp-panel::-webkit-scrollbar-track {
background: transparent;
}
#hp-panel::-webkit-scrollbar-thumb {
background: #4a5a8a;
border-radius: 10px;
}
.hp-item {
padding: 4px 0;
border-bottom: 1px solid #2a3a6a;
font-family: monospace;
font-size: 11px;
}
.hp-item:last-child {
border-bottom: none;
}
.hp-bar {
display: inline-block;
height: 8px;
border-radius: 4px;
margin-left: 6px;
transition: width 0.3s;
}
.hp-bar.high {
background: #44ff88;
}
.hp-bar.mid {
background: #ffaa44;
}
.hp-bar.low {
background: #ff2244;
}
#caps-indicator {
position: fixed;
top: 60px;
right: 20px;
background: rgba(10, 10, 30, 0.7);
backdrop-filter: blur(8px);
padding: 6px 14px;
border-radius: 20px;
border: 1px solid #4a5a8a;
color: #6a7aaa;
font-size: 12px;
z-index: 10;
pointer-events: none;
transition: 0.3s;
}
#caps-indicator.active {
border-color: #44ff88;
color: #44ff88;
box-shadow: 0 0 20px rgba(68, 255, 136, 0.2);
}
/* ЯРКИЙ ПРИЦЕЛ */
#crosshair {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 5;
pointer-events: none;
color: #00ff88;
font-size: 36px;
font-family: monospace;
text-shadow:
0 0 10px rgba(0, 255, 136, 0.8),
0 0 20px rgba(0, 255, 136, 0.5),
0 0 40px rgba(0, 255, 136, 0.3),
0 0 60px rgba(0, 255, 136, 0.2);
opacity: 0.9;
font-weight: bold;
}
#crosshair::before {
content: '+';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
#crosshair-dot {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 6;
pointer-events: none;
width: 4px;
height: 4px;
background: #ff2244;
border-radius: 50%;
box-shadow: 0 0 10px rgba(255, 34, 68, 0.8);
}
#crosshair-ring {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 5;
pointer-events: none;
width: 60px;
height: 60px;
border: 2px solid rgba(0, 255, 136, 0.15);
border-radius: 50%;
box-shadow: 0 0 20px rgba(0, 255, 136, 0.05);
}
@media (max-width: 700px) {
#ui { padding: 10px 14px; gap: 6px; bottom: 16px; }
#ui button { font-size: 12px; padding: 4px 12px; }
#info { font-size: 11px; top: 10px; white-space: normal; }
#status { bottom: 80px; font-size: 11px; }
#controls-hint { bottom: 140px; font-size: 10px; }
#coords { top: 70px; font-size: 11px; }
#hp-panel { right: 10px; padding: 8px 12px; min-width: 80px; font-size: 10px; }
.hp-item { font-size: 9px; }
#caps-indicator { top: 50px; right: 10px; font-size: 10px; padding: 4px 10px; }
#crosshair { font-size: 28px; }
#crosshair-ring { width: 40px; height: 40px; }
}
</style>
</head>
<body>
<!-- ЯРКИЙ ПРИЦЕЛ -->
<div id="crosshair"></div>
<div id="crosshair-dot"></div>
<div id="crosshair-ring"></div>
<div id="info">
<span>W A S D</span> — движение | <span>Пробел</span> — вверх | <span>Shift</span> — вниз | <span>Мышь</span> — осмотр
</div>
<div id="coords">📍 X: 0.0 | Y: 0.0 | Z: 0.0</div>
<div id="status">🎮 Игра загружена | Блоков: 0</div>
<div id="controls-hint">🖱️ ЛКМ — атака (50 урона) | ПКМ — удалить</div>
<div id="caps-indicator">🔒 Caps Lock: OFF</div>
<div id="ui">
<button id="loadBtn">📂 Загрузить карту</button>
<input type="file" id="fileInput" accept=".json" />
<span class="sep"></span>
<button id="clearBtn">🗑️ Очистить</button>
<button id="saveBtn">💾 Сохранить</button>
<span class="sep"></span>
<span style="color:#ff6644; font-weight:bold;">⚔️ Урон: 50</span>
<span class="sep"></span>
<span id="totalHp" style="color:#88aaff;">❤️ Общий HP: 0</span>
</div>
<div id="hp-panel">
<div style="font-weight:bold; margin-bottom:8px; color:#88aaff;">📊 Блоки</div>
<div id="hpList">Нет блоков</div>
</div>
<script src="three.min.js"></script>
<script>
// ----- СЦЕНА, КАМЕРА, РЕНДЕР -----
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a2e);
const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 500);
camera.position.set(0, 3, 8);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
// ----- СВЕТ -----
const ambient = new THREE.AmbientLight(0x404060);
scene.add(ambient);
const sun = new THREE.DirectionalLight(0xffeedd, 1.8);
sun.position.set(10, 20, 5);
sun.castShadow = true;
sun.shadow.mapSize.width = 1024;
sun.shadow.mapSize.height = 1024;
const d = 50;
sun.shadow.camera.left = -d;
sun.shadow.camera.right = d;
sun.shadow.camera.top = d;
sun.shadow.camera.bottom = -d;
sun.shadow.camera.near = 1;
sun.shadow.camera.far = 100;
scene.add(sun);
const fill = new THREE.DirectionalLight(0x4488ff, 0.4);
fill.position.set(-5, 0, 10);
scene.add(fill);
// ----- ПЕРЕМЕННЫЕ КАРТЫ -----
let mapSize = 20;
let floor, grid, gridFine;
function buildMap(size) {
if (floor) {
scene.remove(floor);
floor.geometry.dispose();
floor.material.dispose();
}
if (grid) {
scene.remove(grid);
grid.geometry.dispose();
grid.material.dispose();
}
if (gridFine) {
scene.remove(gridFine);
gridFine.geometry.dispose();
gridFine.material.dispose();
}
const floorMat = new THREE.MeshStandardMaterial({ color: 0x2a2a4a, roughness: 0.9, metalness: 0.1 });
floor = new THREE.Mesh(new THREE.PlaneGeometry(size, size), floorMat);
floor.rotation.x = -Math.PI / 2;
floor.position.y = -0.5;
floor.receiveShadow = true;
floor.name = 'floor';
scene.add(floor);
grid = new THREE.GridHelper(size, size, 0x8888ff, 0x444488);
grid.position.y = -0.49;
scene.add(grid);
gridFine = new THREE.GridHelper(size, size * 2, 0x6666aa, 0x333366);
gridFine.position.y = -0.48;
gridFine.material.transparent = true;
gridFine.material.opacity = 0.3;
scene.add(gridFine);
const halfSize = size / 2;
camera.position.set(0, Math.min(size * 0.3, 15), size * 0.6);
}
buildMap(mapSize);
// ----- МЕНЕДЖЕР ОБЪЕКТОВ -----
const objects = [];
let objectIdCounter = 0;
const shapeMap = {
box: () => new THREE.BoxGeometry(1, 1, 1),
sphere: () => new THREE.SphereGeometry(0.5, 20, 20),
cylinder: () => new THREE.CylinderGeometry(0.5, 0.5, 1, 16),
cone: () => new THREE.ConeGeometry(0.5, 1, 16),
torus: () => new THREE.TorusGeometry(0.5, 0.25, 12, 20),
};
const shapeHeights = {
box: 1,
sphere: 1,
cylinder: 1,
cone: 1,
torus: 0.5,
};
// ----- ФУНКЦИЯ ЦВЕТА ПО HP -----
function getColorForHP(hp) {
let color = new THREE.Color();
if (hp <= 100) {
const t = hp / 100;
color.lerpColors(new THREE.Color(0x228833), new THREE.Color(0x44ff88), t);
} else if (hp <= 500) {
const t = (hp - 100) / 400;
color.lerpColors(new THREE.Color(0x44ff88), new THREE.Color(0x88ff44), t);
} else if (hp <= 1000) {
const t = (hp - 500) / 500;
color.lerpColors(new THREE.Color(0x88ff44), new THREE.Color(0xffaa44), t);
} else if (hp <= 2500) {
const t = (hp - 1000) / 1500;
color.lerpColors(new THREE.Color(0xffaa44), new THREE.Color(0xff6644), t);
} else {
const t = Math.min(1, (hp - 2500) / 2500);
color.lerpColors(new THREE.Color(0xff6644), new THREE.Color(0xff2244), t);
}
return color;
}
function updateObjectColor(obj) {
const hp = obj.userData.hp || 0;
const color = getColorForHP(hp);
obj.material.color.copy(color);
const maxHp = obj.userData.maxHp || 1000;
const percent = Math.max(0, Math.min(1, hp / maxHp));
obj.material.emissive.copy(color);
obj.material.emissive.multiplyScalar(0.1 + (1 - percent) * 0.3);
}
// ----- ДОБАВЛЕНИЕ ОБЪЕКТА -----
function addObject(shape, position, hp) {
const geoFn = shapeMap[shape];
if (!geoFn) return null;
const color = getColorForHP(hp);
const mat = new THREE.MeshStandardMaterial({
color: color,
roughness: 0.4,
metalness: 0.2,
emissive: color.clone().multiplyScalar(0.1),
});
const mesh = new THREE.Mesh(geoFn(), mat);
const height = shapeHeights[shape] || 1;
mesh.position.set(position.x, position.y + height/2, position.z);
mesh.castShadow = true;
mesh.receiveShadow = true;
mesh.name = 'userObject';
mesh.userData.shape = shape;
mesh.userData.height = height;
mesh.userData.baseY = position.y;
mesh.userData.hp = hp;
mesh.userData.maxHp = hp;
mesh.userData.id = objectIdCounter++;
scene.add(mesh);
objects.push(mesh);
updateUI();
return mesh;
}
// ----- ПОДСВЕТКА БЛОКА ПРИ НАВЕДЕНИИ -----
let highlightedObject = null;
function updateHighlight() {
const rect = renderer.domElement.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const pointer = new THREE.Vector2();
pointer.x = ((centerX - rect.left) / rect.width) * 2 - 1;
pointer.y = -((centerY - rect.top) / rect.height) * 2 + 1;
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObjects(objects);
// Снимаем подсветку с предыдущего объекта
if (highlightedObject && highlightedObject !== (intersects.length > 0 ? intersects[0].object : null)) {
const hp = highlightedObject.userData.hp || 0;
const color = getColorForHP(hp);
highlightedObject.material.color.copy(color);
highlightedObject.material.emissive.copy(color);
const maxHp = highlightedObject.userData.maxHp || 1000;
const percent = Math.max(0, Math.min(1, hp / maxHp));
highlightedObject.material.emissive.multiplyScalar(0.1 + (1 - percent) * 0.3);
highlightedObject = null;
}
if (intersects.length > 0) {
const obj = intersects[0].object;
highlightedObject = obj;
// Подсвечиваем белым
obj.material.color.set(0xffffff);
obj.material.emissive.set(0x4488ff);
obj.material.emissiveIntensity = 0.5;
document.getElementById('crosshair-dot').style.background = '#44ff88';
document.getElementById('crosshair-dot').style.boxShadow = '0 0 15px rgba(68, 255, 136, 0.9)';
} else {
document.getElementById('crosshair-dot').style.background = '#ff2244';
document.getElementById('crosshair-dot').style.boxShadow = '0 0 10px rgba(255, 34, 68, 0.8)';
}
}
// ----- АТАКА (50 урона) строго по прицелу -----
function attackAtCrosshair() {
const rect = renderer.domElement.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const pointer = new THREE.Vector2();
pointer.x = ((centerX - rect.left) / rect.width) * 2 - 1;
pointer.y = -((centerY - rect.top) / rect.height) * 2 + 1;
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObjects(objects);
if (intersects.length > 0) {
const obj = intersects[0].object;
attackObject(obj);
return true;
} else {
document.getElementById('status').textContent = '❌ Промах!';
setTimeout(() => updateStatus(), 1000);
return false;
}
}
function attackObject(obj) {
if (!obj || obj.userData.hp <= 0) return;
const damage = 50;
obj.userData.hp = Math.max(0, obj.userData.hp - damage);
updateObjectColor(obj);
// Эффект удара
obj.material.emissiveIntensity = 0.8;
setTimeout(() => {
if (obj.material) {
const percent = obj.userData.hp / obj.userData.maxHp;
obj.material.emissiveIntensity = 0.1 + (1 - percent) * 0.3;
}
}, 100);
if (obj.userData.hp <= 0) {
scene.remove(obj);
const idx = objects.indexOf(obj);
if (idx !== -1) {
objects.splice(idx, 1);
obj.geometry.dispose();
obj.material.dispose();
document.getElementById('status').textContent = `💥 Блок уничтожен! Осталось: ${objects.length}`;
setTimeout(() => updateStatus(), 2000);
}
if (highlightedObject === obj) {
highlightedObject = null;
}
} else {
document.getElementById('status').textContent = `⚔️ Атака! -${damage} HP (${obj.userData.hp}/${obj.userData.maxHp})`;
setTimeout(() => updateStatus(), 1500);
}
updateUI();
updateStatus();
}
// ----- УДАЛЕНИЕ ВСЕХ -----
function clearObjects() {
for (const obj of objects) {
scene.remove(obj);
obj.geometry.dispose();
obj.material.dispose();
}
objects.length = 0;
objectIdCounter = 0;
highlightedObject = null;
updateUI();
updateStatus();
}
// ----- СОХРАНЕНИЕ -----
function saveToFile() {
if (objects.length === 0) {
document.getElementById('status').textContent = '⚠️ Нет объектов для сохранения';
setTimeout(() => updateStatus(), 1500);
return;
}
const data = {
version: '1.0',
mapSize: mapSize,
objects: objects.map(obj => ({
shape: obj.userData.shape || 'box',
x: obj.position.x,
y: obj.userData.baseY !== undefined ? obj.userData.baseY : obj.position.y - shapeHeights[obj.userData.shape]/2,
z: obj.position.z,
hp: obj.userData.hp || 1000,
maxHp: obj.userData.maxHp || 1000,
}))
};
const json = JSON.stringify(data, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'map3d_' + new Date().toISOString().slice(0,19).replace(/:/g, '-') + '.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
document.getElementById('status').textContent = '✅ Карта сохранена!';
setTimeout(() => updateStatus(), 1500);
}
// ----- ЗАГРУЗКА -----
function loadFromFile(file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target.result);
if (data.mapSize) {
mapSize = data.mapSize;
buildMap(mapSize);
}
if (!Array.isArray(data.objects)) {
throw new Error('Неверный формат данных');
}
clearObjects();
let loadedCount = 0;
for (const item of data.objects) {
if (!item.shape || item.x === undefined || item.y === undefined || item.z === undefined) {
continue;
}
const pos = new THREE.Vector3(item.x, item.y, item.z);
const hp = item.hp || item.maxHp || 1000;
const mesh = addObject(item.shape, pos, hp);
if (mesh) {
mesh.userData.shape = item.shape;
mesh.userData.hp = hp;
mesh.userData.maxHp = item.maxHp || hp;
updateObjectColor(mesh);
loadedCount++;
}
}
document.getElementById('status').textContent = `📂 Загружено: ${loadedCount} блоков`;
setTimeout(() => updateStatus(), 2000);
updateUI();
} catch (err) {
console.error(err);
document.getElementById('status').textContent = '❌ Ошибка загрузки файла';
setTimeout(() => updateStatus(), 2000);
}
};
reader.onerror = () => {
document.getElementById('status').textContent = '❌ Ошибка чтения файла';
setTimeout(() => updateStatus(), 2000);
};
reader.readAsText(file);
}
// ----- ОБНОВЛЕНИЕ UI -----
function updateUI() {
const hpList = document.getElementById('hpList');
const totalHp = document.getElementById('totalHp');
if (objects.length === 0) {
hpList.innerHTML = '<div style="color: #6a7aaa; font-size: 12px;">Нет блоков</div>';
totalHp.textContent = '❤️ Общий HP: 0';
return;
}
let total = 0;
let html = '';
const displayObjects = objects.slice(-15);
for (const obj of displayObjects) {
const hp = obj.userData.hp || 0;
const maxHp = obj.userData.maxHp || 1000;
total += hp;
const percent = Math.max(0, (hp / maxHp) * 100);
const shapeName = obj.userData.shape || 'box';
const color = obj.material.color.getHexString();
let hpClass = 'high';
if (percent < 25) hpClass = 'low';
else if (percent < 50) hpClass = 'mid';
html += `<div class="hp-item">
<span style="display:inline-block;width:10px;height:10px;background:#${color};border-radius:3px;vertical-align:middle;"></span>
${shapeName}
<span style="color:#88aaff;">${Math.round(hp)}/${maxHp}</span>
<span class="hp-bar ${hpClass}" style="width:${percent}%;"></span>
</div>`;
}
if (objects.length > 15) {
html += `<div style="color: #6a7aaa; font-size: 10px; text-align:center; padding-top:4px;">+ ещё ${objects.length - 15} блоков</div>`;
}
hpList.innerHTML = html;
totalHp.textContent = `❤️ Общий HP: ${Math.round(total)}`;
}
function updateStatus() {
document.getElementById('status').textContent = `🎮 Игра | Блоков: ${objects.length} | Карта: ${mapSize}x${mapSize}`;
}
// ----- УПРАВЛЕНИЕ -----
const keys = { w: false, a: false, s: false, d: false, space: false, shift: false };
let isCapsLock = false;
document.addEventListener('keydown', (e) => {
switch (e.code) {
case 'KeyW': keys.w = true; e.preventDefault(); break;
case 'KeyA': keys.a = true; e.preventDefault(); break;
case 'KeyS': keys.s = true; e.preventDefault(); break;
case 'KeyD': keys.d = true; e.preventDefault(); break;
case 'Space': keys.space = true; e.preventDefault(); break;
case 'ShiftLeft':
case 'ShiftRight': keys.shift = true; e.preventDefault(); break;
case 'CapsLock':
isCapsLock = !isCapsLock;
const indicator = document.getElementById('caps-indicator');
if (isCapsLock) {
indicator.textContent = '⚡ Caps Lock: ON (x5)';
indicator.classList.add('active');
} else {
indicator.textContent = '🔒 Caps Lock: OFF';
indicator.classList.remove('active');
}
e.preventDefault();
break;
default: break;
}
});
document.addEventListener('keyup', (e) => {
switch (e.code) {
case 'KeyW': keys.w = false; e.preventDefault(); break;
case 'KeyA': keys.a = false; e.preventDefault(); break;
case 'KeyS': keys.s = false; e.preventDefault(); break;
case 'KeyD': keys.d = false; e.preventDefault(); break;
case 'Space': keys.space = false; e.preventDefault(); break;
case 'ShiftLeft':
case 'ShiftRight': keys.shift = false; e.preventDefault(); break;
default: break;
}
});
// ----- МЫШЬ -----
let isLocked = false;
renderer.domElement.addEventListener('click', () => {
if (!isLocked) {
renderer.domElement.requestPointerLock();
}
});
document.addEventListener('pointerlockchange', () => {
isLocked = document.pointerLockElement === renderer.domElement;
});
let yaw = 0, pitch = 0;
document.addEventListener('mousemove', (e) => {
if (!isLocked) return;
const sens = 0.002;
yaw -= e.movementX * sens;
pitch -= e.movementY * sens;
pitch = Math.max(-Math.PI / 2 + 0.1, Math.min(Math.PI / 2 - 0.1, pitch));
// Обновляем подсветку при движении мыши
updateHighlight();
});
// ----- ДВИЖЕНИЕ -----
const baseSpeed = 4.0;
const clock = new THREE.Clock();
function updateCamera() {
const euler = new THREE.Euler(pitch, yaw, 0, 'YXZ');
camera.quaternion.setFromEuler(euler);
}
// ----- АТАКА ПО ЛКМ -----
renderer.domElement.addEventListener('click', (e) => {
if (!isLocked) return;
attackAtCrosshair();
});
// ----- УДАЛЕНИЕ ПО ПКМ -----
renderer.domElement.addEventListener('mousedown', (e) => {
if (e.button === 2 && isLocked) {
const rect = renderer.domElement.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const pointer = new THREE.Vector2();
pointer.x = ((centerX - rect.left) / rect.width) * 2 - 1;
pointer.y = -((centerY - rect.top) / rect.height) * 2 + 1;
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObjects(objects);
if (intersects.length > 0) {
const obj = intersects[0].object;
scene.remove(obj);
const idx = objects.indexOf(obj);
if (idx !== -1) {
objects.splice(idx, 1);
obj.geometry.dispose();
obj.material.dispose();
document.getElementById('status').textContent = `🗑️ Удалён (осталось ${objects.length})`;
setTimeout(() => updateStatus(), 1000);
updateUI();
if (highlightedObject === obj) {
highlightedObject = null;
}
}
}
}
});
renderer.domElement.addEventListener('contextmenu', (e) => e.preventDefault());
// ----- КООРДИНАТЫ -----
function updateCoords() {
document.getElementById('coords').textContent =
`📍 X: ${camera.position.x.toFixed(1)} | Y: ${camera.position.y.toFixed(1)} | Z: ${camera.position.z.toFixed(1)}`;
}
// ----- АНИМАЦИЯ -----
function animate() {
const delta = clock.getDelta();
const currentSpeed = isCapsLock ? baseSpeed * 5 : baseSpeed;
const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
forward.y = 0;
forward.normalize();
const right = new THREE.Vector3(1, 0, 0).applyQuaternion(camera.quaternion);
right.y = 0;
right.normalize();
const move = new THREE.Vector3(0, 0, 0);
if (keys.w) move.add(forward.clone().multiplyScalar(currentSpeed * delta));
if (keys.s) move.sub(forward.clone().multiplyScalar(currentSpeed * delta));
if (keys.a) move.sub(right.clone().multiplyScalar(currentSpeed * delta));
if (keys.d) move.add(right.clone().multiplyScalar(currentSpeed * delta));
if (keys.space) move.y += currentSpeed * delta * 1.2;
if (keys.shift) move.y -= currentSpeed * delta * 1.2;
camera.position.x += move.x;
camera.position.y += move.y;
camera.position.z += move.z;
camera.position.y = Math.max(0.5, Math.min(20, camera.position.y));
updateCamera();
updateCoords();
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
// ----- UI -----
document.getElementById('loadBtn').addEventListener('click', () => {
document.getElementById('fileInput').click();
});
document.getElementById('fileInput').addEventListener('change', (e) => {
if (e.target.files.length > 0) {
loadFromFile(e.target.files[0]);
e.target.value = '';
}
});
document.getElementById('clearBtn').addEventListener('click', () => {
if (confirm('Удалить все блоки?')) {
clearObjects();
}
});
document.getElementById('saveBtn').addEventListener('click', saveToFile);
// ----- РЕСАЙЗ -----
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// ----- СТАРТ -----
animate();
updateStatus();
updateUI();
console.log('🎮 Игра запущена! Яркий прицел + подсветка блоков');
console.log('🖱️ ЛКМ — атака (50 урона) строго по центру экрана');
console.log('💡 Блок подсвечивается белым при наведении');
</script>
</body>
</html>
можно портировать на esp32, сделать загрузку частей карты с sd карты, по мере продвижения, и добавить много чего, но это все в теории! а это просто набросок, и времени проверять нету, но может заинтересует, извините за флуд не по теме