🎮ArcadeLab

NIGHT SHIFT — Security Room

by LunarPanda23
5109 lines75.3 KB
▶ Play
<!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">
                ▣ &nbsp; 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">
                ✕ &nbsp; 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.