← Warden results

Warden

muse spark 1.2 contributor · 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 58,090 bytes · SHA-256 6759190654b2
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>The Warden's Corrosion — Aurelion's Sunken Observatory</title>
<style>
  @import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=JetBrains+Mono:wght@400;600&family=Crimson+Pro:ital,wght@0,400;0,600;1,400&display=swap');
  *{margin:0;padding:0;box-sizing:border-box}
  :root{--gold:#d4a24e;--gold2:#f0c27a;--ember:#ff6b2b;--slate:#1a2332;--slate2:#24344d;--slate3:#2f4a6b;--rust:#8b4a2a;--moss:#5a6b4a;--paper:#e8ddd0;--danger:#c0392b}
  body{background:#0d1117;color:var(--paper);font-family:'Crimson Pro',serif;overflow-x:hidden;min-height:100vh}
  /* header */
  .topbar{height:56px;background:linear-gradient(180deg,#1a2332,#121a27);border-bottom:2px solid #d4a24e33;display:flex;align-items:center;justify-content:space-between;padding:0 18px;position:sticky;top:0;z-index:20}
  .brand{display:flex;align-items:center;gap:12px}
  .sigil{width:36px;height:36px;border:1.5px solid var(--gold);border-radius:50%;display:grid;place-items:center;background:radial-gradient(circle at 30% 30%, #2a3a52, #0f1724);color:var(--gold);font-family:'Cinzel',serif;font-weight:700;font-size:14px;box-shadow:0 0 12px #d4a24e55}
  .brand h1{font-family:'Cinzel',serif;font-size:15px;letter-spacing:0.18em;font-weight:700;color:var(--gold2)}
  .brand p{font-size:11px;letter-spacing:0.12em;opacity:0.6;margin-top:1px;font-family:'JetBrains Mono',monospace}
  .hud{display:flex;align-items:center;gap:18px}
  .hearts{display:flex;gap:6px}
  .heart{width:26px;height:26px;position:relative;display:grid;place-items:center}
  .heart svg{width:24px;height:24px;filter:drop-shadow(0 1px 4px #0008)}
  .heart .half{position:absolute;inset:0;display:flex}
  .heart .half span{flex:1}
  .goldHUD{display:flex;align-items:center;gap:8px;background:#d4a24e14;border:1px solid #d4a24e33;padding:6px 12px;border-radius:20px;font-family:'JetBrains Mono',monospace;font-size:13px}
  .goldHUD b{color:var(--gold2)}
  .zoneBadge{font-family:'JetBrains Mono',monospace;font-size:10px;letter-spacing:0.14em;background:#0f1724;border:1px solid #ffffff14;padding:6px 10px;border-radius:20px}
  /* layout */
  .wrap{max-width:1280px;margin:0 auto;display:grid;grid-template-columns:1fr 340px;gap:16px;padding:16px;align-items:stretch}
  @media(max-width:980px){.wrap{grid-template-columns:1fr}}
  .stage{position:relative;background:linear-gradient(180deg,#16202f 0%, #0f1724 100%);border:1px solid #d4a24e22;border-radius:16px;overflow:hidden;box-shadow:0 20px 60px #0008;display:flex;flex-direction:column;align-self:start}
  .stageTop{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;border-bottom:1px solid #ffffff0d;background:linear-gradient(90deg,#1a2332cc,#121a27cc);backdrop-filter:blur(6px)}
  .zoneTitle{font-family:'Cinzel',serif;font-size:13px;letter-spacing:0.16em;color:var(--gold2)}
  .zoneDesc{font-size:12px;opacity:0.6;margin-top:2px;max-width:520px;line-height:1.3}
  .progress{display:flex;gap:6px;align-items:center}
  .dot{width:28px;height:6px;border-radius:10px;background:#ffffff14;border:1px solid #ffffff0d;position:relative;overflow:hidden}
  .dot.active{background:linear-gradient(90deg,var(--gold),var(--ember));box-shadow:0 0 8px #d4a24e88}
  .dot.done{background:var(--gold)}
  canvas{display:block;width:100%;height:auto;image-rendering:auto;background:radial-gradient(800px 400px at 50% 0%, #1e2f4a 0%, #0f1724 60%, #0a0f1a 100%)}
  .controls{position:absolute;bottom:10px;left:50%;transform:translateX(-50%);display:flex;gap:8px;align-items:center;background:#0a0f1acc;border:1px solid #ffffff14;padding:8px 12px;border-radius:999px;backdrop-filter:blur(8px);font-family:'JetBrains Mono',monospace;font-size:11px;opacity:0.95}
  .controls kbd{background:#1a2332;border:1px solid #ffffff1a;border-bottom-width:2px;padding:2px 6px;border-radius:6px;font-size:11px;color:var(--gold2)}
  .btn{appearance:none;border:1px solid #d4a24e55;background:linear-gradient(180deg,#d4a24e,#b67f2a);color:#1a1206;font-family:'Cinzel',serif;font-weight:700;letter-spacing:0.08em;font-size:12px;padding:10px 14px;border-radius:10px;cursor:pointer;box-shadow:0 4px 16px #0006;transition:.15s}
  .btn:hover{filter:brightness(1.08);transform:translateY(-1px)}
  .btn:active{transform:translateY(0)}
  .btn.ghost{background:#ffffff0a;color:var(--paper);border-color:#ffffff1a;box-shadow:none}
  .btn.small{padding:6px 10px;font-size:11px}
  .btn:disabled{opacity:.4;cursor:not-allowed;transform:none}
  /* side */
  .side{display:flex;flex-direction:column;gap:16px}
  .card{background:linear-gradient(180deg,#1a2332ee,#121a27ee);border:1px solid #d4a24e18;border-radius:16px;overflow:hidden;box-shadow:0 10px 30px #0005}
  .cardHead{padding:12px 14px;border-bottom:1px solid #ffffff0d;display:flex;align-items:center;justify-content:space-between}
  .cardHead h3{font-family:'Cinzel',serif;font-size:11px;letter-spacing:0.16em;color:var(--gold2)}
  .tabs{display:flex;gap:6px;padding:10px 10px 0}
  .tab{flex:1;padding:8px;border-radius:10px;border:1px solid #ffffff0d;background:#ffffff06;font-family:'JetBrains Mono',monospace;font-size:11px;letter-spacing:0.06em;cursor:pointer;color:#ffffff90;text-align:center}
  .tab.active{background:linear-gradient(180deg,#d4a24e22,#d4a24e0a);border-color:#d4a24e33;color:var(--gold2)}
  .panel{padding:12px}
  .invGrid{display:grid;grid-template-columns:1fr 1fr;gap:8px}
  .item{border:1px solid #ffffff0d;background:#0f1724;border-radius:12px;padding:10px;display:flex;gap:10px;align-items:center;position:relative;overflow:hidden}
  .item.has{border-color:#d4a24e33;background:linear-gradient(180deg,#1e2f4a44,#0f172444)}
  .item.equipped{border-color:var(--gold);box-shadow:0 0 0 1px #d4a24e55, 0 0 12px #d4a24e33}
  .ico{width:36px;height:36px;border-radius:10px;display:grid;place-items:center;font-size:18px;background:#0a0f1a;border:1px solid #ffffff0d;flex-shrink:0}
  .item h4{font-family:'Cinzel',serif;font-size:11px;letter-spacing:0.06em}
  .item p{font-size:11px;opacity:.65;line-height:1.3;margin-top:2px}
  .item .tag{position:absolute;top:6px;right:6px;font-family:'JetBrains Mono',monospace;font-size:9px;letter-spacing:0.08em;background:var(--gold);color:#1a1206;padding:2px 6px;border-radius:20px;font-weight:700}
  .shopList{display:flex;flex-direction:column;gap:8px}
  .shopRow{display:flex;align-items:center;justify-content:space-between;gap:10px;background:#0f1724;border:1px solid #ffffff0d;border-radius:12px;padding:10px}
  .shopRow b{font-family:'JetBrains Mono',monospace;font-size:12px;color:var(--gold2)}
  .log{height:132px;overflow:auto;background:#0a0f1a;border:1px solid #ffffff0d;border-radius:12px;padding:10px;font-family:'JetBrains Mono',monospace;font-size:11px;line-height:1.5}
  .log div{opacity:.85;border-left:2px solid transparent;padding-left:8px;margin:4px 0}
  .log .gold{border-color:var(--gold);color:var(--gold2)}
  .log .heal{border-color:#4ade80;color:#86efac}
  .log .danger{border-color:var(--danger);color:#fca5a5}
  .log .lore{border-color:#60a5fa;color:#93c5fd;font-style:italic}
  /* overlays */
  .overlay{position:absolute;inset:0;display:none;place-items:center;background:#060a12cc;backdrop-filter:blur(6px);padding:20px;z-index:5}
  .overlay.show{display:grid}
  .modal{width:min(520px,100%);background:linear-gradient(180deg,#1e2f4a,#121a27);border:1px solid #d4a24e33;border-radius:16px;padding:18px;box-shadow:0 20px 60px #000a;text-align:center}
  .modal h2{font-family:'Cinzel',serif;letter-spacing:0.12em;color:var(--gold2);font-size:18px}
  .modal p{opacity:.75;line-height:1.6;margin:8px 0 14px;font-size:14px}
  .altarGlow{width:100%;height:4px;background:linear-gradient(90deg,transparent,var(--gold),transparent);opacity:.6;margin:10px 0;border-radius:10px}
  /* flash */
  .flash{position:absolute;inset:0;pointer-events:none;opacity:0;transition:.2s;background:radial-gradient(circle at 50% 50%, #ff3b3022, transparent 70%)}
  .flash.hit{opacity:1}
  .shimmer{position:absolute;inset:0;pointer-events:none;opacity:0;border:3px solid var(--gold);border-radius:16px;box-shadow:inset 0 0 30px #d4a24e66;transition:.3s}
  .shimmer.on{opacity:1}
  /* lore strip */
  .loreStrip{margin:0 16px 16px;max-width:1280px;margin-left:auto;margin-right:auto;background:linear-gradient(90deg,#1a2332,#16202f);border:1px solid #d4a24e14;border-radius:12px;padding:10px 14px;display:flex;gap:12px;align-items:center;font-size:12px;line-height:1.5}
  .loreStrip strong{font-family:'Cinzel',serif;color:var(--gold2);letter-spacing:0.08em;font-size:11px;white-space:nowrap}
</style>
</head>
<body>
<header class="topbar">
  <div class="brand">
    <div class="sigil">◈</div>
    <div>
      <h1>THE WARDEN'S CORROSION</h1>
      <p>AURELION'S SUNKEN OBSERVATORY — LAST WARDEN PROTOCOL</p>
    </div>
  </div>
  <div class="hud">
    <div class="hearts" id="hearts"></div>
    <div class="goldHUD">◈ <b id="gold">0</b> <span style="opacity:.6">Sigils</span></div>
    <div class="zoneBadge" id="zoneBadge">ZONE 1 / 6</div>
  </div>
</header>

<div class="wrap">
  <div class="stage" id="stage">
    <div class="stageTop">
      <div>
        <div class="zoneTitle" id="zoneTitle">I — THE ASHEN THRESHOLD</div>
        <div class="zoneDesc" id="zoneDesc">Prologue. Learn movement and the Ember Blade. Activate the first Sanctuary Stone.</div>
      </div>
      <div class="progress" id="progress"></div>
    </div>
    <div style="position:relative">
      <canvas id="c" width="960" height="540"></canvas>
      <div class="flash" id="flash"></div>
      <div class="shimmer" id="shimmer"></div>
      <div class="controls">
        <span><kbd>WASD</kbd> Move</span>
        <span><kbd>SPACE</kbd> Ember Blade</span>
        <span><kbd>F</kbd> Interact</span>
        <span><kbd>Q</kbd> Aether Shard</span>
        <span><kbd>1-3</kbd> Consumable</span>
      </div>
      <!-- overlays -->
      <div class="overlay" id="altarOverlay">
        <div class="modal">
          <div style="font-size:28px">⚱️</div>
          <h2 id="altarTitle">IGNITED EDGE ALTAR</h2>
          <div class="altarGlow"></div>
          <p id="altarText">Ancient bronze altar hums with trapped sunlight. The blade remembers fire.</p>
          <div style="display:flex;gap:10px;justify-content:center">
            <button class="btn" id="altarClaim">Claim Upgrade</button>
            <button class="btn ghost" id="altarClose">Leave</button>
          </div>
        </div>
      </div>
      <div class="overlay" id="beaconOverlay">
        <div class="modal">
          <div style="font-size:32px">☀️</div>
          <h2>CORE BEACON REIGNITED</h2>
          <div class="altarGlow"></div>
          <p>The Corrosion peels back like burnt paper. Gears turn. Light floods the valley below. The Warden's vigil ends — the archive breathes again.</p>
          <p style="font-family:'JetBrains Mono',monospace;font-size:11px;opacity:.6" id="finalStats"></p>
          <button class="btn" onclick="location.reload()">Begin New Vigil</button>
        </div>
      </div>
    </div>
    <div style="display:flex;gap:8px;padding:10px;background:#0a0f1a;border-top:1px solid #ffffff0d;flex-wrap:wrap">
      <button class="btn small" id="btnSave">◈ Activate Sanctuary Stone (F)</button>
      <button class="btn small ghost" id="btnPrev">← Prev Zone</button>
      <button class="btn small ghost" id="btnNext">Next Zone →</button>
      <span style="margin-left:auto;font-family:'JetBrains Mono',monospace;font-size:11px;opacity:.6;display:flex;align-items:center;gap:8px">
        <span id="weaponLabel">Ember Blade — Base</span>
        <span style="width:8px;height:8px;border-radius:50%;background:var(--ember);box-shadow:0 0 8px var(--ember)"></span>
        <span id="aetherLabel">Aether: —</span>
      </span>
    </div>
    <div style="background:#0f1724;border-top:1px solid #ffffff0d;padding:10px 14px;display:flex;gap:10px;align-items:center;flex-wrap:wrap;font-family:'JetBrains Mono',monospace;font-size:10px;opacity:0.7">
      <span>◈ Sigils: <b style="color:var(--gold2)" id="statGold">0</b></span>
      <span>·</span>
      <span>Kills: <b style="color:var(--gold2)" id="statKills">0</b></span>
      <span>·</span>
      <span>Vitality: <b style="color:var(--gold2)" id="statHearts">3 / 5</b></span>
      <span style="margin-left:auto;opacity:0.6">WASD move · SPACE swing · Q Aether · F interact · Break cracked walls</span>
    </div>
  </div>

  <div class="side">
    <div class="card">
      <div class="cardHead"><h3>WARDEN'S KIT</h3><span style="font-family:'JetBrains Mono',monospace;font-size:10px;opacity:.6" id="kitHint">Click to equip</span></div>
      <div class="tabs">
        <div class="tab active" data-tab="inv">Inventory</div>
        <div class="tab" data-tab="shop">Merchant Altar</div>
        <div class="tab" data-tab="log">Chronicle</div>
      </div>
      <div class="panel" id="panelInv">
        <div class="invGrid" id="invGrid"></div>
        <div style="margin-top:10px;display:flex;gap:8px">
          <button class="btn small ghost" id="useVital">Use Vital Stem (1)</button>
          <button class="btn small ghost" id="useSeal">Use Binding Seal (2)</button>
        </div>
        <div style="margin-top:10px;background:#0a0f1a;border:1px solid #ffffff0d;border-radius:10px;padding:10px;font-size:12px;line-height:1.5">
          <b style="font-family:'Cinzel',serif;letter-spacing:.08em;color:var(--gold2);font-size:11px">VISUAL PROGRESSION</b>
          <div id="visualDesc" style="opacity:.75;margin-top:4px">Cloak plain, blade dull. Ember trail absent.</div>
        </div>
      </div>
      <div class="panel" id="panelShop" style="display:none">
        <div style="font-size:12px;opacity:.7;margin-bottom:8px">Merchant Altar — <b style="color:var(--gold2)">The Rusted Archives</b> · Unlocked after first combat. Sigils persist across zones.</div>
        <div class="shopList" id="shopList"></div>
        <div style="margin-top:8px;font-family:'JetBrains Mono',monospace;font-size:11px;opacity:.6">Tip: Save 300 Sigils for the final Heart Container if you miss the ranged switch in the Spire.</div>
      </div>
      <div class="panel" id="panelLog" style="display:none">
        <div class="log" id="log"></div>
      </div>
    </div>

    <div class="card">
      <div class="cardHead"><h3>ZONE ATLAS</h3><span style="font-family:'JetBrains Mono',monospace;font-size:10px;opacity:.6" id="atlasProgress">1 / 6</span></div>
      <div class="panel" id="atlas" style="display:grid;gap:8px"></div>
    </div>
  </div>
</div>

<div class="loreStrip">
  <strong>WARDEN'S NOTE</strong>
  <span id="loreText">The Observatory floats still. Below, the storm never stops. The Corrosion eats bronze like frost. If the Beacon dies, the valley follows.</span>
</div>

<script>
const ZONES = [
  {id:1, name:"I — THE ASHEN THRESHOLD", short:"Ashen Threshold", desc:"Prologue. Movement, first combat, first Sanctuary Stone. Learn the blade's weight.", color:"#1b2a3a", task:"Defeat the Rusted Sentinel and activate the Sanctuary Stone."},
  {id:2, name:"II — THE RUSTED ARCHIVES", short:"Rusted Archives", desc:"Early. Hidden chests, first health cache, Shop unlocked after combat. Gold farming begins.", color:"#2a2f3a", task:"Find the Warden's Band and clear the archive constructs."},
  {id:3, name:"III — THE COLLAPSED GALLERY", short:"Collapsed Gallery", desc:"Mid-Early. Weapon Upgrade 1 altar, pressure-plate puzzle, cracked wall with Heart Container #4.", color:"#2e2a3a", task:"Claim Ignited Edge at the altar and break the cracked wall."},
  {id:4, name:"IV — THE SUNKEN VAULT", short:"Sunken Vault", desc:"Turning Point. Ranged Aether Shard hidden behind breakable wall. Light puzzle. Phantom Lantern.", color:"#1a2e3a", task:"Solve the light puzzle to reveal the Aether Shard."},
  {id:5, name:"V — THE TWIN SPIRE", short:"Twin Spire", desc:"Late. Weapon Upgrade 2 altar, gauntlet, Shadow Cloak. Heart #5 via ranged switch or shop.", color:"#2a1e2e", task:"Claim Solar Falchion and secure the final Heart Container."},
  {id:6, name:"VI — THE CORE CHAMBER", short:"Core Chamber", desc:"Climax. Use every tool — ranged switches, both upgrades, full health — to reignite the Beacon.", color:"#1e2e22", task:"Activate the Core Beacon. The Corrosion ends."},
];

const ITEMS = {
  band:{name:"Warden's Band", icon:"💍", type:"accessory", desc:"+15% attack speed. Silver ring glints on wrist.", zone:2},
  vital:{name:"Vital Stem", icon:"🌿", type:"consumable", desc:"Restores 4 HP (2 hearts). Green vital glow.", zone:2, price:50},
  lantern:{name:"Phantom Lantern", icon:"🏮", type:"accessory", desc:"Reveals hidden cracks & passages. Soft blue glow.", zone:4},
  cloak:{name:"Shadow Cloak", icon:"🧥", type:"accessory", desc:"-30% trap damage. Dark hem on cloak.", zone:5},
  seal:{name:"Binding Seal", icon:"🔷", type:"consumable", desc:"Freezes one enemy ~3s. Blue seal.", zone:5, price:75},
};

let state = {
  zone:1,
  gold:0,
  maxContainers:3,
  hp:6,
  weapon:0, //0 base,1 ignited,2 solar
  hasAether:false,
  aetherAmmo:0,
  inventory:{band:false, vital:1, lantern:false, cloak:false, seal:1},
  equipped:{band:false, lantern:false, cloak:false},
  shop:{vital:3, seal:2, heart:1, ammo:0},
  unlockedShop:false,
  found:{heart4:false, heart5:false, aether:false, altar1:false, altar2:false},
  savedZone:1, savedHP:6, savedGold:0,
  kills:0,
  discoveredWalls:new Set(),
};

const c = document.getElementById('c'), ctx=c.getContext('2d');
let W=960,H=540;
let player={x:140,y:270,r:14, vx:0,vy:0, facing:1, atk:0, atkTime:0, invuln:0, speed:3.2 };
let enemies=[], projectiles=[], particles=[], objects=[], plates=[], switches=[];
let keys={}, mouse={x:0,y:0,down:false};
let shake=0, shimmerTimer=0;
let audioCtx=null;

function ac(){ if(!audioCtx) audioCtx=new (window.AudioContext||window.webkitAudioContext)(); if(audioCtx.state==='suspended') audioCtx.resume(); }
function tone(freq, dur, type='sine', gain=0.18, slide=0){
  try{ ac(); const o=audioCtx.createOscillator(), g=audioCtx.createGain(); o.type=type; o.frequency.value=freq;
    if(slide){ o.frequency.linearRampToValueAtTime(freq+slide, audioCtx.currentTime+dur); }
    g.gain.value=gain; g.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime+dur);
    o.connect(g).connect(audioCtx.destination); o.start(); o.stop(audioCtx.currentTime+dur);
  }catch(e){}
}
function sfx(name){
  if(name==='swing0'){ tone(220,0.12,'square',0.14,120); tone(80,0.18,'triangle',0.12); }
  if(name==='swing1'){ tone(300,0.14,'square',0.15,80); tone(180,0.22,'sawtooth',0.1); tone(900,0.08,'sine',0.08); }
  if(name==='swing2'){ tone(160,0.28,'sawtooth',0.18,-40); tone(60,0.4,'triangle',0.14); setTimeout(()=>tone(1200,0.12,'sine',0.09),80); }
  if(name==='hit'){ tone(140,0.12,'square',0.16); tone(60,0.08,'triangle',0.12); }
  if(name==='hurt'){ tone(180,0.3,'sawtooth',0.18); tone(90,0.35,'square',0.12); }
  if(name==='pickCoin'){ tone(800,0.12,'sine',0.12,400); setTimeout(()=>tone(1100,0.12,'sine',0.1),70); }
  if(name==='pickRare'){ tone(500,0.2,'sine',0.14,300); setTimeout(()=>tone(700,0.2,'sine',0.12),120); setTimeout(()=>tone(1000,0.3,'sine',0.1),240); }
  if(name==='heal'){ tone(400,0.25,'sine',0.14,200); setTimeout(()=>tone(600,0.25,'sine',0.12),140); }
  if(name==='aether'){ tone(900,0.14,'sine',0.16,-300); tone(1400,0.08,'square',0.08); }
  if(name==='freeze'){ tone(300,0.4,'sine',0.14,500); }
  if(name==='save'){ tone(300,0.5,'sine',0.12,400); setTimeout(()=>tone(500,0.6,'sine',0.1),200); }
  if(name==='break'){ tone(120,0.3,'square',0.18); tone(200,0.2,'triangle',0.12); }
  if(name==='shop'){ tone(600,0.12,'sine',0.12,200); }
  if(name==='shimmer'){ tone(700,0.6,'sine',0.1,300); }
}

function log(msg, cls=''){
  const el=document.getElementById('log');
  const d=document.createElement('div'); d.className=cls; d.textContent=msg; el.prepend(d);
  while(el.children.length>40) el.lastChild.remove();
}

function updateHUD(){
  // hearts
  const heartsEl=document.getElementById('hearts'); heartsEl.innerHTML='';
  const maxHP = state.maxContainers*2;
  for(let i=0;i<state.maxContainers;i++){
    const hpForHeart = state.hp - i*2;
    let fill=0; if(hpForHeart>=2) fill=2; else if(hpForHeart===1) fill=1;
    const d=document.createElement('div'); d.className='heart';
    let col = fill===2 ? '#ff4d4d' : fill===1 ? '#ff8a8a' : '#2a3548';
    let stroke = fill>0 ? '#ff6b6b' : '#3a4a62';
    d.innerHTML=`<svg viewBox="0 0 24 24"><path d="M12 19 L4 11 A4 4 0 0 1 12 4 A4 4 0 0 1 20 11 Z" fill="${col}" stroke="${stroke}" stroke-width="1.2"/></svg>`;
    if(fill===1){
      d.innerHTML+=`<div style="position:absolute;inset:0;display:flex"><span style="flex:1;overflow:hidden"><svg viewBox="0 0 24 24" style="width:24px;height:24px"><path d="M12 19 L4 11 A4 4 0 0 1 12 4 L12 19 Z" fill="#ff4d4d"/></svg></span><span style="flex:1"></span></div>`;
    }
    // container outline if empty
    heartsEl.appendChild(d);
  }
  document.getElementById('gold').textContent=state.gold;
  document.getElementById('zoneBadge').textContent=`ZONE ${state.zone} / 6`;
  document.getElementById('atlasProgress').textContent=`${state.zone} / 6`;
  const wl = state.weapon===0?'Ember Blade — Base': state.weapon===1?'Ignited Edge — Fire':'Solar Falchion — Light';
  document.getElementById('weaponLabel').textContent=wl;
  document.getElementById('aetherLabel').textContent = state.hasAether? `Aether: ${state.aetherAmmo} charges` : 'Aether: —';
  const sg=document.getElementById('statGold'), sk=document.getElementById('statKills'), sh=document.getElementById('statHearts');
  if(sg) sg.textContent=state.gold;
  if(sk) sk.textContent=state.kills;
  if(sh) sh.textContent=`${state.maxContainers} / 5`;
  // visual desc
  let vd=[];
  if(state.weapon===0) vd.push('Blade dull, no trail.');
  if(state.weapon===1) vd.push('Blade glows orange + ember trail.');
  if(state.weapon===2) vd.push('Blade golden halo, wide stance.');
  if(state.hasAether) vd.push('Off-hand crystal pulsing.');
  if(state.equipped.band) vd.push('Silver ring on wrist.');
  if(state.equipped.lantern) vd.push('Lantern blue glow.');
  if(state.equipped.cloak) vd.push('Cloak dark hem.');
  document.getElementById('visualDesc').textContent = vd.join(' ') || 'Base cloak, unadorned stance.';
  // progress dots
  const prog=document.getElementById('progress'); prog.innerHTML='';
  for(let i=1;i<=6;i++){ const d=document.createElement('div'); d.className='dot'+(i===state.zone?' active': i<state.zone?' done':''); prog.appendChild(d); }
  // atlas
  const atlas=document.getElementById('atlas'); atlas.innerHTML='';
  ZONES.forEach(z=>{
    const row=document.createElement('div');
    row.style.cssText=`display:flex;gap:10px;align-items:center;padding:8px;border-radius:10px;border:1px solid ${state.zone===z.id?'#d4a24e44':'#ffffff0d'};background:${state.zone===z.id?'#d4a24e14':'#0a0f1a'}`;
    row.innerHTML=`<div style="width:28px;height:28px;border-radius:8px;display:grid;place-items:center;background:${z.color};border:1px solid #ffffff14;font-size:14px">${z.id===6?'☀️':z.id===1?'◈':'⬢'}</div>
      <div style="flex:1"><div style="font-family:'Cinzel',serif;font-size:11px;letter-spacing:.06em">${z.name}</div><div style="font-size:11px;opacity:.6">${z.task}</div></div>
      <div style="font-family:'JetBrains Mono',monospace;font-size:10px;opacity:.6">${state.zone>z.id?'✓':state.zone===z.id?'●':'○'}</div>`;
    row.style.cursor='pointer'; row.onclick=()=>{ if(z.id<=state.zone+1 || z.id < state.zone) goZone(z.id); };
    atlas.appendChild(row);
  });
  // zone header
  const z=ZONES[state.zone-1];
  document.getElementById('zoneTitle').textContent=z.name;
  document.getElementById('zoneDesc').textContent=z.desc;
  document.getElementById('loreText').textContent = loreForZone(state.zone);
  renderInventory(); renderShop();
}

function loreForZone(n){
  return [
    "The Observatory floats still. Below, the storm never stops. The Corrosion eats bronze like frost.",
    "Archives whisper. Gears half-turn, telescopes stare blind. Something silver waits in a chest that shouldn't be there.",
    "The Gallery collapsed when the Warden left. The altar still burns — if you can reach it, it will remember you.",
    "Water has claimed the Vault. Light bends wrong here. Follow the reflection, break the lie in the wall.",
    "Two spires, one shadow. The final altar waits above the gauntlet. The wind will try to take you.",
    "The Core Chamber. Every gear, every sigil, every scar — all of it was for this light."
  ][n-1];
}

function renderInventory(){
  const grid=document.getElementById('invGrid'); grid.innerHTML='';
  const defs=[
    {k:'band', name:ITEMS.band.name, icon:ITEMS.band.icon, desc:ITEMS.band.desc},
    {k:'vital', name:ITEMS.vital.name, icon:ITEMS.vital.icon, desc:ITEMS.vital.desc, count:state.inventory.vital},
    {k:'lantern', name:ITEMS.lantern.name, icon:ITEMS.lantern.icon, desc:ITEMS.lantern.desc},
    {k:'cloak', name:ITEMS.cloak.name, icon:ITEMS.cloak.icon, desc:ITEMS.cloak.desc},
    {k:'seal', name:ITEMS.seal.name, icon:ITEMS.seal.icon, desc:ITEMS.seal.desc, count:state.inventory.seal},
  ];
  defs.forEach(it=>{
    const has = it.count!==undefined ? it.count>0 : state.inventory[it.k];
    const equipped = state.equipped[it.k];
    const d=document.createElement('div'); d.className='item'+(has?' has':'')+(equipped?' equipped':'');
    d.innerHTML=`<div class="ico">${it.icon}</div><div style="flex:1"><h4>${it.name}</h4><p>${it.desc}</p></div>`;
    if(has && it.count!==undefined) d.innerHTML+=`<div class="tag">x${it.count}</div>`;
    else if(has && equipped) d.innerHTML+=`<div class="tag">EQUIPPED</div>`;
    else if(has) d.innerHTML+=`<div class="tag" style="background:#ffffff14;color:#fff;border:1px solid #ffffff1a">OWNED</div>`;
    if(has && ITEMS[it.k].type==='accessory'){
      d.style.cursor='pointer';
      d.onclick=()=>{
        state.equipped[it.k]=!state.equipped[it.k];
        sfx('pickRare'); log(`${state.equipped[it.k]?'Equipped':'Unequipped'} ${it.name}.`, 'gold');
        updateHUD();
      };
    }
    grid.appendChild(d);
  });
  // heart containers display
  const hc=document.createElement('div'); hc.className='item has'; hc.style.gridColumn='1 / -1';
  hc.innerHTML=`<div class="ico">❤️</div><div><h4>Heart Containers: ${state.maxContainers} / 5</h4><p>Each = 2 HP. Max 10 HP. ${state.maxContainers<5?'Find the remaining in Gallery & Spire.': 'Vitality complete.'}</p></div><div class="tag">${state.hp}/${state.maxContainers*2} HP</div>`;
  grid.appendChild(hc);
}

function renderShop(){
  const list=document.getElementById('shopList'); list.innerHTML='';
  if(!state.unlockedShop){
    list.innerHTML=`<div style="padding:14px;text-align:center;opacity:.6;font-size:13px">🔒 Merchant Altar dormant.<br>Clear the Ashen Threshold combat to awaken it.</div>`;
    return;
  }
  const offers=[
    {k:'vital', label:'Vital Stem', icon:'🌿', price:50, stock:state.shop.vital, desc:'Restores 4 HP'},
    {k:'seal', label:'Binding Seal', icon:'🔷', price:75, stock:state.shop.seal, desc:'Freeze enemy 3s'},
    {k:'heart', label:'Heart Container #5', icon:'❤️', price:300, stock:state.shop.heart, desc:'Permanent +2 HP (final)'},
    {k:'ammo', label:'Aether Charge', icon:'✦', price:10, stock: state.hasAether? 99 : 0, desc:'+5 Aether ammo'},
  ];
  offers.forEach(o=>{
    const row=document.createElement('div'); row.className='shopRow';
    const canBuy = state.gold>=o.price && o.stock>0;
    const disabled = o.k==='ammo' && !state.hasAether;
    row.innerHTML=`<div style="display:flex;gap:10px;align-items:center"><div style="width:34px;height:34px;display:grid;place-items:center;background:#0a0f1a;border:1px solid #ffffff0d;border-radius:8px">${o.icon}</div>
      <div><div style="font-family:'Cinzel',serif;font-size:11px">${o.label}</div><div style="font-size:11px;opacity:.6">${o.desc} · Stock: ${o.stock}</div></div></div>
      <div style="text-align:right"><div style="font-family:'JetBrains Mono',monospace;font-size:12px;color:var(--gold2)">${o.price} ◈</div><button class="btn small" ${(!canBuy||disabled)?'disabled':''}>Buy</button></div>`;
    const btn=row.querySelector('button');
    if(disabled) btn.textContent='Need Aether';
    else if(o.stock<=0) btn.textContent='Sold Out';
    btn.onclick=()=>{
      if(!canBuy) return;
      state.gold-=o.price;
      if(o.k==='vital'){ state.inventory.vital++; state.shop.vital--; sfx('shop'); log('Purchased Vital Stem.', 'gold'); }
      if(o.k==='seal'){ state.inventory.seal++; state.shop.seal--; sfx('shop'); log('Purchased Binding Seal.', 'gold'); }
      if(o.k==='heart'){ state.maxContainers=5; state.hp=Math.min(state.hp+2,10); state.shop.heart=0; state.found.heart5=true; sfx('pickRare'); triggerShimmer(); log('Heart Container #5 acquired! Max HP 10.', 'heal'); }
      if(o.k==='ammo'){ state.aetherAmmo+=5; sfx('pickCoin'); log('+5 Aether Charges.', 'gold'); }
      updateHUD();
    };
    list.appendChild(row);
  });
}

// zone objects setup
function buildZone(n){
  enemies=[]; projectiles=[]; particles=[]; objects=[]; plates=[]; switches=[];
  player.x=140; player.y=270; player.vx=0; player.vy=0; player.atk=0; player.invuln=0;
  const z=n;
  // common: sanctuary stone
  const hasStone = [1,2,5].includes(z); // spec: 1 entrance threshold, 2 center archives, 3 entrance twin spire (zone5)
  if(hasStone){
    objects.push({type:'stone', x: W-120, y: H/2, r:26, active:false});
    if(z===1) objects[objects.length-1].x= W/2, objects[objects.length-1].y= H-80;
    if(z===5) objects[objects.length-1].x= 120, objects[objects.length-1].y= 120;
  }
  if(z===1){
    enemies.push(makeEnemy(640,280, 3, 18, 'sentinel'));
    objects.push({type:'cache', x:520,y:140,r:16, gold:12, opened:false});
  }
  if(z===2){
    enemies.push(makeEnemy(500,200,2,16,'construct'), makeEnemy(700,340,2,16,'construct'));
    objects.push({type:'chest', x:200,y:140,r:18, item:'band', opened:false});
    objects.push({type:'cache', x:760,y:120,r:14, gold:18, opened:false});
    objects.push({type:'cache', x:300,y:420,r:14, gold:10, opened:false});
    if(!state.unlockedShop) objects.push({type:'shopAltar', x:W/2,y:H/2,r:28});
  }
  if(z===3){
    enemies.push(makeEnemy(620,260,3,18,'miniBoss'));
    objects.push({type:'altar', x:480,y:140,r:30, tier:1, claimed:state.found.altar1});
    objects.push({type:'crackedWall', x:780,y:270,r:36, hp:3, gold:0, heart:true, broken:false});
    plates.push({x:240,y:400,w:60,h:18, pressed:false, timer:0});
    objects.push({type:'cache', x:180,y:180,r:14, gold:15, opened:false});
  }
  if(z===4){
    enemies.push(makeEnemy(600,220,2,16,'construct'), makeEnemy(740,360,2,16,'construct'));
    objects.push({type:'crackedWall', x:760,y:180,r:32, hp:2, gold:0, aether:true, broken:false});
    objects.push({type:'lanternReward', x:480,y:120,r:20, opened:false});
    switches.push({x:820,y:100,w:18,h:18, hit:false});
    plates.push({x:220,y:420,w:60,h:18, pressed:false},{x:380,y:420,w:60,h:18, pressed:false});
    objects.push({type:'cache', x:160,y:160,r:14, gold:20, opened:false});
  }
  if(z===5){
    enemies.push(makeEnemy(500,200,3,18,'guardian'), makeEnemy(680,300,3,18,'construct'), makeEnemy(760,220,2,16,'construct'));
    objects.push({type:'altar', x:480,y:120,r:30, tier:2, claimed:state.found.altar2});
    objects.push({type:'chest', x:200,y:380,r:18, item:'cloak', opened: state.inventory.cloak});
    switches.push({x:840,y:90,w:18,h:18, hit:false, heart:true});
    objects.push({type:'cache', x:820,y:420,r:18, gold:45, opened:false});
  }
  if(z===6){
    enemies.push(makeEnemy(520,240,4,20,'corrupted'), makeEnemy(700,320,4,20,'corrupted'));
    objects.push({type:'beacon', x:W/2,y:H/2,r:40});
    switches.push({x:160,y:120,w:18,h:18, hit:false},{x:W-160,y:120,w:18,h:18, hit:false});
  }
  // add some decorative breakables
  if(z!==6){
    for(let i=0;i<2;i++) objects.push({type:'jar', x: 180+ i*520, y: 80+ Math.random()*20, r:12, gold: 6+Math.floor(Math.random()*8), opened:false});
  }
}

function makeEnemy(x,y,hp,r,kind){
  return {x,y,hp,max:hp,r, kind, vx:0, vy:0, hit:0, frozen:0, alive:true};
}

function goZone(n){
  if(n<1||n>6) return;
  // gating
  if(n>state.zone){
    if(state.zone===1 && !state.unlockedShop && enemies.some(e=>e.alive)) { log('Defeat the Sentinel and activate the stone first.', 'danger'); return; }
    if(state.zone===3 && !state.found.altar1) { log('The Gallery altar calls — claim Ignited Edge to proceed.', 'danger'); return; }
    if(state.zone===4 && !state.hasAether) { log('The Vault hides the Aether Shard. Break the cracked wall.', 'danger'); return; }
    if(state.zone===5 && !state.found.altar2) { log('The Spire altar awaits — claim Solar Falchion.', 'danger'); return; }
  }
  state.zone=n;
  buildZone(n);
  updateHUD();
  log(`Entered ${ZONES[n-1].name}.`, 'lore');
}

function triggerShimmer(){
  shimmerTimer=120; document.getElementById('shimmer').classList.add('on');
  sfx('shimmer');
  setTimeout(()=>document.getElementById('shimmer').classList.remove('on'), 2000);
}

function addParticles(x,y, color, n=12){
  for(let i=0;i<n;i++) particles.push({x,y, vx:(Math.random()-0.5)*6, vy:(Math.random()-0.5)*6 -1, life: 18+Math.random()*14, max:20, color, size: 2+Math.random()*3});
}

// input
window.addEventListener('keydown', e=>{
  keys[e.key.toLowerCase()]=true;
  if(e.code==='Space'){ e.preventDefault(); doAttack(); }
  if(e.key.toLowerCase()==='f') interact();
  if(e.key.toLowerCase()==='q') fireAether();
  if(e.key==='1') useVital();
  if(e.key==='2') useSeal();
  if(e.key==='3') { if(state.inventory.seal>0) useSeal(); }
});
window.addEventListener('keyup', e=> keys[e.key.toLowerCase()]=false);
c.addEventListener('mousemove', e=>{
  const rect=c.getBoundingClientRect();
  mouse.x = (e.clientX-rect.left)/rect.width*W;
  mouse.y = (e.clientY-rect.top)/rect.height*H;
});
c.addEventListener('mousedown', e=>{ mouse.down=true; doAttack(); });
c.addEventListener('mouseup', ()=> mouse.down=false);

function doAttack(){
  if(player.atk>0) return;
  const speedMul = state.equipped.band? 0.75 : 1;
  const dur = state.weapon===2? 22*speedMul : state.weapon===1? 14*speedMul : 16*speedMul;
  player.atk=dur; player.atkTime=dur;
  // sfx per weapon
  if(state.weapon===0) sfx('swing0');
  if(state.weapon===1) sfx('swing1');
  if(state.weapon===2) sfx('swing2');
}

function fireAether(){
  if(!state.hasAether) { log('Aether Shard not yet found — seek it in the Sunken Vault.', 'danger'); return; }
  if(state.aetherAmmo<=0){ log('Aether charges depleted. Buy more at the Merchant Altar.', 'danger'); return; }
  state.aetherAmmo--;
  const ang = Math.atan2(mouse.y - player.y, mouse.x - player.x);
  // if mouse not moved, use facing
  const a = (mouse.x===0&&mouse.y===0)? (player.facing===1?0:Math.PI) : ang;
  projectiles.push({x:player.x+ Math.cos(a)*18, y:player.y+ Math.sin(a)*6, vx: Math.cos(a)*9, vy: Math.sin(a)*9, life:60, r:6});
  sfx('aether');
  updateHUD();
}

function useVital(){
  if(state.inventory.vital<=0){ log('No Vital Stems.', 'danger'); return; }
  if(state.hp>=state.maxContainers*2){ log('Health already full.', 'danger'); return; }
  state.inventory.vital--; state.hp=Math.min(state.hp+4, state.maxContainers*2);
  addParticles(player.x, player.y, '#4ade80', 16);
  sfx('heal'); updateHUD(); log('Vital Stem restored 4 HP.', 'heal');
}
function useSeal(){
  if(state.inventory.seal<=0){ log('No Binding Seals.', 'danger'); return; }
  // freeze nearest enemy
  let best=null, bd=1e9;
  enemies.forEach(e=>{ if(!e.alive) return; const d=Math.hypot(e.x-player.x,e.y-player.y); if(d<bd){bd=d; best=e;} });
  if(!best){ log('No target for Binding Seal.', 'danger'); return; }
  state.inventory.seal--; best.frozen=180; addParticles(best.x,best.y,'#60a5fa',14); sfx('freeze'); log('Binding Seal — enemy frozen!', 'gold'); updateHUD();
}

function interact(){
  // find nearest interactable
  let best=null, bd=40;
  objects.forEach(o=>{
    const d=Math.hypot(o.x-player.x,o.y-player.y);
    if(d<bd){ bd=d; best=o; }
  });
  if(!best) return;
  if(best.type==='stone'){
    state.savedZone=state.zone; state.savedHP=state.hp; state.savedGold=state.gold;
    state.hp=Math.min(state.hp+2, state.maxContainers*2); // rest
    sfx('save'); triggerShimmer(); log('Sanctuary Stone activated — progress saved, +1 HP rest.', 'heal');
    if(state.zone===1) { state.unlockedShop=true; log('Merchant Altar awakened in the Rusted Archives.', 'gold'); }
    best.active=true; updateHUD();
  }
  if(best.type==='chest' && !best.opened){
    best.opened=true;
    if(best.item==='band'){ state.inventory.band=true; log('Found Warden\'s Band! Equip for attack speed.', 'gold'); }
    if(best.item==='cloak'){ state.inventory.cloak=true; log('Found Shadow Cloak! Equip to reduce trap damage.', 'gold'); }
    sfx('pickRare'); addParticles(best.x,best.y,'#f0c27a',18); updateHUD();
  }
  if(best.type==='cache' && !best.opened){
    best.opened=true; state.gold+=best.gold; sfx('pickCoin'); addParticles(best.x,best.y,'#d4a24e',12); log(`Found cache: +${best.gold} Sigils.`, 'gold'); updateHUD();
  }
  if(best.type==='jar' && !best.opened){
    best.opened=true; state.gold+=best.gold; if(Math.random()<0.3){ state.inventory.seal++; log(`Smashed jar: +${best.gold} Sigils and a Binding Seal!`, 'gold'); } else log(`Smashed jar: +${best.gold} Sigils.`, 'gold');
    sfx('break'); addParticles(best.x,best.y,'#c9b8a0',10); updateHUD();
  }
  if(best.type==='altar' && !best.claimed){
    openAltar(best.tier);
  }
  if(best.type==='lanternReward' && !best.opened){
    best.opened=true; state.inventory.lantern=true; sfx('pickRare'); addParticles(best.x,best.y,'#60a5fa',18); log('Phantom Lantern acquired — hidden cracks now shimmer blue.', 'gold'); updateHUD();
  }
  if(best.type==='beacon'){
    // need both switches hit
    const allHit = switches.every(s=>s.hit);
    if(!allHit){ log('Beacon requires both crystal switches struck by Aether Shard.', 'danger'); return; }
    if(enemies.some(e=>e.alive)){ log('Clear the Corrupted before reigniting.', 'danger'); return; }
    document.getElementById('beaconOverlay').classList.add('show');
    document.getElementById('finalStats').textContent=`Sigils: ${state.gold} · Kills: ${state.kills} · Hearts: ${state.maxContainers}/5 · Weapon: ${['Base','Ignited','Solar'][state.weapon]}`;
    sfx('save'); triggerShimmer();
  }
}

function openAltar(tier){
  const ov=document.getElementById('altarOverlay');
  const title=document.getElementById('altarTitle');
  const text=document.getElementById('altarText');
  if(tier===1){
    title.textContent='IGNITED EDGE ALTAR';
    text.textContent='Bronze runes flare orange. Your Ember Blade drinks fire — faster swings, ember trail, burn-flinch on hit.';
  } else {
    title.textContent='SOLAR FALCHION ALTAR';
    text.textContent='Sunlight trapped for centuries. The blade widens, heavy and golden — wide arc, deep bass, sustained halo.';
  }
  ov.classList.add('show');
  document.getElementById('altarClaim').onclick=()=>{
    ov.classList.remove('show');
    if(tier===1){ state.weapon=1; state.found.altar1=true; objects.find(o=>o.type==='altar'&&o.tier===1).claimed=true; }
    if(tier===2){ state.weapon=2; state.found.altar2=true; objects.find(o=>o.type==='altar'&&o.tier===2).claimed=true; }
    triggerShimmer(); sfx('pickRare'); log(tier===1?'Ignited Edge claimed!':'Solar Falchion claimed!', 'gold'); updateHUD();
  };
}
document.getElementById('altarClose').onclick=()=> document.getElementById('altarOverlay').classList.remove('show');

// buttons
document.getElementById('btnSave').onclick=interact;
document.getElementById('btnPrev').onclick=()=> goZone(state.zone-1);
document.getElementById('btnNext').onclick=()=> goZone(state.zone+1);
document.getElementById('useVital').onclick=useVital;
document.getElementById('useSeal').onclick=useSeal;
document.querySelectorAll('.tab').forEach(t=>{
  t.onclick=()=>{
    document.querySelectorAll('.tab').forEach(x=>x.classList.remove('active')); t.classList.add('active');
    document.getElementById('panelInv').style.display = t.dataset.tab==='inv'?'block':'none';
    document.getElementById('panelShop').style.display = t.dataset.tab==='shop'?'block':'none';
    document.getElementById('panelLog').style.display = t.dataset.tab==='log'?'block':'none';
  };
});

// game loop
function update(){
  // input movement
  let mx=0,my=0;
  if(keys['w']||keys['arrowup']) my-=1;
  if(keys['s']||keys['arrowdown']) my+=1;
  if(keys['a']||keys['arrowleft']) {mx-=1; player.facing=-1;}
  if(keys['d']||keys['arrowright']) {mx+=1; player.facing=1;}
  if(mx!==0||my!==0){
    const len=Math.hypot(mx,my)||1; mx/=len; my/=len;
    let spd=player.speed;
    if(state.equipped.cloak) spd*=1.05;
    player.vx=mx*spd; player.vy=my*spd;
  } else { player.vx*=0.82; player.vy*=0.82; }
  player.x+=player.vx; player.y+=player.vy;
  player.x=Math.max(18,Math.min(W-18,player.x));
  player.y=Math.max(18,Math.min(H-18,player.y));
  if(player.atk>0) player.atk--;
  if(player.invuln>0) player.invuln--;
  if(shake>0) shake--;
  if(shimmerTimer>0) shimmerTimer--;

  // plates
  plates.forEach(p=>{
    const on = player.x>p.x && player.x<p.x+p.w && player.y>p.y-8 && player.y<p.y+p.h+8;
    const enemyOn = enemies.some(e=>e.alive && e.x>p.x && e.x<p.x+p.w && e.y>p.y-8 && e.y<p.y+p.h+8);
    p.pressed = on || enemyOn;
  });
  // open doors via plates logic (zone3/4)
  if(state.zone===3 && plates[0]?.pressed){
    // reveal hint
  }
  if(state.zone===4){
    const both = plates.every(p=>p.pressed);
    if(both){
      // reveal lantern if not yet
      const lr=objects.find(o=>o.type==='lanternReward');
      if(lr) lr.opened = lr.opened; // just visual
    }
  }

  // enemies AI
  enemies.forEach(e=>{
    if(!e.alive) return;
    if(e.frozen>0){ e.frozen--; return; }
    const dx=player.x-e.x, dy=player.y-e.y, d=Math.hypot(dx,dy);
    if(d<420){
      const sp = e.kind==='miniBoss'?1.2: e.kind==='guardian'?1.4:1.0;
      e.vx = (dx/d)*sp; e.vy=(dy/d)*sp*0.9;
      e.x+=e.vx; e.y+=e.vy;
    }
    // contact damage
    if(d < e.r + player.r && player.invuln===0){
      let dmg=1;
      if(state.equipped.cloak) dmg = Math.random()<0.3?0:1; // reduce
      if(dmg>0){
        state.hp-=dmg; player.invuln=60; shake=12;
        document.getElementById('flash').classList.add('hit'); setTimeout(()=>document.getElementById('flash').classList.remove('hit'),180);
        addParticles(player.x,player.y,'#ff4d4d',10);
        sfx('hurt'); log(`Hit! -${dmg} HP`, 'danger'); updateHUD();
        if(state.hp<=0){
          state.hp=0; log('Warden fell. Restored at last Sanctuary Stone.', 'danger');
          // respawn at saved
          state.zone=state.savedZone; state.hp=Math.max(2, state.savedHP); state.gold=state.savedGold;
          buildZone(state.zone); updateHUD(); sfx('hurt');
        }
      }
    }
    if(e.hit>0) e.hit--;
  });

  // melee hit
  if(player.atk>6 && player.atkTime>0){
    // active frames
    const range = state.weapon===2? 54 : state.weapon===1? 42 : 36;
    const arc = state.weapon===2? Math.PI*0.9 : Math.PI*0.6;
    enemies.forEach(e=>{
      if(!e.alive) return;
      const dx=e.x-player.x, dy=e.y-player.y, d=Math.hypot(dx,dy);
      if(d>range) return;
      const ang=Math.atan2(dy,dx);
      const facingAng = player.facing===1?0:Math.PI;
      let diff=Math.abs(Math.atan2(Math.sin(ang-facingAng), Math.cos(ang-facingAng)));
      if(diff>arc/2) return;
      // hit
      if(e.hit===0){
        const dmg = state.weapon===2?3: state.weapon===1?2:1;
        e.hp-=dmg; e.hit=12;
        addParticles(e.x,e.y, state.weapon===1?'#ff8c2a': state.weapon===2?'#ffd86b':'#ff6b4d', 8);
        sfx('hit');
        // knockback
        e.x += Math.cos(ang)*8; e.y+=Math.sin(ang)*8;
        if(e.hp<=0){
          e.alive=false; state.kills++;
          const goldDrop = 5+Math.floor(Math.random()*11);
          state.gold+=goldDrop; addParticles(e.x,e.y,'#d4a24e',14);
          sfx('pickCoin'); log(`${e.kind} shattered +${goldDrop} Sigils.`, 'gold');
          // drops for specific zones
          if(state.zone===3 && e.kind==='miniBoss' && !state.found.heart4){
            // heart container will be via wall, but also drop chance
          }
          updateHUD();
          // check clear
          if(!enemies.some(en=>en.alive)){
            if(state.zone===1) log('Threshold cleared — Sanctuary Stone thrums.', 'heal');
            if(state.zone===2) { state.unlockedShop=true; log('Archives cleared — Merchant Altar awakened.', 'gold'); updateHUD(); }
          }
        }
      }
    });
    // breakable walls / jars with melee
    objects.forEach(o=>{
      if(o.type==='crackedWall' && !o.broken){
        const d=Math.hypot(o.x-player.x,o.y-player.y);
        if(d<48){
          o.hp -= state.weapon===2?1.2: state.weapon===1?0.8:0.5;
          addParticles(o.x,o.y,'#8b7355',6);
          if(o.hp<=0){
            o.broken=true; sfx('break'); addParticles(o.x,o.y,'#d4a24e',16);
            if(o.heart && !state.found.heart4){ state.maxContainers=4; state.hp=Math.min(state.hp+2,8); state.found.heart4=true; triggerShimmer(); log('Heart Container #4 found! Max HP 8.', 'heal'); }
            if(o.aether && !state.hasAether){ state.hasAether=true; state.aetherAmmo=12; state.found.aether=true; log('Aether Shard discovered! Press Q to fire.', 'gold'); sfx('pickRare'); triggerShimmer(); }
            updateHUD();
          }
        }
      }
      if(o.type==='jar' && !o.opened){
        const d=Math.hypot(o.x-player.x,o.y-player.y);
        if(d<40 && player.atk===Math.floor(player.atkTime/2)){
          o.opened=true; state.gold+=o.gold; sfx('break'); addParticles(o.x,o.y,'#c9b8a0',8); log(`Jar smashed +${o.gold} Sigils.`, 'gold'); updateHUD();
        }
      }
    });
  }

  // projectiles
  for(let i=projectiles.length-1;i>=0;i--){
    const p=projectiles[i]; p.x+=p.vx; p.y+=p.vy; p.life--;
    // hit switches
    switches.forEach(s=>{
      if(s.hit) return;
      if(p.x>s.x && p.x<s.x+s.w && p.y>s.y && p.y<s.y+s.h){
        s.hit=true; addParticles(s.x+s.w/2,s.y+s.h/2,'#60a5fa',12); sfx('pickRare');
        if(s.heart && !state.found.heart5){ state.maxContainers=5; state.hp=Math.min(state.hp+2,10); state.found.heart5=true; state.shop.heart=0; triggerShimmer(); log('Distant switch triggered — Heart Container #5 secured!', 'heal'); updateHUD(); }
        else log('Crystal switch struck — mechanism whirs.', 'gold');
        p.life=0;
      }
    });
    // hit enemies
    enemies.forEach(e=>{
      if(!e.alive) return;
      if(Math.hypot(e.x-p.x,e.y-p.y)< e.r+6){
        e.hp-=2; e.hit=10; addParticles(e.x,e.y,'#7dd3fc',8); sfx('hit'); p.life=0;
        if(e.hp<=0){ e.alive=false; state.kills++; state.gold+=8; log('Construct shattered by Aether bolt +8 Sigils.', 'gold'); updateHUD(); }
      }
    });
    // hit cracked walls
    objects.forEach(o=>{
      if(o.type==='crackedWall' && !o.broken){
        if(Math.hypot(o.x-p.x,o.y-p.y)< o.r){
          o.hp-=2; p.life=0; addParticles(o.x,o.y,'#8b7355',6);
          if(o.hp<=0){ o.broken=true; sfx('break'); addParticles(o.x,o.y,'#d4a24e',16);
            if(o.heart && !state.found.heart4){ state.maxContainers=4; state.hp=Math.min(state.hp+2,8); state.found.heart4=true; triggerShimmer(); log('Heart Container #4 found via Aether!', 'heal'); }
            if(o.aether && !state.hasAether){ state.hasAether=true; state.aetherAmmo=12; state.found.aether=true; log('Aether Shard revealed!', 'gold'); sfx('pickRare'); triggerShimmer(); }
            updateHUD();
          }
        }
      }
    });
    if(p.life<=0 || p.x<0||p.x>W||p.y<0||p.y>H) projectiles.splice(i,1);
  }

  // particles
  for(let i=particles.length-1;i>=0;i--){
    const pa=particles[i]; pa.x+=pa.vx; pa.y+=pa.vy; pa.vy+=0.18; pa.life--; if(pa.life<=0) particles.splice(i,1);
  }
}

function draw(){
  ctx.save();
  if(shake>0) ctx.translate((Math.random()-0.5)*shake, (Math.random()-0.5)*shake);
  // background per zone
  const zc=ZONES[state.zone-1].color;
  ctx.fillStyle=zc; ctx.fillRect(0,0,W,H);
  // subtle grid / bronze
  ctx.strokeStyle='#ffffff08'; ctx.lineWidth=1;
  for(let x=0;x<W;x+=80){ ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,H); ctx.stroke(); }
  for(let y=0;y<H;y+=80){ ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(W,y); ctx.stroke(); }
  // vignette
  const grad=ctx.createRadialGradient(W/2,H/2, 200, W/2,H/2, 600);
  grad.addColorStop(0,'transparent'); grad.addColorStop(1,'#00000066'); ctx.fillStyle=grad; ctx.fillRect(0,0,W,H);
  // floating dust
  ctx.fillStyle='#ffffff0a';
  for(let i=0;i<6;i++){ const x=(Date.now()*0.02 + i*140)%W, y= 60+ Math.sin(Date.now()*0.0006+i)*20 + (i*70)%H; ctx.beginPath(); ctx.arc(x,y,1.2,0,Math.PI*2); ctx.fill(); }

  // plates
  plates.forEach(p=>{
    ctx.fillStyle= p.pressed? '#d4a24e' : '#3a4a62';
    ctx.strokeStyle='#d4a24e55'; ctx.lineWidth=1.5;
    ctx.fillRect(p.x,p.y,p.w,p.h); ctx.strokeRect(p.x,p.y,p.w,p.h);
    if(p.pressed){ ctx.fillStyle='#ffd86b88'; ctx.fillRect(p.x+4,p.y+4,p.w-8,4); }
  });
  // switches
  switches.forEach(s=>{
    ctx.fillStyle= s.hit? '#4ade80' : '#60a5fa';
    ctx.shadowColor= s.hit?'#4ade80':'#60a5fa'; ctx.shadowBlur= s.hit?12:8;
    ctx.fillRect(s.x,s.y,s.w,s.h);
    ctx.shadowBlur=0;
    ctx.strokeStyle='#ffffffaa'; ctx.strokeRect(s.x,s.y,s.w,s.h);
    // beam if hit
    if(s.hit){ ctx.strokeStyle='#4ade8066'; ctx.beginPath(); ctx.moveTo(s.x+s.w/2,s.y+s.h); ctx.lineTo(s.x+s.w/2,s.y+s.h+40); ctx.stroke(); }
  });

  // objects
  objects.forEach(o=>{
    if(o.type==='stone'){
      // pedestal
      ctx.fillStyle='#1e2f4a'; ctx.strokeStyle='#d4a24e88'; ctx.lineWidth=2;
      ctx.beginPath(); ctx.ellipse(o.x,o.y+16, 30,10,0,0,Math.PI*2); ctx.fill(); ctx.stroke();
      ctx.fillStyle='#0f1724'; ctx.fillRect(o.x-14,o.y-18,28,26);
      // glow
      const pulse = 0.6+ Math.sin(Date.now()*0.003)*0.2;
      ctx.fillStyle=`rgba(212,162,78,${0.5*pulse})`; ctx.shadowColor='#d4a24e'; ctx.shadowBlur=16;
      ctx.beginPath(); ctx.arc(o.x,o.y-10, 10,0,Math.PI*2); ctx.fill(); ctx.shadowBlur=0;
      ctx.fillStyle='#fff'; ctx.font='10px JetBrains Mono'; ctx.textAlign='center'; ctx.fillText('◈',o.x,o.y-6);
      if(o.active){ ctx.fillStyle='#d4a24e'; ctx.font='9px JetBrains Mono'; ctx.fillText('SAVED',o.x,o.y+34); }
    }
    if(o.type==='chest'){
      if(o.opened){ ctx.fillStyle='#3a2f1a'; ctx.fillRect(o.x-16,o.y-10,32,18); }
      else { ctx.fillStyle='#8b5a2b'; ctx.strokeStyle='#d4a24e'; ctx.lineWidth=1.5; ctx.fillRect(o.x-16,o.y-10,32,18); ctx.strokeRect(o.x-16,o.y-10,32,18); ctx.fillStyle='#d4a24e'; ctx.fillRect(o.x-4,o.y-2,8,6); }
    }
    if(o.type==='cache'){
      if(!o.opened){ ctx.fillStyle='#d4a24e'; ctx.shadowColor='#d4a24e'; ctx.shadowBlur=8; ctx.beginPath(); ctx.arc(o.x,o.y,6,0,Math.PI*2); ctx.fill(); ctx.shadowBlur=0; }
    }
    if(o.type==='jar'){
      if(!o.opened){ ctx.fillStyle='#c9b8a0'; ctx.strokeStyle='#8b7355'; ctx.fillRect(o.x-8,o.y-10,16,16); ctx.strokeRect(o.x-8,o.y-10,16,16); ctx.fillStyle='#8b7355'; ctx.fillRect(o.x-6,o.y-12,12,4); }
    }
    if(o.type==='crackedWall'){
      if(!o.broken){
        ctx.fillStyle='#2a3548'; ctx.strokeStyle='#ff6b4d88'; ctx.lineWidth=2; ctx.fillRect(o.x-o.r,o.y-o.r,o.r*2,o.r*2); ctx.strokeRect(o.x-o.r,o.y-o.r,o.r*2,o.r*2);
        // cracks
        ctx.strokeStyle='#ff6b4d'; ctx.lineWidth=1.2; ctx.beginPath(); ctx.moveTo(o.x-o.r+6,o.y-o.r+8); ctx.lineTo(o.x+o.r-6,o.y+o.r-8); ctx.moveTo(o.x+o.r-8,o.y-o.r+6); ctx.lineTo(o.x-o.r+8,o.y+o.r-6); ctx.stroke();
        // shimmer if lantern equipped
        if(state.equipped.lantern){ ctx.strokeStyle='#60a5fa'; ctx.setLineDash([4,4]); ctx.strokeRect(o.x-o.r-2,o.y-o.r-2,o.r*2+4,o.r*2+4); ctx.setLineDash([]); }
        ctx.fillStyle='#ffffffcc'; ctx.font='9px JetBrains Mono'; ctx.textAlign='center'; ctx.fillText('CRACKED',o.x,o.y+4);
      } else {
        ctx.fillStyle='#00000044'; ctx.fillRect(o.x-o.r,o.y-o.r,o.r*2,o.r*2);
      }
    }
    if(o.type==='altar'){
      const claimed=o.claimed;
      ctx.fillStyle='#1a2332'; ctx.strokeStyle= claimed?'#4ade80':'#d4a24e'; ctx.lineWidth=2;
      ctx.beginPath(); ctx.ellipse(o.x,o.y+12, 36,12,0,0,Math.PI*2); ctx.fill(); ctx.stroke();
      ctx.fillStyle= claimed?'#0f1724':'#2a3a52'; ctx.fillRect(o.x-18,o.y-22,36,30);
      if(!claimed){
        const glow = o.tier===1?'#ff6b2b':'#ffd86b';
        ctx.shadowColor=glow; ctx.shadowBlur=14; ctx.fillStyle=glow; ctx.beginPath(); ctx.arc(o.x,o.y-8,8,0,Math.PI*2); ctx.fill(); ctx.shadowBlur=0;
        ctx.fillStyle='#fff'; ctx.font='12px serif'; ctx.textAlign='center'; ctx.fillText(o.tier===1?'◈':'☀',o.x,o.y-4);
      } else { ctx.fillStyle='#4ade80'; ctx.font='10px JetBrains Mono'; ctx.textAlign='center'; ctx.fillText('CLAIMED',o.x,o.y+28); }
    }
    if(o.type==='shopAltar'){
      ctx.fillStyle='#1e2f4a'; ctx.strokeStyle='#d4a24e'; ctx.lineWidth=2; ctx.beginPath(); ctx.arc(o.x,o.y,28,0,Math.PI*2); ctx.fill(); ctx.stroke();
      ctx.fillStyle='#d4a24e'; ctx.font='16px serif'; ctx.textAlign='center'; ctx.fillText('⚖',o.x,o.y+6);
    }
    if(o.type==='lanternReward'){
      if(!o.opened){ ctx.fillStyle='#1e3a5a'; ctx.strokeStyle='#60a5fa'; ctx.lineWidth=1.5; ctx.beginPath(); ctx.arc(o.x,o.y,18,0,Math.PI*2); ctx.fill(); ctx.stroke(); ctx.fillStyle='#fff'; ctx.font='14px serif'; ctx.textAlign='center'; ctx.fillText('🏮',o.x,o.y+5); }
    }
    if(o.type==='beacon'){
      const pulse= 0.7+ Math.sin(Date.now()*0.002)*0.3;
      ctx.shadowColor='#ffd86b'; ctx.shadowBlur=20*pulse; ctx.fillStyle=`rgba(255,216,107,${0.9*pulse})`;
      ctx.beginPath(); ctx.arc(o.x,o.y, 28,0,Math.PI*2); ctx.fill(); ctx.shadowBlur=0;
      ctx.strokeStyle='#fff'; ctx.lineWidth=2; ctx.beginPath(); ctx.arc(o.x,o.y,36,0,Math.PI*2); ctx.stroke();
      ctx.fillStyle='#1a1206'; ctx.font='11px Cinzel'; ctx.textAlign='center'; ctx.fillText('BEACON',o.x,o.y+4);
    }
  });

  // enemies
  enemies.forEach(e=>{
    if(!e.alive) return;
    ctx.save(); ctx.translate(e.x,e.y);
    // shadow
    ctx.fillStyle='#00000044'; ctx.beginPath(); ctx.ellipse(0,12, e.r+2,6,0,0,Math.PI*2); ctx.fill();
    // body
    let col = e.kind==='miniBoss'?'#8b4a2a': e.kind==='guardian'?'#4a2a5a': e.kind==='corrupted'?'#1a2e2a':'#3a4a62';
    if(e.frozen>0) col='#60a5fa';
    if(e.hit>0) col='#ff4d4d';
    ctx.fillStyle=col; ctx.strokeStyle='#d4a24e55'; ctx.lineWidth=1.5;
    ctx.beginPath(); ctx.arc(0,0,e.r,0,Math.PI*2); ctx.fill(); ctx.stroke();
    // eye
    ctx.fillStyle= e.frozen>0?'#fff':'#ff6b2b'; ctx.beginPath(); ctx.arc(2,-3,3,0,Math.PI*2); ctx.fill();
    // hp bar
    ctx.fillStyle='#000'; ctx.fillRect(-14,-e.r-10,28,4);
    ctx.fillStyle='#4ade80'; ctx.fillRect(-14,-e.r-10,28*(e.hp/e.max),4);
    ctx.restore();
  });

  // projectiles
  projectiles.forEach(p=>{
    ctx.fillStyle='#7dd3fc'; ctx.shadowColor='#7dd3fc'; ctx.shadowBlur=10; ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,Math.PI*2); ctx.fill(); ctx.shadowBlur=0;
    ctx.strokeStyle='#fff'; ctx.lineWidth=1; ctx.beginPath(); ctx.arc(p.x,p.y,2,0,Math.PI*2); ctx.stroke();
  });

  // player
  ctx.save(); ctx.translate(player.x,player.y);
  // ember trail if weapon1
  if(state.weapon===1 && (Math.abs(player.vx)>0.5||player.atk>0)){
    ctx.fillStyle='#ff6b2b44'; ctx.beginPath(); ctx.arc(-player.facing*10,4,6,0,Math.PI*2); ctx.fill();
  }
  // cloak
  ctx.fillStyle='#1a2332'; ctx.strokeStyle='#d4a24e22'; ctx.lineWidth=1;
  ctx.beginPath(); ctx.ellipse(0,8,14,10,0,0,Math.PI*2); ctx.fill(); ctx.stroke();
  if(state.equipped.cloak){ ctx.strokeStyle='#0f1724'; ctx.lineWidth=3; ctx.beginPath(); ctx.arc(0,8,14, -0.6,0.6); ctx.stroke(); }
  // body
  ctx.fillStyle='#e8ddd0'; ctx.beginPath(); ctx.arc(0,0,8,0,Math.PI*2); ctx.fill();
  // ring visual
  if(state.equipped.band){ ctx.strokeStyle='#c0c8d8'; ctx.lineWidth=2; ctx.beginPath(); ctx.arc(6,4,3,0,Math.PI*2); ctx.stroke(); }
  // lantern glow
  if(state.equipped.lantern){ ctx.fillStyle='#60a5fa88'; ctx.beginPath(); ctx.arc(-8,2,5,0,Math.PI*2); ctx.fill(); }
  // off-hand crystal
  if(state.hasAether){
    ctx.fillStyle='#7dd3fc'; ctx.shadowColor='#7dd3fc'; ctx.shadowBlur=8; ctx.beginPath(); ctx.arc(-10,0,4,0,Math.PI*2); ctx.fill(); ctx.shadowBlur=0;
  }
  // blade
  const atkPhase = player.atk>0 ? (1 - player.atk/player.atkTime) : 0;
  let bladeAng = player.facing===1? 0.2 : Math.PI-0.2;
  if(player.atk>0){
    const swing = Math.sin(atkPhase*Math.PI)* (state.weapon===2? 1.2:0.9);
    bladeAng += player.facing*swing*1.6;
  }
  ctx.save(); ctx.rotate(bladeAng);
  let bladeLen = state.weapon===2? 28 : state.weapon===1? 22 : 18;
  let bladeCol = state.weapon===2?'#ffd86b': state.weapon===1?'#ff8c2a':'#c9b8a0';
  if(state.weapon===1){ ctx.shadowColor='#ff6b2b'; ctx.shadowBlur=10; }
  if(state.weapon===2){ ctx.shadowColor='#ffd86b'; ctx.shadowBlur=14; }
  ctx.fillStyle=bladeCol; ctx.fillRect(8,-3, bladeLen,6);
  ctx.fillStyle='#fff'; ctx.fillRect(8+bladeLen-4,-2,4,4);
  ctx.shadowBlur=0;
  // hilt
  ctx.fillStyle='#8b7355'; ctx.fillRect(4,-4,6,8);
  ctx.restore();
  // invuln flash
  if(player.invuln>0 && Math.floor(player.invuln/4)%2===0){ ctx.fillStyle='#ff4d4d66'; ctx.beginPath(); ctx.arc(0,0,14,0,Math.PI*2); ctx.fill(); }
  ctx.restore();

  // particles
  particles.forEach(p=>{
    ctx.globalAlpha = Math.max(0, p.life/p.max);
    ctx.fillStyle=p.color; ctx.beginPath(); ctx.arc(p.x,p.y,p.size,0,Math.PI*2); ctx.fill();
  });
  ctx.globalAlpha=1;
  ctx.restore();
}

function loop(){
  update(); draw(); requestAnimationFrame(loop);
}

// init
buildZone(1); updateHUD();
log('Warden awakens at the Ashen Threshold. Find the Sanctuary Stone.', 'lore');
log('Tip: Break jars and caches for Sigils. Watch for cracked walls.', '');
loop();

// expose for probe
window._state=state; window._player=player; window._goZone=goZone; window._enemies=enemies;

</script>
</body>
</html>