← RPG results

RPG

Qwen 3.6 27B Fable Fusion 711 Uncensored Heretic NM DAU NEO MAX MTP GGUF · 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 73,358 bytes · SHA-256 75b0ea71dca1
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Realm of Echoes - Action RPG</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
  background: #0a0a0a;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  overflow: hidden;
  font-family: 'Courier New', monospace;
}
#gameCanvas {
  border: 2px solid #333;
  image-rendering: pixelated;
}
</style>
</head>
<body>
<canvas id="gameCanvas" width="960" height="640"></canvas>
<script>
// ============================================================
// REALM OF ECHOES - Complete Action-Adventure RPG
// ============================================================

const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');

const TILE_SIZE = 32;
const WORLD_W = 100;
const WORLD_H = 80;

// ============================================================
// AUDIO SYSTEM (Web Audio API - all synthesized)
// ============================================================
const AudioCtx = window.AudioContext || window.webkitAudioContext;
let audioCtx = null;

function initAudio() {
  if (!audioCtx) audioCtx = new AudioCtx();
}

function playTone(freq, duration, type = 'square', vol = 0.15, slide = 0) {
  if (!audioCtx) return;
  const osc = audioCtx.createOscillator();
  const gain = audioCtx.createGain();
  osc.type = type;
  osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
  if (slide) osc.frequency.linearRampToValueAtTime(freq + slide, audioCtx.currentTime + duration);
  gain.gain.setValueAtTime(vol, audioCtx.currentTime);
  gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + duration);
  osc.connect(gain);
  gain.connect(audioCtx.destination);
  osc.start();
  osc.stop(audioCtx.currentTime + duration);
}

function playNoise(duration, vol = 0.1, filterFreq = 3000) {
  if (!audioCtx) return;
  const bufferSize = audioCtx.sampleRate * duration;
  const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
  const data = buffer.getChannelData(0);
  for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
  const source = audioCtx.createBufferSource();
  source.buffer = buffer;
  const gain = audioCtx.createGain();
  const filter = audioCtx.createBiquadFilter();
  filter.type = 'lowpass';
  filter.frequency.value = filterFreq;
  gain.gain.setValueAtTime(vol, audioCtx.currentTime);
  gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + duration);
  source.connect(filter);
  filter.connect(gain);
  gain.connect(audioCtx.destination);
  source.start();
}

const SFX = {
  walk: () => {
    playNoise(0.06, 0.04, 800);
  },
  attack: () => {
    playNoise(0.12, 0.12, 2000);
    playTone(200, 0.1, 'sawtooth', 0.08, -100);
  },
  attack2: () => {
    playNoise(0.15, 0.15, 2500);
    playTone(250, 0.12, 'sawtooth', 0.1, -150);
  },
  attack3: () => {
    playNoise(0.2, 0.18, 3500);
    playTone(300, 0.15, 'sawtooth', 0.12, -200);
  },
  attackBow: () => {
    playTone(400, 0.15, 'triangle', 0.1, 200);
    playNoise(0.08, 0.08, 1500);
  },
  hit: () => {
    playTone(150, 0.15, 'square', 0.12, -80);
    playNoise(0.08, 0.06, 500);
  },
  enemyHit: () => {
    playTone(200, 0.1, 'square', 0.1, -100);
  },
  enemyDie: () => {
    playTone(300, 0.3, 'sawtooth', 0.1, -200);
    playNoise(0.15, 0.08, 1000);
  },
  pickup: () => {
    playTone(523, 0.08, 'sine', 0.1);
    setTimeout(() => playTone(659, 0.08, 'sine', 0.1), 60);
    setTimeout(() => playTone(784, 0.12, 'sine', 0.12), 120);
  },
  coin: () => {
    playTone(880, 0.06, 'square', 0.08);
    setTimeout(() => playTone(1175, 0.1, 'square', 0.1), 50);
  },
  heal: () => {
    playTone(440, 0.15, 'sine', 0.1);
    setTimeout(() => playTone(554, 0.15, 'sine', 0.1), 100);
    setTimeout(() => playTone(660, 0.2, 'sine', 0.12), 200);
  },
  shopOpen: () => {
    playTone(330, 0.1, 'sine', 0.08);
    setTimeout(() => playTone(392, 0.1, 'sine', 0.08), 80);
    setTimeout(() => playTone(440, 0.15, 'sine', 0.1), 160);
  },
  shopBuy: () => {
    playTone(523, 0.1, 'sine', 0.1);
    setTimeout(() => playTone(659, 0.1, 'sine', 0.1), 80);
    setTimeout(() => playTone(784, 0.15, 'sine', 0.12), 160);
    setTimeout(() => playTone(1047, 0.2, 'sine', 0.1), 240);
  },
  save: () => {
    playTone(262, 0.15, 'sine', 0.1);
    setTimeout(() => playTone(330, 0.15, 'sine', 0.1), 120);
    setTimeout(() => playTone(392, 0.15, 'sine', 0.1), 240);
    setTimeout(() => playTone(523, 0.3, 'sine', 0.12), 360);
  },
  door: () => {
    playTone(200, 0.2, 'square', 0.08, -50);
  },
  weaponUp: () => {
    playTone(440, 0.15, 'square', 0.1);
    setTimeout(() => playTone(554, 0.15, 'square', 0.1), 100);
    setTimeout(() => playTone(660, 0.15, 'square', 0.1), 200);
    setTimeout(() => playTone(880, 0.3, 'square', 0.12), 300);
  },
  damage: () => {
    playTone(120, 0.2, 'sawtooth', 0.12, -50);
    playNoise(0.1, 0.08, 400);
  },
  bossWarning: () => {
    playTone(150, 0.3, 'sawtooth', 0.15);
    setTimeout(() => playTone(140, 0.3, 'sawtooth', 0.15), 250);
    setTimeout(() => playTone(130, 0.4, 'sawtooth', 0.18), 500);
  }
};

// ============================================================
// PARTICLE SYSTEM
// ============================================================
let particles = [];

function spawnParticle(x, y, color, count = 5, spread = 2, life = 30, size = 2) {
  for (let i = 0; i < count; i++) {
    particles.push({
      x, y,
      vx: (Math.random() - 0.5) * spread,
      vy: (Math.random() - 0.5) * spread - 1,
      color,
      life: life + Math.random() * 15,
      maxLife: life + 15,
      size: size + Math.random() * size
    });
  }
}

function spawnImpactParticles(x, y, type) {
  if (type === 'melee') {
    spawnParticle(x, y, '#ffaa00', 8, 3, 20, 3);
    spawnParticle(x, y, '#ff4400', 4, 2, 15, 2);
  } else if (type === 'bow') {
    spawnParticle(x, y, '#00ccff', 6, 2, 15, 2);
  } else if (type === 'gold') {
    spawnParticle(x, y, '#ffd700', 10, 3, 25, 2);
  } else if (type === 'heal') {
    spawnParticle(x, y, '#44ff44', 8, 2, 30, 2);
  } else if (type === 'save') {
    spawnParticle(x, y, '#cc88ff', 15, 3, 40, 3);
  } else if (type === 'weapon') {
    spawnParticle(x, y, '#ffffff', 12, 4, 35, 3);
    spawnParticle(x, y, '#ffaa00', 8, 3, 30, 2);
  }
}

function updateParticles() {
  for (let i = particles.length - 1; i >= 0; i--) {
    const p = particles[i];
    p.x += p.vx;
    p.y += p.vy;
    p.vy += 0.05;
    p.life--;
    if (p.life <= 0) particles.splice(i, 1);
  }
}

function drawParticles(camX, camY) {
  for (const p of particles) {
    const alpha = p.life / p.maxLife;
    ctx.globalAlpha = alpha;
    ctx.fillStyle = p.color;
    ctx.fillRect(p.x - camX, p.y - camY, p.size, p.size);
  }
  ctx.globalAlpha = 1;
}

// ============================================================
// MAP SYSTEM
// ============================================================
// Tile types: 0=grass, 1=wall, 2=water, 3=stone, 4=wood, 5=door, 6=sand, 7=forest, 8=snow
const map = [];
const mapDecorations = [];

function generateMap() {
  // Initialize with grass
  for (let y = 0; y < WORLD_H; y++) {
    map[y] = [];
    for (let x = 0; x < WORLD_W; x++) {
      map[y][x] = 0;
    }
  }

  // Border walls
  for (let x = 0; x < WORLD_W; x++) {
    map[0][x] = 1;
    map[WORLD_H - 1][x] = 1;
  }
  for (let y = 0; y < WORLD_H; y++) {
    map[y][0] = 1;
    map[y][WORLD_W - 1] = 1;
  }

  // Village area (center-ish)
  buildVillage(45, 35);

  // Forest region (north)
  buildForest(20, 5, 30, 15);

  // Swamp area (west)
  buildSwamp(5, 30, 18, 15);

  // Mountain/stone path to dungeons (east)
  buildMountainPath(55, 35);

  // Cave dungeon (far east)
  buildCaveDungeon(75, 30);

  // Ice cave (far north-east)
  buildIceCave(65, 10);

  // Secret area south
  buildSecretArea(40, 60);

  // Boss arena (deep cave)
  buildBossArena(88, 40);

  // Add decorative elements
  addDecorations();
}

function buildVillage(cx, cy) {
  // Clear area
  for (let y = cy - 6; y < cy + 6; y++) {
    for (let x = cx - 8; x < cx + 8; x++) {
      if (inBounds(x, y)) map[y][x] = 3;
    }
  }

  // Houses
  buildHouse(cx - 5, cy - 3);
  buildHouse(cx + 4, cy - 3);
  buildShopHouse(cx + 3, cy + 3);

  // Save point
  buildSavePoint(cx - 5, cy + 3);

  // Path from village
  for (let x = cx + 8; x < 55; x++) {
    if (inBounds(x, cy)) map[cy][x] = 6;
  }
  for (let x = cx - 8; x > 20; x--) {
    if (inBounds(x, cy)) map[cy][x] = 0;
  }
  for (let y = cy - 6; y > 18; y--) {
    if (inBounds(cx, y)) map[y][cx] = 7;
  }
}

function buildHouse(x, y) {
  for (let dy = -2; dy <= 0; dy++) {
    for (let dx = -2; dx <= 2; dx++) {
      if (inBounds(x + dx, y + dy)) {
        if (dy === -2 || dx === -2 || dx === 2) {
          map[y + dy][x + dx] = 4;
        } else if (dy === 0 && (dx === -1 || dx === 0 || dx === 1)) {
          map[y + dy][x + dx] = 4;
        } else {
          map[y + dy][x + dx] = 3;
        }
      }
    }
  }
}

function buildShopHouse(x, y) {
  for (let dy = -2; dy <= 0; dy++) {
    for (let dx = -2; dx <= 2; dx++) {
      if (inBounds(x + dx, y + dy)) {
        if (dy === -2 || dx === -2 || dx === 2) {
          map[y + dy][x + dx] = 4;
        } else if (dy === 0 && (dx === -1 || dx === 0 || dx === 1)) {
          map[y + dy][x + dx] = 4;
        } else {
          map[y + dy][x + dx] = 3;
        }
      }
    }
  }
  // Shop entrance
  map[y][x] = 5;
}

function buildSavePoint(x, y) {
  // Fountain/save crystal
  map[y][x] = 3;
  mapDecorations.push({ type: 'saveCrystal', x, y });
}

function buildForest(x, y, w, h) {
  for (let dy = 0; dy < h; dy++) {
    for (let dx = 0; dx < w; dx++) {
      if (inBounds(x + dx, y + dy) && Math.random() > 0.4) {
        map[y + dy][x + dx] = 7;
      }
    }
  }
  // Clear path through forest
  for (let dx = 2; dx < w - 2; dx++) {
    if (inBounds(x + dx, y + h / 2 | 0)) {
      map[y + (h / 2 | 0)][x + dx] = 0;
    }
  }
}

function buildSwamp(x, y, w, h) {
  for (let dy = 0; dy < h; dy++) {
    for (let dx = 0; dx < w; dx++) {
      if (inBounds(x + dx, y + dy) && Math.random() > 0.5) {
        map[y + dy][x + dx] = 2;
      } else if (inBounds(x + dx, y + dy)) {
        map[y + dy][x + dx] = 0;
      }
    }
  }
}

function buildMountainPath(x, y) {
  for (let dx = 0; dx < 25; dx++) {
    const ty = y + Math.floor(Math.random() * 3) - 1;
    if (inBounds(x + dx, ty)) {
      map[ty][x + dx] = 3;
    }
    if (inBounds(x + dx, ty + 1)) map[ty + 1][x + dx] = 1;
  }
}

function buildCaveDungeon(x, y) {
  // Outer walls
  for (let dy = -8; dy <= 8; dy++) {
    for (let dx = -10; dx <= 5; dx++) {
      if (inBounds(x + dx, y + dy)) {
        if (Math.abs(dx) === 10 || Math.abs(dy) === 8) {
          map[y + dy][x + dx] = 1;
        } else if (Math.random() > 0.85) {
          map[y + dy][x + dx] = 1;
        } else {
          map[y + dy][x + dx] = 3;
        }
      }
    }
  }
  // Door entrance
  map[y][x - 10] = 5;
}

function buildIceCave(x, y) {
  for (let dy = -6; dy <= 6; dy++) {
    for (let dx = -8; dx <= 8; dx++) {
      if (inBounds(x + dx, y + dy)) {
        if (Math.abs(dx) === 8 || Math.abs(dy) === 6) {
          map[y + dy][x + dx] = 1;
        } else if (Math.random() > 0.9) {
          map[y + dy][x + dx] = 1;
        } else {
          map[y + dy][x + dx] = 8;
        }
      }
    }
  }
  // Path from mountain
  for (let dx = 55; dx < x - 8; dx++) {
    if (inBounds(dx, 18)) map[18][dx] = 8;
  }
}

function buildSecretArea(x, y) {
  for (let dy = -5; dy <= 5; dy++) {
    for (let dx = -7; dx <= 7; dx++) {
      if (inBounds(x + dx, y + dy)) {
        if (Math.abs(dx) === 7 || Math.abs(dy) === 5) {
          map[y + dy][x + dx] = 1;
        } else {
          map[y + dy][x + dx] = 3;
        }
      }
    }
  }
  // Hidden door
  map[y + 4][x - 3] = 5;
}

function buildBossArena(x, y) {
  for (let dy = -8; dy <= 8; dy++) {
    for (let dx = -10; dx <= 10; dx++) {
      if (inBounds(x + dx, y + dy)) {
        if (Math.abs(dx) === 10 || Math.abs(dy) === 8) {
          map[y + dy][x + dx] = 1;
        } else {
          map[y + dy][x + dx] = 3;
        }
      }
    }
  }
  // Door
  map[y][x - 10] = 5;
  // Boss summoning stone
  mapDecorations.push({ type: 'bossStone', x: x, y: y });
}

function addDecorations() {
  // Trees in forests
  for (let y = 0; y < WORLD_H; y++) {
    for (let x = 0; x < WORLD_W; x++) {
      if (map[y][x] === 7 && Math.random() > 0.7) {
        mapDecorations.push({ type: 'tree', x, y });
      }
    }
  }

  // Flowers on grass
  for (let y = 0; y < WORLD_H; y++) {
    for (let x = 0; x < WORLD_W; x++) {
      if (map[y][x] === 0 && Math.random() > 0.95) {
        mapDecorations.push({ type: 'flower', x, y, color: ['#ff66aa', '#ffcc00', '#66ccff'][Math.floor(Math.random() * 3)] });
      }
    }
  }

  // Rocks
  for (let y = 0; y < WORLD_H; y++) {
    for (let x = 0; x < WORLD_W; x++) {
      if ((map[y][x] === 0 || map[y][x] === 3 || map[y][x] === 8) && Math.random() > 0.97) {
        mapDecorations.push({ type: 'rock', x, y });
      }
    }
  }
}

function inBounds(x, y) {
  return x >= 0 && x < WORLD_W && y >= 0 && y < WORLD_H;
}

function isWalkable(x, y) {
  if (!inBounds(x, y)) return false;
  const t = map[y][x];
  return t !== 1 && t !== 2; // Not wall or water
}

function getTileColor(tile) {
  switch (tile) {
    case 0: return ['#3a7d2c', '#458932', '#3d852f'][Math.floor(Math.random() * 3)];
    case 1: return ['#555566'];
    case 2: return ['#2255aa'];
    case 3: return ['#888888'];
    case 4: return ['#664422'];
    case 5: return ['#885522'];
    case 6: return ['#c2a84a'];
    case 7: return ['#2d6b1f'];
    case 8: return ['#ddeeff'];
    default: return '#3a7d2c';
  }
}

// ============================================================
// WORLD ITEMS, ENEMIES, AND INTERACTABLES
// ============================================================
let worldItems = [];
let enemies = [];
let doors = [];
let interactables = [];

function spawnWorld() {
  worldItems = [];
  enemies = [];
  doors = [];
  interactables = [];

  // Find all doors
  for (let y = 0; y < WORLD_H; y++) {
    for (let x = 0; x < WORLD_W; x++) {
      if (map[y][x] === 5) {
        doors.push({ x, y, open: false });
      }
    }
  }

  // Interactables
  // Shop NPC
  interactables.push({ type: 'shopkeeper', x: 48, y: 38, name: 'Merchant Alder' });
  // Blacksmith (weapon upgrade quest)
  interactables.push({ type: 'blacksmith', x: 40, y: 32, name: 'Blacksmith Irona' });

  // World items - carefully placed for progression
  // Health container 1 - in forest after clearing enemies
  worldItems.push({ type: 'healthContainer', x: 33, y: 10, collected: false });

  // Weapon upgrade 1 (Iron Sword) - in swamp area
  worldItems.push({ type: 'weaponIron', x: 10, y: 38, collected: false });

  // Health container 2 - in cave dungeon
  worldItems.push({ type: 'healthContainer', x: 78, y: 32, collected: false });

  // Ranged weapon (Bow) - mid-game, in ice cave
  worldItems.push({ type: 'rangedBow', x: 68, y: 13, collected: false });

  // Weapon upgrade 2 (Steel Blade) - secret area
  worldItems.push({ type: 'weaponSteel', x: 43, y: 62, collected: false });

  // Health container 3 - near boss arena entrance
  worldItems.push({ type: 'healthContainer', x: 83, y: 38, collected: false });

  // Consumables and accessories
  worldItems.push({ type: 'healthPotion', x: 35, y: 15, collected: false, qty: 2 });
  worldItems.push({ type: 'healthPotion', x: 80, y: 28, collected: false, qty: 2 });
  worldItems.push({ type: 'speedBoots', x: 62, y: 16, collected: false });
  worldItems.push({ type: 'shieldRing', x: 70, y: 20, collected: false });

  // Coin pickups scattered throughout
  for (let i = 0; i < 40; i++) {
    let x, y;
    do {
      x = 5 + Math.floor(Math.random() * 90);
      y = 5 + Math.floor(Math.random() * 70);
    } while (!isWalkable(x, y));
    worldItems.push({ type: 'coin', x, y, collected: false, amount: 5 + Math.floor(Math.random() * 10) });
  }
  // Bigger coin pickups
  for (let i = 0; i < 10; i++) {
    let x, y;
    do {
      x = 5 + Math.floor(Math.random() * 90);
      y = 5 + Math.floor(Math.random() * 70);
    } while (!isWalkable(x, y));
    worldItems.push({ type: 'coin', x, y, collected: false, amount: 25 + Math.floor(Math.random() * 20) });
  }

  // Enemies by area
  // Forest enemies
  for (let i = 0; i < 8; i++) {
    let x, y;
    do { x = 22 + Math.floor(Math.random() * 24); y = 7 + Math.floor(Math.random() * 11); }
    while (!isWalkable(x, y));
    enemies.push(createEnemy('slime', x, y, 10 + Math.random() * 5));
  }
  for (let i = 0; i < 4; i++) {
    let x, y;
    do { x = 22 + Math.floor(Math.random() * 24); y = 7 + Math.floor(Math.random() * 11); }
    while (!isWalkable(x, y));
    enemies.push(createEnemy('wolf', x, y, 18 + Math.random() * 5));
  }

  // Swamp enemies
  for (let i = 0; i < 6; i++) {
    let x, y;
    do { x = 7 + Math.floor(Math.random() * 13); y = 32 + Math.floor(Math.random() * 11); }
    while (!isWalkable(x, y));
    enemies.push(createEnemy('slime', x, y, 12 + Math.random() * 5));
  }
  for (let i = 0; i < 4; i++) {
    let x, y;
    do { x = 7 + Math.floor(Math.random() * 13); y = 32 + Math.floor(Math.random() * 11); }
    while (!isWalkable(x, y));
    enemies.push(createEnemy('skeleton', x, y, 20 + Math.random() * 5));
  }

  // Mountain enemies
  for (let i = 0; i < 6; i++) {
    let x, y;
    do { x = 56 + Math.floor(Math.random() * 15); y = 33 + Math.floor(Math.random() * 5); }
    while (!isWalkable(x, y));
    enemies.push(createEnemy('wolf', x, y, 20 + Math.random() * 8));
  }
  for (let i = 0; i < 4; i++) {
    let x, y;
    do { x = 56 + Math.floor(Math.random() * 15); y = 33 + Math.floor(Math.random() * 5); }
    while (!isWalkable(x, y));
    enemies.push(createEnemy('goblin', x, y, 15 + Math.random() * 5));
  }

  // Cave dungeon enemies
  for (let i = 0; i < 10; i++) {
    let x, y;
    do { x = 67 + Math.floor(Math.random() * 18); y = 24 + Math.floor(Math.random() * 16); }
    while (!isWalkable(x, y));
    enemies.push(createEnemy('skeleton', x, y, 25 + Math.random() * 10));
  }
  for (let i = 0; i < 5; i++) {
    let x, y;
    do { x = 67 + Math.floor(Math.random() * 18); y = 24 + Math.floor(Math.random() * 16); }
    while (!isWalkable(x, y));
    enemies.push(createEnemy('goblin', x, y, 22 + Math.random() * 8));
  }
  // Mini boss guarding boss entrance
  enemies.push(createEnemy('bossGuard', 85, 38, 80));

  // Ice cave enemies
  for (let i = 0; i < 8; i++) {
    let x, y;
    do { x = 58 + Math.floor(Math.random() * 16); y = 10 + Math.floor(Math.random() * 10); }
    while (!isWalkable(x, y));
    enemies.push(createEnemy('iceSlime', x, y, 20 + Math.random() * 8));
  }
  for (let i = 0; i < 4; i++) {
    let x, y;
    do { x = 58 + Math.floor(Math.random() * 16); y = 10 + Math.floor(Math.random() * 10); }
    while (!isWalkable(x, y));
    enemies.push(createEnemy('frostWolf', x, y, 30 + Math.random() * 10));
  }

  // Secret area enemies
  for (let i = 0; i < 6; i++) {
    let x, y;
    do { x = 34 + Math.floor(Math.random() * 12); y = 56 + Math.floor(Math.random() * 10); }
    while (!isWalkable(x, y));
    enemies.push(createEnemy('goblin', x, y, 28 + Math.random() * 8));
  }
}

function createEnemy(type, x, y, hp) {
  const stats = {
    slime: { hp, speed: 0.4, damage: 3, color: '#44aa44', size: 16, ai: 'chase' },
    wolf: { hp, speed: 1.2, damage: 5, color: '#666666', size: 18, ai: 'aggressive' },
    skeleton: { hp, speed: 0.8, damage: 6, color: '#ddddcc', size: 18, ai: 'chase' },
    goblin: { hp, speed: 1.0, damage: 4, color: '#66aa44', size: 16, ai: 'aggressive' },
    iceSlime: { hp, speed: 0.5, damage: 5, color: '#44ccff', size: 16, ai: 'chase' },
    frostWolf: { hp, speed: 1.3, damage: 7, color: '#88bbff', size: 20, ai: 'aggressive' },
    bossGuard: { hp: 80, speed: 0.9, damage: 10, color: '#cc4444', size: 28, ai: 'aggressive' }
  };
  const s = stats[type] || stats.slime;
  return {
    type, x: x * TILE_SIZE + TILE_SIZE / 2, y: y * TILE_SIZE + TILE_SIZE / 2,
    hp, maxHp: hp,
    speed: s.speed,
    damage: s.damage,
    color: s.color,
    size: s.size,
    ai: s.ai || 'chase',
    state: 'idle',
    animTimer: 0,
    attackCooldown: 0,
    knockbackX: 0,
    knockbackY: 0,
    flashTimer: 0
  };
}

// ============================================================
// PLAYER
// ============================================================
const player = {
  x: 45 * TILE_SIZE + TILE_SIZE / 2,
  y: 37 * TILE_SIZE + TILE_SIZE / 2,
  speed: 2.5,
  hp: 6,
  maxHp: 6,
  hpContainers: 3,
  maxContainers: 5,
  money: 20,

  // Equipment
  weapon: 'bronze',      // bronze -> iron -> steel
  weaponLevel: 0,
  hasBow: false,
  bowAmmo: 0,

  // Accessories
  speedBoots: false,
  shieldRing: false,

  // Inventory (consumables)
  healthPotions: 0,

  // Combat state
  attacking: false,
  attackTimer: 0,
  attackCooldown: 0,
  facing: 'down',
  hitTimer: 0,
  invincible: 0,

  // Animation
  animTimer: 0,
  walkFrame: 0,

  // Camera
  camX: 0,
  camY: 0
};

// ============================================================
// SHOP SYSTEM
// ============================================================
const shopItems = [
  { name: 'Health Potion', type: 'healthPotion', price: 15, desc: 'Restores 4 HP' },
  { name: 'Bow Arrow (5)', type: 'arrows', price: 10, desc: '+5 arrows for bow' },
  { name: 'Minor Heal', type: 'quickHeal', price: 25, desc: 'Instantly heal 6 HP' }
];

let shopOpen = false;
let shopSelection = 0;
let gameMessage = '';
let messageTimer = 0;
let paused = false;

function openShop() {
  shopOpen = true;
  shopSelection = 0;
  paused = true;
  SFX.shopOpen();
}

function closeShop() {
  shopOpen = false;
  paused = false;
}

function buyItem(index) {
  const item = shopItems[index];
  if (player.money >= item.price) {
    player.money -= item.price;
    SFX.shopBuy();
    if (item.type === 'healthPotion') {
      player.healthPotions++;
      showMessage('Bought Health Potion!');
    } else if (item.type === 'arrows') {
      player.bowAmmo += 5;
      showMessage('Bought 5 Arrows!');
    } else if (item.type === 'quickHeal') {
      player.hp = Math.min(player.hp + 6, player.maxHp);
      spawnImpactParticles(player.x, player.y, 'heal');
      showMessage('Healed 6 HP!');
    }
  } else {
    showMessage('Not enough gold!');
  }
}

function showMessage(text) {
  gameMessage = text;
  messageTimer = 120;
}

// ============================================================
// SAVE SYSTEM
// ============================================================
const SAVE_KEY = 'realmOfEchoes_save';

function saveGame() {
  const saveData = {
    player: { ...player },
    enemies: enemies.map(e => ({ ...e })),
    worldItems: worldItems.map(i => ({ ...i })),
    doors: doors.map(d => ({ ...d })),
    timestamp: Date.now()
  };
  localStorage.setItem(SAVE_KEY, JSON.stringify(saveData));
  showMessage('Game Saved!');
  SFX.save();
}

function loadGame() {
  const data = localStorage.getItem(SAVE_KEY);
  if (data) {
    try {
      const save = JSON.parse(data);
      Object.assign(player, save.player);
      enemies = save.enemies || [];
      worldItems = save.worldItems || [];
      doors = save.doors || [];
      showMessage('Game Loaded!');
      return true;
    } catch (e) {
      console.error('Failed to load save:', e);
      return false;
    }
  }
  return false;
}

function clearSave() {
  localStorage.removeItem(SAVE_KEY);
  showMessage('Save Cleared!');
}

// ============================================================
// INPUT HANDLING
// ============================================================
const keys = {};
document.addEventListener('keydown', (e) => {
  keys[e.key.toLowerCase()] = true;
  keys[e.code] = true;
  initAudio();

  if (shopOpen) {
    if (e.key === 'Escape') { closeShop(); return; }
    if (e.key === 'ArrowUp' || e.key === 'w') { shopSelection = Math.max(0, shopSelection - 1); }
    if (e.key === 'ArrowDown' || e.key === 's') { shopSelection = Math.min(shopItems.length - 1, shopSelection + 1); }
    if (e.key === 'Enter' || e.key === 'e') { buyItem(shopSelection); }
    return;
  }

  if (e.key === 'Escape') {
    showPauseMenu();
    return;
  }

  if (e.key.toLowerCase() === 'f' && !paused) {
    handleInteraction();
  }

  if (e.key.toLowerCase() === 'u' && !paused) {
    useHealthPotion();
  }

  if (e.key === '1') {
    // Switch to melee
    if (player.hasBow) {
      player.weapon = player.weaponLevel === 0 ? 'bronze' : player.weaponLevel === 1 ? 'iron' : 'steel';
    }
  }
  if (e.key === '2' && player.hasBow) {
    player.weapon = 'bow';
  }

  // Attack with Z / Space / Click area
  if ((e.key.toLowerCase() === 'z' || e.key === 'Space') && !paused) {
    performAttack();
    e.preventDefault();
  }

  // Auto-save with S (only near save crystals)
  if (e.key.toLowerCase() === 's' && !paused && !shopOpen) {
    // S is also movement, save handled via F at crystals
  }
});

document.addEventListener('keyup', (e) => {
  keys[e.key.toLowerCase()] = false;
  keys[e.code] = false;
});

let mouseDown = false;
canvas.addEventListener('mousedown', (e) => {
  mouseDown = true;
  initAudio();
  const rect = canvas.getBoundingClientRect();
  const mx = e.clientX - rect.left;
  const my = e.clientY - rect.top;

  if (shopOpen) {
    const itemH = 40;
    const startY = 280;
    for (let i = 0; i < shopItems.length; i++) {
      if (my >= startY + i * itemH && my < startY + (i + 1) * itemH && mx >= 150 && mx < 750) {
        shopSelection = i;
        buyItem(i);
        break;
      }
    }
    if (my > 460 && mx > 300 && mx < 460) { closeShop(); }
    return;
  }

  if (!paused) {
    // Move towards click direction and attack
    const wx = mx + player.camX;
    const wy = my + player.camY;
    const dx = wx - player.x;
    const dy = wy - player.y;
    if (Math.abs(dx) > 10 || Math.abs(dy) > 10) {
      player.facing = Math.abs(dx) > Math.abs(dy) ? (dx > 0 ? 'right' : 'left') : (dy > 0 ? 'down' : 'up');
    }
    performAttack();
  }
});

canvas.addEventListener('mouseup', () => { mouseDown = false; });

// ============================================================
// GAME STATE MANAGEMENT
// ============================================================
let gameState = 'title'; // title, playing, pauseMenu, gameover, victory
let titleSelection = 0;

function showPauseMenu() {
  if (gameState === 'playing') {
    gameState = 'pauseMenu';
  } else if (gameState === 'pauseMenu') {
    gameState = 'playing';
  }
}

function startNewGame() {
  generateMap();
  spawnWorld();
  gameState = 'playing';
  showMessage('Welcome to the Realm of Echoes!');
}

// ============================================================
// PLAYER UPDATE
// ============================================================
let walkTimer = 0;

function updatePlayer() {
  if (paused || gameState !== 'playing') return;

  const speed = player.speed * (player.speedBoots ? 1.3 : 1);

  // Movement
  let dx = 0, dy = 0;
  if (keys['arrowup'] || keys['w']) { dy = -speed; player.facing = 'up'; }
  if (keys['arrowdown'] || keys['s']) { dy = speed; player.facing = 'down'; }
  if (keys['arrowleft'] || keys['a']) { dx = -speed; player.facing = 'left'; }
  if (keys['arrowright'] || keys['d']) { dx = speed; player.facing = 'right'; }

  if (dx !== 0 || dy !== 0) {
    walkTimer++;
    if (walkTimer > 10) {
      walkTimer = 0;
      SFX.walk();
      player.walkFrame = (player.walkFrame + 1) % 4;
    }

    // Diagonal normalization
    if (dx !== 0 && dy !== 0) {
      dx *= 0.707;
      dy *= 0.707;
    }

    // Try moving X
    if (dx !== 0) {
      const nx = player.x + dx;
      const tileX = Math.floor(nx / TILE_SIZE);
      const tileY = Math.floor(player.y / TILE_SIZE);
      if (isWalkable(tileX, tileY)) {
        player.x = nx;
      }
    }

    // Try moving Y
    if (dy !== 0) {
      const ny = player.y + dy;
      const tileX = Math.floor(player.x / TILE_SIZE);
      const tileY = Math.floor(ny / TILE_SIZE);
      if (isWalkable(tileX, tileY)) {
        player.y = ny;
      }
    }
  } else {
    player.walkFrame = 0;
  }

  // Attack timer
  if (player.attackTimer > 0) player.attackTimer--;
  if (player.attackCooldown > 0) player.attackCooldown--;
  if (player.hitTimer > 0) player.hitTimer--;
  if (player.invincible > 0) player.invincible--;

  // Animation
  player.animTimer += 0.1;

  // Camera follow
  player.camX = player.x - canvas.width / 2;
  player.camY = player.y - canvas.height / 2;

  // Clamp camera
  player.camX = Math.max(0, Math.min(player.camX, WORLD_W * TILE_SIZE - canvas.width));
  player.camY = Math.max(0, Math.min(player.camY, WORLD_H * TILE_SIZE - canvas.height));

  // Check world items
  checkItemPickup();

  // Check enemy combat
  checkEnemyCombat();

  // Message timer
  if (messageTimer > 0) messageTimer--;
}

function performAttack() {
  if (player.attackCooldown > 0 || paused) return;

  const weaponStats = getWeaponStats();
  player.attacking = true;
  player.attackTimer = weaponStats.attackTime;
  player.attackCooldown = weaponStats.cooldown;

  if (weaponStats.type === 'melee') {
    SFX[weaponStats.sfx] || SFX.attack();
    const range = weaponStats.range;
    const damage = weaponStats.damage;
    const knockback = weaponStats.knockback;

    // Attack box based on facing
    const ax = player.x + (player.facing === 'right' ? range : player.facing === 'left' ? -range : 0);
    const ay = player.y + (player.facing === 'down' ? range : player.facing === 'up' ? -range : 0);

    // Spawn hit particles
    spawnImpactParticles(ax, ay, 'melee');

    // Check enemies - hit box centered on attack point with generous radius
    for (const enemy of enemies) {
      if (enemy.hp <= 0) continue;
      const dist = Math.hypot(enemy.x - ax, enemy.y - ay);
      if (dist < range + enemy.size / 2) {
        let dmg = damage;
        if (weaponStats.pierce && Math.random() < weaponStats.pierceChance) {
          dmg *= 1.5;
        }
        damageEnemy(enemy, dmg, knockback);
      }
    }
  } else if (weaponStats.type === 'ranged') {
    SFX.attackBow();
    if (player.bowAmmo > 0) {
      player.bowAmmo--;
      fireArrow(weaponStats);
    } else {
      showMessage('No arrows! Visit the shop.');
    }
  }
}

function fireArrow(stats) {
  // Simple arrow: instant hit toward facing direction
  const dirX = player.facing === 'right' ? 1 : player.facing === 'left' ? -1 : 0;
  const dirY = player.facing === 'down' ? 1 : player.facing === 'up' ? -1 : 0;

  // Check along line
  let bx = player.x;
  let by = player.y;
  const step = 8;
  for (let i = 0; i < stats.range / step; i++) {
    bx += dirX * step;
    by += dirY * step;

    // Visual trail
    spawnParticle(bx, by, '#ffff88', 1, 0.5, 10, 2);

    for (const enemy of enemies) {
      if (enemy.hp <= 0) continue;
      const dist = Math.hypot(enemy.x - bx, enemy.y - by);
      if (dist < enemy.size) {
        spawnImpactParticles(bx, by, 'bow');
        damageEnemy(enemy, stats.damage, 3);
        return;
      }
    }
  }
}

function getWeaponStats() {
  if (player.weapon === 'bow') {
    return { type: 'ranged', damage: 8, range: 120, cooldown: 25, attackTime: 10, sfx: 'attackBow' };
  }

  switch (player.weapon) {
    case 'bronze':
      return { type: 'melee', damage: 4, range: 30, cooldown: 20, attackTime: 8, knockback: 4, sfx: 'attack' };
    case 'iron':
      return { type: 'melee', damage: 7, range: 35, cooldown: 18, attackTime: 10, knockback: 6, sfx: 'attack2' };
    case 'steel':
      return { type: 'melee', damage: 11, range: 40, cooldown: 16, attackTime: 12, knockback: 8, pierce: true, pierceChance: 0.3, sfx: 'attack3' };
    default:
      return { type: 'melee', damage: 4, range: 30, cooldown: 20, attackTime: 8, knockback: 4 };
  }
}

function damageEnemy(enemy, damage, knockback) {
  SFX.enemyHit();
  enemy.hp -= damage;
  enemy.flashTimer = 8;
  enemy.state = 'hurt';

  // Knockback
  const dx = enemy.x - player.x;
  const dy = enemy.y - player.y;
  const dist = Math.hypot(dx, dy) || 1;
  enemy.knockbackX = (dx / dist) * knockback;
  enemy.knockbackY = (dy / dist) * knockback;

  // Damage number
  spawnParticle(enemy.x, enemy.y - 20, '#ffff00', 1, 0, 20, 4);

  if (enemy.hp <= 0) {
    SFX.enemyDie();
    spawnImpactParticles(enemy.x, enemy.y, 'gold');

    // Drop coins
    const coinAmount = 5 + Math.floor(Math.random() * 10);
    player.money += coinAmount;

    // Small chance for health potion drop
    if (Math.random() < 0.1) {
      worldItems.push({ type: 'healthPotion', x: enemy.x / TILE_SIZE | 0, y: enemy.y / TILE_SIZE | 0, collected: false });
    }
  }
}

function damagePlayer(amount) {
  if (player.invincible > 0) return;

  let dmg = amount;
  if (player.shieldRing) dmg = Math.floor(dmg * 0.75);

  player.hp -= dmg;
  player.hitTimer = 15;
  player.invincible = 30;
  SFX.damage();
  spawnImpactParticles(player.x, player.y, 'melee');

  if (player.hp <= 0) {
    player.hp = 0;
    gameState = 'gameover';
  }
}

function handleInteraction() {
  const px = player.x / TILE_SIZE | 0;
  const py = player.y / TILE_SIZE | 0;

  // Check save crystals
  for (const dec of mapDecorations) {
    if (dec.type === 'saveCrystal' && Math.abs(dec.x - px) <= 1 && Math.abs(dec.y - py) <= 1) {
      spawnImpactParticles(dec.x * TILE_SIZE + 16, dec.y * TILE_SIZE + 16, 'save');
      saveGame();
      return;
    }
  }

  // Check NPCs
  for (const npc of interactables) {
    const dist = Math.hypot(npc.x * TILE_SIZE + 16 - player.x, npc.y * TILE_SIZE + 16 - player.y);
    if (dist < 48) {
      if (npc.type === 'shopkeeper') {
        openShop();
        return;
      }
      if (npc.type === 'blacksmith') {
        handleBlacksmithDialogue();
        return;
      }
    }
  }

  // Check doors
  for (const door of doors) {
    const dist = Math.hypot(door.x * TILE_SIZE + 16 - player.x, door.y * TILE_SIZE + 16 - player.y);
    if (dist < 48) {
      SFX.door();
      door.open = !door.open;
      return;
    }
  }
}

function handleBlacksmithDialogue() {
  SFX.shopOpen();
  if (player.weaponLevel === 0) {
    showMessage('Blacksmith: "Find the Iron Sword in the swamp to upgrade!"');
  } else if (player.weaponLevel === 1) {
    showMessage('Blacksmith: "The Steel Blade awaits in the hidden ruins!"');
  } else {
    showMessage('Blacksmith: "Your blade is legendary. No need for more upgrades!"');
  }
}

function useHealthPotion() {
  if (player.healthPotions > 0 && player.hp < player.maxHp) {
    player.healthPotions--;
    const heal = Math.min(4, player.maxHp - player.hp);
    player.hp += heal;
    SFX.heal();
    spawnImpactParticles(player.x, player.y, 'heal');
    showMessage(`Healed ${heal} HP! (${player.healthPotions} left)`);
  }
}

function checkItemPickup() {
  const px = player.x / TILE_SIZE | 0;
  const py = player.y / TILE_SIZE | 0;

  for (const item of worldItems) {
    if (item.collected) continue;
    if (item.x === px && item.y === py) {
      item.collected = true;
      const ix = item.x * TILE_SIZE + 16;
      const iy = item.y * TILE_SIZE + 16;

      switch (item.type) {
        case 'coin':
          player.money += item.amount || 10;
          SFX.coin();
          spawnImpactParticles(ix, iy, 'gold');
          showMessage(`+${item.amount} Gold`);
          break;
        case 'healthPotion':
          player.healthPotions += (item.qty || 1);
          SFX.pickup();
          spawnImpactParticles(ix, iy, 'heal');
          showMessage('Health Potion acquired!');
          break;
        case 'healthContainer':
          if (player.hpContainers < player.maxContainers) {
            player.hpContainers++;
            player.maxHp = player.hpContainers * 2;
            player.hp = player.maxHp;
            SFX.heal();
            spawnImpactParticles(ix, iy, 'heal');
            showMessage(`Health increased! (${player.hpContainers} containers)`);
          } else {
            showMessage('Already at max health containers!');
          }
          break;
        case 'weaponIron':
          player.weapon = 'iron';
          player.weaponLevel = 1;
          SFX.weaponUp();
          spawnImpactParticles(ix, iy, 'weapon');
          showMessage('Iron Sword acquired! (+damage, +range)');
          break;
        case 'weaponSteel':
          player.weapon = 'steel';
          player.weaponLevel = 2;
          SFX.weaponUp();
          spawnImpactParticles(ix, iy, 'weapon');
          showMessage('Steel Blade acquired! (Mighty damage, piercing!)');
          break;
        case 'rangedBow':
          player.hasBow = true;
          player.bowAmmo = 10;
          SFX.weaponUp();
          spawnImpactParticles(ix, iy, 'weapon');
          showMessage('Bow discovered! [2] to equip, [1] for sword. 10 arrows included.');
          break;
        case 'speedBoots':
          player.speedBoots = true;
          SFX.pickup();
          spawnImpactParticles(ix, iy, 'weapon');
          showMessage('Speed Boots! (+30% movement)');
          break;
        case 'shieldRing':
          player.shieldRing = true;
          SFX.pickup();
          spawnImpactParticles(ix, iy, 'weapon');
          showMessage('Shield Ring! (-25% damage taken)');
          break;
      }
    }
  }
}

function checkEnemyCombat() {
  for (const enemy of enemies) {
    if (enemy.hp <= 0) continue;

    // Player hit enemy by bumping (very minor)
    const dist = Math.hypot(enemy.x - player.x, enemy.y - player.y);
    if (dist < enemy.size + 10 && dist > enemy.size + 5) {
      // Check if we're overlapping
      const overlap = (enemy.size + 10) - dist;
      if (overlap > 0) {
        const dx = enemy.x - player.x;
        const dy = enemy.y - player.y;
        const d = Math.hypot(dx, dy) || 1;
        player.x -= (dx / d) * overlap;
        player.y -= (dy / d) * overlap;
      }
    }
  }
}

// ============================================================
// ENEMY UPDATE
// ============================================================
function updateEnemies() {
  if (paused || gameState !== 'playing') return;

  for (const enemy of enemies) {
    if (enemy.hp <= 0) continue;

    const distToPlayer = Math.hypot(player.x - enemy.x, player.y - enemy.y);
    const animTimer = enemy.animTimer += 0.1;

    // Knockback decay
    enemy.knockbackX *= 0.8;
    enemy.knockbackY *= 0.8;
    if (enemy.flashTimer > 0) enemy.flashTimer--;
    if (enemy.attackCooldown > 0) enemy.attackCooldown--;

    // AI state
    if (enemy.state === 'hurt') {
      enemy.x += enemy.knockbackX;
      enemy.y += enemy.knockbackY;
      if (enemy.knockbackX < 0.1 && enemy.knockbackY < 0.1 && enemy.knockbackX > -0.1 && enemy.knockbackY > -0.1) {
        enemy.state = 'idle';
      }
    } else if (distToPlayer < 200) {
      // Chase / aggressive behavior
      let dx = player.x - enemy.x;
      let dy = player.y - enemy.y;
      const d = Math.hypot(dx, dy) || 1;
      dx /= d;
      dy /= d;

      let moveSpeed = enemy.speed;
      if (enemy.ai === 'aggressive' && distToPlayer > 30) {
        moveSpeed = enemy.speed * 1.2;
      } else if (enemy.ai === 'chase') {
        moveSpeed = enemy.speed * (distToPlayer > 100 ? 1.2 : 1.0);
      }

      const nx = enemy.x + dx * moveSpeed;
      const ny = enemy.y + dy * moveSpeed;

      if (isWalkable(Math.floor(nx / TILE_SIZE), Math.floor(enemy.y / TILE_SIZE))) {
        enemy.x = nx;
      }
      if (isWalkable(Math.floor(enemy.x / TILE_SIZE), Math.floor(ny / TILE_SIZE))) {
        enemy.y = ny;
      }

      // Attack player if close enough
      if (distToPlayer < enemy.size + 12 && enemy.attackCooldown <= 0) {
        enemy.attackCooldown = 40;
        damagePlayer(enemy.damage);
      }
    } else {
      // Wander/idle
      if (Math.random() < 0.01) {
        enemy.wanderDirX = (Math.random() - 0.5) * 2;
        enemy.wanderDirY = (Math.random() - 0.5) * 2;
      }
      const wdx = enemy.wanderDirX || 0;
      const wdy = enemy.wanderDirY || 0;
      const w = Math.hypot(wdx, wdy) || 1;

      const nx = enemy.x + (wdx / w) * enemy.speed * 0.3;
      const ny = enemy.y + (wdy / w) * enemy.speed * 0.3;

      if (isWalkable(Math.floor(nx / TILE_SIZE), Math.floor(enemy.y / TILE_SIZE))) {
        enemy.x = nx;
      }
      if (isWalkable(Math.floor(enemy.x / TILE_SIZE), Math.floor(ny / TILE_SIZE))) {
        enemy.y = ny;
      }
      enemy.state = 'idle';
    }

    // Keep in bounds
    enemy.x = Math.max(16, Math.min(WORLD_W * TILE_SIZE - 16, enemy.x));
    enemy.y = Math.max(16, Math.min(WORLD_H * TILE_SIZE - 16, enemy.y));

    // Boss check
    if (enemy.type === 'bossGuard' && enemy.hp <= 0 && !enemy.defeatedAnnounced) {
      enemy.defeatedAnnounced = true;
      SFX.bossWarning();
      showMessage('The path to the Shadow Lord is open...');
      // Open boss door
      for (const door of doors) {
        if (door.x === 78 && door.y === 38) door.open = true;
      }
    }
  }
}

// ============================================================
// RENDERING
// ============================================================
function render() {
  ctx.fillStyle = '#000';
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  if (gameState === 'title') {
    renderTitle();
    return;
  }

  const camX = player.camX;
  const camY = player.camY;

  // Visible tile range
  const startTileX = Math.floor(camX / TILE_SIZE);
  const startTileY = Math.floor(camY / TILE_SIZE);
  const endTileX = startTileX + Math.ceil(canvas.width / TILE_SIZE) + 1;
  const endTileY = startTileY + Math.ceil(canvas.height / TILE_SIZE) + 1;

  // Draw map
  for (let y = startTileY; y < endTileY; y++) {
    for (let x = startTileX; x < endTileX; x++) {
      if (!inBounds(x, y)) continue;

      const screenX = x * TILE_SIZE - camX;
      const screenY = y * TILE_SIZE - camY;
      const tile = map[y][x];

      // Base tile color
      ctx.fillStyle = getTileColor(tile);
      ctx.fillRect(screenX, screenY, TILE_SIZE, TILE_SIZE);

      // Add variation
      if (tile === 0 || tile === 7) {
        ctx.fillStyle = 'rgba(0,0,0,0.1)';
        if ((x + y) % 3 === 0) ctx.fillRect(screenX + 8, screenY + 8, 4, 4);
      }

      // Tile details
      if (tile === 2) {
        ctx.fillStyle = 'rgba(100,180,255,0.3)';
        ctx.fillRect(screenX + 2, screenY + 2 + (Math.sin(Date.now() / 500 + x) * 3) | 0, 28, 4);
      }

      if (tile === 8) {
        ctx.fillStyle = 'rgba(200,230,255,0.4)';
        if ((x * 7 + y * 3) % 5 === 0) ctx.fillRect(screenX + 10, screenY + 10, 3, 3);
      }

      if (tile === 5) {
        // Door frame
        ctx.fillStyle = '#443322';
        ctx.fillRect(screenX + 4, screenY + 4, 24, 24);
      }
    }
  }

  // Draw doors overlay
  for (const door of doors) {
    const sx = door.x * TILE_SIZE - camX;
    const sy = door.y * TILE_SIZE - camY;
    if (!door.open) {
      ctx.fillStyle = '#8B4513';
      ctx.fillRect(sx + 6, sy + 2, 20, 28);
      ctx.fillStyle = '#FFD700';
      ctx.fillRect(sx + 14, sy + 14, 4, 4); // Knob
      ctx.fillStyle = '#654321';
      ctx.fillRect(sx + 10, sy + 6, 2, 20);
      ctx.fillRect(sx + 20, sy + 6, 2, 20);
    } else {
      // Open door (passable, visual indicator)
      ctx.fillStyle = '#3a2211';
      ctx.fillRect(sx + 2, sy + 2, 6, 28);
      ctx.fillRect(sx + 24, sy + 2, 6, 28);
    }
  }

  // Draw decorations
  for (const dec of mapDecorations) {
    const sx = dec.x * TILE_SIZE - camX;
    const sy = dec.y * TILE_SIZE - camY;

    if (sx < -40 || sx > canvas.width + 40 || sy < -40 || sy > canvas.height + 40) continue;

    if (dec.type === 'tree') {
      // Trunk
      ctx.fillStyle = '#5C4033';
      ctx.fillRect(sx + 12, sy + 16, 8, 16);
      // Canopy
      ctx.fillStyle = '#228B22';
      ctx.beginPath();
      ctx.arc(sx + 16, sy + 12, 14, 0, Math.PI * 2);
      ctx.fill();
      ctx.fillStyle = '#32CD32';
      ctx.beginPath();
      ctx.arc(sx + 14, sy + 8, 10, 0, Math.PI * 2);
      ctx.fill();
    } else if (dec.type === 'flower') {
      ctx.fillStyle = '#228B22';
      ctx.fillRect(sx + 15, sy + 18, 2, 14);
      ctx.fillStyle = dec.color || '#ff66aa';
      ctx.beginPath();
      ctx.arc(sx + 16, sy + 16, 4, 0, Math.PI * 2);
      ctx.fill();
    } else if (dec.type === 'rock') {
      ctx.fillStyle = '#666';
      ctx.beginPath();
      ctx.arc(sx + 16, sy + 20, 8, Math.PI, 0);
      ctx.fill();
      ctx.fillStyle = '#888';
      ctx.beginPath();
      ctx.arc(sx + 14, sy + 18, 5, Math.PI, 0);
      ctx.fill();
    } else if (dec.type === 'saveCrystal') {
      // Glowing crystal
      const pulse = Math.sin(Date.now() / 300) * 0.3 + 0.7;
      ctx.fillStyle = `rgba(180, 120, 255, ${pulse})`;
      ctx.beginPath();
      ctx.moveTo(sx + 16, sy + 4);
      ctx.lineTo(sx + 24, sy + 16);
      ctx.lineTo(sx + 16, sy + 28);
      ctx.lineTo(sx + 8, sy + 16);
      ctx.closePath();
      ctx.fill();
      // Glow
      ctx.fillStyle = `rgba(180, 120, 255, ${pulse * 0.3})`;
      ctx.beginPath();
      ctx.arc(sx + 16, sy + 16, 20, 0, Math.PI * 2);
      ctx.fill();
      // Label
      ctx.fillStyle = '#cc88ff';
      ctx.font = '10px monospace';
      ctx.textAlign = 'center';
      ctx.fillText('SAVE', sx + 16, sy - 2);
    } else if (dec.type === 'bossStone') {
      ctx.fillStyle = '#333';
      ctx.fillRect(sx + 6, sy + 12, 20, 20);
      ctx.fillStyle = '#cc0000';
      ctx.fillRect(sx + 12, sy + 16, 8, 8);
      ctx.fillStyle = '#ff4444';
      ctx.font = '8px monospace';
      ctx.textAlign = 'center';
      ctx.fillText('BOSS', sx + 16, sy + 6);
    }
  }

  // Draw world items
  for (const item of worldItems) {
    if (item.collected) continue;

    const sx = item.x * TILE_SIZE - camX + 16;
    const sy = item.y * TILE_SIZE - camY + 16;

    if (sx < -20 || sx > canvas.width + 20 || sy < -20 || sy > canvas.height + 20) continue;

    const bob = Math.sin(Date.now() / 400 + item.x) * 2;

    if (item.type === 'coin') {
      ctx.fillStyle = '#ffd700';
      ctx.beginPath();
      ctx.arc(sx, sy + bob, 4, 0, Math.PI * 2);
      ctx.fill();
      ctx.fillStyle = '#ffaa00';
      ctx.font = '6px monospace';
      ctx.textAlign = 'center';
      ctx.fillText('$', sx, sy + bob + 2);
    } else if (item.type === 'healthPotion') {
      ctx.fillStyle = '#ff4444';
      ctx.fillRect(sx - 4, sy - 6 + bob, 8, 10);
      ctx.fillStyle = '#ff8888';
      ctx.fillRect(sx - 2, sy - 8 + bob, 4, 3);
      ctx.fillStyle = '#ffffff';
      ctx.font = '7px monospace';
      ctx.textAlign = 'center';
      ctx.fillText('+', sx, sy + 2 + bob);
    } else if (item.type === 'healthContainer') {
      ctx.fillStyle = '#ff4444';
      ctx.font = '20px monospace';
      ctx.textAlign = 'center';
      ctx.fillText('♥', sx, sy + 7 + bob);
      ctx.fillStyle = `rgba(255, 100, 100, ${0.3 + Math.sin(Date.now() / 200) * 0.2})`;
      ctx.beginPath();
      ctx.arc(sx, sy + bob, 18, 0, Math.PI * 2);
      ctx.fill();
    } else if (item.type === 'weaponIron' || item.type === 'weaponSteel') {
      ctx.fillStyle = item.type === 'weaponIron' ? '#aaaaaa' : '#ccddff';
      ctx.fillRect(sx - 2, sy - 10 + bob, 4, 14);
      ctx.fillStyle = '#664422';
      ctx.fillRect(sx - 4, sy + 4 + bob, 8, 4);
      ctx.fillStyle = '#ffffff';
      ctx.font = '7px monospace';
      ctx.textAlign = 'center';
      ctx.fillText(item.type === 'weaponIron' ? 'IRON' : 'STEEL', sx, sy - 14 + bob);
    } else if (item.type === 'rangedBow') {
      ctx.strokeStyle = '#44aaff';
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.arc(sx, sy + bob, 8, -Math.PI * 0.3, Math.PI * 0.3 + Math.PI);
      ctx.stroke();
      ctx.fillStyle = '#44aaff';
      ctx.font = '7px monospace';
      ctx.textAlign = 'center';
      ctx.fillText('BOW', sx, sy - 14 + bob);
    } else if (item.type === 'speedBoots') {
      ctx.fillStyle = '#ff8800';
      ctx.fillRect(sx - 6, sy - 3 + bob, 12, 8);
      ctx.fillStyle = '#ffff00';
      ctx.font = '6px monospace';
      ctx.textAlign = 'center';
      ctx.fillText('BOOTS', sx, sy + 6 + bob);
    } else if (item.type === 'shieldRing') {
      ctx.strokeStyle = '#44ff44';
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.arc(sx, sy + bob, 6, 0, Math.PI * 2);
      ctx.stroke();
      ctx.fillStyle = '#44ff44';
      ctx.font = '6px monospace';
      ctx.textAlign = 'center';
      ctx.fillText('RING', sx, sy + 12 + bob);
    }
  }

  // Draw NPCs
  for (const npc of interactables) {
    const sx = npc.x * TILE_SIZE - camX;
    const sy = npc.y * TILE_SIZE - camY;

    if (sx < -40 || sx > canvas.width + 40 || sy < -40 || sy > canvas.height + 40) continue;

    if (npc.type === 'shopkeeper') {
      drawCharacter(sx + 16, sy + 16, '#cc6633', '#663311', 'shopkeeper');
    } else if (npc.type === 'blacksmith') {
      drawCharacter(sx + 16, sy + 16, '#555555', '#333333', 'blacksmith');
    }
  }

  // Draw enemies
  for (const enemy of enemies) {
    if (enemy.hp <= 0) continue;

    const sx = enemy.x - camX;
    const sy = enemy.y - camY;

    if (sx < -40 || sx > canvas.width + 40 || sy < -40 || sy > canvas.height + 40) continue;

    // Body
    ctx.fillStyle = enemy.flashTimer > 0 ? '#ffffff' : enemy.color;
    const bobAmt = Math.sin(enemy.animTimer * 3) * 2;

    if (enemy.type === 'slime' || enemy.type === 'iceSlime') {
      // Slime shape
      ctx.beginPath();
      ctx.ellipse(sx, sy + bobAmt, enemy.size / 2, enemy.size / 2.5, 0, 0, Math.PI * 2);
      ctx.fill();
      // Eyes
      ctx.fillStyle = '#000';
      ctx.fillRect(sx - 4, sy - 4 + bobAmt, 3, 3);
      ctx.fillRect(sx + 2, sy - 4 + bobAmt, 3, 3);
    } else if (enemy.type === 'wolf' || enemy.type === 'frostWolf') {
      // Wolf shape
      ctx.beginPath();
      ctx.ellipse(sx, sy + bobAmt, enemy.size / 2, enemy.size / 3, 0, 0, Math.PI * 2);
      ctx.fill();
      // Head
      const dx = player.x - enemy.x > 0 ? 1 : -1;
      ctx.beginPath();
      ctx.arc(sx + dx * 8, sy - 4 + bobAmt, 6, 0, Math.PI * 2);
      ctx.fill();
      // Eyes
      ctx.fillStyle = '#ff0000';
      ctx.fillRect(sx + dx * 6, sy - 6 + bobAmt, 2, 2);
    } else if (enemy.type === 'skeleton') {
      // Skeleton
      ctx.fillStyle = enemy.flashTimer > 0 ? '#fff' : '#ddd';
      ctx.fillRect(sx - 4, sy - 8 + bobAmt, 8, 16);
      ctx.beginPath();
      ctx.arc(sx, sy - 12 + bobAmt, 5, 0, Math.PI * 2);
      ctx.fill();
      ctx.fillStyle = '#000';
      ctx.fillRect(sx - 2, sy - 13 + bobAmt, 1, 1);
      ctx.fillRect(sx + 1, sy - 13 + bobAmt, 1, 1);
    } else if (enemy.type === 'goblin') {
      ctx.fillStyle = enemy.flashTimer > 0 ? '#fff' : '#558833';
      ctx.fillRect(sx - 5, sy - 6 + bobAmt, 10, 12);
      ctx.beginPath();
      ctx.arc(sx, sy - 10 + bobAmt, 5, 0, Math.PI * 2);
      ctx.fill();
      ctx.fillStyle = '#ff0000';
      ctx.fillRect(sx - 2, sy - 11 + bobAmt, 1, 1);
      ctx.fillRect(sx + 1, sy - 11 + bobAmt, 1, 1);
    } else if (enemy.type === 'bossGuard') {
      // Big menacing enemy
      ctx.fillStyle = enemy.flashTimer > 0 ? '#fff' : '#882222';
      ctx.fillRect(sx - 12, sy - 12 + bobAmt, 24, 24);
      ctx.fillStyle = '#cc0000';
      ctx.fillRect(sx - 16, sy - 16 + bobAmt, 32, 4);
      // Eyes
      ctx.fillStyle = '#ffff00';
      ctx.fillRect(sx - 6, sy - 8 + bobAmt, 4, 4);
      ctx.fillRect(sx + 2, sy - 8 + bobAmt, 4, 4);
    }

    // Health bar
    if (enemy.hp < enemy.maxHp) {
      const barW = 24;
      const barH = 3;
      const ratio = enemy.hp / enemy.maxHp;
      ctx.fillStyle = '#330000';
      ctx.fillRect(sx - barW / 2, sy - enemy.size - 6, barW, barH);
      ctx.fillStyle = ratio > 0.5 ? '#44ff44' : ratio > 0.25 ? '#ffaa00' : '#ff4444';
      ctx.fillRect(sx - barW / 2, sy - enemy.size - 6, barW * ratio, barH);
    }
  }

  // Draw player
  drawPlayer(camX, camY);

  // Draw particles
  drawParticles(camX, camY);

  // Draw HUD
  drawHUD();

  // Draw message
  if (gameMessage && messageTimer > 0) {
    const alpha = Math.min(1, messageTimer / 30);
    ctx.fillStyle = `rgba(0, 0, 0, ${alpha * 0.8})`;
    ctx.fillRect(canvas.width / 2 - 200, canvas.height - 50, 400, 30);
    ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`;
    ctx.font = '14px monospace';
    ctx.textAlign = 'center';
    ctx.fillText(gameMessage, canvas.width / 2, canvas.height - 30);
  }

  // Draw pause/game over/victory overlays
  if (gameState === 'pauseMenu') renderPauseMenu();
  if (gameState === 'gameover') renderGameOver();
  if (gameState === 'victory') renderVictory();

  // Draw shop overlay
  if (shopOpen) renderShop();
}

function drawCharacter(x, y, bodyColor, darkColor, type) {
  const bob = Math.sin(Date.now() / 500) * 1;

  // Body
  ctx.fillStyle = bodyColor;
  ctx.fillRect(x - 6, y - 8 + bob, 12, 16);

  // Head
  ctx.fillStyle = '#ffcc88';
  ctx.beginPath();
  ctx.arc(x, y - 12 + bob, 6, 0, Math.PI * 2);
  ctx.fill();

  // Hair/hat
  if (type === 'shopkeeper') {
    ctx.fillStyle = darkColor;
    ctx.fillRect(x - 7, y - 18 + bob, 14, 4);
    ctx.fillRect(x - 7, y - 18 + bob, 4, 8);
  } else if (type === 'blacksmith') {
    ctx.fillStyle = '#333';
    ctx.fillRect(x - 7, y - 16 + bob, 14, 3);
    ctx.fillStyle = '#666';
    ctx.fillRect(x - 5, y - 18 + bob, 10, 3);
  }

  // Eyes
  ctx.fillStyle = '#000';
  ctx.fillRect(x - 2, y - 13 + bob, 1, 1);
  ctx.fillRect(x + 1, y - 13 + bob, 1, 1);

  // Label
  ctx.fillStyle = '#ffff00';
  ctx.font = '8px monospace';
  ctx.textAlign = 'center';
  ctx.fillText('!', x, y - 22 + bob);
}

function drawPlayer(camX, camY) {
  const sx = player.x - camX;
  const sy = player.y - camY;

  // Blink when invincible
  if (player.invincible > 0 && Math.floor(player.invincible / 3) % 2 === 0) return;

  const bob = Math.sin(player.animTimer * 4) * (player.walkFrame > 0 ? 2 : 0);
  const isAttacking = player.attackTimer > 0;

  // Shadow
  ctx.fillStyle = 'rgba(0,0,0,0.3)';
  ctx.beginPath();
  ctx.ellipse(sx, sy + 12, 10, 4, 0, 0, Math.PI * 2);
  ctx.fill();

  // Body color based on equipment
  const bodyColor = player.speedBoots ? '#4488cc' : '#44aa66';
  const accentColor = player.shieldRing ? '#44aaff' : '#66aa66';

  // Legs (animated)
  ctx.fillStyle = '#334455';
  if (player.walkFrame > 0) {
    const legOffset = Math.sin(player.walkFrame * Math.PI / 2) * 4;
    ctx.fillRect(sx - 4, sy + 6 + bob, 3, 8);
    ctx.fillRect(sx + 1, sy + 6 + bob, 3, 8);
    ctx.fillRect(sx - 4 - legOffset, sy + 10 + bob, 4, 4);
    ctx.fillRect(sx + 1 + legOffset, sy + 10 + bob, 4, 4);
  } else {
    ctx.fillRect(sx - 4, sy + 6 + bob, 3, 8);
    ctx.fillRect(sx + 1, sy + 6 + bob, 3, 8);
  }

  // Body
  ctx.fillStyle = bodyColor;
  ctx.fillRect(sx - 7, sy - 4 + bob, 14, 12);

  // Shield ring visual
  if (player.shieldRing) {
    ctx.strokeStyle = `rgba(68, 170, 255, ${0.5 + Math.sin(Date.now() / 200) * 0.3})`;
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.arc(sx, sy + bob, 18, 0, Math.PI * 2);
    ctx.stroke();
  }

  // Head
  ctx.fillStyle = '#ffcc88';
  ctx.beginPath();
  ctx.arc(sx, sy - 10 + bob, 6, 0, Math.PI * 2);
  ctx.fill();

  // Eyes based on facing
  ctx.fillStyle = '#000';
  if (player.facing === 'left') {
    ctx.fillRect(sx - 3, sy - 11 + bob, 1, 1);
    ctx.fillRect(sx - 1, sy - 11 + bob, 1, 1);
  } else if (player.facing === 'right') {
    ctx.fillRect(sx + 1, sy - 11 + bob, 1, 1);
    ctx.fillRect(sx + 3, sy - 11 + bob, 1, 1);
  } else if (player.facing === 'up') {
    ctx.fillRect(sx - 2, sy - 12 + bob, 1, 1);
    ctx.fillRect(sx + 1, sy - 12 + bob, 1, 1);
  } else {
    ctx.fillRect(sx - 2, sy - 11 + bob, 1, 1);
    ctx.fillRect(sx + 1, sy - 11 + bob, 1, 1);
  }

  // Weapon
  if (player.weapon === 'bow') {
    // Draw bow
    ctx.strokeStyle = '#8B4513';
    ctx.lineWidth = 2;
    if (player.facing === 'right') {
      ctx.beginPath();
      ctx.arc(sx + 8, sy + bob, 10, -Math.PI * 0.4, Math.PI * 0.4 + Math.PI);
      ctx.stroke();
    } else if (player.facing === 'left') {
      ctx.beginPath();
      ctx.arc(sx - 8, sy + bob, 10, Math.PI * 1.6, -Math.PI * 0.4 + Math.PI);
      ctx.stroke();
    } else if (player.facing === 'down') {
      ctx.beginPath();
      ctx.arc(sx, sy + 8 + bob, 10, Math.PI * 0.6, Math.PI * 1.4 + Math.PI);
      ctx.stroke();
    } else {
      ctx.beginPath();
      ctx.arc(sx, sy - 8 + bob, 10, -Math.PI * 0.6, Math.PI * 0.4);
      ctx.stroke();
    }
  } else {
    // Sword based on level
    const swordColor = player.weapon === 'bronze' ? '#cd7f32' : player.weapon === 'iron' ? '#aaaaaa' : '#ccddff';
    const swordLength = player.weapon === 'bronze' ? 14 : player.weapon === 'iron' ? 16 : 18;

    if (isAttacking) {
      // Attack animation - swing
      const swingProgress = 1 - (player.attackTimer / getWeaponStats().attackTime);
      let swingAngle = 0;
      if (player.facing === 'right') swingAngle = -Math.PI / 2 + swingProgress * Math.PI;
      else if (player.facing === 'left') swingAngle = Math.PI / 2 - swingProgress * Math.PI;
      else if (player.facing === 'down') swingAngle = Math.PI - swingProgress * Math.PI;
      else swingAngle = swingProgress * Math.PI;

      ctx.save();
      ctx.translate(sx, sy + bob);
      ctx.rotate(swingAngle);
      ctx.fillStyle = swordColor;
      ctx.fillRect(0, -1, swordLength, 3);
      ctx.fillStyle = '#664422';
      ctx.fillRect(-4, -2, 4, 5);
      ctx.restore();
    } else {
      // Idle weapon
      ctx.fillStyle = swordColor;
      if (player.facing === 'right') {
        ctx.fillRect(sx + 6, sy - 2 + bob, swordLength, 3);
        ctx.fillStyle = '#664422';
        ctx.fillRect(sx + 4, sy - 3 + bob, 4, 5);
      } else if (player.facing === 'left') {
        ctx.fillRect(sx - 6 - swordLength, sy - 2 + bob, swordLength, 3);
        ctx.fillStyle = '#664422';
        ctx.fillRect(sx - 8, sy - 3 + bob, 4, 5);
      } else if (player.facing === 'down') {
        ctx.fillRect(sx + 4, sy + 2 + bob, 3, swordLength);
        ctx.fillStyle = '#664422';
        ctx.fillRect(sx + 3, sy + swordLength + bob, 5, 4);
      } else {
        ctx.fillRect(sx - 4, sy - 12 + bob - swordLength, 3, swordLength);
        ctx.fillStyle = '#664422';
        ctx.fillRect(sx - 5, sy - 12 + bob - swordLength, 5, 4);
      }
    }
  }

  // Damage flash
  if (player.hitTimer > 0) {
    ctx.fillStyle = 'rgba(255, 0, 0, 0.4)';
    ctx.beginPath();
    ctx.arc(sx, sy + bob, 16, 0, Math.PI * 2);
    ctx.fill();
  }
}

function drawHUD() {
  // Background panel
  ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
  ctx.fillRect(8, 8, 260, 65);
  ctx.strokeStyle = '#555';
  ctx.strokeRect(8, 8, 260, 65);

  // Health hearts (each heart = 2 HP, shown as containers)
  let hx = 18;
  for (let i = 0; i < player.hpContainers; i++) {
    const heartY = 22;
    const hpInContainer = player.hp - i * 2;

    // Container frame
    ctx.fillStyle = '#333';
    ctx.fillRect(hx - 2, heartY - 10, 20, 16);

    if (hpInContainer >= 2) {
      // Full heart
      drawHeart(hx + 8, heartY - 2, '#ff4444');
    } else if (hpInContainer >= 1) {
      // Half heart
      ctx.fillStyle = '#ff4444';
      drawHeart(hx + 8, heartY - 2, '#ff4444');
      ctx.fillStyle = '#333';
      ctx.fillRect(hx + 10, heartY - 10, 8, 16);
    }
    hx += 22;
  }

  // Gold
  ctx.fillStyle = '#ffd700';
  ctx.font = '14px monospace';
  ctx.textAlign = 'left';
  ctx.fillText(`💰 ${player.money}`, 18, 50);

  // Weapon info
  const weaponName = player.weapon === 'bronze' ? 'Bronze' : player.weapon === 'iron' ? 'Iron' : player.weapon === 'steel' ? 'Steel' : 'Bow';
  const weaponIcon = player.weapon === 'bow' ? '🏹' : '⚔️';
  ctx.fillStyle = '#aaaaaa';
  ctx.font = '12px monospace';
  ctx.fillText(`${weaponIcon} ${weaponName}`, 18, 68);
  if (player.weapon === 'bow') {
    ctx.fillStyle = '#44aaff';
    ctx.fillText(`(${player.bowAmmo} arrows)`, 110, 68);
  }

  // Accessories indicator
  let ax = 130;
  if (player.speedBoots) {
    ctx.fillStyle = '#ff8800';
    ctx.font = '12px monospace';
    ctx.fillText('👟', ax, 50);
    ax += 16;
  }
  if (player.shieldRing) {
    ctx.fillStyle = '#44ff44';
    ctx.font = '12px monospace';
    ctx.fillText('💍', ax, 50);
    ax += 16;
  }

  // Health potions
  if (player.healthPotions > 0) {
    ctx.fillStyle = '#ff4444';
    ctx.font = '12px monospace';
    ctx.fillText(`🧪 x${player.healthPotions}`, ax, 50);
  }

  // Right side - minimap-ish area indicator
  const px = player.x / TILE_SIZE | 0;
  const py = player.y / TILE_SIZE | 0;
  let areaName = 'Village';
  if (px < 25 && py < 20) areaName = 'Dark Forest';
  else if (px < 20 && py > 25 && py < 45) areaName = 'Murky Swamp';
  else if (px > 55 && px < 65 && py > 30 && py < 40) areaName = 'Mountain Path';
  else if (px > 65 && px < 95 && py > 22 && py < 42) areaName = 'Shadow Cave';
  else if (px > 55 && px < 80 && py < 22) areaName = 'Frost Cave';
  else if (px > 33 && px < 50 && py > 55 && py < 66) areaName = 'Hidden Ruins';
  else if (px > 85) areaName = 'Boss Arena';

  ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
  ctx.fillRect(canvas.width - 180, 8, 172, 28);
  ctx.fillStyle = '#cccccc';
  ctx.font = '12px monospace';
  ctx.textAlign = 'right';
  ctx.fillText(`Location: ${areaName}`, canvas.width - 16, 26);

  // Controls hint
  ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
  ctx.fillRect(canvas.width - 200, canvas.height - 35, 192, 28);
  ctx.fillStyle = '#888';
  ctx.font = '10px monospace';
  ctx.textAlign = 'right';
  ctx.fillText('WASD/Arrows: Move | Z/Space: Attack | F: Interact | U: Heal | Esc: Menu', canvas.width - 16, canvas.height - 16);
}

function drawHeart(x, y, color) {
  ctx.fillStyle = color;
  ctx.beginPath();
  ctx.moveTo(x, y - 4);
  ctx.bezierCurveTo(x - 7, y - 12, x - 12, y - 2, x, y + 4);
  ctx.bezierCurveTo(x + 12, y - 2, x + 7, y - 12, x, y - 4);
  ctx.fill();
}

function renderTitle() {
  // Animated background
  const t = Date.now() / 1000;
  ctx.fillStyle = '#0a0a1a';
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  // Stars
  ctx.fillStyle = '#ffffff';
  for (let i = 0; i < 100; i++) {
    const x = ((i * 137.5) % canvas.width);
    const y = ((i * 97.3 + t * 10) % canvas.height);
    const size = (Math.sin(t + i) * 0.5 + 0.5) * 2;
    ctx.fillRect(x, y, size, size);
  }

  // Title
  ctx.fillStyle = '#ffcc00';
  ctx.font = '48px monospace';
  ctx.textAlign = 'center';
  ctx.fillText('REALM OF ECHOES', canvas.width / 2, 180);

  ctx.fillStyle = '#aaaaaa';
  ctx.font = '16px monospace';
  ctx.fillText('An Action-Adventure RPG', canvas.width / 2, 220);

  // Menu options
  const options = ['New Game', 'Load Game', 'Controls'];
  for (let i = 0; i < options.length; i++) {
    const y = 300 + i * 40;
    if (i === titleSelection) {
      ctx.fillStyle = '#ffcc00';
      ctx.fillRect(canvas.width / 2 - 120, y - 14, 240, 28);
      ctx.fillStyle = '#000';
    } else {
      ctx.fillStyle = '#888';
    }
    ctx.font = '18px monospace';
    ctx.fillText(`> ${options[i]} <`, canvas.width / 2, y + 4);
  }

  // Instructions for title
  ctx.fillStyle = '#666';
  ctx.font = '11px monospace';
  ctx.fillText('Arrow Keys/WASD to select, Enter to confirm', canvas.width / 2, 460);

  // Features
  ctx.fillStyle = '#555';
  ctx.font = '10px monospace';
  const features = [
    '⚔️ Dynamic combat with weapon upgrades',
    '🗺️ Explore multiple regions',
    '🛒 In-game shop & economy',
    '💾 Save your progress at crystals',
    '🏹 Discover ranged combat mid-journey',
    '🎯 Balanced progression for ~1 hour gameplay'
  ];
  for (let i = 0; i < features.length; i++) {
    ctx.fillText(features[i], canvas.width / 2, 490 + i * 16);
  }
}

function renderPauseMenu() {
  ctx.fillStyle = 'rgba(0, 0, 0, 0.75)';
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  ctx.fillStyle = '#ffcc00';
  ctx.font = '32px monospace';
  ctx.textAlign = 'center';
  ctx.fillText('PAUSED', canvas.width / 2, 180);

  const options = ['Resume', 'Save Game', 'Clear Save', 'Quit to Title'];
  for (let i = 0; i < options.length; i++) {
    const y = 240 + i * 40;
    ctx.fillStyle = '#888';
    ctx.font = '18px monospace';
    ctx.fillText(`[ ${i + 1 }] ${options[i]}`, canvas.width / 2, y);
  }

  ctx.fillStyle = '#666';
  ctx.font = '12px monospace';
  ctx.fillText('Press ESC or corresponding number', canvas.width / 2, 440);

  // Handle pause menu input
  document.onkeydown = function(e) {
    if (gameState !== 'pauseMenu') return;
    if (e.key === 'Escape') { gameState = 'playing'; }
    else if (e.key === '1') { gameState = 'playing'; }
    else if (e.key === '2') { saveGame(); }
    else if (e.key === '3') { clearSave(); }
    else if (e.key === '4') { gameState = 'title'; titleSelection = 0; }
  };
}

function renderGameOver() {
  ctx.fillStyle = 'rgba(60, 0, 0, 0.85)';
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  ctx.fillStyle = '#ff4444';
  ctx.font = '40px monospace';
  ctx.textAlign = 'center';
  ctx.fillText('YOU HAVE FALLEN', canvas.width / 2, 220);

  ctx.fillStyle = '#cccccc';
  ctx.font = '16px monospace';
  ctx.fillText(`Gold collected: ${player.money}`, canvas.width / 2, 280);
  ctx.fillText(`Weapons mastered: ${player.weaponLevel + 1}/3`, canvas.width / 2, 310);

  ctx.fillStyle = '#ffcc00';
  ctx.font = '18px monospace';
  ctx.fillText('[ R ] Retry from last save', canvas.width / 2, 380);
  ctx.fillText('[ T ] Return to title', canvas.width / 2, 420);

  document.onkeydown = function(e) {
    if (gameState !== 'gameover') return;
    if (e.key.toLowerCase() === 'r') {
      const loaded = loadGame();
      if (loaded) gameState = 'playing';
      else { gameState = 'title'; }
    } else if (e.key.toLowerCase() === 't') {
      gameState = 'title';
      titleSelection = 0;
    }
  };
}

function renderVictory() {
  ctx.fillStyle = 'rgba(0, 20, 40, 0.85)';
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  const t = Date.now() / 1000;

  // Victory particles
  for (let i = 0; i < 5; i++) {
    const x = (Math.sin(t * 2 + i * 1.5) * 300 + canvas.width / 2);
    const y = (Math.cos(t * 1.5 + i * 2) * 150 + canvas.height / 2);
    ctx.fillStyle = ['#ffcc00', '#ff4444', '#44ff44', '#44aaff'][i % 4];
    ctx.beginPath();
    ctx.arc(x, y, 3 + Math.sin(t * 3 + i) * 2, 0, Math.PI * 2);
    ctx.fill();
  }

  ctx.fillStyle = '#ffcc00';
  ctx.font = '40px monospace';
  ctx.textAlign = 'center';
  ctx.fillText('VICTORY!', canvas.width / 2, 180);

  ctx.fillStyle = '#44ff44';
  ctx.font = '20px monospace';
  ctx.fillText('The Shadow Lord has been vanquished!', canvas.width / 2, 230);

  ctx.fillStyle = '#cccccc';
  ctx.font = '14px monospace';
  ctx.fillText(`Gold: ${player.money} | Weapons: ${player.weaponLevel + 1}/3 | Health: ${player.hpContainers}/5`, canvas.width / 2, 290);

  ctx.fillStyle = '#ffcc00';
  ctx.font = '18px monospace';
  ctx.fillText('[ T ] Return to title', canvas.width / 2, 380);

  document.onkeydown = function(e) {
    if (gameState !== 'victory') return;
    if (e.key.toLowerCase() === 't') {
      gameState = 'title';
      titleSelection = 0;
    }
  };
}

function renderShop() {
  ctx.fillStyle = 'rgba(0, 0, 0, 0.85)';
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  // Shop background
  ctx.fillStyle = '#2a1a0a';
  ctx.fillRect(100, 80, 760, 480);
  ctx.strokeStyle = '#8B4513';
  ctx.lineWidth = 3;
  ctx.strokeRect(100, 80, 760, 480);

  // Title
  ctx.fillStyle = '#ffcc00';
  ctx.font = '28px monospace';
  ctx.textAlign = 'center';
  ctx.fillText('Merchant Alder\'s Shop', canvas.width / 2, 125);

  ctx.fillStyle = '#aaa';
  ctx.font = '14px monospace';
  ctx.fillText(`Your Gold: 💰 ${player.money}`, canvas.width / 2, 155);

  // Items
  for (let i = 0; i < shopItems.length; i++) {
    const item = shopItems[i];
    const y = 190 + i * 50;
    const canBuy = player.money >= item.price;

    if (i === shopSelection) {
      ctx.fillStyle = 'rgba(255, 204, 0, 0.2)';
      ctx.fillRect(120, y - 15, 720, 40);
      ctx.strokeStyle = '#ffcc00';
      ctx.lineWidth = 1;
      ctx.strokeRect(120, y - 15, 720, 40);
    }

    ctx.fillStyle = canBuy ? '#ffffff' : '#666';
    ctx.font = '16px monospace';
    ctx.textAlign = 'left';
    ctx.fillText(item.name, 140, y + 5);

    ctx.fillStyle = '#aaa';
    ctx.font = '12px monospace';
    ctx.fillText(item.desc, 140, y + 22);

    ctx.fillStyle = canBuy ? '#ffd700' : '#663300';
    ctx.font = '14px monospace';
    ctx.textAlign = 'right';
    ctx.fillText(`${item.price} 💰`, 820, y + 12);
  }

  // Close button
  ctx.fillStyle = 'rgba(100, 100, 100, 0.5)';
  ctx.fillRect(canvas.width / 2 - 80, 460, 160, 35);
  ctx.fillStyle = '#fff';
  ctx.font = '14px monospace';
  ctx.textAlign = 'center';
  ctx.fillText('ESC - Close', canvas.width / 2, 482);

  ctx.fillStyle = '#888';
  ctx.font = '10px monospace';
  ctx.fillText('Arrows/WASD to select, Enter/E to buy', canvas.width / 2, 520);
}

// ============================================================
// TITLE MENU INPUT
// ============================================================
function handleTitleInput() {
  if (gameState !== 'title') return;

  const lastKey = titleLastKey;
  titleLastKey = '';

  if ((keys['arrowup'] || keys['w']) && Date.now() - titleKeyTime > 150) {
    titleSelection = Math.max(0, titleSelection - 1);
    titleKeyTime = Date.now();
  }
  if ((keys['arrowdown'] || keys['s']) && Date.now() - titleKeyTime > 150) {
    titleSelection = Math.min(2, titleSelection + 1);
    titleKeyTime = Date.now();
  }
  if (keys['Enter'] || keys['e']) {
    initAudio();
    if (titleSelection === 0) {
      startNewGame();
    } else if (titleSelection === 1) {
      if (loadGame()) {
        gameState = 'playing';
      } else {
        showMessage('No save found!');
        setTimeout(() => { gameMessage = ''; }, 1500);
      }
    } else if (titleSelection === 2) {
      gameState = 'controls';
    }
  }
}

let titleKeyTime = 0;
let titleLastKey = '';

// ============================================================
// CONTROLS SCREEN
// ============================================================
function renderControls() {
  ctx.fillStyle = '#0a0a1a';
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  ctx.fillStyle = '#ffcc00';
  ctx.font = '28px monospace';
  ctx.textAlign = 'center';
  ctx.fillText('CONTROLS', canvas.width / 2, 80);

  const controls = [
    ['Movement', 'WASD / Arrow Keys'],
    ['Attack', 'Z / Space / Mouse Click'],
    ['Interact', 'F'],
    ['Use Health Potion', 'U'],
    ['Switch to Sword', '1'],
    ['Switch to Bow', '2 (when owned)'],
    ['Pause / Menu', 'Escape'],
    ['', ''],
    ['Health System', 'Each ♥ = 2 HP. Start with 3, find 2 more!'],
    ['Weapons', 'Bronze Sword → Iron Sword → Steel Blade'],
    ['Tips', 'Talk to NPCs with F. Save at glowing crystals.'],
    ['', ''],
    ['Objective', 'Explore the realm, grow stronger, defeat the Shadow Lord!']
  ];

  ctx.fillStyle = '#cccccc';
  ctx.font = '14px monospace';
  for (let i = 0; i < controls.length; i++) {
    const y = 130 + i * 26;
    if (controls[i][0]) {
      ctx.fillStyle = controls[i][0].includes('Objective') ? '#ffcc00' : '#88aaff';
      ctx.textAlign = 'left';
      ctx.fillText(controls[i][0] + ':', canvas.width / 2 - 180, y);
      ctx.fillStyle = '#cccccc';
      ctx.textAlign = 'left';
      ctx.fillText(controls[i][1], canvas.width / 2 + 10, y);
    }
  }

  ctx.fillStyle = '#888';
  ctx.font = '14px monospace';
  ctx.textAlign = 'center';
  ctx.fillText('Press ESC to return', canvas.width / 2, 550);

  document.onkeydown = function(e) {
    if (gameState !== 'controls') return;
    if (e.key === 'Escape') {
      gameState = 'title';
      titleSelection = 2;
    }
  };
}

// ============================================================
// MAIN GAME LOOP
// ============================================================
let lastTime = 0;

function gameLoop(timestamp) {
  const dt = timestamp - lastTime;
  lastTime = timestamp;

  // Handle title screen input
  if (gameState === 'title') {
    handleTitleInput();
    renderTitle();
  } else if (gameState === 'controls') {
    renderControls();
  } else {
    updatePlayer();
    updateEnemies();
    updateParticles();

    // Check boss arena win condition (all enemies dead in boss arena area)
    const bossEnemiesAlive = enemies.filter(e => e.hp > 0 && e.x / TILE_SIZE > 85 && e.y / TILE_SIZE > 32 && e.y / TILE_SIZE < 46).length;
    if (bossEnemiesAlive === 0 && !player.victoryAchieved && player.x / TILE_SIZE > 85) {
      player.victoryAchieved = true;
      gameState = 'victory';
    }

    render();
  }

  requestAnimationFrame(gameLoop);
}

// ============================================================
// INITIALIZATION
// ============================================================
document.addEventListener('keydown', (e) => {
  if (gameState === 'title' && e.key === 'Enter') {
    initAudio();
  }
});

canvas.addEventListener('click', () => {
  initAudio();
});

// Start game loop
requestAnimationFrame(gameLoop);

</script>
</body>
</html>
<!-- agent-meta {"model":"Qwen3.6-27B-Fable-Fusion-711-Uncensored-Heretic-NM-DAU-NEO-MAX-MTP-GGUF","provider":"brigid","persona":"full","sessionId":"475b4a13-a134-4503-8f03-ea8bedd89aa1","tokensIn":1721913,"tokensOut":45536,"tokensTotal":1767449,"cost":0,"turns":37,"toolCalls":36,"failedToolCalls":0,"timestamp":"2026-08-06T02:36:53.397Z"} -->