🎮ArcadeLab

Magic Loot 2D — Legendary

by LaserLegend64
2355 lines85.5 KB
▶ Play
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
<meta name="theme-color" content="#05070f">
<title>Magic Loot 2D — Legendary</title>
<style>
  *{margin:0;padding:0;box-sizing:border-box;}
  html,body{
    width:100%;height:100%;overflow:hidden;background:#05070f;
    font-family:'Trebuchet MS',system-ui,-apple-system,sans-serif;
    touch-action:none;-webkit-user-select:none;user-select:none;
    -webkit-tap-highlight-color:transparent;color:#fff;
  }
  canvas{display:block;position:fixed;inset:0;image-rendering:pixelated;}
  #loading{
    position:fixed;inset:0;background:#05070f;z-index:100;
    display:flex;flex-direction:column;align-items:center;justify-content:center;gap:20px;
    transition:opacity 0.6s;
  }
  #loading.hide{opacity:0;pointer-events:none;}
  #loading h1{
    font-size:26px;font-weight:900;letter-spacing:2px;
    background:linear-gradient(135deg,#a855f7,#ec4899,#f59e0b);
    -webkit-background-clip:text;background-clip:text;color:transparent;
    animation:pulse 2s infinite;
  }
  @keyframes pulse{0%,100%{opacity:1}50%{opacity:0.6}}
  #loading .sub{font-size:12px;opacity:0.5;letter-spacing:3px;}
  #loadingBar{width:60%;max-width:300px;height:8px;background:rgba(255,255,255,0.1);border-radius:4px;overflow:hidden;}
  #loadingFill{height:100%;background:linear-gradient(90deg,#a855f7,#ec4899);width:0;transition:width 0.3s;}

  #topbar{
    position:fixed;top:0;left:0;right:0;height:48px;
    display:flex;align-items:center;justify-content:space-around;
    background:linear-gradient(180deg,rgba(10,6,20,0.95),rgba(10,6,20,0.6));
    backdrop-filter:blur(12px);
    border-bottom:1px solid rgba(168,85,247,0.35);
    padding:0 4px;z-index:10;font-size:11px;pointer-events:none;
    box-shadow:0 4px 20px rgba(0,0,0,0.6);
  }
  #topbar .stat{display:flex;flex-direction:column;align-items:center;line-height:1.1;min-width:0;flex:1;}
  #topbar .lbl{opacity:0.6;font-size:8px;text-transform:uppercase;letter-spacing:1.5px;font-weight:700;}
  #topbar .val{font-weight:900;font-size:13px;font-family:monospace;text-shadow:0 0 8px currentColor;}

  #bottom{
    position:fixed;bottom:0;left:0;right:0;height:70px;
    display:flex;gap:6px;padding:8px 10px;
    background:linear-gradient(to top, rgba(5,7,15,0.98), rgba(5,7,15,0.7), rgba(5,7,15,0));
    z-index:10;pointer-events:none;
  }
  #bottom .btn{
    flex:1;border:2px solid rgba(255,255,255,0.15);border-radius:14px;
    background:linear-gradient(145deg, rgba(120,90,220,0.9), rgba(70,40,140,0.9));
    color:#fff;font-weight:900;font-size:11px;cursor:pointer;
    pointer-events:auto;
    box-shadow:0 6px 0 rgba(30,10,60,0.9), 0 8px 20px rgba(120,90,220,0.45);
    display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;
    padding:4px;letter-spacing:0.5px;
    transition:transform 0.08s, box-shadow 0.08s;
    font-family:'Trebuchet MS',sans-serif;
    text-shadow:0 1px 2px rgba(0,0,0,0.7);
  }
  #bottom .btn .bi{font-size:19px;line-height:1;filter:drop-shadow(0 2px 3px rgba(0,0,0,0.5));}
  #bottom .btn:active{transform:translateY(4px);box-shadow:0 2px 0 rgba(30,10,60,0.9), 0 3px 10px rgba(120,90,220,0.4);}
  #bottom #btnSell{background:linear-gradient(145deg, rgba(74,222,128,0.95), rgba(22,163,74,0.95));box-shadow:0 6px 0 rgba(15,80,40,0.9), 0 8px 20px rgba(74,222,128,0.45);}
  #bottom #btnSell:active{box-shadow:0 2px 0 rgba(15,80,40,0.9), 0 3px 10px rgba(74,222,128,0.4);}
  #bottom #btnInv{background:linear-gradient(145deg, rgba(59,130,246,0.95), rgba(29,78,216,0.95));box-shadow:0 6px 0 rgba(15,40,100,0.9), 0 8px 20px rgba(59,130,246,0.45);}
  #bottom #btnInv:active{box-shadow:0 2px 0 rgba(15,40,100,0.9), 0 3px 10px rgba(59,130,246,0.4);}
  #bottom #btnTop{background:linear-gradient(145deg, rgba(245,158,11,0.95), rgba(180,83,9,0.95));box-shadow:0 6px 0 rgba(100,50,5,0.9), 0 8px 20px rgba(245,158,11,0.45);}
  #bottom #btnTop:active{box-shadow:0 2px 0 rgba(100,50,5,0.9), 0 3px 10px rgba(245,158,11,0.4);}

  #hpWrap{
    position:fixed;bottom:82px;left:50%;transform:translateX(-50%);
    width:70%;max-width:340px;pointer-events:none;z-index:11;
  }
  #hpLabel{font-size:10px;text-align:center;margin-bottom:3px;font-weight:900;letter-spacing:2px;color:#fda4af;text-shadow:0 0 6px #ef4444;}
  #hpBar{height:14px;background:rgba(0,0,0,0.7);border-radius:7px;overflow:hidden;border:2px solid rgba(239,68,68,0.5);box-shadow:inset 0 2px 6px rgba(0,0,0,0.8), 0 0 12px rgba(239,68,68,0.4);}
  #hpFill{height:100%;background:linear-gradient(90deg,#dc2626,#ef4444,#f59e0b);border-radius:5px;transition:width 0.2s;width:100%;box-shadow:0 0 8px rgba(245,158,11,0.7);}
  #manaWrap{
    position:fixed;bottom:62px;left:50%;transform:translateX(-50%);
    width:70%;max-width:340px;pointer-events:none;z-index:11;
  }
  #manaBar{height:8px;background:rgba(0,0,0,0.7);border-radius:4px;overflow:hidden;border:2px solid rgba(59,130,246,0.4);box-shadow:inset 0 2px 4px rgba(0,0,0,0.8), 0 0 10px rgba(59,130,246,0.3);}
  #manaFill{height:100%;background:linear-gradient(90deg,#1d4ed8,#3b82f6,#22d3ee);border-radius:3px;transition:width 0.15s;width:100%;box-shadow:0 0 8px rgba(34,211,238,0.7);}

  #panel{
    position:fixed;inset:0;z-index:20;
    background:linear-gradient(180deg, rgba(5,7,15,0.98), rgba(20,10,40,0.98));
    backdrop-filter:blur(14px);
    display:flex;flex-direction:column;
    animation:slideUp 0.25s ease-out;
  }
  @keyframes slideUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}
  #panel.hidden{display:none;}
  #panelHeader{
    display:flex;align-items:center;justify-content:space-between;
    padding:10px 12px;border-bottom:2px solid rgba(168,85,247,0.3);
    padding-top:56px;background:linear-gradient(180deg,rgba(30,10,60,0.9),transparent);
  }
  #panelTitle{
    position:absolute;top:14px;left:50%;transform:translateX(-50%);
    font-size:14px;font-weight:900;letter-spacing:3px;
    background:linear-gradient(135deg,#a855f7,#ec4899,#f59e0b);
    -webkit-background-clip:text;background-clip:text;color:transparent;
    text-transform:uppercase;
  }
  #panelTabs{display:flex;gap:5px;overflow-x:auto;flex:1;}
  #panelTabs::-webkit-scrollbar{display:none;}
  #panelTabs .tab{
    border:none;background:rgba(120,90,220,0.15);color:#fff;
    padding:8px 12px;border-radius:8px;font-size:10px;font-weight:800;
    cursor:pointer;white-space:nowrap;letter-spacing:0.5px;
    border:1px solid rgba(120,90,220,0.2);
    transition:all 0.15s;
  }
  #panelTabs .tab.active{
    background:linear-gradient(135deg,rgba(168,85,247,0.9),rgba(236,72,153,0.9));
    border-color:rgba(255,255,255,0.3);
    box-shadow:0 4px 12px rgba(168,85,247,0.5);
    transform:translateY(-1px);
  }
  #closeBtn{
    background:linear-gradient(145deg,rgba(239,68,68,0.9),rgba(180,20,40,0.9));
    border:2px solid rgba(255,255,255,0.15);color:#fff;
    width:36px;height:36px;border-radius:10px;font-size:16px;font-weight:900;
    cursor:pointer;margin-left:8px;
    box-shadow:0 3px 0 rgba(80,10,20,0.9);
  }
  #closeBtn:active{transform:translateY(2px);box-shadow:0 1px 0 rgba(80,10,20,0.9);}
  #panelBody{flex:1;overflow-y:auto;padding:14px;padding-bottom:100px;}
  #panelBody::-webkit-scrollbar{width:6px;}
  #panelBody::-webkit-scrollbar-thumb{background:linear-gradient(180deg,#a855f7,#ec4899);border-radius:3px;}

  .card{
    background:linear-gradient(145deg,rgba(30,15,60,0.9),rgba(15,8,30,0.9));
    border:1px solid rgba(168,85,247,0.25);
    border-radius:14px;padding:12px;margin-bottom:10px;
    display:flex;align-items:center;gap:12px;
    box-shadow:0 4px 14px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.08);
    transition:transform 0.15s, box-shadow 0.15s;
  }
  .card:active{transform:scale(0.99);}
  .card .icon{font-size:26px;flex-shrink:0;filter:drop-shadow(0 0 8px rgba(168,85,247,0.6));}
  .card .info{flex:1;min-width:0;}
  .card .title{font-weight:900;font-size:13px;margin-bottom:2px;text-shadow:0 1px 3px rgba(0,0,0,0.7);}
  .card .sub{font-size:10.5px;opacity:0.7;margin-bottom:4px;line-height:1.4;}
  .card .bar{height:6px;background:rgba(0,0,0,0.5);border-radius:3px;overflow:hidden;box-shadow:inset 0 1px 3px rgba(0,0,0,0.8);}
  .card .bar>div{height:100%;background:linear-gradient(90deg,#a855f7,#ec4899,#f59e0b);border-radius:3px;transition:width 0.3s;box-shadow:0 0 8px rgba(236,72,153,0.7);}
  .card .right{display:flex;flex-direction:column;align-items:flex-end;gap:6px;}
  .card .price{font-size:11px;color:#fcd34d;font-weight:900;font-family:monospace;text-shadow:0 0 6px rgba(252,211,77,0.5);}
  .card .buy{
    border:2px solid rgba(255,255,255,0.2);border-radius:10px;padding:8px 12px;
    background:linear-gradient(145deg,#a855f7,#7c3aed);
    color:#fff;font-weight:900;font-size:11px;cursor:pointer;
    white-space:nowrap;min-width:70px;letter-spacing:0.5px;
    box-shadow:0 3px 0 rgba(60,20,120,0.9), 0 4px 12px rgba(168,85,247,0.4);
    transition:transform 0.08s, box-shadow 0.08s;
  }
  .card .buy:active:not(:disabled){transform:translateY(2px);box-shadow:0 1px 0 rgba(60,20,120,0.9);}
  .card .buy:disabled{background:rgba(60,60,60,0.5);box-shadow:none;cursor:not-allowed;border-color:rgba(255,255,255,0.1);}
  .card .buy.owned{background:linear-gradient(145deg,#22c55e,#15803d);box-shadow:0 3px 0 rgba(10,60,25,0.9), 0 4px 12px rgba(34,197,94,0.4);}
  .card .buy.active{background:linear-gradient(145deg,#22c55e,#15803d);animation:glowGreen 1.5s infinite;}
  @keyframes glowGreen{0%,100%{box-shadow:0 3px 0 rgba(10,60,25,0.9), 0 4px 12px rgba(34,197,94,0.5);}50%{box-shadow:0 3px 0 rgba(10,60,25,0.9), 0 4px 20px rgba(34,197,94,0.9);}}
  .card .buy.select{background:linear-gradient(145deg,#3b82f6,#1d4ed8);box-shadow:0 3px 0 rgba(15,40,100,0.9), 0 4px 12px rgba(59,130,246,0.4);}

  .sectionTitle{
    font-size:10px;text-transform:uppercase;letter-spacing:2.5px;
    opacity:0.55;margin:16px 0 8px;font-weight:900;
    padding-left:8px;border-left:3px solid #a855f7;
  }

  .rarityRow{
    display:flex;align-items:center;gap:8px;padding:9px 12px;
    background:linear-gradient(90deg, rgba(30,15,60,0.7), rgba(15,8,30,0.5));
    border-radius:10px;margin-bottom:6px;
    border-left:4px solid var(--rc, #888);
    box-shadow:0 2px 8px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.05);
    position:relative;overflow:hidden;
  }
  .rarityRow::before{
    content:'';position:absolute;inset:0;
    background:radial-gradient(circle at left, var(--rc), transparent 60%);
    opacity:0.08;pointer-events:none;
  }
  .rarityRow .rn{flex:1;font-weight:900;font-size:12px;color:var(--rc,#fff);text-shadow:0 0 8px var(--rc);}
  .rarityRow .rp{font-size:10px;opacity:0.75;font-variant-numeric:tabular-nums;min-width:60px;text-align:right;font-family:monospace;}
  .rarityRow .rc{font-weight:900;font-size:12px;min-width:44px;text-align:right;font-family:monospace;}
  .rarityRow .sellOne{
    background:linear-gradient(145deg,rgba(34,197,94,0.4),rgba(21,128,61,0.4));
    border:1px solid rgba(74,222,128,0.5);color:#86efac;
    padding:5px 9px;border-radius:8px;font-size:10px;font-weight:900;cursor:pointer;
    transition:transform 0.1s;
  }
  .rarityRow .sellOne:active:not(:disabled){transform:scale(0.93);}
  .rarityRow .sellOne:disabled{opacity:0.25;cursor:not-allowed;}

  .bigBtn{
    width:100%;padding:16px;border:2px solid rgba(255,255,255,0.2);border-radius:14px;
    background:linear-gradient(135deg,#ec4899,#f59e0b);
    color:#fff;font-weight:900;font-size:14px;cursor:pointer;
    box-shadow:0 5px 0 rgba(100,30,80,0.9), 0 8px 24px rgba(236,72,153,0.5);
    margin-bottom:12px;letter-spacing:1.5px;text-shadow:0 2px 4px rgba(0,0,0,0.5);
    transition:transform 0.08s, box-shadow 0.08s;
  }
  .bigBtn:active:not(:disabled){transform:translateY(3px);box-shadow:0 2px 0 rgba(100,30,80,0.9);}
  .bigBtn:disabled{background:linear-gradient(135deg,rgba(80,80,80,0.5),rgba(50,50,50,0.5));box-shadow:none;cursor:not-allowed;border-color:rgba(255,255,255,0.1);}
  .bigBtn.green{background:linear-gradient(135deg,#16a34a,#22c55e);box-shadow:0 5px 0 rgba(10,60,25,0.9), 0 8px 24px rgba(34,197,94,0.5);}
  .bigBtn.green:active:not(:disabled){box-shadow:0 2px 0 rgba(10,60,25,0.9);}

  .hint{font-size:11px;opacity:0.55;text-align:center;padding:12px;line-height:1.6;font-style:italic;}

  .worldRow{
    display:flex;align-items:center;gap:12px;padding:14px;
    background:linear-gradient(145deg,rgba(30,15,60,0.9),rgba(15,8,30,0.9));
    border-radius:14px;margin-bottom:10px;
    border:2px solid var(--wc,#666);
    box-shadow:0 4px 14px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.08);
    position:relative;overflow:hidden;
  }
  .worldRow::before{
    content:'';position:absolute;inset:0;
    background:radial-gradient(circle at right, var(--wc), transparent 60%);
    opacity:0.15;pointer-events:none;
  }
  .worldRow .wIco{font-size:36px;filter:drop-shadow(0 0 10px var(--wc));}
  .worldRow .wI{flex:1;position:relative;}
  .worldRow .wT{font-weight:900;font-size:14px;margin-bottom:4px;text-shadow:0 0 8px var(--wc);}
  .worldRow .wS{font-size:10.5px;opacity:0.75;line-height:1.6;}
  .worldRow .wB{
    border:2px solid rgba(255,255,255,0.2);border-radius:10px;padding:10px 14px;
    background:linear-gradient(145deg,#a855f7,#7c3aed);
    color:#fff;font-weight:900;font-size:11px;cursor:pointer;
    box-shadow:0 3px 0 rgba(60,20,120,0.9);letter-spacing:0.5px;
    transition:transform 0.08s, box-shadow 0.08s;position:relative;
  }
  .worldRow .wB:active:not(:disabled){transform:translateY(2px);box-shadow:0 1px 0 rgba(60,20,120,0.9);}
  .worldRow .wB:disabled{background:rgba(60,60,60,0.5);box-shadow:none;cursor:not-allowed;}

  .leaderRow{
    display:flex;align-items:center;gap:10px;padding:10px 12px;
    background:linear-gradient(90deg,rgba(30,15,60,0.7),rgba(15,8,30,0.5));
    border-radius:10px;margin-bottom:5px;
    font-size:12px;box-shadow:0 2px 8px rgba(0,0,0,0.4);
  }
  .leaderRow.top1{
    background:linear-gradient(90deg,rgba(252,211,77,0.25),rgba(120,70,10,0.15));
    border-left:4px solid #fcd34d;box-shadow:0 0 20px rgba(252,211,77,0.35);
  }
  .leaderRow.top2{background:linear-gradient(90deg,rgba(203,213,225,0.2),rgba(100,110,120,0.1));border-left:4px solid #cbd5e1;}
  .leaderRow.top3{background:linear-gradient(90deg,rgba(180,83,9,0.25),rgba(80,40,5,0.15));border-left:4px solid #b45309;}
  .leaderRow.you{
    background:linear-gradient(90deg,rgba(168,85,247,0.4),rgba(236,72,153,0.3));
    border:2px solid #a855f7;box-shadow:0 0 20px rgba(168,85,247,0.6);
    animation:glowPurple 2s infinite;
  }
  @keyframes glowPurple{0%,100%{box-shadow:0 0 20px rgba(168,85,247,0.6);}50%{box-shadow:0 0 30px rgba(168,85,247,0.9);}}
  .leaderRow .lrank{font-weight:900;min-width:32px;font-size:13px;font-family:monospace;}

  #toast{
    position:fixed;top:56px;left:50%;transform:translateX(-50%) translateY(-10px);
    background:linear-gradient(145deg,rgba(30,15,60,0.98),rgba(15,8,30,0.98));
    border:2px solid rgba(168,85,247,0.5);
    padding:10px 18px;border-radius:12px;font-size:12px;font-weight:900;
    z-index:30;opacity:0;transition:all 0.3s;pointer-events:none;
    max-width:85%;text-align:center;letter-spacing:0.5px;
    box-shadow:0 8px 24px rgba(0,0,0,0.7), 0 0 20px rgba(168,85,247,0.4);
    text-shadow:0 1px 3px rgba(0,0,0,0.8);
  }
  #toast.show{opacity:1;transform:translateX(-50%) translateY(0);}

  #bossWarning{
    position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);
    font-size:32px;font-weight:900;letter-spacing:6px;color:#ef4444;
    text-shadow:0 0 20px #ef4444, 0 0 40px #ef4444, 0 4px 8px rgba(0,0,0,0.9);
    z-index:15;pointer-events:none;opacity:0;text-transform:uppercase;
  }
  #bossWarning.show{animation:bossWarn 2s ease-out forwards;}
  @keyframes bossWarn{
    0%{opacity:0;transform:translate(-50%,-50%) scale(0.5);}
    20%{opacity:1;transform:translate(-50%,-50%) scale(1.1);}
    40%{transform:translate(-50%,-50%) scale(1);}
    80%{opacity:1;}
    100%{opacity:0;transform:translate(-50%,-50%) scale(1.3);}
  }

  @media (min-width:768px){
    #topbar{height:56px;}
    #topbar .val{font-size:15px;}
    #bottom{height:80px;}
    #bottom .btn{font-size:13px;}
    #bottom .btn .bi{font-size:22px;}
    .card .title{font-size:15px;}
    #hpWrap,#manaWrap{max-width:420px;}
  }
</style>
</head>
<body>

<div id="loading">
  <h1>MAGIC LOOT 2D</h1>
  <div class="sub">LEGENDARY EDITION</div>
  <div id="loadingBar"><div id="loadingFill"></div></div>
  <div class="sub" style="font-size:10px">Tap to start</div>
</div>

<canvas id="c"></canvas>

<div id="topbar">
  <div class="stat"><span class="lbl">World</span><span class="val" id="tbWorld" style="color:#22d3ee">1</span></div>
  <div class="stat"><span class="lbl">Level</span><span class="val" id="tbLevel" style="color:#a855f7">1</span></div>
  <div class="stat"><span class="lbl">Rebirth</span><span class="val" id="tbRebirth" style="color:#ec4899">0</span></div>
  <div class="stat"><span class="lbl">Coins</span><span class="val" id="tbCoins" style="color:#fcd34d">0</span></div>
  <div class="stat"><span class="lbl">Time</span><span class="val" id="tbTime" style="color:#a78bfa">0s</span></div>
</div>

<div id="hpWrap">
  <div id="hpLabel">❤️ HP</div>
  <div id="hpBar"><div id="hpFill"></div></div>
</div>
<div id="manaWrap">
  <div id="manaBar"><div id="manaFill"></div></div>
</div>

<div id="bottom">
  <button class="btn" id="btnShop"><span class="bi">🛒</span>SHOP</button>
  <button class="btn" id="btnInv"><span class="bi">🎒</span>BAG</button>
  <button class="btn" id="btnSell"><span class="bi">💰</span>SELL</button>
  <button class="btn" id="btnTop"><span class="bi">🏆</span>TOP</button>
</div>

<div id="panel" class="hidden">
  <div id="panelTitle">MENU</div>
  <div id="panelHeader">
    <div id="panelTabs"></div>
    <button id="closeBtn">✕</button>
  </div>
  <div id="panelBody"></div>
</div>

<div id="toast"></div>
<div id="bossWarning">⚠️ BOSS ⚠️</div>

<script>
(function(){
'use strict';

/* ============================================================
   UTILITIES
============================================================ */
const SUFFIXES = ['','K','M','B','T','Qa','Qi','Sx','Sp','Oc','No','Dc','Ud','Dd','Td','Qad','Qid','Sxd','Spd','Ocd','Nod','Vg','Uvg','Dvg','Tvg','Qavg'];

function fmt(n){
  if(!isFinite(n)) return '∞';
  if(n < 1000) return Math.floor(n).toString();
  let i = 0;
  while(n >= 1000 && i < SUFFIXES.length-1){ n /= 1000; i++; }
  return (n < 10 ? n.toFixed(2) : n < 100 ? n.toFixed(1) : Math.floor(n).toString()) + SUFFIXES[i];
}
function fmtTime(sec){
  sec = Math.floor(sec);
  const h = Math.floor(sec/3600), m = Math.floor((sec%3600)/60), s = sec%60;
  if(h > 0) return h + 'h' + m + 'm';
  if(m > 0) return m + 'm' + s + 's';
  return s + 's';
}
const rnd = (a,b) => a + Math.random()*(b-a);
const clamp = (v,a,b) => Math.max(a, Math.min(b, v));
const rndi = (a,b) => Math.floor(rnd(a,b+1));
function hexA(hex, a){
  const n = parseInt(hex.slice(1),16);
  return `rgba(${(n>>16)&255},${(n>>8)&255},${n&255},${a})`;
}
function el(tag, cls, html){
  const e = document.createElement(tag);
  if(cls) e.className = cls;
  if(html !== undefined) e.innerHTML = html;
  return e;
}

function toast(msg, color){
  const t = document.getElementById('toast');
  t.textContent = msg;
  t.style.color = color || '#fff';
  t.style.borderColor = color ? hexA(color, 0.6) : 'rgba(168,85,247,0.5)';
  t.classList.add('show');
  clearTimeout(toast._t);
  toast._t = setTimeout(() => t.classList.remove('show'), 2400);
}

function showBossWarning(){
  const w = document.getElementById('bossWarning');
  w.classList.remove('show');
  void w.offsetWidth;
  w.classList.add('show');
}

/* ============================================================
   DATA
============================================================ */
const RARITIES = [
  {id:'common',    name:'Common',    color:'#9ca3af', w: 1000,   pts: 1      },
  {id:'uncommon',  name:'Uncommon',  color:'#4ade80', w: 400,    pts: 3      },
  {id:'rare',      name:'Rare',      color:'#3b82f6', w: 150,    pts: 15     },
  {id:'epic',      name:'Epic',      color:'#a855f7', w: 50,     pts: 90     },
  {id:'legendary', name:'Legendary', color:'#f59e0b', w: 15,     pts: 600    },
  {id:'mythical',  name:'Mythical',  color:'#ef4444', w: 4,      pts: 4500   },
  {id:'divine',    name:'Divine',    color:'#ec4899', w: 1,      pts: 35000  },
  {id:'cosmic',    name:'Cosmic',    color:'#22d3ee', w: 0.2,    pts: 300000 },
  {id:'celestial', name:'Celestial', color:'#fde047', w: 0.03,   pts: 3e6    },
  {id:'eternal',   name:'Eternal',   color:'#a3e635', w: 0.004,  pts: 4e7    },
  {id:'void',      name:'Void',      color:'#8b5cf6', w: 0.0005, pts: 6e8    },
];

const WORLDS = [
  {
    id:1, name:'Forest Realm', icon:'🌲', color:'#4ade80',
    unlockRebirth: 0,
    bg: [20, 40, 28], bg2:[10,25,18],
    chestCount: 5, monsterCount: 12,
    monsterHp: 30, monsterMult: 1,
    bossHp: 1500, bossMult: 25,
    monsterHue: 120,
    itemPool: {
      common:['Wooden Sword','Old Boots','Stick','Stone','Apple'],
      uncommon:['Iron Sword','Herb','Torch','Leather Vest'],
      rare:['Steel Blade','Emerald','Ancient Scroll'],
      epic:['Enchanted Bow','Mystic Orb'],
      legendary:['Dragon Egg','Golden Fleece'],
      mythical:['Heart of Forest'],
      divine:['Nature Crown'],
      cosmic:['World Tree Seed'],
      celestial:['Genesis Leaf'],
      eternal:['Ancient Bark'],
      void:['Void Root'],
    },
  },
  {
    id:2, name:'Desert Abyss', icon:'🏜️', color:'#f59e0b',
    unlockRebirth: 3,
    bg: [50, 40, 20], bg2:[30,22,10],
    chestCount: 4, monsterCount: 16,
    monsterHp: 90, monsterMult: 8,
    bossHp: 12000, bossMult: 80,
    monsterHue: 35,
    itemPool: {
      common:['Sand Stone','Cracked Jar','Bone'],
      uncommon:['Bronze Dagger','Sun Shard'],
      rare:['Gold Ankh','Scarab','Desert Map'],
      epic:['Pharaoh Mask','Sapphire Amulet'],
      legendary:['Thunder Hammer','Genie Lamp'],
      mythical:['Soul Reaper'],
      divine:['Eye of Ra'],
      cosmic:['Sun Fragment'],
      celestial:['Solar Crown'],
      eternal:['Pharaoh Heart'],
      void:['Void Scarab'],
    },
  },
  {
    id:3, name:'Frozen Void', icon:'❄️', color:'#22d3ee',
    unlockRebirth: 8,
    bg: [18, 30, 48], bg2:[8,18,30],
    chestCount: 3, monsterCount: 20,
    monsterHp: 320, monsterMult: 60,
    bossHp: 90000, bossMult: 350,
    monsterHue: 195,
    itemPool: {
      common:['Ice Shard','Frozen Pebble'],
      uncommon:['Frost Blade','Crystal Fragment'],
      rare:['Glacier Ring','Ancient Rune'],
      epic:['Ice Crown','Frozen Orb'],
      legendary:['Dragon Scale','Celestial Crown'],
      mythical:['Void Blade'],
      divine:['Heart of Sea'],
      cosmic:['Star Fragment'],
      celestial:['Aurora Gem'],
      eternal:['Eternal Flame'],
      void:['Aether Crown'],
    },
  },
  {
    id:4, name:'Volcano Core', icon:'🌋', color:'#ef4444',
    unlockRebirth: 15,
    bg: [45, 18, 15], bg2:[28,8,6],
    chestCount: 2, monsterCount: 24,
    monsterHp: 1200, monsterMult: 400,
    bossHp: 800000, bossMult: 2500,
    monsterHue: 15,
    itemPool: {
      common:['Ash','Charred Bone'],
      uncommon:['Magma Blade','Fire Crystal'],
      rare:['Inferno Ring','Lava Stone'],
      epic:['Phoenix Heart','Ember Crown'],
      legendary:['Volcanic Blade','Dragon Heart'],
      mythical:['Infernal Eye'],
      divine:['Sun God Fragment'],
      cosmic:['Supernova Core'],
      celestial:['Star Forge'],
      eternal:['Primordial Flame'],
      void:['End of Time'],
    },
  },
  {
    id:5, name:'Celestial Heaven', icon:'✨', color:'#fde047',
    unlockRebirth: 25,
    bg: [30, 25, 55], bg2:[15,12,30],
    chestCount: 2, monsterCount: 28,
    monsterHp: 5000, monsterMult: 3000,
    bossHp: 8e6, bossMult: 20000,
    monsterHue: 55,
    itemPool: {
      common:['Cloud Dust','Feather'],
      uncommon:['Angelic Blade','Holy Water'],
      rare:['Divine Halo','Cherub Wing'],
      epic:['Seraph Sword','Celestial Armor'],
      legendary:['God Tear','Archangel Crown'],
      mythical:['Heaven Gate Key'],
      divine:['Throne of God'],
      cosmic:['Creation Spark'],
      celestial:['Genesis Stone'],
      eternal:['Eternal Soul'],
      void:['Beyond Heaven'],
    },
  },
];

const CHEST_TIERS = [
  {id:'wooden', color:'#8b5a2b', trim:'#fcd34d', luck: 1,  weight: 70},
  {id:'silver', color:'#9ca3af', trim:'#e5e7eb', luck: 3,  weight: 25},
  {id:'gold',   color:'#f59e0b', trim:'#fef3c7', luck: 10, weight: 5 },
];

const CHAR_MULTS = [1,2,5,10,25,50,100,500,1000,5000,10000,50000,100000,500000,1e6,5e6,1e7,5e7,1e8,5e8,1e9,5e9,1e10,5e10,1e11,5e11,1e12];
const CHAR_NAMES = ['Novice','Apprentice','Squire','Knight','Mage','Wizard','Archmage','Sorcerer','Warlock','Sage','Prophet','Demigod','God','Titan','Cosmic','Eternal','Void','Astral','Transcendent','Infinite','Supreme','Absolute','Omega','Genesis','Creator','Overlord','Primordial'];
const CHAR_ICONS = ['🧙','🧝','🧛','🥷','🧚','🧞','🧜','🧟','🧠','👑','⚔️','🛡️','💫','🌟','🌌','🌠','🔮','💎','✨','⚡','🔥','❄️','🌊','🌪️','🌋','💠','🌈'];
const CHARACTERS = CHAR_MULTS.map((mult, i) => ({
  id: i,
  name: CHAR_NAMES[i],
  icon: CHAR_ICONS[i],
  mult,
  cost: i === 0 ? 0 : Math.floor(Math.pow(mult, 1.18) * 150),
  hue: (i * 33) % 360,
}));

const WANDS = [
  {id:0, name:'Wooden Wand',     icon:'🪄', dmg:1,      cost:0,           color:'#8b5a2b'},
  {id:1, name:'Apprentice Wand', icon:'🪄', dmg:3,      cost:500,         color:'#4ade80'},
  {id:2, name:'Arcane Wand',     icon:'✨', dmg:10,     cost:5000,        color:'#3b82f6'},
  {id:3, name:'Crystal Wand',    icon:'💎', dmg:40,     cost:50000,       color:'#a855f7'},
  {id:4, name:'Flame Wand',      icon:'🔥', dmg:180,    cost:800000,      color:'#f97316'},
  {id:5, name:'Thunder Wand',    icon:'⚡', dmg:900,    cost:1.5e7,       color:'#fcd34d'},
  {id:6, name:'Void Wand',       icon:'🌌', dmg:5500,   cost:3e8,         color:'#8b5cf6'},
  {id:7, name:'Cosmic Wand',     icon:'🌟', dmg:40000,  cost:8e9,         color:'#22d3ee'},
  {id:8, name:'Divine Wand',     icon:'👑', dmg:350000, cost:2e11,        color:'#ec4899'},
  {id:9, name:'Eternal Wand',    icon:'💫', dmg:4e6,    cost:5e12,        color:'#fde047'},
  {id:10,name:'Godslayer Wand',  icon:'⚜️', dmg:5e7,    cost:2e14,        color:'#a3e635'},
];

const ARMORS = [
  {id:0, name:'Cloth Robe',       icon:'👕', hp:50,      cost:0,        color:'#9ca3af'},
  {id:1, name:'Leather Vest',     icon:'🥋', hp:150,     cost:400,      color:'#a16207'},
  {id:2, name:'Chainmail',        icon:'🛡️', hp:500,     cost:4000,     color:'#71717a'},
  {id:3, name:'Steel Plate',      icon:'🛡️', hp:1800,    cost:40000,    color:'#cbd5e1'},
  {id:4, name:'Mystic Armor',     icon:'🔮', hp:7000,    cost:600000,   color:'#a855f7'},
  {id:5, name:'Dragon Scale',     icon:'🐉', hp:30000,   cost:1.2e7,    color:'#dc2626'},
  {id:6, name:'Void Cloak',       icon:'🌑', hp:140000,  cost:2.5e8,    color:'#7c3aed'},
  {id:7, name:'Celestial Guard',  icon:'✨', hp:800000,  cost:6e9,      color:'#fcd34d'},
  {id:8, name:'Divine Aegis',     icon:'⚜️', hp:5e6,     cost:2e11,     color:'#ec4899'},
  {id:9, name:'Eternal Shell',    icon:'💠', hp:4e7,     cost:6e12,     color:'#a3e635'},
  {id:10,name:'Titan Armor',      icon:'🗿', hp:5e8,     cost:3e14,     color:'#f97316'},
];

const TRAIN_GROUNDS = [
  {id:0, name:'Village Dojo',      xpMult: 1,  rebirth: 0,  color:'#4ade80', icon:'🏘️'},
  {id:1, name:"Knight's Arena",    xpMult: 3,  rebirth: 3,  color:'#3b82f6', icon:'⚔️'},
  {id:2, name:'Mage Tower',        xpMult: 10, rebirth: 6,  color:'#a855f7', icon:'🗼'},
  {id:3, name:'Dragon Sanctuary',  xpMult: 35, rebirth: 10, color:'#f59e0b', icon:'🐲'},
  {id:4, name:'Void Rift',         xpMult: 120,rebirth: 15, color:'#8b5cf6', icon:'🌀'},
  {id:5, name:'Celestial Temple',  xpMult: 500,rebirth: 22, color:'#22d3ee', icon:'🛕'},
  {id:6, name:'Eternal Nexus',     xpMult: 2500,rebirth: 30,color:'#fde047', icon:'💫'},
];

/* ============================================================
   STATE
============================================================ */
const SAVE_KEY = 'magicloot_v4';
function defaultState(){
  return {
    px: 1600, py: 1600,
    hp: 100, mana: 100,
    coins: 0, rebirths: 0, playtime: 0,
    xp: 0, level: 1,
    inventory: RARITIES.reduce((o,r)=>(o[r.id]=0,o),{}),
    upgrades: { speed:0, range:0, luck:0, mana:0, clickDmg:0, crit:0, regen:0 },
    currentChar: 0, ownedChars: [0],
    currentWand: 0, ownedWands: [0],
    currentArmor: 0, ownedArmors: [0],
    currentWorld: 1, unlockedWorlds: [1],
    currentTrain: 0,
    stats: { opened:0, earned:0, kills:0, bossKills:0, clicks:0, deaths:0 },
    chests: [], monsters: [], boss: null,
    bossDefeatCooldown: 0,
    leaderboard: [],
    playerName: 'You',
  };
}
let S = defaultState();

function save(){
  try{
    const copy = Object.assign({}, S);
    copy.monsters = []; copy.chests = []; copy.boss = null;
    localStorage.setItem(SAVE_KEY, JSON.stringify(copy));
  } catch(e){}
}
function load(){
  try{
    const raw = localStorage.getItem(SAVE_KEY);
    if(!raw) return;
    const obj = JSON.parse(raw);
    const def = defaultState();
    S = Object.assign(def, obj);
    S.inventory = Object.assign(def.inventory, obj.inventory || {});
    S.upgrades = Object.assign(def.upgrades, obj.upgrades || {});
    if(!Array.isArray(S.ownedChars)) S.ownedChars = [0];
    if(!Array.isArray(S.ownedWands)) S.ownedWands = [0];
    if(!Array.isArray(S.ownedArmors)) S.ownedArmors = [0];
    if(!Array.isArray(S.unlockedWorlds)) S.unlockedWorlds = [1];
    S.monsters = []; S.chests = []; S.boss = null;
  } catch(e){}
}
load();

/* ============================================================
   DERIVED
============================================================ */
function charMult(){ return (CHARACTERS[S.currentChar] || CHARACTERS[0]).mult; }
function rebirthBonus(){ return 1 + S.rebirths * 0.5; }
function totalMult(){ return charMult() * rebirthBonus(); }
function wandDmg(){
  const base = WANDS[S.currentWand].dmg;
  const upg = 1 + S.upgrades.clickDmg * 0.15;
  const trainBonus = 1 + (TRAIN_GROUNDS[S.currentTrain].xpMult - 1) * 0.05;
  return base * upg * trainBonus * rebirthBonus();
}
function critChance(){ return Math.min(0.75, S.upgrades.crit * 0.02); }
function armorHp(){ return ARMORS[S.currentArmor].hp; }
function maxHp(){ return armorHp() + 50 + S.rebirths * 100; }
function regenRate(){ return 2 + S.upgrades.regen * 0.8; }
function playerSpeed(){ return 260 + S.upgrades.speed * 22; }
function playerRange(){ return 55 + S.upgrades.range * 10; }
function luckValue(){ return 1 + S.upgrades.luck * 0.25; }
function maxMana(){ return 100 + S.upgrades.mana * 15; }
function rebirthCost(){ return Math.floor(100000 * Math.pow(3, S.rebirths)); }

function rarityChances(luckMul){
  const weights = RARITIES.map(r => r.id === 'common' ? r.w : r.w * luckMul);
  const total = weights.reduce((a,b)=>a+b, 0);
  return weights.map(w => w / total);
}
function currentChances(){ return rarityChances(luckValue()); }
function currentWorld(){ return WORLDS.find(w => w.id === S.currentWorld) || WORLDS[0]; }
function xpForLevel(lv){ return Math.floor(100 * Math.pow(1.5, lv-1)); }
function addXp(amount){
  S.xp += amount;
  while(S.xp >= xpForLevel(S.level)){
    S.xp -= xpForLevel(S.level);
    S.level++;
    burst(S.px, S.py, '#a855f7', 40, 320);
    popups.push({x:S.px, y:S.py-40, vy:-100, life:2, maxLife:2, text:'⭐ LEVEL UP!', color:'#a855f7', tier:4});
    beep(1046, 0.15, 'triangle', 0.09);
    beep(1568, 0.15, 'triangle', 0.09);
    toast('🎉 Level Up! Now Level ' + S.level, '#a855f7');
  }
}

/* ============================================================
   CANVAS
============================================================ */
const WORLD = 3200;
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let W = 0, H = 0, DPR = 1;
function resize(){
  DPR = Math.min(window.devicePixelRatio || 1, 2);
  W = window.innerWidth;
  H = window.innerHeight;
  canvas.width  = Math.round(W*DPR);
  canvas.height = Math.round(H*DPR);
  canvas.style.width = W+'px';
  canvas.style.height = H+'px';
  ctx.setTransform(DPR,0,0,DPR,0,0);
}
resize();
window.addEventListener('resize', resize);
window.addEventListener('orientationchange', ()=>setTimeout(resize, 200));

/* ============================================================
   RUNTIME
============================================================ */
let particles = [];
let popups = [];
let bullets = [];
let banner = null;
let flash = { color:'#fff', life:0 };
let shakeAmt = 0;
let keys = {};
let joystick = { active:false, id:null, bx:0, by:0, kx:0, ky:0, dx:0, dy:0, maxR:70 };
let camera = { x:0, y:0 };
let time = 0;
let lastT = performance.now();
let lastSaveT = 0;
let mouseAim = { x: 0, y: 0, has: false };
let lastShot = 0;
let stars = [];  // background stars
let ambientParticles = [];

/* ============================================================
   SPAWN
============================================================ */
function pickChestTier(){
  const total = CHEST_TIERS.reduce((a,b)=>a+b.weight, 0);
  let r = Math.random() * total;
  for(const t of CHEST_TIERS){ r -= t.weight; if(r <= 0) return t; }
  return CHEST_TIERS[0];
}

function spawnChest(initial){
  let x, y, tries = 0;
  do{
    x = rnd(150, WORLD-150);
    y = rnd(150, WORLD-150);
    tries++;
  } while(tries < 30 && Math.hypot(x-S.px, y-S.py) < 400);
  S.chests.push({ x, y, tier: pickChestTier(), state:'closed', t:0, bob: Math.random()*6.28, shake:0 });
}

function spawnMonster(initial){
  const w = currentWorld();
  let x, y, tries = 0;
  do{
    x = rnd(150, WORLD-150);
    y = rnd(150, WORLD-150);
    tries++;
  } while(tries < 30 && Math.hypot(x-S.px, y-S.py) < 320);

  const isElite = !initial && Math.random() < 0.1;
  const mult = w.monsterMult * (isElite ? 3 : 1);
  const hp = Math.floor(w.monsterHp * (isElite ? 3 : 1) * rnd(0.8, 1.3));

  S.monsters.push({
    x, y, r: isElite ? 26 : 20,
    vx: rnd(-70, 70), vy: rnd(-70, 70),
    bob: Math.random()*6.28,
    hue: w.monsterHue + rnd(-20, 20),
    elite: isElite,
    hp, maxHp: hp, mult,
    alive: true, dieT: 0, hitT: 0,
    shootCd: rnd(1.5, 3.5),
    faceX: 1, faceY: 0,
  });
}

function spawnBoss(){
  if(S.boss) return;
  const w = currentWorld();
  S.boss = {
    x: clamp(S.px + rnd(400, 700) * (Math.random() < 0.5 ? 1 : -1), 100, WORLD-100),
    y: clamp(S.py + rnd(400, 700) * (Math.random() < 0.5 ? 1 : -1), 100, WORLD-100),
    r: 70, vx: 60, vy: 60,
    hue: w.monsterHue - 40,
    hp: w.bossHp, maxHp: w.bossHp,
    mult: w.bossMult,
    bob: 0, alive: true, dieT: 0, hitT: 0,
    phase: 0, shootCd: 1.5, spawnCd: 5,
  };
  showBossWarning();
  toast('⚠️ BOSS APPEARED!', '#ef4444');
  beep(120, 0.5, 'sawtooth', 0.12);
  setTimeout(()=>beep(90, 0.6, 'sawtooth', 0.12), 200);
}

function initWorld(){
  S.chests = []; S.monsters = []; S.boss = null;
  S.px = WORLD/2; S.py = WORLD/2;
  const w = currentWorld();
  for(let i=0;i<w.chestCount;i++) spawnChest(true);
  for(let i=0;i<w.monsterCount;i++) spawnMonster(true);

  if(S.leaderboard.length === 0){
    const names = ['Alex','Mira','Kai','Zara','Ren','Nova','Lux','Dax','Vex','Iris','Orion','Astra','Ryu','Sora','Yuki','Kira','Zane','Vale','Neo','Echo','Rune','Skye'];
    for(let i=0;i<14;i++){
      S.leaderboard.push({
        name: names[i % names.length] + Math.floor(Math.random()*99+1),
        score: Math.floor(1e5 * Math.pow(1.9, i) * rnd(0.6, 1.4)),
        rebirths: Math.floor(i * 2.5 + rnd(0, 3)),
        isPlayer: false,
      });
    }
  }
}

/* ============================================================
   LOOT
============================================================ */
function rollRarity(luckMul){
  const weights = RARITIES.map(r => r.id === 'common' ? r.w : r.w * luckMul);
  const total = weights.reduce((a,b)=>a+b, 0);
  let roll = Math.random() * total;
  for(let i=0;i<weights.length;i++){
    roll -= weights[i];
    if(roll <= 0) return RARITIES[i];
  }
  return RARITIES[0];
}
function rollItem(rarityId){
  const pool = currentWorld().itemPool[rarityId] || ['Mystery Item'];
  return pool[Math.floor(Math.random()*pool.length)];
}

function openChest(c){
  const luckMul = luckValue() * c.tier.luck;
  const rarity = rollRarity(luckMul);
  const itemName = rollItem(rarity.id);
  S.inventory[rarity.id] = (S.inventory[rarity.id] || 0) + 1;
  S.stats.opened++;

  burst(c.x, c.y-20, rarity.color, rarityTierParticles(rarity.id), 240);

  popups.push({
    x:c.x, y:c.y-30, vy:-110, life:2, maxLife:2,
    text: itemName, color: rarity.color, tier: RARITIES.indexOf(rarity),
  });

  const tierIdx = RARITIES.indexOf(rarity);
  if(tierIdx >= 2){
    banner = { rarity, life: 2.4, maxLife: 2.4, item: itemName };
    if(tierIdx >= 4){
      flash.color = rarity.color;
      flash.life = 0.35;
      shakeAmt = Math.max(shakeAmt, tierIdx === 4 ? 8 : tierIdx === 5 ? 14 : tierIdx === 6 ? 20 : 26);
    }
    jingle(rarity.id);
  } else {
    beep(520, 0.08, 'square', 0.04);
  }
}

function dropLootFromKill(x, y, mult, isBoss){
  const coinGain = 15 * mult * totalMult();
  S.coins += coinGain;
  S.stats.earned += coinGain;
  popups.push({x, y:y-10, vy:-90, life:1.3, maxLife:1.3, text:'+🪙'+fmt(coinGain), color:'#fbbf24', tier:4});
  burst(x, y, '#fbbf24', isBoss?80:22, isBoss?320:180);

  addXp(isBoss ? 500 : 5);

  if(isBoss){
    S.stats.bossKills++;
    const luck = luckValue() * 20;
    const nItems = 3 + Math.floor(Math.random()*3);
    for(let i=0;i<nItems;i++){
      const r = rollRarity(luck);
      const itemName = rollItem(r.id);
      S.inventory[r.id] = (S.inventory[r.id]||0) + 1;
      popups.push({
        x: x + rnd(-60, 60), y: y + rnd(-60, 60), vy:-90, life:2.5, maxLife:2.5,
        text:itemName, color: r.color, tier: RARITIES.indexOf(r),
      });
    }
    const topR = rollRarity(luck * 3);
    banner = { rarity: topR, life: 3.2, maxLife: 3.2, item: 'BOSS DROP: ' + rollItem(topR.id) };
    flash.color = '#ef4444'; flash.life = 0.7;
    shakeAmt = 40;
    jingle('celestial');
    toast('💀 BOSS DEFEATED!', '#ef4444');
    return;
  }

  const dropChance = 0.25 * Math.min(5, 1 + S.rebirths * 0.2);
  if(Math.random() < dropChance){
    const r = rollRarity(luckValue());
    const itemName = rollItem(r.id);
    S.inventory[r.id] = (S.inventory[r.id]||0) + 1;
    popups.push({
      x: x + rnd(-30, 30), y: y - 30, vy:-100, life:2, maxLife:2,
      text:itemName, color: r.color, tier: RARITIES.indexOf(r),
    });
    const tierIdx = RARITIES.indexOf(r);
    if(tierIdx >= 3) jingle(r.id);
  }
}

/* ============================================================
   FX & AUDIO
============================================================ */
function burst(x, y, color, count, speed){
  for(let i=0;i<count;i++){
    const a = Math.random()*Math.PI*2;
    const s = speed * (0.25 + Math.random()*0.9);
    const ml = 0.7 + Math.random()*0.7;
    particles.push({
      x, y,
      vx: Math.cos(a)*s, vy: Math.sin(a)*s - 120,
      life: ml, maxLife: ml,
      color, size: 2 + Math.random()*4,
      rot: Math.random()*6.28, rotSpeed: rnd(-8,8),
      shape: Math.random() < 0.3 ? 'star' : 'circle',
    });
  }
}
function rarityTierParticles(id){
  return { common:14, uncommon:18, rare:26, epic:40, legendary:60, mythical:90, divine:130, cosmic:170, celestial:220, eternal:270, void:330 }[id] || 14;
}

let audioCtx = null;
function ensureAudio(){
  if(!audioCtx){
    try { audioCtx = new (window.AudioContext||window.webkitAudioContext)(); } catch(e){}
  }
  if(audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
}
function beep(freq, dur, type, vol){
  ensureAudio();
  if(!audioCtx) return;
  try{
    const o = audioCtx.createOscillator();
    const g = audioCtx.createGain();
    o.type = type||'sine';
    o.frequency.value = freq;
    g.gain.value = vol||0.05;
    g.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime+dur);
    o.connect(g); g.connect(audioCtx.destination);
    o.start();
    o.stop(audioCtx.currentTime+dur);
  } catch(e){}
}
function jingle(id){
  const seq = {
    rare:[523,659,784],
    epic:[523,659,784,988],
    legendary:[523,659,784,1046,1318],
    mythical:[440,554,659,880,1108,1318],
    divine:[523,659,784,1046,1318,1568,2097],
    cosmic:[523,659,784,1046,1318,1568,2097,2637],
    celestial:[523,659,784,1046,1318,1568,2097,2637,3136],
    eternal:[523,659,784,1046,1318,1568,2097,2637,3136,3520],
    void:[440,554,659,880,1108,1318,1760,2097,2637,3136,3520],
  }[id] || [523];
  seq.forEach((f,i) => setTimeout(()=>beep(f, 0.16, 'triangle', 0.07), i*60));
}
function shootSound(){
  beep(700 + Math.random()*200, 0.04, 'square', 0.025);
}
function hitSound(){
  beep(220 + Math.random()*80, 0.05, 'square', 0.03);
}
function hurtSound(){
  beep(150, 0.12, 'sawtooth', 0.05);
}
function killSound(){
  beep(880, 0.06, 'triangle', 0.04);
  setTimeout(()=>beep(1318, 0.08, 'triangle', 0.04), 40);
}

/* ============================================================
   INPUT
============================================================ */
window.addEventListener('keydown', e => {
  ensureAudio();
  const k = e.key.toLowerCase();
  if(['arrowup','arrowdown','arrowleft','arrowright',' '].includes(k)) e.preventDefault();
  keys[k] = true;
});
window.addEventListener('keyup', e => { keys[e.key.toLowerCase()] = false; });

function pointXY(e){
  const r = canvas.getBoundingClientRect();
  return { x: e.clientX - r.left, y: e.clientY - r.top };
}

let lastAimTap = 0;

canvas.addEventListener('pointerdown', e => {
  ensureAudio();
  if(!panelEl.classList.contains('hidden')) return;
  const p = pointXY(e);

  if(p.x < W*0.5 && !joystick.active){
    joystick.active = true;
    joystick.id = e.pointerId;
    joystick.bx = p.x; joystick.by = p.y;
    joystick.kx = p.x; joystick.ky = p.y;
    joystick.dx = 0; joystick.dy = 0;
    return;
  }
  shootAt(p.x + camera.x, p.y + camera.y);
});

canvas.addEventListener('pointermove', e => {
  if(e.pointerId === joystick.id){
    const p = pointXY(e);
    let dx = p.x - joystick.bx;
    let dy = p.y - joystick.by;
    const d = Math.hypot(dx, dy);
    if(d > joystick.maxR){
      dx = dx/d * joystick.maxR;
      dy = dy/d * joystick.maxR;
    }
    joystick.kx = joystick.bx + dx;
    joystick.ky = joystick.by + dy;
    joystick.dx = dx / joystick.maxR;
    joystick.dy = dy / joystick.maxR;
    return;
  }
  const p = pointXY(e);
  mouseAim.x = p.x + camera.x;
  mouseAim.y = p.y + camera.y;
  mouseAim.has = true;
});

function endJoy(e){
  if(e.pointerId !== joystick.id) return;
  joystick.active = false;
  joystick.id = null;
  joystick.dx = 0; joystick.dy = 0;
}
canvas.addEventListener('pointerup', endJoy);
canvas.addEventListener('pointercancel', endJoy);
canvas.addEventListener('contextmenu', e => e.preventDefault());

function shootAt(wx, wy){
  const now = performance.now();
  if(now - lastShot < 130) return;
  lastShot = now;
  S.stats.clicks++;
  const dx = wx - S.px;
  const dy = wy - S.py;
  const d = Math.hypot(dx, dy) || 1;
  const spd = 900;
  const w = WANDS[S.currentWand];
  const isCrit = Math.random() < critChance();
  bullets.push({
    x: S.px, y: S.py,
    vx: (dx/d)*spd, vy: (dy/d)*spd,
    r: isCrit ? 9 : 6,
    dmg: wandDmg() * (isCrit ? 3 : 1),
    color: isCrit ? '#fcd34d' : w.color,
    life: 1.5, maxLife: 1.5,
    crit: isCrit,
  });
  shootSound();
  shakeAmt = Math.max(shakeAmt, 1.5);
}

/* ============================================================
   UPDATE
============================================================ */
function update(dt){
  time += dt;
  S.playtime += dt;

  S.mana = Math.min(maxMana(), (S.mana||maxMana()) + dt * 12);

  if(S.hp < maxHp()) S.hp = Math.min(maxHp(), S.hp + dt * regenRate());

  let mx = 0, my = 0;
  if(keys['w'] || keys['arrowup'])    my -= 1;
  if(keys['s'] || keys['arrowdown'])  my += 1;
  if(keys['a'] || keys['arrowleft'])  mx -= 1;
  if(keys['d'] || keys['arrowright']) mx += 1;
  if(joystick.active){ mx = joystick.dx; my = joystick.dy; }

  const len = Math.hypot(mx, my);
  if(len > 1){ mx /= len; my /= len; }
  const spd = playerSpeed();
  S.px = clamp(S.px + mx*spd*dt, 24, WORLD-24);
  S.py = clamp(S.py + my*spd*dt, 24, WORLD-24);

  /* Chests */
  const range = playerRange();
  for(let i=S.chests.length-1; i>=0; i--){
    const c = S.chests[i];
    if(c.state === 'closed'){
      c.bob += dt;
      if(Math.hypot(c.x-S.px, c.y-S.py) < range){
        c.state = 'opening'; c.t = 0;
      }
    } else if(c.state === 'opening'){
      c.t += dt;
      c.shake = Math.min(1, c.t / 0.55);
      if(c.t >= 0.55){ c.state = 'opened'; c.t = 0; openChest(c); }
    } else if(c.state === 'opened'){
      c.t += dt;
      if(c.t > 0.4){
        S.chests.splice(i, 1);
        if(S.chests.length < currentWorld().chestCount) spawnChest(false);
      }
    }
  }

  /* Bullets */
  for(let i=bullets.length-1; i>=0; i--){
    const b = bullets[i];
    b.x += b.vx*dt; b.y += b.vy*dt;
    b.life -= dt;
    if(b.life <= 0 || b.x < 0 || b.y < 0 || b.x > WORLD || b.y > WORLD){
      bullets.splice(i,1); continue;
    }
    let hit = false;
    for(const m of S.monsters){
      if(!m.alive) continue;
      if(Math.hypot(m.x-b.x, m.y-b.y) < m.r + b.r){
        m.hp -= b.dmg;
        m.hitT = 0.15;
        burst(b.x, b.y, b.color, b.crit ? 14 : 6, b.crit ? 200 : 120);
        if(b.crit){
          popups.push({x:b.x, y:b.y-10, vy:-60, life:0.6, maxLife:0.6, text:'CRIT!', color:'#fcd34d', tier:4});
        }
        hitSound();
        if(m.hp <= 0){
          m.alive = false; m.dieT = 0; S.stats.kills++;
          killSound();
          dropLootFromKill(m.x, m.y, m.mult, false);
        }
        break;
      }
    }
    if(!hit && S.boss && S.boss.alive){
      const bs = S.boss;
      if(Math.hypot(bs.x-b.x, bs.y-b.y) < bs.r + b.r){
        bs.hp -= b.dmg;
        bs.hitT = 0.12;
        burst(b.x, b.y, b.color, 8, 140);
        hitSound();
        if(bs.hp <= 0){
          bs.alive = false; bs.dieT = 0;
          dropLootFromKill(bs.x, bs.y, bs.mult, true);
        }
        hit = true;
      }
    }
    if(hit) bullets.splice(i,1);
  }

  /* Monsters */
  for(let i=S.monsters.length-1; i>=0; i--){
    const m = S.monsters[i];
    if(!m.alive){
      m.dieT += dt;
      if(m.dieT > 0.5){
        S.monsters.splice(i,1);
        if(S.monsters.length < currentWorld().monsterCount) spawnMonster(false);
      }
      continue;
    }
    if(m.hitT > 0) m.hitT -= dt;

    const dx = S.px - m.x, dy = S.py - m.y;
    const d = Math.hypot(dx, dy) || 1;
    if(d < 500){
      m.x += (dx/d) * 90 * dt;
      m.y += (dy/d) * 90 * dt;
    } else {
      m.x += m.vx * dt;
      m.y += m.vy * dt;
    }
    m.faceX = dx/d; m.faceY = dy/d;
    m.x = clamp(m.x, 40, WORLD-40);
    m.y = clamp(m.y, 40, WORLD-40);
    m.bob += dt;

    if(Math.hypot(m.x-S.px, m.y-S.py) < m.r + 18){
      S.hp -= 5 * dt * (1 + currentWorld().id * 0.5);
      if(S.hp < 0) S.hp = 0;
      if(S.hp <= 0) playerDie();
      if(Math.random() < 0.02) hurtSound();
    }
  }

  /* Boss */
  if(S.boss){
    const b = S.boss;
    if(!b.alive){
      b.dieT += dt;
      if(b.dieT > 2){ S.boss = null; S.bossDefeatCooldown = 60; }
    } else {
      if(b.hitT > 0) b.hitT -= dt;
      const dx = S.px - b.x, dy = S.py - b.y;
      const d = Math.hypot(dx, dy) || 1;
      if(d > 200){
        b.x += (dx/d) * 90 * dt;
        b.y += (dy/d) * 90 * dt;
      }
      b.bob += dt;
      b.shootCd -= dt;
      if(b.shootCd <= 0){
        b.shootCd = 1.6;
        const spd2 = 320;
        bullets.push({
          x: b.x, y: b.y,
          vx: (dx/d)*spd2, vy: (dy/d)*spd2,
          r: 9, dmg: 0, color: '#ef4444',
          life: 3, maxLife: 3, enemy: true,
        });
      }
      b.spawnCd -= dt;
      if(b.spawnCd <= 0){
        b.spawnCd = 6;
        for(let i=0;i<3;i++) spawnMonster(false);
      }
      if(d < b.r + 18){
        S.hp -= 15 * dt;
        if(S.hp < 0) S.hp = 0;
        if(S.hp <= 0) playerDie();
      }
    }
  } else if(S.bossDefeatCooldown > 0){
    S.bossDefeatCooldown -= dt;
    if(S.bossDefeatCooldown <= 0){
      if(Math.random() < 0.5) spawnBoss();
      else S.bossDefeatCooldown = 30;
    }
  } else if(Math.random() < 0.0006){
    spawnBoss();
  }

  /* Enemy bullets */
  for(let i=bullets.length-1; i>=0; i--){
    const b = bullets[i];
    if(!b.enemy) continue;
    if(Math.hypot(b.x-S.px, b.y-S.py) < 18){
      S.hp -= 8;
      burst(b.x, b.y, '#ef4444', 10, 140);
      hurtSound();
      bullets.splice(i,1);
      if(S.hp <= 0) playerDie();
    }
  }

  /* Particles */
  for(let i=particles.length-1; i>=0; i--){
    const p = particles[i];
    p.x += p.vx*dt; p.y += p.vy*dt;
    p.vy += 480*dt; p.vx *= 0.98;
    p.life -= dt;
    p.rot += p.rotSpeed * dt;
    if(p.life <= 0) particles.splice(i,1);
  }
  for(let i=popups.length-1; i>=0; i--){
    const p = popups[i];
    p.y += p.vy*dt; p.vy += 60*dt;
    p.life -= dt;
    if(p.life <= 0) popups.splice(i,1);
  }

  if(banner){ banner.life -= dt; if(banner.life <= 0) banner = null; }
  if(flash.life > 0) flash.life -= dt;
  if(shakeAmt > 0) shakeAmt = Math.max(0, shakeAmt - 100*dt);

  camera.x += ((S.px - W/2) - camera.x) * Math.min(1, dt*10);
  camera.y += ((S.py - H/2) - camera.y) * Math.min(1, dt*10);

  lastSaveT += dt;
  if(lastSaveT > 4){ lastSaveT = 0; save(); }

  updateTopBar();
  updateBars();
}

function playerDie(){
  S.hp = 0;
  S.stats.deaths++;
  const lost = Math.floor(S.coins * 0.1);
  S.coins = Math.max(0, S.coins - lost);
  flash.color = '#ef4444'; flash.life = 0.6;
  shakeAmt = 40;
  hurtSound();
  toast('💀 You died! Lost ' + fmt(lost) + ' coins', '#ef4444');
  S.hp = maxHp();
  S.px = WORLD/2; S.py = WORLD/2;
}

/* ============================================================
   RENDER
============================================================ */
function drawBackground(){
  const w = currentWorld();
  const c = w.bg, c2 = w.bg2;
  const grd = ctx.createRadialGradient(W/2, H/2, 0, W/2, H/2, Math.max(W,H));
  grd.addColorStop(0, `rgb(${c[0]},${c[1]},${c[2]})`);
  grd.addColorStop(1, `rgb(${c2[0]},${c2[1]},${c2[2]})`);
  ctx.fillStyle = grd;
  ctx.fillRect(0,0,W,H);
}

function drawStars(){
  // Parallax stars
  ctx.fillStyle = 'rgba(255,255,255,0.7)';
  const px = camera.x * 0.15, py = camera.y * 0.15;
  for(const s of stars){
    const x = ((s.x - px) % W + W) % W;
    const y = ((s.y - py) % H + H) % H;
    const tw = 0.5 + 0.5*Math.sin(time*2 + s.tw);
    ctx.globalAlpha = s.a * tw;
    ctx.fillRect(x, y, s.s, s.s);
  }
  ctx.globalAlpha = 1;
}

function drawGrid(){
  const gs = 100;
  const x0 = Math.floor(camera.x/gs)*gs;
  const y0 = Math.floor(camera.y/gs)*gs;
  const wc = currentWorld().color;
  ctx.strokeStyle = hexA(wc, 0.07);
  ctx.lineWidth = 1;
  ctx.beginPath();
  for(let x=x0; x<camera.x+W+gs; x+=gs){
    ctx.moveTo(x, camera.y); ctx.lineTo(x, camera.y+H);
  }
  for(let y=y0; y<camera.y+H+gs; y+=gs){
    ctx.moveTo(camera.x, y); ctx.lineTo(camera.x+W, y);
  }
  ctx.stroke();

  // World border glow
  ctx.strokeStyle = hexA(wc, 0.6);
  ctx.lineWidth = 4;
  ctx.shadowColor = wc; ctx.shadowBlur = 20;
  ctx.strokeRect(0,0,WORLD,WORLD);
  ctx.shadowBlur = 0;
}

function drawChest(c){
  const bob = c.state === 'closed' ? Math.sin(c.bob*3) * 2.5 : 0;
  const shakeX = c.state === 'opening' ? (Math.random()-0.5) * c.shake * 8 : 0;
  const x = c.x + shakeX;
  const y = c.y + bob;

  if(c.state === 'closed'){
    const glowP = 0.5 + 0.5*Math.sin(time*3 + c.bob);
    const grd = ctx.createRadialGradient(x, y+10, 0, x, y+10, 60);
    const gc = c.tier.id === 'gold' ? '251,191,36' : c.tier.id === 'silver' ? '200,210,230' : '180,120,60';
    grd.addColorStop(0, `rgba(${gc},${0.3 + glowP*0.25})`);
    grd.addColorStop(1, `rgba(${gc},0)`);
    ctx.fillStyle = grd;
    ctx.beginPath(); ctx.arc(x, y+10, 60, 0, Math.PI*2); ctx.fill();
  }
  if(c.state === 'opening'){
    const gl = c.shake;
    const grd = ctx.createRadialGradient(x, y, 0, x, y, 100);
    grd.addColorStop(0, `rgba(255,240,180,${0.7*gl})`);
    grd.addColorStop(1, 'rgba(255,240,180,0)');
    ctx.fillStyle = grd;
    ctx.beginPath(); ctx.arc(x, y, 100, 0, Math.PI*2); ctx.fill();
  }

  ctx.fillStyle = 'rgba(0,0,0,0.55)';
  ctx.beginPath(); ctx.ellipse(x, y+26, 26, 9, 0, 0, Math.PI*2); ctx.fill();

  const col = c.tier.color, trim = c.tier.trim;

  ctx.fillStyle = col;
  ctx.fillRect(x-24, y-8, 48, 34);
  ctx.fillStyle = 'rgba(0,0,0,0.25)';
  ctx.fillRect(x-24, y+12, 48, 14);
  ctx.fillStyle = 'rgba(255,255,255,0.15)';
  ctx.fillRect(x-24, y-8, 48, 4);
  ctx.strokeStyle = 'rgba(0,0,0,0.7)';
  ctx.lineWidth = 2;
  ctx.strokeRect(x-24, y-8, 48, 34);

  const lidLift = c.state === 'opening' ? c.shake*6 : c.state === 'opened' ? 4 : 0;
  const lidAngle = c.state === 'opening' ? c.shake*0.9 : c.state === 'opened' ? 0.9 : 0;
  ctx.save();
  ctx.translate(x, y-8);
  ctx.rotate(-lidAngle * 0.6);
  ctx.fillStyle = col;
  ctx.fillRect(-26, -18 - lidLift, 52, 18);
  ctx.fillStyle = 'rgba(255,255,255,0.22)';
  ctx.fillRect(-26, -18 - lidLift, 52, 5);
  ctx.strokeStyle = 'rgba(0,0,0,0.7)';
  ctx.lineWidth = 2;
  ctx.strokeRect(-26, -18 - lidLift, 52, 18);
  ctx.restore();

  ctx.fillStyle = trim;
  ctx.fillRect(x-24, y-10, 48, 5);
  ctx.fillRect(x-3, y-10, 6, 36);

  if(c.state === 'closed' || c.state === 'opening'){
    ctx.fillStyle = trim;
    ctx.beginPath(); ctx.arc(x, y+2, 6, 0, Math.PI*2); ctx.fill();
    ctx.fillStyle = 'rgba(0,0,0,0.7)';
    ctx.fillRect(x-1, y+1, 2, 6);
  }
}

function drawMonster(m){
  const x = m.x;
  const y = m.y + Math.sin(m.bob*3) * 2.5;
  const alpha = m.alive ? 1 : Math.max(0, 1 - m.dieT / 0.5);
  const r = m.r;
  ctx.globalAlpha = alpha;

  ctx.fillStyle = 'rgba(0,0,0,0.45)';
  ctx.beginPath(); ctx.ellipse(x, m.y+r-4, r*0.85, 6, 0, 0, Math.PI*2); ctx.fill();

  const hue = m.hue;
  const flash = m.hitT > 0 ? 40 : 0;
  const drawR = m.alive ? r : r * (1 + m.dieT);

  // Body
  const grd = ctx.createRadialGradient(x-r*0.3, y-r*0.3, 0, x, y, drawR);
  grd.addColorStop(0, `hsl(${hue}, 80%, ${65+flash}%)`);
  grd.addColorStop(1, `hsl(${hue}, 65%, ${35+flash}%)`);
  ctx.fillStyle = grd;
  ctx.beginPath(); ctx.arc(x, y, drawR, 0, Math.PI*2); ctx.fill();

  // Outline
  ctx.strokeStyle = `hsl(${hue}, 80%, 20%)`;
  ctx.lineWidth = 2;
  ctx.stroke();

  // Eyes
  const eyeR = r*0.22;
  ctx.fillStyle = '#fff';
  ctx.beginPath();
  ctx.arc(x-r*0.35, y-r*0.1, eyeR, 0, Math.PI*2);
  ctx.arc(x+r*0.35, y-r*0.1, eyeR, 0, Math.PI*2);
  ctx.fill();
  ctx.fillStyle = '#000';
  ctx.beginPath();
  ctx.arc(x-r*0.35 + m.faceX*1.5, y-r*0.1 + m.faceY*1.5, eyeR*0.5, 0, Math.PI*2);
  ctx.arc(x+r*0.35 + m.faceX*1.5, y-r*0.1 + m.faceY*1.5, eyeR*0.5, 0, Math.PI*2);
  ctx.fill();

  // Elite crown
  if(m.elite){
    ctx.fillStyle = '#fcd34d';
    ctx.shadowColor = '#fcd34d';
    ctx.shadowBlur = 10;
    ctx.beginPath();
    ctx.moveTo(x-r*0.5, y-r-2);
    ctx.lineTo(x-r*0.25, y-r-12);
    ctx.lineTo(x, y-r-5);
    ctx.lineTo(x+r*0.25, y-r-12);
    ctx.lineTo(x+r*0.5, y-r-2);
    ctx.closePath();
    ctx.fill();
    ctx.shadowBlur = 0;
  }

  // HP bar
  if(m.alive && m.hp < m.maxHp){
    const bw = r*2.4;
    ctx.fillStyle = 'rgba(0,0,0,0.7)';
    ctx.fillRect(x-bw/2, y-r-16, bw, 6);
    ctx.fillStyle = m.elite ? '#f59e0b' : '#ef4444';
    ctx.fillRect(x-bw/2, y-r-16, bw*(m.hp/m.maxHp), 6);
  }

  ctx.globalAlpha = 1;
}

function drawBoss(b){
  const x = b.x;
  const y = b.y + Math.sin(b.bob*2) * 3;
  const alpha = b.alive ? 1 : Math.max(0, 1 - b.dieT / 2);
  const r = b.r;
  ctx.globalAlpha = alpha;

  const grd = ctx.createRadialGradient(x, b.y, 0, x, b.y, r*3);
  grd.addColorStop(0, `rgba(239,68,68,0.5)`);
  grd.addColorStop(1, 'rgba(239,68,68,0)');
  ctx.fillStyle = grd;
  ctx.beginPath(); ctx.arc(x, b.y, r*3, 0, Math.PI*2); ctx.fill();

  ctx.fillStyle = 'rgba(0,0,0,0.6)';
  ctx.beginPath(); ctx.ellipse(x, b.y+r-10, r*1.15, 14, 0, 0, Math.PI*2); ctx.fill();

  const hue = b.hue;
  const flash = b.hitT > 0 ? 30 : 0;

  const bodyGrd = ctx.createRadialGradient(x-r*0.3, y-r*0.3, 0, x, y, r);
  bodyGrd.addColorStop(0, `hsl(${hue}, 70%, ${55+flash}%)`);
  bodyGrd.addColorStop(1, `hsl(${hue}, 60%, ${20+flash}%)`);
  ctx.fillStyle = bodyGrd;
  ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI*2); ctx.fill();
  ctx.strokeStyle = '#000'; ctx.lineWidth = 3; ctx.stroke();

  // Horns
  ctx.fillStyle = '#1a0a0a';
  ctx.beginPath();
  ctx.moveTo(x-r*0.7, y-r*0.4);
  ctx.lineTo(x-r*1.15, y-r*1.35);
  ctx.lineTo(x-r*0.4, y-r*0.9);
  ctx.closePath(); ctx.fill();
  ctx.beginPath();
  ctx.moveTo(x+r*0.7, y-r*0.4);
  ctx.lineTo(x+r*1.15, y-r*1.35);
  ctx.lineTo(x+r*0.4, y-r*0.9);
  ctx.closePath(); ctx.fill();

  // Eyes
  ctx.fillStyle = '#fef08a';
  ctx.shadowColor = '#fef08a'; ctx.shadowBlur = 15;
  ctx.beginPath();
  ctx.arc(x-r*0.35, y-r*0.15, r*0.24, 0, Math.PI*2);
  ctx.arc(x+r*0.35, y-r*0.15, r*0.24, 0, Math.PI*2);
  ctx.fill();
  ctx.shadowBlur = 0;
  ctx.fillStyle = '#dc2626';
  ctx.beginPath();
  ctx.arc(x-r*0.35, y-r*0.15, r*0.11, 0, Math.PI*2);
  ctx.arc(x+r*0.35, y-r*0.15, r*0.11, 0, Math.PI*2);
  ctx.fill();

  // HP bar
  if(b.alive){
    const bw = 220;
    ctx.fillStyle = 'rgba(0,0,0,0.8)';
    ctx.fillRect(x-bw/2, y-r-36, bw, 16);
    const pct = b.hp/b.maxHp;
    const col = pct > 0.5 ? '#ef4444' : pct > 0.25 ? '#f59e0b' : '#7f1d1d';
    ctx.fillStyle = col;
    ctx.shadowColor = col; ctx.shadowBlur = 10;
    ctx.fillRect(x-bw/2, y-r-36, bw*pct, 16);
    ctx.shadowBlur = 0;
    ctx.strokeStyle = '#fff'; ctx.lineWidth = 2;
    ctx.strokeRect(x-bw/2, y-r-36, bw, 16);
    ctx.fillStyle = '#fff';
    ctx.font = '900 12px system-ui';
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.fillText('⚠️ BOSS ⚠️', x, y-r-28);
  }
  ctx.globalAlpha = 1;
}

function drawPlayer(){
  const x = S.px;
  const y = S.py + Math.sin(time*6) * 1.5;

  ctx.fillStyle = 'rgba(0,0,0,0.5)';
  ctx.beginPath(); ctx.ellipse(S.px, S.py+16, 17, 7, 0, 0, Math.PI*2); ctx.fill();

  // Aura
  const ch = CHARACTERS[S.currentChar];
  const auraColor = `hsl(${ch.hue}, 70%, 60%)`;
  const grd = ctx.createRadialGradient(x, y, 0, x, y, 38);
  grd.addColorStop(0, hexA(auraColor.replace('hsl',''), 0.4));
  grd.addColorStop(1, 'rgba(140,110,255,0)');
  ctx.fillStyle = grd;
  ctx.beginPath(); ctx.arc(x, y, 38, 0, Math.PI*2); ctx.fill();

  // Armor ring
  ctx.strokeStyle = ARMORS[S.currentArmor].color;
  ctx.lineWidth = 3;
  ctx.shadowColor = ARMORS[S.currentArmor].color; ctx.shadowBlur = 10;
  ctx.beginPath(); ctx.arc(x, y, 17, 0, Math.PI*2); ctx.stroke();
  ctx.shadowBlur = 0;

  // Body
  ctx.fillStyle = `hsl(${ch.hue}, 55%, 35%)`;
  ctx.beginPath(); ctx.arc(x, y, 15, 0, Math.PI*2); ctx.fill();
  ctx.fillStyle = ARMORS[S.currentArmor].color;
  ctx.beginPath(); ctx.arc(x-3, y-3, 9, 0, Math.PI*2); ctx.fill();

  // Hat
  ctx.fillStyle = `hsl(${ch.hue}, 55%, 22%)`;
  ctx.beginPath();
  ctx.moveTo(x, y-30); ctx.lineTo(x-14, y-4); ctx.lineTo(x+14, y-4);
  ctx.closePath(); ctx.fill();

  // Wand glow on hat
  ctx.fillStyle = WANDS[S.currentWand].color;
  ctx.shadowColor = WANDS[S.currentWand].color; ctx.shadowBlur = 12;
  ctx.beginPath(); ctx.arc(x, y-18, 3.5, 0, Math.PI*2); ctx.fill();
  ctx.shadowBlur = 0;

  // Eyes
  ctx.fillStyle = '#fff';
  ctx.beginPath();
  ctx.arc(x-5, y-2, 2.8, 0, Math.PI*2);
  ctx.arc(x+5, y-2, 2.8, 0, Math.PI*2);
  ctx.fill();
  ctx.fillStyle = '#000';
  ctx.beginPath();
  ctx.arc(x-5, y-2, 1.4, 0, Math.PI*2);
  ctx.arc(x+5, y-2, 1.4, 0, Math.PI*2);
  ctx.fill();

  // Range indicator
  ctx.strokeStyle = hexA(currentWorld().color, 0.15);
  ctx.lineWidth = 1.5;
  ctx.setLineDash([8, 8]);
  ctx.beginPath(); ctx.arc(S.px, S.py, playerRange(), 0, Math.PI*2); ctx.stroke();
  ctx.setLineDash([]);

  // Aim line
  if(mouseAim.has){
    ctx.strokeStyle = hexA(WANDS[S.currentWand].color, 0.3);
    ctx.lineWidth = 2;
    ctx.setLineDash([6, 6]);
    ctx.beginPath();
    ctx.moveTo(S.px, S.py);
    ctx.lineTo(mouseAim.x, mouseAim.y);
    ctx.stroke();
    ctx.setLineDash([]);
  }
}

function drawBullets(){
  for(const b of bullets){
    const a = Math.max(0, b.life / b.maxLife);
    ctx.globalAlpha = a;
    ctx.shadowColor = b.color;
    ctx.shadowBlur = b.crit ? 25 : 15;
    ctx.fillStyle = b.color;
    ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, Math.PI*2); ctx.fill();
    ctx.shadowBlur = 0;
    ctx.fillStyle = '#fff';
    ctx.beginPath(); ctx.arc(b.x, b.y, b.r*0.5, 0, Math.PI*2); ctx.fill();
  }
  ctx.globalAlpha = 1;
}

function drawParticles(){
  for(const p of particles){
    const a = Math.max(0, p.life / p.maxLife);
    ctx.globalAlpha = a;
    ctx.fillStyle = p.color;
    ctx.shadowColor = p.color;
    ctx.shadowBlur = 6;
    if(p.shape === 'star'){
      ctx.save();
      ctx.translate(p.x, p.y);
      ctx.rotate(p.rot);
      const s = p.size * a;
      ctx.beginPath();
      for(let i=0;i<5;i++){
        const ang = -Math.PI/2 + i*Math.PI*2/5;
        const x1 = Math.cos(ang)*s*1.6, y1 = Math.sin(ang)*s*1.6;
        const ang2 = ang + Math.PI/5;
        const x2 = Math.cos(ang2)*s*0.7, y2 = Math.sin(ang2)*s*0.7;
        if(i===0) ctx.moveTo(x1,y1); else ctx.lineTo(x1,y1);
        ctx.lineTo(x2,y2);
      }
      ctx.closePath();
      ctx.fill();
      ctx.restore();
    } else {
      ctx.beginPath(); ctx.arc(p.x, p.y, p.size*a, 0, Math.PI*2); ctx.fill();
    }
  }
  ctx.shadowBlur = 0;
  ctx.globalAlpha = 1;
}

function drawPopup(p){
  const t = p.life / p.maxLife;
  const alpha = t > 0.7 ? (1 - t)/0.3 : Math.min(1, t/0.35);
  ctx.save();
  ctx.globalAlpha = Math.max(0, Math.min(1, alpha));
  ctx.font = '900 15px system-ui, sans-serif';
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  ctx.shadowColor = p.color;
  ctx.shadowBlur = 16;
  ctx.strokeStyle = 'rgba(0,0,0,0.9)';
  ctx.lineWidth = 5;
  ctx.strokeText(p.text, p.x, p.y);
  ctx.fillStyle = p.color;
  ctx.fillText(p.text, p.x, p.y);
  ctx.restore();
}

function drawJoystick(){
  if(!joystick.active) return;
  ctx.strokeStyle = 'rgba(168,85,247,0.4)';
  ctx.lineWidth = 3;
  ctx.beginPath(); ctx.arc(joystick.bx, joystick.by, joystick.maxR, 0, Math.PI*2); ctx.stroke();
  ctx.fillStyle = 'rgba(168,85,247,0.12)';
  ctx.beginPath(); ctx.arc(joystick.bx, joystick.by, joystick.maxR, 0, Math.PI*2); ctx.fill();
  ctx.fillStyle = 'rgba(200,150,255,0.75)';
  ctx.shadowColor = '#a855f7'; ctx.shadowBlur = 15;
  ctx.beginPath(); ctx.arc(joystick.kx, joystick.ky, 28, 0, Math.PI*2); ctx.fill();
  ctx.shadowBlur = 0;
  ctx.strokeStyle = 'rgba(255,255,255,0.7)';
  ctx.lineWidth = 2;
  ctx.stroke();
}

function drawBanner(){
  if(!banner) return;
  const t = banner.life / banner.maxLife;
  let alpha = 1;
  if(t > 0.9) alpha = (1 - t) / 0.1;
  else if(t < 0.25) alpha = t / 0.25;

  const cx = W/2;
  const cy = H * 0.28 + 60;
  const scale = 1 + (1 - Math.min(1, (1-t)*4)) * 0.25;

  ctx.save();
  ctx.globalAlpha = alpha;
  ctx.translate(cx, cy);
  ctx.scale(scale, scale);

  const grd = ctx.createRadialGradient(0,0,0, 0,0,220);
  grd.addColorStop(0, hexA(banner.rarity.color, 0.5));
  grd.addColorStop(1, hexA(banner.rarity.color, 0));
  ctx.fillStyle = grd;
  ctx.beginPath(); ctx.arc(0,0,220,0,Math.PI*2); ctx.fill();

  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  ctx.font = '900 26px system-ui, sans-serif';
  ctx.shadowColor = banner.rarity.color;
  ctx.shadowBlur = 25;
  ctx.strokeStyle = 'rgba(0,0,0,0.8)';
  ctx.lineWidth = 5;
  ctx.strokeText(banner.rarity.name.toUpperCase(), 0, -22);
  ctx.fillStyle = banner.rarity.color;
  ctx.fillText(banner.rarity.name.toUpperCase(), 0, -22);

  if(banner.item){
    ctx.font = '900 17px system-ui, sans-serif';
    ctx.shadowBlur = 12;
    ctx.strokeText(banner.item, 0, 10);
    ctx.fillStyle = '#fff';
    ctx.fillText(banner.item, 0, 10);
  }
  ctx.restore();
}

function render(){
  ctx.save();
  if(shakeAmt > 0){
    ctx.translate((Math.random()-0.5)*shakeAmt, (Math.random()-0.5)*shakeAmt);
  }

  drawBackground();
  drawStars();
  drawGrid();

  ctx.save();
  ctx.translate(-camera.x, -camera.y);

  const viewL = camera.x - 100, viewR = camera.x + W + 100;
  const viewT = camera.y - 100, viewB = camera.y + H + 100;

  for(const c of S.chests){
    if(c.x > viewL && c.x < viewR && c.y > viewT && c.y < viewB) drawChest(c);
  }
  for(const m of S.monsters){
    if(m.x > viewL && m.x < viewR && m.y > viewT && m.y < viewB) drawMonster(m);
  }
  if(S.boss && S.boss.x > viewL && S.boss.x < viewR && S.boss.y > viewT && S.boss.y < viewB) drawBoss(S.boss);

  drawBullets();
  drawParticles();
  drawPlayer();
  for(const p of popups) drawPopup(p);

  ctx.restore();

  drawBanner();
  drawJoystick();

  ctx.restore();

  if(flash.life > 0){
    ctx.globalAlpha = Math.min(1, flash.life / 0.35) * 0.4;
    ctx.fillStyle = flash.color;
    ctx.fillRect(0,0,W,H);
    ctx.globalAlpha = 1;
  }
}

/* ============================================================
   BARS
============================================================ */
const hpFill = document.getElementById('hpFill');
const hpLabel = document.getElementById('hpLabel');
const manaFill = document.getElementById('manaFill');
function updateBars(){
  const mh = maxHp();
  const hp = Math.max(0, S.hp);
  hpFill.style.width = clamp(hp/mh, 0, 1) * 100 + '%';
  hpLabel.textContent = '❤️ ' + fmt(hp) + ' / ' + fmt(mh);
  manaFill.style.width = clamp((S.mana||maxMana())/maxMana(), 0, 1) * 100 + '%';
}

/* ============================================================
   TOP BAR
============================================================ */
const tbWorld   = document.getElementById('tbWorld');
const tbLevel   = document.getElementById('tbLevel');
const tbRebirth = document.getElementById('tbRebirth');
const tbCoins   = document.getElementById('tbCoins');
const tbTime    = document.getElementById('tbTime');
function updateTopBar(){
  tbWorld.textContent   = 'W' + S.currentWorld;
  tbLevel.textContent   = fmt(S.level);
  tbRebirth.textContent = fmt(S.rebirths);
  tbCoins.textContent   = fmt(S.coins);
  tbTime.textContent    = fmtTime(S.playtime);
}

/* ============================================================
   PANEL
============================================================ */
const panelEl = document.getElementById('panel');
const panelBody = document.getElementById('panelBody');
const panelTabs = document.getElementById('panelTabs');
const closeBtn = document.getElementById('closeBtn');
let currentTab = 'upgrades';
let currentPanel = null;

closeBtn.addEventListener('click', closePanel);
function closePanel(){
  panelEl.classList.add('hidden');
  currentPanel = null;
  panelTabs.innerHTML = '';
  panelBody.innerHTML = '';
  updateTopBar();
}
function openPanel(type, tab){
  currentPanel = type;
  currentTab = tab || (type === 'shop' ? 'upgrades' : type === 'top' ? 'global' : 'items');
  panelEl.classList.remove('hidden');
  renderPanel();
}
document.getElementById('btnShop').addEventListener('click', () => { ensureAudio(); openPanel('shop','upgrades'); });
document.getElementById('btnInv').addEventListener('click', () => { ensureAudio(); openPanel('inventory','items'); });
document.getElementById('btnSell').addEventListener('click', () => { ensureAudio(); sellAll(); });
document.getElementById('btnTop').addEventListener('click', () => { ensureAudio(); openPanel('top','global'); });

function buildTabs(tabs){
  panelTabs.innerHTML = '';
  tabs.forEach(t => {
    const b = el('button', 'tab' + (currentTab === t.id ? ' active' : ''), t.label);
    b.addEventListener('click', () => { currentTab = t.id; renderPanel(); });
    panelTabs.appendChild(b);
  });
}

function renderPanel(){
  if(currentPanel === 'shop'){
    buildTabs([
      { id:'upgrades',   label:'⚡ UPGRADES' },
      { id:'wands',      label:'🪄 WANDS' },
      { id:'armors',     label:'🛡️ ARMOR' },
      { id:'characters', label:'🧙 HEROES' },
      { id:'worlds',     label:'🌍 WORLDS' },
      { id:'training',   label:'🏋️ TRAIN' },
      { id:'rebirth',    label:'♻️ REBIRTH' },
    ]);
    if(currentTab === 'upgrades')   renderUpgrades();
    if(currentTab === 'wands')      renderWands();
    if(currentTab === 'armors')     renderArmors();
    if(currentTab === 'characters') renderCharacters();
    if(currentTab === 'worlds')     renderWorlds();
    if(currentTab === 'training')   renderTraining();
    if(currentTab === 'rebirth')    renderRebirth();
  } else if(currentPanel === 'inventory'){
    buildTabs([
      { id:'items',   label:'🎒 ITEMS' },
      { id:'chances', label:'🎲 CHANCES' },
    ]);
    if(currentTab === 'items')   renderInventory();
    if(currentTab === 'chances') renderChances();
  } else if(currentPanel === 'top'){
    buildTabs([{ id:'global', label:'🏆 TOP PLAYERS' }]);
    renderLeaderboard();
  }
}

/* UPGRADES */
const UPGRADES = [
  { id:'speed',    name:'Speed',        icon:'⚡', max:200, cost: lvl => Math.floor(50 * Math.pow(1.16, lvl)),
    desc: lvl => `Move speed +${lvl*22}` },
  { id:'range',    name:'Pickup Range', icon:'👁️', max:200, cost: lvl => Math.floor(120 * Math.pow(1.20, lvl)),
    desc: lvl => `Range +${lvl*10}` },
  { id:'luck',     name:'Luck',         icon:'🍀', max:1000, cost: lvl => Math.floor(250 * Math.pow(1.18, lvl)),
    desc: lvl => `Luck x${(1+lvl*0.25).toFixed(2)}` },
  { id:'clickDmg', name:'Wand Power',   icon:'🪄', max:1000, cost: lvl => Math.floor(400 * Math.pow(1.22, lvl)),
    desc: lvl => `+${lvl*15}% damage` },
  { id:'mana',     name:'Mana Pool',    icon:'💧', max:200, cost: lvl => Math.floor(300 * Math.pow(1.20, lvl)),
    desc: lvl => `+${lvl*15} mana` },
  { id:'crit',     name:'Critical Hit', icon:'🎯', max:75, cost: lvl => Math.floor(800 * Math.pow(1.25, lvl)),
    desc: lvl => `${(lvl*2).toFixed(0)}% crit chance` },
  { id:'regen',    name:'HP Regen',     icon:'💚', max:200, cost: lvl => Math.floor(600 * Math.pow(1.20, lvl)),
    desc: lvl => `+${(lvl*0.8).toFixed(1)} HP/s regen` },
];

function renderUpgrades(){
  panelBody.innerHTML = '';
  panelBody.appendChild(el('div', 'sectionTitle', '⚡ UPGRADES — Spend coins'));
  UPGRADES.forEach(up => {
    const lvl = S.upgrades[up.id];
    const maxed = lvl >= up.max;
    const cost = up.cost(lvl);
    const canBuy = !maxed && S.coins >= cost;
    const card = el('div', 'card');
    card.appendChild(el('div', 'icon', up.icon));
    const info = el('div', 'info');
    info.appendChild(el('div', 'title', `${up.name} — Lv ${lvl}/${up.max}`));
    info.appendChild(el('div', 'sub', maxed ? '✨ MAXED' : up.desc(lvl+1)));
    const bar = el('div', 'bar');
    const fill = el('div');
    fill.style.width = (lvl / up.max * 100) + '%';
    bar.appendChild(fill);
    info.appendChild(bar);
    card.appendChild(info);
    const right = el('div', 'right');
    right.appendChild(el('div', 'price', maxed ? '—' : '🪙 ' + fmt(cost)));
    const btn = el('button', 'buy' + (maxed ? ' owned' : ''), maxed ? 'MAX' : 'BUY');
    btn.disabled = !canBuy;
    btn.addEventListener('click', () => {
      if(maxed || S.coins < cost) return;
      S.coins -= cost;
      S.upgrades[up.id]++;
      beep(880, 0.08, 'triangle', 0.05);
      save(); renderPanel(); updateTopBar();
    });
    right.appendChild(btn);
    card.appendChild(right);
    panelBody.appendChild(card);
  });
}

/* WANDS */
function renderWands(){
  panelBody.innerHTML = '';
  panelBody.appendChild(el('div', 'sectionTitle', '🪄 WANDS — Weapon damage'));
  WANDS.forEach(w => {
    const owned = S.ownedWands.includes(w.id);
    const active = S.currentWand === w.id;
    const canBuy = !owned && S.coins >= w.cost;
    const card = el('div', 'card');
    const icon = el('div', 'icon', w.icon);
    icon.style.filter = `drop-shadow(0 0 10px ${w.color})`;
    card.appendChild(icon);
    const info = el('div', 'info');
    info.appendChild(el('div', 'title', `${w.name}`));
    info.appendChild(el('div', 'sub', `DMG ${fmt(w.dmg)}  •  ${owned ? (active ? '▶ Active' : 'Owned') : 'Cost: 🪙 ' + fmt(w.cost)}`));
    card.appendChild(info);
    const right = el('div', 'right');
    if(active){
      const b = el('button', 'buy active', 'ACTIVE'); b.disabled = true; right.appendChild(b);
    } else if(owned){
      const b = el('button', 'buy select', 'EQUIP');
      b.addEventListener('click', () => { S.currentWand = w.id; beep(660, 0.08, 'triangle', 0.05); save(); renderPanel(); });
      right.appendChild(b);
    } else {
      const b = el('button', 'buy', 'BUY');
      b.disabled = !canBuy;
      b.addEventListener('click', () => {
        if(S.coins < w.cost) return;
        S.coins -= w.cost; S.ownedWands.push(w.id); S.currentWand = w.id;
        beep(1046, 0.15, 'triangle', 0.08);
        flash.color = w.color; flash.life = 0.3;
        save(); renderPanel(); updateTopBar();
      });
      right.appendChild(b);
    }
    card.appendChild(right);
    panelBody.appendChild(card);
  });
}

/* ARMORS */
function renderArmors(){
  panelBody.innerHTML = '';
  panelBody.appendChild(el('div', 'sectionTitle', '🛡️ ARMOR — Increase max HP'));
  ARMORS.forEach(a => {
    const owned = S.ownedArmors.includes(a.id);
    const active = S.currentArmor === a.id;
    const canBuy = !owned && S.coins >= a.cost;
    const card = el('div', 'card');
    const icon = el('div', 'icon', a.icon);
    icon.style.filter = `drop-shadow(0 0 10px ${a.color})`;
    card.appendChild(icon);
    const info = el('div', 'info');
    info.appendChild(el('div', 'title', a.name));
    info.appendChild(el('div', 'sub', `HP +${fmt(a.hp)}  •  ${owned ? (active ? '▶ Active' : 'Owned') : 'Cost: 🪙 ' + fmt(a.cost)}`));
    card.appendChild(info);
    const right = el('div', 'right');
    if(active){
      const b = el('button', 'buy active', 'ACTIVE'); b.disabled = true; right.appendChild(b);
    } else if(owned){
      const b = el('button', 'buy select', 'EQUIP');
      b.addEventListener('click', () => { S.currentArmor = a.id; S.hp = maxHp(); beep(660, 0.08, 'triangle', 0.05); save(); renderPanel(); });
      right.appendChild(b);
    } else {
      const b = el('button', 'buy', 'BUY');
      b.disabled = !canBuy;
      b.addEventListener('click', () => {
        if(S.coins < a.cost) return;
        S.coins -= a.cost; S.ownedArmors.push(a.id); S.currentArmor = a.id; S.hp = maxHp();
        beep(1046, 0.15, 'triangle', 0.08);
        flash.color = a.color; flash.life = 0.3;
        save(); renderPanel(); updateTopBar();
      });
      right.appendChild(b);
    }
    card.appendChild(right);
    panelBody.appendChild(card);
  });
}

/* CHARACTERS */
function renderCharacters(){
  panelBody.innerHTML = '';
  panelBody.appendChild(el('div', 'sectionTitle', '🧙 HEROES — Multiplier (up to x1T)'));
  CHARACTERS.forEach(c => {
    const owned = S.ownedChars.includes(c.id);
    const active = S.currentChar === c.id;
    const canBuy = !owned && S.coins >= c.cost;
    const card = el('div', 'card');
    const icon = el('div', 'icon', c.icon);
    icon.style.filter = `hue-rotate(${c.hue}deg) drop-shadow(0 0 10px hsl(${c.hue}, 70%, 60%))`;
    card.appendChild(icon);
    const info = el('div', 'info');
    info.appendChild(el('div', 'title', `${c.name} — x${fmt(c.mult)}`));
    info.appendChild(el('div', 'sub', owned ? (active ? '▶ Active' : 'Owned') : 'Cost: 🪙 ' + fmt(c.cost)));
    card.appendChild(info);
    const right = el('div', 'right');
    if(active){
      const b = el('button', 'buy active', 'ACTIVE'); b.disabled = true; right.appendChild(b);
    } else if(owned){
      const b = el('button', 'buy select', 'SELECT');
      b.addEventListener('click', () => { S.currentChar = c.id; beep(660, 0.08, 'triangle', 0.05); save(); renderPanel(); });
      right.appendChild(b);
    } else {
      const b = el('button', 'buy', 'BUY');
      b.disabled = !canBuy;
      b.addEventListener('click', () => {
        if(S.coins < c.cost) return;
        S.coins -= c.cost; S.ownedChars.push(c.id); S.currentChar = c.id;
        beep(1046, 0.15, 'triangle', 0.08);
        flash.color = '#fcd34d'; flash.life = 0.3;
        save(); renderPanel(); updateTopBar();
      });
      right.appendChild(b);
    }
    card.appendChild(right);
    panelBody.appendChild(card);
  });
}

/* WORLDS */
function renderWorlds(){
  panelBody.innerHTML = '';
  panelBody.appendChild(el('div', 'sectionTitle', '🌍 WORLDS — Stronger monsters, better loot'));
  WORLDS.forEach(w => {
    const unlocked = S.unlockedWorlds.includes(w.id) || S.rebirths >= w.unlockRebirth;
    const active = S.currentWorld === w.id;
    const row = el('div', 'worldRow');
    row.style.setProperty('--wc', w.color);
    row.appendChild(el('div', 'wIco', w.icon));
    const info = el('div', 'wI');
    info.appendChild(el('div', 'wT', w.name));
    info.appendChild(el('div', 'wS',
      `Monster HP: ${fmt(w.monsterHp)}  •  Coin x${fmt(w.monsterMult)}<br>` +
      `Boss HP: ${fmt(w.bossHp)}  •  Boss drop x${fmt(w.bossMult)}<br>` +
      `Requires: ${w.unlockRebirth} Rebirths`
    ));
    row.appendChild(info);
    const b = el('button', 'wB');
    if(active){
      b.textContent = 'ACTIVE'; b.disabled = true;
    } else if(unlocked){
      b.textContent = 'ENTER';
      b.addEventListener('click', () => {
        S.currentWorld = w.id;
        if(!S.unlockedWorlds.includes(w.id)) S.unlockedWorlds.push(w.id);
        initWorld();
        beep(660, 0.1, 'triangle', 0.06);
        toast('Entered ' + w.name, w.color);
        save(); renderPanel(); updateTopBar();
      });
    } else {
      b.textContent = '🔒 ' + w.unlockRebirth;
      b.disabled = true;
    }
    row.appendChild(b);
    panelBody.appendChild(row);
  });
}

/* TRAINING */
function renderTraining(){
  panelBody.innerHTML = '';
  panelBody.appendChild(el('div', 'sectionTitle', '🏋️ TRAINING GROUNDS — Bonus based on Rebirths'));
  TRAIN_GROUNDS.forEach(t => {
    const unlocked = S.rebirths >= t.rebirth;
    const active = S.currentTrain === t.id;
    const card = el('div', 'card');
    const icon = el('div', 'icon', t.icon);
    icon.style.filter = `drop-shadow(0 0 10px ${t.color})`;
    card.appendChild(icon);
    const info = el('div', 'info');
    info.appendChild(el('div', 'title', t.name));
    info.appendChild(el('div', 'sub', `XP x${t.xpMult}  •  +${(t.xpMult-1)*5}% wand dmg`));
    info.appendChild(el('div', 'sub', unlocked ? (active ? '▶ Training here' : 'Unlocked') : `🔒 Needs ${t.rebirth} Rebirths`));
    card.appendChild(info);
    const right = el('div', 'right');
    if(active){
      const b = el('button', 'buy active', 'HERE'); b.disabled = true; right.appendChild(b);
    } else if(unlocked){
      const b = el('button', 'buy select', 'TRAIN');
      b.addEventListener('click', () => { S.currentTrain = t.id; beep(660, 0.08, 'triangle', 0.05); save(); renderPanel(); });
      right.appendChild(b);
    } else {
      const b = el('button', 'buy', '🔒'); b.disabled = true; right.appendChild(b);
    }
    card.appendChild(right);
    panelBody.appendChild(card);
  });
}

/* REBIRTH */
function renderRebirth(){
  panelBody.innerHTML = '';
  panelBody.appendChild(el('div', 'sectionTitle', '♻️ REBIRTH — Permanent boost'));
  const cost = rebirthCost();
  const canRebirth = S.coins >= cost;
  const info = el('div', 'card');
  info.appendChild(el('div', 'icon', '♻️'));
  const ii = el('div', 'info');
  ii.appendChild(el('div', 'title', `Rebirths: ${S.rebirths}`));
  ii.appendChild(el('div', 'sub', `Current bonus: +${S.rebirths*50}% to coins & damage`));
  ii.appendChild(el('div', 'sub', `Next: +${(S.rebirths+1)*50}% total`));
  info.appendChild(ii);
  panelBody.appendChild(info);

  const costCard = el('div', 'card');
  costCard.appendChild(el('div', 'icon', '💰'));
  const ci = el('div', 'info');
  ci.appendChild(el('div', 'title', 'Cost to Rebirth'));
  ci.appendChild(el('div', 'sub', '🪙 ' + fmt(cost)));
  costCard.appendChild(ci);
  panelBody.appendChild(costCard);

  const btn = el('button', 'bigBtn', canRebirth ? '♻️ REBIRTH NOW' : '❌ Not enough coins');
  btn.disabled = !canRebirth;
  btn.addEventListener('click', () => {
    if(S.coins < cost) return;
    S.coins = 0; S.rebirths++;
    S.upgrades = { speed:0, range:0, luck:0, mana:0, clickDmg:0, crit:0, regen:0 };
    S.inventory = RARITIES.reduce((o,r)=>(o[r.id]=0,o),{});
    S.hp = maxHp();
    beep(1318, 0.4, 'triangle', 0.1);
    flash.color = '#ec4899'; flash.life = 0.6;
    shakeAmt = 30;
    toast('♻️ REBIRTH #' + S.rebirths + '!', '#ec4899');
    save(); renderPanel(); updateTopBar();
  });
  panelBody.appendChild(btn);
  panelBody.appendChild(el('div', 'hint',
    '⚠️ Resets: coins, upgrades, inventory.<br>Keeps: characters, wands, armor, worlds, level, playtime, rebirths.'));
}

/* INVENTORY */
function sellPrice(rarityId){
  const r = RARITIES.find(x => x.id === rarityId);
  return Math.floor(r.pts * totalMult());
}
function renderInventory(){
  panelBody.innerHTML = '';
  let totalValue = 0, totalItems = 0;
  RARITIES.forEach(r => {
    const cnt = S.inventory[r.id] || 0;
    totalValue += cnt * sellPrice(r.id);
    totalItems += cnt;
  });

  const sum = el('div', 'card');
  sum.appendChild(el('div', 'icon', '🎒'));
  const si = el('div', 'info');
  si.appendChild(el('div', 'title', `Total Items: ${fmt(totalItems)}`));
  si.appendChild(el('div', 'sub', `Total Value: 🪙 ${fmt(totalValue)}`));
  sum.appendChild(si);
  panelBody.appendChild(sum);

  const sellBtn = el('button', 'bigBtn green', '💰 SELL ALL — 🪙 ' + fmt(totalValue));
  sellBtn.disabled = totalValue === 0;
  sellBtn.addEventListener('click', () => sellAll());
  panelBody.appendChild(sellBtn);

  panelBody.appendChild(el('div', 'sectionTitle', 'Items'));
  const chances = currentChances();
  RARITIES.forEach((r, i) => {
    const cnt = S.inventory[r.id] || 0;
    const pct = (chances[i] * 100);
    const price = sellPrice(r.id);
    const row = el('div', 'rarityRow');
    row.style.setProperty('--rc', r.color);
    row.appendChild(el('div', 'rn', r.name));
    row.appendChild(el('div', 'rp', pct < 0.0001 ? '<0.0001%' : pct < 0.01 ? pct.toFixed(4) + '%' : pct.toFixed(2) + '%'));
    row.appendChild(el('div', 'rc', '×' + fmt(cnt)));
    const b = el('button', 'sellOne', 'Sell');
    b.disabled = cnt === 0;
    b.addEventListener('click', () => {
      if(cnt === 0) return;
      S.coins += cnt * price;
      S.inventory[r.id] = 0;
      beep(660, 0.06, 'triangle', 0.04);
      save(); renderPanel(); updateTopBar();
    });
    row.appendChild(b);
    panelBody.appendChild(row);
  });
}
function sellAll(){
  let gain = 0;
  RARITIES.forEach(r => {
    const cnt = S.inventory[r.id] || 0;
    if(cnt > 0){ gain += cnt * sellPrice(r.id); S.inventory[r.id] = 0; }
  });
  if(gain === 0) { toast('Nothing to sell'); return; }
  S.coins += gain;
  beep(880, 0.1, 'triangle', 0.06);
  beep(1318, 0.1, 'triangle', 0.05);
  popups.push({x:S.px, y:S.py-30, vy:-90, life:1.6, maxLife:1.6, text:'+🪙 '+fmt(gain), color:'#4ade80', tier:4});
  toast('💰 Sold for 🪙 ' + fmt(gain), '#4ade80');
  save();
  if(currentPanel === 'inventory') renderPanel();
  updateTopBar();
}

/* CHANCES */
function renderChances(){
  panelBody.innerHTML = '';
  const luck = luckValue();
  panelBody.appendChild(el('div', 'sectionTitle', '🍀 Live Drop Chances'));
  const info = el('div', 'card');
  info.appendChild(el('div', 'icon', '🍀'));
  const ii = el('div', 'info');
  ii.appendChild(el('div', 'title', 'Luck: x' + luck.toFixed(2)));
  ii.appendChild(el('div', 'sub', 'Higher luck = more rare drops.'));
  info.appendChild(ii);
  panelBody.appendChild(info);

  const chances = currentChances();
  RARITIES.forEach((r, i) => {
    const pct = chances[i] * 100;
    const row = el('div', 'rarityRow');
    row.style.setProperty('--rc', r.color);
    row.appendChild(el('div', 'rn', r.name));
    row.appendChild(el('div', 'rp', pct < 0.0001 ? '<0.0001%' : pct < 0.01 ? pct.toFixed(4) + '%' : pct.toFixed(2) + '%'));
    row.appendChild(el('div', 'rc', '🪙 ' + fmt(sellPrice(r.id))));
    panelBody.appendChild(row);
  });

  panelBody.appendChild(el('div', 'sectionTitle', '📦 Chest Tiers'));
  CHEST_TIERS.forEach(t => {
    const card = el('div', 'card');
    card.appendChild(el('div', 'icon', '📦'));
    const ci = el('div', 'info');
    ci.appendChild(el('div', 'title', t.id.charAt(0).toUpperCase() + t.id.slice(1) + ' Chest'));
    ci.appendChild(el('div', 'sub', `Luck x${t.luck}  •  ~${t.weight}% spawn`));
    card.appendChild(ci);
    panelBody.appendChild(card);
  });
}

/* LEADERBOARD */
function playerScore(){
  return Math.floor(S.coins + S.rebirths * 1e6 + S.level * 5000 + S.stats.kills * 100 + S.stats.bossKills * 50000);
}
function renderLeaderboard(){
  panelBody.innerHTML = '';
  panelBody.appendChild(el('div', 'sectionTitle', '🏆 TOP PLAYERS — Live Ranking'));
  const entries = S.leaderboard.map(l => ({...l, isPlayer: false}));
  entries.push({ name: S.playerName, score: playerScore(), rebirths: S.rebirths, isPlayer: true });
  entries.sort((a,b) => b.score - a.score);
  entries.forEach((e, i) => {
    const row = el('div', 'leaderRow' + (i === 0 ? ' top1' : i === 1 ? ' top2' : i === 2 ? ' top3' : '') + (e.isPlayer ? ' you' : ''));
    row.appendChild(el('div', 'lrank', '#' + (i+1)));
    row.appendChild(el('div', 'lname', e.name + (e.isPlayer ? ' ⭐' : '')));
    const right = el('div');
    right.style.textAlign = 'right';
    right.innerHTML = `<div style="font-weight:900;color:#fcd34d;font-family:monospace">${fmt(e.score)}</div><div style="font-size:10px;opacity:0.6">${e.rebirths}♻️</div>`;
    row.appendChild(right);
    panelBody.appendChild(row);
  });
  panelBody.appendChild(el('div', 'hint', 'Score = coins + rebirths × 1M + level × 5K + kills × 100 + bossKills × 50K'));
}

/* ============================================================
   INIT
============================================================ */
function initStars(){
  stars = [];
  for(let i=0;i<80;i++){
    stars.push({
      x: Math.random() * W,
      y: Math.random() * H,
      s: Math.random() < 0.8 ? 1 : 2,
      a: 0.3 + Math.random()*0.7,
      tw: Math.random()*6.28,
    });
  }
}
initStars();
window.addEventListener('resize', initStars);

S.hp = maxHp();
S.mana = maxMana();
initWorld();
updateTopBar();
updateBars();

/* Loading */
let loadP = 0;
const loadFill = document.getElementById('loadingFill');
const loadEl = document.getElementById('loading');
const loadInt = setInterval(() => {
  loadP += rnd(15, 30);
  if(loadP >= 100){ loadP = 100; clearInterval(loadInt); }
  loadFill.style.width = loadP + '%';
}, 200);
loadEl.addEventListener('click', () => {
  loadP = 100;
  loadFill.style.width = '100%';
  loadEl.classList.add('hide');
  ensureAudio();
});
setTimeout(() => { loadEl.classList.add('hide'); }, 2200);

/* Game loop */
function frame(now){
  let dt = (now - lastT) / 1000;
  lastT = now;
  if(dt > 0.05) dt = 0.05;
  update(dt);
  render();
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

window.addEventListener('beforeunload', save);
setInterval(save, 15000);

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

Game Source: Magic Loot 2D — Legendary

Creator: LaserLegend64

Libraries: none

Complexity: complex (2355 lines, 85.5 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: magic-loot-2d-legendary-laserlegend64" to link back to the original. Then publish at arcadelab.ai/publish.