← RPG results

RPG

Qwen 3.6 35b a3b · composer

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 63,606 bytes · SHA-256 f7df9197819d
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Realm of Echoes</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #0a0a12; overflow: hidden; font-family: 'Courier New', monospace; }
#gameCanvas { display: block; image-rendering: pixelated; }
#hud {
  position: fixed; top: 10px; left: 10px; right: 10px;
  display: flex; justify-content: space-between; align-items: center;
  padding: 8px 16px; background: rgba(0,0,0,0.7);
  border: 2px solid #333; border-radius: 8px; color: #fff; z-index: 10;
}
#hearts { font-size: 24px; letter-spacing: 4px; }
#money-display { font-size: 18px; color: #ffd700; }
#weapon-display { font-size: 14px; color: #aaa; }
#inventory-toggle, #save-toggle {
  padding: 6px 12px; background: rgba(255,255,255,0.1);
  border: 1px solid #555; border-radius: 4px; color: #fff;
  cursor: pointer; font-family: inherit; font-size: 13px;
}
#inventory-toggle:hover, #save-toggle:hover { background: rgba(255,255,255,0.2); }
.hidden { display: none !important; }
#inventory-panel {
  position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
  background: rgba(10,10,18,0.95); border: 2px solid #555;
  border-radius: 12px; padding: 24px; color: #fff; z-index: 20;
  min-width: 400px; max-width: 500px;
}
#inventory-panel h3 { margin-bottom: 16px; text-align: center; color: #ffd700; }
#inventory-grid {
  display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px;
  margin-bottom: 16px;
}
.inv-item {
  width: 64px; height: 64px; background: rgba(255,255,255,0.05);
  border: 2px solid #444; border-radius: 8px; display: flex;
  align-items: center; justify-content: center; font-size: 28px;
  cursor: pointer; position: relative;
}
.inv-item:hover { border-color: #ffd700; background: rgba(255,215,0,0.1); }
.inv-item .count {
  position: absolute; bottom: 2px; right: 4px; font-size: 11px;
  color: #aaa; background: rgba(0,0,0,0.6); padding: 1px 4px; border-radius: 3px;
}
#shop-panel {
  border-top: 1px solid #444; padding-top: 16px; margin-top: 16px;
}
#shop-panel h3 { color: #4fc3f7; margin-bottom: 12px; }
#shop-grid {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px;
  margin-bottom: 12px;
}
.shop-item {
  padding: 8px; background: rgba(255,255,255,0.05);
  border: 2px solid #444; border-radius: 8px; text-align: center;
  cursor: pointer; color: #fff; font-family: inherit;
}
.shop-item:hover { border-color: #4fc3f7; }
.shop-item .price { color: #ffd700; font-size: 12px; }
.shop-item .name { font-size: 13px; margin-bottom: 4px; }
#shop-close {
  width: 100%; padding: 8px; background: #333; border: 1px solid #555;
  color: #fff; border-radius: 4px; cursor: pointer; font-family: inherit;
}
#shop-close:hover { background: #444; }
#dialog-box {
  position: fixed; bottom: 60px; left: 50%; transform: translateX(-50%);
  background: rgba(10,10,18,0.95); border: 2px solid #555;
  border-radius: 12px; padding: 20px 30px; color: #fff; z-index: 20;
  min-width: 400px; max-width: 600px; text-align: center;
}
#dialog-box p { font-size: 16px; margin-bottom: 12px; line-height: 1.5; }
#dialog-continue {
  padding: 8px 24px; background: #ffd700; border: none;
  border-radius: 4px; cursor: pointer; font-family: inherit; font-weight: bold;
}
#dialog-continue:hover { background: #ffed4a; }
#game-over, #victory-screen {
  position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
  background: rgba(10,10,18,0.97); border: 2px solid #555;
  border-radius: 16px; padding: 40px; color: #fff; z-index: 30;
  text-align: center; min-width: 350px;
}
#game-over h2 { color: #ff4444; font-size: 36px; margin-bottom: 16px; }
#victory-screen h2 { color: #ffd700; font-size: 36px; margin-bottom: 16px; }
#game-over p, #victory-screen p { font-size: 16px; margin-bottom: 24px; color: #aaa; }
#restart-btn, #victory-restart {
  padding: 12px 32px; background: #ffd700; border: none;
  border-radius: 6px; cursor: pointer; font-family: inherit;
  font-size: 16px; font-weight: bold;
}
#restart-btn:hover, #victory-restart:hover { background: #ffed4a; }
#controls-hint {
  position: fixed; bottom: 10px; left: 50%; transform: translateX(-50%);
  color: #555; font-size: 12px; z-index: 10;
}
/* ── css: animations ── */
@keyframes pulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.1); }
}
@keyframes glow {
  0%, 100% { text-shadow: 0 0 4px currentColor; }
  50% { text-shadow: 0 0 12px currentColor, 0 0 20px currentColor; }
}
#hearts span { display: inline-block; animation: pulse 2s ease-in-out infinite; }
#hearts span:nth-child(odd) { animation-delay: 0.5s; }
#money-display { animation: glow 3s ease-in-out infinite; color: #ffd700; }
#hud { animation: glow 4s ease-in-out infinite; }
.inv-item { transition: all 0.2s ease; }
.inv-item:hover { transform: scale(1.1); }
.shop-item { transition: all 0.2s ease; }
.shop-item:hover { transform: scale(1.05); }
#dialog-box { animation: pulse 0.3s ease-out; }
#game-over, #victory-screen { animation: pulse 0.5s ease-out; }
/* ── css: hud ── */
#hud {
  position: fixed; top: 10px; left: 10px; right: 10px;
  display: flex; justify-content: space-between; align-items: center;
  padding: 8px 16px; background: rgba(0,0,0,0.7);
  border: 2px solid #333; border-radius: 8px; color: #fff; z-index: 10;
}
#hearts { font-size: 24px; letter-spacing: 4px; }
#money-display { font-size: 18px; color: #ffd700; }
#weapon-display { font-size: 14px; color: #aaa; }
#progress-bar { width: 120px; height: 8px; background: rgba(255,255,255,0.1); border-radius: 4px; overflow: hidden; }
#progress-fill { width: 0%; height: 100%; background: linear-gradient(90deg, #4fc3f7, #ffd700); border-radius: 4px; transition: width 0.5s; }
#objective { font-size: 11px; color: #888; max-width: 150px; text-align: center; }
#inventory-toggle, #save-toggle {
  padding: 6px 12px; background: rgba(255,255,255,0.1);
  border: 1px solid #555; border-radius: 4px; color: #fff;
  cursor: pointer; font-family: inherit; font-size: 13px;
}
#inventory-toggle:hover, #save-toggle:hover { background: rgba(255,255,255,0.2); }
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div id="hud">
  <div id="hearts"></div>
  <div id="money-display">💰 0</div>
  <div id="weapon-display">⚔️ Iron Sword</div>
  <div id="inventory-toggle">🎒 Inventory [I]</div>
  <div id="save-toggle">💾 Save [S]</div>
</div><div id="progress-bar" style="width:120px;height:8px;background:rgba(255,255,255,0.1);border-radius:4px;overflow:hidden;">
  <div id="progress-fill" style="width:0%;height:100%;background:linear-gradient(90deg,#4fc3f7,#ffd700);border-radius:4px;transition:width 0.5s;"></div>
</div>
<div id="objective" style="font-size:11px;color:#888;">Find the Echo Blade</div>
<div id="inventory-panel" class="hidden">
  <h3>Inventory</h3>
  <div id="inventory-grid"></div>
  <div id="shop-panel" class="hidden">
    <h3>Item Shop</h3>
    <div id="shop-grid"></div>
    <button id="shop-close">Close</button>
  </div>
</div>
<div id="dialog-box" class="hidden">
  <p id="dialog-text"></p>
  <button id="dialog-continue">Continue</button>
</div>
<div id="game-over" class="hidden">
  <h2>Game Over</h2>
  <p id="death-msg"></p>
  <button id="restart-btn">Restart</button>
</div>
<div id="victory-screen" class="hidden">
  <h2>Victory!</h2>
  <p id="victory-msg"></p>
  <button id="victory-restart">Play Again</button>
</div>
<div id="controls-hint">WASD/Arrows: Move | Click/Space: Attack | I: Inventory | S: Save</div>
<script>
'use strict';

// ── var: TILE ──
// ===== CONSTANTS =====
var TILE = 32;

// ── var: CANVAS_W ──
var CANVAS_W = 960;

// ── var: CANVAS_H ──
var CANVAS_H = 640;

// ── var: PLAYER_SPEED ──
var PLAYER_SPEED = 3;

// ── var: ENEMY_BASE_SPEED ──
var ENEMY_BASE_SPEED = 1.2;

// ── var: ATTACK_COOLDOWN ──
var ATTACK_COOLDOWN = 300;

// ── var: RANGED_COOLDOWN ──
var RANGED_COOLDOWN = 600;

// ── var: MAX_HEALTH ──
var MAX_HEALTH = 5;

// ── var: INITIAL_HEALTH ──
var INITIAL_HEALTH = 3;

// ── var: PROGRESSION_GOAL ──
var PROGRESSION_GOAL = 5;

// ── var: ROOMS ──
var ROOMS = {
  "start": {
    w: 20, h: 15, name: "Starting Hollow",
    bg: "#1a1a2e", walls: [
      [0,0,20,1],[0,0,1,15],[19,0,1,15],[0,14,20,1],
      [5,4,2,3],[8,8,3,2],[12,5,2,4],[15,10,3,2]
    ], doors: [{x:19,y:7,w:1,h:1,target:"forest_edge",dx:1,dy:0}],
    enemies: [], items: [{type:"health_pickup",x:6,y:5},{type:"coin",x:10,y:9}],
    savePoint: false, shopNPC: false, boss: false
  },
  "forest_edge": {
    w: 22, h: 16, name: "Whispering Forest",
    bg: "#0f1a0f", walls: [
      [0,0,22,1],[0,0,1,16],[21,0,1,16],[0,15,22,1],
      [3,3,2,5],[7,6,4,2],[12,3,2,6],[16,8,3,3],[18,12,3,2]
    ], doors: [{x:0,y:7,w:1,h:1,target:"start",dx:-1,dy:0},{x:21,y:8,w:1,h:1,target:"ruins",dx:-1,dy:0}],
    enemies: [{type:"slime",count:3},{type:"bat",count:2}],
    items: [{type:"coin",x:5,y:4},{type:"coin",x:14,y:5},{type:"health_pickup",x:18,y:13}],
    savePoint: false, shopNPC: false, boss: false
  },
  "ruins": {
    w: 24, h: 17, name: "Ancient Ruins",
    bg: "#1a1428", walls: [
      [0,0,24,1],[0,0,1,17],[23,0,1,17],[0,16,24,1],
      [4,3,3,3],[9,2,2,5],[14,5,4,2],[19,9,3,4],[7,10,3,3],[16,13,4,2]
    ], doors: [{x:0,y:8,w:1,h:1,target:"forest_edge",dx:-1,dy:0},{x:23,y:9,w:1,h:1,target:"cave",dx:-1,dy:0}],
    enemies: [{type:"skeleton",count:3},{type:"bat",count:2}],
    items: [{type:"coin",x:6,y:4},{type:"weapon_upgrade",x:16,y:6},{type:"health_pickup",x:10,y:12}],
    savePoint: true, shopNPC: false, boss: false
  },
  "cave": {
    w: 20, h: 15, name: "Dark Cavern",
    bg: "#0a0a1a", walls: [
      [0,0,20,1],[0,0,1,15],[19,0,1,15],[0,14,20,1],
      [3,4,4,2],[9,3,2,4],[13,7,3,3],[6,10,3,2],[16,11,2,2]
    ], doors: [{x:0,y:7,w:1,h:1,target:"ruins",dx:-1,dy:0},{x:19,y:7,w:1,h:1,target:"swamp",dx:1,dy:0}],
    enemies: [{type:"spider",count:3},{type:"skeleton",count:2}],
    items: [{type:"coin",x:5,y:5},{type:"coin",x:12,y:4},{type:"accessory",x:17,y:12}],
    savePoint: false, shopNPC: false, boss: false
  },
  "swamp": {
    w: 22, h: 16, name: "Murky Swamp",
    bg: "#0a1a0a", walls: [
      [0,0,22,1],[0,0,1,16],[21,0,1,16],[0,15,22,1],
      [2,4,3,3],[7,3,2,5],[12,6,4,2],[17,9,3,3],[5,11,3,2],[15,12,4,2]
    ], doors: [{x:0,y:8,w:1,h:1,target:"cave",dx:-1,dy:0},{x:21,y:8,w:1,h:1,target:"boss_chamber",dx:-1,dy:0}],
    enemies: [{type:"slime",count:2},{type:"spider",count:3},{type:"bat",count:2}],
    items: [{type:"coin",x:4,y:5},{type:"coin",x:10,y:4},{type:"ranged_weapon",x:18,y:13}],
    savePoint: false, shopNPC: false, boss: false
  },
  "shop": {
    w: 16, h: 12, name: "Merchant's Rest",
    bg: "#1a1a10", walls: [
      [0,0,16,1],[0,0,1,12],[15,0,1,12],[0,11,16,1]
    ], doors: [{x:7,y:0,w:2,h:1,target:"crossroads",dx:0,dy:1}],
    enemies: [], items: [],
    savePoint: true, shopNPC: true, boss: false
  },
  "crossroads": {
    w: 18, h: 14, name: "Crossroads",
    bg: "#141420", walls: [
      [0,0,18,1],[0,0,1,14],[17,0,1,14],[0,13,18,1],
      [5,3,2,4],[11,6,2,3],[8,10,3,2]
    ], doors: [{x:8,y:0,w:2,h:1,target:"shop",dx:0,dy:1},{x:0,y:7,w:1,h:1,target:"forest_edge",dx:-1,dy:0},{x:17,y:6,w:1,h:1,target:"cave",dx:1,dy:0}],
    enemies: [{type:"skeleton",count:2},{type:"spider",count:1}],
    items: [{type:"coin",x:3,y:4},{type:"health_pickup",x:14,y:11}],
    savePoint: false, shopNPC: false, boss: false
  },
  "boss_chamber": {
    w: 20, h: 16, name: "The Echo Throne",
    bg: "#1a0a0a", walls: [
      [0,0,20,1],[0,0,1,16],[19,0,1,16],[0,15,20,1],
      [3,3,2,4],[15,3,2,4],[8,6,4,3],[10,10,2,3],[5,12,3,2],[12,12,3,2]
    ], doors: [{x:9,y:0,w:2,h:1,target:"swamp",dx:0,dy:1}],
    enemies: [{type:"boss",count:1}],
    items: [{type:"final_upgrade",x:10,y:7}],
    savePoint: false, shopNPC: false, boss: true
  },
  "victory_room": {
    w: 14, h: 10, name: "The Heart of Echoes",
    bg: "#0a0a2a", walls: [
      [0,0,14,1],[0,0,1,10],[13,0,1,10],[0,9,14,1]
    ], doors: [],
    enemies: [], items: [{type:"victory",x:7,y:4}],
    savePoint: true, shopNPC: false, boss: false
  }
};

// ── var: ENEMY_DATA ──
var ENEMY_DATA = {
  slime: { hp: 3, damage: 1, speed: 0.8, color: "#44cc44", size: 20, xp: 10, coinDrop: 5, name: "Slime" },
  bat: { hp: 2, damage: 1, speed: 2, color: "#8844aa", size: 16, xp: 8, coinDrop: 3, name: "Bat" },
  skeleton: { hp: 5, damage: 2, speed: 1.2, color: "#ccccaa", size: 22, xp: 15, coinDrop: 8, name: "Skeleton" },
  spider: { hp: 4, damage: 2, speed: 1.8, color: "#aa4444", size: 18, xp: 12, coinDrop: 6, name: "Spider" },
  boss: { hp: 30, damage: 3, speed: 1.5, color: "#ff4400", size: 36, xp: 100, coinDrop: 50, name: "Echo Lord" }
};

// ── var: SHOP_ITEMS ──
var SHOP_ITEMS = [
  { id: "health_vial", name: "Health Vial", icon: "❤️", price: 15, desc: "Restore 2 HP", type: "consumable" },
  { id: "stamina_tonic", name: "Stamina Tonic", icon: "⚡", price: 20, desc: "Speed boost", type: "consumable" },
  { id: "iron_amulet", name: "Iron Amulet", icon: "🔰", price: 30, desc: "+1 max HP", type: "accessory" },
  { id: "gold_ring", name: "Gold Ring", icon: "💍", price: 40, desc: "Find more coins", type: "accessory" },
  { id: "shadow_cloak", name: "Shadow Cloak", icon: "🧥", price: 50, desc: "Brief invisibility", type: "consumable" },
  { id: "phoenix_feather", name: "Phoenix Feather", icon: "🪶", price: 60, desc: "Auto-revive once", type: "accessory" }
];

// ── var: ACCESSORY_IDS ──
var ACCESSORY_IDS = ["iron_amulet", "gold_ring", "shadow_cloak", "phoenix_feather"];

// ── const: AudioCtx ──
// ===== AUDIO SYSTEM =====
const AudioCtx = window.AudioContext || window.webkitAudioContext;

// ── let: audioCtx ──
let audioCtx;

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

// ── function: playSound ──
function playSound(type) {
  if (!audioCtx) return;
  try {
    const now = audioCtx.currentTime;
    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();
    osc.connect(gain);
    gain.connect(audioCtx.destination);

    switch(type) {
      case 'walk':
        osc.type = 'sine'; osc.frequency.setValueAtTime(80, now);
        gain.gain.setValueAtTime(0.03, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 0.05);
        osc.start(now); osc.stop(now + 0.05);
        break;
      case 'attack':
        osc.type = 'sawtooth'; osc.frequency.setValueAtTime(300, now);
        osc.frequency.exponentialRampToValueAtTime(100, now + 0.15);
        gain.gain.setValueAtTime(0.12, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
        osc.start(now); osc.stop(now + 0.15);
        break;
      case 'ranged_attack':
        osc.type = 'sine'; osc.frequency.setValueAtTime(600, now);
        osc.frequency.exponentialRampToValueAtTime(200, now + 0.2);
        gain.gain.setValueAtTime(0.1, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 0.2);
        osc.start(now); osc.stop(now + 0.2);
        break;
      case 'hit':
        osc.type = 'square'; osc.frequency.setValueAtTime(200, now);
        osc.frequency.exponentialRampToValueAtTime(50, now + 0.1);
        gain.gain.setValueAtTime(0.15, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 0.1);
        osc.start(now); osc.stop(now + 0.1);
        break;
      case 'damage':
        osc.type = 'sawtooth'; osc.frequency.setValueAtTime(150, now);
        osc.frequency.exponentialRampToValueAtTime(40, now + 0.2);
        gain.gain.setValueAtTime(0.15, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 0.2);
        osc.start(now); osc.stop(now + 0.2);
        break;
      case 'pickup':
        osc.type = 'sine'; osc.frequency.setValueAtTime(400, now);
        osc.frequency.exponentialRampToValueAtTime(800, now + 0.1);
        gain.gain.setValueAtTime(0.1, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
        osc.start(now); osc.stop(now + 0.15);
        break;
      case 'coin':
        osc.type = 'sine'; osc.frequency.setValueAtTime(800, now);
        osc.frequency.setValueAtTime(1200, now + 0.05);
        gain.gain.setValueAtTime(0.08, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 0.1);
        osc.start(now); osc.stop(now + 0.1);
        break;
      case 'levelup':
        osc.type = 'sine';
        [523, 659, 784, 1047].forEach((f, i) => {
          osc.frequency.setValueAtTime(f, now + i * 0.1);
        });
        gain.gain.setValueAtTime(0.12, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 0.5);
        osc.start(now); osc.stop(now + 0.5);
        break;
      case 'shop':
        osc.type = 'triangle'; osc.frequency.setValueAtTime(500, now);
        osc.frequency.setValueAtTime(700, now + 0.1);
        osc.frequency.setValueAtTime(500, now + 0.2);
        gain.gain.setValueAtTime(0.08, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 0.3);
        osc.start(now); osc.stop(now + 0.3);
        break;
      case 'save':
        osc.type = 'sine';
        [440, 554, 659].forEach((f, i) => {
          osc.frequency.setValueAtTime(f, now + i * 0.15);
        });
        gain.gain.setValueAtTime(0.1, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 0.5);
        osc.start(now); osc.stop(now + 0.5);
        break;
      case 'death':
        osc.type = 'sawtooth'; osc.frequency.setValueAtTime(300, now);
        osc.frequency.exponentialRampToValueAtTime(30, now + 0.8);
        gain.gain.setValueAtTime(0.15, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 0.8);
        osc.start(now); osc.stop(now + 0.8);
        break;
      case 'victory':
        osc.type = 'sine';
        [523, 659, 784, 1047, 784, 1047].forEach((f, i) => {
          osc.frequency.setValueAtTime(f, now + i * 0.12);
        });
        gain.gain.setValueAtTime(0.12, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 1.0);
        osc.start(now); osc.stop(now + 1.0);
        break;
      case 'door':
        osc.type = 'triangle'; osc.frequency.setValueAtTime(200, now);
        osc.frequency.exponentialRampToValueAtTime(400, now + 0.15);
        gain.gain.setValueAtTime(0.06, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 0.2);
        osc.start(now); osc.stop(now + 0.2);
        break;
      case 'enemy_die':
        osc.type = 'square'; osc.frequency.setValueAtTime(250, now);
        osc.frequency.exponentialRampToValueAtTime(50, now + 0.3);
        gain.gain.setValueAtTime(0.1, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 0.3);
        osc.start(now); osc.stop(now + 0.3);
        break;
    }
  } catch(e) {}
}

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

// ── function: spawnParticles ──
function spawnParticles(x, y, color, count, spread, life, size) {
  for (let i = 0; i < count; i++) {
    particles.push({
      x, y,
      vx: (Math.random() - 0.5) * spread,
      vy: (Math.random() - 0.5) * spread - 1,
      life: life || 30,
      maxLife: life || 30,
      color: color || '#ffd700',
      size: size || 3
    });
  }
}

// ── function: updateParticles ──
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 ──
function drawParticles(ctx, camX, camY) {
  for (const p of particles) {
    const alpha = p.life / p.maxLife;
    ctx.globalAlpha = alpha;
    ctx.fillStyle = p.color;
    ctx.beginPath();
    ctx.arc(p.x - camX, p.y - camY, p.size * alpha, 0, Math.PI * 2);
    ctx.fill();
  }
  ctx.globalAlpha = 1;
}

// ── function: createPlayer ──
// ===== PLAYER =====
function createPlayer(x, y) {
  return {
    x, y, w: 24, h: 24,
    health: INITIAL_HEALTH,
    maxHealth: INITIAL_HEALTH,
    speed: PLAYER_SPEED,
    weapon: { name: "Iron Sword", damage: 2, range: 36, speed: ATTACK_COOLDOWN, type: "melee", icon: "⚔️" },
    weaponLevel: 0,
    hasRanged: false,
    rangedCooldown: 0,
    attackCooldown: 0,
    inventory: [],
    money: 0,
    accessories: [],
    hasPhoenixFeather: false,
    facing: 'down',
    animFrame: 0,
    animTimer: 0,
    isAttacking: false,
    attackTimer: 0,
    attackDir: { x: 0, y: 0 },
    damageFlash: 0,
    isMoving: false,
    walkTimer: 0,
    hpGoalReached: false,
    gameWon: false,
    invincibleTimer: 0,
    staminaBoost: 0,
    shadowCloakTimer: 0
  };
}

// ── function: getAttackHitbox ──
function getAttackHitbox(player) {
  const w = player.weapon;
  let hx = player.x + player.w / 2;
  let hy = player.y + player.h / 2;
  const dir = player.attackDir;
  hx += dir.x * (w.range / 2);
  hy += dir.y * (w.range / 2);
  return { x: hx - w.range / 2, y: hy - w.range / 2, w: w.range, h: w.range };
}

// ── function: getRangedHitbox ──
function getRangedHitbox(player) {
  return {
    x: player.x + player.w / 2 + player.attackDir.x * 20 - 6,
    y: player.y + player.h / 2 + player.attackDir.y * 20 - 6,
    w: 12, h: 12
  };
}

// ── function: playerTakeDamage ──
function playerTakeDamage(dmg, player) {
  if (player.invincibleTimer > 0) return;
  player.health -= dmg;
  player.damageFlash = 15;
  player.invincibleTimer = 30;
  playSound('damage');
  if (player.health <= 0) {
    if (player.hasPhoenixFeather) {
      player.hasPhoenixFeather = false;
      player.health = 1;
      player.invincibleTimer = 60;
      playSound('levelup');
      spawnParticles(player.x + player.w/2, player.y + player.h/2, '#ff69b4', 20, 6, 40, 4);
    } else {
      playSound('death');
    }
  }
}

// ── function: healPlayer ──
function healPlayer(amount, player) {
  const oldHealth = player.health;
  const oldMax = player.maxHealth;
  player.maxHealth += amount;
  player.health = Math.min(player.health + amount, player.maxHealth);
  if (player.health > oldHealth) {
    playSound('pickup');
    spawnParticles(player.x + player.w/2, player.y + player.h/2, '#44ff44', 10, 4, 25, 3);
  }
  if (player.health >= PROGRESSION_GOAL && !player.hpGoalReached) {
    player.hpGoalReached = true;
    playSound('levelup');
  }
}

// ── function: addItem ──
function addItem(player, item) {
  if (player.inventory.length >= 20) return false;
  player.inventory.push(item);
  if (item.type === 'accessory' && ACCESSORY_IDS.includes(item.id)) {
    player.accessories.push(item.id);
    if (item.id === 'iron_amulet') {
      player.maxHealth += 1;
      player.health = Math.min(player.health + 1, player.maxHealth);
    }
    if (item.id === 'gold_ring') player.goldBonus = (player.goldBonus || 0) + 1;
    if (item.id === 'phoenix_feather') player.hasPhoenixFeather = true;
  }
  return true;
}

// ── function: createEnemy ──
// ===== ENEMIES =====
function createEnemy(type, x, y) {
  const data = ENEMY_DATA[type];
  const roomScale = getRoomProgression() * 0.3;
  return {
    type, x, y, w: data.size, h: data.size,
    hp: data.hp + Math.floor(roomScale),
    maxHp: data.hp + Math.floor(roomScale),
    damage: data.damage,
    speed: data.speed + roomScale * 0.2,
    color: data.color,
    xp: data.xp,
    coinDrop: data.coinDrop + Math.floor(Math.random() * 3),
    name: data.name,
    state: 'idle',
    stateTimer: 0,
    aggroRange: 120,
    attackRange: 30,
    attackCooldown: 0,
    knockback: { x: 0, y: 0 },
    animFrame: 0,
    animTimer: 0,
    hitFlash: 0,
    alive: true
  };
}

// ── function: getRoomProgression ──
function getRoomProgression() {
  const roomsVisited = game.roomsVisited.size;
  const totalRooms = Object.keys(ROOMS).length;
  return roomsVisited / totalRooms;
}

// ── function: updateEnemy ──
function updateEnemy(enemy, player, room) {
  if (!enemy.alive) return;
  if (enemy.hitFlash > 0) enemy.hitFlash--;
  if (enemy.attackCooldown > 0) enemy.attackCooldown--;
  if (enemy.knockback.x !== 0 || enemy.knockback.y !== 0) {
    enemy.x += enemy.knockback.x;
    enemy.y += enemy.knockback.y;
    enemy.knockback.x *= 0.8;
    enemy.knockback.y *= 0.8;
    if (Math.abs(enemy.knockback.x) < 0.1) enemy.knockback.x = 0;
    if (Math.abs(enemy.knockback.y) < 0.1) enemy.knockback.y = 0;
    return;
  }
  enemy.stateTimer++;
  const dx = player.x + player.w/2 - (enemy.x + enemy.w/2);
  const dy = player.y + player.h/2 - (enemy.y + enemy.h/2);
  const dist = Math.sqrt(dx*dx + dy*dy);

  if (dist < enemy.aggroRange) {
    enemy.state = 'chase';
  } else {
    if (enemy.stateTimer > 120) {
      enemy.state = 'idle';
      enemy.stateTimer = 0;
    }
  }

  if (enemy.state === 'chase') {
    const angle = Math.atan2(dy, dx);
    let nx = enemy.x + Math.cos(angle) * enemy.speed;
    let ny = enemy.y + Math.sin(angle) * enemy.speed;
    if (!collidesWithWall(nx, ny, enemy.w, enemy.h, room)) {
      enemy.x = nx;
      enemy.y = ny;
    }
    if (dist < enemy.attackRange && enemy.attackCooldown <= 0) {
      playerTakeDamage(enemy.damage, player);
      enemy.attackCooldown = 60;
      spawnParticles(player.x + player.w/2, player.y + player.h/2, '#ff4444', 5, 3, 15, 2);
    }
  }

  enemy.animTimer++;
  if (enemy.animTimer > 15) {
    enemy.animTimer = 0;
    enemy.animFrame = (enemy.animFrame + 1) % 4;
  }
}

// ── function: damageEnemy ──
function damageEnemy(enemy, dmg, player) {
  enemy.hp -= dmg;
  enemy.hitFlash = 8;
  playSound('hit');
  spawnParticles(enemy.x + enemy.w/2, enemy.y + enemy.h/2, '#ffffff', 8, 4, 20, 3);
  if (enemy.hp <= 0) {
    enemy.alive = false;
    playSound('enemy_die');
    spawnParticles(enemy.x + enemy.w/2, enemy.y + enemy.h/2, enemy.color, 15, 6, 30, 4);
    player.money += enemy.coinDrop;
    // Drop coin
    game.roomItems.push({
      type: 'coin', x: enemy.x, y: enemy.y, collected: false, icon: '🪙', value: enemy.coinDrop
    });
  }
}

// ── function: collidesWithWall ──
// ===== COLLISION =====
function collidesWithWall(x, y, w, h, room) {
  for (const wall of room.walls) {
    const wx = wall[0] * TILE, wy = wall[1] * TILE;
    const ww = wall[2] * TILE, wh = wall[3] * TILE;
    if (x < wx + ww && x + w > wx && y < wy + wh && y + h > wy) return true;
  }
  return false;
}

// ── function: rectsOverlap ──
function rectsOverlap(a, b) {
  return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
}

// ── function: getRoomById ──
// ===== WORLD MANAGEMENT =====
function getRoomById(id) {
  const r = ROOMS[id];
  return {
    ...r,
    enemies: r.enemies.map(e => createEnemy(e.type, e.x * TILE + 16, e.y * TILE + 16)),
    items: r.items.map(i => ({...i, collected: false, icon: getItemIcon(i.type), value: getItemValue(i.type), bobPhase: Math.random() * Math.PI * 2}))
  };
}

// ── function: getItemIcon ──
function getItemIcon(type) {
  const icons = { health_pickup: '❤️', coin: '🪙', weapon_upgrade: '🗡️', ranged_weapon: '🏹', accessory: '💎', final_upgrade: '👑', victory: '⭐' };
  return icons[type] || '📦';
}

// ── function: getItemValue ──
function getItemValue(type) {
  if (type === 'coin') return 5;
  return 0;
}

// ── function: drawRoom ──
function drawRoom(ctx, room, camX, camY) {
  // Background with gradient
  const grad = ctx.createRadialGradient(CANVAS_W/2, CANVAS_H/2, 50, CANVAS_W/2, CANVAS_H/2, CANVAS_W/2);
  grad.addColorStop(0, room.bg);
  grad.addColorStop(1, shadeColor(room.bg, -30));
  ctx.fillStyle = grad;
  ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);

  // Floor tiles with pattern
  for (let y = 0; y < room.h; y++) {
    for (let x = 0; x < room.w; x++) {
      const sx = x * TILE - camX;
      const sy = y * TILE - camY;
      if (sx < -TILE || sx > CANVAS_W || sy < -TILE || sy > CANVAS_H) continue;
      const pattern = (x + y) % 2 === 0 ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.04)';
      ctx.fillStyle = pattern;
      ctx.fillRect(sx + 1, sy + 1, TILE - 2, TILE - 2);
    }
  }

  // Ambient light circles
  ctx.fillStyle = 'rgba(255,255,200,0.02)';
  for (let i = 0; i < 3; i++) {
    const lx = (room.w * TILE / 4 * (i + 1)) - camX;
    const ly = (room.h * TILE / 3) - camY;
    ctx.beginPath();
    ctx.arc(lx, ly, 80, 0, Math.PI * 2);
    ctx.fill();
  }

  // Walls with 3D effect
  for (const wall of room.walls) {
    const wx = wall[0] * TILE - camX;
    const wy = wall[1] * TILE - camY;
    const ww = wall[2] * TILE;
    const wh = wall[3] * TILE;

    // Wall body
    ctx.fillStyle = '#2a2a3e';
    ctx.fillRect(wx, wy, ww, wh);

    // Top highlight
    ctx.fillStyle = 'rgba(150,150,200,0.15)';
    if (wall[3] > 1) ctx.fillRect(wx, wy, ww, 3);
    if (wall[2] > 1) ctx.fillRect(wx, wy, 3, wh);

    // Bottom shadow
    ctx.fillStyle = 'rgba(0,0,0,0.2)';
    if (wall[3] > 1) ctx.fillRect(wx, wy + wh - 2, ww, 2);
    if (wall[2] > 1) ctx.fillRect(wx + ww - 2, wy, 2, wh);

    // Brick pattern
    ctx.strokeStyle = 'rgba(80,80,120,0.3)';
    ctx.lineWidth = 0.5;
    for (let bx = 0; bx < ww; bx += 16) {
      ctx.beginPath();
      ctx.moveTo(wx + bx, wy);
      ctx.lineTo(wx + bx, wy + wh);
      ctx.stroke();
    }
    for (let by = 0; by < wh; by += 8) {
      ctx.beginPath();
      ctx.moveTo(wx, wy + by);
      ctx.lineTo(wx + ww, wy + by);
      ctx.stroke();
    }
  }

  // Doors with glow
  for (const door of room.doors) {
    const dx = door.x * TILE - camX;
    const dy = door.y * TILE - camY;
    const dw = door.w * TILE;
    const dh = door.h * TILE;

    // Glow
    ctx.fillStyle = 'rgba(255,215,0,0.1)';
    ctx.fillRect(dx - 4, dy - 4, dw + 8, dh + 8);

    // Door
    ctx.fillStyle = '#4a3a2a';
    ctx.fillRect(dx, dy, dw, dh);

    // Door frame
    ctx.strokeStyle = '#6a5a3a';
    ctx.lineWidth = 2;
    ctx.strokeRect(dx, dy, dw, dh);

    // Arrow indicator
    ctx.fillStyle = '#ffd700';
    ctx.font = '18px serif';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    const arrows = { up: '↑', down: '↓', left: '←', right: '→' };
    let arrow = '→';
    if (door.dx < 0) arrow = '←';
    else if (door.dx > 0) arrow = '→';
    else if (door.dy < 0) arrow = '↑';
    else if (door.dy > 0) arrow = '↓';
    ctx.fillText(arrow, dx + dw/2, dy + dh/2);
  }

  // Save point with glow
  if (room.savePoint) {
    const sx = 10 * TILE - camX;
    const sy = 7 * TILE - camY;
    const glow = 0.15 + Math.sin(Date.now() / 500) * 0.05;
    ctx.fillStyle = `rgba(100,100,255,${glow})`;
    ctx.beginPath();
    ctx.arc(sx + TILE/2, sy + TILE/2, 24, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = `rgba(100,100,255,${glow + 0.1})`;
    ctx.beginPath();
    ctx.arc(sx + TILE/2, sy + TILE/2, 16, 0, Math.PI * 2);
    ctx.fill();
    ctx.font = '24px serif';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText('💾', sx + TILE/2, sy + TILE/2);
  }

  // Shop NPC
  if (room.shopNPC) {
    const nx = 7 * TILE - camX;
    const ny = 5 * TILE - camY;
    const bob = Math.sin(Date.now() / 400) * 2;

    // Glow
    ctx.fillStyle = 'rgba(255,215,0,0.1)';
    ctx.beginPath();
    ctx.arc(nx + TILE/2, ny + TILE/2 + bob, 24, 0, Math.PI * 2);
    ctx.fill();

    ctx.font = '28px serif';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText('🧙', nx + TILE/2, ny + TILE/2 + bob);

    ctx.fillStyle = '#ffd700';
    ctx.font = '11px Courier New';
    ctx.fillText('Shop', nx + TILE/2, ny + TILE/2 + 20);
  }
}

// ── function: drawEnemies ──
function drawEnemies(ctx, enemies, camX, camY) {
  for (const e of enemies) {
    if (!e.alive) continue;
    const ex = e.x - camX;
    const ey = e.y - camY;

    // Shadow
    ctx.fillStyle = 'rgba(0,0,0,0.3)';
    ctx.beginPath();
    ctx.ellipse(ex + e.w/2, ey + e.h + 2, e.w/2, 4, 0, 0, Math.PI * 2);
    ctx.fill();

    // Body
    const flash = e.hitFlash > 0;
    ctx.fillStyle = flash ? '#ffffff' : e.color;
    if (e.type === 'slime') {
      const bounce = Math.sin(e.animFrame * Math.PI / 2) * 3;
      ctx.beginPath();
      ctx.ellipse(ex + e.w/2, ey + e.h/2 + bounce, e.w/2, e.h/2 - bounce, 0, 0, Math.PI * 2);
      ctx.fill();
      // Slime eyes
      ctx.fillStyle = '#222';
      ctx.beginPath();
      ctx.arc(ex + e.w/2 - 4, ey + e.h/2 - 2 + bounce, 2, 0, Math.PI * 2);
      ctx.arc(ex + e.w/2 + 4, ey + e.h/2 - 2 + bounce, 2, 0, Math.PI * 2);
      ctx.fill();
    } else if (e.type === 'bat') {
      const wingAngle = Math.sin(e.animFrame * Math.PI / 2) * 0.5;
      ctx.save();
      ctx.translate(ex + e.w/2, ey + e.h/2);
      // Wings
      ctx.fillStyle = flash ? '#ffffff' : 'rgba(136,68,170,0.6)';
      ctx.beginPath();
      ctx.ellipse(-10, 0, 12, 6, wingAngle, 0, Math.PI * 2);
      ctx.fill();
      ctx.beginPath();
      ctx.ellipse(10, 0, 12, 6, -wingAngle, 0, Math.PI * 2);
      ctx.fill();
      ctx.restore();
      ctx.fillStyle = flash ? '#ffffff' : e.color;
      ctx.beginPath();
      ctx.arc(ex + e.w/2, ey + e.h/2, e.w/2, 0, Math.PI * 2);
      ctx.fill();
      // Eyes
      ctx.fillStyle = '#ff0000';
      ctx.beginPath();
      ctx.arc(ex + e.w/2 - 3, ey + e.h/2 - 2, 2, 0, Math.PI * 2);
      ctx.arc(ex + e.w/2 + 3, ey + e.h/2 - 2, 2, 0, Math.PI * 2);
      ctx.fill();
    } else if (e.type === 'boss') {
      const pulse = 1 + Math.sin(Date.now() / 200) * 0.05;
      ctx.save();
      ctx.translate(ex + e.w/2, ey + e.h/2);
      ctx.scale(pulse, pulse);
      // Aura
      ctx.fillStyle = 'rgba(255,68,0,0.15)';
      ctx.beginPath();
      ctx.arc(0, 0, e.w/2 + 10, 0, Math.PI * 2);
      ctx.fill();
      // Body
      ctx.fillStyle = flash ? '#ffffff' : e.color;
      ctx.beginPath();
      ctx.arc(0, 0, e.w/2, 0, Math.PI * 2);
      ctx.fill();
      // Crown
      ctx.fillStyle = '#ffd700';
      ctx.beginPath();
      ctx.moveTo(-15, -e.w/2);
      ctx.lineTo(-10, -e.w/2 - 12);
      ctx.lineTo(-5, -e.w/2 - 4);
      ctx.lineTo(0, -e.w/2 - 14);
      ctx.lineTo(5, -e.w/2 - 4);
      ctx.lineTo(10, -e.w/2 - 12);
      ctx.lineTo(15, -e.w/2);
      ctx.closePath();
      ctx.fill();
      // Eyes
      ctx.fillStyle = '#ffff00';
      ctx.beginPath();
      ctx.arc(-6, -4, 3, 0, Math.PI * 2);
      ctx.arc(6, -4, 3, 0, Math.PI * 2);
      ctx.fill();
      ctx.restore();
    } else {
      // Skeleton / Spider
      ctx.beginPath();
      ctx.arc(ex + e.w/2, ey + e.h/2, e.w/2, 0, Math.PI * 2);
      ctx.fill();
      // Eyes
      ctx.fillStyle = flash ? '#ff0000' : '#222';
      ctx.beginPath();
      ctx.arc(ex + e.w/2 - 5, ey + e.h/2 - 3, 3, 0, Math.PI * 2);
      ctx.arc(ex + e.w/2 + 5, ey + e.h/2 - 3, 3, 0, Math.PI * 2);
      ctx.fill();
      ctx.fillStyle = flash ? '#ff0000' : '#fff';
      ctx.beginPath();
      ctx.arc(ex + e.w/2 - 5, ey + e.h/2 - 3, 1.5, 0, Math.PI * 2);
      ctx.arc(ex + e.w/2 + 5, ey + e.h/2 - 3, 1.5, 0, Math.PI * 2);
      ctx.fill();
    }

    // HP bar
    if (e.hp < e.maxHp) {
      const barW = e.w;
      const barH = 4;
      const barX = ex;
      const barY = ey - 8;
      ctx.fillStyle = '#333';
      ctx.fillRect(barX, barY, barW, barH);
      ctx.fillStyle = e.hp / e.maxHp > 0.5 ? '#44ff44' : (e.hp / e.maxHp > 0.25 ? '#ffaa00' : '#ff4444');
      ctx.fillRect(barX, barY, barW * (e.hp / e.maxHp), barH);
    }
  }
}

// ── function: drawPlayer ──
function drawPlayer(ctx, player, camX, camY) {
  const px = player.x - camX;
  const py = player.y - camY;

  // Shadow
  ctx.fillStyle = 'rgba(0,0,0,0.3)';
  ctx.beginPath();
  ctx.ellipse(px + player.w/2, py + player.h + 2, player.w/2, 4, 0, 0, Math.PI * 2);
  ctx.fill();

  // Invincibility blink
  if (player.invincibleTimer > 0 && Math.floor(player.invincibleTimer / 3) % 2 === 0) return;

  // Shadow cloak effect
  if (player.shadowCloakTimer > 0) {
    ctx.globalAlpha = 0.4 + Math.sin(Date.now() / 100) * 0.1;
  }

  // Body with gradient
  const bodyGrad = ctx.createRadialGradient(px + player.w/2, py + player.h/2, 2, px + player.w/2, py + player.h/2, player.w/2);
  let bodyColor1 = '#4488ff';
  let bodyColor2 = '#2255cc';
  if (player.damageFlash > 0) { bodyColor1 = '#ff6644'; bodyColor2 = '#cc3322'; }
  bodyGrad.addColorStop(0, bodyColor1);
  bodyGrad.addColorStop(1, bodyColor2);
  ctx.fillStyle = bodyGrad;
  ctx.beginPath();
  ctx.arc(px + player.w/2, py + player.h/2, player.w/2, 0, Math.PI * 2);
  ctx.fill();

  // Border
  ctx.strokeStyle = '#1a3388';
  ctx.lineWidth = 2;
  ctx.beginPath();
  ctx.arc(px + player.w/2, py + player.h/2, player.w/2, 0, Math.PI * 2);
  ctx.stroke();

  // Eyes
  ctx.fillStyle = '#fff';
  const eyeOffX = player.facing === 'left' ? -3 : (player.facing === 'right' ? 3 : 0);
  const eyeOffY = player.facing === 'up' ? -3 : (player.facing === 'down' ? 3 : 0);
  ctx.beginPath();
  ctx.arc(px + player.w/2 - 4 + eyeOffX, py + player.h/2 - 2 + eyeOffY, 3, 0, Math.PI * 2);
  ctx.arc(px + player.w/2 + 4 + eyeOffX, py + player.h/2 - 2 + eyeOffY, 3, 0, Math.PI * 2);
  ctx.fill();
  ctx.fillStyle = '#222';
  ctx.beginPath();
  ctx.arc(px + player.w/2 - 4 + eyeOffX * 1.3, py + player.h/2 - 2 + eyeOffY * 1.3, 1.5, 0, Math.PI * 2);
  ctx.arc(px + player.w/2 + 4 + eyeOffX * 1.3, py + player.h/2 - 2 + eyeOffY * 1.3, 1.5, 0, Math.PI * 2);
  ctx.fill();

  // Weapon visual
  if (player.isAttacking) {
    ctx.save();
    ctx.translate(px + player.w/2, py + player.h/2);
    const angle = Math.atan2(player.attackDir.y, player.attackDir.x);
    ctx.rotate(angle);
    const w = player.weapon;
    // Attack arc
    ctx.strokeStyle = w.type === 'melee' ? '#aaa' : '#4fc3f7';
    ctx.lineWidth = 3;
    ctx.globalAlpha = player.attackTimer / 12;
    ctx.beginPath();
    ctx.arc(0, 0, w.range, -0.6, 0.6);
    ctx.stroke();
    // Weapon blade
    ctx.fillStyle = w.type === 'melee' ? (w.name === 'Echo Blade' ? '#ffd700' : '#cccccc') : '#4fc3f7';
    ctx.fillRect(8, -2, w.range - 8, 4);
    // Glow
    ctx.fillStyle = w.type === 'melee' ? (w.name === 'Echo Blade' ? 'rgba(255,215,0,0.3)' : 'rgba(255,255,255,0.3)') : 'rgba(79,195,247,0.3)';
    ctx.fillRect(8, -4, w.range - 8, 8);
    ctx.restore();
    ctx.globalAlpha = 1;
  } else {
    // Carry weapon
    const angle = Math.atan2(player.attackDir.y, player.attackDir.x);
    ctx.save();
    ctx.translate(px + player.w/2, py + player.h/2);
    ctx.rotate(angle);
    ctx.fillStyle = '#aaa';
    ctx.fillRect(10, -1, 14, 2);
    ctx.restore();
  }

  ctx.globalAlpha = 1;
}

// ── function: drawRoomItems ──
function drawRoomItems(ctx, items, camX, camY) {
  for (const item of items) {
    if (item.collected) continue;
    const ix = item.x * TILE - camX;
    const iy = item.y * TILE - camY;
    const bob = Math.sin(Date.now() / 300 + (item.bobPhase || 0)) * 3;
    const glow = 0.1 + Math.sin(Date.now() / 400 + (item.bobPhase || 0)) * 0.05;

    // Glow
    ctx.fillStyle = `rgba(255,215,0,${glow})`;
    ctx.beginPath();
    ctx.arc(ix + TILE/2, iy + TILE/2 + bob, 18, 0, Math.PI * 2);
    ctx.fill();

    ctx.font = '20px serif';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(item.icon, ix + TILE/2, iy + TILE/2 + bob);
  }
}

// ── function: drawRangedProjectiles ──
function drawRangedProjectiles(ctx, projectiles, camX, camY) {
  for (const p of projectiles) {
    const px = p.x - camX;
    const py = p.y - camY;
    // Trail
    ctx.fillStyle = 'rgba(79,195,247,0.3)';
    ctx.beginPath();
    ctx.arc(px - p.vx * 0.5, py - p.vy * 0.5, 8, 0, Math.PI * 2);
    ctx.fill();
    // Core
    ctx.fillStyle = '#4fc3f7';
    ctx.beginPath();
    ctx.arc(px, py, 5, 0, Math.PI * 2);
    ctx.fill();
    // Bright center
    ctx.fillStyle = '#ffffff';
    ctx.beginPath();
    ctx.arc(px, py, 2, 0, Math.PI * 2);
    ctx.fill();
  }
}

// ── function: drawHearts ──
// ===== HUD & UI =====
function drawHearts(health, maxHealth) {
  const el = document.getElementById('hearts');
  let html = '';
  for (let i = 0; i < maxHealth; i++) {
    if (i < Math.floor(health)) {
      html += '<span style="color:#ff4444;text-shadow:0 0 6px #ff4444">❤️</span>';
    } else if (i < health) {
      html += '<span style="color:#ff8888">🧡</span>';
    } else {
      html += '<span style="color:#444">🤍</span>';
    }
  }
  el.innerHTML = html;
}

// ── function: updateHUD ──
function updateHUD(player) {
  document.getElementById('money-display').textContent = `💰 ${player.money}`;
  document.getElementById('weapon-display').textContent = `${player.weapon.icon} ${player.weapon.name}`;
  drawHearts(player.health, player.maxHealth);
  updateProgress(player);
}

// ── function: updateInventoryUI ──
function updateInventoryUI(player) {
  const grid = document.getElementById('inventory-grid');
  let html = '';
  for (let i = 0; i < 20; i++) {
    const item = player.inventory[i];
    if (item) {
      const isAccessory = ACCESSORY_IDS.includes(item.id);
      html += `<div class="inv-item" data-index="${i}" title="${item.name || item.type}">${item.icon}${item.count > 1 ? `<span class="count">${item.count}</span>` : ''}</div>`;
    } else {
      html += '<div class="inv-item"></div>';
    }
  }
  grid.innerHTML = html;

  grid.querySelectorAll('.inv-item[data-index]').forEach(el => {
    el.addEventListener('click', () => useItem(parseInt(el.dataset.index), player));
  });
}

// ── function: useItem ──
function useItem(index, player) {
  const item = player.inventory[index];
  if (!item) return;

  if (item.type === 'consumable') {
    playSound('pickup');
    if (item.id === 'health_vial') {
      healPlayer(2, player);
    } else if (item.id === 'stamina_tonic') {
      player.staminaBoost = 180;
      spawnParticles(player.x + player.w/2, player.y + player.h/2, '#ffff00', 10, 4, 20, 3);
    } else if (item.id === 'shadow_cloak') {
      player.shadowCloakTimer = 180;
      spawnParticles(player.x + player.w/2, player.y + player.h/2, '#8844aa', 10, 4, 20, 3);
    }
    item.count--;
    if (item.count <= 0) player.inventory.splice(index, 1);
    updateInventoryUI(player);
    updateHUD(player);
  } else if (item.type === 'accessory') {
    if (item.equipped) {
      player.accessories = player.accessories.filter(a => a !== item.id);
      if (item.id === 'iron_amulet') {
        player.maxHealth = Math.max(INITIAL_HEALTH, player.maxHealth - 1);
        player.health = Math.min(player.health, player.maxHealth);
      }
      if (item.id === 'gold_ring') player.goldBonus = (player.goldBonus || 0) - 1;
      if (item.id === 'phoenix_feather') player.hasPhoenixFeather = false;
      item.equipped = false;
    } else {
      player.accessories.push(item.id);
      if (item.id === 'iron_amulet') {
        player.maxHealth += 1;
        player.health = Math.min(player.health + 1, player.maxHealth);
      }
      if (item.id === 'gold_ring') player.goldBonus = (player.goldBonus || 0) + 1;
      if (item.id === 'phoenix_feather') player.hasPhoenixFeather = true;
      item.equipped = true;
    }
    updateInventoryUI(player);
    updateHUD(player);
  }
}

// ── function: updateShopUI ──
function updateShopUI() {
  const grid = document.getElementById('shop-grid');
  let html = '';
  for (const item of SHOP_ITEMS) {
    const canAfford = window.game && window.game.player && window.game.player.money >= item.price;
    html += `<div class="shop-item" data-id="${item.id}" style="${canAfford ? '' : 'opacity:0.4;'}">
      <div class="name">${item.icon} ${item.name}</div>
      <div class="price">💰 ${item.price}</div>
      <div style="font-size:11px;color:#888">${item.desc}</div>
    </div>`;
  }
  grid.innerHTML = html;

  grid.querySelectorAll('.shop-item').forEach(el => {
    el.addEventListener('click', () => buyItem(el.dataset.id));
  });
}

// ── function: buyItem ──
function buyItem(id) {
  const shopItem = SHOP_ITEMS.find(s => s.id === id);
  if (!shopItem) return;
  if (!window.game || window.game.player.money < shopItem.price) {
    showDialog("Not enough money!", 1500);
    return;
  }
  window.game.player.money -= shopItem.price;
  const newItem = { ...shopItem, type: shopItem.type, count: 1, equipped: false };
  if (shopItem.type === 'consumable') {
    const existing = window.game.player.inventory.find(i => i.id === id && i.type === 'consumable');
    if (existing) {
      existing.count++;
    } else {
      window.game.player.inventory.push(newItem);
    }
  } else {
    window.game.player.inventory.push(newItem);
  }
  playSound('shop');
  updateHUD(window.game.player);
  updateShopUI();
  updateInventoryUI(window.game.player);
  showDialog(`Bought ${shopItem.name}!`, 1200);
}

// ── function: showDialog ──
function showDialog(text, duration) {
  const box = document.getElementById('dialog-box');
  document.getElementById('dialog-text').textContent = text;
  box.classList.remove('hidden');
  if (dialogTimeout) clearTimeout(dialogTimeout);
  if (duration) {
    dialogTimeout = setTimeout(() => box.classList.add('hidden'), duration);
  }
}

// ── function: showGameOver ──
function showGameOver() {
  document.getElementById('game-over').classList.remove('hidden');
  document.getElementById('death-msg').textContent = `You fell in ${window.game.roomsVisited.size} rooms. Money: ${window.game.player.money}`;
}

// ── function: showVictory ──
function showVictory() {
  document.getElementById('victory-screen').classList.remove('hidden');
  document.getElementById('victory-msg').textContent = `You conquered the Echo Lord! Final HP: ${window.game.player.health}/${window.game.player.maxHealth}. Money: ${window.game.player.money}`;
}

// ── function: saveGame ──
function saveGame() {
  const saveData = {
    player: {
      x: window.game.player.x, y: window.game.player.y,
      health: window.game.player.health, maxHealth: window.game.player.maxHealth,
      money: window.game.player.money,
      weapon: window.game.player.weapon, weaponLevel: window.game.player.weaponLevel,
      hasRanged: window.game.player.hasRanged,
      inventory: window.game.player.inventory,
      accessories: window.game.player.accessories,
      hasPhoenixFeather: window.game.player.hasPhoenixFeather,
      goldBonus: window.game.player.goldBonus,
      staminaBoost: window.game.player.staminaBoost,
      shadowCloakTimer: window.game.player.shadowCloakTimer,
      hpGoalReached: window.game.player.hpGoalReached
    },
    currentRoom: window.game.currentRoomId,
    roomsVisited: [...window.game.roomsVisited],
    roomItems: {},
    bossDefeated: window.game.bossDefeated
  };
  for (const [id, room] of Object.entries(window.game.rooms)) {
    saveData.roomItems[id] = room.items.map(i => ({...i}));
  }
  localStorage.setItem('realm_of_echoes_save', JSON.stringify(saveData));
  playSound('save');
  showDialog('Game saved at this location!', 1500);
}

// ── function: loadGame ──
function loadGame() {
  const data = localStorage.getItem('realm_of_echoes_save');
  if (!data) return false;
  try {
    const save = JSON.parse(data);
    window.game.player = { ...createPlayer(160, 128), ...save.player };
    window.game.currentRoomId = save.currentRoom;
    window.game.roomsVisited = new Set(save.roomsVisited);
    window.game.bossDefeated = save.bossDefeated || false;
    for (const [id, items] of Object.entries(save.roomItems)) {
      window.game.rooms[id].items = items;
    }
    return true;
  } catch(e) { return false; }
}

// ── function: hasSave ──
function hasSave() {
  return !!localStorage.getItem('realm_of_echoes_save');
}

// ── function: updateGame ──
// ===== GAME LOGIC =====
function updateGame(game) {
  const player = game.player;
  const room = game.rooms[game.currentRoomId];
  if (!room) return;

  // Player position
  const px = player.x + player.w / 2;
  const py = player.y + player.h / 2;

  // Movement
  player.isMoving = false;
  let dx = 0, dy = 0;
  if (window.keys['KeyW'] || window.keys['ArrowUp']) { dy = -1; player.facing = 'up'; }
  if (window.keys['KeyS'] || window.keys['ArrowDown']) { dy = 1; player.facing = 'down'; }
  if (window.keys['KeyA'] || window.keys['ArrowLeft']) { dx = -1; player.facing = 'left'; }
  if (window.keys['KeyD'] || window.keys['ArrowRight']) { dx = 1; player.facing = 'right'; }

  if (dx !== 0 || dy !== 0) {
    player.isMoving = true;
    const spd = player.staminaBoost > 0 ? player.speed * 1.5 : player.speed;
    let nx = player.x + dx * spd;
    let ny = player.y + dy * spd;
    if (!collidesWithWall(nx, player.y, player.w, player.h, room)) player.x = nx;
    if (!collidesWithWall(player.x, ny, player.w, player.h, room)) player.y = ny;

    player.walkTimer++;
    if (player.walkTimer % 15 === 0) playSound('walk');
  }

  // Timers
  if (player.attackCooldown > 0) player.attackCooldown--;
  if (player.rangedCooldown > 0) player.rangedCooldown--;
  if (player.damageFlash > 0) player.damageFlash--;
  if (player.invincibleTimer > 0) player.invincibleTimer--;
  if (player.staminaBoost > 0) player.staminaBoost--;
  if (player.shadowCloakTimer > 0) player.shadowCloakTimer--;

  // Attack
  if (player.isAttacking) {
    player.attackTimer--;
    if (player.attackTimer <= 0) player.isAttacking = false;
  }

  // Check door transitions
  for (const door of room.doors) {
    const doorRect = { x: door.x * TILE, y: door.y * TILE, w: door.w * TILE, h: door.h * TILE };
    if (rectsOverlap(
      { x: player.x, y: player.y, w: player.w, h: player.h },
      doorRect
    )) {
      playSound('door');
      game.currentRoomId = door.target;
      const targetRoom = game.rooms[door.target];
      for (const td of targetRoom.doors) {
        if (td.target === game.currentRoomId) {
          player.x = td.x * TILE + (td.dx > 0 ? 1 : td.dx < 0 ? TILE - player.w : TILE/2 - player.w/2);
          player.y = td.y * TILE + (td.dy > 0 ? 1 : td.dy < 0 ? TILE - player.h : TILE/2 - player.h/2);
        }
      }
      game.roomsVisited.add(game.currentRoomId);
      break;
    }
  }

  // Update enemies
  for (const enemy of room.enemies) {
    updateEnemy(enemy, player, room);
  }

  // Attack collision
  if (player.isAttacking) {
    const hitbox = getAttackHitbox(player);
    for (const enemy of room.enemies) {
      if (!enemy.alive) continue;
      if (rectsOverlap(hitbox, { x: enemy.x, y: enemy.y, w: enemy.w, h: enemy.h })) {
        damageEnemy(enemy, player.weapon.damage, player);
        const angle = Math.atan2(enemy.y - player.y, enemy.x - player.x);
        enemy.knockback.x = Math.cos(angle) * 8;
        enemy.knockback.y = Math.sin(angle) * 8;
      }
    }
  }

  // Ranged projectiles
  for (let i = game.projectiles.length - 1; i >= 0; i--) {
    const p = game.projectiles[i];
    p.x += p.vx;
    p.y += p.vy;
    p.life--;
    if (p.life <= 0) { game.projectiles.splice(i, 1); continue; }

    for (const enemy of room.enemies) {
      if (!enemy.alive) continue;
      const ex = enemy.x + enemy.w/2;
      const ey = enemy.y + enemy.h/2;
      const dist = Math.sqrt((p.x - ex)**2 + (p.y - ey)**2);
      if (dist < enemy.w/2 + 5) {
        damageEnemy(enemy, player.weapon.damage, player);
        spawnParticles(p.x, p.y, '#4fc3f7', 6, 3, 15, 2);
        game.projectiles.splice(i, 1);
        break;
      }
    }
  }

  // Item collection
  for (const item of room.items) {
    if (item.collected) continue;
    const ix = item.x * TILE + TILE/2;
    const iy = item.y * TILE + TILE/2;
    const dist = Math.sqrt((ix - px)**2 + (iy - py)**2);
    if (dist < 28) {
      collectItem(item, player, game);
    }
  }

  // Save point interaction
  if (room.savePoint) {
    const sx = 10 * TILE + TILE/2;
    const sy = 7 * TILE + TILE/2;
    const dist = Math.sqrt((sx - px)**2 + (sy - py)**2);
    if (dist < 30 && window.keys['KeyE']) {
      saveGame();
    }
  }

  // Shop NPC interaction
  if (room.shopNPC) {
    const nx = 7 * TILE + TILE/2;
    const ny = 5 * TILE + TILE/2;
    const dist = Math.sqrt((nx - px)**2 + (ny - py)**2);
    if (dist < 30 && window.keys['KeyE']) {
      openShop();
    }
  }

  // Victory check
  if (game.bossDefeated && !player.gameWon) {
    player.gameWon = true;
    setTimeout(() => showVictory(), 500);
  }

  // Death check
  if (player.health <= 0 && !player.hasPhoenixFeather) {
    showGameOver();
  }

  updateParticles();
}

// ── function: collectItem ──
function collectItem(item, player, game) {
  item.collected = true;
  if (item.type === 'coin') {
    const bonus = player.goldBonus || 0;
    player.money += item.value + bonus;
    playSound('coin');
    spawnParticles(item.x * TILE + TILE/2, item.y * TILE + TILE/2, '#ffd700', 8, 4, 20, 3);
  } else if (item.type === 'health_pickup') {
    healPlayer(1, player);
    spawnParticles(item.x * TILE + TILE/2, item.y * TILE + TILE/2, '#ff4444', 10, 4, 25, 3);
  } else if (item.type === 'weapon_upgrade') {
    if (player.weaponLevel === 0) {
      player.weapon = { name: "Steel Blade", damage: 4, range: 40, speed: ATTACK_COOLDOWN, type: "melee", icon: "🗡️" };
      player.weaponLevel = 1;
      playSound('levelup');
      spawnParticles(item.x * TILE + TILE/2, item.y * TILE + TILE/2, '#ff8800', 20, 6, 40, 4);
      showDialog("Upgraded to Steel Blade! +2 damage, +4 range", 2000);
    }
  } else if (item.type === 'ranged_weapon') {
    player.hasRanged = true;
    playSound('levelup');
    spawnParticles(item.x * TILE + TILE/2, item.y * TILE + TILE/2, '#4fc3f7', 20, 6, 40, 4);
    showDialog("Found the Echo Bow! Press R to shoot", 2000);
  } else if (item.type === 'final_upgrade') {
    if (player.weaponLevel === 1) {
      player.weapon = { name: "Echo Blade", damage: 7, range: 50, speed: ATTACK_COOLDOWN, type: "melee", icon: "👑" };
      player.weaponLevel = 2;
      player.hasRanged = true;
      playSound('levelup');
      spawnParticles(item.x * TILE + TILE/2, item.y * TILE + TILE/2, '#ffd700', 30, 8, 50, 5);
      showDialog("Ultimate weapon acquired! +7 damage! Press R to shoot", 2500);
      setTimeout(() => {
        game.rooms['boss_chamber'].doors.push({x: 10, y: 15, w: 2, h: 1, target: "victory_room", dx: 0, dy: 1});
      }, 500);
    }
  } else if (item.type === 'accessory') {
    const accItem = { id: 'iron_amulet', name: 'Iron Amulet', icon: '🔰', type: 'accessory', count: 1, equipped: true };
    addItem(player, accItem);
    playSound('pickup');
    spawnParticles(item.x * TILE + TILE/2, item.y * TILE + TILE/2, '#aaa', 10, 4, 25, 3);
    showDialog("Found Iron Amulet! +1 Max HP", 1500);
  } else if (item.type === 'victory') {
    game.bossDefeated = true;
    playSound('victory');
    showVictory();
  }
}

// ── function: openShop ──
function openShop() {
  document.getElementById('shop-panel').classList.remove('hidden');
  updateShopUI();
  playSound('shop');
}

// ── function: closeShop ──
function closeShop() {
  document.getElementById('shop-panel').classList.add('hidden');
}

// ── function: render ──
// ===== RENDERING =====
function render(game) {
  const canvas = document.getElementById('gameCanvas');
  const ctx = canvas.getContext('2d');
  const room = game.rooms[game.currentRoomId];
  if (!room) return;

  // Camera
  const camX = game.player.x + game.player.w / 2 - CANVAS_W / 2;
  const camY = game.player.y + game.player.h / 2 - CANVAS_H / 2;

  // Clamp camera
  const maxCamX = room.w * TILE - CANVAS_W;
  const maxCamY = room.h * TILE - CANVAS_H;
  const finalCamX = Math.max(0, Math.min(camX, maxCamX));
  const finalCamY = Math.max(0, Math.min(camY, maxCamY));

  // Draw
  drawRoom(ctx, room, finalCamX, finalCamY);
  drawRoomItems(ctx, room.items, finalCamX, finalCamY);
  drawEnemies(ctx, room.enemies, finalCamX, finalCamY);
  drawPlayer(ctx, game.player, finalCamX, finalCamY);
  drawRangedProjectiles(ctx, game.projectiles, finalCamX, finalCamY);
  drawParticles(ctx, finalCamX, finalCamY);

  // Room name
  ctx.fillStyle = 'rgba(255,255,255,0.5)';
  ctx.font = '14px Courier New';
  ctx.textAlign = 'center';
  ctx.textBaseline = 'top';
  ctx.fillText(room.name, CANVAS_W / 2, 8);

  // Interaction hint
  const px = game.player.x + game.player.w/2;
  const py = game.player.y + game.player.h/2;
  if (room.savePoint) {
    const sx = 10 * TILE + TILE/2, sy = 7 * TILE + TILE/2;
    if (Math.sqrt((sx-px)**2 + (sy-py)**2) < 40) {
      ctx.fillStyle = 'rgba(100,150,255,0.8)';
      ctx.font = '12px Courier New';
      ctx.fillText('[E] Save', CANVAS_W/2, CANVAS_H - 30);
    }
  }
  if (room.shopNPC) {
    const nx = 7 * TILE + TILE/2, ny = 5 * TILE + TILE/2;
    if (Math.sqrt((nx-px)**2 + (ny-py)**2) < 40) {
      ctx.fillStyle = 'rgba(255,215,0,0.8)';
      ctx.font = '12px Courier New';
      ctx.fillText('[E] Shop', CANVAS_W/2, CANVAS_H - 30);
    }
  }

  // Ranged hint
  if (game.player.hasRanged && game.player.rangedCooldown <= 0) {
    ctx.fillStyle = 'rgba(79,195,247,0.6)';
    ctx.font = '12px Courier New';
    ctx.fillText('[R] Shoot Bow', CANVAS_W/2, CANVAS_H - 12);
  }
}

// ── function: shadeColor ──
function shadeColor(color, percent) {
  const num = parseInt(color.replace('#', ''), 16);
  const amt = Math.round(2.55 * percent);
  const R = Math.max(0, Math.min(255, (num >> 16) + amt));
  const G = Math.max(0, Math.min(255, (num >> 8 & 0x00FF) + amt));
  const B = Math.max(0, Math.min(255, (num & 0x0000FF) + amt));
  return '#' + (0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1);
}

// ── function: updateProgress ──
function updateProgress(player) {
  const roomsVisited = player.roomsVisited || (window.game && window.game.roomsVisited.size) || 1;
  const totalRooms = Object.keys(ROOMS).length;
  const progress = Math.min(100, Math.round((roomsVisited / totalRooms) * 100));
  const fill = document.getElementById('progress-fill');
  if (fill) fill.style.width = progress + '%';

  // Update objective text
  const obj = document.getElementById('objective');
  if (obj) {
    if (player.weaponLevel === 0) obj.textContent = 'Find the Steel Blade';
    else if (player.weaponLevel === 1) obj.textContent = 'Find the Echo Blade';
    else if (player.hasRanged) obj.textContent = 'Defeat the Echo Lord';
    else obj.textContent = 'Find the Echo Bow';
  }
}

// ── main ──
// ===== MAIN =====
window.keys = {};
window.game = null;
let animFrame;
let inventoryOpen = false;
let dialogTimeout = null;

function initGame() {
  try {
    window.game = {
      player: createPlayer(288, 224),
      currentRoomId: 'start',
      rooms: {},
      roomsVisited: new Set(['start']),
      projectiles: [],
      bossDefeated: false,
      paused: false
    };

    for (const [id, r] of Object.entries(ROOMS)) {
      window.game.rooms[id] = getRoomById(id);
    }

    if (hasSave()) {
      if (loadGame()) {
        showDialog('Welcome back, adventurer!', 2000);
      }
    }

    updateHUD(window.game.player);
    updateInventoryUI(window.game.player);
    updateShopUI();

    window.addEventListener('keydown', onKeyDown);
    window.addEventListener('keyup', onKeyUp);
    window.addEventListener('mousedown', onMouseDown);

    document.getElementById('inventory-toggle').addEventListener('click', toggleInventory);
    document.getElementById('save-toggle').addEventListener('click', () => {
      if (window.game.rooms[window.game.currentRoomId].savePoint) {
        saveGame();
      } else {
        showDialog('No save point here!', 1500);
      }
    });
    document.getElementById('shop-close').addEventListener('click', () => {
      closeShop();
      inventoryOpen = false;
      document.getElementById('inventory-panel').classList.add('hidden');
    });
    document.getElementById('restart-btn').addEventListener('click', () => {
      document.getElementById('game-over').classList.add('hidden');
      localStorage.removeItem('realm_of_echoes_save');
      initGame();
    });
    document.getElementById('victory-restart').addEventListener('click', () => {
      document.getElementById('victory-screen').classList.add('hidden');
      localStorage.removeItem('realm_of_echoes_save');
      initGame();
    });
    document.getElementById('dialog-continue').addEventListener('click', () => {
      document.getElementById('dialog-box').classList.add('hidden');
    });

    const canvas = document.getElementById('gameCanvas');
    canvas.width = CANVAS_W;
    canvas.height = CANVAS_H;
    canvas.style.width = CANVAS_W + 'px';
    canvas.style.height = CANVAS_H + 'px';

    gameLoop();
  } catch(e) {
    console.error('Init error:', e);
  }
}

function onKeyDown(e) {
  window.keys[e.code] = true;
  initAudio();

  if (e.code === 'KeyI' && !document.getElementById('inventory-panel').classList.contains('hidden')) {
    inventoryOpen = false;
    document.getElementById('inventory-panel').classList.add('hidden');
  }
  if (e.code === 'KeyI') {
    inventoryOpen = !inventoryOpen;
    document.getElementById('inventory-panel').classList.toggle('hidden', !inventoryOpen);
    updateInventoryUI(window.game.player);
  }
  if (e.code === 'KeyR' && window.game && window.game.player.hasRanged && window.game.player.rangedCooldown <= 0) {
    shootRanged(window.game);
  }
}

function onKeyUp(e) {
  window.keys[e.code] = false;
}

function onMouseDown(e) {
  initAudio();
  if (!window.game || window.game.paused) return;
  if (inventoryOpen) return;

  if (window.game.player.attackCooldown <= 0) {
    window.game.player.isAttacking = true;
    window.game.player.attackTimer = 12;
    window.game.player.attackCooldown = window.game.player.weapon.speed;
    window.game.player.attackDir = getAttackDirection(window.game.player);
    playSound('attack');
    spawnParticles(
      window.game.player.x + window.game.player.w/2 + window.game.player.attackDir.x * 20,
      window.game.player.y + window.game.player.h/2 + window.game.player.attackDir.y * 20,
      '#ffffff', 5, 3, 15, 2
    );
  }
}

function getAttackDirection(player) {
  if (window.keys['KeyW'] || window.keys['ArrowUp']) return { x: 0, y: -1 };
  if (window.keys['KeyS'] || window.keys['ArrowDown']) return { x: 0, y: 1 };
  if (window.keys['KeyA'] || window.keys['ArrowLeft']) return { x: -1, y: 0 };
  if (window.keys['KeyD'] || window.keys['ArrowRight']) return { x: 1, y: 0 };
  return { x: 0, y: 1 };
}

function shootRanged(g) {
  const player = g.player;
  const dir = getAttackDirection(player);
  g.projectiles.push({
    x: player.x + player.w/2,
    y: player.y + player.h/2,
    vx: dir.x * 6,
    vy: dir.y * 6,
    life: 60
  });
  player.rangedCooldown = RANGED_COOLDOWN;
  playSound('ranged_attack');
  spawnParticles(player.x + player.w/2, player.y + player.h/2, '#4fc3f7', 5, 3, 15, 2);
}

function toggleInventory() {
  inventoryOpen = !inventoryOpen;
  document.getElementById('inventory-panel').classList.toggle('hidden', !inventoryOpen);
  if (inventoryOpen) updateInventoryUI(window.game.player);
}

function gameLoop() {
  if (!window.game) return;
  updateGame(window.game);
  render(window.game);
  updateHUD(window.game.player);
  animFrame = requestAnimationFrame(gameLoop);
}

initGame();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.6-35b-a3b","provider":"lmstudio","persona":"composer","sessionId":"8a989806-5e16-478f-a187-29318beaa537","tokensIn":3439297,"tokensOut":60939,"tokensTotal":3500236,"turns":69,"toolCalls":75,"failedToolCalls":1,"timestamp":"2026-07-21T00:05:16.294Z"} -->