quantpoe / index.html
PeetPedro's picture
QUANTPOE for Marci — quantum ARPG; loot in superposition collapses to rarity (Born rule) on pickup
036bbb7 verified
Raw
History Blame Contribute Delete
19.9 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">
<title>QUANTPOE</title>
<meta name="description" content="Quantum Path of Exile. Twin-stick ARPG where the loot drops in superposition — it collapses to a rarity (Born rule) only when you grab it. Slay, collect, ascend. For Marci.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@500;700;900&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
<style>
:root{--bg:#0a0706;--ink:#e8ddc8;--dim:#8a7a63;--blood:#c0261f;--gold:#e6c05a;--cyan:#39e6ff;--magic:#5aa0ff;--rare:#f5d84a;--uniq:#e08a2a;--mono:'JetBrains Mono',monospace;--disp:'Cinzel',serif}
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent;user-select:none}
html,body{height:100%;overflow:hidden;background:#000;color:var(--ink);font-family:var(--mono)}
#stage{position:fixed;inset:0}
canvas{position:absolute;inset:0;width:100%;height:100%;cursor:crosshair}
#hud{position:absolute;inset:0;pointer-events:none;z-index:5}
.top{position:absolute;top:0;left:0;right:0;display:flex;justify-content:space-between;padding:12px 16px;font-family:var(--mono);font-size:12px}
.stat{color:var(--dim)}.stat b{color:var(--gold);font-family:var(--disp);font-weight:700}
.top .r{text-align:right}
#lvl b{color:var(--cyan)}
/* orbs */
#orbs{position:absolute;bottom:14px;left:16px;display:flex;gap:14px;align-items:flex-end}
.orb{width:60px;height:60px;border-radius:50%;position:relative;border:2px solid #2a1c16;overflow:hidden;box-shadow:inset 0 0 12px #000,0 0 8px #000}
.orb .fill{position:absolute;left:0;right:0;bottom:0;transition:height .15s}
#hp .fill{background:linear-gradient(180deg,#e0453a,#8f120c)}
#hp .lab{position:absolute;inset:0;display:grid;place-items:center;font-size:11px;font-weight:700;color:#fff;text-shadow:0 1px 2px #000}
#xpbar{position:absolute;bottom:0;left:0;right:0;height:5px;background:#160f0c}
#xpbar .fill{height:100%;background:linear-gradient(90deg,#39e6ff,#5aa0ff);width:0;box-shadow:0 0 8px #39e6ff}
#feed{position:absolute;bottom:84px;left:16px;display:flex;flex-direction:column;gap:2px;font-family:var(--mono);font-size:12px}
#feed div{opacity:0;animation:rise 2.6s ease-out forwards}
@keyframes rise{0%{opacity:0;transform:translateY(6px)}12%{opacity:1}80%{opacity:1}100%{opacity:0;transform:translateY(-6px)}}
#nova{position:absolute;right:18px;bottom:20px;width:74px;height:74px;border-radius:50%;pointer-events:auto;display:grid;place-items:center;font-family:var(--disp);font-size:12px;font-weight:700;color:var(--cyan);background:rgba(57,230,255,.1);border:2px solid rgba(57,230,255,.4)}
#nova.cd{opacity:.35}#nova:active{background:rgba(57,230,255,.3)}
@media(hover:hover){#nova{display:none}}
/* overlays */
.screen{position:absolute;inset:0;z-index:10;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:24px;background:radial-gradient(120% 90% at 50% 30%,rgba(192,38,31,.12),transparent 60%),rgba(10,7,6,.9);pointer-events:auto}
.screen.hide{display:none}
h1{font-family:var(--disp);font-weight:900;font-size:clamp(46px,13vw,110px);line-height:.9;letter-spacing:.03em;background:linear-gradient(180deg,#f5d84a,#c0261f 70%,#6a0f0a);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;filter:drop-shadow(0 3px 30px rgba(192,38,31,.4))}
.sub{font-family:var(--mono);font-size:10px;letter-spacing:.34em;color:var(--dim);text-transform:uppercase;margin-top:8px}
.ded{font-family:var(--disp);font-style:italic;font-size:17px;color:var(--gold);margin-top:12px}
.tag{color:var(--dim);max-width:30rem;margin:20px 0 4px;font-size:13px;line-height:1.6}.tag b{color:var(--ink)}.tag .c{color:var(--cyan)}
.keys{font-family:var(--mono);font-size:11px;color:var(--dim);margin-top:16px;line-height:1.7}.keys b{color:var(--ink)}
button.go{font-family:var(--disp);font-weight:700;font-size:22px;letter-spacing:.05em;cursor:pointer;color:#160500;background:linear-gradient(180deg,#f5d84a,#d89a1e);border:0;border-radius:8px;padding:12px 40px;margin-top:22px;box-shadow:0 5px 26px rgba(230,192,90,.3)}
button.go:active{transform:translateY(2px)}
.fine{margin-top:20px;font-family:var(--mono);font-size:9px;letter-spacing:.16em;color:#5a3f2f;text-transform:uppercase}.fine .f{color:var(--blood)}
#go-score{font-family:var(--disp);font-weight:900;font-size:clamp(30px,7vw,52px);color:var(--gold);margin:6px 0}
</style>
</head>
<body>
<div id="stage">
<canvas id="c"></canvas>
<div id="hud">
<div class="top">
<div class="stat" id="lvl">exile lvl <b>1</b></div>
<div class="stat" style="text-align:center">slain <b id="slain">0</b></div>
<div class="stat r"><b id="curr">0</b> <span style="color:var(--dim)">chaos</span></div>
</div>
<div id="feed"></div>
<div id="orbs"><div class="orb" id="hp"><div class="fill" style="height:100%"></div><div class="lab" id="hp-lab">100</div></div></div>
<div id="nova">NOVA</div>
<div id="xpbar"><div class="fill" id="xp-fill"></div></div>
</div>
<div class="screen" id="start">
<h1>QUANTPOE</h1>
<div class="sub">quantum &middot; path of exile</div>
<div class="ded">for Marci</div>
<p class="tag">An arena of the Atlas. Loot drops in <span class="c">superposition</span> — shimmering through every rarity at once — and only <b>collapses</b> to what it truly is when you <b>grab it</b>. That's the Born rule: the rarer the roll, the smaller its amplitude. Slay, collect, ascend.</p>
<p class="keys"><b>move</b> WASD / arrows &nbsp;·&nbsp; <b>aim &amp; fire</b> mouse / hold &nbsp;·&nbsp; <b>Collapse Nova</b> Space<br>touch: left side to move · right side to fire · NOVA button</p>
<button class="go" id="start-btn">ENTER THE ATLAS</button>
<p class="fine">🜂 loot is a measurement &middot; <span class="f">ahogy a dolgok vannak</span></p>
</div>
<div class="screen hide" id="over">
<div class="sub">you have died, exile</div>
<div id="go-score">0</div>
<p class="tag" style="margin-top:0">reached <b id="go-lvl">lvl 1</b> &middot; slew <b id="go-slain">0</b> &middot; hoarded <b id="go-curr">0</b> chaos</p>
<button class="go" id="again-btn">RISE AGAIN</button>
<p class="fine">PeetPedro &middot; entropy-om &middot; <span class="f">for Marci</span></p>
</div>
</div>
<script>
(function(){
"use strict";
var cv=document.getElementById('c'), ctx=cv.getContext('2d');
var W=0,H=0,DPR=Math.min(2,devicePixelRatio||1);
var RARITY=[
{name:'common',col:'#c8bfa8',p:0.60,val:1,orb:'Scroll of Wisdom'},
{name:'magic', col:'#5aa0ff',p:0.24,val:4,orb:'Orb of Alteration'},
{name:'rare', col:'#f5d84a',p:0.13,val:12,orb:'Chaos Orb'},
{name:'unique',col:'#e08a2a',p:0.03,val:60,orb:'Exalted Orb'}
];
var player, mobs, bolts, loots, parts, keys={}, aim={x:0,y:0,down:false}, moveStick=null;
var running=false, slain=0, curr=0, wave=0, spawnT=0, novaCd=0, shake=0, t=0;
var best=+(localStorage.getItem('qpoe_best')||0);
// audio
var actx=null;
function A(){ if(!actx){try{actx=new(window.AudioContext||window.webkitAudioContext)();}catch(e){}} if(actx&&actx.state==='suspended')actx.resume(); }
function beep(f,d,v,type){ if(!actx)return; var n=actx.currentTime,o=actx.createOscillator(),g=actx.createGain();
o.type=type||'square'; o.frequency.setValueAtTime(f,n); g.gain.setValueAtTime(v||.08,n); g.gain.exponentialRampToValueAtTime(.0005,n+(d||.1));
o.connect(g);g.connect(actx.destination);o.start(n);o.stop(n+(d||.1)+.02); }
function resize(){ W=innerWidth;H=innerHeight; cv.width=W*DPR;cv.height=H*DPR; ctx.setTransform(DPR,0,0,DPR,0,0); }
addEventListener('resize',resize);
function start(){
running=true; slain=0; curr=0; wave=0; spawnT=0; novaCd=0; shake=0;
player={x:W/2,y:H/2,r:15,hp:100,maxHp:100,lvl:1,xp:0,xpNext:8,dmg:14,spd:230,fireCd:0};
mobs=[];bolts=[];loots=[];parts=[];
aim.x=W/2;aim.y=H*0.3;
document.getElementById('start').classList.add('hide'); document.getElementById('over').classList.add('hide');
document.getElementById('feed').innerHTML='';
hud();
for(var i=0;i<4;i++) spawnMob();
}
function over(){ running=false;
if(slain>best){best=slain;localStorage.setItem('qpoe_best',best);}
document.getElementById('go-score').textContent=slain+' slain';
document.getElementById('go-lvl').textContent='lvl '+player.lvl;
document.getElementById('go-slain').textContent=slain;
document.getElementById('go-curr').textContent=curr;
document.getElementById('over').classList.remove('hide');
}
function feed(txt,col){ var f=document.getElementById('feed'); var d=document.createElement('div');
d.style.color=col||'#c8bfa8'; d.textContent=txt; f.appendChild(d); setTimeout(function(){d.remove();},2600); }
function hud(){
document.getElementById('lvl').innerHTML='exile lvl <b>'+player.lvl+'</b>';
document.getElementById('slain').textContent=slain;
document.getElementById('curr').textContent=curr;
var hpf=Math.max(0,player.hp/player.maxHp);
document.querySelector('#hp .fill').style.height=(hpf*100)+'%';
document.getElementById('hp-lab').textContent=Math.max(0,Math.round(player.hp));
document.getElementById('xp-fill').style.width=(player.xp/player.xpNext*100)+'%';
document.getElementById('nova').classList.toggle('cd', novaCd>0);
}
function spawnMob(){
var edge=Math.floor(Math.random()*4), x,y;
if(edge===0){x=Math.random()*W;y=-20;} else if(edge===1){x=W+20;y=Math.random()*H;}
else if(edge===2){x=Math.random()*W;y=H+20;} else {x=-20;y=Math.random()*H;}
var tier=Math.random()< (0.08+wave*0.01)? (Math.random()<0.25?2:1):0; // 0 normal,1 magic,2 rare
var hpm=[1,2.4,5][tier], sc=[1,1.25,1.6][tier];
mobs.push({x:x,y:y,r:12*sc,hp:14*hpm*(1+wave*0.08),maxHp:14*hpm*(1+wave*0.08),
spd:60+Math.random()*40+wave*3, tier:tier, hit:0, dmg:7+wave*0.6});
}
function dropLoot(x,y){ // a SUPERPOSED orb: rarity undetermined until collected
if(Math.random()<0.62) loots.push({x:x,y:y,r:9,phase:Math.random()*6.28,born:t,collapsed:false});
}
function collapseLoot(l){ // measurement: Born rule over rarities
var r=Math.random(),acc=0,pick=RARITY[0];
for(var i=0;i<RARITY.length;i++){ acc+=RARITY[i].p; if(r<=acc){pick=RARITY[i];break;} }
curr+=pick.val; feed('+'+pick.val+' · '+pick.orb, pick.col);
beep(pick.name==='unique'?900:pick.name==='rare'?620:440,0.14,0.1,'triangle');
if(pick.name==='unique'){ shake=Math.max(shake,10); }
for(var p=0;p<8;p++){ var a=Math.random()*6.28,s=1+Math.random()*3;
parts.push({x:l.x,y:l.y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,life:1,col:pick.col}); }
hud();
}
function gainXp(n){ player.xp+=n; if(player.xp>=player.xpNext){ player.xp-=player.xpNext; player.lvl++;
player.xpNext=Math.round(player.xpNext*1.35); player.maxHp+=18; player.hp=player.maxHp; player.dmg+=4;
feed('LEVEL '+player.lvl+' — ascended', '#39e6ff'); beep(700,0.2,0.12,'sine'); shake=Math.max(shake,6); } hud(); }
function fire(){
if(player.fireCd>0) return; player.fireCd=0.16;
var dx=aim.x-player.x, dy=aim.y-player.y, d=Math.hypot(dx,dy)||1;
var crit=Math.random()<0.18; // Born-rule flavored crit
bolts.push({x:player.x,y:player.y,vx:dx/d*620,vy:dy/d*620,life:1.1,dmg:player.dmg*(crit?2.6:1),crit:crit});
beep(crit?520:300,0.06,0.05,'square');
}
function nova(){ if(novaCd>0)return; novaCd=4.5; shake=Math.max(shake,14); beep(120,0.4,0.14,'sawtooth');
parts.push({ring:true,x:player.x,y:player.y,r:0,life:1});
for(var i=mobs.length-1;i>=0;i--){ var m=mobs[i]; if(Math.hypot(m.x-player.x,m.y-player.y)<220){
m.hp-=player.dmg*3.2; m.hit=1; if(m.hp<=0) killMob(i); } }
hud();
}
function killMob(i){ var m=mobs[i]; slain++; gainXp([1,2,4][m.tier]); dropLoot(m.x,m.y);
for(var p=0;p<7;p++){var a=Math.random()*6.28,s=1+Math.random()*2.6;
parts.push({x:m.x,y:m.y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,life:1,col:m.tier?'#f5d84a':'#c0261f'});}
mobs.splice(i,1); beep(180,0.08,0.05,'square'); }
// input
function toWorld(e){ return {x:e.clientX,y:e.clientY}; }
addEventListener('keydown',function(e){ var k=e.key.length===1?e.key.toLowerCase():e.key; keys[k]=true;
if(k===' '){e.preventDefault();A();nova();} });
addEventListener('keyup',function(e){ keys[e.key.length===1?e.key.toLowerCase():e.key]=false; });
cv.addEventListener('pointermove',function(e){ if(e.pointerType==='mouse'){aim.x=e.clientX;aim.y=e.clientY;}
else if(moveStick&&e.pointerId===moveStick.id){ moveStick.dx=e.clientX-moveStick.ox; moveStick.dy=e.clientY-moveStick.oy; }
else if(aim.pid===e.pointerId){ aim.x=e.clientX;aim.y=e.clientY; } });
cv.addEventListener('pointerdown',function(e){ A(); if(!running)return;
if(e.pointerType==='mouse'){ aim.down=true; aim.x=e.clientX;aim.y=e.clientY; return; }
if(e.clientX<W*0.5){ moveStick={id:e.pointerId,ox:e.clientX,oy:e.clientY,dx:0,dy:0}; }
else { aim.pid=e.pointerId; aim.down=true; aim.x=e.clientX;aim.y=e.clientY; } });
function up(e){ if(e.pointerType==='mouse'){aim.down=false;return;}
if(moveStick&&e.pointerId===moveStick.id) moveStick=null;
if(aim.pid===e.pointerId){aim.down=false;aim.pid=null;} }
cv.addEventListener('pointerup',up); cv.addEventListener('pointercancel',up);
document.getElementById('nova').addEventListener('pointerdown',function(e){e.preventDefault();A();nova();});
document.getElementById('start-btn').addEventListener('click',function(){A();start();});
document.getElementById('again-btn').addEventListener('click',function(){A();start();});
function update(dt){ t+=dt;
// move
var mx=(keys.a||keys.arrowleft?-1:0)+(keys.d||keys.arrowright?1:0);
var my=(keys.w||keys.arrowup?-1:0)+(keys.s||keys.arrowdown?1:0);
if(moveStick){ var ml=Math.hypot(moveStick.dx,moveStick.dy); if(ml>6){mx=moveStick.dx/ml;my=moveStick.dy/ml;} }
var ml2=Math.hypot(mx,my); if(ml2>0){ player.x+=mx/ml2*player.spd*dt; player.y+=my/ml2*player.spd*dt; }
player.x=Math.max(player.r,Math.min(W-player.r,player.x)); player.y=Math.max(player.r,Math.min(H-player.r,player.y));
if(moveStick&&!aim.down){ /* face move dir when not aiming (touch) */ if(ml2>0){aim.x=player.x+mx*40;aim.y=player.y+my*40;} }
// fire
player.fireCd-=dt; if(aim.down) fire();
if(novaCd>0){novaCd-=dt; if(novaCd<=0)hud();}
// spawns escalate
spawnT+=dt; wave+=dt*0.05; var cap=6+Math.floor(wave*1.5);
if(spawnT> Math.max(0.5,1.6-wave*0.05) && mobs.length<cap){ spawnT=0; spawnMob(); }
// mobs
for(var i=mobs.length-1;i>=0;i--){ var m=mobs[i]; if(m.hit>0)m.hit-=dt*3;
var dx=player.x-m.x,dy=player.y-m.y,d=Math.hypot(dx,dy)||1; m.x+=dx/d*m.spd*dt; m.y+=dy/d*m.spd*dt;
if(d<m.r+player.r){ player.hp-=m.dmg*dt*2.2; shake=Math.max(shake,3); if(player.hp<=0){hud();over();return;} } }
if(Math.random()<0.3) hud();
// bolts
for(var b=bolts.length-1;b>=0;b--){ var o=bolts[b]; o.x+=o.vx*dt;o.y+=o.vy*dt;o.life-=dt;
if(o.life<=0||o.x<-20||o.x>W+20||o.y<-20||o.y>H+20){bolts.splice(b,1);continue;}
for(var k=mobs.length-1;k>=0;k--){ var m2=mobs[k]; if(Math.hypot(m2.x-o.x,m2.y-o.y)<m2.r+4){
m2.hp-=o.dmg; m2.hit=1; for(var p=0;p<(o.crit?6:3);p++){var a=Math.random()*6.28;parts.push({x:o.x,y:o.y,vx:Math.cos(a)*2,vy:Math.sin(a)*2,life:.6,col:o.crit?'#39e6ff':'#e6c05a'});}
bolts.splice(b,1); if(m2.hp<=0)killMob(k); break; } } }
// loot
for(var l=loots.length-1;l>=0;l--){ var lo=loots[l]; lo.phase+=dt*4;
if(Math.hypot(lo.x-player.x,lo.y-player.y)<player.r+lo.r+6){ collapseLoot(lo); loots.splice(l,1); }
else if(t-lo.born>14){ loots.splice(l,1); } }
// particles
for(var q=parts.length-1;q>=0;q--){ var pr=parts[q];
if(pr.ring){ pr.r+=dt*700; pr.life-=dt*1.6; if(pr.life<=0)parts.splice(q,1); }
else { pr.x+=pr.vx;pr.y+=pr.vy;pr.vx*=.92;pr.vy*=.92;pr.life-=dt*1.8; if(pr.life<=0)parts.splice(q,1); } }
if(shake>0)shake=Math.max(0,shake-dt*30);
}
function draw(){
ctx.fillStyle='#0a0706'; ctx.fillRect(0,0,W,H);
// gritty floor grid
ctx.strokeStyle='rgba(80,50,30,0.08)'; ctx.lineWidth=1;
for(var gx=0;gx<W;gx+=48){ctx.beginPath();ctx.moveTo(gx,0);ctx.lineTo(gx,H);ctx.stroke();}
for(var gy=0;gy<H;gy+=48){ctx.beginPath();ctx.moveTo(0,gy);ctx.lineTo(W,gy);ctx.stroke();}
var sx=0,sy=0; if(shake>0){sx=(Math.random()*2-1)*shake;sy=(Math.random()*2-1)*shake;}
ctx.save(); ctx.translate(sx,sy);
if(!running){ ctx.restore(); return; }
// loot — superposed, cycling rarity colours
for(var l=0;l<loots.length;l++){ var lo=loots[l];
var hue=(lo.phase*40)%360; var pulse=0.6+0.4*Math.sin(lo.phase*2);
var g=ctx.createRadialGradient(lo.x,lo.y,0,lo.x,lo.y,lo.r*3);
g.addColorStop(0,'hsla('+hue+',90%,70%,'+(0.9*pulse)+')'); g.addColorStop(0.5,'hsla('+hue+',90%,60%,0.3)'); g.addColorStop(1,'rgba(0,0,0,0)');
ctx.fillStyle=g; ctx.beginPath(); ctx.arc(lo.x,lo.y,lo.r*3,0,7); ctx.fill();
ctx.fillStyle='hsl('+hue+',90%,75%)'; ctx.beginPath(); ctx.arc(lo.x,lo.y,lo.r*0.6*pulse,0,7); ctx.fill();
}
// mobs
for(var i=0;i<mobs.length;i++){ var m=mobs[i]; var col=['#7a1512','#3a5aa8','#8a6a12'][m.tier];
ctx.fillStyle=m.hit>0?'#fff':col; ctx.beginPath(); ctx.arc(m.x,m.y,m.r,0,7); ctx.fill();
ctx.strokeStyle=['#c0261f','#5aa0ff','#f5d84a'][m.tier]; ctx.lineWidth=2; ctx.beginPath(); ctx.arc(m.x,m.y,m.r,0,7); ctx.stroke();
// eyes
ctx.fillStyle='#ffcf3f'; ctx.beginPath();ctx.arc(m.x-m.r*0.3,m.y-m.r*0.15,1.6,0,7);ctx.arc(m.x+m.r*0.3,m.y-m.r*0.15,1.6,0,7);ctx.fill();
if(m.hp<m.maxHp){ ctx.fillStyle='#000';ctx.fillRect(m.x-m.r,m.y-m.r-6,m.r*2,3); ctx.fillStyle='#c0261f';ctx.fillRect(m.x-m.r,m.y-m.r-6,m.r*2*(m.hp/m.maxHp),3); }
}
// bolts
for(var b=0;b<bolts.length;b++){ var o=bolts[b]; ctx.strokeStyle=o.crit?'#39e6ff':'#e6c05a'; ctx.lineWidth=o.crit?4:2.5;
ctx.beginPath(); ctx.moveTo(o.x,o.y); ctx.lineTo(o.x-o.vx*0.02,o.y-o.vy*0.02); ctx.stroke();
ctx.fillStyle=o.crit?'#bffaff':'#fff2c0'; ctx.beginPath();ctx.arc(o.x,o.y,o.crit?3.5:2.5,0,7);ctx.fill(); }
// particles
for(var q=0;q<parts.length;q++){ var pr=parts[q];
if(pr.ring){ ctx.strokeStyle='rgba(57,230,255,'+pr.life+')'; ctx.lineWidth=3; ctx.beginPath();ctx.arc(pr.x,pr.y,pr.r,0,7);ctx.stroke(); }
else { ctx.globalAlpha=Math.max(0,pr.life); ctx.fillStyle=pr.col; ctx.beginPath();ctx.arc(pr.x,pr.y,2.2,0,7);ctx.fill(); ctx.globalAlpha=1; } }
// player — the exile
var pg=ctx.createRadialGradient(player.x,player.y,0,player.x,player.y,player.r*2.4);
pg.addColorStop(0,'rgba(230,192,90,0.25)');pg.addColorStop(1,'rgba(0,0,0,0)'); ctx.fillStyle=pg;
ctx.beginPath();ctx.arc(player.x,player.y,player.r*2.4,0,7);ctx.fill();
ctx.fillStyle='#1a1410'; ctx.beginPath();ctx.arc(player.x,player.y,player.r,0,7);ctx.fill();
ctx.strokeStyle='#e6c05a';ctx.lineWidth=2.5;ctx.beginPath();ctx.arc(player.x,player.y,player.r,0,7);ctx.stroke();
// aim reticle direction
var adx=aim.x-player.x,ady=aim.y-player.y,ad=Math.hypot(adx,ady)||1;
ctx.strokeStyle='rgba(57,230,255,.5)';ctx.lineWidth=2;ctx.beginPath();ctx.moveTo(player.x+adx/ad*player.r,player.y+ady/ad*player.r);ctx.lineTo(player.x+adx/ad*(player.r+14),player.y+ady/ad*(player.r+14));ctx.stroke();
ctx.restore();
// vignette
var vg=ctx.createRadialGradient(W/2,H/2,H*0.4,W/2,H/2,H*0.85);
vg.addColorStop(0,'rgba(0,0,0,0)');vg.addColorStop(1,'rgba(0,0,0,0.55)'); ctx.fillStyle=vg;ctx.fillRect(0,0,W,H);
}
var last=performance.now();
function frame(now){ var dt=Math.min(0.05,(now-last)/1000); last=now; if(running)update(dt); draw(); requestAnimationFrame(frame); }
resize(); requestAnimationFrame(frame);
window.__qpoe={ start:start, state:function(){return{running:running,slain:slain,curr:curr,lvl:player?player.lvl:0,hp:player?player.hp:0,mobs:mobs?mobs.length:0};},
autoplay:function(){ if(!running)start(); aim.down=true; aim.x=mobs[0]?mobs[0].x:W/2; aim.y=mobs[0]?mobs[0].y:H/2; keys.d=true; } };
})();
</script>
</body>
</html>