← RPG results

RPG

thinkingcap qwen3.6 27b · basic

Initial view of RPG

Browser check: Artifact runtime error. This is a technical check, not a quality review.

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 49,183 bytes · SHA-256 0ad296ee30b8
<!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 { display: block; cursor: crosshair; }
#ui-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10; }
#hud { position: absolute; top: 15px; left: 15px; display: flex; align-items: center; gap: 12px; }
.hearts { display: flex; gap: 3px; }
.heart { width: 24px; height: 24px; position: relative; }
.heart svg { width: 100%; height: 100%; }
#money-display { color: #ffd700; font-size: 18px; font-weight: bold; text-shadow: 0 0 8px rgba(255,215,0,0.5); display: flex; align-items: center; gap: 6px; }
#weapon-display { color: #aaa; font-size: 14px; margin-left: 10px; }
#stamina-bar-container { width: 120px; height: 8px; background: rgba(0,0,0,0.6); border-radius: 4px; overflow: hidden; margin-top: 5px; }
#stamina-bar { height: 100%; background: linear-gradient(90deg, #4fc3f7, #29b6f6); width: 100%; transition: width 0.1s; border-radius: 4px; }
#xp-bar-container { width: 150px; height: 6px; background: rgba(0,0,0,0.6); border-radius: 3px; overflow: hidden; margin-top: 3px; }
#xp-bar { height: 100%; background: linear-gradient(90deg, #ab47bc, #7b1fa2); width: 0%; transition: width 0.3s; border-radius: 3px; }
#level-display { color: #ce93d8; font-size: 13px; margin-left: 5px; }
#minimap { position: absolute; top: 15px; right: 15px; width: 140px; height: 140px; border: 2px solid rgba(255,255,255,0.2); border-radius: 8px; overflow: hidden; background: rgba(0,0,0,0.7); }
#minimap canvas { width: 100%; height: 100%; }
#inventory-panel { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(15,15,25,0.95); border: 2px solid #4a4a6a; border-radius: 12px; padding: 20px; display: none; pointer-events: auto; min-width: 420px; max-height: 80vh; overflow-y: auto; }
#inventory-panel h2 { color: #e0e0ff; text-align: center; margin-bottom: 15px; font-size: 20px; letter-spacing: 2px; }
.inv-grid { display: grid; grid-template-columns: repeat(6, 56px); gap: 8px; justify-content: center; }
.inv-slot { width: 56px; height: 56px; background: rgba(30,30,50,0.8); border: 2px solid #3a3a5a; border-radius: 8px; display: flex; align-items: center; justify-content: center; cursor: pointer; position: relative; transition: all 0.15s; }
.inv-slot:hover { border-color: #7c4dff; background: rgba(60,60,90,0.8); transform: scale(1.05); }
.inv-slot .item-icon { font-size: 24px; }
.inv-slot .item-count { position: absolute; bottom: 2px; right: 4px; color: #fff; font-size: 11px; font-weight: bold; text-shadow: 0 0 3px #000; }
#shop-panel { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(15,15,25,0.97); border: 2px solid #ffd700; border-radius: 12px; padding: 25px; display: none; pointer-events: auto; min-width: 400px; }
#shop-panel h2 { color: #ffd700; text-align: center; margin-bottom: 15px; font-size: 22px; letter-spacing: 2px; }
.shop-item { display: flex; align-items: center; justify-content: space-between; padding: 10px 15px; margin: 6px 0; background: rgba(30,30,50,0.7); border-radius: 8px; cursor: pointer; transition: all 0.15s; }
.shop-item:hover { background: rgba(60,60,90,0.8); transform: translateX(5px); }
.shop-item .name { color: #e0e0ff; font-size: 14px; }
.shop-item .desc { color: #888; font-size: 11px; }
.shop-item .price { color: #ffd700; font-weight: bold; font-size: 14px; }
#save-panel { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(15,15,25,0.97); border: 2px solid #4fc3f7; border-radius: 12px; padding: 25px; display: none; pointer-events: auto; text-align: center; }
#save-panel h2 { color: #4fc3f7; margin-bottom: 10px; }
#save-panel p { color: #aaa; margin-bottom: 15px; font-size: 14px; }
.save-btn { padding: 10px 30px; background: linear-gradient(135deg, #4fc3f7, #29b6f6); border: none; border-radius: 8px; color: #fff; font-size: 16px; cursor: pointer; margin: 5px; transition: all 0.2s; }
.save-btn:hover { transform: scale(1.05); box-shadow: 0 0 15px rgba(79,195,247,0.5); }
#message-log { position: absolute; bottom: 15px; left: 15px; max-width: 350px; pointer-events: none; }
.msg { color: #ccc; font-size: 13px; padding: 4px 10px; margin: 2px 0; background: rgba(0,0,0,0.6); border-radius: 4px; animation: msgFade 4s forwards; white-space: nowrap; }
@keyframes msgFade { 0%,70%{opacity:1} 100%{opacity:0} }
#controls-hint { position: absolute; bottom: 15px; right: 15px; color: #555; font-size: 11px; text-align: right; line-height: 1.6; }
#title-screen { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: radial-gradient(ellipse at center, #1a1a2e 0%, #0a0a0f 70%); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 100; }
#title-screen h1 { color: #e0d0ff; font-size: 48px; letter-spacing: 6px; text-shadow: 0 0 30px rgba(124,77,255,0.5); margin-bottom: 10px; }
#title-screen .subtitle { color: #7c4dff; font-size: 18px; letter-spacing: 3px; margin-bottom: 40px; }
.start-btn { padding: 15px 50px; background: linear-gradient(135deg, #7c4dff, #6200ea); border: none; border-radius: 10px; color: #fff; font-size: 20px; cursor: pointer; letter-spacing: 2px; transition: all 0.3s; }
.start-btn:hover { transform: scale(1.08); box-shadow: 0 0 25px rgba(124,77,255,0.6); }
#death-screen { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(20,0,0,0.9); display: none; flex-direction: column; align-items: center; justify-content: center; z-index: 100; }
#death-screen h1 { color: #ff4444; font-size: 48px; margin-bottom: 20px; }
#death-screen p { color: #aaa; margin-bottom: 30px; }
</style>
</head>
<body>

<div id="title-screen">
  <h1>DUNGEON OF ECHOES</h1>
  <div class="subtitle">An Action-Adventure RPG</div>
  <button class="start-btn" onclick="startGame()">BEGIN QUEST</button>
</div>

<div id="death-screen">
  <h1>YOU HAVE FALLEN</h1>
  <p>Your journey ends here...</p>
  <button class="start-btn" onclick="restartGame()">TRY AGAIN</button>
</div>

<canvas id="gameCanvas"></canvas>

<div id="ui-overlay">
  <div id="hud">
    <div class="hearts" id="hearts-container"></div>
    <div id="money-display"><span>🪙</span><span id="money-val">0</span></div>
    <div id="weapon-display">⚔️ Rusty Sword</div>
    <div id="level-display">Lv.1</div>
    <div id="xp-bar-container"><div id="xp-bar"></div></div>
  </div>
  <div id="stamina-bar-container"><div id="stamina-bar"></div></div>
  <div id="minimap"><canvas id="minimapCanvas" width="140" height="140"></canvas></div>
  <div id="inventory-panel">
    <h2>🎒 INVENTORY</h2>
    <div class="inv-grid" id="inv-grid"></div>
  </div>
  <div id="shop-panel">
    <h2>🏪 MERCHANT'S SHOP</h2>
    <div id="shop-items"></div>
    <p style="text-align:center;color:#888;margin-top:10px;font-size:12px;">Press ESC to close</p>
  </div>
  <div id="save-panel">
    <h2>💾 SAVE POINT</h2>
    <p id="save-info">Your progress will be saved here.</p>
    <button class="save-btn" onclick="saveGame()">SAVE GAME</button>
    <button class="save-btn" style="background:linear-gradient(135deg,#66bb6a,#43a047)" onclick="loadGame()" id="load-btn">LOAD GAME</button>
  </div>
  <div id="message-log"></div>
  <div id="controls-hint">WASD - Move | CLICK - Attack<br>I - Inventory | E - Interact<br>SHIFT - Sprint</div>
</div>

<script>
// ==================== AUDIO ENGINE ====================
const AudioEngine = {
  ctx: null,
  init() {
    this.ctx = new (window.AudioContext || window.webkitAudioContext)();
  },
  play(type, vol = 0.3) {
    if (!this.ctx) return;
    const t = this.ctx.currentTime;
    const g = this.ctx.createGain();
    g.connect(this.ctx.destination);
    g.gain.setValueAtTime(vol, t);

    switch(type) {
      case 'walk': {
        const o = this.ctx.createOscillator();
        o.type = 'sine'; o.frequency.setValueAtTime(80 + Math.random()*40, t);
        g.gain.exponentialRampToValueAtTime(0.001, t+0.06);
        o.connect(g); o.start(t); o.stop(t+0.06);
        break;
      }
      case 'attack': {
        const o = this.ctx.createOscillator();
        o.type = 'sawtooth'; o.frequency.setValueAtTime(300, t);
        o.frequency.exponentialRampToValueAtTime(80, t+0.15);
        g.gain.exponentialRampToValueAtTime(0.001, t+0.15);
        o.connect(g); o.start(t); o.stop(t+0.15);
        break;
      }
      case 'attack2': {
        const o = this.ctx.createOscillator();
        o.type = 'square'; o.frequency.setValueAtTime(400, t);
        o.frequency.exponentialRampToValueAtTime(60, t+0.2);
        g.gain.exponentialRampToValueAtTime(0.001, t+0.2);
        o.connect(g); o.start(t); o.stop(t+0.2);
        break;
      }
      case 'attack3': {
        const buf = this.ctx.createBufferSource();
        const b = this.ctx.createBuffer(1, this.ctx.sampleRate*0.3, this.ctx.sampleRate);
        const d = b.getChannelData(0);
        for(let i=0;i<d.length;i++) d[i]=(Math.random()*2-1)*Math.exp(-i/d.length*4);
        buf.buffer=b; g.gain.exponentialRampToValueAtTime(0.001,t+0.3);
        buf.connect(g); buf.start(t);
        break;
      }
      case 'ranged': {
        const o = this.ctx.createOscillator();
        o.type='sine'; o.frequency.setValueAtTime(800,t);
        o.frequency.exponentialRampToValueAtTime(200,t+0.12);
        g.gain.exponentialRampToValueAtTime(0.001,t+0.12);
        o.connect(g); o.start(t); o.stop(t+0.12);
        break;
      }
      case 'hit': {
        const buf = this.ctx.createBufferSource();
        const b = this.ctx.createBuffer(1, this.ctx.sampleRate*0.15, this.ctx.sampleRate);
        const d = b.getChannelData(0);
        for(let i=0;i<d.length;i++) d[i]=(Math.random()*2-1)*Math.exp(-i/d.length*6);
        buf.buffer=b; g.gain.setValueAtTime(vol*1.5,t);
        g.gain.exponentialRampToValueAtTime(0.001,t+0.15);
        buf.connect(g); buf.start(t);
        break;
      }
      case 'pickup': {
        const o = this.ctx.createOscillator();
        o.type='sine'; o.frequency.setValueAtTime(400,t);
        o.frequency.linearRampToValueAtTime(800,t+0.1);
        g.gain.exponentialRampToValueAtTime(0.001,t+0.2);
        o.connect(g); o.start(t); o.stop(t+0.2);
        break;
      }
      case 'coin': {
        const o = this.ctx.createOscillator();
        o.type='sine'; o.frequency.setValueAtTime(1200,t);
        g.gain.exponentialRampToValueAtTime(0.001,t+0.15);
        o.connect(g); o.start(t); o.stop(t+0.15);
        const o2=this.ctx.createOscillator();
        const g2=this.ctx.createGain(); g2.connect(this.ctx.destination);
        o2.type='sine'; o2.frequency.setValueAtTime(1600,t+0.08);
        g2.gain.setValueAtTime(vol*0.5,t+0.08);
        g2.gain.exponentialRampToValueAtTime(0.001,t+0.25);
        o2.connect(g2); o2.start(t+0.08); o2.stop(t+0.25);
        break;
      }
      case 'shop': {
        const o = this.ctx.createOscillator();
        o.type='triangle'; o.frequency.setValueAtTime(600,t);
        o.frequency.linearRampToValueAtTime(900,t+0.15);
        g.gain.exponentialRampToValueAtTime(0.001,t+0.3);
        o.connect(g); o.start(t); o.stop(t+0.3);
        break;
      }
      case 'save': {
        [400,500,600].forEach((f,i)=>{
          const o=this.ctx.createOscillator();const g2=this.ctx.createGain();g2.connect(this.ctx.destination);
          o.type='sine';o.frequency.setValueAtTime(f,t+i*0.12);
          g2.gain.setValueAtTime(vol*0.4,t+i*0.12);
          g2.gain.exponentialRampToValueAtTime(0.001,t+i*0.12+0.2);
          o.connect(g2);o.start(t+i*0.12);o.stop(t+i*0.12+0.2);
        });
        break;
      }
      case 'levelup': {
        [523,659,784,1047].forEach((f,i)=>{
          const o=this.ctx.createOscillator();const g2=this.ctx.createGain();g2.connect(this.ctx.destination);
          o.type='sine';o.frequency.setValueAtTime(f,t+i*0.15);
          g2.gain.setValueAtTime(vol*0.4,t+i*0.15);
          g2.gain.exponentialRampToValueAtTime(0.001,t+i*0.15+0.3);
          o.connect(g2);o.start(t+i*0.15);o.stop(t+i*0.15+0.3);
        });
        break;
      }
      case 'death': {
        const o=this.ctx.createOscillator();o.type='sawtooth';
        o.frequency.setValueAtTime(400,t);o.frequency.exponentialRampToValueAtTime(50,t+1);
        g.gain.exponentialRampToValueAtTime(0.001,t+1);
        o.connect(g);o.start(t);o.stop(t+1);
        break;
      }
    }
  }
};

// ==================== PARTICLE SYSTEM ====================
class Particle {
  constructor(x,y,vx,vy,life,color,size,type='circle') {
    this.x=x;this.y=y;this.vx=vx;this.vy=vy;this.life=life;this.maxLife=life;
    this.color=color;this.size=size;this.type=type;this.rotation=Math.random()*Math.PI*2;
  }
  update(dt) {
    this.x+=this.vx*dt;this.y+=this.vy*dt;
    this.life-=dt;this.rotation+=3*dt;
    if(this.type==='spark') this.vy+=200*dt;
    return this.life>0;
  }
  draw(ctx,camX,camY) {
    const a=Math.max(0,this.life/this.maxLife);
    ctx.globalAlpha=a;
    ctx.fillStyle=this.color;
    if(this.type==='circle'||this.type==='spark') {
      ctx.beginPath();ctx.arc(this.x-camX,this.y-camY,this.size*a,0,Math.PI*2);ctx.fill();
    } else if(this.type==='square') {
      ctx.save();ctx.translate(this.x-camX,this.y-camY);ctx.rotate(this.rotation);
      ctx.fillRect(-this.size/2,-this.size/2,this.size*a,this.size*a);ctx.restore();
    }
    ctx.globalAlpha=1;
  }
}

const particles = [];
function emitParticles(x,y,count,color,speed=100,life=0.5,size=4,type='circle') {
  for(let i=0;i<count;i++) {
    const angle=Math.random()*Math.PI*2;
    const spd=speed*(0.3+Math.random()*0.7);
    particles.push(new Particle(x,y,Math.cos(angle)*spd,Math.sin(angle)*spd,life*(0.5+Math.random()*0.5),color,size*(0.5+Math.random()),type));
  }
}

// ==================== GAME CONSTANTS ====================
const TILE = 32;
const MAP_W = 80, MAP_H = 60;
const VIEW_TILES_X = Math.ceil(window.innerWidth/TILE)+2;
const VIEW_TILES_Y = Math.ceil(window.innerHeight/TILE)+2;

// Tile types
const T = { VOID:0, FLOOR:1, WALL:2, DOOR:3, SAVE_POINT:4, SHOP:5, CHEST:6, STAIRS:7, PILLAR:8 };

// ==================== MAP GENERATION ====================
let map = [];
let rooms = [];
let exploredMap = [];

function generateMap() {
  map = Array.from({length:MAP_H}, ()=>Array(MAP_W).fill(T.VOID));
  exploredMap = Array.from({length:MAP_H}, ()=>new Uint8Array(MAP_W));
  rooms = [];

  // Generate rooms using BSP-like approach
  const roomCount = 18 + Math.floor(Math.random()*6);
  for(let attempt=0;attempt<200&&rooms.length<roomCount;attempt++) {
    const w=5+Math.floor(Math.random()*8);
    const h=4+Math.floor(Math.random()*6);
    const x=3+Math.floor(Math.random()*(MAP_W-w-6));
    const y=3+Math.floor(Math.random()*(MAP_H-h-6));
    let overlap=false;
    for(const r of rooms) {
      if(x<r.x+r.w+2&&x+w+2>r.y&&y<r.y+r.h+2&&y+h+2>r.y) { overlap=true;break; }
    }
    if(overlap) continue;
    const room={x,y,w,h,cx:Math.floor(x+w/2),cy:Math.floor(y+h/2)};
    rooms.push(room);
    for(let ry=y;ry<y+h;ry++) for(let rx=x;rx<x+w;rx++) map[ry][rx]=T.FLOOR;
  }

  // Connect rooms with corridors
  for(let i=1;i<rooms.length;i++) {
    let cx=rooms[i-1].cx, cy=rooms[i-1].cy;
    const tx=rooms[i].cx, ty=rooms[i].cy;
    while(cx!==tx) { map[cy][cx]=T.FLOOR; cx+=cx<tx?1:-1; }
    while(cy!==ty) { map[cy][cx]=T.FLOOR; cy+=cy<ty?1:-1; }
  }

  // Place walls around floor tiles
  for(let y=0;y<MAP_H;y++) for(let x=0;x<MAP_W;x++) {
    if(map[y][x]===T.VOID) {
      let adj=false;
      for(let dy=-1;dy<=1&&!adj;dy++) for(let dx=-1;dx<=1&&!adj;dx++) {
        const ny=y+dy,nx=x+dx;
        if(ny>=0&&ny<MAP_H&&nx>=0&&nx<MAP_W&&map[ny][nx]===T.FLOOR) adj=true;
      }
      if(adj) map[y][x]=T.WALL;
    }
  }

  // Place special tiles in rooms
  const shuffled = [...rooms].sort(()=>Math.random()-0.5);
  // Save points (3 of them)
  for(let i=0;i<3&&i<shuffled.length;i++) {
    const r=shuffled[i]; map[r.cy][r.cx]=T.SAVE_POINT;
  }
  // Shop
  if(shuffled.length>3) { map[shuffled[3].cy][shuffled[3].cx]=T.SHOP; }
  // Chests (5+)
  for(let i=4;i<Math.min(12,shuffled.length);i++) {
    const r=shuffled[i]; map[r.cy][r.cx]=T.CHEST;
  }
}

// ==================== ITEMS & INVENTORY ====================
const ITEM_DEFS = [
  {id:'health_potion',name:'Health Potion',icon:'🧪',desc:'Restores 2 HP',type:'consumable',price:15,stack:true},
  {id:'stamina_elixir',name:'Stamina Elixir',icon:'⚡',desc:'Full stamina restore',type:'consumable',price:10,stack:true},
  {id:'iron_shield',name:'Iron Shield',icon:'🛡️',desc:'+1 max HP permanently',type:'accessory',price:50,stack:false,effect:{hp:2}},
  {id:'speed_boots',name:'Speed Boots',icon:'👢',desc:'+20% move speed',type:'accessory',price:40,stack:false,effect:{speed:0.2}},
  {id:'lucky_charm',name:'Lucky Charm',icon:'🍀',desc:'+50% gold find',type:'accessory',price:35,stack:false,effect:{goldBonus:0.5}},
  {id:'fire_gem',name:'Fire Gem',icon:'💎',desc:'+2 weapon damage',type:'accessory',price:45,stack:false,effect:{damage:2}},
  {id:'mana_crystal',name:'Mana Crystal',icon:'🔮',desc:'Unlocks ranged power',type:'key',price:0,stack:false},
];

let inventory = [];
function addItem(itemId, count=1) {
  const def = ITEM_DEFS.find(i=>i.id===itemId);
  if(!def) return;
  if(def.stack) {
    const existing = inventory.find(it=>it.id===itemId);
    if(existing) { existing.count+=count; } else { inventory.push({...def,count}); }
  } else {
    if(inventory.find(it=>it.id===itemId)) return; // already have it
    inventory.push({...def,count:1});
  }
}

function useItem(index) {
  const item = inventory[index];
  if(!item || !item.type.startsWith('consumable')) return false;
  switch(item.id) {
    case 'health_potion': player.heal(2); emitParticles(player.x,player.y,15,'#ff4444',80,0.6,5,'spark'); break;
    case 'stamina_elixir': player.stamina=player.maxStamina; emitParticles(player.x,player.y,15,'#4fc3f7',80,0.6,5,'spark'); break;
  }
  item.count--;
  if(item.count<=0) inventory.splice(index,1);
  AudioEngine.play('pickup');
  return true;
}

// ==================== PLAYER ====================
const player = {
  x:0,y:0,vx:0,vy:0,
  hp:3,maxHp:3,
  stamina:100,maxStamina:100,
  speed:120,baseSpeed:120,
  damage:5,baseDamage:5,
  weaponLevel:0, // 0=base, 1=upgrade1, 2=upgrade2
  hasRanged:false,
  money:0,
  xp:0,xpToNext:30,level:1,
  attackCooldown:0,attackSpeed:0.4,
  facing:{x:1,y:0},
  invincible:0,
  animFrame:0,animTimer:0,
  walkSoundTimer:0,
  equipment:{weapon:'Rusty Sword',shield:null,boots:null,charm:null,gem:null},

  heal(amount) { this.hp=Math.min(this.maxHp,this.hp+amount); },
  takeDamage(amount) {
    if(this.invincible>0) return;
    this.hp-=amount;
    this.invincible=0.5;
    emitParticles(this.x,this.y,12,'#ff0000',120,0.4,4,'spark');
    AudioEngine.play('hit',0.4);
    if(this.hp<=0) { this.hp=0; onPlayerDeath(); }
  },

  gainXP(amount) {
    this.xp+=amount;
    while(this.xp>=this.xpToNext) {
      this.xp-=this.xpToNext;
      this.level++;
      this.xpToNext=Math.floor(this.xpToNext*1.5);
      this.maxHp+=2;this.hp=this.maxHp;
      this.baseDamage+=1;this.damage=this.baseDamage+this.getAccessoryBonus('damage');
      AudioEngine.play('levelup',0.3);
      addMessage(`Level Up! Now level ${this.level}`,'#ce93d8');
      emitParticles(this.x,this.y,30,'#ce93d8',150,1,6,'spark');
    }
  },

  getAccessoryBonus(stat) {
    let bonus=0;
    for(const it of inventory) { if(it.effect&&it.effect[stat]) bonus+=it.effect[stat]; }
    return bonus;
  },

  getGoldMultiplier() { return 1+this.getAccessoryBonus('goldBonus'); },
  getSpeedMultiplier() { return 1+this.getAccessoryBonus('speed'); },

  update(dt) {
    // Movement
    let mx=0,my=0;
    if(keys['w']||keys['arrowup']) my=-1;
    if(keys['s']||keys['arrowdown']) my=1;
    if(keys['a']||keys['arrowleft']) mx=-1;
    if(keys['d']||keys['arrowright']) mx=1;

    const sprinting = keys['shift']&&this.stamina>0&&(mx!==0||my!==0);
    const spd = this.speed * this.getSpeedMultiplier() * (sprinting?1.6:1);

    if(sprinting) this.stamina=Math.max(0,this.stamina-30*dt);
    else this.stamina=Math.min(this.maxStamina,this.stamina+20*dt);

    const len=Math.sqrt(mx*mx+my*my)||1;
    mx/=len; my/=len;

    if(mx!==0||my!==0) {
      this.facing={x:mx,y:my};
      this.animTimer+=dt*(sprinting?12:8);
      if(this.animTimer>1) { this.animFrame=(this.animFrame+1)%4; this.animTimer=0; }
    } else {
      this.animFrame=0;
    }

    const nx=this.x+mx*spd*dt;
    const ny=this.y+my*spd*dt;

    if(!isWall(nx,this.y)) this.x=nx;
    if(!isWall(this.x,ny)) this.y=ny;

    // Walk sound
    if(mx!==0||my!==0) {
      this.walkSoundTimer+=dt;
      const interval=sprinting?0.2:0.35;
      if(this.walkSoundTimer>interval) { AudioEngine.play('walk',0.1); this.walkSoundTimer=0; }
    }

    // Attack cooldown
    this.attackCooldown=Math.max(0,this.attackCooldown-dt);
    this.invincible=Math.max(0,this.invincible-dt);

    // Explore map
    const px=Math.floor(this.x/TILE), py=Math.floor(this.y/TILE);
    for(let dy=-5;dy<=5;dy++) for(let dx=-5;dx<=5;dx++) {
      const ty=py+dy,tx=px+dx;
      if(ty>=0&&ty<MAP_H&&tx>=0&&tx<MAP_W) exploredMap[ty][tx]=1;
    }

    // Check tile interactions
    this.checkTileInteractions();
  },

  checkTileInteractions() {
    const tx=Math.floor(this.x/TILE), ty=Math.floor(this.y/TILE);
    if(tx<0||tx>=MAP_W||ty<0||ty>=MAP_H) return;
    const tile=map[ty][tx];
    if(tile===T.CHEST) {
      map[ty][tx]=T.FLOOR;
      this.openChest();
    } else if(tile===T.SAVE_POINT) {
      showSavePanel();
    } else if(tile===T.SHOP) {
      openShop();
    }
  },

  openChest() {
    AudioEngine.play('pickup',0.4);
    emitParticles(this.x,this.y,20,'#ffd700',100,0.8,5,'spark');
    const gold = Math.floor((10+Math.random()*30)*this.getGoldMultiplier());
    this.money+=gold;
    addMessage(`Found ${gold} gold!`,'#ffd700');

    // Random item drop
    if(Math.random()<0.5) {
      const possibleItems = ['health_potion','stamina_elixir'];
      const itemId = possibleItems[Math.floor(Math.random()*possibleItems.length)];
      addItem(itemId);
      addMessage(`Found ${ITEM_DEFS.find(i=>i.id===itemId).name}!`,'#4fc3f7');
    }

    // Weapon upgrade drops at certain points
    if(this.weaponLevel<1 && this.level>=2) {
      this.weaponLevel=1;
      this.equipment.weapon='Iron Blade';
      this.baseDamage+=3;this.damage=this.baseDamage+this.getAccessoryBonus('damage');
      this.attackSpeed=0.35;
      addMessage('Weapon upgraded to Iron Blade!','#ff9800');
      emitParticles(this.x,this.y,25,'#ff9800',150,1,6,'spark');
    } else if(this.weaponLevel<2 && this.level>=4) {
      this.weaponLevel=2;
      this.equipment.weapon='Flame Sword';
      this.baseDamage+=4;this.damage=this.baseDamage+this.getAccessoryBonus('damage');
      this.attackSpeed=0.3;
      addMessage('Weapon upgraded to Flame Sword!','#ff5722');
      emitParticles(this.x,this.y,30,'#ff5722',180,1.2,7,'spark');
    }

    // Ranged weapon discovery mid-game (level 3+)
    if(!this.hasRanged && this.level>=3) {
      this.hasRanged=true;
      addMessage('Discovered ranged attack! Hold SHIFT+CLICK','#9c27b0');
      emitParticles(this.x,this.y,25,'#9c27b0',120,1,5,'spark');
    }

    // Health upgrade
    if(this.maxHp<5 && Math.random()<0.3) {
      this.maxHp=Math.min(5,this.maxHp+2);
      this.hp=this.maxHp;
      addMessage('Max HP increased!','#ff4444');
      emitParticles(this.x,this.y,15,'#ff4444',80,0.6,5,'spark');
    }
  },

  attack() {
    if(this.attackCooldown>0) return;
    this.attackCooldown=this.attackSpeed;

    const isRanged = keys['shift'] && this.hasRanged;

    if(isRanged) {
      AudioEngine.play('ranged',0.3);
      projectiles.push({
        x:this.x,y:this.y,
        vx:this.facing.x*400,vy:this.facing.y*400,
        damage:this.damage,life:1.5,color:'#9c27b0'
      });
      emitParticles(this.x+this.facing.x*16,this.y+this.facing.y*16,5,'#9c27b0',60,0.3,3,'spark');
    } else {
      const soundType = this.weaponLevel===0?'attack':this.weaponLevel===1?'attack2':'attack3';
      AudioEngine.play(soundType,0.3);

      const range = 40 + this.weaponLevel*15;
      const arc = Math.PI/2;
      const fx=this.facing.x, fy=this.facing.y;

      // Melee hitbox check
      for(const e of enemies) {
        if(e.hp<=0) continue;
        const dx=e.x-this.x, dy=e.y-this.y;
        const dist=Math.sqrt(dx*dx+dy*dy);
        if(dist>range) continue;
        const dot=(dx*fx+dy*fy)/dist;
        if(dot<Math.cos(arc/2)) continue;

        e.takeDamage(this.damage);
        emitParticles(e.x,e.y,8,this.weaponLevel===2?'#ff5722':'#fff',100,0.4,4,'spark');
      }

      // Swing visual
      emitParticles(
        this.x+fx*30,this.y+fy*30,
        6,'#ffffff',80,0.2,3,'circle'
      );
    }
  },

  draw(ctx,camX,camY) {
    const sx=this.x-camX, sy=this.y-camY;
    const bob = Math.sin(this.animTimer*Math.PI*2)*2*(this.animFrame!==0?1:0);

    ctx.save();
    ctx.translate(sx,sy+bob);

    // Shadow
    ctx.fillStyle='rgba(0,0,0,0.3)';
    ctx.beginPath();ctx.ellipse(0,14,12,5,0,0,Math.PI*2);ctx.fill();

    // Body color based on equipment
    let bodyColor='#6a8caf';
    if(this.equipment.shield) bodyColor='#7c9ab5';
    if(this.equipment.boots) bodyColor='#8aaecf';

    // Legs
    const legSwing = Math.sin(this.animTimer*Math.PI*2)*4*(this.animFrame!==0?1:0);
    ctx.fillStyle='#3a4a5a';
    ctx.fillRect(-6,6,5,10+legSwing/2);
    ctx.fillRect(1,6,5,10-legSwing/2);

    // Boots visual
    if(this.equipment.boots) {
      ctx.fillStyle='#8d6e63';
      ctx.fillRect(-7,14+Math.max(0,legSwing/2),7,4);
      ctx.fillRect(0,14-Math.max(0,-legSwing/2),7,4);
    }

    // Body
    ctx.fillStyle=bodyColor;
    ctx.fillRect(-8,-6,16,14);

    // Shield visual
    if(this.equipment.shield) {
      ctx.fillStyle='#9e9e9e';
      ctx.fillRect(8,-4,5,10);
      ctx.strokeStyle='#757575';ctx.lineWidth=1;
      ctx.strokeRect(8,-4,5,10);
    }

    // Head
    ctx.fillStyle='#f5d6b8';
    ctx.fillRect(-5,-16,10,12);

    // Eyes
    const eyeOffX = this.facing.x*2;
    const eyeOffY = this.facing.y*1;
    ctx.fillStyle='#333';
    ctx.fillRect(-2+eyeOffX,-12+eyeOffY,2,2);
    ctx.fillRect(2+eyeOffX,-12+eyeOffY,2,2);

    // Weapon
    const weaponAngle = Math.atan2(this.facing.y,this.facing.x);
    ctx.save();
    ctx.translate(this.facing.x*8,this.facing.y*6);
    ctx.rotate(weaponAngle + (this.attackCooldown>0?-0.5:0));

    if(this.weaponLevel===0) {
      // Rusty sword - brownish
      ctx.fillStyle='#a1887f';ctx.fillRect(-2,-14,4,16);
      ctx.fillStyle='#795548';ctx.fillRect(-4,-2,8,3);
    } else if(this.weaponLevel===1) {
      // Iron blade - silver
      ctx.fillStyle='#cfd8dc';ctx.fillRect(-2.5,-16,5,18);
      ctx.fillStyle='#90a4ae';ctx.fillRect(-5,-2,10,3);
      ctx.fillStyle='#b0bec5';ctx.fillRect(-1,-17,3,3);
    } else {
      // Flame sword - red/orange with glow
      ctx.shadowColor='#ff5722';ctx.shadowBlur=8;
      ctx.fillStyle='#ff7043';ctx.fillRect(-2.5,-18,5,20);
      ctx.fillStyle='#ffab91';ctx.fillRect(-1,-19,3,4);
      ctx.shadowBlur=0;
    }
    ctx.restore();

    // Invincibility flash
    if(this.invincible>0 && Math.floor(this.invincible*10)%2===0) {
      ctx.globalAlpha=0.5;
      ctx.fillStyle='#fff';ctx.fillRect(-8,-16,16,30);
      ctx.globalAlpha=1;
    }

    // Lucky charm visual
    if(this.equipment.charm) {
      ctx.fillStyle='#4caf50';
      ctx.beginPath();ctx.arc(0,-10,2,0,Math.PI*2);ctx.fill();
    }

    ctx.restore();
  }
};

// ==================== ENEMIES ====================
let enemies = [];

const ENEMY_TYPES = {
  slime: {hp:8,speed:35,damage:2,xp:10,color:'#4caf50',size:10,attackSpeed:1.2},
  skeleton: {hp:15,speed:50,damage:4,xp:20,color:'#e0e0e0',size:12,attackSpeed:0.9},
  bat: {hp:6,speed:70,damage:3,xp:12,color:'#7b1fa2',size:8,attackSpeed:0.7},
  ghost: {hp:12,speed:45,damage:5,xp:25,color:'#90caf9',size:11,attackSpeed:1.0},
  golem: {hp:30,speed:25,damage:6,xp:40,color:'#8d6e63',size:16,attackSpeed:1.5},
};

function spawnEnemies() {
  enemies = [];
  for(const room of rooms) {
    if(room===rooms[0]) continue; // Don't spawn in starting room
    const count = 1+Math.floor(Math.random()*3);
    for(let i=0;i<count;i++) {
      const types = Object.keys(ENEMY_TYPES);
      let typeIdx = Math.min(types.length-1, Math.floor(Math.random()*(1+rooms.indexOf(room)/rooms.length*2)));
      const type = ENEMY_TYPES[types[typeIdx]];
      enemies.push({
        x: room.x*TILE + (Math.random()*room.w)*TILE,
        y: room.y*TILE + (Math.random()*room.h)*TILE,
        hp:type.hp, maxHp:type.hp, speed:type.speed, damage:type.damage,
        xp:type.xp, color:type.color, size:type.size, attackSpeed:type.attackSpeed,
        type:types[typeIdx],
        attackCooldown:0, aggroRange:120+Math.random()*60,
        state:'idle', alertTimer:0, hitFlash:0,
        vx:0,vy:0, animTimer:Math.random()*Math.PI*2
      });
    }
  }
}

function updateEnemies(dt) {
  for(const e of enemies) {
    if(e.hp<=0) continue;
    const dx=player.x-e.x, dy=player.y-e.y;
    const dist=Math.sqrt(dx*dx+dy*dy);

    e.animTimer+=dt*3;
    e.hitFlash=Math.max(0,e.hitFlash-dt);
    e.attackCooldown=Math.max(0,e.attackCooldown-dt);

    if(dist<e.aggroRange) {
      e.state='chase';
      const len=dist||1;
      const nx=dx/len, ny=dy/len;
      const spd=e.speed*(player.stamina>50?1:1.2); // Slightly faster if player is sprinting

      const ex=e.x+nx*spd*dt, ey=e.y+ny*spd*dt;
      if(!isWall(ex,e.y)) e.x=ex;
      if(!isWall(e.x,ey)) e.y=ey;

      // Attack player
      if(dist<25 && e.attackCooldown<=0) {
        e.attackCooldown=e.attackSpeed;
        player.takeDamage(e.damage);
        emitParticles(player.x,player.y,6,'#ff4444',80,0.3,3,'spark');
      }
    } else {
      e.state='idle';
      // Wander
      if(Math.random()<0.02) {
        const angle=Math.random()*Math.PI*2;
        e.vx=Math.cos(angle)*e.speed*0.3;
        e.vy=Math.sin(angle)*e.speed*0.3;
      }
      const wx=e.x+e.vx*dt, wy=e.y+e.vy*dt;
      if(!isWall(wx,e.y)) e.x=wx;
      if(!isWall(e.x,wy)) e.y=wy;
    }
  }
}

function drawEnemies(ctx,camX,camY) {
  for(const e of enemies) {
    if(e.hp<=0) continue;
    const sx=e.x-camX, sy=e.y-camY;
    if(sx<-50||sx>window.innerWidth+50||sy<-50||sy>window.innerHeight+50) continue;

    ctx.save();
    ctx.translate(sx,sy);

    // Shadow
    ctx.fillStyle='rgba(0,0,0,0.3)';
    ctx.beginPath();ctx.ellipse(0,e.size*0.8,e.size*0.7,e.size*0.25,0,0,Math.PI*2);ctx.fill();

    const bob=Math.sin(e.animTimer)*2;

    if(e.type==='slime') {
      // Slime - squishy blob
      ctx.fillStyle=e.hitFlash>0?'#fff':e.color;
      ctx.beginPath();
      ctx.ellipse(0,bob,e.size, e.size*0.7+Math.sin(e.animTimer*2)*2, 0, 0, Math.PI*2);
      ctx.fill();
      // Eyes
      ctx.fillStyle='#333';
      ctx.fillRect(-4,-4+bob,3,3);ctx.fillRect(2,-4+bob,3,3);
    } else if(e.type==='skeleton') {
      // Skeleton - bony figure
      ctx.fillStyle=e.hitFlash>0?'#fff':e.color;
      ctx.fillRect(-6,-12+bob,12,18);
      ctx.fillRect(-4,-16+bob,8,6);
      ctx.fillStyle='#333';
      ctx.fillRect(-3,-15+bob,2,2);ctx.fillRect(1,-15+bob,2,2);
      // Arms
      ctx.fillRect(-10,-8+bob,4,10);ctx.fillRect(6,-8+bob,4,10);
    } else if(e.type==='bat') {
      // Bat - small with wings
      const wingFlap=Math.sin(e.animTimer*5)*8;
      ctx.fillStyle=e.hitFlash>0?'#fff':e.color;
      ctx.beginPath();ctx.ellipse(0,bob,6,4,0,0,Math.PI*2);ctx.fill();
      // Wings
      ctx.fillRect(-14,-2+bob+wingFlap,8,3);
      ctx.fillRect(6,-2+bob-wingFlap,8,3);
    } else if(e.type==='ghost') {
      // Ghost - translucent
      ctx.globalAlpha=0.7;
      ctx.fillStyle=e.hitFlash>0?'#fff':e.color;
      ctx.beginPath();ctx.arc(0,bob-4,e.size,Math.PI,0);
      ctx.lineTo(e.size,e.size+bob);
      for(let i=0;i<4;i++) {
        const wx=e.size-i*(e.size*2/3);
        ctx.quadraticCurveTo(wx+e.size/6,e.size-4+bob,wx,e.size+bob);
      }
      ctx.fill();
      ctx.globalAlpha=1;
      ctx.fillStyle='#333';
      ctx.fillRect(-4,-6+bob,3,3);ctx.fillRect(2,-6+bob,3,3);
    } else if(e.type==='golem') {
      // Golem - big rock creature
      ctx.fillStyle=e.hitFlash>0?'#fff':e.color;
      ctx.fillRect(-10,-14+bob,20,24);
      ctx.fillRect(-8,-18+bob,16,6);
      ctx.fillStyle='#5d4037';
      ctx.fillRect(-5,-17+bob,3,3);ctx.fillRect(2,-17+bob,3,3);
      // Cracks
      ctx.strokeStyle='#4e342e';ctx.lineWidth=1;
      ctx.beginPath();ctx.moveTo(-3,-8+bob);ctx.lineTo(2,0+bob);ctx.stroke();
    }

    // Health bar
    if(e.hp<e.maxHp) {
      const bw=e.size*2;
      ctx.fillStyle='rgba(0,0,0,0.5)';
      ctx.fillRect(-bw/2,-e.size-10+bob,bw,4);
      ctx.fillStyle='#f44336';
      ctx.fillRect(-bw/2,-e.size-10+bob,bw*(e.hp/e.maxHp),4);
    }

    ctx.restore();
  }
}

// ==================== PROJECTILES ====================
let projectiles = [];

function updateProjectiles(dt) {
  for(let i=projectiles.length-1;i>=0;i--) {
    const p=projectiles[i];
    p.x+=p.vx*dt; p.y+=p.vy*dt;
    p.life-=dt;

    if(isWall(p.x,p.y)||p.life<=0) {
      emitParticles(p.x,p.y,5,p.color,60,0.3,3,'spark');
      projectiles.splice(i,1); continue;
    }

    for(const e of enemies) {
      if(e.hp<=0) continue;
      const dx=e.x-p.x, dy=e.y-p.y;
      if(Math.sqrt(dx*dx+dy*dy)<e.size+5) {
        e.takeDamage(p.damage);
        emitParticles(e.x,e.y,8,p.color,100,0.4,4,'spark');
        projectiles.splice(i,1); break;
      }
    }
  }
}

function drawProjectiles(ctx,camX,camY) {
  for(const p of projectiles) {
    ctx.fillStyle=p.color;
    ctx.shadowColor=p.color;ctx.shadowBlur=6;
    ctx.beginPath();ctx.arc(p.x-camX,p.y-camY,4,0,Math.PI*2);ctx.fill();
    ctx.shadowBlur=0;
  }
}

// ==================== ENEMY DAMAGE ====================
function EnemyTakeDamage(amount) {
  // Already handled in enemy object
}

enemies.prototype.takeDamage = function(amount) {};
// Fix: add takeDamage to enemy objects after creation
Object.defineProperty(Object.getPrototypeOf({}), 'takeDamage', {value:function(){} });

// We'll handle it inline instead - enemies are plain objects
function damageEnemy(e, amount) {
  e.hp-=amount;
  e.hitFlash=0.15;
  if(e.hp<=0) {
    player.gainXP(e.xp);
    const gold=Math.floor((3+Math.random()*8)*player.getGoldMultiplier());
    player.money+=gold;
    emitParticles(e.x,e.y,20,e.color,120,0.6,5,'spark');
    // Drop coins visual
    for(let i=0;i<3;i++) {
      setTimeout(()=>emitParticles(e.x+Math.random()*20-10,e.y,'#ffd700',40,0.4,3,'square'),i*50);
    }
  }
}

// Patch the enemy objects
function patchEnemies() {
  for(const e of enemies) {
    e.takeDamage = function(amount) { damageEnemy(this, amount); };
  }
}

// ==================== MAP HELPERS ====================
function isWall(x,y) {
  const tx=Math.floor(x/TILE), ty=Math.floor(y/TILE);
  if(tx<0||tx>=MAP_W||ty<0||ty>=MAP_H) return true;
  const t=map[ty][tx];
  return t===T.WALL || t===T.VOID || t===T.PILLAR;
}

// ==================== CAMERA ====================
let camX=0, camY=0;

function updateCamera() {
  const targetX = player.x - window.innerWidth/2;
  const targetY = player.y - window.innerHeight/2;
  camX += (targetX-camX)*0.1;
  camY += (targetY-camY)*0.1;
}

// ==================== RENDERING ====================
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const mmCanvas = document.getElementById('minimapCanvas');
const mmCtx = mmCanvas.getContext('2d');

function resize() {
  canvas.width=window.innerWidth;
  canvas.height=window.innerHeight;
}
window.addEventListener('resize', resize);
resize();

const TILE_COLORS = {
  [T.VOID]: '#0a0a0f',
  [T.FLOOR]: '#2a2a3e',
  [T.WALL]: '#1a1a2e',
  [T.DOOR]: '#4a3a2a',
  [T.SAVE_POINT]: '#1a3a4a',
  [T.SHOP]: '#3a2a1a',
  [T.CHEST]: '#2a2a3e',
  [T.PILLAR]: '#2a2a3e',
};

function drawMap() {
  const startX=Math.floor(camX/TILE)-1;
  const startY=Math.floor(camY/TILE)-1;
  const endX=startX+VIEW_TILES_X+2;
  const endY=startY+VIEW_TILES_Y+2;

  for(let ty=startY;ty<endY;ty++) {
    for(let tx=startX;tx<endX;tx++) {
      if(tx<0||tx>=MAP_W||ty<0||ty>=MAP_H) continue;
      const sx=tx*TILE-camX, sy=ty*TILE-camY;
      const tile=map[ty][tx];

      // Fog of war
      if(!exploredMap[ty][tx]) { ctx.fillStyle='#0a0a0f'; ctx.fillRect(sx,sy,TILE,TILE); continue; }

      ctx.fillStyle=TILE_COLORS[tile]||'#2a2a3e';
      ctx.fillRect(sx,sy,TILE,TILE);

      // Floor detail
      if(tile===T.FLOOR) {
        ctx.fillStyle='rgba(255,255,255,0.02)';
        if((tx+ty)%2===0) ctx.fillRect(sx,sy,TILE,TILE);
      }

      // Wall top highlight
      if(tile===T.WALL) {
        ctx.fillStyle='rgba(255,255,255,0.05)';
        ctx.fillRect(sx,sy,TILE,3);
        ctx.fillStyle='rgba(0,0,0,0.3)';
        ctx.fillRect(sx,sy+TILE-3,TILE,3);
      }

      // Special tiles
      if(tile===T.SAVE_POINT) {
        const pulse=Math.sin(Date.now()/500)*0.3+0.7;
        ctx.fillStyle=`rgba(79,195,247,${pulse*0.3})`;
        ctx.fillRect(sx,sy,TILE,TILE);
        // Crystal shape
        ctx.fillStyle='#4fc3f7';
        ctx.beginPath();
        ctx.moveTo(sx+TILE/2,sy+6);ctx.lineTo(sx+TILE-8,sy+TILE/2);
        ctx.lineTo(sx+TILE/2,sy+TILE-6);ctx.lineTo(sx+8,sy+TILE/2);
        ctx.fill();
      } else if(tile===T.SHOP) {
        ctx.fillStyle='rgba(255,215,0,0.15)';
        ctx.fillRect(sx,sy,TILE,TILE);
        // Shop sign
        ctx.fillStyle='#ffd700';
        ctx.font='bold 16px sans-serif';ctx.textAlign='center';
        ctx.fillText('🏪',sx+TILE/2,sy+TILE/2+5);
      } else if(tile===T.CHEST) {
        const bob=Math.sin(Date.now()/400)*2;
        ctx.fillStyle='#8d6e63';
        ctx.fillRect(sx+8,sy+10+bob,16,14);
        ctx.fillStyle='#ffd700';
        ctx.fillRect(sx+12,sy+12+bob,8,4);
        // Glow
        ctx.fillStyle='rgba(255,215,0,0.1)';
        ctx.beginPath();ctx.arc(sx+TILE/2,sy+TILE/2,18,0,Math.PI*2);ctx.fill();
      }
    }
  }
}

function drawMinimap() {
  mmCtx.fillStyle='#0a0a0f';
  mmCtx.fillRect(0,0,140,140);

  const scale=1.5;
  const px=Math.floor(player.x/TILE), py=Math.floor(player.y/TILE);
  const offX=70-px*scale, offY=70-py*scale;

  for(let ty=py-40;ty<py+40;ty++) {
    for(let tx=px-40;tx<px+40;tx++) {
      if(tx<0||tx>=MAP_W||ty<0||ty>=MAP_H) continue;
      if(!exploredMap[ty][tx]) continue;
      const tile=map[ty][tx];
      let color='#333';
      if(tile===T.FLOOR) color='#4a4a5e';
      else if(tile===T.WALL) color='#2a2a3e';
      else if(tile===T.SAVE_POINT) color='#4fc3f7';
      else if(tile===T.SHOP) color='#ffd700';
      else if(tile===T.CHEST) color='#8d6e63';
      mmCtx.fillStyle=color;
      mmCtx.fillRect(tx*scale+offX,ty*scale+offY,scale,scale);
    }
  }

  // Player dot
  mmCtx.fillStyle='#ff4444';
  mmCtx.beginPath();mmCtx.arc(70,70,2.5,0,Math.PI*2);mmCtx.fill();

  // Enemy dots
  for(const e of enemies) {
    if(e.hp<=0) continue;
    const ex=Math.floor(e.x/TILE)*scale+offX;
    const ey=Math.floor(e.y/TILE)*scale+offY;
    mmCtx.fillStyle='#ff6666';
    mmCtx.fillRect(ex-1,ey-1,2,2);
  }
}

// ==================== UI UPDATES ====================
function updateHUD() {
  // Hearts
  const hc=document.getElementById('hearts-container');
  hc.innerHTML='';
  for(let i=0;i<player.maxHp/2;i++) {
    const div=document.createElement('div');
    div.className='heart';
    const filled = player.hp > i*2;
    const half = !filled && player.hp > i*2-1;
    if(filled) {
      div.innerHTML='<svg viewBox="0 0 24 24"><path fill="#f44336" 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(half) {
      div.innerHTML='<svg viewBox="0 0 24 24"><defs><linearGradient id="hg"><stop offset="50%" stop-color="#f44336"/><stop offset="50%" stop-color="#555"/></linearGradient></defs><path fill="url(#hg)" 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 {
      div.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>';
    }
    hc.appendChild(div);
  }

  document.getElementById('money-val').textContent=player.money;
  const weaponNames=['Rusty Sword','Iron Blade','Flame Sword'];
  document.getElementById('weapon-display').innerHTML=`⚔️ ${weaponNames[player.weaponLevel]}`;
  document.getElementById('level-display').textContent=`Lv.${player.level}`;
  document.getElementById('xp-bar').style.width=`${(player.xp/player.xpToNext)*100}%`;
  document.getElementById('stamina-bar').style.width=`${(player.stamina/player.maxStamina)*100}%`;
}

// ==================== MESSAGES ====================
function addMessage(text, color='#ccc') {
  const log=document.getElementById('message-log');
  const msg=document.createElement('div');
  msg.className='msg';msg.style.color=color;msg.textContent=text;
  log.appendChild(msg);
  setTimeout(()=>msg.remove(),4000);
}

// ==================== INVENTORY UI ====================
function updateInventoryUI() {
  const grid=document.getElementById('inv-grid');
  grid.innerHTML='';
  for(let i=0;i<18;i++) {
    const slot=document.createElement('div');
    slot.className='inv-slot';
    if(inventory[i]) {
      const it=inventory[i];
      slot.innerHTML=`<span class="item-icon">${it.icon}</span>${it.count>1?`<span class="item-count">${it.count}</span>`:''}`;
      slot.title=`${it.name}\n${it.desc}`;
      if(it.type==='consumable') {
        slot.onclick=()=>{ useItem(i); updateInventoryUI(); };
      }
    }
    grid.appendChild(slot);
  }
}

// ==================== SHOP ====================
let shopOpen=false;

function openShop() {
  shopOpen=true;
  AudioEngine.play('shop',0.3);
  const panel=document.getElementById('shop-panel');
  panel.style.display='block';
  renderShop();
}

function closeShop() {
  shopOpen=false;
  document.getElementById('shop-panel').style.display='none';
}

function renderShop() {
  const container=document.getElementById('shop-items');
  container.innerHTML='';
  for(const item of ITEM_DEFS) {
    if(item.id==='mana_crystal') continue; // Not sold
    const div=document.createElement('div');
    div.className='shop-item';
    const owned=inventory.find(i=>i.id===item.id);
    div.innerHTML=`<div><span class="name">${item.icon} ${item.name}</span><br><span class="desc">${item.desc}</span></div><span class="price">🪙${item.price}${owned?' ✓':''}</span>`;
    if(!owned) {
      div.onclick=()=>{
        if(player.money>=item.price) {
          player.money-=item.price;
          addItem(item.id);
          AudioEngine.play('coin',0.3);
          addMessage(`Bought ${item.name}!`,'#ffd700');

          // Apply accessory effects
          if(item.type==='accessory') {
            if(item.effect.hp) { player.maxHp+=item.effect.hp; player.hp=player.maxHp; }
            if(item.effect.speed) {} // Applied dynamically
            if(item.effect.damage) { player.baseDamage+=item.effect.damage; player.damage=player.baseDamage+player.getAccessoryBonus('damage'); }
            player.equipment[item.id.replace('_',' ').split(' ')[0].toLowerCase()] = item.name;
          }

          renderShop(); updateInventoryUI();
        } else {
          addMessage('Not enough gold!','#f44336');
        }
      };
    }
    container.appendChild(div);
  }
}

// ==================== SAVE SYSTEM ====================
let savePanelOpen=false;

function showSavePanel() {
  if(savePanelOpen) return;
  savePanelOpen=true;
  AudioEngine.play('save',0.3);
  document.getElementById('save-panel').style.display='block';
  const hasSave=localStorage.getItem('dungeonOfEchoes_save');
  document.getElementById('load-btn').style.display=hasSave?'inline-block':'none';
}

function hideSavePanel() { savePanelOpen=false; document.getElementById('save-panel').style.display='none'; }

function saveGame() {
  const data = {
    player: {
      x:player.x,y:player.y,hp:player.hp,maxHp:player.maxHp,
      money:player.money,xp:player.xp,xpToNext:player.xpToNext,level:player.level,
      weaponLevel:player.weaponLevel,hasRanged:player.hasRanged,
      baseDamage:player.baseDamage,damage:player.damage,attackSpeed:player.attackSpeed,
    },
    inventory: JSON.parse(JSON.stringify(inventory)),
    mapState: rooms.map(r=>({x:r.x,y:r.y,w:r.w,h:r.h})),
  };
  localStorage.setItem('dungeonOfEchoes_save',JSON.stringify(data));
  addMessage('Game saved!','#4fc3f7');
  emitParticles(player.x,player.y,25,'#4fc3f7',100,0.8,5,'spark');
  hideSavePanel();
}

function loadGame() {
  const raw=localStorage.getItem('dungeonOfEchoes_save');
  if(!raw) return;
  const data=JSON.parse(raw);
  Object.assign(player,data.player);
  inventory=data.inventory||[];
  addMessage('Game loaded!','#4fc3f7');
  hideSavePanel();
}

// ==================== INPUT ====================
const keys={};
let mouseDown=false, mouseX=0, mouseY=0;

document.addEventListener('keydown',e=>{
  keys[e.key.toLowerCase()]=true;
  if(e.key==='i'||e.key==='I') {
    const panel=document.getElementById('inventory-panel');
    if(panel.style.display==='block') { panel.style.display='none'; }
    else { panel.style.display='block'; updateInventoryUI(); }
  }
  if(e.key==='Escape') { closeShop(); hideSavePanel(); document.getElementById('inventory-panel').style.display='none'; }
});
document.addEventListener('keyup',e=>{ keys[e.key.toLowerCase()]=false; });

canvas.addEventListener('mousedown',e=>{ mouseDown=true; mouseX=e.clientX; mouseY=e.clientY; });
canvas.addEventListener('mouseup',()=>{ mouseDown=false; });
canvas.addEventListener('mousemove',e=>{ mouseX=e.clientX; mouseY=e.clientY; });

// ==================== GAME STATE ====================
let gameState='title'; // title, playing, dead
let gameTime=0;

function startGame() {
  document.getElementById('title-screen').style.display='none';
  AudioEngine.init();
  generateMap();
  player.x=rooms[0].cx*TILE+TILE/2;
  player.y=rooms[0].cy*TILE+TILE/2;
  spawnEnemies();
  patchEnemies();
  gameState='playing';
  addMessage('Welcome to the Dungeon of Echoes!','#ce93d8');
  addMessage('Explore, fight, and grow stronger!','#aaa');
}

function restartGame() {
  document.getElementById('death-screen').style.display='none';
  player.hp=player.maxHp;
  player.money=Math.floor(player.money*0.5);
  player.xp=0;player.level=1;player.xpToNext=30;
  player.weaponLevel=0;player.hasRanged=false;
  player.baseDamage=5;player.damage=5;player.attackSpeed=0.4;
  player.equipment={weapon:'Rusty Sword',shield:null,boots:null,charm:null,gem:null};
  inventory=[];
  generateMap();
  player.x=rooms[0].cx*TILE+TILE/2;
  player.y=rooms[0].cy*TILE+TILE/2;
  spawnEnemies();patchEnemies();
  gameState='playing';
}

function onPlayerDeath() {
  AudioEngine.play('death',0.5);
  emitParticles(player.x,player.y,40,'#ff0000',150,1.5,6,'spark');
  setTimeout(()=>{
    gameState='dead';
    document.getElementById('death-screen').style.display='flex';
  },800);
}

// ==================== MAIN LOOP ====================
let lastTime=performance.now();

function gameLoop(now) {
  requestAnimationFrame(gameLoop);
  const dt=Math.min(0.05,(now-lastTime)/1000);
  lastTime=now;

  if(gameState!=='playing') return;

  gameTime+=dt;

  // Update
  player.update(dt);
  updateEnemies(dt);
  updateProjectiles(dt);
  updateCamera();

  // Attack input
  if(mouseDown && !shopOpen && !savePanelOpen) {
    player.attack();
  }

  // Update particles
  for(let i=particles.length-1;i>=0;i--) {
    if(!particles[i].update(dt)) particles.splice(i,1);
  }

  // Render
  ctx.fillStyle='#0a0a0f';
  ctx.fillRect(0,0,canvas.width,canvas.height);

  drawMap();
  drawEnemies(ctx,camX,camY);
  player.draw(ctx,camX,camY);
  drawProjectiles(ctx,camX,camY);

  // Particles on top
  for(const p of particles) p.draw(ctx,camX,camY);

  // Ambient lighting effect around player
  const grad=ctx.createRadialGradient(player.x-camX,player.y-camY,50,player.x-camX,player.y-camY,250);
  grad.addColorStop(0,'rgba(0,0,0,0)');
  grad.addColorStop(1,'rgba(0,0,0,0.4)');
  ctx.fillStyle=grad;
  ctx.fillRect(0,0,canvas.width,canvas.height);

  // UI updates (throttled)
  if(Math.floor(now/200)!==Math.floor((now-dt*1000)/200)) {
    updateHUD();
    drawMinimap();
  }
}

requestAnimationFrame(gameLoop);
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"basic","sessionId":"a0e63d4e-372d-4fbb-98d9-28253e348a67","tokensIn":733,"tokensOut":17843,"tokensTotal":18576,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-07-19T02:40:32.253Z"} -->