NIGHT SHIFT — Security Room
by LunarPanda235109 lines75.3 KB
<!DOCTYPE html>
<html lang="he" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>NIGHT SHIFT — Security Room</title>
<style>
/* =========================================================
GLOBAL
========================================================= */
*{
box-sizing:border-box;
-webkit-tap-highlight-color:transparent;
}
html,body{
margin:0;
width:100%;
height:100%;
overflow:hidden;
background:#050708;
color:#e7eeee;
font-family:Arial,Helvetica,sans-serif;
}
body{
user-select:none;
}
button{
font-family:Arial,Helvetica,sans-serif;
color:#e5eeee;
border:0;
outline:0;
cursor:pointer;
touch-action:manipulation;
}
#game{
position:relative;
width:100%;
height:100dvh;
min-height:430px;
overflow:hidden;
background:#080c0d;
}
/* =========================================================
TOP HUD
========================================================= */
#topHUD{
position:absolute;
z-index:20;
top:0;
left:0;
width:100%;
height:64px;
display:flex;
align-items:center;
justify-content:space-between;
direction:ltr;
padding:7px 13px;
background:
linear-gradient(
180deg,
#101617 0%,
#090d0e 100%
);
border-bottom:2px solid #4e5d5e;
box-shadow:
0 5px 18px #000a;
}
.hudLeft{
text-align:left;
}
.hudTitle{
font-size:9px;
letter-spacing:2px;
color:#829191;
}
#time{
margin-top:2px;
font-size:24px;
font-weight:bold;
letter-spacing:1px;
}
#system{
font-size:8px;
letter-spacing:1px;
color:#9fb3aa;
}
.hudRight{
width:130px;
text-align:right;
}
.nightText{
font-size:8px;
color:#879696;
letter-spacing:1.5px;
}
#powerText{
font-size:20px;
font-weight:bold;
}
.powerOuter{
width:125px;
height:5px;
background:#263031;
border:1px solid #4b595a;
}
#powerBar{
width:100%;
height:100%;
background:#b9d4bb;
transition:width .2s;
}
/* =========================================================
MAIN VIEW
========================================================= */
#screen{
position:absolute;
top:64px;
left:0;
width:100%;
height:calc(100% - 64px);
overflow:hidden;
}
canvas{
display:block;
width:100%;
height:100%;
}
/* =========================================================
OFFICE
========================================================= */
#officeLayer{
position:absolute;
inset:0;
z-index:2;
}
#officeCanvas{
position:absolute;
inset:0;
}
/* =========================================================
PHYSICAL DOOR CONTROLS
========================================================= */
.doorPanel{
position:absolute;
z-index:10;
top:36%;
width:78px;
padding:5px;
background:
linear-gradient(
180deg,
#151d1e,
#0a0f10
);
border:1px solid #647374;
box-shadow:
0 5px 18px #000b;
}
#leftPanel{
left:6px;
}
#rightPanel{
right:6px;
}
.panelLabel{
text-align:center;
font-size:8px;
color:#8e9d9d;
letter-spacing:1px;
margin-bottom:4px;
}
.controlButton{
width:100%;
min-height:39px;
margin-bottom:5px;
background:#1a2425;
border:1px solid #697879;
font-size:9px;
font-weight:bold;
transition:
background .12s,
border .12s,
transform .08s;
}
.controlButton:last-child{
margin-bottom:0;
}
.controlButton:active{
transform:scale(.96);
}
.controlButton.doorClosed{
background:#642c2e;
border-color:#db6666;
}
.controlButton.lightOn{
background:#d0d7bb;
color:#101514;
border-color:#e9efd6;
}
/* =========================================================
CAMERA OPEN BUTTON
========================================================= */
#cameraButton{
position:absolute;
z-index:12;
left:50%;
bottom:14px;
transform:translateX(-50%);
width:min(230px,48%);
height:47px;
background:
linear-gradient(
180deg,
#263334,
#121a1b
);
border:2px solid #718081;
box-shadow:
0 4px 15px #000c;
font-size:11px;
font-weight:bold;
letter-spacing:1px;
}
#cameraButton:active{
transform:translateX(-50%) scale(.97);
}
/* =========================================================
CAMERA SYSTEM
========================================================= */
#cameraLayer{
position:absolute;
z-index:30;
inset:0;
display:none;
background:#070b0c;
}
#cameraCanvas{
position:absolute;
inset:0;
}
/* camera top information */
#cameraHeader{
position:absolute;
z-index:4;
left:8px;
top:8px;
display:flex;
gap:7px;
direction:ltr;
}
.cameraBadge{
min-width:120px;
padding:7px 9px;
background:#060a0bf2;
border:1px solid #667677;
box-shadow:0 4px 12px #0009;
}
.cameraBadge strong{
display:block;
font-size:10px;
letter-spacing:1px;
}
.cameraBadge span{
display:block;
margin-top:2px;
font-size:8px;
color:#98a8a8;
}
/* =========================================================
CAMERA MAP
========================================================= */
#cameraMap{
position:absolute;
z-index:8;
top:8px;
right:8px;
width:min(175px,39%);
padding:7px;
background:#070c0df2;
border:1px solid #667677;
box-shadow:0 5px 20px #000c;
}
.mapTitle{
font-size:9px;
letter-spacing:1.7px;
color:#aebbbb;
margin-bottom:6px;
}
#mapGrid{
display:grid;
grid-template-columns:1fr 1fr;
gap:4px;
}
.mapButton{
min-height:36px;
background:#182223;
border:1px solid #566667;
padding:3px;
font-size:8px;
line-height:1.15;
white-space:pre-line;
transition:.12s;
}
.mapButton.active{
border-color:#e2ece7;
box-shadow:0 0 0 2px #dce6e222;
}
.mapButton.enemy{
background:#321b1d;
border-color:#ba5558;
}
.mapButton.you{
grid-column:1 / 3;
background:#1b3028;
border-color:#79aa91;
color:#d7f0df;
font-weight:bold;
}
/* =========================================================
CAMERA CLOSE
========================================================= */
#closeCamera{
position:absolute;
z-index:10;
left:8px;
bottom:73px;
height:37px;
padding:0 13px;
background:#351b1d;
border:1px solid #ad5a5d;
font-size:9px;
font-weight:bold;
}
/* =========================================================
CAMERA NAVIGATION
========================================================= */
#cameraBar{
position:absolute;
z-index:9;
left:0;
right:0;
bottom:0;
height:63px;
display:flex;
align-items:center;
gap:8px;
padding:6px;
direction:ltr;
background:
linear-gradient(
180deg,
#172122,
#0b1011
);
border-top:2px solid #627273;
box-shadow:0 -5px 18px #000c;
}
.cameraArrow{
flex:0 0 47px;
height:45px;
background:#263334;
border:1px solid #728181;
font-size:20px;
}
.cameraArrow:active{
transform:scale(.94);
}
#cameraInfo{
flex:1;
min-width:0;
text-align:center;
}
#cameraName{
font-size:11px;
font-weight:bold;
letter-spacing:1px;
}
#cameraDescription{
margin-top:3px;
color:#98a7a7;
font-size:8px;
}
#cameraIndex{
margin-top:2px;
color:#718080;
font-size:8px;
}
/* =========================================================
STATIC / CAMERA EFFECTS
========================================================= */
#staticOverlay{
pointer-events:none;
position:absolute;
z-index:6;
inset:0;
opacity:0;
background:
repeating-linear-gradient(
0deg,
#ffffff08 0px,
#ffffff08 1px,
transparent 1px,
transparent 4px
);
mix-blend-mode:screen;
}
#vignette{
pointer-events:none;
position:absolute;
inset:0;
z-index:5;
background:
radial-gradient(
ellipse at center,
transparent 30%,
#000b 100%
);
}
/* =========================================================
JUMPSCARE
========================================================= */
#jumpscare{
position:absolute;
z-index:100;
inset:0;
display:none;
background:#090000;
}
#scareCanvas{
width:100%;
height:100%;
}
#scareFlash{
position:absolute;
inset:0;
background:#fff;
opacity:0;
pointer-events:none;
}
/* =========================================================
START / GAME OVER
========================================================= */
#menu{
position:absolute;
z-index:200;
inset:0;
display:flex;
align-items:center;
justify-content:center;
padding:18px;
background:
radial-gradient(
circle at center,
#182122,
#050708 72%
);
}
.menuBox{
width:min(430px,92vw);
padding:27px 22px;
text-align:center;
background:
linear-gradient(
145deg,
#172021,
#080b0c
);
border:1px solid #667576;
box-shadow:
0 15px 60px #000;
direction:rtl;
}
.menuBox h1{
margin:0;
font-size:clamp(28px,8vw,46px);
letter-spacing:2px;
}
.menuBox h2{
margin:5px 0 18px;
font-size:10px;
color:#8d9b9b;
letter-spacing:3px;
}
.menuLine{
width:100%;
height:1px;
background:#536263;
margin:14px 0;
}
.menuText{
font-size:12px;
line-height:1.8;
color:#b8c3c3;
}
#startButton{
margin-top:14px;
width:100%;
height:49px;
background:#dce7df;
color:#101515;
font-weight:bold;
border:0;
box-shadow:0 4px 16px #000;
}
#startButton:active{
transform:scale(.98);
}
/* =========================================================
SMALL STATUS MESSAGE
========================================================= */
#message{
position:absolute;
z-index:50;
left:50%;
top:76px;
transform:translateX(-50%);
padding:7px 12px;
background:#080d0ef0;
border:1px solid #526162;
font-size:9px;
opacity:0;
pointer-events:none;
transition:opacity .2s;
white-space:nowrap;
}
/* =========================================================
MOBILE
========================================================= */
@media(max-width:600px){
#topHUD{
height:58px;
}
#screen{
top:58px;
height:calc(100% - 58px);
}
.hudRight{
width:105px;
}
.powerOuter{
width:100px;
}
.doorPanel{
width:70px;
top:35%;
}
.controlButton{
min-height:36px;
font-size:8px;
}
#cameraMap{
width:145px;
}
.mapButton{
min-height:33px;
font-size:7px;
}
#cameraBar{
height:59px;
}
.cameraArrow{
flex-basis:42px;
height:42px;
}
#closeCamera{
bottom:67px;
}
}
/* =========================================================
LANDSCAPE PHONE
========================================================= */
@media(
orientation:landscape
) and (
max-height:520px
){
#topHUD{
height:45px;
}
#screen{
top:45px;
height:calc(100% - 45px);
}
.doorPanel{
top:30%;
width:78px;
}
.controlButton{
min-height:31px;
}
#cameraBar{
height:51px;
}
.cameraArrow{
height:37px;
}
#closeCamera{
bottom:57px;
}
}
/* =========================================================
REDUCED MOTION
========================================================= */
@media(prefers-reduced-motion:reduce){
*{
animation:none!important;
transition:none!important;
}
}
</style>
</head>
<body>
<div id="game">
<!-- ================= HUD ================= -->
<header id="topHUD">
<div class="hudLeft">
<div class="hudTitle">
NIGHT SHIFT / SECURITY NETWORK
</div>
<div id="time">
12 AM
</div>
<div id="system">
SYSTEM ONLINE
</div>
</div>
<div class="hudRight">
<div class="nightText">
NIGHT 01
</div>
<div id="powerText">
<span id="powerNumber">100</span>%
</div>
<div class="powerOuter">
<div id="powerBar"></div>
</div>
</div>
</header>
<!-- ================= GAME SCREEN ================= -->
<main id="screen">
<!-- OFFICE -->
<section id="officeLayer">
<canvas id="officeCanvas"></canvas>
<!-- LEFT DOOR CONTROLS -->
<div
class="doorPanel"
id="leftPanel"
>
<div class="panelLabel">
WEST CONTROL
</div>
<button
class="controlButton"
id="leftDoor"
>
CLOSE<br>DOOR
</button>
<button
class="controlButton"
id="leftLight"
>
LIGHT OFF
</button>
</div>
<!-- RIGHT DOOR CONTROLS -->
<div
class="doorPanel"
id="rightPanel"
>
<div class="panelLabel">
EAST CONTROL
</div>
<button
class="controlButton"
id="rightDoor"
>
CLOSE<br>DOOR
</button>
<button
class="controlButton"
id="rightLight"
>
LIGHT OFF
</button>
</div>
<!-- CAMERA MONITOR -->
<button id="cameraButton">
▣ OPEN CAMERA SYSTEM
</button>
</section>
<!-- ================= CAMERA SYSTEM ================= -->
<section id="cameraLayer">
<canvas id="cameraCanvas"></canvas>
<div id="staticOverlay"></div>
<div id="vignette"></div>
<div id="cameraHeader">
<div class="cameraBadge">
<strong id="cameraName">
CAM 01 / SHOWROOM
</strong>
<span id="cameraSignal">
● SIGNAL STABLE
</span>
</div>
<div class="cameraBadge">
<strong>
REMOTE MONITOR
</strong>
<span>
AUDIO / VIDEO ONLINE
</span>
</div>
</div>
<!-- MAP -->
<aside id="cameraMap">
<div class="mapTitle">
FACILITY MAP
</div>
<div id="mapGrid"></div>
</aside>
<!-- CLOSE CAMERAS -->
<button id="closeCamera">
✕ CLOSE CAMERAS
</button>
<!-- CAMERA NAVIGATION -->
<div id="cameraBar">
<button
class="cameraArrow"
id="previousCamera"
>
◀
</button>
<div id="cameraInfo">
<div id="cameraTitle">
CAM 01 / SHOWROOM
</div>
<div id="cameraDescription">
MAIN DISPLAY HALL
</div>
<div id="cameraIndex">
01 / 10
</div>
</div>
<button
class="cameraArrow"
id="nextCamera"
>
▶
</button>
</div>
</section>
<!-- MESSAGE -->
<div id="message"></div>
<!-- JUMPSCARE -->
<section id="jumpscare">
<canvas id="scareCanvas"></canvas>
<div id="scareFlash"></div>
</section>
<!-- MENU -->
<section id="menu">
<div class="menuBox">
<h1>NIGHT SHIFT</h1>
<h2>SECURITY FACILITY / 01</h2>
<div class="menuLine"></div>
<div class="menuText">
ארבעה יצורים מסתובבים במתקן בלילה.<br>
השתמש במצלמות כדי לעקוב אחריהם.<br>
בדוק את החלונות בעזרת האורות.<br>
סגור דלת רק כשבאמת צריך.<br>
החשמל מוגבל.
<br><br>
<b>
שרוד עד 6 AM.
</b>
</div>
<div class="menuLine"></div>
<button id="startButton">
START NIGHT
</button>
</div>
</section>
</main>
</div>
<script>
/* =========================================================
NIGHT SHIFT
COMPLETE SINGLE FILE GAME
========================================================= */
/* =========================================================
ELEMENTS
========================================================= */
const game = document.getElementById("game");
const screen = document.getElementById("screen");
const officeCanvas =
document.getElementById("officeCanvas");
const officeCtx =
officeCanvas.getContext("2d");
const cameraCanvas =
document.getElementById("cameraCanvas");
const cameraCtx =
cameraCanvas.getContext("2d");
const scareCanvas =
document.getElementById("scareCanvas");
const scareCtx =
scareCanvas.getContext("2d");
const timeElement =
document.getElementById("time");
const systemElement =
document.getElementById("system");
const powerNumber =
document.getElementById("powerNumber");
const powerBar =
document.getElementById("powerBar");
const cameraLayer =
document.getElementById("cameraLayer");
const officeLayer =
document.getElementById("officeLayer");
const cameraName =
document.getElementById("cameraName");
const cameraSignal =
document.getElementById("cameraSignal");
const cameraTitle =
document.getElementById("cameraTitle");
const cameraDescription =
document.getElementById("cameraDescription");
const cameraIndex =
document.getElementById("cameraIndex");
const mapGrid =
document.getElementById("mapGrid");
const message =
document.getElementById("message");
const staticOverlay =
document.getElementById("staticOverlay");
const jumpscare =
document.getElementById("jumpscare");
const scareFlash =
document.getElementById("scareFlash");
const menu =
document.getElementById("menu");
/* =========================================================
GAME STATE
========================================================= */
let gameRunning = false;
let gameOver = false;
let cameraOpen = false;
let currentCamera = 0;
let power = 100;
let gameSeconds = 0;
let lastTime = 0;
let messageTimer = 0;
let staticTimer = 0;
let scareFrame = 0;
let audioContext = null;
/* =========================================================
DOOR STATE
========================================================= */
let leftDoorClosed = false;
let rightDoorClosed = false;
let leftLightOn = false;
let rightLightOn = false;
/* =========================================================
CAMERA DATA
========================================================= */
const cameras = [
{
id:0,
name:"SHOWROOM",
description:"MAIN DISPLAY HALL",
type:"showroom"
},
{
id:1,
name:"STORAGE",
description:"SUPPLY STORAGE",
type:"storage"
},
{
id:2,
name:"WORKSHOP",
description:"REPAIR WORKSHOP",
type:"workshop"
},
{
id:3,
name:"DINING",
description:"OLD DINING AREA",
type:"dining"
},
{
id:4,
name:"WEST HALL",
description:"WESTERN CORRIDOR",
type:"hall"
},
{
id:5,
name:"EAST HALL",
description:"EASTERN CORRIDOR",
type:"hall"
},
{
id:6,
name:"BACK ROOM",
description:"RESTRICTED AREA",
type:"back"
},
{
id:7,
name:"PARTS ROOM",
description:"MECHANICAL STORAGE",
type:"parts"
},
{
id:8,
name:"STAGE",
description:"PERFORMANCE AREA",
type:"stage"
},
{
id:9,
name:"SERVICE",
description:"SERVICE CORRIDOR",
type:"service"
}
];
/* =========================================================
ENEMIES
========================================================= */
const enemies = [
{
name:"THE WARDEN",
room:0,
route:[
0,
1,
4,
"LEFT"
],
side:"LEFT",
moveTimer:0,
moveDelay:12,
dangerTimer:0,
color:"#8eaaa0",
eye:"#ff4747",
shape:"warden",
scale:1
},
{
name:"TIN GRIN",
room:3,
route:[
3,
7,
5,
"RIGHT"
],
side:"RIGHT",
moveTimer:0,
moveDelay:17,
dangerTimer:0,
color:"#b79a67",
eye:"#fff29a",
shape:"grin",
scale:.96
},
{
name:"THE HOLLOW",
room:1,
route:[
1,
2,
6,
4,
"LEFT"
],
side:"LEFT",
moveTimer:0,
moveDelay:20,
dangerTimer:0,
color:"#697ba9",
eye:"#64edff",
shape:"hollow",
scale:1.02
},
{
name:"MOTHER STATIC",
room:8,
route:[
8,
6,
7,
5,
"RIGHT"
],
side:"RIGHT",
moveTimer:0,
moveDelay:23,
dangerTimer:0,
color:"#a36f92",
eye:"#ff64c9",
shape:"static",
scale:1.04
}
];
/* =========================================================
CANVAS RESIZE
========================================================= */
function resizeCanvas(canvas,ctx){
const ratio =
Math.min(
window.devicePixelRatio || 1,
2
);
const width =
canvas.clientWidth;
const height =
canvas.clientHeight;
canvas.width =
Math.max(1,width * ratio);
canvas.height =
Math.max(1,height * ratio);
ctx.setTransform(
ratio,
0,
0,
ratio,
0,
0
);
}
function resizeAll(){
resizeCanvas(
officeCanvas,
officeCtx
);
resizeCanvas(
cameraCanvas,
cameraCtx
);
resizeCanvas(
scareCanvas,
scareCtx
);
drawOffice();
if(cameraOpen){
drawCamera();
}
}
window.addEventListener(
"resize",
resizeAll
);
/* =========================================================
AUDIO
========================================================= */
function initAudio(){
if(!audioContext){
const Audio =
window.AudioContext ||
window.webkitAudioContext;
if(Audio){
audioContext =
new Audio();
}
}
if(
audioContext &&
audioContext.state === "suspended"
){
audioContext.resume();
}
}
/* small sound */
function beep(
frequency=300,
duration=.08,
type="square",
volume=.025
){
initAudio();
if(!audioContext) return;
const oscillator =
audioContext.createOscillator();
const gain =
audioContext.createGain();
oscillator.type =
type;
oscillator.frequency.setValueAtTime(
frequency,
audioContext.currentTime
);
gain.gain.setValueAtTime(
volume,
audioContext.currentTime
);
gain.gain.exponentialRampToValueAtTime(
.001,
audioContext.currentTime + duration
);
oscillator.connect(gain);
gain.connect(
audioContext.destination
);
oscillator.start();
oscillator.stop(
audioContext.currentTime +
duration
);
}
/* =========================================================
SPECIAL JUMPSCARE SOUND
========================================================= */
function jumpscareSound(){
initAudio();
if(!audioContext) return;
const now =
audioContext.currentTime;
function tone(
freq,
length,
delay,
volume,
type
){
const osc =
audioContext.createOscillator();
const gain =
audioContext.createGain();
osc.type = type;
osc.frequency.setValueAtTime(
freq,
now + delay
);
osc.frequency.exponentialRampToValueAtTime(
Math.max(30,freq*.35),
now + delay + length
);
gain.gain.setValueAtTime(
volume,
now + delay
);
gain.gain.exponentialRampToValueAtTime(
.001,
now + delay + length
);
osc.connect(gain);
gain.connect(
audioContext.destination
);
osc.start(now + delay);
osc.stop(
now + delay + length
);
}
tone(
120,
1.1,
0,
.24,
"sawtooth"
);
tone(
63,
.9,
.05,
.18,
"square"
);
tone(
880,
.35,
.12,
.11,
"sawtooth"
);
tone(
430,
.6,
.2,
.08,
"square"
);
}
/* =========================================================
DRAW HELPERS
========================================================= */
function fillRect(
ctx,
x,
y,
w,
h,
color
){
ctx.fillStyle = color;
ctx.fillRect(
x,
y,
w,
h
);
}
function strokeRect(
ctx,
x,
y,
w,
h,
color,
width=1
){
ctx.strokeStyle =
color;
ctx.lineWidth =
width;
ctx.strokeRect(
x,
y,
w,
h
);
}
function write(
ctx,
text,
x,
y,
size,
color="#dce6e4",
align="left"
){
ctx.fillStyle =
color;
ctx.font =
`bold ${size}px Arial`;
ctx.textAlign =
align;
ctx.fillText(
text,
x,
y
);
}
function polygon(
ctx,
points,
color
){
ctx.fillStyle =
color;
ctx.beginPath();
ctx.moveTo(
points[0][0],
points[0][1]
);
for(
let i=1;
i<points.length;
i++
){
ctx.lineTo(
points[i][0],
points[i][1]
);
}
ctx.closePath();
ctx.fill();
}
/* =========================================================
MONSTER DRAWING
Four visibly different designs
========================================================= */
function drawEnemy(
ctx,
x,
y,
scale,
enemy
){
ctx.save();
ctx.translate(
x,
y
);
ctx.scale(
scale,
scale
);
/* shadow */
ctx.fillStyle =
"#000b";
ctx.beginPath();
ctx.ellipse(
0,
52,
47,
12,
0,
0,
Math.PI*2
);
ctx.fill();
/* -----------------------------------------
WARDEN
----------------------------------------- */
if(enemy.shape === "warden"){
polygon(
ctx,
[
[-25,45],
[-27,-17],
[-39,-40],
[-20,-34],
[-9,-53],
[0,-35],
[15,-50],
[20,-32],
[38,-40],
[27,-16],
[26,45]
],
"#080b0b"
);
fillRect(
ctx,
-22,
-34,
44,
31,
enemy.color
);
/* eyes */
fillRect(
ctx,
-15,
-27,
10,
6,
enemy.eye
);
fillRect(
ctx,
5,
-27,
10,
6,
enemy.eye
);
/* mouth */
fillRect(
ctx,
-15,
-12,
30,
6,
"#080909"
);
for(
let i=0;
i<6;
i++
){
fillRect(
ctx,
-12 + i*5,
-11,
2,
7,
"#d7d6c9"
);
}
/* shoulders */
fillRect(
ctx,
-35,
0,
10,
43,
"#090b0b"
);
fillRect(
ctx,
25,
0,
10,
43,
"#090b0b"
);
}
/* -----------------------------------------
TIN GRIN
----------------------------------------- */
else if(enemy.shape === "grin"){
ctx.fillStyle =
"#090b0b";
ctx.beginPath();
ctx.ellipse(
0,
-18,
33,
34,
0,
0,
Math.PI*2
);
ctx.fill();
ctx.fillStyle =
enemy.color;
ctx.beginPath();
ctx.ellipse(
0,
-20,
25,
27,
0,
0,
Math.PI*2
);
ctx.fill();
/* ears */
ctx.fillStyle =
"#080a0a";
ctx.beginPath();
ctx.arc(
-27,
-25,
11,
0,
Math.PI*2
);
ctx.arc(
27,
-25,
11,
0,
Math.PI*2
);
ctx.fill();
/* eyes */
ctx.fillStyle =
enemy.eye;
ctx.beginPath();
ctx.ellipse(
-11,
-25,
7,
10,
-.2,
0,
Math.PI*2
);
ctx.ellipse(
11,
-25,
7,
10,
.2,
0,
Math.PI*2
);
ctx.fill();
/* huge grin */
ctx.fillStyle =
"#080909";
ctx.beginPath();
ctx.ellipse(
0,
-4,
27,
15,
0,
0,
Math.PI*2
);
ctx.fill();
for(
let i=0;
i<8;
i++
){
fillRect(
ctx,
-19 + i*5,
-8,
3,
8,
"#e8dfc9"
);
}
/* body */
polygon(
ctx,
[
[-25,5],
[-20,47],
[-10,47],
[-7,14],
[7,14],
[10,47],
[21,47],
[26,5]
],
"#090b0b"
);
}
/* -----------------------------------------
HOLLOW
----------------------------------------- */
else if(enemy.shape === "hollow"){
polygon(
ctx,
[
[-13,46],
[-11,-25],
[-19,-43],
[-7,-35],
[0,-60],
[7,-35],
[19,-44],
[12,-23],
[14,46]
],
"#070a0b"
);
polygon(
ctx,
[
[-14,-33],
[-8,-47],
[8,-47],
[14,-31],
[9,-8],
[-9,-8]
],
enemy.color
);
/* narrow eyes */
fillRect(
ctx,
-10,
-29,
7,
4,
enemy.eye
);
fillRect(
ctx,
3,
-29,
7,
4,
enemy.eye
);
/* no normal mouth */
ctx.strokeStyle =
"#111516";
ctx.lineWidth=3;
ctx.beginPath();
ctx.moveTo(
-5,
-15
);
ctx.lineTo(
5,
-15
);
ctx.stroke();
/* extremely long arms */
ctx.strokeStyle =
"#070909";
ctx.lineWidth=6;
ctx.beginPath();
ctx.moveTo(
-11,
0
);
ctx.lineTo(
-27,
27
);
ctx.lineTo(
-32,
45
);
ctx.moveTo(
11,
0
);
ctx.lineTo(
27,
27
);
ctx.lineTo(
32,
45
);
ctx.stroke();
}
/* -----------------------------------------
MOTHER STATIC
----------------------------------------- */
else{
/* antennae */
ctx.strokeStyle =
enemy.color;
ctx.lineWidth=4;
ctx.beginPath();
ctx.moveTo(
-9,
-35
);
ctx.quadraticCurveTo(
-38,
-61,
-34,
-30
);
ctx.moveTo(
9,
-35
);
ctx.quadraticCurveTo(
38,
-61,
34,
-30
);
ctx.stroke();
/* body */
polygon(
ctx,
[
[-28,45],
[-30,-18],
[-20,-39],
[-5,-30],
[0,-46],
[6,-30],
[21,-40],
[30,-17],
[27,45]
],
"#09090c"
);
/* split face */
polygon(
ctx,
[
[-20,-33],
[-3,-39],
[-2,-10],
[-20,-7]
],
"#75516c"
);
polygon(
ctx,
[
[3,-39],
[20,-33],
[20,-7],
[2,-10]
],
"#9b657f"
);
/* eyes */
fillRect(
ctx,
-15,
-27,
8,
5,
enemy.eye
);
fillRect(
ctx,
7,
-27,
8,
5,
enemy.eye
);
/* split line */
fillRect(
ctx,
-1,
-38,
2,
40,
"#170e16"
);
/* lower face */
polygon(
ctx,
[
[-19,3],
[-10,16],
[-3,4],
[4,16],
[12,4],
[20,15],
[16,31],
[-16,31]
],
"#08090a"
);
}
/* random damage/details */
ctx.strokeStyle =
enemy.color;
ctx.globalAlpha=.65;
ctx.lineWidth=2;
ctx.beginPath();
ctx.moveTo(
-20,
-3
);
ctx.lineTo(
-10,
4
);
ctx.lineTo(
-16,
13
);
ctx.moveTo(
19,
-4
);
ctx.lineTo(
10,
7
);
ctx.stroke();
ctx.restore();
}
/* =========================================================
OFFICE DRAWING
========================================================= */
function drawOffice(){
const w =
officeCanvas.clientWidth;
const h =
officeCanvas.clientHeight;
officeCtx.clearRect(
0,
0,
w,
h
);
/* dark background */
fillRect(
officeCtx,
0,
0,
w,
h,
"#070a0b"
);
/* ceiling */
polygon(
officeCtx,
[
[0,0],
[w,0],
[w*.82,h*.24],
[w*.18,h*.24]
],
"#171e1f"
);
/* back wall */
polygon(
officeCtx,
[
[w*.18,h*.24],
[w*.82,h*.24],
[w*.92,h*.67],
[w*.08,h*.67]
],
"#252d2e"
);
/* wall panels */
for(
let i=0;
i<8;
i++
){
officeCtx.strokeStyle =
"#4b575755";
officeCtx.lineWidth=1;
officeCtx.beginPath();
officeCtx.moveTo(
w*(.18+i*.08),
h*.24
);
officeCtx.lineTo(
w*(.08+i*.105),
h*.67
);
officeCtx.stroke();
}
/* floor */
polygon(
officeCtx,
[
[0,h*.67],
[w,h*.67],
[w,h],
[0,h]
],
"#121819"
);
/* floor perspective */
for(
let i=0;
i<8;
i++
){
officeCtx.strokeStyle =
"#3d494a";
officeCtx.beginPath();
officeCtx.moveTo(
w*.5,
h*.67
);
officeCtx.lineTo(
w*i/7,
h
);
officeCtx.stroke();
}
for(
let i=1;
i<6;
i++
){
officeCtx.strokeStyle =
"#303a3a";
officeCtx.beginPath();
officeCtx.moveTo(
0,
h*(.67+i*.065)
);
officeCtx.lineTo(
w,
h*(.67+i*.065)
);
officeCtx.stroke();
}
/* left doorway */
drawDoor(
officeCtx,
w*.025,
h*.29,
Math.min(70,w*.14),
h*.40,
leftDoorClosed,
leftLightOn
);
/* right doorway */
const rightWidth =
Math.min(70,w*.14);
drawDoor(
officeCtx,
w*.975-rightWidth,
h*.29,
rightWidth,
h*.40,
rightDoorClosed,
rightLightOn
);
/* enemies visible at doors */
enemies.forEach(enemy=>{
if(
enemy.room==="LEFT" &&
leftLightOn &&
!leftDoorClosed
){
drawEnemy(
officeCtx,
w*.025 + Math.min(70,w*.14)*.5,
h*.56,
Math.min(.78,h/620),
enemy
);
}
if(
enemy.room==="RIGHT" &&
rightLightOn &&
!rightDoorClosed
){
drawEnemy(
officeCtx,
w*.975-rightWidth*.5,
h*.56,
Math.min(.78,h/620),
enemy
);
}
});
/* desk */
polygon(
officeCtx,
[
[w*.27,h*.78],
[w*.73,h*.78],
[w*.84,h*.98],
[w*.16,h*.98]
],
"#101718"
);
fillRect(
officeCtx,
w*.39,
h*.72,
w*.22,
39,
"#1e292a"
);
strokeRect(
officeCtx,
w*.39,
h*.72,
w*.22,
39,
"#566364",
2
);
write(
officeCtx,
"SECURITY TERMINAL",
w/2,
h*.758,
Math.max(8,w*.022),
"#bac8c7",
"center"
);
/* desk monitor */
fillRect(
officeCtx,
w*.44,
h*.65,
w*.12,
h*.07,
"#090d0e"
);
strokeRect(
officeCtx,
w*.44,
h*.65,
w*.12,
h*.07,
"#5e6c6d",
2
);
write(
officeCtx,
cameraOpen ? "CAM" : "READY",
w/2,
h*.692,
Math.max(8,w*.018),
"#91aaa1",
"center"
);
/* vignette */
const gradient =
officeCtx.createRadialGradient(
w/2,
h*.48,
20,
w/2,
h*.48,
Math.max(w,h)*.78
);
gradient.addColorStop(
0,
"#00000000"
);
gradient.addColorStop(
1,
"#000000cc"
);
officeCtx.fillStyle =
gradient;
officeCtx.fillRect(
0,
0,
w,
h
);
}
/* =========================================================
DOOR
========================================================= */
function drawDoor(
ctx,
x,
y,
width,
height,
closed,
light
){
/* outer frame */
fillRect(
ctx,
x-5,
y-5,
width+10,
height+10,
"#647071"
);
fillRect(
ctx,
x,
y,
width,
height,
"#050708"
);
if(closed){
const gradient =
ctx.createLinearGradient(
x,
y,
x+width,
y
);
gradient.addColorStop(
0,
"#454d4e"
);
gradient.addColorStop(
.5,
"#151a1b"
);
gradient.addColorStop(
1,
"#343c3d"
);
ctx.fillStyle =
gradient;
ctx.fillRect(
x,
y,
width,
height
);
for(
let i=1;
i<5;
i++
){
fillRect(
ctx,
x+width*i/5,
y,
1,
height,
"#5c666666"
);
}
fillRect(
ctx,
x+width*.1,
y+height*.48,
width*.8,
5,
"#090b0c"
);
write(
ctx,
"LOCKED",
x+width/2,
y+height*.55,
Math.max(7,width*.10),
"#d1d9d8",
"center"
);
}
else{
/* dark opening */
fillRect(
ctx,
x+width*.07,
y+height*.04,
width*.86,
height*.92,
"#080c0d"
);
/* window */
const windowX =
x+width*.14;
const windowY =
y+height*.10;
const windowW =
width*.72;
const windowH =
height*.32;
fillRect(
ctx,
windowX,
windowY,
windowW,
windowH,
light
? "#b9caa9"
: "#1a2324"
);
/* glass reflections */
if(light){
const glow =
ctx.createRadialGradient(
x+width/2,
windowY+windowH*.5,
2,
x+width/2,
windowY+windowH*.5,
width
);
glow.addColorStop(
0,
"#f4f4d888"
);
glow.addColorStop(
1,
"#e7e7bb00"
);
ctx.fillStyle =
glow;
ctx.fillRect(
x-width*.5,
y,
width*2,
height
);
}
/* glass lines */
fillRect(
ctx,
windowX+windowW*.49,
windowY,
2,
windowH,
"#687777"
);
fillRect(
ctx,
windowX,
windowY+windowH*.48,
windowW,
2,
"#687777"
);
/* bottom warning strip */
fillRect(
ctx,
x+width*.08,
y+height*.73,
width*.84,
4,
"#343d3d"
);
}
/* frame */
fillRect(
ctx,
x,
y,
width,
3,
"#a0aaaa"
);
fillRect(
ctx,
x,
y,
3,
height,
"#8a9696"
);
fillRect(
ctx,
x+width-3,
y,
3,
height,
"#697575"
);
}
/* =========================================================
CAMERA SCENE
========================================================= */
function drawCamera(){
const w =
cameraCanvas.clientWidth;
const h =
cameraCanvas.clientHeight;
cameraCtx.clearRect(
0,
0,
w,
h
);
const cam =
cameras[currentCamera];
/* base */
fillRect(
cameraCtx,
0,
0,
w,
h,
"#080c0d"
);
/* different room colors */
const colors = {
showroom:"#343a39",
storage:"#453a2c",
workshop:"#293638",
dining:"#3c3027",
hall:"#20292b",
back:"#302d3b",
parts:"#423626",
stage:"#392831",
service:"#29352f"
};
const wallColor =
colors[cam.type] ||
"#303839";
/* ceiling */
polygon(
cameraCtx,
[
[0,0],
[w,0],
[w*.90,h*.30],
[w*.10,h*.30]
],
"#151b1c"
);
/* room */
polygon(
cameraCtx,
[
[w*.10,h*.12],
[w*.90,h*.12],
[w*.98,h*.68],
[w*.02,h*.68]
],
wallColor
);
/* room-specific scenery */
drawRoomObjects(
cameraCtx,
cam.type,
w,
h
);
/* floor */
polygon(
cameraCtx,
[
[0,h*.68],
[w,h*.68],
[w,h],
[0,h]
],
"#111718"
);
/* floor lines */
for(
let i=0;
i<9;
i++
){
cameraCtx.strokeStyle =
"#46525255";
cameraCtx.beginPath();
cameraCtx.moveTo(
w*.5,
h*.68
);
cameraCtx.lineTo(
w*i/8,
h
);
cameraCtx.stroke();
}
for(
let i=1;
i<5;
i++
){
cameraCtx.strokeStyle =
"#354141";
cameraCtx.beginPath();
cameraCtx.moveTo(
0,
h*(.68+i*.065)
);
cameraCtx.lineTo(
w,
h*(.68+i*.065)
);
cameraCtx.stroke();
}
/* enemies */
enemies.forEach(enemy=>{
if(enemy.room === currentCamera){
const visibleEnemies =
enemies.filter(
e=>e.room===currentCamera
);
const index =
visibleEnemies.indexOf(enemy);
const offset =
(index -
(visibleEnemies.length-1)/2)
* Math.min(w*.23,100);
drawEnemy(
cameraCtx,
w/2 + offset,
h*.61,
Math.min(
.95,
h/470
),
enemy
);
}
});
/* camera border */
strokeRect(
cameraCtx,
5,
5,
w-10,
h-10,
"#566565",
2
);
/* scan lines */
for(
let y=0;
y<h;
y+=5
){
fillRect(
cameraCtx,
0,
y,
w,
1,
"#ffffff08"
);
}
/* timestamp */
fillRect(
cameraCtx,
8,
h-31,
160,
22,
"#000b"
);
write(
cameraCtx,
"● REC CAM " +
String(currentCamera+1).padStart(2,"0") +
" LIVE",
16,
h-16,
9,
"#d9e4e2"
);
/* noise */
if(staticTimer>0){
for(
let i=0;
i<35;
i++
){
cameraCtx.fillStyle =
Math.random()>.5
? "#ffffff26"
: "#00000030";
cameraCtx.fillRect(
Math.random()*w,
Math.random()*h,
Math.random()*w*.25+4,
2
);
}
}
}
/* =========================================================
ROOM OBJECTS
========================================================= */
function drawRoomObjects(
ctx,
type,
w,
h
){
if(type==="showroom"){
/* display panels */
for(
let i=0;
i<3;
i++
){
fillRect(
ctx,
w*(.20+i*.25),
h*.39,
w*.15,
h*.20,
"#121919"
);
strokeRect(
ctx,
w*(.20+i*.25),
h*.39,
w*.15,
h*.20,
"#77837f",
2
);
}
write(
ctx,
"DISPLAY",
w*.5,
h*.24,
12,
"#b8c1bd",
"center"
);
}
else if(type==="storage"){
for(
let i=0;
i<5;
i++
){
const x =
w*(.08+i*.18);
fillRect(
ctx,
x,
h*.16,
w*.13,
h*.46,
"#594936"
);
for(
let j=0;
j<4;
j++
){
fillRect(
ctx,
x+4,
h*(.21+j*.09),
w*.11,
3,
"#171c1b"
);
}
}
}
else if(type==="workshop"){
fillRect(
ctx,
w*.16,
h*.42,
w*.68,
h*.09,
"#59625e"
);
fillRect(
ctx,
w*.21,
h*.51,
5,
h*.18,
"#141918"
);
fillRect(
ctx,
w*.76,
h*.51,
5,
h*.18,
"#141918"
);
for(
let i=0;
i<4;
i++
){
fillRect(
ctx,
w*(.23+i*.14),
h*.30,
w*.09,
h*.09,
"#121818"
);
}
}
else if(type==="dining"){
for(
let i=0;
i<3;
i++
){
const x =
w*(.15+i*.27);
fillRect(
ctx,
x,
h*.43,
w*.19,
h*.055,
"#584b3d"
);
fillRect(
ctx,
x+w*.03,
h*.485,
4,
h*.14,
"#161b1b"
);
fillRect(
ctx,
x+w*.14,
h*.485,
4,
h*.14,
"#161b1b"
);
}
}
else if(type==="hall"){
polygon(
ctx,
[
[w*.34,h*.13],
[w*.66,h*.13],
[w*.82,h*.68],
[w*.18,h*.68]
],
"#080b0c"
);
for(
let i=0;
i<5;
i++
){
ctx.strokeStyle =
"#63706f";
ctx.strokeRect(
w*(.32-i*.025),
h*(.20+i*.075),
w*(.36+i*.05),
h*.04
);
}
}
else if(type==="back"){
for(
let i=0;
i<6;
i++
){
fillRect(
ctx,
w*(.10+i*.15),
h*.13,
4,
h*.48,
"#606b68"
);
}
fillRect(
ctx,
w*.24,
h*.31,
w*.52,
h*.17,
"#141b1b"
);
strokeRect(
ctx,
w*.24,
h*.31,
w*.52,
h*.17,
"#697675"
);
}
else if(type==="parts"){
for(
let i=0;
i<4;
i++
){
ctx.beginPath();
ctx.arc(
w*(.22+i*.18),
h*.42,
24,
0,
Math.PI*2
);
ctx.strokeStyle =
"#6d7771";
ctx.lineWidth=5;
ctx.stroke();
}
}
else if(type==="stage"){
fillRect(
ctx,
w*.11,
h*.18,
w*.78,
h*.08,
"#594044"
);
fillRect(
ctx,
w*.18,
h*.26,
w*.64,
h*.34,
"#21181d"
);
/* curtains */
polygon(
ctx,
[
[w*.05,h*.10],
[w*.30,h*.15],
[w*.28,h*.68],
[w*.05,h*.68]
],
"#4a2932"
);
polygon(
ctx,
[
[w*.95,h*.10],
[w*.70,h*.15],
[w*.72,h*.68],
[w*.95,h*.68]
],
"#4a2932"
);
write(
ctx,
"STAGE",
w*.5,
h*.39,
18,
"#d6b7a4",
"center"
);
}
else{
fillRect(
ctx,
w*.25,
h*.25,
w*.5,
h*.2,
"#182322"
);
strokeRect(
ctx,
w*.25,
h*.25,
w*.5,
h*.2,
"#637270"
);
}
}
/* =========================================================
MAP
========================================================= */
function updateMap(){
mapGrid.innerHTML="";
/* YOU */
const you =
document.createElement("button");
you.className =
"mapButton you";
you.textContent =
"● YOU — SECURITY OFFICE";
you.onclick=()=>{
closeCameras();
};
mapGrid.appendChild(you);
/* camera buttons */
cameras.forEach((camera,index)=>{
const button =
document.createElement("button");
const enemyHere =
enemies.some(
enemy=>enemy.room===index
);
button.className =
"mapButton" +
(index===currentCamera
?" active":"") +
(enemyHere
?" enemy":"");
button.textContent =
"CAM " +
String(index+1).padStart(2,"0") +
"\n" +
camera.name;
button.onclick=()=>{
currentCamera =
index;
staticTimer=.45;
updateCameraUI();
beep(
400,
.05,
"square",
.018
);
};
mapGrid.appendChild(
button
);
});
}
/* =========================================================
CAMERA UI
========================================================= */
function updateCameraUI(){
const cam =
cameras[currentCamera];
cameraName.textContent =
"CAM " +
String(currentCamera+1).padStart(2,"0") +
" / " +
cam.name;
cameraTitle.textContent =
"CAM " +
String(currentCamera+1).padStart(2,"0") +
" / " +
cam.name;
cameraDescription.textContent =
cam.description;
cameraIndex.textContent =
String(currentCamera+1).padStart(2,"0") +
" / " +
cameras.length;
cameraSignal.textContent =
staticTimer>0
? "⚠ SIGNAL INTERFERENCE"
: "● SIGNAL STABLE";
updateMap();
drawCamera();
}
/* =========================================================
CAMERA OPEN / CLOSE
========================================================= */
function openCameras(){
if(!gameRunning || gameOver){
return;
}
cameraOpen=true;
cameraLayer.style.display=
"block";
officeLayer.style.display=
"none";
updateCameraUI();
beep(
260,
.09,
"square",
.025
);
}
function closeCameras(){
cameraOpen=false;
cameraLayer.style.display=
"none";
officeLayer.style.display=
"block";
drawOffice();
}
/* =========================================================
MESSAGE
========================================================= */
function showMessage(text){
message.textContent =
text;
message.style.opacity =
"1";
messageTimer=2;
}
/* =========================================================
BUTTON STATES
========================================================= */
function updateButtons(){
const leftDoor =
document.getElementById("leftDoor");
const rightDoor =
document.getElementById("rightDoor");
const leftLight =
document.getElementById("leftLight");
const rightLight =
document.getElementById("rightLight");
leftDoor.classList.toggle(
"doorClosed",
leftDoorClosed
);
rightDoor.classList.toggle(
"doorClosed",
rightDoorClosed
);
leftLight.classList.toggle(
"lightOn",
leftLightOn
);
rightLight.classList.toggle(
"lightOn",
rightLightOn
);
leftDoor.innerHTML =
leftDoorClosed
? "OPEN<br>DOOR"
: "CLOSE<br>DOOR";
rightDoor.innerHTML =
rightDoorClosed
? "OPEN<br>DOOR"
: "CLOSE<br>DOOR";
leftLight.textContent =
leftLightOn
? "LIGHT ON"
: "LIGHT OFF";
rightLight.textContent =
rightLightOn
? "LIGHT ON"
: "LIGHT OFF";
}
/* =========================================================
LEFT DOOR
========================================================= */
document.getElementById(
"leftDoor"
).onclick=()=>{
if(!gameRunning || cameraOpen) return;
leftDoorClosed =
!leftDoorClosed;
if(leftDoorClosed){
leftLightOn=false;
showMessage(
"WEST DOOR CLOSED"
);
}else{
showMessage(
"WEST DOOR OPEN"
);
}
beep(
leftDoorClosed?90:130,
.14,
"square",
.04
);
updateButtons();
drawOffice();
};
/* =========================================================
RIGHT DOOR
========================================================= */
document.getElementById(
"rightDoor"
).onclick=()=>{
if(!gameRunning || cameraOpen) return;
rightDoorClosed =
!rightDoorClosed;
if(rightDoorClosed){
rightLightOn=false;
showMessage(
"EAST DOOR CLOSED"
);
}else{
showMessage(
"EAST DOOR OPEN"
);
}
beep(
rightDoorClosed?90:130,
.14,
"square",
.04
);
updateButtons();
drawOffice();
};
/* =========================================================
LEFT LIGHT
========================================================= */
document.getElementById(
"leftLight"
).onclick=()=>{
if(!gameRunning || cameraOpen) return;
if(leftDoorClosed){
showMessage(
"DOOR IS CLOSED"
);
beep(
180,
.05
);
return;
}
leftLightOn =
!leftLightOn;
beep(
leftLightOn?620:420,
.06,
"square",
.018
);
updateButtons();
drawOffice();
};
/* =========================================================
RIGHT LIGHT
========================================================= */
document.getElementById(
"rightLight"
).onclick=()=>{
if(!gameRunning || cameraOpen) return;
if(rightDoorClosed){
showMessage(
"DOOR IS CLOSED"
);
beep(
180,
.05
);
return;
}
rightLightOn =
!rightLightOn;
beep(
rightLightOn?620:420,
.06,
"square",
.018
);
updateButtons();
drawOffice();
};
/* =========================================================
CAMERA BUTTON
========================================================= */
document.getElementById(
"cameraButton"
).onclick=()=>{
openCameras();
};
/* =========================================================
CLOSE CAMERA
========================================================= */
document.getElementById(
"closeCamera"
).onclick=()=>{
closeCameras();
};
/* =========================================================
PREVIOUS CAMERA
========================================================= */
document.getElementById(
"previousCamera"
).onclick=()=>{
currentCamera--;
if(currentCamera<0){
currentCamera=
cameras.length-1;
}
staticTimer=.35;
updateCameraUI();
beep(
400,
.05,
"square",
.018
);
};
/* =========================================================
NEXT CAMERA
========================================================= */
document.getElementById(
"nextCamera"
).onclick=()=>{
currentCamera++;
if(
currentCamera>=
cameras.length
){
currentCamera=0;
}
staticTimer=.35;
updateCameraUI();
beep(
400,
.05,
"square",
.018
);
};
/* =========================================================
ENEMY MOVEMENT
========================================================= */
function moveEnemies(dt){
for(
const enemy of enemies
){
/* enemy is already at door */
if(
enemy.room==="LEFT" ||
enemy.room==="RIGHT"
){
enemy.dangerTimer += dt;
const closed =
enemy.room==="LEFT"
? leftDoorClosed
: rightDoorClosed;
/* Door gives player time */
if(closed){
if(
enemy.dangerTimer >
3.5
){
const returnRoom =
enemy.side==="LEFT"
? 4
: 5;
enemy.room =
returnRoom;
enemy.dangerTimer=0;
enemy.moveTimer=0;
showMessage(
enemy.name +
" RETREATED"
);
beep(
70,
.12,
"sawtooth",
.025
);
}
}
else{
/* generous reaction time */
if(
enemy.dangerTimer >
4.0
){
triggerJumpscare(
enemy
);
return;
}
}
continue;
}
/* normal movement */
enemy.moveTimer += dt;
if(
enemy.moveTimer >=
enemy.moveDelay
){
enemy.moveTimer=0;
/*
Random chance means enemies
do NOT move together.
*/
if(
Math.random()<.72
){
const position =
enemy.route.indexOf(
enemy.room
);
if(
position>=0 &&
position<
enemy.route.length-1
){
const next =
enemy.route[
position+1
];
enemy.room =
next;
if(
next==="LEFT" ||
next==="RIGHT"
){
enemy.dangerTimer=0;
showMessage(
enemy.name +
" IS NEAR"
);
beep(
80,
.12,
"sawtooth",
.018
);
}
}
}
}
}
}
/* =========================================================
POWER
========================================================= */
function updatePower(dt){
let drain=.035;
if(cameraOpen){
drain+=.045;
}
if(leftDoorClosed){
drain+=.09;
}
if(rightDoorClosed){
drain+=.09;
}
if(leftLightOn){
drain+=.065;
}
if(rightLightOn){
drain+=.065;
}
power -=
drain*dt;
if(power<0){
power=0;
}
powerNumber.textContent =
Math.ceil(power);
powerBar.style.width =
power+"%";
if(power<=20){
powerBar.style.background =
"#c65c5c";
}else{
powerBar.style.background =
"#b9d4bb";
}
if(power===0){
leftDoorClosed=false;
rightDoorClosed=false;
leftLightOn=false;
rightLightOn=false;
updateButtons();
}
}
/* =========================================================
TIME
========================================================= */
function updateClock(){
/*
180 seconds = complete night
*/
const hour =
Math.floor(
gameSeconds/30
);
if(hour===0){
timeElement.textContent=
"12 AM";
}else{
timeElement.textContent=
hour+" AM";
}
if(hour>=6){
winGame();
}
}
/* =========================================================
GAME OVER / JUMPSCARE
========================================================= */
function triggerJumpscare(enemy){
if(gameOver){
return;
}
gameOver=true;
gameRunning=false;
cameraOpen=false;
cameraLayer.style.display=
"none";
officeLayer.style.display=
"none";
jumpscare.style.display=
"block";
scareFrame=0;
jumpscareSound();
animateJumpscare(
enemy
);
}
/* =========================================================
JUMPSCARE ANIMATION
========================================================= */
function animateJumpscare(enemy){
const w =
scareCanvas.clientWidth;
const h =
scareCanvas.clientHeight;
scareCtx.clearRect(
0,
0,
w,
h
);
/* changing red/black background */
fillRect(
scareCtx,
0,
0,
w,
h,
scareFrame%2
? "#260000"
: "#090000"
);
/* huge enemy */
const grow =
1.2 +
Math.min(
scareFrame*.045,
.9
);
drawEnemy(
scareCtx,
w/2 +
(Math.random()-.5)*14,
h*.61,
Math.min(
w/48,
h/42
) * grow,
enemy
);
/* flash */
if(
scareFrame%3===0
){
scareFlash.style.opacity=
".22";
setTimeout(()=>{
scareFlash.style.opacity=
"0";
},35);
}
write(
scareCtx,
enemy.name,
w/2,
h*.13,
Math.max(19,w*.055),
"#ffffff",
"center"
);
write(
scareCtx,
"SIGNAL LOST",
w/2,
h*.91,
Math.max(13,w*.035),
"#ffbaba",
"center"
);
scareFrame++;
if(
scareFrame<28
){
setTimeout(
()=>{
animateJumpscare(enemy);
},
55
);
}else{
setTimeout(
showGameOver,
700
);
}
}
/* =========================================================
GAME OVER SCREEN
========================================================= */
function showGameOver(){
jumpscare.style.display=
"none";
menu.style.display=
"flex";
document.querySelector(
".menuBox"
).innerHTML=`
<h1>GAME OVER</h1>
<h2>SIGNAL LOST</h2>
<div class="menuLine"></div>
<div class="menuText">
היצור הצליח להגיע אליך.
<br><br>
בדוק מצלמות בתדירות נכונה,
<br>
השתמש באורות,
<br>
ואל תשאיר את הדלתות סגורות
כל הלילה.
</div>
<div class="menuLine"></div>
<button id="startButton">
TRY AGAIN
</button>
`;
document.getElementById(
"startButton"
).onclick=startGame;
}
/* =========================================================
WIN
========================================================= */
function winGame(){
if(!gameRunning){
return;
}
gameRunning=false;
cameraOpen=false;
menu.style.display=
"flex";
document.querySelector(
".menuBox"
).innerHTML=`
<h1>6 AM</h1>
<h2>NIGHT COMPLETE</h2>
<div class="menuLine"></div>
<div class="menuText">
שרדת את כל המשמרת.
<br><br>
המערכת חזרה לפעולה.
<br><br>
אבל במצלמה האחרונה
<br>
משהו עדיין זז...
</div>
<div class="menuLine"></div>
<button id="startButton">
PLAY AGAIN
</button>
`;
document.getElementById(
"startButton"
).onclick=startGame;
beep(
700,
.2,
"sine",
.04
);
}
/* =========================================================
MAIN GAME LOOP
========================================================= */
function gameLoop(timestamp){
if(!gameRunning){
return;
}
if(!lastTime){
lastTime =
timestamp;
}
let dt =
(timestamp-lastTime)/1000;
lastTime =
timestamp;
if(dt>.1){
dt=.1;
}
gameSeconds +=
dt;
staticTimer =
Math.max(
0,
staticTimer-dt
);
messageTimer =
Math.max(
0,
messageTimer-dt
);
if(
messageTimer<=0
){
message.style.opacity=
"0";
}
updatePower(dt);
moveEnemies(dt);
updateClock();
if(!gameRunning){
return;
}
drawOffice();
if(cameraOpen){
drawCamera();
cameraSignal.textContent =
staticTimer>0
? "⚠ SIGNAL INTERFERENCE"
: "● SIGNAL STABLE";
}
requestAnimationFrame(
gameLoop
);
}
/* =========================================================
START GAME
========================================================= */
function startGame(){
initAudio();
gameRunning=true;
gameOver=false;
cameraOpen=false;
currentCamera=0;
power=100;
gameSeconds=0;
lastTime=0;
staticTimer=0;
leftDoorClosed=false;
rightDoorClosed=false;
leftLightOn=false;
rightLightOn=false;
/* reset enemies */
enemies[0].room=0;
enemies[1].room=3;
enemies[2].room=1;
enemies[3].room=8;
enemies.forEach(
enemy=>{
enemy.moveTimer=0;
enemy.dangerTimer=0;
}
);
cameraLayer.style.display=
"none";
officeLayer.style.display=
"block";
menu.style.display=
"none";
updateButtons();
updateMap();
drawOffice();
beep(
220,
.15,
"square",
.04
);
requestAnimationFrame(
gameLoop
);
}
/* =========================================================
KEYBOARD CONTROLS
========================================================= */
document.addEventListener(
"keydown",
event=>{
if(!gameRunning){
return;
}
const key =
event.key.toLowerCase();
/* C = cameras */
if(key==="c"){
if(cameraOpen){
closeCameras();
}else{
openCameras();
}
}
/* ESC */
if(
event.key==="Escape" &&
cameraOpen
){
closeCameras();
}
/* arrows */
if(
cameraOpen &&
event.key==="ArrowLeft"
){
document.getElementById(
"previousCamera"
).click();
}
if(
cameraOpen &&
event.key==="ArrowRight"
){
document.getElementById(
"nextCamera"
).click();
}
/* A / D door */
if(
!cameraOpen &&
key==="a"
){
document.getElementById(
"leftDoor"
).click();
}
if(
!cameraOpen &&
key==="d"
){
document.getElementById(
"rightDoor"
).click();
}
/* Q / E lights */
if(
!cameraOpen &&
key==="q"
){
document.getElementById(
"leftLight"
).click();
}
if(
!cameraOpen &&
key==="e"
){
document.getElementById(
"rightLight"
).click();
}
}
);
/* =========================================================
INITIALIZE
========================================================= */
resizeAll();
updateButtons();
updateMap();
/* start button */
document.getElementById(
"startButton"
).onclick=startGame;
/* prevent accidental page gestures */
document.addEventListener(
"touchmove",
event=>{
if(event.target.closest("#game")){
event.preventDefault();
}
},
{passive:false}
);
</script>
</body>
</html>Game Source: NIGHT SHIFT — Security Room
Creator: LunarPanda23
Libraries: none
Complexity: complex (5109 lines, 75.3 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: night-shift-security-room-lunarpanda23" to link back to the original. Then publish at arcadelab.ai/publish.