BACKROOMS - LEVEL 0 / 1 / ???
by NovaWave223746 lines40.9 KB
<!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.