🎮ArcadeLab

BACKROOMS - LEVEL 0 / 1 / ???

by NovaWave22
3746 lines40.9 KB
▶ Play
<!DOCTYPE html>
<html lang="fa">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>BACKROOMS - LEVEL 0 / 1 / ???</title>

<style>
*{
 box-sizing:border-box;
 user-select:none;
 -webkit-user-select:none;
}

html,body{
 margin:0;
 width:100%;
 height:100%;
 overflow:hidden;
 background:#000;
 touch-action:none;
}

canvas{
 width:100%;
 height:100%;
 display:block;
}

#menu,#levelMenu,#win{
 position:fixed;
 inset:0;
 z-index:100;
 display:flex;
 flex-direction:column;
 align-items:center;
 justify-content:center;
 background:#121008;
 color:white;
 font-family:Arial;
 text-align:center;
}

#menu h1,#levelMenu h1,#win h1{
 margin:0 0 15px;
 font-size:38px;
 letter-spacing:4px;
}

button{
 border:0;
 border-radius:10px;
 padding:13px 25px;
 margin:5px;
 font-size:17px;
 font-weight:bold;
}

.levelBtn{
 width:250px;
 background:#292617;
 color:white;
 border:1px solid #665d35;
}

#hud{
 position:fixed;
 top:10px;
 left:50%;
 transform:translateX(-50%);
 z-index:20;
 padding:8px 14px;
 border-radius:9px;
 background:#000a;
 color:white;
 font:13px Arial;
 pointer-events:none;
}

#sanityBox{
 position:fixed;
 top:42px;
 left:50%;
 transform:translateX(-50%);
 z-index:20;
 width:150px;
 height:9px;
 background:#111;
 border:1px solid #777;
 border-radius:6px;
 display:none;
}

#sanity{
 width:100%;
 height:100%;
 background:#eee;
 border-radius:5px;
}

#staminaBox{
 position:fixed;
 top:57px;
 left:50%;
 transform:translateX(-50%);
 z-index:20;
 width:150px;
 height:8px;
 background:#111;
 border:1px solid #555;
 border-radius:6px;
 display:none;
}

#stamina{
 width:100%;
 height:100%;
 background:#ddd;
 border-radius:5px;
}

#waterBox{
 position:fixed;
 top:72px;
 left:50%;
 transform:translateX(-50%);
 z-index:20;
 padding:4px 10px;
 border-radius:8px;
 background:#000a;
 color:white;
 font:bold 12px Arial;
 display:none;
}

#cross{
 position:fixed;
 left:50%;
 top:50%;
 width:8px;
 height:8px;
 margin:-4px;
 border:2px solid white;
 border-radius:50%;
 z-index:20;
 pointer-events:none;
}

#warning{
 position:fixed;
 top:92px;
 left:50%;
 transform:translateX(-50%);
 z-index:25;
 color:white;
 font:bold 16px Arial;
 text-shadow:0 2px 5px #000;
 pointer-events:none;
 opacity:0;
}

#message{
 position:fixed;
 left:50%;
 bottom:150px;
 transform:translateX(-50%);
 z-index:40;
 color:white;
 background:#000c;
 padding:10px 18px;
 border-radius:10px;
 font:bold 14px Arial;
 display:none;
 white-space:nowrap;
}

#joy{
 position:fixed;
 left:18px;
 bottom:18px;
 width:132px;
 height:132px;
 border:3px solid #ffffff44;
 border-radius:50%;
 background:#ffffff12;
 z-index:30;
}

#stick{
 position:absolute;
 left:35px;
 top:35px;
 width:56px;
 height:56px;
 border-radius:50%;
 background:#ffffff77;
}

#look{
 position:fixed;
 right:0;
 top:0;
 width:72%;
 height:100%;
 z-index:10;
}

#runButton,#saveButton{
 position:fixed;
 z-index:35;
 display:none;
 background:#111b;
 color:white;
 border:1px solid #777;
}

#runButton{
 right:18px;
 bottom:35px;
 width:82px;
 height:82px;
 border-radius:50%;
}

#saveButton{
 right:18px;
 bottom:125px;
 padding:10px 15px;
}

#keyPanel{
 position:fixed;
 top:50%;
 left:50%;
 transform:translate(-50%,-50%);
 z-index:80;
 background:#171717ee;
 color:white;
 border:1px solid #777;
 border-radius:15px;
 padding:20px;
 display:none;
 text-align:center;
 font-family:Arial;
}

#keyPanel h2{
 margin-top:0;
}

#keyPanel button{
 background:#333;
 color:white;
}

#codeBox{
 position:fixed;
 inset:0;
 z-index:90;
 display:none;
 align-items:center;
 justify-content:center;
 background:#000b;
}

#codePanel{
 width:280px;
 padding:20px;
 border-radius:18px;
 background:#292617;
 color:white;
 text-align:center;
 font-family:Arial;
}

.keypad{
 display:grid;
 grid-template-columns:repeat(3,1fr);
 gap:8px;
}

.keypad button,#closeCode{
 height:48px;
 border:0;
 border-radius:9px;
 font-size:20px;
 font-weight:bold;
}

#closeCode{
 width:100%;
 margin-top:10px;
}

@media(max-width:700px){

 #menu h1,#levelMenu h1,#win h1{
  font-size:29px;
 }

 .levelBtn{
  width:220px;
 }

 #joy{
  width:118px;
  height:118px;
 }

 #stick{
  left:31px;
  top:31px;
  width:52px;
  height:52px;
 }

 #runButton{
  width:70px;
  height:70px;
  right:12px;
  bottom:25px;
 }

 #saveButton{
  right:12px;
  bottom:105px;
 }
}
</style>
</head>

<body>

<canvas id="game"></canvas>

<div id="menu">
 <h1>BACKROOMS</h1>
 <p>LEVEL 0 • LEVEL 1 • LEVEL ???</p>
 <button id="start">START</button>
 <button id="continueBtn">CONTINUE</button>
</div>

<div id="levelMenu" style="display:none">
 <h1>SELECT LEVEL</h1>
 <button class="levelBtn" onclick="startLevel(0)">🟨 LEVEL 0</button>
 <button class="levelBtn" onclick="startLevel(1)">🅿️ LEVEL 1 — PARKING</button>
 <button class="levelBtn" onclick="startLevel(2)">❓ LEVEL ??? — EXPERIMENTAL</button>
</div>

<div id="hud">LEVEL 0</div>

<div id="sanityBox">
 <div id="sanity"></div>
</div>

<div id="staminaBox">
 <div id="stamina"></div>
</div>

<div id="waterBox">
 💧 HOLY WATER: <span id="waterCount">0</span>
</div>

<div id="warning">⚠ SOMETHING IS NEAR</div>

<div id="message"></div>

<div id="cross"></div>

<div id="joy">
 <div id="stick"></div>
</div>

<div id="look"></div>

<button id="runButton">🏃<br>RUN</button>
<button id="saveButton">💾 SAVE</button>

<div id="keyPanel">
 <h2>🔑 EXIT ROOM</h2>
 <p id="keyStatus">0 / 4 KEYS</p>
 <p>چهار کلید را پیدا کن.</p>
 <button onclick="closeKeyPanel()">CLOSE</button>
</div>

<div id="codeBox">
 <div id="codePanel">
  <h2>🔐 ELEVATOR</h2>
  <p>رنگ خودرو را انتخاب کن</p>

  <div class="keypad">
   <button onclick="elevatorColor('red')">🔴</button>
   <button onclick="elevatorColor('blue')">🔵</button>
   <button onclick="elevatorColor('yellow')">🟡</button>
   <button onclick="elevatorColor('green')">🟢</button>
  </div>

  <button id="closeCode">CLOSE</button>
 </div>
</div>

<div id="win">
 <h1 id="winTitle">🎉 ESCAPED!</h1>
 <p id="winText">LEVEL COMPLETE</p>
 <button onclick="backToMenu()">MENU</button>
</div>

<script>

/* =====================================================
 WEBGL
===================================================== */

const canvas=document.getElementById("game");
const gl=canvas.getContext("webgl");

if(!gl){
 document.body.innerHTML=
  "<h2 style='color:white'>WebGL IS NOT AVAILABLE</h2>";
 throw new Error("WebGL unavailable");
}

function resize(){

 const d=Math.min(
  devicePixelRatio||1,
  1.5
 );

 canvas.width=
  Math.floor(innerWidth*d);

 canvas.height=
  Math.floor(innerHeight*d);

 gl.viewport(
  0,
  0,
  canvas.width,
  canvas.height
 );
}

resize();

addEventListener(
 "resize",
 resize
);


/* =====================================================
 SHADERS
===================================================== */

const VS=`
attribute vec3 position;
uniform mat4 matrix;

void main(){

 gl_Position=
  matrix*vec4(position,1.0);

}
`;

const FS=`
precision mediump float;

uniform vec3 color;

void main(){

 gl_FragColor=
  vec4(color,1.0);

}
`;

function shader(type,src){

 const s=gl.createShader(type);

 gl.shaderSource(s,src);
 gl.compileShader(s);

 return s;
}

const program=
 gl.createProgram();

gl.attachShader(
 program,
 shader(gl.VERTEX_SHADER,VS)
);

gl.attachShader(
 program,
 shader(gl.FRAGMENT_SHADER,FS)
);

gl.linkProgram(program);

gl.useProgram(program);

const position=
 gl.getAttribLocation(
  program,
  "position"
);

const matrix=
 gl.getUniformLocation(
  program,
  "matrix"
);

const color=
 gl.getUniformLocation(
  program,
  "color"
);


/* =====================================================
 CUBE
===================================================== */

const vertices=
 new Float32Array([

  -1,-1,-1,
   1,-1,-1,
   1, 1,-1,
  -1, 1,-1,

  -1,-1, 1,
   1,-1, 1,
   1, 1, 1,
  -1, 1, 1

]);

const indices=
 new Uint16Array([

  0,1,2,
  0,2,3,

  4,6,5,
  4,7,6,

  0,4,5,
  0,5,1,

  3,2,6,
  3,6,7,

  1,5,6,
  1,6,2,

  0,3,7,
  0,7,4

]);

const vb=gl.createBuffer();

gl.bindBuffer(
 gl.ARRAY_BUFFER,
 vb
);

gl.bufferData(
 gl.ARRAY_BUFFER,
 vertices,
 gl.STATIC_DRAW
);

const ib=gl.createBuffer();

gl.bindBuffer(
 gl.ELEMENT_ARRAY_BUFFER,
 ib
);

gl.bufferData(
 gl.ELEMENT_ARRAY_BUFFER,
 indices,
 gl.STATIC_DRAW
);

gl.enableVertexAttribArray(
 position
);

gl.bindBuffer(
 gl.ARRAY_BUFFER,
 vb
);

gl.vertexAttribPointer(
 position,
 3,
 gl.FLOAT,
 false,
 0,
 0
);


/* =====================================================
 MATRIX
===================================================== */

function mul(a,b){

 const r=new Array(16);

 for(let i=0;i<4;i++){

  for(let j=0;j<4;j++){

   r[i*4+j]=
    a[i*4+0]*b[j]+
    a[i*4+1]*b[j+4]+
    a[i*4+2]*b[j+8]+
    a[i*4+3]*b[j+12];

  }

 }

 return r;
}

function perspective(
 fov,
 aspect,
 near,
 far
){

 const f=
  1/Math.tan(fov/2);

 return [

  f/aspect,0,0,0,

  0,f,0,0,

  0,0,
  (far+near)/(near-far),
  -1,

  0,0,
  (2*far*near)/(near-far),
  0

 ];
}

function rotX(a){

 const c=Math.cos(a);
 const s=Math.sin(a);

 return [

  1,0,0,0,

  0,c,s,0,

  0,-s,c,0,

  0,0,0,1

 ];
}

function rotY(a){

 const c=Math.cos(a);
 const s=Math.sin(a);

 return [

  c,0,-s,0,

  0,1,0,0,

  s,0,c,0,

  0,0,0,1

 ];
}

function translate(x,y,z){

 return [

  1,0,0,0,

  0,1,0,0,

  0,0,1,0,

  x,y,z,1

 ];
}


/* =====================================================
 GAME STATE
===================================================== */

let currentLevel=0;

let objects=[];
let lights=[];
let keys=[];

let exitDoor=null;
let exitOpen=false;

let holyWaters=[];

const player={

 x:8,
 y:2,
 z:10,

 yaw:0,
 pitch:0,

 speed:.14

};

let started=false;

let running=false;

let stamina=100;

let sanity=100;


/* =====================================================
 دو هیولا
===================================================== */

let monsters=[];


/* =====================================================
 RESET WORLD
===================================================== */

function resetWorld(){

 objects=[];
 lights=[];
 keys=[];
 holyWaters=[];

 exitOpen=false;

 monsters=[];

 player.x=8;
 player.y=2;
 player.z=10;

 player.yaw=0;
 player.pitch=0;

 stamina=100;
 sanity=100;

 document.getElementById(
  "waterCount"
 ).textContent="0";

 document.getElementById(
  "warning"
 ).style.opacity="0";

 if(currentLevel===0){

  createLevel0();

 }

 if(currentLevel===1){

  createParking(false);

 }

 if(currentLevel===2){

  createParking(true);

 }

 updateUI();

 document.getElementById(
  "keyStatus"
 ).textContent=
  "0 / 4 KEYS";

}


/* =====================================================
 BASIC WORLD
===================================================== */

function wall(x,z,w,d){

 objects.push({

  x:x,
  y:3.5,
  z:z,

  w:w,
  h:7,
  d:d,

  type:"wall"

 });

}

function floorWorld(size){

 objects.push({

  x:size/2,
  y:-.5,
  z:size/2,

  w:size,
  h:1,
  d:size,

  type:"floor"

 });

 objects.push({

  x:size/2,
  y:8,
  z:size/2,

  w:size,
  h:.5,
  d:size,

  type:"ceiling"

 });

 wall(
  size/2,
  0,
  size,
  1
 );

 wall(
  size/2,
  size,
  size,
  1
 );

 wall(
  0,
  size/2,
  1,
  size
 );

 wall(
  size,
  size/2,
  1,
  size
 );

}


/* =====================================================
 LEVEL 0
===================================================== */

function createLevel0(){

 const WORLD=420;

 floorWorld(WORLD);

 const walls=[

  [25,20,45,1],
  [25,40,1,40],

  [48,58,46,1],
  [72,20,1,38],

  [92,45,40,1],
  [112,18,1,54],

  [138,30,48,1],
  [164,62,1,62],

  [22,86,42,1],
  [45,112,1,52],

  [68,95,50,1],
  [91,122,1,60],

  [119,86,44,1],
  [141,110,1,50],

  [171,94,52,1],

  [24,151,45,1],
  [51,174,1,45],

  [78,145,52,1],
  [103,185,1,65],

  [132,157,48,1],
  [158,178,1,54],

  [186,145,54,1],

  [26,222,50,1],
  [54,246,1,52],

  [82,214,44,1],
  [107,262,1,70],

  [137,225,50,1],
  [163,249,1,58],

  [191,216,55,1],

  [25,290,46,1],
  [49,320,1,58],

  [78,276,53,1],
  [102,338,1,70],

  [133,301,48,1],
  [159,326,1,60],

  [190,284,58,1],

  [24,366,48,1],
  [55,392,1,54],

  [86,354,52,1],
  [111,398,1,44],

  [140,372,55,1],
  [169,397,1,45],

  [205,351,50,1]

 ];

 for(const w of walls){

  wall(
   w[0],
   w[1],
   w[2],
   w[3]
  );

 }

 for(let i=0;i<130;i++){

  const x=
   10+Math.random()*400;

  const z=
   10+Math.random()*400;

  if(
   Math.hypot(
    x-8,
    z-10
   )<25
  )
   continue;

  if(Math.random()<.5){

   wall(
    x,z,
    6+Math.random()*18,
    .9
   );

  }else{

   wall(
    x,z,
    .9,
    6+Math.random()*18
   );

  }

 }

 wall(
  8,
  18,
  16,
  .9
 );

 exitDoor={

  x:400,
  y:3.5,
  z:390,

  w:4,
  h:7,
  d:1

 };

 for(let i=0;i<400;i++){

  lights.push({

   x:8+Math.random()*404,
   z:8+Math.random()*404,
   phase:Math.random()*100

  });

 }

}


/* =====================================================
 PARKING
===================================================== */

function createParking(experimental){

 const WORLD=300;

 floorWorld(WORLD);


/* ستون ها */

 for(let x=20;x<290;x+=30){

  for(let z=20;z<290;z+=30){

   objects.push({

    x:x,
    y:3.5,
    z:z,

    w:2.5,
    h:7,
    d:2.5,

    type:"pillar"

   });

  }

 }


/* دیوارها */

 for(let i=0;i<35;i++){

  const x=
   15+Math.random()*270;

  const z=
   15+Math.random()*270;

  if(
   Math.hypot(
    x-8,
    z-10
   )<30
  )
   continue;

  if(Math.random()<.5){

   wall(
    x,z,
    12,.9
   );

  }else{

   wall(
    x,z,
    .9,12
   );

  }

 }


/* ماشین ها */

 const carColors=[

  [.65,.05,.05],
  [.05,.20,.65],
  [.75,.65,.05],
  [.05,.55,.20]

 ];

 for(let i=0;i<25;i++){

  const x=
   20+Math.random()*255;

  const z=
   20+Math.random()*255;

  if(
   Math.hypot(
    x-8,
    z-10
   )<35
  )
   continue;

  const col=
   carColors[
    i%carColors.length
   ];

  objects.push({

   x:x,
   y:.9,
   z:z,

   w:5.5,
   h:1.8,
   d:10,

   type:"car",
   col:col

  });

  objects.push({

   x:x,
   y:2,
   z:z,

   w:4,
   h:.9,
   d:4,

   type:"carTop",
   col:col

  });

 }


/* اتاق وسط */

 const roomX=150;
 const roomZ=150;
 const roomW=28;
 const roomD=28;

 wall(
  roomX,
  roomZ-roomD/2,
  roomW,
  .9
 );

 wall(
  roomX,
  roomZ+roomD/2,
  roomW,
  .9
 );

 wall(
  roomX-roomW/2,
  roomZ,
  .9,
  roomD
 );

 wall(
  roomX+roomW/2,
  roomZ,
  .9,
  roomD
 );

 exitDoor={

  x:roomX,
  y:3.5,
  z:roomZ,

  w:5,
  h:7,
  d:1

 };


/* آسانسور */

 objects.push({

  x:55,
  y:3.5,
  z:55,

  w:10,
  h:7,
  d:2,

  type:"elevator"

 });


/* چراغ */

 for(let i=0;i<100;i++){

  lights.push({

   x:10+Math.random()*280,
   z:10+Math.random()*280,
   phase:Math.random()*100

  });

 }


/* چهار کلید */

 const spots=[];

 while(spots.length<4){

  const x=
   15+Math.random()*270;

  const z=
   15+Math.random()*270;

  if(
   Math.hypot(
    x-8,
    z-10
   )<35
  )
   continue;

  if(
   Math.hypot(
    x-roomX,
    z-roomZ
   )<20
  )
   continue;

  if(
   Math.hypot(
    x-55,
    z-55
   )<15
  )
   continue;

  let okay=true;

  for(const p of spots){

   if(
    Math.hypot(
     x-p.x,
     z-p.z
    )<25
   )
    okay=false;

  }

  if(okay)
   spots.push({x,z});

 }

 keys=
  spots.map((p,i)=>({

   x:p.x,
   y:1,
   z:p.z,

   collected:false,

   id:i

  }));


/* =====================================================
 LEVEL ??? — آب مقدس
===================================================== */

 if(experimental){

  /* دو هیولا */

  monsters=[

   {
    x:230,
    y:0,
    z:230,

    active:false,
    transformed:false,

    speed:.055,
    phase:Math.random()*20
   },

   {
    x:245,
    y:0,
    z:90,

    active:false,
    transformed:false,

    speed:.05,
    phase:Math.random()*20
   }

  ];


  /* آب های مقدس */

  const waterSpots=[

   {x:75,z:65},
   {x:180,z:75},
   {x:75,z:220},
   {x:220,z:180},
   {x:270,z:260}

  ];

  holyWaters=
   waterSpots.map((p,i)=>({

    x:p.x,
    y:1,
    z:p.z,

    collected:false,
    id:i

   }));

 }

}


/* =====================================================
 COLLISION
===================================================== */

function collision(nx,nz){

 const r=.55;

 for(const o of objects){

  if(
   o.type!=="wall" &&
   o.type!=="pillar" &&
   o.type!=="car" &&
   o.type!=="carTop"
  )
   continue;

  if(

   nx>
    o.x-o.w/2-r &&

   nx<
    o.x+o.w/2+r &&

   nz>
    o.z-o.d/2-r &&

   nz<
    o.z+o.d/2+r

  ){

   return true;

  }

 }


 if(
  exitDoor &&
  !exitOpen &&
  currentLevel!==1
 ){

  if(

   nx>
    exitDoor.x-exitDoor.w/2-r &&

   nx<
    exitDoor.x+exitDoor.w/2+r &&

   nz>
    exitDoor.z-exitDoor.d/2-r &&

   nz<
    exitDoor.z+exitDoor.d/2+r

  ){

   return true;

  }

 }

 return false;
}


/* =====================================================
 KEYS
===================================================== */

function updateKeys(){

 if(currentLevel===0)
  return;

 for(const k of keys){

  if(k.collected)
   continue;

  const d=
   Math.hypot(
    player.x-k.x,
    player.z-k.z
   );

  if(d<2.2){

   k.collected=true;

   const count=
    keys.filter(
     x=>x.collected
    ).length;

   document.getElementById(
    "keyStatus"
   ).textContent=
    count+" / 4 KEYS";

   if(count===4){

    exitOpen=true;

    showMessage(
     "🔓 ELEVATOR UNLOCKED!"
    );

   }else{

    showMessage(
     "🔑 KEY FOUND! "+count+" / 4"
    );

   }

  }

 }
}


/* =====================================================
 آب مقدس
===================================================== */

function updateHolyWater(){

 if(currentLevel!==2)
  return;

 for(const w of holyWaters){

  if(w.collected)
   continue;

  const d=
   Math.hypot(
    player.x-w.x,
    player.z-w.z
   );

  if(d<2.5){

   w.collected=true;

   /*
    آب مقدس Sanity را زیاد می‌کند.
    سقف Sanity برابر 100 است.
   */

   sanity=
    Math.min(
     100,
     sanity+35
    );

   showMessage(
    "💧 HOLY WATER — SANITY RESTORED"
   );

  }

 }

 const collected=
  holyWaters.filter(
   w=>w.collected
  ).length;

 document.getElementById(
  "waterCount"
 ).textContent=
  collected;

}


/* =====================================================
 EXIT
===================================================== */

function checkExit(){

 /* LEVEL 0 */

 if(currentLevel===0){

  if(!exitDoor)
   return;

  const d=
   Math.hypot(
    player.x-exitDoor.x,
    player.z-exitDoor.z
   );

  if(d<4){

   if(!exitOpen){

    openCode();

   }else{

    finishLevel();

   }

  }

  return;
 }


 /* ===================================================
    LEVEL 1 — آسانسور
 =================================================== */

 if(currentLevel===1){

  const elevatorX=55;
  const elevatorZ=55;

  const d=
   Math.hypot(
    player.x-elevatorX,
    player.z-elevatorZ
   );

  if(d<5){

   const count=
    keys.filter(
     k=>k.collected
    ).length;

   if(count<4){

    showMessage(
     "🔒 ELEVATOR LOCKED — FIND 4 KEYS"
    );

   }else{

    if(!window.elevatorGoing){

     window.elevatorGoing=true;

     showMessage(
      "🛗 ELEVATOR OPENING..."
     );

     setTimeout(()=>{

      if(currentLevel===1){

       window.elevatorGoing=false;

       finishLevel();

      }

     },900);

    }

   }

  }

  return;
 }


 /* LEVEL ??? */

 if(currentLevel===2){

  if(!exitDoor)
   return;

  const d=
   Math.hypot(
    player.x-exitDoor.x,
    player.z-exitDoor.z
   );

  if(d<4){

   const count=
    keys.filter(
     k=>k.collected
    ).length;

   if(count<4){

    openKeyPanel();

   }else{

    finishLevel();

   }

  }

 }

}


/* =====================================================
 FINISH LEVEL
===================================================== */

function finishLevel(){

 if(currentLevel===0){

  startLevel(1);

  showMessage(
   "➡️ LEVEL 1 UNLOCKED"
  );

  return;

 }

 if(currentLevel===1){

  startLevel(2);

  showMessage(
   "⚠ LEVEL ??? UNLOCKED"
  );

  return;

 }

 if(currentLevel===2){

  started=false;

  document.getElementById(
   "win"
  ).style.display="flex";

  document.getElementById(
   "winTitle"
  ).textContent=
   "🎉 YOU ESCAPED!";

  document.getElementById(
   "winText"
  ).textContent=
   "LEVEL ??? COMPLETE";

 }

}


/* =====================================================
 MONSTERS
===================================================== */

function updateMonster(){

 if(currentLevel!==2)
  return;

 if(!monsters.length)
  return;

 let closest=9999;

 for(const monster of monsters){

  const dx=
   player.x-monster.x;

  const dz=
   player.z-monster.z;

  const distance=
   Math.hypot(dx,dz);

  closest=
   Math.min(
    closest,
    distance
   );


  /*
   وقتی هیولا دور است:
   شبیه یک انسان معمولی باقی می‌ماند.

   وقتی نزدیک می‌شود:
   تبدیل می‌شود.
  */

  if(distance<30){

   monster.active=true;

  }


  if(distance<18){

   monster.transformed=true;

  }


  /*
   هیولا فقط وقتی فعال شده باشد حرکت می‌کند.
  */

  if(
   monster.active &&
   distance>1.5
  ){

   const vx=
    dx/distance*
    monster.speed;

   const vz=
    dz/distance*
    monster.speed;

   const nx=
    monster.x+vx;

   const nz=
    monster.z+vz;

   if(
    !collision(
     nx,
     monster.z
    )
   ){

    monster.x=nx;

   }

   if(
    !collision(
     monster.x,
     nz
    )
   ){

    monster.z=nz;

   }

  }


  /*
   برخورد
  */

  if(distance<2){

   player.x=8;
   player.z=10;

   sanity=
    Math.max(
     0,
     sanity-20
    );

   monster.active=false;
   monster.transformed=false;

   showMessage(
    "⚠ THE CREATURE FOUND YOU"
   );

  }

 }


 /* ===================================================
    SANITY
 =================================================== */

 if(closest<30){

  sanity-=.08;

 }else{

  sanity+=.025;

 }

 sanity=
  Math.max(
   0,
   Math.min(
    100,
    sanity
   )
  );


 if(sanity<25){

  document.getElementById(
   "warning"
  ).textContent=
   "⚠ SANITY VERY LOW";

 }else{

  document.getElementById(
   "warning"
  ).textContent=
   closest<30
    ? "⚠ SOMETHING IS NEAR"
    : "⚠ SOMETHING IS NEAR";

 }

}


/* =====================================================
 RUN
===================================================== */

function updateRun(){

 if(currentLevel!==2)
  return;

 if(
  running &&
  stamina>0
 ){

  player.speed=.27;

  stamina-=.65;

 }else{

  running=false;

  player.speed=.14;

  if(stamina<100)
   stamina+=.25;

 }

 stamina=
  Math.max(
   0,
   Math.min(
    100,
    stamina
   )
  );

 document.getElementById(
  "stamina"
 ).style.width=
  stamina+"%";

}


/* =====================================================
 JOYSTICK
===================================================== */

const joy=
 document.getElementById("joy");

const stick=
 document.getElementById("stick");

let joyActive=false;

let jx=0;
let jy=0;

function joyMove(e){

 const r=
  joy.getBoundingClientRect();

 let x=
  e.clientX-
  (r.left+r.width/2);

 let y=
  e.clientY-
  (r.top+r.height/2);

 const max=48;

 const len=
  Math.hypot(x,y);

 if(len>max){

  x=x/len*max;
  y=y/len*max;

 }

 jx=x/max;
 jy=y/max;

 stick.style.transform=
  `translate(${x}px,${y}px)`;

}

joy.addEventListener(
 "pointerdown",
 e=>{

  joyActive=true;

  joy.setPointerCapture(
   e.pointerId
  );

  joyMove(e);

 }
);

joy.addEventListener(
 "pointermove",
 e=>{

  if(joyActive)
   joyMove(e);

 }
);

function joyReset(){

 joyActive=false;

 jx=0;
 jy=0;

 stick.style.transform=
  "translate(0,0)";

}

joy.addEventListener(
 "pointerup",
 joyReset
);

joy.addEventListener(
 "pointercancel",
 joyReset
);


/* =====================================================
 LOOK
===================================================== */

const look=
 document.getElementById("look");

let looking=false;

let lastX=0;
let lastY=0;

look.addEventListener(
 "pointerdown",
 e=>{

  looking=true;

  lastX=e.clientX;
  lastY=e.clientY;

  look.setPointerCapture(
   e.pointerId
  );

 }
);

look.addEventListener(
 "pointermove",
 e=>{

  if(!looking)
   return;

  const dx=
   e.clientX-lastX;

  const dy=
   e.clientY-lastY;

  lastX=e.clientX;
  lastY=e.clientY;

  player.yaw-=
   dx*.004;

  player.pitch-=
   dy*.004;

  player.pitch=
   Math.max(
    -1.25,
    Math.min(
     1.25,
     player.pitch
    )
   );

 }
);

look.addEventListener(
 "pointerup",
 ()=>{
  looking=false;
 }
);

look.addEventListener(
 "pointercancel",
 ()=>{
  looking=false;
 }
);


/* =====================================================
 RUN BUTTON
===================================================== */

const runButton=
 document.getElementById(
  "runButton"
 );

runButton.addEventListener(
 "pointerdown",
 ()=>{
  if(currentLevel===2)
   running=true;
 }
);

runButton.addEventListener(
 "pointerup",
 ()=>{
  running=false;
 }
);

runButton.addEventListener(
 "pointercancel",
 ()=>{
  running=false;
 }
);


/* =====================================================
 SAVE
===================================================== */

function saveGame(){

 const data={

  level:currentLevel,

  x:player.x,
  y:player.y,
  z:player.z,

  yaw:player.yaw,
  pitch:player.pitch,

  keys:
   keys.map(
    k=>k.collected
   ),

  holyWaters:
   holyWaters.map(
    w=>w.collected
   ),

  sanity:sanity,

  stamina:stamina,

  exitOpen:exitOpen

 };

 localStorage.setItem(
  "backroomsSave",
  JSON.stringify(data)
 );

 showMessage(
  "💾 GAME SAVED"
 );

}

document.getElementById(
 "saveButton"
).onclick=
 saveGame;


/* =====================================================
 CONTINUE
===================================================== */

function continueGame(){

 const raw=
  localStorage.getItem(
   "backroomsSave"
  );

 if(!raw){

  showMessage(
   "❌ NO SAVE FOUND"
  );

  return;

 }

 try{

  const data=
   JSON.parse(raw);

  startLevel(
   data.level
  );

  player.x=data.x;
  player.y=data.y;
  player.z=data.z;

  player.yaw=data.yaw;
  player.pitch=data.pitch;

  sanity=
   data.sanity||100;

  stamina=
   data.stamina||100;

  exitOpen=
   data.exitOpen||false;


  if(
   data.keys &&
   keys.length
  ){

   for(
    let i=0;
    i<keys.length;
    i++
   ){

    keys[i].collected=
     !!data.keys[i];

   }

  }


  if(
   data.holyWaters &&
   holyWaters.length
  ){

   for(
    let i=0;
    i<holyWaters.length;
    i++
   ){

    holyWaters[i].collected=
     !!data.holyWaters[i];

   }

  }


  const count=
   keys.filter(
    k=>k.collected
   ).length;

  const waterCount=
   holyWaters.filter(
    w=>w.collected
   ).length;

  document.getElementById(
   "keyStatus"
  ).textContent=
   count+" / 4 KEYS";

  document.getElementById(
   "waterCount"
  ).textContent=
   waterCount;

  updateUI();

  showMessage(
   "💾 SAVE LOADED"
  );

 }catch(e){

  showMessage(
   "❌ SAVE ERROR"
  );

 }

}

document.getElementById(
 "continueBtn"
).onclick=
 continueGame;


/* =====================================================
 UI
===================================================== */

function updateUI(){

 const hud=
  document.getElementById(
   "hud"
  );

 if(currentLevel===0){

  hud.textContent=
   "LEVEL 0";

 }

 if(currentLevel===1){

  hud.textContent=
   "LEVEL 1 — PARKING";

 }

 if(currentLevel===2){

  hud.textContent=
   "LEVEL ??? — EXPERIMENTAL";

 }

 const special=
  currentLevel===2;

 document.getElementById(
  "sanityBox"
 ).style.display=
  special
   ?"block"
   :"none";

 document.getElementById(
  "staminaBox"
 ).style.display=
  special
   ?"block"
   :"none";

 document.getElementById(
  "waterBox"
 ).style.display=
  special
   ?"block"
   :"none";

 runButton.style.display=
  special
   ?"block"
   :"none";

 saveButton.style.display=
  started
   ?"block"
   :"none";

}


/* =====================================================
 MESSAGE
===================================================== */

const message=
 document.getElementById(
  "message"
 );

function showMessage(text){

 message.textContent=
  text;

 message.style.display=
  "block";

 clearTimeout(
  showMessage.timer
 );

 showMessage.timer=
  setTimeout(
   ()=>{

    message.style.display=
     "none";

   },
   2200
  );

}


/* =====================================================
 KEY PANEL
===================================================== */

function openKeyPanel(){

 const count=
  keys.filter(
   k=>k.collected
  ).length;

 document.getElementById(
  "keyStatus"
 ).textContent=
  count+" / 4 KEYS";

 document.getElementById(
  "keyPanel"
 ).style.display=
  "block";

}

function closeKeyPanel(){

 document.getElementById(
  "keyPanel"
 ).style.display=
  "none";

}


/* =====================================================
 LEVEL START
===================================================== */

function startLevel(level){

 currentLevel=level;

 started=true;

 window.elevatorGoing=false;

 document.getElementById(
  "menu"
 ).style.display=
  "none";

 document.getElementById(
  "levelMenu"
 ).style.display=
  "none";

 document.getElementById(
  "win"
 ).style.display=
  "none";

 resetWorld();

 updateUI();

 startAudio();


 if(currentLevel===0){

  showMessage(
   "🟨 FIND THE EXIT"
  );

 }

 if(currentLevel===1){

  showMessage(
   "🅿️ FIND 4 KEYS — WATCH OUT"
  );

 }

 if(currentLevel===2){

  showMessage(
   "❓ FIND 4 KEYS • FIND HOLY WATER"
  );

 }

}


/* =====================================================
 START MENU
===================================================== */

document.getElementById(
 "start"
).onclick=
 ()=>{

  document.getElementById(
   "menu"
  ).style.display=
   "none";

  document.getElementById(
   "levelMenu"
  ).style.display=
   "flex";

 };


function backToMenu(){

 started=false;

 document.getElementById(
  "win"
 ).style.display=
  "none";

 document.getElementById(
  "menu"
 ).style.display=
  "flex";

}


/* =====================================================
 CODE SYSTEM
===================================================== */

const codeBox=
 document.getElementById(
  "codeBox"
 );

function openCode(){

 codeBox.style.display=
  "flex";

}

function closeCode(){

 codeBox.style.display=
  "none";

}

document.getElementById(
 "closeCode"
).onclick=
 closeCode;


function elevatorColor(c){

 if(c==="red"){

  closeCode();

  exitOpen=true;

  showMessage(
   "🔓 ELEVATOR OPEN"
  );

 }else{

  showMessage(
   "❌ WRONG COLOR"
  );

 }

}


/* =====================================================
 AUDIO
===================================================== */

let audioStarted=false;

let audioContext=null;
let humGain=null;
let humOsc=null;
let humOsc2=null;

function startAudio(){

 if(audioStarted)
  return;

 audioStarted=true;

 try{

  audioContext=
   new(
    window.AudioContext||
    window.webkitAudioContext
   )();

  humOsc=
   audioContext.createOscillator();

  humOsc2=
   audioContext.createOscillator();

  humGain=
   audioContext.createGain();

  humOsc.type=
   "sawtooth";

  humOsc.frequency.value=
   58;

  humOsc2.type=
   "square";

  humOsc2.frequency.value=
   116;

  humGain.gain.value=
   .012;

  humOsc.connect(
   humGain
  );

  humOsc2.connect(
   humGain
  );

  humGain.connect(
   audioContext.destination
  );

  humOsc.start();
  humOsc2.start();

 }catch(e){}

}


/* =====================================================
 UPDATE
===================================================== */

function update(){

 if(!started)
  return;


 /* حرکت */

 const forward=
  -jy*player.speed;

 const side=
  jx*player.speed;

 const s=
  Math.sin(player.yaw);

 const c=
  Math.cos(player.yaw);

 const nx=
  player.x+
  side*c+
  forward*s;

 const nz=
  player.z+
  side*s-
  forward*c;

 if(
  !collision(
   nx,
   player.z
  )
 ){

  player.x=nx;

 }

 if(
  !collision(
   player.x,
   nz
  )
 ){

  player.z=nz;

 }


 /* سیستم ها */

 updateKeys();

 updateHolyWater();

 updateMonster();

 updateRun();

 checkExit();


 /* HUD */

 if(
  currentLevel===1 ||
  currentLevel===2
 ){

  if(currentLevel===2){

   document.getElementById(
    "sanity"
   ).style.width=
    sanity+"%";

  }


  if(
   currentLevel===2 &&
   monsters.length
  ){

   let closest=9999;

   for(
    const m of monsters
   ){

    closest=
     Math.min(
      closest,
      Math.hypot(
       player.x-m.x,
       player.z-m.z
      )
     );

   }

   if(
    monsters.some(
     m=>m.active
    )
   ){

    document.getElementById(
     "warning"
    ).style.opacity=
     closest<35
      ?"1"
      :"0";

   }else{

    document.getElementById(
     "warning"
    ).style.opacity=
     "0";

   }

  }

 }

}


/* =====================================================
 DRAW CUBE
===================================================== */

function drawCube(
 x,y,z,
 w,h,d,
 col,
 vp
){

 const model=[

  w/2,0,0,0,

  0,h/2,0,0,

  0,0,d/2,0,

  x,y,z,1

 ];

 const m=
  mul(
   model,
   vp
  );

 gl.uniformMatrix4fv(
  matrix,
  false,
  new Float32Array(m)
 );

 gl.uniform3fv(
  color,
  new Float32Array(col)
 );

 gl.drawElements(
  gl.TRIANGLES,
  indices.length,
  gl.UNSIGNED_SHORT,
  0
 );

}


/* =====================================================
 DRAW
===================================================== */

function draw(){

 gl.clearColor(
  .025,
  .022,
  .012,
  1
 );

 gl.clear(
  gl.COLOR_BUFFER_BIT|
  gl.DEPTH_BUFFER_BIT
 );

 gl.enable(
  gl.DEPTH_TEST
 );

 const aspect=
  canvas.width/
  canvas.height;

 const proj=
  perspective(
   Math.PI/3,
   aspect,
   .1,
   480
  );

 const rotation=
  mul(
   rotX(player.pitch),
   rotY(player.yaw)
  );

 const camera=
  translate(
   -player.x,
   -player.y,
   -player.z
  );

 const view=
  mul(
   camera,
   rotation
  );

 const vp=
  mul(
   view,
   proj
  );


/* =====================================================
 WORLD
===================================================== */

 for(const o of objects){

  let col;


  if(o.type==="floor"){

   if(currentLevel===0){

    col=[
     .48,
     .42,
     .18
    ];

   }else{

    col=[
     .16,
     .16,
     .15
    ];

   }

  }


  else if(o.type==="ceiling"){

   col=
    currentLevel===0
     ? [.42,.38,.23]
     : [.12,.12,.12];

  }


  else if(o.type==="pillar"){

   col=[
    .28,
    .28,
    .27
   ];

  }


  else if(
   o.type==="car" ||
   o.type==="carTop"
  ){

   col=o.col;

  }


  /* =================================================
     آسانسور
  ================================================= */

  else if(
   o.type==="elevator"
  ){

   /* بدنه */

   drawCube(
    o.x,
    o.y,
    o.z,
    o.w,
    o.h,
    o.d,
    [.22,.23,.25],
    vp
   );


   /* قاب بالا */

   drawCube(
    o.x,
    7.05,
    o.z-1.08,
    10.8,
    .45,
    .35,
    [.08,.08,.09],
    vp
   );


   /* ستون چپ */

   drawCube(
    o.x-5.1,
    3.5,
    o.z-1.08,
    .55,
    7.4,
    .4,
    [.08,.08,.09],
    vp
   );


   /* ستون راست */

   drawCube(
    o.x+5.1,
    3.5,
    o.z-1.08,
    .55,
    7.4,
    .4,
    [.08,.08,.09],
    vp
   );


   /* در چپ */

   drawCube(
    o.x-2.15,
    3.45,
    o.z-1.18,
    4.15,
    6.6,
    .18,
    [.12,.13,.14],
    vp
   );


   /* در راست */

   drawCube(
    o.x+2.15,
    3.45,
    o.z-1.18,
    4.15,
    6.6,
    .18,
    [.12,.13,.14],
    vp
   );


   /* خط وسط */

   drawCube(
    o.x,
    3.45,
    o.z-1.30,
    .12,
    6.55,
    .08,
    [.025,.025,.025],
    vp
   );


   /* چراغ */

   const ep=
    .75+
    Math.sin(
     Date.now()*.008
    )*.18;

   drawCube(
    o.x,
    6.55,
    o.z-1.38,
    1.7,
    .32,
    .08,
    [
     ep,
     ep*.75,
     .08
    ],
    vp
   );


   /* پنل */

   drawCube(
    o.x+5.45,
    3.65,
    o.z-1.18,
    .35,
    2.0,
    .22,
    [.07,.07,.075],
    vp
   );


   const bp=
    .55+
    Math.sin(
     Date.now()*.006
    )*.35;

   drawCube(
    o.x+5.45,
    4.35,
    o.z-1.34,
    .16,
    .16,
    .08,
    [bp,bp,bp],
    vp
   );


   /* تابلو */

   drawCube(
    o.x,
    7.55,
    o.z-1.15,
    3.8,
    .28,
    .12,
    [.45,.45,.42],
    vp
   );


   /* طبقه */

   drawCube(
    o.x,
    7.15,
    o.z-1.2,
    .65,
    .25,
    .08,
    [.7,.7,.65],
    vp
   );

   continue;

  }


  else{

   col=
    currentLevel===0
     ? [.64,.58,.35]
     : [.23,.23,.22];

  }


  drawCube(
   o.x,
   o.y,
   o.z,
   o.w,
   o.h,
   o.d,
   col,
   vp
  );

 }


/* =====================================================
 KEYS
===================================================== */

 if(currentLevel!==0){

  for(const k of keys){

   if(k.collected)
    continue;

   const pulse=
    .85+
    Math.sin(
     Date.now()*.006+k.id
    )*.15;

   drawCube(
    k.x,
    k.y,
    k.z,
    .35,
    .8,
    .15,
    [
     pulse,
     pulse*.75,
     .05
    ],
    vp
   );

  }

 }


/* =====================================================
 HOLY WATER
===================================================== */

 if(currentLevel===2){

  for(const w of holyWaters){

   if(w.collected)
    continue;

   const pulse=
    .65+
    Math.sin(
     Date.now()*.005+
     w.id
    )*.2;

   /*
    بطری آب مقدس
   */

   drawCube(
    w.x,
    w.y,
    w.z,
    .38,
    .8,
    .38,
    [
     .2,
     pulse,
     1
    ],
    vp
   );

   /*
    نور کوچک بالای بطری
   */

   drawCube(
    w.x,
    w.y+.55,
    w.z,
    .18,
    .18,
    .18,
    [
     .5,
     .8,
     1
    ],
    vp
   );

  }

 }


/* =====================================================
 EXIT
===================================================== */

 if(
  exitDoor &&
  !exitOpen &&
  currentLevel!==1
 ){

  drawCube(
   exitDoor.x,
   exitDoor.y,
   exitDoor.z,
   exitDoor.w,
   exitDoor.h,
   exitDoor.d,
   [.10,.10,.10],
   vp
  );

 }

 if(
  exitDoor &&
  currentLevel!==1
 ){

  drawCube(
   exitDoor.x,
   6.7,
   exitDoor.z-.7,
   3.5,
   .55,
   .12,
   [.05,.55,.08],
   vp
  );

 }


/* =====================================================
 LIGHTS
===================================================== */

 for(const l of lights){

  const f=
   .78+
   Math.sin(
    Date.now()*.005+
    l.phase
   )*.05+
   Math.sin(
    Date.now()*.019+
    l.phase
   )*.025;

  let lc;

  if(currentLevel===0){

   lc=[
    f,
    f*.92,
    f*.68
   ];

  }else{

   lc=[
    f*.8,
    f*.8,
    f*.8
   ];

  }

  drawCube(
   l.x,
   7.55,
   l.z,
   1.8,
   .12,
   .55,
   lc,
   vp
  );

 }


/* =====================================================
 MONSTERS
===================================================== */

 if(currentLevel===2){

  for(const monster of monsters){

   /*
    فاصله هیولا تا بازیکن
   */

   const md=
    Math.hypot(
     player.x-monster.x,
     player.z-monster.z
    );


   /*
    حالت انسان
   */

   if(
    !monster.transformed
   ){

    const humanWobble=
     Math.sin(
      Date.now()*.004+
      monster.phase
     )*.04;


    /* بدن انسان */

    drawCube(
     monster.x,
     2.3+humanWobble,
     monster.z,
     .75,
     2.8,
     .55,
     [.42,.42,.42],
     vp
    );


    /* سر انسان */

    drawCube(
     monster.x,
     4.15+humanWobble,
     monster.z,
     .85,
     .85,
     .8,
     [.62,.62,.58],
     vp
    );


    /* لباس */

    drawCube(
     monster.x,
     2.8+humanWobble,
     monster.z-.03,
     .82,
     1.7,
     .58,
     [.12,.14,.16],
     vp
    );

   }


   /*
    تبدیل به موجود
   */

   if(
    monster.transformed
   ){

    const wobble=
     Math.sin(
      Date.now()*.006+
      monster.phase
     )*.08;


    /*
     بدن بلند خاکستری
    */

    drawCube(
     monster.x,
     3.0+wobble,
     monster.z,
     1.05,
     5.8,
     .9,
     [.27,.28,.29],
     vp
    );


    /*
     سر
    */

    drawCube(
     monster.x,
     6.35+wobble,
     monster.z,
     1.45,
     1.15,
     1.2,
     [.32,.33,.34],
     vp
    );


    /*
     دست چپ
    */

    drawCube(
     monster.x-1.0,
     3.2+wobble,
     monster.z,
     .35,
     4.8,
     .35,
     [.25,.26,.27],
     vp
    );


    /*
     دست راست
    */

    drawCube(
     monster.x+1.0,
     3.2+wobble,
     monster.z,
     .35,
     4.8,
     .35,
     [.25,.26,.27],
     vp
    );


    /*
     چشم چپ سفید
    */

    drawCube(
     monster.x-.3,
     6.45+wobble,
     monster.z-.62,
     .2,
     .2,
     .08,
     [1,1,1],
     vp
    );


    /*
     چشم راست سفید
    */

    drawCube(
     monster.x+.3,
     6.45+wobble,
     monster.z-.62,
     .2,
     .2,
     .08,
     [1,1,1],
     vp
    );


    /*
     دهان عمودی
     قرمز، ولی غیرگرافیکی
    */

    drawCube(
     monster.x,
     6.0+wobble,
     monster.z-.64,
     .24,
     .65,
     .08,
     [.75,.02,.02],
     vp
    );


    /*
     هاله قرمز خیلی کم اطراف دهان
    */

    if(md<25){

     drawCube(
      monster.x,
      6.0+wobble,
      monster.z-.67,
      .34,
      .76,
      .04,
      [.35,.01,.01],
      vp
     );

    }

   }

  }

 }

}


/* =====================================================
 LOOP
===================================================== */

let lastFrame=0;

function loop(time){

 if(!lastFrame)
  lastFrame=time;

 if(
  time-lastFrame>=1000/60
 ){

  lastFrame=time;

  update();

  draw();

 }

 requestAnimationFrame(
  loop
 );

}

requestAnimationFrame(
 loop
);

</script>

</body>
</html>

Game Source: BACKROOMS - LEVEL 0 / 1 / ???

Creator: NovaWave22

Libraries: none

Complexity: complex (3746 lines, 40.9 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: backrooms-level-0-1-novawave22" to link back to the original. Then publish at arcadelab.ai/publish.