RPG
Claude Opus 4.8 · 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 28,085 bytes · SHA-256 48c2888c9594
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Emberlight — Action Adventure RPG</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
html,body { height:100%; background:#0b0e14; overflow:hidden; font-family:'Courier New',monospace; color:#e8e8e8; }
#game { display:block; margin:0 auto; background:#1a1e28; image-rendering:pixelated; touch-action:none; }
#wrap { display:flex; flex-direction:column; align-items:center; height:100%; justify-content:center; }
#hud { position:fixed; top:0; left:0; width:100%; pointer-events:none; padding:10px; font-size:13px; }
#hud .row { display:flex; justify-content:space-between; align-items:flex-start; }
#hearts { font-size:22px; letter-spacing:2px; }
#msg { position:fixed; bottom:16px; left:50%; transform:translateX(-50%); background:rgba(0,0,0,.75); padding:8px 16px; border:1px solid #555; border-radius:6px; font-size:13px; opacity:0; transition:opacity .3s; text-align:center; }
#inv { position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(5,8,14,.94); display:none; flex-direction:column; align-items:center; justify-content:center; z-index:10; }
#inv h2 { color:#f5c26b; margin-bottom:14px; letter-spacing:3px; }
.grid { display:grid; grid-template-columns:repeat(3,150px); gap:10px; }
.slot { background:#1c2230; border:1px solid #3a4460; padding:10px; border-radius:6px; font-size:12px; min-height:70px; }
.slot b { color:#f5c26b; }
#shop { position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(5,8,14,.94); display:none; flex-direction:column; align-items:center; justify-content:center; z-index:11; }
#shop h2 { color:#7bd88f; margin-bottom:14px; letter-spacing:3px; }
.buy { background:#1c2230; border:1px solid #3a4460; padding:10px 14px; margin:5px; border-radius:6px; cursor:pointer; width:340px; display:flex; justify-content:space-between; font-size:13px; pointer-events:auto; }
.buy:hover { border-color:#7bd88f; background:#232b3d; }
.buy .cost { color:#f5c26b; }
#inv .hint,#shop .hint { margin-top:16px; color:#888; font-size:11px; }
#title { position:fixed; inset:0; background:#0b0e14; z-index:20; display:flex; flex-direction:column; align-items:center; justify-content:center; text-align:center; }
#title h1 { font-size:48px; color:#f5c26b; letter-spacing:6px; text-shadow:0 0 20px #8b5a2b; }
#title p { margin-top:16px; color:#aaa; max-width:520px; line-height:1.6; font-size:14px; }
#title button { margin-top:26px; padding:12px 30px; background:#8b5a2b; color:#fff; border:none; border-radius:6px; font-size:16px; cursor:pointer; letter-spacing:2px; }
#title button:hover { background:#a86a33; }
.ctrl { color:#6b7690; font-size:11px; margin-top:8px; }
#prog { color:#8fa3c8; }
</style>
</head>
<body>
<div id="wrap">
<canvas id="game" width="640" height="480"></canvas>
</div>
<div id="hud">
<div class="row">
<div>
<div id="hearts"></div>
<div id="prog"></div>
</div>
<div style="text-align:right">
<div id="money" style="color:#f5c26b">◈ 0</div>
<div id="weap"></div>
</div>
</div>
</div>
<div id="msg"></div>
<div id="inv">
<h2>INVENTORY</h2>
<div class="grid" id="invGrid"></div>
<div class="hint">Press [I] to close · Press number keys 1-9 to USE consumables</div>
</div>
<div id="shop">
<h2>MERCHANT</h2>
<div id="shopList"></div>
<div class="hint">Click to buy · Press [E] to leave</div>
</div>
<div id="title">
<h1>EMBERLIGHT</h1>
<p>You awaken in the ruined Vale of Ash. Explore four regions, defeat the shades, gather relics, upgrade your blade, and reach the Sunspire before the dark consumes it all.</p>
<div class="ctrl">WASD / Arrows — Move · J / Space — Attack · K — Ranged · E — Interact · I — Inventory</div>
<button id="startBtn">BEGIN ADVENTURE</button>
</div>
<script>
// ============ AUDIO ============
const AC = window.AudioContext || window.webkitAudioContext;
let actx = null;
function initAudio(){ if(!actx) actx = new AC(); }
function tone(freq, dur, type='square', vol=0.15, slide=0){
if(!actx) return;
const o=actx.createOscillator(), g=actx.createGain();
o.type=type; o.frequency.value=freq;
if(slide) o.frequency.exponentialRampToValueAtTime(Math.max(20,freq+slide), actx.currentTime+dur);
g.gain.setValueAtTime(vol, actx.currentTime);
g.gain.exponentialRampToValueAtTime(0.001, actx.currentTime+dur);
o.connect(g); g.connect(actx.destination);
o.start(); o.stop(actx.currentTime+dur);
}
function noise(dur, vol=0.2){
if(!actx) return;
const b=actx.createBuffer(1, actx.sampleRate*dur, actx.sampleRate);
const d=b.getChannelData(0);
for(let i=0;i<d.length;i++) d[i]=(Math.random()*2-1)*(1-i/d.length);
const s=actx.createBufferSource(), g=actx.createGain();
s.buffer=b; g.gain.value=vol; s.connect(g); g.connect(actx.destination); s.start();
}
const SFX={
step(){ tone(90,0.05,'sine',0.05); },
attack(){ tone(320,0.08,'square',0.12,-180); },
attack2(){ tone(500,0.06,'sawtooth',0.12,-300); noise(0.06,0.08); },
ranged(){ tone(700,0.12,'triangle',0.12,-400); },
hit(){ noise(0.08,0.15); tone(160,0.08,'square',0.1,-80); },
hurt(){ tone(200,0.2,'sawtooth',0.2,-150); noise(0.1,0.2); },
pickup(){ tone(660,0.08,'sine',0.12); setTimeout(()=>tone(990,0.1,'sine',0.12),70); },
shop(){ tone(440,0.06,'triangle',0.1); setTimeout(()=>tone(550,0.06,'triangle',0.1),60); setTimeout(()=>tone(770,0.1,'triangle',0.1),120); },
save(){ tone(523,0.1,'sine',0.12); setTimeout(()=>tone(784,0.15,'sine',0.12),100); },
upgrade(){ [523,659,784,1047].forEach((f,i)=>setTimeout(()=>tone(f,0.15,'triangle',0.14),i*90)); },
die(){ tone(300,0.3,'sawtooth',0.15,-250); }
};
// ============ GAME STATE ============
const cv=document.getElementById('game'), ctx=cv.getContext('2d');
const W=640,H=480;
ctx.imageSmoothingEnabled=false;
const keys={};
window.addEventListener('keydown',e=>{ keys[e.key.toLowerCase()]=true; handleKey(e); });
window.addEventListener('keyup',e=>{ keys[e.key.toLowerCase()]=false; });
let game=null;
// Weapon definitions
const WEAPONS=[
{name:'Rusted Blade', dmg:2, speed:22, range:34, arc:1.1, color:'#9a8f7a'},
{name:'Ember Sword', dmg:4, speed:16, range:40, arc:1.3, color:'#ff8a3c'}, // upgrade 1
{name:'Storm Glaive', dmg:6, speed:10, range:52, arc:1.6, color:'#6bd0ff'} // upgrade 2
];
// Item catalog
const SHOP_ITEMS=[
{id:'potion', name:'Health Potion', desc:'Restore 2 HP', cost:15, type:'consumable'},
{id:'apple', name:'Golden Apple', desc:'Restore 1 HP', cost:8, type:'consumable'},
{id:'boots', name:'Swift Boots', desc:'+30% move speed (worn)', cost:30, type:'accessory'},
{id:'amulet', name:'Ember Amulet', desc:'+1 weapon damage (worn)', cost:40, type:'accessory'},
{id:'quiver', name:'Deep Quiver', desc:'+3 max arrows (worn)', cost:25, type:'accessory'}
];
function newGame(){
game={
px:80, py:240, pdir:1, vx:0, vy:0,
maxHP:6, hp:6, // 6 half-points = 3 hearts. max later 10 = 5 hearts
hpCap:6,
weaponTier:0,
money:0,
hasRanged:false, arrows:0, maxArrows:6,
inv:{}, // id -> count
worn:{boots:false, amulet:false, quiver:false},
region:0, regionsCleared:[false,false,false,false],
attackTimer:0, attackCd:0, hurtTimer:0, walkAnim:0,
stepTimer:0, invincible:0,
projectiles:[], particles:[], enemies:[], items:[],
savePoint:null, saved:false,
won:false, dead:false,
progress:0 // 0-100
};
buildRegion(0);
}
// Regions layout: 0 forest, 1 caverns, 2 ruins, 3 sunspire
const REGION_NAMES=['Vale of Ash','Echoing Caverns','Broken Ruins','The Sunspire'];
function buildRegion(r){
game.region=r; game.enemies=[]; game.items=[]; game.projectiles=[]; game.particles=[];
game.px=70; game.py=240;
const cleared = game.regionsCleared[r];
const spawnEnemy=(x,y,hp,type)=>game.enemies.push({x,y,hp,maxhp:hp,type,vx:0,vy:0,hurt:0,dead:false,anim:Math.random()*6.28});
const spawnItem=(x,y,kind,data)=>game.items.push({x,y,kind,data,bob:Math.random()*6.28,taken:false});
if(r===0){
if(!cleared){
spawnEnemy(280,150,4,'shade'); spawnEnemy(400,320,4,'shade'); spawnEnemy(500,180,6,'shade');
}
spawnItem(200,340,'coin',{v:10});
spawnItem(340,120,'item',{id:'apple'});
spawnItem(540,360,'save',{});
// gate to next region (exit) top-right
} else if(r===1){
if(!cleared){
spawnEnemy(250,300,6,'brute'); spawnEnemy(420,160,6,'shade'); spawnEnemy(520,300,8,'brute');
}
spawnItem(150,150,'coin',{v:15});
spawnItem(360,360,'weapon',{tier:1}); // Ember Sword upgrade
spawnItem(300,120,'save',{});
if(!game.inv['bomb']) spawnItem(500,120,'item',{id:'bomb'});
} else if(r===2){
// Mid-game: ranged weapon discovery
if(!cleared){
spawnEnemy(220,140,8,'wraith'); spawnEnemy(400,340,8,'wraith'); spawnEnemy(520,200,10,'brute');
}
if(!game.hasRanged) spawnItem(320,240,'ranged',{});
spawnItem(180,360,'coin',{v:20});
spawnItem(480,120,'item',{id:'scroll'});
spawnItem(560,340,'save',{});
spawnItem(120,240,'heart',{}); // heart container +2 max
} else if(r===3){
if(!cleared){
spawnEnemy(320,180,14,'boss');
}
spawnItem(200,360,'coin',{v:25});
spawnItem(120,120,'save',{});
spawnItem(500,360,'weapon',{tier:2}); // Storm Glaive
}
updateHUD();
}
// Merchant appears in region 0 corner
function inShopZone(){ return game.region===0 && game.px>560 && game.py<120; }
// ============ INPUT HANDLERS ============
let invOpen=false, shopOpen=false;
function handleKey(e){
const k=e.key.toLowerCase();
if(!game) return;
if(k==='i'){ toggleInv(); }
if(k==='e'){
if(shopOpen){ closeShop(); return; }
if(inShopZone()){ openShop(); return; }
interact();
}
// use consumables by number
if(/^[1-9]$/.test(k)){ useConsumable(parseInt(k)-1); }
}
function toggleInv(){
invOpen=!invOpen;
document.getElementById('inv').style.display = invOpen?'flex':'none';
if(invOpen) renderInv();
}
function useConsumable(idx){
const cons=Object.keys(game.inv).filter(id=>{
const s=SHOP_ITEMS.find(x=>x.id===id); return (s&&s.type==='consumable')||id==='bomb'||id==='scroll';
});
const id=cons[idx];
if(!id||game.inv[id]<=0) return;
if(id==='potion'){ heal(2); }
else if(id==='apple'){ heal(1); }
else if(id==='scroll'){ // temporary damage buff visual
heal(2); flash('The scroll surges with warmth (+2 HP)!');
}
else if(id==='bomb'){ // damage nearby enemies
game.enemies.forEach(en=>{ if(!en.dead){ const d=Math.hypot(en.x-game.px,en.y-game.py); if(d<90){ hurtEnemy(en,6,en.x,en.y);} }});
burst(game.px,game.py,'#ff6a2b',26); SFX.hit(); flash('BOOM! The bomb detonates!');
}
game.inv[id]--; if(game.inv[id]<=0) delete game.inv[id];
SFX.pickup();
renderInv(); updateHUD();
}
function heal(pts){ game.hp=Math.min(game.hpCap, game.hp+pts*2); burst(game.px,game.py,'#7bd88f',10); }
function interact(){
// pick nearest save/interactable within range handled by items; also save point
for(const it of game.items){
if(it.taken) continue;
const d=Math.hypot(it.x-game.px, it.y-game.py);
if(d<40 && it.kind==='save'){
game.savePoint={region:game.region, x:it.x, y:it.y};
game.saved=true; SFX.save();
burst(it.x,it.y,'#6bd0ff',20);
flash('Progress saved at this shrine.');
return;
}
}
}
// ============ SHOP ============
function openShop(){
shopOpen=true; document.getElementById('shop').style.display='flex'; SFX.shop();
renderShop();
}
function closeShop(){ shopOpen=false; document.getElementById('shop').style.display='none'; }
function renderShop(){
const list=document.getElementById('shopList'); list.innerHTML='';
SHOP_ITEMS.forEach(it=>{
const owned = it.type==='accessory' && game.worn[it.id];
const div=document.createElement('div'); div.className='buy';
div.innerHTML=`<span><b>${it.name}</b> — ${it.desc}${owned?' <i style="color:#7bd88f">(owned)</i>':''}</span><span class="cost">◈ ${it.cost}</span>`;
div.onclick=()=>{
if(owned) return;
if(game.money>=it.cost){
game.money-=it.cost; SFX.shop();
if(it.type==='accessory'){ game.worn[it.id]=true; if(it.id==='quiver'){ game.maxArrows+=3; } }
else { game.inv[it.id]=(game.inv[it.id]||0)+1; }
flash('Purchased '+it.name+'!');
renderShop(); updateHUD();
} else { flash('Not enough coin.'); SFX.hurt(); }
};
list.appendChild(div);
});
}
// ============ INVENTORY RENDER ============
function renderInv(){
const g=document.getElementById('invGrid'); g.innerHTML='';
const entries=[];
entries.push({t:`<b>Weapon</b><br>${WEAPONS[game.weaponTier].name}<br>DMG ${WEAPONS[game.weaponTier].dmg+(game.worn.amulet?1:0)}`});
if(game.hasRanged) entries.push({t:`<b>Ranged Bow</b><br>Arrows: ${game.arrows}/${game.maxArrows}`});
Object.keys(game.worn).forEach(w=>{ if(game.worn[w]){ const s=SHOP_ITEMS.find(x=>x.id===w); entries.push({t:`<b>${s.name}</b><br>${s.desc}`}); }});
let num=1;
Object.keys(game.inv).forEach(id=>{
const s=SHOP_ITEMS.find(x=>x.id===id);
const nm = s?s.name : (id==='bomb'?'Fire Bomb':id==='scroll'?'Vital Scroll':id);
const desc = s?s.desc : (id==='bomb'?'AoE damage nearby':'Restores 2 HP');
entries.push({t:`<b>[${num}] ${nm}</b> x${game.inv[id]}<br>${desc}`});
num++;
});
if(entries.length===0) entries.push({t:'Empty — go explore!'});
entries.forEach(e=>{ const d=document.createElement('div'); d.className='slot'; d.innerHTML=e.t; g.appendChild(d); });
}
// ============ HUD ============
function updateHUD(){
// hearts: 2 pts each
const hearts=Math.ceil(game.hpCap/2);
let html='';
for(let i=0;i<hearts;i++){
const v=game.hp - i*2;
if(v>=2) html+='<span style="color:#ff4d5e">♥</span>';
else if(v===1) html+='<span style="color:#ff9aa4">♥</span>';
else html+='<span style="color:#3a2530">♥</span>';
}
document.getElementById('hearts').innerHTML=html;
document.getElementById('money').textContent='◈ '+game.money;
let w=WEAPONS[game.weaponTier].name+' (DMG '+(WEAPONS[game.weaponTier].dmg+(game.worn.amulet?1:0))+')';
if(game.hasRanged) w+=' · ⤳'+game.arrows;
document.getElementById('weap').textContent=w;
const pct = Math.round((game.region + (game.regionsCleared[game.region]?1:0))/4*100);
document.getElementById('prog').textContent=REGION_NAMES[game.region]+' — '+pct+'% explored';
game.progress=pct;
}
let msgTimer=0;
function flash(t){ const m=document.getElementById('msg'); m.textContent=t; m.style.opacity=1; msgTimer=180; }
// ============ PARTICLES ============
function burst(x,y,color,n){
for(let i=0;i<n;i++){
const a=Math.random()*6.28, s=1+Math.random()*3;
game.particles.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,life:20+Math.random()*15,color,size:2+Math.random()*2});
}
}
// ============ COMBAT ============
function hurtEnemy(en,dmg,fx,fy){
en.hp-=dmg; en.hurt=8;
burst(fx,fy,'#ffcf5c',6); SFX.hit();
const kb=6; const ang=Math.atan2(en.y-game.py,en.x-game.px);
en.vx+=Math.cos(ang)*kb; en.vy+=Math.sin(ang)*kb;
if(en.hp<=0 && !en.dead){
en.dead=true; burst(en.x,en.y,'#a06bff',16);
game.money += en.type==='boss'?60:(en.type==='brute'||en.type==='wraith'?12:6);
checkRegionClear();
}
}
function checkRegionClear(){
const alive=game.enemies.some(e=>!e.dead);
if(!alive && !game.regionsCleared[game.region]){
game.regionsCleared[game.region]=true;
flash('Region cleared! The path onward opens.');
game.money+=10;
}
updateHUD();
}
function playerHurt(dmg){
if(game.invincible>0) return;
game.hp-=dmg; game.hurtTimer=20; game.invincible=60;
SFX.hurt(); burst(game.px,game.py,'#ff4d5e',12);
updateHUD();
if(game.hp<=0){ playerDie(); }
}
function playerDie(){
game.dead=true; SFX.die();
flash('You have fallen... respawning at last shrine.');
setTimeout(()=>{
if(game.savePoint){
buildRegion(game.savePoint.region);
game.px=game.savePoint.x; game.py=game.savePoint.y;
} else { buildRegion(0); }
game.hp=game.hpCap; game.dead=false; game.invincible=90;
updateHUD();
},1500);
}
// ============ UPDATE ============
function update(){
if(!game || invOpen || shopOpen || game.won || game.dead) return;
const wp=WEAPONS[game.weaponTier];
let spd = game.worn.boots?2.9:2.2;
let mx=0,my=0;
if(keys['a']||keys['arrowleft']){ mx-=1; game.pdir=-1; }
if(keys['d']||keys['arrowright']){ mx+=1; game.pdir=1; }
if(keys['w']||keys['arrowup']) my-=1;
if(keys['s']||keys['arrowdown']) my+=1;
const moving = mx||my;
if(moving){ const l=Math.hypot(mx,my); mx/=l; my/=l; game.walkAnim+=0.3;
game.stepTimer--; if(game.stepTimer<=0){ SFX.step(); game.stepTimer=14; }
} else game.walkAnim=0;
game.px=Math.max(20,Math.min(W-20,game.px+mx*spd));
game.py=Math.max(30,Math.min(H-20,game.py+my*spd));
// attack
if(game.attackCd>0) game.attackCd--;
if((keys['j']||keys[' ']) && game.attackCd<=0){
game.attackTimer=10; game.attackCd=wp.speed;
game.weaponTier===0?SFX.attack():SFX.attack2();
// hit enemies in arc
const dmg=wp.dmg+(game.worn.amulet?1:0);
game.enemies.forEach(en=>{ if(en.dead) return;
const dx=en.x-game.px, dy=en.y-game.py, d=Math.hypot(dx,dy);
if(d<wp.range && (game.pdir>0?dx>-10:dx<10)){ hurtEnemy(en,dmg,en.x,en.y); }
});
burst(game.px+game.pdir*wp.range*0.6, game.py, wp.color, 4);
}
if(game.attackTimer>0) game.attackTimer--;
// ranged
if(keys['k'] && game.hasRanged && game.arrows>0 && game.attackCd<=0){
game.attackCd=14; game.arrows--; SFX.ranged();
game.projectiles.push({x:game.px+game.pdir*14,y:game.py,vx:game.pdir*7,vy:0,life:60});
updateHUD();
}
keys['k']=false; keys['j']=false;
// projectiles
game.projectiles.forEach(p=>{ p.x+=p.vx; p.y+=p.vy; p.life--;
game.enemies.forEach(en=>{ if(!en.dead && Math.hypot(en.x-p.x,en.y-p.y)<18){ hurtEnemy(en,5,en.x,en.y); p.life=0; }});
});
game.projectiles=game.projectiles.filter(p=>p.life>0 && p.x>0 && p.x<W);
// enemies AI
game.enemies.forEach(en=>{ if(en.dead) return;
en.anim+=0.1;
if(en.hurt>0) en.hurt--;
const dx=game.px-en.x, dy=game.py-en.y, d=Math.hypot(dx,dy);
let es = en.type==='boss'?1.5:(en.type==='brute'?1.0:1.4);
if(d>26){ en.vx+=dx/d*0.15*es; en.vy+=dy/d*0.15*es; }
en.vx*=0.85; en.vy*=0.85;
en.x+=en.vx; en.y+=en.vy;
en.x=Math.max(15,Math.min(W-15,en.x)); en.y=Math.max(25,Math.min(H-15,en.y));
if(d<26){ playerHurt(en.type==='boss'?2:1); }
});
if(game.invincible>0) game.invincible--;
if(game.hurtTimer>0) game.hurtTimer--;
// items pickup
game.items.forEach(it=>{ if(it.taken) return; it.bob+=0.08;
const d=Math.hypot(it.x-game.px, it.y-game.py);
if(d<26 && it.kind!=='save'){
if(it.kind==='coin'){ game.money+=it.data.v; it.taken=true; SFX.pickup(); burst(it.x,it.y,'#f5c26b',8); flash('+◈'+it.data.v); }
else if(it.kind==='item'){ game.inv[it.data.id]=(game.inv[it.data.id]||0)+1; it.taken=true; SFX.pickup(); burst(it.x,it.y,'#7bd88f',8);
const nm=(SHOP_ITEMS.find(x=>x.id===it.data.id)||{}).name || (it.data.id==='bomb'?'Fire Bomb':it.data.id==='scroll'?'Vital Scroll':it.data.id);
flash('Found '+nm+'!'); }
else if(it.kind==='weapon'){ if(it.data.tier>game.weaponTier){ game.weaponTier=it.data.tier; SFX.upgrade(); burst(it.x,it.y,'#ff8a3c',22); flash('WEAPON UPGRADE: '+WEAPONS[it.data.tier].name+'!'); } it.taken=true; }
else if(it.kind==='ranged'){ game.hasRanged=true; game.arrows=game.maxArrows; it.taken=true; SFX.upgrade(); burst(it.x,it.y,'#6bd0ff',22); flash('Discovered the ANCIENT BOW! Press K to fire.'); }
else if(it.kind==='heart'){ game.hpCap=Math.min(10,game.hpCap+2); game.hp=game.hpCap; it.taken=true; SFX.upgrade(); burst(it.x,it.y,'#ff4d5e',20); flash('Heart Container! Max HP increased.'); }
updateHUD();
}
});
// refill arrows near save
// region transitions: right edge if cleared
if(game.regionsCleared[game.region] && game.px>W-24){
if(game.region<3){ buildRegion(game.region+1); game.arrows=Math.min(game.maxArrows,game.arrows+2); flash('Entering '+REGION_NAMES[game.region]+'...'); }
else { winGame(); }
}
if(game.px<24 && game.region>0){ // go back
// allow backtrack
if(keys['s']===false){} // no-op
}
// particles
game.particles.forEach(p=>{ p.x+=p.vx; p.y+=p.vy; p.vx*=0.94; p.vy*=0.94; p.life--; });
game.particles=game.particles.filter(p=>p.life>0);
if(msgTimer>0){ msgTimer--; if(msgTimer===0) document.getElementById('msg').style.opacity=0; }
}
function winGame(){
game.won=true; SFX.upgrade();
flash('The Sunspire is relit. You are the Emberlight. VICTORY!');
}
// ============ RENDER ============
function draw(){
// background per region
const bgs=['#1c2a1e','#16171f','#2a2320','#2a2438'];
ctx.fillStyle=bgs[game?game.region:0]; ctx.fillRect(0,0,W,H);
if(!game) return;
// ground grid decor
ctx.strokeStyle='rgba(255,255,255,0.03)';
for(let x=0;x<W;x+=40){ ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,H); ctx.stroke(); }
for(let y=0;y<H;y+=40){ ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(W,y); ctx.stroke(); }
// exit portal if cleared
if(game.regionsCleared[game.region]){
ctx.fillStyle='rgba(107,208,255,'+(0.3+0.2*Math.sin(Date.now()/200))+')';
ctx.fillRect(W-16,H/2-40,16,80);
ctx.fillStyle='#6bd0ff'; ctx.font='10px monospace'; ctx.fillText(game.region<3?'EXIT →':'SUNSPIRE →', W-88, H/2-46);
}
// shop marker in region 0
if(game.region===0){
ctx.fillStyle='#8b5a2b'; ctx.fillRect(578,60,44,44);
ctx.fillStyle='#f5c26b'; ctx.font='16px monospace'; ctx.fillText('◈',593,90);
ctx.font='9px monospace'; ctx.fillStyle='#f5c26b'; ctx.fillText(inShopZone()?'[E] SHOP':'SHOP',580,54);
}
// items
game.items.forEach(it=>{ if(it.taken) return;
const by=Math.sin(it.bob)*3;
if(it.kind==='coin'){ ctx.fillStyle='#f5c26b'; ctx.beginPath(); ctx.arc(it.x,it.y+by,7,0,6.28); ctx.fill(); ctx.fillStyle='#a8792b'; ctx.font='9px monospace'; ctx.fillText('◈',it.x-4,it.y+by+3); }
else if(it.kind==='save'){ ctx.fillStyle='#6bd0ff'; ctx.fillRect(it.x-8,it.y-14+by,16,24); ctx.fillStyle='#bfe9ff'; ctx.fillRect(it.x-3,it.y-10+by,6,10);
ctx.font='9px monospace'; ctx.fillStyle='#6bd0ff'; if(Math.hypot(it.x-game.px,it.y-game.py)<40) ctx.fillText('[E] SAVE',it.x-24,it.y-20+by); }
else if(it.kind==='weapon'){ ctx.fillStyle=WEAPONS[it.data.tier].color; ctx.save(); ctx.translate(it.x,it.y+by); ctx.rotate(Math.PI/4); ctx.fillRect(-2,-12,4,20); ctx.fillStyle='#fff'; ctx.fillRect(-5,6,10,3); ctx.restore(); glow(it.x,it.y,WEAPONS[it.data.tier].color); }
else if(it.kind==='ranged'){ ctx.strokeStyle='#6bd0ff'; ctx.lineWidth=3; ctx.beginPath(); ctx.arc(it.x,it.y+by,10,-1,1); ctx.stroke(); ctx.lineWidth=1; glow(it.x,it.y,'#6bd0ff'); }
else if(it.kind==='heart'){ drawHeart(it.x,it.y+by,10,'#ff4d5e'); glow(it.x,it.y,'#ff4d5e'); }
else if(it.kind==='item'){ ctx.fillStyle='#7bd88f'; ctx.fillRect(it.x-7,it.y-7+by,14,14); ctx.strokeStyle='#3f6b48'; ctx.strokeRect(it.x-7,it.y-7+by,14,14); }
});
// enemies
game.enemies.forEach(en=>{ if(en.dead) return;
const cols={shade:'#7a5fb5',brute:'#b5654f',wraith:'#4fb59a',boss:'#c23a5e'};
const c = en.hurt>0?'#ffffff':cols[en.type];
const wob=Math.sin(en.anim)*2;
const sz = en.type==='boss'?26:(en.type==='brute'?16:13);
ctx.fillStyle=c;
ctx.beginPath(); ctx.moveTo(en.x,en.y-sz+wob);
ctx.lineTo(en.x-sz,en.y+sz); ctx.lineTo(en.x+sz,en.y+sz); ctx.closePath(); ctx.fill();
// eyes
ctx.fillStyle='#ffdd00'; ctx.fillRect(en.x-6,en.y-4,3,3); ctx.fillRect(en.x+3,en.y-4,3,3);
// hp bar
ctx.fillStyle='#000'; ctx.fillRect(en.x-sz,en.y-sz-8,sz*2,3);
ctx.fillStyle='#ff4d5e'; ctx.fillRect(en.x-sz,en.y-sz-8,sz*2*(en.hp/en.maxhp),3);
});
// projectiles
ctx.fillStyle='#bfe9ff';
game.projectiles.forEach(p=>{ ctx.fillRect(p.x-6,p.y-1,12,2); ctx.beginPath(); ctx.arc(p.x+(p.vx>0?6:-6),p.y,2,0,6.28); ctx.fill(); });
// player
drawPlayer();
// particles
game.particles.forEach(p=>{ ctx.globalAlpha=Math.min(1,p.life/20); ctx.fillStyle=p.color; ctx.fillRect(p.x-p.size/2,p.y-p.size/2,p.size,p.size); });
ctx.globalAlpha=1;
// win overlay
if(game.won){
ctx.fillStyle='rgba(20,10,30,0.75)'; ctx.fillRect(0,0,W,H);
ctx.fillStyle='#f5c26b'; ctx.font='30px monospace'; ctx.textAlign='center';
ctx.fillText('VICTORY',W/2,H/2-20);
ctx.fillStyle='#fff'; ctx.font='14px monospace';
ctx.fillText('The Sunspire burns anew. Coin: ◈'+game.money,W/2,H/2+16);
ctx.fillText('Reload the page to play again.',W/2,H/2+40);
ctx.textAlign='left';
}
}
function glow(x,y,c){ const g=ctx.createRadialGradient(x,y,2,x,y,18); g.addColorStop(0,c+''); g.addColorStop(1,'transparent'); ctx.globalAlpha=0.3; ctx.fillStyle=g; ctx.beginPath(); ctx.arc(x,y,18,0,6.28); ctx.fill(); ctx.globalAlpha=1; }
function drawHeart(x,y,s,c){ ctx.fillStyle=c; ctx.beginPath();
ctx.moveTo(x,y+s*0.7); ctx.bezierCurveTo(x-s,y-s*0.3,x-s*0.5,y-s,x,y-s*0.3);
ctx.bezierCurveTo(x+s*0.5,y-s,x+s,y-s*0.3,x,y+s*0.7); ctx.fill(); }
function drawPlayer(){
const px=game.px, py=game.py;
const flash = game.invincible>0 && Math.floor(Date.now()/80)%2===0;
if(flash) return; // blink when hurt
const hurt = game.hurtTimer>0;
ctx.save(); ctx.translate(px,py);
const bob = Math.sin(game.walkAnim)*2;
const legSwing = Math.sin(game.walkAnim)*4;
// shadow
ctx.fillStyle='rgba(0,0,0,0.3)'; ctx.beginPath(); ctx.ellipse(0,14,10,4,0,0,6.28); ctx.fill();
// legs (walk anim)
ctx.fillStyle='#3a4a6b';
ctx.fillRect(-6,6+legSwing*0.3, 4, 8-legSwing*0.3);
ctx.fillRect(2,6-legSwing*0.3, 4, 8+legSwing*0.3);
// body — color changes with equipment
ctx.fillStyle = game.worn.amulet?'#8b3a3a':'#4a6b9a';
ctx.fillRect(-8,-8+bob,16,16);
if(hurt){ ctx.fillStyle='rgba(255,80,90,0.6)'; ctx.fillRect(-8,-8+bob,16,16); }
// amulet visual
if(game.worn.amulet){ ctx.fillStyle='#ffcf5c'; ctx.beginPath(); ctx.arc(0,0+bob,3,0,6.28); ctx.fill(); }
// head
ctx.fillStyle='#e8c39a'; ctx.fillRect(-6,-18+bob,12,10);
// boots highlight
if(game.worn.boots){ ctx.fillStyle='#ffcf5c'; ctx.fillRect(-6,12,4,2); ctx.fillRect(2,12,4,2); }
// weapon — changes with tier
const wp=WEAPONS[game.weaponTier];
const swing = game.attackTimer>0 ? (10-game.attackTimer)/10 : 0;
ctx.save();
ctx.translate(game.pdir*10, -2+bob);
ctx.scale(game.pdir,1);
ctx.rotate(-swing*1.4 - 0.3);
ctx.fillStyle=wp.color;
const bladeLen = 14 + game.weaponTier*6;
ctx.fillRect(-1.5,-bladeLen,3,bladeLen); // blade
ctx.fillStyle='#5a4a30'; ctx.fillRect(-3,0,6,4); // hilt
if(game.weaponTier===2){ // glaive glow
ctx.globalAlpha=0.5; ctx.fillStyle='#6bd0ff'; ctx.fillRect(-3,-bladeLen,6,bladeLen); ctx.globalAlpha=1;
}
// slash arc
if(swing>0.1){ ctx.strokeStyle=wp.color; ctx.globalAlpha=0.5; ctx.lineWidth=3; ctx.beginPath(); ctx.arc(0,-4,bladeLen,-1.4,0.4); ctx.stroke(); ctx.globalAlpha=1; ctx.lineWidth=1; }
ctx.restore();
// bow on back if owned
if(game.hasRanged){ ctx.strokeStyle='#6bd0ff'; ctx.lineWidth=2; ctx.beginPath(); ctx.arc(-game.pdir*8,-2+bob,8,-1.2,1.2); ctx.stroke(); ctx.lineWidth=1; }
ctx.restore();
}
// ============ LOOP ============
function loop(){ update(); draw(); requestAnimationFrame(loop); }
document.getElementById('startBtn').onclick=()=>{
initAudio();
document.getElementById('title').style.display='none';
newGame();
};
loop();
</script>
</body>
</html>
<!-- agent-meta {"model":"anthropic/claude-opus-4.8","provider":"openrouter","persona":"basic","sessionId":"daa2e708-5411-49d1-b71d-be36a5870a49","tokensIn":1174,"tokensOut":14879,"tokensTotal":16053,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-07-22T20:00:52.384Z"} -->