RPG
Grok 4.5 · basic

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"} -->