← RPG results

RPG

Qwen 3.6 27b MTP · full

Initial view of RPG

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 39,351 bytes · SHA-256 9b0805774032
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dungeon of Echoes</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#0a0a0f;overflow:hidden;font-family:'Segoe UI',sans-serif;user-select:none}
canvas#game{display:block;cursor:crosshair}
#ui{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}
.hud-top{position:absolute;top:8px;left:8px;display:flex;gap:10px;align-items:center;flex-wrap:wrap}
.hud-box{background:rgba(0,0,0,.75);border:1px solid #444;border-radius:6px;padding:4px 10px;color:#fff;font-size:12px;display:flex;align-items:center;gap:5px}
.hearts{display:flex;gap:1px}
.heart-icon{width:16px;height:16px}
#xp-wrap{position:absolute;top:38px;left:8px;width:180px;height:5px;background:rgba(0,0,0,.7);border-radius:3px;border:1px solid #444}
#xp-fill{height:100%;background:linear-gradient(90deg,#f0c040,#ff8800);border-radius:3px;transition:width .3s;width:0%}
#wep-bar{position:absolute;bottom:8px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,.75);border:1px solid #444;border-radius:6px;padding:4px 14px;color:#fff;font-size:12px;display:flex;align-items:center;gap:6px}
#btn-inv{position:absolute;top:8px;right:8px;background:rgba(0,0,0,.75);border:1px solid #444;border-radius:6px;padding:4px 12px;color:#fff;font-size:12px;cursor:pointer;pointer-events:auto}
#btn-inv:hover{background:rgba(50,50,80,.9);border-color:#88f}
#mm-wrap{position:absolute;bottom:8px;right:8px;width:130px;height:130px;background:rgba(0,0,0,.7);border:1px solid #444;border-radius:6px;overflow:hidden}
#mm-canvas{width:100%;height:100%}
#dialog{position:absolute;bottom:50px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,.88);border:2px solid #88f;border-radius:8px;padding:10px 20px;color:#fff;font-size:13px;max-width:460px;text-align:center;display:none;pointer-events:auto}
#shop{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(10,10,30,.97);border:2px solid #f0c040;border-radius:10px;padding:16px;color:#fff;display:none;pointer-events:auto;min-width:340px}
#shop h2{text-align:center;color:#f0c040;margin-bottom:10px;font-size:16px}
.si{display:flex;justify-content:space-between;align-items:center;padding:7px 10px;margin:3px 0;background:rgba(255,255,255,.05);border-radius:5px;cursor:pointer;transition:background .2s}
.si:hover{background:rgba(255,255,255,.15)}
.si .sn{font-size:12px}.si .sc{color:#f0c040;font-size:11px}
#inv-panel{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(10,10,30,.97);border:2px solid #88f;border-radius:10px;padding:16px;color:#fff;display:none;pointer-events:auto;min-width:380px}
#inv-panel h2{text-align:center;color:#88f;margin-bottom:10px;font-size:16px}
.ig{display:grid;grid-template-columns:repeat(5,1fr);gap:5px;margin-top:6px}
.islot{width:54px;height:54px;background:rgba(255,255,255,.05);border:1px solid #444;border-radius:5px;display:flex;align-items:center;justify-content:center;font-size:22px;cursor:pointer;position:relative}
.islot:hover{border-color:#88f;background:rgba(100,100,255,.1)}
.islot .tip{display:none;position:absolute;bottom:58px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,.92);border:1px solid #88f;border-radius:4px;padding:3px 7px;font-size:10px;white-space:nowrap;color:#ccc}
.islot:hover .tip{display:block}
#save-notif{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,80,0,.92);border:2px solid #4f4;border-radius:8px;padding:12px 28px;color:#fff;font-size:15px;display:none;text-align:center}
#go-screen{position:absolute;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.88);display:none;align-items:center;justify-content:center;flex-direction:column;pointer-events:auto;z-index:99}
#go-screen h1{color:#f44;font-size:42px;margin-bottom:16px;text-shadow:0 0 20px rgba(255,50,50,.5)}
#go-screen button,#title-btn{background:#333;border:2px solid #f44;color:#fff;padding:10px 28px;font-size:15px;cursor:pointer;border-radius:6px;margin-top:8px}
#go-screen button:hover,#title-btn:hover{background:#555}
#title{position:absolute;top:0;left:0;width:100%;height:100%;background:linear-gradient(180deg,#0a0a2e,#1a0a3e);display:flex;align-items:center;justify-content:center;flex-direction:column;pointer-events:auto;z-index:100}
#title h1{color:#f0c040;font-size:46px;text-shadow:0 0 30px rgba(240,192,64,.5);margin-bottom:6px}
#title p{color:#aaa;font-size:14px;margin-bottom:24px}
.title-btn{background:linear-gradient(180deg,#f0c040,#cc9020);border:none;color:#1a0a00;padding:12px 42px;font-size:16px;cursor:pointer;border-radius:6px;font-weight:bold}
.title-btn:hover{background:linear-gradient(180deg,#ffd060,#e0a030)}
.ctrls{color:#777;font-size:11px;margin-top:20px;text-align:center;line-height:1.9}
#hint{position:absolute;bottom:42px;left:50%;transform:translateX(-50%);color:rgba(255,255,255,.35);font-size:10px;pointer-events:none}
</style>
</head>
<body>

<canvas id="game"></canvas>
<div id="ui">
  <div class="hud-top">
    <div class="hud-box"><span style="color:#f66">HP:</span><div class="hearts" id="hearts-d"></div></div>
    <div class="hud-box" id="money-d"><span style="color:#fc0">&#9733;</span> 0</div>
    <div class="hud-box" id="lvl-d">Lv.1</div>
  </div>
  <div id="xp-wrap"><div id="xp-fill"></div></div>
  <div id="wep-bar">&#9876; Wooden Sword</div>
  <button id="btn-inv" onclick="toggleInv()">&#128092; [I]</button>
  <div id="mm-wrap"><canvas id="mm-canvas" width="130" height="130"></canvas></div>
  <div id="dialog"></div>
  <div id="shop"><h2>&#9733; Merchant &#9733;</h2><div id="shop-list"></div><p style="text-align:center;margin-top:8px;font-size:10px;color:#888">ESC to close</p></div>
  <div id="inv-panel"><h2>&#128092; Inventory</h2><div class="ig" id="igrid"></div><p style="text-align:center;margin-top:8px;font-size:10px;color:#888">Click to use. I/ESC close.</p></div>
  <div id="save-notif">&#9734; Game Saved!</div>
  <div id="go-screen"><h1>You Have Fallen</h1><p style="color:#aaa">The dungeon claims another soul...</p><button onclick="doRestart()">Try Again</button></div>
  <div id="title">
    <h1>Dungeon of Echoes</h1>
    <p>An Action-Adventure RPG</p>
    <button class="title-btn" onclick="startGame()">Begin Adventure</button>
    <div class="ctrls">WASD/Arrows — Move &nbsp;|&nbsp; Mouse — Aim &nbsp;|&nbsp; Left Click — Attack<br>E — Interact &nbsp;|&nbsp; I — Inventory &nbsp;|&nbsp; 1-3 — Switch Weapons</div>
  </div>
  <div id="hint">WASD:Move | Click:Attack | E:Interact | I:Inventory</div>
</div>

<script>
const C=document.getElementById('game'),X=C.getContext('2d');
const MC=document.getElementById('mm-canvas'),MX=MC.getContext('2d');
let W,H;
function resize(){W=C.width=innerWidth;H=C.height=innerHeight}
resize();addEventListener('resize',resize);

// ===== AUDIO ENGINE =====
let AC;
function initAudio(){if(!AC)AC=new(window.AudioContext||window.webkitAudioContext)()}
function snd(t,v=.25){
  if(!AC)return;const n=AC.currentTime,g=AC.createGain();g.connect(AC.destination);g.gain.setValueAtTime(v,n);
  switch(t){
    case'walk':{const o=AC.createOscillator();o.type='sine';o.frequency.setValueAtTime(70+Math.random()*35,n);g.gain.exponentialRampToValueAtTime(.001,n+.06);o.connect(g);o.start(n);o.stop(n+.06)}break;
    case'sword':{const b=AC.createBuffer(1,AC.sampleRate*.12|0,AC.sampleRate),d=b.getChannelData(0);for(let i=0;i<d.length;i++)d[i]=(Math.random()*2-1)*Math.exp(-i/d.length*5);const s=AC.createBufferSource();s.buffer=b;g.gain.setValueAtTime(v*.4,n);g.gain.exponentialRampToValueAtTime(.001,n+.12);s.connect(g);s.start(n)}break;
    case'flame':{const b=AC.createBuffer(1,AC.sampleRate*.25|0,AC.sampleRate),d=b.getChannelData(0);for(let i=0;i<d.length;i++)d[i]=(Math.random()*2-1)*Math.exp(-i/d.length*3)*.4;const s=AC.createBufferSource();s.buffer=b;g.gain.setValueAtTime(v*.5,n);g.gain.exponentialRampToValueAtTime(.001,n+.25);s.connect(g);s.start(n)}break;
    case'bow':{const o=AC.createOscillator();o.type='sine';o.frequency.setValueAtTime(700,n);o.frequency.exponentialRampToValueAtTime(180,n+.14);g.gain.setValueAtTime(v*.25,n);g.gain.exponentialRampToValueAtTime(.001,n+.14);o.connect(g);o.start(n);o.stop(n+.14)}break;
    case'hit':{const b=AC.createBuffer(1,AC.sampleRate*.08|0,AC.sampleRate),d=b.getChannelData(0);for(let i=0;i<d.length;i++)d[i]=(Math.random()*2-1)*Math.exp(-i/d.length*4);const s=AC.createBufferSource();s.buffer=b;g.gain.setValueAtTime(v*.4,n);g.gain.exponentialRampToValueAtTime(.001,n+.08);s.connect(g);s.start(n)}break;
    case'pickup':{const o=AC.createOscillator();o.type='sine';[523,659,784].forEach((f,i)=>o.frequency.setValueAtTime(f,n+i*.07));g.gain.setValueAtTime(v*.35,n);g.gain.exponentialRampToValueAtTime(.001,n+.22);o.connect(g);o.start(n);o.stop(n+.22)}break;
    case'coin':{const o=AC.createOscillator();o.type='square';o.frequency.setValueAtTime(1100,n);g.gain.setValueAtTime(v*.15,n);g.gain.exponentialRampToValueAtTime(.001,n+.07);o.connect(g);o.start(n);o.stop(n+.07)}break;
    case'shop':{const o=AC.createOscillator();o.type='triangle';[440,554].forEach((f,i)=>o.frequency.setValueAtTime(f,n+i*.08));g.gain.setValueAtTime(v*.25,n);g.gain.exponentialRampToValueAtTime(.001,n+.16);o.connect(g);o.start(n);o.stop(n+.16)}break;
    case'save':{const o=AC.createOscillator();o.type='sine';[392,523,659].forEach((f,i)=>o.frequency.setValueAtTime(f,n+i*.12));g.gain.setValueAtTime(v*.35,n);g.gain.exponentialRampToValueAtTime(.001,n+.45);o.connect(g);o.start(n);o.stop(n+.45)}break;
    case'lvlup':{[523,659,784,1047].forEach((f,i)=>{const o=AC.createOscillator(),g2=AC.createGain();o.type='sine';o.frequency.setValueAtTime(f,n+i*.1);g2.gain.setValueAtTime(v*.25,n+i*.1);g2.gain.exponentialRampToValueAtTime(.001,n+i*.1+.18);o.connect(g2);g2.connect(AC.destination);o.start(n+i*.1);o.stop(n+i*.1+.18)})}break;
    case'death':{const o=AC.createOscillator();o.type='sawtooth';o.frequency.setValueAtTime(350,n);o.frequency.exponentialRampToValueAtTime(40,n+.9);g.gain.setValueAtTime(v*.4,n);g.gain.exponentialRampToValueAtTime(.001,n+.9);o.connect(g);o.start(n);o.stop(n+.9)}break;
  }
}

// ===== PARTICLES =====
let parts=[];
function spark(x,y,c,n=8,s=3,l=.5){for(let i=0;i<n;i++){const a=Math.random()*Math.PI*2,sp=Math.random()*s+1;parts.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,l,ml:l,c,sz:Math.random()*3+1})}}
function updParts(dt){for(let i=parts.length-1;i>=0;i--){const p=parts[i];p.x+=p.vx*dt*60;p.y+=p.vy*dt*60;p.l-=dt;if(p.l<=0)parts.splice(i,1)}}
function drawParts(cx,cy){for(const p of parts){X.globalAlpha=Math.max(0,p.l/p.ml);X.fillStyle=p.c;X.beginPath();X.arc(p.x-cx,p.y-cy,p.sz*(p.l/p.ml),0,Math.PI*2);X.fill()}X.globalAlpha=1}

// ===== GAME STATE =====
const T=40,RW=20,RH=15;
let gs='title',gt=0,midGame=false,bossDead=false;
let cam={x:0,y:0},shake={x:0,y:0,amt:0};
let curRoom=0,dialogT='',dialogTimer=0,shopOpen=false,invOpen=false;

const P={
  x:200,y:200,vx:0,vy:0,spd:160,hp:3,mhp:3,money:0,xp:0,lvl:1,xpNext:50,
  face:{x:1,y:0},wIdx:0,atkCD:0,invT:0,dmgFlash:0,walkT:0,
  weapons:[
    {n:'Wooden Sword',dmg:3,rng:48,spd:.4,c:'#c9a060',ic:'&#9876;',ok:true},
    {n:'Iron Blade',dmg:5,rng:56,spd:.32,c:'#aab',ic:'&#128300;',ok:false},
    {n:'Flame Sword',dmg:7,rng:64,spd:.28,c:'#f80',ic:'&#128293;',ok:false}
  ],
  hasBow:false,bowDmg:4,acc:[],inv:[],rooms:new Set()
};

// ===== WORLD MAP =====
function mkRoom(wc,fc){const r=[];for(let y=0;y<RH;y++){r[y]=[];for(let x=0;x<RW;x++)r[y][x]=(x===0||x===RW-1||y===0||y===RH-1)?1:0}return{t:r,wc,fc}}

const world=[
  mkRoom('#3a2a1a','#2a1f15'), // 0 start - warm earth tones
  mkRoom('#1a3a1a','#152a15'), // 1 forest - green
  mkRoom('#2a2a3a','#1a1a25'), // 2 cave - dark blue-gray
  mkRoom('#4a3a20','#3a2a18'), // 3 merchant - warm sandstone
  mkRoom('#1a1a2a','#0f0f1a'), // 4 dungeon - very dark
  mkRoom('#2a1a4a','#1a1030'), // 5 crystal - purple
  mkRoom('#4a1a1a','#301010'), // 6 boss - red/dark
  mkRoom('#3a3a2a','#252518')  // 7 vault - gold/stone
];

// Special tiles: 3=savepoint, 4=shop, 7=boss_door
world[0].t[7][10]=3;
world[3].t[7][10]=4; world[3].t[5][5]=3;
world[4].t[7][10]=3;
world[6].t[7][10]=7;
world[7].t[7][10]=3;

// Interior obstacles for variety
world[1].t[3][4]=1;world[1].t[3][5]=1;world[1].t[8][14]=1;world[1].t[9][14]=1;
world[2].t[5][8]=6;

const conns=[
  [0,1,'r','l'],[0,2,'b','t'],[1,3,'r','l'],
  [2,4,'b','t'],[4,5,'r','l'],[4,7,'r','l'],[5,6,'b','t']
];

// ===== ENTITIES =====
let enemies=[],items=[],projs=[],dmgNums=[];

function roomEnemies(rid){
  const m=[
    [], // 0 start - peaceful
    [{tp:'slime',x:300,y:250,hp:4,mhp:4,dmg:1,spd:40,c:'#4a4',xp:10,gold:5},
     {tp:'slime',x:600,y:400,hp:4,mhp:4,dmg:1,spd:40,c:'#4a4',xp:10,gold:5},
     {tp:'slime',x:450,y:350,hp:5,mhp:5,dmg:1,spd:45,c:'#5b5',xp:12,gold:6}], // 1 forest
    [{tp:'goblin',x:350,y:300,hp:8,mhp:8,dmg:2,spd:55,c:'#a64',xp:20,gold:10},
     {tp:'goblin',x:500,y:450,hp:8,mhp:8,dmg:2,spd:55,c:'#a64',xp:20,gold:10}], // 2 cave
    [{tp:'goblin',x:250,y:200,hp:10,mhp:10,dmg:2,spd:60,c:'#a64',xp:25,gold:12},
     {tp:'skeleton',x:650,y:350,hp:12,mhp:12,dmg:3,spd:65,c:'#ccc',xp:30,gold:15}], // 3 merchant
    [{tp:'skeleton',x:300,y:300,hp:15,mhp:15,dmg:3,spd:70,c:'#ddd',xp:35,gold:18},
     {tp:'skeleton',x:600,y:250,hp:15,mhp:15,dmg:3,spd:70,c:'#ddd',xp:35,gold:18},
     {tp:'goblin',x:450,y:500,hp:12,mhp:12,dmg:3,spd:60,c:'#843',xp:28,gold:14}], // 4 dungeon
    [{tp:'golem',x:350,y:300,hp:20,mhp:20,dmg:4,spd:30,c:'#a6f',xp:45,gold:25},
     {tp:'skeleton',x:550,y:450,hp:18,mhp:18,dmg:4,spd:75,c:'#eee',xp:40,gold:20}], // 5 crystal
    [{tp:'boss',x:400,y:300,hp:60,mhp:60,dmg:5,spd:45,c:'#f44',xp:100,gold:50}], // 6 boss
    [{tp:'golem',x:300,y:350,hp:22,mhp:22,dmg:4,spd:35,c:'#b8f',xp:50,gold:30},
     {tp:'skeleton',x:600,y:300,hp:18,mhp:18,dmg:4,spd:70,c:'#eee',xp:40,gold:22}] // 7 vault
  ];return m[rid]||[];
}

function roomItems(rid){
  const m=[
    [{tp:'health_potion',x:350,y:400,nm:'Health Potion',ds:'Restores 2 HP',ic:'&#128704;'},
     {tp:'coin',x:500,y:300,nm:'Gold Coins',ds:'+10 Gold',val:10,ic:'&#9733;'}], // 0 start
    [{tp:'speed_boots',x:400,y:250,nm:'Speed Boots',ds:'+20% movement speed',ic:'&#128096;'},
     {tp:'coin',x:650,y:350,nm:'Gold Coins',ds:'+15 Gold',val:15,ic:'&#9733;'}], // 1 forest
    [{tp:'shield_charm',x:400,y:400,nm:'Shield Charm',ds:'Reduces damage by 1',ic:'&#128737;'},
     {tp:'weapon_iron',x:550,y:250,nm:'Iron Blade Upgrade',ds:'Upgrade your sword!',ic:'&#9876;'}], // 2 cave
    [{tp:'gold_ring',x:300,y:450,nm:'Gold Ring',ds:'+25% more gold from enemies',ic:'&#128143;'},
     {tp:'coin',x:600,y:200,nm:'Gold Coins',ds:'+20 Gold',val:20,ic:'&#9733;'}], // 3 merchant
    [{tp:'magic_amulet',x:350,y:350,nm:'Magic Amulet',ds:'+50% ranged damage',ic:'&#129302;'},
     {tp:'weapon_flame',x:600,y:450,nm:'Flame Sword Upgrade',ds:'Fire-infused blade!',ic:'&#128293;'}], // 4 dungeon
    [{tp:'bow_discovery',x:400,y:300,nm:'Crystal Bow',ds:'A powerful ranged weapon!',ic:'&#127938;'},
     {tp:'health_potion',x:650,y:400,nm:'Health Potion',ds:'Restores 2 HP',ic:'&#128704;'}], // 5 crystal
    [], // 6 boss - no items
    [{tp:'heart_container',x:400,y:350,nm:'Heart Container',ds:'+2 Max HP!',ic:'&#10084;&#65039;'},
     {tp:'coin',x:250,y:250,nm:'Gold Coins',ds:'+30 Gold',val:30,ic:'&#9733;'}] // 7 vault
  ];return m[rid]||[];
}

function spawnRoom(rid){
  for(const e of roomEnemies(rid)){enemies.push({...e,kb:{x:0,y:0},hf:0,atkT:Math.random()*2})}
  for(const it of roomItems(rid)){items.push({...it,bob:Math.random()*6.28})}
}

// ===== SHOP =====
const shopData=[
  {nm:'Health Potion',ds:'Restores 2 HP',cost:15,ic:'&#128704;',act:()=>{if(P.hp<P.mhp){P.hp=Math.min(P.hp+2,P.mhp);snd('pickup');spark(P.x,P.y,'#f44',6)}else showD("Already at full health!")}},
  {nm:'Big Health Potion',ds:'Restores 4 HP',cost:30,ic:'&#128704;',act:()=>{P.hp=Math.min(P.hp+4,P.mhp);snd('pickup');spark(P.x,P.y,'#f66',8)}},
  {nm:'Speed Elixir',ds:'+10% speed for 30s',cost:25,ic:'&#9889;',act:()=>{P.spd*=1.1;setTimeout(()=>P.spd/=1.1,30000);snd('pickup')}},
];

// ===== INPUT =====
const keys={};let mx=0,my=0,mDown=false;
addEventListener('keydown',e=>{
  const k=e.key.toLowerCase();keys[k]=true;
  if(k==='i'&&gs==='playing')toggleInv();
  if(e.key==='Escape'){document.getElementById('shop').style.display='none';document.getElementById('inv-panel').style.display='none'}
  if(gs==='playing'){if(k==='1')P.wIdx=0;if(k==='2'&&P.weapons[1].ok)P.wIdx=1;if(k==='3'&&P.weapons[2].ok)P.wIdx=2}
});
addEventListener('keyup',e=>{keys[e.key.toLowerCase()]=false});
C.addEventListener('mousemove',e=>{mx=e.clientX;my=e.clientY});
C.addEventListener('mousedown',e=>{if(e.button===0){mDown=true;doAttack()}});
C.addEventListener('mouseup',e=>{if(e.button===0)mDown=false});
C.addEventListener('contextmenu',e=>e.preventDefault());

// ===== GAME FUNCTIONS =====
function startGame(){initAudio();document.getElementById('title').style.display='none';gs='playing';freshStart()}
function doRestart(){document.getElementById('go-screen').style.display='none';gs='playing';resetP();enemies=[];items=[];projs=[];parts=[];dmgNums=[];midGame=false;bossDead=false;curRoom=0;spawnRoom(0);P.rooms.add(0)}
function resetP(){P.x=200;P.y=200;P.hp=3;P.mhp=3;P.money=0;P.xp=0;P.lvl=1;P.xpNext=50;P.weapons[1].ok=false;P.weapons[2].ok=false;P.hasBow=false;P.acc=[];P.inv=[];P.wIdx=0;P.spd=160;P.rooms=new Set()}
function freshStart(){resetP();spawnRoom(0);P.rooms.add(0);updHUD()}

function showD(t,dur=3){dialogT=t;dialogTimer=dur;const db=document.getElementById('dialog');db.textContent=t;db.style.display='block'}
function toggleInv(){invOpen=!invOpen;document.getElementById('inv-panel').style.display=invOpen?'block':'none';if(invOpen)renderInv()}

function renderInv(){
  const g=document.getElementById('igrid');g.innerHTML='';
  for(let i=0;i<15;i++){
    const s=document.createElement('div');s.className='islot';
    if(i<P.inv.length){const it=P.inv[i];s.textContent=it.ic;const t=document.createElement('div');t.className='tip';t.textContent=`${it.nm} - ${it.ds}`;s.appendChild(t);s.onclick=()=>useItem(i)}
    g.appendChild(s)
  }
}

function useItem(idx){
  const it=P.inv[idx];if(!it)return;snd('pickup');
  if(it.tp==='health_potion'){if(P.hp<P.mhp){P.hp=Math.min(P.hp+2,P.mhp);spark(P.x,P.y,'#f44',6)}else showD("Already at full health!")}
  P.inv.splice(idx,1);renderInv()
}

function openShop(){
  shopOpen=true;snd('shop');const p=document.getElementById('shop');p.style.display='block';
  const c=document.getElementById('shop-list');c.innerHTML='';
  for(const si of shopData){
    const d=document.createElement('div');d.className='si';
    d.innerHTML=`<span class="sn">${si.ic} ${si.nm}</span><span style="font-size:10px;color:#aaa">${si.ds}</span><span class="sc">${si.cost}&#9733;</span>`;
    d.onclick=()=>{if(P.money>=si.cost){P.money-=si.cost;si.act();snd('coin');updHUD()}else showD("Not enough gold!")};
    c.appendChild(d)
  }
}

function doAttack(){
  if(gs!=='playing'||P.atkCD>0)return;
  const wp=P.weapons[P.wIdx];P.atkCD=wp.spd;
  const dx=mx-W/2,dy=my-H/2,d=Math.sqrt(dx*dx+dy*dy)||1;
  P.face={x:dx/d,y:dy/d};

  if(!P.hasBow){
    snd(P.wIdx===2?'flame':'sword');
    const ax=P.x+P.face.x*wp.rng/2,ay=P.y+P.face.y*wp.rng/2;
    for(const e of enemies){if(e.hp<=0)continue;const ex=e.x-ax,ey=e.y-ay;if(Math.sqrt(ex*ex+ey*ey)<wp.rng/2+16)dmgEnemy(e,wp.dmg)}
    spark(ax,ay,P.wIdx===2?'#f80':'#fff',5,2,.3);
  } else {
    snd('bow');const dmg=P.bowDmg*(P.acc.includes('magic_amulet')?1.5:1);
    projs.push({x:P.x,y:P.y,vx:P.face.x*300,vy:P.face.y*300,dmg,l:2,c:'#aaf',sz:4,e:false});
  }
}

function dmgEnemy(e,dmg){
  e.hp-=dmg;e.hf=.15;snd('hit');
  const dx=e.x-P.x,dy=e.y-P.y,d=Math.sqrt(dx*dx+dy*dy)||1;
  e.kb={x:dx/d*80,y:dy/d*80};spark(e.x,e.y,'#ff0',6,3,.4);
  dmgNums.push({x:e.x,y:e.y-20,t:'-'+dmg,c:'#ff4',l:1});
  if(e.hp<=0)killEnemy(e)
}

function killEnemy(e){
  spark(e.x,e.y,e.c,15,4,.8);
  const mm=P.acc.includes('gold_ring')?1.25:1,g=Math.floor(e.gold*mm);
  P.money+=g;P.xp+=e.xp;dmgNums.push({x:e.x,y:e.y-30,t:`+${g}&#9733;`,c:'#fc0',l:1.5});snd('coin');
  while(P.xp>=P.xpNext){P.xp-=P.xpNext;P.lvl++;P.xpNext=Math.floor(P.xpNext*1.4);snd('lvlup');dmgNums.push({x:P.x,y:P.y-30,t:'LEVEL UP!',c:'#ff0',l:2})}
  updHUD()
}

function dmgPlayer(d){
  if(P.invT>0)return;const r=P.acc.includes('shield_charm')?1:0;d=Math.max(1,d-r);
  P.hp-=d;P.invT=.5;P.dmgFlash=.3;snd('hit');spark(P.x,P.y,'#f44',8,3,.5);shake.amt=6;
  dmgNums.push({x:P.x,y:P.y-20,t:'-'+d,c:'#f44',l:1});
  if(P.hp<=0){P.hp=0;gs='gameover';document.getElementById('go-screen').style.display='flex';snd('death')}
  updHUD()
}

// ===== UPDATE LOOP =====
let lt=0;
function loop(ts){const dt=Math.min((ts-lt)/1000,.05);lt=ts;if(gs==='playing')update(dt);render();requestAnimationFrame(loop)}

function update(dt){
  gt+=dt;
  
  // Screen shake decay
  if(shake.amt>0){shake.x=(Math.random()-.5)*shake.amt;shake.y=(Math.random()-.5)*shake.amt;shake.amt*=.85;if(shake.amt<.1)shake.amt=0}

  let mx2=0,my2=0;
  if(keys['w']||keys['arrowup'])my2=-1;if(keys['s']||keys['arrowdown'])my2=1;
  if(keys['a']||keys['arrowleft'])mx2=-1;if(keys['d']||keys['arrowright'])mx2=1;
  if(mx2&&my2){mx2*=.707;my2*=.707}

  const nx=P.x+mx2*P.spd*dt,ny=P.y+my2*P.spd*dt;
  const rw=RW*T,rh=RH*T,tiles=world[curRoom].t;
  if(!isWall(nx,P.y,tiles))P.x=nx;if(!isWall(P.x,ny,tiles))P.y=ny;
  P.x=Math.max(T+14,Math.min(rw-T-14,P.x));P.y=Math.max(T+14,Math.min(rh-T-14,P.y));

  if(mx2||my2){P.walkT+=dt;if(P.walkT>.35){P.walkT=0;snd('walk',.07)}}else P.walkT=0;

  const dxm=mx-W/2,dym=my-H/2;
  if(Math.abs(dxm)>5||Math.abs(dym)>5){const dm=Math.sqrt(dxm*dxm+dym*dym);P.face={x:dxm/dm,y:dym/dm}}

  if(P.atkCD>0)P.atkCD-=dt;if(P.invT>0)P.invT-=dt;if(P.dmgFlash>0)P.dmgFlash-=dt;
  checkTrans();
  if(keys['e']){keys['e']=false;interact()}

  // Enemy AI
  for(const e of enemies){
    if(e.hp<=0)continue;
    e.x+=e.kb.x*dt;e.y+=e.kb.y*dt;e.kb.x*=Math.pow(.01,dt);e.kb.y*=Math.pow(.01,dt);
    if(e.hf>0)e.hf-=dt;
    const edx=P.x-e.x,edy=P.y-e.y,dist=Math.sqrt(edx*edx+edy*edy);
    if(dist<350&&dist>24){e.x+=edx/dist*e.spd*dt;e.y+=edy/dist*e.spd*dt}
    if(dist<28){e.atkT-=dt;if(e.atkT<=0){dmgPlayer(e.dmg);e.atkT=e.tp==='boss'?1.5:1+Math.random()}}
    // Boss ranged attack
    if(e.tp==='boss'&&dist<200&&dist>40){e.atkT-=dt;if(e.atkT<=0){const ba=Math.atan2(edy,edx);for(let i=-1;i<=1;i++){const a=ba+i*.3;projs.push({x:e.x,y:e.y,vx:Math.cos(a)*150,vy:Math.sin(a)*150,dmg:e.dmg,l:2,c:'#f44',sz:6,e:true})}e.atkT=2}}
  }
  enemies=enemies.filter(e=>e.hp>0);

  // Projectiles
  for(let i=projs.length-1;i>=0;i--){const p=projs[i];p.x+=p.vx*dt;p.y+=p.vy*dt;p.l-=dt;
    if(p.l<=0||p.x<0||p.x>rw||p.y<0||p.y>rh){projs.splice(i,1);continue}
    if(!p.e){for(const e of enemies){if(e.hp<=0)continue;if(Math.sqrt((e.x-p.x)**2+(e.y-p.y)**2)<18){dmgEnemy(e,p.dmg);projs.splice(i,1);break}}}
    else{if(Math.sqrt((P.x-p.x)**2+(P.y-p.y)**2)<20){dmgPlayer(p.dmg);projs.splice(i,1)}}
  }

  // Item pickup
  for(let i=items.length-1;i>=0;i--){const it=items[i];it.bob+=dt*3;
    if(Math.sqrt((P.x-it.x)**2+(P.y-it.y)**2)<28){pickupItem(it);items.splice(i,1)}
  }

  updParts(dt);
  for(let i=dmgNums.length-1;i>=0;i--){dmgNums[i].y-=40*dt;dmgNums[i].l-=dt;if(dmgNums[i].l<=0)dmgNums.splice(i,1)}
  if(dialogTimer>0){dialogTimer-=dt;if(dialogTimer<=0)document.getElementById('dialog').style.display='none'}

  cam.x=P.x-W/2;cam.y=P.y-H/2;
}

function isWall(x,y,tiles){const tx=Math.floor(x/T),ty=Math.floor(y/T);if(tx<0||tx>=RW||ty<0||ty>=RH)return true;return tiles[ty][tx]===1}

let transLock=false;
function checkTrans(){
  if(transLock)return;
  const rw=RW*T,rh=RH*T;
  for(const[f,t,fd,td]of conns){if(f!==curRoom)continue;
    let at=false;switch(fd){case'r':at=P.x>=rw-T-10&&Math.abs(P.y-rh/2)<T*1.5;break;case'l':at=P.x<=T+10&&Math.abs(P.y-rh/2)<T*1.5;break;case'b':at=P.y>=rh-T-10&&Math.abs(P.x-rw/2)<T*1.5;break;case't':at=P.y<=T+10&&Math.abs(P.x-rw/2)<T*1.5;break}
    if(at){transLock=true;curRoom=t;P.rooms.add(t);switch(td){case'l':P.x=T+20;break;case'r':P.x=rw-T-20;break;case't':P.y=T+20;break;case'b':P.y=rh-T-20;break}
      spawnRoom(t);if(!midGame&&t>=4){midGame=true;showD("The dungeon grows darker... Powerful enemies ahead!",3)}
      if(t===6&&!bossDead)showD("A massive presence fills the chamber...",3);
      setTimeout(()=>transLock=false,500)}}
}

function interact(){
  const tiles=world[curRoom].t,px=Math.floor(P.x/T),py=Math.floor(P.y/T);
  for(let dy=-1;dy<=1;dy++)for(let dx=-1;dx<=1;dx++){const tx=px+dx,ty=py+dy;if(tx<0||tx>=RW||ty<0||ty>=RH)continue;const tile=tiles[ty][tx];
    if(tile===3){saveGame();return}if(tile===4&&curRoom===3){openShop();return}if(tile===7&&curRoom===6){showD("The boss awaits! Prepare yourself!",2);spawnRoom(6);tiles[ty][tx]=0;return}}
}

function pickupItem(it){
  snd('pickup');spark(it.x,it.y,'#ff0',8,3,.5);
  switch(it.tp){
    case'coin':P.money+=it.val;dmgNums.push({x:it.x,y:it.y-20,t:`+${it.val}&#9733;`,c:'#fc0',l:1});break;
    case'health_potion':if(P.inv.length<15)P.inv.push(it);else{P.hp=Math.min(P.hp+2,P.mhp)}showD(`Got ${it.nm}!`,1.5);break;
    case'speed_boots':if(!P.acc.includes('speed_boots')){P.spd*=1.2;P.acc.push('speed_boots');P.inv.push(it);showD("Speed Boots equipped!",2)}else return;break;
    case'shield_charm':if(!P.acc.includes('shield_charm')){P.acc.push('shield_charm');P.inv.push(it);showD("Shield Charm equipped! Damage reduced!",2)}else return;break;
    case'gold_ring':if(!P.acc.includes('gold_ring')){P.acc.push('gold_ring');P.inv.push(it);showD("Gold Ring equipped! More gold from enemies!",2)}else return;break;
    case'magic_amulet':if(!P.acc.includes('magic_amulet')){P.acc.push('magic_amulet');P.inv.push(it);showD("Magic Amulet equipped! Ranged damage boosted!",2)}else return;break;
    case'weapon_iron':if(!P.weapons[1].ok){P.weapons[1].ok=true;P.wIdx=1;showD("Iron Blade acquired! Press 2 to equip!",2);spark(P.x,P.y,'#aab',12,4,.6)}else return;break;
    case'weapon_flame':if(!P.weapons[2].ok){P.weapons[2].ok=true;P.wIdx=2;showD("Flame Sword acquired! Press 3 to equip!",2);spark(P.x,P.y,'#f80',15,5,.6)}else return;break;
    case'bow_discovery':if(!P.hasBow){P.hasBow=true;showD("Crystal Bow discovered! Click to shoot arrows!",2);spark(P.x,P.y,'#aaf',15,4,.6)}else return;break;
    case'heart_container':if(P.mhp<10){P.mhp+=2;P.hp=Math.min(P.hp+2,P.mhp);showD("Heart Container! Max HP: "+P.mhp+"!",2);spark(P.x,P.y,'#f44',15,4,.8)}else return;break;
  }updHUD()
}

function saveGame(){snd('save');try{localStorage.setItem('dungeonEchoesSave',JSON.stringify({p:{...P,rooms:[...P.rooms]},weapons:P.weapons.map(w=>({...w})),cr:curRoom,mg:midGame,bd:bossDead}))}catch(e){}
  const n=document.getElementById('save-notif');n.style.display='block';spark(P.x,P.y,'#4f4',20,5,1);setTimeout(()=>n.style.display='none',2000)}

function loadGame(){try{const r=localStorage.getItem('dungeonEchoesSave');if(r){const d=JSON.parse(r);Object.assign(P,d.p);P.rooms=new Set(d.p.rooms||[]);curRoom=d.cr;midGame=d.mg;bossDead=d.bd;for(let i=0;i<3;i++)P.weapons[i].ok=d.weapons[i].ok;
    for(const rid of P.rooms)spawnRoom(rid);showD("Game loaded!",2)}}catch(e){}}

function updHUD(){
  const hd=document.getElementById('hearts-d');hd.innerHTML='';const th=Math.ceil(P.mhp/2);
  for(let i=0;i<th;i++){const h=document.createElement('div');h.className='heart-icon';const hp=i*2;
    if(P.hp>hp+1)h.innerHTML='<svg viewBox="0 0 24 24"><path fill="#f44" d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>';
    else if(P.hp>hp)h.innerHTML='<svg viewBox="0 0 24 24"><defs><clipPath id="hc'+i+'"><rect x="0" y="0" width="12" height="24"/></clipPath></defs><g clip-path="url(#hc'+i+')"><path fill="#f44" d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></g><path fill="#444" d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>';
    else h.innerHTML='<svg viewBox="0 0 24 24"><path fill="#444" d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>';
    hd.appendChild(h)}
  document.getElementById('money-d').innerHTML=`<span style="color:#fc0">&#9733;</span> ${P.money}`;
  document.getElementById('lvl-d').textContent=`Lv.${P.lvl}`;
  document.getElementById('xp-fill').style.width=(P.xp/P.xpNext*100)+'%';
  const wp=P.weapons[P.wIdx];let wt=`${wp.ic} ${wp.nm}`;if(P.hasBow)wt+=' | &#127938; Bow';
  document.getElementById('wep-bar').innerHTML=wt;
}

// ===== RENDERING =====
function render(){
  X.fillStyle='#0a0a0f';X.fillRect(0,0,W,H);
  if(gs==='title')return;
  
  const cx=cam.x+shake.x,cy=cam.y+shake.y;
  const room=world[curRoom],tiles=room.t,rw=RW*T,rh=RH*T;

  // Floor fill
  X.fillStyle=room.fc;X.fillRect(0,0,W,H);

  // Draw tiles with detail
  for(let ty=Math.max(0,Math.floor(cy/T));ty<Math.min(RH,Math.ceil((cy+H)/T)+1);ty++){
    for(let tx=Math.max(0,Math.floor(cx/T));tx<Math.min(RW,Math.ceil((cx+W)/T)+1);tx++){
      const sx=tx*T-cx,sy=ty*T-cy,tile=tiles[ty][tx];
      if(tile===1){
        // Wall with texture
        X.fillStyle=room.wc;X.fillRect(sx,sy,T,T);
        // Brick pattern
        X.strokeStyle='rgba(0,0,0,.2)';X.lineWidth=1;
        const off=(ty%2)*T/2;
        X.strokeRect(sx+off,sy,T,T);
        X.fillStyle='rgba(0,0,0,.3)';X.fillRect(sx,sy+T-4,T,4);
        X.fillStyle='rgba(255,255,255,.06)';X.fillRect(sx,sy,T,2);
      } else if(tile===3){
        drawSP(sx+T/2,sy+T/2);
      } else if(tile===4){
        drawShopTile(sx+T/2,sy+T/2);
      } else if(tile===7){
        drawBossDoor(sx+T/2,sy+T/2);
      } else {
        // Floor detail - subtle grid + random details
        X.strokeStyle='rgba(255,255,255,.03)';X.lineWidth=1;X.strokeRect(sx+.5,sy+.5,T-1,T-1);
        const seed=(tx*137+ty*97)%100;
        if(seed<8){X.fillStyle='rgba(255,255,255,.04)';X.fillRect(sx+seed%T,sy+(seed/3)%T,2,2)}
      }
    }
  }

  // Draw doorways with glow effect
  for(const[f,t,fd]of conns){if(f!==curRoom&&t!==curRoom)continue;
    let sx,sy;switch(fd){case'r':sx=rw-cx;sy=rh/2-T/2-cy;break;case'l':sx=-cx;sy=rh/2-T/2-cy;break;case'b':sx=rw/2-T/2-cx;sy=rh-cy;break;case't':sx=rw/2-T/2-cx;sy=-cy;break}
    const glow=Math.sin(gt*3)*.3+.7;
    X.fillStyle=`rgba(100,150,255,${glow*.15})`;X.fillRect(sx-6,sy-6,T+12,T+12);
    X.strokeStyle=`rgba(100,150,255,${glow*.7})`;X.lineWidth=2;X.strokeRect(sx-3,sy-3,T+6,T+6);
  }

  // Items with glow and bobbing
  for(const it of items){const sx=it.x-cx,sy=it.y-cy+Math.sin(it.bob)*5;if(sx<-30||sx>W+30||sy<-30||sy>H+30)continue;
    // Glow circle
    X.fillStyle='rgba(255,255,100,.1)';X.beginPath();X.arc(sx,sy,18,0,Math.PI*2);X.fill();
    X.fillStyle='rgba(255,255,100,.05)';X.beginPath();X.arc(sx,sy,24,0,Math.PI*2);X.fill();
    // Item icon
    X.font='22px serif';X.textAlign='center';X.textBaseline='middle';X.fillText(it.ic,sx,sy);
  }

  // Enemies with detailed rendering
  for(const e of enemies){if(e.hp<=0)continue;const sx=e.x-cx,sy=e.y-cy;if(sx<-50||sx>W+50||sy<-50||sy>H+50)continue;
    drawEnemy(e,sx,sy);
  }

  // Projectiles with trails
  for(const p of projs){const sx=p.x-cx,sy=p.y-cy;
    X.globalAlpha=.3;X.fillStyle=p.c;X.beginPath();X.arc(sx-p.vx*.02,sy-p.vy*.02,p.sz*.7,0,Math.PI*2);X.fill();
    X.globalAlpha=.15;X.beginPath();X.arc(sx-p.vx*.04,sy-p.vy*.04,p.sz*.5,0,Math.PI*2);X.fill();
    X.globalAlpha=1;X.fillStyle=p.c;X.beginPath();X.arc(sx,sy,p.sz,0,Math.PI*2);X.fill();
  }

  // Player character
  drawPlayer(cx,cy);

  // Particles & damage numbers (screen space)
  drawParts(cx,cy);
  for(const dn of dmgNums){const sx=dn.x-cx,sy=dn.y-cy;X.globalAlpha=Math.min(1,dn.l*2);X.font='bold 13px sans-serif';X.textAlign='center';X.textBaseline='middle';X.fillStyle='#000';X.fillText(dn.t,sx+1,sy+1);X.fillStyle=dn.c;X.fillText(dn.t,sx,sy)}
  X.globalAlpha=1;

  // Ambient lighting overlay (vignette)
  const grd=X.createRadialGradient(W/2,H/2,W*.3,W/2,H/2,W*.75);
  grd.addColorStop(0,'rgba(0,0,0,0)');grd.addColorStop(1,'rgba(0,0,0,.4)');
  X.fillStyle=grd;X.fillRect(0,0,W,H);

  // Minimap
  drawMM();
}

function drawEnemy(e,sx,sy){
  const fc=e.hf>0?'#fff':e.c;
  
  // Shadow
  X.fillStyle='rgba(0,0,0,.25)';X.beginPath();X.ellipse(sx,sy+14,14,6,0,0,Math.PI*2);X.fill();

  if(e.tp==='slime'){
    const sq=Math.sin(gt*4+e.x)*2;
    X.fillStyle=fc;X.beginPath();X.ellipse(sx,sy+sq/2,14-sq/2,10+sq/2,0,0,Math.PI*2);X.fill();
    // Highlight
    X.fillStyle='rgba(255,255,255,.2)';X.beginPath();X.ellipse(sx-3,sy-3+sq/2,4,3,-.3,0,Math.PI*2);X.fill();
    // Eyes
    X.fillStyle='#fff';X.fillRect(sx-5,sy-3,4,4);X.fillRect(sx+2,sy-3,4,4);
    X.fillStyle='#111';X.fillRect(sx-4,sy-2,2,2);X.fillRect(sx+3,sy-2,2,2);
  } else if(e.tp==='goblin'){
    // Body
    X.fillStyle=fc;X.beginPath();X.arc(sx,sy-6,10,0,Math.PI*2);X.fill();
    X.fillRect(sx-8,sy+4,16,14);
    // Highlight
    X.fillStyle='rgba(255,255,255,.15)';X.beginPath();X.arc(sx-3,sy-9,4,0,Math.PI*2);X.fill();
    // Eyes (red)
    X.fillStyle='#f00';X.fillRect(sx-5,sy-8,3,3);X.fillRect(sx+2,sy-8,3,3);
  } else if(e.tp==='skeleton'){
    // Skull
    X.fillStyle=fc;X.beginPath();X.arc(sx,sy-6,10,0,Math.PI*2);X.fill();
    // Eye sockets
    X.fillStyle='#111';X.fillRect(sx-5,sy-8,3,3);X.fillRect(sx+2,sy-8,3,3);
    // Ribcage body
    X.fillStyle=fc;X.fillRect(sx-6,sy+4,12,10);
    X.strokeStyle='rgba(0,0,0,.3)';X.lineWidth=1;X.strokeRect(sx-5,sy+5,4,8);X.strokeRect(sx+1,sy+5,4,8);
  } else if(e.tp==='golem'){
    // Crystal body
    X.fillStyle=fc;X.beginPath();X.arc(sx,sy-4,16,0,Math.PI*2);X.fill();
    // Crystal facets
    X.fillStyle='rgba(255,255,255,.3)';X.fillRect(sx-8,sy-12,4,4);X.fillRect(sx+4,sy-8,4,4);
    // Eyes (magenta glow)
    X.fillStyle='#f0f';X.shadowColor='#f0f';X.shadowBlur=6;X.fillRect(sx-6,sy-7,4,4);X.fillRect(sx+3,sy-5,4,4);X.shadowBlur=0;
  } else if(e.tp==='boss'){
    const pu=Math.sin(gt*2)*3;
    // Aura
    X.fillStyle='rgba(255,50,50,.1)';X.beginPath();X.arc(sx,sy-4+pu/2,30,0,Math.PI*2);X.fill();
    // Body
    X.fillStyle=fc;X.beginPath();X.arc(sx,sy-8+pu/2,22-pu/2,0,Math.PI*2);X.fill();
    // Horns
    X.fillStyle='#a00';X.fillRect(sx-18,sy-30,6,14);X.fillRect(sx+12,sy-30,6,14);
    // Eyes (glowing yellow)
    X.fillStyle='#ff0';X.shadowColor='#ff0';X.shadowBlur=8;X.fillRect(sx-10,sy-12,6,5);X.fillRect(sx+5,sy-12,6,5);X.shadowBlur=0;
    // Torso
    X.fillStyle=fc;X.fillRect(sx-14,sy+8+pu/2,28,20);
  }

  // Health bar above enemy (only if damaged)
  if(e.hp<e.mhp){const bw=32,bh=4;X.fillStyle='rgba(0,0,0,.6)';X.fillRect(sx-bw/2,sy-28,bw,bh);X.fillStyle=e.tp==='boss'?'#f44':'#4a4';X.fillRect(sx-bw/2,sy-28,bw*(e.hp/e.mhp),bh)}
}

function drawPlayer(cx,cy){
  const sx=P.x-cx,sy=P.y-cy;
  
  // Shadow
  X.fillStyle='rgba(0,0,0,.25)';X.beginPath();X.ellipse(sx,sy+14,12,5,0,0,Math.PI*2);X.fill();

  if(P.invT>0&&Math.sin(gt*20)>0)return; // Invincibility flash
  const bc=P.dmgFlash>0?'#f88':'#4af';

  // Body with slight animation
  const bob=Math.abs(Math.sin(P.walkT*Math.PI))*2*(keys['w']||keys['s']||keys['a']||keys['d']?1:0);
  
  // Legs (animated)
  X.fillStyle=bc;X.fillRect(sx-8,sy+14-bob,6,6+bob*.5);X.fillRect(sx+2,sy+14+bob,6,6-bob*.5);

  // Torso
  X.fillStyle=bc;X.fillRect(sx-10,sy+2,20,14);
  
  // Armor/accessory visual - shield charm glow
  if(P.acc.includes('shield_charm')){X.strokeStyle='rgba(100,200,255,.3)';X.lineWidth=2;X.beginPath();X.arc(sx,sy+8,16,0,Math.PI*2);X.stroke()}

  // Head
  X.fillStyle=bc;X.beginPath();X.arc(sx,sy-6-bob*.5,9,0,Math.PI*2);X.fill();
  
  // Eyes (direction based)
  const ex=P.face.x*3,ey=P.face.y*2;
  X.fillStyle='#fff';X.fillRect(sx-4+ex,sy-8+ey-bob*.5,3,3);X.fillRect(sx+1+ex,sy-8+ey-bob*.5,3,3);

  // Weapon visual
  const wp=P.weapons[P.wIdx],wx=sx+P.face.x*16,wy=sy+P.face.y*12;
  
  if(P.atkCD>wp.spd*.5){
    // Attack swing arc
    X.strokeStyle=wp.c;X.lineWidth=3;X.beginPath();
    const sa=Math.atan2(P.face.y,P.face.x)-.8;
    X.arc(sx,sy,wp.rng/2,sa,sa+1.6);X.stroke();
    if(P.wIdx===2){spark(wx,wy,'#f40',2,2,.3);spark(wx,wy,'#fa0',1,1,.2)}
  } else {
    // Idle weapon held at side
    X.fillStyle=wp.c;X.save();X.translate(wx,wy);X.rotate(Math.atan2(P.face.y,P.face.x));
    X.fillRect(0,-2,18,4);X.fillStyle='#654';X.fillRect(-4,-3,6,6);X.restore();
  }

  // Speed boots visual (cyan glow on feet)
  if(P.acc.includes('speed_boots')){X.fillStyle='rgba(0,200,255,.3)';X.fillRect(sx-12,sy+14,8,4);X.fillRect(sx+4,sy+14,8,4)}

  // Gold ring visual (small golden circle on hand)
  if(P.acc.includes('gold_ring')){X.fillStyle='rgba(255,200,0,.4)';X.beginPath();X.arc(wx-3,wy-3,3,0,Math.PI*2);X.fill()}
}

function drawSP(x,y){const p=Math.sin(gt*2)*.3+.7;X.fillStyle=`rgba(100,255,100,${p*.2})`;X.beginPath();X.arc(x,y,20,0,Math.PI*2);X.fill();X.fillStyle=`rgba(100,255,100,${p})`;X.beginPath();X.moveTo(x,y-14);X.lineTo(x+8,y);X.lineTo(x,y+6);X.lineTo(x-8,y);X.closePath();X.fill();X.fillStyle=`rgba(255,255,255,${p*.5})`;X.fillRect(x-1,y-3,2,2)}

function drawShopTile(x,y){const p=Math.sin(gt*3)*.2+.8;X.fillStyle=`rgba(240,192,64,${p})`;X.beginPath();X.moveTo(x,y-16);X.lineTo(x+14,y+4);X.lineTo(x-14,y+4);X.closePath();X.fill();X.fillStyle='#fc0';X.font='bold 12px sans-serif';X.textAlign='center';X.textBaseline='middle';X.fillText('$',x,y-6)}

function drawBossDoor(x,y){const p=Math.sin(gt*4)*.3+.7;X.fillStyle=`rgba(255,50,50,${p*.3})`;X.beginPath();X.arc(x,y,18,0,Math.PI*2);X.fill();X.strokeStyle=`rgba(255,50,50,${p})`;X.lineWidth=3;X.beginPath();X.arc(x,y,14,0,Math.PI*2);X.stroke();X.fillStyle='#f44';X.font='16px serif';X.textAlign='center';X.textBaseline='middle';X.fillText('\u2620',x,y)}

function drawMM(){
  MX.fillStyle='rgba(0,0,0,.8)';MX.fillRect(0,0,130,130);
  const rp=[[45,25],[85,25],[45,60],[125,25],[45,95],[85,95],[85,130],[125,95]];
  const rc=['#8B4513','#228B22','#4a4a6a','#DAA520','#2a2a4a','#6a2aaa','#aa2222','#8B8B4a'];
  MX.strokeStyle='rgba(100,150,255,.3)';MX.lineWidth=1;
  for(const[f,t]of conns){const a=rp[f],b=rp[t];MX.beginPath();MX.moveTo(a[0],a[1]);MX.lineTo(b[0],b[1]);MX.stroke()}
  for(let i=0;i<8;i++){const r=rp[i];MX.fillStyle=P.rooms.has(i)?rc[i]:'rgba(50,50,50,.5)';MX.fillRect(r[0]-12,r[1]-12,24,24);if(P.rooms.has(i)){MX.strokeStyle='rgba(255,255,255,.3)';MX.strokeRect(r[0]-12,r[1]-12,24,24)}}
  const pr=rp[curRoom];if(pr){const pxr=(P.x/(RW*T))*24,pyr=(P.y/(RH*T))*24;MX.fillStyle='#fff';MX.beginPath();MX.arc(pr[0]-12+pxr,pr[1]-12+pyr,3,0,Math.PI*2);MX.fill();MX.fillStyle='rgba(74,170,255,.4)';MX.beginPath();MX.arc(pr[0]-12+pxr,pr[1]-12+pyr,6,0,Math.PI*2);MX.fill()}
}

requestAnimationFrame(loop);
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen3.6-27b-mtp","provider":"lmstudio","persona":"full","sessionId":"519b470d-54df-4e82-8ad1-7099ec6982e2","tokensIn":1743184,"tokensOut":59585,"tokensTotal":1802769,"turns":34,"toolCalls":33,"failedToolCalls":0,"timestamp":"2026-07-29T16:03:48.693Z"} -->