Download src/engine.js from Vintendopower/html-trap-2026: direct link, hf CLI and curl.
- Browser
- Download file 14 kB
-
https://huggingface.co/spaces/Vintendopower/html-trap-2026/resolve/main/src/engine.js
- Command line
-
hf download hf://spaces/Vintendopower/html-trap-2026/src/engine.js
-
curl -L -o engine.js https://huggingface.co/spaces/Vintendopower/html-trap-2026/resolve/main/src/engine.js
14 kB
| // Recovered main-game rules. Source references and limitations are in README.md. | |
| export const COLORS=['Blue','Green','Orange','Purple','Red','Yellow']; | |
| export const frameScale = value => Math.fround(Math.fround(Math.fround(Math.fround(value)*2)/Math.fround(23.976))*Math.fround(29.976)); | |
| export class NightTrapEngine { | |
| constructor(data, seed=1987) {this.data=data;this.seed=seed;this.reset();} | |
| reset() { | |
| this.nodes=structuredClone(this.data.nodes);this.time=0;this.room='Hall 1';this.score=0;this.possible=0; | |
| this.color=1;this.securityColor=1;this.flags={};this.log=[];this.events=[];this.cursor=0; | |
| this.active=new Set();this.done=new Set();this.dead=new Set();this.videoStarts={};this.lastRoomFrames={}; | |
| this.randomState=this.seed;this.end=null;this.override=null;this.phase='game';this.forced=null;this.controlLocked=false;this.clockFrozenAt=null;this.checkpoint=null;this.lives=3;this.practice=false;this.inputs=[]; | |
| for(const r of this.data.rooms)this.activate(r.node,0); | |
| this.processSchedule(0);this.update(0); | |
| } | |
| note(text,kind='event') {this.log.push({time:this.time,text,kind});if(this.log.length>300)this.log.shift();} | |
| random(min,max) {this.randomState=(Math.imul(this.randomState,1664525)+1013904223)>>>0;return min+Math.floor(this.randomState/4294967296*(max-min));} | |
| activate(id,at=this.time) { | |
| const n=this.nodes[id];if(!n||this.active.has(n.id)||this.dead.has(n.id))return; | |
| n.active=true;this.active.add(n.id);n.started=at; | |
| if(n.video)this.videoStarts[n.id]=at; | |
| for(const c of n.components.filter(c=>c.enabled)) { | |
| if(c.type==='codeChange') { | |
| let color=this.random(c.startOverride,7);if(color===this.securityColor)color=color%6+1; | |
| c.chosen=color;n.video.clip=c[COLORS[color-1].toLowerCase()]; | |
| } | |
| if(c.type==='setGameFlag')for(const k of Object.keys(c))if((k.endsWith('Captured')||k==='goofyBathroomSequence')&&k!=='cindyCaptured'&&c[k])this.flags[k]=true; | |
| if(['videoSkip','defaultSimms'].includes(c.type)&&n.video)this.videoStarts[n.id]=at-c.startingFrame/this.clip(n).fps; | |
| if(c.type==='destroyAlternatePath')this.deactivate(c.destroyThisVideo,c.destroyVideo); | |
| if(c.type==='disableFix')this.deactivate(c.disableVideo); | |
| if(c.type==='stopNextVideo'){this.deactivate(c.videoToStop);this.disableComponent(c.scriptToStop,'captureSequence');} | |
| if(c.type==='stopJeff')this.disableComponent(c.sequenceContainer,'activateJeff'); | |
| if(c.type==='destroyAlternate'){ | |
| this.deactivate(c.alternateScene); | |
| if(c.trapSequence)this.disableComponent(c.componentToDisable,'captureSequence'); | |
| } | |
| if(c.type==='deactivateVideo')this.deactivate(c.videoToDeactivate); | |
| if(c.type==='gameOverCheck')this.events.push({time:at+1,kind:'scoreCheck',component:c,ratio:this.possible?this.score/this.possible:0}); | |
| } | |
| for(const child of n.children)if(this.nodes[child]?.active)this.activate(child,at); | |
| } | |
| forceSequence(component, original, failure) { | |
| const room=component.roomName||original?.room||this.room; | |
| this.forced={room,component:component.id,original:original?.id??null,failure}; | |
| this.controlLocked=true;this.clockFrozenAt=this.time;this.room=room; | |
| if(failure){this.activate(failure);this.override=failure;} | |
| this.note('SCAT override: camera control locked.','failure'); | |
| } | |
| disableComponent(id,type) {const c=this.nodes[id]?.components.find(c=>c.type===type);if(c)c.enabled=false;} | |
| deactivate(id,destroy=false) { | |
| const n=this.nodes[id];if(!n)return; | |
| for(const child of n.children)this.deactivate(child,destroy); | |
| if(this.active.has(n.id))for(const c of n.components)if(c.enabled&&c.type==='codeChange'&&c.chosen)this.securityColor=c.chosen; | |
| this.active.delete(n.id);n.active=false;if(destroy)this.dead.add(n.id); | |
| } | |
| clip(n) {return this.data.media[n?.video?.clip];} | |
| elapsed(n) {return Math.max(0,this.time-(this.videoStarts[n?.id]??n?.started??this.time));} | |
| frame(n) {const m=this.clip(n);return m?Math.min(m.frames-1,Math.floor(this.elapsed(n)*m.fps*(n.video.speed||1)+1e-6)):-1;} | |
| processSchedule(to) { | |
| while(this.cursor<this.data.schedule.length&&this.data.schedule[this.cursor].time<=to+1e-9) { | |
| const e=this.data.schedule[this.cursor++];this.activate(e.node,e.time); | |
| } | |
| } | |
| advance(seconds) { | |
| if(this.end)return; | |
| const target=this.time+seconds; | |
| // Bound simulation steps so seeks/replays never skip a trap, score, or branch event. | |
| while(this.time<target-1e-9&&!this.end) { | |
| this.time=Math.min(target,this.time+1/60);this.processSchedule(this.time);this.update(); | |
| if(!this.practice&&!this.checkpoint&&this.time>=828)this.checkpoint={time:828,room:this.room,color:this.color,inputs:structuredClone(this.inputs)}; | |
| } | |
| } | |
| once(c,key,fn) {const id=c.id+':'+key;if(this.done.has(id))return;this.done.add(id);fn();} | |
| update() { | |
| for(const id of [...this.active]) { | |
| const n=this.nodes[id];if(!this.active.has(id))continue; | |
| const f=this.frame(n);const m=this.clip(n); | |
| for(const c of n.components) { | |
| if(!c.enabled)continue; | |
| if(c.type==='possibleCaptured'&&f>=frameScale(c.whenPossible))this.once(c,'score',()=>{ | |
| if(c.totalToAddTo==='Captured')this.score+=c.total; | |
| if(c.totalToAddTo==='Possible')this.possible+=c.total; | |
| }); | |
| if(c.type==='setGameFlag'&&c.cindyCaptured&&f>=frameScale(c.cindyTime))this.flags.cindyCaptured=true; | |
| if(c.type==='codeChange'&&(c.chosen===1||f>=frameScale(c.whenToChangeColor)))this.once(c,'color',()=>{this.securityColor=c.chosen;this.note('Security code changed. Listen to the conversation.','code');}); | |
| if(c.type==='nextVideo'&&m&&f>=(c.whenToDeactivateOverride?frameScale(c.whenToStart):m.frames-2))this.once(c,'next',()=>{this.activate(c.next);if(c.deactivateCurrentObject)this.deactivate(n.id);}); | |
| if(c.type==='alternateBathroom'&&!this.flags.goofyBathroomSequence&&f>=frameScale(c.whenToStart))this.once(c,'branch',()=>{this.activate(c.alternateVideo);this.deactivate(n.id);}); | |
| if(c.type==='alternateBranchCindy'&&this.flags.cindyCaptured&&this.frame(this.nodes[c.originalVideo])>=frameScale(c.whenToPlay))this.once(c,'branch',()=>{this.activate(c.nextVideo);this.deactivate(c.originalVideo);}); | |
| if(c.type==='updateStill')this.once(c,'still',()=>{if(this.nodes[c.stills])this.nodes[c.stills].sprite=c.newStill;}); | |
| if(c.type==='disableVideo'&&m){this.disableComponent(c.containerToDisable,'gameOverCheck');if(f>=m.frames-5)this.deactivate(c.containerToDisable);} | |
| if(c.type==='gameEnding'&&f>=frameScale(c.whenToPlay))this.once(c,'ending',()=>{ | |
| const ending=!this.flags.sheilaCaptured?c.sheilaEnding:!this.flags.victorCaptured?c.victorEnding:this.score===this.possible?c.betterEnding:c.goodEnding; | |
| this.activate(ending);this.deactivate(n.id);this.override=ending;this.note('Ending branch selected.','ending'); | |
| }); | |
| if(c.type==='startEnding'&&m&&f>=m.frames-2)this.once(c,'end',()=>{ | |
| this.phase='credits';this.override=c.next||277;this.activate(c.next||277);this.activate(200);this.activate(431);this.activate(544);this.deactivate(n.id); | |
| this.note('Ending complete. Rolling credits started.','ending'); | |
| }); | |
| if(this.phase==='credits'&&n.id===544&&m&&f>=m.frames-2)this.once(c,'creditsEnd',()=>{this.phase='results';this.finish('Credits complete');}); | |
| if(c.type==='gameOverTrigger') { | |
| const original=this.nodes[c.originalSequence]; | |
| if(this.active.has(original?.id)&&this.frame(original)>=frameScale(c.gameOverSequenceStart))this.once(c,'fail',()=>{ | |
| this.forceSequence(c,original,c.gameOver); | |
| }); | |
| } | |
| if(c.type==='autoCamera') { | |
| const original=this.nodes[c.sequence]; | |
| if(this.active.has(original?.id)&&this.frame(original)>=frameScale(c.whenToSwitchCamera))this.once(c,'force',()=>this.forceSequence(c,original,c.scatOverrideVideo)); | |
| } | |
| if(c.type==='scatOverride') { | |
| const original=this.nodes[c.sequence]; | |
| if(this.active.has(original?.id)&&this.frame(original)>=frameScale(c.whenToActivateScatOverride))this.once(c,'force',()=>this.forceSequence(c,original,c.scatOverrideVideo)); | |
| } | |
| if(c.type==='endingGameOver'&&f>=frameScale(c.gameOverStart))this.once(c,'over',()=>this.finish('Game over')); | |
| // Some source thresholds exceed the last valid frame by a fraction (e.g. 500.1001 vs 500). | |
| // Browser adaptation: a finished failure clip must still reach the game-over screen. | |
| if(c.type==='gameOver'&&m&&(f>=frameScale(c.gameOverTrigger)||this.elapsed(n)>=m.duration))this.once(c,'over',()=>this.finish('Game over')); | |
| if(c.type==='screenController'&&m&&!c.disableOverride&&!c.manualOverride&&f>=m.frames-1) { | |
| this.lastRoomFrames[n.room]={clip:n.video.clip,time:Math.max(0,(m.frames-2)/m.fps)}; | |
| this.deactivate(c.destroyParent?n.parent:n.id,true); | |
| } | |
| } | |
| } | |
| // Components on the Sequences container use the global clock. | |
| for(const c of this.nodes[731]?.components??[]) { | |
| if(!c.enabled)continue; | |
| if(c.type==='activateJeff'&&this.time>=c.whenToPlay&&!this.flags.troyCaptured)this.once(c,'jeff',()=>this.activate(c.videoSequence)); | |
| if(c.type==='tooManyAugs'&&this.time>=c.whenToStart)this.once(c,'checkpoint',()=>{ | |
| if(this.score/Math.max(1,this.possible)<.75&&!this.practice){this.activate(c.gameOverVideo);this.override=c.gameOverVideo;this.note('Capture ratio below 75%. SCAT override.','failure');} | |
| }); | |
| } | |
| for(const e of this.events.filter(e=>!e.done&&e.time<=this.time)) { | |
| e.done=true; | |
| if(e.kind==='scoreCheck') {const c=e.component;const pass=this.practice||e.ratio>=.75;this.activate(pass?c.continueGameVideo:c.gameOverVideo);if(!pass)this.override=c.gameOverVideo;} | |
| } | |
| } | |
| windows(room=this.room) { | |
| const result=[]; | |
| for(const id of this.active)for(const c of this.nodes[id].components) { | |
| if(!c.enabled||!['captureSequence','captureMegan'].includes(c.type)||c.roomName!==room||this.done.has(c.id+':capture'))continue; | |
| const original=this.nodes[c.originalSequence];if(!this.active.has(original?.id))continue; | |
| const begin=frameScale(c.trapStart);const end=c.type==='captureMegan'?frameScale(c.last):Math.fround(begin+37.5); | |
| result.push({component:c,node:id,original:original.id,frame:this.frame(original),begin,end,startTime:this.videoStarts[original.id]+Math.ceil(begin)/this.clip(original).fps}); | |
| } | |
| return result; | |
| } | |
| trap() { | |
| if(this.end)return {ok:false,reason:'ended'}; | |
| if(this.controlLocked){this.note('Trap controls disabled during SCAT override.','miss');return {ok:false,reason:'locked'};} | |
| this.inputs.push({time:this.time,type:'trap',room:this.room,color:this.color}); | |
| const valid=this.windows().filter(w=>w.frame>=w.begin&&w.frame<=w.end); | |
| if(!valid.length){this.note('Trap fired outside the capture window.','miss');return {ok:false,reason:'timing'};} | |
| if(this.color!==this.securityColor){this.note('Trap rejected: security code mismatch.','miss');return {ok:false,reason:'color'};} | |
| for(const w of valid) { | |
| const c=w.component;this.done.add(c.id+':capture');this.activate(c.capturedSequence); | |
| if(!c.dontDestroy)this.deactivate(c.originalSequence); | |
| this.note('Capture confirmed in '+this.room+'.','capture'); | |
| } | |
| this.update();return {ok:true,count:valid.length}; | |
| } | |
| selectRoom(room) {if(!this.controlLocked&&this.data.rooms.some(r=>r.id===room))this.room=room;} | |
| selectColor(color) {if(!this.controlLocked&&color>=1&&color<=6)this.color=color;} | |
| view(room=this.room) { | |
| if(this.override) { | |
| const candidates=this.descendants(this.override).map(id=>this.nodes[id]).filter(n=>this.active.has(n.id)&&n.video); | |
| if(candidates.length){const n=candidates[0];return {node:n.id,clip:n.video.clip,time:this.elapsed(n),loop:n.video.loop,override:true};} | |
| } | |
| const candidates=[...this.active].map(id=>this.nodes[id]).filter(n=>n.room===room&&n.video&&this.clip(n)); | |
| candidates.sort((a,b)=>(this.videoStarts[b.id]-this.videoStarts[a.id])||b.id-a.id); | |
| const n=candidates[0];if(n)return {node:n.id,clip:n.video.clip,time:this.elapsed(n),loop:n.video.loop}; | |
| const stills=Object.values(this.nodes).filter(n=>n.room===room&&n.name==='stills'); | |
| const still=stills.find(n=>n.children?.length)||stills[0]; | |
| return {sprite:still?.sprite,still:true}; | |
| } | |
| descendants(id) {const n=this.nodes[id];return n?[n.id,...n.children.flatMap(c=>this.descendants(c))]:[];} | |
| finish(reason) {if(this.practice)return;this.end=reason;this.note(reason,'end');} | |
| retry() { | |
| if(!this.checkpoint||this.lives<=0)return false; | |
| const cp=structuredClone(this.checkpoint);this.lives--;this.reset();this.practice=false;this.lives=Math.max(0,this.lives); | |
| for(const input of cp.inputs??[]){this.advance(Math.max(0,input.time-this.time));this.selectRoom(input.room);this.selectColor(input.color);this.trap();} | |
| this.advance(Math.max(0,cp.time-this.time));this.selectRoom(cp.room);this.selectColor(cp.color); | |
| this.note(`Retry resumed at ${Math.floor(cp.time/60)}:${String(Math.floor(cp.time%60)).padStart(2,'0')} · ${this.lives} lives remaining.`,'retry'); | |
| return true; | |
| } | |
| snapshot() {return {version:2,seed:this.seed,time:this.time,room:this.room,color:this.color,practice:this.practice,inputs:this.inputs,lives:this.lives,checkpoint:this.checkpoint};} | |
| restore(save) { | |
| if(![1,2].includes(save.version)||!Number.isFinite(save.time)||save.time<0||save.time>1900)throw Error('Invalid save'); | |
| this.seed=save.seed;this.reset();this.practice=!!save.practice;this.lives=save.lives??3;this.checkpoint=save.checkpoint??null; | |
| for(const input of save.inputs??[]){this.advance(Math.max(0,input.time-this.time));this.selectRoom(input.room);this.selectColor(input.color);this.trap();} | |
| this.advance(Math.max(0,save.time-this.time));this.selectRoom(save.room);this.selectColor(save.color); | |
| } | |
| } | |