Warden
ember 1 · 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 35,436 bytes · SHA-256 898a24857008
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>The Warden's Corrosion</title>
<style>
html,body{margin:0;padding:0;background:#0a0d14;height:100%;display:flex;align-items:center;justify-content:center;font-family:Georgia,serif;overflow:hidden}
#wrap{position:relative}
canvas{display:block;background:#0a0d14;box-shadow:0 0 60px #000;border:1px solid #2a2f3d}
.overlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;background:rgba(6,8,14,.88);color:#d8c9a3;text-align:center;z-index:10}
.overlay h1{color:#e8b84b;letter-spacing:4px;margin:0 0 10px;font-size:34px;text-shadow:0 0 20px #b8860b}
.overlay p{max-width:560px;line-height:1.5;color:#a99f85}
.overlay .keys{color:#7f8ba3;font-size:13px;margin-top:14px;line-height:1.8}
.btn{margin-top:18px;padding:10px 34px;background:#1a2030;border:1px solid #e8b84b;color:#e8b84b;cursor:pointer;font-family:Georgia,serif;font-size:16px;letter-spacing:2px}
.btn:hover{background:#e8b84b;color:#10131c}
#inv{display:none;position:absolute;inset:0;background:rgba(6,8,14,.85);z-index:9;color:#d8c9a3;padding:40px;box-sizing:border-box}
#inv h2{color:#e8b84b;letter-spacing:3px;margin-top:0}
.invitem{display:flex;justify-content:space-between;padding:8px 14px;border:1px solid #333c4f;margin:6px 0;background:#11151f;cursor:pointer;max-width:520px}
.invitem:hover{border-color:#e8b84b}
.invitem .eq{color:#7fd4a8}
#shop{display:none;position:absolute;inset:0;background:rgba(6,8,14,.88);z-index:9;color:#d8c9a3;padding:40px;box-sizing:border-box}
.shopitem{display:flex;justify-content:space-between;padding:10px 14px;border:1px solid #333c4f;margin:8px 0;background:#11151f;max-width:560px}
.shopitem button{background:#1a2030;border:1px solid #e8b84b;color:#e8b84b;padding:4px 16px;cursor:pointer;font-family:inherit}
.shopitem button:disabled{opacity:.35;cursor:default}
.shopitem button:hover:not(:disabled){background:#e8b84b;color:#10131c}
#toast{position:absolute;top:18px;left:50%;transform:translateX(-50%);color:#ffe9b0;background:rgba(20,24,36,.9);border:1px solid #e8b84b;padding:8px 22px;z-index:8;display:none;letter-spacing:1px;font-size:15px}
#vignette{position:absolute;inset:0;pointer-events:none;z-index:5;box-shadow:inset 0 0 120px rgba(0,0,0,.8)}
#shimmer{position:absolute;inset:0;pointer-events:none;z-index:6;opacity:0;box-shadow:inset 0 0 90px rgba(255,210,110,.9);transition:opacity .3s}
</style>
</head>
<body>
<div id="wrap">
<canvas id="c" width="960" height="600"></canvas>
<div id="vignette"></div><div id="shimmer"></div>
<div id="toast"></div>
<div id="inv"></div>
<div id="shop"></div>
<div class="overlay" id="title">
<h1>THE WARDEN'S CORROSION</h1>
<p>You are the Last Warden of Aurelion's Sunken Observatory. The Corrosion creeps through bronze and stone. Reignite the Core Beacon — or watch the archive fall into the storm.</p>
<div class="keys">WASD / Arrows — Move • J / Space — Attack • K — Fire Aether Shard<br>
E — Interact • I — Inventory • 1–3 — Use consumable</div>
<button class="btn" id="startBtn">BEGIN THE WATCH</button>
</div>
<div class="overlay" id="death" style="display:none">
<h1 style="color:#c0392b">THE CORROSION TAKES YOU</h1>
<p>Your light fades... but the Sanctuary Stone remembers.</p>
<button class="btn" id="respawnBtn">RETURN TO THE STONE</button>
</div>
<div class="overlay" id="win" style="display:none">
<h1>THE BEACON REIGNITES</h1>
<p id="winStats"></p>
<p>Golden light floods the Observatory. The Corrosion recedes like a tide. The Last Warden's watch is over — the archive endures.</p>
<button class="btn" onclick="location.reload()">WATCH AGAIN</button>
</div>
</div>
<script>
"use strict";
const cv=document.getElementById('c'),ctx=cv.getContext('2d');
const W=960,H=600;
// ============ AUDIO ============
let AC=null;
function audio(){if(!AC)AC=new (window.AudioContext||window.webkitAudioContext)();return AC;}
function tone(f,d,type,vol,slide,delay){try{const a=audio(),t=a.currentTime+(delay||0);const o=a.createOscillator(),g=a.createGain();o.type=type||'sine';o.frequency.setValueAtTime(f,t);if(slide)o.frequency.exponentialRampToValueAtTime(Math.max(20,slide),t+d);g.gain.setValueAtTime(vol||.15,t);g.gain.exponentialRampToValueAtTime(.0001,t+d);o.connect(g).connect(a.destination);o.start(t);o.stop(t+d+.02);}catch(e){}}
function noise(d,vol,fq,delay){try{const a=audio(),t=a.currentTime+(delay||0);const b=a.createBuffer(1,a.sampleRate*d,a.sampleRate);const ch=b.getChannelData(0);for(let i=0;i<ch.length;i++)ch[i]=(Math.random()*2-1)*(1-i/ch.length);const s=a.createBufferSource();s.buffer=b;const f=a.createBiquadFilter();f.type='bandpass';f.frequency.value=fq||800;const g=a.createGain();g.gain.value=vol||.2;s.connect(f).connect(g).connect(a.destination);s.start(t);}catch(e){}}
const S={
step(metal){noise(.06,.05,metal?1400:500);},
swing(){noise(.12,.18,2400);},
swingFire(){noise(.12,.18,2400);noise(.2,.12,300,.05);},
swingSolar(){tone(90,.4,'sawtooth',.12,50);noise(.25,.2,1200,.08);},
hit(){noise(.08,.2,900);tone(180,.08,'square',.08);},
hitFire(){noise(.08,.2,900);noise(.15,.12,400,.03);},
hitSolar(){tone(70,.35,'sawtooth',.18,40);noise(.15,.2,700);},
shoot(){tone(1200,.12,'triangle',.15,300);},
boltHit(){tone(1600,.2,'sine',.12,2400);},
hurt(){tone(220,.2,'sawtooth',.2,110);tone(1800,.15,'sine',.1,900,.02);},
pickup(){tone(660,.1,'sine',.12);tone(880,.12,'sine',.12,null,.09);},
rare(){tone(523,.15,'sine',.14);tone(659,.15,'sine',.14,null,.12);tone(784,.3,'sine',.14,null,.24);},
coin(){tone(1400,.07,'square',.08);tone(1800,.09,'square',.07,null,.05);},
buy(){noise(.1,.15,3000);tone(880,.15,'sine',.14,null,.08);tone(1320,.25,'sine',.12,null,.2);},
save(){tone(392,.3,'sine',.12);tone(523,.4,'sine',.12,null,.25);tone(659,.6,'sine',.12,null,.5);},
altar(){tone(196,.5,'sawtooth',.1,392);tone(523,.8,'sine',.12,null,.3);},
breakWall(){noise(.4,.3,300);noise(.3,.2,150,.1);},
plate(){tone(140,.2,'square',.15,90);},
door(){noise(.5,.2,200);tone(80,.5,'sine',.15,60);},
freeze(){tone(2000,.3,'sine',.12,400);},
heal(){tone(523,.15,'sine',.12);tone(784,.3,'sine',.12,null,.12);},
drone(){tone(65,.8,'sine',.06);tone(98,.8,'sine',.05);},
die(){tone(300,.8,'sawtooth',.2,60);},
beacon(){[261,329,392,523,659,784].forEach((f,i)=>tone(f,.8,'sine',.12,null,i*.18));}
};
// ============ INPUT ============
const keys={};
addEventListener('keydown',e=>{keys[e.key.toLowerCase()]=true;
if([' ','arrowup','arrowdown','arrowleft','arrowright'].includes(e.key.toLowerCase()))e.preventDefault();
if(game.started&&!game.over){
if(e.key.toLowerCase()==='i')toggleInv();
if(e.key==='1')useConsumable('vital');
if(e.key==='2')useConsumable('seal');
}});
addEventListener('keyup',e=>keys[e.key.toLowerCase()]=false);
// ============ PARTICLES ============
const parts=[];
function spawn(x,y,n,color,spd,life,size,grav){for(let i=0;i<n;i++){const a=Math.random()*Math.PI*2,v=Math.random()*spd;parts.push({x,y,vx:Math.cos(a)*v,vy:Math.sin(a)*v-(grav?0:0),life:life*(0.5+Math.random()*0.5),max:life,color,size:size*(0.6+Math.random()*0.8),grav:grav||0});}}
function updParts(dt){for(let i=parts.length-1;i>=0;i--){const p=parts[i];p.x+=p.vx*dt;p.y+=p.vy*dt;p.vy+=p.grav*dt;p.life-=dt;if(p.life<=0)parts.splice(i,1);}}
function drawParts(){for(const p of parts){ctx.globalAlpha=Math.max(0,p.life/p.max);ctx.fillStyle=p.color;ctx.fillRect(p.x-p.size/2,p.y-p.size/2,p.size,p.size);}ctx.globalAlpha=1;}
// ============ GAME STATE ============
const game={started:false,over:false,zone:0,gold:0,kills:0,time:0,
save:{zone:0,hearts:3,hp:6},
inv:{vital:1,seal:0,ammo:0},
acc:{band:false,lantern:false,cloak:false}, // owned
eq:{band:false,lantern:false,cloak:false}, // equipped
flags:{}};
const player={x:480,y:480,r:12,hp:6,hearts:3,speed:170,dir:1,face:{x:0,y:-1},
weapon:0, // 0 ember,1 ignited,2 solar
hasShard:false,atkCd:0,atkAnim:0,inv:0,walkT:0,moving:false,swingId:0};
const WSTATS=[{dmg:1,rate:.45,arc:70,range:34},{dmg:2,rate:.3,arc:80,range:38},{dmg:3,rate:.6,arc:110,range:48}];
function atkRate(){let r=WSTATS[player.weapon].rate;if(game.eq.band)r*=0.8;return r;}
// ============ ZONES ============
// Entity types: wall(rect solid), break(hp,cracked), enemy, plate, door(gate), stone(save), altar, chest, jar, shop, switch(ranged), beacon, pickup
let ents=[],enemies=[],bolts=[],ebolts=[];
const ZONES=[
{name:"The Ashen Threshold",floor:"#232a38",wallc:"#3a4356",amb:"dust",
build(){addWalls();addStone(120,300);addEnemy(600,200,1);addEnemy(700,400,1);addEnemy(500,350,1);
addJar(300,150,'gold',15);addPickup(200,480,'vital');
addDoor(930,270,60,()=>true,"The path to the Rusted Archives opens.");}},
{name:"The Rusted Archives",floor:"#2a2620",wallc:"#4a4034",amb:"dust",metal:true,
build(){addWalls();addStone(480,300);addShop(820,120);
addEnemy(300,200,1);addEnemy(350,420,1);addEnemy(600,350,2);addEnemy(650,180,1);
addChest(150,140,'band');addJar(700,480,'gold',20);addJar(200,480,'gold',10);
addBreak(480,80,80,20,'gold',40);
addDoor(930,270,60,()=>game.kills>=0,"The way to the Collapsed Gallery opens.");}},
{name:"The Collapsed Gallery",floor:"#1f2733",wallc:"#37404f",amb:"dust",
build(){addWalls();addAltar(480,140,1,"IGNITED EDGE","The blade drinks the altar's ember-light.");
addEnemy(300,300,2);addEnemy(650,300,2);addEnemy(480,450,3); // 3 = miniboss
addPlate(180,480,'g1');addGate(480,300,20,120,'g1');
addBreak(880,420,60,60,'heart',0); // heart container 4
addJar(120,120,'gold',15);
addDoor(930,270,60,()=>player.weapon>=1,"Only the Ignited Edge may pass.");}},
{name:"The Sunken Vault",floor:"#16202c",wallc:"#26374a",amb:"mist",wet:true,
build(){addWalls();addBreak(120,280,20,120,'shard',0); // aether shard behind cracked wall
addEnemy(500,200,2);addEnemy(600,420,2);addEnemy(350,400,2);addEnemy(700,300,2);
addPlate(200,150,'v1');addPlate(760,480,'v1');addGate(480,300,20,140,'v1'); // timed pair
addChest(840,140,'lantern');
addJar(300,520,'gold',15);addPickup(200,420,'vital');
addDoor(930,270,60,()=>player.hasShard,"The Aether Shard's light is required.");}},
{name:"The Twin Spire",floor:"#25202e",wallc:"#3d3450",amb:"dust",
build(){addWalls();addStone(120,300);addAltar(480,140,2,"SOLAR FALCHION","Solar fire crowns the blade.");
addEnemy(350,250,2);addEnemy(600,250,2);addEnemy(400,450,4);addEnemy(560,450,4);addEnemy(480,350,3);
addSwitch(880,100,'s1');addGate(700,270,20,310,'s1');addChestBehind(800,420,'heart'); // heart 5 behind ranged gate
addChest(150,520,'cloak');addJar(830,500,'seal',0);addJar(120,120,'gold',30);addChest(480,520,'goldchest');
addDoor(930,270,60,()=>player.weapon>=2,"Only the Solar Falchion may pass.");}},
{name:"The Core Chamber",floor:"#1a1420",wallc:"#33284a",amb:"ember",
build(){addWalls();addEnemy(400,250,4);addEnemy(560,250,4);addEnemy(480,400,5);addEnemy(300,400,2);addEnemy(660,400,2);
addSwitch(880,100,'c1');addGate(480,300,300,20,'c1');
addBeacon(480,140);
addPickup(150,500,'vital');addJar(830,500,'gold',20);}}
];
const ETYPES={1:{hp:3,dmg:1,spd:60,r:13,color:'#6b4a3a',name:'Rusted Sentinel',gold:[5,10]},
2:{hp:5,dmg:1,spd:80,r:14,color:'#4a5a6b',name:'Broken Instrument',gold:[8,14]},
3:{hp:12,dmg:2,spd:55,r:20,color:'#5a3a5a',name:'Gallery Construct',gold:[20,30]},
4:{hp:8,dmg:2,spd:95,r:15,color:'#3a3a5a',name:'Spire Guardian',gold:[12,18]},
5:{hp:25,dmg:2,spd:70,r:24,color:'#1a1a1a',name:'Corrosion Avatar',gold:[40,60]}};
function addWalls(){ents.push({t:'wall',x:0,y:0,w:W,h:20},{t:'wall',x:0,y:H-20,w:W,h:20},{t:'wall',x:0,y:0,w:20,h:H},{t:'wall',x:W-20,y:0,w:20,h:H});}
function addStone(x,y){ents.push({t:'stone',x,y,r:18,on:false});}
function addShop(x,y){ents.push({t:'shop',x,y,r:22});}
function addAltar(x,y,up,name,msg){ents.push({t:'altar',x,y,r:20,up,name,msg,done:false});}
function addEnemy(x,y,tp){const s=ETYPES[tp];enemies.push({x,y,tp,hp:s.hp,max:s.hp,r:s.r,spd:s.spd,dmg:s.dmg,color:s.color,gold:s.gold,hitCd:0,flinch:0,frozen:0,burn:0,wx:x,wy:y,wt:0});}
function addJar(x,y,loot,amt){ents.push({t:'jar',x,y,r:12,loot,amt,dead:false});}
function addChest(x,y,loot){ents.push({t:'chest',x,y,r:14,loot,open:false});}
function addChestBehind(x,y,loot){ents.push({t:'chest',x,y,r:14,loot,open:false});}
function addBreak(x,y,w,h,loot,amt){ents.push({t:'break',x,y,w,h,hp:3,loot,amt,dead:false});}
function addPlate(x,y,id){ents.push({t:'plate',x,y,r:20,id,on:false,timer:0});}
function addGate(x,y,w,h,id){ents.push({t:'gate',x,y,w,h,id,open:false});}
function addDoor(x,y,h,cond,msg){ents.push({t:'door',x:W-20,y:270,w:20,h:60,cond,msg});}
function addSwitch(x,y,id){ents.push({t:'switch',x,y,r:12,id,hit:false});}
function addBeacon(x,y){ents.push({t:'beacon',x,y,r:26,lit:false});}
function addPickup(x,y,kind){ents.push({t:'pickup',x,y,r:10,kind});}
function loadZone(i){game.zone=i;ents=[];enemies=[];bolts=[];ebolts=[];parts.length=0;ZONES[i].build();player.x=60;player.y=300;toast(ZONES[i].name);}
// ============ TOAST / UI ============
let toastT=null;
function toast(msg,ms){const t=document.getElementById('toast');t.textContent=msg;t.style.display='block';clearTimeout(toastT);toastT=setTimeout(()=>t.style.display='none',ms||2600);}
function shimmer(){const s=document.getElementById('shimmer');s.style.opacity=1;setTimeout(()=>s.style.opacity=0,2000);}
// ============ COLLISION ============
function solidAt(x,y,r){for(const e of ents){if(e.t==='wall'||(e.t==='gate'&&!e.open)||(e.t==='break'&&!e.dead)){if(x+r>e.x&&x-r<e.x+e.w&&y+r>e.y&&y-r<e.y+e.h)return true;}}return false;}
function moveEnt(o,dx,dy){if(!solidAt(o.x+dx,o.y,o.r))o.x+=dx;if(!solidAt(o.x,o.y+dy,o.r))o.y+=dy;}
// ============ COMBAT ============
function tryAttack(){if(player.atkCd>0)return;const ws=WSTATS[player.weapon];player.atkCd=atkRate();player.atkAnim=.22;player.swingId++;
if(player.weapon===0)S.swing();else if(player.weapon===1)S.swingFire();else S.swingSolar();
const ax=player.x+player.face.x*ws.range,ay=player.y+player.face.y*ws.range;
for(const en of enemies){const d=Math.hypot(en.x-ax,en.y-ay);if(d<ws.arc+en.r&&en.frozen<=0||d<ws.arc+en.r){if(d<ws.arc+en.r){damageEnemy(en,ws.dmg);}}}
for(const e of ents){if(e.t==='break'&&!e.dead){const cx=Math.max(e.x,Math.min(ax,e.x+e.w)),cy=Math.max(e.y,Math.min(ay,e.y+e.h));if(Math.hypot(cx-ax,cy-ay)<ws.arc){e.hp-=(player.weapon===2?2:1);spawn(cx,cy,10,'#8a7a5a',80,.5,4,300);S.hit();if(e.hp<=0)breakWall(e);}}}
for(const e of ents){if(e.t==='jar'&&!e.dead&&Math.hypot(e.x-ax,e.y-ay)<ws.arc+e.r){e.dead=true;spawn(e.x,e.y,12,'#b09a6a',90,.5,4,200);S.breakWall();dropLoot(e.x,e.y,e.loot,e.amt);}}
}
function damageEnemy(en,dmg){if(en.frozen>0)dmg+=0;en.hp-=dmg;en.flinch=.25;
if(player.weapon===0){S.hit();spawn(en.x,en.y,8,'#ff9a3c',120,.4,3);}
else if(player.weapon===1){S.hitFire();en.burn=2;spawn(en.x,en.y,10,'#ff6a1c',120,.5,3);spawn(en.x,en.y,4,'#666',40,.8,3);}
else{S.hitSolar();spawn(en.x,en.y,14,'#ffd76a',150,.5,4);shimmerEdge();}
if(en.hp<=0)killEnemy(en);}
function killEnemy(en){const i=enemies.indexOf(en);if(i<0)return;enemies.splice(i,1);game.kills++;
const g=en.gold[0]+Math.floor(Math.random()*(en.gold[1]-en.gold[0]));game.gold+=g;S.coin();
spawn(en.x,en.y,20,en.color,140,.7,4,150);spawn(en.x,en.y,10,'#e8b84b',100,.6,3);
toast(en.tp===5?"The Corrosion Avatar dissolves!":"+"+g+" sigils",1200);}
function breakWall(e){e.dead=true;S.breakWall();spawn(e.x+e.w/2,e.y+e.h/2,30,'#7a6a50',160,.8,5,250);if(e.loot)dropLoot(e.x+e.w/2,e.y+e.h/2,e.loot,e.amt);}
function dropLoot(x,y,loot,amt){
if(loot==='gold'){game.gold+=amt;S.coin();spawn(x,y,14,'#e8b84b',110,.7,3);toast("+"+amt+" sigils",1200);}
else if(loot==='heart'){player.hearts=Math.min(5,player.hearts+1);player.hp=player.hearts*2;S.rare();shimmer();spawn(player.x,player.y,26,'#ffd76a',120,1,4);toast("HEART CONTAINER ACQUIRED — maximum health increased!",3500);}
else if(loot==='shard'){player.hasShard=true;game.inv.ammo=10;S.rare();shimmer();toast("THE AETHER SHARD — press K to fire crystal bolts!",4000);}
else if(loot==='band'){game.acc.band=true;game.eq.band=true;S.rare();toast("WARDEN'S BAND equipped — attack speed increased!",3500);}
else if(loot==='lantern'){game.acc.lantern=true;game.eq.lantern=true;S.rare();toast("PHANTOM LANTERN equipped — hidden cracks revealed!",3500);}
else if(loot==='cloak'){game.acc.cloak=true;game.eq.cloak=true;S.rare();toast("SHADOW CLOAK equipped — trap damage reduced!",3500);}
else if(loot==='seal'){game.inv.seal++;S.pickup();toast("Binding Seal acquired (press 2 to use)",2500);}
else if(loot==='vital'){game.inv.vital++;S.pickup();toast("Vital Stem acquired (press 1 to use)",2500);}
else if(loot==='goldchest'){game.gold+=100;S.rare();spawn(x,y,30,'#e8b84b',130,.9,4);toast("ANCIENT CACHE — +100 sigils!",3000);}}
function fireBolt(){if(!player.hasShard||player.atkCd>0)return;if(game.inv.ammo<=0){toast("No Aether charges — restock at the Archives merchant.",2500);return;}
game.inv.ammo--;player.atkCd=.35;S.shoot();
bolts.push({x:player.x+player.face.x*16,y:player.y+player.face.y*16,vx:player.face.x*420,vy:player.face.y*420,life:1.6});}
function useConsumable(k){if(k==='vital'){if(game.inv.vital<=0){toast("No Vital Stems.",1500);return;}if(player.hp>=player.hearts*2){toast("Health already full.",1500);return;}game.inv.vital--;player.hp=Math.min(player.hearts*2,player.hp+4);S.heal();spawn(player.x,player.y,16,'#7fd4a8',80,.8,3);toast("Vital Stem: +4 HP",1800);}
else if(k==='seal'){if(game.inv.seal<=0){toast("No Binding Seals.",1500);return;}let best=null,bd=1e9;for(const en of enemies){const d=Math.hypot(en.x-player.x,en.y-player.y);if(d<bd){bd=d;best=en;}}if(!best){toast("No enemy to bind.",1500);return;}game.inv.seal--;best.frozen=3;S.freeze();spawn(best.x,best.y,18,'#6ab8ff',90,.8,3);toast("Enemy bound in place!",1800);}}
// ============ UPDATE ============
let stepT=0;
function update(dt){game.time+=dt;
if(player.inv>0)player.inv-=dt;if(player.atkCd>0)player.atkCd-=dt;if(player.atkAnim>0)player.atkAnim-=dt;
// movement
let mx=0,my=0;
if(keys['w']||keys['arrowup'])my-=1;if(keys['s']||keys['arrowdown'])my+=1;
if(keys['a']||keys['arrowleft'])mx-=1;if(keys['d']||keys['arrowright'])mx+=1;
player.moving=!!(mx||my);
if(player.moving){const l=Math.hypot(mx,my);mx/=l;my/=l;player.face={x:mx,y:my};
moveEnt(player,mx*player.speed*dt,my*player.speed*dt);player.walkT+=dt;
stepT-=dt;if(stepT<=0){stepT=.32;S.step(ZONES[game.zone].metal);if(player.weapon>=1)spawn(player.x,player.y+10,1,player.weapon===2?'#ffd76a':'#ff7a2c',20,.5,2);}}
if(keys['j']||keys[' '])tryAttack();
if(keys['k'])fireBolt();
if(keys['e'])interact();
// enemies
for(const en of enemies){if(en.hitCd>0)en.hitCd-=dt;if(en.flinch>0)en.flinch-=dt;
if(en.burn>0){en.burn-=dt;if(Math.random()<dt*3){en.hp-=.5;spawn(en.x,en.y,2,'#ff6a1c',40,.4,2);if(en.hp<=0)killEnemy(en);}}
if(en.frozen>0){en.frozen-=dt;continue;}
const d=Math.hypot(player.x-en.x,player.y-en.y);
if(d<320&&d>en.r+player.r+2){const vx=(player.x-en.x)/d,vy=(player.y-en.y)/d;moveEnt(en,vx*en.spd*dt,vy*en.spd*dt);}
else if(d<=en.r+player.r+4&&en.hitCd<=0){en.hitCd=.9;hurtPlayer(en.dmg,en);}
else if(d>=320){en.wt-=dt;if(en.wt<=0){en.wt=2;en.wx=en.x+(Math.random()-.5)*120;en.wy=en.y+(Math.random()-.5)*120;}
const vx=en.wx-en.x,vy=en.wy-en.y,l=Math.hypot(vx,vy);if(l>4)moveEnt(en,vx/l*en.spd*.3*dt,vy/l*en.spd*.3*dt);}}
// bolts
for(let i=bolts.length-1;i>=0;i--){const b=bolts[i];b.x+=b.vx*dt;b.y+=b.vy*dt;b.life-=dt;
spawn(b.x,b.y,1,'#9adfff',10,.3,2);
let dead=b.life<=0||solidAt(b.x,b.y,3);
for(const en of enemies){if(Math.hypot(en.x-b.x,en.y-b.y)<en.r+4){damageEnemy(en,2);S.boltHit();dead=true;break;}}
for(const e of ents){if(e.t==='switch'&&!e.hit&&Math.hypot(e.x-b.x,e.y-b.y)<e.r+6){e.hit=true;S.boltHit();S.plate();spawn(e.x,e.y,16,'#9adfff',100,.6,3);openGateId(e.id);toast("A distant mechanism clicks open.",2500);dead=true;}
if(e.t==='break'&&!e.dead&&b.x>e.x&&b.x<e.x+e.w&&b.y>e.y&&b.y<e.y+e.h){e.hp-=2;S.hit();if(e.hp<=0)breakWall(e);dead=true;}
if(e.t==='jar'&&!e.dead&&Math.hypot(e.x-b.x,e.y-b.y)<e.r+6){e.dead=true;S.breakWall();dropLoot(e.x,e.y,e.loot,e.amt);dead=true;}}
if(dead){spawn(b.x,b.y,8,'#9adfff',90,.4,3);bolts.splice(i,1);}}
// plates
for(const e of ents){if(e.t==='plate'){const was=e.on;e.on=Math.hypot(player.x-e.x,player.y-e.y)<e.r||enemies.some(en=>Math.hypot(en.x-e.x,en.y-e.y)<e.r);
if(e.on&&!was){S.plate();spawn(e.x,e.y,8,'#c9a959',60,.4,3);e.timer=6;checkPlateGroup(e.id);}
if(!e.on&&e.timer>0){e.timer-=dt;if(e.timer<=0)closeGateId(e.id);}}}
// door to next zone
for(const e of ents){if(e.t==='door'&&player.x>W-40&&player.y>270&&player.y<330){
if(e.cond()){if(game.zone<5){S.door();loadZone(game.zone+1);} }
else if(!e.warned||game.time-e.warned>3){e.warned=game.time;toast(e.msg,2500);player.x=W-50;}}}
// ambient particles
const amb=ZONES[game.zone].amb;
if(Math.random()<dt*8){if(amb==='dust')spawn(Math.random()*W,Math.random()*H,1,'#6a6250',8,2.5,1.5);
else if(amb==='mist')spawn(Math.random()*W,H-30,1,'#4a6a8a',-14,3,3);
else if(amb==='ember')spawn(Math.random()*W,H-20,1,'#ff8a3c',-20,2,2);}
for(const e of ents){if(e.t==='stone'&&Math.random()<dt*4)spawn(e.x+(Math.random()-.5)*30,e.y-10,1,'#ffd76a',-15,1.5,2);}
updParts(dt);}
function checkPlateGroup(id){const plates=ents.filter(e=>e.t==='plate'&&e.id===id);
if(plates.every(p=>p.on)){openGateId(id);S.door();toast("Bronze machinery groans — a gate opens.",2500);}}
function openGateId(id){for(const e of ents)if(e.t==='gate'&&e.id===id)e.open=true;}
function closeGateId(id){for(const e of ents)if(e.t==='gate'&&e.id===id){e.open=false;}}
function hurtPlayer(dmg,src){if(player.inv>0)return;
if(game.eq.cloak&&dmg>1)dmg-=1;
player.hp-=dmg;player.inv=1;S.hurt();spawn(player.x,player.y,16,'#e03c2c',130,.6,4);
if(src){const d=Math.hypot(player.x-src.x,player.y-src.y)||1;moveEnt(player,(player.x-src.x)/d*30,(player.y-src.y)/d*30);}
if(player.hp<=0){player.hp=0;game.over=true;S.die();document.getElementById('death').style.display='flex';}}
// ============ INTERACT ============
function near(e,d){return Math.hypot(player.x-e.x,player.y-e.y)<d;}
function interact(){for(const e of ents){
if(e.t==='stone'&&near(e,50)){e.on=true;game.save={zone:game.zone,hearts:player.hearts};player.hp=player.hearts*2;S.save();shimmer();spawn(e.x,e.y,24,'#ffd76a',90,1,3);toast("Sanctuary Stone attuned — progress saved, health restored.",3000);return;}
if(e.t==='shop'&&near(e,60)){openShop();return;}
if(e.t==='altar'&&!e.done&&near(e,55)){e.done=true;player.weapon=e.up;S.altar();shimmer();spawn(e.x,e.y,40,e.up===1?'#ff7a2c':'#ffd76a',140,1.2,4);
toast(e.name+" — "+e.msg,4000);return;}
if(e.t==='chest'&&!e.open&&near(e,45)){e.open=true;S.pickup();dropLoot(e.x,e.y,e.loot,e.amt);return;}
if(e.t==='beacon'&&near(e,60)){if(enemies.length===0){e.lit=true;winGame();}else toast("The Beacon cannot ignite while Corruption lingers.",2500);return;}}}
function winGame(){game.over=true;S.beacon();shimmer();
document.getElementById('winStats').textContent="Constructs felled: "+game.kills+" • Sigils gathered: "+game.gold+" • Hearts: "+player.hearts+"/5";
document.getElementById('win').style.display='flex';}
// ============ SHOP ============
const SHOP=[{id:'vital',name:'Vital Stem',price:50,max:3,desc:'Restores 4 HP'},
{id:'seal',name:'Binding Seal',price:75,max:2,desc:'Freezes one enemy 3s'},
{id:'heart',name:'Heart Container',price:300,max:1,desc:'+1 max heart (2 HP)'},
{id:'ammo',name:'Aether Charge',price:10,max:99,desc:'+1 ranged bolt (post-Vault)'}];
const shopStock={vital:3,seal:2,heart:1,ammo:99};
function openShop(){const s=document.getElementById('shop');S.drone();renderShop();s.style.display='block';}
function renderShop(){const s=document.getElementById('shop');
let h='<h2 style="color:#e8b84b;letter-spacing:3px">MERCHANT ALTAR</h2><p>Sigils: <b style="color:#e8b84b">'+game.gold+'</b> — press E or click outside to leave</p>';
for(const it of SHOP){const stock=shopStock[it.id];const locked=it.id==='ammo'&&!player.hasShard;
h+='<div class="shopitem"><span><b>'+it.name+'</b> — '+it.desc+' <i style="color:#7f8ba3">('+(locked?'locked':stock+' left')+')</i></span><span><button '+(locked||stock<=0||game.gold<it.price?'disabled':'')+' onclick="buy(\''+it.id+'\')">'+it.price+'g</button></span></div>';}
h+='<button class="btn" onclick="closeShop()">LEAVE</button>';s.innerHTML=h;}
function buy(id){const it=SHOP.find(i=>i.id===id);if(game.gold<it.price||shopStock[id]<=0)return;
game.gold-=it.price;shopStock[id]--;S.buy();
if(id==='vital')game.inv.vital++;else if(id==='seal')game.inv.seal++;else if(id==='ammo')game.inv.ammo++;
else if(id==='heart'){player.hearts=Math.min(5,player.hearts+1);player.hp=player.hearts*2;shimmer();}
renderShop();}
function closeShop(){document.getElementById('shop').style.display='none';}
// ============ INVENTORY ============
function toggleInv(){const el=document.getElementById('inv');if(el.style.display==='block'){el.style.display='none';return;}
let h='<h2>INVENTORY</h2><p>Sigils: <b style="color:#e8b84b">'+game.gold+'</b> • Aether charges: '+game.inv.ammo+'</p>';
h+='<div class="invitem" onclick="useConsumable(\'vital\');toggleInv()"><span>🌿 Vital Stem ×'+game.inv.vital+'</span><span>Restore 4 HP [1]</span></div>';
h+='<div class="invitem" onclick="useConsumable(\'seal\');toggleInv()"><span>🔷 Binding Seal ×'+game.inv.seal+'</span><span>Freeze enemy [2]</span></div>';
const accs=[['band',"Warden's Band","+attack speed"],['lantern',"Phantom Lantern","reveals hidden cracks"],['cloak',"Shadow Cloak","reduces heavy damage"]];
for(const [k,n,d] of accs){if(game.acc[k])h+='<div class="invitem" onclick="togEq(\''+k+'\')"><span>'+n+' — '+d+'</span><span class="eq">'+(game.eq[k]?'EQUIPPED':'equip')+'</span></div>';}
h+='<p style="color:#7f8ba3">Weapon: '+['Ember Blade','Ignited Edge','Solar Falchion'][player.weapon]+(player.hasShard?' • Aether Shard':'')+'</p><button class="btn" onclick="toggleInv()">CLOSE</button>';
el.innerHTML=h;el.style.display='block';}
function togEq(k){game.eq[k]=!game.eq[k];S.pickup();toggleInv();toggleInv();}
// ============ RENDER ============
function shimmerEdge(){shimmer();}
function draw(){const z=ZONES[game.zone];
ctx.fillStyle=z.floor;ctx.fillRect(0,0,W,H);
// floor texture
ctx.strokeStyle='rgba(255,255,255,.03)';for(let x=0;x<W;x+=60){ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,H);ctx.stroke();}
for(let y=0;y<H;y+=60){ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(W,y);ctx.stroke();}
for(const e of ents){
if(e.t==='wall'){ctx.fillStyle=z.wallc;ctx.fillRect(e.x,e.y,e.w,e.h);}
else if(e.t==='break'&&!e.dead){ctx.fillStyle='#4a4038';ctx.fillRect(e.x,e.y,e.w,e.h);
const reveal=game.eq.lantern||true;
ctx.strokeStyle=game.eq.lantern?'#9adfff':'#2c2620';ctx.lineWidth=1.5;ctx.beginPath();
ctx.moveTo(e.x+4,e.y+4);ctx.lineTo(e.x+e.w*.5,e.y+e.h*.6);ctx.lineTo(e.x+e.w*.3,e.y+e.h-4);
ctx.moveTo(e.x+e.w-4,e.y+6);ctx.lineTo(e.x+e.w*.5,e.y+e.h*.6);ctx.stroke();}
else if(e.t==='gate'){if(!e.open){ctx.fillStyle='#6a5a2a';ctx.fillRect(e.x,e.y,e.w,e.h);ctx.fillStyle='#8a7a3a';for(let i=4;i<(e.w>e.h?e.w:e.h);i+=12){if(e.w>e.h)ctx.fillRect(e.x+i,e.y,3,e.h);else ctx.fillRect(e.x,e.y+i,e.w,3);}}
else{ctx.strokeStyle='rgba(200,170,80,.3)';ctx.strokeRect(e.x,e.y,e.w,e.h);}}
else if(e.t==='plate'){ctx.fillStyle=e.on?'#c9a959':'#7a6a3a';ctx.beginPath();ctx.arc(e.x,e.y,e.r,0,7);ctx.fill();ctx.strokeStyle='#4a4020';ctx.stroke();
if(e.on){ctx.fillStyle='#e8b84b';ctx.beginPath();ctx.arc(e.x,e.y,5,0,7);ctx.fill();}}
else if(e.t==='stone'){ctx.fillStyle='#3a4356';ctx.beginPath();ctx.arc(e.x,e.y,e.r,0,7);ctx.fill();
ctx.fillStyle=e.on?'#ffd76a':'#8a7a4a';ctx.beginPath();ctx.arc(e.x,e.y-8,7,0,7);ctx.fill();
if(e.on){ctx.shadowColor='#ffd76a';ctx.shadowBlur=20;ctx.fill();ctx.shadowBlur=0;}}
else if(e.t==='shop'){ctx.fillStyle='#2a3448';ctx.beginPath();ctx.arc(e.x,e.y,e.r,0,7);ctx.fill();
ctx.fillStyle='#e8b84b';ctx.font='20px serif';ctx.textAlign='center';ctx.fillText('⚖',e.x,e.y+7);
ctx.fillStyle='#a99f85';ctx.font='11px serif';ctx.fillText('MERCHANT [E]',e.x,e.y+38);}
else if(e.t==='altar'&&!e.done){ctx.fillStyle='#3a2a3a';ctx.beginPath();ctx.arc(e.x,e.y,e.r,0,7);ctx.fill();
ctx.fillStyle=e.up===1?'#ff7a2c':'#ffd76a';ctx.shadowColor=ctx.fillStyle;ctx.shadowBlur=15;
ctx.beginPath();ctx.arc(e.x,e.y-6,6,0,7);ctx.fill();ctx.shadowBlur=0;
ctx.fillStyle='#a99f85';ctx.font='11px serif';ctx.textAlign='center';ctx.fillText('ALTAR [E]',e.x,e.y+36);}
else if(e.t==='chest'&&!e.open){ctx.fillStyle='#5a4a2a';ctx.fillRect(e.x-14,e.y-10,28,20);ctx.fillStyle='#e8b84b';ctx.fillRect(e.x-2,e.y-4,4,8);}
else if(e.t==='jar'&&!e.dead){ctx.fillStyle='#8a6a4a';ctx.beginPath();ctx.arc(e.x,e.y,e.r,0,7);ctx.fill();ctx.fillStyle='#6a4a2a';ctx.fillRect(e.x-5,e.y-e.r-2,10,4);}
else if(e.t==='switch'){ctx.fillStyle=e.hit?'#9adfff':'#3a5a7a';ctx.beginPath();ctx.arc(e.x,e.y,e.r,0,7);ctx.fill();
if(!e.hit){ctx.strokeStyle='#9adfff';ctx.setLineDash([3,4]);ctx.beginPath();ctx.arc(e.x,e.y,e.r+6,0,7);ctx.stroke();ctx.setLineDash([]);}}
else if(e.t==='beacon'){ctx.fillStyle='#2a2a3a';ctx.beginPath();ctx.arc(e.x,e.y,e.r,0,7);ctx.fill();
ctx.fillStyle=e.lit?'#ffd76a':'#4a4a5a';ctx.beginPath();ctx.arc(e.x,e.y-10,10,0,7);ctx.fill();
ctx.fillStyle='#a99f85';ctx.font='11px serif';ctx.textAlign='center';ctx.fillText('CORE BEACON [E]',e.x,e.y+42);}
else if(e.t==='pickup'){const bob=Math.sin(game.time*3+e.x)*3;ctx.fillStyle=e.kind==='vital'?'#7fd4a8':'#6ab8ff';
ctx.shadowColor=ctx.fillStyle;ctx.shadowBlur=10;ctx.beginPath();ctx.arc(e.x,e.y+bob,6,0,7);ctx.fill();ctx.shadowBlur=0;}}
// enemies
for(const en of enemies){ctx.save();ctx.translate(en.x,en.y);
if(en.flinch>0)ctx.translate((Math.random()-.5)*4,0);
ctx.fillStyle=en.frozen>0?'#6ab8ff':en.color;
ctx.beginPath();ctx.arc(0,0,en.r,0,7);ctx.fill();
ctx.fillStyle=en.frozen>0?'#bfe4ff':'#ff4a2c';ctx.beginPath();ctx.arc(-en.r*.3,-en.r*.2,2.5,0,7);ctx.arc(en.r*.3,-en.r*.2,2.5,0,7);ctx.fill();
if(en.burn>0){ctx.fillStyle='rgba(255,106,28,.5)';ctx.beginPath();ctx.arc(0,0,en.r+3,0,7);ctx.fill();}
ctx.restore();
ctx.fillStyle='#222';ctx.fillRect(en.x-en.r,en.y-en.r-9,en.r*2,4);
ctx.fillStyle='#c0392b';ctx.fillRect(en.x-en.r,en.y-en.r-9,en.r*2*Math.max(0,en.hp/en.max),4);}
// bolts
for(const b of bolts){ctx.fillStyle='#9adfff';ctx.shadowColor='#9adfff';ctx.shadowBlur=12;ctx.beginPath();ctx.arc(b.x,b.y,4,0,7);ctx.fill();ctx.shadowBlur=0;}
drawParts();
drawPlayer();
drawHUD();}
function drawPlayer(){const p=player;ctx.save();ctx.translate(p.x,p.y);
if(p.inv>0&&Math.floor(game.time*12)%2===0)ctx.globalAlpha=.5;
// shadow cloak trim
if(game.eq.cloak){ctx.fillStyle='rgba(20,20,40,.7)';ctx.beginPath();ctx.arc(0,2,p.r+4,0,7);ctx.fill();}
// cloak
ctx.fillStyle=game.eq.cloak?'#2a2a44':'#3a4a6b';ctx.beginPath();ctx.arc(0,0,p.r,0,7);ctx.fill();
if(p.weapon===2){ctx.strokeStyle='rgba(255,215,106,.6)';ctx.lineWidth=2;ctx.beginPath();ctx.arc(0,0,p.r+1,0,7);ctx.stroke();}
// head
ctx.fillStyle='#c9b89a';ctx.beginPath();ctx.arc(0,-4,5,0,7);ctx.fill();
// warden's band
if(game.eq.band){ctx.fillStyle='#d8d8e8';ctx.beginPath();ctx.arc(-p.r+2,4,2.5,0,7);ctx.fill();}
// aether shard off-hand
if(p.hasShard){const px=-p.face.y*10,py=p.face.x*10;ctx.fillStyle='#9adfff';ctx.shadowColor='#9adfff';ctx.shadowBlur=8+Math.sin(game.time*5)*4;
ctx.beginPath();ctx.arc(px,py,4,0,7);ctx.fill();ctx.shadowBlur=0;}
// lantern glow
if(game.eq.lantern){ctx.fillStyle='rgba(154,223,255,.15)';ctx.beginPath();ctx.arc(0,0,60,0,7);ctx.fill();}
// blade
const ws=WSTATS[p.weapon];const ang=Math.atan2(p.face.y,p.face.x);
const sw=p.atkAnim>0?(0.22-p.atkAnim)/0.22:0;
const ba=ang+(p.atkAnim>0?(-1+sw*2)*1.1:0.5);
const bl=p.atkAnim>0?ws.range+8:22;
const bx=Math.cos(ba),by=Math.sin(ba);
ctx.strokeStyle=p.weapon===0?'#b8b8c8':p.weapon===1?'#ff8a3c':'#ffd76a';
ctx.lineWidth=p.weapon===2?5:3.5;
if(p.weapon>0){ctx.shadowColor=ctx.strokeStyle;ctx.shadowBlur=p.weapon===2?16:10;}
ctx.beginPath();ctx.moveTo(bx*6,by*6);ctx.lineTo(bx*bl,by*bl);ctx.stroke();ctx.shadowBlur=0;
if(p.atkAnim>0){ctx.strokeStyle=p.weapon===2?'rgba(255,215,106,.4)':p.weapon===1?'rgba(255,138,60,.4)':'rgba(200,200,220,.3)';
ctx.lineWidth=2;ctx.beginPath();ctx.arc(0,0,ws.range,ang-1.1,ang+1.1);ctx.stroke();}
ctx.restore();}
function drawHUD(){
// hearts
for(let i=0;i<player.hearts;i++){const x=24+i*30,y=24;const full=player.hp>=i*2+2,half=player.hp===i*2+1;
ctx.font='22px serif';ctx.textAlign='center';
ctx.fillStyle=full?'#e03c5c':half?'#a03a4c':'#3a2a30';
ctx.fillText('♥',x,y+8);if(half){ctx.fillStyle='#3a2a30';ctx.fillRect(x+1,y-8,10,20);ctx.fillStyle='#e03c5c';ctx.font='22px serif';ctx.fillText('♥',x,y+8);ctx.fillStyle='#3a2a30';ctx.fillRect(x+2,y-10,9,22);}}
ctx.fillStyle='#e8b84b';ctx.font='15px Georgia';ctx.textAlign='left';
ctx.fillText('◈ '+game.gold,20,58);
ctx.fillStyle='#9adfff';if(player.hasShard)ctx.fillText('✦ '+game.inv.ammo,20,78);
ctx.fillStyle='#a99f85';ctx.font='13px Georgia';ctx.textAlign='right';
ctx.fillText(ZONES[game.zone].name+' — Zone '+(game.zone+1)+'/6',W-20,28);
ctx.fillText(['Ember Blade','Ignited Edge','Solar Falchion'][player.weapon],W-20,48);
// interact hint
for(const e of ents){if((e.t==='stone'||e.t==='shop'||e.t==='chest'&&!e.open||e.t==='altar'&&!e.done||e.t==='beacon')&&near(e,60)){
ctx.fillStyle='#ffe9b0';ctx.font='13px Georgia';ctx.textAlign='center';ctx.fillText('[E]',player.x,player.y-24);break;}}}
// ============ LOOP ============
let last=0;
function loop(t){requestAnimationFrame(loop);const dt=Math.min(.05,(t-last)/1000);last=t;
if(!game.started)return;
const uiOpen=document.getElementById('inv').style.display==='block'||document.getElementById('shop').style.display==='block';
if(!game.over&&!uiOpen)update(dt);
draw();}
requestAnimationFrame(loop);
// ============ START ============
document.getElementById('startBtn').onclick=()=>{audio();document.getElementById('title').style.display='none';game.started=true;loadZone(0);toast("The Ashen Threshold — find the Sanctuary Stone.",3500);};
document.getElementById('respawnBtn').onclick=()=>{document.getElementById('death').style.display='none';game.over=false;
player.hearts=game.save.hearts;player.hp=player.hearts*2;loadZone(game.save.zone);toast("The Stone's light restores you.",2500);};
addEventListener('keydown',e=>{if(e.key.toLowerCase()==='e'&&document.getElementById('shop').style.display==='block')closeShop();});
</script>
</body>
</html>
<!-- agent-meta {"model":"fireworks/ember-1","provider":"openrouter","persona":"full","sessionId":"58a5cabb-d6ad-42ed-aa82-f8f56ac972e1","tokensIn":214064,"tokensOut":15481,"tokensTotal":229545,"cost":0.36472260000000006,"turns":12,"toolCalls":11,"failedToolCalls":0,"timestamp":"2026-09-25T15:47:43.803Z"} -->