← Warden results

Warden

Grok 4.6 · 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 78,851 bytes · SHA-256 23760964e5d5
<!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</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=Cormorant+Garamond:ital,wght@0,400;0,600;1,400&display=swap" rel="stylesheet" />
<style>
  :root {
    --gold: #e0b45a;
    --gold-dim: #a67c3a;
    --ember: #d4652f;
    --slate: #121820;
    --ink: #0a0d12;
    --mist: #8aa0b8;
    --heart: #c94a4a;
    --ok: #7dba6a;
  }
  * { box-sizing: border-box; margin: 0; padding: 0; }
  html, body {
    width: 100%; height: 100%; overflow: hidden;
    background: var(--ink);
    color: #e8dcc4;
    font-family: "Cormorant Garamond", Palatino, Georgia, serif;
    user-select: none;
  }
  #wrap {
    position: relative; width: 100%; height: 100%;
    display: flex; align-items: center; justify-content: center;
    background: radial-gradient(ellipse at 50% 40%, #1a2230 0%, #0a0d12 70%);
  }
  canvas { display: block; background: #0d1218; }
  #hud {
    position: absolute; inset: 0; pointer-events: none;
    font-family: Cinzel, Palatino, serif;
  }
  #hud-top {
    position: absolute; top: 18px; left: 22px; right: 22px;
    display: flex; justify-content: space-between; align-items: flex-start;
  }
  #hearts { display: flex; gap: 6px; filter: drop-shadow(0 2px 4px #000a); }
  .heart {
    width: 26px; height: 24px; position: relative;
  }
  .heart svg { width: 100%; height: 100%; }
  #meta {
    text-align: right; font-size: 13px; letter-spacing: 0.14em;
    color: var(--gold); text-shadow: 0 1px 6px #000c;
  }
  #meta .gold { font-size: 18px; font-weight: 600; }
  #meta .zone { color: #b8c4d4; letter-spacing: 0.22em; font-size: 11px; margin-top: 4px; opacity: 0.85; }
  #meta .charges { color: #8ec8e8; font-size: 12px; margin-top: 3px; letter-spacing: 0.12em; }
  #prompt {
    position: absolute; bottom: 86px; left: 50%; transform: translateX(-50%);
    font-size: 15px; letter-spacing: 0.18em; color: var(--gold);
    text-shadow: 0 2px 10px #000; opacity: 0; transition: opacity 0.2s;
    white-space: nowrap;
  }
  #prompt.show { opacity: 1; }
  #toast {
    position: absolute; top: 22%; left: 50%; transform: translateX(-50%);
    text-align: center; pointer-events: none;
  }
  #toast .title {
    font-family: Cinzel, serif; font-size: 22px; letter-spacing: 0.28em;
    color: var(--gold); text-shadow: 0 0 24px #e0b45a88, 0 2px 8px #000;
    opacity: 0; transform: translateY(8px);
    transition: opacity 0.5s, transform 0.5s;
  }
  #toast .sub {
    font-style: italic; font-size: 16px; color: #d4c8b0; margin-top: 6px;
    opacity: 0; transition: opacity 0.6s 0.15s;
  }
  #toast.show .title, #toast.show .sub { opacity: 1; transform: none; }
  #log {
    position: absolute; bottom: 18px; left: 22px; max-width: 420px;
    font-size: 14px; color: #c9bda4; text-shadow: 0 1px 4px #000;
    line-height: 1.35;
  }
  #log div { opacity: 0.95; animation: fadeLog 5s forwards; }
  @keyframes fadeLog { 0%{opacity:1} 70%{opacity:1} 100%{opacity:0} }
  #controls-hint {
    position: absolute; bottom: 16px; right: 20px;
    font-size: 11px; letter-spacing: 0.16em; color: #8a8070; text-align: right;
    line-height: 1.6;
  }
  #inv-panel, #shop-panel, #pause-panel {
    pointer-events: auto;
    position: absolute; inset: 0;
    background: radial-gradient(ellipse at center, #121820ee 0%, #0a0d12f2 100%);
    display: none; align-items: center; justify-content: center;
    flex-direction: column;
  }
  #inv-panel.open, #shop-panel.open, #pause-panel.open { display: flex; }
  .panel {
    width: min(640px, 92vw); border: 1px solid #a67c3a66;
    background: linear-gradient(180deg, #1a2230 0%, #121820 100%);
    box-shadow: 0 0 0 1px #000, 0 20px 80px #000c, 0 0 60px #e0b45a18;
    padding: 28px 32px 24px;
  }
  .panel h2 {
    font-family: Cinzel, serif; font-weight: 600; letter-spacing: 0.32em;
    font-size: 15px; color: var(--gold); text-align: center;
    margin-bottom: 18px;
  }
  .slots { display: flex; flex-direction: column; gap: 8px; }
  .slot {
    display: grid; grid-template-columns: 42px 1fr auto; gap: 12px; align-items: center;
    padding: 10px 12px; border: 1px solid #3d4d6355; background: #0e141c;
    cursor: pointer; transition: border-color 0.15s, background 0.15s;
  }
  .slot:hover, .slot.sel { border-color: var(--gold-dim); background: #1a2430; }
  .slot .ico { width: 36px; height: 36px; display: grid; place-items: center; font-size: 20px; }
  .slot .nm { font-family: Cinzel, serif; font-size: 13px; letter-spacing: 0.12em; color: #e8dcc4; }
  .slot .ds { font-size: 13px; color: #9aa6b4; font-style: italic; }
  .slot .st { font-size: 11px; letter-spacing: 0.16em; color: var(--gold); }
  .slot.on { border-color: #7dba6a88; }
  .panel .foot {
    margin-top: 16px; text-align: center; font-size: 12px; letter-spacing: 0.2em; color: #7a7264;
  }
  #overlay-title {
    position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
    flex-direction: column; background: #07090d;
    z-index: 20;
  }
  #overlay-title.gone { display: none; }
  #overlay-title .crest {
    width: 72px; height: 72px; margin-bottom: 18px; opacity: 0.9;
  }
  #overlay-title h1 {
    font-family: Cinzel, serif; font-weight: 700; font-size: clamp(28px, 5vw, 52px);
    letter-spacing: 0.22em; color: var(--gold);
    text-shadow: 0 0 40px #e0b45a44;
  }
  #overlay-title .subhead {
    margin-top: 10px; letter-spacing: 0.46em; font-size: 12px; color: #8aa0b8;
    text-transform: uppercase;
  }
  #overlay-title .lore {
    max-width: 540px; text-align: center; margin: 28px 24px 0;
    font-size: 17px; line-height: 1.55; color: #c4b8a0; font-style: italic;
  }
  #overlay-title button, .menu-btn {
    margin-top: 36px; pointer-events: auto; cursor: pointer;
    background: transparent; color: var(--gold);
    border: 1px solid var(--gold-dim);
    font-family: Cinzel, serif; letter-spacing: 0.34em; font-size: 13px;
    padding: 12px 34px; transition: background 0.2s, box-shadow 0.2s;
  }
  #overlay-title button:hover, .menu-btn:hover {
    background: #e0b45a18; box-shadow: 0 0 24px #e0b45a33;
  }
  #end-screen {
    position: absolute; inset: 0; display: none; align-items: center; justify-content: center;
    flex-direction: column; background: radial-gradient(ellipse, #1c1810ee, #07090df6);
    z-index: 25; text-align: center; padding: 24px;
  }
  #end-screen.show { display: flex; }
  #end-screen h1 {
    font-family: Cinzel, serif; letter-spacing: 0.28em; color: var(--gold);
    font-size: clamp(26px, 4vw, 44px); text-shadow: 0 0 50px #e0b45a66;
  }
  #end-screen p { max-width: 520px; margin-top: 20px; font-size: 18px; font-style: italic; color: #d4c8b0; line-height: 1.5; }
  #dmg-flash {
    position: absolute; inset: 0; pointer-events: none;
    background: radial-gradient(ellipse at center, transparent 40%, #c94a4a55 100%);
    opacity: 0; transition: opacity 0.08s;
  }
  #shimmer {
    position: absolute; inset: 0; pointer-events: none;
    box-shadow: inset 0 0 80px 20px #e0b45a33;
    opacity: 0; transition: opacity 0.4s;
  }
  #death {
    position: absolute; inset: 0; display: none; align-items: center; justify-content: center;
    flex-direction: column; background: #0a0608ee; z-index: 18;
  }
  #death.show { display: flex; }
  #death h2 { font-family: Cinzel, serif; letter-spacing: 0.4em; color: #c94a4a; font-size: 28px; }
  #death p { margin-top: 12px; color: #b8a898; letter-spacing: 0.12em; }
</style>
</head>
<body>
<div id="wrap">
  <canvas id="c"></canvas>
  <div id="hud">
    <div id="hud-top">
      <div id="hearts"></div>
      <div id="meta">
        <div class="gold" id="gold-lbl">0 ✦</div>
        <div class="zone" id="zone-lbl">THE ASHEN THRESHOLD</div>
        <div class="charges" id="charge-lbl"></div>
      </div>
    </div>
    <div id="prompt"></div>
    <div id="toast"><div class="title"></div><div class="sub"></div></div>
    <div id="log"></div>
    <div id="controls-hint">WASD MOVE · J / CLICK SWING · K SHARD<br>E INTERACT · I INVENTORY · ESC PAUSE</div>
  </div>
  <div id="dmg-flash"></div>
  <div id="shimmer"></div>
  <div id="inv-panel">
    <div class="panel">
      <h2>WARDEN'S SATCHEL</h2>
      <div class="slots" id="inv-slots"></div>
      <div class="foot">CLICK TO USE / TOGGLE · I OR ESC TO CLOSE</div>
    </div>
  </div>
  <div id="shop-panel">
    <div class="panel">
      <h2>MERCHANT ALTAR</h2>
      <div class="slots" id="shop-slots"></div>
      <div class="foot">CLICK TO PURCHASE · ESC TO LEAVE</div>
    </div>
  </div>
  <div id="pause-panel">
    <div class="panel" style="text-align:center">
      <h2>SUSPENDED</h2>
      <p style="font-style:italic;color:#b8a898;margin-bottom:8px">The Observatory holds its breath.</p>
      <button class="menu-btn" id="btn-resume">RESUME</button>
      <div class="foot" style="margin-top:20px">ESC TO RETURN</div>
    </div>
  </div>
  <div id="death">
    <h2>CORRODED</h2>
    <p>The Sanctuary Stone recalls you…</p>
  </div>
  <div id="end-screen">
    <h1>THE BEACON IGNITES</h1>
    <p>Light pours through the Sunken Observatory. The Corrosion recedes into the storm. The Last Warden stands watch once more.</p>
    <button class="menu-btn" id="btn-again" style="margin-top:28px">BEGIN ANEW</button>
  </div>
  <div id="overlay-title">
    <svg class="crest" viewBox="0 0 72 72" fill="none">
      <circle cx="36" cy="36" r="30" stroke="#e0b45a" stroke-width="1.2" opacity="0.7"/>
      <path d="M36 10 L40 28 L58 28 L44 38 L50 56 L36 46 L22 56 L28 38 L14 28 L32 28 Z" fill="#e0b45a" opacity="0.85"/>
    </svg>
    <h1>THE WARDEN'S CORROSION</h1>
    <div class="subhead">Aurelion's Sunken Observatory</div>
    <p class="lore">The Core Beacon gutters. Black-iron decay climbs the bronze halls. You are the Last Warden — the only guardian left who can reignite the light before the structure falls into the storm.</p>
    <button id="btn-begin">TAKE UP THE BLADE</button>
  </div>
</div>
<script>
window.__G = {};
(() => {
"use strict";

const canvas = document.getElementById("c");
const ctx = canvas.getContext("2d");
const T = 40;
const COL = {
  gold: "#e0b45a", ember: "#e07030", slate: "#1c2836", floor: "#243040",
  wall: "#3a2c22", rust: "#6a3a22", mist: "#2a4050", deep: "#121820"
};

let W = 1280, H = 720, dpr = 1;
function resize() {
  const wrap = document.getElementById("wrap");
  W = wrap.clientWidth; H = wrap.clientHeight;
  dpr = Math.min(2, window.devicePixelRatio || 1);
  canvas.width = W * dpr; canvas.height = H * dpr;
  canvas.style.width = W + "px"; canvas.style.height = H + "px";
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
window.addEventListener("resize", resize);
resize();

// ─── Audio ───────────────────────────────────────────────
const AudioSys = (() => {
  let ac = null, master, ambGain, footT = 0;
  const ensure = () => {
    if (ac) return ac;
    ac = new (window.AudioContext || window.webkitAudioContext)();
    master = ac.createGain(); master.gain.value = 0.28; master.connect(ac.destination);
    ambGain = ac.createGain(); ambGain.gain.value = 0.07; ambGain.connect(master);
    return ac;
  };
  const env = (g, t, a, s, r) => {
    g.gain.setValueAtTime(0, t);
    g.gain.linearRampToValueAtTime(s, t + a);
    g.gain.exponentialRampToValueAtTime(0.0001, t + a + r);
  };
  const tone = (f, dur, type, vol, atk=0.01, slide=0) => {
    const a = ensure(), t = a.currentTime;
    const o = a.createOscillator(), g = a.createGain();
    o.type = type; o.frequency.setValueAtTime(f, t);
    if (slide) o.frequency.exponentialRampToValueAtTime(Math.max(20, f + slide), t + dur);
    g.gain.value = 0; env(g, t, atk, vol, dur);
    o.connect(g); g.connect(master); o.start(t); o.stop(t + dur + 0.05);
  };
  const noise = (dur, vol, bp, q=4) => {
    const a = ensure(), t = a.currentTime, n = a.sampleRate * dur;
    const buf = a.createBuffer(1, n, a.sampleRate);
    const d = buf.getChannelData(0);
    for (let i = 0; i < n; i++) d[i] = Math.random() * 2 - 1;
    const src = a.createBufferSource(); src.buffer = buf;
    const f = a.createBiquadFilter(); f.type = "bandpass"; f.frequency.value = bp; f.Q.value = q;
    const g = a.createGain(); env(g, t, 0.005, vol, dur);
    src.connect(f); f.connect(g); g.connect(master); src.start(t);
  };
  return {
    resume() { ensure().resume(); },
    swingBase() { noise(0.12, 0.35, 800, 1.2); tone(220, 0.1, "triangle", 0.15, 0.005, -80); },
    swingFire() { noise(0.16, 0.3, 500, 0.8); tone(180, 0.14, "sawtooth", 0.1, 0.01, -40); tone(420, 0.2, "triangle", 0.08, 0.02, 80); },
    swingSolar() { tone(90, 0.28, "sine", 0.28, 0.06, -20); tone(180, 0.22, "triangle", 0.16, 0.04); noise(0.2, 0.25, 300, 0.6); },
    impact() { noise(0.08, 0.4, 200, 0.8); tone(90, 0.1, "sine", 0.2); },
    impactFire() { noise(0.12, 0.35, 350, 0.7); tone(140, 0.16, "sawtooth", 0.12, 0.01, -60); },
    impactSolar() { tone(70, 0.3, "sine", 0.32, 0.02); tone(220, 0.18, "triangle", 0.12); noise(0.15, 0.3, 180, 0.5); },
    twang() { tone(880, 0.18, "triangle", 0.18, 0.005, -200); tone(1320, 0.12, "sine", 0.08, 0.005, -400); },
    chimeHit() { tone(1480, 0.25, "sine", 0.1, 0.005, -100); },
    hurt() { noise(0.14, 0.45, 160, 0.6); tone(310, 0.16, "square", 0.08, 0.005, -180); tone(980, 0.1, "sine", 0.07); },
    pickup() { tone(523, 0.12, "sine", 0.14, 0.005, 80); tone(784, 0.18, "sine", 0.1, 0.01); },
    rare() { tone(392, 0.15, "sine", 0.14); tone(523, 0.2, "sine", 0.12, 0.04); tone(784, 0.28, "triangle", 0.1, 0.08); },
    coin() { tone(980, 0.08, "square", 0.08, 0.002); tone(1310, 0.1, "triangle", 0.07, 0.02); },
    shopNear() { tone(110, 0.6, "sine", 0.05, 0.2, 10); },
    buy() { tone(330, 0.08, "triangle", 0.12); tone(660, 0.14, "sine", 0.1, 0.04); },
    save() { tone(261, 0.3, "sine", 0.12, 0.05); tone(329, 0.4, "sine", 0.1, 0.1); tone(392, 0.5, "triangle", 0.08, 0.15); },
    door() { noise(0.3, 0.3, 120, 0.5); tone(80, 0.35, "sine", 0.15); },
    break() { noise(0.22, 0.4, 280, 0.7); tone(100, 0.15, "sawtooth", 0.1, 0.005, -50); },
    plate() { tone(180, 0.12, "triangle", 0.1); noise(0.08, 0.15, 400, 2); },
    switchHit() { tone(640, 0.2, "sine", 0.14, 0.005); tone(960, 0.25, "triangle", 0.08, 0.04); },
    altar() { tone(196, 0.4, "sine", 0.14, 0.08); tone(392, 0.5, "triangle", 0.1, 0.12); tone(588, 0.7, "sine", 0.08, 0.2); },
    freeze() { tone(740, 0.3, "sine", 0.12, 0.01, -200); tone(1480, 0.2, "triangle", 0.06); },
    foot(kind, now) {
      if (now - footT < 0.28) return; footT = now;
      if (kind === "grate") { noise(0.06, 0.18, 700, 3); tone(180, 0.05, "square", 0.03); }
      else if (kind === "wet") { noise(0.08, 0.14, 400, 1.4); tone(140, 0.07, "sine", 0.04); }
      else { noise(0.05, 0.12, 220, 1.1); }
    },
    amb(zone) {
      if (!ac) return;
      // simple drifting pad
    },
    boom() { tone(55, 0.6, "sine", 0.35, 0.02, -10); noise(0.4, 0.4, 90, 0.4); }
  };
})();

// ─── Particles ───────────────────────────────────────────
const P = [];
function burst(x, y, n, cols, spd, life, size, grav=0) {
  for (let i = 0; i < n; i++) {
    const a = Math.random() * Math.PI * 2, s = Math.random() * spd;
    P.push({
      x, y, vx: Math.cos(a)*s, vy: Math.sin(a)*s,
      life, max: life, col: cols[i % cols.length],
      size: size * (0.5 + Math.random()), grav, type: 0
    });
  }
}
function emberTrail(x, y) {
  P.push({
    x: x + (Math.random()-0.5)*8, y: y + (Math.random()-0.5)*8,
    vx: (Math.random()-0.5)*12, vy: -20 - Math.random()*20,
    life: 0.4 + Math.random()*0.3, max: 0.7, col: Math.random()>0.5 ? "#e07030" : "#f0b060",
    size: 2 + Math.random()*2, grav: 30, type: 1
  });
}
function goldDust(x, y) {
  for (let i = 0; i < 14; i++) {
    P.push({
      x, y, vx: (Math.random()-0.5)*50, vy: -20 - Math.random()*40,
      life: 0.6 + Math.random()*0.4, max: 1, col: "#e0b45a",
      size: 2 + Math.random()*2, grav: 40, type: 1
    });
  }
}
function whiteMotes(x, y) {
  for (let i = 0; i < 18; i++) {
    P.push({
      x, y, vx: (Math.random()-0.5)*30, vy: -30 - Math.random()*50,
      life: 0.8 + Math.random()*0.5, max: 1.3, col: i%2?"#fff8e0":"#c8e8ff",
      size: 2 + Math.random()*3, grav: 10, type: 1
    });
  }
}
function tickP(dt) {
  for (let i = P.length - 1; i >= 0; i--) {
    const p = P[i];
    p.life -= dt; p.x += p.vx * dt; p.y += p.vy * dt; p.vy += p.grav * dt;
    if (p.life <= 0) P.splice(i, 1);
  }
}
function drawP(cam) {
  for (const p of P) {
    const a = Math.max(0, p.life / p.max);
    ctx.globalAlpha = a;
    ctx.fillStyle = p.col;
    ctx.beginPath();
    ctx.arc(p.x - cam.x, p.y - cam.y, p.size * (0.5 + a*0.5), 0, Math.PI*2);
    ctx.fill();
  }
  ctx.globalAlpha = 1;
}

// ─── Input ───────────────────────────────────────────────
const keys = Object.create(null);
const mouse = { x: 0, y: 0, down: false, worldX: 0, worldY: 0, moved: false };
window.addEventListener("keydown", e => {
  keys[e.code] = true;
  if (["Space","ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(e.code)) e.preventDefault();
  onKey(e);
});
window.addEventListener("keyup", e => { keys[e.code] = false; });
canvas.addEventListener("mousemove", e => {
  const r = canvas.getBoundingClientRect();
  mouse.x = e.clientX - r.left; mouse.y = e.clientY - r.top; mouse.moved = true;
});
canvas.addEventListener("mousedown", e => { mouse.down = true; if (e.button === 0) tryMelee(); });
window.addEventListener("mouseup", () => { mouse.down = false; });

// ─── Game State ──────────────────────────────────────────
const S = {
  started: false, paused: false, over: false, invOpen: false, shopOpen: false,
  zone: 0,
  gold: 0,
  hp: 6, maxHp: 6, hearts: 3,
  weapon: 0, // 0 base, 1 ignited, 2 solar
  hasShard: false, charges: 0, maxCharges: 12,
  items: {
    band: false, lantern: false, cloak: false,
    stems: 0, seals: 0
  },
  equip: { band: false, lantern: false, cloak: false },
  flags: {
    save1: false, save2: false, save3: false,
    shopUnlocked: false,
    heart4: false, heart5: false,
    bandGot: false, lanternGot: false, cloakGot: false,
    shardGot: false, w1: false, w2: false,
    galleryBoss: false, spireGuard: false, coreDead: false,
    wallA: false, wallB: false, wallC: false, wallD: false, wallE: false,
    plateA: false, plateB: false, plateC: false,
    switchA: false, switchB: false, switchC: false, switchD: false, switchE: false,
    chestBand: false, chestGold5: false, jarSeal: false, cacheStem: false,
    door2: false, door3: false, door4: false, door5: false, door6: false,
    galleryBridge: false, vaultGate: false, spireGate: false,
    ammoShop: false, boughtHeart: false
  },
  shopStock: { stem: 3, seal: 2, heart: 1 },
  lastSave: { zone: 0, x: 0, y: 0 },
  msg: "", prompt: "",
  freezeT: 0
};

const ZN = [
  { id: 0, name: "THE ASHEN THRESHOLD", floor: "stone", amb: "dust" },
  { id: 1, name: "THE RUSTED ARCHIVES", floor: "grate", amb: "dust" },
  { id: 2, name: "THE COLLAPSED GALLERY", floor: "stone", amb: "dust" },
  { id: 3, name: "THE SUNKEN VAULT", floor: "wet", amb: "mist" },
  { id: 4, name: "THE TWIN SPIRE", floor: "stone", amb: "wind" },
  { id: 5, name: "THE CORE CHAMBER", floor: "stone", amb: "core" }
];

// ─── Maps ────────────────────────────────────────────────
// # wall  . floor  , grate  ~ wet  = bronze
// + breakable  P plate  ^ save  $ shop  A altar
// X ranged switch  D next-door  H hazard  B pot
// C chest  G gold  ? faint crack (lantern)
const MAPS = [];
const ENT = []; // per-zone entity lists built from maps + extras

function parseMap(str) {
  const rows = str.trim().split("\n").map(r => r.replace(/^\s+/, ""));
  const h = rows.length, w = rows[0].length;
  const tiles = [];
  const ents = [];
  for (let y = 0; y < h; y++) {
    tiles[y] = [];
    const row = rows[y].padEnd(w, "#");
    for (let x = 0; x < w; x++) {
      const ch = row[x] || "#";
      let t = 1;
      if (ch === "." || ch === " " ) t = 0;
      else if (ch === ",") t = 2;
      else if (ch === "~") t = 3;
      else if (ch === "=") t = 4;
      else if (ch === "#") t = 1;
      else if (ch === "+") { t = 5; }
      else if (ch === "?") { t = 6; }
      else { t = 0; }
      tiles[y][x] = t;
      const px = x * T + T/2, py = y * T + T/2;
      if (ch === "^") ents.push({ kind: "save", x: px, y: py });
      if (ch === "$") ents.push({ kind: "shop", x: px, y: py });
      if (ch === "A") ents.push({ kind: "altar", x: px, y: py });
      if (ch === "P") ents.push({ kind: "plate", x: px, y: py, id: "auto" });
      if (ch === "X") ents.push({ kind: "switch", x: px, y: py, id: "auto" });
      if (ch === "D") ents.push({ kind: "door", x: px, y: py });
      if (ch === "H") ents.push({ kind: "hazard", x: px, y: py });
      if (ch === "B") ents.push({ kind: "pot", x: px, y: py });
      if (ch === "C") ents.push({ kind: "chest", x: px, y: py });
      if (ch === "G") ents.push({ kind: "cache", x: px, y: py });
      if (ch === "E") ents.push({ kind: "enemy", x: px, y: py, type: "construct" });
      if (ch === "N") ents.push({ kind: "enemy", x: px, y: py, type: "sentinel" });
      if (ch === "M") ents.push({ kind: "enemy", x: px, y: py, type: "brute" });
      if (ch === "K") ents.push({ kind: "beacon", x: px, y: py });
    }
  }
  return { w, h, tiles, ents };
}

MAPS[0] = parseMap(`
########################################
#......................................#
#..^...................................#
#......................................#
#..........E..............B............#
#......................................#
#....................E.................#
#......####................####........#
#......#..#........B.......#..#........#
#......#..+................#..#........#
#......................................#
#.................E....................#
#......................................#
#..............................E.......#
#......................................#
#...............................D......#
#......................................#
#......................................#
########################################
`);

MAPS[1] = parseMap(`
########################################
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
#,,D,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,D,,#
#,,,,,,,,,,,,,,,,,,E,,,,,,,,,,,,,,,,,,,#
#,,,,####,,,,,,,,,,,,,,,,,,,,####,,,,,,#
#,,,,#,,#,,,,,,,,B,,,,,,,,,,,#,C#,,,,,,#
#,,,,#,,+,,,,,,,,,,,,,,,,,,,,#,,#,,,,,,#
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
#,,,,,,,,,,E,,,,,,,,,,,,E,,,,,,,,,,,,,,#
#,,,,,,,,,,,,,,,,,,,,^,,,,,,,,,,,,,,,,,#
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
#,,,,####,,,,,,,,,,,,,,,,,,,,####,,,,,,#
#,,,,#,,#,,,,,,,,,,,,,,,,,,,,#,,#,,,,,,#
#,,,,#G,#,,,,,,,,,N,,,,,,,,,,#,,#,,,,,,#
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
#,,,,,,,,E,,,,,,,,,,,,,,B,,,,,,,E,,,,,,#
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
#,,,,,,,,,,,,,,,$,,,,,,,,,,,,,,,,,,,,,,#
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
########################################
`);

MAPS[2] = parseMap(`
##########################################
#........................................#
#..D..................................D..#
#........................................#
#.....P......................P...........#
#........................................#
#...........############.................#
#...........#..........#......E..........#
#...........#....A.....#.................#
#.....E.....#..........#.................#
#...........#..........#........M........#
#...........######+#####.................#
#........................................#
#.................E......................#
#....####....................####........#
#....#B.#....................#.G#........#
#....#..+....................#..#........#
#........................................#
#........................................#
##########################################
`);

MAPS[3] = parseMap(`
##########################################
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
#~~D~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~D~~#
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
#~~~~~####~~~~~~~~~~~~~~~~~~~~X~~~~~~~~~~#
#~~~~~#~~#~~~~~~~~~~~~~~E~~~~~~~~~~~~~~~~#
#~~~~~#~C?~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
#~~~~~~~~~~E~~~~~~~~~~~~~####~~~~~~~~~~~~#
#~~~~~~~~~~~~~~~~~~~~~~~~#~~#~~~~~E~~~~~~#
#~~~~~P~~~~~~~~~~~~~~~~~~#~A#~~~~~~~~~~~~#
#~~~~~~~~~~~~~~~~~~~~~~~~#~~#~~~~~~~~~~~~#
#~~~~~~~~~~~~~~~N~~~~~~~~####~~~~~~~~~~~~#
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
#~~~~~####~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
#~~~~~#~G#~~~~~~~~~~B~~~~~~~~~E~~~~~~~~~~#
#~~~~~#~~+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~X~~~#
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
##########################################
`);

MAPS[4] = parseMap(`
##########################################
#........................................#
#..^..................................D..#
#........................................#
#.....H....E......H...........E.....H....#
#........................................#
#...........############.................#
#...........#..........#.................#
#.....N.....#....A.....#........N........#
#...........#..........#.................#
#...........######+#####.................#
#........................................#
#..X..............................C......#
#........................................#
#.....####......M..............####......#
#.....#.B#.....................#.G#......#
#.....#..+.....................#..#......#
#........................................#
#.................H......................#
##########################################
`);

MAPS[5] = parseMap(`
######################################
#====================================#
#==D=================================#
#====================================#
#==========E========E================#
#====================================#
#==============####==================#
#==============#KK#==================#
#=======N======#KK#======N===========#
#==============####==================#
#====================================#
#==========E========E================#
#====================================#
#================M===================#
#====================================#
#====================================#
######################################
`);

// Assign specific IDs / loot after parse
function decorate() {
  // Zone 0
  const z0 = MAPS[0];
  z0.ents.filter(e => e.kind === "save")[0].sid = 1;
  z0.ents.filter(e => e.kind === "door")[0].to = 1;
  z0.ents.filter(e => e.kind === "pot").forEach((e,i) => e.loot = i===0 ? "gold" : "gold");
  // hidden wall gold
  const bw0 = findTiles(z0, 5)[0];
  if (bw0) z0.breakLoot = z0.breakLoot || [];
  // Zone 1
  const z1 = MAPS[1];
  z1.ents.filter(e => e.kind === "save")[0].sid = 2;
  const doors1 = z1.ents.filter(e => e.kind === "door");
  doors1[0].to = 0; doors1[0].back = true;
  doors1[1].to = 2;
  z1.ents.filter(e => e.kind === "chest")[0].loot = "band";
  z1.ents.filter(e => e.kind === "cache")[0].loot = "stem";
  // Zone 2
  const z2 = MAPS[2];
  const doors2 = z2.ents.filter(e => e.kind === "door");
  doors2[0].to = 1; doors2[0].back = true;
  doors2[1].to = 3;
  z2.ents.filter(e => e.kind === "altar")[0].up = 1;
  const plates2 = z2.ents.filter(e => e.kind === "plate");
  plates2[0].id = "gA"; plates2[1].id = "gB";
  z2.ents.filter(e => e.kind === "cache")[0].loot = "gold";
  z2.ents.filter(e => e.kind === "enemy").find(e => e.type === "brute").isMini = true;
  // Zone 3
  const z3 = MAPS[3];
  const doors3 = z3.ents.filter(e => e.kind === "door");
  doors3[0].to = 2; doors3[0].back = true;
  doors3[1].to = 4;
  z3.ents.filter(e => e.kind === "chest")[0].loot = "shard";
  z3.ents.filter(e => e.kind === "altar")[0].up = "lantern";
  z3.ents.filter(e => e.kind === "plate")[0].id = "vA";
  const sw3 = z3.ents.filter(e => e.kind === "switch");
  sw3[0].id = "v1"; sw3[1].id = "v2";
  z3.ents.filter(e => e.kind === "cache")[0].loot = "gold";
  // Zone 4
  const z4 = MAPS[4];
  z4.ents.filter(e => e.kind === "save")[0].sid = 3;
  z4.ents.filter(e => e.kind === "door")[0].to = 5;
  z4.ents.filter(e => e.kind === "altar")[0].up = 2;
  z4.ents.filter(e => e.kind === "chest")[0].loot = "cloak";
  z4.ents.filter(e => e.kind === "cache")[0].loot = "goldbig";
  z4.ents.filter(e => e.kind === "switch")[0].id = "h5";
  z4.ents.filter(e => e.kind === "pot")[0].loot = "seal";
  z4.ents.filter(e => e.kind === "enemy").filter(e => e.type === "brute")[0].isGuard = true;
  z4.ents.push({ kind: "door", x: 2.5 * T, y: 4.5 * T, to: 3, back: true });
  // heart-5 switch is ranged-only; tag it
  const hsw = z4.ents.find(e => e.kind === "switch" && e.id === "h5");
  if (hsw) hsw.ranged = true;
  // Zone 5
  const z5 = MAPS[5];
  z5.ents.filter(e => e.kind === "door")[0].to = 4; z5.ents.filter(e => e.kind === "door")[0].back = true;
  z5.ents.filter(e => e.kind === "enemy").filter(e => e.type === "brute")[0].isCore = true;
}
function findTiles(map, type) {
  const out = [];
  for (let y = 0; y < map.h; y++)
    for (let x = 0; x < map.w; x++)
      if (map.tiles[y][x] === type) out.push({x,y});
  return out;
}
decorate();

// ─── Player ──────────────────────────────────────────────
const player = {
  x: 4.5 * T, y: 3.5 * T, vx: 0, vy: 0, r: 12,
  facing: 0, speed: 145,
  atkT: 0, atkWind: 0, swinging: false, swingA: 0,
  invuln: 0, flash: 0, bob: 0,
  shootT: 0
};

function spawnInZone(z, goingBack) {
  S.zone = z;
  const m = MAPS[z];
  const doors = m.ents.filter(e => e.kind === "door");
  if (goingBack) {
    // arrived from a later zone — stand at this zone's forward door
    const d = doors.find(e => !e.back) || doors[doors.length - 1];
    if (d) {
      player.x = d.x + (d.back ? 36 : -36);
      player.y = d.y + 8;
    }
  } else {
    // arrived from an earlier zone — stand at the entrance (back door)
    const d = doors.find(e => e.back);
    if (d) { player.x = d.x + 40; player.y = d.y; }
    else { player.x = 4.5 * T; player.y = 3.5 * T; }
  }
  player.vx = player.vy = 0;
  document.getElementById("zone-lbl").textContent = ZN[z].name;
  toast(ZN[z].name, zoneFlavor(z));
  rebuildEnemies(z);
}

function zoneFlavor(z) {
  return [
    "Bronze dust hangs in the dying light.",
    "Ledgers rust. The merchant altar hums.",
    "A collapsed nave. An altar waits, cold.",
    "Mist rises from drowned star-charts.",
    "Twin towers pierce the storm-lock.",
    "The Core Beacon flickers. End this."
  ][z];
}

// ─── Enemies ─────────────────────────────────────────────
let enemies = [];
let projs = [];
let floats = [];

function rebuildEnemies(z) {
  enemies = [];
  const m = MAPS[z];
  for (const e of m.ents) {
    if (e.kind !== "enemy") continue;
    if (e.isMini && S.flags.galleryBoss) continue;
    if (e.isGuard && S.flags.spireGuard) continue;
    if (e.isCore && S.flags.coreDead) continue;
    enemies.push(makeEnemy(e));
  }
}

function makeEnemy(e) {
  const t = e.type;
  const base = {
    construct: { hp: 4, spd: 48, dmg: 1, r: 13, gold: [6,12], name: "Corrupted Construct", color: "#6a5344" },
    sentinel: { hp: 6, spd: 72, dmg: 1, r: 12, gold: [8,14], name: "Rusted Sentinel", color: "#7a4030" },
    brute: { hp: 16, spd: 40, dmg: 2, r: 18, gold: [20,30], name: "Iron Brute", color: "#4a3028" }
  }[t];
  let hp = base.hp + S.zone;
  if (e.isMini) { hp = 18; }
  if (e.isGuard) { hp = 26; }
  if (e.isCore) { hp = 42; base.r = 22; base.name = "Corroded Warden"; }
  return {
    ...e, ...base, hp, maxHp: hp, alive: true,
    vx: 0, vy: 0, hitT: 0, atkT: 0, stun: 0, burn: 0, frozen: 0,
    ang: 0, wander: Math.random()*6, phase: 0
  };
}

function atkDmg() { return S.weapon === 0 ? 1 : S.weapon === 1 ? 2 : 3; }
function atkCD() {
  let c = S.weapon === 0 ? 0.42 : S.weapon === 1 ? 0.30 : 0.55;
  if (S.equip.band) c *= 0.82;
  return c;
}
function atkRange() { return S.weapon === 2 ? 64 : 48; }
function atkArc() { return S.weapon === 2 ? 2.6 : 2.0; }

function aimAtPointer() {
  if (!mouse.moved) return;
  const dx = mouse.worldX - player.x, dy = mouse.worldY - player.y;
  if (Math.hypot(dx, dy) > 4) player.facing = Math.atan2(dy, dx);
}
function tryMelee() {
  if (!S.started || S.paused || S.over || S.invOpen || S.shopOpen) return;
  if (player.atkT > 0 || player.atkWind > 0) return;
  aimAtPointer();
  if (S.weapon === 2) {
    player.atkWind = 0.22;
    AudioSys.swingSolar();
  } else {
    startSwing();
  }
}
function startSwing() {
  player.swinging = true;
  player.swingA = 0;
  player.atkT = atkCD();
  if (S.weapon === 0) AudioSys.swingBase();
  else if (S.weapon === 1) AudioSys.swingFire();
  else AudioSys.swingSolar();
  // hit detection
  const ang = player.facing;
  const range = atkRange();
  const arc = atkArc();
  let hit = false;
  for (const en of enemies) {
    if (!en.alive || en.frozen > 0 && false) {}
    if (!en.alive) continue;
    const dx = en.x - player.x, dy = en.y - player.y;
    const d = Math.hypot(dx, dy);
    if (d > range + en.r) continue;
    const a = Math.atan2(dy, dx);
    let da = Math.abs(normAng(a - ang));
    if (da > arc / 2) continue;
    hurtEnemy(en, atkDmg(), a);
    hit = true;
  }
  // breakable walls
  hitWalls(player.x + Math.cos(ang)*28, player.y + Math.sin(ang)*28, S.weapon === 2 ? 2 : 1);
  // pots
  hitPots(player.x + Math.cos(ang)*30, player.y + Math.sin(ang)*30);
  if (S.weapon === 2) {
    S.shimmer = 0.45;
    document.getElementById("shimmer").style.opacity = "1";
    setTimeout(() => document.getElementById("shimmer").style.opacity = "0", 180);
  }
}

function tryShoot() {
  if (!S.hasShard || S.charges <= 0) return;
  if (player.shootT > 0) return;
  if (!S.started || S.paused || S.over || S.invOpen || S.shopOpen) return;
  aimAtPointer();
  S.charges--;
  player.shootT = 0.38;
  const a = player.facing;
  projs.push({
    x: player.x + Math.cos(a)*16, y: player.y + Math.sin(a)*16,
    vx: Math.cos(a)*340, vy: Math.sin(a)*340,
    life: 1.1, r: 5, friendly: true
  });
  AudioSys.twang();
  updateHUD();
}

function hurtEnemy(en, dmg, ang) {
  if (en.frozen > 0) dmg += 1;
  en.hp -= dmg;
  en.hitT = 0.15;
  en.stun = 0.18;
  en.vx += Math.cos(ang) * 80;
  en.vy += Math.sin(ang) * 80;
  if (S.weapon === 1) { en.burn = 1.4; AudioSys.impactFire(); burst(en.x, en.y, 10, ["#e07030","#f0b060","#802000"], 80, 0.35, 3); }
  else if (S.weapon === 2) { AudioSys.impactSolar(); burst(en.x, en.y, 14, ["#ffe08a","#e0b45a","#fff"], 100, 0.4, 3); }
  else { AudioSys.impact(); burst(en.x, en.y, 8, ["#c0a070","#888"], 70, 0.3, 2); }
  floatTxt(en.x, en.y - 16, ""+dmg, "#f0d080");
  if (en.hp <= 0) killEnemy(en);
}

function killEnemy(en) {
  en.alive = false;
  const g = en.gold[0] + ((Math.random() * (en.gold[1]-en.gold[0]))|0);
  S.gold += g;
  goldDust(en.x, en.y);
  AudioSys.coin();
  floatTxt(en.x, en.y, "+"+g+" ✦", "#e0b45a");
  log("The " + en.name + " collapses. " + g + " sigils.");
  if (en.isMini) {
    S.flags.galleryBoss = true;
    if (!S.flags.heart4) grantHeart(4);
  }
  if (en.isGuard) {
    S.flags.spireGuard = true;
    if (!S.flags.cloakGot) {
      S.flags.cloakGot = true; S.items.cloak = true;
      whiteMotes(en.x, en.y); AudioSys.rare();
      toast("SHADOW CLOAK", "Dark cloth settles on your shoulders.");
      log("The Spire Guardian yields a Shadow Cloak.");
    }
  }
  if (en.isCore) {
    S.flags.coreDead = true;
    AudioSys.boom();
    burst(en.x, en.y, 40, ["#e0b45a","#fff","#e07030"], 160, 1.2, 5, 20);
    setTimeout(() => igniteBeacon(), 800);
  }
  updateHUD();
}

function hurtPlayer(dmg, src) {
  if (player.invuln > 0 || S.over) return;
  if (src === "hazard" && S.equip.cloak) dmg = Math.max(1, dmg - 1);
  S.hp = Math.max(0, S.hp - dmg);
  player.invuln = 0.85;
  player.flash = 0.2;
  AudioSys.hurt();
  burst(player.x, player.y, 12, ["#c94a4a","#802020"], 90, 0.35, 3);
  floatTxt(player.x, player.y - 18, "-"+dmg, "#ff6a6a");
  const fl = document.getElementById("dmg-flash");
  fl.style.opacity = "1";
  setTimeout(() => fl.style.opacity = "0", 120);
  updateHUD();
  if (S.hp <= 0) die();
}

function die() {
  document.getElementById("death").classList.add("show");
  AudioSys.boom();
  setTimeout(() => {
    document.getElementById("death").classList.remove("show");
    S.zone = S.lastSave.zone;
    player.x = S.lastSave.x; player.y = S.lastSave.y;
    S.hp = S.maxHp;
    player.invuln = 1.2;
    document.getElementById("zone-lbl").textContent = ZN[S.zone].name;
    rebuildEnemies(S.zone);
    updateHUD();
    log("You wake at the Sanctuary Stone.");
  }, 1600);
}

function grantHeart(n) {
  if (n === 4 && S.flags.heart4) return;
  if (n === 5 && S.flags.heart5) return;
  if (n === 4) S.flags.heart4 = true;
  if (n === 5) S.flags.heart5 = true;
  S.hearts = Math.min(5, S.hearts + 1);
  S.maxHp = S.hearts * 2;
  S.hp = Math.min(S.maxHp, S.hp + 2);
  document.getElementById("shimmer").style.opacity = "1";
  setTimeout(() => document.getElementById("shimmer").style.opacity = "0", 2000);
  whiteMotes(player.x, player.y);
  burst(player.x, player.y, 20, ["#ffe08a","#fff"], 60, 0.8, 3, -20);
  AudioSys.altar();
  toast("HEART CONTAINER", "A new vessel of light binds to you.");
  updateHUD();
}

function igniteBeacon() {
  S.over = true;
  document.getElementById("end-screen").classList.add("show");
  AudioSys.altar();
}

// ─── Combat helpers ──────────────────────────────────────
function hitWalls(x, y, power) {
  const m = MAPS[S.zone];
  const tx = Math.floor(x / T), ty = Math.floor(y / T);
  for (let iy = ty-1; iy <= ty+1; iy++)
    for (let ix = tx-1; ix <= tx+1; ix++) {
      if (!inMap(m, ix, iy)) continue;
      const t = m.tiles[iy][ix];
      if (t === 5 || t === 6) {
        // breakable
        const key = wallKey(S.zone, ix, iy);
        if (S.flags[key]) continue;
        // some walls need more hits — store hp on map
        m.whp = m.whp || {};
        const k = ix+","+iy;
        m.whp[k] = (m.whp[k] || (t===6?6:4)) - power;
        burst(ix*T+T/2, iy*T+T/2, 6, ["#5a4030","#888"], 50, 0.3, 2);
        AudioSys.break();
        if (m.whp[k] <= 0) {
          S.flags[key] = true;
          m.tiles[iy][ix] = S.zone === 3 ? 3 : 0;
          onWallBreak(S.zone, ix, iy);
        }
      }
    }
}
function wallKey(z, x, y) { return "W_"+z+"_"+x+"_"+y; }
function onWallBreak(z, x, y) {
  log("The corroded masonry gives way.");
  if (z === 2 && !S.flags.heart4) {
    // only the cracked seal under the gallery altar
    const m = MAPS[2];
    if (m.tiles[y] && y >= 10 && y <= 12) grantHeart(4);
  }
  if (z === 0) {
    S.gold += 12; goldDust(x * T + T / 2, y * T + T / 2); AudioSys.coin();
  }
}
function hitPots(x, y) {
  const m = MAPS[S.zone];
  for (const e of m.ents) {
    if (e.kind !== "pot" || e.broke) continue;
    if (Math.hypot(e.x - x, e.y - y) < 34 || Math.hypot(e.x - player.x, e.y - player.y) < 26) {
      e.broke = true;
      AudioSys.break();
      burst(e.x, e.y, 10, ["#8a7050","#c0a070"], 60, 0.3, 2);
      if (e.loot === "seal" && !S.flags.jarSeal) {
        S.flags.jarSeal = true; S.items.seals++;
        whiteMotes(e.x, e.y); AudioSys.rare();
        toast("BINDING SEAL", "A cold blue glyph, waiting to be spent.");
      } else {
        const g = 5 + (Math.random()*8|0);
        S.gold += g; goldDust(e.x, e.y); AudioSys.coin();
        floatTxt(e.x, e.y, "+"+g+" ✦", "#e0b45a");
      }
      updateHUD();
    }
  }
}

function inMap(m, x, y) { return y>=0 && x>=0 && y<m.h && x<m.w; }
function tileAt(wx, wy) {
  const m = MAPS[S.zone];
  const x = Math.floor(wx / T), y = Math.floor(wy / T);
  if (!inMap(m, x, y)) return 1;
  const t = m.tiles[y][x];
  if ((t === 5 || t === 6) && S.flags[wallKey(S.zone, x, y)]) return 0;
  return t;
}
function solid(t) { return t === 1 || t === 5 || t === 6; }
function floorKind(t) {
  if (t === 2) return "grate";
  if (t === 3) return "wet";
  return "stone";
}
function blocked(x, y, r) {
  const pts = [[x-r,y-r],[x+r,y-r],[x-r,y+r],[x+r,y+r],[x,y]];
  for (const [px,py] of pts) if (solid(tileAt(px, py))) return true;
  return false;
}

// ─── Interact ────────────────────────────────────────────
let nearEnt = null;
function scanInteract() {
  const m = MAPS[S.zone];
  nearEnt = null;
  let best = 36;
  for (const e of m.ents) {
    if (e.kind === "enemy" || e.kind === "hazard" || e.kind === "pot") continue;
    if (e.kind === "switch") continue;
    const d = Math.hypot(e.x - player.x, e.y - player.y);
    if (d < best) { best = d; nearEnt = e; }
  }
  const pr = document.getElementById("prompt");
  if (nearEnt) {
    pr.textContent = promptFor(nearEnt);
    pr.classList.add("show");
  } else {
    pr.classList.remove("show");
  }
}
function promptFor(e) {
  if (e.kind === "save") return "E  ·  SANCTUARY STONE";
  if (e.kind === "shop") return "E  ·  MERCHANT ALTAR";
  if (e.kind === "altar") {
    if (e.up === 1) return S.flags.w1 ? "IGNITED EDGE — RESTING" : "E  ·  WEAPON ALTAR";
    if (e.up === 2) return S.flags.w2 ? "SOLAR FALCHION — RESTING" : "E  ·  SOLAR ALTAR";
    if (e.up === "lantern") return S.flags.lanternGot ? "LIGHT PUZZLE — SOLVED" : "E  ·  REFLECTION ALTAR";
  }
  if (e.kind === "door") return "E  ·  PASSAGE";
  if (e.kind === "chest") return e.opened ? "EMPTY VESSEL" : "E  ·  SEALED CHEST";
  if (e.kind === "cache") return e.opened ? "EMPTY CACHE" : "E  ·  HIDDEN CACHE";
  if (e.kind === "plate") return "PRESSURE PLATE";
  if (e.kind === "beacon") return S.flags.coreDead ? "THE BEACON BURNS" : "THE CORE BEACON";
  return "E  ·  INTERACT";
}

function interact() {
  scanInteract();
  if (!nearEnt) return;
  const e = nearEnt;
  if (e.kind === "save") activateSave(e);
  else if (e.kind === "shop") openShop();
  else if (e.kind === "altar") useAltar(e);
  else if (e.kind === "door") useDoor(e);
  else if (e.kind === "chest") openChest(e);
  else if (e.kind === "cache") openCache(e);
}

function activateSave(e) {
  S.hp = S.maxHp;
  S.lastSave = { zone: S.zone, x: e.x, y: e.y };
  if (e.sid === 1) S.flags.save1 = true;
  if (e.sid === 2) S.flags.save2 = true;
  if (e.sid === 3) S.flags.save3 = true;
  persist();
  AudioSys.save();
  burst(e.x, e.y, 20, ["#ffe08a","#e0b45a"], 40, 0.9, 3, -10);
  toast("SANCTUARY BOUND", "Wounds close. The stone remembers you.");
  updateHUD();
}

function useAltar(e) {
  if (e.up === 1 && !S.flags.w1) {
    S.flags.w1 = true; S.weapon = 1;
    AudioSys.altar();
    document.getElementById("shimmer").style.opacity = "1";
    setTimeout(() => document.getElementById("shimmer").style.opacity = "0", 2000);
    burst(e.x, e.y, 28, ["#e07030","#f0b060"], 70, 1, 4, -15);
    toast("IGNITED EDGE", "The blade drinks ember. Swings leave fire in their wake.");
    log("The first altar answers. Your blade is flame.");
  } else if (e.up === 2 && !S.flags.w2) {
    S.flags.w2 = true; S.weapon = 2;
    AudioSys.altar();
    document.getElementById("shimmer").style.opacity = "1";
    setTimeout(() => document.getElementById("shimmer").style.opacity = "0", 2000);
    burst(e.x, e.y, 32, ["#ffe08a","#fff","#e0b45a"], 80, 1.1, 4, -15);
    toast("SOLAR FALCHION", "A wider arc. A heavier truth. Light answers steel.");
    log("The second altar crowns the blade in solar fire.");
  } else if (e.up === "lantern") {
    // require both vault switches
    if (S.flags.switchA && S.flags.switchB) {
      if (!S.flags.lanternGot) {
        S.flags.lanternGot = true; S.items.lantern = true; S.equip.lantern = true;
        AudioSys.rare(); whiteMotes(e.x, e.y);
        toast("PHANTOM LANTERN", "Soft blue light. Hidden wounds in the stone reveal themselves.");
      }
    } else {
      log("The reflection altar is dark. Distant crystals still sleep.");
    }
  }
  updateHUD();
}

function useDoor(e) {
  const to = e.to;
  // gates
  if (!e.back) {
    if (S.zone === 0) { /* free */ }
    if (S.zone === 1) {
      const live = enemies.filter(en => en.alive).length;
      if (live > 2 && !S.flags.save2) { log("Constructs still bar the way forward."); return; }
      S.flags.shopUnlocked = true;
    }
    if (S.zone === 2 && !S.flags.galleryBridge) {
      log("The far gate is sealed. The bronze plates must speak together.");
      return;
    }
    if (S.zone === 3 && !S.hasShard) {
      log("A crystal lock. You need a ranged key.");
      return;
    }
    if (S.zone === 3 && !S.flags.vaultGate) {
      log("The vault's outer seal still holds. Strike the high crystals.");
      return;
    }
    if (S.zone === 4) {
      const live = enemies.filter(en => en.alive && (en.isGuard || en.type==="brute" || en.type==="sentinel")).length;
      if (!S.flags.w2) { log("The Solar Altar must be claimed before the Core."); return; }
    }
  }
  AudioSys.door();
  spawnInZone(to, !!e.back);
  if (to === 1) S.flags.shopUnlocked = true;
  if (S.zone === 4 && !e.back) S.flags.ammoShop = true;
}

function openChest(e) {
  if (e.opened) return;
  e.opened = true;
  if (e.loot === "band" && !S.flags.bandGot) {
    S.flags.bandGot = true; S.items.band = true; S.equip.band = true;
    AudioSys.rare(); whiteMotes(e.x, e.y);
    toast("WARDEN'S BAND", "A silver ring. Your hands remember a faster cadence.");
  } else if (e.loot === "shard" && !S.flags.shardGot) {
    S.flags.shardGot = true; S.hasShard = true; S.charges = 8;
    AudioSys.rare(); whiteMotes(e.x, e.y);
    toast("AETHER SHARD", "A pulsing crystal fills your off-hand. K to loose a bolt.");
    log("The halfway secret is yours. Distance is no longer a wall.");
  } else if (e.loot === "cloak" && !S.flags.cloakGot) {
    S.flags.cloakGot = true; S.items.cloak = true; S.equip.cloak = true;
    AudioSys.rare(); whiteMotes(e.x, e.y);
    toast("SHADOW CLOAK", "Trap-light dulls against the hem.");
  } else {
    S.gold += 20; goldDust(e.x, e.y); AudioSys.coin();
  }
  updateHUD();
}

function openCache(e) {
  if (e.opened) return;
  e.opened = true;
  if (e.loot === "stem" && !S.flags.cacheStem) {
    S.flags.cacheStem = true; S.items.stems++;
    AudioSys.pickup(); whiteMotes(e.x, e.y);
    toast("VITAL STEM", "A green pulse. Four points of life, waiting.");
  } else if (e.loot === "goldbig") {
    S.gold += 80; goldDust(e.x, e.y); AudioSys.coin();
    toast("SPIRE HOARD", "Eighty golden sigils spill into your palm.");
  } else {
    const g = 15 + (Math.random()*15|0);
    S.gold += g; goldDust(e.x, e.y); AudioSys.coin();
    floatTxt(e.x, e.y, "+"+g+" ✦", "#e0b45a");
  }
  updateHUD();
}

// ─── Shop / Inventory ────────────────────────────────────
function openShop() {
  S.shopOpen = true;
  document.getElementById("shop-panel").classList.add("open");
  AudioSys.shopNear();
  renderShop();
}
function closeShop() {
  S.shopOpen = false;
  document.getElementById("shop-panel").classList.remove("open");
}
function renderShop() {
  const el = document.getElementById("shop-slots");
  const items = [
    { id: "stem", name: "VITAL STEM", ds: "Restore 2 containers (4 HP)", cost: 50, stock: S.shopStock.stem },
    { id: "seal", name: "BINDING SEAL", ds: "Freeze one foe for 3 seconds", cost: 75, stock: S.shopStock.seal },
    { id: "heart", name: "HEART CONTAINER", ds: "A fifth vessel. Permanent.", cost: 300, stock: S.flags.heart5 ? 0 : S.shopStock.heart },
  ];
  if (S.flags.ammoShop || S.hasShard) {
    items.push({ id: "ammo", name: "AETHER CHARGE", ds: "One crystalline bolt", cost: 10, stock: 99 });
  }
  el.innerHTML = items.map(it => `
    <div class="slot" data-id="${it.id}">
      <div class="ico">${it.id==="stem"?"🌿":it.id==="seal"?"◈":it.id==="heart"?"♥":"✧"}</div>
      <div><div class="nm">${it.name}</div><div class="ds">${it.ds}</div></div>
      <div class="st">${it.stock<=0?"SOLD":it.cost+" ✦"}</div>
    </div>`).join("");
  el.querySelectorAll(".slot").forEach(s => s.addEventListener("click", () => buy(s.dataset.id)));
}
function buy(id) {
  const price = { stem:50, seal:75, heart:300, ammo:10 }[id];
  if (id === "stem") {
    if (S.shopStock.stem <= 0) return;
    if (S.gold < price) { log("The altar is unmoved. You lack sigils."); return; }
    S.gold -= price; S.shopStock.stem--; S.items.stems++;
  } else if (id === "seal") {
    if (S.shopStock.seal <= 0) return;
    if (S.gold < price) { log("The altar is unmoved. You lack sigils."); return; }
    S.gold -= price; S.shopStock.seal--; S.items.seals++;
  } else if (id === "heart") {
    if (S.flags.heart5 || S.shopStock.heart <= 0) return;
    if (S.gold < price) { log("Three hundred sigils. The vessel waits."); return; }
    S.gold -= price; S.shopStock.heart = 0; S.flags.boughtHeart = true;
    grantHeart(5);
  } else if (id === "ammo") {
    if (!S.hasShard) return;
    if (S.gold < price) return;
    if (S.charges >= S.maxCharges) { log("The shard is already sated."); return; }
    S.gold -= price; S.charges++;
  }
  AudioSys.buy();
  updateHUD();
  renderShop();
}

function toggleInv() {
  if (S.shopOpen) return;
  S.invOpen = !S.invOpen;
  document.getElementById("inv-panel").classList.toggle("open", S.invOpen);
  if (S.invOpen) renderInv();
}
function renderInv() {
  const el = document.getElementById("inv-slots");
  const rows = [];
  rows.push(accRow("band", "WARDEN'S BAND", "Silver ring. Attack cadence quickens.", S.items.band, S.equip.band, "○"));
  rows.push(accRow("lantern", "PHANTOM LANTERN", "Reveals hairline cracks and hidden paths.", S.items.lantern, S.equip.lantern, "◉"));
  rows.push(accRow("cloak", "SHADOW CLOAK", "Softens environmental harm.", S.items.cloak, S.equip.cloak, "▴"));
  rows.push({
    html: `<div class="slot" data-use="stem">
      <div class="ico">🌿</div>
      <div><div class="nm">VITAL STEM  ×${S.items.stems}</div>
      <div class="ds">Restore 2 containers (4 HP)</div></div>
      <div class="st">${S.items.stems?"USE":"—"}</div></div>`
  });
  rows.push({
    html: `<div class="slot" data-use="seal">
      <div class="ico">◈</div>
      <div><div class="nm">BINDING SEAL  ×${S.items.seals}</div>
      <div class="ds">Freeze the nearest foe (~3s)</div></div>
      <div class="st">${S.items.seals?"USE":"—"}</div></div>`
  });
  const wname = ["EMBER BLADE","IGNITED EDGE","SOLAR FALCHION"][S.weapon];
  rows.push({
    html: `<div class="slot">
      <div class="ico">⚔</div>
      <div><div class="nm">${wname}</div>
      <div class="ds">${S.hasShard?"Aether Shard bound to the off-hand.":"A relic of the last watch."}</div></div>
      <div class="st">WIELDED</div></div>`
  });
  el.innerHTML = rows.map(r => r.html || r).join("");
  el.querySelectorAll(".slot").forEach(s => {
    s.addEventListener("click", () => {
      const acc = s.dataset.acc, use = s.dataset.use;
      if (acc) {
        if (!S.items[acc]) return;
        S.equip[acc] = !S.equip[acc];
        AudioSys.pickup();
        renderInv();
      }
      if (use === "stem") useStem();
      if (use === "seal") useSeal();
    });
  });
}
function accRow(id, name, ds, have, on, ico) {
  return { html: `<div class="slot ${on?"on":""}" data-acc="${id}">
    <div class="ico">${have?ico:"·"}</div>
    <div><div class="nm">${name}</div><div class="ds">${have?ds:"Not yet found."}</div></div>
    <div class="st">${!have?"UNKNOWN":on?"EQUIPPED":"EQUIP"}</div></div>` };
}
function useStem() {
  if (S.items.stems <= 0) return;
  if (S.hp >= S.maxHp) { log("You are already whole."); return; }
  S.items.stems--;
  S.hp = Math.min(S.maxHp, S.hp + 4);
  AudioSys.pickup();
  burst(player.x, player.y, 14, ["#7dba6a","#c0f0a0"], 40, 0.6, 3, -20);
  toast("VITAL STEM", "Green fire knits the wounds.");
  updateHUD(); renderInv();
}
function useSeal() {
  if (S.items.seals <= 0) return;
  const live = enemies.filter(e => e.alive).sort((a,b) =>
    Math.hypot(a.x-player.x,a.y-player.y) - Math.hypot(b.x-player.x,b.y-player.y));
  if (!live.length) { log("No construct near enough to bind."); return; }
  S.items.seals--;
  live[0].frozen = 3;
  AudioSys.freeze();
  burst(live[0].x, live[0].y, 16, ["#80c8ff","#fff"], 40, 0.5, 3);
  log("The Binding Seal locks a construct in pale ice.");
  updateHUD(); renderInv();
}

// ─── HUD ─────────────────────────────────────────────────
function updateHUD() {
  const h = document.getElementById("hearts");
  let html = "";
  for (let i = 0; i < S.hearts; i++) {
    const v = S.hp - i*2;
    const fill = v >= 2 ? 1 : v === 1 ? 0.5 : 0;
    html += heartSVG(fill);
  }
  h.innerHTML = html;
  document.getElementById("gold-lbl").textContent = S.gold + " ✦";
  const cl = document.getElementById("charge-lbl");
  cl.textContent = S.hasShard ? ("SHARD  " + "◆".repeat(S.charges) + "◇".repeat(Math.max(0, Math.min(8, S.maxCharges) - S.charges))) : "";
}
let _hid = 0;
function heartSVG(fill) {
  const id = "h" + (_hid++);
  const col = "#c43b3b";
  return `<div class="heart"><svg viewBox="0 0 32 30">
    <defs><clipPath id="${id}"><rect x="0" y="0" width="${fill>=1?32:fill>=0.5?16:0}" height="30"/></clipPath></defs>
    <path d="M16 26.5 C16 26.5 4 17.5 4 10.2 C4 5.8 7.4 3.2 11.1 3.2 C13.8 3.2 15.6 4.8 16 6.8 C16.4 4.8 18.2 3.2 20.9 3.2 C24.6 3.2 28 5.8 28 10.2 C28 17.5 16 26.5 16 26.5Z"
      fill="#2a181c" stroke="#a67c3a88" stroke-width="1.1"/>
    <path d="M16 26.5 C16 26.5 4 17.5 4 10.2 C4 5.8 7.4 3.2 11.1 3.2 C13.8 3.2 15.6 4.8 16 6.8 C16.4 4.8 18.2 3.2 20.9 3.2 C24.6 3.2 28 5.8 28 10.2 C28 17.5 16 26.5 16 26.5Z"
      fill="${col}" clip-path="url(#${id})"/>
  </svg></div>`;
}

function toast(t, s) {
  const el = document.getElementById("toast");
  el.querySelector(".title").textContent = t;
  el.querySelector(".sub").textContent = s || "";
  el.classList.add("show");
  clearTimeout(toast._t);
  toast._t = setTimeout(() => el.classList.remove("show"), 3200);
}
function log(m) {
  const el = document.getElementById("log");
  const d = document.createElement("div");
  d.textContent = m;
  el.prepend(d);
  while (el.children.length > 3) el.lastChild.remove();
}
function floatTxt(x, y, t, col) {
  floats.push({ x, y, t, col, life: 0.9 });
}

function persist() {
  try {
    localStorage.setItem("warden_corrosion", JSON.stringify({
      S, px: player.x, py: player.y
    }));
  } catch (_) {}
}

// ─── Keys ────────────────────────────────────────────────
function onKey(e) {
  if (!S.started) return;
  if (e.code === "Escape") {
    if (S.shopOpen) { closeShop(); return; }
    if (S.invOpen) { toggleInv(); return; }
    S.paused = !S.paused;
    document.getElementById("pause-panel").classList.toggle("open", S.paused);
    return;
  }
  if (S.over) return;
  if (e.code === "KeyI") toggleInv();
  if (e.code === "KeyE" && !S.invOpen && !S.shopOpen && !S.paused) interact();
  if ((e.code === "KeyJ" || e.code === "Space") && !S.invOpen && !S.shopOpen) tryMelee();
  if (e.code === "KeyK" && !S.invOpen && !S.shopOpen) tryShoot();
  if (e.code === "Digit1" && S.invOpen) useStem();
  if (e.code === "Digit2" && S.invOpen) useSeal();
}
document.getElementById("btn-resume").onclick = () => {
  S.paused = false; document.getElementById("pause-panel").classList.remove("open");
};
document.getElementById("btn-begin").onclick = () => startGame();
document.getElementById("btn-again").onclick = () => location.reload();

function startGame() {
  AudioSys.resume();
  S.started = true;
  document.getElementById("overlay-title").classList.add("gone");
  S.lastSave = { zone: 0, x: 4.5*T, y: 3.5*T };
  spawnInZone(0, false);
  player.x = 4.5*T; player.y = 3.5*T;
  updateHUD();
  toast("THE ASHEN THRESHOLD", "The outer gate remembers your step.");
  log("A Sanctuary Stone glows to the west. Constructs stir ahead.");
}

// ─── Update ──────────────────────────────────────────────
const cam = { x: 0, y: 0 };
let last = performance.now();
let dustT = 0;

function update(dt) {
  if (!S.started || S.paused || S.over || S.invOpen || S.shopOpen) {
    tickP(dt);
    return;
  }
  const m = MAPS[S.zone];

  // movement
  let ix = 0, iy = 0;
  if (keys["KeyA"] || keys["ArrowLeft"]) ix -= 1;
  if (keys["KeyD"] || keys["ArrowRight"]) ix += 1;
  if (keys["KeyW"] || keys["ArrowUp"]) iy -= 1;
  if (keys["KeyS"] || keys["ArrowDown"]) iy += 1;
  const moving = ix || iy;
  if (moving) {
    const len = Math.hypot(ix, iy);
    ix /= len; iy /= len;
    player.facing = Math.atan2(iy, ix);
  }
  // face mouse if aiming
  mouse.worldX = mouse.x + cam.x;
  mouse.worldY = mouse.y + cam.y;
  if (!moving) {
    const dx = mouse.worldX - player.x, dy = mouse.worldY - player.y;
    if (Math.hypot(dx,dy) > 8) player.facing = Math.atan2(dy, dx);
  }

  let spd = player.speed;
  if (player.atkWind > 0) spd *= 0.35;
  player.vx = ix * spd; player.vy = iy * spd;

  let nx = player.x + player.vx * dt;
  if (!blocked(nx, player.y, player.r)) player.x = nx;
  let ny = player.y + player.vy * dt;
  if (!blocked(player.x, ny, player.r)) player.y = ny;

  // clamp
  player.x = Math.max(T+8, Math.min(m.w*T - T - 8, player.x));
  player.y = Math.max(T+8, Math.min(m.h*T - T - 8, player.y));

  if (moving) {
    player.bob += dt * 10;
    AudioSys.foot(floorKind(tileAt(player.x, player.y)), performance.now()/1000);
    if (S.weapon >= 1 && Math.random() < dt * 8) emberTrail(player.x - Math.cos(player.facing)*6, player.y + 8);
  }

  if (player.atkT > 0) player.atkT -= dt;
  if (player.shootT > 0) player.shootT -= dt;
  if (player.invuln > 0) player.invuln -= dt;
  if (player.flash > 0) player.flash -= dt;
  if (player.atkWind > 0) {
    player.atkWind -= dt;
    if (player.atkWind <= 0) startSwing();
  }
  if (player.swinging) {
    player.swingA += dt * (S.weapon===2?8:14);
    if (player.swingA > 1) player.swinging = false;
  }

  updatePlates(dt);
  // hazards
  for (const e of m.ents) {
    if (e.kind === "hazard" && Math.hypot(e.x-player.x,e.y-player.y) < 18) {
      hurtPlayer(1, "hazard");
    }
  }

  // enemies
  for (const en of enemies) {
    if (!en.alive) continue;
    if (en.hitT > 0) en.hitT -= dt;
    if (en.stun > 0) en.stun -= dt;
    if (en.frozen > 0) { en.frozen -= dt; continue; }
    if (en.burn > 0) {
      en.burn -= dt;
      if (Math.random() < dt * 3) { en.hp -= 0.35; emberTrail(en.x, en.y); if (en.hp<=0) killEnemy(en); }
    }
    en.atkT -= dt;
    const dx = player.x - en.x, dy = player.y - en.y;
    const d = Math.hypot(dx, dy);
    en.ang = Math.atan2(dy, dx);
    const aggro = en.isCore ? 420 : en.isGuard || en.isMini ? 240 : 170;
    if (en.stun <= 0) {
      if (d < aggro) {
        const sp = en.spd * (d < 40 ? 0.7 : 1);
        const nx = en.x + (dx / d) * sp * dt;
        const ny = en.y + (dy / d) * sp * dt;
        if (!blocked(nx, en.y, en.r - 2)) en.x = nx;
        if (!blocked(en.x, ny, en.r - 2)) en.y = ny;
      } else {
        en.wander += dt;
        en.x += Math.cos(en.wander) * 18 * dt;
        en.y += Math.sin(en.wander * 0.7) * 18 * dt;
      }
    } else {
      en.x += en.vx * dt; en.y += en.vy * dt;
      en.vx *= 0.85; en.vy *= 0.85;
    }
    // core special: shoot
    if (en.isCore && d < 360 && en.atkT <= 0) {
      en.atkT = 1.6;
      const a = en.ang;
      projs.push({ x: en.x, y: en.y, vx: Math.cos(a)*160, vy: Math.sin(a)*160, life: 2, r: 6, friendly: false, core: true });
    }
    if (d < en.r + player.r + 4 && en.atkT <= 0) {
      en.atkT = 0.9;
      hurtPlayer(en.dmg, "melee");
      const a = Math.atan2(player.y-en.y, player.x-en.x);
      player.x += Math.cos(a)*10; player.y += Math.sin(a)*10;
    }
  }

  // projectiles
  for (let i = projs.length-1; i>=0; i--) {
    const p = projs[i];
    p.x += p.vx * dt; p.y += p.vy * dt; p.life -= dt;
    if (p.life <= 0 || solid(tileAt(p.x, p.y))) {
      if (solid(tileAt(p.x,p.y))) {
        hitWalls(p.x, p.y, 2);
        AudioSys.chimeHit();
      }
      // switches
      hitSwitches(p.x, p.y);
      projs.splice(i,1);
      continue;
    }
    if (p.friendly) {
      hitSwitches(p.x, p.y);
      for (const en of enemies) {
        if (!en.alive) continue;
        if (Math.hypot(en.x-p.x, en.y-p.y) < en.r + p.r) {
          hurtEnemy(en, 2, Math.atan2(en.y-p.y, en.x-p.x));
          AudioSys.chimeHit();
          projs.splice(i,1);
          break;
        }
      }
    } else {
      if (Math.hypot(player.x-p.x, player.y-p.y) < player.r + p.r) {
        hurtPlayer(1, "proj");
        projs.splice(i,1);
      }
    }
  }

  // env particles
  dustT += dt;
  if (dustT > 0.08) {
    dustT = 0;
    const zx = cam.x + Math.random()*W, zy = cam.y + Math.random()*H;
    if (S.zone === 1) P.push({ x: zx, y: zy, vx: 8, vy: 12, life: 2, max: 2, col: "#c0a07044", size: 1.2, grav: 5, type: 1 });
    if (S.zone === 3) P.push({ x: zx, y: zy+40, vx: (Math.random()-0.5)*10, vy: -18, life: 2.2, max: 2.2, col: "#8ec8e833", size: 2, grav: -4, type: 1 });
    if (S.zone === 5) P.push({ x: zx, y: zy, vx: 0, vy: -10, life: 1.6, max: 1.6, col: "#e0b45a33", size: 1.5, grav: -6, type: 1 });
  }

  for (let i = floats.length-1; i>=0; i--) {
    floats[i].life -= dt; floats[i].y -= 22*dt;
    if (floats[i].life <= 0) floats.splice(i,1);
  }

  tickP(dt);
  scanInteract();

  // camera
  const tw = m.w * T, th = m.h * T;
  let cx = player.x - W/2, cy = player.y - H/2;
  cx = Math.max(0, Math.min(tw - W, cx));
  cy = Math.max(0, Math.min(th - H, cy));
  if (tw < W) cx = (tw - W)/2;
  if (th < H) cy = (th - H)/2;
  cam.x += (cx - cam.x) * Math.min(1, dt * 6);
  cam.y += (cy - cam.y) * Math.min(1, dt * 6);
}

function updatePlates(dt) {
  const m = MAPS[S.zone];
  for (const e of m.ents) {
    if (e.kind !== "plate") continue;
    const held = Math.hypot(e.x-player.x, e.y-player.y) < 22 ||
      enemies.some(en => en.alive && Math.hypot(en.x-e.x,en.y-e.y) < 22);
    if (held) e.latch = 3.2;
    if (e.latch > 0) e.latch -= dt || 0.016;
    const on = held || e.latch > 0;
    if (on && !e.on) { e.on = true; AudioSys.plate(); burst(e.x, e.y, 6, ["#e0b45a"], 30, 0.3, 2); }
    if (!on) e.on = false;
    if (e.id === "gA") S._pA = on;
    if (e.id === "gB") S._pB = on;
    if (e.id === "vA") S._pV = on;
  }
  if (S.zone === 2) {
    if (S._pA) S._pAt = performance.now();
    if (S._pB) S._pBt = performance.now();
    const both = S._pA && S._pB;
    const seq = S._pAt && S._pBt && Math.abs(S._pAt - S._pBt) < 2200;
    if ((both || seq) && !S.flags.galleryBridge) {
      S.flags.galleryBridge = true;
      AudioSys.door();
      toast("GALLERY GATE", "Twin plates agree. The far nave unlocks.");
    }
  }
  if (S.zone === 3 && S._pV) {
    // reveal / weaken the shard wall — already breakable
  }
}

function hitSwitches(x, y) {
  const m = MAPS[S.zone];
  for (const e of m.ents) {
    if (e.kind !== "switch" || e.on) continue;
    if (Math.hypot(e.x-x, e.y-y) < 22) {
      e.on = true;
      AudioSys.switchHit();
      burst(e.x, e.y, 14, ["#80e0ff","#fff"], 50, 0.5, 3);
      if (e.id === "v1") S.flags.switchA = true;
      if (e.id === "v2") S.flags.switchB = true;
      if (e.id === "h5" && !S.flags.heart5) grantHeart(5);
      if (S.flags.switchA && S.flags.switchB && !S.flags.vaultGate) {
        S.flags.vaultGate = true;
        toast("VAULT SEAL", "The outer crystals unlock the ascent.");
      }
      log("A distant mechanism answers the shard.");
    }
  }
}

function normAng(a) {
  while (a > Math.PI) a -= Math.PI*2;
  while (a < -Math.PI) a += Math.PI*2;
  return a;
}

// ─── Draw ────────────────────────────────────────────────
function draw() {
  ctx.clearRect(0, 0, W, H);
  const m = MAPS[S.started ? S.zone : 0];

  // backdrop
  const bg = S.zone === 3 ? "#0c1820" : S.zone === 5 ? "#1a140c" : S.zone === 1 ? "#16120e" : "#10161e";
  ctx.fillStyle = bg;
  ctx.fillRect(0,0,W,H);

  const x0 = Math.max(0, Math.floor(cam.x / T) - 1);
  const y0 = Math.max(0, Math.floor(cam.y / T) - 1);
  const x1 = Math.min(m.w-1, Math.floor((cam.x+W)/T) + 1);
  const y1 = Math.min(m.h-1, Math.floor((cam.y+H)/T) + 1);

  for (let y = y0; y <= y1; y++) {
    for (let x = x0; x <= x1; x++) {
      let t = m.tiles[y][x];
      if ((t === 5 || t === 6) && S.flags[wallKey(S.zone, x, y)]) t = S.zone===3 ? 3 : 0;
      const sx = x*T - cam.x, sy = y*T - cam.y;
      drawTile(sx, sy, t, x, y);
    }
  }

  // entities under player
  for (const e of m.ents) {
    if (e.kind === "enemy") continue;
    drawEnt(e);
  }

  // dead enemies fade
  for (const en of enemies) if (!en.alive) drawEnemy(en);
  for (const en of enemies) if (en.alive) drawEnemy(en);

  drawPlayer();

  // projs
  for (const p of projs) {
    ctx.save();
    ctx.translate(p.x-cam.x, p.y-cam.y);
    ctx.fillStyle = p.friendly ? "#9ae8ff" : "#ff8060";
    ctx.shadowColor = p.friendly ? "#6cf" : "#f40";
    ctx.shadowBlur = 12;
    ctx.beginPath(); ctx.arc(0,0,p.r,0,Math.PI*2); ctx.fill();
    ctx.restore();
  }

  drawP(cam);

  for (const f of floats) {
    ctx.globalAlpha = Math.max(0, f.life);
    ctx.fillStyle = f.col;
    ctx.font = "bold 14px Cinzel, serif";
    ctx.textAlign = "center";
    ctx.fillText(f.t, f.x-cam.x, f.y-cam.y);
    ctx.globalAlpha = 1;
  }

  // vignette
  const g = ctx.createRadialGradient(W/2,H/2, W*0.25, W/2,H/2, W*0.72);
  g.addColorStop(0, "rgba(0,0,0,0)");
  g.addColorStop(1, S.zone===5 ? "rgba(40,20,0,0.55)" : "rgba(0,0,0,0.55)");
  ctx.fillStyle = g;
  ctx.fillRect(0,0,W,H);

  if (player.flash > 0) {
    ctx.fillStyle = `rgba(180,30,30,${player.flash})`;
    ctx.fillRect(0,0,W,H);
  }

  // lantern reveal pulse
  if (S.equip.lantern) {
    ctx.save();
    ctx.globalCompositeOperation = "lighter";
    const lg = ctx.createRadialGradient(player.x-cam.x, player.y-cam.y, 10, player.x-cam.x, player.y-cam.y, 160);
    lg.addColorStop(0, "rgba(120,180,220,0.12)");
    lg.addColorStop(1, "rgba(0,0,0,0)");
    ctx.fillStyle = lg;
    ctx.fillRect(0,0,W,H);
    ctx.restore();
  }
}

function drawTile(sx, sy, t, tx, ty) {
  const hash = ((tx*17 + ty*31) & 7);
  if (t === 1) {
    ctx.fillStyle = (tx+ty)%2 ? "#2a221c" : "#241c18";
    ctx.fillRect(sx, sy, T, T);
    ctx.strokeStyle = "#3a3028";
    ctx.strokeRect(sx+0.5, sy+0.5, T-1, T-1);
    ctx.fillStyle = "#4a3a28";
    if (hash===0) ctx.fillRect(sx+8, sy+10, 10, 4);
    return;
  }
  // floor
  if (t === 2) {
    ctx.fillStyle = hash%2 ? "#3a2a20" : "#32241c";
    ctx.fillRect(sx, sy, T, T);
    ctx.strokeStyle = "#5a4030";
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(sx, sy+T/2); ctx.lineTo(sx+T, sy+T/2);
    ctx.moveTo(sx+T/2, sy); ctx.lineTo(sx+T/2, sy+T);
    ctx.stroke();
  } else if (t === 3) {
    ctx.fillStyle = hash%2 ? "#1a3040" : "#162838";
    ctx.fillRect(sx, sy, T, T);
    ctx.fillStyle = "rgba(80,140,160,0.15)";
    ctx.fillRect(sx+4, sy+6, 12, 5);
  } else if (t === 4) {
    ctx.fillStyle = hash%2 ? "#3a3020" : "#443818";
    ctx.fillRect(sx, sy, T, T);
    ctx.fillStyle = "#e0b45a22";
    ctx.fillRect(sx, sy, T, T);
  } else {
    const pal = S.zone === 5
      ? ["#3a2c18", "#322614"]
      : S.zone === 4
        ? ["#2a2834", "#22202c"]
        : ["#243040", "#1e2a38"];
    ctx.fillStyle = pal[hash % 2];
    ctx.fillRect(sx, sy, T, T);
    ctx.fillStyle = "rgba(0,0,0,0.14)";
    ctx.fillRect(sx, sy + T - 6, T, 6);
    if (hash === 3) {
      ctx.fillStyle = "rgba(224,180,90,0.05)";
      ctx.beginPath(); ctx.arc(sx + 20, sy + 18, 7, 0, Math.PI * 2); ctx.fill();
    }
    if (hash === 5) {
      ctx.fillStyle = "rgba(60,90,50,0.12)";
      ctx.fillRect(sx + 4, sy + 22, 16, 5);
    }
  }
  // breakable
  if (t === 5 || t === 6) {
    ctx.fillStyle = t===6 ? "#2a2420" : "#3a2820";
    ctx.fillRect(sx, sy, T, T);
    ctx.strokeStyle = "#1a120e";
    ctx.strokeRect(sx, sy, T, T);
    ctx.strokeStyle = S.equip.lantern || t===5 ? "#c0a070" : "#5a4038";
    ctx.lineWidth = S.equip.lantern ? 1.6 : 1;
    ctx.beginPath();
    ctx.moveTo(sx+8, sy+6); ctx.lineTo(sx+14, sy+22); ctx.lineTo(sx+28, sy+30);
    ctx.moveTo(sx+22, sy+4); ctx.lineTo(sx+18, sy+18);
    ctx.stroke();
    if (S.equip.lantern) {
      ctx.fillStyle = "rgba(140,200,230,0.12)";
      ctx.fillRect(sx, sy, T, T);
    }
  }
}

function drawEnt(e) {
  const x = e.x - cam.x, y = e.y - cam.y;
  if (e.kind === "save") {
    const on = (e.sid===1&&S.flags.save1)||(e.sid===2&&S.flags.save2)||(e.sid===3&&S.flags.save3);
    ctx.save();
    ctx.translate(x, y);
    ctx.fillStyle = "#2a2418";
    ctx.beginPath(); ctx.moveTo(-10,8); ctx.lineTo(10,8); ctx.lineTo(6,14); ctx.lineTo(-6,14); ctx.fill();
    ctx.fillStyle = on ? "#e0b45a" : "#8a7040";
    ctx.shadowColor = "#e0b45a"; ctx.shadowBlur = on ? 18 : 6;
    ctx.beginPath(); ctx.moveTo(0,-16); ctx.lineTo(8,8); ctx.lineTo(-8,8); ctx.closePath(); ctx.fill();
    ctx.restore();
    if (Math.random() < 0.3) P.push({ x: e.x+(Math.random()-0.5)*10, y: e.y-16, vx:0, vy:-12, life:0.8, max:0.8, col:"#e0b45a", size:1.5, grav:-4, type:1 });
  } else if (e.kind === "shop") {
    ctx.save(); ctx.translate(x,y);
    ctx.fillStyle = "#1a1820";
    ctx.fillRect(-16, -8, 32, 16);
    ctx.strokeStyle = "#e0b45a"; ctx.strokeRect(-16,-8,32,16);
    ctx.fillStyle = "#e0b45a"; ctx.font = "12px Cinzel, serif"; ctx.textAlign="center";
    ctx.fillText("✦", 0, 4);
    ctx.restore();
  } else if (e.kind === "altar") {
    ctx.save(); ctx.translate(x,y);
    const lit = (e.up===1&&S.flags.w1)||(e.up===2&&S.flags.w2)||(e.up==="lantern"&&S.flags.lanternGot);
    ctx.fillStyle = "#2a2018";
    ctx.beginPath(); ctx.ellipse(0, 8, 16, 7, 0, 0, Math.PI*2); ctx.fill();
    ctx.fillStyle = lit ? (e.up===2?"#e0b45a":"#e07030") : "#4a4030";
    ctx.shadowColor = lit ? "#e0b45a" : "transparent"; ctx.shadowBlur = 16;
    ctx.fillRect(-8, -14, 16, 20);
    ctx.restore();
  } else if (e.kind === "plate") {
    ctx.save(); ctx.translate(x,y);
    ctx.fillStyle = e.on ? "#a08040" : "#5a4a30";
    ctx.beginPath(); ctx.ellipse(0, 0, 16, 10, 0, 0, Math.PI*2); ctx.fill();
    ctx.strokeStyle = "#e0b45a88"; ctx.stroke();
    ctx.restore();
  } else if (e.kind === "switch") {
    ctx.save(); ctx.translate(x,y);
    ctx.fillStyle = e.on ? "#80e0ff" : "#406080";
    ctx.shadowColor = e.on ? "#6cf" : "#246"; ctx.shadowBlur = 14;
    ctx.beginPath(); ctx.arc(0, 0, 8, 0, Math.PI*2); ctx.fill();
    ctx.restore();
  } else if (e.kind === "door") {
    ctx.save(); ctx.translate(x, y);
    ctx.fillStyle = "#1a120c";
    ctx.fillRect(-12, -20, 24, 36);
    ctx.strokeStyle = "#e0b45a"; ctx.lineWidth = 1.4;
    ctx.strokeRect(-12, -20, 24, 36);
    ctx.fillStyle = "#e0b45a88";
    ctx.fillRect(-2, -4, 4, 10);
    ctx.restore();
  } else if (e.kind === "hazard") {
    ctx.save(); ctx.translate(x,y);
    ctx.fillStyle = "#6a2020";
    for (let i=0;i<5;i++) {
      const a = i/5*Math.PI*2 + 0.3;
      ctx.beginPath(); ctx.moveTo(0,0); ctx.lineTo(Math.cos(a)*10, Math.sin(a)*7); ctx.lineTo(Math.cos(a+0.3)*6, Math.sin(a+0.3)*4); ctx.fill();
    }
    ctx.restore();
  } else if (e.kind === "pot" && !e.broke) {
    ctx.save(); ctx.translate(x,y);
    ctx.fillStyle = "#6a5038";
    ctx.beginPath(); ctx.ellipse(0, 4, 7, 5, 0, 0, Math.PI*2); ctx.fill();
    ctx.fillRect(-6, -6, 12, 10);
    ctx.fillStyle = "#8a6a48"; ctx.fillRect(-4, -8, 8, 4);
    ctx.restore();
  } else if (e.kind === "chest") {
    ctx.save(); ctx.translate(x,y);
    ctx.fillStyle = e.opened ? "#3a3020" : "#8a6030";
    ctx.fillRect(-10, -6, 20, 14);
    ctx.strokeStyle = "#e0b45a"; ctx.strokeRect(-10,-6,20,14);
    if (!e.opened) { ctx.fillStyle = "#e0b45a"; ctx.fillRect(-2,-2,4,6); }
    ctx.restore();
  } else if (e.kind === "cache" && !e.opened) {
    ctx.save(); ctx.translate(x,y);
    ctx.fillStyle = "#4a4030";
    ctx.beginPath(); ctx.arc(0,0,6,0,Math.PI*2); ctx.fill();
    ctx.fillStyle = "#e0b45a88"; ctx.fillRect(-2,-2,4,4);
    ctx.restore();
  } else if (e.kind === "beacon") {
    ctx.save(); ctx.translate(x,y);
    const burn = S.flags.coreDead;
    ctx.fillStyle = "#2a2010";
    ctx.beginPath(); ctx.moveTo(-14,16); ctx.lineTo(14,16); ctx.lineTo(8,-4); ctx.lineTo(-8,-4); ctx.fill();
    ctx.fillStyle = burn ? "#ffe08a" : "#806030";
    ctx.shadowColor = "#e0b45a"; ctx.shadowBlur = burn ? 40 : 10;
    ctx.beginPath(); ctx.arc(0, -10, burn?14:8, 0, Math.PI*2); ctx.fill();
    ctx.restore();
  }
}

function drawEnemy(en) {
  const x = en.x - cam.x, y = en.y - cam.y;
  ctx.save();
  ctx.translate(x, y);
  if (!en.alive) { ctx.globalAlpha = 0.25; }
  // shadow
  ctx.fillStyle = "rgba(0,0,0,0.35)";
  ctx.beginPath(); ctx.ellipse(0, en.r+2, en.r*0.9, 5, 0, 0, Math.PI*2); ctx.fill();
  if (en.frozen > 0) { ctx.shadowColor = "#6cf"; ctx.shadowBlur = 16; }
  if (en.hitT > 0) ctx.globalAlpha = 0.55;
  // body
  ctx.fillStyle = en.isCore ? "#2a1810" : en.color;
  ctx.beginPath(); ctx.arc(0, -2, en.r, 0, Math.PI*2); ctx.fill();
  ctx.fillStyle = en.isCore ? "#e07030" : "#c94a4a";
  ctx.fillRect(-6, -8, 12, 4);
  ctx.strokeStyle = "#1a120c";
  ctx.stroke();
  if (en.type === "sentinel") {
    ctx.fillStyle = "#8a5040";
    ctx.fillRect(-10, -4, 4, 10);
    ctx.fillRect(6, -4, 4, 10);
  }
  if (en.type === "brute" || en.isCore) {
    ctx.fillStyle = "#2a1810";
    ctx.fillRect(-en.r + 2, 2, en.r * 2 - 4, 6);
  }
  if (en.isCore || en.isGuard || en.isMini) {
    ctx.strokeStyle = "#e0b45a66";
    ctx.beginPath(); ctx.arc(0, -2, en.r + 3, 0, Math.PI * 2); ctx.stroke();
  }
  // hp
  if (en.alive && en.hp < en.maxHp) {
    ctx.fillStyle = "#000"; ctx.fillRect(-en.r, -en.r-10, en.r*2, 3);
    ctx.fillStyle = "#c94a4a"; ctx.fillRect(-en.r, -en.r-10, en.r*2 * (en.hp/en.maxHp), 3);
  }
  ctx.restore();
}

function drawPlayer() {
  const x = player.x - cam.x, y = player.y - cam.y;
  ctx.save();
  ctx.translate(x, y);
  if (player.invuln > 0 && Math.floor(player.invuln*12)%2===0) ctx.globalAlpha = 0.45;

  const bob = Math.sin(player.bob) * 1.5;
  // shadow
  ctx.fillStyle = "rgba(0,0,0,0.4)";
  ctx.beginPath(); ctx.ellipse(0, 12, 11, 5, 0, 0, Math.PI*2); ctx.fill();

  const ang = player.facing;
  // cloak
  ctx.save();
  ctx.rotate(ang + Math.PI/2);
  const cloakCol = S.equip.cloak ? "#24182c" : "#4a3a30";
  ctx.fillStyle = cloakCol;
  ctx.beginPath();
  ctx.moveTo(0, -6 + bob);
  ctx.quadraticCurveTo(16, 4, 8, 18 + bob);
  ctx.lineTo(-8, 18 + bob);
  ctx.quadraticCurveTo(-16, 4, 0, -6 + bob);
  ctx.fill();
  if (S.weapon === 2) {
    ctx.strokeStyle = "#e0b45a88"; ctx.lineWidth = 1.4;
    ctx.stroke();
  }
  if (S.equip.cloak) {
    ctx.strokeStyle = "#4a3060"; ctx.lineWidth = 2;
    ctx.beginPath(); ctx.moveTo(-7, 16+bob); ctx.lineTo(7, 16+bob); ctx.stroke();
  }
  ctx.restore();

  ctx.fillStyle = "#5a5048";
  ctx.beginPath(); ctx.arc(0, -2+bob, 10, 0, Math.PI*2); ctx.fill();
  ctx.fillStyle = "#2a2220";
  ctx.beginPath(); ctx.arc(0, -7+bob, 8.5, Math.PI, 0); ctx.fill();
  ctx.beginPath(); ctx.arc(0, -5+bob, 6.5, 0, Math.PI*2); ctx.fill();
  ctx.fillStyle = "#e0b45a";
  ctx.fillRect(-3.5, -6+bob, 7, 2.2);

  // lantern glow token
  if (S.equip.lantern) {
    ctx.fillStyle = "#8ec8e8";
    ctx.shadowColor = "#6cf"; ctx.shadowBlur = 10;
    ctx.beginPath(); ctx.arc(-10, 2+bob, 3, 0, Math.PI*2); ctx.fill();
    ctx.shadowBlur = 0;
  }
  // band
  if (S.equip.band) {
    ctx.strokeStyle = "#d0d8e0"; ctx.lineWidth = 2;
    ctx.beginPath(); ctx.arc(8, 2+bob, 3, 0, Math.PI*2); ctx.stroke();
  }

  // blade
  const swing = player.swinging ? (player.swingA - 0.5) * atkArc() : 0;
  const bladeA = ang + swing;
  const bl = S.weapon === 2 ? 28 : 22;
  ctx.save();
  ctx.rotate(bladeA);
  if (S.weapon === 1) { ctx.shadowColor = "#e07030"; ctx.shadowBlur = 12; }
  if (S.weapon === 2) { ctx.shadowColor = "#e0b45a"; ctx.shadowBlur = 16; }
  ctx.strokeStyle = S.weapon===2 ? "#ffe08a" : S.weapon===1 ? "#e07030" : "#c0c4c8";
  ctx.lineWidth = S.weapon===2 ? 3.4 : 2.4;
  ctx.beginPath(); ctx.moveTo(8, 0); ctx.lineTo(8+bl, 0); ctx.stroke();
  ctx.fillStyle = "#8a7030";
  ctx.fillRect(6, -2, 5, 4);
  ctx.restore();

  // aether shard offhand
  if (S.hasShard) {
    ctx.save();
    ctx.rotate(ang);
    ctx.fillStyle = "#7ad0f0";
    ctx.shadowColor = "#6cf"; ctx.shadowBlur = 10;
    ctx.beginPath();
    ctx.moveTo(-12, -6); ctx.lineTo(-6, 0); ctx.lineTo(-12, 6); ctx.lineTo(-18, 0);
    ctx.closePath(); ctx.fill();
    ctx.restore();
  }

  // solar halo
  if (S.weapon === 2) {
    ctx.strokeStyle = "rgba(224,180,90,0.25)";
    ctx.lineWidth = 6;
    ctx.beginPath(); ctx.arc(0, -2+bob, 16, 0, Math.PI*2); ctx.stroke();
  }

  ctx.restore();
}

// ─── Loop ────────────────────────────────────────────────
function frame(now) {
  const dt = Math.min(0.033, (now - last) / 1000);
  last = now;
  update(dt);
  draw();
  requestAnimationFrame(frame);
}
updateHUD();
requestAnimationFrame(frame);

window.__G.S = S;
window.__G.player = player;
window.__G.MAPS = MAPS;
window.__G.enemies = () => enemies;
window.__G.spawnInZone = spawnInZone;
window.__G.grantHeart = grantHeart;
window.__G.updateHUD = updateHUD;
window.__G.openShop = openShop;
window.__G.rebuildEnemies = rebuildEnemies;
window.__G.tryMelee = tryMelee;
window.__G.startSwing = startSwing;
window.__G.tryShoot = tryShoot;
window.__G.interact = interact;
window.__G.scanInteract = scanInteract;
window.__G.useStem = useStem;
window.__G.useSeal = useSeal;
window.__G.hurtEnemy = hurtEnemy;
window.__G.igniteBeacon = igniteBeacon;
window.__G.buy = buy;

window.addEventListener("pointerdown", () => AudioSys.resume(), { once: false });
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"x-ai/grok-4.6","provider":"openrouter","persona":"full","sessionId":"f1e5838e-40af-4fea-8f94-602a9918c522","tokensIn":2356670,"tokensOut":54835,"tokensTotal":2411505,"cost":1.7411019999999997,"turns":25,"toolCalls":50,"failedToolCalls":0,"timestamp":"2026-08-17T17:47:40.955Z"} -->