Meta Description" name="description" />

Share this result

Previews are deleted daily. Get a permanent share link sent to your inbox:
Script
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Whisper House</title> <script src="https://cdn.tailwindcss.com"></script> <link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700;900&family=Crimson+Text:ital,wght@0,400;0,600;1,400&display=swap" rel="stylesheet"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"> <style> :root{--bg:#060606;--fg:#c4b8a8;--muted:#5a5044;--accent:#8b1a1a;--card:#0d0d0d;--border:#1e1a14;--glow:#c9a44c} *{margin:0;padding:0;box-sizing:border-box} body{background:var(--bg);color:var(--fg);font-family:'Crimson Text',serif;overflow:hidden;height:100vh;width:100vw;user-select:none} h1,h2,h3,.tf{font-family:'Cinzel',serif} .screen{position:fixed;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:100;transition:opacity .8s ease} .screen.hidden{opacity:0;pointer-events:none} #menu-screen{background:var(--bg)} .menu-bg{position:absolute;inset:0;overflow:hidden} .menu-bg::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse at 50% 80%,rgba(139,26,26,.08) 0%,transparent 60%),radial-gradient(ellipse at 20% 20%,rgba(201,164,76,.03) 0%,transparent 50%)} .mp{position:absolute;width:2px;height:2px;background:rgba(201,164,76,.15);border-radius:50%;animation:fu linear infinite} @keyframes fu{0%{transform:translateY(100vh) scale(1);opacity:0}10%{opacity:1}90%{opacity:1}100%{transform:translateY(-10vh) scale(0);opacity:0}} .mc{position:relative;z-index:2;text-align:center;padding:1rem} .mt{font-size:clamp(2.5rem,6vw,4.5rem);font-weight:900;letter-spacing:.15em;color:var(--fg);text-shadow:0 0 40px rgba(139,26,26,.3);line-height:1.1} .ms{font-size:clamp(.85rem,1.5vw,1.1rem);color:var(--muted);letter-spacing:.3em;text-transform:uppercase;margin-top:.5rem} .md{width:120px;height:1px;background:linear-gradient(90deg,transparent,var(--accent),transparent);margin:1.8rem auto} .btn{display:inline-flex;align-items:center;gap:.75rem;padding:.85rem 2.2rem;border:1px solid var(--border);background:var(--card);color:var(--fg);font-family:'Cinzel',serif;font-size:.95rem;letter-spacing:.08em;cursor:pointer;transition:all .3s ease;min-width:240px;justify-content:center} .btn:hover{border-color:var(--accent);background:rgba(139,26,26,.12);box-shadow:0 0 20px rgba(139,26,26,.15)} .btn:active{transform:scale(.97)} .btn-p{border-color:var(--accent);background:rgba(139,26,26,.18)} .ctrl-box{background:rgba(13,13,13,.6);border:1px solid var(--border);padding:1.2rem 1.5rem;margin-top:1.5rem;text-align:left;max-width:460px;width:90%} .ctrl-box h3{font-size:.75rem;letter-spacing:.15em;color:var(--glow);margin-bottom:.6rem;text-transform:uppercase} .ctrl-row{display:flex;justify-content:space-between;font-size:.8rem;color:var(--muted);padding:.15rem 0} .ctrl-row span:last-child{color:var(--fg);font-size:.75rem} #game-screen{z-index:50} #game-canvas{position:fixed;inset:0;width:100%;height:100%;display:block} #hud{position:fixed;inset:0;pointer-events:none;z-index:60} #crosshair{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:4px;height:4px;border:1px solid rgba(196,184,168,.4);border-radius:50%;transition:all .2s} #crosshair.act{width:8px;height:8px;border-color:var(--glow);box-shadow:0 0 8px rgba(201,164,76,.3)} #prompt{position:absolute;top:56%;left:50%;transform:translateX(-50%);font-family:'Cinzel',serif;font-size:.78rem;letter-spacing:.1em;color:var(--glow);opacity:0;transition:opacity .2s;text-shadow:0 0 10px rgba(201,164,76,.3);white-space:nowrap} #prompt.vis{opacity:1} #inv{position:absolute;bottom:1.5rem;left:50%;transform:translateX(-50%);display:flex;gap:.6rem} .islot{width:52px;height:52px;border:1px solid var(--border);background:rgba(13,13,13,.85);display:flex;align-items:center;justify-content:center;position:relative;transition:all .3s} .islot.has{border-color:var(--glow);box-shadow:0 0 10px rgba(201,164,76,.1)} .islot i{font-size:1rem;color:var(--glow);opacity:.7} .islot .il{position:absolute;bottom:-1.1rem;left:50%;transform:translateX(-50%);font-size:.55rem;color:var(--muted);white-space:nowrap} #smsg{position:absolute;top:22%;left:50%;transform:translateX(-50%);font-family:'Cinzel',serif;font-size:.82rem;letter-spacing:.08em;color:var(--fg);opacity:0;transition:opacity .5s;text-align:center;max-width:420px;pointer-events:none} #smsg.vis{opacity:1} #note-panel{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--card);border:1px solid var(--border);padding:2rem 2.2rem;max-width:380px;width:90%;z-index:75;pointer-events:auto;opacity:0;transition:opacity .4s;cursor:pointer} #note-panel.hid{opacity:0;pointer-events:none} .nt{font-family:'Cinzel',serif;font-size:.82rem;color:var(--glow);letter-spacing:.1em;margin-bottom:.7rem} .nb{font-size:1rem;line-height:1.7;color:var(--fg);font-style:italic} .nh{font-size:.65rem;color:var(--muted);margin-top:1rem;letter-spacing:.05em} #pindicator{position:absolute;top:1.2rem;left:50%;transform:translateX(-50%);font-family:'Cinzel',serif;font-size:.7rem;letter-spacing:.12em;color:var(--muted);background:rgba(6,6,6,.6);padding:.3rem 1rem;border:1px solid var(--border)} #pindicator .pname{color:var(--glow)} #vignette{position:fixed;inset:0;pointer-events:none;z-index:55;background:radial-gradient(ellipse at center,transparent 35%,rgba(0,0,0,.9) 100%)} #distort{position:fixed;inset:0;pointer-events:none;z-index:56;opacity:0;transition:opacity .1s;mix-blend-mode:screen;background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(139,26,26,.04) 2px,rgba(139,26,26,.04) 4px)} #esc-screen{background:rgba(6,6,6,.95)} .et{font-size:clamp(2rem,5vw,3.5rem);color:var(--fg);letter-spacing:.1em} #lt{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-family:'Cinzel',serif;font-size:.85rem;color:var(--muted);letter-spacing:.15em;animation:lp 1.5s ease infinite} @keyframes lp{0%,100%{opacity:.3}50%{opacity:.8}} @media(prefers-reduced-motion:reduce){.mp{animation:none;display:none}*{transition-duration:0s!important;animation-duration:0s!important}} </style> </head> <body> <div id="menu-screen" class="screen"> <div class="menu-bg" id="mbg"></div> <div class="mc"> <div class="mt">WHISPER HOUSE</div> <div class="ms">A Cooperative Horror Experience</div> <div class="md"></div> <button class="btn btn-p" id="btn-start"><i class="fas fa-door-open"></i> Enter the House</button> <div class="ctrl-box"> <h3>Local Co-op Controls</h3> <div class="ctrl-row"><span>Player 1 — Move</span><span>W A S D</span></div> <div class="ctrl-row"><span>Player 1 — Look</span><span>Mouse</span></div> <div class="ctrl-row"><span>Player 1 — Interact</span><span>E</span></div> <div class="ctrl-row" style="margin-top:.5rem;border-top:1px solid var(--border);padding-top:.5rem"><span>Player 2 — Move</span><span>Arrow Keys</span></div> <div class="ctrl-row"><span>Player 2 — Look</span><span>I J K L</span></div> <div class="ctrl-row"><span>Player 2 — Interact</span><span>U</span></div> <div class="ctrl-row" style="margin-top:.5rem;border-top:1px solid var(--border);padding-top:.5rem"><span>Switch Active Player</span><span>Tab</span></div> </div> <div style="margin-top:1.5rem;font-size:.65rem;color:var(--muted);letter-spacing:.08em;max-width:380px;margin-left:auto;margin-right:auto;line-height:1.5"> Two players explore the same house. Each sees different clues. Communicate to escape. </div> </div> </div> <div id="game-screen" class="screen hidden"> <canvas id="game-canvas"></canvas> <div id="vignette"></div> <div id="distort"></div> <div id="hud"> <div id="pindicator">Active: <span class="pname" id="pname">Player 1</span> (Tab to switch)</div> <div id="crosshair"></div> <div id="prompt">[E] INTERACT</div> <div id="smsg"></div> <div id="inv"> <div class="islot" id="s0"><span class="il"></span></div> <div class="islot" id="s1"><span class="il"></span></div> </div> </div> <div id="note-panel" class="hid"> <div class="nt" id="ntitle"></div> <div class="nb" id="nbody"></div> <div class="nh">Click to close</div> </div> <div id="lt">ENTERING THE HOUSE...</div> </div> <div id="esc-screen" class="screen hidden"> <div class="et tf">YOU ESCAPED</div> <div class="md" style="margin:1.5rem auto"></div> <div style="font-size:1.05rem;color:var(--muted);max-width:400px;text-align:center;line-height:1.8;margin-bottom:2rem;padding:0 1rem"> The door yields to the night air. Cold wind rushes past your face.<br> Behind you, the house groans — then falls silent.<br><br> <em>Don't look back.</em> </div> <button class="btn" id="btn-re"><i class="fas fa-rotate-left"></i> Return to Menu</button> </div> <script type="importmap"> { "imports": { "three": "https://unpkg.com/three@0.160.0/build/three.module.js", "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/" } } </script> <script type="module"> import * as THREE from 'three'; import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js'; /* ===== CONSTANTS ===== */ const RW=5.5, RH=3.2, RD=5.5, WL=0.15, FY=0, CY=3.2; const PH=1.6, PR=0.28, MSPD=2.8, SSPD=4.2; const IDIST=2.2, IANGLE=0.55; const ROOMS=[ {id:'foyer',name:'Foyer',x:0,z:0}, {id:'kitchen',name:'Kitchen',x:5.65,z:0}, {id:'study',name:'Study',x:0,z:-5.65}, {id:'corridor',name:'Corridor',x:5.65,z:-5.65} ]; const ITEMP={ brassKey:{name:'Brass Key',icon:'fa-key'}, moonLight:{name:'Moonstone',icon:'fa-moon'} }; const NOTES={ tornPage:{title:'Torn Page',body:'I can hear it moving above me now. Not walking — sliding. Like something dragged across the floor by invisible hands. The door won\'t hold. The code is the date we arrived. 1-8-4-7. Remember the order. Remember the—'}, journal:{title:'Journal Entry',body:'November 14th. The door to the east appeared today — the one I\'ve been trying to reach. It\'s sealed with a lock I don\'t recognize. The key must be somewhere in this house. I\'ve searched the kitchen a hundred times. Maybe it doesn\'t want to be found.'}, diary:{title:'Diary Page',body:'We found the moonstone in the corridor. It pulses when the entity is near. I don\'t know what it means. But I know we shouldn\'t stay here after dark. The whispers get louder when the lights go out. If you\'re reading this — check the keypad by the front door. The code was written on a page. Find it.'} }; const DLABEL={foyer_kitchen:'Door to Kitchen',foyer_study:'Door to Study',kitchen_corridor:'Door to Corridor',study_corridor:'Door to Corridor'}; /* ===== STATE ===== */ const S={ screen:'menu',activeP:0,noteOpen:false,locked:false, opened:new Set(),readN:new Set(),solved:false,escaped:false, inv:[],horrorT:10,ftT:0, p:[ {inv:[],readN:new Set()}, {inv:[],readN:new Set()} ] }; const K={}; const players=[null,null]; let scene,cam,ren,clock,controls,fl,flT; let colls=[],inters=[],dMeshes={},entObj; let listener,droneOsc,droneG,droneF,ambG; const rv=new THREE.Vector3(),rv2=new THREE.Vector3(),rc=new THREE.Raycaster(); /* ===== MENU PARTICLES ===== */ (function(){const b=document.getElementById('mbg');for(let i=0;i<25;i++){const p=document.createElement('div');p.className='mp';p.style.left=Math.random()*100+'%';p.style.animationDuration=(8+Math.random()*12)+'s';p.style.animationDelay=Math.random()*10+'s';b.appendChild(p)}})(); /* ===== UI ===== */ function switchScreen(n){S.screen=n;['menu-screen','game-screen','esc-screen'].forEach(id=>{document.getElementById(id).classList.toggle('hidden',id!==n+'-screen')})} function showMsg(t,d=2500){const e=document.getElementById('smsg');e.textContent=t;e.classList.add('vis');setTimeout(()=>e.classList.remove('vis'),d)} function showNote(id){const n=NOTES[id];if(!n)return;document.getElementById('ntitle').textContent=n.title;document.getElementById('nbody').textContent=n.body;document.getElementById('note-panel').classList.remove('hid');S.noteOpen=true;if(controls)controls.unlock()} function hideNote(){document.getElementById('note-panel').classList.add('hid');S.noteOpen=false} function updInv(){const inv=S.p[S.activeP].inv;for(let i=0;i<2;i++){const sl=document.getElementById('s'+i);const it=inv[i];if(it){const d=ITEMP[it];sl.innerHTML=`<i class="fas ${d.icon}"></i><span class="il">${d.name}</span>`;sl.classList.add('has')}else{sl.innerHTML='<span class="il"></span>';sl.classList.remove('has')}}} function updPInd(){document.getElementById('pname').textContent='Player '+(S.activeP+1)} /* ===== TEXTURES ===== */ function mkTex(bR,bG,bB,planks){ const c=document.createElement('canvas');c.width=256;c.height=256;const x=c.getContext('2d'); x.fillStyle=`rgb(${bR},${bG},${bB})`;x.fillRect(0,0,256,256); const pw=256/planks; for(let i=0;i<planks;i++){const px=i*pw;x.fillStyle='rgba(0,0,0,0.1)';x.fillRect(px,0,1,256); for(let j=0;j<30;j++){const gy=Math.random()*256;x.strokeStyle=`rgba(0,0,0,${0.02+Math.random()*0.03})`;x.lineWidth=.5+Math.random();x.beginPath();x.moveTo(px+2,gy);x.lineTo(px+pw-2,gy+(Math.random()-.5)*5);x.stroke()}} for(let i=0;i<200;i++){x.fillStyle=`rgba(0,0,0,${Math.random()*0.03})`;x.fillRect(Math.random()*256,Math.random()*256,1+Math.random(),1)} const t=new THREE.CanvasTexture(c);t.wrapS=t.wrapT=THREE.RepeatWrapping;return t; } function mkWallTex(){ const c=document.createElement('canvas');c.width=256;c.height=256;const x=c.getContext('2d'); x.fillStyle='#1a1714';x.fillRect(0,0,256,256); for(let i=0;i<256;i+=16){x.fillStyle='rgba(30,26,20,0.25)';x.fillRect(i,0,8,256)} for(let s=0;s<4;s++){const sx=Math.random()*256,sy=Math.random()*256,sr=15+Math.random()*25; const g=x.createRadialGradient(sx,sy,0,sx,sy,sr);g.addColorStop(0,'rgba(10,8,5,0.12)');g.addColorStop(1,'rgba(10,8,5,0)');x.fillStyle=g;x.fillRect(sx-sr,sy-sr,sr*2,sr*2)} for(let i=0;i<400;i++){x.fillStyle=`rgba(0,0,0,${Math.random()*0.04})`;x.fillRect(Math.random()*256,Math.random()*256,1,1)} const t=new THREE.CanvasTexture(c);t.wrapS=t.wrapT=THREE.RepeatWrapping;return t; } function mkCeilTex(){ const c=document.createElement('canvas');c.width=128;c.height=128;const x=c.getContext('2d'); x.fillStyle='#141210';x.fillRect(0,0,128,128); for(let i=0;i<2;i++){x.strokeStyle='rgba(0,0,0,0.15)';x.lineWidth=.5;x.beginPath();let cx=Math.random()*128,cy=Math.random()*128;x.moveTo(cx,cy);for(let j=0;j<4;j++){cx+=(Math.random()-.5)*25;cy+=(Math.random()-.5)*25;x.lineTo(cx,cy)}x.stroke()} for(let i=0;i<150;i++){x.fillStyle=`rgba(0,0,0,${Math.random()*0.03})`;x.fillRect(Math.random()*128,Math.random()*128,1,1)} const t=new THREE.CanvasTexture(c);t.wrapS=t.wrapT=THREE.RepeatWrapping;return t; } let wTex,wallTex,cTex,dwTex; function initTex(){wTex=mkTex(42,30,18,5);dwTex=mkTex(28,20,12,4);wallTex=mkWallTex();cTex=mkCeilTex()} /* ===== THREE INIT ===== */ function initThree(){ scene=new THREE.Scene();scene.background=new THREE.Color(0x020202);scene.fog=new THREE.FogExp2(0x020202,0.11); cam=new THREE.PerspectiveCamera(68,innerWidth/innerHeight,0.05,50); cam.position.set(0,PH,1.5); ren=new THREE.WebGLRenderer({canvas:document.getElementById('game-canvas'),antialias:true}); ren.setSize(innerWidth,innerHeight);ren.setPixelRatio(Math.min(devicePixelRatio,2)); ren.shadowMap.enabled=true;ren.shadowMap.type=THREE.PCFSoftShadowMap; ren.toneMapping=THREE.ACESFilmicToneMapping;ren.toneMappingExposure=0.55; controls=new PointerLockControls(cam,document.body); controls.addEventListener('lock',()=>{S.locked=true}); controls.addEventListener('unlock',()=>{if(!S.noteOpen)S.locked=false}); listener=new THREE.AudioListener();cam.add(listener); clock=new THREE.Clock(); addEventListener('resize',()=>{cam.aspect=innerWidth/innerHeight;cam.updateProjectionMatrix();ren.setSize(innerWidth,innerHeight)}); } /* ===== BUILDING ===== */ function addC(m){colls.push(m);return m} function mkWall(x,y,z,w,h,d){ const wt=wallTex.clone();const sx=Math.max(1,Math.round(Math.max(w,d)/2.5)),sy=Math.max(1,Math.round(h/2.5)); wt.repeat.set(sx,sy);wt.wrapS=wt.wrapT=THREE.RepeatWrapping; const g=new THREE.BoxGeometry(Math.max(.01,w),Math.max(.01,h),Math.max(.01,d)); const m=new THREE.Mesh(g,new THREE.MeshStandardMaterial({map:wt,roughness:.92})); m.position.set(x,y,z);m.receiveShadow=true;scene.add(m);addC(m);return m; } function buildHouse(){ // Floors & Ceilings ROOMS.forEach(r=>{ const ft=wTex.clone();ft.repeat.set(3,3);ft.wrapS=ft.wrapT=THREE.RepeatWrapping; const fl=new THREE.Mesh(new THREE.PlaneGeometry(RW,RD),new THREE.MeshStandardMaterial({map:ft,roughness:.85})); fl.rotation.x=-Math.PI/2;fl.position.set(r.x,FY+.01,r.z);fl.receiveShadow=true;scene.add(fl); const ct=cTex.clone();ct.repeat.set(2,2);ct.wrapS=ct.wrapT=THREE.RepeatWrapping; const ce=new THREE.Mesh(new THREE.PlaneGeometry(RW,RD),new THREE.MeshStandardMaterial({map:ct,roughness:.95})); ce.rotation.x=Math.PI/2;ce.position.set(r.x,CY,r.z);scene.add(ce); }); const RHW=RW/2,RHD=RD/2,dW=1.0,dH=2.3; // Solid walls // Foyer: South + West mkWall(0,RH/2,RHD,RW,RH,WL);mkWall(-RHW,RH/2,0,WL,RH,RD); // Kitchen: East + South mkWall(ROOMS[1].x+RHW,RH/2,0,WL,RH,RD);mkWall(ROOMS[1].x,RH/2,RHD,RW,RH,WL); // Study: West + North mkWall(-RHW,RH/2,ROOMS[2].z,WL,RH,RD);mkWall(0,RH/2,ROOMS[2].z-RHD,RW,RH,WL); // Corridor: North mkWall(ROOMS[3].x,RH/2,ROOMS[3].z-RHD,RW,RH,WL); // Doorways mkDoor(0,0,'foyer_kitchen','x');mkDoor(0,0,'foyer_study','z'); mkDoor(ROOMS[1].x,ROOMS[1].z,'kitchen_corridor','z'); mkDoor(ROOMS[2].x,ROOMS[2].z,'study_corridor','x'); mkExit(ROOMS[3].x+RHW,ROOMS[3].z,'x'); buildFurniture(); } function mkDoor(rx,rz,id,axis){ const RHW=RW/2,RHD=RD/2,dW=1.0,dH=2.3; let cx,cz; if(axis==='x'){cx=rx+RHW;cz=rz}else{cx=rx;cz=rz-RHD} if(axis==='x'){ if(RHD-dW/2>.01){mkWall(cx,RH/2,cz-RHD+(RHD-dW/2)/2,WL,RH,RHD-dW/2);mkWall(cx,RH/2,cz+RHD-(RHD-dW/2)/2,WL,RH,RHD-dW/2)} mkWall(cx,dH+(RH-dH)/2,cz,WL,RH-dH,dW); }else{ if(RHW-dW/2>.01){mkWall(cx-RHW+(RHW-dW/2)/2,RH/2,cz,RHW-dW/2,RH,WL);mkWall(cx+RHW-(RHW-dW/2)/2,RH/2,cz,RHW-dW/2,RH,WL)} mkWall(cx,dH+(RH-dH)/2,cz,dW,RH-dH,WL); } const dg=new THREE.BoxGeometry(axis==='x'?.08:dW,dH,axis==='z'?.08:dW); const dm=new THREE.MeshStandardMaterial({map:dwTex.clone(),roughness:.8,color:0x2a1e14}); const mesh=new THREE.Mesh(dg,dm);mesh.position.set(cx,dH/2,cz);mesh.castShadow=true;mesh.receiveShadow=true;scene.add(mesh);addC(mesh); dMeshes[id]={mesh,cx,cz,axis,dW,dH}; inters.push({id,type:'door',mesh,label:DLABEL[id],getPos:()=>new THREE.Vector3(cx,dH/2,cz),act:()=>openDoor(id)}); } function mkExit(cx,cz,axis){ const RHD=RD/2,dW=1.1,dH=2.4; if(RHD-dW/2>.01){mkWall(cx,RH/2,cz-RHD+(RHD-dW/2)/2,WL,RH,RHD-dW/2);mkWall(cx,RH/2,cz+RHD-(RHD-dW/2)/2,WL,RH,RHD-dW/2)} mkWall(cx,dH+(RH-dH)/2,cz,WL,RH-dH,dW); const dg=new THREE.BoxGeometry(.12,dH,dW); const dm=new THREE.MeshStandardMaterial({map:dwTex.clone(),roughness:.7,metalness:.2,color:0x1a1208}); const mesh=new THREE.Mesh(dg,dm);mesh.position.set(cx,dH/2,cz);mesh.castShadow=true;scene.add(mesh);addC(mesh); // Lock const lk=new THREE.Mesh(new THREE.BoxGeometry(.05,.08,.08),new THREE.MeshStandardMaterial({color:0x8b7340,roughness:.3,metalness:.8})); lk.position.set(cx-.07,dH/2,cz);scene.add(lk); dMeshes.exit={mesh,cx,cz,axis,dW,dH}; inters.push({id:'exit',type:'exit',mesh,label:'Front Door — Locked',getPos:()=>new THREE.Vector3(cx,dH/2,cz),act:()=>tryExit()}); // Keypad const kp=new THREE.Mesh(new THREE.BoxGeometry(.04,.2,.15),new THREE.MeshStandardMaterial({color:0x222222,roughness:.5,metalness:.5})); kp.position.set(cx-.55,1.3,cz);scene.add(kp); const led=new THREE.Mesh(new THREE.SphereGeometry(.015,8,8),new THREE.MeshStandardMaterial({color:0x8b1a1a,emissive:0x8b1a1a,emissiveIntensity:.5})); led.position.set(cx-.57,1.3,cz);scene.add(led); inters.push({id:'keypad',type:'puzzle',mesh:kp,led,label:'Keypad — Enter Code',getPos:()=>kp.position.clone(),act:()=>tryPuzzle()}); } function buildFurniture(){ const tm=new THREE.MeshStandardMaterial({map:dwTex.clone(),roughness:.8,color:0x2a2018}); const dm=new THREE.MeshStandardMaterial({color:0x1a1410,roughness:.85}); // Foyer table const ft=new THREE.Mesh(new THREE.BoxGeometry(.8,.75,.5),tm);ft.position.set(-1.8,.375,0);ft.castShadow=true;scene.add(ft);addC(ft); // Foyer chair const cs=new THREE.Mesh(new THREE.BoxGeometry(.4,.05,.4),dm);cs.position.set(1.5,.45,1.5);cs.castShadow=true;scene.add(cs);addC(cs); const cb=new THREE.Mesh(new THREE.BoxGeometry(.4,.5,.05),dm);cb.position.set(1.5,.7,1.3);cb.castShadow=true;scene.add(cb);addC(cb); // Kitchen counter const cn=new THREE.Mesh(new THREE.BoxGeometry(.6,.9,3.5),tm.clone());cn.position.set(7.8,.45,0);cn.castShadow=true;scene.add(cn);addC(cn); // Stove const st=new THREE.Mesh(new THREE.BoxGeometry(.6,.9,.7),new THREE.MeshStandardMaterial({color:0x1a1a1a,roughness:.6,metalness:.3}));st.position.set(7.8,.45,-2);st.castShadow=true;scene.add(st);addC(st); // BRASS KEY (Kitchen) const km=new THREE.MeshStandardMaterial({color:0xc9a44c,roughness:.3,metalness:.9,emissive:0xc9a44c,emissiveIntensity:.12}); const keyM=new THREE.Mesh(new THREE.CylinderGeometry(.02,.02,.08,8),km);keyM.position.set(7.4,.95,-.5);keyM.rotation.z=Math.PI/2;keyM.castShadow=true;scene.add(keyM); const ring=new THREE.Mesh(new THREE.TorusGeometry(.04,.005,6,12),km);ring.position.set(7.4,.95,-.42);scene.add(ring); inters.push({id:'brassKey',type:'item',iid:'brassKey',mesh:keyM,ex:[ring],label:'Brass Key',getPos:()=>keyM.position.clone(),act:()=>pickItem('brassKey',keyM,[ring])}); // Study desk const dk=new THREE.Mesh(new THREE.BoxGeometry(1.2,.75,.6),tm.clone());dk.position.set(-.5,.375,-7.5);dk.castShadow=true;scene.add(dk);addC(dk); // Bookshelf for(let i=0;i<3;i++){const sh=new THREE.Mesh(new THREE.BoxGeometry(.35,.03,1.8),tm.clone());sh.position.set(-2.5,.8+i*.9,-5.65);sh.castShadow=true;scene.add(sh); for(let b=0;b<5;b++){const bh=.2+Math.random()*.4;const bc=[0x3a1515,0x152a3a,0x1a3320,0x3a2a10,0x2a1530][Math.floor(Math.random()*5)]; const bk=new THREE.Mesh(new THREE.BoxGeometry(.2,bh,.03+Math.random()*.04),new THREE.MeshStandardMaterial({color:bc,roughness:.8}));bk.position.set(-2.5,.83+i*.9+bh/2,-6.2+b*.35);scene.add(bk)}} const sb=new THREE.Mesh(new THREE.BoxGeometry(.03,2.7,1.8),dm.clone());sb.position.set(-2.68,1.35,-5.65);scene.add(sb);addC(sb); // JOURNAL (Study) const jM=new THREE.Mesh(new THREE.BoxGeometry(.2,.03,.15),new THREE.MeshStandardMaterial({color:0x3a2810,roughness:.7}));jM.position.set(-.3,.78,-7.5);jM.rotation.y=.3;jM.castShadow=true;scene.add(jM); inters.push({id:'journal',type:'note',nid:'journal',mesh:jM,label:'Old Journal',getPos:()=>jM.position.clone(),act:()=>readN('journal')}); // Corridor table const ct=new THREE.Mesh(new THREE.BoxGeometry(.5,.6,.4),tm.clone());ct.position.set(5.65,.3,-6.5);ct.castShadow=true;scene.add(ct);addC(ct); // TORN PAGE (Corridor) const pM=new THREE.Mesh(new THREE.PlaneGeometry(.18,.25),new THREE.MeshStandardMaterial({color:0xd4c8a8,roughness:.9,side:THREE.DoubleSide}));pM.position.set(5.65,.63,-6.5);pM.rotation.x=-Math.PI/2;pM.rotation.z=.15;scene.add(pM); inters.push({id:'tornPage',type:'note',nid:'tornPage',mesh:pM,label:'Torn Page',getPos:()=>pM.position.clone(),act:()=>readN('tornPage')}); // MOONSTONE (Corridor floor) const mm=new THREE.Mesh(new THREE.SphereGeometry(.05,12,12),new THREE.MeshStandardMaterial({color:0xc8d8e8,roughness:.2,metalness:.3,emissive:0x4a5a7a,emissiveIntensity:.4})); mm.position.set(4.8,.05,-8.2);mm.castShadow=true;scene.add(mm); const ml=new THREE.PointLight(0x4a5a7a,.3,2);ml.position.set(4.8,.15,-8.2);scene.add(ml); inters.push({id:'moonLight',type:'item',iid:'moonLight',mesh:mm,ex:[ml],label:'Moonstone',getPos:()=>mm.position.clone(),act:()=>pickItem('moonLight',mm,[ml])}); // DIARY (Study - hidden behind books) const dM=new THREE.Mesh(new THREE.BoxGeometry(.15,.02,.12),new THREE.MeshStandardMaterial({color:0x4a3820,roughness:.8}));dM.position.set(-2.45,.83+.9*2+.02,-5.3);dM.rotation.y=-.2;scene.add(dM); inters.push({id:'diary',type:'note',nid:'diary',mesh:dM,label:'Small Diary',getPos:()=>dM.position.clone(),act:()=>readN('diary')}); } /* ===== LIGHTING ===== */ function setupLights(){ scene.add(new THREE.AmbientLight(0x0a0a0a,.3)); // Foyer candle const fl=new THREE.PointLight(0xffa040,.8,8,2);fl.position.set(0,2.8,0);fl.castShadow=true;fl.shadow.mapSize.set(512,512);fl.shadow.bias=-.002;scene.add(fl); const candle=new THREE.Mesh(new THREE.CylinderGeometry(.02,.02,.12,8),new THREE.MeshStandardMaterial({color:0xe8dcc8,roughness:.8}));candle.position.set(0,FY+.06,0);scene.add(candle); const flame=new THREE.Mesh(new THREE.SphereGeometry(.015,6,6),new THREE.MeshStandardMaterial({color:0xff8820,emissive:0xff6600,emissiveIntensity:3}));flame.position.set(0,FY+.14,0);flame.scale.set(1,1.5,1);scene.add(flame); fl._flame=flame;fl._bi=.8;scene._fL=fl; // Kitchen const kl=new THREE.PointLight(0x2040a0,.12,6,2);kl.position.set(5.65,2.8,0);scene.add(kl); // Study const sl=new THREE.PointLight(0xff8040,.18,6,2);sl.position.set(0,2.5,-5.65);scene.add(sl); // Corridor const cl=new THREE.PointLight(0x601010,.06,5,2);cl.position.set(5.65,2.8,-5.65);scene.add(cl); // Flashlight flT=new THREE.Object3D();scene.add(flT); fl=new THREE.SpotLight(0xffe8c0,2.5,14,Math.PI/6.5,.4,1.5);fl.castShadow=true;fl.shadow.mapSize.set(512,512);fl.shadow.bias=-.002;fl.target=flT;fl.position.set(0,-.1,0);cam.add(fl);scene.add(cam); } /* ===== PLAYERS ===== */ function mkPlayer(idx){ const startPos=idx===0?{x:0,z:1.5}:{x:1,z:1.5}; const p={pos:new THREE.Vector3(startPos.x,PH,startPos.z),yaw:0,pitch:0,vel:new THREE.Vector3()}; players[idx]=p; return p; } /* ===== ENTITY ===== */ function mkEntity(){ const g=new THREE.Group(); const bm=new THREE.MeshBasicMaterial({color:0x000000,transparent:true,opacity:0}); const body=new THREE.Mesh(new THREE.CylinderGeometry(.15,.2,2.2,8),bm);body.position.y=1.1;g.add(body); const hm=bm.clone();const head=new THREE.Mesh(new THREE.SphereGeometry(.18,8,8),hm);head.position.y=2.35;g.add(head); const el=new THREE.PointLight(0x8b1a1a,0,5,2);el.position.set(0,2.3,.15);g.add(el); g.position.set(5.65,0,-5.65);g.visible=false;scene.add(g); entObj={g,bm,hm,el}; } /* ===== INTERACTIONS ===== */ function pickItem(iid,mesh,ex){ const pInv=S.p[S.activeP].inv; if(pInv.includes(iid))return; if(pInv.length>=2){showMsg('Inventory full',1500);return} pInv.push(iid);mesh.visible=false;if(ex)ex.forEach(e=>{if(e.visible!==undefined)e.visible=false;if(e.intensity!==undefined)e.intensity=0}); updInv();showMsg('Picked up '+ITEMP[iid].name,2000); } function readN(nid){ if(S.p[S.activeP].readN.has(nid)){showMsg('Already read',1000);return} S.p[S.activeP].readN.add(nid);S.readN.add(nid);showNote(nid); } function openDoor(id){ if(S.opened.has(id))return;S.opened.add(id);syncDoors(); showMsg(DLABEL[id]+' opened',2000); } function syncDoors(){ Object.entries(dMeshes).forEach(([id,d])=>{ if(S.opened.has(id)){ if(d.axis==='x')d.mesh.position.x=d.cx+.5;else d.mesh.position.z=d.cz+.5; const i=colls.indexOf(d.mesh);if(i>-1)colls.splice(i,1); } }); } function tryPuzzle(){ if(S.solved){showMsg('Already unlocked',1500);return} showMsg('Enter the code: 1-8-4-7',3000); setTimeout(()=>{ if(S.readN.has('tornPage')){ S.solved=true;showMsg('The lock clicks open...',3000); const kp=inters.find(i=>i.id==='keypad');if(kp&&kp.led){kp.led.material.color.setHex(0x2d8a2d);kp.led.material.emissive.setHex(0x2d8a2d)} setTimeout(()=>triggerHorror('strong'),2000); }else{showMsg('Wrong code. You need to find it somewhere...',2500)} },1500); } function tryExit(){ if(S.solved){S.escaped=true;setTimeout(()=>{controls.unlock();switchScreen('esc')},1500);return} showMsg('The door is sealed. A keypad glows faintly beside it.',2500); } /* ===== AUDIO ===== */ function initAudio(){ const ctx=listener.context; droneOsc=ctx.createOscillator();droneG=ctx.createGain();droneF=ctx.createBiquadFilter(); droneOsc.type='sawtooth';droneOsc.frequency.value=38;droneF.type='lowpass';droneF.frequency.value=80;droneF.Q.value=3;droneG.gain.value=.05; droneOsc.connect(droneF);droneF.connect(droneG);droneG.connect(ctx.destination);droneOsc.start(); const d2=ctx.createOscillator();const g2=ctx.createGain();const f2=ctx.createBiquadFilter(); d2.type='sine';d2.frequency.value=55;f2.type='lowpass';f2.frequency.value=100;g2.gain.value=.03; d2.connect(f2);f2.connect(g2);g2.connect(ctx.destination);d2.start(); const bs=ctx.sampleRate*2;const nb=ctx.createBuffer(1,bs,ctx.sampleRate);const nd=nb.getChannelData(0); for(let i=0;i<bs;i++)nd[i]=(Math.random()*2-1)*.3; const ns=ctx.createBufferSource();ns.buffer=nb;ns.loop=true; const nf=ctx.createBiquadFilter();nf.type='bandpass';nf.frequency.value=200;nf.Q.value=.5; ambG=ctx.createGain();ambG.gain.value=.012; ns.connect(nf);nf.connect(ambG);ambG.connect(ctx.destination);ns.start(); } function playStep(){ if(!listener)return;const ctx=listener.context;const bs=ctx.sampleRate*.07; const b=ctx.createBuffer(1,bs,ctx.sampleRate);const d=b.getChannelData(0); for(let i=0;i<d.length;i++)d[i]=(Math.random()*2-1)*Math.exp(-i/(ctx.sampleRate*.012)); const s=ctx.createBufferSource();s.buffer=b; const f=ctx.createBiquadFilter();f.type='lowpass';f.frequency.value=350+Math.random()*200; const g=ctx.createGain();g.gain.value=.07;s.connect(f);f.connect(g);g.connect(ctx.destination);s.start(); } function playWhisper(){ if(!listener)return;const ctx=listener.context;const dur=1.5+Math.random()*2; const b=ctx.createBuffer(1,ctx.sampleRate*dur,ctx.sampleRate);const d=b.getChannelData(0); for(let i=0;i<d.length;i++){const t=i/ctx.sampleRate;d[i]=(Math.random()*2-1)*Math.sin(Math.PI*t/dur)*.3} const s=ctx.createBufferSource();s.buffer=b; const bp=ctx.createBiquadFilter();bp.type='bandpass';bp.frequency.value=1500+Math.random()*2000;bp.Q.value=5; const g=ctx.createGain();g.gain.value=.02; const pan=ctx.createStereoPanner();pan.pan.value=Math.random()*2-1; s.connect(bp);bp.connect(g);g.connect(pan);pan.connect(ctx.destination);s.start(); } function playCreak(){ if(!listener)return;const ctx=listener.context; const o=ctx.createOscillator();o.type='sawtooth';o.frequency.value=80+Math.random()*40; const g=ctx.createGain();g.gain.value=.03;g.gain.exponentialRampToValueAtTime(.001,ctx.currentTime+.5); const f=ctx.createBiquadFilter();f.type='bandpass';f.frequency.value=200;f.Q.value=2; o.connect(f);f.connect(g);g.connect(ctx.destination);o.start();o.stop(ctx.currentTime+.5); } function playSting(){ if(!listener)return;const ctx=listener.context; const o=ctx.createOscillator();o.type='sawtooth';o.frequency.value=60; o.frequency.exponentialRampToValueAtTime(30,ctx.currentTime+1); const g=ctx.createGain();g.gain.value=.1;g.gain.exponentialRampToValueAtTime(.001,ctx.currentTime+1.5); const f=ctx.createBiquadFilter();f.type='lowpass';f.frequency.value=150; o.connect(f);f.connect(g);g.connect(ctx.destination);o.start();o.stop(ctx.currentTime+1.5); } /* ===== HORROR ===== */ function triggerHorror(intensity){ const ov=document.getElementById('distort'); if(intensity==='subtle'){ov.style.opacity='.4';setTimeout(()=>ov.style.opacity='0',150);if(Math.random()>.5)playWhisper()} else if(intensity==='medium'){playCreak();ov.style.opacity='.7';setTimeout(()=>ov.style.opacity='0',300);cam.userData.shake=.3;setTimeout(()=>cam.userData.shake=0,400)} else if(intensity==='strong'){playSting();ov.style.opacity='1';setTimeout(()=>ov.style.opacity='0',500);cam.userData.shake=.6;setTimeout(()=>cam.userData.shake=0,800); if(entObj){const eg=entObj.g;eg.visible=true;entObj.bm.opacity=.3;entObj.hm.opacity=.3;entObj.el.intensity=.5; const a=Math.random()*Math.PI*2;eg.position.set(cam.position.x+Math.cos(a)*3,0,cam.position.z+Math.sin(a)*3);eg.lookAt(cam.position.x,0,cam.position.z); setTimeout(()=>{entObj.bm.opacity=0;entObj.hm.opacity=0;entObj.el.intensity=0;setTimeout(()=>eg.visible=false,500)},1500)} } } /* ===== COLLISION ===== */ function checkCol(pos){ const pb=new THREE.Box3(new THREE.Vector3(pos.x-PR,pos.y-PH/2,pos.z-PR),new THREE.Vector3(pos.x+PR,pos.y+PH/2,pos.z+PR)); const tb=new THREE.Box3(); for(const c of colls){tb.setFromObject(c);if(pb.intersectsBox(tb))return true} return false; } /* ===== GAME LOOP ===== */ function update(dt){ if(S.screen!=='game'||!S.locked||S.noteOpen)return; const p=players[S.activeP];if(!p)return; const spd=K['ShiftLeft']?SSPD:MSPD; // Build forward/right from yaw const fwd=new THREE.Vector3(-Math.sin(p.yaw),0,-Math.cos(p.yaw)); const rt=new THREE.Vector3(Math.cos(p.yaw),0,-Math.sin(p.yaw)); const dir=new THREE.Vector3(); if(S.activeP===0){ if(K['KeyW'])dir.add(fwd);if(K['KeyS'])dir.sub(fwd);if(K['KeyD'])dir.add(rt);if(K['KeyA'])dir.sub(rt); }else{ if(K['ArrowUp'])dir.add(fwd);if(K['ArrowDown'])dir.sub(fwd);if(K['ArrowRight'])dir.add(rt);if(K['ArrowLeft'])dir.sub(rt); } let moving=false; if(dir.lengthSq()>0){ dir.normalize();const np=p.pos.clone().add(dir.clone().multiplyScalar(spd*dt)); if(!checkCol(np))p.pos.copy(np); else{const nx=p.pos.clone();nx.x+=dir.x*spd*dt;if(!checkCol(nx))p.pos.x=nx.x; const nz=p.pos.clone();nz.z+=dir.z*spd*dt;if(!checkCol(nz))p.pos.z=nz.z} moving=true; } // Apply camera cam.position.set(p.pos.x,PH,p.pos.z); cam.rotation.order='YXZ';cam.rotation.y=p.yaw;cam.rotation.x=p.pitch; // Flashlight target const flFwd=new THREE.Vector3();cam.getWorldDirection(flFwd);flT.position.copy(cam.position).add(flFwd.multiplyScalar(5)); // Footsteps if(moving){S.ftT-=dt;if(S.ftT<=0){playStep();S.ftT=K['ShiftLeft']?.3:.45}}else S.ftT=0; // Interaction updInteract(); // Foyer flicker if(scene._fL){const f=scene._fL;const t=clock.elapsedTime; f.intensity=Math.max(.1,f._bi+Math.sin(t*15)*.1+Math.sin(t*23)*.05+(Math.random()>.97?-.5:0)); if(f._flame){f._flame.scale.y=1.5+Math.sin(t*12)*.3;f._flame.position.x=Math.sin(t*8)*.003}} // Camera shake if(cam.userData.shake){const s=cam.userData.shake;cam.position.x+=(Math.random()-.5)*s*.1;cam.position.z+=(Math.random()-.5)*s*.1;cam.userData.shake*=.95;if(cam.userData.shake<.01)cam.userData.shake=0} // Horror S.horrorT-=dt;if(S.horrorT<=0){triggerHorror(['subtle','subtle','subtle','medium'][Math.floor(Math.random()*4)]);S.horrorT=8+Math.random()*15} if(Math.random()<.002)playCreak();if(Math.random()<.001)playWhisper(); } function updInteract(){ const p=players[S.activeP];if(!p)return; const fwd=new THREE.Vector3(-Math.sin(p.yaw),0,-Math.cos(p.yaw)); const camDir=new THREE.Vector3(0,0,-1).applyQuaternion(cam.quaternion); rc.set(cam.position,camDir);rc.far=IDIST; let closest=null,cd=Infinity; for(const obj of inters){ if(obj.type==='item'&&S.p[S.activeP].inv.includes(obj.iid))continue; if(obj.type==='door'&&S.opened.has(obj.id))continue; const pos=obj.getPos();rv.copy(pos).sub(cam.position);const dist=rv.length(); if(dist>IDIST)continue;rv.normalize();if(camDir.dot(rv)<IANGLE)continue; if(dist<cd){cd=dist;closest=obj} } const pr=document.getElementById('prompt'),ch=document.getElementById('crosshair'); if(closest){let lb=closest.label;if(closest.type==='note'&&S.p[S.activeP].readN.has(closest.nid))lb+=' (read)'; if(closest.type==='exit'&&S.solved)lb='Front Door — Open'; pr.textContent='[E] '+lb;pr.classList.add('vis');ch.classList.add('act'); S._interact=closest; }else{pr.classList.remove('vis');ch.classList.remove('act');S._interact=null} } /* ===== INPUT ===== */ document.addEventListener('keydown',e=>{ K[e.code]=true; if(e.code==='Tab'){e.preventDefault();switchPlayer();return} if(e.code==='KeyE'&&S.locked&&!S.noteOpen&&S._interact){S._interact.act()} if(e.code==='KeyU'&&S.locked&&!S.noteOpen&&S._interact){S._interact.act()} }); document.addEventListener('keyup',e=>{K[e.code]=false}); document.addEventListener('mousemove',e=>{ if(!S.locked||S.noteOpen)return; const p=players[S.activeP];if(!p)return; const sens=.002; if(S.activeP===0){p.yaw-=e.movementX*sens;p.pitch-=e.movementY*sens} // P2 mouse look not used (uses IJKL instead) }); // P2 look keys - handled in update via continuous key checking document.getElementById('note-panel').addEventListener('click',hideNote); document.getElementById('game-screen').addEventListener('click',()=>{if(S.screen==='game'&&!S.locked&&!S.noteOpen)controls.lock()}); function switchPlayer(){ S.activeP=S.activeP===0?1:0; const p=players[S.activeP];if(!p)return; cam.position.set(p.pos.x,PH,p.pos.z); cam.rotation.order='YXZ';cam.rotation.y=p.yaw;cam.rotation.x=p.pitch; updInv();updPInd();showMsg('Switched to Player '+(S.activeP+1),1200); } // P2 look handled in update const origUpdate=update; const patchedUpdate=function(dt){ // P2 look from IJKL keys if(S.activeP===1&&S.locked&&!S.noteOpen){ const p=players[1];if(p){ const ls=.03; if(K['KeyJ'])p.yaw+=ls; if(K['KeyL'])p.yaw-=ls; if(K['KeyI'])p.pitch=Math.min(Math.PI/2-.1,p.pitch+ls); if(K['KeyK'])p.pitch=Math.max(-Math.PI/2+.1,p.pitch-ls); } } origUpdate(dt); }; /* ===== MENU ===== */ document.getElementById('btn-start').addEventListener('click',startGame); document.getElementById('btn-re').addEventListener('click',()=>location.reload()); function startGame(){ switchScreen('game');document.getElementById('lt').style.display='block'; initThree();initTex();buildHouse();setupLights();mkPlayer(0);mkPlayer(1);mkEntity(); const resumeA=()=>{if(listener.context.state==='suspended')listener.context.resume();document.removeEventListener('click',resumeA);document.removeEventListener('keydown',resumeA)}; document.addEventListener('click',resumeA);document.addEventListener('keydown',resumeA); setTimeout(()=>{document.getElementById('lt').style.display='none';initAudio();controls.lock();updInv();updPInd()},1200); function animate(){requestAnimationFrame(animate);const dt=Math.min(clock.getDelta(),.05);patchedUpdate(dt);ren.render(scene,cam)} animate(); } </script> </body> </html>
Landing Page
This ad does not have a landing page available
Network Timeline
Performance Summary

12

Requests

6

Domains

690KB

Transfer Size

2043KB

Content Size

614.0ms

Dom Content Loaded

408.0ms

First Paint

758.0ms

Load Time
Domain Breakdown
Transfer Size (bytes)
Loading...
Content Size (bytes)
Loading...
Header Size (bytes)
Loading...
Requests
Loading...
Timings (ms)
Loading...
Total Time
Loading...
Content Breakdown
Transfer Size (bytes)
Loading...
Content Size (bytes)
Loading...
Header Size (bytes)
Loading...
Requests
Loading...