Magic Loot 2D — Legendary
by LaserLegend642355 lines85.5 KB
<!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.