import {NightTrapEngine,COLORS} from './engine.js'; import {assetUrl,resolveAssetTree} from './asset-resolver.js'; const $=id=>document.getElementById(id); const [data,images,sotc,audioCatalog,survivorRules]=await Promise.all(['data/game.json','data/images.json','data/sotc.json','data/audio.json','data/survivor.json'].map(async url=>{const r=await fetch(url);if(!r.ok)throw Error('Missing '+url);return r.json();})); resolveAssetTree(data);resolveAssetTree(images);resolveAssetTree(sotc);resolveAssetTree(audioCatalog);resolveAssetTree(survivorRules); const engine=new NightTrapEngine(data,(Date.now()>>>0)); const video=$('video');video.preload='auto';let mode='standby',paused=true,speed=1,lastWall=performance.now(),mediaKey='',switchToken=0,busy=false,bufferHold=false,activeClip=null,lastUI=0,sotcIndex=0,sotcRoom='Hall 1',sotcSelected=null,survivorIndex=0,survivorRoom='Hall 1',survivorScore=0,survivorPossible=0,survivorWave=1,survivorTime=0,survivorRoomIndices=Object.fromEntries(survivorRules.roomOrder.map(room=>[room,0])); const sotcRooms=Object.keys(sotc.rooms);sotc.flat=[sotc.intro,...sotcRooms.flatMap(room=>sotc.rooms[room])]; const fallbackSurvivorPool=data.rooms.flatMap(room=>room.sequence.map(id=>data.nodes[id]).filter(node=>node?.video?.clip).map(node=>({clip:node.video.clip,node:node.id,room:room.id}))).filter((item,index,pool)=>pool.findIndex(other=>other.clip===item.clip)===index); const survivorPoolNames={"Hall 1":"hallAAugs","Hall 2":"hallBAugs","Kitchen":"kitchenAugs","Bedroom":"bedroomAugs","Driveway":"drivewayAugs","Entryway":"entrywayAugs","Bathroom":"bathroomAugs","Living Room":"livingRoomAugs"}; const survivorPoolByRoom=Object.fromEntries(survivorRules.roomOrder.map(room=>{const entries=survivorRules.sourcePools?.[survivorPoolNames[room]]??[];const mapped=entries.filter(item=>item.media&&data.media[item.media]).map(item=>({clip:item.media,node:`survivor-prefab-${item.prefabId}`,room}));return [room,mapped.length?mapped:fallbackSurvivorPool.filter(item=>item.room===room)];})); const survivorPool=Object.values(survivorPoolByRoom).flat(); const survivorRoomCounts=Object.fromEntries(survivorRules.roomOrder.map(room=>[room,0])); const audioByName=Object.values(audioCatalog).reduce((map,clip)=>{(map[clip.name]??=[]).push(clip);return map;},{}); let fxVolume=.42,staticAudio=null,audioContext=null,videoBus=null,fxBus=null,ambientBus=null,ambientFilter=null,videoSource=null,fxBuffers=new Map(),duckTimer=0; function ensureAudio(){if(audioContext){if(audioContext.state==='suspended')audioContext.resume().catch(()=>{});video.muted=false;video.volume=Number($('volume').value);return audioContext;}const C=window.AudioContext||window.webkitAudioContext;if(!C)return null;audioContext=new C();fxBus=audioContext.createGain();ambientBus=audioContext.createGain();ambientFilter=audioContext.createBiquadFilter();ambientFilter.type='lowpass';ambientFilter.frequency.value=1800;ambientFilter.Q.value=.35;fxBus.gain.value=fxVolume;ambientBus.gain.value=fxVolume*.18;fxBus.connect(audioContext.destination);ambientBus.connect(ambientFilter);ambientFilter.connect(audioContext.destination);video.muted=false;video.volume=Number($('volume').value);audioContext.resume().catch(()=>{});return audioContext;} function primeAudio(){const ctx=ensureAudio();if(ctx?.state==='suspended')ctx.resume().catch(()=>{});} function duckVideo(){const level=Number($('volume').value);if(!videoBus){video.volume=level*.72;clearTimeout(duckTimer);duckTimer=setTimeout(()=>{video.volume=Number($('volume').value);},260);return;}const now=audioContext.currentTime;videoBus.gain.cancelScheduledValues(now);videoBus.gain.setTargetAtTime(level*.72,now,.012);clearTimeout(duckTimer);duckTimer=setTimeout(()=>{const t=audioContext?.currentTime;if(videoBus&&t!==undefined)videoBus.gain.setTargetAtTime(Number($('volume').value),t,.12);},260);} async function playFx(name,volume=1){const clip=audioByName[name]?.[0];if(!clip)return;const ctx=ensureAudio();duckVideo();if(!ctx){const sound=new Audio(clip.url);sound.volume=Math.max(0,Math.min(1,fxVolume*volume));sound.play().catch(()=>{});return sound;}if(ctx.state==='suspended')ctx.resume().catch(()=>{});let buffer=fxBuffers.get(clip.url);try{if(!buffer){buffer=await fetch(clip.url).then(r=>r.arrayBuffer()).then(bytes=>ctx.decodeAudioData(bytes));fxBuffers.set(clip.url,buffer);}const source=ctx.createBufferSource();const gain=ctx.createGain();gain.gain.value=Math.max(0,Math.min(1,volume));source.buffer=buffer;source.connect(gain);gain.connect(fxBus);source.start();return source;}catch{}} function startStatic(){const clip=audioByName.static?.[0];if(!clip||staticAudio)return;primeAudio();staticAudio=new Audio(clip.url);staticAudio.loop=true;staticAudio.volume=1;if(audioContext){const source=audioContext.createMediaElementSource(staticAudio);source.connect(ambientBus);ambientBus.gain.value=fxVolume*.18;}staticAudio.play().catch(()=>{});} function stopStatic(){if(!staticAudio)return;staticAudio.pause();staticAudio.currentTime=0;staticAudio=null;} const swatches=['#5ac2f1','#98d46b','#efa050','#c993e9','#ed7067','#efce6c']; function clock(t){return `${String(Math.floor(t/60)).padStart(2,'0')}:${String(Math.floor(t%60)).padStart(2,'0')}`;} function message(s){$('message').textContent=s;} const buttons=data.rooms.map((room,i)=>{ const b=document.createElement('button');b.className='camera';b.setAttribute('aria-label',`Camera ${i+1}: ${room.label}`); const stills=Object.values(data.nodes).filter(n=>n.room===room.id&&n.name==='stills'); const still=stills.find(n=>n.children?.length)||stills[0]; const preview=document.createElement('video');preview.className='preview';preview.muted=true;preview.playsInline=true;preview.loop=true;preview.preload='metadata';preview.hidden=true;b.append(preview); const img=document.createElement('img');img.src=images[still?.sprite]?.url??'';img.alt='';b.append(img); const label=document.createElement('span');label.textContent=room.label;const key=document.createElement('em');key.textContent=String(i+1);label.append(key);b.append(label); b.dataset.previewId='';b.onclick=()=>selectRoom(room.id);$('rooms').append(b);return b; }); const colorButtons=COLORS.map((name,i)=>{const b=document.createElement('button');b.textContent=name;b.style.setProperty('--swatch',swatches[i]);b.onclick=()=>{engine.selectColor(i+1);document.body.classList.remove('mobile-codes-open');playFx(name.toLowerCase(),.8);renderUI();};$('colors').append(b);return b;}); const suspectButtons=Array.from({length:7},(_,i)=>{const b=document.createElement('button');b.textContent=`Suspect ${i+1}`;b.onclick=()=>{playFx('optionselect2',.8);sotcSelected=i;b.classList.add('selected');suspectButtons.filter(x=>x!==b).forEach(x=>x.classList.remove('selected'));};$('suspects').append(b);return b;}); function selectRoom(id){ if(mode==='sotc'){playFx('page_turn',.8);sotcRoom=id;const list=sotc.rooms[id]??[];const next=list.map(clip=>sotc.flat.indexOf(clip)).find(i=>i>=0);if(next!==undefined)sotcIndex=next;renderFeed();renderUI();return;} if(mode==='survivor'){playFx('click');survivorRoom=id;renderFeed();renderUI();return;} playFx('click'); if(mode!=='game')return;engine.selectRoom(id);renderFeed();renderUI(); } function syncBrand(){const isSotc=['sotc','sotc-result','sotc-ending'].includes(mode);document.body.dataset.mode=isSotc?'sotc':'night';window.nightTrapVisuals?.setMode(mode);$('brandLogo').src=assetUrl(isSotc?'assets/images/scene-of-the-crime-logo.png':'assets/images/night-trap-logo.png');$('brandLogo').alt=isSotc?'Scene of the Crime':'Night Trap';$('brandEyebrow').textContent=isSotc?'CASE FILE // SCENE OF THE CRIME':'SCAT SURVEILLANCE SYSTEM';$('brandSubtitle').textContent=isSotc?'BONUS INVESTIGATION':'LOCAL EDITION';$('statusText').textContent=isSotc?'EVIDENCE REVIEW · PRIVATE BUILD':'OFFLINE · PRIVATE BUILD';$('cameraSectionLabel').textContent=isSotc?'EVIDENCE LOCATIONS':'CAMERA SELECT';$('cameraSectionHint').textContent=isSotc?'ROOMS 1–8':'KEYS 1–8';document.title=isSotc?'Scene of the Crime · Bonus Investigation':'Night Trap · Local Edition';} const mobilePreviewMode=()=>window.matchMedia('(max-width: 700px), (pointer: coarse)').matches; function updateCameraPreviews(){const slideshow=mobilePreviewMode();buttons.forEach((button,i)=>{const room=data.rooms[i].id;const preview=button.querySelector('video.preview');const img=button.querySelector('img');let view=null,live=false;if(mode==='game'&&!engine.forced){view=engine.view(room);live=!!view?.clip;}else if(mode==='survivor'){const pool=survivorPoolByRoom[room]??[];const item=pool[survivorRoomIndices[room]%Math.max(1,pool.length)];view=item?{clip:item.clip,time:0}:null;live=!!item&&!!survivorRoomCounts[room];}const media=view?.clip?data.media[view.clip]:null;if(live&&media){if(button.dataset.previewId!==media.url){button.dataset.previewId=media.url;preview.src=media.url;preview.load();}const target=Math.max(0,Math.min(view.time??0,Math.max(0,(media.duration??1)-.08)));if(preview.readyState>=1&&!preview.seeking&&Math.abs(preview.currentTime-target)>.5)preview.currentTime=target;preview.hidden=false;img.hidden=true;if(slideshow){preview.pause();}else if(preview.paused){preview.play().catch(()=>{});}}else{button.dataset.previewId='';if(!preview.paused)preview.pause();if(preview.getAttribute('src'))preview.removeAttribute('src');preview.hidden=true;img.hidden=false;}});} function stateControls(){syncBrand();const isSotc=['sotc','sotc-result','sotc-ending'].includes(mode);$('mainToolbarControls').hidden=isSotc;$('sotcToolbarControls').hidden=!isSotc;$('save').disabled=mode!=='game'||!!engine.end;$('trap').disabled=!['game','survivor'].includes(mode)||!!engine.end;$('retry').disabled=mode!=='game'||!engine.end||!engine.checkpoint||engine.lives<=0;$('pause').disabled=!['intro','game','sotc','survivor'].includes(mode);$('skipIntro').hidden=mode!=='intro';$('mobileSkipIntro').hidden=mode!=='intro';$('pause').textContent=paused?'Resume':'Pause';$('sotcControls').hidden=!isSotc;$('survivorControls').hidden=!['survivor','survivor-result'].includes(mode);} function begin(intro=true){primeAudio();stopStatic();engine.reset();engine.practice=$('practice').checked;mode=intro?'intro':'game';paused=false;bufferHold=false;mediaKey='';$('overlay').hidden=true;stateControls();playFx('button-39');message(intro?'SCAT briefing. Skip it to begin immediately.':'Surveillance online. Monitor rooms and listen for security code changes.');renderFeed();renderUI();} function beginSotc(){primeAudio();stopStatic();engine.reset();mode='sotc';paused=false;bufferHold=false;mediaKey='';sotcIndex=0;sotcRoom='Hall 1';sotcSelected=null;$('overlay').hidden=true;stateControls();playFx('page_turn');message('Scene of the Crime: review the evidence and identify the suspect.');renderFeed();renderUI();} function startSurvivorWave(){const target=survivorRules.initialTargetsPerRoom+Math.floor((survivorWave-1)/survivorRules.additionalTargetsEveryWaves);survivorRules.roomOrder.forEach(room=>{survivorRoomCounts[room]=target;});survivorPossible+=target*survivorRules.roomOrder.length;engine.selectColor(((survivorWave-1)%survivorRules.colors.length)+1);playFx(survivorWave===1?'survivor_intro':'survivor3',.8);playFx(survivorRules.colors[(survivorWave-1)%survivorRules.colors.length].toLowerCase(),.8);} function finishSurvivor(reason){mode='survivor-result';paused=true;video.pause();playFx('survivorend',1);stateControls();renderUI();message(reason);} function beginSurvivor(){primeAudio();stopStatic();engine.reset();mode='survivor';paused=false;bufferHold=false;mediaKey='';survivorIndex=0;survivorRoom='Hall 1';survivorScore=0;survivorPossible=0;survivorWave=1;survivorTime=survivorRules.minutes*60+survivorRules.seconds;survivorRoomIndices=Object.fromEntries(survivorRules.roomOrder.map(room=>[room,0]));Object.keys(survivorRoomCounts).forEach(room=>survivorRoomCounts[room]=0);startSurvivorWave();$('overlay').hidden=true;stateControls();playFx('button-39');message(`Survivor online. Wave ${survivorWave}; protect every room for ${clock(survivorTime)}.`);renderFeed();renderUI();} function pause(){if(!['game','intro','sotc','survivor'].includes(mode))return;paused=!paused;if(paused)video.pause();else if(!busy&&video.readyState>=2)video.play().catch(e=>message(e.message));stateControls();renderUI();} async function switchMedia(view){ const key=view.clip?view.clip+':'+(view.node??mode):'still:'+view.sprite; if(key===mediaKey)return; mediaKey=key;const token=++switchToken;video.pause();busy=false;bufferHold=false;startStatic(); if(view.still){stopStatic();activeClip=null;video.hidden=true;$('still').hidden=false;const src=images[view.sprite]?.url;$('still').src=src??'';$('buffering').hidden=true;return;} const clip=data.media[view.clip];if(!clip){stopStatic();message('No media mapped for this sequence.');return;} busy=true;bufferHold=true;activeClip=view.clip;$('buffering').hidden=false;$('still').hidden=true;video.hidden=false; video.src=clip.url;video.loop=!!view.loop;video.playbackRate=speed;video.load(); try { await new Promise((resolve,reject)=>{ const abort=new AbortController();const options={once:true,signal:abort.signal}; const finish=(error)=>{clearTimeout(timer);abort.abort();error?reject(error):resolve();}; const timer=setTimeout(()=>finish(Error('Video loading timed out.')),20000); video.addEventListener('canplay',()=>finish(),options); video.addEventListener('error',()=>finish(Error(video.error?.message||'Video decoder error')),options); }); if(token!==switchToken)return; const t=Math.max(0,Math.min(view.time??0,Math.max(0,video.duration-.08))); if(t>.03){await new Promise(resolve=>{const timer=setTimeout(resolve,3000);video.addEventListener('seeked',()=>{clearTimeout(timer);resolve();},{once:true});video.currentTime=t;});} if(token!==switchToken)return; if(!paused){ensureAudio();video.muted=false;video.volume=1;await video.play();}stopStatic();busy=false;bufferHold=false;$('buffering').hidden=true; }catch(e){if(token===switchToken){stopStatic();busy=false;bufferHold=false;paused=true;$('buffering').hidden=true;message('Playback paused: '+e.message);stateControls();}} } function renderFeed(){ if(mode==='intro')return switchMedia({clip:data.nodes[560].video.clip,node:560,time:0}); if(mode==='game')return switchMedia(engine.view()); if(mode==='sotc')return switchMedia({clip:sotc.flat[sotcIndex],node:`sotc-${sotcIndex}`,time:0}); if(mode==='sotc-ending')return switchMedia({clip:sotcSelected===sotc.correctSuspect?sotc.goodEnding:sotc.badEnding,node:'sotc-ending',time:0}); if(mode==='survivor'){const pool=survivorPoolByRoom[survivorRoom]??[];const index=pool.length?survivorRoomIndices[survivorRoom]%pool.length:0;const item=pool[index];return item&&switchMedia({clip:item.clip,node:`survivor-${item.node}`,time:0});} } function renderUI(){ $('cameraName').textContent=mode==='intro'?'SCAT BRIEFING':mode==='library'?'LOCAL CLIP LIBRARY':mode==='sotc'||mode==='sotc-ending'?`SOTC · ${sotcRoom.toUpperCase()}`:mode==='survivor'?`SURVIVOR · ${survivorRoom.toUpperCase()}`:mode==='game'?engine.room.toUpperCase():'SYSTEM STANDBY'; $('clock').textContent=clock(mode==='intro'?video.currentTime:mode==='survivor'?survivorTime:(engine.clockFrozenAt??engine.time)); const captured=mode==='survivor'?survivorScore:engine.score;const possible=mode==='survivor'?survivorPossible:engine.possible;$('captured').textContent=String(captured).padStart(2,'0');$('possible').textContent=String(possible).padStart(2,'0');$('ratio').textContent=mode==='survivor'?`WAVE ${String(survivorWave).padStart(2,'0')} · ${possible?Math.round(100*survivorScore/possible):0}%`:engine.possible?Math.round(100*engine.score/engine.possible)+'%':'—'; buttons.forEach((b,i)=>{const selected=data.rooms[i].id===(mode==='survivor'?survivorRoom:engine.room);b.classList.toggle('selected',selected);b.setAttribute('aria-pressed',String(selected));}); window.nightTrapVisuals?.setRoom(data.rooms.findIndex(room=>room.id===(mode==='survivor'?survivorRoom:engine.room))); $('mobileCodes').style.setProperty('--code-color',swatches[engine.securityColor-1]??swatches[0]); updateCameraPreviews(); colorButtons.forEach((b,i)=>{const selected=engine.color===i+1;b.classList.toggle('selected',selected);b.setAttribute('aria-pressed',String(selected));}); const windows=engine.windows();const level=windows.some(w=>w.frame>=w.begin&&w.frame<=w.end)?3:windows.some(w=>w.frame>=w.begin-23&&w.frame`${clock(e.time)} ${e.text}`).join('\n'); if(engine.end){paused=true;video.pause();message(engine.end+'. '+(engine.end==='Game over'&&engine.checkpoint&&engine.lives>0?'Retry the recovered checkpoint or start a new game.':'Choose New game or load a browser save.'));stateControls();} suspectButtons.forEach((b,i)=>b.classList.toggle('selected',i===sotcSelected)); } function trap(){if(paused||busy||bufferHold)return;if(mode==='survivor'){if(!survivorRoomCounts[survivorRoom]){playFx('DISARM0',.8);message(`${survivorRoom}: no active augmentation. Switch to a room with an active target.`);return {ok:false,reason:'no-survivor-target'};}playFx('TRIGGER',1.1);survivorRoomCounts[survivorRoom]--;survivorScore++;survivorRoomIndices[survivorRoom]=(survivorRoomIndices[survivorRoom]+1)%(survivorPoolByRoom[survivorRoom]?.length||1);if(Object.values(survivorRoomCounts).every(value=>value===0)){const ratio=survivorPossible?survivorScore/survivorPossible:1;if(survivorWave>=survivorRules.waveFailureCheck&&ratioa+b,0)} active targets remain.`);renderFeed();renderUI();return {ok:true,reason:'survivor-target'};}if(mode!=='game')return;playFx('TRIGGER',1.1);const result=engine.trap();if(!result.ok)playFx('DISARM0',.8);message(engine.log.at(-1)?.text??'Trap fired.');renderFeed();renderUI();return result;} function seek(t){if(!Number.isFinite(t)||t<0||t>1550)return;engine.reset();engine.practice=true;$('practice').checked=true;engine.advance(t);mode='game';paused=false;mediaKey='';$('overlay').hidden=true;stateControls();renderFeed();renderUI();message('Practice jump: a new run simulated with no captures.');} $('newGame').onclick=()=>begin(true);$('sotc').onclick=beginSotc;$('begin').onclick=()=>begin(true);$('skipIntro').onclick=()=>begin(false);$('mobileSkipIntro').onclick=()=>$('skipIntro').click();$('pause').onclick=pause;$('trap').onclick=trap; $('returnNightTrap').onclick=()=>begin(true); $('submitSuspect').onclick=()=>{if(mode!=='sotc'||sotcSelected===null){message('Select a suspect first.');return;}playFx('button-35');mode='sotc-ending';paused=false;mediaKey='';stateControls();renderFeed();renderUI();message(sotcSelected===sotc.correctSuspect?'Correct identification. Evidence confirmed.':'Incorrect identification. The case remains unsolved.');}; $('retry').onclick=()=>{if(engine.retry()){mode='game';paused=false;mediaKey='';$('overlay').hidden=true;stateControls();renderFeed();renderUI();message('Checkpoint restored. Continue surveillance.');}}; $('save').onclick=()=>{try{localStorage.setItem('nighttrap-html5-save-v1',JSON.stringify(engine.snapshot()));message('Saved in this browser.');}catch(e){message('Save failed: '+e.message);}}; $('load').onclick=()=>{try{const save=localStorage.getItem('nighttrap-html5-save-v1');if(!save){message('No browser save yet.');return;}engine.restore(JSON.parse(save));mode='game';paused=true;mediaKey='';$('overlay').hidden=true;stateControls();renderFeed();renderUI();message('Browser save loaded. Press Resume.');}catch(e){message('Could not load save: '+e.message);}}; $('fullscreen').onclick=()=>{if(document.fullscreenElement)document.exitFullscreen();else $('console').requestFullscreen().catch(e=>message(e.message));}; async function exitGame(){const button=$('exitGame');button.disabled=true;button.textContent='Closing…';try{await fetch('/__nighttrap_exit',{method:'GET',cache:'no-store',keepalive:true});}catch{}setTimeout(()=>window.close(),120);message('Night Trap has been shut down.');} $('exitGame').onclick=exitGame; async function openQr(){const dialog=$('qrDialog'),image=$('qrImage'),urlLabel=$('qrUrl');try{const info=await fetch('/__nighttrap_info').then(r=>r.json());const url=`${location.protocol}//${info.host}:${info.port}/index.html`;image.src=`/__nighttrap_qr?data=${encodeURIComponent(url)}`;urlLabel.textContent=url;dialog.hidden=false;}catch{message('QR code unavailable. Start the local launcher first.');}} $('qrCode').onclick=openQr;$('closeQr').onclick=()=>{$('qrDialog').hidden=true;};$('qrDialog').onclick=e=>{if(e.target===$('qrDialog'))$('qrDialog').hidden=true;}; function closeMobileMenu(){$('mobileMenuPanel').hidden=true;} $('mobileTrap').onclick=()=>$('trap').click();$('mobileStatus').onclick=()=>{const status=$('mainStatus');const open=status.classList.toggle('mobile-status-open');$('mobileStatus').setAttribute('aria-pressed',String(open));};$('mobileCodes').onclick=()=>{const open=document.body.classList.toggle('mobile-codes-open');$('mobileCodes').setAttribute('aria-expanded',String(open));};$('mobileMenu').onclick=()=>{$('mobileMenuPanel').hidden=false;};$('closeMobileMenu').onclick=closeMobileMenu;$('mobileMenuPanel').onclick=e=>{if(e.target===$('mobileMenuPanel'))closeMobileMenu();};document.querySelectorAll('[data-mobile-action]').forEach(button=>button.onclick=()=>{const target=$(button.dataset.mobileAction);if(target)target.click();closeMobileMenu();}); $('volume').oninput=()=>{const value=Number($('volume').value);video.volume=value;if(videoBus)videoBus.gain.value=value;fxVolume=value*.56;if(fxBus)fxBus.gain.value=fxVolume;if(ambientBus)ambientBus.gain.value=fxVolume*.18;};video.volume=Number($('volume').value); $('practice').onchange=()=>engine.practice=$('practice').checked; $('seek').onclick=()=>seek(Number($('seekTime').value));$('firstTrap').onclick=()=>{seek(11.8);engine.selectRoom('Hall 1');engine.color=engine.securityColor;renderFeed();renderUI();message('First trap rehearsal: Hall 1, about 00:13.2. Press Space when the meter is green.');}; $('speed').onchange=()=>{speed=Number($('speed').value);video.playbackRate=speed;}; function library(){const term=$('filter').value.toLowerCase();$('clips').replaceChildren();Object.values(data.media).filter(c=>(c.name+' '+c.originalPath).toLowerCase().includes(term)).forEach(c=>{const option=document.createElement('option');option.value=c.id;option.textContent=`${c.originalPath.replace('Assets/videos/','')} · ${c.duration.toFixed(1)} s`;$('clips').append(option);});$('clips').selectedIndex=0;} $('filter').oninput=library;library(); $('previewClip').onclick=()=>{if(!$('clips').value)return;mode='library';paused=false;$('overlay').hidden=true;stateControls();switchMedia({clip:$('clips').value,time:0});renderUI();video.controls=true;message('Clip library playback. The game clock is paused.');}; $('returnGame').onclick=()=>{video.controls=false;mode='game';paused=false;mediaKey='';stateControls();renderFeed();renderUI();}; document.addEventListener('keydown',e=>{if(['INPUT','SELECT','TEXTAREA'].includes(e.target.tagName)||e.repeat)return;const k=e.key.toLowerCase();if('12345678'.includes(k)&&k.length===1)selectRoom(data.rooms[Number(k)-1].id);else if(k===' '){e.preventDefault();trap();}else if(k==='p')pause();else if(k==='f')$('fullscreen').click();else if(k==='q'||k==='e'){engine.selectColor((engine.color-1+(k==='e'?1:5))%6+1);renderUI();}else if(k.startsWith('arrow')){e.preventDefault();const i=data.rooms.findIndex(r=>r.id===engine.room);selectRoom(data.rooms[(i+(['arrowleft','arrowup'].includes(k)?7:1))%8].id);}}); $('screen').addEventListener('contextmenu',e=>{e.preventDefault();trap();}); video.addEventListener('waiting',()=>{if(['game','survivor'].includes(mode)&&!paused){bufferHold=true;$('buffering').hidden=false;video.pause();}}); video.addEventListener('canplay',()=>{if(bufferHold&&!paused&&!busy){ensureAudio();video.muted=false;video.volume=1;bufferHold=false;$('buffering').hidden=true;video.play().catch(()=>{});}}); video.addEventListener('playing',()=>{if(!busy){bufferHold=false;$('buffering').hidden=true;}}); document.addEventListener('visibilitychange',()=>{if(document.hidden&&!paused&&['game','survivor'].includes(mode))pause();}); video.addEventListener('ended',()=>{ if(mode==='intro')begin(false); else if(mode==='sotc'){ if(sotcIndex=2){const desired=Math.min(view.time,Math.max(0,video.duration-.08));const syncTolerance=mobilePreviewMode()?1.15:.5;if(Math.abs(video.currentTime-desired)>syncTolerance)video.currentTime=desired;}} if(mode==='survivor'&&!paused&&!busy&&!bufferHold){survivorTime=Math.max(0,survivorTime-dt*speed);if(survivorTime<=0)finishSurvivor(`Survivor timer complete. Captured ${survivorScore} of ${survivorPossible} targets across ${survivorWave} waves.`);else renderUI();} if(mode==='intro'&&!paused&&video.currentTime>=164)begin(false); if(now-lastUI>90){lastUI=now;renderUI();}requestAnimationFrame(tick); } stateControls();renderUI();message('Local assets ready. Begin surveillance or open the investigation tools.');requestAnimationFrame(tick); // Deliberately exposed local diagnostics for validation and continued reverse engineering. window.nightTrap={engine,data,images,survivorRules,begin,beginSotc,beginSurvivor,seek,selectRoom,trap,pause,getState:()=>({mode,paused,busy,bufferHold,mediaKey,time:mode==='survivor'?survivorTime:engine.time,room:mode==='survivor'?survivorRoom:engine.room,score:mode==='survivor'?survivorScore:engine.score,possible:mode==='survivor'?survivorPossible:engine.possible,wave:survivorWave,activeSurvivorTargets:{...survivorRoomCounts},view:engine.view(),videoTime:video.currentTime,videoReady:video.readyState,videoError:video.error?.message??null})};