Sketch 2026-03-06 00:04

This sketch creates a 3D first-person horror game inspired by Roblox's DOORS, where players navigate an infinite hallway of numbered rooms while avoiding the entity 'Rush' that spawns randomly and hunts them down. Players can hide in closets, open doors, and experience atmospheric lighting effects and audio cues that build tension.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make the hallway wider — Increase ROOM_WIDTH from 600 to 1200 so there's much more space to move side-to-side, making it easier to escape Rush
  2. Slow down Rush's speed — Reduce Rush's movement speed so it takes longer to catch you, giving more time to find a closet
  3. Make the player faster — Increase MOVE_SPEED so WASD movement is quicker, making it easier to reach closets or doors
  4. Change Rush's starting distance — Modify the spawn distance (2000) to make Rush appear closer or farther behind you when it spawns
  5. Make closets easier to find — Increase closet interaction distance from 150 to 300, so you can hide from farther away
  6. Brighten the dark lighting — Increase ambient light during the flicker warning from 10 to 40, so you can see better even when lights are off
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch recreates the core gameplay loop of the Roblox horror game DOORS: navigate through an infinite hallway of numbered rooms, open doors to progress, and survive encounters with 'Rush'—a terrifying entity that spawns randomly and chases you down. The sketch uses p5.js WEBGL for 3D rendering, procedurally generated room geometry with textures, camera controls via mouse look with pointer lock, and audio synthesis to create atmospheric tension. It demonstrates how to build a complete game experience combining 3D graphics, collision detection, entity AI, and interactive UI overlays.

The code is organized into three main classes—Player, Room, and Rush—that handle movement, world generation, and enemy behavior respectively. The setup() function initializes the canvas, audio system, and first room, while the draw() function runs the game loop that updates player position, checks interactions, spawns entities, and renders everything from the player's perspective. By studying it, you'll learn how to combine WEBGL geometry, procedural texture generation, pointer lock for immersive mouse control, collision detection in 3D space, and state management to create a complete interactive experience.

⚙️ How It Works

  1. When the sketch loads, setup() creates a WEBGL canvas, initializes p5.sound oscillators for ambient and rush audio, generates procedural textures for walls/floors/doors/closets/rush face, creates the player object, and generates the first room. Pointer lock is requested on canvas click for immersive mouse look.
  2. Every frame, draw() updates the game state: player input updates camera position and rotation, collision detection keeps the player inside the room bounds, entity spawning logic randomly summons Rush after a few seconds, and a lighting system flickers to warn the player when Rush arrives.
  3. The Player class handles WASD movement and mouse look (yaw/pitch rotation), checks for nearby interactive objects like doors and closets, and manages hiding state. When the player presses E near a door, it opens; pressing E near a closet toggles hiding inside.
  4. The Room class procedurally generates hallway geometry—floor, ceiling, two side walls, a door wall with the numbered door, and 1-3 closets randomly placed on the walls. Each room stores its Z position, and new rooms are generated automatically as the player progresses forward.
  5. The Rush class represents the chasing entity: it spawns several rooms behind the player and moves forward at constant speed. As it approaches, audio volume increases via distance mapping. If Rush gets within 200 pixels of the player and the player is not hiding, the player dies; otherwise Rush despawns after passing room 0.
  6. The game state machine tracks whether the player is PLAYING or DEAD. Death triggers a full-screen overlay with the reason and a respawn button. Interactive UI overlays (crosshair, interaction prompt, room counter, death screen) are updated each frame via updateHUD() and managed through HTML/CSS.

🎓 Concepts You'll Learn

WEBGL 3D RenderingProcedural Texture GenerationFirst-Person Camera and Pointer LockCollision Detection (AABB)Entity AI and State ManagementAudio Synthesis and Distance-Based EffectsGame State MachinesInteractive Object Detection

📝 Code Breakdown

generateTextures()

generateTextures() uses createGraphics() to build five textures procedurally—this is much faster and more flexible than loading image files. Each texture is drawn into an off-screen buffer and stored in a global object so room geometry can apply them via the texture() function in WEBGL mode.

🔬 This creates a checkerboard floor—what happens if you swap the rect coordinates to g.rect(100,0,100,100); g.rect(0,100,100,100)? Try it and see how the checker pattern flips.

  // 2. Floor Texture (Carpet)
  g = createGraphics(200, 200);
  g.background(30, 30, 40);
  g.noStroke();
  g.fill(40, 40, 50);
  g.rect(0,0,100,100); g.rect(100,100,100,100); // Checkers
  textures.floor = g;
function generateTextures() {
  // 1. Wall Texture (Wallpaper)
  let g = createGraphics(200, 200);
  g.background(50, 40, 30); // Dark brown
  g.stroke(70, 60, 50);
  g.strokeWeight(2);
  for(let i=0; i<200; i+=20) g.line(i, 0, i, 200); // Stripes
  textures.wall = g;

  // 2. Floor Texture (Carpet)
  g = createGraphics(200, 200);
  g.background(30, 30, 40);
  g.noStroke();
  g.fill(40, 40, 50);
  g.rect(0,0,100,100); g.rect(100,100,100,100); // Checkers
  textures.floor = g;

  // 3. Door Texture
  g = createGraphics(100, 200);
  g.background(60, 40, 20);
  g.fill(40, 20, 10);
  g.rect(10, 10, 80, 180); // Panel
  g.fill(200);
  g.circle(85, 100, 10); // Handle
  textures.door = g;

  // 4. Wardrobe Texture
  g = createGraphics(200, 400);
  g.background(50, 30, 10);
  g.fill(30, 10, 0);
  g.rect(10, 10, 180, 380); // Inset
  g.fill(100);
  g.rect(20, 50, 160, 10); // Slats
  g.rect(20, 80, 160, 10);
  g.rect(20, 110, 160, 10);
  textures.closet = g;

  // 5. Rush Face (Procedural Horror)
  g = createGraphics(256, 256);
  g.clear(); // Transparent background
  g.fill(30); g.noStroke(); g.circle(128, 128, 200); // Head
  g.fill(0); 
  g.ellipse(80, 100, 50, 70); // Eye L
  g.ellipse(176, 100, 50, 70); // Eye R
  g.fill(255); // Teeth
  g.rect(60, 160, 136, 40);
  textures.rush = g;
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

for-loop Wall Stripe Generation for(let i=0; i<200; i+=20) g.line(i, 0, i, 200);

Draws evenly-spaced vertical lines to create a striped wallpaper effect

calculation Floor Checkerboard g.rect(0,0,100,100); g.rect(100,100,100,100);

Draws alternating squares to create a checkerboard carpet pattern

calculation Rush Face Assembly g.circle(128, 128, 200); g.fill(0); g.ellipse(80, 100, 50, 70); g.ellipse(176, 100, 50, 70);

Procedurally draws a horrific face with a dark head and large void-black eyes

let g = createGraphics(200, 200);
Creates an off-screen graphics buffer 200×200 pixels to draw textures into without affecting the main canvas
g.background(50, 40, 30);
Fills the graphics buffer with a dark brown color that will be the wallpaper base
for(let i=0; i<200; i+=20) g.line(i, 0, i, 200);
Loops 10 times, drawing vertical lines spaced 20 pixels apart to create vertical stripes on the wall
textures.wall = g;
Stores this completed texture in the global textures object so it can be applied to wall geometry later
g.rect(0,0,100,100); g.rect(100,100,100,100);
Draws two light-colored squares diagonally opposite to create a classic checkerboard floor pattern
g.clear();
Makes the Rush face texture transparent so only the drawn features (head, eyes, teeth) are visible
g.circle(128, 128, 200);
Draws a large dark circle at the center to form the head of the Rush entity
g.ellipse(80, 100, 50, 70);
Draws a tall dark ellipse on the left side to represent the left eye—tall proportions make it unsettling

setup()

setup() runs exactly once when the sketch starts. It initializes the 3D canvas, audio context, textures, the player object, and the first room. The pointer lock setup is critical for immersive first-person games—it hides the cursor and locks mouse movement to the canvas, allowing smooth camera control without the cursor leaving the window.

function setup() {
  createCanvas(windowWidth, windowHeight, WEBGL);
  
  // Audio Setup
  userStartAudio();
  ambOsc = new p5.Oscillator('sine');
  ambOsc.freq(60); ambOsc.amp(0.05); ambOsc.start();
  
  rushOsc = new p5.Noise('pink');
  rushOsc.amp(0); rushOsc.start();
  
  generateTextures();
  
  // Initialize Player
  player = new Player();
  
  // Generate first room
  rooms.push(new Room(0, 0));
  
  // Pointer lock for immersive mouse look
  document.addEventListener('click', () => requestPointerLock());
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

calculation Audio System Initialization userStartAudio(); ambOsc = new p5.Oscillator('sine'); ambOsc.freq(60); ambOsc.amp(0.05); ambOsc.start();

Enables audio context and creates a low-frequency sine wave for ambient background tone

calculation Pointer Lock Setup document.addEventListener('click', () => requestPointerLock());

Captures mouse movement exclusively when canvas is clicked, allowing immersive first-person camera control

createCanvas(windowWidth, windowHeight, WEBGL);
Creates a fullscreen WEBGL canvas (3D rendering mode) that fills the entire window
userStartAudio();
Initializes the p5.sound audio system—required before creating oscillators or playing sounds
ambOsc = new p5.Oscillator('sine');
Creates a sine wave oscillator that will produce a smooth, continuous tone for ambient background atmosphere
ambOsc.freq(60);
Sets the oscillator frequency to 60 Hz, a very low subharmonic frequency that creates eerie tension
ambOsc.amp(0.05);
Sets volume to 5% to create a quiet background sound that builds dread without drowning out other audio
rushOsc = new p5.Noise('pink');
Creates pink noise (a filtered random signal) that will play when Rush is nearby—less piercing than white noise
rushOsc.amp(0);
Starts with volume 0 (silent) so it only becomes audible when Rush spawns and gets close
player = new Player();
Creates the player object, initializing their position, camera angle, and interaction state
rooms.push(new Room(0, 0));
Generates the first room at position 0 and adds it to the rooms array
document.addEventListener('click', () => requestPointerLock());
When the canvas is clicked, requests pointer lock to hide the cursor and capture all mouse movement for camera control

draw()

draw() is the game loop, running 60 times per second. It orchestrates all updates (logic phase), then renders the scene (render phase). The optimization comment is important: rendering only 3 rooms instead of all 50 means the GPU does 94% less work, keeping the frame rate smooth even on slow devices.

function draw() {
  if (gameState === "DEAD") return;

  background(0);
  
  // --- LOGIC ---
  handleLighting();
  player.update();
  handleEntity();
  
  // --- RENDER ---
  player.applyCamera();
  
  // Lighting
  if (lightsOn) {
    ambientLight(60);
    // Flashlight (Point light following player)
    pointLight(255, 240, 200, player.pos.x, player.pos.y, player.pos.z);
    // Distant light
    directionalLight(50, 50, 60, 0, 0, -1);
  } else {
    ambientLight(10); // Pitch black
  }
  
  // Draw Rooms
  // Optimization: Only draw current, prev, and next room
  for (let i = max(0, currentRoomIndex - 1); i < min(rooms.length, currentRoomIndex + 2); i++) {
    rooms[i].display();
  }
  
  // Draw Entity
  if (activeEntity) activeEntity.display();
  
  // HUD Update (DOM elements)
  updateHUD();
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

conditional Death State Guard if (gameState === "DEAD") return;

Immediately exits draw if player is dead, stopping all game logic and rendering

conditional Dynamic Lighting if (lightsOn) { ambientLight(60); pointLight(255, 240, 200, player.pos.x, player.pos.y, player.pos.z); directionalLight(50, 50, 60, 0, 0, -1); } else { ambientLight(10); }

Switches between full visibility with three light sources and near-darkness when lights flicker off

for-loop Optimized Room Rendering for (let i = max(0, currentRoomIndex - 1); i < min(rooms.length, currentRoomIndex + 2); i++) { rooms[i].display(); }

Only draws the previous, current, and next room to avoid rendering overhead from rooms far away

if (gameState === "DEAD") return;
If the player is dead, exit immediately without updating or rendering anything—stops the game
background(0);
Clears the screen to black each frame, erasing the previous frame so new geometry can be drawn
handleLighting();
Updates the lighting state—flickers lights on/off when Rush spawns to warn the player
player.update();
Updates player position based on WASD input, applies mouse look via pointer lock, and checks for nearby interactive objects
handleEntity();
Spawns Rush randomly, updates its position, checks if it caught the player, and removes it if it despawned
player.applyCamera();
Sets the 3D camera to the player's position and look direction, so the scene is rendered from their perspective
if (lightsOn) { ambientLight(60); pointLight(255, 240, 200, player.pos.x, player.pos.y, player.pos.z); directionalLight(50, 50, 60, 0, 0, -1); } else { ambientLight(10); }
If lights are on, adds three light sources (ambient, flashlight-like point light, and directional); if off, reduces ambient to near-black
for (let i = max(0, currentRoomIndex - 1); i < min(rooms.length, currentRoomIndex + 2); i++) { rooms[i].display(); }
Loops through and draws only the previous, current, and next room—skip distant rooms to save GPU performance
if (activeEntity) activeEntity.display();
If Rush is spawned, renders it with its glitched appearance and particle trail
updateHUD();
Updates on-screen text like the interaction prompt and room counter

handleLighting()

handleLighting() manages the flickering warning system. When Rush spawns, flickerTimer is set to 180 (3 seconds at 60 FPS). Each frame, the timer counts down and the lights strobe. This visual cue alerts the player that danger is approaching before they can see or hear Rush clearly.

function handleLighting() {
  if (flickerTimer > 0) {
    flickerTimer--;
    if (frameCount % 5 === 0) lightsOn = !lightsOn;
  } else {
    lightsOn = true;
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Flicker Logic if (frameCount % 5 === 0) lightsOn = !lightsOn;

Every 5 frames, toggles lights on/off to create a visual strobe effect warning the player

if (flickerTimer > 0) {
If there are frames remaining in the flicker sequence, continue the flickering effect
flickerTimer--;
Decrement the flicker timer by 1 each frame, counting down to when flickering stops
if (frameCount % 5 === 0) lightsOn = !lightsOn;
Every 5th frame (frameCount % 5 === 0), flip the lights on/off to create a strobing effect
} else { lightsOn = true; }
When flicker timer reaches 0, turn lights back on permanently

spawnRush()

spawnRush() is called by handleEntity() when random conditions are met. It ensures only one Rush exists at a time, positions it behind the player, and triggers the visual warning system. The 2000-unit offset gives the player crucial seconds to find a hiding spot before the entity is visible.

function spawnRush() {
  if (activeEntity) return;
  // Rush starts 3 rooms back and moves forward
  let startZ = rooms[currentRoomIndex].z + 2000; 
  activeEntity = new Rush(startZ);
  flickerTimer = 180; // 3 seconds of warning lights
}
Line-by-line explanation (4 lines)
if (activeEntity) return;
If Rush is already active, don't spawn another one—prevents multiple Rushes from appearing simultaneously
let startZ = rooms[currentRoomIndex].z + 2000;
Places Rush's starting position 2000 units behind the current room (about 2 rooms back) so the player has warning before it appears
activeEntity = new Rush(startZ);
Creates a new Rush object at the calculated position and stores it in the global activeEntity variable
flickerTimer = 180;
Sets flicker timer to 180 frames (3 seconds at 60 FPS), triggering the warning light strobe effect

handleEntity()

handleEntity() is the central hub for Rush behavior: it controls spawning frequency (every 10 seconds), spawn probability (40%), and the state machine that prevents movement during the warning flicker. This structure allows tuning difficulty by adjusting spawn rate and probability values.

🔬 This spawns Rush every 10 seconds (600 frames) with 40% probability. What happens if you change the 600 to 300? Rush will try to spawn twice as often—making the game much harder. Try it!

  // Random Spawn Logic
  if (!activeEntity && frameCount % 600 === 0 && currentRoomIndex > 1) {
    if (random() < 0.4) spawnRush();
  }
function handleEntity() {
  // Random Spawn Logic
  if (!activeEntity && frameCount % 600 === 0 && currentRoomIndex > 1) {
    if (random() < 0.4) spawnRush();
  }

  if (activeEntity) {
    // Wait for flicker to finish before moving
    if (flickerTimer <= 0) {
        activeEntity.update();
        if (activeEntity.finished) activeEntity = null;
    }
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Spawn Probability Check if (!activeEntity && frameCount % 600 === 0 && currentRoomIndex > 1) { if (random() < 0.4) spawnRush(); }

Every 10 seconds (600 frames), if no entity exists and player is past room 1, roll a 40% chance to spawn Rush

if (!activeEntity && frameCount % 600 === 0 && currentRoomIndex > 1) {
Three conditions: no Rush is active, 600 frames (10 seconds) have passed, and player is past the first room—all must be true to attempt spawn
if (random() < 0.4) spawnRush();
Generates a random number 0-1; if it's less than 0.4 (40% chance), spawn Rush
if (activeEntity) {
If Rush is currently active, update it
if (flickerTimer <= 0) { activeEntity.update(); if (activeEntity.finished) activeEntity = null; }
Don't move Rush while lights are flickering (giving player warning time)—only update once flicker finishes; remove Rush when it despawns

Player.update()

Player.update() is the most complex function, handling three systems: mouse look (uses movedX/movedY from pointer lock), 3D movement (WASD converted to world-space X/Z vectors using trigonometry), and collision detection (prevents wall penetration and requires doors to open before progressing). The key insight is that 3D movement in a hallway uses sin/cos of the yaw angle to rotate movement vectors from player-relative (forward/strafe) to world-relative (X/Z).

🔬 These four lines map WASD to 3D movement—W and S move forward/back, A and D strafe left/right. What happens if you swap the S line with the W line? Your controls will be completely reversed (W goes backward, S goes forward). Try it and predict the chaos!

    if (keyIsDown(87)) { moveX -= dx; moveZ -= dz; } // W
    if (keyIsDown(83)) { moveX += dx; moveZ += dz; } // S
    if (keyIsDown(65)) { moveX -= dz; moveZ += dx; } // A (Strafe)
    if (keyIsDown(68)) { moveX += dz; moveZ -= dx; } // D (Strafe)
update() {
    // MOUSE LOOK
    if (document.pointerLockElement === canvas) {
      this.yaw += movedX * LOOK_SENSITIVITY;
      this.pitch += movedY * LOOK_SENSITIVITY;
      this.pitch = constrain(this.pitch, -PI/2.1, PI/2.1);
    }

    if (this.isHiding) {
      // Check Exit
      this.checkInteraction();
      if (keyIsDown(69)) { // E Key debounce handled in KeyPressed? No, lets do check here
         // Handled in keyPressed
      }
      return; 
    }

    // MOVEMENT
    let fwd = createVector(0, 0, -1).rotate(this.yaw);
    let right = createVector(1, 0, 0).rotate(this.yaw); // p5 3D rotates around Y axis usually
    
    // Actually in WebGL Y is up. So rotating around Y is correct for yaw.
    // However, createVector rotation is 2D.
    // Custom 3D rotation logic:
    let dx = sin(this.yaw) * MOVE_SPEED;
    let dz = cos(this.yaw) * MOVE_SPEED; // Moving in -Z direction is forward usually
    
    // Z is negative into screen.
    let moveX = 0;
    let moveZ = 0;
    
    if (keyIsDown(87)) { moveX -= dx; moveZ -= dz; } // W
    if (keyIsDown(83)) { moveX += dx; moveZ += dz; } // S
    if (keyIsDown(65)) { moveX -= dz; moveZ += dx; } // A (Strafe)
    if (keyIsDown(68)) { moveX += dz; moveZ -= dx; } // D (Strafe)
    
    // COLLISION (Simple AABB)
    let newX = this.pos.x + moveX;
    let newZ = this.pos.z + moveZ;
    
    // Walls (X axis)
    if (newX > -ROOM_WIDTH/2 + this.w && newX < ROOM_WIDTH/2 - this.w) {
      this.pos.x = newX;
    }
    
    // Doors (Z Axis)
    // Check if within bounds of current room Z range
    // If attempting to leave room Z bounds:
    let room = rooms[currentRoomIndex];
    let minZ = room.z - ROOM_LENGTH/2;
    let maxZ = room.z + ROOM_LENGTH/2;
    
    // Allow walking forward into next room IF door is open
    let doorOpen = room.doorOpen;
    
    // Simple Z collision logic for infinite hallway
    if (newZ < minZ) {
      // Trying to go forward (negative Z)
      if (doorOpen) {
         // Proceed
         this.pos.z = newZ;
         // Trigger next room generation if we cross threshold
         if (currentRoomIndex < rooms.length - 1 && this.pos.z < rooms[currentRoomIndex+1].z + ROOM_LENGTH/2) {
             currentRoomIndex++;
         } else if (currentRoomIndex === rooms.length - 1) {
             // Generate new room
             rooms.push(new Room(rooms.length, rooms[rooms.length-1].z - ROOM_LENGTH));
             currentRoomIndex++;
         }
      } else {
         // Hit door
      }
    } else {
       // Moving back or inside room
       this.pos.z = newZ;
    }
    
    this.checkInteraction();
  }
Line-by-line explanation (11 lines)

🔧 Subcomponents:

conditional Pointer Lock Mouse Look if (document.pointerLockElement === canvas) { this.yaw += movedX * LOOK_SENSITIVITY; this.pitch += movedY * LOOK_SENSITIVITY; this.pitch = constrain(this.pitch, -PI/2.1, PI/2.1); }

Updates camera angles (yaw/pitch) based on mouse movement when pointer lock is active

calculation 3D Movement Vectors let dx = sin(this.yaw) * MOVE_SPEED; let dz = cos(this.yaw) * MOVE_SPEED; if (keyIsDown(87)) { moveX -= dx; moveZ -= dz; }

Converts yaw angle and WASD input into 3D movement along X and Z axes in world space

conditional Room Boundary Collision if (newX > -ROOM_WIDTH/2 + this.w && newX < ROOM_WIDTH/2 - this.w) { this.pos.x = newX; }

Prevents player from walking through left and right walls by clamping X position within bounds

conditional Door Collision and Room Progression if (newZ < minZ) { if (doorOpen) { this.pos.z = newZ; if (currentRoomIndex < rooms.length - 1 && this.pos.z < rooms[currentRoomIndex+1].z + ROOM_LENGTH/2) { currentRoomIndex++; } else if (currentRoomIndex === rooms.length - 1) { rooms.push(new Room(rooms.length, rooms[rooms.length-1].z - ROOM_LENGTH)); currentRoomIndex++; } } }

Only allows forward movement when door is open; triggers room progression and generates new rooms as player advances

if (document.pointerLockElement === canvas) {
Checks if pointer lock is active (cursor is captured by the canvas)
this.yaw += movedX * LOOK_SENSITIVITY;
Horizontal mouse movement updates yaw (left/right rotation)—multiplied by LOOK_SENSITIVITY to control responsiveness
this.pitch += movedY * LOOK_SENSITIVITY;
Vertical mouse movement updates pitch (up/down rotation)
this.pitch = constrain(this.pitch, -PI/2.1, PI/2.1);
Clamps pitch to prevent player from looking too far up/down (PI/2.1 radians ≈ 85°) to avoid disorienting upside-down views
if (this.isHiding) { this.checkInteraction(); return; }
If player is hiding in a closet, skip movement entirely—only allow checking for interaction (exiting)
let dx = sin(this.yaw) * MOVE_SPEED; let dz = cos(this.yaw) * MOVE_SPEED;
Converts yaw angle into X and Z direction vectors using sine/cosine—basis for all WASD movement calculations
if (keyIsDown(87)) { moveX -= dx; moveZ -= dz; }
If W is pressed, move forward by subtracting the yaw direction (negative direction moves forward in WEBGL)
if (keyIsDown(65)) { moveX -= dz; moveZ += dx; }
If A is pressed (strafe left), move perpendicular to forward direction
if (newX > -ROOM_WIDTH/2 + this.w && newX < ROOM_WIDTH/2 - this.w) { this.pos.x = newX; }
Only update X position if it stays within the hallway walls—prevents walking through left/right walls
if (newZ < minZ) { if (doorOpen) { this.pos.z = newZ; ... }
If trying to move forward past the door: check if door is open; if so, move forward and potentially switch to next room
this.checkInteraction();
At end of update, scan for nearby interactive objects (doors and closets) and update interaction state

Player.checkInteraction()

checkInteraction() runs every frame to detect nearby interactive objects. It resets state each frame, then checks (in priority order) if the player is already hiding (can exit), if they're near the door, or if they're near any closets. The dist() function efficiently calculates Euclidean distance in 2D space, making proximity detection simple and fast.

🔬 This checks if you're within 200 units of the door AND the door is closed. What happens if you change the condition to if (dDoor < 1000)—removing the && !room.doorOpen check? You'll be able to interact even with open doors, which doesn't break anything but lets you test what happens when you press E on an already-open door.

    // 1. Check Door
    // Dist to door (Door is at room.z - ROOM_LENGTH/2)
    let doorZ = room.z - ROOM_LENGTH/2;
    let dDoor = dist(this.pos.x, this.pos.z, 0, doorZ);
    
    if (dDoor < 200 && !room.doorOpen) {
checkInteraction() {
    this.canInteract = false;
    this.interactObj = null;
    
    let room = rooms[currentRoomIndex];
    
    if (this.isHiding) {
      this.canInteract = true;
      this.interactType = "CLOSET";
      return;
    }
    
    // 1. Check Door
    // Dist to door (Door is at room.z - ROOM_LENGTH/2)
    let doorZ = room.z - ROOM_LENGTH/2;
    let dDoor = dist(this.pos.x, this.pos.z, 0, doorZ);
    
    if (dDoor < 200 && !room.doorOpen) {
      // Check looking angle (simplified)
      this.canInteract = true;
      this.interactObj = room;
      this.interactType = "DOOR";
      return;
    }
    
    // 2. Check Closets
    for (let c of room.closets) {
      let dCloset = dist(this.pos.x, this.pos.z, c.x, c.z);
      if (dCloset < 150) {
        this.canInteract = true;
        this.interactObj = c;
        this.interactType = "CLOSET";
        return;
      }
    }
  }
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Door Proximity Detection let dDoor = dist(this.pos.x, this.pos.z, 0, doorZ); if (dDoor < 200 && !room.doorOpen) { this.canInteract = true; this.interactObj = room; this.interactType = "DOOR"; return; }

Calculates distance to the door at the end of the room; enables door interaction if within 200 units and door is closed

for-loop Closet Proximity Check for (let c of room.closets) { let dCloset = dist(this.pos.x, this.pos.z, c.x, c.z); if (dCloset < 150) { ... } }

Loops through all closets in the current room; enables closet interaction if player is within 150 units of any closet

this.canInteract = false; this.interactObj = null;
Reset interaction state each frame—only nearby objects will set it to true
if (this.isHiding) { this.canInteract = true; this.interactType = "CLOSET"; return; }
If player is already hiding, allow exiting the closet (pressing E again) and skip all other interaction checks
let doorZ = room.z - ROOM_LENGTH/2;
Door is positioned at the far end of the current room (negative Z direction from room center)
let dDoor = dist(this.pos.x, this.pos.z, 0, doorZ);
Calculates 2D distance from player to door center—dist() finds Euclidean distance
if (dDoor < 200 && !room.doorOpen) {
Door interaction is available if: player is within 200 units AND the door is currently closed
for (let c of room.closets) { let dCloset = dist(this.pos.x, this.pos.z, c.x, c.z); if (dCloset < 150) {
Loops through each closet in the room; enables interaction if player gets within 150 units of any closet

Player.interact()

interact() is called when the player presses E or clicks. It's split into two branches: door opening (one-time action) and closet toggling (reversible). The closet logic is elegant: the ternary operator checks if the closet is on the right wall (x > 0) and ejects the player in the opposite direction.

interact() {
    if (!this.canInteract) return;
    
    if (this.interactType === "DOOR") {
      this.interactObj.doorOpen = true;
      // Sound
      let osc = new p5.Oscillator('triangle');
      osc.freq(100); osc.amp(0.5); osc.start(); osc.stop(0.5);
    }
    
    if (this.interactType === "CLOSET") {
      if (this.isHiding) {
        // Exit
        this.isHiding = false;
        // Move player slightly out
        this.pos.x = this.hidingIn.x + (this.hidingIn.x > 0 ? -100 : 100);
        this.pos.z = this.hidingIn.z;
        this.hidingIn = null;
      } else {
        // Enter
        this.isHiding = true;
        this.hidingIn = this.interactObj;
        // Snap pos
        this.pos.x = this.interactObj.x;
        this.pos.z = this.interactObj.z;
        // Force look out
        this.yaw = (this.interactObj.x > 0) ? PI/2 : -PI/2; 
      }
    }
  }
Line-by-line explanation (10 lines)

🔧 Subcomponents:

conditional Door Opening Mechanics if (this.interactType === "DOOR") { this.interactObj.doorOpen = true; let osc = new p5.Oscillator('triangle'); osc.freq(100); osc.amp(0.5); osc.start(); osc.stop(0.5); }

Sets the door's open flag and creates a one-shot sound effect

conditional Closet Hide/Exit Toggle if (this.interactType === "CLOSET") { if (this.isHiding) { ... } else { ... } }

Toggles between hiding in and exiting the closet, repositioning player and updating state

if (!this.canInteract) return;
Guard clause: if there's nothing to interact with, exit immediately
if (this.interactType === "DOOR") {
If the nearby object is a door, execute door logic
this.interactObj.doorOpen = true;
Set the door's open flag to true so collision detection will allow the player to pass through
let osc = new p5.Oscillator('triangle');
Create a temporary triangle wave oscillator for the door sound effect
osc.freq(100); osc.amp(0.5); osc.start(); osc.stop(0.5);
Set frequency to 100 Hz, volume to 50%, start playing, and stop after 0.5 seconds (one-shot sound)
if (this.isHiding) {
If player is currently hiding, pressing E will exit the closet
this.pos.x = this.hidingIn.x + (this.hidingIn.x > 0 ? -100 : 100);
Teleport player slightly outside the closet (100 units away)—left if closet is on right wall, right if closet is on left wall
this.isHiding = true; this.hidingIn = this.interactObj;
Enter hiding mode and store reference to the closet object
this.pos.x = this.interactObj.x; this.pos.z = this.interactObj.z;
Snap player position to the closet center—player is now 'inside' the closet
this.yaw = (this.interactObj.x > 0) ? PI/2 : -PI/2;
Force player to look sideways toward the closet door (so they look out while hiding)

Player.applyCamera()

applyCamera() converts the player's position, yaw, and pitch into a 3D camera view using p5.js's camera() function. The camera target is calculated by adding sine/cosine offsets from the player position—this ensures the camera always looks in the direction of yaw and pitch angles. The (0, 1, 0) up vector keeps the horizon level.

applyCamera() {
    camera(this.pos.x, -this.h, this.pos.z, 
           this.pos.x - sin(this.yaw), -this.h + sin(this.pitch), this.pos.z - cos(this.yaw), 
           0, 1, 0);
  }
Line-by-line explanation (3 lines)
camera(this.pos.x, -this.h, this.pos.z,
First three arguments are the camera position (eye): player X, negative height (camera is at player height, negated because Y is up), player Z
this.pos.x - sin(this.yaw), -this.h + sin(this.pitch), this.pos.z - cos(this.yaw),
Next three arguments are the target point (where camera looks): offset from player position by the yaw and pitch angles using sine/cosine
0, 1, 0);
Last three arguments are the up vector (0, 1, 0 = Y axis points up), which keeps the camera from rolling sideways

Room.display()

Room.display() is the rendering engine for hallway geometry. It uses push/pop to manage transformation matrices, ensuring each piece of geometry (floor, ceiling, walls, door, closets) is positioned and oriented correctly relative to the room's center. The key technique is rotateX() and rotateY() to turn 2D planes into 3D-facing geometry.

🔬 These two blocks draw the left and right walls—they use rotateY(PI/2) and rotateY(-PI/2) respectively, which are opposite rotations. What happens if you change both to rotateY(PI/2)? Both walls will rotate the same way, and one will be invisible or flipped. Try it!

    // WALL LEFT
    push();
    translate(-ROOM_WIDTH/2, -ROOM_HEIGHT/2, 0);
    rotateY(PI/2);
    texture(textures.wall);
    plane(ROOM_LENGTH, ROOM_HEIGHT);
    pop();

    // WALL RIGHT
    push();
    translate(ROOM_WIDTH/2, -ROOM_HEIGHT/2, 0);
    rotateY(-PI/2);
    texture(textures.wall);
    plane(ROOM_LENGTH, ROOM_HEIGHT);
    pop();
display() {
    push();
    translate(0, 0, this.z);
    
    // FLOOR
    push();
    translate(0, 0, 0);
    rotateX(PI/2);
    texture(textures.floor);
    plane(ROOM_WIDTH, ROOM_LENGTH);
    pop();
    
    // CEILING
    push();
    translate(0, -ROOM_HEIGHT, 0);
    rotateX(PI/2);
    fill(20);
    plane(ROOM_WIDTH, ROOM_LENGTH);
    pop();
    
    // WALL LEFT
    push();
    translate(-ROOM_WIDTH/2, -ROOM_HEIGHT/2, 0);
    rotateY(PI/2);
    texture(textures.wall);
    plane(ROOM_LENGTH, ROOM_HEIGHT);
    pop();

    // WALL RIGHT
    push();
    translate(ROOM_WIDTH/2, -ROOM_HEIGHT/2, 0);
    rotateY(-PI/2);
    texture(textures.wall);
    plane(ROOM_LENGTH, ROOM_HEIGHT);
    pop();
    
    // DOOR WALL (Front/Back)
    // Only draw the "Far" wall with the door
    push();
    translate(0, -ROOM_HEIGHT/2, -ROOM_LENGTH/2);
    
    // Wall part around door
    fill(10);
    // Left of door
    beginShape(); vertex(-ROOM_WIDTH/2, -ROOM_HEIGHT/2); vertex(-50, -ROOM_HEIGHT/2); vertex(-50, ROOM_HEIGHT/2); vertex(-ROOM_WIDTH/2, ROOM_HEIGHT/2); endShape();
    // Right of door
    beginShape(); vertex(50, -ROOM_HEIGHT/2); vertex(ROOM_WIDTH/2, -ROOM_HEIGHT/2); vertex(ROOM_WIDTH/2, ROOM_HEIGHT/2); vertex(50, ROOM_HEIGHT/2); endShape();
    // Top of door
    beginShape(); vertex(-50, -ROOM_HEIGHT/2); vertex(50, -ROOM_HEIGHT/2); vertex(50, -100); vertex(-50, -100); endShape();
    
    // THE DOOR ITSELF
    if (!this.doorOpen) {
      push();
      translate(0, 50, 0); // Center of door plane
      texture(textures.door);
      plane(100, 200);
      
      // Door Number
      translate(0, -80, 2);
      fill(255); noStroke();
      textAlign(CENTER); textSize(40);
      text((this.index + 1).toString().padStart(4, '0'), 0, 0);
      pop();
    }
    
    pop();
    
    // DRAW CLOSETS
    for (let c of room.closets) {
      push();
      translate(c.x, -ROOM_HEIGHT/2, c.z - this.z); // Adjust relative Z
      // Face inward
      if (c.x > 0) rotateY(-PI/2);
      else rotateY(PI/2);
      
      texture(textures.closet);
      box(200, 350, 50); // Wardrobe dimensions
      pop();
    }
    
    pop();
  }
Line-by-line explanation (11 lines)

🔧 Subcomponents:

calculation Floor and Ceiling Planes rotateX(PI/2); texture(textures.floor); plane(ROOM_WIDTH, ROOM_LENGTH);

Rotates a plane 90° around X-axis to lay it flat on the ground (or ceiling), then applies texture

calculation Side Wall Pair translate(-ROOM_WIDTH/2, -ROOM_HEIGHT/2, 0); rotateY(PI/2); texture(textures.wall); plane(ROOM_LENGTH, ROOM_HEIGHT);

Positions and rotates a plane to form a vertical wall, applied twice for left and right sides

calculation Door Wall Geometry beginShape(); vertex(-ROOM_WIDTH/2, -ROOM_HEIGHT/2); vertex(-50, -ROOM_HEIGHT/2); vertex(-50, ROOM_HEIGHT/2); vertex(-ROOM_WIDTH/2, ROOM_HEIGHT/2); endShape();

Draws three filled polygons (left wall section, right wall section, top section) around the door opening

for-loop Closet Rendering Loop for (let c of room.closets) { push(); translate(c.x, -ROOM_HEIGHT/2, c.z - this.z); ... box(200, 350, 50); pop(); }

Loops through each closet and renders it as a textured 3D box at its world position

push(); translate(0, 0, this.z);
Save the current transformation matrix, then move everything to this room's Z position (each room is offset along the Z-axis)
rotateX(PI/2); texture(textures.floor); plane(ROOM_WIDTH, ROOM_LENGTH);
Rotates a plane 90° to lay it flat, applies floor texture, and draws it at full room width and length
translate(0, -ROOM_HEIGHT, 0);
Moves the ceiling up by ROOM_HEIGHT units so it floats above the floor
translate(-ROOM_WIDTH/2, -ROOM_HEIGHT/2, 0); rotateY(PI/2);
Positions the left wall at the far left edge and rotates it 90° to stand vertically
translate(0, -ROOM_HEIGHT/2, -ROOM_LENGTH/2);
Moves to the position of the door wall (far end of the room), centered horizontally, at wall height
beginShape(); vertex(-ROOM_WIDTH/2, -ROOM_HEIGHT/2); vertex(-50, -ROOM_HEIGHT/2); vertex(-50, ROOM_HEIGHT/2); vertex(-ROOM_WIDTH/2, ROOM_HEIGHT/2); endShape();
Draws a rectangle from the far left corner to 50 units left of center—this is the left half of the door wall
if (!this.doorOpen) {
Only draw the door itself if it hasn't been opened yet
texture(textures.door); plane(100, 200);
Applies the door texture and draws a 100×200 unit plane (the visible door)
text((this.index + 1).toString().padStart(4, '0'), 0, 0);
Renders the room number padded to 4 digits (0001, 0002, etc.) on the door
for (let c of room.closets) { translate(c.x, -ROOM_HEIGHT/2, c.z - this.z); ... box(200, 350, 50); }
Loops through closets: positions each one at its XZ coordinates and draws a 3D box with closet texture
pop();
Restore the transformation matrix, so subsequent rooms are positioned correctly

Rush.update()

Rush.update() implements the core threat mechanic: constant forward movement, distance-based audio feedback (you hear it getting louder), and collision detection with death or safety depending on hiding state. The map() function elegantly converts a 0-3000 unit distance into a 0-1.0 volume range, creating audio-based spatial awareness.

🔬 This checks if Rush is within 200 units—if so, the player dies (unless hiding). What happens if you change 200 to 1000? Rush's kill distance becomes much larger, so you die from farther away—making the game way harder. Try it and see how much earlier you need to hide!

    // Kill Logic
    if (distToPlayer < 200) {
      if (player.isHiding) {
        // Safe
      } else {
        die("You died to Rush.");
      }
    }
update() {
    this.z -= this.speed; // Move negative Z (towards door 0)
    
    // Sound & Shake Logic
    let distToPlayer = abs(this.z - player.pos.z);
    
    // Volume based on distance
    let vol = map(distToPlayer, 0, 3000, 1.0, 0, true);
    rushOsc.amp(vol);
    
    // Kill Logic
    if (distToPlayer < 200) {
      if (player.isHiding) {
        // Safe
      } else {
        die("You died to Rush.");
      }
    }
    
    // Despawn
    if (this.z < rooms[0].z - 2000) {
      this.finished = true;
      rushOsc.amp(0);
    }
  }
Line-by-line explanation (7 lines)

🔧 Subcomponents:

calculation Distance-Based Audio Volume let distToPlayer = abs(this.z - player.pos.z); let vol = map(distToPlayer, 0, 3000, 1.0, 0, true); rushOsc.amp(vol);

Calculates distance to player and maps it to audio volume—louder as Rush gets closer, creating audio-based threat sensing

conditional Rush Collision and Death if (distToPlayer < 200) { if (player.isHiding) { } else { die("You died to Rush."); } }

If Rush gets within 200 units: check if player is hiding (safe) or exposed (death)

this.z -= this.speed;
Moves Rush forward at constant speed (40 units per frame) toward the player
let distToPlayer = abs(this.z - player.pos.z);
Calculates distance between Rush and player along the Z-axis (hallway length)
let vol = map(distToPlayer, 0, 3000, 1.0, 0, true);
Maps distance (0 to 3000 units) to volume (1.0 to 0): far away = silent, close = loud. The 'true' parameter clamps the value so it never goes outside 0-1.0
rushOsc.amp(vol);
Updates the Rush oscillator volume so the player hears it getting louder/quieter with distance
if (distToPlayer < 200) {
If Rush is within 200 units of the player, the collision check triggers
if (player.isHiding) { } else { die("You died to Rush."); }
If player is hiding, they're safe (do nothing); if exposed, they die and see the death screen
if (this.z < rooms[0].z - 2000) { this.finished = true; rushOsc.amp(0); }
If Rush travels past room 0 (far behind where it started), mark it as finished and silence the audio

Rush.display()

Rush.display() renders the entity using two effects: a jittering 300×300 plane with the procedural face texture (creates the glitch effect), and 5 random particle planes scattered around it (creates an eerie disintegration trail). The randomness each frame makes Rush look unstable and corrupted, enhancing the horror atmosphere.

display() {
    push();
    translate(0, -150, this.z);
    
    // Face player
    // Simple billboard
    texture(textures.rush);
    // Add glitch shake
    translate(random(-10,10), random(-10,10), 0);
    plane(300, 300);
    
    // Particle trail
    fill(50); noStroke();
    for(let i=0; i<5; i++) {
        push();
        translate(random(-100,100), random(-100,100), random(50, 200));
        plane(20, 20);
        pop();
    }
    
    pop();
  }
Line-by-line explanation (3 lines)

🔧 Subcomponents:

calculation Glitch Shake Effect translate(random(-10,10), random(-10,10), 0); plane(300, 300);

Adds random offset to Rush's position each frame to create a glitchy, unstable visual effect

for-loop Particle Trail for(let i=0; i<5; i++) { push(); translate(random(-100,100), random(-100,100), random(50, 200)); plane(20, 20); pop(); }

Draws 5 random small planes around Rush to create a ghostly, disintegrating particle effect

push(); translate(0, -150, this.z);
Save transformation, then position Rush's geometry at its Z coordinate and 150 units below the ceiling (so it appears at roughly head height)
texture(textures.rush); translate(random(-10,10), random(-10,10), 0); plane(300, 300);
Applies the horrific Rush face texture, adds random jitter to position (±10 units), and draws a 300×300 plane—the glitch effect makes it look unstable and terrifying
fill(50); noStroke(); for(let i=0; i<5; i++) { ... plane(20, 20); }
Loops 5 times, drawing small dark planes at random positions around Rush to create a particle trail of disintegration

keyPressed()

keyPressed() is a p5.js event handler that fires once when any key is pressed. Here it checks for the E key and triggers interaction. This is the simplest way to handle one-time input (unlike keyIsDown, which checks continuous state).

function keyPressed() {
  if (gameState === "DEAD") return;
  if (keyCode === 69) { // E
    player.interact();
  }
}
Line-by-line explanation (3 lines)
if (gameState === "DEAD") return;
If the game is over, ignore all key input
if (keyCode === 69) {
Checks if the E key (ASCII code 69) was pressed
player.interact();
Calls the player's interact function to open doors or enter/exit closets

mousePressed()

mousePressed() fires once per click. It provides an alternative interaction method to the E key, making the game accessible to different input preferences.

function mousePressed() {
  if (gameState === "DEAD") return;
  // Alternative interaction
  player.interact();
}
Line-by-line explanation (2 lines)
if (gameState === "DEAD") return;
If the game is over, ignore mouse clicks
player.interact();
Allows clicking the mouse to interact with doors and closets—an alternative to pressing E

die()

die() is the end-game sequence. It changes game state, shows the death overlay, silences audio, and releases pointer lock. This is a complete and clean way to transition from playing to dead state.

function die(reason) {
  gameState = "DEAD";
  document.getElementById('death-screen').style.display = 'flex';
  document.getElementById('death-reason').innerText = reason;
  rushOsc.stop();
  ambOsc.stop();
  exitPointerLock();
}
Line-by-line explanation (5 lines)
gameState = "DEAD";
Sets the game state to DEAD, which causes draw() to exit immediately and freeze the game
document.getElementById('death-screen').style.display = 'flex';
Shows the death screen overlay by changing its CSS display from 'none' to 'flex'
document.getElementById('death-reason').innerText = reason;
Updates the death reason text to show the player why they died
rushOsc.stop(); ambOsc.stop();
Stops both audio oscillators so silence follows death
exitPointerLock();
Releases pointer lock, returning control of the cursor to the user so they can click the respawn button

updateHUD()

updateHUD() is called every frame and updates the DOM elements that overlay the 3D scene. It reads the player's current interaction state and displays context-sensitive prompts, and displays the room counter to show progress. Separating UI update logic into its own function keeps the code organized.

function updateHUD() {
  let interactText = "";
  if (player.canInteract) {
    if (player.interactType === "DOOR") interactText = "[E] OPEN DOOR";
    if (player.interactType === "CLOSET") interactText = player.isHiding ? "[E] EXIT" : "[E] HIDE";
  }
  document.getElementById('interact').innerText = interactText;
  
  // Pad room number
  let roomStr = (currentRoomIndex + 1).toString().padStart(4, '0');
  document.getElementById('room-counter').innerText = roomStr;
}
Line-by-line explanation (7 lines)
let interactText = "";
Initialize the interaction prompt as an empty string
if (player.canInteract) {
Only show interaction prompts if something is nearby to interact with
if (player.interactType === "DOOR") interactText = "[E] OPEN DOOR";
If near a door, show the door opening prompt
interactText = player.isHiding ? "[E] EXIT" : "[E] HIDE";
If near a closet, show different text depending on whether player is already hiding: 'EXIT' if hiding, 'HIDE' if not
document.getElementById('interact').innerText = interactText;
Updates the on-screen DOM element with the interaction prompt
let roomStr = (currentRoomIndex + 1).toString().padStart(4, '0');
Converts room index (0-based) to 1-based number, converts to string, and pads with zeros (0 → '0001', 99 → '0100')
document.getElementById('room-counter').innerText = roomStr;
Updates the on-screen room counter display

📦 Key Variables

MOVE_SPEED number

Controls how many units the player moves per frame when holding WASD keys—higher values feel faster

const MOVE_SPEED = 8;
LOOK_SENSITIVITY number

Multiplier for mouse movement to camera rotation—higher values make the camera turn faster when you move the mouse

const LOOK_SENSITIVITY = 0.002;
ROOM_LENGTH number

Z-axis depth of each room (how long the hallway is front-to-back)

const ROOM_LENGTH = 1000;
ROOM_WIDTH number

X-axis width of the hallway (side-to-side distance)

const ROOM_WIDTH = 600;
ROOM_HEIGHT number

Y-axis height of each room (floor to ceiling)

const ROOM_HEIGHT = 400;
player object

Stores the Player instance containing position, rotation, interaction state, and all player logic

let player;
rooms array

Array of Room objects—initially contains one room, grows as player progresses forward

let rooms = [];
currentRoomIndex number

Index of the room the player is currently in, used to track progress and for rendering optimization

let currentRoomIndex = 0;
gameState string

Tracks whether the game is currently 'PLAYING' or 'DEAD'—controls whether logic updates execute

let gameState = "PLAYING";
textures object

Stores procedurally generated textures for walls, floor, door, closet, and rush—applied to 3D geometry via texture()

let textures = {};
activeEntity object

Stores the current Rush entity if spawned, or null if none exists—ensures only one Rush exists at a time

let activeEntity = null;
flickerTimer number

Countdown timer for the warning light flicker effect—counts down from 180 (3 seconds) to 0

let flickerTimer = 0;
lightsOn boolean

Current state of the room lights—true means lights are on and visible, false means nearly dark

let lightsOn = true;
ambOsc object

p5.Oscillator instance that plays ambient low-frequency background sound throughout the game

let ambOsc;
rushOsc object

p5.Noise instance that plays ominous pink noise when Rush is nearby—volume increases as Rush gets closer

let rushOsc;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG Rush spawn logic in spawnRush()

Rush spawns 2000 units behind the current room, but if the player moves backward rapidly, Rush could spawn ahead of them, making it instantly kill them without warning

💡 Clamp Rush's spawn position to always be behind the player: let startZ = max(rooms[currentRoomIndex].z + 2000, player.pos.z + 2000);

PERFORMANCE Room.display() closet rendering

Every frame, the code recalculates closet positions and renders them even if they're far away and invisible

💡 Add distance checks: only render closets if they're within a certain distance of the player using dist(player.pos.x, player.pos.z, c.x, c.z)

BUG Player.update() Z collision logic

The door Z collision code only prevents backward movement when door is closed, but doesn't handle the case where currentRoomIndex becomes invalid or out of bounds

💡 Add a bounds check: if (currentRoomIndex >= 0 && currentRoomIndex < rooms.length) before accessing rooms[currentRoomIndex]

FEATURE Rush class

Rush only moves in a straight line and can be easily avoided by standing still in a closet—no intelligence or pathfinding

💡 Add simple AI: detect if player is in line of sight and adjust Rush's path slightly toward them using lerp()

STYLE Global variables at top of sketch

Mix of constants (MOVE_SPEED, ROOM_LENGTH) and mutable state (player, currentRoomIndex, gameState) in the same scope makes the code harder to reason about

💡 Group constants into a CONFIG object and game state into a GAME object to improve organization and clarity

BUG Pointer lock setup in setup()

requestPointerLock() may fail silently on some browsers if not called in response to a user gesture—the setup() call happens before any click

💡 Move the pointer lock request into the click handler in mousePressed() or create a 'Click to Lock' UI element that explicitly triggers it

🔄 Code Flow

Code flow showing generatetextures, setup, draw, handleLighting, spawnrush, handleentity, playerupdate, checkinteraction, interact, applycamera, roomdisplay, rushupdate, rushdisplay, keypressed, mousepressed, die, updatehud

💡 Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> game-state-check[game-state-check] game-state-check -->|if not dead| handleLighting[handleLighting] handleLighting --> lighting-setup[lighting-setup] lighting-setup --> room-render-loop[room-render-loop] room-render-loop --> applycamera[applyCamera] applycamera --> playerupdate[Player.update] playerupdate --> mouse-look[mouse-look] mouse-look --> movement-calculation[movement-calculation] movement-calculation --> collision-check[collision-check] collision-check --> door-logic[door-logic] door-logic --> door-distance-check[door-distance-check] door-distance-check --> checkinteraction[checkInteraction] checkinteraction --> closet-loop[closet-loop] closet-loop --> interact[interact] interact --> door-open[door-open] interact --> closet-toggle[closet-toggle] draw --> flicker-loop[flicker-loop] flicker-loop --> handleentity[handleEntity] handleentity --> spawn-condition[spawn-condition] spawn-condition --> spawnrush[spawnRush] spawnrush --> rushupdate[Rush.update] rushupdate --> collision-check-rush[collision-check-rush] collision-check-rush --> rushdisplay[Rush.display] rushdisplay --> particle-trail[particle-trail] particle-trail --> draw draw --> updatehud[updateHUD] click setup href "#fn-setup" click draw href "#fn-draw" click game-state-check href "#sub-game-state-check" click handleLighting href "#fn-handleLighting" click lighting-setup href "#sub-lighting-setup" click room-render-loop href "#sub-room-render-loop" click applycamera href "#fn-applycamera" click playerupdate href "#fn-playerupdate" click mouse-look href "#sub-mouse-look" click movement-calculation href "#sub-movement-calculation" click collision-check href "#sub-collision-check" click door-logic href "#sub-door-logic" click door-distance-check href "#sub-door-distance-check" click checkinteraction href "#fn-checkinteraction" click closet-loop href "#sub-closet-loop" click interact href "#fn-interact" click door-open href "#sub-door-open" click closet-toggle href "#sub-closet-toggle" click flicker-loop href "#sub-flicker-loop" click handleentity href "#fn-handleentity" click spawn-condition href "#sub-spawn-condition" click spawnrush href "#fn-spawnrush" click rushupdate href "#fn-rushupdate" click collision-check-rush href "#sub-collision-check-rush" click rushdisplay href "#fn-rushdisplay" click particle-trail href "#sub-particle-trail" click updatehud href "#fn-updatehud"

❓ Frequently Asked Questions

What visual elements are featured in the Sketch 2026-03-06 00:04?

This sketch creates a 3D environment with various textured walls, floors, and doors, alongside a procedural horror character represented as a 'Rush' face.

How can users interact with the Sketch 2026-03-06 00:04?

Users can navigate through the 3D space using movement controls, exploring different rooms and potentially encountering the active entity.

What creative coding techniques are showcased in the Sketch 2026-03-06 00:04?

The sketch demonstrates procedural texture generation and 3D rendering in p5.js, utilizing graphics to create immersive environments and entities.

Preview

Sketch 2026-03-06 00:04 - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Sketch 2026-03-06 00:04 - Code flow showing generatetextures, setup, draw, handleLighting, spawnrush, handleentity, playerupdate, checkinteraction, interact, applycamera, roomdisplay, rushupdate, rushdisplay, keypressed, mousepressed, die, updatehud
Code Flow Diagram