← Warden results

Warden

Qwen 3.8 27B GGUF · 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 98,035 bytes · SHA-256 399290e66fd3
<!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>
:root{
  --gold:#d9a441; --gold2:#f0d9a0; --bronze:#8a6a3f;
  --ink:#070a10; --panel1:#141b28; --panel2:#0b1018;
  --red:#c0452e; --blue:#5aa8c8; --text:#d8d2c4; --dim:#9a8f78;
}
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%}
body{
  background:radial-gradient(120% 100% at 50% 0%, #0c1119 0%, #05070b 70%);
  color:var(--text); font-family:Georgia,'Times New Roman',serif;
  display:flex;align-items:center;justify-content:center;min-height:100vh;
  overflow:hidden;
}
#wrap{
  position:relative; width:min(1080px, 97vw, calc(96vh * 16 / 9));
  aspect-ratio:16/9;
}
canvas{
  width:100%;height:100%;display:block;background:#070a10;
  border:1px solid #33291a;
  box-shadow:0 0 60px rgba(0,0,0,.85), 0 0 24px rgba(217,164,65,.06) inset;
}
/* ---------- zone banner ---------- */
#banner{
  position:absolute; top:13%; left:0; width:100%; text-align:center;
  pointer-events:none; opacity:0; transition:opacity .55s ease; z-index:5;
}
#banner.show{opacity:1}
#banner h2{
  font-size:30px; font-weight:normal; color:#ecc984; letter-spacing:.28em;
  text-transform:uppercase; text-shadow:0 0 26px rgba(217,164,65,.5);
}
#banner p{color:#b9a98c;font-style:italic;margin-top:10px;font-size:14.5px;letter-spacing:.04em;
  text-shadow:0 1px 8px #000}
/* ---------- overlays ---------- */
.ov{
  position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
  background:rgba(3,5,9,.68); z-index:20;
}
.ov[hidden]{display:none}
.panel{
  width:min(620px, 90%); max-height:94%; overflow-y:auto; text-align:center;
  padding:26px 34px 24px;
  background:linear-gradient(165deg, rgba(21,28,42,.98), rgba(9,13,20,.98));
  border:1px solid #57482a;
  box-shadow:0 0 70px rgba(0,0,0,.85), inset 0 0 90px rgba(0,0,0,.45);
}
.panel::-webkit-scrollbar{width:8px}
.panel::-webkit-scrollbar-thumb{background:#3a3226}
.overline{color:#8d7a52;letter-spacing:.34em;font-size:11px;text-transform:uppercase}
h1{
  font-size:40px; font-weight:normal; color:#ecd09a; letter-spacing:.1em;
  margin:10px 0 14px; text-shadow:0 0 24px rgba(217,164,65,.4);
}
h1.small{font-size:28px;margin-bottom:10px}
.lore{color:#b7ad97;font-size:14.5px;line-height:1.6;max-width:470px;margin:0 auto 20px;font-style:italic}
.btnrow{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;margin:4px 0 18px}
.btn{
  background:#1a2230; border:1px solid #6a5a33; color:#e8d9b0;
  padding:10px 22px; font:15px Georgia,serif; letter-spacing:.08em; cursor:pointer;
  transition:all .16s ease;
}
.btn:hover{background:#243048;border-color:var(--gold);box-shadow:0 0 14px rgba(217,164,65,.25);color:#f7e7bd}
.btn.primary{border-color:#a8823f;background:#241f12;color:#f2dfa8}
.btn.small{padding:5px 14px;font-size:13px}
.btn:disabled{opacity:.35;cursor:default;box-shadow:none}
.btn:disabled:hover{background:#1a2230;border-color:#6a5a33;color:#e8d9b0}
.ctrls{
  display:grid;grid-template-columns:auto auto;gap:5px 20px;justify-content:center;
  font-size:12.5px;color:var(--dim);text-align:left;margin-top:6px;
}
kbd{
  background:#1a2230;border:1px solid #4a3d24;padding:1px 7px;border-radius:3px;
  font:12px Georgia,serif;color:#cfc2a0;display:inline-block;min-width:14px;text-align:center
}
.gold-line{color:#e8c67a;font-size:15px;margin:8px 0 2px;letter-spacing:.05em}
.row{
  display:flex;align-items:center;justify-content:space-between;gap:14px;text-align:left;
  padding:9px 13px;border:1px solid #2c3547;border-radius:4px;margin:8px 0;
  background:rgba(15,21,33,.65);
}
.row .nm{color:#e6d3a3;font-size:14.5px;letter-spacing:.04em}
.row .ds{color:#8f8672;font-size:12px;margin-top:2px;line-height:1.35}
.price{color:#e8c67a;font-size:14px;white-space:nowrap}
.tag{
  display:inline-block;font-size:9.5px;letter-spacing:.18em;text-transform:uppercase;
  padding:2px 8px;border:1px solid #4a3d24;color:#a8956a;border-radius:2px;
  margin-left:8px;vertical-align:2px;
}
.tag.on{color:#8fd4a5;border-color:#3d5a46}
.tag.off{color:#6a6152}
.mnote{color:#6f6857;font-size:12px;margin-top:14px;letter-spacing:.06em}
.stats{
  display:grid;grid-template-columns:auto auto;gap:6px 26px;justify-content:center;
  font-size:14px;color:#cfc2a0;margin:12px auto 18px;text-align:left;
}
.stats b{color:#e8c67a;font-weight:normal;text-align:right}
.foot{color:#6f6857;font-size:11.5px;margin-top:16px;letter-spacing:.1em}
</style>
</head>
<body>
<div id="wrap">
  <canvas id="cv" width="960" height="540"></canvas>
  <div id="banner"><h2></h2><p></p></div>

  <!-- TITLE -->
  <div class="ov" id="ovTitle">
    <div class="panel">
      <div class="overline">Aurelion's Sunken Observatory</div>
      <h1>The Warden's Corrosion</h1>
      <p class="lore">The sky-wheel has stopped. Corrosion — a black-iron hunger — climbs the
      bronze halls, and the archive drifts toward the storm-locked valley below.
      You are the <b>Last Warden</b>. Rekindle the Core Beacon, or all that was kept is lost.</p>
      <div class="btnrow">
        <button class="btn primary" id="btnNew">Begin the Vigil</button>
        <button class="btn" id="btnCont" hidden>Continue</button>
      </div>
      <div class="ctrls">
        <span><kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd> / Arrows</span><span>Move</span>
        <span><kbd>J</kbd> or <kbd>Z</kbd> / Click</span><span>Strike</span>
        <span><kbd>K</kbd> or <kbd>X</kbd></span><span>Aether Shard (when found)</span>
        <span><kbd>E</kbd></span><span>Interact</span>
        <span><kbd>Tab</kbd> / <kbd>I</kbd></span><span>Warden's Kit</span>
        <span><kbd>1</kbd> / <kbd>2</kbd></span><span>Vital Stem / Binding Seal</span>
        <span><kbd>P</kbd> / <kbd>Esc</kbd></span><span>Pause</span>
        <span><kbd>M</kbd></span><span>Sound on / off</span>
      </div>
      <div class="foot">A melancholy of bronze and brass &mdash; a keyboard is required.</div>
    </div>
  </div>

  <!-- PAUSE -->
  <div class="ov" id="ovPause" hidden>
    <div class="panel">
      <div class="overline">The Vigil Pauses</div>
      <h1 class="small">Rest, Warden</h1>
      <p class="lore">Outside, the storm keeps its counsel. Your progress is kept at the last Sanctuary Stone.</p>
      <div class="btnrow">
        <button class="btn primary" id="btnResume">Resume</button>
        <button class="btn" id="btnMute">Sound: On</button>
        <button class="btn" id="btnAbandon">Abandon to Title</button>
      </div>
      <div class="ctrls">
        <span><kbd>WASD</kbd></span><span>Move</span>
        <span><kbd>J</kbd></span><span>Strike</span>
        <span><kbd>K</kbd></span><span>Aether Shard</span>
        <span><kbd>E</kbd></span><span>Interact</span>
        <span><kbd>Tab</kbd></span><span>Kit</span>
        <span><kbd>1</kbd>/<kbd>2</kbd></span><span>Use supplies</span>
      </div>
    </div>
  </div>

  <!-- INVENTORY -->
  <div class="ov" id="ovInv" hidden>
    <div class="panel">
      <div class="overline">The Warden's Kit</div>
      <h1 class="small">What the Vigil Holds</h1>
      <div id="invBody"></div>
      <div class="btnrow"><button class="btn" id="btnInvClose">Close <span class="tag off">Tab</span></button></div>
    </div>
  </div>

  <!-- SHOP -->
  <div class="ov" id="ovShop" hidden>
    <div class="panel">
      <div class="overline">The Merchant of the Archives</div>
      <h1 class="small">The Corrosion Hastens&hellip;</h1>
      <p class="lore" id="shopLine">"A Warden, at my door. The vaults are full of what you'll need &mdash; and pricier than the valley below."</p>
      <div class="gold-line" id="shopGold"></div>
      <div id="shopBody"></div>
      <div class="btnrow"><button class="btn" id="btnShopClose">Leave <span class="tag off">Esc</span></button></div>
    </div>
  </div>

  <!-- VICTORY -->
  <div class="ov" id="ovWin" hidden>
    <div class="panel">
      <div class="overline">Dawn Over the Valley</div>
      <h1 class="small">The Beacon Kindled</h1>
      <p class="lore">Light climbs the spire through the broken dome, and for one held breath the
      storm remembers its manners. The Corrosion stills. Aurelion's archive endures &mdash;
      and so does its Warden.</p>
      <div class="stats" id="winStats"></div>
      <div class="btnrow"><button class="btn primary" id="btnWinTitle">Return to Title</button></div>
    </div>
  </div>
</div>

<script>
'use strict';
/* ================================================================
   THE WARDEN'S CORROSION — single-file action-adventure
   ================================================================ */
const T=32, CW=960, CH=540;
const clamp=(v,a,b)=>v<a?a:(v>b?b:v);
const lerp=(a,b,t)=>a+(b-a)*t;
const rnd=(a,b)=>a+Math.random()*(b-a);
const ri=(a,b)=>Math.floor(a+Math.random()*(b-a+1));
const dist=(ax,ay,bx,by)=>Math.hypot(bx-ax,by-ay);
const angN=a=>{while(a>Math.PI)a-=2*Math.PI;while(a<-Math.PI)a+=2*Math.PI;return a;};
const angDiff=(a,b)=>angN(a-b);
const ease=t=>t<0?0:(t>1?1:t*t*(3-2*t));
const $=id=>document.getElementById(id);

/* ================= AUDIO (all synthesized) ================= */
const AudioSys={
  ctx:null, master:null, muted:false, drone:null,
  init(){
    if(this.ctx) return;
    const C=window.AudioContext||window.webkitAudioContext;
    if(!C) return;
    this.ctx=new C();
    this.master=this.ctx.createGain();
    this.master.gain.value=this.muted?0:0.8;
    const comp=this.ctx.createDynamicsCompressor();
    this.master.connect(comp); comp.connect(this.ctx.destination);
  },
  resume(){ if(this.ctx&&this.ctx.state==='suspended') this.ctx.resume(); },
  setMuted(m){ this.muted=m; if(this.master) this.master.gain.value=m?0:0.8; },
  t(){ return this.ctx?this.ctx.currentTime:0; },
  env(g,t0,a,peak,dur){
    g.gain.setValueAtTime(0.0001,t0);
    g.gain.linearRampToValueAtTime(peak,t0+a);
    g.gain.exponentialRampToValueAtTime(0.0001,t0+dur);
  },
  osc(type,f0,f1,dur,peak,t0,dest){
    if(!this.ctx||this.muted) return;
    t0=t0||this.t();
    const o=this.ctx.createOscillator(), g=this.ctx.createGain();
    o.type=type; o.frequency.setValueAtTime(Math.max(1,f0),t0);
    if(f1) o.frequency.exponentialRampToValueAtTime(Math.max(1,f1),t0+dur);
    this.env(g,t0,0.006,peak,dur);
    o.connect(g); g.connect(dest||this.master);
    o.start(t0); o.stop(t0+dur+0.06);
  },
  noise(dur,peak,ftype,f0,f1,q,t0){
    if(!this.ctx||this.muted) return;
    t0=t0||this.t();
    const n=this.ctx.createBufferSource();
    const len=Math.max(16,(dur*this.ctx.sampleRate)|0);
    const b=this.ctx.createBuffer(1,len,this.ctx.sampleRate);
    const d=b.getChannelData(0);
    for(let i=0;i<len;i++) d[i]=Math.random()*2-1;
    n.buffer=b;
    const f=this.ctx.createBiquadFilter(); f.type=ftype||'lowpass';
    f.frequency.setValueAtTime(f0||800,t0);
    if(f1) f.frequency.exponentialRampToValueAtTime(Math.max(20,f1),t0+dur);
    f.Q.value=q||0.8;
    const g=this.ctx.createGain();
    this.env(g,t0,0.004,peak,dur);
    n.connect(f); f.connect(g); g.connect(this.master);
    n.start(t0); n.stop(t0+dur+0.06);
  },
  arp(freqs,gap,dur,peak,type){
    freqs.forEach((f,i)=>this.osc(type||'sine',f,null,dur,peak,this.t()+i*gap));
  },
  sfx(name, arg){
    if(!this.ctx||this.muted) return;
    const t=this.t();
    switch(name){
      case 'step':{
        const z=arg||0;
        if(z===1){ this.noise(0.05,0.045,'highpass',900,null,1); this.osc('triangle',180+rnd(-30,30),140,0.05,0.028); }
        else if(z===3){ this.noise(0.09,0.05,'lowpass',520,160); }
        else { this.noise(0.05,0.032,'lowpass',720,280); }
        break; }
      case 'whoosh':{
        const w=arg||0;
        if(w===0){ this.noise(0.11,0.08,'bandpass',520,1250,1.4); }
        else if(w===1){ this.noise(0.1,0.08,'bandpass',600,1400,1.4); this.noise(0.08,0.05,'highpass',2600,null,1); }
        else { this.noise(0.22,0.1,'bandpass',240,700,1); this.osc('sine',72,48,0.26,0.06); }
        break; }
      case 'impact':{
        const w=arg||0;
        if(w===0){ this.osc('triangle',110,55,0.09,0.16); this.noise(0.06,0.1,'lowpass',420,200); }
        else if(w===1){ this.osc('triangle',100,50,0.11,0.17); this.noise(0.07,0.1,'lowpass',380,180); this.noise(0.09,0.06,'highpass',2400,null,1); }
        else { this.osc('sine',62,36,0.32,0.26); this.osc('triangle',920,1500,0.12,0.05); this.noise(0.16,0.14,'lowpass',260,120); }
        break; }
      case 'shoot':{ this.osc('square',660,190,0.07,0.08); this.osc('sine',1900,1400,0.06,0.035); break; }
      case 'chime':{ this.osc('sine',1250,900,0.24,0.08); this.osc('sine',2500,2000,0.16,0.03); break; }
      case 'hurt':{
        this.osc('triangle',92,44,0.16,0.24);
        this.osc('sine',1180,1180,0.2,0.05); this.osc('sine',1246,1246,0.2,0.05);
        break; }
      case 'coin':{ this.osc('square',1500,1500,0.05,0.05); this.osc('square',2100,2100,0.07,0.05,t+0.05); break; }
      case 'item':{ this.arp([660,880,1320],0.07,0.12,0.07); break; }
      case 'rare':{
        this.arp([523,659,784],0.02,0.4,0.06);
        this.arp([1568,2093],0.1,0.2,0.04); break; }
      case 'heart':{ this.arp([523,659,784,1046],0.09,0.22,0.08); this.noise(0.4,0.03,'highpass',3000,null,1); break; }
      case 'shimmer':{ this.osc('sine',1046,2093,0.3,0.06); break; }
      case 'altar':{
        this.osc('sawtooth',82,52,0.6,0.07);
        this.arp([392,494,587],0.02,0.7,0.06);
        this.arp([1046,1318,1568],0.12,0.25,0.04); break; }
      case 'save':{ this.arp([330,415,494],0.02,0.6,0.06); this.osc('sine',990,880,0.4,0.03, t+0.25); break; }
      case 'break':{ this.noise(0.2,0.2,'lowpass',900,180); this.osc('triangle',120,58,0.12,0.1); break; }
      case 'tick':{ this.noise(0.05,0.06,'lowpass',1300,500); break; }
      case 'plate':{ this.osc('square',150,88,0.09,0.12); this.noise(0.06,0.08,'lowpass',320,140); break; }
      case 'door':{ this.noise(0.55,0.12,'lowpass',620,140); this.osc('sine',55,48,0.55,0.08); break; }
      case 'seal':{ this.osc('sine',1800,820,0.22,0.09); this.noise(0.18,0.05,'highpass',2800,null,1); break; }
      case 'block':{ this.osc('triangle',210,120,0.08,0.1); this.noise(0.05,0.06,'highpass',2200,null,1); break; }
      case 'boss':{ this.osc('sawtooth',72,38,0.55,0.14); this.noise(0.4,0.1,'lowpass',420,120); break; }
      case 'charge':{ this.noise(0.3,0.08,'bandpass',300,950,1.2); this.osc('sawtooth',60,130,0.3,0.05); break; }
      case 'ebolt':{ this.osc('sine',310,170,0.12,0.06); break; }
      case 'die':{ this.osc('triangle',150,48,0.16,0.12); this.noise(0.12,0.09,'lowpass',520,160); break; }
      case 'death':{ this.osc('sawtooth',220,38,0.9,0.15); this.noise(0.6,0.1,'lowpass',600,80); break; }
      case 'victory':{ this.arp([392,523,659,784,1046,1318],0.15,0.5,0.09); this.noise(1.2,0.04,'highpass',3000,null,1); break; }
      case 'buy':{ this.osc('square',920,300,0.09,0.07); this.osc('sine',1568,1568,0.18,0.08,t+0.1); break; }
      case 'empty':{ this.osc('sine',240,220,0.09,0.05); break; }
    }
  },
  droneOn(){
    if(!this.ctx||this.muted||this.drone) return;
    const g=this.ctx.createGain(); g.gain.value=0.0001; g.connect(this.master);
    const o1=this.ctx.createOscillator(), o2=this.ctx.createOscillator();
    const f=this.ctx.createBiquadFilter(); f.type='lowpass'; f.frequency.value=240;
    o1.type='sine'; o1.frequency.value=55; o2.type='sine'; o2.frequency.value=55.8;
    o1.connect(f); o2.connect(f); f.connect(g);
    o1.start(); o2.start();
    g.gain.linearRampToValueAtTime(0.05,this.t()+1.4);
    this.drone={g,o1,o2};
  },
  droneOff(){
    if(!this.drone) return;
    const d=this.drone, t=this.t();
    d.g.gain.cancelScheduledValues(t);
    d.g.gain.setValueAtTime(Math.max(0.0001,d.g.gain.value),t);
    d.g.gain.linearRampToValueAtTime(0.0001,t+0.8);
    d.o1.stop(t+1); d.o2.stop(t+1);
    this.drone=null;
  },
  ambient(zone){
    this.droneOff();
    if(!this.ctx||this.muted) return;
    const base=[55,49,64,41,58,36.7][zone]||55;
    const g=this.ctx.createGain(); g.gain.value=0.0001; g.connect(this.master);
    const o1=this.ctx.createOscillator(), o2=this.ctx.createOscillator();
    const f=this.ctx.createBiquadFilter(); f.type='lowpass'; f.frequency.value=200+zone*30;
    o1.type='sine'; o1.frequency.value=base; o2.type='triangle'; o2.frequency.value=base*1.007;
    o1.connect(f); o2.connect(f); f.connect(g);
    o1.start(); o2.start();
    g.gain.linearRampToValueAtTime(0.035,this.t()+2);
    this.drone={g,o1,o2};
  }
};

/* ================= CONSTANTS & DATA ================= */
const ZONES=[
  {name:'The Ashen Threshold', sub:'Prologue', w:60, h:13},
  {name:'The Rusted Archives', sub:'Early Halls', w:26, h:28},
  {name:'The Collapsed Gallery', sub:'Mid Halls', w:28, h:14},
  {name:'The Sunken Vault', sub:'Halfway Point', w:32, h:16},
  {name:'The Twin Spire', sub:'Final Ascent', w:18, h:42},
  {name:'The Core Chamber', sub:'The Beacon', w:34, h:22}
];
const ITEMS={
  band:{name:"Warden's Band", type:'accessory', desc:'A ring of unbroken ward. Attack speed rises slightly.'},
  stem:{name:'Vital Stem', type:'consumable', use:'Use (1)', desc:'A living sprig of the archive garden. Restores 2 full hearts (4 HP).'},
  lantern:{name:'Phantom Lantern', type:'accessory', desc:'Blue flame, no heat. Reveals hidden cracks and paths in the dark.'},
  cloak:{name:'Shadow Cloak', type:'accessory', desc:'A trim of storm-dark cloth. Halves damage from the observatory’s traps.'},
  seal:{name:'Binding Seal', type:'consumable', use:'Use (2)', desc:'A knot of cold light. Freezes the nearest construct for ~3 seconds.'}
};
const ACCS=['band','lantern','cloak'];
const ENEMY_TYPES={
  crawler:{name:'Rusted Sentinel', hp:4, dmg:1, sp:44, touchCd:0.8, r:9, gold:[5,9],  flash:0.09, touchD:0.3, body:'#6e462f', trim:'#2f2019', eye:'#ff9d3c'},
  sentinel:{name:'Corrupted Sentinel', hp:8, dmg:1, sp:42, touchCd:0.8, r:10, gold:[7,13], flash:0.1, touchD:0.32, body:'#57606f', trim:'#232a33', eye:'#e0483a'},
  warden:{name:'Warden Construct', hp:12, dmg:1, sp:60, touchCd:0.7, r:10, gold:[10,16], flash:0.1, touchD:0.35, body:'#4a3a2e', trim:'#241d16', eye:'#7fd4ff', boss:true},
  guardian:{name:'Spire Guardian', hp:30, dmg:1, sp:78, touchCd:0.7, r:15, gold:[60,90], flash:0.12, touchD:0.4, body:'#3b4152', trim:'#1c212c', eye:'#ffd257', boss:true}
};
const EKEYS=['crawler','sentinel','warden','guardian'];
const ECOL=[['#5c4632','#2c221a'],['#4c5866','#242d38'],['#43302a','#221a14'],['#303848','#181e2a']];
const WEAPONS=[
  {name:'Ember Blade',   dmg:1, wind:0.30, swing:0.16, rec:0.14, arc:1.95, range:30, burn:0, trail:0},
  {name:'Ignited Edge',  dmg:2, wind:0.20, swing:0.14, rec:0.10, arc:2.15, range:32, burn:1.2, trail:1},
  {name:'Solar Falchion',dmg:3, wind:0.46, swing:0.22, rec:0.17, arc:2.65, range:40, burn:1.2, trail:2}
];
const SHOT={speed:340, life:1.1, dmg:2};

/* --- map builder --- */
function buildMap(){
  const M=[];
  for(let z=0;z<6;z++){
    const {w,h}=ZONES[z];
    const g=new Array(w*h).fill(0);
    const put=(x,y,v)=>{ if(x>=0&&y>=0&&x<w&&y<h) g[y*w+x]=v; };
    const ring=(x0,y0,x1,y1,v)=>{ for(let y=y0;y<=y1;y++) for(let x=x0;x<=x1;x++){ if(x===x0||x===x1||y===y0||y===y1) put(x,y,v); } };
    const box=(x0,y0,x1,y1,v)=>{ for(let y=y0;y<=y1;y++) for(let x=x0;x<=x1;x++) put(x,y,v); };
    const door=(x,y)=>{ if(x>0&&x<w-1&&y>0&&y<h-1) put(x,y,2); };
    const wall=(x,y)=>put(x,y,1);
    const breakw=(x,y,v)=>put(x,y,v||5);
    const deco=(x,y,v)=>{ if(g[y*w+x]===0) put(x,y,v); };
    const clear=(x,y)=>put(x,y,0);
    if(z===0){
      // The Ashen Threshold
      wall(2,1); wall(2,2); wall(2,3); wall(2,4);
      box(6,1,7,1,1); box(6,5,7,5,1);
      wall(5,1); wall(5,2); wall(5,3); wall(5,4); wall(5,5);
      box(13,1,14,1,1); box(13,11,14,11,1);
      wall(12,1); wall(12,2); wall(12,3); wall(12,4); wall(12,5);
      wall(12,7); wall(12,8); wall(12,9); wall(12,10); wall(12,11);
      box(21,3,22,4,1); box(21,8,22,9,1);
      box(26,2,27,3,1); box(26,9,27,10,1);
      wall(25,2); wall(25,3); wall(25,4); wall(25,5);
      wall(25,7); wall(25,8); wall(25,9); wall(25,10);
      wall(24,2); wall(24,3); wall(24,4);
      wall(24,8); wall(24,9); wall(24,10);
      deco(3,1,3); deco(3,2,3); deco(3,5,3); deco(3,6,3); deco(3,10,3);
      deco(4,3,4); deco(4,4,4);
      deco(9,1,3); deco(9,11,3); deco(10,6,5); deco(15,2,4); deco(15,9,4);
      deco(17,4,7); deco(17,8,7); deco(18,4,7); deco(18,8,7); deco(19,6,7);
      deco(28,1,5); deco(28,10,5); deco(29,1,5); deco(29,10,5); deco(30,6,9);
      deco(31,1,5); deco(31,10,5); deco(32,1,5); deco(32,10,5);
      deco(33,1,8); deco(33,11,8); deco(34,1,8); deco(34,11,8);
      deco(35,1,8); deco(35,11,8); deco(36,1,8); deco(36,11,8); deco(37,1,8); deco(37,11,8);
      door(39,6);
      breakw(19,7,5); breakw(19,8,5);
      breakw(33,7,8);
    }
    if(z===1){
      // The Rusted Archives — shelves, dark vault, hidden alcove
      box(0,3,4,3,1); box(0,10,2,10,1); box(0,14,2,14,1); box(0,19,3,19,1); box(0,25,4,25,1);
      box(21,3,25,3,1); box(23,10,25,10,1); box(25,14,25,14,1); box(22,19,25,19,1); box(24,25,25,25,1);
      box(3,6,3,8,1); box(12,7,12,8,1); box(17,7,17,8,1); box(22,6,22,8,1);
      box(2,12,3,12,1); box(12,12,12,13,1); box(17,12,17,13,1);
      box(2,17,4,17,1); box(12,16,12,17,1); box(17,16,17,17,1); box(22,16,24,16,1);
      box(2,21,4,22,1); clear(3,22);
      box(5,21,5,22,1); box(13,21,13,22,1); box(21,21,21,22,1);
      box(8,10,13,10,5); box(8,11,13,11,5); box(8,12,13,12,5); box(8,13,13,13,5);
      box(8,14,13,14,5); box(8,15,13,15,5);
      clear(9,11); clear(9,12); clear(9,13); clear(9,14);
      clear(12,11); clear(12,12); clear(12,13); clear(12,14);
      clear(10,13); clear(11,13); clear(10,14); clear(11,14); clear(11,11); clear(11,12);
      breakw(8,6,5); breakw(8,7,5); breakw(12,8,5);
      breakw(18,12,5); breakw(8,17,5); breakw(13,16,5); breakw(20,17,5);
      breakw(9,21,9); breakw(10,21,9); breakw(11,21,9);
      breakw(18,21,9); breakw(19,21,9); breakw(20,21,9);
      deco(4,4,3); deco(5,4,3); deco(4,10,4); deco(5,10,4);
      deco(20,4,3); deco(21,4,3); deco(20,10,4); deco(21,10,4);
      deco(3,14,3); deco(4,14,4); deco(21,14,3); deco(22,14,4);
      deco(20,19,3); deco(21,19,3);
      for(let y=11;y<=21;y+=2){ deco(6,y,6); deco(15,y,6); }
      door(12,26); door(13,26);
    }
    if(z===2){
      // The Collapsed Gallery
      box(5,2,5,11,1); breakw(5,6);
      box(14,2,14,4,1); box(14,6,14,11,1); clear(14,5);
      box(15,6,22,6,1);
      box(23,6,27,6,1);
      box(14,1,27,1,1);
      box(25,11,27,13,1); clear(26,12); clear(27,12);
      breakw(25,2); breakw(6,2);
      door(27,7);
      deco(2,2,3); deco(3,3,4);
      deco(2,6,4); deco(3,7,4);
      deco(2,10,4); deco(3,11,3);
      deco(6,4,7); deco(12,4,7); deco(6,8,7); deco(12,8,7);
      deco(9,5,6); deco(9,7,6);
      deco(16,1,4); deco(17,2,4); deco(21,1,3); deco(22,2,4);
      deco(16,9,4); deco(17,10,6); deco(21,10,6); deco(22,9,4);
      deco(25,11,3); deco(26,11,4);
      door(26,6);
    }
    if(z===3){
      // The Sunken Vault
      box(5,4,6,5,1);
      box(11,2,11,6,1); box(12,5,12,6,1);
      box(17,9,18,13,1);
      box(19,11,20,12,1);
      box(24,3,25,8,1);
      box(11,8,12,13,1);
      box(11,15,12,15,1);
      box(15,9,15,10,1); box(16,9,16,10,1); box(17,9,17,10,1);
      box(15,14,15,15,1); box(16,14,16,15,1); box(17,14,17,15,1);
      clear(15,11); clear(15,12);
      clear(24,11);
      box(24,12,27,12,1); box(25,10,27,10,1); box(27,11,27,11,1); box(24,13,24,14,1);
      breakw(12,5,5);
      breakw(5,7,8); breakw(6,7,8); breakw(6,8,8);
      breakw(14,14,9);
      breakw(23,10,8);
      deco(1,5,3); deco(1,8,4); deco(2,10,4);
      deco(9,1,3); deco(10,1,4);
      deco(14,1,3); deco(15,2,4);
      deco(21,1,3); deco(22,1,4); deco(23,2,4);
      deco(29,2,3); deco(30,3,4); deco(30,5,4);
      deco(1,13,4); deco(2,14,3);
      deco(9,14,4); deco(10,15,4);
      deco(22,14,4); deco(23,15,3);
      deco(30,13,4); deco(30,15,3);
      deco(19,1,6);
      door(16,7); door(17,7);
    }
    if(z===4){
      // The Twin Spire — ascending columns
      for(let y=2;y<=4;y+=2){
        for(let x=2;x<8;x++) if((x+y)%2===0) box(x,y,x,y,1);
        for(let x=9;x<16;x++) if((x+y)%2===1) box(x,y,x,y,1);
      }
      box(1,7,16,7,1);
      for(let y=9;y<=21;y+=2){
        for(let x=2;x<8;x++) if((x+y)%2===0) box(x,y,x,y,1);
        for(let x=9;x<16;x++) if((x+y)%2===1) box(x,y,x,y,1);
      }
      box(1,22,16,22,1);
      for(let y=24;y<=36;y+=2){
        for(let x=2;x<8;x++) if((x+y)%2===0) box(x,y,x,y,1);
        for(let x=9;x<16;x++) if((x+y)%2===1) box(x,y,x,y,1);
      }
      box(5,38,12,38,1); box(5,39,12,39,1);
      clear(7,39); clear(8,39); clear(9,39); clear(10,39);
      clear(8,5); clear(10,5); clear(9,7);
      clear(8,17); clear(10,17); clear(9,22);
      clear(8,29); clear(10,29); clear(9,34);
      box(1,1,4,1,1); box(1,2,2,2,1); box(4,2,4,2,1);
      clear(3,2); breakw(3,3,9);
      box(1,36,4,36,1); box(1,37,1,37,1); box(4,37,4,37,1);
      clear(2,37); clear(3,37); breakw(2,38,9); breakw(3,38,9);
      box(13,10,16,10,1);
      box(13,16,16,16,1);
      clear(15,11); clear(14,11); breakw(14,10,5);
      clear(15,15); clear(14,15); breakw(14,16,5);
      clear(9,3); clear(10,3);
      deco(1,1,3); deco(16,1,4);
      deco(4,10,7); deco(13,10,7);
      deco(4,14,6); deco(13,14,6);
      deco(4,25,7); deco(13,25,7);
      deco(4,31,6); deco(13,31,6);
      deco(2,38,3); deco(15,38,3);
      door(8,41); door(9,41);
    }
    if(z===5){
      // The Core Chamber — ring of light
      ring(9,6,24,15,1);
      clear(16,6); clear(17,6);
      for(let x=11;x<=23;x+=3) clear(x,15);
      for(let y=7;y<=14;y+=3) clear(9,y);
      for(let y=7;y<=14;y+=3) clear(24,y);
      clear(16,15); clear(17,15);
      box(11,8,11,9,1); box(22,8,22,9,1);
      box(11,12,11,13,1); box(22,12,22,13,1);
      clear(11,10); clear(11,11);
      clear(22,10); clear(22,11);
      deco(3,3,3); deco(4,4,4);
      deco(13,2,6); deco(17,2,6);
      deco(2,18,3); deco(3,19,4);
      deco(30,18,3); deco(31,19,4);
      deco(16,2,9);
      door(16,15); door(17,15);
    }
    M[z]=g;
  }
  return M;
}

/* --- entity factories --- */
function EntFactory(){
  const E1={
    e(type,gx,gy,vx,vy,opts){
      const d=ENEMY_TYPES[type];
      const o=Object.assign({},d,opts||{});
      o.type=type; o.x=gx*T+T/2; o.y=gy*T+T/2; o.vx=vx||0; o.vy=vy||0;
      o.state='chase'; o.hp=d.hp; o.maxhp=d.hp;
      o.t=0; o.ia=0; o.hitT=0; o.burnT=0; o.frozenT=0; o.dieT=0; o.gold=ri(o.gold[0],o.gold[1]);
      o.sx=rnd(0,6); o.sy=rnd(0,6);
      o.cd2=null; o.spd2=0; o.atk=0; o.atkT=0; o.touchCdT=0;
      return o;
    },
    chest(gx,gy,type){ return {x:gx*T+T/2, y:gy*T+T/2, type:type||'gold', openT:0, id:nextId++}; },
    heart(gx,gy){ return {x:gx*T+T/2, y:gy*T+T/2, type:'heart', id:nextId++}; },
    stone(gx,gy){ return {x:gx*T+T/2, y:gy*T+T/2, type:'stone', id:nextId++}; },
    altar(gx,gy,kind){ return {x:gx*T+T/2, y:gy*T+T/2, kind:kind, done:false, id:nextId++}; },
    merchant(gx,gy){ return {x:gx*T+T/2, y:gy*T+T/2, type:'merchant', id:nextId++}; },
    shard(gx,gy){ return {x:gx*T+T/2, y:gy*T+T/2, type:'shard', id:nextId++}; },
    trap(gx,gy,dir){ return {x:gx*T+T/2, y:gy*T+T/2, dir:dir||'x', t:rnd(0,2), on:false, hitT:0, id:nextId++}; },
    plate(gx,gy,tid){ return {x:gx*T+T/2, y:gy*T+T/2, tid:tid||null, t:0, press:false, id:nextId++}; },
    crystal(gx,gy){ return {x:gx*T+T/2, y:gy*T+T/2, type:'crystal', id:nextId++, hitT:undefined}; },
    beacon(gx,gy){ return {x:gx*T+T/2, y:gy*T+T/2, type:'beacon', id:nextId++, lit:false, animT:0}; }
  };
  return E1;
}

/* --- zone entity placement (uses E = EntFactory) --- */
let nextId=1;
const E=EntFactory();
function zoneEntities(z){
  const out=[];
  const add=o=>out.push(o);
  if(z===0){
    add(E.stone(3,8));
    add(E.e('crawler',10,7)); add(E.e('crawler',11,6));
    add(E.e('crawler',17,6)); add(E.e('crawler',17,7));
    add(E.e('crawler',23,6)); add(E.e('crawler',28,7));
    add(E.chest(20,3,'stem'));
    add(E.chest(18,11,'gold'));
    add(E.chest(33,7,'gold'));
  }
  if(z===1){
    add(E.stone(13,13));
    add(E.e('crawler',3,2)); add(E.e('crawler',22,2));
    add(E.e('crawler',3,26)); add(E.e('crawler',22,26));
    add(E.e('sentinel',12,11)); add(E.e('sentinel',14,13));
    add(E.chest(3,22,'band'));
    add(E.chest(14,12,'gold'));
    add(E.chest(18,17,'gold'));
    add(E.heart(18,14));
    add(E.merchant(12,13));
  }
  if(z===2){
    add(E.plate(8,4,'A')); add(E.plate(8,9,'A'));
    add(E.plate(13,2,'A')); add(E.plate(13,9,'A'));
    add(E.plate(26,3,'B')); add(E.plate(26,9,'B'));
    add(E.plate(21,13,'C'));
    add(E.crystal(26,12));
    add(E.chest(16,2,'stem'));
    add(E.chest(14,5,'seal'));
    add(E.altar(19,5,'u1'));
    add(E.e('crawler',17,2)); add(E.e('crawler',17,10));
    add(E.e('sentinel',9,7));
    add(E.e('warden',21,5));
  }
  if(z===3){
    add(E.chest(12,2,'shard'));
    add(E.e('crawler',13,2)); add(E.e('crawler',14,2));
    add(E.e('sentinel',9,2)); add(E.e('sentinel',10,2));
    add(E.e('sentinel',13,11)); add(E.e('sentinel',14,11));
    add(E.chest(1,1,'gold'));
    add(E.chest(30,1,'gold'));
    add(E.chest(26,11,'lantern'));
    add(E.crystal(1,11));
  }
  if(z===4){
    add(E.stone(11,1));
    add(E.altar(9,3,'u2'));
    add(E.e('sentinel',5,10)); add(E.e('sentinel',5,14));
    add(E.e('sentinel',10,10)); add(E.e('sentinel',10,14));
    add(E.e('sentinel',5,18)); add(E.e('sentinel',12,20)); add(E.e('sentinel',13,19));
    add(E.chest(3,37,'cloak'));
    add(E.chest(3,36,'seal'));
    add(E.chest(9,37,'gold'));
    add(E.e('warden',6,33)); add(E.e('warden',10,30));
    add(E.e('guardian',9,39));
  }
  if(z===5){
    add(E.crystal(10,6)); add(E.crystal(23,6));
    add(E.crystal(9,11)); add(E.crystal(24,11));
    add(E.beacon(16,10));
    add(E.trap(13,10,'x')); add(E.trap(20,10,'x'));
    add(E.trap(16,8,'y')); add(E.trap(16,13,'y'));
    add(E.e('sentinel',16,9)); add(E.e('sentinel',15,12));
    add(E.e('warden',14,10)); add(E.e('warden',18,10));
    add(E.e('guardian',16,11));
    add(E.chest(16,11,'stem'));
  }
  return out;
}

/* ================= GAME STATE ================= */
const cv=$('cv'), ctx=cv.getContext('2d');
const game={
  state:'title', // title, playing, paused, inv, shop, dead, win
  zone:0, maps:null, ents:[], bolts:[], parts:[], floaters:[],
  player:null, time:0, shake:0, flash:0, edge:0, deathT:0, winT:0,
  saveIdx:0, lastSaveZone:0, winShown:false, backOk:false, looted:[],
  stats:{time:0, kills:0, gold:0, steps:0},
  banners:[], interactHint:'', shopStock:null,
  broken:[], // persistent broken wall keys "z:tx,ty"
  vaultDoor:false, // zone 3 mechanism door to the lantern room
  doorAnim:{},
  ambientAcc:0, stepAcc:0, emberAcc:0
};
function newPlayer(){
  return {
    x:0, y:0, r:7, dir:0, moving:false,
    hp:6, maxhp:6, containers:3,
    wpn:0, hasShard:false, shardCharges:0,
    gold:0, inv:{stem:0, seal:0}, equipped:{},
    atkT:0, atkPhase:'idle', hitDone:false, trail:[],
    hitFlash:0, invuln:0, dead:false, aura:0, emberT:0,
    kills:0
  };
}
function saveStock(){ return {stem:3, seal:2, heart:true, ammo:0}; }

/* ---- save system ---- */
const SAVE_KEY='wardensCorrosion.v1';
function doSave(){
  const p=game.player;
  const d={
    zone:game.zone, saveIdx:game.saveIdx, lastSaveZone:game.zone,
    broken:game.broken, vaultDoor:game.vaultDoor,
    player:{x:p.x,y:p.y,hp:p.hp,maxhp:p.maxhp,containers:p.containers,wpn:p.wpn,
      hasShard:p.hasShard,shardCharges:p.shardCharges,gold:p.gold,
      inv:{...p.inv}, equipped:{...p.equipped}},
    stock:game.shopStock,
    stats:{...game.stats},
    ents:game.ents.map(e=>{
      const o={};
      o.x=e.x; o.y=e.y;
      if(e.type) o.type=e.type;
      if(e.kind) o.kind=e.kind;
      if(e.hp!==undefined){o.hp=e.hp; o.maxhp=e.maxhp;}
      if(e.openT) o.openT=e.openT;
      if(e.done!==undefined) o.done=e.done;
      if(e.tid) o.tid=e.tid;
      return o;
    })
  };
  try{ localStorage.setItem(SAVE_KEY, JSON.stringify(d)); }catch(e){}
}
function loadSave(){
  try{
    const s=localStorage.getItem(SAVE_KEY);
    if(!s) return null;
    return JSON.parse(s);
  }catch(e){ return null; }
}
function hasSave(){ return !!loadSave(); }
function clearSave(){ try{ localStorage.removeItem(SAVE_KEY); }catch(e){} }

/* ================= INPUT ================= */
const keys={};
let mouseDown=false, mouseX=480, mouseY=270;
addEventListener('keydown',e=>{
  const k=e.key.toLowerCase();
  if(['tab',' ','arrowup','arrowdown','arrowleft','arrowright'].includes(k)) e.preventDefault();
  keys[k]=true;
  onKeyPress(k);
});
addEventListener('keyup',e=>{ keys[e.key.toLowerCase()]=false; });
cv.addEventListener('mousedown',e=>{
  mouseDown=true;
  const r=cv.getBoundingClientRect();
  mouseX=(e.clientX-r.left)*CW/r.width; mouseY=(e.clientY-r.top)*CH/r.height;
  onKeyPress('j');
});
addEventListener('mouseup',()=>mouseDown=false);
cv.addEventListener('contextmenu',e=>e.preventDefault());
function axisX(){ return (keys['d']||keys['arrowright']?1:0)-(keys['a']||keys['arrowleft']?1:0); }
function axisY(){ return (keys['s']||keys['arrowdown']?1:0)-(keys['w']||keys['arrowup']?1:0); }
function onKeyPress(k){
  if(k==='m'){
    AudioSys.setMuted(!AudioSys.muted);
    $('btnMute').textContent='Sound: '+(AudioSys.muted?'Off':'On');
    return;
  }
  if(game.state==='title' && k==='enter'){ startNew(); return; }
  if(game.state==='dead'){ if(game.deathT>1 && (k==='r'||k==='enter')) respawn(); return; }
  if(game.state==='win'){ if(game.winT>1.5 && (k==='r'||k==='enter')) toTitle(); return; }
  if(k==='p'||k==='escape'){
    if(game.state==='playing'){ game.state='paused'; $('ovPause').hidden=false; }
    else if(game.state==='paused'){ game.state='playing'; $('ovPause').hidden=true; }
    else if(game.state==='inv'){ game.state='playing'; $('ovInv').hidden=true; }
    else if(game.state==='shop'){ game.state='playing'; $('ovShop').hidden=true; }
    return;
  }
  if(game.state!=='playing') return;
  if(k==='tab'||k==='i'){ game.state='inv'; renderInv(); $('ovInv').hidden=false; return; }
  if(k==='j'||k==='z'||k==='enter') tryAttack();
  if((k==='k'||k==='x')) tryShoot();
  if(k==='e') tryInteract();
  if(k==='1') useItem('stem');
  if(k==='2') useItem('seal');
}
function setOv(shown){
  $('ovTitle').hidden=!shown; $('ovPause').hidden=true;
  $('ovInv').hidden=true; $('ovShop').hidden=true; $('ovWin').hidden=true;
}
function toTitle(){
  game.state='title';
  setOv(true);
  $('btnCont').hidden=!hasSave();
  $('btnCont').textContent='Continue';
  AudioSys.droneOff();
}
function startNew(){
  AudioSys.init(); AudioSys.resume();
  nextId=1;
  game.maps=buildMap();
  const p=newPlayer();
  p.x=2*T+T/2; p.y=8*T+T/2;
  game.player=p;
  game.zone=0; game.saveIdx=0; game.lastSaveZone=0;
  game.ents=zoneEntities(0);
  game.bolts=[]; game.parts=[]; game.floaters=[];
  game.stats={time:0,kills:0,gold:0,steps:0};
  game.shopStock=saveStock();
  game.broken=[]; game.vaultDoor=false; game.doorAnim={};
  game.looted=[]; game.winShown=false; game.backOk=false;
  game.state='playing'; setOv(false);
  banner(0);
  AudioSys.ambient(0);
}
function continueGame(){
  const d=loadSave(); if(!d) return startNew();
  AudioSys.init(); AudioSys.resume();
  nextId=100000;
  game.maps=buildMap();
  game.zone=d.zone;
  game.saveIdx=d.saveIdx; game.lastSaveZone=d.lastSaveZone||d.zone;
  game.broken=d.broken||[];
  game.vaultDoor=!!d.vaultDoor;
  applyBroken(d.zone);
  const fresh=zoneEntities(d.zone);
  for(const se of d.ents||[]){
    const m=fresh.find(e=>{
      if(e.kind==='u1'||e.kind==='u2') return e.kind===se.kind;
      if(e.type) return e.type===se.type&&Math.abs(e.x-se.x)<1&&Math.abs(e.y-se.y)<1;
      return Math.abs(e.x-se.x)<1&&Math.abs(e.y-se.y)<1;
    });
    if(m){
      if(se.hp!==undefined) m.hp=se.hp;
      if(se.openT) m.openT=se.openT;
      if(se.done!==undefined) m.done=se.done;
    }
  }
  if(d.zone===4){
    const has=fresh.find(e=>e.kind==='u2');
    if(!has) fresh.push(Object.assign(E.altar(9,3,'u2'),{done:(d.player&&d.player.wpn>=2)}));
    else has.done=(d.player&&d.player.wpn>=2);
  }
  game.ents=fresh;
  game.bolts=[]; game.parts=[]; game.floaters=[];
  const p=newPlayer();
  const sp=d.player||{};
  p.x=sp.x||2*T+T/2; p.y=sp.y||8*T+T/2;
  p.hp=sp.hp!=null?sp.hp:6; p.maxhp=sp.maxhp||6; p.containers=sp.containers||3;
  p.wpn=sp.wpn||0; p.hasShard=!!sp.hasShard; p.shardCharges=sp.shardCharges||0;
  p.gold=sp.gold||0; p.inv=sp.inv||{stem:0,seal:0}; p.equipped=sp.equipped||{};
  game.player=p;
  game.shopStock=d.stock||saveStock();
  game.stats=d.stats||{time:0,kills:0,gold:0,steps:0};
  game.doorAnim={};
  game.state='playing'; setOv(false);
  banner(game.zone);
  AudioSys.ambient(game.zone);
}
function respawn(){
  const p=game.player;
  p.hp=p.maxhp;
  const zx=game.lastSaveZone;
  if(zx!==game.zone){
    game.zone=zx; game.ents=zoneEntities(zx);
    game.bolts=[]; game.parts=[]; game.floaters=[];
    game.doorAnim={};
    applyBroken(zx);
    AudioSys.ambient(zx);
  } else {
    const fresh=zoneEntities(zx);
    for(const f of fresh){
      const cur=game.ents.find(e=>(e.type&&e.type===f.type)||(!e.type&&!f.type));
      if(cur&&f.hp!==undefined&&cur.hp<=0){ cur.hp=f.hp; cur.dieT=0; cur.frozenT=0; cur.burnT=0; }
    }
  }
  const st=game.ents.find(e=>e.type==='stone');
  if(st){ p.x=st.x; p.y=st.y+T*0.8; }
  p.dead=false; p.invuln=2; p.hitFlash=0;
  game.state='playing'; banner(game.zone, true);
  game.stats.deaths=(game.stats.deaths||0)+1;
}

/* ================= ZONE FLOW ================= */
function applyBroken(z){
  const {w,h}=ZONES[z];
  for(const key of game.broken){
    if(!key.startsWith(z+':')) continue;
    const [tx,ty]=key.slice(2).split(',').map(Number);
    if(tx>=0&&ty>=0&&tx<w&&ty<h) game.maps[z][ty*w+tx]=0;
  }
}
function banner(z, soft){
  game.banners.push({t:0, dur:3.2, title:ZONES[z].name, sub:ZONES[z].sub, soft:!!soft});
}
function loadZone(z){
  game.zone=z;
  game.ents=zoneEntities(z);
  game.bolts=[]; game.parts=[]; game.floaters=[];
  game.doorAnim={};
  applyBroken(z);
  AudioSys.ambient(z);
  banner(z);
}
function zoneDoorPos(){
  switch(game.zone){
    case 0: return {x:40*T-16, y:7*T-16, n:1, p:0};
    case 1: return {x:13*T-16, y:27*T-16, n:2, p:0};
    case 2: return {x:27*T-16, y:7*T-16, n:3, p:2};
    case 3: return {x:17*T-16, y:8*T-16, n:4, p:3};
    case 4: return {x:9*T-16, y:42*T-16, n:5, p:4};
    default: return null;
  }
}
function zoneEnterPos(z){
  switch(z){
    case 0: return {x:2*T+T/2, y:8*T+T/2};
    case 1: return {x:13*T+T/2, y:2*T+T/2};
    case 2: return {x:3*T+T/2, y:2*T+T/2};
    case 3: return {x:3*T+T/2, y:7*T+T/2};
    case 4: return {x:8*T+T/2, y:1*T+T/2};
    case 5: return {x:16*T+T/2, y:3*T+T/2};
  }
}
function tryAdvance(){
  const d=zoneDoorPos(); if(!d) return;
  const p=game.player;
  if(dist(p.x,p.y,d.x+16,d.y+16)>30) return;
  if(d.n===game.zone+1){
    loadZone(d.n);
    const ep=zoneEnterPos(d.n);
    p.x=ep.x; p.y=ep.y;
    if(d.n>game.lastSaveZone) game.lastSaveZone=d.n; // forward progress is never barred
  }
}
function toast(msg){ game.floaters.push({x:game.player.x,y:game.player.y-26,txt:msg,t:0,dur:2.4,c:'#cbb88a',size:11}); }

/* ================= PARTICLES & FX ================= */
function spark(x,y,n,cols,spd,life,grav){
  for(let i=0;i<n;i++){
    const a=rnd(0,6.283), s=rnd(spd*0.3,spd);
    game.parts.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s-10,
      t:0, life:rnd(life*0.6,life), c:cols[ri(0,cols.length-1)],
      s:rnd(1.2,2.6), g:grav||40});
  }
}
function floatText(x,y,txt,c,size){ game.floaters.push({x,y,txt,t:0,dur:1.1,c:c||'#e8d9b0',size:size||12}); }
function goldSwirl(x,y){
  for(let i=0;i<10;i++){
    const a=rnd(0,6.283);
    game.parts.push({x:x+Math.cos(a)*8, y:y+Math.sin(a)*8,
      vx:Math.cos(a)*20, vy:-rnd(20,60), t:0, life:rnd(0.5,0.9),
      c:'#e8c67a', s:rnd(1.2,2.4), g:-20, gold:true, a:a});
  }
}
function auraBurst(x,y){
  for(let i=0;i<26;i++){
    const a=rnd(0,6.283);
    game.parts.push({x,y,vx:Math.cos(a)*rnd(10,50),vy:Math.sin(a)*rnd(10,50)-20,
      t:0,life:rnd(0.8,1.4),c:i%2?'#fff3d0':'#e8c67a',s:rnd(1.5,3),g:-10});
  }
}
function updateFx(dt){
  for(let i=game.parts.length-1;i>=0;i--){
    const p=game.parts[i];
    p.t+=dt;
    if(p.t>p.life){ game.parts.splice(i,1); continue; }
    if(p.gold){ p.a+=dt*5; p.x+=Math.cos(p.a)*22*dt; p.y+=p.vy*dt; }
    else { p.x+=p.vx*dt; p.y+=p.vy*dt; p.vy+=(p.g||0)*dt; }
  }
  for(let i=game.floaters.length-1;i>=0;i--){
    const f=game.floaters[i];
    f.t+=dt; f.y-=22*dt;
    if(f.t>f.dur) game.floaters.splice(i,1);
  }
  for(let i=game.banners.length-1;i>=0;i--){
    const b=game.banners[i]; b.t+=dt;
    if(b.t>b.dur) game.banners.splice(i,1);
  }
  game.shake=Math.max(0,game.shake-dt*22);
  game.flash=Math.max(0,game.flash-dt*2.2);
  game.edge=Math.max(0,game.edge-dt*0.5);
}

/* ================= PLAYER & COMBAT ================= */
function solidAt(x,y){
  const {w,h}=ZONES[game.zone];
  const tx=Math.floor(x/T), ty=Math.floor(y/T);
  if(tx<0||ty<0||tx>=w||ty>=h) return true;
  let g=game.maps[game.zone][ty*w+tx];
  if(game.zone===3 && !game.vaultDoor && (tx===24)&&(ty===11||ty===12)) g=1;
  return g===1||(g>=5&&g<=9);
}
function canStand(x,y,r){
  return !solidAt(x-r,y-r)&&!solidAt(x+r,y-r)&&!solidAt(x-r,y+r)&&!solidAt(x+r,y+r);
}
function moveCircle(o, r, dt){
  const ax=axisX(), ay=axisY();
  if(!ax&&!ay) return;
  const l=Math.hypot(ax,ay), sp=165;
  let dx=ax/l*sp*dt, dy=ay/l*sp*dt;
  if(dx){ const nx=o.x+dx; if(canStand(nx,o.y,r)) o.x=nx; }
  if(dy){ const ny=o.y+dy; if(canStand(o.x,ny,r)) o.y=ny; }
}
function weapon(){ return WEAPONS[game.player.wpn]; }
function atkSpeedMul(){ return game.player.equipped.band?1.18:1; }
function tryAttack(){
  const p=game.player;
  if(p.dead||p.atkPhase!=='idle') return;
  p.atkPhase='windup'; p.atkT=0; p.hitDone=false;
  AudioSys.sfx('whoosh', p.wpn);
}
function swingActive(p){
  return p.atkPhase==='swing';
}
function attackAngle(p){
  if(p.atkPhase==='swing') return p.dir;
  return p.dir;
}
function meleeHitEnemies(p, ang, range, arc, dmg, widx){
  const w=WEAPONS[widx];
  let hitAny=false;
  for(const e of game.ents){
    if(e.hp===undefined||e.hp<=0) continue;
    const d=dist(p.x,p.y,e.x,e.y);
    if(d>range+ (e.r||9)) continue;
    const a=Math.atan2(e.y-p.y,e.x-p.x);
    if(Math.abs(angDiff(a,ang))>arc/2) continue;
      if(e.hitT!==undefined&&e.hitT>0) continue;
    hitEnemy(e, dmg, a, p, w.burn, widx);
    hitAny=true;
  }
  // breakable walls
  const tx0=Math.floor(p.x/T), ty0=Math.floor(p.y/T);
  for(let yy=ty0-2;yy<=ty0+2;yy++) for(let xx=tx0-2;xx<=tx0+2;xx++){
    const w=ZONES[game.zone].w, h=ZONES[game.zone].h;
    if(xx<1||yy<1||xx>=w-1||yy>=h-1) continue;
    const g=game.maps[game.zone][yy*w+xx];
    if(g>=5&&g<=9){
      const cx=xx*T+T/2, cy=yy*T+T/2;
      const a=Math.atan2(cy-p.y,cx-p.x);
      if(Math.abs(angDiff(a,ang))<arc/2+0.4 && dist(p.x,p.y,cx,cy)<range+18){
        strikeWall(game.zone,xx,yy,g,p);
      }
    }
  }
  return hitAny;
}
function wallMaxHp(v){ return v===9?4:(v===8?0:5); } // 0 = bolts only
function strikeWall(z,tx,ty,v,p){
  const key=z+':'+tx+','+ty;
  const pwr=p.wpn===2?3:(p.wpn===1?2:1);
  if(v===8){
    if(p.hasShard){ setWallBroken(z,tx,ty); }
    else toast('The aether-vein hums beyond the blade’s reach. A shard must strike it.');
    return;
  }
  if(v===9){
    if(!p.hasShard){ toast('A hairline crack only — the blade skates off. A shard must open this.'); return; }
    pwr=3;
  }
  const maxHp=wallMaxHp(v);
  p._bw=p._bw||{};
  p._bw[key]=(p._bw[key]==null?maxHp:p._bw[key])-pwr;
  spark(tx*T+T/2, ty*T+T/2, 7, ['#b9a98c','#6a5a45'], 110, 0.35, 80);
  AudioSys.sfx('tick');
  if(p._bw[key]<=0){
    setWallBroken(z,tx,ty);
    delete p._bw[key];
  }
}
function setWallBroken(z,tx,ty){
  const w=ZONES[z].w, h=ZONES[z].h;
  if(tx<0||ty<0||tx>=w||ty>=h) return;
  const g=game.maps[z];
  if(g[ty*w+tx]===0) return;
  g[ty*w+tx]=0;
  const key=z+':'+tx+','+ty;
  if(!game.broken.includes(key)) game.broken.push(key);
  spark(tx*T+T/2, ty*T+T/2, 18, ['#b9a98c','#8a7a5f','#d9c9a8'], 200, 0.7, 90);
  AudioSys.sfx('break');
  game.shake=Math.max(game.shake,2.5);
  const p=game.player;
  // loot conventions per zone
  if(z===1){
    if(tx===18||tx===19||tx===20) game.ents.push(E.chest(tx,ty,'heart'));
    if(tx===9||tx===10||tx===11) game.ents.push(E.chest(tx,ty,'gold'));
  }
  if(z===2){
    if(tx===26&&ty===7) game.ents.push(E.chest(26,7,'heart'));
    if(tx===16) game.ents.push(E.chest(16,8,'stem'));
    if(tx===20&&(ty===5||ty===6)) game.ents.push(E.chest(tx,ty,'heart'));
    if(tx===23) game.ents.push(E.chest(tx,ty,'gold'));
  }
  if(z===3){
    if((tx===5||tx===6)&&ty>=7) game.ents.push(E.chest(tx,ty,'heart'));
    if(tx===23) game.ents.push(E.chest(tx,ty,'gold'));
    if(tx===14) game.ents.push(E.chest(tx,ty,'shard'));
  }
  if(z===4){
    // (hidden alcove loot placed directly — no wall drops here)
  }
}
function hitEnemy(e, dmg, ang, p, burn, widx){
  e.hp-=dmg; e.hitT=ENEMY_TYPES[e.type].flash;
  e.vx=Math.cos(ang)*130; e.vy=Math.sin(ang)*130;
  e.burnT=Math.max(e.burnT, burn);
  spark(e.x,e.y, widx===2?10:6, widx===0?['#ffb45e','#ff8a3c']:['#ffd257','#fff3d0','#ff9d3c'], 150, 0.5, 60);
  floatText(e.x, e.y-14, dmg, '#f2dfae', 12);
  AudioSys.sfx('impact', widx);
  if(e.hp<=0) killEnemy(e, p);
}
function killEnemy(e, p){
  e.dieT=0.5; e.hp=0;
  AudioSys.sfx('die');
  spark(e.x,e.y,14,['#8a5a3a','#3a2a22','#ff9d3c'],180,0.7,80);
  game.stats.kills++; p.kills++;
  if(e.boss) game.shake=Math.max(game.shake,4);
  if(game.zone===4 && e.type==='guardian'){
    // drops Shadow Cloak if not held
    const has=Object.values(p.equipped).includes('cloak')||p.inv.cloak;
    if(!has) game.ents.push(E.chest(e.x/T-0.5, e.y/T-0.5,'cloak'));
  }
}
function enemyGoldDrop(e){
  goldSwirl(e.x,e.y);
  game.player.gold+=e.gold;
  game.stats.gold+=e.gold;
  floatText(e.x, e.y-4, '+'+e.gold, '#e8c67a', 11);
  AudioSys.sfx('coin');
}
function hurtPlayer(dmg, srcX, srcY, kind){
  const p=game.player;
  if(p.dead||p.invuln>0) return;
  let d=dmg;
  if(kind==='trap'&&p.equipped.cloak) d=Math.ceil(d/2);
  p.hp-=d; p.invuln=0.9; p.hitFlash=1;
  game.flash=0.6; game.shake=Math.max(game.shake,3.5);
  spark(p.x,p.y,12,['#ff5a3c','#c0452e','#8a2a1a'],160,0.6,60);
  floatText(p.x, p.y-16, '-'+d, '#ff8a6a', 13);
  AudioSys.sfx('hurt');
  const a=srcX!==undefined?Math.atan2(p.y-srcY,p.x-srcX):0;
  p.x+=Math.cos(a)*6; p.y+=Math.sin(a)*6;
  if(p.hp<=0){
    p.hp=0; p.dead=true;
    game.state='dead'; game.deathT=0;
    AudioSys.sfx('death');
    spark(p.x,p.y,30,['#ff8a3c','#c0452e','#8a8a8a'],220,1.1,90);
  }
}
function addContainer(p, where){
  if(p.containers>=5){ floatText(p.x,p.y-20,'The vigil is already full','#8f8672',11); return false; }
  p.containers++; p.maxhp=p.containers*2; p.hp=p.maxhp;
  p.aura=2; game.edge=2;
  auraBurst(p.x,p.y);
  AudioSys.sfx('heart');
  toast('Heart Container restored — the vigil deepens ('+p.containers+'/5).');
  return true;
}
function tryShoot(){
  const p=game.player;
  if(!p.hasShard){ toast('You feel for the Shard — but find only empty air.'); return; }
  if(p.shardCharges<=0){ toast('The Aether Shard is dim. Seek the merchant’s restock (10g).'); return; }
  if(p.atkPhase!=='idle') return;
  p.shardCharges--;
  const a=angN(p.dir);
  game.bolts.push({x:p.x+Math.cos(a)*14, y:p.y+Math.sin(a)*14,
    vx:Math.cos(a)*SHOT.speed, vy:Math.sin(a)*SHOT.speed, t:0, life:SHOT.life});
  AudioSys.sfx('shoot');
}
function updateBolts(dt){
  const p=game.player;
  for(let i=game.bolts.length-1;i>=0;i--){
    const b=game.bolts[i];
    b.t+=dt;
    if(b.t>b.life){ game.bolts.splice(i,1); continue; }
    const steps=3;
    let dead=false;
    for(let s=0;s<steps&&!dead;s++){
      b.x+=b.vx*dt/steps; b.y+=b.vy*dt/steps;
      if(solidAt(b.x,b.y)){
        spark(b.x,b.y,8,['#7fd4ff','#c8ecff'],140,0.4,0);
        AudioSys.sfx('chime'); dead=true; break;
      }
      // crystals
      for(const e of game.ents){
        if(e.type!=='crystal'&&e.type!=='beacon') continue;
        if(e.type==='beacon'&&e.lit) continue;
        if(dist(b.x,b.y,e.x,e.y)<12){
          hitCrystal(e); dead=true; break;
        }
      }
      if(dead) break;
      // enemies
      for(const e of game.ents){
        if(e.hp===undefined||e.hp<=0) continue;
        if(dist(b.x,b.y,e.x,e.y)<(e.r||9)+3){
          const a=Math.atan2(b.vy,b.vx);
          hitEnemy(e, SHOT.dmg, a, p, WEAPONS[p.wpn].burn, p.wpn);
          spark(b.x,b.y,8,['#7fd4ff','#c8ecff'],150,0.4,0);
          AudioSys.sfx('chime'); dead=true; break;
        }
      }
    }
    if(dead) game.bolts.splice(i,1);
  }
}
function hitCrystal(c){
  if(c.hitT!==undefined) return;
  AudioSys.sfx('rare');
  spark(c.x,c.y,16,['#7fd4ff','#fff'],200,0.7,0);
  if(game.zone===2){
    // Gallery light-mechanism: the sealed door on the east wall
    c.hitT=1;
    const w=ZONES[2].w;
    const key='2:27,7';
    if(!game.broken.includes(key)) game.broken.push(key);
    game.maps[2][7*w+27]=0;
    spark(27*T+16,7*T+16,20,['#b9a98c','#8a7a5f'],180,0.7,80);
    AudioSys.sfx('door');
    toast('A distant mechanism answers — the gallery door seals open.');
    return;
  }
  if(game.zone===5){
    c.hitT=1;
    const beacon=game.ents.find(e=>e.type==='beacon');
    const unlit=game.ents.filter(e=>e.type==='crystal'&&e.hitT===undefined).length;
    if(unlit<=0 && beacon){
      beacon.lit=true; beacon.animT=0;
      game.state='win'; game.winT=0;
      game.edge=3; game.shake=5;
      AudioSys.sfx('victory');
      spark(beacon.x,beacon.y,60,['#ffd257','#fff3d0','#7fd4ff'],260,1.6,-30);
    } else toast('The crystal answers — the ring brightens. ('+unlit+' remain)');
  } else if(game.zone===3){
    // Vault mechanism: opens the hidden lantern chamber on the east side
    c.hitT=1;
    game.vaultDoor=true;
    const w=ZONES[3].w;
    for(const ty of [11,12]){
      const key='3:24,'+ty;
      if(!game.broken.includes(key)) game.broken.push(key);
      game.maps[3][ty*w+24]=0;
    }
    spark(24*T+16,12*T,20,['#b9a98c','#8a7a5f'],180,0.7,80);
    AudioSys.sfx('door');
    toast('Stone grinds aside — a hidden chamber yawns open on the east wall.');
  }
}
function useItem(kind){
  const p=game.player;
  if(p.dead) return;
  if(kind==='stem'){
    if(p.inv.stem<=0){ AudioSys.sfx('empty'); return; }
    if(p.hp>=p.maxhp){ toast('The vigil is already whole.'); return; }
    p.inv.stem--;
    p.hp=Math.min(p.maxhp,p.hp+4);
    spark(p.x,p.y,14,['#8fd4a5','#d0ffe0'],120,0.8,-20);
    AudioSys.sfx('item');
    floatText(p.x,p.y-16,'+4','#8fd4a5',12);
  } else if(kind==='seal'){
    if(p.inv.seal<=0){ AudioSys.sfx('empty'); return; }
    let best=null,bd=1e9;
    for(const e of game.ents){
      if(e.hp===undefined||e.hp<=0) continue;
      const d=dist(p.x,p.y,e.x,e.y);
      if(d<bd&&d<420){bd=d;best=e;}
    }
    if(!best){ toast('No construct is within reach of the seal.'); AudioSys.sfx('empty'); return; }
    p.inv.seal--;
    best.frozenT=3;
    AudioSys.sfx('seal');
    spark(best.x,best.y,16,['#7fd4ff','#c8ecff'],140,0.8,0);
    floatText(best.x,best.y-16,'bound','#7fd4ff',11);
  }
}
function useItemEnd(){}
/* ================= PLAYER UPDATE ================= */
function updatePlayer(dt){
  const p=game.player;
  if(p.dead){ game.deathT+=dt; updateFx(dt); return; }
  const ax=axisX(), ay=axisY();
  p.moving=(ax!==0||ay!==0);
  if(p.moving){
    const l=Math.hypot(ax,ay);
    moveCircle(p, p.r, dt);
    const na=Math.atan2(ay,ax);
    p.dir=angN(p.dir+angDiff(na,p.dir)*Math.min(1,dt*12));
    // footsteps
    game.stepAcc+=dt;
    const zone=game.zone;
    if(game.stepAcc>0.34){
      game.stepAcc=0;
      AudioSys.sfx('step', zone===1?1:zone===3?2:zone===4?0:0);
    }
    // ember trail (upgraded blade)
    if(p.wpn>=1){
      game.emberAcc+=dt;
      if(game.emberAcc>0.05){
        game.emberAcc=0;
        const back=p.dir+Math.PI;
        game.parts.push({x:p.x+Math.cos(back)*10+rnd(-3,3), y:p.y+Math.sin(back)*10+rnd(-3,3),
          vx:0,vy:-14,t:0,life:rnd(0.4,0.7),
          c:p.wpn===1?'#ff8a3c':'#ffd257',s:rnd(1,2.2),g:-6});
      }
    }
    if(p.hasShard && Math.random()<0.12){
      const side=p.dir+Math.PI/2;
      game.parts.push({x:p.x+Math.cos(side)*9, y:p.y+Math.sin(side)*9,
        vx:0,vy:-10,t:0,life:0.5,c:'#7fd4ff',s:1.4,g:-8});
    }
  }
  // attack phases
  if(p.atkPhase!=='idle'){
    p.atkT+=dt*atkSpeedMul();
    const w=weapon();
    if(p.atkPhase==='windup'&&p.atkT>=w.wind){
      p.atkPhase='swing'; p.atkT=0;
      meleeHitEnemies(p, p.dir, w.range, w.arc, w.dmg, p.wpn);
      if(p.wpn===2){ game.edge=Math.max(game.edge,0.8); game.shake=Math.max(game.shake,2.5); }
    } else if(p.atkPhase==='swing'){
      if(p.atkT>=w.swing*0.5){
        meleeHitEnemies(p, p.dir, w.range, w.arc, w.dmg*0.6, p.wpn);
      }
      if(p.atkT>=w.swing){ p.atkPhase='rec'; p.atkT=0; }
    } else if(p.atkT>=w.rec){ p.atkPhase='idle'; p.atkT=0; }
  }
  p.invuln=Math.max(0,p.invuln-dt);
  p.hitFlash=Math.max(0,p.hitFlash-dt*3);
  p.aura=Math.max(0,p.aura-dt);
  // traps
  for(const e of game.ents){
    if(e.type!=='trap') continue;
    e.t+=dt;
    const cyc=(e.t%2.4);
    e.on=cyc>0.9&&cyc<1.5;
    if(e.on&&dist(p.x,p.y,e.x,e.y)<13){
      if(e.hitT<=0){
        e.hitT=1;
        hurtPlayer(1, e.x, e.y, 'trap');
        if(p.equipped.cloak) floatText(p.x,p.y-30,'the cloak darkens','#b0a890',10);
      }
    }
    e.hitT=Math.max(0,e.hitT-dt);
  }
  // interact hint
  const hint=interactHint();
  game.interactHint=hint;
}
function interactHint(){
  const p=game.player;
  for(const e of game.ents){
    const d=dist(p.x,p.y,e.x,e.y);
    if(e.type==='stone'&&d<30){
      if(game.lastSaveZone===game.zone) return 'Rest at the Sanctuary Stone';
      return 'The stone is quiet here';
    }
    if(e.type==='chest'&&d<26) return 'Open the chest';
    if(e.type==='heart'&&d<24) return 'Take the heart';
    if(e.type==='shard'&&d<24) return 'Claim the Aether Shard';
    if(e.type==='merchant'&&d<30) return 'Barter with the merchant';
    if(e.type==='altar'&&d<28&&!e.done) return e.kind==='u1'?'Approach the Weapon Altar':'Approach the altar';
    if(e.type==='beacon'&&!e.lit&&game.zone===5&&d<60) return 'Kindle the Core Beacon (E)';
  }
  const d=zoneDoorPos();
  if(d&&dist(p.x,p.y,d.x+16,d.y+16)<30) return 'Pass through the arch';
  return '';
}
function tryInteract(){
  const p=game.player;
  for(const e of game.ents){
    const d=dist(p.x,p.y,e.x,e.y);
    if(e.type==='stone'&&d<30){
      if(game.lastSaveZone!==game.zone){ toast('This stone has no memory here.'); AudioSys.sfx('empty'); }
      else {
        p.hp=p.maxhp;
        doSave();
        game.edge=Math.max(game.edge,1);
        spark(e.x,e.y-10,20,['#ffd257','#fff3d0'],90,1,-10);
        AudioSys.sfx('save');
        toast('Progress kept. The vigil renews.');
      }
      return;
    }
    if(e.type==='chest'&&d<26){
      openChest(e); return;
    }
    if(e.type==='heart'&&d<24){
      const ok=addContainer(p);
      if(ok||p.containers>=5) game.ents.splice(game.ents.indexOf(e),1);
      return;
    }
    if(e.type==='shard'&&d<24){
      p.hasShard=true; p.shardCharges=8;
      game.ents.splice(game.ents.indexOf(e),1);
      game.edge=2; p.aura=2;
      auraBurst(p.x,p.y);
      AudioSys.sfx('rare');
      toast('The Aether Shard bonds to your off-hand. (K to fire)');
      return;
    }
    if(e.type==='merchant'&&d<30){
      openShop(); return;
    }
    if(e.type==='altar'&&d<28&&!e.done){
      e.done=true;
      const widx=(e.kind==='u2')?2:1;
      p.wpn=widx;
      game.edge=2; p.aura=2;
      auraBurst(p.x,p.y);
      spark(e.x,e.y-8,24,['#ff9d3c','#ffd257','#fff3d0'],180,0.9,20);
      AudioSys.sfx('altar');
      if(widx===1) toast('IGNITED EDGE — the blade drinks the ember. It burns, and strikes faster.');
      else toast('SOLAR FALCHION — the blade becomes noonlight. Wider arc, heavier impact.');
      return;
    }
    if(e.type==='beacon'&&!e.lit&&game.zone===5&&d<60){
      const unlit=game.ents.filter(c=>c.type==='crystal'&&c.hitT===undefined).length;
      if(unlit>0){ toast('The ring is dark. Strike its four crystals with the Shard ('+unlit+' remain).'); }
      else { toast('The ring is dark.'); }
      return;
    }
  }
  const d=zoneDoorPos();
  if(d&&dist(p.x,p.y,d.x+16,d.y+16)<30) tryAdvance();
}
function openChest(e){
  if(e.openT>0) return;
  e.openT=0.6;
  AudioSys.sfx('item');
  spark(e.x,e.y-6,14,['#fff3d0','#e8c67a'],110,0.7,-20);
  const p=game.player;
  const loot=e.type;
  if(loot==='gold'){
    const g=ri(20,40);
    p.gold+=g; game.stats.gold+=g;
    goldSwirl(e.x,e.y);
    floatText(e.x,e.y-16,'+'+g,'#e8c67a',12);
    AudioSys.sfx('coin');
  } else if(loot==='heart'){
    const ok=addContainer(p);
    if(!ok){ p.gold+=30; goldSwirl(e.x,e.y); floatText(e.x,e.y-16,'+30g','#e8c67a',11); }
  } else if(loot==='stem'||loot==='seal'){
    p.inv[loot]++;
    AudioSys.sfx('item');
    toast(ITEMS[loot].name+' taken.');
  } else if(loot==='shard'){
    p.hasShard=true; p.shardCharges=8;
    game.edge=2; p.aura=2;
    auraBurst(p.x,p.y);
    AudioSys.sfx('rare');
    toast('The Aether Shard bonds to your off-hand. (K to fire)');
  } else if(ACCS.includes(loot)){
    p.equipped[loot]=true;
    AudioSys.sfx('rare');
    toast(ITEMS[loot].name+' equipped — '+(loot==='band'?'strike faster.':loot==='lantern'?'hidden cracks glow blue in the dark.':'traps lose half their teeth.'));
  }
}

/* ================= ENEMIES ================= */
function updateEnemies(dt){
  const p=game.player;
  for(const e of game.ents){
    if(e.hp===undefined) continue;
    e.hitT=Math.max(0,e.hitT-dt);
    if(e.frozenT>0){ e.frozenT-=dt; e.burnT=0; continue; }
    if(e.burnT>0){
      e.burnT-=dt;
      e.burnAcc=(e.burnAcc||0)+dt;
      if(Math.random()<0.3) game.parts.push({x:e.x+rnd(-4,4),y:e.y+rnd(-6,2),vx:0,vy:-20,t:0,life:0.4,c:'#ff9d3c',s:1.4,g:-40});
      if(e.burnAcc>0.5){ e.burnAcc=0; e.hp-=1; if(e.hp<=0&&e.dieT===0){ killEnemy(e,p); } }
    }
    if(e.hp<=0){
      if(e.dieT>0){ e.dieT-=dt; if(e.dieT<=0&&!e.goldGiven){ e.goldGiven=true; enemyGoldDrop(e); } }
      continue;
    }
    const d=dist(p.x,p.y,e.x,e.y);
    const dDef=ENEMY_TYPES[e.type];
    // touch damage
    if(d<e.r+p.r+2 && e.touchCdT<=0 && !p.dead){
      hurtPlayer(dDef.dmg, e.x, e.y, 'melee');
      e.touchCdT=dDef.touchCd;
    }
    e.touchCdT=Math.max(0,(e.touchCdT||0)-dt);
    if(p.dead) continue;
    // behavior
    if(e.type==='crawler'||e.type==='sentinel'){
      if(d<400){
        const a=Math.atan2(p.y-e.y,p.x-e.x);
        e.ia=angN(e.ia!==undefined?e.ia:0);
        e.vx=lerp(e.vx, Math.cos(a)*dDef.sp, dt*3);
        e.vy=lerp(e.vy, Math.sin(a)*dDef.sp, dt*3);
        e.face=angN((e.face!==undefined?e.face:0)+angDiff(a,e.face)*dt*6);
      }
      if(d<700){ /* stay */ }
    }
    else if(e.type==='warden'){
      if(e.atkT>0){
        e.atkT-=dt;
        if(e.cd2){ // lunge in progress
          e.vx=e.cd2.vx; e.vy=e.cd2.vy;
          if(!e.cd2.hit){
            e.cd2.hit=dist(p.x,p.y,e.x,e.y)<26;
            if(e.cd2.hit&&p.invuln<=0&&!p.dead){ hurtPlayer(1,e.x,e.y,'melee'); }
          }
          if(Math.hypot(e.vx,e.vy)<10) e.cd2=null;
        }
      } else if(d<320){
        const a=Math.atan2(p.y-e.y,p.x-e.x);
        e.face=angN((e.face!==undefined?e.face:0)+angDiff(a,e.face)*dt*5);
        e.atkT=0.7+rnd(0,0.3);
        const sp=dDef.sp*4.2;
        e.cd2={vx:Math.cos(a)*sp, vy:Math.sin(a)*sp, hit:false};
        AudioSys.sfx('charge');
        spark(e.x,e.y,6,['#7fd4ff'],90,0.3,0);
      }
    }
    else if(e.type==='guardian'){
      if(e.atkT>0){
        e.atkT-=dt;
        if(e.atkT<=0) e.atkC=false;
        if(e.cd2){
          e.vx=e.cd2.vx; e.vy=e.cd2.vy;
          e.cd2.t-=dt;
          if(e.cd2.t<=0){ e.cd2=null; e.vx*=0.3; e.vy*=0.3; }
        }
      } else if(d<420){
        const a=Math.atan2(p.y-e.y,p.x-e.x);
        e.face=angN((e.face!==undefined?e.face:0)+angDiff(a,e.face)*dt*4);
        if(!e.atkC){
          e.atkT=0.9;
          e.cd2={vx:Math.cos(a)*dDef.sp*4, vy:Math.sin(a)*dDef.sp*4, t:0.5};
          e.atkC=true;
          AudioSys.sfx('boss');
          spark(e.x,e.y,10,['#ffd257','#ff9d3c'],120,0.5,0);
        }
      }
    }
    // integrate
    if(e.frozenT<=0){
      e.x+=e.vx*dt; e.y+=e.vy*dt;
      e.vx*=Math.pow(0.0009,dt); e.vy*=Math.pow(0.0009,dt);
    }
    // collide with walls (slide)
    if(!canStand(e.x,e.y,e.r)){
      if(canStand(e.x,e.y- (e.vy>0?e.r*2:-e.r*2), e.r)) e.y=e.y-(e.vy>0?e.r*2:-e.r*2);
      else { e.x-=e.vx*dt; e.y-=e.vy*dt; }
    }
  }
  // enemy-enemy separation
  const list=game.ents.filter(e=>e.hp!==undefined&&e.hp>0);
  for(let i=0;i<list.length;i++) for(let j=i+1;j<list.length;j++){
    const a=list[i], b=list[j];
    const d=dist(a.x,a.y,b.x,b.y);
    const min=(a.r+b.r)*0.8;
    if(d<min&&d>0.01){
      const push=(min-d)/2;
      const nx=(b.x-a.x)/d, ny=(b.y-a.y)/d;
      a.x-=nx*push; a.y-=ny*push;
      b.x+=nx*push; b.y+=ny*push;
    }
  }
}

/* ================= UI: SHOP & INVENTORY ================= */
function openShop(){
  game.state='shop';
  renderShop();
  $('ovShop').hidden=false;
  AudioSys.droneOn();
}
function renderShop(){
  const p=game.player, st=game.shopStock;
  $('shopGold').innerHTML='&nbsp;'+p.gold+'<span class="price"> golden sigils</span>';
  const rows=[];
  rows.push({k:'stem', nm:'Vital Stem', ds:'Restores 2 full hearts (4 HP).', price:50,
    avail:st.stem>0, note:st.stem>0?('left: '+st.stem):'sold out'});
  rows.push({k:'seal', nm:'Binding Seal', ds:'Freezes one construct for ~3 seconds.', price:75,
    avail:st.seal>0, note:st.seal>0?('left: '+st.seal):'sold out'});
  const canHeart=p.containers<5;
  rows.push({k:'heart', nm:'Heart Container ('+p.containers+'/5)', ds:'Widens the vigil to '+(p.containers+1)+' heart containers. The price of permanence.',
    price:300, avail:st.heart&&canHeart, note:!canHeart?'the vigil is already full':(st.heart?'one available':'already taken')});
  const canAmmo=p.hasShard;
  rows.push({k:'ammo', nm:'Aether Charge', ds:'Restock the Shard: +1 charge per purchase.', price:10,
    avail:canAmmo, note:canAmmo?'unlimited':('requires the Aether Shard (Zone 4)')});
  const body=$('shopBody');
  body.innerHTML='';
  for(const r of rows){
    const div=document.createElement('div'); div.className='row';
    div.innerHTML='<div><div class="nm">'+r.nm+'</div><div class="ds">'+r.ds+' — <i>'+r.note+'</i></div></div>'+
      '<div style="text-align:right"><div class="price">'+r.price+'g</div>'+
      '<button class="btn small" '+(r.avail&&p.gold>=r.price?'':'disabled')+'>'+
      (r.avail?'Buy':'—')+'</button></div>';
    const b=div.querySelector('button');
    if(r.avail){
      b.addEventListener('click',()=>buyItem(r.k,r.price));
    }
    body.appendChild(div);
  }
}
function buyItem(k,price){
  const p=game.player, st=game.shopStock;
  if(p.gold<price){ AudioSys.sfx('empty'); return; }
  p.gold-=price;
  if(k==='stem'){ st.stem--; p.inv.stem++; }
  else if(k==='seal'){ st.seal--; p.inv.seal++; }
  else if(k==='heart'){ st.heart=false; addContainer(p,'shop'); }
  else if(k==='ammo'){ p.shardCharges++; }
  AudioSys.sfx('buy');
  renderShop();
}
function renderInv(){
  const p=game.player;
  const body=$('invBody');
  let html='<div class="mnote" style="margin-top:0;margin-bottom:8px">Weapon — <b style="color:#e8c67a">'+WEAPONS[p.wpn].name+'</b>'+
    (p.wpn===1?' <i>(fire/ember: burn, faster)</i>':p.wpn===2?' <i>(light/solar: wide arc, heavy)</i>':'')+'</div>';
  for(const a of ACCS){
    const eq=p.equipped[a];
    html+='<div class="row"><div><div class="nm">'+ITEMS[a].name+'<span class="tag '+(eq?'on':'off')+'">'+(eq?'equipped':'')+'</span></div>'+
      '<div class="ds">'+ITEMS[a].desc+'</div></div>'+
      '<button class="btn small invEq" data-a="'+a+'">'+(eq?'Unequip':'Equip')+'</button></div>';
  }
  html+='<div class="mnote" style="margin:10px 0 4px">Consumables</div>';
  html+='<div class="row"><div><div class="nm">Vital Stem <span class="tag off">×'+p.inv.stem+'</span> <span class="tag off">1</span></div>'+
    '<div class="ds">'+ITEMS.stem.desc+'</div></div>'+
    '<button class="btn small invUse" data-k="stem">Use</button></div>';
  html+='<div class="row"><div><div class="nm">Binding Seal <span class="tag off">×'+p.inv.seal+'</span> <span class="tag off">2</span></div>'+
    '<div class="ds">'+ITEMS.seal.desc+'</div></div>'+
    '<button class="btn small invUse" data-k="seal">Use</button></div>';
  if(p.hasShard){
    html+='<div class="row"><div><div class="nm">Aether Shard <span class="tag off">×'+p.shardCharges+' charges</span></div>'+
      '<div class="ds">A crystalline launcher. Fire with <b>K</b> or <b>X</b> — strikes distant switches and weak barriers. Restock at the merchant (10g).</div></div></div>';
  } else {
    html+='<div class="row"><div><div class="nm">Aether Shard</div><div class="ds">Not yet found. It sleeps in the Sunken Vault (Zone 4), behind a wall of hairline cracks.</div></div></div>';
  }
  html+='<div class="mnote">The merchant keeps the archives’ heart (Zone 2). Sanctuary Stones hold your progress.</div>';
  body.innerHTML=html;
  body.querySelectorAll('.invEq').forEach(b=>b.addEventListener('click',()=>{
    const a=b.dataset.a; p.equipped[a]=!p.equipped[a];
    AudioSys.sfx('item'); renderInv();
  }));
  body.querySelectorAll('.invUse').forEach(b=>b.addEventListener('click',()=>{
    useItem(b.dataset.k); renderInv();
  }));
}
function closeInv(){ $('ovInv').hidden=true; game.state='playing'; }

/* ================= RENDER ================= */
const PAL=[
  {floor:'#2a2621', floor2:'#242019', wall:'#57452c', wall2:'#3a2f1e', amb:'rgba(214,160,90,0.05)'},
  {floor:'#25221c', floor2:'#201d18', wall:'#4c3f2e', wall2:'#332a1e', amb:'rgba(200,150,80,0.04)'},
  {floor:'#282429', floor2:'#232024', wall:'#4a4152', wall2:'#322b38', amb:'rgba(170,140,200,0.035)'},
  {floor:'#1e262d', floor2:'#1a2127', wall:'#3d4c59', wall2:'#2a353f', amb:'rgba(110,170,220,0.05)'},
  {floor:'#26232e', floor2:'#211e28', wall:'#463d52', wall2:'#2f2937', amb:'rgba(150,130,200,0.04)'},
  {floor:'#231f2b', floor2:'#1e1b25', wall:'#4b3f5a', wall2:'#332a40', amb:'rgba(255,200,120,0.05)'}
];
function hash2(x,y){ let h=x*374761393+y*668265263; h=(h^(h>>13))*1274126177; return ((h^(h>>16))>>>0)/4294967295; }
function camera(){
  const {w,h}=ZONES[game.zone];
  const wpx=w*T, hpx=h*T;
  const cx = wpx<CW ? (wpx-CW)/2 : clamp(game.player.x-CW/2, 0, wpx-CW);
  const cy = hpx<CH ? (hpx-CH)/2 : clamp(game.player.y-CH/2, 0, hpx-CH);
  return {x:cx, y:cy};
}
function render(){
  ctx.setTransform(1,0,0,1,0,0);
  ctx.clearRect(0,0,CW,CH);
  const pal=PAL[game.zone]||PAL[0];
  // base
  ctx.fillStyle='#070a10'; ctx.fillRect(0,0,CW,CH);
  if(game.state==='title'){ renderTitle(); return; }
  if(!game.player) return;
  const cam=camera();
  const sh=game.shake;
  const ox=-cam.x + (sh?rnd(-sh,sh):0), oy=-cam.y + (sh?rnd(-sh,sh):0);
  ctx.save();
  ctx.translate(ox,oy);
  drawFloor(pal,cam);
  drawPlatesAndTraps();
  drawWorldEntities();
  drawEnemies();
  drawPlayer();
  drawBolts();
  drawParticles();
  drawWalls(pal,cam);
  drawFloaters();
  ctx.restore();
  // lantern glow (screen)
  if(game.player.equipped.lantern){
    const p=game.player;
    const px=p.x-cam.x, py=p.y-cam.y;
    const g=ctx.createRadialGradient(px,py,20,px,py,150);
    g.addColorStop(0,'rgba(90,168,220,0.14)'); g.addColorStop(1,'rgba(90,168,220,0)');
    ctx.fillStyle=g; ctx.fillRect(px-160,py-160,320,320);
  }
  // ambient zone tint
  ctx.fillStyle=pal.amb; ctx.fillRect(0,0,CW,CH);
  drawVignette(pal);
  drawHUD();
  drawEdgeFx();
  drawBanner();
}
function renderTitle(){
  // quiet attract scene
  const t=game.time;
  const g=ctx.createLinearGradient(0,0,0,CH);
  g.addColorStop(0,'#0a0f18'); g.addColorStop(0.6,'#141019'); g.addColorStop(1,'#070a10');
  ctx.fillStyle=g; ctx.fillRect(0,0,CW,CH);
  // gear ring
  ctx.save(); ctx.translate(CW/2, CH/2+30);
  for(let i=0;i<9;i++){
    const a=t*0.08+i*6.283/9;
    ctx.save(); ctx.translate(Math.cos(a)*210, Math.sin(a)*210*0.55);
    drawGear(26, i*0.7+t*(i%2?0.3:-0.3), 'rgba(160,120,60,0.25)');
    ctx.restore();
  }
  drawGear(64, -t*0.12, 'rgba(200,150,70,0.3)');
  ctx.restore();
  // falling ash
  for(let i=0;i<40;i++){
    const x=(hash2(i,7)*CW + t*12*(1+hash2(i,3)))%CW;
    const y=(hash2(i,11)*CH + t*24*(1+hash2(i,5)))%CH;
    ctx.fillStyle='rgba(200,170,120,'+(0.05+hash2(i,9)*0.12)+')';
    ctx.fillRect(x,y,1.6,1.6);
  }
}
function drawGear(r, rot, col){
  ctx.save(); ctx.rotate(rot);
  ctx.strokeStyle=col; ctx.lineWidth=3;
  ctx.beginPath(); ctx.arc(0,0,r,0,6.283); ctx.stroke();
  for(let i=0;i<8;i++){
    const a=i*6.283/8;
    ctx.beginPath();
    ctx.moveTo(Math.cos(a)*r, Math.sin(a)*r);
    ctx.lineTo(Math.cos(a)*(r+6), Math.sin(a)*(r+6));
    ctx.stroke();
  }
  ctx.restore();
}
function drawFloor(pal,cam){
  const {w,h}=ZONES[game.zone];
  const x0=Math.max(0,Math.floor(cam.x/T)-1), x1=Math.min(w-1,Math.ceil((cam.x+CW)/T)+1);
  const y0=Math.max(0,Math.floor(cam.y/T)-1), y1=Math.min(h-1,Math.ceil((cam.y+CH)/T)+1);
  for(let y=y0;y<=y1;y++) for(let x=x0;x<=x1;x++){
    const g=game.maps[game.zone][y*w+x];
    if(g===1) continue;
    ctx.fillStyle=(x+y)%2?pal.floor:pal.floor2;
    ctx.fillRect(x*T,y*T,T,T);
    if(hash2(x,y)<0.16){
      ctx.fillStyle='rgba(0,0,0,0.14)';
      ctx.fillRect(x*T+6,y*T+6,10,3);
    }
    if(g>=3&&g<=7) drawDecoTile(g,x,y);
    if(g>=5&&g<=9) drawCracks(g,x,y);
    if(g===2) drawDoorTile(x,y);
  }
}
function drawDecoTile(v,x,y){
  const px=x*T, py=y*T;
  ctx.save(); ctx.translate(px+16,py+16);
  if(v===3){ // pillar base
    ctx.fillStyle='rgba(0,0,0,0.25)'; ctx.fillRect(-10,-10,20,20);
    ctx.strokeStyle='rgba(180,140,80,0.5)'; ctx.lineWidth=2;
    ctx.strokeRect(-8,-8,16,16);
    ctx.fillStyle='rgba(140,110,60,0.4)'; ctx.fillRect(-4,-4,8,8);
  } else if(v===4){ // rubble
    ctx.fillStyle='rgba(0,0,0,0.22)';
    ctx.beginPath(); ctx.moveTo(-10,8); ctx.lineTo(0,-6); ctx.lineTo(10,8); ctx.closePath(); ctx.fill();
    ctx.fillStyle='rgba(120,100,70,0.3)'; ctx.fillRect(-3,0,6,4);
  } else if(v===6){ // grate
    ctx.fillStyle='rgba(0,0,0,0.4)'; ctx.fillRect(-11,-11,22,22);
    ctx.strokeStyle='rgba(150,120,70,0.5)'; ctx.lineWidth=1.5;
    for(let i=-8;i<=8;i+=4){ ctx.beginPath(); ctx.moveTo(i,-10); ctx.lineTo(i,10); ctx.stroke(); }
  } else if(v===7){ // mosaic
    ctx.strokeStyle='rgba(190,150,80,0.35)'; ctx.lineWidth=1.5;
    ctx.beginPath(); ctx.arc(0,0,9,0,6.283); ctx.stroke();
    ctx.beginPath(); ctx.arc(0,0,4,0,6.283); ctx.stroke();
  }
  ctx.restore();
}
function drawCracks(v,x,y){
  const px=x*T+16, py=y*T+16;
  const strong=(v===9)?(game.player.equipped.lantern?0.9:0.28):0.7;
  const base=v===8;
  ctx.save(); ctx.translate(px,py);
  if(base){
    ctx.fillStyle='rgba(70,110,160,0.12)'; ctx.fillRect(-14,-14,28,28);
    const gl=0.4+0.3*Math.sin(game.time*3+x*2+y);
    ctx.strokeStyle='rgba(127,212,255,'+(0.3+gl*0.4)+')';
  } else {
    ctx.strokeStyle='rgba(20,12,8,'+strong+')';
  }
  ctx.lineWidth=base?1:1.4;
  ctx.beginPath();
  ctx.moveTo(-10,-12); ctx.lineTo(-3,-4); ctx.lineTo(-7,4); ctx.lineTo(2,12);
  ctx.moveTo(-3,-4); ctx.lineTo(6,-8); ctx.lineTo(12,-2);
  ctx.moveTo(-7,4); ctx.lineTo(4,7);
  ctx.stroke();
  if(game.player.equipped.lantern){
    ctx.strokeStyle='rgba(127,212,255,0.75)'; ctx.lineWidth=1;
    ctx.beginPath();
    ctx.moveTo(-10,-12); ctx.lineTo(-3,-4); ctx.lineTo(-7,4); ctx.lineTo(2,12);
    ctx.stroke();
  }
  ctx.restore();
}
function drawDoorTile(x,y){
  const px=x*T, py=y*T;
  ctx.save(); ctx.translate(px+16,py+16);
  ctx.fillStyle='rgba(0,0,0,0.5)'; ctx.fillRect(-12,-15,24,30);
  const d=zoneDoorPos();
  const locked=d&&d.p>game.lastSaveZone;
  const gl=0.5+0.4*Math.sin(game.time*2);
  ctx.strokeStyle=locked?'rgba(90,80,90,0.8)':'rgba(230,180,90,'+(0.5+gl*0.4)+')';
  ctx.lineWidth=3;
  ctx.beginPath();
  ctx.moveTo(-12,14); ctx.lineTo(-12,-6); ctx.arc(0,-6,12,Math.PI,0); ctx.lineTo(12,14);
  ctx.stroke();
  ctx.restore();
}
function drawPlatesAndTraps(){
  for(const e of game.ents){
    if(e.type==='plate'){
      const p=game.player;
      const pressed=dist(p.x,p.y,e.x,e.y)<16;
      ctx.save(); ctx.translate(e.x,e.y);
      ctx.fillStyle='rgba(0,0,0,0.35)'; ctx.fillRect(-13,-13,26,26);
      ctx.fillStyle=pressed?'#7a5c2e':'#5c4a2c';
      ctx.beginPath(); ctx.arc(0,0,11,0,6.283); ctx.fill();
      ctx.strokeStyle='rgba(230,180,90,0.7)'; ctx.lineWidth=1.5; ctx.stroke();
      if(pressed){
        const g=ctx.createRadialGradient(0,0,2,0,0,18);
        g.addColorStop(0,'rgba(255,210,120,0.35)'); g.addColorStop(1,'rgba(255,210,120,0)');
        ctx.fillStyle=g; ctx.fillRect(-20,-20,40,40);
      }
      ctx.restore();
    }
    else if(e.type==='trap'){
      ctx.save(); ctx.translate(e.x,e.y);
      ctx.fillStyle='rgba(0,0,0,0.4)'; ctx.fillRect(-13,-13,26,26);
      ctx.strokeStyle='rgba(150,60,40,0.8)'; ctx.lineWidth=1.5;
      ctx.strokeRect(-13,-13,26,26);
      if(e.on){
        ctx.fillStyle='#a03a28';
        for(let i=-1;i<=1;i++){
          ctx.beginPath();
          ctx.moveTo(i*8-3,10); ctx.lineTo(i*8,-8- (i===0?4:0)); ctx.lineTo(i*8+3,10);
          ctx.closePath(); ctx.fill();
        }
      }
      ctx.restore();
    }
  }
}
function drawWorldEntities(){
  const p=game.player;
  for(const e of game.ents){
    switch(e.type||e.kind){
      case 'chest': drawChest(e); break;
      case 'heart': drawHeartItem(e); break;
      case 'shard': drawShardItem(e); break;
      case 'stone': drawStone(e); break;
      case 'altar': drawAltar(e); break;
      case 'merchant': drawMerchant(e); break;
      case 'crystal': drawCrystal(e); break;
      case 'beacon': drawBeacon(e); break;
    }
  }
}
function drawChest(e){
  const open=e.openT>0||e.opened;
  ctx.save(); ctx.translate(e.x,e.y);
  ctx.fillStyle='rgba(0,0,0,0.3)'; ctx.beginPath(); ctx.ellipse(0,8,10,4,0,0,6.283); ctx.fill();
  ctx.fillStyle=open?'#5c4a2e':'#6a5636';
  ctx.fillRect(-9,-6,18,12);
  ctx.fillStyle=open?'#7a6542':'#8a6a3f';
  ctx.fillRect(-9,-10,18,5);
  ctx.strokeStyle='#d9a441'; ctx.lineWidth=1.5;
  ctx.strokeRect(-9,-10,18,16);
  ctx.fillStyle='#f0d9a0'; ctx.fillRect(-1.5,-6,3,6);
  if(!open){
    const gl=0.3+0.3*Math.sin(game.time*2.5+e.x);
    const g=ctx.createRadialGradient(0,0,2,0,0,20);
    g.addColorStop(0,'rgba(255,230,160,'+gl*0.4+')'); g.addColorStop(1,'rgba(255,230,160,0)');
    ctx.fillStyle=g; ctx.fillRect(-22,-22,44,44);
  }
  ctx.restore();
}
function drawHeartItem(e){
  ctx.save(); ctx.translate(e.x,e.y+Math.sin(game.time*3+e.x)*3);
  heartPath(0,0,11);
  ctx.fillStyle='#c0452e'; ctx.fill();
  ctx.strokeStyle='#f0d9a0'; ctx.lineWidth=1.5; ctx.stroke();
  const g=ctx.createRadialGradient(0,0,2,0,0,18);
  g.addColorStop(0,'rgba(255,120,90,0.5)'); g.addColorStop(1,'rgba(255,120,90,0)');
  ctx.fillStyle=g; ctx.fillRect(-20,-20,40,40);
  ctx.restore();
}
function drawShardItem(e){
  ctx.save(); ctx.translate(e.x,e.y+Math.sin(game.time*2.6+e.x)*3);
  ctx.rotate(game.time*0.8);
  ctx.beginPath(); ctx.moveTo(0,-10); ctx.lineTo(7,0); ctx.lineTo(0,10); ctx.lineTo(-7,0); ctx.closePath();
  ctx.fillStyle='#7fd4ff'; ctx.fill();
  ctx.strokeStyle='#fff'; ctx.lineWidth=1.5; ctx.stroke();
  const g=ctx.createRadialGradient(0,0,2,0,0,22);
  g.addColorStop(0,'rgba(127,212,255,0.5)'); g.addColorStop(1,'rgba(127,212,255,0)');
  ctx.fillStyle=g; ctx.fillRect(-24,-24,48,48);
  ctx.restore();
}
function drawStone(e){
  const active=game.lastSaveZone===game.zone;
  ctx.save(); ctx.translate(e.x,e.y);
  ctx.fillStyle='rgba(0,0,0,0.35)'; ctx.beginPath(); ctx.ellipse(0,12,14,5,0,0,6.283); ctx.fill();
  ctx.fillStyle=active?'#5a5268':'#3c3844';
  ctx.fillRect(-8,-4,16,16);
  ctx.beginPath(); ctx.moveTo(-9,-4); ctx.lineTo(9,-4); ctx.lineTo(6,-10); ctx.lineTo(-6,-10); ctx.closePath(); ctx.fill();
  const gl=active?(0.5+0.4*Math.sin(game.time*2.4)):0.15;
  ctx.beginPath(); ctx.moveTo(0,-24); ctx.lineTo(6,-12); ctx.lineTo(0,-4); ctx.lineTo(-6,-12); ctx.closePath();
  ctx.fillStyle='rgba(255,210,120,'+(0.35+gl*0.5)+')'; ctx.fill();
  ctx.strokeStyle='#f0d9a0'; ctx.lineWidth=1; ctx.stroke();
  const g=ctx.createRadialGradient(0,-14,4,0,-14,40);
  g.addColorStop(0,'rgba(255,210,120,'+gl*0.25+')'); g.addColorStop(1,'rgba(255,210,120,0)');
  ctx.fillStyle=g; ctx.fillRect(-44,-58,88,88);
  ctx.restore();
  if(active && Math.random()<0.08){
    game.parts.push({x:e.x+rnd(-10,10), y:e.y-rnd(4,26), vx:rnd(-6,6), vy:-rnd(8,20),
      t:0, life:rnd(0.6,1.2), c:'#f0d9a0', s:rnd(1,2), g:-6});
  }
}
function drawAltar(e){
  const u2=e.kind==='u2';
  ctx.save(); ctx.translate(e.x,e.y);
  ctx.fillStyle='rgba(0,0,0,0.35)'; ctx.beginPath(); ctx.ellipse(0,10,14,5,0,0,6.283); ctx.fill();
  ctx.fillStyle='#4a4256'; ctx.fillRect(-10,-2,20,12);
  ctx.fillStyle='#5a5268'; ctx.beginPath(); ctx.ellipse(0,-2,11,4,0,0,6.283); ctx.fill();
  const fl=e.done?1:0.7;
  const col=u2?'255,210,90':'255,157,60';
  const flh=(8+3*Math.sin(game.time*6))*(1+fl*0.3);
  ctx.beginPath(); ctx.moveTo(0,-4-flh); ctx.quadraticCurveTo(6,-4-flh*0.4,0,-2); ctx.quadraticCurveTo(-6,-4-flh*0.4,0,-4-flh);
  ctx.fillStyle='rgba('+col+','+(0.5+fl*0.3)+')'; ctx.fill();
  if(e.done){
    const g=ctx.createRadialGradient(0,-8,4,0,-8,36);
    g.addColorStop(0,'rgba('+col+',0.3)'); g.addColorStop(1,'rgba('+col+',0)');
    ctx.fillStyle=g; ctx.fillRect(-40,-50,80,80);
  }
  ctx.restore();
}
function drawMerchant(e){
  ctx.save(); ctx.translate(e.x,e.y);
  ctx.fillStyle='rgba(0,0,0,0.3)'; ctx.beginPath(); ctx.ellipse(0,10,12,4,0,0,6.283); ctx.fill();
  ctx.fillStyle='#3a4256';
  ctx.beginPath(); ctx.moveTo(-8,10); ctx.lineTo(-5,-8); ctx.lineTo(5,-8); ctx.lineTo(8,10); ctx.closePath(); ctx.fill();
  ctx.fillStyle='#222838'; ctx.beginPath(); ctx.arc(0,-11,5,0,6.283); ctx.fill();
  ctx.fillStyle='#d9a441'; ctx.fillRect(-2,-12,4,2);
  const gl=0.4+0.3*Math.sin(game.time*3);
  ctx.fillStyle='rgba(230,180,90,'+gl+')';
  ctx.beginPath(); ctx.arc(0,-20,2.5,0,6.283); ctx.fill();
  const g=ctx.createRadialGradient(0,-6,6,0,-6,44);
  g.addColorStop(0,'rgba(120,160,220,0.10)'); g.addColorStop(1,'rgba(120,160,220,0)');
  ctx.fillStyle=g; ctx.fillRect(-48,-54,96,96);
  ctx.restore();
}
function drawCrystal(e){
  const lit=game.zone===5&&e.hitT!==undefined;
  ctx.save(); ctx.translate(e.x,e.y+Math.sin(game.time*2+e.x)*2);
  ctx.beginPath(); ctx.moveTo(0,-14); ctx.lineTo(8,0); ctx.lineTo(0,14); ctx.lineTo(-8,0); ctx.closePath();
  ctx.fillStyle=lit?'#fff3d0':'#7fd4ff'; ctx.fill();
  ctx.strokeStyle='#fff'; ctx.lineWidth=1.5; ctx.stroke();
  const g=ctx.createRadialGradient(0,0,3,0,0,26);
  g.addColorStop(0,lit?'rgba(255,240,200,0.5)':'rgba(127,212,255,0.45)');
  g.addColorStop(1,'rgba(0,0,0,0)');
  ctx.fillStyle=g; ctx.fillRect(-28,-28,56,56);
  ctx.restore();
}
function drawBeacon(e){
  ctx.save(); ctx.translate(e.x,e.y);
  ctx.fillStyle='rgba(0,0,0,0.4)'; ctx.beginPath(); ctx.ellipse(0,14,18,6,0,0,6.283); ctx.fill();
  ctx.fillStyle='#3a3248'; ctx.fillRect(-8,-14,16,26);
  ctx.strokeStyle='#d9a441'; ctx.lineWidth=2;
  ctx.beginPath(); ctx.arc(0,-16,9,0,6.283); ctx.stroke();
  drawGear(7, game.time*(e.lit?2:0.3), e.lit?'rgba(255,220,140,0.9)':'rgba(160,120,60,0.5)');
  if(e.lit){
    e.animT+=0.016;
    const h=140+20*Math.sin(e.animT*2);
    const g=ctx.createLinearGradient(0,-16,0,-16-h);
    g.addColorStop(0,'rgba(255,220,140,0.85)'); g.addColorStop(1,'rgba(255,220,140,0)');
    ctx.fillStyle=g;
    ctx.beginPath(); ctx.moveTo(-7,-16); ctx.lineTo(7,-16); ctx.lineTo(16,-16-h); ctx.lineTo(-16,-16-h); ctx.closePath(); ctx.fill();
    if(Math.random()<0.5) game.parts.push({x:e.x+rnd(-8,8), y:e.y-rnd(16,60), vx:rnd(-10,10), vy:-rnd(20,50),
      t:0,life:rnd(0.5,1),c:'#ffd257',s:rnd(1,2.5),g:-20});
  }
  ctx.restore();
}
function heartPath(x,y,s){
  ctx.beginPath();
  ctx.moveTo(x, y+s*0.32);
  ctx.bezierCurveTo(x, y, x-s*0.52, y, x-s*0.52, y+s*0.34);
  ctx.bezierCurveTo(x-s*0.52, y+s*0.62, x-s*0.1, y+s*0.8, x, y+s*0.95);
  ctx.bezierCurveTo(x+s*0.1, y+s*0.8, x+s*0.52, y+s*0.62, x+s*0.52, y+s*0.34);
  ctx.bezierCurveTo(x+s*0.52, y, x, y, x, y+s*0.32);
  ctx.closePath();
}
function drawEnemies(){
  const p=game.player;
  for(const e of game.ents){
    if(e.hp===undefined) continue;
    const alive=e.hp>0;
    if(!alive && e.dieT<=0) continue;
    const def=ENEMY_TYPES[e.type];
    const alpha=!alive?clamp(e.dieT*2,0,1):1;
    const bob=Math.sin(game.time*7+e.sx)* (Math.hypot(e.vx,e.vy)>8?1.8:0.8);
    ctx.save(); ctx.translate(e.x, e.y+bob*0.5); ctx.globalAlpha=alpha;
    const face=e.face!==undefined?e.face:(p?Math.atan2(p.y-e.y,p.x-e.x):0);
    if(e.type==='crawler'){
      ctx.strokeStyle='#2c221a'; ctx.lineWidth=1.6;
      for(let i=0;i<3;i++){
        const la=game.time*10+e.sx+i;
        ctx.beginPath(); ctx.moveTo(-6,2+i*3); ctx.lineTo(-10-Math.abs(Math.sin(la))*4, 5+i*3); ctx.stroke();
        ctx.beginPath(); ctx.moveTo(6,2+i*3); ctx.lineTo(10+Math.abs(Math.sin(la+1))*4, 5+i*3); ctx.stroke();
      }
      ctx.fillStyle=ECOL[0][0];
      ctx.beginPath(); ctx.ellipse(0,0,9,7,0,0,6.283); ctx.fill();
      ctx.strokeStyle=ECOL[0][1]; ctx.lineWidth=1.5; ctx.stroke();
      ctx.fillStyle=def.eye; ctx.beginPath(); ctx.arc(Math.cos(face)*4, Math.sin(face)*4-2,2,0,6.283); ctx.fill();
    }
    else if(e.type==='sentinel'){
      ctx.fillStyle=ECOL[1][0];
      ctx.fillRect(-7,-11,14,20);
      ctx.strokeStyle=ECOL[1][1]; ctx.lineWidth=1.6; ctx.strokeRect(-7,-11,14,20);
      ctx.fillStyle='#3a4252';
      ctx.fillRect(Math.cos(face)*4-5, Math.sin(face)*4-6, 10, 14);
      ctx.fillStyle=def.eye; ctx.beginPath(); ctx.arc(Math.cos(face)*3, Math.sin(face)*3-4,2.2,0,6.283); ctx.fill();
    }
    else if(e.type==='warden'){
      ctx.fillStyle=ECOL[2][0];
      ctx.beginPath(); ctx.moveTo(-8,12); ctx.lineTo(-5,-10); ctx.lineTo(5,-10); ctx.lineTo(8,12); ctx.closePath(); ctx.fill();
      ctx.strokeStyle=ECOL[2][1]; ctx.lineWidth=1.6; ctx.stroke();
      ctx.fillStyle='#221a14'; ctx.beginPath(); ctx.arc(0,-9,5,Math.PI,0); ctx.fill();
      if(e.cd2){
        const gl=0.5+0.4*Math.sin(game.time*14);
        ctx.strokeStyle='rgba(127,212,255,'+gl+')'; ctx.lineWidth=2;
        ctx.beginPath(); ctx.arc(0,0,15,face-0.9,face+0.9); ctx.stroke();
      }
      ctx.fillStyle=def.eye; ctx.beginPath(); ctx.arc(Math.cos(face)*3, Math.sin(face)*3-8,2,0,6.283); ctx.fill();
    }
    else if(e.type==='guardian'){
      ctx.fillStyle=ECOL[3][0];
      ctx.beginPath(); ctx.moveTo(-12,16); ctx.lineTo(-9,-14); ctx.lineTo(9,-14); ctx.lineTo(12,16); ctx.closePath(); ctx.fill();
      ctx.strokeStyle=ECOL[3][1]; ctx.lineWidth=2; ctx.stroke();
      ctx.fillStyle='#2a3040';
      ctx.fillRect(-14,-4,5,16); ctx.fillRect(9,-4,5,16);
      ctx.fillStyle=ECOL[3][0];
      ctx.beginPath(); ctx.moveTo(-9,-14); ctx.lineTo(-13,-22); ctx.lineTo(-6,-16); ctx.closePath(); ctx.fill();
      ctx.beginPath(); ctx.moveTo(9,-14); ctx.lineTo(13,-22); ctx.lineTo(6,-16); ctx.closePath(); ctx.fill();
      ctx.fillStyle=def.eye;
      ctx.beginPath(); ctx.arc(Math.cos(face)*4-2, Math.sin(face)*4-8,2.6,0,6.283); ctx.fill();
      ctx.beginPath(); ctx.arc(Math.cos(face)*4+4, Math.sin(face)*4-8,2.6,0,6.283); ctx.fill();
    }
    if(e.hitT>0){
      ctx.globalAlpha=alpha*0.7;
      ctx.fillStyle='#fff';
      ctx.beginPath(); ctx.ellipse(0,0,10,12,0,0,6.283); ctx.fill();
    }
    if(e.frozenT>0){
      ctx.globalAlpha=alpha*0.75;
      ctx.strokeStyle='#7fd4ff'; ctx.lineWidth=2;
      ctx.beginPath(); ctx.ellipse(0,0,11,13,0,0,6.283); ctx.stroke();
      ctx.fillStyle='rgba(127,212,255,0.25)'; ctx.fill();
    }
    ctx.restore();
    // boss bar
    if(def.boss&&alive){
      ctx.save();
      const bw=120, bx=e.x-60, by=e.y-26;
      ctx.fillStyle='rgba(0,0,0,0.6)'; ctx.fillRect(bx-1,by-1,bw+2,6);
      ctx.fillStyle='#3a2a22'; ctx.fillRect(bx,by,bw,4);
      ctx.fillStyle=e.type==='guardian'?'#ffd257':'#7fd4ff';
      ctx.fillRect(bx,by,bw*clamp(e.hp/e.maxhp,0,1),4);
      ctx.fillStyle='rgba(232,217,176,0.85)';
      ctx.font='9px Georgia'; ctx.textAlign='center';
      ctx.fillText(def.name, e.x, by-3);
      ctx.restore();
    }
  }
}
function drawPlayer(){
  const p=game.player;
  if(!p||p.dead) return;
  const cam=camera();
  const px=p.x-cam.x, py=p.y-cam.y;
  ctx.save(); ctx.translate(px,py);
  const bob=p.moving?Math.sin(game.time*10)*1.5:0;
  const w=p.wpn;
  // shadow
  ctx.fillStyle='rgba(0,0,0,0.35)';
  ctx.beginPath(); ctx.ellipse(0,9, w===2?10:8, 3.5,0,0,6.283); ctx.fill();
  // cloak
  ctx.translate(0,bob*0.5);
  const cloakCol=w===0?'#4a4458':w===1?'#54465e':'#5c4c3c';
  ctx.fillStyle=cloakCol;
  ctx.beginPath();
  const baseW=w===2?9:7;
  ctx.moveTo(-baseW,8); ctx.quadraticCurveTo(-6,-8, 0,-10); ctx.quadraticCurveTo(6,-8, baseW,8);
  ctx.quadraticCurveTo(0,11,-baseW,8);
  ctx.closePath(); ctx.fill();
  if(p.equipped.cloak){
    ctx.strokeStyle='#14121c'; ctx.lineWidth=2.5;
    ctx.beginPath(); ctx.moveTo(-baseW+1,7); ctx.quadraticCurveTo(0,10,baseW-1,7); ctx.stroke();
  }
  if(w===2 && Math.random()<0.25){
    ctx.fillStyle='rgba(255,210,120,0.5)';
    ctx.fillRect(rnd(-baseW,baseW), rnd(-6,6), 1.5, 1.5);
  }
  // head
  ctx.fillStyle='#d8c9a8';
  ctx.beginPath(); ctx.arc(0,-11,4.2,0,6.283); ctx.fill();
  ctx.fillStyle=cloakCol;
  ctx.beginPath(); ctx.arc(0,-12.5,4.4,Math.PI*0.9,Math.PI*2.1); ctx.fill();
  // accessories
  if(p.equipped.band){
    ctx.strokeStyle='#cfcfe8'; ctx.lineWidth=2;
    ctx.beginPath(); ctx.arc(7,2+bob,3,0,6.283); ctx.stroke();
  }
  if(p.equipped.lantern){
    const gl=0.5+0.35*Math.sin(game.time*4);
    ctx.fillStyle='rgba(127,212,255,'+(0.6+gl*0.4)+')';
    ctx.beginPath(); ctx.arc(-9,-4+bob,2.5,0,6.283); ctx.fill();
    const g=ctx.createRadialGradient(-9,-4,1,-9,-4,18);
    g.addColorStop(0,'rgba(127,212,255,0.35)'); g.addColorStop(1,'rgba(127,212,255,0)');
    ctx.fillStyle=g; ctx.fillRect(-30,-24,40,40);
  }
  if(p.equipped.cloak && !p.equipped.band){
    ctx.fillStyle='#cfcfe8'; ctx.fillRect(3,-9,2,2);
  }
  // blade
  const wIdx=w;
  let bAng=p.dir, bLen=WEAPONS[wIdx].range*0.72;
  if(p.atkPhase==='windup'){
    const t=p.atkT/WEAPONS[wIdx].wind;
    bAng=p.dir-WEAPONS[wIdx].arc*0.55*t;
  } else if(p.atkPhase==='swing'){
    const t=clamp(p.atkT/WEAPONS[wIdx].swing,0,1);
    bAng=p.dir-WEAPONS[wIdx].arc/2 + WEAPONS[wIdx].arc*ease(t);
    p.trail.push({a:bAng,t:0});
    if(p.trail.length>10) p.trail.shift();
  }
  for(let i=p.trail.length-1;i>=0;i--){ p.trail[i].t+=1/60; if(p.trail[i].t>0.22) p.trail.splice(i,1); }
  if(p.wpn>=1){
    for(const tr of p.trail){
      const a=tr.a;
      ctx.strokeStyle=w===1?'rgba(255,138,60,'+(0.5*tr.t/0.22)+')':'rgba(255,210,120,'+(0.5*tr.t/0.22)+')';
      ctx.lineWidth=3;
      ctx.beginPath();
      ctx.arc(0,0,bLen*0.9, a-0.22, a+0.06);
      ctx.stroke();
    }
  }
  ctx.save();
  ctx.rotate(bAng);
  ctx.strokeStyle=w===0?'#c8b898':w===1?'#ff9d3c':'#ffd257';
  ctx.lineWidth=w===2?3.4:2.6;
  if(w>0){ ctx.shadowColor=w===1?'#ff8a3c':'#ffd257'; ctx.shadowBlur=10; }
  ctx.beginPath(); ctx.moveTo(5,0); ctx.lineTo(bLen,0); ctx.stroke();
  if(w===2){
    ctx.strokeStyle='rgba(255,240,200,0.8)'; ctx.lineWidth=1.4;
    ctx.beginPath(); ctx.moveTo(8,0); ctx.lineTo(bLen-2,0); ctx.stroke();
  }
  ctx.restore();
  // off-hand shard
  if(p.hasShard){
    const sa=p.dir+Math.PI/2;
    const sx=Math.cos(sa)*8, sy=Math.sin(sa)*8;
    const pul=0.6+0.4*Math.sin(game.time*5);
    ctx.fillStyle='rgba(127,212,255,'+pul+')';
    ctx.beginPath(); ctx.moveTo(sx,sy-4); ctx.lineTo(sx+3,sy); ctx.lineTo(sx,sy+4); ctx.lineTo(sx-3,sy); ctx.closePath(); ctx.fill();
  }
  // windup glow
  if(p.atkPhase==='windup'&&w===2){
    const t=p.atkT/WEAPONS[2].wind;
    const g=ctx.createRadialGradient(0,0,2,0,0,26);
    g.addColorStop(0,'rgba(255,220,140,'+t*0.5+')'); g.addColorStop(1,'rgba(255,220,140,0)');
    ctx.fillStyle=g; ctx.fillRect(-28,-28,56,56);
  }
  ctx.restore();
  // invuln flicker & hit flash (screen-space over player)
  if(p.invuln>0 && Math.floor(game.time*20)%2===0){
    ctx.save(); ctx.globalAlpha=0.3;
    heartPath(px,py-14,14); ctx.fillStyle='#fff'; ctx.fill();
    ctx.restore();
  }
  if(p.hitFlash>0){
    ctx.save(); ctx.globalAlpha=p.hitFlash*0.5;
    ctx.fillStyle='#ff4020';
    ctx.beginPath(); ctx.arc(px,py,14,0,6.283); ctx.fill();
    ctx.restore();
  }
  if(p.aura>0){
    ctx.save();
    const t=1-p.aura/2;
    ctx.strokeStyle='rgba(255,220,140,'+(0.7*(1-t))+')';
    ctx.lineWidth=2;
    ctx.beginPath(); ctx.arc(px,py,16+t*30,0,6.283); ctx.stroke();
    ctx.restore();
  }
}
function drawBolts(){
  for(const b of game.bolts){
    const cam=camera();
    const px=b.x-cam.x, py=b.y-cam.y;
    ctx.save(); ctx.translate(px,py);
    const a=Math.atan2(b.vy,b.vx);
    ctx.rotate(a);
    const g=ctx.createRadialGradient(0,0,1,0,0,12);
    g.addColorStop(0,'rgba(255,255,255,0.9)'); g.addColorStop(0.4,'rgba(127,212,255,0.6)'); g.addColorStop(1,'rgba(127,212,255,0)');
    ctx.fillStyle=g; ctx.fillRect(-12,-12,24,24);
    ctx.strokeStyle='rgba(200,236,255,0.8)'; ctx.lineWidth=2.5;
    ctx.beginPath(); ctx.moveTo(-8,0); ctx.lineTo(8,0); ctx.stroke();
    ctx.restore();
  }
}
function drawParticles(){
  for(const p of game.parts){
    const cam=camera();
    const a=1-p.t/p.life;
    ctx.globalAlpha=a;
    ctx.fillStyle=p.c;
    ctx.fillRect(p.x-cam.x-p.s/2, p.y-cam.y-p.s/2, p.s, p.s);
  }
  ctx.globalAlpha=1;
}
function drawWalls(pal,cam){
  const {w,h}=ZONES[game.zone];
  const x0=Math.max(0,Math.floor(cam.x/T)-1), x1=Math.min(w-1,Math.ceil((cam.x+CW)/T)+1);
  const y0=Math.max(0,Math.floor(cam.y/T)-1), y1=Math.min(h-1,Math.ceil((cam.y+CH)/T)+1);
  for(let y=y0;y<=y1;y++) for(let x=x0;x<=x1;x++){
    const g=game.maps[game.zone][y*w+x];
    if(g!==1) continue;
    const px=x*T, py=y*T;
    ctx.fillStyle=pal.wall2; ctx.fillRect(px,py+6,T,T-6);
    ctx.fillStyle=pal.wall; ctx.fillRect(px,py,T,6);
    // brick seams
    ctx.strokeStyle='rgba(0,0,0,0.25)'; ctx.lineWidth=1;
    ctx.beginPath(); ctx.moveTo(px,py+16); ctx.lineTo(px+T,py+16); ctx.stroke();
    const off=(y%2)?16:8;
    ctx.beginPath(); ctx.moveTo(px+off,py+6); ctx.lineTo(px+off,py+16); ctx.stroke();
    // moss
    if(hash2(x,y*7)<0.3){
      ctx.fillStyle='rgba(90,110,70,0.25)';
      ctx.fillRect(px+hash2(x,y)*20, py+6, 5, 4);
    }
  }
}
function drawFloaters(){
  const cam=camera();
  for(const f of game.floaters){
    const a=1-f.t/f.dur;
    ctx.globalAlpha=clamp(a*1.5,0,1);
    ctx.font=(f.size||12)+'px Georgia';
    ctx.textAlign='center';
    ctx.strokeStyle='rgba(0,0,0,0.8)'; ctx.lineWidth=3;
    ctx.strokeText(f.txt, f.x-cam.x, f.y-cam.y);
    ctx.fillStyle=f.c||'#e8d9b0';
    ctx.fillText(f.txt, f.x-cam.x, f.y-cam.y);
  }
  ctx.globalAlpha=1; ctx.textAlign='left';
}
function drawVignette(pal){
  const g=ctx.createRadialGradient(CW/2,CH/2,CH*0.42,CW/2,CH/2,CH*0.78);
  g.addColorStop(0,'rgba(0,0,0,0)'); g.addColorStop(1,'rgba(3,5,9,0.62)');
  ctx.fillStyle=g; ctx.fillRect(0,0,CW,CH);
}
function drawHUD(){
  const p=game.player;
  ctx.save();
  // hearts
  for(let i=0;i<p.containers;i++){
    const x=26+i*28, y=24;
    heartPath(x,y,15);
    ctx.fillStyle='rgba(20,14,12,0.75)'; ctx.fill();
    const full=p.hp>=i*2+2, half=p.hp===i*2+1;
    if(full){
      heartPath(x,y,15);
      ctx.fillStyle='#c0452e'; ctx.fill();
      ctx.fillStyle='rgba(255,190,150,0.5)';
      ctx.beginPath(); ctx.arc(x-3,y+4,2,0,6.283); ctx.fill();
    } else if(half){
      ctx.save();
      heartPath(x,y,15); ctx.clip();
      ctx.fillStyle='#c0452e';
      ctx.fillRect(x-8,y-1,9,16);
      ctx.restore();
    }
    heartPath(x,y,15);
    ctx.strokeStyle='#d9a441'; ctx.lineWidth=1.5; ctx.stroke();
  }
  ctx.font='11px Georgia'; ctx.fillStyle='#9a8f78';
  ctx.fillText(WEAPONS[p.wpn].name, 24, 50);
  // gold
  ctx.textAlign='right';
  ctx.fillStyle='#d9a441';
  ctx.beginPath(); ctx.arc(CW-70,24,7,0,6.283); ctx.fill();
  ctx.fillStyle='#8a6a3f';
  ctx.beginPath(); ctx.arc(CW-70,24,4,0,6.283); ctx.fill();
  ctx.fillStyle='#f0d9a0'; ctx.font='15px Georgia';
  ctx.fillText(p.gold, CW-24, 29);
  ctx.font='11px Georgia'; ctx.fillStyle='#9a8f78';
  ctx.fillText(ZONES[game.zone].name, CW-24, 46);
  if(p.hasShard){
    ctx.fillStyle='rgba(127,212,255,0.9)';
    ctx.beginPath(); ctx.moveTo(CW-70,52); ctx.lineTo(CW-66,57); ctx.lineTo(CW-70,62); ctx.lineTo(CW-74,57); ctx.closePath(); ctx.fill();
    ctx.fillText(p.shardCharges+' ⌁', CW-24, 61);
  }
  ctx.textAlign='left';
  // interact hint
  if(game.state==='playing'&&game.interactHint){
    const msg=game.interactHint;
    ctx.font='13px Georgia';
    const wdt=ctx.measureText(msg).width+64;
    ctx.fillStyle='rgba(10,12,18,0.8)';
    ctx.fillRect(CW/2-wdt/2, CH-44, wdt, 26);
    ctx.strokeStyle='rgba(217,164,65,0.5)'; ctx.lineWidth=1;
    ctx.strokeRect(CW/2-wdt/2, CH-44, wdt, 26);
    ctx.fillStyle='#e8c67a';
    ctx.textAlign='center';
    const k=msg.startsWith('Rest')||msg.startsWith('Open')||msg.startsWith('Take')||msg.startsWith('Claim')||msg.startsWith('Barter')||msg.startsWith('Kindle')||msg.startsWith('Approach')?'E':'J';
    ctx.fillText('['+k+']  '+msg, CW/2, CH-26);
    ctx.textAlign='left';
  }
  // control footer
  ctx.fillStyle='rgba(154,143,120,0.55)'; ctx.font='10px Georgia';
  ctx.fillText('WASD move · J strike · K shard · E interact · Tab kit', 12, CH-10);
  ctx.restore();
}
function drawEdgeFx(){
  if(game.edge>0){
    const a=clamp(game.edge/2.5,0,0.85);
    ctx.save();
    ctx.strokeStyle='rgba(255,215,140,'+a+')';
    ctx.lineWidth=5;
    ctx.shadowColor='#ffd257'; ctx.shadowBlur=24;
    ctx.strokeRect(3,3,CW-6,CH-6);
    ctx.restore();
  }
  if(game.flash>0){
    ctx.fillStyle='rgba(190,40,20,'+game.flash*0.22+')';
    ctx.fillRect(0,0,CW,CH);
  }
  if(game.state==='dead'){
    const a=clamp(game.deathT/1.2,0,1)*0.75;
    ctx.fillStyle='rgba(20,4,4,'+a+')';
    ctx.fillRect(0,0,CW,CH);
    ctx.fillStyle='rgba(232,217,176,'+clamp(game.deathT-0.4,0,1)+')';
    ctx.font='24px Georgia'; ctx.textAlign='center';
    ctx.fillText('The vigil falters…', CW/2, CH/2-16);
    if(game.deathT>1){
      ctx.font='14px Georgia'; ctx.fillStyle='#9a8f78';
      ctx.fillText('press R or Enter — you return to the Sanctuary Stone, whole again.', CW/2, CH/2+14);
    }
    ctx.textAlign='left';
  }
  if(game.state==='win'){
    game.winT+=1/60;
    const a=clamp(game.winT/2,0,1)*0.5;
    ctx.fillStyle='rgba(255,235,190,'+a*0.5+')';
    ctx.fillRect(0,0,CW,CH);
  }
}
function drawBanner(){
  for(const b of game.banners){
    const t=b.t;
    const a=t<0.5?t/0.5:(t>b.dur-0.8?clamp((b.dur-t)/0.8,0,1):1);
    ctx.save(); ctx.globalAlpha=a;
    ctx.textAlign='center';
    ctx.font='28px Georgia';
    ctx.fillStyle='#ecc984';
    ctx.shadowColor='rgba(217,164,65,0.6)'; ctx.shadowBlur=18;
    ctx.fillText(b.title.toUpperCase(), CW/2, CH*0.3);
    ctx.shadowBlur=0;
    ctx.font='italic 13px Georgia';
    ctx.fillStyle='#b9a98c';
    ctx.fillText(b.sub, CW/2, CH*0.3+24);
    ctx.textAlign='left';
    ctx.restore();
  }
}

/* ================= MAIN LOOP ================= */
let lastTs=0;
function loop(ts){
  requestAnimationFrame(loop);
  let dt=(ts-lastTs)/1000; lastTs=ts;
  if(!dt||dt>0.1) dt=0.016;
  update(dt);
  render();
}
function update(dt){
  game.time+=dt;
  updateFx(dt);
  if(game.state==='dead'){ game.deathT+=dt; return; }
  if(game.state==='win'){
    game.winT+=dt;
    const b=game.ents.find(e=>e.type==='beacon');
    if(b&&b.lit&&Math.random()<0.4){
      game.parts.push({x:b.x+rnd(-30,30), y:b.y-rnd(20,120), vx:rnd(-14,14), vy:-rnd(30,80),
        t:0,life:rnd(0.6,1.4),c:Math.random()<0.5?'#ffd257':'#fff3d0',s:rnd(1.2,2.6),g:-16});
    }
    return;
  }
  if(game.state!=='playing') return;
  game.stats.time+=dt;
  // ambient particles
  game.ambientAcc+=dt;
  if(game.ambientAcc>0.07){
    game.ambientAcc=0;
    if(game.parts.length<420){
      const cam=camera();
      if(game.zone===1){ // floating dust
        game.parts.push({x:cam.x+rnd(0,CW), y:cam.y+rnd(0,CH), vx:rnd(-4,4), vy:rnd(2,7),
          t:0,life:rnd(2,4),c:'rgba(200,170,120,0.5)',s:1.2,g:0});
      } else if(game.zone===3){ // rising mist
        game.parts.push({x:cam.x+rnd(0,CW), y:cam.y+CH+rnd(-20,0), vx:rnd(-5,5), vy:-rnd(12,26),
          t:0,life:rnd(3,5),c:'rgba(140,180,220,0.22)',s:rnd(2,3.5),g:0});
      } else if(game.zone===0){ // falling ash
        game.parts.push({x:cam.x+rnd(0,CW), y:cam.y-10, vx:rnd(-8,4), vy:rnd(16,30),
          t:0,life:rnd(3,6),c:'rgba(190,160,120,0.4)',s:1.4,g:0});
      }
    }
  }
  updatePlayer(dt);
  updateEnemies(dt);
  updateBolts(dt);
}

/* ================= BOOTSTRAP ================= */
function bindUI(){
  $('btnNew').addEventListener('click',()=>{ clearSave(); startNew(); });
  $('btnCont').addEventListener('click',()=>continueGame());
  $('btnResume').addEventListener('click',()=>{ game.state='playing'; $('ovPause').hidden=true; });
  $('btnMute').addEventListener('click',()=>{
    AudioSys.setMuted(!AudioSys.muted);
    $('btnMute').textContent='Sound: '+(AudioSys.muted?'Off':'On');
  });
  $('btnAbandon').addEventListener('click',toTitle);
  $('btnInvClose').addEventListener('click',closeInv);
  $('btnShopClose').addEventListener('click',()=>{ $('ovShop').hidden=true; game.state='playing'; AudioSys.droneOff(); });
  $('btnWinTitle').addEventListener('click',()=>{ clearSave(); toTitle(); });
  addEventListener('keydown',e=>{
    if((e.key==='Enter')&&game.state==='win'&&game.winT>1.5){ clearSave(); toTitle(); }
  });
}
function refreshTitle(){
  $('btnCont').hidden=!hasSave();
}
bindUI();
refreshTitle();
game.time=0;
requestAnimationFrame(loop);
</script>
</body>
</html>
<!-- agent-meta {"model":"unsloth/Qwen3.8-27B-GGUF","provider":"brigid","persona":"full","sessionId":"bcc833cd-4604-467b-8b64-3cf9d295ecf8","tokensIn":4575540,"tokensOut":246213,"tokensTotal":4821753,"cost":0,"turns":62,"toolCalls":61,"failedToolCalls":0,"timestamp":"2026-08-17T20:36:07.160Z"} -->