← RPG results

RPG

muse spark 1.2 · 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 32,828 bytes · SHA-256 4e7e5411795e
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>EMBER HOLLOW — Compact Adventure</title>
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=JetBrains+Mono:wght@600;800&family=Cinzel:wght@700;900&display=swap" rel="stylesheet">
<style>
*{box-sizing:border-box;margin:0;padding:0}
:root{--bg:#0f1a14;--panel:#1a2a22;--line:#2f4a3a;--accent:#ffb338;--accent2:#4ade80;--danger:#ff4d4d}
body{background:#08110d;color:#e8f5e9;font-family:'JetBrains Mono',monospace;overflow-x:hidden}
.topbar{height:58px;background:#0e231b;border-bottom:3px solid #1e3d2e;display:flex;align-items:center;justify-content:space-between;padding:0 16px;position:sticky;top:0;z-index:20}
.brand{display:flex;align-items:center;gap:12px}
.logo{width:36px;height:36px;background:var(--accent);display:grid;place-items:center;border:3px solid #000;color:#000;font-family:'Press Start 2P';font-size:10px}
.brand h1{font-family:'Cinzel',serif;letter-spacing:.12em;font-size:15px}
.brand p{font-size:9px;opacity:.7;letter-spacing:.18em}
.hud{display:flex;align-items:center;gap:18px}
.hearts{display:flex;gap:4px}
.heart{width:24px;height:24px;background:#1a1a1a;border:2px solid #000;position:relative;display:grid;place-items:center;font-size:13px;clip-path:polygon(50% 85%, 90% 45%, 75% 15%, 50% 30%, 25% 15%, 10% 45%)}
.heart.full{background:#ff3b30;color:#fff}
.heart.half{background:linear-gradient(90deg,#ff3b30 50%,#1a1a1a 50%);color:#fff}
.heart.empty{background:#1a1a1a;color:#555}
.stat-pill{background:#0b1a14;border:2px solid #2a4a38;padding:6px 10px;display:flex;align-items:center;gap:8px;font-size:11px;min-width:110px}
.stat-pill b{color:var(--accent)}
.layout{display:grid;grid-template-columns:1fr 320px;gap:14px;padding:14px;max-width:1280px;margin:auto}
@media(max-width:980px){.layout{grid-template-columns:1fr}}
.viewport{background:#0b1f16;border:3px solid #1e3d2e;position:relative;overflow:hidden;box-shadow:0 10px 40px rgba(0,0,0,.5)}
canvas{display:block;width:100%;height:520px;background:radial-gradient(ellipse at center,#143326 0%,#0b1f16 70%)}
.overlay-bar{position:absolute;bottom:0;left:0;right:0;background:rgba(5,18,13,.92);border-top:2px solid #2a4a38;display:flex;gap:10px;padding:8px 10px;align-items:center;flex-wrap:wrap}
.chip{border:2px solid #2a4a38;background:#0f2a1d;padding:6px 8px;font-size:10px;display:flex;align-items:center;gap:6px}
.chip b{color:var(--accent)}
.progress{flex:1;height:10px;background:#0b1a14;border:2px solid #2a4a38;position:relative;overflow:hidden;min-width:140px}
.progress i{position:absolute;inset:2px;background:linear-gradient(90deg,#ffb338,#ff7a18);width:var(--p);transition:.4s}
.side{display:flex;flex-direction:column;gap:14px}
.card{background:var(--panel);border:3px solid var(--line);padding:12px}
.card h3{font-family:'Press Start 2P';font-size:9px;letter-spacing:.1em;margin-bottom:10px;display:flex;justify-content:space-between;align-items:center}
.card h3 span{font-family:'JetBrains Mono';font-size:10px;background:#0b1a14;padding:3px 6px;border:1px solid var(--line)}
.weapon-row{display:grid;grid-template-columns:56px 1fr;gap:10px;align-items:center}
.weapon-icon{width:56px;height:56px;background:#0b1a14;border:2px solid #2a4a38;display:grid;place-items:center;font-size:26px}
.tiers{display:flex;gap:6px;margin-top:8px}
.tier{flex:1;height:18px;border:2px solid #000;display:grid;place-items:center;font-size:8px;font-weight:800}
.tier.on{background:var(--accent);color:#000}
.tier.off{background:#0b1a14;color:#5a7a68}
.bow-badge{margin-top:8px;border:2px dashed #3a5a4a;padding:6px;text-align:center;font-size:10px}
.bow-badge.has{border-style:solid;background:rgba(74,222,128,.12);color:#4ade80}
.stamina{height:10px;background:#0b1a14;border:2px solid #2a4a38;position:relative;overflow:hidden;margin-top:8px}
.stamina i{position:absolute;inset:2px;background:linear-gradient(90deg,#38bdf8,#4ade80);width:var(--s)}
.inv-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:6px}
.slot{aspect-ratio:1;background:#0b1a14;border:2px solid #2a4a38;display:grid;place-items:center;font-size:18px;position:relative;cursor:pointer}
.slot.has{border-color:var(--accent)}
.slot small{position:absolute;bottom:2px;right:3px;font-size:8px;background:#000;padding:1px 3px}
.shop-item{display:flex;justify-content:space-between;align-items:center;padding:8px;border:2px solid #2a4a38;background:#0f231b;margin-bottom:6px;gap:8px}
.shop-item button{background:var(--accent);border:2px solid #000;color:#000;font-family:'Press Start 2P';font-size:8px;padding:6px 8px;cursor:pointer}
.shop-item button:disabled{opacity:.4;filter:grayscale(1)}
.controls{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-top:8px}
.kbd{background:#0b1a14;border:2px solid #2a4a38;padding:8px;text-align:center;font-size:10px}
.kbd b{color:var(--accent);display:block;font-size:11px}
.save-row{display:flex;gap:8px;margin-top:10px}
.btn{flex:1;padding:10px;border:2px solid #000;font-family:'Press Start 2P';font-size:8px;cursor:pointer}
.btn-save{background:#4ade80;color:#000}
.btn-load{background:#0b1a14;color:#e8f5e9;border-color:#2a4a38}
.log{height:120px;overflow:auto;background:#0b1a14;border:2px solid #2a4a38;padding:8px;font-size:10px;line-height:1.6}
.log b{color:var(--accent)}
.hint{position:absolute;top:10px;left:50%;transform:translateX(-50%);background:#0b1a14;border:2px solid var(--accent);padding:6px 10px;font-size:10px;pointer-events:none;opacity:0;transition:.2s}
.hint.show{opacity:1}
.modal{position:absolute;inset:0;background:rgba(5,18,13,.88);display:none;place-items:center;padding:16px}
.modal.show{display:grid}
.modal-card{background:#12281d;border:3px solid #2a4a38;padding:16px;max-width:420px;width:100%;text-align:center}
.modal-card h2{font-family:'Cinzel';letter-spacing:.1em;margin-bottom:8px}
.modal-card p{font-size:11px;opacity:.85;line-height:1.6;margin-bottom:12px}
.big{width:100%;padding:12px;background:var(--accent);border:3px solid #000;font-family:'Press Start 2P';font-size:10px;cursor:pointer}
.dmg{position:absolute;color:#ff3b30;font-weight:900;font-size:14px;text-shadow:0 2px 0 #000;pointer-events:none;animation:float .6s forwards}
@keyframes float{to{transform:translateY(-28px);opacity:0}}
.pulse{animation:pulse .2s}
@keyframes pulse{0%{filter:brightness(2)}100%{filter:none}}
</style>
</head>
<body>
<div class="topbar">
  <div class="brand">
    <div class="logo">EH</div>
    <div><h1>EMBER HOLLOW</h1><p>FIELD • 00:00 — ACT I: THE VERDANT SCAR</p></div>
  </div>
  <div class="hud">
    <div class="hearts" id="hearts"></div>
    <div class="stat-pill">◉ <b id="coin">0</b> GOLD</div>
    <div class="stat-pill">◆ <b id="sigil">0/3</b> SIGILS</div>
  </div>
</div>

<div class="layout">
  <div class="viewport" id="viewport">
    <canvas id="c" width="960" height="520"></canvas>
    <div class="hint" id="hint">Press [E] to interact</div>
    <div class="overlay-bar">
      <div class="chip">⚔ <b id="wName">Rusty Shortsword</b> <span id="wStats" style="opacity:.7">DMG 1 • RNG 28</span></div>
      <div class="chip">◐ STAMINA</div>
      <div class="progress"><i id="prog" style="--p:15%"></i></div>
      <div class="chip" style="font-size:9px">WASD Move • SHIFT Sprint • SPACE Slash • Q Bow • E Interact</div>
    </div>
    <div class="modal" id="shopModal">
      <div class="modal-card">
        <h2>⛺ WAYFARER'S CART</h2><p>Spend gold earned from battles & exploration.</p>
        <div id="shopList"></div>
        <button class="big" onclick="closeShop()">LEAVE SHOP [E]</button>
      </div>
    </div>
    <div class="modal" id="winModal">
      <div class="modal-card">
        <h2>🌟 HOLLOW RESTORED</h2><p>You recovered all 5 Heart Vessels, forged the Ember Greatblade, and cleansed the Sanctuary. One hour, one legend.</p>
        <button class="big" onclick="location.reload()">PLAY AGAIN</button>
      </div>
    </div>
  </div>

  <div class="side">
    <div class="card">
      <h3>EQUIPMENT <span id="equipLabel">TIER 1/3</span></h3>
      <div class="weapon-row">
        <div class="weapon-icon" id="weaponIcon">🗡</div>
        <div>
          <div style="font-size:11px;font-weight:800" id="weaponTitle">Rusty Shortsword</div>
          <div style="font-size:10px;opacity:.7" id="weaponDesc">Quick jab. Low range.</div>
          <div class="tiers"><div class="tier on">I</div><div class="tier off" id="t2">II</div><div class="tier off" id="t3">III</div></div>
        </div>
      </div>
      <div class="stamina"><i id="stamBar" style="--s:100%"></i></div>
      <div class="bow-badge" id="bowBadge">🏹 Ranged: LOCKED — Reach mid-game (2 Sigils) to discover the Gale Bow in the Gloomed Thicket.</div>
    </div>

    <div class="card">
      <h3>INVENTORY <span id="invCount">0 / 8</span></h3>
      <div class="inv-grid" id="invGrid"></div>
      <div style="font-size:9px;opacity:.6;margin-top:8px">Collect ≥5 distinct items • Click to use consumables</div>
    </div>

    <div class="card">
      <h3>WAYFARER SHOP</h3>
      <div id="sideShop"></div>
    </div>

    <div class="card">
      <h3>SAVE SHRINE <span>CHECKPOINT</span></h3>
      <p style="font-size:10px;opacity:.75;line-height:1.5">Stand on the glowing shrine and press <b>[E]</b> to bind your soul. Progress persists after reload.</p>
      <div class="save-row"><button class="btn btn-save" onclick="doSave()">BIND [E]</button><button class="btn btn-load" onclick="doLoad()">RESTORE</button></div>
      <div class="controls">
        <div class="kbd"><b>WASD</b>Move</div><div class="kbd"><b>SPACE</b>Attack</div><div class="kbd"><b>Q</b>Bow</div>
      </div>
      <div class="log" id="log"><b>→ Ember Hollow awakens...</b><br>Find the Blacksmith chest for Upgrade I, then chase Sigils. Mid-game Bow awaits at 2 Sigils. Seek 2 Heart Vessels to reach 5 containers.</div>
    </div>
  </div>
</div>

<script>
const c=document.getElementById('c'),ctx=c.getContext('2d'), viewport=document.getElementById('viewport')
let audioCtx
function sfx(f=440,d=0.12,type='sine',v=0.2,slide=0){
  try{
    if(!audioCtx) audioCtx=new (window.AudioContext||window.webkitAudioContext)()
    const o=audioCtx.createOscillator(), g=audioCtx.createGain()
    o.type=type; o.frequency.value=f
    o.connect(g); g.connect(audioCtx.destination)
    g.gain.value=v
    o.start()
    if(slide) o.frequency.linearRampToValueAtTime(f*slide,d)
    g.gain.exponentialRampToValueAtTime(0.0001,audioCtx.currentTime+d)
    setTimeout(()=>o.stop(),d*1000+50)
  }catch(e){}
}
const S={walk:()=>sfx(180,0.06,'square',0.06,1.4), swing:(t)=>sfx(t==1?320:t==2?420:560,0.12,'square',0.18, t==3?1.6:1.2), hit:()=>sfx(120,0.2,'sawtooth',0.18,0.6), hurt:()=>sfx(90,0.35,'sawtooth',0.22,0.7), pickup:()=>{sfx(660,0.12,'sine',0.2,1.5); setTimeout(()=>sfx(880,0.15,'sine',0.16),90)}, shop:()=>{sfx(500,0.1,'triangle',0.18); setTimeout(()=>sfx(700,0.18,'triangle',0.15),110)}, save:()=>{sfx(300,0.4,'sine',0.18,2); setTimeout(()=>sfx(600,0.5,'sine',0.18),180)} }

const weapons=[
  {name:'Rusty Shortsword',desc:'Quick jab. Low range.',icon:'🗡',dmg:1,range:30,cooldown:320},
  {name:'Iron Cleaver',desc:'Heavy arc • +Range & Damage',icon:'⚔',dmg:2,range:44,cooldown:260},
  {name:'Ember Greatblade',desc:'Flame wave • Long reach',icon:'🔥',dmg:4,range:58,cooldown:220}
]
const shopDefs=[
  {id:'potion',icon:'🧪',name:'Healing Brew',price:18,desc:'Restore 4 HP'},
  {id:'vial',icon:'⚗',name:'Stamina Vial',price:20,desc:'+ Max Stamina'},
  {id:'charm',icon:'🔰',name:'Thorn Charm',price:35,desc:'-1 dmg taken'},
  {id:'feather',icon:'🪶',name:'Ember Feather',price:28,desc:'Dash speed+'},
  {id:'key',icon:'🗝',name:"Explorer's Key",price:22,desc:'Open sealed chest'}
]

let state={coins:12,maxContainers:3,health:6,weaponTier:0,hasBow:false,arrows:12,inventory:[],sigils:0,zone:0}
let player={x:140,y:140, r:14, dir:0, stamina:100, maxStamina:100, invuln:0, moving:false}
let enemies=[], chests=[], pickups=[], particles=[], projectiles=[], shrine={x:460,y:260}, shopPos={x:760,y:150}
let keys={}, attackCooldown=0, walkTick=0, facingShop=false, facingShrine=false, chestNear=null

function log(m){ const L=document.getElementById('log'); L.innerHTML+=`<br>• ${m}`; L.scrollTop=L.scrollHeight}
function saveKey(){return 'ember_save_v2'}
function doSave(){ localStorage.setItem(saveKey(), JSON.stringify({state,player:{x:player.x,y:player.y}})); S.save(); pop('Soul Bound ✓'); log('<b>Progress saved at shrine.</b>');}
function doLoad(){ const d=JSON.parse(localStorage.getItem(saveKey())||'null'); if(!d){pop('No save found');return} state=d.state; player.x=d.player.x; player.y=d.player.y; updateHUD(); buildWorld(); pop('Progress restored'); log('Save restored.')}
function pop(t){ const h=document.getElementById('hint'); h.textContent=t; h.classList.add('show'); setTimeout(()=>h.classList.remove('show'),1400)}

function updateHUD(){
  // hearts
  const wrap=document.getElementById('hearts'); wrap.innerHTML=''
  const maxHP=state.maxContainers*2
  for(let i=0;i<state.maxContainers;i++){
    const v = state.health - i*2
    const el=document.createElement('div'); el.className='heart '+(v>=2?'full':v==1?'half':'empty'); el.textContent='♥'; wrap.appendChild(el)
    if(v>=2) el.title='Full'; else if(v==1) el.title='Half'
  }
  if(state.maxContainers<5){ for(let i=state.maxContainers;i<5;i++){ const e=document.createElement('div'); e.className='heart empty'; e.style.opacity=.25; e.textContent='♡'; wrap.appendChild(e)}}
  document.getElementById('coin').textContent=state.coins
  document.getElementById('sigil').textContent=`${state.sigils}/3`
  document.getElementById('prog').style.setProperty('--p', (15+ state.sigils*20 + state.weaponTier*12 + (state.hasBow?15:0)) +'%')
  document.getElementById('wName').textContent=weapons[state.weaponTier].name
  document.getElementById('wStats').textContent=`DMG ${weapons[state.weaponTier].dmg} • RNG ${weapons[state.weaponTier].range}`
  document.getElementById('weaponTitle').textContent=weapons[state.weaponTier].name
  document.getElementById('weaponDesc').textContent=weapons[state.weaponTier].desc
  document.getElementById('weaponIcon').textContent=weapons[state.weaponTier].icon
  document.getElementById('equipLabel').textContent=`TIER ${state.weaponTier+1}/3`
  document.getElementById('t2').className='tier '+(state.weaponTier>=1?'on':'off')
  document.getElementById('t3').className='tier '+(state.weaponTier>=2?'on':'off')
  const bb=document.getElementById('bowBadge'); bb.className= state.hasBow ? 'bow-badge has':'bow-badge'
  bb.innerHTML= state.hasBow ? `🏹 Gale Bow — READY [Q] • Arrows: ${state.arrows}` : `🏹 Ranged: LOCKED — Reach mid-game (2 Sigils) to discover the Gale Bow.`
  document.getElementById('stamBar').style.setProperty('--s', (player.stamina/player.maxStamina*100)+'%')
  // inv
  const grid=document.getElementById('invGrid'); grid.innerHTML=''
  for(let i=0;i<8;i++){
    const it=state.inventory[i]
    const d=document.createElement('div'); d.className='slot '+(it?'has':''); d.innerHTML= it ? it.icon : '·'
    if(it) d.innerHTML+=`<small>x${it.qty}</small>`
    d.title= it ? `${it.name}: ${it.desc}`:'Empty'
    d.onclick=()=>{ if(it) useItem(it.id)}
    grid.appendChild(d)
  }
  document.getElementById('invCount').textContent=`${state.inventory.length} / 8`
  if(state.sigils>=3 && state.maxContainers>=5 && state.weaponTier>=2) document.getElementById('winModal').classList.add('show')
}
function addItem(id, qty=1){
  const def= shopDefs.find(s=>s.id===id) || extraDefs.find(s=>s.id===id)
  let ex=state.inventory.find(i=>i.id===id)
  if(ex) ex.qty+=qty
  else state.inventory.push({id:def.id,icon:def.icon,name:def.name,desc:def.desc,qty})
  S.pickup(); spawnParticles(player.x,player.y,'#ffb338',10); log(`Picked up <b>${def.name}</b>`)
  updateHUD()
}
const extraDefs=[
  {id:'sigil',icon:'◆',name:'Ember Sigil',desc:'Progress fragment'},
  {id:'heart',icon:'❤',name:'Heart Vessel',desc:'+1 Container (+2 HP)'},
  {id:'bow',icon:'🏹',name:'Gale Bow',desc:'Ranged weapon'},
  ...shopDefs
]
function useItem(id){
  const idx=state.inventory.findIndex(i=>i.id===id)
  if(idx<0) return
  const it=state.inventory[idx]
  if(id==='potion'){ state.health=Math.min(state.maxContainers*2, state.health+4); log('Used Healing Brew +4 HP'); S.pickup() }
  else if(id==='vial'){ player.maxStamina+=20; player.stamina=player.maxStamina; log('Stamina expanded!');}
  else if(id==='key'){ log('Key used to open sealed paths');}
  else { log(`${it.name} is passive.`); return}
  it.qty-- ; if(it.qty<=0) state.inventory.splice(idx,1); updateHUD()
}

function buildWorld(){
  enemies=[]; chests=[]; pickups=[]
  const tier=state.weaponTier, sig=state.sigils
  // World layout changes per zone progression
  // Chests
  chests.push({x:220,y:90,w:28,h:28,type:'coin',opened:false, label:'Coin Cache'})
  chests.push({x:820,y:380,w:28,h:28,type:'charm',opened:false, label:'Charm Chest'})
  if(tier<1) chests.push({x:680,y:420,w:32,h:32,type:'upgrade1',opened:false,label:'Blacksmith Cache — IRON CLEAVER'})
  if(tier<2 && sig>=1) chests.push({x:150,y:420,w:32,h:32,type:'upgrade2',opened:false,label:'Ember Vault — GREATBLADE'})
  if(state.maxContainers<4) chests.push({x:760,y:300,w:28,h:28,type:'heart',opened:false,label:'Heart Vessel'})
  if(state.maxContainers<5 && sig>=2) chests.push({x:120,y:300,w:28,h:28,type:'heart2',opened:false,label:'Heart Vessel'})
  if(!state.hasBow && sig>=2) chests.push({x:480,y:90,w:32,h:32,type:'bow',opened:false,label:'Wind Shrine — GALE BOW'})
  // Sigil chests
  if(sig<1) chests.push({x:540,y:420,w:30,h:30,type:'sigil',opened:false,label:'Sigil I'})
  if(sig<2) chests.push({x:860,y:90,w:30,h:30,type:'sigil',opened:false,label:'Sigil II'})
  if(sig<3) chests.push({x:300,y:180,w:30,h:30,type:'sigil',opened:false,label:'Final Sigil'})
  // Enemies scaled with progression
  const count= 4 + sig*2
  for(let i=0;i<count;i++){
    enemies.push({x: 180+Math.random()*620, y:180+Math.random()*300, hp: 2 + Math.floor(sig*0.8)+ (tier), max:3, vx:0,vy:0, hit:0, alive:true})
  }
  // coin pickups scattered
  for(let i=0;i<6;i++) pickups.push({x:100+Math.random()*760,y:120+Math.random()*340,type:'coin',v: 5+Math.floor(Math.random()*6),taken:false})
}

function buildShop(){
  const list=document.getElementById('shopList'), side=document.getElementById('sideShop')
  list.innerHTML=''; side.innerHTML=''
  shopDefs.forEach(def=>{
    const can=state.coins>=def.price
    const row=`<div class="shop-item"><div style="display:flex;gap:8px;align-items:center"><span style="font-size:18px">${def.icon}</span><div style="text-align:left"><div style="font-size:11px;font-weight:800">${def.name}</div><div style="font-size:9px;opacity:.7">${def.desc}</div></div></div><button ${can?'':'disabled'} onclick="buy('${def.id}')">${def.price} G</button></div>`
    side.innerHTML+=row
  })
  shopDefs.forEach(def=>{
    const can=state.coins>=def.price
    const row=document.createElement('div'); row.className='shop-item'
    row.innerHTML=`<div style="display:flex;gap:8px;align-items:center"><span style="font-size:20px">${def.icon}</span><div><div style="font-size:12px;font-weight:800">${def.name}</div><div style="font-size:10px;opacity:.7">${def.desc}</div></div></div><button ${can?'':'disabled'} onclick="buy('${def.id}')">${def.price} G</button>`
    list.appendChild(row)
  })
}
function buy(id){
  const def=shopDefs.find(d=>d.id===id)
  if(state.coins<def.price){ S.hurt(); pop('Not enough gold'); return}
  state.coins-=def.price
  addItem(id,1)
  if(id==='charm') log('Thorn Charm reduces damage by 1')
  S.shop(); spawnParticles(shopPos.x,shopPos.y,'#4ade80',14); buildShop(); updateHUD()
}
function openShop(){ document.getElementById('shopModal').classList.add('show'); buildShop(); S.shop()}
function closeShop(){ document.getElementById('shopModal').classList.remove('show')}

function spawnParticles(x,y,color,n=8){
  for(let i=0;i<n;i++) particles.push({x,y, vx:(Math.random()-.5)*6, vy:(Math.random()-.5)*6, life:1, color, r:2+Math.random()*3})
}
function floatingDmg(x,y,txt,col='#ff3b30'){
  const d=document.createElement('div'); d.className='dmg'; d.textContent=txt; d.style.left=x+'px'; d.style.top=y+'px'; d.style.color=col
  viewport.appendChild(d); setTimeout(()=>d.remove(),700)
}

let walls=[
  {x:0,y:0,w:960,h:18},{x:0,y:502,w:960,h:18},{x:0,y:0,w:18,h:520},{x:942,y:0,w:18,h:520},
  {x:320,y:130,w:18,h:90},{x:500,y:200,w:120,h:18},{x:650,y:250,w:18,h:140},{x:240,y:340,w:180,h:18},
]

function rectCollide(ax,ay,ar, bx,by,bw,bh){
  return ax+ar>bx && ax-ar<bx+bw && ay+ar>by && ay-ar<by+bh
}
function wallCollide(x,y){
  for(let w of walls) if(rectCollide(x,y,player.r,w.x,w.y,w.w,w.h)) return true
  return false
}

// input
window.addEventListener('keydown',e=>{
  keys[e.key.toLowerCase()]=true
  if(e.code==='Space'){ e.preventDefault(); doAttack()}
  if(e.key.toLowerCase()==='q'){ doShoot()}
  if(e.key.toLowerCase()==='e'){ doInteract()}
  if(e.key==='Escape') closeShop()
})
window.addEventListener('keyup',e=>keys[e.key.toLowerCase()]=false)

function doAttack(){
  if(attackCooldown>0) return
  const w=weapons[state.weaponTier]
  attackCooldown=w.cooldown; spawnParticles(player.x+Math.cos(player.dir)*20, player.y+Math.sin(player.dir)*20, w.icon==='🔥'?'#ff7a18':'#fde68a', 7)
  S.swing(state.weaponTier+1)
  // hit enemies in range
  const ax= player.x + Math.cos(player.dir)*w.range*0.6, ay= player.y + Math.sin(player.dir)*w.range*0.6
  enemies.forEach(en=>{
    if(!en.alive) return
    const d=Math.hypot(en.x-ax,en.y-ay)
    if(d< w.range*0.7){
      en.hp-=w.dmg; en.hit=10; spawnParticles(en.x,en.y,'#ff4d4d',6); floatingDmg(en.x,en.y-12,'-'+w.dmg)
      if(w.icon==='🔥'){ en.hp-=1; floatingDmg(en.x,en.y-28,'burn', '#ff7a18')}
      S.hit()
      if(en.hp<=0){ en.alive=false; state.coins+= 6+ Math.floor(Math.random()*6); spawnParticles(en.x,en.y,'#ffb338',12); pickups.push({x:en.x,y:en.y,type:'coin',v:4,taken:false}); log('Foe defeated +gold');}
    }
  })
  // hit chests? handled via interact
  // screen shake
  c.style.transform='translate(1px,1px)'; setTimeout(()=>c.style.transform='',60)
}
function doShoot(){
  if(!state.hasBow){ pop('Bow locked — 2 Sigils needed'); return}
  if(state.arrows<=0){ pop('Out of arrows!'); return}
  if(attackCooldown>80) return
  state.arrows--; attackCooldown=340; S.swing(2)
  const sp=6, vx=Math.cos(player.dir)*sp, vy=Math.sin(player.dir)*sp
  projectiles.push({x:player.x,y:player.y,vx,vy,life:60})
  updateHUD()
}
function doInteract(){
  if(facingShop){ openShop(); return}
  if(facingShrine){ doSave(); return}
  if(chestNear){ openChest(chestNear); return}
  // pickup coin touch handled auto but also pickup if near
  pop('Nothing to interact')
}
function openChest(ch){
  if(ch.opened) return
  ch.opened=true; S.pickup(); spawnParticles(ch.x,ch.y,'#ffb338',16)
  if(ch.type==='coin'){ state.coins+=14; log('Found 14 Gold in chest')}
  if(ch.type==='charm'){ addItem('charm',1)}
  if(ch.type==='upgrade1'){ state.weaponTier=1; log('<b>UPGRADE: Iron Cleaver acquired!</b> Damage & Range up'); S.shop()}
  if(ch.type==='upgrade2'){ state.weaponTier=2; log('<b>UPGRADE: Ember Greatblade — flame damage!</b>'); S.shop()}
  if(ch.type==='bow'){ state.hasBow=true; state.arrows=24; log('<b>DISCOVERY: Gale Bow found! Press Q to shoot.</b>')}
  if(ch.type==='heart'){ state.maxContainers=Math.min(5,state.maxContainers+1); state.health=state.maxContainers*2; log('<b>Heart Vessel! Max HP increased to '+(state.maxContainers*2)+'</b>')}
  if(ch.type==='heart2'){ state.maxContainers=5; state.health=10; log('<b>Final Heart Vessel! 5 Containers (10 HP)!</b>')}
  if(ch.type==='sigil'){ state.sigils=Math.min(3,state.sigils+1); log(`<b>Ember Sigil ${state.sigils}/3 collected</b> ${state.sigils===2?' — Gale Bow now discoverable!':''}`); buildShop()}
  updateHUD(); buildWorld()
  floatingDmg(ch.x,ch.y-10,'+REWARD','#ffb338')
}

function update(dt){
  // move
  let mx=(keys['d']||keys['arrowright']?1:0)-(keys['a']||keys['arrowleft']?1:0)
  let my=(keys['s']||keys['arrowdown']?1:0)-(keys['w']||keys['arrowup']?1:0)
  const sprint=keys['shift']
  let len=Math.hypot(mx,my)
  if(len){ mx/=len; my/=len; player.dir=Math.atan2(my,mx); player.moving=true } else player.moving=false
  const speed = sprint ? 3.2 : 2.1
  if(player.moving){
    if(sprint){ player.stamina=Math.max(0,player.stamina-0.5); if(player.stamina===0) player.moving=false }
    else player.stamina=Math.min(player.maxStamina, player.stamina+0.3)
    let nx=player.x+mx*speed, ny=player.y+my*speed
    if(!wallCollide(nx,player.y)) player.x=nx
    if(!wallCollide(player.x,ny)) player.y=ny
    walkTick+= sprint?0.35:0.2
    if(Math.floor(walkTick)%14===0) S.walk()
  } else {
    player.stamina=Math.min(player.maxStamina, player.stamina+0.6)
  }
  if(attackCooldown>0) attackCooldown-=dt
  if(player.invuln>0) player.invuln-=dt

  // enemies chase
  enemies.forEach(en=>{
    if(!en.alive) return
    if(en.hit>0) en.hit--
    const d=Math.hypot(en.x-player.x,en.y-player.y)
    if(d<220){
      const ang=Math.atan2(player.y-en.y,player.x-en.x)
      en.x+=Math.cos(ang)* (0.9 + state.sigils*0.15)
      en.y+=Math.sin(ang)* (0.9 + state.sigils*0.15)
    } else {
      en.x+= Math.sin(Date.now()/700 + en.x)*0.3
    }
    // damage player
    if(d< 22 && player.invuln<=0){
      let dmg=1
      if(state.inventory.find(i=>i.id==='charm')) dmg=Math.max(0,dmg-1)
      if(dmg>0){
        state.health-=dmg; player.invuln=800; viewport.classList.add('pulse'); setTimeout(()=>viewport.classList.remove('pulse'),200)
        S.hurt(); spawnParticles(player.x,player.y,'#ff3b30',8); floatingDmg(player.x,player.y-18,'-'+dmg)
        if(state.health<=0){ state.health=state.maxContainers*2; player.x=140; player.y=140; log('<b>You fell... returned to start.</b>'); S.hurt()}
        updateHUD()
      }
    }
  })

  // pickups auto
  pickups.forEach(p=>{
    if(p.taken) return
    if(Math.hypot(p.x-player.x,p.y-player.y)<22){ p.taken=true; state.coins+=p.v; spawnParticles(p.x,p.y,'#ffb338',6); S.pickup(); updateHUD(); floatingDmg(p.x,p.y-10,'+'+p.v+'g','#ffb338')}
  })
  // projectiles
  projectiles.forEach(pr=>{ pr.x+=pr.vx; pr.y+=pr.vy; pr.life-- })
  projectiles=projectiles.filter(pr=>pr.life>0 && pr.x>12 && pr.x<948 && pr.y>12 && pr.y<508)
  projectiles.forEach((pr,i)=>{
    enemies.forEach(en=>{
      if(!en.alive) return
      if(Math.hypot(en.x-pr.x,en.y-pr.y)<18){ en.hp-=2; en.hit=10; pr.life=0; spawnParticles(en.x,en.y,'#4ade80',7); floatingDmg(en.x,en.y-10,'-2'); S.hit(); if(en.hp<=0){en.alive=false; state.coins+=5; spawnParticles(en.x,en.y,'#ffb338',10)}}
    })
  })

  // proximity checks
  facingShop = Math.hypot(shopPos.x-player.x, shopPos.y-player.y)<50
  facingShrine= Math.hypot(shrine.x-player.x, shrine.y-player.y)<44
  chestNear=null
  chests.forEach(ch=>{ if(!ch.opened && Math.hypot(ch.x-player.x,ch.y-player.y)<40) chestNear=ch })
  if(facingShop||facingShrine||chestNear){ document.getElementById('hint').classList.add('show'); document.getElementById('hint').textContent = facingShop? '⛺ Wayfarer Cart — Press [E] to shop' : facingShrine? '🔥 Save Shrine — Press [E] to bind' : `📦 ${chestNear.label} — Press [E] to open`}
  else document.getElementById('hint').classList.remove('show')

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

function draw(){
  ctx.clearRect(0,0,c.width,c.height)
  // floor pattern
  ctx.fillStyle='#0f2a1d'; ctx.fillRect(0,0,c.width,c.height)
  ctx.fillStyle='rgba(255,255,255,.03)'
  for(let x=0;x<c.width;x+=32) for(let y=0;y<c.height;y+=32) ctx.fillRect(x,y,1,1)
  // walls
  ctx.fillStyle='#1a3a2a'; walls.forEach(w=>{ ctx.fillRect(w.x,w.y,w.w,w.h); ctx.strokeStyle='#2a5a3a'; ctx.strokeRect(w.x,w.y,w.w,w.h)})
  // shrine
  const pulse = Math.sin(Date.now()/300)*4
  ctx.fillStyle='#0d281e'; ctx.beginPath(); ctx.arc(shrine.x,shrine.y, 28+pulse*0.3,0,Math.PI*2); ctx.fill()
  ctx.fillStyle= facingShrine? '#4ade80':'#2a6b4a'; ctx.beginPath(); ctx.arc(shrine.x,shrine.y,10,0,Math.PI*2); ctx.fill()
  ctx.fillStyle='#ffb338'; ctx.font='14px monospace'; ctx.textAlign='center'; ctx.fillText('✧',shrine.x,shrine.y+5)
  ctx.fillStyle='rgba(255,255,255,.6)'; ctx.font='9px JetBrains Mono'; ctx.fillText('SHRINE',shrine.x,shrine.y+24)
  // shop
  ctx.fillStyle='#1f3a2e'; ctx.fillRect(shopPos.x-28,shopPos.y-20,56,40); ctx.strokeStyle='#ffb338'; ctx.lineWidth=2; ctx.strokeRect(shopPos.x-28,shopPos.y-20,56,40)
  ctx.font='20px monospace'; ctx.textAlign='center'; ctx.fillText('⛺',shopPos.x,shopPos.y+7)
  ctx.fillStyle='#ffb338'; ctx.font='8px Press Start 2P'; ctx.fillText('SHOP',shopPos.x,shopPos.y+22)
  // chests
  chests.forEach(ch=>{
    if(ch.opened){ ctx.fillStyle='#0b1a14'; ctx.fillRect(ch.x-14,ch.y-14,ch.w,ch.h); ctx.strokeStyle='#2a4a38'; ctx.strokeRect(ch.x-14,ch.y-14,ch.w,ch.h); ctx.fillStyle='#5a7a68'; ctx.font='10px monospace'; ctx.textAlign='center'; ctx.fillText('✓',ch.x, ch.y+4); return}
    const glow = ch.type.includes('upgrade') || ch.type==='bow' || ch.type.includes('heart')
    if(glow){ ctx.shadowColor='#ffb338'; ctx.shadowBlur=12}
    ctx.fillStyle= ch.type==='upgrade2' ? '#7c2d12' : ch.type==='bow' ? '#0ea5e9' : ch.type.includes('heart') ? '#b91c1c' : ch.type==='sigil' ? '#7c3aed' : '#8b5a2b'
    ctx.fillRect(ch.x-14,ch.y-14,ch.w,ch.h); ctx.shadowBlur=0
    ctx.fillStyle='#ffd27a'; ctx.fillRect(ch.x-14,ch.y-14,ch.w,6)
    ctx.strokeStyle='#000'; ctx.lineWidth=2; ctx.strokeRect(ch.x-14,ch.y-14,ch.w,ch.h)
    ctx.fillStyle='#fff'; ctx.font='12px monospace'; ctx.textAlign='center'; ctx.fillText('▢',ch.x,ch.y+4)
    // label
    ctx.fillStyle='rgba(0,0,0,.6)'; ctx.fillRect(ch.x-42,ch.y-34,84,12); ctx.fillStyle='#fff'; ctx.font='7px JetBrains Mono'; ctx.fillText(ch.type.toUpperCase(),ch.x,ch.y-25)
  })
  pickups.forEach(p=>{ if(p.taken) return; ctx.fillStyle='#ffb338'; ctx.beginPath(); ctx.arc(p.x,p.y,6,0,Math.PI*2); ctx.fill(); ctx.fillStyle='#000'; ctx.font='8px monospace'; ctx.textAlign='center'; ctx.fillText('¢',p.x,p.y+3)})
  // enemies
  enemies.forEach(en=>{
    if(!en.alive) return
    ctx.fillStyle= en.hit>0?'#fff':'#ff4d4d'; ctx.beginPath(); ctx.arc(en.x,en.y,12,0,Math.PI*2); ctx.fill()
    ctx.strokeStyle='#000'; ctx.lineWidth=2; ctx.stroke()
    ctx.fillStyle='#000'; ctx.beginPath(); ctx.arc(en.x-4,en.y-3,2,0,Math.PI*2); ctx.arc(en.x+4,en.y-3,2,0,Math.PI*2); ctx.fill()
    // hp bar
    ctx.fillStyle='#000'; ctx.fillRect(en.x-14,en.y-20,28,4); ctx.fillStyle='#4ade80'; ctx.fillRect(en.x-14,en.y-20,28*(en.hp/en.max),4)
  })
  // projectiles
  ctx.fillStyle='#4ade80'; projectiles.forEach(pr=>{ ctx.beginPath(); ctx.arc(pr.x,pr.y,4,0,Math.PI*2); ctx.fill(); ctx.fillStyle='rgba(74,222,128,.3)'; ctx.beginPath(); ctx.arc(pr.x,pr.y,8,0,Math.PI*2); ctx.fill(); ctx.fillStyle='#4ade80' })
  // player
  const bob = player.moving ? Math.sin(walkTick*0.7)*2 : 0
  ctx.save(); ctx.translate(player.x, player.y + bob)
  if(player.invuln>0 && Math.floor(Date.now()/80)%2===0) ctx.globalAlpha=0.4
  // shadow
  ctx.fillStyle='rgba(0,0,0,.35)'; ctx.beginPath(); ctx.ellipse(0,12,12,6,0,0,Math.PI*2); ctx.fill()
  // body
  ctx.fillStyle='#1e3a2e'; ctx.fillRect(-10,-12,20,18); ctx.strokeStyle='#000'; ctx.lineWidth=2; ctx.strokeRect(-10,-12,20,18)
  // head
  ctx.fillStyle='#fde68a'; ctx.beginPath(); ctx.arc(0,-16,9,0,Math.PI*2); ctx.fill(); ctx.stroke()
  // weapon visual based on tier
  ctx.rotate(player.dir)
  const wcol = state.weaponTier===0?'#cbd5e1': state.weaponTier===1?'#94a3b8' : '#ff7a18'
  ctx.fillStyle=wcol; ctx.fillRect(8,-3, weapons[state.weaponTier].range*0.45,6); ctx.fillStyle='#000'; ctx.fillRect(8,-1,4,2)
  if(state.hasBow){ ctx.fillStyle='#4ade80'; ctx.beginPath(); ctx.arc(-10,0,4, -0.9,0.9); ctx.strokeStyle='#4ade80'; ctx.lineWidth=2; ctx.stroke()}
  ctx.restore()
  // particles
  particles.forEach(p=>{ ctx.globalAlpha=Math.max(0,p.life); ctx.fillStyle=p.color; ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,Math.PI*2); ctx.fill()}); ctx.globalAlpha=1
}

let last=performance.now()
function loop(now){
  const dt= now-last; last=now
  update(dt); draw(); requestAnimationFrame(loop)
}
buildWorld(); updateHUD(); buildShop(); requestAnimationFrame(loop)

// expose
window.buy=buy; window.closeShop=closeShop; window.doSave=doSave; window.doLoad=doLoad
</script>
</body>
</html>
<!-- agent-meta {"model":"meta/muse-spark-1.2","provider":"openrouter","persona":"basic","sessionId":"0b4080f1-d82b-4821-8640-e9bab3f6d839","tokensIn":706,"tokensOut":12131,"tokensTotal":12837,"cost":0.05243925,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-08-06T15:32:49.313Z"} -->