← RPG results

RPG

longcat 2.0 · full

Initial view of RPG

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

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Imported historical artifact. Its exact original instructions and lineage are not known.

Generation prompt · exact text unavailable

See the current prompt for reference. It may differ from the instructions used to generate this artifact.

Inspect original source 63,182 bytes · SHA-256 32523851a126
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Heart of the Ruins — Action Adventure RPG</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
  background: #0a0a0f;
  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;
  cursor: crosshair;
}
#ui-overlay {
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  pointer-events: none;
}
#inventory-panel {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  background: rgba(15, 15, 25, 0.95);
  border: 2px solid #555;
  border-radius: 8px;
  padding: 20px;
  pointer-events: all;
  display: none;
  min-width: 400px;
  z-index: 100;
}
#inventory-panel h2 { color: #ffd700; margin-bottom: 10px; font-size: 16px; }
.inv-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px; }
.inv-slot {
  width: 56px; height: 56px;
  background: rgba(30,30,50,0.8);
  border: 1px solid #444;
  border-radius: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  cursor: pointer;
  transition: border-color 0.2s;
}
.inv-slot:hover { border-color: #ffd700; }
.inv-slot canvas { width: 40px; height: 40px; }
.inv-slot .count {
  position: absolute;
  bottom: 2px; right: 4px;
  color: #fff; font-size: 10px;
  text-shadow: 1px 1px 1px #000;
}
#shop-panel {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  background: rgba(15, 15, 25, 0.95);
  border: 2px solid #ffd700;
  border-radius: 8px;
  padding: 20px;
  pointer-events: all;
  display: none;
  min-width: 420px;
  z-index: 100;
}
#shop-panel h2 { color: #ffd700; margin-bottom: 10px; }
#shop-panel .shop-items { display: flex; flex-direction: column; gap: 6px; }
.shop-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  background: rgba(40,40,60,0.8);
  padding: 8px 12px;
  border-radius: 4px;
  border: 1px solid #555;
  cursor: pointer;
  transition: border-color 0.2s;
}
.shop-item:hover { border-color: #ffd700; }
.shop-item .name { color: #ddd; font-size: 12px; }
.shop-item .price { color: #ffd700; font-size: 12px; }
#dialogue-box {
  position: absolute;
  bottom: 60px;
  left: 50%;
  transform: translateX(-50%);
  background: rgba(10, 10, 20, 0.9);
  border: 1px solid #666;
  border-radius: 6px;
  padding: 12px 20px;
  color: #ddd;
  font-size: 13px;
  max-width: 500px;
  display: none;
  z-index: 90;
  text-align: center;
}
#controls-hint {
  position: absolute;
  bottom: 8px;
  left: 50%;
  transform: translateX(-50%);
  color: rgba(255,255,255,0.4);
  font-size: 11px;
}
.hud-btn {
  pointer-events: all;
  background: rgba(30,30,50,0.8);
  border: 1px solid #555;
  color: #ddd;
  padding: 4px 10px;
  border-radius: 4px;
  cursor: pointer;
  font-family: inherit;
  font-size: 11px;
}
.hud-btn:hover { border-color: #ffd700; color: #ffd700; }
#minimap {
  position: absolute;
  top: 8px; right: 8px;
  width: 120px; height: 120px;
  background: rgba(10,10,20,0.7);
  border: 1px solid #444;
  border-radius: 4px;
}
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div id="ui-overlay">
  <canvas id="minimap"></canvas>
  <div id="inventory-panel">
    <h2>⚔ INVENTORY — Press E to close</h2>
    <div class="inv-grid" id="inv-grid"></div>
    <div id="inv-desc" style="color:#aaa;font-size:11px;margin-top:8px;"></div>
  </div>
  <div id="shop-panel">
    <h2>🏪 Item Shop — Gold: <span id="shop-gold">0</span></h2>
    <div class="shop-items" id="shop-items"></div>
    <div style="margin-top:10px;text-align:right;">
      <button class="hud-btn" onclick="game.closeShop()">Close (F)</button>
    </div>
  </div>
  <div id="dialogue-box"></div>
  <div id="controls-hint">WASD Move | SPACE Attack | SHIFT Ranged | E Inventory | F Interact | M Minimap</div>
</div>
<script>
// ============================================================
// HEART OF THE RUINS — Action Adventure RPG
// A single-file HTML5 action-adventure experience
// ============================================================

// ===== CONSTANTS =====
const W = 800, H = 600;
const TILE = 32;
const GRAVITY = 0;

const WEAPONS = {
  rustySword: { name: 'Rusty Sword', dmg: 1, range: 50, cooldown: 380, arc: Math.PI/2, color: '#aaa', type: 'melee' },
  steelBlade: { name: 'Steel Blade', dmg: 2, range: 65, cooldown: 360, arc: Math.PI/2.2, color: '#ccf', type: 'melee' },
  flameSword: { name: 'Flame Sword', dmg: 2, range: 70, cooldown: 400, arc: Math.PI/1.5, color: '#f80', type: 'melee', flame: true },
  hunterBow: { name: "Hunter's Bow", dmg: 1, range: 250, cooldown: 500, speed: 7, color: '#8f8', type: 'ranged' }
};

// ===== AUDIO SYSTEM =====
class AudioSystem {
  constructor() {
    this.ctx = null;
    this.initialized = false;
  }
  init() {
    if (this.initialized) return;
    try {
      this.ctx = new (window.AudioContext || window.webkitAudioContext)();
      this.initialized = true;
    } catch(e) {}
  }
  play(freq, duration, type = 'square', vol = 0.12) {
    if (!this.ctx) this.init();
    if (!this.ctx) return;
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();
    osc.connect(gain);
    gain.connect(this.ctx.destination);
    osc.type = type;
    osc.frequency.setValueAtTime(freq, this.ctx.currentTime);
    gain.gain.setValueAtTime(vol, this.ctx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + duration);
    osc.start(this.ctx.currentTime);
    osc.stop(this.ctx.currentTime + duration);
  }
  hit() { this.play(200, 0.08, 'square', 0.15); this.play(120, 0.1, 'sawtooth', 0.08); }
  swing() { this.play(400, 0.06, 'triangle', 0.08); this.play(600, 0.04, 'sine', 0.05); }
  pickup() { this.play(800, 0.08, 'sine', 0.1); setTimeout(() => this.play(1000, 0.08, 'sine', 0.08), 60); }
  coin() { this.play(1200, 0.05, 'sine', 0.08); setTimeout(() => this.play(1600, 0.06, 'sine', 0.06), 40); }
  damage() { this.play(100, 0.15, 'sawtooth', 0.15); this.play(80, 0.2, 'square', 0.1); }
  walk() { this.play(80 + Math.random()*20, 0.04, 'sine', 0.03); }
  shop() { this.play(500, 0.1, 'sine', 0.08); setTimeout(() => this.play(700, 0.1, 'sine', 0.08), 80); setTimeout(() => this.play(900, 0.12, 'sine', 0.08), 160); }
  save() { this.play(440, 0.15, 'sine', 0.1); setTimeout(() => this.play(550, 0.15, 'sine', 0.1), 120); setTimeout(() => this.play(660, 0.2, 'sine', 0.1), 240); }
  explosion() { this.play(60, 0.3, 'sawtooth', 0.15); this.play(40, 0.4, 'square', 0.1); }
  bowshot() { this.play(900, 0.06, 'triangle', 0.06); this.play(700, 0.08, 'sawtooth', 0.04); }
  levelup() {
    [400,500,600,800].forEach((f,i) => setTimeout(() => this.play(f, 0.15, 'sine', 0.1), i*100));
  }
  interact() { this.play(600, 0.06, 'sine', 0.06); }
}

// ===== PARTICLE SYSTEM =====
class ParticleSystem {
  constructor() { this.particles = []; }
  emit(x, y, count, color, life = 30, speed = 2, size = 3) {
    for (let i = 0; i < count; i++) {
      const angle = Math.random() * Math.PI * 2;
      const spd = (Math.random() * 0.7 + 0.3) * speed;
      this.particles.push({
        x, y,
        vx: Math.cos(angle) * spd,
        vy: Math.sin(angle) * spd - 0.5,
        life: life + Math.random() * life * 0.5,
        maxLife: life,
        color,
        size: size + Math.random() * size * 0.5
      });
    }
  }
  update() {
    for (let i = this.particles.length - 1; i >= 0; i--) {
      const p = this.particles[i];
      p.x += p.vx;
      p.y += p.vy;
      p.vy += 0.05;
      p.life--;
      if (p.life <= 0) this.particles.splice(i, 1);
    }
  }
  draw(ctx, camX, camY) {
    for (const p of this.particles) {
      const alpha = p.life / p.maxLife;
      ctx.globalAlpha = alpha;
      ctx.fillStyle = p.color;
      ctx.fillRect(p.x - camX - p.size/2, p.y - camY - p.size/2, p.size, p.size);
    }
    ctx.globalAlpha = 1;
  }
}

// ===== INPUT =====
class Input {
  constructor() {
    this.keys = {};
    this.justPressed = {};
    window.addEventListener('keydown', e => {
      if (!this.keys[e.key.toLowerCase()] && !this.keys[e.code]) {
        this.justPressed[e.key.toLowerCase()] = true;
        this.justPressed[e.code] = true;
      }
      this.keys[e.key.toLowerCase()] = true;
      this.keys[e.code] = true;
      if (['arrowup','arrowdown','arrowleft','arrowright',' '].includes(e.key.toLowerCase())) e.preventDefault();
    });
    window.addEventListener('keyup', e => {
      this.keys[e.key.toLowerCase()] = false;
      this.keys[e.code] = false;
    });
  }
  isDown(key) { return !!this.keys[key]; }
  wasPressed(key) {
    if (this.justPressed[key]) { this.justPressed[key] = false; return true; }
    return false;
  }
  clearFrame() { this.justPressed = {}; }
}

// ===== CAMERA =====
class Camera {
  constructor(w, h) {
    this.x = 0; this.y = 0;
    this.w = w; this.h = h;
    this.smoothing = 0.08;
  }
  follow(targetX, targetY, worldW, worldH) {
    const tx = targetX - this.w / 2;
    const ty = targetY - this.h / 2;
    this.x += (tx - this.x) * this.smoothing;
    this.y += (ty - this.y) * this.smoothing;
    this.x = Math.max(0, Math.min(this.x, worldW - this.w));
    this.y = Math.max(0, Math.min(this.y, worldH - this.h));
  }
}
// ===== WORLD MAP =====
// Tile-based world: each area is a connected region. The world is one large grid.
// Tile types: 0=grass, 1=wall/rock, 2=water, 3=path, 4=tree, 5=bridge, 6=door, 7=chest, 8=save, 9=shop, 10=stairs

const WORLD_W = 96; // tiles
const WORLD_H = 64; // tiles

function generateWorld() {
  const tiles = new Uint8Array(WORLD_W * WORLD_H);
  const meta = {}; // tile metadata: chests, doors, etc.

  // Fill with grass
  for (let i = 0; i < tiles.length; i++) tiles[i] = 0;

  // Helper
  const set = (x, y, t) => { if (x >= 0 && x < WORLD_W && y >= 0 && y < WORLD_H) tiles[y * WORLD_W + x] = t; };
  const get = (x, y) => (x >= 0 && x < WORLD_W && y >= 0 && y < WORLD_H) ? tiles[y * WORLD_W + x] : 1;

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

  // Define regions (each roughly 20x16 tiles)
  // Region layout:
  //   [Meadow]  [Forest]  [Mountain]
  //   [Village] [Plains]  [Darkwood]
  //   [Caves]   [Ruins]   [Castle]

  // Meadow (0,0) to (31,21) — starting area, safe
  // Add some trees/bushes as decoration
  for (let i = 0; i < 30; i++) {
    const tx = 3 + Math.floor(Math.random()*26);
    const ty = 3 + Math.floor(Math.random()*16);
    set(tx, ty, 4);
  }
  // Meadow pond
  for (let x = 20; x < 26; x++) for (let y = 4; y < 8; y++) set(x, y, 2);
  set(20, 5, 0); set(21, 5, 0); set(20, 6, 0); // small shore

  // Forest (32,0) to (63,21) — enemies, first weapon upgrade
  for (let i = 0; i < 50; i++) {
    const tx = 34 + Math.floor(Math.random()*27);
    const ty = 2 + Math.floor(Math.random()*18);
    set(tx, ty, 4);
  }
  // Clearing in forest
  for (let x = 44; x < 52; x++) for (let y = 8; y < 14; y++) set(x, y, 0);
  // Steel blade in clearing
  set(47, 10, 7); meta[`47,10`] = { type: 'chest', item: 'steelBlade', label: 'Steel Blade' };

  // Mountain (64,0) to (95,21) — impassable cliffs + cave entrance
  for (let x = 66; x < 94; x++) for (let y = 2; y < 20; y++) set(x, y, 1);
  // Path around mountain
  for (let y = 22; y < 28; y++) set(78, y, 3);
  for (let x = 64; x < 94; x++) set(x, 22, 3);
  // Cave entrance
  set(78, 20, 6); meta[`78,20`] = { type: 'door', label: 'Cave Entrance', target: 'cave' };

  // Village (0,22) to (31,43) — shop, save point, NPC
  for (let x = 2; x < 30; x++) for (let y = 24; y < 42; y++) set(x, y, 3);
  // Shop building
  for (let x = 5; x < 12; x++) for (let y = 26; y < 32; y++) set(x, y, 0);
  set(8, 32, 9); meta[`8,32`] = { type: 'shop' };
  // Save point
  set(15, 35, 8); meta[`15,35`] = { type: 'save' };
  // NPC
  set(20, 30, 7); meta[`20,30`] = { type: 'chest', item: 'npc_quest', label: 'Village Elder: Bring me 3 Dark Essences for a reward!' };
  // Village walls (partial)
  for (let x = 2; x < 30; x++) { set(x, 24, 1); set(x, 41, 1); }
  for (let y = 24; y < 42; y++) { set(2, y, 1); set(29, y, 1); }
  // Village gates
  set(15, 24, 0); set(16, 24, 0);
  set(15, 41, 0); set(16, 41, 0);
  set(2, 32, 0); set(2, 33, 0);
  set(29, 32, 0); set(29, 33, 0);

  // Plains (32,22) to (63,43) — open area with scattered enemies
  for (let x = 33; x < 63; x++) for (let y = 23; y < 42; y++) set(x, y, 0);
  // Some rocks
  for (let i = 0; i < 15; i++) {
    const tx = 34 + Math.floor(Math.random()*27);
    const ty = 24 + Math.floor(Math.random()*17);
    set(tx, ty, 1);
  }
  // Mid point - ranged weapon discovery here
  set(47, 32, 7); meta[`47,32`] = { type: 'chest', item: 'hunterBow', label: "Hunter's Bow", midgame: true };
  // Health upgrade
  set(38, 28, 7); meta[`38,28`] = { type: 'chest', item: 'heart', label: 'Heart Shard' };

  // Darkwood (64,22) to (95,43) — dense forest, harder enemies
  for (let x = 65; x < 95; x++) for (let y = 23; y < 42; y++) set(x, y, 0);
  for (let i = 0; i < 60; i++) {
    const tx = 66 + Math.floor(Math.random()*27);
    const ty = 24 + Math.floor(Math.random()*17);
    set(tx, ty, 4);
  }
  // Path through
  for (let y = 30; y < 36; y++) set(78, y, 0);
  for (let x = 70; x < 88; x++) set(x, 38, 0);
  // Second weapon upgrade
  set(85, 35, 7); meta[`85,35`] = { type: 'chest', item: 'flameSword', label: 'Flame Sword' };
  // More health
  set(70, 26, 7); meta[`70,26`] = { type: 'chest', item: 'heart', label: 'Heart Shard' };

  // Caves (0,44) to (31,63) — underground area
  for (let x = 2; x < 30; x++) for (let y = 44; y < 63; y++) set(x, y, 1);
  // Cave rooms
  for (let x = 4; x < 14; x++) for (let y = 46; y < 54; y++) set(x, y, 0);
  for (let x = 14; x < 28; x++) for (let y = 46; y < 54; y++) set(x, y, 0);
  for (let x = 4; x < 28; x++) for (let y = 54; y < 61; y++) set(x, y, 0);
  // Save point in cave
  set(10, 50, 8); meta[`10,50`] = { type: 'save' };
  // Gold cache
  set(22, 57, 7); meta[`22,57`] = { type: 'chest', item: 'gold50', label: 'Gold Cache (50g)' };

  // Ruins (32,44) to (63,63) — ancient temple area
  for (let x = 33; x < 62; x++) for (let y = 45; y < 62; y++) set(x, y, 3);
  // Pillar ruins
  for (let x = 36; x < 60; x += 4) for (let y = 48; y < 60; y += 4) set(x, y, 1);
  set(37, 49, 1); set(38, 49, 1); set(37, 50, 1);
  // Chest with accessory
  set(55, 55, 7); meta[`55,55`] = { type: 'chest', item: 'boots', label: 'Swift Boots' };
  set(40, 57, 7); meta[`40,57`] = { type: 'chest', item: 'potion', label: 'Health Potion' };
  // Save
  set(58, 48, 8); meta[`58,48`] = { type: 'save' };

  // Castle (64,44) to (95,63) — final area
  for (let x = 65; x < 94; x++) for (let y = 45; y < 62; y++) set(x, y, 1);
  // Castle interior
  for (let x = 67; x < 92; x++) for (let y = 47; y < 60; y++) set(x, y, 3);
  // Throne room
  for (let x = 75; x < 85; x++) for (let y = 48; y < 54; y++) set(x, y, 0);
  // Boss trigger
  set(80, 50, 7); meta[`80,50`] = { type: 'boss', label: 'Shadow Lord' };
  // Castle gates
  set(67, 53, 0); set(68, 53, 0);

  // Connecting paths between regions
  // Meadow-Plains connection
  for (let x = 30; x < 33; x++) for (let y = 30; y < 35; y++) set(x, y, 0);

  // Fix some connectivity - ensure paths exist between adjacent regions
  // Village to Cave
  for (let y = 42; y < 46; y++) set(10, y, 0);
  set(10, 43, 0); set(11, 43, 0);

  // Plains to Ruins
  for (let y = 42; y < 46; y++) set(47, y, 0);

  // Village-Plains gate already open via shared area
  // Plains-Darkwood shared border
  for (let y = 30; y < 36; y++) set(63, y, 0);

  return { tiles, meta };
}
// ===== PLAYER =====
class Player {
  constructor(x, y) {
    this.x = x; this.y = y;
    this.w = 20; this.h = 20;
    this.vx = 0; this.vy = 0;
    this.speed = 2.2;
    this.maxHp = 3; // Start with 3 HP (1.5 hearts)
    this.targetMaxHp = 5; // Must reach 5 HP (2.5 hearts)
    this.hp = 3;
    this.facing = 0; // angle in radians
    this.attackCooldown = 0;
    this.weapon = 'rustySword';
    this.inventory = [];
    this.gold = 0;
    this.attackAnim = 0;
    this.invincible = 0;
    this.essences = 0;
    this.steps = 0;
    this.walkTimer = 0;
    // Visual
    this.flashTimer = 0;
    // Ranged ammo
    this.arrows = 20;
    this.hasRanged = false;
    this.buffs = { speed: 0, damage: 0, shield: 0 };
  }

  get currentWeapon() { return WEAPONS[this.weapon]; }
  get heartCount() { return Math.ceil(this.maxHp / 2); }

  update(dt, input, world) {
    // Movement
    let dx = 0, dy = 0;
    if (input.isDown('w') || input.isDown('arrowup')) dy -= 1;
    if (input.isDown('s') || input.isDown('arrowdown')) dy += 1;
    if (input.isDown('a') || input.isDown('arrowleft')) dx -= 1;
    if (input.isDown('d') || input.isDown('arrowright')) dx += 1;

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

    const spd = this.speed + (this.buffs.speed > 0 ? 0.8 : 0);
    const newX = this.x + dx * spd;
    const newY = this.y + dy * spd;

    // Collision with tiles
    if (!this.collides(newX, this.y, world)) this.x = newX;
    if (!this.collides(this.x, newY, world)) this.y = newY;

    // Walk sound
    if (dx !== 0 || dy !== 0) {
      this.walkTimer += dt;
      if (this.walkTimer > 200) { this.walkTimer = 0; audio.walk(); }
    }

    // Facing direction (towards mouse or movement)
    if (game && game.mouse) {
      const centerX = this.x - camera.x + this.w/2;
      const centerY = this.y - camera.y + this.h/2;
      this.facing = Math.atan2(game.mouse.y - centerY, game.mouse.x - centerX);
    }

    // Attack cooldown
    if (this.attackCooldown > 0) this.attackCooldown -= dt;
    if (this.invincible > 0) this.invincible -= dt;
    if (this.flashTimer > 0) this.flashTimer -= dt;
    if (this.attackAnim > 0) this.attackAnim -= dt;
    if (this.buffs.shield > 0) this.buffs.shield -= dt;
    if (this.buffs.speed > 0) this.buffs.speed -= dt;

    // Dash regen
    if (this.attackAnim < 0) this.attackAnim = 0;
  }

  collides(x, y, world) {
    const margin = 2;
    const points = [
      [x + margin, y + margin],
      [x + this.w - margin, y + margin],
      [x + margin, y + this.h - margin],
      [x + this.w - margin, y + this.h - margin]
    ];
    for (const [px, py] of points) {
      const tx = Math.floor(px / TILE);
      const ty = Math.floor(py / TILE);
      const tile = world.getTile(tx, ty);
      if (tile === 1 || tile === 2 || tile === 4) return true;
    }
    return false;
  }

  takeDamage(amount) {
    if (this.invincible > 0) return;
    if (this.buffs.shield > 0) { this.buffs.shield = 0; this.invincible = 500; return; }
    this.hp -= amount;
    this.invincible = 800;
    this.flashTimer = 800;
    audio.damage();
    particles.emit(this.x + this.w/2, this.y + this.h/2, 8, '#f44', 20, 2);
    if (this.hp <= 0) {
      this.hp = 0;
      game.onPlayerDeath();
    }
  }

  heal(amount) {
    this.hp = Math.min(this.maxHp, this.hp + amount);
    particles.emit(this.x + this.w/2, this.y + this.h/2, 10, '#4f4', 25, 1.5);
    audio.pickup();
  }

  upgradeMaxHp() {
    if (this.maxHp < this.targetMaxHp) {
      this.maxHp += 2;
      this.hp = Math.min(this.hp + 2, this.maxHp);
      audio.levelup();
      game.showDialogue('❤ Max HP increased! You feel stronger.');
    }
  }

  draw(ctx, camX, camY) {
    const sx = this.x - camX;
    const sy = this.y - camY;

    // Invincibility flash
    if (this.flashTimer > 0 && Math.floor(this.flashTimer / 80) % 2 === 0) return;

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

    // Body
    ctx.fillStyle = '#4488cc';
    ctx.fillRect(sx + 4, sy + 2, this.w - 8, this.h - 2);
    // Armor highlight
    ctx.fillStyle = '#55aaee';
    ctx.fillRect(sx + 6, sy + 2, this.w - 12, 6);

    // Eyes based on facing
    const eyeOffX = Math.cos(this.facing) * 3;
    const eyeOffY = Math.sin(this.facing) * 2;
    ctx.fillStyle = '#fff';
    ctx.fillRect(sx + 7 + eyeOffX, sy + 6 + eyeOffY, 4, 4);
    ctx.fillRect(sx + 13 + eyeOffX, sy + 6 + eyeOffY, 4, 4);
    ctx.fillStyle = '#000';
    ctx.fillRect(sx + 8 + eyeOffX + Math.cos(this.facing), sy + 7 + eyeOffY, 2, 2);
    ctx.fillRect(sx + 14 + eyeOffX + Math.cos(this.facing), sy + 7 + eyeOffY, 2, 2);

    // Weapon visualization
    const wpn = this.currentWeapon;
    const wAngle = this.facing;
    const wLen = this.w + (this.attackAnim > 0 ? 8 : 2);
    const wx = sx + this.w/2 + Math.cos(wAngle) * wLen;
    const wy = sy + this.h/2 + Math.sin(wAngle) * wLen;

    ctx.strokeStyle = wpn.color;
    ctx.lineWidth = 3;
    ctx.beginPath();
    ctx.moveTo(sx + this.w/2, sy + this.h/2);
    ctx.lineTo(wx, wy);
    ctx.stroke();

    // Blade
    if (wpn.type === 'melee') {
      ctx.fillStyle = wpn.color;
      ctx.beginPath();
      ctx.arc(wx, wy, 4, 0, Math.PI*2);
      ctx.fill();
      if (wpn.flame) {
        ctx.fillStyle = '#ff0';
        ctx.beginPath();
        ctx.arc(wx + Math.cos(wAngle)*3, wy + Math.sin(wAngle)*3, 3, 0, Math.PI*2);
        ctx.fill();
      }
    }

    // Shield buff
    if (this.buffs.shield > 0) {
      ctx.strokeStyle = '#4ff';
      ctx.lineWidth = 2;
      ctx.globalAlpha = 0.6;
      ctx.beginPath();
      ctx.arc(sx + this.w/2, sy + this.h/2, this.w, 0, Math.PI*2);
      ctx.stroke();
      ctx.globalAlpha = 1;
    }
  }
}

// ===== ENEMY =====
class Enemy {
  constructor(x, y, type) {
    this.x = x; this.y = y;
    this.type = type;
    this.flashTimer = 0;
    this.attackCooldown = 0;
    this.knockbackX = 0; this.knockbackY = 0;

    switch(type) {
      case 'slime':
        this.w = 18; this.h = 18; this.maxHp = 3; this.speed = 0.6;
        this.color = '#4a4'; this.contactDmg = 0.5; this.goldDrop = [2,5];
        this.xpWorth = 1;
        break;
      case 'bat':
        this.w = 16; this.h = 16; this.maxHp = 2; this.speed = 1.8;
        this.color = '#848'; this.contactDmg = 0.5; this.goldDrop = [1,3];
        this.xpWorth = 1; this.flying = true;
        break;
      case 'skeleton':
        this.w = 20; this.h = 22; this.maxHp = 5; this.speed = 0.9;
        this.color = '#ddd'; this.contactDmg = 1; this.goldDrop = [4,8];
        this.xpWorth = 2;
        break;
      case 'knight':
        this.w = 24; this.h = 24; this.maxHp = 8; this.speed = 0.7;
        this.color = '#888'; this.contactDmg = 1.5; this.goldDrop = [8,15];
        this.xpWorth = 3;
        break;
      case 'darkmage':
        this.w = 20; this.h = 22; this.maxHp = 6; this.speed = 0.5;
        this.color = '#808'; this.contactDmg = 1; this.ranged = true;
        this.goldDrop = [6,12]; this.xpWorth = 3;
        break;
      case 'boss':
        this.w = 40; this.h = 40; this.maxHp = 30; this.speed = 0.6;
        this.color = '#202'; this.contactDmg = 2; this.ranged = true;
        this.goldDrop = [50,50]; this.xpWorth = 20;
        this.isBoss = true;
        break;
    }
    this.hp = this.maxHp;
  }

  update(dt, player, world) {
    this.attackCooldown -= dt;
    if (this.flashTimer > 0) this.flashTimer -= dt;

    // Knockback
    if (Math.abs(this.knockbackX) > 0.1 || Math.abs(this.knockbackY) > 0.1) {
      this.x += this.knockbackX;
      this.y += this.knockbackY;
      this.knockbackX *= 0.8;
      this.knockbackY *= 0.8;
      return;
    }

    const dx = player.x - this.x;
    const dy = player.y - this.y;
    const dist = Math.sqrt(dx*dx + dy*dy);

    if (dist < 300) {
      const nx = dx / dist;
      const ny = dy / dist;
      const newX = this.x + nx * this.speed;
      const newY = this.y + ny * this.speed;

      if (!this.flying) {
        if (!this.collides(newX, this.y, world)) this.x = newX;
        if (!this.collides(this.x, newY, world)) this.y = newY;
      } else {
        this.x = newX; this.y = newY;
      }

      // Contact damage
      if (dist < 25 && this.attackCooldown <= 0) {
        player.takeDamage(this.contactDmg);
        this.attackCooldown = 800;
      }
    }
  }

  collides(x, y, world) {
    const points = [[x, y], [x+this.w, y], [x, y+this.h], [x+this.w, y+this.h]];
    for (const [px, py] of points) {
      const tx = Math.floor(px / TILE);
      const ty = Math.floor(py / TILE);
      const tile = world.getTile(tx, ty);
      if (tile === 1 || tile === 2 || tile === 4) return true;
    }
    return false;
  }

  takeDamage(amount, fromX, fromY) {
    this.hp -= amount;
    this.flashTimer = 150;
    const angle = Math.atan2(this.y - fromY, this.x - fromX);
    this.knockbackX = Math.cos(angle) * 4;
    this.knockbackY = Math.sin(angle) * 4;
    audio.hit();
    particles.emit(this.x + this.w/2, this.y + this.h/2, 6, this.color, 15, 2);

    if (this.hp <= 0) {
      this.die();
      return true;
    }
    return false;
  }

  die() {
    audio.explosion();
    particles.emit(this.x + this.w/2, this.y + this.h/2, 15, this.color, 25, 3);

    // Drop gold
    const goldAmt = this.goldDrop[0] + Math.floor(Math.random() * (this.goldDrop[1] - this.goldDrop[0] + 1));
    for (let i = 0; i < Math.min(goldAmt, 5); i++) {
      game.items.push(new ItemPickup(
        this.x + Math.random() * this.w,
        this.y + Math.random() * this.h,
        'coin', goldAmt <= 5 ? goldAmt : Math.ceil(goldAmt / 5)
      ));
    }

    // Darkwood enemies drop essences
    if (this.type === 'knight' || this.type === 'darkmage') {
      if (Math.random() < 0.4) {
        game.items.push(new ItemPickup(this.x + this.w/2, this.y + this.h/2, 'essence', 1));
      }
    }

    if (this.isBoss) game.onBossDefeated();
  }

  draw(ctx, camX, camY) {
    const sx = this.x - camX;
    const sy = this.y - camY;

    if (this.flashTimer > 0) {
      ctx.globalAlpha = 0.7;
      ctx.fillStyle = '#fff';
    } else {
      ctx.fillStyle = this.color;
    }

    if (this.type === 'slime') {
      ctx.beginPath();
      ctx.ellipse(sx + this.w/2, sy + this.h/2, this.w/2, this.h/2, 0, 0, Math.PI*2);
      ctx.fill();
      // Eyes
      ctx.fillStyle = '#fff';
      ctx.fillRect(sx + 4, sy + 6, 4, 4);
      ctx.fillRect(sx + 10, sy + 6, 4, 4);
      ctx.fillStyle = '#000';
      ctx.fillRect(sx + 5, sy + 7, 2, 2);
      ctx.fillRect(sx + 11, sy + 7, 2, 2);
    } else if (this.type === 'bat') {
      ctx.beginPath();
      ctx.ellipse(sx + this.w/2, sy + this.h/2, this.w/2, this.h/3, 0, 0, Math.PI*2);
      ctx.fill();
      // Wings
      const wing = Math.sin(Date.now() * 0.01) * 4;
      ctx.fillRect(sx - 4, sy + 4 + wing, 6, 3);
      ctx.fillRect(sx + this.w - 2, sy + 4 - wing, 6, 3);
      // Eyes
      ctx.fillStyle = '#f00';
      ctx.fillRect(sx + 5, sy + 6, 3, 3);
      ctx.fillRect(sx + 9, sy + 6, 3, 3);
    } else {
      // General enemy body
      ctx.fillRect(sx + 2, sy, this.w - 4, this.h);
      // Head
      ctx.fillRect(sx + 4, sy - 2, this.w - 8, 6);
      // Eyes
      ctx.fillStyle = this.flashTimer > 0 ? '#fff' : '#f00';
      ctx.fillRect(sx + 5, sy + 2, 4, 4);
      ctx.fillRect(sx + this.w - 9, sy + 2, 4, 4);
    }

    ctx.globalAlpha = 1;

    // HP bar
    if (this.hp < this.maxHp) {
      const barW = this.w;
      ctx.fillStyle = '#600';
      ctx.fillRect(sx, sy - 8, barW, 4);
      ctx.fillStyle = '#f44';
      ctx.fillRect(sx, sy - 8, barW * (this.hp / this.maxHp), 4);
    }

    // Boss indicator
    if (this.isBoss) {
      ctx.strokeStyle = '#f0f';
      ctx.lineWidth = 2;
      ctx.strokeRect(sx - 4, sy - 4, this.w + 8, this.h + 8);
    }
  }
}

// ===== PROJECTILE (ranged) =====
class Projectile {
  constructor(x, y, angle, speed, dmg, owner) {
    this.x = x; this.y = y;
    this.vx = Math.cos(angle) * speed;
    this.vy = Math.sin(angle) * speed;
    this.dmg = dmg;
    this.owner = owner;
    this.life = 120;
    this.size = 5;
  }
  update(dt, world) {
    this.x += this.vx;
    this.y += this.vy;
    this.life--;
    // Check tile collision
    const tx = Math.floor(this.x / TILE);
    const ty = Math.floor(this.y / TILE);
    const tile = world.getTile(tx, ty);
    if (tile === 1 || tile === 2 || tile === 4) this.life = 0;
  }
  draw(ctx, camX, camY) {
    const sx = this.x - camX;
    const sy = this.y - camY;
    ctx.fillStyle = '#ff8';
    ctx.beginPath();
    ctx.arc(sx, sy, this.size/2, 0, Math.PI*2);
    ctx.fill();
    // Trail
    ctx.fillStyle = 'rgba(255,200,50,0.5)';
    ctx.beginPath();
    ctx.arc(sx - this.vx*2, sy - this.vy*2, 2, 0, Math.PI*2);
    ctx.fill();
  }
}

// ===== ITEM PICKUP =====
class ItemPickup {
  constructor(x, y, type, amount) {
    this.x = x; this.y = y;
    this.type = type;
    this.amount = amount;
    this.bobOffset = Math.random() * Math.PI * 2;
    this.life = 99999;
    this.collected = false;
  }
  update(dt) {
    this.life--;
    // Bob animation
    this.bobOffset += 0.05;
  }
  draw(ctx, camX, camY) {
    const sx = this.x - camX;
    const sy = this.y - camY + Math.sin(this.bobOffset) * 3;

    switch(this.type) {
      case 'coin':
        ctx.fillStyle = '#ffd700';
        ctx.beginPath();
        ctx.arc(sx + 6, sy + 6, 5, 0, Math.PI*2);
        ctx.fill();
        ctx.fillStyle = '#aa8800';
        ctx.font = '7px monospace';
        ctx.fillText('$', sx + 3, sy + 9);
        break;
      case 'heart':
        ctx.fillStyle = '#f44';
        // Heart shape
        ctx.beginPath();
        ctx.moveTo(sx + 7, sy + 4);
        ctx.bezierCurveTo(sx + 7, sy + 2, sx + 4, sy + 1, sx + 4, sy + 4);
        ctx.bezierCurveTo(sx + 4, sy + 7, sx + 7, sy + 9, sx + 7, sy + 10);
        ctx.bezierCurveTo(sx + 7, sy + 9, sx + 10, sy + 7, sx + 10, sy + 4);
        ctx.bezierCurveTo(sx + 10, sy + 1, sx + 7, sy + 2, sx + 7, sy + 4);
        ctx.fill();
        break;
      case 'potion':
        ctx.fillStyle = '#f44';
        ctx.fillRect(sx + 2, sy + 4, 8, 8);
        ctx.fillStyle = '#8f8';
        ctx.fillRect(sx + 4, sy + 2, 4, 4);
        break;
      case 'essence':
        ctx.fillStyle = '#a0f';
        ctx.beginPath();
        ctx.arc(sx + 6, sy + 6, 5, 0, Math.PI*2);
        ctx.fill();
        ctx.fillStyle = '#f0f';
        ctx.beginPath();
        ctx.arc(sx + 6, sy + 6, 3, 0, Math.PI*2);
        ctx.fill();
        break;
      default:
        ctx.fillStyle = '#fff';
        ctx.fillRect(sx + 1, sy + 1, 10, 10);
    }
  }
}
// ===== GAME =====
class Game {
  constructor() {
    this.canvas = document.getElementById('gameCanvas');
    this.canvas.width = W;
    this.canvas.height = H;
    this.ctx = this.canvas.getContext('2d');
    this.minimap = document.getElementById('minimap');
    this.mctx = this.minimap.getContext('2d');

    // Generate world
    const worldData = generateWorld();
    this.worldTiles = worldData.tiles;
    this.worldMeta = worldData.meta;

    // World utility
    this.getTile = (x, y) => {
      if (x < 0 || x >= WORLD_W || y < 0 || y >= WORLD_H) return 1;
      return this.worldTiles[y * WORLD_W + x];
    };

    // Player
    this.player = new Player(8 * TILE, 8 * TILE);

    // Entities
    this.enemies = [];
    this.items = [];
    this.projectiles = [];

    // Camera
    window.camera = new Camera(W, H);
    camera = window.camera;

    // Input
    window.input = new Input();
    input = window.input;

    // Audio & Particles
    window.audio = new AudioSystem();
    audio = window.audio;
    window.particles = new ParticleSystem();
    particles = window.particles;

    // Mouse
    this.mouse = { x: W/2, y: H/2 };
    this.canvas.addEventListener('mousemove', e => {
      const rect = this.canvas.getBoundingClientRect();
      this.mouse.x = (e.clientX - rect.left) * (W / rect.width);
      this.mouse.y = (e.clientY - rect.top) * (H / rect.height);
    });

    // UI state
    this.showInventory = false;
    this.showShop = false;
    this.dialogue = '';
    this.dialogueTimer = 0;
    this.gameOver = false;
    this.gameWon = false;
    this.paused = false;
    this.hasSaved = false;

    // Spawn enemies
    this.spawnEnemies();

    // Shop items
    this.shopData = [
      { name: 'Health Potion (heals 2HP)', price: 15, action: () => { this.player.heal(2); } },
      { name: 'Speed Elixir (temp boost)', price: 20, action: () => { this.player.buffs.speed = 8000; } },
      { name: 'Shield Charm (1 free hit)', price: 25, action: () => { this.player.buffs.shield = 99999; } },
      { name: 'Quiver (+10 arrows)', price: 10, action: () => { this.player.arrows += 10; } },
      { name: 'Heart Shard (+2 Max HP)', price: 50, action: () => { this.player.upgradeMaxHp(); } },
    ];

    // Track progress
    this.midgameTriggered = false;
    this.weaponsFound = new Set(['rustySword']);

    // Timing
    this.lastTime = 0;
    this.accumulator = 0;
    this.fixedDt = 16;

    // Bind inventory close listener
    document.addEventListener('keydown', e => {
      if (e.key.toLowerCase() === 'e') {
        if (this.showShop) return;
        this.showInventory = !this.showInventory;
      }
      if (e.key.toLowerCase() === 'escape') {
        this.showInventory = false;
        this.showShop = false;
      }
    });

    // Start
    this.showDialogue('Welcome to the Ruins. Find chests, defeat enemies, and grow stronger. WASD to move, SPACE to attack.');
  }

  spawnEnemies() {
    // Forest region (32,0) to (63,21)
    for (let i = 0; i < 6; i++) {
      const x = (34 + Math.random() * 27) * TILE;
      const y = (3 + Math.random() * 17) * TILE;
      this.enemies.push(new Enemy(x, y, 'slime'));
    }
    for (let i = 0; i < 3; i++) {
      const x = (34 + Math.random() * 27) * TILE;
      const y = (3 + Math.random() * 17) * TILE;
      this.enemies.push(new Enemy(x, y, 'bat'));
    }

    // Plains region (32,22) to (63,43)
    for (let i = 0; i < 4; i++) {
      const x = (34 + Math.random() * 27) * TILE;
      const y = (24 + Math.random() * 17) * TILE;
      this.enemies.push(new Enemy(x, y, 'skeleton'));
    }
    for (let i = 0; i < 3; i++) {
      const x = (34 + Math.random() * 27) * TILE;
      const y = (24 + Math.random() * 17) * TILE;
      this.enemies.push(new Enemy(x, y, 'bat'));
    }

    // Darkwood region (64,22) to (95,43)
    for (let i = 0; i < 5; i++) {
      const x = (66 + Math.random() * 27) * TILE;
      const y = (24 + Math.random() * 17) * TILE;
      this.enemies.push(new Enemy(x, y, 'knight'));
    }
    for (let i = 0; i < 3; i++) {
      const x = (66 + Math.random() * 27) * TILE;
      const y = (24 + Math.random() * 17) * TILE;
      this.enemies.push(new Enemy(x, y, 'darkmage'));
    }

    // Caves (0,44) to (31,63)
    for (let i = 0; i < 5; i++) {
      const x = (4 + Math.random() * 24) * TILE;
      const y = (47 + Math.random() * 13) * TILE;
      this.enemies.push(new Enemy(x, y, 'skeleton'));
    }
    for (let i = 0; i < 3; i++) {
      const x = (4 + Math.random() * 24) * TILE;
      const y = (47 + Math.random() * 13) * TILE;
      this.enemies.push(new Enemy(x, y, 'bat'));
    }

    // Ruins (32,44) to (63,63)
    for (let i = 0; i < 4; i++) {
      const x = (34 + Math.random() * 26) * TILE;
      const y = (46 + Math.random() * 14) * TILE;
      this.enemies.push(new Enemy(x, y, 'knight'));
    }
    for (let i = 0; i < 3; i++) {
      const x = (34 + Math.random() * 26) * TILE;
      const y = (46 + Math.random() * 14) * TILE;
      this.enemies.push(new Enemy(x, y, 'darkmage'));
    }

    // Castle (64,44) to (95,63)
    for (let i = 0; i < 4; i++) {
      const x = (68 + Math.random() * 23) * TILE;
      const y = (48 + Math.random() * 11) * TILE;
      this.enemies.push(new Enemy(x, y, 'knight'));
    }
    for (let i = 0; i < 2; i++) {
      const x = (68 + Math.random() * 23) * TILE;
      const y = (48 + Math.random() * 11) * TILE;
      this.enemies.push(new Enemy(x, y, 'darkmage'));
    }
  }

  spawnBoss() {
    this.enemies.push(new Enemy(80 * TILE, 50 * TILE, 'boss'));
    this.showDialogue('The Shadow Lord awakens! Defeat him to escape the Ruins!');
  }

  update(dt) {
    if (this.gameOver || this.gameWon) return;
    if (this.paused) return;

    if (this.showInventory || this.showShop) return;

    // Dialogue timer
    if (this.dialogueTimer > 0) {
      this.dialogueTimer -= dt;
      if (this.dialogueTimer <= 0) this.dialogue = '';
    }

    // Player update
    this.player.update(dt, input, this);

    // Check for mid-game progression
    if (!this.midgameTriggered && this.weaponsFound.size >= 2) {
      this.midgameTriggered = true;
      this.showDialogue('You feel the wind shift. New paths open before you...');
    }

    // Player attack
    if ((input.isDown(' ') || input.isDown('Space')) && this.player.attackCooldown <= 0) {
      this.playerAttack();
    }

    // Ranged attack
    this.player.rangedCooldown = (this.player.rangedCooldown || 0);
    if (this.player.rangedCooldown > 0) this.player.rangedCooldown -= dt;
    if (input.isDown('shift') && this.player.hasRanged && this.player.arrows > 0 && this.player.rangedCooldown <= 0) {
      this.rangedAttack();
    }

    // Camera
    camera.follow(this.player.x + this.player.w/2, this.player.y + this.player.h/2, WORLD_W * TILE, WORLD_H * TILE);

    // Update enemies (only those near camera)
    const camLeft = camera.x - 100, camRight = camera.x + W + 100;
    const camTop = camera.y - 100, camBottom = camera.y + H + 100;

    for (let i = this.enemies.length - 1; i >= 0; i--) {
      const e = this.enemies[i];
      if (e.x + e.w > camLeft && e.x < camRight && e.y + e.h > camTop && e.y < camBottom) {
        e.update(dt, this.player, this);
      }
      if (e.hp <= 0) {
        this.enemies.splice(i, 1);
      }
    }

    // Update items
    for (let i = this.items.length - 1; i >= 0; i--) {
      this.items[i].update(dt);
      if (this.items[i].life <= 0) { this.items.splice(i, 1); continue; }

      // Collect
      const dx = this.player.x + this.player.w/2 - this.items[i].x;
      const dy = this.player.y + this.player.h/2 - this.items[i].y;
      if (Math.sqrt(dx*dx + dy*dy) < 25) {
        this.collectItem(this.items[i]);
        this.items.splice(i, 1);
      }
    }

    // Update projectiles
    for (let i = this.projectiles.length - 1; i >= 0; i--) {
      const p = this.projectiles[i];
      p.update(dt, this);
      if (p.life <= 0) { this.projectiles.splice(i, 1); continue; }

      // Hit enemies
      for (let j = this.enemies.length - 1; j >= 0; j--) {
        const e = this.enemies[j];
        if (p.x > e.x && p.x < e.x + e.w && p.y > e.y && p.y < e.y + e.h) {
          const killed = e.takeDamage(p.dmg, p.x, p.y);
          this.projectiles.splice(i, 1);
          break;
        }
      }
    }

    // Particles
    particles.update();

    // Check interactions (tile in front of player)
    this.checkTileInteractions();

    // Clear input
    input.clearFrame();
  }

  playerAttack() {
    const wpn = this.player.currentWeapon;
    this.player.attackCooldown = wpn.cooldown;
    this.player.attackAnim = 200;
    audio.swing();

    // Melee attack
    if (wpn.type === 'melee') {
      const ax = this.player.x + this.player.w/2 + Math.cos(this.player.facing) * wpn.range * 0.5;
      const ay = this.player.y + this.player.h/2 + Math.sin(this.player.facing) * wpn.range * 0.5;

      for (const enemy of this.enemies) {
        const ex = enemy.x + enemy.w/2;
        const ey = enemy.y + enemy.h/2;
        const dx = ex - ax;
        const dy = ey - ay;
        const dist = Math.sqrt(dx*dx + dy*dy);
        const angle = Math.atan2(dy, dx);
        const angleDiff = Math.abs(angle - this.player.facing);

        if (dist < wpn.range && (angleDiff < wpn.arc || angleDiff > Math.PI*2 - wpn.arc)) {
          const killed = enemy.takeDamage(wpn.dmg + this.player.buffs.damage, ax, ay);
          if (wpn.flame) {
            particles.emit(ex, ey, 5, '#f80', 15, 1);
          }
        }
      }

      // Slash visual
      particles.emit(ax, ay, 3, wpn.color, 10, 2);
    }
  }

  rangedAttack() {
    const wpn = WEAPONS.hunterBow;
    this.player.rangedCooldown = wpn.cooldown;
    this.player.arrows--;
    audio.bowshot();

    const bx = this.player.x + this.player.w/2;
    const by = this.player.y + this.player.h/2;
    this.projectiles.push(new Projectile(bx, by, this.player.facing, wpn.speed, wpn.dmg, 'player'));

    particles.emit(bx, by, 3, '#ff8', 8, 2);
  }

  collectItem(item) {
    audio.pickup();
    switch(item.type) {
      case 'coin':
        this.player.gold += item.amount;
        audio.coin();
        break;
      case 'heart':
        this.player.heal(2);
        break;
      case 'potion':
        this.player.heal(3);
        break;
      case 'essence':
        this.player.essences++;
        this.showDialogue(`Dark Essence collected! (${this.player.essences}/3)`);
        break;
    }
  }

  checkTileInteractions() {
    const px = Math.floor((this.player.x + this.player.w/2) / TILE);
    const py = Math.floor((this.player.y + this.player.h/2) / TILE);

    // Check surrounding tiles
    for (let dx = -1; dx <= 1; dx++) {
      for (let dy = -1; dy <= 1; dy++) {
        const key = `${px+dx},${py+dy}`;
        const meta = this.worldMeta[key];
        if (!meta) continue;

        if (meta.type === 'save' && input.wasPressed('f')) {
          this.saveGame();
          return;
        }

        if (meta.type === 'shop' && input.wasPressed('f')) {
          this.openShop();
          return;
        }

        if (meta.type === 'chest' && input.wasPressed('f')) {
          this.openChest(key, meta);
          return;
        }

        if (meta.type === 'boss' && input.wasPressed('f')) {
          this.triggerBoss();
          return;
        }
      }
    }
  }

  openChest(key, meta) {
    if (meta.opened) {
      this.showDialogue('This chest is empty.');
      return;
    }
    meta.opened = true;
    audio.shop();
    particles.emit(
      parseInt(key.split(',')[0]) * TILE + TILE/2,
      parseInt(key.split(',')[1]) * TILE + TILE/2,
      15, '#ffd700', 30, 2
    );

    switch(meta.item) {
      case 'steelBlade':
        this.player.weapon = 'steelBlade';
        this.weaponsFound.add('steelBlade');
        this.showDialogue('⚔ Found Steel Blade! More damage, longer reach.');
        break;
      case 'flameSword':
        this.player.weapon = 'flameSword';
        this.weaponsFound.add('flameSword');
        this.showDialogue('🔥 Found Flame Sword! Wide arc, fire damage!');
        break;
      case 'hunterBow':
        this.player.hasRanged = true;
        this.player.arrows += 20;
        this.showDialogue('🏹 Found Hunter\'s Bow! Press SHIFT to fire arrows at range.');
        break;
      case 'heart':
        this.player.upgradeMaxHp();
        break;
      case 'boots':
        this.player.speed = 3.0;
        this.showDialogue('👢 Found Swift Boots! Movement speed increased.');
        this.player.inventory.push({ name: 'Swift Boots', type: 'accessory' });
        break;
      case 'potion':
        this.player.heal(3);
        this.showDialogue('🧪 Found Health Potion! HP restored.');
        break;
      case 'gold50':
        this.player.gold += 50;
        audio.coin();
        this.showDialogue('💰 Found 50 Gold!');
        break;
      case 'npc_quest':
        if (this.player.essences >= 3) {
          this.player.essences -= 3;
          this.player.gold += 40;
          this.player.upgradeMaxHp();
          meta.item = 'npc_done';
          this.showDialogue('The Elder thanks you! +40 Gold, +2 Max HP!');
        } else {
          this.showDialogue(`The Elder asks for 3 Dark Essences. You have ${this.player.essences}/3. Defeat knights and mages in Darkwood and Ruins to find them.`);
        }
        break;
      case 'npc_done':
        this.showDialogue('Thanks again, adventurer!');
        break;
    }
  }

  openShop() {
    this.showShop = true;
    audio.interact();
    this.renderShop();
  }

  closeShop() {
    this.showShop = false;
  }

  renderShop() {
    const panel = document.getElementById('shop-panel');
    const goldEl = document.getElementById('shop-gold');
    const itemsEl = document.getElementById('shop-items');

    goldEl.textContent = this.player.gold;
    panel.style.display = 'block';

    itemsEl.innerHTML = '';
    this.shopData.forEach((item, i) => {
      const div = document.createElement('div');
      div.className = 'shop-item';
      div.innerHTML = `<span class="name">${item.name}</span><span class="price">${item.price}g</span>`;
      div.onclick = () => {
        if (this.player.gold >= item.price) {
          this.player.gold -= item.price;
          item.action();
          audio.shop();
          this.renderShop();
        } else {
          this.showDialogue('Not enough gold!');
        }
      };
      itemsEl.appendChild(div);
    });
  }

  saveGame() {
    this.hasSaved = true;
    audio.save();

    const saveData = {
      playerX: this.player.x,
      playerY: this.player.y,
      hp: this.player.hp,
      maxHp: this.player.maxHp,
      gold: this.player.gold,
      weapon: this.player.weapon,
      hasRanged: this.player.hasRanged,
      arrows: this.player.arrows,
      essences: this.player.essences,
      speed: this.player.speed,
      inventory: this.player.inventory,
      weaponsFound: [...this.weaponsFound],
      openedChests: Object.keys(this.worldMeta).filter(k => this.worldMeta[k].opened),
      timestamp: Date.now()
    };

    localStorage.setItem('ruins_save', JSON.stringify(saveData));
    this.showDialogue('💾 Game saved! Your progress has been recorded.');

    // Visual effect
    particles.emit(this.player.x + this.player.w/2, this.player.y, 20, '#4ff', 40, 2);
  }

  loadGame() {
    const data = localStorage.getItem('ruins_save');
    if (!data) return false;

    try {
      const save = JSON.parse(data);
      this.player.x = save.playerX;
      this.player.y = save.playerY;
      this.player.hp = save.hp;
      this.player.maxHp = save.maxHp;
      this.player.gold = save.gold;
      this.player.weapon = save.weapon;
      this.player.hasRanged = save.hasRanged;
      this.player.arrows = save.arrows;
      this.player.essences = save.essences;
      this.player.speed = save.speed;
      this.player.inventory = save.inventory || [];
      this.weaponsFound = new Set(save.weaponsFound || ['rustySword']);

      // Restore opened chests
      if (save.openedChests) {
        save.openedChests.forEach(k => {
          if (this.worldMeta[k]) this.worldMeta[k].opened = true;
        });
      }

      this.showDialogue('💾 Save loaded! Welcome back, adventurer.');
      return true;
    } catch(e) {
      return false;
    }
  }

  triggerBoss() {
    // Check if player has flame sword (final weapon) or enough upgrades
    if (this.player.weapon !== 'flameSword' && this.player.maxHp < 5) {
      this.showDialogue('A powerful evil stirs within... You are not ready yet. Seek the Flame Sword and more health.');
      return;
    }
    this.spawnBoss();
    // Mark so it doesn't trigger again
    this.worldMeta['80,50'].triggered = true;
  }

  onBossDefeated() {
    this.gameWon = true;
    audio.levelup();
    particles.emit(this.player.x, this.player.y, 50, '#ffd700', 60, 4);
    this.showDialogue('🎉 VICTORY! The Shadow Lord has been vanquished! You have escaped the Ruins! Explore freely or press R to restart.');
  }

  onPlayerDeath() {
    this.gameOver = true;
    audio.damage();
    this.showDialogue('💀 You have fallen... Press R to restart from your last save.');
  }

  showDialogue(text) {
    this.dialogue = text;
    this.dialogueTimer = 4000;
    document.getElementById('dialogue-box').textContent = text;
    document.getElementById('dialogue-box').style.display = 'block';
  }

  render() {
    const ctx = this.ctx;
    ctx.fillStyle = '#1a1a2e';
    ctx.fillRect(0, 0, W, H);

    this.drawTiles(ctx);
    this.drawItems(ctx);
    this.drawEnemies(ctx);
    this.drawProjectiles(ctx);
    this.player.draw(ctx, camera.x, camera.y);
    particles.draw(ctx, camera.x, camera.y);
    this.drawHUD(ctx);

    if (this.gameWon) this.drawVictoryScreen(ctx);
    if (this.gameOver) this.drawDeathScreen(ctx);
  }

  drawTiles(ctx) {
    const startX = Math.max(0, Math.floor(camera.x / TILE));
    const startY = Math.max(0, Math.floor(camera.y / TILE));
    const endX = Math.min(WORLD_W, Math.ceil((camera.x + W) / TILE) + 1);
    const endY = Math.min(WORLD_H, Math.ceil((camera.y + H) / TILE) + 1);

    for (let y = startY; y < endY; y++) {
      for (let x = startX; x < endX; x++) {
        const tile = this.worldTiles[y * WORLD_W + x];
        const sx = x * TILE - camera.x;
        const sy = y * TILE - camera.y;

        switch(tile) {
          case 0: // grass
            ctx.fillStyle = ((x+y)%2 === 0) ? '#2d5a2d' : '#285028';
            ctx.fillRect(sx, sy, TILE, TILE);
            break;
          case 1: // wall/rock
            ctx.fillStyle = '#444';
            ctx.fillRect(sx, sy, TILE, TILE);
            ctx.fillStyle = '#555';
            ctx.fillRect(sx+2, sy+2, TILE-4, TILE-4);
            break;
          case 2: // water
            ctx.fillStyle = '#226';
            ctx.fillRect(sx, sy, TILE, TILE);
            ctx.fillStyle = '#338';
            ctx.fillRect(sx+4, sy+4, TILE-8, TILE-8);
            break;
          case 3: // path
            ctx.fillStyle = '#5a4a3a';
            ctx.fillRect(sx, sy, TILE, TILE);
            ctx.fillStyle = '#6a5a4a';
            ctx.fillRect(sx+4, sy+4, TILE-8, TILE-8);
            break;
          case 4: // tree
            ctx.fillStyle = '#2d5a2d';
            ctx.fillRect(sx, sy, TILE, TILE);
            ctx.fillStyle = '#1a3a1a';
            ctx.fillRect(sx+4, sy+8, TILE-8, TILE-8);
            ctx.fillStyle = '#2a6a2a';
            ctx.beginPath();
            ctx.arc(sx+TILE/2, sy+8, 10, 0, Math.PI*2);
            ctx.fill();
            break;
          case 5: // bridge
            ctx.fillStyle = '#654';
            ctx.fillRect(sx, sy, TILE, TILE);
            break;
          case 6: // door
            ctx.fillStyle = '#864';
            ctx.fillRect(sx, sy, TILE, TILE);
            ctx.fillStyle = '#a86';
            ctx.fillRect(sx+4, sy+4, TILE-8, TILE-8);
            break;
          case 7: // chest
            ctx.fillStyle = ((x+y)%2 === 0) ? '#2d5a2d' : '#285028';
            ctx.fillRect(sx, sy, TILE, TILE);
            ctx.fillStyle = '#a86';
            ctx.fillRect(sx+6, sy+8, TILE-12, TILE-12);
            ctx.fillStyle = '#ffd700';
            ctx.fillRect(sx+12, sy+12, TILE-24, TILE-24);
            // Opened check
            const key = `${x},${y}`;
            if (this.worldMeta[key] && this.worldMeta[key].opened) {
              ctx.fillStyle = '#553';
              ctx.fillRect(sx+6, sy+8, TILE-12, TILE-12);
            }
            break;
          case 8: // save point
            ctx.fillStyle = ((x+y)%2 === 0) ? '#2d5a2d' : '#285028';
            ctx.fillRect(sx, sy, TILE, TILE);
            ctx.fillStyle = '#4ff';
            ctx.beginPath();
            ctx.arc(sx+TILE/2, sy+TILE/2, 8, 0, Math.PI*2);
            ctx.fill();
            ctx.fillStyle = '#fff';
            ctx.beginPath();
            ctx.arc(sx+TILE/2, sy+TILE/2, 4, 0, Math.PI*2);
            ctx.fill();
            // Pulsing ring
            const pulse = Math.sin(Date.now() * 0.005) * 3;
            ctx.strokeStyle = 'rgba(68,255,255,0.4)';
            ctx.lineWidth = 2;
            ctx.beginPath();
            ctx.arc(sx+TILE/2, sy+TILE/2, 10 + pulse, 0, Math.PI*2);
            ctx.stroke();
            break;
          case 9: // shop
            ctx.fillStyle = '#654';
            ctx.fillRect(sx, sy, TILE, TILE);
            ctx.fillStyle = '#ffd700';
            ctx.font = '10px monospace';
            ctx.fillText('$$$', sx+8, sy+20);
            break;
          case 10: // stairs
            ctx.fillStyle = '#555';
            ctx.fillRect(sx, sy, TILE, TILE);
            ctx.fillStyle = '#777';
            ctx.fillRect(sx+4, sy+4, TILE-8, TILE-8);
            break;
        }
      }
    }
  }

  drawItems(ctx) {
    for (const item of this.items) {
      if (item.x > camera.x - 20 && item.x < camera.x + W + 20 &&
          item.y > camera.y - 20 && item.y < camera.y + H + 20) {
        item.draw(ctx, camera.x, camera.y);
      }
    }
  }

  drawEnemies(ctx) {
    for (const enemy of this.enemies) {
      if (enemy.x + enemy.w > camera.x - 50 && enemy.x < camera.x + W + 50 &&
          enemy.y + enemy.h > camera.y - 50 && enemy.y < camera.y + H + 50) {
        enemy.draw(ctx, camera.x, camera.y);
      }
    }
  }

  drawProjectiles(ctx) {
    for (const p of this.projectiles) {
      p.draw(ctx, camera.x, camera.y);
    }
  }
  drawHUD(ctx) {
    // Heath hearts
    const heartSize = 18;
    const startX = 12;
    const startY = 12;

    for (let i = 0; i < Math.ceil(this.player.maxHp / 2); i++) {
      const hx = startX + i * (heartSize + 4);
      const hy = startY;
      const hpInHeart = Math.min(2, Math.max(0, this.player.hp - i * 2));

      // Heart background
      ctx.fillStyle = '#400';
      this.drawHeart(ctx, hx, hy, heartSize);

      // Heart fill (based on HP in this heart)
      if (hpInHeart > 0) {
        ctx.fillStyle = hpInHeart >= 2 ? '#f44' : '#f88';
        ctx.save();
        ctx.beginPath();
        ctx.rect(hx, hy + heartSize * (1 - hpInHeart/2), heartSize, heartSize * hpInHeart/2);
        ctx.clip();
        this.drawHeart(ctx, hx, hy, heartSize);
        ctx.restore();
      }

      // Heart outline
      ctx.strokeStyle = '#a22';
      ctx.lineWidth = 1;
      this.drawHeart(ctx, hx, hy, heartSize, true);
    }

    // Gold
    ctx.fillStyle = '#ffd700';
    ctx.font = 'bold 14px monospace';
    ctx.fillText(`💰 ${this.player.gold}`, 12, 50);

    // Weapon
    ctx.fillStyle = '#aaa';
    ctx.font = '11px monospace';
    ctx.fillText(`⚔ ${this.player.currentWeapon.name}`, 12, 68);

    // Arrows
    if (this.player.hasRanged) {
      ctx.fillStyle = '#8f8';
      ctx.fillText(`🏹 ${this.player.arrows}`, 12, 84);
    }

    // Essences
    if (this.player.essences > 0) {
      ctx.fillStyle = '#a0f';
      ctx.fillText(`✦ ${this.player.essences}/3`, 12, 100);
    }

    // Buffs indicator
    if (this.player.buffs.speed > 0) {
      ctx.fillStyle = '#0ff';
      ctx.fillText('⚡ Speed', 12, 116);
    }

    // Mini-map indicator dots for chests/unopened
    // (drawn on minimap canvas separately)

    // Attack cooldown indicator
    if (this.player.attackCooldown > 0) {
      const pct = this.player.attackCooldown / this.player.currentWeapon.cooldown;
      ctx.fillStyle = 'rgba(255,255,255,0.2)';
      ctx.fillRect(W - 60, H - 20, 50, 8);
      ctx.fillStyle = 'rgba(255,200,50,0.6)';
      ctx.fillRect(W - 60, H - 20, 50 * (1 - pct), 8);
    }

    // Interaction hint
    const px = Math.floor((this.player.x + this.player.w/2) / TILE);
    const py = Math.floor((this.player.y + this.player.h/2) / TILE);
    let nearInteractable = false;
    for (let dx = -1; dx <= 1; dx++) {
      for (let dy = -1; dy <= 1; dy++) {
        const key = `${px+dx},${py+dy}`;
        const meta = this.worldMeta[key];
        if (meta && (meta.type === 'chest' || meta.type === 'save' || meta.type === 'shop' || meta.type === 'boss')) {
          if (meta.type === 'boss' && meta.triggered) continue;
          if (meta.type === 'chest' && meta.opened) continue;
          nearInteractable = true;
        }
      }
    }
    if (nearInteractable) {
      ctx.fillStyle = '#ffd700';
      ctx.font = '12px monospace';
      ctx.textAlign = 'center';
      ctx.fillText('[F] Interact', W/2, H - 30);
      ctx.textAlign = 'left';
    }
  }

  drawHeart(ctx, x, y, size, stroke = false) {
    const s = size / 12;
    ctx.beginPath();
    ctx.moveTo(x + 6*s, y + 4*s);
    ctx.bezierCurveTo(x + 6*s, y + 2*s, x + 2*s, y + 0*s, x + 2*s, y + 3*s);
    ctx.bezierCurveTo(x + 2*s, y + 7*s, x + 6*s, y + 10*s, x + 6*s, y + 11*s);
    ctx.bezierCurveTo(x + 6*s, y + 10*s, x + 10*s, y + 7*s, x + 10*s, y + 3*s);
    ctx.bezierCurveTo(x + 10*s, y + 0*s, x + 6*s, y + 2*s, x + 6*s, y + 4*s);
    if (stroke) {
      ctx.stroke();
    } else {
      ctx.fill();
    }
  }

  drawVictoryScreen(ctx) {
    ctx.fillStyle = 'rgba(0,0,0,0.6)';
    ctx.fillRect(0, 0, W, H);
    ctx.fillStyle = '#ffd700';
    ctx.font = 'bold 32px monospace';
    ctx.textAlign = 'center';
    ctx.fillText('VICTORY!', W/2, H/2 - 40);
    ctx.fillStyle = '#ddd';
    ctx.font = '14px monospace';
    ctx.fillText('The Shadow Lord has been defeated!', W/2, H/2);
    ctx.fillText(`Gold: ${this.player.gold} | Weapon: ${this.player.currentWeapon.name}`, W/2, H/2 + 25);
    ctx.fillText('Press R to play again', W/2, H/2 + 55);
    ctx.textAlign = 'left';
  }

  drawDeathScreen(ctx) {
    ctx.fillStyle = 'rgba(0,0,0,0.7)';
    ctx.fillRect(0, 0, W, H);
    ctx.fillStyle = '#f44';
    ctx.font = 'bold 32px monospace';
    ctx.textAlign = 'center';
    ctx.fillText('YOU DIED', W/2, H/2 - 20);
    ctx.fillStyle = '#aaa';
    ctx.font = '14px monospace';
    ctx.fillText('Press R to restart from last save', W/2, H/2 + 20);
    ctx.textAlign = 'left';
  }

  drawMinimap() {
    const ctx = this.mctx;
    const mmW = 120, mmH = 120;
    const scaleX = mmW / WORLD_W;
    const scaleY = mmH / WORLD_H;

    ctx.fillStyle = '#0a0a15';
    ctx.fillRect(0, 0, mmW, mmH);

    // Draw simplified tiles
    for (let y = 0; y < WORLD_H; y += 2) {
      for (let x = 0; x < WORLD_W; x += 2) {
        const tile = this.worldTiles[y * WORLD_W + x];
        let color = null;
        switch(tile) {
          case 1: color = '#444'; break;
          case 2: color = '#226'; break;
          case 3: color = '#654'; break;
          case 4: color = '#262'; break;
          case 7: color = this.worldMeta[`${x},${y}`]?.opened ? '#333' : '#ffd700'; break;
          case 8: color = '#4ff'; break;
          case 9: color = '#f80'; break;
          default: color = null;
        }
        if (color) {
          ctx.fillStyle = color;
          ctx.fillRect(x * scaleX, y * scaleY, scaleX * 2 + 1, scaleY * 2 + 1);
        }
      }
    }

    // Player position
    ctx.fillStyle = '#4f4';
    ctx.fillRect(
      (this.player.x / TILE) * scaleX - 2,
      (this.player.y / TILE) * scaleY - 2,
      4, 4
    );

    // Boss
    ctx.fillStyle = '#f0f';
    ctx.fillRect(80 * scaleX - 2, 50 * scaleY - 2, 4, 4);
  }

  renderInventoryUI() {
    const panel = document.getElementById('inventory-panel');
    const grid = document.getElementById('inv-grid');

    if (!this.showInventory) {
      panel.style.display = 'none';
      return;
    }

    panel.style.display = 'block';
    grid.innerHTML = '';

    // Show equipped weapon
    const wpnSlot = document.createElement('div');
    wpnSlot.className = 'inv-slot';
    wpnSlot.innerHTML = `<canvas width="40" height="40" id="wpn-canvas"></canvas><div style="position:absolute;top:2px;left:2px;font:9px monospace;color:#fff;">E</div>`;
    grid.appendChild(wpnSlot);
    setTimeout(() => {
      const c = document.getElementById('wpn-canvas');
      if (c) {
        const cx = c.getContext('2d');
        cx.fillStyle = this.player.currentWeapon.color;
        cx.fillRect(8, 18, 24, 4);
        cx.beginPath();
        cx.arc(32, 20, 4, 0, Math.PI*2);
        cx.fill();
      }
    }, 10);

    // Show bow if owned
    if (this.player.hasRanged) {
      const bowSlot = document.createElement('div');
      bowSlot.className = 'inv-slot';
      bowSlot.innerHTML = `<canvas width="40" height="40" id="bow-canvas"></canvas><div style="position:absolute;top:2px;left:2px;font:9px monospace;color:#fff;">R</div><div class="count">${this.player.arrows}</div>`;
      grid.appendChild(bowSlot);
      setTimeout(() => {
        const c = document.getElementById('bow-canvas');
        if (c) {
          const cx = c.getContext('2d');
          cx.strokeStyle = '#8f8';
          cx.lineWidth = 2;
          cx.beginPath();
          cx.arc(20, 20, 12, -0.8, 0.8);
          cx.stroke();
          cx.fillStyle = '#fff';
          cx.fillRect(20, 18, 15, 4);
        }
      }, 10);
    }

    // Show inventory items
    const itemCounts = {};
    for (const item of this.player.inventory) {
      itemCounts[item.name] = (itemCounts[item.name] || 0) + 1;
    }
    // Also add consumables from game state
    if (this.player.essences > 0) itemCounts['Dark Essence'] = this.player.essences;

    for (const [name, count] of Object.entries(itemCounts)) {
      const slot = document.createElement('div');
      slot.className = 'inv-slot';
      slot.innerHTML = `<div style="color:#fff;font:10px monospace;text-align:center;">${name.substring(0,6)}</div><div class="count">${count}</div>`;
      grid.appendChild(slot);
    }

    // Fill empty slots
    const totalSlots = 10;
    const usedSlots = grid.children.length;
    for (let i = usedSlots; i < totalSlots; i++) {
      const slot = document.createElement('div');
      slot.className = 'inv-slot';
      slot.style.opacity = '0.3';
      grid.appendChild(slot);
    }

    // Description
    document.getElementById('inv-desc').textContent =
      `Max HP: ${this.player.maxHp}/${this.player.targetMaxHp} | Speed: ${this.player.speed.toFixed(1)} | Attack: ${this.player.currentWeapon.dmg} | Gold: ${this.player.gold}`;
  }

  restart() {
    localStorage.removeItem('ruins_save');
    location.reload();
  }
}

// ===== MAIN LOOP =====
let game;

function init() {
  game = new Game();

  // Try to load save
  game.loadGame();

  // Restart handler
  document.addEventListener('keydown', e => {
    if (e.key.toLowerCase() === 'r') {
      if (game.gameOver || game.gameWon) game.restart();
    }
  });

  // Close shop on escape
  document.addEventListener('keydown', e => {
    if (e.key === 'Escape') {
      game.showShop = false;
      document.getElementById('shop-panel').style.display = 'none';
    }
  });

  // Start game loop
  requestAnimationFrame(gameLoop);
}

function gameLoop(timestamp) {
  if (!game.lastTime) game.lastTime = timestamp;
  const delta = timestamp - game.lastTime;
  game.lastTime = timestamp;

  // Fixed timestep
  game.accumulator += delta;
  while (game.accumulator >= game.fixedDt) {
    game.update(game.fixedDt);
    game.accumulator -= game.fixedDt;
  }

  // Render
  game.render();
  game.drawMinimap();
  game.renderInventoryUI();

  // Hide dialogue when timer expires
  if (game.dialogueTimer <= 0) {
    document.getElementById('dialogue-box').style.display = 'none';
  }

  requestAnimationFrame(gameLoop);
}

// Start
window.addEventListener('load', init);

</script>
</body>
</html>
<!-- agent-meta {"model":"meituan/longcat-2.0","provider":"openrouter","persona":"full","sessionId":"40e861dd-e999-4b82-becc-6ffc4e94fdc2","tokensIn":622719,"tokensOut":28751,"tokensTotal":651470,"turns":20,"toolCalls":19,"failedToolCalls":0,"timestamp":"2026-07-20T18:22:55.595Z"} -->