← RPG results

RPG

GPT 6 Luna · full

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 34,492 bytes · SHA-256 b660d625d2eb
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"><title>Hollowvale — The Ember Trail</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Manrope:wght@400;500;600;700;800&display=swap');
:root{--ink:#182422;--muted:#74807a;--paper:#f2f0e7;--line:#d8d7ca;--gold:#e8b85c;--green:#5b896d}*{box-sizing:border-box}body{margin:0;background:#e9e7dc;color:var(--ink);font-family:Manrope,system-ui,sans-serif;min-height:100vh;display:flex;justify-content:center;align-items:center;padding:18px}.shell{width:min(1240px,100%);background:#f7f5ec;border:1px solid #d6d4c8;box-shadow:0 22px 70px #20282020;border-radius:17px;overflow:hidden}.top{height:61px;padding:0 23px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line)}.brand{display:flex;align-items:center;gap:11px;font-weight:800;letter-spacing:.08em;font-size:13px}.brandmark{width:28px;height:28px;background:#263b35;border-radius:8px;display:grid;place-items:center;color:#edc777;font-size:16px}.topright{display:flex;align-items:center;gap:18px;color:#64716b;font-size:11px;font-weight:700;letter-spacing:.12em;text-transform:uppercase}.live{display:flex;align-items:center;gap:7px}.live i{width:7px;height:7px;background:#75a675;border-radius:50%;box-shadow:0 0 0 3px #75a67522}.stage{display:grid;grid-template-columns:minmax(0,1fr) 260px;min-height:570px}.gamearea{position:relative;background:#283d37;min-width:0;overflow:hidden}.canvaswrap{height:100%;min-height:570px;position:relative;display:flex;align-items:center;justify-content:center}.canvaswrap canvas{width:100%;height:100%;max-height:680px;object-fit:contain;display:block;image-rendering:pixelated}.side{background:#f7f5ec;border-left:1px solid var(--line);padding:21px 18px;display:flex;flex-direction:column;gap:17px}.eyebrow{font-family:'DM Mono',monospace;font-size:9px;color:#8a9186;letter-spacing:.16em;text-transform:uppercase}.region{font-size:20px;font-weight:800;margin:5px 0 3px;letter-spacing:-.04em}.regiondesc{font-size:11px;color:#7c847b;line-height:1.5}.rule{height:1px;background:var(--line)}.statrow{display:flex;align-items:center;justify-content:space-between;font-size:11px;color:#727b72}.statrow strong{color:#25342f;font-family:'DM Mono',monospace;font-size:12px}.hearts{display:flex;gap:5px;margin-top:10px}.heart{width:24px;height:24px;background:#e4a58c;clip-path:polygon(50% 88%,8% 48%,8% 30%,16% 15%,32% 13%,50% 30%,68% 13%,84% 15%,92% 30%,92% 48%);position:relative}.heart:after{content:'';position:absolute;inset:3px;background:#f7f5ec;clip-path:polygon(50% 89%,8% 48%,8% 30%,16% 15%,32% 13%,50% 30%,68% 13%,84% 15%,92% 30%,92% 48%)}.heart.full{background:#dd705b}.heart.full:after{display:none}.heart.half{background:linear-gradient(90deg,#dd705b 50%,#e4a58c 50%)}.heart.half:after{display:none}.currency{display:flex;align-items:center;gap:7px;color:#99703a;font-weight:800}.coin{width:14px;height:14px;border-radius:50%;background:#e9bf65;border:2px solid #c5933d;display:inline-block}.weapon{display:flex;align-items:center;gap:10px;padding:10px;background:#eeede3;border-radius:9px}.weaponico{width:33px;height:33px;border-radius:8px;background:#34483f;color:#edc777;display:grid;place-items:center;font-size:17px}.weapon strong{font-size:11px;display:block}.weapon span{font-size:9px;color:#788078}.progress{height:5px;background:#e3e2d8;border-radius:8px;margin-top:8px;overflow:hidden}.progress i{display:block;height:100%;background:#6e9b78;border-radius:8px;transition:width .3s}.objective{background:#edf0e5;padding:12px;border-radius:9px;border:1px solid #e0e4d8}.objective .eyebrow{color:#729078}.objective p{font-size:11px;line-height:1.55;margin:7px 0 0;color:#43534a}.itemchips{display:flex;gap:5px;flex-wrap:wrap;margin-top:8px}.chip{border:1px solid #d6d8cb;border-radius:20px;padding:4px 7px;font-size:9px;color:#59675d;background:#f5f4ec}.controls{margin-top:auto}.keygrid{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-top:9px}.key{font-size:9px;color:#7a8179;display:flex;align-items:center;gap:6px}.kbd{font-family:'DM Mono',monospace;color:#48554d;border:1px solid #d4d4c9;background:#fbfaf4;padding:3px 5px;border-radius:4px;font-size:9px}.bottom{height:42px;border-top:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;padding:0 20px;color:#92958a;font-family:'DM Mono',monospace;font-size:9px;letter-spacing:.1em}.bottom b{color:#6f796f;font-weight:500}.toast{position:absolute;top:18px;left:50%;transform:translate(-50%,-10px);padding:10px 16px;background:#f5f1df;border:1px solid #d9c994;border-radius:8px;color:#4b4b39;font-size:12px;font-weight:700;opacity:0;transition:.25s;pointer-events:none;white-space:nowrap;z-index:3;box-shadow:0 6px 20px #1113}.toast.show{opacity:1;transform:translate(-50%,0)}.overlay{position:absolute;inset:0;background:#172722c9;display:none;align-items:center;justify-content:center;z-index:4;padding:20px}.overlay.open{display:flex}.panel{width:min(430px,100%);background:#f6f4e9;border:1px solid #e0dcc7;border-radius:14px;padding:22px;box-shadow:0 18px 50px #0005}.paneltop{display:flex;justify-content:space-between;align-items:start}.panel h2{font-size:22px;letter-spacing:-.04em;margin:5px 0}.close{border:0;background:#e7e5d9;border-radius:6px;padding:6px 10px;color:#58635b;cursor:pointer;font-weight:700}.panel p{font-size:12px;line-height:1.6;color:#687268}.shoprow,.invrow{display:flex;align-items:center;gap:11px;padding:10px 0;border-top:1px solid #e0dfd3}.shoprow .desc,.invrow .desc{flex:1}.shoprow strong,.invrow strong{font-size:12px}.shoprow small,.invrow small{display:block;color:#81877c;font-size:10px;margin-top:3px}.buy{border:0;background:#31473e;color:#f5e9c5;padding:8px 11px;border-radius:6px;font-size:10px;font-weight:800;cursor:pointer}.buy:disabled{background:#b8b8aa;color:#efeee6;cursor:default}.iconbox{width:34px;height:34px;border-radius:8px;background:#e8eadf;display:grid;place-items:center;font-size:17px}.mobilehelp{display:none}
@media(max-width:820px){body{padding:0;display:block}.shell{width:100%;min-height:100vh;border-radius:0}.top{height:52px;padding:0 14px}.topright{gap:9px;font-size:9px}.stage{display:flex;flex-direction:column}.gamearea{height:58vh;min-height:360px}.canvaswrap{min-height:360px}.side{display:grid;grid-template-columns:1fr 1fr;padding:13px;gap:10px;border-left:0;border-top:1px solid var(--line)}.side .controls{display:none}.side .objective{grid-column:span 2}.side .rule{display:none}.side .regiondesc{font-size:10px}.bottom{height:34px}.mobilehelp{display:block;color:#aeb9aa;font-size:9px;position:absolute;bottom:8px;left:10px}}
</style>
</head><body><div class="shell"><header class="top"><div class="brand"><span class="brandmark">✦</span> HOLLOWVALE <span style="font-size:9px;color:#919589;font-weight:600;letter-spacing:.16em">FIELD JOURNAL</span></div><div class="topright"><span class="live"><i></i> FIELD ACTIVE</span><span>DAY 01</span></div></header><main class="stage"><section class="gamearea"><div class="canvaswrap"><canvas id="game" width="960" height="600"></canvas><div class="toast" id="toast"></div><div class="mobilehelp">MOVE · WASD / ARROWS &nbsp; ATTACK · SPACE &nbsp; INTERACT · E</div><div class="overlay" id="overlay"><div class="panel" id="panel"></div></div></div></section><aside class="side"><div><div class="eyebrow">Current region</div><div class="region" id="region">Mosslight Glade</div><div class="regiondesc" id="regiondesc">A quiet forest, though something stirs in the ferns.</div></div><div><div class="statrow"><span>VITALITY</span><strong id="hptext">6 / 6</strong></div><div class="hearts" id="hearts"></div></div><div class="statrow"><span>POUCH</span><strong class="currency"><i class="coin"></i><span id="coins">0</span></strong></div><div class="weapon"><div class="weaponico" id="weaponico">⚔</div><div><strong id="weaponname">Trailblade</strong><span id="weaponmeta">Balanced · close range</span></div></div><div class="objective"><div class="eyebrow">Trail objective <span id="progresslabel" style="float:right">0 / 4</span></div><p id="objective">Clear the glade and find the old trail chest.</p><div class="progress"><i id="progressbar" style="width:0%"></i></div><div class="itemchips" id="chips"></div></div><div class="controls"><div class="eyebrow">Field controls</div><div class="keygrid"><div class="key"><span class="kbd">WASD</span> Move</div><div class="key"><span class="kbd">SPACE</span> Attack</div><div class="key"><span class="kbd">E</span> Interact</div><div class="key"><span class="kbd">I</span> Inventory</div><div class="key"><span class="kbd">Q</span> Ranged</div><div class="key"><span class="kbd">SHIFT</span> Dash</div></div></div></aside></main><footer class="bottom"><span>AN EMBER TRAIL <b>·</b> A SHORT ADVENTURE</span><span id="saveinfo">NO CHECKPOINT MARKED</span></footer></div>
<script>
(()=>{'use strict';
const c=document.getElementById('game'),ctx=c.getContext('2d'),W=960,H=600;
const regions=[{name:'Mosslight Glade',desc:'A quiet forest, though something stirs in the ferns.',objective:'Clear the glade. Find the trail chest and mark your first save point.'},{name:'The Sunken Archive',desc:'A buried library. Its warding mechanism is still alive.',objective:'Defeat the archive keeper, then activate the two rune stones.'},{name:'Cinderworks',desc:'The mountain forge hums beneath a veil of ash.',objective:'The midway cache holds a ranged tool. Break the three cinder seals.'},{name:'The Crown of Hollowvale',desc:'The last ascent. The old guardian waits above.',objective:'Climb the crown, claim the final edge, and face the Hollow Warden.'}];
const player={x:135,y:300,r:13,hp:6,max:6,coins:0,dir:0,weapon:0,hasBow:false,items:[],potions:0,guard:false,boots:false,attack:0,invuln:0,dash:0,step:0,walk:0};
const weaponData=[['Trailblade','Balanced · close range','⚔'],['Emberfang','Fire edge · wide arc','✦'],['Stormcleaver','Lightning · long reach','ϟ']];
let room=0,keys={},paused=false,dialog=null,toastTimer=0,last=0,saveRoom=0,saveX=135,saveY=300,gameDone=false,gameStarted=false,roomElapsed=0,shards=0,worldTick=0;
const flags={chest:false,heartA:false,keeper:false,runeA:false,runeB:false,midCache:false,seal1:false,seal2:false,seal3:false,boss1:false,boss3:false,save0:false,save2:false};
const enemies=[],pickups=[],particles=[],projectiles=[],switches=[];
const roomDone=[false,false,false,false];
let audio;
function sound(type){try{audio=audio||new(window.AudioContext||window.webkitAudioContext)();if(audio.state==='suspended')audio.resume();let o=audio.createOscillator(),g=audio.createGain();o.connect(g);g.connect(audio.destination);let t=audio.currentTime;let cfg={walk:[95,.025,'sine'],hit:[115,.12,'sawtooth'],attack:[210,.07,'triangle'],pickup:[660,.12,'sine'],shop:[440,.18,'sine'],hurt:[90,.22,'square'],switch:[330,.2,'triangle'],bow:[520,.1,'triangle'] }[type]||[250,.08,'sine'];o.type=cfg[2];o.frequency.setValueAtTime(cfg[0],t);if(type==='pickup'||type==='shop')o.frequency.exponentialRampToValueAtTime(cfg[0]*1.5,t+.12);g.gain.setValueAtTime(.0001,t);g.gain.exponentialRampToValueAtTime(.075,t+.012);g.gain.exponentialRampToValueAtTime(.0001,t+cfg[1]);o.start(t);o.stop(t+cfg[1]+.03)}catch(e){}}
function say(s){let t=document.getElementById('toast');t.textContent=s;t.classList.add('show');toastTimer=2.2}
function addParticle(x,y,color,n=9){for(let i=0;i<n;i++){let a=Math.random()*Math.PI*2,v=35+Math.random()*115;particles.push({x,y,vx:Math.cos(a)*v,vy:Math.sin(a)*v,life:.3+Math.random()*.5,max:.8,color,size:2+Math.random()*3})}}
function makeEnemy(x,y,type='mote',hp=2){enemies.push({x,y,type,hp,max:hp,r:type==='warden'?22:13,speed:type==='warden'?47:type==='keeper'?48:36,phase:Math.random()*6,hit:0,attackWait:1+Math.random()*2,dead:false})}
function spawnRoom(){enemies.length=0;pickups.length=0;switches.length=0;projectiles.length=0;
if(room===0){if(!roomDone[0]){makeEnemy(530,210,'mote',2);makeEnemy(670,390,'mote',2);makeEnemy(790,250,'mote',3)} if(!flags.chest)pickups.push({x:830,y:470,type:'chest',label:'Trail chest'});if(!flags.heartA)pickups.push({x:235,y:145,type:'heart',label:'Heart vessel'});if(!player.items.includes('Wildmint'))pickups.push({x:380,y:475,type:'mint',label:'Wildmint'});if(!flags.save0)switches.push({x:120,y:500,type:'save',label:'Waystone'})}
if(room===1){if(!flags.keeper)makeEnemy(690,275,'keeper',7);switches.push({x:360,y:200,type:'runeA',label:'Sun rune'});switches.push({x:360,y:420,type:'runeB',label:'Moon rune'});if(!player.items.includes('Amber charm'))pickups.push({x:165,y:430,type:'charm',label:'Amber charm'});if(!flags.boss1)pickups.push({x:790,y:475,type:'upgrade1',label:'Emberfang'})}
if(room===2){if(!flags.midCache)pickups.push({x:175,y:160,type:'cache',label:'Ranger cache'});if(!flags.heartB)pickups.push({x:830,y:480,type:'heart',label:'Heart vessel'});if(!flags.seal1)makeEnemy(420,175,'cinder',4);if(!flags.seal2)makeEnemy(650,300,'cinder',4);if(!flags.seal3)makeEnemy(420,445,'cinder',4);if(!flags.save2)switches.push({x:120,y:500,type:'save',label:'Forge waystone'});if(!player.items.includes('Ash boots'))pickups.push({x:790,y:150,type:'boots',label:'Ash boots'})}
if(room===3){if(!flags.boss3)makeEnemy(690,300,'warden',18);if(!flags.boss3)pickups.push({x:180,y:475,type:'upgrade2',label:'Stormcleaver'});if(!player.items.includes('Sun tonic'))pickups.push({x:140,y:140,type:'tonic',label:'Sun tonic'})}
}
function updateUI(){document.getElementById('region').textContent=regions[room].name;document.getElementById('regiondesc').textContent=regions[room].desc;document.getElementById('hptext').textContent=`${player.hp} / ${player.max}`;let hearts=document.getElementById('hearts');hearts.innerHTML='';for(let i=0;i<player.max/2;i++){let h=document.createElement('div');h.className='heart '+(player.hp>=i*2+2?'full':player.hp===i*2+1?'half':'');hearts.appendChild(h)}document.getElementById('coins').textContent=player.coins;let wd=weaponData[player.weapon];document.getElementById('weaponname').textContent=wd[0];document.getElementById('weaponmeta').textContent=wd[1]+(player.hasBow?' · bow ready':'');document.getElementById('weaponico').textContent=wd[2];let n=roomDone.filter(Boolean).length;document.getElementById('progresslabel').textContent=`${n} / 4`;document.getElementById('progressbar').style.width=(n*25)+'%';document.getElementById('objective').textContent=room===2&&!player.hasBow?'Midway cache ahead — claim the bow before taking on the cinder seals.':room===0&&!flags.chest?'Clear the glade. Find the trail chest and mark your first save point.':regions[room].objective;let chips=document.getElementById('chips');chips.innerHTML='';for(let it of player.items){let x=document.createElement('span');x.className='chip';x.textContent=it;chips.appendChild(x)}if(player.potions){let x=document.createElement('span');x.className='chip';x.textContent=`Tonic ×${player.potions}`;chips.appendChild(x)}document.getElementById('saveinfo').textContent=`CHECKPOINT · ${regions[saveRoom].name.toUpperCase()}`}
function enterRoom(r,fromRight=false){room=Math.max(0,Math.min(3,r));player.x=fromRight?W-70:70;player.y=300;roomElapsed=0;spawnRoom();updateUI();say(regions[room].name);}
function transition(dir){if(dir>0){if(room===0&&!flags.chest){say('The trail chest still waits.');return}if(room===1&&!flags.boss1){say('The archive seal holds.');return}if(room===2&&!(flags.seal1&&flags.seal2&&flags.seal3)){say('The cinder seals are still lit.');return}if(room===3&&flags.boss3)return;if(room<3)enterRoom(room+1,false)}else if(room>0)enterRoom(room-1,true)}
function hitEnemy(e,damage,kind){if(e.dead)return;e.hp-=damage;e.hit=.18;addParticle(e.x,e.y,kind==='fire'?'#f28b4d':kind==='spark'?'#e8d477':'#f1d9a0',8);sound('hit');if(e.hp<=0){e.dead=true;let cash=e.type==='warden'?50:e.type==='keeper'?24:8;player.coins+=cash;addParticle(e.x,e.y,'#e8bd62',12);if(e.type==='keeper'){flags.keeper=true;flags.boss1=true;player.weapon=1;player.max=Math.min(10,player.max+2);player.hp=Math.min(player.max,player.hp+2);say('Keeper defeated · Emberfang awakened · +1 heart vessel');roomDone[1]=false;}if(e.type==='warden'){flags.boss3=true;roomDone[3]=true;player.weapon=2;say('The Hollow Warden falls. Stormcleaver is yours. Trail complete!');gameDone=true;}if(e.type==='cinder'){let k=e.x<500?(e.y<300?'seal1':'seal3'):'seal2';flags[k]=true;roomDone[2]=flags.seal1&&flags.seal2&&flags.seal3;if(roomDone[2])say('All cinder seals broken. The ascent is open.');}else if(e.type==='mote'){}updateUI()}}
function attack(){if(player.attack>0||paused)return;player.attack=player.weapon===2?.36:player.weapon===1?.48:.54;sound('attack');let range=player.weapon===2?87:player.weapon===1?61:48;let angle=player.dir;for(let e of enemies){if(e.dead)continue;let dx=e.x-player.x,dy=e.y-player.y,d=Math.hypot(dx,dy),a=Math.atan2(dy,dx);let diff=Math.atan2(Math.sin(a-angle),Math.cos(a-angle));if(d<range+e.r&&Math.abs(diff)<(player.weapon===1?.9:.72))hitEnemy(e,player.weapon===2?3:player.weapon===1?3:2,player.weapon===1?'fire':player.weapon===2?'spark':'steel')}}
function fireBow(){if(!player.hasBow){say('No ranged weapon yet. Search the midway cache.');return}if(player.attack>0)return;player.attack=.42;sound('bow');projectiles.push({x:player.x+Math.cos(player.dir)*18,y:player.y+Math.sin(player.dir)*18,vx:Math.cos(player.dir)*390,vy:Math.sin(player.dir)*390,life:1.3});}
function near(obj,range=48){return Math.hypot(obj.x-player.x,obj.y-player.y)<range}
function collect(p){if(p.type==='chest'){flags.chest=true;player.coins+=32;player.items.push('Trail key');roomDone[0]=true;say('Trail chest opened · +32 coins · trail key');addParticle(p.x,p.y,'#f3cc75',20)}else if(p.type==='heart'){if(room===0)flags.heartA=true;else flags.heartB=true;player.max=Math.min(10,player.max+2);player.hp=player.max;say('Heart vessel found · +1 container');addParticle(p.x,p.y,'#e98672',18)}else if(p.type==='mint'){player.items.push('Wildmint');player.potions++;say('Wildmint · restores 2 health');}else if(p.type==='charm'){player.items.push('Amber charm');player.guard=true;say('Amber charm · damage taken reduced');}else if(p.type==='boots'){player.items.push('Ash boots');player.boots=true;say('Ash boots · swift travel');}else if(p.type==='tonic'){if(player.items.includes('Sun tonic'))return;player.items.push('Sun tonic');player.potions++;say('Sun tonic collected');}else if(p.type==='cache'){flags.midCache=true;player.hasBow=true;player.items.push('Wayfinder bow');player.coins+=16;say('MIDWAY DISCOVERY · Wayfinder bow · +16 coins');sound('pickup')}else if(p.type==='upgrade1'){if(!flags.boss1){say('Defeat the keeper to claim the Emberfang.');return}player.weapon=1;}else if(p.type==='upgrade2'){if(!flags.boss3){say('The Warden guards the Stormcleaver.');return}player.weapon=2;}sound('pickup');if(p.type!=='cache')addParticle(p.x,p.y,'#edc76c',12);pickups.splice(pickups.indexOf(p),1);updateUI()}
function interact(){if(paused){closePanel();return}let s=switches.find(x=>near(x,55));if(s){if(s.type==='save'){saveRoom=room;saveX=player.x;saveY=player.y;if(room===0)flags.save0=true;if(room===2)flags.save2=true;sound('switch');say('Waystone marked · progress saved');spawnRoom();return}if(s.type==='runeA'||s.type==='runeB'){if(!flags.keeper){say('The keeper’s ward blocks the rune.');return}flags[s.type]=true;sound('switch');addParticle(s.x,s.y,'#8be0d2',14);say(s.type==='runeA'?'Sun rune aligned.':'Moon rune aligned.');if(flags.runeA&&flags.runeB){roomDone[1]=true;player.coins+=24;player.max=Math.min(10,player.max+2);player.hp=player.max;player.items.push('Archive sigil');player.weapon=1;say('Archive solved · +24 coins · +1 heart vessel');}updateUI();return}}
let p=pickups.find(x=>near(x,46));if(p){if(p.type==='chest'||p.type==='heart'||p.type==='mint'||p.type==='charm'||p.type==='boots'||p.type==='cache'||p.type==='tonic'){collect(p);return}if(p.type==='upgrade1'&&!flags.boss1){say('The Emberfang rests beyond the keeper.');return}if(p.type==='upgrade2'&&!flags.boss3){say('The Stormcleaver rests beyond the Warden.');return}collect(p);return}
if(room===1&&Math.hypot(player.x-135,player.y-300)<90){openShop();return}if(room===3&&gameDone){openEnding();return}if(player.x>W-62){transition(1);return}if(player.x<62){transition(-1);return}say('Nothing nearby to interact with.')}
function openPanel(html){paused=true;let ov=document.getElementById('overlay');document.getElementById('panel').innerHTML=html;ov.classList.add('open')}
function closePanel(){paused=false;document.getElementById('overlay').classList.remove('open')}
function openShop(){sound('shop');let inventory=[['Wildmint','Restores 2 health · usable from inventory',12,'🌿','mint'],['Copper ward','A second charm; reduces damage further',22,'◈','ward'],['Smoke bomb','A one-use burst that harms nearby foes',18,'✹','bomb'],['Sun tonic','Restores 4 health · one use',20,'✚','tonic']];let rows=inventory.map(it=>`<div class="shoprow"><div class="iconbox">${it[3]}</div><div class="desc"><strong>${it[0]}</strong><small>${it[1]}</small></div><button class="buy" onclick="buyItem('${it[4]}',${it[2]})">${it[2]} ◉</button></div>`).join('');openPanel(`<div class="paneltop"><div><div class="eyebrow">Wayfarer's stall · ${player.coins} coins</div><h2>Stock for the road</h2></div><button class="close" onclick="closePanel()">✕</button></div><p>Hard-won coin for practical comforts. Purchases stay with you.</p>${rows}`)}
window.closePanel=closePanel;window.buyItem=function(type,cost){if(player.coins<cost){say('Not enough coin.');return}player.coins-=cost;sound('shop');if(type==='mint'){player.items.push('Wildmint');player.potions++}if(type==='ward'){player.items.push('Copper ward');player.guard=true}if(type==='bomb')player.items.push('Smoke bomb');if(type==='tonic'){player.items.push('Sun tonic');player.potions++}updateUI();openShop();say('Added to your pack.')};
function openInventory(){let rows=player.items.map(x=>`<div class="invrow"><div class="iconbox">${({Wildmint:'🌿','Sun tonic':'✚','Amber charm':'◈','Copper ward':'◇','Ash boots':'⌁','Wayfinder bow':'➶','Smoke bomb':'✹','Trail key':'⚿','Archive sigil':'☼'})[x]||'✦'}</div><div class="desc"><strong>${x}</strong><small>${itemDesc(x)}</small></div>${(x==='Wildmint'||x==='Sun tonic')?'<button class="buy" onclick="useTonic()">USE</button>':''}${x==='Smoke bomb'?'<button class="buy" onclick="useBomb()">USE</button>':''}</div>`).join('');openPanel(`<div class="paneltop"><div><div class="eyebrow">Field pack · ${player.items.length} keepsakes</div><h2>Your inventory</h2></div><button class="close" onclick="closePanel()">✕</button></div><p>Consumables are used here. Charms and boots are active as soon as they are found.</p>${rows||'<p>Your pack is empty. Explore the glade.</p>'}<div class="rule"></div><p style="margin-bottom:0">Wildmint / tonic charges: <b>${player.potions}</b> · Coins: <b>${player.coins}</b></p>`)}
function itemDesc(x){return {'Wildmint':'Small restorative · +2 health','Sun tonic':'Restorative · +4 health','Amber charm':'Passive · soften incoming hits','Copper ward':'Passive · sturdy protection','Ash boots':'Passive · move faster','Wayfinder bow':'Ranged · Q to fire','Smoke bomb':'Consumable · damage nearby foes','Trail key':'A brass key with a leaf-shaped bow','Archive sigil':'Proof the archive is quiet'}[x]||'A useful find.'}
window.useTonic=function(){if(!player.potions){say('No restorative charges.');return}if(player.hp>=player.max){say('Already at full health.');return}player.potions--;player.hp=Math.min(player.max,player.hp+2);sound('pickup');updateUI();openInventory();say('Restored 2 health.')};window.useBomb=function(){let i=player.items.indexOf('Smoke bomb');if(i<0)return;player.items.splice(i,1);for(let e of enemies)if(!e.dead&&Math.hypot(e.x-player.x,e.y-player.y)<145)hitEnemy(e,3,'fire');addParticle(player.x,player.y,'#c5e1ca',28);updateUI();openInventory()};
function openEnding(){openPanel(`<div class="eyebrow">The trail goes on</div><h2>Hollowvale remembers.</h2><p>You carried the light from Mosslight to the crown. Every waystone you marked will still be here when the next road calls.</p><div class="objective"><b>Adventure complete</b><p>Rooms cleared: ${roomDone.filter(Boolean).length}/4 · Keepsakes: ${player.items.length} · Coin remaining: ${player.coins}</p></div><button class="buy" style="margin-top:14px" onclick="closePanel()">RETURN TO THE CROWN</button>`)}
function damagePlayer(){if(player.invuln>0)return;player.hp-=player.guard?1:2;player.invuln=1.2;sound('hurt');addParticle(player.x,player.y,'#f07869',12);say('Hit! Find space to recover.');if(player.hp<=0){player.hp=player.max;player.coins=Math.floor(player.coins*.7);enterRoom(saveRoom);player.x=saveX;player.y=saveY;say('You wake at the waystone. Some coin was lost.')}updateUI()}
function update(dt){worldTick+=dt;if(toastTimer>0){toastTimer-=dt;if(toastTimer<=0)document.getElementById('toast').classList.remove('show')}if(paused)return;roomElapsed+=dt;player.attack=Math.max(0,player.attack-dt);player.invuln=Math.max(0,player.invuln-dt);player.dash=Math.max(0,player.dash-dt);let x=0,y=0;if(keys.ArrowLeft||keys.KeyA)x--;if(keys.ArrowRight||keys.KeyD)x++;if(keys.ArrowUp||keys.KeyW)y--;if(keys.ArrowDown||keys.KeyS)y++;if(x||y){let m=Math.hypot(x,y);x/=m;y/=m;player.dir=Math.atan2(y,x);let speed=(player.boots?188:156)*(player.dash>0?2.25:1);let nx=player.x+x*speed*dt,ny=player.y+y*speed*dt;player.x=Math.max(20,Math.min(W-20,nx));player.y=Math.max(62,Math.min(H-25,ny));player.walk+=dt;if(player.walk>.28){sound('walk');player.walk=0}}
if(keys.Space){keys.Space=false;attack()}if(keys.KeyQ){keys.KeyQ=false;fireBow()}if(keys.ShiftLeft&&player.dash<=0&&(x||y)){player.dash=.18;player.invuln=Math.max(player.invuln,.22)}
for(let e of enemies){if(e.dead)continue;e.phase+=dt;e.hit=Math.max(0,e.hit-dt);let dx=player.x-e.x,dy=player.y-e.y,d=Math.hypot(dx,dy);if(d<250){let sp=e.speed*(d<36?-.2:1);e.x+=dx/(d||1)*sp*dt;e.y+=dy/(d||1)*sp*dt;if(d<e.r+player.r+4){e.attackWait-=dt;if(e.attackWait<=0){damagePlayer();e.attackWait=1.2}}}else{e.x+=Math.sin(e.phase)*8*dt;e.y+=Math.cos(e.phase*.8)*8*dt}}
for(let p of projectiles){p.x+=p.vx*dt;p.y+=p.vy*dt;p.life-=dt;for(let e of enemies)if(!e.dead&&Math.hypot(p.x-e.x,p.y-e.y)<e.r+7){hitEnemy(e,3,'spark');p.life=0;break}}for(let i=projectiles.length-1;i>=0;i--)if(projectiles[i].life<=0)projectiles.splice(i,1);
for(let p of particles){p.x+=p.vx*dt;p.y+=p.vy*dt;p.vx*=.96;p.vy*=.96;p.life-=dt}for(let i=particles.length-1;i>=0;i--)if(particles[i].life<=0)particles.splice(i,1);
if(player.x>W-28&&room<3){player.x=W-28}if(player.x<28&&room>0)player.x=28;
}
function rounded(x,y,w,h,r,color){ctx.fillStyle=color;ctx.beginPath();ctx.roundRect(x,y,w,h,r);ctx.fill()}
function draw(){ctx.clearRect(0,0,W,H);let t=worldTick;let base=['#344b3d','#363f3b','#493e34','#353e3a'][room];ctx.fillStyle=base;ctx.fillRect(0,0,W,H);
// subtle room floor texture
for(let i=0;i<100;i++){let x=(i*137+43)%W,y=(i*83+29)%H;ctx.globalAlpha=.09;ctx.fillStyle=room===0?'#b7c98c':room===2?'#d49055':'#d0cba9';if(room===1){ctx.strokeStyle=ctx.fillStyle;ctx.lineWidth=1;ctx.strokeRect(x,y,24,17)}else{ctx.beginPath();ctx.ellipse(x,y,4+(i%4),2+(i%3),.3,0,Math.PI*2);ctx.fill()}}ctx.globalAlpha=1;
// border path / exits
ctx.fillStyle=room===0?'#506244':room===2?'#6d4d37':'#59604d';ctx.fillRect(0,0,W,48);ctx.fillRect(0,H-22,W,22);ctx.fillStyle='#d1bd86';ctx.fillRect(0,49,W,2);ctx.fillRect(0,H-24,W,2);
// gates
if(room>0){rounded(0,255,30,90,8,'#827a5e');ctx.fillStyle='#d3bd80';ctx.fillRect(20,260,8,80)}if(room<3){rounded(W-30,255,30,90,8,(room===0&&!flags.chest||room===1&&!flags.boss1||room===2&&!(flags.seal1&&flags.seal2&&flags.seal3))?'#5d5445':'#9b8756');ctx.fillStyle='#e2c976';ctx.fillRect(W-27,261,6,78)}
// room scenery
if(room===0){for(let i=0;i<12;i++){let x=60+(i*181)%835,y=90+(i*137)%420;if(Math.hypot(x-player.x,y-player.y)>55){ctx.fillStyle='#293d32';ctx.beginPath();ctx.arc(x,y,19+(i%3)*4,0,7);ctx.fill();ctx.fillStyle='#5e7650';ctx.beginPath();ctx.arc(x-4,y-5,13+(i%3)*4,0,7);ctx.fill();ctx.fillStyle='#756344';ctx.fillRect(x-3,y+12,6,18)}}}
if(room===1){ctx.strokeStyle='#76745f';ctx.lineWidth=7;for(let x=90;x<900;x+=110){ctx.beginPath();ctx.moveTo(x,65);ctx.lineTo(x,535);ctx.stroke()}for(let y=110;y<540;y+=105){ctx.beginPath();ctx.moveTo(55,y);ctx.lineTo(900,y);ctx.stroke()}ctx.fillStyle='#635c49';ctx.fillRect(78,75,804,460)}
if(room===2){for(let i=0;i<9;i++){let x=120+i*92;ctx.fillStyle='#77543d';ctx.fillRect(x,75,26,450);ctx.fillStyle='#a3663b';ctx.fillRect(x+4,75,5,450)}for(let i=0;i<7;i++){ctx.fillStyle='#e48643';ctx.globalAlpha=.18+Math.sin(t*3+i)*.08;ctx.beginPath();ctx.ellipse(95+i*130,545,45,10,0,0,Math.PI*2);ctx.fill()}ctx.globalAlpha=1}
if(room===3){ctx.strokeStyle='#6c6655';ctx.lineWidth=3;for(let i=0;i<8;i++){ctx.beginPath();ctx.moveTo(150+i*90,50);ctx.lineTo(480,120+i*48);ctx.lineTo(810-i*90,50);ctx.stroke()}ctx.fillStyle='#4e5248';ctx.beginPath();ctx.ellipse(690,300,84,105,0,0,Math.PI*2);ctx.fill();ctx.strokeStyle='#b3824f';ctx.lineWidth=4;ctx.stroke()}
// save stones and runes
for(let s of switches){let glow=.5+Math.sin(t*3)*.15;ctx.fillStyle=s.type==='save'?'#789c87':flags[s.type]?'#78c6b0':'#8f8154';ctx.beginPath();ctx.arc(s.x,s.y,15+glow*3,0,7);ctx.fill();ctx.fillStyle='#e5d69e';ctx.beginPath();ctx.arc(s.x,s.y,6,0,7);ctx.fill()}
// pickups
for(let p of pickups){let bob=Math.sin(t*3+p.x)*4;ctx.save();ctx.translate(p.x,p.y+bob);ctx.shadowColor='#e7c66a';ctx.shadowBlur=16;ctx.fillStyle=p.type==='heart'?'#e87970':p.type==='chest'?'#bb8e4e':p.type==='cache'?'#81b3a4':p.type==='boots'?'#e2a05c':'#d8bc69';ctx.beginPath();ctx.arc(0,0,p.type==='chest'?13:10,0,7);ctx.fill();ctx.shadowBlur=0;ctx.fillStyle='#fff1c4';ctx.font='bold 12px sans-serif';ctx.textAlign='center';ctx.fillText(({heart:'♥',chest:'▣',cache:'➶',boots:'⌁',mint:'✿',charm:'◈',tonic:'✚',upgrade1:'✦',upgrade2:'ϟ'})[p.type]||'✦',0,4);ctx.restore();if(Math.hypot(player.x-p.x,player.y-p.y)<55){ctx.font='10px sans-serif';ctx.textAlign='center';ctx.fillStyle='#fff3d0';ctx.fillText('E · '+p.label,p.x,p.y-21)}}
// enemies
for(let e of enemies){if(e.dead)continue;let bob=Math.sin(t*5+e.phase)*3;ctx.save();ctx.translate(e.x,e.y+bob);if(e.hit>0)ctx.globalAlpha=.45;let color=({mote:'#91ba81',keeper:'#c77d58',cinder:'#e18648',warden:'#6c8d89'})[e.type];ctx.fillStyle='#1f2927';ctx.beginPath();ctx.ellipse(1,11,e.r*1.1,e.r*.48,0,0,7);ctx.fill();ctx.fillStyle=color;ctx.beginPath();ctx.arc(0,0,e.r,0,7);ctx.fill();ctx.fillStyle='#f6e7bc';ctx.beginPath();ctx.arc(-4,-2,2,0,7);ctx.arc(5,-2,2,0,7);ctx.fill();if(e.type==='warden'){ctx.strokeStyle='#e3bd69';ctx.lineWidth=3;ctx.beginPath();ctx.arc(0,0,e.r+5,0,Math.PI*2);ctx.stroke()}ctx.restore();if(e.max>2){ctx.fillStyle='#222c29';ctx.fillRect(e.x-20,e.y-e.r-14,40,4);ctx.fillStyle='#d77b62';ctx.fillRect(e.x-20,e.y-e.r-14,40*Math.max(0,e.hp/e.max),4)}}
// projectiles
for(let p of projectiles){ctx.fillStyle='#dce797';ctx.beginPath();ctx.arc(p.x,p.y,5,0,7);ctx.fill();ctx.strokeStyle='#e9da9b';ctx.lineWidth=2;ctx.beginPath();ctx.moveTo(p.x-p.vx*.035,p.y-p.vy*.035);ctx.lineTo(p.x,p.y);ctx.stroke()}
// player shadow and avatar
ctx.fillStyle='#18231f88';ctx.beginPath();ctx.ellipse(player.x,player.y+13,13,6,0,0,7);ctx.fill();let blink=player.invuln>0&&Math.floor(t*14)%2===0;ctx.save();ctx.translate(player.x,player.y+Math.sin(player.walk*15)*1.5);if(blink)ctx.globalAlpha=.42;ctx.fillStyle='#d7c59c';ctx.beginPath();ctx.ellipse(0,2,11,13,0,0,7);ctx.fill();ctx.fillStyle='#425c4c';ctx.beginPath();ctx.ellipse(0,-2,12,12,0,0,7);ctx.fill();ctx.fillStyle='#d6a980';ctx.beginPath();ctx.arc(0,-11,7,0,7);ctx.fill();ctx.fillStyle='#38423a';ctx.beginPath();ctx.arc(0,-14,7,Math.PI,Math.PI*2);ctx.fill();ctx.strokeStyle=player.weapon===0?'#ded7bd':player.weapon===1?'#f3a35c':'#c6e1d2';ctx.lineWidth=player.weapon===2?4:3;ctx.lineCap='round';let reach=player.attack>0?(player.weapon===2?43:35):20;ctx.beginPath();ctx.moveTo(Math.cos(player.dir)*8,Math.sin(player.dir)*8);ctx.lineTo(Math.cos(player.dir)*reach,Math.sin(player.dir)*reach);ctx.stroke();if(player.weapon===1&&player.attack>0){ctx.strokeStyle='#ff9a4d88';ctx.lineWidth=12;ctx.beginPath();ctx.arc(0,0,38,player.dir-.6,player.dir+.6);ctx.stroke()}if(player.weapon===2&&player.attack>0){ctx.strokeStyle='#b8e6d288';ctx.lineWidth=9;ctx.beginPath();ctx.arc(0,0,48,player.dir-.45,player.dir+.45);ctx.stroke()}ctx.restore();
// area caption
ctx.fillStyle='#f2ebd1';ctx.font='600 11px Manrope';ctx.textAlign='left';ctx.fillText(regions[room].name.toUpperCase(),22,31);ctx.textAlign='right';ctx.font='10px DM Mono';ctx.fillStyle='#d4d6bb';ctx.fillText('✦ '+player.coins, W-22,31);
// interaction hint
let nearSwitch=switches.some(s=>near(s,55)),nearPickup=pickups.some(p=>near(p,50)),atDoor=player.x>W-62||player.x<62,shopnear=room===1&&Math.hypot(player.x-135,player.y-300)<90;if(nearSwitch||nearPickup||atDoor||shopnear){ctx.fillStyle='#18231ddc';ctx.beginPath();ctx.roundRect(W/2-118,H-54,236,30,8);ctx.fill();ctx.fillStyle='#f3e7c4';ctx.font='bold 11px Manrope';ctx.textAlign='center';ctx.fillText('E  ·  '+(shopnear?'OPEN WAYFARER’S STALL':atDoor?'TAKE THE TRAIL':nearSwitch?'INTERACT':'COLLECT'),W/2,H-34)}
for(let p of particles){ctx.globalAlpha=Math.max(0,p.life/p.max);ctx.fillStyle=p.color;ctx.beginPath();ctx.arc(p.x,p.y,p.size,0,7);ctx.fill()}ctx.globalAlpha=1;
}
function frame(ts){let dt=Math.min(.04,(ts-last)/1000||0);last=ts;update(dt);draw();requestAnimationFrame(frame)}
window.addEventListener('keydown',e=>{let k=e.code;if(['ArrowUp','ArrowDown','ArrowLeft','ArrowRight','Space'].includes(k))e.preventDefault();keys[k]=true;if(k==='KeyE'){interact()}if(k==='KeyI'&&!paused)openInventory();if(k==='Escape'&&paused)closePanel();if(k==='KeyR'&&gameDone){enterRoom(0);gameDone=false}});window.addEventListener('keyup',e=>keys[e.code]=false);
document.getElementById('overlay').addEventListener('click',e=>{if(e.target.id==='overlay')closePanel()});
spawnRoom();updateUI();requestAnimationFrame(frame);
})();
</script></body></html>
<!-- agent-meta {"model":"openai/gpt-6-luna","provider":"openrouter","persona":"full","sessionId":"d8603459-cd7c-4fa8-bb30-0a9c67d0a846","tokensIn":67755,"tokensOut":13504,"tokensTotal":81259,"cost":0.009236745000000001,"turns":5,"toolCalls":4,"failedToolCalls":0,"timestamp":"2026-09-23T02:56:34.675Z"} -->