🎮ArcadeLab

THE LAST WATCH — Night Security

by LunarPanda23
815 lines34.6 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>THE LAST WATCH — Night Security</title>
<style>
:root{
  --bg:#080d0e;--panel:#131c1d;--line:#607170;
  --text:#e0e9e5;--muted:#8da19b;
  --red:#a53d48;--green:#91c6a7;--gold:#bd9061;
}
*{box-sizing:border-box}
html,body{margin:0;width:100%;height:100%;background:#050809;color:var(--text);
font-family:Arial,Helvetica,sans-serif;overflow:hidden}
button{font:inherit;color:inherit;cursor:pointer;touch-action:manipulation}
button:focus-visible{outline:3px solid #f4d28b;outline-offset:2px}
#game{width:100%;height:100dvh;min-height:360px;display:flex;flex-direction:column;
max-width:1100px;margin:auto;background:#0b1112;border-left:2px solid #303c3b;border-right:2px solid #303c3b}
#top{flex:none;min-height:65px;padding:9px 14px;display:flex;align-items:center;
justify-content:space-between;gap:10px;background:#0c1314;border-bottom:3px solid #455452}
.brand{font-size:10px;letter-spacing:3px;color:#9aada7;margin-bottom:5px}
#clock{font-size:clamp(22px,5vw,38px);font-weight:900;letter-spacing:1px}
.sub{font-size:10px;color:var(--muted);letter-spacing:1.4px;margin-top:3px}
.stats{text-align:left;min-width:90px}
#power{font-size:22px;font-weight:bold}
.meter{height:6px;background:#273331;border:1px solid #63726d;margin-top:5px;width:100px;max-width:27vw}
#powerbar{height:100%;width:100%;background:#b5d5c2;transition:width .2s}
#status{font-size:10px;letter-spacing:1px;color:#a5b9b1;margin-top:4px}
#stage{position:relative;flex:1;min-height:0;overflow:hidden;background:#101818}
#scene{display:block;width:100%;height:100%}
#leftControls,#rightControls{position:absolute;top:42%;width:clamp(85px,19%,155px);
display:flex;flex-direction:column;gap:6px;z-index:3}
#leftControls{left:8px}#rightControls{right:8px}
.controlTitle{font-size:10px;letter-spacing:1.6px;text-align:center;color:#b8c9c2;
background:#0b1112eF;border:1px solid #71817b;padding:6px 2px}
.ctrl{min-height:43px;background:#172121;border:2px solid #657671;font-weight:bold;
font-size:clamp(10px,2.6vw,13px);padding:8px 4px}
.ctrl.active{background:#683038;border-color:#db7b83}
.ctrl.lit{background:#7a682c;border-color:#e7d17b;color:white}
.ctrl:disabled{opacity:.4;cursor:not-allowed}
#hint{position:absolute;bottom:7px;left:50%;transform:translateX(-50%);width:92%;
text-align:center;font-size:11px;color:#b4c2bd;text-shadow:0 1px 3px #000;pointer-events:none}
#camScreen{position:absolute;inset:8px 8px 5px;background:#090f10;border:2px solid #73847e;
z-index:5;display:none;flex-direction:column;min-height:0}
#camScreen.open{display:flex}
#camHeader{padding:8px 10px;display:flex;justify-content:space-between;align-items:center;
gap:8px;background:#172120;border-bottom:2px solid #52635e;flex:none}
#camName{font-weight:bold;font-size:clamp(12px,3.5vw,18px);letter-spacing:1px}
#signal{font-size:10px;color:#9eb1a8}
#camBody{flex:1;min-height:0;position:relative;overflow:hidden}
#camCanvas{width:100%;height:100%;display:block}
#camOverlay{position:absolute;inset:0;pointer-events:none;padding:9px;
display:flex;flex-direction:column;justify-content:space-between;text-shadow:0 2px 3px #000}
.camStamp{font-size:10px;color:#d0dfd7;letter-spacing:1.4px}
#camBottom{flex:none;background:#111919;border-top:2px solid #647570;padding:7px;
display:grid;grid-template-columns:56px 1fr 56px;gap:8px;align-items:center}
.arrow{height:48px;background:#1c2927;border:2px solid #8b9d95;font-size:25px;font-weight:bold}
#camCurrent{text-align:center;font-size:clamp(10px,3vw,14px);letter-spacing:2px;font-weight:bold}
#camActions{display:flex;justify-content:space-between;align-items:center;gap:8px;padding:7px;
border-top:1px solid #3b4a46;flex:none}
.smallBtn{background:#172120;border:1px solid #74857e;padding:8px 10px;font-size:11px}
#mapToggle{font-size:11px}
#map{display:none;position:absolute;right:8px;top:8px;width:min(52%,310px);
padding:9px;background:#0a1112f5;border:2px solid #758680;z-index:2}
#map.show{display:block}
.mapTitle{font-size:11px;letter-spacing:2px;text-align:center;margin-bottom:8px;color:#c4d4cc}
#mapGrid{display:grid;grid-template-columns:1fr 1fr;gap:5px}
.mapCell{padding:8px 3px;min-height:47px;background:#172120;border:1px solid #667770;
font-size:10px;text-align:center;line-height:1.4;position:relative}
.mapCell.selected{outline:2px solid #d9e6de;background:#30403b}
.mapCell.danger{border-color:#bd626a;color:#ffc3c3}
.mapCell.you{grid-column:1/-1;background:#243b32;border-color:#9ac9ac}
#bottom{flex:none;min-height:58px;display:grid;grid-template-columns:1fr 1fr 1fr;
gap:6px;padding:7px;background:#0c1314;border-top:2px solid #596a65}
.bottomBtn{border:1px solid #657671;background:#172120;font-size:11px;font-weight:bold;padding:8px 3px}
.bottomBtn.primary{border:2px solid #9aafa5;background:#263632}
#overlay{position:absolute;inset:0;z-index:20;background:#060a0bf5;
display:flex;align-items:center;justify-content:center;padding:18px;text-align:center}
#overlay.hidden{display:none}
.dialog{width:min(100%,480px);max-height:95%;overflow:auto;background:#111a1b;
border:2px solid #667a71;padding:22px 16px;box-shadow:0 0 40px #000}
.eyebrow{font-size:10px;letter-spacing:3px;color:#9ab4a5}
h1{font-size:clamp(27px,8vw,44px);margin:12px 0 4px;line-height:1}
h2{margin:10px 0}
.desc{color:#a8b8b1;font-size:13px;line-height:1.65}
.menuButtons{display:grid;gap:9px;margin-top:20px}
.menuBtn{padding:13px 8px;background:#1c2926;border:2px solid #81958a;font-weight:bold}
.menuBtn:disabled{opacity:.35}
.menuBtn.red{background:#512a30;border-color:#b7656d}
#overlayMsg{font-size:13px;line-height:1.6;color:#c2d0c9;margin:12px 0}
#scare{position:absolute;inset:0;z-index:30;display:none;align-items:center;justify-content:center;
background:#020303;overflow:hidden}
#scare.show{display:flex;animation:flash .13s steps(2) infinite}
#scareCanvas{width:100%;height:100%}
@keyframes flash{0%{filter:contrast(1.5)}50%{filter:contrast(2.4) brightness(1.4)}}
#toast{position:absolute;top:12%;left:50%;transform:translateX(-50%);z-index:15;
background:#101817;border:1px solid #8ca39a;padding:9px 14px;font-size:12px;
display:none;max-width:90%;text-align:center}
@media(min-width:700px){
 #game{border:3px solid #374440}
 #leftControls,#rightControls{width:125px;top:43%}
 #leftControls{left:15px}#rightControls{right:15px}
 .ctrl{min-height:48px}
}
</style>
</head>
<body>
<div id="game">
 <header id="top">
  <div>
   <div class="brand">THE LAST WATCH / SECURITY NETWORK</div>
   <div id="clock">12 AM</div>
   <div id="status">SYSTEM ONLINE · NIGHT 01</div>
  </div>
  <div class="stats">
   <div class="sub">NIGHT <span id="nightLabel">01</span></div>
   <div id="power">100%</div>
   <div class="meter"><div id="powerbar"></div></div>
   <div class="sub">POWER</div>
  </div>
 </header>

 <main id="stage">
  <canvas id="scene"></canvas>

  <section id="leftControls">
   <div class="controlTitle">WEST · LEFT</div>
   <button class="ctrl" id="leftDoor">CLOSE DOOR</button>
   <button class="ctrl" id="leftLight">LIGHT OFF</button>
  </section>

  <section id="rightControls">
   <div class="controlTitle">EAST · RIGHT</div>
   <button class="ctrl" id="rightDoor">CLOSE DOOR</button>
   <button class="ctrl" id="rightLight">LIGHT OFF</button>
  </section>

  <div id="hint">בדוק את החלונות, הקשב לצלילים ושמור על החשמל.</div>

  <section id="camScreen">
   <div id="camHeader">
    <div>
     <div id="camName">CAM 01 / SHOWROOM</div>
     <div id="signal">LIVE FEED · SIGNAL STABLE</div>
    </div>
    <button class="smallBtn" id="closeCam">✕ סגור מצלמות</button>
   </div>
   <div id="camBody">
    <canvas id="camCanvas"></canvas>
    <div id="camOverlay">
     <div class="camStamp">SECURITY FEED · REC</div>
     <div class="camStamp" id="camFoot">NIGHT 01 · LIVE</div>
    </div>
    <aside id="map">
     <div class="mapTitle">FACILITY MAP</div>
     <div id="mapGrid"></div>
    </aside>
   </div>
   <div id="camBottom">
    <button class="arrow" id="prevCam" aria-label="המצלמה הקודמת">◀</button>
    <div id="camCurrent">CAM 01 / SHOWROOM</div>
    <button class="arrow" id="nextCam" aria-label="המצלמה הבאה">▶</button>
   </div>
   <div id="camActions">
    <button class="smallBtn" id="mapToggle">מפה: הצג / הסתר</button>
    <span class="sub">9 CAMERAS · USE ARROWS</span>
   </div>
  </section>

  <div id="toast"></div>

  <div id="scare">
   <canvas id="scareCanvas"></canvas>
  </div>

  <div id="overlay">
   <div class="dialog">
    <div class="eyebrow">NIGHT SECURITY DIVISION</div>
    <h1>THE LAST<br>WATCH</h1>
    <div class="desc">חמישה לילות. משרד אחד. ארבעה דברים שלא אמורים לזוז.</div>
    <div id="overlayMsg">שרוד מ־12 AM עד 6 AM. בדוק מצלמות, האזן לצעדים והשתמש בדלתות בחוכמה.</div>
    <div class="menuButtons">
     <button class="menuBtn primary" id="startBtn">התחל לילה 1</button>
     <button class="menuBtn" id="continueBtn">CONTINUE · המשך</button>
     <button class="menuBtn" id="howBtn">איך שורדים?</button>
    </div>
    <div class="desc" style="margin-top:14px">התקדמות הלילות נשמרת במכשיר זה.</div>
   </div>
  </div>
 </main>

 <footer id="bottom">
  <button class="bottomBtn" id="soundBtn">♫ צליל: פעיל</button>
  <button class="bottomBtn primary" id="cameraBtn">▣ פתח מצלמות</button>
  <button class="bottomBtn" id="pauseBtn">Ⅱ השהה</button>
 </footer>
</div>

<script>
(() => {
"use strict";

const $ = id => document.getElementById(id);
const scene = $("scene"), ctx = scene.getContext("2d");
const camCanvas = $("camCanvas"), cctx = camCanvas.getContext("2d");
const scareCanvas = $("scareCanvas"), sctx = scareCanvas.getContext("2d");

const CAMS = [
 {name:"SHOWROOM", short:"SHOWROOM",x:0,y:0},
 {name:"STORAGE",short:"STORAGE",x:1,y:0},
 {name:"WORKSHOP",short:"WORKSHOP",x:2,y:0},
 {name:"DINING",short:"DINING",x:0,y:1},
 {name:"WEST HALL",short:"WEST HALL",x:1,y:1},
 {name:"EAST HALL",short:"EAST HALL",x:2,y:1},
 {name:"BACK ROOM",short:"BACK ROOM",x:0,y:2},
 {name:"STAGE",short:"STAGE",x:1,y:2},
 {name:"LOADING BAY",short:"LOADING",x:2,y:2}
];

const MONSTERS = [
 {name:"THE WARDEN",type:0,route:[0,2,5,8,5,"right"],color:"#819b91",speed:1},
 {name:"TIN JACK",type:1,route:[1,3,6,4,"left"],color:"#b5a16c",speed:1.1},
 {name:"THE HOLLOW",type:2,route:[7,2,1,4,"left"],color:"#a3a8bd",speed:.86},
 {name:"MOTHER STATIC",type:3,route:[6,3,0,5,"right"],color:"#a66f79",speed:.77}
];

const state = {
 running:false,paused:false,night:1,unlocked:1,power:100,
 elapsed:0,duration:90,camOpen:false,camIndex:0,mapOpen:true,
 leftDoor:false,rightDoor:false,leftLight:false,rightLight:false,
 sound:true,usage:1,toastTimer:0,ending:false,
 enemies:[],lastTick:0,raf:0
};

let audio=null,master=null,noiseBuffer=null;
let lastFrame=0,blink=0,shake=0;
let bestNight=1;

try{
 bestNight=Math.max(1,Math.min(5,Number(localStorage.getItem("lastWatchUnlocked")||1)));
}catch(e){bestNight=1}
state.unlocked=bestNight;

function saveProgress(){
 try{
  localStorage.setItem("lastWatchUnlocked",String(state.unlocked));
 }catch(e){}
 updateMenu();
}
function updateMenu(){
 $("continueBtn").textContent="CONTINUE · המשך מלילה "+String(state.unlocked).padStart(2,"0");
 $("startBtn").textContent="התחל לילה "+state.unlocked;
 $("continueBtn").disabled=state.unlocked<1;
}
function initAudio(){
 if(audio)return;
 try{
  audio=new (window.AudioContext||window.webkitAudioContext)();
  master=audio.createGain();master.gain.value=state.sound?.65:0;master.connect(audio.destination);
  const len=audio.sampleRate;
  noiseBuffer=audio.createBuffer(1,len,audio.sampleRate);
  const d=noiseBuffer.getChannelData(0);
  for(let i=0;i<len;i++)d[i]=Math.random()*2-1;
 }catch(e){audio=null}
}
function tone(freq=100,dur=.15,type="sine",vol=.12){
 if(!state.sound)return;
 initAudio();if(!audio)return;
 if(audio.state==="suspended")audio.resume().catch(()=>{});
 const o=audio.createOscillator(),g=audio.createGain();
 o.type=type;o.frequency.setValueAtTime(freq,audio.currentTime);
 g.gain.setValueAtTime(vol,audio.currentTime);
 g.gain.exponentialRampToValueAtTime(.001,audio.currentTime+dur);
 o.connect(g);g.connect(master);o.start();o.stop(audio.currentTime+dur+.02);
}
function hiss(dur=.3,vol=.1){
 if(!state.sound)return;
 initAudio();if(!audio||!noiseBuffer)return;
 const src=audio.createBufferSource(),g=audio.createGain();
 src.buffer=noiseBuffer;g.gain.value=vol;src.connect(g);g.connect(master);
 src.start();g.gain.exponentialRampToValueAtTime(.001,audio.currentTime+dur);
 src.stop(audio.currentTime+dur+.02);
}
function setSound(on){
 state.sound=on;
 if(master)master.gain.setTargetAtTime(on?.65:0,audio.currentTime,.03);
 $("soundBtn").textContent=on?"♫ צליל: פעיל":"♫ צליל: כבוי";
}
function fitCanvas(c,context){
 const r=c.getBoundingClientRect();if(!r.width||!r.height)return;
 const dpr=Math.min(window.devicePixelRatio||1,2);
 const w=Math.round(r.width*dpr),h=Math.round(r.height*dpr);
 if(c.width!==w||c.height!==h){c.width=w;c.height=h}
 context.setTransform(dpr,0,0,dpr,0,0);
 return {w:r.width,h:r.height};
}
function rect(c,x,y,w,h,color){
 c.fillStyle=color;c.fillRect(x,y,w,h);
}
function line(c,x1,y1,x2,y2,color,width=1){
 c.strokeStyle=color;c.lineWidth=width;c.beginPath();c.moveTo(x1,y1);c.lineTo(x2,y2);c.stroke();
}
function text(c,t,x,y,size,color="#d8e3de",align="left"){
 c.fillStyle=color;c.font=`${size}px Arial`;c.textAlign=align;c.fillText(t,x,y);
}
function gradient(c,x,y,w,h,a,b){
 const g=c.createLinearGradient(x,y,x,y+h);g.addColorStop(0,a);g.addColorStop(1,b);
 rect(c,x,y,w,h,g);
}
function roundRect(c,x,y,w,h,r,color,stroke){
 c.beginPath();c.roundRect(x,y,w,h,r);c.fillStyle=color;c.fill();
 if(stroke){c.strokeStyle=stroke;c.lineWidth=2;c.stroke()}
}
function drawFloor(c,w,h){
 const y=h*.70;
 gradient(c,0,y,w,h-y,"#17201f","#070a0b");
 line(c,0,y,w,y,"#64736d",2);
 for(let i=-4;i<=4;i++){
  line(c,w/2+i*w*.055,y,w/2+i*w*.20,h,"#293432",1);
 }
 for(let i=1;i<5;i++){
  const yy=y+(h-y)*i/5;
  line(c,0,yy,w,yy,"#1c2725",1);
 }
}
function drawDoor(c,x,y,w,h,closed,side,light,monsterPresent){
 // Door frame remains visible and controls never cover its central opening.
 rect(c,x-4,y-4,w+8,h+8,"#6c7a76");
 rect(c,x,y,w,h,"#080c0d");
 rect(c,x+5,y+5,w-10,h-10,"#1b2524");
 for(let i=1;i<4;i++)line(c,x+i*w/4,y+6,x+i*w/4,y+h-6,"#263330",2);
 line(c,x+5,y+h*.78,x+w-5,y+h*.78,"#3d4a46",2);
 if(light){
  gradient(c,x+4,y+4,w-8,h-8,"#c0c9a6","#343c2d");
  if(monsterPresent)drawMonster(c,monsterPresent,x+w/2,y+h*.56,Math.min(w*.68,h*.40),false);
  else{
   rect(c,x+w*.25,y+h*.26,w*.5,h*.32,"#9aa69a22");
   line(c,x+w*.5,y+h*.2,x+w*.5,y+h*.7,"#7f9287",2);
  }
 }else{
  rect(c,x+5,y+5,w-10,h-10,"#080c0d");
  if(monsterPresent && closed){
   // A silhouette behind the separate narrow inspection window.
   rect(c,x+w*.17,y+h*.15,w*.66,h*.26,"#151c1c");
   drawMonster(c,monsterPresent,x+w/2,y+h*.34,Math.min(w*.5,h*.20),false);
  }
 }
 if(closed){
  rect(c,x-2,y+h*.20,w+4,h*.58,"#52615d");
  rect(c,x+4,y+h*.20+5,w-8,h*.58-10,"#293331");
  for(let yy=y+h*.25;yy<y+h*.73;yy+=9)line(c,x+5,yy,x+w-5,yy,"#43514c",1);
 }
}
function drawMonster(c,m,x,y,size,scary=true){
 const r=size;
 c.save();c.translate(x,y);
 const flick=scary?(Math.random()-.5)*2:0;
 c.translate(flick,0);
 if(m.type===0){
  // Tall armored Warden
  roundRect(c,-r*.27,-r*.9,r*.54,r*1.18,r*.07,"#34453f","#94a89c");
  roundRect(c,-r*.31,-r*.76,r*.62,r*.35,r*.04,"#111716","#74867c");
  rect(c,-r*.20,-r*.64,r*.12,r*.055,"#ff4545");
  rect(c,r*.07,-r*.64,r*.12,r*.055,"#ff4545");
  line(c,-r*.18,-r*.2,-r*.38,r*.52,"#687b71",r*.11);
  line(c,r*.18,-r*.2,r*.37,r*.55,"#687b71",r*.11);
  line(c,-r*.12,r*.22,-r*.2,r*.85,"#7b8b81",r*.13);
  line(c,r*.12,r*.22,r*.22,r*.85,"#7b8b81",r*.13);
 }else if(m.type===1){
  // Tin Jack: square head, long mechanical arms
  roundRect(c,-r*.36,-r*.82,r*.72,r*.55,r*.03,"#87774d","#c9b47a");
  rect(c,-r*.24,-r*.65,r*.13,r*.09,"#ff2b36");
  rect(c,r*.11,-r*.65,r*.13,r*.09,"#ff2b36");
  rect(c,-r*.22,-r*.19,r*.44,r*.52,"#645d45");
  line(c,-r*.2,-r*.05,-r*.56,r*.49,"#b6a46d",r*.09);
  line(c,r*.2,-r*.05,r*.56,r*.49,"#b6a46d",r*.09);
  line(c,-r*.12,r*.28,-r*.18,r*.82,"#9c8b5c",r*.13);
  line(c,r*.12,r*.28,r*.18,r*.82,"#9c8b5c",r*.13);
 }else if(m.type===2){
  // Hollow: hunched creature with a long head
  c.fillStyle="#626c85";c.beginPath();c.ellipse(0,-r*.48,r*.29,r*.45,0,0,Math.PI*2);c.fill();
  c.fillStyle="#151921";c.beginPath();c.ellipse(0,-r*.16,r*.35,r*.46,0,0,Math.PI*2);c.fill();
  line(c,-r*.2,-r*.16,-r*.45,r*.44,"#77819a",r*.12);
  line(c,r*.2,-r*.16,r*.46,r*.42,"#77819a",r*.12);
  line(c,-r*.12,r*.12,-r*.27,r*.77,"#707c94",r*.1);
  line(c,r*.12,r*.12,r*.27,r*.77,"#707c94",r*.1);
  rect(c,-r*.16,-r*.58,r*.1,r*.07,"#e8e4d4");
  rect(c,r*.06,-r*.58,r*.1,r*.07,"#e8e4d4");
 }else{
  // Mother Static: wide hood and cracked porcelain mask
  c.fillStyle="#67434c";c.beginPath();c.moveTo(-r*.48,-r*.13);c.lineTo(-r*.34,-r*.88);
  c.lineTo(r*.34,-r*.88);c.lineTo(r*.48,-r*.13);c.lineTo(r*.31,r*.55);
  c.lineTo(-r*.31,r*.55);c.closePath();c.fill();
  roundRect(c,-r*.25,-r*.72,r*.5,r*.43,r*.04,"#d0c2b4","#887e7c");
  rect(c,-r*.17,-r*.59,r*.10,r*.06,"#170e13");
  rect(c,r*.07,-r*.59,r*.10,r*.06,"#170e13");
  line(c,-r*.06,-r*.45,r*.12,-r*.37,"#534047",2);
  line(c,-r*.1,-r*.35,r*.13,-r*.35,"#39262d",2);
  line(c,-r*.2,-r*.05,-r*.45,r*.5,"#95616b",r*.1);
  line(c,r*.2,-r*.05,r*.45,r*.5,"#95616b",r*.1);
  line(c,-r*.1,r*.3,-r*.2,r*.8,"#95616b",r*.12);
  line(c,r*.1,r*.3,r*.2,r*.8,"#95616b",r*.12);
 }
 c.restore();
}
function currentRoomMonster(room){
 return state.enemies.find(m=>m.room===room&&!m.dead)||null;
}
function drawOffice(){
 const dim=fitCanvas(scene,ctx);if(!dim)return;
 const w=dim.w,h=dim.h;
 ctx.clearRect(0,0,w,h);
 gradient(ctx,0,0,w,h,"#121b1b","#06090a");
 // ceiling, back wall, floor and perspective
 ctx.fillStyle="#080d0e";ctx.beginPath();ctx.moveTo(0,0);ctx.lineTo(w,0);
 ctx.lineTo(w*.82,h*.38);ctx.lineTo(w*.18,h*.38);ctx.closePath();ctx.fill();
 ctx.fillStyle="#1a2423";ctx.beginPath();ctx.moveTo(w*.18,h*.38);
 ctx.lineTo(w*.82,h*.38);ctx.lineTo(w*.92,h*.73);ctx.lineTo(w*.08,h*.73);ctx.closePath();ctx.fill();
 for(let i=0;i<=8;i++){
  const x=w*.18+(w*.64*i/8);line(ctx,x,h*.38,w/2+(x-w/2)*1.4,h*.73,"#2b3733",1);
 }
 drawFloor(ctx,w,h);
 // Both doorways are tall and wide; controls live above their frames.
 const doorY=h*.39,doorH=h*.34,doorW=Math.min(w*.16,105);
 const leftX=w*.035,rightX=w-doorW-w*.035;
 const leftM=currentRoomMonster("left"),rightM=currentRoomMonster("right");
 drawDoor(ctx,leftX,doorY,doorW,doorH,state.leftDoor,"left",state.leftLight,leftM);
 drawDoor(ctx,rightX,doorY,doorW,doorH,state.rightDoor,"right",state.rightLight,rightM);
 // Side inspection windows beside the door frames, not underneath the buttons.
 const winW=Math.min(w*.12,74),winH=h*.23,winY=h*.43;
 const leftWinX=leftX+doorW+6,rightWinX=rightX-winW-6;
 drawWindow(ctx,leftWinX,winY,winW,winH,state.leftLight,leftM);
 drawWindow(ctx,rightWinX,winY,winW,winH,state.rightLight,rightM);
 // Labels above the doors, never over the opening.
 text(ctx,"WEST",leftX+doorW/2,doorY-10,10,"#c4d1ca","center");
 text(ctx,"EAST",rightX+doorW/2,doorY-10,10,"#c4d1ca","center");
 // Central security desk
 const deskY=h*.72;
 ctx.fillStyle="#0c1212";ctx.beginPath();ctx.moveTo(w*.27,deskY);
 ctx.lineTo(w*.73,deskY);ctx.lineTo(w*.82,h*.91);ctx.lineTo(w*.18,h*.91);ctx.closePath();ctx.fill();
 line(ctx,w*.27,deskY,w*.73,deskY,"#52615c",3);
 rect(ctx,w*.40,h*.70,w*.20,h*.10,"#263431");
 rect(ctx,w*.415,h*.712,w*.17,h*.07,"#0c1212");
 text(ctx,"SECURITY TERMINAL",w*.5,h*.755,Math.max(8,w*.018),"#9bada5","center");
 text(ctx,"OFFICE / YOU",w*.5,h*.84,10,"#61756b","center");
 // status indicator
 if(state.leftLight||state.rightLight){
  ctx.fillStyle="#d9d7a4";ctx.beginPath();ctx.arc(w*.5,h*.65,3,0,Math.PI*2);ctx.fill();
 }
 // Camera screen is closed only when camOpen=false; no invisible overlay.
}
function drawWindow(c,x,y,w,h,light,m){
 rect(c,x-3,y-3,w+6,h+6,"#73817b");
 rect(c,x,y,w,h,light?"#777c5d":"#070b0c");
 if(light){
  gradient(c,x+2,y+2,w-4,h-4,"#b7bf9a","#3a4335");
  if(m)drawMonster(c,m,x+w/2,y+h*.73,Math.min(w*.78,h*.7),false);
  else{
   line(c,x+w*.5,y+4,x+w*.5,y+h-4,"#7e8a72",2);
   line(c,x+3,y+h*.55,x+w-3,y+h*.55,"#9aa48b",1);
  }
 }else{
  rect(c,x+3,y+3,w-6,h-6,"#0b1010");
  if(m){
   // Small shadow can be spotted through the glass in darkness.
   c.globalAlpha=.45;drawMonster(c,m,x+w/2,y+h*.68,Math.min(w*.68,h*.55),false);c.globalAlpha=1;
  }else{
   line(c,x+w*.5,y+4,x+w*.5,y+h-4,"#1c2825",1);
   line(c,x+3,y+h*.55,x+w-3,y+h*.55,"#1c2825",1);
  }
 }
}
function drawCamRoom(){
 const dim=fitCanvas(camCanvas,cctx);if(!dim)return;
 const w=dim.w,h=dim.h;const cam=CAMS[state.camIndex];
 cctx.clearRect(0,0,w,h);
 // Each camera has a visibly distinct room layout.
 gradient(cctx,0,0,w,h,"#172120","#050809");
 // Scanlines
 for(let y=0;y<h;y+=4)line(cctx,0,y,w,y,"#ffffff08",1);
 const seed=state.camIndex;
 if(seed===0||seed===7){
  // Stage/showroom
  rect(cctx,w*.08,h*.2,w*.84,h*.47,"#222e2b");
  rect(cctx,w*.15,h*.14,w*.7,h*.10,"#36443d");
  for(let i=0;i<5;i++)rect(cctx,w*(.18+i*.14),h*.25,w*.08,h*.32,"#111817");
  line(cctx,w*.08,h*.68,w*.92,h*.68,"#9a8770",3);
  for(let i=0;i<6;i++)line(cctx,w*(.1+i*.16),h*.68,w*(.05+i*.18),h*.94,"#46524b",1);
 }else if(seed===1||seed===8){
  // Storage/loading: boxes and shelves
  for(let i=0;i<4;i++){
   const x=w*(.08+i*.22);
   rect(cctx,x,h*.2,w*.17,h*.55,"#28312d");
   rect(cctx,x+3,h*.22,w*.15,h*.13,"#414b42");
   rect(cctx,x+3,h*.39,w*.15,h*.15,"#19211e");
   rect(cctx,x+3,h*.58,w*.15,h*.14,"#333b35");
  }
  line(cctx,0,h*.78,w,h*.78,"#718078",2);
 }else if(seed===2){
  // Workshop
  rect(cctx,w*.08,h*.25,w*.84,h*.08,"#3d4941");
  for(let i=0;i<4;i++){
   rect(cctx,w*(.13+i*.2),h*.34,w*.12,h*.42,"#242d2a");
   line(cctx,w*(.13+i*.2),h*.46,w*(.25+i*.2),h*.46,"#6b7770",2);
  }
  rect(cctx,w*.3,h*.76,w*.4,h*.08,"#48544c");
 }else if(seed===3){
  // Dining tables
  for(let i=0;i<3;i++){
   const x=w*(.16+i*.28);
   rect(cctx,x,h*.43,w*.18,h*.06,"#606454");
   rect(cctx,x+w*.025,h*.49,w*.025,h*.20,"#4a5147");
   rect(cctx,x+w*.13,h*.49,w*.025,h*.20,"#4a5147");
   rect(cctx,x+w*.04,h*.32,w*.10,h*.09,"#202925");
  }
 }else if(seed===4||seed===5){
  // Halls
  cctx.fillStyle="#222c29";cctx.beginPath();cctx.moveTo(w*.18,0);cctx.lineTo(w*.82,0);
  cctx.lineTo(w*.62,h);cctx.lineTo(w*.38,h);cctx.closePath();cctx.fill();
  for(let i=0;i<6;i++)line(cctx,w*(.18+i*.13),0,w/2+(i-2.5)*w*.12,h,"#37423d",2);
  rect(cctx,w*.40,h*.17,w*.2,h*.19,"#080c0c");
 }else{
  // Back room
  for(let i=0;i<4;i++){
   rect(cctx,w*(.06+i*.23),h*.20,w*.19,h*.62,"#222c29");
   rect(cctx,w*(.08+i*.23),h*.24,w*.15,h*.12,"#3d4740");
  }
  line(cctx,0,h*.82,w,h*.82,"#56635b",2);
 }
 // camera interference, occasionally
 if(Math.random()<.025){
  rect(cctx,0,Math.random()*h,w,2,"#dbe9df55");
 }
 // Enemies shown only if they are actually in this camera room
 const occupants=state.enemies.filter(m=>m.room===state.camIndex);
 occupants.forEach((m,i)=>{
  const x=w*(.35+i*.3),y=h*.72;
  drawMonster(cctx,m,x,y,Math.min(w*.22,h*.38),false);
 });
 // Noise vignette
 const vg=cctx.createRadialGradient(w/2,h/2,h*.1,w/2,h/2,h*.8);
 vg.addColorStop(0,"#00000000");vg.addColorStop(1,"#000000a8");
 cctx.fillStyle=vg;cctx.fillRect(0,0,w,h);
}
function buildMap(){
 const grid=$("mapGrid");grid.innerHTML="";
 CAMS.forEach((cam,i)=>{
  const b=document.createElement("button");
  b.className="mapCell";
  b.innerHTML="CAM "+String(i+1).padStart(2,"0")+"<br>"+cam.short;
  b.addEventListener("click",()=>selectCam(i));
  grid.appendChild(b);
 });
 const you=document.createElement("div");
 you.className="mapCell you";you.textContent="● YOU — SECURITY OFFICE";
 grid.appendChild(you);
}
function updateMap(){
 const cells=$("mapGrid").querySelectorAll(".mapCell");
 CAMS.forEach((cam,i)=>{
  const cell=cells[i];if(!cell)return;
  cell.classList.toggle("selected",i===state.camIndex);
  const danger=state.enemies.some(m=>m.room===i);
  cell.classList.toggle("danger",danger);
  cell.innerHTML="CAM "+String(i+1).padStart(2,"0")+"<br>"+cam.short+(danger?"<br>⚠ MOVEMENT":"");
 });
}
function selectCam(i){
 state.camIndex=(i+CAMS.length)%CAMS.length;
 updateCamUI();tone(480,.05,"square",.035);
}
function updateCamUI(){
 const cam=CAMS[state.camIndex];
 $("camName").textContent="CAM "+String(state.camIndex+1).padStart(2,"0")+" / "+cam.name;
 $("camCurrent").textContent="CAM "+String(state.camIndex+1).padStart(2,"0")+" / "+cam.name;
 $("signal").textContent=state.power<=0?"SIGNAL LOST": "LIVE FEED · SIGNAL STABLE";
 $("camFoot").textContent="NIGHT "+String(state.night).padStart(2,"0")+" · LIVE";
 updateMap();
}
function toggleCams(force){
 if(!state.running||state.paused||state.ending||state.power<=0)return;
 state.camOpen=typeof force==="boolean"?force:!state.camOpen;
 $("camScreen").classList.toggle("open",state.camOpen);
 $("cameraBtn").textContent=state.camOpen?"▣ סגור מצלמות":"▣ פתח מצלמות";
 $("leftControls").style.display=state.camOpen?"none":"flex";
 $("rightControls").style.display=state.camOpen?"none":"flex";
 $("hint").style.display=state.camOpen?"none":"block";
 tone(state.camOpen?320:240,.09,"square",.06);
 if(state.camOpen)updateCamUI();
}
function toast(msg){
 const el=$("toast");el.textContent=msg;el.style.display="block";
 state.toastTimer=2.2;
}
function makeEnemies(){
 state.enemies=MONSTERS.map((base,i)=>({
  ...base,room:base.route[0],step:0,moveTimer:5+i*3,
  attackTimer:0,doorSide:null,atDoor:false,dead:false,
  interval:Math.max(2.5,8-state.night*.85-i*.3),
  seed:Math.random()*10
 }));
}
function startNight(n){
 initAudio();
 state.night=Math.max(1,Math.min(5,n));
 state.running=true;state.paused=false;state.ending=false;
 state.power=100;state.elapsed=0;state.duration=Math.max(65,100-(state.night-1)*7);
 state.camOpen=false;state.camIndex=0;state.mapOpen=true;
 state.leftDoor=false;state.rightDoor=false;state.leftLight=false;state.rightLight=false;
 makeEnemies();
 $("overlay").classList.add("hidden");
 $("scare").classList.remove("show");
 $("camScreen").classList.remove("open");
 $("map").classList.add("show");
 $("nightLabel").textContent=String(state.night).padStart(2,"0");
 $("status").textContent="SYSTEM ONLINE · NIGHT "+String(state.night).padStart(2,"0");
 $("leftControls").style.display="flex";$("rightControls").style.display="flex";
 $("hint").style.display="block";
 $("cameraBtn").textContent="▣ פתח מצלמות";
 $("pauseBtn").textContent="Ⅱ השהה";
 updateCamUI();updateDoors();tone(440,.12,"sine",.06);
 lastFrame=performance.now();
}
function endNight(won){
 if(state.ending)return;
 state.ending=true;state.running=false;state.camOpen=false;
 $("camScreen").classList.remove("open");
 $("leftControls").style.display="flex";$("rightControls").style.display="flex";
 if(won){
  tone(660,.2,"sine",.08);setTimeout(()=>tone(880,.35,"sine",.08),220);
  if(state.night<5)state.unlocked=Math.max(state.unlocked,state.night+1);
  else state.unlocked=5;
  saveProgress();
  $("overlayMsg").innerHTML=state.night===5?
   "שרדַת את כל חמשת הלילות. המשמרת הסתיימה — אבל המצלמות עדיין פועלות.":
   "שרדת את לילה "+state.night+"! הלילה הבא נפתח. החשמל והאויבים יתאפסו.";
  $("startBtn").textContent=state.night===5?"שחק שוב — לילה 1":"התחל לילה "+(state.night+1);
  $("startBtn").onclick=()=>startNight(state.night===5?1:state.night+1);
 }else{
  $("overlayMsg").textContent="המשמרת הסתיימה. בדוק מאיפה המפלצת הגיעה ונסה שוב. ההתקדמות שלך בלילות שנפתחו נשמרת.";
  $("startBtn").textContent="נסה שוב — לילה "+state.night;
  $("startBtn").onclick=()=>startNight(state.night);
  $("status").textContent="SYSTEM FAILURE";
 }
 $("overlay").classList.remove("hidden");
 $("continueBtn").onclick=()=>startNight(state.unlocked);
 $("continueBtn").textContent="CONTINUE · לילה "+state.unlocked;
}
function jumpscare(m){
 if(state.ending)return;
 state.ending=true;state.running=false;state.camOpen=false;
 $("camScreen").classList.remove("open");
 $("scare").classList.add("show");
 drawScare(m);
 // layered original sound; WebAudio, no external files required
 tone(75,.9,"sawtooth",.35);hiss(.8,.32);
 setTimeout(()=>tone(48,.7,"square",.3),100);
 setTimeout(()=>tone(900,.25,"sawtooth",.22),180);
 setTimeout(()=>{
  $("scare").classList.remove("show");
  endNight(false);
 },1250);
}
function drawScare(m){
 const d=fitCanvas(scareCanvas,sctx);if(!d)return;
 const w=d.w,h=d.h;
 rect(sctx,0,0,w,h,"#020303");
 // strong backlight and huge detailed monster silhouette
 const g=sctx.createRadialGradient(w/2,h*.45,4,w/2,h*.45,w*.65);
 g.addColorStop(0,m.color);g.addColorStop(.45,"#211b1e");g.addColorStop(1,"#000");
 sctx.fillStyle=g;sctx.fillRect(0,0,w,h);
 // Face grows from the bottom, unique to each monster
 drawMonster(sctx,m,w/2,h*.68,Math.min(w*.94,h*1.12),true);
 const red=sctx.createRadialGradient(w/2,h*.45,3,w/2,h*.45,w*.5);
 red.addColorStop(0,"#ffdddd22");red.addColorStop(1,"#000000bb");
 sctx.fillStyle=red;sctx.fillRect(0,0,w,h);
 text(sctx,"TOO LATE",w/2,h*.12,Math.max(18,w*.075),"#fff","center");
}
function updateDoors(){
 $("leftDoor").textContent=state.leftDoor?"OPEN DOOR":"CLOSE DOOR";
 $("leftDoor").classList.toggle("active",state.leftDoor);
 $("rightDoor").textContent=state.rightDoor?"OPEN DOOR":"CLOSE DOOR";
 $("rightDoor").classList.toggle("active",state.rightDoor);
 $("leftLight").textContent=state.leftLight?"LIGHT ON":"LIGHT OFF";
 $("leftLight").classList.toggle("lit",state.leftLight);
 $("rightLight").textContent=state.rightLight?"LIGHT ON":"LIGHT OFF";
 $("rightLight").classList.toggle("lit",state.rightLight);
}
function toggleDoor(side){
 if(!state.running||state.paused||state.camOpen||state.power<=0)return;
 const key=side+"Door";state[key]=!state[key];
 tone(state[key]?120:220,.08,"square",.05);
 updateDoors();
}
function toggleLight(side){
 if(!state.running||state.paused||state.camOpen||state.power<=0)return;
 const key=side+"Light";state[key]=!state[key];
 tone(state[key]?520:260,.05,"sine",.035);
 updateDoors();
}
function advanceEnemy(m,dt){
 if(m.dead||state.ending)return;
 m.moveTimer-=dt;
 if(m.room==="left"||m.room==="right"){
  m.atDoor=true;m.attackTimer+=dt;
  const closed=m.room==="left"?state.leftDoor:state.rightDoor;
  if(closed){
   if(m.attackTimer>1.3){
    // Door blocks attack; creature retreats after banging.
    tone(55,.15,"square",.13);
    m.step=Math.max(0,m.step-1);
    m.room=m.route[m.step]===undefined?m.route[0]:m.route[m.step];
    m.attackTimer=0;m.atDoor=false;
   }
  }else{
   if(m.attackTimer>Math.max(1.5,4.2-state.night*.45))jumpscare(m);
  }
  return;
 }
 if(m.moveTimer>0)return;
 const camOpenBonus=state.camOpen&&state.camIndex===m.room?.4:1;
 const chance=Math.min(.95,(.27+state.night*.105)*m.speed*camOpenBonus);
 m.moveTimer=m.interval*(.72+Math.random()*.7);
 if(Math.random()>chance)return;
 // A camera can slow an enemy briefly if it is watched.
 if(state.camOpen&&state.camIndex===m.room&&Math.random()<.28){
  toast("תנועה זוהתה: "+m.name);m.moveTimer+=2;return;
 }
 m.step++;
 if(m.step>=m.route.length)m.step=0;
 m.room=m.route[m.step];
 m.atDoor=false;m.attackTimer=0;
 if(m.room==="left"||m.room==="right"){
  tone(70,.13,"sine",.07);
  toast("תנועה ליד דלת "+(m.room==="left"?"שמאל":"ימין")+"!");
 }
}
function update(dt){
 if(!state.running||state.paused||state.ending)return;
 state.elapsed+=dt;
 // Every extra system consumes power; avoid impossible power drain.
 let usage=1;
 if(state.leftDoor)usage+=1;
 if(state.rightDoor)usage+=1;
 if(state.leftLight)usage+=.45;
 if(state.rightLight)usage+=.45;
 if(state.camOpen)usage+=.55;
 state.power=Math.max(0,state.power-dt*(.085+usage*.018+state.night*.003));
 if(state.power<=0){
  state.leftDoor=false;state.rightDoor=false;
  state.leftLight=false;state.rightLight=false;
  if(Math.random()<dt*.6)jumpscare(state.enemies[Math.floor(Math.random()*state.enemies.length)]);
 }
 state.enemies.forEach(m=>advanceEnemy(m,dt));
 const hour=Math.min(6,Math.floor(state.elapsed/state.duration*6));
 $("clock").textContent=(hour===0?"12":hour)+" AM";
 $("power").textContent=Math.ceil(state.power)+"%";
 $("powerbar").style.width=state.power+"%";
 $("powerbar").style.background=state.power<25?"#d86b71":state.power<50?"#d7b875":"#b5d5c2";
 if(state.toastTimer>0){state.toastTimer-=dt;if(state.toastTimer<=0)$("toast").style.display="none"}
 updateDoors();
 if(state.camOpen){updateCamUI();drawCamRoom()}
 if(state.elapsed>=state.duration)endNight(true);
}
function render(){
 if(state.camOpen)drawCamRoom();else drawOffice();
}
function loop(now){
 const dt=Math.min(.06,Math.max(0,(now-lastFrame)/1000||0));
 lastFrame=now;update(dt);render();
 state.raf=requestAnimationFrame(loop);
}
function showHow(){
 $("overlayMsg").innerHTML=
 "1. בדוק את שני החלונות בעזרת האורות.<br>"+
 "2. אם מפלצת מגיעה לדלת, סגור אותה בזמן.<br>"+
 "3. פתח מצלמות כדי לבדוק חדרים ולראות את המפה.<br>"+
 "4. דלתות, אורות ומצלמות צורכים חשמל — אל תשאיר אותם פועלים סתם.<br>"+
 "5. שרוד עד 6 AM. כל לילה נעשה קשה יותר.";
}
$("leftDoor").addEventListener("click",()=>toggleDoor("left"));
$("rightDoor").addEventListener("click",()=>toggleDoor("right"));
$("leftLight").addEventListener("click",()=>toggleLight("left"));
$("rightLight").addEventListener("click",()=>toggleLight("right"));
$("cameraBtn").addEventListener("click",()=>toggleCams());
$("closeCam").addEventListener("click",()=>toggleCams(false));
$("prevCam").addEventListener("click",()=>selectCam(state.camIndex-1));
$("nextCam").addEventListener("click",()=>selectCam(state.camIndex+1));
$("mapToggle").addEventListener("click",()=>{
 state.mapOpen=!state.mapOpen;$("map").classList.toggle("show",state.mapOpen);
});
$("soundBtn").addEventListener("click",()=>setSound(!state.sound));
$("pauseBtn").addEventListener("click",()=>{
 if(!state.running||state.ending)return;
 state.paused=!state.paused;
 $("pauseBtn").textContent=state.paused?"▶ המשך":"Ⅱ השהה";
 $("status").textContent=state.paused?"GAME PAUSED":"SYSTEM ONLINE · NIGHT "+String(state.night).padStart(2,"0");
 if(state.paused)toast("המשחק מושהה");else tone(320,.08,"sine",.04);
});
$("startBtn").addEventListener("click",()=>startNight(state.unlocked));
$("continueBtn").addEventListener("click",()=>startNight(state.unlocked));
$("howBtn").addEventListener("click",showHow);
window.addEventListener("resize",()=>{drawOffice();if(state.camOpen)drawCamRoom()});
document.addEventListener("visibilitychange",()=>{
 if(document.hidden&&state.running){state.paused=true;$("pauseBtn").textContent="▶ המשך"}
});
buildMap();updateMenu();updateDoors();
state.raf=requestAnimationFrame(loop);
})();
</script>
</body>
</html>

Game Source: THE LAST WATCH — Night Security

Creator: LunarPanda23

Libraries: none

Complexity: complex (815 lines, 34.6 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: the-last-watch-night-security-lunarpanda23" to link back to the original. Then publish at arcadelab.ai/publish.