← RPG results

RPG

Claude Opus 4.8 · 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 28,085 bytes · SHA-256 48c2888c9594
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Emberlight — Action Adventure RPG</title>
<style>
  * { margin:0; padding:0; box-sizing:border-box; }
  html,body { height:100%; background:#0b0e14; overflow:hidden; font-family:'Courier New',monospace; color:#e8e8e8; }
  #game { display:block; margin:0 auto; background:#1a1e28; image-rendering:pixelated; touch-action:none; }
  #wrap { display:flex; flex-direction:column; align-items:center; height:100%; justify-content:center; }
  #hud { position:fixed; top:0; left:0; width:100%; pointer-events:none; padding:10px; font-size:13px; }
  #hud .row { display:flex; justify-content:space-between; align-items:flex-start; }
  #hearts { font-size:22px; letter-spacing:2px; }
  #msg { position:fixed; bottom:16px; left:50%; transform:translateX(-50%); background:rgba(0,0,0,.75); padding:8px 16px; border:1px solid #555; border-radius:6px; font-size:13px; opacity:0; transition:opacity .3s; text-align:center; }
  #inv { position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(5,8,14,.94); display:none; flex-direction:column; align-items:center; justify-content:center; z-index:10; }
  #inv h2 { color:#f5c26b; margin-bottom:14px; letter-spacing:3px; }
  .grid { display:grid; grid-template-columns:repeat(3,150px); gap:10px; }
  .slot { background:#1c2230; border:1px solid #3a4460; padding:10px; border-radius:6px; font-size:12px; min-height:70px; }
  .slot b { color:#f5c26b; }
  #shop { position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(5,8,14,.94); display:none; flex-direction:column; align-items:center; justify-content:center; z-index:11; }
  #shop h2 { color:#7bd88f; margin-bottom:14px; letter-spacing:3px; }
  .buy { background:#1c2230; border:1px solid #3a4460; padding:10px 14px; margin:5px; border-radius:6px; cursor:pointer; width:340px; display:flex; justify-content:space-between; font-size:13px; pointer-events:auto; }
  .buy:hover { border-color:#7bd88f; background:#232b3d; }
  .buy .cost { color:#f5c26b; }
  #inv .hint,#shop .hint { margin-top:16px; color:#888; font-size:11px; }
  #title { position:fixed; inset:0; background:#0b0e14; z-index:20; display:flex; flex-direction:column; align-items:center; justify-content:center; text-align:center; }
  #title h1 { font-size:48px; color:#f5c26b; letter-spacing:6px; text-shadow:0 0 20px #8b5a2b; }
  #title p { margin-top:16px; color:#aaa; max-width:520px; line-height:1.6; font-size:14px; }
  #title button { margin-top:26px; padding:12px 30px; background:#8b5a2b; color:#fff; border:none; border-radius:6px; font-size:16px; cursor:pointer; letter-spacing:2px; }
  #title button:hover { background:#a86a33; }
  .ctrl { color:#6b7690; font-size:11px; margin-top:8px; }
  #prog { color:#8fa3c8; }
</style>
</head>
<body>
<div id="wrap">
  <canvas id="game" width="640" height="480"></canvas>
</div>

<div id="hud">
  <div class="row">
    <div>
      <div id="hearts"></div>
      <div id="prog"></div>
    </div>
    <div style="text-align:right">
      <div id="money" style="color:#f5c26b">◈ 0</div>
      <div id="weap"></div>
    </div>
  </div>
</div>

<div id="msg"></div>

<div id="inv">
  <h2>INVENTORY</h2>
  <div class="grid" id="invGrid"></div>
  <div class="hint">Press [I] to close · Press number keys 1-9 to USE consumables</div>
</div>

<div id="shop">
  <h2>MERCHANT</h2>
  <div id="shopList"></div>
  <div class="hint">Click to buy · Press [E] to leave</div>
</div>

<div id="title">
  <h1>EMBERLIGHT</h1>
  <p>You awaken in the ruined Vale of Ash. Explore four regions, defeat the shades, gather relics, upgrade your blade, and reach the Sunspire before the dark consumes it all.</p>
  <div class="ctrl">WASD / Arrows — Move&nbsp;&nbsp;·&nbsp;&nbsp;J / Space — Attack&nbsp;&nbsp;·&nbsp;&nbsp;K — Ranged&nbsp;&nbsp;·&nbsp;&nbsp;E — Interact&nbsp;&nbsp;·&nbsp;&nbsp;I — Inventory</div>
  <button id="startBtn">BEGIN ADVENTURE</button>
</div>

<script>
// ============ AUDIO ============
const AC = window.AudioContext || window.webkitAudioContext;
let actx = null;
function initAudio(){ if(!actx) actx = new AC(); }
function tone(freq, dur, type='square', vol=0.15, slide=0){
  if(!actx) return;
  const o=actx.createOscillator(), g=actx.createGain();
  o.type=type; o.frequency.value=freq;
  if(slide) o.frequency.exponentialRampToValueAtTime(Math.max(20,freq+slide), actx.currentTime+dur);
  g.gain.setValueAtTime(vol, actx.currentTime);
  g.gain.exponentialRampToValueAtTime(0.001, actx.currentTime+dur);
  o.connect(g); g.connect(actx.destination);
  o.start(); o.stop(actx.currentTime+dur);
}
function noise(dur, vol=0.2){
  if(!actx) return;
  const b=actx.createBuffer(1, actx.sampleRate*dur, actx.sampleRate);
  const d=b.getChannelData(0);
  for(let i=0;i<d.length;i++) d[i]=(Math.random()*2-1)*(1-i/d.length);
  const s=actx.createBufferSource(), g=actx.createGain();
  s.buffer=b; g.gain.value=vol; s.connect(g); g.connect(actx.destination); s.start();
}
const SFX={
  step(){ tone(90,0.05,'sine',0.05); },
  attack(){ tone(320,0.08,'square',0.12,-180); },
  attack2(){ tone(500,0.06,'sawtooth',0.12,-300); noise(0.06,0.08); },
  ranged(){ tone(700,0.12,'triangle',0.12,-400); },
  hit(){ noise(0.08,0.15); tone(160,0.08,'square',0.1,-80); },
  hurt(){ tone(200,0.2,'sawtooth',0.2,-150); noise(0.1,0.2); },
  pickup(){ tone(660,0.08,'sine',0.12); setTimeout(()=>tone(990,0.1,'sine',0.12),70); },
  shop(){ tone(440,0.06,'triangle',0.1); setTimeout(()=>tone(550,0.06,'triangle',0.1),60); setTimeout(()=>tone(770,0.1,'triangle',0.1),120); },
  save(){ tone(523,0.1,'sine',0.12); setTimeout(()=>tone(784,0.15,'sine',0.12),100); },
  upgrade(){ [523,659,784,1047].forEach((f,i)=>setTimeout(()=>tone(f,0.15,'triangle',0.14),i*90)); },
  die(){ tone(300,0.3,'sawtooth',0.15,-250); }
};

// ============ GAME STATE ============
const cv=document.getElementById('game'), ctx=cv.getContext('2d');
const W=640,H=480;
ctx.imageSmoothingEnabled=false;

const keys={};
window.addEventListener('keydown',e=>{ keys[e.key.toLowerCase()]=true; handleKey(e); });
window.addEventListener('keyup',e=>{ keys[e.key.toLowerCase()]=false; });

let game=null;

// Weapon definitions
const WEAPONS=[
  {name:'Rusted Blade', dmg:2, speed:22, range:34, arc:1.1, color:'#9a8f7a'},
  {name:'Ember Sword', dmg:4, speed:16, range:40, arc:1.3, color:'#ff8a3c'},   // upgrade 1
  {name:'Storm Glaive', dmg:6, speed:10, range:52, arc:1.6, color:'#6bd0ff'}    // upgrade 2
];

// Item catalog
const SHOP_ITEMS=[
  {id:'potion', name:'Health Potion', desc:'Restore 2 HP', cost:15, type:'consumable'},
  {id:'apple',  name:'Golden Apple', desc:'Restore 1 HP', cost:8, type:'consumable'},
  {id:'boots',  name:'Swift Boots', desc:'+30% move speed (worn)', cost:30, type:'accessory'},
  {id:'amulet', name:'Ember Amulet', desc:'+1 weapon damage (worn)', cost:40, type:'accessory'},
  {id:'quiver', name:'Deep Quiver', desc:'+3 max arrows (worn)', cost:25, type:'accessory'}
];

function newGame(){
  game={
    px:80, py:240, pdir:1, vx:0, vy:0,
    maxHP:6, hp:6,               // 6 half-points = 3 hearts. max later 10 = 5 hearts
    hpCap:6,
    weaponTier:0,
    money:0,
    hasRanged:false, arrows:0, maxArrows:6,
    inv:{},                       // id -> count
    worn:{boots:false, amulet:false, quiver:false},
    region:0, regionsCleared:[false,false,false,false],
    attackTimer:0, attackCd:0, hurtTimer:0, walkAnim:0,
    stepTimer:0, invincible:0,
    projectiles:[], particles:[], enemies:[], items:[],
    savePoint:null, saved:false,
    won:false, dead:false,
    progress:0 // 0-100
  };
  buildRegion(0);
}

// Regions layout: 0 forest, 1 caverns, 2 ruins, 3 sunspire
const REGION_NAMES=['Vale of Ash','Echoing Caverns','Broken Ruins','The Sunspire'];

function buildRegion(r){
  game.region=r; game.enemies=[]; game.items=[]; game.projectiles=[]; game.particles=[];
  game.px=70; game.py=240;
  const cleared = game.regionsCleared[r];

  const spawnEnemy=(x,y,hp,type)=>game.enemies.push({x,y,hp,maxhp:hp,type,vx:0,vy:0,hurt:0,dead:false,anim:Math.random()*6.28});
  const spawnItem=(x,y,kind,data)=>game.items.push({x,y,kind,data,bob:Math.random()*6.28,taken:false});

  if(r===0){
    if(!cleared){
      spawnEnemy(280,150,4,'shade'); spawnEnemy(400,320,4,'shade'); spawnEnemy(500,180,6,'shade');
    }
    spawnItem(200,340,'coin',{v:10});
    spawnItem(340,120,'item',{id:'apple'});
    spawnItem(540,360,'save',{});
    // gate to next region (exit) top-right
  } else if(r===1){
    if(!cleared){
      spawnEnemy(250,300,6,'brute'); spawnEnemy(420,160,6,'shade'); spawnEnemy(520,300,8,'brute');
    }
    spawnItem(150,150,'coin',{v:15});
    spawnItem(360,360,'weapon',{tier:1}); // Ember Sword upgrade
    spawnItem(300,120,'save',{});
    if(!game.inv['bomb']) spawnItem(500,120,'item',{id:'bomb'});
  } else if(r===2){
    // Mid-game: ranged weapon discovery
    if(!cleared){
      spawnEnemy(220,140,8,'wraith'); spawnEnemy(400,340,8,'wraith'); spawnEnemy(520,200,10,'brute');
    }
    if(!game.hasRanged) spawnItem(320,240,'ranged',{});
    spawnItem(180,360,'coin',{v:20});
    spawnItem(480,120,'item',{id:'scroll'});
    spawnItem(560,340,'save',{});
    spawnItem(120,240,'heart',{}); // heart container +2 max
  } else if(r===3){
    if(!cleared){
      spawnEnemy(320,180,14,'boss');
    }
    spawnItem(200,360,'coin',{v:25});
    spawnItem(120,120,'save',{});
    spawnItem(500,360,'weapon',{tier:2}); // Storm Glaive
  }
  updateHUD();
}

// Merchant appears in region 0 corner
function inShopZone(){ return game.region===0 && game.px>560 && game.py<120; }

// ============ INPUT HANDLERS ============
let invOpen=false, shopOpen=false;
function handleKey(e){
  const k=e.key.toLowerCase();
  if(!game) return;
  if(k==='i'){ toggleInv(); }
  if(k==='e'){
    if(shopOpen){ closeShop(); return; }
    if(inShopZone()){ openShop(); return; }
    interact();
  }
  // use consumables by number
  if(/^[1-9]$/.test(k)){ useConsumable(parseInt(k)-1); }
}

function toggleInv(){
  invOpen=!invOpen;
  document.getElementById('inv').style.display = invOpen?'flex':'none';
  if(invOpen) renderInv();
}

function useConsumable(idx){
  const cons=Object.keys(game.inv).filter(id=>{
    const s=SHOP_ITEMS.find(x=>x.id===id); return (s&&s.type==='consumable')||id==='bomb'||id==='scroll';
  });
  const id=cons[idx];
  if(!id||game.inv[id]<=0) return;
  if(id==='potion'){ heal(2); }
  else if(id==='apple'){ heal(1); }
  else if(id==='scroll'){ // temporary damage buff visual
    heal(2); flash('The scroll surges with warmth (+2 HP)!');
  }
  else if(id==='bomb'){ // damage nearby enemies
    game.enemies.forEach(en=>{ if(!en.dead){ const d=Math.hypot(en.x-game.px,en.y-game.py); if(d<90){ hurtEnemy(en,6,en.x,en.y);} }});
    burst(game.px,game.py,'#ff6a2b',26); SFX.hit(); flash('BOOM! The bomb detonates!');
  }
  game.inv[id]--; if(game.inv[id]<=0) delete game.inv[id];
  SFX.pickup();
  renderInv(); updateHUD();
}

function heal(pts){ game.hp=Math.min(game.hpCap, game.hp+pts*2); burst(game.px,game.py,'#7bd88f',10); }

function interact(){
  // pick nearest save/interactable within range handled by items; also save point
  for(const it of game.items){
    if(it.taken) continue;
    const d=Math.hypot(it.x-game.px, it.y-game.py);
    if(d<40 && it.kind==='save'){
      game.savePoint={region:game.region, x:it.x, y:it.y};
      game.saved=true; SFX.save();
      burst(it.x,it.y,'#6bd0ff',20);
      flash('Progress saved at this shrine.');
      return;
    }
  }
}

// ============ SHOP ============
function openShop(){
  shopOpen=true; document.getElementById('shop').style.display='flex'; SFX.shop();
  renderShop();
}
function closeShop(){ shopOpen=false; document.getElementById('shop').style.display='none'; }
function renderShop(){
  const list=document.getElementById('shopList'); list.innerHTML='';
  SHOP_ITEMS.forEach(it=>{
    const owned = it.type==='accessory' && game.worn[it.id];
    const div=document.createElement('div'); div.className='buy';
    div.innerHTML=`<span><b>${it.name}</b> — ${it.desc}${owned?' <i style="color:#7bd88f">(owned)</i>':''}</span><span class="cost">◈ ${it.cost}</span>`;
    div.onclick=()=>{
      if(owned) return;
      if(game.money>=it.cost){
        game.money-=it.cost; SFX.shop();
        if(it.type==='accessory'){ game.worn[it.id]=true; if(it.id==='quiver'){ game.maxArrows+=3; } }
        else { game.inv[it.id]=(game.inv[it.id]||0)+1; }
        flash('Purchased '+it.name+'!');
        renderShop(); updateHUD();
      } else { flash('Not enough coin.'); SFX.hurt(); }
    };
    list.appendChild(div);
  });
}

// ============ INVENTORY RENDER ============
function renderInv(){
  const g=document.getElementById('invGrid'); g.innerHTML='';
  const entries=[];
  entries.push({t:`<b>Weapon</b><br>${WEAPONS[game.weaponTier].name}<br>DMG ${WEAPONS[game.weaponTier].dmg+(game.worn.amulet?1:0)}`});
  if(game.hasRanged) entries.push({t:`<b>Ranged Bow</b><br>Arrows: ${game.arrows}/${game.maxArrows}`});
  Object.keys(game.worn).forEach(w=>{ if(game.worn[w]){ const s=SHOP_ITEMS.find(x=>x.id===w); entries.push({t:`<b>${s.name}</b><br>${s.desc}`}); }});
  let num=1;
  Object.keys(game.inv).forEach(id=>{
    const s=SHOP_ITEMS.find(x=>x.id===id);
    const nm = s?s.name : (id==='bomb'?'Fire Bomb':id==='scroll'?'Vital Scroll':id);
    const desc = s?s.desc : (id==='bomb'?'AoE damage nearby':'Restores 2 HP');
    entries.push({t:`<b>[${num}] ${nm}</b> x${game.inv[id]}<br>${desc}`});
    num++;
  });
  if(entries.length===0) entries.push({t:'Empty — go explore!'});
  entries.forEach(e=>{ const d=document.createElement('div'); d.className='slot'; d.innerHTML=e.t; g.appendChild(d); });
}

// ============ HUD ============
function updateHUD(){
  // hearts: 2 pts each
  const hearts=Math.ceil(game.hpCap/2);
  let html='';
  for(let i=0;i<hearts;i++){
    const v=game.hp - i*2;
    if(v>=2) html+='<span style="color:#ff4d5e">♥</span>';
    else if(v===1) html+='<span style="color:#ff9aa4">♥</span>';
    else html+='<span style="color:#3a2530">♥</span>';
  }
  document.getElementById('hearts').innerHTML=html;
  document.getElementById('money').textContent='◈ '+game.money;
  let w=WEAPONS[game.weaponTier].name+' (DMG '+(WEAPONS[game.weaponTier].dmg+(game.worn.amulet?1:0))+')';
  if(game.hasRanged) w+=' · ⤳'+game.arrows;
  document.getElementById('weap').textContent=w;
  const pct = Math.round((game.region + (game.regionsCleared[game.region]?1:0))/4*100);
  document.getElementById('prog').textContent=REGION_NAMES[game.region]+'  —  '+pct+'% explored';
  game.progress=pct;
}

let msgTimer=0;
function flash(t){ const m=document.getElementById('msg'); m.textContent=t; m.style.opacity=1; msgTimer=180; }

// ============ PARTICLES ============
function burst(x,y,color,n){
  for(let i=0;i<n;i++){
    const a=Math.random()*6.28, s=1+Math.random()*3;
    game.particles.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,life:20+Math.random()*15,color,size:2+Math.random()*2});
  }
}

// ============ COMBAT ============
function hurtEnemy(en,dmg,fx,fy){
  en.hp-=dmg; en.hurt=8;
  burst(fx,fy,'#ffcf5c',6); SFX.hit();
  const kb=6; const ang=Math.atan2(en.y-game.py,en.x-game.px);
  en.vx+=Math.cos(ang)*kb; en.vy+=Math.sin(ang)*kb;
  if(en.hp<=0 && !en.dead){
    en.dead=true; burst(en.x,en.y,'#a06bff',16);
    game.money += en.type==='boss'?60:(en.type==='brute'||en.type==='wraith'?12:6);
    checkRegionClear();
  }
}

function checkRegionClear(){
  const alive=game.enemies.some(e=>!e.dead);
  if(!alive && !game.regionsCleared[game.region]){
    game.regionsCleared[game.region]=true;
    flash('Region cleared! The path onward opens.');
    game.money+=10;
  }
  updateHUD();
}

function playerHurt(dmg){
  if(game.invincible>0) return;
  game.hp-=dmg; game.hurtTimer=20; game.invincible=60;
  SFX.hurt(); burst(game.px,game.py,'#ff4d5e',12);
  updateHUD();
  if(game.hp<=0){ playerDie(); }
}

function playerDie(){
  game.dead=true; SFX.die();
  flash('You have fallen... respawning at last shrine.');
  setTimeout(()=>{
    if(game.savePoint){
      buildRegion(game.savePoint.region);
      game.px=game.savePoint.x; game.py=game.savePoint.y;
    } else { buildRegion(0); }
    game.hp=game.hpCap; game.dead=false; game.invincible=90;
    updateHUD();
  },1500);
}

// ============ UPDATE ============
function update(){
  if(!game || invOpen || shopOpen || game.won || game.dead) return;

  const wp=WEAPONS[game.weaponTier];
  let spd = game.worn.boots?2.9:2.2;
  let mx=0,my=0;
  if(keys['a']||keys['arrowleft']){ mx-=1; game.pdir=-1; }
  if(keys['d']||keys['arrowright']){ mx+=1; game.pdir=1; }
  if(keys['w']||keys['arrowup']) my-=1;
  if(keys['s']||keys['arrowdown']) my+=1;
  const moving = mx||my;
  if(moving){ const l=Math.hypot(mx,my); mx/=l; my/=l; game.walkAnim+=0.3;
    game.stepTimer--; if(game.stepTimer<=0){ SFX.step(); game.stepTimer=14; }
  } else game.walkAnim=0;
  game.px=Math.max(20,Math.min(W-20,game.px+mx*spd));
  game.py=Math.max(30,Math.min(H-20,game.py+my*spd));

  // attack
  if(game.attackCd>0) game.attackCd--;
  if((keys['j']||keys[' ']) && game.attackCd<=0){
    game.attackTimer=10; game.attackCd=wp.speed;
    game.weaponTier===0?SFX.attack():SFX.attack2();
    // hit enemies in arc
    const dmg=wp.dmg+(game.worn.amulet?1:0);
    game.enemies.forEach(en=>{ if(en.dead) return;
      const dx=en.x-game.px, dy=en.y-game.py, d=Math.hypot(dx,dy);
      if(d<wp.range && (game.pdir>0?dx>-10:dx<10)){ hurtEnemy(en,dmg,en.x,en.y); }
    });
    burst(game.px+game.pdir*wp.range*0.6, game.py, wp.color, 4);
  }
  if(game.attackTimer>0) game.attackTimer--;

  // ranged
  if(keys['k'] && game.hasRanged && game.arrows>0 && game.attackCd<=0){
    game.attackCd=14; game.arrows--; SFX.ranged();
    game.projectiles.push({x:game.px+game.pdir*14,y:game.py,vx:game.pdir*7,vy:0,life:60});
    updateHUD();
  }
  keys['k']=false; keys['j']=false;

  // projectiles
  game.projectiles.forEach(p=>{ p.x+=p.vx; p.y+=p.vy; p.life--;
    game.enemies.forEach(en=>{ if(!en.dead && Math.hypot(en.x-p.x,en.y-p.y)<18){ hurtEnemy(en,5,en.x,en.y); p.life=0; }});
  });
  game.projectiles=game.projectiles.filter(p=>p.life>0 && p.x>0 && p.x<W);

  // enemies AI
  game.enemies.forEach(en=>{ if(en.dead) return;
    en.anim+=0.1;
    if(en.hurt>0) en.hurt--;
    const dx=game.px-en.x, dy=game.py-en.y, d=Math.hypot(dx,dy);
    let es = en.type==='boss'?1.5:(en.type==='brute'?1.0:1.4);
    if(d>26){ en.vx+=dx/d*0.15*es; en.vy+=dy/d*0.15*es; }
    en.vx*=0.85; en.vy*=0.85;
    en.x+=en.vx; en.y+=en.vy;
    en.x=Math.max(15,Math.min(W-15,en.x)); en.y=Math.max(25,Math.min(H-15,en.y));
    if(d<26){ playerHurt(en.type==='boss'?2:1); }
  });

  if(game.invincible>0) game.invincible--;
  if(game.hurtTimer>0) game.hurtTimer--;

  // items pickup
  game.items.forEach(it=>{ if(it.taken) return; it.bob+=0.08;
    const d=Math.hypot(it.x-game.px, it.y-game.py);
    if(d<26 && it.kind!=='save'){
      if(it.kind==='coin'){ game.money+=it.data.v; it.taken=true; SFX.pickup(); burst(it.x,it.y,'#f5c26b',8); flash('+◈'+it.data.v); }
      else if(it.kind==='item'){ game.inv[it.data.id]=(game.inv[it.data.id]||0)+1; it.taken=true; SFX.pickup(); burst(it.x,it.y,'#7bd88f',8);
        const nm=(SHOP_ITEMS.find(x=>x.id===it.data.id)||{}).name || (it.data.id==='bomb'?'Fire Bomb':it.data.id==='scroll'?'Vital Scroll':it.data.id);
        flash('Found '+nm+'!'); }
      else if(it.kind==='weapon'){ if(it.data.tier>game.weaponTier){ game.weaponTier=it.data.tier; SFX.upgrade(); burst(it.x,it.y,'#ff8a3c',22); flash('WEAPON UPGRADE: '+WEAPONS[it.data.tier].name+'!'); } it.taken=true; }
      else if(it.kind==='ranged'){ game.hasRanged=true; game.arrows=game.maxArrows; it.taken=true; SFX.upgrade(); burst(it.x,it.y,'#6bd0ff',22); flash('Discovered the ANCIENT BOW! Press K to fire.'); }
      else if(it.kind==='heart'){ game.hpCap=Math.min(10,game.hpCap+2); game.hp=game.hpCap; it.taken=true; SFX.upgrade(); burst(it.x,it.y,'#ff4d5e',20); flash('Heart Container! Max HP increased.'); }
      updateHUD();
    }
  });

  // refill arrows near save
  // region transitions: right edge if cleared
  if(game.regionsCleared[game.region] && game.px>W-24){
    if(game.region<3){ buildRegion(game.region+1); game.arrows=Math.min(game.maxArrows,game.arrows+2); flash('Entering '+REGION_NAMES[game.region]+'...'); }
    else { winGame(); }
  }
  if(game.px<24 && game.region>0){ // go back
    // allow backtrack
    if(keys['s']===false){} // no-op
  }

  // particles
  game.particles.forEach(p=>{ p.x+=p.vx; p.y+=p.vy; p.vx*=0.94; p.vy*=0.94; p.life--; });
  game.particles=game.particles.filter(p=>p.life>0);

  if(msgTimer>0){ msgTimer--; if(msgTimer===0) document.getElementById('msg').style.opacity=0; }
}

function winGame(){
  game.won=true; SFX.upgrade();
  flash('The Sunspire is relit. You are the Emberlight. VICTORY!');
}

// ============ RENDER ============
function draw(){
  // background per region
  const bgs=['#1c2a1e','#16171f','#2a2320','#2a2438'];
  ctx.fillStyle=bgs[game?game.region:0]; ctx.fillRect(0,0,W,H);
  if(!game) return;

  // ground grid decor
  ctx.strokeStyle='rgba(255,255,255,0.03)';
  for(let x=0;x<W;x+=40){ ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,H); ctx.stroke(); }
  for(let y=0;y<H;y+=40){ ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(W,y); ctx.stroke(); }

  // exit portal if cleared
  if(game.regionsCleared[game.region]){
    ctx.fillStyle='rgba(107,208,255,'+(0.3+0.2*Math.sin(Date.now()/200))+')';
    ctx.fillRect(W-16,H/2-40,16,80);
    ctx.fillStyle='#6bd0ff'; ctx.font='10px monospace'; ctx.fillText(game.region<3?'EXIT →':'SUNSPIRE →', W-88, H/2-46);
  }

  // shop marker in region 0
  if(game.region===0){
    ctx.fillStyle='#8b5a2b'; ctx.fillRect(578,60,44,44);
    ctx.fillStyle='#f5c26b'; ctx.font='16px monospace'; ctx.fillText('◈',593,90);
    ctx.font='9px monospace'; ctx.fillStyle='#f5c26b'; ctx.fillText(inShopZone()?'[E] SHOP':'SHOP',580,54);
  }

  // items
  game.items.forEach(it=>{ if(it.taken) return;
    const by=Math.sin(it.bob)*3;
    if(it.kind==='coin'){ ctx.fillStyle='#f5c26b'; ctx.beginPath(); ctx.arc(it.x,it.y+by,7,0,6.28); ctx.fill(); ctx.fillStyle='#a8792b'; ctx.font='9px monospace'; ctx.fillText('◈',it.x-4,it.y+by+3); }
    else if(it.kind==='save'){ ctx.fillStyle='#6bd0ff'; ctx.fillRect(it.x-8,it.y-14+by,16,24); ctx.fillStyle='#bfe9ff'; ctx.fillRect(it.x-3,it.y-10+by,6,10);
      ctx.font='9px monospace'; ctx.fillStyle='#6bd0ff'; if(Math.hypot(it.x-game.px,it.y-game.py)<40) ctx.fillText('[E] SAVE',it.x-24,it.y-20+by); }
    else if(it.kind==='weapon'){ ctx.fillStyle=WEAPONS[it.data.tier].color; ctx.save(); ctx.translate(it.x,it.y+by); ctx.rotate(Math.PI/4); ctx.fillRect(-2,-12,4,20); ctx.fillStyle='#fff'; ctx.fillRect(-5,6,10,3); ctx.restore(); glow(it.x,it.y,WEAPONS[it.data.tier].color); }
    else if(it.kind==='ranged'){ ctx.strokeStyle='#6bd0ff'; ctx.lineWidth=3; ctx.beginPath(); ctx.arc(it.x,it.y+by,10,-1,1); ctx.stroke(); ctx.lineWidth=1; glow(it.x,it.y,'#6bd0ff'); }
    else if(it.kind==='heart'){ drawHeart(it.x,it.y+by,10,'#ff4d5e'); glow(it.x,it.y,'#ff4d5e'); }
    else if(it.kind==='item'){ ctx.fillStyle='#7bd88f'; ctx.fillRect(it.x-7,it.y-7+by,14,14); ctx.strokeStyle='#3f6b48'; ctx.strokeRect(it.x-7,it.y-7+by,14,14); }
  });

  // enemies
  game.enemies.forEach(en=>{ if(en.dead) return;
    const cols={shade:'#7a5fb5',brute:'#b5654f',wraith:'#4fb59a',boss:'#c23a5e'};
    const c = en.hurt>0?'#ffffff':cols[en.type];
    const wob=Math.sin(en.anim)*2;
    const sz = en.type==='boss'?26:(en.type==='brute'?16:13);
    ctx.fillStyle=c;
    ctx.beginPath(); ctx.moveTo(en.x,en.y-sz+wob);
    ctx.lineTo(en.x-sz,en.y+sz); ctx.lineTo(en.x+sz,en.y+sz); ctx.closePath(); ctx.fill();
    // eyes
    ctx.fillStyle='#ffdd00'; ctx.fillRect(en.x-6,en.y-4,3,3); ctx.fillRect(en.x+3,en.y-4,3,3);
    // hp bar
    ctx.fillStyle='#000'; ctx.fillRect(en.x-sz,en.y-sz-8,sz*2,3);
    ctx.fillStyle='#ff4d5e'; ctx.fillRect(en.x-sz,en.y-sz-8,sz*2*(en.hp/en.maxhp),3);
  });

  // projectiles
  ctx.fillStyle='#bfe9ff';
  game.projectiles.forEach(p=>{ ctx.fillRect(p.x-6,p.y-1,12,2); ctx.beginPath(); ctx.arc(p.x+(p.vx>0?6:-6),p.y,2,0,6.28); ctx.fill(); });

  // player
  drawPlayer();

  // particles
  game.particles.forEach(p=>{ ctx.globalAlpha=Math.min(1,p.life/20); ctx.fillStyle=p.color; ctx.fillRect(p.x-p.size/2,p.y-p.size/2,p.size,p.size); });
  ctx.globalAlpha=1;

  // win overlay
  if(game.won){
    ctx.fillStyle='rgba(20,10,30,0.75)'; ctx.fillRect(0,0,W,H);
    ctx.fillStyle='#f5c26b'; ctx.font='30px monospace'; ctx.textAlign='center';
    ctx.fillText('VICTORY',W/2,H/2-20);
    ctx.fillStyle='#fff'; ctx.font='14px monospace';
    ctx.fillText('The Sunspire burns anew. Coin: ◈'+game.money,W/2,H/2+16);
    ctx.fillText('Reload the page to play again.',W/2,H/2+40);
    ctx.textAlign='left';
  }
}

function glow(x,y,c){ const g=ctx.createRadialGradient(x,y,2,x,y,18); g.addColorStop(0,c+''); g.addColorStop(1,'transparent'); ctx.globalAlpha=0.3; ctx.fillStyle=g; ctx.beginPath(); ctx.arc(x,y,18,0,6.28); ctx.fill(); ctx.globalAlpha=1; }

function drawHeart(x,y,s,c){ ctx.fillStyle=c; ctx.beginPath();
  ctx.moveTo(x,y+s*0.7); ctx.bezierCurveTo(x-s,y-s*0.3,x-s*0.5,y-s,x,y-s*0.3);
  ctx.bezierCurveTo(x+s*0.5,y-s,x+s,y-s*0.3,x,y+s*0.7); ctx.fill(); }

function drawPlayer(){
  const px=game.px, py=game.py;
  const flash = game.invincible>0 && Math.floor(Date.now()/80)%2===0;
  if(flash) return; // blink when hurt
  const hurt = game.hurtTimer>0;
  ctx.save(); ctx.translate(px,py);
  const bob = Math.sin(game.walkAnim)*2;
  const legSwing = Math.sin(game.walkAnim)*4;

  // shadow
  ctx.fillStyle='rgba(0,0,0,0.3)'; ctx.beginPath(); ctx.ellipse(0,14,10,4,0,0,6.28); ctx.fill();

  // legs (walk anim)
  ctx.fillStyle='#3a4a6b';
  ctx.fillRect(-6,6+legSwing*0.3, 4, 8-legSwing*0.3);
  ctx.fillRect(2,6-legSwing*0.3, 4, 8+legSwing*0.3);

  // body — color changes with equipment
  ctx.fillStyle = game.worn.amulet?'#8b3a3a':'#4a6b9a';
  ctx.fillRect(-8,-8+bob,16,16);
  if(hurt){ ctx.fillStyle='rgba(255,80,90,0.6)'; ctx.fillRect(-8,-8+bob,16,16); }

  // amulet visual
  if(game.worn.amulet){ ctx.fillStyle='#ffcf5c'; ctx.beginPath(); ctx.arc(0,0+bob,3,0,6.28); ctx.fill(); }

  // head
  ctx.fillStyle='#e8c39a'; ctx.fillRect(-6,-18+bob,12,10);
  // boots highlight
  if(game.worn.boots){ ctx.fillStyle='#ffcf5c'; ctx.fillRect(-6,12,4,2); ctx.fillRect(2,12,4,2); }

  // weapon — changes with tier
  const wp=WEAPONS[game.weaponTier];
  const swing = game.attackTimer>0 ? (10-game.attackTimer)/10 : 0;
  ctx.save();
  ctx.translate(game.pdir*10, -2+bob);
  ctx.scale(game.pdir,1);
  ctx.rotate(-swing*1.4 - 0.3);
  ctx.fillStyle=wp.color;
  const bladeLen = 14 + game.weaponTier*6;
  ctx.fillRect(-1.5,-bladeLen,3,bladeLen); // blade
  ctx.fillStyle='#5a4a30'; ctx.fillRect(-3,0,6,4); // hilt
  if(game.weaponTier===2){ // glaive glow
    ctx.globalAlpha=0.5; ctx.fillStyle='#6bd0ff'; ctx.fillRect(-3,-bladeLen,6,bladeLen); ctx.globalAlpha=1;
  }
  // slash arc
  if(swing>0.1){ ctx.strokeStyle=wp.color; ctx.globalAlpha=0.5; ctx.lineWidth=3; ctx.beginPath(); ctx.arc(0,-4,bladeLen,-1.4,0.4); ctx.stroke(); ctx.globalAlpha=1; ctx.lineWidth=1; }
  ctx.restore();

  // bow on back if owned
  if(game.hasRanged){ ctx.strokeStyle='#6bd0ff'; ctx.lineWidth=2; ctx.beginPath(); ctx.arc(-game.pdir*8,-2+bob,8,-1.2,1.2); ctx.stroke(); ctx.lineWidth=1; }

  ctx.restore();
}

// ============ LOOP ============
function loop(){ update(); draw(); requestAnimationFrame(loop); }

document.getElementById('startBtn').onclick=()=>{
  initAudio();
  document.getElementById('title').style.display='none';
  newGame();
};
loop();
</script>
</body>
</html>
<!-- agent-meta {"model":"anthropic/claude-opus-4.8","provider":"openrouter","persona":"basic","sessionId":"daa2e708-5411-49d1-b71d-be36a5870a49","tokensIn":1174,"tokensOut":14879,"tokensTotal":16053,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-07-22T20:00:52.384Z"} -->