RPG
Claude Fable 5 · 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 21,849 bytes · SHA-256 b46d6eeea7db
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Emberfall — A Tiny Action-Adventure RPG</title>
<style>
html,body{margin:0;padding:0;background:#0a0a12;overflow:hidden;font-family:'Courier New',monospace;user-select:none}
#cv{display:block;image-rendering:pixelated}
.overlay{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(5,5,15,.82);z-index:10}
.panel{background:#181828;border:3px solid #6a5a3a;border-radius:8px;padding:24px 32px;color:#e8dcc0;max-width:560px;box-shadow:0 0 40px #000}
h1{color:#ffcf5a;letter-spacing:3px;margin:0 0 6px;text-shadow:0 0 12px #a06010}
h2{color:#ffcf5a;margin:0 0 12px}
.btn{display:inline-block;background:#3a4a2a;border:2px solid #8aa05a;color:#dfffb0;padding:8px 20px;margin:6px 6px 0 0;cursor:pointer;border-radius:4px;font-family:inherit;font-size:15px}
.btn:hover{background:#4c6236}
.btn.dis{opacity:.4;cursor:default}
.shopItem,.invItem{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #333;padding:7px 2px;gap:14px}
.shopItem small,.invItem small{color:#9a9ab0;display:block}
.price{color:#ffd35a;white-space:nowrap}
#msg{position:fixed;left:50%;bottom:26px;transform:translateX(-50%);background:rgba(10,10,25,.88);border:2px solid #6a5a3a;color:#f0e8d0;padding:10px 22px;border-radius:6px;font-size:16px;z-index:9;display:none;max-width:70vw;text-align:center;pointer-events:none}
.hidden{display:none!important}
.key{background:#333;border:1px solid #777;border-radius:3px;padding:1px 6px;color:#ffd;font-size:13px}
ul{padding-left:18px;line-height:1.7;margin:8px 0}
.tag{color:#7ad07a;font-size:12px}
</style>
</head>
<body>
<canvas id="cv"></canvas>
<div id="msg"></div>
<div id="title" class="overlay">
<div class="panel" style="text-align:center">
<h1>⚔ EMBERFALL ⚔</h1>
<div style="color:#b0a890;margin-bottom:10px">A one-hour action-adventure</div>
<div style="text-align:left;font-size:14px;line-height:1.7;border:1px solid #444;padding:10px 14px;border-radius:6px;background:#10101e">
<span class="key">WASD / Arrows</span> move <span class="key">Space / J</span> sword<br>
<span class="key">K</span> bow (once found) <span class="key">E</span> interact / talk / open<br>
<span class="key">I</span> inventory <span class="key">H</span> drink potion<br><br>
Cut bushes, smash pots, earn gold. Buy gear at the shop.<br>
Defeat the <b style="color:#8ad06a">Forest Guardian</b> to open the cave.<br>
Find the key, break the seal, slay the <b style="color:#c07a5a">Stone Golem</b>.<br>
Pray at glowing statues to <b style="color:#7ad0ff">save</b> your progress.
</div>
<button class="btn" id="btnNew">✦ New Game</button>
<button class="btn hidden" id="btnCont">↻ Continue</button>
</div>
</div>
<div id="shopUI" class="overlay hidden">
<div class="panel" style="min-width:440px">
<h2>🏪 Marla's Trading Post</h2>
<div id="shopGold" style="color:#ffd35a;margin-bottom:8px"></div>
<div id="shopList"></div>
<button class="btn" onclick="closeUI()">Leave (Esc)</button>
</div>
</div>
<div id="invUI" class="overlay hidden">
<div class="panel" style="min-width:440px">
<h2>🎒 Inventory</h2>
<div id="invList"></div>
<button class="btn" onclick="closeUI()">Close (I / Esc)</button>
</div>
</div>
<div id="winUI" class="overlay hidden">
<div class="panel" style="text-align:center">
<h1>🏆 VICTORY 🏆</h1>
<div id="winStats" style="line-height:1.9;margin:10px 0"></div>
<button class="btn" onclick="closeUI()">Keep Exploring</button>
</div>
</div>
<script>
'use strict';
/* ============ CONSTANTS / MAP ============ */
const TS=32, MW=70, MH=54;
const cv=document.getElementById('cv'), ctx=cv.getContext('2d');
let VW=innerWidth, VH=innerHeight;
function resize(){VW=cv.width=innerWidth;VH=cv.height=innerHeight}
addEventListener('resize',resize);resize();
const SOLID=new Set([2,3,4,6,7,8,10]);
let map=new Uint8Array(MW*MH);
const T=(x,y)=>(x<0||y<0||x>=MW||y>=MH)?3:map[y*MW+x];
const setT=(x,y,v)=>{if(x>=0&&y>=0&&x<MW&&y<MH)map[y*MW+x]=v};
const rectT=(x0,y0,x1,y1,v)=>{for(let y=y0;y<=y1;y++)for(let x=x0;x<=x1;x++)setT(x,y,v)};
const ringT=(x0,y0,x1,y1,v)=>{for(let x=x0;x<=x1;x++){setT(x,y0,v);setT(x,y1,v)}for(let y=y0;y<=y1;y++){setT(x0,y,v);setT(x1,y,v)}};
function mulberry(a){return function(){a|=0;a=a+0x6D2B79F5|0;let t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296}}
const SLIME_SP=[[26,47],[30,44],[38,42],[42,47],[46,44],[50,47],[45,34],[38,31],[52,40],[60,38],[28,35]];
const BAT_SP=[[20,21],[28,21],[40,21],[48,21],[56,21],[12,16],[34,20],[63,19]];
const RED_SP=[[16,22],[52,22],[56,17]];
const CHEST_DEF=[{id:'steel',tx:65,ty:47},{id:'charm',tx:26,ty:31},{id:'bow',tx:28,ty:16},{id:'ring',tx:46,ty:16},{id:'key',tx:63,ty:17},{id:'flame',tx:7,ty:8},{id:'coins1',tx:52,ty:4},{id:'coins2',tx:60,ty:44}];
const STATUES=[{tx:20,ty:50},{tx:36,ty:22}];
const SIGN={tx:17,ty:48};
const SHOPKEEPER={tx:10,ty:44};
const GATE_DEF=[{id:'g1',tx:34,ty:27},{id:'g2',tx:63,ty:47},{id:'g3',tx:9,ty:8},{id:'g4',tx:56,ty:14}];
const SWITCH_DEF=[{tx:61,ty:47,gate:'g2'},{tx:13,ty:11,gate:'g3'}];
const BLOCK_DEF=[{tx:58,ty:47},{tx:13,ty:6}];
function genMap(){
map.fill(0);
const R=mulberry(1337);
// cave
rectT(0,0,MW-1,25,6);
rectT(4,20,65,23,5); // main corridor
rectT(33,20,35,25,5); // entrance shaft
ringT(6,3,18,13,6); rectT(7,4,17,12,5); setT(12,13,5); rectT(11,13,13,19,5); // puzzle room 2
for(let y=4;y<=12;y++)setT(9,y,6); setT(9,8,5); // inner wall + gate hole
ringT(48,2,66,14,6); rectT(49,3,65,13,5); setT(56,14,5); rectT(55,14,57,19,5); // boss room
rectT(62,16,64,19,5); // key alcove
rectT(26,15,30,18,5); setT(28,19,5); // bow alcove
rectT(44,15,48,18,5); setT(46,19,5); // ring alcove
// rock band + gate corridor
rectT(0,26,MW-1,28,3); setT(34,26,1);setT(34,27,1);setT(34,28,1);
// overworld borders
for(let y=29;y<MH;y++){setT(0,y,3);setT(MW-1,y,3)}
rectT(0,MH-1,MW-1,MH-1,3);
// village + roads
rectT(5,44,22,50,1);
rectT(22,46,40,48,1);
rectT(33,29,35,46,1);
rectT(7,40,13,43,10); // shop building
rectT(17,39,21,42,4); // pond
// puzzle room 1 (forest)
ringT(55,42,67,52,3); rectT(56,43,66,51,0); setT(55,47,0);
for(let y=43;y<=51;y++)setT(63,y,3); setT(63,47,0);
// flowers in village
for(let y=44;y<=52;y++)for(let x=2;x<=23;x++)if(T(x,y)===0&&R()<0.09)setT(x,y,11);
// trees & bushes
for(let y=29;y<=52;y++)for(let x=1;x<=68;x++){
if(T(x,y)!==0)continue;
if(x<24&&y>=38)continue;
if(x>=54&&y>=41)continue;
if(x>=31&&x<=37&&y<=38)continue;
let nearPath=false;
for(let dy=-1;dy<=1;dy++)for(let dx=-1;dx<=1;dx++)if(T(x+dx,y+dy)===1)nearPath=true;
if(nearPath)continue;
const r=R();
if(r<0.16)setT(x,y,2); else if(r<0.22)setT(x,y,7);
}
// pots in cave
const R2=mulberry(777);
for(let y=1;y<=25;y++)for(let x=1;x<=68;x++){
if(T(x,y)!==5)continue;
if(x>=6&&x<=18&&y<=13)continue;
if(x>=48&&y<=14)continue;
if(R2()<0.05)setT(x,y,8);
}
// clear important spots
const clear=[...SLIME_SP,...BAT_SP,...RED_SP,[34,31],[SIGN.tx,SIGN.ty],[SHOPKEEPER.tx,SHOPKEEPER.ty]];
CHEST_DEF.forEach(c=>{clear.push([c.tx,c.ty],[c.tx,c.ty+1])});
STATUES.forEach(s=>{clear.push([s.tx,s.ty],[s.tx,s.ty+1])});
BLOCK_DEF.forEach(b=>clear.push([b.tx,b.ty]));
SWITCH_DEF.forEach(s=>clear.push([s.tx,s.ty]));
GATE_DEF.forEach(g=>clear.push([g.tx,g.ty]));
clear.push([15,47],[16,47]);
for(const [x,y] of clear){const t=T(x,y);if(SOLID.has(t))setT(x,y,y<26?5:(t===10?10:0))}
}
/* ============ AUDIO ============ */
let AC=null;
function A(){if(!AC)AC=new (window.AudioContext||window.webkitAudioContext)();return AC}
function tone(f,d,type='square',v=0.1,slide=0,delay=0){try{const a=A(),t=a.currentTime+delay,o=a.createOscillator(),g=a.createGain();o.type=type;o.frequency.setValueAtTime(f,t);if(slide)o.frequency.exponentialRampToValueAtTime(Math.max(30,f+slide),t+d);g.gain.setValueAtTime(v,t);g.gain.exponentialRampToValueAtTime(0.001,t+d);o.connect(g).connect(a.destination);o.start(t);o.stop(t+d+0.05)}catch(e){}}
function noise(d,v,delay=0,fl=800){try{const a=A(),t=a.currentTime+delay,b=a.createBuffer(1,a.sampleRate*d|0,a.sampleRate),ch=b.getChannelData(0);for(let i=0;i<ch.length;i++)ch[i]=Math.random()*2-1;const s=a.createBufferSource();s.buffer=b;const f=a.createBiquadFilter();f.type='lowpass';f.frequency.value=fl;const g=a.createGain();g.gain.setValueAtTime(v,t);g.gain.exponentialRampToValueAtTime(0.001,t+d);s.connect(f);f.connect(g);g.connect(a.destination);s.start(t)}catch(e){}}
const SFX={
step:()=>noise(0.05,0.035,0,380),
swing:()=>{tone(520,0.09,'sawtooth',0.06,-340);noise(0.07,0.04,0,1400)},
shoot:()=>{tone(950,0.13,'square',0.05,-620);noise(0.05,0.05,0,2200)},
hit:()=>{noise(0.07,0.16,0,900);tone(170,0.08,'square',0.1,-70)},
hurt:()=>{tone(200,0.26,'sawtooth',0.16,-130);noise(0.14,0.1,0,500)},
pickup:()=>{tone(660,0.08,'square',0.09);tone(990,0.12,'square',0.09,0,0.07)},
coin:()=>{tone(1046,0.06,'square',0.08);tone(1568,0.13,'square',0.08,0,0.06)},
buy:()=>[523,659,784,1046].forEach((f,i)=>tone(f,0.1,'square',0.08,0,i*0.06)),
save:()=>[523,659,784].forEach((f,i)=>tone(f,0.55,'sine',0.07,0,i*0.09)),
chest:()=>[392,523,659,784,1046].forEach((f,i)=>tone(f,0.14,'square',0.07,0,i*0.09)),
die:()=>tone(300,0.3,'sawtooth',0.11,-210),
open:()=>{tone(140,0.35,'square',0.09,70);noise(0.3,0.07,0,320)},
heart:()=>[659,784,988,1319].forEach((f,i)=>tone(f,0.22,'sine',0.09,0,i*0.1)),
denied:()=>tone(150,0.15,'square',0.09,-40),
push:()=>noise(0.13,0.07,0,240),
potion:()=>{tone(420,0.16,'sine',0.09,220);tone(620,0.2,'sine',0.09,320,0.12)},
};
/* ============ STATE ============ */
let player, flags, chests, gates, blocks, enemies, pickups, projs, parts, floats, spawnPoint, playTime, frame=0, shake=0, hurtFlash=0, running=false, uiOpen=null, victoryTimer=0, stepT=0;
const keys={};
function newWorld(fromSave){
genMap();
chests=CHEST_DEF.map(c=>({...c,opened:false}));
gates=GATE_DEF.map(g=>({...g,open:false}));
blocks=BLOCK_DEF.map(b=>({...b}));
pickups=[];projs=[];parts=[];floats=[];
if(!fromSave){
flags={mini:false,boss:false,g2:false,g3:false,victory:false,caveHint:false};
player={x:15.5*TS,y:47*TS,w:18,h:20,dir:'down',hp:3,maxhp:3,coins:15,weapon:0,hasBow:false,arrows:0,
inv:{potion:1,key:0},acc:{boots:false,ring:false,amulet:false,charm:false},
atkCd:0,swing:0,bowCd:0,bowAnim:0,invuln:0,pushT:0,moving:false};
spawnPoint={x:15.5*TS,y:47*TS};
playTime=0;
}
spawnEnemies();
}
function spawnEnemies(){
enemies=[];
SLIME_SP.forEach(([x,y])=>enemies.push(mkEnemy('slime',x,y)));
BAT_SP.forEach(([x,y])=>enemies.push(mkEnemy('bat',x,y)));
RED_SP.forEach(([x,y])=>enemies.push(mkEnemy('red',x,y)));
if(!flags.mini)enemies.push(mkEnemy('guardian',34,31));
if(!flags.boss)enemies.push(mkEnemy('golem',57,8));
}
function mkEnemy(type,tx,ty){
const base={x:tx*TS+16,y:ty*TS+16,type,t:Math.random()*100,flash:0,kx:0,ky:0,state:0,st:0,dx:0,dy:0};
const cfg={
slime:{hp:3,dmg:1,spd:0.9,w:20,h:16,coins:[2,4]},
bat:{hp:2,dmg:1,spd:1.6,w:18,h:14,coins:[3,5]},
red:{hp:6,dmg:2,spd:1.0,w:24,h:18,coins:[6,9]},
guardian:{hp:18,dmg:1,spd:1.1,w:30,h:30,coins:[25,25]},
golem:{hp:42,dmg:2,spd:0.8,w:36,h:36,coins:[60,60]},
}[type];
return Object.assign(base,cfg,{maxhp:cfg.hp});
}
/* ============ COLLISION ============ */
function tileSolid(px,py){
const tx=Math.floor(px/TS),ty=Math.floor(py/TS);
if(SOLID.has(T(tx,ty)))return true;
for(const g of gates)if(!g.open&&g.tx===tx&&g.ty===ty)return true;
for(const b of blocks)if(b.tx===tx&&b.ty===ty)return true;
for(const c of chests)if(c.tx===tx&&c.ty===ty)return true;
return false;
}
function rectSolid(x,y,w,h){
return tileSolid(x,y)||tileSolid(x+w,y)||tileSolid(x,y+h)||tileSolid(x+w,y+h)||tileSolid(x+w/2,y)||tileSolid(x+w/2,y+h);
}
function moveEnt(e,dx,dy,isPlayer){
let hitBlock=null;
if(dx){
if(!rectSolid(e.x-e.w/2+dx,e.y-e.h/2,e.w,e.h))e.x+=dx;
else if(isPlayer)hitBlock=findBlock(e.x-e.w/2+dx,e.y-e.h/2,e.w,e.h);
}
if(dy){
if(!rectSolid(e.x-e.w/2,e.y-e.h/2+dy,e.w,e.h))e.y+=dy;
else if(isPlayer&&!hitBlock)hitBlock=findBlock(e.x-e.w/2,e.y-e.h/2+dy,e.w,e.h);
}
return hitBlock;
}
function findBlock(x,y,w,h){
for(const b of blocks){
const bx=b.tx*TS,by=b.ty*TS;
if(x<bx+TS&&x+w>bx&&y<by+TS&&y+h>by)return b;
}
return null;
}
/* ============ MESSAGES / UI ============ */
let msgTimer=0;
const msgEl=document.getElementById('msg');
function msg(t,dur=200){msgEl.textContent=t;msgEl.style.display='block';msgTimer=dur}
function closeUI(){
document.getElementById('shopUI').classList.add('hidden');
document.getElementById('invUI').classList.add('hidden');
document.getElementById('winUI').classList.add('hidden');
uiOpen=null;
}
const SHOP_ITEMS=[
{id:'potion',name:'Health Potion',price:25,desc:'Restores 2 HP — press H to drink'},
{id:'arrows',name:'Bundle of Arrows ×10',price:15,desc:'Ammunition for a bow'},
{id:'boots',name:'Swift Boots',price:50,desc:'+30% movement speed',once:true},
{id:'amulet',name:'Guard Amulet',price:70,desc:'35% chance to block all damage',once:true},
];
function openShop(){
uiOpen='shop';
document.getElementById('shopGold').textContent='Your gold: '+player.coins+' 🪙';
const L=document.getElementById('shopList');L.innerHTML='';
SHOP_ITEMS.forEach((it,i)=>{
const owned=it.once&&player.acc[it.id];
const d=document.createElement('div');d.className='shopItem';
d.innerHTML=`<div><b>${it.name}</b><small>${it.desc}</small></div><div class="price">${owned?'<span class="tag">OWNED</span>':it.price+'g'}</div>`;
if(!owned){const b=document.createElement('button');b.className='btn'+(player.coins<it.price?' dis':'');b.textContent='Buy';
b.onclick=()=>buyItem(i);d.appendChild(b);}
L.appendChild(d);
});
document.getElementById('shopUI').classList.remove('hidden');
}
function buyItem(i){
const it=SHOP_ITEMS[i];
if(player.coins<it.price){SFX.denied();msg("Not enough gold!");return}
if(it.once&&player.acc[it.id])return;
player.coins-=it.price;SFX.buy();
if(it.id==='potion')player.inv.potion++;
else if(it.id==='arrows')player.arrows+=10;
else if(it.id==='boots'){player.acc.boots=true;msg("Swift Boots equipped! You feel lighter.")}
else if(it.id==='amulet'){player.acc.amulet=true;msg("Guard Amulet equipped! It hums softly.")}
burst(player.x,player.y-20,'#ffd35a',10,2);
openShop();
}
function openInv(){
uiOpen='inv';
const p=player,L=document.getElementById('invList');
const wname=['Rusty Sword','Steel Sword ★','Flame Blade ★★'][p.weapon];
let h=`<div class="invItem"><div><b>⚔ ${wname}</b><small>Damage ${[1,2,3][p.weapon]+(p.acc.ring?1:0)} · Range ${['short','medium','long'][p.weapon]}${p.weapon===2?' · burns foes':''}</small></div><span class="tag">EQUIPPED</span></div>`;
if(p.hasBow)h+=`<div class="invItem"><div><b>🏹 Hunter's Bow</b><small>Ranged, 2 dmg · Arrows: ${p.arrows}</small></div><span class="tag">K to fire</span></div>`;
h+=`<div class="invItem"><div><b>🧪 Health Potion ×${p.inv.potion}</b><small>Restores 2 HP</small></div>${p.inv.potion>0?'<button class="btn" onclick="usePotion();openInv()">Use</button>':''}</div>`;
const accs=[['boots','👢 Swift Boots','+30% move speed'],['ring','💍 Power Ring','+1 sword damage'],['amulet','🔮 Guard Amulet','35% chance to block damage'],['charm','🍀 Swift Charm','Attack speed up']];
for(const [k,n,d] of accs)if(p.acc[k])h+=`<div class="invItem"><div><b>${n}</b><small>${d}</small></div><span class="tag">EQUIPPED</span></div>`;
if(p.inv.key>0)h+=`<div class="invItem"><div><b>🗝 Ancient Key</b><small>Opens the sealed door deep in the cave</small></div></div>`;
L.innerHTML=h;
document.getElementById('invUI').classList.remove('hidden');
}
function usePotion(){
if(player.inv.potion<=0){SFX.denied();return}
if(player.hp>=player.maxhp){msg("Health is already full.");SFX.denied();return}
player.inv.potion--;player.hp=Math.min(player.maxhp,player.hp+2);
SFX.potion();burst(player.x,player.y,'#ff6a8a',14,2.2);
msg("You drink the potion. +2 HP");
}
/* ============ SAVE ============ */
function saveGame(){
spawnPoint={x:player.x,y:player.y};
const data={p:{x:player.x,y:player.y,hp:player.hp,maxhp:player.maxhp,coins:player.coins,weapon:player.weapon,hasBow:player.hasBow,arrows:player.arrows,inv:player.inv,acc:player.acc},
flags,opened:chests.filter(c=>c.opened).map(c=>c.id),gateOpen:gates.filter(g=>g.open).map(g=>g.id),
blocks:blocks.map(b=>[b.tx,b.ty]),spawn:spawnPoint,time:playTime};
localStorage.setItem('emberfall_save',JSON.stringify(data));
player.hp=player.maxhp;
SFX.save();burst(player.x,player.y,'#7ad0ff',24,2.5);
msg("✦ Game saved. Your wounds heal. ✦");
}
function loadGame(){
const raw=localStorage.getItem('emberfall_save');if(!raw)return false;
try{
const d=JSON.parse(raw);
flags=d.flags;
newWorld(true);
Object.assign(flags,d.flags);
player={x:d.p.x,y:d.p.y,w:18,h:20,dir:'down',hp:d.p.hp,maxhp:d.p.maxhp,coins:d.p.coins,weapon:d.p.weapon,hasBow:d.p.hasBow,arrows:d.p.arrows,
inv:d.p.inv,acc:d.p.acc,atkCd:0,swing:0,bowCd:0,bowAnim:0,invuln:0,pushT:0,moving:false};
chests.forEach(c=>{if(d.opened.includes(c.id))c.opened=true});
gates.forEach(g=>{if(d.gateOpen.includes(g.id))g.open=true});
d.blocks.forEach((b,i)=>{if(blocks[i]){blocks[i].tx=b[0];blocks[i].ty=b[1]}});
spawnPoint=d.spawn;playTime=d.time||0;
spawnEnemies();
return true;
}catch(e){return false}
}
/* ============ PARTICLES / FLOATERS ============ */
function burst(x,y,col,n,sp){
for(let i=0;i<n;i++){
const a=Math.random()*Math.PI*2,s=(0.4+Math.random())*sp;
parts.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s-0.5,life:20+Math.random()*20,col,r:1.5+Math.random()*2});
}
}
function floatTxt(x,y,txt,col){floats.push({x,y,txt,col,life:50})}
/* ============ COMBAT ============ */
const DIRA={down:Math.PI/2,up:-Math.PI/2,left:Math.PI,right:0};
function melee(){
const p=player;
if(p.atkCd>0||uiOpen)return;
p.atkCd=(p.acc.charm?[22,20,17]:[30,26,22])[p.weapon];
p.swing=14;SFX.swing();
const dmg=[1,2,3][p.weapon]+(p.acc.ring?1:0);
const range=[38,46,54][p.weapon];
const fa=DIRA[p.dir];
for(const e of enemies){
const dx=e.x-p.x,dy=e.y-p.y,d=Math.hypot(dx,dy);
if(d<range+e.w/2){
let ad=Math.abs(((Math.atan2(dy,dx)-fa+Math.PI*3)%(Math.PI*2))-Math.PI);
if(ad<1.25)hurtEnemy(e,dmg,fa);
}
}
// cut bushes / pots
for(const dist of [22,44]){
const cx=p.x+Math.cos(fa)*dist,cy=p.y+Math.sin(fa)*dist;
const tx=Math.floor(cx/TS),ty=Math.floor(cy/TS),t=T(tx,ty);
if(t===7||t===8){
setT(tx,ty,t===7?0:5);
SFX.hit();burst(tx*TS+16,ty*TS+16,t===7?'#5a9a3a':'#b08a5a',10,2);
const r=Math.random();
if(r<0.4)pickups.push({x:tx*TS+16,y:ty*TS+16,type:'coin',v:2+Math.floor(Math.random()*3)});
else if(r<0.5)pickups.push({x:tx*TS+16,y:ty*TS+16,type:'heart'});
else if(r<0.62&&p.hasBow)pickups.push({x:tx*TS+16,y:ty*TS+16,type:'arrows',v:4});
}
}
if(p.weapon===2)for(let i=0;i<5;i++)parts.push({x:p.x+Math.cos(fa)*range*0.7,y:p.y+Math.sin(fa)*range*0.7,vx:(Math.random()-0.5)*2,vy:-1-Math.random(),life:18,col:Math.random()<0.5?'#ff8a2a':'#ffd35a',r:2.5});
}
function shootBow(){
const p=player;
if(!p.hasBow||uiOpen)return;
if(p.bowCd>0)return;
if(p.arrows<=0){msg("Out of arrows! Buy more at the shop.");SFX.denied();p.bowCd=20;return}
p.bowCd=32;p.bowAnim=14;p.arrows--;SFX.shoot();
const fa=DIRA[p.dir];
projs.push({x:p.x,y:p.y-4,vx:Math.cos(fa)*7,vy:Math.sin(fa)*7,life:110,friendly:true,dmg:2,ang:fa});
}
function hurtEnemy(e,dmg,fromAng){
e.hp-=dmg;e.flash=8;
e.kx=Math.cos(fromAng)*4;e.ky=Math.sin(fromAng)*4;
SFX.hit();shake=4;
burst(e.x,e.y,'#ffd0a0',8,2.4);
floatTxt(e.x,e.y-14,'-'+dmg,'#ffe07a');
if(player.weapon===2&&e.hp>0){e.hp-=0; burst(e.x,e.y,'#ff7a2a',4,1.5)}
if(e.hp<=0)killEnemy(e);
}
function killEnemy(e){
SFX.die();burst(e.x,e.y,'#c0c0d0',16,3);
const n=e.coins[0]+Math.floor(Math.random()*(e.coins[1]-e.coins[0]+1));
for(let i=0;i<Math.min(n,8);i++)pickups.push({x:e.x+(Math.random()-0.5)*20,y:e.y+(Math.random()-0.5)*20,type:'coin',v:Math.ceil(n/Math.min(n,8))});
if(Math.random()<0.14)pickups.push({x:e.x,y:e.y,type:'heart'});
if(player.hasBow&&Math.random()<0.22)pickups.push({x:e.x,y:e.y,type:'arrows',v:3});
if(e.type==='guardian'){
flags.mini=true;
const g=gates.find(g=>g.id==='g1');g.open=true;SFX.open();
pickups.push({x:e.x,y:e.y-10,type:'crystal'});
msg("The Forest Guardian falls! The cave gate rumbles open to the north...",380);
shake=14;
}
if(e.type==='golem'){
flags.boss=true;
pickups.push({x:e.x,y:e.y-10,type:'crystal'});
msg("The Stone Golem crumbles to dust!",300);
shake=18;victoryTimer=190;
}
enemies=enemies.filter(x=>x!==e);
}
function damagePlayer(d,fx,fy){
const p=player;
if(p.invuln>0)return;
if(p.acc.amulet&&Math.random()<0.35){
floatTxt(p.x,p.y-20,'BLOCKED','#7ad0ff');burst(p.x,p.y,'#7ad0ff',10,2);
tone(880,0.1,'triangle',0.1);p.invuln=40;return;
}
p.hp-=d;p.invuln=75;hurtFlash=16;shake=8;
SFX.hurt();burst(p.x,p.y,'#ff4a4a',14,3);
floatTxt(p.x,p.y-22,'-'+d,'#ff6a6a');
const
<!-- agent-meta {"model":"anthropic/claude-fable-5","provider":"openrouter","persona":"basic","sessionId":"b4a747cc-cc0a-4781-be25-eaa634bdca75","tokensIn":1174,"tokensOut":32768,"tokensTotal":33942,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-07-22T20:12:56.243Z"} -->