← Warden results

Warden

Ornith 1.5 35B A3B · 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 59,155 bytes · SHA-256 5aecaca43047
<!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>
<style>
  :root{
    --gold:#e7b954; --gold2:#f5d98a; --orange:#e0702b; --deep:#1a1f2e;
    --slate:#2a3346; --rust:#7a4a2a; --panel:#12161f; --panel2:#1c2331;
    --text:#e9e2d0; --dim:#8a93a6;
  }
  *{box-sizing:border-box;margin:0;padding:0;}
  html,body{height:100%;background:#05070d;overflow:hidden;font-family:'Segoe UI',system-ui,sans-serif;color:var(--text);}
  #wrap{position:relative;width:100vw;height:100vh;display:flex;align-items:center;justify-content:center;}
  canvas{background:#05070d;display:block;}
  .overlay{position:absolute;inset:0;display:none;align-items:center;justify-content:center;
    background:radial-gradient(ellipse at center,rgba(20,25,40,.88),rgba(4,6,12,.97));
    z-index:50;padding:24px;}
  .overlay.show{display:flex;}
  .panel{background:linear-gradient(160deg,var(--panel2),var(--panel));border:1px solid #333d52;
    border-radius:14px;box-shadow:0 20px 60px rgba(0,0,0,.7),inset 0 1px 0 rgba(231,185,84,.15);
    max-width:940px;width:100%;max-height:92vh;overflow:auto;padding:26px 30px;
    border-top:2px solid var(--gold);}
  h1{font-size:40px;letter-spacing:3px;background:linear-gradient(90deg,var(--gold),var(--orange),var(--gold2));
    -webkit-background-clip:text;background-clip:text;color:transparent;text-align:center;font-weight:800;}
  h2{font-size:22px;color:var(--gold2);letter-spacing:2px;margin-bottom:14px;text-align:center;}
  h3{color:var(--gold);font-size:14px;letter-spacing:1px;margin:16px 0 8px;text-transform:uppercase;}
  p{color:var(--dim);line-height:1.6;font-size:14px;text-align:center;}
  .flavor{max-width:660px;margin:12px auto 20px;font-style:italic;color:#b7bfae;}
  .btn{cursor:pointer;border:1px solid #414c63;background:linear-gradient(180deg,#26303f,#1a202c);
    color:var(--text);padding:11px 22px;border-radius:9px;font-size:15px;letter-spacing:1px;
    transition:.15s;text-align:center;display:inline-block;}
  .btn:hover{border-color:var(--gold);background:linear-gradient(180deg,#303c4f,#232b38);color:#fff;
    box-shadow:0 0 16px rgba(231,185,84,.25);}
  .btn.primary{border-color:var(--gold);color:var(--gold2);}
  .btn:disabled{opacity:.4;cursor:not-allowed;box-shadow:none;}
  .btnrow{text-align:center;margin-top:20px;display:flex;gap:12px;flex-wrap:wrap;justify-content:center;}
  .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:12px;}
  .card{border:1px solid #333d52;border-radius:10px;background:#161b26;padding:14px;position:relative;}
  .card .ic{font-size:30px;text-align:center;margin-bottom:6px;}
  .card .nm{color:var(--gold2);font-weight:700;font-size:14px;text-align:center;letter-spacing:.5px;}
  .card .ds{color:var(--dim);font-size:12px;text-align:center;margin-top:5px;line-height:1.4;}
  .card .pr{color:var(--gold);font-size:13px;text-align:center;margin-top:8px;font-weight:700;}
  .card.equipped{border-color:var(--gold);box-shadow:0 0 14px rgba(231,185,84,.25);}
  .card.equipped::after{content:'EQUIPPED';position:absolute;top:6px;right:8px;font-size:10px;
    color:#0a0d13;background:var(--gold);padding:2px 6px;border-radius:5px;font-weight:800;letter-spacing:1px;}
  .card.locked{filter:grayscale(.6);opacity:.55;}
  #hud{position:absolute;top:0;left:0;right:0;pointer-events:none;z-index:20;padding:12px 16px;}
  #topbar{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;}
  #hearts{display:flex;gap:4px;padding:6px 10px;background:rgba(8,10,16,.6);border-radius:8px;
    border:1px solid #2a3346;}
  #resources{display:flex;gap:10px;align-items:center;padding:6px 12px;background:rgba(8,10,16,.6);
    border-radius:8px;border:1px solid #2a3346;font-size:14px;font-weight:700;}
  #resources .sigil{color:var(--gold);}
  #resources .shard{color:#7fd0ff;}
  #zoneinfo{text-align:right;padding:6px 12px;background:rgba(8,10,16,.6);border-radius:8px;
    border:1px solid #2a3346;}
  #zoneinfo .zn{color:var(--gold2);font-weight:700;letter-spacing:1px;font-size:15px;}
  #zoneinfo .ds{color:var(--dim);font-size:11px;}
  #minimap{position:absolute;bottom:12px;right:14px;border:1px solid #333d52;border-radius:8px;
    background:rgba(8,10,16,.72);z-index:20;}
  #invbar{position:absolute;bottom:12px;left:14px;display:flex;gap:8px;z-index:20;}
  .invslot{width:52px;height:52px;background:rgba(12,15,22,.8);border:1px solid #333d52;border-radius:9px;
    display:flex;flex-direction:column;align-items:center;justify-content:center;position:relative;
    cursor:pointer;font-size:22px;transition:.12s;}
  .invslot:hover{border-color:var(--gold);}
  .invslot.active{border-color:var(--gold);box-shadow:0 0 12px rgba(231,185,84,.3);}
  .invslot .cnt{position:absolute;bottom:2px;right:4px;font-size:11px;color:var(--gold2);font-weight:700;}
  .invslot .key{position:absolute;top:2px;left:5px;font-size:9px;color:var(--dim);}
  #msg{position:absolute;top:72px;left:50%;transform:translateX(-50%);z-index:25;
    text-align:center;pointer-events:none;}
  #msg .line{color:var(--gold2);font-size:16px;letter-spacing:1px;text-shadow:0 2px 8px #000;
    opacity:0;animation:fade 3.8s ease forwards;margin:4px 0;white-space:nowrap;}
  #msg .line.big{font-size:22px;color:var(--gold);font-weight:800;letter-spacing:2px;}
  @keyframes fade{0%{opacity:0;transform:translateY(8px);}12%{opacity:1;transform:none;}
    76%{opacity:1;}100%{opacity:0;transform:translateY(-6px);}}
  #controls{position:absolute;bottom:12px;left:50%;transform:translateX(-50%);z-index:20;
    color:var(--dim);font-size:11px;text-align:center;background:rgba(8,10,16,.5);
    padding:6px 14px;border-radius:20px;border:1px solid #2a3346;letter-spacing:.5px;}
  #controls b{color:var(--gold2);}
  .flash{position:absolute;inset:0;pointer-events:none;z-index:30;background:#d12f2f;opacity:0;}
  .flash.foot{background:#fff;}
  .title-keys{display:flex;gap:16px;justify-content:center;margin:18px 0;flex-wrap:wrap;}
  .kkey{display:flex;flex-direction:column;align-items:center;gap:6px;}
  .kbox{width:42px;height:42px;border:1px solid #4a546a;border-radius:8px;background:#1c2331;
    display:flex;align-items:center;justify-content:center;font-weight:700;color:var(--gold2);
    font-size:12px;text-align:center;line-height:1.1;}
  .shop-notice{color:#e0702b;font-size:12px;text-align:center;margin-top:12px;font-style:italic;}
  .credit{color:#5a6478;font-size:11px;text-align:center;margin-top:16px;letter-spacing:1px;}
  .toast{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:26;
    color:var(--gold2);font-size:20px;letter-spacing:2px;text-shadow:0 2px 10px #000;
    pointer-events:none;opacity:0;animation:pop 1.6s ease forwards;}
  @keyframes pop{0%{opacity:0;transform:translate(-50%,-45%) scale(.9);}
    20%{opacity:1;transform:translate(-50%,-50%) scale(1);}
    80%{opacity:1;}100%{opacity:0;transform:translate(-50%,-55%);}}
</style>
</head>
<body>
<div id="wrap">
  <canvas id="c"></canvas>
  <div id="hud">
    <div id="topbar">
      <div id="hearts"></div>
      <div id="resources">
        <span class="sigil">◆ <b id="goldAmt">0</b></span>
        <span class="shard">◈ <b id="shardAmt">0</b></span>
      </div>
      <div id="zoneinfo">
        <div class="zn" id="zoneName"></div>
        <div class="ds" id="zoneSub"></div>
      </div>
    </div>
  </div>
  <canvas id="minimap" width="172" height="132"></canvas>
  <div id="invbar"></div>
  <div id="msg"></div>
  <div id="controls"><b>WASD/Arrows</b> Move · <b>J/Z/Click</b> Attack · <b>K/X</b> Ranged · <b>Space</b> Interact · <b>I</b> Inventory · <b>Shift</b> Dash</div>
  <div class="flash" id="flash"></div>

  <div class="overlay show" id="titleScreen">
    <div class="panel" style="max-width:760px;">
      <h1>THE WARDEN&#8217;S CORROSION</h1>
      <p class="flavor">Aurelion&#8217;s Sunken Observatory floats above a storm-locked valley. As the Last Warden you must reignite the Core Beacon before the creeping black-iron decay — the Corrosion — collapses the ancient halls.</p>
      <div class="title-keys">
        <div class="kkey"><div class="kbox">W A S D</div><span>Move</span></div>
        <div class="kkey"><div class="kbox">J Z</div><span>Melee</span></div>
        <div class="kkey"><div class="kbox">K X</div><span>Ranged</span></div>
        <div class="kkey"><div class="kbox">Space</div><span>Interact</span></div>
        <div class="kkey"><div class="kbox">I</div><span>Inventory</span></div>
        <div class="kkey"><div class="kbox">Shift</div><span>Dash</span></div>
      </div>
      <div class="btnrow">
        <button class="btn primary" id="startBtn">Enter the Observatory</button>
      </div>
      <p class="credit">Six zones · two weapon altars · a hidden ranged weapon · three Sanctuary Stones. Restore the Beacon.</p>
    </div>
  </div>

  <div class="overlay" id="deathScreen">
    <div class="panel" style="max-width:560px;">
      <h1 style="background:linear-gradient(90deg,#c0392b,#e0702b);-webkit-background-clip:text;background-clip:text;">THE DARKNESS SPREADS</h1>
      <p class="flavor">The Warden falls, and the Corrosion claims another hall. But the Beacon remembers its guardians...</p>
      <div class="btnrow">
        <button class="btn primary" id="respawnBtn">Awaken at the Last Sanctuary</button>
      </div>
    </div>
  </div>

  <div class="overlay" id="shopScreen">
    <div class="panel">
      <h2>✦ RUSTED MERCHANT ALTAR ✦</h2>
      <p id="shopGold" style="color:var(--gold);font-weight:700;margin-bottom:12px;"></p>
      <div class="grid" id="shopGrid"></div>
      <p class="shop-notice">The merchant keeps only limited stock. Return when your pouch is heavy.</p>
      <div class="btnrow">
        <button class="btn" id="shopClose">Leave Altar</button>
      </div>
    </div>
  </div>

  <div class="overlay" id="invScreen">
    <div class="panel" style="max-width:760px;">
      <h2>INVENTORY &amp; GEAR</h2>
      <h3>Accessories — click to toggle equip</h3>
      <div class="grid" id="invAcc"></div>
      <h3>Consumables — click to use</h3>
      <div class="grid" id="invCon"></div>
      <div class="btnrow">
        <button class="btn" id="invClose">Close (I)</button>
      </div>
    </div>
  </div>

  <div class="overlay" id="winScreen">
    <div class="panel" style="max-width:640px;">
      <h1 style="background:linear-gradient(90deg,#f5d98a,#e7b954,#fff);-webkit-background-clip:text;background-clip:text;">THE BEACON IGNITES</h1>
      <p class="flavor">You raise the Solar Falchion aloft. Golden fire roars from the Beacon, piercing the storm. The Corrosion recoils, and light spills across the sunken halls once more. As the Last Warden, you have kept your vow.</p>
      <div class="btnrow">
        <button class="btn primary" id="winBtn">Stand Vigil, Eternal</button>
      </div>
    </div>
  </div>
</div>

<script>
"use strict";
/* ============================================================
   THE WARDEN'S CORROSION
   ============================================================ */
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const mm = document.getElementById('minimap');
const mctx = mm.getContext('2d');
let VW = 960, VH = 600;
function resize(){
  const s = Math.min(window.innerWidth/VW, window.innerHeight/VH);
  canvas.width = VW; canvas.height = VH;
  canvas.style.width = (VW*s)+'px';
  canvas.style.height = (VH*s)+'px';
}
window.addEventListener('resize', resize); resize();

// ---------- Utility ----------
const rand=(a,b)=>a+Math.random()*(b-a), clamp=(v,a,b)=>v<a?a:v>b?b:v;
const dist=(x1,y1,x2,y2)=>Math.hypot(x2-x1,y2-y1);
const now=()=>performance.now();
const T=40; // tile size

// ---------- Audio ----------
const Audio=(()=>{
  let actx=null,master=null;
  function ensure(){if(actx)return;actx=new(window.AudioContext||window.webkitAudioContext)();master=actx.createGain();master.gain.value=0.5;master.connect(actx.destination);}
  function tone(f,dur,type,vol,slideTo){if(!actx)return;const t=actx.currentTime;const o=actx.createOscillator(),g=actx.createGain();o.type=type||'sine';o.frequency.setValueAtTime(f,t);if(slideTo)o.frequency.exponentialRampToValueAtTime(Math.max(1,slideTo),t+dur);g.gain.setValueAtTime(vol||0.3,t);g.gain.exponentialRampToValueAtTime(0.0001,t+dur);o.connect(g);g.connect(master);o.start(t);o.stop(t+dur+0.02);}
  function noise(dur,vol,ff){if(!actx)return;const n=Math.floor(actx.sampleRate*dur);const buf=actx.createBuffer(1,n,actx.sampleRate);const d=buf.getChannelData(0);for(let i=0;i<n;i++)d[i]=(Math.random()*2-1)*(1-i/n);const src=actx.createBufferSource();src.buffer=buf;const f=actx.createBiquadFilter();f.type='bandpass';f.frequency.value=ff||800;const g=actx.createGain();g.gain.value=vol||0.2;g.gain.exponentialRampToValueAtTime(0.0001,actx.currentTime+dur);src.connect(f);f.connect(g);g.connect(master);src.start();}
  return{
    resume(){ensure();if(actx.state==='suspended')actx.resume();},
    whoosh(){noise(0.13,0.1,1500);},
    impact(k){if(k==='base'){tone(180,0.12,'triangle',0.28);noise(0.08,0.14,600);}else if(k==='fire'){tone(140,0.16,'sawtooth',0.2);noise(0.14,0.15,900);}else if(k==='solar'){tone(90,0.4,'sine',0.34,40);tone(420,0.25,'triangle',0.18);}else{tone(300,0.09,'square',0.16);}},
    ranged(){tone(900,0.12,'sine',0.2,1400);tone(1400,0.18,'triangle',0.12);},
    crash(){tone(110,0.22,'sawtooth',0.28,60);tone(70,0.3,'sine',0.2,40);},
    hurt(){tone(220,0.18,'sawtooth',0.28,120);tone(520,0.12,'square',0.12);},
    chime(){tone(660,0.14,'sine',0.2);tone(990,0.2,'sine',0.16);},
    rare(){tone(523,0.2,'triangle',0.22);tone(784,0.25,'triangle',0.18);tone(1046,0.3,'sine',0.14);},
    coin(){tone(1200,0.06,'square',0.12);tone(1600,0.1,'square',0.1);},
    buy(){tone(660,0.1,'triangle',0.2);tone(990,0.16,'triangle',0.16);tone(1320,0.2,'sine',0.12);},
    reveal(){tone(800,0.25,'sine',0.14,1400);},
    stone(){tone(196,0.5,'sine',0.22,392);tone(294,0.6,'triangle',0.14);},
    altar(){tone(262,0.4,'triangle',0.22,524);tone(392,0.5,'sine',0.16);},
    plate(){tone(300,0.12,'square',0.16,450);},
    break(){noise(0.2,0.22,400);tone(140,0.16,'sawtooth',0.16,80);},
    dash(){tone(500,0.12,'sine',0.14,200);},
    unlock(){tone(523,0.15,'triangle',0.2);tone(784,0.2,'triangle',0.16);tone(1046,0.28,'sine',0.14);},
    win(){[523,659,784,1046].forEach((f,i)=>setTimeout(()=>tone(f,0.5,'triangle',0.22),i*160));},
    foot(metal){if(metal)tone(90,0.06,'square',0.05,70);else tone(160,0.05,'sine',0.04,120);},
    amb(){if(!actx)return;const t=actx.currentTime;const o=actx.createOscillator();const g=actx.createGain();o.type='sine';o.frequency.value=55;g.gain.value=0.025;o.connect(g);g.connect(master);o.start(t);o.stop(t+2.4);}
  };
})();

// ---------- Weapons ----------
const WEAPONS={
  ember:{name:'Ember Blade',dmg:2,cd:420,arc:1.3,reach:52,color:'#c9a06a',kind:'base',trail:false,wide:false},
  ignited:{name:'Ignited Edge',dmg:3,cd:280,arc:1.5,reach:58,color:'#ff8a3a',kind:'fire',trail:true,wide:false},
  solar:{name:'Solar Falchion',dmg:5,cd:520,arc:2.1,reach:76,color:'#ffe27a',kind:'solar',trail:true,wide:true},
};

// ---------- Game State ----------
const G={
  running:false,open:false,
  player:null,enemies:[],bullets:[],particles:[],pickups:[],floaters:[],
  cur:null, zones:[], cam:{x:0,y:0}, shake:0,
  gold:180, shard:0, hasRanged:false,
  inv:{stem:1,seal:0,soldStem:0,soldSeal:0,soldHeart:0},
  acc:{band:false,lantern:false,cloak:false},
  flags:{}, time:0, lastFoot:0, over:false, won:false,
  playerEntered:{}, saved:false, saveZone:'z1',
};

// ============================================================
// ZONE DEFINITIONS — explicit geometry
// ============================================================
// Each zone: id, name, sub, bg, spawn:{x,y}, exits:[{x,y,to,x,y}],
// walls:[{x,y,w,h,break?,hp}], ents:[...], spawns:[{...}]
// entity: {type,x,y,r/..,...}
function makeZones(){
const Z=[];
const wall=(x,y,w,h,brk,hp)=>({type:'wall',x,y,w,h,break:!!brk,hp:hp||3,maxhp:hp||3});

// ============ ZONE 1: The Ashen Threshold ============
{
const walls=[
  wall(0,0,800,T),wall(0,0,T,360),wall(760,0,T,360),
  wall(0,320,800,T),
  wall(200,40,40,120),wall(520,40,40,120),
  wall(300,200,200,40),
  wall(120,200,40,120),wall(640,200,40,120),
];
const ents=[
  {type:'sanctuary',x:140,y:120,r:22},
  {type:'door',x:60,y:300,w:60,h:60,to:'z2',tx:60,ty:60},
  {type:'spawnEnemy',x:400,y:120,group:'z1a',n:2},
];
const spawns=[{x:60,y:300}];
Z.push({id:'z1',name:'The Ashen Threshold',sub:'Prologue',bg:'#161b26',walls,ents,spawns});
}

// ============ ZONE 2: The Rusted Archives ============
{
const walls=[
  wall(0,0,880,T),wall(0,0,T,440),wall(840,0,T,440),wall(0,400,880,T),
  wall(200,40,40,160),wall(300,40,40,160),
  wall(600,40,40,160),wall(700,40,40,160),
  wall(200,200,300,40),
  wall(400,240,40,120),
  wall(560,240,40,120),
  wall(120,240,40,120),
  // shop alcove walls around merchant
  wall(720,240,120,40),
];
const ents=[
  {type:'shop',x:760,y:140,r:20},
  {type:'chest',x:130,y:300,item:'band'},
  {type:'chest',x:320,y:320,item:'stem'},
  {type:'health',x:740,y:340},
  {type:'door',x:60,y:420,w:60,h:60,to:'z3',tx:60,ty:60},
  {type:'spawnEnemy',x:420,y:300,group:'z2a',n:3},
];
const spawns=[{x:60,y:60}];
Z.push({id:'z2',name:'The Rusted Archives',sub:'Early — Shop unlocked',bg:'#1a1712',walls,ents,spawns});
}

// ============ ZONE 3: The Collapsed Gallery ============
{
const walls=[
  wall(0,0,920,T),wall(0,0,T,480),wall(880,0,T,480),wall(0,440,920,T),
  wall(200,40,40,200),wall(640,40,40,200),
  wall(300,240,200,40),
  // breakable wall hiding health container 4
  wall(440,120,40,120,false,3),
  wall(120,240,40,120),
];
const ents=[
  {type:'altar',x:720,y:120,item:'ignited',label:'Ignited Edge'},
  {type:'chest',x:130,y:120,item:'seed'},
  {type:'health',x:460,y:180},  // hidden behind breakable wall
  {type:'sanctuary',x:820,y:380,r:22},
  {type:'door',x:60,y:420,w:60,h:60,to:'z4',tx:60,ty:60},
  {type:'spawnEnemy',x:460,y:320,group:'z3a',n:2},
];
const spawns=[{x:60,y:60}];
Z.push({id:'z3',name:'The Collapsed Gallery',sub:'Mid-Early — Weapon Upgrade 1',bg:'#16181e',walls,ents,spawns});
}

// ============ ZONE 4: The Sunken Vault ============
{
const walls=[
  wall(0,0,960,T),wall(0,0,T,520),wall(920,0,T,520),wall(0,480,960,T),
  wall(200,40,40,240),wall(680,40,40,240),
  wall(200,240,40,120),wall(680,240,40,120),
  wall(300,360,320,40),
  // breakable wall behind which Aether Shard is hidden
  wall(440,120,40,160,false,3),
  wall(120,240,40,160),
];
const ents=[
  {type:'shrine',x:760,y:360,item:'lantern'},
  {type:'switch',x:220,y:120},
  {type:'door',x:60,y:460,w:60,h:60,to:'z5',tx:60,ty:60},
  {type:'spawnEnemy',x:500,y:340,group:'z4a',n:3},
];
const spawns=[{x:60,y:60}];
Z.push({id:'z4',name:'The Sunken Vault',sub:'Mid-Game — Ranged Discovery',bg:'#101a22',walls,ents,spawns});
}

// ============ ZONE 5: The Twin Spire ============
{
const walls=[
  wall(0,0,900,T),wall(0,0,T,520),wall(860,0,T,520),wall(0,480,900,T),
  wall(240,40,40,200),wall(560,40,40,200),
  wall(240,300,40,120),wall(560,300,40,120),
  wall(340,200,200,40),
  wall(120,200,40,160),
  // ranged-only switch controlling health container 5
  wall(720,200,40,120,false,3),
];
const ents=[
  {type:'altar',x:660,y:120,item:'solar',label:'Solar Falchion'},
  {type:'chest',x:130,y:120,item:'seal'},
  {type:'chest',x:740,y:360,item:'bonus'},
  {type:'switch',x:740,y:120},
  {type:'health',x:220,y:420},  // unlocked by ranged switch
  {type:'sanctuary',x:60,y:60,r:22},
  {type:'door',x:420,y:500,w:60,h:60,to:'z6',tx:420,ty:60},
  {type:'boss',x:450,y:260,group:'z5boss',n:1},
];
const spawns=[{x:60,y:420}];
Z.push({id:'z5',name:'The Twin Spire',sub:'Late — Weapon Upgrade 2',bg:'#1a1420',walls,ents,spawns});
}

// ============ ZONE 6: The Core Chamber ============
{
const walls=[
  wall(0,0,1000,T),wall(0,0,T,560),wall(960,0,T,560),wall(0,520,1000,T),
  wall(200,40,40,200),wall(760,40,40,200),
  wall(200,320,40,120),wall(760,320,40,120),
  wall(300,200,400,40),
];
const ents=[
  {type:'sanctuary',x:60,y:60,r:22},
  {type:'beacon',x:500,y:260},
  {type:'boss',x:500,y:260,group:'z6boss',n:1},
];
const spawns=[{x:60,y:420}];
Z.push({id:'z6',name:'The Core Chamber',sub:'Climax — Restore the Beacon',bg:'#141018',walls,ents,spawns});
}
return Z;
}

// ============================================================
// PARSE & LOAD ZONE
// ============================================================
function loadZone(id){
  const z=G.zones.find(x=>x.id===id);
  G.cur=z;
  G.enemies=[];G.bullets=[];G.particles=[];G.pickups=[];G.floaters=[];
  G.walls=z.walls.slice();
  G.ents=z.ents.map(e=>{
    if(e.type==='switch'){return {...e,off:false};}
    if(e.type==='health'||e.type==='chest'||e.type==='sanctuary'||e.type==='shrine'){return {...e};}
    if(e.type==='door'){return {...e};}
    if(e.type==='beacon'){return {...e};}
    return {...e};
  });
  G.cam.x=0;G.cam.y=0;
  const s=z.spawns[0];
  if(G.playerEntered[id]){G.player.x=s.x;G.player.y=s.y;}
  else{G.player.x=s.x;G.player.y=s.y;G.playerEntered[id]=true;}
  // spawn enemies defined on the zone
  for(const e of z.ents){
    if(e.type==='spawnEnemy') spawnGroup(e.group,e.x,e.y);
    if(e.type==='boss') spawnEnemy({type:'boss',x:e.x,y:e.y});
  }
  // resolve any overlap with walls
  for(let n=0;n<8;n++){
    if(!collideCircle(G.player.x,G.player.y,G.player.w/2))break;
    let best=null,bd=1e9;
    for(const w of G.walls){
      if(w.break&&w.hp<=0)continue;
      const cx=clamp(G.player.x,w.x,w.x+w.w),cy=clamp(G.player.y,w.y,w.y+w.h);
      const d=dist(G.player.x,G.player.y,cx,cy);
      if(d<bd){bd=d;best=w;}
    }
    if(!best)break;
    const ang=Math.atan2(G.player.y-(best.y+best.h/2),G.player.x-(best.x+best.w/2));
    G.player.x+=Math.cos(ang)*4;G.player.y+=Math.sin(ang)*4;
  }
}

// ============================================================
// PLAYER
// ============================================================
function makePlayer(){
  return{x:0,y:0,w:22,h:22,vx:0,vy:0,dir:0,speed:2.8,hp:6,maxHp:6,
    containers:3,maxContainers:3,weapon:'ember',attackCd:0,attackT:0,
    dashCd:0,dashT:0,invuln:0,moving:false,animT:0,hurtFlash:0,hasBand:false};
}

// ============================================================
// INPUT
// ============================================================
const keys={};
const pressed={};
window.addEventListener('keydown',e=>{
  const k=e.key.toLowerCase();
  if(!keys[k])pressed[k]=true;
  keys[k]=true;
  if([' ','arrowup','arrowdown','arrowleft','arrowright'].includes(k))e.preventDefault();
  if(G.running&&!G.open){
    if(k==='i')toggleInv();
    if(k===' '||k==='e')interact();
  }
});
window.addEventListener('keyup',e=>{keys[e.key.toLowerCase()]=false;});
canvas.addEventListener('mousedown',e=>{if(G.running&&!G.open){if(e.button===0)interact();}});

// ============================================================
// COLLISION
// ============================================================
function solidAt(x,y){
  for(const w of G.walls){
    if(w.break&&w.hp<=0)continue;
    if(x>w.x&&x<w.x+w.w&&y>w.y&&y<w.y+w.h)return w;
  }
  return null;
}
function collideCircle(px,py,r){
  for(const w of G.walls){
    if(w.break&&w.hp<=0)continue;
    const cx=clamp(px,w.x,w.x+w.w),cy=clamp(py,w.y,w.y+w.h);
    if(dist(px,py,cx,cy)<r)return w;
  }
  return null;
}
function moveEntity(e,rx,ry){
  // axis-separated
  let nx=e.x+rx;
  if(!collideCircle(nx,e.y,e.w/2))e.x=nx; else {e.vx=0;}
  let ny=e.y+ry;
  if(!collideCircle(e.x,ny,e.w/2))e.y=ny; else {e.vy=0;}
}
function rectsOverlap(a,b){return a.x<b.x+b.w&&a.x+a.w>b.x&&a.y<b.y+b.h&&a.y+a.h>b.y;}

// ============================================================
// ENEMIES
// ============================================================
const ENEMY_TYPES={
  grunt:{hp:4,speed:1.2,dmg:2,r:16,gold:[6,12],color:'#5a4a52'},
  archer:{hp:3,speed:1.0,dmg:1,r:14,gold:[8,15],color:'#4a5258',ranged:true},
  sentinel:{hp:10,speed:0.9,dmg:3,r:20,gold:[10,16],color:'#6a4a3a'},
  boss:{hp:40,speed:1.4,dmg:4,r:28,gold:[120],color:'#3a1a2a'},
};
function spawnEnemy(def){
  const t=ENEMY_TYPES[def.type];
  G.enemies.push({type:def.type,x:def.x,y:def.y,hp:t.hp,maxhp:t.hp,speed:t.speed,dmg:t.dmg,r:t.r,
    color:t.color,ranged:!!t.ranged,gold:def.gold||[8,12],vx:0,vy:0,atkCd:0,stun:0,hitFlash:0,
    dir:0,animT:Math.random()*6,phase:Math.random()*6,alive:true,aggro:false});
}
function spawnGroup(g,x,y){
  const map={
    z1a:['grunt','grunt'],
    z2a:['grunt','grunt','sentinel'],
    z3a:['grunt','archer'],
    z4a:['archer','archer','sentinel'],
  };
  (map[g]||['grunt']).forEach((t,i)=>{
    spawnEnemy({type:t,x:x+rand(-40,40),y:y+rand(-40,40)});
  });
}

// ============================================================
// PARTICLES / FLOATERS
// ============================================================
function emitP(x,y,n,opts={}){
  for(let i=0;i<n;i++){
    const a=rand(0,Math.PI*2),sp=rand(opts.sp||1,opts.sp2||3);
    G.particles.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,life:opts.life||30,max:30,
      color:opts.color||'#ff9040',r:opts.r||2,grav:opts.grav||0});
  }
}
function floater(x,y,txt,color){G.floaters.push({x,y,txt,color,life:40,max:40});}

// ============================================================
// COMBAT — melee
// ============================================================
function meleeStrike(){
  const w=WEAPONS[G.player.weapon];
  G.player.attackT=0.18;
  const arcW=w.arc;
  // check enemies in arc
  let hitAny=false;
  for(const en of G.enemies){
    if(!en.alive)continue;
    const d=dist(G.player.x,G.player.y,en.x,en.y);
    if(d>w.reach+en.r-6)continue;
    const ang=Math.atan2(en.y-G.player.y,en.x-G.player.x);
    let diff=Math.abs(normalizeAng(ang-G.player.dir));
    if(diff>Math.PI)diff=2*Math.PI-diff;
    if(diff<arcW/2){
      damageEnemy(en,w.dmg);
      hitAny=true;
    }
  }
  if(hitAny)Audio.impact(w.kind);else Audio.whoosh();
  // trail
  if(w.trail){
    const px=G.player.x+Math.cos(G.player.dir)*w.reach*0.6,py=G.player.y+Math.sin(G.player.dir)*w.reach*0.6;
    emitP(px,py,6,{color:w.color,sp:1.5,life:18,r:3,grav:0});
  }
}
function damageEnemy(en,dmg){
  const w=WEAPONS[G.player.weapon];
  en.hp-=dmg;en.hitFlash=1;en.aggro=true;
  const kb=Math.atan2(en.y-G.player.y,en.x-G.player.x);
  en.x+=Math.cos(kb)*8;en.y+=Math.sin(kb)*8;
  floater(en.x,en.y-en.r,'-'+dmg, w.kind==='solar'?'#ffe27a':'#ff9040');
  emitP(en.x,en.y,8,{color:w.kind==='solar'?'#fff2b0':(w.kind==='fire'?'#ff7020':'#ffb060'),sp:2.5,life:16,r:2});
  if(en.type==='boss'){G.shake=6;G.shakeT=0.3;}
  if(en.hp<=0)killEnemy(en);
}
function killEnemy(en){
  en.alive=false;
  const g=en.gold[0]+Math.floor(Math.random()*(en.gold[1]-en.gold[0]+1));
  G.gold+=g;
  floater(en.x,en.y-20,'+'+g+'◆','#f5d98a');
  emitP(en.x,en.y,16,{color:'#3a2a3a',sp:3,life:24,r:3});
  Audio.crash();
  // maybe drop consumable
  if(Math.random()<0.18)G.pickups.push({type:'stem',x:en.x,y:en.y,r:10,life:600});
  updateHUD();
}

// ============================================================
// RANGED
// ============================================================
function fireRanged(){
  if(!G.hasRanged)return;
  if(G.shard<=0){toast('No Aether Charges!');return;}
  G.shard--;
  const bx=G.player.x+Math.cos(G.player.dir)*18,by=G.player.y+Math.sin(G.player.dir)*18;
  G.bullets.push({x:bx,y:by,vx:Math.cos(G.player.dir)*7,vy:Math.sin(G.player.dir)*7,r:6,life:80,dmg:3,fromPlayer:true});
  Audio.ranged();
  updateHUD();
}

// ============================================================
// INTERACT / PROGRESSION
// ============================================================
function interact(){
  const p=G.player;
  // check nearby ents
  for(const e of G.ents){
    const d=dist(p.x,p.y,e.x||e.x,e.y);
    if(d>56)continue;
    if(e.type==='sanctuary'&&!e.used){activateSanctuary(e);return;}
    if(e.type==='sanctuary'&&e.used){toast('Sanctuary Stone — already rested');return;}
    if(e.type==='shop'){openShop();return;}
    if(e.type==='altar'){activateAltar(e);return;}
    if(e.type==='chest'){openChest(e);return;}
    if(e.type==='health'){takeHealth(e);return;}
    if(e.type==='switch'){toggleSwitch(e);return;}
    if(e.type==='shrine'){activateShrine(e);return;}
    if(e.type==='beacon'){activateBeacon();return;}
  }
}
function activateSanctuary(e){
  e.used=true;
  G.saved=true;
  G.saveZone=G.cur.id;
  G.player.hp=G.player.maxHp;
  Audio.stone();
  emitP(e.x,e.y,20,{color:'#f5d98a',sp:2,life:40,r:3});
  toast('SANCTUARY RESTED — Progress Saved');
  // save to localStorage
  try{localStorage.setItem('wardenSave',JSON.stringify(saveState()));}catch(x){}
}
function activateAltar(e){
  const w=e.item;
  if(G.player.weapon!==w){toast('Already upgraded');return;}
  G.player.weapon=e.item;
  Audio.altar();
  emitP(e.x,e.y,30,{color:w==='solar'?'#ffe27a':'#ff8a3a',sp:2.5,life:50,r:4});
  G.shake=8;G.shakeT=0.5;
  toast(e.label+' ACQUIRED!',true);
  floatAnim();
}
function openChest(e){
  if(e.open)return;
  e.open=true;
  const it=e.item;
  if(it==='band'){G.acc.band=true;Audio.rare();toast('WARDEN\'S BAND FOUND!',true);}
  else if(it==='stem'){G.inv.stem++;Audio.chime();toast('VITAL STEM FOUND');}
  else if(it==='seal'){G.inv.seal++;Audio.chime();toast('BINDING SEAL FOUND');}
  else if(it==='bonus'){G.gold+=250;Audio.rare();toast('+250 GOLD — Bonus Chest!',true);}
  else if(it==='lantern'){G.acc.lantern=true;Audio.rare();toast('PHANTUM LANTERN FOUND!',true);}
  emitP(e.x,e.y,14,{color:'#f5d98a',sp:2,life:30,r:3});
  updateHUD();
  updateInvBar();
}
function takeHealth(e){
  if(e.taken)return;
  e.taken=true;
  if(G.player.maxContainers<7){
    G.player.maxContainers++;G.player.containers++;
    G.player.hp=Math.min(G.player.maxHp,G.player.hp+2);
    Audio.unlock();
    emitP(e.x,e.y,24,{color:'#ff5070',sp:2.5,life:40,r:4});
    toast('HEART CONTAINER — Max HP +2!',true);
    floatAnim();
  }
  updateHUD();
}
function toggleSwitch(e){
  e.off=!e.off;
  Audio.plate();
  // find the paired health container (zone5)
  if(G.cur.id==='z5'){
    const h=G.ents.find(x=>x.type==='health');
    if(h){h.disabled=!e.off; if(!h.disabled){h.taken=false;}}
  }
}
function activateShrine(e){
  G.acc.lantern=true;
  Audio.rare();
  toast('PHANTOM LANTERN FOUND!',true);
  emitP(e.x,e.y,24,{color:'#7fd0ff',sp:2.5,life:40,r:4});
  updateInvBar();
}
function activateBeacon(){
  if(!G.flags.beaconReady){toast('The Beacon is cold. It needs the twin switches lit.');return;}
  winGame();
}

// ============================================================
// SWITCHES & DOORS LOGIC
// ============================================================
function switchEffects(){
  // zone4 switch opens breakable wall path? Actually opens a wall gap
  // We'll use zone4 switch to remove a wall so player can reach back area.
  // zone5 switch unlocks health container
}
function findWallById(id){return null;}

// ============================================================
// SHOP
// ============================================================
const SHOP_ITEMS=[
  {id:'stem',name:'Vital Stem',desc:'Restores 2 full containers (4 HP).',price:50,max:3,con:'stem'},
  {id:'seal',name:'Binding Seal',desc:'Freezes one enemy ~3s.',price:75,max:2,con:'seal'},
  {id:'heart',name:'Heart Container #5',desc:'Permanently +1 container (max HP +2).',price:300,max:1,special:'heart'},
  {id:'charge',name:'Aether Charge',desc:'+1 ranged bolt. Requires Aether Shard.',price:10,req:'ranged'},
];
function openShop(){
  G.open=true;
  renderShop();
  document.getElementById('shopScreen').classList.add('show');
}
function closeShop(){
  document.getElementById('shopScreen').classList.remove('show');
  G.open=false;
}
function renderShop(){
  const gold=document.getElementById('shopGold');
  gold.textContent='Your Sigils: ◆ '+G.gold;
  const grid=document.getElementById('shopGrid');grid.innerHTML='';
  SHOP_ITEMS.forEach(it=>{
    const locked=it.req==='ranged'&&!G.hasRanged;
    const sold=it.id==='heart'?G.inv.soldHeart:0;
    const maxed=locked||(it.max!==Infinity&&sold>=it.max);
    const card=document.createElement('div');
    card.className='card'+(locked?' locked':'');
    const icon=it.id==='stem'?'🌿':(it.id==='seal'?'🔷':(it.id==='heart'?'❤️':'🔋'));
    card.innerHTML=`<div class="ic">${icon}</div><div class="nm">${it.name}</div>
      <div class="ds">${it.desc}</div>
      <div class="pr">${locked?'LOCKED (Zone 4+)':(maxed?'SOLD OUT':'◆ '+it.price)}</div>`;
    const b=document.createElement('button');
    b.className='btn';b.style.width='100%';b.style.marginTop='8px';
    b.disabled=locked||maxed||G.gold<it.price;
    b.textContent=(locked||maxed)?'':'Buy';
    b.onclick=()=>buyItem(it);
    card.appendChild(b);
    grid.appendChild(card);
  });
}
function buyItem(it){
  if(G.gold<it.price)return;
  G.gold-=it.price;
  if(it.con){G.inv[it.con]++;G.inv['sold'+(it.con==='stem'?'Stem':'Seal')]++;}
  if(it.special==='heart'){G.inv.soldHeart++;G.player.maxContainers++;G.player.containers++;G.player.hp=Math.min(G.player.maxHp,G.player.hp+2);}
  if(it.req==='ranged'){G.shard++;G.hasRanged=true;}
  Audio.buy();
  emitP(G.player.x,G.player.y,10,{color:'#f5d98a',sp:2,life:24,r:3});
  renderShop();updateHUD();updateInvBar();
}

// ============================================================
// INVENTORY
// ============================================================
function toggleInv(){
  if(G.open){closeInv();return;}
  G.open=true;renderInv();
  document.getElementById('invScreen').classList.add('show');
}
function closeInv(){
  document.getElementById('invScreen').classList.remove('show');
  G.open=false;
}
function renderInv(){
  const acc=document.getElementById('invAcc');acc.innerHTML='';
  const accs=[
    {k:'band',name:'Warden\'s Band',desc:'Faster melee attack speed.',icon:'💍',have:G.acc.band},
    {k:'lantern',name:'Phantom Lantern',desc:'Reveals hidden paths & cracks.',icon:'🏮',have:G.acc.lantern},
    {k:'cloak',name:'Shadow Cloak',desc:'Reduces trap damage.',icon:'🧥',have:G.acc.cloak},
  ];
  accs.forEach(a=>{
    const card=document.createElement('div');
    card.className='card'+(a.have?'':' locked')+(G.acc[a.k]?' equipped':'');
    card.innerHTML=`<div class="ic">${a.icon}</div><div class="nm">${a.name}</div>
      <div class="ds">${a.have?a.desc:'Not found yet'}</div>`;
    if(a.have){
      const b=document.createElement('button');b.className='btn';b.style.width='100%';b.style.marginTop='8px';
      b.textContent=G.acc[a.k]?'EQUIPPED':'Equip';
      b.onclick=()=>{G.acc[a.k]=!G.acc[a.k];if(a.k==='band')G.player.hasBand=G.acc.band;renderInv();updateInvBar();};
      card.appendChild(b);
    }
    acc.appendChild(card);
  });
  const con=document.getElementById('invCon');con.innerHTML='';
  const cons=[
    {k:'stem',name:'Vital Stem',desc:'Restore 4 HP.',icon:'🌿',cnt:G.inv.stem},
    {k:'seal',name:'Binding Seal',desc:'Freeze an enemy.',icon:'🔷',cnt:G.inv.seal},
  ];
  cons.forEach(c=>{
    const card=document.createElement('div');card.className='card';
    card.innerHTML=`<div class="ic">${c.icon}</div><div class="nm">${c.name}</div>
      <div class="ds">${c.desc}</div><div class="pr">×${c.cnt}</div>`;
    if(c.cnt>0){
      const b=document.createElement('button');b.className='btn';b.style.width='100%';b.style.marginTop='8px';
      b.textContent='Use';
      b.onclick=()=>useCon(c.k);
      card.appendChild(b);
    }
    con.appendChild(card);
  });
}
function useCon(k){
  if(k==='stem'&&G.inv.stem>0){
    G.inv.stem--;
    const heal=(G.player.maxContainers*2)-G.player.hp;
    G.player.hp=Math.min(G.player.maxHp,G.player.hp+4);
    Audio.chime();emitP(G.player.x,G.player.y,12,{color:'#70e070',sp:2,life:24,r:3});
    floater(G.player.x,G.player.y-30,'+4 HP','#70e070');
  }else if(k==='seal'&&G.inv.seal>0){
    G.inv.seal--;
    // freeze nearest enemy
    let target=null,bd=1e9;
    for(const en of G.enemies){if(!en.alive)continue;const d=dist(G.player.x,G.player.y,en.x,en.y);if(d<300&&d<bd){bd=d;target=en;}}
    if(target){target.stun=180;Audio.unlock();floater(target.x,target.y-24,'FROZEN','#7fd0ff');}
    else toast('No enemy in range');
  }
  updateInvBar();renderInv();
}

// ============================================================
// HUD
// ============================================================
function updateHUD(){
  // hearts
  const h=document.getElementById('hearts');h.innerHTML='';
  for(let i=0;i<G.player.maxContainers;i++){
    const full=Math.min(2,G.player.hp-2*i);
    const cv=document.createElement('svg');cv.setAttribute('viewBox','0 0 24 24');cv.style.width='22px';cv.style.height='22px';
    const col=full===2?'#e0405b':full===1?'#e0405b':'#3a2028';
    const path=document.createElementNS('http://www.w3.org/2000/svg','path');
    path.setAttribute('d','M12 20 C6 14 2 10 2 6.5 C2 3.5 4.5 1.5 7.5 2.5 C9.5 3 11 4.5 12 6 C13 4.5 14.5 3 16.5 2.5 C19.5 1.5 22 3.5 22 6.5 C22 10 18 14 12 20 Z');
    path.setAttribute('fill',col);path.setAttribute('stroke','#2a1015');path.setAttribute('stroke-width','1');
    cv.appendChild(path);h.appendChild(cv);
  }
  document.getElementById('goldAmt').textContent=G.gold;
  document.getElementById('shardAmt').textContent=G.shard;
  document.getElementById('zoneName').textContent=G.cur.name;
  document.getElementById('zoneSub').textContent=G.cur.sub;
}
function updateInvBar(){
  const bar=document.getElementById('invbar');bar.innerHTML='';
  const slots=[
    {k:'stem',icon:'🌿',cnt:G.inv.stem},
    {k:'seal',icon:'🔷',cnt:G.inv.seal},
    {k:'ranged',icon:'◈',cnt:G.hasRanged?G.shard:0,disabled:!G.hasRanged},
  ];
  slots.forEach((s,i)=>{
    const d=document.createElement('div');d.className='invslot';
    if(s.disabled)d.style.opacity='.4';
    d.innerHTML=`<span>${s.icon}</span><span class="cnt">${s.cnt}</span><span class="key">${i+1}</span>`;
    d.onclick=()=>{ if(s.k==='stem')useCon('stem'); else if(s.k==='seal')useCon('seal'); else if(s.k==='ranged')fireRanged(); };
    bar.appendChild(d);
  });
}

// ============================================================
// MESSAGES / TOASTS
// ============================================================
function toast(t,big){
  const m=document.getElementById('msg');
  const d=document.createElement('div');
  d.className='line'+(big?' big':'');d.textContent=t;
  m.appendChild(d);
  setTimeout(()=>d.remove(),3800);
}
function floatAnim(){
  const f=document.getElementById('flash');
  f.classList.add('flash');f.style.background='radial-gradient(circle,rgba(255,215,140,.5),transparent 70%)';
  f.style.opacity='.6';f.style.transition='opacity .6s';
  setTimeout(()=>{f.style.opacity='0';},50);
  setTimeout(()=>{f.classList.remove('flash');},700);
}

// ============================================================
// GAME FLOW
// ============================================================
function saveState(){
  return{
    wp:G.player.weapon,hp:G.player.hp,maxHp:G.player.maxHp,
    containers:G.player.containers,maxContainers:G.player.maxContainers,
    gold:G.gold,shard:G.shard,hasRanged:G.hasRanged,
    inv:G.inv,acc:G.acc,saved:G.saved,
    flags:G.flags,entered:Object.keys(G.playerEntered),
  };
}
function restoreState(s){
  const p=G.player;
  p.weapon=s.wp;p.maxHp=s.maxHp;p.containers=s.containers;p.maxContainers=s.maxContainers;
  p.hp=s.hp;
  G.gold=s.gold;G.shard=s.shard;G.hasRanged=s.hasRanged;
  G.inv=Object.assign({},s.inv);G.acc=Object.assign({},s.acc);
  G.saved=s.saved;G.flags=s.flags||{};
  if(s.entered)G.playerEntered={};s.entered.forEach(k=>G.playerEntered[k]=true);
  G.player.hasBand=G.acc.band;
}
function winGame(){
  if(G.won)return;
  G.won=true;G.running=false;
  Audio.win();
  emitP(G.player.x,G.player.y,60,{color:'#ffe27a',sp:4,life:90,r:5});
  setTimeout(()=>document.getElementById('winScreen').classList.add('show'),900);
}
function gameOver(){
  G.over=true;G.running=false;
  Audio.hurt();
  document.getElementById('deathScreen').classList.add('show');
}
function respawn(){
  document.getElementById('deathScreen').classList.remove('show');
  // restore from save if available
  let s=null;try{s=JSON.parse(localStorage.getItem('wardenSave'));}catch(e){}
  if(s){
    // place at save zone spawn
    restoreState(s);
    loadZone(s.saveZone||'z1');
    G.player.x=G.cur.spawns[0].x;G.player.y=G.cur.spawns[0].y;
  }else{
    resetGame();
  }
  G.over=false;G.running=true;
  updateHUD();updateInvBar();
}
function resetGame(){
  G.player=makePlayer();
  G.zones=makeZones();
  G.playerEntered={};
  G.gold=180;G.shard=0;G.hasRanged=false;
  G.inv={stem:1,seal:0,soldStem:0,soldSeal:0,soldHeart:0};
  G.acc={band:false,lantern:false,cloak:false};
  G.flags={};G.saved=false;G.won=false;G.over=false;
  loadZone('z1');
  G.player.x=G.zones[0].spawns[0].x;G.player.y=G.zones[0].spawns[0].y;
}

// ============================================================
// ANG HELPERS
// ============================================================
function normalizeAng(a){while(a>Math.PI)a-=2*Math.PI;while(a<-Math.PI)a+=2*Math.PI;return a;}

// ============================================================
// UPDATE
// ============================================================
let lastT=now();
function loop(){
  requestAnimationFrame(loop);
  const t=now();let dt=(t-lastT)/1000;lastT=t;
  if(dt>0.05)dt=0.05;
  G.time+=dt;
  if(G.shakeT>0){G.shakeT-=dt;G.shake=0;}

  if(G.running&&!G.open){
    update(dt);
  }
  render();
  // handle pressed keys
  if(G.running&&!G.open){
    const p=G.player;
    if(p.attackCd>0)p.attackCd-=dt*1000;
    if(p.dashCd>0)p.dashCd-=dt*1000;
    if((pressed['j']||pressed['z'])&&p.attackCd<=0){ meleeStrike(); p.attackCd=WEAPONS[p.weapon].cd*(p.hasBand?0.85:1); Audio.whoosh(); p.attackT=0.18; }
    if((pressed['k']||pressed['x'])&&p.attackCd<=0){ fireRanged(); p.attackCd=150; }
    if(pressed['shift'])startDash();
  }
  for(const k in pressed)pressed[k]=false;

  // ambient sound occasionally
  if(G.running&&Math.random()<0.002)Audio.amb();
}

function startDash(){
  const p=G.player;
  if(p.dashCd>0||p.dashT>0)return;
  const dx=keys['w']||keys['arrowup']?-1:0, dy=keys['s']||keys['arrowdown']?1:0;
  const dl=keys['a']||keys['arrowleft']?-1:0, dr=keys['d']||keys['arrowright']?1:0;
  let ang=Math.atan2(dy,dr);
  if(dl===1)ang=Math.PI;
  if(dy===1&&!dx)ang=0;
  if(dy===-1&&!dx)ang=-Math.PI/2;
  if(dx===1&&!dy)ang=Math.PI/2;
  p.dir=ang;p.dashT=0.16;p.dashCd=700;p.invuln=Math.max(p.invuln,120);
  Audio.dash();
}

function update(dt){
  const p=G.player;
  // movement input
  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;
  if(keys['d']||keys['arrowright'])mx+=1;
  const ml=Math.hypot(mx,my);
  p.moving=ml>0;
  if(p.moving){
    const nx=mx/ml, ny=my/ml;
    p.dir=Math.atan2(ny,nx);
    let sp=p.speed;
    if(p.dashT>0)sp*=3.4;
    moveEntity(p,nx*sp,ny*sp);
    // footstep
    const metal=G.cur.id==='z2'||G.cur.id==='z3';
    if(G.time-G.lastFoot>0.28){Audio.foot(metal);G.lastFoot=G.time;}
    p.animT+=dt*10;
  }
  if(p.dashT>0)p.dashT-=dt;
  if(p.invuln>0)p.invuln-=dt*1000;
  if(p.hurtFlash>0)p.hurtFlash-=dt*3;
  if(p.attackT>0)p.attackT-=dt; else if(p.attackT<=0&&p.attackCd<=0){}
  if(p.attackCd>0)p.attackCd-=dt*1000;

  // ember trail for ignited/solar
  const w=WEAPONS[p.weapon];
  if(w.trail&&p.moving&&Math.random()<0.4){
    emitP(p.x,p.y,1,{color:w.color,sp:0.3,life:20,r:3,grav:0});
  }

  updateEnemies(dt);
  updateBullets(dt);
  updateParticles(dt);
  updateFloaters(dt);
  updatePickups(dt);
  updateEnts(dt);

  // camera follow
  G.cam.x+=(p.x-VW/2-G.cam.x)*0.12;
  G.cam.y+=(p.y-VH/2-G.cam.y)*0.12;
  G.cam.x=clamp(G.cam.x,-200,1200-G.cam.x<0?-G.cam.x:200);
  G.cam.y=clamp(G.cam.y,-200,700-G.cam.y<0?-G.cam.y:200);

  // enemy near player -> damage
  for(const en of G.enemies){
    if(!en.alive)continue;
    if(dist(p.x,p.y,en.x,en.y)<en.r+p.w/2&&p.invuln<=0&&en.stun<=0){
      takeDamage(en.dmg,en);
    }
  }

  // door transitions
  for(const e of G.ents){
    if(e.type==='door'){
      const rect={x:e.x,y:e.y,w:e.w,h:e.h};
      if(rectsOverlap({x:p.x-p.w/2,y:p.y-p.h/2,w:p.w,h:p.h},rect)){
        loadZone(e.to);
        G.player.x=e.tx;G.player.y=e.ty;
        break;
      }
    }
  }

  if(p.hp<=0){p.hp=0;gameOver();}
}

function takeDamage(dmg,src){
  const p=G.player;
  if(p.invuln>0)return;
  if(G.acc.cloak&&src&&src.type==='trap')dmg*=0.5;
  p.hp-=dmg;
  p.hurtFlash=1;
  p.invuln=700;
  Audio.hurt();
  const f=document.getElementById('flash');
  f.style.background='#d12f2f';f.style.opacity='.5';f.style.transition='none';
  setTimeout(()=>{f.style.opacity='0';},60);
  emitP(p.x,p.y,12,{color:'#ff3030',sp:3,life:20,r:3});
  floater(p.x,p.y-30,'-'+dmg,'#ff5050');
  G.shake=6;G.shakeT=0.3;
  updateHUD();
}

function updateEnemies(dt){
  for(const en of G.enemies){
    if(!en.alive)continue;
    en.animT+=dt;en.phase+=dt;
    if(en.hitFlash>0)en.hitFlash-=dt*5;
    if(en.stun>0){en.stun-=1;continue;}
    const d=dist(en.x,en.y,G.player.x,G.player.y);
    en.aggro=d<320;
    if(en.aggro){
      const ang=Math.atan2(G.player.y-en.y,G.player.x-en.x);
      let sp=en.speed;
      if(en.type==='boss')sp*=1.1;
      moveEntity(en,Math.cos(ang)*sp,Math.sin(ang)*sp);
      en.dir=ang;
      if(en.ranged&&en.atkCd<=0&&d<280){
        en.atkCd=1600;
        G.bullets.push({x:en.x,y:en.y,vx:Math.cos(ang)*4,vy:Math.sin(ang)*4,r:5,life:90,dmg:1,fromPlayer:false,color:'#ff6a6a'});
      }
    }
    if(en.atkCd>0)en.atkCd-=dt*1000;
  }
  // remove dead
  G.enemies=G.enemies.filter(e=>e.alive||e.hitFlash>0);
}

function updateBullets(dt){
  for(const b of G.bullets){
    b.x+=b.vx;b.y+=b.vy;b.life-=1;
    if(solidAt(b.x,b.y)){emitP(b.x,b.y,5,{color:b.color||'#7fd0ff',sp:2,life:14,r:2});b.life=0;continue;}
    if(b.fromPlayer){
      for(const en of G.enemies){
        if(!en.alive)continue;
        if(dist(b.x,b.y,en.x,en.y)<en.r+b.r){
          damageEnemy(en,b.dmg);
          emitP(b.x,b.y,6,{color:'#7fd0ff',sp:2,life:14,r:2});
          b.life=0;break;
        }
      }
    }else{
      if(dist(b.x,b.y,G.player.x,G.player.y)<G.player.w/2+b.r){
        takeDamage(b.dmg,null);b.life=0;
      }
    }
  }
  G.bullets=G.bullets.filter(b=>b.life>0);
}

function updateParticles(dt){
  for(const p of G.particles){
    p.x+=p.vx;p.y+=p.vy;p.vx*=0.92;p.vy*=0.92;p.vy+=p.grav;
    p.life-=1;
  }
  G.particles=G.particles.filter(p=>p.life>0);
}
function updateFloaters(dt){
  for(const f of G.floaters){f.y-=0.5;f.life-=1;}
  G.floaters=G.floaters.filter(f=>f.life>0);
}
function updatePickups(dt){
  for(const pk of G.pickups){pk.life-=1;if(pk.takeT)pk.takeT-=1;
    if(dist(pk.x,pk.y,G.player.x,G.player.y)<24){
      if(pk.type==='stem'){G.inv.stem++;Audio.chime();floater(pk.x,pk.y,'+Vital Stem','#70e070');}
      G.pickups=G.pickups.filter(x=>x!==pk);
    }
  }
  G.pickups=G.pickups.filter(p=>p.life>0);
}
function updateEnts(dt){
  // animate switches/altars
  for(const e of G.ents){
    if(e.type==='switch'){e.tE=(e.tE||0)+dt;if(e.off)e.tE*=0.9;}
  }
}

// ============================================================
// RENDER
// ============================================================
function render(){
  ctx.clearRect(0,0,VW,VH);
  if(!G.cur||!G.player)return;
  const cx=G.cam.x,cy=G.cam.y;
  let sx=0,sy=0;
  if(G.shakeT>0){sx=rand(-G.shake,G.shake);sy=rand(-G.shake,G.shake);}
  ctx.save();
  ctx.translate(sx,sy);
  ctx.translate(-cx,-cy);

  // bg
  ctx.fillStyle=G.cur.bg;
  ctx.fillRect(cx,cy,VW,VH);
  // floor grid
  ctx.strokeStyle='rgba(255,255,255,.03)';ctx.lineWidth=1;
  for(let x=Math.ceil(cx/T)*T;x<cx+VW;x+=T){ctx.beginPath();ctx.moveTo(x,cy);ctx.lineTo(x,cy+VH);ctx.stroke();}
  for(let y=Math.ceil(cy/T)*T;y<cy+VH;y+=T){ctx.beginPath();ctx.moveTo(cx,y);ctx.lineTo(cx+VW,y);ctx.stroke();}

  // walls
  for(const w of G.walls){
    if(w.break&&w.hp<=0)continue;
    if(w.x>w.x+T+cx+VW||w.x<w.x-cx-VW)continue;
    drawWall(w);
  }
  // entities
  for(const e of G.ents)drawEnt(e);
  // pickups
  for(const pk of G.pickups)drawPk(pk);
  // enemies
  for(const en of G.enemies)if(en.alive)drawEnemy(en);
  // player
  drawPlayer();
  // bullets
  for(const b of G.bullets)drawBullet(b);
  // particles
  for(const p of G.particles)drawParticle(p);
  // floaters
  for(const f of G.floaters)drawFloater(f);

  ctx.restore();
  drawMinimap();
}

function drawWall(w){
  const breakable=w.break;
  if(breakable){
    const t=w.hp/w.maxhp;
    ctx.fillStyle='#5a4a3a';
    ctx.fillRect(w.x,w.y,w.w,w.h);
    ctx.fillStyle='#4a3a2a';
    ctx.fillRect(w.x+2,w.y+2,w.w-4,w.h-4);
    // cracks
    ctx.strokeStyle='#2a2018';ctx.lineWidth=2;
    ctx.beginPath();ctx.moveTo(w.x+10,w.y+5);ctx.lineTo(w.x+20,w.y+18);ctx.lineTo(w.x+15,w.y+30);ctx.stroke();
    ctx.beginPath();ctx.moveTo(w.x+25,w.y+8);ctx.lineTo(w.x+22,w.y+25);ctx.stroke();
  }else{
    // bronze stone wall
    ctx.fillStyle='#2a3346';
    ctx.fillRect(w.x,w.y,w.w,w.h);
    ctx.fillStyle='#333f54';
    ctx.fillRect(w.x+2,w.y+2,w.w-4,w.h-4);
    // rivets
    ctx.fillStyle='#e7b954';
    ctx.beginPath();ctx.arc(w.x+8,w.y+8,2,0,7);ctx.fill();
    ctx.beginPath();ctx.arc(w.x+w.w-8,w.y+w.h-8,2,0,7);ctx.fill();
    ctx.fillStyle='#8a6a30';
    ctx.fillRect(w.x+2,w.y+w.h-4,w.w-4,3);
  }
}

function drawEnt(e){
  const t=G.time;
  if(e.type==='sanctuary'){
    const glow=0.6+0.4*Math.sin(t*3);
    ctx.save();ctx.shadowColor='#f5d98a';ctx.shadowBlur=20*glow;
    ctx.fillStyle=e.used?'#c9a06a':'#f5d98a';
    ctx.beginPath();ctx.moveTo(e.x,e.y-22);ctx.lineTo(e.x+14,e.y+8);ctx.lineTo(e.x-14,e.y+8);ctx.closePath();ctx.fill();
    ctx.fillStyle=e.used?'#8a7040':'#e7b954';
    ctx.fillRect(e.x-12,e.y+8,24,10);
    ctx.restore();
    if(!e.used){emitP(e.x,e.y-6,1,{color:'#f5d98a',sp:0.5,life:30,r:2});}
  }else if(e.type==='shop'){
    ctx.fillStyle='#7a4a2a';ctx.fillRect(e.x-16,e.y-14,32,28);
    ctx.fillStyle='#e7b954';ctx.beginPath();ctx.moveTo(e.x-20,e.y-14);ctx.lineTo(e.x+20,e.y-14);ctx.lineTo(e.x,e.y-26);ctx.closePath();ctx.fill();
    ctx.fillStyle='#f5d98a';ctx.font='14px sans-serif';ctx.textAlign='center';ctx.fillText('◆',e.x,e.y+4);
    ctx.fillStyle='#ffd';ctx.font='10px sans-serif';ctx.fillText('SHOP',e.x,e.y+26);
  }else if(e.type==='altar'){
    const glow=0.6+0.4*Math.sin(t*2.5);
    ctx.save();ctx.shadowColor=e.item==='solar'?'#ffe27a':'#ff8a3a';ctx.shadowBlur=24*glow;
    ctx.fillStyle='#4a4a5a';ctx.fillRect(e.x-16,e.y-16,32,32);
    ctx.fillStyle=e.item==='solar'?'#ffe27a':'#ff8a3a';
    ctx.beginPath();ctx.arc(e.x,e.y,10,0,7);ctx.fill();
    ctx.restore();
    ctx.fillStyle='#fff';ctx.font='9px sans-serif';ctx.textAlign='center';ctx.fillText('ALTAR',e.x,e.y+30);
  }else if(e.type==='chest'){
    if(e.open){ctx.fillStyle='#3a2a1a';ctx.fillRect(e.x-10,e.y-6,20,12);}
    else{ctx.fillStyle='#8a6a30';ctx.fillRect(e.x-11,e.y-9,22,18);ctx.fillStyle='#e7b954';ctx.fillRect(e.x-3,e.y-2,6,6);}
  }else if(e.type==='health'){
    if(e.taken){ctx.fillStyle='#3a2028';ctx.beginPath();ctx.arc(e.x,e.y,8,0,7);ctx.fill();}
    else{
      ctx.save();ctx.shadowColor='#ff5070';ctx.shadowBlur=16;
      ctx.fillStyle='#e0405b';
      ctx.beginPath();ctx.moveTo(e.x,e.y+8);ctx.quadraticCurveTo(e.x-10,e.y-6,e.x-4,e.y-10);
      ctx.quadraticCurveTo(e.x,e.y-4,e.x+4,e.y-10);ctx.quadraticCurveTo(e.x+10,e.y-6,e.x,e.y+8);ctx.fill();
      ctx.restore();
    }
  }else if(e.type==='switch'){
    ctx.fillStyle='#2a3346';ctx.beginPath();ctx.arc(e.x,e.y,14,0,7);ctx.fill();
    ctx.fillStyle=e.off?'#7fd0ff':'#5a4a3a';
    ctx.beginPath();ctx.arc(e.x,e.y,8,0,7);ctx.fill();
    if(e.off){ctx.save();ctx.shadowColor='#7fd0ff';ctx.shadowBlur=12;ctx.beginPath();ctx.arc(e.x,e.y,8,0,7);ctx.fill();ctx.restore();}
  }else if(e.type==='shrine'){
    ctx.save();ctx.shadowColor='#7fd0ff';ctx.shadowBlur=20;
    ctx.fillStyle='#3a4a5a';ctx.fillRect(e.x-14,e.y-18,28,36);
    ctx.fillStyle='#7fd0ff';ctx.beginPath();ctx.arc(e.x,e.y,8,0,7);ctx.fill();
    ctx.restore();
  }else if(e.type==='door'){
    ctx.fillStyle='rgba(120,90,50,.25)';ctx.fillRect(e.x,e.y,e.w,e.h);
    ctx.strokeStyle='#e7b954';ctx.lineWidth=2;ctx.strokeRect(e.x+2,e.y+2,e.w-4,e.h-4);
    ctx.fillStyle='#f5d98a';ctx.font='10px sans-serif';ctx.textAlign='center';
    ctx.fillText('▼',e.x+e.w/2,e.y+e.h/2+4);
  }else if(e.type==='beacon'){
    const ready=G.flags.beaconReady;
    ctx.save();
    ctx.shadowColor=ready?'#ffe27a':'#556';ctx.shadowBlur=ready?40:10;
    ctx.fillStyle=ready?'#ffe27a':'#3a3a4a';
    ctx.beginPath();ctx.moveTo(e.x,e.y-40);ctx.lineTo(e.x+16,e.y+30);ctx.lineTo(e.x-16,e.y+30);ctx.closePath();ctx.fill();
    ctx.restore();
    ctx.fillStyle='#fff';ctx.font='10px sans-serif';ctx.textAlign='center';
    ctx.fillText(ready?'BEACON':'CORE',e.x,e.y+46);
  }
}

function drawPk(pk){
  ctx.save();
  const gl=0.6+0.4*Math.sin(G.time*5);
  ctx.shadowColor='#70e070';ctx.shadowBlur=10*gl;
  ctx.fillStyle='#70e070';
  ctx.beginPath();ctx.arc(pk.x,pk.y,7,0,7);ctx.fill();
  ctx.restore();
}

function drawEnemy(en){
  const t=en.hitFlash>0;
  ctx.save();
  ctx.translate(en.x,en.y);
  // body
  ctx.fillStyle=t?'#fff':en.color;
  ctx.beginPath();ctx.arc(0,0,en.r,0,7);ctx.fill();
  // rust detail
  ctx.fillStyle='rgba(0,0,0,.3)';
  ctx.beginPath();ctx.arc(en.r*0.3,en.r*0.2,en.r*0.4,0,7);ctx.fill();
  // eyes
  ctx.fillStyle='#ff5050';
  ctx.beginPath();ctx.arc(-5,-3,3,0,7);ctx.arc(5,-3,3,0,7);ctx.fill();
  if(en.type==='boss'){
    // crown spikes
    ctx.fillStyle='#3a1a2a';
    for(let i=0;i<5;i++){const a=i/5*Math.PI*2-Math.PI/2;ctx.beginPath();ctx.moveTo(Math.cos(a)*en.r*0.7,Math.sin(a)*en.r*0.7);ctx.lineTo(Math.cos(a)*en.r*1.2,Math.sin(a)*en.r*1.2-4);ctx.lineTo(Math.cos(a+0.3)*en.r*0.7,Math.sin(a+0.3)*en.r*0.7);ctx.fill();}
  }
  if(en.stun>0){
    ctx.fillStyle='#7fd0ff';ctx.font='12px sans-serif';ctx.textAlign='center';ctx.fillText('✦',0,-en.r-8);
  }
  ctx.restore();
  // hp bar
  if(en.hp<en.maxhp){
    ctx.fillStyle='rgba(0,0,0,.6)';ctx.fillRect(en.x-en.r,en.y-en.r-10,en.r*2,4);
    ctx.fillStyle='#e0405b';ctx.fillRect(en.x-en.r,en.y-en.r-10,en.r*2*(en.hp/en.maxhp),4);
  }
}

function drawPlayer(){
  const p=G.player;
  const w=WEAPONS[p.weapon];
  const blink=p.invuln>0&&(Math.floor(G.time*20)%2===0);
  ctx.save();
  ctx.translate(p.x,p.y);
  if(blink)ctx.globalAlpha=0.4;
  // shadow
  ctx.fillStyle='rgba(0,0,0,.3)';ctx.beginPath();ctx.ellipse(0,p.w/2+2,p.w/2+2,4,0,0,7);ctx.fill();
  // cloak
  ctx.fillStyle='#3a4a5a';
  ctx.beginPath();ctx.arc(0,0,p.w/2+2,0,7);ctx.fill();
  // cloak hem (shadow cloak)
  if(G.acc.cloak){ctx.fillStyle='#2a1a2a';ctx.beginPath();ctx.arc(0,p.w/2+2,p.w/2+2,0,Math.PI);ctx.fill();}
  // body center
  ctx.fillStyle='#5a6a7a';
  ctx.beginPath();ctx.arc(0,0,p.w/2-2,0,7);ctx.fill();
  // dir indicator / hood
  ctx.fillStyle='#e9e2d0';
  ctx.beginPath();ctx.arc(Math.cos(p.dir)*4,Math.sin(p.dir)*4,4,0,7);ctx.fill();
  // weapon
  drawWeapon(w);
  // off-hand crystal (ranged)
  if(G.hasRanged){
    ctx.fillStyle='#7fd0ff';ctx.save();
    ctx.rotate(p.dir+Math.PI/2);ctx.beginPath();ctx.arc(-8,10,4,0,7);ctx.fill();ctx.restore();
  }
  // band ring
  if(G.acc.band){ctx.fillStyle='#c9d0d8';ctx.beginPath();ctx.arc(p.w/2+3,0,3,0,7);ctx.fill();}
  // lantern glow
  if(G.acc.lantern){ctx.save();ctx.shadowColor='#7fd0ff';ctx.shadowBlur=16;ctx.fillStyle='rgba(127,208,255,.5)';ctx.beginPath();ctx.arc(-10,-8,4,0,7);ctx.fill();ctx.restore();}
  // hurt flash
  if(p.hurtFlash>0){ctx.fillStyle='rgba(220,50,50,'+(p.hurtFlash*0.4)+')';ctx.beginPath();ctx.arc(0,0,p.w/2+6,0,7);ctx.fill();}
  ctx.restore();
}
function drawWeapon(w){
  ctx.save();
  ctx.rotate(pdir());
  ctx.strokeStyle=w.color;ctx.lineWidth=4;ctx.lineCap='round';
  const reach=w.wide?w.reach+6:w.reach;
  ctx.beginPath();ctx.moveTo(4,0);ctx.lineTo(reach,0);ctx.stroke();
  // blade glow
  ctx.save();ctx.shadowColor=w.color;ctx.shadowBlur=10;ctx.strokeStyle=w.color;ctx.lineWidth=3;
  ctx.beginPath();ctx.moveTo(4,0);ctx.lineTo(reach,0);ctx.stroke();ctx.restore();
  if(w.wide){ctx.fillStyle=w.color;ctx.beginPath();ctx.moveTo(reach,0);ctx.lineTo(reach-8,-6);ctx.lineTo(reach-8,6);ctx.closePath();ctx.fill();}
  ctx.restore();
}
function pdir(){return G.player.dir;}

function drawBullet(b){
  ctx.save();ctx.shadowColor=b.color||'#7fd0ff';ctx.shadowBlur=10;
  ctx.fillStyle=b.color||'#7fd0ff';ctx.beginPath();ctx.arc(b.x,b.y,b.r,0,7);ctx.fill();
  ctx.restore();
}
function drawParticle(p){
  ctx.globalAlpha=clamp(p.life/p.max,0,1);
  ctx.fillStyle=p.color;ctx.beginPath();ctx.arc(p.x,p.y,p.r,0,7);ctx.fill();
  ctx.globalAlpha=1;
}
function drawFloater(f){
  ctx.globalAlpha=clamp(f.life/f.max,0,1);
  ctx.fillStyle=f.color;ctx.font='bold 14px sans-serif';ctx.textAlign='center';
  ctx.fillText(f.txt,f.x,f.y);ctx.globalAlpha=1;
}

function drawMinimap(){
  mctx.clearRect(0,0,172,132);
  mctx.fillStyle='rgba(8,10,16,.9)';mctx.fillRect(0,0,172,132);
  const z=G.cur;
  // scale zone to fit (assume max ~1000x560)
  const sx=172/1000, sy=132/560;
  // walls
  mctx.fillStyle='#3a4a5a';
  for(const w of z.walls){mctx.fillRect(w.x*sx,w.y*sy,Math.max(2,w.w*sx),Math.max(2,w.h*sy));}
  // ents
  for(const e of z.ents){
    if(e.type==='sanctuary'){mctx.fillStyle='#f5d98a';mctx.beginPath();mctx.arc(e.x*sx,e.y*sy,2.5,0,7);mctx.fill();}
    else if(e.type==='shop'){mctx.fillStyle='#e7b954';mctx.fillRect(e.x*sx-2,e.y*sy-2,4,4);}
    else if(e.type==='altar'){mctx.fillStyle=e.item==='solar'?'#ffe27a':'#ff8a3a';mctx.beginPath();mctx.arc(e.x*sx,e.y*sy,2.5,0,7);mctx.fill();}
    else if(e.type==='door'){mctx.fillStyle='#7fd0ff';mctx.fillRect(e.x*sx-1,e.y*sy-1,3,3);}
  }
  // player
  mctx.fillStyle='#fff';
  mctx.beginPath();mctx.arc(G.player.x*sx,G.player.y*sy,3,0,7);mctx.fill();
  // enemies
  mctx.fillStyle='#ff5050';
  for(const en of G.enemies){if(en.alive){mctx.beginPath();mctx.arc(en.x*sx,en.y*sy,2,0,7);mctx.fill();}}
}

// ============================================================
// INIT
// ============================================================
function startGame(){
  document.getElementById('titleScreen').classList.remove('show');
  Audio.resume();
  resetGame();
  G.running=true;
  updateHUD();updateInvBar();
  toast('The Ashen Threshold',true);
}

document.getElementById('startBtn').onclick=startGame;
document.getElementById('respawnBtn').onclick=respawn;
document.getElementById('shopClose').onclick=closeShop;
document.getElementById('invClose').onclick=closeInv;
document.getElementById('winBtn').onclick=()=>location.reload();

// init
G.player=makePlayer();
G.zones=makeZones();
loadZone('z1');
G.player.x=G.zones[0].spawns[0].x;G.player.y=G.zones[0].spawns[0].y;
updateHUD();updateInvBar();
loop();
</script>
</body>
</html>
<!-- agent-meta {"model":"bartowski/Ornith-1.5-35B-A3B","provider":"brigid","persona":"full","sessionId":"4174e7d8-fb4b-4e65-8252-ed2f191ae6d2","tokensIn":7690488,"tokensOut":90994,"tokensTotal":7781482,"cost":0,"turns":91,"toolCalls":90,"failedToolCalls":0,"timestamp":"2026-08-23T23:31:20.720Z"} -->