← Warden results

Warden

Claude Sonnet 5 · 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 43,114 bytes · SHA-256 a30b0366d7f5
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>The Warden's Corrosion</title>
<style>
  :root{ --gold:#e0a838; --ember:#ff7a2e; --solar:#ffd75e; --slate:#1b2230; }
  html,body{margin:0;padding:0;background:#05070a;overflow:hidden;height:100%;font-family:'Georgia',serif;color:#f0e6d2;}
  #wrap{position:relative;width:960px;height:600px;margin:20px auto;box-shadow:0 0 60px rgba(0,0,0,.8);border:3px solid #3a2e1a;border-radius:4px;background:#000;}
  canvas{display:block;background:#0a0d12;image-rendering:pixelated;}
  #ui{position:absolute;top:0;left:0;width:960px;height:600px;pointer-events:none;}
  #hearts{position:absolute;top:10px;left:10px;display:flex;gap:4px;}
  .heart{width:26px;height:22px;position:relative;filter:drop-shadow(0 0 3px #000);}
  .heart svg{width:100%;height:100%;}
  #gold{position:absolute;top:10px;right:14px;font-size:20px;color:var(--gold);text-shadow:0 0 6px #000;display:flex;align-items:center;gap:6px;}
  #weaponInfo{position:absolute;top:40px;left:10px;font-size:13px;color:#dcc89a;text-shadow:0 0 4px #000;}
  #zoneBanner{position:absolute;top:220px;left:0;width:100%;text-align:center;font-size:34px;letter-spacing:4px;color:#f5e3b8;text-shadow:0 0 20px #000,0 0 8px var(--gold);opacity:0;transition:opacity 1.2s;}
  #toastBox{position:absolute;bottom:70px;left:0;width:100%;text-align:center;}
  .toast{display:inline-block;background:rgba(10,8,4,.75);border:1px solid #6b5327;padding:6px 14px;margin:2px;border-radius:4px;font-size:14px;color:#f5e3b8;opacity:0;animation:toastIn 3.2s ease forwards;}
  @keyframes toastIn{0%{opacity:0;transform:translateY(10px);}15%{opacity:1;transform:translateY(0);}80%{opacity:1;}100%{opacity:0;}}
  #hint{position:absolute;bottom:8px;left:10px;font-size:12px;color:#9a8b6a;}
  #interactPrompt{position:absolute;left:50%;top:60%;transform:translate(-50%,0);background:rgba(0,0,0,.6);padding:6px 14px;border:1px solid var(--gold);border-radius:4px;display:none;font-size:14px;}
  .modal{position:absolute;top:60px;left:130px;width:700px;max-height:480px;overflow:auto;background:linear-gradient(#1a140a,#0c0a06);border:2px solid var(--gold);border-radius:6px;padding:16px;pointer-events:auto;box-shadow:0 0 40px rgba(0,0,0,.8);display:none;}
  .modal h2{margin:0 0 10px;color:var(--gold);text-align:center;letter-spacing:2px;}
  .row{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #332a1a;padding:8px 4px;}
  .row button{background:#3a2e14;color:#f0e0b0;border:1px solid var(--gold);padding:4px 10px;border-radius:4px;cursor:pointer;font-family:inherit;}
  .row button:disabled{opacity:.35;cursor:not-allowed;}
  .closeBtn{position:absolute;top:8px;right:10px;cursor:pointer;color:#e08a8a;font-size:18px;}
  #deathScreen,#victoryScreen{position:absolute;inset:0;display:none;align-items:center;justify-content:center;flex-direction:column;background:rgba(0,0,0,.88);pointer-events:auto;text-align:center;}
  #deathScreen h1{color:#c0302a;font-size:48px;letter-spacing:6px;}
  #victoryScreen h1{color:var(--solar);font-size:40px;letter-spacing:6px;text-shadow:0 0 30px var(--solar);}
  #deathScreen button,#victoryScreen button{margin-top:16px;padding:10px 24px;background:#3a2e14;border:1px solid var(--gold);color:#f0e0b0;border-radius:4px;cursor:pointer;font-size:16px;}
  #fade{position:absolute;inset:0;background:#000;opacity:0;pointer-events:none;transition:opacity .6s;}
</style>
</head>
<body>
<div id="wrap">
<canvas id="c" width="960" height="600"></canvas>
<div id="ui">
  <div id="hearts"></div>
  <div id="gold">⛁ <span id="goldAmt">0</span></div>
  <div id="weaponInfo"></div>
  <div id="zoneBanner"></div>
  <div id="toastBox"></div>
  <div id="interactPrompt"></div>
  <div id="hint">WASD move · Space melee · F ranged (once found) · E interact · I inventory · 1 Vital Stem · 2 Binding Seal</div>

  <div class="modal" id="shopModal">
    <span class="closeBtn" id="shopClose">✕</span>
    <h2>Merchant Altar</h2>
    <div id="shopList"></div>
  </div>
  <div class="modal" id="invModal">
    <span class="closeBtn" id="invClose">✕</span>
    <h2>Inventory</h2>
    <div id="invList"></div>
  </div>

  <div id="deathScreen"><h1>THE WARDEN FALLS</h1><p>The Corrosion claims another guardian.</p><button id="respawnBtn">Respawn at Last Sanctuary</button></div>
  <div id="victoryScreen"><h1>THE BEACON IGNITES</h1><p>Aurelion's light returns to the valley.</p><button onclick="location.reload()">Restart</button></div>
</div>
<div id="fade"></div>
</div>

<script>
(function(){
"use strict";
const canvas=document.getElementById('c'), ctx=canvas.getContext('2d');
const VW=960,VH=600;

// ---------- AUDIO ----------
let actx=null;
function ac(){ if(!actx) actx=new (window.AudioContext||window.webkitAudioContext)(); return actx; }
function tone(freq,dur,type='sine',gain=0.15,delay=0,slide=0){
  try{
    const a=ac(); const t0=a.currentTime+delay;
    const osc=a.createOscillator(); const g=a.createGain();
    osc.type=type; osc.frequency.setValueAtTime(freq,t0);
    if(slide) osc.frequency.linearRampToValueAtTime(freq+slide,t0+dur);
    g.gain.setValueAtTime(gain,t0);
    g.gain.exponentialRampToValueAtTime(0.0001,t0+dur);
    osc.connect(g); g.connect(a.destination);
    osc.start(t0); osc.stop(t0+dur+0.02);
  }catch(e){}
}
function noise(dur,gain=0.12,delay=0){
  try{
    const a=ac(); const bufferSize=a.sampleRate*dur;
    const buffer=a.createBuffer(1,bufferSize,a.sampleRate);
    const data=buffer.getChannelData(0);
    for(let i=0;i<bufferSize;i++) data[i]=(Math.random()*2-1)*(1-i/bufferSize);
    const src=a.createBufferSource(); src.buffer=buffer;
    const g=a.createGain(); g.gain.setValueAtTime(gain,a.currentTime+delay);
    src.connect(g); g.connect(a.destination);
    src.start(a.currentTime+delay);
  }catch(e){}
}
const SFX={
  swing(level){ if(level>=2){tone(140,0.28,'sawtooth',0.18);tone(90,0.35,'triangle',0.15,0.02);} else if(level>=1){tone(300,0.14,'sawtooth',0.15);noise(0.1,0.08,0.03);} else {tone(420,0.08,'square',0.12);} },
  hit(){ noise(0.08,0.15); tone(180,0.1,'square',0.12); },
  rangedFire(){ tone(900,0.08,'sine',0.15,0,600); },
  rangedHit(){ tone(1200,0.15,'triangle',0.15); },
  damage(){ tone(90,0.2,'sawtooth',0.2); tone(1500,0.08,'sine',0.08,0.02); },
  pickupCommon(){ tone(700,0.08,'sine',0.12); tone(950,0.1,'sine',0.12,0.08); },
  pickupRare(){ tone(500,0.15,'triangle',0.14); tone(750,0.15,'triangle',0.12,0.05); tone(1000,0.2,'triangle',0.1,0.1); },
  coin(){ tone(1400,0.06,'square',0.1); tone(1800,0.06,'square',0.08,0.04); },
  save(){ tone(440,0.3,'sine',0.12); tone(660,0.4,'sine',0.1,0.15); tone(880,0.5,'sine',0.08,0.3); },
  upgrade(){ tone(300,0.4,'sawtooth',0.15); tone(450,0.4,'triangle',0.15,0.15); tone(600,0.6,'sine',0.12,0.3); },
  breakWall(){ noise(0.25,0.22); tone(120,0.2,'square',0.1); },
  door(){ tone(200,0.4,'sine',0.1,0,-60); },
  plate(){ tone(250,0.12,'square',0.1); },
  purchase(){ tone(600,0.08,'sine',0.1); tone(900,0.12,'sine',0.12,0.08); },
  denied(){ tone(150,0.2,'sawtooth',0.1); },
  footstep(){ tone(90+Math.random()*20,0.05,'square',0.03); }
};

// ---------- STATE ----------
const KEYS={};
window.addEventListener('keydown',e=>{KEYS[e.code]=true; handleKeyOnce(e.code);});
window.addEventListener('keyup',e=>{KEYS[e.code]=false;});

const GAME={
  zoneIndex:0,
  gold:0,
  maxContainers:3,
  hp:6,
  weaponLevel:0,
  hasRanged:false,
  ammo:0,
  inventory:{ band:false, lantern:false, cloak:false },
  equipped:{ band:false, lantern:false, cloak:false },
  consumables:{ vitalStem:0, bindingSeal:0 },
  flags:{ zone3PlateA:0, zone3PlateB:0, zone3DoorOpen:false, zone3WallBroken:false, zone4WallBroken:false, zone4LightPuzzle:false,
    zone5SwitchHit:false, heart5Bought:false, heart5Got:false, savedZone:0, altar1Used:false, altar2Used:false, zone2BossDown:false,
    zone5BossDown:false },
  lastSave:null,
  invuln:0,
  facing:'down',
};

function totalMaxHP(){ return GAME.maxContainers*2; }

// ---------- PARTICLES ----------
let particles=[];
function spawnParticles(x,y,n,opts={}){
  for(let i=0;i<n;i++){
    const ang=Math.random()*Math.PI*2;
    const spd=(opts.speed||60)*(0.5+Math.random());
    particles.push({
      x,y, vx:Math.cos(ang)*spd*(opts.spread!==false?1:0.2), vy:Math.sin(ang)*spd*(opts.spread!==false?1:0.2)-(opts.up||0),
      life:0, maxLife:opts.life||0.6, color:opts.color||'#ff9a3c', size:opts.size||3, grav:opts.grav!==undefined?opts.grav:40
    });
  }
}
function updateParticles(dt){
  for(let i=particles.length-1;i>=0;i--){
    const p=particles[i]; p.life+=dt;
    if(p.life>=p.maxLife){particles.splice(i,1);continue;}
    p.x+=p.vx*dt; p.y+=p.vy*dt; p.vy+=p.grav*dt; p.vx*=0.98;
  }
}
function drawParticles(cam){
  for(const p of particles){
    const a=1-p.life/p.maxLife;
    ctx.globalAlpha=Math.max(0,a);
    ctx.fillStyle=p.color;
    ctx.fillRect(p.x-cam.x-p.size/2,p.y-cam.y-p.size/2,p.size,p.size);
  }
  ctx.globalAlpha=1;
}

// ---------- TOAST / BANNER ----------
function toast(msg,cls=''){
  const box=document.getElementById('toastBox');
  const d=document.createElement('div'); d.className='toast '+cls; d.textContent=msg;
  box.appendChild(d);
  setTimeout(()=>d.remove(),3300);
}
function showZoneBanner(name){
  const b=document.getElementById('zoneBanner');
  b.textContent=name; b.style.opacity=1;
  setTimeout(()=>b.style.opacity=0,2600);
}
let shimmerTimer=0;
function screenShimmer(){ shimmerTimer=2.0; }

// ---------- ENTITIES ----------
class Rect{constructor(x,y,w,h){this.x=x;this.y=y;this.w=w;this.h=h;}
  intersects(o){return this.x<o.x+o.w&&this.x+this.w>o.x&&this.y<o.y+o.h&&this.y+this.h>o.y;}
}

const player={x:120,y:300,w:20,h:26,speed:130,vx:0,vy:0,attackCD:0,attacking:0,rangedCD:0,emberT:0};

function rectsCollide(a,b){return a.x<b.x+b.w&&a.x+a.w>b.x&&a.y<b.y+b.h&&a.y+a.h>b.y;}

// ---------- ZONES ----------
// Each zone: {name,w,h,bg,walls:[Rect],breakables:[{...}],enemies:[],pickups:[],plates:[],saveStone:{},altar:{},shopNpc:{},exit:{to,rect,cond,lockedMsg},entrance:{x,y},decor:[]}
function makeEnemy(x,y,hp,dmg,speed,type='construct',drop=[8,15]){
  return {x,y,w:22,h:26,hp,maxhp:hp,dmg,speed,type,drop,state:'idle',hitCD:0,flinch:0,alive:true,aiT:Math.random()*2};
}

const ZONES=[];

// ZONE 0 - Ashen Threshold
ZONES.push({
  name:"THE ASHEN THRESHOLD", w:1400,h:600, ground:'#2a2420',
  walls:[ new Rect(0,0,1400,40), new Rect(0,560,1400,40), new Rect(0,0,40,600) ],
  breakables:[], plates:[],
  enemies:[ makeEnemy(500,300,6,1,55), makeEnemy(780,420,6,1,55), makeEnemy(1000,200,6,1,55) ],
  pickups:[ {type:'gold',x:640,y:300,amt:6} ],
  saveStone:{x:150,y:300,used:false},
  altar:null, shopNpc:null,
  exit:{to:1,rect:new Rect(1360,0,40,600),cond:()=>true},
  entrance:{x:100,y:300},
});

// ZONE 1 - Rusted Archives
ZONES.push({
  name:"THE RUSTED ARCHIVES", w:1500,h:600, ground:'#2c2a1c',
  walls:[ new Rect(0,0,1500,40), new Rect(0,560,1500,40), new Rect(0,0,40,600), new Rect(300,200,40,260), new Rect(900,60,40,260) ],
  breakables:[ {x:1150,y:420,w:40,h:60,hp:1,maxhp:1,broken:false,reveal:{type:'item',item:'chest_band'}} ],
  plates:[],
  enemies:[ makeEnemy(500,300,6,1,60), makeEnemy(700,150,6,1,60), makeEnemy(1050,480,8,2,65,'construct',[10,18]) ],
  pickups:[ {type:'gold',x:400,y:480,amt:10}, {type:'item',item:'vitalStem',x:650,y:500} ],
  saveStone:{x:760,y:300,used:false},
  altar:null,
  shopNpc:{x:1250,y:150},
  exit:{to:2,rect:new Rect(1460,0,40,600),cond:()=>true},
  entrance:{x:80,y:300},
});

// ZONE 2 - Collapsed Gallery
ZONES.push({
  name:"THE COLLAPSED GALLERY", w:1500,h:640, ground:'#241f1a',
  walls:[ new Rect(0,0,1500,40), new Rect(0,600,1500,40), new Rect(0,0,40,640), new Rect(600,0,40,300), new Rect(600,420,40,220) ],
  breakables:[ {x:980,y:500,w:40,h:60,hp:1,maxhp:1,broken:false,reveal:{type:'heart'}} ],
  plates:[
    {x:300,y:500,w:50,h:50,pressed:false,id:'A'},
    {x:450,y:200,w:50,h:50,pressed:false,id:'B'}
  ],
  doorForPlates:new Rect(600,300,40,120),
  enemies:[ makeEnemy(700,300,10,2,70), makeEnemy(900,450,10,2,70), makeEnemy(1150,200,14,2,75,'construct',[15,25]) ],
  pickups:[ {type:'gold',x:200,y:200,amt:12} ],
  saveStone:null,
  altar:{x:1300,y:300,used:false,type:'ignited'},
  shopNpc:null,
  exit:{to:3,rect:new Rect(1460,0,40,640),cond:()=>GAME.weaponLevel>=1, lockedMsg:"The way forward is sealed by cold iron. Perhaps a stronger flame is needed — find the altar."},
  entrance:{x:80,y:320},
});

// ZONE 3 - Sunken Vault
ZONES.push({
  name:"THE SUNKEN VAULT", w:1500,h:640, ground:'#151d24',
  walls:[ new Rect(0,0,1500,40), new Rect(0,600,1500,40), new Rect(0,0,40,640) ],
  breakables:[ {x:1050,y:300,w:40,h:80,hp:2,maxhp:2,broken:false,reveal:{type:'item',item:'aetherShard'}} ],
  plates:[],
  lightSwitch:{x:1250,y:150,w:24,h:24,hit:false},
  puzzleDoor:new Rect(1300,120,40,120),
  enemies:[ makeEnemy(600,300,10,2,65), makeEnemy(850,450,10,2,65) ],
  pickups:[ {type:'item',item:'phantomLantern',x:400,y:200}, {type:'gold',x:700,y:500,amt:14} ],
  saveStone:{x:150,y:300,used:false},
  altar:null, shopNpc:null,
  exit:{to:4,rect:new Rect(1460,0,40,640),cond:()=>GAME.hasRanged, lockedMsg:"Only the Aether Shard's light can part these deep waters onward."},
  entrance:{x:80,y:320},
});

// ZONE 4 - Twin Spire
ZONES.push({
  name:"THE TWIN SPIRE", w:1600,h:680, ground:'#211a2a',
  walls:[ new Rect(0,0,1600,40), new Rect(0,640,1600,40), new Rect(0,0,40,680) ],
  breakables:[],
  plates:[],
  farSwitch:{x:1500,y:120,w:24,h:24,hit:false},
  enemies:[ makeEnemy(500,300,14,2,75), makeEnemy(750,450,14,2,75), makeEnemy(1000,200,14,2,80),
            makeEnemy(1250,400,20,3,85,'construct',[20,30]) ],
  pickups:[ {type:'item',item:'shadowCloak',x:300,y:500}, {type:'item',item:'bindingSeal',x:600,y:150},
            {type:'gold',x:900,y:550,amt:40} ],
  saveStone:{x:150,y:340,used:false},
  altar:{x:1400,y:560,used:false,type:'solar'},
  shopNpc:null,
  exit:{to:5,rect:new Rect(1560,0,40,680),cond:()=>GAME.weaponLevel>=2, lockedMsg:"The Core Chamber demands the Solar Falchion's light. The altar awaits, guarded still."},
  entrance:{x:80,y:340},
});

// ZONE 5 - Core Chamber
ZONES.push({
  name:"THE CORE CHAMBER", w:1200,h:640, ground:'#1c1410',
  walls:[ new Rect(0,0,1200,40), new Rect(0,600,1200,40), new Rect(0,0,40,640), new Rect(1160,0,40,640) ],
  breakables:[], plates:[],
  boss:makeEnemy(900,320,60,4,60,'boss',[80,120]),
  enemies:[],
  pickups:[],
  saveStone:null, altar:null, shopNpc:null,
  beacon:{x:1000,y:280,w:60,h:90,lit:false},
  exit:null,
  entrance:{x:80,y:320},
});
ZONES[5].beacon.y=280;

let cam={x:0,y:0};
function currentZone(){return ZONES[GAME.zoneIndex];}

function resetPlayerToEntrance(){
  const z=currentZone();
  player.x=z.entrance.x; player.y=z.entrance.y;
}

// ---------- ITEM DEFS ----------
const ITEM_NAMES={
  vitalStem:"Vital Stem", bindingSeal:"Binding Seal", wardenBand:"Warden's Band",
  phantomLantern:"Phantom Lantern", shadowCloak:"Shadow Cloak", aetherShard:"Aether Shard"
};

function grantItem(item){
  if(item==='vitalStem'){ GAME.consumables.vitalStem++; toast("+1 Vital Stem",'rare'); SFX.pickupRare(); }
  else if(item==='bindingSeal'){ GAME.consumables.bindingSeal++; toast("+1 Binding Seal",'rare'); SFX.pickupRare(); }
  else if(item==='chest_band' || item==='wardenBand'){ GAME.inventory.band=true; GAME.equipped.band=true; toast("Warden's Band acquired — attack speed increased",'rare'); SFX.pickupRare(); }
  else if(item==='phantomLantern'){ GAME.inventory.lantern=true; GAME.equipped.lantern=true; toast("Phantom Lantern acquired — hidden paths revealed",'rare'); SFX.pickupRare(); }
  else if(item==='shadowCloak'){ GAME.inventory.cloak=true; GAME.equipped.cloak=true; toast("Shadow Cloak acquired — trap damage reduced",'rare'); SFX.pickupRare(); }
  else if(item==='aetherShard'){ GAME.hasRanged=true; GAME.ammo=20; toast("THE AETHER SHARD — ranged attacks unlocked! Press F",'rare'); SFX.pickupRare(); screenShimmer(); }
}

function gainHeartContainer(){
  GAME.maxContainers=Math.min(5,GAME.maxContainers+1);
  GAME.hp=totalMaxHP();
  screenShimmer();
  spawnParticles(player.x,player.y,26,{color:'#ffdf8a',life:1.2,speed:40,up:60,grav:-10});
  toast("Health Container acquired! Max HP increased",'rare');
  SFX.pickupRare();
}

// ---------- SAVE / LOAD ----------
function saveGame(){
  const data={GAME:JSON.parse(JSON.stringify(GAME)),px:player.x,py:player.y};
  data.GAME.flags.savedZone=GAME.zoneIndex;
  localStorage.setItem('wardenCorrosionSave',JSON.stringify(data));
  GAME.lastSave={zone:GAME.zoneIndex,x:player.x,y:player.y};
  toast("Progress saved at the Sanctuary Stone",'rare');
  SFX.save(); screenShimmer();
}
function respawnAtSave(){
  if(GAME.lastSave){
    GAME.zoneIndex=GAME.lastSave.zone;
    player.x=GAME.lastSave.x; player.y=GAME.lastSave.y;
  } else { GAME.zoneIndex=0; resetPlayerToEntrance(); }
  GAME.hp=totalMaxHP();
  document.getElementById('deathScreen').style.display='none';
}

// ---------- KEY ONE-SHOT HANDLING ----------
function handleKeyOnce(code){
  if(code==='KeyE') tryInteract();
  if(code==='KeyI') toggleInventory();
  if(code==='Digit1') useVitalStem();
  if(code==='Digit2') useBindingSeal();
  if(code==='Space') meleeAttack();
  if(code==='KeyF') rangedAttack();
}

function useVitalStem(){
  if(GAME.consumables.vitalStem>0 && GAME.hp<totalMaxHP()){
    GAME.consumables.vitalStem--; GAME.hp=Math.min(totalMaxHP(),GAME.hp+4);
    toast("Vital Stem restores 4 HP"); SFX.pickupCommon();
  } else if(GAME.consumables.vitalStem<=0) toast("No Vital Stems remaining");
}
let freezeTimer=0, frozenEnemy=null;
function useBindingSeal(){
  if(GAME.consumables.bindingSeal>0){
    const z=currentZone(); const list=z.enemies.concat(z.boss?[z.boss]:[]);
    let nearest=null,best=99999;
    for(const en of list){ if(!en.alive)continue; const d=Math.hypot(en.x-player.x,en.y-player.y); if(d<best){best=d;nearest=en;} }
    if(nearest && best<220){
      GAME.consumables.bindingSeal--; nearest.frozen=3.0; toast("Binding Seal — enemy frozen!"); SFX.pickupRare();
    } else toast("No enemy nearby to bind");
  } else toast("No Binding Seals remaining");
}

// ---------- INTERACTION ----------
let nearInteract=null;
function tryInteract(){
  if(!nearInteract) return;
  if(nearInteract.type==='save'){ saveGame(); nearInteract.obj.used=true; }
  else if(nearInteract.type==='shop'){ openShop(); }
  else if(nearInteract.type==='altar'){ activateAltar(nearInteract.obj); }
  else if(nearInteract.type==='pickup'){ collectPickup(nearInteract.obj); }
  else if(nearInteract.type==='chest'){ breakChestDirect(nearInteract.obj); }
}

function activateAltar(altar){
  if(altar.used){ toast("The altar's power has already been claimed."); return; }
  if(altar.type==='ignited'){
    if(GAME.weaponLevel<1){ GAME.weaponLevel=1; altar.used=true; toast("IGNITED EDGE forged — the Ember Blade burns anew",'rare'); SFX.upgrade(); screenShimmer(); }
  } else if(altar.type==='solar'){
    if(GAME.weaponLevel<2){ GAME.weaponLevel=2; altar.used=true; toast("SOLAR FALCHION awakened — golden light fills the blade",'rare'); SFX.upgrade(); screenShimmer(); }
  }
}

function collectPickup(p){
  const z=currentZone();
  if(p.type==='gold'){ GAME.gold+=p.amt; toast("+"+p.amt+" gold"); SFX.coin(); }
  else if(p.type==='item'){ grantItem(p.item); }
  p.collected=true;
  z.pickups=z.pickups.filter(x=>x!==p);
}

// ---------- SHOP ----------
function openShop(){
  document.getElementById('shopModal').style.display='block';
  renderShop();
}
function renderShop(){
  const el=document.getElementById('shopList'); el.innerHTML='';
  const entries=[
    {key:'vitalStem',label:"Vital Stem (restore 4 HP)",cost:50,max:3,owned:()=>GAME.shopBought?.vitalStem||0},
    {key:'bindingSeal',label:"Binding Seal (freeze enemy)",cost:75,max:2,owned:()=>GAME.shopBought?.bindingSeal||0},
    {key:'heart5',label:"Heart Container Upgrade #5",cost:300,max:1,owned:()=>GAME.flags.heart5Bought?1:0},
    {key:'ammo',label:"Aether Shard Ammo x5 (needs Zone 4 cleared)",cost:10,max:999,owned:()=>0,locked:()=>!GAME.hasRanged}
  ];
  GAME.shopBought=GAME.shopBought||{vitalStem:0,bindingSeal:0};
  for(const e of entries){
    const ownedCount=e.owned();
    const soldOut=ownedCount>=e.max;
    const locked=e.locked&&e.locked();
    const row=document.createElement('div'); row.className='row';
    row.innerHTML=`<span>${e.label} — <b>${e.cost}g</b></span>`;
    const btn=document.createElement('button');
    btn.textContent=locked?"Locked":(soldOut?"Sold Out":"Buy");
    btn.disabled=soldOut||locked;
    btn.onclick=()=>buyShopItem(e);
    row.appendChild(btn);
    el.appendChild(row);
  }
}
function buyShopItem(e){
  if(GAME.gold<e.cost){ toast("Not enough gold"); SFX.denied(); return; }
  GAME.gold-=e.cost;
  if(e.key==='vitalStem'){ GAME.consumables.vitalStem++; GAME.shopBought.vitalStem++; }
  else if(e.key==='bindingSeal'){ GAME.consumables.bindingSeal++; GAME.shopBought.bindingSeal++; }
  else if(e.key==='heart5'){ GAME.flags.heart5Bought=true; gainHeartContainer(); }
  else if(e.key==='ammo'){ GAME.ammo+=5; }
  SFX.purchase(); toast("Purchased "+e.label);
  renderShop(); updateHUD();
}
document.getElementById('shopClose').onclick=()=>document.getElementById('shopModal').style.display='none';

// ---------- INVENTORY ----------
function toggleInventory(){
  const m=document.getElementById('invModal');
  if(m.style.display==='block'){ m.style.display='none'; return; }
  renderInventory(); m.style.display='block';
}
function renderInventory(){
  const el=document.getElementById('invList'); el.innerHTML='';
  const accessories=[
    {key:'band',label:"Warden's Band (attack speed)"},
    {key:'lantern',label:"Phantom Lantern (reveal hidden paths)"},
    {key:'cloak',label:"Shadow Cloak (reduce trap damage)"}
  ];
  for(const a of accessories){
    if(!GAME.inventory[a.key]) continue;
    const row=document.createElement('div'); row.className='row';
    row.innerHTML=`<span>${a.label} ${GAME.equipped[a.key]?'<b style="color:#ffd75e">[Equipped]</b>':''}</span>`;
    const btn=document.createElement('button'); btn.textContent=GAME.equipped[a.key]?"Unequip":"Equip";
    btn.onclick=()=>{ GAME.equipped[a.key]=!GAME.equipped[a.key]; renderInventory(); };
    row.appendChild(btn); el.appendChild(row);
  }
  const cRow=document.createElement('div'); cRow.className='row';
  cRow.innerHTML=`<span>Vital Stem x${GAME.consumables.vitalStem} &nbsp;&nbsp; Binding Seal x${GAME.consumables.bindingSeal}</span>`;
  el.appendChild(cRow);
  const wRow=document.createElement('div'); wRow.className='row';
  const wname=GAME.weaponLevel===0?"Ember Blade":GAME.weaponLevel===1?"Ignited Edge":"Solar Falchion";
  wRow.innerHTML=`<span>Weapon: <b>${wname}</b>${GAME.hasRanged?' + Aether Shard ('+GAME.ammo+' charges)':''}</span>`;
  el.appendChild(wRow);
  if(!accessories.some(a=>GAME.inventory[a.key]) ){
    const e=document.createElement('div'); e.className='row'; e.innerHTML='<span style="color:#877">No accessories found yet.</span>'; el.appendChild(e);
  }
}
document.getElementById('invClose').onclick=()=>document.getElementById('invModal').style.display='none';

// ---------- COMBAT ----------
function meleeAttack(){
  if(player.attackCD>0) return;
  const lvl=GAME.weaponLevel;
  const speedFactor = GAME.equipped.band?0.82:1;
  const cooldowns=[0.5,0.32,0.62];
  player.attackCD=cooldowns[lvl]*speedFactor;
  player.attacking=0.22+(lvl===2?0.12:0);
  SFX.swing(lvl);
  const range= lvl===2?46:34;
  const dmgs=[2,3,4];
  const dmg=dmgs[lvl];
  const hb=attackHitbox(range);
  const z=currentZone();
  const list=z.enemies.concat(z.boss?[z.boss]:[]);
  let hitSomething=false;
  for(const en of list){
    if(!en.alive) continue;
    const er=new Rect(en.x-en.w/2,en.y-en.h/2,en.w,en.h);
    if(rectsCollide(hb,er)){
      damageEnemy(en,dmg,lvl);
      hitSomething=true;
    }
  }
  // breakables
  for(const b of z.breakables){
    if(b.broken) continue;
    const br=new Rect(b.x,b.y,b.w,b.h);
    if(rectsCollide(hb,br)){
      const dmgToWall = lvl===2?2:(lvl===1?1:0.5);
      b.hp-=dmgToWall;
      spawnParticles(b.x+b.w/2,b.y+b.h/2,6,{color:'#a08050',life:0.4,speed:50});
      if(b.hp<=0){ breakWall(b); } else { toast("The wall cracks further...",''); }
      hitSomething=true;
    }
  }
  if(!hitSomething){} // whiff, no extra sfx
  if(lvl>=1){ spawnParticles(hb.x+hb.w/2,hb.y+hb.h/2,8,{color:lvl===2?'#ffe08a':'#ff8a3c',life:0.4,speed:70}); }
}
function attackHitbox(range){
  const cx=player.x, cy=player.y;
  if(GAME.facing==='up') return new Rect(cx-14,cy-range,28,range);
  if(GAME.facing==='down') return new Rect(cx-14,cy,28,range);
  if(GAME.facing==='left') return new Rect(cx-range,cy-14,range,28);
  return new Rect(cx,cy-14,range,28);
}
function breakWall(b){
  b.broken=true; SFX.breakWall();
  spawnParticles(b.x+b.w/2,b.y+b.h/2,18,{color:'#8a6a3a',life:0.6,speed:90});
  if(b.reveal){
    if(b.reveal.type==='item'){ currentZone().pickups.push({type:'item',item:b.reveal.item,x:b.x+b.w/2,y:b.y+b.h/2}); toast("A hidden item is revealed!"); }
    else if(b.reveal.type==='heart'){ currentZone().pickups.push({type:'heartContainer',x:b.x+b.w/2,y:b.y+b.h/2}); toast("A health container gleams within!"); }
  }
}
function rangedAttack(){
  if(!GAME.hasRanged) return;
  if(GAME.ammo<=0){ toast("No Aether Shard charges left"); SFX.denied(); return; }
  if(player.rangedCD>0) return;
  player.rangedCD=0.5;
  GAME.ammo--;
  SFX.rangedFire();
  const dir={up:[0,-1],down:[0,1],left:[-1,0],right:[1,0]}[GAME.facing];
  projectiles.push({x:player.x,y:player.y,vx:dir[0]*420,vy:dir[1]*420,life:1.2,dmg:3});
}
let projectiles=[];
function updateProjectiles(dt){
  const z=currentZone();
  for(let i=projectiles.length-1;i>=0;i--){
    const p=projectiles[i]; p.life-=dt; p.x+=p.vx*dt; p.y+=p.vy*dt;
    let removed=false;
    if(p.life<=0) removed=true;
    for(const w of z.walls){ if(p.x>w.x&&p.x<w.x+w.w&&p.y>w.y&&p.y<w.y+w.h){ removed=true; break; } }
    const list=z.enemies.concat(z.boss?[z.boss]:[]);
    for(const en of list){ if(!en.alive) continue; if(Math.hypot(en.x-p.x,en.y-p.y)<20){ damageEnemy(en,p.dmg,3); removed=true; SFX.rangedHit(); break; } }
    if(z.lightSwitch && !z.lightSwitch.hit && Math.hypot(z.lightSwitch.x-p.x,z.lightSwitch.y-p.y)<20){
      z.lightSwitch.hit=true; SFX.plate(); toast("The crystal switch resonates — a path opens!"); removed=true;
      screenShimmer();
    }
    if(z.farSwitch && !z.farSwitch.hit && Math.hypot(z.farSwitch.x-p.x,z.farSwitch.y-p.y)<20){
      z.farSwitch.hit=true; SFX.plate(); if(!GAME.flags.heart5Got){ GAME.flags.heart5Got=true; gainHeartContainer(); }
      removed=true;
    }
    for(const b of z.breakables){ if(!b.broken && p.x>b.x&&p.x<b.x+b.w&&p.y>b.y&&p.y<b.y+b.h){ b.hp-=1.5; removed=true; if(b.hp<=0) breakWall(b); } }
    if(removed) projectiles.splice(i,1);
  }
}
function damageEnemy(en,dmg,weaponLvl){
  if(en.hitCD>0) return;
  en.hp-=dmg; en.hitCD=0.15; en.flinch=0.25;
  const critColor = weaponLvl>=2?'#7ecbff':'#ff9a3c';
  spawnParticles(en.x,en.y,10,{color:critColor,life:0.4,speed:90});
  SFX.hit();
  if(weaponLvl===1) spawnParticles(en.x,en.y,6,{color:'#553','life':0.5,speed:20,grav:-20});
  if(en.hp<=0 && en.alive){
    en.alive=false;
    const g=Math.floor(en.drop[0]+Math.random()*(en.drop[1]-en.drop[0]));
    currentZone().pickups.push({type:'gold',x:en.x,y:en.y,amt:g});
    spawnParticles(en.x,en.y,22,{color:'#c8a040',life:0.8,speed:110});
    if(en.type==='boss'){ onBossDown(); }
  }
}
function onBossDown(){
  const z=currentZone();
  if(GAME.zoneIndex===5){
    toast("The Corrupted Warden falls... the Beacon stirs.",'rare');
    z.beacon.lit=true; screenShimmer();
  }
}

// ---------- BREAKABLE DIRECT ----------
function breakChestDirect(obj){ /* unused placeholder */ }

// ---------- UPDATE ----------
let lastT=performance.now();
let stepTimer=0;
function update(dt){
  if(GAME.invuln>0) GAME.invuln-=dt;
  if(player.attackCD>0) player.attackCD-=dt;
  if(player.rangedCD>0) player.rangedCD-=dt;
  if(player.attacking>0) player.attacking-=dt;
  if(shimmerTimer>0) shimmerTimer-=dt;

  const z=currentZone();
  let mvx=0,mvy=0;
  if(KEYS['KeyW']||KEYS['ArrowUp']){mvy=-1;GAME.facing='up';}
  if(KEYS['KeyS']||KEYS['ArrowDown']){mvy=1;GAME.facing='down';}
  if(KEYS['KeyA']||KEYS['ArrowLeft']){mvx=-1;GAME.facing='left';}
  if(KEYS['KeyD']||KEYS['ArrowRight']){mvx=1;GAME.facing='right';}
  const len=Math.hypot(mvx,mvy)||1;
  const moving=(mvx!==0||mvy!==0);
  const nx=player.x+ (mvx/len)*player.speed*dt;
  const ny=player.y+ (mvy/len)*player.speed*dt;
  const pr=new Rect(nx-player.w/2,player.y-player.h/2,player.w,player.h);
  let blockedX=false;
  for(const w of z.walls) if(rectsCollide(pr,w)) blockedX=true;
  for(const b of z.breakables) if(!b.broken && rectsCollide(pr,new Rect(b.x,b.y,b.w,b.h))) blockedX=true;
  if(z.doorForPlates && !z.flags_doorOpenCache && rectsCollide(pr,z.doorForPlates)) blockedX=true;
  if(!blockedX) player.x=nx;
  const pr2=new Rect(player.x-player.w/2,ny-player.h/2,player.w,player.h);
  let blockedY=false;
  for(const w of z.walls) if(rectsCollide(pr2,w)) blockedY=true;
  for(const b of z.breakables) if(!b.broken && rectsCollide(pr2,new Rect(b.x,b.y,b.w,b.h))) blockedY=true;
  if(z.doorForPlates && rectsCollide(pr2,z.doorForPlates) && !isPlateDoorOpen(z)) blockedY=true;
  if(!blockedY) player.y=ny;
  player.x=Math.max(20,Math.min(z.w-20,player.x));
  player.y=Math.max(20,Math.min(z.h-20,player.y));

  if(moving){ stepTimer-=dt; if(stepTimer<=0){ SFX.footstep(); stepTimer=0.26; } if(GAME.weaponLevel>=1){ player.emberT-=dt; if(player.emberT<=0){ spawnParticles(player.x,player.y+8,1,{color:'#ff8a3c',life:0.5,speed:10,grav:-5,size:2}); player.emberT=0.08; } } }

  // plates
  if(z.plates){
    for(const pl of z.plates){
      const wasPressed=pl.pressed;
      pl.pressed = rectsCollide(new Rect(player.x-10,player.y-10,20,20),pl) || false;
      if(pl.pressed && !wasPressed){ SFX.plate(); if(pl.id==='A') GAME.flags.zone3PlateA=performance.now(); if(pl.id==='B') GAME.flags.zone3PlateB=performance.now(); }
    }
  }

  // enemies AI
  const list=z.enemies.concat(z.boss?[z.boss]:[]);
  for(const en of list){
    if(!en.alive) continue;
    if(en.hitCD>0) en.hitCD-=dt;
    if(en.frozen>0){ en.frozen-=dt; continue; }
    const d=Math.hypot(player.x-en.x,player.y-en.y);
    if(d<260){
      const ang=Math.atan2(player.y-en.y,player.x-en.x);
      const sp=en.flinch>0?en.speed*0.2:en.speed;
      if(en.flinch>0) en.flinch-=dt;
      if(d>34){ en.x+=Math.cos(ang)*sp*dt; en.y+=Math.sin(ang)*sp*dt; }
      else {
        en.aiT-=dt;
        if(en.aiT<=0){ en.aiT=1.0; tryDamagePlayer(en.dmg); }
      }
    }
  }

  updateProjectiles(dt);
  updateParticles(dt);

  // pickups collision
  for(const p of z.pickups.slice()){
    if(Math.hypot(p.x-player.x,p.y-player.y)<26){
      if(p.type==='heartContainer'){ gainHeartContainer(); z.pickups=z.pickups.filter(x=>x!==p); }
      else collectPickup(p);
    }
  }

  // interact detection
  nearInteract=null;
  if(z.saveStone && !z.saveStone.used && Math.hypot(z.saveStone.x-player.x,z.saveStone.y-player.y)<40) nearInteract={type:'save',obj:z.saveStone};
  if(z.shopNpc && Math.hypot(z.shopNpc.x-player.x,z.shopNpc.y-player.y)<50) nearInteract={type:'shop',obj:z.shopNpc};
  if(z.altar && !z.altar.used && Math.hypot(z.altar.x-player.x,z.altar.y-player.y)<50) nearInteract={type:'altar',obj:z.altar};

  updateInteractPrompt();

  // pressure-plate door (zone2 collapsed gallery)
  if(z.doorForPlates){
    const open=isPlateDoorOpen(z);
    z.flags_doorOpenCache=open;
  }

  // zone exit
  if(z.exit){
    const pr3=new Rect(player.x-10,player.y-10,20,20);
    if(rectsCollide(pr3,z.exit.rect)){
      if(z.exit.cond()){
        goToZone(z.exit.to);
      } else if(z.exit.lockedMsg && Math.random()<0.02){
        toast(z.exit.lockedMsg); SFX.denied();
      }
    }
  }

  // beacon interact (zone5)
  if(z.beacon && z.beacon.lit && Math.hypot(z.beacon.x+z.beacon.w/2-player.x,z.beacon.y+z.beacon.h/2-player.y)<60){
    if(KEYS['KeyE']) winGame();
  }

  // camera
  cam.x=Math.max(0,Math.min(z.w-VW,player.x-VW/2));
  cam.y=Math.max(0,Math.min(z.h-VH,player.y-VH/2));

  if(GAME.hp<=0) onDeath();
  updateHUD();
}
function isPlateDoorOpen(z){
  if(!z.plates) return true;
  const allPressed=z.plates.every(p=>p.pressed);
  return allPressed;
}
function tryDamagePlayer(dmg){
  if(GAME.invuln>0) return;
  let d=dmg;
  if(GAME.equipped.cloak) d=Math.max(1,Math.round(d*0.6));
  GAME.hp-=d*2; // dmg in "containers worth" -> scale to hp units
  GAME.invuln=0.9;
  SFX.damage();
  spawnParticles(player.x,player.y,16,{color:'#ff3030',life:0.5,speed:80});
  toast("-"+ (d*2) +" HP");
}
function onDeath(){
  document.getElementById('deathScreen').style.display='flex';
}
document.getElementById('respawnBtn').onclick=respawnAtSave;
let won=false;
function winGame(){
  if(won) return; won=true;
  document.getElementById('victoryScreen').style.display='flex';
}

function goToZone(idx){
  GAME.zoneIndex=idx;
  const z=currentZone();
  player.x=z.entrance.x; player.y=z.entrance.y;
  showZoneBanner(z.name);
}

// ---------- HUD ----------
function heartSVG(fillFrac){
  const c = fillFrac>=1?'#c0302a':(fillFrac>0?'#7a1f1a':'#2a1414');
  return `<svg viewBox="0 0 32 28"><path d="M16 26 C16 26 2 16 2 8 C2 2 8 0 11 3 C13 5 16 8 16 8 C16 8 19 5 21 3 C24 0 30 2 30 8 C30 16 16 26 16 26 Z" fill="${c}" stroke="#000" stroke-width="1.5"/></svg>`;
}
function updateHUD(){
  const hEl=document.getElementById('hearts'); hEl.innerHTML='';
  for(let i=0;i<GAME.maxContainers;i++){
    const containerHP=Math.max(0,Math.min(2,GAME.hp-i*2));
    const frac=containerHP/2;
    const div=document.createElement('div'); div.className='heart'; div.innerHTML=heartSVG(frac);
    hEl.appendChild(div);
  }
  document.getElementById('goldAmt').textContent=GAME.gold;
  const wname=GAME.weaponLevel===0?"Ember Blade":GAME.weaponLevel===1?"Ignited Edge":"Solar Falchion";
  document.getElementById('weaponInfo').textContent = wname + (GAME.hasRanged?" | Aether Shard x"+GAME.ammo:"");
}
function updateInteractPrompt(){
  const el=document.getElementById('interactPrompt');
  if(nearInteract){
    let msg = nearInteract.type==='save'?"Press E to rest at the Sanctuary Stone":
      nearInteract.type==='shop'?"Press E to trade with the Merchant Altar":
      nearInteract.type==='altar'?"Press E to channel the altar's power":"";
    el.textContent=msg; el.style.display='block';
  } else el.style.display='none';
}

// ---------- RENDER ----------
function drawEnvironment(z){
  ctx.fillStyle=z.ground; ctx.fillRect(0,0,VW,VH);
  // ambient decoration: gears / dust based on zone index
  ctx.save();
  ctx.translate(-cam.x,-cam.y);
  // vignette floor pattern
  ctx.strokeStyle='rgba(255,255,255,0.03)';
  for(let gx=0;gx<z.w;gx+=60){ ctx.beginPath(); ctx.moveTo(gx,0); ctx.lineTo(gx,z.h); ctx.stroke(); }
  for(let gy=0;gy<z.h;gy+=60){ ctx.beginPath(); ctx.moveTo(0,gy); ctx.lineTo(z.w,gy); ctx.stroke(); }

  for(const w of z.walls){
    const grad=ctx.createLinearGradient(w.x,w.y,w.x,w.y+w.h);
    grad.addColorStop(0,'#4a3a20'); grad.addColorStop(1,'#241a0e');
    ctx.fillStyle=grad; ctx.fillRect(w.x,w.y,w.w,w.h);
    ctx.strokeStyle='#6b5327'; ctx.strokeRect(w.x,w.y,w.w,w.h);
  }
  for(const b of z.breakables){
    if(b.broken) continue;
    ctx.fillStyle='#3a2e1c'; ctx.fillRect(b.x,b.y,b.w,b.h);
    ctx.strokeStyle='#1a1408'; ctx.strokeRect(b.x,b.y,b.w,b.h);
    const dmgFrac=1-(b.hp/b.maxhp);
    ctx.strokeStyle='rgba(20,10,5,'+(0.4+dmgFrac*0.6)+')'; ctx.lineWidth=2;
    ctx.beginPath(); ctx.moveTo(b.x+4,b.y+4); ctx.lineTo(b.x+b.w-6,b.y+b.h-8); ctx.moveTo(b.x+b.w-4,b.y+6); ctx.lineTo(b.x+6,b.y+b.h-6); ctx.stroke();
    ctx.lineWidth=1;
  }
  if(z.plates){
    for(const pl of z.plates){
      ctx.fillStyle=pl.pressed?'#8a6a2a':'#5a4a24';
      ctx.fillRect(pl.x,pl.y,pl.w,pl.h);
      ctx.strokeStyle='#c9a24a'; ctx.strokeRect(pl.x,pl.y,pl.w,pl.h);
    }
  }
  if(z.doorForPlates){
    const open=isPlateDoorOpen(z);
    if(!open){ ctx.fillStyle='#6b3a2a'; ctx.fillRect(z.doorForPlates.x,z.doorForPlates.y,z.doorForPlates.w,z.doorForPlates.h); }
  }
  if(z.lightSwitch){
    ctx.fillStyle=z.lightSwitch.hit?'#7ecbff':'#345'; ctx.beginPath(); ctx.arc(z.lightSwitch.x,z.lightSwitch.y,12,0,7); ctx.fill();
    ctx.strokeStyle='#bcf'; ctx.stroke();
    if(z.puzzleDoor && !z.lightSwitch.hit){ ctx.fillStyle='#403050'; ctx.fillRect(z.puzzleDoor.x,z.puzzleDoor.y,z.puzzleDoor.w,z.puzzleDoor.h); }
  }
  if(z.farSwitch){
    ctx.fillStyle=z.farSwitch.hit?'#7ecbff':'#345'; ctx.beginPath(); ctx.arc(z.farSwitch.x,z.farSwitch.y,12,0,7); ctx.fill();
    ctx.strokeStyle='#bcf'; ctx.stroke();
  }
  if(z.saveStone){
    const s=z.saveStone;
    const glow=s.used?0.4:0.9;
    ctx.save(); ctx.globalAlpha=glow*(0.7+0.3*Math.sin(performance.now()/300));
    ctx.fillStyle='#e0a838'; ctx.beginPath(); ctx.arc(s.x,s.y,10,0,7); ctx.fill();
    ctx.restore();
    ctx.fillStyle='#8a6a2a'; ctx.fillRect(s.x-14,s.y+6,28,14);
  }
  if(z.altar){
    const a=z.altar;
    ctx.fillStyle='#5a4428'; ctx.fillRect(a.x-22,a.y+10,44,20);
    ctx.save();
    const col=a.type==='ignited'?'#ff7a2e':'#ffd75e';
    ctx.globalAlpha=a.used?0.25:0.9+0.1*Math.sin(performance.now()/200);
    ctx.fillStyle=col; ctx.beginPath(); ctx.arc(a.x,a.y,14,0,7); ctx.fill();
    ctx.restore();
  }
  if(z.shopNpc){
    ctx.fillStyle='#7a5a2e'; ctx.fillRect(z.shopNpc.x-16,z.shopNpc.y-24,32,48);
    ctx.fillStyle='#e0c890'; ctx.beginPath(); ctx.arc(z.shopNpc.x,z.shopNpc.y-30,10,0,7); ctx.fill();
  }
  if(z.beacon){
    const bc=z.beacon;
    ctx.save();
    ctx.fillStyle=bc.lit?'#ffe089':'#334';
    ctx.globalAlpha=bc.lit?0.85+0.15*Math.sin(performance.now()/150):0.6;
    ctx.fillRect(bc.x,bc.y,bc.w,bc.h);
    ctx.restore();
    ctx.strokeStyle='#c9a24a'; ctx.strokeRect(bc.x,bc.y,bc.w,bc.h);
  }
  for(const p of z.pickups){
    if(p.type==='gold'){ ctx.fillStyle='#e0c04a'; ctx.beginPath(); ctx.arc(p.x,p.y,6,0,7); ctx.fill(); ctx.strokeStyle='#8a6a1a'; ctx.stroke(); }
    else if(p.type==='heartContainer'){ ctx.save(); ctx.translate(p.x-10,p.y-10); ctx.scale(0.7,0.7); ctx.fillStyle='#c0302a'; ctx.beginPath(); ctx.arc(10,10,10,0,7); ctx.fill(); ctx.restore(); }
    else { ctx.save(); ctx.globalAlpha=0.85+0.15*Math.sin(performance.now()/200+p.x);
      ctx.fillStyle = p.item==='vitalStem'?'#5ad16a': p.item==='bindingSeal'?'#4ab0ff' : p.item==='aetherShard'?'#7ecbff':'#e8d9a0';
      ctx.beginPath(); ctx.arc(p.x,p.y,8,0,7); ctx.fill(); ctx.restore();
    }
  }
  const list=z.enemies.concat(z.boss?[z.boss]:[]);
  for(const en of list){
    if(!en.alive) continue;
    ctx.save();
    if(en.frozen>0){ ctx.globalAlpha=0.6; ctx.fillStyle='#7ecbff'; ctx.fillRect(en.x-en.w/2-2,en.y-en.h/2-2,en.w+4,en.h+4); ctx.globalAlpha=1; }
    ctx.fillStyle= en.type==='boss'?'#8a2a2a':'#5a4a3a';
    const scale= en.type==='boss'?1.8:1;
    ctx.fillRect(en.x-en.w/2*scale,en.y-en.h/2*scale,en.w*scale,en.h*scale);
    ctx.strokeStyle= en.flinch>0?'#ff7a2e':'#2a2015'; ctx.strokeRect(en.x-en.w/2*scale,en.y-en.h/2*scale,en.w*scale,en.h*scale);
    // hp bar
    ctx.fillStyle='#000'; ctx.fillRect(en.x-16,en.y-en.h/2*scale-10,32,5);
    ctx.fillStyle='#c0302a'; ctx.fillRect(en.x-16,en.y-en.h/2*scale-10,32*Math.max(0,en.hp/en.maxhp),5);
    ctx.restore();
  }
  // player draw
  drawPlayer();
  drawParticles(cam);
  ctx.restore();
}
function drawPlayer(){
  const x=player.x-cam.x, y=player.y-cam.y;
  ctx.save();
  ctx.translate(x,y);
  // cloak
  ctx.fillStyle = GAME.equipped.cloak?'#241826':'#2e2a3a';
  ctx.beginPath(); ctx.ellipse(0,4,13,16,0,0,7); ctx.fill();
  if(GAME.equipped.cloak){ ctx.strokeStyle='#5a3a6a'; ctx.stroke(); }
  // body
  ctx.fillStyle='#d8c39a';
  ctx.fillRect(-7,-14,14,20);
  // blade
  let bladeColor='#9aa0a8', bladeGlow=null;
  if(GAME.weaponLevel===1){ bladeColor='#ff7a2e'; bladeGlow='rgba(255,120,40,0.6)'; }
  if(GAME.weaponLevel===2){ bladeColor='#ffd75e'; bladeGlow='rgba(255,220,100,0.7)'; }
  let bx=10,by=0,bw=6,bh= GAME.weaponLevel===2?32:24;
  if(GAME.facing==='left'){bx=-10-bw;}
  if(GAME.facing==='up'){bx=-3;by=-22;bw=GAME.weaponLevel===2?32:24;bh=6;}
  if(GAME.facing==='down'){bx=-3;by=16;bw=GAME.weaponLevel===2?32:24;bh=6;}
  if(bladeGlow){ ctx.save(); ctx.shadowColor=bladeGlow; ctx.shadowBlur=14; ctx.fillStyle=bladeColor; ctx.fillRect(bx,by-bh/2,bw,bh); ctx.restore(); }
  else { ctx.fillStyle=bladeColor; ctx.fillRect(bx,by-bh/2,bw,bh); }
  if(player.attacking>0){
    ctx.save(); ctx.globalAlpha=0.5; ctx.strokeStyle=bladeColor; ctx.lineWidth=3;
    ctx.beginPath(); ctx.arc(0,0, GAME.weaponLevel===2?44:32, -0.8,0.8); ctx.stroke(); ctx.restore();
  }
  // off-hand crystal for ranged
  if(GAME.hasRanged){
    ctx.save(); ctx.shadowColor='rgba(120,200,255,0.8)'; ctx.shadowBlur=10+4*Math.sin(performance.now()/150);
    ctx.fillStyle='#7ecbff'; ctx.beginPath(); ctx.arc(-11,-2,4,0,7); ctx.fill(); ctx.restore();
  }
  // accessory tokens
  if(GAME.equipped.band){ ctx.strokeStyle='#dde'; ctx.lineWidth=2; ctx.beginPath(); ctx.arc(-7,6,3,0,7); ctx.stroke(); ctx.lineWidth=1; }
  if(GAME.equipped.lantern){ ctx.save(); ctx.shadowColor='rgba(90,160,255,0.9)'; ctx.shadowBlur=12; ctx.fillStyle='#8ac6ff'; ctx.beginPath(); ctx.arc(7,10,3,0,7); ctx.fill(); ctx.restore(); }
  // head
  ctx.fillStyle='#caa876'; ctx.beginPath(); ctx.arc(0,-18,7,0,7); ctx.fill();
  ctx.restore();
  // damage flash
  if(GAME.invuln>0.6){
    ctx.save(); ctx.globalAlpha=0.35; ctx.fillStyle='#ff2020';
    ctx.beginPath(); ctx.arc(x,y,26,0,7); ctx.fill(); ctx.restore();
  }
}

function render(){
  const z=currentZone();
  ctx.clearRect(0,0,VW,VH);
  drawEnvironment(z);
  for(const p of projectiles){
    ctx.save(); ctx.translate(p.x-cam.x,p.y-cam.y);
    ctx.shadowColor='rgba(120,200,255,0.9)'; ctx.shadowBlur=10;
    ctx.fillStyle='#bfe8ff'; ctx.beginPath(); ctx.arc(0,0,5,0,7); ctx.fill();
    ctx.restore();
  }
  if(shimmerTimer>0){
    ctx.save(); ctx.globalAlpha=Math.min(1,shimmerTimer)*0.5;
    const grad=ctx.createLinearGradient(0,0,VW,0);
    grad.addColorStop(0,'rgba(255,220,130,0.5)'); grad.addColorStop(0.15,'rgba(0,0,0,0)');
    grad.addColorStop(0.85,'rgba(0,0,0,0)'); grad.addColorStop(1,'rgba(255,220,130,0.5)');
    ctx.fillStyle=grad; ctx.fillRect(0,0,VW,VH);
    ctx.restore();
  }
}

// ---------- LOOP ----------
function loop(now){
  const dt=Math.min(0.05,(now-lastT)/1000); lastT=now;
  if(document.getElementById('deathScreen').style.display!=='flex' && document.getElementById('victoryScreen').style.display!=='flex'){
    update(dt);
  }
  render();
  requestAnimationFrame(loop);
}

// init
resetPlayerToEntrance();
updateHUD();
showZoneBanner(currentZone().name);
requestAnimationFrame(loop);

})();
</script>
</body>
</html>
<!-- agent-meta {"model":"anthropic/claude-sonnet-5","provider":"openrouter","persona":"full","sessionId":"cbec64e3-ce80-448c-a97a-6827397cdc80","tokensIn":293112,"tokensOut":28095,"tokensTotal":321207,"cost":0.44042909999999996,"turns":8,"toolCalls":7,"failedToolCalls":0,"timestamp":"2026-09-30T21:57:24.494Z"} -->