← RPG results

RPG

Deepseek v4 Pro · composer

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 50,070 bytes · SHA-256 b4aa087cccf9
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>EmberQuest - Action RPG</title>
<style>
/* ── css: layout ── */
* { margin:0; padding:0; box-sizing:border-box; }
body { background:#111; overflow:hidden; font-family:'Segoe UI',system-ui,sans-serif; }
#gameCanvas { display:block; position:absolute; top:0; left:0; width:100%; height:100%; }
#ui-layer { position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none; }
#ui-layer > * { pointer-events:auto; }
#hud { position:absolute; top:0; left:0; right:0; padding:10px 16px; display:flex; align-items:center; gap:14px; z-index:10; }
#health-bar { display:flex; gap:3px; }
.heart { width:22px; height:22px; position:relative; }
.heart img, .heart svg { width:100%; height:100%; }
#weapon-display { background:rgba(0,0,0,0.7); color:#ffcc00; padding:5px 12px; border-radius:6px; font-size:13px; font-weight:bold; border:1px solid rgba(255,204,0,0.4); }
#coin-display { background:rgba(0,0,0,0.7); color:#ffd700; padding:5px 12px; border-radius:6px; font-size:13px; font-weight:bold; border:1px solid rgba(255,215,0,0.4); display:flex; align-items:center; gap:5px; }
#minimap { margin-left:auto; width:100px; height:70px; background:rgba(0,0,0,0.6); border:1px solid rgba(255,255,255,0.3); border-radius:4px; }
.overlay { position:absolute; inset:0; z-index:100; display:flex; align-items:center; justify-content:center; }
.overlay.hidden { display:none; }
.overlay-bg { position:absolute; inset:0; background:rgba(0,0,0,0.75); }
.overlay-content { position:relative; background:#1a1a2e; border:2px solid #c9a96e; border-radius:12px; padding:24px 28px; min-width:340px; max-width:500px; color:#eee; z-index:1; }
.overlay-content h2 { color:#ffcc00; margin-bottom:14px; font-size:20px; text-align:center; }
#inv-grid, #shop-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:8px; margin-bottom:12px; }
.inv-slot, .shop-slot { background:rgba(255,255,255,0.06); border:1px solid rgba(255,255,255,0.2); border-radius:8px; padding:10px 6px; text-align:center; cursor:pointer; transition:all 0.15s; font-size:12px; min-height:60px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:4px; }
.inv-slot:hover, .shop-slot:hover { background:rgba(255,255,255,0.14); border-color:#ffcc00; }
.inv-slot .item-icon { font-size:26px; }
.inv-slot .item-name { font-size:10px; color:#ccc; }
.shop-slot .item-price { font-size:10px; color:#ffd700; }
#inv-desc { background:rgba(0,0,0,0.3); padding:8px 12px; border-radius:6px; font-size:12px; color:#aaa; margin-bottom:10px; min-height:32px; }
#shop-coins { text-align:center; color:#ffd700; margin-bottom:10px; font-size:14px; }
.overlay-content button { display:block; margin:8px auto 0; padding:8px 24px; background:#c9a96e; color:#1a1a2e; border:none; border-radius:6px; font-weight:bold; cursor:pointer; font-size:14px; }
.overlay-content button:hover { background:#dfb97e; }
#dialog-box { position:absolute; bottom:60px; left:50%; transform:translateX(-50%); background:rgba(0,0,0,0.85); color:#fff; padding:14px 22px; border-radius:8px; border:1px solid rgba(255,255,255,0.3); z-index:90; max-width:500px; text-align:center; }
#dialog-box.hidden { display:none; }
#dialog-hint { display:block; font-size:11px; color:#888; margin-top:6px; }
#death-screen .overlay-content h2 { color:#e04040; }
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div id="ui-layer">
  <div id="hud">
    <div id="health-bar"></div>
    <div id="weapon-display"></div>
    <div id="coin-display"></div>
    <div id="minimap"></div>
  </div>
  <div id="inventory-screen" class="overlay hidden">
    <div class="overlay-bg"></div>
    <div class="overlay-content">
      <h2>Inventory</h2>
      <div id="inv-grid"></div>
      <div id="inv-desc"></div>
      <button id="inv-close">Close [I]</button>
    </div>
  </div>
  <div id="shop-screen" class="overlay hidden">
    <div class="overlay-bg"></div>
    <div class="overlay-content">
      <h2>Shop</h2>
      <div id="shop-grid"></div>
      <div id="shop-coins"></div>
      <button id="shop-close">Close [E]</button>
    </div>
  </div>
  <div id="save-screen" class="overlay hidden">
    <div class="overlay-bg"></div>
    <div class="overlay-content">
      <h2>Save Point</h2>
      <p id="save-message"></p>
      <button id="save-btn">Save Game</button>
      <button id="save-close">Close [E]</button>
    </div>
  </div>
  <div id="dialog-box" class="hidden">
    <p id="dialog-text"></p>
    <span id="dialog-hint">Press E to continue</span>
  </div>
  <div id="death-screen" class="overlay hidden">
    <div class="overlay-bg"></div>
    <h2>You Died</h2><p>Your journey has ended... for now.</p><button id="death-load">Load Last Save</button><button id="death-new">New Game</button>
  </div>
</div>
<script>
'use strict';

// ── const: TILE ──
// ── Game Configuration ──
const TILE = 32;

// ── const: MAP_W ──
const MAP_W = 60;

// ── const: MAP_H ──
const MAP_H = 40;

// ── const: COLORS ──
const COLORS = {
  grass: '#3a7d32', grass2: '#4a8d42', path: '#b8945c', wall: '#6b6b6b',
  water: '#2855a0', water2: '#3065b8', tree: '#1e5c1e', trunk: '#8b5a2b',
  roof: '#8b3a3a', house: '#c4946a', door: '#5a3020', stone: '#7a7a7a',
  cave: '#3a3a3a', bridge: '#a08060', chest: '#d4a040', savepoint: '#5ab0ff',
  shopMat: '#e8c860',
};

// ── const: WEAPONS ──
const WEAPONS = {
  sword:   { name:'Wooden Sword',   dmg:2, speed:380, range:36, color:'#c9b896', desc:'A simple wooden blade.' },
  iron:    { name:'Iron Broadsword',dmg:4, speed:500, range:44, color:'#b0b8c0', desc:'Heavier, hits harder.' },
  flame:   { name:'Flame Blade',    dmg:5, speed:380, range:40, color:'#ff6600', desc:'Wreathed in fire. Burns foes.' },
  bow:     { name:'Hunter Bow',     dmg:3, speed:600, range:200,color:'#8b6b3a', desc:'Fires arrows from afar.', projectile:true },
};

// ── const: ITEMS ──
const ITEMS = {
  potion:    { name:'Health Potion',  icon:'🧪', desc:'Restores 2 health points.', type:'consumable', price:15, effect:'heal', value:2 },
  shield:    { name:'Iron Shield',    icon:'🛡️', desc:'Blocks 1 hit when equipped.', type:'accessory', price:30, effect:'block' },
  boots:     { name:'Swift Boots',    icon:'👢', desc:'+15% movement speed.', type:'accessory', price:25, effect:'speed' },
  charm:     { name:'Lucky Charm',    icon:'🍀', desc:'Enemies drop more coins.', type:'accessory', price:20, effect:'luck' },
  ring:      { name:'Power Ring',     icon:'💍', desc:'+1 melee damage.', type:'accessory', price:35, effect:'power' },
  bomb:      { name:'Bomb',           icon:'💣', desc:'Damages nearby enemies.', type:'consumable', price:12, effect:'bomb', value:4 },
  hcontainer:{ name:'Heart Container',icon:'❤️', desc:'Permanently +2 max HP.', type:'consumable', price:0, effect:'maxhp', value:2 },
};

// ── const: ENEMIES ──
const ENEMIES = {
  slime:  { name:'Slime',  hp:3, dmg:1, speed:1.2, size:14, color:'#5dc95d', coins:[1,3], xp:10 },
  bat:    { name:'Bat',    hp:2, dmg:1, speed:2.5, size:10, color:'#8b5a8b', coins:[1,2], xp:8 },
  skel:   { name:'Skeleton',hp:6, dmg:2, speed:1.5, size:16,color:'#d4c8b8', coins:[2,5], xp:20 },
  golem:  { name:'Golem',  hp:10,dmg:3, speed:0.8, size:22,color:'#8b7355', coins:[4,8], xp:35 },
  boss:   { name:'Shadow Knight',hp:20,dmg:4,speed:1.8,size:24,color:'#3a1030',coins:[15,30],xp:100 },
};

// ── function: buildMap ──
// ── Tile Map (60x40) ── Legend: 0=grass 1=grass2 2=path 3=wall 4=water 5=tree 6=house 7=bridge 8=stone 9=cave
function buildMap() {
  const m = [];
  for (let y=0; y<MAP_H; y++) { m[y]=[]; for (let x=0; x<MAP_W; x++) m[y][x]=0; }
  // River
  for (let x=28; x<34; x++) for (let y=0; y<MAP_H; y++) m[y][x]=4;
  for (let y=15; y<26; y++) for (let x=28; x<34; x++) m[y][x]=0; // gap = bridge area
  // Bridge
  for (let x=29; x<33; x++) { m[19][x]=7; m[20][x]=7; m[21][x]=7; }
  // Paths
  function path(x1,y1,x2,y2) {
    let cx=x1, cy=y1;
    while (cx!==x2 || cy!==y2) {
      if (cx!==x2) cx+=Math.sign(x2-cx); else cy+=Math.sign(y2-cy);
      if (m[cy]&&m[cy][cx]===0) m[cy][cx]=2;
      // widen
      if (m[cy]&&m[cy][Math.min(cx+1,MAP_W-1)]===0) m[cy][Math.min(cx+1,MAP_W-1)]=2;
      if (m[cy]&&m[cy][Math.max(cx-1,0)]===0) m[cy][Math.max(cx-1,0)]=2;
    }
  }
  path(8,33,22,21); // village to bridge
  path(22,21,45,24); // bridge to castle area
  path(22,15,22,8);  // bridge north to cave
  path(8,33,8,30);   // village center
  // Village houses (left area)
  for (let dy=0; dy<4; dy++) for (let dx=0; dx<5; dx++) { m[30+dy][4+dx]=6; }
  for (let dy=0; dy<3; dy++) for (let dx=0; dx<4; dx++) { m[27+dy][12+dx]=6; }
  // Shop (special house)
  for (let dy=0; dy<4; dy++) for (let dx=0; dx<5; dx++) { m[30+dy][18+dx]=6; }
  // Trees (forest top-left)
  for (let i=0; i<180; i++) {
    const tx=2+Math.floor(Math.random()*26), ty=1+Math.floor(Math.random()*20);
    if (m[ty][tx]===0 && !(tx>=4&&tx<=20&&ty>=15&&ty<=22)) m[ty][tx]=5;
  }
  // Trees right side
  for (let i=0; i<60; i++) {
    const tx=35+Math.floor(Math.random()*22), ty=1+Math.floor(Math.random()*20);
    if (m[ty][tx]===0) m[ty][tx]=5;
  }
  // Castle walls
  for (let dy=0; dy<8; dy++) for (let dx=0; dx<8; dx++) {
    if (dy===0||dy===7||dx===0||dx===7) m[22+dy][45+dx]=3;
    else m[22+dy][45+dx]=8;
  }
  m[26][48]=2; m[26][49]=2; // castle entrance
  // Cave area
  for (let dy=0; dy<5; dy++) for (let dx=0; dx<5; dx++) { m[5+dy][18+dx]=9; }
  m[7][20]=2; // cave entrance path connect
  // Stone ruins near castle
  for (let dy=0; dy<3; dy++) for (let dx=0; dx<4; dx++) { m[28+dy][40+dx]=8; }
  // Scatter grass variants
  for (let y=0; y<MAP_H; y++) for (let x=0; x<MAP_W; x++) {
    if (m[y][x]===0 && Math.random()<0.3) m[y][x]=1;
  }
  return m;
}

// ── const: TILE_MAP ──
const TILE_MAP = buildMap();

// ── class: SoundEngine ──
// ── Sound Engine (Web Audio synthesis) ──
class SoundEngine {
  constructor() {
    this.ctx = null;
    this.enabled = true;
  }
  init() {
    try { this.ctx = new (window.AudioContext||window.webkitAudioContext)(); } catch(e) { this.enabled=false; }
  }
  play(freq, type, duration, vol=0.12, slide=0) {
    if (!this.enabled || !this.ctx) return;
    if (this.ctx.state==='suspended') this.ctx.resume();
    const t = this.ctx.currentTime;
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.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(this.ctx.destination);
    osc.start(t); osc.stop(t+duration);
  }
  sfx(name) {
    switch(name) {
      case 'step': this.play(80,'square',0.08,0.03); break;
      case 'swing': this.play(200,'sawtooth',0.15,0.06,-80); break;
      case 'hit': this.play(60,'square',0.18,0.1); break;
      case 'kill': this.play(300,'triangle',0.2,0.15,200); break;
      case 'pickup': this.play(600,'sine',0.12,0.1); setTimeout(()=>this.play(900,'sine',0.1,0.1),60); break;
      case 'coin': this.play(1000,'sine',0.08,0.08); break;
      case 'buy': this.play(500,'triangle',0.1,0.12,200); break;
      case 'save': this.play(400,'sine',0.15,0.2,300); break;
      case 'hurt': this.play(40,'sawtooth',0.25,0.2); break;
      case 'bow': this.play(150,'triangle',0.1,0.2,400); break;
      case 'bomb': this.play(25,'sawtooth',0.3,0.4,-20); break;
      case 'heal': this.play(400,'sine',0.1,0.15); setTimeout(()=>this.play(600,'sine',0.1,0.15),100); break;
      case 'upgrade': this.play(300,'triangle',0.15,0.3,500); break;
    }
  }
  footstep() { this.sfx('step'); }
}

// ── const: SFX ──
const SFX = new SoundEngine();

// ── class: Particle ──
// ── Particle System ──
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; }
  alive() { return this.life>0; }
  alpha() { return Math.max(0,this.life/this.maxLife); }
}

// ── class: ParticleSystem ──
class ParticleSystem {
  constructor() { this.particles=[]; }
  emit(x,y,count,color,spread=60,life=0.5,size=3) {
    for (let i=0; i<count; i++) {
      const angle=Math.random()*Math.PI*2, speed=spread*(0.5+Math.random());
      this.particles.push(new Particle(x,y,Math.cos(angle)*speed,Math.sin(angle)*speed,life*(0.5+Math.random()),color,size*(0.5+Math.random())));
    }
  }
  update(dt) { this.particles=this.particles.filter(p=>{p.update(dt);return p.alive();}); }
  render(ctx,cam) {
    for (const p of this.particles) {
      ctx.globalAlpha=p.alpha();
      ctx.fillStyle=p.color;
      ctx.fillRect(p.x-cam.x-p.size/2,p.y-cam.y-p.size/2,p.size,p.size);
    }
    ctx.globalAlpha=1;
  }
}

// ── const: PART ──
const PART = new ParticleSystem();

// ── class: Player ──
// ── Player ──
class Player {
  constructor() {
    this.x = 8*TILE + TILE/2;
    this.y = 31*TILE + TILE/2;
    this.w = 20; this.h = 20;
    this.vx = 0; this.vy = 0;
    this.speed = 150;
    this.facing = {x:0,y:1};
    this.hp = 3; this.maxHp = 3;
    this.weapon = 'sword';
    this._prevWeapon = 'sword';
    this.hasBow = false;
    this.inventory = [];
    this.coins = 0;
    this.accessories = [];
    this.attackCooldown = 0;
    this.invincible = 0;
    this.alive = true;
    this.animTimer = 0;
    this.footstepTimer = 0;
    this.attackFlash = 0;
    this.aimX = 0; this.aimY = 1;
  }
  get weaponData() { return WEAPONS[this.weapon]; }
  get attackDmg() {
    let d=this.weaponData.dmg;
    if (this.accessories.includes('ring')) d+=1;
    return d;
  }
  get moveSpeed() {
    let s=this.speed;
    if (this.accessories.includes('boots')) s*=1.15;
    return s;
  }
  get hasShield() { return this.accessories.includes('shield'); }
  get hasLucky() { return this.accessories.includes('charm'); }
  addItem(id, qty=1) {
    const existing = this.inventory.find(i=>i.id===id);
    if (existing) { existing.qty+=qty; } else { this.inventory.push({id,qty}); }
  }
  removeItem(id, qty=1) {
    const existing = this.inventory.find(i=>i.id===id);
    if (!existing) return false;
    existing.qty -= qty;
    if (existing.qty <= 0) this.inventory = this.inventory.filter(i=>i.id!==id);
    return true;
  }
  hasItem(id) { const e=this.inventory.find(i=>i.id===id); return e?e.qty:0; }
  useItem(id) {
    const item = ITEMS[id];
    if (!item) return false;
    if (item.type==='consumable') {
      if (!this.removeItem(id)) return false;
      if (item.effect==='heal') { this.hp=Math.min(this.maxHp,this.hp+item.value); SFX.sfx('heal'); }
      if (item.effect==='maxhp') { this.maxHp+=item.value; this.hp+=item.value; SFX.sfx('upgrade'); }
      if (item.effect==='bomb') { SFX.sfx('bomb'); return 'bomb'; }
      return true;
    }
    if (item.type==='accessory') {
      if (this.accessories.includes(id)) { this.accessories=this.accessories.filter(a=>a!==id); }
      else { this.accessories.push(id); }
      SFX.sfx('pickup');
      return true;
    }
    return false;
  }
  takeDamage(dmg) {
    if (this.invincible>0) return false;
    if (this.hasShield) { this.accessories=this.accessories.filter(a=>a!=='shield'); SFX.sfx('hit'); this.invincible=0.3; return false; }
    this.hp -= dmg;
    this.invincible = 0.5;
    SFX.sfx('hurt');
    PART.emit(this.x,this.y,8,'#ff4444',80,0.4,4);
    if (this.hp <= 0) { this.hp=0; this.alive=false; }
    return true;
  }
  update(dt, map, enemies) {
    if (!this.alive) return;
    this.attackCooldown = Math.max(0, this.attackCooldown-dt);
    this.invincible = Math.max(0, this.invincible-dt);
    this.attackFlash = Math.max(0, this.attackFlash-dt);
    let mx=0, my=0;
    if (KEYS['KeyW']||KEYS['ArrowUp']) my-=1;
    if (KEYS['KeyS']||KEYS['ArrowDown']) my+=1;
    if (KEYS['KeyA']||KEYS['ArrowLeft']) mx-=1;
    if (KEYS['KeyD']||KEYS['ArrowRight']) mx+=1;
    if (mx!==0||my!==0) {
      const len=Math.sqrt(mx*mx+my*my);
      mx/=len; my/=len;
      this.facing={x:mx,y:my};
      this.footstepTimer-=dt;
      if (this.footstepTimer<=0) { this.footstepTimer=0.35; SFX.footstep(); }
    }
    let nx=this.x+mx*this.moveSpeed*dt;
    let ny=this.y+my*this.moveSpeed*dt;
    if (!this.collidesTile(nx,this.y,map)) this.x=nx;
    if (!this.collidesTile(this.x,ny,map)) this.y=ny;
    this.x=Math.max(this.w/2,Math.min(MAP_W*TILE-this.w/2,this.x));
    this.y=Math.max(this.h/2,Math.min(MAP_H*TILE-this.h/2,this.y));
    this.animTimer+=dt;
  }
  collidesTile(px,py,map) {
    const cx = Math.floor(px/TILE), cy=Math.floor(py/TILE);
    for (let dy=-1;dy<=1;dy++) for (let dx=-1;dx<=1;dx++) {
      const tx=cx+dx, ty=cy+dy;
      if (tx<0||ty<0||tx>=MAP_W||ty>=MAP_H) return true;
      const t=map[ty][tx];
      if (t===3||t===4||t===5) return true;
    }
    return false;
  }
  render(ctx, cam) {
    if (!this.alive) return;
    const sx=this.x-cam.x, sy=this.y-cam.y;
    const flash = this.invincible>0 && Math.floor(this.invincible*20)%2===0;
    // Shadow
    ctx.fillStyle='rgba(0,0,0,0.3)'; ctx.beginPath(); ctx.ellipse(sx,sy+8,9,3,0,0,Math.PI*2); ctx.fill();
    // Body
    ctx.fillStyle = flash?'#ff8888':'#e8c890';
    ctx.beginPath();
    ctx.arc(sx, sy-2, 10, 0, Math.PI*2);
    ctx.fill();
    ctx.strokeStyle='#8b6b3a'; ctx.lineWidth=1.5; ctx.stroke();
    // Eyes - face movement direction
    ctx.fillStyle='#222';
    const ex=this.facing.x*3, ey=this.facing.y*3;
    ctx.beginPath(); ctx.arc(sx+ex-3, sy+ey-5, 2, 0, Math.PI*2); ctx.fill();
    ctx.beginPath(); ctx.arc(sx+ex+3, sy+ey-5, 2, 0, Math.PI*2); ctx.fill();
    // Weapon on back or in hand
    const angle = Math.atan2(this.facing.y, this.facing.x);
    if (this.attackFlash>0) {
      // Swing arc
      const swingAngle = angle + Math.sin(this.attackFlash*30)*1.2;
      const wx=sx+Math.cos(swingAngle)*18, wy=sy+Math.sin(swingAngle)*18;
      ctx.strokeStyle=this.weaponData.color;
      ctx.lineWidth=3; ctx.lineCap='round';
      ctx.beginPath(); ctx.moveTo(sx+Math.cos(angle)*5,sy+Math.sin(angle)*5); ctx.lineTo(wx,wy); ctx.stroke();
      // Arc trail
      ctx.strokeStyle='rgba(255,255,200,0.5)';
      ctx.lineWidth=1;
      ctx.beginPath(); ctx.arc(sx,sy,18,angle-0.8,angle+0.8); ctx.stroke();
    } else {
      // Small weapon held at side
      ctx.save();
      ctx.translate(sx-this.facing.x*2, sy+this.facing.y*4);
      ctx.rotate(angle+Math.PI/2);
      ctx.fillStyle=this.weaponData.color;
      ctx.fillRect(-2,-2,4,14);
      // Handle
      ctx.fillStyle='#8b5a2b'; ctx.fillRect(-2,10,4,5);
      ctx.restore();
    }
    // Bow on back if owned
    if (this.hasBow && this.weapon!=='bow') {
      ctx.strokeStyle='#8b6b3a'; ctx.lineWidth=2;
      ctx.beginPath(); ctx.arc(sx+this.facing.x*2, sy-this.facing.y*8, 8, angle-1.8, angle+0.3);
      ctx.stroke();
      // Bowstring
      ctx.strokeStyle='#ddd'; ctx.lineWidth=0.7;
      const bx=sx+this.facing.x*2, by=sy-this.facing.y*8;
      ctx.beginPath(); ctx.moveTo(bx+Math.cos(angle-1.8)*8, by+Math.sin(angle-1.8)*8);
      ctx.lineTo(bx+Math.cos(angle+0.3)*8, by+Math.sin(angle+0.3)*8); ctx.stroke();
    }
    // Drawn bow when equipped
    if (this.weapon==='bow') {
      ctx.save();
      ctx.translate(sx, sy-2);
      ctx.rotate(angle);
      ctx.strokeStyle='#8b6b3a'; ctx.lineWidth=2.5;
      ctx.beginPath(); ctx.arc(0,0,12, -1.2, 1.2); ctx.stroke();
      ctx.strokeStyle='#ddd'; ctx.lineWidth=0.8;
      ctx.beginPath(); ctx.moveTo(Math.cos(-1.2)*12,Math.sin(-1.2)*12); ctx.lineTo(Math.cos(1.2)*12,Math.sin(1.2)*12); ctx.stroke();
      ctx.fillStyle='#c94'; ctx.beginPath(); ctx.arc(Math.cos(0)*4,Math.sin(0)*4, 2.5,0,Math.PI*2); ctx.fill();
      ctx.restore();
    }
    // Shield visual
    if (this.hasShield) {
      ctx.fillStyle='#8899bb';
      ctx.beginPath(); ctx.arc(sx+this.facing.x*6+this.facing.y*5, sy+this.facing.y*3-this.facing.x*5, 7, 0, Math.PI*2); ctx.fill();
      ctx.strokeStyle='#667'; ctx.lineWidth=1.2; ctx.stroke();
    }
  }
  attack(enemies, targetX, targetY) {
    if (this.attackCooldown>0 || !this.alive) return null;
    const wd=this.weaponData;
    // Determine direction
    let dirX=this.facing.x, dirY=this.facing.y;
    if (targetX!==undefined && targetY!==undefined) {
      const ddx=targetX-this.x, ddy=targetY-this.y;
      const dlen=Math.sqrt(ddx*ddx+ddy*ddy);
      if (dlen>0.1) { dirX=ddx/dlen; dirY=ddy/dlen; }
    }
    if (wd.projectile) {
      this.attackCooldown=wd.speed/1000;
      SFX.sfx('bow');
      return {type:'projectile', x:this.x+dirX*16, y:this.y+dirY*16,
              vx:dirX*350, vy:dirY*350, dmg:this.attackDmg, life:wd.range/350, color:wd.color};
    }
    this.attackCooldown=wd.speed/1000;
    this.attackFlash=0.15;
    SFX.sfx('swing');
    const cx=this.x+dirX*wd.range*0.5, cy=this.y+dirY*wd.range*0.5;
    PART.emit(cx,cy,5,wd.color,40,0.2,3);
    const hits=[];
    for (const e of enemies) {
      if (!e.alive) continue;
      const dx=e.x-cx, dy=e.y-cy;
      if (Math.sqrt(dx*dx+dy*dy)<wd.range*0.6) hits.push(e);
    }
    return {type:'melee', hits, dmg:this.attackDmg};
  }
}

// ── class: Enemy ──
// ── Enemy ──
class Enemy {
  constructor(x, y, type) {
    const def = ENEMIES[type];
    this.x = x; this.y = y;
    this.type = type;
    this.name = def.name;
    this.hp = def.hp;
    this.maxHp = def.hp;
    this.dmg = def.dmg;
    this.speed = def.speed * (40 + Math.random()*20);
    this.size = def.size;
    this.color = def.color;
    this.coinRange = def.coins;
    this.xp = def.xp;
    this.alive = true;
    this.attackCooldown = 0;
    this.hitFlash = 0;
    this.wanderAngle = Math.random()*Math.PI*2;
    this.wanderTimer = 0;
    this.patrolX = x; this.patrolY = y;
    this.aggro = false;
    this.aggroRange = 150;
  }
  update(dt, player, map) {
    if (!this.alive) return;
    this.attackCooldown = Math.max(0, this.attackCooldown-dt);
    this.hitFlash = Math.max(0, this.hitFlash-dt);
    const dx = player.x - this.x, dy = player.y - this.y;
    const dist = Math.sqrt(dx*dx+dy*dy);
    this.aggro = dist < this.aggroRange && player.alive;
    let mx=0, my=0;
    if (this.aggro) {
      mx = dx/dist; my = dy/dist;
      if (dist < 28 && this.attackCooldown<=0) {
        this.attackCooldown = 1.2;
        player.takeDamage(this.dmg);
      }
    } else {
      this.wanderTimer -= dt;
      if (this.wanderTimer <= 0) {
        this.wanderTimer = 1+Math.random()*2;
        this.wanderAngle += (Math.random()-0.5)*Math.PI;
      }
      mx = Math.cos(this.wanderAngle);
      my = Math.sin(this.wanderAngle);
    }
    const nx = this.x + mx*this.speed*dt;
    const ny = this.y + my*this.speed*dt;
    if (!this.collidesTile(nx, this.y, map)) this.x = nx;
    else this.wanderAngle += Math.PI;
    if (!this.collidesTile(this.x, ny, map)) this.y = ny;
    else this.wanderAngle += Math.PI;
    this.x = Math.max(this.size, Math.min(MAP_W*TILE-this.size, this.x));
    this.y = Math.max(this.size, Math.min(MAP_H*TILE-this.size, this.y));
  }
  collidesTile(px, py, map) {
    const cx=Math.floor(px/TILE), cy=Math.floor(py/TILE);
    for (let dy=-1;dy<=1;dy++) for (let dx=-1;dx<=1;dx++) {
      const tx=cx+dx, ty=cy+dy;
      if (tx<0||ty<0||tx>=MAP_W||ty>=MAP_H) return true;
      const t=map[ty][tx];
      if (t===3||t===4||t===5) return true;
    }
    return false;
  }
  takeDamage(dmg) {
    this.hp -= dmg;
    this.hitFlash = 0.12;
    PART.emit(this.x, this.y, 4, '#ffffff', 30, 0.2, 2);
    if (this.hp <= 0) {
      this.alive = false;
      SFX.sfx('kill');
      const [min,max]=this.coinRange;
      const coins = min + Math.floor(Math.random()*(max-min+1));
      PART.emit(this.x, this.y, 10, '#ffd700', 50, 0.5, 3);
      return {coins, xp:this.xp};
    }
    SFX.sfx('hit');
    return null;
  }
  render(ctx, cam) {
    if (!this.alive) return;
    const sx=this.x-cam.x, sy=this.y-cam.y;
    const flash = this.hitFlash>0;
    ctx.fillStyle = flash ? '#ffffff' : this.color;
    if (this.type==='slime') {
      ctx.beginPath(); ctx.ellipse(sx,sy,this.size,this.size*0.7,0,0,Math.PI*2); ctx.fill();
      ctx.fillStyle='rgba(255,255,255,0.3)'; ctx.beginPath(); ctx.arc(sx-3,sy-2,3,0,Math.PI*2); ctx.fill();
    } else if (this.type==='bat') {
      ctx.beginPath(); ctx.arc(sx,sy,this.size,0,Math.PI*2); ctx.fill();
      ctx.fillStyle='#fff'; ctx.beginPath(); ctx.arc(sx-3,sy-3,2,0,Math.PI*2); ctx.fill();
    } else if (this.type==='skel') {
      ctx.fillRect(sx-6,sy-10,12,20);
      ctx.fillStyle='#fff'; ctx.beginPath(); ctx.arc(sx,sy-8,7,0,Math.PI*2); ctx.fill();
      ctx.fillStyle='#222'; ctx.beginPath(); ctx.arc(sx-2,sy-9,1.5,0,Math.PI*2); ctx.fill();
      ctx.beginPath(); ctx.arc(sx+2,sy-9,1.5,0,Math.PI*2); ctx.fill();
    } else if (this.type==='golem') {
      ctx.fillRect(sx-10,sy-12,20,24);
      ctx.fillStyle='#aaa'; ctx.beginPath(); ctx.arc(sx,sy-10,9,0,Math.PI*2); ctx.fill();
      ctx.fillStyle='#f44'; ctx.beginPath(); ctx.arc(sx-3,sy-12,2,0,Math.PI*2); ctx.fill();
      ctx.beginPath(); ctx.arc(sx+3,sy-12,2,0,Math.PI*2); ctx.fill();
    } else if (this.type==='boss') {
      ctx.fillRect(sx-12,sy-14,24,28);
      ctx.fillStyle='#5a2050'; ctx.beginPath(); ctx.arc(sx,sy-12,11,0,Math.PI*2); ctx.fill();
      ctx.fillStyle='#f00'; ctx.beginPath(); ctx.arc(sx-3,sy-14,2.5,0,Math.PI*2); ctx.fill();
      ctx.beginPath(); ctx.arc(sx+3,sy-14,2.5,0,Math.PI*2); ctx.fill();
    }
    // HP bar
    if (this.aggro || this.hp<this.maxHp) {
      const bw=this.size*2, bh=3;
      ctx.fillStyle='#400'; ctx.fillRect(sx-bw/2,sy-this.size-8,bw,bh);
      ctx.fillStyle='#f44'; ctx.fillRect(sx-bw/2,sy-this.size-8,bw*(this.hp/this.maxHp),bh);
    }
  }
}

// ── class: Projectile ──
// ── Projectile ──
class Projectile {
  constructor(x,y,vx,vy,dmg,life,color) {
    this.x=x; this.y=y; this.vx=vx; this.vy=vy; this.dmg=dmg; this.life=life; this.color=color; this.alive=true;
  }
  update(dt,map,enemies) {
    this.x+=this.vx*dt; this.y+=this.vy*dt; this.life-=dt;
    if (this.life<=0) { this.alive=false; return; }
    const cx=Math.floor(this.x/TILE),cy=Math.floor(this.y/TILE);
    if (cx<0||cy<0||cx>=MAP_W||cy>=MAP_H) { this.alive=false; return; }
    const t=map[cy][cx];
    if (t===3||t===4||t===5) { this.alive=false; PART.emit(this.x,this.y,4,'#ffcc00',30,0.2,2); return; }
    for (const e of enemies) {
      if (!e.alive) continue;
      const dx=e.x-this.x, dy=e.y-this.y;
      if (Math.sqrt(dx*dx+dy*dy)<e.size+4) {
        e.takeDamage(this.dmg);
        this.alive=false;
        PART.emit(this.x,this.y,5,'#ffcc00',40,0.2,2);
        return;
      }
    }
  }
  render(ctx,cam) {
    if (!this.alive) return;
    ctx.fillStyle=this.color;
    ctx.beginPath(); ctx.arc(this.x-cam.x,this.y-cam.y,3,0,Math.PI*2); ctx.fill();
  }
}

// ── class: WorldObject ──
// ── World Objects ──
class WorldObject {
  constructor(x,y,type,data) {
    this.x=x; this.y=y; this.type=type; this.data=data; this.interacted=false;
  }
}

// ── function: createWorldObjects ──
function createWorldObjects() {
  const objs=[];
  // Save points
  objs.push(new WorldObject(8*TILE,29*TILE,'save',{name:'Village Save'}));
  objs.push(new WorldObject(22*TILE,14*TILE,'save',{name:'Forest Edge Save'}));
  objs.push(new WorldObject(37*TILE,22*TILE,'save',{name:'Castle Approach Save'}));
  // Shop NPC
  objs.push(new WorldObject(15*TILE,30*TILE,'shop',{greeting:'Welcome traveler! Buy somethin?'}));
  // Chests
  objs.push(new WorldObject(14*TILE,24*TILE,'chest',{contents:[{id:'potion',qty:2},{id:'coins',qty:10}]}));
  objs.push(new WorldObject(10*TILE,8*TILE,'chest',{contents:[{id:'iron',qty:1},{id:'potion',qty:1}]}));  // Weapon upgrade 1
  objs.push(new WorldObject(19*TILE,10*TILE,'chest',{contents:[{id:'shield',qty:1},{id:'coins',qty:15}]}));
  objs.push(new WorldObject(22*TILE,6*TILE,'chest',{contents:[{id:'flame',qty:1},{id:'bow',qty:1},{id:'hcontainer',qty:1}]})); // Upgrade 2 + ranged + heart
  objs.push(new WorldObject(44*TILE,26*TILE,'chest',{contents:[{id:'bomb',qty:2},{id:'coins',qty:20}]}));
  objs.push(new WorldObject(48*TILE,23*TILE,'chest',{contents:[{id:'ring',qty:1},{id:'hcontainer',qty:1}]})); // final heart container
  objs.push(new WorldObject(40*TILE,28*TILE,'chest',{contents:[{id:'boots',qty:1},{id:'charm',qty:1}]}));
  return objs;
}

// ── class: Camera ──
// ── Camera ──
class Camera {
  constructor(canvas) { this.x=0; this.y=0; this.w=canvas.width; this.h=canvas.height; }
  follow(px,py,mapW,mapH) {
    this.x = px - this.w/2;
    this.y = py - this.h/2;
    this.x = Math.max(0, Math.min(mapW*TILE - this.w, this.x));
    this.y = Math.max(0, Math.min(mapH*TILE - this.h, this.y));
  }
}

// ── class: Game ──
// ── Game ──
class Game {
  constructor(canvas) {
    this.canvas = canvas;
    this.ctx = canvas.getContext('2d');
    this.cam = new Camera(canvas);
    this.resize();
    this.player = new Player();
    this.enemies = [];
    this.projectiles = [];
    this.worldObjects = createWorldObjects();
    this.chestsOpened = [];
    this.dialogText = null;
    this.dialogTimer = 0;
    this.showInventory = false;
    this.showShop = false;
    this.showSave = false;
    this.showDeath = false;
    this.currentSaveObj = null;
    this.lastTime = performance.now();
    this.enemyRespawnTimers = {};
    this.totalKills = 0;
    this.gameProgression = 0;
    this.keysDown = {};
    this.mouseWorld = {x:0,y:0};
    this.mouseCanvas = {x:0,y:0};
    this._mmCanvas = null;
    this._mmCtx = null;
    this._mmSetup = false;
  }
  resize() {
    this.canvas.width = window.innerWidth;
    this.canvas.height = window.innerHeight;
    this.cam.w = this.canvas.width;
    this.cam.h = this.canvas.height;
  }
  spawnEnemies() {
    const spawns = [
      {x:12*TILE,y:18*TILE,type:'slime'},{x:16*TILE,y:12*TILE,type:'slime'},{x:8*TILE,y:8*TILE,type:'slime'},
      {x:20*TILE,y:16*TILE,type:'bat'},{x:6*TILE,y:14*TILE,type:'bat'},{x:24*TILE,y:10*TILE,type:'slime'},
      {x:20*TILE,y:7*TILE,type:'skel'},{x:17*TILE,y:5*TILE,type:'skel'},{x:22*TILE,y:4*TILE,type:'bat'},
      {x:30*TILE,y:18*TILE,type:'slime'},{x:32*TILE,y:22*TILE,type:'bat'},
      {x:40*TILE,y:24*TILE,type:'skel'},{x:43*TILE,y:22*TILE,type:'golem'},{x:38*TILE,y:25*TILE,type:'skel'},
      {x:47*TILE,y:25*TILE,type:'golem'},
      {x:50*TILE,y:15*TILE,type:'slime'},{x:54*TILE,y:12*TILE,type:'bat'},
      {x:48*TILE,y:25*TILE,type:'boss'},
    ];
    for (const s of spawns) {
      this.enemies.push(new Enemy(s.x+TILE/2, s.y+TILE/2, s.type));
    }
  }
  getEnemyAt(wx,wy) {
    for (const e of this.enemies) {
      if (!e.alive) continue;
      const dx=e.x-wx, dy=e.y-wy;
      if (Math.sqrt(dx*dx+dy*dy)<e.size+6) return e;
    }
    return null;
  }
  getObjectAt(wx,wy) {
    for (const obj of this.worldObjects) {
      if (obj.interacted) continue;
      const dx=obj.x-wx, dy=obj.y-wy;
      if (Math.sqrt(dx*dx+dy*dy)<28) return obj;
    }
    return null;
  }
  openChest(obj) {
    if (obj.interacted) return;
    obj.interacted = true;
    this.chestsOpened.push(obj);
    SFX.sfx('pickup');
    const msgs = [];
    for (const item of obj.data.contents) {
      if (item.id==='coins') { this.player.coins+=item.qty; msgs.push(`+${item.qty} Gold`); continue; }
      if (item.id==='iron' || item.id==='flame') {
        this.player.weapon = item.id;
        this.player._prevWeapon = item.id;
        this.gameProgression = Math.max(this.gameProgression, item.id==='flame'?2:1);
        msgs.push(`Found: ${WEAPONS[item.id].name}!`);
        PART.emit(obj.x,obj.y,15,'#ffcc00',70,0.6,4);
        SFX.sfx('upgrade');
        continue;
      }
      if (item.id==='bow') {
        this.player.hasBow = true;
        this.gameProgression = 2;
        msgs.push('Found: Hunter Bow! (Press R to use)');
        PART.emit(obj.x,obj.y,15,'#ffaa00',70,0.6,4);
        SFX.sfx('upgrade');
        continue;
      }
      this.player.addItem(item.id, item.qty);
      msgs.push(`+${item.qty}x ${ITEMS[item.id].name}`);
    }
    this.showDialog(msgs.join(' | '));
  }
  interactWithShop() {
    this.showShop = true;
    SFX.sfx('coin');
    this.updateShopUI();
  }
  interactWithSave(obj) {
    this.currentSaveObj = obj;
    this.showSave = true;
    document.getElementById('save-message').textContent = `Save at: ${obj.data.name}`;
  }
  showDialog(text) {
    this.dialogText = text;
    this.dialogTimer = 4;
    document.getElementById('dialog-text').textContent = text;
    document.getElementById('dialog-box').classList.remove('hidden');
  }
  hideDialog() {
    this.dialogText = null;
    this.dialogTimer = 0;
    document.getElementById('dialog-box').classList.add('hidden');
  }
  saveGame() {
    const data = {
      x: this.player.x, y: this.player.y,
      hp: this.player.hp, maxHp: this.player.maxHp,
      weapon: this.player.weapon, hasBow: this.player.hasBow,
      coins: this.player.coins, inventory: this.player.inventory,
      accessories: this.player.accessories,
      chestsOpened: this.worldObjects.filter(o=>o.interacted&&o.type==='chest').map(o=>`${o.x},${o.y}`),
      progression: this.gameProgression, totalKills: this.totalKills,
    };
    localStorage.setItem('emberquest_save', JSON.stringify(data));
    SFX.sfx('save');
    this.showDialog('Game saved!');
  }
  loadGame() {
    try {
      const raw = localStorage.getItem('emberquest_save');
      if (!raw) return false;
      const data = JSON.parse(raw);
      if (data.hp <= 0) { localStorage.removeItem('emberquest_save'); return false; }
      this.player.x = data.x; this.player.y = data.y;
      this.player.hp = data.hp; this.player.maxHp = data.maxHp;
      this.player.weapon = data.weapon; this.player._prevWeapon = data.weapon;
      this.player.hasBow = !!data.hasBow;
      this.player.coins = data.coins||0; this.player.inventory = data.inventory||[];
      this.player.accessories = data.accessories||[];
      this.gameProgression = data.progression||0;
      this.totalKills = data.totalKills||0;
      this.player.alive = true;
      const openedSet = new Set(data.chestsOpened||[]);
      for (const obj of this.worldObjects) {
        if (obj.type==='chest' && openedSet.has(`${obj.x},${obj.y}`)) obj.interacted=true;
      }
      this.showDeath = false;
      this.showDialog('Game loaded!');
      return true;
    } catch(e) { localStorage.removeItem('emberquest_save'); return false; }
  }
  updateShopUI() {
    const grid = document.getElementById('shop-grid');
    const shopItems = ['potion','shield','boots','charm','ring','bomb'];
    grid.innerHTML = shopItems.map(id=>{
      const item=ITEMS[id];
      return `<div class="shop-slot" data-id="${id}"><span class="item-icon">${item.icon}</span><span class="item-name">${item.name}</span><span class="item-price">${item.price}g</span></div>`;
    }).join('');
    document.getElementById('shop-coins').textContent = `Your Gold: ${this.player.coins}`;
    grid.querySelectorAll('.shop-slot').forEach(el=>{
      el.addEventListener('click',()=>this.buyItem(el.dataset.id));
    });
  }
  buyItem(id) {
    const item=ITEMS[id];
    if (this.player.coins < item.price) { this.showDialog('Not enough gold!'); return; }
    this.player.coins -= item.price;
    this.player.addItem(id);
    SFX.sfx('buy');
    this.showDialog(`Bought ${item.name}!`);
    this.updateShopUI();
    this.updateHUD();
  }
  updateHUD() {
    const hb = document.getElementById('health-bar');
    let html = '';
    for (let i=0; i<Math.floor(this.player.maxHp/2); i++) {
      const fill = Math.min(2, Math.max(0, this.player.hp - i*2));
      html += `<svg class="heart" viewBox="0 0 24 24"><defs><linearGradient id="hg${i}" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#e04040"/><stop offset="100%" stop-color="#333"/></linearGradient></defs><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" fill="${fill>=2?'#e04040':(fill>=1?`url(#hg${i})`:'#333')}" stroke="#882020" stroke-width="1"/></svg>`;
    }
    if (this.player.maxHp % 2 === 1) {
      const lastHp = this.player.hp - Math.floor(this.player.hp/2)*2;
      html += `<svg class="heart" viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" fill="${lastHp>=1?'#e04040':'#333'}" stroke="#882020" stroke-width="1"/></svg>`;
    }
    hb.innerHTML = html;
    document.getElementById('weapon-display').textContent = this.player.weaponData.name;
    document.getElementById('coin-display').textContent = `🪙 ${this.player.coins}`;
  }
  updateInventoryUI() {
    const grid = document.getElementById('inv-grid');
    const items = this.player.inventory;
    grid.innerHTML = items.map(inv=>{
      const item=ITEMS[inv.id];
      return `<div class="inv-slot" data-id="${inv.id}"><span class="item-icon">${item.icon}</span><span class="item-name">${item.name} x${inv.qty}</span></div>`;
    }).join('');
    if (items.length===0) grid.innerHTML='<div style="grid-column:1/-1;text-align:center;color:#888;">Empty</div>';
    grid.querySelectorAll('.inv-slot').forEach(el=>{
      el.addEventListener('click',()=>{
        const id=el.dataset.id;
        const item=ITEMS[id];
        if (item.type==='consumable') {
          const result=this.player.useItem(id);
          if (result==='bomb') {
            PART.emit(this.player.x,this.player.y,20,'#ff8800',100,0.5,5);
            for (const e of this.enemies) {
              if (!e.alive) continue;
              const dx=e.x-this.player.x, dy=e.y-this.player.y;
              if (Math.sqrt(dx*dx+dy*dy)<80) e.takeDamage(4);
            }
          }
        } else if (item.type==='accessory') {
          this.player.useItem(id);
        }
        this.updateInventoryUI();
        this.updateHUD();
      });
    });
    document.getElementById('inv-desc').textContent = 'Click an item to use/equip it.';
  }
  renderMap() {
    const ctx=this.ctx, cam=this.cam;
    const startTX = Math.floor(cam.x/TILE)-1;
    const startTY = Math.floor(cam.y/TILE)-1;
    const endTX = Math.ceil((cam.x+cam.w)/TILE)+1;
    const endTY = Math.ceil((cam.y+cam.h)/TILE)+1;
    for (let ty=startTY; ty<=endTY; ty++) {
      for (let tx=startTX; tx<=endTX; tx++) {
        if (tx<0||ty<0||tx>=MAP_W||ty>=MAP_H) continue;
        const t=TILE_MAP[ty][tx];
        const sx=tx*TILE-cam.x, sy=ty*TILE-cam.y;
        if (t===0||t===1) { ctx.fillStyle=t===0?COLORS.grass:COLORS.grass2; ctx.fillRect(sx,sy,TILE,TILE); }
        else if (t===2) { ctx.fillStyle=COLORS.path; ctx.fillRect(sx,sy,TILE,TILE); }
        else if (t===3) { ctx.fillStyle=COLORS.wall; ctx.fillRect(sx,sy,TILE,TILE); ctx.strokeStyle='#555';ctx.lineWidth=0.5;ctx.strokeRect(sx,sy,TILE,TILE); }
        else if (t===4) { ctx.fillStyle=COLORS.water; ctx.fillRect(sx,sy,TILE,TILE); }
        else if (t===5) { ctx.fillStyle=COLORS.grass; ctx.fillRect(sx,sy,TILE,TILE); ctx.fillStyle=COLORS.tree; ctx.beginPath();ctx.arc(sx+TILE/2,sy+TILE/2,TILE/2-2,0,Math.PI*2);ctx.fill(); ctx.fillStyle=COLORS.trunk;ctx.fillRect(sx+TILE/2-2,sy+TILE/2,4,TILE/2); }
        else if (t===6) { ctx.fillStyle=COLORS.house; ctx.fillRect(sx,sy,TILE,TILE); }
        else if (t===7) { ctx.fillStyle=COLORS.bridge; ctx.fillRect(sx,sy,TILE,TILE); ctx.strokeStyle='#8a7050';ctx.strokeRect(sx,sy,TILE,TILE); }
        else if (t===8) { ctx.fillStyle=COLORS.stone; ctx.fillRect(sx,sy,TILE,TILE); }
        else if (t===9) { ctx.fillStyle=COLORS.cave; ctx.fillRect(sx,sy,TILE,TILE); }
      }
    }
  }
  renderWorldObjects() {
    const ctx=this.ctx, cam=this.cam;
    for (const obj of this.worldObjects) {
      const sx=obj.x-cam.x, sy=obj.y-cam.y;
      if (sx<-20||sy<-20||sx>cam.w+20||sy>cam.h+20) continue;
      if (obj.type==='save') {
        ctx.fillStyle=COLORS.savepoint;
        ctx.beginPath(); ctx.arc(sx,sy,10,0,Math.PI*2); ctx.fill();
        ctx.fillStyle='#fff'; ctx.font='10px sans-serif'; ctx.textAlign='center';
        ctx.fillText('💾',sx,sy+4);
      } else if (obj.type==='chest') {
        const color = obj.interacted ? '#886630' : COLORS.chest;
        ctx.fillStyle=color;
        ctx.fillRect(sx-8,sy-6,16,14);
        ctx.fillStyle=obj.interacted?'#665020':'#b89030';
        ctx.fillRect(sx-7,sy-4,14,5);
        ctx.fillStyle='#ffd700'; ctx.fillRect(sx-2,sy-3,4,3);
      } else if (obj.type==='shop') {
        ctx.fillStyle=COLORS.shopMat;
        ctx.beginPath(); ctx.arc(sx,sy,12,0,Math.PI*2); ctx.fill();
        ctx.fillStyle='#000'; ctx.font='14px sans-serif'; ctx.textAlign='center';
        ctx.fillText('💰',sx,sy+5);
      }
    }
  }
  renderMinimap() {
    if (!this._mmSetup) {
      const mm = document.getElementById('minimap');
      if (!mm) return;
      const canvas = document.createElement('canvas');
      canvas.width=100; canvas.height=70;
      canvas.style.width='100px'; canvas.style.height='70px';
      mm.innerHTML='';
      mm.appendChild(canvas);
      this._mmCanvas = canvas;
      this._mmCtx = canvas.getContext('2d');
      this._mmSetup = true;
    }
    const ctx = this._mmCtx;
    if (!ctx) return;
    ctx.fillStyle='#1a3a1a'; ctx.fillRect(0,0,100,70);
    const sx=100/MAP_W, sy=70/MAP_H;
    for (let y=0; y<MAP_H; y++) for (let x=0; x<MAP_W; x++) {
      const t=TILE_MAP[y][x];
      if (t===2) { ctx.fillStyle='#b8945c'; ctx.fillRect(x*sx,y*sy,sx,sy); }
      else if (t===3) { ctx.fillStyle='#666'; ctx.fillRect(x*sx,y*sy,sx,sy); }
      else if (t===4) { ctx.fillStyle='#2855a0'; ctx.fillRect(x*sx,y*sy,sx,sy); }
    }
    ctx.fillStyle='#ff4444';
    ctx.fillRect(this.player.x/TILE*sx-1, this.player.y/TILE*sy-1, 2, 2);
  }
  update(timestamp) {
    const dt = Math.min((timestamp-this.lastTime)/1000, 0.1);
    this.lastTime = timestamp;
    if (this.dialogTimer>0) {
      this.dialogTimer-=dt;
      if (this.dialogTimer<=0) this.hideDialog();
    }
    this.player.update(dt, TILE_MAP, this.enemies);
    for (const e of this.enemies) e.update(dt, this.player, TILE_MAP);
    for (const p of this.projectiles) p.update(dt, TILE_MAP, this.enemies);
    this.projectiles = this.projectiles.filter(p=>p.alive);
    PART.update(dt);
    for (const e of this.enemies) {
      if (!e.alive && e.type!=='boss') {
        const key=`${e.type}_${Math.floor(e.x/100)}_${Math.floor(e.y/100)}`;
        if (!this.enemyRespawnTimers[key]) this.enemyRespawnTimers[key]=0;
        this.enemyRespawnTimers[key]+=dt;
        if (this.enemyRespawnTimers[key]>25) {
          const def=ENEMIES[e.type];
          e.hp=def.hp; e.alive=true; e.x=e.patrolX; e.y=e.patrolY;
          this.enemyRespawnTimers[key]=0;
        }
      }
    }
    this.cam.follow(this.player.x, this.player.y, MAP_W, MAP_H);
  }
  render() {
    const ctx=this.ctx;
    ctx.clearRect(0,0,this.canvas.width,this.canvas.height);
    this.renderMap();
    this.renderWorldObjects();
    for (const p of this.projectiles) p.render(ctx, this.cam);
    for (const e of this.enemies) e.render(ctx, this.cam);
    this.player.render(ctx, this.cam);
    PART.render(ctx, this.cam);
    const nearObj = this.getObjectAt(this.player.x, this.player.y);
    if (nearObj && !this.dialogText) {
      const sx=nearObj.x-this.cam.x, sy=nearObj.y-this.cam.y-20;
      ctx.fillStyle='#fff'; ctx.font='12px sans-serif'; ctx.textAlign='center';
      if (nearObj.type==='chest' && !nearObj.interacted) ctx.fillText('[E] Open Chest', sx, sy);
      else if (nearObj.type==='save') ctx.fillText('[E] Save Point', sx, sy);
      else if (nearObj.type==='shop') ctx.fillText('[E] Talk to Merchant', sx, sy);
    }
  }
  handleClick(cx, cy) {
    const wx = cx + this.cam.x, wy = cy + this.cam.y;
    const result = this.player.attack(this.enemies, wx, wy);
    if (!result) return;
    if (result.type==='melee') {
      for (const e of result.hits) {
        const drop = e.takeDamage(result.dmg);
        if (drop) {
          this.player.coins += drop.coins;
          this.totalKills++;
          SFX.sfx('coin');
        }
      }
    } else if (result.type==='projectile') {
      this.projectiles.push(new Projectile(result.x,result.y,result.vx,result.vy,result.dmg,result.life,result.color));
    }
  }
  handleInteract() {
    const obj = this.getObjectAt(this.player.x, this.player.y);
    if (!obj) return;
    if (obj.type==='chest' && !obj.interacted) this.openChest(obj);
    else if (obj.type==='shop') this.interactWithShop();
    else if (obj.type==='save') this.interactWithSave(obj);
  }
}

// ── main ──
// ── Entry Point ──
const KEYS = {};
function resumeAudio() {
  if (SFX.ctx && SFX.ctx.state==='suspended') SFX.ctx.resume();
}
window.addEventListener('keydown', e => { KEYS[e.code]=true; resumeAudio();
  if (e.code==='KeyI' && GAME) { GAME.showInventory=!GAME.showInventory;
    document.getElementById('inventory-screen').classList.toggle('hidden',!GAME.showInventory);
    if (GAME.showInventory) GAME.updateInventoryUI();
    if (GAME.showShop) { GAME.showShop=false; document.getElementById('shop-screen').classList.add('hidden'); }
    if (GAME.showSave) { GAME.showSave=false; document.getElementById('save-screen').classList.add('hidden'); }
  }
  if (e.code==='KeyE' && GAME && !GAME.showInventory && !GAME.showShop) {
    if (GAME.showSave) { GAME.saveGame(); GAME.showSave=false; document.getElementById('save-screen').classList.add('hidden'); }
    else GAME.handleInteract();
  }
  if (e.code==='KeyR' && GAME && GAME.player.hasBow && GAME.player.weapon!=='bow') {
    GAME.player._prevWeapon = GAME.player.weapon;
    GAME.player.weapon='bow';
  } else if (e.code==='Digit1' && GAME && GAME.player.weapon==='bow') {
    GAME.player.weapon=GAME.player._prevWeapon||'sword';
  }
  if (e.code==='Escape') {
    if (GAME.showInventory) { GAME.showInventory=false; document.getElementById('inventory-screen').classList.add('hidden'); }
    if (GAME.showShop) { GAME.showShop=false; document.getElementById('shop-screen').classList.add('hidden'); }
    if (GAME.showSave) { GAME.showSave=false; document.getElementById('save-screen').classList.add('hidden'); }
  }
});
window.addEventListener('keyup', e => { KEYS[e.code]=false; });
window.addEventListener('resize', () => { if (GAME) GAME.resize(); });

const canvas = document.getElementById('gameCanvas');
const GAME = new Game(canvas);
SFX.init();

// Spawn initial enemies
GAME.spawnEnemies();

function startFresh() {
  GAME.player = new Player();
  GAME.enemies = [];
  GAME.worldObjects = createWorldObjects();
  GAME.spawnEnemies();
  GAME.showDeath = false;
  GAME.gameProgression = 0;
  GAME.totalKills = 0;
  GAME.chestsOpened = [];
  GAME.projectiles = [];
  document.getElementById('death-screen').classList.add('hidden');
  GAME.showDialog('New adventure begins! WASD move, click attack, E interact, I inventory.');
}

// Button handlers
document.getElementById('inv-close').addEventListener('click', ()=>{
  GAME.showInventory=false; document.getElementById('inventory-screen').classList.add('hidden');
});
document.getElementById('shop-close').addEventListener('click', ()=>{
  GAME.showShop=false; document.getElementById('shop-screen').classList.add('hidden');
});
document.getElementById('save-btn').addEventListener('click', ()=>{
  GAME.saveGame(); GAME.showSave=false; document.getElementById('save-screen').classList.add('hidden');
});
document.getElementById('save-close').addEventListener('click', ()=>{
  GAME.showSave=false; document.getElementById('save-screen').classList.add('hidden');
});
document.getElementById('death-load').addEventListener('click', ()=>{
  if (GAME.loadGame()) {
    document.getElementById('death-screen').classList.add('hidden');
    GAME.updateHUD();
  } else {
    GAME.showDialog('No save found! Start a new game instead.');
  }
});
document.getElementById('death-new').addEventListener('click', startFresh);

// Click to attack
canvas.addEventListener('click', (e)=>{
  resumeAudio();
  if (GAME.showInventory||GAME.showShop||GAME.showSave) return;
  const rect=canvas.getBoundingClientRect();
  GAME.handleClick(e.clientX-rect.left, e.clientY-rect.top);
});

// Mouse tracking for ranged direction
canvas.addEventListener('mousemove', (e)=>{
  const rect=canvas.getBoundingClientRect();
  GAME.mouseCanvas={x:e.clientX-rect.left, y:e.clientY-rect.top};
  GAME.mouseWorld={x:GAME.mouseCanvas.x+GAME.cam.x, y:GAME.mouseCanvas.y+GAME.cam.y};
});

// Clear any corrupted save
try {
  const raw = localStorage.getItem('emberquest_save');
  if (raw) {
    const data = JSON.parse(raw);
    if (data.hp <= 0) localStorage.removeItem('emberquest_save');
  }
} catch(e) { localStorage.removeItem('emberquest_save'); }

// Try auto-load
const hasSave = GAME.loadGame();
if (!hasSave) {
  GAME.showDialog('Welcome to EmberQuest! WASD to move, click to attack, E to interact, I for inventory. Find the merchant 💰 in the village!');
}

// Game loop
function gameLoop(timestamp) {
  if (!GAME.player.alive && !GAME.showDeath) {
    GAME.showDeath=true;
    document.getElementById('death-screen').classList.remove('hidden');
  }
  if (!GAME.showDeath) {
    GAME.update(timestamp);
  }
  GAME.render();
  GAME.updateHUD();
  GAME.renderMinimap();
  requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
</script>
</body>
</html>
<!-- agent-meta {"model":"deepseek/deepseek-v4-pro","provider":"openrouter","persona":"composer","sessionId":"f8b66565-da3f-4d34-b11a-0d1c99562c7a","tokensIn":1622160,"tokensOut":53996,"tokensTotal":1676156,"turns":42,"toolCalls":42,"failedToolCalls":2,"timestamp":"2026-07-18T20:06:21.206Z"} -->