Lethal ape: redux update

This sketch creates an immersive top-down horror game where you navigate a multi-floor facility while evading AI-controlled monsters. Device tilt controls the camera view, touch controls let you move and run, and you can collect items, use a scrap machine, and access a mod menu to tweak gameplay settings.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make the player twice as fast — Increase the walk and run speeds to see the player zip around the facility at double speed.
  2. Make monsters super aggressive — Monsters will detect and chase the player from much farther away, making the game harder.
  3. Spawn way more monsters — The facility will be crawling with monsters, ramping up the difficulty significantly.
  4. Change the player color — The player's body will be drawn in a different color—try changing 200, 0, 0 (red) to other RGB values.
  5. Disable the scary background drone
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a full-featured horror game that combines multiple advanced p5.js techniques: device orientation for camera control, touch input for joystick movement, procedural audio generation with p5.sound (oscillators and envelopes), sprite-like rendering of the player and monsters, collision detection, and state machine design. The player explores a facility across multiple connected rooms while avoiding procedurally-drawn creatures with intelligent chase behavior.

The code is organized into distinct systems: game state management (loading, start screen, game, jumpscare, mod menu), player input (touch joystick and run button), monster AI with room-aware pathfinding, camera control with device tilt and smoothing, and multiple UI layers (inventory, monster tracker, interaction buttons). By studying it, you will learn how to structure a complex game with multiple game states, implement touch-based mobile controls, create procedural sound effects, and design simple but convincing AI behavior for non-player characters.

⚙️ How It Works

  1. When the sketch loads, preload() initializes all procedural audio using p5.sound oscillators and envelopes—these create ambient drones, footsteps, and jumpscare screams without loading audio files.
  2. setup() creates the canvas, defines five floor rooms and two safe zones (mainRoom and monsterRoom), spawns the player in the safe zone, places monsters randomly on floors 0-4, and initializes touch input handlers for the joystick and run button.
  3. The game starts in a loading screen, transitions to a start screen on touch, then enters the main game state where draw() runs at 60 FPS.
  4. In the game state, updatePlayer() handles joystick/keyboard movement, collectible collision, room transitions via staircases, and boombox audio proximity. updateMonsters() runs AI for each monster: they roam normally but chase the player if spotted, and intelligently navigate between floors using staircases when the player is on a different floor.
  5. updateCamera() calculates the camera position centered on the player, then adds device orientation offsets (gamma for horizontal, beta for vertical tilt) if VR mode is enabled, smoothly interpolating to prevent jerky movement.
  6. Every frame, the game world is drawn with push/translate to apply camera offset, then the UI (joystick, buttons, inventory, monster tracker) is drawn on top without camera translation. If a monster collides with the player, triggerJumpscare() activates an intense multi-phase animation with coordinated sound and visual effects before resetting the game.

🎓 Concepts You'll Learn

Game state machineTouch input and mobile joystickDevice orientation (accelerometer) integrationProcedural audio with p5.soundMulti-room navigation and collision detectionAI pathfinding across multiple roomsCamera control and smoothingProcedural graphics and drawingInventory systemInteractive UI buttons

📝 Code Breakdown

preload()

preload() runs once before setup() and is the ideal place to initialize audio objects and other resources. p5.sound oscillators must be created and started here so they're ready when the game begins. The ADSR envelope (Attack, Decay, Sustain, Release) shapes how a sound fades in and out, crucial for realistic audio effects.

function preload() {
  ambientSoundOsc = new p5.Oscillator('sine');
  ambientSoundOsc.freq(50);
  ambientSoundOsc.amp(0);
  ambientSoundOsc.start();

  ambientSoundNoise = new p5.Noise('white');
  ambientSoundNoise.amp(0);
  ambientSoundNoise.start();

  ambientFilter = new p5.Filter('lowpass');
  ambientFilter.freq(500);
  ambientFilter.res(2);
  ambientSoundNoise.disconnect();
  ambientSoundNoise.connect(ambientFilter);
  ambientFilter.connect();

  jumpscareNoise = new p5.Noise('white');
  jumpscareNoise.amp(0);
  jumpscareNoise.start();

  jumpscareOsc = new p5.Oscillator('sawtooth');
  jumpscareOsc.freq(1);
  jumpscareOsc.amp(0);
  jumpscareOsc.start();

  jumpscareEnvelope = new p5.Envelope();
  jumpscareEnvelope.setADSR(0.01, 0.2, 0.0, 0.5);
  jumpscareEnvelope.setRange(1.0, 0);
  jumpscareNoise.amp(jumpscareEnvelope);

  footstepOsc = new p5.Oscillator('sine');
  footstepOsc.amp(0);
  footstepOsc.start();
  footstepEnvelope = new p5.Envelope();
  footstepEnvelope.setADSR(0.01, 0.1, 0, 0.1);
  footstepOsc.amp(footstepEnvelope);

  collectOsc = new p5.Oscillator('triangle');
  collectOsc.amp(0);
  collectOsc.start();
  collectEnvelope = new p5.Envelope();
  collectEnvelope.setADSR(0.01, 0.05, 0, 0.2);
  collectOsc.amp(collectEnvelope);

  scrapCollectOsc = new p5.Oscillator('square');
  scrapCollectOsc.amp(0);
  scrapCollectOsc.start();
  scrapCollectEnv = new p5.Envelope();
  scrapCollectEnv.setADSR(0.01, 0.08, 0, 0.15);
  scrapCollectEnv.setRange(0.7, 0);
  scrapCollectOsc.amp(scrapCollectEnv);

  buyItemOsc = new p5.Oscillator('sine');
  buyItemOsc.amp(0);
  buyItemOsc.start();
  buyItemEnv = new p5.Envelope();
  buyItemEnv.setADSR(0.01, 0.1, 0, 0.2);
  buyItemEnv.setRange(0.8, 0);
  buyItemOsc.amp(buyItemEnv);

  sellScrapOsc = new p5.Oscillator('triangle');
  sellScrapOsc.amp(0);
  sellScrapOsc.start();
  sellScrapEnv = new p5.Envelope();
  sellScrapEnv.setADSR(0.01, 0.08, 0, 0.15);
  sellScrapEnv.setRange(0.6, 0);
  sellScrapOsc.amp(sellScrapEnv);

  loadingSoundOsc = new p5.Oscillator('sine');
  loadingSoundOsc.freq(100);
  loadingSoundOsc.amp(0);
  loadingSoundOsc.start();
  loadingSoundEnv = new p5.Envelope();
  loadingSoundEnv.setADSR(0.1, 0.5, 0, 0.5);
  loadingSoundEnv.setRange(0.5, 0);
  loadingSoundOsc.amp(loadingSoundEnv);

  boombox = {
    osc: new p5.Oscillator('triangle'),
    delay: new p5.Delay(),
    nearbyThreshold: 200,
    fadeZone: 100
  };
  boombox.osc.freq(220);
  boombox.osc.amp(0);
  boombox.osc.start();
  boombox.osc.disconnect();
  boombox.delay.process(boombox.osc, 0.5, 0.7, 2300);
  boombox.delay.amp(0);
  boombox.delay.connect();
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

audio-setup Ambient Drone Oscillator ambientSoundOsc = new p5.Oscillator('sine'); ambientSoundOsc.freq(50); ambientSoundOsc.start();

Creates a low-frequency sine wave drone (50 Hz) that runs continuously to provide an ominous atmospheric hum

audio-setup Filtered Ambient Noise ambientSoundNoise.disconnect(); ambientSoundNoise.connect(ambientFilter);

Connects white noise through a low-pass filter to create a subtle hiss/static effect without harsh highs

audio-setup Jumpscare Envelope jumpscareEnvelope.setADSR(0.01, 0.2, 0.0, 0.5); jumpscareEnvelope.setRange(1.0, 0);

Defines how the jumpscare noise fades in quickly (10ms attack) and decays dramatically for a startling effect

audio-setup Boombox with Echo boombox.delay.process(boombox.osc, 0.5, 0.7, 2300);

Adds a 0.5-second delayed echo with 70% feedback to create a room-like reverb effect for the music

ambientSoundOsc = new p5.Oscillator('sine');
Creates a sine wave oscillator for the ambient background drone—sine waves are smooth and good for low-frequency tones
ambientSoundOsc.freq(50);
Sets the oscillator frequency to 50 Hz, which is below human hearing but felt as vibration, creating unease
ambientSoundOsc.amp(0); ambientSoundOsc.start();
Starts the oscillator silently (amp 0)—it will fade in later when the game begins
ambientSoundNoise = new p5.Noise('white'); ambientSoundNoise.start();
Creates white noise (all frequencies equally loud) and starts it, which will be filtered to reduce harshness
ambientFilter = new p5.Filter('lowpass'); ambientFilter.freq(500);
Creates a low-pass filter set to 500 Hz, allowing only frequencies below 500 Hz through—removes high hissing
ambientSoundNoise.disconnect(); ambientSoundNoise.connect(ambientFilter);
Disconnects noise from the default speaker output and reroutes it through the filter instead
jumpscareOsc = new p5.Oscillator('sawtooth'); jumpscareOsc.freq(1);
Creates a harsh sawtooth wave oscillator for the jumpscare screech—sawtooth contains many harmonics for a terrifying sound
jumpscareEnvelope.setADSR(0.01, 0.2, 0.0, 0.5);
Sets up the jumpscare envelope: 10ms attack (very fast), 200ms decay, no sustain, 500ms release—for a sudden shock
boombox.delay.process(boombox.osc, 0.5, 0.7, 2300);
Creates a delay effect with 0.5-second delay time and 70% feedback, making the music echo convincingly

setup()

setup() runs once when the sketch starts and prepares all the game data structures. For games, setup() typically initializes the canvas, creates all entities (rooms, player, enemies), defines level layouts, and sets starting state. Organizing data into objects (like the rooms and staircases) makes it easy to modify level design without touching game logic.

function setup() {
  createCanvas(windowWidth, windowHeight);
  noStroke();
  textAlign(CENTER, CENTER);

  rooms = {
    mainRoom: { x: 0, y: 0, w: 600, h: 400, color: '#333333', floor: -1,
      scrapMachine: { x: 500, y: 150, w: 80, h: 100 },
      lockers: [
        { x: 100, y: 300, w: 40, h: 60 },
        { x: 150, y: 300, w: 40, h: 60 },
        { x: 200, y: 300, w: 40, h: 60 }
      ]
    },
    monsterRoom: { x: 600, y: 0, w: 400, h: 400, color: '#222222', floor: -1 },
    floor0: { x: 0, y: 400, w: 800, h: 600, color: '#554444', floor: 0 },
    floor1: { x: 800, y: 400, w: 800, h: 600, color: '#445544', floor: 1 },
    floor2: { x: 1600, y: 400, w: 800, h: 600, color: '#665555', floor: 2 },
    floor3: { x: 2400, y: 400, w: 800, h: 600, color: '#556655', floor: 3 },
    floor4: { x: 3200, y: 400, w: 800, h: 600, color: '#776666', floor: 4 }
  };

  player = {
    x: rooms.mainRoom.x + 100,
    y: rooms.mainRoom.y + 100,
    w: 30,
    h: 30,
    vx: 0,
    vy: 0,
    isMoving: false,
    isRunning: false
  };

  boombox.x = rooms.mainRoom.x + rooms.mainRoom.w / 2;
  boombox.y = rooms.mainRoom.y + rooms.mainRoom.h / 2;
  boombox.w = 80;
  boombox.h = 60;

  itemsForSale = [
    { name: "Speed Boost", cost: 10, type: "speedBoost", effect: () => { player.isRunning = true; }, description: "Run faster for a limited time." },
    { name: "Health Pack", cost: 5, type: "healthPack", description: "Restores health (not implemented yet)." }
  ];

  staircases = [
    { x: rooms.mainRoom.x + 200, y: rooms.mainRoom.y + rooms.mainRoom.h - 50, w: 200, h: 50, targetRoom: "floor0", targetPlayerX: rooms.floor0.x + 300, targetPlayerY: rooms.floor0.y + 50 },
    { x: rooms.floor0.x + 300, y: rooms.floor0.y, w: 200, h: 50, targetRoom: "mainRoom", targetPlayerX: rooms.mainRoom.x + 300, targetPlayerY: rooms.mainRoom.y + rooms.mainRoom.h - player.h },
    { x: rooms.mainRoom.x + rooms.mainRoom.w - 50, y: rooms.mainRoom.y + 150, w: 50, h: 100, targetRoom: "monsterRoom", targetPlayerX: rooms.monsterRoom.x + 50, targetPlayerY: rooms.monsterRoom.y + 200 },
    { x: rooms.monsterRoom.x, y: rooms.monsterRoom.y + 150, w: 50, h: 100, targetRoom: "mainRoom", targetPlayerX: rooms.mainRoom.x + rooms.mainRoom.w - 50, targetPlayerY: rooms.mainRoom.y + 200 },
    { x: rooms.floor0.x + rooms.floor0.w - 50, y: rooms.floor0.y + 200, w: 50, h: 200, targetRoom: "floor1", targetPlayerX: rooms.floor1.x + 50, targetPlayerY: rooms.floor1.y + 300 },
    { x: rooms.floor1.x, y: rooms.floor1.y + 200, w: 50, h: 200, targetRoom: "floor0", targetPlayerX: rooms.floor0.x + rooms.floor0.w - 50, targetPlayerY: rooms.floor0.y + 300 },
    { x: rooms.floor1.x + rooms.floor1.w - 50, y: rooms.floor1.y + 200, w: 50, h: 200, targetRoom: "floor2", targetPlayerX: rooms.floor2.x + 50, targetPlayerY: rooms.floor2.y + 300 },
    { x: rooms.floor2.x, y: rooms.floor2.y + 200, w: 50, h: 200, targetRoom: "floor1", targetPlayerX: rooms.floor1.x + rooms.floor1.w - 50, targetPlayerY: rooms.floor1.y + 300 },
    { x: rooms.floor2.x + rooms.floor2.w - 50, y: rooms.floor2.y + 200, w: 50, h: 200, targetRoom: "floor3", targetPlayerX: rooms.floor3.x + 50, targetPlayerY: rooms.floor3.y + 300 },
    { x: rooms.floor3.x, y: rooms.floor3.y + 200, w: 50, h: 200, targetRoom: "floor2", targetPlayerX: rooms.floor2.x + rooms.floor2.w - 50, targetPlayerY: rooms.floor2.y + 300 },
    { x: rooms.floor3.x + rooms.floor3.w - 50, y: rooms.floor3.y + 200, w: 50, h: 200, targetRoom: "floor4", targetPlayerX: rooms.floor4.x + 50, targetPlayerY: rooms.floor4.y + 300 },
    { x: rooms.floor4.x, y: rooms.floor4.y + 200, w: 50, h: 200, targetRoom: "floor3", targetPlayerX: rooms.floor3.x + rooms.floor3.w - 50, targetPlayerY: rooms.floor0.y + 300 },
  ];

  joystick = {
    baseX: width * 0.2,
    baseY: height * 0.8,
    radius: 60,
    knobX: width * 0.2,
    knobY: height * 0.8,
    active: false,
    touchId: -1,
    maxX: width * 0.4
  };

  runButton = {
    x: width * 0.8,
    y: height * 0.8,
    w: 80,
    h: 80,
    active: false,
    touchId: -1
  };

  modButton = {
    x: width * 0.9,
    y: height * 0.1,
    w: 80,
    h: 40,
    active: false,
    touchId: -1
  };

  scrapMachineButton = {
    x: width / 2,
    y: height - 50,
    w: 200,
    h: 60,
    active: false,
    touchId: -1,
    visible: false
  };

  lockerButton = {
    x: width / 2,
    y: height - 50,
    w: 200,
    h: 60,
    active: false,
    touchId: -1,
    visible: false
  };

  for (let i = 0; i < MAX_MONSTERS; i++) {
    spawnMonster();
  }

  spawnCollectibles();

  if (typeof DeviceOrientationEvent !== 'undefined' && typeof DeviceOrientationEvent.requestPermission === 'function') {
    // Permission will be requested in touchStarted()
  } else {
    window.addEventListener('deviceorientation', handleDeviceOrientation);
    deviceOrientationGranted = true;
  }

  gameState = "loading";
  loadingTimer = 0;
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

object-initialization Room Layout rooms = { mainRoom: {...}, floor0: {...}, ... };

Defines all five game rooms with their positions, sizes, colors, and attached objects like the scrap machine and lockers

object-initialization Player Object player = { x: rooms.mainRoom.x + 100, y: rooms.mainRoom.y + 100, w: 30, h: 30, vx: 0, vy: 0, isMoving: false, isRunning: false };

Creates the player object with position, dimensions, velocity, and state flags for movement and running

array-initialization Staircases (Room Transitions) staircases = [ ... ];

Defines all transitions between rooms—each staircase has a position and target room/player position

object-initialization UI Buttons joystick = {...}; runButton = {...}; modButton = {...};

Initializes positions and state for all interactive touch buttons

createCanvas(windowWidth, windowHeight);
Creates a full-screen canvas sized to match the window—essential for mobile games
noStroke(); textAlign(CENTER, CENTER);
Disables outlines on shapes and centers all text, making UI cleaner
rooms = { mainRoom: {...}, floor0: {...}, ... };
Defines all game rooms as objects with position (x, y), size (w, h), color, and floor number for pathfinding
player = { x: rooms.mainRoom.x + 100, y: rooms.mainRoom.y + 100, ... };
Spawns the player 100 pixels into the main room—stores position, size, velocity (vx/vy), and movement state
staircases = [ ... ];
Defines all room transitions as rectangular areas that teleport the player when touched
joystick = { baseX: width * 0.2, baseY: height * 0.8, ... };
Creates a joystick UI element on the left side of the screen at 20% width and 80% height
for (let i = 0; i < MAX_MONSTERS; i++) { spawnMonster(); }
Loops to spawn the initial set of monsters (up to MAX_MONSTERS) in random floor rooms
spawnCollectibles();
Populates the game world with keys and scrap pieces based on current mod settings
gameState = "loading"; loadingTimer = 0;
Sets the game to start in the loading screen before transitioning to start screen

draw()

The draw() function runs 60 times per second (by default). For games, draw() is where you: clear the screen, update all game logic, render the world with camera transforms, then render UI on top. The switch statement keeps code organized by game state, making it easy to manage different screens (loading, gameplay, menus, gameover).

function draw() {
  background(0);

  switch (gameState) {
    case "loading":
      drawLoadingScreen();
      break;
    case "startScreen":
      drawStartScreen();
      break;
    case "game":
      updatePlayer();
      updateMonsters();
      updateCamera();

      push();
      translate(-cameraX, -cameraY);

      drawRooms();
      drawStaircases();

      if (currentRoom === "mainRoom") {
        drawBoombox(boombox.x, boombox.y, boombox.w, boombox.h);
      }

      if (currentRoom === "mainRoom") {
        drawScrapMachine(rooms.mainRoom.scrapMachine.x, rooms.mainRoom.scrapMachine.y, rooms.mainRoom.scrapMachine.w, rooms.mainRoom.scrapMachine.h);
      }

      if (currentRoom === "mainRoom") {
        for (let locker of rooms.mainRoom.lockers) {
          drawLocker(locker.x, locker.y, locker.w, locker.h);
        }
      }

      drawPlayer();
      drawMonsters();
      drawCollectibles();

      pop();

      drawJoystick();
      drawMonsterTracker();
      drawInventory();
      drawVRToggleButton();
      drawRunButton();
      drawModMenuButton();
      drawScrapMachineButton();
      drawLockerButton();
      break;
    case "jumpscare":
      drawJumpscare();
      break;
    case "modMenu":
      drawModMenu();
      break;
    case "scrapMachine":
      drawScrapMachineUI();
      break;
    case "lockerMenu":
      drawLockerMenuUI();
      break;
    case "monsterRoomView":
      drawMonsterRoomView();
      break;
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

switch-case Game State Manager switch (gameState) { case "loading": ... case "game": ... }

Routes execution to different rendering and update functions based on current game state

conditional Game State Update case "game": updatePlayer(); updateMonsters(); updateCamera();

Calls all update functions for the active game world when in gameplay

transform Camera Transform push(); translate(-cameraX, -cameraY); ... pop();

Applies the camera offset to all world objects, making the camera follow the player

drawing UI Rendering drawJoystick(); drawMonsterTracker(); drawInventory(); ...

Draws all UI elements on top of the world (outside the camera transform) so they stay visible

background(0);
Clears the canvas with black, essential for erasing previous frames and creating animation
switch (gameState) {
Branches execution based on the current game state—loading, start screen, gameplay, jumpscare, or menus
updatePlayer(); updateMonsters(); updateCamera();
Calls update functions for the player, monsters, and camera every frame to move everything and respond to input
push(); translate(-cameraX, -cameraY);
Saves the current graphics state and translates everything by negative camera position—the standard way to implement a camera
drawRooms(); drawStaircases(); drawPlayer(); drawMonsters(); drawCollectibles();
Draws all world objects—they are affected by the camera translation, so they move when the camera moves
pop();
Restores the graphics state, undoing the camera translation—everything after this is drawn at screen coordinates
drawJoystick(); drawMonsterTracker(); drawInventory(); drawVRToggleButton(); drawRunButton(); drawModMenuButton();
Draws UI elements at fixed screen positions (not affected by camera), so they stay visible no matter where the player is

updatePlayer()

updatePlayer() is called every frame and handles all player logic: movement, collectible pickup, room transitions, and audio. The loop backwards through collectibles is a common pattern when removing items from an array during iteration—going forwards would skip items. The constrain() function clips values to a range, a simple but essential tool for keeping entities inside game boundaries.

function updatePlayer() {
  let currentSpeed = player.isRunning ? PLAYER_RUN_SPEED : PLAYER_SPEED;

  player.x += player.vx * currentSpeed;
  player.y += player.vy * currentSpeed;

  player.isMoving = (abs(player.vx) > 0.1 || abs(player.vy) > 0.1);

  if (player.isMoving) {
    footstepCounter++;
    if (footstepCounter >= FOOTSTEP_INTERVAL) {
      footstepOsc.freq(random(100, 150));
      footstepEnvelope.play(footstepOsc);
      footstepCounter = 0;
    }
  } else {
    footstepCounter = 0;
  }

  for (let i = collectibles.length - 1; i >= 0; i--) {
    let item = collectibles[i];
    if (item.room === currentRoom && rectCollision(player.x, player.y, player.w, player.h, item.x, item.y, item.w, item.h)) {
      if (item.type === "key") {
        playerInventory.push(item);
        collectOsc.freq(random(600, 800));
        collectEnvelope.play(collectOsc);
        console.log(`Grabbed a ${item.type}! Inventory:`, playerInventory);
      } else if (item.type === "scrap") {
        playerScrap += 1;
        scrapCollectOsc.freq(random(200, 300));
        scrapCollectEnv.play(scrapCollectOsc);
        console.log(`Grabbed some scrap! Scrap: ${playerScrap}`);
      }
      collectibles.splice(i, 1);
    }
  }

  if (canTransition) {
    let hasTransitioned = false;
    for (let staircase of staircases) {
      if (rectCollision(player.x, player.y, player.w, player.h, staircase.x, staircase.y, staircase.w, staircase.h)) {
        if (currentRoom !== staircase.targetRoom) {
          currentRoom = staircase.targetRoom;
          player.x = staircase.targetPlayerX;
          player.y = staircase.targetPlayerY;
          player.vx = 0;
          player.vy = 0;
          player.isRunning = false;
          hasTransitioned = true;
          canTransition = false;
          setTimeout(() => { canTransition = true; }, TRANSITION_COOLDOWN);
          console.log(`Transitioned to ${currentRoom}`);
          break;
        }
      }
    }

    if (hasTransitioned) {
      if (currentRoom === "monsterRoom") {
        gameState = "monsterRoomView";
      }
      return;
    }
  }

  let currentRoomData = rooms[currentRoom];
  player.x = constrain(player.x, currentRoomData.x, currentRoomData.x + currentRoomData.w - player.w);
  player.y = constrain(player.y, currentRoomData.y, currentRoomData.y + currentRoomData.h - player.h);

  if (!joystick.active && touches.length === 0) {
    player.vx = 0;
    player.vy = 0;
  }

  if (currentRoom === "mainRoom") {
    let d = dist(player.x, player.y, boombox.x, boombox.y);
    let targetAmp = 0;
    let targetDelayAmp = 0;

    if (d < boombox.nearbyThreshold) {
      targetAmp = map(d, boombox.nearbyThreshold - boombox.fadeZone, boombox.nearbyThreshold, 0.5, 0, true);
      targetDelayAmp = map(d, boombox.nearbyThreshold - boombox.fadeZone, boombox.nearbyThreshold, 0.8, 0, true);
    } else {
      targetAmp = 0;
      targetDelayAmp = 0;
    }

    boombox.osc.amp(targetAmp, 0.5);
    boombox.delay.amp(targetDelayAmp, 0.5);
  } else {
    boombox.osc.amp(0, 0.5);
    boombox.delay.amp(0, 0.5);
  }

  scrapMachineButton.visible = false;
  lockerButton.visible = false;

  if (currentRoom === "mainRoom") {
    let scrapMachineData = rooms.mainRoom.scrapMachine;
    if (rectCollision(player.x, player.y, player.w, player.h, scrapMachineData.x, scrapMachineData.y, scrapMachineData.w, scrapMachineData.h)) {
      scrapMachineButton.visible = true;
    }

    for (let locker of rooms.mainRoom.lockers) {
      if (rectCollision(player.x, player.y, player.w, player.h, locker.x, locker.y, locker.w, locker.h)) {
        lockerButton.visible = true;
        break;
      }
    }
  }
}
Line-by-line explanation (14 lines)

🔧 Subcomponents:

calculation Player Movement let currentSpeed = player.isRunning ? PLAYER_RUN_SPEED : PLAYER_SPEED; player.x += player.vx * currentSpeed; player.y += player.vy * currentSpeed;

Updates player position based on velocity and current speed (walk or run)

conditional Footstep Sound if (player.isMoving) { footstepCounter++; if (footstepCounter >= FOOTSTEP_INTERVAL) { footstepOsc.freq(...); footstepEnvelope.play(...); } }

Plays a footstep sound every FOOTSTEP_INTERVAL frames while the player moves

for-loop Collectible Pickup for (let i = collectibles.length - 1; i >= 0; i--) { if (rectCollision(...)) { ... collectibles.splice(i, 1); } }

Checks every collectible for collision with the player, collecting keys and scrap, then removing them from the world

for-loop Room Transitions for (let staircase of staircases) { if (rectCollision(player.x, player.y, ...)) { currentRoom = staircase.targetRoom; ... } }

Teleports the player to a new room when they step on a staircase

calculation Room Boundary Clamping player.x = constrain(player.x, currentRoomData.x, currentRoomData.x + currentRoomData.w - player.w);

Prevents the player from leaving the current room by clamping their position to the room's edges

conditional Boombox Audio Distance if (d < boombox.nearbyThreshold) { targetAmp = map(d, ..., 0.5, 0, true); }

Fades in boombox music as the player approaches it—realistic audio proximity

conditional Interaction Button Visibility if (rectCollision(player.x, player.y, player.w, player.h, scrapMachineData.x, ...)) { scrapMachineButton.visible = true; }

Shows interaction buttons only when the player is near the scrap machine or a locker

let currentSpeed = player.isRunning ? PLAYER_RUN_SPEED : PLAYER_SPEED;
Picks the walk or run speed based on the player's running state—ternary operator is a shorthand if-else
player.x += player.vx * currentSpeed;
Moves the player horizontally by multiplying velocity (direction) by speed—the core of animation
player.isMoving = (abs(player.vx) > 0.1 || abs(player.vy) > 0.1);
Checks if the player is moving by looking at velocity magnitude—used to play footsteps only when moving
if (footstepCounter >= FOOTSTEP_INTERVAL) { footstepOsc.freq(random(100, 150)); footstepEnvelope.play(footstepOsc); footstepCounter = 0; }
Every FOOTSTEP_INTERVAL frames, plays a footstep sound at a random pitch and resets the counter
for (let i = collectibles.length - 1; i >= 0; i--) {
Loops backwards through collectibles (important when removing items mid-loop to avoid skipping)
if (item.room === currentRoom && rectCollision(player.x, player.y, player.w, player.h, item.x, item.y, item.w, item.h)) {
Checks if the item is in the current room AND touching the player using axis-aligned bounding box collision
collectibles.splice(i, 1);
Removes the collected item from the array so it no longer appears in the world
if (canTransition) {
Only allows transitions if the cooldown timer has expired—prevents rapid back-and-forth transitions
if (currentRoom !== staircase.targetRoom) {
Only transitions if the staircase leads to a different room—prevents stuck transitions
setTimeout(() => { canTransition = true; }, TRANSITION_COOLDOWN);
Disables transitions for TRANSITION_COOLDOWN milliseconds, re-enabling them after using a staircase
player.x = constrain(player.x, currentRoomData.x, currentRoomData.x + currentRoomData.w - player.w);
Clamps the player's x position within the room boundaries—prevents leaving the room edges
if (!joystick.active && touches.length === 0) { player.vx = 0; player.vy = 0; }
Stops the player moving if no touch or keyboard input is active—natural deceleration without friction
let d = dist(player.x, player.y, boombox.x, boombox.y);
Calculates distance from player to boombox using the Euclidean distance formula—the foundation of proximity audio
targetAmp = map(d, boombox.nearbyThreshold - boombox.fadeZone, boombox.nearbyThreshold, 0.5, 0, true);
Maps distance to amplitude: close = 0.5 volume, far = 0 volume, with smooth interpolation between—'true' constrains to range

updateMonsters()

updateMonsters() is a thin wrapper that iterates through all monsters and calls their individual update methods. This pattern keeps the main game loop clean and delegates responsibility to the Monster class. In a larger game, you might filter monsters here (e.g., only update visible ones) or apply global behaviors like spawning or despawning.

function updateMonsters() {
  for (let monster of monsters) {
    monster.update();
  }
}
Line-by-line explanation (2 lines)
for (let monster of monsters) {
Loops through every monster in the monsters array—uses for-of syntax for cleaner code
monster.update();
Calls the update method on each Monster object, handling movement, AI, and collision detection

drawPlayer()

drawPlayer() uses procedural drawing to create the player's appearance without loading an image. By translating to the center and using relative coordinates (negative values for left, positive for right), the code is more readable and easier to scale. Each part (body, head, eyes, mouth, arms) is drawn in layers, with colors from darker to lighter to create depth and personality.

🔬 These lines draw two white eyes with red pupils. What happens if you change the second pair of ellipse calls to draw much larger red pupils, like player.w * 0.25 instead of 0.1? How does that change the player's expression?

  fill(255);
  ellipse(-player.w * 0.2, -player.h * 0.8, player.w * 0.2, player.h * 0.2);
  ellipse(player.w * 0.2, -player.h * 0.8, player.w * 0.2, player.h * 0.2);
  fill(255, 0, 0);
  ellipse(-player.w * 0.2, -player.h * 0.8, player.w * 0.1, player.h * 0.1);
  ellipse(player.w * 0.2, -player.h * 0.8, player.w * 0.1, player.h * 0.1);
function drawPlayer() {
  push();
  translate(player.x + player.w / 2, player.y + player.h / 2);

  fill(200, 0, 0);
  ellipse(0, 0, player.w * 1.2, player.h * 1.5);

  fill(150, 0, 0);
  ellipse(0, -player.h * 0.7, player.w * 0.8, player.h * 0.8);

  fill(255);
  ellipse(-player.w * 0.2, -player.h * 0.8, player.w * 0.2, player.h * 0.2);
  ellipse(player.w * 0.2, -player.h * 0.8, player.w * 0.2, player.h * 0.2);
  fill(255, 0, 0);
  ellipse(-player.w * 0.2, -player.h * 0.8, player.w * 0.1, player.h * 0.1);
  ellipse(player.w * 0.2, -player.h * 0.8, player.w * 0.1, player.h * 0.1);

  stroke(255, 0, 0);
  strokeWeight(2);
  line(-player.w * 0.2, -player.h * 0.6, player.w * 0.2, -player.h * 0.6);
  noStroke();

  stroke(200, 0, 0);
  strokeWeight(4);
  line(-player.w * 0.6, -player.h * 0.3, -player.w * 0.2, 0);
  line(player.w * 0.6, -player.h * 0.3, player.w * 0.2, 0);
  noStroke();

  pop();
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

transform Center Transform push(); translate(player.x + player.w / 2, player.y + player.h / 2);

Centers the coordinate system on the player's center, making symmetrical drawing easier

drawing Body and Head fill(200, 0, 0); ellipse(0, 0, player.w * 1.2, player.h * 1.5); fill(150, 0, 0); ellipse(0, -player.h * 0.7, ...);

Draws the player's dark red body (ellipse) and darker head above it

drawing Eyes with Pupils fill(255); ellipse(...); fill(255, 0, 0); ellipse(...);

Draws white eye circles with red pupils, giving the player a menacing appearance

drawing Mouth stroke(255, 0, 0); strokeWeight(2); line(-player.w * 0.2, -player.h * 0.6, player.w * 0.2, -player.h * 0.6);

Draws a simple red line for the mouth

drawing Arms line(-player.w * 0.6, -player.h * 0.3, -player.w * 0.2, 0); line(player.w * 0.6, -player.h * 0.3, player.w * 0.2, 0);

Draws two thick red lines as arms extending from the body

push(); translate(player.x + player.w / 2, player.y + player.h / 2);
Saves graphics state and moves origin to the center of the player—symmetrical shapes are easier to draw from center
fill(200, 0, 0); ellipse(0, 0, player.w * 1.2, player.h * 1.5);
Draws the player's body as a dark red ellipse centered at (0, 0) with scaled dimensions
fill(150, 0, 0); ellipse(0, -player.h * 0.7, player.w * 0.8, player.h * 0.8);
Draws the head above the body (negative y) in an even darker red, slightly smaller than the body
fill(255); ellipse(-player.w * 0.2, -player.h * 0.8, player.w * 0.2, player.h * 0.2);
Draws the left white eye, positioned at relative coordinates within the head
fill(255, 0, 0); ellipse(-player.w * 0.2, -player.h * 0.8, player.w * 0.1, player.h * 0.1);
Draws a red pupil inside the white eye, half the size of the white eye
stroke(255, 0, 0); strokeWeight(2); line(-player.w * 0.2, -player.h * 0.6, player.w * 0.2, -player.h * 0.6);
Draws a horizontal red line as the mouth using stroke weight 2
line(-player.w * 0.6, -player.h * 0.3, -player.w * 0.2, 0);
Draws the left arm as a thick red line from shoulder to hand position
pop();
Restores the graphics state and coordinate system to normal

Monster (class)

The Monster class demonstrates object-oriented design: each monster is an instance with its own position, velocity, state, and behavior logic. The update() method contains the AI: simple state machine (roaming vs chasing), wandering logic, and sophisticated pathfinding for cross-floor pursuit. The draw() method renders the monster procedurally, rotating it to face its direction of movement for a more lifelike appearance.

class Monster {
  constructor(x, y, roomName) {
    this.x = x;
    this.y = y;
    this.w = 40;
    this.h = 40;
    this.vx = random([-MONSTER_SPEED, MONSTER_SPEED]);
    this.vy = random([-MONSTER_SPEED, MONSTER_SPEED]);
    this.room = roomName;
    this.state = "roaming";
    this.chaseTimer = 0;
    this.chaseDuration = 120;
    this.roamTimer = 0;
    this.roamDuration = random(60, 240);
    this.monsterCanTransition = true;
  }

  update() {
    let roomData = rooms[this.room];
    let speedModifier = monsterSpeedFast ? (MONSTER_CHASE_SPEED / MONSTER_SPEED) : 1;
    let currentMonsterSpeed = MONSTER_SPEED * speedModifier;

    if (this.state === "chasing") {
      currentMonsterSpeed = MONSTER_CHASE_SPEED * speedModifier;
    }

    this.x += this.vx * currentMonsterSpeed;
    this.y += this.vy * currentMonsterSpeed;

    if (this.x < roomData.x || this.x + this.w > roomData.x + roomData.w) {
      this.vx *= -1;
      this.x = constrain(this.x, roomData.x, roomData.x + roomData.w - this.w);
    }
    if (this.y < roomData.y || this.y + this.h > roomData.y + roomData.h) {
      this.vy *= -1;
      this.y = constrain(this.y, roomData.y, roomData.y + roomData.h - this.h);
    }

    if (this.room === currentRoom && currentRoom !== "mainRoom" && currentRoom !== "monsterRoomView") {
      let d = dist(this.x, this.y, player.x, player.y);
      if (d < MONSTER_CHASE_RANGE) {
        this.state = "chasing";
        this.chaseTimer = this.chaseDuration;
      } else if (this.state === "chasing") {
        this.chaseTimer--;
        if (this.chaseTimer <= 0) {
          this.state = "roaming";
          let baseRoamSpeed = monsterSpeedFast ? MONSTER_CHASE_SPEED : MONSTER_SPEED;
          this.vx = random([-baseRoamSpeed, baseRoamSpeed]);
          this.vy = random([-baseRoamSpeed, baseRoamSpeed]);
          this.roamTimer = 0;
        }
      }
    } else {
      this.state = "roaming";
      let baseRoamSpeed = monsterSpeedFast ? MONSTER_CHASE_SPEED : MONSTER_SPEED;
      this.vx = random([-baseRoamSpeed, baseRoamSpeed]);
      this.vy = random([-baseRoamSpeed, baseRoamSpeed]);
      this.roamTimer = 0;
    }

    if (this.state === "roaming") {
      this.roamTimer++;
      if (this.roamTimer > this.roamDuration) {
        this.vx *= -1;
        this.vy *= -1;
        this.roamTimer = 0;
        this.roamDuration = random(60, 240);
      }
    } else if (this.state === "chasing") {
      let playerFloor = rooms[currentRoom].floor;
      let monsterFloor = rooms[this.room].floor;

      if (playerFloor !== monsterFloor && monsterFloor !== -1 && this.monsterCanTransition) {
        let targetStaircase = null;
        let minStaircaseDist = Infinity;

        for (let staircase of staircases) {
          if (rectCollision(this.x, this.y, this.w, this.h, staircase.x, staircase.y, staircase.w, staircase.h)) {
            let targetStaircaseFloor = rooms[staircase.targetRoom].floor;
            if ((playerFloor > monsterFloor && targetStaircaseFloor > monsterFloor) ||
                (playerFloor < monsterFloor && targetStaircaseFloor < monsterFloor) ||
                (playerFloor === targetStaircaseFloor)) {
              this.room = staircase.targetRoom;
              this.x = staircase.targetPlayerX;
              this.y = staircase.targetPlayerY;
              this.vx = 0;
              this.vy = 0;
              this.monsterCanTransition = false;
              setTimeout(() => { this.monsterCanTransition = true; }, TRANSITION_COOLDOWN);
              return;
            }
          }

          if (staircase.targetRoom !== this.room && staircase.targetRoom !== "mainRoom" && staircase.targetRoom !== "monsterRoom") {
            let targetStaircaseFloor = rooms[staircase.targetRoom].floor;
            let currentStaircaseFloor = rooms[this.room].floor;

            if (abs(targetStaircaseFloor - playerFloor) < abs(currentStaircaseFloor - playerFloor) || targetStaircaseFloor === playerFloor) {
              let d = dist(this.x, this.y, staircase.x + staircase.w / 2, staircase.y + staircase.h / 2);
              if (d < minStaircaseDist) {
                minStaircaseDist = d;
                targetStaircase = staircase;
              }
            }
          }
        }

        if (targetStaircase) {
          let targetX = targetStaircase.x + targetStaircase.w / 2;
          let targetY = targetStaircase.y + targetStaircase.h / 2;
          let dx = targetX - this.x;
          let dy = targetY - this.y;
          let angle = atan2(dy, dx);
          this.vx = cos(angle) * currentMonsterSpeed;
          this.vy = sin(angle) * currentMonsterSpeed;
        } else {
          let baseRoamSpeed = monsterSpeedFast ? MONSTER_CHASE_SPEED : MONSTER_SPEED;
          this.vx = random([-baseRoamSpeed, baseRoamSpeed]);
          this.vy = random([-baseRoamSpeed, baseRoamSpeed]);
        }
      } else {
        this.vx = (player.x > this.x ? 1 : -1) * currentMonsterSpeed;
        this.vy = (player.y > this.y ? 1 : -1) * currentMonsterSpeed;
      }
    }

    let safeRooms = ["mainRoom", "monsterRoom"];
    for (let safeRoomName of safeRooms) {
      if (this.room !== safeRoomName) {
        let safeRoom = rooms[safeRoomName];
        if (rectCollision(this.x, this.y, this.w, this.h, safeRoom.x, safeRoom.y, safeRoom.w, safeRoom.h)) {
          if (this.vx > 0 && this.x < safeRoom.x + safeRoom.w && this.x + this.w > safeRoom.x + safeRoom.w) {
            this.x = safeRoom.x + safeRoom.w;
            this.vx *= -1;
          } else if (this.vx < 0 && this.x + this.w > safeRoom.x && this.x < safeRoom.x) {
            this.x = safeRoom.x - this.w;
            this.vx *= -1;
          }
          if (this.vy > 0 && this.y < safeRoom.y + safeRoom.h && this.y + this.h > safeRoom.y + safeRoom.h) {
            this.y = safeRoom.y + safeRoom.h;
            this.vy *= -1;
          } else if (this.vy < 0 && this.y + this.h > safeRoom.y && this.y < safeRoom.y) {
            this.y = safeRoom.y - this.h;
            this.vy *= -1;
          }
          this.state = "roaming";
          this.roamTimer = 0;
        }
      }
    }

    if (currentRoom === this.room && rectCollision(player.x, player.y, player.w, player.h, this.x, this.y, this.w, this.h)) {
      triggerJumpscare();
    }
  }

  draw() {
    push();
    translate(this.x + this.w / 2, this.y + this.h / 2);

    let rotationAngle = atan2(this.vy, this.vx);
    rotate(rotationAngle);

    fill(20, 60, 20);
    beginShape();
    vertex(-this.w * 0.7, -this.h * 0.3);
    vertex(this.w * 0.7, -this.h * 0.5);
    vertex(this.w * 0.5, this.h * 0.5);
    vertex(-this.w * 0.5, this.h * 0.7);
    endShape(CLOSE);

    fill(10, 40, 10);
    ellipse(this.w * 0.6, -this.h * 0.4, this.w * 0.8, this.h * 0.6);

    fill(255, 0, 0);
    ellipse(this.w * 0.7, -this.h * 0.5, this.w * 0.2, this.h * 0.2);
    ellipse(this.w * 0.5, -this.h * 0.3, this.w * 0.2, this.h * 0.2);

    fill(0);
    beginShape();
    vertex(this.w * 0.6, -this.h * 0.1);
    vertex(this.w * 0.8, this.h * 0.1);
    vertex(this.w * 0.5, this.h * 0.3);
    vertex(this.w * 0.3, this.h * 0.1);
    vertex(this.w * 0.6, -this.h * 0.1);
    endShape(CLOSE);
    stroke(255, 0, 0);
    strokeWeight(1);
    beginShape();
    vertex(this.w * 0.6, -this.h * 0.1);
    vertex(this.w * 0.8, this.h * 0.1);
    vertex(this.w * 0.5, this.h * 0.3);
    vertex(this.w * 0.3, this.h * 0.1);
    vertex(this.w * 0.6, -this.h * 0.1);
    endShape(CLOSE);
    noStroke();

    pop();
  }
}
Line-by-line explanation (14 lines)

🔧 Subcomponents:

method Constructor constructor(x, y, roomName) { this.x = x; this.y = y; this.room = roomName; this.state = "roaming"; ... }

Initializes a new monster at a specific position with random initial velocity and state

conditional Roaming Behavior if (this.state === "roaming") { this.roamTimer++; if (this.roamTimer > this.roamDuration) { this.vx *= -1; this.vy *= -1; ... } }

Makes the monster wander randomly and periodically change direction

conditional Chase Behavior if (this.state === "chasing") { ... this.vx = (player.x > this.x ? 1 : -1) * currentMonsterSpeed; ... }

When chasing, moves the monster toward the player using directional velocity

for-loop Cross-Floor Pathfinding if (playerFloor !== monsterFloor && monsterFloor !== -1 && this.monsterCanTransition) { ... for (let staircase of staircases) { ... } }

When the player is on a different floor, the monster intelligently finds and uses staircases to chase them

for-loop Safe Zone Boundary for (let safeRoomName of safeRooms) { if (rectCollision(...)) { this.vx *= -1; ... } }

Prevents monsters from entering the main room and monster room by bouncing them back

conditional Player Collision if (currentRoom === this.room && rectCollision(player.x, player.y, player.w, player.h, this.x, this.y, this.w, this.h)) { triggerJumpscare(); }

Detects when a monster touches the player and triggers the jumpscare sequence

class Monster {
Declares a new class called Monster—objects of this class represent individual monsters with properties and methods
constructor(x, y, roomName) {
The constructor method runs once when a new Monster is created with `new Monster(...)`
this.vx = random([-MONSTER_SPEED, MONSTER_SPEED]);
Picks a random starting velocity: either negative MONSTER_SPEED (left/up) or positive (right/down)
this.state = "roaming";
Sets initial state to roaming—monsters start by wandering randomly
let speedModifier = monsterSpeedFast ? (MONSTER_CHASE_SPEED / MONSTER_SPEED) : 1;
Calculates a speed multiplier from the mod menu setting—allows toggling 'fast monsters' mode
if (this.x < roomData.x || this.x + this.w > roomData.x + roomData.w) { this.vx *= -1; }
Bounces monster off left/right walls by reversing horizontal velocity
if (d < MONSTER_CHASE_RANGE) { this.state = "chasing"; this.chaseTimer = this.chaseDuration; }
If the player is nearby and in the same room, switch to chasing and set a timer for how long to chase
if (this.roamTimer > this.roamDuration) { this.vx *= -1; this.vy *= -1; this.roamTimer = 0; }
After roaming for a duration, flip both velocities to change direction—simple wandering behavior
let playerFloor = rooms[currentRoom].floor; let monsterFloor = rooms[this.room].floor;
Gets the floor numbers of the player's room and the monster's room—used for intelligent pathfinding
if ((playerFloor > monsterFloor && targetStaircaseFloor > monsterFloor) || ...) {
Checks if a staircase moves the monster toward the player's floor—only use helpful transitions
let angle = atan2(dy, dx); this.vx = cos(angle) * currentMonsterSpeed; this.vy = sin(angle) * currentMonsterSpeed;
Calculates the angle toward a target (staircase or player) and sets velocity components accordingly—smart steering
if (rectCollision(this.x, this.y, this.w, this.h, safeRoom.x, safeRoom.y, safeRoom.w, safeRoom.h)) { this.vx *= -1; }
If the monster hits the safe zone boundary, bounce it back by reversing velocity
if (currentRoom === this.room && rectCollision(...)) { triggerJumpscare(); }
If the monster is in the same room as the player and collides, trigger the jumpscare event
let rotationAngle = atan2(this.vy, this.vx); rotate(rotationAngle);
Rotates the monster to face its direction of movement—makes it look more alive

updateCamera()

updateCamera() implements a 2D camera system using the translate() function. The pattern—calculate target position, clamp to boundaries, smoothly interpolate—is fundamental to game development. The dead zones (GAMMA_DEAD_ZONE and BETA_DEAD_ZONE) are crucial for device orientation: without them, tiny accelerometer noise would cause the camera to jitter constantly. The smoothing with lerp() makes camera movement feel natural rather than mechanical.

function updateCamera() {
  targetCameraX = player.x - width / 2;
  targetCameraY = player.y - height / 2;

  if (vrMode && deviceOrientationGranted) {
    let gammaOffset = 0;
    if (abs(deviceGamma) > GAMMA_DEAD_ZONE) {
      gammaOffset = map(deviceGamma, -90, 90, -GAMMA_SENSITIVITY * width, GAMMA_SENSITIVITY * width);
    }
    targetCameraX += gammaOffset;

    let betaOffset = 0;
    if (abs(deviceBeta) > BETA_DEAD_ZONE) {
      betaOffset = map(deviceBeta, -180, 180, -BETA_SENSITIVITY * height, BETA_SENSITIVITY * height);
    }
    targetCameraY += betaOffset;
  }

  let minWorldX = rooms.mainRoom.x;
  let maxWorldX = rooms.floor4.x + rooms.floor4.w;
  let minWorldY = rooms.mainRoom.y;
  let maxWorldY = rooms.floor4.y + rooms.floor4.h;

  targetCameraX = constrain(targetCameraX, minWorldX, maxWorldX - width);
  targetCameraY = constrain(targetCameraY, minWorldY, maxWorldY - height);

  smoothedCameraX = lerp(smoothedCameraX, targetCameraX, 0.1);
  smoothedCameraY = lerp(smoothedCameraY, targetCameraY, 0.1);
  cameraX = smoothedCameraX;
  cameraY = smoothedCameraY;
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

calculation Base Camera Position targetCameraX = player.x - width / 2; targetCameraY = player.y - height / 2;

Centers the camera on the player by placing the player at the screen center

conditional Device Orientation Offset if (vrMode && deviceOrientationGranted) { let gammaOffset = 0; if (abs(deviceGamma) > GAMMA_DEAD_ZONE) { gammaOffset = map(...); } }

Adds camera offset based on device tilt, allowing the player to look around by tilting their phone

calculation World Boundary Clamping targetCameraX = constrain(targetCameraX, minWorldX, maxWorldX - width);

Prevents the camera from panning beyond the game world edges

calculation Smooth Camera Interpolation smoothedCameraX = lerp(smoothedCameraX, targetCameraX, 0.1);

Smoothly interpolates the camera toward its target position, preventing jerky movement

targetCameraX = player.x - width / 2;
Calculates the camera x position to center the player on screen (player x minus half screen width)
targetCameraY = player.y - height / 2;
Calculates the camera y position to center the player vertically
if (vrMode && deviceOrientationGranted) {
Only applies device tilt if VR mode is enabled and the device orientation permission was granted
if (abs(deviceGamma) > GAMMA_DEAD_ZONE) {
Checks if device tilt exceeds the dead zone—small movements are ignored to prevent camera jitter
gammaOffset = map(deviceGamma, -90, 90, -GAMMA_SENSITIVITY * width, GAMMA_SENSITIVITY * width);
Maps device tilt angle (-90 to 90 degrees) to camera offset (negative to positive), scaled by GAMMA_SENSITIVITY
targetCameraX += gammaOffset;
Applies the tilt offset to the camera position—tilting left moves the camera view left
targetCameraX = constrain(targetCameraX, minWorldX, maxWorldX - width);
Clamps the camera position within world boundaries so it never shows beyond the game world edges
smoothedCameraX = lerp(smoothedCameraX, targetCameraX, 0.1);
Interpolates smoothly toward the target: 10% toward target per frame creates a gentle easing effect
cameraX = smoothedCameraX;
Updates the global cameraX variable, which is used by the translate() in draw()

triggerJumpscare()

triggerJumpscare() demonstrates coordinated event handling: it changes game state, manages multiple audio layers (fade out ambient, play noise, schedule oscillator sweeps), and initializes the jumpscare animation sequence. The Web Audio API scheduling (second and third parameters to freq()) allows precise timing of audio effects without waiting for the game loop. The guard clause at the start is essential for preventing rapid re-triggering.

function triggerJumpscare() {
  if (jumpscare.isActive) return;

  jumpscare.isActive = true;
  jumpscare.timer = jumpscare.duration;
  jumpscare.animationFrame = 0;
  jumpscare.phase = "monster";
  gameState = "jumpscare";

  ambientSoundOsc.amp(0, 0.1);
  ambientSoundNoise.amp(0, 0.1);
  boombox.osc.amp(0, 0.1);
  boombox.delay.amp(0, 0.1);

  jumpscareEnvelope.play(jumpscareNoise);

  let currentTime = getAudioContext().currentTime;

  if (isFinite(2000) && isFinite(0.2) && isFinite(500) && isFinite(0.5) && isFinite(0.8) && isFinite(0.05)) {
      jumpscareOsc.amp(0.8, 0.05, currentTime);
      jumpscareOsc.freq(2000, 0.2, currentTime);
      jumpscareOsc.freq(500, 0.5, currentTime + 0.2);
      jumpscareOsc.amp(0, 0.5, currentTime + 0.2);
      jumpscareOsc.amp(0, 0.1, currentTime + 0.2 + 0.5);
  } else {
      console.error("Jumpscare Oscillator parameters are not finite. Skipping screech effect.");
      jumpscareOsc.amp(0);
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

conditional Prevent Double-Trigger if (jumpscare.isActive) return;

Prevents multiple jumpscare triggers in quick succession

assignment Jumpscare Initialization jumpscare.isActive = true; jumpscare.timer = jumpscare.duration; jumpscare.animationFrame = 0; jumpscare.phase = "monster"; gameState = "jumpscare";

Sets all jumpscare state variables and switches the game state to display the jumpscare

audio-control Fade Out Ambient Audio ambientSoundOsc.amp(0, 0.1); ambientSoundNoise.amp(0, 0.1); boombox.osc.amp(0, 0.1); boombox.delay.amp(0, 0.1);

Silences all background audio to create an eerie silence before the jumpscare screech

audio-effect Jumpscare Screech jumpscareOsc.freq(2000, 0.2, currentTime); jumpscareOsc.freq(500, 0.5, currentTime + 0.2);

Sweeps the oscillator from 2000 Hz down to 500 Hz for a terrifying sound effect

if (jumpscare.isActive) return;
Guard clause: exits immediately if a jumpscare is already playing, preventing overlapping jumpscares
jumpscare.isActive = true;
Marks the jumpscare as active so the guard clause will prevent re-triggering
gameState = "jumpscare";
Changes the game state so the draw() function switches to drawing the jumpscare instead of the game world
ambientSoundOsc.amp(0, 0.1);
Fades out the ambient drone oscillator over 0.1 seconds (very quick), creating sudden silence
jumpscareEnvelope.play(jumpscareNoise);
Triggers the jumpscare envelope, which controls how the white noise fades in and out
let currentTime = getAudioContext().currentTime;
Gets the current Web Audio API time—needed for scheduling frequency changes at precise moments
jumpscareOsc.freq(2000, 0.2, currentTime);
Schedules the oscillator frequency to sweep to 2000 Hz over 0.2 seconds starting now
jumpscareOsc.freq(500, 0.5, currentTime + 0.2);
After 0.2 seconds, schedules another sweep down to 500 Hz over the next 0.5 seconds—creates a descending wail
if (isFinite(...)) { ... } else { console.error(...); jumpscareOsc.amp(0); }
Checks that all numeric parameters are valid (finite numbers), skipping the screech if they're NaN or infinite—defensive programming

resetGame()

resetGame() is a comprehensive reset function that clears game state and returns to a playable condition. It demonstrates the importance of initializing variables at game start—every important game variable (player position, monsters, collectibles, audio, game state) is reset here. The function is called at the end of the jumpscare sequence, allowing players to immediately attempt another run. Good reset functions make the game feel fair and repeatable.

function resetGame() {
  jumpscare.isActive = false;
  jumpscare.timer = 0;
  jumpscare.animationFrame = 0;
  jumpscare.phase = "monster";
  gameState = "startScreen";

  player.x = rooms.mainRoom.x + 100;
  player.y = rooms.mainRoom.y + 100;
  player.vx = 0;
  player.vy = 0;
  player.isMoving = false;
  player.isRunning = false;
  currentRoom = "mainRoom";
  canTransition = true;

  monsters = [];
  for (let i = 0; i < MAX_MONSTERS; i++) {
    spawnMonster();
  }

  collectibles = [];
  playerInventory = [];
  playerScrap = 0;
  spawnCollectibles();

  ambientSoundOsc.amp(0.2, 1);
  ambientSoundNoise.amp(0.1, 1);
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

assignment Reset Jumpscare jumpscare.isActive = false; jumpscare.timer = 0; jumpscare.animationFrame = 0; jumpscare.phase = "monster"; gameState = "startScreen";

Clears the jumpscare state and returns to the start screen

assignment Reset Player player.x = rooms.mainRoom.x + 100; player.y = rooms.mainRoom.y + 100; player.vx = 0; player.vy = 0; player.isMoving = false; player.isRunning = false; currentRoom = "mainRoom";

Returns the player to their starting position in the main room with zero velocity

for-loop Reset Monsters monsters = []; for (let i = 0; i < MAX_MONSTERS; i++) { spawnMonster(); }

Clears all monsters and spawns a fresh set in random floor rooms

assignment Reset Collectibles collectibles = []; playerInventory = []; playerScrap = 0; spawnCollectibles();

Clears collected items and respawns all keys and scrap in the world

audio-control Resume Ambient Audio ambientSoundOsc.amp(0.2, 1); ambientSoundNoise.amp(0.1, 1);

Fades the ambient audio back in over 1 second, restoring the atmospheric horror feel

jumpscare.isActive = false; jumpscare.phase = "monster"; gameState = "startScreen";
Deactivates the jumpscare and returns to the start screen for the player to begin again
player.x = rooms.mainRoom.x + 100; player.y = rooms.mainRoom.y + 100;
Teleports the player back to the safe zone starting position
monsters = []; for (let i = 0; i < MAX_MONSTERS; i++) { spawnMonster(); }
Clears the monsters array and respawns the initial number of monsters—resets the threat level
playerInventory = []; playerScrap = 0;
Wipes the player's inventory and scrap count—resets progression for a fresh playthrough
spawnCollectibles();
Regenerates all collectibles based on the current mod menu settings (keys and scrap enabled/disabled)
ambientSoundOsc.amp(0.2, 1);
Fades the ambient drone back in over 1 second, creating the horror atmosphere again

touchStarted()

touchStarted() is p5.js's touch input handler, called once when a finger touches the screen. This implementation demonstrates multi-touch tracking by storing touch IDs for each UI element (joystick, run button), allowing independent control of multiple simultaneous touches. The function also handles game state transitions (start screen to game, game to menus) and permission requests for device orientation. The guard clause checking both touches.length and mouse position is a workaround for p5.js's dual-event quirk.

function touchStarted() {
  if (touches.length > 1 && touches[touches.length - 1].x === mouseX && touches[touches.length - 1].y === mouseY) {
    return false;
  }

  if (gameState === "loading") return false;

  if (gameState === "game" && isHovering(mouseX, mouseY, modButton.x, modButton.y, modButton.w, modButton.h)) {
    gameState = "modMenu";
    console.log("Opened mod menu.");
    return false;
  }

  if (gameState === "modMenu") {
    handleModMenuInteraction(mouseX, mouseY);
    return false;
  }

  if (gameState === "scrapMachine") {
    handleScrapMachineUIInteraction(mouseX, mouseY);
    return false;
  }

  if (gameState === "lockerMenu") {
    handleLockerUIInteraction(mouseX, mouseY);
    return false;
  }

  if (gameState === "monsterRoomView") {
    handleMonsterRoomViewInteraction(mouseX, mouseY);
    return false;
  }

  let vrButtonX = width * 0.1;
  let vrButtonY = height * 0.1;
  let vrButtonW = 120;
  let vrButtonH = 60;

  let newTouch = touches[touches.length - 1];

  if (gameState === "game" && isHovering(newTouch.x, newTouch.y, vrButtonX, vrButtonY, vrButtonW, vrButtonH)) {
    vrMode = !vrMode;
    console.log(`VR Mode: ${vrMode ? "ON" : "OFF"}`);
    if (vrMode && !deviceOrientationGranted) {
      if (typeof DeviceOrientationEvent !== 'undefined' && typeof DeviceOrientationEvent.requestPermission === 'function') {
        DeviceOrientationEvent.requestPermission()
          .then(permissionState => {
            if (permissionState === 'granted') {
              deviceOrientationGranted = true;
              window.addEventListener('deviceorientation', handleDeviceOrientation);
              console.log('Device orientation permission granted.');
            } else {
              console.warn('Device orientation permission denied.');
              deviceOrientationGranted = false;
              vrMode = false;
            }
          })
          .catch(console.error);
      } else {
        window.addEventListener('deviceorientation', handleDeviceOrientation);
        deviceOrientationGranted = true;
      }
    }
    return false;
  }

  if (gameState === "game" && isHovering(newTouch.x, newTouch.y, runButton.x, runButton.y, runButton.w, runButton.h)) {
    if (!runButton.active) {
      player.isRunning = true;
      runButton.active = true;
      runButton.touchId = newTouch.id;
      console.log(`Running: ${player.isRunning}`);
    }
    return false;
  }

  if (gameState === "game" && scrapMachineButton.visible && isHovering(newTouch.x, newTouch.y, scrapMachineButton.x, scrapMachineButton.y, scrapMachineButton.w, scrapMachineButton.h)) {
    gameState = "scrapMachine";
    console.log("Opened scrap machine.");
    return false;
  }

  if (gameState === "game" && lockerButton.visible && isHovering(newTouch.x, newTouch.y, lockerButton.x, lockerButton.y, lockerButton.w, lockerButton.h)) {
    gameState = "lockerMenu";
    console.log("Opened locker.");
    return false;
  }

  if (gameState === "startScreen") {
    gameState = "game";
    userStartAudio();

    if (vrMode && typeof DeviceOrientationEvent !== 'undefined' && typeof DeviceOrientationEvent.requestPermission === 'function') {
      DeviceOrientationEvent.requestPermission()
        .then(permissionState => {
          if (permissionState === 'granted') {
            deviceOrientationGranted = true;
            window.addEventListener('deviceorientation', handleDeviceOrientation);
            console.log('Device orientation permission granted.');
          } else {
            console.warn('Device orientation permission denied.');
            deviceOrientationGranted = false;
            vrMode = false;
          }
        })
        .catch(console.error);
    } else if (vrMode) {
      window.addEventListener('deviceorientation', handleDeviceOrientation);
      deviceOrientationGranted = true;
    }

    ambientSoundOsc.amp(0.2, 1);
    ambientSoundNoise.amp(0.1, 1);

    return false;
  }

  if (gameState === "game" && newTouch.x < joystick.maxX) {
    if (!joystick.active) {
      joystick.active = true;
      joystick.touchId = newTouch.id;
      joystick.knobX = newTouch.x;
      joystick.knobY = newTouch.y;
    }
  }

  return false;
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

conditional Prevent Double-Fire if (touches.length > 1 && touches[touches.length - 1].x === mouseX && touches[touches.length - 1].y === mouseY) { return false; }

Prevents both touchStarted() and mousePressed() from firing on the same touch—technical quirk of p5.js

conditional Loading Screen Guard if (gameState === "loading") return false;

Prevents input during the loading screen

conditional Mod Menu Toggle if (gameState === "game" && isHovering(mouseX, mouseY, modButton.x, modButton.y, modButton.w, modButton.h)) { gameState = "modMenu"; }

Opens the mod menu when the mod button is tapped

conditional VR Mode Toggle if (gameState === "game" && isHovering(newTouch.x, newTouch.y, vrButtonX, vrButtonY, vrButtonW, vrButtonH)) { vrMode = !vrMode; ... }

Toggles VR mode and requests device orientation permission if needed

conditional Run Button Activation if (gameState === "game" && isHovering(newTouch.x, newTouch.y, runButton.x, runButton.y, runButton.w, runButton.h)) { player.isRunning = true; runButton.active = true; runButton.touchId = newTouch.id; }

Activates running and tracks the touch ID so running stops when this specific finger lifts

conditional Joystick Activation if (gameState === "game" && newTouch.x < joystick.maxX) { joystick.active = true; joystick.touchId = newTouch.id; }

Activates joystick on the left side of the screen for movement control

conditional Start Game Transition if (gameState === "startScreen") { gameState = "game"; userStartAudio(); ... }

Begins the game and initializes audio when the player taps the start screen

if (touches.length > 1 && touches[touches.length - 1].x === mouseX && touches[touches.length - 1].y === mouseY) { return false; }
Guard against double-firing: p5.js calls both touchStarted() and mousePressed() on touch devices, so this prevents duplicate events
if (gameState === "loading") return false;
Early exit during loading screen—prevents input until the game is ready
let newTouch = touches[touches.length - 1];
Gets the most recent touch (the one that just started this frame)
if (gameState === "game" && isHovering(newTouch.x, newTouch.y, vrButtonX, vrButtonY, vrButtonW, vrButtonH)) { vrMode = !vrMode; }
Checks if the touch landed on the VR button and toggles VR mode
DeviceOrientationEvent.requestPermission().then(permissionState => { ... });
Requests permission to access device orientation (iOS requires explicit permission, Android may not)
if (!runButton.active) { player.isRunning = true; runButton.active = true; runButton.touchId = newTouch.id; }
Activates running only if the button isn't already active, and stores the touch ID for later deactivation
if (gameState === "game" && newTouch.x < joystick.maxX) { joystick.active = true; joystick.touchId = newTouch.id; }
Activates the joystick if the touch is on the left side (< 40% of screen width) and stores the touch ID
userStartAudio();
Initializes the Web Audio API context—required before playing p5.sound audio on user interaction

touchMoved()

touchMoved() is called every frame that a touch is in progress. This implementation demonstrates multi-touch tracking: each UI element has its own touchId, and touchMoved() only updates elements with active touches. The dead zone (10% of radius) is crucial for mobile controls—without it, tiny accelerometer noise or finger tremors would cause unintended movement. The velocity is set to a unit direction (cos/sin of angle) so movement is always at the full speed regardless of how far the player's finger moves.

function touchMoved() {
  if (joystick.active) {
    let joystickTouch = touches.find(t => t.id === joystick.touchId);
    if (joystickTouch) {
      let dx = joystickTouch.x - joystick.baseX;
      let dy = joystickTouch.y - joystick.baseY;
      let distance = dist(joystick.baseX, joystick.baseY, joystickTouch.x, joystickTouch.y);
      let angle = atan2(dy, dx);

      if (distance > joystick.radius) {
        joystick.knobX = joystick.baseX + joystick.radius * cos(angle);
        joystick.knobY = joystick.baseY + joystick.radius * sin(angle);
      } else {
        joystick.knobX = joystickTouch.x;
        joystick.knobY = joystickTouch.y;
      }

      if (distance > joystick.radius * 0.1) {
        player.vx = cos(angle);
        player.vy = sin(angle);
      } else {
        player.vx = 0;
        player.vy = 0;
      }
    } else {
      joystick.active = false;
      joystick.touchId = -1;
      joystick.knobX = joystick.baseX;
      joystick.knobY = joystick.baseY;
      player.vx = 0;
      player.vy = 0;
    }
  }

  if (runButton.active) {
    let runTouch = touches.find(t => t.id === runButton.touchId);
    if (!runTouch) {
      runButton.active = false;
      runButton.touchId = -1;
      player.isRunning = false;
      console.log("Running: false");
    }
  }

  return false;
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

conditional Joystick Position Update if (joystick.active) { let joystickTouch = touches.find(t => t.id === joystick.touchId); ... }

Tracks the finger on the joystick and updates player velocity based on finger distance and angle from base

conditional Joystick Radius Clamp if (distance > joystick.radius) { joystick.knobX = joystick.baseX + joystick.radius * cos(angle); }

Prevents the joystick knob from moving beyond its maximum radius

conditional Joystick Dead Zone if (distance > joystick.radius * 0.1) { player.vx = cos(angle); player.vy = sin(angle); } else { player.vx = 0; player.vy = 0; }

Prevents tiny joystick movements from causing unwanted movement (dead zone)

conditional Touch Loss Detection if (!joystickTouch) { joystick.active = false; ... player.vx = 0; player.vy = 0; }

Stops movement if the joystick touch is no longer detected (finger lifted)

conditional Run Button Touch Loss if (!runTouch) { runButton.active = false; player.isRunning = false; }

Stops running if the run button touch is lifted

let joystickTouch = touches.find(t => t.id === joystick.touchId);
Searches the touches array for the touch with the stored ID—finds the finger controlling the joystick
let dx = joystickTouch.x - joystick.baseX; let dy = joystickTouch.y - joystick.baseY;
Calculates offset from joystick base to current finger position—tells us how far the finger has moved
let distance = dist(joystick.baseX, joystick.baseY, joystickTouch.x, joystickTouch.y);
Calculates straight-line distance from base to finger—used to determine movement magnitude
let angle = atan2(dy, dx);
Calculates the angle from base to finger—converts to player movement direction
if (distance > joystick.radius) { joystick.knobX = joystick.baseX + joystick.radius * cos(angle); }
If finger moves beyond the joystick radius, clamp the visual knob to the radius edge but keep the angle
if (distance > joystick.radius * 0.1) { player.vx = cos(angle); player.vy = sin(angle); }
Only move the player if the finger is beyond 10% of the radius (dead zone), preventing drift from tiny movements
if (!joystickTouch) { joystick.active = false; joystick.touchId = -1; ... }
If the touch is no longer found, deactivate the joystick and reset player velocity

📦 Key Variables

player object

Stores the player's position (x, y), dimensions (w, h), velocity (vx, vy), and state flags (isMoving, isRunning)

let player = { x: 100, y: 100, w: 30, h: 30, vx: 0, vy: 0, isMoving: false, isRunning: false };
monsters array

Array of Monster objects currently active in the game—updated and drawn every frame

let monsters = [];
rooms object

Object mapping room names to room data (position, size, color, floor number)—defines the level layout

let rooms = { mainRoom: {...}, floor0: {...}, ... };
currentRoom string

The name of the room the player is currently in—used to determine what to draw and which monsters can chase

let currentRoom = "mainRoom";
cameraX number

The x-coordinate of the camera offset—applied to all world drawings to create the camera follow effect

let cameraX = 0;
cameraY number

The y-coordinate of the camera offset—allows vertical camera movement

let cameraY = 0;
joystick object

Stores joystick UI state: base position, knob position, active status, and touch ID for multi-touch tracking

let joystick = { baseX: 0, baseY: 0, radius: 60, knobX: 0, knobY: 0, active: false, touchId: -1 };
gameState string

The current game state—controls what draw() displays (loading, startScreen, game, jumpscare, modMenu, etc.)

let gameState = "loading";
jumpscare object

Tracks the jumpscare animation state: whether it's active, which phase (monster/blood/fadeToBlack), and animation frames

let jumpscare = { isActive: false, timer: 0, duration: 120, animationFrame: 0, phase: "monster", ... };
collectibles array

Array of collectible items in the world (keys and scrap)—managed by spawnCollectibles() and checked in updatePlayer()

let collectibles = [];
playerInventory array

Array of items the player has collected (currently keys)—displayed in the HUD

let playerInventory = [];
playerScrap number

Counter for how much scrap the player has collected—used for the scrap machine economy

let playerScrap = 0;
deviceGamma number

The device's left-right tilt angle (gamma) from the accelerometer—used for VR camera control in VR mode

let deviceGamma = 0;
deviceBeta number

The device's front-back tilt angle (beta) from the accelerometer—used for vertical VR camera control

let deviceBeta = 0;
vrMode boolean

Toggle for whether device orientation controls are active—toggled via the VR button in the mod menu

let vrMode = false;
monsterSpeedFast boolean

Mod menu toggle—when true, monsters move at MONSTER_CHASE_SPEED instead of MONSTER_SPEED while roaming

let monsterSpeedFast = false;
keysSpawnEnabled boolean

Mod menu toggle—controls whether key collectibles are spawned in the game world

let keysSpawnEnabled = true;
scrapSpawnEnabled boolean

Mod menu toggle—controls whether scrap collectibles are spawned in the game world

let scrapSpawnEnabled = true;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG Monster.update() staircase transition logic

When multiple monsters are chasing the player on different floors, the staircase loop runs many times per frame and could theoretically cause performance issues or race conditions if async transitions overlap

💡 Add a check to limit how many monsters can transition per frame, or defer pathfinding calculations to every other frame using a counter

PERFORMANCE draw() and updateMonsters()

Every monster's update() method loops through all staircases multiple times per frame (roaming direction selection, pathfinding, collision checks) even when not chasing

💡 Cache staircase data in each monster or use spatial partitioning; only run pathfinding when the monster is actually chasing

STYLE Monster class constructor

Monster initialization picks random velocity using random([-MONSTER_SPEED, MONSTER_SPEED]), which picks from only two values instead of a continuous range

💡 Use random(-MONSTER_SPEED, MONSTER_SPEED) to get smooth random velocities in the full range, or random([MONSTER_SPEED]) * random([-1, 1])

BUG collectibles loop in updatePlayer()

The loop iterates backwards (for (let i = collectibles.length - 1; i >= 0; i--)) but doesn't check if the item belongs to the current room before removing—collectibles in other rooms are never collected but the loop doesn't skip them efficiently

💡 Add early continue if item.room !== currentRoom to avoid unnecessary collision checks

FEATURE drawJumpscare()

The jumpscare animation is purely visual/audio with no interaction—the player can't escape or interact during the jumpscare sequence

💡 Add a skip button or allow rapid tapping to interrupt the jumpscare early, giving players some control during the intense moment

BUG handleDeviceOrientation()

Device orientation events are never unregistered if VR mode is turned off—the event listener continues to fire and consume resources

💡 Add window.removeEventListener('deviceorientation', handleDeviceOrientation) when VR mode is disabled

🔄 Code Flow

Code flow showing preload, setup, draw, updateplayer, updatemonsters, drawplayer, monsterclass, updatecamera, triggerjumpscare, resetgame, touchstarted, touchemoved

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] click setup href "#fn-setup" click draw href "#fn-draw" setup --> ambient-oscillator[Ambient Drone Oscillator] setup --> ambient-noise-filter[Filtered Ambient Noise] setup --> jumpscare-envelope[Jumpscare Envelope] setup --> boombox-delay[Boombox with Echo] setup --> room-definitions[Room Layout] setup --> player-initialization[Player Object] setup --> staircase-definitions[Staircases] setup --> ui-button-initialization[UI Buttons] draw --> state-switch[Game State Manager] click state-switch href "#sub-state-switch" state-switch --> game-loop-update[Game State Update] click game-loop-update href "#sub-game-loop-update" game-loop-update --> updateplayer[updatePlayer] game-loop-update --> updatemonsters[updateMonsters] game-loop-update --> updatecamera[updateCamera] game-loop-update --> ui-overlay[UI Rendering] updateplayer --> player-movement[Player Movement] click player-movement href "#sub-player-movement" player-movement --> boundary-clamp[Room Boundary Clamping] player-movement --> footstep-audio[Footstep Sound] player-movement --> collectible-collision[Collectible Pickup] player-movement --> staircase-transition[Room Transitions] updatemonsters --> constructor[Constructor] click constructor href "#sub-constructor" constructor --> monster-roaming[Roaming Behavior] constructor --> monster-chase[Chase Behavior] constructor --> multi-floor-pathfinding[Cross-Floor Pathfinding] constructor --> safe-zone-collision[Safe Zone Boundary] constructor --> player-collision[Player Collision] updatecamera --> base-camera[Base Camera Position] updatecamera --> vr-offset[Device Orientation Offset] updatecamera --> world-clamping[World Boundary Clamping] updatecamera --> camera-smoothing[Smooth Camera Interpolation] draw --> drawplayer[drawPlayer] click drawplayer href "#fn-drawplayer" drawplayer --> player-centering[Center Transform] drawplayer --> player-body[Body and Head] drawplayer --> player-eyes[Eyes with Pupils] drawplayer --> player-mouth[Mouth] drawplayer --> player-arms[Arms] draw --> touchstarted[touchStarted] click touchstarted href "#fn-touchstarted" touchstarted --> double-fire-guard[Prevent Double-Fire] touchstarted --> loading-guard[Loading Screen Guard] touchstarted --> mod-menu-button[Mod Menu Toggle] touchstarted --> vr-button[VR Mode Toggle] touchstarted --> run-button[Run Button Activation] touchstarted --> joystick-activation[Joystick Activation] touchstarted --> start-screen-transition[Start Game Transition] draw --> touchemoved[touchMoved] click touchemoved href "#fn-touchemoved" touchemoved --> joystick-tracking[Joystick Position Update] touchemoved --> radius-clamp[Joystick Radius Clamp] touchemoved --> dead-zone[Joystick Dead Zone] touchemoved --> touch-deactivation[Touch Loss Detection] touchemoved --> run-button-deactivation[Run Button Touch Loss]

❓ Frequently Asked Questions

What visual experience does the 'Lethal ape: redux update' sketch offer?

The sketch creates an immersive lab room environment filled with wandering monsters, using procedural drawings to generate visuals and an atmospheric setting.

How can players interact with the 'Lethal ape: redux update' sketch?

Users can guide the player character through the environment using on-screen buttons and device tilt, while also triggering jumpscare moments and accessing a mod menu.

What creative coding techniques are showcased in the 'Lethal ape: redux update' sketch?

This sketch demonstrates procedural drawing techniques for visual assets and incorporates device orientation to influence camera movement, enhancing user interaction.

Preview

Lethal ape: redux update - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Lethal ape: redux update - Code flow showing preload, setup, draw, updateplayer, updatemonsters, drawplayer, monsterclass, updatecamera, triggerjumpscare, resetgame, touchstarted, touchemoved
Code Flow Diagram