Warden
Claude Sonnet 5.5 · full

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 39,518 bytes · SHA-256 e6bdf1b3958e
<!DOCTYPE html><html><head><meta charset="utf-8"><title>The Warden's Corrosion</title>
<style>html,body{margin:0;background:#07090f;height:100%;overflow:hidden;display:flex;align-items:center;justify-content:center}canvas{max-width:100vw;max-height:100vh;aspect-ratio:16/9;background:#000;cursor:crosshair}</style></head><body><canvas id="c" width="960" height="540"></canvas>
<script>
const cv=document.getElementById('c'),g=cv.getContext('2d'),W=960,H=540;
const dk=document.createElement('canvas');dk.width=W;dk.height=H;const dg=dk.getContext('2d');
// ---------- audio
let AC=null,drone=null,droneG=null;
function ac(){if(!AC){AC=new(window.AudioContext||window.webkitAudioContext)();droneG=AC.createGain();droneG.gain.value=0;drone=AC.createOscillator();drone.frequency.value=82;const d2=AC.createOscillator();d2.frequency.value=123;d2.connect(droneG);drone.connect(droneG);droneG.connect(AC.destination);drone.start();d2.start();}return AC}
function tone(f,d,ty='sine',v=.1,sl=1,dl=0){if(!AC)return;const t=AC.currentTime+dl,o=AC.createOscillator(),n=AC.createGain();o.type=ty;o.frequency.setValueAtTime(f,t);o.frequency.exponentialRampToValueAtTime(Math.max(20,f*sl),t+d);n.gain.setValueAtTime(v,t);n.gain.exponentialRampToValueAtTime(.001,t+d);o.connect(n);n.connect(AC.destination);o.start(t);o.stop(t+d+.02)}
function noise(d,v,fq,ty='lowpass',dl=0,fq2){if(!AC)return;const t=AC.currentTime+dl,b=AC.createBuffer(1,AC.sampleRate*d|0,AC.sampleRate),a=b.getChannelData(0);for(let i=0;i<a.length;i++)a[i]=Math.random()*2-1;const s=AC.createBufferSource();s.buffer=b;const f=AC.createBiquadFilter();f.type=ty;f.frequency.setValueAtTime(fq,t);if(fq2)f.frequency.exponentialRampToValueAtTime(fq2,t+d);const n=AC.createGain();n.gain.setValueAtTime(v,t);n.gain.exponentialRampToValueAtTime(.001,t+d);s.connect(f);f.connect(n);n.connect(AC.destination);s.start(t)}
const sfx={
step(z){if(z==1){tone(110,.08,'square',.04,.6);noise(.08,.08,1800,'bandpass')}else if(z==3){noise(.14,.08,1400,'bandpass',0,500);tone(300,.1,'sine',.02,.5)}else{noise(.1,.09,380);tone(70,.08,'sine',.04)}},
sw0(){noise(.16,.14,800,'bandpass',0,3000)},sw1(){noise(.2,.14,700,'bandpass',0,3500);for(let i=0;i<5;i++)noise(.03,.06,4000,'highpass',i*.03)},
sw2(){tone(60,.25,'sawtooth',.08,2);tone(420,.7,'triangle',.14,.9,.22);tone(42,.9,'sine',.35,.8,.22);noise(.3,.14,300,'lowpass',.2)},
hit(w){tone(w>1?55:w?75:95,.15,'sine',.3,.5);noise(.05,.15,1500)},
shoot(){tone(1500,.18,'triangle',.13,.35);tone(2200,.1,'sine',.05,.5)},chime(){tone(1800,.5,'sine',.1);tone(2400,.6,'sine',.06,1,.05)},
hurt(){tone(70,.2,'sine',.35,.5);tone(1480,.35,'square',.04,1);tone(1570,.35,'square',.04,1)},
pick(){[523,659,784,1046].forEach((f,i)=>tone(f,.25,'sine',.09,1,i*.07))},rare(){[392,494,587,784,988].forEach(f=>tone(f,1.2,'sine',.07,1))},
coin(){tone(2200,.09,'square',.05);tone(3100,.14,'square',.04,1,.05)},buy(){tone(2400,.1,'square',.05);tone(3000,.1,'square',.05,1,.07);[784,988,1318].forEach((f,i)=>tone(f,.4,'sine',.08,1,.15+i*.06))},
shim(){for(let i=0;i<6;i++)tone(1500+i*350,.5,'sine',.04,1.2,i*.05)},brk(){noise(.4,.3,900,'lowpass',0,150);tone(60,.3,'sine',.3,.5)},clink(){tone(2600,.15,'triangle',.08);tone(3900,.1,'triangle',.05)},boom(){noise(1,.4,300,'lowpass',0,60);tone(50,1,'sine',.4,.5)}};
// ---------- state
const NAMES=['The Ashen Threshold','The Rusted Archives','The Collapsed Gallery','The Sunken Vault','The Twin Spire','The Core Chamber'];
const SUBS=['Prologue','Exploration begins','Kindle the old fire','Where the water remembers','The last ascent','The Core Beacon'];
function newS(){return{z:0,x:150,y:270,cont:3,hp:6,gold:0,weapon:0,shard:false,ammo:0,stem:0,seal:0,band:false,lantern:false,cloak:false,eq:{band:false,lantern:false,cloak:false},flags:{},bought:{stem:0,seal:0,heart:0},stone:-1}}
let S=newS(),mode='title',objs=[],en=[],pk=[],pr=[],pt=[],ft=[],T=0,msgs=[],banner=null,shake=0,flash=0,aura=0,shimmer=0,dead=false;
const P={x:150,y:270,inv:0,atk:0,cd:0,hitDone:true,sw:0,step:0,fa:0,vx:0,vy:0,moving:false,frozenSeal:0};
let keys={},mouse={x:600,y:270,down:false},menuSel=0;
const STONES=[{z:0,x:90,y:270},{z:1,x:480,y:300},{z:4,x:90,y:270}];
const F=()=>S.flags;
// ---------- zones
const PIL=[[[300,150,40,40],[300,350,40,40],[560,250,40,40]],[[200,200,50,50],[700,200,50,50],[200,300,50,50],[700,300,50,50]],[[450,250,50,50],[200,150,40,40]],[[260,150,40,40],[260,380,40,40],[700,150,40,40],[700,380,40,40]],[[300,180,40,40],[300,320,40,40],[620,250,40,40]],[[250,200,50,50],[660,200,50,50],[250,300,50,50],[660,300,50,50]]];
const ENS=[[['w',600,150],['w',700,380],['w',820,250]],[['w',600,120],['w',700,420],['w',820,270],['w',400,430]],[['w',600,120],['w',700,420],['w',450,140],['mini',700,300]],[['w',600,120],['w',500,420],['s',800,400],['s',800,140],['w',400,200]],[['w',600,120],['s',600,420],['w',450,400],['s',800,300],['w',500,140],['s',820,440],['w',700,200],['guard',700,270]],[['boss',480,190]]];
function build(z){objs=[];en=[];pk=[];pr=[];const f=F();
PIL[z].forEach(r=>objs.push({k:'pil',x:r[0],y:r[1],w:r[2],h:r[3]}));
const O=o=>objs.push(o);
if(z==0)O({k:'stone',id:0,x:90,y:270});
if(z==1){O({k:'stone',id:1,x:480,y:300});O({k:'merch',x:480,y:130});
O({k:'jar',id:'j1',x:150,y:450,drop:'gold'});O({k:'jar',id:'jstem',x:820,y:450,drop:'stem'});O({k:'jar',id:'j3',x:150,y:90,drop:'gold'});
O({k:'crack',id:'c_band',x:840,y:40,w:80,h:90,hp:7});if(!f.o_band)O({k:'chest',id:'o_band',x:880,y:85,item:'band',req:'c_band'});}
if(z==2){O({k:'plate',x:300,y:400,w:80,h:50});O({k:'pdoor',id:'d3',x:700,y:40,w:24,h:170});O({k:'pil',x:700,y:210,w:220,h:24});
O({k:'altar',id:'alt1',x:830,y:110});O({k:'crack',id:'c_heart',x:30,y:420,w:90,h:80,hp:7});if(!f.h4)O({k:'heart',id:'h4',x:75,y:460,req:'c_heart'});
O({k:'vent',x:500,y:150,ph:0});O({k:'vent',x:500,y:400,ph:1.5});}
if(z==3){O({k:'crack',id:'c_shard',x:400,y:40,w:120,h:70,hp:7});if(!S.shard)O({k:'item',item:'shard',x:460,y:75,req:'c_shard'});
O({k:'crystal',id:'cr4',x:890,y:90,lit:!!f.crystal4});if(f.crystal4&&!f.o_lantern)O({k:'chest',id:'o_lantern',x:480,y:440,item:'lantern'});
O({k:'vent',x:300,y:270,ph:0});O({k:'vent',x:650,y:270,ph:1.5});O({k:'jar',id:'k1',x:200,y:120,drop:'gold',hid:1});O({k:'jar',id:'k2',x:760,y:450,drop:'gold',hid:1});}
if(z==4){O({k:'stone',id:2,x:90,y:270});O({k:'altar',id:'alt2',x:480,y:90});
O({k:'crack',id:'c_bonus',x:820,y:420,w:100,h:80,hp:7});if(!f.o_bonus)O({k:'chest',id:'o_bonus',x:870,y:460,item:'bonus',req:'c_bonus'});
O({k:'crystal',id:'cr5',x:890,y:200,lit:!!f.c5});O({k:'pdoor',id:'d5',x:20,y:150,w:140,h:20});O({k:'pdoor',id:'d5',x:160,y:40,w:20,h:130});if(!f.h5&&S.cont<5)O({k:'heart',id:'h5',x:80,y:90});
O({k:'jar',id:'jseal',x:200,y:450,drop:'seal'});O({k:'jar',id:'j5',x:850,y:340,drop:'gold'});O({k:'jar',id:'k3',x:500,y:480,drop:'gold',hid:1});
O({k:'vent',x:400,y:200,ph:0});O({k:'vent',x:400,y:350,ph:1});O({k:'vent',x:600,y:270,ph:2});
if(f.g5&&!f.o_cloak&&!S.cloak)O({k:'item',id:'o_cloak',item:'cloak',x:700,y:270});}
if(z==5){O({k:'crystal',id:'a',x:100,y:90,lit:false});O({k:'crystal',id:'b',x:860,y:90,lit:false});if(f.win)O({k:'beacon',x:480,y:270});}
ENS[z].forEach(e=>{const t=e[0];if(t=='mini'&&f.mini3)return;if(t=='guard'&&f.g5)return;if(t=='boss'&&f.win)return;if(f['cl'+z]&&(t=='w')&&en.length>=2)return;if(f['cl'+z]&&t=='s')return;
const d={w:[3,12,60],s:[2,12,45],mini:[14,26,75],guard:[26,30,65],boss:[48,40,60]}[t];
en.push({t,x:e[1],y:e[2],hp:d[0],mhp:d[0],r:d[1],spd:d[2],cd:1+Math.random()*2,fz:0,fl:0,kx:0,ky:0,ph:Math.random()*6,st:0,tm:2,hitId:-1})});
}
function gateOpen(z){return z==0?F().cl0:z==1?F().cl1:z==2?S.weapon>=1:z==3?F().crystal4:z==4?S.weapon>=2:false}
function loadZone(z,fromRight){S.z=z;build(z);P.x=fromRight?W-45:45;if(fromRight===null){P.x=S.x;P.y=S.y}else P.y=Math.max(220,Math.min(320,P.y));
banner={t:3.2,a:NAMES[z],b:SUBS[z]};if(z==3&&!F().sunk){F().sunk=1;msg('Cracks: shatter what looks broken. Something glints in the far crystal.')}
const hint=['Move WASD · aim mouse · click/Space to strike. Clear the hall to open the door east.','Merchant altar and Sanctuary Stone unlock once the hall is clear. Hairline cracks hide things.','Step on the bronze plate—the door stays open a short while.','Right-click / K fires the Aether Shard. Strike the far crystal.','Rest at the stone before the ascent. Spire Guardian holds the Cloak.','Shoot both crystals to break the Colossus\'s shield.'][z];if(!F()['seen'+z]){F()['seen'+z]=1;msg(hint)}}
function msg(t){msgs.push({t,l:5})}
// ---------- helpers
function solids(){return objs.filter(o=>(o.k=='pil')||(o.k=='crack'&&!F()['b_'+o.id])||(o.k=='pdoor'&&!doorOpen(o)))}
function doorOpen(o){return o.id=='d3'?T<(F().d3t||0):o.id=='d5'?!!F().c5:false}
function rectHit(x,y,r,o){const cx=Math.max(o.x,Math.min(x,o.x+o.w)),cy=Math.max(o.y,Math.min(y,o.y+o.h));return(x-cx)**2+(y-cy)**2<r*r}
function mv(e,dx,dy,r){const s=solids();e.x+=dx;if(s.some(o=>rectHit(e.x,e.y,r,o)))e.x-=dx;e.y+=dy;if(s.some(o=>rectHit(e.x,e.y,r,o)))e.y-=dy}
function parts(x,y,n,col,sp=80,life=.6,up=0){for(let i=0;i<n;i++){const a=Math.random()*6.28,s=Math.random()*sp;pt.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s-up,l:life*(.5+Math.random()*.7),m:life,c:col,s:2+Math.random()*2})}}
function ftxt(x,y,t,c='#ff6b5a'){ft.push({x,y,t,c,l:1})}
function heal(n){S.hp=Math.min(S.cont*2,S.hp+n);ftxt(P.x,P.y-30,'+'+n+' HP','#7dff8a');parts(P.x,P.y,14,'#7dff8a',50,1,40)}
function hurt(d,trap){if(P.inv>0||mode!='play')return;if(trap&&S.eq.cloak)d=Math.max(1,d-1);S.hp-=d;P.inv=.9;flash=.35;shake=8;sfx.hurt();parts(P.x,P.y,22,'#ff2a2a',150,.6);ftxt(P.x,P.y-24,'-'+d);if(S.hp<=0){S.hp=0;mode='dead'}}
function give(it){sfx.rare();parts(P.x,P.y,30,'#fff',60,1.5,70);shimmer=2;
if(it=='band'){S.band=true;msg("Warden's Band — silver ring on your wrist. Equip in inventory (I): quicker strikes.")}
if(it=='lantern'){S.lantern=true;msg('Phantom Lantern — equip to reveal hidden caches in the dark.')}
if(it=='cloak'){S.cloak=true;msg('Shadow Cloak — equip to halve trap damage.')}
if(it=='shard'){S.shard=true;S.ammo+=12;msg('AETHER SHARD — right-click / K to fire. 12 charges. Strike distant crystals.')}
if(it=='bonus'){S.gold+=100;msg('Bonus chest: +100 gold');sfx.coin()}}
function addCont(){if(S.cont>=5){S.gold+=100;msg('+100 gold');return}S.cont++;S.hp=Math.min(S.cont*2,S.hp+2);aura=2.5;shimmer=2;sfx.rare();sfx.shim();msg('Heart container gained! '+S.cont+'/5');parts(P.x,P.y,40,'#ffd24a',120,1.4,60)}
function useItem(k){if(k=='stem'&&S.stem>0&&S.hp<S.cont*2){S.stem--;heal(4);sfx.pick()}
if(k=='seal'&&S.seal>0){let b=null,bd=1e9;en.forEach(e=>{const d=Math.hypot(e.x-P.x,e.y-P.y);if(d<bd){bd=d;b=e}});if(b&&bd<500){S.seal--;b.fz=3;sfx.shim();parts(b.x,b.y,20,'#6ab8ff',90,1)}else msg('No enemy nearby.')}}
function save(){try{localStorage.setItem('wc_save',JSON.stringify(S))}catch(e){}}
function hasSave(){try{return!!localStorage.getItem('wc_save')}catch(e){return false}}
function loadSave(){let s=null;try{s=JSON.parse(localStorage.getItem('wc_save'))}catch(e){}S=s||newS();if(s&&S.stone>=0){const st=STONES[S.stone];S.z=st.z;S.x=st.x+50;S.y=st.y}else{S=newS()}S.hp=S.cont*2;dead=false;mode='play';loadZone(S.z,null)}
function killEnemy(e){sfx.hit(S.weapon);parts(e.x,e.y,26,'#ff9a3a',140,.8);const n=e.t=='boss'?0:5+Math.random()*10|0;
if(e.t=='mini'){F().mini3=1;for(let i=0;i<4;i++)pk.push({k:'gold',v:10,x:e.x,y:e.y,vx:Math.random()*100-50,vy:Math.random()*100-50})}
if(e.t=='guard'){F().g5=1;msg('The Spire Guardian falls. It drops the Shadow Cloak; the altar stirs.');sfx.boom();objs.push({k:'item',id:'o_cloak',item:'cloak',x:e.x,y:e.y});for(let i=0;i<5;i++)pk.push({k:'gold',v:12,x:e.x,y:e.y,vx:Math.random()*160-80,vy:Math.random()*160-80})}
if(e.t=='boss'){F().win=1;sfx.boom();objs.push({k:'beacon',x:480,y:270});msg('The Colossus crumbles. Reach the Core Beacon (E).');shake=25}
if(n)pk.push({k:'gold',v:n,x:e.x,y:e.y,vx:0,vy:0});if(S.shard&&Math.random()<.25)pk.push({k:'ammo',v:1,x:e.x+8,y:e.y+8,vx:0,vy:0});
en=en.filter(x=>x!=e);if(!en.length&&!F()['cl'+S.z]){F()['cl'+S.z]=1;sfx.shim();shimmer=1.5;msg(S.z==1?'Hall cleared. The Sanctuary Stone wakes and the Merchant Altar hums.':S.z==0?'Hall cleared. The eastern door opens.':'Hall cleared.')}}
const WD=[1,1.5,3],WR=[52,56,80],WA=[1.3,1.4,2.6],WC=[.45,.28,.65],WW=[.06,.04,.22];
function attack(){if(P.cd>0||mode!='play')return;P.cd=WC[S.weapon]*(S.eq.band?.85:1);P.atk=WW[S.weapon]+.2;P.hitDone=false;P.hid=(P.hid||0)+1;[sfx.sw0,sfx.sw1,sfx.sw2][S.weapon]()}
function shoot(){if(!S.shard||mode!='play'||P.sc>0)return;if(S.ammo<=0){msg('Out of charges — restock at the Merchant (10g).');P.sc=.5;return}S.ammo--;P.sc=.35;sfx.shoot();const a=P.fa;pr.push({x:P.x+Math.cos(a)*16,y:P.y+Math.sin(a)*16,vx:Math.cos(a)*520,vy:Math.sin(a)*520,l:1.2})}
function doHit(){const a=P.fa,R=WR[S.weapon],A=WA[S.weapon]/2,dm=WD[S.weapon];
const fire=S.weapon==1?'#ff7a1a':S.weapon==2?'#ffe26a':'#ffb060';
en.forEach(e=>{const d=Math.hypot(e.x-P.x,e.y-P.y);if(d>R+e.r)return;let da=Math.abs(((Math.atan2(e.y-P.y,e.x-P.x)-a)+9.42)%6.283-3.14159);if(da>A&&d>e.r+12)return;
if(e.t=='boss'&&!(F().vuln>T)){sfx.clink();parts(e.x,e.y,10,'#6ab8ff',120,.4);ftxt(e.x,e.y-e.r,'shielded','#6ab8ff');return}
e.hp-=dm;e.fl=.15;e.kx=Math.cos(a)*(S.weapon==2?320:200)*(e.r>25?.3:1);e.ky=Math.sin(a)*(S.weapon==2?320:200)*(e.r>25?.3:1);sfx.hit(S.weapon);ftxt(e.x,e.y-e.r,''+dm,'#ffe0a0');
parts(e.x,e.y,S.weapon==2?18:10,fire,130,.5);if(S.weapon==1){parts(e.x,e.y,6,'#555',40,1,30);e.burn=.5}if(S.weapon==2){shimmer=Math.max(shimmer,.5);shake=6}
if(e.hp<=0)killEnemy(e)});
objs.forEach(o=>{if(o.k=='crack'&&!F()['b_'+o.id]){if(rectHit(P.x+Math.cos(a)*R*.7,P.y+Math.sin(a)*R*.7,20,o)){o.hp-=dm;o.sh=.2;sfx.hit(S.weapon);parts(P.x+Math.cos(a)*R,P.y+Math.sin(a)*R,8,'#a08060',90,.5);if(o.hp<=0){F()['b_'+o.id]=1;sfx.brk();shake=10;parts(o.x+o.w/2,o.y+o.h/2,40,'#8a6a44',150,1);msg('The wall crumbles.')}}}
if(o.k=='jar'&&!o.gone&&(!o.hid||S.eq.lantern)&&Math.hypot(o.x-(P.x+Math.cos(a)*30),o.y-(P.y+Math.sin(a)*30))<40&&!F()['j_'+o.id]){o.gone=1;F()['j_'+o.id]=1;sfx.brk();parts(o.x,o.y,16,'#a07040',100,.7);
if(o.drop=='gold'){for(let i=0;i<3;i++)pk.push({k:'gold',v:8,x:o.x,y:o.y,vx:Math.random()*120-60,vy:Math.random()*120-60})}else{objs.push({k:'item2',item:o.drop,x:o.x,y:o.y})}}})}
// ---------- update
function update(dt){T+=dt;const f=F();
if(mode!='play'){return}
P.cd-=dt;P.sc=(P.sc||0)-dt;P.inv-=dt;shake*=.9;flash-=dt;aura-=dt;shimmer-=dt;
mouse.wx=mouse.x;P.fa=Math.atan2(mouse.y-P.y,mouse.x-P.x);
if(P.atk>0){P.atk-=dt;if(!P.hitDone&&P.atk<.2){P.hitDone=true;doHit()}}
if(mouse.down||keys[' ']||keys['j'])attack();
let dx=(keys['d']||keys['arrowright']?1:0)-(keys['a']||keys['arrowleft']?1:0),dy=(keys['s']||keys['arrowdown']?1:0)-(keys['w']||keys['arrowup']?1:0);
const sp=(P.atk>.1?.5:1)*175;P.moving=!!(dx||dy);
if(dx||dy){const l=Math.hypot(dx,dy);dx=dx/l*sp*dt;dy=dy/l*sp*dt;mv(P,dx,dy,12);P.step+=Math.hypot(dx,dy);if(P.step>36){P.step=0;sfx.step(S.z)}
if(S.weapon==1&&Math.random()<.5)pt.push({x:P.x-dx*3,y:P.y+8,vx:0,vy:-20,l:.5,m:.5,c:'#ff8a2a',s:2.5})}
const gap=P.y>200&&P.y<340;
if(P.y<24)P.y=24;if(P.y>H-24)P.y=H-24;
if(P.x>W-24){if(gap&&gateOpen(S.z)&&S.z<5){loadZone(S.z+1,false);return}else P.x=W-24}
if(P.x<24){if(gap&&S.z>0){loadZone(S.z-1,true);return}else P.x=24}
// plate
objs.forEach(o=>{if(o.k=='plate'){const on=rectHit(P.x,P.y,8,o)||en.some(e=>rectHit(e.x,e.y,4,o));if(on&&!o.on){sfx.clink();f.d3t=T+12;msg('The plate sinks. The bronze door grinds open...')}o.on=on;if(on)f.d3t=Math.max(f.d3t||0,T+10)}
if(o.k=='vent'){const c=(T+o.ph)%3;o.fire=c>2.3;o.warn=c>1.6&&c<=2.3;if(o.fire&&Math.hypot(P.x-o.x,P.y-o.y)<28)hurt(2,true);if(o.fire&&!o.snd){o.snd=1;noise(.3,.1,600,'lowpass',0,200)}if(!o.fire)o.snd=0}
if(o.sh>0)o.sh-=dt;
if((o.k=='heart'||o.k=='item'||o.k=='item2')&&(!o.req||f['b_'+o.req])&&Math.hypot(P.x-o.x,P.y-o.y)<24){o.k='x';
if(o.item=='stem'){S.stem++;sfx.pick();msg('Vital Stem found.')}else if(o.item=='seal'){S.seal++;sfx.pick();msg('Binding Seal found.')}else if(o.k=='x'&&o.id!==undefined&&o.id=='o_cloak'){f.o_cloak=1;give('cloak')}else if(o.item)give(o.item);else{f[o.id]=1;addCont()}}});
objs=objs.filter(o=>o.k!='x'&&!o.gone);
// pickups
pk.forEach(p=>{const d=Math.hypot(P.x-p.x,P.y-p.y);p.x+=p.vx*dt;p.y+=p.vy*dt;p.vx*=.93;p.vy*=.93;if(d<90){p.x+=(P.x-p.x)*dt*6;p.y+=(P.y-p.y)*dt*6}
if(d<16){p.d=1;if(p.k=='gold'){S.gold+=p.v;sfx.coin();parts(P.x,P.y,6,'#ffd24a',50,.5,20)}else{S.ammo+=1;sfx.chime()}}});pk=pk.filter(p=>!p.d);
// projectiles
const sol=solids();
pr.forEach(p=>{p.x+=p.vx*dt;p.y+=p.vy*dt;p.l-=dt;pt.push({x:p.x,y:p.y,vx:0,vy:0,l:.25,m:.25,c:p.e?'#ff5a3a':'#7ae8ff',s:3});
if(p.x<-10||p.x>W+10||p.y<0||p.y>H)p.l=0;
if(p.e){if(Math.hypot(p.x-P.x,p.y-P.y)<14){hurt(1);p.l=0}}
else{objs.forEach(o=>{if(o.k=='crystal'&&Math.hypot(p.x-o.x,p.y-o.y)<22&&p.l>0){p.l=0;hitCrystal(o)}});
en.forEach(e=>{if(p.l>0&&Math.hypot(p.x-e.x,p.y-e.y)<e.r+5){p.l=0;sfx.chime();if(e.t=='boss'&&!(f.vuln>T)){parts(p.x,p.y,8,'#6ab8ff',90,.4);return}e.hp-=2;e.fl=.15;ftxt(e.x,e.y-e.r,'2','#7ae8ff');parts(e.x,e.y,10,'#7ae8ff',120,.5);if(e.hp<=0)killEnemy(e)}})}
if(p.l>0&&sol.some(o=>rectHit(p.x,p.y,3,o))){p.l=0;parts(p.x,p.y,5,'#aaa',60,.3);if(!p.e)sfx.clink();
if(!p.e)sol.forEach(o=>{if(o.k=='crack'&&rectHit(p.x,p.y,4,o)){o.hp-=.5}})}});
pr=pr.filter(p=>p.l>0);
// enemies
en.forEach(e=>{e.fl-=dt;if(e.burn>0){e.burn-=dt;if(Math.random()<.3)parts(e.x,e.y,1,'#ff7a1a',30,.5,30)}
e.x+=e.kx*dt;e.y+=e.ky*dt;e.kx*=.85;e.ky*=.85;e.x=Math.max(20,Math.min(W-20,e.x));e.y=Math.max(20,Math.min(H-20,e.y));
if(e.fz>0){e.fz-=dt;if(Math.random()<.2)parts(e.x,e.y,1,'#8ad0ff',30,.6);return}
const d=Math.hypot(P.x-e.x,P.y-e.y)||1,ux=(P.x-e.x)/d,uy=(P.y-e.y)/d;e.cd-=dt;
const fire=(a,s=190)=>pr.push({e:1,x:e.x,y:e.y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,l:3});
if(e.t=='w'||e.t=='mini'){if(d<450)mv(e,ux*e.spd*dt,uy*e.spd*dt,e.r*.8)}
else if(e.t=='s'){const k=d<200?-1:d>320?1:0;mv(e,ux*k*e.spd*dt+-uy*20*dt,uy*k*e.spd*dt+ux*20*dt,10);if(e.cd<=0&&d<520){e.cd=2.2;fire(Math.atan2(uy,ux))}}
else if(e.t=='guard'){mv(e,ux*e.spd*dt,uy*e.spd*dt,24);if(e.cd<=0){e.cd=3;for(let i=0;i<8;i++)fire(i*.785+T)}}
else if(e.t=='boss'){e.tm-=dt;if(e.st==1){e.x+=e.cx*dt*330;e.y+=e.cy*dt*330;if(e.tm<=0){e.st=0;e.tm=2.5}}else{mv(e,ux*e.spd*.6*dt,uy*e.spd*.6*dt,34);if(e.tm<=0){e.st=1;e.tm=.7;e.cx=ux;e.cy=uy;noise(.5,.2,200)}}
if(e.cd<=0){e.cd=3;for(let i=0;i<10;i++)fire(i*.628+T,170);if(en.length<3){en.push({t:'w',x:e.x,y:e.y,hp:3,mhp:3,r:12,spd:70,cd:1,fz:0,fl:0,kx:0,ky:0,ph:0,st:0,tm:0});}}}
if(d<e.r+12&&e.cd!==undefined){const dm=e.t=='w'?1:2;hurt(dm);if(P.inv>.8){P.x+=ux*30;P.y+=uy*30}}
});
if(f.vuln>T&&!f.vs){f.vs=1}
// pt
pt.forEach(p=>{p.x+=p.vx*dt;p.y+=p.vy*dt;p.l-=dt});pt=pt.filter(p=>p.l>0);ft.forEach(t=>{t.y-=25*dt;t.l-=dt});ft=ft.filter(t=>t.l>0);
if(Math.random()<.3){if(S.z==1)pt.push({x:Math.random()*W,y:Math.random()*H,vx:6,vy:-4,l:4,m:4,c:'#c8a870',s:1.5});if(S.z==3)pt.push({x:Math.random()*W,y:H,vx:0,vy:-30,l:5,m:5,c:'#8ab8d0',s:3});}
objs.forEach(o=>{if(o.k=='stone'&&Math.random()<.15)pt.push({x:o.x+Math.random()*20-10,y:o.y+10,vx:0,vy:-25,l:1.2,m:1.2,c:'#ffd24a',s:2})});
msgs.forEach(m=>m.l-=dt);msgs=msgs.filter(m=>m.l>0);if(banner){banner.t-=dt;if(banner.t<=0)banner=null}
// merchant drone
const mr=objs.find(o=>o.k=='merch');if(droneG){const d=mr&&f.cl1?Math.hypot(P.x-mr.x,P.y-mr.y):999;droneG.gain.value=d<200?.05*(1-d/200):0}
}
function hitCrystal(o){const f=F();sfx.chime();sfx.shim();parts(o.x,o.y,30,'#7ae8ff',140,1);
if(o.id=='cr4'&&!f.crystal4){f.crystal4=1;o.lit=true;msg('The crystal blazes. A beam of light reveals a chest, and the east door unseals.');objs.push({k:'chest',id:'o_lantern',x:480,y:440,item:'lantern'})}
if(o.id=='cr5'&&!f.c5){f.c5=1;o.lit=true;msg('Bronze grating lifts far across the Spire...')}
if(o.id=='a'||o.id=='b'){o.lit=true;const a=objs.find(x=>x.id=='a'),b=objs.find(x=>x.id=='b');if(a.lit&&b.lit){f.vuln=T+10;a.lit=b.lit=false;msg('The Colossus\'s shield flickers out! Strike now!');shake=10;sfx.boom()}}}
function interact(){const f=F();let best=null,bd=55;objs.forEach(o=>{if(['stone','merch','chest','altar','beacon'].includes(o.k)){const d=Math.hypot(P.x-o.x,P.y-o.y);if(d<bd&&(!o.req||f['b_'+o.req])){bd=d;best=o}}});if(!best)return;const o=best;
if(o.k=='stone'){if(o.id==1&&!f.cl1){msg('The stone is dormant. Clear the hall.');return}S.hp=S.cont*2;S.stone=o.id;S.z=STONES[o.id].z;S.x=o.x+50;S.y=o.y;save();sfx.shim();sfx.pick();parts(o.x,o.y,30,'#ffd24a',90,1.4,40);msg('Sanctuary Stone: health restored, progress saved.')}
if(o.k=='merch'){if(!f.cl1){msg('The altar is silent. Clear the hall.');return}mode='shop';menuSel=0;sfx.chime()}
if(o.k=='chest'&&!f[o.id]){f[o.id]=1;o.k='x';give(o.item);objs=objs.filter(x=>x.k!='x')}
if(o.k=='altar'){const id=o.id;if(id=='alt1'&&S.weapon<1){S.weapon=1;shimmer=2.5;sfx.rare();sfx.shim();parts(o.x,o.y,50,'#ff8a2a',160,1.6,50);msg('IGNITED EDGE: the blade burns. Faster strikes, fire damage.')}
else if(id=='alt2'&&S.weapon<2){if(!f.g5){msg('The altar is sealed while the Guardian lives.');return}S.weapon=2;shimmer=2.5;sfx.rare();sfx.boom();parts(o.x,o.y,60,'#ffe26a',180,1.8,50);msg('SOLAR FALCHION: wider sweep, heavier impact. The east door opens.')}}
if(o.k=='beacon'){mode='win';sfx.rare();sfx.shim()}}
function buy(i){const f=S.bought;const c=[[50,'stem',3],[75,'seal',2],[300,'heart',1]];
if(i<3){const [p,k,m]=c[i];if(f[k]>=m){msg('Sold out.');return}if(k=='heart'&&S.cont>=5){msg('Your vessel can hold no more.');return}if(S.gold<p){msg('Not enough gold.');sfx.clink();return}S.gold-=p;f[k]++;sfx.coin();sfx.buy();if(k=='stem')S.stem++;if(k=='seal')S.seal++;if(k=='heart')addCont()}
else if(i==3){if(!F().crystal4||!S.shard){msg('Not yet available.');return}if(S.gold<10){msg('Not enough gold.');return}S.gold-=10;S.ammo++;sfx.coin();sfx.buy()}}
// ---------- draw
function heartPath(x,y,s){g.beginPath();g.moveTo(x,y+s*.7);g.bezierCurveTo(x-s*1.2,y-s*.1,x-s*.7,y-s*.95,x,y-s*.3);g.bezierCurveTo(x+s*.7,y-s*.95,x+s*1.2,y-s*.1,x,y+s*.7);g.closePath()}
function txt(t,x,y,sz=16,c='#f0d9a8',al='left'){g.font=sz+'px Georgia,serif';g.fillStyle=c;g.textAlign=al;g.fillText(t,x,y)}
const BG=[['#1a2436','#33303e'],['#1c2a34','#3a3226'],['#1a2030','#2c2a3a'],['#10222e','#1a3038'],['#18203a','#40303a'],['#161a2e','#4a2c22']];
function drawBG(){const b=BG[S.z],gr=g.createLinearGradient(0,0,0,H);gr.addColorStop(0,b[0]);gr.addColorStop(1,b[1]);g.fillStyle=gr;g.fillRect(0,0,W,H);
g.strokeStyle='rgba(200,150,70,.07)';g.lineWidth=1;for(let x=0;x<W;x+=60){g.beginPath();g.moveTo(x,0);g.lineTo(x,H);g.stroke()}for(let y=0;y<H;y+=60){g.beginPath();g.moveTo(0,y);g.lineTo(W,y);g.stroke()}
for(let i=0;i<3;i++){const cx=[150,500,830][i],cy=[420,110,400][i],r=90+i*20;g.save();g.translate(cx,cy);g.rotate(T*.05*(i%2?1:-1));g.strokeStyle='rgba(180,120,50,.13)';g.lineWidth=10;g.beginPath();g.arc(0,0,r,0,6.28);g.stroke();g.lineWidth=6;for(let k=0;k<8;k++){g.rotate(.785);g.strokeRect(r-4,-8,22,16);g.beginPath();g.moveTo(0,0);g.lineTo(r,0);g.stroke()}g.restore()}
g.strokeStyle='#5a4426';g.lineWidth=14;g.strokeRect(0,0,W,H);
g.clearRect(0,0,0,0);const f=F();g.fillStyle=BG[S.z][0];g.fillRect(W-16,205,16,130);g.fillRect(0,205,16,130);
if(S.z>0){g.fillStyle='#0a0f18';g.fillRect(0,215,14,110)}
if(S.z<5){if(gateOpen(S.z)){g.fillStyle='#2a1c0e';g.fillRect(W-14,215,14,110);g.fillStyle='#ffd24a';g.font='14px serif';g.textAlign='right';g.fillText('→',W-18,275)}else{g.fillStyle='#5a3a1a';g.fillRect(W-16,205,16,130);g.strokeStyle='#a5702a';g.lineWidth=3;for(let y=210;y<335;y+=20){g.beginPath();g.moveTo(W-16,y);g.lineTo(W,y+8);g.stroke()}}}}
function drawObjs(){const f=F();objs.forEach(o=>{
if(o.k=='pil'){g.fillStyle='#3a2f24';g.fillRect(o.x,o.y,o.w,o.h);g.fillStyle='#8a6a3a';g.fillRect(o.x,o.y,o.w,4);g.strokeStyle='#5a4426';g.strokeRect(o.x+.5,o.y+.5,o.w,o.h)}
if(o.k=='crack'&&!f['b_'+o.id]){const s=o.sh>0?3*Math.random():0;g.fillStyle='#2a2420';g.fillRect(o.x+s,o.y,o.w,o.h);g.strokeStyle='#9a805a';g.lineWidth=1.5;g.strokeRect(o.x+s+.5,o.y+.5,o.w,o.h);g.strokeStyle='#0a0806';g.lineWidth=2;g.beginPath();g.moveTo(o.x+o.w*.5,o.y);g.lineTo(o.x+o.w*.4,o.y+o.h*.3);g.lineTo(o.x+o.w*.6,o.y+o.h*.55);g.lineTo(o.x+o.w*.45,o.y+o.h);g.moveTo(o.x+o.w*.4,o.y+o.h*.3);g.lineTo(o.x+o.w*.15,o.y+o.h*.45);g.stroke();g.fillStyle='rgba(0,0,0,.25)';g.fillRect(o.x+s,o.y,o.w,o.h*(1-o.hp/7)+0)}
if(o.k=='pdoor'&&!doorOpen(o)){g.fillStyle='#6a4a22';g.fillRect(o.x,o.y,o.w,o.h);g.strokeStyle='#c99a4a';g.lineWidth=2;g.strokeRect(o.x,o.y,o.w,o.h)}
if(o.k=='plate'){g.fillStyle=o.on?'#6a4a20':'#a67a34';g.fillRect(o.x,o.y+(o.on?4:0),o.w,o.h-(o.on?4:0));g.strokeStyle='#e0b060';g.strokeRect(o.x,o.y,o.w,o.h);g.beginPath();g.arc(o.x+o.w/2,o.y+o.h/2,14,0,6.28);g.stroke()}
if(o.k=='vent'){g.fillStyle='#222';g.beginPath();g.arc(o.x,o.y,18,0,6.28);g.fill();if(o.warn||o.fire){g.fillStyle=o.fire?'rgba(255,120,30,.8)':'rgba(255,120,30,.3)';g.beginPath();g.arc(o.x,o.y,o.fire?28:16,0,6.28);g.fill()}}
if(o.k=='stone'){const act=S.stone==o.id;g.fillStyle='#4a5468';g.fillRect(o.x-14,o.y-8,28,24);g.fillStyle='#6a7488';g.fillRect(o.x-10,o.y-22,20,18);g.fillStyle=act||(o.id==1&&f.cl1)||o.id!=1?'#ffd24a':'#555';g.globalAlpha=.6+.4*Math.sin(T*3);g.beginPath();g.arc(o.x,o.y-14,6,0,6.28);g.fill();g.globalAlpha=1}
if(o.k=='merch'){g.fillStyle='#5a3a1a';g.fillRect(o.x-30,o.y-8,60,24);g.fillStyle='#c99a4a';g.fillRect(o.x-30,o.y-10,60,5);g.fillStyle=f.cl1?'#ffe08a':'#444';g.beginPath();g.arc(o.x,o.y-26,10+Math.sin(T*2)*2,0,6.28);g.fill();txt('MERCHANT',o.x,o.y+34,11,'#c99a4a','center')}
if(o.k=='chest'&&(!o.req||f['b_'+o.req])){g.fillStyle='#7a4a1a';g.fillRect(o.x-14,o.y-10,28,20);g.fillStyle='#e0b060';g.fillRect(o.x-14,o.y-10,28,5);g.fillRect(o.x-3,o.y-4,6,8)}
if(o.k=='altar'){const on=(o.id=='alt1'&&S.weapon>=1)||(o.id=='alt2'&&S.weapon>=2);g.fillStyle='#4a3a2a';g.fillRect(o.x-22,o.y,44,16);g.fillStyle=on?'#333':o.id=='alt2'?'#ffe26a':'#ff8a2a';if(!on){g.shadowColor=g.fillStyle;g.shadowBlur=20}g.beginPath();g.moveTo(o.x,o.y-34);g.lineTo(o.x+4,o.y-14);g.lineTo(o.x-4,o.y-14);g.fill();g.fillRect(o.x-10,o.y-16,20,4);g.shadowBlur=0}
if(o.k=='crystal'){g.fillStyle=o.lit?'#e8ffff':'#3aa0d0';g.shadowColor='#7ae8ff';g.shadowBlur=o.lit?30:12+Math.sin(T*4)*5;g.beginPath();g.moveTo(o.x,o.y-22);g.lineTo(o.x+12,o.y);g.lineTo(o.x,o.y+18);g.lineTo(o.x-12,o.y);g.fill();g.shadowBlur=0}
if(o.k=='heart'&&(!o.req||f['b_'+o.req])){g.fillStyle='#ff4a5a';g.shadowColor='#ffd24a';g.shadowBlur=20;heartPath(o.x,o.y+Math.sin(T*3)*3,16);g.fill();g.shadowBlur=0}
if((o.k=='item'||o.k=='item2')&&(!o.req||f['b_'+o.req])){const c={shard:'#7ae8ff',cloak:'#553377',stem:'#6aff7a',seal:'#6ab8ff'}[o.item]||'#fff';g.fillStyle=c;g.shadowColor='#fff';g.shadowBlur=18;const y=o.y+Math.sin(T*3)*3;g.beginPath();g.moveTo(o.x,y-12);g.lineTo(o.x+8,y);g.lineTo(o.x,y+12);g.lineTo(o.x-8,y);g.fill();g.shadowBlur=0}
if(o.k=='jar'&&!f['j_'+o.id]&&(!o.hid||S.eq.lantern)){g.fillStyle=o.hid?'#3a80a8':'#8a5a2a';g.beginPath();g.ellipse(o.x,o.y,11,14,0,0,6.28);g.fill();g.fillStyle='#222';g.fillRect(o.x-6,o.y-16,12,4);if(o.hid){g.strokeStyle='#8ae';g.beginPath();g.arc(o.x,o.y,20,0,6.28);g.stroke()}}
if(o.k=='beacon'){g.fillStyle='#ffd24a';g.shadowColor='#ffd24a';g.shadowBlur=40;g.beginPath();g.arc(480,270,18+Math.sin(T*3)*3,0,6.28);g.fill();g.shadowBlur=0;g.fillStyle='#4a3a2a';g.fillRect(455,285,50,16)}})}
function drawPlayer(){const x=P.x,y=P.y,a=P.fa,w=S.weapon;if(P.inv>0&&Math.floor(T*20)%2)g.globalAlpha=.5;
g.fillStyle='rgba(0,0,0,.35)';g.beginPath();g.ellipse(x,y+12,14,6,0,0,6.28);g.fill();
if(aura>0){g.fillStyle='rgba(255,210,74,'+Math.min(.5,aura*.3)+')';g.beginPath();g.arc(x,y,30+Math.sin(T*8)*3,0,6.28);g.fill()}
if(S.eq.lantern){const gr=g.createRadialGradient(x,y,2,x,y,50);gr.addColorStop(0,'rgba(120,200,255,.4)');gr.addColorStop(1,'rgba(120,200,255,0)');g.fillStyle=gr;g.fillRect(x-50,y-50,100,100)}
const cw=w==2?15:12;g.fillStyle=w==2?'#7a4a2a':'#3a4a6a';g.beginPath();g.moveTo(x-cw,y+12);g.lineTo(x+cw,y+12);g.lineTo(x+7,y-8);g.lineTo(x-7,y-8);g.fill();
g.strokeStyle=S.eq.cloak?'#1a1226':w==2?'rgba(255,220,120,'+(.6+.4*Math.sin(T*6))+')':'#8a6a3a';g.lineWidth=S.eq.cloak?4:2;g.beginPath();g.moveTo(x-cw,y+12);g.lineTo(x+cw,y+12);g.stroke();
g.fillStyle='#d8b890';g.beginPath();g.arc(x,y-11,7,0,6.28);g.fill();g.fillStyle='#c98a2a';g.beginPath();g.arc(x,y-13,7,3.3,6.1);g.fill();
// shoulder clasp
if(S.eq.cloak){g.fillStyle='#8a5aaa';g.fillRect(x-9,y-8,4,4)}
const side=a+(S.shard?-1.5:0);
// offhand
if(S.shard){const ox=x+Math.cos(a-1.4)*13,oy=y+Math.sin(a-1.4)*13;g.fillStyle='#7ae8ff';g.shadowColor='#7ae8ff';g.shadowBlur=8+6*Math.sin(T*5);g.beginPath();g.moveTo(ox,oy-7);g.lineTo(ox+4,oy);g.lineTo(ox,oy+7);g.lineTo(ox-4,oy);g.fill();g.shadowBlur=0}
// blade
const sw=P.atk>0?Math.max(0,P.atk/.4):0,ba=a+(P.atk>0&&P.atk<.2?(P.atk/.2-.5)*WA[w]*-1:0)+ (P.atk>.2?-WA[w]/2:0);
const bx=x+Math.cos(a+1.1)*10,by=y+Math.sin(a+1.1)*10,L=w==2?36:28,col=w==0?'#c8c8d0':w==1?'#ff8a2a':'#ffe26a';
if(w>0){g.shadowColor=col;g.shadowBlur=w==2?22:14}g.strokeStyle=col;g.lineWidth=w==2?6:4;g.lineCap='round';g.beginPath();g.moveTo(bx,by);g.lineTo(bx+Math.cos(ba)*L,by+Math.sin(ba)*L);g.stroke();g.shadowBlur=0;
if(S.eq.band){g.strokeStyle='#e8f0ff';g.lineWidth=2;g.beginPath();g.arc(bx,by,4,0,6.28);g.stroke()}
if(P.atk>0&&P.atk<.2){g.strokeStyle=col;g.globalAlpha*=.6;g.lineWidth=3;g.beginPath();g.arc(x,y,WR[w]*.8,a-WA[w]/2,a-WA[w]/2+WA[w]*(1-P.atk/.2));g.stroke();g.globalAlpha=1}
if(w==2){g.globalAlpha=.25+.1*Math.sin(T*4);g.fillStyle='#ffd24a';g.beginPath();g.arc(x,y-4,22,0,6.28);g.fill()}g.globalAlpha=1}
function drawEn(e){const x=e.x,y=e.y,r=e.r;g.save();if(e.fl>0)g.globalAlpha=.6;const c=e.fz>0?'#8ad0ff':e.fl>0?'#fff':'#4a3428';
g.fillStyle='rgba(0,0,0,.35)';g.beginPath();g.ellipse(x,y+r*.8,r,r*.4,0,0,6.28);g.fill();
g.fillStyle=c;g.strokeStyle='#8a4a1a';g.lineWidth=2;
if(e.t=='w'||e.t=='boss'||e.t=='mini'||e.t=='guard'){g.beginPath();g.arc(x,y,r,0,6.28);g.fill();g.stroke();g.fillStyle='#ff5a2a';g.fillRect(x-r*.5,y-r*.2,r*.35,r*.25);g.fillRect(x+r*.15,y-r*.2,r*.35,r*.25);g.strokeStyle='#1a1410';g.lineWidth=3;g.beginPath();g.moveTo(x-r*.6,y+r*.5);g.lineTo(x-r*.2,y+r*.3);g.lineTo(x+r*.2,y+r*.6);g.stroke()}
if(e.t=='s'){g.beginPath();g.moveTo(x,y-r);g.lineTo(x+r,y+r*.7);g.lineTo(x-r,y+r*.7);g.closePath();g.fill();g.stroke();g.fillStyle='#ff5a2a';g.beginPath();g.arc(x,y,3,0,6.28);g.fill()}
if(e.t=='boss'){if(!(F().vuln>T)){g.strokeStyle='rgba(106,184,255,.7)';g.lineWidth=4;g.beginPath();g.arc(x,y,r+8,0,6.28);g.stroke()}else{g.strokeStyle='#ffd24a';g.beginPath();g.arc(x,y,r+8,0,6.28);g.stroke()}}
if(e.mhp>3){g.fillStyle='#000';g.fillRect(x-r,y-r-12,r*2,5);g.fillStyle='#d24a2a';g.fillRect(x-r,y-r-12,r*2*e.hp/e.mhp,5)}g.restore()}
function drawDark(){if(![2,3,4].includes(S.z))return;dg.globalCompositeOperation='source-over';dg.clearRect(0,0,W,H);dg.fillStyle='rgba(4,6,14,.72)';dg.fillRect(0,0,W,H);dg.globalCompositeOperation='destination-out';const R=S.eq.lantern?420:290;const gr=dg.createRadialGradient(P.x,P.y,30,P.x,P.y,R);gr.addColorStop(0,'rgba(0,0,0,1)');gr.addColorStop(1,'rgba(0,0,0,0)');dg.fillStyle=gr;dg.fillRect(0,0,W,H);
objs.forEach(o=>{if(o.k=='stone'||o.k=='altar'||o.k=='crystal'){const gr=dg.createRadialGradient(o.x,o.y,5,o.x,o.y,90);gr.addColorStop(0,'rgba(0,0,0,.8)');gr.addColorStop(1,'rgba(0,0,0,0)');dg.fillStyle=gr;dg.fillRect(o.x-90,o.y-90,180,180)}});g.drawImage(dk,0,0)}
function hud(){for(let i=0;i<S.cont;i++){const hx=28+i*32;g.fillStyle='#2a1418';heartPath(hx,26,15);g.fill();g.strokeStyle='#ff8a8a';g.lineWidth=1.5;g.stroke();
if(S.hp>=i*2+1){g.save();heartPath(hx,26,15);g.clip();g.fillStyle='#e8303f';g.fillRect(hx-16,0,S.hp>=i*2+2?32:16,60);g.restore()}}
g.fillStyle='#ffd24a';g.beginPath();g.arc(20,58,7,0,6.28);g.fill();txt(''+S.gold,34,63,16,'#ffd24a');
let y=84;txt('Blade: '+['Ember Blade','Ignited Edge','Solar Falchion'][S.weapon],14,y,13,['#ccc','#ff9a4a','#ffe26a'][S.weapon]);y+=18;
if(S.shard){txt('Aether Shard ×'+S.ammo+' (K / R-click)',14,y,13,'#7ae8ff');y+=18}
txt('Stem Q: '+S.stem+' Seal R: '+S.seal+' Inventory: I',14,y,13,'#9ad89a');
if(shimmer>0){const a=Math.min(.5,shimmer*.3),gr=g.createLinearGradient(0,0,40,0);g.fillStyle='rgba(255,200,80,'+a+')';g.fillRect(0,0,12,H);g.fillRect(W-12,0,12,H);g.fillRect(0,0,W,12);g.fillRect(0,H-12,W,12)}
if(flash>0){const gr=g.createRadialGradient(W/2,H/2,H*.3,W/2,H/2,H*.9);gr.addColorStop(0,'rgba(255,0,0,0)');gr.addColorStop(1,'rgba(255,0,0,'+flash*1.6+')');g.fillStyle=gr;g.fillRect(0,0,W,H)}
if(banner){g.globalAlpha=Math.min(1,banner.t);txt(banner.a,W/2,120,34,'#f0c878','center');txt(banner.b,W/2,150,16,'#b89a70','center');g.globalAlpha=1}
msgs.slice(-3).forEach((m,i,a)=>{g.globalAlpha=Math.min(1,m.l);g.fillStyle='rgba(0,0,0,.55)';g.fillRect(W/2-380,H-40-(a.length-1-i)*26-16,760,22);txt(m.t,W/2,H-40-(a.length-1-i)*26,14,'#f0e0c0','center');g.globalAlpha=1});
let best=null,bd=55;objs.forEach(o=>{if(['stone','merch','chest','altar','beacon'].includes(o.k)&&(!o.req||F()['b_'+o.req])){const d=Math.hypot(P.x-o.x,P.y-o.y);if(d<bd){bd=d;best=o}}});
if(best)txt('[E] '+({stone:'Rest at Sanctuary Stone',merch:'Trade',chest:'Open',altar:'Take the power',beacon:'Ignite the Core Beacon'})[best.k],best.x,best.y-46,14,'#fff','center')}
function panel(t){g.fillStyle='rgba(6,8,14,.85)';g.fillRect(0,0,W,H);g.strokeStyle='#c99a4a';g.lineWidth=2;g.strokeRect(160,60,640,420);txt(t,W/2,100,26,'#f0c878','center')}
function invRows(){const r=[];if(S.stem)r.push({n:'Vital Stem ×'+S.stem+' — restores 4 HP',a:()=>useItem('stem'),c:'#6aff7a'});if(S.seal)r.push({n:'Binding Seal ×'+S.seal+' — freeze nearest foe 3s',a:()=>useItem('seal'),c:'#6ab8ff'});
[['band',"Warden's Band — attack speed",'#e8f0ff'],['lantern','Phantom Lantern — reveals hidden caches','#7ac8ff'],['cloak','Shadow Cloak — halves trap damage','#b48ad8']].forEach(([k,n,c])=>{if(S[k])r.push({n:n+(S.eq[k]?' [EQUIPPED]':' [off]'),a:()=>{S.eq[k]=!S.eq[k];sfx.chime()},c})});
if(S.shard)r.push({n:'Aether Shard — '+S.ammo+' charges (right-click / K)',a:()=>{},c:'#7ae8ff'});return r}
function drawMenus(){
if(mode=='inv'){panel('Inventory');const r=invRows();if(!r.length)txt('You carry nothing yet. Explore.',W/2,200,16,'#aaa','center');r.forEach((x,i)=>txt((i+1)+'. '+x.n,200,160+i*34,18,x.c));txt('Press number to use / toggle · I or Esc to close',W/2,455,14,'#a89060','center');txt('Gold '+S.gold+' · Hearts '+S.cont+'/5 · Weapon '+['Ember Blade','Ignited Edge','Solar Falchion'][S.weapon],W/2,430,14,'#ffd24a','center')}
if(mode=='shop'){panel('Merchant Altar');const b=S.bought;const rows=[['Vital Stem — restores 4 HP',50,3-b.stem],['Binding Seal — freezes a foe',75,2-b.seal],['Heart Container #5',300,S.cont>=5?0:1-b.heart],['Aether charge'+(F().crystal4&&S.shard?'':' (after Zone 4)'),10,F().crystal4&&S.shard?99:0]];
rows.forEach((r,i)=>{txt((i+1)+'. '+r[0],200,170+i*44,18,r[2]>0&&S.gold>=r[1]?'#f0e0c0':'#776');txt(r[2]>0?r[1]+'g':'sold out',740,170+i*44,18,'#ffd24a','right')});txt('Gold: '+S.gold,W/2,400,18,'#ffd24a','center');txt('Press 1–4 to buy · Esc to leave',W/2,455,14,'#a89060','center');if(msgs.length)txt(msgs[msgs.length-1].t,W/2,430,14,'#f0e0c0','center')}
if(mode=='dead'){g.fillStyle='rgba(30,0,0,.8)';g.fillRect(0,0,W,H);txt('The Corrosion Claims You',W/2,240,40,'#c04a3a','center');txt('Enter — return to the last Sanctuary Stone',W/2,290,18,'#d8b890','center')}
if(mode=='win'){g.fillStyle='rgba(20,12,0,.9)';g.fillRect(0,0,W,H);const gr=g.createRadialGradient(W/2,H/2,10,W/2,H/2,400+Math.sin(T*2)*30);gr.addColorStop(0,'rgba(255,220,100,.9)');gr.addColorStop(1,'rgba(255,150,40,0)');g.fillStyle=gr;g.fillRect(0,0,W,H);txt('The Beacon Burns',W/2,220,50,'#fff4d0','center');txt('The Corrosion recedes. Aurelion\'s Observatory lights the storm once more.',W/2,270,18,'#5a3a10','center');txt('Gold: '+S.gold+' Hearts: '+S.cont+'/5 Press Enter for title',W/2,320,16,'#5a3a10','center')}
if(mode=='title'){g.fillStyle='#0a0d16';g.fillRect(0,0,W,H);for(let i=0;i<40;i++){g.fillStyle='rgba(255,200,100,.5)';g.fillRect((i*137+T*8)%W,(i*71)%H,2,2)}drawGear();txt("THE WARDEN'S CORROSION",W/2,200,48,'#f0c878','center');txt("Aurelion's Sunken Observatory",W/2,238,20,'#8a9ab8','center');txt(hasSave()?'Enter — Continue N — New Game':'Enter — Begin',W/2,320,20,'#f0e0c0','center');
['WASD move · Mouse aim · Click/Space strike · Right-click/K Aether Shard','E interact · Q Vital Stem · R Binding Seal · I inventory'].forEach((t,i)=>txt(t,W/2,400+i*24,14,'#8a8a9a','center'))}}
function drawGear(){g.save();g.translate(W/2,270);g.rotate(T*.1);g.strokeStyle='rgba(200,140,60,.15)';g.lineWidth=14;g.beginPath();g.arc(0,0,220,0,6.28);g.stroke();for(let k=0;k<12;k++){g.rotate(.5236);g.strokeRect(214,-14,30,28)}g.restore()}
function draw(){g.save();g.clearRect(0,0,W,H);if(mode=='title'){drawMenus();g.restore();return}
g.translate((Math.random()-.5)*shake,(Math.random()-.5)*shake);drawBG();drawObjs();
pk.forEach(p=>{g.fillStyle=p.k=='gold'?'#ffd24a':'#7ae8ff';g.beginPath();g.arc(p.x,p.y,p.k=='gold'?5:4,0,6.28);g.fill();g.strokeStyle='#fff8';g.stroke()});
en.forEach(drawEn);drawPlayer();
pr.forEach(p=>{g.fillStyle=p.e?'#ff5a3a':'#aef';g.shadowColor=p.e?'#f50':'#7ae8ff';g.shadowBlur=10;g.beginPath();g.arc(p.x,p.y,p.e?5:4,0,6.28);g.fill();g.shadowBlur=0});
pt.forEach(p=>{g.globalAlpha=Math.max(0,p.l/p.m);g.fillStyle=p.c;g.fillRect(p.x,p.y,p.s,p.s)});g.globalAlpha=1;
ft.forEach(t=>{g.globalAlpha=Math.min(1,t.l*2);txt(t.t,t.x,t.y,14,t.c,'center')});g.globalAlpha=1;
drawDark();g.restore();hud();drawMenus()}
// ---------- input/loop
addEventListener('keydown',e=>{const k=e.key.toLowerCase();keys[k]=true;ac();if(k==' '||k.startsWith('arrow'))e.preventDefault();
if(mode=='title'){if(k=='enter'){hasSave()?loadSave():startNew()}if(k=='n')startNew();return}
if(mode=='dead'&&k=='enter'){loadSave();return}if(mode=='win'&&k=='enter'){mode='title';return}
if(mode=='shop'){if(k=='escape'||k=='e')mode='play';if('1234'.includes(k)&&k)buy(+k-1);return}
if(mode=='inv'){if(k=='i'||k=='escape'||k=='tab')mode='play';const n=parseInt(k);if(n){const r=invRows()[n-1];if(r)r.a()}return}
if(mode=='play'){if(k=='e')interact();if(k=='i'||k=='tab'){e.preventDefault();mode='inv'}if(k=='q')useItem('stem');if(k=='r')useItem('seal');if(k=='k')shoot()}});
addEventListener('keyup',e=>keys[e.key.toLowerCase()]=false);
function startNew(){try{localStorage.removeItem('wc_save')}catch(e){}S=newS();mode='play';loadZone(0,null);S.hp=6}
function mpos(e){const r=cv.getBoundingClientRect();mouse.x=(e.clientX-r.left)*W/r.width;mouse.y=(e.clientY-r.top)*H/r.height}
cv.addEventListener('mousemove',mpos);cv.addEventListener('mousedown',e=>{mpos(e);ac();if(e.button==2)shoot();else mouse.down=true});addEventListener('mouseup',()=>mouse.down=false);cv.addEventListener('contextmenu',e=>e.preventDefault());
let last=performance.now();function loop(t){const dt=Math.min(.05,(t-last)/1000);last=t;if(mode=='title')T+=dt;try{update(dt)}catch(e){console.error(e)}draw();requestAnimationFrame(loop)}requestAnimationFrame(loop);
</script></body></html>
<!-- agent-meta {"model":"anthropic/claude-sonnet-5.5","provider":"openrouter","persona":"full","sessionId":"36317b61-ad7a-4c68-8eb0-791a15ae5406","tokensIn":122185,"tokensOut":37444,"tokensTotal":159629,"cost":0.5765302,"turns":4,"toolCalls":4,"failedToolCalls":0,"timestamp":"2026-09-29T14:51:33.055Z"} -->