7 painful nights at corbuns zombie island

This is a 3D first-person island survival game built with Three.js where you walk around a low-poly island, harvest fruit from palm trees, shoot zombies that spawn at night, hire guards to help defend, and try to survive 7 nights. The game combines exploration, resource management, combat, and a day-night cycle.

🧪 Try This!

Experiment with the code by making these changes:

  1. Speed up the day-night cycle — Reduce dayLength and nightLength to make cycles faster—test how it feels and see if you can survive faster nights
  2. Make zombies slower — Reduce zombie speed so they're easier to outrun and shoot—changes the difficulty and feel of combat
  3. Create more palm trees — Increase the number of trees spawned so fruit is more abundant and exploration is rewarding
  4. Make hunger drain slower — Reduce hunger decrease rate so the player has more time to explore before needing to eat
  5. Change zombie color — Modify the hex color in the Zombie class to make them purple, red, or any color you choose
  6. Hire guards for free — Reduce the hunger cost to recruit guards so you can build a larger army more easily
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a fully playable 3D first-person survival game set on a zombie-infested island. Using Three.js for 3D rendering, PointerLockControls for free-look camera movement, and p5.js to manage the game loop, it combines multiple advanced techniques: day-night cycles that change lighting and spawn enemies, collision detection for walls and zombies, a hunger system that drains over time and requires foraging, an ammo and reload mechanic, and NPC guards that can be recruited and fight alongside you. The visual experience shifts dramatically from bright daylight exploration to dark, tense nighttime combat.

The code is organized into environment setup (ground, house, palm trees), game state management (health, ammo, hunger, nights survived), object-oriented classes for dynamic entities (Zombie, Bullet, Casing, Guard), and continuous update loops that handle movement, collision, spawning, and rendering. By studying this sketch you will learn how to structure a large interactive Three.js project, implement a game state machine with day-night cycles, build an NPC AI system that hunts the player or guards, manage inventory-like variables (ammo, hunger), and integrate keyboard/mouse input into 3D first-person controls.

⚙️ How It Works

  1. When setup() runs, Three.js is initialized with a scene, camera, lights (directional sun and hemisphere for global illumination), and the environment is built: a ground plane, an interactive house with collision walls, and 10 palm trees scattered randomly. The p5.js canvas is hidden, and the game waits for the player to click 'Start Game' and request pointer lock (mouse capture).
  2. Once the game starts, the draw loop runs 60 times per second. Every frame updates game time, checks what phase of the day-night cycle is active, and updates the player position based on keyboard input (WASD), applies gravity and collision detection with house walls, and syncs the camera to the player's position.
  3. During the day (first 60 seconds of each cycle), the sun orbits overhead via the directional light, the sky is bright blue, zombies are cleared if any remain, fruit respawns on trees, and the gun is hidden. The player can walk around freely and harvest fruit by pressing E near a palm tree to gain hunger points.
  4. At night (next 300 seconds), the sky turns black, the directional light dims to a blue tint, and a new zombie spawns every 2 seconds. The player gains a gun and can shoot with left-click, reload with R, and must manage limited ammo. Guards can be recruited with Q if the player has enough hunger.
  5. Zombies pursue the nearest target (player or guard), deal damage on contact, and die when shot. Guards patrol, target nearby zombies, shoot at them automatically, and defend the player. Bullets are fast projectiles that vanish after 100 frames. Casings (ejected ammo) fall with physics and decay.
  6. If the player's health reaches 0 (from zombie bites or starvation), the game ends. If hunger reaches 0, health slowly drains. If the player survives 7 nights without dying, they win. The game state (health, ammo, hunger, night counter) is displayed in the top-left corner and updates every frame.

🎓 Concepts You'll Learn

Three.js 3D renderingFirst-person camera controlsGame state machine (day-night cycle)Object-oriented game entities (classes)Collision detection and responseNPC AI and pathfindingResource management systemsEvent handling (keyboard and mouse)Dynamic lightingParticle effects (bullet casings)

📝 Code Breakdown

setup()

setup() runs once at startup and initializes the entire game environment: the Three.js scene, lights, camera, all static geometry (ground, house, trees), UI elements, and input listeners. It's the crucial foundation where you would add new environment objects or change rendering quality.

function setup() {
  createCanvas(1, 1);
  noLoop();
  scene = new THREE.Scene();
  camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
  renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(window.innerWidth, window.innerHeight);
  document.body.appendChild(renderer.domElement);
  renderer.shadowMap.enabled = true;
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
  camera.position.copy(playerPosition);
  hemisphereLight = new THREE.HemisphereLight(0xadd8e6, 0x00008b, 0.5);
  scene.add(hemisphereLight);
  directionalLight = new THREE.DirectionalLight(0xffffff, 1);
  directionalLight.position.set(5, 10, 5);
  directionalLight.castShadow = true;
  directionalLight.shadow.mapSize.width = 2048;
  directionalLight.shadow.mapSize.height = 2048;
  directionalLight.shadow.camera.near = 0.5;
  directionalLight.shadow.camera.far = 50;
  directionalLight.shadow.camera.left = -20;
  directionalLight.shadow.camera.right = 20;
  directionalLight.shadow.camera.top = 20;
  directionalLight.shadow.camera.bottom = -20;
  scene.add(directionalLight);
  houseInteriorLight = new THREE.PointLight(0xffffee, 0.8, 10);
  houseInteriorLight.position.set(0, 3, 0);
  houseInteriorLight.castShadow = true;
  scene.add(houseInteriorLight);
  createGround();
  createMapBarriers();
  createHouse();
  createPalmTrees(10);
  controls = new THREE.PointerLockControls(camera, renderer.domElement);
  scene.add(controls.getObject());
  uiDiv = createDiv();
  uiDiv.id('ui');
  uiDiv.style('position', 'absolute');
  uiDiv.style('top', '10px');
  uiDiv.style('left', '10px');
  uiDiv.style('color', 'white');
  uiDiv.style('font-family', 'monospace');
  uiDiv.style('font-size', '16px');
  uiDiv.style('z-index', '100');
  healthSpan = createSpan(`Health: ${playerHealth}`);
  healthSpan.id('health');
  uiDiv.child(healthSpan);
  uiDiv.child(createElement('br'));
  ammoSpan = createSpan(`Ammo: ${playerAmmo}`);
  ammoSpan.id('ammo');
  uiDiv.child(ammoSpan);
  uiDiv.child(createElement('br'));
  hungerSpan = createSpan(`Hunger: ${playerHunger}`);
  hungerSpan.id('hunger');
  uiDiv.child(hungerSpan);
  uiDiv.child(createElement('br'));
  nightSpan = createSpan(`Nights Survived: ${nightsSurvived}`);
  nightSpan.id('nights');
  uiDiv.child(nightSpan);
  uiDiv.child(createElement('br'));
  messageDiv = createDiv('');
  messageDiv.id('message');
  messageDiv.style('font-size', '24px');
  messageDiv.style('text-align', 'center');
  messageDiv.style('position', 'absolute');
  messageDiv.style('width', '100%');
  messageDiv.style('top', '40%');
  uiDiv.child(messageDiv);
  instructionsDiv = createDiv(`<p>Welcome to Island Survival!</p><p>Controls:</p><ul><li>W, A, S, D: Move</li><li>Space: Jump</li><li>Mouse: Look around</li><li>Left Click: Shoot (at night)</li><li>R: Reload (at night)</li><li>E: Interact (e.g., harvest fruit from trees)</li><li>Q: Hire a guard (${GUARD_COST_HUNGER} hunger)</li><li>Esc: Exit Pointer Lock</li></ul><p>Survive 7 nights to win. Zombies appear at night. A gun will appear when night falls. Keep your hunger up by eating fruit!</p>`);
  instructionsDiv.id('instructions');
  instructionsDiv.style('position', 'absolute');
  instructionsDiv.style('top', '50%');
  instructionsDiv.style('left', '50%');
  instructionsDiv.style('transform', 'translate(-50%, -50%)');
  instructionsDiv.style('background', 'rgba(0,0,0,0.7)');
  instructionsDiv.style('padding', '20px');
  instructionsDiv.style('border-radius', '10px');
  instructionsDiv.style('color', 'white');
  instructionsDiv.style('text-align', 'left');
  instructionsDiv.style('z-index', '101');
  document.body.appendChild(instructionsDiv.elt);
  startGameButton = createButton('Click to Start Game');
  startGameButton.parent(instructionsDiv);
  startGameButton.style('margin-top', '20px');
  startGameButton.style('padding', '10px 20px');
  startGameButton.style('font-size', '18px');
  startGameButton.style('cursor', 'pointer');
  startGameButton.mousePressed(startGame);
  gunMesh = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.1, 0.5), new THREE.MeshStandardMaterial({ color: 0x808080 }));
  gunMesh.position.set(0.3, -0.2, -0.5);
  gunMesh.castShadow = true;
  gunMesh.receiveShadow = true;
  gunMesh.visible = false;
  camera.add(gunMesh);
  renderer.domElement.addEventListener('mousedown', onMouseDown, false);
}
Line-by-line explanation (25 lines)

🔧 Subcomponents:

initialization Three.js Scene Setup scene = new THREE.Scene();

Creates the 3D scene container that holds all game objects, lights, and the camera

initialization Perspective Camera camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);

Sets up a 75-degree field-of-view first-person camera with aspect ratio matching the window

initialization WebGL Renderer renderer = new THREE.WebGLRenderer({ antialias: true });

Creates the WebGL renderer that draws the 3D scene to the canvas

initialization Lights hemisphereLight = new THREE.HemisphereLight(0xadd8e6, 0x00008b, 0.5);

Adds ambient lighting that simulates the sky and ground colors

initialization UI Elements Creation uiDiv = createDiv();

Creates HTML UI elements (health, ammo, hunger, nights display) overlaid on the canvas

createCanvas(1, 1);
Creates a tiny p5.js canvas (1x1 pixel) just to enable p5.js lifecycle—Three.js does the actual rendering
noLoop();
Stops p5.js draw loop initially; it will restart in startGame() when the player clicks Start
scene = new THREE.Scene();
Initializes the 3D scene that will contain all 3D objects (ground, house, trees, zombies, etc.)
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
Creates a perspective camera with 75-degree field of view, window aspect ratio, near clipping at 0.1 units, far at 1000 units
renderer = new THREE.WebGLRenderer({ antialias: true });
Creates a WebGL renderer with antialiasing enabled for smoother edges
renderer.setSize(window.innerWidth, window.innerHeight);
Sizes the renderer to fill the entire window
document.body.appendChild(renderer.domElement);
Adds the Three.js canvas to the HTML body, making it visible
renderer.shadowMap.enabled = true;
Enables shadow rendering so objects can cast shadows on the ground and walls
hemisphereLight = new THREE.HemisphereLight(0xadd8e6, 0x00008b, 0.5);
Adds a hemisphere light with light-blue sky color (0xadd8e6) and dark-blue ground color (0x00008b) at half intensity
directionalLight = new THREE.DirectionalLight(0xffffff, 1);
Creates a white directional light (the sun) at full intensity that casts shadows
directionalLight.position.set(5, 10, 5);
Positions the sun initially in the sky; this will animate during the day-night cycle
directionalLight.shadow.mapSize.width = 2048;
Sets shadow map resolution to 2048x2048 pixels for crisp, detailed shadows
houseInteriorLight = new THREE.PointLight(0xffffee, 0.8, 10);
Creates a point light (pale yellow) inside the house at intensity 0.8 with range of 10 units
createGround();
Calls function to create the ground plane mesh and add it to the scene
createMapBarriers();
Calls function to create invisible collision barriers at the island edges
createHouse();
Calls function to create the house with walls, roof, door, furniture, and collision objects
createPalmTrees(10);
Calls function to create 10 palm trees with trunks, leaves, and harvestable red fruit
controls = new THREE.PointerLockControls(camera, renderer.domElement);
Initializes pointer lock controls that capture mouse movement for free-look camera control
scene.add(controls.getObject());
Adds the camera to the scene via the controls object so movement updates are applied correctly
healthSpan = createSpan(`Health: ${playerHealth}`);
Creates a text span showing current health; will be updated every frame in drawUI()
hungerSpan = createSpan(`Hunger: ${playerHunger}`);
Creates a text span showing current hunger level
gunMesh = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.1, 0.5), new THREE.MeshStandardMaterial({ color: 0x808080 }));
Creates a simple grey box to represent the gun; it will be positioned at the camera and shown only at night
gunMesh.visible = false;
Hides the gun initially; it appears when night starts
camera.add(gunMesh);
Attaches the gun to the camera so it moves and rotates with the player's view
renderer.domElement.addEventListener('mousedown', onMouseDown, false);
Registers a mousedown listener so left-click fires bullets when the gun is equipped

draw()

The draw() function is the heartbeat of the game—it runs 60 times per second. Every frame, it updates game logic (movement, collisions, AI), then renders the scene. The order matters: logic updates must happen before collision checks, and rendering must happen last. This is the main game loop pattern used in nearly all real-time games.

function draw() {
  if (gameOver || winGame) {
    noLoop();
    return;
  }
  const deltaTime = (millis() - time) / 1000;
  time = millis();
  updateDayNightCycle();
  updatePlayerMovement(deltaTime);
  updateZombies(deltaTime);
  updateBullets(deltaTime);
  updateCasings(deltaTime);
  updatePalmTrees(deltaTime);
  updateHunger(deltaTime);
  updateGuards(deltaTime);
  checkCollisions();
  checkGuardCollisions();
  drawUI();
  renderer.render(scene, camera);
}
Line-by-line explanation (17 lines)

🔧 Subcomponents:

conditional Game Over Check if (gameOver || winGame) { noLoop(); return; }

Stops the game loop if the player dies or wins, halting all updates and rendering

calculation Delta Time Calculation const deltaTime = (millis() - time) / 1000;

Calculates elapsed time in seconds since last frame for smooth frame-rate-independent animation

sequence Game Update Sequence updateDayNightCycle(); updatePlayerMovement(deltaTime); updateZombies(deltaTime);

Updates all game logic: time of day, player position, zombie AI, bullets, hunger, guards, and collisions

sequence Collision Detection checkCollisions(); checkGuardCollisions();

Detects and resolves all collisions: bullets hitting zombies, player hitting walls, zombies hitting guards

function-call Three.js Rendering renderer.render(scene, camera);

Renders the 3D scene to the screen with all updated positions, animations, and lighting

if (gameOver || winGame) {
Checks if the player has died or won—if so, the game ends
noLoop();
Stops the p5.js draw loop, halting all further game updates and rendering
return;
Exits the draw function early, skipping the rest of the frame updates
const deltaTime = (millis() - time) / 1000;
Calculates how many seconds have elapsed since the last frame by comparing current time to previous frame time, then dividing by 1000 to convert milliseconds to seconds
time = millis();
Stores the current timestamp for the next frame's delta time calculation
updateDayNightCycle();
Updates the day-night cycle: changes lighting, spawns zombies at night, respawns fruit during day, transitions between phases
updatePlayerMovement(deltaTime);
Processes keyboard input, applies gravity, updates player position, and handles player-environment collisions
updateZombies(deltaTime);
Updates all active zombies: moves them toward nearest target (player or guard), makes them attack on contact, and removes dead ones
updateBullets(deltaTime);
Advances bullet positions, removing bullets that have traveled beyond their lifespan
updateCasings(deltaTime);
Updates ejected ammo casings: applies gravity, handles ground collision, rotates them, and removes old casings
updatePalmTrees(deltaTime);
Checks if fruit respawn cooldowns have elapsed and makes fruit visible again if so
updateHunger(deltaTime);
Decreases hunger over time, and if hunger hits zero, damages the player health
updateGuards(deltaTime);
Updates all hired guards: targets zombies, moves toward them or the player, and shoots at enemies
checkCollisions();
Detects bullet-zombie hits and player-wall collisions, handling damage and position corrections
checkGuardCollisions();
Detects zombie-guard contacts and applies attack damage to guards
drawUI();
Updates the on-screen UI text: health, ammo, hunger, and nights survived
renderer.render(scene, camera);
Renders the entire 3D scene to the screen from the camera's perspective, drawing all updated objects, lights, and shadows

updatePlayerMovement()

This function handles first-person camera movement every frame. It reads keyboard state, applies physics (gravity), transforms camera-relative movement into world-space movement, handles ground collisions, and syncs the camera. The key insight is applyQuaternion()—this converts WASD input from 'camera-local' (W is always forward from the player's view) into 'world-space' (where the game actually is) using the camera's rotation.

function updatePlayerMovement(deltaTime) {
  if (!controls.isLocked || gameOver || winGame) return;
  playerVelocity.y -= gravity;
  let moveX = 0;
  let moveZ = 0;
  if (moveForward) moveZ -= moveSpeed;
  if (moveBackward) moveZ += moveSpeed;
  if (moveLeft) moveX -= moveSpeed;
  if (moveRight) moveX += moveSpeed;
  playerVelocity.x = moveX;
  playerVelocity.z = moveZ;
  playerVelocity.applyQuaternion(camera.quaternion);
  playerPosition.add(playerVelocity);
  if (playerPosition.y < 1.7) {
    playerPosition.y = 1.7;
    playerVelocity.y = 0;
    canJump = true;
  }
  camera.position.copy(playerPosition);
  if (keyState['Digit6'] && keyState['Digit7'] && !cheatActive && millis() - lastCheatTime > CHEAT_COOLDOWN) {
    cheatActive = true;
    lastCheatTime = millis();
    for (let i = 0; i < CHEAT_GUARD_COUNT; i++) {
      const guardPosition = playerPosition.clone().add(new THREE.Vector3(random(-5, 5), 0, random(-5, 5)));
      guards.push(new Guard(guardPosition.x, guardPosition.y, guardPosition.z));
    }
    messageDiv.html(`Cheat Activated! ${CHEAT_GUARD_COUNT} free guards have joined your team! 💰`);
    setTimeout(() => messageDiv.html(''), 3000);
    keyState['Digit6'] = false;
    keyState['Digit7'] = false;
  }
  if (keyState['Digit6'] && keyState['Digit7'] && keyState['Digit8'] && keyState['Digit9'] && !godModeActive && millis() - lastGodModeCheatTime > GOD_MODE_CHEAT_COOLDOWN) {
    godModeActive = true;
    lastGodModeCheatTime = millis();
    messageDiv.html('God Mode Activated! Your defenders are invincible, and your supplies are endless! ✨');
    setTimeout(() => messageDiv.html(''), 3000);
    keyState['Digit6'] = false;
    keyState['Digit7'] = false;
    keyState['Digit8'] = false;
    keyState['Digit9'] = false;
  }
}
Line-by-line explanation (22 lines)

🔧 Subcomponents:

conditional Pointer Lock Check if (!controls.isLocked || gameOver || winGame) return;

Exits early if mouse is not captured, or if the game is over—prevents movement after death or before pointer lock

calculation Gravity Application playerVelocity.y -= gravity;

Applies downward acceleration (gravity) every frame to simulate falling

conditional-sequence Movement Input Processing if (moveForward) moveZ -= moveSpeed;

Reads keyboard state variables and sets movement in camera-local space

calculation Camera Rotation Transform playerVelocity.applyQuaternion(camera.quaternion);

Transforms movement from camera-local space to world space based on where the player is looking

conditional Ground Collision if (playerPosition.y < 1.7) { playerPosition.y = 1.7; canJump = true; }

Prevents player from falling through ground and allows jumping only when touching ground

calculation Camera Sync camera.position.copy(playerPosition);

Updates camera position to match player position so the view always follows the player

conditional Guard Cheat Code if (keyState['Digit6'] && keyState['Digit7'] && !cheatActive && millis() - lastCheatTime > CHEAT_COOLDOWN)

Detects simultaneous key press (6+7) and spawns 10 free guards as a hidden cheat

conditional God Mode Cheat Code if (keyState['Digit6'] && keyState['Digit7'] && keyState['Digit8'] && keyState['Digit9'] && !godModeActive && millis() - lastGodModeCheatTime > GOD_MODE_CHEAT_COOLDOWN)

Detects simultaneous key press (6+7+8+9) and activates god mode (infinite ammo, guards invincible, no hunger drain)

if (!controls.isLocked || gameOver || winGame) return;
Returns early if the pointer is not locked (mouse not captured), or if the game is over—prevents unintended movement
playerVelocity.y -= gravity;
Subtracts gravity (0.05) from vertical velocity every frame, making the player accelerate downward
let moveX = 0;
Initializes horizontal velocity in camera-local space (will be modified by WASD keys)
let moveZ = 0;
Initializes forward/backward velocity in camera-local space
if (moveForward) moveZ -= moveSpeed;
If W is pressed, subtracts moveSpeed (0.1) from forward/backward velocity, moving the player forward relative to camera view
if (moveBackward) moveZ += moveSpeed;
If S is pressed, adds moveSpeed, moving backward
if (moveLeft) moveX -= moveSpeed;
If A is pressed, subtracts moveSpeed, moving left relative to camera
if (moveRight) moveX += moveSpeed;
If D is pressed, adds moveSpeed, moving right
playerVelocity.x = moveX;
Sets the velocity x component to the calculated movement
playerVelocity.z = moveZ;
Sets the velocity z component to the calculated movement
playerVelocity.applyQuaternion(camera.quaternion);
Rotates the velocity vector by the camera's rotation, converting camera-local movement to world-space movement—this makes the player move in the direction they're looking
playerPosition.add(playerVelocity);
Adds the velocity to the current position, moving the player
if (playerPosition.y < 1.7) {
Checks if the player has fallen below ground level (1.7 units is player height)
playerPosition.y = 1.7;
Clamps the player's y position to ground level, preventing them from sinking through
playerVelocity.y = 0;
Resets vertical velocity to zero when touching ground, stopping falling motion
canJump = true;
Allows the player to jump again only when their feet are on the ground
camera.position.copy(playerPosition);
Synchronizes the camera position to the player position so the view always follows where the player is
if (keyState['Digit6'] && keyState['Digit7'] && !cheatActive && millis() - lastCheatTime > CHEAT_COOLDOWN) {
Checks if keys 6 and 7 are both pressed, the cheat hasn't been used already, and enough time has passed since last cheat use
for (let i = 0; i < CHEAT_GUARD_COUNT; i++) {
Loops 10 times to spawn 10 guard objects around the player
const guardPosition = playerPosition.clone().add(new THREE.Vector3(random(-5, 5), 0, random(-5, 5)));
Creates a position for each guard at a random offset from the player within ±5 units
if (keyState['Digit6'] && keyState['Digit7'] && keyState['Digit8'] && keyState['Digit9'] && !godModeActive && millis() - lastGodModeCheatTime > GOD_MODE_CHEAT_COOLDOWN) {
Checks if all four keys (6, 7, 8, 9) are pressed simultaneously and god mode isn't already active
godModeActive = true;
Sets the flag to enable god mode, which prevents hunger drain and makes guards invincible

updateDayNightCycle()

This function implements the day-night game cycle. It uses modulo arithmetic to create a repeating 360-second cycle, checks whether we're in day or night phase, detects transitions, and updates lighting accordingly. The clever part is using sine and cosine to animate the sun position—this creates a smooth arc across the sky. Transitions trigger important game events: day clears zombies and respawns fruit, night equips the gun and begins spawning enemies.

🔬 These four lines animate the sun's position throughout the day using trigonometry (sin and cos). What happens if you change the multiplier '* 10' to '* 20'? Or change '* Math.PI' to '* Math.PI * 2'?

    const dayProgress = currentCycleTime / dayLength;
    directionalLight.position.x = Math.sin(dayProgress * Math.PI) * 10;
    directionalLight.position.y = 10;
    directionalLight.position.z = -5 + Math.cos(dayProgress * Math.PI) * 10;
function updateDayNightCycle() {
  const currentCycleTime = millis() % cycleLength;
  if (currentCycleTime < dayLength) {
    if (isNight) {
      isNight = false;
      nightsSurvived++;
      messageDiv.html(`Night ${nightsSurvived} Survived!`);
      zombies.forEach(z => { scene.remove(z.mesh); });
      zombies = [];
      gunEquipped = false;
      gunMesh.visible = false;
      casings.forEach(c => { scene.remove(c.mesh); });
      casings = [];
      palmTrees.forEach(tree => {
        tree.hasFruit = true;
        tree.harvestCooldown = 0;
        if (tree.fruitMesh) tree.fruitMesh.visible = true;
      });
      if (nightsSurvived >= MAX_NIGHTS) {
        winGame = true;
        messageDiv.html(`You survived ${MAX_NIGHTS} nights! YOU WIN! 🎉`);
        gameOver = true;
      } else {
        setTimeout(() => messageDiv.html(''), 3000);
      }
    }
    const dayProgress = currentCycleTime / dayLength;
    directionalLight.position.x = Math.sin(dayProgress * Math.PI) * 10;
    directionalLight.position.y = 10;
    directionalLight.position.z = -5 + Math.cos(dayProgress * Math.PI) * 10;
    directionalLight.color.setHex(0xffffff);
    directionalLight.intensity = 1;
    hemisphereLight.color.setHex(0xadd8e6);
    hemisphereLight.intensity = 0.5;
    renderer.setClearColor(0x87ceeb);
  } else {
    if (!isNight) {
      isNight = true;
      nightfallTime = millis();
      messageDiv.html('Nightfall! Zombies are coming! 🧟');
      gunEquipped = true;
      gunMesh.visible = true;
      playerAmmo = 10;
      setTimeout(() => messageDiv.html(''), 3000);
    }
    const nightProgress = (currentCycleTime - dayLength) / nightLength;
    directionalLight.position.x = Math.sin(nightProgress * Math.PI) * 10;
    directionalLight.position.y = 10;
    directionalLight.position.z = 5 - Math.cos(nightProgress * Math.PI) * 10;
    directionalLight.color.setHex(0xaaaaee);
    directionalLight.intensity = 0.3;
    hemisphereLight.color.setHex(0x000033);
    hemisphereLight.intensity = 0.2;
    renderer.setClearColor(0x000000);
    if (millis() - lastZombieSpawnTime > zombieSpawnInterval) {
      spawnZombie();
      lastZombieSpawnTime = millis();
    }
  }
}
Line-by-line explanation (33 lines)

🔧 Subcomponents:

calculation Cycle Position Calculation const currentCycleTime = millis() % cycleLength;

Calculates position within the day-night cycle using modulo—repeats every dayLength + nightLength milliseconds

conditional Day Phase Check if (currentCycleTime < dayLength) {

Determines if the current time is in the day phase or night phase

conditional-sequence Night to Day Transition if (isNight) { isNight = false; nightsSurvived++;

When transitioning FROM night TO day, increments nights survived counter, clears zombies, respawns fruit, and hides the gun

calculation Day Lighting Update directionalLight.color.setHex(0xffffff); directionalLight.intensity = 1;

Sets the sun to white at full intensity during the day

calculation Day Sky Color renderer.setClearColor(0x87ceeb);

Sets the background clear color to sky blue

conditional-sequence Day to Night Transition if (!isNight) { isNight = true; gunEquipped = true;

When transitioning FROM day TO night, equips the gun, shows ammo, and warns the player

calculation Night Lighting Update directionalLight.color.setHex(0xaaaaee); directionalLight.intensity = 0.3;

Sets the moon to blue-tinted and dims it to 30% intensity

conditional Zombie Spawn Check if (millis() - lastZombieSpawnTime > zombieSpawnInterval)

Spawns a new zombie every 2 seconds during night

const currentCycleTime = millis() % cycleLength;
Calculates the current position within a complete day-night cycle using modulo operator—resets after 360 seconds (60s day + 300s night)
if (currentCycleTime < dayLength) {
Checks if we are in the day phase (first 60 seconds of the cycle)
if (isNight) {
Checks if we just transitioned FROM night TO day—this block only runs once per transition
isNight = false;
Sets the night flag to false, marking that we are now in day phase
nightsSurvived++;
Increments the nights counter by 1 whenever a night ends
zombies.forEach(z => { scene.remove(z.mesh); });
Removes all remaining zombie meshes from the 3D scene
zombies = [];
Clears the zombies array, deleting all zombie references
gunEquipped = false;
Unequips the gun—the player can no longer shoot
gunMesh.visible = false;
Hides the gun mesh from the 3D view
palmTrees.forEach(tree => { tree.hasFruit = true; tree.harvestCooldown = 0; if (tree.fruitMesh) tree.fruitMesh.visible = true; });
Restores fruit to all palm trees and makes them visible again at the start of each day
if (nightsSurvived >= MAX_NIGHTS) {
Checks if the player has survived 7 nights (MAX_NIGHTS)
winGame = true;
Sets the win flag to true, triggering game over with a victory condition
const dayProgress = currentCycleTime / dayLength;
Calculates a progress value from 0 to 1 representing how far through the day we are
directionalLight.position.x = Math.sin(dayProgress * Math.PI) * 10;
Animates the sun's x position using sine: at dayProgress=0, x=-0; at 0.5, x=10; at 1, x=0—creates an east-to-west arc
directionalLight.position.y = 10;
Keeps the sun at a constant height of 10 units above the ground
directionalLight.color.setHex(0xffffff);
Sets the sun to white during the day
directionalLight.intensity = 1;
Sets the sun to full brightness during the day
hemisphereLight.color.setHex(0xadd8e6);
Sets hemisphere light to light blue during the day
renderer.setClearColor(0x87ceeb);
Sets the background to sky blue (RGB: 135, 206, 235)
} else {
Else block—we are in the night phase
if (!isNight) {
Checks if we just transitioned FROM day TO night—this block only runs once per transition
isNight = true;
Sets the night flag to true
gunEquipped = true;
Equips the gun for combat
gunMesh.visible = true;
Makes the gun visible in the player's hand
playerAmmo = 10;
Resets ammo to 10 bullets at the start of each night
const nightProgress = (currentCycleTime - dayLength) / nightLength;
Calculates a progress value from 0 to 1 representing how far through the night we are
directionalLight.color.setHex(0xaaaaee);
Sets the moon to a blue-tinted color (RGB: 170, 170, 238)
directionalLight.intensity = 0.3;
Dims the moon to 30% brightness, creating a dark nighttime atmosphere
hemisphereLight.intensity = 0.2;
Dims the hemisphere light to very low intensity, keeping the scene mostly dark
renderer.setClearColor(0x000000);
Sets the background to black
if (millis() - lastZombieSpawnTime > zombieSpawnInterval) {
Checks if at least 2 seconds have passed since the last zombie spawn
spawnZombie();
Spawns a new zombie at a random location on the island
lastZombieSpawnTime = millis();
Records the current time as the last spawn time, resetting the spawn timer

Zombie class

The Zombie class represents an enemy entity. The constructor builds the zombie's visual model (body + head), and the update() method runs every frame to move the zombie toward the nearest target, make it face that target, and attack if close enough. The key AI logic is finding the nearest target—this makes zombies smart enough to avoid empty space and gang up on vulnerable targets.

class Zombie {
  constructor(x, y, z) {
    const bodyGeometry = new THREE.BoxGeometry(0.8, 1.5, 0.5);
    const headGeometry = new THREE.BoxGeometry(0.6, 0.6, 0.6);
    const bodyMaterial = new THREE.MeshStandardMaterial({ color: 0x006400 });
    const headMaterial = new THREE.MeshStandardMaterial({ color: 0x8b4513 });
    this.body = new THREE.Mesh(bodyGeometry, bodyMaterial);
    this.head = new THREE.Mesh(headGeometry, headMaterial);
    this.head.position.y = 1;
    this.mesh = new THREE.Group();
    this.mesh.add(this.body);
    this.mesh.add(this.head);
    this.mesh.position.set(x, y + 0.75, z);
    this.mesh.castShadow = true;
    this.mesh.receiveShadow = true;
    scene.add(this.mesh);
    this.health = ZOMBIE_HEALTH;
    this.attackTimer = 0;
  }

  update(deltaTime) {
    if (!gameActive || gameOver || winGame) return;
    let targetPosition = playerPosition.clone();
    let targetMesh = null;
    let minDistance = this.mesh.position.distanceTo(playerPosition);
    if (guards.length > 0) {
      for (const guard of guards) {
        const distanceToGuard = this.mesh.position.distanceTo(guard.mesh.position);
        if (distanceToGuard < minDistance) {
          minDistance = distanceToGuard;
          targetPosition = guard.mesh.position.clone();
          targetMesh = guard;
        }
      }
    }
    const direction = targetPosition.clone().sub(this.mesh.position).normalize();
    this.mesh.position.addScaledVector(direction, ZOMBIE_SPEED);
    this.mesh.lookAt(targetPosition.x, this.mesh.position.y, targetPosition.z);
    if (minDistance < 1.5) {
      if (millis() - this.attackTimer > ZOMBIE_ATTACK_COOLDOWN) {
        if (targetMesh) {
          targetMesh.takeDamage(ZOMBIE_ATTACK_DAMAGE);
        } else {
          playerHealth -= ZOMBIE_ATTACK_DAMAGE;
          if (playerHealth <= 0) {
            playerHealth = 0;
            gameOver = true;
            messageDiv.html('GAME OVER! You were overwhelmed by zombies. 💀');
            noLoop();
          }
        }
        this.attackTimer = millis();
      }
    }
  }

  takeDamage(damage) {
    this.health -= damage;
    if (this.health <= 0) {
      scene.remove(this.mesh);
      zombies = zombies.filter(z => z !== this);
    }
  }
}
Line-by-line explanation (28 lines)

🔧 Subcomponents:

method Constructor constructor(x, y, z) {

Initializes a zombie with a body, head, position, health, and attack timer

initialization Geometry Creation const bodyGeometry = new THREE.BoxGeometry(0.8, 1.5, 0.5);

Creates 3D shapes: a box for the body and head

initialization Mesh Assembly this.mesh = new THREE.Group();

Groups body and head meshes together so they move and rotate as one unit

loop Target Selection for (const guard of guards) { const distanceToGuard = this.mesh.position.distanceTo(guard.mesh.position);

Finds the nearest target (player or guard) by checking distances to all guards

calculation Movement Toward Target this.mesh.position.addScaledVector(direction, ZOMBIE_SPEED);

Moves the zombie toward the nearest target at constant speed

conditional Attack Logic if (minDistance < 1.5) { if (millis() - this.attackTimer > ZOMBIE_ATTACK_COOLDOWN)

Checks if zombie is close enough to attack and if enough time has passed since last attack

conditional Death Detection if (this.health <= 0) { scene.remove(this.mesh); zombies = zombies.filter(z => z !== this);

Removes dead zombies from the scene and the game's zombie array

const bodyGeometry = new THREE.BoxGeometry(0.8, 1.5, 0.5);
Creates a box shape 0.8 wide, 1.5 tall, 0.5 deep to represent the zombie body
const bodyMaterial = new THREE.MeshStandardMaterial({ color: 0x006400 });
Creates a dark green material (RGB: 0, 100, 0) for the body
this.body = new THREE.Mesh(bodyGeometry, bodyMaterial);
Combines the geometry and material into a renderable mesh
this.head.position.y = 1;
Positions the head 1 unit above the body's center
this.mesh = new THREE.Group();
Creates a group that will hold both body and head, so they move together
this.mesh.add(this.body);
Adds the body mesh to the group
this.mesh.add(this.head);
Adds the head mesh to the group
this.mesh.position.set(x, y + 0.75, z);
Positions the zombie at the given coordinates (usually on the ground at y=0, offset by 0.75 so the bottom sits at ground level)
scene.add(this.mesh);
Adds the zombie to the 3D scene so it renders
this.health = ZOMBIE_HEALTH;
Initializes the zombie with 30 health points
this.attackTimer = 0;
Initializes the attack cooldown timer to 0 (can attack immediately)
let targetPosition = playerPosition.clone();
Sets the default target to the player's current position
let minDistance = this.mesh.position.distanceTo(playerPosition);
Calculates the distance to the player and stores it as the initial minimum
for (const guard of guards) {
Loops through all hired guards to check if any are closer than the player
const distanceToGuard = this.mesh.position.distanceTo(guard.mesh.position);
Calculates the distance from this zombie to the current guard
if (distanceToGuard < minDistance) {
Checks if this guard is closer than the current nearest target
targetPosition = guard.mesh.position.clone();
If so, updates the target position to the guard's position
targetMesh = guard;
Stores a reference to the guard object so we can call takeDamage() on it later
const direction = targetPosition.clone().sub(this.mesh.position).normalize();
Calculates a unit vector pointing from the zombie to its target
this.mesh.position.addScaledVector(direction, ZOMBIE_SPEED);
Moves the zombie 0.02 units per frame in the direction of the target
this.mesh.lookAt(targetPosition.x, this.mesh.position.y, targetPosition.z);
Rotates the zombie to face the target
if (minDistance < 1.5) {
Checks if the zombie is within 1.5 units (touching distance) of its target
if (millis() - this.attackTimer > ZOMBIE_ATTACK_COOLDOWN) {
Checks if at least 1 second (1000 ms) has passed since the last attack
if (targetMesh) { targetMesh.takeDamage(ZOMBIE_ATTACK_DAMAGE); } else { playerHealth -= ZOMBIE_ATTACK_DAMAGE;
If the target is a guard, damages the guard; otherwise damages the player
this.attackTimer = millis();
Records the current time as the last attack time, starting the cooldown countdown
this.health -= damage;
Subtracts the bullet damage from zombie health
if (this.health <= 0) { scene.remove(this.mesh);
If the zombie's health reaches 0, removes its mesh from the 3D scene
zombies = zombies.filter(z => z !== this);
Removes this zombie object from the zombies array by keeping only zombies that are not this one

Guard class

The Guard class is a hired NPC that fights alongside the player. It has sophisticated AI: it finds targets, chases them if out of range, attacks them if in range, and follows the player if they stray too far. Guards have more health than zombies and attack faster, making them valuable allies. The layered targeting logic (target must exist, be alive, and be in range) prevents wasted effort on dead enemies.

🔬 This condition re-targets if the current target is invalid. What happens if you remove the 'this.target.health <= 0' check? Guards would keep shooting at zombies even after they're dead—but would the bullets still be generated?

    if (!this.target || this.target.health <= 0 || this.mesh.position.distanceTo(this.target.mesh.position) > GUARD_ATTACK_RANGE) {
      this.target = this.findNearestZombie();
    }
class Guard {
  constructor(x, y, z) {
    const bodyGeometry = new THREE.BoxGeometry(0.8, 1.8, 0.6);
    const headGeometry = new THREE.SphereGeometry(0.4, 16, 16);
    const armGeometry = new THREE.BoxGeometry(0.2, 0.8, 0.2);
    const legGeometry = new THREE.BoxGeometry(0.3, 0.8, 0.3);
    const bodyMaterial = new THREE.MeshStandardMaterial({ color: 0x4682b4 });
    const headMaterial = new THREE.MeshStandardMaterial({ color: 0xffe0bd });
    const armMaterial = new THREE.MeshStandardMaterial({ color: 0x4682b4 });
    const legMaterial = new THREE.MeshStandardMaterial({ color: 0x2f4f4f });
    this.body = new THREE.Mesh(bodyGeometry, bodyMaterial);
    this.head = new THREE.Mesh(headGeometry, headMaterial);
    this.head.position.y = 1.1;
    this.leftArm = new THREE.Mesh(armGeometry, armMaterial);
    this.leftArm.position.set(-0.5, 0.7, 0);
    this.rightArm = new THREE.Mesh(armGeometry, armMaterial);
    this.rightArm.position.set(0.5, 0.7, 0);
    this.leftLeg = new THREE.Mesh(legGeometry, legMaterial);
    this.leftLeg.position.set(-0.25, -0.5, 0);
    this.rightLeg = new THREE.Mesh(legGeometry, legMaterial);
    this.rightLeg.position.set(0.25, -0.5, 0);
    this.mesh = new THREE.Group();
    this.mesh.add(this.body);
    this.mesh.add(this.head);
    this.mesh.add(this.leftArm);
    this.mesh.add(this.rightArm);
    this.mesh.add(this.leftLeg);
    this.mesh.add(this.rightLeg);
    this.mesh.position.set(x, 0, z);
    this.mesh.castShadow = true;
    this.mesh.receiveShadow = true;
    scene.add(this.mesh);
    const hugeGunGeometry = new THREE.BoxGeometry(0.2, 0.2, 1.5);
    const hugeGunMaterial = new THREE.MeshStandardMaterial({ color: 0x404040 });
    this.hugeGun = new THREE.Mesh(hugeGunGeometry, hugeGunMaterial);
    this.hugeGun.position.set(0.7, 0.6, 0.75);
    this.hugeGun.castShadow = true;
    this.hugeGun.receiveShadow = true;
    this.mesh.add(this.hugeGun);
    this.health = GUARD_HEALTH;
    this.attackTimer = 0;
    this.target = null;
  }

  update(deltaTime) {
    if (!gameActive || gameOver || winGame) return;
    const distanceToPlayer = this.mesh.position.distanceTo(playerPosition);
    if (!this.target || this.target.health <= 0 || this.mesh.position.distanceTo(this.target.mesh.position) > GUARD_ATTACK_RANGE) {
      this.target = this.findNearestZombie();
    }
    if (this.target) {
      const distanceToTarget = this.mesh.position.distanceTo(this.target.mesh.position);
      if (distanceToTarget < GUARD_ATTACK_RANGE) {
        this.mesh.lookAt(this.target.mesh.position.x, this.mesh.position.y, this.target.mesh.position.z);
        if (millis() - this.attackTimer > GUARD_ATTACK_COOLDOWN) {
          const direction = this.target.mesh.position.clone().sub(this.mesh.position).normalize();
          bullets.push(new Bullet(this.mesh.position.clone().add(new THREE.Vector3(0, 0.5, 0)), direction));
          this.attackTimer = millis();
        }
      } else {
        const direction = this.target.mesh.position.clone().sub(this.mesh.position).normalize();
        this.mesh.position.addScaledVector(direction, GUARD_SPEED);
        if (distanceToPlayer > GUARD_FOLLOW_DISTANCE) {
          const followDirection = playerPosition.clone().sub(this.mesh.position).normalize();
          this.mesh.position.addScaledVector(followDirection, GUARD_SPEED);
        }
      }
    } else {
      if (distanceToPlayer > GUARD_FOLLOW_DISTANCE) {
        const direction = playerPosition.clone().sub(this.mesh.position).normalize();
        this.mesh.position.addScaledVector(direction, GUARD_SPEED);
        this.mesh.lookAt(playerPosition.x, this.mesh.position.y, playerPosition.z);
      }
    }
  }

  takeDamage(damage) {
    if (godModeActive) return;
    this.health -= damage;
    if (this.health <= 0) {
      scene.remove(this.mesh);
      guards = guards.filter(g => g !== this);
      messageDiv.html("A guard has fallen! 😥");
      setTimeout(() => messageDiv.html(''), 3000);
    }
  }
}
Line-by-line explanation (35 lines)

🔧 Subcomponents:

method Constructor constructor(x, y, z) {

Builds a guard NPC with detailed geometry (body, head, arms, legs, gun) and initializes combat properties

initialization Geometry Assembly this.leftArm = new THREE.Mesh(armGeometry, armMaterial);

Creates individual body parts (arms, legs, head) positioned relative to the body

initialization Gun Attachment this.hugeGun = new THREE.Mesh(hugeGunGeometry, hugeGunMaterial);

Creates a large gun mesh and attaches it to the guard's body

conditional Target Acquisition if (!this.target || this.target.health <= 0 || this.mesh.position.distanceTo(this.target.mesh.position) > GUARD_ATTACK_RANGE)

Re-evaluates the guard's target if none exists, target is dead, or is out of range

conditional-sequence Attack Behavior if (distanceToTarget < GUARD_ATTACK_RANGE) { this.mesh.lookAt(this.target.mesh.position.x, this.mesh.position.y, this.target.mesh.position.z);

If target is in range, face it and shoot every GUARD_ATTACK_COOLDOWN milliseconds

conditional-sequence Chase Behavior } else { const direction = this.target.mesh.position.clone().sub(this.mesh.position).normalize();

If target is out of range, move toward it and also toward the player if too far away

conditional-sequence Idle Behavior } else { if (distanceToPlayer > GUARD_FOLLOW_DISTANCE) {

If no target found, follow the player if they stray too far

const bodyGeometry = new THREE.BoxGeometry(0.8, 1.8, 0.6);
Creates a taller (1.8 units) body box compared to zombies, representing a stronger guard
const headGeometry = new THREE.SphereGeometry(0.4, 16, 16);
Creates a spherical head (more humanlike than the zombie's cubic head)
this.leftArm.position.set(-0.5, 0.7, 0);
Positions the left arm 0.5 units to the left and 0.7 units up from the body center
this.mesh = new THREE.Group();
Creates a group to hold all body parts so they move and rotate together
this.mesh.add(this.body);
Adds the body to the group
this.mesh.add(this.head);
Adds the head to the group
this.mesh.add(this.leftArm);
Adds the left arm to the group
this.mesh.add(this.rightArm);
Adds the right arm to the group
this.mesh.add(this.leftLeg);
Adds the left leg to the group
this.mesh.add(this.rightLeg);
Adds the right leg to the group
this.mesh.position.set(x, 0, z);
Positions the guard at the given x, z coordinates on the ground (y=0)
this.hugeGun = new THREE.Mesh(hugeGunGeometry, hugeGunMaterial);
Creates a large gun geometry for the guard to hold
this.hugeGun.position.set(0.7, 0.6, 0.75);
Positions the gun in the guard's right hand area
this.mesh.add(this.hugeGun);
Attaches the gun to the guard so it moves with them
this.health = GUARD_HEALTH;
Initializes the guard with 50 health points (more than zombies' 30)
this.attackTimer = 0;
Initializes the attack cooldown timer to 0 (can attack immediately)
this.target = null;
Initializes with no target—will search for one in update()
const distanceToPlayer = this.mesh.position.distanceTo(playerPosition);
Calculates how far the guard is from the player
if (!this.target || this.target.health <= 0 || this.mesh.position.distanceTo(this.target.mesh.position) > GUARD_ATTACK_RANGE) {
Checks if the guard needs a new target because it has none, target is dead, or target is out of range
this.target = this.findNearestZombie();
Searches for the nearest zombie and sets it as the target
if (distanceToTarget < GUARD_ATTACK_RANGE) {
Checks if the target is within 10 units (the attack range)
this.mesh.lookAt(this.target.mesh.position.x, this.mesh.position.y, this.target.mesh.position.z);
Rotates the guard to face the target
if (millis() - this.attackTimer > GUARD_ATTACK_COOLDOWN) {
Checks if at least 500 milliseconds have passed since the last attack
const direction = this.target.mesh.position.clone().sub(this.mesh.position).normalize();
Calculates a unit vector pointing from the guard to the target
bullets.push(new Bullet(this.mesh.position.clone().add(new THREE.Vector3(0, 0.5, 0)), direction));
Creates a new bullet at the guard's position (raised 0.5 units to gun height) pointing toward the target
} else {
Target exists but is out of range
this.mesh.position.addScaledVector(direction, GUARD_SPEED);
Moves the guard 0.05 units per frame toward the target (faster than zombies' 0.02)
if (distanceToPlayer > GUARD_FOLLOW_DISTANCE) {
Checks if the guard has strayed more than 5 units from the player
const followDirection = playerPosition.clone().sub(this.mesh.position).normalize();
Calculates a direction back toward the player
this.mesh.position.addScaledVector(followDirection, GUARD_SPEED);
Moves the guard toward the player to stay within the follow distance
if (godModeActive) return;
In god mode, guards are invincible and take no damage
this.health -= damage;
Subtracts damage from the guard's health
if (this.health <= 0) {
Checks if the guard's health reaches 0
scene.remove(this.mesh);
Removes the guard's mesh from the 3D scene
guards = guards.filter(g => g !== this);
Removes the guard from the guards array by keeping only guards that are not this one

checkCollisions()

Collision detection is crucial for any game. This function uses two separate collision systems: distance-based for bullets (simple and fast—just check if objects are close), and box-sphere intersection for the player and walls (more complex but necessary for solid geometry). The bullet loop iterates backward and breaks early to avoid processing the same bullet multiple times. The wall collision uses Three.js's built-in box-sphere intersection test and clamps the player away from walls.

🔬 Notice we loop through bullets and zombies backward (i--, j--). Why backward instead of forward? What would happen if you changed 'i >= 0' to 'i < bullets.length'? (Hint: think about what happens when you remove an element from an array and then try to access the next index)

  for (let i = bullets.length - 1; i >= 0; i--) {
    const bullet = bullets[i];
    for (let j = zombies.length - 1; j >= 0; j--) {
      const zombie = zombies[j];
      const distance = bullet.mesh.position.distanceTo(zombie.mesh.position);
      if (distance < 1) {
        zombie.takeDamage(BULLET_DAMAGE);
        scene.remove(bullet.mesh);
        bullets.splice(i, 1);
        break;
function checkCollisions() {
  if (!gameActive || gameOver || winGame) return;
  for (let i = bullets.length - 1; i >= 0; i--) {
    const bullet = bullets[i];
    for (let j = zombies.length - 1; j >= 0; j--) {
      const zombie = zombies[j];
      const distance = bullet.mesh.position.distanceTo(zombie.mesh.position);
      if (distance < 1) {
        zombie.takeDamage(BULLET_DAMAGE);
        scene.remove(bullet.mesh);
        bullets.splice(i, 1);
        break;
      }
    }
  }
  const playerSphere = new THREE.Sphere(playerPosition, 0.5);
  for (const collisionObject of houseCollisionObjects) {
    if (collisionObject.box.intersectsSphere(playerSphere)) {
      const closestPoint = collisionObject.box.clampPoint(playerPosition, new THREE.Vector3());
      const direction = playerPosition.clone().sub(closestPoint).normalize();
      playerPosition.copy(closestPoint.addScaledVector(direction, 0.5));
    }
  }
}
Line-by-line explanation (17 lines)

🔧 Subcomponents:

nested-loop Bullet-Zombie Collision Detection for (let i = bullets.length - 1; i >= 0; i--) { for (let j = zombies.length - 1; j >= 0; j--)

Nested loops check every bullet against every zombie to detect hits

conditional Distance Check const distance = bullet.mesh.position.distanceTo(zombie.mesh.position); if (distance < 1)

Checks if the bullet and zombie are close enough (1 unit) to register a hit

sequence Damage and Removal zombie.takeDamage(BULLET_DAMAGE); scene.remove(bullet.mesh); bullets.splice(i, 1);

Damages the zombie, removes the bullet from the scene, and removes it from the bullets array

loop Player-Wall Collision Detection for (const collisionObject of houseCollisionObjects)

Loops through all house walls and barriers to check if the player is inside them

calculation Sphere-Box Intersection Test if (collisionObject.box.intersectsSphere(playerSphere))

Tests if the player's collision sphere overlaps with a wall's bounding box

calculation Collision Response const closestPoint = collisionObject.box.clampPoint(playerPosition, new THREE.Vector3());

Finds the closest point on the wall to the player and pushes the player out

if (!gameActive || gameOver || winGame) return;
Exits early if the game is not active—no collisions to check if paused or finished
for (let i = bullets.length - 1; i >= 0; i--) {
Loops through bullets backward (important for safe removal during iteration)
const bullet = bullets[i];
Gets the current bullet
for (let j = zombies.length - 1; j >= 0; j--) {
Inner loop: checks the current bullet against every zombie
const zombie = zombies[j];
Gets the current zombie
const distance = bullet.mesh.position.distanceTo(zombie.mesh.position);
Calculates the distance between the bullet and zombie centers
if (distance < 1) {
Checks if the distance is less than 1 unit—if so, they've collided
zombie.takeDamage(BULLET_DAMAGE);
Calls the zombie's takeDamage() method, subtracting 10 health from it
scene.remove(bullet.mesh);
Removes the bullet mesh from the 3D scene so it's no longer visible
bullets.splice(i, 1);
Removes the bullet from the bullets array at index i
break;
Exits the inner zombie loop early because this bullet has already hit a zombie and is being removed
const playerSphere = new THREE.Sphere(playerPosition, 0.5);
Creates a sphere centered at the player's position with radius 0.5—this is the player's collision volume
for (const collisionObject of houseCollisionObjects) {
Loops through all collision objects (walls, barriers, furniture, roof)
if (collisionObject.box.intersectsSphere(playerSphere)) {
Tests if the player's sphere overlaps with a collision box (Three.js built-in method)
const closestPoint = collisionObject.box.clampPoint(playerPosition, new THREE.Vector3());
Finds the closest point on the box surface to the player; if the player is outside, this is on the surface; if inside, this is clamped to the box
const direction = playerPosition.clone().sub(closestPoint).normalize();
Calculates a unit vector pointing from the closest point outward—this is the direction to push the player
playerPosition.copy(closestPoint.addScaledVector(direction, 0.5));
Moves the player 0.5 units away from the wall along the direction vector, pushing them out of the collision

onMouseDown()

onMouseDown() is the shooting system. When the player left-clicks, this function fires, consuming ammo, creating a bullet in the direction the player is looking, and ejecting a shell casing with realistic physics. The god mode check allows infinite ammo and prevents ammo depletion. The precondition checks prevent exploits like shooting before the game starts or after winning.

function onMouseDown(event) {
  if (!gameActive || gameOver || winGame || !controls.isLocked) return;
  if (isNight && gunEquipped && (playerAmmo > 0 || godModeActive)) {
    if (!godModeActive) {
      playerAmmo--;
    }
    const direction = new THREE.Vector3();
    camera.getWorldDirection(direction);
    const gunWorldPosition = new THREE.Vector3();
    gunMesh.getWorldPosition(gunWorldPosition);
    bullets.push(new Bullet(gunWorldPosition, direction));
    const ejectDirection = new THREE.Vector3(
      random(0.05, 0.1),
      random(0.2, 0.3),
      random(-0.05, 0.05)
    );
    ejectDirection.applyQuaternion(camera.quaternion);
    casings.push(new Casing(gunWorldPosition, ejectDirection));
  } else if (isNight && gunEquipped && playerAmmo <= 0 && !godModeActive) {
    messageDiv.html('Out of ammo! Press R to reload.');
    setTimeout(() => messageDiv.html(''), 2000);
  }
}
Line-by-line explanation (18 lines)

🔧 Subcomponents:

conditional Precondition Checks if (!gameActive || gameOver || winGame || !controls.isLocked) return;

Exits if game is inactive, over, won, or pointer is not locked—prevents unintended shooting

conditional Fire Condition if (isNight && gunEquipped && (playerAmmo > 0 || godModeActive))

Only allows shooting during night, when gun is equipped, and either ammo exists or god mode is on

conditional Ammo Consumption if (!godModeActive) { playerAmmo--; }

Decreases ammo by 1 unless in god mode (infinite ammo)

sequence Bullet Creation const direction = new THREE.Vector3(); camera.getWorldDirection(direction);

Gets the camera's view direction to fire bullets forward from where the player is looking

calculation Casing Ejection const ejectDirection = new THREE.Vector3(random(0.05, 0.1), random(0.2, 0.3), random(-0.05, 0.05));

Creates a random direction for the ejected shell casing to fly out

conditional Out of Ammo Message else if (isNight && gunEquipped && playerAmmo <= 0 && !godModeActive)

Shows a message if the player tries to shoot without ammo

if (!gameActive || gameOver || winGame || !controls.isLocked) return;
Prevents shooting if: game is not running, player died, player won, or mouse is not captured—returns early to skip the rest of the function
if (isNight && gunEquipped && (playerAmmo > 0 || godModeActive)) {
Checks three conditions to allow shooting: (1) it must be night, (2) gun must be equipped, (3) either ammo exists OR god mode is on
if (!godModeActive) {
Checks if god mode is NOT active
playerAmmo--;
Decreases ammo by 1; only happens if god mode is off
const direction = new THREE.Vector3();
Creates an empty 3D vector
camera.getWorldDirection(direction);
Fills the direction vector with the camera's forward direction (where the player is looking)
const gunWorldPosition = new THREE.Vector3();
Creates an empty vector to hold the gun's world position
gunMesh.getWorldPosition(gunWorldPosition);
Gets the gun's position in world space (accounting for its offset from the camera)
bullets.push(new Bullet(gunWorldPosition, direction));
Creates a new Bullet object starting at the gun position and traveling in the camera's direction, then adds it to the bullets array
const ejectDirection = new THREE.Vector3(
Starts creating a random direction for the ejected shell casing
random(0.05, 0.1),
Random x component between 0.05 and 0.1—shells eject slightly to the right
random(0.2, 0.3),
Random y component between 0.2 and 0.3—shells eject upward
random(-0.05, 0.05)
Random z component between -0.05 and 0.05—shells eject slightly forward or backward
ejectDirection.applyQuaternion(camera.quaternion);
Rotates the eject direction by the camera's rotation so shells eject in world-space (not camera-space)
casings.push(new Casing(gunWorldPosition, ejectDirection));
Creates a new Casing object starting at the gun position with the calculated eject direction, then adds it to the casings array
} else if (isNight && gunEquipped && playerAmmo <= 0 && !godModeActive) {
If the player tries to shoot during night with gun equipped but no ammo and not in god mode, show a message
messageDiv.html('Out of ammo! Press R to reload.');
Displays a helpful message telling the player to reload
setTimeout(() => messageDiv.html(''), 2000);
Clears the message after 2 seconds

updateHunger()

updateHunger() implements a resource management mechanic that forces the player to explore and forage during the day. Hunger drains passively, and when it hits 0, the player begins starving to death. This creates urgency and strategy—the player must balance combat at night with harvesting fruit during the day. God mode disables hunger to test other systems.

🔬 This nested block handles starvation—when hunger is 0, health drains and the game ends. What happens if you change the condition to 'if (playerHunger <= 50)' instead of '<= 0'? Players would start taking damage at 50% hunger instead of 0%—making the game harder!

  if (playerHunger <= 0) {
    playerHealth -= HUNGER_DAMAGE_RATE * deltaTime;
    messageDiv.html('You are starving! Find fruit quickly! 🍎');
    if (playerHealth <= 0) {
      playerHealth = 0;
      gameOver = true;
      messageDiv.html('GAME OVER! You starved to death. 💀');
      noLoop();
    }
  }
function updateHunger(deltaTime) {
  if (!gameActive || gameOver || winGame || godModeActive) return;
  playerHunger -= HUNGER_DECREASE_RATE * deltaTime;
  playerHunger = constrain(playerHunger, 0, 100);
  if (playerHunger <= 0) {
    playerHealth -= HUNGER_DAMAGE_RATE * deltaTime;
    messageDiv.html('You are starving! Find fruit quickly! 🍎');
    if (playerHealth <= 0) {
      playerHealth = 0;
      gameOver = true;
      messageDiv.html('GAME OVER! You starved to death. 💀');
      noLoop();
    }
  }
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

calculation Hunger Decrement playerHunger -= HUNGER_DECREASE_RATE * deltaTime;

Decreases hunger over time at a rate of 0.05 per second

calculation Hunger Clamp playerHunger = constrain(playerHunger, 0, 100);

Ensures hunger stays between 0 (starving) and 100 (full)

conditional Starvation Damage if (playerHunger <= 0) { playerHealth -= HUNGER_DAMAGE_RATE * deltaTime;

If hunger is 0, the player takes damage at a rate of 2 health per second

conditional Death Check if (playerHealth <= 0)

Ends the game if the player dies from starvation

if (!gameActive || gameOver || winGame || godModeActive) return;
Exits early if game is not active, already over/won, or in god mode—in god mode, hunger doesn't drain
playerHunger -= HUNGER_DECREASE_RATE * deltaTime;
Decreases hunger by 0.05 points per second (HUNGER_DECREASE_RATE=0.05, deltaTime is seconds since last frame)
playerHunger = constrain(playerHunger, 0, 100);
Clamps hunger to stay between 0 (minimum) and 100 (maximum) using p5.js's constrain() function
if (playerHunger <= 0) {
Checks if hunger has reached 0—if so, the player begins starving
playerHealth -= HUNGER_DAMAGE_RATE * deltaTime;
When starving, the player loses 2 health points per second (HUNGER_DAMAGE_RATE=2)
messageDiv.html('You are starving! Find fruit quickly! 🍎');
Displays a warning message to urgently inform the player they need to eat
if (playerHealth <= 0) {
Checks if the player's health has reached 0
playerHealth = 0;
Clamps health to 0 (ensures it doesn't go negative)
gameOver = true;
Sets the game over flag, which will be checked in draw() to stop the game loop
messageDiv.html('GAME OVER! You starved to death. 💀');
Displays the game over message explaining the cause of death
noLoop();
Stops the p5.js draw loop, halting all game updates and rendering

createPalmTrees()

createPalmTrees() procedurally generates trees with visual variety (random heights) and game logic (harvest cooldown tracking). Each tree is a group of meshes (trunk, leaves, fruit) positioned together. The do-while loop enforces spatial constraints—trees avoid spawning on top of the house. Custom properties (hasFruit, harvestCooldown) are stored directly on the Three.js objects, merging game logic and graphics elegantly.

🔬 This do-while loop prevents trees from spawning near the center (within 5 units of the house). What happens if you change '<' to '>'? Trees would spawn ONLY in the center, away from the edges!

    do {
      x = random(-18, 18);
      z = random(-18, 18);
    } while (Math.abs(x) < 5 && Math.abs(z) < 5);
function createPalmTrees(count) {
  const trunkMaterial = new THREE.MeshStandardMaterial({ color: 0x8b4513 });
  const leavesMaterial = new THREE.MeshStandardMaterial({ color: 0x228b22 });
  for (let i = 0; i < count; i++) {
    const treeGroup = new THREE.Group();
    const trunkHeight = random(3, 5);
    const trunkGeometry = new THREE.CylinderGeometry(0.2, 0.3, trunkHeight, 8);
    const trunkMesh = new THREE.Mesh(trunkGeometry, trunkMaterial);
    trunkMesh.position.y = trunkHeight / 2;
    trunkMesh.castShadow = true;
    trunkMesh.receiveShadow = true;
    treeGroup.add(trunkMesh);
    const leavesGeometry = new THREE.ConeGeometry(2, 2, 8);
    const leavesMesh = new THREE.Mesh(leavesGeometry, leavesMaterial);
    leavesMesh.position.y = trunkHeight + 1;
    leavesMesh.castShadow = true;
    leavesMesh.receiveShadow = true;
    treeGroup.add(leavesMesh);
    const fruitGeometry = new THREE.SphereGeometry(0.3, 16, 16);
    const fruitMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000 });
    const fruitMesh = new THREE.Mesh(fruitGeometry, fruitMaterial);
    fruitMesh.position.y = trunkHeight + 1.5;
    fruitMesh.castShadow = true;
    fruitMesh.receiveShadow = true;
    treeGroup.add(fruitMesh);
    treeGroup.fruitMesh = fruitMesh;
    let x, z;
    do {
      x = random(-18, 18);
      z = random(-18, 18);
    } while (Math.abs(x) < 5 && Math.abs(z) < 5);
    treeGroup.position.set(x, 0, z);
    treeGroup.hasFruit = true;
    treeGroup.harvestCooldown = 0;
    palmTrees.push(treeGroup);
    scene.add(treeGroup);
  }
}
Line-by-line explanation (30 lines)

🔧 Subcomponents:

initialization Material Setup const trunkMaterial = new THREE.MeshStandardMaterial({ color: 0x8b4513 });

Defines brown material for tree trunks

for-loop Tree Creation Loop for (let i = 0; i < count; i++)

Repeats tree creation 'count' times (usually 10)

sequence Trunk Creation const trunkHeight = random(3, 5); const trunkGeometry = new THREE.CylinderGeometry(0.2, 0.3, trunkHeight, 8);

Creates a cylinder with random height (3-5 units) for the trunk

sequence Leaves Creation const leavesGeometry = new THREE.ConeGeometry(2, 2, 8);

Creates a cone shape for the foliage

sequence Fruit Creation const fruitGeometry = new THREE.SphereGeometry(0.3, 16, 16);

Creates a red sphere for the harvestable fruit

do-while-loop Position Validation do { x = random(-18, 18); z = random(-18, 18); } while (Math.abs(x) < 5 && Math.abs(z) < 5);

Ensures trees are placed away from the center of the map (where the house is)

initialization Tree Properties treeGroup.hasFruit = true; treeGroup.harvestCooldown = 0;

Initializes game logic properties on the tree: fruit availability and harvest cooldown

const trunkMaterial = new THREE.MeshStandardMaterial({ color: 0x8b4513 });
Creates a brown material (RGB: 139, 69, 19) for all tree trunks
const leavesMaterial = new THREE.MeshStandardMaterial({ color: 0x228b22 });
Creates a forest green material (RGB: 34, 139, 34) for all tree foliage
for (let i = 0; i < count; i++) {
Loops from 0 to count-1 (usually 0 to 9) to create 'count' trees
const treeGroup = new THREE.Group();
Creates a group to hold the trunk, leaves, and fruit so they move and render together
const trunkHeight = random(3, 5);
Randomly picks a trunk height between 3 and 5 units to vary tree sizes
const trunkGeometry = new THREE.CylinderGeometry(0.2, 0.3, trunkHeight, 8);
Creates a cylinder: radius 0.2 at top, 0.3 at bottom (slightly wider at base), height = trunkHeight, 8-sided (not round, saves performance)
const trunkMesh = new THREE.Mesh(trunkGeometry, trunkMaterial);
Combines the cylinder geometry with the brown material to create the trunk mesh
trunkMesh.position.y = trunkHeight / 2;
Positions the trunk so its bottom is at y=0 and it extends upward (cylinders are centered on y-axis by default)
trunkMesh.castShadow = true;
Enables shadow casting for the trunk—it will cast a shadow on the ground and walls
trunkMesh.receiveShadow = true;
Enables shadow receiving for the trunk—shadows from other objects will appear on it
treeGroup.add(trunkMesh);
Adds the trunk mesh to the group
const leavesGeometry = new THREE.ConeGeometry(2, 2, 8);
Creates a cone: base radius 2, height 2, 8-sided
const leavesMesh = new THREE.Mesh(leavesGeometry, leavesMaterial);
Combines the cone with the green material to create the foliage
leavesMesh.position.y = trunkHeight + 1;
Positions the cone at the top of the trunk, 1 unit above the trunk's top
treeGroup.add(leavesMesh);
Adds the leaves mesh to the group
const fruitGeometry = new THREE.SphereGeometry(0.3, 16, 16);
Creates a sphere: radius 0.3, 16 width segments, 16 height segments (detailed for a small object)
const fruitMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000 });
Creates a bright red material (RGB: 255, 0, 0) for the fruit
const fruitMesh = new THREE.Mesh(fruitGeometry, fruitMaterial);
Combines the sphere with the red material to create the fruit
fruitMesh.position.y = trunkHeight + 1.5;
Positions the fruit 1.5 units above the top of the trunk, so it hangs from the leaves
treeGroup.fruitMesh = fruitMesh;
Stores a reference to the fruit mesh on the group so we can toggle its visibility when harvested
let x, z;
Declares variables for the tree's x and z position
do {
Starts a do-while loop—always runs at least once
x = random(-18, 18);
Randomly picks an x position between -18 and 18
z = random(-18, 18);
Randomly picks a z position between -18 and 18
} while (Math.abs(x) < 5 && Math.abs(z) < 5);
Repeats the random selection if both |x| and |z| are less than 5—this prevents trees from spawning too close to the house (at the center)
treeGroup.position.set(x, 0, z);
Positions the entire tree group at the randomly selected location on the ground
treeGroup.hasFruit = true;
Initializes a custom property: the tree has fruit available
treeGroup.harvestCooldown = 0;
Initializes a custom property: the harvest cooldown is 0, so the fruit can be picked immediately
palmTrees.push(treeGroup);
Adds the tree group to the global palmTrees array so it can be tracked for interaction and respawning
scene.add(treeGroup);
Adds the tree to the 3D scene so it renders

📦 Key Variables

scene THREE.Scene

The 3D scene container that holds all game objects, lights, and the camera—the main world the game renders

scene = new THREE.Scene();
camera THREE.PerspectiveCamera

The player's viewpoint—defines what the player sees and tracks their position/rotation

camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
renderer THREE.WebGLRenderer

The WebGL renderer that draws the 3D scene to the HTML canvas each frame

renderer = new THREE.WebGLRenderer({ antialias: true });
controls THREE.PointerLockControls

Manages first-person camera movement via mouse input—captures the pointer and rotates the camera based on mouse movement

controls = new THREE.PointerLockControls(camera, renderer.domElement);
playerHealth number

Tracks player health (0-100)—decreases when hit by zombies or starving, game ends at 0

let playerHealth = 100;
playerHunger number

Tracks player hunger level (0-100)—decreases over time, requires eating fruit, damages health when 0

let playerHunger = 100;
playerAmmo number

Count of available bullets—decreases by 1 per shot, resets to 10 each night, unlimited in god mode

let playerAmmo = 10;
playerPosition THREE.Vector3

Stores the player's 3D world position—updated by movement input and collisions, synced to camera

let playerPosition = new THREE.Vector3(0, 1.7, 0);
zombies array

Array holding all active Zombie objects—updated every frame, cleared at dawn

let zombies = [];
guards array

Array holding all hired Guard objects—each can fight zombies independently

let guards = [];
bullets array

Array holding all active Bullet objects from player and guards—removed on collision or age

let bullets = [];
isNight boolean

Flag indicating if it's currently nighttime—true triggers zombie spawning and gun usage

let isNight = false;
nightsSurvived number

Counter of completed nights—incremented at dawn, player wins when reaching 7

let nightsSurvived = 0;
gameActive boolean

Flag indicating if the game is currently running—set to false when paused or ended

let gameActive = false;
gameOver boolean

Flag indicating if the player has lost—set to true on death or starvation

let gameOver = false;
godModeActive boolean

Cheat flag making guards invincible, player has infinite ammo, hunger doesn't drain—activated by 6+7+8+9 cheat code

let godModeActive = false;
dayLength number

Duration of daytime in milliseconds (60000 = 1 minute)—controls pacing of day-night cycle

const dayLength = 60 * 1000;
nightLength number

Duration of nighttime in milliseconds (300000 = 5 minutes)—longer nights spawn more zombies

const nightLength = 300 * 1000;
ZOMBIE_SPEED number

How far zombies move per frame toward their target—0.02 units, slower than guards

const ZOMBIE_SPEED = 0.02;
ZOMBIE_HEALTH number

How many hit points each zombie has—30, dies after 3 shots with 10-damage bullets

const ZOMBIE_HEALTH = 30;
GUARD_HEALTH number

How many hit points each guard has—50, more durable than zombies

const GUARD_HEALTH = 50;
MAX_NIGHTS number

Number of nights the player must survive to win—7

const MAX_NIGHTS = 7;
moveForward boolean

Set to true when W key is pressed, false when released—controls forward movement

let moveForward = false;
moveBackward boolean

Set to true when S key is pressed, false when released—controls backward movement

let moveBackward = false;
moveLeft boolean

Set to true when A key is pressed, false when released—controls left strafe

let moveLeft = false;
moveRight boolean

Set to true when D key is pressed, false when released—controls right strafe

let moveRight = false;
canJump boolean

Flag indicating if the player can jump—true only when feet are on ground

let canJump = false;
palmTrees array

Array of all palm tree groups on the island—tracks fruit availability and respawn timers

let palmTrees = [];

🔧 Potential Improvements (10)

Here are some ways this code could be enhanced:

BUG checkCollisions() player-wall collision

The collision response can cause jitter or tunneling if the player moves very fast and overlaps a wall deeply—clampPoint() assumes the player is barely overlapping, but if velocity is high, they can be deep inside and the correction might not be enough

💡 Add a more robust collision solver: check collision BEFORE updating position, or use a sweeping sphere test to predict collisions and prevent overlap

PERFORMANCE checkCollisions() bullet-zombie nested loop

Every frame, every bullet checks distance to every zombie—with many zombies and guards firing, this becomes O(n*m) complexity and can slow the game

💡 Use spatial partitioning (grid, quadtree, or octree) to only check nearby objects, or implement a broad-phase collision system that filters impossible collisions before distance checks

BUG updatePlayerMovement() cheat code activation

The cheat code cooldown uses a single timer (lastCheatTime) for all cheats, so activating the 6+7 guard cheat prevents 6+7+8+9 god mode cheat from being used immediately after

💡 Use separate cooldown timers (lastGuardCheatTime, lastGodModeCheatTime) so each cheat has its own cooldown period

FEATURE Guard class

Guards have no patrolling or idle behavior—when there are no zombies, they stand still or follow the player monotonously

💡 Add a patrol behavior: guards could wander within a certain radius of the player, or defend specific zones, making them feel more alive and strategic

STYLE Zombie and Guard class constructors

Both classes have duplicate code for creating geometry, materials, and positioning parts—this violates DRY principle

💡 Extract shared geometry and material creation into a helper function or parent class to reduce duplication and make changes easier

FEATURE Shooting system (onMouseDown)

No weapon feedback—bullets make no sound and casings have no visual impact when they land

💡 Add a visual particle effect when bullets hit zombies, and implement sound effects (gunshot, casing impact) using p5.sound or Web Audio API

BUG updateDayNightCycle()

Fruit respawn times vary randomly between FRUIT_RESPAWN_TIME and MAX_FRUIT_RESPAWN_TIME, but if a tree is never interacted with after being harvested, its fruit may never respawn during gameplay

💡 Add a daily reset that automatically restores all fruit at dawn (in the 'isNight to day transition' block) to ensure fruit is always available

PERFORMANCE createPalmTrees() do-while loop

The do-while loop can theoretically infinite-loop if the random position is always in the excluded zone, though probability is low with -18 to 18 range and <5 exclusion

💡 Add a safety counter: if (failCount > 100) break, and place the tree anyway—prevents rare infinite loops

FEATURE General game design

No victory celebration or end-game screen—winning feels anticlimactic compared to dying

💡 When the player wins, display a detailed victory screen with stats (nights survived, zombies killed, time played), and offer a restart button

STYLE Global variables section

Many game constants are declared individually—ZOMBIE_SPEED, ZOMBIE_HEALTH, etc.—cluttering the top of the file

💡 Group constants into objects by category (const ZOMBIE_CONFIG = { speed: 0.02, health: 30, ... }) to improve readability and make balancing easier

🔄 Code Flow

Code flow showing setup, draw, updateplayermovement, updatedaynightcycle, zombie, guard, checkcollisions, onmousedown, updatehunger, createpalmrtrees

💡 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" draw --> delta-time-calc[Delta Time Calculation] click delta-time-calc href "#sub-delta-time-calc" delta-time-calc --> lock-check[Pointer Lock Check] click lock-check href "#sub-lock-check" lock-check --> game-over-check[Game Over Check] click game-over-check href "#sub-game-over-check" game-over-check --> update-cycle[Game Update Sequence] click update-cycle href "#sub-update-cycle" update-cycle --> updateplayermovement[updatePlayerMovement] click updateplayermovement href "#fn-updateplayermovement" updateplayermovement --> movement-input[Movement Input Processing] click movement-input href "#sub-movement-input" movement-input --> gravity-apply[Gravity Application] click gravity-apply href "#sub-gravity-apply" gravity-apply --> rotation-transform[Camera Rotation Transform] click rotation-transform href "#sub-rotation-transform" rotation-transform --> ground-collision[Ground Collision] click ground-collision href "#sub-ground-collision" ground-collision --> camera-sync[Camera Sync] click camera-sync href "#sub-camera-sync" camera-sync --> updateplayermovement update-cycle --> updatedaynightcycle[updateDayNightCycle] click updatedaynightcycle href "#fn-updatedaynightcycle" updatedaynightcycle --> cycle-position[Cycle Position Calculation] click cycle-position href "#sub-cycle-position" cycle-position --> day-check[Day Phase Check] click day-check href "#sub-day-check" day-check -->|Day| day-lighting[Day Lighting Update] click day-lighting href "#sub-day-lighting" day-lighting --> day-sky[Day Sky Color] click day-sky href "#sub-day-sky" day-sky --> night-transition-alt[Night to Day Transition] click night-transition-alt href "#sub-night-transition-alt" night-transition-alt --> update-cycle day-check -->|Night| night-lighting[Night Lighting Update] click night-lighting href "#sub-night-lighting" night-lighting --> zombie-spawn[Zombie Spawn Check] click zombie-spawn href "#sub-zombie-spawn" zombie-spawn --> update-cycle update-cycle --> checkcollisions[checkCollisions] click checkcollisions href "#fn-checkcollisions" checkcollisions --> collision-checks[Collision Detection] click collision-checks href "#sub-collision-checks" collision-checks --> rendering[Three.js Rendering] click rendering href "#sub-rendering" rendering --> draw draw --> onmousedown[onMouseDown] click onmousedown href "#fn-onmousedown" onmousedown --> precondition-check[Precondition Checks] click precondition-check href "#sub-precondition-check" precondition-check --> fire-condition[Fire Condition] click fire-condition href "#sub-fire-condition" fire-condition -->|True| ammo-consumption[Ammo Consumption] click ammo-consumption href "#sub-ammo-consumption" ammo-consumption --> bullet-creation[Bullet Creation] click bullet-creation href "#sub-bullet-creation" bullet-creation --> casing-ejection[Casing Ejection] click casing-ejection href "#sub-casing-ejection" casing-ejection --> draw fire-condition -->|False| no-ammo-message[Out of Ammo Message] click no-ammo-message href "#sub-no-ammo-message" draw --> updatehunger[updateHunger] click updatehunger href "#fn-updatehunger" updatehunger --> hunger-decrement[Hunger Decrement] click hunger-decrement href "#sub-hunger-decrement" hunger-decrement --> hunger-clamp[Hunger Clamp] click hunger-clamp href "#sub-hunger-clamp" hunger-clamp --> starvation-damage[Starvation Damage] click starvation-damage href "#sub-starvation-damage" starvation-damage --> death-check[Death Check] click death-check href "#sub-death-check" death-check --> draw draw --> createpalmrtrees[createPalmTrees] click createpalmrtrees href "#fn-createpalmrtrees" createpalmrtrees --> material-setup[Material Setup] click material-setup href "#sub-material-setup" material-setup --> loop-trees[Tree Creation Loop] click loop-trees href "#sub-loop-trees" loop-trees --> trunk-creation[Trunk Creation] click trunk-creation href "#sub-trunk-creation" trunk-creation --> leaves-creation[Leaves Creation] click leaves-creation href "#sub-leaves-creation" leaves-creation --> fruit-creation[Fruit Creation] click fruit-creation href "#sub-fruit-creation" fruit-creation --> position-validation[Position Validation] click position-validation href "#sub-position-validation" position-validation --> properties-init[Tree Properties] click properties-init href "#sub-properties-init" properties-init --> createpalmrtrees draw --> guard[guard] click guard href "#fn-guard" guard --> constructor-guard[Constructor] click constructor-guard href "#sub-constructor-guard" constructor-guard --> geometry-assembly[Geometry Assembly] click geometry-assembly href "#sub-geometry-assembly" geometry-assembly --> gun-attachment[Gun Attachment] click gun-attachment href "#sub-gun-attachment" gun-attachment --> target-acquisition[Target Acquisition] click target-acquisition href "#sub-target-acquisition" target-acquisition --> attack-behavior[Attack Behavior] click attack-behavior href "#sub-attack-behavior" attack-behavior --> chase-behavior[Chase Behavior] click chase-behavior href "#sub-chase-behavior" chase-behavior --> idle-behavior[Idle Behavior] click idle-behavior href "#sub-idle-behavior" idle-behavior --> guard draw --> zombie[zombie] click zombie href "#fn-zombie" zombie --> constructor[Constructor] click constructor href "#sub-constructor" constructor --> geometry-creation[Geometry Creation] click geometry-creation href "#sub-geometry-creation" geometry-creation --> mesh-assembly[Mesh Assembly] click mesh-assembly href "#sub-mesh-assembly" mesh-assembly --> target-finding[Target Selection] click target-finding href "#sub-target-finding" target-finding --> movement[Movement Toward Target] click movement href "#sub-movement" movement --> attack-logic[Attack Logic] click attack-logic href "#sub-attack-logic" attack-logic --> death-detection[Death Detection] click death-detection href "#sub-death-detection" death-detection --> zombie

❓ Frequently Asked Questions

What visual experience does '7 painful nights at corbuns zombie island' provide?

The sketch creates a low-poly 3D island environment where players can explore, featuring detailed elements like trees, houses, and animated zombie characters.

How can players interact with the game on zombie island?

Players can walk around the island, scavenge for fruit, shoot zombies with a gun, manage their health and hunger, and recruit guards for defense.

What creative coding techniques are showcased in this p5.js sketch?

The sketch demonstrates the use of real-time game mechanics, including day-night cycles, player state management, and dynamic enemy spawning.

Preview

7 painful nights at corbuns zombie island - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of 7 painful nights at corbuns zombie island - Code flow showing setup, draw, updateplayermovement, updatedaynightcycle, zombie, guard, checkcollisions, onmousedown, updatehunger, createpalmrtrees
Code Flow Diagram