← RPG results

RPG

Kimi k3 · 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 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>