🎮ArcadeLab

SNAKE CLASH.IO

by CosmicRanger90
3805 lines150.8 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="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#0b120b">
<title>SNAKE CLASH.IO</title>
<style>
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent;}
html,body{width:100%;height:100%;overflow:hidden;background:#0b120b;
  font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif;
  touch-action:none;user-select:none;-webkit-user-select:none;color:#fff;}
canvas{position:fixed;inset:0;width:100%;height:100%;display:block;}

#menu,#langScreen,#authorScreen{position:fixed;inset:0;z-index:100;display:flex;flex-direction:column;
  align-items:center;justify-content:center;gap:14px;
  background:radial-gradient(circle at 50% 30%,#16281a 0%,#080d08 70%);
  padding:calc(20px + env(safe-area-inset-top)) 20px calc(20px + env(safe-area-inset-bottom));
  overflow-y:auto;}
#menu.hidden,#skinsScreen.hidden,#hud.hidden,#deathScreen.hidden,#langScreen.hidden,#authorScreen.hidden{display:none;}
#title{font-size:32px;font-weight:900;letter-spacing:2px;text-align:center;
  background:linear-gradient(90deg,#7fe07f,#f7d94a,#7fe07f);
  -webkit-background-clip:text;background-clip:text;color:transparent;
  text-shadow:0 0 40px rgba(120,240,120,.25);}
#subtitle{font-size:12px;color:#7a9a7a;margin-top:-8px;letter-spacing:1px;text-align:center;}

#coinBadge{font-size:13px;font-weight:900;color:#ffe45e;background:rgba(0,0,0,.5);
  padding:6px 16px;border-radius:10px;border:2px solid #7a6a1a;letter-spacing:1px;}

.mbtn{width:min(260px,80vw);padding:14px 0;border-radius:14px;font-size:17px;font-weight:800;
  text-align:center;letter-spacing:1px;cursor:pointer;pointer-events:auto;
  background:linear-gradient(180deg,#3a8f3a,#245a24);
  border:2px solid #58c058;box-shadow:0 4px 0 #143414,0 6px 18px rgba(0,0,0,.5);
  transition:transform .08s;text-decoration:none;color:#fff;display:block;}
.mbtn:active{transform:translateY(3px);box-shadow:0 1px 0 #143414;}
.mbtn.alt{background:linear-gradient(180deg,#3a5f8f,#24385a);border-color:#5878c0;
  box-shadow:0 4px 0 #142034,0 6px 18px rgba(0,0,0,.5);}
.mbtn.gold{background:linear-gradient(180deg,#b8860b,#7a5a08);border-color:#f7d94a;
  box-shadow:0 4px 0 #3a2a04,0 6px 18px rgba(0,0,0,.5);}
.mbtn.small{padding:10px 0;font-size:15px;width:min(220px,70vw);}

#nickWrap,.nickWrap{display:flex;flex-direction:column;gap:6px;align-items:center;margin:4px 0;}
#nickWrap label,.nickWrap label{font-size:11px;color:#8fc08f;letter-spacing:1px;font-weight:800;}
#nickInput{width:min(260px,80vw);padding:12px 14px;font-size:15px;font-weight:700;
  background:#0f1a0f;color:#fff;border:2px solid #2e4a2e;border-radius:12px;
  outline:none;text-align:center;letter-spacing:1px;pointer-events:auto;}
#nickInput:focus{border-color:#5cc05c;box-shadow:0 0 12px rgba(90,200,90,.3);}

#langRow,#mapRow{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;}
.chip{padding:8px 14px;border-radius:10px;background:#0f1a0f;border:2px solid #2e4a2e;
  font-size:13px;font-weight:800;cursor:pointer;color:#cfe8cf;letter-spacing:.5px;pointer-events:auto;}
.chip.sel{background:#1e3a1e;border-color:#7fe07f;color:#fff;box-shadow:0 0 10px rgba(120,240,120,.35);}
.chip:active{transform:scale(.96);}

#skinBadge{font-size:10px;color:#8fb08f;background:rgba(0,0,0,.55);
  padding:4px 12px;border-radius:8px;letter-spacing:1px;}

#langScreen h1{font-size:34px;font-weight:900;letter-spacing:3px;
  background:linear-gradient(90deg,#7fe07f,#f7d94a,#7fe07f);
  -webkit-background-clip:text;background-clip:text;color:transparent;}
#langScreen p{color:#9fc09f;font-size:13px;letter-spacing:1px;}

#authorScreen h1{font-size:26px;font-weight:900;letter-spacing:2px;color:#f7d94a;text-align:center;}
#authorBox{max-width:min(520px,92vw);display:flex;flex-direction:column;gap:16px;align-items:center;
  background:rgba(15,26,15,.7);border:2px solid #3a5a3a;border-radius:16px;padding:20px 18px;}
#authorText{font-size:14px;line-height:1.65;color:#d8e8d8;text-align:center;letter-spacing:.3px;}

#skinsScreen{position:fixed;inset:0;z-index:110;display:flex;flex-direction:column;
  background:linear-gradient(180deg,#0d160d,#081008);}
#skinsHeader{padding:calc(10px + env(safe-area-inset-top)) calc(14px + env(safe-area-inset-right)) 10px calc(14px + env(safe-area-inset-left));
  display:flex;align-items:center;gap:12px;border-bottom:1px solid #1e321e;}
#backBtn{padding:9px 16px;border-radius:10px;background:#1e321e;border:1px solid #3a5a3a;
  font-size:14px;font-weight:700;cursor:pointer;pointer-events:auto;}
#skinsHeader h2{font-size:15px;font-weight:800;letter-spacing:1px;flex:1;}
#skinsHeaderCoins{font-size:13px;font-weight:900;color:#ffe45e;background:rgba(0,0,0,.5);
  padding:5px 10px;border-radius:8px;border:1px solid #7a6a1a;}
#skinsGrid{flex:1;overflow-y:auto;-webkit-overflow-scrolling:touch;
  padding:12px calc(12px + env(safe-area-inset-right)) calc(20px + env(safe-area-inset-bottom)) calc(12px + env(safe-area-inset-left));
  display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px;
  touch-action:pan-y;}
.skinCard{background:#121d12;border:2px solid #24381f;border-radius:12px;padding:8px;
  display:flex;flex-direction:column;gap:5px;cursor:pointer;transition:.15s;position:relative;}
.skinCard.sel{border-color:#7fe07f;box-shadow:0 0 14px rgba(120,240,120,.4);background:#182a18;}
.skinCard.locked{opacity:.78;}
.skinCard.locked::before{content:'🔒';position:absolute;top:4px;right:6px;font-size:18px;z-index:2;}
.skinCard.sel::after{content:'✓';position:absolute;top:4px;right:6px;color:#7fe07f;
  font-size:18px;font-weight:900;z-index:3;}
.skinCard.sel.locked::before{display:none;}
.skinCard.cantafford{animation:cantflash .4s;}
@keyframes cantflash{0%,100%{border-color:#24381f}50%{border-color:#e04a3a}}
.skinPrev{height:52px;border-radius:9px;position:relative;overflow:hidden;}
.skinHead{position:absolute;width:30px;height:30px;border-radius:50%;
  left:50%;top:50%;transform:translate(-50%,-50%);box-shadow:0 0 10px rgba(0,0,0,.6);}
.skinName{font-size:12px;font-weight:800;text-align:center;}
.skinPrice{font-size:12px;font-weight:900;text-align:center;color:#ffe45e;letter-spacing:.5px;}
.skinPrice.owned{color:#7fe07f;font-size:11px;}
.skinStats{font-size:9.5px;color:#8fb08f;text-align:center;line-height:1.4;}
.skinAb{font-size:10px;color:#d8c86a;text-align:center;font-weight:800;}
.skinDesc{font-size:9px;color:#a8c0a8;text-align:center;line-height:1.35;font-style:italic;}
.skinCard.legendary{border-color:#f7d94a;box-shadow:0 0 12px rgba(247,217,74,.35);}

#hud{position:fixed;inset:0;z-index:20;pointer-events:none;}
#topbar{position:absolute;top:0;left:0;right:0;
  padding:calc(6px + env(safe-area-inset-top)) calc(6px + env(safe-area-inset-right)) 6px calc(6px + env(safe-area-inset-left));
  display:flex;align-items:flex-start;gap:6px;}
#bars{display:flex;flex-direction:column;gap:4px;flex:1;max-width:190px;}
.bar{position:relative;height:14px;border-radius:8px;background:rgba(0,0,0,.55);
  border:1px solid rgba(255,255,255,.16);overflow:hidden;}
.bar>i{position:absolute;left:0;top:0;bottom:0;width:100%;display:block;
  transition:width .12s linear;border-radius:7px;}
#hpfill{background:linear-gradient(90deg,#e04a3a,#f7c948,#5fd85f);}
#shfill{background:linear-gradient(90deg,#2a7fd0,#63d8f0);}
#stfill{background:linear-gradient(90deg,#b0b0b0,#ffe45e,#f0a040);}
.bar>span{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
  font-size:9px;font-weight:800;text-shadow:0 1px 2px #000;letter-spacing:.5px;}
#scorebox{font-size:11px;font-weight:800;background:rgba(0,0,0,.5);
  padding:4px 10px;border-radius:9px;border:1px solid rgba(255,255,255,.14);
  white-space:nowrap;}
#scorebox b{color:#8ef08e;font-size:14px;}
#menuBtnHud{width:34px;height:34px;border-radius:9px;background:rgba(0,0,0,.55);
  border:1px solid rgba(255,255,255,.2);display:flex;align-items:center;justify-content:center;
  font-size:18px;font-weight:900;pointer-events:auto;color:#dfe;flex-shrink:0;}

#lb{position:absolute;right:calc(6px + env(safe-area-inset-right));
  top:calc(52px + env(safe-area-inset-top));
  width:112px;background:rgba(4,12,4,.62);border:1px solid rgba(120,200,120,.22);
  border-radius:9px;padding:4px 6px;font-size:9px;line-height:1.4;}
#lb .lbtitle{font-size:8.5px;font-weight:900;letter-spacing:1px;color:#8ee08e;
  text-align:center;margin-bottom:2px;}
.lbrow{display:flex;justify-content:space-between;gap:3px;overflow:hidden;white-space:nowrap;}
.lbrow span:first-child{overflow:hidden;text-overflow:ellipsis;max-width:72px;}
.lbrow.me{color:#ffe45e;font-weight:800;}
.lbrow .sc{color:#9fd89f;}

#joyzone{position:absolute;left:0;bottom:0;width:52%;height:62%;
  pointer-events:auto;touch-action:none;}
#joybase{position:absolute;width:108px;height:108px;margin-left:-54px;margin-top:-54px;
  border-radius:50%;border:2px solid rgba(140,220,140,.35);
  background:radial-gradient(circle,rgba(30,70,30,.45),rgba(10,25,10,.28));
  opacity:.4;transition:opacity .15s;pointer-events:none;}
#joyknob{position:absolute;left:50%;top:50%;width:48px;height:48px;border-radius:50%;
  background:radial-gradient(circle at 35% 30%,#a8efa8,#3d8a3d);
  box-shadow:0 0 14px rgba(120,240,120,.4);transform:translate(-50%,-50%);
  pointer-events:none;}

#btns{position:absolute;right:calc(12px + env(safe-area-inset-right));
  bottom:calc(20px + env(safe-area-inset-bottom));
  display:flex;flex-direction:column;gap:10px;pointer-events:none;align-items:flex-end;}
.abtn{width:64px;height:64px;border-radius:50%;pointer-events:auto;touch-action:none;
  display:flex;align-items:center;justify-content:center;font-size:22px;
  border:2px solid rgba(255,255,255,.28);position:relative;
  background:radial-gradient(circle at 35% 30%,rgba(90,150,220,.75),rgba(20,45,90,.75));
  box-shadow:0 3px 12px rgba(0,0,0,.5);transition:transform .06s;}
.abtn:active{transform:scale(.93);}
#btnAttack{background:radial-gradient(circle at 35% 30%,rgba(230,110,80,.8),rgba(110,25,15,.8));}
#btnSprint{background:radial-gradient(circle at 35% 30%,rgba(240,200,80,.8),rgba(120,80,10,.8));}
.abtn.disabled{opacity:.28;pointer-events:none;}
.abtn .cd{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
  font-size:15px;font-weight:900;background:rgba(0,0,0,.62);border-radius:50%;
  color:#ffd45e;text-shadow:0 1px 3px #000;}
.abtn .cd.hidden{display:none;}
.abtn .lbl{position:absolute;bottom:-14px;width:100%;text-align:center;font-size:8px;
  font-weight:800;color:#cfe8cf;letter-spacing:.5px;text-shadow:0 1px 3px #000;white-space:nowrap;}

#deathScreen{position:fixed;inset:0;z-index:90;display:flex;flex-direction:column;
  align-items:center;justify-content:center;gap:14px;background:rgba(20,0,0,.72);
  backdrop-filter:blur(4px);padding:20px;}
#deathScreen h1{font-size:28px;font-weight:900;color:#ff6b5b;letter-spacing:2px;
  text-shadow:0 0 24px rgba(255,80,60,.6);}
#deathInfo{font-size:13px;color:#e8c0b0;text-align:center;line-height:1.7;}
#deathInfo b{color:#ffe45e;font-size:19px;}

@media (orientation:landscape){
  #topbar{padding-top:calc(4px + env(safe-area-inset-top));}
  #lb{top:calc(42px + env(safe-area-inset-top));width:96px;font-size:8.5px;}
  #bars{max-width:150px;}
  .bar{height:12px;}
  #joybase{width:92px;height:92px;margin-left:-46px;margin-top:-46px;}
  #joyknob{width:42px;height:42px;}
  #btns{bottom:calc(10px + env(safe-area-inset-bottom));gap:8px;}
  .abtn{width:56px;height:56px;font-size:19px;}
  .abtn .lbl{font-size:7.5px;bottom:-12px;}
  #lb .lbrow span:first-child{max-width:58px;}
  #title{font-size:26px;}
  #menu,#langScreen,#authorScreen{gap:10px;}
  .mbtn{padding:11px 0;font-size:15px;}
}
@media (max-width:380px){
  #scorebox{font-size:10px;padding:3px 7px;}
  #scorebox b{font-size:12px;}
}
</style>
</head>
<body>
<canvas id="game"></canvas>

<div id="hud" class="hidden">
  <div id="topbar">
    <div id="bars">
      <div class="bar"><i id="hpfill"></i><span id="hptext">100</span></div>
      <div class="bar"><i id="shfill"></i><span id="shtext">100</span></div>
      <div class="bar"><i id="stfill"></i><span id="sttext">100</span></div>
    </div>
    <div id="scorebox">Score: <b id="scoreval">0</b></div>
    <div id="menuBtnHud">≡</div>
  </div>
  <div id="lb"><div class="lbtitle" id="lbTitle">TOP 5</div><div id="lblist"></div></div>

  <div id="joyzone"><div id="joybase"><div id="joyknob"></div></div></div>

  <div id="btns">
    <div class="abtn" id="btnShield">🛡<div class="lbl" id="shieldLbl">SHIELD</div></div>
    <div class="abtn" id="btnSprint">💨<div class="lbl" id="sprintLbl">SPRINT</div></div>
    <div class="abtn" id="btnAttack">⚔<div class="cd hidden" id="atkCd">0</div><div class="lbl" id="atkLbl">ATTACK</div></div>
  </div>
</div>

<div id="langScreen" class="hidden">
  <h1>SNAKE CLASH.IO</h1>
  <p id="langPrompt">Choose your language · Выберите язык</p>
  <div class="mbtn" id="langRu">🇷🇺 РУССКИЙ</div>
  <div class="mbtn alt" id="langEn">🇬🇧 ENGLISH</div>
</div>

<div id="menu" class="hidden">
  <div id="title">SNAKE CLASH.IO</div>
  <div id="subtitle" data-i18n="subtitle">31 змея · арена на выживание</div>
  <div id="coinBadge">🪙 <span id="coinVal">0</span></div>

  <div id="nickWrap">
    <label data-i18n="nickLabel">ТВОЙ НИК</label>
    <input id="nickInput" type="text" maxlength="14" placeholder="—" autocomplete="off">
  </div>

  <div class="nickWrap" style="gap:8px">
    <label data-i18n="mapLabel">КАРТА</label>
    <div id="mapRow">
      <div class="chip sel" data-map="jungle" id="mapJungle">🌴 <span data-i18n="mapJungle">Джунгли</span></div>
      <div class="chip" data-map="winter" id="mapWinter">❄️ <span data-i18n="mapWinter">Зимняя</span></div>
    </div>
  </div>

  <div class="mbtn" id="playBtn" data-i18n="play">ИГРАТЬ</div>
  <div class="mbtn alt" id="skinsBtn" data-i18n="skins">СКИНЫ</div>
  <div class="mbtn gold" id="authorBtn" data-i18n="authorBtn">💚 ПОМОЩЬ АВТОРУ</div>
  <div class="mbtn small" id="langBtn" data-i18n="langBtn">🌐 СМЕНИТЬ ЯЗЫК</div>
  <div id="skinBadge">—</div>
</div>

<div id="authorScreen" class="hidden">
  <div id="authorBox">
    <h1 id="authorTitle">💚 Помощь автору</h1>
    <div id="authorText"></div>
    <a href="https://dalink.to/snakeclashio" target="_blank" rel="noopener" class="mbtn gold" id="authorLink">💚 Поддержать</a>
    <div class="mbtn alt" id="authorBackBtn" data-i18n="back">← НАЗАД</div>
  </div>
</div>

<div id="skinsScreen" class="hidden">
  <div id="skinsHeader">
    <div id="backBtn" data-i18n="back">← НАЗАД</div>
    <h2 id="skinsTitle">СКИНЫ</h2>
    <div id="skinsHeaderCoins">🪙 <span id="skinsCoins">0</span></div>
  </div>
  <div id="skinsGrid"></div>
</div>

<div id="deathScreen" class="hidden">
  <h1 id="deathTitle">ТЫ ПОГИБ</h1>
  <div id="deathInfo"></div>
  <div class="mbtn" id="respawnBtn" data-i18n="respawn">ВОЗРОДИТЬСЯ</div>
  <div class="mbtn alt" id="toMenuBtn" data-i18n="toMenu">В МЕНЮ</div>
</div>

<script>
"use strict";
(function(){
/* ============================================================
   SNAKE CLASH.IO — UPDATE v2.2
   Changes:
   - Полностью удалена система погоды (дождь/снег/звуки).
   - Автосохранение монет/скинов мгновенное (saveProgress → doSaveProgress).
   - Звуки атак у всех 31 змеи (уникальные + боты слышны рядом).
   - Анкилозавр: звук punch (удар кулаком/пощёчина).
   - Бургер: визуал под бургер (кунжут, салат, помидор, сыр).
   - Пул ников используется полностью (без обрезки).
   - Fix: handleCollisions учитывает invuln.
   - Fix: killSnake защищён от nBody<=0 (NaN-еда).
   - Fix: selectedSkin сохраняется вместе с монетами.
   - Fix: respawn/teleport/burrow чистят нулевые сегменты пути.
   ============================================================ */

const I18N = {
  ru:{
    subtitle:'31 змея · арена на выживание',
    nickLabel:'ТВОЙ НИК', mapLabel:'КАРТА', mapJungle:'Джунгли', mapWinter:'Зимняя',
    play:'ИГРАТЬ', skins:'СКИНЫ', langBtn:'🌐 СМЕНИТЬ ЯЗЫК',
    authorBtn:'💚 ПОМОЩЬ АВТОРУ',
    back:'← НАЗАД', skinsTitle:'ВСЕ СКИНЫ', top:'ТОП 5',
    respawn:'ВОЗРОДИТЬСЯ', toMenu:'В МЕНЮ',
    death:'ТЫ ПОГИБ', score:'Длина', killer:'Убийца',
    shield:'ЩИТ', attack:'АТАКА', sprint:'СПРИНТ', scoreHud:'Длина',
    nickPlaceholder:'ВВЕДИ ИМЯ', nickDefault:'Игрок',
    langPrompt:'Выберите язык · Choose your language',
    authorTitle:'💚 Помощь автору',
    authorText:'Мне 14, я в 8 классе. Мне надо готовиться к ВПР, постоянно делать уроки, и мой единственный помощник в создании игры — это ИИ. Если бы ты мог помочь финансовой суммой, я был бы очень рад — это даёт мне мотивацию, что кому-то реально нравится моя простая игра и кто-то реально ждёт обновлений. Я был бы рад даже 10 рублям.',
    authorLink:'💚 Поддержать',
    owned:'Куплено', buy:'Купить', notEnough:'Не хватает монет!'
  },
  en:{
    subtitle:'31 snakes · survival arena',
    nickLabel:'YOUR NICK', mapLabel:'MAP', mapJungle:'Jungle', mapWinter:'Winter',
    play:'PLAY', skins:'SKINS', langBtn:'🌐 CHANGE LANGUAGE',
    authorBtn:'💚 SUPPORT AUTHOR',
    back:'← BACK', skinsTitle:'ALL SKINS', top:'TOP 5',
    respawn:'RESPAWN', toMenu:'TO MENU',
    death:'YOU DIED', score:'Length', killer:'Killer',
    shield:'SHIELD', attack:'ATTACK', sprint:'SPRINT', scoreHud:'Length',
    nickPlaceholder:'ENTER NAME', nickDefault:'Player',
    langPrompt:'Choose your language',
    authorTitle:'💚 Support Author',
    authorText:"I'm 14, in 8th grade. I have exams to prepare for, homework all the time, and my only helper in making games is AI. If you could help with a small donation, I'd be super happy — it gives me motivation that someone really likes my simple game. Even 10 rubles would make my day.",
    authorLink:'💚 Support',
    owned:'Owned', buy:'Buy', notEnough:'Not enough coins!'
  }
};
let LANG = 'ru';
function t(k){ return (I18N[LANG] && I18N[LANG][k]) || k; }

const NICKS_RU = ["Тень","Волк","ЗмейГорыныч","Патсан","Братан","КорольДвора","СерыйВолк","КрасныйЗмей","ЧёрныйКот","БелыйТигр","ЗолотойДракон","СеребряныйЗмей","Влад_А4","МаксОН","ЛёхаТоп","ГлебПро","СтёпаГГ","БорисКилл","ОлегВин","ИванYT","Пётр777","ФёдорX","Димасик","Ярик","Тимоха","Мишаня","Пашка","Костян","Саня","Никитос","КириллУТ","АртёмГГ","ЕгорBest","Рома_777","ЖеняПро","ИльяТоп","АндрейВин","Серёга","Данила","Денчик","ЛёхаЮТ","ТохаПро","ВоваПро","Костик","Славик","Гриша","Валера","Стас","ЕгорПро","НикитаТоп","Максон","Даниил","Игорёк","Серж","Ден","СлавикПРО","Шкибиди","СигмаБой","СкибидиТуалет","Риззлорд","ГошаМогучий","БогданДруг","Нагибатор","Хацкер","Терминатор","КрутойПерец","МегаБосс","СуперКиллер","Псих","Маньяк","Безумный","Дикий","Злой","Хитрый","Ловкий","Быстрый","Молниеносный","Тихий","Громкий","Тёмный","Светлый","Огненный","Ледяной","Громовой","Змеиный","ЗмеиныйКороль","ЗмейБог","ЗмейЛорд","ЗмейБосс","ЗмейПро","КобраКинг","ПитонПро","УдавГГ","МамбаТоп","ГадюкаПро","Анаконда","ЧернаяМамба","БелаяКобра","КрасныйПитон","СинийУдав","ЗеленыйЗмей","ЖелтаяГадюка","ОгненнаяМамба","ЛедянойПитон","ГромовойУдав","ПустотныйЗмей","Космический","Звёздный","Галактический","ТёмнаяМатерия","ЧёрнаяДыра","Сверхновая","Метеор","Астероид","Комета","Планета","Вселенная","Бесконечность","ВечныйЗмей","Бессмертный","Нежить","Призрак","Фантом","Дух","Демон","Ангел","Дьявол","БогЗмей","ЦарьЗмей","Император","Король","Принц","Барон","Граф","Герцог","Рыцарь","Воин","Маг","Волшебник","Чародей","Колдун","Шаман","Друид","Варвар","Паладин","Рейнджер","Ассасин","Разбойник","Пират","Ниндзя","Самурай","Викинг","Спартанец","Гладиатор","Легионер","Титан","Гигант","Колосс","Атлант","Геркулес","Зевс","Посейдон","Аид","Арес","Аполлон","Гермес","Гефест","Дионис","Пан","Хаос","Тартар","Олимп","Кронос","Уран","Атлас","Прометей","Эпиметей","Тифон","Цербер","Химера","Горгона","Медуза","Циклоп","Минотавр","Кентавр","Сатир","Нимфа","Сирена","Харибда","Сцилла","Гарпия","Феникс","Грифон","Дракон","Виверна","Василиск","Кракен","Левиафан","Бегемот"];

const NICKS_EN = ["ShadowWolf","DarkSlayer","ProGamer2010","xX_Killer_Xx","SnakeMaster","DragonFire","NightHunter","GhostRider","IronFist","StormBreaker","FrostBite","VenomStrike","ThunderBolt","BlazeKing","ShadowNinja","SkullCrusher","DarkPhoenix","SilverArrow","RedViper","BlackMamba","CrazyFrog","MegaTron","UltimaWolf","DeadPool2011","SuperSnake","GamerPro","NoobSlayer","HeadShot","SniperKing","KillerBee","WildCat","AlphaWolf","LoneWolf","BloodMoon","DeathWish","NightFury","FireStorm","IceDragon","SkyWalker","EarthShaker","GreenAnaconda","CobraKing","PythonPro","Rattlesnake","BoaConstrictor","ToxicSlime","AcidBurn","PoisonIvy","Venomous","ToxicKing","xXx_Dark_xXx","_Pro_Player_","Mr_Snake","TheBeast","MonsterKill","RageQuit","LuckyShot","FastAndDead","SpeedDemon","TurboSnake","KingOfSnakes","QueenBee","LordVader","DarkKnight","WhiteWolf","GreyFox","RedDragon","BlueThunder","GreenGiant","PurpleHaze","Sasha2010","Dima_Pro","Maximka","VDanyaTop","Kirill_YT","ArtemGG","Nikitos","Vladislav","EgorBest","Romaragon_777","Zhenya_Pro","Ilya_TOPRed","Andrey_Win","DSergey_Kill","Pasha_2009","Kostya_GG","Denchik","Sanek_777","Lyoha_YT","Toha_Pro","ShadowKiller","DarkHunter","NightStalker","GhostWolf","PhantomLord","DemonSlayer","AngelOfDeath","DevilInside","HeavenFall","HellFire","SupaHot","MinecraftPro","FortniteKing","CSGO_Pro","RobloxGamer","BrawlStars","PubgMobile","FreeFire","GenshinPro","DotaLord","MrNobody","UnknownSoldier","SilentDeath","QuietStorm","LoudNoise","BigBoss","LittleDevil","CrazyWolf","MadDog","WildBoar","SnakeEater","SnakeHunter","SnakeKing","SnakeGod","SnakeLord","SnakeBoss","SnakePro","SnakeYT","SnakeX","GoldenSnake","SilverSnake","DiamondSnake","RubySnake","EmeraldSnake","CrystalSnake","Titanium","Platinum","Obsidian","Netherite","FireDragon","IceDragon","EarthDragon","WindDragon","WaterDragon","ThunderDragon","ShadowDragon","LightDragon","ChaosDragon","VoidDragon","xX_Shadow_Xx","xX_Dark_Xx","xX_Pro_Xx","xX_King_Xx","xX_Boss_Xx","xX_God_Xx","xX_Snake_Xx","xX_Wolf_Xx","xX_Kill_Xx","xX_YT_Xx","MegaKiller","UltraPro","SuperBoss","HyperWolf","GigaSnake","NanoBot","CyberSnake","RoboWolf","TechnoKing","DigitalGod","AnimeLover","MangaFan","NarutoFan","GokuSSJ","LuffyKing","ZoroPro","SasukeUchiha","ItachiSan","LeviAckerman","ErenYeager","BulletProof","IronMan2010","SpiderMan","BatmanPro","Superman","HulkSmash","ThorHammer","LokiTrick","DeadpoolX","Wolverine","PandaKing","TigerClaw","LionHeart","BearStrength","EagleEye","FalconWing","HawkEye","SharkBite","Crocodile","Alligator","MrBeastFan","SigmaBoy","SkibidiKing","RizzLord","GyattMaster","OhayoOni","FanumTax","SusAmogus","BabyShark","GojoSatoru","NekitPro","Vovchik","Serega","Dimasik","Yarik","Timoha","Mishanya","Pashka","Kostyan","Sanya","ShadowYT","DarkYT","ProYT","KingYT","BossYT","SnakeYT","WolfYT","KillerYT","GamerYT","NoobYT","MegaSnake228","Killer228","Pro228","Boss228","Wolf228","Dark228","Shadow228","King228","YT228","GG228","RainbowSnake","NeonSnake","GlowSnake","FireSnake","IceSnake","StormSnake","ThunderSnake","LightningSnake","BlizzardSnake","InfernoSnake","BlackDragon","WhiteDragonX","BlueDragon","GreenDragon","GoldDragon","SilverDragon","PurpleDragon","CrimsonDragon","AzureDragon","SeaSerpent","SerpentKing","FrostGiant","FireGiant","StormLord","FrostLord","FlameLord","ShadowLord","InfinityPro","EternalSnake","ImmortalKing","UndeadWolf","ZombiePro","SkeletonKing","GhostKing","PhantomX","WraithLord","Reaper","SonicBoom","FlashSpeed","QuickSilver","RapidFire","SwiftSnake","FastWolf","SpeedyGonzo","TurboKing","NitroBoost","HyperSpeed","MegaBoss","GigaChad","AlphaMale","SigmaWolf","OmegaSnake","DeltaForce","BetaTester","GammaRay","ZetaPro","ThetaKing","MrSnake2010","MrsSnake","DrSnake","ProfSnake","SirSnake","LadySnake","PrinceSnake","DukeSnake","BaronSnake","CraftMaster","BuildKing","MinePro","BlockBuster","PixelGamer","RetroBoy","ArcadeKing","GameOver","RespawnGod","Checkpoint","NinjaKing","SamuraiX","VikingPro","SpartanGG","Gladiator","LegionX","TitanLord","ColossusX","AtlasKing","ZeusBolt","PoseidonX","HadesKing","AresGod","ApolloX","HermesRun","Hephaestus","Dionysus","ChaosLord","TartarusX","OlympusX","KronosX","UranusX","PrometheusX","CerberusX","ChimeraX","GorgonX","MedusaX","CyclopsX","MinotaurX","CentaurX","SatyrX","NymphX","SirenX","Charybdis","ScyllaX","HarpyX","PhoenixX","GriffinX","WyvernX","BasiliskX","KrakenX","LeviathanX","BehemothX"];

/* ============ SKINS (each has unique attack sound) ============ */
const SKINS = [
  { n:{ru:'Дракон',en:'Dragon'}, b:'#c0392b', b2:'#3a0a04', p:'#f7b733', h:'#e8543f',
    head:{shape:'long',eyes:'angry',eyeColor:'#ffee55',pupilColor:'#000',pupil:'slit',horns:4,teeth:'fangs'},
    sideSpikes:{count:3,style:'sharp',len:1.0}, tail:'spikes', crest:'ridge',
    scale:'diamond', ab:'fire', abn:{ru:'Огненное дыхание',en:'Fire Breath'},
    desc:{ru:'Конус огня 2.6с, поджигает (3-6с).',en:'Fire cone 2.6s, burns (3-6s).'},
    sound:'fire', hp:130, sh:70, sp:1.00, atk:1.20, cd:6.0, abDur:2.6, shieldDur:16, shieldRes:1.0,
    maxS:90, sprintSpd:2.0, drain:35, regen:16, price:545 },

  { n:{ru:'Анкилозавр',en:'Ankylosaurus'}, b:'#6b7a4a', b2:'#1f2410', p:'#a8a06a', h:'#8a9a5a',
    head:{shape:'broad',eyes:'small',eyeColor:'#c8e050',pupilColor:'#000',pupil:'round',horns:'curve',teeth:'rows'},
    sideSpikes:{count:5,style:'round',len:0.9}, tail:'club', crest:'plate',
    scale:'armor', ab:'tail', abn:{ru:'Удар булавой',en:'Tail Smash'},
    desc:{ru:'Хвост всегда бьёт сзади, где булава.',en:'Tail always strikes behind where the club is.'},
    sound:'punch', hp:175, sh:150, sp:0.90, atk:1.05, cd:5.0, abDur:0.8, shieldDur:9, shieldRes:0.6,
    maxS:130, sprintSpd:1.7, drain:28, regen:22, price:285 },

  { n:{ru:'Гадюка',en:'Viper'}, b:'#4a6b3a', b2:'#152208', p:'#8fbf4a', h:'#5c8248',
    head:{shape:'flat',eyes:'slit',eyeColor:'#e0ff50',pupilColor:'#000',pupil:'slit',horns:0,teeth:'fangs'},
    sideSpikes:{count:0,style:'sharp',len:0}, tail:'plain', crest:'none',
    scale:'spots', ab:'vdash', abn:{ru:'Ядовитый рывок',en:'Venom Dash'},
    desc:{ru:'Рывок + ядовитый след.',en:'Dash + poison trail.'},
    sound:'venom', hp:110, sh:60, sp:1.05, atk:1.10, cd:3.4, abDur:1.6, shieldDur:18, shieldRes:1.2,
    maxS:100, sprintSpd:2.3, drain:40, regen:14, price:150 },

  { n:{ru:'Кобра',en:'Cobra'}, b:'#8a6d3b', b2:'#3a2a10', p:'#d9b36c', h:'#a88549',
    head:{shape:'long',eyes:'angry',eyeColor:'#ff5533',pupilColor:'#000',pupil:'slit',horns:0,teeth:'fangs',frill:'hood'},
    sideSpikes:{count:0,style:'sharp',len:0}, tail:'plain', crest:'hood',
    scale:'stripes', ab:'spit', abn:{ru:'Плевок ядом',en:'Venom Spit'},
    desc:{ru:'Снаряд яда, замедляет.',en:'Venom projectile, slows.'},
    sound:'spit', hp:105, sh:55, sp:1.05, atk:1.15, cd:2.8, abDur:1.6, shieldDur:22, shieldRes:1.3,
    maxS:80, sprintSpd:2.4, drain:42, regen:13, price:300 },

  { n:{ru:'Питон',en:'Python'}, b:'#7a6a4a', b2:'#2a2210', p:'#b09a70', h:'#93815b',
    head:{shape:'broad',eyes:'slit',eyeColor:'#a0e050',pupilColor:'#000',pupil:'slit',horns:0,teeth:'rows'},
    sideSpikes:{count:2,style:'round',len:0.5}, tail:'whip', crest:'none',
    scale:'diamond', ab:'constrict', abn:{ru:'Сжатие',en:'Constrict'},
    desc:{ru:'Тянет врагов, урон по времени.',en:'Pulls enemies, deals DoT.'},
    sound:'hiss', hp:150, sh:90, sp:0.92, atk:1.00, cd:6.5, abDur:2.4, shieldDur:12, shieldRes:0.85,
    maxS:140, sprintSpd:1.6, drain:30, regen:24, price:255 },

  { n:{ru:'Гремучник',en:'Rattlesnake'}, b:'#9a8a5a', b2:'#3a3018', p:'#e0d090', h:'#b3a26c',
    head:{shape:'flat',eyes:'small',eyeColor:'#ffe050',pupilColor:'#000',pupil:'slit',horns:0,teeth:'fangs'},
    sideSpikes:{count:3,style:'round',len:0.6}, tail:'spikes', crest:'none',
    scale:'armor', ab:'shock', abn:{ru:'Ударная волна',en:'Shockwave'},
    desc:{ru:'Взрыв вокруг + замедление.',en:'AoE blast + slow.'},
    sound:'rattle', hp:118, sh:65, sp:1.00, atk:1.15, cd:5.0, abDur:1.0, shieldDur:20, shieldRes:1.0,
    maxS:110, sprintSpd:2.0, drain:32, regen:18, price:320 },

  { n:{ru:'Мамба',en:'Mamba'}, b:'#2e5a2e', b2:'#061206', p:'#7fd97f', h:'#3d7a3d',
    head:{shape:'pointed',eyes:'slit',eyeColor:'#c8ff50',pupilColor:'#000',pupil:'slit',horns:0,teeth:'fangs'},
    sideSpikes:{count:0,style:'sharp',len:0}, tail:'plain', crest:'none',
    scale:'belly', ab:'sprint', abn:{ru:'Ускорение',en:'Speed Burst'},
    desc:{ru:'Сверхбыстрый рывок 1.8с.',en:'Ultra-fast dash 1.8s.'},
    sound:'whoosh', hp:100, sh:50, sp:1.18, atk:1.15, cd:3.5, abDur:1.8, shieldDur:24, shieldRes:1.4,
    maxS:70, sprintSpd:2.7, drain:50, regen:11, price:360 },

  { n:{ru:'Василиск',en:'Basilisk'}, b:'#3a6b6b', b2:'#0a1e1e', p:'#7fd9d9', h:'#4e8a8a',
    head:{shape:'long',eyes:'glow',eyeColor:'#7fffff',pupilColor:'#003030',pupil:'slit',horns:'curve',teeth:'rows'},
    sideSpikes:{count:4,style:'blade',len:0.8}, tail:'blade', crest:'ridge',
    scale:'glow', ab:'freeze', abn:{ru:'Окаменение',en:'Petrify'},
    desc:{ru:'Ледяное поле 3с, сильное замедление.',en:'Ice field 3s, heavy slow.'},
    sound:'ice', hp:120, sh:75, sp:0.98, atk:1.00, cd:7.0, abDur:3.0, shieldDur:16, shieldRes:0.9,
    maxS:95, sprintSpd:1.9, drain:30, regen:19, price:440 },

  { n:{ru:'Гидра',en:'Hydra'}, b:'#4a3a6b', b2:'#150a2a', p:'#a97fd9', h:'#634d8a',
    head:{shape:'broad',eyes:'multiple',eyeColor:'#ff33aa',pupilColor:'#000',pupil:'round',horns:4,teeth:'fangs'},
    sideSpikes:{count:4,style:'sharp',len:0.9}, tail:'spikes', crest:'none',
    scale:'spots', ab:'triple', abn:{ru:'Тройной плевок',en:'Triple Spit'},
    desc:{ru:'Три снаряда веером с ядом.',en:'Three venom projectiles.'},
    sound:'spit', hp:125, sh:70, sp:0.98, atk:1.20, cd:4.5, abDur:1.8, shieldDur:18, shieldRes:1.1,
    maxS:100, sprintSpd:2.0, drain:36, regen:16, price:460 },

  { n:{ru:'Левиафан',en:'Leviathan'}, b:'#1f4a6b', b2:'#04101c', p:'#5fa8d9', h:'#2d6288',
    head:{shape:'pointed',eyes:'glow',eyeColor:'#5fd8ff',pupilColor:'#001820',pupil:'slit',horns:'curve',teeth:'rows'},
    sideSpikes:{count:5,style:'blade',len:1.0}, tail:'fan', crest:'ridge',
    scale:'glow', ab:'wave', abn:{ru:'Волна',en:'Tsunami Wave'},
    desc:{ru:'Мощная отбрасывающая волна.',en:'Powerful knockback wave.'},
    sound:'splash', hp:160, sh:100, sp:0.88, atk:1.05, cd:6.0, abDur:1.4, shieldDur:11, shieldRes:0.75,
    maxS:150, sprintSpd:1.55, drain:30, regen:26, price:570 },

  { n:{ru:'Виверна',en:'Wyvern'}, b:'#a83a2a', b2:'#3d0e08', p:'#f0a050', h:'#c95236',
    head:{shape:'long',eyes:'angry',eyeColor:'#ffcc00',pupilColor:'#000',pupil:'slit',horns:2,teeth:'fangs'},
    sideSpikes:{count:2,style:'blade',len:1.1}, tail:'blade', crest:'none',
    scale:'diamond', ab:'fireball', abn:{ru:'Огненный шар',en:'Fireball'},
    desc:{ru:'Снаряд с взрывом по площади.',en:'AoE explosive projectile.'},
    sound:'fire', hp:120, sh:65, sp:1.02, atk:1.20, cd:4.2, abDur:2.0, shieldDur:18, shieldRes:1.05,
    maxS:90, sprintSpd:2.1, drain:38, regen:15, price:500 },

  { n:{ru:'Крайт',en:'Krait'}, b:'#2a2a5a', b2:'#0a0a20', p:'#6a6ad9', h:'#3d3d7a',
    head:{shape:'flat',eyes:'slit',eyeColor:'#dadaff',pupilColor:'#000',pupil:'slit',horns:0,teeth:'fangs'},
    sideSpikes:{count:0,style:'sharp',len:0}, tail:'whip', crest:'none',
    scale:'stripes', ab:'trail', abn:{ru:'Ядовитый след',en:'Poison Trail'},
    desc:{ru:'Оставляет долгое облако яда.',en:'Leaves lingering poison.'},
    sound:'venom', hp:110, sh:60, sp:1.03, atk:1.00, cd:5.5, abDur:5.0, shieldDur:20, shieldRes:1.1,
    maxS:105, sprintSpd:2.15, drain:34, regen:17, price:180 },

  { n:{ru:'Удав',en:'Boa'}, b:'#5a4a3a', b2:'#1f1810', p:'#8a7a5a', h:'#73604b',
    head:{shape:'broad',eyes:'sleepy',eyeColor:'#c8a060',pupilColor:'#000',pupil:'slit',horns:0,teeth:'rows'},
    sideSpikes:{count:0,style:'round',len:0}, tail:'whip', crest:'none',
    scale:'belly', ab:'crush', abn:{ru:'Удушение',en:'Crush'},
    desc:{ru:'Мощное притяжение + сильный DoT.',en:'Strong pull + heavy DoT.'},
    sound:'crush', hp:155, sh:95, sp:0.90, atk:0.95, cd:7.0, abDur:2.6, shieldDur:12, shieldRes:0.8,
    maxS:145, sprintSpd:1.6, drain:28, regen:25, price:225 },

  { n:{ru:'Коралловый',en:'Coral'}, b:'#c0392b', b2:'#3a0d06', p:'#f0e050', h:'#d9503a',
    head:{shape:'flat',eyes:'crazy',eyeColor:'#ffee00',pupilColor:'#000',pupil:'star',horns:0,teeth:'rows'},
    sideSpikes:{count:4,style:'round',len:0.6}, tail:'fan', crest:'plate',
    scale:'stripes', ab:'confuse', abn:{ru:'Дезориентация',en:'Confusion'},
    desc:{ru:'Поле дезориентации 3.5с.',en:'Confusion field 3.5s.'},
    sound:'hiss', hp:108, sh:60, sp:1.05, atk:0.95, cd:7.5, abDur:3.5, shieldDur:20, shieldRes:1.15,
    maxS:100, sprintSpd:2.05, drain:34, regen:18, price:165 },

  { n:{ru:'Тайпан',en:'Taipan'}, b:'#3a3a2a', b2:'#0a0a05', p:'#e0e050', h:'#54543c',
    head:{shape:'pointed',eyes:'glow',eyeColor:'#ffff80',pupilColor:'#000',pupil:'slit',horns:0,teeth:'fangs'},
    sideSpikes:{count:2,style:'sharp',len:0.7}, tail:'blade', crest:'none',
    scale:'belly', ab:'lightning', abn:{ru:'Молния',en:'Lightning'},
    desc:{ru:'Цепь молний по 4 врагам.',en:'Chain lightning to 4.'},
    sound:'zap', hp:110, sh:55, sp:1.08, atk:1.30, cd:4.8, abDur:0.4, shieldDur:22, shieldRes:1.3,
    maxS:80, sprintSpd:2.4, drain:45, regen:12, price:600 },

  { n:{ru:'Медноголовый',en:'Copperhead'}, b:'#8a5a3a', b2:'#2e1810', p:'#d9a070', h:'#a87049',
    head:{shape:'broad',eyes:'slit',eyeColor:'#e0c060',pupilColor:'#000',pupil:'slit',horns:'curve',teeth:'fangs'},
    sideSpikes:{count:2,style:'round',len:0.55}, tail:'plain', crest:'none',
    scale:'spots', ab:'poison', abn:{ru:'Облако яда',en:'Poison Cloud'},
    desc:{ru:'Стоячее облако яда 5с.',en:'Standing poison cloud 5s.'},
    sound:'venom', hp:115, sh:62, sp:1.00, atk:1.10, cd:6.0, abDur:5.0, shieldDur:18, shieldRes:1.05,
    maxS:95, sprintSpd:2.0, drain:34, regen:17, price:270 },

  { n:{ru:'Боковой',en:'Sidewinder'}, b:'#b08040', b2:'#3d2410', p:'#f0d090', h:'#cc9a52',
    head:{shape:'flat',eyes:'small',eyeColor:'#ffdd44',pupilColor:'#000',pupil:'round',horns:0,teeth:'rows'},
    sideSpikes:{count:3,style:'round',len:0.5}, tail:'fan', crest:'none',
    scale:'stripes', ab:'zigzag', abn:{ru:'Зигзаг',en:'Zigzag'},
    desc:{ru:'Много зигзагов, малый откат.',en:'Fast zigzag dash.'},
    sound:'whoosh', hp:105, sh:58, sp:1.12, atk:1.15, cd:2.8, abDur:2.0, shieldDur:22, shieldRes:1.35,
    maxS:115, sprintSpd:2.25, drain:36, regen:20, price:195 },

  { n:{ru:'Морской змей',en:'Sea Serpent'}, b:'#1a6b6b', b2:'#021414', p:'#50d0c0', h:'#268f8f',
    head:{shape:'long',eyes:'glow',eyeColor:'#40ffd0',pupilColor:'#001820',pupil:'slit',horns:0,teeth:'fangs'},
    sideSpikes:{count:4,style:'blade',len:0.85}, tail:'fan', crest:'ridge',
    scale:'glow', ab:'maelstrom', abn:{ru:'Прилив',en:'Maelstrom'},
    desc:{ru:'Водоворот тянет врагов внутрь.',en:'Vortex pulls enemies in.'},
    sound:'splash', hp:140, sh:85, sp:0.95, atk:1.05, cd:6.5, abDur:2.5, shieldDur:14, shieldRes:0.9,
    maxS:125, sprintSpd:1.85, drain:30, regen:21, price:520 },

  { n:{ru:'Громовой',en:'Thunder'}, b:'#4a4a7a', b2:'#101026', p:'#e0e0ff', h:'#6666a3',
    head:{shape:'pointed',eyes:'glow',eyeColor:'#e0e0ff',pupilColor:'#000060',pupil:'slit',horns:2,teeth:'fangs'},
    sideSpikes:{count:3,style:'blade',len:0.9}, tail:'spikes', crest:'ridge',
    scale:'glow', ab:'thunder', abn:{ru:'Гром',en:'Thunder Strike'},
    desc:{ru:'Сразу 5 молний по врагам.',en:'5 lightning bolts on enemies.'},
    sound:'thunder', hp:125, sh:70, sp:1.00, atk:1.25, cd:5.0, abDur:0.5, shieldDur:16, shieldRes:1.0,
    maxS:105, sprintSpd:2.1, drain:38, regen:15, price:650 },

  { n:{ru:'Пустотный',en:'Void'}, b:'#1a1a2a', b2:'#000000', p:'#a050f0', h:'#2e2e4a',
    head:{shape:'skull',eyes:'glow',eyeColor:'#c070ff',pupilColor:'#000000',pupil:'star',horns:'curve',teeth:'fangs'},
    sideSpikes:{count:5,style:'sharp',len:1.0}, tail:'blade', crest:'none',
    scale:'glow', ab:'teleport', abn:{ru:'Прыжок в пустоту',en:'Void Jump'},
    desc:{ru:'Телепорт + имплозия на выходе.',en:'Teleport + implosion.'},
    sound:'teleport', hp:115, sh:65, sp:1.05, atk:1.25, cd:5.5, abDur:1.2, shieldDur:18, shieldRes:1.0,
    maxS:100, sprintSpd:2.15, drain:40, regen:14, price:770 },

  { n:{ru:'Скорпион',en:'Scorpion'}, b:'#5a3a1a', b2:'#1a0e04', p:'#f0c060', h:'#7a5030',
    head:{shape:'broad',eyes:'angry',eyeColor:'#ff8800',pupilColor:'#000',pupil:'slit',horns:2,teeth:'fangs'},
    sideSpikes:{count:6,style:'sharp',len:0.7}, tail:'blade', crest:'plate',
    scale:'armor', ab:'sting', abn:{ru:'Жало',en:'Sting'},
    desc:{ru:'Дальний удар жалом (конус).',en:'Long tail sting (cone).'},
    sound:'sting', hp:120, sh:75, sp:1.00, atk:1.20, cd:3.8, abDur:0.6, shieldDur:17, shieldRes:1.0,
    maxS:100, sprintSpd:2.05, drain:36, regen:17, price:400 },

  { n:{ru:'Скунс',en:'Skunk'}, b:'#2a2a2a', b2:'#000000', p:'#f0f0f0', h:'#3a3a3a',
    head:{shape:'round',eyes:'small',eyeColor:'#ffee55',pupilColor:'#000',pupil:'round',horns:0,teeth:'none'},
    sideSpikes:{count:0,style:'sharp',len:0}, tail:'fan', crest:'none',
    scale:'stripes', ab:'stink', abn:{ru:'Вонь',en:'Stink Cloud'},
    desc:{ru:'Облако вони: замедление + дезориентация.',en:'Stink: slow + confuse.'},
    sound:'puff', hp:115, sh:65, sp:1.02, atk:0.95, cd:6.0, abDur:4.0, shieldDur:18, shieldRes:1.1,
    maxS:110, sprintSpd:2.1, drain:32, regen:20, price:210 },

  { n:{ru:'Дикобраз',en:'Porcupine'}, b:'#6b5030', b2:'#20180c', p:'#f0e0c0', h:'#8a6a42',
    head:{shape:'round',eyes:'small',eyeColor:'#e0c080',pupilColor:'#000',pupil:'round',horns:0,teeth:'rows'},
    sideSpikes:{count:8,style:'sharp',len:0.7}, tail:'spikes', crest:'none',
    scale:'armor', ab:'spikeburst', abn:{ru:'Взрыв игл',en:'Spike Burst'},
    desc:{ru:'Взрыв игл вокруг + отброс.',en:'Spike burst + knockback.'},
    sound:'stab', hp:145, sh:100, sp:0.94, atk:1.15, cd:5.5, abDur:0.5, shieldDur:11, shieldRes:0.7,
    maxS:140, sprintSpd:1.7, drain:26, regen:24, price:340 },

  { n:{ru:'Электроугорь',en:'Electric Eel'}, b:'#1a3a4a', b2:'#02101a', p:'#5fd0ff', h:'#2a5a70',
    head:{shape:'long',eyes:'glow',eyeColor:'#a0f0ff',pupilColor:'#000820',pupil:'slit',horns:0,teeth:'rows'},
    sideSpikes:{count:0,style:'sharp',len:0}, tail:'whip', crest:'ridge',
    scale:'glow', ab:'efield', abn:{ru:'Электрополе',en:'Electric Field'},
    desc:{ru:'Электрополе вокруг, DoT + замедление.',en:'Field around, DoT + slow.'},
    sound:'zap', hp:125, sh:70, sp:1.02, atk:1.10, cd:5.0, abDur:2.5, shieldDur:15, shieldRes:0.95,
    maxS:105, sprintSpd:2.1, drain:36, regen:17, price:420 },

  { n:{ru:'Огненный муравей',en:'Fire Ant'}, b:'#7a1a0a', b2:'#2a0602', p:'#ff8030', h:'#a02a10',
    head:{shape:'round',eyes:'angry',eyeColor:'#ffcc00',pupilColor:'#000',pupil:'round',horns:2,teeth:'fangs'},
    sideSpikes:{count:4,style:'sharp',len:0.5}, tail:'spikes', crest:'none',
    scale:'armor', ab:'acid', abn:{ru:'Кислота',en:'Acid Spray'},
    desc:{ru:'Кислотный конус, поджигает.',en:'Acid cone, ignites.'},
    sound:'acid', hp:110, sh:60, sp:1.10, atk:1.20, cd:4.0, abDur:2.0, shieldDur:20, shieldRes:1.2,
    maxS:90, sprintSpd:2.35, drain:44, regen:13, price:380 },

  { n:{ru:'Ледяной червь',en:'Ice Worm'}, b:'#8fbfd9', b2:'#0a2030', p:'#e0f8ff', h:'#a8d0e0',
    head:{shape:'round',eyes:'sleepy',eyeColor:'#a0e8ff',pupilColor:'#002030',pupil:'round',horns:0,teeth:'rows'},
    sideSpikes:{count:2,style:'blade',len:0.6}, tail:'blade', crest:'none',
    scale:'glow', ab:'icetrail', abn:{ru:'Ледяной след',en:'Ice Trail'},
    desc:{ru:'Оставляет ледяной след (slow).',en:'Leaves ice trail (slow).'},
    sound:'ice', hp:135, sh:80, sp:0.98, atk:1.05, cd:5.5, abDur:4.5, shieldDur:16, shieldRes:1.0,
    maxS:125, sprintSpd:1.85, drain:30, regen:22, price:240 },

  { n:{ru:'Песчаный червь',en:'Sandworm'}, b:'#b09060', b2:'#3a2a14', p:'#f0d8a8', h:'#d0b080',
    head:{shape:'round',eyes:'multiple',eyeColor:'#a06020',pupilColor:'#000',pupil:'round',horns:0,teeth:'rows'},
    sideSpikes:{count:6,style:'round',len:0.5}, tail:'spikes', crest:'none',
    scale:'belly', ab:'burrow', abn:{ru:'Норка',en:'Burrow'},
    desc:{ru:'Уход под землю + мощный удар.',en:'Burrow + powerful strike.'},
    sound:'burrow', hp:160, sh:95, sp:0.92, atk:1.10, cd:6.0, abDur:2.0, shieldDur:12, shieldRes:0.85,
    maxS:140, sprintSpd:1.7, drain:28, regen:24, price:480 },

  { n:{ru:'Феникс',en:'Phoenix'}, b:'#c04020', b2:'#3a0a04', p:'#ffd050', h:'#f07030',
    head:{shape:'pointed',eyes:'glow',eyeColor:'#ffcc00',pupilColor:'#800000',pupil:'star',horns:'curve',teeth:'fangs'},
    sideSpikes:{count:4,style:'blade',len:1.1}, tail:'fan', crest:'ridge',
    scale:'glow', ab:'rebirth', abn:{ru:'Возрождение',en:'Rebirth'},
    desc:{ru:'Лечение + усиленный щит + огонь вокруг.',en:'Heal + shield + fire ring.'},
    sound:'fire', hp:120, sh:80, sp:1.05, atk:1.10, cd:8.0, abDur:3.0, shieldDur:14, shieldRes:0.9,
    maxS:115, sprintSpd:2.1, drain:34, regen:18, price:0 },

  { n:{ru:'Богдан',en:'Bogdan'}, b:'#7fd97f', b2:'#1a3a1a', p:'#d0ffd0', h:'#a8efa8',
    head:{shape:'round',eyes:'heart',eyeColor:'#ff5588',pupilColor:'#000',pupil:'round',horns:0,teeth:'none'},
    sideSpikes:{count:0,style:'sharp',len:0}, tail:'plain', crest:'none',
    scale:'smooth', ab:'none', abn:{ru:'Нет атаки',en:'No attack'},
    desc:{ru:'Друг разработчика. Растёт быстрее всех от еды!',en:"Dev's friend. Grows fastest from food!"},
    sound:'none', hp:200, sh:120, sp:1.00, atk:0, cd:99, abDur:0, shieldDur:14, shieldRes:1.0,
    maxS:160, sprintSpd:1.7, drain:22, regen:26, noAttack:true, growMul:2.5, price:0 },

  { n:{ru:'Бургер',en:'Burger'}, b:'#c88040', b2:'#3a1c08', p:'#e8b060', h:'#e0a050',
    head:{shape:'round',eyes:'sleepy',eyeColor:'#3a1c08',pupilColor:'#000',pupil:'round',horns:0,teeth:'none'},
    sideSpikes:{count:0,style:'sharp',len:0}, tail:'plain', crest:'none',
    scale:'armor', ab:'none', abn:{ru:'Нет атаки',en:'No attack'},
    desc:{ru:'Друг Богдана. Из него выпадает куча еды!',en:"Bogdan's friend. Drops lots of food!"},
    sound:'none', hp:180, sh:100, sp:0.95, atk:0, cd:99, abDur:0, shieldDur:14, shieldRes:1.0,
    maxS:130, sprintSpd:1.75, drain:26, regen:22, noAttack:true, foodDropMul:3.5, price:0,
    visual:'burger' },

  { n:{ru:'Титанобоа',en:'Titanoboa'}, b:'#4a2a1a', b2:'#140703', p:'#f0a050', h:'#6b3d24',
    head:{shape:'broad',eyes:'angry',eyeColor:'#ff5500',pupilColor:'#000',pupil:'slit',horns:'curve',teeth:'fangs'},
    sideSpikes:{count:6,style:'sharp',len:1.2}, tail:'whip', crest:'ridge',
    scale:'diamond', ab:'roar', abn:{ru:'Сокрушительный рёв',en:'Devastating Roar'},
    desc:{ru:'Рёв на дистанции: оглушает и наносит урон по площади. Спавнится с 600 длины, еда ×1.7.',en:'Roar at range: stuns and AoE damage. Spawns at 600 length, food ×1.7.'},
    sound:'roar', hp:200, sh:110, sp:0.98, atk:1.15, cd:9.0, abDur:1.4, shieldDur:14, shieldRes:0.95,
    maxS:180, sprintSpd:1.75, drain:28, regen:24,
    price:1890, playerOnly:true, bigStart:600, growMul:1.7 }
];

/* ====== WORLD / CONST ====== */
const WORLD = 7700;
const MAX_SNAKES = 55;
const BOT_COUNT = MAX_SNAKES - 1;
const BASE_SPEED = 168;
const FOOD_COUNT = 3600;
const FOOD_TARGET = 3600;
const BODY_CELL = 130;
const FOOD_CELL = 150;
const BODY_GW = Math.ceil(WORLD / BODY_CELL) + 2;
const FOOD_GW = Math.ceil(WORLD / FOOD_CELL) + 2;
const MAX_BODY = 130;
const TAU = Math.PI*2;
const SOUND_RADIUS = 1100;

const rand=(a,b)=>a+Math.random()*(b-a);
const randInt=(a,b)=>Math.floor(a+Math.random()*(b-a+1));
const clamp=(v,a,b)=>v<a?a:(v>b?b:v);
function angDiff(a,b){let d=a-b;while(d>Math.PI)d-=TAU;while(d<-Math.PI)d+=TAU;return d;}
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;}

/* ====== MAPS (weather removed) ====== */
const MAPS = {
  jungle:{
    name:{ru:'Джунгли',en:'Jungle'},
    ground:['#182613','#1a2a15','#162210','#1c2d17','#152009'],
    border:'#5a1a1a', border2:'#a83030',
    decoTree:'#1e3a16', decoTree2:'#2a4d1e', decoTreeHi:'rgba(90,150,60,0.35)',
    pond1:'#12303a', pond2:'#1a4450',
    rock1:'#3a3a34', rock2:'#4a4a42',
    bush1:'#2d4a1e', bush2:'#3a5a28', bushHi:'#5c9a3a',
    treeCount:640, pondCount:36, rockCount:260, bushCount:280, smallPondCount:38
  },
  winter:{
    name:{ru:'Зимняя',en:'Winter'},
    ground:['#d8e4ec','#cfdce6','#dde8ee','#c8d6e0','#d2dee6'],
    border:'#2a4a6a', border2:'#5a8ab0',
    decoTree:'#2a4a3a', decoTree2:'#1a3028', decoTreeHi:'rgba(255,255,255,0.6)',
    pond1:'#4a7a9a', pond2:'#6aa0c0',
    rock1:'#a8b0b8', rock2:'#c0c8d0',
    bush1:'#3a5a6a', bush2:'#4a6a7a', bushHi:'#e0f0ff',
    ice1:'#a8d0e8', ice2:'#c8e4f4', ice3:'#dff0fa',
    treeCount:440, pondCount:26, rockCount:300, snowmanCount:70, iceRiverCount:24
  }
};
let currentMapKey = 'jungle';
let currentMap = MAPS.jungle;

/* ====== CANVAS ====== */
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d',{alpha:false});
let cw=0,ch=0,dpr=1;
const cam = { x:WORLD/2, y:WORLD/2, scale:1, baseScale:1, zoom:1, zoomCur:1 };

function resize(){
  dpr = Math.min(window.devicePixelRatio||1, 2);
  cw = window.innerWidth; ch = window.innerHeight;
  canvas.width = Math.floor(cw*dpr);
  canvas.height = Math.floor(ch*dpr);
  canvas.style.width = cw+'px'; canvas.style.height = ch+'px';
  const base = Math.min(cw, ch);
  cam.baseScale = base / 620;
  render._vg = null;
  placeJoyDefault();
}
window.addEventListener('resize', resize);
window.addEventListener('orientationchange', ()=>setTimeout(resize,250));

/* ====== STATE ====== */
let state = 'lang';
let snakes = [];
let snakeById = new Map();
let player = null;
let nextId = 1;
let effects=[], particles=[], decos=[];
let textPops = [];
let time=0, hudTimer=0;
let respawnQueue=[];
let usedNicks=[];
let playerDeathInfo={score:0,killer:''};
let selectedSkin = 0;
let audioCtx = null;

/* ====== ECONOMY ====== */
let coins = 0;
let ownedSkins = new Set();

function isSkinOwned(idx){
  const s = SKINS[idx];
  if (!s) return false;
  if (s.price === 0) return true;
  return ownedSkins.has(idx);
}
function isSkinAvailableForBots(idx){
  const s = SKINS[idx];
  return s && !s.playerOnly;
}

/* Полное мгновенное сохранение — монеты + купленные скины + выбранный скин */
function doSaveProgress(){
  try{
    localStorage.setItem('snakeCoins', String(coins));
    localStorage.setItem('snakeOwnedSkins', JSON.stringify([...ownedSkins]));
    localStorage.setItem('snakeSkin', String(selectedSkin));
  }catch(e){}
}
function saveProgress(){ doSaveProgress(); }

function loadProgress(){
  try{
    const c = localStorage.getItem('snakeCoins');
    if (c !== null){ const v = parseInt(c,10); if (Number.isFinite(v) && v>=0) coins = v; }
    const o = localStorage.getItem('snakeOwnedSkins');
    if (o){
      const arr = JSON.parse(o);
      if (Array.isArray(arr)) ownedSkins = new Set(arr.filter(n => Number.isInteger(n) && n>=0 && n<SKINS.length));
    }
  }catch(e){}
  for (let i=0;i<SKINS.length;i++){ if (SKINS[i].price === 0) ownedSkins.add(i); }
  if (!isSkinOwned(selectedSkin)) selectedSkin = 27;
}
function updateCoinDisplay(){
  const el = document.getElementById('coinVal');
  if (el) el.textContent = coins;
  const el2 = document.getElementById('skinsCoins');
  if (el2) el2.textContent = coins;
}

/* ====== LOAD SAVED ====== */
try{
  const savedNick = localStorage.getItem('snakeNick');
  const savedSkin = localStorage.getItem('snakeSkin');
  const savedLang = localStorage.getItem('snakeLang');
  const savedMap  = localStorage.getItem('snakeMap');
  if (savedNick) document.getElementById('nickInput').value = savedNick;
  if (savedSkin !== null) selectedSkin = clamp(parseInt(savedSkin,10)||0, 0, SKINS.length-1);
  if (savedLang === 'ru' || savedLang === 'en') LANG = savedLang;
  if (savedMap && MAPS[savedMap]) currentMapKey = savedMap;
}catch(e){}
loadProgress();
currentMap = MAPS[currentMapKey];

/* ====== GRIDS ====== */
const bodyGrid = new Array(BODY_GW*BODY_GW);
for (let i=0;i<bodyGrid.length;i++) bodyGrid[i]=[];
const cellMark = new Int32Array(BODY_GW*BODY_GW);
let frameId = 0;

const foodGrid = new Array(FOOD_GW*FOOD_GW).fill(null);
function foodCellIndex(x,y){ return ((x/FOOD_CELL)|0)*FOOD_GW + ((y/FOOD_CELL)|0); }

/* ====== FOOD ====== */
const FOOD_COLORS = ['#e74c3c','#f39c12','#f1c40f','#2ecc71','#1abc9c','#3498db','#9b59b6','#e84393','#ff7675','#74b9ff','#55efc4','#ffeaa7','#fd79a8','#a29bfe','#81ecec','#fab1a0'];

function spawnFood(x,y,r,v,c){
  if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
  x=clamp(x,15,WORLD-15); y=clamp(y,15,WORLD-15);
  const ci = foodCellIndex(x,y);
  let arr = foodGrid[ci];
  if (!arr){ arr=[]; foodGrid[ci]=arr; }
  const f = {x,y,r,v,c,alive:true,cd:0};
  arr.push(f);
  return f;
}
function spawnRandomFood(){
  const roll = Math.random();
  let r,v;
  if (roll<0.78){ r=rand(4.5,6.5); v=2; }
  else if (roll<0.96){ r=rand(7.5,9.5); v=6; }
  else { r=rand(12,15); v=16; }
  spawnFood(rand(20,WORLD-20), rand(20,WORLD-20), r, v, FOOD_COLORS[randInt(0,FOOD_COLORS.length-1)]);
}
function generateFood(){
  for (let i=0;i<foodGrid.length;i++) foodGrid[i] = null;
  for (let i=0;i<FOOD_COUNT;i++) spawnRandomFood();
}
function countAndCompactFood(){
  let count = 0;
  for (let i=0;i<foodGrid.length;i++){
    const arr = foodGrid[i];
    if (!arr) continue;
    if (arr.length > 64){
      const alive = arr.filter(f => f.alive);
      foodGrid[i] = alive.length ? alive : null;
      count += alive.length;
    } else {
      for (let k=arr.length-1;k>=0;k--){ if (!arr[k].alive) arr.splice(k,1); }
      if (arr.length === 0) foodGrid[i] = null;
      count += arr.length;
    }
  }
  return count;
}
let foodRespawnTimer = 0;
function updateFoodRespawn(dt){
  foodRespawnTimer -= dt;
  if (foodRespawnTimer > 0) return;
  foodRespawnTimer = 0.6;
  const count = countAndCompactFood();
  const toSpawn = Math.min(40, Math.max(0, FOOD_TARGET - count));
  for (let i=0;i<toSpawn;i++) spawnRandomFood();
}

/* ====== DECOS ====== */
function generateDecos(){
  decos = [];
  const M = currentMap;
  for (let i=0;i<M.treeCount;i++) decos.push({t:'tree',x:rand(40,WORLD-40),y:rand(40,WORLD-40),r:rand(34,82),sh:Math.random()});
  for (let i=0;i<M.pondCount;i++) decos.push({t:'pond',x:rand(300,WORLD-300),y:rand(300,WORLD-300),rx:rand(160,420),ry:rand(120,340),rot:rand(0,Math.PI)});
  for (let i=0;i<M.rockCount;i++) decos.push({t:'rock',x:rand(30,WORLD-30),y:rand(30,WORLD-30),r:rand(9,22)});
  if (M.bushCount){
    for (let i=0;i<M.bushCount;i++){
      decos.push({t:'bush',x:rand(30,WORLD-30),y:rand(30,WORLD-30),r:rand(16,36),sh:Math.random()});
    }
  }
  if (M.smallPondCount){
    for (let i=0;i<M.smallPondCount;i++){
      decos.push({t:'smallpond',x:rand(100,WORLD-100),y:rand(100,WORLD-100),r:rand(28,60)});
    }
  }
  if (M.snowmanCount){
    for (let i=0;i<M.snowmanCount;i++){
      decos.push({t:'snowman',x:rand(40,WORLD-40),y:rand(40,WORLD-40),r:rand(20,34)});
    }
  }
  if (M.iceRiverCount){
    for (let i=0;i<M.iceRiverCount;i++){
      const ang = rand(0,Math.PI);
      decos.push({t:'iceriver',x:rand(200,WORLD-200),y:rand(200,WORLD-200),
        len:rand(180,420), wid:rand(24,52), rot:ang});
    }
  }
}

/* ====== SNAKE ====== */
function makeSnake(name, skinIdx, isPlayer, x, y){
  const skin = SKINS[skinIdx];
  const startScore = skin.bigStart || 22;
  const s = {
    id:nextId++, name, skin, skinIdx, isPlayer,
    x, y, angle:rand(0,TAU), targetAngle:0,
    score:startScore, hp:skin.hp, hpMax:skin.hp,
    shMax:skin.sh, shield:skin.sh, shieldOn:false,
    stMax:skin.maxS, stamina:skin.maxS, sprinting:false,
    alive:true,
    path:[{x,y}], pathLen:0,
    body:new Float32Array(MAX_BODY*2),
    maxBody:12, nBody:1, r:10, dStep:5.5,
    acd:0, slowT:0, confuseT:0, dashT:0, burnT:0, burnDps:0, zigT:0, zigAngle:0,
    invuln:0, hitFlash:0, speed:BASE_SPEED,
    aiTimer:rand(0,0.2), aiState:'farm', aiTarget:null, aiFood:null,
    aiWanderAng:rand(0,TAU), aiWanderT:0,
    aiPreferredSide:(Math.random()<0.5?1:-1),
    aiLastTargetPos:null, aiInterceptT:0, aiSprintT:0, aiShieldT:0,
    personality:{
      aggr:rand(0.25,0.95),
      caution:rand(0.2,0.9),
      greed:rand(0.3,0.9),
      skill:rand(0.6,1.0),
      reaction:rand(0.05,0.16),
      predict:rand(0.6,1.0)
    }
  };
  if (skin.bigStart){
    for (let i=0;i<40;i++){
      s.path.push({x: x - Math.cos(s.angle)*(i+1)*2, y: y - Math.sin(s.angle)*(i+1)*2});
    }
  }
  updateSize(s);
  return s;
}

function growthMul(s){
  return Math.min(1.55, 1 + Math.sqrt(s.score) * 0.011);
}
function rangeMul(s){
  return Math.min(1.25, 1 + Math.sqrt(s.score) * 0.0055);
}

function updateSize(s){
  const prevHpMax = s.hpMax;
  s.r = Math.min(52, 9 + Math.sqrt(s.score)*0.38);
  s.maxBody = Math.min(MAX_BODY, Math.floor(10 + Math.pow(s.score,0.55)*1.25));
  s.dStep = s.r*0.56;
  s.totalLen = (s.maxBody-1)*s.dStep;
  s.hpMax = Math.round(s.skin.hp + Math.sqrt(s.score)*4.5);
  if (s.hpMax > prevHpMax) s.hp += (s.hpMax-prevHpMax);
  s.hp = Math.min(s.hp, s.hpMax);
  s.shMax = Math.round(s.skin.sh + Math.sqrt(s.score)*2.2);
  s.shield = Math.min(s.shMax, s.shield);
}

function addSnake(s){ snakes.push(s); snakeById.set(s.id,s); }
function removeSnake(s){
  const i = snakes.indexOf(s);
  if (i>=0) snakes.splice(i,1);
  snakeById.delete(s.id);
}

/* ====== BODY ====== */
function computeBody(s){
  const out = s.body;
  const maxC = s.maxBody;
  out[0]=s.x; out[1]=s.y;
  let count=1;
  if (maxC<=1){ s.nBody=1; return; }
  let px=s.x, py=s.y, acc=0, target=s.dStep;
  const path = s.path;
  for (let i=path.length-1;i>=0 && count<maxC;i--){
    const p = path[i];
    const dx=p.x-px, dy=p.y-py;
    const seg = Math.sqrt(dx*dx+dy*dy);
    if (seg>1e-6){
      while (acc+seg>=target && count<maxC){
        const t = (target-acc)/seg;
        out[count*2] = px+dx*t;
        out[count*2+1] = py+dy*t;
        count++;
        target += s.dStep;
      }
      acc += seg;
      px=p.x; py=p.y;
    }
  }
  s.nBody = count;
}

function buildBodyGrid(){
  frameId++;
  for (const s of snakes){
    if (!s.alive) continue;
    const n = s.nBody;
    const stride = n>60?2:1;
    for (let i=0;i<n;i+=stride){
      const x=s.body[i*2], y=s.body[i*2+1];
      if (!Number.isFinite(x) || !Number.isFinite(y)) continue;
      const cx=(x/BODY_CELL)|0, cy=(y/BODY_CELL)|0;
      const k = cx*BODY_GW + cy;
      if (k<0||k>=bodyGrid.length) continue;
      if (cellMark[k]!==frameId){ cellMark[k]=frameId; bodyGrid[k].length=0; }
      bodyGrid[k].push(x, y, s.r*0.92, s.id);
    }
  }
}
function pointBlocked(x,y,pr,ignoreId){
  const cx=(x/BODY_CELL)|0, cy=(y/BODY_CELL)|0;
  for (let i=cx-1;i<=cx+1;i++){
    if (i<0||i>=BODY_GW) continue;
    for (let j=cy-1;j<=cy+1;j++){
      if (j<0||j>=BODY_GW) continue;
      const k=i*BODY_GW+j;
      if (cellMark[k]!==frameId) continue;
      const arr=bodyGrid[k];
      for (let m=0;m<arr.length;m+=4){
        if (arr[m+3]===ignoreId) continue;
        const dx=arr[m]-x, dy=arr[m+1]-y;
        const rr=arr[m+2]+pr;
        if (dx*dx+dy*dy<rr*rr) return true;
      }
    }
  }
  return false;
}
function pointHit(x,y,pr,ignoreId){
  const cx=(x/BODY_CELL)|0, cy=(y/BODY_CELL)|0;
  for (let i=cx-1;i<=cx+1;i++){
    if (i<0||i>=BODY_GW) continue;
    for (let j=cy-1;j<=cy+1;j++){
      if (j<0||j>=BODY_GW) continue;
      const k=i*BODY_GW+j;
      if (cellMark[k]!==frameId) continue;
      const arr=bodyGrid[k];
      for (let m=0;m<arr.length;m+=4){
        if (arr[m+3]===ignoreId) continue;
        const dx=arr[m]-x, dy=arr[m+1]-y;
        const rr=arr[m+2]+pr;
        if (dx*dx+dy*dy<rr*rr) return arr[m+3];
      }
    }
  }
  return -1;
}

function snakeHitTest(s, x, y, radius){
  const dx0 = s.x - x, dy0 = s.y - y;
  const rrH = radius + s.r;
  if (dx0*dx0 + dy0*dy0 < rrH*rrH) return true;
  const n = s.nBody;
  if (n <= 1) return false;
  const stride = n > 55 ? 2 : 1;
  const bodyR = s.r * 0.92;
  const rrB = radius + bodyR;
  const rrB2 = rrB * rrB;
  for (let i = stride; i < n; i += stride){
    const dx = s.body[i*2] - x;
    const dy = s.body[i*2+1] - y;
    if (dx*dx + dy*dy < rrB2) return true;
  }
  const ti = (n-1)*2;
  const tdx = s.body[ti] - x;
  const tdy = s.body[ti+1] - y;
  if (tdx*tdx + tdy*tdy < rrB2) return true;
  return false;
}
function snakeDistanceApprox(s, x, y){
  let minD = Math.hypot(s.x - x, s.y - y) - s.r;
  const n = s.nBody;
  const stride = n > 55 ? 2 : 1;
  const bodyR = s.r * 0.92;
  for (let i = stride; i < n; i += stride){
    const dx = s.body[i*2] - x;
    const dy = s.body[i*2+1] - y;
    const d = Math.hypot(dx, dy) - bodyR;
    if (d < minD) minD = d;
  }
  return minD;
}

/* ====== PARTICLES / EFFECTS / TEXT ====== */
function addParticle(x,y,vx,vy,life,r,c){
  if (particles.length>680) return;
  particles.push({x,y,vx,vy,life,max:life,r,c});
}
function burst(x,y,n,c,spd,r){
  for (let i=0;i<n;i++){
    const a=rand(0,TAU), sp=rand(spd*0.4,spd);
    addParticle(x,y,Math.cos(a)*sp,Math.sin(a)*sp,rand(0.3,0.8),rand(r*0.5,r),c);
  }
}
function addTextPop(x, y, text, color){
  textPops.push({x, y, text, color, life:1.4, max:1.4, vy:-55});
}
function updateTextPops(dt){
  for (let i=textPops.length-1;i>=0;i--){
    const p = textPops[i];
    p.life -= dt;
    p.y += p.vy * dt;
    p.vy *= 0.96;
    if (p.life <= 0) textPops.splice(i,1);
  }
}
function drawTextPops(){
  if (!textPops.length) return;
  for (const p of textPops){
    const a = Math.min(1, p.life/p.max * 1.6);
    ctx.globalAlpha = a;
    ctx.font = 'bold 18px sans-serif';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.lineWidth = 4;
    ctx.strokeStyle = 'rgba(0,0,0,0.85)';
    ctx.strokeText(p.text, p.x, p.y);
    ctx.fillStyle = p.color;
    ctx.fillText(p.text, p.x, p.y);
  }
  ctx.globalAlpha = 1;
}

function damageSnake(s, dmg, attacker){
  if (!s.alive || s.invuln>0) return;
  if (s.shieldOn && s.shield>0){
    const resMul = s.skin.shieldRes || 1.0;
    s.shield -= dmg*(1.5/Math.max(0.4,resMul));
    dmg *= 0.28/resMul;
    if (s.shield<=0){ s.shield=0; s.shieldOn=false; }
  }
  s.hp -= dmg;
  s.hitFlash = 0.16;
  if (s.hp<=0) killSnake(s, attacker);
}

function killSnake(s, killer){
  if (!s.alive) return;
  s.alive = false;
  const dropMul = s.skin.foodDropMul || 1;
  const dropN = Math.min(90, Math.floor((8 + Math.floor(s.score/10)) * dropMul));
  const perVal = Math.max(2.5, s.score/dropN*0.7);
  const safeN = Math.max(1, s.nBody);
  for (let i=0;i<dropN;i++){
    const idx = Math.min(safeN-1, Math.floor(Math.random()*safeN));
    const fx = s.body[idx*2];
    const fy = s.body[idx*2+1];
    if (!Number.isFinite(fx) || !Number.isFinite(fy)) continue;
    const f = spawnFood(fx+rand(-14,14), fy+rand(-14,14), s.r*0.45, perVal, s.skin.p);
    if (f) f.cd = 0.35; // short delay before being eaten so traces stay visible
  }
  for (let i=0;i<22;i++){
    const idx = Math.min(safeN-1, Math.floor(Math.random()*safeN));
    const fx = s.body[idx*2], fy = s.body[idx*2+1];
    if (!Number.isFinite(fx) || !Number.isFinite(fy)) continue;
    burst(fx, fy, 2, s.skin.p, 140, 5);
  }
  if (killer && killer.alive && killer !== s) killer.score += s.score*0.32;

  if (killer && killer.isPlayer && s !== killer){
    let reward = 10;
    if (s.score >= 300) reward = 20;
    else if (s.score >= 100) reward = 15;
    coins += reward;
    saveProgress();
    updateCoinDisplay();
    addTextPop(s.x, s.y - 30, '+'+reward+' 🪙', '#ffe45e');
  }

  if (s.isPlayer){
    playerDeathInfo = { score:Math.floor(s.score), killer:killer?killer.name:'—' };
    player = null;
    setTimeout(showDeath, 480);
  }
  removeSnake(s);
  respawnQueue.push(rand(1.4,3.2));
}

/* ====== SOUND ====== */
function initAudio(){
  if (!audioCtx){
    try{ audioCtx = new (window.AudioContext||window.webkitAudioContext)(); }catch(e){}
  } else if (audioCtx.state === 'suspended'){
    audioCtx.resume();
  }
}

function sfx(type, dur, vol){
  if (!type || type === 'none') return;
  initAudio();
  if (!audioCtx) return;
  dur = dur || 0.35;
  const v = (vol === undefined) ? 0.13 : vol;
  const t0 = audioCtx.currentTime;

  const makeOut = (peak) => {
    const out = audioCtx.createGain();
    out.connect(audioCtx.destination);
    out.gain.setValueAtTime(0.0001, t0);
    out.gain.linearRampToValueAtTime(peak, t0+0.015);
    out.gain.linearRampToValueAtTime(peak*0.75, t0+dur*0.6);
    out.gain.linearRampToValueAtTime(0.0001, t0+dur);
    return out;
  };
  const makeNoise = (len) => {
    const buf = audioCtx.createBuffer(1, Math.floor(audioCtx.sampleRate*len), audioCtx.sampleRate);
    const d = buf.getChannelData(0);
    for (let i=0;i<d.length;i++) d[i] = Math.random()*2-1;
    const src = audioCtx.createBufferSource();
    src.buffer = buf;
    return src;
  };

  // === NOISE-BASED ===
  if (type === 'fire' || type === 'spit' || type === 'hiss' || type === 'ice' || type === 'venom' || type === 'splash' || type === 'puff'){
    const out = makeOut(v);
    const src = makeNoise(dur);
    const filt = audioCtx.createBiquadFilter();
    filt.type = 'bandpass';
    if (type === 'fire'){ filt.frequency.value = 750; filt.Q.value = 0.6; }
    else if (type === 'spit'){ filt.frequency.value = 2200; filt.Q.value = 3.5; }
    else if (type === 'hiss'){ filt.frequency.value = 4800; filt.Q.value = 1.1; }
    else if (type === 'ice'){ filt.frequency.value = 3600; filt.Q.value = 6; }
    else if (type === 'venom'){ filt.frequency.value = 1500; filt.Q.value = 1.8; }
    else if (type === 'splash'){ filt.frequency.value = 900; filt.Q.value = 0.7; }
    else if (type === 'puff'){ filt.frequency.value = 500; filt.Q.value = 0.5; }
    src.connect(filt); filt.connect(out);
    src.start(t0); src.stop(t0+dur);
    // For fire add a subtle low crackle layer
    if (type === 'fire'){
      const src2 = makeNoise(dur);
      const f2 = audioCtx.createBiquadFilter();
      f2.type = 'lowpass'; f2.frequency.value = 400;
      const g2 = audioCtx.createGain();
      g2.gain.setValueAtTime(0.0001, t0);
      g2.gain.linearRampToValueAtTime(v*0.7, t0+0.05);
      g2.gain.linearRampToValueAtTime(0.0001, t0+dur);
      src2.connect(f2); f2.connect(g2); g2.connect(audioCtx.destination);
      src2.start(t0); src2.stop(t0+dur);
    }
    return;
  }

  // === TONE-BASED ===
  const out = makeOut(v);

  if (type === 'zap'){
    const o = audioCtx.createOscillator();
    o.type = 'sawtooth';
    o.frequency.setValueAtTime(1400, t0);
    o.frequency.exponentialRampToValueAtTime(70, t0+dur);
    o.connect(out);
    o.start(t0); o.stop(t0+dur);
  } else if (type === 'thunder'){
    // deep rumble + fast zap
    const o = audioCtx.createOscillator();
    o.type = 'sawtooth';
    o.frequency.setValueAtTime(2200, t0);
    o.frequency.exponentialRampToValueAtTime(120, t0+0.08);
    const outA = makeOut(v*0.9);
    o.connect(outA);
    o.start(t0); o.stop(t0+0.12);
    const o2 = audioCtx.createOscillator();
    o2.type = 'sine';
    o2.frequency.setValueAtTime(70, t0+0.05);
    o2.frequency.exponentialRampToValueAtTime(35, t0+dur);
    const outB = makeOut(v*1.2);
    o2.connect(outB);
    o2.start(t0+0.05); o2.stop(t0+dur);
  } else if (type === 'boom'){
    const o = audioCtx.createOscillator();
    o.type = 'sine';
    o.frequency.setValueAtTime(130, t0);
    o.frequency.exponentialRampToValueAtTime(28, t0+dur);
    o.connect(out);
    o.start(t0); o.stop(t0+dur);
  } else if (type === 'punch'){
    // slap-like: short thump + short noise click
    const o = audioCtx.createOscillator();
    o.type = 'sine';
    o.frequency.setValueAtTime(200, t0);
    o.frequency.exponentialRampToValueAtTime(55, t0+dur*0.5);
    const g = audioCtx.createGain();
    g.gain.setValueAtTime(0.0001, t0);
    g.gain.linearRampToValueAtTime(v*1.4, t0+0.005);
    g.gain.linearRampToValueAtTime(0.0001, t0+dur);
    o.connect(g); g.connect(audioCtx.destination);
    o.start(t0); o.stop(t0+dur);
    const src = makeNoise(0.09);
    const f = audioCtx.createBiquadFilter();
    f.type = 'bandpass'; f.frequency.value = 1300; f.Q.value = 1.6;
    const g2 = audioCtx.createGain();
    g2.gain.setValueAtTime(v*1.1, t0);
    g2.gain.exponentialRampToValueAtTime(0.0001, t0+0.09);
    src.connect(f); f.connect(g2); g2.connect(audioCtx.destination);
    src.start(t0); src.stop(t0+0.1);
  } else if (type === 'teleport'){
    const o = audioCtx.createOscillator();
    o.type = 'sine';
    o.frequency.setValueAtTime(180, t0);
    o.frequency.exponentialRampToValueAtTime(1400, t0+dur*0.5);
    o.frequency.exponentialRampToValueAtTime(120, t0+dur);
    o.connect(out);
    o.start(t0); o.stop(t0+dur);
  } else if (type === 'whoosh'){
    const src = makeNoise(dur);
    const filt = audioCtx.createBiquadFilter();
    filt.type = 'bandpass';
    filt.frequency.setValueAtTime(300, t0);
    filt.frequency.exponentialRampToValueAtTime(2600, t0+dur*0.5);
    filt.frequency.exponentialRampToValueAtTime(400, t0+dur);
    filt.Q.value = 1.2;
    src.connect(filt); filt.connect(out);
    src.start(t0); src.stop(t0+dur);
  } else if (type === 'sting'){
    const o = audioCtx.createOscillator();
    o.type = 'sawtooth';
    o.frequency.setValueAtTime(2000, t0);
    o.frequency.exponentialRampToValueAtTime(180, t0+dur);
    o.connect(out);
    o.start(t0); o.stop(t0+dur);
    const src = makeNoise(0.06);
    const f = audioCtx.createBiquadFilter();
    f.type = 'highpass'; f.frequency.value = 2000;
    const g = audioCtx.createGain();
    g.gain.setValueAtTime(v*0.9, t0);
    g.gain.exponentialRampToValueAtTime(0.0001, t0+0.06);
    src.connect(f); f.connect(g); g.connect(audioCtx.destination);
    src.start(t0); src.stop(t0+0.08);
  } else if (type === 'stab'){
    const o = audioCtx.createOscillator();
    o.type = 'triangle';
    o.frequency.setValueAtTime(3000, t0);
    o.frequency.exponentialRampToValueAtTime(600, t0+dur*0.4);
    o.connect(out);
    o.start(t0); o.stop(t0+dur);
  } else if (type === 'crush'){
    // Low grindy noise
    const src = makeNoise(dur);
    const filt = audioCtx.createBiquadFilter();
    filt.type = 'lowpass';
    filt.frequency.setValueAtTime(600, t0);
    filt.frequency.exponentialRampToValueAtTime(120, t0+dur);
    const g = audioCtx.createGain();
    g.gain.setValueAtTime(v*0.9, t0);
    g.gain.linearRampToValueAtTime(0.0001, t0+dur);
    src.connect(filt); filt.connect(g); g.connect(audioCtx.destination);
    src.start(t0); src.stop(t0+dur);
  } else if (type === 'burrow'){
    const o = audioCtx.createOscillator();
    o.type = 'sawtooth';
    o.frequency.setValueAtTime(80, t0);
    o.frequency.exponentialRampToValueAtTime(40, t0+dur);
    const filt = audioCtx.createBiquadFilter();
    filt.type = 'lowpass'; filt.frequency.value = 250;
    const g = audioCtx.createGain();
    g.gain.setValueAtTime(0.0001, t0);
    g.gain.linearRampToValueAtTime(v*0.95, t0+0.05);
    g.gain.linearRampToValueAtTime(0.0001, t0+dur);
    o.connect(filt); filt.connect(g); g.connect(audioCtx.destination);
    o.start(t0); o.stop(t0+dur);
  } else if (type === 'acid'){
    // bubbling
    for (let i=0;i<6;i++){
      const it = t0 + i*0.06;
      const o = audioCtx.createOscillator();
      o.type = 'sine';
      o.frequency.setValueAtTime(rand(180,320), it);
      o.frequency.exponentialRampToValueAtTime(rand(60,120), it+0.09);
      const g = audioCtx.createGain();
      g.gain.setValueAtTime(0.0001, it);
      g.gain.linearRampToValueAtTime(v*0.55, it+0.01);
      g.gain.linearRampToValueAtTime(0.0001, it+0.1);
      o.connect(g); g.connect(audioCtx.destination);
      o.start(it); o.stop(it+0.12);
    }
  } else if (type === 'rattle'){
    // rapid clicks
    for (let i=0;i<10;i++){
      const it = t0 + i*0.032;
      const o = audioCtx.createOscillator();
      o.type = 'square';
      o.frequency.value = 2400 + rand(-250, 250);
      const g = audioCtx.createGain();
      g.gain.setValueAtTime(0.0001, it);
      g.gain.linearRampToValueAtTime(v*0.4, it+0.003);
      g.gain.linearRampToValueAtTime(0.0001, it+0.028);
      o.connect(g); g.connect(audioCtx.destination);
      o.start(it); o.stop(it+0.035);
    }
  } else if (type === 'roar'){
    const roarDur = 1.5;
    const outR = audioCtx.createGain();
    outR.connect(audioCtx.destination);
    outR.gain.setValueAtTime(0.0001, t0);
    outR.gain.linearRampToValueAtTime(v*1.8, t0+0.12);
    outR.gain.linearRampToValueAtTime(v*1.5, t0+0.85);
    outR.gain.linearRampToValueAtTime(0.0001, t0+roarDur);

    const o1 = audioCtx.createOscillator();
    o1.type = 'sawtooth';
    o1.frequency.setValueAtTime(90, t0);
    o1.frequency.linearRampToValueAtTime(65, t0+roarDur*0.7);
    o1.frequency.exponentialRampToValueAtTime(42, t0+roarDur);
    const f1 = audioCtx.createBiquadFilter();
    f1.type = 'lowpass'; f1.frequency.value = 420; f1.Q.value = 3;
    o1.connect(f1); f1.connect(outR);
    o1.start(t0); o1.stop(t0+roarDur);

    const src = makeNoise(roarDur);
    const d = src.buffer.getChannelData(0);
    for (let i=0;i<d.length;i++) d[i] *= (1 - i/d.length) * 0.5;
    const f2 = audioCtx.createBiquadFilter();
    f2.type = 'bandpass'; f2.frequency.value = 180; f2.Q.value = 0.9;
    src.connect(f2); f2.connect(outR);
    src.start(t0); src.stop(t0+roarDur);
  }
}

/* Helper: play ability sound for snake s, if player or near player */
function playAttackSound(s, dur){
  if (!s || !s.alive) return;
  const snd = s.skin.sound;
  if (!snd || snd === 'none') return;
  if (s.isPlayer){
    sfx(snd, dur);
  } else if (player && player.alive){
    const dx = player.x - s.x, dy = player.y - s.y;
    if (dx*dx + dy*dy < SOUND_RADIUS*SOUND_RADIUS){
      // Fade volume by distance so far ones are quieter
      const d = Math.sqrt(dx*dx + dy*dy);
      const vol = 0.13 * Math.max(0.15, 1 - d/SOUND_RADIUS);
      sfx(snd, dur, vol);
    }
  }
}

/* ====== ABILITIES ====== */
function useAbility(s){
  if (!s.alive || s.skin.noAttack || s.acd>0 || s.skin.ab==='none') return;
  if (state !== 'playing' && s.isPlayer) return;
  const ab = s.skin.ab;
  const atk = s.skin.atk * growthMul(s);
  const rm = rangeMul(s);
  const r = s.r;
  const a = s.angle;
  const hx = s.x + Math.cos(a)*r*0.6;
  const hy = s.y + Math.sin(a)*r*0.6;
  const oid = s.id;
  const dur = s.skin.abDur || 1.0;

  switch(ab){
    case 'fire': {
      const range = 220*rm;
      effects.push({k:'cone',o:oid,x:hx,y:hy,ang:a,spread:0.55,range,
        life:dur,max:dur,dmg:36*atk,c:'#ff8c1a',head:true,burn:true});
      s.acd = s.skin.cd;
      burst(hx,hy,14,'#ffb340',140,7);
      playAttackSound(s, dur);
      break;
    }

    case 'tail': {
      const tailAng = a + Math.PI;
      const range = 175*rm;
      const ti = (s.nBody - 1) * 2;
      const tx = s.body[ti] !== undefined ? s.body[ti] : s.x;
      const ty = s.body[ti+1] !== undefined ? s.body[ti+1] : s.y;
      effects.push({k:'cone',o:oid,x:tx,y:ty,ang:tailAng,spread:0.9,range,
        life:dur,max:dur,dmg:50*atk,c:'#d8c878',once:true,slow:0.9,knock:280});
      s.acd = s.skin.cd;
      burst(tx+Math.cos(tailAng)*70, ty+Math.sin(tailAng)*70, 18, '#d8c878', 200, 7);
      playAttackSound(s, dur);
      break;
    }

    case 'vdash':
      s.dashT = dur;
      effects.push({k:'cloud',o:oid,x:s.x,y:s.y,r:55*rm,life:dur+2,max:dur+2,
        dps:18*atk,slow:0.55,c:'#9ee04a',owner:oid,move:true});
      s.acd = s.skin.cd;
      playAttackSound(s, 0.3);
      break;

    case 'spit': {
      const sp = 420;
      effects.push({k:'proj',o:oid,x:hx,y:hy,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,
        r:10,life:1.9,dmg:28*atk,c:'#9ee04a',slow:1.6,aoe:0});
      s.acd = s.skin.cd;
      playAttackSound(s, 0.25);
      break;
    }

    case 'triple': {
      const sp = 400;
      for (let i=-1;i<=1;i++){
        const ang = a + i*0.24;
        effects.push({k:'proj',o:oid,x:hx,y:hy,vx:Math.cos(ang)*sp,vy:Math.sin(ang)*sp,
          r:9,life:1.9,dmg:20*atk,c:'#c07fe0',slow:1.3,aoe:0});
      }
      s.acd = s.skin.cd;
      playAttackSound(s, 0.28);
      break;
    }

    case 'fireball': {
      const sp = 320;
      effects.push({k:'proj',o:oid,x:hx,y:hy,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,
        r:16,life:2.0,dmg:42*atk,c:'#ff7a20',aoe:110*rm,burn:true});
      s.acd = s.skin.cd;
      playAttackSound(s, 0.4);
      break;
    }

    case 'shock':
      effects.push({k:'ring',o:oid,x:s.x,y:s.y,r0:r,r1:270*rm,life:dur,max:dur,
        dmg:30*atk,slow:2.6,knock:180,c:'#ffe066'});
      s.acd = s.skin.cd;
      playAttackSound(s, 0.35);
      break;

    case 'freeze':
      effects.push({k:'ring',o:oid,x:s.x,y:s.y,r0:r,r1:300*rm,life:dur,max:dur,
        dmg:5*atk,slow:3.6,knock:0,c:'#8fe8f0',field:true,radius:300*rm});
      s.acd = s.skin.cd;
      playAttackSound(s, 0.5);
      break;

    case 'sprint':
      s.dashT = dur; s.speedBoost = 2.8;
      burst(s.x,s.y,16,'#7fd97f',220,7);
      s.acd = s.skin.cd;
      playAttackSound(s, 0.3);
      break;

    case 'wave':
      effects.push({k:'ring',o:oid,x:s.x,y:s.y,r0:r,r1:320*rm,life:dur,max:dur,
        dmg:26*atk,slow:1.4,knock:440,c:'#5fd8f0'});
      s.acd = s.skin.cd;
      playAttackSound(s, 0.5);
      break;

    case 'constrict':
      effects.push({k:'pull',o:oid,x:s.x,y:s.y,radius:240*rm,life:dur,max:dur,
        dps:22*atk,force:340,c:'#a89070'});
      s.acd = s.skin.cd;
      playAttackSound(s, 0.6);
      break;

    case 'crush':
      effects.push({k:'pull',o:oid,x:s.x,y:s.y,radius:260*rm,life:dur,max:dur,
        dps:32*atk,force:400,c:'#8a7a5a'});
      s.acd = s.skin.cd;
      playAttackSound(s, 0.6);
      break;

    case 'trail':
      effects.push({k:'cloud',o:oid,x:s.x,y:s.y,r:80*rm,life:dur,max:dur,
        dps:14*atk,slow:0.6,c:'#7a6ad9',owner:oid,move:true});
      s.acd = s.skin.cd;
      playAttackSound(s, 0.3);
      break;

    case 'confuse':
      effects.push({k:'ring',o:oid,x:s.x,y:s.y,r0:r,r1:280*rm,life:dur,max:dur,
        dmg:8*atk,confuse:3.0,knock:0,c:'#f0e050',field:true,radius:280*rm});
      s.acd = s.skin.cd;
      playAttackSound(s, 0.4);
      break;

    case 'lightning': {
      const targets = [];
      for (const o of snakes){
        if (o===s || !o.alive) continue;
        const d = Math.hypot(o.x-s.x, o.y-s.y);
        if (d < 420*rm) targets.push({s:o,d});
      }
      targets.sort((p,q)=>p.d-q.d);
      const hit = targets.slice(0,4);
      let px=s.x, py=s.y;
      for (const tg of hit){
        effects.push({k:'beam',x1:px,y1:py,x2:tg.s.x,y2:tg.s.y,life:0.35,max:0.35,c:'#e8e8ff'});
        damageSnake(tg.s, 32*atk, s);
        burst(tg.s.x,tg.s.y,12,'#e8e8ff',200,6);
        px=tg.s.x; py=tg.s.y;
      }
      s.acd = s.skin.cd;
      playAttackSound(s, 0.25);
      break;
    }

    case 'thunder': {
      const targets = [];
      for (const o of snakes){
        if (o===s || !o.alive) continue;
        const d = Math.hypot(o.x-s.x, o.y-s.y);
        if (d < 480*rm) targets.push({s:o,d});
      }
      targets.sort((p,q)=>p.d-q.d);
      const hit = targets.slice(0,5);
      for (const tg of hit){
        effects.push({k:'bolt',x:tg.s.x,y:tg.s.y,life:0.4,max:0.4,c:'#e0e0ff'});
        damageSnake(tg.s, 28*atk, s);
        burst(tg.s.x,tg.s.y,14,'#e0e0ff',220,6);
      }
      s.acd = s.skin.cd;
      playAttackSound(s, 0.35);
      break;
    }

    case 'poison':
      effects.push({k:'cloud',o:oid,x:hx,y:hy,r:120*rm,life:dur,max:dur,
        dps:15*atk,slow:0.65,c:'#7fd94a',owner:oid});
      s.acd = s.skin.cd;
      playAttackSound(s, 0.4);
      break;

    case 'zigzag':
      s.dashT = dur; s.zigT = 0.28; s.zigAngle = 1; s.speedBoost = 2.0;
      s.acd = s.skin.cd;
      playAttackSound(s, 0.3);
      break;

    case 'maelstrom':
      effects.push({k:'pull',o:oid,x:s.x,y:s.y,radius:280*rm,life:dur,max:dur,
        dps:18*atk,force:360,c:'#50d0c0',spin:true});
      s.acd = s.skin.cd;
      playAttackSound(s, 0.6);
      break;

    case 'teleport': {
      const d = 280*Math.min(1.25, rm);
      const tx = clamp(s.x+Math.cos(a)*d, 40, WORLD-40);
      const ty = clamp(s.y+Math.sin(a)*d, 40, WORLD-40);
      const steps = 12;
      const sx0 = s.x, sy0 = s.y;
      for (let i=1;i<=steps;i++){
        const px = sx0 + (tx-sx0)*i/steps;
        const py = sy0 + (ty-sy0)*i/steps;
        aoeDamage(px, py, s.r+28, oid, 18*atk, null);
        burst(px, py, 2, '#b070ff', 80, 6);
      }
      // translate path
      const dx = tx - s.x, dy = ty - s.y;
      for (const p of s.path){ p.x += dx; p.y += dy; }
      s.x = tx; s.y = ty;
      // push, but skip if identical to last point
      const lastP = s.path[s.path.length-1];
      if (!lastP || Math.hypot(lastP.x-tx, lastP.y-ty) > 0.5) s.path.push({x:tx,y:ty});
      computeBody(s);
      effects.push({k:'ring',o:oid,x:tx,y:ty,r0:10,r1:160*rm,life:dur,max:dur,
        dmg:28*atk,slow:1.5,knock:180,c:'#b070ff'});
      burst(tx,ty,18,'#b070ff',200,7);
      s.acd = s.skin.cd;
      playAttackSound(s, 0.4);
      break;
    }

    case 'sting': {
      const range = 240*rm;
      effects.push({k:'cone',o:oid,x:hx,y:hy,ang:a,spread:0.35,range,
        life:0.5,max:0.5,dmg:52*atk,c:'#ffbb40',once:true,slow:1.5,knock:200});
      s.acd = s.skin.cd;
      playAttackSound(s, 0.25);
      break;
    }

    case 'stink':
      effects.push({k:'ring',o:oid,x:s.x,y:s.y,r0:r,r1:240*rm,life:dur,max:dur,
        dmg:5*atk,confuse:3.5,slow:2.0,knock:0,c:'#c0e0a0',field:true,radius:240*rm});
      s.acd = s.skin.cd;
      playAttackSound(s, 0.5);
      break;

    case 'spikeburst': {
      const rad = 200*rm;
      effects.push({k:'ring',o:oid,x:s.x,y:s.y,r0:r,r1:rad,life:0.5,max:0.5,
        dmg:48*atk,slow:0.8,knock:460,c:'#f0e0c0'});
      for (let i=0;i<24;i++){
        const aa = (i/24)*TAU;
        addParticle(s.x, s.y, Math.cos(aa)*rand(180,340), Math.sin(aa)*rand(180,340), 0.5, 5, '#f0e0c0');
      }
      s.acd = s.skin.cd;
      playAttackSound(s, 0.35);
      break;
    }

    case 'efield': {
      effects.push({k:'cloud',o:oid,x:s.x,y:s.y,r:150*rm,life:dur,max:dur,
        dps:17*atk,slow:0.75,c:'#7fd8ff',owner:oid,move:true,shock:true});
      s.acd = s.skin.cd;
      playAttackSound(s, 0.4);
      break;
    }

    case 'acid': {
      const range = 220*rm;
      effects.push({k:'cone',o:oid,x:hx,y:hy,ang:a,spread:0.5,range,
        life:dur,max:dur,dmg:26*atk,c:'#ffcc30',head:true,burn:true});
      s.acd = s.skin.cd;
      playAttackSound(s, 0.4);
      break;
    }

    case 'icetrail':
      effects.push({k:'cloud',o:oid,x:s.x,y:s.y,r:75*rm,life:dur,max:dur,
        dps:10*atk,slow:0.5,c:'#a0e8ff',owner:oid,move:true});
      s.acd = s.skin.cd;
      playAttackSound(s, 0.3);
      break;

    case 'burrow': {
      const d = 300*Math.min(1.25, rm);
      const tx = clamp(s.x+Math.cos(a)*d, 40, WORLD-40);
      const ty = clamp(s.y+Math.sin(a)*d, 40, WORLD-40);
      const steps = 10;
      const sx0=s.x, sy0=s.y;
      for (let i=1;i<=steps;i++){
        const px = sx0 + (tx-sx0)*i/steps;
        const py = sy0 + (ty-sy0)*i/steps;
        aoeDamage(px, py, s.r+32, oid, 16*atk, null);
        burst(px, py, 3, '#b09060', 100, 7);
      }
      const dx = tx - s.x, dy = ty - s.y;
      for (const p of s.path){ p.x += dx; p.y += dy; }
      s.x = tx; s.y = ty;
      const lastP = s.path[s.path.length-1];
      if (!lastP || Math.hypot(lastP.x-tx, lastP.y-ty) > 0.5) s.path.push({x:tx,y:ty});
      computeBody(s);
      effects.push({k:'ring',o:oid,x:tx,y:ty,r0:10,r1:200*rm,life:0.6,max:0.6,
        dmg:38*atk,slow:1.2,knock:300,c:'#d0b080'});
      burst(tx,ty,20,'#d0b080',240,8);
      s.acd = s.skin.cd;
      playAttackSound(s, 0.45);
      break;
    }

    case 'rebirth': {
      s.hp = Math.min(s.hpMax, s.hp + s.hpMax*0.4);
      s.shield = s.shMax;
      s.shieldOn = true;
      effects.push({k:'ring',o:oid,x:s.x,y:s.y,r0:r,r1:220*rm,life:dur,max:dur,
        dmg:14*atk,slow:1.0,knock:80,c:'#ffb040',burn:true,field:true,radius:200*rm});
      burst(s.x,s.y,30,'#ffcc50',240,9);
      s.acd = s.skin.cd;
      playAttackSound(s, 0.6);
      break;
    }

    case 'roar': {
      const roarRange = 480*rm;
      effects.push({k:'ring',o:oid,x:s.x,y:s.y,r0:s.r,r1:roarRange,life:dur,max:dur,
        dmg:70*atk,slow:2.5,knock:520,c:'#f0a050'});
      burst(s.x,s.y,60,'#ffcc80',420,10);
      burst(s.x,s.y,30,'#ff8030',260,14);
      s.acd = s.skin.cd;
      playAttackSound(s, 1.5);
      break;
    }
  }
}

function aoeDamage(x,y,rad,ownerId,dmg,opts){
  const list = snakes.slice();
  const attacker = snakeById.get(ownerId);
  for (const s of list){
    if (!s.alive || s.id===ownerId) continue;
    if (!snakeHitTest(s, x, y, rad)) continue;
    damageSnake(s, dmg, attacker||null);
    if (opts && s.alive){
      if (opts.slow) s.slowT = Math.max(s.slowT, opts.slow);
      if (opts.confuse) s.confuseT = Math.max(s.confuseT, opts.confuse);
      if (opts.knock){
        const dx=s.x-x, dy=s.y-y;
        const d=Math.hypot(dx,dy)||1;
        s.x = clamp(s.x + dx/d*opts.knock*0.08, 30, WORLD-30);
        s.y = clamp(s.y + dy/d*opts.knock*0.08, 30, WORLD-30);
      }
    }
  }
}

function updateEffects(dt){
  const activeSnakes = snakes.slice();
  for (let i=effects.length-1;i>=0;i--){
    const e = effects[i];
    e.life -= dt;
    if (e.life<=0){ effects.splice(i,1); continue; }

    switch(e.k){
      case 'cone': {
        const o = snakeById.get(e.o);
        if (e.head){
          if (!o || !o.alive){ effects.splice(i,1); continue; }
          e.x = o.x; e.y = o.y; e.ang = o.angle;
          e.range = 220 * rangeMul(o);
        }

        const pointInCone = (px, py, s) => {
          const dx=px-e.x, dy=py-e.y;
          const d = Math.hypot(dx,dy);
          if (d > e.range + s.r) return false;
          const aa = Math.atan2(dy,dx);
          if (Math.abs(angDiff(aa, e.ang)) > e.spread) return false;
          return true;
        };
        const coneHits = (s) => {
          if (pointInCone(s.x, s.y, s)) return true;
          const stride = s.nBody > 40 ? 2 : 1;
          for (let k=0;k<s.nBody;k+=stride){
            if (pointInCone(s.body[k*2], s.body[k*2+1], s)) return true;
          }
          return false;
        };

        if (e.once){
          e.once = false;
          const attacker = snakeById.get(e.o);
          for (const s of activeSnakes){
            if (!s.alive || s.id===e.o) continue;
            if (!coneHits(s)) continue;
            damageSnake(s, e.dmg, attacker||null);
            if (e.slow && s.alive) s.slowT = Math.max(s.slowT, e.slow);
            if (e.knock && s.alive){
              const ddx=s.x-e.x, ddy=s.y-e.y;
              const dd=Math.hypot(ddx,ddy)||1;
              s.x = clamp(s.x + ddx/dd*e.knock*0.09, 30, WORLD-30);
              s.y = clamp(s.y + ddy/dd*e.knock*0.09, 30, WORLD-30);
            }
          }
          continue;
        }

        const dmg = e.dmg * dt;
        for (const s of activeSnakes){
          if (!s.alive || s.id===e.o) continue;
          if (!coneHits(s)) continue;
          damageSnake(s, dmg, o||null);
          if (e.burn && s.alive){
            s.burnT = Math.max(s.burnT, rand(3.0,6.0));
            s.burnDps = 10 * (o?o.skin.atk:1);
          }
          if (Math.random()<0.4) addParticle(s.x,s.y,rand(-40,40),rand(-40,40),0.4,5,'#ffb340');
        }
        if (Math.random()<0.9){
          const aa = e.ang + rand(-e.spread,e.spread);
          const d = rand(20,e.range);
          addParticle(e.x+Math.cos(aa)*d, e.y+Math.sin(aa)*d,
            Math.cos(aa)*rand(40,160), Math.sin(aa)*rand(40,160),
            rand(0.25,0.55), rand(4,10), Math.random()<0.5?'#ff8c1a':'#ffd24a');
        }
        break;
      }

      case 'ring': {
        const prog = 1 - e.life/e.max;
        const rad = e.r0 + (e.r1-e.r0)*prog;
        e.curR = rad;
        if (!e.hitSet) e.hitSet = new Set();
        const attacker = snakeById.get(e.o);
        for (const s of activeSnakes){
          if (!s.alive || s.id===e.o) continue;
          if (e.hitSet.has(s.id)) continue;
          const d = snakeDistanceApprox(s, e.x, e.y);
          if (d > rad) continue;
          e.hitSet.add(s.id);
          damageSnake(s, e.dmg, attacker||null);
          if (!s.alive) continue;
          if (e.slow) s.slowT = Math.max(s.slowT, e.slow);
          if (e.confuse) s.confuseT = Math.max(s.confuseT, e.confuse);
          if (e.knock){
            const dx=s.x-e.x, dy=s.y-e.y;
            const dd=Math.hypot(dx,dy)||1;
            s.x = clamp(s.x + dx/dd*e.knock*0.09, 30, WORLD-30);
            s.y = clamp(s.y + dy/dd*e.knock*0.09, 30, WORLD-30);
          }
          if (e.burn && attacker){
            s.burnT = Math.max(s.burnT, rand(3.0,5.0));
            s.burnDps = 10*attacker.skin.atk;
          }
        }
        if (e.field){
          for (const s of activeSnakes){
            if (!s.alive || s.id===e.o) continue;
            if (!snakeHitTest(s, e.x, e.y, e.radius)) continue;
            s.slowT = Math.max(s.slowT, 0.4);
            if (e.confuse) s.confuseT = Math.max(s.confuseT, 0.5);
          }
        }
        break;
      }

      case 'proj': {
        e.x += e.vx*dt; e.y += e.vy*dt;
        if (e.x<10||e.y<10||e.x>WORLD-10||e.y>WORLD-10){ effects.splice(i,1); continue; }
        let hitSomething=false;
        for (const s of activeSnakes){
          if (!s.alive || s.id===e.o) continue;
          if (!snakeHitTest(s, e.x, e.y, e.r)) continue;
          const atk = snakeById.get(e.o);
          if (e.aoe>0){
            aoeDamage(e.x, e.y, e.aoe, e.o, e.dmg, {slow:1.0});
            burst(e.x,e.y,22,e.c,220,9);
            if (e.burn){
              for (const o of snakes.slice()){
                if (!o.alive || o.id===e.o) continue;
                if (snakeHitTest(o, e.x, e.y, e.aoe)){
                  o.burnT = Math.max(o.burnT, rand(3.0,5.0));
                  o.burnDps = 10*(atk?atk.skin.atk:1);
                }
              }
            }
          } else {
            damageSnake(s, e.dmg, atk||null);
            if (e.slow && s.alive) s.slowT = Math.max(s.slowT, e.slow);
            burst(e.x,e.y,10,e.c,170,7);
          }
          hitSomething=true;
          break;
        }
        if (hitSomething){ effects.splice(i,1); continue; }
        if (Math.random()<0.6) addParticle(e.x,e.y,rand(-20,20),rand(-20,20),0.3,e.r*0.6,e.c);
        break;
      }

      case 'cloud': case 'trail': {
        const atk = snakeById.get(e.o);
        if (e.move && atk && atk.alive){
          e.x = atk.x; e.y = atk.y;
        }
        const age = e.max - e.life;
        const growT = Math.min(1, age / 0.35);
        const effR = e.r * growT;
        e.curR = effR;
        const dmg = e.dps*dt;
        for (const s of activeSnakes){
          if (!s.alive || s.id===e.o) continue;
          if (!snakeHitTest(s, e.x, e.y, effR)) continue;
          damageSnake(s, dmg, atk||null);
          if (s.alive && e.slow) s.slowT = Math.max(s.slowT, e.slow);
        }
        if (Math.random()<0.5){
          const aa=rand(0,TAU), d=rand(0,effR);
          addParticle(e.x+Math.cos(aa)*d, e.y+Math.sin(aa)*d, rand(-15,15), rand(-25,-5), rand(0.5,1.1), rand(5,10), e.c);
        }
        break;
      }

      case 'pull': {
        const atk = snakeById.get(e.o);
        const dmg = e.dps*dt;
        for (const s of activeSnakes){
          if (!s.alive || s.id===e.o) continue;
          if (!snakeHitTest(s, e.x, e.y, e.radius)) continue;
          damageSnake(s, dmg, atk||null);
          if (!s.alive) continue;
          const dx=e.x-s.x, dy=e.y-s.y;
          const d=Math.hypot(dx,dy);
          if (d > 1){
            s.x += dx/d*e.force*dt;
            s.y += dy/d*e.force*dt;
          }
          s.slowT = Math.max(s.slowT, 0.3);
        }
        break;
      }

      case 'beam': case 'bolt': break;
    }
  }
}

function updateParticles(dt){
  for (let i=particles.length-1;i>=0;i--){
    const p = particles[i];
    p.life -= dt;
    if (p.life<=0){ particles.splice(i,1); continue; }
    p.x += p.vx*dt; p.y += p.vy*dt;
    p.vx *= 0.94; p.vy *= 0.94;
  }
}

/* ====== PHYSICS ====== */
function physics(s, dt){
  if (!s.alive) return;

  if (s.acd>0) s.acd -= dt;
  if (s.slowT>0) s.slowT -= dt;
  if (s.confuseT>0) s.confuseT -= dt;
  if (s.invuln>0) s.invuln -= dt;
  if (s.hitFlash>0) s.hitFlash -= dt;

  if (s.burnT>0){
    s.burnT -= dt;
    damageSnake(s, s.burnDps*dt, null);
    if (!s.alive) return;
    if (Math.random()<0.6) addParticle(s.x+rand(-s.r,s.r), s.y+rand(-s.r,s.r),
      rand(-10,10), -rand(20,50), rand(0.4,0.8), rand(4,9),
      Math.random()<0.5?'#ff7a20':'#ffd24a');
    if (s.burnT<=0) s.burnDps=0;
  }

  if (s.shieldOn){
    const durRate = s.skin.shieldDur || 16;
    s.shield -= durRate*dt;
    if (s.shield<=0){ s.shield=0; s.shieldOn=false; }
  } else {
    s.shield = Math.min(s.shMax, s.shield + 9*dt);
  }

  if (s.sprinting && s.stamina > 0){
    s.stamina -= s.skin.drain * dt;
    if (s.stamina < 0) s.stamina = 0;
  } else {
    s.sprinting = false;
    s.stamina = Math.min(s.stMax, s.stamina + s.skin.regen * dt);
  }

  let turnRate = 4.6 - Math.min(2.2, s.r/26);
  if (s.dashT>0) turnRate *= 0.55;

  if (s.zigT>0 && s.dashT>0){
    s.zigT -= dt;
    if (s.zigT<=0){
      s.zigAngle = -s.zigAngle;
      s.zigT = 0.28;
      s.targetAngle = s.angle + s.zigAngle*0.8;
    }
  }

  const diff = angDiff(s.targetAngle, s.angle);
  const maxTurn = turnRate*dt;
  s.angle += clamp(diff, -maxTurn, maxTurn);
  if (s.angle > Math.PI) s.angle -= TAU;
  if (s.angle < -Math.PI) s.angle += TAU;

  let spd = BASE_SPEED * s.skin.sp;
  if (s.slowT>0) spd *= 0.52;
  if (s.sprinting) spd *= s.skin.sprintSpd;
  if (s.dashT>0){
    const boost = s.speedBoost || 2.15;
    spd *= boost;
    s.dashT -= dt;
    if (s.dashT<=0) s.speedBoost = 0;
  }
  s.speed = spd;

  s.x += Math.cos(s.angle)*spd*dt;
  s.y += Math.sin(s.angle)*spd*dt;

  const pad = s.r + 4;
  let hitWall = false;
  if (s.x < pad){ s.x = pad; hitWall = true; }
  if (s.x > WORLD-pad){ s.x = WORLD-pad; hitWall = true; }
  if (s.y < pad){ s.y = pad; hitWall = true; }
  if (s.y > WORLD-pad){ s.y = WORLD-pad; hitWall = true; }
  if (hitWall){
    const toCenter = Math.atan2(WORLD/2-s.y, WORLD/2-s.x);
    s.targetAngle = toCenter;
    s.angle = toCenter;
  }

  // Safety: if somehow we got NaN, snap to center-ish and reset path
  if (!Number.isFinite(s.x) || !Number.isFinite(s.y)){
    s.x = rand(400, WORLD-400);
    s.y = rand(400, WORLD-400);
    s.path = [{x:s.x, y:s.y}];
    s.pathLen = 0;
  }

  const last = s.path[s.path.length-1];
  const dx = s.x-last.x, dy = s.y-last.y;
  const seg = Math.sqrt(dx*dx+dy*dy);
  s.pathLen += seg;
  if (seg > 0.4) s.path.push({x:s.x,y:s.y});
  const need = s.totalLen + s.r*3 + 60;
  while (s.path.length>3){
    const a = s.path[0], b = s.path[1];
    const d = Math.hypot(b.x-a.x, b.y-a.y);
    if (s.pathLen - d > need){ s.pathLen -= d; s.path.shift(); }
    else break;
  }

  computeBody(s);

  if (s.dashT>0){
    for (const o of snakes.slice()){
      if (o===s || !o.alive) continue;
      const ddx=o.x-s.x, ddy=o.y-s.y;
      const dd=Math.hypot(ddx,ddy);
      if (dd < s.r+o.r+6){
        damageSnake(o, 42 * s.skin.atk * growthMul(s) * dt * 3, s);
        if (o.alive) o.slowT = Math.max(o.slowT, 0.5);
      }
    }
  }

  eatFood(s);
}

function eatFood(s){
  const rad = s.r + 16;
  const cx = (s.x/FOOD_CELL)|0, cy = (s.y/FOOD_CELL)|0;
  const growMul = s.skin.growMul || 1;
  for (let i=cx-1;i<=cx+1;i++){
    if (i<0||i>=FOOD_GW) continue;
    for (let j=cy-1;j<=cy+1;j++){
      if (j<0||j>=FOOD_GW) continue;
      const arr = foodGrid[i*FOOD_GW+j];
      if (!arr) continue;
      for (let k=0;k<arr.length;k++){
        const f = arr[k];
        if (!f.alive) continue;
        if (f.cd > 0){ f.cd -= 1/60; continue; }
        const dx=f.x-s.x, dy=f.y-s.y;
        const rr=rad+f.r;
        if (dx*dx+dy*dy < rr*rr){
          f.alive = false;
          s.score += f.v * growMul;
          s.hp = Math.min(s.hpMax, s.hp + f.v*0.5);
          updateSize(s);
          if (Math.random()<0.3) addParticle(f.x,f.y,rand(-30,30),rand(-30,30),0.35,f.r,f.c);
        }
      }
    }
  }
}

/* ====== AI ====== */
function pathClear(s, ang, look){
  const cs = Math.cos(ang), sn = Math.sin(ang);
  const step = 50;
  for (let d=44; d<=look; d+=step){
    const x = s.x+cs*d, y = s.y+sn*d;
    if (x<60||y<60||x>WORLD-60||y>WORLD-60) return false;
    if (pointBlocked(x,y,s.r*0.9,s.id)) return false;
  }
  return true;
}

function aiSteer(s, desired){
  const look = 150 + s.r*2.8 + s.speed*0.6;
  if (pathClear(s, desired, look)) return desired;
  const offsets = [0.28,-0.28,0.55,-0.55,0.9,-0.9,1.3,-1.3,1.75,-1.75,2.3,-2.3,2.9,-2.9,3.14];
  if (s.aiPreferredSide === -1) {
    for (let i=0;i<offsets.length;i++) offsets[i] = -offsets[i];
  }
  for (let i=0;i<offsets.length;i++){
    const a = desired + offsets[i];
    if (pathClear(s, a, look*0.92)) return a;
  }
  return desired + Math.PI + rand(-0.6,0.6);
}

function predictPos(target, s, mySpeed){
  const dist = Math.hypot(target.x-s.x, target.y-s.y);
  const t = dist / Math.max(60, mySpeed);
  const skill = s.personality.predict;
  const lx = target.x + Math.cos(target.angle) * target.speed * t * skill;
  const ly = target.y + Math.sin(target.angle) * target.speed * t * skill;
  const turn = angDiff(target.targetAngle, target.angle);
  const futureAngle = target.angle + turn * Math.min(1, t*1.5);
  const lx2 = target.x + Math.cos(futureAngle) * target.speed * t;
  const ly2 = target.y + Math.sin(futureAngle) * target.speed * t;
  return { x:(lx+lx2)*0.5, y:(ly+ly2)*0.5 };
}

function threatAhead(s, tx, ty){
  const dx = tx - s.x, dy = ty - s.y;
  const d = Math.hypot(dx,dy) || 1;
  const ux = dx/d, uy = dy/d;
  const step = 60;
  for (let dist=80; dist<=d && dist<=600; dist+=step){
    const px = s.x + ux*dist, py = s.y + uy*dist;
    if (pointBlocked(px, py, s.r*1.05, s.id)) return dist;
  }
  return 0;
}

function aiUpdate(s, dt){
  s.aiTimer -= dt;

  const P = s.personality;
  const mySpeed = BASE_SPEED * s.skin.sp * (s.sprinting ? s.skin.sprintSpd : 1);
  const ahead = 70 + s.r*1.7 + s.speed*0.4;
  const ax = s.x + Math.cos(s.angle)*ahead;
  const ay = s.y + Math.sin(s.angle)*ahead;
  const nearEdge = ax<130 || ay<130 || ax>WORLD-130 || ay>WORLD-130;

  if (nearEdge || pointBlocked(ax, ay, s.r*0.95, s.id)){
    const away = Math.atan2(WORLD/2-s.y, WORLD/2-s.x);
    let best = away, bestScore = -1e9;
    for (let i=0;i<20;i++){
      const a = s.angle + (i-10)*0.28;
      if (pathClear(s, a, 200 + s.r*2.6)){
        const sc = Math.cos(angDiff(a, away))*100 - Math.abs(i-10)*2.2;
        if (sc > bestScore){ bestScore=sc; best=a; }
      }
    }
    s.targetAngle = best;
    s.aiTimer = 0;
    s.sprinting = false;
    return;
  }

  if (s.aiTimer > 0) return;
  s.aiTimer = P.reaction + Math.random()*0.04;

  const percept = 1100 * (0.65 + P.skill*0.55);

  let prey = null, preyD = 1e9, preyScore = -1e9;
  let threat = null, threatD = 1e9;
  let nearest = null, nearestD = 1e9;

  for (const o of snakes){
    if (o===s || !o.alive) continue;
    const dx = o.x-s.x, dy = o.y-s.y;
    const d = Math.sqrt(dx*dx+dy*dy);
    if (d < nearestD){ nearestD = d; nearest = o; }
    if (d > percept) continue;
    const ratio = o.score / Math.max(1, s.score);
    if (ratio < 0.8){
      const sc = 900 - d + (1-ratio)*600;
      if (sc > preyScore){ preyScore=sc; prey = o; preyD = d; }
    }
    if (ratio > 1.25){
      if (d < threatD){ threatD = d; threat = o; }
    }
  }

  const hpFrac = s.hp / s.hpMax;
  const fleeRadius = 280 + s.r*4 + P.caution*380;
  const danger = threat && threatD < fleeRadius && (hpFrac < 0.85 || threat.score > s.score*1.8);

  if (danger){
    s.aiState = 'flee';
    let away = Math.atan2(s.y-threat.y, s.x-threat.x);
    if (s.x<700 || s.y<700 || s.x>WORLD-700 || s.y>WORLD-700){
      away = Math.atan2(WORLD/2-s.y, WORLD/2-s.x);
    }
    s.targetAngle = aiSteer(s, away);
    if (s.shield > s.shMax*0.4 && threatD < 400) s.shieldOn = true;
    else s.shieldOn = false;
    if (threatD < 380 && s.stamina > s.stMax*0.25) s.sprinting = true;
    else s.sprinting = false;
    return;
  }
  s.shieldOn = false;

  const huntRange = P.aggr*900 + 150;
  const wantHunt = prey && preyD < huntRange && P.aggr > 0.28;

  if (wantHunt){
    s.aiState = 'hunt';
    const predicted = predictPos(prey, s, mySpeed);
    let ang = Math.atan2(predicted.y-s.y, predicted.x-s.x);

    if (preyD > 200 && preyD < 700 && P.skill > 0.7){
      const perp = Math.atan2(prey.y-s.y, prey.x-s.x) + Math.PI/2 * s.aiPreferredSide;
      const blend = 0.25;
      ang = Math.atan2(
        Math.sin(ang)*(1-blend) + Math.sin(perp)*blend,
        Math.cos(ang)*(1-blend) + Math.cos(perp)*blend
      );
    }

    const dangerDist = threatAhead(s, predicted.x, predicted.y);
    if (dangerDist && dangerDist < 220){
      const perp = ang + Math.PI/2 * s.aiPreferredSide;
      s.targetAngle = aiSteer(s, perp);
    } else {
      s.targetAngle = aiSteer(s, ang);
    }

    if (preyD > 260 && preyD < 700 && s.stamina > s.stMax*0.35){
      s.sprinting = true;
    } else if (preyD < 180){
      s.sprinting = false;
    }

    if (s.acd <= 0 && !s.skin.noAttack){
      const angToPrey = Math.atan2(prey.y-s.y, prey.x-s.x);
      const facing = Math.abs(angDiff(s.angle, angToPrey)) < 0.55;
      const abKind = s.skin.ab;
      const rm = rangeMul(s);
      const isMelee = abKind==='vdash'||abKind==='sprint'||abKind==='zigzag'||abKind==='tail'||abKind==='shock'||abKind==='wave'||abKind==='maelstrom'||abKind==='constrict'||abKind==='crush'||abKind==='sting'||abKind==='spikeburst'||abKind==='burrow'||abKind==='rebirth'||abKind==='stink';
      const isRanged = abKind==='spit'||abKind==='fireball'||abKind==='triple'||abKind==='fire'||abKind==='lightning'||abKind==='thunder'||abKind==='acid';
      const dist = preyD;

      if (facing){
        if (isMelee && dist < 200*rm && dist > 55) useAbility(s);
        else if (isRanged && dist < 360*rm) useAbility(s);
        else if (!isMelee && !isRanged && dist < 300*rm) useAbility(s);
      }
    }
    return;
  }

  s.aiState = 'farm';
  s.sprinting = false;

  let target = null, bestSc = -1e9;
  const frad = 640 + P.greed*440;
  const cx = (s.x/FOOD_CELL)|0, cy = (s.y/FOOD_CELL)|0;
  const span = Math.ceil(frad/FOOD_CELL);
  for (let i=cx-span;i<=cx+span;i++){
    if (i<0||i>=FOOD_GW) continue;
    for (let j=cy-span;j<=cy+span;j++){
      if (j<0||j>=FOOD_GW) continue;
      const arr = foodGrid[i*FOOD_GW+j];
      if (!arr) continue;
      for (let k=0;k<arr.length;k++){
        const f = arr[k];
        if (!f.alive) continue;
        const dx=f.x-s.x, dy=f.y-s.y;
        const d2 = dx*dx+dy*dy;
        if (d2 > frad*frad) continue;
        const sc = f.v*60 - Math.sqrt(d2);
        if (sc > bestSc){ bestSc=sc; target=f; }
      }
    }
  }

  let ang;
  if (target){
    ang = Math.atan2(target.y-s.y, target.x-s.x);
  } else {
    s.aiWanderT -= 0.1;
    if (s.aiWanderT <= 0){ s.aiWanderAng = rand(0,TAU); s.aiWanderT = rand(1.6,3.5); }
    ang = s.aiWanderAng;
  }

  if (threat && threatD < 480){
    const away = Math.atan2(s.y-threat.y, s.x-threat.x);
    ang = Math.atan2(
      Math.sin(ang)*0.55 + Math.sin(away)*0.45,
      Math.cos(ang)*0.55 + Math.cos(away)*0.45
    );
  }
  const edgeMargin = 650;
  if (s.x < edgeMargin || s.y < edgeMargin || s.x > WORLD-edgeMargin || s.y > WORLD-edgeMargin){
    const toCenter = Math.atan2(WORLD/2-s.y, WORLD/2-s.x);
    ang = Math.atan2(
      Math.sin(ang)*0.35 + Math.sin(toCenter)*0.65,
      Math.cos(ang)*0.35 + Math.cos(toCenter)*0.65
    );
  }

  if (nearestD < 60 && nearest){
    ang += 0.4 * s.aiPreferredSide;
  }

  s.targetAngle = aiSteer(s, ang);
}

/* ====== COLLISIONS ====== */
function handleCollisions(){
  const dead = [];
  for (const s of snakes){
    if (!s.alive) continue;
    if (s.invuln > 0) continue; // fresh spawn protection
    const hitId = pointHit(s.x, s.y, s.r*0.82, s.id);
    if (hitId !== -1){
      if (s.shieldOn && s.shield > 0){
        s.shield -= 34;
        if (s.shield < 0) s.shield = 0;
        const other = snakeById.get(hitId);
        if (other){
          const dx=s.x-other.x, dy=s.y-other.y;
          const d=Math.hypot(dx,dy)||1;
          s.x += dx/d*6; s.y += dy/d*6;
        }
        if (Math.random()<0.3) burst(s.x,s.y,3,'#7fd8f0',90,5);
      } else {
        dead.push([s, snakeById.get(hitId)]);
      }
    }
  }
  for (const [s, killer] of dead){
    if (s.alive) killSnake(s, killer);
  }
}

/* ====== CAMERA ====== */
function updateCamera(dt){
  if (!player || !player.alive) return;
  const lead = 70;
  const tx = player.x + Math.cos(player.angle)*lead;
  const ty = player.y + Math.sin(player.angle)*lead;
  const k = Math.min(1, dt*6.5);
  cam.x += (tx-cam.x)*k;
  cam.y += (ty-cam.y)*k;
  cam.zoomCur += (cam.zoom-cam.zoomCur)*Math.min(1, dt*9);
  cam.scale = cam.baseScale * cam.zoomCur;
}

/* ====== SPAWN ====== */
function getPlayerNick(){
  const v = document.getElementById('nickInput').value.trim();
  if (v) return v.slice(0,14);
  return t('nickDefault');
}

function spawnPlayer(){
  const skinIdx = selectedSkin;
  const name = getPlayerNick();
  let x, y, tries=0;
  do {
    x = rand(600, WORLD-600);
    y = rand(600, WORLD-600);
    tries++;
  } while (tries<40 && pointBlocked(x, y, 60, -1));
  const s = makeSnake(name, skinIdx, true, x, y);
  s.invuln = 2.5;
  addSnake(s);
  player = s;
  cam.x = x; cam.y = y;
  cam.zoom = 1; cam.zoomCur = 1;
  cam.scale = cam.baseScale;
}

function spawnBot(){
  let skinIdx;
  let tries = 0;
  do {
    skinIdx = randInt(0, SKINS.length-1);
    tries++;
  } while (tries < 30 && !isSkinAvailableForBots(skinIdx));
  if (!isSkinAvailableForBots(skinIdx)) skinIdx = 0;

  const pool = (LANG==='ru') ? NICKS_RU : NICKS_EN;
  const name = usedNicks.length ? usedNicks.pop() : pool[randInt(0, pool.length-1)];
  let x, y, tries2 = 0;
  do {
    x = rand(180, WORLD-180);
    y = rand(180, WORLD-180);
    tries2++;
  } while (tries2 < 30 && pointBlocked(x, y, 40, -1));
  const s = makeSnake(name, skinIdx, false, x, y);
  s.invuln = 1.5;
  addSnake(s);
}

function setupMatch(){
  snakes = [];
  snakeById.clear();
  effects = [];
  particles = [];
  textPops = [];
  respawnQueue = [];
  nextId = 1;
  // Use FULL shuffled nick pool for the language
  const pool = shuffle((LANG==='ru'?NICKS_RU:NICKS_EN).slice());
  usedNicks = pool.slice();
  spawnPlayer();
  for (let i=0;i<BOT_COUNT;i++) spawnBot();
}

/* ====== RENDER ====== */
function render(){
  ctx.setTransform(dpr,0,0,dpr,0,0);
  if (state !== 'playing' && state !== 'dead'){
    drawMenuBg();
    return;
  }
  ctx.fillStyle = currentMap.ground[0];
  ctx.fillRect(0,0,cw,ch);

  ctx.save();
  ctx.translate(cw/2, ch/2);
  ctx.scale(cam.scale, cam.scale);
  ctx.translate(-cam.x, -cam.y);

  const vw = cw/cam.scale, vh = ch/cam.scale;
  const vx0 = cam.x - vw/2 - 60, vy0 = cam.y - vh/2 - 60;
  const vx1 = cam.x + vw/2 + 60, vy1 = cam.y + vh/2 + 60;

  drawWorld(vx0, vy0, vx1, vy1);
  drawFood(vx0, vy0, vx1, vy1);
  drawEffects(false);
  drawSnakes(vx0, vy0, vx1, vy1);
  drawEffects(true);
  drawParticles();
  drawTextPops();
  drawBorder();

  ctx.restore();

  if (!render._vg){
    render._vg = ctx.createRadialGradient(cw/2, ch/2, Math.min(cw,ch)*0.35, cw/2, ch/2, Math.max(cw,ch)*0.75);
    render._vg.addColorStop(0, 'rgba(0,0,0,0)');
    render._vg.addColorStop(1, 'rgba(0,0,0,0.5)');
  }
  ctx.fillStyle = render._vg;
  ctx.fillRect(0,0,cw,ch);
}

function drawMenuBg(){
  const isWinter = currentMapKey === 'winter';
  const g = ctx.createRadialGradient(cw/2, ch*0.35, 20, cw/2, ch*0.5, Math.max(cw,ch)*0.8);
  if (isWinter){ g.addColorStop(0,'#1a2c3a'); g.addColorStop(1,'#070c12'); }
  else { g.addColorStop(0,'#182a18'); g.addColorStop(1,'#070c07'); }
  ctx.fillStyle = g;
  ctx.fillRect(0,0,cw,ch);
  for (let i=0;i<14;i++){
    const a = time*0.22 + i*1.7;
    const x = cw/2 + Math.cos(a*0.7+i)*(cw*0.45);
    const y = ch/2 + Math.sin(a+i*2.1)*(ch*0.4);
    const r = 30 + (i%4)*24;
    ctx.beginPath();
    ctx.arc(x,y,r,0,TAU);
    if (isWinter) ctx.fillStyle = `rgba(${180+i*3},${200+i*3},${220+i*2},0.12)`;
    else ctx.fillStyle = `rgba(${40+i*6},${90+i*4},${40+i*3},0.14)`;
    ctx.fill();
  }
}

function drawWorld(vx0, vy0, vx1, vy1){
  const M = currentMap;
  const gs = 220;
  const sx = Math.max(0, Math.floor(vx0/gs));
  const sy = Math.max(0, Math.floor(vy0/gs));
  const ex = Math.min(Math.ceil(WORLD/gs), Math.ceil(vx1/gs));
  const ey = Math.min(Math.ceil(WORLD/gs), Math.ceil(vy1/gs));
  for (let i=sx;i<ex;i++){
    for (let j=sy;j<ey;j++){
      const h = ((i*73856093) ^ (j*19349663)) >>> 0;
      ctx.fillStyle = M.ground[h%5];
      ctx.fillRect(i*gs, j*gs, gs+1, gs+1);
    }
  }
  for (let i=0;i<decos.length;i++){
    const d = decos[i];
    if (d.x < vx0-300 || d.x > vx1+300 || d.y < vy0-300 || d.y > vy1+300) continue;
    if (d.t==='pond'){
      ctx.save();
      ctx.translate(d.x, d.y);
      ctx.rotate(d.rot);
      ctx.beginPath(); ctx.ellipse(0,0,d.rx,d.ry,0,0,TAU);
      ctx.fillStyle = M.pond1; ctx.fill();
      ctx.beginPath(); ctx.ellipse(0,0,d.rx*0.86,d.ry*0.86,0,0,TAU);
      ctx.fillStyle = M.pond2; ctx.fill();
      ctx.restore();
    } else if (d.t==='smallpond'){
      ctx.beginPath(); ctx.arc(d.x, d.y, d.r, 0, TAU);
      ctx.fillStyle = M.pond2; ctx.fill();
      ctx.beginPath(); ctx.arc(d.x, d.y, d.r*0.7, 0, TAU);
      ctx.fillStyle = M.pond1; ctx.fill();
      ctx.beginPath(); ctx.arc(d.x + d.r*0.3, d.y - d.r*0.2, d.r*0.28, 0, TAU);
      ctx.fillStyle = 'rgba(60,120,60,0.7)'; ctx.fill();
    } else if (d.t==='tree'){
      const r = d.r;
      ctx.beginPath(); ctx.arc(d.x+r*0.12, d.y+r*0.16, r, 0, TAU);
      ctx.fillStyle = 'rgba(0,0,0,0.32)'; ctx.fill();
      ctx.beginPath(); ctx.arc(d.x, d.y, r, 0, TAU);
      ctx.fillStyle = d.sh>0.5?M.decoTree:M.decoTree2; ctx.fill();
      ctx.beginPath(); ctx.arc(d.x-r*0.28, d.y-r*0.3, r*0.56, 0, TAU);
      ctx.fillStyle = M.decoTree2; ctx.fill();
      ctx.beginPath(); ctx.arc(d.x-r*0.36, d.y-r*0.4, r*0.3, 0, TAU);
      ctx.fillStyle = M.decoTreeHi; ctx.fill();
    } else if (d.t==='bush'){
      const r = d.r;
      for (let k=0;k<3;k++){
        const ax = d.x + (k-1)*r*0.55;
        const ay = d.y + (k%2 ? -r*0.15 : r*0.15);
        ctx.beginPath(); ctx.arc(ax, ay, r, 0, TAU);
        ctx.fillStyle = d.sh>0.5 ? M.bush1 : M.bush2; ctx.fill();
      }
      ctx.beginPath(); ctx.arc(d.x - r*0.5, d.y - r*0.4, r*0.35, 0, TAU);
      ctx.fillStyle = M.bushHi; ctx.globalAlpha = 0.6; ctx.fill(); ctx.globalAlpha = 1;
    } else if (d.t==='snowman'){
      const r = d.r;
      ctx.beginPath(); ctx.arc(d.x, d.y + r*0.7, r*0.7, 0, TAU);
      ctx.fillStyle = '#eef4fa'; ctx.fill();
      ctx.beginPath(); ctx.arc(d.x, d.y + r*0.05, r*0.5, 0, TAU);
      ctx.fillStyle = '#f8fbff'; ctx.fill();
      ctx.beginPath(); ctx.arc(d.x, d.y - r*0.45, r*0.38, 0, TAU);
      ctx.fillStyle = '#ffffff'; ctx.fill();
      ctx.fillStyle = '#222';
      ctx.beginPath(); ctx.arc(d.x-r*0.14, d.y-r*0.5, r*0.06, 0, TAU); ctx.fill();
      ctx.beginPath(); ctx.arc(d.x+r*0.14, d.y-r*0.5, r*0.06, 0, TAU); ctx.fill();
      ctx.fillStyle = '#ff8030';
      ctx.beginPath();
      ctx.moveTo(d.x, d.y-r*0.42);
      ctx.lineTo(d.x+r*0.3, d.y-r*0.36);
      ctx.lineTo(d.x, d.y-r*0.32);
      ctx.closePath(); ctx.fill();
      ctx.fillStyle = '#3a3a3a';
      ctx.fillRect(d.x-r*0.35, d.y-r*0.75, r*0.7, r*0.08);
      ctx.fillRect(d.x-r*0.22, d.y-r*1.05, r*0.44, r*0.32);
    } else if (d.t==='iceriver'){
      ctx.save();
      ctx.translate(d.x, d.y);
      ctx.rotate(d.rot);
      const grad = ctx.createLinearGradient(0, -d.wid, 0, d.wid);
      grad.addColorStop(0, 'rgba(200,230,250,0.75)');
      grad.addColorStop(0.5, 'rgba(150,200,235,0.9)');
      grad.addColorStop(1, 'rgba(180,215,240,0.75)');
      ctx.fillStyle = grad;
      ctx.beginPath();
      ctx.ellipse(0, 0, d.len/2, d.wid/2, 0, 0, TAU);
      ctx.fill();
      ctx.strokeStyle = 'rgba(255,255,255,0.6)';
      ctx.lineWidth = 2;
      for (let k=-1;k<=1;k++){
        ctx.beginPath();
        ctx.moveTo(-d.len/2 + 10, k*d.wid*0.25);
        ctx.lineTo(d.len/2 - 10, k*d.wid*0.25);
        ctx.stroke();
      }
      ctx.restore();
    } else {
      ctx.beginPath(); ctx.arc(d.x, d.y, d.r, 0, TAU);
      ctx.fillStyle = M.rock1; ctx.fill();
      ctx.beginPath(); ctx.arc(d.x-d.r*0.25, d.y-d.r*0.25, d.r*0.55, 0, TAU);
      ctx.fillStyle = M.rock2; ctx.fill();
    }
  }
}

function drawBorder(){
  ctx.lineWidth = 16;
  ctx.strokeStyle = currentMap.border;
  ctx.strokeRect(0,0,WORLD,WORLD);
  ctx.lineWidth = 5;
  ctx.strokeStyle = currentMap.border2;
  ctx.strokeRect(8,8,WORLD-16,WORLD-16);
}

function drawFood(vx0, vy0, vx1, vy1){
  const i0 = Math.max(0, Math.floor(vx0/FOOD_CELL));
  const i1 = Math.min(FOOD_GW-1, Math.ceil(vx1/FOOD_CELL));
  const j0 = Math.max(0, Math.floor(vy0/FOOD_CELL));
  const j1 = Math.min(FOOD_GW-1, Math.ceil(vy1/FOOD_CELL));
  const pulse = 1 + Math.sin(time*5)*0.08;
  for (let i=i0;i<=i1;i++){
    for (let j=j0;j<=j1;j++){
      const arr = foodGrid[i*FOOD_GW+j];
      if (!arr) continue;
      for (let k=0;k<arr.length;k++){
        const f = arr[k];
        if (!f.alive) continue;
        if (f.x<vx0||f.x>vx1||f.y<vy0||f.y>vy1) continue;
        const r = f.v>=16 ? f.r*pulse : f.r;
        if (f.v>=16){
          ctx.beginPath(); ctx.arc(f.x, f.y, r*2.0, 0, TAU);
          ctx.fillStyle = f.c; ctx.globalAlpha = 0.16; ctx.fill(); ctx.globalAlpha = 1;
        } else if (f.v>=6){
          ctx.beginPath(); ctx.arc(f.x, f.y, r*1.6, 0, TAU);
          ctx.fillStyle = f.c; ctx.globalAlpha = 0.13; ctx.fill(); ctx.globalAlpha = 1;
        }
        ctx.beginPath(); ctx.arc(f.x, f.y, r, 0, TAU);
        ctx.fillStyle = f.c; ctx.fill();
        ctx.beginPath(); ctx.arc(f.x-r*0.28, f.y-r*0.3, r*0.34, 0, TAU);
        ctx.fillStyle = 'rgba(255,255,255,0.5)'; ctx.fill();
      }
    }
  }
}

function drawSnakes(vx0, vy0, vx1, vy1){
  for (const s of snakes){
    if (!s.alive) continue;
    if (s.x < vx0-400 || s.x > vx1+400 || s.y < vy0-400 || s.y > vy1+400) continue;
    drawSnake(s);
  }
}

function drawSnake(s){
  const n = s.nBody;
  if (n<1) return;
  const r = s.r;
  const b = s.body;

  ctx.lineJoin = 'round';
  ctx.lineCap = 'round';

  ctx.beginPath();
  ctx.moveTo(b[0], b[1]);
  for (let i=1;i<n;i++) ctx.lineTo(b[i*2], b[i*2+1]);
  if (n===1) ctx.lineTo(b[0]+0.01, b[1]);
  ctx.strokeStyle = s.skin.b2;
  ctx.lineWidth = r*2 + Math.max(3, r*0.34);
  ctx.stroke();

  ctx.strokeStyle = s.hitFlash>0 ? '#ffffff' : s.skin.b;
  ctx.lineWidth = r*2;
  ctx.stroke();

  drawSideSpikes(s, r, n);
  drawScalePattern(s, r, n);
  drawTail(s, r, n);

  // Burger-body decorations (lettuce/tomato/cheese stripes)
  if (s.skin.visual === 'burger'){
    ctx.save();
    // green lettuce line
    ctx.beginPath();
    ctx.moveTo(b[0], b[1]);
    for (let i=1;i<n;i++){
      const t = i/n;
      const px = b[i*2], py = b[i*2+1];
      const off = Math.sin(i*0.7)*r*0.25;
      ctx.lineTo(px + off*0.3, py + off*0.3);
    }
    ctx.strokeStyle = '#6fbf3a';
    ctx.lineWidth = r*0.4;
    ctx.globalAlpha = 0.75;
    ctx.stroke();
    // red tomato line
    ctx.beginPath();
    ctx.moveTo(b[0], b[1]);
    for (let i=1;i<n;i++){
      const px = b[i*2], py = b[i*2+1];
      const off = Math.sin(i*0.7 + 1.2)*r*0.35;
      ctx.lineTo(px + off*0.2, py + off*0.2);
    }
    ctx.strokeStyle = '#d6382a';
    ctx.lineWidth = r*0.28;
    ctx.globalAlpha = 0.8;
    ctx.stroke();
    ctx.globalAlpha = 1;
    ctx.restore();
  }

  ctx.beginPath();
  ctx.moveTo(b[0], b[1]);
  for (let i=1;i<n;i++) ctx.lineTo(b[i*2], b[i*2+1]);
  ctx.strokeStyle = s.skin.p;
  ctx.globalAlpha = 0.45;
  ctx.lineWidth = r*0.55;
  ctx.stroke();
  ctx.globalAlpha = 1;

  if (s.burnT > 0){
    ctx.beginPath();
    ctx.moveTo(b[0], b[1]);
    for (let i=1;i<n;i++) ctx.lineTo(b[i*2], b[i*2+1]);
    ctx.strokeStyle = `rgba(255,140,40,${0.4+Math.sin(time*20)*0.15})`;
    ctx.lineWidth = r*2.3;
    ctx.stroke();
  }

  if (s.shieldOn && s.shield>0){
    ctx.beginPath();
    ctx.moveTo(b[0], b[1]);
    for (let i=1;i<n;i++) ctx.lineTo(b[i*2], b[i*2+1]);
    ctx.strokeStyle = 'rgba(110,220,255,' + (0.25+0.18*Math.sin(time*12)) + ')';
    ctx.lineWidth = r*2.55;
    ctx.stroke();
  }

  if (s.sprinting){
    ctx.beginPath();
    ctx.arc(s.x, s.y, r*1.9, 0, TAU);
    ctx.strokeStyle = 'rgba(255,220,120,0.5)';
    ctx.lineWidth = 3;
    ctx.stroke();
  }

  if (s.slowT > 0){
    ctx.beginPath();
    ctx.arc(s.x, s.y, r*1.7, 0, TAU);
    ctx.strokeStyle = 'rgba(140,230,245,0.55)';
    ctx.lineWidth = 3;
    ctx.stroke();
  }
  if (s.confuseT > 0){
    ctx.beginPath();
    ctx.arc(s.x, s.y, r*2.0, time*4, time*4+1.6);
    ctx.strokeStyle = 'rgba(240,225,80,0.75)';
    ctx.lineWidth = 4;
    ctx.stroke();
  }

  drawHead(s);
}

function drawTail(s, r, n){
  if (n<2) return;
  const b = s.body;
  const tailType = s.skin.tail;
  if (tailType === 'plain') return;
  const ix = (n-1);
  const tx = b[ix*2], ty = b[ix*2+1];
  const px = b[(ix-1)*2], py = b[(ix-1)*2+1];
  const ang = Math.atan2(ty-py, tx-px);

  ctx.save();
  ctx.translate(tx, ty);
  ctx.rotate(ang);

  if (tailType === 'club'){
    ctx.beginPath();
    ctx.arc(0, 0, r*1.35, 0, TAU);
    ctx.fillStyle = s.skin.b2; ctx.fill();
    ctx.strokeStyle = s.skin.p; ctx.lineWidth = Math.max(2, r*0.2); ctx.stroke();
    ctx.fillStyle = s.skin.p;
    for (let i=0;i<6;i++){
      const aa = i/6*TAU;
      ctx.beginPath();
      ctx.moveTo(Math.cos(aa)*r*1.2, Math.sin(aa)*r*1.2);
      ctx.lineTo(Math.cos(aa)*r*1.85, Math.sin(aa)*r*1.85);
      ctx.lineTo(Math.cos(aa+0.3)*r*1.15, Math.sin(aa+0.3)*r*1.15);
      ctx.closePath();
      ctx.fill();
    }
  } else if (tailType === 'blade'){
    ctx.beginPath();
    ctx.moveTo(0, -r*0.6);
    ctx.lineTo(r*2.0, 0);
    ctx.lineTo(0, r*0.6);
    ctx.closePath();
    ctx.fillStyle = s.skin.p; ctx.fill();
    ctx.strokeStyle = s.skin.b2; ctx.lineWidth = 2; ctx.stroke();
  } else if (tailType === 'fan'){
    for (let i=-3;i<=3;i++){
      const a = i*0.18;
      ctx.beginPath();
      ctx.moveTo(0, 0);
      ctx.lineTo(Math.cos(a)*r*1.9, Math.sin(a)*r*1.9);
      ctx.lineTo(Math.cos(a+0.15)*r*1.4, Math.sin(a+0.15)*r*1.4);
      ctx.closePath();
      ctx.fillStyle = s.skin.p; ctx.fill();
    }
  } else if (tailType === 'whip'){
    ctx.beginPath();
    ctx.moveTo(0, 0);
    ctx.quadraticCurveTo(r*1.8, -r*0.7, r*3.4, 0);
    ctx.strokeStyle = s.skin.p;
    ctx.lineWidth = Math.max(2, r*0.35);
    ctx.stroke();
  } else if (tailType === 'spikes'){
    ctx.fillStyle = s.skin.p;
    for (let i=0;i<3;i++){
      const x = i*r*0.5;
      for (const sg of [-1,1]){
        ctx.beginPath();
        ctx.moveTo(x, sg*r*0.4);
        ctx.lineTo(x + r*0.3, sg*r*1.2);
        ctx.lineTo(x + r*0.6, sg*r*0.4);
        ctx.closePath();
        ctx.fill();
      }
    }
  }
  ctx.restore();
}

function drawSideSpikes(s, r, n){
  const sp = s.skin.sideSpikes;
  if (!sp || !sp.count || !sp.len) return;
  const b = s.body;
  const stride = Math.max(2, Math.floor(n / (sp.count*3)));

  for (let i=stride; i<n-1; i+=stride){
    const nx = b[i*2+2] - b[i*2-2];
    const ny = b[i*2+3] - b[i*2-1];
    const len = Math.hypot(nx,ny)||1;
    const perpX = -ny/len, perpY = nx/len;

    for (const sg of [-1,1]){
      const bx = b[i*2] + perpX*sg*r*0.85;
      const by = b[i*2+1] + perpY*sg*r*0.85;
      const ex = b[i*2] + perpX*sg*(r + r*sp.len);
      const ey = b[i*2+1] + perpY*sg*(r + r*sp.len);

      let w;
      if (sp.style === 'round') w = r*0.22;
      else if (sp.style === 'blade') w = r*0.32;
      else w = r*0.26;

      ctx.beginPath();
      ctx.moveTo(bx + (nx/len)*w, by + (ny/len)*w);
      if (sp.style === 'round'){
        ctx.quadraticCurveTo(ex, ey, bx - (nx/len)*w, by - (ny/len)*w);
      } else {
        ctx.lineTo(ex, ey);
        ctx.lineTo(bx - (nx/len)*w, by - (ny/len)*w);
      }
      ctx.closePath();
      ctx.fillStyle = s.skin.p;
      ctx.strokeStyle = s.skin.b2;
      ctx.lineWidth = 1.5;
      ctx.fill();
      ctx.stroke();
    }
  }
}

function drawScalePattern(s, r, n){
  const style = s.skin.scale;
  const b = s.body;
  if (style === 'smooth') return;
  ctx.save();
  const step = style === 'diamond' ? 6 : (style === 'spots' ? 7 : (style === 'glow' ? 5 : 8));
  for (let i=0;i<n;i+=step){
    const x = b[i*2], y = b[i*2+1];
    if (style === 'stripes'){
      ctx.beginPath(); ctx.arc(x, y, r*0.75, 0, TAU);
      ctx.fillStyle = s.skin.b2; ctx.globalAlpha = 0.5; ctx.fill(); ctx.globalAlpha = 1;
    } else if (style === 'spots'){
      ctx.beginPath(); ctx.arc(x, y, r*0.35, 0, TAU);
      ctx.fillStyle = s.skin.p; ctx.globalAlpha = 0.7; ctx.fill(); ctx.globalAlpha = 1;
    } else if (style === 'diamond'){
      ctx.save(); ctx.translate(x, y);
      ctx.beginPath();
      ctx.moveTo(0, -r*0.55); ctx.lineTo(r*0.42, 0);
      ctx.lineTo(0, r*0.55); ctx.lineTo(-r*0.42, 0);
      ctx.closePath();
      ctx.fillStyle = s.skin.p; ctx.globalAlpha = 0.35; ctx.fill();
      ctx.restore(); ctx.globalAlpha = 1;
    } else if (style === 'armor'){
      ctx.beginPath(); ctx.arc(x, y, r*0.85, 0, TAU);
      ctx.strokeStyle = s.skin.b2; ctx.lineWidth = 2;
      ctx.globalAlpha = 0.6; ctx.stroke(); ctx.globalAlpha = 1;
    } else if (style === 'belly'){
      if (i % 2 === 0){
        ctx.beginPath(); ctx.arc(x, y, r*0.5, 0, TAU);
        ctx.fillStyle = 'rgba(255,255,255,0.12)'; ctx.fill();
      }
    } else if (style === 'glow'){
      const pulse = 0.5 + Math.sin(time*3 + i)*0.3;
      ctx.beginPath(); ctx.arc(x, y, r*0.5, 0, TAU);
      ctx.fillStyle = s.skin.p; ctx.globalAlpha = pulse*0.7; ctx.fill(); ctx.globalAlpha = 1;
    }
  }
  ctx.restore();
}

function drawHead(s){
  const r = s.r;
  const a = s.angle;
  const ex = Math.cos(a), ey = Math.sin(a);
  const px = -ey, py = ex;
  const H = s.skin.head;
  const skin = s.skin;
  const isBurger = (skin.visual === 'burger');

  ctx.save();
  ctx.translate(s.x, s.y);
  ctx.rotate(a);

  if (H.frill === 'hood'){
    ctx.beginPath();
    ctx.ellipse(-r*0.4, 0, r*1.35, r*1.55, 0, 0, TAU);
    ctx.fillStyle = skin.b2;
    ctx.fill();
    ctx.strokeStyle = skin.p;
    ctx.lineWidth = Math.max(1.5, r*0.1);
    ctx.stroke();
  }

  let hrx = r*1.1, hry = r*1.1;
  if (H.shape === 'long'){ hrx = r*1.4; hry = r*0.95; }
  else if (H.shape === 'broad'){ hrx = r*1.2; hry = r*1.28; }
  else if (H.shape === 'pointed'){ hrx = r*1.45; hry = r*0.9; }
  else if (H.shape === 'flat'){ hrx = r*1.28; hry = r*0.88; }
  else if (H.shape === 'skull'){ hrx = r*1.18; hry = r*1.08; }
  else if (H.shape === 'round'){ hrx = r*1.05; hry = r*1.05; }

  ctx.beginPath();
  ctx.ellipse(0, 0, hrx, hry, 0, 0, TAU);
  ctx.fillStyle = isBurger ? '#c88040' : skin.h;
  ctx.fill();
  ctx.strokeStyle = isBurger ? '#8a5a20' : skin.b2;
  ctx.lineWidth = Math.max(2, r*0.15);
  ctx.stroke();

  // Burger: lettuce fringe + cheese + tomato + sesame seeds
  if (isBurger){
    // Lettuce (wavy green line)
    ctx.beginPath();
    for (let k=-3;k<=3;k++){
      const ang = k*0.45;
      const lx = Math.cos(ang)*hrx*1.02;
      const ly = Math.sin(ang)*hry*1.02;
      if (k===-3) ctx.moveTo(lx, ly); else ctx.lineTo(lx, ly);
      ctx.lineTo(lx*0.85, ly*0.85);
    }
    ctx.strokeStyle = '#6fbf3a';
    ctx.lineWidth = Math.max(2, r*0.15);
    ctx.stroke();
    // Tomato circle at back
    ctx.beginPath();
    ctx.ellipse(-hrx*0.55, 0, r*0.35, r*0.55, 0, 0, TAU);
    ctx.fillStyle = '#d6382a';
    ctx.globalAlpha = 0.9;
    ctx.fill();
    ctx.globalAlpha = 1;
    // Cheese drip
    ctx.beginPath();
    ctx.moveTo(-hrx*0.2, hry*0.4);
    ctx.lineTo(hrx*0.1, hry*0.85);
    ctx.lineTo(hrx*0.35, hry*0.4);
    ctx.closePath();
    ctx.fillStyle = '#ffd24a';
    ctx.fill();
    // Sesame seeds on top
    ctx.fillStyle = '#fff8e0';
    const seeds = [[-0.55,-0.35],[0.0,-0.55],[0.55,-0.35],[-0.35,0.0],[0.35,0.0],[-0.15,0.35],[0.35,0.35],[-0.6,0.15]];
    for (const [sx, sy] of seeds){
      ctx.beginPath();
      ctx.ellipse(sx*hrx, sy*hry, r*0.14, r*0.08, sx*0.6, 0, TAU);
      ctx.fill();
    }
  }

  if (H.crest === 'ridge'){
    ctx.fillStyle = skin.p;
    for (let i=0;i<4;i++){
      const ax = -r*0.6 + i*r*0.32;
      const hh = r*(0.85 - Math.abs(i-1.5)*0.15);
      ctx.beginPath();
      ctx.moveTo(ax-r*0.1, -hry*0.6);
      ctx.lineTo(ax, -hry*0.6-hh);
      ctx.lineTo(ax+r*0.1, -hry*0.6);
      ctx.closePath(); ctx.fill();
      ctx.beginPath();
      ctx.moveTo(ax-r*0.1, hry*0.6);
      ctx.lineTo(ax, hry*0.6+hh);
      ctx.lineTo(ax+r*0.1, hry*0.6);
      ctx.closePath(); ctx.fill();
    }
  } else if (H.crest === 'plate'){
    ctx.fillStyle = skin.p;
    ctx.beginPath();
    ctx.moveTo(-r*0.6, -hry*0.7);
    ctx.lineTo(-r*0.2, -hry*1.3);
    ctx.lineTo(r*0.2, -hry*0.7);
    ctx.closePath(); ctx.fill();
    ctx.beginPath();
    ctx.moveTo(-r*0.6, hry*0.7);
    ctx.lineTo(-r*0.2, hry*1.3);
    ctx.lineTo(r*0.2, hry*0.7);
    ctx.closePath(); ctx.fill();
  }

  if (H.horns === 2){
    ctx.fillStyle = '#e8e0c0';
    for (const sg of [-1,1]){
      ctx.beginPath();
      ctx.moveTo(-r*0.15, sg*r*0.7);
      ctx.lineTo(-r*0.95, sg*r*1.2);
      ctx.lineTo(-r*0.05, sg*r*0.95);
      ctx.closePath(); ctx.fill();
    }
  } else if (H.horns === 4){
    ctx.fillStyle = '#e8e0c0';
    for (const sg of [-1,1]){
      ctx.beginPath();
      ctx.moveTo(r*0.15, sg*r*0.55);
      ctx.lineTo(r*0.6, sg*r*1.05);
      ctx.lineTo(r*0.35, sg*r*0.5);
      ctx.closePath(); ctx.fill();
      ctx.beginPath();
      ctx.moveTo(-r*0.5, sg*r*0.6);
      ctx.lineTo(-r*1.0, sg*r*1.05);
      ctx.lineTo(-r*0.3, sg*r*0.6);
      ctx.closePath(); ctx.fill();
    }
  } else if (H.horns === 'curve'){
    ctx.strokeStyle = '#e8e0c0';
    ctx.lineWidth = Math.max(3, r*0.18);
    ctx.lineCap = 'round';
    for (const sg of [-1,1]){
      ctx.beginPath();
      ctx.moveTo(-r*0.1, sg*r*0.55);
      ctx.quadraticCurveTo(-r*0.9, sg*r*1.35, -r*1.2, sg*r*0.65);
      ctx.stroke();
    }
  } else if (H.horns === 'straight'){
    ctx.strokeStyle = '#e8e0c0';
    ctx.lineWidth = Math.max(3, r*0.16);
    for (const sg of [-1,1]){
      ctx.beginPath();
      ctx.moveTo(-r*0.15, sg*r*0.7);
      ctx.lineTo(-r*1.1, sg*r*0.7);
      ctx.stroke();
    }
  }

  if (H.teeth === 'fangs' && !isBurger){
    ctx.fillStyle = '#ffffff';
    for (const sg of [-1,1]){
      ctx.beginPath();
      ctx.moveTo(hrx*0.65, sg*r*0.35);
      ctx.lineTo(hrx*1.05, sg*r*0.15);
      ctx.lineTo(hrx*0.7, sg*r*0.5);
      ctx.closePath(); ctx.fill();
    }
  } else if (H.teeth === 'rows' && !isBurger){
    ctx.fillStyle = '#f0f0f0';
    for (const sg of [-1,1]){
      for (let i=0;i<3;i++){
        const tx = hrx*0.4 + i*hrx*0.18;
        ctx.beginPath();
        ctx.moveTo(tx, sg*r*0.35);
        ctx.lineTo(tx+r*0.06, sg*r*0.52);
        ctx.lineTo(tx+r*0.12, sg*r*0.35);
        ctx.closePath(); ctx.fill();
      }
    }
  }

  ctx.restore();

  drawEyes(s, r, ex, ey, px, py);
  if (s.isPlayer || cam.scale * r > 9){
    ctx.font = 'bold ' + Math.max(10, r*0.75) + 'px sans-serif';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.lineWidth = Math.max(2, r*0.18);
    ctx.strokeStyle = 'rgba(0,0,0,0.75)';
    ctx.strokeText(s.name, s.x, s.y - r*1.8);
    ctx.fillStyle = s.isPlayer ? '#ffe45e' : '#e8f0e8';
    ctx.fillText(s.name, s.x, s.y - r*1.8);
  }
}

function drawEyes(s, r, ex, ey, px, py){
  const H = s.skin.head;
  const eyeOff = r*0.58;
  const eyeR = Math.max(2, r*0.30);
  const a2 = s.angle;
  const eType = H.eyes;
  const eyeCol = H.eyeColor || '#ffee55';
  const pupCol = H.pupilColor || '#000';
  const pupShape = H.pupil || 'round';

  for (const sg of [-1,1]){
    const cx = s.x + ex*r*0.42 + px*eyeOff*sg;
    const cy = s.y + ey*r*0.42 + py*eyeOff*sg;

    if (eType === 'angry'){
      ctx.save();
      ctx.translate(cx, cy); ctx.rotate(a2);
      ctx.beginPath(); ctx.ellipse(0, 0, eyeR*1.15, eyeR*0.8, 0, 0, TAU);
      ctx.fillStyle = eyeCol; ctx.fill();
      ctx.strokeStyle = '#200'; ctx.lineWidth = Math.max(1, r*0.1); ctx.stroke();
      ctx.beginPath();
      ctx.moveTo(-eyeR*1.2, -eyeR*sg*1.2);
      ctx.lineTo(eyeR*1.1, -eyeR*sg*0.4);
      ctx.strokeStyle = s.skin.b2;
      ctx.lineWidth = Math.max(3, r*0.2); ctx.stroke();
      drawPupil(eyeR*0.15, 0, eyeR, pupShape, pupCol);
      ctx.restore();
    } else if (eType === 'slit'){
      ctx.beginPath(); ctx.arc(cx, cy, eyeR, 0, TAU);
      ctx.fillStyle = eyeCol; ctx.fill();
      ctx.save(); ctx.translate(cx, cy); ctx.rotate(a2);
      drawPupil(ex*eyeR*0.15, ey*eyeR*0.15, eyeR, pupShape, pupCol);
      ctx.restore();
    } else if (eType === 'glow'){
      ctx.beginPath(); ctx.arc(cx, cy, eyeR*1.4, 0, TAU);
      ctx.fillStyle = eyeCol;
      ctx.globalAlpha = 0.35 + Math.sin(time*6+sg)*0.15;
      ctx.fill(); ctx.globalAlpha = 1;
      ctx.beginPath(); ctx.arc(cx, cy, eyeR, 0, TAU);
      ctx.fillStyle = '#ffffff'; ctx.fill();
      ctx.beginPath(); ctx.arc(cx+ex*eyeR*0.2, cy+ey*eyeR*0.2, eyeR*0.5, 0, TAU);
      ctx.fillStyle = pupCol; ctx.fill();
    } else if (eType === 'small'){
      ctx.beginPath(); ctx.arc(cx, cy, eyeR*0.7, 0, TAU);
      ctx.fillStyle = eyeCol; ctx.fill();
      ctx.beginPath(); ctx.arc(cx+ex*eyeR*0.15, cy+ey*eyeR*0.15, eyeR*0.35, 0, TAU);
      ctx.fillStyle = pupCol; ctx.fill();
    } else if (eType === 'multiple'){
      for (let m=0;m<3;m++){
        const ang = (m-1)*0.35;
        const cx2 = cx + Math.cos(a2+Math.PI/2)*r*0.2*ang*2;
        const cy2 = cy + Math.sin(a2+Math.PI/2)*r*0.2*ang*2;
        ctx.beginPath(); ctx.arc(cx2, cy2, eyeR*0.5, 0, TAU);
        ctx.fillStyle = eyeCol; ctx.fill();
        ctx.beginPath(); ctx.arc(cx2, cy2, eyeR*0.22, 0, TAU);
        ctx.fillStyle = pupCol; ctx.fill();
      }
    } else if (eType === 'sleepy'){
      ctx.save();
      ctx.translate(cx, cy); ctx.rotate(a2);
      ctx.beginPath(); ctx.ellipse(0, 0, eyeR*1.1, eyeR*0.7, 0, 0, TAU);
      ctx.fillStyle = eyeCol; ctx.fill();
      ctx.beginPath(); ctx.ellipse(0, -eyeR*0.4, eyeR*1.2, eyeR*0.55, 0, 0, TAU);
      ctx.fillStyle = s.skin.h; ctx.fill();
      ctx.strokeStyle = s.skin.b2; ctx.lineWidth = Math.max(1.5, r*0.12); ctx.stroke();
      ctx.restore();
    } else if (eType === 'crazy'){
      ctx.beginPath(); ctx.arc(cx, cy, eyeR*1.3, 0, TAU);
      ctx.fillStyle = '#ffffff'; ctx.fill();
      ctx.beginPath(); ctx.arc(cx, cy, eyeR*0.9, 0, TAU);
      ctx.fillStyle = eyeCol; ctx.fill();
      ctx.beginPath(); ctx.arc(cx+ex*eyeR*0.1, cy+ey*eyeR*0.1, eyeR*0.28, 0, TAU);
      ctx.fillStyle = pupCol; ctx.fill();
    } else if (eType === 'heart'){
      ctx.save();
      ctx.translate(cx, cy); ctx.rotate(a2);
      ctx.fillStyle = eyeCol;
      ctx.beginPath();
      ctx.arc(-eyeR*0.4, -eyeR*0.3, eyeR*0.6, 0, TAU); ctx.fill();
      ctx.beginPath();
      ctx.arc(eyeR*0.4, -eyeR*0.3, eyeR*0.6, 0, TAU); ctx.fill();
      ctx.beginPath();
      ctx.moveTo(-eyeR*0.95, -eyeR*0.05);
      ctx.lineTo(eyeR*0.95, -eyeR*0.05);
      ctx.lineTo(0, eyeR*1.0);
      ctx.closePath(); ctx.fill();
      ctx.restore();
    } else {
      ctx.beginPath(); ctx.arc(cx, cy, eyeR, 0, TAU);
      ctx.fillStyle = eyeCol; ctx.fill();
      ctx.beginPath(); ctx.arc(cx+ex*eyeR*0.15, cy+ey*eyeR*0.15, eyeR*0.45, 0, TAU);
      ctx.fillStyle = pupCol; ctx.fill();
    }
  }
}

function drawPupil(x, y, eyeR, shape, color){
  ctx.fillStyle = color;
  if (shape === 'slit'){
    ctx.beginPath();
    ctx.ellipse(x, y, eyeR*0.22, eyeR*0.85, 0, 0, TAU);
    ctx.fill();
  } else if (shape === 'star'){
    const R = eyeR*0.6, r2 = eyeR*0.28;
    ctx.beginPath();
    for (let i=0;i<10;i++){
      const rad = i%2===0 ? R : r2;
      const aa = -Math.PI/2 + i*Math.PI/5;
      const xx = x + Math.cos(aa)*rad;
      const yy = y + Math.sin(aa)*rad;
      if (i===0) ctx.moveTo(xx, yy); else ctx.lineTo(xx, yy);
    }
    ctx.closePath(); ctx.fill();
  } else if (shape === 'cross'){
    ctx.fillRect(x-eyeR*0.15, y-eyeR*0.55, eyeR*0.3, eyeR*1.1);
    ctx.fillRect(x-eyeR*0.55, y-eyeR*0.15, eyeR*1.1, eyeR*0.3);
  } else {
    ctx.beginPath(); ctx.arc(x, y, eyeR*0.45, 0, TAU); ctx.fill();
  }
}

function drawEffects(above){
  for (const e of effects){
    if (!e.k) continue;
    const isAbove = (e.k === 'beam' || e.k === 'bolt');
    if (isAbove !== above) continue;
    const alpha = Math.max(0, e.life/e.max);

    if (e.k === 'ring'){
      let rad;
      if (e.curR !== undefined) rad = e.curR;
      else {
        const prog = 1 - e.life/e.max;
        rad = e.r0 + (e.r1-e.r0)*prog;
      }
      ctx.beginPath(); ctx.arc(e.x, e.y, rad, 0, TAU);
      ctx.strokeStyle = e.c; ctx.globalAlpha = alpha*0.8;
      ctx.lineWidth = Math.max(4, rad*0.12); ctx.stroke();
      ctx.globalAlpha = alpha*0.14;
      ctx.fillStyle = e.c; ctx.fill();
      ctx.globalAlpha = 1;
    } else if (e.k === 'cone'){
      const g = ctx.createRadialGradient(e.x, e.y, 0, e.x, e.y, e.range);
      g.addColorStop(0, 'rgba(255,220,90,0.55)');
      g.addColorStop(0.5, 'rgba(255,130,20,0.3)');
      g.addColorStop(1, 'rgba(255,60,0,0)');
      ctx.beginPath();
      ctx.moveTo(e.x, e.y);
      ctx.arc(e.x, e.y, e.range, e.ang-e.spread, e.ang+e.spread);
      ctx.closePath();
      ctx.fillStyle = g;
      ctx.globalAlpha = alpha;
      ctx.fill();
      ctx.globalAlpha = 1;
    } else if (e.k === 'proj'){
      ctx.beginPath(); ctx.arc(e.x, e.y, e.r*1.8, 0, TAU);
      ctx.fillStyle = e.c; ctx.globalAlpha = 0.22; ctx.fill(); ctx.globalAlpha = 1;
      ctx.beginPath(); ctx.arc(e.x, e.y, e.r, 0, TAU);
      ctx.fillStyle = e.c; ctx.fill();
      ctx.beginPath(); ctx.arc(e.x-e.r*0.25, e.y-e.r*0.25, e.r*0.42, 0, TAU);
      ctx.fillStyle = '#fff'; ctx.globalAlpha = 0.75; ctx.fill(); ctx.globalAlpha = 1;
    } else if (e.k === 'cloud' || e.k === 'trail'){
      const a2 = Math.min(1, alpha*2.4) * 0.34;
      const rad = e.curR !== undefined ? e.curR : e.r;
      if (rad > 1){
        ctx.beginPath(); ctx.arc(e.x, e.y, rad, 0, TAU);
        ctx.fillStyle = e.c; ctx.globalAlpha = a2; ctx.fill();
        ctx.globalAlpha = a2*2;
        ctx.strokeStyle = e.c; ctx.lineWidth = 3; ctx.stroke();
        ctx.globalAlpha = 1;
      }
    } else if (e.k === 'pull'){
      ctx.beginPath(); ctx.arc(e.x, e.y, e.radius*(1-alpha*0.4), 0, TAU);
      ctx.strokeStyle = e.c; ctx.globalAlpha = alpha*0.55;
      ctx.lineWidth = 4; ctx.stroke();
      if (e.spin){
        ctx.beginPath();
        ctx.arc(e.x, e.y, e.radius*(1-alpha*0.4), time*3, time*3+2);
        ctx.strokeStyle = e.c; ctx.lineWidth = 6; ctx.stroke();
      }
      ctx.globalAlpha = 1;
    } else if (e.k === 'beam'){
      ctx.beginPath(); ctx.moveTo(e.x1, e.y1); ctx.lineTo(e.x2, e.y2);
      ctx.strokeStyle = e.c; ctx.globalAlpha = alpha; ctx.lineWidth = 10; ctx.stroke();
      ctx.strokeStyle = '#fff'; ctx.lineWidth = 4; ctx.stroke();
      ctx.globalAlpha = 1;
    } else if (e.k === 'bolt'){
      ctx.save();
      ctx.translate(e.x, e.y);
      ctx.strokeStyle = e.c; ctx.lineWidth = 8; ctx.globalAlpha = alpha;
      ctx.beginPath();
      let cx=0, cy=-400;
      ctx.moveTo(cx, cy);
      for (let i=0;i<8;i++){ cx+=rand(-18,18); cy+=50; ctx.lineTo(cx, cy); }
      ctx.stroke();
      ctx.lineWidth = 3; ctx.strokeStyle = '#fff'; ctx.stroke();
      ctx.restore();
      ctx.globalAlpha = 1;
    }
  }
}

function drawParticles(){
  for (const p of particles){
    const a = Math.max(0, p.life/p.max);
    ctx.globalAlpha = a;
    ctx.beginPath();
    ctx.arc(p.x, p.y, p.r*(0.4+a*0.6), 0, TAU);
    ctx.fillStyle = p.c; ctx.fill();
  }
  ctx.globalAlpha = 1;
}

/* ====== HUD ====== */
const hpfill=document.getElementById('hpfill');
const shfill=document.getElementById('shfill');
const stfill=document.getElementById('stfill');
const hptext=document.getElementById('hptext');
const shtext=document.getElementById('shtext');
const sttext=document.getElementById('sttext');
const scoreval=document.getElementById('scoreval');
const lblist=document.getElementById('lblist');
const lbTitle=document.getElementById('lbTitle');
const atkCdEl=document.getElementById('atkCd');
const atkLblEl=document.getElementById('atkLbl');
const btnAttack=document.getElementById('btnAttack');
const shieldLbl=document.getElementById('shieldLbl');
const sprintLbl=document.getElementById('sprintLbl');

function updateHUD(){
  if (!player || !player.alive) return;
  const hpPct = clamp(player.hp/player.hpMax, 0, 1);
  hpfill.style.width = (hpPct*100)+'%';
  hptext.textContent = Math.max(0, Math.round(player.hp))+' / '+Math.round(player.hpMax);
  const shPct = clamp(player.shield/player.shMax, 0, 1);
  shfill.style.width = (shPct*100)+'%';
  shtext.textContent = Math.round(player.shield)+' / '+Math.round(player.shMax);
  const stPct = clamp(player.stamina/player.stMax, 0, 1);
  stfill.style.width = (stPct*100)+'%';
  sttext.textContent = Math.round(player.stamina)+' / '+Math.round(player.stMax);
  scoreval.textContent = Math.floor(player.score);

  if (player.skin.noAttack){
    btnAttack.classList.add('disabled');
    atkLblEl.textContent = '—';
  } else {
    btnAttack.classList.remove('disabled');
    if (player.acd > 0){
      atkCdEl.classList.remove('hidden');
      atkCdEl.textContent = player.acd.toFixed(1);
    } else {
      atkCdEl.classList.add('hidden');
    }
    atkLblEl.textContent = (player.skin.abn[LANG]||player.skin.abn.ru).split(' ')[0].toUpperCase();
  }

  const sorted = snakes.slice().sort((a,b)=>b.score-a.score);
  let html = '';
  const top = sorted.slice(0, 5);
  for (let i=0;i<top.length;i++){
    const s = top[i];
    const me = (s===player) ? ' me' : '';
    html += '<div class="lbrow'+me+'"><span>'+(i+1)+'. '+escapeHtml(s.name)+
            '</span><span class="sc">'+Math.floor(s.score)+'</span></div>';
  }
  const myIdx = sorted.indexOf(player);
  if (player && myIdx >= 5){
    html += '<div class="lbrow me"><span>'+(myIdx+1)+'. '+escapeHtml(player.name)+
            '</span><span class="sc">'+Math.floor(player.score)+'</span></div>';
  }
  lblist.innerHTML = html;
}

function escapeHtml(t){
  return String(t).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}

/* ====== INPUT ====== */
const joyzone=document.getElementById('joyzone');
const joybase=document.getElementById('joybase');
const joyknob=document.getElementById('joyknob');
let joyPointer=null;
const joyOrigin={x:0,y:0};
const joyVec={x:0,y:0};
const JOY_R=50;

function placeJoyDefault(){
  if (joyPointer !== null) return;
  const w = joyzone.clientWidth || cw*0.52;
  const h = joyzone.clientHeight || ch*0.62;
  const x = Math.min(120, w*0.34);
  const y = h - Math.min(130, h*0.32);
  joybase.style.left = x+'px';
  joybase.style.top = y+'px';
  joyOrigin.x = x; joyOrigin.y = y;
}

joyzone.addEventListener('pointerdown', e => {
  if (state !== 'playing') return;
  e.preventDefault();
  if (joyPointer !== null) return;
  joyPointer = e.pointerId;
  try{ joyzone.setPointerCapture(e.pointerId); }catch(err){}
  const r = joyzone.getBoundingClientRect();
  joyOrigin.x = e.clientX-r.left;
  joyOrigin.y = e.clientY-r.top;
  joybase.style.left = joyOrigin.x+'px';
  joybase.style.top = joyOrigin.y+'px';
  joybase.style.opacity = '1';
  joyknob.style.transform = 'translate(-50%,-50%)';
  joyVec.x=0; joyVec.y=0;
}, {passive:false});

joyzone.addEventListener('pointermove', e => {
  if (e.pointerId !== joyPointer) return;
  e.preventDefault();
  const r = joyzone.getBoundingClientRect();
  let dx = (e.clientX-r.left)-joyOrigin.x;
  let dy = (e.clientY-r.top)-joyOrigin.y;
  const d = Math.hypot(dx,dy);
  if (d > JOY_R){ dx = dx/d*JOY_R; dy = dy/d*JOY_R; }
  joyknob.style.transform = `translate(calc(-50% + ${dx}px), calc(-50% + ${dy}px))`;
  joyVec.x = dx/JOY_R;
  joyVec.y = dy/JOY_R;
}, {passive:false});

function joyEnd(e){
  if (e.pointerId !== joyPointer) return;
  joyPointer = null;
  joyVec.x=0; joyVec.y=0;
  joybase.style.opacity = '0.4';
  joyknob.style.transform = 'translate(-50%,-50%)';
  placeJoyDefault();
}
joyzone.addEventListener('pointerup', joyEnd);
joyzone.addEventListener('pointercancel', joyEnd);

const btnShield=document.getElementById('btnShield');
const btnSprint=document.getElementById('btnSprint');
let shieldHeld=false;
let sprintHeld=false;

btnShield.addEventListener('pointerdown', e => {
  e.preventDefault(); shieldHeld=true;
  if (player && player.alive) player.shieldOn = true;
  btnShield.style.transform = 'scale(0.93)';
}, {passive:false});
function releaseShield(e){
  if (e) e.preventDefault();
  shieldHeld = false;
  if (player) player.shieldOn = false;
  btnShield.style.transform = '';
}
btnShield.addEventListener('pointerup', releaseShield);
btnShield.addEventListener('pointercancel', releaseShield);
btnShield.addEventListener('pointerleave', releaseShield);

btnSprint.addEventListener('pointerdown', e => {
  e.preventDefault(); sprintHeld=true;
  if (player && player.alive) player.sprinting = true;
  btnSprint.style.transform = 'scale(0.9)';
}, {passive:false});
function releaseSprint(e){
  if (e) e.preventDefault();
  sprintHeld = false;
  if (player) player.sprinting = false;
  btnSprint.style.transform = '';
}
btnSprint.addEventListener('pointerup', releaseSprint);
btnSprint.addEventListener('pointercancel', releaseSprint);
btnSprint.addEventListener('pointerleave', releaseSprint);

btnAttack.addEventListener('pointerdown', e => {
  e.preventDefault();
  if (player && player.alive && !player.skin.noAttack) useAbility(player);
  btnAttack.style.transform = 'scale(0.9)';
  setTimeout(()=>btnAttack.style.transform='', 90);
}, {passive:false});

const pointers = new Map();
let pinchStartDist=0, pinchStartZoom=1;
canvas.addEventListener('pointerdown', e => {
  if (state !== 'playing') return;
  pointers.set(e.pointerId, {x:e.clientX, y:e.clientY});
  if (pointers.size===2){
    const p = [...pointers.values()];
    pinchStartDist = Math.hypot(p[0].x-p[1].x, p[0].y-p[1].y);
    pinchStartZoom = cam.zoom;
  }
});
canvas.addEventListener('pointermove', e => {
  if (!pointers.has(e.pointerId)) return;
  pointers.set(e.pointerId, {x:e.clientX, y:e.clientY});
  if (pointers.size===2 && pinchStartDist>0){
    const p = [...pointers.values()];
    const d = Math.hypot(p[0].x-p[1].x, p[0].y-p[1].y);
    cam.zoom = clamp(pinchStartZoom*(d/pinchStartDist), 0.42, 3.0);
  }
});
function canvasUp(e){
  pointers.delete(e.pointerId);
  if (pointers.size<2) pinchStartDist = 0;
}
canvas.addEventListener('pointerup', canvasUp);
canvas.addEventListener('pointercancel', canvasUp);
canvas.addEventListener('pointerout', canvasUp);
canvas.addEventListener('wheel', e => {
  if (state !== 'playing') return;
  e.preventDefault();
  cam.zoom = clamp(cam.zoom*(e.deltaY>0?0.9:1.11), 0.42, 3.0);
}, {passive:false});

const keys = {};
window.addEventListener('keydown', e => {
  keys[e.code] = true;
  if (e.code === 'Space'){ e.preventDefault(); if (player && player.alive && !player.skin.noAttack) useAbility(player); }
  if (e.code === 'ShiftLeft' || e.code === 'ShiftRight'){ if (player) player.shieldOn = true; }
  if (e.code === 'KeyE'){ if (player && player.alive) player.sprinting = true; }
  if (e.code === 'Escape' && state === 'playing'){ toMenu(); }
});
window.addEventListener('keyup', e => {
  keys[e.code] = false;
  if (e.code === 'ShiftLeft' || e.code === 'ShiftRight'){ if (player) player.shieldOn = false; }
  if (e.code === 'KeyE'){ if (player) player.sprinting = false; }
});

function updatePlayerInput(){
  if (!player || !player.alive) return;
  let dx = joyVec.x, dy = joyVec.y;
  if (keys['KeyA']||keys['ArrowLeft']) dx -= 1;
  if (keys['KeyD']||keys['ArrowRight']) dx += 1;
  if (keys['KeyW']||keys['ArrowUp']) dy -= 1;
  if (keys['KeyS']||keys['ArrowDown']) dy += 1;
  const mag = Math.hypot(dx,dy);
  if (mag > 0.14){
    let ang = Math.atan2(dy,dx);
    if (player.confuseT > 0) ang += Math.PI;
    player.targetAngle = ang;
  }
  player.shieldOn = shieldHeld || (player.shieldOn && (keys['ShiftLeft']||keys['ShiftRight']));
  if (!player.skin.noAttack){
    if (keys['KeyE']) player.sprinting = true;
    if (!sprintHeld && !keys['KeyE']) player.sprinting = false;
  } else {
    if (!sprintHeld && !keys['KeyE']) player.sprinting = false;
  }
}

/* ====== MAIN LOOP ====== */
let lastT = 0;

function update(dt){
  time += dt;

  updateFoodRespawn(dt);
  buildBodyGrid();
  updatePlayerInput();
  const snapshot = snakes.slice();
  for (let i=0;i<snapshot.length;i++){
    const s = snapshot[i];
    if (!s.alive) continue;
    if (!s.isPlayer) aiUpdate(s, dt);
    if (!s.alive) continue;
    physics(s, dt);
  }
  buildBodyGrid();
  handleCollisions();
  updateEffects(dt);
  updateParticles(dt);
  updateTextPops(dt);
  updateCamera(dt);

  for (let i=respawnQueue.length-1;i>=0;i--){
    respawnQueue[i] -= dt;
    if (respawnQueue[i] <= 0){
      respawnQueue.splice(i,1);
      if (snakes.length < MAX_SNAKES) spawnBot();
    }
  }
  if (snakes.length < MAX_SNAKES-2 && Math.random() < dt*0.8) spawnBot();

  hudTimer -= dt;
  if (hudTimer<=0){ hudTimer = 0.28; updateHUD(); }
}

function loop(t){
  requestAnimationFrame(loop);
  const dt = Math.min(0.05, (t-lastT)/1000 || 0);
  lastT = t;
  if (state === 'playing') update(dt);
  else time += dt;
  render();
}

/* ====== SCREENS ====== */
const menuEl=document.getElementById('menu');
const hudEl=document.getElementById('hud');
const skinsScreen=document.getElementById('skinsScreen');
const deathScreen=document.getElementById('deathScreen');
const langScreen=document.getElementById('langScreen');
const authorScreen=document.getElementById('authorScreen');

function applyI18N(){
  document.querySelectorAll('[data-i18n]').forEach(el => {
    const k = el.getAttribute('data-i18n');
    const v = t(k);
    if (v) el.textContent = v;
  });
  document.getElementById('nickInput').placeholder = t('nickPlaceholder');
  document.getElementById('skinsTitle').textContent = t('skinsTitle') + ' (' + SKINS.length + ')';
  document.getElementById('lbTitle').textContent = t('top');
  document.getElementById('deathTitle').textContent = t('death');
  document.getElementById('shieldLbl').textContent = t('shield');
  document.getElementById('sprintLbl').textContent = t('sprint');
  document.getElementById('scorebox').childNodes[0].nodeValue = t('scoreHud') + ': ';
  document.getElementById('menu').querySelector('#subtitle').textContent = t('subtitle');
  document.getElementById('authorTitle').textContent = t('authorTitle');
  document.getElementById('authorText').textContent = t('authorText');
  document.getElementById('authorLink').textContent = t('authorLink');
  updateSkinBadge();
  updateCoinDisplay();
  if (player && player.alive) updateHUD();
}

function startGame(){
  initAudio();
  const nick = document.getElementById('nickInput').value.trim();
  try{
    localStorage.setItem('snakeNick', nick);
    localStorage.setItem('snakeSkin', String(selectedSkin));
    localStorage.setItem('snakeLang', LANG);
    localStorage.setItem('snakeMap', currentMapKey);
  }catch(e){}

  menuEl.classList.add('hidden');
  skinsScreen.classList.add('hidden');
  deathScreen.classList.add('hidden');
  langScreen.classList.add('hidden');
  authorScreen.classList.add('hidden');
  hudEl.classList.remove('hidden');
  state = 'playing';
  cam.zoom = 1; cam.zoomCur = 1;
  currentMap = MAPS[currentMapKey];
  generateFood();
  generateDecos();
  setupMatch();
  hudTimer = 0;
  updateHUD();
  placeJoyDefault();
  updateCoinDisplay();
}

function toMenu(){
  state = 'menu';
  hudEl.classList.add('hidden');
  deathScreen.classList.add('hidden');
  skinsScreen.classList.add('hidden');
  langScreen.classList.add('hidden');
  authorScreen.classList.add('hidden');
  menuEl.classList.remove('hidden');
  player = null;
  updateSkinBadge();
  updateCoinDisplay();
  doSaveProgress();
}

function showDeath(){
  if (state !== 'playing') return;
  state = 'dead';
  deathScreen.classList.remove('hidden');
  document.getElementById('deathInfo').innerHTML =
    t('score')+': <b>'+playerDeathInfo.score+'</b><br>'+t('killer')+': '+escapeHtml(playerDeathInfo.killer);
  hudEl.classList.add('hidden');
}

function updateSkinBadge(){
  const badge = document.getElementById('skinBadge');
  if (badge){
    const n = SKINS[selectedSkin].n[LANG] || SKINS[selectedSkin].n.ru;
    badge.textContent = t('skins') + ': ' + n;
  }
}

function renderSkins(){
  const grid = document.getElementById('skinsGrid');
  let html = '';
  for (let i=0;i<SKINS.length;i++){
    const s = SKINS[i];
    const owned = isSkinOwned(i);
    const sel = (i === selectedSkin && owned) ? ' sel' : '';
    const locked = !owned ? ' locked' : '';
    const legendary = (s.price >= 1500) ? ' legendary' : '';
    const abn = s.abn[LANG] || s.abn.ru;
    const desc = s.desc[LANG] || s.desc.ru;
    const name = s.n[LANG] || s.n.ru;

    let priceHtml = '';
    if (owned){
      priceHtml = '<div class="skinPrice owned">✓ '+t('owned')+'</div>';
    } else if (s.price >= 1500){
      priceHtml = '<div class="skinPrice" style="color:#f7d94a">★ '+s.price+' 🪙</div>';
    } else {
      priceHtml = '<div class="skinPrice">🔒 '+s.price+' 🪙</div>';
    }

    html += '<div class="skinCard'+sel+locked+legendary+'" data-idx="'+i+'">' +
      '<div class="skinPrev" style="background:linear-gradient(135deg,'+s.b2+','+s.b+')">' +
        '<div class="skinHead" style="background:'+s.h+';border:2px solid '+s.p+'"></div>' +
      '</div>' +
      '<div class="skinName">'+name+'</div>' +
      priceHtml +
      '<div class="skinAb">'+abn+'</div>' +
      '<div class="skinDesc">'+desc+'</div>' +
      '<div class="skinStats">❤'+s.hp+' · 🛡'+s.sh+' · ⚡'+s.sp.toFixed(2)+'<br>⚔'+s.atk.toFixed(2)+' · 🏃'+s.maxS+'</div>' +
    '</div>';
  }
  grid.innerHTML = html;
  grid.querySelectorAll('.skinCard').forEach(card => {
    card.addEventListener('click', () => {
      const idx = parseInt(card.getAttribute('data-idx'),10);
      const s = SKINS[idx];
      if (isSkinOwned(idx)){
        selectedSkin = idx;
        saveProgress(); // мгновенно сохраняем выбранный скин
        grid.querySelectorAll('.skinCard').forEach(c => c.classList.remove('sel'));
        card.classList.add('sel');
        updateSkinBadge();
      } else {
        if (coins >= s.price){
          coins -= s.price;
          ownedSkins.add(idx);
          selectedSkin = idx;
          saveProgress(); // мгновенно сохраняем покупку
          updateCoinDisplay();
          renderSkins();
          updateSkinBadge();
        } else {
          card.classList.add('cantafford');
          setTimeout(()=>card.classList.remove('cantafford'), 500);
        }
      }
    });
  });
}

function setLang(lang){
  LANG = lang;
  try{ localStorage.setItem('snakeLang', lang); }catch(e){}
  applyI18N();
  if (!skinsScreen.classList.contains('hidden')) renderSkins();
}

/* ====== BINDINGS ====== */
document.getElementById('langRu').addEventListener('click', () => {
  setLang('ru');
  document.getElementById('langScreen').classList.add('hidden');
  document.getElementById('menu').classList.remove('hidden');
  state = 'menu';
  initAudio();
});
document.getElementById('langEn').addEventListener('click', () => {
  setLang('en');
  document.getElementById('langScreen').classList.add('hidden');
  document.getElementById('menu').classList.remove('hidden');
  state = 'menu';
  initAudio();
});
document.getElementById('langBtn').addEventListener('click', () => {
  menuEl.classList.add('hidden');
  langScreen.classList.remove('hidden');
});

document.getElementById('playBtn').addEventListener('click', startGame);
document.getElementById('skinsBtn').addEventListener('click', () => {
  menuEl.classList.add('hidden');
  skinsScreen.classList.remove('hidden');
  renderSkins();
  updateCoinDisplay();
});
document.getElementById('backBtn').addEventListener('click', () => {
  skinsScreen.classList.add('hidden');
  menuEl.classList.remove('hidden');
  updateSkinBadge();
});
document.getElementById('authorBtn').addEventListener('click', () => {
  menuEl.classList.add('hidden');
  authorScreen.classList.remove('hidden');
  updateCoinDisplay();
});
document.getElementById('authorBackBtn').addEventListener('click', () => {
  authorScreen.classList.add('hidden');
  menuEl.classList.remove('hidden');
});
document.getElementById('respawnBtn').addEventListener('click', () => {
  deathScreen.classList.add('hidden');
  hudEl.classList.remove('hidden');
  state = 'playing';
  spawnPlayer();
  updateHUD();
});
document.getElementById('toMenuBtn').addEventListener('click', toMenu);
document.getElementById('menuBtnHud').addEventListener('click', toMenu);

document.querySelectorAll('.chip[data-map]').forEach(chip => {
  chip.addEventListener('click', () => {
    const m = chip.getAttribute('data-map');
    if (!MAPS[m]) return;
    currentMapKey = m;
    currentMap = MAPS[m];
    try{ localStorage.setItem('snakeMap', m); }catch(e){}
    document.querySelectorAll('.chip[data-map]').forEach(c => c.classList.remove('sel'));
    chip.classList.add('sel');
  });
});

const ni = document.getElementById('nickInput');
if (ni) {
  ni.addEventListener('input', () => {
    try{ localStorage.setItem('snakeNick', ni.value.trim()); }catch(e){}
  });
}

/* ====== BOOT ====== */
document.addEventListener('contextmenu', e => e.preventDefault());
document.addEventListener('gesturestart', e => e.preventDefault());
document.addEventListener('dblclick', e => e.preventDefault());

resize();

document.querySelectorAll('.chip[data-map]').forEach(c => {
  c.classList.toggle('sel', c.getAttribute('data-map') === currentMapKey);
});

generateFood();
generateDecos();
buildBodyGrid();

if (localStorage.getItem('snakeLang')){
  document.getElementById('menu').classList.remove('hidden');
  state = 'menu';
  applyI18N();
} else {
  document.getElementById('langScreen').classList.remove('hidden');
  state = 'lang';
}
updateCoinDisplay();

requestAnimationFrame(loop);

document.body.addEventListener('touchmove', e => {
  if (e.target === document.body) e.preventDefault();
}, {passive:false});

window.addEventListener('beforeunload', doSaveProgress);
window.addEventListener('visibilitychange', () => {
  if (document.hidden) doSaveProgress();
});

})();
</script>
</body>
</html>

Game Source: SNAKE CLASH.IO

Creator: CosmicRanger90

Libraries: none

Complexity: complex (3805 lines, 150.8 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: snake-clash-io-cosmicranger90" to link back to the original. Then publish at arcadelab.ai/publish.