← Warden results

Warden

Claude Opus 5.5 · full

Initial view of Warden

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 108,529 bytes · SHA-256 04ad46739c77
<!DOCTYPE html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>The Warden's Corrosion</title>
<style>
*{box-sizing:border-box}
html,body{margin:0;height:100%;background:#04060b;overflow:hidden;font-family:'Palatino Linotype',Palatino,Georgia,serif;color:#f0dcae;user-select:none}
#game{position:absolute;left:50%;top:50%;width:960px;height:608px;transform:translate(-50%,-50%)}
canvas{display:block;background:#000;box-shadow:0 0 60px #000}
.ov{position:absolute;inset:0;display:none;flex-direction:column;align-items:center;justify-content:center;background:radial-gradient(ellipse at center,rgba(22,28,48,.9),rgba(3,4,9,.97));text-align:center;z-index:5}
.ov.show{display:flex}
.ov.light{background:rgba(4,6,12,.72)}
h1{font-size:52px;margin:0 0 4px;letter-spacing:5px;color:#f3c66b;text-shadow:0 0 18px #c8792a,0 0 44px #6a3a10;font-weight:normal}
h2{font-size:26px;margin:0 0 12px;color:#f3c66b;font-weight:normal;letter-spacing:3px}
.sub{color:#8fa3c8;letter-spacing:3px;font-size:14px;margin-bottom:18px}
.lore{max-width:600px;font-size:15px;line-height:1.55;color:#cdbb92;margin:6px 0 18px;font-style:italic}
.btn{background:linear-gradient(#5a4220,#2f2210);border:1px solid #c9973f;color:#ffe3a6;padding:9px 24px;margin:5px;font:inherit;font-size:17px;cursor:pointer;border-radius:3px;min-width:210px;transition:.15s}
.btn:hover{background:linear-gradient(#7a5a2a,#3f2d14);box-shadow:0 0 14px #c8792a}
.btn:disabled{opacity:.35;cursor:default;box-shadow:none}
.ctrl{font-size:13px;color:#8fa3c8;margin-top:16px;line-height:1.7}
.ctrl b{color:#f0c878;font-weight:normal}
.panel{background:linear-gradient(#1a2135,#0f1322);border:1px solid #a87b34;box-shadow:0 0 30px rgba(200,140,50,.25),inset 0 0 30px rgba(0,0,0,.6);padding:20px 28px;border-radius:6px;width:700px;text-align:left}
.row{display:flex;align-items:center;gap:12px;padding:7px 10px;border-bottom:1px solid rgba(168,123,52,.25)}
.row img{width:36px;height:36px}
.row .nm{flex:1}
.row .nm small{display:block;color:#8fa3c8;font-size:12px}
.tag{font-size:12px;padding:3px 8px;border:1px solid #5a6a90;border-radius:3px;color:#a9bde0}
.tag.on{border-color:#e6b552;color:#ffe3a6;background:rgba(200,140,50,.18)}
.sbtn{background:#2f2210;border:1px solid #c9973f;color:#ffe3a6;font:inherit;padding:5px 12px;cursor:pointer;border-radius:3px;min-width:96px}
.sbtn:hover{background:#5a4220}
.sbtn:disabled{opacity:.35;cursor:default}
.cols{display:flex;gap:22px}
.cols>div{flex:1}
.sec{color:#f3c66b;letter-spacing:2px;font-size:13px;margin:10px 0 4px;text-transform:uppercase}
.hint{font-size:12px;color:#7d8fb3;margin-top:10px;text-align:center}
.gold{color:#ffd36a}
</style></head><body>
<div id="game"><canvas id="c" width="960" height="608"></canvas>
<div id="title" class="ov show">
  <div class="sub">AURELION'S SUNKEN OBSERVATORY</div>
  <h1>THE WARDEN'S CORROSION</h1>
  <div class="lore">Black iron creeps through the bronze halls. The stars no longer turn. You are the Last Warden — reach the Core Chamber and reignite the Beacon before the Corrosion drags the Observatory into the storm below.</div>
  <button class="btn" id="bNew">Begin the Vigil</button>
  <button class="btn" id="bCont">Continue from Sanctuary</button>
  <div class="ctrl"><b>WASD / Arrows</b> move &nbsp;·&nbsp; <b>J / Space / Left-click</b> strike &nbsp;·&nbsp; <b>K / Right-click</b> Aether Shard (once found)<br>
  <b>E</b> interact &nbsp;·&nbsp; <b>1</b> Vital Stem &nbsp;·&nbsp; <b>2</b> Binding Seal &nbsp;·&nbsp; <b>I / Tab</b> inventory &nbsp;·&nbsp; <b>M</b> mute</div>
</div>
<div id="inv" class="ov light"><div class="panel">
  <h2>Warden's Satchel</h2>
  <div class="cols"><div><div class="sec">Arms</div><div id="invArms"></div><div class="sec">Vitality</div><div id="invHp"></div></div>
  <div><div class="sec">Accessories (toggle)</div><div id="invAcc"></div><div class="sec">Consumables</div><div id="invCons"></div></div></div>
  <div class="hint">Keys in satchel: <b>Q</b> Band · <b>W</b> Lantern · <b>R</b> Cloak · <b>1</b> Stem · <b>2</b> Seal &nbsp;—&nbsp; <b>I / Esc</b> close</div>
</div></div>
<div id="shop" class="ov light"><div class="panel">
  <h2>Merchant Altar of the Archives</h2>
  <div style="color:#8fa3c8;font-size:13px;margin-bottom:8px;font-style:italic">"Sigils for salvation, Warden. The old machines still honour a fair trade." &nbsp; — Purse: <span class="gold" id="shopGold">0</span> gold</div>
  <div id="shopList"></div>
  <div class="hint">Click or press <b>1–4</b> to buy · <b>E / Esc</b> leave</div>
</div></div>
<div id="dead" class="ov"><h1 style="color:#d0574a;text-shadow:0 0 20px #800">THE CORROSION CLAIMS YOU</h1>
  <div class="lore">Black iron seals your joints... yet a Sanctuary Stone still remembers your oath.</div>
  <button class="btn" id="bRevive">Reawaken at Sanctuary</button></div>
<div id="end" class="ov"><div class="sub">THE CORE BEACON BURNS AGAIN</div><h1>DAWN OVER AURELION</h1>
  <div class="lore" id="endText"></div><button class="btn" id="bAgain">Begin a New Vigil</button></div>
</div>
<script>
'use strict';
// ================= AUDIO =================
const AU={ctx:null,muted:false};
function aInit(){
  if(AU.ctx){if(AU.ctx.state==='suspended')AU.ctx.resume();return;}
  try{
  const C=new (window.AudioContext||window.webkitAudioContext)();AU.ctx=C;
  AU.master=C.createGain();AU.master.gain.value=.55;AU.master.connect(C.destination);
  AU.del=C.createDelay(1);AU.del.delayTime.value=.22;AU.fb=C.createGain();AU.fb.gain.value=.33;
  AU.wet=C.createGain();AU.wet.gain.value=.45;AU.del.connect(AU.fb);AU.fb.connect(AU.del);AU.del.connect(AU.wet);AU.wet.connect(AU.master);
  const len=C.sampleRate;const b=C.createBuffer(1,len,C.sampleRate);const d=b.getChannelData(0);for(let i=0;i<len;i++)d[i]=Math.random()*2-1;AU.nb=b;
  AU.drone=C.createGain();AU.drone.gain.value=0;AU.drone.connect(AU.master);
  const lp=C.createBiquadFilter();lp.type='lowpass';lp.frequency.value=380;lp.connect(AU.drone);
  [55,82.4,110.3,164.8].forEach((f,i)=>{const o=C.createOscillator();o.type=i%2?'triangle':'sawtooth';o.frequency.value=f;o.detune.value=(i-1.5)*4;const g=C.createGain();g.gain.value=.1;o.connect(g);g.connect(lp);o.start();});
  // ambient wind
  const ns=C.createBufferSource();ns.buffer=b;ns.loop=true;const wf=C.createBiquadFilter();wf.type='lowpass';wf.frequency.value=260;
  AU.wind=C.createGain();AU.wind.gain.value=.05;ns.connect(wf);wf.connect(AU.wind);AU.wind.connect(AU.master);ns.start();
  }catch(e){AU.ctx=null;}
}
function tone(f,dur,o={}){
  const C=AU.ctx;if(!C||AU.muted)return;const t=C.currentTime+(o.delay||0);
  const os=C.createOscillator();os.type=o.type||'sine';os.frequency.setValueAtTime(f,t);
  if(o.to)os.frequency.exponentialRampToValueAtTime(o.to,t+dur);
  const g=C.createGain();const v=o.vol||.2,a=o.att||.004;
  g.gain.setValueAtTime(.0001,t);g.gain.exponentialRampToValueAtTime(v,t+a);g.gain.exponentialRampToValueAtTime(.0001,t+dur);
  os.connect(g);g.connect(AU.master);
  if(o.echo){const s=C.createGain();s.gain.value=o.echo;g.connect(s);s.connect(AU.del);}
  os.start(t);os.stop(t+dur+.05);
}
function noise(dur,o={}){
  const C=AU.ctx;if(!C||AU.muted)return;const t=C.currentTime+(o.delay||0);
  const s=C.createBufferSource();s.buffer=AU.nb;
  const fl=C.createBiquadFilter();fl.type=o.ft||'bandpass';fl.frequency.setValueAtTime(o.f||1000,t);fl.Q.value=o.q||1;
  if(o.to)fl.frequency.exponentialRampToValueAtTime(o.to,t+dur);
  const g=C.createGain();const v=o.vol||.2;g.gain.setValueAtTime(.0001,t);g.gain.exponentialRampToValueAtTime(v,t+(o.att||.004));g.gain.exponentialRampToValueAtTime(.0001,t+dur);
  s.connect(fl);fl.connect(g);g.connect(AU.master);
  if(o.echo){const e=C.createGain();e.gain.value=o.echo;g.connect(e);e.connect(AU.del);}
  s.start(t,Math.random()*.5);s.stop(t+dur+.05);
}
function crackle(dur,vol){for(let i=0;i<8;i++)noise(.018,{f:1800+Math.random()*3500,q:2,vol:vol*(.4+Math.random()*.6),delay:Math.random()*dur});}
const SFX={
  step(k){
    if(k==='~'){noise(.17,{f:1000,to:280,q:.8,vol:.17,echo:.25});noise(.07,{f:2600,q:2,vol:.05,delay:.04});}
    else if(k===','){tone(175,.09,{type:'square',vol:.045,to:110});noise(.05,{f:3300,q:4,vol:.08});tone(1800+Math.random()*400,.06,{type:'triangle',vol:.025,echo:.2});}
    else{noise(.08,{ft:'lowpass',f:480,vol:.13,echo:Z?Z.D.echo:.15});noise(.06,{f:1300,q:3,vol:.03});}
  },
  swing(w){
    if(w===0){noise(.14,{f:700,to:3400,q:1.3,vol:.22});}
    else if(w===1){noise(.11,{f:900,to:3800,q:1.2,vol:.22});crackle(.22,.12);}
    else{noise(.24,{f:260,to:1700,q:1,vol:.3});tone(70,.25,{type:'triangle',vol:.12,to:50});}
  },
  hit(w){
    if(w===0){tone(150,.12,{to:55,vol:.35});noise(.04,{f:2600,vol:.14});}
    else if(w===1){tone(105,.22,{to:38,vol:.42});crackle(.3,.16);noise(.06,{f:1500,vol:.14});}
    else{[523,747,1118,1490].forEach(f=>tone(f,.75,{vol:.08,echo:.45}));tone(52,.7,{to:32,vol:.55});tone(80,.45,{type:'triangle',vol:.2,to:40});noise(.08,{f:2000,vol:.15});}
  },
  windup(){tone(85,.14,{type:'sawtooth',to:270,vol:.07});noise(.14,{ft:'lowpass',f:250,to:1000,vol:.13});},
  shard(){tone(1750,.2,{type:'triangle',to:860,vol:.16});tone(2600,.1,{vol:.08});tone(440,.12,{type:'sawtooth',to:220,vol:.035});},
  chime(){tone(2637,.55,{vol:.07,echo:.6});tone(3951,.45,{vol:.04,echo:.6,delay:.02});},
  hurt(){tone(95,.22,{to:40,vol:.5});noise(.1,{ft:'lowpass',f:420,vol:.3});tone(1396,.3,{type:'square',vol:.035});tone(1480,.3,{type:'square',vol:.035});},
  coin(){tone(2093+Math.random()*200,.06,{type:'square',vol:.035});tone(3136,.12,{type:'triangle',vol:.055,delay:.03});},
  item(){[523,659,784,1046].forEach((f,i)=>tone(f,.28,{type:'triangle',vol:.1,delay:i*.07,echo:.3}));},
  rare(){[392,494,587,784,988].forEach((f,i)=>tone(f,1.7,{vol:.075,att:.05,delay:i*.035,echo:.5}));tone(196,1.7,{type:'triangle',vol:.08,att:.06});},
  shimmer(d=0){for(let i=0;i<9;i++)tone(2000+Math.random()*3200,.32,{vol:.022,delay:d+i*.04,echo:.5});},
  buy(){for(let i=0;i<4;i++)tone(1700+i*230,.07,{type:'square',vol:.03,delay:i*.06});tone(1046,.45,{vol:.1,delay:.28,echo:.3});tone(1568,.55,{vol:.08,delay:.34,echo:.4});SFX.shimmer(.45);},
  door(){noise(.9,{ft:'lowpass',f:220,vol:.35});tone(60,.9,{type:'sawtooth',vol:.05,to:44});},
  plate(){tone(220,.1,{type:'square',vol:.06,to:140});noise(.09,{ft:'lowpass',f:600,vol:.16});},
  tick(){tone(1250,.04,{type:'square',vol:.03});},
  stone(){[261,329,392,523,659].forEach((f,i)=>tone(f,2.2,{vol:.065,att:.25,delay:i*.05,echo:.6}));},
  crack(){noise(.09,{f:900,q:.7,vol:.2});tone(210,.07,{to:90,vol:.14});},
  brk(){noise(.55,{ft:'lowpass',f:1000,to:140,vol:.45});tone(70,.45,{to:34,vol:.35});},
  jar(){noise(.13,{f:3000,q:1.5,vol:.17});tone(1300,.08,{type:'triangle',vol:.06});},
  edie(){noise(.32,{f:620,to:110,q:.8,vol:.3});tone(320,.25,{type:'square',to:60,vol:.05});tone(1200,.2,{type:'triangle',vol:.03,to:600});},
  ebolt(){tone(480,.16,{type:'sawtooth',to:190,vol:.035});},
  block(){tone(1900,.15,{type:'square',vol:.035});tone(880,.25,{vol:.1,echo:.3});},
  seal(){tone(880,.7,{vol:.09,to:440,echo:.4});tone(1320,.7,{vol:.05,echo:.4});noise(.4,{f:4000,q:3,vol:.05});},
  heal(){[392,523,659,784].forEach((f,i)=>tone(f,.45,{vol:.08,delay:i*.05,echo:.3}));},
  denied(){tone(160,.16,{type:'square',vol:.05});tone(150,.16,{type:'square',vol:.05,delay:.1});},
  roar(){noise(1.2,{ft:'lowpass',f:300,to:80,vol:.5});tone(45,1.2,{type:'sawtooth',vol:.12,to:30});},
  light(){[523,784,1046,1568].forEach((f,i)=>tone(f,1.4,{vol:.06,delay:i*.12,echo:.6}));},
  thunder(){noise(1.6,{ft:'lowpass',f:160,vol:.25,att:.05});}
};
// ================= UTILS =================
const cv=document.getElementById('c'),ctx=cv.getContext('2d');
const TS=32,VW=960,VH=608;
const $=id=>document.getElementById(id);
const rnd=(a,b)=>a+Math.random()*(b-a);
const ri=(a,b)=>Math.floor(rnd(a,b+1));
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const dist=(a,b)=>Math.hypot(a.x-b.x,a.y-b.y);
const hash=(x,y,s=0)=>{let h=(x*374761393+y*668265263+s*1442695041)|0;h=(h^(h>>>13))*1274126177|0;return((h^(h>>>16))>>>0)/4294967296;};
function angDiff(a,b){let d=a-b;while(d>Math.PI)d-=Math.PI*2;while(d<-Math.PI)d+=Math.PI*2;return d;}
function fit(){const s=Math.min(innerWidth/VW,innerHeight/VH);$('game').style.transform=`translate(-50%,-50%) scale(${s})`;}
addEventListener('resize',fit);fit();
// ================= STATE =================
const SAVE_KEY='wardens_corrosion_v1';
function newState(){return{zone:0,hp:6,maxC:3,gold:0,weapon:0,shard:false,ammo:0,acc:{band:0,lantern:0,cloak:0},eq:{band:0,lantern:0,cloak:0},stems:0,seals:0,flags:{},shop:{stem:0,seal:0,heart:0},stone:null,kills:0,time:0};}
let st=newState(),savedStr=null;
const fl=k=>!!st.flags[k];
const setF=k=>{st.flags[k]=1;};
function saveGame(z,x,y){st.stone={z,x,y};savedStr=JSON.stringify(st);try{localStorage.setItem(SAVE_KEY,savedStr);}catch(e){}}
function getSave(){if(savedStr)return savedStr;try{return localStorage.getItem(SAVE_KEY);}catch(e){return null;}}
</script>
<script>
// ================= ZONES =================
function Bld(w,h,floor){
  const g=[];for(let y=0;y<h;y++)g.push(new Array(w).fill('#'));
  const b={w,h,g,floor,ents:[],
    R(x,y,ww,hh,t){for(let j=y;j<y+hh;j++)for(let i=x;i<x+ww;i++)if(i>=0&&j>=0&&i<w&&j<h)g[j][i]=t||floor;},
    e(t,x,y,p){b.ents.push(Object.assign({t,tx:x,ty:y},p||{}));},
    door(id,x,y,ww,hh,p){for(let j=y;j<y+hh;j++)for(let i=x;i<x+ww;i++){g[j][i]=floor;b.e('door',i,j,Object.assign({id},p||{}));}},
    crack(x,y,p){g[y][x]=floor;b.e('crack',x,y,p);}
  };
  return b;
}
const ZD=[
{name:'The Ashen Threshold',sub:'I · Prologue',w:47,h:22,floor:'.',amb:'ash',echo:.18,dark:0,
 pal:{f1:'#3a3f4f',f2:'#333846',wt:'#1b2030',wf:'#6b4a26',accent:'#c8862f'},P:[6,11],Q:[42,10],
 build(b){
  b.R(2,6,10,9);b.R(12,9,6,3);b.R(18,4,12,14);b.R(30,9,4,3);b.R(34,3,10,16);b.R(44,9,3,3);
  b.R(36,11,3,1,'^');b.R(39,15,2,2,'^');
  b.e('stone',5,8,{sid:0});
  b.e('note',9,7,{text:'Sanctuary Stones remember a Warden\'s oath. Rest upon one to mend your wounds and bind your progress.'});
  b.e('note',20,15,{text:'A bronze plate. Its weight-sigil holds the far gate open only while the clock-hand turns. Step and run!'});
  b.e('gear',6,11,{rad:60});b.e('gear',38,7,{rad:44});b.e('gear',24,10,{rad:70});
  b.e('statue',3,13);b.e('statue',10,13);
  b.door('td1',31,9,1,3,{timed:1});
  b.e('plate',25,15,{door:'td1',time:4.5});
  b.e('husk',22,7);b.e('husk',26,12);b.e('wisp',38,6);b.e('husk',41,14);b.e('husk',36,16);
  b.e('jar',19,5);b.e('jar',28,16);b.e('jar',43,4);b.e('jar',35,17);b.e('jar',10,13);b.e('jar',29,5);
  b.e('exit',46,9,{h:3,to:1});
  b.e('telescope',41,5);
 }},
{name:'The Rusted Archives',sub:'II · Early',w:53,h:32,floor:',',amb:'dust',echo:.3,dark:.25,
 pal:{f1:'#2e3440',f2:'#3a3428',wt:'#1d1f28',wf:'#7a4a22',accent:'#b8742a'},P:[4,15],Q:[49,15],
 build(b){
  b.R(1,13,8,5);b.R(9,14,5,3);b.R(14,8,16,16);b.R(30,14,4,3);b.R(34,9,14,13,'.');b.R(48,14,5,3);
  b.R(15,1,14,3);b.R(20,4,4,4);b.R(10,1,4,3);
  b.R(21,24,3,3);b.R(14,27,16,4);b.R(9,27,4,3);
  b.crack(14,2,{hp:8,key:'cw2a'});b.crack(13,28,{hp:8,key:'cw2b'});
  b.e('chest',11,2,{key:'ch2band',item:'band'});
  b.e('chest',10,28,{key:'ch2stem',item:'stem',gold:15});
  b.door('ar2w',13,14,1,3);b.door('ar2e',30,14,1,3,{flag:'z2arena'});b.door('ar2n',20,7,4,1,{flag:'z2arena'});b.door('ar2s',21,24,3,1,{flag:'z2arena'});
  b.e('arena',15,9,{w:14,h:14,flag:'z2arena',doors:['ar2w','ar2e','ar2n','ar2s'],msg:'The Archive seals slam shut — Constructs stir!',
     waves:[[['husk',18,11],['husk',25,11],['sentinel',22,20]],[['husk',17,19],['husk',27,19],['wisp',21,10],['wisp',23,14]]],done:'The seals of the Archives crack open. The Sanctum lies east.'});
  b.e('stone',40,15,{sid:1});b.e('merchant',44,11);
  b.e('note',37,19,{text:'"Spend wisely, Warden. The heart-engine I guard costs three hundred sigils — I will not part with it for less."'});
  b.e('note',4,14,{text:'The Archives. Shelves of star-charts rot beneath the Corrosion. Mind the cracked masonry — some walls are hollow.'});
  for(const x of [35,37,45,47])b.e('shelf',x,9);
  b.e('gear',22,15,{rad:90});b.e('gear',41,16,{rad:50});
  b.e('husk',6,14);
  b.e('sentinel',25,2);b.e('wisp',18,2);
  b.e('husk',17,29);b.e('husk',26,29);b.e('sentinel',22,28);
  b.e('jar',16,1);b.e('jar',28,1);b.e('jar',28,3);b.e('jar',15,30);b.e('jar',29,30);b.e('jar',29,27);b.e('jar',2,17);b.e('jar',34,21);
  b.e('exit',52,14,{h:3,to:2});b.e('exit',1,13,{h:5,to:0});
 }},
{name:'The Collapsed Gallery',sub:'III · Mid-Early',w:50,h:28,floor:'.',amb:'ash',echo:.35,dark:.2,
 pal:{f1:'#3b3a44',f2:'#43392f',wt:'#1e1c24',wf:'#7d5328',accent:'#d08a35'},P:[4,13],Q:[44,14],
 build(b){
  b.R(1,12,7,4);b.R(8,8,20,14);b.R(13,2,10,5);b.R(28,13,5,3);b.R(33,6,14,16);b.R(47,13,3,3);b.R(10,23,5,3);
  b.crack(12,22,{hp:8,key:'cw3'});
  b.e('pickup',12,24,{kind:'heart',key:'heart4'});
  b.door('altar1d',17,7,2,1,{flag:'pp3'});
  b.e('altar',17.5,3.2,{up:1});
  b.e('plate',10,10,{group:'pp3',window:5});b.e('plate',25,19,{group:'pp3',window:5});
  b.e('note',5,12,{text:'Two bronze plates, far apart. The altar door yields only when both are pressed within a few heartbeats — or when something heavy holds one down.'});
  b.door('curd',30,13,1,3,{flag:'up1'});
  b.door('z3exit',47,13,1,3,{flag:'curator'});
  b.e('arena',34,7,{w:12,h:14,flag:'curator',doors:['curd','z3exit'],msg:'The Curator Construct awakens!',waves:[[['curator',40,13]]],done:'The Curator collapses. The eastern gate grinds open.'});
  b.e('statue',14,15);b.e('statue',21,11);b.e('statue',20,19);b.e('statue',35,8);b.e('statue',45,8);b.e('statue',35,20);b.e('statue',45,20);
  b.e('telescope',9,9);
  b.e('gear',18,15,{rad:100});b.e('gear',40,14,{rad:80});
  b.e('husk',14,12);b.e('husk',22,16);b.e('wisp',18,18);b.e('sentinel',24,10);b.e('husk',11,18);b.e('wisp',26,14);
  b.e('jar',9,20);b.e('jar',26,9);b.e('jar',14,3);b.e('jar',21,3);b.e('jar',27,21);b.e('jar',2,15);
  b.e('exit',49,13,{h:3,to:3});b.e('exit',1,12,{h:4,to:1});
 }},
{name:'The Sunken Vault',sub:'IV · The Turning Point',w:50,h:34,floor:'~',amb:'mist',echo:.5,dark:.72,
 pal:{f1:'#1d3040',f2:'#22394a',wt:'#121a24',wf:'#3e4a50',accent:'#5aa0c8'},P:[4,4],Q:[46,25],
 build(b){
  b.R(1,3,6,4);b.R(7,2,18,12);b.R(25,6,1,3);b.R(26,2,16,12,'.');b.R(32,15,5,3,'.');b.R(9,15,7,4,'.');
  b.R(20,14,3,6);b.R(8,20,34,12);b.R(26,20,7,12,'_');b.R(42,24,8,3);
  b.door('lantd',34,14,1,1,{flag:'light4'});
  b.crack(12,14,{hp:8,key:'cw4',faint:1});
  b.e('pickup',12,16.5,{kind:'shard',key:'shard'});
  b.e('note',10,17,{text:'THE AETHER SHARD. A crystal that remembers starlight. Loose it [K / Right-click] at distant crystals, weak barriers and foes.'});
  b.e('emitter',27,3,{dx:1,dy:0});b.e('mirror',36,3,{o:'/'});b.e('mirror',36,10,{o:'\\'});b.e('mirror',31,6,{o:'/'});b.e('mirror',39,7,{o:'\\'});
  b.e('receiver',30,10,{flag:'light4'});
  b.e('note',28,8,{text:'Bronze mirrors on pivots. Strike one, or press [E] beside it, to turn it. Guide the starlight to the dark receiver.'});
  b.e('pickup',34,16.5,{kind:'lantern',key:'lantern'});
  b.e('chest',33,16,{key:'ch4gold',gold:40});
  b.e('bridge',26,25,{w:7,h:2,flag:'bridge4'});
  b.e('switch',38,21,{group:'br4',flag:'bridge4'});
  b.e('note',21,22,{text:'A collapsed span. Across the void, a crystal mechanism sleeps — beyond the reach of any blade.'});
  b.door('z4exit',42,24,1,3,{flag:'bridge4'});
  b.e('gear',16,8,{rad:80});b.e('gear',33,7,{rad:70});b.e('gear',16,26,{rad:90});
  b.e('husk',12,5);b.e('husk',18,10);b.e('wisp',20,4);b.e('sentinel',22,7);b.e('wisp',33,9);b.e('husk',39,4);
  b.e('husk',12,24);b.e('husk',16,29);b.e('brute',19,26);b.e('sentinel',36,23);b.e('sentinel',38,29);b.e('husk',40,27);
  b.e('jar',8,3);b.e('jar',24,13);b.e('jar',41,12);b.e('jar',9,30);b.e('jar',24,30);b.e('jar',41,21);b.e('jar',15,15);
  b.e('exit',49,24,{h:3,to:4});b.e('exit',1,3,{h:4,to:2});
 }},
{name:'The Twin Spire',sub:'V · The Ascent',w:48,h:40,floor:'.',amb:'spark',echo:.45,dark:.35,
 pal:{f1:'#3c3a3a',f2:'#4a3f30',wt:'#1e1b22',wf:'#8a5a28',accent:'#e0a040'},P:[23,35],Q:[24,5],
 build(b){
  b.R(20,34,8,5);b.R(10,24,28,10);
  b.R(4,11,10,11);b.R(11,22,3,2);b.R(6,6,6,4);b.R(4,13,2,3,'_');b.R(12,17,2,3,'_');
  b.R(34,11,10,11);b.R(34,22,3,2);b.R(36,6,6,4);b.R(35,12,8,5,'_');
  b.R(22,16,4,8);b.R(14,3,20,13);b.R(23,0,2,3);b.R(39,29,4,3);
  b.door('altar2d',8,10,2,1,{flag:'alt2'});
  b.e('altar',8.5,7,{up:2});
  b.e('switch',4.5,14,{group:'alt2',flag:'alt2'});b.e('switch',12.5,18,{group:'alt2',flag:'alt2'});
  b.e('note',7,20,{text:'Two crystals hang above the void, too high to strike. Together they once fed the Solar Altar.'});
  b.door('heart5d',38,10,2,1,{flag:'h5sw'});
  b.e('switch',38.5,14,{group:'h5',flag:'h5sw'});
  b.e('pickup',38.5,7.5,{kind:'heart',key:'heart5'});
  b.door('spiregate',22,23,4,1,{flag:'up2'});
  b.e('note',27,22,{text:'The Spire\'s heart-gate answers only to solar light. Kindle the western altar.'});
  b.door('z5exit',23,2,2,1,{flag:'gauntlet5'});
  b.e('arena',15,4,{w:18,h:11,flag:'gauntlet5',doors:['spiregate','z5exit'],msg:'THE GAUNTLET OF THE SPIRE — hold fast, Warden!',
     waves:[[['husk',17,6],['husk',30,6],['husk',24,12],['sentinel',24,5]],[['brute',18,9],['wisp',28,6],['wisp',28,12],['sentinel',16,13],['husk',31,12]],[['guardian',24,8]]],done:'The Spire Guardian falls. The way to the Core is open.'});
  b.crack(38,30,{hp:8,key:'cw5',hidden:1});
  b.e('chest',41,30,{key:'ch5big',gold:120,big:1});
  b.e('jar',12,26,{special:'seal',key:'jar5seal'});
  b.e('stone',26,35,{sid:2});
  b.e('note',21,37,{text:'The Twin Spire. Its walls are thick with secrets the dark keeps hidden — a lantern of phantoms may reveal what the eye cannot.'});
  b.e('gear',24,28,{rad:110});b.e('gear',9,16,{rad:60});b.e('gear',39,18,{rad:60});b.e('gear',24,9,{rad:120});
  b.e('statue',15,25);b.e('statue',32,25);b.e('statue',15,32);b.e('statue',32,32);
  b.e('husk',14,30);b.e('husk',30,28);b.e('brute',24,29);b.e('sentinel',35,26);b.e('wisp',18,27);
  b.e('wisp',8,15);b.e('sentinel',9,19);b.e('sentinel',36,19);b.e('brute',41,19);
  b.e('jar',11,32);b.e('jar',36,32);b.e('jar',5,21);b.e('jar',43,21);b.e('jar',36,24);b.e('jar',13,11);
  b.e('exit',23,0,{w:2,h:1,to:5});b.e('exit',20,38,{w:8,h:1,to:3});
 }},
{name:'The Core Chamber',sub:'VI · Climax',w:40,h:32,floor:'.',amb:'core',echo:.6,dark:.55,
 pal:{f1:'#2a2834',f2:'#3a2c24',wt:'#120f16',wf:'#5a3a22',accent:'#ffb040'},P:[19.5,28],Q:[19.5,28],
 build(b){
  b.R(18,27,4,4);b.R(4,3,32,23);b.R(19,26,2,1);
  b.R(5,4,4,4,'_');b.R(31,4,4,4,'_');b.R(5,20,4,4,'_');b.R(31,20,4,4,'_');
  b.door('z6d',19,26,2,1);
  b.e('switch',7,6,{group:'core',core:1});b.e('switch',33,6,{group:'core',core:1});b.e('switch',7,22,{group:'core',core:1});b.e('switch',33,22,{group:'core',core:1});
  b.e('beacon',20,4.5);
  b.e('arena',5,4,{w:30,h:20,flag:'engine',doors:['z6d'],msg:'THE CORRODED ENGINE — the Corrosion\'s heart!',waves:[[['engine',20,12]]],done:''});
  b.e('gear',20,13,{rad:150});
  b.e('note',22,28,{text:'The Engine is warded by four conduit crystals. Shatter their light with the Aether Shard, then strike while its shell is bare.'});
  b.e('exit',18,30,{w:4,h:1,to:4});
 }}
];
// extra hazards placed after layouts
const _b4=ZD[4].build;ZD[4].build=b=>{_b4(b);b.R(16,28,3,2,'^');b.R(29,26,3,2,'^');b.R(22,31,4,1,'^');b.R(7,12,2,2,'^');b.R(42,18,2,2,'^');};
const _b5=ZD[5].build;ZD[5].build=b=>{_b5(b);b.R(12,10,3,2,'^');b.R(25,10,3,2,'^');b.R(12,17,2,2,'^');b.R(26,17,2,2,'^');};
const _b2=ZD[2].build;ZD[2].build=b=>{_b2(b);b.R(9,12,2,2,'^');b.R(24,9,2,2,'^');};
const _b1=ZD[1].build;ZD[1].build=b=>{_b1(b);b.R(19,28,2,2,'^');b.R(24,27,1,3,'^');};

// ================= RUNTIME =================
let Z=null,P=null,cam={x:0,y:0},T=0,shake=0,shimmer=0,shimmerCol='255,200,90',lightning=0,paused=true,mode='title';
let banner=null,toasts=[],dialog=null,hitstop=0,redFlash=0;
const keys={},mouse={x:0,y:0,l:0,r:0,usedT:0};
const ENEMY={
 husk:{hp:4,spd:62,r:12,dmg:1,gold:[5,10]},
 wisp:{hp:3,spd:70,r:11,dmg:1,gold:[6,11],fly:1},
 sentinel:{hp:9,spd:42,r:14,dmg:2,gold:[9,15]},
 brute:{hp:15,spd:48,r:18,dmg:2,gold:[12,15]},
 curator:{hp:34,spd:55,r:24,dmg:2,gold:[40,40],boss:1,fly:1},
 guardian:{hp:52,spd:80,r:22,dmg:2,gold:[60,60],boss:1},
 engine:{hp:90,spd:0,r:40,dmg:2,gold:[0,0],boss:1}
};
const WEAP=[
 {name:'Ember Blade',dmg:2,cd:.40,arc:1.75,range:46,wall:1,dur:.16,col:'220,225,235',type:'physical'},
 {name:'Ignited Edge',dmg:3,cd:.28,arc:1.95,range:50,wall:2,dur:.14,col:'255,140,40',type:'fire'},
 {name:'Solar Falchion',dmg:6,cd:.50,arc:3.1,range:68,wall:4,dur:.22,wind:.16,col:'255,215,100',type:'solar'}
];
function tile(tx,ty){if(tx<0||ty<0||tx>=Z.w||ty>=Z.h)return'#';return Z.g[ty][tx];}
function blockObj(tx,ty){const o=Z.bmap[ty*Z.w+tx];return o&&o.blocking();}
function bridgeOn(tx,ty){for(const b of Z.bridges)if(fl(b.flag)&&tx>=b.tx&&tx<b.tx+b.w&&ty>=b.ty&&ty<b.ty+b.h)return true;return false;}
function solidT(tx,ty,fly){const t=tile(tx,ty);if(t==='#')return true;if(t==='_'&&!fly&&!bridgeOn(tx,ty))return true;return blockObj(tx,ty);}
function projSolid(x,y){const tx=Math.floor(x/TS),ty=Math.floor(y/TS);return tile(tx,ty)==='#'||blockObj(tx,ty);}
function boxFree(x,y,r,fly){
 const x0=Math.floor((x-r)/TS),x1=Math.floor((x+r)/TS),y0=Math.floor((y-r)/TS),y1=Math.floor((y+r)/TS);
 for(let ty=y0;ty<=y1;ty++)for(let tx=x0;tx<=x1;tx++)if(solidT(tx,ty,fly))return false;return true;
}
function moveBody(o,dx,dy,fly){
 let hitW=false;
 const sx=Math.ceil(Math.abs(dx)/8)||1,sy=Math.ceil(Math.abs(dy)/8)||1;
 for(let i=0;i<sx;i++){const nx=o.x+dx/sx;if(boxFree(nx,o.y,o.r,fly))o.x=nx;else{hitW=true;break;}}
 for(let i=0;i<sy;i++){const ny=o.y+dy/sy;if(boxFree(o.x,ny,o.r,fly))o.y=ny;else{hitW=true;break;}}
 for(const s of Z.solids){if(s.dead)continue;const ddx=o.x-s.x,ddy=o.y-s.y,d=Math.hypot(ddx,ddy),m=o.r+s.r;
  if(d<m&&d>0.01){const px=o.x+ddx/d*(m-d),py=o.y+ddy/d*(m-d);if(boxFree(px,py,o.r,fly)){o.x=px;o.y=py;}}}
 return hitW;
}
function los(a,b){const d=dist(a,b),n=Math.ceil(d/12);for(let i=1;i<n;i++){const x=a.x+(b.x-a.x)*i/n,y=a.y+(b.y-a.y)*i/n;if(projSolid(x,y))return false;}return true;}

// ---- particles ----
function part(x,y,o){if(Z.parts.length>900)return;Z.parts.push(Object.assign({x,y,vx:0,vy:0,life:1,ml:1,size:2,col:'255,180,80',g:0,drag:1,glow:0,shape:'c'},o,{ml:o.life||1}));}
function burst(x,y,n,col,spd,o={}){for(let i=0;i<n;i++){const a=Math.random()*6.283,s=rnd(spd*.3,spd);part(x,y,Object.assign({vx:Math.cos(a)*s,vy:Math.sin(a)*s,life:rnd(.3,.7),size:rnd(1.5,3),col,drag:.9,glow:1},o));}}
function sparks(x,y,col,n=10){for(let i=0;i<n;i++){const a=Math.random()*6.283,s=rnd(80,260);part(x,y,{vx:Math.cos(a)*s,vy:Math.sin(a)*s,life:rnd(.2,.45),size:rnd(1,2.2),col,drag:.88,glow:1,shape:'l'});}}
function goldDust(x,y){for(let i=0;i<14;i++){const a=i/14*6.283;part(x,y,{vx:Math.cos(a)*50,vy:Math.sin(a)*50-20,life:.7,size:2,col:'255,210,90',drag:.92,glow:1,swirl:4});}}
function motes(x,y,col='255,255,255',n=22){for(let i=0;i<n;i++)part(x+rnd(-14,14),y+rnd(-8,10),{vx:rnd(-12,12),vy:rnd(-70,-25),life:rnd(.8,1.6),size:rnd(1.5,3),col,glow:1,drag:.98});}
function ftext(x,y,txt,col='#fff',sz=16){Z.texts.push({x,y,txt,col,sz,life:.9});}
function toast(t,col){toasts.push({t,col:col||'#f0dcae',life:4});if(toasts.length>4)toasts.shift();}
function showBanner(a,b){banner={a,b,life:3.5};}
function doShake(v){shake=Math.max(shake,v);}
function doShimmer(d,c){shimmer=Math.max(shimmer,d);shimmerCol=c||'255,200,90';}

// ---- objects ----
function mkObj(e){
 const o=Object.assign({},e);o.x=e.tx*TS+16;o.y=e.ty*TS+16;o.blocking=()=>false;o.phase=Math.random()*6;
 switch(e.t){
  case 'door':o.open=0;o.blocking=()=>o.open<.5;o.isOpen=()=>{
     if(Z.arena&&Z.arena.doors.includes(o.id))return false;
     if(o.timed)return Z.timers[o.id]>0;
     return !o.flag||fl(o.flag);};
    Z.bmap[e.ty*Z.w+e.tx]=o;o.open=o.isOpen()?1:0;break;
  case 'crack':o.hpMax=o.hp;if(fl(o.key))o.broken=1;o.blocking=()=>!o.broken;Z.bmap[e.ty*Z.w+e.tx]=o;break;
  case 'jar':o.r=10;if(o.key&&fl(o.key))return null;Z.solids.push(o);break;
  case 'chest':o.r=13;o.opened=fl(o.key);Z.solids.push(o);break;
  case 'stone':o.r=14;Z.solids.push(o);break;
  case 'statue':o.r=13;Z.solids.push(o);break;
  case 'telescope':o.r=16;Z.solids.push(o);break;
  case 'merchant':o.r=18;Z.solids.push(o);break;
  case 'shelf':o.r=14;Z.solids.push(o);break;
  case 'altar':o.r=16;Z.solids.push(o);break;
  case 'beacon':o.r=26;Z.solids.push(o);break;
  case 'mirror':o.r=11;Z.solids.push(o);break;
  case 'emitter':case 'receiver':o.r=12;Z.solids.push(o);break;
  case 'pickup':if(fl(o.key))return null;o.r=10;break;
  case 'switch':o.lit=!o.core&&o.flag&&fl(o.flag);o.litT=0;break;
  case 'bridge':Z.bridges.push(o);break;
  case 'plate':o.pressed=0;o.last=-99;break;
  case 'exit':o.w=o.w||1;o.h=o.h||1;break;
  case 'arena':o.rx=o.tx*TS;o.ry=o.ty*TS;o.rw=o.w*TS;o.rh=o.h*TS;break;
 }
 return o;
}
function spawnEnemy(type,tx,ty,arena,px){
 const d=ENEMY[type],sc=1+st.zone*.12;
 const e={type,x:px?tx:tx*TS+16,y:px?ty:ty*TS+16,r:d.r,hp:Math.round(d.hp*(d.boss?1:sc)),spd:d.spd,dmg:d.dmg,fly:d.fly,boss:d.boss,
  state:'idle',t:rnd(0,1),cd:rnd(.5,1.5),fa:Math.PI/2,hurt:0,burn:0,burnTick:0,frozen:0,kx:0,ky:0,arena,ph:Math.random()*6,aggro:0,stun:0};
 e.mhp=e.hp;
 if(type==='engine'){e.shield=1;e.shieldT=0;e.summonT=8;e.poolT=4;e.spiral=0;}
 Z.enemies.push(e);
 if(arena){for(let i=0;i<18;i++)part(e.x+rnd(-14,14),e.y+rnd(-14,14),{vy:-rnd(30,90),life:rnd(.4,.9),size:3,col:'60,30,70',glow:0});burst(e.x,e.y,12,'255,120,60',120);}
 return e;
}
function loadZone(i,from){
 st.zone=i;const D=ZD[i];const b=Bld(D.w,D.h,D.floor);D.build(b);
 Z={D,idx:i,w:D.w,h:D.h,g:b.g,objs:[],enemies:[],projs:[],eproj:[],parts:[],texts:[],coins:[],solids:[],bmap:{},bridges:[],timers:{},pools:[],arena:null,beam:[],arenaWave:0};
 for(const e of b.ents){
   if(['husk','wisp','sentinel','brute'].includes(e.t)){spawnEnemy(e.t,e.tx,e.ty);continue;}
   const o=mkObj(e);if(o)Z.objs.push(o);
 }
 if(i===4&&fl('gauntlet5')&&!fl('cloak'))Z.objs.push(mkObj({t:'pickup',tx:24,ty:8,kind:'cloak',key:'cloak'}));
 if(i===5&&fl('engine')){Z.engineDead=1;}
 const seen={};for(const o of Z.objs)if(o.t==='door'&&!seen[o.id]){seen[o.id]=1;o.lead=1;}
 Z.tiles=renderTiles();
 let sp=from==='back'?D.Q:D.P;
 if(from&&from.stone){sp=[from.x,from.y];}
 P={x:sp[0]*TS+16,y:sp[1]*TS+16,r:10,fa:0,face:1,cd:0,swingT:0,swingDur:.15,swingA:0,windT:0,inv:1,flash:0,stepT:0,walk:0,moving:0,kx:0,ky:0,shardCd:0,trapCd:0,ammoT:0};
 if(i===4&&from!=='back'&&!from)P.fa=-Math.PI/2;
 cam.x=P.x-VW/2;cam.y=P.y-VH/2;clampCam();
 showBanner(D.name,D.sub);
 if(AU.ctx){AU.wind.gain.value=i===3?.08:i===5?.03:.05;}
 if(i===5&&!fl('engine'))setTimeout(()=>{if(st.zone===5)SFX.thunder();},400);
}
function clampCam(){
 const mw=Z.w*TS,mh=Z.h*TS;
 cam.x=mw<VW?(mw-VW)/2:clamp(cam.x,0,mw-VW);cam.y=mh<VH?(mh-VH)/2:clamp(cam.y,0,mh-VH);
}
// ---- static tile prerender ----
function renderTiles(){
 const c=document.createElement('canvas');c.width=Z.w*TS;c.height=Z.h*TS;const g=c.getContext('2d');const pal=Z.D.pal;
 for(let y=0;y<Z.h;y++)for(let x=0;x<Z.w;x++){
  const t=Z.g[y][x],X=x*TS,Y=y*TS,h=hash(x,y,Z.idx);
  if(t==='#'){
   const below=tile(x,y+1);
   g.fillStyle=pal.wt;g.fillRect(X,Y,TS,TS);
   g.fillStyle='rgba(255,255,255,'+(h*.03)+')';g.fillRect(X,Y,TS,TS);
   if(below!=='#'){
     const gr=g.createLinearGradient(0,Y,0,Y+TS);gr.addColorStop(0,pal.wf);gr.addColorStop(1,'#1a120a');g.fillStyle=gr;g.fillRect(X,Y+6,TS,TS-6);
     g.fillStyle='rgba(0,0,0,.35)';for(let i=0;i<3;i++)g.fillRect(X,Y+12+i*7,TS,1);
     g.fillStyle='rgba(0,0,0,.3)';g.fillRect(X+((x%2)?10:22),Y+6,1,TS-6);
     g.fillStyle=pal.accent;g.globalAlpha=.75;g.fillRect(X,Y+5,TS,2);g.globalAlpha=1;
     if(h<.22){g.fillStyle='#14100c';g.beginPath();g.ellipse(X+16,Y+20,6+h*20,4,0,0,7);g.fill();g.fillStyle='rgba(30,25,35,.8)';g.fillRect(X+8,Y+20,2,12);}
     if(h>.86){g.fillStyle='rgba(70,110,50,.6)';for(let i=0;i<6;i++)g.fillRect(X+hash(x,i,3)*30,Y+6+hash(i,y,4)*8,3,4+hash(x,y,i)*8);}
     if(h>.45&&h<.5){g.fillStyle=pal.accent;g.beginPath();g.arc(X+16,Y+19,5,0,7);g.fill();g.fillStyle='#1a120a';g.beginPath();g.arc(X+16,Y+19,2.5,0,7);g.fill();}
   } else {
     g.fillStyle='rgba(120,90,50,.08)';g.fillRect(X,Y,TS,1);
   }
  } else if(t==='_'){
   const gr=g.createLinearGradient(0,Y,0,Y+TS);gr.addColorStop(0,'#05070c');gr.addColorStop(1,'#0a1020');g.fillStyle=gr;g.fillRect(X,Y,TS,TS);
   if(tile(x,y-1)!=='_'&&tile(x,y-1)!=='#'){g.fillStyle='#2a2018';g.fillRect(X,Y,TS,8);g.fillStyle='rgba(0,0,0,.6)';g.fillRect(X,Y+8,TS,4);}
   if(h>.8){g.fillStyle='rgba(180,200,255,.5)';g.fillRect(X+h*30,Y+hash(y,x)*30,1,1);}
  } else {
   const alt=(x+y)%2;
   g.fillStyle=alt?pal.f1:pal.f2;g.fillRect(X,Y,TS,TS);
   if(t===','){ // grate
     g.fillStyle='rgba(0,0,0,.35)';for(let i=3;i<TS;i+=6)g.fillRect(X+i,Y+2,2,TS-4);
     g.fillStyle='rgba(150,80,30,.25)';g.fillRect(X,Y,TS,2);g.fillRect(X,Y,2,TS);
     if(h>.7){g.fillStyle='rgba(140,60,20,.35)';g.beginPath();g.arc(X+h*32,Y+hash(y,x)*32,6,0,7);g.fill();}
   } else if(t==='~'){
     g.fillStyle='rgba(60,120,150,.12)';g.fillRect(X,Y,TS,TS);
     if(h>.55){g.fillStyle='rgba(90,170,210,.18)';g.beginPath();g.ellipse(X+16,Y+16,10+h*6,5,0,0,7);g.fill();}
     g.strokeStyle='rgba(0,0,0,.25)';g.strokeRect(X+.5,Y+.5,TS-1,TS-1);
   } else {
     g.strokeStyle='rgba(0,0,0,.28)';g.strokeRect(X+.5,Y+.5,TS-1,TS-1);
     if(h>.82){g.strokeStyle='rgba(0,0,0,.35)';g.beginPath();g.moveTo(X+h*30,Y+2);g.lineTo(X+16,Y+16);g.lineTo(X+hash(y,x)*30,Y+30);g.stroke();}
     if(h<.07){g.fillStyle='rgba(200,150,60,.25)';g.beginPath();g.arc(X+16,Y+16,9,0,7);g.fill();g.strokeStyle='rgba(220,170,80,.35)';g.beginPath();g.arc(X+16,Y+16,6,0,7);g.stroke();}
     if(h>.93){g.fillStyle='rgba(70,110,50,.35)';g.beginPath();g.arc(X+8+h*10,Y+20,7,0,7);g.fill();}
   }
   if(t==='^'){
     g.fillStyle='#0d0a10';g.beginPath();g.ellipse(X+16,Y+16,15,13,0,0,7);g.fill();
     g.fillStyle='#2a1c24';for(let i=0;i<5;i++){const sx=X+5+hash(x,y,i+9)*22,sy=Y+6+hash(y,x,i+2)*20;g.beginPath();g.moveTo(sx-4,sy+5);g.lineTo(sx,sy-6);g.lineTo(sx+4,sy+5);g.fill();}
   }
   // shadow under walls
   if(tile(x,y-1)==='#'){const gr=g.createLinearGradient(0,Y,0,Y+12);gr.addColorStop(0,'rgba(0,0,0,.5)');gr.addColorStop(1,'rgba(0,0,0,0)');g.fillStyle=gr;g.fillRect(X,Y,TS,12);}
   if(tile(x-1,y)==='#'){g.fillStyle='rgba(0,0,0,.25)';g.fillRect(X,Y,5,TS);}
  }
 }
 // corrosion creep near walls
 for(let y=0;y<Z.h;y++)for(let x=0;x<Z.w;x++){if(Z.g[y][x]==='#'&&tile(x,y+1)!=='#'&&tile(x,y+1)!=='_'&&hash(x,y,77)<.12+Z.idx*.03){g.fillStyle='rgba(15,10,20,.55)';g.beginPath();g.arc(x*TS+16,y*TS+16,22+hash(x,y,5)*16,0,7);g.fill();}}
 return c;
}

// ================= INPUT =================
addEventListener('keydown',e=>{
 const k=e.key.toLowerCase();keys[k]=true;
 if(['arrowup','arrowdown','arrowleft','arrowright',' ','tab'].includes(k))e.preventDefault();
 aInit();
 if(k==='m'){AU.muted=!AU.muted;if(AU.master)AU.master.gain.value=AU.muted?0:.55;toast(AU.muted?'Sound muted':'Sound on');}
 if(mode==='play'){
  if(k==='e')interact();
  else if(k==='1')useStem();
  else if(k==='2')useSeal();
  else if(k==='i'||k==='tab')openInv();
  else if(k==='escape')openInv();
  else if((k==='k'||k==='l')&&!e.repeat)fireShard();
  else if(dialog&&(k==='enter'))dialog=null;
 } else if(mode==='inv'){
  if(k==='i'||k==='tab'||k==='escape')closeOv();
  else if(k==='q')toggleAcc('band');else if(k==='w')toggleAcc('lantern');else if(k==='r')toggleAcc('cloak');
  else if(k==='1'){useStem();renderInv();}else if(k==='2'){useSeal();renderInv();}
 } else if(mode==='shop'){
  if(k==='e'||k==='escape')closeOv();else if('1234'.includes(k))buy(+k-1);
 } else if(mode==='menu'){
  if(k==='escape')closeOv();else if(/[1-9]/.test(k)){const b=document.querySelectorAll('#shopList .sbtn')[+k-1];if(b&&!b.disabled)b.click();}
 }
});
addEventListener('keyup',e=>{keys[e.key.toLowerCase()]=false;});
cv.addEventListener('mousemove',e=>{const r=cv.getBoundingClientRect();mouse.x=(e.clientX-r.left)*VW/r.width;mouse.y=(e.clientY-r.top)*VH/r.height;});
cv.addEventListener('mousedown',e=>{aInit();if(mode!=='play')return;const r=cv.getBoundingClientRect();mouse.x=(e.clientX-r.left)*VW/r.width;mouse.y=(e.clientY-r.top)*VH/r.height;
 const a=Math.atan2(mouse.y+cam.y-P.y,mouse.x+cam.x-P.x);
 if(e.button===2){P.fa=a;fireShard(true);}else{mouse.l=1;P.fa=a;mouse.usedT=1.2;}});
addEventListener('mouseup',e=>{if(e.button===0)mouse.l=0;});
cv.addEventListener('contextmenu',e=>e.preventDefault());

// ================= PLAYER ACTIONS =================
const W=()=>WEAP[st.weapon];
function maxHp(){return st.maxC*2;}
function startSwing(){
 const w=W();if(P.cd>0||P.windT>0)return;
 let cd=w.cd;if(st.eq.band)cd*=.8;
 P.cd=cd+(w.wind||0);
 if(w.wind){P.windT=w.wind;SFX.windup();}else doSwing();
}
function doSwing(){
 const w=W();P.swingT=w.dur;P.swingDur=w.dur;P.swingA=P.fa;P.swingSide=-(P.swingSide||1);SFX.swing(st.weapon);
 const hx=P.x,hy=P.y;let hitAny=false;
 if(st.weapon>=1)for(let i=0;i<12;i++){const a=P.fa+rnd(-w.arc/2,w.arc/2),r=rnd(20,w.range);part(hx+Math.cos(a)*r,hy+Math.sin(a)*r,{vx:Math.cos(a)*40,vy:Math.sin(a)*40-20,life:rnd(.25,.5),size:rnd(1.5,3),col:st.weapon===2?'255,220,120':'255,130,40',glow:1,drag:.9});}
 for(const e of Z.enemies){if(e.dead)continue;const d=Math.hypot(e.x-hx,e.y-hy);if(d>w.range+e.r)continue;
  const a=Math.atan2(e.y-hy,e.x-hx);if(Math.abs(angDiff(a,P.fa))>w.arc/2&&d>e.r+8)continue;
  hitEnemy(e,w.dmg,a,'melee');hitAny=true;}
 for(const o of Z.objs){
  if(o.t==='crack'&&!o.broken){if(o.hidden&&!st.eq.lantern)continue;const d=Math.hypot(o.x-hx,o.y-hy);if(d<w.range+18&&Math.abs(angDiff(Math.atan2(o.y-hy,o.x-hx),P.fa))<w.arc/2+.3){damageCrack(o,w.wall);hitAny=true;}}
  else if(o.t==='jar'&&!o.dead){const d=Math.hypot(o.x-hx,o.y-hy);if(d<w.range+10&&Math.abs(angDiff(Math.atan2(o.y-hy,o.x-hx),P.fa))<w.arc/2+.4){breakJar(o);hitAny=true;}}
  else if(o.t==='mirror'){const d=Math.hypot(o.x-hx,o.y-hy);if(d<w.range+10&&Math.abs(angDiff(Math.atan2(o.y-hy,o.x-hx),P.fa))<w.arc/2){rotMirror(o);hitAny=true;}}
 }
 if(st.weapon===2){doShimmer(.35,'255,215,110');doShake(3);}
 if(hitAny)hitstop=st.weapon===2?.07:.04;
}
function hitEnemy(e,dmg,a,src){
 if(e.type==='engine'&&e.shield){sparks(e.x+Math.cos(a+Math.PI)*e.r,e.y+Math.sin(a+Math.PI)*e.r,'110,170,255',12);SFX.block();ftext(e.x,e.y-e.r-10,'WARDED','#8ab4ff',13);return;}
 let col='255,150,50',crit=false,blocked=false;
 if(e.type==='sentinel'&&src==='melee'&&st.weapon===0){const front=Math.abs(angDiff(e.fa,a+Math.PI))<1.0;if(front){dmg=1;blocked=true;}}
 if(!blocked&&Math.random()<.1){dmg*=2;crit=true;}
 e.hp-=dmg;e.hurt=.18;
 const kb=src==='shard'?120:st.weapon===2?340:220;
 if(!e.boss){e.kx=Math.cos(a)*kb;e.ky=Math.sin(a)*kb;}else{e.kx=Math.cos(a)*kb*.15;e.ky=Math.sin(a)*kb*.15;}
 if(blocked||crit){sparks(e.x,e.y,'120,180,255',14);SFX.block();}
 if(src==='shard'){sparks(e.x,e.y,'150,220,255',10);SFX.chime();}
 else if(st.weapon===0){sparks(e.x,e.y,col,10);SFX.hit(0);}
 else if(st.weapon===1){sparks(e.x,e.y,'255,120,30',12);for(let i=0;i<6;i++)part(e.x+rnd(-8,8),e.y+rnd(-8,8),{vy:-rnd(20,50),vx:rnd(-10,10),life:rnd(.5,1),size:rnd(3,6),col:'70,60,60',glow:0,drag:.97});e.burn=.6;e.stun=Math.max(e.stun,.22);SFX.hit(1);}
 else {sparks(e.x,e.y,'255,230,140',16);burst(e.x,e.y,10,'255,240,180',160);e.stun=Math.max(e.stun,e.boss?.1:.35);SFX.hit(2);doShake(6);}
 ftext(e.x+rnd(-6,6),e.y-e.r-6,(crit?'CRIT ':'')+dmg,crit?'#9cc8ff':blocked?'#9cb0d0':st.weapon===1?'#ff9a40':st.weapon===2?'#ffe08a':'#fff',crit?18:15);
 if(e.state==='idle')e.aggro=1;
 if(e.hp<=0)killEnemy(e);
}
function killEnemy(e){
 e.dead=1;st.kills++;SFX.edie();burst(e.x,e.y,26,'255,140,60',220);
 for(let i=0;i<16;i++)part(e.x+rnd(-10,10),e.y+rnd(-10,10),{vx:rnd(-60,60),vy:rnd(-80,10),life:rnd(.5,1.1),size:rnd(2,4),col:'40,32,40',glow:0,drag:.95,g:80});
 const d=ENEMY[e.type];let g=ri(d.gold[0],d.gold[1]);
 dropCoins(e.x,e.y,g);
 if(st.shard&&Math.random()<.25)Z.coins.push({x:e.x,y:e.y,vx:rnd(-60,60),vy:rnd(-60,60),ammo:1,t:0});
 if(e.type==='curator'){Z.objs.push(mkObjPx({t:'pickup',kind:'stem',key:'curstem'},e.x,e.y));}
 if(e.type==='guardian'){Z.objs.push(mkObjPx({t:'pickup',kind:'cloak',key:'cloak'},e.x,e.y));}
 if(e.type==='engine')engineDefeated(e);
 if(e.boss){doShake(14);doShimmer(1,'255,180,90');}
}
function mkObjPx(e,x,y){const o=mkObj(Object.assign({tx:0,ty:0},e));o.x=x;o.y=y;return o;}
function dropCoins(x,y,g){while(g>0){const v=g>=10?5:g>=3?3:1;g-=v;Z.coins.push({x,y,vx:rnd(-90,90),vy:rnd(-90,90),v,t:0});}}
function damageCrack(o,dmg){
 o.hp-=dmg;o.shk=.2;SFX.crack();doShake(2);
 for(let i=0;i<6;i++)part(o.x+rnd(-12,12),o.y+rnd(-12,12),{vx:rnd(-60,60),vy:rnd(-60,20),life:.5,size:rnd(2,3.5),col:'120,100,80',glow:0,g:200});
 if(o.hp<=0){o.broken=1;setF(o.key);SFX.brk();doShake(8);burst(o.x,o.y,30,'150,120,90',200,{glow:0,g:160});toast('The cracked wall crumbles — a hidden passage!','#ffd38a');}
}
function breakJar(o){
 o.dead=1;Z.solids=Z.solids.filter(s=>s!==o);SFX.jar();burst(o.x,o.y,14,'170,120,70',140,{glow:0,g:200});
 if(o.special==='seal'){setF(o.key);Z.objs.push(mkObjPx({t:'pickup',kind:'seal',key:'jarseal'},o.x,o.y));return;}
 const r=Math.random();if(r<.65)dropCoins(o.x,o.y,ri(2,6));
 else if(st.shard&&r<.85)Z.coins.push({x:o.x,y:o.y,vx:0,vy:0,ammo:1,t:0});
}
function rotMirror(o){if(o.rotCd>0)return;o.rotCd=.3;o.o=o.o==='/'?'\\':'/';SFX.tick();tone(600,.12,{type:'triangle',vol:.08});sparks(o.x,o.y,'255,210,120',5);}
function fireShard(fromMouse){
 if(!st.shard){if(fromMouse!==true)toast('You carry no ranged focus... yet.','#8fa3c8');return;}
 if(P.shardCd>0)return;
 if(st.ammo<=0){SFX.denied();toast('The Aether Shard is drained. It recharges slowly — or restock at the Merchant.','#8ab4ff');P.shardCd=.4;return;}
 st.ammo--;P.shardCd=.38;SFX.shard();
 const a=P.fa;Z.projs.push({x:P.x+Math.cos(a)*14,y:P.y+Math.sin(a)*14-6,vx:Math.cos(a)*520,vy:Math.sin(a)*520,life:1.1,a});
 burst(P.x+Math.cos(a)*16,P.y+Math.sin(a)*16-6,8,'150,220,255',90);
}
function hurtPlayer(dmg,srcx,srcy,trap){
 if(P.inv>0||mode!=='play')return;
 if(trap&&st.eq.cloak){if(P.trapCd<=0){ftext(P.x,P.y-24,'Cloak absorbs','#b49ae0',12);P.trapCd=1;tone(300,.2,{type:'triangle',vol:.06});}return;}
 st.hp=Math.max(0,st.hp-dmg);P.inv=1;P.flash=.35;redFlash=.4;SFX.hurt();doShake(7);
 burst(P.x,P.y-8,22,'255,50,40',200);ftext(P.x,P.y-28,'-'+dmg,'#ff5a4a',18);
 if(srcx!=null){const a=Math.atan2(P.y-srcy,P.x-srcx);P.kx=Math.cos(a)*260;P.ky=Math.sin(a)*260;}
 heartShake=.5;
 if(st.hp<=0)die();
}
let heartShake=0;
function useStem(){
 if(st.stems<=0){toast('No Vital Stems in your satchel.','#8fa3c8');SFX.denied();return;}
 if(st.hp>=maxHp()){toast('Your vitality is already full.','#8fa3c8');return;}
 st.stems--;st.hp=Math.min(maxHp(),st.hp+4);SFX.heal();motes(P.x,P.y,'120,255,140',28);ftext(P.x,P.y-28,'+4','#7dff9a',18);
}
function useSeal(){
 if(st.seals<=0){toast('No Binding Seals in your satchel.','#8fa3c8');SFX.denied();return;}
 let best=null,bd=320;for(const e of Z.enemies){if(e.dead||e.frozen>0)continue;const d=dist(e,P);if(d<bd){bd=d;best=e;}}
 if(!best){toast('No construct near enough to bind.','#8fa3c8');return;}
 st.seals--;best.frozen=best.boss?2:3;SFX.seal();burst(best.x,best.y,30,'120,190,255',180);
 for(let i=0;i<12;i++){const a=i/12*6.28;part(best.x+Math.cos(a)*30,best.y+Math.sin(a)*30,{vx:-Math.cos(a)*40,vy:-Math.sin(a)*40,life:.8,size:3,col:'140,200,255',glow:1});}
 ftext(best.x,best.y-best.r-12,'BOUND','#9cd0ff',15);
}
function die(){
 mode='dead';paused=true;SFX.roar();
 setTimeout(()=>{$('dead').classList.add('show');},900);
}
function revive(){
 $('dead').classList.remove('show');
 const s=getSave();
 if(s){st=JSON.parse(s);st.hp=maxHp();loadZone(st.stone.z,{stone:1,x:st.stone.x,y:st.stone.y+1.3});toast('You reawaken at the Sanctuary Stone. Progress since your last rest is lost.','#d0a070');}
 else{st=newState();loadZone(0);}
 mode='play';paused=false;P.inv=2;
}
// ================= INTERACTION & ITEMS =================
const ITEM={
 band:{name:"Warden's Band",type:'acc',desc:'Silver ring on the wrist. Strikes come 20% faster.'},
 lantern:{name:'Phantom Lantern',type:'acc',desc:'Blue phantom-light. Widens your sight in the dark and reveals hidden cracks.'},
 cloak:{name:'Shadow Cloak',type:'acc',desc:'Dark-trimmed mantle. Corrosion spikes and pools cannot harm you.'},
 stem:{name:'Vital Stem',type:'con',desc:'Restores 2 heart containers (4 HP). [1]'},
 seal:{name:'Binding Seal',type:'con',desc:'Freezes the nearest construct for ~3 seconds. [2]'}
};
function gainItem(kind,x,y){
 if(kind==='band'||kind==='lantern'||kind==='cloak'){
  st.acc[kind]=1;st.eq[kind]=1;SFX.rare();motes(x,y,'255,255,255',30);doShimmer(1,'220,230,255');
  toast('Accessory found: '+ITEM[kind].name+' — equipped. ('+ITEM[kind].desc+')','#e8ecff');
  showBanner(ITEM[kind].name,ITEM[kind].desc);
 } else if(kind==='stem'){st.stems++;SFX.item();motes(x,y,'140,255,150');toast('Vital Stem acquired. Press [1] to use.','#9cffb0');}
 else if(kind==='seal'){st.seals++;SFX.item();motes(x,y,'140,200,255');toast('Binding Seal acquired. Press [2] near a foe.','#9cd0ff');}
 else if(kind==='heart'){
  st.maxC=Math.min(5,st.maxC+1);st.hp=maxHp();SFX.rare();setTimeout(()=>SFX.heal(),300);doShimmer(2,'255,120,120');P.aura=2;
  motes(x,y,'255,150,150',36);showBanner('Heart Container','Your vitality swells — '+st.maxC+' containers ('+maxHp()+' HP).');
 } else if(kind==='shard'){
  st.shard=true;st.ammo=12;SFX.rare();setTimeout(()=>SFX.shard(),250);doShimmer(2,'140,210,255');motes(x,y,'150,220,255',40);
  showBanner('The Aether Shard','Press [K] or Right-click to loose a bolt of starlight.');
 }
}
function nearInteract(){
 let best=null,bd=50;
 for(const o of Z.objs){
  if(!['stone','merchant','chest','note','altar','mirror','beacon'].includes(o.t))continue;
  if(o.t==='chest'&&o.opened)continue;if(o.t==='altar'&&(st.weapon>=o.up))continue;
  if(o.t==='beacon'&&!Z.engineDead)continue;
  const d=dist(o,P);if(d<bd+(o.r||0)){bd=d;best=o;}
 }
 return best;
}
function interact(){
 if(dialog){dialog=null;return;}
 const o=nearInteract();if(!o)return;
 if(o.t==='stone'){
  const first=!fl('stone'+o.sid);setF('stone'+o.sid);
  st.hp=maxHp();if(st.shard)st.ammo=Math.max(st.ammo,12);
  saveGame(st.zone,o.tx,o.ty);SFX.stone();motes(o.x,o.y,'255,220,120',40);P.aura=1.2;doShimmer(1,'255,210,120');
  toast((first?'Sanctuary Stone awakened. ':'')+'You rest. Wounds mended — progress bound to the stone.','#ffe3a6');
  o.pulse=1.5;
 } else if(o.t==='merchant'){openShop();}
 else if(o.t==='chest'){
  o.opened=1;setF(o.key);SFX.door();setTimeout(()=>SFX.shimmer(),150);
  if(o.gold){dropCoins(o.x,o.y+10,o.gold);if(o.big){toast('A great Sigil Coffer! '+o.gold+' gold spills forth.','#ffd36a');doShimmer(1);}}
  if(o.item)setTimeout(()=>gainItem(o.item,o.x,o.y),200);
  burst(o.x,o.y,18,'255,220,120',140);
 } else if(o.t==='note'){dialog={text:o.text,o};}
 else if(o.t==='mirror'){rotMirror(o);}
 else if(o.t==='altar'){
  if(st.weapon<o.up){st.weapon=o.up;SFX.rare();doShimmer(2.2,o.up===1?'255,130,40':'255,220,110');doShake(10);P.aura=2;
   if(o.up===1){SFX.hit(1);burst(o.x,o.y,60,'255,120,30',260);showBanner('Ignited Edge','Your blade drinks the altar\'s ember. Fire damage, faster strikes, foes flinch from the burn.');}
   else{SFX.hit(2);burst(o.x,o.y,70,'255,230,140',300);showBanner('Solar Falchion','Sunlight forged into steel. Heavy wind-up, vast sweeping arc, devastating impact. Sentinel shields can no longer stop you.');}
   setF('up'+o.up);
  }
 } else if(o.t==='beacon'){finishGame();}
}
function pickupCheck(){
 for(const o of Z.objs){
  if(o.t!=='pickup'||o.got)continue;
  if(dist(o,P)<24){o.got=1;setF(o.key);
   if(o.key==='heart5')st.shop.heart=1;
   gainItem(o.kind,o.x,o.y);}
 }
 Z.objs=Z.objs.filter(o=>!(o.t==='pickup'&&o.got));
}
// ================= OVERLAYS =================
function icon(kind,s=36){
 const c=document.createElement('canvas');c.width=c.height=s;const g=c.getContext('2d');g.translate(s/2,s/2);g.scale(s/36,s/36);drawIcon(g,kind,0,0,1);return c.toDataURL();
}
function drawIcon(g,k,x,y,a){
 g.save();g.translate(x,y);g.globalAlpha=a;
 if(k==='band'){g.shadowColor='#dfe8ff';g.shadowBlur=10;g.strokeStyle='#e6ecf5';g.lineWidth=4;g.beginPath();g.ellipse(0,2,11,8,0,0,7);g.stroke();g.fillStyle='#9fd4ff';g.beginPath();g.arc(0,-6,4,0,7);g.fill();}
 else if(k==='stem'){g.shadowColor='#6f6';g.shadowBlur=10;g.strokeStyle='#4ec26a';g.lineWidth=3;g.beginPath();g.moveTo(0,14);g.quadraticCurveTo(-3,0,2,-12);g.stroke();g.fillStyle='#7dff9a';g.beginPath();g.ellipse(-6,-2,7,3.5,-.6,0,7);g.fill();g.beginPath();g.ellipse(6,-7,7,3.5,.6,0,7);g.fill();g.fillStyle='#d8ffe0';g.beginPath();g.arc(2,-13,3,0,7);g.fill();}
 else if(k==='lantern'){g.shadowColor='#6bf';g.shadowBlur=14;g.fillStyle='#4a3a20';g.fillRect(-8,-12,16,4);g.fillRect(-8,10,16,4);g.fillStyle='rgba(120,190,255,.85)';g.fillRect(-6,-8,12,18);g.fillStyle='#e6f4ff';g.beginPath();g.arc(0,1,3.5,0,7);g.fill();g.strokeStyle='#c9973f';g.lineWidth=2;g.beginPath();g.arc(0,-14,5,Math.PI,0);g.stroke();}
 else if(k==='cloak'){g.fillStyle='#2a1f3a';g.beginPath();g.moveTo(0,-13);g.lineTo(12,12);g.lineTo(-12,12);g.closePath();g.fill();g.strokeStyle='#8a6ad0';g.lineWidth=2;g.stroke();g.fillStyle='#c9973f';g.beginPath();g.arc(0,-8,3,0,7);g.fill();}
 else if(k==='seal'){g.shadowColor='#6af';g.shadowBlur=12;g.strokeStyle='#8cc8ff';g.lineWidth=2.5;g.beginPath();g.arc(0,0,12,0,7);g.stroke();g.beginPath();for(let i=0;i<6;i++){const a=i/6*6.283-1.57;g.lineTo(Math.cos(a)*9,Math.sin(a)*9);const b=a+Math.PI*2/12;g.lineTo(Math.cos(b)*4,Math.sin(b)*4);}g.closePath();g.fillStyle='#bfe0ff';g.fill();}
 else if(k==='heart'){drawHeart(g,0,0,1.5,2);}
 else if(k==='shard'){g.shadowColor='#8df';g.shadowBlur=14;g.fillStyle='#a8e4ff';g.beginPath();g.moveTo(0,-14);g.lineTo(7,0);g.lineTo(0,14);g.lineTo(-7,0);g.closePath();g.fill();g.fillStyle='#fff';g.beginPath();g.moveTo(0,-8);g.lineTo(3,0);g.lineTo(0,6);g.lineTo(-3,0);g.fill();}
 else if(k==='ammo'){g.shadowColor='#8df';g.shadowBlur=8;g.fillStyle='#7cc8f0';g.beginPath();g.moveTo(0,-10);g.lineTo(5,0);g.lineTo(0,10);g.lineTo(-5,0);g.fill();g.fillStyle='#ffd36a';g.font='bold 14px serif';g.fillText('+',6,-2);}
 else if(k.startsWith('blade')){const t=+k[5];const col=['#d8dde6','#ff8a30','#ffd86a'][t];g.rotate(-.8);g.shadowColor=col;g.shadowBlur=t?14:4;g.fillStyle=col;g.beginPath();g.moveTo(-2,8);g.lineTo(-2.5+(t==2?-2:0),-14);g.lineTo(0,-18);g.lineTo(3+(t==2?2:0),-12);g.lineTo(2,8);g.fill();g.shadowBlur=0;g.fillStyle='#8a6a30';g.fillRect(-7,8,14,3);g.fillStyle='#4a3420';g.fillRect(-1.5,11,3,7);}
 g.restore();
}
function drawHeart(g,x,y,s,fill){
 g.save();g.translate(x,y);g.scale(s,s);
 const path=()=>{g.beginPath();g.moveTo(0,6);g.bezierCurveTo(-10,-1,-7,-9,0,-4);g.bezierCurveTo(7,-9,10,-1,0,6);g.closePath();};
 path();g.fillStyle='#2a1418';g.fill();
 if(fill>0){g.save();path();g.clip();g.fillStyle='#e0473c';g.fillRect(-10,-10,fill>=2?20:10,20);g.fillStyle='rgba(255,180,160,.6)';g.fillRect(-6,-6,3,2);g.restore();}
 path();g.lineWidth=1.3;g.strokeStyle='#e6b552';g.stroke();g.restore();
}
function openInv(){mode='inv';paused=true;renderInv();$('inv').classList.add('show');SFX.tick();}
function closeOv(){['inv','shop'].forEach(i=>$(i).classList.remove('show'));mode='play';paused=false;}
function renderInv(){
 const wn=['Ember Blade','Ignited Edge','Solar Falchion'],wd=['Physical. Swift, honest steel.','Fire. Faster strikes; foes flinch and burn.','Solar. Vast arc, heavy impact, pierces Sentinel guard.'];
 let h='';for(let i=0;i<=st.weapon;i++)h+=`<div class="row"><img src="${icon('blade'+i)}"><div class="nm">${wn[i]}<small>${wd[i]}</small></div>${i===st.weapon?'<span class="tag on">wielded</span>':'<span class="tag">surpassed</span>'}</div>`;
 if(st.shard)h+=`<div class="row"><img src="${icon('shard')}"><div class="nm">Aether Shard<small>Ranged bolt. Strikes switches, foes, distant crystals.</small></div><span class="tag on">${st.ammo} charges</span></div>`;
 else h+=`<div class="row" style="opacity:.4"><div class="nm"><small>An empty off-hand… something waits in the deep vault.</small></div></div>`;
 $('invArms').innerHTML=h;
 $('invHp').innerHTML=`<div class="row"><img src="${icon('heart')}"><div class="nm">${st.maxC} / 5 Heart Containers<small>${st.hp} of ${maxHp()} HP · rest at Sanctuary Stones to mend</small></div></div><div class="row"><div class="nm">Golden Sigils<small>Constructs, jars, coffers</small></div><span class="tag on gold">${st.gold}</span></div>`;
 const keysA={band:'Q',lantern:'W',cloak:'R'};
 $('invAcc').innerHTML=['band','lantern','cloak'].map(k=>st.acc[k]?`<div class="row"><img src="${icon(k)}"><div class="nm">${ITEM[k].name}<small>${ITEM[k].desc}</small></div><button class="sbtn" onclick="toggleAcc('${k}')">${st.eq[k]?'Unequip':'Equip'} [${keysA[k]}]</button></div>`:`<div class="row" style="opacity:.35"><div class="nm">— undiscovered —<small>&nbsp;</small></div></div>`).join('');
 $('invCons').innerHTML=['stem','seal'].map((k,i)=>`<div class="row"><img src="${icon(k)}"><div class="nm">${ITEM[k].name} ×${k==='stem'?st.stems:st.seals}<small>${ITEM[k].desc}</small></div><button class="sbtn" ${(k==='stem'?st.stems:st.seals)?'':'disabled'} onclick="${k==='stem'?'useStem':'useSeal'}();renderInv()">Use [${i+1}]</button></div>`).join('');
}
function toggleAcc(k){if(!st.acc[k])return;st.eq[k]=st.eq[k]?0:1;SFX.tick();if(st.eq[k]){tone(880,.2,{vol:.08,echo:.3});}renderInv();}
const SHOP=[
 {k:'stem',name:'Vital Stem',price:50,desc:'Restores 4 HP.',left:()=>3-st.shop.stem,max:3},
 {k:'seal',name:'Binding Seal',price:75,desc:'Freeze one construct ~3s.',left:()=>2-st.shop.seal,max:2},
 {k:'heart',name:'Heart Container #5',price:300,desc:'The merchant\'s heart-engine. One of a kind.',left:()=>st.shop.heart||fl('heart5')?0:(st.maxC>=5?0:1)},
 {k:'ammo',name:'Aether Charges ×3',price:30,desc:'10 gold per charge. Requires the Shard & a cleared Vault.',left:()=>(st.shard&&fl('z4done'))?99:0,lock:()=>!(st.shard&&fl('z4done'))}
];
function openShop(){mode='shop';paused=true;renderShop();$('shop').classList.add('show');SFX.shimmer();}
function renderShop(){
 $('shopGold').textContent=st.gold;
 $('shopList').innerHTML=SHOP.map((s,i)=>{const left=s.left();const locked=s.lock&&s.lock();
  let lab=locked?'Locked':left<=0?'Sold out':s.price+' gold';
  const dis=left<=0||st.gold<s.price||locked;
  const ic=s.k==='ammo'?'ammo':s.k;
  return `<div class="row"><img src="${icon(ic)}"><div class="nm">${s.name}<small>${s.desc}${s.max&&left>0?' · '+left+' left':''}</small></div><button class="sbtn" ${dis?'disabled':''} onclick="buy(${i})">[${i+1}] ${lab}</button></div>`;}).join('');
}
function buy(i){
 const s=SHOP[i];if(s.left()<=0||st.gold<s.price||(s.lock&&s.lock())){SFX.denied();return;}
 st.gold-=s.price;SFX.buy();
 if(s.k==='stem'){st.shop.stem++;st.stems++;}
 else if(s.k==='seal'){st.shop.seal++;st.seals++;}
 else if(s.k==='heart'){st.shop.heart=1;setF('heart5');closeOv();gainItem('heart',P.x,P.y);return;}
 else if(s.k==='ammo'){st.ammo+=3;}
 motes(P.x,P.y,'255,255,255',16);renderShop();
}
// ================= UPDATE =================
function update(dt){
 T+=dt;st.time+=dt;
 if(hitstop>0){hitstop-=dt;return;}
 updPlayer(dt);
 updObjects(dt);
 updEnemies(dt);
 updProj(dt);
 updCoins(dt);
 updParts(dt);
 ambient(dt);
 // camera
 const tx=P.x-VW/2+Math.cos(P.fa)*30,ty=P.y-VH/2+Math.sin(P.fa)*20;
 cam.x+=(tx-cam.x)*Math.min(1,dt*5);cam.y+=(ty-cam.y)*Math.min(1,dt*5);clampCam();
 shake=Math.max(0,shake-dt*30);shimmer=Math.max(0,shimmer-dt);redFlash=Math.max(0,redFlash-dt);heartShake=Math.max(0,heartShake-dt);
 if(banner){banner.life-=dt;if(banner.life<=0)banner=null;}
 toasts.forEach(t=>t.life-=dt);toasts=toasts.filter(t=>t.life>0);
 // merchant drone
 if(AU.ctx){let target=0;for(const o of Z.objs)if(o.t==='merchant'){const d=dist(o,P);target=clamp(1-d/260,0,1)*.32;}
  if(AU.muted)target=0;AU.drone.gain.value+=(target-AU.drone.gain.value)*Math.min(1,dt*3);}
 // lightning ambience
 if(Math.random()<dt*.04){lightning=.5;setTimeout(()=>SFX.thunder(),500+Math.random()*900);}
 lightning=Math.max(0,lightning-dt*1.8);
}
function updPlayer(dt){
 let mx=0,my=0;
 if(!dialog){
 if(keys['a']||keys['arrowleft'])mx--;if(keys['d']||keys['arrowright'])mx++;
 if(keys['w']||keys['arrowup'])my--;if(keys['s']||keys['arrowdown'])my++;}
 const mv=Math.hypot(mx,my);
 let spd=150;if(P.windT>0)spd*=.35;else if(P.swingT>0)spd*=.6;
 if(mv){mx/=mv;my/=mv;if(mouse.usedT<=0)P.fa=Math.atan2(my,mx);}
 mouse.usedT-=dt;
 if(mouse.usedT>0||mouse.l){P.fa=Math.atan2(mouse.y+cam.y-P.y,mouse.x+cam.x-P.x);}
 if(Math.cos(P.fa)>.15)P.face=1;else if(Math.cos(P.fa)<-.15)P.face=-1;
 P.kx*=Math.pow(.0005,dt);P.ky*=Math.pow(.0005,dt);
 moveBody(P,(mx*spd+P.kx)*dt,(my*spd+P.ky)*dt,false);
 P.moving=mv>0;
 if(P.moving){P.walk+=dt*10;P.stepT-=dt;if(P.stepT<=0){P.stepT=.32;SFX.step(tile(Math.floor(P.x/TS),Math.floor(P.y/TS)));
   if(st.weapon>=1)for(let i=0;i<2;i++)part(P.x+rnd(-5,5),P.y+6,{vx:rnd(-10,10),vy:rnd(-30,-10),life:rnd(.4,.8),size:rnd(1,2.2),col:st.weapon===2?'255,220,120':'255,120,40',glow:1});
   if(Z.D.floor==='~')for(let i=0;i<4;i++)part(P.x+rnd(-6,6),P.y+9,{vx:rnd(-30,30),vy:rnd(-40,-10),g:150,life:.35,size:1.5,col:'150,200,230',glow:0});
 }}
 if(st.weapon>=1&&P.moving&&Math.random()<dt*14)part(P.x-Math.cos(P.fa)*6+rnd(-3,3),P.y+rnd(-4,6),{vx:rnd(-6,6),vy:rnd(-25,-8),life:rnd(.4,.7),size:rnd(.8,1.8),col:st.weapon===2?'255,220,120':'255,140,50',glow:1});
 P.cd-=dt;P.swingT-=dt;P.inv-=dt;P.flash-=dt;P.shardCd-=dt;P.trapCd-=dt;if(P.aura>0)P.aura-=dt;
 if(P.windT>0){P.windT-=dt;if(P.windT<=0)doSwing();}
 if(!dialog&&(keys['j']||keys[' ']||mouse.l))startSwing();
 // ammo trickle
 if(st.shard&&st.ammo<(st.zone===5?8:5)){P.ammoT+=dt;if(P.ammoT>(st.zone===5?1.6:4)){P.ammoT=0;st.ammo++;}}
 // traps
 const tt=tile(Math.floor(P.x/TS),Math.floor(P.y/TS));
 if(tt==='^')hurtPlayer(1,null,null,true);
 for(const pl of Z.pools){if(Math.hypot(pl.x-P.x,pl.y-P.y)<pl.r*.8)hurtPlayer(1,null,null,true);}
 pickupCheck();
 // exits
 for(const o of Z.objs){if(o.t!=='exit')continue;
  const x0=o.tx*TS,y0=o.ty*TS,x1=x0+o.w*TS,y1=y0+o.h*TS;
  if(P.x>x0-2&&P.x<x1+2&&P.y>y0-2&&P.y<y1+2){
   const back=o.to<Z.idx;
   if(!back&&o.to===3&&!fl('curator'))continue;
   loadZone(o.to,back?'back':null);return;}
 }
 // arenas
 if(!Z.arena)for(const o of Z.objs){if(o.t==='arena'&&!fl(o.flag)&&P.x>o.rx+8&&P.x<o.rx+o.rw-8&&P.y>o.ry+8&&P.y<o.ry+o.rh-8){startArena(o);break;}}
}
function startArena(o){
 Z.arena=o;Z.arenaWave=0;showBanner(o.msg,'');SFX.roar();doShake(10);
 spawnWave(o,0);
}
function spawnWave(o,i){
 for(const [t,x,y] of o.waves[i]){const e=spawnEnemy(t,x,y,true);e.aggro=1;e.cd=1.2;}
 if(i>0)toast('Another wave rises from the black iron...','#d08a6a');
}
function arenaTick(){
 const a=Z.arena;if(!a)return;
 if(Z.enemies.some(e=>e.arena&&!e.dead))return;
 Z.arenaWave++;
 if(Z.arenaWave<a.waves.length){spawnWave(a,Z.arenaWave);return;}
 setF(a.flag);Z.arena=null;if(a.done){toast(a.done,'#ffe3a6');}SFX.light();doShimmer(1);
 const lootGold={z2arena:30,curator:0,gauntlet5:40}[a.flag]||0;if(lootGold)dropCoins(P.x,P.y-30,lootGold);
}
function lightTrace(){
 // returns path & whether receiver hit
 const em=Z.objs.find(o=>o.t==='emitter');if(!em)return null;
 let x=em.tx,y=em.ty,dx=em.dx,dy=em.dy;const pts=[[em.x,em.y]];let hit=false;
 for(let i=0;i<80;i++){
  x+=dx;y+=dy;const t=tile(x,y);
  if(t==='#'||blockObj(x,y)){pts.push([x*TS+16-dx*16,y*TS+16-dy*16]);break;}
  const m=Z.objs.find(o=>o.t==='mirror'&&o.tx===x&&o.ty===y);
  const r=Z.objs.find(o=>o.t==='receiver'&&o.tx===x&&o.ty===y);
  if(r){pts.push([r.x,r.y]);hit=true;break;}
  if(m){pts.push([m.x,m.y]);if(m.o==='/'){const ndx=-dy,ndy=-dx;dx=ndx;dy=ndy;}else{const ndx=dy,ndy=dx;dx=ndx;dy=ndy;}}
 }
 return{pts,hit};
}
function updObjects(dt){
 arenaTick();
 for(const k in Z.timers){const before=Z.timers[k];Z.timers[k]-=dt;if(Z.timers[k]>0&&Math.floor(before*2)!==Math.floor(Z.timers[k]*2))SFX.tick();if(before>0&&Z.timers[k]<=0)SFX.door();}
 for(const o of Z.objs){
  o.phase+=dt;if(o.rotCd>0)o.rotCd-=dt;if(o.shk>0)o.shk-=dt;if(o.pulse>0)o.pulse-=dt;
  if(o.t==='door'){const tgt=o.isOpen()?1:0;if(o.lead&&tgt!==Math.round(o.open)&&Math.abs(tgt-o.open)>.98){if(dist(o,P)<600){SFX.door();doShake(3);}}
    o.open+=(tgt-o.open)*Math.min(1,dt*6);if(Math.abs(tgt-o.open)<.02)o.open=tgt;
    if(o.open<.5&&Math.abs(P.x-o.x)<16+P.r-1&&Math.abs(P.y-o.y)<16+P.r-1){ // closing on player: push out to free side
      for(const [dx,dy] of [[1,0],[-1,0],[0,1],[0,-1]]){const nx=o.x+dx*(16+P.r+1),ny=o.y+dy*(16+P.r+1);if(boxFree(nx,ny,P.r)){P.x=nx;P.y=ny;break;}}}}
  else if(o.t==='plate'){
   let pressed=dist(o,P)<20;
   if(!pressed)for(const e of Z.enemies)if(!e.dead&&!e.fly&&dist(o,e)<22){pressed=true;break;}
   if(pressed&&!o.pressed){SFX.plate();sparks(o.x,o.y,'255,200,100',6);
    if(o.door){Z.timers[o.door]=o.time;toast('The plate sinks — a gate grinds open somewhere... hurry!','#ffd38a');}
    if(o.group&&!fl(o.group)){o.last=T;const grp=Z.objs.filter(p=>p.t==='plate'&&p.group===o.group);
      if(grp.every(p=>T-p.last<p.window)){setF(o.group);SFX.light();toast('Both plates resonate — the altar door opens!','#ffe3a6');doShimmer(.6);}
      else toast('The plate hums... its twin must be pressed within '+o.window+' heartbeats.','#cdbb92');}
   }
   if(o.door&&pressed)Z.timers[o.door]=Math.max(Z.timers[o.door]||0,o.time);
   o.pressed=pressed;
  }
  else if(o.t==='switch'&&o.core){if(o.off>0){o.off-=dt;if(o.off<=0){o.lit=false;sparks(o.x,o.y,'255,120,60',12);}}}
  else if(o.t==='stone'){if(Math.random()<dt*8)part(o.x+rnd(-16,16),o.y+rnd(-10,10),{vy:-rnd(10,30),life:rnd(1,2),size:rnd(1,2),col:'255,220,120',glow:1});}
  else if(o.t==='altar'&&st.weapon<o.up){if(Math.random()<dt*10)part(o.x+rnd(-12,12),o.y-10,{vy:-rnd(15,40),vx:rnd(-6,6),life:rnd(.6,1.2),size:rnd(1,2.5),col:o.up===1?'255,130,40':'255,220,120',glow:1});}
  else if(o.t==='pickup'){if(Math.random()<dt*6)part(o.x+rnd(-8,8),o.y+rnd(-4,4),{vy:-rnd(10,30),life:1,size:1.5,col:'255,255,255',glow:1});}
  else if(o.t==='beacon'&&Z.engineDead){if(Math.random()<dt*20)part(o.x+rnd(-20,20),o.y+rnd(-10,10),{vy:-rnd(20,60),life:rnd(.8,1.5),size:rnd(1,3),col:'255,220,140',glow:1});}
 }
 // light puzzle
 const lt=lightTrace();if(lt){Z.beam=lt.pts;if(lt.hit&&!fl('light4')){setF('light4');SFX.light();doShimmer(1,'150,200,255');toast('Starlight floods the receiver — a sealed alcove opens to the south!','#bfe0ff');}}
}
function strikeSwitch(o){
 if(o.core){if(o.lit)return;o.lit=true;o.off=9;SFX.chime();SFX.brk();burst(o.x,o.y,24,'150,220,255',200);
  const all=Z.objs.filter(s=>s.t==='switch'&&s.core);
  if(all.every(s=>s.lit)){const en=Z.enemies.find(e=>e.type==='engine'&&!e.dead);if(en){en.shield=0;en.shieldT=8;all.forEach(s=>s.off=8);toast('The Engine\'s ward shatters — STRIKE NOW!','#ffe08a');doShake(10);SFX.roar();}}
  else toast('Conduit shattered ('+all.filter(s=>s.lit).length+'/4)','#bfe0ff');
  return;}
 if(o.lit)return;o.lit=true;SFX.chime();tone(1318,.6,{vol:.1,echo:.5});burst(o.x,o.y,20,'150,220,255',150);
 const grp=Z.objs.filter(s=>s.t==='switch'&&s.group===o.group);
 if(grp.every(s=>s.lit)){if(!fl(o.flag)){setF(o.flag);SFX.light();doShimmer(.8,'150,210,255');
   if(o.flag==='bridge4'){setF('z4done');toast('A bridge of light-forged bronze unfolds across the void! The Merchant now restocks Aether charges.','#bfe0ff');}
   else toast('The crystal mechanism awakens — something unseals.','#bfe0ff');}}
 else toast('One crystal sings... its twin remains dark.','#bfe0ff');
}
function updProj(dt){
 for(const p of Z.projs){
  p.x+=p.vx*dt;p.y+=p.vy*dt;p.life-=dt;
  if(Math.random()<.8)part(p.x,p.y,{vx:rnd(-15,15),vy:rnd(-15,15),life:.3,size:rnd(1,2.5),col:'150,220,255',glow:1});
  if(projSolid(p.x,p.y)){
   const tx=Math.floor(p.x/TS),ty=Math.floor(p.y/TS);const ob=Z.bmap[ty*Z.w+tx];
   if(ob&&ob.t==='crack'&&!ob.broken&&(!ob.hidden||st.eq.lantern))damageCrack(ob,1);
   p.life=0;sparks(p.x,p.y,'150,220,255',8);SFX.chime();continue;}
  for(const e of Z.enemies){if(e.dead)continue;if(Math.hypot(e.x-p.x,e.y-p.y)<e.r+5){hitEnemy(e,2,p.a,'shard');p.life=0;break;}}
  if(p.life<=0)continue;
  for(const o of Z.objs){
   if(o.t==='switch'&&Math.hypot(o.x-p.x,o.y-6-p.y)<16){strikeSwitch(o);p.life=0;break;}
   if(o.t==='jar'&&!o.dead&&Math.hypot(o.x-p.x,o.y-p.y)<12){breakJar(o);p.life=0;break;}
   if(o.t==='mirror'&&Math.hypot(o.x-p.x,o.y-p.y)<13){rotMirror(o);p.life=0;break;}
  }
 }
 Z.projs=Z.projs.filter(p=>p.life>0);
 for(const p of Z.eproj){
  p.x+=p.vx*dt;p.y+=p.vy*dt;p.life-=dt;
  if(Math.random()<.5)part(p.x,p.y,{life:.25,size:rnd(1,2.5),col:p.col||'200,80,255',glow:1});
  if(projSolid(p.x,p.y)){p.life=0;sparks(p.x,p.y,p.col||'200,80,255',4);continue;}
  if(Math.hypot(P.x-p.x,P.y-6-p.y)<P.r+p.r){hurtPlayer(p.dmg||1,p.x-p.vx,p.y-p.vy);p.life=0;}
 }
 Z.eproj=Z.eproj.filter(p=>p.life>0);
 for(const pl of Z.pools){pl.life-=dt;if(Math.random()<dt*4)part(pl.x+rnd(-pl.r,pl.r)*.7,pl.y+rnd(-pl.r,pl.r)*.4,{vy:-rnd(10,25),life:.8,size:2,col:'120,50,160',glow:1});}
 Z.pools=Z.pools.filter(p=>p.life>0);
}
function updCoins(dt){
 for(const c of Z.coins){
  c.t+=dt;c.vx*=Math.pow(.02,dt);c.vy*=Math.pow(.02,dt);
  const d=dist(c,P);
  if(c.t>.35&&d<110){const a=Math.atan2(P.y-c.y,P.x-c.x);const s=(110-d)*8;c.vx+=Math.cos(a)*s*dt;c.vy+=Math.sin(a)*s*dt;}
  c.x+=c.vx*dt;c.y+=c.vy*dt;
  if(c.t>.3&&d<16){c.got=1;
   if(c.ammo){st.ammo=Math.min(20,st.ammo+1);tone(1900,.12,{type:'triangle',vol:.08});ftext(P.x,P.y-30,'+1 charge','#9cd8ff',12);}
   else{st.gold+=c.v;SFX.coin();goldDust(c.x,c.y);ftext(P.x+rnd(-8,8),P.y-30,'+'+c.v,'#ffd36a',12);}}
 }
 Z.coins=Z.coins.filter(c=>!c.got);
}
function updParts(dt){
 for(const p of Z.parts){p.life-=dt;p.vy+=p.g*dt;p.vx*=Math.pow(p.drag,dt*60);p.vy*=Math.pow(p.drag,dt*60);
  if(p.swirl){const a=Math.atan2(p.vy,p.vx)+p.swirl*dt;const s=Math.hypot(p.vx,p.vy);p.vx=Math.cos(a)*s;p.vy=Math.sin(a)*s;}
  p.x+=p.vx*dt;p.y+=p.vy*dt;}
 Z.parts=Z.parts.filter(p=>p.life>0);
 for(const t of Z.texts){t.life-=dt;t.y-=40*dt;}Z.texts=Z.texts.filter(t=>t.life>0);
}
function ambient(dt){
 const a=Z.D.amb,n=dt*30;
 for(let i=0;i<n;i++){if(Math.random()>.5)continue;
  const x=cam.x+rnd(0,VW),y=cam.y+rnd(0,VH);
  if(a==='dust')part(x,y,{vx:rnd(-6,6),vy:rnd(-4,4),life:rnd(2,4),size:rnd(.8,1.8),col:'220,200,160',glow:0,drag:1,amb:1});
  else if(a==='mist')part(x,y+60,{vx:rnd(-8,8),vy:-rnd(8,20),life:rnd(2,4),size:rnd(8,18),col:'140,190,220',glow:0,amb:2});
  else if(a==='ash')part(x,y,{vx:rnd(4,14),vy:rnd(6,16),life:rnd(2,4),size:rnd(.8,1.6),col:'170,160,150',glow:0,amb:1});
  else if(a==='spark'){if(Math.random()<.4)part(x,y,{vx:rnd(-5,5),vy:-rnd(5,15),life:rnd(1.5,3),size:rnd(.8,1.6),col:'255,190,90',glow:1,amb:1});}
  else if(a==='core')part(x,y,{vx:rnd(-5,5),vy:-rnd(10,25),life:rnd(1.5,3),size:rnd(1,2),col:Math.random()<.5?'255,170,80':'120,60,150',glow:1,amb:1});
 }
}
// ================= ENEMIES =================
function eShoot(e,a,spd,col,r=5,dmg=1){Z.eproj.push({x:e.x,y:e.y-6,vx:Math.cos(a)*spd,vy:Math.sin(a)*spd,life:4,col,r,dmg});}
function updEnemies(dt){
 for(const e of Z.enemies){
  if(e.dead)continue;
  e.ph+=dt;e.hurt-=dt;
  if(e.burn>0){e.burn-=dt;if(Math.random()<dt*25)part(e.x+rnd(-8,8),e.y+rnd(-10,4),{vy:-rnd(20,50),life:.4,size:rnd(1,2.5),col:'255,120,30',glow:1});}
  e.kx*=Math.pow(.002,dt);e.ky*=Math.pow(.002,dt);
  if(e.frozen>0){e.frozen-=dt;moveBody(e,e.kx*dt*.2,e.ky*dt*.2,e.fly);if(Math.random()<dt*10)part(e.x+rnd(-e.r,e.r),e.y+rnd(-e.r,e.r),{life:.5,size:2,col:'150,210,255',glow:1});continue;}
  moveBody(e,e.kx*dt,e.ky*dt,e.fly);
  if(e.stun>0){e.stun-=dt;continue;}
  const d=dist(e,P),a=Math.atan2(P.y-e.y,P.x-e.x);
  if(!e.aggro&&d<230&&los(e,P))e.aggro=1;
  if(!e.aggro){e.t-=dt;if(e.t<=0){e.t=rnd(1,3);e.wa=Math.random()*6.28;e.wm=Math.random()<.5;}if(e.wm)moveBody(e,Math.cos(e.wa)*20*dt,Math.sin(e.wa)*20*dt,e.fly);continue;}
  if(d>600&&!e.arena){e.aggro=0;continue;}
  e.cd-=dt;
  const step=(ang,s)=>moveBody(e,Math.cos(ang)*s*dt,Math.sin(ang)*s*dt,e.fly);
  switch(e.type){
  case 'husk':
   if(e.state==='lunge'){e.t-=dt;step(e.la,260);if(d<e.r+P.r+4)hurtPlayer(e.dmg,e.x,e.y);if(e.t<=0){e.state='idle';e.cd=rnd(1,1.6);}}
   else if(e.state==='tele'){e.t-=dt;if(e.t<=0){e.state='lunge';e.t=.28;}}
   else{e.fa=a;if(d>60)step(a,e.spd);else step(a+1.57,e.spd*.5);
    if(d<95&&e.cd<=0){e.state='tele';e.t=.45;e.la=a;tone(220,.15,{type:'square',vol:.03,to:330});}
    if(d<e.r+P.r)hurtPlayer(e.dmg,e.x,e.y);}
   break;
  case 'wisp':
   e.fa=a;{const want=d<130?-1:d>200?1:0;step(a,e.spd*want);step(a+1.57,Math.sin(e.ph*1.3)*40);}
   if(e.cd<=0&&los(e,P)){e.cd=rnd(1.8,2.6);eShoot(e,a,170,'200,100,255');SFX.ebolt();}
   break;
  case 'sentinel':
   if(e.state==='tele'){e.t-=dt;if(e.t<=0){e.state='idle';e.cd=rnd(1.8,2.4);doShake(5);SFX.brk();
      for(let i=0;i<20;i++){const aa=i/20*6.28;part(e.x+Math.cos(aa)*20,e.y+Math.sin(aa)*20,{vx:Math.cos(aa)*200,vy:Math.sin(aa)*200,life:.35,size:3,col:'200,120,60',glow:1,drag:.9});}
      if(d<74)hurtPlayer(e.dmg,e.x,e.y);}}
   else{e.fa+=angDiff(a,e.fa)*Math.min(1,dt*2.5);if(d>50)step(e.fa,e.spd);if(d<70&&e.cd<=0){e.state='tele';e.t=.7;tone(110,.6,{type:'sawtooth',vol:.05,to:220});}
    if(d<e.r+P.r)hurtPlayer(1,e.x,e.y);}
   break;
  case 'brute':
   if(e.state==='charge'){e.t-=dt;const hw=moveBody(e,Math.cos(e.la)*330*dt,Math.sin(e.la)*330*dt,false);if(Math.random()<.5)part(e.x,e.y+12,{vx:rnd(-20,20),vy:-rnd(10,30),life:.4,size:3,col:'120,100,80'});
     if(d<e.r+P.r+2)hurtPlayer(e.dmg,e.x,e.y);
     if(hw||e.t<=0){e.state='idle';e.cd=rnd(1.5,2.2);if(hw){e.stun=1;doShake(6);SFX.brk();sparks(e.x,e.y,'255,180,80',14);}}}
   else if(e.state==='tele'){e.t-=dt;e.la=a;if(e.t<=0){e.state='charge';e.t=.9;SFX.roar();}}
   else{e.fa=a;step(a,e.spd);if(d<e.r+P.r)hurtPlayer(e.dmg,e.x,e.y);if(e.cd<=0&&d<260&&los(e,P)){e.state='tele';e.t=.6;}}
   break;
  case 'curator':
   e.fa=a;
   {const want=d<140?-1:d>240?1:0;step(a,e.spd*want);step(a+1.57,Math.sin(e.ph*.8)*70);}
   if(d<e.r+P.r)hurtPlayer(e.dmg,e.x,e.y);
   if(e.cd<=0){
    const hp=e.hp/e.mhp;const pat=ri(0,hp<.5?2:1);
    if(pat===0){const n=hp<.5?12:9;for(let i=0;i<n;i++)eShoot(e,i/n*6.283+e.ph,130,'255,140,80',6);SFX.ebolt();e.cd=2.1;}
    else if(pat===1){for(let i=-1;i<=1;i++)eShoot(e,a+i*.22,210,'200,100,255',5);SFX.ebolt();e.cd=1.5;}
    else{if(Z.enemies.filter(x=>!x.dead&&x.type==='wisp').length<2){spawnEnemy('wisp',e.x+rnd(-40,40),e.y+rnd(-40,40),true,true).aggro=1;}e.cd=2.2;}
   }
   break;
  case 'guardian':
   if(e.state==='dash'){e.t-=dt;moveBody(e,Math.cos(e.la)*430*dt,Math.sin(e.la)*430*dt,false);if(Math.random()<.8)part(e.x,e.y,{life:.3,size:6,col:'255,170,60',glow:1});
     if(d<e.r+P.r+2)hurtPlayer(e.dmg,e.x,e.y);if(e.t<=0){e.state='idle';e.cd=rnd(.8,1.3);e.stun=.35;}}
   else if(e.state==='tele'){e.t-=dt;e.la=a;if(e.t<=0){e.state='dash';e.t=.42;SFX.swing(2);}}
   else if(e.state==='spin'){e.t-=dt;e.spin=(e.spin||0)+dt*8;if(Math.floor(e.t*10)%2===0&&!e.fired){e.fired=1;eShoot(e,e.spin,180,'255,180,70',6);eShoot(e,e.spin+Math.PI,180,'255,180,70',6);}else e.fired=0;
     if(e.t<=0){e.state='idle';e.cd=1;}}
   else{e.fa=a;step(a,d>90?e.spd:-e.spd*.4);if(d<e.r+P.r)hurtPlayer(e.dmg,e.x,e.y);
    if(e.cd<=0){const r=Math.random();if(r<.5){e.state='tele';e.t=.55;tone(300,.5,{type:'sawtooth',vol:.05,to:600});}
     else if(r<.8){e.state='spin';e.t=1.8;}
     else{for(let i=-2;i<=2;i++)eShoot(e,a+i*.25,200,'255,200,90',5);SFX.ebolt();e.cd=1.4;}}}
   break;
  case 'engine':
   if(!e.shield){e.shieldT-=dt;if(e.shieldT<=0){e.shield=1;toast('The Engine re-wards itself. Shatter the conduits again!','#d08a6a');SFX.block();Z.objs.forEach(s=>{if(s.core){s.lit=false;s.off=0;}});}}
   const hpF=e.hp/e.mhp;
   if(e.cd<=0){e.spiral+=1;const n=hpF<.4?4:3;for(let i=0;i<n;i++)eShoot(e,e.ph*1.6+i/n*6.283,140,'180,80,255',6);e.cd=hpF<.4?.32:.45;}
   e.poolT-=dt;if(e.poolT<=0){e.poolT=hpF<.5?3:4.5;Z.pools.push({x:P.x,y:P.y,r:0,mr:34,life:6,warn:1});}
   e.summonT-=dt;if(e.summonT<=0){e.summonT=11;const n=Z.enemies.filter(x=>!x.dead&&x.type!=='engine').length;if(n<3){spawnEnemy(Math.random()<.5?'husk':'wisp',rnd(9,31),rnd(10,20),true).aggro=1;}}
   if(d<e.r+P.r+4)hurtPlayer(e.dmg,e.x,e.y);
   break;
  }
 }
 for(const pl of Z.pools){if(pl.warn){pl.warn-=dt*.9;if(pl.warn<=0){pl.warn=0;pl.r=pl.mr;}}}
 Z.pools=Z.pools.filter(p=>p.life>0);
 Z.enemies=Z.enemies.filter(e=>!e.dead);
}
function engineDefeated(e){
 Z.engineDead=1;setF('engine');Z.eproj=[];Z.pools=[];
 Z.enemies.forEach(x=>{if(!x.dead&&x!==e){x.dead=1;burst(x.x,x.y,20,'255,140,60',200);}});
 SFX.roar();setTimeout(()=>SFX.light(),600);doShimmer(3,'255,220,140');
 showBanner('The Corroded Engine Falls','Approach the Core Beacon and press [E] to reignite it.');
}
function finishGame(){
 mode='end';paused=true;SFX.light();setTimeout(()=>SFX.rare(),400);
 const m=Math.floor(st.time/60),s=Math.floor(st.time%60);
 $('endText').innerHTML=`Light pours up the Core shaft and through every shattered lens. The Corrosion recoils into dust; the great gears turn once more, and the Observatory rises above the storm.<br><br>
 <span style="color:#8fa3c8;font-style:normal">Time ${m}m ${s}s · Constructs felled ${st.kills} · Hearts ${st.maxC}/5 · Gold ${st.gold} · Accessories ${Object.values(st.acc).filter(Boolean).length}/3</span>`;
 setTimeout(()=>$('end').classList.add('show'),1200);
 try{localStorage.removeItem(SAVE_KEY);}catch(e){}savedStr=null;
}
// ================= RENDER =================
const dark=document.createElement('canvas');dark.width=VW;dark.height=VH;const dg=dark.getContext('2d');
function glowDot(g,x,y,r,col,a){const gr=g.createRadialGradient(x,y,0,x,y,r);gr.addColorStop(0,`rgba(${col},${a})`);gr.addColorStop(1,`rgba(${col},0)`);g.fillStyle=gr;g.fillRect(x-r,y-r,r*2,r*2);}
function render(){
 const g=ctx;g.save();
 g.fillStyle='#04060b';g.fillRect(0,0,VW,VH);
 // starfield backdrop
 for(let i=0;i<80;i++){const x=(hash(i,1,9)*VW*1.3-cam.x*.1)%VW,y=(hash(i,2,9)*VH*1.3-cam.y*.1)%VH;g.fillStyle=`rgba(200,210,255,${.2+.4*Math.abs(Math.sin(T+i))})`;g.fillRect((x+VW)%VW,(y+VH)%VH,1.5,1.5);}
 if(lightning>0){g.fillStyle=`rgba(160,180,255,${lightning*.25})`;g.fillRect(0,0,VW,VH);}
 const sx=shake?rnd(-shake,shake)*.5:0,sy=shake?rnd(-shake,shake)*.5:0;
 g.translate(Math.round(-cam.x+sx),Math.round(-cam.y+sy));
 g.drawImage(Z.tiles,0,0);
 // ground layer
 for(const b of Z.bridges)drawBridge(g,b);
 for(const o of Z.objs){if(o.t==='plate')drawPlate(g,o);else if(o.t==='crack')drawCrack(g,o);else if(o.t==='door')drawDoor(g,o);else if(o.t==='arena'&&Z.arena===o)drawArenaRing(g,o);}
 for(const pl of Z.pools)drawPool(g,pl);
 // light beam
 if(Z.beam&&Z.beam.length>1){g.save();g.globalCompositeOperation='lighter';g.strokeStyle='rgba(150,200,255,.25)';g.lineWidth=10;g.beginPath();Z.beam.forEach((p,i)=>i?g.lineTo(p[0],p[1]):g.moveTo(p[0],p[1]));g.stroke();g.strokeStyle='rgba(220,240,255,.9)';g.lineWidth=2;g.stroke();g.restore();}
 // ysorted
 const list=[];
 for(const o of Z.objs)if(!['plate','crack','door','exit','arena','bridge'].includes(o.t)&&!o.dead)list.push({y:o.y,f:()=>drawObj(g,o)});
 for(const e of Z.enemies)list.push({y:e.y,f:()=>drawEnemy(g,e)});
 list.push({y:P.y,f:()=>drawPlayer(g)});
 list.sort((a,b)=>a.y-b.y);list.forEach(l=>l.f());
 // coins
 for(const c of Z.coins){if(c.ammo){g.save();g.translate(c.x,c.y+Math.sin(T*5+c.t)*2);drawIcon(g,'ammo',0,0,1);g.restore();continue;}
  const w=Math.abs(Math.cos(T*6+c.x))*5+1.5;g.fillStyle='#b8871c';g.beginPath();g.ellipse(c.x,c.y,w,5,0,0,7);g.fill();g.fillStyle='#ffd36a';g.beginPath();g.ellipse(c.x,c.y-.5,w*.7,3.6,0,0,7);g.fill();
  if(Math.random()<.04)part(c.x,c.y,{vy:-20,life:.5,size:1.2,col:'255,230,140',glow:1});}
 // projectiles
 g.save();g.globalCompositeOperation='lighter';
 for(const p of Z.projs){glowDot(g,p.x,p.y,18,'130,200,255',.8);g.fillStyle='#fff';g.save();g.translate(p.x,p.y);g.rotate(p.a);g.fillRect(-7,-2,14,4);g.restore();}
 for(const p of Z.eproj){glowDot(g,p.x,p.y,p.r*3,p.col,.7);g.fillStyle=`rgb(${p.col})`;g.beginPath();g.arc(p.x,p.y,p.r*.7,0,7);g.fill();}
 // particles
 for(const p of Z.parts){const a=clamp(p.life/p.ml,0,1);
  if(p.amb===2){g.globalCompositeOperation='source-over';g.fillStyle=`rgba(${p.col},${a*.06})`;g.beginPath();g.arc(p.x,p.y,p.size,0,7);g.fill();g.globalCompositeOperation='lighter';continue;}
  if(!p.glow){g.globalCompositeOperation='source-over';g.fillStyle=`rgba(${p.col},${a})`;g.fillRect(p.x-p.size/2,p.y-p.size/2,p.size,p.size);g.globalCompositeOperation='lighter';continue;}
  if(p.shape==='l'){g.strokeStyle=`rgba(${p.col},${a})`;g.lineWidth=p.size;g.beginPath();g.moveTo(p.x,p.y);g.lineTo(p.x-p.vx*.04,p.y-p.vy*.04);g.stroke();}
  else{g.fillStyle=`rgba(${p.col},${a*(p.amb?.6:1)})`;g.beginPath();g.arc(p.x,p.y,p.size*(.5+a*.5),0,7);g.fill();}}
 g.restore();
 // texts
 g.textAlign='center';for(const t of Z.texts){g.globalAlpha=clamp(t.life*2,0,1);g.font=`bold ${t.sz}px Georgia`;g.fillStyle='#000';g.fillText(t.txt,t.x+1,t.y+1);g.fillStyle=t.col;g.fillText(t.txt,t.x,t.y);}g.globalAlpha=1;
 // interact prompt
 const ni=mode==='play'&&!dialog&&nearInteract();
 if(ni){const lab={stone:'Rest',merchant:'Trade',chest:'Open',note:'Read',altar:'Claim',mirror:'Turn',beacon:'Reignite the Beacon'}[ni.t];
  g.font='13px Georgia';const w=g.measureText(lab).width+40;g.fillStyle='rgba(10,12,22,.85)';g.fillRect(ni.x-w/2,ni.y-ni.r-44,w,22);g.strokeStyle='#c9973f';g.strokeRect(ni.x-w/2+.5,ni.y-ni.r-43.5,w-1,21);
  g.fillStyle='#ffe3a6';g.fillText('[E] '+lab,ni.x,ni.y-ni.r-28);}
 g.restore();
 drawDarkness();
 drawScreenFx();
 drawHUD();
}
function drawDarkness(){
 let dk=Z.D.dark;if(dk<=0)return;
 dg.globalCompositeOperation='source-over';dg.clearRect(0,0,VW,VH);dg.fillStyle=`rgba(3,5,12,${dk})`;dg.fillRect(0,0,VW,VH);
 dg.globalCompositeOperation='destination-out';
 const px=P.x-cam.x,py=P.y-cam.y;const R=st.eq.lantern?300:170;
 const gr=dg.createRadialGradient(px,py,20,px,py,R);gr.addColorStop(0,'rgba(0,0,0,1)');gr.addColorStop(.6,'rgba(0,0,0,.75)');gr.addColorStop(1,'rgba(0,0,0,0)');dg.fillStyle=gr;dg.fillRect(px-R,py-R,R*2,R*2);
 const hole=(x,y,r,a=1)=>{x-=cam.x;y-=cam.y;if(x<-r||y<-r||x>VW+r||y>VH+r)return;const gg=dg.createRadialGradient(x,y,0,x,y,r);gg.addColorStop(0,`rgba(0,0,0,${a})`);gg.addColorStop(1,'rgba(0,0,0,0)');dg.fillStyle=gg;dg.fillRect(x-r,y-r,r*2,r*2);};
 for(const o of Z.objs){
  if(o.t==='stone')hole(o.x,o.y,120);else if(o.t==='merchant')hole(o.x,o.y,140);else if(o.t==='switch')hole(o.x,o.y-8,o.lit?110:60,.8);
  else if(o.t==='altar')hole(o.x,o.y,100);else if(o.t==='pickup')hole(o.x,o.y,70);else if(o.t==='emitter'||o.t==='receiver')hole(o.x,o.y,80);else if(o.t==='beacon')hole(o.x,o.y,Z.engineDead?300:110);
 }
 if(Z.beam)for(const p of Z.beam)hole(p[0],p[1],60,.8);
 for(const p of Z.projs)hole(p.x,p.y,70);for(const p of Z.eproj)hole(p.x,p.y,36,.6);
 for(const e of Z.enemies)if(e.boss)hole(e.x,e.y,120,.7);
 ctx.drawImage(dark,0,0);
 if(st.eq.lantern){ctx.save();ctx.globalCompositeOperation='lighter';glowDot(ctx,px,py-8,110,'90,150,255',.08);ctx.restore();}
}
function drawScreenFx(){
 const g=ctx;
 // vignette
 const v=g.createRadialGradient(VW/2,VH/2,VH*.35,VW/2,VH/2,VH*.85);v.addColorStop(0,'rgba(0,0,0,0)');v.addColorStop(1,'rgba(0,0,0,.6)');g.fillStyle=v;g.fillRect(0,0,VW,VH);
 if(redFlash>0){const r=g.createRadialGradient(VW/2,VH/2,VH*.25,VW/2,VH/2,VH*.75);r.addColorStop(0,'rgba(255,0,0,0)');r.addColorStop(1,`rgba(200,20,20,${redFlash})`);g.fillStyle=r;g.fillRect(0,0,VW,VH);}
 if(st.hp<=2&&st.hp>0){const a=.12+.1*Math.sin(T*5);g.strokeStyle=`rgba(200,30,30,${a})`;g.lineWidth=18;g.strokeRect(0,0,VW,VH);}
 if(shimmer>0){const a=Math.min(1,shimmer)*.55;g.save();g.globalCompositeOperation='lighter';
  for(let i=0;i<4;i++){const w=26+Math.sin(T*9+i)*8;
   const grd=[g.createLinearGradient(0,0,w,0),g.createLinearGradient(VW,0,VW-w,0),g.createLinearGradient(0,0,0,w),g.createLinearGradient(0,VH,0,VH-w)][i];
   grd.addColorStop(0,`rgba(${shimmerCol},${a})`);grd.addColorStop(1,`rgba(${shimmerCol},0)`);g.fillStyle=grd;g.fillRect(0,0,VW,VH);}
  for(let i=0;i<10;i++){const t=(T*0.7+i/10)%1;const side=i%4;const x=side<2?(side?VW-8:8):t*VW,y=side<2?t*VH:(side===2?8:VH-8);g.fillStyle=`rgba(255,255,230,${a})`;g.fillRect(x-1,y-1,3,3);}
  g.restore();}
}
function drawHUD(){
 const g=ctx;
 // hearts
 g.save();
 for(let i=0;i<st.maxC;i++){const fill=clamp(st.hp-i*2,0,2);let x=34+i*36,y=34;if(heartShake>0&&fill<2&&st.hp-i*2>-2){x+=rnd(-2,2);y+=rnd(-2,2);}
  if(P.aura>0&&i===st.maxC-1){g.save();g.globalCompositeOperation='lighter';glowDot(g,x,y,30,'255,200,120',P.aura*.4);g.restore();}
  drawHeart(g,x,y,1.6,fill);}
 for(let i=st.maxC;i<5;i++){g.globalAlpha=.18;drawHeart(g,34+i*36,34,1.6,0);g.globalAlpha=1;}
 // gold
 g.font='bold 17px Georgia';g.textAlign='left';
 g.fillStyle='#b8871c';g.beginPath();g.arc(30,68,7,0,7);g.fill();g.fillStyle='#ffd36a';g.beginPath();g.arc(30,67.5,5,0,7);g.fill();
 g.fillStyle='#000';g.fillText(st.gold,44,74);g.fillStyle='#ffd36a';g.fillText(st.gold,43,73);
 // weapon box
 const bx=VW-210,by=16;g.fillStyle='rgba(10,12,22,.75)';g.fillRect(bx,by,194,st.shard?82:52);g.strokeStyle='rgba(201,151,63,.7)';g.strokeRect(bx+.5,by+.5,193,(st.shard?82:52)-1);
 drawIcon(g,'blade'+st.weapon,bx+24,by+26,1);g.font='15px Georgia';g.fillStyle=['#d8dde6','#ffa050','#ffe08a'][st.weapon];g.fillText(W().name,bx+46,by+23);
 g.font='11px Georgia';g.fillStyle='#8fa3c8';g.fillText(W().type+' · dmg '+W().dmg+(st.eq.band?' · banded':''),bx+46,by+39);
 if(st.shard){drawIcon(g,'shard',bx+24,by+60,.5+.5*Math.abs(Math.sin(T*3)));g.font='14px Georgia';g.fillStyle='#a8e4ff';g.fillText('Aether Shard  ×'+st.ammo,bx+46,by+65);}
 // consumables + acc
 let y=VH-26;g.font='13px Georgia';
 const slot=(k,n,key,x)=>{g.fillStyle='rgba(10,12,22,.75)';g.fillRect(x,y-22,64,36);g.strokeStyle='rgba(201,151,63,.5)';g.strokeRect(x+.5,y-21.5,63,35);drawIcon(g,k,x+18,y-4,n?1:.3);g.fillStyle=n?'#ffe3a6':'#666';g.fillText('×'+n,x+34,y);g.fillStyle='#8fa3c8';g.font='10px Georgia';g.fillText(key,x+4,y-10);g.font='13px Georgia';};
 slot('stem',st.stems,'1',16);slot('seal',st.seals,'2',86);
 let ax=170;for(const k of ['band','lantern','cloak'])if(st.acc[k]){g.globalAlpha=st.eq[k]?1:.3;drawIcon(g,k,ax+12,y-4,1);g.globalAlpha=1;ax+=32;}
 // zone
 g.textAlign='right';g.font='13px Georgia';g.fillStyle='rgba(240,220,174,.7)';g.fillText(Z.D.name,VW-18,VH-14);
 // timers
 for(const k in Z.timers)if(Z.timers[k]>0){g.textAlign='center';g.font='bold 16px Georgia';g.fillStyle='#ffd38a';g.fillText('⧗ Gate open: '+Z.timers[k].toFixed(1)+'s',VW/2,96);}
 // boss bar
 const boss=Z.enemies.find(e=>e.boss&&!e.dead);
 if(boss){const nm={curator:'THE CURATOR CONSTRUCT',guardian:'THE SPIRE GUARDIAN',engine:'THE CORRODED ENGINE'}[boss.type];
  const w=480,x=VW/2-w/2,yy=VH-60;g.fillStyle='rgba(0,0,0,.7)';g.fillRect(x-4,yy-4,w+8,18);
  g.fillStyle=boss.shield?'#5a7ac8':'#c84a2a';g.fillRect(x,yy,w*boss.hp/boss.mhp,10);g.strokeStyle='#c9973f';g.strokeRect(x-4.5,yy-4.5,w+9,19);
  g.textAlign='center';g.font='14px Georgia';g.fillStyle='#ffe3a6';g.fillText(nm+(boss.shield&&boss.type==='engine'?' — WARDED':''),VW/2,yy-10);}
 // banner
 if(banner){const a=clamp(Math.min(banner.life,3.5-banner.life)*2,0,1);g.globalAlpha=a;g.textAlign='center';
  g.fillStyle='rgba(5,7,14,.6)';g.fillRect(0,VH*.22-40,VW,banner.b?80:56);
  g.font='32px Georgia';g.fillStyle='#f3c66b';g.shadowColor='#c8792a';g.shadowBlur=16;g.fillText(banner.a,VW/2,VH*.22);g.shadowBlur=0;
  if(banner.b){g.font='italic 15px Georgia';g.fillStyle='#bfcbe6';g.fillText(banner.b,VW/2,VH*.22+26);}g.globalAlpha=1;}
 // toasts
 g.textAlign='center';toasts.forEach((t,i)=>{g.globalAlpha=clamp(t.life,0,1);g.font='15px Georgia';const w=g.measureText(t.t).width+30;const yy=VH-150-(toasts.length-1-i)*30;
  g.fillStyle='rgba(8,10,20,.75)';g.fillRect(VW/2-w/2,yy-18,w,26);g.fillStyle=t.col;g.fillText(t.t,VW/2,yy);});g.globalAlpha=1;
 // dialog
 if(dialog){const w=640,h=110,x=VW/2-w/2,yy=VH-170;g.fillStyle='rgba(12,15,28,.93)';g.fillRect(x,yy,w,h);g.strokeStyle='#c9973f';g.strokeRect(x+.5,yy+.5,w-1,h-1);
  g.textAlign='left';g.font='italic 16px Georgia';g.fillStyle='#e8d6a8';wrap(g,dialog.text,x+20,yy+30,w-40,22);g.font='11px Georgia';g.fillStyle='#8fa3c8';g.textAlign='right';g.fillText('[E] close',x+w-12,yy+h-10);
  if(dist(dialog.o,P)>90)dialog=null;}
 g.restore();
}
function wrap(g,t,x,y,w,lh){const ws=t.split(' ');let l='';for(const s of ws){if(g.measureText(l+s).width>w){g.fillText(l,x,y);y+=lh;l='';}l+=s+' ';}g.fillText(l,x,y);}
// ---- world object drawing ----
function shadow(g,x,y,rx,ry){g.fillStyle='rgba(0,0,0,.35)';g.beginPath();g.ellipse(x,y,rx,ry,0,0,7);g.fill();}
function drawBridge(g,b){
 const x=b.tx*TS,y=b.ty*TS,w=b.w*TS,h=b.h*TS;
 if(fl(b.flag)){g.fillStyle='#6b4a26';g.fillRect(x,y,w,h);g.fillStyle='#8a6232';for(let i=0;i<b.w*2;i++)g.fillRect(x+i*16+1,y+2,14,h-4);g.fillStyle='#c8862f';g.fillRect(x,y,w,2);g.fillRect(x,y+h-2,w,2);
  g.save();g.globalCompositeOperation='lighter';g.fillStyle=`rgba(150,200,255,${.08+.05*Math.sin(T*3)})`;g.fillRect(x,y,w,h);g.restore();}
 else{g.strokeStyle='rgba(150,200,255,.12)';g.setLineDash([4,6]);g.strokeRect(x+.5,y+.5,w-1,h-1);g.setLineDash([]);}
}
function drawPlate(g,o){
 const d=o.pressed?2:0;g.fillStyle='#2a1e10';g.fillRect(o.x-15,o.y-13,30,28);
 g.fillStyle=o.pressed?'#a67a30':'#c9973f';g.fillRect(o.x-13,o.y-12+d,26,24-d);g.fillStyle='rgba(0,0,0,.25)';g.fillRect(o.x-13,o.y+8,26,4);
 g.strokeStyle='#5a3a14';g.lineWidth=1.5;g.beginPath();g.arc(o.x,o.y+d/2,7,0,7);g.stroke();g.beginPath();g.moveTo(o.x,o.y-7+d/2);g.lineTo(o.x,o.y+7+d/2);g.moveTo(o.x-7,o.y+d/2);g.lineTo(o.x+7,o.y+d/2);g.stroke();
 const active=(o.group&&(fl(o.group)||T-o.last<o.window))||(o.door&&Z.timers[o.door]>0);
 if(active){g.save();g.globalCompositeOperation='lighter';glowDot(g,o.x,o.y,30,'255,190,80',.45);g.restore();
  if(o.group&&!fl(o.group)){const f=1-(T-o.last)/o.window;g.strokeStyle='#ffd38a';g.lineWidth=2;g.beginPath();g.arc(o.x,o.y,18,-1.57,-1.57+f*6.28);g.stroke();}}
}
function drawCrack(g,o){
 if(o.broken){g.fillStyle='rgba(60,50,40,.6)';for(let i=0;i<5;i++)g.fillRect(o.x-12+hash(o.tx,i)*24,o.y-4+hash(i,o.ty)*20,4,3);return;}
 const X=o.tx*TS,Y=o.ty*TS;const pal=Z.D.pal;const sh=o.shk>0?rnd(-2,2):0;
 g.save();g.translate(sh,0);
 g.fillStyle=pal.wt;g.fillRect(X,Y,TS,TS);
 const gr=g.createLinearGradient(0,Y,0,Y+TS);gr.addColorStop(0,pal.wf);gr.addColorStop(1,'#1a120a');g.fillStyle=gr;g.fillRect(X,Y+6,TS,TS-6);
 g.fillStyle='rgba(0,0,0,.35)';for(let i=0;i<3;i++)g.fillRect(X,Y+12+i*7,TS,1);
 const reveal=!o.hidden||st.eq.lantern;
 if(reveal){
  const vis=o.faint&&!st.eq.lantern?.35:1;
  g.fillStyle=`rgba(20,14,8,${.35*vis})`;g.fillRect(X+3,Y+8,TS-6,TS-10); // discoloration
  g.strokeStyle=`rgba(10,6,4,${.9*vis})`;g.lineWidth=1.2;g.beginPath();
  g.moveTo(X+16,Y+7);g.lineTo(X+13,Y+14);g.lineTo(X+18,Y+19);g.lineTo(X+12,Y+27);g.moveTo(X+18,Y+19);g.lineTo(X+25,Y+23);g.moveTo(X+13,Y+14);g.lineTo(X+6,Y+16);
  const dmg=1-o.hp/o.hpMax;if(dmg>0){g.moveTo(X+25,Y+23);g.lineTo(X+29,Y+30);g.moveTo(X+6,Y+16);g.lineTo(X+3,Y+26);g.moveTo(X+12,Y+27);g.lineTo(X+8,Y+31);}
  g.stroke();
  if(st.eq.lantern&&(o.hidden||o.faint)){g.globalCompositeOperation='lighter';g.strokeStyle=`rgba(110,170,255,${.4+.3*Math.sin(T*4)})`;g.lineWidth=1;g.stroke();}
 }
 g.restore();
}
function drawDoor(g,o){
 const X=o.tx*TS,Y=o.ty*TS;
 if(o.open>=.98){g.fillStyle='rgba(0,0,0,.25)';g.fillRect(X+2,Y+2,TS-4,4);return;}
 const h=TS*(1-o.open);
 g.fillStyle='#1b1410';g.fillRect(X,Y,TS,TS);
 const gr=g.createLinearGradient(X,0,X+TS,0);gr.addColorStop(0,'#6b4a26');gr.addColorStop(.5,'#a77a3a');gr.addColorStop(1,'#5a3e20');
 g.fillStyle=gr;g.fillRect(X+2,Y,TS-4,h);
 g.fillStyle='#3a2814';for(let i=4;i<h;i+=8)g.fillRect(X+2,Y+i,TS-4,2);
 const arena=Z.arena&&Z.arena.doors.includes(o.id);
 g.fillStyle=arena?'#c84a2a':o.timed?'#ffd38a':'#5aa0c8';g.globalAlpha=.6+.3*Math.sin(T*4);g.beginPath();g.arc(X+16,Y+Math.min(h,TS)/2,3,0,7);g.fill();g.globalAlpha=1;
}
function drawArenaRing(g,o){g.strokeStyle=`rgba(200,70,40,${.25+.15*Math.sin(T*4)})`;g.lineWidth=3;g.strokeRect(o.rx+4,o.ry+4,o.rw-8,o.rh-8);}
function drawPool(g,p){
 if(p.warn>0){g.strokeStyle=`rgba(200,90,255,${.4+.4*Math.sin(T*20)})`;g.lineWidth=2;g.beginPath();g.ellipse(p.x,p.y,p.mr,p.mr*.6,0,0,7);g.stroke();return;}
 const a=Math.min(1,p.life);g.fillStyle=`rgba(30,10,40,${.8*a})`;g.beginPath();g.ellipse(p.x,p.y,p.r,p.r*.6,0,0,7);g.fill();
 g.fillStyle=`rgba(140,60,200,${.35*a})`;g.beginPath();g.ellipse(p.x,p.y,p.r*.7,p.r*.4,0,0,7);g.fill();
}
function drawObj(g,o){
 const x=o.x,y=o.y,t=T+o.phase;
 switch(o.t){
 case 'stone':{
  shadow(g,x,y+12,18,7);g.fillStyle='#4a4a58';g.beginPath();g.moveTo(x-16,y+12);g.lineTo(x-12,y-6);g.lineTo(x+12,y-6);g.lineTo(x+16,y+12);g.fill();
  g.fillStyle='#6a6a7a';g.fillRect(x-12,y-8,24,4);
  g.fillStyle='#3a3a48';g.beginPath();g.moveTo(x-7,y-8);g.lineTo(x-5,y-34);g.lineTo(x,y-40);g.lineTo(x+5,y-34);g.lineTo(x+7,y-8);g.fill();
  const lit=fl('stone'+o.sid);const p=.6+.4*Math.sin(t*2)+(o.pulse>0?o.pulse:0);
  g.save();g.globalCompositeOperation='lighter';glowDot(g,x,y-22,lit?50:30,'255,200,100',(lit?.45:.18)*p);
  g.strokeStyle=`rgba(255,220,130,${(lit?.9:.4)*p})`;g.lineWidth=1.5;g.beginPath();g.moveTo(x,y-34);g.lineTo(x,y-14);g.moveTo(x-3,y-26);g.lineTo(x+3,y-26);g.arc(x,y-22,4,0,7);g.stroke();g.restore();break;}
 case 'merchant':{
  shadow(g,x,y+14,24,8);
  g.fillStyle='#3a2c1a';g.fillRect(x-22,y-4,44,18);g.fillStyle='#c9973f';g.fillRect(x-22,y-6,44,3);
  // robed figure
  g.fillStyle='#2a2440';g.beginPath();g.moveTo(x-12,y-4);g.lineTo(x-8,y-34);g.lineTo(x+8,y-34);g.lineTo(x+12,y-4);g.fill();
  g.fillStyle='#1a1428';g.beginPath();g.arc(x,y-38,9,0,7);g.fill();
  g.save();g.globalCompositeOperation='lighter';g.fillStyle='#ffd38a';g.fillRect(x-4,y-40,2,2);g.fillRect(x+2,y-40,2,2);
  glowDot(g,x,y-50+Math.sin(t*2)*3,28,'255,200,100',.5);g.restore();
  g.fillStyle='#ffd36a';g.beginPath();g.arc(x,y-50+Math.sin(t*2)*3,4,0,7);g.fill();
  drawIcon(g,'stem',x-14,y+4,.8);drawIcon(g,'seal',x+14,y+4,.8);break;}
 case 'chest':{
  shadow(g,x,y+11,15,5);const big=o.big;const w=big?30:24;
  g.fillStyle=big?'#5a3a14':'#4a3018';g.fillRect(x-w/2,y-6,w,16);g.fillStyle='#c9973f';g.fillRect(x-w/2,y-6,w,2);g.fillRect(x-w/2,y+2,w,2);g.fillRect(x-2,y-2,4,6);
  if(o.opened){g.fillStyle='#2a1a0a';g.fillRect(x-w/2,y-12,w,6);}
  else{g.fillStyle=big?'#6b4a26':'#5a3a20';g.beginPath();g.moveTo(x-w/2,y-6);g.quadraticCurveTo(x,y-18,x+w/2,y-6);g.fill();g.strokeStyle='#c9973f';g.stroke();
   g.save();g.globalCompositeOperation='lighter';glowDot(g,x,y-4,24,'255,210,120',.15+.1*Math.sin(t*3));g.restore();}
  break;}
 case 'note':{g.fillStyle='#3a3226';g.fillRect(x-4,y-6,8,18);g.fillStyle='#d8c8a0';g.save();g.translate(x,y-14);g.rotate(Math.sin(t)*.05);g.fillRect(-9,-8,18,13);g.fillStyle='#6a5a40';for(let i=0;i<3;i++)g.fillRect(-6,-5+i*3.5,12,1);g.restore();
  g.save();g.globalCompositeOperation='lighter';glowDot(g,x,y-14,18,'255,230,170',.15+.08*Math.sin(t*2));g.restore();break;}
 case 'jar':{shadow(g,x,y+8,9,4);g.fillStyle=o.special?'#4a5a7a':'#7a5a3a';g.beginPath();g.ellipse(x,y,8,10,0,0,7);g.fill();g.fillStyle=o.special?'#6a7aa0':'#9a7048';g.fillRect(x-4,y-12,8,4);g.fillStyle='rgba(255,255,255,.12)';g.fillRect(x-5,y-5,2,8);
  if(o.special){g.save();g.globalCompositeOperation='lighter';glowDot(g,x,y,16,'120,180,255',.3);g.restore();}break;}
 case 'statue':{shadow(g,x,y+12,14,5);g.fillStyle='#3a3a40';g.fillRect(x-12,y,24,12);g.fillStyle='#5a5048';g.fillRect(x-7,y-30,14,30);g.beginPath();g.arc(x,y-34,7,0,7);g.fill();g.fillStyle='#2a2830';g.fillRect(x-9,y-26,4,16);
  g.fillStyle='rgba(60,90,50,.5)';g.fillRect(x-7,y-12,6,12);g.fillStyle='#8a6a40';g.fillRect(x+6,y-38,2,40);break;}
 case 'telescope':{shadow(g,x,y+12,18,6);g.fillStyle='#4a3420';g.fillRect(x-2,y-10,4,22);g.fillRect(x-12,y+8,24,4);g.save();g.translate(x,y-12);g.rotate(-.6+Math.sin(t*.2)*.05);
  g.fillStyle='#a77a3a';g.fillRect(-22,-5,40,10);g.fillStyle='#c9973f';g.fillRect(14,-7,6,14);g.fillStyle='#7a5a30';g.fillRect(-24,-3,4,6);
  g.fillStyle='rgba(150,200,255,.5)';g.fillRect(19,-5,2,10);g.restore();break;}
 case 'gear':{g.save();g.translate(x,y);g.rotate(t*.1*(o.tx%2?1:-1));g.globalAlpha=.13;g.strokeStyle=Z.D.pal.accent;g.lineWidth=6;g.beginPath();g.arc(0,0,o.rad,0,7);g.stroke();
  g.lineWidth=3;for(let i=0;i<12;i++){const a=i/12*6.28;g.beginPath();g.moveTo(Math.cos(a)*o.rad,Math.sin(a)*o.rad);g.lineTo(Math.cos(a)*(o.rad+10),Math.sin(a)*(o.rad+10));g.stroke();}
  for(let i=0;i<4;i++){const a=i/4*6.28;g.beginPath();g.moveTo(0,0);g.lineTo(Math.cos(a)*o.rad,Math.sin(a)*o.rad);g.stroke();}g.restore();break;}
 case 'shelf':{g.fillStyle='#3a2414';g.fillRect(x-14,y-30,28,44);for(let i=0;i<3;i++){g.fillStyle='#2a1a0e';g.fillRect(x-12,y-26+i*13,24,10);for(let j=0;j<5;j++){g.fillStyle=['#6a3a2a','#3a4a6a','#6a5a2a','#2a4a3a'][(j+i+o.tx)%4];g.fillRect(x-11+j*4.6,y-25+i*13,3.5,9);}}break;}
 case 'altar':{
  const used=st.weapon>=o.up;const col=o.up===1?'255,130,40':'255,220,110';
  shadow(g,x,y+14,22,7);g.fillStyle='#3a3238';g.fillRect(x-18,y,36,14);g.fillStyle='#5a4a40';g.fillRect(x-14,y-8,28,10);g.fillStyle='#c9973f';g.fillRect(x-14,y-9,28,2);
  if(!used){g.save();g.translate(x,y-26+Math.sin(t*2)*3);drawIcon(g,'blade'+o.up,0,0,1);g.restore();g.save();g.globalCompositeOperation='lighter';glowDot(g,x,y-24,50,col,.4+.15*Math.sin(t*3));g.restore();}
  else{g.save();g.globalCompositeOperation='lighter';glowDot(g,x,y-6,24,col,.2);g.restore();}break;}
 case 'pickup':{const by=y+Math.sin(t*3)*3-6;g.save();g.globalCompositeOperation='lighter';glowDot(g,x,by,34,o.kind==='heart'?'255,120,120':o.kind==='shard'?'130,200,255':'255,255,230',.45);g.restore();
  shadow(g,x,y+8,8,3);drawIcon(g,o.kind,x,by,1);break;}
 case 'switch':{
  const lit=o.lit;const by=y-10+Math.sin(t*2)*2;
  g.strokeStyle='#6a5a3a';g.lineWidth=1;g.beginPath();g.moveTo(x,by-12);g.lineTo(x,by-40);g.stroke();
  g.save();g.globalCompositeOperation='lighter';glowDot(g,x,by,lit?42:24,lit?'150,220,255':'120,100,140',lit?.6:.3+.2*Math.sin(t*4));g.restore();
  g.fillStyle=lit?'#c8f0ff':'#4a4a6a';g.beginPath();g.moveTo(x,by-12);g.lineTo(x+7,by);g.lineTo(x,by+12);g.lineTo(x-7,by);g.closePath();g.fill();g.strokeStyle=lit?'#fff':'#8a8aaa';g.stroke();
  if(!lit&&st.shard){g.strokeStyle=`rgba(150,220,255,${.3+.2*Math.sin(t*5)})`;g.beginPath();g.arc(x,by,14+Math.sin(t*5)*2,0,7);g.stroke();}
  break;}
 case 'mirror':{shadow(g,x,y+10,10,4);g.fillStyle='#4a3420';g.fillRect(x-9,y+4,18,6);g.save();g.translate(x,y-4);g.rotate(o.o==='/'?-Math.PI/4:Math.PI/4);
  g.fillStyle='#c9973f';g.fillRect(-14,-3,28,6);g.fillStyle='#d8eaff';g.fillRect(-12,-1.5,24,3);g.restore();break;}
 case 'emitter':{g.fillStyle='#2a2a3a';g.beginPath();g.arc(x,y,11,0,7);g.fill();g.save();g.globalCompositeOperation='lighter';glowDot(g,x,y,30,'190,220,255',.7);g.restore();g.fillStyle='#fff';g.beginPath();g.arc(x,y,4,0,7);g.fill();break;}
 case 'receiver':{const on=fl(o.flag);g.fillStyle='#2a2a3a';g.fillRect(x-11,y-11,22,22);g.strokeStyle='#c9973f';g.strokeRect(x-11,y-11,22,22);g.save();g.globalCompositeOperation='lighter';glowDot(g,x,y,on?44:18,'150,200,255',on?.8:.25);g.restore();break;}
 case 'beacon':{
  shadow(g,x,y+24,40,12);g.fillStyle='#3a2a20';g.beginPath();g.moveTo(x-34,y+22);g.lineTo(x-22,y-10);g.lineTo(x+22,y-10);g.lineTo(x+34,y+22);g.fill();
  g.fillStyle='#c9973f';g.fillRect(x-24,y-12,48,4);
  const on=Z.engineDead;g.save();g.translate(x,y-34);g.rotate(t*.3);g.strokeStyle=on?'#ffd38a':'#5a4a3a';g.lineWidth=3;g.beginPath();g.arc(0,0,22,0,7);g.stroke();g.rotate(-t*.6);g.beginPath();g.ellipse(0,0,28,10,0,0,7);g.stroke();g.restore();
  g.save();g.globalCompositeOperation='lighter';glowDot(g,x,y-34,on?110:40,on?'255,210,120':'130,60,160',on?.7+.2*Math.sin(t*3):.35);g.restore();
  g.fillStyle=on?'#fff6d8':'#3a2040';g.beginPath();g.arc(x,y-34,9,0,7);g.fill();break;}
 }
}
// ---- player ----
function drawPlayer(g){
 const x=P.x,y=P.y,f=P.face,w=st.weapon;
 const bob=P.moving?Math.abs(Math.sin(P.walk))*2:Math.sin(T*2)*.6;
 const stance=w===2?1.35:st.shard?1.15:1;
 if(P.inv>0&&P.flash<=0&&Math.floor(T*20)%2)g.globalAlpha=.55;
 shadow(g,x,y+10,11*stance,4);
 // aura
 g.save();g.globalCompositeOperation='lighter';
 if(P.aura>0)glowDot(g,x,y-10,60,'255,210,110',Math.min(1,P.aura)*.55);
 if(w===2)glowDot(g,x,y-14,46,'255,215,100',.18+.06*Math.sin(T*3));
 else if(w===1)glowDot(g,x,y-10,30,'255,130,40',.12);
 if(st.eq.lantern)glowDot(g,x-f*9,y-6,26,'100,170,255',.3+.1*Math.sin(T*4));
 g.restore();
 g.save();g.translate(x,y-bob);
 const legS=P.moving?Math.sin(P.walk)*4:0;
 // legs
 g.fillStyle='#2a2430';g.fillRect(-4*stance-1+legS*.5,0,4,10);g.fillRect(4*stance-3-legS*.5,0,4,10);
 // cloak back
 const sway=P.moving?Math.sin(P.walk*.5)*2:Math.sin(T)*1;
 g.fillStyle=st.eq.cloak?'#1e1828':'#5a3a2a';
 g.beginPath();g.moveTo(-8,-18);g.lineTo(-11*stance-sway*f,6);g.lineTo(11*stance-sway*f,6);g.lineTo(8,-18);g.fill();
 // hem
 if(st.eq.cloak){g.fillStyle='#8a6ad0';g.fillRect(-11*stance-sway*f,4,22*stance,2);g.fillStyle='rgba(20,10,40,.7)';for(let i=0;i<4;i++)g.fillRect(-10*stance+i*6*stance-sway*f,5,3,3);}
 else{g.fillStyle='#8a5a30';g.fillRect(-11*stance-sway*f,4,22*stance,2);}
 if(w===2){g.save();g.globalCompositeOperation='lighter';g.strokeStyle=`rgba(255,220,120,${.5+.4*Math.sin(T*6)})`;g.lineWidth=1.5;g.beginPath();g.moveTo(-11*stance-sway*f,6);g.lineTo(-8,-18);g.moveTo(11*stance-sway*f,6);g.lineTo(8,-18);g.stroke();g.restore();}
 // body
 g.fillStyle='#3a4a6a';g.fillRect(-6,-18,12,16);g.fillStyle='#c9973f';g.fillRect(-6,-6,12,2);
 // shoulder clasp (cloak)
 if(st.eq.cloak){g.fillStyle='#c9973f';g.beginPath();g.arc(-6*f,-17,3,0,7);g.fill();g.fillStyle='#8a6ad0';g.beginPath();g.arc(-6*f,-17,1.5,0,7);g.fill();}
 // hood/head
 g.fillStyle='#4a3428';g.beginPath();g.arc(0,-23,7,0,7);g.fill();
 g.fillStyle='#1a1418';g.beginPath();g.arc(f*2,-22,4.5,0,7);g.fill();
 g.fillStyle=w===2?'#ffe08a':w===1?'#ff9a40':'#bcd0f0';g.fillRect(f*2+f*1-1,-23,2,1.5);
 // off-hand: crystal or lantern
 const ohx=-f*9,ohy=-8;
 if(st.shard){const p=.6+.4*Math.sin(T*5);g.save();g.globalCompositeOperation='lighter';glowDot(g,ohx,ohy-2,16,'130,200,255',.6*p);g.restore();
  g.fillStyle=`rgba(170,230,255,${.8+.2*p})`;g.beginPath();g.moveTo(ohx,ohy-9);g.lineTo(ohx+3.5,ohy-2);g.lineTo(ohx,ohy+4);g.lineTo(ohx-3.5,ohy-2);g.fill();}
 if(st.eq.lantern){g.fillStyle='#4a3a20';g.fillRect(ohx-3,ohy+4,6,2);g.fillStyle='rgba(120,190,255,.9)';g.fillRect(ohx-2.5,ohy+6,5,6);}
 // hand/arm
 g.fillStyle='#3a4a6a';g.fillRect(ohx-2,-14,4,6);
 // band on wrist
 if(st.eq.band){g.save();g.globalCompositeOperation='lighter';glowDot(g,f*8,-10,8,'220,230,255',.6);g.restore();g.fillStyle='#e6ecf5';g.fillRect(f*8-3,-11,6,2.5);}
 g.restore();
 // blade
 drawBlade(g,x,y-bob-10);
 g.globalAlpha=1;
 if(P.flash>0){g.save();g.globalCompositeOperation='lighter';glowDot(g,x,y-10,34,'255,30,30',P.flash*2);g.restore();}
 // wind-up glow
 if(P.windT>0){g.save();g.globalCompositeOperation='lighter';glowDot(g,x+Math.cos(P.fa)*12,y-10+Math.sin(P.fa)*12,40,'255,220,110',.7);g.restore();}
}
function drawBlade(g,x,y){
 const w=st.weapon,Wd=W();
 const col=['#dfe4ee','#ffa04a','#ffe08a'][w];
 let ang;
 const len=w===2?30:w===1?24:22;
 if(P.swingT>0){
  const p=1-P.swingT/P.swingDur;const side=P.swingSide||1;
  ang=P.swingA+side*(-Wd.arc/2+Wd.arc*p);
  // arc trail
  g.save();g.globalCompositeOperation='lighter';
  const a0=P.swingA+side*(-Wd.arc/2),a1=ang;
  for(let i=0;i<3;i++){g.strokeStyle=`rgba(${Wd.col},${.5-i*.14})`;g.lineWidth=w===2?10-i*3:6-i*1.5;g.beginPath();g.arc(x,y,Wd.range-6-i*5,Math.min(a0,a1),Math.max(a0,a1));g.stroke();}
  g.restore();
 } else if(P.windT>0){ang=P.fa+Math.PI*.85*(P.swingSide?-P.swingSide:1);}
 else{ang=P.fa+(P.face>0?.9:-.9)*(w===2?1.2:1);}
 const hx=x+Math.cos(P.fa)*8*(w===2?1.3:1),hy=y+Math.sin(P.fa)*4;
 g.save();g.translate(hx,hy);g.rotate(ang);
 if(w>=1){g.globalCompositeOperation='lighter';const gr=g.createLinearGradient(0,0,len,0);gr.addColorStop(0,`rgba(${Wd.col},0)`);gr.addColorStop(1,`rgba(${Wd.col},${w===2?.55:.4})`);g.strokeStyle=gr;g.lineWidth=w===2?10:7;g.beginPath();g.moveTo(4,0);g.lineTo(len,0);g.stroke();g.globalCompositeOperation='source-over';}
 g.fillStyle='#8a6a30';g.fillRect(-1,-4,3,8);g.fillStyle='#4a3420';g.fillRect(-6,-1.5,6,3);
 g.fillStyle=col;g.beginPath();g.moveTo(2,-2-(w===2?1:0));g.lineTo(len-4,-1.6-(w===2?1:0));g.lineTo(len,0);g.lineTo(len-4,1.6+(w===2?1:0));g.lineTo(2,2+(w===2?1:0));g.fill();
 g.restore();
 if(w>=1&&Math.random()<.35){const tx=hx+Math.cos(ang)*rnd(6,len),ty=hy+Math.sin(ang)*rnd(6,len);part(tx,ty,{vx:rnd(-8,8),vy:rnd(-30,-10),life:rnd(.3,.6),size:rnd(.8,1.8),col:w===2?'255,225,130':'255,130,40',glow:1});}
}
// ---- enemies ----
function drawEnemy(g,e){
 const x=e.x,y=e.y,t=e.ph,hurt=e.hurt>0;
 g.save();
 if(!e.fly||e.type==='curator')shadow(g,x,y+e.r*.6,e.r*.9,e.r*.35);
 const tele=e.state==='tele';
 const metal=hurt?'#ffdcc0':'#4a3a34',rust='#6a3a20',eye=e.frozen>0?'150,210,255':'255,90,40';
 if(e.type==='husk'){
  const by=Math.sin(t*6)*1.5;const s=tele?1+.1*Math.sin(t*40):1;g.translate(x,y+by);g.scale(s,s);
  g.fillStyle='#2a2228';g.fillRect(-6,2,4,9);g.fillRect(2,2,4,9);
  g.fillStyle=metal;g.beginPath();g.moveTo(-10,4);g.lineTo(-8,-14);g.lineTo(8,-14);g.lineTo(10,4);g.fill();
  g.fillStyle=rust;g.fillRect(-8,-8,16,3);g.fillStyle='#1a1418';g.fillRect(-6,-22,12,9);g.fillStyle=hurt?'#fff':'#5a4a40';g.fillRect(-7,-24,14,3);
  glowEye(g,0,-18,eye,tele?1:.7);
  g.strokeStyle='#5a4a40';g.lineWidth=2;g.beginPath();g.moveTo(9,-8);g.lineTo(9+Math.cos(e.fa)*12,-8+Math.sin(e.fa)*12);g.stroke();
 } else if(e.type==='wisp'){
  const by=Math.sin(t*3)*5-14;g.translate(x,y+by);
  g.save();g.globalCompositeOperation='lighter';glowDot(g,0,0,24,'180,90,255',.45);g.restore();
  g.save();g.rotate(t*2);g.strokeStyle=hurt?'#fff':'#a88a50';g.lineWidth=2;g.beginPath();g.ellipse(0,0,13,5,0,0,7);g.stroke();g.rotate(1.2);g.beginPath();g.ellipse(0,0,13,5,0,0,7);g.stroke();g.restore();
  g.fillStyle=hurt?'#fff':'#2a1838';g.beginPath();g.arc(0,0,6,0,7);g.fill();glowEye(g,0,0,e.frozen>0?eye:'220,120,255',1);
  shadow(g,0,-by+6,8,3);
 } else if(e.type==='sentinel'){
  g.translate(x,y);const s=tele?1+.06*Math.sin(t*30):1;g.scale(s,s);
  g.fillStyle='#2a2228';g.fillRect(-8,4,6,10);g.fillRect(2,4,6,10);
  g.fillStyle=metal;g.fillRect(-11,-18,22,24);g.fillStyle=rust;g.fillRect(-11,-4,22,3);g.fillStyle='#5a4a40';g.fillRect(-8,-30,16,12);
  glowEye(g,0,-24,eye,1);
  // shield
  const sa=e.fa;g.save();g.rotate(sa);g.fillStyle=hurt?'#fff':'#7a5a30';g.fillRect(12,-14,6,28);g.fillStyle='#c9973f';g.fillRect(13,-12,4,24);g.restore();
  if(tele){g.save();g.globalCompositeOperation='lighter';g.strokeStyle='rgba(255,120,50,.5)';g.lineWidth=2;g.beginPath();g.arc(0,0,74*(1-e.t/.7),0,7);g.stroke();g.restore();}
 } else if(e.type==='brute'){
  g.translate(x,y);const s=tele?1+.08*Math.sin(t*40):1;g.scale(s,s);
  g.fillStyle='#2a2228';g.fillRect(-12,6,8,12);g.fillRect(4,6,8,12);
  g.fillStyle=metal;g.beginPath();g.moveTo(-18,8);g.lineTo(-16,-22);g.lineTo(16,-22);g.lineTo(18,8);g.fill();
  g.fillStyle=rust;g.fillRect(-16,-10,32,4);g.fillStyle='#3a2a24';g.fillRect(-22,-20,8,22);g.fillRect(14,-20,8,22);
  g.fillStyle='#1a1418';g.fillRect(-7,-32,14,10);glowEye(g,-3,-27,eye,1);glowEye(g,3,-27,eye,1);
  if(tele){g.strokeStyle='rgba(255,90,40,.5)';g.lineWidth=3;g.beginPath();g.moveTo(0,0);g.lineTo(Math.cos(e.la)*120,Math.sin(e.la)*120);g.stroke();}
  if(e.stun>0){for(let i=0;i<3;i++){const a=T*5+i*2.1;g.fillStyle='#ffd38a';g.fillRect(Math.cos(a)*12-1.5,-40+Math.sin(a)*4,3,3);}}
 } else if(e.type==='curator'){
  const by=Math.sin(t*2)*6-20;g.translate(x,y+by);
  g.save();g.globalCompositeOperation='lighter';glowDot(g,0,0,60,'255,120,60',.3);g.restore();
  g.save();g.rotate(t*.8);g.strokeStyle=hurt?'#fff':'#c9973f';g.lineWidth=3;g.beginPath();g.arc(0,0,26,0,7);g.stroke();
  for(let i=0;i<8;i++){g.rotate(Math.PI/4);g.fillStyle='#8a6a30';g.fillRect(24,-3,8,6);}g.restore();
  g.save();g.rotate(-t*.5);g.strokeStyle='#6a5a40';g.lineWidth=2;g.beginPath();g.ellipse(0,0,36,12,0,0,7);g.stroke();g.restore();
  g.fillStyle=hurt?'#fff':'#2a1a20';g.beginPath();g.arc(0,0,14,0,7);g.fill();
  // lens eye
  g.fillStyle='#4a2a2a';g.beginPath();g.arc(Math.cos(e.fa)*4,Math.sin(e.fa)*4,8,0,7);g.fill();glowEye(g,Math.cos(e.fa)*5,Math.sin(e.fa)*5,eye,1.5);
 } else if(e.type==='guardian'){
  g.translate(x,y);if(e.state==='spin')g.rotate(e.spin||0);
  const s=e.state==='tele'?1+.06*Math.sin(t*40):1;g.scale(s,s);
  g.save();g.globalCompositeOperation='lighter';glowDot(g,0,-10,56,'255,170,60',.25);g.restore();
  g.fillStyle='#2a2228';g.fillRect(-12,6,8,14);g.fillRect(4,6,8,14);
  g.fillStyle=hurt?'#fff':'#5a4430';g.beginPath();g.moveTo(-18,8);g.lineTo(-14,-26);g.lineTo(14,-26);g.lineTo(18,8);g.fill();
  g.fillStyle='#c9973f';g.fillRect(-14,-26,28,4);g.fillRect(-2,-22,4,26);
  g.fillStyle='#3a2a20';g.beginPath();g.moveTo(-10,-26);g.lineTo(0,-46);g.lineTo(10,-26);g.fill();
  glowEye(g,0,-32,eye,1.4);
  // twin blades
  g.strokeStyle='#ffd38a';g.lineWidth=3;for(const sd of [-1,1]){g.beginPath();g.moveTo(sd*18,-10);g.lineTo(sd*34,-26+Math.sin(t*4)*4);g.stroke();}
  if(e.state==='tele'){g.strokeStyle='rgba(255,200,90,.45)';g.lineWidth=4;g.beginPath();g.moveTo(0,0);g.lineTo(Math.cos(e.la)*170,Math.sin(e.la)*170);g.stroke();}
 } else if(e.type==='engine'){
  g.translate(x,y-10);
  g.save();g.globalCompositeOperation='lighter';glowDot(g,0,0,110,e.shield?'90,120,255':'255,120,60',.35);g.restore();
  for(let k=0;k<3;k++){g.save();g.rotate(t*(k%2?-.4:.6)*(1+k*.3));g.strokeStyle=hurt?'#fff':['#6a4a2a','#8a6a30','#c9973f'][k];g.lineWidth=6-k;g.beginPath();g.arc(0,0,48-k*12,0,7);g.stroke();
   for(let i=0;i<10;i++){g.rotate(Math.PI/5);g.fillStyle='#3a2a20';g.fillRect(46-k*12,-4,10,8);}g.restore();}
  g.fillStyle='#1a0e1e';g.beginPath();g.arc(0,0,18,0,7);g.fill();
  g.save();g.globalCompositeOperation='lighter';glowDot(g,0,0,30,'200,80,255',.9);g.restore();
  // corrosion tendrils
  g.strokeStyle='rgba(30,10,40,.8)';g.lineWidth=4;for(let i=0;i<6;i++){const a=i/6*6.28+t*.2;g.beginPath();g.moveTo(Math.cos(a)*40,Math.sin(a)*40);g.quadraticCurveTo(Math.cos(a+.3)*70,Math.sin(a+.3)*70,Math.cos(a)*90+Math.sin(t+i)*8,Math.sin(a)*90);g.stroke();}
  if(e.shield){g.strokeStyle=`rgba(120,170,255,${.5+.3*Math.sin(t*5)})`;g.lineWidth=3;g.beginPath();g.arc(0,0,58,0,7);g.stroke();
   for(const s of Z.objs)if(s.core&&!s.lit){g.strokeStyle='rgba(120,170,255,.25)';g.lineWidth=2;g.beginPath();g.moveTo(0,0);g.lineTo(s.x-x,s.y-y-10+10);g.stroke();}}
 }
 g.restore();
 if(e.frozen>0){g.save();g.globalCompositeOperation='lighter';glowDot(g,x,y-10,e.r*2,'130,190,255',.5);g.strokeStyle='rgba(180,220,255,.8)';g.lineWidth=1.5;g.beginPath();g.arc(x,y-10,e.r+6,T*3,T*3+4.5);g.stroke();g.restore();}
 if(!e.boss&&e.hp<e.mhp){g.fillStyle='rgba(0,0,0,.6)';g.fillRect(x-14,y-e.r-26,28,4);g.fillStyle='#c84a2a';g.fillRect(x-14,y-e.r-26,28*e.hp/e.mhp,4);}
}
function glowEye(g,x,y,col,s){g.save();g.globalCompositeOperation='lighter';glowDot(g,x,y,8*s,col,.9);g.fillStyle=`rgb(${col})`;g.fillRect(x-1.5,y-1,3,2);g.restore();}

// ================= LOOP & BOOT =================
let last=performance.now();
function loop(now){
 const dt=Math.min(.05,(now-last)/1000);last=now;
 if(mode==='play'&&!paused)update(dt);
 else if(Z&&(mode==='dead'||mode==='end')){updParts(dt);T+=dt;}
 if(Z)render();
 requestAnimationFrame(loop);
}
function startNew(){aInit();st=newState();savedStr=null;try{localStorage.removeItem(SAVE_KEY);}catch(e){}
 loadZone(0);$('title').classList.remove('show');mode='play';paused=false;
 setTimeout(()=>toast('WASD to move · J / Space / Click to strike · E to interact','#cdbb92'),800);}
function cont(){aInit();const s=getSave();if(!s){startNew();return;}st=JSON.parse(s);st.hp=maxHp();
 loadZone(st.stone.z,{stone:1,x:st.stone.x,y:st.stone.y+1.3});$('title').classList.remove('show');mode='play';paused=false;}
$('bNew').onclick=startNew;$('bCont').onclick=cont;
$('bRevive').onclick=revive;$('bAgain').onclick=()=>{$('end').classList.remove('show');startNew();};
if(!getSave())$('bCont').disabled=true;
// backdrop for title
loadZone(0);P.inv=99;
requestAnimationFrame(loop);
</script>
</body></html>
<!-- agent-meta {"model":"anthropic/claude-opus-5.5","provider":"openrouter","persona":"full","sessionId":"4c0add2c-c40f-47fe-b04f-8d8894251ede","tokensIn":1281766,"tokensOut":200738,"tokensTotal":1482504,"cost":4.881384,"turns":21,"toolCalls":24,"failedToolCalls":0,"timestamp":"2026-10-02T16:12:44.921Z"} -->