Warden
pareto · full

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 76,555 bytes · SHA-256 af63213fc2cc
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>The Warden's Corrosion</title>
<style>
:root{
--gold:#d9a24a; --gold2:#f4c86a; --orange:#e07b39; --slate:#0c1018; --slate2:#131a28;
--ink:#e8e0cf; --dim:#8f9bb3; --red:#c0392b; --blue:#5aa7d6; --green:#7fbf6a; --panel:rgba(12,16,24,.92);
}
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%}
body{background:#05070b;color:var(--ink);font-family:Georgia,'Times New Roman',serif;overflow:hidden;
-webkit-user-select:none;user-select:none}
#stage{position:fixed;inset:0;display:flex;align-items:center;justify-content:center}
#wrap{position:relative}
canvas{display:block;background:#05070b;image-rendering:auto}
.overlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;
text-align:center;padding:24px;background:rgba(4,6,10,.86);backdrop-filter:blur(2px);z-index:30}
.hidden{display:none!important}
h1{font-size:clamp(26px,4.4vw,46px);letter-spacing:.14em;font-weight:400;
background:linear-gradient(180deg,#f6d98a,#b97b2e);-webkit-background-clip:text;background-clip:text;color:transparent;
text-shadow:0 0 26px rgba(217,162,74,.25)}
h2{font-size:clamp(17px,2.2vw,24px);letter-spacing:.1em;font-weight:400;color:var(--gold2);margin-bottom:10px}
p.sub{color:var(--dim);max-width:640px;line-height:1.6;font-size:14px;margin:12px auto 0}
.key{display:inline-block;border:1px solid #3a4358;border-bottom-width:3px;border-radius:4px;padding:1px 7px;margin:0 2px;
background:#1a2233;color:#cfd8ea;font-family:Consolas,monospace;font-size:12px}
.btn{margin-top:18px;padding:11px 34px;font-family:inherit;font-size:15px;letter-spacing:.18em;cursor:pointer;
color:#1a1206;background:linear-gradient(180deg,#f0c96e,#c08a33);border:1px solid #6e4c17;border-radius:3px;
box-shadow:0 0 24px rgba(217,162,74,.35)}
.btn:hover{filter:brightness(1.12)}
.btn:active{transform:translateY(1px)}
.btn:disabled{filter:grayscale(.8) brightness(.6);cursor:default}
.btn.ghost{background:none;color:var(--gold2);border:1px solid #4a3d22;box-shadow:none}
#hud{position:absolute;inset:0;pointer-events:none;z-index:20;font-family:Georgia,serif}
#hearts{position:absolute;top:12px;left:14px;display:flex;gap:3px;filter:drop-shadow(0 2px 3px rgba(0,0,0,.7))}
#goldbox{position:absolute;top:12px;right:14px;display:flex;align-items:center;gap:7px;padding:5px 11px;
background:rgba(10,13,20,.72);border:1px solid #3a3320;border-radius:16px}
#goldbox canvas{background:transparent}
#goldval{font-size:15px;color:var(--gold2);min-width:26px;text-align:right}
#zonelabel{position:absolute;top:12px;left:50%;transform:translateX(-50%);font-size:12px;letter-spacing:.3em;
color:#b7a877;text-transform:uppercase;text-shadow:0 1px 3px #000;white-space:nowrap}
#ammo{position:absolute;top:44px;right:14px;display:flex;align-items:center;gap:7px;padding:4px 11px;
background:rgba(10,13,20,.72);border:1px solid #23384a;border-radius:16px;font-size:13px;color:#9fd0f0}
#toasts{position:absolute;bottom:76px;left:50%;transform:translateX(-50%);display:flex;flex-direction:column;
align-items:center;gap:5px;width:100%;pointer-events:none}
.toast{padding:6px 18px;background:rgba(10,13,20,.85);border:1px solid #3d3417;border-radius:3px;font-size:13px;
color:var(--gold2);letter-spacing:.06em;animation:toastIn .25s ease-out;box-shadow:0 2px 14px rgba(0,0,0,.5)}
.toast.gain{color:#bfe3a8;border-color:#2c4222}
.toast.blue{color:#a8d4f0;border-color:#22384a}
@keyframes toastIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
.toast.out{opacity:0;transform:translateY(-8px);transition:all .4s}
#interact{position:absolute;bottom:120px;left:50%;transform:translateX(-50%);padding:7px 18px;font-size:14px;
background:rgba(10,13,20,.88);border:1px solid #4a3d22;border-radius:3px;color:var(--gold2);letter-spacing:.05em;
box-shadow:0 0 18px rgba(217,162,74,.25)}
#interact .key{margin-right:8px}
#bossbar{position:absolute;top:40px;left:50%;transform:translateX(-50%);width:min(480px,70vw)}
#bossname{font-size:11px;letter-spacing:.35em;color:#d8b9a0;text-align:center;margin-bottom:4px;text-transform:uppercase}
#bosstrack{height:9px;background:#1a0f0d;border:1px solid #5a3028;border-radius:2px;overflow:hidden}
#bossfill{height:100%;width:100%;background:linear-gradient(90deg,#a03028,#e07040);transition:width .15s}
#vignette{position:absolute;inset:0;pointer-events:none;z-index:15;opacity:0;transition:opacity .3s;
box-shadow:inset 0 0 130px 40px rgba(140,20,10,.55)}
#shimmer{position:absolute;inset:0;pointer-events:none;z-index:16;opacity:0;
box-shadow:inset 0 0 90px 10px rgba(240,200,110,.5)}
#flash{position:absolute;inset:0;pointer-events:none;z-index:17;opacity:0;background:#fff}
#shop .panelbox,#inv .panelbox{width:min(560px,92vw)}
.panelbox{background:var(--panel);border:1px solid #4a3d22;border-radius:4px;padding:20px 22px;text-align:left;
box-shadow:0 0 50px rgba(0,0,0,.7),inset 0 0 40px rgba(217,162,74,.05);max-height:84vh;overflow:auto}
.shoprow{display:flex;align-items:center;gap:12px;padding:9px 8px;border-bottom:1px solid #22201a}
.shoprow:last-child{border-bottom:none}
.shoprow .ic{width:34px;height:34px;flex:none;background:#0a0e15;border:1px solid #333;border-radius:3px;
display:flex;align-items:center;justify-content:center}
.shoprow .info{flex:1}
.shoprow .nm{font-size:14px;color:var(--ink)}
.shoprow .ds{font-size:11.5px;color:var(--dim);margin-top:2px;line-height:1.4}
.shoprow .pr{font-size:14px;color:var(--gold2);white-space:nowrap;margin-right:4px}
.sbtn{padding:6px 14px;font-family:inherit;font-size:12px;letter-spacing:.08em;cursor:pointer;color:#1a1206;
background:linear-gradient(180deg,#f0c96e,#c08a33);border:1px solid #6e4c17;border-radius:3px}
.sbtn:disabled{filter:grayscale(.9) brightness(.55);cursor:default}
.sbtn.ghost{background:none;color:#9fb6d8;border:1px solid #2c3a4c}
#invgrid{display:grid;grid-template-columns:repeat(5,1fr);gap:8px;margin:12px 0}
.slot{aspect-ratio:1;background:#0a0e15;border:1px solid #2e2a1e;border-radius:3px;position:relative;
display:flex;align-items:center;justify-content:center;cursor:pointer}
.slot:hover{border-color:var(--gold)}
.slot canvas{width:80%;height:80%}
.slot .cnt{position:absolute;bottom:1px;right:4px;font-size:11px;color:#cfd8ea;font-family:Consolas,monospace}
.slot .eq{position:absolute;top:1px;left:4px;font-size:9px;color:var(--gold2);letter-spacing:.05em}
#invdesc{min-height:44px;font-size:12.5px;color:var(--dim);line-height:1.5;padding:8px;border:1px dashed #2e2a1e;
border-radius:3px;margin-bottom:6px}
#invdesc b{color:var(--ink)}
#savednote{font-size:11px;color:#6d7a92;text-align:center;margin-top:6px;letter-spacing:.05em}
#dialog{position:absolute;bottom:26px;left:50%;transform:translateX(-50%);width:min(680px,92vw);z-index:25;
background:rgba(8,11,17,.94);border:1px solid #4a3d22;border-radius:4px;padding:14px 20px;pointer-events:none}
#dlgname{font-size:12px;letter-spacing:.28em;color:var(--gold2);text-transform:uppercase;margin-bottom:5px}
#dlgtext{font-size:14.5px;line-height:1.55;color:#d8d2c2}
#dlgtext .key{font-size:11px}
#dlgnext{font-size:11px;color:#6d7a92;text-align:right;margin-top:6px;letter-spacing:.1em}
#zonecard{position:absolute;top:32%;left:0;width:100%;text-align:center;pointer-events:none;z-index:18;opacity:0}
#zonecard.show{animation:cardIn 3.4s ease forwards}
#zc1{font-size:11px;letter-spacing:.5em;color:#8f9bb3;text-transform:uppercase}
#zc2{font-size:clamp(22px,3.4vw,34px);letter-spacing:.2em;color:var(--gold2);margin-top:6px;
text-shadow:0 0 30px rgba(217,162,74,.4)}
@keyframes cardIn{0%{opacity:0;transform:translateY(12px)}12%{opacity:1;transform:none}80%{opacity:1}100%{opacity:0}}
#fader{position:absolute;inset:0;background:#05070b;z-index:40;pointer-events:none;opacity:1;transition:opacity .9s}
#deathscr .big{font-size:clamp(24px,4vw,40px);letter-spacing:.3em;color:#c05038;text-shadow:0 0 40px rgba(160,40,20,.5)}
#credits{margin-top:14px;font-size:12px;color:var(--dim);line-height:1.7}
#mobilehint{display:none}
@media (pointer:coarse){#mobilehint{display:block}}
</style>
</head>
<body>
<div id="stage"><div id="wrap">
<canvas id="game" width="960" height="540"></canvas>
<div id="hud">
<div id="hearts"></div>
<div id="goldbox"><canvas id="coinico" width="14" height="14"></canvas><span id="goldval">0</span></div>
<div id="ammo" class="hidden"><canvas id="ammoico" width="14" height="14"></canvas><span id="ammoval">0</span></div>
<div id="zonelabel"></div>
<div id="bossbar" class="hidden"><div id="bossname"></div><div id="bosstrack"><div id="bossfill"></div></div></div>
<div id="toasts"></div>
<div id="interact" class="hidden"><span class="key">E</span><span id="intext"></span></div>
<div id="zonecard"><div id="zc1"></div><div id="zc2"></div></div>
</div>
<div id="vignette"></div><div id="shimmer"></div><div id="flash"></div>
<div id="dialog" class="hidden"><div id="dlgname"></div><div id="dlgtext"></div><div id="dlgnext">E / click — continue</div></div>
<div id="shop" class="overlay hidden"><div class="panelbox">
<h2>Sigil-Merchant's Altar</h2>
<p class="sub" style="margin:0 0 10px;font-size:12.5px">The merchant's bronze hands never still. "Gold for the Warden," it hums. "The Corrosion waits for no one."</p>
<div id="shoprows"></div>
<div style="text-align:center;margin-top:14px"><button class="btn ghost" id="shopclose">Leave (Q)</button></div>
</div></div>
<div id="inv" class="overlay hidden"><div class="panelbox">
<h2>Warden's Satchel</h2>
<div id="invgrid"></div>
<div id="invdesc">Select an item. Consumables are used with <span class="key">F</span> or by clicking. Accessories are toggled here.</div>
<div style="display:flex;gap:10px;justify-content:center">
<button class="btn ghost" id="invuse">Use / Toggle (F)</button>
<button class="btn ghost" id="invclose">Close (I)</button>
</div>
<div id="savednote"></div>
</div></div>
<div id="title" class="overlay">
<h1>THE WARDEN'S CORROSION</h1>
<p class="sub" style="letter-spacing:.3em;font-size:11px;color:#8f9bb3;margin-top:4px">AURELION'S SUNKEN OBSERVATORY</p>
<p class="sub">The archive floats above a storm-locked valley. Its keepers are dust; its halls fill with black-iron rot.
You are the Last Warden. Reignite the Core Beacon before the Corrosion claims the sky itself.</p>
<p class="sub" style="font-size:13px">
<span class="key">W A S D</span> move <span class="key">J</span>/<span class="key">Space</span> attack
<span class="key">K</span> fire shard <span class="key">E</span> interact
<span class="key">I</span> satchel <span class="key">F</span> use item <span class="key">Q</span> close</p>
<button class="btn" id="startbtn">BEGIN THE VIGIL</button>
<p id="mobilehint" class="sub" style="font-size:11px">A keyboard is required for this vigil.</p>
</div>
<div id="deathscr" class="overlay hidden">
<div class="big">THE VIGIL FAILS</div>
<p class="sub">The Corrosion takes another warden. But the Observatory remembers its keepers…</p>
<button class="btn" id="respawnbtn">RISE AT THE LAST SANCTUARY</button>
</div>
<div id="endscr" class="overlay hidden">
<h1>THE BEACON BURNS</h1>
<p class="sub">Gold floods the gears. The Observatory shudders — and holds. Far below, the storm parts around a pillar of light,
and in the halls the black iron retreats, hissing, back into the dark. The vigil is kept. There will be wardens again.</p>
<p id="credits"></p>
<button class="btn" id="againbtn">KEEP THE VIGIL AGAIN</button>
</div>
<div id="fader"></div>
</div></div>
<script>
"use strict";
/* ============================== utils ============================== */
const TAU=Math.PI*2;
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const lerp=(a,b,t)=>a+(b-a)*t;
const dist=(a,b)=>Math.hypot(a.x-b.x,a.y-b.y);
const rnd=(a,b)=>a+Math.random()*(b-a);
const irnd=(a,b)=>Math.floor(rnd(a,b+1));
let _seed=1234567;
function srand(s){_seed=s>>>0||1;}
function srnd(){_seed^=_seed<<13;_seed>>>=0;_seed^=_seed>>17;_seed^=_seed<<5;_seed>>>=0;return _seed/4294967296;}
function srndr(a,b){return a+srnd()*(b-a);}
const el=id=>document.getElementById(id);
const W=960,H=540;
const cvs=el('game'),ctx=cvs.getContext('2d');
function fit(){const s=Math.min(innerWidth/W,innerHeight/H);cvs.style.width=W*s+'px';cvs.style.height=H*s+'px';}
addEventListener('resize',fit);fit();
/* ============================== audio ============================== */
const AU={ac:null,master:null,musicGain:null,started:false,
init(){if(this.ac)return;try{this.ac=new (window.AudioContext||window.webkitAudioContext)();
this.master=this.ac.createGain();this.master.gain.value=.55;this.master.connect(this.ac.destination);
this.musicGain=this.ac.createGain();this.musicGain.gain.value=.30;this.musicGain.connect(this.master);}catch(e){}},
now(){return this.ac?this.ac.currentTime:0;},
tone(f,dur,type,vol,when,slideTo){if(!this.ac)return;const t=(when||this.now());
const o=this.ac.createOscillator(),g=this.ac.createGain();o.type=type||'sine';o.frequency.setValueAtTime(f,t);
if(slideTo)o.frequency.exponentialRampToValueAtTime(Math.max(20,slideTo),t+dur);
g.gain.setValueAtTime(0,t);g.gain.linearRampToValueAtTime(vol||.2,t+.012);
g.gain.exponentialRampToValueAtTime(.0001,t+dur);o.connect(g);g.connect(this.master);o.start(t);o.stop(t+dur+.05);},
noise(dur,vol,fc,when,q){if(!this.ac)return;const t=(when||this.now());const n=Math.floor(this.ac.sampleRate*dur);
const buf=this.ac.createBuffer(1,n,this.ac.sampleRate);const d=buf.getChannelData(0);
for(let i=0;i<n;i++)d[i]=(Math.random()*2-1)*(1-i/n);
const src=this.ac.createBufferSource();src.buffer=buf;
const f=this.ac.createBiquadFilter();f.type='bandpass';f.frequency.value=fc||800;f.Q.value=q||.8;
const g=this.ac.createGain();g.gain.setValueAtTime(vol||.25,t);g.gain.exponentialRampToValueAtTime(.0001,t+dur);
src.connect(f);f.connect(g);g.connect(this.master);src.start(t);src.stop(t+dur+.05);},
step(){this.noise(.09,.05,300+Math.random()*120,0,1.2);},
swing(){this.noise(.14,.16,1500,0,1);this.tone(220,.1,'triangle',.05,0,120);},
hit(){this.noise(.12,.22,500,0,1.5);this.tone(140,.12,'square',.1,0,70);},
burnHit(){this.noise(.2,.2,900,0,.7);this.tone(120,.18,'sawtooth',.09,0,55);this.noise(.3,.1,2400,0,.4);},
solarHit(){this.tone(70,.5,'sine',.28,0,42);this.tone(210,.35,'triangle',.12);this.noise(.2,.14,300,0,1);},
windup(){this.tone(90,.3,'sawtooth',.1,0,160);},
twang(){this.tone(1400,.16,'triangle',.16,0,700);this.tone(2100,.1,'sine',.08);},
chimeImpact(){this.tone(1900,.25,'sine',.12);this.tone(2533,.2,'sine',.07,this.now()+.05);},
hurt(){this.tone(110,.22,'square',.16,0,60);this.tone(1244,.3,'sine',.09,this.now()+.02,1100);this.noise(.15,.2,250);},
coin(){const t=this.now();this.tone(2300,.07,'square',.06,t);this.tone(3100,.09,'square',.05,t+.05);},
pickup(){[523,659,784].forEach((f,i)=>this.tone(f,.22,'sine',.11,this.now()+i*.07));},
rare(){[392,494,587,784].forEach((f,i)=>this.tone(f,.5,'sine',.12,this.now()+i*.1));
this.tone(196,.8,'triangle',.08);},
heart(){[262,330,392,523,660].forEach((f,i)=>this.tone(f,.6,'sine',.13,this.now()+i*.09));},
upgrade(){[196,294,392,587].forEach((f,i)=>this.tone(f,.7,'sawtooth',.07,this.now()+i*.12));
this.tone(98,1.2,'sine',.15);this.noise(.6,.08,600);},
plate(){this.tone(90,.25,'square',.13,0,60);this.noise(.12,.1,400);},
door(){this.noise(.7,.14,180,0,.6);this.tone(60,.7,'sine',.14,0,45);},
crumble(){this.noise(.5,.3,300,0,.5);this.noise(.35,.2,700,this.now()+.1);},
save(){[330,415,494,660].forEach((f,i)=>this.tone(f,.9,'sine',.1,this.now()+i*.13));},
switchHit(){this.tone(880,.3,'sine',.14,0,660);this.tone(1320,.25,'sine',.08,this.now()+.06);},
buy(){const t=this.now();this.tone(1800,.08,'square',.09,t);this.tone(2400,.12,'sine',.1,t+.07);
this.tone(1200,.25,'sine',.08,t+.14);},
deny(){this.tone(180,.18,'square',.12,0,140);},
freeze(){this.tone(1600,.4,'sine',.12,0,400);this.noise(.3,.06,3000,0,3);},
seal(){this.tone(660,.3,'sine',.1);this.tone(990,.4,'sine',.07,this.now()+.08);},
bossRoar(){this.tone(55,1.1,'sawtooth',.22,0,38);this.noise(.9,.18,220,0,.5);this.tone(110,.9,'square',.08,0,70);},
beacon(){[131,196,262,330,392,523].forEach((f,i)=>this.tone(f,2.2,'sine',.12,this.now()+i*.22));
this.tone(65,3.5,'sine',.16);this.noise(2,.06,500,0,.4);},
drone:null,
startDrone(){if(!this.ac||this.drone)return;const o1=this.ac.createOscillator(),o2=this.ac.createOscillator(),
g=this.ac.createGain();o1.type='sine';o1.frequency.value=55;o2.type='sine';o2.frequency.value=82.5;
g.gain.value=0;g.gain.linearRampToValueAtTime(.05,this.now()+1.2);o1.connect(g);o2.connect(g);g.connect(this.master);
o1.start();o2.start();this.drone={o1,o2,g};},
stopDrone(){if(!this.drone)return;const d=this.drone;this.drone=null;
d.g.gain.linearRampToValueAtTime(0,this.now()+.5);setTimeout(()=>{try{d.o1.stop();d.o2.stop();}catch(e){}},700);},
music:null,musicStep:0,
startMusic(){if(!this.ac||this.music)return;this.music=setInterval(()=>{if(!G.running||G.paused)return;
const sc=[0,3,5,7,10];const base=110*Math.pow(2,(G.zone-1)/12*2);
const n=sc[irnd(0,4)]+12*irnd(0,2);const f=base*Math.pow(2,n/12);
this.tone(f,1.4,'sine',.045);if(this.musicStep%4===0)this.tone(f/2,1.8,'triangle',.03);
this.musicStep++;},900);},
stopMusic(){if(this.music){clearInterval(this.music);this.music=null;}}
};
/* ============================== input ============================== */
const Keys={};let interactPressed=false;
addEventListener('keydown',e=>{if(e.repeat)return;const k=e.key.toLowerCase();Keys[k]=true;
if(['w','a','s','d',' ','arrowup','arrowdown','arrowleft','arrowright'].includes(k))e.preventDefault();
if(k==='e')interactPressed=true;
if(k==='i')toggleInv();
if(k==='q')toggleShop();
if(k==='f')useSelected();
if(k==='j'||k===' ')G.wantAttack=true;
if(k==='k')G.wantFire=true;});
addEventListener('keyup',e=>{Keys[e.key.toLowerCase()]=false;});
addEventListener('mousedown',e=>{if(e.target===cvs){G.wantAttack=true;}});
cvs.addEventListener('contextmenu',e=>e.preventDefault());
/* ============================== state ============================== */
const G={running:false,paused:false,zone:1,zoneT:0,time:0,frame:0,shake:0,shakeT:0,
gold:0,wantAttack:false,wantFire:false,aim:{x:1,y:0},
flags:{},checkpoint:null,selSlot:0,shopOpen:false,invOpen:false,dialogQ:[],dialogI:0,dialogCb:null,
dmgNums:[],floaters:[],parts:[],bullets:[],freezeT:0,transition:0,transitionTo:0,pendingZone:0,
stats:{kills:0,goldEarned:0,items:0}};
const P={x:120,y:300,vx:0,vy:0,r:11,speed:2.5,face:1,anim:0,moving:false,
hp:6,maxHp:6,containers:3,maxContainers:3,inv:[],iframes:0,attackT:0,attackCd:0,swingDir:1,
fireCd:0,ammo:0,weapon:0,accessories:{},emberTrail:[],stepT:0,dead:false};
const WEAPONS=[
{name:'Ember Blade',dmg:1,arc:.95,range:40,cd:24,knock:3.2,color:'#c8cdd6',type:'phys',windup:4},
{name:'Ignited Edge',dmg:2,arc:1.05,range:44,cd:17,knock:3.6,color:'#ff9a3d',type:'fire',windup:3},
{name:'Solar Falchion',dmg:3,arc:1.5,range:54,cd:30,knock:5,color:'#ffd75e',type:'solar',windup:9}];
const ITEMS={
wardenband:{name:"Warden's Band",type:'acc',desc:'A silver ring etched with ward-script. Attack speed +25%. Worn on the wrist.',
icon:'ring',acq:'Hidden chest — The Rusted Archives'},
vitstem:{name:'Vital Stem',type:'con',desc:'A living shoot from the old gardens. Restores 2 full containers (4 HP).',
icon:'stem',heal:4,acq:'Hidden cache or Shop (50 gold)'},
lantern:{name:'Phantom Lantern',type:'acc',desc:'A cold blue flame. Reveals hidden cracks and passages nearby.',
icon:'lantern',acq:'Light-reflection puzzle — The Sunken Vault'},
cloak:{name:'Shadow Cloak',type:'acc',desc:'Woven dusk-cloth. Halves damage from environmental traps.',
icon:'cloak',acq:'Hidden treasure / Spire Guardian — The Twin Spire'},
seal:{name:'Binding Seal',type:'con',desc:'A sigil of stillness. Freezes one enemy for 3 seconds.',
icon:'seal',freeze:180,acq:'Broken jar / Shop (75 gold)'},
shard:{name:'Aether Shard',type:'tool',desc:'A crystalline launcher. Press K to fire a glowing bolt at distant switches and barriers.',
icon:'shard',acq:'The Sunken Vault — hidden behind cracked wall'},
heart4:{name:'Heart Container',type:'perm',desc:'A vessel of vigor. +1 heart container, permanently.',icon:'heart',acq:'The Collapsed Gallery'},
heart5:{name:'Heart Container',type:'perm',desc:'A vessel of vigor. +1 heart container, permanently.',icon:'heart',acq:'The Twin Spire / Shop'}};
const SHOPDEF=[
{id:'vitstem',price:50,max:3},
{id:'seal',price:75,max:2},
{id:'heart5',price:300,max:1},
{id:'ammo',price:10,max:99,req:'shard'}];
const ZONES=[
{id:1,name:'The Ashen Threshold',label:'I · THE ASHEN THRESHOLD',sky:['#1a1410','#241a12'],fog:'#2a1c12',amb:'#0d0906',dustC:'rgba(200,160,100,.25)'},
{id:2,name:'The Rusted Archives',label:'II · THE RUSTED ARCHIVES',sky:['#101820','#182430'],fog:'#16222e',amb:'#0a1016',dustC:'rgba(140,170,200,.18)'},
{id:3,name:'The Collapsed Gallery',label:'III · THE COLLAPSED GALLERY',sky:['#141018','#221a26'],fog:'#1e1622',amb:'#0b0810',dustC:'rgba(180,150,190,.16)'},
{id:4,name:'The Sunken Vault',label:'IV · THE SUNKEN VAULT',sky:['#0a1418','#10222a'],fog:'#0e2028',amb:'#050d10',dustC:'rgba(120,200,220,.15)'},
{id:5,name:'The Twin Spire',label:'V · THE TWIN SPIRE',sky:['#181008','#2a1c0e'],fog:'#241808',amb:'#0e0904',dustC:'rgba(220,180,110,.2)'},
{id:6,name:'The Core Chamber',label:'VI · THE CORE CHAMBER',sky:['#120c06','#1e140a'],fog:'#1a1208',amb:'#0a0603',dustC:'rgba(230,200,140,.22)'}];
/* ============================== world ============================== */
const world={walls:[],breakables:[],plates:[],doors:[],enemies:[],pickups:[],chests:[],npcs:[],traps:[],
switches:[],crystals:[],deco:[],lights:[],zone:1,w:1920,h:1080,exit:null,entry:null,boss:null,
saveStone:null,altar:null,shopAltar:null,beacon:null,bridge:null,lightPuzzle:null};
function wall(x,y,w,h,opts){world.walls.push(Object.assign({x,y,w,h},opts||{}));}
function breakable(x,y,w,h,loot,id){world.breakables.push({x,y,w,h,loot:loot||null,hp:6,id:id||('bw'+world.breakables.length),
cracks:0,shakes:0});}
function plate(x,y,w,h,id,group){world.plates.push({x,y,w,h,id,group:group||id,down:false,t:0});}
function door(x,y,w,h,id,open){world.doors.push({x,y,w,h,id,open:!!open,t:0});}
function chest(x,y,loot,hidden,id){world.chests.push({x,y,loot,hidden:!!hidden,opened:false,id:id||('ch'+world.chests.length)});}
function pickup(x,y,kind,val){world.pickups.push({x,y,kind,val:val||0,t:0,vx:0,vy:0,mag:true});}
function enemy(x,y,type,id){const defs={
sentry:{hp:4,spd:.85,dmg:1,r:13,gold:[5,9],color:'#6a7484',sight:230,atkR:24,cd:50,melee:true,name:'Rusted Sentry'},
stalker:{hp:3,spd:1.35,dmg:1,r:12,gold:[7,12],color:'#4a5a6a',sight:300,atkR:22,cd:38,melee:true,name:'Archive Stalker'},
warden:{hp:6,spd:.7,dmg:1,r:14,gold:[10,15],color:'#7a6a4a',sight:260,atkR:26,cd:44,melee:true,name:'Corroded Warden'},
spire:{hp:8,spd:1.0,dmg:2,r:14,gold:[12,18],color:'#5a4a5a',sight:280,atkR:26,cd:40,melee:true,name:'Spire Sentinel'},
turret:{hp:5,spd:0,dmg:1,r:14,gold:[8,12],color:'#3a4a5a',sight:340,atkR:330,cd:90,melee:false,name:'Gaze Turret'}};
const d=defs[type]||defs.sentry;world.enemies.push(Object.assign({x,y,type,id:id||('en'+world.enemies.length),
hp:d.hp,maxHp:d.hp,cd:0,stun:0,burn:0,hitT:0,dead:false,anim:Math.random()*6,face:-1,aggro:false,
home:{x,y},flash:0},d));}
function trap(x,y,w,h,dmg,type){world.traps.push({x,y,w,h,dmg:dmg||1,type:type||'spikes',t:Math.random()*100});}
function switch_(x,y,id,target){world.switches.push({x,y,id,target,hit:false,t:0});}
function crystal(x,y,id){world.crystals.push({x,y,id,lit:false,t:0});}
function deco(type,x,y,s){world.deco.push({type,x,y,s:s||1,t:Math.random()*100});}
function light(x,y,r,c){world.lights.push({x,y,r,c:c||'rgba(217,162,74,.10)'});}
function saveStone(x,y,id){world.saveStone={x,y,id,active:false,t:0};}
function altar(x,y,level,id){world.altar={x,y,level,id,t:0};}
function shopAltar(x,y){world.shopAltar={x,y,t:0};}
function beacon(x,y){world.beacon={x,y,t:0,lit:false};}
function setExit(x,y,w,h,to,ex,ey){world.exit={x,y,w:w||60,h:h||60,to,ex,ey};}
function buildZone(n){
world.walls=[];world.breakables=[];world.plates=[];world.doors=[];world.enemies=[];world.pickups=[];
world.chests=[];world.traps=[];world.switches=[];world.crystals=[];world.deco=[];world.lights=[];
world.boss=null;world.saveStone=null;world.altar=null;world.shopAltar=null;world.beacon=null;
world.bridge=null;world.lightPuzzle=null;world.exit=null;
world.zone=n;const Z=ZONES[n-1];world.w=1920;world.h=1080;
srand(1000+n*77);
const bw=26;wall(0,0,world.w,bw);wall(0,world.h-bw,world.w,bw);wall(0,0,bw,world.h);wall(world.w-bw,0,bw,world.h);
if(n===1){
wall(300,26,24,300);wall(300,470,24,240);wall(700,26,24,180);wall(700,350,24,340);
wall(1050,26,24,420);wall(1050,590,24,200);
wall(1400,470,24,320);wall(1400,26,24,180);
deco('gear',180,860,1.4);deco('gear',520,180,1);deco('pillar',420,540,1);deco('pillar',950,540,1);
deco('pillar',1250,540,1);deco('telescope',1650,300,1.2);deco('rubble',880,900,1);deco('rubble',1300,700,1);
light(420,540,180);light(950,540,180);light(1250,540,180);
saveStone(200,540,'s1');
enemy(560,300,'sentry');enemy(620,760,'sentry');enemy(900,540,'sentry');
enemy(1180,300,'sentry');enemy(1180,780,'sentry');enemy(1560,540,'warden');
chest(640,140,'gold30');chest(1500,880,'vitstem',false,'ch_vit1');
breakable(1440,140,60,60,'gold15');
plate(1000,860,70,70,'p1a');door(1400,206,24,264,'d1');
setExit(1860,500,40,120,2,120,540);
}else if(n===2){
wall(400,26,24,360);wall(400,530,24,290);wall(800,26,24,260);wall(800,430,24,380);
wall(1150,26,24,500);wall(1150,670,24,140);
wall(1500,430,24,380);wall(1500,26,24,260);
deco('shelf',200,200,1);deco('shelf',200,420,1);deco('shelf',600,900,1);deco('shelf',1000,180,1);
deco('grate',520,540,1);deco('grate',950,300,1);deco('grate',1350,540,1);
deco('gear',300,850,1.2);deco('telescope',1700,850,1);
deco('jar',450,880,1);deco('jar',700,150,1);deco('jar',1250,900,1);
light(520,540,200);light(950,540,200);light(1350,540,200);
saveStone(260,540,'s2');
shopAltar(660,540);
enemy(500,200,'stalker');enemy(560,840,'stalker');enemy(900,300,'sentry');enemy(940,760,'stalker');
enemy(1300,540,'warden');enemy(1350,200,'stalker');enemy(1600,700,'sentry');enemy(1650,350,'warden');
chest(200,940,'wardenband',true,'ch_band');
chest(1750,140,'gold40');
breakable(860,140,60,60,'gold15');breakable(860,900,60,60,'gold20');breakable(1600,140,60,60,'vitstem');
plate(1000,540,70,70,'p2a');door(1150,526,24,144,'d2');
setExit(1860,500,40,120,3,120,540);
}else if(n===3){
wall(340,26,24,300);wall(340,470,24,340);wall(680,26,24,460);wall(680,630,24,180);
wall(1020,26,24,180);wall(1020,350,24,460);
wall(1360,26,24,340);wall(1360,510,24,300);
deco('rubble',200,300,1.2);deco('rubble',500,800,1);deco('rubble',900,200,1);deco('rubble',1200,900,1);
deco('pillar',250,540,1);deco('pillar',850,540,1);deco('pillar',1200,540,1);deco('pillar',1550,540,1);
deco('gear',1600,850,1.5);deco('gear',400,150,1);
light(250,540,170);light(850,540,170);light(1200,540,170);
saveStone(180,540,'s3');
altar(1700,540,1,'a1');
enemy(450,300,'sentry');enemy(500,780,'sentry');enemy(850,540,'warden');enemy(900,250,'stalker');
enemy(900,850,'stalker');enemy(1200,540,'warden');enemy(1250,300,'sentry');enemy(1250,800,'sentry');
enemy(1550,540,'warden');enemy(1600,250,'sentry');enemy(1600,850,'sentry');
chest(120,940,'gold25');
breakable(640,140,60,60,'gold15');breakable(1060,900,60,60,'gold20');
breakable(1080,140,80,80,'heart4',true,'bw_heart4');
plate(860,540,70,70,'p3a');door(1020,206,24,144,'d3');
setExit(1860,500,40,120,4,120,540);
}else if(n===4){
wall(340,26,24,340);wall(340,510,24,300);wall(680,26,24,180);wall(680,350,24,460);
wall(1020,26,24,420);wall(1020,590,24,220);
wall(1360,26,24,260);wall(1360,430,24,380);
deco('grate',250,540,1);deco('grate',550,540,1);deco('grate',850,540,1);deco('grate',1200,540,1);
deco('pillar',250,540,1);deco('pillar',550,540,1);deco('pillar',850,540,1);deco('pillar',1200,540,1);
deco('jar',420,880,1);deco('jar',760,150,1);deco('jar',1500,880,1);
light(250,540,170,'rgba(90,167,214,.12)');light(550,540,170,'rgba(90,167,214,.12)');
light(850,540,170,'rgba(90,167,214,.12)');light(1200,540,170,'rgba(90,167,214,.12)');
saveStone(180,540,'s4');
enemy(450,300,'stalker');enemy(500,780,'sentry');enemy(850,540,'warden');enemy(900,250,'stalker');
enemy(900,850,'stalker');enemy(1200,540,'warden');enemy(1250,300,'sentry');enemy(1250,800,'sentry');
enemy(1550,540,'spire');enemy(1600,250,'turret');enemy(1600,850,'turret');
chest(120,940,'gold30');
breakable(640,140,60,60,'gold15');breakable(1060,900,60,60,'gold20');
breakable(1500,140,80,80,'shard',true,'bw_shard');
plate(860,540,70,70,'p4a');door(1020,446,24,144,'d4');
crystal(1800,120,'c1');crystal(1800,960,'c2');
world.lightPuzzle={crystals:['c1','c2'],solved:false,reward:'lantern',rewardPos:{x:1700,y:540}};
setExit(1860,500,40,120,5,120,540);
}else if(n===5){
wall(340,26,24,300);wall(340,470,24,340);wall(680,26,24,460);wall(680,630,24,180);
wall(1020,26,24,180);wall(1020,350,24,460);
wall(1360,26,24,340);wall(1360,510,24,300);
deco('pillar',250,540,1);deco('pillar',550,540,1);deco('pillar',850,540,1);deco('pillar',1200,540,1);
deco('gear',300,850,1.4);deco('gear',1600,200,1.2);deco('telescope',1750,900,1);
deco('rubble',450,300,1);deco('rubble',950,900,1);
light(250,540,170);light(550,540,170);light(850,540,170);light(1200,540,170);
saveStone(180,540,'s5');
altar(1750,540,2,'a2');
enemy(450,300,'spire');enemy(500,780,'spire');enemy(850,540,'spire');enemy(900,250,'spire');
enemy(900,850,'spire');enemy(1200,540,'spire');enemy(1250,300,'spire');enemy(1250,800,'spire');
enemy(1550,540,'spire');enemy(1600,250,'turret');enemy(1600,850,'turret');
chest(120,940,'gold60');
chest(640,140,'cloak',true,'ch_cloak');
chest(1060,900,'seal',false,'ch_seal');
breakable(640,900,60,60,'gold20');breakable(1060,140,60,60,'gold25');
breakable(1400,140,80,80,'heart5',true,'bw_heart5');
plate(860,540,70,70,'p5a');door(1020,206,24,144,'d5');
crystal(1800,120,'c5');
setExit(1860,500,40,120,6,120,540);
}else if(n===6){
wall(340,26,24,340);wall(340,510,24,300);wall(680,26,24,180);wall(680,350,24,460);
wall(1020,26,24,420);wall(1020,590,24,220);
wall(1360,26,24,260);wall(1360,430,24,380);
deco('gear',250,540,1.6);deco('gear',550,540,1.4);deco('gear',850,540,1.4);deco('gear',1200,540,1.4);
deco('pillar',250,540,1);deco('pillar',550,540,1);
deco('pillar',850,540,1);deco('pillar',1200,540,1);
light(250,540,170);light(550,540,170);light(850,540,170);light(1200,540,170);
saveStone(180,540,'s6');
enemy(450,300,'spire');enemy(500,780,'spire');enemy(850,540,'spire');
enemy(900,250,'spire');enemy(900,850,'spire');enemy(1200,540,'spire');
enemy(1250,300,'spire');enemy(1250,800,'spire');
beacon(1750,540);
plate(860,540,70,70,'p6a');door(1020,446,24,144,'d6');
}
// Only three Sanctuary Stones exist: threshold, archives, and spire.
if(![1,2,5].includes(n))world.saveStone=null;
if(n===2){world.saveStone.x=960;world.saveStone.y=590;}
world.entry=n>1?{x:65,y:540,to:n-1}:null;
if(n===3){world.plates=[];plate(760,640,70,70,'p3a');plate(910,640,70,70,'p3b');}
if(n===4){world.crystals[0].x=1700;world.crystals[0].y=360;world.crystals[1].x=1700;world.crystals[1].y=740;}
if(n===5){world.crystals[0].x=1520;world.crystals[0].y=180;
const b=world.breakables.find(b=>b.loot==='heart5');b.locked=true;
trap(1110,490,110,36,2);trap(1450,600,130,36,2);
deco('jar',1090,850,1);}
if(n===6){crystal(1570,380,'core1');crystal(1570,700,'core2');
world.boss={x:1660,y:540,r:30,hp:42,maxHp:42,cd:100,stun:0,hitT:0,type:'boss',dead:false,phase:0};
world.enemies.push(world.boss);trap(1420,470,70,32,2);trap(1420,620,70,32,2);}
for(const e of world.enemies){if(e.type==='spire')e.gold=[12,15];}
for(const b of world.breakables)b.id='wall_'+b.x+'_'+b.y;
}
buildZone(1);
/* ============================== simulation ============================== */
const cam={x:0,y:270};const zoneStates={};let currentPrompt=null,selectedItem='vitstem',saveData=null;
const objectives=[
'Silence the constructs · Step on the bronze plate · Reach the eastern arch',
'Find the hidden silver band · Rest at the central Sanctuary · Visit the merchant',
'Press both plates within 4 seconds · Find the cracked vessel · Ignite the altar',
'Find the hairline wall in the northeast · Recover the Shard · Light both crystals',
'Strike the northern crystal for a heart · Survive the gauntlet · Claim the Solar Falchion',
'Light both ward crystals · Break the Core Custodian · Reignite the Beacon'];
function rectNear(o,p,r){return Math.hypot(p.x-clamp(p.x,o.x,o.x+o.w),p.y-clamp(p.y,o.y,o.y+o.h))<r;}
function solids(){return world.walls.concat(world.breakables.filter(b=>b.hp>0),world.doors.filter(d=>!d.open));}
function moveBody(o,dx,dy){o.x+=dx;for(const b of solids())if(rectNear(b,o,o.r)){if(dx>0)o.x=b.x-o.r;else if(dx<0)o.x=b.x+b.w+o.r;}
o.y+=dy;for(const b of solids())if(rectNear(b,o,o.r)){if(dy>0)o.y=b.y-o.r;else if(dy<0)o.y=b.y+b.h+o.r;}
o.x=clamp(o.x,38,world.w-38);o.y=clamp(o.y,38,world.h-38);}
function burst(x,y,c,n=12,force=2){for(let i=0;i<n;i++){let a=rnd(0,TAU),s=rnd(.2,force);G.parts.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,life:rnd(20,45),max:45,c,r:rnd(1,3)});}}
function number(x,y,text,c='#f3c36b'){G.dmgNums.push({x,y,text,c,t:60});}
function toast(text,type=''){const d=document.createElement('div');d.className='toast '+type;d.textContent=text;el('toasts').append(d);setTimeout(()=>{d.classList.add('out');setTimeout(()=>d.remove(),400);},3400);}
function shimmer(){G.shimmer=120;}
function own(id){return P.inv.find(s=>s.id===id);}
function gain(id){if(id.startsWith('gold')){let v=+id.slice(4);G.gold+=v;G.stats.goldEarned+=v;number(P.x,P.y-35,'+'+v+' sigils');AU.coin();burst(P.x,P.y,'#eec16a',14);return;}
if(id==='heart4'||id==='heart5'){
if(G.flags[id])return;G.flags[id]=true;P.maxHp+=2;P.containers++;P.hp=P.maxHp;AU.heart();shimmer();burst(P.x,P.y,'#ffe49a',40,3);toast('A vessel rekindled · +1 heart container','gain');updateHUD();return;}
if(!ITEMS[id])return;
let slot=own(id);if(slot){if(ITEMS[id].type==='con')slot.count++;else return;}else P.inv.push({id,count:1});
if(id==='shard'){P.ammo+=24;G.flags.shard=true;dialog('Aether Shard',['Cold light gathers in your off-hand. Face a crystal and press <span class="key">K</span> to send a bolt through the dark.','The two crystals in this vault bend light toward a forgotten lantern. Strike them both.']);}
G.stats.items++;AU[ITEMS[id].type==='acc'||id==='shard'?'rare':'pickup']();burst(P.x,P.y,'#dceaf3',24);toast(ITEMS[id].name+' discovered'+(ITEMS[id].type==='acc'?' · Equip in satchel [I]':''),'blue');updateHUD();}
function loot(x,y,id){world.pickups.push({x,y,kind:id,t:0,vx:0,vy:0,mag:true});}
function takeDamage(d,trapHit=false){if(P.iframes>0||P.dead)return;if(trapHit&&P.accessories.cloak)d=Math.max(.5,d/2);
P.hp=Math.max(0,P.hp-d);P.iframes=65;G.shake=6;G.shakeT=14;AU.hurt();burst(P.x,P.y,'#cf493c',25,4);number(P.x,P.y-25,'−'+d,'#ff7564');updateHUD();
if(P.hp<=0){P.dead=true;G.paused=true;el('deathscr').classList.remove('hidden');}}
function strikeEnemy(e,d,range=false){if(e.dead)return;
if(e.type==='boss'&&!world.crystals.every(c=>c.lit)){burst(e.x,e.y,'#70bbef',10);number(e.x,e.y-35,'WARDED','#7fc5e4');AU.chimeImpact();return;}
e.hp-=d;e.hitT=12;e.stun=Math.max(e.stun||0,P.weapon===1?18:10);if(P.weapon===1&&!range)e.burn=40;
number(e.x,e.y-25,'−'+d,range?'#9ddbef':'#ffe0a1');burst(e.x,e.y,range?'#85d9f4':WEAPONS[P.weapon].color,12,3);
if(range)AU.chimeImpact();else AU[P.weapon===2?'solarHit':P.weapon===1?'burnHit':'hit']();
if(e.hp<=0){e.dead=true;G.stats.kills++;burst(e.x,e.y,'#b79257',26,4);if(e.type==='boss'){G.flags.boss=true;AU.bossRoar();G.shakeT=50;G.shake=7;toast('The Custodian falls. The Beacon awaits.');loot(e.x,e.y,'gold15');}
else{loot(e.x,e.y,'gold'+irnd(e.gold[0],e.gold[1]));if(G.zone>=4)world.pickups.push({x:e.x+12,y:e.y,kind:'charge',t:0});}}
else if(e.type!=='boss'){const a=Math.atan2(e.y-P.y,e.x-P.x);moveBody(e,Math.cos(a)*8,Math.sin(a)*8);}}
function breakHit(b,d){if(b.hp<=0)return;if(b.locked&&!world.crystals.every(c=>c.lit)){number(b.x+30,b.y,'CRYSTAL LOCK','#82bedb');AU.deny();return;}
b.hp-=d;b.shakes=12;burst(b.x+b.w/2,b.y+b.h/2,'#b6a17c',8,2);AU.hit();if(b.hp<=0){AU.crumble();burst(b.x+b.w/2,b.y+b.h/2,'#84755e',30,4);if(b.loot)loot(b.x+b.w/2,b.y+b.h/2,b.loot);}}
function doAttack(){if(P.attackCd>0||P.dead)return;let w=WEAPONS[P.weapon];P.attackCd=w.cd*(P.accessories.wardenband?.75:1);P.attackT=w.cd;P.swingDir*=-1;P.swingAge=0;P.didHit=false;P.attackAngle=Math.atan2(G.aim.y,G.aim.x);AU[P.weapon===2?'windup':'swing']();}
function doFire(){if(!G.flags.shard){toast('No ranged mechanism. Search the Sunken Vault.','blue');return;}if(P.fireCd>0)return;if(P.ammo<=0){AU.deny();toast('The Shard is empty. Find charges or return to the merchant.');return;}
P.ammo--;P.fireCd=24;AU.twang();G.bullets.push({x:P.x+G.aim.x*17,y:P.y+G.aim.y*17,vx:G.aim.x*8,vy:G.aim.y*8,life:95,friendly:true});updateHUD();}
function dialog(name,lines,cb){G.dialogQ=lines;G.dialogI=0;G.dialogCb=cb;G.paused=true;el('dlgname').textContent=name;el('dlgtext').innerHTML=lines[0];el('dialog').classList.remove('hidden');}
function nextDialog(){if(!G.dialogQ.length)return;G.dialogI++;AU.tone(660,.06,'sine',.035);if(G.dialogI<G.dialogQ.length)el('dlgtext').innerHTML=G.dialogQ[G.dialogI];else{G.dialogQ=[];el('dialog').classList.add('hidden');G.paused=false;if(G.dialogCb)G.dialogCb();}}
function prompt(text,fn){currentPrompt={text,fn};}
function copy(o){return JSON.parse(JSON.stringify(o));}
function storeZone(){zoneStates[G.zone]=copy(world);}
function saveGame(){P.hp=P.maxHp;world.saveStone.active=true;G.flags['save'+G.zone]=true;
// Distant constructs re-form when a Warden rests; caches and discoveries stay claimed.
for(const [z,state] of Object.entries(zoneStates))if(+z!==G.zone)for(const e of state.enemies){if(e.dead&&e.type!=='boss'){e.dead=false;e.hp=e.maxHp;e.stun=0;e.hitT=0;if(e.home){e.x=e.home.x;e.y=e.home.y;}}}
storeZone();
saveData={player:copy(P),zone:G.zone,zones:copy(zoneStates),gold:G.gold,flags:copy(G.flags),stats:copy(G.stats)};
G.checkpoint={zone:G.zone,name:ZONES[G.zone-1].name};try{localStorage.setItem('warden-vigil-v2',JSON.stringify(saveData));}catch(e){}
AU.save();shimmer();burst(P.x,P.y,'#f8d78d',30);toast('Sanctuary kindled · Vigor restored · Vigil saved');updateHUD();}
function restore(){if(!saveData){resetGame();return;}Object.assign(P,copy(saveData.player));G.zone=saveData.zone;G.gold=saveData.gold;G.flags=copy(saveData.flags);G.stats=copy(saveData.stats);
Object.keys(zoneStates).forEach(k=>delete zoneStates[k]);Object.assign(zoneStates,copy(saveData.zones));Object.assign(world,copy(zoneStates[G.zone]));
world.boss=world.enemies.find(e=>e.type==='boss')||null;P.dead=false;P.hp=P.maxHp;P.iframes=100;G.paused=false;G.bullets=[];G.parts=[];G.dialogQ=[];G.shopOpen=false;G.invOpen=false;
el('deathscr').classList.add('hidden');el('title').classList.add('hidden');G.running=true;AU.init();AU.startMusic();cam.x=clamp(P.x-W/2,0,world.w-W);cam.y=clamp(P.y-H/2,0,world.h-H);zoneCard();updateHUD();}
function enterZone(n,x,y){if(G.transition)return;G.transition=70;G.pendingZone=n;G.nextX=x;G.nextY=y;AU.door();el('fader').style.transition='opacity .45s';el('fader').style.opacity=1;}
function finishTransition(){storeZone();G.zone=G.pendingZone;
if(zoneStates[G.zone])Object.assign(world,copy(zoneStates[G.zone]));else buildZone(G.zone);
world.boss=world.enemies.find(e=>e.type==='boss')||null;P.x=G.nextX;P.y=G.nextY;P.iframes=100;G.bullets=[];G.parts=[];G.zoneT=0;cam.x=clamp(P.x-W/2,0,world.w-W);cam.y=clamp(P.y-H/2,0,world.h-H);el('fader').style.opacity=0;zoneCard();updateHUD();
if(!G.flags['visit'+G.zone]){G.flags['visit'+G.zone]=true;setTimeout(()=>toast(G.zone===4?'HALFWAY · A buried mechanism stirs beneath the water.':objectives[G.zone-1]),800);}}
function gateReason(){if(G.zone===1&&!G.flags.plate1)return 'A bronze plate still sleeps to the southwest.';
if(G.zone===2&&world.enemies.filter(e=>!e.dead).length>2)return 'The archive is still held by constructs.';
if(G.zone===3&&(!G.flags.galleryPuzzle||P.weapon<1))return 'The twin plates and the weapon altar must be restored.';
if(G.zone===4&&(!G.flags.shard||!G.flags.vaultPuzzle))return 'The hidden Shard and both light crystals are required.';
if(G.zone===5&&(P.weapon<2||world.enemies.filter(e=>!e.dead).length>0))return 'Restore the solar altar and silence the spire gauntlet.';
return '';}
function checkInteractions(){currentPrompt=null;
if(G.zone===4&&G.flags.shard&&P.ammo<4){const socket=world.breakables.find(b=>b.loot==='shard');if(socket&&socket.hp<=0&&rectNear(socket,P,65))prompt('Draw four charges from the broken Aether socket',()=>{P.ammo=4;AU.rare();updateHUD();toast('The socket lends four charges. Aim carefully.','blue');});}
for(const c of world.chests)if(!c.opened&&dist(P,c)<55){prompt('Open '+(c.hidden?'concealed treasure':'bronze cache'),()=>{c.opened=true;gain(c.loot);AU.rare();});}
if(world.saveStone&&dist(P,world.saveStone)<58){let ready=G.zone!==2||world.enemies.filter(e=>!e.dead&&e.x<1000).length===0;
prompt(ready?'Rest & save at Sanctuary':'Sanctuary sealed · clear nearby constructs',()=>ready?saveGame():toast('Silence the constructs west of the Sanctuary.'));}
if(world.shopAltar&&dist(P,world.shopAltar)<62){let ready=world.enemies.filter(e=>!e.dead&&e.x<800).length===0;
prompt(ready?'Trade with the Sigil-Merchant':'Merchant sealed · clear the western hall',()=>ready?toggleShop(true):toast('Defeat the two western archive stalkers first.'));}
if(world.altar&&P.weapon<world.altar.level&&dist(P,world.altar)<65){let ready=world.enemies.every(e=>e.dead||dist(e,world.altar)>230)&&(G.zone!==3||G.flags.galleryPuzzle);
prompt(ready?'Restore '+WEAPONS[world.altar.level].name:'Altar bound · silence its guardians',()=>{if(!ready){AU.deny();return;}P.weapon=world.altar.level;AU.upgrade();shimmer();burst(P.x,P.y,WEAPONS[P.weapon].color,50,3);toast(WEAPONS[P.weapon].name+' awakened · '+(P.weapon===1?'quicker ember strikes':'wider, heavier solar sweeps'));});}
if(world.entry&&dist(P,world.entry)<58)prompt('Return to '+ZONES[G.zone-2].name,()=>enterZone(G.zone-1,1820,540));
if(world.exit&&rectNear(world.exit,P,65)){let reason=gateReason();prompt(reason?'Arch sealed · inspect [E]':'Advance to '+ZONES[G.zone].name,()=>{if(reason)toast(reason);else{if(G.zone===4)G.flags.vaultComplete=true;enterZone(G.zone+1,120,540);}});}
if(world.beacon&&dist(P,world.beacon)<75)prompt(G.flags.boss?'Reignite the Core Beacon':'Beacon held by the Core Custodian',()=>{if(!G.flags.boss){toast('Light the two blue wards, then break the Custodian.');return;}world.beacon.lit=true;G.flags.won=true;AU.beacon();shimmer();G.paused=true;burst(P.x,P.y,'#fff1bd',120,7);setTimeout(()=>{el('credits').textContent=G.stats.kills+' constructs released · '+G.stats.goldEarned+' sigils recovered · '+P.containers+' vessels rekindled';el('endscr').classList.remove('hidden');},1700);});
el('interact').classList.toggle('hidden',!currentPrompt||G.paused);if(currentPrompt)el('intext').textContent=currentPrompt.text;
}
function useSelected(){if(!G.running||G.shopOpen||G.dialogQ.length||P.dead)return;let s=own(selectedItem);if(!s||s.count<1){toast('Select a carried item in your satchel [I].');return;}
let def=ITEMS[s.id];if(def.type==='acc'){P.accessories[s.id]=!P.accessories[s.id];AU.seal();toast(def.name+(P.accessories[s.id]?' equipped':' unequipped'),'blue');}
else if(s.id==='vitstem'){if(P.hp>=P.maxHp){toast('Your vessels are already full.');return;}s.count--;P.hp=Math.min(P.maxHp,P.hp+4);AU.heart();burst(P.x,P.y,'#9ae5a0',25);toast('Vital Stem · 4 HP restored','gain');}
else if(s.id==='seal'){let e=world.enemies.filter(e=>!e.dead&&dist(e,P)<300).sort((a,b)=>dist(a,P)-dist(b,P))[0];if(!e){toast('No construct within binding range.');return;}s.count--;e.stun=180;AU.freeze();burst(e.x,e.y,'#8adbff',25);toast('Binding Seal · construct frozen for 3 seconds','blue');}
updateHUD();if(G.invOpen)renderInventory();}
function update(dt){G.time+=dt;G.frame++;if(G.shimmer>0)G.shimmer-=dt;el('shimmer').style.opacity=Math.max(0,(G.shimmer||0)/120)*.7;
if(G.shakeT>0)G.shakeT-=dt;
for(const p of G.parts){p.x+=p.vx*dt;p.y+=p.vy*dt;p.vx*=.97;p.vy*=.97;p.life-=dt;}G.parts=G.parts.filter(p=>p.life>0);
for(const p of G.dmgNums){p.y-=.42*dt;p.t-=dt;}G.dmgNums=G.dmgNums.filter(p=>p.t>0);
if(interactPressed&&G.dialogQ.length){interactPressed=false;nextDialog();return;}
if(G.transition){G.transition-=dt;if(G.transition<=35&&G.pendingZone){finishTransition();G.pendingZone=0;}if(G.transition<=0)G.transition=0;interactPressed=false;return;}
if(!G.running||G.paused){G.wantAttack=false;G.wantFire=false;interactPressed=false;return;}
G.zoneT+=dt;P.iframes=Math.max(0,P.iframes-dt);P.attackCd=Math.max(0,P.attackCd-dt);P.fireCd=Math.max(0,P.fireCd-dt);P.attackT=Math.max(0,P.attackT-dt);
let dx=(Keys.d||Keys.arrowright?1:0)-(Keys.a||Keys.arrowleft?1:0),dy=(Keys.s||Keys.arrowdown?1:0)-(Keys.w||Keys.arrowup?1:0);
P.moving=!!(dx||dy);if(P.moving){let l=Math.hypot(dx,dy);dx/=l;dy/=l;G.aim={x:dx,y:dy};if(dx)P.face=dx>0?1:-1;
let slow=P.attackT>0&&P.weapon===2?.6:1;moveBody(P,dx*P.speed*slow*dt,dy*P.speed*slow*dt);P.anim+=.18*dt;P.stepT-=dt;
if(P.stepT<=0){P.stepT=15;AU.step();if(G.zone===2)AU.tone(320,.08,'triangle',.04);if(G.zone===4)AU.noise(.18,.07,1400);}
if(P.weapon>0&&G.frame%5===0)burst(P.x,P.y+8,WEAPONS[P.weapon].color,1,.5);}
if(G.wantAttack||Keys.j||Keys[' '])doAttack();if(G.wantFire||Keys.k)doFire();G.wantAttack=false;G.wantFire=false;
if(P.attackT>0){P.swingAge=(P.swingAge||0)+dt;let w=WEAPONS[P.weapon];if(!P.didHit&&P.swingAge>=w.windup){P.didHit=true;
for(const e of world.enemies){if(e.dead||dist(e,P)>w.range+e.r)continue;let a=Math.atan2(e.y-P.y,e.x-P.x)-P.attackAngle;a=Math.atan2(Math.sin(a),Math.cos(a));if(Math.abs(a)<w.arc)strikeEnemy(e,w.dmg);}
for(const b of world.breakables)if(rectNear(b,P,w.range))breakHit(b,w.dmg);
for(const d of world.deco)if(d.type==='jar'&&!d.broken&&dist(d,P)<w.range){d.broken=true;AU.crumble();loot(d.x,d.y,G.zone===5?'seal':'gold10');burst(d.x,d.y,'#927357',12);}
if(P.weapon===2){G.shake=2;G.shakeT=8;}}}
for(const b of world.breakables)b.shakes=Math.max(0,b.shakes-dt);
for(const p of world.plates){let down=rectNear(p,P,P.r)||world.enemies.some(e=>!e.dead&&rectNear(p,e,e.r));if(down&&!p.down){AU.plate();burst(p.x+p.w/2,p.y+p.h/2,'#d7a955',8);p.t=G.time+240;}
p.down=down;if(down)p.t=G.time+240;}
if(G.zone===3){if(world.plates.every(p=>p.t>G.time)&&!G.flags.galleryPuzzle){G.flags.galleryPuzzle=true;AU.door();gain('gold30');toast('Both plates answer · the gallery bridge is secured.');}}
for(const d of world.doors){let open=G.zone===3?G.flags.galleryPuzzle:world.plates.some(p=>p.down||p.t>G.time);if(open&&!d.open){AU.door();if(G.zone===1)G.flags.plate1=true;}d.open=!!open||G.zone===1&&G.flags.plate1;}
for(const e of world.enemies){if(e.dead)continue;e.hitT=Math.max(0,(e.hitT||0)-dt);e.stun=Math.max(0,(e.stun||0)-dt);e.burn=Math.max(0,(e.burn||0)-dt);e.anim=(e.anim||0)+.04*dt;if(e.stun>0)continue;
let l=dist(e,P);if(e.type==='boss'){if(l>430)continue;e.cd-=dt;if(e.cd<=0){e.cd=e.hp<20?85:125;AU.bossRoar();G.shake=3;G.shakeT=12;
for(let i=0;i<10;i++){let a=i*TAU/10+G.time*.01;G.bullets.push({x:e.x,y:e.y,vx:Math.cos(a)*2.5,vy:Math.sin(a)*2.5,life:160,friendly:false,dmg:2});}}
if(l>50){e.x+=(P.x-e.x)/l*.45*dt;e.y+=(P.y-e.y)/l*.45*dt;}if(l<e.r+P.r+4)takeDamage(2);continue;}
e.cd=Math.max(0,e.cd-dt);e.aggro=l<e.sight;if(!e.aggro)continue;e.face=P.x>e.x?1:-1;
if(e.melee){if(l>e.atkR+5)moveBody(e,(P.x-e.x)/l*e.spd*dt,(P.y-e.y)/l*e.spd*dt);else if(e.cd<=0){e.cd=e.type==='spire'?62:70;e.wind=23;}}
else if(e.cd<=0){e.cd=105;AU.tone(180,.2,'sawtooth',.05,0,420);G.bullets.push({x:e.x,y:e.y,vx:(P.x-e.x)/l*3,vy:(P.y-e.y)/l*3,life:140,friendly:false,dmg:1});}
if(e.wind>0){e.wind-=dt;if(e.wind<=0&&dist(e,P)<e.atkR+12){takeDamage(e.dmg);burst(e.x,e.y,'#ce6346',6);}}}
for(const b of G.bullets){b.life-=dt;b.x+=b.vx*dt;b.y+=b.vy*dt;if(G.frame%2===0)burst(b.x,b.y,b.friendly?'#86d9f4':'#d76c46',1,.2);
if(b.friendly){for(const c of world.crystals)if(!c.lit&&dist(b,c)<22){c.lit=true;b.life=0;AU.switchHit();burst(c.x,c.y,'#b6eaff',30,3);toast('A crystal answers the Shard.','blue');}
for(const e of world.enemies)if(!e.dead&&dist(b,e)<e.r+5){strikeEnemy(e,2,true);b.life=0;break;}
for(const w of world.breakables)if(w.hp>0&&rectNear(w,b,4)){breakHit(w,2);b.life=0;}}
else if(dist(b,P)<P.r+5){takeDamage(b.dmg);b.life=0;}
if(world.walls.some(w=>rectNear(w,b,3))||world.doors.some(w=>!w.open&&rectNear(w,b,3)))b.life=0;}
G.bullets=G.bullets.filter(b=>b.life>0);
if(G.zone===4&&!G.flags.vaultPuzzle&&world.crystals.every(c=>c.lit)){G.flags.vaultPuzzle=true;world.lightPuzzle.solved=true;loot(1700,540,'lantern');gain('gold40');AU.rare();toast('The light path is whole · the Phantom Lantern manifests.');}
if(G.zone===5&&!G.flags.spireCrystal&&world.crystals.every(c=>c.lit)){G.flags.spireCrystal=true;let b=world.breakables.find(b=>b.loot==='heart5');b.locked=false;b.hp=0;loot(b.x+40,b.y+40,'heart5');AU.crumble();toast('The distant mechanism opens a vessel chamber.');}
for(const t of world.traps){t.t+=dt;let active=t.t%180<95;if(active&&rectNear(t,P,P.r))takeDamage(t.dmg,true);}
for(const p of world.pickups){p.t+=dt;let l=dist(p,P);if(l<95&&l>1){p.x+=(P.x-p.x)/l*3.5*dt;p.y+=(P.y-p.y)/l*3.5*dt;}if(l<20){p.gone=true;if(p.kind==='charge'){P.ammo+=2;AU.chimeImpact();number(P.x,P.y-30,'+2 charges','#8edaff');}else gain(p.kind);updateHUD();}}
world.pickups=world.pickups.filter(p=>!p.gone);
let nearShop=world.shopAltar&&dist(P,world.shopAltar)<180;if(nearShop)AU.startDrone();else AU.stopDrone();
checkInteractions();if(interactPressed&¤tPrompt)currentPrompt.fn();interactPressed=false;
cam.x=lerp(cam.x,clamp(P.x-W/2,0,world.w-W),.12);cam.y=lerp(cam.y,clamp(P.y-H/2,0,world.h-H),.12);
el('vignette').style.opacity=P.iframes>45?.7:P.hp<=2?.3:0;
}
/* ============================== hand-drawn observatory ============================== */
function circle(x,y,r,c,stroke){ctx.beginPath();ctx.arc(x,y,r,0,TAU);if(c){ctx.fillStyle=c;ctx.fill();}if(stroke){ctx.strokeStyle=stroke;ctx.stroke();}}
function line(x,y,xx,yy,c,width=1){ctx.beginPath();ctx.moveTo(x,y);ctx.lineTo(xx,yy);ctx.strokeStyle=c;ctx.lineWidth=width;ctx.stroke();ctx.lineWidth=1;}
function poly(points,c,stroke){ctx.beginPath();points.forEach((p,i)=>i?ctx.lineTo(...p):ctx.moveTo(...p));ctx.closePath();if(c){ctx.fillStyle=c;ctx.fill();}if(stroke){ctx.strokeStyle=stroke;ctx.stroke();}}
function glow(x,y,r,c){let g=ctx.createRadialGradient(x,y,0,x,y,r);g.addColorStop(0,c);g.addColorStop(1,'transparent');circle(x,y,r,g);}
function text(str,x,y,color='#c6b994',size=11,align='center'){ctx.font=size+'px Georgia';ctx.textAlign=align;ctx.fillStyle=color;ctx.fillText(str,x,y);}
function diamond(x,y,r,c,stroke){poly([[x,y-r],[x+r*.62,y],[x,y+r],[x-r*.62,y]],c,stroke);}
function gear(x,y,r,angle,col='#655131'){ctx.save();ctx.translate(x,y);ctx.rotate(angle);ctx.strokeStyle=col;ctx.lineWidth=5;
ctx.beginPath();for(let i=0;i<96;i++){let a=i/96*TAU,rr=(i%4<2?r:r*.89);let px=Math.cos(a)*rr,py=Math.sin(a)*rr;i?ctx.lineTo(px,py):ctx.moveTo(px,py);}ctx.closePath();ctx.stroke();
circle(0,0,r*.75,null,col);circle(0,0,r*.24,'#111820',col);for(let i=0;i<8;i++){let a=i*TAU/8;line(Math.cos(a)*r*.27,Math.sin(a)*r*.27,Math.cos(a)*r*.73,Math.sin(a)*r*.73,col,3);}circle(0,0,5,'#9b7b43');ctx.restore();}
function icon(c,id,x,y,s=1){c.save();c.translate(x,y);c.scale(s,s);c.lineWidth=1.4;
const diamondLocal=(r,color)=>{c.beginPath();c.moveTo(0,-r);c.lineTo(r*.55,0);c.lineTo(0,r);c.lineTo(-r*.55,0);c.closePath();c.fillStyle=color;c.fill();c.strokeStyle='#d5eff5';c.stroke();};
if(id==='gold'||id.startsWith('gold')){c.fillStyle='#9b642b';c.beginPath();c.ellipse(0,1,7,9,0,0,TAU);c.fill();c.strokeStyle='#f2ce79';c.stroke();c.strokeRect(-2,-5,4,10);}
else if(id==='heart'||id.startsWith('heart')){c.beginPath();c.moveTo(0,9);c.bezierCurveTo(-23,-4,-6,-18,0,-7);c.bezierCurveTo(6,-18,23,-4,0,9);c.fillStyle='#df9666';c.fill();c.strokeStyle='#ffe2a3';c.stroke();}
else if(id==='wardenband'){c.strokeStyle='#b7e4ec';c.lineWidth=3;c.beginPath();c.ellipse(0,1,9,7,-.5,0,TAU);c.stroke();c.fillStyle='#ecfbff';c.fillRect(4,-9,5,5);}
else if(id==='vitstem'){c.strokeStyle='#9fd483';c.beginPath();c.moveTo(-4,11);c.quadraticCurveTo(5,0,1,-12);c.stroke();c.fillStyle='#80bc7c';for(let i=0;i<3;i++){c.beginPath();c.ellipse(i%2?6:-3,4-i*7,6,3,i%2?-.6:.7,0,TAU);c.fill();}}
else if(id==='lantern'){c.strokeStyle='#adbbc1';c.strokeRect(-7,-7,14,17);c.beginPath();c.arc(0,-8,5,Math.PI,0);c.stroke();c.fillStyle='#62b7d3';c.fillRect(-4,-3,8,9);c.fillStyle='#e2f9ff';c.fillRect(-1,-1,2,5);}
else if(id==='cloak'){c.fillStyle='#354353';c.beginPath();c.moveTo(-5,-12);c.lineTo(5,-12);c.lineTo(13,10);c.quadraticCurveTo(0,5,-13,10);c.closePath();c.fill();c.strokeStyle='#7b849f';c.stroke();c.fillStyle='#ceb778';c.fillRect(-3,-10,6,3);}
else if(id==='seal'){c.strokeStyle='#7bd0e4';c.strokeRect(-9,-9,18,18);c.rotate(Math.PI/4);c.strokeRect(-6,-6,12,12);c.fillStyle='#c5eafa';c.fillRect(-2,-2,4,4);}
else if(id==='shard'||id==='charge'||id==='ammo')diamondLocal(12,'#75cbdc');
c.restore();}
function drawFloor(){let z=ZONES[G.zone-1];ctx.fillStyle='#0a1018';ctx.fillRect(0,0,world.w,world.h);
// Storm void beyond the broken perimeter.
for(let y=0;y<world.h;y+=72){for(let x=0;x<world.w;x+=96){let n=Math.sin(x*18.32+y*9.17)*.5+.5;
let palettes=[['#30302b','#2b2b29','#292b2a'],['#252d32','#222930','#20282e'],['#2e2b30','#29272d','#26262d'],['#18292f','#1b2d32','#182a30'],['#302e27','#2b2a26','#292a27'],['#343027','#2e2a24','#2c2924']];ctx.fillStyle=palettes[G.zone-1][n>.6?0:n>.25?1:2];ctx.fillRect(x+1,y+1,94,70);
ctx.strokeStyle='#31383a';ctx.strokeRect(x+2,y+2,92,68);if(n>.68){line(x+21,y+1,x+28,y+20,'#131b22');line(x+28,y+20,x+51,y+31,'#131b22');}
if(n<.22){ctx.fillStyle='#233735';for(let i=0;i<5;i++)ctx.fillRect(x+4+i*5,y+3+i%3*4,5,4);}}}
// A long astronomical meridian runs through all six halls.
for(let yy of [495,585]){line(40,yy,1880,yy,'#574c37',2);line(40,yy+5,1880,yy+5,'#10191f');}
for(let x=80;x<1900;x+=120){diamond(x,540,5,'#544d3a');line(x,492,x,481,'#786143');}
for(let x=200;x<1920;x+=400){circle(x,540,94,null,'#3d413b');circle(x,540,88,null,'#42463e');for(let i=0;i<24;i++){let a=i*TAU/24;line(x+Math.cos(a)*89,540+Math.sin(a)*89,x+Math.cos(a)*94,540+Math.sin(a)*94,'#706047');}}
// Black-iron filaments and moss cling to the perimeter.
for(let i=0;i<55;i++){let x=(i*137+G.zone*23)%1880+20,y=i%2?70:1010;line(x,y,x+30,y+(i%2?60:-60),'#10161d',4);line(x+13,y+26*(i%2?1:-1),x-20,y+50*(i%2?1:-1),'#121920',2);}
if(G.zone===4){for(let i=0;i<22;i++){let x=(i*197)%1800+60,y=(i*127)%900+80;ctx.save();ctx.scale(1,.35);circle(x,y/.35,25+Math.sin(G.time*.025+i)*8,null,'#38545d');ctx.restore();}}
}
function drawWall(b,broken=false){let x=b.x,y=b.y,w=b.w,h=b.h;ctx.fillStyle='#0b1118';ctx.fillRect(x+7,y+9,w,h);
ctx.fillStyle=broken?'#4b4d48':'#384046';ctx.fillRect(x,y-9,w,h);ctx.fillStyle='#1a242c';ctx.fillRect(x,y+h-9,w,10);
ctx.strokeStyle=broken?'#85785b':'#62604e';ctx.strokeRect(x+.5,y-9.5,w-1,h);line(x,y-8,x+w,y-8,'#8b7753');
if(w<h){for(let yy=y+15;yy<y+h-12;yy+=48){line(x,yy,x+w,yy,'#1b242a');ctx.fillStyle='#9a7f4f';ctx.fillRect(x+3,yy-5,3,3);}}
else for(let xx=x+30;xx<x+w;xx+=60)line(xx,y-8,xx,y+h-9,'#1b242a');
if(broken){let cx=x+w/2,cy=y+h/2;line(cx-7,y-9,cx+4,cy-10,'#161e22',2);line(cx+4,cy-10,cx-8,cy+4,'#161e22',2);line(cx-8,cy+4,cx+6,y+h-9,'#161e22',2);line(cx+4,cy-10,x+w,cy+3,'#161e22',2);
if(P.accessories.lantern||b.loot==='heart4'){line(cx-5,y-8,cx+6,cy-10,'#99bbc0');glow(cx,cy,70,'rgba(123,175,173,.1)');}if(b.locked)diamond(cx,cy,7,'#76a8bd');}
}
function drawDeco(d){if(d.broken)return;ctx.save();ctx.translate(d.x,d.y);ctx.scale(d.s,d.s);
if(d.type==='gear'){gear(0,0,72,G.time*.001*(G.zone%2?1:-1),'#60523b');}
else if(d.type==='pillar'){ctx.fillStyle='#141c23';ctx.fillRect(-22,-11,52,40);poly([[-20,9],[0,18],[22,8],[0,-4]],'#6c644e','#897653');ctx.fillStyle='#444c4e';ctx.fillRect(-13,-44,26,51);ctx.fillStyle='#687071';ctx.fillRect(-10,-44,7,50);poly([[-20,-43],[0,-34],[21,-44],[0,-55]],'#77735e','#9f865b');line(0,-30,0,-3,'#a38a58');}
else if(d.type==='shelf'){ctx.fillStyle='#151c22';ctx.fillRect(-72,-25,144,49);ctx.fillStyle='#4c4537';ctx.fillRect(-70,-35,140,45);for(let row=0;row<2;row++){for(let i=0;i<22;i++){ctx.fillStyle=['#6c5641','#536267','#93704b','#505e51'][i%4];ctx.fillRect(-65+i*6,-30+row*20,4,15);}line(-69,-12+row*20,70,-12+row*20,'#aa8351',3);}}
else if(d.type==='grate'){ctx.fillStyle='#131c24';ctx.fillRect(-55,-40,110,80);ctx.strokeStyle='#4b5353';ctx.strokeRect(-55,-40,110,80);for(let i=-48;i<55;i+=9)line(i,-38,i,38,'#4a504a',2);}
else if(d.type==='telescope'){gear(0,17,37,0);ctx.rotate(-.45);ctx.fillStyle='#777153';ctx.fillRect(-12,-45,24,90);ctx.fillStyle='#b39760';ctx.fillRect(-17,-45,34,9);ctx.fillRect(-17,24,34,9);ctx.fillStyle='#334956';ctx.fillRect(-10,-47,20,3);line(-5,-34,-5,22,'#b6a075',3);}
else if(d.type==='rubble'){for(let i=0;i<9;i++){let x=Math.sin(i*9)*37,y=Math.cos(i*7)*27;poly([[x-9,y-7],[x+6,y-12],[x+16,y],[x+3,y+7]],i%2?'#52534b':'#3e4647','#65614d');}}
else if(d.type==='jar'){circle(2,7,13,'#11191e');circle(0,0,12,'#846b4e','#af8c57');circle(0,-7,7,'#25272a','#b7986d');line(5,-4,2,4,'#453b31',2);}
ctx.restore();}
function drawPedestal(o,type){let x=o.x,y=o.y;let active=type==='save'?o.active:type==='altar'?P.weapon>=o.level:type==='beacon'?o.lit:false;
let color=type==='save'?'#f7d993':type==='shop'?'#c4a969':type==='beacon'?'#f4bc66':WEAPONS[o.level].color;
glow(x,y,100,(type==='save'?'rgba(238,188,89,.18)':type==='shop'?'rgba(194,165,105,.13)':'rgba(226,168,67,.15)'));
circle(x,y+4,34,'#1b242a','#716248');circle(x,y+4,29,null,'#a08452');poly([[x-21,y],[x,y+11],[x+21,y],[x,y-12]],'#6e7066','#a4926d');
poly([[x-15,y-27],[x-15,y],[x,y+7],[x+15,y],[x+15,y-27]],'#444d4e','#80785e');
poly([[x-19,y-27],[x,y-17],[x+19,y-27],[x,y-37]],'#9b9377','#d5bd82');
ctx.save();ctx.shadowBlur=20;ctx.shadowColor=color;
if(type==='save'){diamond(x,y-47+Math.sin(G.time*.03)*3,17,color,'#fff1cd');line(x,y-61,x,y-36,'#fff9dd');}
if(type==='altar'){line(x,y-68,x,y-29,color,4);line(x-10,y-34,x+10,y-34,'#b59c65',3);circle(x,y-26,3,'#d8b56d');}
if(type==='shop'){gear(x,y-47,16,G.time*.008,'#c7a567');circle(x,y-47,5,'#ffe0a0');}
if(type==='beacon'){diamond(x,y-58,30,active?'#fff4c2':'#3f3d36','#d9b76a');if(active){let g=ctx.createLinearGradient(x,0,x,y);g.addColorStop(0,'transparent');g.addColorStop(1,'rgba(255,222,143,.75)');ctx.fillStyle=g;ctx.fillRect(x-18,y-600,36,550);}}
ctx.restore();if(G.frame%12===0&&dist(P,o)<400)burst(x+rnd(-24,24),y-22,color,1,.5);
text(type==='save'?'SANCTUARY':type==='shop'?'SIGIL MERCHANT':type==='altar'?(active?'ALTAR RESTORED':WEAPONS[o.level].name.toUpperCase()):'CORE BEACON',x,y+47,'#b5a782',9);
}
function drawEnemy(e){if(e.dead){circle(e.x,e.y,11,'#151d24');line(e.x-12,e.y-6,e.x+13,e.y+8,'#525045',3);return;}
let x=e.x,y=e.y,boss=e.type==='boss';ctx.save();ctx.translate(x,y);if(boss)ctx.scale(1.9,1.9);
let color=e.hitT>0?'#fff0c1':e.stun>20?'#6ba5b9':e.color||'#786d54';
ctx.fillStyle='rgba(4,9,14,.5)';ctx.beginPath();ctx.ellipse(0,10,19,8,0,0,TAU);ctx.fill();
if(e.wind>0)circle(0,0,31,null,'#e79661');
if(e.type==='turret'){gear(0,0,18,G.time*.005,'#697772');diamond(0,-7,12,color,'#a6b6b9');circle(0,-7,4,'#efaa73');}
else{let step=e.aggro?Math.sin(e.anim*3)*3:0;
ctx.fillStyle='#303d45';ctx.fillRect(-10,3,7,13+step);ctx.fillRect(3,3,7,13-step);
poly([[-11,-13],[0,-18],[12,-13],[10,4],[0,10],[-10,4]],color,'#9a9177');
poly([[-16,-15],[-8,-19],[-5,-9],[-16,-5]],color,'#a1987c');poly([[16,-15],[8,-19],[5,-9],[16,-5]],color,'#a1987c');
poly([[-7,-25],[5,-27],[9,-19],[5,-12],[-7,-13]],'#434d52','#8a8874');line(-5,-19,5,-19,e.stun>20?'#b6eeff':'#f8994d',2);
line(-2,-8,2,3,'#171e24',3);line(15,-3,23,14,'#989b92',3);if(boss){gear(0,-28,21,-G.time*.004,'#a68e56');diamond(0,-28,10,'#242a2c','#e5bc68');}}
if(e.stun>20){circle(0,0,23,null,'#87ceea');for(let i=0;i<4;i++)diamond(Math.cos(i*TAU/4+G.time*.02)*23,Math.sin(i*TAU/4+G.time*.02)*23,3,'#b6e9fa');}
if(e.burn>0)glow(0,0,28,'rgba(233,112,39,.3)');ctx.restore();
if(!boss&&e.hp<e.maxHp){ctx.fillStyle='#10181e';ctx.fillRect(x-16,y-39,32,3);ctx.fillStyle='#d5a15d';ctx.fillRect(x-16,y-39,32*e.hp/e.maxHp,3);}
}
function drawPlayer(){ctx.save();ctx.translate(P.x,P.y);let stride=P.moving?Math.sin(P.anim)*3:0;
ctx.fillStyle='rgba(4,9,14,.6)';ctx.beginPath();ctx.ellipse(0,12,18,7,0,0,TAU);ctx.fill();
if(P.iframes>0&&Math.floor(G.time/5)%2===0)ctx.globalAlpha=.6;
let wide=P.weapon===2?4:0;line(-5-wide,6,-6-wide,17+stride,'#b7a988',5);line(5+wide,6,6+wide,17-stride,'#b7a988',5);
poly([[-9,-17],[9,-17],[15+stride*.5,13],[4,10],[-2,15],[-15,12]],P.accessories.cloak?'#233346':'#ad6e3a','#d19e59');
if(P.weapon===2){ctx.shadowColor='#ffd877';ctx.shadowBlur=8;line(-14,12,-2,15,'#edc97a',2);line(4,10,15,13,'#edc97a',2);ctx.shadowBlur=0;}
poly([[-9,-16],[0,-21],[9,-16],[8,-2],[0,3],[-8,-2]],'#495d68','#a6ac9e');line(-7,-8,7,-3,'#cbb281',2);
circle(0,-24,9,'#b5b5a0','#e0cfaa');poly([[-9,-26],[0,-32],[9,-26],[8,-19],[3,-24],[-7,-19]],'#405762','#829591');line(-5,-23,5,-23,'#192932',3);
let a=P.attackT>0?P.attackAngle+(P.swingAge/WEAPONS[P.weapon].cd-.5)*WEAPONS[P.weapon].arc*2*P.swingDir:Math.atan2(G.aim.y,G.aim.x)+.35;
ctx.save();ctx.rotate(a);line(8,0,20,0,'#c6b799',5);
if(P.accessories.wardenband){ctx.shadowBlur=8;ctx.shadowColor='#ccefff';line(16,-3,16,3,'#e0faff',3);ctx.shadowBlur=0;}
let w=WEAPONS[P.weapon];ctx.shadowColor=w.color;ctx.shadowBlur=P.weapon?15:0;poly([[20,-2],[w.range-3,-3],[w.range+3,0],[w.range-3,3],[20,2]],w.color,'#f5dfba');line(22,-7,22,7,'#b59656',3);ctx.restore();
if(G.flags.shard){glow(-14,0,20,'rgba(117,208,236,.3)');diamond(-14,-2,7+Math.sin(G.time*.05),'#8cdae9','#e2faff');}
if(P.accessories.lantern){glow(0,0,100,'rgba(100,191,218,.12)');icon(ctx,'lantern',-16,10,.6);}
if(P.attackT>0){ctx.save();ctx.rotate(P.attackAngle);ctx.strokeStyle=w.color;ctx.lineWidth=P.weapon===2?5:3;ctx.globalAlpha=.65*(P.attackT/w.cd);ctx.shadowBlur=15;ctx.shadowColor=w.color;ctx.beginPath();ctx.arc(0,0,w.range,-w.arc,w.arc);ctx.stroke();ctx.restore();}
ctx.restore();}
function render(){ctx.setTransform(1,0,0,1,0,0);ctx.fillStyle='#080d13';ctx.fillRect(0,0,W,H);ctx.save();let sx=G.shakeT>0?rnd(-G.shake,G.shake):0,sy=G.shakeT>0?rnd(-G.shake,G.shake):0;ctx.translate(-Math.round(cam.x)+sx,-Math.round(cam.y)+sy);drawFloor();
for(const l of world.lights)glow(l.x,l.y,l.r,l.c);
for(const d of world.deco)drawDeco(d);
for(const p of world.plates){ctx.fillStyle=p.down?'#9e824e':'#4d4b3d';ctx.fillRect(p.x,p.y,p.w,p.h);ctx.strokeStyle=p.t>G.time?'#e7c77f':'#a28657';ctx.strokeRect(p.x+3,p.y+3,p.w-6,p.h-6);gear(p.x+p.w/2,p.y+p.h/2,22,0,p.down?'#ecd694':'#9b865b');if(G.zone===3&&p.t>G.time)text(((p.t-G.time)/60).toFixed(1)+'s',p.x+35,p.y-10,'#f0cc80');}
for(const t of world.traps){ctx.fillStyle='#111a21';ctx.fillRect(t.x,t.y,t.w,t.h);for(let x=t.x+10;x<t.x+t.w;x+=16)poly([[x-5,t.y+t.h-5],[x,t.y+(t.t%180<95?2:20)],[x+5,t.y+t.h-5]],t.t%180<95?'#ba795a':'#536167');}
for(const w of world.walls)drawWall(w);
for(const b of world.breakables)if(b.hp>0){ctx.save();if(b.shakes)ctx.translate(rnd(-2,2),0);drawWall(b,true);ctx.restore();}else{ctx.fillStyle='#3e433d';ctx.fillRect(b.x+3,b.y+b.h-5,b.w-6,5);}
for(const d of world.doors){if(!d.open){ctx.fillStyle='#1b2429';ctx.fillRect(d.x,d.y,d.w,d.h);for(let y=d.y;y<d.y+d.h;y+=18){line(d.x,y,d.x+d.w,y,'#ab8751',3);}circle(d.x+d.w/2,d.y+d.h/2,7,'#ba8d48');}else{line(d.x,d.y,d.x,d.y+d.h,'#bba470',2);}}
for(const c of world.chests){let x=c.x,y=c.y;ctx.fillStyle=c.opened?'#2f302c':'#766045';ctx.fillRect(x-17,y-10,34,23);ctx.strokeStyle='#aa8c58';ctx.strokeRect(x-17,y-10,34,23);line(x-17,y-1,x+17,y-1,'#1c2327',2);ctx.fillStyle=c.opened?'#191f23':'#dbbd70';ctx.fillRect(x-3,y-5,6,8);if(!c.opened){glow(x,y,30,'rgba(232,197,123,.1)');if(c.hidden)text('✧',x,y-21,'#cab782',13);}}
for(const c of world.crystals){glow(c.x,c.y,60,c.lit?'rgba(154,222,231,.28)':'rgba(97,152,183,.1)');gear(c.x,c.y,27,G.time*.003,c.lit?'#a8cfcc':'#6c736b');diamond(c.x,c.y-6,16,c.lit?'#c9f5f0':'#467b91','#9cd2df');if(G.zone===4&&(P.accessories.lantern||c.lit))line(c.x,c.y,1700,540,'rgba(139,213,222,.3)',2);}
if(world.saveStone)drawPedestal(world.saveStone,'save');if(world.shopAltar)drawPedestal(world.shopAltar,'shop');if(world.altar)drawPedestal(world.altar,'altar');if(world.beacon)drawPedestal(world.beacon,'beacon');
if(world.exit){let o=world.exit;glow(o.x,540,100,'rgba(221,170,74,.12)');ctx.strokeStyle='#bea068';ctx.lineWidth=5;ctx.strokeRect(o.x-8,485,26,110);ctx.lineWidth=1;text('EASTERN ARCH',o.x-25,466,'#c5b080',9);}
if(world.entry){text('‹ RETURN',world.entry.x,490,'#a99777',11);}
let actors=world.enemies.map(e=>({y:e.y,draw:()=>drawEnemy(e)}));actors.push({y:P.y,draw:drawPlayer});actors.sort((a,b)=>a.y-b.y).forEach(a=>a.draw());
for(const p of world.pickups){glow(p.x,p.y,25,'rgba(202,191,132,.14)');icon(ctx,p.kind,p.x,p.y+Math.sin(G.time*.05)*3,.75);}
for(const b of G.bullets){glow(b.x,b.y,18,b.friendly?'rgba(94,198,235,.6)':'rgba(234,113,57,.4)');circle(b.x,b.y,3,b.friendly?'#e3fbff':'#ffc07c');}
for(const p of G.parts){ctx.globalAlpha=p.life/p.max;circle(p.x,p.y,p.r,p.c);}ctx.globalAlpha=1;
for(const n of G.dmgNums){ctx.globalAlpha=Math.min(1,n.t/20);text(n.text,n.x,n.y,n.c,13);}ctx.globalAlpha=1;
// Suspended dust, rainlight, and submerged mist.
for(let i=0;i<55;i++){let x=cam.x+((i*137.73+G.time*.15)%W),y=cam.y+((i*79.17-G.time*.08+H*100)%H);ctx.globalAlpha=.15+.15*Math.sin(i+G.time*.012);circle(x,y,G.zone===4?2.5:1.2,G.zone===4?'#a3d1db':'#edcea0');}ctx.globalAlpha=1;
ctx.restore();
let vign=ctx.createRadialGradient(W/2,H/2,140,W/2,H/2,560);vign.addColorStop(0,'transparent');vign.addColorStop(1,'rgba(3,8,15,.72)');ctx.fillStyle=vign;ctx.fillRect(0,0,W,H);
if(G.running){drawCompass();drawMiniMap();}
if(world.boss&&!world.boss.dead&&dist(P,world.boss)<600){el('bossbar').classList.remove('hidden');el('bossname').textContent=world.crystals.every(c=>c.lit)?'THE CORE CUSTODIAN':'THE CORE CUSTODIAN · CRYSTAL-WARDED';el('bossfill').style.width=world.boss.hp/world.boss.maxHp*100+'%';}else el('bossbar').classList.add('hidden');
}
function drawCompass(){ctx.fillStyle='rgba(9,15,22,.86)';ctx.fillRect(0,H-51,W,51);line(20,H-51,W-20,H-51,'#655536');
text(WEAPONS[P.weapon].name.toUpperCase(),23,H-29,'#d5bc83',10,'left');text('J / SPACE strike K shard E interact I satchel F quick-use',23,H-12,'#87959b',10,'left');
text('VIGIL '+String(G.zone).padStart(2,'0')+' / 06',W-25,H-30,'#d5bc83',10,'right');text('Quick-use: '+(ITEMS[selectedItem]?.name||'Vital Stem'),W-25,H-12,'#87959b',10,'right');
ctx.fillStyle='rgba(8,14,20,.76)';ctx.fillRect(18,68,360,29);text(objectives[G.zone-1],29,86,'#a7b1af',9,'left');}
function drawMiniMap(){let mx=820,my=340,scale=.06;ctx.save();ctx.translate(mx,my);ctx.fillStyle='rgba(7,13,19,.85)';ctx.fillRect(-7,-19,130,85);ctx.strokeStyle='#574f3b';ctx.strokeRect(-7,-19,130,85);text('OBSERVATORY / '+G.zone,57,-7,'#a89b78',8);
for(const w of world.walls){ctx.fillStyle='#495557';ctx.fillRect(w.x*scale,w.y*scale,Math.max(1,w.w*scale),Math.max(1,w.h*scale));}
for(const e of world.enemies)if(!e.dead)circle(e.x*scale,e.y*scale,1.4,'#a75f45');
for(const b of world.breakables)if(b.hp>0&&P.accessories.lantern)circle((b.x+20)*scale,(b.y+20)*scale,2,'#79c2d6');
if(world.saveStone)diamond(world.saveStone.x*scale,world.saveStone.y*scale,3,'#ebc87c');if(world.altar)circle(world.altar.x*scale,world.altar.y*scale,2,'#d79b50');if(world.shopAltar)circle(world.shopAltar.x*scale,world.shopAltar.y*scale,2,'#8ebbc0');
circle(P.x*scale,P.y*scale,2.4,'#fff1c6');ctx.restore();}
/* ============================== interface & boot ============================== */
function updateHUD(){el('goldval').textContent=G.gold;el('ammoval').textContent=P.ammo;el('ammo').classList.toggle('hidden',!G.flags.shard);el('zonelabel').textContent=ZONES[G.zone-1].label;
el('hearts').innerHTML='';for(let i=0;i<P.containers;i++){let c=document.createElement('canvas');c.width=28;c.height=30;c.style.background='transparent';c.title=Math.max(0,Math.min(2,P.hp-i*2))+' / 2 HP';let h=c.getContext('2d');
h.translate(14,13);h.beginPath();h.moveTo(0,12);h.bezierCurveTo(-26,-5,-7,-22,0,-9);h.bezierCurveTo(7,-22,26,-5,0,12);h.fillStyle='#252c32';h.fill();h.save();h.clip();h.fillStyle='#dc9c67';h.fillRect(-14,-16,28*clamp((P.hp-i*2)/2,0,1),32);h.restore();h.strokeStyle='#cbb082';h.lineWidth=1.3;h.stroke();el('hearts').append(c);}
icon(el('coinico').getContext('2d'),'gold',7,7,.6);icon(el('ammoico').getContext('2d'),'shard',7,7,.5);
}
function zoneCard(){el('zc1').textContent='AURELION’S SUNKEN OBSERVATORY · '+['I','II','III','IV','V','VI'][G.zone-1];el('zc2').textContent=ZONES[G.zone-1].name;let c=el('zonecard');c.classList.remove('show');void c.offsetWidth;c.classList.add('show');}
function renderInventory(){el('invgrid').innerHTML='';for(const id of ['wardenband','vitstem','lantern','cloak','seal','shard']){let slot=own(id),d=document.createElement('button');d.className='slot';d.title=slot?ITEMS[id].name:'Undiscovered';d.style.opacity=slot&&slot.count?1:.3;d.style.borderColor=selectedItem===id?'#e6ba67':'';
let c=document.createElement('canvas');c.width=60;c.height=60;icon(c.getContext('2d'),id,30,30,1.6);d.append(c);let count=document.createElement('span');count.className='cnt';count.textContent=slot?ITEMS[id].type==='con'?'×'+slot.count:'':'?';d.append(count);
if(P.accessories[id]){let eq=document.createElement('span');eq.className='eq';eq.textContent='EQUIPPED';d.append(eq);}d.onclick=()=>{selectedItem=id;renderInventory();};el('invgrid').append(d);}
const d=ITEMS[selectedItem];el('invdesc').innerHTML='<b>'+d.name+'</b><br>'+d.desc+'<br><small>'+d.acq+'</small>';el('invuse').disabled=!own(selectedItem)||!own(selectedItem).count||d.type==='tool';
el('invuse').textContent=d.type==='acc'?(P.accessories[selectedItem]?'Unequip':'Equip')+' (F)':'Use (F)';el('savednote').textContent='Vigor '+P.hp+' / '+P.maxHp+' HP · '+WEAPONS[P.weapon].name+' · Saved at '+(G.checkpoint?.name||'the Threshold');}
function toggleInv(){if(!G.running||P.dead||G.dialogQ.length||G.shopOpen||G.flags.won||G.transition)return;G.invOpen=!G.invOpen;G.paused=G.invOpen;el('inv').classList.toggle('hidden',!G.invOpen);if(G.invOpen){AU.seal();renderInventory();}}
function renderShop(){el('shoprows').innerHTML='';for(const s of SHOPDEF){if(s.id==='ammo'&&!G.flags.vaultComplete)continue;let bought=G.flags['bought_'+s.id]||0,def=ITEMS[s.id]||{name:'Aether charge',desc:'One crystal bolt. The Shard remembers the vault.'};
let row=document.createElement('div');row.className='shoprow';let ico=document.createElement('div');ico.className='ic';let c=document.createElement('canvas');c.width=32;c.height=32;icon(c.getContext('2d'),s.id,16,16,.9);ico.append(c);row.append(ico);
let info=document.createElement('div');info.className='info';let remain=s.id==='heart5'&&G.flags.heart5?0:Math.max(0,s.max-bought);info.innerHTML='<div class="nm">'+def.name+'</div><div class="ds">'+(s.id==='heart5'?'One permanent container · shared with the spire vessel':def.desc)+'<br>'+remain+' remaining</div>';row.append(info);
let price=document.createElement('span');price.className='pr';price.textContent=s.price+' ◈';row.append(price);let btn=document.createElement('button');btn.className='sbtn';btn.textContent=remain?'Buy':'Sold';btn.disabled=!remain||G.gold<s.price;
btn.onclick=()=>{if(G.gold<s.price||!remain)return;G.gold-=s.price;G.flags['bought_'+s.id]=bought+1;if(s.id==='ammo')P.ammo++;else gain(s.id);AU.buy();updateHUD();renderShop();};row.append(btn);el('shoprows').append(row);}}
function toggleShop(force){if(G.shopOpen){G.shopOpen=false;G.paused=false;el('shop').classList.add('hidden');return;}if(force!==true){if(G.invOpen)toggleInv();return;}
if(!G.running||G.invOpen||P.dead)return;G.shopOpen=true;G.paused=true;el('shop').classList.remove('hidden');renderShop();AU.rare();}
function resetGame(){G.zone=1;G.gold=0;G.flags={visit1:true};G.stats={kills:0,goldEarned:0,items:0};G.parts=[];G.bullets=[];G.dmgNums=[];G.dialogQ=[];G.shopOpen=false;G.invOpen=false;G.transition=0;G.checkpoint=null;G.paused=false;G.running=true;G.time=0;
Object.keys(zoneStates).forEach(k=>delete zoneStates[k]);Object.assign(P,{x:130,y:540,hp:6,maxHp:6,containers:3,weapon:0,ammo:0,inv:[],accessories:{},dead:false,attackCd:0,attackT:0,fireCd:0,iframes:60});G.aim={x:1,y:0};buildZone(1);cam.x=0;cam.y=270;saveData=null;selectedItem='vitstem';
for(let id of ['title','endscr','deathscr','shop','inv','dialog'])el(id).classList.add('hidden');AU.init();AU.ac?.resume();AU.startMusic();updateHUD();zoneCard();
toast('The Last Warden wakes. Follow the gold meridian east.');setTimeout(()=>toast('Move with WASD · Strike with J · Rest at the glowing Sanctuary with E'),1900);}
function start(){resetGame();}
try{saveData=JSON.parse(localStorage.getItem('warden-vigil-v2'));}catch(e){}
if(saveData){let resume=document.createElement('button');resume.className='btn ghost';resume.textContent='RESUME THE VIGIL';resume.style.marginTop='10px';resume.onclick=restore;el('startbtn').after(resume);}
el('startbtn').onclick=start;el('respawnbtn').onclick=restore;el('againbtn').onclick=resetGame;el('shopclose').onclick=()=>toggleShop();el('invclose').onclick=toggleInv;el('invuse').onclick=useSelected;
cvs.addEventListener('click',()=>{if(G.dialogQ.length)nextDialog();});
addEventListener('blur',()=>{Object.keys(Keys).forEach(k=>Keys[k]=false);});
let last=performance.now();function loop(now){let dt=Math.min(2,(now-last)/16.6667);last=now;update(dt);render();requestAnimationFrame(loop);}
P.x=200;P.y=540;cam.x=0;cam.y=270;updateHUD();requestAnimationFrame(loop);requestAnimationFrame(()=>el('fader').style.opacity=0);
</script>
<style>
#title{background:linear-gradient(90deg,rgba(5,10,16,.97),rgba(5,10,16,.85) 55%,rgba(5,10,16,.55));align-items:flex-start;text-align:left;padding:6% 7%;overflow:hidden}
#title:before{content:'✧';position:absolute;right:6%;top:12%;font-size:250px;color:rgba(204,167,98,.15);border:1px solid #66563a88;border-radius:50%;width:290px;height:290px;text-align:center;line-height:275px;box-shadow:0 0 0 15px #b7a16c08,0 0 0 17px #b7a16c22;pointer-events:none}
#title h1{font-size:clamp(27px,4vw,48px);max-width:690px;line-height:1.26;letter-spacing:.12em;position:relative}
#title h1:before{content:'A CELESTIAL VIGIL / 01';display:block;background:none;color:#9b937c;font:10px monospace;letter-spacing:.3em;margin-bottom:23px;text-shadow:none}
#title .sub{margin-left:0;max-width:590px;position:relative}
#title .sub:nth-of-type(3){max-width:520px;line-height:2.2;margin-top:22px}
#title .btn{position:relative;font-size:12px;margin-top:24px;box-shadow:none;border-radius:0;padding:14px 27px}
#title:after{content:'SIX HALLS. THREE SANCTUARIES. ONE LAST LIGHT.';position:absolute;bottom:24px;left:7%;font:9px monospace;letter-spacing:.24em;color:#78848a}
#hud{transition:opacity .4s}body:has(#title:not(.hidden)) #hud{opacity:0}
#invgrid{grid-template-columns:repeat(6,1fr)}
button:focus-visible{outline:2px solid #a7d7e7;outline-offset:4px}
#dialog{pointer-events:auto;cursor:pointer}
</style>
</body>
</html>
<!-- agent-meta {"model":"unbiased/pareto","provider":"openrouter","persona":"full","sessionId":"3ec11c43-cd6a-4793-af7f-168f5ca53644","tokensIn":790361,"tokensOut":71172,"tokensTotal":861533,"cost":1.0567527500000002,"turns":27,"toolCalls":26,"failedToolCalls":1,"timestamp":"2026-09-18T02:41:14.372Z"} -->