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


// ============================================================
//  ПВО — защита склада Яндекса. Three.js
// ============================================================
(() => {
"use strict";

// ---------- Константы ----------
const MAX_LIVES = 10;
const MAX_HITS_FOR_LOSE = 10;    // 10 попаданий = проигрыш
const LEVEL_DRONES = (lvl) => 5 + lvl * 5; // 10, 15, 20, ...
const DRONE_SPEED_BASE = 9;
const PROJECTILE_SPEED = 160;
const FIRE_COOLDOWN = 0.28;

// ---------- DOM ----------
const elLives = document.getElementById("lives");
const elLevel = document.getElementById("level");
const elKilled = document.getElementById("killed");
const elTotal = document.getElementById("total");
const elHits = document.getElementById("hits");
const overlay = document.getElementById("overlay");
const ovTitle = document.getElementById("ov-title");
const ovText = document.getElementById("ov-text");
const ovBtn = document.getElementById("ov-btn");

// ---------- Сцена ----------
const scene = new THREE.Scene();

// Летний вечер: тёплый градиент неба
const skyColorTop = new THREE.Color(0x2b3a6b);
const skyColorBottom = new THREE.Color(0xff9a5c);
scene.background = skyColorBottom.clone();
scene.fog = new THREE.Fog(0xd67a55, 120, 420);

const camera = new THREE.PerspectiveCamera(65, innerWidth / innerHeight, 0.1, 2000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);

// ---------- Свет ----------
const hemi = new THREE.HemisphereLight(0xffc79a, 0x2b1a3a, 0.85);
scene.add(hemi);

const sun = new THREE.DirectionalLight(0xffb070, 1.15);
sun.position.set(-90, 120, 60);
sun.castShadow = true;
sun.shadow.mapSize.set(2048, 2048);
sun.shadow.camera.left = -160;
sun.shadow.camera.right = 160;
sun.shadow.camera.top = 160;
sun.shadow.camera.bottom = -160;
sun.shadow.camera.far = 400;
scene.add(sun);

const ambient = new THREE.AmbientLight(0x8899cc, 0.35);
scene.add(ambient);

// ---------- Земля ----------
const ground = new THREE.Mesh(
  new THREE.PlaneGeometry(2000, 2000),
  new THREE.MeshStandardMaterial({ color: 0x3d4a35, roughness: 1 })
);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -0.01;
ground.receiveShadow = true;
scene.add(ground);

// ---------- Склад Яндекс ----------
const warehouse = new THREE.Group();
const WAREHOUSE_W = 60, WAREHOUSE_H = 18, WAREHOUSE_D = 40;
const roofY = WAREHOUSE_H;

// Корпус
const bodyMat = new THREE.MeshStandardMaterial({ color: 0xd9dde6, roughness: 0.8 });
const body = new THREE.Mesh(
  new THREE.BoxGeometry(WAREHOUSE_W, WAREHOUSE_H, WAREHOUSE_D),
  bodyMat
);
body.position.y = WAREHOUSE_H / 2;
body.castShadow = true;
body.receiveShadow = true;
warehouse.add(body);

// Крыша (плоская, чуть выступает)
const roof = new THREE.Mesh(
  new THREE.BoxGeometry(WAREHOUSE_W + 2, 1.2, WAREHOUSE_D + 2),
  new THREE.MeshStandardMaterial({ color: 0x7a8494, roughness: 0.85 })
);
roof.position.y = WAREHOUSE_H + 0.6;
roof.castShadow = true;
roof.receiveShadow = true;
warehouse.add(roof);

// Логотип "ЯНДЕКС" на фасаде (canvas-текстура)
function makeLabelTexture(text, bg = "#ffffff", fg = "#000000") {
  const c = document.createElement("canvas");
  c.width = 1024; c.height = 256;
  const ctx = c.getContext("2d");
  ctx.fillStyle = bg;
  ctx.fillRect(0, 0, c.width, c.height);
  ctx.fillStyle = fg;
  ctx.font = "bold 180px Arial";
  ctx.textAlign = "center";
  ctx.textBaseline = "middle";
  ctx.fillText(text, c.width / 2, c.height / 2 + 10);
  const tex = new THREE.CanvasTexture(c);
  tex.anisotropy = 8;
  return tex;
}

const labelTex = makeLabelTexture("ЯНДЕКС", "#ffffff", "#ff0000");
const labelMat = new THREE.MeshBasicMaterial({ map: labelTex, transparent: false });
const label = new THREE.Mesh(new THREE.PlaneGeometry(40, 10), labelMat);
label.position.set(0, WAREHOUSE_H * 0.6, WAREHOUSE_D / 2 + 0.05);
warehouse.add(label);

// Боковая надпись
const label2 = new THREE.Mesh(new THREE.PlaneGeometry(28, 7), labelMat);
label2.position.set(WAREHOUSE_W / 2 + 0.05, WAREHOUSE_H * 0.6, 0);
label2.rotation.y = Math.PI / 2;
warehouse.add(label2);

warehouse.position.set(0, 0, 0);
scene.add(warehouse);

// ---------- Зенитная установка ПВО ----------
const turret = new THREE.Group();
turret.position.set(0, roofY + 1.4, 0);
scene.add(turret);

// База
const baseGeo = new THREE.CylinderGeometry(3.2, 3.8, 1.6, 24);
const baseMat = new THREE.MeshStandardMaterial({ color: 0x3a4a2f, roughness: 0.7, metalness: 0.3 });
const baseMesh = new THREE.Mesh(baseGeo, baseMat);
baseMesh.castShadow = true;
baseMesh.receiveShadow = true;
turret.add(baseMesh);

// Вращающаяся часть (голова)
const turretHead = new THREE.Group();
turretHead.position.y = 1.2;
turret.add(turretHead);

// Корпус головы
const headMat = new THREE.MeshStandardMaterial({ color: 0x4b5a3a, roughness: 0.65, metalness: 0.35 });
const headMesh = new THREE.Mesh(new THREE.BoxGeometry(3.4, 2.2, 3.4), headMat);
headMesh.castShadow = true;
turretHead.add(headMesh);

// Ствол (две пушки)
const barrelMat = new THREE.MeshStandardMaterial({ color: 0x222622, roughness: 0.45, metalness: 0.7 });
const barrelGeo = new THREE.CylinderGeometry(0.22, 0.28, 6, 16);
for (const dx of [-0.7, 0.7]) {
  const barrel = new THREE.Mesh(barrelGeo, barrelMat);
  barrel.rotation.x = Math.PI / 2;
  barrel.position.set(dx, 0.4, 2.6);
  barrel.castShadow = true;
  turretHead.add(barrel);
}

// Надпись "ПВО" на стволе
const pvoTex = makeLabelTexture("ПВО", "#b81d1d", "#ffffff");
const pvoMat = new THREE.MeshBasicMaterial({ map: pvoTex });
const pvoSide = new THREE.Mesh(new THREE.PlaneGeometry(2.4, 0.6), pvoMat);
pvoSide.position.set(0, 1.25, 1.3);
turretHead.add(pvoSide);

const pvoBack = new THREE.Mesh(new THREE.PlaneGeometry(2.4, 0.6), pvoMat);
pvoBack.position.set(0, 1.25, -1.3);
pvoBack.rotation.y = Math.PI;
turretHead.add(pvoBack);

// Маленькая красная лампочка
const lamp = new THREE.Mesh(
  new THREE.SphereGeometry(0.18, 12, 12),
  new THREE.MeshBasicMaterial({ color: 0xff2200 })
);
lamp.position.set(1.4, 1.2, 0);
turretHead.add(lamp);

// ---------- Огоньки (костры от попаданий) ----------
const fires = [];
function spawnFireOnWarehouse(worldPos) {
  const g = new THREE.Group();

  // Огонь (конусы)
  const fireMat = new THREE.MeshBasicMaterial({ color: 0xff6a00, transparent: true, opacity: 0.9 });
  const fireMat2 = new THREE.MeshBasicMaterial({ color: 0xffd23f, transparent: true, opacity: 0.85 });
  for (let i = 0; i < 3; i++) {
    const h = 1.6 + Math.random() * 1.4;
    const cone = new THREE.Mesh(new THREE.ConeGeometry(0.7, h, 8), i === 0 ? fireMat : fireMat2);
    cone.position.y = h / 2 + i * 0.1;
    cone.position.x = (Math.random() - 0.5) * 0.6;
    cone.position.z = (Math.random() - 0.5) * 0.6;
    cone.userData.baseY = cone.position.y;
    cone.userData.phase = Math.random() * Math.PI * 2;
    g.add(cone);
  }

  // Дым
  const smokeMat = new THREE.MeshBasicMaterial({ color: 0x444444, transparent: true, opacity: 0.35 });
  for (let i = 0; i < 3; i++) {
    const s = new THREE.Mesh(new THREE.SphereGeometry(0.6 + Math.random() * 0.4, 8, 8), smokeMat);
    s.position.set((Math.random() - 0.5) * 1.5, 2 + i * 1.2, (Math.random() - 0.5) * 1.5);
    s.userData.phase = Math.random() * Math.PI * 2;
    g.add(s);
  }

  // Свет
  const light = new THREE.PointLight(0xff7a2a, 1.6, 18, 2);
  light.position.y = 1.5;
  g.add(light);

  g.position.copy(worldPos);
  scene.add(g);

  fires.push({
    group: g,
    age: 0,
    light,
    cones: g.children.filter(c => c.geometry && c.geometry.type === "ConeGeometry"),
    smokes: g.children.filter(c => c.geometry && c.geometry.type === "SphereGeometry"),
  });
}

function updateFires(dt, t) {
  for (const f of fires) {
    f.age += dt;
    const flick = 0.75 + Math.sin(t * 18 + f.age * 5) * 0.2 + Math.random() * 0.1;
    f.light.intensity = 1.2 * flick;
    for (const c of f.cones) {
      c.scale.y = 0.85 + flick * 0.4;
      c.position.y = c.userData.baseY * (0.9 + flick * 0.15);
    }
    for (const s of f.smokes) {
      s.position.y += dt * 0.9;
      s.material.opacity = Math.max(0, 0.35 - f.age * 0.02);
      if (s.position.y > 8) s.position.y = 2;
    }
  }
}

// ---------- Дроны ----------
const drones = [];
function createDrone() {
  const g = new THREE.Group();
  const mat = new THREE.MeshStandardMaterial({ color: 0x1a1a1a, roughness: 0.5, metalness: 0.6 });

  // Фюзеляж
  const bodyGeo = new THREE.CylinderGeometry(0.4, 0.9, 4.2, 10);
  const bodyM = new THREE.Mesh(bodyGeo, mat);
  bodyM.rotation.x = Math.PI / 2;
  bodyM.castShadow = true;
  g.add(bodyM);

  // Крылья (дельта)
  const wingGeo = new THREE.BufferGeometry();
  const verts = new Float32Array([
     0, 0, 1.4,
    -4, 0, -1.8,
    -1.2, 0, -1.8,
     0, 0, 1.4,
     1.2, 0, -1.8,
     4, 0, -1.8,
  ]);
  wingGeo.setAttribute("position", new THREE.BufferAttribute(verts, 3));
  wingGeo.computeVertexNormals();
  const wings = new THREE.Mesh(wingGeo, mat);
  wings.castShadow = true;
  g.add(wings);

  // Хвост
  const tail = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.9, 1.2), mat);
  tail.position.set(0, 0.5, -1.7);
  g.add(tail);

  // Красный огонёк
  const blink = new THREE.Mesh(
    new THREE.SphereGeometry(0.15, 8, 8),
    new THREE.MeshBasicMaterial({ color: 0xff0000 })
  );
  blink.position.set(0, 0, -1.9);
  g.add(blink);
  g.userData.blink = blink;

  // Точка попадания (для рейкаста — невидимая сфера-обёртка)
  const hitGeo = new THREE.SphereGeometry(2.4, 8, 8);
  const hitMat = new THREE.MeshBasicMaterial({ visible: false });
  const hitBox = new THREE.Mesh(hitGeo, hitMat);
  hitBox.userData.isDroneHit = true;
  g.add(hitBox);

  g.userData.hitBox = hitBox;
  return g;
}

function spawnDrone(level) {
  const g = createDrone();

  // Случайная точка спавна на краю карты
  const angle = Math.random() * Math.PI * 2;
  const radius = 320 + Math.random() * 60;
  const sx = Math.cos(angle) * radius;
  const sz = Math.sin(angle) * radius;
  const sy = 55 + Math.random() * 45;

  g.position.set(sx, sy, sz);

  // Цель — случайная точка на крыше склада
  const tx = (Math.random() - 0.5) * (WAREHOUSE_W - 6);
  const tz = (Math.random() - 0.5) * (WAREHOUSE_D - 6);
  const target = new THREE.Vector3(tx, roofY + 0.8, tz);

  const dir = target.clone().sub(g.position).normalize();
  g.lookAt(target);

  const speed = DRONE_SPEED_BASE + level * 1.4 + Math.random() * 2;

  drones.push({
    mesh: g,
    velocity: dir.multiplyScalar(speed),
    target,
    hp: 1,
    speed,
    wobble: Math.random() * Math.PI * 2,
    hitBox: g.userData.hitBox,
  });

  scene.add(g);
  return g;
}

// ---------- Снаряды ----------
const projectiles = [];
const projGeo = new THREE.SphereGeometry(0.35, 8, 8);
const projMat = new THREE.MeshBasicMaterial({ color: 0xffdd44 });
const projTrailMat = new THREE.MeshBasicMaterial({ color: 0xff6600, transparent: true, opacity: 0.6 });

function shoot() {
  if (state.fireTimer > 0 || state.phase !== "play") return;
  state.fireTimer = FIRE_COOLDOWN;

  // Направление — из камеры (прицел)
  const dir = new THREE.Vector3();
  camera.getWorldDirection(dir);

  // Стартовая точка — приблизительно из ствола
  const muzzleWorld = new THREE.Vector3();
  turretHead.localToWorld(muzzleWorld.set(0, 0.4, 3));

  // Но стреляем в точку перед камерой, чтоб визуально попадало в прицел
  const origin = camera.position.clone().add(dir.clone().multiplyScalar(2.5));

  const p = new THREE.Mesh(projGeo, projMat);
  p.position.copy(origin);
  p.scale.setScalar(1);
  scene.add(p);

  // Вспышка у ствола
  spawnMuzzleFlash(muzzleWorld);

  projectiles.push({
    mesh: p,
    velocity: dir.clone().multiplyScalar(PROJECTILE_SPEED),
    life: 0,
    maxLife: 3,
  });

  // Звук выстрела (Web Audio — простой синтез)
  playShootSound();
}

function spawnMuzzleFlash(pos) {
  const flash = new THREE.Mesh(
    new THREE.SphereGeometry(0.6, 8, 8),
    new THREE.MeshBasicMaterial({ color: 0xffcc33, transparent: true, opacity: 0.9 })
  );
  flash.position.copy(pos);
  scene.add(flash);
  let t = 0;
  const anim = () => {
    t += 0.06;
    flash.scale.setScalar(1 + t * 3);
    flash.material.opacity = 0.9 * (1 - t);
    if (t < 1) requestAnimationFrame(anim);
    else scene.remove(flash);
  };
  anim();
}

// ---------- Взрывы ----------
const explosions = [];
function spawnExplosion(pos, big = false) {
  const count = big ? 40 : 20;
  const geo = new THREE.SphereGeometry(big ? 0.6 : 0.35, 6, 6);
  const group = new THREE.Group();

  const particles = [];
  for (let i = 0; i < count; i++) {
    const m = new THREE.Mesh(
      geo,
      new THREE.MeshBasicMaterial({
        color: i % 2 === 0 ? 0xffcc33 : 0xff5500,
        transparent: true,
        opacity: 1,
      })
    );
    const dir = new THREE.Vector3(
      Math.random() - 0.5,
      Math.random() - 0.5,
      Math.random() - 0.5
    ).normalize();
    const speed = 6 + Math.random() * (big ? 22 : 12);
    m.position.copy(pos);
    m.userData.vel = dir.multiplyScalar(speed);
    m.userData.life = 0;
    m.userData.max = 0.6 + Math.random() * 0.5;
    group.add(m);
    particles.push(m);
  }
  scene.add(group);

  const light = new THREE.PointLight(0xffaa33, big ? 6 : 3, big ? 40 : 20, 2);
  light.position.copy(pos);
  group.add(light);

  explosions.push({ group, particles, light, age: 0, big });
  playExplosionSound(big);
}

function updateExplosions(dt) {
  for (let i = explosions.length - 1; i >= 0; i--) {
    const e = explosions[i];
    e.age += dt;
    let alive = false;
    for (const p of e.particles) {
      p.userData.life += dt;
      if (p.userData.life < p.userData.max) {
        alive = true;
        p.position.addScaledVector(p.userData.vel, dt);
        p.userData.vel.multiplyScalar(0.94);
        p.material.opacity = 1 - p.userData.life / p.userData.max;
        p.scale.multiplyScalar(0.98);
      } else {
        p.visible = false;
      }
    }
    e.light.intensity = Math.max(0, (e.big ? 6 : 3) * (1 - e.age / 0.5));
    if (!alive && e.age > 0.7) {
      scene.remove(e.group);
      explosions.splice(i, 1);
    }
  }
}

// ---------- Звук (Web Audio) ----------
let audioCtx = null;
function initAudio() {
  if (!audioCtx) {
    try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) {}
  }
}
function playShootSound() {
  if (!audioCtx) return;
  const t = audioCtx.currentTime;
  const osc = audioCtx.createOscillator();
  const gain = audioCtx.createGain();
  osc.type = "square";
  osc.frequency.setValueAtTime(220, t);
  osc.frequency.exponentialRampToValueAtTime(60, t + 0.15);
  gain.gain.setValueAtTime(0.12, t);
  gain.gain.exponentialRampToValueAtTime(0.001, t + 0.15);
  osc.connect(gain).connect(audioCtx.destination);
  osc.start(t); osc.stop(t + 0.16);
}
function playExplosionSound(big) {
  if (!audioCtx) return;
  const t = audioCtx.currentTime;
  const bufferSize = audioCtx.sampleRate * (big ? 0.6 : 0.35);
  const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
  const data = buffer.getChannelData(0);
  for (let i = 0; i < bufferSize; i++) {
    data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / bufferSize, 2);
  }
  const src = audioCtx.createBufferSource();
  src.buffer = buffer;
  const gain = audioCtx.createGain();
  gain.gain.value = big ? 0.4 : 0.25;
  const filter = audioCtx.createBiquadFilter();
  filter.type = "lowpass";
  filter.frequency.value = big ? 900 : 1500;
  src.connect(filter).connect(gain).connect(audioCtx.destination);
  src.start(t);
}

// ---------- Состояние игры ----------
const state = {
  phase: "menu", // menu | play | win | lose
  level: 1,
  lives: MAX_LIVES,
  hits: 0,
  killed: 0,
  totalToSpawn: 0,
  spawned: 0,
  fireTimer: 0,
  spawnTimer: 0,
  spawnInterval: 1.6,
  nextLevelDelay: 0,
};

// ---------- Управление камерой (мышь) ----------
const mouse = { x: 0, y: 0 };
const cameraBase = new THREE.Vector3(0, roofY + 12, 60);
const cameraTarget = new THREE.Vector3(0, roofY + 4, 0);

// ---------- Ввод ----------
const keys = {};
addEventListener("keydown", (e) => {
  keys[e.code] = true;
  if (e.code === "Space") { e.preventDefault(); shoot(); }
  if (e.code === "KeyR" && (state.phase === "win" || state.phase === "lose")) resetGame();
});
addEventListener("keyup", (e) => { keys[e.code] = false; });

addEventListener("mousemove", (e) => {
  mouse.x = (e.clientX / innerWidth) * 2 - 1;
  mouse.y = -((e.clientY / innerHeight) * 2 - 1);
});
addEventListener("mousedown", (e) => {
  if (e.button === 0) {
    initAudio();
    shoot();
  }
});
addEventListener("contextmenu", (e) => e.preventDefault());

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

// ---------- Логика стрельбы и попаданий ----------
const raycaster = new THREE.Raycaster();

function checkProjectileHits() {
  for (let i = projectiles.length - 1; i >= 0; i--) {
    const p = projectiles[i];
    const ppos = p.mesh.position;

    let hit = false;

    // Проверка по дронам
    for (let j = drones.length - 1; j >= 0; j--) {
      const d = drones[j];
      const dp = d.mesh.position;
      const dist = ppos.distanceTo(dp);
      if (dist < 2.6) {
        // Попадание
        destroyDrone(j);
        hit = true;
        break;
      }
    }

    if (hit) {
      scene.remove(p.mesh);
      projectiles.splice(i, 1);
    }
  }
}

function destroyDrone(index) {
  const d = drones[index];
  const pos = d.mesh.position.clone();
  spawnExplosion(pos, false);
  scene.remove(d.mesh);
  drones.splice(index, 1);
  state.killed++;
  updateHUD();
}

// ---------- Попадание дрона в склад ----------
function droneHitsWarehouse(d) {
  const pos = new THREE.Vector3(
    (Math.random() - 0.5) * (WAREHOUSE_W - 8),
    roofY + 0.7,
    (Math.random() - 0.5) * (WAREHOUSE_D - 8)
  );
  // Ограничим, чтобы огоньки были на крыше
  spawnExplosion(pos, true);
  spawnFireOnWarehouse(pos);

  state.lives--;
  state.hits++;

  // Тряска камеры
  shakeAmount = 0.6;

  updateHUD();

  if (state.hits >= MAX_HITS_FOR_LOSE || state.lives <= 0) {
    endGame(false);
  }
}

// ---------- Спавн ----------
function startLevel(level) {
  state.level = level;
  state.totalToSpawn = LEVEL_DRONES(level);
  state.spawned = 0;
  state.killed = 0;
  state.spawnTimer = 1.5;
  state.spawnInterval = Math.max(0.7, 1.8 - level * 0.12);
  updateHUD();
}

function updateHUD() {
  elLives.textContent = Math.max(0, state.lives);
  elLevel.textContent = state.level;
  elKilled.textContent = state.killed;
  elTotal.textContent = state.totalToSpawn;
  elHits.textContent = state.hits;
}

// ---------- Камера-тряска ----------
let shakeAmount = 0;

// ---------- Игровой цикл ----------
const clock = new THREE.Clock();
let timeAcc = 0;

function animate() {
  requestAnimationFrame(animate);
  const dt = Math.min(clock.getDelta(), 0.05);
  timeAcc += dt;

  // Таймеры
  if (state.fireTimer > 0) state.fireTimer -= dt;

  // Камера следует за мышью
  const camX = cameraBase.x + mouse.x * 45;
  const camY = cameraBase.y + mouse.y * 22;
  const camZ = cameraBase.z;

  camera.position.x += (camX - camera.position.x) * Math.min(1, dt * 8);
  camera.position.y += (camY - camera.position.y) * Math.min(1, dt * 8);
  camera.position.z = camZ;

  // Тряска
  if (shakeAmount > 0) {
    camera.position.x += (Math.random() - 0.5) * shakeAmount;
    camera.position.y += (Math.random() - 0.5) * shakeAmount;
    shakeAmount *= Math.pow(0.001, dt);
    if (shakeAmount < 0.02) shakeAmount = 0;
  }

  camera.lookAt(cameraTarget);

  // Поворот башни в сторону прицела
  const aimPoint = camera.position.clone().add(
    new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion).multiplyScalar(80)
  );
  const localAim = turret.worldToLocal(aimPoint.clone());
  const angleY = Math.atan2(localAim.x, localAim.z);
  // Плавно
  let da = angleY - turretHead.rotation.y;
  while (da > Math.PI) da -= Math.PI * 2;
  while (da < -Math.PI) da += Math.PI * 2;
  turretHead.rotation.y += da * Math.min(1, dt * 10);
  // Наклон ствола вверх
  const horiz = Math.hypot(localAim.x, localAim.z);
  const pitchTarget = Math.atan2(localAim.y, horiz);
  turretHead.rotation.x += (pitchTarget * 0.5 - turretHead.rotation.x) * Math.min(1, dt * 6);

  // Мигание лампочки и огней дронов
  lamp.material.color.setHex(Math.sin(timeAcc * 8) > 0 ? 0xff2200 : 0x550000);
  for (const d of drones) {
    if (d.mesh.userData.blink) {
      d.mesh.userData.blink.material.color.setHex(
        Math.sin(timeAcc * 10 + d.wobble) > 0 ? 0xff0000 : 0x330000
      );
    }
  }

  // Спавн дронов
  if (state.phase === "play" && state.spawned < state.totalToSpawn) {
    state.spawnTimer -= dt;
    if (state.spawnTimer <= 0) {
      spawnDrone(state.level);
      state.spawned++;
      state.spawnTimer = state.spawnInterval * (0.7 + Math.random() * 0.6);
    }
  }

  // Движение дронов
  for (let i = drones.length - 1; i >= 0; i--) {
    const d = drones[i];
    d.wobble += dt * 3;
    const wob = new THREE.Vector3(
      Math.sin(d.wobble) * 0.4,
      Math.sin(d.wobble * 1.3) * 0.3,
      Math.cos(d.wobble * 0.7) * 0.4
    );
    d.mesh.position.addScaledVector(d.velocity, dt);
    d.mesh.position.addScaledVector(wob, dt);
    // Лёгкий крен
    d.mesh.rotation.z = Math.sin(d.wobble) * 0.15;

    // Проверка достижения цели
    if (d.mesh.position.distanceTo(d.target) < 4) {
      droneHitsWarehouse(d);
      scene.remove(d.mesh);
      drones.splice(i, 1);
      // Если это была последняя цель и всё сбито — уровень пройден
      continue;
    }

    // Если дрон улетел слишком далеко (защита от багов)
    if (d.mesh.position.length() > 800) {
      scene.remove(d.mesh);
      drones.splice(i, 1);
    }
  }

  // Снаряды
  for (let i = projectiles.length - 1; i >= 0; i--) {
    const p = projectiles[i];
    p.mesh.position.addScaledVector(p.velocity, dt);
    p.life += dt;
    // Вращение для красоты
    p.mesh.rotation.x += dt * 15;
    if (p.life > p.maxLife) {
      scene.remove(p.mesh);
      projectiles.splice(i, 1);
    }
  }

  checkProjectileHits();
  updateExplosions(dt);
  updateFires(dt, timeAcc);

  // Проверка завершения уровня
  if (state.phase === "play"
      && state.spawned >= state.totalToSpawn
      && drones.length === 0
      && state.hits < MAX_HITS_FOR_LOSE) {
    // Уровень пройден
    state.phase = "win";
    state.nextLevelDelay = 0;
    showOverlay(
      `Уровень ${state.level} пройден!`,
      `Сбито дронов: ${state.killed} из ${state.totalToSpawn}. Жизни: ${state.lives}/10`,
      `Уровень ${state.level + 1}`
    );
  }

  renderer.render(scene, camera);
}

// ---------- Оверлей / состояние ----------
function showOverlay(title, text, btnText) {
  ovTitle.textContent = title;
  ovText.textContent = text;
  ovBtn.textContent = btnText;
  overlay.classList.remove("hidden");
}
function hideOverlay() {
  overlay.classList.add("hidden");
}

ovBtn.addEventListener("click", () => {
  initAudio();
  if (state.phase === "menu") {
    resetGame();
    startGameplay();
  } else if (state.phase === "win") {
    startLevel(state.level + 1);
    state.phase = "play";
    hideOverlay();
  } else if (state.phase === "lose") {
    resetGame();
    startGameplay();
  }
});

function resetGame() {
  // Очистить всё
  for (const d of drones) scene.remove(d.mesh);
  drones.length = 0;
  for (const p of projectiles) scene.remove(p.mesh);
  projectiles.length = 0;
  for (const f of fires) scene.remove(f.group);
  fires.length = 0;
  for (const e of explosions) scene.remove(e.group);
  explosions.length = 0;

  state.phase = "menu";
  state.level = 1;
  state.lives = MAX_LIVES;
  state.hits = 0;
  state.killed = 0;
  state.spawned = 0;
  state.totalToSpawn = 0;
  state.fireTimer = 0;
  state.spawnTimer = 0;
  shakeAmount = 0;
  updateHUD();
}

function startGameplay() {
  state.phase = "play";
  startLevel(1);
  hideOverlay();
}

function endGame(win) {
  state.phase = "lose";
  showOverlay(
    "Склад уничтожен!",
    `Уровень ${state.level}. Сбито дронов: ${state.killed}. Попаданий: ${state.hits}/10.`,
    "Играть заново"
  );
}

// ---------- Старт ----------
function init() {
  camera.position.copy(cameraBase);
  camera.lookAt(cameraTarget);

  updateHUD();

  showOverlay(
    "ПВО — Защита склада",
    "Управляй зенитной установкой «ПВО» на крыше склада Яндекса. Сбивай чёрные дроны самолётного типа. Не дай им попасть в склад! 10 попаданий — и склада не станет.",
    "Начать игру"
  );

  animate();
}

init();

})();