Загрузка данных


привет, сделай мне аимбот под игру в виде скрипта в консоль. сделай гуи.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Arena FPS Online — тренировочная карта</title>
<link rel="stylesheet" href="style.css">
</head>
<body>

<div id="menu">
  <div class="menu-box">
    <h1>ARENA FPS <span>online</span></h1>
    <p class="subtitle">3D-шутер · AK-47 · реальная отдача · баннихоп · мультиплеер</p>
    <input id="nameInput" type="text" maxlength="16" placeholder="Ваш никнейм" value="Player">
    <select id="mapSelect">
      <option value="training">Тренировочная арена</option>
      <option value="bazaar">Пустынный рынок (в стиле Mirage)</option>
    </select>
    <div class="map-note">Карту задаёт первый игрок в сессии — остальные подключаются к той же</div>
    <button id="playBtn">ИГРАТЬ</button>
    <div class="hints">
      <b>Управление:</b><br>
      WASD — движение &nbsp;|&nbsp; Мышь — обзор &nbsp;|&nbsp; ЛКМ — стрельба<br>
      ПРОБЕЛ — прыжок (жми в момент приземления для баннихопа) &nbsp;|&nbsp; R — перезарядка<br>
      SHIFT — бег &nbsp;|&nbsp; Esc — меню
    </div>
  </div>
</div>

<div id="hud" class="hidden">
  <div id="crosshair">
    <div class="ch-line ch-top"></div>
    <div class="ch-line ch-bottom"></div>
    <div class="ch-line ch-left"></div>
    <div class="ch-line ch-right"></div>
  </div>

  <div id="healthBar">
    <div class="stat-icon">❤</div>
    <div id="healthValue">100</div>
  </div>

  <div id="ammoBar">
    <div id="ammoValue">30 / 90</div>
    <div class="stat-icon">AK-47</div>
  </div>

  <div id="speedBar">Скорость: <span id="speedValue">0</span></div>

  <div id="killfeed"></div>

  <div id="scoreboard" class="hidden"></div>

  <div id="chatBox">
    <div id="chatMessages"></div>
    <input id="chatInput" type="text" maxlength="200" placeholder="Нажми T чтобы написать...">
  </div>

  <div id="deathScreen" class="hidden">
    <div>ВАС УБИЛИ</div>
    <div class="respawn-text">Возрождение через <span id="respawnTimer">3</span>...</div>
  </div>
</div>

<!-- Сенсорное управление (показывается только на мобильных/тач-устройствах) -->
<div id="touchControls" class="hidden">
  <div id="lookZone"></div>
  <div id="joystickZone">
    <div id="joystickBase">
      <div id="joystickStick"></div>
    </div>
  </div>
  <div id="touchButtons">
    <button id="touchReload" class="touch-btn small">R</button>
    <button id="touchJump" class="touch-btn medium">↑</button>
    <button id="touchFire" class="touch-btn large">●</button>
  </div>
</div>

<div id="crosshairLockHint" class="hidden">Кликните, чтобы захватить курсор</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.5/socket.io.min.js"></script>
<script type="importmap">
{
  "imports": {
    "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js"
  }
}
</script>
<script type="module" src="client.js"></script>
</body>
</html>
import * as THREE from 'three';

/* =========================================================
   ARENA FPS ONLINE — клиент
   Three.js рендер, физика движения с баннихопом,
   AK-47 со spray-паттерном отдачи, Socket.io мультиплеер.
========================================================= */

// ---------- Базовая настройка сцены ----------
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87b7d9);
scene.fog = new THREE.Fog(0x87b7d9, 40, 140);

const camera = new THREE.PerspectiveCamera(90, window.innerWidth / window.innerHeight, 0.05, 500);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);

window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});

// ---------- Свет ----------
const hemi = new THREE.HemisphereLight(0xffffff, 0x445566, 0.9);
scene.add(hemi);
const sun = new THREE.DirectionalLight(0xfff2d8, 1.1);
sun.position.set(40, 60, 20);
sun.castShadow = true;
sun.shadow.mapSize.set(2048, 2048);
sun.shadow.camera.left = -60;
sun.shadow.camera.right = 60;
sun.shadow.camera.top = 60;
sun.shadow.camera.bottom = -60;
sun.shadow.camera.far = 200;
scene.add(sun);

// ---------- Материалы ----------
const matFloor = new THREE.MeshStandardMaterial({ color: 0x555f6b, roughness: 0.9 });
const matWall = new THREE.MeshStandardMaterial({ color: 0x7a6a55, roughness: 0.85 });
const matBox = new THREE.MeshStandardMaterial({ color: 0xb8863b, roughness: 0.7 });
const matRamp = new THREE.MeshStandardMaterial({ color: 0x3d6b8a, roughness: 0.6 });
const matMetal = new THREE.MeshStandardMaterial({ color: 0x8b8f94, roughness: 0.35, metalness: 0.6 });

// ---------- Коллизионные объекты (простые AABB) ----------
const colliders = []; // {mesh, box3}

function addBox(w, h, d, x, y, z, material, castShadow = true) {
  const geo = new THREE.BoxGeometry(w, h, d);
  const mesh = new THREE.Mesh(geo, material);
  mesh.position.set(x, y, z);
  mesh.castShadow = castShadow;
  mesh.receiveShadow = true;
  scene.add(mesh);
  const box3 = new THREE.Box3().setFromObject(mesh);
  colliders.push({ mesh, box3 });
  return mesh;
}

// ---------- Тренировочная карта ----------
function buildTrainingMap() {
  // Пол
  const floorGeo = new THREE.PlaneGeometry(140, 140);
  const floor = new THREE.Mesh(floorGeo, matFloor);
  floor.rotation.x = -Math.PI / 2;
  floor.receiveShadow = true;
  scene.add(floor);

  // Внешние стены арены
  addBox(140, 8, 1, 0, 4, -70, matWall);
  addBox(140, 8, 1, 0, 4, 70, matWall);
  addBox(1, 8, 140, -70, 4, 0, matWall);
  addBox(1, 8, 140, 70, 4, 0, matWall);

  // Центральные укрытия (для стрельбы/тренировки прицела)
  addBox(4, 3, 4, 8, 1.5, 8, matBox);
  addBox(4, 3, 4, -8, 1.5, 8, matBox);
  addBox(4, 3, 4, 8, 1.5, -8, matBox);
  addBox(4, 3, 4, -8, 1.5, -8, matBox);
  addBox(2, 2, 12, 0, 1, 0, matBox);

  // Мишени-стены для тренировки отдачи (AK-47 spray)
  for (let i = -2; i <= 2; i++) {
    addBox(3, 4, 0.3, i * 4, 2, -30, matMetal);
  }

  // ---- Зона баннихопа: серия рамп и платформ разной высоты ----
  const bhopZ = 30;
  const rampCount = 7;
  for (let i = 0; i < rampCount; i++) {
    const x = -24 + i * 8;
    const height = 1 + i * 0.6;
    // платформа
    addBox(4, 0.5, 4, x, height, bhopZ, matRamp);
    // рампа-подъём к следующей платформе
    if (i < rampCount - 1) {
      const rampMesh = addBox(4, 0.4, 4.2, x + 4, height + 0.4, bhopZ, matRamp);
      rampMesh.rotation.x = -0.35;
    }
  }
  // Финальная высокая платформа с прыжком вниз (для практики приземления в бхоп)
  addBox(6, 0.5, 6, -24 + rampCount * 8, 1 + (rampCount - 1) * 0.6, bhopZ, matRamp);

  // Отдельная низкая полоса препятствий (страйф-джампы)
  for (let i = 0; i < 10; i++) {
    addBox(1.2, 0.4, 3, -20 + i * 3.5, 0.6, 45 + (i % 2 === 0 ? 0 : 1.5), matRamp);
  }

  // Лестница из ящиков
  for (let i = 0; i < 5; i++) {
    addBox(3, 1, 3, 40, 0.5 + i * 1, -20 + i * 3, matBox);
  }

  // ---- Дополнительные объекты для тренировки (бочки, ящики разных форм) ----
  addBarrel(15, 1, -20);
  addBarrel(15.8, 1, -19.3);
  addBarrel(-15, 1, -20);
  addCrateStack(25, 0, 0, 3);
  addCrateStack(-25, 0, 0, 2);
  addCrateStack(0, 0, -45, 4);
  addBox(6, 0.15, 6, 25, 0.08, 20, matMetal, false); // плита-платформа
  addBarrel(25, 1, 20.5);
  addBarrel(-8, 1, -8.5);
}

// ---------- Переиспользуемые объекты-пропы ----------
function addBarrel(x, y, z) {
  const geo = new THREE.CylinderGeometry(0.5, 0.5, 1.1, 16);
  const mat = new THREE.MeshStandardMaterial({ color: 0x8a3b2b, roughness: 0.6, metalness: 0.3 });
  const mesh = new THREE.Mesh(geo, mat);
  mesh.position.set(x, y, z);
  mesh.castShadow = true;
  mesh.receiveShadow = true;
  scene.add(mesh);
  const box3 = new THREE.Box3().setFromObject(mesh);
  colliders.push({ mesh, box3 });
  return mesh;
}

function addCrateStack(cx, cy, cz, count) {
  const positions = [
    [0, 0, 0], [1.1, 0, 0], [0.55, 1.1, 0], [0, 0, 1.1], [1.1, 0, 1.1]
  ];
  for (let i = 0; i < Math.min(count, positions.length); i++) {
    const [dx, dy, dz] = positions[i];
    addBox(1, 1, 1, cx + dx, cy + 0.5 + dy, cz + dz, matBox);
  }
}

// Карта строится после получения ответа от сервера (см. socket.on('init', ...) ниже),
// чтобы все игроки сессии видели одну и ту же карту.
function buildMap(mapId) {
  if (mapId === 'bazaar') {
    buildDesertBazaarMap();
  } else {
    buildTrainingMap();
  }
  colliders.forEach(c => c.box3.setFromObject(c.mesh));
}

// ---------- Пустынный рынок (оригинальная карта в духе ближневосточного города,
// вдохновлена общей эстетикой пустынных карт жанра — не копия конкретного
// официального левел-дизайна) ----------
function buildDesertBazaarMap() {
  const matSand = new THREE.MeshStandardMaterial({ color: 0xd9b877, roughness: 0.95 });
  const matSandstone = new THREE.MeshStandardMaterial({ color: 0xc9a06a, roughness: 0.85 });
  const matSandstoneDark = new THREE.MeshStandardMaterial({ color: 0xa67e4d, roughness: 0.85 });
  const matWood = new THREE.MeshStandardMaterial({ color: 0x6b4a2b, roughness: 0.8 });
  const matCloth = new THREE.MeshStandardMaterial({ color: 0xb3402f, roughness: 0.9 });
  const matClothBlue = new THREE.MeshStandardMaterial({ color: 0x2f6fb3, roughness: 0.9 });
  const matPalm = new THREE.MeshStandardMaterial({ color: 0x3c6b34, roughness: 0.8 });
  const matTrunk = new THREE.MeshStandardMaterial({ color: 0x5a3d24, roughness: 0.9 });

  scene.background = new THREE.Color(0xe8c98a);
  scene.fog = new THREE.Fog(0xe8c98a, 45, 150);
  hemi.color.set(0xfff3d0);
  hemi.groundColor.set(0xc9a06a);
  sun.color.set(0xffe3ad);
  sun.intensity = 1.3;

  // Песчаный пол
  const floor = new THREE.Mesh(new THREE.PlaneGeometry(150, 150), matSand);
  floor.rotation.x = -Math.PI / 2;
  floor.receiveShadow = true;
  scene.add(floor);

  // Внешние границы города
  addBox(150, 10, 1, 0, 5, -75, matSandstoneDark);
  addBox(150, 10, 1, 0, 5, 75, matSandstoneDark);
  addBox(1, 10, 150, -75, 5, 0, matSandstoneDark);
  addBox(1, 10, 150, 75, 5, 0, matSandstoneDark);

  // ---- Здание А (юго-западный квартал, "точка А") ----
  function building(x, z, w, d, h, mat, doorGapX = null) {
    const wallH = h;
    addBox(w, wallH, 0.5, x, wallH / 2, z - d / 2, mat); // задняя стена
    addBox(0.5, wallH, d, x - w / 2, wallH / 2, z, mat);  // левая
    addBox(0.5, wallH, d, x + w / 2, wallH / 2, z, mat);  // правая
    if (doorGapX === null) {
      addBox(w, wallH, 0.5, x, wallH / 2, z + d / 2, mat); // передняя (без прохода)
    } else {
      const gap = 2.2;
      addBox((w - gap) / 2, wallH, 0.5, x - gap / 2 - (w - gap) / 4, wallH / 2, z + d / 2, mat);
      addBox((w - gap) / 2, wallH, 0.5, x + gap / 2 + (w - gap) / 4, wallH / 2, z + d / 2, mat);
    }
    addBox(w + 0.6, 0.4, d + 0.6, x, h + 0.2, z, matSandstoneDark); // крыша
  }

  building(-30, -35, 14, 12, 6, matSandstone, 0);
  building(-30, 35, 14, 12, 6, matSandstone, 0);
  building(30, -35, 14, 12, 6, matSandstone, 0);
  building(30, 35, 14, 12, 6, matSandstone, 0);

  // Арки-проходы между зданиями (характерный элемент ближневосточной архитектуры)
  function archway(x, z, rotY) {
    const g = new THREE.Group();
    const pillarL = new THREE.Mesh(new THREE.BoxGeometry(0.6, 4, 0.6), matSandstone);
    pillarL.position.set(-1.5, 2, 0);
    const pillarR = pillarL.clone();
    pillarR.position.set(1.5, 2, 0);
    const top = new THREE.Mesh(new THREE.BoxGeometry(3.6, 0.6, 0.6), matSandstone);
    top.position.set(0, 4, 0);
    g.add(pillarL, pillarR, top);
    g.position.set(x, 0, z);
    g.rotation.y = rotY;
    g.traverse(o => { if (o.isMesh) { o.castShadow = true; o.receiveShadow = true; } });
    scene.add(g);
    [pillarL, pillarR].forEach(p => {
      const worldPos = p.getWorldPosition(new THREE.Vector3());
      const box3 = new THREE.Box3().setFromObject(p);
      colliders.push({ mesh: p, box3 });
    });
  }
  archway(0, -35, 0);
  archway(0, 35, 0);

  // ---- Средний коридор (mid) с длинной стеной-прикрытием ----
  addBox(1, 3, 40, -3, 1.5, 0, matSandstone);
  addBox(1, 3, 40, 3, 1.5, 0, matSandstoneDark);
  for (let i = -1; i <= 1; i += 2) {
    addBox(2, 1.2, 2, i * 3, 0.6, 15, matSandstone);
    addBox(2, 1.2, 2, i * 3, 0.6, -15, matSandstone);
  }

  // ---- Рыночная площадь в центре с прилавками и навесами ----
  function marketStall(x, z, clothMat) {
    const g = new THREE.Group();
    const poleGeo = new THREE.CylinderGeometry(0.06, 0.06, 2.2, 8);
    const positions = [[-1.2, -0.8], [1.2, -0.8], [-1.2, 0.8], [1.2, 0.8]];
    positions.forEach(([px, pz]) => {
      const pole = new THREE.Mesh(poleGeo, matWood);
      pole.position.set(px, 1.1, pz);
      pole.castShadow = true;
      g.add(pole);
    });
    const roof = new THREE.Mesh(new THREE.BoxGeometry(2.8, 0.1, 2, 1), clothMat);
    roof.position.set(0, 2.25, 0);
    roof.castShadow = true;
    g.add(roof);
    const counter = new THREE.Mesh(new THREE.BoxGeometry(2.4, 0.9, 1.6), matWood);
    counter.position.set(0, 0.45, 0);
    counter.castShadow = true;
    counter.receiveShadow = true;
    g.add(counter);
    g.position.set(x, 0, z);
    scene.add(g);
    const box3 = new THREE.Box3().setFromObject(counter);
    colliders.push({ mesh: counter, box3 });
    return g;
  }
  marketStall(-8, 5, matCloth);
  marketStall(8, 5, matClothBlue);
  marketStall(-8, -5, matClothBlue);
  marketStall(8, -5, matCloth);

  // Ящики и бочки, разбросанные по рынку (прикрытия для перестрелок)
  addCrateStackDesert(0, 0, 10, 3, matWood);
  addCrateStackDesert(14, 0, 0, 2, matWood);
  addCrateStackDesert(-14, 0, 0, 4, matWood);
  addBarrelDesert(3, 1, 2);
  addBarrelDesert(-3, 1, -2);
  addBarrelDesert(18, 1, 18);
  addBarrelDesert(-18, 1, -18);
  addBarrelDesert(18, 1, -18);
  addBarrelDesert(-18, 1, 18);

  // Пальмы для атмосферы
  function palm(x, z) {
    const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.15, 0.22, 3.2, 8), matTrunk);
    trunk.position.set(x, 1.6, z);
    trunk.rotation.z = 0.08;
    trunk.castShadow = true;
    scene.add(trunk);
    for (let i = 0; i < 6; i++) {
      const leaf = new THREE.Mesh(new THREE.ConeGeometry(0.25, 2.2, 4), matPalm);
      leaf.position.set(x, 3.1, z);
      leaf.rotation.z = Math.PI / 2.4;
      leaf.rotation.y = (i / 6) * Math.PI * 2;
      leaf.castShadow = true;
      scene.add(leaf);
    }
  }
  palm(22, 4); palm(-22, -4); palm(4, 22); palm(-4, -22); palm(0, 55); palm(0, -55);

  // Высокие сторожевые башни по углам (визуальные ориентиры)
  [[-55, -55], [55, -55], [-55, 55], [55, 55]].forEach(([x, z]) => {
    addBox(4, 9, 4, x, 4.5, z, matSandstoneDark);
    addBox(4.6, 0.4, 4.6, x, 9.2, z, matSandstone);
  });

  // Дальняя зона для баннихопа — те же рампы, что и на тренировочной, но в песочной тематике
  const bhopZ = 60;
  const rampCount = 6;
  for (let i = 0; i < rampCount; i++) {
    const x = -18 + i * 7;
    const height = 1 + i * 0.6;
    addBox(4, 0.5, 4, x, height, bhopZ, matSandstone);
    if (i < rampCount - 1) {
      const rampMesh = addBox(4, 0.4, 4.2, x + 3.5, height + 0.4, bhopZ, matSandstone);
      rampMesh.rotation.x = -0.35;
    }
  }
}

function addBarrelDesert(x, y, z) {
  const geo = new THREE.CylinderGeometry(0.5, 0.5, 1.1, 16);
  const mat = new THREE.MeshStandardMaterial({ color: 0x6b5a3b, roughness: 0.7, metalness: 0.2 });
  const mesh = new THREE.Mesh(geo, mat);
  mesh.position.set(x, y, z);
  mesh.castShadow = true;
  mesh.receiveShadow = true;
  scene.add(mesh);
  const box3 = new THREE.Box3().setFromObject(mesh);
  colliders.push({ mesh, box3 });
}

function addCrateStackDesert(cx, cy, cz, count, mat) {
  const positions = [
    [0, 0, 0], [1.1, 0, 0], [0.55, 1.1, 0], [0, 0, 1.1], [1.1, 0, 1.1]
  ];
  for (let i = 0; i < Math.min(count, positions.length); i++) {
    const [dx, dy, dz] = positions[i];
    addBox(1, 1, 1, cx + dx, cy + 0.5 + dy, cz + dz, mat);
  }
}

// ---------- AK-47 (процедурная модель, не текстуры из игр) ----------
function buildAK47() {
  const group = new THREE.Group();

  const woodMat = new THREE.MeshStandardMaterial({ color: 0x6b3f1d, roughness: 0.6 });
  const steelMat = new THREE.MeshStandardMaterial({ color: 0x2b2b2b, roughness: 0.35, metalness: 0.7 });
  const magMat = new THREE.MeshStandardMaterial({ color: 0x1c1c1c, roughness: 0.4, metalness: 0.5 });

  // Ствольная коробка (receiver)
  const receiver = new THREE.Mesh(new THREE.BoxGeometry(0.07, 0.09, 0.55), steelMat);
  receiver.position.set(0, 0, 0);
  group.add(receiver);

  // Приклад
  const stock = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.08, 0.28), woodMat);
  stock.position.set(0, -0.01, 0.38);
  group.add(stock);

  // Цевьё
  const handguard = new THREE.Mesh(new THREE.BoxGeometry(0.065, 0.07, 0.22), woodMat);
  handguard.position.set(0, -0.02, -0.32);
  group.add(handguard);

  // Ствол
  const barrel = new THREE.Mesh(new THREE.CylinderGeometry(0.014, 0.014, 0.42, 12), steelMat);
  barrel.rotation.x = Math.PI / 2;
  barrel.position.set(0, 0.01, -0.62);
  group.add(barrel);

  // Дульный тормоз
  const muzzleTip = new THREE.Mesh(new THREE.CylinderGeometry(0.018, 0.018, 0.06, 12), steelMat);
  muzzleTip.rotation.x = Math.PI / 2;
  muzzleTip.position.set(0, 0.01, -0.84);
  group.add(muzzleTip);

  // Изогнутый магазин (несколько сегментов для изгиба)
  const magGroup = new THREE.Group();
  const segCount = 5;
  for (let i = 0; i < segCount; i++) {
    const seg = new THREE.Mesh(new THREE.BoxGeometry(0.045, 0.05, 0.05), magMat);
    const t = i / (segCount - 1);
    seg.position.set(0, -0.12 - t * 0.16, -0.02 + Math.sin(t * 1.2) * 0.07);
    seg.rotation.x = -t * 0.5;
    magGroup.add(seg);
  }
  group.add(magGroup);

  // Мушка/прицел
  const foreSight = new THREE.Mesh(new THREE.BoxGeometry(0.012, 0.03, 0.012), steelMat);
  foreSight.position.set(0, 0.05, -0.78);
  group.add(foreSight);
  const rearSight = new THREE.Mesh(new THREE.BoxGeometry(0.02, 0.025, 0.012), steelMat);
  rearSight.position.set(0, 0.05, -0.1);
  group.add(rearSight);

  // Рукоятка
  const grip = new THREE.Mesh(new THREE.BoxGeometry(0.045, 0.16, 0.06), woodMat);
  grip.position.set(0, -0.11, 0.12);
  grip.rotation.x = 0.35;
  group.add(grip);

  group.traverse(o => { if (o.isMesh) { o.castShadow = true; } });

  // Точка дульной вспышки (в мировых координатах вычисляется отдельно)
  const muzzlePoint = new THREE.Object3D();
  muzzlePoint.position.set(0, 0.01, -0.87);
  group.add(muzzlePoint);
  group.userData.muzzlePoint = muzzlePoint;

  return group;
}

const weaponGroup = buildAK47();
weaponGroup.position.set(0.22, -0.22, -0.45);
weaponGroup.rotation.y = Math.PI;
camera.add(weaponGroup);
scene.add(camera);

// Дульная вспышка
const flashLight = new THREE.PointLight(0xffb347, 0, 4, 2);
weaponGroup.add(flashLight);
flashLight.position.copy(weaponGroup.userData.muzzlePoint.position);

const flashGeo = new THREE.ConeGeometry(0.03, 0.12, 8);
const flashMat = new THREE.MeshBasicMaterial({ color: 0xffcc66, transparent: true, opacity: 0 });
const flashMesh = new THREE.Mesh(flashGeo, flashMat);
flashMesh.rotation.x = -Math.PI / 2;
flashMesh.position.copy(weaponGroup.userData.muzzlePoint.position);
flashMesh.position.z -= 0.05;
weaponGroup.add(flashMesh);

// ---------- Игрок: физика движения (Source-style, с баннихопом) ----------
const player = {
  pos: new THREE.Vector3(0, 2, 0),
  vel: new THREE.Vector3(0, 0, 0),
  yaw: 0,
  pitch: 0,
  onGround: false,
  height: 1.7,
  radius: 0.35,
  health: 100,
  alive: true,
  ammo: 30,
  reserve: Infinity, // бесконечные патроны
  reloading: false,
  jumpQueued: false
};

const keys = {};
let pointerLocked = false;

document.addEventListener('keydown', (e) => {
  keys[e.code] = true;
  if (e.code === 'Space' && player.onGround) {
    player.jumpQueued = true;
  }
  if (e.code === 'KeyR') startReload();
  if (e.code === 'KeyT') {
    const chatInput = document.getElementById('chatInput');
    if (document.activeElement !== chatInput) {
      chatInput.style.display = 'block';
      chatInput.style.pointerEvents = 'auto';
      chatInput.focus();
    }
  }
  if (e.code === 'Escape') document.exitPointerLock();
  if (e.code === 'Tab') {
    e.preventDefault();
    document.getElementById('scoreboard').classList.remove('hidden');
    updateScoreboard();
  }
});
document.addEventListener('keyup', (e) => {
  keys[e.code] = false;
  if (e.code === 'Tab') document.getElementById('scoreboard').classList.add('hidden');
});

document.getElementById('chatInput').addEventListener('keydown', (e) => {
  if (e.code === 'Enter') {
    const val = e.target.value.trim();
    if (val) socket.emit('chat', val);
    e.target.value = '';
    e.target.style.display = 'none';
    e.target.style.pointerEvents = 'none';
    e.target.blur();
  } else if (e.code === 'Escape') {
    e.target.style.display = 'none';
    e.target.style.pointerEvents = 'none';
    e.target.blur();
  }
});

// ---------- Мышь / Pointer Lock / Тач-управление ----------
const canvas = renderer.domElement;
const lockHint = document.getElementById('crosshairLockHint');

const isTouchDevice = ('ontouchstart' in window) || navigator.maxTouchPoints > 0 || window.matchMedia('(pointer: coarse)').matches;
let touchControlsActive = false;

function requestLock() { canvas.requestPointerLock(); }

document.getElementById('playBtn').addEventListener('click', () => {
  const name = document.getElementById('nameInput').value || 'Player';
  const mapId = document.getElementById('mapSelect').value;
  document.getElementById('menu').classList.add('hidden');
  document.getElementById('hud').classList.remove('hidden');
  socket.emit('join', { name, mapId });

  if (isTouchDevice) {
    document.getElementById('touchControls').classList.remove('hidden');
    touchControlsActive = true;
  } else {
    requestLock();
  }
});

canvas.addEventListener('click', () => {
  if (isTouchDevice) return; // на тач-устройствах стрельба идёт через кнопку, а не клик по канвасу
  if (!pointerLocked && !document.getElementById('menu').classList.contains('hidden')) return;
  if (!pointerLocked) requestLock();
  else if (player.alive) shoot();
});

document.addEventListener('pointerlockchange', () => {
  pointerLocked = document.pointerLockElement === canvas;
  lockHint.classList.toggle('hidden', pointerLocked || isTouchDevice);
  if (document.getElementById('menu').classList.contains('hidden') === false) {
    lockHint.classList.add('hidden');
  }
});

const MOUSE_SENS = 0.0022;
document.addEventListener('mousemove', (e) => {
  if (!pointerLocked) return;
  player.yaw -= e.movementX * MOUSE_SENS;
  player.pitch -= e.movementY * MOUSE_SENS;
  player.pitch = Math.max(-Math.PI / 2 + 0.05, Math.min(Math.PI / 2 - 0.05, player.pitch));
  applyRecoilToView(); // отдача аккуратно суммируется поверх
});

let mouseHeld = false;
document.addEventListener('mousedown', (e) => { if (e.button === 0) mouseHeld = true; });
document.addEventListener('mouseup', (e) => { if (e.button === 0) mouseHeld = false; });

// ================= СЕНСОРНОЕ УПРАВЛЕНИЕ (телефон/планшет) =================
// Джойстик слева — движение, свайп справа — обзор камерой,
// отдельные кнопки — стрельба/прыжок/перезарядка.
const touchMoveVector = { x: 0, y: 0 }; // x: право(+)/лево(-), y: вперёд(+)/назад(-)

if (isTouchDevice) {
  const joystickZone = document.getElementById('joystickZone');
  const joystickBase = document.getElementById('joystickBase');
  const joystickStick = document.getElementById('joystickStick');
  const lookZone = document.getElementById('lookZone');

  let joystickTouchId = null;
  let joystickOriginX = 0, joystickOriginY = 0;
  const JOYSTICK_RADIUS = 55;

  joystickZone.addEventListener('touchstart', (e) => {
    const t = e.changedTouches[0];
    if (joystickTouchId !== null) return;
    joystickTouchId = t.identifier;
    joystickOriginX = t.clientX;
    joystickOriginY = t.clientY;
    joystickBase.style.display = 'block';
    joystickBase.style.left = (t.clientX - 55) + 'px';
    joystickBase.style.top = (t.clientY - 55) + 'px';
    joystickStick.style.top = '30px';
    joystickStick.style.left = '30px';
  }, { passive: true });

  joystickZone.addEventListener('touchmove', (e) => {
    for (const t of e.changedTouches) {
      if (t.identifier !== joystickTouchId) continue;
      let dx = t.clientX - joystickOriginX;
      let dy = t.clientY - joystickOriginY;
      const dist = Math.min(Math.hypot(dx, dy), JOYSTICK_RADIUS);
      const angle = Math.atan2(dy, dx);
      dx = Math.cos(angle) * dist;
      dy = Math.sin(angle) * dist;
      joystickStick.style.left = (30 + dx) + 'px';
      joystickStick.style.top = (30 + dy) + 'px';
      touchMoveVector.x = dx / JOYSTICK_RADIUS;
      touchMoveVector.y = -dy / JOYSTICK_RADIUS; // вверх на экране = вперёд
    }
  }, { passive: true });

  function endJoystick(e) {
    for (const t of e.changedTouches) {
      if (t.identifier !== joystickTouchId) continue;
      joystickTouchId = null;
      joystickBase.style.display = 'none';
      touchMoveVector.x = 0;
      touchMoveVector.y = 0;
    }
  }
  joystickZone.addEventListener('touchend', endJoystick, { passive: true });
  joystickZone.addEventListener('touchcancel', endJoystick, { passive: true });

  // Обзор камерой свайпом по правой части экрана
  let lookTouchId = null;
  let lastLookX = 0, lastLookY = 0;
  const TOUCH_LOOK_SENS = 0.0055;

  lookZone.addEventListener('touchstart', (e) => {
    const t = e.changedTouches[0];
    if (lookTouchId !== null) return;
    lookTouchId = t.identifier;
    lastLookX = t.clientX;
    lastLookY = t.clientY;
  }, { passive: true });

  lookZone.addEventListener('touchmove', (e) => {
    for (const t of e.changedTouches) {
      if (t.identifier !== lookTouchId) continue;
      const dx = t.clientX - lastLookX;
      const dy = t.clientY - lastLookY;
      lastLookX = t.clientX;
      lastLookY = t.clientY;
      player.yaw -= dx * TOUCH_LOOK_SENS;
      player.pitch -= dy * TOUCH_LOOK_SENS;
      player.pitch = Math.max(-Math.PI / 2 + 0.05, Math.min(Math.PI / 2 - 0.05, player.pitch));
    }
  }, { passive: true });

  function endLook(e) {
    for (const t of e.changedTouches) {
      if (t.identifier === lookTouchId) lookTouchId = null;
    }
  }
  lookZone.addEventListener('touchend', endLook, { passive: true });
  lookZone.addEventListener('touchcancel', endLook, { passive: true });

  // Кнопки
  const fireBtn = document.getElementById('touchFire');
  fireBtn.addEventListener('touchstart', (e) => { e.preventDefault(); mouseHeld = true; }, { passive: false });
  fireBtn.addEventListener('touchend', (e) => { e.preventDefault(); mouseHeld = false; }, { passive: false });
  fireBtn.addEventListener('touchcancel', () => { mouseHeld = false; });

  const jumpBtn = document.getElementById('touchJump');
  jumpBtn.addEventListener('touchstart', (e) => {
    e.preventDefault();
    if (player.onGround) player.jumpQueued = true;
  }, { passive: false });

  const reloadBtn = document.getElementById('touchReload');
  reloadBtn.addEventListener('touchstart', (e) => { e.preventDefault(); startReload(); }, { passive: false });
}



// =========================================================
// СИСТЕМА ОТДАЧИ AK-47 (spray-паттерн, как в CS)
// Каждый выстрел даёт смещение по pitch (вверх) и yaw (в сторону),
// паттерн нарастает по мере продолжения очереди и плавно восстанавливается,
// когда игрок отпускает гашетку.
// =========================================================
const AK_RECOIL_PATTERN = (() => {
  // 30 патронов: сначала строго вверх, затем расхождение вправо-влево с нарастанием (классика AK spray)
  const pattern = [];
  for (let i = 0; i < 30; i++) {
    let up, side;
    if (i < 4) {
      up = 0.9 + i * 0.15;
      side = (Math.random() - 0.5) * 0.08;
    } else if (i < 12) {
      up = 1.5 + (i - 4) * 0.05;
      side = 0.35 + (i - 4) * 0.12 + (Math.random() - 0.5) * 0.15;
    } else if (i < 20) {
      up = 1.9 - (i - 12) * 0.02;
      side = 1.3 - (i - 12) * 0.18 + (Math.random() - 0.5) * 0.2;
    } else {
      up = 1.7 + (Math.random() - 0.5) * 0.3;
      side = (Math.random() - 0.5) * 1.4;
    }
    pattern.push({ up: up * 0.011, side: side * 0.011 });
  }
  return pattern;
})();

let recoilIndex = 0;
let recoilOffsetPitch = 0; // накопленное смещение прицела от отдачи
let recoilOffsetYaw = 0;
let lastShotTime = 0;
const RECOIL_RECOVERY_DELAY = 180; // мс после последнего выстрела, когда начинается восстановление
const RECOIL_RECOVERY_SPEED = 0.09; // скорость возврата камеры

function applyRecoilToView() {
  // применяется вместе с движением мыши в render loop, ничего доп. тут не требуется
}

function fireRecoilKick() {
  const step = AK_RECOIL_PATTERN[Math.min(recoilIndex, AK_RECOIL_PATTERN.length - 1)];
  recoilOffsetPitch += step.up;
  recoilOffsetYaw += step.side;
  recoilIndex++;
  lastShotTime = performance.now();
}

function updateRecoilRecovery(dt) {
  const since = performance.now() - lastShotTime;
  if (since > RECOIL_RECOVERY_DELAY) {
    recoilOffsetPitch *= Math.pow(1 - RECOIL_RECOVERY_SPEED, dt * 60);
    recoilOffsetYaw *= Math.pow(1 - RECOIL_RECOVERY_SPEED, dt * 60);
    if (Math.abs(recoilOffsetPitch) < 0.0005) recoilOffsetPitch = 0;
    if (Math.abs(recoilOffsetYaw) < 0.0005) recoilOffsetYaw = 0;
    if (recoilOffsetPitch === 0 && recoilOffsetYaw === 0) recoilIndex = 0;
  }
}

// ---------- Стрельба ----------
const RATE_OF_FIRE = 100; // мс между выстрелами (~600 RPM как у AK-47)
let lastFireTime = 0;
const raycaster = new THREE.Raycaster();
const otherPlayerMeshes = {}; // id -> group (для попаданий)

function startReload() {
  // Бесконечные патроны: перезарядка мгновенно заполняет магазин, резерв не тратится
  if (player.reloading || player.ammo === 30) return;
  player.reloading = true;
  setTimeout(() => {
    player.ammo = 30;
    player.reloading = false;
    updateAmmoHUD();
  }, 1600);
}

function shoot() {
  if (!player.alive || player.reloading) return;
  const now = performance.now();
  if (now - lastFireTime < RATE_OF_FIRE) return;
  if (player.ammo <= 0) { startReload(); return; }
  lastFireTime = now;
  player.ammo--; // магазин по-прежнему ограничен на 30, но резерв бесконечен — патроны никогда не заканчиваются насовсем
  updateAmmoHUD();

  fireRecoilKick();
  muzzleFlash();

  // Направление выстрела берётся из камеры (с учётом уже применённой отдачи в render loop)
  const dir = new THREE.Vector3();
  camera.getWorldDirection(dir);
  const origin = camera.getWorldPosition(new THREE.Vector3());

  raycaster.set(origin, dir);
  raycaster.far = 200;

  const targets = Object.values(otherPlayerMeshes);
  const meshList = [];
  targets.forEach(g => g.traverse(o => { if (o.isMesh) meshList.push(o); }));

  const hits = raycaster.intersectObjects(meshList, false);
  const envHits = raycaster.intersectObjects(colliders.map(c => c.mesh), false);

  let validHit = null;
  if (hits.length > 0) {
    const envDist = envHits.length > 0 ? envHits[0].distance : Infinity;
    if (hits[0].distance < envDist) validHit = hits[0];
  }

  if (validHit) {
    let obj = validHit.object;
    while (obj && !obj.userData.playerId) obj = obj.parent;
    if (obj) {
      const dmg = obj.userData.headHit && validHit.object === obj.userData.headHit ? 60 : 27;
      socket.emit('hit', { targetId: obj.userData.playerId, damage: dmg });
    }
  }

  socket.emit('shoot', { origin: { x: origin.x, y: origin.y, z: origin.z }, dir: { x: dir.x, y: dir.y, z: dir.z } });
}

function muzzleFlash() {
  flashLight.intensity = 6;
  flashMat.opacity = 1;
  setTimeout(() => { flashLight.intensity = 0; flashMat.opacity = 0; }, 45);

  // лёгкая тряска камеры/оружия визуально
  weaponGroup.position.z += 0.03;
  setTimeout(() => { weaponGroup.position.z -= 0.03; }, 40);
}

// ---------- Движение (WASD) + баннихоп физика ----------
const GRAVITY = 20;
const JUMP_SPEED = 7.2;
const GROUND_ACCEL = 70;
const AIR_ACCEL = 60;          // высокое ускорение в воздухе даёт возможность бхопа/страйфа
const MAX_GROUND_SPEED = 6.5;
const MAX_AIR_WISH_SPEED = 1.6; // ограничение "wish speed" в воздухе как в Source (даёт speedgain через strafe)
const FRICTION = 6.5;

function getWishDir() {
  const forward = new THREE.Vector3(Math.sin(player.yaw), 0, Math.cos(player.yaw)).negate();
  const right = new THREE.Vector3(Math.cos(player.yaw), 0, -Math.sin(player.yaw));
  const wish = new THREE.Vector3();
  if (keys['KeyW']) wish.add(forward);
  if (keys['KeyS']) wish.sub(forward);
  if (keys['KeyD']) wish.add(right);
  if (keys['KeyA']) wish.sub(right);

  // Виртуальный джойстик (мобильные устройства)
  if (isTouchDevice) {
    wish.addScaledVector(forward, touchMoveVector.y);
    wish.addScaledVector(right, touchMoveVector.x);
  }

  if (wish.lengthSq() > 1) wish.normalize();
  return wish;
}

function applyFriction(dt) {
  const speed = Math.hypot(player.vel.x, player.vel.z);
  if (speed < 0.05) { player.vel.x = 0; player.vel.z = 0; return; }
  const drop = speed * FRICTION * dt;
  const newSpeed = Math.max(speed - drop, 0);
  const scale = newSpeed / speed;
  player.vel.x *= scale;
  player.vel.z *= scale;
}

function accelerate(wishDir, wishSpeed, accel, dt) {
  const currentSpeed = player.vel.x * wishDir.x + player.vel.z * wishDir.z;
  const addSpeed = wishSpeed - currentSpeed;
  if (addSpeed <= 0) return;
  let accelSpeed = accel * dt * wishSpeed;
  if (accelSpeed > addSpeed) accelSpeed = addSpeed;
  player.vel.x += accelSpeed * wishDir.x;
  player.vel.z += accelSpeed * wishDir.z;
}

function updateMovement(dt) {
  const wishDir = getWishDir();

  if (player.onGround) {
    applyFriction(dt);
    const wishSpeed = MAX_GROUND_SPEED * (keys['ShiftLeft'] ? 1.35 : 1);
    accelerate(wishDir, wishSpeed, GROUND_ACCEL, dt);

    if (player.jumpQueued) {
      player.vel.y = JUMP_SPEED;
      player.onGround = false;
      player.jumpQueued = false;
      // ВАЖНО: friction не применяется в момент прыжка — это и есть основа баннихопа:
      // горизонтальная скорость, набранная на земле, полностью переносится в прыжок.
    }
  } else {
    // В воздухе — ограниченный wishSpeed, но накопленная скорость (vel) не режется трением,
    // что позволяет наращивать скорость через постоянный strafe (A/D + движение мыши), как в CS/Source.
    accelerate(wishDir, MAX_AIR_WISH_SPEED, AIR_ACCEL, dt);
    player.vel.y -= GRAVITY * dt;
    player.jumpQueued = false;
  }

  // Интеграция позиции
  const nextPos = player.pos.clone();
  nextPos.x += player.vel.x * dt;
  nextPos.z += player.vel.z * dt;
  nextPos.y += player.vel.y * dt;

  resolveCollisions(nextPos);
  player.pos.copy(nextPos);
}

function resolveCollisions(nextPos) {
  // Пол
  if (nextPos.y <= 1.7) {
    nextPos.y = 1.7;
    player.vel.y = 0;
    player.onGround = true;
  } else {
    player.onGround = false;
  }

  // Простое AABB столкновение с объектами карты (по XZ, плюс проверка приземления сверху)
  const r = player.radius;
  for (const c of colliders) {
    const b = c.box3;
    const withinX = nextPos.x + r > b.min.x && nextPos.x - r < b.max.x;
    const withinZ = nextPos.z + r > b.min.z && nextPos.z - r < b.max.z;
    const feetY = nextPos.y - 1.7;
    const headY = nextPos.y;

    if (withinX && withinZ) {
      // Приземление на верх объекта
      if (player.vel.y <= 0 && feetY >= b.max.y - 0.35 && feetY <= b.max.y + 0.4) {
        nextPos.y = b.max.y + 1.7;
        player.vel.y = 0;
        player.onGround = true;
        continue;
      }
      // Боковое столкновение — выталкивание
      if (feetY < b.max.y - 0.1 && headY > b.min.y) {
        const dx = nextPos.x - c.mesh.position.x;
        const dz = nextPos.z - c.mesh.position.z;
        if (Math.abs(dx) > Math.abs(dz)) {
          nextPos.x = c.mesh.position.x + Math.sign(dx) * ((b.max.x - b.min.x) / 2 + r);
          player.vel.x = 0;
        } else {
          nextPos.z = c.mesh.position.z + Math.sign(dz) * ((b.max.z - b.min.z) / 2 + r);
          player.vel.z = 0;
        }
      }
    }
  }

  // Границы арены
  nextPos.x = Math.max(-68, Math.min(68, nextPos.x));
  nextPos.z = Math.max(-68, Math.min(68, nextPos.z));
}

// ---------- Прочие игроки ----------
function createOtherPlayerMesh(name) {
  const group = new THREE.Group();
  const bodyMat = new THREE.MeshStandardMaterial({ color: 0x3b6ea5, roughness: 0.7 });
  const body = new THREE.Mesh(new THREE.CapsuleGeometry(0.35, 1.0, 4, 8), bodyMat);
  body.position.y = 0.9;
  body.castShadow = true;
  group.add(body);

  const head = new THREE.Mesh(new THREE.SphereGeometry(0.28, 12, 12), bodyMat);
  head.position.y = 1.65;
  head.castShadow = true;
  group.add(head);
  group.userData.headHit = head;

  // Мини-табличка с ником
  scene.add(group);
  return group;
}

// ---------- Сеть (Socket.io) ----------
const socket = io();
let myId = null;

socket.on('init', (data) => {
  myId = data.id;
  buildMap(data.mapId || 'training');
  Object.values(data.players).forEach(p => {
    if (p.id !== myId) spawnRemotePlayer(p);
  });
});

socket.on('playerJoined', (p) => {
  if (p.id !== myId) spawnRemotePlayer(p);
});

function spawnRemotePlayer(p) {
  if (otherPlayerMeshes[p.id]) return;
  const mesh = createOtherPlayerMesh(p.name);
  mesh.userData.playerId = p.id;
  mesh.position.set(p.pos.x, p.pos.y - 1.7, p.pos.z);
  otherPlayerMeshes[p.id] = mesh;
}

socket.on('playerMoved', (data) => {
  const m = otherPlayerMeshes[data.id];
  if (!m) return;
  m.position.set(data.pos.x, data.pos.y - 1.7, data.pos.z);
  m.rotation.y = data.rot.yaw;
});

socket.on('playerLeft', (data) => {
  const m = otherPlayerMeshes[data.id];
  if (m) { scene.remove(m); delete otherPlayerMeshes[data.id]; }
});

socket.on('playerDamaged', (data) => {
  if (data.id === myId) {
    player.health = data.health;
    updateHealthHUD();
    flashDamageVignette();
  }
});

socket.on('playerKilled', (data) => {
  addKillfeed(data.killerName, data.victimName);
  if (data.victim === myId) {
    player.alive = false;
    document.getElementById('deathScreen').classList.remove('hidden');
    let t = 3;
    document.getElementById('respawnTimer').textContent = t;
    const interval = setInterval(() => {
      t--;
      document.getElementById('respawnTimer').textContent = Math.max(t, 0);
      if (t <= 0) clearInterval(interval);
    }, 1000);
  }
});

socket.on('playerRespawn', (data) => {
  if (data.id === myId) {
    player.pos.set(data.pos.x, data.pos.y, data.pos.z);
    player.vel.set(0, 0, 0);
    player.health = data.health;
    player.alive = true;
    updateHealthHUD();
    document.getElementById('deathScreen').classList.add('hidden');
  } else {
    const m = otherPlayerMeshes[data.id];
    if (m) m.position.set(data.pos.x, data.pos.y - 1.7, data.pos.z);
  }
});

socket.on('chat', (data) => {
  const box = document.getElementById('chatMessages');
  const el = document.createElement('div');
  el.className = 'chat-msg';
  el.textContent = `${data.name}: ${data.msg}`;
  box.appendChild(el);
  while (box.children.length > 6) box.removeChild(box.firstChild);
  setTimeout(() => el.remove(), 8000);
});

let lastNetSend = 0;
function sendNetworkUpdate() {
  const now = performance.now();
  if (now - lastNetSend < 50) return; // 20 Гц
  lastNetSend = now;
  const speed = Math.hypot(player.vel.x, player.vel.z);
  socket.emit('move', {
    pos: { x: player.pos.x, y: player.pos.y, z: player.pos.z },
    rot: { yaw: player.yaw, pitch: player.pitch },
    speed
  });
}

// ---------- HUD ----------
function updateHealthHUD() {
  document.getElementById('healthValue').textContent = Math.max(0, Math.round(player.health));
}
function updateAmmoHUD() {
  document.getElementById('ammoValue').textContent = `${player.ammo} / ∞`;
}
function addKillfeed(killer, victim) {
  const feed = document.getElementById('killfeed');
  const el = document.createElement('div');
  el.className = 'kill-entry';
  el.textContent = `${killer}  ➤  ${victim}`;
  feed.appendChild(el);
  setTimeout(() => el.remove(), 4000);
}
function flashDamageVignette() {
  document.body.style.transition = 'none';
  document.body.style.boxShadow = 'inset 0 0 150px 40px rgba(255,0,0,0.5)';
  requestAnimationFrame(() => {
    document.body.style.transition = 'box-shadow 0.4s';
    document.body.style.boxShadow = 'none';
  });
}
function updateScoreboard() {
  // упрощённый скорборд по известным игрокам
  const board = document.getElementById('scoreboard');
  board.innerHTML = '<table><tr><th>Игрок</th></tr>' +
    Object.values(otherPlayerMeshes).map(m => `<tr><td>${m.userData.playerId}</td></tr>`).join('') +
    '</table>';
}

// ---------- Главный цикл рендера ----------
let lastTime = performance.now();

function animate() {
  requestAnimationFrame(animate);
  const now = performance.now();
  const dt = Math.min((now - lastTime) / 1000, 0.05);
  lastTime = now;

  if ((pointerLocked || touchControlsActive) && player.alive) {
    updateMovement(dt);
    if (mouseHeld) shoot();
  }

  updateRecoilRecovery(dt);

  // Камера: позиция игрока + смещение отдачи по pitch/yaw
  camera.position.set(player.pos.x, player.pos.y, player.pos.z);
  camera.rotation.order = 'YXZ';
  camera.rotation.y = player.yaw + recoilOffsetYaw;
  camera.rotation.x = player.pitch + recoilOffsetPitch;

  document.getElementById('speedValue').textContent = Math.round(Math.hypot(player.vel.x, player.vel.z) * 50);

  sendNetworkUpdate();
  renderer.render(scene, camera);
}
animate();
* { margin:0; padding:0; box-sizing:border-box; }
html, body { width:100%; height:100%; overflow:hidden; background:#0a0c10; font-family:'Segoe UI', Arial, sans-serif; }
canvas { display:block; }

#menu {
  position:fixed; inset:0;
  display:flex; align-items:center; justify-content:center;
  background: radial-gradient(circle at 50% 30%, #1b2230 0%, #05070a 80%);
  z-index:50;
}
.menu-box {
  background: rgba(15,18,24,0.85);
  border: 1px solid #2c3646;
  padding: 40px 50px;
  border-radius: 10px;
  text-align:center;
  box-shadow: 0 0 40px rgba(255,140,0,0.08);
}
.menu-box h1 { color:#eee; letter-spacing:3px; font-size:34px; }
.menu-box h1 span { color:#ff8c00; }
.subtitle { color:#8a93a3; margin:10px 0 25px; font-size:13px; }
#nameInput {
  width:100%; padding:12px; border-radius:6px; border:1px solid #333c4a;
  background:#11151c; color:#fff; font-size:15px; margin-bottom:15px; outline:none;
}
#mapSelect {
  width:100%; padding:11px; border-radius:6px; border:1px solid #333c4a;
  background:#11151c; color:#fff; font-size:14px; margin-bottom:8px; outline:none; cursor:pointer;
}
.map-note { color:#5c6576; font-size:11px; margin-bottom:15px; text-align:left; }

#playBtn {
  width:100%; padding:14px; border:none; border-radius:6px;
  background:#ff8c00; color:#101010; font-weight:bold; font-size:16px;
  cursor:pointer; letter-spacing:2px; transition:0.15s;
}
#playBtn:hover { background:#ffa733; }
.hints { margin-top:25px; color:#6c7686; font-size:12px; line-height:1.8; text-align:left; }
.hints b { color:#9aa4b5; }

.hidden { display:none !important; }

#hud { position:fixed; inset:0; pointer-events:none; z-index:10; }

#crosshair {
  position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
  width:24px; height:24px;
}
.ch-line { position:absolute; background:#00ff88; }
.ch-top { top:0; left:50%; width:2px; height:8px; transform:translateX(-50%); }
.ch-bottom { bottom:0; left:50%; width:2px; height:8px; transform:translateX(-50%); }
.ch-left { left:0; top:50%; width:8px; height:2px; transform:translateY(-50%); }
.ch-right { right:0; top:50%; width:8px; height:2px; transform:translateY(-50%); }

#healthBar, #ammoBar, #speedBar {
  position:absolute; bottom:25px; color:#fff; font-weight:bold; font-size:24px;
  display:flex; align-items:center; gap:10px; text-shadow:0 2px 4px rgba(0,0,0,0.8);
}
#healthBar { left:30px; color:#ff5757; }
#ammoBar { right:30px; flex-direction:row-reverse; color:#ffd257; }
#speedBar { left:50%; transform:translateX(-50%); bottom:70px; font-size:14px; color:#8fd6ff; opacity:0.85; }
.stat-icon { font-size:14px; opacity:0.7; }

#killfeed {
  position:absolute; top:20px; right:20px; display:flex; flex-direction:column; gap:6px;
  align-items:flex-end;
}
.kill-entry {
  background:rgba(10,12,16,0.7); color:#ddd; padding:6px 12px; border-radius:4px;
  font-size:13px; border-left:3px solid #ff8c00;
}

#chatBox { position:absolute; left:30px; bottom:110px; width:340px; pointer-events:none; }
#chatMessages { display:flex; flex-direction:column; gap:3px; margin-bottom:6px; max-height:150px; overflow:hidden; }
.chat-msg { background:rgba(10,12,16,0.55); color:#cfd6e0; padding:4px 8px; border-radius:3px; font-size:12px; }
#chatInput {
  width:100%; padding:8px; border-radius:4px; border:1px solid #333c4a;
  background:rgba(10,12,16,0.85); color:#fff; font-size:13px; outline:none;
  display:none; pointer-events:auto;
}

#deathScreen {
  position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center;
  background:rgba(120,0,0,0.25); color:#fff; font-size:42px; font-weight:bold; letter-spacing:4px;
}
.respawn-text { font-size:16px; margin-top:14px; letter-spacing:1px; color:#ffd; }

#crosshairLockHint {
  position:fixed; inset:0; display:flex; align-items:center; justify-content:center;
  color:#fff; font-size:20px; background:rgba(0,0,0,0.35); z-index:20; cursor:pointer;
}

#scoreboard {
  position:absolute; top:20px; left:50%; transform:translateX(-50%);
  background:rgba(10,12,16,0.85); color:#fff; padding:14px 20px; border-radius:6px;
  font-size:13px; min-width:280px;
}
#scoreboard table { width:100%; border-collapse:collapse; }
#scoreboard th { text-align:left; color:#ff8c00; padding-bottom:6px; font-size:12px; }
#scoreboard td { padding:3px 6px; }

/* ================= СЕНСОРНОЕ УПРАВЛЕНИЕ (мобильные) ================= */
#touchControls {
  position:fixed; inset:0; z-index:15; touch-action:none;
}
#lookZone {
  position:absolute; top:0; right:0; width:60%; height:100%;
  pointer-events:auto;
}
#joystickZone {
  position:absolute; left:0; bottom:0; width:45%; height:55%;
  pointer-events:auto;
}
#joystickBase {
  position:fixed; width:110px; height:110px; border-radius:50%;
  background:rgba(255,255,255,0.08); border:2px solid rgba(255,255,255,0.25);
  display:none; z-index:16;
}
#joystickStick {
  position:absolute; width:50px; height:50px; border-radius:50%;
  background:rgba(255,140,0,0.55); border:2px solid rgba(255,255,255,0.5);
  top:30px; left:30px; pointer-events:none;
}
#touchButtons {
  position:absolute; right:18px; bottom:18px; display:flex; align-items:flex-end; gap:14px;
  pointer-events:auto;
}
.touch-btn {
  border-radius:50%; border:2px solid rgba(255,255,255,0.4);
  background:rgba(20,24,30,0.55); color:#fff; font-size:20px; font-weight:bold;
  display:flex; align-items:center; justify-content:center; user-select:none;
  -webkit-user-select:none; touch-action:none;
}
.touch-btn.large { width:82px; height:82px; background:rgba(255,80,50,0.4); border-color:rgba(255,120,90,0.7); font-size:26px; }
.touch-btn.medium { width:62px; height:62px; margin-bottom:6px; }
.touch-btn.small { width:46px; height:46px; margin-bottom:60px; font-size:15px; background:rgba(255,255,255,0.12); }
.touch-btn:active { background:rgba(255,140,0,0.5); }

/* ================= Адаптивность для маленьких экранов ================= */
@media (max-width: 820px), (pointer: coarse) {
  .menu-box { padding:28px 26px; width:88vw; max-width:380px; }
  .menu-box h1 { font-size:24px; letter-spacing:1.5px; }
  .subtitle { font-size:11px; }
  .hints { font-size:11px; line-height:1.6; }

  #healthBar, #ammoBar { font-size:18px; bottom:130px; }
  #healthBar { left:16px; }
  #ammoBar { right:16px; }
  #speedBar { display:none; }
  #killfeed { top:12px; right:12px; }
  .kill-entry { font-size:11px; padding:5px 9px; }
  #chatBox { left:16px; bottom:220px; width:60vw; max-width:260px; }
  #chatInput { font-size:16px; } /* 16px — чтобы iOS не зумил при фокусе */
  #crosshairLockHint { display:none !important; }
  #scoreboard { font-size:11px; min-width:220px; padding:10px 14px; }
}

игрок это синий овал