RPG
Kimi k3 · full

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 13,449 bytes · SHA-256 19af30c24011
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Embervale — Action-Adventure RPG</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
html,body{margin:0;padding:0;background:#0b0e14;overflow:hidden;height:100%;font-family:"Segoe UI",Verdana,sans-serif;}
canvas{display:block;cursor:crosshair;}
</style>
</head>
<body>
<canvas id="game"></canvas>
<script>
"use strict";
/* ============================================================
EMBERVALE — a compact action-adventure RPG
exploration • combat • items • upgrades • bosses • saves
============================================================ */
// ---------------- utils ----------------
const TAU=Math.PI*2;
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const lerp=(a,b,t)=>a+(b-a)*t;
const dist=(x1,y1,x2,y2)=>Math.hypot(x2-x1,y2-y1);
const angTo=(x1,y1,x2,y2)=>Math.atan2(y2-y1,x2-x1);
function angDiff(a,b){let d=(a-b)%TAU;if(d>Math.PI)d-=TAU;if(d<-Math.PI)d+=TAU;return d;}
function mulberry32(a){return function(){a|=0;a=a+0x6D2B79F5|0;var t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296;};}
function thash(x,y){let h=(x*374761393+y*668265263)|0;h=(h^(h>>>13))*1274126177|0;return((h^(h>>>16))>>>0)/4294967296;}
const rnd=(a,b)=>a+Math.random()*(b-a);
const irnd=(a,b)=>Math.floor(rnd(a,b+1));
// ---------------- audio ----------------
let AC=null,masterGain=null,musicGain=null,muted=false;
function audioInit(){
if(AC)return;
try{
AC=new (window.AudioContext||window.webkitAudioContext)();
masterGain=AC.createGain();masterGain.gain.value=0.9;masterGain.connect(AC.destination);
musicGain=AC.createGain();musicGain.gain.value=0.30;musicGain.connect(masterGain);
}catch(e){AC=null;}
}
function tone(freq,dur,type,vol,slideTo,when,dest){
if(!AC||muted)return;
type=type||'sine';vol=vol===undefined?0.2:vol;when=when||0;dest=dest||masterGain;
const t0=AC.currentTime+when;
const o=AC.createOscillator(),g=AC.createGain();
o.type=type;o.frequency.setValueAtTime(freq,t0);
if(slideTo)o.frequency.exponentialRampToValueAtTime(Math.max(20,slideTo),t0+dur);
g.gain.setValueAtTime(vol,t0);
g.gain.exponentialRampToValueAtTime(0.0001,t0+dur);
o.connect(g);g.connect(dest);
o.start(t0);o.stop(t0+dur+0.02);
}
function noiseBurst(dur,vol,fFreq,q,when,slideTo){
if(!AC||muted)return;
q=q||1;when=when||0;
const t0=AC.currentTime+when;
const len=Math.max(1,Math.floor(AC.sampleRate*dur));
const buf=AC.createBuffer(1,len,AC.sampleRate);
const d=buf.getChannelData(0);
for(let i=0;i<len;i++)d[i]=Math.random()*2-1;
const src=AC.createBufferSource();src.buffer=buf;
const f=AC.createBiquadFilter();f.type='lowpass';f.frequency.setValueAtTime(fFreq,t0);f.Q.value=q;
if(slideTo)f.frequency.exponentialRampToValueAtTime(Math.max(30,slideTo),t0+dur);
const g=AC.createGain();
g.gain.setValueAtTime(vol,t0);
g.gain.exponentialRampToValueAtTime(0.0001,t0+dur);
src.connect(f);f.connect(g);g.connect(masterGain);
src.start(t0);src.stop(t0+dur+0.02);
}
const SFX={
step(alt){noiseBurst(0.05,0.05,alt?700:520,1);},
swing(){noiseBurst(0.13,0.16,2600,2,0,300);},
hit(){tone(180,0.09,'square',0.22,90);noiseBurst(0.08,0.14,1800,1);},
hurt(){tone(140,0.28,'sawtooth',0.32,55);noiseBurst(0.2,0.2,500,1);},
enemyDie(){noiseBurst(0.25,0.2,1400,1,0,150);tone(220,0.2,'triangle',0.14,60);},
coin(){tone(880,0.07,'sine',0.16);tone(1318,0.12,'sine',0.16,null,0.06);},
pickup(){tone(523,0.07,'square',0.12);tone(659,0.07,'square',0.12,null,0.07);tone(784,0.12,'square',0.12,null,0.14);},
potion(){tone(300,0.3,'sine',0.22,900);tone(600,0.25,'sine',0.12,1200,0.1);},
bow(){tone(500,0.1,'triangle',0.2,160);noiseBurst(0.05,0.1,3000,2);},
explosion(){noiseBurst(0.55,0.5,900,0.7,0,80);tone(70,0.5,'sine',0.4,35);},
chest(){tone(392,0.1,'triangle',0.16);tone(523,0.1,'triangle',0.16,null,0.09);tone(659,0.1,'triangle',0.16,null,0.18);tone(784,0.22,'triangle',0.18,null,0.27);},
save(){for(let i=0;i<5;i++)tone(392*Math.pow(1.335,i),0.3,'triangle',0.12,null,i*0.07);},
shop(){tone(659,0.15,'sine',0.15);tone(880,0.25,'sine',0.15,null,0.1);},
buy(){tone(784,0.08,'square',0.14);tone(1046,0.16,'square',0.14,null,0.08);},
error(){tone(130,0.18,'sawtooth',0.2,90);},
gate(){tone(90,0.5,'sawtooth',0.22,45);noiseBurst(0.4,0.18,300,1,0.1,60);},
roar(){tone(95,0.7,'sawtooth',0.35,60);tone(142,0.6,'sawtooth',0.22,80,0.05);noiseBurst(0.5,0.18,350,1);},
heartUp(){tone(523,0.15,'sine',0.2);tone(659,0.15,'sine',0.2,null,0.12);tone(784,0.15,'sine',0.2,null,0.24);tone(1046,0.4,'sine',0.22,null,0.36);},
ui(){tone(700,0.05,'square',0.08);},
bowPick(){tone(440,0.12,'triangle',0.16);tone(554,0.12,'triangle',0.16,null,0.1);tone(659,0.12,'triangle',0.16,null,0.2);tone(880,0.3,'triangle',0.18,null,0.3);},
burn(){noiseBurst(0.15,0.08,1200,1);},
arrowHit(){tone(260,0.06,'square',0.14,140);},
bombPlace(){tone(220,0.08,'square',0.12,180);},
fuse(){tone(1200,0.04,'square',0.06);},
jump(){tone(200,0.15,'sine',0.12,500);},
slam(){tone(80,0.3,'sine',0.35,40);noiseBurst(0.25,0.3,600,1,0,100);},
skeletonShoot(){tone(320,0.07,'square',0.1,240);},
victory(){const n=[523,659,784,1046,784,1046];for(let i=0;i<n.length;i++)tone(n[i],0.25,'triangle',0.18,null,i*0.14);}
};
function sfx(n,a){if(AC&&!muted&&SFX[n])SFX[n](a);}
// adaptive music
const MUSIC={
village:{bpm:92,lead:[262,0,330,0,392,0,330,0,294,0,349,0,392,0,0,0],bass:[131,0,0,0,110,0,0,0,98,0,0,0,131,0,0,0],vol:0.10},
forest:{bpm:104,lead:[330,0,392,440,0,392,330,0,294,330,294,0,262,0,0,0],bass:[131,0,131,0,147,0,147,0,165,0,147,0,131,0,0,0],vol:0.09},
cave:{bpm:80,lead:[220,0,0,262,0,0,247,0,220,0,0,0,196,0,0,0],bass:[110,0,0,0,0,0,104,0,110,0,0,0,98,0,0,0],vol:0.09},
desert:{bpm:100,lead:[294,0,311,294,0,0,349,0,294,0,262,0,294,0,0,0],bass:[147,0,147,0,139,0,139,0,147,0,0,0,131,0,0,0],vol:0.09},
boss:{bpm:148,lead:[220,262,220,0,247,220,196,0,220,262,294,262,220,0,196,0],bass:[110,110,0,110,104,104,0,104,110,110,0,110,98,98,0,98],vol:0.11}
};
let musStep=0,nextNoteT=0,curZone='village',bossActive=null;
function updateMusic(){
if(!AC||muted||state==='title')return;
const zone=bossActive&&!bossActive.dead?'boss':curZone;
const m=MUSIC[zone]||MUSIC.forest;
const stepDur=60/m.bpm/2; // 8th notes
if(nextNoteT<AC.currentTime)nextNoteT=AC.currentTime+0.05;
while(nextNoteT<AC.currentTime+0.12){
const i=musStep%16;
const l=m.lead[i],b=m.bass[i];
if(l)tone(l,stepDur*0.9,'square',m.vol,null,nextNoteT-AC.currentTime,musicGain);
if(b)tone(b,stepDur*1.4,'triangle',m.vol*1.2,null,nextNoteT-AC.currentTime,musicGain);
musStep++;nextNoteT+=stepDur;
}
}
// ---------------- input ----------------
const keys={};
const mouse={x:0,y:0,clicked:false};
addEventListener('keydown',e=>{
audioInit();if(AC&&AC.state==='suspended')AC.resume();
const k=e.key.toLowerCase();
if(k===' '||k==='tab'||k.startsWith('arrow'))e.preventDefault();
if(!e.repeat){keys[k]=true;handleKey(k);}
});
addEventListener('keyup',e=>{keys[e.key.toLowerCase()]=false;});
const canvas=document.getElementById('game');
const ctx=canvas.getContext('2d');
let VW=0,VH=0;
function resize(){VW=canvas.width=innerWidth;VH=canvas.height=innerHeight;}
addEventListener('resize',resize);resize();
canvas.addEventListener('mousemove',e=>{mouse.x=e.clientX;mouse.y=e.clientY;});
canvas.addEventListener('mousedown',e=>{audioInit();if(AC&&AC.state==='suspended')AC.resume();mouse.x=e.clientX;mouse.y=e.clientY;mouse.clicked=true;});
canvas.addEventListener('contextmenu',e=>e.preventDefault());
// ---------------- world / map ----------------
const TILE=32,MW=72,MH=44;
let map=[];
const SOLID={'R':1,'T':1,'W':1,'H':1,'C':1,'d':1,'G':1,'X':1};
function tileAt(tx,ty){if(tx<0||ty<0||tx>=MW||ty>=MH)return'R';return map[ty][tx];}
function solidAtPx(px,py){return !!SOLID[tileAt(Math.floor(px/TILE),Math.floor(py/TILE))];}
function canStand(x,y,r){
return !solidAtPx(x-r,y-r)&&!solidAtPx(x+r,y-r)&&!solidAtPx(x-r,y+r)&&!solidAtPx(x+r,y+r);
}
function moveEntity(e,dx,dy){
const r=e.r||10;
if(canStand(e.x+dx,e.y,r))e.x+=dx;
if(canStand(e.x,e.y+dy,r))e.y+=dy;
}
function zoneAt(x,y){
const tx=Math.floor(x/TILE),ty=Math.floor(y/TILE);
if(tx<31)return ty>=29?'village':'forest';
if(tx>32)return ty<=22?'cave':'desert';
return curZone;
}
const ZONENAMES={village:'Embervale Village',forest:'Whisperwood Forest',cave:'Ashen Caverns',desert:'Scorchsand Wastes'};
// chests, npcs, save points, gates — declared here, populated in genMap
let chests=[],npcs=[],savePoints=[],gates={},crackedTiles=[];
function genMap(){
map=[];
for(let y=0;y<MH;y++){map.push(new Array(MW).fill('.'));}
const rndG=mulberry32(1337);
// borders
for(let x=0;x<MW;x++){map[0][x]='R';map[MH-1][x]='R';}
for(let y=0;y<MH;y++){map[y][0]='R';map[y][MW-1]='R';}
// river
for(let y=0;y<MH;y++){map[y][31]='W';map[y][32]='W';}
// bridge to cave
map[13][31]='B';map[13][32]='B';
// forest: grass variants + trees
const clearings=[[14,10,6],[5,4,3],[26,24,2],[12,28,3],[12,20,2],[20,6,2],[24,14,2],[8,16,2]];
for(let y=1;y<28;y++)for(let x=1;x<30;x++){
let clear=false;
if(x===12||x===13){clear=true;} // main path column
for(const c of clearings)if(dist(x,y,c[0],c[1])<c[2])clear=true;
if(clear)continue;
const r=rndG();
if(r<0.17)map[y][x]='T';
else if(r<0.25)map[y][x]=',';
}
// hidden bomb chest grove (26,24)
map[23][25]='T';map[23][27]='T';map[24][25]='T';map[24][27]='T';
map[25][25]='T';map[25][26]='T';map[25][27]='T';
// village paths
for(let x=5;x<=29;x++)map[34][x]='P';
for(let y=29;y<=34;y++){map[y][12]='P';map[y][13]='P';}
for(let y=32;y<=34;y++)map[y][16]='P';
for(let x=6;x<=10;x++)map[36][x]='P';
// houses (solid H), doors D
function house(x0,y0,w,h,doorX,doorY){
for(let y=y0;y<y0+h;y++)for(let x=x0;x<x0+w;x++)map[y][x]='H';
map[doorY][doorX]='D';
}
house(6,29,4,3, 7,32);
house(14,29,5,3, 16,32); // shop
house(7,37,4,3, 8,36);
// cave region
for(let y=0;y<22;y++)for(let x=33;x<MW;x++)map[y][x]='c';
for(let y=0;y<22;y++)map[y][33]='C';
for(let x=33;x<MW;x++)map[21][x]='C';
for(let x=0;x<MW;x++)map[0][x]='R';
map[13][33]='G'; // cave gate (needs cave key)
// interior pillars
const caveClear=[[58,14,7],[36,13,3],[38,3,3],[66,3,2],[50,13,2],[45,8,2],[40,16,2],[64,10,2],[52,6,2]];
for(let y=1;y<21;y++)for(let x=34;x<MW-1;x++){
let clear=false;
for(const c of caveClear)if(dist(x,y,c[0],c[1])<c[2])clear=true;
if(!clear&&rndG()<0.055)map[y][x]='C';
}
// bow chamber (cracked rock entrance)
for(let x=63;x<=70;x++){map[1][x]='C';map[5][x]='C';}
for(let y=1;y<=5;y++){map[y][63]='C';}
map[3][63]='X'; // bombable
for(let y=2;y<=4;y++)for(let x=64;x<=69;x++)map[y][x]='c';
// wall row cave|desert
for(let x=33;x<MW;x++)map[22][x]='R';
map[22][52]='G'; // desert gate (needs desert key)
// desert
for(let y=23;y<MH-1;y++)for(let x=33;x<MW-1;x++){
map[y][x]='~';
if(rndG()<0.09)map[y][x]=',';
}
// desert rocks
const desClear=[[52,24,3],[58,33,7],[68,40,3],[38,40,3],[45,30,3]];
for(let y=23;y<MH-1;y++)for(let x=33;x<MW-1;x++){
let clear=false;
for(const c of desClear)if(dist(x,y,c[0],c[1])<c[2])clear=true;
if(!clear&&rndG()<0.05)map[y][x]='d';
}
// golem arena ring
for(let y=27;y<=40;y++)for(let x=51;x<=66;x++){
const d=dist(x,y,58,33);
if(d>3.6&&d<5.0&&!(Math.abs(x-58)<=1&&y>33))map[y][x]='d';
}
map[38][58]='~';map[38][57]='~';map[38][59]='~'; // arena entrance
// ---- gates registry ----
gates={
cave:{tx:33,ty:13,open:false,key:'caveKey',name:'Cave Gate'},
desert:{tx:52,ty:22,open:false,key:'desertKey',name:'Desert Gate'}
};
// ---- chests ----
chests=[
{id:'forest_gold',tx:5,ty:4,loot:{type:'gold',amt:40},label:'40 gold'},
{id:'forest_bombs',tx:26,ty:24,loot:{type:'bomb',amt:3},label:'Bombs ×3'},
{id:'cave_bow',tx:66,ty:3,loot:{type:'bow'},label:'Hunter\'s Bow'},
{id:'cave_heart',tx:38,ty:3,loot:{type:'heart'},label:'Heart Container'},
{id:'desert_storm',tx:68,ty:40,loot:{type:'storm'},label:'Storm Blade'},
{id:'desert_gold',tx:38,ty:40,loot:{type:'gold',amt:100},label:'100 gold'}
];
for(const c of chests){c.x=c.tx*TILE+16;c.y=c.ty*TILE+16;c.opened=false;}
// ---- npcs ----
npcs=[
{x:6.5*TILE,y:35.5*TILE,kind:'villager',name:'Elder Mira',gift:true,lines:[
"Welcome to Embervale, traveler. Dark things stir beyond the river...",
"A hulking slime nests in the north forest clearing. It guards a KEY to the Ashen Caverns.",
"Take this potion, dear. Press TAB for your bag, Q to drink. And do SAVE at the blue crystal!"]},
{x:16.5*TILE,y:33.4*TILE,kind:'shop',name:'Merchant Pell',lines:["Fine wares! Stand close and press E to trade."]},
{x:24.5*TILE,y:33.5*TILE,kind:'sign',name:'Signpost',lines:["EMBERVALE VILLAGE — Save crystals glow blue. The forest lies north."]},
{x:35.5*TILE,y:15.5*TILE,kind:'sign',name:'Old Sign',lines:["ASHEN CAVERNS — Cracked rocks hide secrets. Bombs break them."]},
{x:52.5*TILE,y:24.5*TILE,kind:'sign',name:'Weathered Sign',lines:["SCORCHSAND WASTES — The Golem King sleeps in the stone ring. Bring your strongest blade."]}
];
// ---- save points ----
savePoints=[
{x:11*TILE+16,y:33*TILE+16,id:'village'},
{x:35*TILE+16,y:16*TILE+16,id:'cave'}
];
}
</script>
</body>
</html>