Download src/app.js from Vintendopower/html-trap-2026: direct link, hf CLI and curl.
- Browser
- Download file 28.1 kB
-
https://huggingface.co/spaces/Vintendopower/html-trap-2026/resolve/main/src/app.js
- Command line
-
hf download hf://spaces/Vintendopower/html-trap-2026/src/app.js
-
curl -L -o app.js https://huggingface.co/spaces/Vintendopower/html-trap-2026/resolve/main/src/app.js
28.1 kB
| 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<w.begin)?2:1; | |
| $('trapMeter').className='meter level'+level;$('trapStatus').textContent=['','Monitoring','Approaching trap','Capture window open'][level]; | |
| $('feedStatus').textContent=engine.end??(engine.forced?'SCAT OVERRIDE':paused?'PAUSED':bufferHold?'BUFFERING':mode==='game'?'FEED ACTIVE':mode==='sotc'?'EVIDENCE FEED':mode==='survivor'?'SURVIVOR FEED':'STANDBY'); | |
| $('scatAlert').hidden=!engine.forced;window.nightTrapVisuals?.setAlert(engine.forced); | |
| $('survivorWave').textContent=String(survivorWave).padStart(2,'0');$('survivorTime').textContent=clock(survivorTime); | |
| $('diagnostics').textContent=`Game ${mode==='survivor'?survivorTime.toFixed(3):engine.time.toFixed(3)} s · Media ${video.currentTime.toFixed(3)} s · ${activeClip??'none'} · Security code: ${COLORS[engine.securityColor-1]} · ${engine.active.size} active objects · ${mode==='survivor'?'survivor':' '+(engine.practice?'practice':'story')} · Lives ${engine.lives}`; | |
| if($('lab').open)$('eventLog').textContent=engine.log.slice(-25).map(e=>`${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&&ratio<survivorRules.minimumCaptureRatio){playFx('DISARM0',.8);finishSurvivor(`SCAT override: ${Math.round(ratio*100)}% captured; Survivor requires ${Math.round(survivorRules.minimumCaptureRatio*100)}% after wave ${survivorRules.waveFailureCheck}.`);}else{survivorWave++;startSurvivorWave();message(`Wave ${survivorWave-1} cleared. Wave ${survivorWave} is active.`);}}else message(`${survivorRoom} augmentation captured. ${Object.values(survivorRoomCounts).reduce((a,b)=>a+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<sotc.flat.length-1){sotcIndex++;renderFeed();renderUI();} | |
| else {mode='sotc-result';paused=true;message('Evidence review complete. Select a suspect and submit your conclusion.');stateControls();renderUI();} | |
| } else if(mode==='sotc-ending'){mode='sotc-result';paused=true;message(sotcSelected===sotc.correctSuspect?'Case solved. The good ending is unlocked.':'Case failed. The bad ending has been recorded.');stateControls();renderUI();} | |
| else if(mode==='survivor'){const pool=survivorPoolByRoom[survivorRoom]??[];if(pool.length){survivorRoomIndices[survivorRoom]=(survivorRoomIndices[survivorRoom]+1)%pool.length;renderFeed();renderUI();}} | |
| }); | |
| function tick(now){const dt=Math.min(.1,(now-lastWall)/1000);lastWall=now; | |
| if(mode==='game'&&!paused&&!busy&&!bufferHold){engine.advance(dt*speed);renderFeed();const view=engine.view();if(view.clip&&!video.seeking&&video.readyState>=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})}; | |