← RPG results

RPG

Deepseek v4 Pro · 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 56,170 bytes · SHA-256 36ee72b3747f
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Shadowgate Hollow — Action RPG</title>
<style>
  :root { --ui-bg: #1a1a2e; --ui-text: #e0d8c0; --gold: #d4a843; --red: #c0392b; --green: #27ae60; }
  * { margin:0; padding:0; box-sizing:border-box; }
  body { background:#0a0a14; display:flex; justify-content:center; align-items:center; height:100vh; overflow:hidden; font-family:'Georgia',serif; }
  #wrapper { position:relative; width:960px; height:640px; }
  canvas { display:block; border:3px solid #3a2a1a; border-radius:4px; image-rendering:pixelated; }
  #ui-layer { position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none; }
  #hud { position:absolute; top:12px; left:16px; display:flex; align-items:center; gap:6px; }
  .heart { width:28px; height:26px; position:relative; }
  .heart-full { color:#e74c3c; font-size:26px; line-height:1; text-shadow:0 0 6px #e74c3c; }
  .heart-half { color:#e74c3c; font-size:26px; line-height:1; text-shadow:0 0 6px #e74c3c; opacity:0.5; }
  .heart-empty { color:#555; font-size:26px; line-height:1; }
  #money-display { position:absolute; top:12px; right:16px; color:var(--gold); font-size:20px; font-weight:bold; text-shadow:0 0 8px rgba(212,168,67,0.6); background:rgba(0,0,0,0.5); padding:4px 12px; border-radius:4px; }
  #weapon-display { position:absolute; top:50px; right:16px; color:#ccc; font-size:13px; background:rgba(0,0,0,0.5); padding:4px 10px; border-radius:4px; }
  #message-box { position:absolute; bottom:140px; left:50%; transform:translateX(-50%); color:#fff; font-size:16px; text-align:center; background:rgba(0,0,0,0.7); padding:6px 20px; border-radius:6px; opacity:0; transition:opacity 0.3s; pointer-events:none; white-space:nowrap; }
  #inventory-panel { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:420px; background:var(--ui-bg); border:2px solid #5a4a2a; border-radius:8px; padding:16px; display:none; pointer-events:auto; color:var(--ui-text); }
  #inventory-panel.open { display:block; }
  #inventory-panel h3 { text-align:center; margin-bottom:10px; color:var(--gold); font-size:18px; }
  #inventory-panel .slot { display:flex; align-items:center; gap:10px; padding:8px; margin:4px 0; background:rgba(255,255,255,0.05); border-radius:4px; cursor:pointer; transition:background 0.2s; }
  #inventory-panel .slot:hover { background:rgba(255,255,255,0.12); }
  #inventory-panel .slot .icon { font-size:22px; width:32px; text-align:center; }
  #inventory-panel .slot .name { flex:1; font-size:14px; }
  #inventory-panel .slot .use { color:var(--green); font-size:11px; }
  #inventory-panel .close-btn { display:block; margin:12px auto 0; padding:6px 24px; background:#5a3a1a; color:#e0d8c0; border:1px solid #7a5a2a; border-radius:4px; cursor:pointer; font-family:inherit; font-size:14px; }
  #shop-panel { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:440px; background:var(--ui-bg); border:2px solid #5a4a2a; border-radius:8px; padding:16px; display:none; pointer-events:auto; color:var(--ui-text); }
  #shop-panel.open { display:block; }
  #shop-panel h3 { text-align:center; margin-bottom:10px; color:var(--gold); font-size:18px; }
  #shop-panel .shop-item { display:flex; align-items:center; gap:10px; padding:8px; margin:4px 0; background:rgba(255,255,255,0.05); border-radius:4px; }
  #shop-panel .shop-item button { padding:4px 14px; background:#5a3a1a; color:#e0d8c0; border:1px solid #7a5a2a; border-radius:4px; cursor:pointer; font-family:inherit; }
  #shop-panel .shop-item button:disabled { opacity:0.4; cursor:not-allowed; }
  #shop-panel .close-btn { display:block; margin:12px auto 0; padding:6px 24px; background:#5a3a1a; color:#e0d8c0; border:1px solid #7a5a2a; border-radius:4px; cursor:pointer; font-family:inherit; font-size:14px; }
  #controls-hint { position:absolute; bottom:8px; left:50%; transform:translateX(-50%); color:#666; font-size:11px; pointer-events:none; }
  #minimap { position:absolute; bottom:16px; left:16px; background:rgba(0,0,0,0.6); border:1px solid #333; border-radius:4px; padding:4px; pointer-events:none; }
  #death-screen { position:absolute; top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.8);display:none;flex-direction:column;align-items:center;justify-content:center;pointer-events:auto; }
  #death-screen.active { display:flex; }
  #death-screen h2 { color:#c0392b; font-size:36px; margin-bottom:16px; }
  #death-screen button { padding:10px 32px; background:#5a3a1a; color:#e0d8c0; border:2px solid #7a5a2a; border-radius:6px; cursor:pointer; font-family:inherit; font-size:18px; }
  #save-indicator { position:absolute; top:50%;left:50%;transform:translate(-50%,-50%);color:#5dade2;font-size:20px;opacity:0;transition:opacity 0.5s;pointer-events:none;text-shadow:0 0 10px #5dade2; }
</style>
</head>
<body>
<div id="wrapper">
  <canvas id="game" width="960" height="640"></canvas>
  <div id="ui-layer">
    <div id="hud"></div>
    <div id="money-display">🪙 0</div>
    <div id="weapon-display">⚔️ Rusty Sword</div>
    <div id="message-box"></div>
    <div id="inventory-panel"><h3>🎒 Inventory</h3><div id="inv-slots"></div><button class="close-btn" id="close-inv">Close [I]</button></div>
    <div id="shop-panel"><h3>🏪 Merchant's Wares</h3><div id="shop-items"></div><button class="close-btn" id="close-shop">Close [E]</button></div>
    <div id="controls-hint">WASD/Arrows: Move | Space: Attack | E: Interact | I: Inventory | 1-5: Use Item</div>
    <div id="minimap"><canvas id="minimap-canvas" width="100" height="80"></canvas></div>
    <div id="save-indicator">💾 Game Saved</div>
    <div id="death-screen"><h2>You Died</h2><button id="respawn-btn">Load Last Save</button></div>
  </div>
</div>
<script>
// ═══════════════════════════════════════════
// SHADOWGATE HOLLOW — Action RPG
// ═══════════════════════════════════════════

const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const W = 960, H = 640;
const TILE = 32;
const ROOM_COLS = 20, ROOM_ROWS = 14;
const MAP_W = ROOM_COLS * TILE;  // 640
const MAP_H = ROOM_ROWS * TILE;  // 448
const MAP_OX = (W - MAP_W) / 2;  // 160
const MAP_OY = 60;

// ── Sound Engine (Web Audio API) ──
let audioCtx = null;
function initAudio() {
  if (!audioCtx) { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); }
  if (audioCtx.state === 'suspended') audioCtx.resume();
}
function playSound(freq, type, duration, vol=0.12, slide=0) {
  if (!audioCtx) return;
  const t = audioCtx.currentTime;
  const osc = audioCtx.createOscillator();
  const gain = audioCtx.createGain();
  osc.type = type;
  osc.frequency.setValueAtTime(freq, t);
  if (slide) osc.frequency.linearRampToValueAtTime(freq + slide, t + duration);
  gain.gain.setValueAtTime(vol, t);
  gain.gain.exponentialRampToValueAtTime(0.001, t + duration);
  osc.connect(gain); gain.connect(audioCtx.destination);
  osc.start(t); osc.stop(t + duration);
}
function sfx(name) {
  switch(name) {
    case 'step': playSound(80, 'sine', 0.08, 0.03); break;
    case 'swing': playSound(200, 'sawtooth', 0.15, 0.08, -100); break;
    case 'hit': playSound(60, 'square', 0.2, 0.15, -30); break;
    case 'playerHit': playSound(40, 'sawtooth', 0.3, 0.2); break;
    case 'pickup': playSound(600, 'sine', 0.1, 0.1); playSound(800, 'sine', 0.1, 0.1, 0.08); break;
    case 'coin': playSound(900, 'triangle', 0.08, 0.08); playSound(1200, 'triangle', 0.06, 0.06, 0.04); break;
    case 'shop': playSound(500, 'sine', 0.12, 0.12); playSound(700, 'sine', 0.08, 0.1, 0.06); break;
    case 'save': playSound(300, 'sine', 0.15, 0.15); playSound(500, 'sine', 0.1, 0.2, 0.1); break;
    case 'bow': playSound(150, 'triangle', 0.2, 0.2, -80); playSound(400, 'sine', 0.06, 0.1, 0.05); break;
    case 'upgrade': playSound(400, 'sine', 0.1, 0.1); playSound(600, 'sine', 0.08, 0.1); playSound(900, 'sine', 0.06, 0.15, 0.08); break;
    case 'explore': playSound(200, 'sine', 0.08, 0.3, 200); break;
    case 'door': playSound(100, 'triangle', 0.2, 0.3, 50); break;
    case 'drink': playSound(300, 'sine', 0.08, 0.2); break;
  }
}

// ── Particle System ──
const particles = [];
class Particle {
  constructor(x, y, vx, vy, life, color, size=3) {
    this.x = x; this.y = y; this.vx = vx; this.vy = vy; this.life = life; this.maxLife = life; this.color = color; this.size = size;
  }
  update(dt) {
    this.x += this.vx * dt; this.y += this.vy * dt; this.life -= dt; this.size *= 0.995;
  }
  get alive() { return this.life > 0; }
  get alpha() { return Math.max(0, this.life / this.maxLife); }
}
function spawnParticles(x, y, count, color, speed=80, life=0.4, size=3) {
  for (let i = 0; i < count; i++) {
    const angle = Math.random() * Math.PI * 2;
    const spd = speed * (0.5 + Math.random());
    particles.push(new Particle(x, y, Math.cos(angle) * spd, Math.sin(angle) * spd, life * (0.5 + Math.random()), color, size));
  }
}

// ── Map Data ──
// Tile types: 0=floor, 1=wall, 2=doorU, 3=doorD, 4=doorL, 5=doorR, 6=water, 7=save, 8=shop, 9=chest, 10=exit
const T = { FLOOR:0, WALL:1, DOOR_U:2, DOOR_D:3, DOOR_L:4, DOOR_R:5, WATER:6, SAVE:7, SHOP:8, CHEST:9, EXIT:10 };

// Room definitions [roomId]: { map:2D array, enemies:[], pickups:[], chests:[], savePoint:bool, shop:bool }
const roomDefs = {};

function makeRoom(mapStr, opts={}) {
  const rows = mapStr.trim().split('\n');
  const map = rows.map(r => r.split('').map(c => parseInt(c)));
  return { map, enemies: opts.enemies || [], pickups: opts.pickups || [], chests: opts.chests || [], decorations: opts.decorations || [], roomType: opts.roomType || 'normal' };
}

// Room 0: Starting chamber (safe)
roomDefs['0,0'] = makeRoom(`
11111111111111111111
10000000000000000001
10000000000000000001
10000000070000000001
10000000000000000001
10000000000000000001
10000000000000000001
10000000000000000001
10000000000000000001
10000000000000000001
10000000000000000001
10000000000000000001
10000003000000000001
11111111111111111111
`, { roomType: 'start' });

// Room 1: Forest path (easy enemies, coins)
roomDefs['0,1'] = makeRoom(`
11111111111111111111
10000200000000000001
10000010000001000001
10000000000000000001
10010000000000001001
10000000000000000001
10000100000000010001
10000000000000000001
10000001000010000001
10000000000000000001
10000000000000000001
10000000000000000001
10000005000000000001
11111111111111111111
`, {
  enemies: [
    { type:'slime', x:5, y:3 },
    { type:'slime', x:14, y:4 },
    { type:'slime', x:8, y:8 },
  ],
  pickups: [
    { type:'coin', x:3, y:6, amount:5 },
    { type:'coin', x:16, y:3, amount:3 },
    { type:'coin', x:10, y:10, amount:8 },
  ],
  chests: [
    { x:17, y:9, item:'healthPotion', opened:false }
  ]
});

// Room 2: Dense thicket
roomDefs['1,1'] = makeRoom(`
11111111111111111111
10000000000000000001
10000100000001000001
10000000000000000001
10000001001000000001
10000000000000000001
10001000000000100001
10000000000000000001
10000001001000000001
10000000000000000001
10000000000000000001
10000004000050000001
10000002000000000001
11111111111111111111
`, {
  enemies: [
    { type:'slime', x:4, y:3 },
    { type:'slime', x:13, y:5 },
    { type:'skeleton', x:10, y:8 },
  ],
  pickups: [
    { type:'coin', x:7, y:4, amount:6 },
    { type:'coin', x:15, y:7, amount:4 },
  ],
  chests: [
    { x:3, y:10, item:'ironSword', opened:false }
  ]
});

// Room 3: Forest clearing (health upgrade)
roomDefs['1,0'] = makeRoom(`
11111111111111111111
10000000000000000001
10000000000000000001
10000000000000000001
10000000000000000001
10000000090000000001
10000000000000000001
10000000000000000001
10000000000000000001
10000000000000000001
10000000000000000001
10000003000000000001
10000000000000000001
11111111111111111111
`, {
  enemies: [
    { type:'skeleton', x:7, y:3 },
    { type:'skeleton', x:13, y:4 },
  ],
  pickups: [
    { type:'coin', x:5, y:8, amount:10 },
    { type:'coin', x:15, y:9, amount:10 },
  ],
  chests: [
    { x:9, y:5, item:'heartContainer', opened:false }
  ]
});

// Room 4: Cave entrance
roomDefs['2,0'] = makeRoom(`
11111111111111111111
10000000000000000001
10000000000000000001
11110011111111001111
10000000000000000001
10000000070000000001
10000000000000000051
10000000000000000001
11110000111100001111
10000000000000000001
10000000000000000001
10000003000000000001
10000002000000000001
11111111111111111111
`, {
  enemies: [
    { type:'skeleton', x:5, y:5 },
    { type:'skeleton', x:15, y:5 },
    { type:'slime', x:10, y:2 },
  ],
  pickups: [
    { type:'coin', x:3, y:9, amount:8 },
    { type:'coin', x:17, y:9, amount:8 },
  ]
});

// Room 5: Deep cavern (mid-game - ranged weapon!)
roomDefs['2,1'] = makeRoom(`
11111111111111111111
10000200000000000001
10000000000000000001
10001110000001110001
10001000000000100001
10000000000000000001
10000000090000000001
10000000000000000001
10001000000000100001
10001110000001110001
10000000000000000001
10000004000000000001
10000005000903000001
11111111111111111111
`, {
  enemies: [
    { type:'skeleton', x:4, y:5 },
    { type:'skeleton', x:16, y:5 },
    { type:'skeleton', x:10, y:3 },
    { type:'slime', x:8, y:9 },
    { type:'slime', x:12, y:9 },
  ],
  pickups: [
    { type:'coin', x:3, y:3, amount:12 },
    { type:'coin', x:17, y:3, amount:12 },
  ],
  chests: [
    { x:9, y:6, item:'huntersBow', opened:false, isRanged:true }
  ]
});

// Room 6: Underground lake
roomDefs['3,1'] = makeRoom(`
11111111111111111111
10000200000000000001
10000066666600000001
10000666666666000001
10000660000066000001
10000660000066000001
10000660000066000001
10000660000066000001
10000066666666000001
10000006666600000001
10000000000000000001
10000004000000000001
10000000000000000001
11111111111111111111
`, {
  enemies: [
    { type:'skeleton', x:3, y:3 },
    { type:'skeleton', x:8, y:6 },
    { type:'skeleton', x:17, y:3 },
    { type:'skeleton', x:14, y:8 },
  ],
  pickups: [
    { type:'coin', x:1, y:8, amount:15 },
    { type:'coin', x:18, y:8, amount:15 },
  ],
  chests: [
    { x:10, y:9, item:'magicBlade', opened:false }
  ]
});

// Room 7: Shop
roomDefs['3,0'] = makeRoom(`
11111111111111111111
10000000000000000001
10000000000000000001
10000000000000000001
10000000000000000001
10000000000000000001
10000008888000000001
10000008888000000001
10000008888000000001
10000000000000000001
10000000070000000001
10000004000000000001
10000003000000000001
11111111111111111111
`, {
  roomType: 'shop',
  enemies: [],
  pickups: [
    { type:'coin', x:3, y:2, amount:20 },
    { type:'coin', x:17, y:2, amount:20 },
  ]
});

// Room 8: Boss approach
roomDefs['2,2'] = makeRoom(`
11111111111111111111
10000000000000000001
10000000000000000001
10010000000000001001
10000000000000000001
10000100000000100001
10000000000000000001
10000001000100000001
10000000000000000001
10000000000000000001
10000000000000000001
10000002000000000001
10000005001000000001
11111111111111111111
`, {
  enemies: [
    { type:'skeleton', x:7, y:4 },
    { type:'skeleton', x:13, y:4 },
    { type:'skeleton', x:10, y:7 },
    { type:'skeleton', x:4, y:9 },
    { type:'skeleton', x:16, y:9 },
  ],
  pickups: [
    { type:'coin', x:10, y:5, amount:25 },
  ],
  chests: [
    { x:13, y:12, item:'elixir', opened:false }
  ]
});

// Room 9: Boss room
roomDefs['3,2'] = makeRoom(`
11111111111111111111
10000000000000000001
10000000000000000001
10000000000000000001
10000000000000000001
10000000000000000001
10000000000000000001
10000000000000000001
10000000000000000001
10000000000000000001
10000000000000000001
10000000000000000001
10040000000000000001
11111111111111111111
`, {
  enemies: [
    { type:'boss', x:10, y:6 },
  ],
  roomType: 'boss',
  chests: [
    { x:10, y:11, item:'heartContainer2', opened:false }
  ]
});

// World grid: which roomDef key at each world coordinate
const worldGrid = {
  '0,0': '0,0', '0,1': '0,1', '0,2': null,
  '1,0': '1,0', '1,1': '1,1', '1,2': null,
  '2,0': '2,0', '2,1': '2,1', '2,2': '2,2',
  '3,0': '3,0', '3,1': '3,1', '3,2': '3,2',
};

// ── Game State ──
const state = {
  playerX: 5 * TILE + TILE/2,
  playerY: 7 * TILE + TILE/2,
  worldX: 0, worldY: 0,
  hp: 3, maxHp: 3,
  weapon: 0, // 0=rusty sword, 1=iron sword, 2=magic blade
  hasRanged: false,
  money: 0,
  inventory: [], // [{id, name, icon, desc, usable, effect}]
  facing: 2, // 0=up,1=right,2=down,3=left
  attacking: false, attackTimer: 0, attackCooldown: 0,
  rangedTimer: 0, rangedCooldown: 0,
  invincible: 0,
  stepTimer: 0,
  moving: false,
  bossDefeated: false,
};

const roomState = {}; // per-room: { enemies:[], pickups:[], chests:[] }

function getRoomKey(wx, wy) { return `${wx},${wy}`; }
function getCurrentRoomDef() {
  const key = getRoomKey(state.worldX, state.worldY);
  const defKey = worldGrid[key];
  if (!defKey) return null;
  return roomDefs[defKey];
}
function getCurrentRoomState() {
  const key = getRoomKey(state.worldX, state.worldY);
  if (!roomState[key]) {
    const def = getCurrentRoomDef();
    if (!def) { roomState[key] = { enemies:[], pickups:[], chests:[] }; return roomState[key]; }
    roomState[key] = {
      enemies: def.enemies.map((e,i) => ({...e, id:i, hp: e.type==='boss'?12:(e.type==='skeleton'?5:3), alive:true})),
      pickups: def.pickups.map((p,i) => ({...p, collected:false})),
      chests: def.chests.map((c,i) => ({...c, opened:c.opened||false})),
    };
  }
  return roomState[key];
}

// ── Input ──
const keys = {};
window.addEventListener('keydown', e => {
  keys[e.key] = true;
  initAudio();
  if (e.key === 'i' || e.key === 'I') { toggleInventory(); e.preventDefault(); }
  if (e.key >= '1' && e.key <= '5') { useInventoryItem(parseInt(e.key) - 1); e.preventDefault(); }
  if (e.key === 'e' || e.key === 'E') { interact(); e.preventDefault(); }
  if (e.key === ' ') { e.preventDefault(); }
});
window.addEventListener('keyup', e => { keys[e.key] = false; });

// ── UI Elements ──
const hudEl = document.getElementById('hud');
const moneyDisplay = document.getElementById('money-display');
const weaponDisplay = document.getElementById('weapon-display');
const messageBox = document.getElementById('message-box');
const invPanel = document.getElementById('inventory-panel');
const invSlots = document.getElementById('inv-slots');
const shopPanel = document.getElementById('shop-panel');
const shopItems = document.getElementById('shop-items');
const saveIndicator = document.getElementById('save-indicator');
const deathScreen = document.getElementById('death-screen');
const minimapCanvas = document.getElementById('minimap-canvas');
const minimapCtx = minimapCanvas.getContext('2d');

let messageTimeout = null;
function showMessage(msg, duration=2500) {
  messageBox.textContent = msg;
  messageBox.style.opacity = '1';
  if (messageTimeout) clearTimeout(messageTimeout);
  messageTimeout = setTimeout(() => { messageBox.style.opacity = '0'; }, duration);
}

function updateHUD() {
  // Hearts
  let html = '';
  const fullHearts = Math.floor(state.hp / 2);
  const halfHeart = state.hp % 2;
  for (let i = 0; i < Math.floor(state.maxHp / 2); i++) {
    if (i < fullHearts) html += '<span class="heart-full">♥</span>';
    else if (i === fullHearts && halfHeart) html += '<span class="heart-half">♥</span>';
    else html += '<span class="heart-empty">♥</span>';
  }
  hudEl.innerHTML = html;
  moneyDisplay.textContent = `🪙 ${state.money}`;
  const weaponNames = ['Rusty Sword', 'Iron Blade', 'Arcane Edge'];
  const weaponIcons = ['🗡️', '⚔️', '✨'];
  let wText = `${weaponIcons[state.weapon]} ${weaponNames[state.weapon]}`;
  if (state.hasRanged) wText += '  🏹 Bow';
  weaponDisplay.textContent = wText;
}

function openShop() {
  shopPanel.classList.add('open');
  invPanel.classList.remove('open');
  renderShop();
}
function closeShop() {
  shopPanel.classList.remove('open');
}
function toggleInventory() {
  if (shopPanel.classList.contains('open')) { closeShop(); return; }
  invPanel.classList.toggle('open');
  if (invPanel.classList.contains('open')) renderInventory();
}
function renderInventory() {
  if (state.inventory.length === 0) {
    invSlots.innerHTML = '<div style="color:#888;text-align:center;padding:20px;">Your pack is empty.</div>';
  } else {
    invSlots.innerHTML = state.inventory.map((item, i) => `
      <div class="slot" onclick="useInventoryItem(${i})">
        <span class="icon">${item.icon}</span>
        <span class="name">${item.name}</span>
        ${item.usable ? `<span class="use">[${i+1}] Use</span>` : ''}
      </div>
    `).join('');
  }
}
document.getElementById('close-inv').addEventListener('click', () => invPanel.classList.remove('open'));
document.getElementById('close-shop').addEventListener('click', closeShop);

// ── Shop Items ──
const shopStock = [
  { id:'healthPotion', name:'Health Potion', icon:'🧪', desc:'Restore 2 HP', cost:15, usable:true, effect:()=>{ state.hp = Math.min(state.maxHp, state.hp+2); showMessage('Restored 2 HP!'); sfx('drink'); spawnParticles(state.playerX, state.playerY, 12, '#4ade80', 60, 0.5, 4); }},
  { id:'elixir', name:'Elixir of Power', icon:'⚗️', desc:'+1 damage for 60s', cost:25, usable:true, effect:()=>{ state.damageBoost=(state.damageBoost||0)+1; showMessage('Damage boosted!'); sfx('drink'); spawnParticles(state.playerX, state.playerY, 16, '#a78bfa', 50, 0.6, 5); }},
  { id:'ironSkin', name:'Iron Skin Tonic', icon:'🛡️', desc:'Reduce damage 60s', cost:30, usable:true, effect:()=>{ state.defenseBoost=(state.defenseBoost||0)+1; showMessage('Defense boosted!'); sfx('drink'); spawnParticles(state.playerX, state.playerY, 14, '#60a5fa', 50, 0.6, 5); }},
  { id:'bomb', name:'Bomb', icon:'💣', desc:'Damages all enemies in room', cost:20, usable:true, effect:()=>{ const rs = getCurrentRoomState(); rs.enemies.forEach(e=>{ if(e.alive){ e.hp-=3; if(e.hp<=0){ e.alive=false; spawnParticles(e.x*TILE+TILE/2, e.y*TILE+TILE/2, 20, '#fbbf24', 120, 0.5, 6); } } }); showMessage('BOOM!'); sfx('hit'); }},
  { id:'luckyCharm', name:'Lucky Charm', icon:'🍀', desc:'More coin drops (permanent)', cost:40, usable:false, effect:()=>{ state.luckyCharm=true; showMessage('Luck increased!'); sfx('upgrade'); }},
];

function renderShop() {
  shopItems.innerHTML = shopStock.map((item, i) => {
    const owned = state.inventory.some(inv => inv.id === item.id && !inv.usable);
    const disabled = owned || state.money < item.cost;
    return `<div class="shop-item">
      <span style="font-size:20px;">${item.icon}</span>
      <span style="flex:1;">${item.name} <small style="color:#aaa;">${item.desc}</small></span>
      <span style="color:var(--gold);">🪙${item.cost}</span>
      <button ${disabled?'disabled':''} onclick="buyItem(${i})">${owned?'Owned':'Buy'}</button>
    </div>`;
  }).join('');
}

function buyItem(index) {
  const item = shopStock[index];
  if (state.money < item.cost) return;
  const owned = state.inventory.some(inv => inv.id === item.id && !inv.usable);
  if (owned) return;
  state.money -= item.cost;
  state.inventory.push({...item});
  showMessage(`Bought ${item.name}!`, 2000);
  sfx('shop');
  spawnParticles(state.playerX, state.playerY, 10, '#d4a843', 40, 0.4, 3);
  renderShop();
  renderInventory();
  saveGame();
}

function useInventoryItem(index) {
  if (index < 0 || index >= state.inventory.length) return;
  const item = state.inventory[index];
  if (!item.usable) return;
  item.effect();
  state.inventory.splice(index, 1);
  renderInventory();
  updateHUD();
  saveGame();
}

// Expose to onclick
window.buyItem = buyItem;
window.useInventoryItem = useInventoryItem;

// ── Save System ──
function saveGame() {
  const data = {
    playerX: state.playerX, playerY: state.playerY,
    worldX: state.worldX, worldY: state.worldY,
    hp: state.hp, maxHp: state.maxHp,
    weapon: state.weapon, hasRanged: state.hasRanged,
    money: state.money, inventory: state.inventory,
    bossDefeated: state.bossDefeated,
    luckyCharm: state.luckyCharm||false,
    damageBoost: state.damageBoost||0, defenseBoost: state.defenseBoost||0,
    roomState: {},
  };
  // Save room states
  for (const [key, rs] of Object.entries(roomState)) {
    data.roomState[key] = {
      enemies: rs.enemies.map(e => ({...e})),
      pickups: rs.pickups.map(p => ({...p})),
      chests: rs.chests.map(c => ({...c})),
    };
  }
  localStorage.setItem('shadowgate_save', JSON.stringify(data));
  saveIndicator.style.opacity = '1';
  setTimeout(() => { saveIndicator.style.opacity = '0'; }, 1500);
}

function loadGame() {
  const raw = localStorage.getItem('shadowgate_save');
  if (!raw) return false;
  try {
    const data = JSON.parse(raw);
    state.playerX = data.playerX; state.playerY = data.playerY;
    state.worldX = data.worldX; state.worldY = data.worldY;
    state.hp = data.hp; state.maxHp = data.maxHp;
    state.weapon = data.weapon; state.hasRanged = data.hasRanged;
    state.money = data.money; state.inventory = data.inventory || [];
    state.bossDefeated = data.bossDefeated || false;
    state.luckyCharm = data.luckyCharm || false;
    state.damageBoost = data.damageBoost || 0;
    state.defenseBoost = data.defenseBoost || 0;
    if (data.roomState) {
      for (const [key, rs] of Object.entries(data.roomState)) {
        roomState[key] = rs;
      }
    }
    return true;
  } catch(e) { return false; }
}

document.getElementById('respawn-btn').addEventListener('click', () => {
  if (loadGame()) {
    state.hp = Math.max(1, state.hp);
    state.invincible = 1.5;
    deathScreen.classList.remove('active');
    showMessage('Loaded last save.', 2000);
  } else {
    resetGame();
    deathScreen.classList.remove('active');
  }
  updateHUD();
});

function resetGame() {
  state.playerX = 5 * TILE + TILE/2;
  state.playerY = 7 * TILE + TILE/2;
  state.worldX = 0; state.worldY = 0;
  state.hp = 3; state.maxHp = 3;
  state.weapon = 0; state.hasRanged = false;
  state.money = 0; state.inventory = [];
  state.bossDefeated = false;
  state.luckyCharm = false;
  state.damageBoost = 0; state.defenseBoost = 0;
  state.attacking = false; state.attackTimer = 0; state.attackCooldown = 0;
  state.rangedTimer = 0; state.rangedCooldown = 0;
  state.invincible = 0;
  for (const k of Object.keys(roomState)) delete roomState[k];
}

// ── Collision Detection ──
function getTile(wx, wy, tx, ty) {
  const def = getCurrentRoomDef();
  if (!def) return T.WALL;
  if (tx < 0 || tx >= ROOM_COLS || ty < 0 || ty >= ROOM_ROWS) return T.WALL;
  return def.map[ty][tx];
}

function isSolid(tile) {
  return tile === T.WALL || tile === T.WATER;
}

function canMoveTo(px, py, radius=10) {
  const left = Math.floor((px - radius) / TILE);
  const right = Math.floor((px + radius) / TILE);
  const top = Math.floor((py - radius) / TILE);
  const bottom = Math.floor((py + radius) / TILE);
  for (let ty = top; ty <= bottom; ty++) {
    for (let tx = left; tx <= right; tx++) {
      if (isSolid(getTile(state.worldX, state.worldY, tx, ty))) return false;
    }
  }
  return true;
}

function getDoorTile(px, py) {
  const tx = Math.floor(px / TILE);
  const ty = Math.floor(py / TILE);
  return getTile(state.worldX, state.worldY, tx, ty);
}

// ── Player Update ──
function updatePlayer(dt) {
  // Timers
  if (state.invincible > 0) state.invincible -= dt;
  if (state.attackTimer > 0) state.attackTimer -= dt;
  if (state.attackCooldown > 0) state.attackCooldown -= dt;
  if (state.rangedTimer > 0) state.rangedTimer -= dt;
  if (state.rangedCooldown > 0) state.rangedCooldown -= dt;
  if (state.stepTimer > 0) state.stepTimer -= dt;
  if (state.damageBoost > 0 && state._damageBoostTimer) { state._damageBoostTimer -= dt; if (state._damageBoostTimer <= 0) { state.damageBoost = Math.max(0, (state.damageBoost||0)-1); if (state.damageBoost>0) state._damageBoostTimer=60; } }
  if (state.defenseBoost > 0 && state._defenseBoostTimer) { state._defenseBoostTimer -= dt; if (state._defenseBoostTimer <= 0) { state.defenseBoost = Math.max(0, (state.defenseBoost||0)-1); if (state.defenseBoost>0) state._defenseBoostTimer=60; } }

  // Movement
  let dx = 0, dy = 0;
  if (keys['ArrowUp'] || keys['w'] || keys['W']) dy = -1;
  if (keys['ArrowDown'] || keys['s'] || keys['S']) dy = 1;
  if (keys['ArrowLeft'] || keys['a'] || keys['A']) dx = -1;
  if (keys['ArrowRight'] || keys['d'] || keys['D']) dx = 1;

  state.moving = (dx !== 0 || dy !== 0);
  if (state.moving) {
    if (dx !== 0 && dy !== 0) { dx *= 0.707; dy *= 0.707; }
    const speed = 160;
    const nx = state.playerX + dx * speed * dt;
    const ny = state.playerY + dy * speed * dt;
    if (canMoveTo(nx, state.playerY, 8)) state.playerX = nx;
    if (canMoveTo(state.playerX, ny, 8)) state.playerY = ny;
    // Facing
    if (Math.abs(dx) > Math.abs(dy)) state.facing = dx > 0 ? 1 : 3;
    else state.facing = dy > 0 ? 2 : 0;
    // Step sound
    if (state.stepTimer <= 0) { sfx('step'); state.stepTimer = 0.35; }
  }

  // Clamp to room
  state.playerX = Math.max(TILE/2, Math.min(MAP_W - TILE/2, state.playerX));
  state.playerY = Math.max(TILE/2, Math.min(MAP_H - TILE/2, state.playerY));

  // Attack
  if (keys[' '] && state.attackCooldown <= 0 && state.attackTimer <= 0) {
    state.attacking = true;
    state.attackTimer = 0.2;
    state.attackCooldown = state.weapon >= 2 ? 0.3 : (state.weapon >= 1 ? 0.4 : 0.5);
    sfx('swing');
    // Check melee hit
    const atkRange = state.weapon >= 2 ? 40 : (state.weapon >= 1 ? 34 : 28);
    const atkDmg = (state.weapon >= 2 ? 5 : (state.weapon >= 1 ? 3 : 2)) + (state.damageBoost||0);
    const facingAngle = state.facing * Math.PI/2;
    const atkCX = state.playerX + Math.cos(facingAngle) * (atkRange/2 + 8);
    const atkCY = state.playerY + Math.sin(facingAngle) * (atkRange/2 + 8);
    spawnParticles(atkCX, atkCY, 5, '#fff', 40, 0.2, 3);

    const rs = getCurrentRoomState();
    for (const enemy of rs.enemies) {
      if (!enemy.alive) continue;
      const ex = enemy.x * TILE + TILE/2;
      const ey = enemy.y * TILE + TILE/2;
      const edx = ex - state.playerX;
      const edy = ey - state.playerY;
      const dist = Math.sqrt(edx*edx + edy*edy);
      // Check if enemy is in front arc
      const dot = (Math.cos(facingAngle) * edx + Math.sin(facingAngle) * edy) / Math.max(0.1, dist);
      if (dist < atkRange && dot > 0.3) {
        enemy.hp -= atkDmg;
        spawnParticles(ex, ey, 8, '#ff6b6b', 80, 0.3, 4);
        sfx('hit');
        if (enemy.hp <= 0) {
          enemy.alive = false;
          spawnParticles(ex, ey, 16, '#fbbf24', 100, 0.5, 5);
          // Drop coins
          const coinDrop = enemy.type==='boss'?50:(enemy.type==='skeleton'?8:4);
          const bonus = state.luckyCharm ? Math.floor(coinDrop*0.5) : 0;
          state.money += coinDrop + bonus;
          showMessage(`+${coinDrop+bonus} coins`, 1200);
          sfx('coin');
          // Boss defeat
          if (enemy.type === 'boss') {
            state.bossDefeated = true;
            showMessage('The Shadow Lord is defeated!', 4000);
            sfx('upgrade');
          }
        }
      }
    }
  }

  // Ranged attack
  if ((keys['f'] || keys['F']) && state.hasRanged && state.rangedCooldown <= 0 && state.rangedTimer <= 0) {
    state.rangedTimer = 0.15;
    state.rangedCooldown = 0.7;
    sfx('bow');
    const arrowDmg = 3 + (state.damageBoost||0);
    const facingAngle = state.facing * Math.PI/2;
    const ax = state.playerX + Math.cos(facingAngle) * 20;
    const ay = state.playerY + Math.sin(facingAngle) * 20;
    spawnParticles(ax, ay, 6, '#87ceeb', 30, 0.3, 2);
    // Arrow projectile
    state.arrow = { x:ax, y:ay, vx:Math.cos(facingAngle)*300, vy:Math.sin(facingAngle)*300, life:0.8, dmg:arrowDmg };
  }

  // Arrow update
  if (state.arrow) {
    state.arrow.x += state.arrow.vx * dt;
    state.arrow.y += state.arrow.vy * dt;
    state.arrow.life -= dt;
    // Check wall collision
    const atx = Math.floor(state.arrow.x / TILE);
    const aty = Math.floor(state.arrow.y / TILE);
    if (isSolid(getTile(state.worldX, state.worldY, atx, aty)) || state.arrow.life <= 0) {
      spawnParticles(state.arrow.x, state.arrow.y, 4, '#87ceeb', 30, 0.2, 2);
      state.arrow = null;
    } else {
      // Check enemy hit
      const rs = getCurrentRoomState();
      for (const enemy of rs.enemies) {
        if (!enemy.alive) continue;
        const ex = enemy.x * TILE + TILE/2;
        const ey = enemy.y * TILE + TILE/2;
        const dist = Math.hypot(state.arrow.x - ex, state.arrow.y - ey);
        if (dist < 18) {
          enemy.hp -= state.arrow.dmg;
          spawnParticles(ex, ey, 8, '#87ceeb', 60, 0.3, 3);
          sfx('hit');
          if (enemy.hp <= 0) {
            enemy.alive = false;
            spawnParticles(ex, ey, 16, '#fbbf24', 100, 0.5, 5);
            const coinDrop = enemy.type==='boss'?50:(enemy.type==='skeleton'?8:4);
            const bonus = state.luckyCharm ? Math.floor(coinDrop*0.5) : 0;
            state.money += coinDrop + bonus;
            showMessage(`+${coinDrop+bonus} coins`, 1200);
            sfx('coin');
            if (enemy.type === 'boss') {
              state.bossDefeated = true;
              showMessage('The Shadow Lord is defeated!', 4000);
              sfx('upgrade');
            }
          }
          state.arrow = null;
          break;
        }
      }
    }
  }

  // Door transitions
  const tile = getDoorTile(state.playerX, state.playerY);
  if (tile === T.DOOR_U || tile === T.DOOR_D || tile === T.DOOR_L || tile === T.DOOR_R) {
    // Apply door transition (with cooldown)
    if (!state._doorCooldown) {
      state._doorCooldown = 0.6;
      let nwx = state.worldX, nwy = state.worldY;
      let npx = state.playerX, npy = state.playerY;
      if (tile === T.DOOR_U) { nwy--; npy = MAP_H - TILE*2; }
      else if (tile === T.DOOR_D) { nwy++; npy = TILE*2; }
      else if (tile === T.DOOR_L) { nwx--; npx = MAP_W - TILE*2; }
      else if (tile === T.DOOR_R) { nwx++; npx = TILE*2; }
      const nkey = getRoomKey(nwx, nwy);
      if (worldGrid[nkey]) {
        state.worldX = nwx; state.worldY = nwy;
        state.playerX = npx; state.playerY = npy;
        sfx('door');
        showMessage('Entering new area...', 1500);
        updateHUD();
      }
    }
  }
  if (state._doorCooldown) { state._doorCooldown -= dt; if (state._doorCooldown <= 0) state._doorCooldown = 0; }

  // Pickups
  const rs = getCurrentRoomState();
  for (const pickup of rs.pickups) {
    if (pickup.collected) continue;
    const px = pickup.x * TILE + TILE/2;
    const py = pickup.y * TILE + TILE/2;
    if (Math.hypot(state.playerX - px, state.playerY - py) < 20) {
      pickup.collected = true;
      if (pickup.type === 'coin') {
        state.money += pickup.amount;
        showMessage(`+${pickup.amount} coins`, 1200);
        sfx('coin');
        spawnParticles(px, py, 8, '#d4a843', 50, 0.3, 3);
      }
    }
  }
}

// ── Interact ──
function interact() {
  const px = state.playerX, py = state.playerY;
  // Check chests
  const rs = getCurrentRoomState();
  for (const chest of rs.chests) {
    if (chest.opened) continue;
    const cx = chest.x * TILE + TILE/2;
    const cy = chest.y * TILE + TILE/2;
    if (Math.hypot(px - cx, py - cy) < 36) {
      chest.opened = true;
      handleChestItem(chest);
      sfx('pickup');
      spawnParticles(cx, cy, 20, '#fbbf24', 80, 0.6, 5);
      return;
    }
  }
  // Check save point
  const tile = getDoorTile(px, py);
  const def = getCurrentRoomDef();
  if (def) {
    // Check save tile
    for (let ty = 0; ty < ROOM_ROWS; ty++) {
      for (let tx = 0; tx < ROOM_COLS; tx++) {
        if (def.map[ty][tx] === T.SAVE) {
          const sx = tx * TILE + TILE/2;
          const sy = ty * TILE + TILE/2;
          if (Math.hypot(px - sx, py - sy) < 32) {
            saveGame();
            sfx('save');
            showMessage('Game saved! 💾', 2000);
            spawnParticles(sx, sy, 14, '#5dade2', 60, 0.5, 4);
            return;
          }
        }
      }
    }
    // Check shop
    if (def.roomType === 'shop') {
      for (let ty = 0; ty < ROOM_ROWS; ty++) {
        for (let tx = 0; tx < ROOM_COLS; tx++) {
          if (def.map[ty][tx] === T.SHOP) {
            const sx = tx * TILE + TILE/2;
            const sy = ty * TILE + TILE/2;
            if (Math.hypot(px - sx, py - sy) < 48) {
              openShop();
              return;
            }
          }
        }
      }
    }
  }
}

function handleChestItem(chest) {
  switch(chest.item) {
    case 'healthPotion':
      state.inventory.push({id:'healthPotion', name:'Health Potion', icon:'🧪', desc:'Restore 2 HP', usable:true, effect:shopStock[0].effect});
      showMessage('Found: Health Potion!', 2000);
      break;
    case 'heartContainer':
      state.maxHp = Math.min(5, state.maxHp + 1);
      state.hp = Math.min(state.maxHp, state.hp + 2);
      showMessage('Heart Container! Max HP +1!', 2500);
      sfx('upgrade');
      break;
    case 'heartContainer2':
      state.maxHp = Math.min(5, state.maxHp + 1);
      state.hp = Math.min(state.maxHp, state.hp + 2);
      showMessage('Heart Container! Max HP +1!', 2500);
      sfx('upgrade');
      break;
    case 'ironSword':
      state.weapon = Math.max(1, state.weapon);
      showMessage('Iron Blade acquired! Damage + Range up!', 2500);
      sfx('upgrade');
      break;
    case 'magicBlade':
      state.weapon = Math.max(2, state.weapon);
      showMessage('Arcane Edge! Speed + Damage maxed!', 2500);
      sfx('upgrade');
      break;
    case 'huntersBow':
      state.hasRanged = true;
      showMessage('Hunter\'s Bow! Press F to fire arrows!', 2500);
      sfx('upgrade');
      break;
    case 'elixir':
      state.inventory.push({id:'elixir', name:'Elixir of Power', icon:'⚗️', desc:'+1 damage for 60s', usable:true, effect:shopStock[1].effect});
      showMessage('Found: Elixir of Power!', 2000);
      break;
  }
  updateHUD();
  saveGame();
}

// ── Enemy AI ──
function updateEnemies(dt) {
  const rs = getCurrentRoomState();
  for (const enemy of rs.enemies) {
    if (!enemy.alive) continue;
    const ex = enemy.x * TILE + TILE/2;
    const ey = enemy.y * TILE + TILE/2;
    const dx = state.playerX - ex;
    const dy = state.playerY - ey;
    const dist = Math.hypot(dx, dy);

    if (enemy.type === 'boss') {
      // Boss: chase but also strafe
      if (dist < 250 && state.invincible <= 0) {
        const speed = 55;
        const ndx = dx / Math.max(0.1, dist);
        const ndy = dy / Math.max(0.1, dist);
        const nx = ex + ndx * speed * dt;
        const ny = ey + ndy * speed * dt;
        if (canMoveTo(nx, ey, 10)) enemy.x = nx / TILE - 0.5;
        if (canMoveTo(ex, ny, 10)) enemy.y = ny / TILE - 0.5;
        // Boss attack on contact
        if (dist < 28) {
          damagePlayer(2);
          const knockX = state.playerX + ndx * 30;
          const knockY = state.playerY + ndy * 30;
          if (canMoveTo(knockX, state.playerY, 8)) state.playerX = knockX;
          if (canMoveTo(state.playerX, knockY, 8)) state.playerY = knockY;
        }
      }
      // Boss projectiles
      if (!enemy._shotTimer) enemy._shotTimer = 0;
      enemy._shotTimer -= dt;
      if (enemy._shotTimer <= 0 && dist < 300) {
        enemy._shotTimer = 1.5;
        // Spawn projectile toward player
        if (!state._bossProjectiles) state._bossProjectiles = [];
        const angle = Math.atan2(dy, dx);
        state._bossProjectiles.push({x:ex, y:ey, vx:Math.cos(angle)*120, vy:Math.sin(angle)*120, life:2.5});
      }
    } else if (enemy.type === 'skeleton') {
      if (dist < 200 && dist > 35) {
        const speed = 70;
        const ndx = dx / Math.max(0.1, dist);
        const ndy = dy / Math.max(0.1, dist);
        const nx = ex + ndx * speed * dt;
        const ny = ey + ndy * speed * dt;
        if (canMoveTo(nx, ey, 9)) enemy.x = nx / TILE - 0.5;
        if (canMoveTo(ex, ny, 9)) enemy.y = ny / TILE - 0.5;
      }
      if (dist < 28 && state.invincible <= 0) {
        damagePlayer(1);
      }
    } else if (enemy.type === 'slime') {
      if (dist < 180 && dist > 28) {
        const speed = 50;
        const ndx = dx / Math.max(0.1, dist);
        const ndy = dy / Math.max(0.1, dist);
        const nx = ex + ndx * speed * dt;
        const ny = ey + ndy * speed * dt;
        if (canMoveTo(nx, ey, 8)) enemy.x = nx / TILE - 0.5;
        if (canMoveTo(ex, ny, 8)) enemy.y = ny / TILE - 0.5;
      }
      if (dist < 24 && state.invincible <= 0) {
        damagePlayer(1);
      }
    }
  }

  // Boss projectiles
  if (state._bossProjectiles) {
    for (let i = state._bossProjectiles.length-1; i >= 0; i--) {
      const p = state._bossProjectiles[i];
      p.x += p.vx * dt;
      p.y += p.vy * dt;
      p.life -= dt;
      const ptx = Math.floor(p.x / TILE);
      const pty = Math.floor(p.y / TILE);
      if (isSolid(getTile(state.worldX, state.worldY, ptx, pty)) || p.life <= 0) {
        spawnParticles(p.x, p.y, 5, '#ff4444', 40, 0.2, 3);
        state._bossProjectiles.splice(i,1);
      } else if (state.invincible <= 0 && Math.hypot(p.x - state.playerX, p.y - state.playerY) < 16) {
        damagePlayer(1);
        spawnParticles(p.x, p.y, 8, '#ff4444', 60, 0.3, 4);
        state._bossProjectiles.splice(i,1);
      }
    }
  }
}

function damagePlayer(amount) {
  if (state.invincible > 0) return;
  const actualDmg = Math.max(1, amount - (state.defenseBoost||0));
  state.hp -= actualDmg;
  state.invincible = 0.8;
  sfx('playerHit');
  spawnParticles(state.playerX, state.playerY, 10, '#ff0000', 100, 0.4, 5);
  updateHUD();
  if (state.hp <= 0) {
    state.hp = 0;
    deathScreen.classList.add('active');
  }
}

// ── Render ──
function drawTile(tx, ty, screenX, screenY) {
  const tile = getTile(state.worldX, state.worldY, tx, ty);
  if (tile === T.WALL) {
    ctx.fillStyle = '#3a3028';
    ctx.fillRect(screenX, screenY, TILE, TILE);
    ctx.fillStyle = '#4a3a2a';
    ctx.fillRect(screenX+2, screenY+2, TILE-4, TILE-4);
    // Brick lines
    ctx.strokeStyle = '#2a1a0a';
    ctx.lineWidth = 1;
    ctx.strokeRect(screenX+1, screenY+1, TILE-2, TILE-2);
  } else if (tile === T.WATER) {
    ctx.fillStyle = '#1a4a6a';
    ctx.fillRect(screenX, screenY, TILE, TILE);
    ctx.fillStyle = '#1e5a7e';
    for (let i = 0; i < 3; i++) {
      ctx.fillRect(screenX+4+i*10, screenY+8+(i%2)*6, 6, 2);
    }
  } else if (tile === T.DOOR_U || tile === T.DOOR_D || tile === T.DOOR_L || tile === T.DOOR_R) {
    ctx.fillStyle = '#5a4a1a';
    ctx.fillRect(screenX, screenY, TILE, TILE);
    ctx.fillStyle = '#8a7a3a';
    ctx.fillRect(screenX+6, screenY+6, TILE-12, TILE-12);
    // Arrow indicator
    ctx.fillStyle = '#d4a843';
    ctx.beginPath();
    const cx = screenX + TILE/2, cy = screenY + TILE/2;
    if (tile === T.DOOR_U) { ctx.moveTo(cx,cy-8); ctx.lineTo(cx-6,cy+2); ctx.lineTo(cx+6,cy+2); }
    else if (tile === T.DOOR_D) { ctx.moveTo(cx,cy+8); ctx.lineTo(cx-6,cy-2); ctx.lineTo(cx+6,cy-2); }
    else if (tile === T.DOOR_L) { ctx.moveTo(cx-8,cy); ctx.lineTo(cx+2,cy-6); ctx.lineTo(cx+2,cy+6); }
    else { ctx.moveTo(cx+8,cy); ctx.lineTo(cx-2,cy-6); ctx.lineTo(cx-2,cy+6); }
    ctx.fill();
  } else if (tile === T.SAVE) {
    ctx.fillStyle = '#3a3028';
    ctx.fillRect(screenX, screenY, TILE, TILE);
    // Save statue
    ctx.fillStyle = '#7a8a9a';
    ctx.fillRect(screenX+10, screenY+4, 12, 24);
    ctx.fillStyle = '#5dade2';
    ctx.beginPath(); ctx.arc(screenX+16, screenY+4, 6, 0, Math.PI*2); ctx.fill();
    ctx.fillStyle = '#fff';
    ctx.font = '10px serif'; ctx.fillText('S', screenX+12, screenY+20);
  } else if (tile === T.SHOP) {
    ctx.fillStyle = '#5a3a1a';
    ctx.fillRect(screenX, screenY, TILE, TILE);
    ctx.fillStyle = '#8a6a3a';
    ctx.fillRect(screenX+2, screenY+2, TILE-4, TILE-16);
    ctx.fillStyle = '#d4a843';
    ctx.font = '18px serif'; ctx.fillText('🏪', screenX+6, screenY+20);
  } else if (tile === T.CHEST) {
    ctx.fillStyle = '#3a3028';
    ctx.fillRect(screenX, screenY, TILE, TILE);
    // Check if opened
    const rs = getCurrentRoomState();
    const chest = rs.chests.find(c => Math.floor(c.x) === tx && Math.floor(c.y) === ty);
    if (chest && chest.opened) {
      ctx.fillStyle = '#6a5a2a';
      ctx.fillRect(screenX+4, screenY+10, TILE-8, TILE-14);
      ctx.fillStyle = '#8a7a3a';
      ctx.fillRect(screenX+2, screenY+6, TILE-4, 6);
    } else {
      ctx.fillStyle = '#8a6a2a';
      ctx.fillRect(screenX+4, screenY+8, TILE-8, TILE-12);
      ctx.fillStyle = '#d4a843';
      ctx.fillRect(screenX+2, screenY+4, TILE-4, 8);
      ctx.fillStyle = '#fbbf24';
      ctx.beginPath(); ctx.arc(screenX+TILE/2, screenY+6, 4, 0, Math.PI*2); ctx.fill();
    }
  } else {
    // Floor
    ctx.fillStyle = '#2a2a1a';
    ctx.fillRect(screenX, screenY, TILE, TILE);
    // Subtle texture
    if ((tx + ty) % 2 === 0) {
      ctx.fillStyle = '#2d2d1d';
      ctx.fillRect(screenX, screenY, TILE, TILE);
    }
    ctx.fillStyle = '#252515';
    ctx.fillRect(screenX+1, screenY+1, 1, 1);
  }
}

function drawPlayer() {
  const px = MAP_OX + state.playerX;
  const py = MAP_OY + state.playerY;
  if (state.invincible > 0 && Math.floor(state.invincible * 20) % 2 === 0) return;

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

  // Body
  ctx.fillStyle = '#4a90d9';
  ctx.fillRect(px-7, py-10, 14, 18);

  // Equipment-dependent visuals
  // Sword on back/side
  const weaponColors = ['#888', '#aaa', '#88ccff'];
  const weaponSizes = [[16,3], [18,4], [20,5]];
  const [wLen, wWid] = weaponSizes[state.weapon];
  ctx.fillStyle = weaponColors[state.weapon];
  ctx.save();
  ctx.translate(px, py-2);
  // Sword held in facing direction
  const facingAngles = [-Math.PI/2, 0, Math.PI/2, Math.PI]; // up, right, down, left
  const angle = facingAngles[state.facing];
  if (state.attacking && state.attackTimer > 0.1) {
    const swingAngle = angle + (state.attackTimer - 0.1) * 10 * (state.facing % 2 === 0 ? 1 : -1);
    ctx.rotate(swingAngle);
    ctx.fillRect(6, -wWid/2, wLen, wWid);
    // Glow for magic blade
    if (state.weapon >= 2) {
      ctx.fillStyle = 'rgba(136,204,255,0.4)';
      ctx.fillRect(4, -wWid-1, wLen+4, wWid*3);
    }
  } else {
    ctx.rotate(angle + 0.3);
    ctx.fillRect(6, -wWid/2, wLen, wWid);
  }
  ctx.restore();

  // Bow on back if has ranged
  if (state.hasRanged) {
    ctx.strokeStyle = '#8B4513';
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.arc(px-10, py-4, 10, -Math.PI*0.7, Math.PI*0.7);
    ctx.stroke();
    ctx.strokeStyle = '#654321';
    ctx.lineWidth = 1;
    ctx.beginPath(); ctx.moveTo(px-10, py-14); ctx.lineTo(px-10, py+6); ctx.stroke();
  }

  // Head
  ctx.fillStyle = '#f4c89a';
  ctx.beginPath(); ctx.arc(px, py-12, 7, 0, Math.PI*2); ctx.fill();
  // Eyes
  ctx.fillStyle = '#222';
  const eyeOffX = state.facing === 1 ? 3 : (state.facing === 3 ? -3 : 0);
  const eyeOffY = state.facing === 2 ? 2 : (state.facing === 0 ? -2 : 0);
  ctx.beginPath(); ctx.arc(px+2+eyeOffX*0.3, py-13+eyeOffY*0.3, 2, 0, Math.PI*2); ctx.fill();

  // Legs
  ctx.fillStyle = '#3a3028';
  if (state.moving) {
    const legAnim = Math.sin(Date.now() * 0.01) * 3;
    ctx.fillRect(px-5, py+6, 4, 8+legAnim);
    ctx.fillRect(px+1, py+6, 4, 8-legAnim);
  } else {
    ctx.fillRect(px-5, py+6, 4, 8);
    ctx.fillRect(px+1, py+6, 4, 8);
  }
}

function drawEnemy(enemy) {
  const ex = MAP_OX + enemy.x * TILE + TILE/2;
  const ey = MAP_OY + enemy.y * TILE + TILE/2;

  if (enemy.type === 'slime') {
    ctx.fillStyle = '#4ade80';
    ctx.beginPath();
    ctx.arc(ex, ey+2, 11, 0, Math.PI*2);
    ctx.fill();
    ctx.fillStyle = '#22c55e';
    ctx.beginPath(); ctx.arc(ex, ey-2, 8, 0, Math.PI*2); ctx.fill();
    ctx.fillStyle = '#fff';
    ctx.beginPath(); ctx.arc(ex-3, ey-3, 2.5, 0, Math.PI*2); ctx.fill();
    ctx.beginPath(); ctx.arc(ex+3, ey-3, 2.5, 0, Math.PI*2); ctx.fill();
    ctx.fillStyle = '#000';
    ctx.beginPath(); ctx.arc(ex-3, ey-3, 1.2, 0, Math.PI*2); ctx.fill();
    ctx.beginPath(); ctx.arc(ex+3, ey-3, 1.2, 0, Math.PI*2); ctx.fill();
  } else if (enemy.type === 'skeleton') {
    ctx.fillStyle = '#ddd';
    ctx.fillRect(ex-6, ey-10, 12, 18);
    ctx.fillStyle = '#ccc';
    ctx.beginPath(); ctx.arc(ex, ey-12, 7, 0, Math.PI*2); ctx.fill();
    ctx.fillStyle = '#000';
    ctx.beginPath(); ctx.arc(ex-2, ey-13, 1.5, 0, Math.PI*2); ctx.fill();
    ctx.beginPath(); ctx.arc(ex+2, ey-13, 1.5, 0, Math.PI*2); ctx.fill();
    ctx.fillStyle = '#555';
    ctx.fillRect(ex-8, ey-4, 4, 10);
    ctx.fillRect(ex+4, ey-4, 4, 10);
  } else if (enemy.type === 'boss') {
    // Shadow Lord - larger, menacing
    const bob = Math.sin(Date.now() * 0.003) * 3;
    ctx.fillStyle = '#2a0a2a';
    ctx.beginPath(); ctx.arc(ex, ey-2+bob, 18, 0, Math.PI*2); ctx.fill();
    ctx.fillStyle = '#4a1a4a';
    ctx.beginPath(); ctx.arc(ex, ey-4+bob, 14, 0, Math.PI*2); ctx.fill();
    // Horns
    ctx.fillStyle = '#1a0a1a';
    ctx.beginPath(); ctx.moveTo(ex-8, ey-14+bob); ctx.lineTo(ex-4, ey-22+bob); ctx.lineTo(ex-2, ey-14+bob); ctx.fill();
    ctx.beginPath(); ctx.moveTo(ex+8, ey-14+bob); ctx.lineTo(ex+4, ey-22+bob); ctx.lineTo(ex+2, ey-14+bob); ctx.fill();
    // Eyes
    ctx.fillStyle = '#ff0000';
    ctx.beginPath(); ctx.arc(ex-5, ey-5+bob, 3, 0, Math.PI*2); ctx.fill();
    ctx.beginPath(); ctx.arc(ex+5, ey-5+bob, 3, 0, Math.PI*2); ctx.fill();
    ctx.fillStyle = '#ff6666';
    ctx.beginPath(); ctx.arc(ex-5, ey-5+bob, 1.5, 0, Math.PI*2); ctx.fill();
    ctx.beginPath(); ctx.arc(ex+5, ey-5+bob, 1.5, 0, Math.PI*2); ctx.fill();
    // Aura
    ctx.strokeStyle = 'rgba(255,0,0,0.2)';
    ctx.lineWidth = 3;
    ctx.beginPath(); ctx.arc(ex, ey+bob, 22, 0, Math.PI*2); ctx.stroke();
  }

  // HP bar for boss/skeletons
  if (enemy.type === 'boss' || enemy.type === 'skeleton') {
    const maxHp = enemy.type === 'boss' ? 12 : 5;
    const barW = enemy.type === 'boss' ? 30 : 20;
    ctx.fillStyle = '#333';
    ctx.fillRect(ex-barW/2, ey-20, barW, 4);
    ctx.fillStyle = enemy.type === 'boss' ? '#ff4444' : '#ff8844';
    ctx.fillRect(ex-barW/2, ey-20, barW * (enemy.hp/maxHp), 4);
  }
}

function render() {
  ctx.clearRect(0, 0, W, H);

  // Background
  ctx.fillStyle = '#0a0a10';
  ctx.fillRect(0, 0, W, H);

  // Room background
  ctx.fillStyle = '#1a1a0a';
  ctx.fillRect(MAP_OX, MAP_OY, MAP_W, MAP_H);

  const def = getCurrentRoomDef();
  if (!def) return;

  // Draw tiles
  for (let ty = 0; ty < ROOM_ROWS; ty++) {
    for (let tx = 0; tx < ROOM_COLS; tx++) {
      drawTile(tx, ty, MAP_OX + tx * TILE, MAP_OY + ty * TILE);
    }
  }

  // Draw pickups
  const rs = getCurrentRoomState();
  for (const pickup of rs.pickups) {
    if (pickup.collected) continue;
    const px = MAP_OX + pickup.x * TILE + TILE/2;
    const py = MAP_OY + pickup.y * TILE + TILE/2;
    const bob = Math.sin(Date.now() * 0.004) * 3;
    if (pickup.type === 'coin') {
      ctx.fillStyle = '#d4a843';
      ctx.beginPath(); ctx.arc(px, py+bob, 8, 0, Math.PI*2); ctx.fill();
      ctx.fillStyle = '#fbbf24';
      ctx.beginPath(); ctx.arc(px, py-2+bob, 5, 0, Math.PI*2); ctx.fill();
      ctx.fillStyle = '#fff';
      ctx.font = 'bold 10px serif'; ctx.fillText('$', px-4, py+3+bob);
    }
  }

  // Draw chests (unopened)
  for (const chest of rs.chests) {
    if (chest.opened) {
      const cx = MAP_OX + chest.x * TILE + TILE/2;
      const cy = MAP_OY + chest.y * TILE + TILE/2;
      ctx.fillStyle = '#6a5a2a';
      ctx.fillRect(cx-10, cy-4, 20, 14);
      ctx.fillStyle = '#4a3a1a';
      ctx.fillRect(cx-8, cy-6, 16, 4);
    }
  }

  // Draw enemies
  for (const enemy of rs.enemies) {
    if (!enemy.alive) continue;
    drawEnemy(enemy);
  }

  // Draw boss projectiles
  if (state._bossProjectiles) {
    for (const p of state._bossProjectiles) {
      ctx.fillStyle = '#ff4444';
      ctx.beginPath(); ctx.arc(MAP_OX + p.x, MAP_OY + p.y, 5, 0, Math.PI*2); ctx.fill();
      ctx.fillStyle = '#ff8888';
      ctx.beginPath(); ctx.arc(MAP_OX + p.x, MAP_OY + p.y, 3, 0, Math.PI*2); ctx.fill();
    }
  }

  // Draw arrow
  if (state.arrow) {
    ctx.fillStyle = '#87ceeb';
    ctx.beginPath(); ctx.arc(MAP_OX + state.arrow.x, MAP_OY + state.arrow.y, 3, 0, Math.PI*2); ctx.fill();
    ctx.strokeStyle = '#fff';
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(MAP_OX + state.arrow.x, MAP_OY + state.arrow.y);
    ctx.lineTo(MAP_OX + state.arrow.x - state.arrow.vx * 0.03, MAP_OY + state.arrow.y - state.arrow.vy * 0.03);
    ctx.stroke();
  }

  // Draw player
  drawPlayer();

  // Draw particles
  for (const p of particles) {
    ctx.fillStyle = p.color.replace(')', `,${p.alpha})`).replace('rgb', 'rgba');
    if (p.color.startsWith('#')) {
      const hex = p.color;
      const r = parseInt(hex.slice(1,3),16);
      const g = parseInt(hex.slice(3,5),16);
      const b = parseInt(hex.slice(5,7),16);
      ctx.fillStyle = `rgba(${r},${g},${b},${p.alpha})`;
    }
    ctx.beginPath(); ctx.arc(MAP_OX + p.x, MAP_OY + p.y, p.size, 0, Math.PI*2); ctx.fill();
  }

  // Room label
  ctx.fillStyle = 'rgba(255,255,255,0.4)';
  ctx.font = '11px Georgia';
  const roomNames = {
    '0,0':'Sanctuary', '0,1':'Forest Path', '1,0':'Forest Clearing',
    '1,1':'Dense Thicket', '2,0':'Cave Entrance', '2,1':'Deep Cavern',
    '3,0':'Underground Market', '3,1':'Underground Lake', '2,2':'Shadow Passage',
    '3,2':'Shadow Lord\'s Lair'
  };
  const rkey = getRoomKey(state.worldX, state.worldY);
  const rname = roomNames[rkey] || 'Unknown';
  ctx.fillText(rname, MAP_OX + 8, MAP_OY + MAP_H - 8);

  // Minimap
  drawMinimap();
}

function drawMinimap() {
  minimapCtx.clearRect(0, 0, 100, 80);
  minimapCtx.fillStyle = 'rgba(0,0,0,0.8)';
  minimapCtx.fillRect(0, 0, 100, 80);

  const cellW = 18, cellH = 16;
  const ox = 14, oy = 4;
  for (let wy = 0; wy < 4; wy++) {
    for (let wx = 0; wx < 3; wx++) {
      const key = getRoomKey(wx, wy);
      const exists = !!worldGrid[key];
      const isCurrent = wx === state.worldX && wy === state.worldY;
      const visited = roomState[key] !== undefined;

      if (exists) {
        minimapCtx.fillStyle = isCurrent ? '#d4a843' : (visited ? '#5a7a5a' : '#333');
        minimapCtx.fillRect(ox + wx * cellW, oy + wy * cellH, cellW-2, cellH-2);
        if (isCurrent) {
          minimapCtx.fillStyle = '#fff';
          minimapCtx.fillRect(ox + wx * cellW + 6, oy + wy * cellH + 4, 4, 4);
        }
      }
    }
  }

  // Legend
  minimapCtx.fillStyle = '#888';
  minimapCtx.font = '7px monospace';
  minimapCtx.fillText('gold=here', 4, 76);
}

// ── Game Loop ──
let lastTime = performance.now();
let gameTime = 0;

function gameLoop(timestamp) {
  let dt = (timestamp - lastTime) / 1000;
  if (dt > 0.1) dt = 0.1; // Cap to prevent jumps
  if (dt <= 0) dt = 0.016;
  lastTime = timestamp;
  gameTime += dt;

  if (!deathScreen.classList.contains('active') && !shopPanel.classList.contains('open') && !invPanel.classList.contains('open')) {
    updatePlayer(dt);
    updateEnemies(dt);
  }

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

  render();
  updateHUD();
  requestAnimationFrame(gameLoop);
}

// ── Initialize ──
function init() {
  // Try to load save
  if (!loadGame()) {
    resetGame();
  }
  updateHUD();
  invPanel.classList.remove('open');
  shopPanel.classList.remove('open');
  deathScreen.classList.remove('active');
  requestAnimationFrame(gameLoop);
}

// Click handler for audio init
document.addEventListener('click', initAudio, {once:true});
document.addEventListener('keydown', initAudio, {once:true});

init();
</script>
</body>
</html>
<!-- agent-meta {"model":"deepseek/deepseek-v4-pro","provider":"openrouter","persona":"full","sessionId":"989c3403-c768-4c88-9c28-66c165da40fb","tokensIn":1885855,"tokensOut":45255,"tokensTotal":1931110,"turns":32,"toolCalls":31,"failedToolCalls":1,"timestamp":"2026-07-18T19:43:24.310Z"} -->