A browser horror game built with Three.js.
You wake up in an endless maze of yellow rooms. Explore, manage your sanity, and find a telephone before the game fades out around you.
- Node.js 22+
- npm 11+
npm install
npm run devnpm run dev
npm run lint
npm run lint:fix
npm run build
npm run preview- Move with
W,A,S,D - Look with the mouse
- Answer a phone with
E - On mobile, use the virtual joystick and tap the phone directly
The objective is simple: reach a phone before sanity reaches zero.
- Infinite chunk-based Backrooms maze
- Sanity system with escalating visual and audio effects
- Procedural world generation with deterministic chunk layout
- Web Audio ambient soundscape
- Mobile touch controls
- Low-sanity bacteria encounters
- Shader-based wake-up and fade transitions
- Quality presets (
?quality=desktop|mobile|low) and a profiling overlay (?profile)
src/main.js: menu entry; lazy-loads the runtime when a level startssrc/runtime.js: game runtime and frame loopsrc/world.js: chunk generation, cached world data, wall spatial index, line of sightsrc/audio.js: sound playback and distortion pipelinesrc/input.js: desktop and mobile inputsrc/hud.js: sanity bar and interaction promptssrc/entity.js: bacteria logicsrc/models.js: materials, geometry, and GLTF loadingsrc/menu.js: level-selection menusrc/levels.js: level definitionssrc/random.js: shared runtime randomnesssrc/shaders/: shader definitions
The project uses a strict ESLint setup with:
@eslint/jseslint-plugin-importeslint-plugin-sonarjseslint-plugin-unicorn
It also includes local checks for Three.js hot paths. Render and update functions should reuse temporary math objects instead of allocating THREE.Vector2, THREE.Vector3, THREE.Box3, or large typed arrays during gameplay.
Run:
npm run lintand keep it clean.
npm run buildThe build is code-split: the menu loads first and the game runtime (including Three.js) loads when a level starts. Vite still warns about the size of the Three.js vendor chunk; that is expected.