🎮ArcadeLab

NEON SHOOTER

by PrismPilot23
3647 lines144.6 KB
▶ Play
<!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">
<title>NEON SHOOTER</title>
<script src="/sdk.js"></script>
<style>
  html, body {
    margin: 0; padding: 0; width: 100%; height: 100%;
    background: #05050a; overflow: hidden;
    font-family: Consolas, 'Courier New', monospace;
    touch-action: none; overscroll-behavior: none;
    -webkit-user-select: none; user-select: none;
    -webkit-touch-callout: none;
  }
  canvas {
    display: block; position: fixed; top: 0; left: 0;
    background: #07070d; cursor: crosshair;
    touch-action: none; -webkit-tap-highlight-color: transparent;
  }
  #cheatInput {
    position: fixed; display: none;
    background: rgba(14,20,40,0.98);
    border: 2px solid #7df9ff; color: #7df9ff;
    font-family: Consolas, 'Courier New', monospace;
    letter-spacing: 2px; outline: none; border-radius: 4px;
    box-shadow: 0 0 22px rgba(125,249,255,0.6);
    text-align: center; text-transform: lowercase;
    z-index: 100; box-sizing: border-box;
  }
  #cheatInput::placeholder { color: rgba(125,249,255,0.35); letter-spacing: 1px; }
  #cheatInput:focus { box-shadow: 0 0 30px rgba(125,249,255,0.9); border-color: #ffffff; }
</style>
</head>
<body>
<canvas id="game"></canvas>
<input id="cheatInput" type="text" placeholder="введи код..." autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" maxlength="24">

<script>
"use strict";

/* =========================================================================
   YANDEX GAMES SDK
   ========================================================================= */
let ysdk = null;
let sdkPlayer = null;
let sdkReady = false;

async function initYandexSDK() {
  try {
    ysdk = await YaGames.init();
    try {
      sdkPlayer = await ysdk.getPlayer({ scopes: false });
    } catch (e) { sdkPlayer = null; }
    ysdk.on('game_api_pause', () => {
      if (game.state === 'playing') {
        game.state = 'paused';
        game.moveStick.end(); game.aimStick.end();
      }
      sound.muteForPause();
      setGameplayActive(false);
    });
    ysdk.on('game_api_resume', () => {
      sound.unmuteAfterPause();
    });
    sdkReady = true;
  } catch (e) { sdkReady = false; }
}
async function savePlayerProgress(data) {
  if (!sdkPlayer) return;
  try { await sdkPlayer.setData(data, false); } catch (e) {}
}
async function loadPlayerProgress() {
  if (!sdkPlayer) return null;
  try { return await sdkPlayer.getData(['highScore', 'totalKills', 'totalRooms']); }
  catch (e) { return null; }
}
function notifyGameReady() {
  if (ysdk && ysdk.features && ysdk.features.LoadingAPI) {
    try { ysdk.features.LoadingAPI.ready(); } catch (e) {}
  }
}
function setGameplayActive(active) {
  if (ysdk && ysdk.features && ysdk.features.GameplayAPI) {
    try {
      if (active) ysdk.features.GameplayAPI.start();
      else ysdk.features.GameplayAPI.stop();
    } catch (e) {}
  }
}
function showFullscreenAd(onClose) {
  if (!ysdk || !ysdk.adv) { if (onClose) onClose(); return; }
  try {
    ysdk.adv.showFullscreenAdv({
      callbacks: {
        onClose: () => { if (onClose) onClose(); },
        onError: () => { if (onClose) onClose(); }
      }
    });
  } catch (e) { if (onClose) onClose(); }
}

/* =========================================================================
   КОНФИГУРАЦИЯ
   ========================================================================= */
const CONFIG = {
  W: 1280, H: 720,
  WORLD_W: 2600, WORLD_H: 1900,
  FIXED_DT: 1 / 60, GRID: 80,
  PLAYER: {
    radius: 15, speed: 167, sprintMult: 1.75, hp: 100,
    magSize: 30, reloadTime: 1.45, fireRate: 0.085,
    bulletSpeed: 980, damage: 17, spread: 0.035,
    spreadPerShot: 0.022, maxSpread: 0.16, spreadRecover: 0.42,
    dash: { speed: 900, duration: 0.15, cooldown: 1.5 },
    rage: { duration: 6.0, cooldown: 20.0, damageMult: 1.5, fireRateMult: 0.7, speedMult: 1.25 }
  },
  BOT_TYPES: {
    normal: { key:'normal', name:'Обычный', color:'#ff3b6b', radius:15, hp:60,
      speed:138, vision:560, fireRate:0.5, damage:9, bulletSpeed:640,
      spread:0.085, score:100, magSize:10, reloadTime:2.0, preferred:260 },
    heavy: { key:'heavy', name:'Тяжёлый', color:'#ff9f1c', radius:22, hp:195,
      speed:88, vision:470, fireRate:1.1, damage:21, bulletSpeed:500,
      spread:0.14, score:250, magSize:6, reloadTime:3.0, preferred:190 },
    sniper: { key:'sniper', name:'Снайпер', color:'#b06bff', radius:14, hp:45,
      speed:122, vision:980, fireRate:1.7, damage:33, bulletSpeed:1180,
      spread:0.018, score:200, magSize:3, reloadTime:3.2, preferred:440 },
    sapper: { key:'sapper', name:'Подрывник', color:'#ffcc00', radius:16,
      hp:38, speed:215, vision:1200, fireRate:0, damage:0, bulletSpeed:0,
      spread:0, score:180, magSize:0, reloadTime:0, preferred:0,
      explodeRadius: 135, explodeDmgPlayer: 40, explodeDmgBots: 60 }
  },
  HEAL_ON_BREAK: 0.35,
  ROOM_HP_SCALE: 0.15, ROOM_DMG_SCALE: 0.10, ROOM_SPEED_SCALE: 0.03,
  BOSS_EVERY: 5
};

const DIFFICULTIES = {
  easy: { key:'easy', name:'ЛЕГКО', color:'#00e5a0',
    playerHpMult:1.5, botHpMult:0.8, botDmgMult:0.7,
    botCount:5, botGrowth:2, respawns:99,
    desc:'Старт 5 · +2 за комнату · ∞ возрождений' },
  medium: { key:'medium', name:'СРЕДНЕ', color:'#ffcc00',
    playerHpMult:1.0, botHpMult:1.0, botDmgMult:1.0,
    botCount:5, botGrowth:3, respawns:3,
    desc:'Старт 5 · +3 за комнату · 3 возрождения' },
  hard: { key:'hard', name:'СЛОЖНО', color:'#ff9f1c',
    playerHpMult:0.85, botHpMult:1.3, botDmgMult:1.3,
    botCount:10, botGrowth:4, respawns:1,
    desc:'Старт 10 · +4 за комнату · 1 возрождение' },
  hardcore: { key:'hardcore', name:'ХАРДКОР', color:'#ff3b6b',
    playerHpMult:1.0, botHpMult:1.5, botDmgMult:1.5,
    botCount:20, botGrowth:6, respawns:0,
    desc:'Старт 20 · +6 за комнату · Без возрождений' }
};
const DIFFICULTY_ORDER = ['easy','medium','hard','hardcore'];

/* =========================================================================
   МАТЕМАТИКА
   ========================================================================= */
const TAU = Math.PI * 2;
const clamp = (v,a,b)=> v<a?a:v>b?b:v;
const rand  = (a,b)=> a + Math.random()*(b-a);
const lerp  = (a,b,t)=> a + (b-a)*t;
const dist2 = (ax,ay,bx,by)=> Math.hypot(ax-bx, ay-by);
function normAngle(a){ a%=TAU; if(a>Math.PI)a-=TAU; if(a<-Math.PI)a+=TAU; return a; }
function angleDiff(a,b){ return normAngle(b-a); }
function turnToward(a,b,step){
  const d = angleDiff(a,b);
  if (Math.abs(d) <= step) return b;
  return a + Math.sign(d)*step;
}
function pointSegDist(px,py,x1,y1,x2,y2){
  const dx=x2-x1, dy=y2-y1, l2=dx*dx+dy*dy;
  let t = l2>1e-6 ? ((px-x1)*dx+(py-y1)*dy)/l2 : 0;
  t = clamp(t,0,1);
  return Math.hypot(px-(x1+t*dx), py-(y1+t*dy));
}
function segIntersectsRect(x1,y1,x2,y2,r){
  let t0=0,t1=1;
  const dx=x2-x1, dy=y2-y1;
  const p=[-dx,dx,-dy,dy];
  const q=[x1-r.x, r.x+r.w-x1, y1-r.y, r.y+r.h-y1];
  for(let i=0;i<4;i++){
    if(p[i]===0){ if(q[i]<0) return false; }
    else {
      const t=q[i]/p[i];
      if(p[i]<0){ if(t>t1) return false; if(t>t0) t0=t; }
      else      { if(t<t0) return false; if(t<t1) t1=t; }
    }
  }
  return true;
}
function rectsOverlap(a,b,pad=0){
  return !(a.x+a.w+pad<b.x || b.x+b.w+pad<a.x ||
           a.y+a.h+pad<b.y || b.y+b.h+pad<a.y);
}

/* =========================================================================
   ЗВУК
   ========================================================================= */
class Sound {
  constructor(){
    this.ctx = null; this.master = null;
    this.enabled = true; this.volume = 0.55;
    this._lastPlayed = {};
  }
  init(){
    if(this.ctx) return;
    try {
      const AC = window.AudioContext || window.webkitAudioContext;
      if(!AC) { this.enabled = false; return; }
      this.ctx = new AC();
      this.master = this.ctx.createGain();
      this.master.gain.value = this.volume;
      this.master.connect(this.ctx.destination);
    } catch(e){ this.enabled = false; }
  }
  resume(){ if(this.ctx && this.ctx.state === 'suspended') this.ctx.resume(); }
  toggle(){
    this.enabled = !this.enabled;
    if(this.master) this.master.gain.value = this.enabled ? this.volume : 0;
    return this.enabled;
  }
  muteForPause(){ if(this.master) this.master.gain.value = 0; }
  unmuteAfterPause(){ if(this.master && this.enabled) this.master.gain.value = this.volume; }
  setVolume(v){
    this.volume = clamp(v, 0, 1);
    if(this.master && this.enabled) this.master.gain.value = this.volume;
  }
  _throttle(key, ms=40){
    const now = performance.now();
    if(this._lastPlayed[key] && now - this._lastPlayed[key] < ms) return false;
    this._lastPlayed[key] = now;
    return true;
  }
  spatialVolume(x, y, camX, camY, maxDist = 900){
    const d = Math.hypot(x - camX, y - camY);
    if(d >= maxDist) return 0;
    return 1 - (d / maxDist);
  }
  _tone(freq, duration, type='square', vol=0.2, freqEnd=null, delay=0, spatial=1){
    if(!this.ctx || !this.enabled || spatial <= 0.01) return;
    const v = vol * spatial;
    if(v <= 0.001) return;
    const t = this.ctx.currentTime + delay;
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();
    osc.type = type;
    osc.frequency.setValueAtTime(Math.max(1, freq), t);
    if(freqEnd !== null && freqEnd > 0){
      osc.frequency.exponentialRampToValueAtTime(Math.max(1, freqEnd), t + duration);
    }
    gain.gain.setValueAtTime(0.0001, t);
    gain.gain.exponentialRampToValueAtTime(v, t + 0.005);
    gain.gain.exponentialRampToValueAtTime(0.0001, t + duration);
    osc.connect(gain); gain.connect(this.master);
    osc.start(t); osc.stop(t + duration + 0.02);
  }
  _noise(duration, vol=0.3, filterStart=3000, filterEnd=200, delay=0, spatial=1){
    if(!this.ctx || !this.enabled || spatial <= 0.01) return;
    const v = vol * spatial;
    if(v <= 0.001) return;
    const t = this.ctx.currentTime + delay;
    const sr = this.ctx.sampleRate;
    const bufSize = Math.max(1, Math.floor(sr * duration));
    const buffer = this.ctx.createBuffer(1, bufSize, sr);
    const data = buffer.getChannelData(0);
    for(let i = 0; i < bufSize; i++) data[i] = Math.random() * 2 - 1;
    const src = this.ctx.createBufferSource();
    src.buffer = buffer;
    const filter = this.ctx.createBiquadFilter();
    filter.type = 'lowpass';
    filter.frequency.setValueAtTime(filterStart, t);
    filter.frequency.exponentialRampToValueAtTime(Math.max(20, filterEnd), t + duration);
    filter.Q.value = 1.2;
    const gain = this.ctx.createGain();
    gain.gain.setValueAtTime(v, t);
    gain.gain.exponentialRampToValueAtTime(0.0001, t + duration);
    src.connect(filter); filter.connect(gain); gain.connect(this.master);
    src.start(t); src.stop(t + duration + 0.02);
  }
  shoot(spatial=1){ if(!this._throttle('shoot',30)) return;
    this._tone(950, 0.075, 'square', 0.11, 200, 0, spatial);
    this._noise(0.05, 0.06, 3500, 900, 0, spatial); }
  botShoot(spatial=1){ if(!this._throttle('botShoot',25)) return;
    this._tone(420, 0.10, 'sawtooth', 0.055, 90, 0, spatial);
    this._noise(0.06, 0.045, 1800, 400, 0, spatial); }
  sniperShoot(spatial=1){ if(!this._throttle('sniperShoot',60)) return;
    this._tone(1400, 0.18, 'sawtooth', 0.09, 180, 0, spatial);
    this._noise(0.12, 0.05, 4200, 500, 0, spatial); }
  heavyShoot(spatial=1){ if(!this._throttle('heavyShoot',60)) return;
    this._tone(180, 0.20, 'square', 0.10, 60, 0, spatial);
    this._noise(0.16, 0.08, 900, 100, 0, spatial); }
  droneShoot(spatial=1){ if(!this._throttle('droneShoot',40)) return;
    this._tone(1650, 0.045, 'square', 0.045, 800, 0, spatial); }
  hit(spatial=1){ if(!this._throttle('hit',20)) return;
    this._noise(0.05, 0.09, 2200, 300, 0, spatial);
    this._tone(320, 0.045, 'square', 0.05, 120, 0, spatial); }
  playerHit(){ if(!this._throttle('playerHit',80)) return;
    this._tone(500, 0.16, 'sawtooth', 0.16, 90);
    this._noise(0.10, 0.13, 1500, 200); }
  explosion(spatial=1){
    this._noise(0.42, 0.20, 1200, 60, 0, spatial);
    this._tone(90, 0.35, 'sine', 0.12, 30, 0, spatial);
    this._tone(220, 0.18, 'square', 0.06, 60, 0.01, spatial); }
  bigExplosion(){
    this._noise(0.9, 0.28, 1600, 40);
    this._tone(70, 0.8, 'sine', 0.20, 25);
    this._tone(180, 0.5, 'sawtooth', 0.12, 45, 0.02);
    this._tone(400, 0.3, 'square', 0.08, 80, 0.05); }
  reloadStart(){
    this._noise(0.04, 0.10, 1800, 400);
    this._tone(220, 0.05, 'square', 0.06, 110, 0.03); }
  reloadDone(){
    this._tone(880, 0.06, 'square', 0.08, 1200, 0);
    this._tone(1400, 0.05, 'square', 0.05, 900, 0.05); }
  upgrade(){
    if(!this.ctx || !this.enabled) return;
    this._tone(660, 0.10, 'triangle', 0.10, 880, 0);
    this._tone(880, 0.10, 'triangle', 0.10, 1100, 0.07);
    this._tone(1320, 0.16, 'triangle', 0.10, 1650, 0.14); }
  deny(){ this._tone(200, 0.14, 'square', 0.10, 120); }
  roomStart(){
    if(!this.ctx || !this.enabled) return;
    this._tone(80, 0.6, 'sine', 0.14, 40);
    this._tone(400, 0.4, 'triangle', 0.09, 200, 0.05);
    this._tone(600, 0.4, 'triangle', 0.08, 300, 0.10);
    this._tone(800, 0.4, 'triangle', 0.07, 400, 0.15); }
  roomClear(){
    if(!this.ctx || !this.enabled) return;
    this._tone(523, 0.35, 'triangle', 0.11, null, 0);
    this._tone(659, 0.35, 'triangle', 0.11, null, 0.08);
    this._tone(784, 0.55, 'triangle', 0.12, null, 0.16); }
  respawn(){ this._tone(200, 0.5, 'sine', 0.14, 900);
    this._tone(400, 0.5, 'triangle', 0.09, 1200, 0.02); }
  gameOver(){
    if(!this.ctx || !this.enabled) return;
    this._tone(440, 1.2, 'sawtooth', 0.16, 60);
    this._tone(220, 1.4, 'sine', 0.12, 40, 0.15);
    this._noise(1.0, 0.10, 800, 50, 0.1); }
  click(){ this._tone(1200, 0.03, 'square', 0.06, 700); }
  startGame(){
    if(!this.ctx || !this.enabled) return;
    this._tone(220, 0.35, 'triangle', 0.14, 440, 0);
    this._tone(330, 0.35, 'triangle', 0.12, 550, 0.05);
    this._tone(440, 0.5, 'triangle', 0.12, 660, 0.1); }
  tickLowHp(){ if(!this._throttle('lowHp',700)) return;
    this._tone(180, 0.15, 'sine', 0.10, 120); }
  dash(){
    if(!this.ctx || !this.enabled) return;
    this._tone(180, 0.18, 'sawtooth', 0.14, 1400);
    this._noise(0.15, 0.10, 4000, 800); }
  rage(){
    if(!this.ctx || !this.enabled) return;
    this._tone(120, 0.5, 'sawtooth', 0.16, 180);
    this._tone(240, 0.5, 'square', 0.10, 320, 0.03);
    this._tone(360, 0.5, 'triangle', 0.08, 480, 0.06);
    this._noise(0.4, 0.10, 500, 200, 0); }
  rageEnd(){ this._tone(400, 0.25, 'sine', 0.08, 200); }
  bossAppear(){
    if(!this.ctx || !this.enabled) return;
    this._tone(60, 1.4, 'sawtooth', 0.18, 35);
    this._tone(90, 1.2, 'square', 0.10, 50, 0.1);
    this._tone(150, 1.0, 'sawtooth', 0.08, 70, 0.2);
    this._noise(1.2, 0.16, 400, 80, 0);
    this._noise(0.8, 0.10, 1200, 200, 0.3); }
  bossHiss(){
    if(!this._throttle('bossHiss', 400)) return;
    this._noise(0.35, 0.10, 3000, 400);
    this._tone(500, 0.25, 'sawtooth', 0.06, 200); }
  bossDefeated(){
    if(!this.ctx || !this.enabled) return;
    this._noise(1.6, 0.30, 1600, 40);
    this._tone(60, 1.6, 'sine', 0.22, 20);
    this._tone(200, 1.0, 'sawtooth', 0.12, 40, 0.1);
    this._tone(523, 0.6, 'triangle', 0.14, null, 0.8);
    this._tone(659, 0.6, 'triangle', 0.14, null, 1.0);
    this._tone(784, 0.9, 'triangle', 0.15, null, 1.2); }
  sapperAlarm(spatial=1, fast=false){
    if(!this._throttle('sapperAlarm', fast ? 90 : 220)) return;
    this._tone(fast ? 1400 : 1100, 0.05, 'square', 0.07, 800, 0, spatial); }
  sapperExplode(spatial=1){
    this._noise(0.6, 0.28, 2000, 50, 0, spatial);
    this._tone(80, 0.5, 'sine', 0.18, 25, 0, spatial);
    this._tone(180, 0.3, 'sawtooth', 0.10, 40, 0.02, spatial);
    this._tone(420, 0.2, 'square', 0.08, 90, 0.05, spatial); }
  cheatOn(){
    if(!this.ctx || !this.enabled) return;
    this._tone(880, 0.08, 'triangle', 0.14, 1320, 0);
    this._tone(1320, 0.10, 'triangle', 0.12, 1760, 0.06);
    this._tone(1760, 0.14, 'square', 0.09, 2200, 0.12); }
  cheatOff(){
    if(!this.ctx || !this.enabled) return;
    this._tone(880, 0.10, 'sawtooth', 0.12, 440);
    this._tone(440, 0.15, 'square', 0.10, 220, 0.06); }
  megaUpgrade(){
    if(!this.ctx || !this.enabled) return;
    for(let i = 0; i < 6; i++){
      this._tone(660 + i*220, 0.14, 'triangle', 0.11, 880 + i*220, i*0.07);
    }
    this._noise(0.6, 0.10, 3000, 400, 0.3);
  }
}
const sound = new Sound();

/* =========================================================================
   VIRTUAL JOYSTICK
   ========================================================================= */
class VirtualJoystick {
  constructor(radius=58){
    this.radius=radius; this.stickRadius=24;
    this.active=false; this.touchId=null;
    this.baseX=0; this.baseY=0;
    this.stickX=0; this.stickY=0;
    this.dx=0; this.dy=0;
  }
  start(id,x,y){ this.active=true; this.touchId=id;
    this.baseX=x; this.baseY=y; this.stickX=x; this.stickY=y;
    this.dx=0; this.dy=0; }
  move(x,y){
    let dx=x-this.baseX, dy=y-this.baseY;
    const d=Math.hypot(dx,dy);
    if(d>this.radius){ dx=dx/d*this.radius; dy=dy/d*this.radius; }
    this.stickX=this.baseX+dx; this.stickY=this.baseY+dy;
    this.dx=dx/this.radius; this.dy=dy/this.radius;
  }
  end(){ this.active=false; this.touchId=null; this.dx=0; this.dy=0; }
  getMag(){ return Math.hypot(this.dx,this.dy); }
  draw(ctx){
    if(!this.active) return;
    ctx.globalAlpha=0.35;
    ctx.fillStyle='rgba(10,20,45,0.55)';
    ctx.beginPath(); ctx.arc(this.baseX,this.baseY,this.radius,0,TAU); ctx.fill();
    ctx.globalAlpha=0.55; ctx.strokeStyle='#7df9ff'; ctx.lineWidth=3;
    ctx.beginPath(); ctx.arc(this.baseX,this.baseY,this.radius,0,TAU); ctx.stroke();
    ctx.globalAlpha=0.85; ctx.fillStyle='#22e6ff';
    ctx.shadowColor='#22e6ff'; ctx.shadowBlur=14;
    ctx.beginPath(); ctx.arc(this.stickX,this.stickY,this.stickRadius,0,TAU); ctx.fill();
    ctx.shadowBlur=0; ctx.globalAlpha=1;
  }
}

/* =========================================================================
   WALL / PARTICLE / RING / BULLET
   ========================================================================= */
class Wall {
  constructor(x,y,w,h){ this.x=x; this.y=y; this.w=w; this.h=h; }
  circleCollide(cx,cy,r){
    const nx=clamp(cx,this.x,this.x+this.w);
    const ny=clamp(cy,this.y,this.y+this.h);
    const dx=cx-nx, dy=cy-ny, d2=dx*dx+dy*dy;
    if(d2>r*r) return null;
    if(d2>1e-4){
      const d=Math.sqrt(d2);
      return { nx:dx/d, ny:dy/d, depth:r-d };
    }
    const L=cx-this.x, R=this.x+this.w-cx;
    const T=cy-this.y, B=this.y+this.h-cy;
    const m=Math.min(L,R,T,B);
    if(m===L) return {nx:-1,ny:0,depth:r+L};
    if(m===R) return {nx:1,ny:0,depth:r+R};
    if(m===T) return {nx:0,ny:-1,depth:r+T};
    return {nx:0,ny:1,depth:r+B};
  }
  draw(ctx){
    ctx.fillStyle='#12122a';
    ctx.fillRect(this.x,this.y,this.w,this.h);
    ctx.strokeStyle='#3b6bff'; ctx.lineWidth=2;
    ctx.shadowColor='#3b6bff'; ctx.shadowBlur=14;
    ctx.strokeRect(this.x+1,this.y+1,this.w-2,this.h-2);
    ctx.shadowBlur=0;
  }
}
class Particle {
  constructor(x,y,vx,vy,life,color,size,type='spark'){
    this.x=x; this.y=y; this.vx=vx; this.vy=vy;
    this.life=life; this.maxLife=life;
    this.color=color; this.size=size; this.type=type;
    this.dead=false;
  }
  update(dt){
    this.x+=this.vx*dt; this.y+=this.vy*dt;
    const f=Math.pow(0.04,dt);
    this.vx*=f; this.vy*=f;
    this.life-=dt;
    if(this.life<=0) this.dead=true;
  }
  draw(ctx){
    const a=Math.max(0,this.life/this.maxLife);
    ctx.globalAlpha=a;
    ctx.fillStyle=this.color;
    ctx.beginPath();
    ctx.arc(this.x,this.y,this.size*(this.type==='spark'?a:1),0,TAU);
    ctx.fill();
    ctx.globalAlpha=1;
  }
}
class Ring {
  constructor(x,y,color,maxR,life){
    this.x=x; this.y=y; this.color=color;
    this.maxR=maxR; this.life=life; this.maxLife=life;
    this.dead=false;
  }
  update(dt){ this.life-=dt; if(this.life<=0) this.dead=true; }
  draw(ctx){
    const t=1-this.life/this.maxLife;
    ctx.globalAlpha=(1-t)*0.9;
    ctx.strokeStyle=this.color;
    ctx.lineWidth=4*(1-t)+1;
    ctx.beginPath(); ctx.arc(this.x,this.y,this.maxR*t,0,TAU); ctx.stroke();
    ctx.globalAlpha=1;
  }
}
class Bullet {
  constructor(x,y,angle,speed,damage,owner,color,radius=3){
    this.x=x; this.y=y; this.px=x; this.py=y;
    this.vx=Math.cos(angle)*speed;
    this.vy=Math.sin(angle)*speed;
    this.damage=damage; this.owner=owner;
    this.color=color; this.r=radius;
    this.life=2.4; this.dead=false; this.fromDrone=false; this.isBossBullet=false;
  }
  update(dt,game){
    this.px=this.x; this.py=this.y;
    this.x+=this.vx*dt; this.y+=this.vy*dt;
    this.life-=dt;
    if(this.life<=0){ this.dead=true; return; }
    for(const w of game.walls){
      if(this.x>w.x && this.x<w.x+w.w && this.y>w.y && this.y<w.y+w.h){
        this.dead=true;
        game.spawnSparks(this.x,this.y,this.color,5);
        return;
      }
    }
    const targets=[];
    if(game.player && game.player.alive && game.player!==this.owner) targets.push(game.player);
    for(const b of game.bots) if(b.alive && b!==this.owner) targets.push(b);
    for(const t of targets){
      const d=pointSegDist(t.x,t.y,this.px,this.py,this.x,this.y);
      if(d < t.r + this.r){
        game.damageEntity(t,this.damage,this.owner);
        game.spawnBlood(this.x,this.y,this.color,5);
        this.dead=true;
        return;
      }
    }
    if(game.boss && game.boss.alive && game.boss !== this.owner){
      const parts = [{x: game.boss.x, y: game.boss.y, r: game.boss.r}].concat(game.boss.segments);
      for(const part of parts){
        const d = pointSegDist(part.x, part.y, this.px, this.py, this.x, this.y);
        if(d < part.r + this.r){
          game.damageEntity(game.boss, this.damage, this.owner);
          game.spawnBlood(this.x, this.y, this.color, 6);
          this.dead = true;
          return;
        }
      }
    }
  }
  draw(ctx){
    const a=Math.atan2(this.vy,this.vx);
    const len=this.fromDrone?14:(this.isBossBullet?30:22);
    ctx.strokeStyle=this.color; ctx.lineWidth=this.r*1.1; ctx.lineCap='round';
    ctx.shadowColor=this.color; ctx.shadowBlur=this.isBossBullet?18:10;
    ctx.beginPath();
    ctx.moveTo(this.x-Math.cos(a)*len, this.y-Math.sin(a)*len);
    ctx.lineTo(this.x,this.y);
    ctx.stroke(); ctx.shadowBlur=0;
    ctx.fillStyle='#ffffff';
    ctx.beginPath(); ctx.arc(this.x,this.y,this.r*0.6,0,TAU); ctx.fill();
  }
}

/* =========================================================================
   PLAYER — с поддержкой русской раскладки (ЦФЫВ)
   ========================================================================= */
class Player {
  constructor(x,y){
    const C=CONFIG.PLAYER;
    this.x=x; this.y=y; this.vx=0; this.vy=0;
    this.r=C.radius;
    this.hp=C.hp; this.maxHp=C.hp;
    this.angle=0; this.mag=C.magSize;
    this.reloading=false; this.reloadTimer=0;
    this.fireTimer=0; this.spread=0;
    this.hitFlash=0; this.alive=true;
    this.color='#22e6ff'; this.isPlayer=true;
    this.damageMult=1; this.fireRateMult=1;
    this.reloadMult=1; this.speedMult=1;
    this.magBonus=0;
    this.dashTimer=0; this.dashCooldownTimer=0; this.dashAngle=0;
    this.rageActive=false; this.rageTimer=0; this.rageCooldownTimer=0;
    this.invulnerable=false;
  }
  get maxMag(){ return CONFIG.PLAYER.magSize + this.magBonus; }
  get dashReady(){ return this.dashTimer <= 0 && this.dashCooldownTimer <= 0; }
  get rageReady(){ return !this.rageActive && this.rageCooldownTimer <= 0; }

  startReload(){
    if(this.reloading || this.mag===this.maxMag) return;
    this.reloading=true;
    this.reloadTimer=CONFIG.PLAYER.reloadTime*this.reloadMult;
    sound.reloadStart();
  }
  startDash(game){
    if(!this.alive || !this.dashReady) return false;
    let mx=0, my=0;
    const k=game.keys;
    if(k['w']||k['ц']||k['arrowup'])    my-=1;
    if(k['s']||k['ы']||k['arrowdown'])  my+=1;
    if(k['a']||k['ф']||k['arrowleft'])  mx-=1;
    if(k['d']||k['в']||k['arrowright']) mx+=1;
    if(game.moveStick.active){ mx=game.moveStick.dx; my=game.moveStick.dy; }
    if(Math.hypot(mx,my) > 0.1) this.dashAngle = Math.atan2(my, mx);
    else this.dashAngle = this.angle;
    this.dashTimer = CONFIG.PLAYER.dash.duration;
    this.dashCooldownTimer = CONFIG.PLAYER.dash.cooldown + CONFIG.PLAYER.dash.duration;
    this.invulnerable = true;
    game.rings.push(new Ring(this.x, this.y, '#7df9ff', 60, 0.35));
    for(let i=0;i<10;i++){
      const a = this.dashAngle + Math.PI + rand(-0.6, 0.6);
      const s = rand(120, 320);
      game.particles.push(new Particle(this.x, this.y, Math.cos(a)*s, Math.sin(a)*s, rand(0.2, 0.4), '#7df9ff', rand(2, 4)));
    }
    game.shake(3);
    sound.dash();
    return true;
  }
  startRage(game){
    if(!this.alive || !this.rageReady) return false;
    this.rageActive = true;
    this.rageTimer = CONFIG.PLAYER.rage.duration;
    game.rings.push(new Ring(this.x, this.y, '#ff3b6b', 90, 0.6));
    for(let i=0;i<24;i++){
      const a = rand(0, TAU);
      const s = rand(100, 400);
      game.particles.push(new Particle(this.x, this.y, Math.cos(a)*s, Math.sin(a)*s, rand(0.4, 0.8), '#ff5577', rand(2, 5)));
    }
    game.shake(8);
    sound.rage();
    return true;
  }
  update(dt,game){
    if(!this.alive) return;
    const C=CONFIG.PLAYER;
    this.hitFlash=Math.max(0,this.hitFlash-dt*4);
    if(this.dashTimer > 0){
      this.dashTimer -= dt;
      if(this.dashTimer <= 0){ this.dashTimer = 0; this.invulnerable = false; }
    }
    if(this.dashCooldownTimer > 0) this.dashCooldownTimer -= dt;
    if(this.rageActive){
      this.rageTimer -= dt;
      if(this.rageTimer <= 0){
        this.rageActive = false; this.rageTimer = 0;
        this.rageCooldownTimer = CONFIG.PLAYER.rage.cooldown;
        sound.rageEnd();
      }
    }
    if(this.rageCooldownTimer > 0 && !this.rageActive) this.rageCooldownTimer -= dt;

    const k=game.keys;
    let mx=0, my=0;
    if(k['w']||k['ц']||k['arrowup'])    my-=1;
    if(k['s']||k['ы']||k['arrowdown'])  my+=1;
    if(k['a']||k['ф']||k['arrowleft'])  mx-=1;
    if(k['d']||k['в']||k['arrowright']) mx+=1;
    const lk=Math.hypot(mx,my);
    if(lk>0){ mx/=lk; my/=lk; }
    let sprinting=!!k['shift'];
    if(game.moveStick.active){
      mx=game.moveStick.dx; my=game.moveStick.dy;
      if(game.moveStick.getMag()>0.88) sprinting=true;
    }
    if(this.dashTimer > 0){
      this.vx = Math.cos(this.dashAngle) * C.dash.speed;
      this.vy = Math.sin(this.dashAngle) * C.dash.speed;
      if(Math.random() < 0.9){
        game.particles.push(new Particle(this.x, this.y, rand(-30,30), rand(-30,30), rand(0.18, 0.32), '#7df9ff', rand(2, 3.5)));
      }
    } else {
      const rageSpeed = this.rageActive ? C.rage.speedMult : 1;
      const cheatSpd = game.cheatSpeedMult || 1;
      const spd = C.speed * this.speedMult * rageSpeed * cheatSpd * (sprinting ? C.sprintMult : 1);
      this.vx=lerp(this.vx,mx*spd,0.28);
      this.vy=lerp(this.vy,my*spd,0.28);
    }
    game.moveEntity(this,this.vx*dt,this.vy*dt);

    if(game.aimMode==='touch'){
      if(game.aimStick.active && game.aimStick.getMag()>0.2){
        this.angle=Math.atan2(game.aimStick.dy,game.aimStick.dx);
      }
    } else {
      const wm=game.worldMouse();
      this.angle=Math.atan2(wm.y-this.y, wm.x-this.x);
    }
    if(this.reloading){
      this.reloadTimer-=dt;
      if(this.reloadTimer<=0){ this.reloading=false; this.mag=this.maxMag; sound.reloadDone(); }
    }
    this.spread=Math.max(0,this.spread - C.spreadRecover*dt);
    this.fireTimer-=dt;
    let wantShoot=game.mouse.down;
    if(game.aimStick.active && game.aimStick.getMag()>0.35) wantShoot=true;
    if(wantShoot && !this.reloading && this.fireTimer<=0){
      if(this.mag>0 || game.infiniteAmmo) this.shoot(game);
      else this.startReload();
    }
  }
  shoot(game){
    const C=CONFIG.PLAYER;
    const rageDmg = this.rageActive ? C.rage.damageMult : 1;
    const rageRate = this.rageActive ? C.rage.fireRateMult : 1;
    const cheatDmg = game.cheatDamageMult || 1;
    const ang=this.angle + rand(-1,1)*(C.spread+this.spread);
    const mx=this.x+Math.cos(this.angle)*(this.r+8);
    const my=this.y+Math.sin(this.angle)*(this.r+8);
    game.bullets.push(new Bullet(mx,my,ang,C.bulletSpeed, C.damage*this.damageMult*rageDmg*cheatDmg,this,'#7df9ff',3.2));
    if(game.infiniteAmmo) this.mag = this.maxMag;
    else this.mag--;
    this.fireTimer=C.fireRate*this.fireRateMult*rageRate;
    this.spread=Math.min(this.spread+C.spreadPerShot, C.maxSpread);
    game.spawnMuzzle(mx,my,ang,'#7df9ff');
    game.shake(1.8);
    sound.shoot();
    if(this.mag<=0 && !game.infiniteAmmo) this.startReload();
  }
  draw(ctx){
    if(!this.alive) return;
    const r=this.r;
    drawShadow(ctx,this.x,this.y,r);
    if(this.rageActive){
      const t = performance.now() / 100;
      const pulse = 0.5 + Math.sin(t) * 0.15;
      ctx.save();
      ctx.globalAlpha = 0.35 + Math.sin(t*0.7) * 0.1;
      const grd = ctx.createRadialGradient(this.x, this.y, r, this.x, this.y, r + 22);
      grd.addColorStop(0, 'rgba(255, 60, 100, ' + pulse + ')');
      grd.addColorStop(1, 'rgba(255, 60, 100, 0)');
      ctx.fillStyle = grd;
      ctx.beginPath(); ctx.arc(this.x, this.y, r + 22, 0, TAU); ctx.fill();
      ctx.restore();
      ctx.save();
      ctx.globalAlpha = 0.55;
      ctx.strokeStyle = '#ff5577'; ctx.lineWidth = 2;
      ctx.beginPath(); ctx.arc(this.x, this.y, r + 8 + Math.sin(t*1.5)*3, 0, TAU); ctx.stroke();
      ctx.restore();
    }
    if(this.invulnerable){
      ctx.save();
      ctx.globalAlpha = 0.5;
      ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 3;
      ctx.beginPath(); ctx.arc(this.x, this.y, r + 6, 0, TAU); ctx.stroke();
      ctx.restore();
    }
    if(this.hitFlash>0){
      ctx.globalAlpha=this.hitFlash*0.6;
      ctx.fillStyle='#ff2244';
      ctx.beginPath(); ctx.arc(this.x,this.y,r+8,0,TAU); ctx.fill();
      ctx.globalAlpha=1;
    }
    ctx.save();
    ctx.translate(this.x,this.y);
    ctx.rotate(this.angle);
    const bodyColor = this.rageActive ? '#ff5577' : '#22e6ff';
    ctx.strokeStyle = bodyColor; ctx.lineWidth=5; ctx.lineCap='round';
    ctx.shadowColor = bodyColor; ctx.shadowBlur = this.rageActive ? 22 : 14;
    ctx.beginPath(); ctx.moveTo(r*0.4,0); ctx.lineTo(r+11,0); ctx.stroke();
    ctx.fillStyle='#0b2230';
    ctx.strokeStyle=bodyColor; ctx.lineWidth=3;
    ctx.beginPath(); ctx.arc(0,0,r,0,TAU); ctx.fill(); ctx.stroke();
    ctx.fillStyle= this.hitFlash>0 ? '#ff5566' : (this.rageActive ? '#ffcc66' : '#a8f7ff');
    ctx.beginPath(); ctx.arc(0,0,r*0.4,0,TAU); ctx.fill();
    ctx.restore();
    ctx.shadowBlur=0;
    drawHealthBar(ctx,this.x,this.y-r-16,54,this.hp,this.maxHp,'#22e6ff');
  }
}

/* =========================================================================
   ОБЩИЕ ФУНКЦИИ
   ========================================================================= */
function drawShadow(ctx,x,y,r){
  ctx.fillStyle='rgba(0,0,0,0.45)';
  ctx.beginPath();
  ctx.ellipse(x,y+r*0.75,r*0.95,r*0.42,0,0,TAU);
  ctx.fill();
}
function drawHealthBar(ctx,x,y,w,hp,maxHp,color){
  const pct=clamp(hp/maxHp,0,1);
  const h=5;
  const bx=x-w/2, by=y;
  ctx.fillStyle='rgba(0,0,0,0.65)';
  ctx.fillRect(bx-1,by-1,w+2,h+2);
  ctx.fillStyle='#1b1b2e';
  ctx.fillRect(bx,by,w,h);
  const r=Math.floor(lerp(255,60,pct));
  const g=Math.floor(lerp(60,255,pct));
  ctx.fillStyle= pct>0.3 ? `rgb(${r},${g},90)` : '#ff3355';
  ctx.fillRect(bx,by,w*pct,h);
  ctx.strokeStyle='rgba(255,255,255,0.25)';
  ctx.lineWidth=1;
  ctx.strokeRect(bx+0.5,by+0.5,w-1,h-1);
  if(color){
    ctx.strokeStyle=color; ctx.globalAlpha=0.5;
    ctx.strokeRect(bx-1.5,by-1.5,w+3,h+3);
    ctx.globalAlpha=1;
  }
}

/* =========================================================================
   BOT
   ========================================================================= */
class Bot {
  constructor(x,y,type,game){
    const cfg=CONFIG.BOT_TYPES[type];
    this.cfg=cfg; this.type=type;
    this.x=x; this.y=y; this.vx=0; this.vy=0;
    this.r=cfg.radius;
    const roomHpScale  = 1 + (game.room - 1) * CONFIG.ROOM_HP_SCALE;
    const roomDmgScale = 1 + (game.room - 1) * CONFIG.ROOM_DMG_SCALE;
    const roomSpdScale = 1 + (game.room - 1) * CONFIG.ROOM_SPEED_SCALE;
    this.maxHp = Math.round(cfg.hp * roomHpScale * game.difficulty.botHpMult);
    this.hp = this.maxHp;
    this.damage = (cfg.damage||0) * roomDmgScale * game.difficulty.botDmgMult;
    this.speed = cfg.speed * roomSpdScale;
    this.angle=rand(0,TAU);
    this.mag=cfg.magSize;
    this.reloading=false; this.reloadTimer=0;
    this.fireTimer=rand(0,0.8);
    this.hitFlash=0; this.alive=true; this.dead=false;
    this.state='patrol';
    this.target=null;
    this.patrolPoint=null; this.patrolTimer=0;
    this.strafeDir = Math.random()<0.5 ? -1 : 1;
    this.strafeTimer=rand(0.4,1.4);
    this.reactionTimer=0;
    this.color=cfg.color;
    this.isSapper = (type === 'sapper');
    this.arming = false; this.armTimer = 0;
    this.alarmTimer = 0;
    this.bobPhase = Math.random() * TAU;
  }
  startReload(){
    if(this.reloading) return;
    this.reloading=true;
    this.reloadTimer=this.cfg.reloadTime;
  }
  hasLOS(target,game){
    for(const w of game.walls){
      if(segIntersectsRect(this.x,this.y,target.x,target.y,w)) return false;
    }
    return true;
  }
  findTarget(game){
    let best=null, bestScore=Infinity;
    const cands=[];
    if(game.player && game.player.alive) cands.push(game.player);
    for(const b of game.bots) if(b!==this && b.alive && !b.isSapper) cands.push(b);
    for(const c of cands){
      const d=dist2(this.x,this.y,c.x,c.y);
      if(d>this.cfg.vision) continue;
      if(c!==game.player && d>420) continue;
      if(!this.hasLOS(c,game)) continue;
      const score = (c===game.player) ? d*0.55 : d;
      if(score<bestScore){ bestScore=score; best=c; }
    }
    return best;
  }
  dodgeVector(game){
    let dx=0, dy=0;
    for(const b of game.bullets){
      if(b.owner===this) continue;
      const rx=this.x-b.x, ry=this.y-b.y;
      const d=Math.hypot(rx,ry);
      if(d>190 || d<1e-3) continue;
      const bl=Math.hypot(b.vx,b.vy);
      if(bl<1) continue;
      const ux=b.vx/bl, uy=b.vy/bl;
      const dot=(rx/d)*ux+(ry/d)*uy;
      if(dot<0.86) continue;
      const strength=1-d/190;
      const proj=rx*ux+ry*uy;
      const perpX=rx-proj*ux, perpY=ry-proj*uy;
      const pl=Math.hypot(perpX,perpY);
      if(pl<0.5){
        dx += -uy*this.strafeDir*strength;
        dy +=  ux*this.strafeDir*strength;
      } else {
        dx += (perpX/pl)*strength;
        dy += (perpY/pl)*strength;
      }
    }
    return {x:dx, y:dy};
  }
  shoot(target,game){
    const cfg=this.cfg;
    const d=dist2(this.x,this.y,target.x,target.y);
    const t=d/cfg.bulletSpeed;
    const px=target.x+(target.vx||0)*t;
    const py=target.y+(target.vy||0)*t;
    let ang=Math.atan2(py-this.y, px-this.x);
    ang+=rand(-1,1)*cfg.spread;
    const mx=this.x+Math.cos(ang)*(this.r+7);
    const my=this.y+Math.sin(ang)*(this.r+7);
    game.bullets.push(new Bullet(mx,my,ang,cfg.bulletSpeed,this.damage,this,cfg.color,3));
    this.mag--;
    this.fireTimer=cfg.fireRate*rand(0.9,1.25);
    game.spawnMuzzle(mx,my,ang,cfg.color);
    const sv = sound.spatialVolume(this.x, this.y, game.cam.x, game.cam.y, 1000);
    if(sv > 0.05){
      if(this.type === 'sniper') sound.sniperShoot(sv);
      else if(this.type === 'heavy') sound.heavyShoot(sv);
      else sound.botShoot(sv);
    }
    if(this.mag<=0) this.startReload();
  }
  updateSapper(dt, game){
    this.hitFlash = Math.max(0, this.hitFlash - dt*4);
    this.bobPhase += dt * 12;
    const p = game.player;
    if(p && p.alive){
      const d = dist2(this.x, this.y, p.x, p.y);
      const ang = Math.atan2(p.y - this.y, p.x - this.x);
      this.angle = turnToward(this.angle, ang, dt * 9);
      const dodge = this.dodgeVector(game);
      let tx = Math.cos(this.angle) + dodge.x * 0.9;
      let ty = Math.sin(this.angle) + dodge.y * 0.9;
      const l = Math.hypot(tx, ty);
      if(l > 1e-3){ tx /= l; ty /= l; }
      this.vx = lerp(this.vx, tx * this.speed, 0.22);
      this.vy = lerp(this.vy, ty * this.speed, 0.22);
      if(!this.arming && d < 110){
        this.arming = true;
        this.armTimer = 1.6;
      }
      if(d < this.r + p.r + 4){ this.explode(game); return; }
    } else { this.vx *= 0.9; this.vy *= 0.9; }
    if(this.arming){
      this.armTimer -= dt;
      const fast = this.armTimer < 0.6;
      const sv = sound.spatialVolume(this.x, this.y, game.cam.x, game.cam.y, 700);
      if(sv > 0.03) sound.sapperAlarm(sv, fast);
      if(Math.random() < (fast ? 0.5 : 0.15)){
        game.particles.push(new Particle(
          this.x + rand(-8,8), this.y - this.r - 4 + rand(-4,4),
          rand(-40,40), rand(-80,-20),
          rand(0.2,0.4), '#ff2244', rand(1.5,2.5)
        ));
      }
      if(this.armTimer <= 0){ this.explode(game); return; }
    }
    game.moveEntity(this, this.vx * dt, this.vy * dt);
  }
  explode(game){
    if(!this.alive) return;
    const cfg = this.cfg;
    const R = cfg.explodeRadius || 130;
    const roomDmgScale = 1 + (game.room - 1) * CONFIG.ROOM_DMG_SCALE;
    const dmgPlayer = (cfg.explodeDmgPlayer || 40) * roomDmgScale * game.difficulty.botDmgMult;
    const dmgBots   = (cfg.explodeDmgBots   || 60) * roomDmgScale;
    if(game.player && game.player.alive){
      const d = dist2(this.x, this.y, game.player.x, game.player.y);
      if(d < R + game.player.r){
        const k = clamp(1 - d / (R + game.player.r), 0.4, 1);
        game.damageEntity(game.player, dmgPlayer * k, this);
      }
    }
    for(const b of game.bots){
      if(b === this || !b.alive) continue;
      const d = dist2(this.x, this.y, b.x, b.y);
      if(d < R + b.r){
        const k = clamp(1 - d / (R + b.r), 0.3, 1);
        game.damageEntity(b, dmgBots * k, this);
      }
    }
    if(game.boss && game.boss.alive){
      const parts = [{x: game.boss.x, y: game.boss.y, r: game.boss.r}].concat(game.boss.segments);
      for(const part of parts){
        const d = dist2(this.x, this.y, part.x, part.y);
        if(d < R + part.r){
          game.damageEntity(game.boss, 45, game.player);
          break;
        }
      }
    }
    game.rings.push(new Ring(this.x, this.y, '#ffcc00', R * 1.05, 0.5));
    game.rings.push(new Ring(this.x, this.y, '#ff2244', R * 0.75, 0.35));
    for(let i = 0; i < 36; i++){
      const a = rand(0, TAU), s = rand(120, 620);
      game.particles.push(new Particle(this.x, this.y,
        Math.cos(a)*s, Math.sin(a)*s,
        rand(0.3, 0.9), i % 2 ? '#ffcc00' : '#ff5577', rand(2, 4.5)));
    }
    game.shake(14);
    const sv = sound.spatialVolume(this.x, this.y, game.cam.x, game.cam.y, 1200);
    if(sv > 0.05){
      sound.explosion(sv);
      if(sv > 0.4) sound.sapperExplode(sv);
    }
    game.kills++;
    game.score += cfg.score;
    this.alive = false;
    this.dead = true;
  }
  update(dt,game){
    if(!this.alive) return;
    if(this.isSapper){ this.updateSapper(dt, game); return; }
    const cfg=this.cfg;
    this.hitFlash=Math.max(0,this.hitFlash-dt*4);
    this.fireTimer-=dt;
    this.reactionTimer-=dt;
    this.strafeTimer-=dt;
    if(this.strafeTimer<=0){
      this.strafeTimer=rand(0.55,1.6);
      this.strafeDir*=-1;
    }
    if(this.reloading){
      this.reloadTimer-=dt;
      if(this.reloadTimer<=0){ this.reloading=false; this.mag=cfg.magSize; }
    }
    const target=this.findTarget(game);
    this.target=target;
    let moveX=0, moveY=0;
    if(target){
      const d=dist2(this.x,this.y,target.x,target.y);
      const ang=Math.atan2(target.y-this.y, target.x-this.x);
      this.angle=turnToward(this.angle,ang,dt*7.5);
      const lowHp=this.hp<this.maxHp*0.35;
      if(lowHp && d<cfg.vision*0.85){
        this.state='retreat';
        moveX=-Math.cos(ang);
        moveY=-Math.sin(ang);
        moveX+=Math.cos(ang+Math.PI/2)*this.strafeDir*0.8;
        moveY+=Math.sin(ang+Math.PI/2)*this.strafeDir*0.8;
      } else {
        this.state='attack';
        const pref=cfg.preferred;
        let dir=0;
        if(d>pref*1.18) dir=1;
        else if(d<pref*0.72) dir=-1;
        moveX=Math.cos(ang)*dir;
        moveY=Math.sin(ang)*dir;
        moveX+=Math.cos(ang+Math.PI/2)*this.strafeDir*0.95;
        moveY+=Math.sin(ang+Math.PI/2)*this.strafeDir*0.95;
      }
      if(!this.reloading && this.fireTimer<=0 && this.hasLOS(target,game)){
        if(this.reactionTimer<=0){
          this.shoot(target,game);
          this.reactionTimer=rand(0.05,0.2);
        }
      }
      if(this.mag<=0 && !this.reloading) this.startReload();
    } else {
      this.state='patrol';
      this.patrolTimer-=dt;
      if(!this.patrolPoint || this.patrolTimer<=0 ||
         dist2(this.x,this.y,this.patrolPoint.x,this.patrolPoint.y)<45){
        this.patrolPoint=game.randomFreePoint();
        this.patrolTimer=rand(4,8);
      }
      const ang=Math.atan2(this.patrolPoint.y-this.y, this.patrolPoint.x-this.x);
      this.angle=turnToward(this.angle,ang,dt*5);
      moveX=Math.cos(ang);
      moveY=Math.sin(ang);
      if(this.mag<cfg.magSize && !this.reloading) this.startReload();
    }
    const dodge=this.dodgeVector(game);
    moveX+=dodge.x*1.7;
    moveY+=dodge.y*1.7;
    for(const o of game.bots){
      if(o===this || !o.alive || o.isSapper) continue;
      const dx=this.x-o.x, dy=this.y-o.y;
      const d=Math.hypot(dx,dy);
      const minD=this.r+o.r+8;
      if(d<minD && d>1e-3){
        const f=(1-d/minD)*1.6;
        moveX+=(dx/d)*f;
        moveY+=(dy/d)*f;
      }
    }
    const len=Math.hypot(moveX,moveY);
    if(len>1e-3){ moveX/=len; moveY/=len; }
    const spd=this.speed*(this.state==='retreat'?1.18:1);
    this.vx=lerp(this.vx, moveX*spd, 0.18);
    this.vy=lerp(this.vy, moveY*spd, 0.18);
    game.moveEntity(this,this.vx*dt,this.vy*dt);
  }
  draw(ctx){
    if(!this.alive) return;
    if(this.isSapper){ this.drawSapper(ctx); return; }
    const r=this.r;
    const col=this.cfg.color;
    drawShadow(ctx,this.x,this.y,r);
    if(this.hitFlash>0){
      ctx.globalAlpha=this.hitFlash*0.65;
      ctx.fillStyle='#ffffff';
      ctx.beginPath(); ctx.arc(this.x,this.y,r+7,0,TAU); ctx.fill();
      ctx.globalAlpha=1;
    }
    if(this.state==='retreat'){
      ctx.globalAlpha=0.25+Math.sin(performance.now()/120)*0.12;
      ctx.strokeStyle='#ffcc00'; ctx.lineWidth=2;
      ctx.beginPath(); ctx.arc(this.x,this.y,r+9,0,TAU); ctx.stroke();
      ctx.globalAlpha=1;
    }
    ctx.save();
    ctx.translate(this.x,this.y);
    ctx.rotate(this.angle);
    ctx.strokeStyle=col; ctx.lineWidth=this.type==='heavy'?6:4; ctx.lineCap='round';
    ctx.shadowColor=col; ctx.shadowBlur=12;
    ctx.beginPath();
    ctx.moveTo(r*0.4,0);
    ctx.lineTo(r+(this.type==='sniper'?16:10),0);
    ctx.stroke();
    ctx.fillStyle='#150c1a';
    ctx.strokeStyle=col; ctx.lineWidth=3;
    if(this.type==='heavy'){
      ctx.beginPath(); ctx.rect(-r*0.8,-r*0.8,r*1.6,r*1.6); ctx.fill(); ctx.stroke();
    } else if(this.type==='sniper'){
      ctx.beginPath();
      ctx.moveTo(r,0);
      ctx.lineTo(-r*0.75,-r*0.85);
      ctx.lineTo(-r*0.75, r*0.85);
      ctx.closePath();
      ctx.fill(); ctx.stroke();
    } else {
      ctx.beginPath(); ctx.arc(0,0,r,0,TAU); ctx.fill(); ctx.stroke();
    }
    ctx.fillStyle=col;
    ctx.beginPath(); ctx.arc(0,0,r*0.33,0,TAU); ctx.fill();
    ctx.restore();
    ctx.shadowBlur=0;
    drawHealthBar(ctx,this.x,this.y-r-15,46,this.hp,this.maxHp,col);
  }
  drawSapper(ctx){
    const r = this.r;
    const col = '#ffcc00';
    ctx.fillStyle = 'rgba(0,0,0,0.45)';
    ctx.beginPath();
    ctx.ellipse(this.x, this.y + r * 0.75, r * 0.95, r * 0.42, 0, 0, TAU);
    ctx.fill();
    if(this.arming){
      const blinkRate = this.armTimer < 0.6 ? 40 : 110;
      const pulse = 0.5 + Math.sin(performance.now() / blinkRate) * 0.5;
      ctx.save();
      ctx.globalAlpha = 0.35 + pulse * 0.35;
      const grd = ctx.createRadialGradient(this.x, this.y, r*0.5, this.x, this.y, r + 26);
      grd.addColorStop(0, 'rgba(255, 40, 80, 0.9)');
      grd.addColorStop(1, 'rgba(255, 40, 80, 0)');
      ctx.fillStyle = grd;
      ctx.beginPath(); ctx.arc(this.x, this.y, r + 26, 0, TAU); ctx.fill();
      ctx.restore();
    }
    if(this.hitFlash > 0){
      ctx.globalAlpha = this.hitFlash * 0.65;
      ctx.fillStyle = '#ffffff';
      ctx.beginPath(); ctx.arc(this.x, this.y, r + 6, 0, TAU); ctx.fill();
      ctx.globalAlpha = 1;
    }
    const bodyStroke = this.arming ? '#ff2244' : col;
    ctx.save();
    ctx.translate(this.x, this.y);
    ctx.rotate(performance.now() * 0.0035 + this.bobPhase * 0.1);
    ctx.fillStyle = '#2a1a05';
    ctx.strokeStyle = bodyStroke;
    ctx.lineWidth = 3;
    ctx.shadowColor = bodyStroke;
    ctx.shadowBlur = this.arming ? 22 : 12;
    ctx.beginPath();
    const sides = 6;
    for(let i = 0; i < sides; i++){
      const a = (i / sides) * TAU;
      const px = Math.cos(a) * r;
      const py = Math.sin(a) * r;
      if(i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
    }
    ctx.closePath();
    ctx.fill(); ctx.stroke();
    ctx.shadowBlur = 0;
    ctx.strokeStyle = bodyStroke;
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(-r*0.45, 0); ctx.lineTo(r*0.45, 0);
    ctx.moveTo(0, -r*0.45); ctx.lineTo(0, r*0.45);
    ctx.stroke();
    const blink = this.arming
      ? (Math.sin(performance.now() / (this.armTimer < 0.6 ? 40 : 90)) > 0 ? 1 : 0.15)
      : 1;
    ctx.globalAlpha = blink;
    ctx.fillStyle = this.arming ? '#ff2244' : '#ffee66';
    ctx.shadowColor = this.arming ? '#ff2244' : '#ffcc00';
    ctx.shadowBlur = 16;
    ctx.beginPath(); ctx.arc(0, 0, r * 0.28, 0, TAU); ctx.fill();
    ctx.shadowBlur = 0;
    ctx.globalAlpha = 1;
    ctx.restore();
    if(this.arming){
      const t = 1 - this.armTimer / 1.6;
      const yOff = -r - 22 - Math.sin(performance.now() / 80) * 3;
      ctx.save();
      ctx.globalAlpha = 0.85 + Math.sin(performance.now() / 60) * 0.15;
      ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
      ctx.font = 'bold ' + (18 + t * 10) + 'px Consolas, monospace';
      ctx.fillStyle = '#ff2244';
      ctx.shadowColor = '#ff2244'; ctx.shadowBlur = 18;
      ctx.fillText('!', this.x, yOff);
      ctx.restore();
    }
    drawHealthBar(ctx, this.x, this.y - r - 15, 46, this.hp, this.maxHp, col);
  }
}

/* =========================================================================
   BOSS
   ========================================================================= */
class BossSnake {
  constructor(x, y, game){
    this.x = x; this.y = y;
    this.vx = 0; this.vy = 0;
    this.r = 30;
    this.angle = rand(0, TAU);
    this.isBoss = true; this.isPlayer = false;
    this.alive = true; this.dead = false;
    this.hitFlash = 0;
    const roomIdx = Math.max(1, Math.floor(game.room / CONFIG.BOSS_EVERY));
    const baseHp = 2400;
    const roomScale = 1 + (roomIdx - 1) * 0.6;
    this.maxHp = Math.round(baseHp * roomScale * game.difficulty.botHpMult);
    this.hp = this.maxHp;
    this.speed = 160;
    this.turnSpeed = 3.0;
    this.segmentCount = 7;
    this.segmentSpacing = 30;
    this.segments = [];
    for(let i = 0; i < this.segmentCount; i++){
      this.segments.push({
        x: x - (i + 1) * this.segmentSpacing,
        y: y, r: this.r * 0.82, angle: 0
      });
    }
    this.fireTimer = 2.0;
    this.contactDamage = 22 * game.difficulty.botDmgMult;
    this.bulletDamage = 12 * game.difficulty.botDmgMult;
    this.contactCooldown = 0;
    this.hissTimer = 0;
  }
  update(dt, game){
    if(!this.alive) return;
    this.hitFlash = Math.max(0, this.hitFlash - dt * 3);
    let speedMult = 1;
    if(game.player && game.player.alive){
      const d = dist2(this.x, this.y, game.player.x, game.player.y);
      if(d < 320) speedMult = 1.45;
      else if(d > 750) speedMult = 1.30;
      const targetAng = Math.atan2(game.player.y - this.y, game.player.x - this.x);
      this.angle = turnToward(this.angle, targetAng, dt * this.turnSpeed);
    }
    const spd = this.speed * speedMult;
    this.vx = Math.cos(this.angle) * spd;
    this.vy = Math.sin(this.angle) * spd;
    game.moveEntity(this, this.vx * dt, this.vy * dt);
    let prevX = this.x, prevY = this.y;
    for(let i = 0; i < this.segments.length; i++){
      const seg = this.segments[i];
      const dx = prevX - seg.x;
      const dy = prevY - seg.y;
      const d = Math.hypot(dx, dy);
      if(d > this.segmentSpacing){
        const nx = dx / d, ny = dy / d;
        seg.x = prevX - nx * this.segmentSpacing;
        seg.y = prevY - ny * this.segmentSpacing;
      }
      seg.angle = Math.atan2(prevY - seg.y, prevX - seg.x);
      prevX = seg.x; prevY = seg.y;
    }
    this.contactCooldown -= dt;
    if(this.contactCooldown <= 0 && game.player && game.player.alive){
      const parts = [{x: this.x, y: this.y, r: this.r}].concat(this.segments);
      for(const part of parts){
        const d = dist2(part.x, part.y, game.player.x, game.player.y);
        if(d < part.r + game.player.r){
          game.damageEntity(game.player, this.contactDamage, this);
          this.contactCooldown = 0.55;
          break;
        }
      }
    }
    this.fireTimer -= dt;
    if(this.fireTimer <= 0 && game.player && game.player.alive){
      this.fireBurst(game);
      this.fireTimer = rand(1.6, 2.4);
    }
    this.hissTimer -= dt;
    if(this.hissTimer <= 0){
      const sv = sound.spatialVolume(this.x, this.y, game.cam.x, game.cam.y, 1200);
      if(sv > 0.05) sound.bossHiss();
      this.hissTimer = rand(3.5, 6.5);
    }
  }
  fireBurst(game){
    if(!game.player || !game.player.alive) return;
    const baseAng = Math.atan2(game.player.y - this.y, game.player.x - this.x);
    const count = 5;
    for(let i = 0; i < count; i++){
      const offset = (i - (count - 1) / 2) * 0.16;
      const ang = baseAng + offset + rand(-0.05, 0.05);
      const mx = this.x + Math.cos(ang) * (this.r + 6);
      const my = this.y + Math.sin(ang) * (this.r + 6);
      const b = new Bullet(mx, my, ang, 540, this.bulletDamage, this, '#ff5577', 4);
      b.isBossBullet = true;
      game.bullets.push(b);
    }
    game.spawnMuzzle(this.x + Math.cos(baseAng)*(this.r+4), this.y + Math.sin(baseAng)*(this.r+4), baseAng, '#ff5577');
    sound.heavyShoot(1);
    game.shake(4);
  }
  draw(ctx){
    for(let i = this.segments.length - 1; i >= 0; i--){
      this.drawCube(ctx, this.segments[i], i, false);
    }
    this.drawCube(ctx, {x: this.x, y: this.y, r: this.r, angle: this.angle}, -1, true);
  }
  drawCube(ctx, part, idx, isHead){
    const r = part.r;
    const flash = this.hitFlash > 0;
    ctx.fillStyle = 'rgba(0,0,0,0.5)';
    ctx.beginPath();
    ctx.ellipse(part.x, part.y + r * 0.75, r * 0.95, r * 0.4, 0, 0, TAU);
    ctx.fill();
    ctx.save();
    ctx.translate(part.x, part.y);
    ctx.rotate((part.angle || 0) + performance.now() * 0.0015 * (isHead ? 2 : 1));
    const size = r * 1.7;
    ctx.fillStyle = flash ? '#ffffff' : (isHead ? '#1a0a2a' : '#160822');
    ctx.fillRect(-size/2, -size/2, size, size);
    const borderCol = flash ? '#ffffff' : (isHead ? '#ff3b6b' : '#b06bff');
    ctx.strokeStyle = borderCol;
    ctx.lineWidth = 3;
    ctx.shadowColor = borderCol;
    ctx.shadowBlur = isHead ? 20 : 12;
    ctx.strokeRect(-size/2, -size/2, size, size);
    ctx.shadowBlur = 0;
    ctx.strokeStyle = flash ? '#ffffff' : 'rgba(255,150,200,0.55)';
    ctx.lineWidth = 1.5;
    ctx.beginPath();
    ctx.moveTo(-size/2 + 4, -size/2 + 4);
    ctx.lineTo(size/2 - 4, size/2 - 4);
    ctx.moveTo(size/2 - 4, -size/2 + 4);
    ctx.lineTo(-size/2 + 4, size/2 - 4);
    ctx.stroke();
    if(isHead){
      ctx.fillStyle = '#ff5577';
      ctx.shadowColor = '#ff3b6b';
      ctx.shadowBlur = 22;
      ctx.beginPath();
      ctx.arc(0, 0, r * 0.4, 0, TAU);
      ctx.fill();
      ctx.shadowBlur = 0;
      ctx.fillStyle = '#ffffff';
      ctx.beginPath();
      ctx.arc(r * 0.55, -r * 0.32, 2.8, 0, TAU);
      ctx.arc(r * 0.55,  r * 0.32, 2.8, 0, TAU);
      ctx.fill();
    } else {
      ctx.fillStyle = '#b06bff';
      ctx.shadowColor = '#b06bff';
      ctx.shadowBlur = 12;
      ctx.beginPath();
      ctx.arc(0, 0, r * 0.24, 0, TAU);
      ctx.fill();
      ctx.shadowBlur = 0;
    }
    ctx.restore();
  }
}

/* =========================================================================
   DRONE
   ========================================================================= */
class Drone {
  constructor(owner,index){
    this.owner=owner;
    const ring = Math.floor(index / 8);
    const posInRing = index % 8;
    this.orbitR = 58 + ring * 20;
    this.angle = posInRing * (TAU / 8) + ring * 0.4;
    this.x = owner.x + Math.cos(this.angle) * this.orbitR;
    this.y = owner.y + Math.sin(this.angle) * this.orbitR;
    this.r = 8;
    this.fireTimer = rand(0, 0.4);
    this.target = null;
    this.aimAngle = 0;
    this.pulse = Math.random() * TAU;
    this.orbitSpeed = 1.0 + ring * 0.15;
  }
  update(dt,game){
    this.angle += dt * this.orbitSpeed;
    this.pulse += dt * 4;
    const tx = this.owner.x + Math.cos(this.angle) * this.orbitR;
    const ty = this.owner.y + Math.sin(this.angle) * this.orbitR;
    const f = Math.min(1, dt * 14);
    this.x = lerp(this.x, tx, f);
    this.y = lerp(this.y, ty, f);
    let best=null, bestD=520;
    for(const b of game.bots){
      if(!b.alive) continue;
      const d=dist2(this.x,this.y,b.x,b.y);
      if(d<bestD){ bestD=d; best=b; }
    }
    if(game.boss && game.boss.alive){
      const d = dist2(this.x, this.y, game.boss.x, game.boss.y);
      if(d < bestD){ bestD = d; best = game.boss; }
    }
    this.target=best;
    if(best){
      const targetAng=Math.atan2(best.y-this.y, best.x-this.x);
      this.aimAngle=turnToward(this.aimAngle,targetAng,dt*9);
      this.fireTimer-=dt;
      if(this.fireTimer<=0){
        this.shoot(game);
        this.fireTimer=0.36*game.droneFireRateMult;
      }
    }
  }
  shoot(game){
    const dmg = 2.33 * game.droneDamageMult;
    const ang=this.aimAngle+rand(-1,1)*0.07;
    const mx=this.x+Math.cos(ang)*(this.r+3);
    const my=this.y+Math.sin(ang)*(this.r+3);
    const b=new Bullet(mx,my,ang,880,dmg,this.owner,'#7df9ff',2.2);
    b.fromDrone=true;
    game.bullets.push(b);
    game.spawnMuzzle(mx,my,ang,'#aaffff');
    sound.droneShoot();
  }
  draw(ctx){
    const t=this.pulse;
    ctx.save();
    ctx.globalAlpha=0.35+Math.sin(t)*0.1;
    ctx.fillStyle='rgba(125,249,255,0.35)';
    ctx.beginPath(); ctx.arc(this.x,this.y,this.r+4,0,TAU); ctx.fill();
    ctx.restore();
    ctx.save();
    ctx.shadowColor='#7df9ff'; ctx.shadowBlur=10;
    ctx.fillStyle='#0b2230';
    ctx.beginPath(); ctx.arc(this.x,this.y,this.r,0,TAU); ctx.fill();
    ctx.strokeStyle='#7df9ff'; ctx.lineWidth=2; ctx.stroke();
    ctx.shadowBlur=0;
    ctx.restore();
    ctx.fillStyle='#aaffff';
    ctx.beginPath(); ctx.arc(this.x,this.y,this.r*0.38,0,TAU); ctx.fill();
    if(this.target){
      ctx.save();
      ctx.globalAlpha=0.55;
      ctx.strokeStyle='#7df9ff'; ctx.lineWidth=1.5;
      ctx.beginPath();
      ctx.moveTo(this.x,this.y);
      ctx.lineTo(this.x+Math.cos(this.aimAngle)*14, this.y+Math.sin(this.aimAngle)*14);
      ctx.stroke();
      ctx.restore();
    }
  }
}

/* =========================================================================
   УЛУЧШЕНИЯ
   ========================================================================= */
const UPGRADE_DEFS = [
  { id:'heal', name:'Аптечка', desc:'+40 HP', cost:80, costMul:1.10,
    apply:g=>{ g.player.hp=Math.min(g.player.maxHp, g.player.hp+40); } },
  { id:'maxhp', name:'Броня', desc:'+25 макс. HP', cost:200, costMul:1.30,
    apply:g=>{ g.player.maxHp+=25; g.player.hp+=25; } },
  { id:'damage', name:'Урон', desc:'+15% урона', cost:300, costMul:1.35,
    apply:g=>{ g.player.damageMult*=1.15; } },
  { id:'firerate', name:'Скорострельность', desc:'+15% скорости огня', cost:300, costMul:1.35,
    apply:g=>{ g.player.fireRateMult*=0.87; } },
  { id:'reload', name:'Быстрая перезарядка', desc:'-15% времени перезарядки', cost:250, costMul:1.30,
    apply:g=>{ g.player.reloadMult*=0.85; } },
  { id:'speed', name:'Ускорение', desc:'+12% скорости', cost:200, costMul:1.30,
    apply:g=>{ g.player.speedMult*=1.12; } },
  { id:'mag', name:'Большой магазин', desc:'+8 патронов', cost:250, costMul:1.35,
    apply:g=>{ g.player.magBonus+=8; } },
  { id:'drone', name:'Дрон', desc:'+1 дрон-помощник', cost:500, costMul:1.55,
    apply:g=>{ g.addDrone(); } },
  { id:'dronedmg', name:'Урон дронов', desc:'+25% урона дронов', cost:280, costMul:1.40,
    apply:g=>{ g.droneDamageMult*=1.25; } },
  { id:'dronefirerate', name:'Скорость дронов', desc:'+20% скорости огня дронов', cost:280, costMul:1.40,
    apply:g=>{ g.droneFireRateMult*=0.83; } }
];

/* =========================================================================
   ЧИТ-КОДЫ
   ========================================================================= */
const CHEAT_CODES = [
  { code: '100xdrones',    action: 'drones100',    desc: '+100 дронов' },
  { code: '50xdrones',     action: 'drones50',     desc: '+50 дронов' },
  { code: '10xdrones',     action: 'drones10',     desc: '+10 дронов' },
  { code: '100upgrades',   action: 'upgrades100',  desc: 'Все улучшения до 100 ур.' },
  { code: 'maxupgrades',   action: 'upgrades100',  desc: 'Все улучшения до 100 ур.' },
  { code: 'godmode',       action: 'godmode',      desc: 'Бессмертие (toggle)' },
  { code: 'infiniteammo',  action: 'infiniteammo', desc: 'Бесконечные патроны' },
  { code: 'superdamage',   action: 'superdamage',  desc: '×10 урон (toggle)' },
  { code: 'superspeed',    action: 'superspeed',   desc: '×3 скорость (toggle)' },
  { code: 'killall',       action: 'killall',      desc: 'Убить всех врагов' },
  { code: 'getcoins',      action: 'getcoins',     desc: '+10000 очков' },
  { code: 'spawnboss',     action: 'spawnboss',    desc: 'Вызвать босса' },
  { code: 'healme',        action: 'healme',       desc: 'Полное HP' },
  { code: 'resetcd',       action: 'resetcd',      desc: 'Сбросить кулдауны' },
  { code: 'nuke',          action: 'nuke',         desc: 'Взрыв всего экрана' },
  { code: 'longerage',     action: 'longerage',    desc: 'Ярость 60 сек' }
];

/* =========================================================================
   GAME
   ========================================================================= */
class Game {
  constructor(canvas){
    this.canvas=canvas;
    this.ctx=canvas.getContext('2d');
    this.isTouch=('ontouchstart' in window)||(navigator.maxTouchPoints>0);
    this.dpr=1;
    this.state='menu';
    this.keys={};
    this.mouse={x:0,y:0,down:false};
    this.aimMode='mouse';
    this.selectedDifficulty = 'medium';

    this.cam={x:0,y:0};
    this.shakeAmount=0;
    this.shakeX=0; this.shakeY=0;
    this.damageFlash=0;

    this.walls=[];
    this.bullets=[];
    this.particles=[];
    this.rings=[];
    this.bots=[];
    this.drones=[];
    this.player=null;
    this.boss = null;
    this.bossDefeatedTimer = 0;

    this.room=0;
    this.score=0;
    this.kills=0;
    this.toSpawn=0;
    this.spawnTimer=0;
    this.roomActive=false;
    this.message={text:'',sub:'',t:0,max:1};
    this.elapsed=0;
    this.respawnsLeft=0;
    this.deathTimer=0;
    this.difficulty = DIFFICULTIES.medium;

    this.upgradeCounts={};
    this.availableUpgrades=UPGRADE_DEFS.slice();
    this.upgradeCards=[];
    this.droneDamageMult=1;
    this.droneFireRateMult=1;
    this._lowHpWarnTimer = 0;
    this._adCooldown = 0;

    this.cheatBuffer = '';
    this.cheatBufferTime = 0;
    this.cheatBufferTimeout = 2.5;
    this.cheatMaxLen = 20;
    this.cheatHistory = [];
    this.godMode = false;
    this.infiniteAmmo = false;
    this.cheatDamageMult = 1;
    this.cheatSpeedMult = 1;
    this.pending = { droneCount:0, heal:false, resetCD:false, longRage:false, spawnBoss:false, maxUpgrades:false };

    this.showCheatList = false;
    this.settingsMessage = '';
    this.settingsMessageColor = '#7df9ff';
    this.settingsMessageUntil = 0;
    this.settingsReturnState = 'menu';

    this.highScore = 0;
    this.totalKills = 0;
    this.totalRooms = 0;

    this.moveStick=new VirtualJoystick(58);
    this.aimStick=new VirtualJoystick(58);

    this.startBtn={x:0,y:0,w:0,h:0};
    this.restartBtn={x:0,y:0,w:0,h:0};
    this.resumeBtn={x:0,y:0,w:0,h:0};
    this.pauseMenuBtn={x:0,y:0,w:0,h:0};
    this.pauseSoundBtn={x:0,y:0,w:0,h:0};
    this.pauseSettingsBtn={x:0,y:0,w:0,h:0};
    this.reloadBtn={x:0,y:0,w:0,h:0};
    this.dashBtn={x:0,y:0,w:0,h:0};
    this.rageBtn={x:0,y:0,w:0,h:0};
    this.pauseBtn={x:0,y:0,w:0,h:0};
    this.fullscreenBtn={x:0,y:0,w:0,h:0};
    this.soundBtn={x:0,y:0,w:0,h:0};
    this.settingsBtn={x:0,y:0,w:0,h:0};
    this.upgradeContinueBtn={x:0,y:0,w:0,h:0};
    this.difficultyBtns=[];

    this.settingsBackBtn={x:0,y:0,w:0,h:0};
    this.settingsSoundBtn={x:0,y:0,w:0,h:0};
    this.settingsVolumeDownBtn={x:0,y:0,w:0,h:0};
    this.settingsVolumeUpBtn={x:0,y:0,w:0,h:0};
    this.settingsCheatInputRect={x:0,y:0,w:0,h:0};
    this.settingsCheatActivateBtn={x:0,y:0,w:0,h:0};
    this.settingsShowCodesBtn={x:0,y:0,w:0,h:0};
    this.settingsResetCheatsBtn={x:0,y:0,w:0,h:0};

    this.inputEl = document.getElementById('cheatInput');

    this.layoutUI();
    this.setupInput();
    this.loadLocalProgress();
  }

  loadLocalProgress(){
    try {
      this.highScore = parseInt(localStorage.getItem('neon_highScore') || '0', 10);
      this.totalKills = parseInt(localStorage.getItem('neon_totalKills') || '0', 10);
      this.totalRooms = parseInt(localStorage.getItem('neon_totalRooms') || '0', 10);
    } catch(e) {}
  }
  saveLocalProgress(){
    try {
      localStorage.setItem('neon_highScore', String(this.highScore));
      localStorage.setItem('neon_totalKills', String(this.totalKills));
      localStorage.setItem('neon_totalRooms', String(this.totalRooms));
    } catch(e) {}
  }

  layoutUI(){
    const W=CONFIG.W, H=CONFIG.H;
    const cols = W >= 820 ? 4 : 2;
    const rows = Math.ceil(4 / cols);
    const gapX = 12, gapY = 12;
    const bw = Math.min(210, (W - 60 - (cols-1)*gapX) / cols);
    const bh = 72;
    const totalW = cols*bw + (cols-1)*gapX;
    const startX = (W - totalW) / 2;
    const startY = H * 0.40;
    this.difficultyBtns = DIFFICULTY_ORDER.map((k, i) => ({
      key: k,
      x: startX + (i % cols) * (bw + gapX),
      y: startY + Math.floor(i / cols) * (bh + gapY),
      w: bw, h: bh
    }));
    const startBtnY = startY + rows * (bh + gapY) + 18;
    this.startBtn = { x: W/2 - 160, y: startBtnY, w: 260, h: 56 };
    this.fullscreenBtn = { x: W/2 + 110, y: startBtnY + 2, w: 52, h: 52 };
    this.soundBtn = { x: W/2 + 170, y: startBtnY + 2, w: 52, h: 52 };
    const settingsBtnY = startBtnY + 56 + 12;
    this.settingsBtn = { x: W/2 - 120, y: settingsBtnY, w: 240, h: 42 };

    const pauseBtnW = 260;
    const pauseBtnH = 56;
    const pauseCx = W/2 - pauseBtnW/2;
    const pauseStartY = H*0.38;
    this.resumeBtn = { x: pauseCx, y: pauseStartY, w: pauseBtnW, h: pauseBtnH };
    const rowY = pauseStartY + pauseBtnH + 14;
    const smallGap = 8;
    const smallW = 148;
    const smallTotal = smallW * 3 + smallGap * 2;
    const smallStart = W/2 - smallTotal/2;
    this.pauseSoundBtn    = { x: smallStart, y: rowY, w: smallW, h: 48 };
    this.pauseSettingsBtn = { x: smallStart + smallW + smallGap, y: rowY, w: smallW, h: 48 };
    this.pauseMenuBtn     = { x: smallStart + (smallW + smallGap) * 2, y: rowY, w: smallW, h: 48 };

    this.restartBtn = { x: W/2 - 130, y: H*0.62, w: 260, h: 64 };

    const btnSize = 66;
    const btnX = W - 24 - btnSize;
    this.reloadBtn = { x: btnX, y: H - 105, w: btnSize, h: btnSize };
    this.dashBtn   = { x: btnX, y: H - 105 - (btnSize + 10), w: btnSize, h: btnSize };
    this.rageBtn   = { x: btnX, y: H - 105 - (btnSize + 10) * 2, w: btnSize, h: btnSize };
    this.pauseBtn  = { x: btnX, y: H - 105 - (btnSize + 10) * 3, w: btnSize, h: btnSize };

    this.upgradeContinueBtn = { x: W/2 - 140, y: H - 88, w: 280, h: 58 };

    const cx = W/2;
    this.settingsSoundBtn = { x: cx - 110, y: H*0.20, w: 220, h: 50 };
    this.settingsVolumeDownBtn = { x: cx - 220, y: H*0.30, w: 52, h: 52 };
    this.settingsVolumeUpBtn   = { x: cx + 168, y: H*0.30, w: 52, h: 52 };
    this.settingsCheatInputRect = { x: cx - 240, y: H*0.46, w: 480, h: 52 };
    this.settingsCheatActivateBtn = { x: cx - 110, y: H*0.46 + 62, w: 220, h: 44 };
    this.settingsShowCodesBtn = { x: cx - 190, y: H*0.64, w: 180, h: 40 };
    this.settingsResetCheatsBtn = { x: cx + 10, y: H*0.64, w: 180, h: 40 };
    this.settingsBackBtn = { x: cx - 130, y: H - 80, w: 260, h: 56 };

    if(this.state==='upgrade') this.layoutUpgradeCards();
  }

  layoutUpgradeCards(){
    const W=CONFIG.W, H=CONFIG.H;
    const list=this.availableUpgrades;
    let cols=5;
    if(W<1200) cols=4;
    if(W<900) cols=3;
    if(W<640) cols=2;
    const rows=Math.ceil(list.length/cols);
    const gap=14, padding=40;
    const cardW=Math.min(240, (W - padding*2 - gap*(cols-1))/cols);
    const cardH=Math.min(150, (H*0.52 - gap*(rows-1))/rows);
    const totalW=cols*cardW+gap*(cols-1);
    const startX=(W-totalW)/2;
    const startY=Math.max(H*0.26, (H - cardH*rows - gap*(rows-1))/2 + 20);
    this.upgradeCards=list.map((up,i)=>({
      upgrade:up,
      x:startX + (i%cols)*(cardW+gap),
      y:startY + Math.floor(i/cols)*(cardH+gap),
      w:cardW, h:cardH
    }));
  }

  getUpgradeCost(up){
    const c=this.upgradeCounts[up.id]||0;
    return Math.round(up.cost*Math.pow(up.costMul,c));
  }

  /* ======================== ЧИТ-СИСТЕМА ======================== */
  handleCheatInput(key){
    if(key.length !== 1) return;
    if(key === ' ') return;
    const now = performance.now() / 1000;
    if(now - this.cheatBufferTime > this.cheatBufferTimeout) this.cheatBuffer = '';
    this.cheatBufferTime = now;
    this.cheatBuffer += key.toLowerCase();
    if(this.cheatBuffer.length > this.cheatMaxLen) this.cheatBuffer = this.cheatBuffer.slice(-this.cheatMaxLen);
    for(const c of CHEAT_CODES){
      if(this.cheatBuffer.endsWith(c.code)){
        this.activateCheat(c);
        this.cheatBuffer = '';
        return;
      }
    }
  }
  tryActivateCheatFromInput(){
    const raw = (this.inputEl.value || '').trim().toLowerCase();
    if(!raw){ this.showSettingsMessage('Введи код', '#ffcc00'); return; }
    const found = CHEAT_CODES.find(c => c.code === raw);
    if(found){
      this.activateCheat(found);
      this.inputEl.value = '';
      this.inputEl.blur();
    } else {
      this.showSettingsMessage('Неизвестный код: "' + raw + '"', '#ff5577');
      sound.deny();
    }
  }
  activateCheat(cheat){
    sound.cheatOn();
    this.cheatHistory.push({ desc: cheat.desc, time: performance.now() });
    if(this.cheatHistory.length > 5) this.cheatHistory.shift();
    this.executeCheat(cheat.action);
    this.showSettingsMessage('✓ ' + cheat.desc, '#7df9ff');
  }
  showSettingsMessage(text, color){
    this.settingsMessage = text;
    this.settingsMessageColor = color || '#7df9ff';
    this.settingsMessageUntil = performance.now() + 2800;
  }
  executeCheat(action){
    const hasPlayer = !!this.player;
    switch(action){
      case 'drones100':    this.cheatAddDrones(100, hasPlayer); break;
      case 'drones50':     this.cheatAddDrones(50, hasPlayer); break;
      case 'drones10':     this.cheatAddDrones(10, hasPlayer); break;
      case 'upgrades100':  this.cheatMaxUpgrades(hasPlayer); break;
      case 'godmode':      this.cheatToggleGod(); break;
      case 'infiniteammo': this.cheatToggleInfiniteAmmo(); break;
      case 'superdamage':  this.cheatToggleDamage(); break;
      case 'superspeed':   this.cheatToggleSpeed(); break;
      case 'killall':      this.cheatKillAll(); break;
      case 'getcoins':     this.cheatAddScore(10000); break;
      case 'spawnboss':    this.cheatSpawnBoss(hasPlayer); break;
      case 'healme':       this.cheatHeal(hasPlayer); break;
      case 'resetcd':      this.cheatResetCooldowns(hasPlayer); break;
      case 'nuke':         this.cheatNuke(); break;
      case 'longerage':    this.cheatLongRage(hasPlayer); break;
    }
  }

  cheatAddDrones(n, hasPlayer){
    if(!hasPlayer){
      this.pending.droneCount += n;
      this.showSettingsMessage('Дроны (+' + n + ') будут добавлены при старте', '#ffcc00');
      return;
    }
    const canAdd = Math.max(0, 200 - this.drones.length);
    const toAdd = Math.min(n, canAdd);
    for(let i = 0; i < toAdd; i++){
      this.drones.push(new Drone(this.player, this.drones.length));
    }
    this.rings.push(new Ring(this.player.x, this.player.y, '#7df9ff', 140, 0.8));
    for(let i = 0; i < 40; i++){
      const a = rand(0, TAU), s = rand(100, 500);
      this.particles.push(new Particle(this.player.x, this.player.y,
        Math.cos(a)*s, Math.sin(a)*s, rand(0.3, 0.8), '#7df9ff', rand(2, 4)));
    }
    this.showMessage('ЧИТ АКТИВИРОВАН', '+' + toAdd + ' дронов (всего: ' + this.drones.length + ')', 2.5);
  }

  /** Прокачивает все доступные улучшения до 100 уровня, КРОМЕ скорости (кап 3 ур.) */
  cheatMaxUpgrades(hasPlayer){
    if(!hasPlayer){
      this.pending.maxUpgrades = true;
      this.showSettingsMessage('Все улучшения до 100 ур. применятся при старте (кроме скорости)', '#ffcc00');
      return;
    }
    const targetLevel = 100;
    const SPEED_CAP = 3; // скорость капаем на 3 уровнях, иначе игрок летает
    let totalLevelsApplied = 0;
    for(const up of this.availableUpgrades){
      const current = this.upgradeCounts[up.id] || 0;
      // Специальное исключение для скорости — кап 3
      const target = (up.id === 'speed') ? SPEED_CAP : targetLevel;
      const diff = target - current;
      if(diff <= 0) continue;
      for(let i = 0; i < diff; i++){
        try { up.apply(this); } catch(e) {}
      }
      this.upgradeCounts[up.id] = target;
      totalLevelsApplied += diff;
    }
    // Ограничение HP разумным максимумом
    if(this.player.maxHp > 9999){
      this.player.maxHp = 9999;
      this.player.hp = Math.min(this.player.hp, this.player.maxHp);
    }
    // Ограничение дронов
    if(this.drones.length > 200){
      this.drones.length = 200;
    }
    // Эффекты
    if(this.player){
      this.rings.push(new Ring(this.player.x, this.player.y, '#ffd166', 180, 1.2));
      this.rings.push(new Ring(this.player.x, this.player.y, '#a06bff', 130, 1.0));
      for(let i = 0; i < 60; i++){
        const a = rand(0, TAU), s = rand(150, 700);
        this.particles.push(new Particle(this.player.x, this.player.y,
          Math.cos(a)*s, Math.sin(a)*s, rand(0.5, 1.2),
          i % 2 ? '#ffd166' : '#a06bff', rand(2, 5)));
      }
    }
    sound.megaUpgrade();
    this.shake(15);
    this.damageFlash = 0.4;
    this.showMessage('⚡ МЕГА-ПРОКАЧКА ⚡',
      'Улучшения на ур. 100 · Скорость — кап 3 ур.', 3.5);
  }

  cheatToggleGod(){
    this.godMode = !this.godMode;
    if(this.godMode) sound.cheatOn(); else sound.cheatOff();
    this.showMessage('ЧИТ: GODMODE', this.godMode ? 'Бессмертие ВКЛ' : 'Бессмертие ВЫКЛ', 2.0);
  }
  cheatToggleInfiniteAmmo(){
    this.infiniteAmmo = !this.infiniteAmmo;
    if(this.infiniteAmmo && this.player) this.player.mag = this.player.maxMag;
    if(this.infiniteAmmo) sound.cheatOn(); else sound.cheatOff();
    this.showMessage('ЧИТ: INFINITE AMMO', this.infiniteAmmo ? 'Патроны бесконечны' : 'Обычный режим', 2.0);
  }
  cheatToggleDamage(){
    if(this.cheatDamageMult === 1){ this.cheatDamageMult = 10; sound.cheatOn();
      this.showMessage('ЧИТ: SUPER DAMAGE', 'Урон ×10', 2.0);
    } else { this.cheatDamageMult = 1; sound.cheatOff();
      this.showMessage('ЧИТ: SUPER DAMAGE', 'Урон обычный', 2.0);
    }
  }
  cheatToggleSpeed(){
    if(this.cheatSpeedMult === 1){ this.cheatSpeedMult = 3; sound.cheatOn();
      this.showMessage('ЧИТ: SUPER SPEED', 'Скорость ×3', 2.0);
    } else { this.cheatSpeedMult = 1; sound.cheatOff();
      this.showMessage('ЧИТ: SUPER SPEED', 'Скорость обычная', 2.0);
    }
  }
  cheatKillAll(){
    let count = 0;
    for(const b of this.bots){
      if(b.alive){ b.alive = false; b.dead = true;
        this.spawnExplosion(b.x, b.y, b.isSapper ? '#ffcc00' : b.cfg.color);
        count++;
      }
    }
    if(this.boss && this.boss.alive){ this.killEntity(this.boss, this.player); count++; }
    this.shake(15);
    this.showMessage('ЧИТ: KILL ALL', 'Уничтожено: ' + count, 2.0);
  }
  cheatAddScore(n){ this.score += n; sound.cheatOn(); this.showMessage('ЧИТ: COINS', '+' + n + ' очков', 2.0); }
  cheatSpawnBoss(hasPlayer){
    if(!hasPlayer){ this.pending.spawnBoss = true;
      this.showSettingsMessage('Босс появится при старте игры', '#ffcc00'); return; }
    if(this.boss && this.boss.alive){ this.boss.alive = false; this.boss.dead = true;
      this.spawnExplosion(this.boss.x, this.boss.y, '#ff3b6b'); }
    const p = this.findBossSpawnPoint();
    this.boss = new BossSnake(p.x, p.y, this);
    this.bossDefeatedTimer = 0;
    if(this.toSpawn === 0 && this.bots.length === 0 && !this.isBossRoom()) this.roomActive = true;
    this.rings.push(new Ring(p.x, p.y, '#ff3b6b', 180, 1.2));
    this.spawnSparks(p.x, p.y, '#ff3b6b', 40);
    sound.bossAppear();
    this.shake(20);
    this.showMessage('ЧИТ: SPAWN BOSS', 'Кубическая змея появилась!', 2.5);
  }
  cheatHeal(hasPlayer){
    if(!hasPlayer){ this.pending.heal = true;
      this.showSettingsMessage('HP восстановится при старте', '#ffcc00'); return; }
    this.player.hp = this.player.maxHp;
    sound.cheatOn();
    this.showMessage('ЧИТ: HEAL', 'HP полностью восстановлено', 1.8);
  }
  cheatResetCooldowns(hasPlayer){
    if(!hasPlayer){ this.pending.resetCD = true;
      this.showSettingsMessage('Кулдауны сбросятся при старте', '#ffcc00'); return; }
    this.player.dashCooldownTimer = 0;
    this.player.rageCooldownTimer = 0;
    this.player.dashTimer = 0;
    sound.cheatOn();
    this.showMessage('ЧИТ: RESET CD', 'Кулдауны сброшены', 1.8);
  }
  cheatNuke(){
    for(const b of this.bots){
      if(b.alive){ b.alive = false; b.dead = true; this.spawnExplosion(b.x, b.y, '#ff5577'); }
    }
    if(this.boss && this.boss.alive) this.damageEntity(this.boss, 1500, this.player);
    const px = this.player ? this.player.x : CONFIG.WORLD_W/2;
    const py = this.player ? this.player.y : CONFIG.WORLD_H/2;
    for(let i = 0; i < 20; i++){
      const a = rand(0, TAU), d = rand(100, 700);
      this.spawnExplosion(px + Math.cos(a) * d, py + Math.sin(a) * d, '#ff5577');
    }
    this.shake(22); this.damageFlash = 1;
    this.showMessage('ЧИТ: NUKE', 'Всё снесено!', 2.0);
  }
  cheatLongRage(hasPlayer){
    if(!hasPlayer){ this.pending.longRage = true;
      this.showSettingsMessage('Ярость 60 сек включится при старте', '#ffcc00'); return; }
    this.player.rageActive = true;
    this.player.rageTimer = 60.0;
    this.player.rageCooldownTimer = 0;
    sound.rage();
    this.showMessage('ЧИТ: LONG RAGE', 'Ярость на 60 сек!', 2.5);
  }
  applyPendingCheats(){
    if(!this.player) return;
    if(this.pending.droneCount > 0){
      const canAdd = Math.max(0, 200 - this.drones.length);
      const toAdd = Math.min(this.pending.droneCount, canAdd);
      for(let i = 0; i < toAdd; i++) this.drones.push(new Drone(this.player, this.drones.length));
      if(toAdd > 0){
        this.rings.push(new Ring(this.player.x, this.player.y, '#7df9ff', 140, 0.8));
        this.showMessage('ЧИТ: DRONES', '+' + toAdd + ' дронов', 2.0);
      }
      this.pending.droneCount = 0;
    }
    if(this.pending.maxUpgrades){
      this.cheatMaxUpgrades(true);
      this.pending.maxUpgrades = false;
    }
    if(this.pending.heal){ this.player.hp = this.player.maxHp; this.pending.heal = false; }
    if(this.pending.resetCD){
      this.player.dashCooldownTimer = 0;
      this.player.rageCooldownTimer = 0;
      this.pending.resetCD = false;
    }
    if(this.pending.longRage){
      this.player.rageActive = true;
      this.player.rageTimer = 60.0;
      this.player.rageCooldownTimer = 0;
      this.pending.longRage = false;
    }
    if(this.pending.spawnBoss){ this.cheatSpawnBoss(true); this.pending.spawnBoss = false; }
  }
  resetCheats(){
    this.godMode = false;
    this.infiniteAmmo = false;
    this.cheatDamageMult = 1;
    this.cheatSpeedMult = 1;
    this.cheatHistory = [];
    this.pending = { droneCount:0, heal:false, resetCD:false, longRage:false, spawnBoss:false, maxUpgrades:false };
    this.showSettingsMessage('Все читы сброшены', '#ffcc00');
    sound.cheatOff();
  }
  /* =================== КОНЕЦ ЧИТ-СИСТЕМЫ =================== */

  positionCheatInput(){
    if(!this.inputEl) return;
    if(this.state !== 'settings' || this.showCheatList){
      if(this.inputEl.style.display !== 'none') this.inputEl.style.display = 'none';
      return;
    }
    const r = this.canvas.getBoundingClientRect();
    const scale = r.width / CONFIG.W;
    const rect = this.settingsCheatInputRect;
    this.inputEl.style.display = 'block';
    this.inputEl.style.left = (r.left + rect.x * scale) + 'px';
    this.inputEl.style.top  = (r.top  + rect.y * scale) + 'px';
    this.inputEl.style.width = (rect.w * scale) + 'px';
    this.inputEl.style.height = (rect.h * scale) + 'px';
    this.inputEl.style.fontSize = Math.max(14, Math.round(20 * scale)) + 'px';
    this.inputEl.style.padding = Math.round(8*scale) + 'px ' + Math.round(14*scale) + 'px';
  }

  returnToMenu(){
    sound.click();
    this.saveProgressOnGameOver();
    this.state = 'menu';
    this.player = null;
    this.boss = null;
    this.bossDefeatedTimer = 0;
    this.walls = []; this.bullets = []; this.particles = []; this.rings = [];
    this.bots = []; this.drones = [];
    this.message = {text:'', sub:'', t:0, max:1};
    this.moveStick.end(); this.aimStick.end();
    this.mouse.down = false;
    this.keys = {};
    this.godMode = false;
    this.infiniteAmmo = false;
    this.cheatDamageMult = 1;
    this.cheatSpeedMult = 1;
    this.cheatBuffer = '';
    this.cheatHistory = [];
    this.pending = { droneCount:0, heal:false, resetCD:false, longRage:false, spawnBoss:false, maxUpgrades:false };
    if(this.inputEl) { this.inputEl.value = ''; this.inputEl.blur(); }
    this.positionCheatInput();
    setGameplayActive(false);
  }

  saveProgressOnGameOver(){
    if(this.score > this.highScore) this.highScore = this.score;
    this.totalKills += this.kills;
    if(this.room > 0) this.totalRooms += Math.max(0, this.room - 1);
    this.saveLocalProgress();
    savePlayerProgress({
      highScore: this.highScore,
      totalKills: this.totalKills,
      totalRooms: this.totalRooms
    });
  }

  openSettings(returnState){
    sound.click();
    this.settingsReturnState = returnState || 'menu';
    this.state = 'settings';
    this.showCheatList = false;
    this.settingsMessage = '';
    this.settingsMessageUntil = 0;
    this.moveStick.end(); this.aimStick.end();
    this.positionCheatInput();
  }
  closeSettings(){
    sound.click();
    if(this.inputEl) { this.inputEl.value = ''; this.inputEl.blur(); }
    this.showCheatList = false;
    this.state = this.settingsReturnState || 'menu';
    this.positionCheatInput();
  }

  setupInput(){
    const c=this.canvas;

    window.addEventListener('contextmenu', e => e.preventDefault());
    document.addEventListener('selectstart', e => e.preventDefault());
    document.addEventListener('dragstart', e => e.preventDefault());
    document.addEventListener('touchstart', e => { if(e.target === c) e.preventDefault(); }, { passive: false });

    window.addEventListener('keydown', e=>{
      const k=e.key.toLowerCase();
      if(document.activeElement === this.inputEl){
        if(e.key === 'Enter'){ e.preventDefault(); this.tryActivateCheatFromInput(); }
        else if(e.key === 'Escape'){ e.preventDefault(); this.inputEl.blur(); }
        return;
      }
      this.keys[k]=true;
      if(k==='escape'){
        if(this.state==='playing'){ this.state='paused'; this.moveStick.end(); this.aimStick.end(); setGameplayActive(false); }
        else if(this.state==='paused'){ this.state='playing'; setGameplayActive(true); }
        else if(this.state==='settings'){ this.closeSettings(); }
      }
      if(k==='f') this.toggleFullscreen();
      if(k==='m'){ const on = sound.toggle(); this.showMessage('', on ? 'Звук ВКЛ' : 'Звук ВЫКЛ', 1.0); }
      if(this.state==='playing' && this.player){
        if(k==='r' || k==='к') this.player.startReload();
        if(k===' ' && !e.repeat) this.player.startDash(this);
        if(k==='q' || k==='й'){ if(!e.repeat) this.player.startRage(this); }
      }
      if(this.state==='menu'){
        if(k==='1'){ this.selectedDifficulty='easy'; sound.click(); }
        if(k==='2'){ this.selectedDifficulty='medium'; sound.click(); }
        if(k==='3'){ this.selectedDifficulty='hard'; sound.click(); }
        if(k==='4'){ this.selectedDifficulty='hardcore'; sound.click(); }
      }
      if((k==='enter'||k===' ') && this.state==='menu'){ e.preventDefault(); this.startGame(); }
      if((k==='enter'||k===' ') && this.state==='gameover'){ e.preventDefault(); this.startGame(); }
      if(k==='enter' && this.state==='upgrade'){ e.preventDefault(); this.finishUpgradeAndContinue(); }
      if(['arrowup','arrowdown','arrowleft','arrowright',' '].includes(k)) e.preventDefault();
      if(this.state === 'playing' || this.state === 'paused'){
        if(!e.repeat && k.length === 1) this.handleCheatInput(k);
      }
    });
    window.addEventListener('keyup', e=>{ this.keys[e.key.toLowerCase()]=false; });

    c.addEventListener('mousemove', e=>{
      const m=this.toCanvasCoords(e);
      this.mouse.x=m.x; this.mouse.y=m.y;
      if(this.state!=='paused') this.aimMode='mouse';
    });
    c.addEventListener('mousedown', e=>{
      const m=this.toCanvasCoords(e);
      this.mouse.x=m.x; this.mouse.y=m.y;
      sound.init(); sound.resume();
      if(document.activeElement === this.inputEl) this.inputEl.blur();

      if(this.state==='menu'){
        for(const btn of this.difficultyBtns){
          if(this.inRect(m,btn)){ this.selectedDifficulty = btn.key; sound.click(); return; }
        }
        if(this.inRect(m,this.startBtn)){ this.startGame(); return; }
        if(this.inRect(m,this.fullscreenBtn)){ sound.click(); this.toggleFullscreen(); return; }
        if(this.inRect(m,this.soundBtn)){ const on=sound.toggle(); this.showMessage('', on?'Звук ВКЛ':'Звук ВЫКЛ', 1.0); return; }
        if(this.inRect(m,this.settingsBtn)){ this.openSettings('menu'); return; }
        return;
      }
      if(this.state==='settings'){
        if(this.showCheatList){
          if(this.inRect(m,this.settingsBackBtn)){ sound.click(); this.showCheatList = false; this.positionCheatInput(); return; }
          return;
        }
        if(this.inRect(m,this.settingsBackBtn)){ this.closeSettings(); return; }
        if(this.inRect(m,this.settingsSoundBtn)){ sound.init(); sound.resume(); const on = sound.toggle(); this.showSettingsMessage(on?'Звук ВКЛ':'Звук ВЫКЛ', '#7df9ff'); return; }
        if(this.inRect(m,this.settingsVolumeDownBtn)){ sound.setVolume(sound.volume - 0.1); this.showSettingsMessage('Громкость: ' + Math.round(sound.volume*100) + '%', '#7df9ff'); return; }
        if(this.inRect(m,this.settingsVolumeUpBtn)){ sound.setVolume(sound.volume + 0.1); this.showSettingsMessage('Громкость: ' + Math.round(sound.volume*100) + '%', '#7df9ff'); return; }
        if(this.inRect(m,this.settingsCheatActivateBtn)){ this.tryActivateCheatFromInput(); return; }
        if(this.inRect(m,this.settingsShowCodesBtn)){ sound.click(); this.showCheatList = true; this.positionCheatInput(); return; }
        if(this.inRect(m,this.settingsResetCheatsBtn)){ this.resetCheats(); return; }
        return;
      }
      if(this.state==='gameover'){
        if(this.inRect(m,this.restartBtn)){ this.startGame(); return; }
        return;
      }
      if(this.state==='paused'){
        if(this.inRect(m,this.resumeBtn)){ sound.click(); this.state='playing'; setGameplayActive(true); return; }
        if(this.inRect(m,this.pauseSoundBtn)){
          const on = sound.toggle();
          this.showMessage('', on ? 'Звук ВКЛ' : 'Звук ВЫКЛ', 1.0);
          return;
        }
        if(this.inRect(m,this.pauseSettingsBtn)){ this.openSettings('paused'); return; }
        if(this.inRect(m,this.pauseMenuBtn)){ this.returnToMenu(); return; }
        return;
      }
      if(this.state==='upgrade'){ this.handleUpgradeClick(m); return; }
      if(this.state==='playing' && this.player){
        if(this.inRect(m,this.reloadBtn)){ this.player.startReload(); return; }
        if(this.inRect(m,this.dashBtn)){ this.player.startDash(this); return; }
        if(this.inRect(m,this.rageBtn)){ this.player.startRage(this); return; }
        if(this.inRect(m,this.pauseBtn)){ this.state='paused'; this.moveStick.end(); this.aimStick.end(); setGameplayActive(false); sound.click(); return; }
      }
      if(e.button===0) this.mouse.down=true;
    });
    window.addEventListener('mouseup', e=>{
      if(e.button===0) this.mouse.down=false;
    });
    window.addEventListener('blur', ()=>{
      this.keys={}; this.mouse.down=false;
      this.moveStick.end(); this.aimStick.end();
      if(this.state === 'playing'){
        this.state = 'paused';
        sound.muteForPause();
        setGameplayActive(false);
      }
    });
    window.addEventListener('focus', ()=>{
      sound.unmuteAfterPause();
    });

    c.addEventListener('touchstart', e=>{
      e.preventDefault();
      this.aimMode='touch';
      this.mouse.down=false;
      sound.init(); sound.resume();
      for(const t of e.changedTouches){
        const p=this.toCanvasCoords(t);
        if(this.state==='menu'){
          for(const btn of this.difficultyBtns){
            if(this.inRect(p,btn)){ this.selectedDifficulty = btn.key; sound.click(); return; }
          }
          if(this.inRect(p,this.startBtn)){ this.startGame(); return; }
          if(this.inRect(p,this.fullscreenBtn)){ sound.click(); this.toggleFullscreen(); return; }
          if(this.inRect(p,this.soundBtn)){ const on=sound.toggle(); this.showMessage('', on?'Звук ВКЛ':'Звук ВЫКЛ', 1.0); return; }
          if(this.inRect(p,this.settingsBtn)){ this.openSettings('menu'); return; }
          continue;
        }
        if(this.state==='settings'){
          if(this.showCheatList){
            if(this.inRect(p,this.settingsBackBtn)){ sound.click(); this.showCheatList = false; this.positionCheatInput(); return; }
            continue;
          }
          if(this.inRect(p, this.settingsCheatInputRect)){ this.inputEl.focus(); continue; }
          if(this.inRect(p,this.settingsBackBtn)){ this.closeSettings(); return; }
          if(this.inRect(p,this.settingsSoundBtn)){ const on = sound.toggle(); this.showSettingsMessage(on?'Звук ВКЛ':'Звук ВЫКЛ', '#7df9ff'); continue; }
          if(this.inRect(p,this.settingsVolumeDownBtn)){ sound.setVolume(sound.volume - 0.1); this.showSettingsMessage('Громкость: ' + Math.round(sound.volume*100) + '%', '#7df9ff'); continue; }
          if(this.inRect(p,this.settingsVolumeUpBtn)){ sound.setVolume(sound.volume + 0.1); this.showSettingsMessage('Громкость: ' + Math.round(sound.volume*100) + '%', '#7df9ff'); continue; }
          if(this.inRect(p,this.settingsCheatActivateBtn)){ this.tryActivateCheatFromInput(); continue; }
          if(this.inRect(p,this.settingsShowCodesBtn)){ sound.click(); this.showCheatList = true; this.positionCheatInput(); continue; }
          if(this.inRect(p,this.settingsResetCheatsBtn)){ this.resetCheats(); continue; }
          continue;
        }
        if(this.state==='gameover'){
          if(this.inRect(p,this.restartBtn)){ this.startGame(); return; }
          continue;
        }
        if(this.state==='paused'){
          if(this.inRect(p,this.resumeBtn)){ sound.click(); this.state='playing'; setGameplayActive(true); return; }
          if(this.inRect(p,this.pauseSoundBtn)){
            const on = sound.toggle();
            this.showMessage('', on ? 'Звук ВКЛ' : 'Звук ВЫКЛ', 1.0);
            continue;
          }
          if(this.inRect(p,this.pauseSettingsBtn)){ this.openSettings('paused'); return; }
          if(this.inRect(p,this.pauseMenuBtn)){ this.returnToMenu(); return; }
          continue;
        }
        if(this.state==='upgrade'){ this.handleUpgradeClick(p); continue; }
        if(this.state!=='playing') continue;
        if(this.inRect(p,this.reloadBtn)){ if(this.player) this.player.startReload(); continue; }
        if(this.inRect(p,this.dashBtn)){ if(this.player) this.player.startDash(this); continue; }
        if(this.inRect(p,this.rageBtn)){ if(this.player) this.player.startRage(this); continue; }
        if(this.inRect(p,this.pauseBtn)){
          this.state='paused';
          this.moveStick.end(); this.aimStick.end();
          setGameplayActive(false);
          sound.click();
          continue;
        }
        if(p.x<CONFIG.W/2){
          if(!this.moveStick.active) this.moveStick.start(t.identifier,p.x,p.y);
        } else {
          if(!this.aimStick.active) this.aimStick.start(t.identifier,p.x,p.y);
        }
      }
    }, {passive:false});

    c.addEventListener('touchmove', e=>{
      e.preventDefault();
      for(const t of e.changedTouches){
        const p=this.toCanvasCoords(t);
        if(this.moveStick.active && this.moveStick.touchId===t.identifier)
          this.moveStick.move(p.x,p.y);
        if(this.aimStick.active && this.aimStick.touchId===t.identifier)
          this.aimStick.move(p.x,p.y);
      }
    }, {passive:false});

    const endTouch=e=>{
      e.preventDefault();
      for(const t of e.changedTouches){
        if(this.moveStick.active && this.moveStick.touchId===t.identifier) this.moveStick.end();
        if(this.aimStick.active && this.aimStick.touchId===t.identifier) this.aimStick.end();
      }
    };
    c.addEventListener('touchend', endTouch, {passive:false});
    c.addEventListener('touchcancel', endTouch, {passive:false});
  }

  toCanvasCoords(e){
    const r=this.canvas.getBoundingClientRect();
    const sx = r.width  > 0 ? CONFIG.W / r.width  : 1;
    const sy = r.height > 0 ? CONFIG.H / r.height : 1;
    return { x: (e.clientX - r.left) * sx, y: (e.clientY - r.top ) * sy };
  }
  inRect(p,r){
    return p.x>=r.x && p.x<=r.x+r.w && p.y>=r.y && p.y<=r.y+r.h;
  }
  worldMouse(){ return { x:this.mouse.x+this.cam.x, y:this.mouse.y+this.cam.y }; }
  toggleFullscreen(){
    const doc=document, el=doc.documentElement;
    if(!doc.fullscreenElement){
      if(el.requestFullscreen) el.requestFullscreen().catch(()=>{});
      else if(el.webkitRequestFullscreen) el.webkitRequestFullscreen();
    } else {
      if(doc.exitFullscreen) doc.exitFullscreen();
      else if(doc.webkitExitFullscreen) doc.webkitExitFullscreen();
    }
  }

  startGame(){
    sound.init(); sound.resume();
    sound.startGame();
    this.reset();
    this.state='playing';
    this.mouse.down=false;
    this.moveStick.end(); this.aimStick.end();
    this.applyPendingCheats();
    setGameplayActive(true);
    this._adCooldown = 90;
  }

  reset(){
    this.walls=[]; this.bullets=[]; this.particles=[];
    this.rings=[]; this.bots=[]; this.drones=[];
    this.boss = null; this.bossDefeatedTimer = 0;
    this.difficulty = DIFFICULTIES[this.selectedDifficulty];
    this.respawnsLeft = this.difficulty.respawns;
    this.deathTimer = 0;
    this.room=0; this.score=0; this.kills=0;
    this.toSpawn=0; this.spawnTimer=0;
    this.roomActive=false; this.elapsed=0;
    this.shakeAmount=0; this.damageFlash=0;
    this.message={text:'',sub:'',t:0,max:1};
    this.upgradeCounts={};
    this.droneDamageMult=1;
    this.droneFireRateMult=1;
    this._lowHpWarnTimer = 0;
    this.cheatBuffer = '';
    this.cheatBufferTime = 0;
    this.cheatHistory = [];
    this.generateMap();
    this.player=new Player(CONFIG.WORLD_W/2, CONFIG.WORLD_H/2);
    this.player.maxHp = Math.round(this.player.maxHp * this.difficulty.playerHpMult);
    this.player.hp = this.player.maxHp;
    this.recenterCamera();
    this.startRoom();
  }
  recenterCamera(){
    if(!this.player) return;
    const maxX=Math.max(0,CONFIG.WORLD_W-CONFIG.W);
    const maxY=Math.max(0,CONFIG.WORLD_H-CONFIG.H);
    this.cam.x=clamp(this.player.x-CONFIG.W/2, 0, maxX);
    this.cam.y=clamp(this.player.y-CONFIG.H/2, 0, maxY);
  }
  generateMap(){
    const t=50;
    const WW=CONFIG.WORLD_W, WH=CONFIG.WORLD_H;
    this.walls.push(new Wall(-t,-t,WW+t*2,t));
    this.walls.push(new Wall(-t,WH,WW+t*2,t));
    this.walls.push(new Wall(-t,-t,t,WH+t*2));
    this.walls.push(new Wall(WW,-t,t,WH+t*2));
    const spawnZone={x:WW/2-200,y:WH/2-200,w:400,h:400};
    let attempts=0, placed=0;
    while(placed<30 && attempts<900){
      attempts++;
      const w=rand(70,230), h=rand(70,230);
      const x=rand(90,WW-90-w), y=rand(90,WH-90-h);
      const rect={x,y,w,h};
      if(rectsOverlap(rect,spawnZone,90)) continue;
      let ok=true;
      for(const wl of this.walls){
        if(rectsOverlap(rect,wl,72)){ ok=false; break; }
      }
      if(!ok) continue;
      this.walls.push(new Wall(x,y,w,h));
      placed++;
    }
  }
  randomFreePoint(){
    for(let i=0;i<250;i++){
      const x=rand(90,CONFIG.WORLD_W-90);
      const y=rand(90,CONFIG.WORLD_H-90);
      if(this.pointFree(x,y,42)) return {x,y};
    }
    return {x:CONFIG.WORLD_W/2, y:CONFIG.WORLD_H/2};
  }
  pointFree(x,y,r){
    for(const w of this.walls){
      if(x>w.x-r && x<w.x+w.w+r && y>w.y-r && y<w.y+w.h+r) return false;
    }
    return true;
  }
  findSpawnPoint(){
    for(let i=0;i<300;i++){
      const x=rand(110,CONFIG.WORLD_W-110);
      const y=rand(110,CONFIG.WORLD_H-110);
      if(!this.pointFree(x,y,50)) continue;
      if(this.player && dist2(x,y,this.player.x,this.player.y)<620) continue;
      return {x,y};
    }
    return this.randomFreePoint();
  }
  findBossSpawnPoint(){
    for(let i=0;i<300;i++){
      const x=rand(200,CONFIG.WORLD_W-200);
      const y=rand(200,CONFIG.WORLD_H-200);
      if(!this.pointFree(x,y,80)) continue;
      if(this.player && dist2(x,y,this.player.x,this.player.y)<800) continue;
      return {x,y};
    }
    return {x: 300, y: 300};
  }
  getRoomBotCount(){
    return this.difficulty.botCount + (this.room - 1) * this.difficulty.botGrowth;
  }
  isBossRoom(room = this.room){
    return room % CONFIG.BOSS_EVERY === 0;
  }

  startRoom(){
    this.room++;
    this.roomActive = true;
    this.boss = null;
    this.bossDefeatedTimer = 0;
    if(this.isBossRoom()){
      this.toSpawn = 0;
      this.spawnTimer = 999;
      this.spawnBoss();
      sound.bossAppear();
      this.shake(20);
      this.showMessage('КОМНАТА ' + this.room, '⚠ БОСС: КУБИЧЕСКАЯ ЗМЕЯ ⚠', 3.0);
      return;
    }
    this.toSpawn = this.getRoomBotCount();
    this.spawnTimer = 0;
    sound.roomStart();
    const hpMul  = (1 + (this.room-1)*CONFIG.ROOM_HP_SCALE)  * this.difficulty.botHpMult;
    const dmgMul = (1 + (this.room-1)*CONFIG.ROOM_DMG_SCALE) * this.difficulty.botDmgMult;
    if(this.room === 1){
      this.showMessage('КОМНАТА 1', this.difficulty.name + ' · ' + this.toSpawn + ' врагов', 2.5);
    } else {
      const nextBoss = CONFIG.BOSS_EVERY - (this.room % CONFIG.BOSS_EVERY);
      const suffix = nextBoss <= 2 ? ' · босс через ' + nextBoss : '';
      this.showMessage('КОМНАТА ' + this.room,
        this.toSpawn + ' врагов · HP ×' + hpMul.toFixed(2) + ' · Урон ×' + dmgMul.toFixed(2) + suffix, 2.5);
    }
  }
  spawnBoss(){
    const p = this.findBossSpawnPoint();
    this.boss = new BossSnake(p.x, p.y, this);
    this.rings.push(new Ring(p.x, p.y, '#ff3b6b', 180, 1.2));
    this.rings.push(new Ring(p.x, p.y, '#b06bff', 120, 0.9));
    this.spawnSparks(p.x, p.y, '#ff3b6b', 40);
    this.spawnSparks(p.x, p.y, '#b06bff', 30);
  }
  updateRooms(dt){
    if(!this.roomActive) return;
    if(this.bossDefeatedTimer > 0){
      this.bossDefeatedTimer -= dt;
      if(this.bossDefeatedTimer <= 0){
        this.boss = null;
        const heal = Math.round(this.player.maxHp * CONFIG.HEAL_ON_BREAK);
        this.player.hp = Math.min(this.player.maxHp, this.player.hp + heal);
        const bossRoomNum = Math.floor(this.room / CONFIG.BOSS_EVERY);
        const bonus = 1500 * Math.max(1, bossRoomNum);
        this.score += bonus;
        this.showMessage('БОСС ПОВЕРЖЕН', '+' + bonus + ' очков · +' + heal + ' HP', 3.5);
        this.enterUpgradeState();
      }
      return;
    }
    if(this.isBossRoom()){
      if(this.boss && !this.boss.alive) this.bossDefeatedTimer = 2.2;
      return;
    }
    const maxAlive = Math.max(20, Math.floor(this.getRoomBotCount() * 0.6));
    if(this.toSpawn > 0){
      this.spawnTimer -= dt;
      if(this.spawnTimer <= 0 && this.bots.length < maxAlive){
        this.spawnBot();
        this.toSpawn--;
        this.spawnTimer = rand(0.20, 0.55);
      }
    }
    if(this.toSpawn === 0 && this.bots.length === 0){
      if(this.boss && this.boss.alive) return;
      this.roomActive = false;
      const heal = Math.round(this.player.maxHp * CONFIG.HEAL_ON_BREAK);
      this.player.hp = Math.min(this.player.maxHp, this.player.hp + heal);
      const bonus = 100 * this.room;
      this.score += bonus;
      sound.roomClear();
      this.enterUpgradeState();
    }
  }
  pickBotType(){
    const r = this.room;
    const rnd = Math.random();
    if(r <= 2) {
      if(r === 1) return 'normal';
      return rnd < 0.75 ? 'normal' : 'heavy';
    }
    if(r <= 4){
      if(rnd < 0.5)  return 'normal';
      if(rnd < 0.75) return 'heavy';
      if(rnd < 0.9)  return 'sniper';
      return 'sapper';
    }
    if(rnd < 0.35) return 'normal';
    if(rnd < 0.55) return 'heavy';
    if(rnd < 0.75) return 'sniper';
    return 'sapper';
  }
  spawnBot(){
    const p=this.findSpawnPoint();
    const type=this.pickBotType();
    this.bots.push(new Bot(p.x,p.y,type,this));
    this.rings.push(new Ring(p.x,p.y,CONFIG.BOT_TYPES[type].color,55,0.5));
    this.spawnSparks(p.x,p.y,CONFIG.BOT_TYPES[type].color,12);
  }
  restartRoom(){
    this.bots = []; this.bullets = []; this.particles = []; this.rings = [];
    this.boss = null; this.bossDefeatedTimer = 0;
    if(this.isBossRoom()){
      this.toSpawn = 0; this.spawnTimer = 999; this.spawnBoss();
    } else {
      this.toSpawn = this.getRoomBotCount(); this.spawnTimer = 0;
    }
    this.roomActive = true;
    for(const d of this.drones){
      d.x = this.player.x + Math.cos(d.angle) * d.orbitR;
      d.y = this.player.y + Math.sin(d.angle) * d.orbitR;
    }
  }
  respawnPlayer(){
    const p = this.player;
    p.alive = true;
    p.hp = p.maxHp;
    p.x = CONFIG.WORLD_W/2; p.y = CONFIG.WORLD_H/2;
    p.vx = 0; p.vy = 0;
    p.mag = p.maxMag;
    p.reloading = false; p.reloadTimer = 0;
    p.spread = 0; p.hitFlash = 0;
    p.dashTimer = 0; p.dashCooldownTimer = 0;
    p.rageActive = false; p.rageTimer = 0; p.rageCooldownTimer = 0;
    p.invulnerable = false;
    this.restartRoom();
    this.recenterCamera();
    this.state = 'playing';
    sound.respawn();
    const msg = this.difficulty.respawns >= 99
      ? 'Комната ' + this.room + ' перезапущена'
      : 'Комната ' + this.room + ' перезапущена · Возрождений: ' + this.respawnsLeft;
    this.showMessage('ВОЗРОЖДЕНИЕ', msg, 2.5);
  }
  enterUpgradeState(){
    this.state='upgrade';
    this.moveStick.end(); this.aimStick.end();
    this.mouse.down=false;
    this.layoutUpgradeCards();
    setGameplayActive(false);
    this._adCooldown -= 1;
    if(this._adCooldown <= 0 && this.room > 2 && this.room % 3 === 0){
      this._adCooldown = 3;
      showFullscreenAd(() => {});
    }
  }
  handleUpgradeClick(p){
    if(this.inRect(p,this.upgradeContinueBtn)){ sound.click(); this.finishUpgradeAndContinue(); return; }
    for(const card of this.upgradeCards){
      if(this.inRect(p,card)){ this.buyUpgrade(card.upgrade); return; }
    }
  }
  buyUpgrade(up){
    const cost=this.getUpgradeCost(up);
    if(this.score<cost){ sound.deny(); this.showMessage('','Недостаточно очков',0.9); return false; }
    this.score-=cost;
    this.upgradeCounts[up.id]=(this.upgradeCounts[up.id]||0)+1;
    up.apply(this);
    this.spawnSparks(this.player.x,this.player.y,'#7df9ff',18);
    this.rings.push(new Ring(this.player.x,this.player.y,'#7df9ff',60,0.5));
    sound.upgrade();
    this.showMessage('',up.name+' улучшено',0.9);
    return true;
  }
  finishUpgradeAndContinue(){ this.state='playing'; setGameplayActive(true); this.startRoom(); }
  addDrone(){
    if(!this.player) return;
    const d=new Drone(this.player,this.drones.length);
    this.drones.push(d);
    this.rings.push(new Ring(d.x,d.y,'#7df9ff',40,0.5));
    this.spawnSparks(d.x,d.y,'#7df9ff',12);
  }
  shake(v){ this.shakeAmount=Math.min(this.shakeAmount+v,22); }
  showMessage(text,sub,t){
    if(!text && this.message.t>t) return;
    this.message={text, sub:sub||'', t, max:t};
  }
  spawnMuzzle(x,y,ang,color){
    for(let i=0;i<4;i++){
      const a=ang+rand(-0.4,0.4);
      const s=rand(90,300);
      this.particles.push(new Particle(x,y,Math.cos(a)*s,Math.sin(a)*s, rand(0.07,0.17),color,rand(1.5,3)));
    }
  }
  spawnSparks(x,y,color,n){
    for(let i=0;i<n;i++){
      const a=rand(0,TAU), s=rand(60,320);
      this.particles.push(new Particle(x,y,Math.cos(a)*s,Math.sin(a)*s, rand(0.12,0.4),color,rand(1,2.6)));
    }
  }
  spawnBlood(x,y,color,n){
    for(let i=0;i<n;i++){
      const a=rand(0,TAU), s=rand(40,260);
      this.particles.push(new Particle(x,y,Math.cos(a)*s,Math.sin(a)*s, rand(0.25,0.6),color,rand(1.5,3.5),'blood'));
    }
  }
  spawnExplosion(x,y,color){
    for(let i=0;i<28;i++){
      const a=rand(0,TAU), s=rand(80,540);
      this.particles.push(new Particle(x,y,Math.cos(a)*s,Math.sin(a)*s, rand(0.3,0.85),color,rand(1.5,4.2)));
    }
    this.rings.push(new Ring(x,y,color,85,0.45));
  }
  moveEntity(ent,dx,dy){
    ent.x+=dx;
    for(const w of this.walls){
      const c=w.circleCollide(ent.x,ent.y,ent.r);
      if(c){ ent.x+=c.nx*c.depth; ent.y+=c.ny*c.depth; }
    }
    ent.y+=dy;
    for(const w of this.walls){
      const c=w.circleCollide(ent.x,ent.y,ent.r);
      if(c){ ent.x+=c.nx*c.depth; ent.y+=c.ny*c.depth; }
    }
    ent.x=clamp(ent.x,ent.r,CONFIG.WORLD_W-ent.r);
    ent.y=clamp(ent.y,ent.r,CONFIG.WORLD_H-ent.r);
  }
  damageEntity(ent,dmg,source){
    if(!ent.alive) return;
    if(ent.isPlayer && ent.invulnerable) return;
    if(ent.isPlayer && this.godMode) return;
    ent.hp-=dmg;
    ent.hitFlash=1;
    let col;
    if(ent.isPlayer) col = '#ff3355';
    else if(ent.isBoss) col = '#ff3b6b';
    else col = ent.cfg.color;
    this.spawnBlood(ent.x, ent.y, col, ent.isBoss ? 10 : 6);
    if(ent.isPlayer){ this.shake(7); this.damageFlash=1; sound.playerHit(); }
    else {
      const sv = sound.spatialVolume(ent.x, ent.y, this.cam.x, this.cam.y, 1000);
      if(sv > 0.05) sound.hit(sv);
    }
    if(ent.hp<=0){ ent.hp=0; this.killEntity(ent,source); }
  }
  killEntity(ent,source){
    if(ent.isPlayer){
      if(this.godMode){ ent.hp = ent.maxHp; return; }
      ent.alive=false;
      this.spawnExplosion(ent.x,ent.y,'#22e6ff');
      this.shake(18);
      sound.bigExplosion();
      this.state = 'dying';
      this.deathTimer = 1.6;
      return;
    }
    if(ent.isBoss){
      ent.alive=false; ent.dead=true;
      const parts = [{x: ent.x, y: ent.y}].concat(ent.segments);
      for(const p of parts) this.spawnExplosion(p.x, p.y, '#ff3b6b');
      this.shake(22);
      sound.bossDefeated();
      if(source===this.player){ this.kills++; this.score += 2000; }
      return;
    }
    ent.alive=false; ent.dead=true;
    this.spawnExplosion(ent.x,ent.y, ent.isSapper ? '#ffcc00' : ent.cfg.color);
    this.shake(ent.isSapper ? 6 : 4);
    const sv = sound.spatialVolume(ent.x, ent.y, this.cam.x, this.cam.y, 1000);
    if(sv > 0.05) sound.explosion(sv);
    if(source===this.player){ this.kills++; this.score+=ent.cfg.score; }
  }
  update(dt){
    if(this.state === 'dying'){
      this.deathTimer -= dt;
      this.shakeAmount *= Math.pow(0.0015,dt);
      if(this.shakeAmount<0.05) this.shakeAmount=0;
      this.damageFlash = Math.max(0, this.damageFlash - dt*2.2);
      for(const p of this.particles) p.update(dt);
      for(const r of this.rings) r.update(dt);
      for(const b of this.bullets) b.update(dt,this);
      this.particles = this.particles.filter(p=>!p.dead);
      this.rings = this.rings.filter(r=>!r.dead);
      this.bullets = this.bullets.filter(b=>!b.dead);
      if(this.message.t>0) this.message.t -= dt;
      if(this.deathTimer <= 0){
        if(this.respawnsLeft > 0){
          this.respawnsLeft--;
          this.respawnPlayer();
        } else {
          this.state = 'gameover';
          this.mouse.down = false;
          this.moveStick.end(); this.aimStick.end();
          sound.gameOver();
          setGameplayActive(false);
          this.saveProgressOnGameOver();
        }
      }
      return;
    }
    if(this.state!=='playing') return;
    this.elapsed+=dt;
    const maxCamX=Math.max(0,CONFIG.WORLD_W-CONFIG.W);
    const maxCamY=Math.max(0,CONFIG.WORLD_H-CONFIG.H);
    const tcx=clamp(this.player.x-CONFIG.W/2, 0, maxCamX);
    const tcy=clamp(this.player.y-CONFIG.H/2, 0, maxCamY);
    const k=1-Math.pow(0.0015,dt);
    this.cam.x=lerp(this.cam.x,tcx,k);
    this.cam.y=lerp(this.cam.y,tcy,k);
    this.shakeAmount*=Math.pow(0.0015,dt);
    if(this.shakeAmount<0.05) this.shakeAmount=0;
    this.damageFlash=Math.max(0,this.damageFlash-dt*2.2);
    if(this.player.alive && this.player.hp < this.player.maxHp*0.3 && !this.godMode){
      this._lowHpWarnTimer -= dt;
      if(this._lowHpWarnTimer <= 0){ sound.tickLowHp(); this._lowHpWarnTimer = 0.7; }
    }
    this.player.update(dt,this);
    for(const b of this.bots) b.update(dt,this);
    if(this.boss && this.boss.alive) this.boss.update(dt, this);
    for(const d of this.drones) d.update(dt,this);
    for(const b of this.bullets) b.update(dt,this);
    for(const p of this.particles) p.update(dt);
    for(const r of this.rings) r.update(dt);
    this.bullets=this.bullets.filter(b=>!b.dead);
    this.particles=this.particles.filter(p=>!p.dead);
    this.rings=this.rings.filter(r=>!r.dead);
    this.bots=this.bots.filter(b=>!b.dead);
    this.updateRooms(dt);
    if(this.message.t>0) this.message.t-=dt;
    const now = performance.now();
    this.cheatHistory = this.cheatHistory.filter(h => now - h.time < 4000);
  }

  render(){
    const ctx=this.ctx;
    ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
    ctx.clearRect(0,0,CONFIG.W,CONFIG.H);
    ctx.fillStyle='#07070d';
    ctx.fillRect(0,0,CONFIG.W,CONFIG.H);
    if(this.state === 'settings' && !this.showCheatList){
      if(this.inputEl.style.display !== 'block') this.positionCheatInput();
    } else {
      if(this.inputEl && this.inputEl.style.display !== 'none') this.inputEl.style.display = 'none';
    }
    if(this.state==='menu'){ this.drawBackgroundGrid(ctx); this.drawMenu(ctx); return; }
    if(this.state==='settings'){ this.drawBackgroundGrid(ctx); this.drawSettings(ctx); return; }
    this.shakeX=rand(-1,1)*this.shakeAmount;
    this.shakeY=rand(-1,1)*this.shakeAmount;
    ctx.save();
    ctx.translate(-this.cam.x+this.shakeX, -this.cam.y+this.shakeY);
    this.drawWorldGrid(ctx);
    for(const w of this.walls) w.draw(ctx);
    for(const r of this.rings) r.draw(ctx);
    for(const p of this.particles) p.draw(ctx);
    for(const b of this.bots) b.draw(ctx);
    if(this.boss && this.boss.alive) this.boss.draw(ctx);
    for(const d of this.drones) d.draw(ctx);
    if(this.player) this.player.draw(ctx);
    for(const b of this.bullets) b.draw(ctx);
    ctx.restore();
    if(this.damageFlash>0){
      const g=ctx.createRadialGradient(CONFIG.W/2,CONFIG.H/2,CONFIG.H*0.25,CONFIG.W/2,CONFIG.H/2,CONFIG.H*0.8);
      g.addColorStop(0,'rgba(255,0,40,0)');
      g.addColorStop(1,'rgba(255,0,40,'+(this.damageFlash*0.55)+')');
      ctx.fillStyle=g; ctx.fillRect(0,0,CONFIG.W,CONFIG.H);
    }
    if(this.state==='playing' && this.player && this.player.rageActive){
      ctx.save(); ctx.globalAlpha = 0.10 + Math.sin(performance.now()/150) * 0.03;
      ctx.fillStyle = '#ff2244'; ctx.fillRect(0,0,CONFIG.W,CONFIG.H);
      ctx.restore();
    }
    if(this.state!=='upgrade'){
      this.drawHUD(ctx);
      this.drawMinimap(ctx);
      this.drawTouchControls(ctx);
      this.drawBossBar(ctx);
      this.drawMessage(ctx);
      this.drawCheatIndicators(ctx);
      this.drawCheatHistory(ctx);
    }
    if(this.state==='dying'){
      ctx.save();
      ctx.fillStyle='rgba(60,0,20,0.35)';
      ctx.fillRect(0,0,CONFIG.W,CONFIG.H);
      ctx.textAlign='center'; ctx.textBaseline='middle';
      ctx.font='bold 52px Consolas, monospace';
      const canRespawn = this.respawnsLeft > 0;
      ctx.fillStyle = canRespawn ? '#7df9ff' : '#ff3355';
      ctx.shadowColor = ctx.fillStyle; ctx.shadowBlur = 30;
      ctx.fillText(canRespawn ? 'ВОЗРОЖДЕНИЕ...' : 'СМЕРТЬ', CONFIG.W/2, CONFIG.H/2 - 30);
      if(canRespawn){
        ctx.font='bold 20px Consolas, monospace';
        ctx.fillStyle='#ffd166'; ctx.shadowColor='#ff9f1c';
        ctx.fillText('Комната ' + this.room + ' будет перезапущена · Осталось: ' + (this.respawnsLeft-1), CONFIG.W/2, CONFIG.H/2 + 30);
      }
      ctx.restore();
    }
    if(this.bossDefeatedTimer > 0){
      ctx.save();
      const a = Math.min(1, (2.2 - this.bossDefeatedTimer) * 2);
      ctx.globalAlpha = a;
      ctx.textAlign='center'; ctx.textBaseline='middle';
      ctx.font='bold 68px Consolas, monospace';
      ctx.fillStyle='#ffd166'; ctx.shadowColor='#ff9f1c'; ctx.shadowBlur=40;
      ctx.fillText('БОСС ПОВЕРЖЕН', CONFIG.W/2, CONFIG.H*0.45);
      ctx.restore();
    }
    if(this.state==='paused') this.drawPause(ctx);
    if(this.state==='gameover') this.drawGameOver(ctx);
    if(this.state==='upgrade') this.drawUpgradeScreen(ctx);
  }

  drawCheatIndicators(ctx){
    const active = [];
    if(this.godMode) active.push('GOD');
    if(this.infiniteAmmo) active.push('AMMO ∞');
    if(this.cheatDamageMult > 1) active.push('DMG×' + this.cheatDamageMult);
    if(this.cheatSpeedMult > 1) active.push('SPD×' + this.cheatSpeedMult);
    if(this.upgradeCounts && Object.values(this.upgradeCounts).some(v => v >= 100)) active.push('MAX UPGR');
    if(active.length === 0) return;
    ctx.save();
    ctx.textAlign='right'; ctx.textBaseline='top';
    ctx.font='bold 14px Consolas, monospace';
    const t = performance.now() / 300;
    const pulse = 0.75 + Math.sin(t) * 0.25;
    ctx.fillStyle = 'rgba(255,80,170,' + pulse + ')';
    ctx.shadowColor = '#ff44aa'; ctx.shadowBlur = 12;
    ctx.fillText('⚡ ' + active.join(' · ') + ' ⚡', CONFIG.W - 20, 170);
    ctx.restore();
  }
  drawCheatHistory(ctx){
    if(!this.cheatHistory || this.cheatHistory.length === 0) return;
    ctx.save();
    ctx.textAlign='left'; ctx.textBaseline='bottom';
    ctx.font='bold 13px Consolas, monospace';
    const now = performance.now();
    const baseY = CONFIG.H - 200;
    for(let i = 0; i < this.cheatHistory.length; i++){
      const h = this.cheatHistory[i];
      const age = (now - h.time) / 4000;
      const a = clamp(1 - age, 0, 1);
      ctx.globalAlpha = a * 0.9;
      ctx.fillStyle = '#ff44aa'; ctx.shadowColor = '#ff44aa'; ctx.shadowBlur = 8;
      ctx.fillText('✓ ЧИТ: ' + h.desc, 24, baseY + i * 20);
    }
    ctx.restore();
  }
  drawWorldGrid(ctx){
    const g=CONFIG.GRID;
    const x0=Math.floor(this.cam.x/g)*g;
    const y0=Math.floor(this.cam.y/g)*g;
    ctx.strokeStyle='rgba(60,90,160,0.13)';
    ctx.lineWidth=1;
    ctx.beginPath();
    for(let x=x0; x<this.cam.x+CONFIG.W+g; x+=g){
      ctx.moveTo(x,this.cam.y-g); ctx.lineTo(x,this.cam.y+CONFIG.H+g);
    }
    for(let y=y0; y<this.cam.y+CONFIG.H+g; y+=g){
      ctx.moveTo(this.cam.x-g,y); ctx.lineTo(this.cam.x+CONFIG.W+g,y);
    }
    ctx.stroke();
  }
  drawBackgroundGrid(ctx){
    const g=60;
    ctx.strokeStyle='rgba(60,90,160,0.10)';
    ctx.lineWidth=1;
    ctx.beginPath();
    for(let x=0;x<=CONFIG.W;x+=g){ ctx.moveTo(x,0); ctx.lineTo(x,CONFIG.H); }
    for(let y=0;y<=CONFIG.H;y+=g){ ctx.moveTo(0,y); ctx.lineTo(CONFIG.W,y); }
    ctx.stroke();
  }
  drawBossBar(ctx){
    if(!this.boss || !this.boss.alive) return;
    const W = CONFIG.W;
    const barW = Math.min(720, W * 0.72);
    const barH = 22;
    const x = (W - barW) / 2;
    const y = 92;
    ctx.save();
    ctx.fillStyle = 'rgba(10,12,24,0.88)';
    ctx.fillRect(x - 8, y - 8, barW + 16, barH + 42);
    ctx.strokeStyle = '#ff3b6b'; ctx.lineWidth = 2;
    ctx.shadowColor = '#ff3b6b'; ctx.shadowBlur = 18;
    ctx.strokeRect(x - 8, y - 8, barW + 16, barH + 42);
    ctx.shadowBlur = 0;
    ctx.textAlign='center'; ctx.textBaseline='bottom';
    ctx.font='bold 15px Consolas, monospace';
    ctx.fillStyle='#ff5577'; ctx.shadowColor='#ff3b6b'; ctx.shadowBlur=12;
    ctx.fillText('☠  КУБИЧЕСКАЯ ЗМЕЯ  ☠', W/2, y + 2);
    ctx.shadowBlur=0;
    ctx.fillStyle='#151a30';
    ctx.fillRect(x, y + 10, barW, barH);
    const pct = clamp(this.boss.hp / this.boss.maxHp, 0, 1);
    const grad = ctx.createLinearGradient(x, 0, x + barW, 0);
    grad.addColorStop(0, '#ff3b6b');
    grad.addColorStop(0.5, '#ff5577');
    grad.addColorStop(1, '#b06bff');
    ctx.fillStyle = grad;
    ctx.shadowColor = '#ff3b6b'; ctx.shadowBlur = 14;
    ctx.fillRect(x, y + 10, barW * pct, barH);
    ctx.shadowBlur = 0;
    ctx.strokeStyle = 'rgba(255,255,255,0.25)';
    ctx.lineWidth = 1;
    ctx.strokeRect(x + 0.5, y + 10.5, barW - 1, barH - 1);
    ctx.fillStyle = '#ffffff';
    ctx.font='bold 14px Consolas, monospace';
    ctx.textAlign='center'; ctx.textBaseline='middle';
    ctx.fillText(Math.ceil(this.boss.hp) + ' / ' + this.boss.maxHp, W/2, y + 10 + barH/2 + 1);
    ctx.restore();
  }
  drawHUD(ctx){
    const p=this.player;
    if(!p) return;
    ctx.save();
    ctx.textBaseline='middle';
    const infoW = 240;
    ctx.font='bold 17px Consolas, monospace';
    ctx.textAlign='left';
    ctx.fillStyle='rgba(10,12,24,0.72)';
    ctx.fillRect(18,18,infoW,124);
    ctx.strokeStyle=this.difficulty.color;
    ctx.globalAlpha = 0.6; ctx.lineWidth=1.5;
    ctx.strokeRect(18,18,infoW,124);
    ctx.globalAlpha = 1;
    ctx.fillStyle= this.isBossRoom() ? '#ff5577' : '#7df9ff';
    ctx.fillText('КОМНАТА  ' + this.room + (this.isBossRoom() ? ' ☠' : ''), 32, 42);
    ctx.fillStyle='#ff5577';
    ctx.fillText('УБИЙСТВА  ' + this.kills, 32, 66);
    ctx.fillStyle='#ffd166';
    ctx.fillText('ОЧКИ  ' + this.score, 32, 90);
    ctx.font='bold 14px Consolas, monospace';
    ctx.fillStyle = this.difficulty.color;
    ctx.fillText(this.difficulty.name, 32, 116);
    ctx.fillStyle = 'rgba(180,200,240,0.85)';
    const respText = this.difficulty.respawns >= 99 ? '∞ возрождений' : (this.respawnsLeft + ' возр.');
    ctx.fillText('· ' + respText, 32 + 88, 116);
    this.drawAbilityIndicator(ctx, 42, CONFIG.H - 100, 'D', 'РЫВОК',
      p.dashReady ? 1 : (1 - p.dashCooldownTimer / (CONFIG.PLAYER.dash.cooldown + CONFIG.PLAYER.dash.duration)),
      p.dashReady ? '#7df9ff' : 'rgba(120,150,200,0.6)');
    this.drawAbilityIndicator(ctx, 92, CONFIG.H - 100, 'R', 'ЯРОСТЬ',
      p.rageActive ? 1 : (p.rageCooldownTimer > 0 ? 1 - p.rageCooldownTimer / CONFIG.PLAYER.rage.cooldown : 1),
      p.rageActive ? '#ff5577' : (p.rageReady ? '#ffcc66' : 'rgba(180,120,140,0.6)'),
      p.rageActive);
    const hbW = 300, hbH = 26;
    const hbX = 24, hbY = CONFIG.H-62;
    ctx.fillStyle='rgba(10,12,24,0.75)';
    ctx.fillRect(hbX-6,hbY-6,hbW+12,hbH+12);
    ctx.strokeStyle='rgba(80,140,255,0.5)';
    ctx.strokeRect(hbX-6,hbY-6,hbW+12,hbH+12);
    const hpPct=clamp(p.hp/p.maxHp,0,1);
    ctx.fillStyle='#151a30';
    ctx.fillRect(hbX,hbY,hbW,hbH);
    const grad=ctx.createLinearGradient(hbX,0,hbX+hbW,0);
    if(hpPct>0.5){ grad.addColorStop(0,'#00e5a0'); grad.addColorStop(1,'#7df9ff'); }
    else if(hpPct>0.25){ grad.addColorStop(0,'#ffcc00'); grad.addColorStop(1,'#ff9f1c'); }
    else { grad.addColorStop(0,'#ff3355'); grad.addColorStop(1,'#ff6688'); }
    ctx.fillStyle=grad;
    ctx.shadowColor=hpPct>0.25?'#00e5a0':'#ff3355';
    ctx.shadowBlur=12;
    ctx.fillRect(hbX,hbY,hbW*hpPct,hbH);
    ctx.shadowBlur=0;
    ctx.fillStyle='#ffffff';
    ctx.font='bold 16px Consolas, monospace';
    ctx.textAlign='center';
    ctx.fillText('HP  '+Math.ceil(p.hp)+' / '+p.maxHp, hbX+hbW/2, hbY+hbH/2+1);
    const amW=220, amH=62;
    const amX=CONFIG.W-24, amY=CONFIG.H-62;
    ctx.fillStyle='rgba(10,12,24,0.75)';
    ctx.fillRect(amX-amW, amY-6, amW, amH);
    ctx.strokeStyle='rgba(80,140,255,0.5)';
    ctx.lineWidth=1.5;
    ctx.strokeRect(amX-amW, amY-6, amW, amH);
    ctx.textAlign='right';
    if(p.reloading){
      ctx.fillStyle='#ffcc00';
      ctx.font='bold 18px Consolas, monospace';
      ctx.fillText('ПЕРЕЗАРЯДКА', amX-14, amY+16);
      const rp=1-p.reloadTimer/(CONFIG.PLAYER.reloadTime*p.reloadMult);
      ctx.fillStyle='#22283d';
      ctx.fillRect(amX-amW+12, amY+32, amW-24, 12);
      ctx.fillStyle='#ffcc00';
      ctx.shadowColor='#ffcc00'; ctx.shadowBlur=10;
      ctx.fillRect(amX-amW+12, amY+32, (amW-24)*rp, 12);
      ctx.shadowBlur=0;
    } else {
      const low = !this.infiniteAmmo && p.mag<=6;
      ctx.fillStyle= this.infiniteAmmo ? '#00e5a0' : (low?'#ff3355':'#7df9ff');
      ctx.font='bold 32px Consolas, monospace';
      ctx.fillText(this.infiniteAmmo ? '∞' : p.mag, amX-82, amY+26);
      ctx.fillStyle='rgba(180,200,255,0.55)';
      ctx.font='bold 18px Consolas, monospace';
      ctx.fillText('/ '+p.maxMag, amX-14, amY+28);
      if(!this.isTouch){
        ctx.fillStyle='rgba(160,180,220,0.7)';
        ctx.font='12px Consolas, monospace';
        ctx.fillText('R — перезарядка · Space — рывок · Q — ярость', amX-14, amY+50);
      }
    }
    if(this.drones.length>0){
      ctx.textAlign='left';
      ctx.fillStyle='rgba(125,249,255,0.9)';
      ctx.font='bold 14px Consolas, monospace';
      ctx.fillText('DRONES  ×'+this.drones.length, 24, CONFIG.H-130);
    }
    if(this.state==='playing' && this.roomActive){
      ctx.textAlign='right';
      ctx.font='bold 16px Consolas, monospace';
      if(this.isBossRoom() && this.boss && this.boss.alive){
        ctx.fillStyle='rgba(255,120,150,'+(0.75+Math.sin(performance.now()/180)*0.25)+')';
        ctx.fillText('БОСС АКТИВЕН', CONFIG.W-24, CONFIG.H-100);
      } else if(!this.isBossRoom()){
        ctx.fillStyle='rgba(255,120,150,'+(0.75+Math.sin(performance.now()/180)*0.25)+')';
        ctx.fillText('ВРАГИ  '+this.bots.length+' / '+this.toSpawn, CONFIG.W-24, CONFIG.H-100);
      }
    }
    ctx.restore();
  }
  drawAbilityIndicator(ctx, cx, cy, letter, label, progress, color, active=false){
    const r = 18;
    ctx.save();
    ctx.fillStyle = 'rgba(10,12,24,0.85)';
    ctx.beginPath(); ctx.arc(cx, cy, r, 0, TAU); ctx.fill();
    ctx.strokeStyle = color; ctx.lineWidth = 2;
    ctx.shadowColor = active ? '#ff5577' : color;
    ctx.shadowBlur = active ? 16 : 6;
    ctx.beginPath(); ctx.arc(cx, cy, r, 0, TAU); ctx.stroke();
    ctx.shadowBlur = 0;
    if(progress < 1){
      ctx.strokeStyle = 'rgba(80,100,140,0.9)';
      ctx.lineWidth = 3;
      ctx.beginPath();
      ctx.arc(cx, cy, r - 3, -Math.PI/2, -Math.PI/2 + TAU * progress);
      ctx.stroke();
    }
    ctx.fillStyle = color;
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.font = 'bold 15px Consolas, monospace';
    ctx.fillText(letter, cx, cy + 1);
    ctx.fillStyle = 'rgba(170,190,230,0.75)';
    ctx.font = 'bold 9px Consolas, monospace';
    ctx.fillText(label, cx, cy + r + 8);
    ctx.restore();
  }
  drawMinimap(ctx){
    const mw = 180, mh = 130;
    const mx = CONFIG.W - mw - 20, my = 20;
    const sx = mw/CONFIG.WORLD_W, sy = mh/CONFIG.WORLD_H;
    ctx.save();
    ctx.fillStyle='rgba(8,10,22,0.82)';
    ctx.fillRect(mx,my,mw,mh);
    ctx.strokeStyle='rgba(80,140,255,0.6)';
    ctx.lineWidth=1.5;
    ctx.shadowColor='rgba(80,140,255,0.7)'; ctx.shadowBlur=12;
    ctx.strokeRect(mx,my,mw,mh);
    ctx.shadowBlur=0;
    ctx.fillStyle='rgba(59,107,255,0.45)';
    for(const w of this.walls){
      ctx.fillRect(mx+w.x*sx, my+w.y*sy, Math.max(1,w.w*sx), Math.max(1,w.h*sy));
    }
    for(const b of this.bots){
      ctx.fillStyle = b.isSapper ? '#ffcc00' : b.cfg.color;
      ctx.beginPath(); ctx.arc(mx+b.x*sx, my+b.y*sy, b.isSapper ? 3.5 : 2.5, 0, TAU); ctx.fill();
    }
    if(this.boss && this.boss.alive){
      ctx.fillStyle='#ff3b6b';
      ctx.shadowColor='#ff3b6b'; ctx.shadowBlur=10;
      ctx.beginPath(); ctx.arc(mx+this.boss.x*sx, my+this.boss.y*sy, 5, 0, TAU); ctx.fill();
      for(const seg of this.boss.segments){
        ctx.fillStyle='#b06bff';
        ctx.beginPath(); ctx.arc(mx+seg.x*sx, my+seg.y*sy, 2.5, 0, TAU); ctx.fill();
      }
      ctx.shadowBlur=0;
    }
    if(this.player && this.player.alive){
      ctx.fillStyle='#22e6ff';
      ctx.shadowColor='#22e6ff'; ctx.shadowBlur=8;
      ctx.beginPath(); ctx.arc(mx+this.player.x*sx, my+this.player.y*sy, 3.5, 0, TAU); ctx.fill();
      ctx.shadowBlur=0;
    }
    ctx.fillStyle='rgba(150,180,230,0.75)';
    ctx.font='10px Consolas, monospace';
    ctx.textAlign='left'; ctx.textBaseline='top';
    ctx.fillText('КАРТА', mx+5, my+4);
    ctx.restore();
  }
  drawTouchControls(ctx){
    if(!this.isTouch) return;
    if(this.state==='playing'){
      if(!this.moveStick.active)
        this.drawJoystickHint(ctx, 150, CONFIG.H-140, 'ДВИЖЕНИЕ', '#7df9ff');
      if(!this.aimStick.active)
        this.drawJoystickHint(ctx, CONFIG.W-150, CONFIG.H-140, 'ПРИЦЕЛ / ОГОНЬ', '#ff3b6b');
      this.drawActionButton(ctx, this.reloadBtn, 'RELOAD', '#7df9ff',
        this.player && this.player.reloading,
        this.player && this.player.reloading ? this.player.reloadTimer/(CONFIG.PLAYER.reloadTime*this.player.reloadMult) : 0);
      this.drawActionButton(ctx, this.dashBtn, 'DASH', '#7df9ff',
        !this.player || !this.player.dashReady,
        this.player && this.player.dashCooldownTimer > 0 ? this.player.dashCooldownTimer/(CONFIG.PLAYER.dash.cooldown+CONFIG.PLAYER.dash.duration) : 0);
      this.drawActionButton(ctx, this.rageBtn, 'RAGE', '#ff5577',
        !this.player || !this.player.rageReady,
        this.player && this.player.rageCooldownTimer > 0 ? this.player.rageCooldownTimer/CONFIG.PLAYER.rage.cooldown : 0,
        this.player && this.player.rageActive);
      this.drawActionButton(ctx, this.pauseBtn, 'PAUSE', '#a06bff', false, 0, false);
    }
    this.moveStick.draw(ctx);
    this.aimStick.draw(ctx);
  }
  drawJoystickHint(ctx,cx,cy,label,color){
    ctx.save();
    ctx.globalAlpha=0.16;
    ctx.strokeStyle=color; ctx.lineWidth=2;
    ctx.beginPath(); ctx.arc(cx,cy,48,0,TAU); ctx.stroke();
    ctx.globalAlpha=0.32;
    ctx.fillStyle=color;
    ctx.font='bold 11px Consolas, monospace';
    ctx.textAlign='center'; ctx.textBaseline='middle';
    ctx.fillText(label, cx, cy+66);
    ctx.restore();
  }
  drawActionButton(ctx, r, label, color, onCooldown, cooldownPct, active=false){
    const cx = r.x + r.w/2;
    const cy = r.y + r.h/2;
    const rad = Math.min(r.w, r.h)/2;
    ctx.save();
    ctx.fillStyle = active ? 'rgba(60,15,30,0.9)' : (onCooldown ? 'rgba(20,22,35,0.85)' : 'rgba(14,20,40,0.85)');
    ctx.beginPath(); ctx.arc(cx,cy,rad,0,TAU); ctx.fill();
    const drawColor = active ? '#ff5577' : (onCooldown ? 'rgba(120,140,180,0.6)' : color);
    ctx.strokeStyle = drawColor;
    ctx.lineWidth = 2.5;
    ctx.shadowColor = drawColor;
    ctx.shadowBlur = active ? 22 : (onCooldown ? 4 : 10);
    ctx.beginPath(); ctx.arc(cx,cy,rad,0,TAU); ctx.stroke();
    ctx.shadowBlur = 0;
    if(onCooldown && cooldownPct > 0){
      ctx.strokeStyle = 'rgba(200,220,255,0.75)';
      ctx.lineWidth = 4;
      ctx.beginPath();
      ctx.arc(cx,cy,rad-4, -Math.PI/2, -Math.PI/2 + TAU * (1 - cooldownPct));
      ctx.stroke();
    }
    ctx.fillStyle = drawColor;
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.font = 'bold 12px Consolas, monospace';
    ctx.fillText(label, cx, cy);
    ctx.restore();
  }
  drawMessage(ctx){
    if(this.message.t<=0) return;
    const m=this.message;
    const p=m.t/m.max;
    const alpha = p>0.75 ? (1-p)/0.25 : Math.min(1, p/0.35);
    ctx.save();
    ctx.globalAlpha=clamp(alpha,0,1);
    ctx.textAlign='center'; ctx.textBaseline='middle';
    if(m.text){
      ctx.font='bold 54px Consolas, monospace';
      ctx.fillStyle='#7df9ff';
      ctx.shadowColor='#22e6ff'; ctx.shadowBlur=26;
      ctx.fillText(m.text, CONFIG.W/2, CONFIG.H*0.30);
    }
    if(m.sub){
      ctx.font='bold 20px Consolas, monospace';
      ctx.fillStyle='#ffd166';
      ctx.shadowColor='#ff9f1c'; ctx.shadowBlur=16;
      ctx.fillText(m.sub, CONFIG.W/2, CONFIG.H*0.30 + (m.text?52:0));
    }
    ctx.restore();
  }
  drawMenu(ctx){
    const cx=CONFIG.W/2;
    ctx.save();
    ctx.textAlign='center'; ctx.textBaseline='middle';
    ctx.font='bold ' + Math.min(76, CONFIG.W*0.06) + 'px Consolas, monospace';
    ctx.fillStyle='#7df9ff';
    ctx.shadowColor='#22e6ff'; ctx.shadowBlur=40;
    ctx.fillText('NEON SHOOTER', cx, CONFIG.H*0.10);
    ctx.font='bold 18px Consolas, monospace';
    ctx.fillStyle='#ff3b6b';
    ctx.shadowColor='#ff3b6b'; ctx.shadowBlur=20;
    ctx.fillText('БОСС КАЖДЫЕ 5 КОМНАТ · ПОДРЫВНИКИ · РЫВОК · ЯРОСТЬ', cx, CONFIG.H*0.17);
    ctx.shadowBlur=0;
    if(this.highScore > 0){
      ctx.font='bold 16px Consolas, monospace';
      ctx.fillStyle='#ffd166';
      ctx.fillText('🏆 Рекорд: ' + this.highScore + '  ·  Всего убийств: ' + this.totalKills, cx, CONFIG.H*0.215);
    }
    ctx.font='bold 20px Consolas, monospace';
    ctx.fillStyle='rgba(180,210,255,0.95)';
    ctx.fillText('ВЫБЕРИ СЛОЖНОСТЬ', cx, CONFIG.H*0.28);
    ctx.font='15px Consolas, monospace';
    ctx.fillStyle='rgba(160,185,225,0.8)';
    const controls=this.isTouch?[
      'Левый джойстик — движение   ·   Правый — прицел и огонь',
      'Кнопки DASH, RAGE, RELOAD, PAUSE — справа снизу'
    ]:[
      'WASD (или ЦФЫВ) / Стрелки — движение   ·   Мышь + ЛКМ — огонь',
      'R — перезарядка   ·   Space — РЫВОК   ·   Q — ЯРОСТЬ   ·   Shift — спринт',
      'Клавиши 1–4 — сложность   ·   M — звук   ·   F — полный экран'
    ];
    controls.forEach((line,i)=>{
      ctx.fillText(line, cx, CONFIG.H*0.32 + i*22);
    });
    ctx.restore();
    for(const btn of this.difficultyBtns){
      const diff = DIFFICULTIES[btn.key];
      const selected = this.selectedDifficulty === btn.key;
      this.drawDifficultyButton(ctx, btn, diff, selected);
    }
    this.drawButton(ctx, this.startBtn, 'START', '#22e6ff');
    this.drawButton(ctx, this.fullscreenBtn, '⛶', '#7df9ff');
    this.drawButton(ctx, this.soundBtn, sound.enabled ? '🔊' : '🔇', sound.enabled ? '#7df9ff' : '#555');
    this.drawButtonCustom(ctx, this.settingsBtn, '⚙ НАСТРОЙКИ', '#a06bff', 20);
    ctx.save();
    ctx.textAlign='center';
    ctx.fillStyle='rgba(140,170,220,0.55)';
    ctx.font='13px Consolas, monospace';
    ctx.fillText(this.isTouch?'Нажми на сложность, затем START':'или нажми ENTER · 1-4 — сложность · M — звук',
      cx, this.settingsBtn.y + this.settingsBtn.h + 16);
    ctx.restore();
  }
  drawDifficultyButton(ctx, r, diff, selected){
    ctx.save();
    ctx.fillStyle = selected ? 'rgba(20,40,80,0.95)' : 'rgba(12,16,32,0.85)';
    ctx.fillRect(r.x, r.y, r.w, r.h);
    ctx.strokeStyle = diff.color;
    ctx.lineWidth = selected ? 3 : 1.8;
    ctx.shadowColor = diff.color;
    ctx.shadowBlur = selected ? 22 : 8;
    ctx.strokeRect(r.x, r.y, r.w, r.h);
    ctx.shadowBlur = 0;
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.font = 'bold ' + Math.min(22, r.w*0.14) + 'px Consolas, monospace';
    ctx.fillStyle = diff.color;
    ctx.shadowColor = diff.color;
    ctx.shadowBlur = selected ? 16 : 6;
    ctx.fillText(diff.name, r.x + r.w/2, r.y + r.h*0.30);
    ctx.shadowBlur = 0;
    ctx.font = Math.min(10, r.w*0.065) + 'px Consolas, monospace';
    ctx.fillStyle = 'rgba(200,215,240,0.85)';
    ctx.fillText(diff.desc, r.x + r.w/2, r.y + r.h*0.68);
    if(selected){
      ctx.fillStyle = diff.color;
      ctx.beginPath(); ctx.arc(r.x + r.w - 12, r.y + 12, 5, 0, TAU); ctx.fill();
    }
    ctx.restore();
  }
  drawButton(ctx,r,label,color){
    const m=this.mouse;
    const hover=!this.isTouch && this.inRect(m,r);
    ctx.save();
    ctx.fillStyle=hover?'rgba(30,50,90,0.92)':'rgba(14,20,40,0.88)';
    ctx.fillRect(r.x,r.y,r.w,r.h);
    ctx.strokeStyle=color;
    ctx.lineWidth=hover?3:2;
    ctx.shadowColor=color;
    ctx.shadowBlur=hover?26:14;
    ctx.strokeRect(r.x,r.y,r.w,r.h);
    ctx.shadowBlur=0;
    ctx.textAlign='center'; ctx.textBaseline='middle';
    ctx.font='bold 24px Consolas, monospace';
    ctx.fillStyle=color;
    ctx.shadowColor=color;
    ctx.shadowBlur=hover?18:8;
    ctx.fillText(label, r.x+r.w/2, r.y+r.h/2+2);
    ctx.restore();
  }
  drawButtonCustom(ctx, r, label, color, fontSize){
    const m=this.mouse;
    const hover=!this.isTouch && this.inRect(m,r);
    ctx.save();
    ctx.fillStyle=hover?'rgba(30,50,90,0.92)':'rgba(14,20,40,0.88)';
    ctx.fillRect(r.x,r.y,r.w,r.h);
    ctx.strokeStyle=color;
    ctx.lineWidth=hover?2.5:1.6;
    ctx.shadowColor=color;
    ctx.shadowBlur=hover?20:10;
    ctx.strokeRect(r.x,r.y,r.w,r.h);
    ctx.shadowBlur=0;
    ctx.textAlign='center'; ctx.textBaseline='middle';
    ctx.font='bold ' + fontSize + 'px Consolas, monospace';
    ctx.fillStyle=color;
    ctx.shadowColor=color;
    ctx.shadowBlur=hover?14:6;
    ctx.fillText(label, r.x+r.w/2, r.y+r.h/2+1);
    ctx.restore();
  }
  drawSettings(ctx){
    const W = CONFIG.W, H = CONFIG.H;
    ctx.save();
    ctx.fillStyle='rgba(4,6,18,0.94)';
    ctx.fillRect(0,0,W,H);
    ctx.textAlign='center'; ctx.textBaseline='middle';
    ctx.font='bold '+Math.min(52, W*0.04)+'px Consolas, monospace';
    ctx.fillStyle='#a06bff';
    ctx.shadowColor='#a06bff'; ctx.shadowBlur=30;
    ctx.fillText('НАСТРОЙКИ', W/2, H*0.10);
    ctx.shadowBlur=0;
    ctx.restore();
    if(this.showCheatList){ this.drawCheatListScreen(ctx); return; }
    ctx.save();
    ctx.textAlign='center'; ctx.textBaseline='middle';
    ctx.font='13px Consolas, monospace';
    ctx.fillStyle='rgba(160,180,220,0.6)';
    ctx.fillText(this.settingsReturnState === 'paused' ? '(вернёшься в паузу)' : '(вернёшься в меню)', W/2, H*0.135);
    ctx.restore();
    ctx.save();
    ctx.textAlign='center'; ctx.textBaseline='middle';
    ctx.font='bold 16px Consolas, monospace';
    ctx.fillStyle='rgba(180,200,240,0.85)';
    ctx.fillText('ЗВУК', W/2, this.settingsSoundBtn.y - 20);
    ctx.restore();
    const soundLabel = sound.enabled ? '🔊 ВКЛ' : '🔇 ВЫКЛ';
    const soundColor = sound.enabled ? '#7df9ff' : '#777';
    this.drawButtonCustom(ctx, this.settingsSoundBtn, soundLabel, soundColor, 22);
    ctx.save();
    ctx.textAlign='center'; ctx.textBaseline='middle';
    ctx.font='bold 16px Consolas, monospace';
    ctx.fillStyle='rgba(180,200,240,0.85)';
    ctx.fillText('ГРОМКОСТЬ: ' + Math.round(sound.volume * 100) + '%', W/2, this.settingsVolumeDownBtn.y - 18);
    ctx.restore();
    this.drawButtonCustom(ctx, this.settingsVolumeDownBtn, '−', '#7df9ff', 32);
    this.drawButtonCustom(ctx, this.settingsVolumeUpBtn, '+', '#7df9ff', 32);
    const barX = this.settingsVolumeDownBtn.x + this.settingsVolumeDownBtn.w + 14;
    const barY = this.settingsVolumeDownBtn.y + 16;
    const barW = this.settingsVolumeUpBtn.x - barX - 14;
    const barH = this.settingsVolumeDownBtn.h - 32;
    ctx.save();
    ctx.fillStyle='rgba(10,14,28,0.9)';
    ctx.fillRect(barX, barY, barW, barH);
    ctx.strokeStyle='rgba(125,249,255,0.6)';
    ctx.lineWidth=1.5;
    ctx.strokeRect(barX+0.5, barY+0.5, barW-1, barH-1);
    ctx.fillStyle='#7df9ff';
    ctx.shadowColor='#7df9ff'; ctx.shadowBlur=12;
    ctx.fillRect(barX+1, barY+1, (barW-2) * sound.volume, barH-2);
    ctx.shadowBlur=0;
    for(let i=1;i<10;i++){
      const x = barX + barW * (i/10);
      ctx.strokeStyle='rgba(0,0,0,0.35)';
      ctx.lineWidth=1;
      ctx.beginPath(); ctx.moveTo(x, barY); ctx.lineTo(x, barY+barH); ctx.stroke();
    }
    ctx.restore();
    ctx.save();
    ctx.textAlign='center'; ctx.textBaseline='middle';
    ctx.font='bold 16px Consolas, monospace';
    ctx.fillStyle='#ffcc00';
    ctx.fillText('ЧИТ-КОД', W/2, this.settingsCheatInputRect.y - 18);
    ctx.restore();
    ctx.save();
    ctx.strokeStyle = (document.activeElement === this.inputEl) ? '#ffffff' : 'rgba(125,249,255,0.6)';
    ctx.lineWidth=2;
    ctx.shadowColor='#7df9ff';
    ctx.shadowBlur=(document.activeElement === this.inputEl) ? 22 : 8;
    ctx.strokeRect(this.settingsCheatInputRect.x, this.settingsCheatInputRect.y,
                   this.settingsCheatInputRect.w, this.settingsCheatInputRect.h);
    ctx.shadowBlur=0;
    ctx.restore();
    this.drawButtonCustom(ctx, this.settingsCheatActivateBtn, 'АКТИВИРОВАТЬ', '#00e5a0', 18);
    if(this.settingsMessage && performance.now() < this.settingsMessageUntil){
      const remaining = (this.settingsMessageUntil - performance.now()) / 2800;
      const alpha = Math.min(1, remaining * 3);
      ctx.save();
      ctx.globalAlpha = alpha;
      ctx.textAlign='center'; ctx.textBaseline='middle';
      ctx.font='bold 17px Consolas, monospace';
      ctx.fillStyle = this.settingsMessageColor;
      ctx.shadowColor = this.settingsMessageColor;
      ctx.shadowBlur=14;
      ctx.fillText(this.settingsMessage, W/2, this.settingsCheatActivateBtn.y + this.settingsCheatActivateBtn.h + 24);
      ctx.restore();
    }
    this.drawButtonCustom(ctx, this.settingsShowCodesBtn, 'СПИСОК КОДОВ', '#ff44aa', 14);
    this.drawButtonCustom(ctx, this.settingsResetCheatsBtn, 'СБРОС ЧИТОВ', '#ff9f1c', 14);
    const active = [];
    if(this.godMode) active.push('GOD');
    if(this.infiniteAmmo) active.push('AMMO∞');
    if(this.cheatDamageMult > 1) active.push('DMG×' + this.cheatDamageMult);
    if(this.cheatSpeedMult > 1) active.push('SPD×' + this.cheatSpeedMult);
    if(this.pending.droneCount > 0) active.push('DRONES+' + this.pending.droneCount);
    if(this.pending.maxUpgrades) active.push('MAXUPG');
    if(active.length){
      ctx.save();
      ctx.textAlign='center'; ctx.textBaseline='middle';
      ctx.font='bold 14px Consolas, monospace';
      ctx.fillStyle='rgba(255,80,170,0.95)';
      ctx.shadowColor='#ff44aa'; ctx.shadowBlur=12;
      ctx.fillText('Активны: ' + active.join(' · '), W/2, this.settingsResetCheatsBtn.y + this.settingsResetCheatsBtn.h + 26);
      ctx.restore();
    }
    this.drawButtonCustom(ctx, this.settingsBackBtn, '← НАЗАД', '#7df9ff', 24);
  }
  drawCheatListScreen(ctx){
    const W = CONFIG.W, H = CONFIG.H;
    ctx.save();
    ctx.textAlign='center'; ctx.textBaseline='middle';
    ctx.font='bold '+Math.min(38, W*0.03)+'px Consolas, monospace';
    ctx.fillStyle='#ff44aa';
    ctx.shadowColor='#ff44aa'; ctx.shadowBlur=24;
    ctx.fillText('СПИСОК КОДОВ', W/2, H*0.10);
    ctx.shadowBlur=0;
    ctx.restore();
    const total = CHEAT_CODES.length;
    const colCount = W >= 700 ? 2 : 1;
    const perCol = Math.ceil(total / colCount);
    const colW = Math.min(420, (W - 120) / colCount);
    const startX = W/2 - (colCount * colW) / 2;
    const startY = H * 0.16;
    const rowH = Math.min(38, (H * 0.62) / perCol);
    ctx.save();
    ctx.textBaseline='middle';
    for(let i = 0; i < total; i++){
      const col = Math.floor(i / perCol);
      const row = i % perCol;
      const x = startX + col * colW;
      const y = startY + row * rowH;
      const c = CHEAT_CODES[i];
      ctx.fillStyle = (i % 2 === 0) ? 'rgba(20,30,55,0.55)' : 'rgba(14,20,40,0.55)';
      ctx.fillRect(x + 10, y - rowH*0.45, colW - 20, rowH*0.9);
      ctx.textAlign='left';
      ctx.font='bold 14px Consolas, monospace';
      ctx.fillStyle='#7df9ff';
      ctx.shadowColor='#7df9ff';
      ctx.shadowBlur=6;
      ctx.fillText(c.code, x + 20, y);
      ctx.textAlign='right';
      ctx.font='13px Consolas, monospace';
      ctx.fillStyle='rgba(200,220,255,0.85)';
      ctx.shadowBlur=0;
      ctx.fillText(c.desc, x + colW - 20, y);
    }
    ctx.restore();
    this.drawButtonCustom(ctx, this.settingsBackBtn, '← НАЗАД', '#7df9ff', 24);
  }
  drawPause(ctx){
    ctx.save();
    ctx.fillStyle='rgba(4,6,14,0.82)';
    ctx.fillRect(0,0,CONFIG.W,CONFIG.H);
    ctx.textAlign='center'; ctx.textBaseline='middle';
    ctx.font='bold 68px Consolas, monospace';
    ctx.fillStyle='#7df9ff';
    ctx.shadowColor='#22e6ff'; ctx.shadowBlur=30;
    ctx.fillText('ПАУЗА', CONFIG.W/2, CONFIG.H*0.26);
    ctx.shadowBlur=0;
    ctx.font='18px Consolas, monospace';
    ctx.fillStyle='rgba(170,195,240,0.75)';
    ctx.fillText(this.isTouch ? 'Управление приостановлено' : 'Esc — продолжить игру', CONFIG.W/2, CONFIG.H*0.33);
    ctx.restore();
    this.drawButtonCustom(ctx, this.resumeBtn, 'ПРОДОЛЖИТЬ', '#22e6ff', 24);
    const soundLabel = sound.enabled ? '🔊 ВКЛ' : '🔇 ВЫКЛ';
    const soundColor = sound.enabled ? '#7df9ff' : '#777';
    this.drawButtonCustom(ctx, this.pauseSoundBtn, soundLabel, soundColor, 16);
    this.drawButtonCustom(ctx, this.pauseSettingsBtn, '⚙ НАСТРОЙКИ', '#a06bff', 16);
    this.drawButtonCustom(ctx, this.pauseMenuBtn, 'В МЕНЮ', '#ff5577', 16);
    ctx.save();
    ctx.textAlign='center'; ctx.textBaseline='top';
    ctx.font='13px Consolas, monospace';
    ctx.fillStyle='rgba(140,170,220,0.55)';
    ctx.fillText(this.isTouch ? 'Нажми кнопку, чтобы продолжить' : 'M — звук   ·   Esc — вернуться в игру',
      CONFIG.W/2, this.pauseMenuBtn.y + this.pauseMenuBtn.h + 22);
    ctx.restore();
  }
  drawGameOver(ctx){
    ctx.save();
    ctx.fillStyle='rgba(12,2,8,0.88)';
    ctx.fillRect(0,0,CONFIG.W,CONFIG.H);
    ctx.textAlign='center'; ctx.textBaseline='middle';
    ctx.font='bold 76px Consolas, monospace';
    ctx.fillStyle='#ff3355';
    ctx.shadowColor='#ff3355'; ctx.shadowBlur=36;
    ctx.fillText('GAME OVER', CONFIG.W/2, CONFIG.H*0.18);
    ctx.shadowBlur=0;
    ctx.font='bold 20px Consolas, monospace';
    ctx.fillStyle = this.difficulty.color;
    ctx.fillText('СЛОЖНОСТЬ: ' + this.difficulty.name, CONFIG.W/2, CONFIG.H*0.28);
    ctx.font='bold 24px Consolas, monospace';
    ctx.fillStyle='#ffd166';
    ctx.fillText('ИТОГОВАЯ СТАТИСТИКА', CONFIG.W/2, CONFIG.H*0.35);
    const stats=[
      ['Комнат пройдено', this.room > 0 ? (this.room - 1) : 0],
      ['Убийств', this.kills],
      ['Очков', this.score],
      ['Время', this.elapsed.toFixed(1)+' сек'],
      ['🏆 Рекорд', this.highScore]
    ];
    ctx.font='20px Consolas, monospace';
    stats.forEach((s,i)=>{
      const y=CONFIG.H*0.41 + i*30;
      ctx.textAlign='right';
      ctx.fillStyle='rgba(170,195,240,0.85)';
      ctx.fillText(s[0], CONFIG.W/2-20, y);
      ctx.textAlign='left';
      ctx.fillStyle='#7df9ff';
      ctx.fillText(''+s[1], CONFIG.W/2+20, y);
    });
    ctx.restore();
    this.drawButton(ctx, this.restartBtn, 'В МЕНЮ', '#ff3b6b');
    ctx.save();
    ctx.textAlign='center';
    ctx.fillStyle='rgba(140,170,220,0.55)';
    ctx.font='13px Consolas, monospace';
    ctx.fillText(this.isTouch?'или нажми кнопку':'или нажми ENTER (вернёт в меню)',
      CONFIG.W/2, this.restartBtn.y + this.restartBtn.h + 24);
    ctx.restore();
  }
  drawUpgradeScreen(ctx){
    const W=CONFIG.W, H=CONFIG.H;
    ctx.save();
    ctx.fillStyle='rgba(4,6,18,0.90)';
    ctx.fillRect(0,0,W,H);
    ctx.textAlign='center'; ctx.textBaseline='middle';
    ctx.font='bold '+Math.min(54, W*0.042)+'px Consolas, monospace';
    ctx.fillStyle='#7df9ff';
    ctx.shadowColor='#22e6ff'; ctx.shadowBlur=30;
    ctx.fillText('УЛУЧШЕНИЯ', W/2, H*0.10);
    ctx.shadowBlur=0;
    ctx.font='bold 20px Consolas, monospace';
    ctx.fillStyle='#ffd166';
    ctx.fillText('Комната ' + this.room + ' зачищена   •   Очки: ' + this.score, W/2, H*0.165);
    const nextRoom = this.room + 1;
    const isBossNext = nextRoom % CONFIG.BOSS_EVERY === 0;
    const nextBotCount = this.difficulty.botCount + (nextRoom - 1) * this.difficulty.botGrowth;
    const hpMul = (1 + (nextRoom-1)*CONFIG.ROOM_HP_SCALE) * this.difficulty.botHpMult;
    const dmgMul = (1 + (nextRoom-1)*CONFIG.ROOM_DMG_SCALE) * this.difficulty.botDmgMult;
    ctx.font='bold 15px Consolas, monospace';
    if(isBossNext){
      ctx.fillStyle='rgba(255,80,120,'+(0.8+Math.sin(performance.now()/180)*0.2)+')';
      ctx.shadowColor='#ff3b6b';
      ctx.shadowBlur=18;
      ctx.fillText('⚠ СЛЕДУЮЩАЯ КОМНАТА — БОСС: КУБИЧЕСКАЯ ЗМЕЯ ⚠', W/2, H*0.205);
      ctx.shadowBlur=0;
    } else {
      ctx.fillStyle='rgba(255,140,160,'+(0.8+Math.sin(performance.now()/200)*0.15)+')';
      ctx.fillText('Следующая комната: '+nextBotCount+' врагов · HP ×'+hpMul.toFixed(2)+' · Урон ×'+dmgMul.toFixed(2),
        W/2, H*0.205);
    }
    ctx.restore();
    for(const card of this.upgradeCards){
      const up=card.upgrade;
      const cost=this.getUpgradeCost(up);
      const canBuy=this.score>=cost;
      const lvl=this.upgradeCounts[up.id]||0;
      ctx.save();
      ctx.fillStyle = canBuy ? 'rgba(16,26,50,0.94)' : 'rgba(14,14,22,0.75)';
      ctx.fillRect(card.x, card.y, card.w, card.h);
      const borderCol = lvl >= 100 ? '#ffd166' : (canBuy ? '#7df9ff' : 'rgba(90,100,130,0.55)');
      ctx.strokeStyle = borderCol;
      ctx.lineWidth = lvl >= 100 ? 3 : 2;
      ctx.shadowColor = lvl >= 100 ? '#ffd166' : (canBuy ? '#22e6ff' : 'transparent');
      ctx.shadowBlur = lvl >= 100 ? 20 : (canBuy ? 12 : 0);
      ctx.strokeRect(card.x, card.y, card.w, card.h);
      ctx.shadowBlur = 0;
      if(lvl>0){
        ctx.fillStyle = lvl >= 100 ? '#ffd166' : (canBuy ? '#ffd166' : 'rgba(150,140,100,0.7)');
        ctx.font = 'bold 12px Consolas, monospace';
        ctx.textAlign = 'right';
        ctx.textBaseline = 'top';
        ctx.fillText('УР. '+lvl + (lvl >= 100 ? ' ★' : ''), card.x+card.w-8, card.y+7);
      }
      ctx.textAlign = 'center';
      ctx.textBaseline = 'top';
      ctx.font = 'bold 18px Consolas, monospace';
      ctx.fillStyle = canBuy ? '#ffffff' : 'rgba(200,200,200,0.7)';
      ctx.fillText(up.name, card.x + card.w/2, card.y + 16);
      ctx.font = '14px Consolas, monospace';
      ctx.fillStyle = canBuy ? 'rgba(180,210,255,0.95)' : 'rgba(150,160,180,0.7)';
      ctx.fillText(up.desc, card.x + card.w/2, card.y + 44);
      ctx.font = 'bold 16px Consolas, monospace';
      ctx.fillStyle = canBuy ? '#ffd166' : '#885555';
      ctx.textBaseline = 'bottom';
      ctx.fillText(cost + ' очк.', card.x + card.w/2, card.y + card.h - 10);
      ctx.restore();
    }
    this.drawButton(ctx, this.upgradeContinueBtn, 'СЛЕДУЮЩАЯ КОМНАТА →', '#22e6ff');
    ctx.save();
    ctx.textAlign='center'; ctx.textBaseline='top';
    ctx.font='13px Consolas, monospace';
    ctx.fillStyle='rgba(140,170,220,0.55)';
    ctx.fillText(this.isTouch ? 'Нажми карточку, чтобы купить улучшение'
      : 'Кликни карточку для покупки   ·   ENTER — продолжить',
      W/2, H - 22);
    ctx.restore();
  }
}

/* =========================================================================
   ЗАПУСК
   ========================================================================= */
const canvas = document.getElementById('game');
const game = new Game(canvas);

function resizeCanvas(){
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
  const screenW = window.innerWidth;
  const screenH = window.innerHeight;
  const LOGICAL_H = 720;
  const LOGICAL_W = Math.max(640, Math.floor(LOGICAL_H * (screenW / screenH)));
  CONFIG.W = LOGICAL_W;
  CONFIG.H = LOGICAL_H;
  canvas.width  = Math.floor(LOGICAL_W * dpr);
  canvas.height = Math.floor(LOGICAL_H * dpr);
  canvas.style.width  = screenW + 'px';
  canvas.style.height = screenH + 'px';
  game.dpr = dpr;
  game.layoutUI();
  if(game.player) game.recenterCamera();
  game.positionCheatInput();
}

window.addEventListener('resize', resizeCanvas);
window.addEventListener('orientationchange', ()=>setTimeout(resizeCanvas,150));
resizeCanvas();

const cheatInputEl = document.getElementById('cheatInput');
cheatInputEl.addEventListener('focus', ()=>{ sound.init(); sound.resume(); });
cheatInputEl.addEventListener('keydown', (e)=>{
  if(e.key === 'Escape') cheatInputEl.blur();
});

(async function boot(){
  await initYandexSDK();
  if(sdkPlayer){
    const cloudData = await loadPlayerProgress();
    if(cloudData){
      if(cloudData.highScore && cloudData.highScore > game.highScore) game.highScore = cloudData.highScore;
      if(cloudData.totalKills) game.totalKills = Math.max(game.totalKills, cloudData.totalKills);
      if(cloudData.totalRooms) game.totalRooms = Math.max(game.totalRooms, cloudData.totalRooms);
      game.saveLocalProgress();
    }
  }
  notifyGameReady();
  let lastTime = performance.now();
  let accumulator = 0;
  function loop(now){
    requestAnimationFrame(loop);
    let frameTime = (now - lastTime) / 1000;
    lastTime = now;
    if(frameTime > 0.25) frameTime = 0.25;
    accumulator += frameTime;
    let steps = 0;
    while(accumulator >= CONFIG.FIXED_DT && steps < 5){
      game.update(CONFIG.FIXED_DT);
      accumulator -= CONFIG.FIXED_DT;
      steps++;
    }
    game.render();
  }
  requestAnimationFrame(loop);
})();
</script>
</body>
</html>

Game Source: NEON SHOOTER

Creator: PrismPilot23

Libraries: none

Complexity: complex (3647 lines, 144.6 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: neon-shooter-prismpilot23" to link back to the original. Then publish at arcadelab.ai/publish.