🎮ArcadeLab

НЕОН: Арена

by BlazingBear24
2309 lines93.1 KB
▶ Play
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
<meta name="theme-color" content="#05060d">
<title>НЕОН: Арена</title>
<style>
*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent;}
.hidden{display:none !important;}
html,body{height:100%;overflow:hidden;background:#05060d;
  font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,system-ui,sans-serif;
  color:#e8f0ff;user-select:none;-webkit-user-select:none;overscroll-behavior:none;}
canvas{position:fixed;inset:0;display:block;touch-action:none;}

#hud{position:fixed;inset:0;pointer-events:none;z-index:10;
  padding:calc(10px + env(safe-area-inset-top)) 12px 0 12px;}
.hud-row{display:flex;align-items:center;gap:10px;}
.stat{display:flex;align-items:center;gap:8px;flex:1;}
.stat.center{flex-direction:column;gap:2px;justify-content:center;align-items:center;}
.stat.right{justify-content:flex-end;gap:8px;}
.hpbar{width:clamp(70px,22vw,150px);height:14px;border-radius:8px;
  background:rgba(255,255,255,.09);border:1px solid rgba(140,180,255,.25);overflow:hidden;}
#hpFill{height:100%;width:100%;border-radius:8px;
  background:linear-gradient(90deg,#ff4d6d,#ff9f45,#4cff9d);transition:width .18s ease;}
#hpText{font-size:13px;font-weight:700;color:#cfe3ff;min-width:28px;}
#lvlText{font-size:14px;font-weight:800;letter-spacing:.5px;color:#dff0ff;
  text-shadow:0 0 10px rgba(76,214,255,.6);}
#progText{font-size:11px;color:#8fb6d8;font-weight:600;letter-spacing:.5px;}
.coins{font-size:13px;font-weight:800;color:#ffd86b;text-shadow:0 0 10px rgba(255,200,60,.5);white-space:nowrap;}
.energy-hud{font-size:13px;font-weight:800;color:#7ee0ff;text-shadow:0 0 10px rgba(120,224,255,.5);white-space:nowrap;}
.icon-btn{pointer-events:auto;width:40px;height:40px;border-radius:12px;
  border:1px solid rgba(140,180,255,.3);background:rgba(18,26,48,.75);
  color:#cfe3ff;font-size:15px;font-weight:800;cursor:pointer;
  -webkit-appearance:none;appearance:none;}

#btnDash{position:fixed;z-index:11;
  right:calc(18px + env(safe-area-inset-right));
  bottom:calc(132px + env(safe-area-inset-bottom));
  width:66px;height:66px;border-radius:50%;font-size:26px;
  border:2px solid rgba(120,220,255,.55);
  background:radial-gradient(circle at 35% 30%,rgba(76,214,255,.4),rgba(10,20,40,.85));
  color:#bff0ff;cursor:pointer;touch-action:none;
  box-shadow:0 0 24px rgba(76,214,255,.35);
  -webkit-appearance:none;appearance:none;padding:0;line-height:1;}
#btnDash:active{transform:scale(.9);}
#btnDash.cool{opacity:.35;}

#btnFire{position:fixed;z-index:11;
  right:calc(18px + env(safe-area-inset-right));
  bottom:calc(24px + env(safe-area-inset-bottom));
  width:92px;height:92px;border-radius:50%;font-size:38px;
  border:2px solid rgba(255,140,80,.65);
  background:radial-gradient(circle at 35% 30%,rgba(255,140,80,.45),rgba(40,15,10,.85));
  color:#ffd0b0;cursor:pointer;touch-action:none;
  box-shadow:0 0 28px rgba(255,140,80,.4);
  -webkit-appearance:none;appearance:none;padding:0;line-height:1;}
#btnFire.pressed,#btnFire:active{transform:scale(.9);
  background:radial-gradient(circle at 35% 30%,rgba(255,190,130,.65),rgba(60,25,15,.9));
  box-shadow:0 0 40px rgba(255,160,80,.75);}

#banner{position:fixed;left:50%;top:38%;transform:translate(-50%,-50%) scale(.7);
  z-index:20;font-size:clamp(20px,6vw,40px);font-weight:900;letter-spacing:2px;
  color:#fff;text-shadow:0 0 24px rgba(76,214,255,.9),0 0 60px rgba(76,214,255,.5);
  opacity:0;pointer-events:none;transition:opacity .25s,transform .25s;
  text-align:center;white-space:nowrap;}
#banner.show{opacity:1;transform:translate(-50%,-50%) scale(1);}
#toasts{position:fixed;left:50%;bottom:calc(120px + env(safe-area-inset-bottom));
  transform:translateX(-50%);z-index:30;display:flex;flex-direction:column;gap:8px;
  align-items:center;pointer-events:none;width:92%;}
.toast{background:rgba(16,24,46,.94);border:1px solid rgba(255,210,90,.5);
  color:#ffe9a8;padding:10px 16px;border-radius:12px;font-size:13px;font-weight:700;
  box-shadow:0 6px 24px rgba(0,0,0,.5);animation:tin .3s ease;text-align:center;}
.toast.out{opacity:0;transform:translateY(-8px);transition:.4s;}
@keyframes tin{from{opacity:0;transform:translateY(14px);}to{opacity:1;transform:none;}}

#overlay{position:fixed;inset:0;z-index:40;
  display:flex;
  background:radial-gradient(circle at 50% 40%,rgba(12,20,44,.9),rgba(3,4,10,.97));
  overflow-y:auto;overflow-x:hidden;overscroll-behavior:contain;padding:0;}
#overlay.hidden{display:none !important;}

.screen{width:100%;max-width:520px;margin:auto;flex-shrink:0;
  display:flex;flex-direction:column;align-items:center;gap:12px;
  padding:calc(8px + env(safe-area-inset-top)) 16px calc(32px + env(safe-area-inset-bottom));}
.screen.hidden{display:none !important;}

.title{font-size:clamp(34px,11vw,58px);font-weight:900;letter-spacing:3px;line-height:1;
  text-align:center;color:#fff;
  text-shadow:0 0 14px rgba(76,214,255,.9),0 0 50px rgba(76,214,255,.45);}
.title span{display:block;font-size:.62em;letter-spacing:9px;color:#b45cff;
  text-shadow:0 0 18px rgba(180,92,255,.9);margin-top:4px;}
h2{font-size:22px;font-weight:900;letter-spacing:2px;color:#eaf4ff;
  text-shadow:0 0 16px rgba(76,214,255,.55);}
.dead{color:#ff6b8a;text-shadow:0 0 22px rgba(255,60,100,.7);}
.subtitle{font-size:12.5px;color:#8fb6d8;text-align:center;max-width:340px;line-height:1.65;font-weight:600;}

.btn{width:100%;max-width:330px;padding:15px 20px;border-radius:14px;cursor:pointer;
  font-size:15px;font-weight:800;letter-spacing:1.4px;color:#dcecff;
  border:1px solid rgba(120,170,255,.3);
  background:linear-gradient(180deg,rgba(30,44,80,.9),rgba(14,22,44,.9));
  transition:transform .09s;position:relative;
  -webkit-appearance:none;appearance:none;}
.btn:active{transform:scale(.97);}
.btn.primary{border-color:rgba(76,214,255,.65);color:#fff;
  background:linear-gradient(180deg,rgba(40,110,180,.95),rgba(20,50,100,.95));
  box-shadow:0 0 26px rgba(76,214,255,.35);}
.btn.danger{border-color:rgba(255,90,120,.5);color:#ffd3dc;
  background:linear-gradient(180deg,rgba(90,24,44,.9),rgba(40,10,22,.9));}
.btn.gold{border-color:rgba(255,200,80,.6);color:#fff5d0;
  background:linear-gradient(180deg,rgba(140,90,20,.95),rgba(70,40,10,.95));
  box-shadow:0 0 22px rgba(255,180,40,.35);}
.btn.cyan{border-color:rgba(120,224,255,.6);color:#eafcff;
  background:linear-gradient(180deg,rgba(30,110,160,.95),rgba(10,50,90,.95));
  box-shadow:0 0 22px rgba(120,224,255,.35);}
.btn.green{border-color:rgba(76,255,157,.6);color:#eaffef;
  background:linear-gradient(180deg,rgba(20,120,70,.95),rgba(10,60,35,.95));
  box-shadow:0 0 22px rgba(76,255,157,.35);}
.btn.small{width:auto;padding:9px 14px;font-size:14px;max-width:none;border-radius:11px;}
.badge{display:inline-block;background:#ff3d6e;color:#fff;border-radius:8px;
  font-size:11px;padding:1px 7px;margin-left:6px;vertical-align:middle;
  animation:pulse 1.4s infinite;}
.badge.hidden{display:none !important;}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
.hint{font-size:11.5px;color:#7d97b8;text-align:center;line-height:1.7;max-width:340px;margin-top:6px;}

.res-row{display:flex;gap:18px;align-items:center;justify-content:center;flex-wrap:wrap;}
.coins-big{font-size:19px;font-weight:900;color:#ffd86b;
  text-shadow:0 0 14px rgba(255,200,60,.55);}
.energy-big{font-size:19px;font-weight:900;color:#7ee0ff;
  text-shadow:0 0 14px rgba(120,224,255,.55);}

.head{position:sticky;top:0;z-index:6;
  display:flex;align-items:center;gap:10px;width:100%;
  padding:10px 4px;margin:0 -4px;
  background:linear-gradient(180deg,rgba(6,10,20,.98) 65%,rgba(6,10,20,0));}
.head h2{flex:1;text-align:center;}
.head .btn.small{font-size:18px;padding:10px 16px;line-height:1;min-width:46px;}

.menu-footer{display:flex;align-items:center;gap:10px;margin-top:4px;
  padding:8px 14px;border-radius:12px;background:rgba(14,20,40,.7);
  border:1px solid rgba(120,160,255,.2);font-size:13px;color:#a8c4e4;font-weight:700;
  flex-wrap:wrap;justify-content:center;}
.menu-footer .btn.small{padding:7px 12px;font-size:12px;}

.auth-input{width:100%;max-width:330px;padding:14px 18px;border-radius:12px;
  background:rgba(14,20,40,.9);border:1px solid rgba(120,170,255,.3);
  color:#e8f0ff;font-size:16px;font-weight:600;outline:none;
  -webkit-appearance:none;appearance:none;font-family:inherit;
  transition:border-color .15s,box-shadow .15s;}
.auth-input:focus{border-color:rgba(76,214,255,.7);box-shadow:0 0 18px rgba(76,214,255,.28);}
.auth-input::placeholder{color:#5f7694;font-weight:600;}
.auth-err{color:#ff6b8a;font-size:13px;font-weight:700;min-height:18px;text-align:center;
  max-width:330px;line-height:1.4;}

.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(136px,1fr));
  gap:11px;width:100%;margin-top:6px;}
.card{background:rgba(14,20,40,.85);border:1.5px solid rgba(120,160,255,.2);
  border-radius:15px;padding:13px 10px;display:flex;flex-direction:column;
  align-items:center;gap:7px;cursor:pointer;transition:transform .09s;
  -webkit-appearance:none;appearance:none;color:inherit;font:inherit;}
.card:active{transform:scale(.96);}
.card.equipped{border-color:#4cff9d;box-shadow:0 0 20px rgba(76,255,157,.4);}
.dot{width:52px;height:52px;border-radius:50%;background:var(--c);
  box-shadow:0 0 18px var(--c),inset 0 -6px 14px rgba(0,0,0,.45);
  border:2px solid rgba(255,255,255,.35);}
.dot.rainbow{animation:rainbow 3s linear infinite;}
@keyframes rainbow{0%{filter:hue-rotate(0)}100%{filter:hue-rotate(360deg)}}
.cname{font-size:13px;font-weight:800;color:#e8f2ff;text-align:center;}
.crar{font-size:10px;font-weight:800;letter-spacing:1px;text-transform:uppercase;}
.cbtn{font-size:12px;font-weight:800;padding:6px 10px;border-radius:9px;width:100%;
  text-align:center;background:rgba(255,255,255,.07);color:#cfe3ff;}
.card.equipped .cbtn{background:rgba(76,255,157,.18);color:#8dffc0;}
.r-common .crar{color:#9aa4b2;} .r-common .dot{--c:#9aa4b2;}
.r-uncommon .crar{color:#4cd97b;} .r-rare .crar{color:#3fa9ff;}
.r-epic .crar{color:#b45cff;} .r-legend .crar{color:#ffb020;}
.r-legend{border-color:rgba(255,176,32,.5);box-shadow:0 0 18px rgba(255,176,32,.22);}

.timer{font-size:12.5px;color:#8fb6d8;font-weight:600;}
.timer b{color:#4cd6ff;}
.qlist{display:flex;flex-direction:column;gap:11px;width:100%;margin-top:6px;}
.quest{background:rgba(14,20,40,.85);border:1.5px solid rgba(120,160,255,.2);
  border-radius:15px;padding:13px 15px;display:flex;gap:12px;align-items:center;}
.quest.done{border-color:rgba(76,255,157,.55);background:rgba(16,42,32,.8);}
.qicon{font-size:24px;width:34px;text-align:center;}
.qbody{flex:1;min-width:0;}
.qname{font-size:13.5px;font-weight:800;color:#e8f2ff;margin-bottom:6px;}
.qbar{height:8px;border-radius:6px;background:rgba(255,255,255,.09);overflow:hidden;}
.qfill{height:100%;border-radius:6px;background:linear-gradient(90deg,#4cd6ff,#b45cff);transition:width .3s;}
.quest.done .qfill{background:linear-gradient(90deg,#4cff9d,#9dff4c);}
.qprog{font-size:11px;color:#8fb6d8;margin-top:5px;font-weight:600;}
.qrew{font-size:12px;font-weight:900;color:#ffd86b;white-space:nowrap;text-align:right;line-height:1.5;}
.qrew .e{color:#7ee0ff;}
.stats-out{font-size:15px;color:#b9d4ee;text-align:center;line-height:1.9;font-weight:600;}
.stats-out b{color:#4cd6ff;}

.upg-intro{font-size:12px;color:#8fb6d8;text-align:center;line-height:1.6;max-width:360px;font-weight:600;}
.upg-intro .warn{color:#ffb03a;font-weight:800;}
.upg-list{display:flex;flex-direction:column;gap:11px;width:100%;margin-top:6px;}
.upg-card{background:rgba(14,20,40,.85);border:1.5px solid rgba(120,160,255,.2);
  border-radius:15px;padding:13px 15px;display:flex;gap:12px;align-items:center;width:100%;}
.upg-card.maxed{border-color:rgba(76,255,157,.5);background:rgba(16,42,32,.75);}
.upg-card.can-buy{border-color:rgba(120,224,255,.5);box-shadow:0 0 14px rgba(120,224,255,.18);}
.upg-card.boss-immune{border-color:rgba(255,176,32,.35);}
.upg-icon{font-size:26px;width:38px;text-align:center;}
.upg-body{flex:1;min-width:0;}
.upg-name{font-size:14px;font-weight:800;color:#e8f2ff;margin-bottom:3px;}
.upg-lvl{font-size:11px;color:#8fb6d8;font-weight:700;}
.upg-val{font-size:11.5px;color:#b9d4ee;font-weight:600;margin-top:4px;}
.upg-val b{color:#7ee0ff;}
.upg-note{font-size:10.5px;color:#ffb03a;font-weight:700;margin-top:3px;}
.upg-btn{padding:10px 12px;border-radius:10px;
  border:1px solid rgba(76,214,255,.6);
  background:linear-gradient(180deg,rgba(40,110,180,.9),rgba(20,50,100,.9));
  color:#fff;font-weight:800;font-size:12.5px;cursor:pointer;
  -webkit-appearance:none;appearance:none;font-family:inherit;
  min-width:88px;text-align:center;}
.upg-btn:disabled{opacity:.4;border-color:rgba(120,160,255,.3);
  background:linear-gradient(180deg,rgba(30,44,80,.7),rgba(14,22,44,.7));cursor:default;}

#fatal{position:fixed;inset:0;z-index:9999;background:#1a0000;
  color:#ffb0b0;padding:20px;font-family:ui-monospace,Menlo,monospace;
  font-size:13px;line-height:1.5;overflow:auto;white-space:pre-wrap;display:none;}
#fatal b{color:#ff5a6e;font-size:16px;display:block;margin-bottom:12px;}
</style>
</head>
<body>

<canvas id="cv"></canvas>

<div id="hud" class="hidden">
  <div class="hud-row">
    <div class="stat">
      <div class="hpbar"><div id="hpFill"></div></div>
      <span id="hpText">120</span>
    </div>
    <div class="stat center">
      <div id="lvlText">Уровень 1</div>
      <div id="progText">0/10</div>
    </div>
    <div class="stat right">
      <span class="coins">🪙 <span id="coinText">0</span></span>
      <span class="energy-hud">🔋 <span id="energyText">0</span></span>
      <button id="btnMute" class="icon-btn" type="button" title="Звук">🔊</button>
      <button id="btnPause" class="icon-btn" type="button">❚❚</button>
    </div>
  </div>
</div>

<button id="btnDash" class="hidden" type="button">⚡</button>
<button id="btnFire" class="hidden" type="button">🔥</button>
<div id="banner"></div>
<div id="toasts"></div>
<div id="fatal"></div>

<div id="overlay">

  <div id="scr-auth" class="screen">
    <h1 class="title">НЕОН<span>АРЕНА</span></h1>
    <div class="subtitle">Войди в аккаунт, чтобы твой прогресс сохранялся на устройстве</div>
    <button class="btn primary" type="button" onclick="showLogin()">ВХОД</button>
    <button class="btn" type="button" onclick="showRegister()">РЕГИСТРАЦИЯ</button>
    <button class="btn gold" type="button" onclick="loginAsGuest()">ИГРАТЬ КАК ГОСТЬ</button>
    <button class="btn small" type="button" id="btnMuteAuth" style="margin-top:4px">🔊 Звук вкл</button>
    <div class="hint">Гостевой прогресс сохраняется отдельно.<br>Зарегистрируйся, чтобы не потерять монеты и скины.</div>
  </div>

  <div id="scr-login" class="screen hidden">
    <div class="head">
      <button class="btn small" type="button" onclick="showAuth()">←</button>
      <h2>ВХОД</h2>
      <div style="width:46px"></div>
    </div>
    <input id="loginUser" class="auth-input" type="text" placeholder="Имя игрока" maxlength="16" autocomplete="username" enterkeyhint="next">
    <input id="loginPass" class="auth-input" type="password" placeholder="Пароль" maxlength="32" autocomplete="current-password" enterkeyhint="go">
    <button class="btn primary" type="button" onclick="doLogin()">ВОЙТИ</button>
    <div id="loginErr" class="auth-err"></div>
  </div>

  <div id="scr-register" class="screen hidden">
    <div class="head">
      <button class="btn small" type="button" onclick="showAuth()">←</button>
      <h2>РЕГИСТРАЦИЯ</h2>
      <div style="width:46px"></div>
    </div>
    <input id="regUser" class="auth-input" type="text" placeholder="Имя игрока (3–16)" maxlength="16" autocomplete="username" enterkeyhint="next">
    <input id="regPass" class="auth-input" type="password" placeholder="Пароль (4+)" maxlength="32" autocomplete="new-password" enterkeyhint="next">
    <input id="regPass2" class="auth-input" type="password" placeholder="Повтори пароль" maxlength="32" autocomplete="new-password" enterkeyhint="go">
    <button class="btn primary" type="button" onclick="doRegister()">СОЗДАТЬ АККАУНТ</button>
    <div id="regErr" class="auth-err"></div>
  </div>

  <div id="scr-menu" class="screen hidden">
    <h1 class="title">НЕОН<span>АРЕНА</span></h1>
    <div class="res-row">
      <span class="coins-big">🪙 <span id="menuCoins">0</span></span>
      <span class="energy-big">🔋 <span id="menuEnergy">0</span></span>
    </div>
    <button class="btn primary" type="button" id="btnPlay" onclick="continueGame()">ИГРАТЬ</button>
    <button class="btn" type="button" id="btnRestart" onclick="startGame(1)">С НАЧАЛА</button>
    <button class="btn cyan" type="button" onclick="openUpgrades()">ПРОКАЧКА</button>
    <button class="btn" type="button" onclick="openShop()">МАГАЗИН СКИНОВ</button>
    <button class="btn" type="button" onclick="openQuests()">КВЕСТЫ <span id="questBadge" class="badge hidden">!</span></button>
    <div class="menu-footer">
      <span id="userBadge">👤 Гость</span>
      <button class="btn small" type="button" id="btnMuteMenu">🔊</button>
      <button class="btn small" type="button" onclick="logout()">ВЫЙТИ</button>
    </div>
    <div class="hint">📱 СТИК слева — движение · 🔥 — огонь · ⚡ — рывок<br>
    💻 WASD — движение · ЛКМ / ПРОБЕЛ — огонь · SHIFT — рывок · ESC — пауза<br>
    💚 Аптечки на карте восстанавливают здоровье</div>
  </div>

  <div id="scr-upgrades" class="screen hidden">
    <div class="head">
      <button class="btn small" type="button" onclick="closeSub()">←</button>
      <h2>ПРОКАЧКА</h2>
      <div class="res-row"><span class="energy-big">🔋 <span id="upgEnergy">0</span></span></div>
    </div>
    <div class="upg-intro">Энергию 🔋 дают за квесты (10–15 за каждый).<br>Улучшения применяются в следующем бою.<br>
      <span class="warn">⚠ Урон пули не действует на боссов.</span></div>
    <div id="upgradeList" class="upg-list"></div>
  </div>

  <div id="scr-shop" class="screen hidden">
    <div class="head">
      <button class="btn small" type="button" onclick="closeSub()">←</button>
      <h2>МАГАЗИН</h2>
      <div class="res-row"><span class="coins-big">🪙 <span id="shopCoins">0</span></span></div>
    </div>
    <div id="shopGrid" class="grid"></div>
  </div>

  <div id="scr-quests" class="screen hidden">
    <div class="head">
      <button class="btn small" type="button" onclick="closeSub()">←</button>
      <h2>КВЕСТЫ</h2>
      <div class="res-row">
        <span class="coins-big">🪙 <span id="questCoins">0</span></span>
        <span class="energy-big">🔋 <span id="questEnergy">0</span></span>
      </div>
    </div>
    <div class="timer">Новые задания через <b id="questTimer">15:00</b></div>
    <div id="questList" class="qlist"></div>
    <div class="hint">Монеты 🪙 и энергия 🔋 падают только за выполненные квесты.<br>Задания обновляются каждые 15 минут.</div>
  </div>

  <div id="scr-pause" class="screen hidden">
    <h2>ПАУЗА</h2>
    <button class="btn primary" type="button" onclick="togglePause()">ПРОДОЛЖИТЬ</button>
    <button class="btn" type="button" onclick="openQuestsFromPause()">КВЕСТЫ</button>
    <button class="btn danger" type="button" onclick="goMenu()">В ГЛАВНОЕ МЕНЮ</button>
  </div>

  <div id="scr-over" class="screen hidden">
    <h2 class="dead">ВЫ ПОГИБЛИ</h2>
    <div class="stats-out" id="overStats"></div>
    <button class="btn primary" type="button" id="btnRetryLevel">С ЭТОГО УРОВНЯ</button>
    <button class="btn gold" type="button" onclick="startGame(1)">С НАЧАЛА</button>
    <button class="btn" type="button" onclick="goMenu()">В ГЛАВНОЕ МЕНЮ</button>
  </div>

</div>

<script>
window.addEventListener('error', function(e){
  showFatal((e.error && e.error.stack) ? e.error.stack : (e.message || 'Unknown error') + ' @ ' + (e.filename||'') + ':' + (e.lineno||''));
});
window.addEventListener('unhandledrejection', function(e){
  showFatal('Promise rejected: ' + (e.reason && e.reason.stack ? e.reason.stack : e.reason));
});
function showFatal(msg){
  try{
    var el = document.getElementById('fatal');
    if(el){
      el.style.display = 'block';
      el.innerHTML = '<b>⚠ ОШИБКА</b>' + String(msg).replace(/</g,'&lt;');
    }
  }catch(e){}
}

'use strict';
const TAU = Math.PI * 2;
const rand = (a,b) => a + Math.random()*(b-a);
const randInt = (a,b) => Math.floor(rand(a,b+1));
const clamp = (v,a,b) => v<a?a:(v>b?b:v);
const pick = a => a[Math.floor(Math.random()*a.length)];
function shuffle(a){ for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));const t=a[i];a[i]=a[j];a[j]=t;} return a; }
function hexToRgb(hex){
  hex = hex.replace('#','');
  if(hex.length===3) hex = hex.split('').map(c=>c+c).join('');
  const n = parseInt(hex,16);
  return {r:(n>>16)&255, g:(n>>8)&255, b:n&255};
}
function hslToRgb(h,s,l){
  let r,g,b;
  if(s===0){ r=g=b=l; } else {
    const hue=(p,q,t)=>{ if(t<0)t+=1; if(t>1)t-=1;
      if(t<1/6) return p+(q-p)*6*t; if(t<1/2) return q;
      if(t<2/3) return p+(q-p)*(2/3-t)*6; return p; };
    const q = l<0.5 ? l*(1+s) : l+s-l*s; const p = 2*l-q;
    r=hue(p,q,h+1/3); g=hue(p,q,h); b=hue(p,q,h-1/3);
  }
  return {r:Math.round(r*255), g:Math.round(g*255), b:Math.round(b*255)};
}
const rgba = (c,a) => 'rgba('+(c.r|0)+','+(c.g|0)+','+(c.b|0)+','+a+')';
function circleRect(px,py,pr, rx,ry,rw,rh){
  const cx = clamp(px, rx, rx+rw);
  const cy = clamp(py, ry, ry+rh);
  const dx = px-cx, dy = py-cy;
  return dx*dx + dy*dy < pr*pr;
}
function pointLineDist(px,py, x1,y1, x2,y2){
  const dx = x2-x1, dy = y2-y1;
  const len2 = dx*dx + dy*dy || 1;
  const t = clamp(((px-x1)*dx + (py-y1)*dy) / len2, 0, 1);
  const cx = x1 + t*dx, cy = y1 + t*dy;
  return Math.hypot(px-cx, py-cy);
}

/* ==========================================================
   ЗВУК
   ========================================================== */
const MUTED_KEY = 'neonArena_muted';
let muted = false;
try{ muted = localStorage.getItem(MUTED_KEY) === '1'; }catch(e){}

const Sound = {
  ctx:null, master:null, musicGain:null, sfxGain:null,
  musicTimer:null, musicType:null, musicSeq:null, musicStep:0,
  musicStepDur:0, nextNoteTime:0, musicVol:0.20, muted:muted,

  init(){
    if(this.ctx) return;
    try{
      const AC = window.AudioContext || window.webkitAudioContext;
      if(!AC) return;
      this.ctx = new AC();
      this.master = this.ctx.createGain();
      this.master.gain.value = this.muted ? 0 : 0.75;
      this.master.connect(this.ctx.destination);
      this.musicGain = this.ctx.createGain();
      this.musicGain.gain.value = this.musicVol;
      this.musicGain.connect(this.master);
      this.sfxGain = this.ctx.createGain();
      this.sfxGain.gain.value = 0.35;
      this.sfxGain.connect(this.master);
    }catch(e){ this.ctx = null; }
  },
  resume(){ if(this.ctx && this.ctx.state === 'suspended') this.ctx.resume(); },
  setMuted(m){
    this.muted = m; muted = m;
    try{ localStorage.setItem(MUTED_KEY, m ? '1' : '0'); }catch(e){}
    if(this.master) this.master.gain.value = m ? 0 : 0.75;
    updateMuteButtons();
  },
  toggleMute(){ this.setMuted(!this.muted); },
  setMusicVolume(v){ this.musicVol = v; if(this.musicGain) this.musicGain.gain.value = v; },

  startMusic(type){
    if(!this.ctx) return;
    if(this.musicType === type) return;
    this.stopMusic();
    this.musicType = type;
    this.musicSeq = type === 'menu' ? MENU_SEQ : GAME_SEQ;
    this.musicStepDur = type === 'menu' ? 0.30 : 0.14;
    this.musicStep = 0;
    this.nextNoteTime = this.ctx.currentTime + 0.08;
    this.musicTimer = setInterval(() => this.musicPlay(), 25);
  },
  stopMusic(){
    if(this.musicTimer){ clearInterval(this.musicTimer); this.musicTimer = null; }
    this.musicType = null;
  },
  musicPlay(){
    if(!this.ctx || !this.musicSeq) return;
    const lookahead = 0.18, now = this.ctx.currentTime;
    let guard = 0;
    while(this.nextNoteTime < now + lookahead && guard++ < 80){
      const idx = this.musicStep % this.musicSeq.length;
      const notes = this.musicSeq[idx];
      if(notes && notes.length){
        for(let i=0;i<notes.length;i++){
          const n = notes[i];
          this.playMusicNote(n[0], n[1], n[2], this.nextNoteTime, n[3]);
        }
      }
      this.musicStep++;
      this.nextNoteTime += this.musicStepDur;
    }
  },
  playMusicNote(freq, dur, vol, when, type){
    if(!this.ctx) return;
    if(type === 'noise'){
      const sr = this.ctx.sampleRate;
      const size = Math.max(1, Math.floor(sr * dur));
      const buf = this.ctx.createBuffer(1, size, sr);
      const d = buf.getChannelData(0);
      for(let i=0;i<size;i++) d[i] = Math.random()*2 - 1;
      const src = this.ctx.createBufferSource(); src.buffer = buf;
      const f = this.ctx.createBiquadFilter();
      f.type = 'highpass'; f.frequency.value = 6500;
      const g = this.ctx.createGain();
      g.gain.setValueAtTime(vol, when);
      g.gain.exponentialRampToValueAtTime(0.001, when + dur);
      src.connect(f); f.connect(g); g.connect(this.musicGain);
      src.start(when); src.stop(when + dur);
      return;
    }
    if(type === 'kick'){
      const o = this.ctx.createOscillator(), g = this.ctx.createGain();
      o.frequency.setValueAtTime(130, when);
      o.frequency.exponentialRampToValueAtTime(38, when + 0.1);
      g.gain.setValueAtTime(vol, when);
      g.gain.exponentialRampToValueAtTime(0.001, when + dur);
      o.connect(g); g.connect(this.musicGain);
      o.start(when); o.stop(when + dur + 0.05);
      return;
    }
    const o = this.ctx.createOscillator(), g = this.ctx.createGain();
    o.type = type || 'triangle';
    o.frequency.value = freq;
    g.gain.setValueAtTime(0, when);
    g.gain.linearRampToValueAtTime(vol, when + 0.012);
    g.gain.exponentialRampToValueAtTime(0.001, when + dur);
    o.connect(g); g.connect(this.musicGain);
    o.start(when); o.stop(when + dur + 0.05);
  },

  sfx(name){
    if(!this.ctx || this.muted) return;
    const t = this.ctx.currentTime;
    if(name === 'shoot'){
      const o = this.ctx.createOscillator(), g = this.ctx.createGain();
      o.type = 'square';
      o.frequency.setValueAtTime(900, t);
      o.frequency.exponentialRampToValueAtTime(280, t + 0.06);
      g.gain.setValueAtTime(0.10, t);
      g.gain.exponentialRampToValueAtTime(0.001, t + 0.07);
      o.connect(g); g.connect(this.sfxGain);
      o.start(t); o.stop(t + 0.09);
    }
    else if(name === 'hit'){ this.noise(t, 0.05, 0.12, 2500); }
    else if(name === 'boss_hit'){ this.noise(t, 0.09, 0.20, 700); this.osc(220, 0.09, 0.12, 'sawtooth', t); }
    else if(name === 'explosion'){ this.noise(t, 0.24, 0.28, 250); this.osc(90, 0.22, 0.16, 'sawtooth', t, 40); }
    else if(name === 'player_hit'){ this.osc(340, 0.22, 0.22, 'sawtooth', t, 90); }
    else if(name === 'coin'){ this.osc(880, 0.09, 0.14, 'triangle', t); this.osc(1320, 0.16, 0.14, 'triangle', t + 0.08); }
    else if(name === 'energy'){ this.osc(660, 0.10, 0.13, 'sine', t); this.osc(990, 0.14, 0.13, 'sine', t + 0.07); this.osc(1480, 0.18, 0.11, 'sine', t + 0.14); }
    else if(name === 'heal'){
      this.osc(660, 0.12, 0.14, 'sine', t);
      this.osc(880, 0.16, 0.13, 'sine', t + 0.07);
      this.osc(1180, 0.22, 0.11, 'sine', t + 0.15);
    }
    else if(name === 'upgrade'){ this.osc(440, 0.14, 0.16, 'triangle', t); this.osc(660, 0.16, 0.16, 'triangle', t + 0.10); this.osc(880, 0.22, 0.16, 'triangle', t + 0.20); }
    else if(name === 'level_up'){ [523, 659, 784, 1047].forEach((f, i) => this.osc(f, 0.35, 0.14, 'triangle', t + i*0.08)); }
    else if(name === 'boss_die'){ [523, 659, 784, 1047, 1319].forEach((f, i) => this.osc(f, 0.5, 0.18, 'triangle', t + i*0.1)); this.noise(t, 0.6, 0.22, 250); }
    else if(name === 'death'){ this.osc(420, 0.4, 0.24, 'sawtooth', t, 80); this.osc(300, 0.55, 0.20, 'sawtooth', t + 0.12, 60); }
    else if(name === 'dash'){ this.noise(t, 0.14, 0.14, 3500); }
    else if(name === 'click'){ this.osc(1200, 0.045, 0.09, 'square', t); }
  },
  osc(freq, dur, vol, type, when, targetFreq){
    const o = this.ctx.createOscillator(), g = this.ctx.createGain();
    o.type = type || 'sine';
    o.frequency.setValueAtTime(freq, when);
    if(targetFreq) o.frequency.exponentialRampToValueAtTime(targetFreq, when + dur);
    g.gain.setValueAtTime(vol, when);
    g.gain.exponentialRampToValueAtTime(0.001, when + dur);
    o.connect(g); g.connect(this.sfxGain);
    o.start(when); o.stop(when + dur + 0.03);
  },
  noise(when, dur, vol, hp){
    const sr = this.ctx.sampleRate;
    const size = Math.max(1, Math.floor(sr * dur));
    const buf = this.ctx.createBuffer(1, size, sr);
    const d = buf.getChannelData(0);
    for(let i=0;i<size;i++) d[i] = Math.random()*2 - 1;
    const src = this.ctx.createBufferSource(); src.buffer = buf;
    const f = this.ctx.createBiquadFilter();
    f.type = 'highpass'; f.frequency.value = hp || 1000;
    const g = this.ctx.createGain();
    g.gain.setValueAtTime(vol, when);
    g.gain.exponentialRampToValueAtTime(0.001, when + dur);
    src.connect(f); f.connect(g); g.connect(this.sfxGain);
    src.start(when); src.stop(when + dur);
  },
};

const MENU_SEQ = (function(){
  const s = []; for(let i=0;i<32;i++) s.push([]);
  s[0].push([220, 2.0, 0.10, 'sine']);   s[0].push([330, 2.0, 0.05, 'sine']);   s[0].push([262, 2.0, 0.05, 'sine']);
  s[8].push([175, 2.0, 0.10, 'sine']);   s[8].push([262, 2.0, 0.05, 'sine']);   s[8].push([220, 2.0, 0.05, 'sine']);
  s[16].push([262, 2.0, 0.10, 'sine']);  s[16].push([330, 2.0, 0.05, 'sine']);  s[16].push([392, 2.0, 0.05, 'sine']);
  s[24].push([196, 2.0, 0.10, 'sine']);  s[24].push([294, 2.0, 0.05, 'sine']);  s[24].push([247, 2.0, 0.05, 'sine']);
  const mel = [[0,440],[2,523],[4,659],[6,523],[8,349],[10,440],[12,523],[14,440],
    [16,523],[18,659],[20,784],[22,659],[24,494],[26,587],[28,784],[30,587]];
  for(let i=0;i<mel.length;i++){ s[mel[i][0]].push([mel[i][1], 0.32, 0.15, 'triangle']); }
  return s;
})();
const GAME_SEQ = (function(){
  const s = []; for(let i=0;i<32;i++) s.push([]);
  const bass = [[0,110],[8,87],[16,131],[24,98]];
  for(let i=0;i<bass.length;i++) s[bass[i][0]].push([bass[i][1], 0.65, 0.20, 'sawtooth']);
  const arp = [[1,220],[2,330],[3,262],[4,330],[5,262],[6,330],[7,440],
    [9,175],[10,262],[11,220],[12,262],[13,220],[14,262],[15,349],
    [17,262],[18,392],[19,330],[20,392],[21,330],[22,392],[23,523],
    [25,196],[26,294],[27,247],[28,294],[29,247],[30,294],[31,392]];
  for(let i=0;i<arp.length;i++) s[arp[i][0]].push([arp[i][1], 0.09, 0.12, 'square']);
  for(let i=0;i<32;i+=2){ s[i].push([0, 0.03, 0.07, 'noise']); }
  [0,8,16,24].forEach(k => s[k].push([60, 0.14, 0.26, 'kick']));
  return s;
})();

function updateMuteButtons(){
  try{
    const ic = Sound.muted ? '🔇' : '🔊';
    const b1 = document.getElementById('btnMute');
    const b2 = document.getElementById('btnMuteMenu');
    const b3 = document.getElementById('btnMuteAuth');
    if(b1) b1.textContent = ic;
    if(b2) b2.textContent = ic;
    if(b3) b3.textContent = ic + (Sound.muted ? ' Звук выкл' : ' Звук вкл');
  }catch(e){}
}

/* ==========================================================
   ПРОКАЧКА
   ========================================================== */
const UPGRADES = [
  {id:'damage',   name:'Урон пули',       icon:'⚔️', maxLevel:20, costBase:10, costStep:8,
    base:14,  perLevel:3, bossNote:true},
  {id:'speed',    name:'Скорость',        icon:'💨', maxLevel:10, costBase:8,  costStep:6,
    base:275, perLevel:15},
  {id:'hp',       name:'Макс. здоровье',  icon:'❤️', maxLevel:15, costBase:12, costStep:10,
    base:120, perLevel:20},
  {id:'fireRate', name:'Скорострельность',icon:'🔥', maxLevel:10, costBase:10, costStep:8,
    base:0.14, perLevel:-0.009, isTime:true},
  {id:'healBonus',name:'Сила аптечек',    icon:'💚', maxLevel:10, costBase:10, costStep:9,
    base:40,  perLevel:8},
];
function getUpgradeLevel(id){
  if(!save.upgrades) return 0;
  const v = save.upgrades[id];
  return (typeof v === 'number') ? v : 0;
}
function getUpgradeValue(id){
  const up = UPGRADES.filter(u => u.id === id)[0];
  if(!up) return 0;
  const lvl = getUpgradeLevel(id);
  let v = up.base + up.perLevel * lvl;
  if(up.isTime) v = Math.max(0.05, v);
  return v;
}
function getUpgradeCost(id){
  const up = UPGRADES.filter(u => u.id === id)[0];
  if(!up) return 0;
  const lvl = getUpgradeLevel(id);
  return up.costBase + lvl * up.costStep;
}

const SKINS = [
  {id:'novice', name:'Новичок',   rarity:'Базовый',    price:0,    color:'#7ec8ff'},
  {id:'steel',  name:'Сталь',     rarity:'Базовый',    price:80,   color:'#b9c4d4'},
  {id:'ember',  name:'Уголёк',    rarity:'Базовый',    price:140,  color:'#ff8a4c'},
  {id:'lime',   name:'Лайм',      rarity:'Необычный',  price:280,  color:'#a6ff4c'},
  {id:'aqua',   name:'Аква',      rarity:'Необычный',  price:340,  color:'#38e8d0'},
  {id:'violet', name:'Фиалка',    rarity:'Необычный',  price:420,  color:'#b06cff'},
  {id:'neon',   name:'Неон',      rarity:'Редкий',     price:760,  color:'#ff2fd0', glow:true},
  {id:'emerald',name:'Изумруд',   rarity:'Редкий',     price:880,  color:'#19ff8a', glow:true},
  {id:'gold',   name:'Золото',    rarity:'Редкий',     price:1000, color:'#ffd23f', glow:true},
  {id:'plasma', name:'Плазма',    rarity:'Эпический',  price:1700, color:'#7b5cff', glow:true, trail:true},
  {id:'cosmos', name:'Космос',    rarity:'Эпический',  price:1950, color:'#4a63d8', glow:true, trail:true},
  {id:'inferno',name:'Инферно',   rarity:'Эпический',  price:2250, color:'#ff3d1f', glow:true, trail:true},
  {id:'rainbow',name:'Радуга',    rarity:'Легендарный',price:3800, color:'#ff0055', glow:true, trail:true, rainbow:true},
  {id:'void',   name:'Пустота',   rarity:'Легендарный',price:4500, color:'#5a1ecc', glow:true, trail:true, dark:true},
  {id:'dragon', name:'Дракон',    rarity:'Легендарный',price:5400, color:'#ffb800', glow:true, trail:true, dragon:true},
];
const RARITY_CLASS = {'Базовый':'r-common','Необычный':'r-uncommon','Редкий':'r-rare','Эпический':'r-epic','Легендарный':'r-legend'};
function skinById(id){
  for(let i=0;i<SKINS.length;i++) if(SKINS[i].id === id) return SKINS[i];
  return SKINS[0];
}

/* ==========================================================
   АККАУНТЫ + СЕЙВ
   ========================================================== */
const ACCOUNTS_KEY = 'neonArena_accounts';
const CURRENT_KEY  = 'neonArena_current';
let accounts = {};
let currentUser = null;

function defaultSave(){
  return {
    coins:0, energy:0,
    owned:['novice'], skin:'novice',
    quests:null, cycleStart:0, best:1,
    lastLevel:1,
    upgrades:{damage:0, speed:0, hp:0, fireRate:0, healBonus:0}
  };
}
let save = defaultSave();
let returnTo = 'menu';
let lastDeathLevel = 1;

function normalizeSave(){
  if(typeof save.coins !== 'number' || isNaN(save.coins)) save.coins = 0;
  if(typeof save.energy !== 'number' || isNaN(save.energy)) save.energy = 0;
  if(!Array.isArray(save.owned) || !save.owned.length) save.owned = ['novice'];
  if(save.owned.indexOf('novice') < 0) save.owned.push('novice');
  if(!save.skin) save.skin = 'novice';
  if(typeof save.lastLevel !== 'number' || save.lastLevel < 1 || isNaN(save.lastLevel)) save.lastLevel = 1;
  if(save.lastLevel > 9999) save.lastLevel = 9999;
  if(!save.upgrades || typeof save.upgrades !== 'object') save.upgrades = {};
  for(let i=0;i<UPGRADES.length;i++){
    const id = UPGRADES[i].id;
    if(typeof save.upgrades[id] !== 'number') save.upgrades[id] = 0;
    save.upgrades[id] = clamp(Math.floor(save.upgrades[id]), 0, UPGRADES[i].maxLevel);
  }
}
function loadAccounts(){
  try{ accounts = JSON.parse(localStorage.getItem(ACCOUNTS_KEY) || '{}'); }catch(e){ accounts = {}; }
  if(!accounts || typeof accounts !== 'object') accounts = {};
}
function saveAccounts(){ try{ localStorage.setItem(ACCOUNTS_KEY, JSON.stringify(accounts)); }catch(e){} }
function hashPass(str){
  let h = 5381;
  for(let i=0;i<str.length;i++) h = ((h<<5)+h) ^ str.charCodeAt(i);
  return (h >>> 0).toString(36) + '_' + str.length;
}
function getSaveKey(){
  if(currentUser === 'guest') return 'neonArena_save_guest';
  if(currentUser) return 'neonArena_save_' + currentUser;
  return 'neonArena_save_default';
}
function setCurrentUser(u){
  currentUser = u;
  try{ if(u) localStorage.setItem(CURRENT_KEY, u); else localStorage.removeItem(CURRENT_KEY); }catch(e){}
}
function loadSave(){
  save = defaultSave();
  try{
    const r = localStorage.getItem(getSaveKey());
    if(r) save = Object.assign(defaultSave(), JSON.parse(r));
  }catch(e){}
  normalizeSave();
}
function writeSave(){ try{ localStorage.setItem(getSaveKey(), JSON.stringify(save)); }catch(e){} }

/* ==========================================================
   КВЕСТЫ
   ========================================================== */
const CYCLE_MS = 15*60*1000;
const QUEST_POOL = [
  {type:'kill',   name:'Зачистка',          icon:'💀', target:25},
  {type:'kill',   name:'Бойня',             icon:'💀', target:60},
  {type:'kill',   name:'Геноцид',           icon:'💀', target:120},
  {type:'kill',   name:'Резня',             icon:'🩸', target:40},
  {type:'kill',   name:'Мясорубка',         icon:'🩸', target:90},
  {type:'kill',   name:'Легион смерти',     icon:'☠️', target:200},

  {type:'level',  name:'Прорыв',            icon:'🏁', target:2},
  {type:'level',  name:'Марафон',           icon:'🏁', target:5},
  {type:'level',  name:'Спринт',            icon:'⚡', target:3},
  {type:'level',  name:'Легенда арены',     icon:'🏆', target:8},

  {type:'boss',   name:'Охотник на боссов', icon:'👑', target:1},
  {type:'boss',   name:'Убийца королей',    icon:'👑', target:3},
  {type:'boss',   name:'Гроза боссов',      icon:'⚔️', target:2},
  {type:'boss',   name:'Палач королей',     icon:'🗡', target:5},

  {type:'survive',name:'Живучесть',         icon:'⏱', target:180},
  {type:'survive',name:'Несокрушимый',      icon:'⏱', target:420},
  {type:'survive',name:'Стойкость',         icon:'🛡', target:300},
  {type:'survive',name:'Бессмертный',       icon:'💎', target:600},

  {type:'heal',   name:'Полевой медик',     icon:'💚', target:3},
  {type:'heal',   name:'Санитар',           icon:'💚', target:8},
  {type:'heal',   name:'Госпиталь',         icon:'🏥', target:15},
];
function genQuests(){
  const pool = QUEST_POOL.slice();
  shuffle(pool);
  const usedTypes = {};
  const out = [];
  for(let i=0;i<pool.length && out.length<10;i++){
    const q = pool[i];
    const cnt = usedTypes[q.type] || 0;
    if(cnt < 2){
      out.push({
        type:q.type, name:q.name, icon:q.icon, target:q.target,
        rewardCoins: randInt(15,25),
        rewardEnergy: randInt(10,15),
        progress:0, done:false
      });
      usedTypes[q.type] = cnt + 1;
    }
  }
  return out;
}
function checkCycle(){
  const now = Date.now();
  if(!save.quests || !save.cycleStart || now - save.cycleStart >= CYCLE_MS){
    save.cycleStart = now;
    save.quests = genQuests();
    writeSave();
    renderQuests();
    updateBadge();
  }
}
function addQuestProgress(type, amount){
  if(!save.quests) return;
  let changed = false;
  for(let i=0;i<save.quests.length;i++){
    const q = save.quests[i];
    if(q.type === type && !q.done){
      q.progress += amount;
      if(q.progress >= q.target){
        q.progress = q.target; q.done = true;
        const c = (typeof q.rewardCoins === 'number') ? q.rewardCoins : 0;
        const en = (typeof q.rewardEnergy === 'number') ? q.rewardEnergy : 0;
        save.coins += c; save.energy += en;
        changed = true;
        Sound.sfx('coin');
        setTimeout(function(){ Sound.sfx('energy'); }, 220);
        toast(q.icon + ' ' + q.name + ' — выполнено! +' + c + ' 🪙 +' + en + ' 🔋');
      }
    }
  }
  if(changed){ writeSave(); updateHud(); updateBadge(); renderQuests(); renderUpgrades(); }
}

/* ==========================================================
   CANVAS
   ========================================================== */
const cv = document.getElementById('cv');
const ctx = cv ? cv.getContext('2d') : null;
let W=0,H=0,DPR=1;
function resize(){
  if(!cv || !ctx) return;
  DPR = Math.min(window.devicePixelRatio||1, 2);
  W = window.innerWidth; H = window.innerHeight;
  cv.width = Math.floor(W*DPR); cv.height = Math.floor(H*DPR);
  cv.style.width = W+'px'; cv.style.height = H+'px';
  ctx.setTransform(DPR,0,0,DPR,0,0);
}
window.addEventListener('resize', resize);
window.addEventListener('orientationchange', function(){ setTimeout(resize,120); });

const G = {
  scene:'auth', level:1, mode:'normal', player:null,
  enemies:[], pb:[], eb:[], parts:[], trail:[],
  beams:[], warnings:[], events:[],
  medkits:[], medkitTimer:8,
  killed:0, quota:0, boss:null, spawnTimer:0, time:0,
  clearTimer:0, shakeT:0, shakeMag:0,
  killsTotal:0, bossesTotal:0, surviveTimer:0, healsTotal:0,
  bossSpawned:false,
};

const $ = id => document.getElementById(id);
const overlay = $('overlay');
const hud = $('hud');
const btnDash = $('btnDash');
const btnFire = $('btnFire');

function showScreen(name){
  if(!overlay) return;
  overlay.classList.remove('hidden');
  const screens = ['scr-auth','scr-login','scr-register','scr-menu','scr-upgrades','scr-shop','scr-quests','scr-pause','scr-over'];
  for(let i=0;i<screens.length;i++){
    const el = $(screens[i]);
    if(el) el.classList.toggle('hidden', screens[i] !== name);
  }
  overlay.scrollTop = 0;
}
function hideOverlay(){ if(overlay) overlay.classList.add('hidden'); }

function toast(msg){
  const el = document.createElement('div');
  el.className = 'toast'; el.textContent = msg;
  const t = $('toasts'); if(t) t.appendChild(el);
  setTimeout(function(){ el.classList.add('out'); setTimeout(function(){ el.remove(); },420); }, 2600);
}
let bannerTO = null;
function banner(txt){
  const b = $('banner'); if(!b) return;
  b.textContent = txt; b.classList.add('show');
  clearTimeout(bannerTO);
  bannerTO = setTimeout(function(){ b.classList.remove('show'); }, 1500);
}

/* ==========================================================
   AUTH UI
   ========================================================== */
function showAuth(){
  const le = $('loginErr'); if(le) le.textContent = '';
  const re = $('regErr'); if(re) re.textContent = '';
  G.scene = 'auth';
  Sound.startMusic('menu');
  showScreen('scr-auth');
}
function showLogin(){ const e = $('loginErr'); if(e) e.textContent = ''; G.scene = 'auth'; showScreen('scr-login'); }
function showRegister(){ const e = $('regErr'); if(e) e.textContent = ''; G.scene = 'auth'; showScreen('scr-register'); }

function loginAsGuest(){
  setCurrentUser('guest');
  loadSave();
  refreshAllUI();
  goMenu();
}
function doLogin(){
  const u = $('loginUser').value.trim();
  const p = $('loginPass').value;
  const err = $('loginErr');
  if(!u || !p){ err.textContent = 'Заполни оба поля'; return; }
  if(!accounts[u]){ err.textContent = 'Игрок не найден'; return; }
  if(accounts[u].pass !== hashPass(p)){ err.textContent = 'Неверный пароль'; return; }
  setCurrentUser(u); loadSave(); refreshAllUI();
  $('loginUser').value = ''; $('loginPass').value = '';
  goMenu();
  toast('С возвращением, ' + u + '!');
}
function doRegister(){
  const u = $('regUser').value.trim();
  const p = $('regPass').value;
  const p2 = $('regPass2').value;
  const err = $('regErr');
  if(u.length < 3 || u.length > 16){ err.textContent = 'Имя: от 3 до 16 символов'; return; }
  if(!/^[a-zA-Zа-яА-Я0-9_]+$/.test(u)){ err.textContent = 'Только буквы, цифры и _'; return; }
  if(p.length < 4){ err.textContent = 'Пароль: минимум 4 символа'; return; }
  if(p !== p2){ err.textContent = 'Пароли не совпадают'; return; }
  if(accounts[u]){ err.textContent = 'Имя уже занято'; return; }
  accounts[u] = { pass: hashPass(p), created: Date.now() };
  saveAccounts();
  setCurrentUser(u);
  try{ localStorage.removeItem('neonArena_save_' + u); }catch(e){}
  loadSave(); refreshAllUI();
  $('regUser').value = ''; $('regPass').value = ''; $('regPass2').value = '';
  goMenu();
  toast('Добро пожаловать, ' + u + '!');
}
function logout(){
  setCurrentUser(null);
  save = defaultSave();
  refreshAllUI();
  showAuth();
}
(function bindAuthKeys(){
  const lu = $('loginUser'), lp = $('loginPass');
  const ru = $('regUser'), rp = $('regPass'), rp2 = $('regPass2');
  if(lu) lu.addEventListener('keydown', function(e){ if(e.key === 'Enter' && lp) lp.focus(); });
  if(lp) lp.addEventListener('keydown', function(e){ if(e.key === 'Enter') doLogin(); });
  if(ru) ru.addEventListener('keydown', function(e){ if(e.key === 'Enter' && rp) rp.focus(); });
  if(rp) rp.addEventListener('keydown', function(e){ if(e.key === 'Enter' && rp2) rp2.focus(); });
  if(rp2) rp2.addEventListener('keydown', function(e){ if(e.key === 'Enter') doRegister(); });
})();

function refreshAllUI(){
  const ub = $('userBadge');
  if(ub) ub.textContent = currentUser === 'guest'
    ? '👤 Гость'
    : (currentUser ? '👤 ' + currentUser : '👤 Не в аккаунте');
  checkCycle();
  updateBadge();
  updateHud();
  updateQuestTimer();
  renderShop();
  renderQuests();
  renderUpgrades();
}

/* ==========================================================
   SHOP
   ========================================================== */
function renderShop(){
  const grid = $('shopGrid'); if(!grid) return;
  grid.innerHTML = '';
  const order = {'Базовый':0,'Необычный':1,'Редкий':2,'Эпический':3,'Легендарный':4};
  const sorted = SKINS.slice().sort(function(a,b){ return order[a.rarity]-order[b.rarity] || a.price-b.price; });
  for(let i=0;i<sorted.length;i++){
    const sk = sorted[i];
    const owned = save.owned.indexOf(sk.id) >= 0;
    const equipped = save.skin === sk.id;
    const card = document.createElement('div');
    card.className = 'card ' + (RARITY_CLASS[sk.rarity]||'') + (equipped?' equipped':'');
    const dot = document.createElement('div');
    dot.className = 'dot' + (sk.rainbow?' rainbow':'');
    dot.style.setProperty('--c', sk.color);
    const nm = document.createElement('div'); nm.className='cname'; nm.textContent = sk.name;
    const rr = document.createElement('div'); rr.className='crar'; rr.textContent = sk.rarity;
    const bt = document.createElement('div'); bt.className='cbtn';
    bt.textContent = equipped ? 'НАДЕТО' : (owned ? 'НАДЕТЬ' : sk.price + ' 🪙');
    card.appendChild(dot); card.appendChild(nm); card.appendChild(rr); card.appendChild(bt);
    card.addEventListener('click', (function(skCopy){
      return function(){
        if(save.skin === skCopy.id) return;
        if(save.owned.indexOf(skCopy.id) >= 0){
          save.skin = skCopy.id; writeSave(); renderShop(); updateHud();
        } else if(save.coins >= skCopy.price){
          save.coins -= skCopy.price; save.owned.push(skCopy.id); save.skin = skCopy.id;
          writeSave(); renderShop(); updateHud(); Sound.sfx('coin');
          toast('Куплен скин: ' + skCopy.name + ' (' + skCopy.rarity + ')');
        } else toast('Недостаточно монет! Выполняй квесты.');
      };
    })(sk));
    grid.appendChild(card);
  }
  const sc = $('shopCoins'); if(sc) sc.textContent = save.coins;
}

/* ==========================================================
   UPGRADES UI
   ========================================================== */
function renderUpgrades(){
  const list = $('upgradeList'); if(!list) return;
  list.innerHTML = '';
  for(let i=0;i<UPGRADES.length;i++){
    const up = UPGRADES[i];
    const lvl = getUpgradeLevel(up.id);
    const maxed = lvl >= up.maxLevel;
    const cost = getUpgradeCost(up.id);
    const canBuy = !maxed && save.energy >= cost;
    let cur = up.base + up.perLevel * lvl;
    let next = up.base + up.perLevel * (lvl + 1);
    if(up.isTime){ cur = Math.max(0.05, cur); next = Math.max(0.05, next); }

    let valText;
    if(up.isTime){
      valText = 'Перезарядка: <b>' + cur.toFixed(3) + 'с</b>';
      if(!maxed) valText += ' → ' + next.toFixed(3) + 'с';
    } else {
      valText = 'Значение: <b>' + cur + '</b>';
      if(!maxed) valText += ' → ' + next;
    }

    let noteHtml = '';
    if(up.bossNote) noteHtml = '<div class="upg-note">⚠ По боссам — урона НЕ добавляет</div>';

    const el = document.createElement('div');
    el.className = 'upg-card' + (maxed ? ' maxed' : '') + (canBuy ? ' can-buy' : '') + (up.bossNote ? ' boss-immune' : '');
    el.innerHTML =
      '<div class="upg-icon">' + up.icon + '</div>' +
      '<div class="upg-body">' +
        '<div class="upg-name">' + up.name + '</div>' +
        '<div class="upg-lvl">Ур. ' + lvl + ' / ' + up.maxLevel + '</div>' +
        '<div class="upg-val">' + valText + '</div>' +
        noteHtml +
      '</div>' +
      '<button class="upg-btn" type="button" ' + (maxed ? 'disabled' : '') + '>' +
        (maxed ? 'МАКС' : '🔋 ' + cost) +
      '</button>';
    if(!maxed){
      (function(id){
        el.querySelector('.upg-btn').addEventListener('click', function(e){
          e.preventDefault(); buyUpgrade(id);
        });
      })(up.id);
    }
    list.appendChild(el);
  }
  const ue = $('upgEnergy'); if(ue) ue.textContent = save.energy;
}
function buyUpgrade(id){
  const up = UPGRADES.filter(u => u.id === id)[0];
  if(!up) return;
  const lvl = getUpgradeLevel(id);
  if(lvl >= up.maxLevel) return;
  const cost = getUpgradeCost(id);
  if(save.energy < cost){ toast('Не хватает энергии! 🔋 Выполняй квесты.'); return; }
  save.energy -= cost;
  save.upgrades[id] = lvl + 1;
  writeSave();
  Sound.sfx('upgrade');
  renderUpgrades();
  updateHud();
  toast('Улучшено: ' + up.name + ' → ур. ' + (lvl + 1));
}

/* ==========================================================
   QUESTS UI
   ========================================================== */
function renderQuests(){
  const list = $('questList'); if(!list) return;
  if(!save.quests || !Array.isArray(save.quests)) save.quests = genQuests();
  list.innerHTML = '';
  for(let i=0;i<save.quests.length;i++){
    const q = save.quests[i];
    const pct = clamp(q.progress / q.target, 0, 1) * 100;
    const el = document.createElement('div');
    el.className = 'quest' + (q.done ? ' done' : '');
    const prog = q.done ? 'Выполнено ✓' : (Math.floor(q.progress) + ' / ' + q.target);
    const rc = (typeof q.rewardCoins === 'number') ? q.rewardCoins : 0;
    const re = (typeof q.rewardEnergy === 'number') ? q.rewardEnergy : 0;
    el.innerHTML =
      '<div class="qicon">'+q.icon+'</div>'+
      '<div class="qbody">'+
        '<div class="qname">'+q.name+'</div>'+
        '<div class="qbar"><div class="qfill" style="width:'+pct+'%"></div></div>'+
        '<div class="qprog">'+prog+'</div>'+
      '</div>'+
      '<div class="qrew">+'+rc+' 🪙<br><span class="e">+'+re+' 🔋</span></div>';
    list.appendChild(el);
  }
  const qc = $('questCoins'); if(qc) qc.textContent = save.coins;
  const qe = $('questEnergy'); if(qe) qe.textContent = save.energy;
}
function updateBadge(){
  const el = $('questBadge'); if(!el) return;
  const has = save.quests && save.quests.some(function(q){ return !q.done; });
  el.classList.toggle('hidden', !has);
}
function updateQuestTimer(){
  const el = $('questTimer'); if(!el) return;
  const left = Math.max(0, CYCLE_MS - (Date.now() - save.cycleStart));
  const m = Math.floor(left/60000), s = Math.floor((left%60000)/1000);
  el.textContent = String(m).padStart(2,'0') + ':' + String(s).padStart(2,'0');
}

function updateHud(){
  const ct = $('coinText'); if(ct) ct.textContent = save.coins;
  const mc = $('menuCoins'); if(mc) mc.textContent = save.coins;
  const et = $('energyText'); if(et) et.textContent = save.energy;
  const me = $('menuEnergy'); if(me) me.textContent = save.energy;
  if(G.player){
    const p = G.player;
    const hf = $('hpFill'); if(hf) hf.style.width = clamp(p.hp/p.maxHp,0,1)*100 + '%';
    const ht = $('hpText'); if(ht) ht.textContent = Math.max(0, Math.ceil(p.hp));
  }
  const lt = $('lvlText'); if(lt) lt.textContent = 'Уровень ' + G.level;
  const pt = $('progText'); if(pt) pt.textContent = G.mode === 'boss' ? '☠ БОСС' : (G.killed + '/' + G.quota);
}

/* ==========================================================
   INPUT
   ========================================================== */
const isTouchDevice = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
let inputMode = isTouchDevice ? 'touch' : 'mouse';
let firing = false;
let mouseX = (window.innerWidth||400)/2, mouseY = (window.innerHeight||600)/2;

function ensureAudio(){
  if(!Sound.ctx) Sound.init();
  Sound.resume();
  if(Sound.ctx && !Sound.musicType){
    if(G.scene === 'playing' || G.scene === 'paused') Sound.startMusic('game');
    else Sound.startMusic('menu');
  }
}
document.addEventListener('touchstart', ensureAudio);
document.addEventListener('mousedown', ensureAudio);

const keys = {};
addEventListener('keydown', function(e){
  const tag = e.target && e.target.tagName;
  if(tag === 'INPUT' || tag === 'TEXTAREA') return;
  const k = e.key.toLowerCase();
  keys[k] = true;
  if(k === 'escape' || k === 'p'){ togglePause(); }
  if(k === ' '){ e.preventDefault(); firing = true; }
  if(k === 'shift' || k === 'e' || k === 'у'){ doDash(); }
});
addEventListener('keyup', function(e){
  const k = e.key.toLowerCase();
  keys[k] = false;
  if(k === ' ') firing = false;
});

const joy = {active:false, id:null, ox:0, oy:0, x:0, y:0, dx:0, dy:0};
const JOY_R = 58;

function onTouchStart(e){
  if(G.scene !== 'playing') return;
  inputMode = 'touch'; e.preventDefault();
  for(let i=0;i<e.changedTouches.length;i++){
    const t = e.changedTouches[i];
    if(joy.id === null){
      joy.id = t.identifier; joy.ox = t.clientX; joy.oy = t.clientY;
      joy.x = t.clientX; joy.y = t.clientY;
      joy.active = true; joy.dx = 0; joy.dy = 0;
    }
  }
}
function onTouchMove(e){
  if(joy.id === null) return;
  e.preventDefault();
  for(let i=0;i<e.changedTouches.length;i++){
    const t = e.changedTouches[i];
    if(t.identifier === joy.id){
      joy.x = t.clientX; joy.y = t.clientY;
      let dx = joy.x - joy.ox, dy = joy.y - joy.oy;
      const d = Math.sqrt(dx*dx+dy*dy);
      if(d > JOY_R){
        joy.ox += (dx/d)*(d-JOY_R); joy.oy += (dy/d)*(d-JOY_R);
        dx = joy.x - joy.ox; dy = joy.y - joy.oy;
      }
      const dd = Math.sqrt(dx*dx+dy*dy) || 1;
      const mag = Math.min(dd / JOY_R, 1);
      joy.dx = (dx/dd)*mag; joy.dy = (dy/dd)*mag;
    }
  }
}
function onTouchEnd(e){
  for(let i=0;i<e.changedTouches.length;i++){
    const t = e.changedTouches[i];
    if(t.identifier === joy.id){ joy.id = null; joy.active = false; joy.dx = 0; joy.dy = 0; }
  }
}
if(cv){
  cv.addEventListener('touchstart', onTouchStart, {passive:false});
  cv.addEventListener('touchmove', onTouchMove, {passive:false});
  cv.addEventListener('touchend', onTouchEnd, {passive:false});
  cv.addEventListener('touchcancel', onTouchEnd, {passive:false});
}

addEventListener('mousemove', function(e){
  mouseX = e.clientX; mouseY = e.clientY;
  if(!isTouchDevice) inputMode = 'mouse';
});
addEventListener('mousedown', function(e){
  if(e.button !== 0) return;
  if(G.scene !== 'playing') return;
  const t = e.target;
  if(t && t.closest && (t.closest('button') || t.closest('input'))) return;
  if(isTouchDevice && inputMode === 'touch') return;
  firing = true;
});
addEventListener('mouseup', function(e){ if(e.button === 0) firing = false; });
if(cv) cv.addEventListener('contextmenu', function(e){ e.preventDefault(); });

function fireDown(e){ e.preventDefault(); firing = true; if(btnFire) btnFire.classList.add('pressed'); }
function fireUp(e){ if(e) e.preventDefault(); firing = false; if(btnFire) btnFire.classList.remove('pressed'); }
if(btnFire){
  btnFire.addEventListener('touchstart', fireDown, {passive:false});
  btnFire.addEventListener('touchend', fireUp, {passive:false});
  btnFire.addEventListener('touchcancel', fireUp, {passive:false});
  btnFire.addEventListener('mousedown', fireDown);
}
addEventListener('mouseup', fireUp);

/* ==========================================================
   PLAYER
   ========================================================== */
function makePlayer(){
  const dmgUpgraded = getUpgradeValue('damage');
  const speed = getUpgradeValue('speed');
  const hpMax = getUpgradeValue('hp');
  const fRate = getUpgradeValue('fireRate');
  return {
    x: W/2, y: H/2, r: 14,
    hp: hpMax, maxHp: hpMax,
    speed: speed,
    damage: dmgUpgraded,
    baseDamage: 14,
    fireCd: 0, fireRate: fRate, invuln: 1.5,
    dashTime: 0, dashCd: 0, dashX: 1, dashY: 0,
    moveX: 0, moveY: 0, aimX: 1, aimY: 0, hurtFlash: 0,
  };
}

/* ==========================================================
   ENEMIES
   ========================================================== */
function isBossLevel(l){ return l % 3 === 0; }
function enemyStats(type, level){
  const hpS = 1 + (level-1)*0.15;
  const spS = 1 + (level-1)*0.020;
  const dmS = 1 + (level-1)*0.08;
  if(type === 'grunt')   return {r:13, hp:22*hpS,  speed:95*spS,  dmg:10*dmS, color:'#ff5a6e', sides:3, rot:0.9};
  if(type === 'runner')  return {r:10, hp:14*hpS,  speed:178*spS, dmg:7*dmS,  color:'#ffb03a', sides:4, rot:2.4};
  if(type === 'shooter') return {r:13, hp:26*hpS,  speed:70*spS,  dmg:8*dmS,  color:'#4cd6ff', sides:5, rot:0.6};
  return {r:21, hp:88*hpS, speed:52*spS, dmg:18*dmS, color:'#a45cff', sides:6, rot:0.35};
}
function pickEnemyType(level){
  const pool = [{t:'grunt', w:10}];
  if(level >= 3) pool.push({t:'runner', w: 4 + level*0.7});
  if(level >= 5) pool.push({t:'shooter', w: 2.5 + level*0.5});
  if(level >= 7) pool.push({t:'tank', w: 1.2 + level*0.3});
  let total = 0; for(let i=0;i<pool.length;i++) total += pool[i].w;
  let r = Math.random()*total;
  for(let i=0;i<pool.length;i++){ r -= pool[i].w; if(r <= 0) return pool[i].t; }
  return 'grunt';
}
function spawnPos(){
  const m = 70; const side = randInt(0,3);
  if(side === 0) return {x: rand(0,W), y: -m};
  if(side === 1) return {x: W+m, y: rand(0,H)};
  if(side === 2) return {x: rand(0,W), y: H+m};
  return {x: -m, y: rand(0,H)};
}
function spawnEnemy(){
  const type = pickEnemyType(G.level);
  const st = enemyStats(type, G.level);
  const pos = spawnPos();
  G.enemies.push({
    type:type, x: pos.x, y: pos.y, r: st.r,
    hp: st.hp, maxHp: st.hp, speed: st.speed, dmg: st.dmg,
    color: st.color, sides: st.sides, rot: Math.random()*TAU, rotSpd: st.rot,
    atkCd: 0, shootCd: rand(0.6, 1.8), keep: rand(190, 280),
    hitFlash: 0, isBoss:false, dead:false,
  });
}
function bossHpByIndex(bossIndex){
  return Math.round(500 * Math.pow(1.3, bossIndex - 1));
}
function spawnBoss(){
  const bossIndex = Math.floor(G.level / 3);
  const hp = bossHpByIndex(bossIndex);
  const b = {
    type:'boss', isBoss:true, dead:false,
    x: W/2, y: -90, r: 44, hp: hp, maxHp: hp,
    speed: 55 + bossIndex*3, dmg: 18 + bossIndex*3,
    color:'#ff2f6e',
    patternTimer: 1.5,
    strafeDir: Math.random() < .5 ? 1 : -1,
    hitFlash: 0, sides: 8, rot: 0, rotSpd: 0.6, entering: true,
    bossIndex: bossIndex,
  };
  G.enemies.push(b); G.boss = b;
  G.bossSpawned = true;
  banner('⚠ БОСС ' + bossIndex + ' ⚠');
  G.shakeT = 0.5; G.shakeMag = 12;
}

/* ==========================================================
   АПТЕЧКИ
   ========================================================== */
function spawnMedkit(){
  const m = 60;
  let x, y, tries = 0;
  do{
    x = rand(m, W-m);
    y = rand(m, H-m);
    tries++;
  } while(G.player && tries < 20 && Math.hypot(x - G.player.x, y - G.player.y) < 120);

  const healAmount = getUpgradeValue('healBonus');
  G.medkits.push({
    x: x, y: y, r: 14,
    heal: healAmount,
    life: 15,
    maxLife: 15,
    pulse: Math.random() * 2,
  });
}

/* ==========================================================
   ПАТТЕРНЫ БОССА
   ========================================================== */
function runBossPattern(b){
  const phase = b.hp / b.maxHp;
  const rate = phase < 0.35 ? 1.5 : (phase < 0.7 ? 1.25 : 1);
  const pattern = pick(['spiral','bones','blaster','spears','minions']);

  if(pattern === 'spiral'){
    const n = 12 + Math.floor((1-phase)*8);
    const off = Math.random()*TAU;
    const sp = 190 + G.level*4;
    for(let i=0;i<n;i++){
      const a = off + i/n*TAU;
      G.eb.push({x:b.x, y:b.y, vx:Math.cos(a)*sp, vy:Math.sin(a)*sp,
        r:6, dmg:b.dmg*0.5, life:4, color:'#ff5a8a', kind:'normal'});
    }
    G.shakeT = 0.2; G.shakeMag = 6;
    b.patternTimer = 2.1 / rate;
  }
  else if(pattern === 'bones'){
    const spd = 280 + G.level*6;
    const rows = 4 + Math.floor((1-phase)*3);
    const fromLeft = Math.random() < 0.5;
    for(let i=0;i<rows;i++){
      (function(iLocal){
        const t = G.time + 0.15 + iLocal*0.12;
        const y = 90 + (H-180) * (iLocal/rows) + rand(-25,25);
        const fromL = (iLocal % 2 === 0) ? fromLeft : !fromLeft;
        G.events.push({t: t, fn: function(){
          const x = fromL ? -60 : W+60;
          const vx = fromL ? spd : -spd;
          G.eb.push({x:x, y:y, vx:vx, vy:0, r:10, w:70, h:14,
            dmg:b.dmg*0.5, life:4, color:'#ffffff', kind:'bone'});
        }});
      })(i);
    }
    b.patternTimer = 2.3 / rate;
  }
  else if(pattern === 'blaster'){
    const n = 2 + Math.floor((1-phase)*1);
    for(let i=0;i<n;i++){
      (function(iLocal){
        const t = G.time + 0.45 + iLocal*0.6;
        G.events.push({t: t, fn: function(){
          if(!G.player) return;
          const px = G.player.x, py = G.player.y;
          const a = Math.atan2(py - b.y, px - b.x) + rand(-0.12, 0.12);
          G.beams.push({
            x: b.x, y: b.y, angle: a,
            len: Math.max(W,H)*1.5, w: 30,
            phase: 'warn', t: 0.7, tmax: 0.7,
            dmg: b.dmg * 0.7, color: '#7ee0ff', hit: false,
          });
        }});
      })(i);
    }
    b.patternTimer = 2.6 / rate;
  }
  else if(pattern === 'spears'){
    const n = 4 + Math.floor((1-phase)*4);
    const px = G.player ? G.player.x : W/2;
    const py = G.player ? G.player.y : H/2;
    for(let i=0;i<n;i++){
      (function(iLocal){
        const t = G.time + 0.3 + iLocal*0.22;
        const tx = clamp(px + rand(-170, 170), 30, W-30);
        const ty = clamp(py + rand(-100, 100), 30, H-30);
        G.events.push({t: t, fn: function(){
          G.warnings.push({x: tx, y: ty, r: 24, t: 0.6, tmax: 0.6, color:'#ffb03a'});
          G.events.push({t: t + 0.6, fn: function(){
            G.eb.push({x: tx, y: -30, vx: 0, vy: 500,
              r:8, w:12, h:46, dmg:b.dmg*0.55, life:3,
              color:'#ffb03a', kind:'spear'});
          }});
        }});
      })(i);
    }
    b.patternTimer = 2.7 / rate;
  }
  else if(pattern === 'minions'){
    const n = 2 + Math.floor((1-phase)*2);
    for(let i=0;i<n;i++){
      const a = Math.random()*TAU;
      const st = enemyStats('grunt', G.level);
      G.enemies.push({
        type:'grunt', x: b.x + Math.cos(a)*70, y: b.y + Math.sin(a)*70, r: st.r,
        hp: st.hp*0.7, maxHp: st.hp*0.7, speed: st.speed*1.1, dmg: st.dmg,
        color:'#ff5a6e', sides:3, rot:0, rotSpd:1.4, atkCd:0, shootCd:1, keep:200,
        hitFlash:0, isBoss:false, dead:false
      });
    }
    explode(b.x, b.y, '#ff2f6e', 18, 4);
    b.patternTimer = 2.0 / rate;
  }
}

/* ==========================================================
   LEVEL FLOW
   ========================================================== */
function startLevel(){
  G.enemies.length = 0; G.pb.length = 0; G.eb.length = 0; G.parts.length = 0;
  G.beams.length = 0; G.warnings.length = 0; G.events.length = 0;
  G.medkits.length = 0; G.medkitTimer = 8 + rand(0, 5);
  G.boss = null; G.killed = 0;
  G.bossSpawned = false;
  save.lastLevel = G.level;
  writeSave();
  G.mode = isBossLevel(G.level) ? 'boss' : 'normal';
  G.quota = G.mode === 'boss' ? 999 : Math.min(5 + G.level, 20);
  G.spawnTimer = 0.6;
  if(G.mode === 'boss'){
    setTimeout(function(){ if((G.scene==='playing') && !G.boss) spawnBoss(); }, 300);
  }
  updateHud();
}
function continueGame(){
  startGame(save.lastLevel || 1);
}
function startGame(fromLevel){
  G.level = fromLevel || 1;
  G.time = 0; G.killsTotal = 0; G.bossesTotal = 0; G.surviveTimer = 0; G.healsTotal = 0;
  G.player = makePlayer();
  startLevel();
  G.scene = 'playing'; hideOverlay();
  if(hud) hud.classList.remove('hidden');
  if(btnDash) btnDash.classList.remove('hidden');
  if(isTouchDevice && btnFire) btnFire.classList.remove('hidden');
  updateHud();
  if(G.level > 1) banner('УРОВЕНЬ ' + G.level);
  Sound.startMusic('game');
  Sound.setMusicVolume(0.20);
}
function completeLevel(){
  if(G.scene !== 'playing') return;
  G.scene = 'clear'; G.clearTimer = 2.0;
  addQuestProgress('level', 1);
  if(G.mode === 'boss'){ addQuestProgress('boss', 1); G.bossesTotal++; }
  for(let i=0;i<G.enemies.length;i++){
    const e = G.enemies[i];
    explode(e.x, e.y, e.color, e.isBoss?40:12, e.isBoss?6:3);
  }
  G.enemies.length = 0; G.eb.length = 0; G.boss = null;
  G.beams.length = 0; G.warnings.length = 0; G.events.length = 0;
  if(G.player) G.player.hp = Math.min(G.player.maxHp, G.player.hp + 45);
  G.shakeT = 0.35; G.shakeMag = 8;
  banner(G.mode === 'boss' ? 'БОСС ПОВЕРЖЕН!' : 'УРОВЕНЬ ' + G.level + ' ПРОЙДЕН');
  Sound.sfx('level_up');
  writeSave();
}
function gameOver(){
  G.scene = 'gameover';
  lastDeathLevel = G.level;
  save.best = Math.max(save.best || 1, G.level);
  writeSave();
  if(hud) hud.classList.add('hidden');
  if(btnDash) btnDash.classList.add('hidden');
  if(btnFire) btnFire.classList.add('hidden');
  firing = false; if(btnFire) btnFire.classList.remove('pressed');
  const os = $('overStats');
  if(os) os.innerHTML =
    'Вы погибли на уровне: <b>'+G.level+'</b><br>'+
    'Убито врагов: <b>'+G.killsTotal+'</b><br>'+
    'Побеждено боссов: <b>'+G.bossesTotal+'</b><br>'+
    'Подобрано аптечек: <b>'+G.healsTotal+'</b> 💚<br>'+
    'Монет: <b>'+save.coins+'</b> 🪙 · Энергии: <b>'+save.energy+'</b> 🔋';
  Sound.sfx('death');
  showScreen('scr-over');
}
(function bindRetry(){
  const el = $('btnRetryLevel');
  if(el) el.addEventListener('click', function(){ startGame(lastDeathLevel); });
})();

function explode(x, y, color, count, size){
  for(let i=0;i<count;i++){
    const a = Math.random()*TAU, s = rand(40, 260);
    G.parts.push({
      x: x, y: y, vx: Math.cos(a)*s, vy: Math.sin(a)*s,
      life: rand(0.25,0.65), max: 0.65,
      r: rand(1.5, size||3.5), color: color
    });
  }
}

function doDash(){
  const p = G.player;
  if(!p || G.scene !== 'playing') return;
  if(p.dashCd > 0 || p.dashTime > 0) return;
  let dx = p.moveX, dy = p.moveY;
  if(Math.sqrt(dx*dx+dy*dy) < 0.15){ dx = p.aimX; dy = p.aimY; }
  const l = Math.sqrt(dx*dx+dy*dy) || 1;
  p.dashX = dx/l; p.dashY = dy/l;
  p.dashTime = 0.16; p.dashCd = 1.5;
  p.invuln = Math.max(p.invuln, 0.34);
  explode(p.x, p.y, '#9fe8ff', 10, 3);
  Sound.sfx('dash');
}
if(btnDash){
  btnDash.addEventListener('touchstart', function(e){ e.preventDefault(); doDash(); }, {passive:false});
  btnDash.addEventListener('mousedown', function(e){ e.preventDefault(); doDash(); });
}

/* ==========================================================
   PAUSE / MENU
   ========================================================== */
function togglePause(){
  if(G.scene === 'playing'){
    G.scene = 'paused'; showScreen('scr-pause');
    if(hud) hud.classList.add('hidden');
    if(btnDash) btnDash.classList.add('hidden');
    if(btnFire) btnFire.classList.add('hidden');
    firing = false; if(btnFire) btnFire.classList.remove('pressed');
    Sound.setMusicVolume(0.06);
  } else if(G.scene === 'paused'){
    G.scene = 'playing'; hideOverlay();
    if(hud) hud.classList.remove('hidden');
    if(btnDash) btnDash.classList.remove('hidden');
    if(isTouchDevice && btnFire) btnFire.classList.remove('hidden');
    Sound.setMusicVolume(0.20);
  }
}
function goMenu(){
  G.scene = 'menu'; showScreen('scr-menu');
  if(hud) hud.classList.add('hidden');
  if(btnDash) btnDash.classList.add('hidden');
  if(btnFire) btnFire.classList.add('hidden');
  firing = false; if(btnFire) btnFire.classList.remove('pressed');
  updateHud(); updateBadge();
  const ub = $('userBadge');
  if(ub) ub.textContent = currentUser === 'guest'
    ? '👤 Гость'
    : (currentUser ? '👤 ' + currentUser : '👤 Не в аккаунте');

  const bp = $('btnPlay'), br = $('btnRestart');
  const lvl = save.lastLevel || 1;
  if(bp) bp.textContent = (lvl > 1) ? ('ПРОДОЛЖИТЬ · УР. ' + lvl) : 'ИГРАТЬ';
  if(br){
    if(lvl > 1) br.classList.remove('hidden');
    else br.classList.add('hidden');
  }

  Sound.startMusic('menu');
  Sound.setMusicVolume(0.20);
}
function openShop(){ returnTo = 'menu'; renderShop(); showScreen('scr-shop'); }
function openUpgrades(){ returnTo = 'menu'; renderUpgrades(); showScreen('scr-upgrades'); }
function openQuests(){ returnTo = 'menu'; renderQuests(); showScreen('scr-quests'); }
function openQuestsFromPause(){ returnTo = 'pause'; renderQuests(); showScreen('scr-quests'); }
function closeSub(){
  if(returnTo === 'pause') showScreen('scr-pause');
  else showScreen('scr-menu');
}
(function bindUI(){
  const bp = $('btnPause'); if(bp) bp.addEventListener('click', togglePause);
  const bm = $('btnMute'); if(bm) bm.addEventListener('click', function(){ Sound.toggleMute(); });
  const bmm = $('btnMuteMenu'); if(bmm) bmm.addEventListener('click', function(){ Sound.toggleMute(); });
  const bma = $('btnMuteAuth'); if(bma) bma.addEventListener('click', function(){ Sound.toggleMute(); });
})();

document.addEventListener('click', function(e){
  const t = e.target;
  if(t && t.closest && t.closest('button')) Sound.sfx('click');
});

/* ==========================================================
   UPDATE
   ========================================================== */
function update(dt){
  G.time += dt;
  const p = G.player; if(!p) return;

  let ix = 0, iy = 0;
  if(keys['w'] || keys['arrowup'] || keys['ц']) iy -= 1;
  if(keys['s'] || keys['arrowdown'] || keys['ы']) iy += 1;
  if(keys['a'] || keys['arrowleft'] || keys['ф']) ix -= 1;
  if(keys['d'] || keys['arrowright'] || keys['в']) ix += 1;
  ix += joy.dx; iy += joy.dy;
  const mag = Math.sqrt(ix*ix+iy*iy);
  if(mag > 1){ ix /= mag; iy /= mag; }
  p.moveX = ix; p.moveY = iy;

  if(p.invuln > 0) p.invuln -= dt;
  if(p.hurtFlash > 0) p.hurtFlash -= dt;
  if(p.dashCd > 0) p.dashCd -= dt;
  if(p.fireCd > 0) p.fireCd -= dt;

  if(p.dashTime > 0){
    p.dashTime -= dt;
    p.x += p.dashX * 980 * dt; p.y += p.dashY * 980 * dt;
    if(Math.random() < 0.7) G.parts.push({x:p.x, y:p.y, vx:rand(-30,30), vy:rand(-30,30),
      life:0.3, max:0.3, r:rand(2,4), color:'#8fe6ff'});
  } else {
    p.x += ix * p.speed * dt; p.y += iy * p.speed * dt;
  }
  p.x = clamp(p.x, p.r, W - p.r);
  p.y = clamp(p.y, p.r, H - p.r);

  const sk = skinById(save.skin);
  if(sk.trail){
    G.trail.push({x:p.x, y:p.y, life:0.35});
    if(G.trail.length > 22) G.trail.shift();
  } else if(G.trail.length){ G.trail.shift(); }
  for(let i=G.trail.length-1;i>=0;i--){
    G.trail[i].life -= dt;
    if(G.trail[i].life <= 0) G.trail.splice(i,1);
  }

  G.surviveTimer += dt;
  if(G.surviveTimer >= 1){ addQuestProgress('survive', 1); G.surviveTimer -= 1; }

  for(let i=G.events.length-1;i>=0;i--){
    if(G.time >= G.events[i].t){
      const fn = G.events[i].fn;
      G.events.splice(i,1);
      fn();
    }
  }

  if(G.medkits.length < 3){
    G.medkitTimer -= dt;
    if(G.medkitTimer <= 0){
      G.medkitTimer = rand(18, 28);
      spawnMedkit();
    }
  }
  for(let i=G.medkits.length-1;i>=0;i--){
    const m = G.medkits[i];
    m.life -= dt;
    m.pulse += dt;
    if(m.life <= 0){ G.medkits.splice(i,1); continue; }
    const mdx = p.x - m.x, mdy = p.y - m.y;
    if(mdx*mdx + mdy*mdy < (p.r + m.r)*(p.r + m.r)){
      const need = p.maxHp - p.hp;
      if(need <= 0.5) continue;
      const heal = Math.min(m.heal, need);
      p.hp += heal;
      G.healsTotal++;
      addQuestProgress('heal', 1);
      Sound.sfx('heal');
      toast('+ ' + Math.round(heal) + ' ❤️');
      explode(m.x, m.y, '#4cff9d', 16, 3.4);
      G.medkits.splice(i,1);
    }
  }

  if(inputMode === 'mouse'){
    const dx = mouseX - p.x, dy = mouseY - p.y;
    const l = Math.sqrt(dx*dx+dy*dy) || 1;
    p.aimX = dx/l; p.aimY = dy/l;
  } else {
    let nearest = null, nd = 1e9;
    for(let i=0;i<G.enemies.length;i++){
      const e = G.enemies[i];
      if(e.dead) continue;
      const d = Math.sqrt((e.x-p.x)*(e.x-p.x)+(e.y-p.y)*(e.y-p.y));
      if(d < nd){ nd = d; nearest = e; }
    }
    if(nearest){
      const dx = nearest.x - p.x, dy = nearest.y - p.y;
      const l = Math.sqrt(dx*dx+dy*dy) || 1;
      p.aimX = dx/l; p.aimY = dy/l;
    } else if(mag > 0.15){
      p.aimX = ix; p.aimY = iy;
    }
  }

  if(firing && p.fireCd <= 0){
    p.fireCd = p.fireRate;
    const levelBonus = G.level * 1.6;
    const dmgEnemy = p.damage + levelBonus;
    const dmgBoss  = p.baseDamage + levelBonus;
    const spread = (Math.random()-0.5) * 0.05;
    const ca = Math.cos(spread), sa = Math.sin(spread);
    const vx = (p.aimX*ca - p.aimY*sa) * 720;
    const vy = (p.aimX*sa + p.aimY*ca) * 720;
    G.pb.push({
      x: p.x + p.aimX*16, y: p.y + p.aimY*16,
      vx: vx, vy: vy, r: 4.5,
      dmg: dmgEnemy, dmgBoss: dmgBoss,
      life: 1.4
    });
    G.parts.push({x:p.x + p.aimX*20, y:p.y + p.aimY*20,
      vx:p.aimX*140 + rand(-40,40), vy:p.aimY*140 + rand(-40,40),
      life:0.14, max:0.14, r:2.5, color:'#ffd6a0'});
    Sound.sfx('shoot');
  }

  if(G.mode !== 'boss'){
    const maxEnemies = Math.min(4 + G.level*0.8, 18);
    G.spawnTimer -= dt;
    const spawnInterval = Math.max(0.35, (1.4 - G.level*0.035));
    if(G.spawnTimer <= 0 && G.enemies.length < maxEnemies){
      G.spawnTimer = spawnInterval; spawnEnemy();
    }
  } else {
    const maxEnemies = 5;
    G.spawnTimer -= dt;
    if(G.spawnTimer <= 0 && G.enemies.length < maxEnemies){
      G.spawnTimer = 3.2;
      spawnEnemy();
    }
  }

  for(let i=G.pb.length-1;i>=0;i--){
    const b = G.pb[i];
    b.x += b.vx*dt; b.y += b.vy*dt; b.life -= dt;
    if(b.life <= 0 || b.x < -30 || b.x > W+30 || b.y < -30 || b.y > H+30){ G.pb.splice(i,1); continue; }
    for(let j=0;j<G.enemies.length;j++){
      const e = G.enemies[j];
      if(e.dead) continue;
      if(Math.sqrt((e.x-b.x)*(e.x-b.x)+(e.y-b.y)*(e.y-b.y)) < e.r + b.r){
        const applied = e.isBoss ? (b.dmgBoss || b.dmg) : b.dmg;
        e.hp -= applied; e.hitFlash = 0.12;
        explode(b.x, b.y, e.color, 4, 2.6);
        if(e.hp <= 0){ killEnemy(e); }
        else { Sound.sfx(e.isBoss ? 'boss_hit' : 'hit'); }
        G.pb.splice(i,1); break;
      }
    }
  }

  for(let i=G.enemies.length-1;i>=0;i--){
    const e = G.enemies[i];
    if(e.dead){ G.enemies.splice(i,1); continue; }
    if(e.hitFlash > 0) e.hitFlash -= dt;
    e.rot += e.rotSpd * dt;

    const dx = p.x - e.x, dy = p.y - e.y;
    const d = Math.sqrt(dx*dx+dy*dy) || 1;
    const ux = dx/d, uy = dy/d;

    if(e.isBoss){ updateBoss(e, dt, d, ux, uy); }
    else if(e.type === 'shooter'){
      if(d > e.keep){ e.x += ux*e.speed*dt; e.y += uy*e.speed*dt; }
      else if(d < e.keep - 55){ e.x -= ux*e.speed*dt; e.y -= uy*e.speed*dt; }
      else { e.x += -uy*e.speed*0.6*dt; e.y += ux*e.speed*0.6*dt; }
      e.shootCd -= dt;
      if(e.shootCd <= 0 && d < 560){
        e.shootCd = Math.max(1.0, 2.2 - G.level*0.03);
        const sp = 240 + G.level*5;
        G.eb.push({x:e.x, y:e.y, vx:ux*sp, vy:uy*sp, r:5.5, dmg:e.dmg, life:3.2, color:'#4cd6ff', kind:'normal'});
      }
    } else {
      e.x += ux * e.speed * dt; e.y += uy * e.speed * dt;
    }

    for(let j=0;j<G.enemies.length;j++){
      const o = G.enemies[j];
      if(o === e || o.dead) continue;
      const ddx = e.x-o.x, ddy = e.y-o.y;
      const dd = Math.sqrt(ddx*ddx+ddy*ddy); const minD = e.r + o.r;
      if(dd > 0 && dd < minD){
        const push = (minD-dd)/dd * 0.5;
        e.x += ddx*push; e.y += ddy*push;
      }
    }

    if(e.atkCd > 0) e.atkCd -= dt;
    if(d < p.r + e.r && e.atkCd <= 0 && p.invuln <= 0){
      damagePlayer(e.dmg); e.atkCd = 0.75;
      e.x -= ux*22; e.y -= uy*22;
    }
    if(!e.isBoss){
      e.x = clamp(e.x, -80, W+80);
      e.y = clamp(e.y, -80, H+80);
    }
  }

  for(let i=G.eb.length-1;i>=0;i--){
    const b = G.eb[i];
    b.x += b.vx*dt; b.y += b.vy*dt; b.life -= dt;
    if(b.life <= 0 || b.x < -100 || b.x > W+100 || b.y < -100 || b.y > H+100){ G.eb.splice(i,1); continue; }
    let hit = false;
    if(b.kind === 'bone' || b.kind === 'spear'){
      hit = circleRect(p.x, p.y, p.r, b.x - b.w/2, b.y - b.h/2, b.w, b.h);
    } else {
      const dx = p.x-b.x, dy = p.y-b.y;
      hit = (dx*dx+dy*dy) < (p.r + b.r)*(p.r + b.r);
    }
    if(hit && p.invuln <= 0){
      damagePlayer(b.dmg);
      explode(b.x, b.y, b.color, 6, 3);
      G.eb.splice(i,1);
    }
  }

  for(let i=G.beams.length-1;i>=0;i--){
    const bm = G.beams[i];
    bm.t -= dt;
    if(bm.phase === 'warn'){
      if(bm.t <= 0){
        bm.phase = 'fire'; bm.t = 0.18; bm.hit = false;
        G.shakeT = 0.18; G.shakeMag = 6;
        explode(bm.x, bm.y, bm.color, 14, 4);
        Sound.sfx('boss_hit');
      }
    } else if(bm.phase === 'fire'){
      if(!bm.hit && p.invuln <= 0){
        const x2 = bm.x + Math.cos(bm.angle)*bm.len;
        const y2 = bm.y + Math.sin(bm.angle)*bm.len;
        const dist = pointLineDist(p.x, p.y, bm.x, bm.y, x2, y2);
        if(dist < p.r + bm.w/2){
          damagePlayer(bm.dmg);
          bm.hit = true;
        }
      }
      if(bm.t <= 0) G.beams.splice(i,1);
    }
  }

  for(let i=G.warnings.length-1;i>=0;i--){
    G.warnings[i].t -= dt;
    if(G.warnings[i].t <= 0) G.warnings.splice(i,1);
  }

  for(let i=G.parts.length-1;i>=0;i--){
    const q = G.parts[i];
    q.x += q.vx*dt; q.y += q.vy*dt;
    q.vx *= 0.94; q.vy *= 0.94;
    q.life -= dt;
    if(q.life <= 0) G.parts.splice(i,1);
  }

  if(G.shakeT > 0) G.shakeT -= dt;

  if(G.mode === 'boss'){
    if(G.bossSpawned && !G.boss) completeLevel();
  } else if(G.killed >= G.quota){
    completeLevel();
  }
  updateHud();
}

function updateBoss(b, dt, d, ux, uy){
  const p = G.player;
  if(b.entering){
    b.y += 90*dt;
    if(b.y >= 110){ b.y = 110; b.entering = false; }
    return;
  }
  if(d > 230){ b.x += ux*b.speed*dt; b.y += uy*b.speed*dt; }
  else if(d < 160){ b.x -= ux*b.speed*0.8*dt; b.y -= uy*b.speed*0.8*dt; }
  b.x += -uy * b.speed * 0.5 * b.strafeDir * dt;
  b.y +=  ux * b.speed * 0.5 * b.strafeDir * dt;
  b.x = clamp(b.x, b.r+10, W-b.r-10);
  b.y = clamp(b.y, b.r+10, H-b.r-10);

  if(Math.random() < 0.0025){
    b.x = clamp(p.x + rand(-260, 260), b.r+10, W-b.r-10);
    b.y = clamp(p.y + rand(-260, 260), b.r+10, H-b.r-10);
    explode(b.x, b.y, '#ff2f6e', 16, 4);
  }

  b.patternTimer -= dt;
  if(b.patternTimer <= 0){ runBossPattern(b); }
}

function killEnemy(e){
  if(e.dead) return;
  e.dead = true;
  G.killsTotal++;
  if(e.isBoss){
    G.boss = null;
    G.events.length = 0; G.beams.length = 0; G.warnings.length = 0;
    explode(e.x, e.y, '#ff2f6e', 60, 7);
    explode(e.x, e.y, '#ffd86b', 40, 5);
    G.shakeT = 0.6; G.shakeMag = 18;
    Sound.sfx('boss_die');
  } else {
    explode(e.x, e.y, e.color, 12, 3.4);
    G.killed++;
    addQuestProgress('kill', 1);
    Sound.sfx('explosion');
  }
}

function damagePlayer(dmg){
  const p = G.player;
  if(!p || p.invuln > 0) return;
  p.hp -= dmg; p.invuln = 0.7; p.hurtFlash = 0.25;
  G.shakeT = 0.22; G.shakeMag = 7;
  explode(p.x, p.y, '#ff4d6d', 10, 3);
  Sound.sfx('player_hit');
  if(p.hp <= 0){ p.hp = 0; gameOver(); }
}

/* ==========================================================
   RENDER
   ========================================================== */
function drawBg(){
  if(!ctx) return;
  ctx.fillStyle = '#05060d'; ctx.fillRect(0,0,W,H);
  const gs = 62;
  const ox = (G.time * 9) % gs, oy = (G.time * 6) % gs;
  ctx.strokeStyle = 'rgba(70,110,200,0.10)'; ctx.lineWidth = 1;
  ctx.beginPath();
  for(let x = -gs + ox; x < W + gs; x += gs){ ctx.moveTo(x, 0); ctx.lineTo(x, H); }
  for(let y = -gs + oy; y < H + gs; y += gs){ ctx.moveTo(0, y); ctx.lineTo(W, y); }
  ctx.stroke();
  const vg = ctx.createRadialGradient(W/2, H/2, Math.min(W,H)*0.22, W/2, H/2, Math.max(W,H)*0.78);
  vg.addColorStop(0, 'rgba(0,0,0,0)');
  vg.addColorStop(1, 'rgba(0,0,0,0.78)');
  ctx.fillStyle = vg; ctx.fillRect(0,0,W,H);
}
function drawPoly(x, y, r, sides, rot, fill, stroke){
  ctx.beginPath();
  for(let i=0;i<sides;i++){
    const a = rot + i/sides*TAU;
    const px = x + Math.cos(a)*r, py = y + Math.sin(a)*r;
    if(i) ctx.lineTo(px,py); else ctx.moveTo(px,py);
  }
  ctx.closePath();
  if(fill){ ctx.fillStyle = fill; ctx.fill(); }
  if(stroke){ ctx.strokeStyle = stroke; ctx.lineWidth = 2; ctx.stroke(); }
}
function skinRGB(sk){
  if(sk.rainbow){ const h = (G.time * 150) % 360; return hslToRgb(h/360, 1, 0.6); }
  return hexToRgb(sk.color);
}
function render(){
  if(!ctx) return;
  drawBg();
  if(!G.player) return;

  ctx.save();
  if(G.shakeT > 0){
    const m = G.shakeMag * (G.shakeT / 0.6);
    ctx.translate(rand(-m,m), rand(-m,m));
  }

  const p = G.player;
  const sk = skinById(save.skin);
  const rgb = skinRGB(sk);

  for(let i=0;i<G.medkits.length;i++){
    const m = G.medkits[i];
    const blink = m.life < 4 && Math.floor(G.time*12) % 2 === 0;
    if(blink) continue;
    const pulse = 0.5 + 0.5*Math.sin(m.pulse * 4);
    const scale = 1 + 0.09 * pulse;
    ctx.beginPath();
    ctx.arc(m.x, m.y, m.r * 2.6 * scale, 0, TAU);
    ctx.fillStyle = 'rgba(76,255,157,' + (0.10 + 0.08*pulse) + ')';
    ctx.fill();
    ctx.beginPath();
    ctx.arc(m.x, m.y, m.r * 1.6 * scale, 0, TAU);
    ctx.strokeStyle = 'rgba(76,255,157,' + (0.5 - 0.3*pulse) + ')';
    ctx.lineWidth = 2;
    ctx.stroke();
    ctx.save();
    ctx.translate(m.x, m.y);
    ctx.scale(scale, scale);
    ctx.beginPath();
    ctx.arc(0, 0, m.r, 0, TAU);
    ctx.fillStyle = '#ffffff';
    ctx.shadowColor = '#4cff9d'; ctx.shadowBlur = 20;
    ctx.fill();
    ctx.shadowBlur = 0;
    ctx.fillStyle = '#ff3d5a';
    ctx.fillRect(-8, -3, 16, 6);
    ctx.fillRect(-3, -8, 6, 16);
    ctx.restore();
  }

  for(let i=0;i<G.warnings.length;i++){
    const w = G.warnings[i];
    const a = 1 - (w.t / w.tmax);
    const pulse = 0.5 + 0.5 * Math.sin(G.time * 22);
    ctx.save();
    ctx.translate(w.x, w.y);
    ctx.beginPath();
    ctx.arc(0, 0, w.r + 4*pulse, 0, TAU);
    ctx.strokeStyle = rgba(hexToRgb(w.color), 0.9);
    ctx.lineWidth = 3; ctx.stroke();
    ctx.beginPath();
    ctx.arc(0, 0, w.r * (0.3 + a*0.7), 0, TAU);
    ctx.fillStyle = rgba(hexToRgb(w.color), 0.28); ctx.fill();
    ctx.beginPath();
    ctx.moveTo(-w.r-10, 0); ctx.lineTo(-w.r+4, 0);
    ctx.moveTo(w.r-4, 0); ctx.lineTo(w.r+10, 0);
    ctx.moveTo(0, -w.r-10); ctx.lineTo(0, -w.r+4);
    ctx.moveTo(0, w.r-4); ctx.lineTo(0, w.r+10);
    ctx.strokeStyle = rgba(hexToRgb(w.color), 0.7);
    ctx.lineWidth = 2; ctx.stroke();
    ctx.restore();
  }

  for(let i=0;i<G.beams.length;i++){
    const bm = G.beams[i];
    const x2 = bm.x + Math.cos(bm.angle)*bm.len;
    const y2 = bm.y + Math.sin(bm.angle)*bm.len;
    if(bm.phase === 'warn'){
      const pulse = 0.5 + 0.5 * Math.sin(G.time * 30);
      ctx.beginPath();
      ctx.moveTo(bm.x, bm.y); ctx.lineTo(x2, y2);
      ctx.strokeStyle = 'rgba(255,90,90,' + (0.35 + 0.4*pulse) + ')';
      ctx.lineWidth = 3; ctx.stroke();
      ctx.beginPath();
      ctx.moveTo(bm.x, bm.y); ctx.lineTo(x2, y2);
      ctx.strokeStyle = 'rgba(255,180,180,.55)';
      ctx.lineWidth = 1.2; ctx.stroke();
      ctx.beginPath();
      ctx.arc(bm.x, bm.y, 10 + 6*pulse, 0, TAU);
      ctx.fillStyle = rgba(hexToRgb(bm.color), 0.5); ctx.fill();
    } else {
      const alpha = clamp(bm.t / 0.18, 0, 1);
      ctx.save();
      ctx.globalCompositeOperation = 'lighter';
      ctx.beginPath();
      ctx.moveTo(bm.x, bm.y); ctx.lineTo(x2, y2);
      ctx.strokeStyle = rgba(hexToRgb(bm.color), 0.35 * alpha);
      ctx.lineWidth = bm.w * 2; ctx.lineCap = 'round'; ctx.stroke();
      ctx.beginPath();
      ctx.moveTo(bm.x, bm.y); ctx.lineTo(x2, y2);
      ctx.strokeStyle = rgba(hexToRgb(bm.color), 0.8 * alpha);
      ctx.lineWidth = bm.w; ctx.stroke();
      ctx.beginPath();
      ctx.moveTo(bm.x, bm.y); ctx.lineTo(x2, y2);
      ctx.strokeStyle = 'rgba(255,255,255,' + (0.95*alpha) + ')';
      ctx.lineWidth = bm.w * 0.35; ctx.stroke();
      ctx.restore();
      ctx.beginPath();
      ctx.arc(bm.x, bm.y, 16, 0, TAU);
      ctx.fillStyle = 'rgba(255,255,255,' + alpha + ')'; ctx.fill();
    }
  }

  if(p){
    for(let i=0;i<G.trail.length;i++){
      const t = G.trail[i];
      const a = t.life / 0.35;
      ctx.beginPath(); ctx.arc(t.x, t.y, p.r * 0.85 * a, 0, TAU);
      ctx.fillStyle = rgba(rgb, a*0.28); ctx.fill();
    }
  }

  for(let i=0;i<G.eb.length;i++){
    const b = G.eb[i];
    if(b.kind === 'bone'){
      const x = b.x - b.w/2, y = b.y - b.h/2;
      ctx.fillStyle = 'rgba(255,255,255,.18)';
      ctx.fillRect(x-3, y-3, b.w+6, b.h+6);
      ctx.fillStyle = '#ffffff'; ctx.fillRect(x, y, b.w, b.h);
      ctx.fillStyle = 'rgba(0,0,0,.55)';
      ctx.fillRect(x, y, 4, b.h);
      ctx.fillRect(x+b.w-4, y, 4, b.h);
    } else if(b.kind === 'spear'){
      const x = b.x - b.w/2, y = b.y - b.h/2;
      ctx.fillStyle = rgba(hexToRgb(b.color), 0.35);
      ctx.fillRect(x-3, y-3, b.w+6, b.h+6);
      ctx.fillStyle = b.color; ctx.fillRect(x, y, b.w, b.h);
      ctx.beginPath();
      ctx.moveTo(b.x, y - 12);
      ctx.lineTo(b.x - b.w/2 - 3, y + 4);
      ctx.lineTo(b.x + b.w/2 + 3, y + 4);
      ctx.closePath();
      ctx.fillStyle = '#fff5cc'; ctx.fill();
      ctx.fillStyle = 'rgba(0,0,0,.4)';
      ctx.fillRect(x, y + b.h*0.35, b.w, 2);
      ctx.fillRect(x, y + b.h*0.65, b.w, 2);
    } else {
      ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, TAU);
      ctx.fillStyle = b.color; ctx.shadowColor = b.color; ctx.shadowBlur = 14;
      ctx.fill(); ctx.shadowBlur = 0;
    }
  }

  for(let i=0;i<G.enemies.length;i++){
    const e = G.enemies[i];
    const col = e.hitFlash > 0 ? '#ffffff' : e.color;
    ctx.beginPath(); ctx.arc(e.x, e.y, e.r*1.7, 0, TAU);
    ctx.fillStyle = rgba(hexToRgb(e.color), 0.12); ctx.fill();
    drawPoly(e.x, e.y, e.r, e.sides, e.rot, col, 'rgba(255,255,255,.45)');
    if(e.isBoss){
      ctx.beginPath(); ctx.arc(e.x, e.y, e.r*0.45, 0, TAU);
      ctx.fillStyle = '#fff'; ctx.globalAlpha = 0.85; ctx.fill(); ctx.globalAlpha = 1;
      ctx.beginPath();
      ctx.arc(e.x + Math.cos(e.rot*2)*10, e.y + Math.sin(e.rot*2)*10, 5, 0, TAU);
      ctx.fillStyle = '#ff3060'; ctx.fill();
      ctx.beginPath();
      ctx.arc(e.x, e.y, e.r*1.15, 0, TAU);
      ctx.strokeStyle = 'rgba(255,176,32,.35)';
      ctx.lineWidth = 2;
      ctx.stroke();
    }
    if(!e.isBoss && e.hp < e.maxHp){
      const w = e.r*2.2; const hpPct = clamp(e.hp/e.maxHp,0,1);
      ctx.fillStyle = 'rgba(0,0,0,.55)'; ctx.fillRect(e.x-w/2, e.y-e.r-11, w, 4);
      ctx.fillStyle = hpPct > 0.5 ? '#4cff9d' : (hpPct > 0.25 ? '#ffd23f' : '#ff4d6d');
      ctx.fillRect(e.x-w/2, e.y-e.r-11, w*hpPct, 4);
    }
  }

  for(let i=0;i<G.pb.length;i++){
    const b = G.pb[i];
    ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, TAU);
    ctx.fillStyle = '#eafcff'; ctx.shadowColor = '#4cd6ff'; ctx.shadowBlur = 16;
    ctx.fill(); ctx.shadowBlur = 0;
  }

  for(let i=0;i<G.parts.length;i++){
    const q = G.parts[i];
    const a = clamp(q.life/q.max, 0, 1);
    ctx.globalAlpha = a;
    ctx.beginPath(); ctx.arc(q.x, q.y, q.r*a, 0, TAU);
    ctx.fillStyle = q.color; ctx.fill();
  }
  ctx.globalAlpha = 1;

  if(p && (G.scene === 'playing' || G.scene === 'paused' || G.scene === 'clear')){
    const blink = p.invuln > 0 && Math.floor(G.time*22) % 2 === 0;
    if(!blink){
      const g = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.r*3.4);
      g.addColorStop(0, rgba(rgb, sk.glow ? 0.6 : 0.34));
      g.addColorStop(1, rgba(rgb, 0));
      ctx.fillStyle = g; ctx.beginPath(); ctx.arc(p.x, p.y, p.r*3.4, 0, TAU); ctx.fill();

      ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, TAU);
      ctx.fillStyle = p.hurtFlash > 0 ? '#ff8fa3' : rgba(rgb, 1);
      ctx.shadowColor = rgba(rgb, 1); ctx.shadowBlur = sk.glow ? 26 : 12;
      ctx.fill(); ctx.shadowBlur = 0;

      ctx.lineWidth = 2.5; ctx.strokeStyle = 'rgba(255,255,255,.9)'; ctx.stroke();

      ctx.beginPath(); ctx.arc(p.x, p.y, p.r*0.42, 0, TAU);
      ctx.fillStyle = 'rgba(255,255,255,.95)'; ctx.fill();

      ctx.beginPath();
      ctx.moveTo(p.x + p.aimX*p.r*0.7, p.y + p.aimY*p.r*0.7);
      ctx.lineTo(p.x + p.aimX*(p.r+11), p.y + p.aimY*(p.r+11));
      ctx.strokeStyle = rgba(rgb, 0.95); ctx.lineWidth = 5; ctx.lineCap = 'round'; ctx.stroke();
    }
    if(p.dashCd > 0){
      const pct = 1 - p.dashCd/1.5;
      ctx.beginPath(); ctx.arc(p.x, p.y, p.r + 11, -Math.PI/2, -Math.PI/2 + TAU*pct);
      ctx.strokeStyle = 'rgba(120,220,255,.55)'; ctx.lineWidth = 3; ctx.stroke();
    }
  }

  if(inputMode === 'mouse' && G.scene === 'playing'){
    const r = 12 + Math.sin(G.time*8)*2;
    ctx.beginPath(); ctx.arc(mouseX, mouseY, r, 0, TAU);
    ctx.strokeStyle = 'rgba(255,180,120,.85)'; ctx.lineWidth = 2; ctx.stroke();
    ctx.beginPath(); ctx.arc(mouseX, mouseY, 3, 0, TAU);
    ctx.fillStyle = 'rgba(255,180,120,.9)'; ctx.fill();
    ctx.beginPath();
    ctx.moveTo(mouseX-18, mouseY); ctx.lineTo(mouseX-6, mouseY);
    ctx.moveTo(mouseX+6, mouseY); ctx.lineTo(mouseX+18, mouseY);
    ctx.moveTo(mouseX, mouseY-18); ctx.lineTo(mouseX, mouseY-6);
    ctx.moveTo(mouseX, mouseY+6); ctx.lineTo(mouseX, mouseY+18);
    ctx.strokeStyle = 'rgba(255,180,120,.55)'; ctx.lineWidth = 1.5; ctx.stroke();
  }

  if(joy.active && G.scene === 'playing'){
    ctx.beginPath(); ctx.arc(joy.ox, joy.oy, JOY_R, 0, TAU);
    ctx.strokeStyle = 'rgba(120,180,255,.28)'; ctx.lineWidth = 2.5; ctx.stroke();
    ctx.beginPath(); ctx.arc(joy.ox + joy.dx*JOY_R, joy.oy + joy.dy*JOY_R, 26, 0, TAU);
    ctx.fillStyle = 'rgba(120,200,255,.28)'; ctx.fill();
    ctx.strokeStyle = 'rgba(160,220,255,.7)'; ctx.lineWidth = 2; ctx.stroke();
  }

  if(G.boss && !G.boss.entering){
    const bw = Math.min(W - 60, 460);
    const bx = (W - bw)/2; const by = 76;
    ctx.fillStyle = 'rgba(0,0,0,.6)'; ctx.fillRect(bx-3, by-3, bw+6, 20);
    ctx.fillStyle = 'rgba(255,255,255,.1)'; ctx.fillRect(bx, by, bw, 14);
    const pct = clamp(G.boss.hp/G.boss.maxHp, 0, 1);
    const grd = ctx.createLinearGradient(bx, 0, bx+bw, 0);
    grd.addColorStop(0, '#ff2f6e'); grd.addColorStop(1, '#ffb03a');
    ctx.fillStyle = grd; ctx.fillRect(bx, by, bw*pct, 14);
    ctx.strokeStyle = 'rgba(255,120,160,.8)'; ctx.lineWidth = 1.5; ctx.strokeRect(bx, by, bw, 14);
    ctx.fillStyle = '#fff'; ctx.font = 'bold 11px system-ui, sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText('БОСС ' + (G.boss.bossIndex || '') + ' — ' + Math.ceil(G.boss.hp) + ' / ' + G.boss.maxHp, W/2, by - 7);
    ctx.fillStyle = 'rgba(255,176,32,.8)'; ctx.font = 'bold 10px system-ui, sans-serif';
    ctx.fillText('⚠ прокачка урона не действует', W/2, by + 26);
  }

  ctx.restore();
}

/* ==========================================================
   LOOP
   ========================================================== */
let lastT = performance.now();
function loop(t){
  const dt = Math.min(0.05, (t - lastT) / 1000);
  lastT = t;

  if(G.scene === 'playing'){
    update(dt);
    if(btnDash) btnDash.classList.toggle('cool', !!(G.player && G.player.dashCd > 0));
  } else if(G.scene === 'clear'){
    G.clearTimer -= dt;
    for(let i=G.parts.length-1;i>=0;i--){
      const q = G.parts[i];
      q.x += q.vx*dt; q.y += q.vy*dt;
      q.vx *= 0.94; q.vy *= 0.94; q.life -= dt;
      if(q.life <= 0) G.parts.splice(i,1);
    }
    if(G.shakeT > 0) G.shakeT -= dt;
    G.time += dt;
    if(G.clearTimer <= 0){
      G.level++; startLevel();
      G.scene = 'playing'; hideOverlay();
      if(hud) hud.classList.remove('hidden');
      if(btnDash) btnDash.classList.remove('hidden');
      if(isTouchDevice && btnFire) btnFire.classList.remove('hidden');
      banner('УРОВЕНЬ ' + G.level);
    }
  }

  render();
  requestAnimationFrame(loop);
}

setInterval(function(){
  if(!currentUser) return;
  checkCycle(); updateQuestTimer();
  const qs = $('scr-quests');
  if(qs && !qs.classList.contains('hidden')) renderQuests();
}, 1000);

setInterval(function(){
  if(currentUser && (G.scene === 'playing' || G.scene === 'paused')) writeSave();
}, 5000);

document.addEventListener('visibilitychange', function(){
  if(document.hidden && G.scene === 'playing') togglePause();
});

/* ==========================================================
   BOOT
   ========================================================== */
function boot(){
  try{
    resize();
    loadAccounts();
    let stored = null;
    try{ stored = localStorage.getItem(CURRENT_KEY); }catch(e){}
    if(stored){
      if(stored === 'guest'){ currentUser = 'guest'; }
      else if(accounts[stored]){ currentUser = stored; }
      else { currentUser = null; try{ localStorage.removeItem(CURRENT_KEY); }catch(e){} }
    }
    if(currentUser){
      loadSave();
      refreshAllUI();
      goMenu();
    } else {
      save = defaultSave();
      refreshAllUI();
      showAuth();
    }
    updateMuteButtons();
    requestAnimationFrame(loop);
  }catch(err){
    showFatal('BOOT: ' + (err && err.stack ? err.stack : err));
  }
}
boot();
</script>
</body>
</html>

Game Source: НЕОН: Арена

Creator: BlazingBear24

Libraries: none

Complexity: complex (2309 lines, 93.1 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: game-blazingbear24" to link back to the original. Then publish at arcadelab.ai/publish.