← Warden results

Warden

Mimo v2.6 Pro · 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 77,881 bytes · SHA-256 3adf7dfa65df
<!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 — Aurelion's Sunken Observatory</title>
<style>
  :root{
    --slate0:#070a0d; --slate1:#0d1319; --slate2:#16202a; --slate3:#223140;
    --bronze:#8a6a3a; --bronze2:#b08a4c; --gold:#e8b45a; --gold2:#ffd98a;
    --ember:#c8632a; --ember2:#ff8a3c; --moss:#5a6b4a; --parch:#e8d9b0;
    --corrode:#191519; --blood:#a8262a;
  }
  *{box-sizing:border-box; -webkit-user-select:none; user-select:none;}
  html,body{margin:0;height:100%;background:radial-gradient(120% 100% at 50% 0%,#0c1218 0%,#05080b 70%);overflow:hidden;}
  body{display:flex;align-items:center;justify-content:center;font-family:"Segoe UI",system-ui,-apple-system,sans-serif;color:var(--parch);}
  #wrap{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;}
  #stage{position:relative;width:960px;height:600px;transform-origin:center center;
    box-shadow:0 0 0 1px #3a3226, 0 30px 90px rgba(0,0,0,.8), 0 0 120px rgba(232,180,90,.06);}
  canvas#game{display:block;width:960px;height:600px;background:#080c11;}
  .vig{position:absolute;inset:0;pointer-events:none;
    box-shadow:inset 0 0 130px rgba(0,0,0,.85), inset 0 0 34px rgba(0,0,0,.6);}
  #flash{position:absolute;inset:0;pointer-events:none;opacity:0;
    background:radial-gradient(circle at 50% 55%, rgba(200,30,30,0) 35%, rgba(190,30,30,.75) 100%);}
  #healFlash{position:absolute;inset:0;pointer-events:none;opacity:0;
    background:radial-gradient(circle at 50% 55%, rgba(140,255,180,0) 40%, rgba(110,230,150,.5) 100%);}
  #shimmer{position:absolute;inset:0;pointer-events:none;opacity:0;
    box-shadow:inset 0 0 90px rgba(255,205,105,.85), inset 0 0 26px rgba(255,235,180,.7);}
  .hidden{display:none !important;}

  /* ---------- HUD ---------- */
  #hud{position:absolute;inset:0;pointer-events:none;}
  .hudtop{position:absolute;top:0;left:0;right:0;display:flex;justify-content:space-between;align-items:flex-start;
    padding:12px 16px;background:linear-gradient(180deg, rgba(5,8,11,.85), rgba(5,8,11,0));}
  .hudl{display:flex;flex-direction:column;gap:5px;}
  #hearts{display:flex;gap:5px;}
  .heart{position:relative;width:26px;height:24px;filter:drop-shadow(0 2px 3px rgba(0,0,0,.7));}
  .heart svg{position:absolute;inset:0;width:26px;height:24px;}
  .heart .fillwrap{position:absolute;inset:0;overflow:hidden;width:0%;transition:width .18s ease;}
  .wname{font-family:Georgia,"Times New Roman",serif;font-size:13px;letter-spacing:.18em;text-transform:uppercase;
    color:var(--gold);text-shadow:0 1px 2px #000, 0 0 12px rgba(232,180,90,.35);}
  .wname.t1{color:var(--ember2);text-shadow:0 1px 2px #000,0 0 14px rgba(255,138,60,.55);}
  .wname.t2{color:var(--gold2);text-shadow:0 1px 2px #000,0 0 18px rgba(255,217,138,.8);}
  .hudc{display:flex;flex-direction:column;align-items:center;gap:4px;padding-top:2px;}
  #ammo{display:flex;align-items:center;gap:7px;font-size:10px;letter-spacing:.16em;color:#9fb6c9;}
  #amPips{display:flex;gap:3px;}
  .pip{width:7px;height:11px;background:#223646;border:1px solid #3d5f78;transform:skewX(-12deg);}
  .pip.on{background:linear-gradient(180deg,#bfe9ff,#5fb6e0);box-shadow:0 0 7px rgba(120,200,255,.8);border-color:#9fdcff;}
  .hudr{display:flex;flex-direction:column;align-items:flex-end;gap:6px;}
  .coin{display:flex;align-items:center;gap:6px;font-family:Georgia,serif;font-size:17px;color:var(--gold2);text-shadow:0 1px 2px #000;}
  .coin svg{width:18px;height:18px;filter:drop-shadow(0 0 5px rgba(232,180,90,.5));}
  #objective{max-width:270px;text-align:right;font-size:11px;line-height:1.5;letter-spacing:.05em;color:#a9bccb;
    border-right:2px solid rgba(232,180,90,.45);padding-right:9px;}
  #objective b{color:var(--gold);font-weight:600;letter-spacing:.14em;text-transform:uppercase;font-size:9.5px;display:block;margin-bottom:2px;}

  #belt{position:absolute;left:16px;bottom:14px;display:flex;gap:8px;align-items:center;}
  .slot{position:relative;width:44px;height:44px;border:1px solid #4a4130;border-radius:3px;
    background:linear-gradient(180deg,rgba(22,32,42,.9),rgba(10,15,20,.9));display:flex;align-items:center;justify-content:center;}
  .slot svg{width:24px;height:24px;}
  .slot .key{position:absolute;top:1px;left:3px;font-size:9px;color:#8d9bab;letter-spacing:.06em;}
  .slot .cnt{position:absolute;bottom:1px;right:4px;font-size:11px;color:var(--parch);font-weight:700;text-shadow:0 1px 2px #000;}
  .slot.empty{opacity:.32;}
  .slot.usable{border-color:var(--bronze2);box-shadow:0 0 10px rgba(232,180,90,.18);}
  #slotAcc{width:auto;min-width:120px;padding:0 12px;gap:8px;font-size:11px;letter-spacing:.08em;color:#cfe0ee;}
  #slotAcc .accIco{width:16px;height:16px;border:1px solid var(--bronze2);border-radius:50%;box-shadow:0 0 8px rgba(180,220,255,.4);}

  #prompt{position:absolute;left:50%;bottom:70px;transform:translateX(-50%);font-size:12.5px;letter-spacing:.12em;
    text-transform:uppercase;color:var(--gold2);background:rgba(8,12,16,.8);border:1px solid rgba(138,106,58,.7);
    padding:6px 14px;border-radius:3px;opacity:0;transition:opacity .15s;text-shadow:0 1px 3px #000;}
  #prompt.show{opacity:1;}
  #prompt em{font-style:normal;color:#fff;background:rgba(232,180,90,.22);border:1px solid rgba(232,180,90,.5);
    padding:1px 6px;border-radius:3px;margin-right:8px;font-size:11px;}

  #toast{position:absolute;left:50%;bottom:118px;transform:translateX(-50%);max-width:640px;min-width:320px;
    background:linear-gradient(180deg,rgba(12,18,24,.94),rgba(8,12,16,.94));border:1px solid rgba(138,106,58,.75);
    border-radius:4px;padding:11px 18px;font-family:Georgia,serif;font-style:italic;font-size:14.5px;line-height:1.5;
    color:#e9dcb8;text-align:center;opacity:0;transition:opacity .35s;pointer-events:none;
    box-shadow:0 10px 30px rgba(0,0,0,.6);}
  #toast.show{opacity:1;}
  #toast b{font-style:normal;color:var(--gold);letter-spacing:.14em;text-transform:uppercase;font-size:11px;display:block;margin-bottom:3px;}

  #zoneTitle{position:absolute;left:0;right:0;top:200px;text-align:center;pointer-events:none;opacity:0;}
  #zoneTitle .z{font-family:Georgia,serif;font-size:13px;letter-spacing:.55em;color:var(--gold);text-transform:uppercase;}
  #zoneTitle .n{font-family:Georgia,serif;font-size:44px;letter-spacing:.12em;color:#f2e6c4;margin-top:4px;
    text-shadow:0 3px 18px rgba(0,0,0,.9), 0 0 34px rgba(232,180,90,.35);}
  #zoneTitle .s{font-size:12.5px;letter-spacing:.2em;color:#9db1c2;margin-top:8px;font-style:italic;}

  /* ---------- Overlays ---------- */
  .overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;
    background:radial-gradient(90% 80% at 50% 40%, rgba(10,16,22,.9), rgba(4,6,9,.97));z-index:20;}
  #title{background:linear-gradient(180deg, rgba(6,9,13,.72), rgba(4,6,9,.9));}
  #title h1{font-family:Georgia,"Times New Roman",serif;font-size:60px;letter-spacing:.14em;margin:0;
    background:linear-gradient(180deg,#ffeec2 0%,#e8b45a 45%,#a5702c 100%);-webkit-background-clip:text;background-clip:text;color:transparent;
    filter:drop-shadow(0 4px 16px rgba(0,0,0,.8));text-align:center;line-height:1.05;}
  #title .sub{margin-top:14px;font-family:Georgia,serif;font-style:italic;font-size:16px;color:#a9bccb;letter-spacing:.12em;}
  #title .rule{width:340px;height:1px;margin:26px 0;background:linear-gradient(90deg,transparent,var(--bronze2),transparent);}
  #title .lore{max-width:560px;text-align:center;font-family:Georgia,serif;font-size:14.5px;line-height:1.75;color:#c9d5de;font-style:italic;}
  .btn{pointer-events:auto;cursor:pointer;margin-top:12px;min-width:250px;text-align:center;padding:11px 24px;
    font-family:Georgia,serif;letter-spacing:.22em;text-transform:uppercase;font-size:13px;color:#0b0f13;
    background:linear-gradient(180deg,#f0d79b,#c39a4d 55%,#8a6a3a);border:1px solid #5d4a2a;border-radius:3px;
    box-shadow:0 4px 0 #4a3a1f, 0 10px 26px rgba(0,0,0,.55);transition:transform .1s, filter .15s;}
  .btn:hover{filter:brightness(1.12);transform:translateY(-1px);}
  .btn:active{transform:translateY(2px);box-shadow:0 2px 0 #4a3a1f;}
  .btn.ghost{background:linear-gradient(180deg,rgba(34,49,64,.9),rgba(16,23,30,.9));color:#d7e3ec;border-color:#3f5468;box-shadow:0 4px 0 #101820;}
  .btn.small{min-width:0;padding:7px 14px;font-size:11px;}
  .btn[disabled]{filter:grayscale(.7) brightness(.6);cursor:not-allowed;pointer-events:none;}
  .ctl{margin-top:22px;display:flex;gap:26px;font-size:11px;letter-spacing:.08em;color:#8fa3b3;}
  .ctl div{line-height:1.9;}
  .ctl b{color:var(--gold);font-weight:600;}
  .key-cap{display:inline-block;border:1px solid #4f5f6d;background:rgba(20,28,36,.9);border-radius:3px;padding:1px 6px;color:#dfeaf2;font-size:10px;}

  .panel{pointer-events:auto;width:640px;max-height:540px;overflow:hidden;background:linear-gradient(180deg,rgba(14,20,27,.97),rgba(8,12,17,.98));
    border:1px solid rgba(138,106,58,.8);border-radius:5px;box-shadow:0 24px 70px rgba(0,0,0,.75), inset 0 0 40px rgba(232,180,90,.05);
    padding:22px 26px;}
  .panel h2{margin:0 0 4px;font-family:Georgia,serif;font-size:24px;letter-spacing:.16em;text-transform:uppercase;
    background:linear-gradient(180deg,#ffeec2,#c39a4d);-webkit-background-clip:text;background-clip:text;color:transparent;}
  .panel .flav{font-family:Georgia,serif;font-style:italic;color:#9db1c2;font-size:13px;margin-bottom:14px;}
  .row{display:flex;align-items:center;gap:14px;padding:11px 12px;margin-bottom:8px;border:1px solid #2b3944;border-radius:4px;
    background:linear-gradient(180deg,rgba(22,32,42,.7),rgba(12,18,24,.7));}
  .row .ic{width:40px;height:40px;flex:0 0 40px;display:flex;align-items:center;justify-content:center;
    border:1px solid #4a4130;border-radius:3px;background:rgba(8,12,16,.7);}
  .row .ic svg{width:24px;height:24px;}
  .row .tx{flex:1;}
  .row .tx b{display:block;font-family:Georgia,serif;font-size:15px;color:#f0e4c2;letter-spacing:.06em;}
  .row .tx span{font-size:12px;color:#9db1c2;line-height:1.5;}
  .row .pr{font-family:Georgia,serif;color:var(--gold2);font-size:16px;min-width:74px;text-align:right;}
  .row .pr s{color:#6d7d8b;font-size:12px;}
  .tag{display:inline-block;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;padding:2px 7px;border-radius:2px;
    border:1px solid #4f6a80;color:#a9d3ec;background:rgba(40,80,110,.25);margin-left:8px;}
  .tag.acc{border-color:#7c6a3f;color:#f0d79b;background:rgba(120,90,30,.2);}
  .tag.cons{border-color:#4f8a5f;color:#b8ecbf;background:rgba(40,110,60,.2);}
  .eqbtn{min-width:104px;}
  .panel .foot{display:flex;justify-content:space-between;align-items:center;margin-top:14px;}
  .goldbig{font-family:Georgia,serif;font-size:18px;color:var(--gold2);display:flex;align-items:center;gap:8px;}
  .goldbig svg{width:20px;height:20px;}
  .note{font-size:11px;color:#7f93a3;letter-spacing:.06em;}

  #end .epic{font-family:Georgia,serif;font-size:46px;letter-spacing:.14em;
    background:linear-gradient(180deg,#fff3d2,#e8b45a 60%,#8a6a3a);-webkit-background-clip:text;background-clip:text;color:transparent;}
  #end .body{max-width:600px;text-align:center;font-family:Georgia,serif;font-size:15.5px;line-height:1.85;color:#cfdbe4;font-style:italic;margin-top:18px;}
  #end .stats{margin-top:22px;display:flex;gap:36px;font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:#8fa3b3;}
  #end .stats b{display:block;font-family:Georgia,serif;font-size:22px;color:var(--gold2);letter-spacing:.06em;margin-top:5px;}

  .flashmsg{position:absolute;left:50%;top:120px;transform:translateX(-50%);z-index:30;
    font-family:Georgia,serif;letter-spacing:.3em;text-transform:uppercase;font-size:14px;color:#ffdce0;
    background:rgba(60,10,12,.9);border:1px solid rgba(200,60,60,.7);padding:10px 26px;border-radius:3px;opacity:0;transition:opacity .3s;}
  .flashmsg.show{opacity:1;}
  #muteTip{position:absolute;right:16px;bottom:14px;font-size:10px;letter-spacing:.14em;color:#5f7385;}
  @media (max-width:820px){ .ctl{flex-direction:column;gap:6px;} }
</style>
</head>
<body>
<div id="wrap">
  <div id="stage">
    <canvas id="game" width="960" height="600"></canvas>
    <div class="vig"></div>
    <div id="flash"></div>
    <div id="healFlash"></div>
    <div id="shimmer"></div>

    <div id="hud" class="hidden">
      <div class="hudtop">
        <div class="hudl">
          <div id="hearts"></div>
          <div class="wname" id="wname">Ember Blade</div>
        </div>
        <div class="hudc">
          <div id="ammo" class="hidden">
            <span>AETHER SHARD</span><span id="amPips"></span>
          </div>
        </div>
        <div class="hudr">
          <div class="coin">
            <svg viewBox="0 0 20 20"><circle cx="10" cy="10" r="8" fill="#e8b45a" stroke="#8a6a3a" stroke-width="2"/><circle cx="10" cy="10" r="3.4" fill="none" stroke="#8a6a3a" stroke-width="1.4"/></svg>
            <span id="gold">0</span>
          </div>
          <div id="objective"><b>Objective</b><span id="objText">—</span></div>
        </div>
      </div>
      <div id="belt">
        <div class="slot empty" id="slotStem" title="Vital Stem — press 1">
          <span class="key">1</span>
          <svg viewBox="0 0 20 20"><path d="M10 18.5V9" stroke="#7fd08a" stroke-width="1.8" fill="none"/><path d="M10.2 10.5c0-4 2.8-6.4 6.3-6.6.2 4-2 6.6-6.3 6.6z" fill="#7fd08a"/><path d="M9.8 13.5c0-3.1-2-5-5-5.2.1 3.1 1.9 5.2 5 5.2z" fill="#4f9d63"/></svg>
          <span class="cnt" id="cntStem">0</span>
        </div>
        <div class="slot empty" id="slotSeal" title="Binding Seal — press 2">
          <span class="key">2</span>
          <svg viewBox="0 0 20 20"><path d="M10 2l7 3.9v8.2L10 18l-7-3.9V5.9z" fill="none" stroke="#7fb6ff" stroke-width="1.7"/><circle cx="10" cy="10" r="2.6" fill="#7fb6ff"/><path d="M10 5.5v9M5.5 10h9" stroke="#7fb6ff" stroke-width="1"/></svg>
          <span class="cnt" id="cntSeal">0</span>
        </div>
        <div class="slot empty" id="slotAcc" title="Equipped accessory — TAB for inventory">
          <span class="key">TAB</span>
          <span class="accIco" id="accIco"></span>
          <span id="accName">No accessory</span>
        </div>
      </div>
      <div id="prompt"><em>E</em><span id="promptTxt"></span></div>
      <div id="toast"></div>
      <div id="zoneTitle"><div class="z"></div><div class="n"></div><div class="s"></div></div>
      <div class="flashmsg" id="deathMsg">The Corrosion takes you</div>
    </div>

    <div id="muteTip">M — sound on/off &nbsp;·&nbsp; ESC — pause</div>

    <!-- TITLE -->
    <div class="overlay" id="title">
      <h1>THE WARDEN'S<br>CORROSION</h1>
      <div class="sub">Aurelion's Sunken Observatory</div>
      <div class="rule"></div>
      <div class="lore">Black-iron decay climbs the gears of the Observatory. You are the Last Warden.<br>
        Carry the Ember Blade through six sunken halls, restore two weapon altars,<br>
        unearth the Aether Shard, and rekindle the Core Beacon before the vault falls.</div>
      <button class="btn" id="btnNew">Begin the Descent</button>
      <button class="btn ghost hidden" id="btnContinue">Continue — Sealed Pilgrimage</button>
      <div class="ctl">
        <div><b>MOVE</b> <span class="key-cap">WASD</span> / <span class="key-cap">ARROWS</span><br>
          <b>MELEE</b> <span class="key-cap">SPACE</span> / <span class="key-cap">J</span> / <span class="key-cap">LMB</span><br>
          <b>DASH</b> <span class="key-cap">SHIFT</span></div>
        <div><b>RANGED</b> <span class="key-cap">K</span> / <span class="key-cap">RMB</span> (Aether Shard)<br>
          <b>INTERACT</b> <span class="key-cap">E</span><br>
          <b>USE ITEM</b> <span class="key-cap">1</span> Vital Stem · <span class="key-cap">2</span> Binding Seal</div>
        <div><b>INVENTORY</b> <span class="key-cap">TAB</span><br>
          <b>PAUSE</b> <span class="key-cap">ESC</span><br>
          <b>SOUND</b> <span class="key-cap">M</span></div>
      </div>
    </div>

    <!-- INVENTORY -->
    <div class="overlay hidden" id="invOv">
      <div class="panel">
        <h2>Warden's Satchel</h2>
        <div class="flav" id="invFlav">One accessory may be worn at a time. Consumables are spent freely.</div>
        <div id="invBody"></div>
        <div class="foot">
          <div class="note">Press <span class="key-cap">TAB</span> or <span class="key-cap">ESC</span> to close</div>
          <button class="btn small ghost" id="invClose">Close</button>
        </div>
      </div>
    </div>

    <!-- SHOP -->
    <div class="overlay hidden" id="shopOv">
      <div class="panel">
        <h2>Merchant Altar</h2>
        <div class="flav" id="shopFlav">"Bronze remembers every hand that spent it. Spend, Warden."</div>
        <div id="shopBody"></div>
        <div class="foot">
          <div class="goldbig"><svg viewBox="0 0 20 20"><circle cx="10" cy="10" r="8" fill="#e8b45a" stroke="#8a6a3a" stroke-width="2"/><circle cx="10" cy="10" r="3.4" fill="none" stroke="#8a6a3a" stroke-width="1.4"/></svg><span id="shopGold">0</span> Golden Sigils</div>
          <button class="btn small ghost" id="shopClose">Leave</button>
        </div>
      </div>
    </div>

    <!-- PAUSE -->
    <div class="overlay hidden" id="pauseOv">
      <div class="panel" style="width:520px">
        <h2>Paired Breath</h2>
        <div class="flav">The Corrosion waits. It is patient, but it is still moving.</div>
        <div class="ctl" style="margin:0 0 6px">
          <div><b>MOVE</b> WASD · <b>MELEE</b> SPACE/LMB · <b>DASH</b> SHIFT</div>
          <div><b>RANGED</b> K/RMB · <b>INTERACT</b> E · <b>ITEMS</b> 1 / 2</div>
          <div><b>INVENTORY</b> TAB · <b>SOUND</b> M</div>
        </div>
        <div class="foot">
          <div class="note" id="pauseNote">Progress seals only at Sanctuary Stones.</div>
          <div style="display:flex;gap:10px">
            <button class="btn small ghost" id="btnMute">Sound: On</button>
            <button class="btn small" id="btnResume">Resume</button>
          </div>
        </div>
      </div>
    </div>

    <!-- ENDING -->
    <div class="overlay hidden" id="endOv">
      <div class="epic">THE BEACON REKINDLED</div>
      <div class="body" id="endBody"></div>
      <div class="stats">
        <div>Foes Unmade<b id="stKills">0</b></div>
        <div>Sigils Gathered<b id="stGold">0</b></div>
        <div>Heart Containers<b id="stHp">3</b></div>
        <div>Blade<b id="stBlade">Ember</b></div>
      </div>
      <button class="btn" id="btnAgain" style="margin-top:28px">Walk the Halls Again</button>
    </div>
  </div>
</div>

<script>
/* ============================================================
   THE WARDEN'S CORROSION — single-file action adventure
   ============================================================ */
const T=40, COLS=24, ROWS=15, VW=COLS*T, VH=ROWS*T;
const $=id=>document.getElementById(id);
const rnd=(a,b)=>a+Math.random()*(b-a);
const ri=(a,b)=>Math.floor(rnd(a,b,b));
const clamp=(v,a,b)=>v<a?a:(v>b?b:v);
const dist=(a,b)=>Math.hypot(a.x-b.x,a.y-b.y);
const tx=v=>v*T+T/2;                 // tile index -> centre px
const rr=(x,y,w,h)=>({x:x*T,y:y*T,w:w*T,h:h*T});
const now=()=>performance.now()/1000;

/* ============================ AUDIO ============================ */
const A=(()=>{
  let ready=false, ctx=null, master=null, sfxG=null, ambG=null, noiseBuf=null;
  let muted=false, ambSrc=[], ambGn=null, drone=null, lastStep=0;
  function init(){
    if(ready) return;
    const AC=window.AudioContext||window.webkitAudioContext; if(!AC) return;
    ctx=new AC();
    master=ctx.createGain(); master.gain.value=muted?0:0.8; master.connect(ctx.destination);
    sfxG=ctx.createGain(); sfxG.gain.value=0.95; sfxG.connect(master);
    ambG=ctx.createGain(); ambG.gain.value=0.3; ambG.connect(master);
    const len=Math.floor(ctx.sampleRate*1.5);
    noiseBuf=ctx.createBuffer(1,len,ctx.sampleRate);
    const d=noiseBuf.getChannelData(0);
    for(let i=0;i<len;i++) d[i]=Math.random()*2-1;
    ready=true;
  }
  const T0=()=>ctx.currentTime;
  function tone(fr,o={}){
    if(!ready)return; o=o||{};
    const dur=o.dur!==undefined?o.dur:0.3, t=T0()+(o.delay||0);
    const os=ctx.createOscillator(); os.type=o.type||'sine';
    os.frequency.setValueAtTime(Math.max(20,fr),t);
    if(o.f2) os.frequency.exponentialRampToValueAtTime(Math.max(20,o.f2),t+dur);
    if(o.det) os.detune.value=o.det;
    const g=ctx.createGain();
    g.gain.setValueAtTime(0.0001,t);
    g.gain.exponentialRampToValueAtTime(Math.max(0.001,o.vol||0.18),t+(o.att||0.006));
    g.gain.exponentialRampToValueAtTime(0.0001,t+dur);
    os.connect(g); g.connect(o.dest||sfxG);
    os.start(t); os.stop(t+dur+0.03);
  }
  function noise(o={}){
    if(!ready)return;
    const dur=o.dur!==undefined?o.dur:0.2, t=T0()+(o.delay||0);
    const s=ctx.createBufferSource(); s.buffer=noiseBuf; s.loop=true;
    const f=ctx.createBiquadFilter(); f.type=o.type||'lowpass';
    f.frequency.setValueAtTime(Math.max(40,o.f||800),t);
    if(o.f2) f.frequency.exponentialRampToValueAtTime(Math.max(40,o.f2),t+dur);
    f.Q.value=o.q||1;
    const g=ctx.createGain();
    g.gain.setValueAtTime(0.0001,t);
    g.gain.exponentialRampToValueAtTime(Math.max(0.001,o.vol||0.2),t+(o.att||0.005));
    g.gain.exponentialRampToValueAtTime(0.0001,t+dur);
    s.connect(f); f.connect(g); g.connect(o.dest||sfxG);
    s.start(t); s.stop(t+dur+0.03);
  }
  const sfx={
    step(kind){
      const t=now(); if(t-lastStep<0.26) return; lastStep=t;
      if(kind==='grate') noise({dur:0.07,vol:0.1,f:2400,f2:1100,type:'bandpass',q:2});
      else if(kind==='damp'){ noise({dur:0.13,vol:0.12,f:700,f2:260,q:1.4}); tone(180,{dur:0.09,vol:0.04,f2:90,type:'triangle'}); }
      else if(kind==='spire') noise({dur:0.09,vol:0.09,f:3200,f2:1500,type:'bandpass',q:3});
      else noise({dur:0.08,vol:0.1,f:1200,f2:520,q:1});
    },
    swing(tier){
      if(tier===0){ noise({dur:0.15,vol:0.18,f:1900,f2:520,type:'bandpass',q:1.4}); }
      else if(tier===1){ noise({dur:0.18,vol:0.2,f:2300,f2:600,type:'bandpass',q:1.2});
        noise({dur:0.2,vol:0.09,f:3400,f2:1400,type:'highpass',q:1,delay:0.03}); }
      else { noise({dur:0.3,vol:0.16,f:900,f2:260,type:'lowpass',att:0.05});
        tone(96,{dur:0.3,vol:0.16,f2:44,type:'sine',att:0.04});
        noise({dur:0.22,vol:0.16,f:3600,f2:900,type:'bandpass',q:2,delay:0.08}); }
    },
    hit(tier){
      if(tier===0){ noise({dur:0.1,vol:0.22,f:520,f2:180,q:1}); }
      else if(tier===1){ noise({dur:0.14,vol:0.24,f:620,f2:200,q:1}); noise({dur:0.16,vol:0.08,f:2600,f2:1200,type:'highpass',q:1}); }
      else { tone(140,{dur:0.3,vol:0.22,f2:70,type:'square'});
        tone(520,{dur:0.16,vol:0.1,f2:240,type:'sawtooth'});
        noise({dur:0.2,vol:0.16,f:1400,f2:400,q:1}); }
    },
    block(){ tone(880,{dur:0.1,vol:0.12,type:'triangle'}); tone(1320,{dur:0.08,vol:0.08,type:'triangle',det:6}); },
    kill(){ noise({dur:0.3,vol:0.16,f:900,f2:180}); tone(220,{dur:0.3,vol:0.1,f2:70,type:'triangle'}); },
    hurt(){
      noise({dur:0.2,vol:0.24,f:380,f2:120});
      tone(700,{dur:0.28,vol:0.13,type:'square',f2:640});
      tone(745,{dur:0.28,vol:0.11,type:'square',f2:800,det:14});
      tone(110,{dur:0.24,vol:0.16,f2:60,type:'sine'});
    },
    coin(){ tone(1560,{dur:0.06,vol:0.09,type:'square'});
      tone(2100,{dur:0.09,vol:0.07,type:'square',delay:0.05}); noise({dur:0.05,vol:0.06,f:5200,type:'highpass'}); },
    pickup(kind){
      if(kind==='rare'){ [523,659,784,1046].forEach((f,i)=>tone(f,{dur:0.55,vol:0.11,type:'triangle',delay:i*0.06}));
        tone(262,{dur:1.1,vol:0.08,type:'sine'}); tone(392,{dur:1.1,vol:0.06,type:'sine',delay:0.05}); noise({dur:0.7,vol:0.05,f:5000,f2:2200,type:'highpass'}); }
      else { [659,880,1174].forEach((f,i)=>tone(f,{dur:0.24,vol:0.1,type:'triangle',delay:i*0.055})); }
    },
    heart(){ [392,523,659,880,1046].forEach((f,i)=>tone(f,{dur:0.8,vol:0.1,type:'sine',delay:i*0.08}));
      tone(196,{dur:1.4,vol:0.09,type:'sine'}); noise({dur:1.1,vol:0.05,f:4200,f2:1600,type:'highpass'}); },
    shoot(){ tone(1180,{dur:0.1,vol:0.14,type:'triangle',f2:380});
      tone(2360,{dur:0.05,vol:0.07,type:'square',f2:900}); noise({dur:0.07,vol:0.06,f:5200,type:'highpass'}); },
    boltHit(){ tone(1760,{dur:0.14,vol:0.1,type:'sine'}); tone(2640,{dur:0.2,vol:0.05,type:'sine',delay:0.03}); },
    enemyShoot(){ tone(320,{dur:0.18,vol:0.09,type:'sawtooth',f2:150}); noise({dur:0.1,vol:0.06,f:900,q:1}); },
    plate(){ tone(180,{dur:0.2,vol:0.16,type:'triangle',f2:80}); noise({dur:0.18,vol:0.12,f:600,f2:200}); },
    gate(){ noise({dur:0.7,vol:0.16,f:340,f2:120,att:0.05}); tone(70,{dur:0.7,vol:0.14,f2:44,type:'sine',att:0.05});
      noise({dur:0.4,vol:0.1,f:2400,f2:800,type:'bandpass',q:2,delay:0.1}); },
    crack(){ noise({dur:0.24,vol:0.22,f:1800,f2:300}); tone(90,{dur:0.2,vol:0.14,f2:50,type:'square'}); },
    wallDown(){ noise({dur:0.7,vol:0.26,f:900,f2:120,att:0.02}); tone(64,{dur:0.8,vol:0.2,f2:34,type:'sine'});
      noise({dur:0.5,vol:0.12,f:2600,f2:600,type:'bandpass',q:1.4,delay:0.05}); },
    altar(tier){
      [262,330,392,523].forEach((f,i)=>tone(f,{dur:1.2,vol:0.1,type:'triangle',delay:i*0.07}));
      tone(tier===1?148:196,{dur:1.8,vol:0.14,type:'sine'});
      noise({dur:1.4,vol:0.07,f:3200,f2:1200,type:'highpass'});
      if(tier===2){ tone(98,{dur:2,vol:0.16,f2:52,type:'sine'}); tone(392,{dur:1.6,vol:0.07,type:'sine',delay:0.2}); }
    },
    save(){ tone(392,{dur:1.5,vol:0.13,type:'sine'}); tone(588,{dur:1.5,vol:0.09,type:'sine',delay:0.05});
      tone(784,{dur:1.8,vol:0.06,type:'sine',delay:0.1}); noise({dur:1.2,vol:0.05,f:4600,f2:1800,type:'highpass'}); },
    buy(){ [880,1174,1568].forEach((f,i)=>tone(f,{dur:0.3,vol:0.1,type:'triangle',delay:i*0.05}));
      noise({dur:0.2,vol:0.1,f:3600,f2:1400,type:'bandpass',q:2}); tone(1320,{dur:0.08,vol:0.07,type:'square'}); },
    deny(){ tone(150,{dur:0.18,vol:0.12,type:'square',f2:110}); tone(120,{dur:0.2,vol:0.09,type:'square',f2:90,delay:0.08}); },
    potion(){ [440,587,880].forEach((f,i)=>tone(f,{dur:0.5,vol:0.1,type:'sine',delay:i*0.06})); noise({dur:0.4,vol:0.05,f:2600,f2:5200,type:'highpass'}); },
    seal(){ tone(300,{dur:0.5,vol:0.13,type:'triangle',f2:80}); tone(1200,{dur:0.3,vol:0.08,type:'sine'});
      noise({dur:0.4,vol:0.08,f:2200,f2:600,type:'bandpass',q:3}); },
    roar(){ tone(70,{dur:1.1,vol:0.2,f2:42,type:'sawtooth'}); noise({dur:1,vol:0.16,f:500,f2:140});
      tone(140,{dur:0.9,vol:0.1,f2:60,type:'square'}); },
    beacon(){
      [131,196,262,330,392,523].forEach((f,i)=>tone(f,{dur:3.4,vol:0.1,type:'sine',delay:i*0.12}));
      noise({dur:2.6,vol:0.08,f:2200,f2:6000,type:'highpass'});
      tone(65,{dur:3.6,vol:0.14,type:'sine'});
    },
    pulse(){ tone(rnd(300,420),{dur:0.1,vol:0.03,type:'triangle'}); }
  };
  function amb(kind){
    if(!ready) return;
    stopAmb();
    ambGn=ctx.createGain(); ambGn.gain.value=0.0; ambGn.connect(ambG);
    const base={stone:73.4,grate:82.4,damp:65.4,spire:98,core:55}[kind]||73.4;
    [1,1.5,2.02,3.0].forEach((m,i)=>{
      const o=ctx.createOscillator(); o.type=i%2?'triangle':'sine';
      o.frequency.value=base*m; o.detune.value=rnd(-7,7);
      const g=ctx.createGain(); g.gain.value=[0.5,0.22,0.14,0.07][i];
      const lp=ctx.createBiquadFilter(); lp.type='lowpass'; lp.frequency.value=kind==='core'?520:380;
      o.connect(g); g.connect(lp); lp.connect(ambGn); o.start(); ambSrc.push(o);
    });
    ambGn.gain.setTargetAtTime(0.24,T0(),1.2);
  }
  function stopAmb(){
    if(ambGn){ ambGn.gain.setTargetAtTime(0.0001,T0(),0.4);
      const g=ambGn, s=ambSrc.slice(); setTimeout(()=>{ s.forEach(o=>{try{o.stop()}catch(e){}}); },900); }
    ambSrc=[]; ambGn=null;
  }
  function shopDrone(on){
    if(!ready) return;
    if(on && !drone){
      const g=ctx.createGain(); g.gain.value=0.0001; g.connect(sfxG);
      const lp=ctx.createBiquadFilter(); lp.type='lowpass'; lp.frequency.value=420; lp.connect(g);
      const o1=ctx.createOscillator(), o2=ctx.createOscillator();
      o1.type='triangle'; o2.type='sine'; o1.frequency.value=55; o2.frequency.value=82.9; o2.detune.value=8;
      o1.connect(lp); o2.connect(lp); o1.start(); o2.start();
      g.gain.setTargetAtTime(0.05,T0(),0.5);
      drone={o:[o1,o2],g};
    } else if(!on && drone){
      const d=drone; drone=null; d.g.gain.setTargetAtTime(0.0001,T0(),0.3);
      setTimeout(()=>d.o.forEach(o=>{try{o.stop()}catch(e){}}),800);
    }
  }
  function setMute(m){ muted=m; if(master) master.gain.value=m?0:0.8; }
  return {init,sfx,amb,stopAmb,shopDrone,setMute,isMuted:()=>muted,ready:()=>ready};
})();

/* ============================ WORLD DATA ============================ */
/* tile coordinates; 24 x 15 tiles of 40px */
const ROOMS={
/* ---------- ZONE 1 : THE ASHEN THRESHOLD ---------- */
z1a:{zone:1,name:"The Ashen Threshold",sub:"Outer Verge — the storm-locked valley below",amb:"stone",pal:"threshold",
  ex:{E:"z1b"}, gap:{E:[6,8]},
  walls:[[17,2,1,4],[17,9,1,4]],
  crates:[[9,3],[9,11]],
  jars:[[5,3],[6,12],[14,3]],
  feats:[{t:"stone",id:"s1",x:3,y:7},
         {t:"sign",id:"sg1",x:6,y:5,text:"AURELION’S SUNKEN OBSERVATORY — no warden has wound these gears in nine years. The Corrosion climbs from the valley like black frost."}],
  foes:[["husk",16,7],["husk",20,11]],
  gears:[[770,440,150,0.05],[835,175,95,-0.08]],dust:14},

z1b:{zone:1,name:"The Ashen Threshold",sub:"The Broken Walk",amb:"stone",pal:"threshold",
  ex:{W:"z1a",E:"z2a"},
  walls:[[11,2,2,4],[11,9,2,4]],
  crates:[[7,7],[8,7]],
  jars:[[4,3],[4,11],[19,3],[19,11]],
  feats:[{t:"sign",id:"sg2",x:21,y:6,text:"Your Ember Blade was forged for this vigil. Swing it with SPACE or the left mouse; SHIFT to dash through a killing stroke."}],
  foes:[["husk",15,7],["chime",18,4]],
  gears:[[120,300,110,0.06]],dust:10},

/* ---------- ZONE 2 : THE RUSTED ARCHIVES ---------- */
z2a:{zone:2,name:"The Rusted Archives",sub:"Archive Vestibule — bronze grating, drifting dust",amb:"grate",pal:"archive",
  ex:{W:"z1b",E:"z2b"},
  walls:[[6,1,2,5],[6,9,2,5],[16,1,2,5],[16,9,2,5]],
  crates:[[11,3],[12,11]],
  jars:[[3,3],[3,11],[21,3],[21,11],[21,12]],
  feats:[{t:"sign",id:"sg3",x:2,y:2,text:"INDEX OF BRIGHT HOURS — every construct you unmake lets go of 5 to 15 Golden Sigils. Broken jars keep older coin."}],
  foes:[["husk",12,7],["chime",19,6],["sentry",20,2]],
  gears:[[470,215,80,-0.05],[500,430,110,0.04]],dust:26},

z2b:{zone:2,name:"The Rusted Archives",sub:"Index Hall — Sanctuary Stone & Merchant Altar",amb:"grate",pal:"archive",
  ex:{W:"z2a",E:"z2c"}, clear:true,
  walls:[[11,5,2,5]],
  crates:[[19,2],[19,3],[8,2]],
  jars:[[3,12],[21,12]],
  feats:[{t:"stone",id:"s2",x:6,y:7,needClear:true},
         {t:"shop",id:"shop",x:3,y:3,needClear:true},
         {t:"chest",id:"band",x:21,y:2,loot:{acc:"band"}},
         {t:"sign",id:"sg4",x:13,y:3,text:"The merchant altar still hums with commerce. Silence the constructs first — bronze will not bargain over noise."}],
  foes:[["husk",8,3],["husk",15,3],["chime",18,11],["sentry",20,7]],
  gears:[[470,300,130,0.03]],dust:30},

z2c:{zone:2,name:"The Rusted Archives",sub:"Catalogue Annex — coin-jars by the hundred",amb:"grate",pal:"archive",
  ex:{W:"z2b",E:"z3a"},
  walls:[[5,2,3,3],[13,8,4,3]],
  crates:[[10,12],[11,12],[18,2]],
  jars:[[2,11],[3,11],[4,11],[21,4],[22,4],[2,3],[20,11],[21,11]],
  feats:[{t:"chest",id:"hid2c",x:21,y:2,loot:{gold:90,item:"stem",n:1},lantern:true},
         {t:"sign",id:"sg5",x:12,y:6,text:"Warden’s note: some caches were sealed with light. Only a lantern of living phosphor shows the seams."}],
  foes:[["husk",10,4],["husk",17,12],["sentry",22,2],["chime",6,12]],
  gears:[[700,480,120,0.04]],dust:24},

/* ---------- ZONE 3 : THE COLLAPSED GALLERY ---------- */
z3a:{zone:3,name:"The Collapsed Gallery",sub:"Tilted Hall — two bronze plates, one held breath",amb:"stone",pal:"gallery",
  ex:{W:"z2c",E:"z3b"},
  walls:[[10,1,3,4],[11,10,3,4]],
  crates:[[8,12],[9,12]],
  jars:[[4,3],[20,3]],
  gates:[{id:"g3",x:23,y:6,w:1,h:3,flag:"g3"}],
  plates:[{id:"pA",x:6,y:4},{id:"pB",x:17,y:11}],
  seq:{plates:["pA","pB"],gate:"g3",window:3.4},
  feats:[{t:"sign",id:"sg6",x:8,y:6,text:"GALLERY MECHANISM: press both bronze plates within one held breath and the eastern grate rises. A construct lured onto a plate will hold it for you."}],
  foes:[["husk",12,7],["chime",18,4]],
  gears:[[430,180,90,-0.05]],dust:18},

z3b:{zone:3,name:"The Collapsed Gallery",sub:"Fracture Alcove — the Curator still bows to no one",amb:"stone",pal:"gallery",
  ex:{W:"z3a",E:"z3c"},
  walls:[[19,1,1,1],[19,4,1,2],[20,6,3,1]],
  cracks:[{id:"crack4",r:[[19,2,1,2]],hp:8}],
  crates:[[6,3],[7,3]],
  jars:[[4,11],[5,11]],
  feats:[{t:"pedestal",id:"ped4",x:21,y:3,loot:{heart:true}}],
  foes:[["curator",10,7,"boss"]],
  gears:[[300,470,120,0.05]],dust:16},

z3c:{zone:3,name:"The Collapsed Gallery",sub:"Ember Altar — Upgrade I",amb:"stone",pal:"gallery",
  ex:{W:"z3b",E:"z4a"},
  walls:[[3,2,2,3],[19,10,2,3]],
  crates:[[8,12],[16,3]],
  jars:[[4,11],[21,2]],
  feats:[{t:"altar",id:"al1",x:12,y:4,up:1},
         {t:"sign",id:"sg7",x:14,y:6,text:"ALTAR OF THE IGNITED EDGE: lay the Ember Blade upon the coals. Fire remembers; the blade will too."}],
  foes:[["husk",7,11],["chime",17,3],["husk",20,7]],
  gears:[[480,300,140,0.03]],dust:14}
};

Object.assign(ROOMS,{
/* ---------- ZONE 4 : THE SUNKEN VAULT ---------- */
z4a:{zone:4,name:"The Sunken Vault",sub:"Flooded Steps — mist rises, water remembers",amb:"damp",pal:"vault",
  ex:{W:"z3c",E:"z4b"},
  pits:[[6,1,4,3],[14,10,5,4]],
  walls:[[11,5,2,5]],
  crates:[[4,6]],
  jars:[[3,4],[20,3]],
  feats:[{t:"cistern",id:"cis",x:3,y:11},
         {t:"sign",id:"sg8",x:8,y:3,text:"CISTERN OF QUIET WATER: drink and the water knits flesh — one draught per vigil, refilled only at a Sanctuary Stone."}],
  foes:[["chime",15,4],["husk",10,12],["chime",20,7]],
  gears:[[250,200,90,0.04]],mist:26},

z4b:{zone:4,name:"The Sunken Vault",sub:"Cracked Sea Wall — hairline fractures, older stone",amb:"damp",pal:"vault",
  ex:{W:"z4a",E:"z4c"},
  pits:[[5,9,6,4],[13,2,3,3]],
  walls:[[18,1,1,1],[18,3,1,1],[19,4,4,1],[16,7,2,3]],
  cracks:[{id:"crackShard",r:[[18,2,1,1]],hp:8,hidden:true}],
  crates:[[8,4],[9,4]],
  jars:[[3,12],[21,11]],
  feats:[{t:"pedestal",id:"pedShard",x:20,y:2,loot:{shard:true}},
         {t:"sign",id:"sg9",x:12,y:11,text:"Warden’s note: the sea wall is not solid. Look for hairline fractures and a discoloration in the stone — then strike it until it gives."}],
  foes:[["chime",8,6],["husk",14,12],["sentry",21,12],["husk",6,2]],
  gears:[[640,470,100,-0.04]],mist:30},

z4c:{zone:4,name:"The Sunken Vault",sub:"Mirror Gallery — bend the light to earn the lantern",amb:"damp",pal:"vault",
  ex:{W:"z4b",E:"z5a"},
  walls:[[15,4,4,3],[17,10,4,3],[5,3,3,2],[20,8,3,2]],
  gates:[{id:"g4",x:23,y:6,w:1,h:3,flag:"g4"}],
  feats:[{t:"emitter",x:2,y:2,dir:"E"},
         {t:"mirror",id:"m1",x:10,y:2,o:0},
         {t:"mirror",id:"m2",x:10,y:12,o:0},
         {t:"mirror",id:"m3",x:3,y:12,o:0},
         {t:"mirror",id:"m4",x:3,y:6,o:1},
         {t:"receiver",x:8,y:6,flag:"lightOn"},
         {t:"pedestal",id:"pedLantern",x:21,y:2,loot:{acc:"lantern"},needFlag:"lightOn"},
         {t:"sign",id:"sg10",x:12,y:8,text:"LIGHT MECHANISM: mirrors turn on the flat (press E). The old rule stands — a beam running east leaves by the south face; running south leaves by the west."}],
  foes:[["chime",16,7],["husk",21,11]],
  gears:[[720,220,90,0.05]],mist:22},

/* ---------- ZONE 5 : THE TWIN SPIRE ---------- */
z5a:{zone:5,name:"The Twin Spire",sub:"Spire Approach — Sanctuary Stone & the sealed vault",amb:"spire",pal:"spire",
  ex:{W:"z4c",E:"z5b"},
  pits:[[10,2,5,5]],
  walls:[[16,1,1,2],[16,4,1,2],[17,6,6,1]],
  gates:[{id:"g5v",x:16,y:3,w:1,h:1,flag:"g5v"}],
  crates:[[7,12],[8,12]],
  jars:[[3,3],[21,12]],
  feats:[{t:"stone",id:"s3",x:3,y:7},
         {t:"switch",id:"sw5",x:12,y:4,flag:"g5v"},
         {t:"pedestal",id:"ped5",x:20,y:3,loot:{heart:true}},
         {t:"sign",id:"sg11",x:6,y:10,text:"THE SEALED VAULT: the crystal switch stands across the chasm, far beyond any blade’s reach. Only the Aether Shard will wake it."}],
  foes:[["husk",8,3],["sentry",21,12],["chime",17,8]],
  gears:[[430,330,120,-0.03]],dust:16},

z5b:{zone:5,name:"The Twin Spire",sub:"Gauntlet Ledge — the Spire Guardian",amb:"spire",pal:"spire",
  ex:{W:"z5a",E:"z5c"}, clear:true,
  walls:[[11,1,2,3],[11,11,2,3]],
  vents:[[5,7,3,1,0],[15,3,1,4,1.3]],
  crates:[[3,5],[4,5],[20,8]],
  jars:[[3,3],[12,4]],
  feats:[{t:"chest",id:"chestBonus",x:21,y:2,loot:{gold:200}},
         {t:"chest",id:"hid5",x:2,y:12,loot:{gold:120,item:"seal",n:2},lantern:true},
         {t:"sign",id:"sg12",x:13,y:8,text:"Steam vents grind along the ledge on a fixed rhythm. A Shadow Cloak turns their scalding aside; a dash will do, in a pinch."}],
  foes:[["husk",7,3],["husk",16,11],["chime",12,7],["sentry",20,3],["sentry",4,12],["guardian",20,12,"boss"]],
  gears:[[230,180,110,0.05]],dust:18},

z5c:{zone:5,name:"The Twin Spire",sub:"Solar Altar — Upgrade II",amb:"spire",pal:"spire",
  ex:{W:"z5b",E:"z6a"},
  walls:[[3,10,3,2],[18,2,3,2]],
  crates:[[8,3]],
  jars:[[21,11],[4,3]],
  feats:[{t:"altar",id:"al2",x:12,y:3,up:2},
         {t:"sign",id:"sg13",x:14,y:5,text:"ALTAR OF THE SOLAR FALCHION: a slow wind-up, a heavier fall. Light does not hurry — it simply arrives."}],
  foes:[["chime",6,4],["husk",17,11],["husk",21,5]],
  gears:[[480,420,150,-0.03]],dust:14},

/* ---------- ZONE 6 : THE CORE CHAMBER ---------- */
z6a:{zone:6,name:"The Core Chamber",sub:"The Long Bridge — two crystals, one gate",amb:"core",pal:"core",
  ex:{W:"z5c",E:"z6b"},
  pits:[[4,1,5,5],[4,9,5,5],[13,1,4,4],[13,10,4,4]],
  gates:[{id:"g6",x:23,y:6,w:1,h:3,flag:"sw6a+sw6b"}],
  feats:[{t:"switch",id:"sw6a",x:6,y:3,flag:"sw6a"},
         {t:"switch",id:"sw6b",x:6,y:11,flag:"sw6b"},
         {t:"sign",id:"sg14",x:11,y:7,text:"The Core Beacon burns one hall east. Two crystal switches hold the last grate — and the Corroded Regent holds the Beacon."}],
  foes:[["sentry",14,7],["chime",18,4],["husk",18,11]],
  gears:[[200,300,140,0.03],[760,300,140,-0.03]],dust:12},

z6b:{zone:6,name:"The Core Chamber",sub:"Beacon Core — the Corroded Regent",amb:"core",pal:"core",clear:true,
  ex:{W:"z6a"},
  pits:[[2,1,4,4],[19,1,4,4],[19,10,4,4]],
  feats:[{t:"switch",id:"swB1",x:3,y:2,flag:"swB1"},
         {t:"switch",id:"swB2",x:21,y:2,flag:"swB2"},
         {t:"switch",id:"swB3",x:21,y:12,flag:"swB3"},
         {t:"beacon",x:12,y:3},
         {t:"sign",id:"sg15",x:2,y:8,text:"The Regent wears a corrosion-shield. Three crystal switches hold it shut — the Shard reaches them where your blade cannot."}],
  foes:[["regent",12,9,"boss"]],
  gears:[[480,300,190,0.02]],dust:10}
});

const PALS={
  threshold:{skyA:"#101822",skyB:"#070a0e",floor:"#1b232b",floor2:"#151c23",grout:"#0d1318",wall:"#3b3527",wall2:"#5a4d33",edge:"#8a6a3a",moss:"#43503a"},
  archive:{skyA:"#12181f",skyB:"#06090c",floor:"#1d232a",floor2:"#161b21",grout:"#0b0f13",wall:"#3a3226",wall2:"#63533a",edge:"#a07c42",moss:"#3f4a38"},
  gallery:{skyA:"#151a20",skyB:"#07090c",floor:"#22262a",floor2:"#1a1e22",grout:"#0d1013",wall:"#453a2b",wall2:"#6b5a3c",edge:"#b08a4c",moss:"#4a5540"},
  vault:{skyA:"#0d1620",skyB:"#04070a",floor:"#17222a",floor2:"#111a21",grout:"#080d11",wall:"#2f3a3f",wall2:"#4a5a5c",edge:"#6f8a86",moss:"#3c5a4a"},
  spire:{skyA:"#181c26",skyB:"#07080d",floor:"#20242c",floor2:"#181c23",grout:"#0b0e12",wall:"#3c3a33",wall2:"#5f5947",edge:"#c19a52",moss:"#4b5340"},
  core:{skyA:"#1a1414",skyB:"#060405",floor:"#241f1d",floor2:"#1a1615",grout:"#0a0808",wall:"#3d2f26",wall2:"#6a4b31",edge:"#e0a44a",moss:"#423b2c"}
};
const ZONE_TITLE={1:["ZONE I","The Ashen Threshold"],2:["ZONE II","The Rusted Archives"],3:["ZONE III","The Collapsed Gallery"],
  4:["ZONE IV","The Sunken Vault"],5:["ZONE V","The Twin Spire"],6:["ZONE VI","The Core Chamber"]};

/* ============================ GAME STATE ============================ */
const G={
  state:"title", room:null, roomId:null, t:0, runT:0,
  gold:0, items:{stem:0,seal:0}, acc:{band:0,lantern:0,cloak:0}, worn:null,
  weapon:0, hasShard:false, ammo:0, maxAmmo:24, ammoT:0,
  containers:3, hp:6, flags:{}, kills:0, earned:0,
  stoneRoom:"z1a", stonePos:{x:tx(3),y:tx(7)+36},
  prj:[], parts:[], floats:[], drops:[], shake:0, hitstop:0, seqT:0, seqStep:0,
  menu:null, dead:false, deadT:0, ending:false
};
const maxHp=()=>G.containers*2;
function flag(k,v){ if(v===undefined) return !!G.flags[k]; G.flags[k]=v; }
const P={x:400,y:300,r:13,hp:6,face:0,walk:0,moving:false,iframes:0,atkT:0,swing:0,swingHit:false,
  dashT:0,dashCd:0,dashDir:0,hurtT:0,burn:null,stepT:0};

/* ============================ ROOM BUILD ============================ */
function segsH(y,gap){const o=[];if(!gap){o.push(rr(0,y,COLS,1));return o;}
  const a=gap[0],b=gap[1]; if(a>0)o.push(rr(0,y,a,1)); if(b<COLS-1)o.push(rr(b+1,y,COLS-1-b,1)); return o;}
function segsV(x,gap){const o=[];if(!gap){o.push(rr(x,0,1,ROWS));return o;}
  const a=gap[0],b=gap[1]; if(a>0)o.push(rr(x,0,1,a)); if(b<ROWS-1)o.push(rr(x,b+1,1,ROWS-1-b)); return o;}

function buildRoom(id){
  const d=ROOMS[id], R={id,d,solid:[],objs:[],gates:[],plates:[],feats:[],foes:[],bg:null,gap:{}};
  R.gap.W=d.gap&&d.gap.W?d.gap.W:[6,8]; R.gap.E=d.gap&&d.gap.E?d.gap.E:[6,8];
  R.gap.N=d.gap&&d.gap.N?d.gap.N:[10,12]; R.gap.S=d.gap&&d.gap.S?d.gap.S:[10,12];
  if(d.ex.W) segsV(0,R.gap.W).forEach(r=>R.solid.push(r)); else R.solid.push(rr(0,0,1,ROWS));
  if(d.ex.E) segsV(COLS-1,R.gap.E).forEach(r=>R.solid.push(r)); else R.solid.push(rr(COLS-1,0,1,ROWS));
  if(d.ex.N) segsH(0,R.gap.N).forEach(r=>R.solid.push(r)); else R.solid.push(rr(0,0,COLS,1));
  if(d.ex.S) segsH(ROWS-1,R.gap.S).forEach(r=>R.solid.push(r)); else R.solid.push(rr(0,ROWS-1,COLS,1));
  (d.walls||[]).forEach(w=>R.solid.push(rr(w[0],w[1],w[2],w[3])));
  (d.pits||[]).forEach(w=>R.solid.push(Object.assign(rr(w[0],w[1],w[2],w[3]),{pit:true})));
  (d.crates||[]).forEach((c,i)=>{ const id2=id+".crate"+i;
    if(!flag(id2)) R.objs.push({id:id2,kind:"crate",rect:rr(c[0],c[1],1,1),hp:3,dead:false}); });
  (d.jars||[]).forEach((c,i)=>{ const id2=id+".jar"+i;
    if(!flag(id2)) R.objs.push({id:id2,kind:"jar",rect:rr(c[0],c[1],1,1),hp:1,dead:false}); });
  (d.cracks||[]).forEach(c=>{ if(!flag(c.id)) R.objs.push({id:c.id,kind:c.hidden?"hidden":"crack",rects:c.r.map(w=>rr(w[0],w[1],w[2],w[3])),hp:c.hp,dead:false}); });
  (d.gates||[]).forEach(g=>R.gates.push({id:g.id,rect:rr(g.x,g.y,g.w,g.h),flag:g.flag}));
  (d.plates||[]).forEach(p=>R.plates.push({id:p.id,rect:rr(p.x,p.y,1,1),on:false,was:false}));
  (d.feats||[]).forEach(f=>R.feats.push(Object.assign({},f,{px:tx(f.x),py:tx(f.y)})));
  (d.foes||[]).forEach((f,i)=>{ const pers=!!f[3];
    if(pers&&flag(id+".foe"+i)) return;
    R.foes.push(spawnEnemy(f[0],tx(f[1]),tx(f[2]),pers?id+".foe"+i:null)); });
  return R;
}
function dynBlockers(R){
  const out=[];
  R.objs.forEach(o=>{ if(o.dead) return;
    if(o.rects) o.rects.forEach(r=>out.push(r)); else out.push(o.rect); });
  R.gates.forEach(g=>{ if(!gateOpen(g)) out.push(g.rect); });
  if(G.room&&G.room.d&&G.room.d.clear&&R.foes.some(f=>f.hp>0)){
    // corridor seals over exits while the hall is unquiet
    const d=R.d, gp=R.gap;
    if(d.ex.W) out.push({x:0,y:gp.W[0]*T,w:T*0.9,h:(gp.W[1]-gp.W[0]+1)*T,seal:true});
    if(d.ex.E) out.push({x:VW-T*0.9,y:gp.E[0]*T,w:T*0.9,h:(gp.E[1]-gp.E[0]+1)*T,seal:true});
    if(d.ex.N) out.push({x:gp.N[0]*T,y:0,w:(gp.N[1]-gp.N[0]+1)*T,h:T*0.9,seal:true});
    if(d.ex.S) out.push({x:gp.S[0]*T,y:VH-T*0.9,w:(gp.S[1]-gp.S[0]+1)*T,h:T*0.9,seal:true});
  }
  return out;
}
function gateOpen(g){ return g.flag.split("+").every(f=>flag(f)); }
function blockers(){ return G.room.solid.concat(dynBlockers(G.room)); }
function rectOf(o){ return o.rect||o; }
function hitRect(x,y,r,rects){
  for(const q of rects){ const cx=clamp(x,q.x,q.x+q.w), cy=clamp(y,q.y,q.y+q.h);
    if((x-cx)*(x-cx)+(y-cy)*(y-cy) < r*r) return q; }
  return null;
}
function moveEnt(e,dx,dy){
  const B=blockers();
  e.x+=dx; let h=hitRect(e.x,e.y,e.r,B);
  if(h){ e.x = dx>0 ? h.x-e.r-0.01 : h.x+h.w+e.r+0.01; }
  e.y+=dy; h=hitRect(e.x,e.y,e.r,B);
  if(h){ e.y = dy>0 ? h.y-e.r-0.01 : h.y+h.h+e.r+0.01; }
  e.x=clamp(e.x,e.r,VW-e.r); e.y=clamp(e.y,e.r,VH-e.r);
}

/* ============================ PARTICLES / FLOATERS ============================ */
function part(o){ G.parts.push(Object.assign({x:0,y:0,vx:0,vy:0,life:.6,max:.6,r:2,c:"#fff",glow:false,g:0,drag:1},o)); }
function burst(x,y,c,n,spd,o){ o=o||{};
  for(let i=0;i<n;i++){ const a=rnd(0,Math.PI*2), s=rnd(spd*0.3,spd);
    part({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,c,r:rnd(1.2,o.r||3),life:rnd(.25,o.life||.6),max:o.life||.6,
      glow:o.glow!==false,g:o.g||220,drag:o.drag||0.92}); } }
function sparks(x,y,c){ burst(x,y,c,12,260,{life:.4,g:300,r:2.2}); }
function emberSmoke(x,y){ for(let i=0;i<7;i++) part({x:x+rnd(-6,6),y:y+rnd(-6,6),vx:rnd(-18,18),vy:rnd(-46,-14),
  c:Math.random()<.5?"#ff8a3c":"#5b5f66",r:rnd(2,4),life:rnd(.5,.9),max:.9,g:-16,glow:false,drag:.97}); }
function goldSwirl(x,y){ for(let i=0;i<10;i++) part({x:x+rnd(-8,8),y:y+rnd(-8,8),vx:rnd(-40,40),vy:rnd(-90,-20),
  c:Math.random()<.5?"#e8b45a":"#ffd98a",r:rnd(1,2.4),life:rnd(.4,.8),max:.8,g:60,drag:.95}); }
function motes(x,y,c,n){ for(let i=0;i<(n||12);i++) part({x:x+rnd(-12,12),y:y+rnd(-4,14),vx:rnd(-14,14),vy:rnd(-70,-26),
  c:c||"#ffffff",r:rnd(1,2.6),life:rnd(.6,1.2),max:1.2,g:-8,drag:.985}); }
function auraPulse(){ for(let i=0;i<34;i++){ const a=rnd(0,Math.PI*2);
  part({x:P.x+Math.cos(a)*20,y:P.y+Math.sin(a)*20,vx:Math.cos(a)*70,vy:Math.sin(a)*70,
    c:i%2?"#ffd98a":"#fff3d2",r:rnd(1.5,3.2),life:rnd(.5,1),max:1,g:0,drag:.94}); } }
function floatTxt(x,y,txt,c){ G.floats.push({x,y,txt,c:c||"#fff",t:0}); }
function updateParticles(dt){
  for(let i=G.parts.length-1;i>=0;i--){ const p=G.parts[i];
    p.life-=dt; if(p.life<=0){ G.parts.splice(i,1); continue; }
    p.vy+=(p.g||0)*dt; p.x+=p.vx*dt; p.y+=p.vy*dt;
    p.vx*=Math.pow(p.drag||1,dt*60); p.vy*=Math.pow(p.drag||1,dt*60); }
  for(let i=G.floats.length-1;i>=0;i--){ const f=G.floats[i]; f.t+=dt; f.y-=26*dt; if(f.t>0.9) G.floats.splice(i,1); }
  for(let i=G.drops.length-1;i>=0;i--){ const d=G.drops[i];
    d.t+=dt; const dx=P.x-d.x, dy=P.y-d.y, dd=Math.hypot(dx,dy);
    if(dd<86||d.t>1.2){ const s=clamp(240/Math.max(dd,12),3,11); d.x+=dx/dd*s*dt*60*0.35; d.y+=dy/dd*s*dt*60*0.35; }
    else { d.x+=Math.sin(d.t*4+d.seed)*10*dt; }
    if(dd<P.r+8){ G.gold+=d.v; G.earned+=d.v; A.sfx.coin(); goldSwirl(d.x,d.y); floatTxt(d.x,d.y-8,"+"+d.v,"#ffd98a"); G.drops.splice(i,1); updateHUD(); } }
}

/* ============================ LOOT / ITEMS ============================ */
function give(loot,srcX,srcY){
  if(!loot) return;
  if(loot.gold){ G.gold+=loot.gold; G.earned+=loot.gold; goldSwirl(srcX||P.x,srcY||P.y); A.sfx.coin(); }
  if(loot.item){ G.items[loot.item]+=loot.n||1; A.sfx.pickup("common"); motes(srcX||P.x,srcY||P.y,"#b8ecbf");
    const inm=loot.item==="stem"?"Vital Stem":"Binding Seal";
    const ifx=loot.item==="stem"?"Press 1 to drink — it restores 2 full containers (4 HP).":"Press 2 to bind — it freezes one construct in place for 3 seconds.";
    toast(inm+" acquired",ifx); }
  if(loot.acc){ G.acc[loot.acc]=1; A.sfx.pickup("rare"); motes(srcX||P.x,srcY||P.y,"#ffffff",18);
    const nm={band:"Warden’s Band",lantern:"Phantom Lantern",cloak:"Shadow Cloak"}[loot.acc];
    const fx={band:"Wrist-ring of living silver. Attack speed rises a little.",
      lantern:"Soft blue phosphor. Hidden caches and hairline cracks now shine for you.",
      cloak:"Dark cloth at your hem. Steam vents and traps bite half as deep."}[loot.acc];
    toast(nm+" acquired",fx+" Open the Satchel (TAB) to wear it."); shimmer(1.2); }
  if(loot.shard){ G.hasShard=true; G.ammo=16; A.sfx.pickup("rare"); A.sfx.altar(1); motes(srcX||P.x,srcY||P.y,"#bfe9ff",22);
    toast("Aether Shard acquired","A crystalline launcher settles into your off-hand. Fire with K or the right mouse — it reaches where blades cannot."); shimmer(1.6); }
  if(loot.heart){ G.containers=Math.min(5,G.containers+1); G.hp=maxHp(); A.sfx.heart(); auraPulse(); shimmer(1.6);
    toast("Heart Container acquired","Your lifeblood deepens. "+G.containers+" containers — "+maxHp()+" HP."); }
  if(loot.weapon){ G.weapon=loot.weapon; A.sfx.altar(loot.weapon); shimmer(1.8); }
  if(loot.ammo){ G.ammo=Math.min(G.maxAmmo,G.ammo+loot.ammo); A.sfx.pickup("common"); }
  updateHUD();
}
function useStem(){
  if(G.items.stem<=0){ A.sfx.deny(); return; }
  if(G.hp>=maxHp()){ toast("Vital Stem","Already at full lifeblood. Save it."); A.sfx.deny(); return; }
  G.items.stem--; G.hp=Math.min(maxHp(),G.hp+4); A.sfx.potion(); healFlash();
  motes(P.x,P.y,"#b8ecbf",16); floatTxt(P.x,P.y-18,"+4 HP","#b8ecbf"); updateHUD();
}
function useSeal(){
  if(G.items.seal<=0){ A.sfx.deny(); return; }
  let best=null,bd=280;
  for(const e of G.room.foes){ if(e.hp<=0||e.boss) continue; const d=dist(e,P); if(d<bd){bd=d;best=e;} }
  if(!best){ toast("Binding Seal","Nothing close enough to bind."); A.sfx.deny(); return; }
  G.items.seal--; best.frozen=3; A.sfx.seal();
  burst(best.x,best.y,"#7fb6ff",18,180,{life:.6,g:0});
  floatTxt(best.x,best.y-24,"BOUND","#7fb6ff"); updateHUD();
}

/* ============================ ENEMIES ============================ */
const ETYPES={
  husk:{hp:6,r:15,spd:64,dmg:1,gmin:5,gmax:10,c:"#3d434c",eye:"#ff8a3c"},
  chime:{hp:4,r:13,spd:94,dmg:1,gmin:6,gmax:12,c:"#4a4230",eye:"#ffd98a"},
  sentry:{hp:5,r:14,spd:32,dmg:1,gmin:8,gmax:14,c:"#33404c",eye:"#9fe8ff",ranged:true},
  curator:{hp:26,r:22,spd:48,dmg:2,gmin:40,gmax:60,c:"#4a4030",eye:"#ff8a3c",big:true},
  guardian:{hp:32,r:22,spd:78,dmg:2,gmin:50,gmax:80,c:"#3a3f46",eye:"#ff5a3c",big:true},
  regent:{hp:70,r:30,spd:56,dmg:2,gmin:100,gmax:140,c:"#2e2a2c",eye:"#c8632a",big:true,boss:true}
};
function spawnEnemy(type,x,y,pers){
  const s=ETYPES[type];
  return {type,st:s,x,y,r:s.r,hp:s.hp,maxHp:s.hp,spd:s.spd,dmg:s.dmg,cd:rnd(.3,1),wind:0,face:0,hitT:0,burnT:0,
    frozen:0,t:rnd(0,6),state:"idle",vx:0,vy:0,phase:0,pers:persistKey(pers),boss:!!s.big,anim:rnd(0,6),chargeT:0};
}
function persistKey(k){ return k; }
function damageEnemy(e,dmg,kx,ky,tier){
  if(e.hp<=0) return;
  if(e.type==="regent" && bossShieldUp()){ shieldPing(e); return; }
  e.hp-=dmg; e.hitT=.16;
  if(tier>0){ e.burnT=tier===2?1.4:0.9; if(tier===2) emberSmoke(e.x,e.y); }
  if(kx!==undefined){ e.vx=(e.vx||0)+kx; e.vy=(e.vy||0)+ky; }
  floatTxt(e.x+rnd(-6,6),e.y-e.r-6,""+dmg,tier===2?"#ffd98a":(tier===1?"#ff8a3c":"#f0e4c2"));
  sparks(e.x,e.y,tier===2?"#ffd98a":(tier===1?"#ff8a3c":"#e8a24a"));
  if(e.hp<=0) killEnemy(e); else A.sfx.hit(tier);
}
function killEnemy(e){
  e.hp=0; G.kills++; A.sfx.kill();
  burst(e.x,e.y,e.st.eye,20,240,{life:.6});
  burst(e.x,e.y,"#2a2622",14,160,{life:.8,glow:false,g:120});
  if(e.burnT>0) emberSmoke(e.x,e.y);
  const gold=ri(e.st.gmin,e.st.gmax);
  const n=Math.max(1,Math.round(gold/6));
  for(let i=0;i<n;i++) G.drops.push({x:e.x+rnd(-10,10),y:e.y+rnd(-10,10),v:Math.round(gold/n),t:0,seed:rnd(0,9)});
  if(e.pers) flag(e.pers,true);
  if(e.type==="curator"){ toast("Gallery Curator unmade","The alcove stands quiet. The crack-marked wall to the north-east looks thinner than the rest.");
    burst(e.x,e.y,"#ffd98a",26,300,{life:.9}); }
  if(e.type==="guardian"){ toast("Spire Guardian unmade","Something dark slips from its armour — the Shadow Cloak.");
    give({acc:"cloak"},e.x,e.y); }
  if(e.type==="regent"){ bossDefeated(); }
  if(G.room.d.clear && !G.room.foes.some(f=>f.hp>0)){ A.sfx.gate();
    toast("The hall falls quiet","Corrosion seals loosen. The way onward opens."); }
}
function damagePlayer(dmg,srcX,srcY,env){
  if(P.iframes>0||G.dead) return;
  let d=dmg;
  if(env && G.worn==="cloak") d=Math.max(0,d-1);
  if(d<=0){ floatTxt(P.x,P.y-20,"turned aside","#a9bccb"); P.iframes=.6; A.sfx.block(); return; }
  G.hp-=d; P.iframes=.9; P.hurtT=.3; G.shake=Math.max(G.shake,10);
  flashRed(); A.sfx.hurt();
  burst(P.x,P.y,"#c8302a",18,220,{life:.5});
  floatTxt(P.x,P.y-22,"-"+d,"#ff6a5a");
  updateHUD();
  if(G.hp<=0) die();
}
function die(){
  G.dead=true; G.deadT=0; G.hp=0; A.sfx.roar();
  burst(P.x,P.y,"#c8302a",30,300,{life:.9});
  showDeath();
}

/* ============================ PROJECTILES ============================ */
function spawnPrj(o){ G.prj.push(Object.assign({x:0,y:0,vx:0,vy:0,r:5,dmg:1,life:1.4,from:"en",kind:"bolt",c:"#9fe8ff",t:0},o)); }
function updatePrj(dt){
  for(let i=G.prj.length-1;i>=0;i--){ const p=G.prj[i];
    p.t+=dt; p.life-=dt; p.x+=p.vx*dt; p.y+=p.vy*dt;
    if(p.kind==="ring"){ p.r+=p.grow*dt;
      if(p.r>p.max){ G.prj.splice(i,1); continue; }
      if(p.from==="en"&&Math.abs(dist(p,P)-p.r)<9&&p.hit!==true){ damagePlayer(p.dmg,p.x,p.y); p.hit=true; }
      if(p.life<=0) G.prj.splice(i,1);
      continue; }
    if(p.life<=0||p.x<-20||p.x>VW+20||p.y<-20||p.y>VH+20){ if(p.kind==="bolt"&&p.from==="pl") burst(p.x,p.y,"#bfe9ff",5,90,{life:.3});
      G.prj.splice(i,1); continue; }
    const B=blockers(); const hit=hitRect(p.x,p.y,p.r,B);
    if(hit){ impactPrj(p,i,hit); continue; }
    if(p.from==="pl"){
      // breakables
      let done=false;
      for(const o of G.room.objs){ if(o.dead) continue;
        const r=o.rects?null:o.rect; const rs=o.rects||[r];
        for(const q of rs){ if(p.x>q.x-p.r&&p.x<q.x+q.w+p.r&&p.y>q.y-p.r&&p.y<q.y+q.h+p.r){ damageObj(o,1,true); done=true; break; } }
        if(done) break; }
      if(done){ burst(p.x,p.y,"#bfe9ff",8,140,{life:.3}); A.sfx.boltHit(); G.prj.splice(i,1); continue; }
      // crystal switches
      for(const f of G.room.feats){ if(f.t!=="switch"||flag(f.flag)) continue;
        if(Math.abs(p.x-f.px)<T*0.6&&Math.abs(p.y-f.py)<T*0.6){ hitSwitch(f); done=true; break; } }
      if(done){ G.prj.splice(i,1); continue; }
      for(const e of G.room.foes){ if(e.hp<=0) continue;
        if(dist(p,e)<p.r+e.r){ damageEnemy(e,p.dmg,0,0,1); A.sfx.boltHit(); G.prj.splice(i,1); done=true; break; } }
      if(done) continue;
    } else {
      if(dist(p,P)<p.r+P.r){ damagePlayer(p.dmg,p.x,p.y); G.prj.splice(i,1); continue; }
    }
  }
}
function impactPrj(p,i,rect){
  if(p.kind==="bolt"&&p.from==="pl"){ burst(p.x,p.y,"#bfe9ff",7,150,{life:.3}); A.sfx.boltHit(); }
  else burst(p.x,p.y,p.c,5,110,{life:.25});
  G.prj.splice(i,1);
}
function hitSwitch(f){
  flag(f.flag,true); A.sfx.plate(); A.sfx.boltHit();
  burst(f.px,f.py,"#bfe9ff",22,240,{life:.7});
  G.shake=Math.max(G.shake,6);
  let opened=false;
  G.room.gates.forEach(g=>{ if(gateOpen(g)) opened=true; });
  if(opened){ A.sfx.gate(); toast("Crystal switch struck","Grates grind open somewhere in the hall."); }
  else toast("Crystal switch struck","Something unlocks in the dark.");
  if(f.flag==="swB1"||f.flag==="swB2"||f.flag==="swB3"){
    if(flag("swB1")&&flag("swB2")&&flag("swB3")){ A.sfx.roar(); toast("Corrosion-shield collapsed","The Regent is open. Close and finish it!"); }
    else floatTxt(f.px,f.py-18,"SHIELD FADES","#bfe9ff");
  }
}

/* ============================ PLAYER COMBAT ============================ */
const WNAME=["Ember Blade","Ignited Edge","Solar Falchion"];
function meleeAttack(){
  if(P.atkT>0||P.swing>0||G.dead||G.menu) return;
  const tier=G.weapon;
  const cd=[0.42,0.30,0.55][tier]*(G.worn==="band"?0.85:1);
  P.atkT=cd; P.swingMax=[0.24,0.22,0.34][tier]; P.swing=P.swingMax; P.swingHit=false; P.swingTier=tier;
  A.sfx.swing(tier);
  if(tier>0) for(let i=0;i<6;i++) part({x:P.x+Math.cos(P.face)*10,y:P.y+Math.sin(P.face)*10,
    vx:Math.cos(P.face+rnd(-.6,.6))*rnd(40,140),vy:Math.sin(P.face+rnd(-.6,.6))*rnd(40,140),
    c:tier===2?"#ffd98a":"#ff8a3c",r:rnd(1,2.4),life:rnd(.2,.45),max:.45,g:120,drag:.93});
}
function doMeleeHit(){
  const tier=P.swingTier, reach=[52,58,84][tier], arc=[2.0,2.2,2.6][tier], dmg=[2,3,5][tier];
  for(const e of G.room.foes){ if(e.hp<=0) continue;
    const d=dist(P,e); if(d>reach+e.r) continue;
    let da=Math.atan2(e.y-P.y,e.x-P.x)-P.face; while(da>Math.PI)da-=Math.PI*2; while(da<-Math.PI)da+=Math.PI*2;
    if(Math.abs(da)>arc/2) continue;
    const k=tier===2?190:(tier===1?110:70);
    damageEnemy(e,dmg,Math.cos(P.face)*k,Math.sin(P.face)*k,tier);
  }
  for(const o of G.room.objs){ if(o.dead) continue;
    const rs=o.rects||[o.rect]; let hit=false;
    for(const q of rs){ const cx=clamp(P.x,q.x,q.x+q.w), cy=clamp(P.y,q.y,q.y+q.h);
      const d=Math.hypot(P.x-cx,P.y-cy); if(d>reach) continue;
      let da=Math.atan2(cy-P.y,cx-P.x)-P.face; while(da>Math.PI)da-=Math.PI*2; while(da<-Math.PI)da+=Math.PI*2;
      if(Math.abs(da)<arc/2+0.5){ hit=true; break; } }
    if(hit) damageObj(o,dmg,false); }
  G.shake=Math.max(G.shake,tier===2?7:3);
}
function damageObj(o,dmg,isRanged){
  if(o.dead) return;
  let d=dmg;
  if(o.kind==="crack"||o.kind==="hidden") d=isRanged?1:[1,2,4][G.weapon];
  o.hp-=d; o.hitT=.15;
  if(o.kind==="crate"){ A.sfx.crack(); sparks(o.rect.x+o.rect.w/2,o.rect.y+o.rect.h/2,"#b08a4c"); }
  else if(o.kind==="jar"){ A.sfx.crack(); }
  else { A.sfx.crack(); sparks((o.rects[0].x+o.rects[o.rects.length-1].x)/2+20,(o.rects[0].y+o.rects[0].h/2),"#8a8a8a"); }
  if(o.hp>0) return;
  o.dead=true; flag(o.id,true);
  const cx=o.rect?o.rect.x+o.rect.w/2:(o.rects[0].x+o.rects[0].w/2), cy=o.rect?o.rect.y+o.rect.h/2:(o.rects[0].y+o.rects[0].h/2);
  if(o.kind==="crate"){ A.sfx.wallDown(); burst(cx,cy,"#8a6a3a",14,180,{life:.6,glow:false,g:200});
    const g=ri(3,8); for(let i=0;i<2;i++) G.drops.push({x:cx+rnd(-8,8),y:cy+rnd(-8,8),v:Math.round(g/2),t:0,seed:rnd(0,9)}); }
  else if(o.kind==="jar"){ A.sfx.wallDown(); burst(cx,cy,"#6b5636",12,180,{life:.5,glow:false,g:220});
    const g=ri(5,15); for(let i=0;i<3;i++) G.drops.push({x:cx+rnd(-8,8),y:cy+rnd(-8,8),v:Math.round(g/3),t:0,seed:rnd(0,9)}); }
  else { A.sfx.wallDown(); G.shake=14; burst(cx,cy,"#3a3f42",26,260,{life:.8,glow:false,g:260});
    burst(cx,cy,"#ffd98a",14,220,{life:.7});
    toast("The stone gives way","A hollow breathes behind the wall."); }
}
function rangedAttack(){
  if(G.dead||G.menu) return;
  if(!G.hasShard) return;
  if(P.atkT>0) return;
  if(G.ammo<=0){ A.sfx.deny(); floatTxt(P.x,P.y-24,"NO CHARGES","#9fb6c9"); return; }
  G.ammo--; P.atkT=0.3; A.sfx.shoot();
  spawnPrj({x:P.x+Math.cos(P.face)*20,y:P.y+Math.sin(P.face)*20,vx:Math.cos(P.face)*540,vy:Math.sin(P.face)*540,
    from:"pl",kind:"bolt",dmg:2,r:6,life:1.6,c:"#bfe9ff"});
  motes(P.x+Math.cos(P.face)*16,P.y+Math.sin(P.face)*16,"#bfe9ff",4);
  updateHUD();
}

/* ============================ ENEMY AI ============================ */
function updateFoes(dt){
  const R=G.room, cleared=!R.foes.some(f=>f.hp>0);
  for(const e of R.foes){ if(e.hp<=0) continue;
    e.t+=dt; e.anim+=dt; e.hitT=Math.max(0,e.hitT-dt); e.strike=Math.max(0,(e.strike||0)-dt);
    if(e.burnT>0){ e.burnT-=dt; e.burnDmg=(e.burnDmg||0)+dt;
      if(Math.random()<0.35) part({x:e.x+rnd(-8,8),y:e.y+rnd(-8,8),vx:rnd(-16,16),vy:rnd(-40,-12),
        c:Math.random()<.5?"#ff8a3c":"#ffb060",r:rnd(1,2.2),life:.4,max:.4,g:-10,drag:.97});
      if(e.burnDmg>0.9){ e.burnDmg-=0.9; damageEnemy(e,1,undefined,undefined,1); if(e.hp<=0) continue; } }
    if(e.vx||e.vy){ moveEnt(e,e.vx*dt,e.vy*dt); e.vx*=Math.pow(0.86,dt*60); e.vy*=Math.pow(0.86,dt*60);
      if(Math.abs(e.vx)<2)e.vx=0; if(Math.abs(e.vy)<2)e.vy=0; }
    if(e.frozen>0){ e.frozen-=dt; continue; }
    const d=dist(e,P), ang=Math.atan2(P.y-e.y,P.x-e.x);
    e.face=ang;
    if(G.dead) continue;

    if(e.wind>0){ e.wind-=dt;
      if(e.wind<=0){ e.strike=.22;
        if(e.type==="curator"){
          spawnPrj({x:e.x,y:e.y,from:"en",kind:"ring",r:10,grow:320,max:130,dmg:2,life:.7,c:"#ff8a3c",hit:false});
          A.sfx.roar(); G.shake=8; burst(e.x,e.y,"#8a6a3a",18,220,{life:.5,glow:false});
        } else if(e.type==="guardian"){
          e.charging=.55; e.cvx=Math.cos(ang)*430; e.cvy=Math.sin(ang)*430;
          A.sfx.roar(); burst(e.x,e.y,e.st.eye,12,180,{life:.4});
        } else {
          if(dist(e,P)<e.r+P.r+18) damagePlayer(e.dmg,e.x,e.y);
          e.cd=1.2;
        } }
      continue; }
    if(e.cd>0) e.cd-=dt;

    if(e.type==="husk"){
      if(d<330){ moveEnt(e,Math.cos(ang)*e.spd*dt,Math.sin(ang)*e.spd*dt); }
      else { moveEnt(e,Math.cos(e.t)*e.spd*0.3*dt,Math.sin(e.t*1.3)*e.spd*0.3*dt); }
      if(d<e.r+P.r+8&&e.cd<=0) e.wind=.3;
    } else if(e.type==="chime"){
      const wob=Math.sin(e.t*3)*0.9;
      const sp=e.spd*(e.dashT>0?2.4:1);
      if(e.dashT>0) e.dashT-=dt;
      if(d<300){ moveEnt(e,Math.cos(ang+wob)*sp*dt,Math.sin(ang+wob)*sp*dt);
        if(d<130&&e.cd<=0){ e.dashT=.5; e.cd=1.6; A.sfx.enemyShoot(); } }
      else moveEnt(e,Math.cos(e.t*1.6)*e.spd*0.4*dt,Math.sin(e.t*2)*e.spd*0.4*dt);
      if(d<e.r+P.r+4&&e.cd<=0){ e.wind=.16; }
    } else if(e.type==="sentry"){
      const want=190;
      if(d<want-40) moveEnt(e,-Math.cos(ang)*e.spd*dt,-Math.sin(ang)*e.spd*dt);
      else if(d>want+60) moveEnt(e,Math.cos(ang)*e.spd*dt,Math.sin(ang)*e.spd*dt);
      if(d<420&&e.cd<=0){ e.cd=2.2; e.strike=.2;
        A.sfx.enemyShoot();
        spawnPrj({x:e.x+Math.cos(ang)*16,y:e.y+Math.sin(ang)*16,vx:Math.cos(ang)*230,vy:Math.sin(ang)*230,
          from:"en",kind:"sbolt",dmg:1,r:6,life:2.4,c:"#9fe8ff"}); }
    } else if(e.type==="curator"){
      if(d<420) moveEnt(e,Math.cos(ang)*e.spd*dt,Math.sin(ang)*e.spd*dt);
      if(e.cd<=0&&d<260){ e.cd=3.4; e.wind=.55; }
    } else if(e.type==="guardian"){
      if(e.charging>0){ e.charging-=dt; moveEnt(e,e.cvx*dt,e.cvy*dt);
        if(d<e.r+P.r+8&&e.cd<=0){ damagePlayer(2,e.x,e.y); e.cd=1.4; }
        continue; }
      if(d<420) moveEnt(e,Math.cos(ang)*e.spd*dt,Math.sin(ang)*e.spd*dt);
      if(e.cd<=0&&d<340){ e.cd=3.6; e.wind=.4; }
    } else if(e.type==="regent"){
      regentAI(e,dt,d,ang);
      continue;
    }
  }
}
function regentAI(e,dt,d,ang){
  const shield=bossShieldUp();
  if(shield) e.shieldGlow=1; else e.shieldGlow=0;
  if(e.hp<=35&&e.phase===0){ e.phase=1; flag("swB1",false); flag("swB2",false);
    A.sfx.roar(); G.shake=16; burst(e.x,e.y,"#c8632a",30,320,{life:.9});
    toast("The Regent re-knits its shield","Corrosion crawls back over the armour — two crystal switches wake again. Bring the Shard to bear."); }
  e.cd-=dt;
  if(d<460&&e.state!=="charge"){ moveEnt(e,Math.cos(ang)*e.spd*dt,Math.sin(ang)*e.spd*dt); }
  if(e.state==="charge"){ e.ct-=dt; moveEnt(e,e.cvx*dt,e.cvy*dt);
    if(d<e.r+P.r+6&&e.ccd<=0){ damagePlayer(2,e.x,e.y); e.ccd=1; } e.ccd-=dt;
    if(e.ct<=0) e.state="idle"; return; }
  if(e.wind>0){ e.wind-=dt;
    if(e.wind<=0){
      if(e.atk==="ring"){
        const n=e.phase>=2?12:8;
        for(let i=0;i<n;i++){ const a=(Math.PI*2/n)*i+e.t*0.4;
          spawnPrj({x:e.x,y:e.y,vx:Math.cos(a)*200,vy:Math.sin(a)*200,from:"en",kind:"sbolt",dmg:1,r:7,life:2.6,c:"#c8632a"}); }
        A.sfx.enemyShoot(); A.sfx.roar();
      } else if(e.atk==="charge"){ e.state="charge"; e.ct=.6; e.ccd=0;
        e.cvx=Math.cos(ang)*460; e.cvy=Math.sin(ang)*460; A.sfx.roar();
      } else if(e.atk==="slam"){
        spawnPrj({x:e.x,y:e.y,from:"en",kind:"ring",r:12,grow:340,max:180,dmg:2,life:.8,c:"#ffd98a",hit:false});
        G.shake=12; A.sfx.roar();
      }
    }
    return; }
  if(e.cd<=0){
    const rate=e.phase>=2?1.7:2.5;
    e.cd=rate;
    const roll=Math.random();
    e.atk=roll<0.45?"ring":(roll<0.8?"charge":"slam");
    e.wind=e.atk==="slam"?.5:.35;
    if(e.summonT===undefined) e.summonT=0;
    e.summonT+=rate;
    if(e.summonT>8){ e.summonT=0;
      const alive=G.room.foes.filter(f=>f.hp>0&&f!==e).length;
      if(alive<2){ const s=spawnEnemy("husk",e.x+rnd(-70,70),e.y+rnd(-70,70),null); G.room.foes.push(s);
        burst(s.x,s.y,"#ff8a3c",14,200,{life:.5}); A.sfx.roar(); } }
  }
}
function bossShieldUp(){ return G.roomId==="z6b" && !flag("bossDead") && !(flag("swB1")&&flag("swB2")&&flag("swB3")); }
function shieldPing(e){ A.sfx.block(); burst(e.x,e.y,"#c8632a",8,140,{life:.3});
  floatTxt(e.x,e.y-e.r-8,"SHIELDED","#ff9a5a"); }
function bossDefeated(){
  flag("bossDead",true); A.sfx.roar(); A.sfx.beacon(); G.shake=20;
  burst(P.x,P.y,"#ffd98a",40,380,{life:1.2});
  const e=G.room.foes.find(f=>f.type==="regent");
  if(e){ burst(e.x,e.y,"#ffd98a",50,420,{life:1.4}); burst(e.x,e.y,"#2a2622",30,260,{life:1,glow:false,g:200}); }
  toast("The Corroded Regent unmade","Black-iron flakes away from the Core Beacon. Go to it — press E — and end the vigil.");
  shimmer(2);
}

/* ============================ PUZZLES & WORLD ============================ */
const RMAP={o0:{E:"N",N:"E",S:"W",W:"S"}, o1:{E:"S",S:"E",N:"W",W:"N"}};
function blockedTile(x,y){
  const px=x*T+T/2, py=y*T+T/2;
  const B=G.room.solid.concat(dynBlockers(G.room));
  for(const q of B){ if(px>q.x&&px<q.x+q.w&&py>q.y&&py<q.y+q.h) return true; }
  for(const o of G.room.objs){ if(o.dead) continue;
    const rs=o.rects||[o.rect];
    for(const q of rs){ if(px>q.x&&px<q.x+q.w&&py>q.y&&py<q.y+q.h) return true; } }
  return false;
}
function lightBeam(){
  const R=G.room, em=R.feats.find(f=>f.t==="emitter"); if(!em) return null;
  const dirs={E:[1,0],W:[-1,0],N:[0,-1],S:[0,1]};
  let x=em.x, y=em.y, dir=em.dir, ok=false;
  const pts=[[x,y]];
  const mirrors=R.feats.filter(f=>f.t==="mirror"), recv=R.feats.find(f=>f.t==="receiver");
  for(let s=0;s<80;s++){
    const d=dirs[dir]; x+=d[0]; y+=d[1];
    if(x<0||y<0||x>=COLS||y>=ROWS) break;
    pts.push([x,y]);
    const m=mirrors.find(mm=>mm.x===x&&mm.y===y);
    if(m){ dir=RMAP["o"+m.o][dir]; continue; }
    if(recv&&recv.x===x&&recv.y===y){ ok=true; break; }
    if(blockedTile(x,y)) break;
  }
  return {pts,ok};
}
function updatePlates(dt){
  const R=G.room; if(!R.plates.length) return;
  for(const p of R.plates){
    const pr={x:p.rect.x-2,y:p.rect.y-2,w:p.rect.w+4,h:p.rect.h+4};
    let on=hitRect(P.x,P.y,P.r,pr)!==null;
    if(!on) for(const e of R.foes){ if(e.hp>0&&hitRect(e.x,e.y,e.r,pr)){ on=true; break; } }
    p.was=p.on; p.on=on;
    if(on&&!p.was){ A.sfx.plate(); burst(p.px||p.rect.x+T/2,p.rect.y+T*0.7,"#b08a4c",8,120,{life:.35,glow:false}); }
  }
  const sq=R.d.seq; if(!sq||flag(sq.gate)) return;
  const both=sq.plates.every(id=>R.plates.find(p=>p.id===id).on);
  if(both){ openGate(sq.gate,"Both plates held — the grate rises."); return; }
  for(const p of R.plates){
    if(p.on&&!p.was){
      if(G.seqT>0&&G.seqLast&&G.seqLast!==p.id){ openGate(sq.gate,"Pressed in one breath — the grate rises."); return; }
      G.seqLast=p.id; G.seqT=sq.window;
    }
  }
  G.seqT=Math.max(0,G.seqT-dt);
}
function openGate(f,msg){ if(flag(f)) return; flag(f,true); A.sfx.gate(); G.shake=8; toast("Mechanism answered",msg); }
function updateVents(dt){
  const d=G.room.d; if(!d.vents) return;
  for(const v of d.vents){ const r=rr(v[0],v[1],v[2],v[3]);
    v._act=((G.t+v[4])%2.4)<1.0;
    if(v._act){
      if(Math.random()<0.5) part({x:rnd(r.x,r.x+r.w),y:r.y+r.h*0.5,vx:rnd(-14,14),vy:rnd(-70,-30),
        c:"#c8d2d6",r:rnd(2,5),life:rnd(.4,.8),max:.8,g:-30,glow:false,drag:.97});
      if(hitRect(P.x,P.y,P.r-3,[r])) damagePlayer(2,r.x+r.w/2,r.y,true);
    }
  }
}
function updateLight(){
  const R=G.room; if(!R.feats.some(f=>f.t==="emitter")) { G.beam=null; return; }
  G.beam=lightBeam();
  if(G.beam&&G.beam.ok&&!flag("lightOn")){
    flag("lightOn",true); flag("g4",true);
    A.sfx.save(); A.sfx.gate(); shimmer(1.6);
    burst(tx(8),tx(6),"#bfe9ff",30,260,{life:.9});
    toast("Light-path restored","The beam finds its receiver. The Phantom Lantern lifts from its plinth — and the eastern grate rises.");
  }
}

/* ============================ INTERACTION ============================ */
function featAvailable(f){
  if(f.t==="chest"&&f.lantern&&G.worn!=="lantern") return false;
  return true;
}
function nearestFeat(){
  let best=null,bd=60;
  for(const f of G.room.feats){
    if(!featAvailable(f)) continue;
    if(f.t==="emitter") continue;
    const d=Math.hypot(P.x-f.px,P.y-(f.py+12));
    if(d<bd){ bd=d; best=f; }
  }
  return best;
}
function promptFor(f){
  if(!f) return "";
  const used=(f.id&&(flag(f.id)&&f.t!=="switch"))||(f.t==="altar"&&flag(f.id));
  switch(f.t){
    case "stone": return flag("s"+f.id)?"Rest at the Sanctuary Stone":"Touch the Sanctuary Stone";
    case "shop": return "Open the Merchant Altar";
    case "altar": return used?"The altar is cold":"Lay the blade upon the altar";
    case "chest": return used?"Empty coffer":"Open the coffer";
    case "pedestal": return used?"Empty plinth":"Take the offering";
    case "cistern": return flag("cisternUsed")?"The cistern is dry":"Drink from the Cistern";
    case "mirror": return "Turn the mirror";
    case "sign": return "Read the inscription";
    case "beacon": return flag("bossDead")?"Rekindle the Core Beacon":"The Beacon is bound";
  }
  return "";
}
function interact(){
  const f=nearestFeat(); if(!f) return;
  const used=f.id?flag(f.id):false;
  switch(f.t){
    case "stone": {
      if(f.needClear&&G.room.foes.some(e=>e.hp>0)){ toast("Sanctuary Stone","A corrosive hymn binds the hall. Silence the constructs first."); A.sfx.deny(); return; }
      G.hp=maxHp(); flag("cisternUsed",false);
      G.stoneRoom=G.roomId; G.stonePos={x:P.x,y:P.y};
      flag("stone_"+f.id,true);
      A.sfx.save(); shimmer(1.4); motes(f.px,f.py,"#ffd98a",22); auraPulse();
      saveGame();
      toast("Progress sealed","Lifeblood restored, the vigil written into the stone. You will wake here if the Corrosion takes you.");
      break; }
    case "shop": {
      if(f.needClear&&G.room.foes.some(e=>e.hp>0)){ toast("Merchant Altar","Bronze will not bargain over noise. Silence the constructs."); A.sfx.deny(); return; }
      openShop();
      break; }
    case "altar": {
      if(used){ toast("Spent altar","Cold coals. It gave what it held."); return; }
      flag(f.id,true);
      give({weapon:f.up},f.px,f.py);
      toast(f.up===1?"IGNITED EDGE":"SOLAR FALCHION",
        f.up===1?"The Ember Blade drinks the altar-fire. Faster strokes, ember-trail on every swing, and stone cracks twice as easily."
                :"The blade widens into solar gold. A slow wind-up, a far heavier fall, a wider arc — and hairline walls shatter at a touch.");
      break; }
    case "chest": {
      if(used){ toast("Empty coffer","Only dust and older dust."); return; }
      flag(f.id,true); A.sfx.buy(); A.sfx.pickup("rare");
      burst(f.px,f.py,"#ffd98a",18,200,{life:.7});
      give(f.loot,f.px,f.py);
      break; }
    case "pedestal": {
      if(f.needFlag&&!flag(f.needFlag)){ toast("Sealed plinth","The mechanism above it is unlit."); A.sfx.deny(); return; }
      if(used){ toast("Empty plinth","It remembers what it gave."); return; }
      flag(f.id,true); A.sfx.pickup("rare");
      burst(f.px,f.py,"#ffffff",20,220,{life:.8});
      if(f.loot.heart&&flag("shopHeart")){ give({gold:150},f.px,f.py);
        toast("The vault yields sigils","You already carry the merchant’s fifth container — so the plinth pays 150 Golden Sigils instead."); }
      else give(f.loot,f.px,f.py);
      break; }
    case "cistern": {
      if(flag("cisternUsed")){ toast("Cistern of Quiet Water","Dry as bone. It will fill again when you rest at a Sanctuary Stone."); return; }
      flag("cisternUsed",true); G.hp=maxHp(); A.sfx.potion(); healFlash(); motes(f.px,f.py,"#b8ecbf",18);
      toast("Cistern of Quiet Water","Still water knits flesh. Lifeblood restored to full.");
      break; }
    case "mirror": {
      f.o=1-f.o; A.sfx.block(); burst(f.px,f.py,"#bfe9ff",10,140,{life:.4});
      break; }
    case "sign": { A.sfx.pickup("common"); toast("Inscription",f.text); break; }
    case "beacon": {
      if(!flag("bossDead")){ toast("The Core Beacon","Black-iron coils bind it. The Corroded Regent holds the last knot."); A.sfx.deny(); return; }
      endGame();
      break; }
  }
  updateHUD();
}

/* ============================ ROOM FLOW ============================ */
function goRoom(id,from){
  G.roomId=id; G.room=buildRoom(id);
  G.prj.length=0; G.seqT=0; G.seqLast=null;
  const gp=G.room.gap;
  if(from==="E"){ P.x=3*T; P.y=(gp.W[0]+gp.W[1]+1)/2*T; }
  else if(from==="W"){ P.x=VW-3*T; P.y=(gp.E[0]+gp.E[1]+1)/2*T; }
  else if(from==="N"){ P.y=3*T; P.x=(gp.S[0]+gp.S[1]+1)/2*T; }
  else if(from==="S"){ P.y=VH-3*T; P.x=(gp.N[0]+gp.N[1]+1)/2*T; }
  else { P.x=tx(3); P.y=tx(8); }
  P.iframes=Math.max(P.iframes,.5);
  A.amb(G.room.d.amb);
  const z=ZONE_TITLE[G.room.d.zone];
  showZone(z[0],z[1],G.room.d.sub);
  updateHUD();
  if(id==="z6b"&&!flag("bossDead")){ A.sfx.roar();
    toast("The Corroded Regent","Its shield is corrosion-made. Three crystal switches ring the pit — the Aether Shard reaches all of them."); }
}
function checkTransitions(){
  const d=G.room.d, gp=G.room.gap;
  const inGap=(lo,hi,v)=>v>lo*T-14&&v<(hi+1)*T+14;
  if(P.x>930&&d.ex.E&&inGap(gp.E[0],gp.E[1],P.y)) goRoom(d.ex.E,"E");
  else if(P.x<30&&d.ex.W&&inGap(gp.W[0],gp.W[1],P.y)) goRoom(d.ex.W,"W");
  else if(P.y<30&&d.ex.N&&inGap(gp.N[0],gp.N[1],P.x)) goRoom(d.ex.N,"N");
  else if(P.y>570&&d.ex.S&&inGap(gp.S[0],gp.S[1],P.x)) goRoom(d.ex.S,"S");
}

/* ============================ PLAYER UPDATE ============================ */
const KEY={};
const M={x:480,y:300,moved:false,down:false,rdown:false};
function updatePlayer(dt){
  if(G.dead){ G.deadT+=dt; return; }
  P.iframes=Math.max(0,P.iframes-dt); P.hurtT=Math.max(0,P.hurtT-dt);
  P.atkT=Math.max(0,P.atkT-dt); P.dashCd=Math.max(0,P.dashCd-dt);
  let ax=(KEY.d||KEY.arrowright?1:0)-(KEY.a||KEY.arrowleft?1:0);
  let ay=(KEY.s||KEY.arrowdown?1:0)-(KEY.w||KEY.arrowup?1:0);
  const L=Math.hypot(ax,ay)||1; ax/=L; ay/=L;
  P.moving=(ax!==0||ay!==0);
  if(P.dashT>0){ P.dashT-=dt; moveEnt(P,Math.cos(P.dashDir)*520*dt,Math.sin(P.dashDir)*520*dt);
    part({x:P.x,y:P.y,vx:rnd(-20,20),vy:rnd(-20,20),c:"#8fb6d8",r:rnd(1,2.5),life:.3,max:.3,g:0,drag:.9}); }
  else {
    const sp=182;
    if(P.moving) moveEnt(P,ax*sp*dt,ay*sp*dt);
  }
  if(P.moving){
    P.walk+=dt*9;
    P.stepT+=dt*182;
    if(P.stepT>30){ P.stepT=0; A.sfx.step(G.room.d.amb);
      part({x:P.x+rnd(-4,4),y:P.y+14,vx:0,vy:-6,c:"#6d7d8b",r:1.6,life:.35,max:.35,g:-6,glow:false}); }
    if(G.weapon>0&&Math.random()<dt*22) part({x:P.x+rnd(-6,6),y:P.y+rnd(0,14),vx:rnd(-8,8),vy:rnd(-24,-8),
      c:G.weapon===2?"#ffd98a":"#ff8a3c",r:rnd(1,2),life:rnd(.3,.6),max:.6,g:-6,drag:.97});
  }
  if(M.moved) P.face=Math.atan2(M.y-P.y,M.x-P.x);
  else if(P.moving) P.face=Math.atan2(ay,ax);
  if(M.down) meleeAttack();
  if(P.swing>0){ P.swing-=dt;
    if(!P.swingHit&&P.swing<P.swingMax-0.09){ P.swingHit=true; doMeleeHit(); } }
  // shard regrowth
  if(G.hasShard){ G.ammoT+=dt;
    if(G.ammoT>5){ G.ammoT=0; if(G.ammo<8){ G.ammo++; A.sfx.pulse(); motes(P.x+12,P.y-6,"#bfe9ff",3); updateHUD(); } } }
  checkTransitions();
}
function dash(){
  if(P.dashCd>0||G.dead||G.menu) return;
  let dx=(KEY.d||KEY.arrowright?1:0)-(KEY.a||KEY.arrowleft?1:0);
  let dy=(KEY.s||KEY.arrowdown?1:0)-(KEY.w||KEY.arrowup?1:0);
  if(!dx&&!dy){ dx=Math.cos(P.face); dy=Math.sin(P.face); }
  P.dashDir=Math.atan2(dy,dx); P.dashT=.16; P.dashCd=.9; P.iframes=Math.max(P.iframes,.3);
  A.sfx.step("spire"); burst(P.x,P.y,"#8fb6d8",8,120,{life:.3,g:0});
}

/* ============================ SAVE / LOAD ============================ */
const SAVE_KEY="warden_corrosion_save_v1";
function saveGame(){
  const data={gold:G.gold,items:G.items,acc:G.acc,worn:G.worn,weapon:G.weapon,hasShard:G.hasShard,ammo:G.ammo,
    containers:G.containers,hp:G.hp,flags:G.flags,kills:G.kills,earned:G.earned,
    stoneRoom:G.stoneRoom,stonePos:G.stonePos,roomId:G.roomId,pos:{x:P.x,y:P.y},runT:G.runT};
  try{ localStorage.setItem(SAVE_KEY,JSON.stringify(data)); return true; }catch(e){ G.memSave=data; return false; }
}
function loadSave(){
  try{ const s=localStorage.getItem(SAVE_KEY); return s?JSON.parse(s):G.memSave||null; }catch(e){ return G.memSave||null; }
}
function hasSave(){ return !!loadSave(); }
function applySave(d){
  G.gold=d.gold; G.items=d.items; G.acc=d.acc; G.worn=d.worn; G.weapon=d.weapon;
  G.hasShard=d.hasShard; G.ammo=d.ammo; G.containers=d.containers; G.hp=d.hp;
  G.flags=d.flags; G.kills=d.kills; G.earned=d.earned; G.runT=d.runT||0;
  G.stoneRoom=d.stoneRoom; G.stonePos=d.stonePos;
  G.dead=false; G.ending=false;
  goRoom(d.roomId||d.stoneRoom,null);
  P.x=d.pos?d.pos.x:G.stonePos.x; P.y=d.pos?d.pos.y:G.stonePos.y;
}
function newGame(){
  G.gold=0; G.items={stem:1,seal:0}; G.acc={band:0,lantern:0,cloak:0}; G.worn=null;
  G.weapon=0; G.hasShard=false; G.ammo=0; G.containers=3; G.hp=6; G.flags={}; G.kills=0; G.earned=0; G.runT=0;
  G.stoneRoom="z1a"; G.stonePos={x:tx(3),y:tx(7)+38};
  G.dead=false; G.ending=false;
  goRoom("z1a",null);
  P.x=tx(7); P.y=tx(8);
  toast("The Ashen Threshold","You are the Last Warden. Touch the Sanctuary Stone to seal your vigil, then take the eastern walk.");
}
function respawn(){
  G.dead=false; G.hp=maxHp();
  goRoom(G.stoneRoom,null);
  P.x=G.stonePos.x; P.y=G.stonePos.y;
  hideDeath();
  toast("The stone takes you back","Lifeblood restored. What you carry, you keep.");
}

/* ============================ MAIN UPDATE ============================ */
function update(dt){
  G.t+=dt; G.runT+=dt;
  if(G.state!=="play"||G.menu) { updateParticles(dt); return; }
  if(G.hitstop>0){ G.hitstop-=dt; return; }
  updatePlayer(dt);
  updateFoes(dt);
  updatePrj(dt);
  updatePlates(dt);
  updateVents(dt);
  updateLight();
  updateParticles(dt);
  G.shake=Math.max(0,G.shake-dt*26);
  // shop drone ambience
  const shop=G.room.feats.find(f=>f.t==="shop");
  A.shopDrone(!!shop&&dist({x:shop.px,y:shop.py},P)<170&&(!shop.needClear||!G.room.foes.some(e=>e.hp>0)));
  // prompt
  const f=nearestFeat();
  setPrompt(f?promptFor(f):"");
}