← RPG results

RPG

Grok 4.5 · basic

Initial view of RPG

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

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

Generation prompt · exact text unavailable

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

Inspect original source 51,317 bytes · SHA-256 88845e44b928
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>The Artifact Quest</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#0a0a12;overflow:hidden;font-family:Georgia,serif;color:#e8d5a3;user-select:none}
#gameWrap{position:relative;width:100vw;height:100vh;display:flex;align-items:center;justify-content:center}
canvas{display:block;background:#1a1a2e;image-rendering:pixelated;box-shadow:0 0 40px #000}
#ui{position:absolute;pointer-events:none;inset:0;display:flex;flex-direction:column}
#topBar{display:flex;justify-content:space-between;padding:12px 20px;background:linear-gradient(180deg,rgba(0,0,0,.7),transparent)}
#healthWrap,#statsWrap{display:flex;align-items:center;gap:8px}
.heart{width:28px;height:28px;position:relative}
.heart svg{width:100%;height:100%;filter:drop-shadow(0 0 3px #c22)}
#money{font-size:18px;text-shadow:0 0 6px #fa0}
#weaponLabel,#ammoLabel{font-size:14px;opacity:.9}
#msgLog{position:absolute;bottom:80px;left:50%;transform:translateX(-50%);text-align:center;min-height:24px;font-size:16px;text-shadow:0 2px 8px #000;transition:opacity .3s;max-width:80%}
#invPanel{position:absolute;right:16px;top:70px;background:rgba(10,10,20,.92);border:2px solid #8a7040;border-radius:8px;padding:12px;min-width:200px;pointer-events:auto;display:none}
#invPanel h3{margin-bottom:8px;color:#c9a84c;font-size:14px;letter-spacing:1px}
.inv-item{padding:6px 8px;margin:3px 0;background:rgba(255,255,255,.05);border-radius:4px;cursor:pointer;font-size:13px;border:1px solid transparent;display:flex;justify-content:space-between}
.inv-item:hover,.inv-item.active{border-color:#c9a84c;background:rgba(201,168,76,.15)}
.inv-item .key{opacity:.5;font-size:11px}
#shopPanel{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);background:rgba(15,12,8,.96);border:3px solid #c9a84c;border-radius:12px;padding:24px;min-width:340px;pointer-events:auto;display:none;box-shadow:0 0 60px rgba(201,168,76,.3)}
#shopPanel h2{color:#c9a84c;margin-bottom:4px;text-align:center}
#shopPanel .sub{text-align:center;font-size:12px;opacity:.7;margin-bottom:16px}
.shop-item{display:flex;justify-content:space-between;align-items:center;padding:10px 12px;margin:6px 0;background:rgba(255,255,255,.04);border-radius:6px;cursor:pointer;border:1px solid #333;transition:border .15s}
.shop-item:hover{border-color:#c9a84c}
.shop-item .price{color:#fa0;font-weight:bold}
#shopClose,#dlgClose{margin-top:14px;text-align:center;padding:8px;cursor:pointer;color:#aaa;font-size:13px}
#shopClose:hover,#dlgClose:hover{color:#fff}
#dlgBox{position:absolute;bottom:40px;left:50%;transform:translateX(-50%);width:min(640px,90%);background:rgba(8,8,18,.95);border:2px solid #6a5a3a;border-radius:10px;padding:20px 24px;pointer-events:auto;display:none}
#dlgBox .speaker{color:#c9a84c;font-size:13px;margin-bottom:6px;letter-spacing:1px}
#dlgBox .text{font-size:16px;line-height:1.5}
#titleScreen,#endScreen{position:absolute;inset:0;background:radial-gradient(ellipse at center,#1a1528 0%,#0a0a12 70%);display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:auto;z-index:10}
#titleScreen h1{font-size:52px;color:#c9a84c;text-shadow:0 0 30px rgba(201,168,76,.5);letter-spacing:4px;margin-bottom:8px}
#titleScreen .tag{font-size:16px;opacity:.6;margin-bottom:40px;font-style:italic}
.btn{padding:14px 48px;font-size:18px;background:linear-gradient(180deg,#3a2a10,#2a1a08);border:2px solid #c9a84c;color:#e8d5a3;cursor:pointer;border-radius:6px;font-family:Georgia,serif;letter-spacing:2px;transition:all .2s}
.btn:hover{background:linear-gradient(180deg,#5a4020,#3a2a10);box-shadow:0 0 20px rgba(201,168,76,.4)}
#endScreen{display:none}
#endScreen h1{font-size:42px;color:#c9a84c;margin-bottom:16px}
#endScreen p{max-width:480px;text-align:center;line-height:1.6;margin-bottom:32px;opacity:.85}
#controls{position:absolute;bottom:12px;left:16px;font-size:11px;opacity:.45;line-height:1.6}
#minimap{position:absolute;top:70px;left:16px;border:2px solid #4a3a20;border-radius:4px;background:rgba(0,0,0,.6)}
#saveFlash{position:absolute;inset:0;background:#8f8;opacity:0;pointer-events:none;transition:opacity .3s}
</style>
</head>
<body>
<div id="gameWrap">
<canvas id="c"></canvas>
<div id="ui">
<div id="topBar">
<div id="healthWrap"></div>
<div id="statsWrap">
<span id="weaponLabel">Weapon: Wooden Staff</span>
<span id="ammoLabel" style="display:none">| Arrows: 0</span>
<span id="money">◈ 0</span>
</div>
</div>
<div id="msgLog"></div>
<canvas id="minimap" width="120" height="90"></canvas>
<div id="controls">WASD/Arrows Move · Space Attack · Shift Sprint · E Interact · Q Bow · I Inventory · 1-5 Use Item · F Save at Altar</div>
<div id="invPanel"><h3>INVENTORY</h3><div id="invList"></div></div>
<div id="shopPanel">
<h2>Wanderer's Shop</h2>
<div class="sub">Press number or click to buy · Esc to close</div>
<div id="shopList"></div>
<div id="shopClose">[Esc] Close</div>
</div>
<div id="dlgBox"><div class="speaker" id="dlgSpeaker"></div><div class="text" id="dlgText"></div><div id="dlgClose">[E / Space] Continue</div></div>
<div id="saveFlash"></div>
</div>
<div id="titleScreen">
<h1>THE ARTIFACT QUEST</h1>
<div class="tag">A short action-adventure RPG</div>
<button class="btn" id="startBtn">BEGIN QUEST</button>
<p style="margin-top:24px;font-size:12px;opacity:.4">Explore · Fight · Collect · Ascend</p>
</div>
<div id="endScreen">
<h1>Artifact Reclaimed</h1>
<p id="endText">You have recovered the Ancient Artifact and restored balance to the realm. Your legend will be remembered.</p>
<button class="btn" id="reBtn">PLAY AGAIN</button>
</div>
</div>
<script>
const canvas=document.getElementById('c');
const ctx=canvas.getContext('2d');
const mm=document.getElementById('minimap');
const mctx=mm.getContext('2d');
let W,H,TILE=40;
function resize(){
  const maxW=Math.min(960,window.innerWidth-20);
  const maxH=Math.min(640,window.innerHeight-20);
  W=Math.floor(maxW/TILE)*TILE;
  H=Math.floor(maxH/TILE)*TILE;
  canvas.width=W;canvas.height=H;
  canvas.style.width=W+'px';canvas.style.height=H+'px';
}
resize();
window.addEventListener('resize',resize);

// ─── Audio (Web Audio API) ───
const Actx=new (window.AudioContext||window.webkitAudioContext)();
function tone(freq,dur,type='square',vol=.08,slide=0){
  if(Actx.state==='suspended')Actx.resume();
  const o=Actx.createOscillator(),g=Actx.createGain();
  o.type=type;o.frequency.value=freq;
  if(slide)o.frequency.linearRampToValueAtTime(freq+slide,Actx.currentTime+dur);
  g.gain.value=vol;
  g.gain.exponentialRampToValueAtTime(.001,Actx.currentTime+dur);
  o.connect(g);g.connect(Actx.destination);
  o.start();o.stop(Actx.currentTime+dur);
}
function noise(dur,vol=.06){
  if(Actx.state==='suspended')Actx.resume();
  const n=Actx.createBufferSource();
  const buf=Actx.createBuffer(1,Actx.sampleRate*dur,Actx.sampleRate);
  const d=buf.getChannelData(0);
  for(let i=0;i<d.length;i++)d[i]=(Math.random()*2-1)*vol*(1-i/d.length);
  n.buffer=buf;n.connect(Actx.destination);n.start();
}
const SFX={
  walk:()=>tone(80+Math.random()*40,.04,'triangle',.03),
  swing:()=>tone(200,.08,'sawtooth',.07,-120),
  hit:()=>{tone(150,.1,'square',.1,-80);noise(.08,.05)},
  hurt:()=>{tone(120,.15,'sawtooth',.12,-60);tone(80,.2,'square',.08)},
  pickup:()=>{tone(520,.08,'sine',.07);setTimeout(()=>tone(780,.1,'sine',.06),70)},
  coin:()=>{tone(880,.06,'square',.05);setTimeout(()=>tone(1100,.08,'square',.04),50)},
  shop:()=>tone(440,.12,'triangle',.06),
  buy:()=>{tone(600,.08,'sine',.07);setTimeout(()=>tone(900,.12,'sine',.06),80)},
  save:()=>{tone(300,.1,'sine',.06);setTimeout(()=>tone(450,.1,'sine',.06),100);setTimeout(()=>tone(600,.2,'sine',.07),200)},
  levelup:()=>{[400,500,600,800].forEach((f,i)=>setTimeout(()=>tone(f,.15,'sine',.08),i*100))},
  shoot:()=>tone(600,.06,'square',.06,-300),
  enemyDie:()=>{tone(100,.2,'sawtooth',.1,-50);noise(.15,.08)},
  door:()=>tone(180,.15,'triangle',.07,40),
  heal:()=>{tone(400,.1,'sine',.06);setTimeout(()=>tone(600,.15,'sine',.06),80)},
  win:()=>{[523,659,784,1047].forEach((f,i)=>setTimeout(()=>tone(f,.3,'sine',.09),i*180))}
};

// ─── Particles ───
let particles=[];
function spawnParticles(x,y,col,n=8,spd=2,life=30,size=3){
  for(let i=0;i<n;i++){
    const a=Math.random()*Math.PI*2,s=spd*(.5+Math.random());
    particles.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,col,life,max:life,size:size*(.5+Math.random())});
  }
}
function updateParticles(){
  for(let i=particles.length-1;i>=0;i--){
    const p=particles[i];
    p.x+=p.vx;p.y+=p.vy;p.vy+=.04;p.life--;
    if(p.life<=0)particles.splice(i,1);
  }
}
function drawParticles(cam){
  for(const p of particles){
    ctx.globalAlpha=p.life/p.max;
    ctx.fillStyle=p.col;
    ctx.beginPath();
    ctx.arc(p.x-cam.x,p.y-cam.y,p.size,0,Math.PI*2);
    ctx.fill();
  }
  ctx.globalAlpha=1;
}

// ─── Map Data ───
// Tiles: 0 floor, 1 wall, 2 water, 3 tall grass, 4 stone floor, 5 carpet, 6 sand, 7 lava, 8 bridge, 9 flower
const MAP_W=60,MAP_H=50;
let map=[];
const zones={
  village:{x:2,y:2,w:14,h:12},
  forest:{x:16,y:2,w:18,h:16},
  crossroads:{x:16,y:18,w:12,h:10},
  cave:{x:34,y:4,w:16,h:14},
  ruins:{x:28,y:28,w:22,h:18},
  shop_area:{x:4,y:6,w:4,h:3}
};

function genMap(){
  map=Array.from({length:MAP_H},()=>Array(MAP_W).fill(1));
  // Village
  fillRect(2,2,14,12,0);
  fillRect(5,5,5,4,5); // carpet plaza
  // Path to forest
  for(let x=15;x<20;x++)map[8][x]=0;
  // Forest
  fillRect(16,2,20,16,0);
  // Scatter trees as walls later via entities; grass
  for(let y=3;y<17;y++)for(let x=17;x<35;x++)if(Math.random()<.3)map[y][x]=3;
  // Path south
  for(let y=17;y<22;y++)map[y][24]=0;
  // Crossroads
  fillRect(16,18,14,12,0);
  // Path to cave
  for(let x=34;x<38;x++)map[10][x]=0;
  // Cave entrance corridor
  fillRect(36,6,14,12,4);
  // Irregular cave
  for(let y=6;y<18;y++)for(let x=36;x<50;x++){
    if(Math.random()<.15)map[y][x]=1;
    else map[y][x]=4;
  }
  fillRect(37,7,12,10,4);
  // Path to ruins
  for(let y=29;y<32;y++)for(let x=24;x<30;x++)map[y][x]=0;
  for(let y=28;y<32;y++)map[y][24]=0;
  // Ruins
  fillRect(28,28,24,18,4);
  for(let y=30;y<44;y++)for(let x=30;x<50;x++)if(Math.random()<.08)map[y][x]=1;
  // Inner sanctum
  fillRect(38,36,8,6,5);
  // Decorative water pond near village
  fillRect(10,12,4,3,2);
  // River between forest and ruins
  for(let x=18;x<36;x++){map[26][x]=2;map[27][x]=2;}
  map[26][24]=8;map[27][24]=8; // bridge
  // Flowers in village
  map[4][4]=9;map[4][12]=9;map[11][3]=9;
  // Lava in deep cave
  map[14][46]=7;map[15][47]=7;map[14][47]=7;
  // Border walls already 1
  // Ensure spawn walkable
  map[8][6]=0;
}

function fillRect(x,y,w,h,t){
  for(let j=y;j<y+h&&j<MAP_H;j++)for(let i=x;i<x+w&&i<MAP_W;i++)if(i>=0&&j>=0)map[j][i]=t;
}

const walkable=new Set([0,3,4,5,6,8,9]);

// ─── Game State ───
const WEAPONS=[
  {id:'staff',name:'Wooden Staff',dmg:1,range:42,speed:320,color:'#8B6914',reach:1},
  {id:'sword',name:'Iron Sword',dmg:2,range:50,speed:260,color:'#b0b8c8',reach:1},
  {id:'flame',name:'Flame Blade',dmg:3,range:56,speed:220,color:'#ff6a20',reach:1,fire:true}
];

const SHOP_ITEMS=[
  {id:'potion',name:'Health Potion',desc:'Restore 2 HP',price:15,type:'consumable'},
  {id:'meat',name:'Hearty Meat',desc:'Restore 1 HP',price:8,type:'consumable'},
  {id:'bomb',name:'Smoke Bomb',desc:'Damage nearby foes',price:20,type:'consumable'},
  {id:'boots',name:'Swift Boots',desc:'+30% move speed',price:35,type:'accessory',unique:true},
  {id:'amulet',name:'Vital Amulet',desc:'+1 Max Health',price:50,type:'accessory',unique:true},
  {id:'ring',name:'Power Ring',desc:'+1 Attack Damage',price:40,type:'accessory',unique:true},
  {id:'arrows',name:'Arrow Bundle',desc:'+10 arrows',price:12,type:'ammo'}
];

let player,enemies,npcs,items,chests,projectiles,cam,gameTime,keys,msgTimer,msgText;
let flags,shopOpen,invOpen,dlgOpen,dlgQueue,saveSpots,gameActive,attackAnim,bowCooldown;
let trees,decor;

function msg(t,dur=2.5){msgText=t;msgTimer=dur;document.getElementById('msgLog').textContent=t;document.getElementById('msgLog').style.opacity=1}
function clearMsg(){msgTimer=0;document.getElementById('msgLog').style.opacity=0}

function initGame(){
  genMap();
  gameTime=0;gameActive=true;shopOpen=false;invOpen=false;dlgOpen=false;dlgQueue=[];
  attackAnim=0;bowCooldown=0;particles=[];projectiles=[];
  flags={
    weaponTier:0,hasBow:false,maxHp:3,hp:3,money:0,arrows:0,
    killed:0,bossDefeated:false,gotSword:false,gotFlame:false,
    heartCave:false,heartRuins:false,introDone:false,
    accessories:{}, // boots, amulet, ring
    inventory:[], // consumables stacked
    saves:0,progress:0 // 0-100
  };
  // Starter potion
  addItem('potion',1);

  player={
    x:6*TILE+20,y:8*TILE+20,w:22,h:26,
    vx:0,vy:0,facing:1,// 0 up 1 right 2 down 3 left
    speed:2.2,sprint:false,moving:false,hurtTimer:0,anim:0,
    attacking:false
  };
  cam={x:0,y:0};
  keys={};

  // Trees (impassable-ish decor + collision)
  trees=[];
  for(let y=3;y<17;y++)for(let x=17;x<35;x++){
    if(map[y][x]===0||map[y][x]===3){
      if(Math.random()<.12){trees.push({x:x*TILE+8,y:y*TILE+4,r:14});map[y][x]=0;}
    }
  }

  // Enemies
  enemies=[];
  // Forest slimes
  for(let i=0;i<6;i++)spawnEnemy('slime',18+Math.random()*14,4+Math.random()*10);
  // Crossroads wolves
  for(let i=0;i<4;i++)spawnEnemy('wolf',18+Math.random()*10,19+Math.random()*8);
  // Cave bats / golems
  for(let i=0;i<5;i++)spawnEnemy('bat',38+Math.random()*10,8+Math.random()*8);
  spawnEnemy('golem',44,12);
  // Ruins guardians
  for(let i=0;i<6;i++)spawnEnemy('guardian',30+Math.random()*18,30+Math.random()*12);
  // Boss
  spawnEnemy('boss',42,39);

  // NPCs
  npcs=[
    {x:7*TILE,y:6*TILE,w:24,h:28,name:'ELDER MIRA',color:'#6a4',lines:[
      "Welcome, traveler. Darkness seeps from the southern ruins.",
      "An Ancient Artifact was stolen — it keeps our lands in balance.",
      "Explore the forest east, then the caves. Arm yourself well.",
      "There is a shopkeep nearby. Spend wisely. Altars can save your journey."
    ],li:0},
    {x:5*TILE,y:9*TILE,w:24,h:28,name:'SHOPKEEP BREN',color:'#c94',shop:true,lines:[
      "Ah, a customer! Browse my wares — best gear this side of the ruins."
    ],li:0},
    {x:12*TILE,y:7*TILE,w:24,h:28,name:'HUNTER KAEL',color:'#48a',lines:[
      "The forest beasts grow bold. A fine sword rests in a chest deep among the trees.",
      "If you find a bow in the caves, you'll thank me. Stay light on your feet."
    ],li:0},
    {x:40*TILE,y:8*TILE,w:24,h:28,name:'HERMIT',color:'#888',lines:[
      "Few venture into these caves... A bow of the old wardens lies sealed ahead.",
      "Beyond the river, the ruins await. The Artifact calls to the worthy."
    ],li:0}
  ];

  // Chests
  chests=[
    {x:30*TILE,y:5*TILE,open:false,loot:'sword',msg:'You found the Iron Sword!'},
    {x:47*TILE,y:15*TILE,open:false,loot:'bow',msg:'You discovered the Warden Bow!'},
    {x:42*TILE,y:10*TILE,open:false,loot:'heart',msg:'A Heart Container! Max HP increased!'},
    {x:34*TILE,y:40*TILE,open:false,loot:'flame',msg:'The Flame Blade ignites in your hands!'},
    {x:48*TILE,y:42*TILE,open:false,loot:'heart2',msg:'Another Heart Container! Destiny strengthens you!'},
    {x:22*TILE,y:14*TILE,open:false,loot:'coins15',msg:'You found 15 coins!'},
    {x:20*TILE,y:22*TILE,open:false,loot:'potion',msg:'A potion was stashed here.'},
    {x:45*TILE,y:32*TILE,open:false,loot:'coins25',msg:'A stash of 25 coins!'}
  ];

  // Ground items / pickups
  items=[
    {x:9*TILE+10,y:10*TILE,type:'coin',v:5,r:8},
    {x:25*TILE,y:10*TILE,type:'coin',v:3,r:8},
    {x:28*TILE,y:20*TILE,type:'coin',v:5,r:8},
    {x:40*TILE,y:14*TILE,type:'coin',v:8,r:8}
  ];

  // Save altars
  saveSpots=[
    {x:8*TILE,y:5*TILE,name:'Village Altar'},
    {x:24*TILE,y:20*TILE,name:'Crossroads Altar'},
    {x:40*TILE,y:7*TILE,name:'Cave Altar'},
    {x:36*TILE,y:34*TILE,name:'Ruins Altar'}
  ];

  updateHealthUI();
  updateStatsUI();
  msg('Speak with Elder Mira to begin your quest.',4);
}

function spawnEnemy(type,tx,ty){
  const defs={
    slime:{hp:3,spd:.6,dmg:1,r:14,color:'#5c3',xp:5,coin:2,name:'Slime'},
    wolf:{hp:5,spd:1.3,dmg:1,r:16,color:'#888',xp:8,coin:4,name:'Wolf'},
    bat:{hp:2,spd:1.6,dmg:1,r:12,color:'#a6a',xp:6,coin:3,name:'Bat'},
    golem:{hp:12,spd:.4,dmg:2,r:22,color:'#789',xp:20,coin:12,name:'Cave Golem'},
    guardian:{hp:8,spd:.9,dmg:2,r:18,color:'#c66',xp:12,coin:7,name:'Ruins Guardian'},
    boss:{hp:40,spd:.7,dmg:3,r:28,color:'#a2f',xp:100,coin:50,name:'Artifact Warden',boss:true}
  };
  const d=defs[type];
  enemies.push({
    type,x:tx*TILE+TILE/2,y:ty*TILE+TILE/2,
    hp:d.hp,maxHp:d.hp,spd:d.spd,dmg:d.dmg,r:d.r,color:d.color,
    xp:d.xp,coin:d.coin,name:d.name,boss:!!d.boss,
    hurt:0,alive:true,cd:0,anim:Math.random()*10,ai:0
  });
}

function addItem(id,n=1){
  const inv=flags.inventory;
  const ex=inv.find(i=>i.id===id);
  if(ex)ex.qty+=n;
  else{
    const def=SHOP_ITEMS.find(s=>s.id===id)||{id,name:id,type:'consumable'};
    inv.push({id,name:def.name||id,qty:n,type:def.type||'consumable'});
  }
  renderInv();
}

function renderInv(){
  const el=document.getElementById('invList');
  el.innerHTML='';
  const inv=flags.inventory;
  if(!inv.length){el.innerHTML='<div style="opacity:.5;font-size:12px">Empty</div>';return}
  inv.forEach((it,i)=>{
    const d=document.createElement('div');
    d.className='inv-item';
    d.innerHTML=`<span>${it.name} x${it.qty}</span><span class="key">${i+1}</span>`;
    d.onclick=()=>useItem(i);
    el.appendChild(d);
  });
  // Show accessories
  const acc=Object.keys(flags.accessories).filter(k=>flags.accessories[k]);
  if(acc.length){
    const h=document.createElement('div');
    h.style.cssText='margin-top:8px;font-size:11px;color:#c9a84c';
    h.textContent='Equipped: '+acc.map(a=>a.charAt(0).toUpperCase()+a.slice(1)).join(', ');
    el.appendChild(h);
  }
}

function useItem(idx){
  const it=flags.inventory[idx];
  if(!it)return;
  if(it.id==='potion'){
    if(flags.hp>=flags.maxHp){msg('Already at full health!');return}
    flags.hp=Math.min(flags.maxHp,flags.hp+2);
    SFX.heal();spawnParticles(player.x,player.y,'#f66',12,2,40,4);
    msg('Used Health Potion (+2 HP)');
  }else if(it.id==='meat'){
    if(flags.hp>=flags.maxHp){msg('Already at full health!');return}
    flags.hp=Math.min(flags.maxHp,flags.hp+1);
    SFX.heal();spawnParticles(player.x,player.y,'#f96',8,2,30,3);
    msg('Ate Hearty Meat (+1 HP)');
  }else if(it.id==='bomb'){
    SFX.hit();spawnParticles(player.x,player.y,'#888',20,4,35,5);
    msg('Smoke Bomb detonated!');
    for(const e of enemies){
      if(!e.alive)continue;
      const dx=e.x-player.x,dy=e.y-player.y;
      if(Math.hypot(dx,dy)<100){damageEnemy(e,3)}
    }
  }else{return}
  it.qty--;
  if(it.qty<=0)flags.inventory.splice(idx,1);
  updateHealthUI();renderInv();
}

function updateHealthUI(){
  const wrap=document.getElementById('healthWrap');
  wrap.innerHTML='';
  const maxH=flags.maxHp;
  const hp=flags.hp;
  // Each heart = 2 HP
  const hearts=Math.ceil(maxH/2);
  for(let i=0;i<hearts;i++){
    const filled=Math.min(2,Math.max(0,hp-i*2));
    const div=document.createElement('div');
    div.className='heart';
    // SVG heart with half/full/empty
    let fill='#e22',fill2='none';
    if(filled>=2){fill='#e22';fill2='#e22'}
    else if(filled===1){fill='#e22';fill2='none'}
    else{fill='#333';fill2='#333'}
    div.innerHTML=`<svg viewBox="0 0 32 30">
      <defs><clipPath id="hc${i}"><rect x="0" y="0" width="${filled>=1?16:0}" height="30"/></clipPath></defs>
      <path d="M16 28C16 28 2 18 2 10C2 5 6 2 10 2C13 2 15 4 16 6C17 4 19 2 22 2C26 2 30 5 30 10C30 18 16 28 16 28Z" fill="#333" stroke="#a22" stroke-width="1"/>
      <path d="M16 28C16 28 2 18 2 10C2 5 6 2 10 2C13 2 15 4 16 6C17 4 19 2 22 2C26 2 30 5 30 10C30 18 16 28 16 28Z" fill="#e22" clip-path="url(#hc${i})"/>
      ${filled>=2?'':''}
    </svg>`;
    // Simpler approach - redraw properly
    wrap.appendChild(div);
  }
  // Redo cleaner
  wrap.innerHTML='';
  for(let i=0;i<hearts;i++){
    const filled=Math.min(2,Math.max(0,hp-i*2));
    const div=document.createElement('div');
    div.className='heart';
    const svg=document.createElementNS('http://www.w3.org/2000/svg','svg');
    svg.setAttribute('viewBox','0 0 32 30');
    const path="M16 27S3 18 3 10C3 5.5 6.5 2.5 10.5 2.5 13.2 2.5 15.2 4 16 6.2 16.8 4 18.8 2.5 21.5 2.5 25.5 2.5 29 5.5 29 10 29 18 16 27 16 27Z";
    // bg
    const bg=document.createElementNS('http://www.w3.org/2000/svg','path');
    bg.setAttribute('d',path);bg.setAttribute('fill','#2a1520');bg.setAttribute('stroke','#833');bg.setAttribute('stroke-width','1.5');
    svg.appendChild(bg);
    if(filled>=2){
      const f=document.createElementNS('http://www.w3.org/2000/svg','path');
      f.setAttribute('d',path);f.setAttribute('fill','#e22');
      svg.appendChild(f);
    }else if(filled===1){
      const cp=document.createElementNS('http://www.w3.org/2000/svg','clipPath');
      cp.id='cl'+i+gameTime;
      const r=document.createElementNS('http://www.w3.org/2000/svg','rect');
      r.setAttribute('x','0');r.setAttribute('y','0');r.setAttribute('width','16');r.setAttribute('height','30');
      cp.appendChild(r);
      const defs=document.createElementNS('http://www.w3.org/2000/svg','defs');
      defs.appendChild(cp);svg.appendChild(defs);
      const f=document.createElementNS('http://www.w3.org/2000/svg','path');
      f.setAttribute('d',path);f.setAttribute('fill','#e22');f.setAttribute('clip-path',`url(#cl${i}${gameTime})`);
      svg.appendChild(f);
    }
    div.appendChild(svg);
    wrap.appendChild(div);
  }
}

function updateStatsUI(){
  const w=WEAPONS[flags.weaponTier];
  document.getElementById('weaponLabel').textContent='Weapon: '+w.name;
  document.getElementById('money').textContent='◈ '+flags.money;
  const al=document.getElementById('ammoLabel');
  if(flags.hasBow){al.style.display='inline';al.textContent='| Arrows: '+flags.arrows}
  else al.style.display='none';
}

// ─── Input ───
window.addEventListener('keydown',e=>{
  if(Actx.state==='suspended')Actx.resume();
  keys[e.code]=true;
  if(e.code==='KeyI'){invOpen=!invOpen;document.getElementById('invPanel').style.display=invOpen?'block':'none';renderInv()}
  if(e.code==='Escape'){closeShop();invOpen=false;document.getElementById('invPanel').style.display='none';if(dlgOpen)advanceDlg()}
  if(e.code==='KeyE'||e.code==='Space'){
    if(dlgOpen){e.preventDefault();advanceDlg();return}
  }
  if(dlgOpen||shopOpen||!gameActive)return;
  if(e.code==='Space'){e.preventDefault();doAttack()}
  if(e.code==='KeyQ')doBow();
  if(e.code==='KeyF')trySave();
  if(e.code==='KeyE')tryInteract();
  // item hotkeys
  if(e.code>='Digit1'&&e.code<='Digit5'){
    const idx=parseInt(e.code.replace('Digit',''))-1;
    useItem(idx);
  }
  // shop hotkeys
  if(shopOpen&&e.code>='Digit1'&&e.code<='Digit9'){
    buyShop(parseInt(e.code.replace('Digit',''))-1);
  }
});
window.addEventListener('keyup',e=>{keys[e.code]=false});

function closeShop(){shopOpen=false;document.getElementById('shopPanel').style.display='none'}

function openShop(){
  shopOpen=true;SFX.shop();
  const panel=document.getElementById('shopPanel');
  panel.style.display='block';
  const list=document.getElementById('shopList');
  list.innerHTML='';
  SHOP_ITEMS.forEach((it,i)=>{
    if(it.unique&&flags.accessories[it.id])return; // sold out unique
    const d=document.createElement('div');
    d.className='shop-item';
    d.innerHTML=`<div><b>${i+1}. ${it.name}</b><br><span style="font-size:11px;opacity:.6">${it.desc}</span></div><div class="price">◈ ${it.price}</div>`;
    d.onclick=()=>buyShop(i);
    list.appendChild(d);
  });
}

function buyShop(i){
  const it=SHOP_ITEMS[i];
  if(!it)return;
  if(it.unique&&flags.accessories[it.id]){msg('Already owned!');return}
  if(flags.money<it.price){msg('Not enough coins!');return}
  flags.money-=it.price;
  SFX.buy();
  spawnParticles(player.x,player.y,'#fc0',10,2,30,3);
  if(it.type==='accessory'){
    flags.accessories[it.id]=true;
    if(it.id==='amulet'){flags.maxHp=Math.min(5,flags.maxHp+1);flags.hp=Math.min(flags.maxHp,flags.hp+1);updateHealthUI();SFX.levelup()}
    if(it.id==='boots')msg('Swift Boots equipped! You feel faster.');
    if(it.id==='ring')msg('Power Ring equipped! Attacks grow stronger.');
    if(it.id==='amulet')msg('Vital Amulet equipped! Max health increased!');
  }else if(it.type==='ammo'){
    flags.arrows+=10;msg('+10 Arrows');
  }else{
    addItem(it.id,1);msg('Purchased '+it.name);
  }
  updateStatsUI();updateHealthUI();renderInv();
  openShop(); // refresh
}

function tryInteract(){
  // NPCs
  for(const n of npcs){
    if(Math.hypot(n.x+12-player.x,n.y+14-player.y)<50){
      if(n.shop){openShop();return}
      startDlg(n);
      return;
    }
  }
  // Chests
  for(const c of chests){
    if(c.open)continue;
    if(Math.hypot(c.x+16-player.x,c.y+12-player.y)<45){
      openChest(c);return;
    }
  }
}

function startDlg(n){
  dlgOpen=true;
  const lines=n.lines;
  dlgQueue=lines.map(t=>({speaker:n.name,text:t}));
  showDlg();
}
function showDlg(){
  if(!dlgQueue.length){dlgOpen=false;document.getElementById('dlgBox').style.display='none';return}
  const d=dlgQueue[0];
  document.getElementById('dlgSpeaker').textContent=d.speaker;
  document.getElementById('dlgText').textContent=d.text;
  document.getElementById('dlgBox').style.display='block';
}
function advanceDlg(){
  dlgQueue.shift();
  if(!dlgQueue.length){dlgOpen=false;document.getElementById('dlgBox').style.display='none';flags.introDone=true;return}
  showDlg();
}

function openChest(c){
  c.open=true;SFX.pickup();
  spawnParticles(c.x+16,c.y+12,'#fc0',16,3,40,4);
  const L=c.loot;
  if(L==='sword'&&flags.weaponTier<1){flags.weaponTier=1;flags.gotSword=true;SFX.levelup();msg(c.msg);updateStatsUI()}
  else if(L==='flame'&&flags.weaponTier<2){flags.weaponTier=2;flags.gotFlame=true;SFX.levelup();msg(c.msg);updateStatsUI()}
  else if(L==='bow'){flags.hasBow=true;flags.arrows+=15;SFX.levelup();msg(c.msg);updateStatsUI()}
  else if(L==='heart'||L==='heart2'){
    if(flags.maxHp<5){flags.maxHp++;flags.hp=flags.maxHp;if(L==='heart')flags.heartCave=true;else flags.heartRuins=true}
    SFX.levelup();msg(c.msg);updateHealthUI();
  }
  else if(L==='coins15'){flags.money+=15;SFX.coin();msg(c.msg);updateStatsUI()}
  else if(L==='coins25'){flags.money+=25;SFX.coin();msg(c.msg);updateStatsUI()}
  else if(L==='potion'){addItem('potion',1);msg(c.msg)}
  else msg(c.msg);
  updateProgress();
}

function trySave(){
  for(const s of saveSpots){
    if(Math.hypot(s.x+16-player.x,s.y+16-player.y)<50){
      doSave(s);return;
    }
  }
  msg('No save altar nearby. Find a glowing altar.');
}

function doSave(s){
  const data={
    flags:JSON.parse(JSON.stringify(flags)),
    px:player.x,py:player.y,
    enemies:enemies.map(e=>({type:e.type,x:e.x,y:e.y,hp:e.hp,alive:e.alive})),
    chests:chests.map(c=>c.open),
    items:items.map(i=>({x:i.x,y:i.y,type:i.type,v:i.v}))
  };
  try{localStorage.setItem('artifactQuestSave',JSON.stringify(data))}catch(e){}
  flags.saves++;
  SFX.save();
  msg('Progress saved at '+s.name+'.');
  const fl=document.getElementById('saveFlash');
  fl.style.opacity='.25';
  setTimeout(()=>fl.style.opacity='0',300);
  spawnParticles(s.x+16,s.y+16,'#8f8',20,2.5,50,4);
}

function loadSave(){
  try{
    const raw=localStorage.getItem('artifactQuestSave');
    if(!raw)return false;
    const data=JSON.parse(raw);
    initGame();
    Object.assign(flags,data.flags);
    player.x=data.px;player.y=data.py;
    // restore chests
    data.chests.forEach((o,i)=>{if(chests[i])chests[i].open=o});
    // restore enemies roughly
    enemies.forEach((e,i)=>{
      if(data.enemies[i]){e.hp=data.enemies[i].hp;e.alive=data.enemies[i].alive;e.x=data.enemies[i].x;e.y=data.enemies[i].y}
    });
    items=data.items||items;
    updateHealthUI();updateStatsUI();renderInv();
    msg('Save loaded.',2);
    return true;
  }catch(e){return false}
}

function updateProgress(){
  let p=0;
  if(flags.gotSword)p+=15;
  if(flags.hasBow)p+=20;
  if(flags.gotFlame)p+=20;
  if(flags.heartCave)p+=10;
  if(flags.heartRuins)p+=10;
  if(flags.accessories.boots)p+=5;
  if(flags.accessories.amulet)p+=5;
  if(flags.accessories.ring)p+=5;
  if(flags.bossDefeated)p+=10;
  flags.progress=Math.min(100,p+flags.killed);
}

// ─── Combat ───
function doAttack(){
  if(attackAnim>0||dlgOpen||shopOpen)return;
  const w=WEAPONS[flags.weaponTier];
  attackAnim=w.speed/1000*60; // frames approx
  player.attacking=true;
  SFX.swing();
  // hitbox in facing dir
  const reach=w.range;
  let hx=player.x,hy=player.y;
  if(player.facing===0)hy-=reach*.6;
  if(player.facing===1)hx+=reach*.6;
  if(player.facing===2)hy+=reach*.6;
  if(player.facing===3)hx-=reach*.6;

  let dmg=w.dmg+(flags.accessories.ring?1:0);
  for(const e of enemies){
    if(!e.alive||e.hurt>0)continue;
    if(Math.hypot(e.x-hx,e.y-hy)<e.r+reach*.45){
      damageEnemy(e,dmg,w.fire);
    }
  }
  // swing particles
  spawnParticles(hx,hy,w.color,6,3,20,3);
}

function doBow(){
  if(!flags.hasBow){msg('You have no ranged weapon yet.');return}
  if(flags.arrows<=0){msg('Out of arrows!');return}
  if(bowCooldown>0||dlgOpen||shopOpen)return;
  flags.arrows--;bowCooldown=25;updateStatsUI();SFX.shoot();
  const sp=6;
  let vx=0,vy=0;
  if(player.facing===0)vy=-sp;
  if(player.facing===1)vx=sp;
  if(player.facing===2)vy=sp;
  if(player.facing===3)vx=-sp;
  projectiles.push({x:player.x,y:player.y,vx,vy,life:60,friendly:true,dmg:2+(flags.accessories.ring?1:0),r:5});
}

function damageEnemy(e,dmg,fire){
  e.hp-=dmg;e.hurt=12;
  SFX.hit();
  spawnParticles(e.x,e.y,fire?'#f80':'#fff',10,3,25,3);
  if(fire)spawnParticles(e.x,e.y,'#ff0',6,2,20,2);
  // knockback
  const dx=e.x-player.x,dy=e.y-player.y,l=Math.hypot(dx,dy)||1;
  e.x+=dx/l*10;e.y+=dy/l*10;
  if(e.hp<=0){
    e.alive=false;SFX.enemyDie();
    spawnParticles(e.x,e.y,e.color,20,4,40,5);
    flags.money+=e.coin;flags.killed++;
    SFX.coin();updateStatsUI();
    // drop chance
    if(Math.random()<.25)items.push({x:e.x,y:e.y,type:'coin',v:1+Math.floor(Math.random()*3),r:8});
    if(Math.random()<.1)items.push({x:e.x,y:e.y,type:'heart',v:1,r:8});
    if(e.boss){
      flags.bossDefeated=true;
      // Artifact drop
      items.push({x:e.x,y:e.y,type:'artifact',v:1,r:12});
      msg('The Artifact Warden falls! Claim the Ancient Artifact!',5);
      SFX.levelup();
    }
    updateProgress();
  }
}

function hurtPlayer(dmg){
  if(player.hurtTimer>0)return;
  flags.hp-=dmg;
  player.hurtTimer=45;
  SFX.hurt();
  spawnParticles(player.x,player.y,'#e22',12,3,30,4);
  updateHealthUI();
  if(flags.hp<=0){
    flags.hp=0;updateHealthUI();
    msg('You fall... Returning to last memory...',3);
    setTimeout(()=>{
      // respawn at village with half money loss mild + full heal
      player.x=6*TILE+20;player.y=8*TILE+20;
      flags.hp=flags.maxHp;
      flags.money=Math.floor(flags.money*.75);
      player.hurtTimer=60;
      updateHealthUI();updateStatsUI();
      msg('You awaken at the village...',3);
    },2000);
  }
}

// ─── Collision ───
function solidAt(x,y){
  const tx=Math.floor(x/TILE),ty=Math.floor(y/TILE);
  if(tx<0||ty<0||tx>=MAP_W||ty>=MAP_H)return true;
  if(!walkable.has(map[ty][tx]))return true;
  // trees
  for(const t of trees){
    if(Math.hypot(t.x+8-x,t.y+16-y)<t.r)return true;
  }
  return false;
}
function moveEnt(ent,dx,dy,rad){
  const nx=ent.x+dx,ny=ent.y+dy;
  if(!solidAt(nx,ent.y))ent.x=nx;
  if(!solidAt(ent.x,ny))ent.y=ny;
  // clamp
  ent.x=Math.max(rad,Math.min(MAP_W*TILE-rad,ent.x));
  ent.y=Math.max(rad,Math.min(MAP_H*TILE-rad,ent.y));
}

// ─── Update ───
function update(){
  if(!gameActive)return;
  gameTime++;
  if(msgTimer>0){msgTimer-=1/60;if(msgTimer<=0)clearMsg()}
  if(attackAnim>0){attackAnim--;if(attackAnim<=0)player.attacking=false}
  if(bowCooldown>0)bowCooldown--;
  if(player.hurtTimer>0)player.hurtTimer--;

  if(dlgOpen||shopOpen)return;

  // Movement
  let dx=0,dy=0;
  if(keys['KeyW']||keys['ArrowUp'])dy-=1;
  if(keys['KeyS']||keys['ArrowDown'])dy+=1;
  if(keys['KeyA']||keys['ArrowLeft'])dx-=1;
  if(keys['KeyD']||keys['ArrowRight'])dx+=1;
  player.moving=dx!==0||dy!==0;
  player.sprint=keys['ShiftLeft']||keys['ShiftRight'];
  let spd=player.speed*(player.sprint?1.55:1)*(flags.accessories.boots?1.3:1);
  if(player.moving){
    const l=Math.hypot(dx,dy);dx/=l;dy/=l;
    moveEnt(player,dx*spd,dy*spd,10);
    if(Math.abs(dx)>Math.abs(dy))player.facing=dx>0?1:3;
    else player.facing=dy>0?2:0;
    player.anim+=player.sprint?.25:.15;
    if(gameTime%12===0)SFX.walk();
  }

  // Camera
  cam.x=player.x-W/2;cam.y=player.y-H/2;
  cam.x=Math.max(0,Math.min(MAP_W*TILE-W,cam.x));
  cam.y=Math.max(0,Math.min(MAP_H*TILE-H,cam.y));

  // Enemies AI
  for(const e of enemies){
    if(!e.alive)continue;
    e.anim+=.1;if(e.hurt>0)e.hurt--;
    e.cd=Math.max(0,e.cd-1);
    const dist=Math.hypot(e.x-player.x,e.y-player.y);
    const aggro=e.boss?280:160;
    if(dist<aggro&&dist>e.r+8){
      const a=Math.atan2(player.y-e.y,player.x-e.x);
      let sp=e.spd;
      // bats weave
      if(e.type==='bat'){sp*=1+Math.sin(e.anim)*.3}
      moveEnt(e,Math.cos(a)*sp,Math.sin(a)*sp,e.r*.5);
    }
    // attack
    if(dist<e.r+14&&e.cd<=0&&player.hurtTimer<=0){
      hurtPlayer(e.dmg);
      e.cd=e.boss?50:70;
    }
    // boss special
    if(e.boss&&e.cd===25&&dist<250){
      // ring shot
      for(let i=0;i<8;i++){
        const a=i*Math.PI/4+gameTime*.01;
        projectiles.push({x:e.x,y:e.y,vx:Math.cos(a)*3,vy:Math.sin(a)*3,life:90,friendly:false,dmg:2,r:7,col:'#a6f'});
      }
      SFX.shoot();
    }
  }

  // Projectiles
  for(let i=projectiles.length-1;i>=0;i--){
    const p=projectiles[i];
    p.x+=p.vx;p.y+=p.vy;p.life--;
    if(p.life<=0||solidAt(p.x,p.y)){projectiles.splice(i,1);continue}
    if(p.friendly){
      for(const e of enemies){
        if(!e.alive)continue;
        if(Math.hypot(e.x-p.x,e.y-p.y)<e.r+p.r){
          damageEnemy(e,p.dmg);projectiles.splice(i,1);break;
        }
      }
    }else{
      if(Math.hypot(player.x-p.x,player.y-p.y)<14+p.r){
        hurtPlayer(p.dmg);projectiles.splice(i,1);
        spawnParticles(p.x,p.y,'#a6f',8,2,20,3);
      }
    }
  }

  // Items pickup
  for(let i=items.length-1;i>=0;i--){
    const it=items[i];
    if(Math.hypot(it.x-player.x,it.y-player.y)<20+it.r){
      if(it.type==='coin'){flags.money+=it.v;SFX.coin();msg('+'+it.v+' coins');updateStatsUI();spawnParticles(it.x,it.y,'#fc0',8,2,25,3)}
      else if(it.type==='heart'){if(flags.hp<flags.maxHp){flags.hp=Math.min(flags.maxHp,flags.hp+1);SFX.heal();updateHealthUI();msg('+1 HP')}else{flags.money+=3;SFX.coin();msg('HP full — converted to coins')}spawnParticles(it.x,it.y,'#f44',8,2,25,3)}
      else if(it.type==='artifact'){winGame();spawnParticles(it.x,it.y,'#ffd700',30,4,60,5)}
      items.splice(i,1);
    }
  }

  updateParticles();
}

function winGame(){
  gameActive=false;
  SFX.win();
  flags.bossDefeated=true;
  setTimeout(()=>{
    document.getElementById('endScreen').style.display='flex';
    document.getElementById('endText').textContent=
      `You reclaimed the Ancient Artifact! Enemies felled: ${flags.killed}. Gold held: ${flags.money}. Weapon: ${WEAPONS[flags.weaponTier].name}. Max HP: ${flags.maxHp}. The realm is safe once more.`;
  },1500);
  msg('The Artifact is yours! Light returns to the land!',6);
}

// ─── Draw ───
const TILE_COLS={
  0:'#3d5c3a',1:'#2a2438',2:'#2a6a9a',3:'#4a7a3a',4:'#5a5548',
  5:'#6a3050',6:'#c2b280',7:'#d45a10',8:'#6a5030',9:'#3d5c3a'
};
const TILE_COLS2={
  0:'#355532',1:'#1e1a2a',2:'#1a5a8a',3:'#3a6a2a',4:'#4a493t',
  5:'#5a2840',6:'#b0a070',7:'#e06a18',8:'#5a4020',9:'#355532'
};

function draw(){
  ctx.fillStyle='#0d0d18';
  ctx.fillRect(0,0,W,H);

  const t0x=Math.floor(cam.x/TILE),t0y=Math.floor(cam.y/TILE);
  const t1x=Math.min(MAP_W-1,t0x+Math.ceil(W/TILE)+1);
  const t1y=Math.min(MAP_H-1,t0y+Math.ceil(H/TILE)+1);

  for(let ty=Math.max(0,t0y);ty<=t1y;ty++){
    for(let tx=Math.max(0,t0x);tx<=t1x;tx++){
      const t=map[ty][tx];
      const sx=tx*TILE-cam.x,sy=ty*TILE-cam.y;
      // base
      if(t===1){
        ctx.fillStyle='#252038';
        ctx.fillRect(sx,sy,TILE,TILE);
        ctx.fillStyle='#1a1628';
        ctx.fillRect(sx+2,sy+2,TILE-4,TILE-4);
        // brick lines
        ctx.strokeStyle='#322a48';ctx.lineWidth=1;
        ctx.beginPath();ctx.moveTo(sx,sy+TILE/2);ctx.lineTo(sx+TILE,sy+TILE/2);ctx.stroke();
        ctx.beginPath();ctx.moveTo(sx+TILE/2,sy);ctx.lineTo(sx+TILE/2,sy+TILE/2);ctx.stroke();
      }else if(t===2){
        const wave=Math.sin(gameTime*.05+tx*.5+ty*.3)*3;
        ctx.fillStyle='#1a5a8a';ctx.fillRect(sx,sy,TILE,TILE);
        ctx.fillStyle='#2a7aaa';
        ctx.beginPath();ctx.ellipse(sx+20,sy+20+wave,12,5,0,0,Math.PI*2);ctx.fill();
      }else if(t===7){
        ctx.fillStyle='#8a2008';ctx.fillRect(sx,sy,TILE,TILE);
        const flick=.5+Math.sin(gameTime*.1+tx+ty)*.5;
        ctx.fillStyle=`rgba(255,${100+flick*80},20,.7)`;
        ctx.beginPath();ctx.arc(sx+20,sy+20,10+flick*4,0,Math.PI*2);ctx.fill();
      }else if(t===5){
        ctx.fillStyle='#5a2848';ctx.fillRect(sx,sy,TILE,TILE);
        ctx.fillStyle='#7a3860';
        if((tx+ty)%2===0)ctx.fillRect(sx+4,sy+4,TILE-8,TILE-8);
      }else if(t===4){
        ctx.fillStyle='#4a4640';ctx.fillRect(sx,sy,TILE,TILE);
        ctx.fillStyle='#3a3630';ctx.fillRect(sx+1,sy+1,TILE-2,TILE-2);
      }else if(t===8){
        ctx.fillStyle='#6a5030';ctx.fillRect(sx,sy,TILE,TILE);
        ctx.strokeStyle='#3a2810';ctx.lineWidth=2;
        for(let i=0;i<4;i++){ctx.beginPath();ctx.moveTo(sx,sy+i*10+5);ctx.lineTo(sx+TILE,sy+i*10+5);ctx.stroke()}
      }else if(t===3){
        ctx.fillStyle='#3a6a32';ctx.fillRect(sx,sy,TILE,TILE);
        ctx.fillStyle='#4a8a3a';
        for(let i=0;i<5;i++){
          const gx=(tx*7+i*13)%TILE,gy=(ty*11+i*9)%TILE;
          ctx.fillRect(sx+gx,sy+gy,3,5);
        }
      }else if(t===9){
        ctx.fillStyle='#3d5c3a';ctx.fillRect(sx,sy,TILE,TILE);
        // flower
        ctx.fillStyle='#e68';
        ctx.beginPath();ctx.arc(sx+20,sy+18,5,0,Math.PI*2);ctx.fill();
        ctx.fillStyle='#ff8';ctx.beginPath();ctx.arc(sx+20,sy+18,2,0,Math.PI*2);ctx.fill();
      }else{
        // grass floor variation
        ctx.fillStyle=(tx+ty)%2===0?'#3a6338':'#356035';
        ctx.fillRect(sx,sy,TILE,TILE);
      }
    }
  }

  // Trees
  for(const t of trees){
    const sx=t.x-cam.x,sy=t.y-cam.y;
    if(sx<-40||sy<-60||sx>W+40||sy>H+40)continue;
    // trunk
    ctx.fillStyle='#4a3020';
    ctx.fillRect(sx+10,sy+20,10,18);
    // foliage
    ctx.fillStyle='#2a5a28';
    ctx.beginPath();ctx.arc(sx+15,sy+14,18,0,Math.PI*2);ctx.fill();
    ctx.fillStyle='#3a7a38';
    ctx.beginPath();ctx.arc(sx+10,sy+10,12,0,Math.PI*2);ctx.fill();
    ctx.beginPath();ctx.arc(sx+22,sy+12,11,0,Math.PI*2);ctx.fill();
  }

  // Save altars
  for(const s of saveSpots){
    const sx=s.x-cam.x,sy=s.y-cam.y;
    if(sx<-40||sy<-40||sx>W||sy>H)continue;
    const glow=.5+Math.sin(gameTime*.06)*.3;
    ctx.fillStyle='#3a3a50';
    ctx.fillRect(sx+4,sy+20,24,12);
    ctx.fillStyle=`rgba(120,255,160,${glow})`;
    ctx.beginPath();ctx.moveTo(sx+16,sy+2);ctx.lineTo(sx+28,sy+22);ctx.lineTo(sx+4,sy+22);ctx.closePath();ctx.fill();
    ctx.fillStyle=`rgba(200,255,220,${glow})`;
    ctx.beginPath();ctx.arc(sx+16,sy+14,4,0,Math.PI*2);ctx.fill();
  }

  // Chests
  for(const c of chests){
    const sx=c.x-cam.x,sy=c.y-cam.y;
    if(sx<-40||sy<-40||sx>W||sy>H)continue;
    if(c.open){
      ctx.fillStyle='#5a4030';ctx.fillRect(sx+4,sy+14,24,14);
      ctx.fillStyle='#3a2818';ctx.fillRect(sx+6,sy+8,20,8);
    }else{
      ctx.fillStyle='#8a6030';ctx.fillRect(sx+4,sy+10,24,18);
      ctx.fillStyle='#c9a84c';ctx.fillRect(sx+14,sy+16,4,6);
      ctx.fillStyle='#6a4020';ctx.fillRect(sx+4,sy+10,24,6);
      // sparkle
      if(gameTime%40<20){
        ctx.fillStyle='#ff8';ctx.fillRect(sx+18,sy+4,3,3);
      }
    }
  }

  // Items on ground
  for(const it of items){
    const sx=it.x-cam.x,sy=it.y-cam.y+Math.sin(gameTime*.08+it.x)*.0;
    const bob=Math.sin(gameTime*.1+it.x)*.0;
    if(it.type==='coin'){
      ctx.fillStyle='#fa0';ctx.beginPath();ctx.arc(sx,sy,7,0,Math.PI*2);ctx.fill();
      ctx.fillStyle='#fc6';ctx.beginPath();ctx.arc(sx-1,sy-1,3,0,Math.PI*2);ctx.fill();
    }else if(it.type==='heart'){
      ctx.fillStyle='#e33';
      ctx.beginPath();ctx.moveTo(sx,sy+4);ctx.bezierCurveTo(sx-10,sy-6,sx-6,sy-12,sx,sy-6);ctx.bezierCurveTo(sx+6,sy-12,sx+10,sy-6,sx,sy+4);ctx.fill();
    }else if(it.type==='artifact'){
      const g=.6+Math.sin(gameTime*.1)*.4;
      ctx.fillStyle=`rgba(255,215,0,${g})`;
      ctx.beginPath();
      ctx.moveTo(sx,sy-14);ctx.lineTo(sx+10,sy);ctx.lineTo(sx,sy+14);ctx.lineTo(sx-10,sy);ctx.closePath();
      ctx.fill();
      ctx.fillStyle='#fff';ctx.beginPath();ctx.arc(sx,sy,4,0,Math.PI*2);ctx.fill();
    }
  }

  // NPCs
  for(const n of npcs){
    const sx=n.x-cam.x,sy=n.y-cam.y;
    if(sx<-40||sy<-40||sx>W||sy>H)continue;
    drawChar(sx+12,sy+14,n.color,0,Math.sin(gameTime*.05)*.5,false,null,true);
    // name plate
    ctx.font='10px Georgia';ctx.fillStyle='#c9a84c';ctx.textAlign='center';
    ctx.fillText(n.name.split(' ')[0],sx+12,sy-4);
    // indicator
    if(Math.hypot(n.x+12-player.x,n.y+14-player.y)<55){
      ctx.fillStyle='#fff';ctx.font='12px Georgia';
      ctx.fillText('E',sx+12,sy-14);
    }
  }

  // Enemies
  for(const e of enemies){
    if(!e.alive)continue;
    const sx=e.x-cam.x,sy=e.y-cam.y;
    if(sx<-50||sy<-50||sx>W+50||sy>H+50)continue;
    drawEnemy(e,sx,sy);
  }

  // Projectiles
  for(const p of projectiles){
    const sx=p.x-cam.x,sy=p.y-cam.y;
    if(p.friendly){
      ctx.fillStyle='#ddd';
      ctx.save();ctx.translate(sx,sy);ctx.rotate(Math.atan2(p.vy,p.vx));
      ctx.fillRect(-8,-2,12,4);
      ctx.fillStyle='#8a5';ctx.fillRect(4,-3,6,6);
      ctx.restore();
    }else{
      ctx.fillStyle=p.col||'#a6f';
      ctx.beginPath();ctx.arc(sx,sy,p.r,0,Math.PI*2);ctx.fill();
      ctx.fillStyle='rgba(255,255,255,.4)';ctx.beginPath();ctx.arc(sx-1,sy-1,p.r*.4,0,Math.PI*2);ctx.fill();
    }
  }

  // Player
  drawPlayer();

  // Attack arc
  if(player.attacking&&attackAnim>0){
    const w=WEAPONS[flags.weaponTier];
    const ang=[ -Math.PI/2,0,Math.PI/2,Math.PI][player.facing];
    const progress=1-attackAnim/(w.speed/1000*60);
    const sweep=ang-0.9+progress*1.8;
    const px=player.x-cam.x,py=player.y-cam.y;
    ctx.strokeStyle=w.color;ctx.lineWidth=3;ctx.globalAlpha=.7;
    ctx.beginPath();ctx.arc(px,py,w.range*.7,sweep,sweep+.5);ctx.stroke();
    ctx.globalAlpha=1;
    // weapon line
    ctx.strokeStyle=w.color;ctx.lineWidth=4;
    ctx.beginPath();
    ctx.moveTo(px,py);
    ctx.lineTo(px+Math.cos(sweep+.25)*w.range*.75,py+Math.sin(sweep+.25)*w.range*.75);
    ctx.stroke();
    if(w.fire){
      spawnParticles(px+Math.cos(sweep+.25)*w.range*.6,py+Math.sin(sweep+.25)*w.range*.6+cam.y-cam.y,'#f80',1,1,15,2);
      const fx=player.x+Math.cos(sweep+.25)*w.range*.6;
      const fy=player.y+Math.sin(sweep+.25)*w.range*.6;
      if(gameTime%2===0)spawnParticles(fx,fy,'#f80',2,1.5,15,2);
    }
  }

  drawParticles(cam);

  // Hurt vignette
  if(player.hurtTimer>30){
    ctx.fillStyle=`rgba(180,0,0,${(player.hurtTimer-30)/40})`;
    ctx.fillRect(0,0,W,H);
  }

  // Minimap
  drawMinimap();
}

function drawChar(sx,sy,bodyCol,facing,walkPhase,hurt,weaponCol,npc){
  // shadow
  ctx.fillStyle='rgba(0,0,0,.3)';
  ctx.beginPath();ctx.ellipse(sx,sy+14,10,4,0,0,Math.PI*2);ctx.fill();
  // legs
  const legO=Math.sin(walkPhase)*4;
  ctx.fillStyle='#2a2040';
  ctx.fillRect(sx-6,sy+6,5,10+legO);
  ctx.fillRect(sx+1,sy+6,5,10-legO);
  // body
  ctx.fillStyle=hurt? '#fff':bodyCol;
  ctx.fillRect(sx-8,sy-8,16,16);
  // head
  ctx.fillStyle=hurt?'#fff':'#e8c8a0';
  ctx.beginPath();ctx.arc(sx,sy-14,8,0,Math.PI*2);ctx.fill();
  // eyes
  ctx.fillStyle='#222';
  const ex=facing===3?-2:facing===1?2:0;
  ctx.fillRect(sx-4+ex,sy-16,3,3);
  ctx.fillRect(sx+1+ex,sy-16,3,3);
  // weapon on back/side if any
  if(weaponCol){
    ctx.fillStyle=weaponCol;
    if(facing===1){ctx.fillRect(sx+8,sy-10,3,18);ctx.fillStyle='#aaa';ctx.fillRect(sx+6,sy-12,7,4)}
    else if(facing===3){ctx.fillRect(sx-11,sy-10,3,18);ctx.fillStyle='#aaa';ctx.fillRect(sx-13,sy-12,7,4)}
    else{ctx.fillRect(sx+9,sy-6,3,16);ctx.fillStyle='#aaa';ctx.fillRect(sx+7,sy-8,7,4)}
  }
}

function drawPlayer(){
  const sx=player.x-cam.x,sy=player.y-cam.y;
  const hurt=player.hurtTimer>0&&Math.floor(player.hurtTimer/3)%2===0;
  const w=WEAPONS[flags.weaponTier];
  // boots trail if accessory
  if(flags.accessories.boots&&player.moving&&gameTime%4===0){
    spawnParticles(player.x,player.y+10,'#8cf',2,1,15,2);
  }
  drawChar(sx,sy,hurt?'#fff':'#4a7ec8',player.facing,player.anim,hurt,w.color,false);
  // amulet glow
  if(flags.accessories.amulet){
    ctx.fillStyle=`rgba(255,100,120,${.3+Math.sin(gameTime*.1)*.2})`;
    ctx.beginPath();ctx.arc(sx,sy-2,5,0,Math.PI*2);ctx.fill();
  }
  // ring spark
  if(flags.accessories.ring&&gameTime%20===0){
    spawnParticles(player.x+8,player.y-4,'#fc0',1,1,20,2);
  }
  // bow on back
  if(flags.hasBow){
    ctx.strokeStyle='#8a5a30';ctx.lineWidth=2;
    ctx.beginPath();ctx.arc(sx-10,sy-4,10, -1,1);ctx.stroke();
  }
}

function drawEnemy(e,sx,sy){
  const hurt=e.hurt>0&&Math.floor(e.hurt/2)%2===0;
  const col=hurt?'#fff':e.color;
  ctx.fillStyle='rgba(0,0,0,.3)';
  ctx.beginPath();ctx.ellipse(sx,sy+e.r*.6,e.r*.7,e.r*.25,0,0,Math.PI*2);ctx.fill();

  if(e.type==='slime'){
    const squash=1+Math.sin(e.anim)*.1;
    ctx.fillStyle=col;
    ctx.beginPath();ctx.ellipse(sx,sy,e.r,e.r*squash,0,0,Math.PI*2);ctx.fill();
    ctx.fillStyle='#222';ctx.fillRect(sx-6,sy-4,4,4);ctx.fillRect(sx+2,sy-4,4,4);
  }else if(e.type==='wolf'){
    ctx.fillStyle=col;
    ctx.fillRect(sx-12,sy-8,24,16);
    // head
    ctx.fillRect(sx+8,sy-12,12,10);
    ctx.fillStyle='#222';ctx.fillRect(sx+14,sy-10,3,3);
    // legs
    ctx.fillStyle=col;
    const lo=Math.sin(e.anim*2)*3;
    ctx.fillRect(sx-10,sy+6,5,8+lo);ctx.fillRect(sx+4,sy+6,5,8-lo);
    // tail
    ctx.fillRect(sx-18,sy-4,8,4);
  }else if(e.type==='bat'){
    ctx.fillStyle=col;
    const wing=Math.sin(e.anim*3)*10;
    ctx.beginPath();ctx.ellipse(sx-12,sy+wing*.2,12,5+wing*.3, -0.3,0,Math.PI*2);ctx.fill();
    ctx.beginPath();ctx.ellipse(sx+12,sy-wing*.2,12,5-wing*.3, 0.3,0,Math.PI*2);ctx.fill();
    ctx.beginPath();ctx.arc(sx,sy,8,0,Math.PI*2);ctx.fill();
    ctx.fillStyle='#222';ctx.fillRect(sx-3,sy-2,2,2);ctx.fillRect(sx+1,sy-2,2,2);
  }else if(e.type==='golem'){
    ctx.fillStyle=col;
    ctx.fillRect(sx-16,sy-18,32,36);
    ctx.fillStyle=hurt?'#fff':'#9ab';
    ctx.fillRect(sx-12,sy-12,10,8);ctx.fillRect(sx+2,sy-12,10,8);
    ctx.fillStyle='#445';ctx.fillRect(sx-8,sy+4,16,6);
    // cracks
    ctx.strokeStyle='#345';ctx.lineWidth=2;
    ctx.beginPath();ctx.moveTo(sx-4,sy-18);ctx.lineTo(sx,sy);ctx.lineTo(sx+6,sy+18);ctx.stroke();
  }else if(e.type==='guardian'){
    ctx.fillStyle=col;
    ctx.beginPath();ctx.moveTo(sx,sy-20);ctx.lineTo(sx+16,sy+4);ctx.lineTo(sx+10,sy+16);ctx.lineTo(sx-10,sy+16);ctx.lineTo(sx-16,sy+4);ctx.closePath();ctx.fill();
    ctx.fillStyle='#222';ctx.beginPath();ctx.arc(sx,sy-4,4,0,Math.PI*2);ctx.fill();
    ctx.fillStyle='#f44';ctx.beginPath();ctx.arc(sx,sy-4,2,0,Math.PI*2);ctx.fill();
  }else if(e.type==='boss'){
    const pulse=1+Math.sin(gameTime*.08)*.05;
    ctx.fillStyle=col;
    ctx.save();ctx.translate(sx,sy);ctx.scale(pulse,pulse);
    // body
    ctx.beginPath();ctx.arc(0,0,e.r,0,Math.PI*2);ctx.fill();
    ctx.fillStyle='#60a';
    ctx.beginPath();ctx.arc(0,0,e.r*.7,0,Math.PI*2);ctx.fill();
    // eyes
    ctx.fillStyle='#f0f';
    ctx.beginPath();ctx.arc(-10,-6,6,0,Math.PI*2);ctx.arc(10,-6,6,0,Math.PI*2);ctx.fill();
    ctx.fillStyle='#200';
    ctx.beginPath();ctx.arc(-10,-6,3,0,Math.PI*2);ctx.arc(10,-6,3,0,Math.PI*2);ctx.fill();
    // crown
    ctx.fillStyle='#fd0';
    ctx.beginPath();ctx.moveTo(-14,-22);ctx.lineTo(-8,-34);ctx.lineTo(-2,-22);ctx.lineTo(4,-32);ctx.lineTo(10,-22);ctx.lineTo(14,-22);ctx.lineTo(14,-18);ctx.lineTo(-14,-18);ctx.closePath();ctx.fill();
    ctx.restore();
    // aura
    ctx.strokeStyle=`rgba(160,80,255,${.3+Math.sin(gameTime*.1)*.2})`;
    ctx.lineWidth=3;
    ctx.beginPath();ctx.arc(sx,sy,e.r+8+Math.sin(gameTime*.1)*4,0,Math.PI*2);ctx.stroke();
  }

  // HP bar for damaged / boss
  if(e.hp<e.maxHp||e.boss){
    const bw=e.r*2;
    ctx.fillStyle='#300';ctx.fillRect(sx-bw/2,sy-e.r-12,bw,5);
    ctx.fillStyle=e.boss?'#a0f':'#e33';
    ctx.fillRect(sx-bw/2,sy-e.r-12,bw*(e.hp/e.maxHp),5);
  }
}

function drawMinimap(){
  const mw=120,mh=90;
  mctx.fillStyle='rgba(0,0,0,.7)';
  mctx.fillRect(0,0,mw,mh);
  const sx=mw/MAP_W,sy=mh/MAP_H;
  // explored region around player roughly
  const px=Math.floor(player.x/TILE),py=Math.floor(player.y/TILE);
  for(let y=0;y<MAP_H;y++){
    for(let x=0;x<MAP_W;x++){
      if(Math.abs(x-px)>18||Math.abs(y-py)>14)continue;
      const t=map[y][x];
      if(t===1)mctx.fillStyle='#333';
      else if(t===2)mctx.fillStyle='#248';
      else if(t===4||t===5)mctx.fillStyle='#555';
      else if(t===7)mctx.fillStyle='#a40';
      else mctx.fillStyle='#2a4a28';
      mctx.fillRect(x*sx,y*sy,sx+0.5,sy+0.5);
    }
  }
  // player
  mctx.fillStyle='#4af';
  mctx.fillRect(px*sx-1,py*sy-1,3,3);
  // boss
  for(const e of enemies){
    if(e.boss&&e.alive){
      mctx.fillStyle='#f4f';
      mctx.fillRect(Math.floor(e.x/TILE)*sx-1,Math.floor(e.y/TILE)*sy-1,3,3);
    }
  }
  // save spots
  mctx.fillStyle='#8f8';
  for(const s of saveSpots){
    mctx.fillRect(Math.floor(s.x/TILE)*sx,Math.floor(s.y/TILE)*sy,2,2);
  }
}

// ─── Loop ───
let last=performance.now();
function loop(now){
  const dt=Math.min(50,now-last);last=now;
  update();
  draw();
  requestAnimationFrame(loop);
}

// ─── Start ───
document.getElementById('startBtn').onclick=()=>{
  if(Actx.state==='suspended')Actx.resume();
  document.getElementById('titleScreen').style.display='none';
  // try load?
  const has=localStorage.getItem('artifactQuestSave');
  if(has&&confirm('Continue from save?')){
    if(!loadSave())initGame();
  }else initGame();
  requestAnimationFrame(loop);
};
document.getElementById('reBtn').onclick=()=>{
  document.getElementById('endScreen').style.display='none';
  try{localStorage.removeItem('artifactQuestSave')}catch(e){}
  initGame();
  gameActive=true;
};
document.getElementById('shopClose').onclick=closeShop;

// Pre-warm title particles nothing — show canvas dark
ctx.fillStyle='#0d0d18';ctx.fillRect(0,0,W,H);
ctx.fillStyle='#c9a84c';ctx.font='20px Georgia';ctx.textAlign='center';
ctx.fillText('The Artifact Quest',W/2,H/2);
</script>
</body>
</html>
<!-- agent-meta {"model":"x-ai/grok-4.5","provider":"openrouter","persona":"basic","sessionId":"dd65e2ee-c7c1-4cc9-ad10-cd92d3a13c03","tokensIn":928,"tokensOut":20609,"tokensTotal":21537,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-07-18T16:12:12.092Z"} -->