← Warden results

Warden

GLM 5.3 Flash · full

Initial view of Warden

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Imported historical artifact. Its exact original instructions and lineage are not known.

Generation prompt · exact text unavailable

See the current prompt for reference. It may differ from the instructions used to generate this artifact.

Inspect original source 48,337 bytes · SHA-256 cefff2bf792e
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>The Warden's Corrosion</title>
<style>
:root{--gold:#e8b64a;--ember:#ff7b2e;--slate:#1b2233;--deep:#0d1220}
*{box-sizing:border-box;margin:0;padding:0}
body{background:radial-gradient(ellipse at 50% 30%,#232c44 0%,#0d1220 70%);color:#d9cfae;font-family:'Georgia',serif;display:flex;align-items:center;justify-content:center;min-height:100vh;overflow:hidden}
#wrap{position:relative}
canvas{display:block;background:#141a2a;border:2px solid #3a3424;box-shadow:0 0 60px rgba(232,182,74,.12),inset 0 0 80px rgba(0,0,0,.5);image-rendering:pixelated}
#hud{position:absolute;top:8px;left:10px;pointer-events:none;text-shadow:0 2px 4px #000}
#hearts{display:flex;gap:4px;margin-bottom:6px}
.hv{width:26px;height:24px}
#zonename{font-size:15px;color:var(--gold);letter-spacing:2px;text-transform:uppercase}
#golds{font-size:14px;color:#ffd76a;margin-top:2px}
#ammo{font-size:13px;color:#8fd8ff;margin-top:2px}
#msg{position:absolute;left:50%;bottom:16px;transform:translateX(-50%);max-width:80%;text-align:center;font-size:15px;color:#f0e6c8;background:rgba(13,18,32,.82);border:1px solid #6b5a2a;padding:8px 18px;border-radius:6px;pointer-events:none;transition:opacity .4s;opacity:0;line-height:1.45}
#hint{position:absolute;top:8px;right:12px;text-align:right;font-size:12px;color:#8b84a0;pointer-events:none;line-height:1.5;text-shadow:0 1px 3px #000}
#shimmer{position:absolute;inset:0;pointer-events:none;opacity:0;box-shadow:inset 0 0 60px 20px rgba(255,215,120,.55);transition:opacity .5s}
.panel{position:absolute;inset:0;display:none;align-items:center;justify-content:center;background:rgba(8,11,20,.86);z-index:10}
.panel.open{display:flex}
.card{background:linear-gradient(#1c2338,#12172a);border:2px solid #6b5a2a;border-radius:10px;padding:26px 34px;min-width:420px;box-shadow:0 0 40px rgba(0,0,0,.7)}
.card h2{color:var(--gold);letter-spacing:2px;font-weight:normal;margin-bottom:14px;text-align:center}
.invitem{display:flex;justify-content:space-between;gap:18px;padding:8px 10px;border-bottom:1px solid #2a3350;cursor:pointer;font-size:14px;align-items:center}
.invitem:hover{background:rgba(232,182,74,.08)}
.invitem .nm{color:#eee2bd}.invitem .ds{color:#8b84a0;font-size:12px}
.tag{font-size:11px;padding:2px 8px;border-radius:10px;border:1px solid #556}
.tag.eq{border-color:var(--gold);color:var(--gold)}
.btnrow{text-align:center;margin-top:18px}
button{background:#2a3050;color:#e8d9a8;border:1px solid #6b5a2a;padding:8px 22px;font-family:inherit;font-size:15px;cursor:pointer;border-radius:5px;letter-spacing:1px}
button:hover{background:#3a4270;color:#fff}
.shopitem{display:flex;justify-content:space-between;padding:8px 10px;border-bottom:1px solid #2a3350;font-size:14px;align-items:center;gap:16px}
.price{color:#ffd76a;white-space:nowrap}
.soldout{color:#666;font-style:italic}
#title,#endcard{text-align:center}
#title p,#endcard p{margin:8px 0;line-height:1.6;font-size:14px;color:#a99f85}
#title h1{font-size:30px;color:var(--gold);letter-spacing:4px;font-weight:normal;margin-bottom:6px}
kbd{background:#2a3050;border:1px solid #556;border-radius:3px;padding:1px 6px;font-size:12px;color:#ddd}
#flashfx{position:absolute;inset:0;pointer-events:none;background:rgba(200,30,30,.25);opacity:0}
</style>
</head>
<body>
<div id="wrap">
<canvas id="cv" width="960" height="576"></canvas>
<div id="hud"><div id="hearts"></div><div id="zonename"></div><div id="golds"></div><div id="ammo"></div></div>
<div id="hint">WASD / Arrows — move<br>Space / J — attack · K — shard bolt<br>E — interact · Q — vital stem · F — binding seal<br>I — inventory</div>
<div id="msg"></div><div id="shimmer"></div><div id="flashfx"></div>

<div class="panel" id="titlePanel"><div class="card" id="title">
<h1>THE WARDEN'S CORROSION</h1>
<p><i>Aurelion's Sunken Observatory drifts above a storm-locked valley.<br>The Corrosion creeps through its bronze halls. You are the Last Warden.</i></p>
<p style="color:#777;font-size:12px">Reignite the Core Beacon before the ruin collapses.</p>
<div class="btnrow"><button id="btnNew">Begin the Vigil</button> <button id="btnCont" style="display:none">Continue</button></div>
<p style="font-size:11px;color:#556;margin-top:14px">Progress is kept at Sanctuary Stones.</p>
</div></div>

<div class="panel" id="invPanel"><div class="card"><h2>WARDEN'S SATCHEL</h2><div id="invList"></div>
<p style="font-size:12px;color:#776;margin-top:10px">Click an accessory to equip · click a consumable to use.</p>
<div class="btnrow"><button onclick="closePanels()">Close</button></div></div></div>

<div class="panel" id="shopPanel"><div class="card"><h2>MERCHANT ALTAR</h2><div id="shopList"></div>
<div class="btnrow"><button onclick="closePanels()">Leave</button></div></div></div>

<div class="panel" id="endPanel"><div class="card" id="endcard">
<h2 id="endTitle">THE BEACON BURNS ANEW</h2>
<p>Golden light floods the Observatory. The Corrosion recedes like a tide called home.<br>The Warden keeps the vigil still — but no longer alone in it.</p>
<p id="endStats" style="color:#887f68;font-size:12px"></p>
<div class="btnrow"><button onclick="location.reload()">Begin Again</button></div>
</div></div>
</div>
<script>
"use strict";
// ============ AUDIO ENGINE (WebAudio synth) ============
let AC=null;
function ac(){if(!AC)AC=new (window.AudioContext||window.webkitAudioContext)();if(AC.state==='suspended')AC.resume();return AC;}
function tone(f,dur,type='sine',vol=.15,delay=0,slide=0){const c=ac(),o=c.createOscillator(),g=c.createGain();o.type=type;o.frequency.setValueAtTime(f,c.currentTime+delay);if(slide)o.frequency.exponentialRampToValueAtTime(Math.max(30,f+slide),c.currentTime+delay+dur);g.gain.setValueAtTime(vol,c.currentTime+delay);g.gain.exponentialRampToValueAtTime(.001,c.currentTime+delay+dur);o.connect(g).connect(c.destination);o.start(c.currentTime+delay);o.stop(c.currentTime+delay+dur+.02);}
function noise(dur,vol=.12,delay=0,hp=800){const c=ac(),len=c.sampleRate*dur,b=c.createBuffer(1,len,c.sampleRate),d=b.getChannelData(0);for(let i=0;i<len;i++)d[i]=(Math.random()*2-1)*(1-i/len);const s=c.createBufferSource();s.buffer=b;const f=c.createBiquadFilter();f.type='highpass';f.frequency.value=hp;const g=c.createGain();g.gain.value=vol;s.connect(f).connect(g).connect(c.destination);s.start(c.currentTime+delay);}
const SFX={
 step(){noise(.06,.03,0,300);},
 swing(){noise(.12,.09,0,1200);tone(220,.1,'sawtooth',.04,0,-100);},
 hitBase(){noise(.07,.14,0,500);tone(140,.1,'square',.1);},
 hitFire(){noise(.18,.12,0,400);tone(90,.22,'sine',.14,0,-40);tone(700,.15,'sawtooth',.04);},
 hitSolar(){tone(70,.5,'sine',.22);tone(110,.4,'triangle',.12,.02);noise(.1,.1,0,900);},
 twang(){tone(1300,.15,'triangle',.12,0,-500);tone(2600,.2,'sine',.05,.05);},
 chimeHit(){tone(1800,.25,'sine',.06,.12);},
 hurt(){tone(80,.25,'square',.16);tone(1900,.3,'sine',.05,.03,-600);},
 coin(){tone(1500,.07,'square',.06);tone(2100,.1,'square',.05,.06);},
 pickup(){[520,660,780].forEach((f,i)=>tone(f,.18,'sine',.09,i*.07));},
 rare(){[392,494,587,784].forEach((f,i)=>tone(f,.5,'triangle',.09,i*.09));},
 save(){[262,330,392,523].forEach((f,i)=>tone(f,.7,'sine',.1,i*.15));},
 drone(){tone(65,2.5,'sine',.07);tone(98,2.5,'sine',.05);},
 buy(){tone(900,.1,'square',.08);tone(1350,.25,'sine',.1,.09);},
 shimmer(){tone(1200,.4,'sine',.06);tone(1600,.5,'sine',.05,.1);tone(2400,.6,'sine',.04,.2);},
 door(){tone(60,.6,'sine',.18);noise(.3,.08,0,200);},
 plate(){tone(160,.15,'square',.08,0,-60);},
 seal(){tone(300,.4,'sine',.1);tone(450,.4,'sine',.08,.1);},
 altar(){[196,262,330,392,523].forEach((f,i)=>tone(f,.9,'triangle',.09,i*.12));},
 breakWall(){noise(.4,.2,0,150);tone(70,.4,'square',.12);},
 bossHit(){tone(55,.4,'sine',.2);noise(.15,.15,0,300);},
 freeze(){tone(2000,.5,'sine',.07,0,-800);}
};
</script>
<script>
"use strict";
// ============ WORLD DATA ============
// Legend: # wall  . floor  , grate  ~ mist-water  B breakable(gold)  H breakable(heart)  W breakable(aether shard)
// C chest(accessory)  J jar(binding seal)  $ gold pile  S sanctuary stone  M merchant
// 1 altar upgrade1  2 altar upgrade2  E enemy  G tough enemy  Z boss
// P pressure plate  D plate door  T crystal switch  X exit(north)  ^ spikes  * beacon
const TS=32, COLS=30, ROWS=18;
const ZONES=[
{name:"The Ashen Threshold", map:[
"##############################",
"##############################",
"######..........X#############",
"#####.....S.......############",
"####....###..E....###########.",
"###....####...B..$...#######..",
"###....###..........#######...",
"##.....###...###....######....",
"##......#....#E#..............",
"#.......#..$.###....E.........",
"#...........................#",
"#....E.........####.........##",
"#..............#..#........###",
"###.....###....#..#.......####",
"#####..........####......#####",
"#######................#######",
"##########......##############",
"##############################"],
 intro:"The Ashen Threshold. Your blade feels heavy in the salt wind. Learn its arc, Warden.",
 gate:()=>G.killsThisZone>=4?null:"The way onward is barred until the threshold constructs fall. ("+G.killsThisZone+"/4)",
 onEnter(){showMsg(this.intro);}},
{name:"The Rusted Archives", map:[
"##############################",
"#####..,,,,,,....X###########",
"####...,,S,,,..E..###########",
"###....,,,,,...B.$..#########",
"###....,,,,,.......##########",
"##..M..,,,,,...E....#########",
"##..^..,,,,,.####...#########",
"##.....,,,,,.#C#......#######",
"##..E..,,,,,.....^^...######.",
"#......,,,,,$....^^....#####..",
"#..B...,,,,,,,,,,......####...",
"#....................E.###....",
"#...^^^^....,,,,,,.....###....",
"#...........,,,,,,..B..###....",
"###..E.......,,,,,......##....",
"#####...............$....#....",
"#######..............#########",
"##############################"],
 intro:"The Rusted Archives. Grates clank underfoot. A merchant altar hums beyond the first fray; something silver hides in a bronze chest.",
 gate:()=>{if(G.killsThisZone<5)return "Archives sentinels still stir ("+G.killsThisZone+"/5).";if(!G.flags.band)return "A chest in this hall has not been opened.";return null;},
 onEnter(){showMsg(this.intro);}},

{name:"The Collapsed Gallery", map:[
"##############################",
"####...........X#############",
"###......P.D.P...############",
"###...S....D....1.###########",
"##....##...D........#########",
"##..E.##...D....E...#########",
"#.....##...D.........########",
"#..H..##...D...G.....########",
"#.....##...D.........########",
"#..$..##...D....E....########",
"#.....##..............#######",
"#....######..E....$...###....",
"#.....................###....",
"###......^^^^.........###....",
"#####.................##.....",
"#######...............#......",
"#########...........#########",
"##############################"],
 intro:"The Collapsed Gallery. Bronze plates stud the floor — weight opens their doors. An altar sleeps here, and hairline cracks scar a western wall.",
 gate:()=>{if(!G.flags.upg1)return "The altar's flame is unlit.";if(!G.flags.galleryDoor)return "The plate-doors remain sealed.";return null;},
 onEnter(){showMsg(this.intro);}},

{name:"The Sunken Vault", map:[
"~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~",
"~~~~~~~..........X~~~~~~~~~~~",
"~~~~~....S....T...~~~~~~~~~~~",
"~~~~..T............~~~~~~~~~~",
"~~~....~~~~.........~~~~~~~~~",
"~~..E..~~~~..T......~~~~~~~~~",
"~~.....~~~~.........~~~~~~~~~",
"~...W..~~~~....E.....~~~~~~~~",
"~......~~~~..........~~~~~~~~",
"~..$...~~~~......C...~~~~~~~~",
"~......~~~~..........~~~~~~~~",
"~~..E..~~~~....^^^^..~~~~~~~~",
"~~~....~~~~..........~~~~~~~~",
"~~~~............E....~~~~~~~~",
"~~~~~................~~~~~~~~",
"~~~~~~............~~~~~~~~~~~",
"~~~~~~~~........~~~~~~~~~~~~~",
"~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"],
 intro:"The Sunken Vault. Water swallows each footstep. Crystals glimmer across the dark — and one wall wears a hairline crack that whispers.",
 gate:()=>{if(!G.flags.shard)return "Something is hidden behind cracked stone in this vault.";if(!G.flags.lantern)return "Three vault crystals must be struck by shard-light.";return null;},
 onEnter(){showMsg(this.intro);}},

{name:"The Twin Spire", map:[
"##############################",
"#####...........X#############",
"####.....2.......#############",
"###...S....E.G.E..###########",
"###...............###########",
"##..J..####TTT......#########",
"##.....####.........#########",
"#......####...G......########",
"#..C...####.........E########",
"#......####....$.....########",
"#..^...####..........########",
"#......####...^^^....########",
"#..E...####..........########",
"###....####....E.....####....",
"#####.................##.....",
"#######.......$......###.....",
"#########..........#########",
"##############################"],
 intro:"The Twin Spire. The final ascent. Crystal switches wait across an unreachable span — only shard-light may touch them. An altar of the sun burns somewhere above.",
 gate:()=>{if(!G.flags.upg2)return "The solar altar has not accepted you.";if(!G.killsThisZone>=0&&G.killsThisZone<6)return "The spire gauntlet remains ("+Math.min(G.killsThisZone,6)+"/6).";return null;},
 onEnter(){showMsg(this.intro);}},

{name:"The Core Chamber", map:[
"##############################",
"##############################",
"####......................###",
"###...T..........T........###",
"###........................##",
"##....####..........####...##",
"##....####....Z.....####...##",
"##..........................#",
"#....T........*.......T......",
"#............................#",
"##....####..........####...##",
"##....####..........####...##",
"###........................##",
"###..T.............T.......##",
"####......................###",
"#####....................#####",
"#######.................######",
"##############################"],
 intro:"The Core Chamber. The Beacon gutters like a dying candle. The Corrosion has made itself a body to defend it.",
 gate:null,
 onEnter(){showMsg("Strike the distant crystals with shard-light when the Colossus roars — then close with your blade.");}}
];
const SHOP=[
 {id:'stem', name:"Vital Stem", desc:"Restores 2 full containers (4 HP).", price:50, max:3},
 {id:'seal', name:"Binding Seal", desc:"Freezes one enemy for ~3 seconds.", price:75, max:2},
 {id:'heart5',name:"Heart Container", desc:"A fifth vessel of vigor, wrought in gold.", price:300, max:1},
 {id:'ammo', name:"Aether Charge", desc:"+3 bolts for the Shard.", price:10, max:99}
];
// ============ GAME STATE ============
let G=null;
function newState(cont){
 const s=cont||{
  zone:0, hp:6, maxHp:6, gold:0, ammo:0,
  upg:0, shard:false,
  inv:{stem:0, seal:0}, acc:{band:false,lantern:false,cloak:false}, have:{band:false,lantern:false,cloak:false},
  flags:{band:false,galleryDoor:false,upg1:false,shard:false,lantern:false,upg2:false,spireSwitch:false,bossDown:false},
  shopSold:{}, kills:0, deaths:0, playtime:0
 };
 return s;
}
function saveGame(){try{localStorage.setItem('warden_save',JSON.stringify(G));}catch(e){}}
function loadGame(){try{return JSON.parse(localStorage.getItem('warden_save'));}catch(e){return null;}}

// ============ RUNTIME (per-zone entities) ============
let R={tiles:[],enemies:[],pickups:[],parts:[],bolts:[],plates:[],doors:[],switches:[],breakables:[],player:null,timer:0,plateTimer:0,zoneDone:false};
function idx(x,y){return y*COLS+x;}
function tileAt(tx,ty){if(tx<0||ty<0||tx>=COLS||ty>=ROWS)return '#';return R.tiles[ty][tx];}
function solid(ch){return ch==='#'||ch==='D'||ch==='B'||ch==='H'||ch==='W';}

function buildZone(n){
 G.zone=n; G.killsThisZone=0; R.enemies=[];R.pickups=[];R.parts=[];R.bolts=[];R.plates=[];R.doors=[];R.switches=[];R.breakables=[];R.timer=0;R.plateTimer=0;
 const z=ZONES[n];
 R.tiles=z.map.map(r=>r.padEnd(COLS,'#').slice(0,COLS).split(''));
 // spawn player at bottom-center open tile
 let sy=ROWS-2,sx=Math.floor(COLS/2);
 outer:for(let y=ROWS-2;y>0;y--)for(let x=2;x<COLS-2;x++){if(!solid(R.tiles[y][x])&&R.tiles[y][x]!==','||R.tiles[y][x]===','){sy=y;sx=x;break outer;}}
 if(!G.px){G.px=(sx+.5)*TS;G.py=(sy+.5)*TS;}
 R.player={x:G.px,y:G.py,vx:0,vy:0,face:2,cd:0,hurtT:0,swing:0,stepT:0,trailT:0};
 let ei=0;
 for(let y=0;y<ROWS;y++)for(let x=0;x<COLS;x++){
  const ch=R.tiles[y][x],cx=(x+.5)*TS,cy=(y+.5)*TS;
  switch(ch){
   case 'E': R.enemies.push(mkEnemy(cx,cy,'grunt'));R.tiles[y][x]='.';break;
   case 'G': R.enemies.push(mkEnemy(cx,cy,n===5?'guardian':'brute'));R.tiles[y][x]='.';break;
   case 'Z': R.enemies.push(mkBoss(cx,cy));R.tiles[y][x]='.';break;
   case 'S': R.pickups.push({t:'stone',x:cx,y:cy});R.tiles[y][x]='.';break;
   case 'M': R.pickups.push({t:'merchant',x:cx,y:cy});R.tiles[y][x]='.';break;
   case '1': R.pickups.push({t:'altar1',x:cx,y:cy});R.tiles[y][x]='.';break;
   case '2': R.pickups.push({t:'altar2',x:cx,y:cy});R.tiles[y][x]='.';break;
   case 'C': {
     let item = n===1?'band' : n===4?'cloak' : 'stem';
     R.pickups.push({t:'chest',x:cx,y:cy,item:item});R.tiles[y][x]='.';break;}
   case 'J': R.pickups.push({t:'jar',x:cx,y:cy});R.tiles[y][x]='.';break;
   case '$': R.pickups.push({t:'gold',x:cx,y:cy,amt:20+Math.floor(Math.random()*15)});R.tiles[y][x]='.';break;
   case 'P': R.plates.push({tx:x,ty:y,on:false});break;
   case 'D': R.doors.push({tx:x,ty:y,open:false});break;
   case 'T': R.switches.push({tx:x,ty:y,hit:false,flash:0});break;
   case 'B': R.breakables.push({tx:x,ty:y,hp:n>=4?4:3,item:'gold'});break;
   case 'H': R.breakables.push({tx:x,ty:y,hp:4,item:'heart'});break;
   case 'W': R.breakables.push({tx:x,ty:y,hp:5,item:'shard'});break;
   case 'X': /* exit stays as floor marker */ break;
   case '*': R.pickups.push({t:'beacon',x:cx,y:cy});R.tiles[y][x]='.';break;
  }
 }
 R.zoneDone=false;
 document.getElementById('zonename').textContent="II · "+z.name+" · Zone "+(n+1)+" of 6";
 z.onEnter();
}
</script>
<script>
"use strict";
// ============ ENTITIES ============
function mkEnemy(x,y,kind){
 const base={x,y,vx:0,vy:0,frozen:0,hurtT:0,burn:0,cd:0,t:Math.random()*6};
 if(kind==='grunt')return Object.assign(base,{kind,hp:3,dmg:1,spd:.9,r:12,gold:[5,10],col:'#7a8296'});
 if(kind==='brute')return Object.assign(base,{kind,hp:7,dmg:2,spd:.75,r:15,gold:[10,18],col:'#8a6a52'});
 return Object.assign(base,{kind:'guardian',hp:11,dmg:2,spd:1.05,r:16,gold:[20,30],col:'#a08a5a'}); // spire guardian
}
function mkBoss(x,y){return {x,y,vx:0,vy:0,kind:'boss',hp:42,maxHp:42,dmg:2,spd:.62,r:26,frozen:0,hurtT:0,burn:0,cd:0,stunned:0,t:0,gold:[0,0],col:'#3a3f55',sumT:5};}

// ============ HELPERS ============
const K={};
addEventListener('keydown',e=>{K[e.code]=true;handleKey(e);});
addEventListener('keyup',e=>{K[e.code]=false;});
let msgT=null;
function showMsg(t,long){const m=document.getElementById('msg');m.innerHTML=t;m.style.opacity=1;clearTimeout(msgT);msgT=setTimeout(()=>m.style.opacity=0,long?6500:3200);}
function shimmerFX(){const s=document.getElementById('shimmer');s.style.opacity=1;setTimeout(()=>s.style.opacity=0,2000);SFX.shimmer();}
function hurtFX(){const f=document.getElementById('flashfx');f.style.transition='none';f.style.opacity=.85;requestAnimationFrame(()=>{f.style.transition='opacity .45s';f.style.opacity=0;});}
function spawnParts(x,y,n,col,spread=2,life=30,g=0){for(let i=0;i<n;i++)R.parts.push({x,y,vx:(Math.random()-.5)*spread*2,vy:(Math.random()-.5)*spread*2-(g?g*.4:0),life:life*(.6+Math.random()*.7),maxLife:life,col,sz:1+Math.random()*2.5});}
function addGold(n){G.gold+=n;SFX.coin();updateHUD();}
function heal(n){G.hp=Math.min(G.maxHp,G.hp+n);updateHUD();}

// movement collision
function moveEnt(e,dx,dy,r){
 const tryAxis=(nx,ny)=>{
  for(const[ox,oy]of[[-r,-r],[r,-r],[-r,r],[r,r],[0,-r],[0,r],[-r,0],[r,0]]){
   const tx=Math.floor((nx+ox)/TS),ty=Math.floor((ny+oy)/TS);
   if(solid(tileAt(tx,ty)))return false;
  }return true;};
 if(tryAxis(e.x+dx,e.y))e.x+=dx;
 if(tryAxis(e.x,e.y+dy))e.y+=dy;
}
function dist(a,b){return Math.hypot(a.x-b.x,a.y-b.y);}

// ============ COMBAT ============
const WEAPONS=[
 {name:"Ember Blade",dmg:1,cd:.45,arc:50,range:44,type:'phys'},
 {name:"Ignited Edge",dmg:2,cd:.32,arc:60,range:48,type:'fire'},
 {name:"Solar Falchion",dmg:3,cd:.58,arc:95,range:58,type:'solar'}
];
function attack(){
 const p=R.player,w=WEAPONS[G.upg];
 if(p.cd>0)return;
 let cd=w.cd; if(G.acc.band)cd*=.82;
 p.cd=cd;p.swing=.22;
 if(w.type==='solar'){SFX.hitSolar();tone(45,.4,'sine',.14,.05);}else SFX.swing();
 const fx=[0,-1][0]; // face-based dir
 const dir=[[1,0],[-1,0],[0,1],[0,-1]][p.face];
 // breakables in arc
 for(const b of R.breakables){
  const bx=(b.tx+.5)*TS,by=(b.ty+.5)*TS;
  if(Math.hypot(bx-p.x,by-p.y)<w.range+14&&dot(dir,bx-p.x,by-p.y)>-10){
   b.hp-= (w.type==='solar'?2: w.type==='fire'?1:1) * (w.type==='solar'?2:1);
   SFX.breakWall();spawnParts(bx,by,8,'#9a8a6a');
   if(b.hp<=0)breakWall(b);
  }
 }
 // switches (melee can hit adjacent ones)
 for(const s of R.switches){
  const sx=(s.tx+.5)*TS,sy=(s.ty+.5)*TS;
  if(!s.hit&&dist({x:sx,y:sy},p)<w.range){
   hitSwitch(s);
  }
 }
 // enemies
 for(const e of R.enemies){
  if(dist(e,p)<w.range+e.r && dot(dir,e.x-p.x,e.y-p.y)>-14) damageEnemy(e,w.dmg,w.type);
 }
}
function dot(d,dx,dy){return d[0]*dx+d[1]*dy;}
function fireBolt(){
 if(!G.shard)return;
 if(G.ammo<=0){showMsg("The Aether Shard is spent. Charges may be bought at the Archives merchant.");return;}
 const p=R.player;if(p.cd>0)return;
 G.ammo--;p.cd=.35;SFX.twang();
 const dir=[[1,0],[-1,0],[0,1],[0,-1]][p.face];
 R.bolts.push({x:p.x+dir[0]*14,y:p.y+dir[1]*14,vx:dir[0]*7,vy:dir[1]*7,life:90});
 updateHUD();
}
function updateBolts(){
 for(let i=R.bolts.length-1;i>=0;i--){
  const b=R.bolts[i];b.x+=b.vx;b.y+=b.vy;b.life--;
  spawnParts(b.x,b.y,1,'#8fd8ff',.4,14);
  let dead=b.life<=0||solid(tileAt(Math.floor(b.x/TS),Math.floor(b.y/TS)));
  for(const s of R.switches){if(!s.hit&&Math.abs(b.x-(s.tx+.5)*TS)<16&&Math.abs(b.y-(s.ty+.5)*TS)<16){hitSwitch(s);dead=true;break;}}
  if(!dead)for(const e of R.enemies){if(dist(b,e)<e.r+6){damageEnemy(e,2,'aether');dead=true;break;}}
  if(dead)R.bolts.splice(i,1);
 }
}
function hitSwitch(s){
 s.hit=true;s.flash=30;SFX.chimeHit();spawnParts((s.tx+.5)*TS,(s.ty+.5)*TS,14,'#bfe8ff');
 const zone=G.zone;
 if(zone===3&&R.switches.every(q=>q.hit)){
  G.flags.lantern=true;G.have.lantern=true;G.acc.lantern=true;SFX.rare();
  showMsg("The third crystal sings — a <b>Phantom Lantern</b> materializes! Hidden passages now glow faint blue when equipped.");shimmerFX();
 }
 if(zone===4&&R.switches.every(q=>q.hit)&&!G.flags.spireSwitch){
  G.flags.spireSwitch=true;SFX.rare();
  revealHeart5();
 }
 const boss=R.enemies.find(e=>e.kind==='boss');
 if(boss&&boss.stunned<=0){boss.stunned=4;showMsg("The crystal's resonance staggers the Colossus — its core is exposed!");SFX.freeze();spawnParts(boss.x,boss.y,20,'#bfe8ff');}
}
function revealHeart5(){
 // spawn heart container pickup near the switches alcove
 R.pickups.push({t:'heart',x:(14.5)*TS,y:(6.5)*TS});
 showMsg("Far mechanisms grind — a hidden alcove opens. A <b>golden Heart Container</b> waits within.");shimmerFX();
}
function breakWall(b){
 R.tiles[b.ty][b.tx]='.';
 R.breakables.splice(R.breakables.indexOf(b),1);
 SFX.door();spawnParts((b.tx+.5)*TS,(b.ty+.5)*TS,20,'#8a7a5a',3,40);
 if(b.item==='heart'){R.pickups.push({t:'heart',x:(b.tx+.5)*TS,y:(b.ty+.5)*TS});showMsg("Behind the cracked stone: a <b>Heart Container</b>!");SFX.rare();}
 else if(b.item==='shard'){
  R.pickups.push({t:'shardPickup',x:(b.tx+.5)*TS,y:(b.ty+.5)*TS});
 } else {addGold(25+Math.floor(Math.random()*20));spawnParts((b.tx+.5)*TS,(b.ty+.5)*TS,12,'#ffd76a');}
}
function damageEnemy(e,dmg,type){
 if(e.kind==='boss'&&e.stunned>0)dmg*=2;
 e.hp-=dmg;e.hurtT=.18;
 if(type==='fire'){e.burn=1.2;SFX.hitFire();spawnParts(e.x,e.y,10,'#ff9a3c');}
 else if(type==='solar'){SFX.bossHit();spawnParts(e.x,e.y,14,'#ffe08a',3);}
 else if(type==='aether'){SFX.chimeHit();spawnParts(e.x,e.y,8,'#8fd8ff');}
 else {SFX.hitBase();spawnParts(e.x,e.y,7,'#ffb066');}
 if(e.hp<=0)killEnemy(e);
}
function killEnemy(e){
 R.enemies.splice(R.enemies.indexOf(e),1);
 G.kills++;G.killsThisZone++;
 const amt=e.gold[0]+Math.floor(Math.random()*(e.gold[1]-e.gold[0]+1));
 if(amt>0){for(let i=0;i<Math.min(6,Math.ceil(amt/5));i++)R.pickups.push({t:'coin',x:e.x+(Math.random()-.5)*24,y:e.y+(Math.random()-.5)*24,amt:Math.ceil(amt/Math.min(6,Math.ceil(amt/5)))});}
 spawnParts(e.x,e.y,16,'#c9b98a',3,36);
 if(e.kind==='boss'){
  G.flags.bossDown=true;SFX.altar();shimmerFX();
  setTimeout(()=>endGame(),2600);
 }
}

// ============ PLAYER DAMAGE / CONSUMABLES ============
function hurtPlayer(dmg,srcName){
 const p=R.player;if(p.hurtT>0)return;
 if(G.acc.cloak&&srcName==='trap')dmg=Math.max(1,dmg-1);
 G.hp-=dmg*1;p.hurtT=1;hurtFX();SFX.hurt();
 spawnParts(p.x,p.y,12,'#ff4444',3,28);
 updateHUD();
 if(G.hp<=0){G.deaths++;respawn();}
}
function respawn(){
 showMsg("The Corrosion claims you... but a Sanctuary Stone pulls you back.");
 G.hp=G.maxHp;
 // return to last activated stone zone position or zone start
 if(G.stoneZone!==undefined){buildZone(G.stoneZone);}
 else buildZone(G.zone);
 saveGame();
}
function useStem(){
 if(G.inv.stem<=0){showMsg("No Vital Stems remain.");return;}
 if(G.hp>=G.maxHp){showMsg("Your hearts are already full.");return;}
 G.inv.stem--;heal(4);SFX.pickup();spawnParts(R.player.x,R.player.y,12,'#6fe07a');showMsg("The Vital Stem's warmth knits your wounds. (+4 HP)");
 updateHUD();
}
function useSeal(){
 if(G.inv.seal<=0){showMsg("No Binding Seals remain.");return;}
 let best=null,bd=1e9;
 for(const e of R.enemies){const d=dist(e,R.player);if(d<180&&d<bd){bd=d;best=e;}}
 if(!best){showMsg("No enemy within reach of the seal.");return;}
 G.inv.seal--;best.frozen=3;SFX.seal();SFX.freeze();
 spawnParts(best.x,best.y,14,'#66aaff');showMsg("A Binding Seal flares — the construct is locked in place!");
 updateHUD();
}

// ============ INTERACTION ============
function nearestInteract(){
 const p=R.player;let best=null,bd=52;
 for(const k of R.pickups){const d=dist(k,p);if(d<bd){bd=d;best=k;}}
 return best;
}
function interact(){
 const k=nearestInteract();if(!k)return;
 switch(k.t){
  case 'stone':
   SFX.save();G.stoneZone=G.zone;saveGame();
   heal(G.maxHp);shimmerFX();
   spawnParts(k.x,k.y,24,'#ffe9a8',3,50);
   showMsg("<b>Sanctuary Stone attuned.</b> Progress secured, wounds healed. The golden motes settle around you.",true);
   break;
  case 'merchant': openShop();break;
  case 'altar1':
   if(G.flags.upg1){showMsg("The altar's flame already lives in your blade.");return;}
   G.upg=1;G.flags.upg1=true;SFX.altar();shimmerFX();
   spawnParts(k.x,k.y,30,'#ff9a3c',4,60);
   showMsg("<b>IGNITED EDGE</b> — your blade burns with ember-fire. Swings come faster, strike harder, and enemies flinch in flame.",true);
   break;
  case 'altar2':
   if(G.flags.upg2){showMsg("The sun already answers your call.");return;}
   G.upg=2;G.flags.upg2=true;SFX.altar();shimmerFX();
   spawnParts(k.x,k.y,34,'#ffe08a',4,70);
   showMsg("<b>SOLAR FALCHION</b> — light floods the steel. Each heavy sweep carves a wide golden arc through the Corrosion.",true);
   break;
  case 'chest':{
   const item=k.item;R.pickups.splice(R.pickups.indexOf(k),1);
   if(item==='band'&&!G.have.band){G.have.band=true;G.acc.band=true;G.flags.band=true;SFX.rare();showMsg("Inside the bronze chest: the <b>Warden's Band</b>. A silver ring hums as you clasp it — your swings quicken.",true);}
   else if(item==='cloak'&&!G.have.cloak){G.have.cloak=true;G.acc.cloak=true;SFX.rare();showMsg("The <b>Shadow Cloak</b>, torn from a Spire Guardian's shoulders. Its dark hem drinks the light — traps bite less deeply.",true);}
   else {addGold(40);G.inv.stem++;}
   break;}
  case 'jar':R.pickups.splice(R.pickups.indexOf(k),1);G.inv.seal++;SFX.pickup();showMsg("A broken jar yields a <b>Binding Seal</b>, still faintly glowing.");break;
  case 'gold':case 'coin':R.pickups.splice(R.pickups.indexOf(k),1);addGold(k.amt);spawnParts(k.x,k.y,8,'#ffd76a');break;
  case 'heart':
   R.pickups.splice(R.pickups.indexOf(k),1);
   G.maxHp+=2;G.hp=G.maxHp;SFX.rare();shimmerFX();
   spawnParts(R.player.x,R.player.y,26,'#ffd76a',3,60);
   showMsg("A <b>Heart Container</b> fuses to your chest. Your vigor grows — "+(G.maxHp/2)+" containers now burn within you.",true);
   updateHUD();break;
  case 'shardPickup':
   R.pickups.splice(R.pickups.indexOf(k),1);
   G.shard=true;G.flags.shard=true;G.ammo=10;SFX.rare();shimmerFX();
   showMsg("The <b>AETHER SHARD</b> pulses in your off-hand. Press <kbd>K</kbd> to loose crystalline bolts — ten charges hum within.",true);
   updateHUD();break;
  case 'beacon':
   if(!G.flags.bossDown){showMsg("The Beacon gutters — the Colossus bars your reach.");return;}
   SFX.altar();SFX.save();endGame();break;
 }
}

// ============ PLATES & DOORS ============
function updatePlates(dt){
 const p=R.player;
 for(const pl of R.plates){
  const px=(pl.tx+.5)*TS,py=(pl.ty+.5)*TS;
  const on=Math.hypot(px-p.x,py-p.y)<16;
  if(on&&!pl.on){pl.on=true;SFX.plate();spawnParts(px,py,6,'#e8b64a');
   if(!(R.plateTimer>0))for(const q of R.plates)q.hit=false;
   pl.hit=true;R.plateTimer=4;
  }
  if(!on&&pl.on)pl.on=false;
 }
 if(R.plateTimer>0){
  R.plateTimer-=dt;
  if(R.plates.length&&R.plates.every(q=>q.hit)){
   for(const d of R.doors)if(!d.open){d.open=true;R.tiles[d.ty][d.tx]='.';SFX.door();spawnParts((d.tx+.5)*TS,(d.ty+.5)*TS,10,'#e8b64a');}
   if(G.zone===2&&!G.flags.galleryDoor){G.flags.galleryDoor=true;showMsg("The bronze plates sang in sequence — the segmented doors grind open, permanently!");}
   R.plateTimer=0;
  } else if(R.plateTimer<=0){for(const q of R.plates)q.hit=false;}
 }
}

// ============ SHOP ============
function openShop(){
 SFX.drone();
 const L=document.getElementById('shopList');L.innerHTML='';
 for(const it of SHOP){
  const sold=(G.shopSold[it.id]||0)>=it.max||(it.id==='ammo'&&!G.shard)||(it.id==='heart5'&&G.maxHp>=10);
  const left=it.id==='ammo'?'∞':(it.max-(G.shopSold[it.id]||0));
  const row=document.createElement('div');row.className='shopitem';
  row.innerHTML=`<div><div class="nm">${it.name}</div><div class="ds" style="color:#8b84a0;font-size:12px">${it.desc}</div></div>
   <div>${sold?`<span class="soldout">${it.id==='ammo'&&!G.shard?'requires Aether Shard':'sold out'}</span>`:
   `<span class="price">${it.price} g</span> <button data-id="${it.id}">Buy (${left})</button>`}</div>`;
  L.appendChild(row);
 }
 const b=L.querySelectorAll('button');b.forEach(btn=>btn.onclick=()=>buy(btn.dataset.id));
 document.getElementById('shopPanel').classList.add('open');
}
function buy(id){
 const it=SHOP.find(s=>s.id===id);
 if(G.gold<it.price){showMsg("Not enough golden sigils.");return;}
 if(id==='ammo'){G.gold-=it.price;G.ammo+=3;SFX.buy();}
 else{
  if((G.shopSold[id]||0)>=it.max)return;
  G.gold-=it.price;G.shopSold[id]=(G.shopSold[id]||0)+1;
  if(id==='stem'){G.inv.stem++;}
  if(id==='seal'){G.inv.seal++;}
  if(id==='heart5'){G.maxHp+=2;G.hp=G.maxHp;}
  SFX.buy();SFX.shimmer();
 }
 updateHUD();openShop();
}
function closePanels(){document.getElementById('invPanel').classList.remove('open');document.getElementById('shopPanel').classList.remove('open');}

// ============ INVENTORY UI ============
function openInv(){
 closePanels();
 const L=document.getElementById('invList');L.innerHTML='';
 const accs=[
  ['band',"Warden's Band","Attack speed +18%. Silver ring glints on your wrist."],
  ['lantern',"Phantom Lantern","Reveals cracks and hidden passages with soft blue light."],
  ['cloak',"Shadow Cloak","Environmental traps deal 1 less damage. Dark cloth trim."]];
 for(const[id,nm,ds]of accs){
  if(!G.have[id])continue;
  const eq=G.acc[id];
  const row=document.createElement('div');row.className='invitem';
  row.innerHTML=`<div><span class="nm">${nm}</span><div class="ds">${ds}</div></div><span class="tag ${eq?'eq':''}">${eq?'EQUIPPED':'equip'}</span>`;
  row.onclick=()=>{for(const k in G.acc)G.acc[k]=false;G.acc[id]=true;SFX.pickup();openInv();};
  L.appendChild(row);
 }
 const cons=[['stem',"Vital Stem × "+G.inv.stem,"Restores 4 HP."],['seal',"Binding Seal × "+G.inv.seal,"Freezes one enemy ~3s."]];
 for(const[id,nm,ds]of cons){
  const row=document.createElement('div');row.className='invitem';
  row.innerHTML=`<div><span class="nm">${nm}</span><div class="ds">${ds}</div></div><span class="tag">use</span>`;
  row.onclick=()=>{closePanels();id==='stem'?useStem():useSeal();};
  L.appendChild(row);
 }
 if(!L.children.length)L.innerHTML='<p style="text-align:center;color:#776;padding:20px">The satchel is empty.</p>';
 document.getElementById('invPanel').classList.add('open');
}
function handleKey(e){
 if(!running)return;
 if(e.code==='KeyI'){document.getElementById('invPanel').classList.contains('open')?closePanels():openInv();}
 if(e.code==='Escape')closePanels();
 if(e.code==='KeyQ')useStem();
 if(e.code==='KeyF')useSeal();
 if(e.code==='KeyE')interact();
 if(e.code==='Space'||e.code==='KeyJ'){attack();e.preventDefault();}
 if(e.code==='KeyK')fireBolt();
}
</script>
<script>
"use strict";
// ============ UPDATE ============
let running=false,last=0,elapsed=0;
function update(dt){
 const p=R.player;R.timer+=dt;G.playtime+=dt;
 if(p.cd>0)p.cd-=dt;if(p.hurtT>0)p.hurtT-=dt;if(p.swing>0)p.swing-=dt;
 // input
 let dx=0,dy=0;
 if(K['KeyW']||K['ArrowUp'])dy=-1;if(K['KeyS']||K['ArrowDown'])dy=1;
 if(K['KeyA']||K['ArrowLeft'])dx=-1;if(K['KeyD']||K['ArrowRight'])dx=1;
 const spd=(G.acc.cloak?2.15:2.3);
 if(dx||dy){
  const l=Math.hypot(dx,dy);dx/=l;dy/=l;
  moveEnt(p,dx*spd*60*dt,dy*spd*60*dt,10);
  if(Math.abs(dx)>Math.abs(dy))p.face=dx>0?0:1;else p.face=dy>0?2:3;
  p.stepT-=dt;
  if(p.stepT<=0){p.stepT=.34;SFX.step();}
  // ember trail
  if(G.upg>=1){p.trailT-=dt;if(p.trailT<=0){p.trailT=.06;spawnParts(p.x,p.y+8,G.upg===2?2:1,G.upg===2?'#ffd76a':'#ff7b2e',.6,22);}}
 }
 // hazards
 const tc=tileAt(Math.floor(p.x/TS),Math.floor(p.y/TS));
 if(tc==='^'&&p.hurtT<=.4)hurtPlayer(1,'trap');
 // exit
 if(tc==='X'){
  const gate=ZONES[G.zone].gate;
  const blocked=gate&&gate();
  if(!blocked){
   G.px=null;G.py=null;saveGame();
   buildZone(G.zone+1);return;
  } else if(!R.gateMsgT||R.timer-R.gateMsgT>3){R.gateMsgT=R.timer;showMsg(blocked);}
 }
 updatePlates(dt);
 updateBolts();
 // pickups auto-collect for coins
 for(let i=R.pickups.length-1;i>=0;i--){
  const k=R.pickups[i];
  if(k.t==='coin'&&dist(k,p)<20){addGold(k.amt);spawnParts(k.x,k.y,5,'#ffd76a');R.pickups.splice(i,1);continue;}
  if(k.t==='heart'&&dist(k,p)<26)interact();
  if(k.t==='shardPickup'&&dist(k,p)<26)interact();
 }
 // enemies
 for(const e of R.enemies){
  e.t+=dt;if(e.cd>0)e.cd-=dt;if(e.hurtT>0)e.hurtT-=dt;
  if(e.burn>0){e.burn-=dt;e.hp-=dt*1.2;if(Math.random()<.15)spawnParts(e.x,e.y,1,'#ff7b2e',.8,18);if(e.hp<=0){killEnemy(e);break;}}
  if(e.frozen>0){e.frozen-=dt;continue;}
  if(e.kind==='boss'){
   if(e.stunned>0){e.stunned-=dt;continue;}
   const d=dist(e,p),dir=[(p.x-e.x)/d,(p.y-e.y)/d];
   moveEnt(e,dir[0]*e.spd*60*dt,dir[1]*e.spd*60*dt,e.r);
   e.sumT-=dt;
   if(e.sumT<=0&&R.enemies.length<5){e.sumT=7;
    const a=Math.random()*6.28;const m=mkEnemy(e.x+Math.cos(a)*60,e.y+Math.sin(a)*60,'grunt');
    if(tileAt(Math.floor(m.x/TS),Math.floor(m.y/TS))!=='#')R.enemies.push(m);
    showMsg("The Colossus tears shards from itself — new constructs rise!");SFX.door();}
   if(d<e.r+14&&e.cd<=0){e.cd=1.2;hurtPlayer(2,'boss');}
   continue;
  }
  const d=dist(e,p);
  if(d<260){
   const dir=[(p.x-e.x)/d,(p.y-e.y)/d];
   let mx=dir[0]*e.spd*60*dt,my=dir[1]*e.spd*60*dt;
   // simple wall slide
   const ox=e.x,oy=e.y;moveEnt(e,mx,my,e.r);
   if(Math.abs(e.x-ox)<Math.abs(mx)*.3)e.y+=(my>=0?1:-1)*e.spd*30*dt;
   if(Math.abs(e.y-oy)<Math.abs(my)*.3)e.x+=(mx>=0?1:-1)*e.spd*30*dt;
   if(d<e.r+12&&e.cd<=0){e.cd=.9;hurtPlayer(e.dmg,'melee');}
  }
 }
 // particles
 for(let i=R.parts.length-1;i>=0;i--){const q=R.parts[i];q.x+=q.vx;q.y+=q.vy;q.vy+=.02;q.life--;if(q.life<=0)R.parts.splice(i,1);}
 for(const s of R.switches)if(s.flash>0)s.flash-=dt*60;
}
function endGame(){
 running=false;
 document.getElementById('endStats').textContent=`Constructs felled: ${G.kills} · Sigils gathered: ${G.gold} · Deaths: ${G.deaths} · Vigil kept: ${Math.floor(G.playtime/60)}m ${Math.floor(G.playtime%60)}s`;
 document.getElementById('endPanel').classList.add('open');
 try{localStorage.removeItem('warden_save');}catch(e){}
}

// ============ RENDER ============
const cv=document.getElementById('cv'),ctx=cv.getContext('2d');
function drawHeart(x,y,fill,sz){
 ctx.save();ctx.translate(x,y);ctx.scale(sz/24,sz/24);
 ctx.beginPath();ctx.moveTo(12,21);
 ctx.bezierCurveTo(2,13,-1,6,5,3);ctx.bezierCurveTo(9,1,12,4,12,7);
 ctx.bezierCurveTo(12,4,15,1,19,3);ctx.bezierCurveTo(25,6,22,13,12,21);ctx.closePath();
 ctx.fillStyle=fill?'#ff5a5a':'#2a2532';ctx.fill();
 ctx.strokeStyle='#e8b64a';ctx.lineWidth=1.6;ctx.stroke();
 ctx.restore();
}
function draw(){
 const z=ZONES[G.zone];
 // zone palette
 const pal={0:['#232a3c','#1b2130'],1:['#2a2822','#211f1a'],2:['#262238','#1c192b'],3:['#1e3038','#16242b'],4:['#2c2436','#201a29'],5:['#33222a','#251820']}[G.zone];
 ctx.fillStyle=pal[1];ctx.fillRect(0,0,cv.width,cv.height);
 // tiles
 for(let y=0;y<ROWS;y++)for(let x=0;x<COLS;x++){
  const ch=R.tiles[y][x],px=x*TS,py=y*TS;
  if(ch==='#'){ctx.fillStyle=pal[0];ctx.fillRect(px,py,TS,TS);ctx.fillStyle='rgba(0,0,0,.35)';ctx.fillRect(px,py+TS-4,TS,4);ctx.fillStyle='rgba(255,255,255,.03)';ctx.fillRect(px,py,TS,3);}
  else{
   ctx.fillStyle='rgba(0,0,0,.18)';ctx.fillRect(px,py,TS,TS);
   if(ch===','){ctx.fillStyle='#3a3628';for(let i=0;i<TS;i+=8)ctx.fillRect(px+i+(y%2?4:0),py,4,TS);}
   if(ch==='~'){ctx.fillStyle='rgba(80,140,160,.12)';ctx.fillRect(px,py,TS,TS);if((x+y+Math.floor(R.timer*2))%7===0){ctx.fillStyle='rgba(160,220,240,.15)';ctx.fillRect(px+6,py+8,12,2);}}
   if(ch==='^'){ctx.fillStyle='#443';ctx.fillRect(px+4,py+4,TS-8,TS-8);ctx.fillStyle='#998';for(let i=0;i<3;i++){ctx.beginPath();ctx.moveTo(px+7+i*8,py+24);ctx.lineTo(px+11+i*8,py+8);ctx.lineTo(px+15+i*8,py+24);ctx.fill();}}
   if(ch==='B'||ch==='H'||ch==='W'){ctx.fillStyle='#4a4034';ctx.fillRect(px,py,TS,TS);ctx.strokeStyle='rgba(20,15,10,.8)';ctx.lineWidth=1.4;ctx.beginPath();ctx.moveTo(px+4,py+6);ctx.lineTo(px+14,py+16);ctx.lineTo(px+10,py+27);ctx.moveTo(px+22,py+4);ctx.lineTo(px+17,py+15);ctx.lineTo(px+26,py+24);ctx.stroke();
    if(ch!=='B'){ctx.strokeStyle=G.acc.lantern?'rgba(120,200,255,.9)':'rgba(200,190,170,.45)';ctx.strokeRect(px+.5,py+.5,TS-1,TS-1);}}
   if(ch==='X'){ctx.fillStyle='rgba(232,182,74,.14)';ctx.fillRect(px,py,TS,TS);ctx.fillStyle='rgba(255,225,150,.5)';for(let i=0;i<3;i++)ctx.fillRect(px+8,py+6+i*8,16,3);}
   if(ch==='D'){ctx.fillStyle='#5a4a30';ctx.fillRect(px,py,TS,TS);ctx.fillStyle='#3a2f1c';ctx.fillRect(px+3,py+3,TS-6,TS-6);}
  }
 }
 // plates
 for(const pl of R.plates){const px=pl.tx*TS,py=pl.ty*TS;ctx.fillStyle=pl.on?'#c89a3a':'#6a5636';ctx.fillRect(px+4,py+4,TS-8,TS-8);ctx.strokeStyle='#2a2214';ctx.strokeRect(px+4.5,py+4.5,TS-9,TS-9);}
 // pickups
 for(const k of R.pickups)drawPickup(k);
 // switches
 for(const s of R.switches){
  const px=(s.tx+.5)*TS,py=(s.ty+.5)*TS;
  ctx.save();ctx.translate(px,py);
  const gl=s.hit?.4:1;
  ctx.fillStyle=s.hit?'rgba(140,200,230,.5)':`rgba(150,220,255,${gl})`;
  ctx.beginPath();ctx.moveTo(0,-12);ctx.lineTo(9,0);ctx.lineTo(0,12);ctx.lineTo(-9,0);ctx.closePath();ctx.fill();
  if(s.flash>0){ctx.globalAlpha=s.flash/30;ctx.strokeStyle='#fff';ctx.lineWidth=3;ctx.stroke();ctx.globalAlpha=1;}
  ctx.restore();
 }
 // enemies
 for(const e of R.enemies)drawEnemy(e);
 // bolts
 for(const b of R.bolts){ctx.fillStyle='#bfe8ff';ctx.beginPath();ctx.arc(b.x,b.y,4,0,7);ctx.fill();ctx.fillStyle='rgba(143,216,255,.4)';ctx.beginPath();ctx.arc(b.x,b.y,8,0,7);ctx.fill();}
 // player
 drawPlayer();
 // particles
 for(const q of R.parts){ctx.globalAlpha=Math.max(0,q.life/q.maxLife);ctx.fillStyle=q.col;ctx.fillRect(q.x-q.sz/2,q.y-q.sz/2,q.sz,q.sz);}
 ctx.globalAlpha=1;
 // lantern darkness overlay in vault
 if(G.zone===3){
  ctx.fillStyle='rgba(5,10,20,.35)';ctx.fillRect(0,0,cv.width,cv.height);
  if(G.acc.lantern){const g=ctx.createRadialGradient(R.player.x,R.player.y,20,R.player.x,R.player.y,180);g.addColorStop(0,'rgba(110,180,255,0)');g.addColorStop(1,'rgba(5,10,20,.55)');
   ctx.save();ctx.globalCompositeOperation='destination-out';
   const g2=ctx.createRadialGradient(R.player.x,R.player.y,30,R.player.x,R.player.y,170);g2.addColorStop(0,'rgba(0,0,0,.75)');g2.addColorStop(1,'rgba(0,0,0,0)');
   ctx.fillStyle=g2;ctx.fillRect(0,0,cv.width,cv.height);ctx.restore();}
 }
}
function drawPickup(k){
 const t=R.timer;
 ctx.save();ctx.translate(k.x,k.y);
 switch(k.t){
  case 'stone':
   ctx.fillStyle='#3a3f55';ctx.fillRect(-12,-6,24,20);ctx.fillStyle='#565f85';ctx.fillRect(-9,-22,18,18);
   {const g=.5+Math.sin(t*3)*.2;ctx.fillStyle=`rgba(255,230,150,${g})`;ctx.beginPath();ctx.arc(0,-13,6,0,7);ctx.fill();
    if(Math.random()<.05)spawnParts(k.x,k.y-14,1,'#ffe9a8',.5,50);}
   break;
  case 'merchant':
   ctx.fillStyle='#4a3a28';ctx.fillRect(-16,-4,32,14);ctx.fillStyle='#6a5636';ctx.fillRect(-13,-16,26,12);
   {const g=.4+Math.sin(t*2)*.15;ctx.fillStyle=`rgba(200,150,255,${g})`;ctx.beginPath();ctx.arc(0,-10,5,0,7);ctx.fill();}
   ctx.fillStyle='#ffd76a';ctx.font='10px Georgia';ctx.textAlign='center';ctx.fillText('⌂',0,-20);
   break;
  case 'altar1':case 'altar2':{
   const c=k.t==='altar1'?'#ff7b2e':'#ffe08a';
   ctx.fillStyle='#3a3f55';ctx.fillRect(-14,0,28,12);ctx.fillStyle='#565f85';ctx.fillRect(-10,-12,20,12);
   const fl=Math.sin(t*6+k.x)*2;
   ctx.fillStyle=c;ctx.beginPath();ctx.moveTo(-5,-12);ctx.quadraticCurveTo(fl,-30-fl,5,-12);ctx.fill();
   if(Math.random()<.2)spawnParts(k.x,k.y-24,1,c,.7,40);
   break;}
  case 'chest':
   ctx.fillStyle='#6a4a22';ctx.fillRect(-12,-10,24,18);ctx.fillStyle='#8a6432';ctx.fillRect(-12,-14,24,6);ctx.fillStyle='#e8b64a';ctx.fillRect(-2,-8,4,6);
   break;
  case 'jar':ctx.fillStyle='#7a6a52';ctx.fillRect(-6,-8,12,14);ctx.fillStyle='#66aaff';ctx.fillRect(-3,-5,6,6);break;
  case 'gold':case 'coin':
   ctx.fillStyle='#ffd76a';ctx.beginPath();ctx.arc(0,0,5+Math.sin(t*4+k.x)*1,0,7);ctx.fill();ctx.strokeStyle='#a87c20';ctx.stroke();break;
  case 'heart':{const g=.6+Math.sin(t*3)*.3;ctx.globalAlpha=g;drawHeart(0,-8,1,22);ctx.globalAlpha=1;
   if(Math.random()<.08)spawnParts(k.x,k.y,1,'#ffe9a8',.5,40);break;}
  case 'shardPickup':{const g=.6+Math.sin(t*4)*.3;ctx.fillStyle=`rgba(143,216,255,${g})`;ctx.beginPath();ctx.moveTo(0,-14);ctx.lineTo(8,0);ctx.lineTo(0,14);ctx.lineTo(-8,0);ctx.closePath();ctx.fill();
   if(Math.random()<.1)spawnParts(k.x,k.y,1,'#8fd8ff',.6,40);break;}
  case 'beacon':{
   const g=.5+Math.sin(t*2)*.3;
   ctx.fillStyle='#2a2f45';ctx.fillRect(-16,4,32,16);
   ctx.fillStyle=`rgba(255,215,120,${g*.7})`;ctx.beginPath();ctx.arc(0,-6,14+Math.sin(t*3)*3,0,7);ctx.fill();
   ctx.fillStyle=`rgba(255,245,200,${g})`;ctx.beginPath();ctx.arc(0,-6,7,0,7);ctx.fill();
   if(Math.random()<.3)spawnParts(k.x,k.y-6,1,'#ffe9a8',1,60);break;}
 }
 ctx.restore();
}
function drawEnemy(e){
 ctx.save();ctx.translate(e.x,e.y);
 const bob=Math.sin(e.t*4)*2;
 if(e.frozen>0){ctx.fillStyle='rgba(100,170,255,.35)';ctx.beginPath();ctx.arc(0,0,e.r+6,0,7);ctx.fill();}
 if(e.burn>0&&Math.random()<.3)spawnParts(e.x,e.y-6,1,'#ff7b2e',1,20);
 // body
 ctx.fillStyle=e.hurtT>0?'#fff':e.col;
 if(e.kind==='boss'){
  const st=e.stunned>0;
  ctx.fillStyle=st?'#8fb8dd':(e.hurtT>0?'#fff':'#3a3f55');
  ctx.fillRect(-e.r,-e.r+bob,e.r*2,e.r*2);
  ctx.fillStyle=st?'#ffe08a':'#c93a3a';ctx.beginPath();ctx.arc(0,bob-4,st?10:7,0,7);ctx.fill();
  ctx.strokeStyle='#141824';ctx.lineWidth=3;ctx.strokeRect(-e.r,-e.r+bob,e.r*2,e.r*2);
  // hp bar
  ctx.fillStyle='#000';ctx.fillRect(-30,-e.r-14,60,6);ctx.fillStyle='#c93a3a';ctx.fillRect(-30,-e.r-14,60*e.hp/e.maxHp,6);
 }else{
  ctx.fillRect(-e.r*.7,-e.r+bob,e.r*1.4,e.r*1.7);
  ctx.beginPath();ctx.arc(0,-e.r+bob,e.r*.55,0,7);ctx.fill();
  ctx.fillStyle=e.hurtT>0?'#fff':'#c93a3a';ctx.fillRect(-4,-e.r-2+bob,3,3);ctx.fillRect(2,-e.r-2+bob,3,3);
  ctx.strokeStyle='#141824';ctx.lineWidth=2;ctx.strokeRect(-e.r*.7,-e.r+bob,e.r*1.4,e.r*1.7);
 }
 ctx.restore();
}
function drawPlayer(){
 const p=R.player,t=R.timer;
 ctx.save();ctx.translate(p.x,p.y);
 const bob=Math.abs(Math.sin(t*8))*( (K['KeyW']||K['KeyA']||K['KeyS']||K['KeyD']||K['ArrowUp']||K['ArrowLeft']||K['ArrowDown']||K['ArrowRight'])?1:0);
 if(p.hurtT>.6){ctx.fillStyle='rgba(255,60,60,.35)';ctx.beginPath();ctx.arc(0,0,20,0,7);ctx.fill();}
 // cloak
 const wide=G.upg===2?15:12;
 ctx.fillStyle=G.acc.cloak?'#2c2438':'#4a4438';
 ctx.beginPath();ctx.moveTo(0,-12-bob);ctx.lineTo(wide,12);ctx.lineTo(-wide,12);ctx.closePath();ctx.fill();
 if(G.upg===2){ctx.strokeStyle='rgba(255,220,130,.7)';ctx.lineWidth=1.5;ctx.stroke();}
 if(G.acc.cloak){ctx.strokeStyle='#161022';ctx.lineWidth=3;ctx.beginPath();ctx.moveTo(-wide+2,10);ctx.lineTo(wide-2,10);ctx.stroke();}
 // head
 ctx.fillStyle='#d9cfae';ctx.beginPath();ctx.arc(0,-14-bob,6,0,7);ctx.fill();
 // hood
 ctx.fillStyle='#5a5344';ctx.beginPath();ctx.arc(0,-16-bob,6.5,Math.PI,0);ctx.fill();
 // accessories tokens
 if(G.acc.band){ctx.strokeStyle='#cfd8e8';ctx.lineWidth=2;ctx.beginPath();ctx.arc(9,2,3.5,0,7);ctx.stroke();}
 if(G.acc.lantern){const g=.5+Math.sin(t*3)*.3;ctx.fillStyle=`rgba(120,200,255,${g})`;ctx.beginPath();ctx.arc(-11,0,4,0,7);ctx.fill();
  ctx.fillStyle=`rgba(120,200,255,${g*.25})`;ctx.beginPath();ctx.arc(-11,0,12,0,7);ctx.fill();}
 if(G.shard){const g=.6+Math.sin(t*5)*.3;ctx.fillStyle=`rgba(143,216,255,${g})`;ctx.beginPath();ctx.moveTo(-12,-6);ctx.lineTo(-8,0);ctx.lineTo(-12,6);ctx.lineTo(-16,0);ctx.closePath();ctx.fill();}
 // blade
 const dir=[[1,0],[-1,0],[0,1],[0,-1]][p.face];
 const ang=Math.atan2(dir[1],dir[0]);
 const swingOff=p.swing>0?(p.swing/.22)*1.4-0.7:0.35;
 ctx.save();ctx.rotate(ang+swingOff);
 const bl=G.upg===0?18:G.upg===1?20:24;
 ctx.strokeStyle=['#9aa2b2','#ff7b2e','#ffd76a'][G.upg];ctx.lineWidth=G.upg===2?5:3.5;
 ctx.beginPath();ctx.moveTo(6,4);ctx.lineTo(6+bl,4);ctx.stroke();
 if(G.upg>0){ctx.shadowColor=G.upg===1?'#ff7b2e':'#ffd76a';ctx.shadowBlur=10;ctx.stroke();}
 ctx.restore();
 // swing arc
 if(p.swing>0){
  ctx.globalAlpha=p.swing/.22*.5;
  ctx.strokeStyle=['#cfd8e8','#ff9a3c','#ffe08a'][G.upg];ctx.lineWidth=3;
  ctx.beginPath();ctx.arc(0,0,WEAPONS[G.upg].range*(1-p.swing/.22*.4)+8,ang-1.2,ang+1.2);ctx.stroke();
  ctx.globalAlpha=1;
 }
 ctx.restore();
}
// HUD
function updateHUD(){
 const H=document.getElementById('hearts');H.innerHTML='';
 for(let i=0;i<G.maxHp/2;i++){
  const s=document.createElementNS('http://www.w3.org/2000/svg','svg');
  s.setAttribute('class','hv');s.setAttribute('viewBox','0 0 24 24');
  const full=G.hp>=(i+1)*2,half=!full&&G.hp>i*2;
  s.innerHTML=`<path d="M12 21 C2 13 -1 6 5 3 C9 1 12 4 12 7 C12 4 15 1 19 3 C25 6 22 13 12 21Z" fill="${full?'#ff5a5a':half?'url(#hg)':'#2a2532'}" stroke="#e8b64a" stroke-width="1.4"/>`+
   (half?`<clipPath id="hgc${i}"><rect x="0" y="0" width="12" height="24"/></clipPath><path d="M12 21 C2 13 -1 6 5 3 C9 1 12 4 12 7 C12 4 15 1 19 3 C25 6 22 13 12 21Z" fill="#ff5a5a" clip-path="url(#hgc${i})"/>`:'');
  H.appendChild(s);
 }
 document.getElementById('golds').textContent="◆ "+G.gold+" sigils";
 document.getElementById('ammo').textContent=G.shard?("✦ Aether charges: "+G.ammo):"";
}
// ============ LOOP & BOOT ============
function frame(ts){
 requestAnimationFrame(frame);
 if(!running)return;
 const dt=Math.min(.05,(ts-last)/1000)||.016;last=ts;
 update(dt);draw();
}
function startGame(cont){
 ac();
 G=newState(cont);
 document.getElementById('titlePanel').classList.remove('open');
 buildZone(G.zone);
 updateHUD();
 running=true;last=0;
 requestAnimationFrame(frame);
}
document.getElementById('btnNew').onclick=()=>startGame(null);
if(loadGame())document.getElementById('btnCont').style.display='';
document.getElementById('btnCont').onclick=()=>startGame(loadGame());
document.getElementById('titlePanel').classList.add('open');
</script>
</body>
</html>
<!-- agent-meta {"model":"z-ai/glm-5.3-flash","provider":"openrouter","persona":"full","sessionId":"cb9b98ca-d2cc-496a-b5a9-b02e37863bc4","tokensIn":1104173,"tokensOut":36187,"tokensTotal":1140360,"cost":0.03464694499999999,"turns":40,"toolCalls":39,"failedToolCalls":4,"timestamp":"2026-08-27T02:04:57.466Z"} -->