RPG
muse spark 1.2 · 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 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"} -->