RPG
Qwen 3.6 27b MTP · full

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 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">★</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">⚔ Wooden Sword</div>
<button id="btn-inv" onclick="toggleInv()">👜 [I]</button>
<div id="mm-wrap"><canvas id="mm-canvas" width="130" height="130"></canvas></div>
<div id="dialog"></div>
<div id="shop"><h2>★ Merchant ★</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>👜 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">☆ 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 | Mouse — Aim | Left Click — Attack<br>E — Interact | I — Inventory | 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:'⚔',ok:true},
{n:'Iron Blade',dmg:5,rng:56,spd:.32,c:'#aab',ic:'🔬',ok:false},
{n:'Flame Sword',dmg:7,rng:64,spd:.28,c:'#f80',ic:'🔥',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:'🛀'},
{tp:'coin',x:500,y:300,nm:'Gold Coins',ds:'+10 Gold',val:10,ic:'★'}], // 0 start
[{tp:'speed_boots',x:400,y:250,nm:'Speed Boots',ds:'+20% movement speed',ic:'👠'},
{tp:'coin',x:650,y:350,nm:'Gold Coins',ds:'+15 Gold',val:15,ic:'★'}], // 1 forest
[{tp:'shield_charm',x:400,y:400,nm:'Shield Charm',ds:'Reduces damage by 1',ic:'🛡'},
{tp:'weapon_iron',x:550,y:250,nm:'Iron Blade Upgrade',ds:'Upgrade your sword!',ic:'⚔'}], // 2 cave
[{tp:'gold_ring',x:300,y:450,nm:'Gold Ring',ds:'+25% more gold from enemies',ic:'💏'},
{tp:'coin',x:600,y:200,nm:'Gold Coins',ds:'+20 Gold',val:20,ic:'★'}], // 3 merchant
[{tp:'magic_amulet',x:350,y:350,nm:'Magic Amulet',ds:'+50% ranged damage',ic:'🤖'},
{tp:'weapon_flame',x:600,y:450,nm:'Flame Sword Upgrade',ds:'Fire-infused blade!',ic:'🔥'}], // 4 dungeon
[{tp:'bow_discovery',x:400,y:300,nm:'Crystal Bow',ds:'A powerful ranged weapon!',ic:'🏂'},
{tp:'health_potion',x:650,y:400,nm:'Health Potion',ds:'Restores 2 HP',ic:'🛀'}], // 5 crystal
[], // 6 boss - no items
[{tp:'heart_container',x:400,y:350,nm:'Heart Container',ds:'+2 Max HP!',ic:'❤️'},
{tp:'coin',x:250,y:250,nm:'Gold Coins',ds:'+30 Gold',val:30,ic:'★'}] // 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:'🛀',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:'🛀',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:'⚡',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}★</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}★`,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}★`,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">★</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+=' | 🏂 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"} -->