Wings Slayer (Lucas)

Wings Slayer is a 3D first-person shooting game where you hunt flying birds across multiple themed worlds, earn points to unlock weapons and new bird targets, and eventually face off against powerful bosses. The game combines fast-paced action with a progression system and environmental variety.

🧪 Try This!

Experiment with the code by making these changes:

  1. Change moveSpeed to run faster — A higher moveSpeed makes player movement snappier and more responsive—try 12 to zoom around the environment
  2. Jump higher with more jump force — Increase jumpForce to launch higher into the air, making platforming more dynamic
  3. Make gravity heavier for arcade feel — Higher gravity pulls the player down faster, making jumps feel more snappy and arcade-like
  4. Spawn more ducks for chaos
  5. Lower the first boss threshold — A lower threshold means bosses spawn earlier, making the game more challenging sooner
  6. Give the player more health — Increase max health to survive longer against bosses and give more room for error
Prefer the full editor? Open it there →

📖 About This Sketch

Wings Slayer is a fully-featured 3D shooting game built in p5.js that challenges you to hunt ducks, pigeons, and mythical creatures across ten distinct worlds—each with its own visual theme, environmental structures, and a unique boss encounter. The gameplay combines first-person controls, ray-casting collision detection, a shop system with unlockable weapons and bird targets, and progressive boss battles that grow more challenging as your score climbs. Visually, the game uses 3D transforms, procedurally generated environments, ambient and directional lighting, and particle effects to create immersive scenes.

The code is organized around several key systems: a player object that tracks position and velocity with gravity-based jumping; a draw loop that updates the 3D camera, environment, enemies (ducks or bosses), and the on-screen gun; a shop system that tracks unlocked items and manages world switching; and a Boss class that handles health, movement, and attacks. By studying this sketch, you'll learn how to build a complete 3D game loop, implement raycasting for hit detection, manage complex game state (score, health, active world, equipment), and coordinate multiple 3D objects with lighting and effects.

⚙️ How It Works

  1. When the sketch loads, setup() creates a fullscreen WEBGL canvas, initializes the player at ground level, generates trees and structures for the default forest world, spawns 15 ducks as initial targets, and sets up audio synths for gunshots and background music.
  2. Every frame, the draw() function clears the background with world-specific colors and lighting, updates the player's vertical position based on gravity (allowing jumping), reads keyboard input to move the player in the direction they're facing, and calculates the camera view from the player's eyes using yaw (horizontal rotation) and pitch (vertical rotation) angles controlled by mouse movement.
  3. The environment is rendered by drawing a ground plane colored and lit to match the active world, then drawing 200+ procedurally-positioned trees or structures unique to each biome (cacti in desert, ice pines in arctic, coral underwater, etc.).
  4. Gameplay centers on ducks: 15 targets spawn and fly randomly in 3D space. When you click to fire, fireGun() casts a ray in the direction you're looking and uses dot-product math to check if the ray passes within a radius of any duck. If a hit is detected, the duck dies, particles explode outward, and you gain points to unlock new weapons or worlds.
  5. When your score reaches a threshold (starting at 2,500), a boss spawns instead of ducks. Bosses are complex 3D models (treants, scorpions, yetis, etc.) that hover and slowly drift toward the player, attacking every 30–90 frames. Hitting a boss 10+ times defeats it, awards a large point bonus, and respawns ducks until the next threshold is reached. The Arena mode spawns all bosses simultaneously in a circular arrangement.
  6. A shop overlay (toggled by ESC) lets you buy weapons with different fire rates and accuracies, select bird targets worth different points, and switch between worlds—each with a unique boss and environment theme.

🎓 Concepts You'll Learn

3D camera and first-person viewRaycasting collision detectionGame state management (health, score, inventory)Procedural world generationBoss behavior and AIWEBGL 3D transforms and lightingParticle effectsAudio synthesisUI overlays and shop systems

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It is the ideal place to initialize all global state: create the canvas, set starting variables, generate content (trees, ducks), attach event listeners, and call helper setup functions like setupAudio() and buildShopUI().

function setup() {
  cnv = createCanvas(windowWidth, windowHeight, WEBGL);

  // Setup player health UI
  playerHealth = playerMaxHealth;
  updatePlayerHealthBar(playerHealth, playerMaxHealth);

  document.getElementById('start-btn').addEventListener('click', async () => {
    cnv.elt.requestPointerLock();
    await userStartAudio(); // Start audio context on user gesture
    if (musicLoop && !musicLoop.isPlaying) {
      musicLoop.start(); // Start the background music
    }
  });

  // Generate environment structures based on the initial world
  // This will clear ducks and generate trees for the initial activeWorld
  generateEnvironmentTrees(activeWorld);

  // Only spawn ducks if not starting in The Arena
  if (activeWorld !== 'theArena') {
    for (let i = 0; i < 15; i++) ducks.push(new Target());
  }

  setupAudio();
  buildShopUI();

  // Restart button listener
  document.getElementById('restart-btn').addEventListener('click', () => {
    location.reload(); // Simple reload to restart the game
  });
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

calculation WEBGL Canvas Setup cnv = createCanvas(windowWidth, windowHeight, WEBGL);

Creates a fullscreen 3D canvas using WEBGL mode, which enables 3D transforms, lighting, and cameras

calculation Player Health Setup playerHealth = playerMaxHealth;

Sets the player's starting health to maximum (100) and updates the health bar UI

calculation Pointer Lock Event document.getElementById('start-btn').addEventListener('click', async () => {

Listens for START button click, locks the mouse pointer to the canvas, and starts background music

calculation World Generation generateEnvironmentTrees(activeWorld);

Procedurally generates 200+ trees/structures positioned randomly around the world based on the active world type

conditional Initial Duck Spawn if (activeWorld !== 'theArena') { for (let i = 0; i < 15; i++) ducks.push(new Target()); }

Spawns 15 duck targets at game start, unless playing in The Arena mode which has only bosses

cnv = createCanvas(windowWidth, windowHeight, WEBGL);
Creates a fullscreen WEBGL canvas stored in variable cnv. WEBGL mode enables 3D graphics, transforms, and lighting—essential for the 3D game world
playerHealth = playerMaxHealth;
Sets player starting health to 100 (the max value). This variable is checked every frame to determine if the player has died
updatePlayerHealthBar(playerHealth, playerMaxHealth);
Calls a function to update the HTML health bar UI to show full health at startup
cnv.elt.requestPointerLock();
When START is clicked, locks the mouse pointer to the canvas so mouse movement doesn't leave the window—essential for first-person control
await userStartAudio();
Modern browsers require user interaction before playing audio. This waits for the audio context to be ready before starting music
generateEnvironmentTrees(activeWorld);
Fills the trees array with 200+ randomly-positioned tree/structure objects unique to the active world type (forest, desert, arctic, etc.)
if (activeWorld !== 'theArena') { for (let i = 0; i < 15; i++) ducks.push(new Target()); }
Creates 15 duck target objects and adds them to the ducks array. The Arena mode skips this because it has only bosses
setupAudio();
Initializes p5.sound synths for gunshots, hit sounds, and background music loops
buildShopUI();
Dynamically generates HTML for the shop overlay, listing all weapons, bird targets, and worlds with their costs and descriptions

draw()

draw() runs every frame (60 times per second) and is the heartbeat of the game. It handles player input, physics, enemy AI, collision detection, and rendering. The main flow is: check game over → process input → apply gravity → update environment and enemies → render particles and gun. Game state management happens here: bosses spawn when score thresholds are met, ducks respawn when bosses die, and invincibility timers countdown.

🔬 This block shows/hides the shop overlay based on pointer lock. What happens if you remove the else branch and the overlay never closes? Or change the display values to 'block' instead of 'none'—does the menu break?

  if (isLocked) {
    document.getElementById('overlay').style.display = 'none';
    handleInput();
    if (gunData.auto && mouseIsPressed) fireGun();
  } else {
    document.getElementById('overlay').style.display = 'flex';
  }

🔬 These lines create the 3D camera direction and recoil shake. What happens if you remove the recoil multiplier by changing 1.5 to 0—does the camera still shake, or does it feel too stable?

  let lookX = cos(yaw) * cos(pitch); let lookY = sin(pitch); let lookZ = sin(yaw) * cos(pitch);
  let shakeX = random(-recoil, recoil) * 1.5; let shakeY = random(-recoil, recoil) * 1.5;
function draw() {
  if (gameOver) {
    document.getElementById('overlay').style.display = 'flex';
    document.getElementById('game-over-overlay').style.display = 'block';
    return; // Stop drawing and game logic
  }

  document.getElementById('game-over-overlay').style.display = 'none'; // Ensure game over overlay is hidden

  let isLocked = document.pointerLockElement === cnv.elt;
  let gunData = items.guns[activeGun];

  if (isLocked) {
    document.getElementById('overlay').style.display = 'none';
    handleInput();
    if (gunData.auto && mouseIsPressed) fireGun();
  } else {
    document.getElementById('overlay').style.display = 'flex';
  }

  // Player vertical movement and gravity
  if (!player.isOnGround) {
    player.vy += gravity; // Apply gravity
    player.y += player.vy; // Update vertical position
    if (player.y >= groundY) { // Check if player hits the ground
      player.y = groundY; // Snap to ground
      player.vy = 0; // Stop vertical movement
      player.isOnGround = true; // Player is on the ground again
    }
  }

  // Invincibility timer countdown
  if (invincibilityTimer > 0) {
    invincibilityTimer--;
  }

  // Background and lighting based on activeWorld
  // This function sets ambientLight and directionalLight for the entire scene
  setWorldBackgroundAndLighting(activeWorld);

  let lookX = cos(yaw) * cos(pitch); let lookY = sin(pitch); let lookZ = sin(yaw) * cos(pitch);
  let shakeX = random(-recoil, recoil) * 1.5; let shakeY = random(-recoil, recoil) * 1.5;

  camera(
    player.x + shakeX, player.y + shakeY, player.z,
    player.x + lookX + shakeX, player.y + lookY + shakeY, player.z + lookZ,
    0, 1, 0
  );

  drawEnvironment(activeWorld);

  // Arena specific logic
  if (activeWorld === 'theArena') {
    document.getElementById('boss-health-bar-container').style.display = 'block'; // Always show boss bar in arena

    if (arenaSpawnTimer < arenaSpawnDelay) {
      arenaSpawnTimer++;
      // Display a countdown or message
      push();
      camera(); // Reset camera matrix to screen space
      clearDepth();
      textAlign(CENTER, CENTER);
      textSize(64);
      fill(255, 200, 0);
      text(`BOSSES APPEARING IN ${floor((arenaSpawnDelay - arenaSpawnTimer) / 60)}...`, 0, -100);
      pop();
    } else if (arenaBosses.length === 0) {
      // Spawn all bosses once timer is up and no bosses are active
      spawnAllBossesInArena();
    }

    // Update and draw all arena bosses
    let activeBossFound = false;
    for (let i = arenaBosses.length - 1; i >= 0; i--) {
      let currentArenaBoss = arenaBosses[i];
      if (!currentArenaBoss.dead) {
        if (isLocked) {
          currentArenaBoss.update();
        }
        currentArenaBoss.draw();
        activeBossFound = true;
        // Update the main boss health bar with the first active boss's info
        drawBossBar(currentArenaBoss.health, currentArenaBoss.maxHealth);
        document.getElementById('boss-name').innerText = `ARENA: ${currentArenaBoss.type.toUpperCase()}`;
      } else {
        // Boss is dead, award points and remove
        score += currentArenaBoss.pointsAwarded;
        document.getElementById('score').innerText = score;
        arenaBosses.splice(i, 1);
      };
    }

    if (!activeBossFound && arenaBosses.length === 0 && arenaSpawnTimer >= arenaSpawnDelay) {
      // All arena bosses defeated
      gameOver = true;
      document.exitPointerLock(); // Release pointer lock on game over
      document.getElementById('game-over-overlay').style.display = 'block';
      document.getElementById('game-over-overlay').querySelector('h2').innerText = 'ARENA CLEARED!';
      document.getElementById('game-over-overlay').querySelector('p').innerText = `You defeated all bosses! Final Score: ${score.toLocaleString()}`;
    }

    // Clear ducks if any were accidentally spawned (shouldn't happen with the Arena logic)
    ducks = [];
  }
  // --- ADDED BOSS SPAWNING LOGIC HERE for single worlds ---
  else if (boss === null && score >= bossThreshold) {
    let bossData = items.worlds[activeWorld].boss;
    if (bossData) {
      let bossX = player.x + random(-2000, 2000);
      let bossZ = player.z + random(-2000, 2000);
      let bossY = player.y + 100; // Hover slightly above player eye level
      boss = new Boss(bossX, bossY, bossZ, bossData.health, bossData.damage, bossData.points, bossData.type, bossData.attackRate);
      ducks = []; // Clear all ducks when boss spawns
    }
  }
  // Original single world logic (ducks or one boss)
  else if (boss === null) { // Only draw ducks if no boss is active
    // Spawn ducks if no boss is active
    if (ducks.length === 0) {
      for (let i = 0; i < 15; i++) ducks.push(new Target());
    }
    // Update and draw ducks
    for (let d of ducks) {
      if (isLocked) {
        d.update();
      }
      d.draw();
    }
    document.getElementById('boss-health-bar-container').style.display = 'none';
  } else { // Boss is active, update and draw boss
    // Update and draw single boss
    if (isLocked) {
      boss.update();
    }
    boss.draw();
    drawBossBar(boss.health, boss.maxHealth); // Update HTML boss bar
    if (boss.dead) {
      score += boss.pointsAwarded;
      document.getElementById('score').innerText = score;
      boss = null; // Remove boss
      document.getElementById('boss-health-bar-container').style.display = 'none';
      // Re-spawn ducks after boss defeat
      ducks = []; // Clear remaining ducks
      for (let i = 0; i < 15; i++) ducks.push(new Target());

      // Increase boss threshold for the next boss
      if (bossThreshold === 2500) { // If this was the very first boss
        bossThreshold = 10000; // Set a higher threshold for the second boss
      } else {
        bossThreshold = bossThreshold * 1.5; // Increase subsequent thresholds by 50%
      }
      pointsSinceLastBoss = 0; // Reset points since last boss
    }
    // Hide ducks when boss is active
    ducks = []; // Clear ducks to ensure only boss is drawn
    document.getElementById('boss-health-bar-container').style.display = 'block';
  }

  for (let i = particles.length - 1; i >= 0; i--) {
    if (isLocked) { // Update particles only when game is active
      particles[i].update();
    }
    particles[i].draw();
    if (particles[i].life <= 0) particles.splice(i, 1);
  }

  recoil = lerp(recoil, 0, 0.1);

  camera(); // Reset camera matrix to screen space
  clearDepth(); // Guarantees the gun renders ON TOP of the environment, never clipping

  // Removed redundant lighting calls here as setWorldBackgroundAndLighting already handles it
  drawGun();
}
Line-by-line explanation (19 lines)

🔧 Subcomponents:

conditional Game Over State Check if (gameOver) { document.getElementById('overlay').style.display = 'flex'; document.getElementById('game-over-overlay').style.display = 'block'; return; }

If the player has died or won, displays the game over overlay and stops all game logic from running

conditional Pointer Lock State let isLocked = document.pointerLockElement === cnv.elt;

Checks whether the mouse pointer is locked to the canvas (true during active gameplay, false in menu)

conditional Player Input Processing if (isLocked) { document.getElementById('overlay').style.display = 'none'; handleInput(); if (gunData.auto && mouseIsPressed) fireGun(); }

When locked, hides the overlay, reads keyboard/mouse input, and fires automatically for auto-fire guns

conditional Gravity and Jump Physics if (!player.isOnGround) { player.vy += gravity; player.y += player.vy; if (player.y >= groundY) { player.y = groundY; player.vy = 0; player.isOnGround = true; } }

Applies gravity to the player's vertical velocity, updates position, and snaps the player to the ground when landing

conditional Invincibility Timer if (invincibilityTimer > 0) { invincibilityTimer--; }

Decrements the invincibility counter each frame; when it reaches 0, the player can be hit again

calculation 3D Camera and View let lookX = cos(yaw) * cos(pitch); let lookY = sin(pitch); let lookZ = sin(yaw) * cos(pitch); let shakeX = random(-recoil, recoil) * 1.5; let shakeY = random(-recoil, recoil) * 1.5; camera( player.x + shakeX, player.y + shakeY, player.z, player.x + lookX + shakeX, player.y + lookY + shakeY, player.z + lookZ, 0, 1, 0 );

Converts yaw and pitch angles into a 3D look direction, applies gun recoil camera shake, and sets the camera from player's eyes looking forward

conditional The Arena Gameplay if (activeWorld === 'theArena') { ... }

If in The Arena, spawns all bosses in a circle after a countdown, updates them, and declares victory when all are defeated

conditional Single-World Boss Threshold else if (boss === null && score >= bossThreshold) { ... }

In regular worlds, spawns a single boss when score reaches the threshold, clears ducks, and tracks health

conditional Duck Spawning and Rendering else if (boss === null) { ... }

When no boss is active, spawns and draws 15 ducks for the player to shoot

for-loop Particle Update and Removal for (let i = particles.length - 1; i >= 0; i--) { ... }

Updates all particles (from gunshots and explosions), removes them when life expires, and draws them on top

if (gameOver) { ... return; }
If gameOver is true (player died or won arena), show the game over overlay and stop all game logic immediately
let isLocked = document.pointerLockElement === cnv.elt;
Checks if the mouse pointer is currently locked to the canvas. True = game is active and processing input. False = user is in menu
let gunData = items.guns[activeGun];
Retrieves the stats object for the currently equipped gun (fire rate, accuracy radius, auto-fire flag, etc.)
handleInput();
Reads WASD keys and updates player position based on movement speed and direction player is facing
if (gunData.auto && mouseIsPressed) fireGun();
For automatic weapons, fire every frame that the mouse is held down. For semi-auto guns, fireGun() checks the rate limit internally
if (!player.isOnGround) { player.vy += gravity; player.y += player.vy; ... }
Each frame, gravity pulls the player down by adding to their downward velocity. When y-position reaches ground level, landing is detected
setWorldBackgroundAndLighting(activeWorld);
Sets the background color, ambient light, and directional light based on the current world (desert has warm orange light, arctic has cool blue, etc.)
let lookX = cos(yaw) * cos(pitch); let lookY = sin(pitch); let lookZ = sin(yaw) * cos(pitch);
Converts yaw (left/right angle) and pitch (up/down angle) into a 3D unit vector pointing where the player is looking
let shakeX = random(-recoil, recoil) * 1.5;
Creates camera shake by adding random offset when gun fires. Recoil fades each frame, creating a brief jitter effect
camera( player.x + shakeX, player.y + shakeY, player.z, ... );
Sets the 3D camera position (player's eyes) and look direction. This defines what the player sees each frame
drawEnvironment(activeWorld);
Draws the ground plane and all trees/structures for the current world type
if (activeWorld === 'theArena') { ... }
Special logic for The Arena: spawns all bosses in a circle, updates them, and declares victory when all are defeated
else if (boss === null && score >= bossThreshold) { boss = new Boss(...); }
If the player's score is high enough and no boss is active yet, spawn a single boss for the current world
else if (boss === null) { ... spawn 15 ducks ... }
If no boss is active, ensure 15 ducks are in the game and keep rendering them
else { boss.update(); boss.draw(); ... }
If a boss is active, update its position and attack timer, draw it, and check if it's been defeated
for (let i = particles.length - 1; i >= 0; i--) { particles[i].update(); particles[i].draw(); ... }
Loops backwards through all particles (to safely remove them), updates position and fades them, then removes when life reaches 0
recoil = lerp(recoil, 0, 0.1);
Gradually fades gun recoil (camera shake) back to 0. lerp smoothly interpolates—higher value fades faster
camera(); clearDepth();
Resets the camera to screen-space (2D coordinates) and clears depth testing so the 2D gun always renders on top of 3D world
drawGun();
Draws the first-person gun model on screen, with animations like recoil and muzzle flash

fireGun()

fireGun() implements raycasting: casting an invisible ray from the player's eyes in the direction they're looking, then checking if any enemy is close to that ray. It uses dot-product vector math to find the closest point on the ray to each target, then checks if that distance is within the gun's accuracy radius. This is the core collision detection system—the foundation of the entire game's combat.

function fireGun() {
  let gunData = items.guns[activeGun];
  if (frameCount - lastFired < gunData.rate) return;
  lastFired = frameCount;

  shootEnv.play();
  recoil = 3;

  let lookDir = createVector(cos(yaw) * cos(pitch), sin(pitch), sin(yaw) * cos(pitch));

  let birdScale = 1.0;
  if (activeBird === 'phoenix') birdScale = 4.0;
  if (activeBird === 'dragon') birdScale = 7.0;

  // Check for arena bosses first if activeWorld is 'theArena'
  if (activeWorld === 'theArena' && arenaBosses.length > 0) {
    for (let i = arenaBosses.length - 1; i >= 0; i--) {
      let currentArenaBoss = arenaBosses[i];
      if (currentArenaBoss.dead) continue;

      let vectorToBoss = createVector(currentArenaBoss.x - player.x, currentArenaBoss.y - player.y, currentArenaBoss.z - player.z);
      let t = vectorToBoss.dot(lookDir);

      if (t > 0) { // Boss is in front of the player
        let projection = p5.Vector.mult(lookDir, t);
        let distToLine = p5.Vector.sub(vectorToBoss, projection).mag();

        let effectiveRadius = gunData.radius + (currentArenaBoss.size * 0.6);

        if (distToLine < effectiveRadius) {
          currentArenaBoss.takeDamage(10); // Adjust damage as needed
          hitEnv.play();
          // Spawn particles
          let bossCols = getBossColors(currentArenaBoss.type);
          for(let j = 0; j < 50; j++) particles.push(new Particle(currentArenaBoss.x, currentArenaBoss.y, currentArenaBoss.z, bossCols.main));
          if (activeGun !== 'sniper') return; // Sniper can penetrate, other guns hit one boss
        }
      }
    }
    if (activeGun !== 'sniper') return; // If a boss was hit, and not sniper, return
  }
  // Original single boss hit logic (only if no arena bosses)
  else if (boss !== null) {
    let vectorToBoss = createVector(boss.x - player.x, boss.y - player.y, boss.z - player.z);
    let t = vectorToBoss.dot(lookDir);

    if (t > 0) { // Boss is in front of the player
      let projection = p5.Vector.mult(lookDir, t);
      let distToLine = p5.Vector.sub(vectorToBoss, projection).mag();

      let effectiveRadius = gunData.radius + (boss.size * 0.6);

      if (distToLine < effectiveRadius) {
        boss.takeDamage(10); // Adjust damage as needed
        hitEnv.play();
        // Spawn particles
        let bossCols = getBossColors(boss.type);
        for(let i = 0; i < 50; i++) particles.push(new Particle(boss.x, boss.y, boss.z, bossCols.main));
        if (activeGun !== 'sniper') return; // Sniper can penetrate, other guns hit one boss
      }
    }
  }

  // Original duck hit logic (only if no boss or arena bosses active, or sniper hit a boss and continues)
  for (let d of ducks) {
    if (d.dead) continue;
    let vectorToDuck = createVector(d.x - player.x, d.y - player.y, d.z - player.z);
    let t = vectorToDuck.dot(lookDir);

    if (t > 0) {
      let projection = p5.Vector.mult(lookDir, t);
      let distToLine = p5.Vector.sub(vectorToDuck, projection).mag();

      let effectiveRadius = gunData.radius + (d.size * birdScale * 0.6);

      if (distToLine < effectiveRadius) {
        d.die();
        score += items.birds[activeBird].pts;
        document.getElementById('score').innerText = score;
        hitEnv.play();

        let cols = getBirdColors(activeBird);
        for(let i = 0; i < 25; i++) particles.push(new Particle(d.x, d.y, d.z, cols.body));

        if (activeGun !== 'sniper') break;
      }
    }
  }
}
Line-by-line explanation (17 lines)

🔧 Subcomponents:

conditional Fire Rate Limiting if (frameCount - lastFired < gunData.rate) return;

Prevents firing faster than the gun's rate. Different guns have different rates (pistol=15, sniper=60, laser=1)

calculation Recoil and Sound shootEnv.play(); recoil = 3;

Plays the gunshot sound and sets recoil to 3 (which then decays each frame for camera shake)

calculation 3D Look Direction let lookDir = createVector(cos(yaw) * cos(pitch), sin(pitch), sin(yaw) * cos(pitch));

Converts yaw and pitch angles into a unit vector pointing where the player is looking—used as the raycast direction

for-loop Arena Boss Collision Loop if (activeWorld === 'theArena' && arenaBosses.length > 0) { for (let i = arenaBosses.length - 1; i >= 0; i--) { ... } }

In Arena mode, checks if the ray hits any of the bosses arranged in a circle

calculation Raycasting Collision Detection let vectorToBoss = createVector(currentArenaBoss.x - player.x, currentArenaBoss.y - player.y, currentArenaBoss.z - player.z); let t = vectorToBoss.dot(lookDir); let projection = p5.Vector.mult(lookDir, t); let distToLine = p5.Vector.sub(vectorToBoss, projection).mag();

Uses vector math to find the closest point on the ray to the boss, then checks if that distance is within the hit radius

calculation Hit Response currentArenaBoss.takeDamage(10); hitEnv.play(); let bossCols = getBossColors(currentArenaBoss.type); for(let j = 0; j < 50; j++) particles.push(new Particle(...));

When a hit occurs, deals 10 damage, plays a hit sound, and spawns 50 particles in the boss's color for visual feedback

for-loop Duck Collision Loop for (let d of ducks) { ... }

Checks if the ray hits any of the 15 ducks in the scene using the same raycasting math

if (frameCount - lastFired < gunData.rate) return;
Calculates frames elapsed since last shot. If less than the gun's rate (e.g., 15 for pistol), exit early—don't fire yet. This enforces a minimum delay between shots
lastFired = frameCount;
Records the current frame number as the last time the gun was fired, used to calculate the next fire-rate check
shootEnv.play();
Plays the gunshot sound synth (a white-noise burst with a quick envelope)
recoil = 3;
Sets recoil to 3. Each frame, recoil is lerped back toward 0, creating a brief camera shake before steadying
let lookDir = createVector(cos(yaw) * cos(pitch), sin(pitch), sin(yaw) * cos(pitch));
Converts the player's yaw (horizontal angle) and pitch (vertical angle) into a unit direction vector. This is the ray direction—where the gun is 'pointing'
let birdScale = 1.0; if (activeBird === 'phoenix') birdScale = 4.0;
Sets the bird scale multiplier. Phoenix and Dragon birds are much larger, so their hit radius is scaled up proportionally
let vectorToBoss = createVector(currentArenaBoss.x - player.x, currentArenaBoss.y - player.y, currentArenaBoss.z - player.z);
Creates a vector from player to boss. This is one point on the target to check against the ray
let t = vectorToBoss.dot(lookDir);
Computes the dot product: how much of the 'to boss' vector aligns with the look direction. If t > 0, the boss is in front; if t < 0, behind
if (t > 0) { // Boss is in front of the player
Only check collision if t > 0 (boss is ahead). Prevents hitting things behind you
let projection = p5.Vector.mult(lookDir, t);
Scales the look direction by t to find the closest point on the ray to the boss
let distToLine = p5.Vector.sub(vectorToBoss, projection).mag();
Subtracts the projection from the boss vector, then takes magnitude—this is the perpendicular distance from the boss to the ray
let effectiveRadius = gunData.radius + (currentArenaBoss.size * 0.6);
Calculates the hit radius by combining the gun's accuracy radius (15-80 pixels depending on gun type) with the boss's scaled size
if (distToLine < effectiveRadius) {
If the perpendicular distance is smaller than the effective radius, it's a hit
currentArenaBoss.takeDamage(10);
Reduces the boss's health by 10 points. When health reaches 0, the boss is marked dead
for(let j = 0; j < 50; j++) particles.push(new Particle(currentArenaBoss.x, currentArenaBoss.y, currentArenaBoss.z, bossCols.main));
Spawns 50 particles at the boss's position in the boss's main color. They fly outward and fade—pure visual feedback for the hit
if (activeGun !== 'sniper') return;
Most guns hit one target and stop. Sniper ignores this return, so it can hit multiple targets in a line (penetrating shots)
if (distToLine < effectiveRadius) { d.die(); score += items.birds[activeBird].pts; ... }
When a duck is hit: mark it as dead, add points to the score based on the bird type, play hit sound, and spawn particles

handleInput()

handleInput() is the core of player movement. It reads mouse position (movedX, movedY) to rotate the view (yaw and pitch), converts those angles into directional vectors (walkDir and rightDir), checks which WASD keys are pressed each frame, and updates the player's position accordingly. The key insight is that movement is always relative to where the player is facing—if you're looking left, pressing W moves you left, not north.

🔬 These four lines map WASD to movement in the direction you're facing. What happens if you swap the signs—change += to -= for W? Can you now walk backwards when pressing W? Or what if you change moveSpeed to moveSpeed * 2 to run twice as fast?

  if (keyIsDown(87)) { player.x += walkDir.x * moveSpeed; player.z += walkDir.z * moveSpeed; }
  if (keyIsDown(83)) { player.x -= walkDir.x * moveSpeed; player.z -= walkDir.z * moveSpeed; }
  if (keyIsDown(65)) { player.x -= rightDir.x * moveSpeed; player.z -= rightDir.z * moveSpeed; }
  if (keyIsDown(68)) { player.x += rightDir.x * moveSpeed; player.z -= rightDir.z * moveSpeed; }
function handleInput() {
  yaw += movedX * 0.002; pitch += movedY * 0.002;
  pitch = constrain(pitch, -PI / 2 + 0.05, PI / 2 - 0.05);

  let walkDir = createVector(cos(yaw), 0, sin(yaw)).normalize();
  let rightDir = createVector(cos(yaw + HALF_PI), 0, sin(yaw + HALF_PI)).normalize();

  if (keyIsDown(87)) { player.x += walkDir.x * moveSpeed; player.z += walkDir.z * moveSpeed; }
  if (keyIsDown(83)) { player.x -= walkDir.x * moveSpeed; player.z -= walkDir.z * moveSpeed; }
  if (keyIsDown(65)) { player.x -= rightDir.x * moveSpeed; player.z -= rightDir.z * moveSpeed; }
  if (keyIsDown(68)) { player.x += rightDir.x * moveSpeed; player.z -= rightDir.z * moveSpeed; }

  player.x = constrain(player.x, -3500, 3500); player.z = constrain(player.z, -3500, 3500);
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

calculation Mouse Look (Yaw and Pitch) yaw += movedX * 0.002; pitch += movedY * 0.002;

Updates yaw (left/right) and pitch (up/down) angles based on mouse movement. movedX and movedY are built-in p5.js variables tracking frame-to-frame pointer movement

calculation Pitch Limiting pitch = constrain(pitch, -PI / 2 + 0.05, PI / 2 - 0.05);

Prevents the player from looking more than 90° up or down (prevents flipping camera and disorienting the view)

calculation Walking Direction Vectors let walkDir = createVector(cos(yaw), 0, sin(yaw)).normalize(); let rightDir = createVector(cos(yaw + HALF_PI), 0, sin(yaw + HALF_PI)).normalize();

Calculates the direction the player is facing (walkDir) and the direction perpendicular to it (rightDir). Used for forward/back and strafe movement

for-loop WASD Key Movement if (keyIsDown(87)) { player.x += walkDir.x * moveSpeed; player.z += walkDir.z * moveSpeed; } if (keyIsDown(83)) { player.x -= walkDir.x * moveSpeed; player.z -= walkDir.z * moveSpeed; } if (keyIsDown(65)) { player.x -= rightDir.x * moveSpeed; player.z -= rightDir.z * moveSpeed; } if (keyIsDown(68)) { player.x += rightDir.x * moveSpeed; player.z -= rightDir.z * moveSpeed; }

Checks if W (87), A (65), S (83), or D (68) is pressed each frame and updates player x/z position in the corresponding direction

calculation World Boundary Clamping player.x = constrain(player.x, -3500, 3500); player.z = constrain(player.z, -3500, 3500);

Prevents the player from walking beyond the world bounds (-3500 to 3500 in both x and z). Creates an invisible fence

yaw += movedX * 0.002;
Adds mouse horizontal movement (movedX) scaled by 0.002 to yaw. Negative movedX = looking left; positive = looking right. The 0.002 sensitivity can be tuned for snappier or more relaxed controls
pitch += movedY * 0.002;
Adds mouse vertical movement (movedY) to pitch. Negative movedY = looking up; positive = looking down
pitch = constrain(pitch, -PI / 2 + 0.05, PI / 2 - 0.05);
Locks pitch between -90° and +90°. The 0.05 offset prevents exactly ±90°, avoiding gimbal lock and keeping the view stable at the extreme angles
let walkDir = createVector(cos(yaw), 0, sin(yaw)).normalize();
Converts the yaw angle into a 2D unit vector pointing in the direction the player is facing horizontally. Y is always 0 (no vertical component)
let rightDir = createVector(cos(yaw + HALF_PI), 0, sin(yaw + HALF_PI)).normalize();
Creates a perpendicular direction (90° to the right) by adding HALF_PI (90°) to yaw. Used for strafing left/right
if (keyIsDown(87)) { player.x += walkDir.x * moveSpeed; player.z += walkDir.z * moveSpeed; }
If W (key 87) is pressed, move the player forward along the walkDir direction by moveSpeed pixels
if (keyIsDown(83)) { player.x -= walkDir.x * moveSpeed; player.z -= walkDir.z * moveSpeed; }
If S (key 83) is pressed, move backward (negative walkDir) by moveSpeed pixels
if (keyIsDown(65)) { player.x -= rightDir.x * moveSpeed; player.z -= rightDir.z * moveSpeed; }
If A (key 65) is pressed, strafe left (negative rightDir) by moveSpeed pixels
if (keyIsDown(68)) { player.x += rightDir.x * moveSpeed; player.z -= rightDir.z * moveSpeed; }
If D (key 68) is pressed, strafe right (positive rightDir) by moveSpeed pixels. Note: the Z component is negated due to coordinate system differences
player.x = constrain(player.x, -3500, 3500);
Clamps the player's x position to the range [-3500, 3500]. If x exceeds these bounds, it's snapped back to the limit
player.z = constrain(player.z, -3500, 3500);
Clamps the player's z position to the same range, preventing them from walking infinitely in any direction

drawGun()

drawGun() renders the first-person gun model in 2D screen space (not the 3D world). After camera() is called in draw() and the 3D scene is rendered, drawGun() resets the camera to 2D, allowing it to draw on top. The gun is positioned at a fixed screen location but animates based on game state: recoil makes it jump, the minigun spins when firing, and the muzzle flash brightens with each shot. This creates the illusion of a real gun in your hands.

🔬 This block draws the pistol using three boxes. What happens if you change the first box dimensions from (8, 12, 50) to (20, 20, 80)—does the gun become thicker and longer? Or if you change fill(60) to fill(200), does the gun turn lighter?

  if (activeGun === 'pistol') {
    fill(60); box(8, 12, 50);
    push(); translate(0, 15, 15); fill(30); box(8, 25, 15); pop();
    push(); translate(0, 8, 5); fill(20); box(2, 8, 10); pop();
  }
function drawGun() {
  push();
  translate(width * 0.15, height * 0.25 + recoil * 10, 250);
  scale(1.2);
  rotateY(-PI / 9); rotateX(PI / 20);

  let skin = color(255, 204, 153);

  push();
  if (activeGun === 'laser') translate(0, 25, 45);
  else translate(0, 15, 25);
  fill(skin); sphere(9);
  pop();

  push();
  if(activeGun === 'shotgun') translate(0, 5, -20 + recoil * 10);
  else if(activeGun === 'pistol') translate(0, 18, 15);
  else if(activeGun === 'laser') translate(-18, 15, -40);
  else if(activeGun === 'minigun') translate(22, 5, -10);
  else translate(0, 10, -20);
  fill(skin); sphere(9);
  pop();

  if (activeGun === 'pistol') {
    fill(60); box(8, 12, 50);
    push(); translate(0, 15, 15); fill(30); box(8, 25, 15); pop();
    push(); translate(0, 8, 5); fill(20); box(2, 8, 10); pop();
  }
  else if (activeGun === 'shotgun') {
    fill(40); box(10, 10, 140);
    push(); translate(0, 5, -20 + recoil * 10); fill(100, 50, 20); box(14, 14, 35); pop();
    translate(0, 10, 60); fill(80, 50, 20); box(12, 25, 60);
  }
  else if (activeGun === 'ar') {
    fill(30); box(10, 15, 120);
    push(); translate(0, -10, -20); fill(10); box(2, 5, 20); pop();
    push(); translate(0, -10, 30); fill(10); box(2, 5, 10); pop();
    push(); translate(0, 20, -5); fill(20); box(8, 30, 15); pop();
    push(); translate(0, 15, 25); fill(20); box(10, 25, 15); pop();
    translate(0, 10, 55); fill(20); box(10, 20, 50);
  }
  else if (activeGun === 'sniper') {
    fill(30, 40, 30); box(6, 6, 200);
    push(); translate(0, -12, 10); rotateX(HALF_PI); fill(10); cylinder(5, 50); pop();
    push(); translate(0, 15, 30); fill(20); box(10, 25, 15); pop();
    translate(0, 10, 70); fill(20); box(10, 25, 60);
  }
  else if (activeGun === 'minigun') {
    let spin = mouseIsPressed ? frameCount * 0.5 : 0;
    push();
    translate(0, 0, -50); rotateZ(spin);
    for (let i=0; i<4; i++) {
      push(); rotateZ(i * HALF_PI); translate(14, 0, 0); fill(70); box(8, 8, 160); pop();
    }
    pop();
    translate(0, 15, 40); fill(30); box(35, 35, 100);
  }
  else if (activeGun === 'laser') {
    fill(200, 200, 255); box(36, 45, 200);
    push(); translate(0, 0, -80); rotateX(-HALF_PI); fill(0, 255, 255); cylinder(12, 260); pop();
    push(); translate(0, 0, -120); fill(0, 255, 255); box(45, 55, 20); pop();
    push(); translate(0, 25, 45); fill(80); box(12, 35, 25); pop();
  }

  // --- MUZZLE FLASH ---
  if (recoil > 2.5 && activeGun !== 'sniper') {
    push();
    let flashOffset = -60;
    if (activeGun === 'minigun') flashOffset = -140;
    else if (activeGun === 'laser') flashOffset = -220;

    translate(0, 0, flashOffset); rotateX(-HALF_PI);
    noStroke();

    if (activeGun === 'laser') {
      fill(0, 255, 255, 200); cone(35, 90);
      translate(0, -15, 0); fill(255, 255, 200); sphere(20);
    } else if (activeGun === 'minigun') {
      fill(255, 150, 0, 200); cone(25, 60);
      translate(0, -10, 0); fill(255, 255, 200); sphere(15);
    } else {
      fill(255, 150, 0, 200); cone(12, 30);
      translate(0, -8, 0); fill(255, 255, 200); sphere(8);
    }
    pop();
  }
  pop();
}
Line-by-line explanation (13 lines)

🔧 Subcomponents:

calculation Gun Screen Position and Rotation translate(width * 0.15, height * 0.25 + recoil * 10, 250); scale(1.2); rotateY(-PI / 9); rotateX(PI / 20);

Positions the gun in the lower-right corner of the screen, scaled up, and rotated at a slight angle. The + recoil * 10 makes it kick upward when firing

calculation Hand Model (Skin-Colored Spheres) push(); if (activeGun === 'laser') translate(0, 25, 45); else translate(0, 15, 25); fill(skin); sphere(9); pop();

Draws two skin-colored spheres to represent the player's hands gripping the gun. Positions vary by gun type

switch-case Gun Model Selection if (activeGun === 'pistol') { ... } else if (activeGun === 'shotgun') { ... } else if (activeGun === 'ar') { ... }

Draws a different 3D gun model for each weapon type using boxes, cylinders, and colors. Each gun has unique proportions and details

conditional Muzzle Flash Effect if (recoil > 2.5 && activeGun !== 'sniper') { ... }

When recoil is high (gun just fired), draws a bright cone and sphere at the gun's muzzle. Fades quickly as recoil decays

translate(width * 0.15, height * 0.25 + recoil * 10, 250);
Positions the gun at 15% from the left edge and 25% from the top of the screen. The + recoil * 10 makes it kick upward when firing. Z=250 puts it in front of the camera (toward the viewer)
scale(1.2);
Enlarges the entire gun model by 20% so it fills more of the screen and is easier to see
rotateY(-PI / 9); rotateX(PI / 20);
Tilts the gun slightly down (negative X rotation) and outward to the right (negative Y rotation), matching a relaxed holding position
let skin = color(255, 204, 153);
Defines a skin tone color (peach) used for the hands. RGB (255, 204, 153) is a common skin tone in digital art
fill(skin); sphere(9);
Draws a sphere with diameter 18 in the skin color—one of two 'hands' holding the gun
if (activeGun === 'pistol') { fill(60); box(8, 12, 50); ... }
For pistol, draws a thin dark gray box (the slide), then adds smaller boxes for the trigger guard and trigger. Each box's position and size defines part of the gun silhouette
else if (activeGun === 'shotgun') { fill(40); box(10, 10, 140); ... }
Shotgun is wider and has a large barrel. The recoil-based translate makes the barrel recoil upward when firing
else if (activeGun === 'minigun') { let spin = mouseIsPressed ? frameCount * 0.5 : 0; ... }
Minigun barrels spin when the mouse is pressed. The rotation angle is frameCount * 0.5 (spinning faster each frame), reset to 0 when mouse is released
else if (activeGun === 'laser') { fill(200, 200, 255); box(36, 45, 200); ... }
Laser is bright white-blue and has a large cylindrical barrel. Very futuristic proportions—much larger than the pistol
if (recoil > 2.5 && activeGun !== 'sniper') {
Draws muzzle flash only when recoil is high (fresh shot) and the gun isn't a sniper (snipers are silent/precise). Sniper is excluded because it's a silent one-shot
let flashOffset = -60;
Positions the muzzle flash in front of the gun barrel. Different guns have different barrel lengths, so flashOffset varies
fill(0, 255, 255, 200); cone(35, 90);
For laser, the muzzle flash is a bright cyan cone (electric blue). Alpha 200 makes it semi-transparent
fill(255, 150, 0, 200); cone(25, 60);
For other guns, the muzzle flash is an orange-yellow cone—classic gunfire color—with brightness falling off as recoil decays

class Target

The Target class is a complete entity: it spawns at a random location around the player, flies in a wavy pattern, bounces off invisible boundaries, and when hit, plays a death animation (flying upward and spinning). The class renders different bird models (6 types) with animated wings. The key insight is that each target encapsulates its own state (position, velocity, size, dead flag) and behavior (update logic, drawing), making it easy to manage multiple targets in the ducks array.

class Target {
  constructor() { this.reset(); }
  reset() {
    let angle = random(TWO_PI);
    let dist = random(500, 2000);
    this.x = player.x + cos(angle) * dist;
    this.z = player.z + sin(angle) * dist;
    this.y = random(-600, -150);
    this.vx = random(-6, 6);
    this.vz = random(-6, 6);
    this.vy = random(-1.0, 1.0);
    this.size = random(18, 28);
    this.dead = false; this.deathSpin = 0;
  }
  die() { this.dead = true; this.vx = 0; this.vz = 0; this.vy = 12; }

  update() {
    let spdMult = items.birds[activeBird].speed;
    this.x += this.vx * (this.dead ? 1 : spdMult);
    this.y += this.vy * (this.dead ? 1 : spdMult);
    this.z += this.vz * (this.dead ? 1 : spdMult);

    if (this.dead) {
      this.deathSpin += 0.3; if (this.y > 0) this.reset();
    } else {
      this.y += sin(frameCount * 0.05 * spdMult + this.size) * 1.5;
      if (this.y < -800 || this.y > -100) this.vy *= -1;
      if (dist(this.x, 0, this.z, player.x, 0, player.z) > 2500) { this.vx *= -1; this.vz *= -1; }
    }
  }

  draw() {
    push(); translate(this.x, this.y, this.z);
    if (!this.dead) rotateY(atan2(this.vx, this.vz)); else { rotateZ(this.deathSpin); rotateX(this.deathSpin); }
    noStroke();

    let cols = getBirdColors(activeBird);
    let scaleMult = 1.0;
    if (activeBird === 'phoenix') scaleMult = 4.0;
    if (activeBird === 'dragon') scaleMult = 7.0;
    let s = this.size * scaleMult;

    let flap = this.dead ? 0 : sin(frameCount * items.birds[activeBird].speed * 0.4 + s) * 0.6;

    fill(cols.body[0], cols.body[1], cols.body[2]);

    if (activeBird === 'duck') {
      ellipsoid(s, s * 0.8, s * 1.3);
      push(); translate(0, -s * 0.8, s); fill(cols.head[0], cols.head[1], cols.head[2]); sphere(s * 0.7);
      translate(0, 0, s * 0.6); fill(cols.beak[0], cols.beak[1], cols.beak[2]); box(s * 0.5, s * 0.2, s * 0.8); pop();
      push(); translate(-s * 1.3, 0, 0); rotateZ(flap); box(s * 1.5, s * 0.1, s); pop();
      push(); translate(s * 1.3, 0, 0); rotateZ(-flap); box(s * 1.5, s * 0.1, s); pop();
    }
    else if (activeBird === 'pigeon') {
      ellipsoid(s * 1.2, s * 1.1, s * 1.3);
      push(); translate(0, -s * 0.7, s * 0.8); fill(cols.head[0], cols.head[1], cols.head[2]); sphere(s * 0.5);
      translate(0, 0, s * 0.5); rotateX(HALF_PI); fill(cols.beak[0], cols.beak[1], cols.beak[2]); cone(s * 0.2, s * 0.4); pop();
      push(); translate(-s * 1.3, 0, 0); rotateZ(flap); box(s * 1.5, s * 0.1, s); pop();
      push(); translate(s * 1.3, 0, 0); rotateZ(-flap); box(s * 1.5, s * 0.1, s); pop();
    }
    else if (activeBird === 'crow') {
      ellipsoid(s * 0.8, s * 0.7, s * 1.5);
      push(); translate(0, -s * 0.6, s * 1.2); fill(cols.head[0], cols.head[1], cols.head[2]); sphere(s * 0.6);
      translate(0, 0, s * 0.6); rotateX(HALF_PI); fill(cols.beak[0], cols.beak[1], cols.beak[2]); cone(s * 0.2, s * 1.0); pop();
      push(); translate(-s * 1.2, 0, -s * 0.5); rotateZ(flap); rotateY(PI/6); box(s * 1.8, s * 0.1, s); pop();
      push(); translate(s * 1.2, 0, -s * 0.5); rotateZ(-flap); rotateY(-PI/6); box(s * 1.8, s * 0.1, s); pop();
    }
    else if (activeBird === 'goose') {
      ellipsoid(s * 1.2, s, s * 1.5);
      push();
      translate(0, -s * 1.2, s * 1.2); rotateX(PI/8); cylinder(s * 0.3, s * 2);
      translate(0, -s, s * 0.5); fill(cols.head[0], cols.head[1], cols.head[2]); sphere(s * 0.6);
      translate(0, 0, s * 0.6); fill(cols.beak[0], cols.beak[1], cols.beak[2]); box(s * 0.6, s * 0.3, s * 1.2);
      pop();
      push(); translate(-s * 1.5, 0, 0); rotateZ(flap); box(s * 2, s * 0.1, s * 1.5); pop();
      push(); translate(s * 1.5, 0, 0); rotateZ(-flap); box(s * 2, s * 0.1, s * 1.5); pop();
    }
    else if (activeBird === 'phoenix') {
      ellipsoid(s, s * 0.8, s * 1.3);
      push(); translate(0, -s * 0.8, s); fill(cols.head[0], cols.head[1], cols.head[2]); sphere(s * 0.7);
      translate(0, -s * 0.6, -s * 0.2); rotateX(-PI/4); fill(255, 200, 0); cone(s * 0.3, s * 1.5); pop();
      push(); translate(0, -s * 0.8, s * 1.6); fill(cols.beak[0], cols.beak[1], cols.beak[2]); box(s * 0.4, s * 0.2, s * 0.6); pop();
      push(); translate(0, 0, -s * 1.5); rotateX(-HALF_PI); fill(255, 100, 0); cone(s * 0.6, s * 2.5); pop();
      push(); translate(-s * 1.5, 0, 0); rotateZ(flap); fill(255, 150, 0); box(s * 2, s * 0.1, s * 1.2); pop();
      push(); translate(s * 1.5, 0, 0); rotateZ(-flap); fill(255, 150, 0); box(s * 2, s * 0.1, s * 1.2); pop();
    }
    else if (activeBird === 'dragon') {
      ellipsoid(s * 0.8, s * 0.8, s * 2.2);
      push(); translate(0, -s * 0.8, s * 1.8); fill(cols.head[0], cols.head[1], cols.head[2]); box(s * 1.2, s, s * 1.2);
      translate(0, 0, s * 0.8); fill(cols.beak[0], cols.beak[1], cols.beak[2]); box(s * 0.8, s * 0.5, s); pop();
      push(); translate(-s * 0.4, -s * 1.5, s * 1.5); rotateX(-PI/4); fill(200); cone(s * 0.15, s * 1.2); pop();
      push(); translate(s * 0.4, -s * 1.5, s * 1.5); rotateX(-PI/4); fill(200); cone(s * 0.15, s * 1.2); pop();
      push(); translate(-s * 1.5, 0, 0); rotateZ(flap); box(s * 2, s * 0.1, s * 1.5); translate(-s, 0, -s * 0.5); box(s, s * 0.1, s * 2); pop();
      push(); translate(s * 1.5, 0, 0); rotateZ(-flap); box(s * 2, s * 0.1, s * 1.5); translate(s, 0, -s * 0.5); box(s, s * 0.1, s * 2); pop();
    }
    pop();
  }
}
Line-by-line explanation (19 lines)

🔧 Subcomponents:

calculation Target Reset reset() { let angle = random(TWO_PI); let dist = random(500, 2000); ... }

Spawns a new target at a random angle and distance from the player, with random size and velocity

calculation Death Animation Start die() { this.dead = true; this.vx = 0; this.vz = 0; this.vy = 12; }

When hit, stops horizontal movement and sends the bird flying upward at velocity 12, then spinning as it falls

calculation Target Update Logic update() { let spdMult = items.birds[activeBird].speed; ... }

Every frame, updates position based on velocity, applies wavy bobbing motion, bounces off boundaries, and resets if off-screen

calculation Target Rendering draw() { push(); translate(this.x, this.y, this.z); ... }

Draws a 3D bird model based on activeBird type (duck, pigeon, crow, goose, phoenix, dragon), with wing flapping animation and spinning death

constructor() { this.reset(); }
When a new Target is created, immediately call reset() to initialize it with random position, size, and velocity
let angle = random(TWO_PI);
Chooses a random angle (0 to 2π radians = 0 to 360°) for the target to spawn around the player
let dist = random(500, 2000);
Randomly chooses how far from the player (500 to 2000 units) the target spawns. Keeps targets visible but not too close
this.x = player.x + cos(angle) * dist;
Converts the angle and distance into an x-coordinate relative to the player's position
this.z = player.z + sin(angle) * dist;
Converts to a z-coordinate (forward/backward in the world) using sin of the angle
this.y = random(-600, -150);
Spawns the target somewhere in the upper half of the space (y=0 is ground, so -600 to -150 is 150 to 600 units above)
this.vx = random(-6, 6); this.vz = random(-6, 6);
Gives the target random horizontal velocities so it drifts and flies around the space
this.vy = random(-1.0, 1.0);
Small vertical velocity—targets bob up and down slightly
this.size = random(18, 28);
Randomizes the target's size between 18 and 28 pixels, making them subtly different
die() { this.dead = true; this.vx = 0; this.vz = 0; this.vy = 12; }
When hit, marks dead=true, stops horizontal drift (vx/vz=0), and launches it upward (vy=12) for a dramatic death animation
let spdMult = items.birds[activeBird].speed;
Retrieves the speed multiplier for the active bird type (duck=1.0, phoenix=2.5, dragon=3.0, etc.). Faster birds move faster
this.x += this.vx * (this.dead ? 1 : spdMult);
Updates x-position. If dead, velocity is unchanged. If alive, velocity is multiplied by spdMult to make faster birds zip around
if (this.dead) { this.deathSpin += 0.3; if (this.y > 0) this.reset(); }
When dead, increments the spin counter each frame. If the target falls above ground (y > 0), reset it for a new target
this.y += sin(frameCount * 0.05 * spdMult + this.size) * 1.5;
Adds a wavy bobbing motion to alive targets. The sin function oscillates, creating a smooth up-down flutter. Faster birds bob faster
if (this.y < -800 || this.y > -100) this.vy *= -1;
If the target drifts too high (y > -100) or too low (y < -800), flip its vertical velocity to bounce it back into view
if (dist(this.x, 0, this.z, player.x, 0, player.z) > 2500) { this.vx *= -1; this.vz *= -1; }
If the target drifts more than 2500 units away horizontally, reverse its velocity to turn it around
if (!this.dead) rotateY(atan2(this.vx, this.vz)); else { rotateZ(this.deathSpin); rotateX(this.deathSpin); }
If alive, rotate the bird to face the direction it's flying. If dead, spin it around both X and Z axes for a tumbling effect
let flap = this.dead ? 0 : sin(frameCount * items.birds[activeBird].speed * 0.4 + s) * 0.6;
Calculates wing flapping amplitude. Alive birds' wings flap smoothly (oscillating between -0.6 and 0.6). Dead birds' wings don't flap
if (activeBird === 'duck') { ellipsoid(s, s * 0.8, s * 1.3); ... }
Draws a duck with a rounded body, small head, short beak, and flapping wings. Each body part is positioned and colored based on bird type

class Boss

The Boss class encapsulates all boss behavior: it spawns at a location, hovers and drifts toward the player, attacks periodically within range, and tracks health. The key AI technique is using lerp() to smoothly chase the player without snapping directly to them—creating a dramatic, threatening movement that's not instant teleportation. When health reaches 0, the boss is marked dead, triggering victory and reward logic in the main draw() loop.

class Boss {
  constructor(x, y, z, health, damage, points, type, attackRate) {
    this.x = x;
    this.y = y;
    this.z = z;
    this.health = health;
    this.maxHealth = health;
    this.damage = damage;
    this.pointsAwarded = points;
    this.type = type;
    this.attackRate = attackRate;
    this.attackTimer = attackRate;
    this.dead = false;
    this.size = random(80, 150); // General size for collision
    this.hoverOffset = 0; // For hovering effect
  }

  update() {
    if (this.dead) return;

    // Simple boss movement: hover and slowly lerp towards player
    this.hoverOffset = sin(frameCount * 0.03) * 30; // Hover effect
    this.y = player.y + 100 + this.hoverOffset; // Maintain height relative to player

    // Lerp towards the player slowly
    this.x = lerp(this.x, player.x, 0.005);
    this.z = lerp(this.z, player.z, 0.005);

    // Boss Attack Logic
    this.attackTimer--;
    if (this.attackTimer <= 0) {
      let distToPlayer = dist(this.x, this.y, this.z, player.x, player.y, player.z);
      if (distToPlayer < 600) { // If player is within attack range
        takePlayerDamage(this.damage);
        // Play hit sound or specific boss attack sound here
      }
      this.attackTimer = this.attackRate; // Reset attack timer
    }
  }

  draw() {
    if (this.dead) return;

    push();
    translate(this.x, this.y, this.z);
    rotateY(atan2(player.x - this.x, player.z - this.z)); // Always face player
    scale(this.size / 100); // Scale based on general size
    drawSpecificBoss(this.type); // Draw the boss's specific model
    pop();
  }

  takeDamage(amount) {
    if (this.dead) return;
    this.health -= amount;
    if (this.health < 0) this.health = 0;
    if (this.health <= 0) {
      this.dead = true;
    }
  }
}
Line-by-line explanation (15 lines)

🔧 Subcomponents:

calculation Boss Initialization constructor(x, y, z, health, damage, points, type, attackRate) { ... }

Creates a boss at a specific location with health, damage output, point reward, type (determines model), and attack frequency

calculation Boss AI and Attack update() { ... this.x = lerp(this.x, player.x, 0.005); ... }

Every frame, makes the boss hover with a sine wave, slowly drift toward the player, and attack if the player is within range

calculation Boss Rendering draw() { ... rotateY(atan2(player.x - this.x, player.z - this.z)); drawSpecificBoss(this.type); ... }

Draws the boss at its position, always facing the player, scaled to its random size, using a model-specific drawing function

calculation Damage Handling takeDamage(amount) { this.health -= amount; if (this.health <= 0) { this.dead = true; } }

Reduces boss health by the damage amount. When health reaches 0, marks the boss as dead (triggering despawn and victory logic)

constructor(x, y, z, health, damage, points, type, attackRate) {
Constructor receives all parameters needed to define a unique boss: position (x, y, z), stats (health, damage), reward (points), appearance (type), and behavior (attackRate in frames)
this.size = random(80, 150);
Each boss gets a random size for visual variety. Used in collision detection (hitRadius) and in the draw() scale
this.hoverOffset = 0;
Initialized to 0; updated each frame using a sine wave to create a floating/bobbing effect
this.hoverOffset = sin(frameCount * 0.03) * 30;
Creates a smooth oscillating vertical bob. sin() oscillates between -1 and 1, multiplied by 30 gives ±30 pixel offset. Slower frequency (0.03) than ducks
this.y = player.y + 100 + this.hoverOffset;
Keeps the boss floating 100 units above the player's eye level, plus the hover offset. Maintains a consistent relative height as the player moves
this.x = lerp(this.x, player.x, 0.005);
Slowly interpolates the boss's x-position toward the player's x. The 0.005 factor makes this very slow (takes ~400 frames to reach player)
this.z = lerp(this.z, player.z, 0.005);
Same as x, but for the z-axis (forward/backward). Boss gradually drifts toward player on both horizontal axes
this.attackTimer--;
Decrements the attack timer each frame. When it reaches 0, the boss attacks
let distToPlayer = dist(this.x, this.y, this.z, player.x, player.y, player.z);
Calculates the 3D distance between the boss and player in units
if (distToPlayer < 600) { takePlayerDamage(this.damage); }
If player is within 600 units, the boss hits the player with its damage value. Creates an attack range zone
this.attackTimer = this.attackRate;
Resets the attack timer to its maximum value so the boss won't attack again until the timer counts down
rotateY(atan2(player.x - this.x, player.z - this.z));
Calculates the angle from boss to player and rotates the boss to face that direction. atan2 converts (x, z) delta into a yaw angle
scale(this.size / 100);
Scales the boss model by its random size. A size of 100 results in 1x scale; 80 = 80% size, 150 = 150% size
this.health -= amount;
Reduces health by the damage amount (usually 10 from gunshots)
if (this.health <= 0) { this.dead = true; }
When health reaches 0 or below, marks the boss dead. The draw() and update() methods return early when dead = true

generateEnvironmentTrees()

generateEnvironmentTrees() is a procedural generation system. It doesn't hardcode tree positions; instead, it generates them randomly based on world type parameters. This allows each world to feel distinct (arctic is dense, desert is sparse) while keeping the code flexible. The pattern of setting a base configuration then overriding it per world type is common in game dev—it's how you customize multiple environments from a single system.

function generateEnvironmentTrees(worldType) {
  trees = []; // Clear existing trees
  let treeCount = 200;
  let treeSizeRange = [1.5, 4.0];
  let treeOffsetY = 0; // Vertical offset for structures (e.g., Space Outpost)

  if (worldType === 'desert') {
    treeCount = 100; // Fewer structures (cacti/shrubs)
    treeSizeRange = [1.0, 2.5]; // Smaller cacti/shrubs
  } else if (worldType === 'arctic') {
    treeCount = 250; // Denser forest
    treeSizeRange = [2.0, 5.0]; // Taller pines
  } else if (worldType === 'swamp') {
    treeCount = 150; // Moderate density
    treeSizeRange = [1.8, 4.5]; // Taller, thinner trees
  } else if (worldType === 'volcanic') {
    treeCount = 75; // Sparse, rocky structures
    treeSizeRange = [1.0, 3.0];
  } else if (worldType === 'spaceOutpost') {
    treeCount = 50; // Few, larger futuristic structures
    treeSizeRange = [2.0, 6.0];
    treeOffsetY = -150; // Structures sit on the platform, which is at player eye level
  } else if (worldType === 'wasteland') {
    treeCount = 120;
    treeSizeRange = [0.8, 3.0];
    treeOffsetY = 0; // Ground level
  } else if (worldType === 'alienJungle') {
    treeCount = 180;
    treeSizeRange = [1.5, 5.0];
    treeOffsetY = 0;
  } else if (worldType === 'underwater') {
    treeCount = 100;
    treeSizeRange = [1.0, 4.0];
    treeOffsetY = 0;
  } else if (worldType === 'theArena') { // No trees for The Arena
    treeCount = 0;
  }

  for (let i = 0; i < treeCount; i++) {
    trees.push({
      x: random(-4000, 4000),
      z: random(-4000, 4000),
      size: random(treeSizeRange[0], treeSizeRange[1]),
      offsetY: treeOffsetY
    });
  }
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

calculation Tree Array Reset trees = [];

Clears any existing trees before generating new ones, preventing duplicate objects from accumulating

switch-case World-Specific Tree Counts and Sizes if (worldType === 'desert') { ... } else if (worldType === 'arctic') { ... }

Sets treeCount, treeSizeRange, and treeOffsetY based on the world type. Desert has few small cacti; Arctic has many tall pines; etc.

for-loop Procedural Tree Placement for (let i = 0; i < treeCount; i++) { trees.push({ ... }); }

Generates treeCount random tree objects at positions scattered across a 8000x8000 unit area around the player's start location

trees = [];
Empties the trees array so old trees don't persist when switching worlds
let treeCount = 200;
Default tree count (200) for forest. Overridden for each world type
let treeSizeRange = [1.5, 4.0];
Default size range (scale multiplier). Forest trees are 1.5–4x scale. Desert trees are smaller; Arctic trees are larger
if (worldType === 'desert') { treeCount = 100; treeSizeRange = [1.0, 2.5]; }
Desert is sparse with small cacti. 100 trees at 1–2.5x scale makes the landscape feel open and arid
else if (worldType === 'arctic') { treeCount = 250; treeSizeRange = [2.0, 5.0]; }
Arctic is dense with tall pine trees. 250 trees create a visually packed, wintry forest. Larger trees (2–5x) create dramatic scenery
else if (worldType === 'spaceOutpost') { treeOffsetY = -150; }
Space Outpost structures sit on a platform at the player's eye level (y = -150). The offsetY positions them on that platform
else if (worldType === 'theArena') { treeCount = 0; }
The Arena has no trees—it's a bare circular platform for boss battles. Empty arena for unfettered combat
x: random(-4000, 4000), z: random(-4000, 4000),
Scatters trees across an 8000×8000 unit square (-4000 to +4000 on both axes). Large area keeps targets visible and interesting
size: random(treeSizeRange[0], treeSizeRange[1]),
Gives each tree a random size within the world's specified range, adding visual variety
offsetY: treeOffsetY
Stores the vertical offset (0 for most worlds, -150 for Space Outpost) to position structures correctly

drawEnvironment()

drawEnvironment() handles two tasks: drawing the ground plane with world-specific colors, and rendering all generated trees. The Arena gets special treatment with a circular platform and walls instead of an infinite plane. The key design pattern is: generateEnvironmentTrees() creates the data (tree array), and drawEnvironment() renders it. This separation makes it easy to regenerate the environment when switching worlds without re-writing drawing code.

🔬 This switch sets ground color per world. What happens if you swap the desert and arctic colors—does the desert look icy? Or add a new case for 'forest': groundColor = color(200, 0, 0) to turn the forest floor red?

  switch(worldType) {
    case 'desert':
      groundColor = color(240, 220, 160);
      break;
    case 'arctic':
      groundColor = color(220, 240, 255);
      break;
function drawEnvironment(worldType) {
  push();
  rotateX(HALF_PI);

  let groundColor;
  let arenaRadius = 2000; // Radius for the arena platform

  switch(worldType) {
    case 'desert':
      groundColor = color(240, 220, 160);
      break;
    case 'arctic':
      groundColor = color(220, 240, 255);
      break;
    case 'swamp':
      groundColor = color(50, 70, 40);
      break;
    case 'volcanic':
      groundColor = color(60, 30, 10);
      break;
    case 'spaceOutpost':
      groundColor = color(80, 80, 100);
      break;
    case 'wasteland':
      groundColor = color(100, 90, 80); // Dusty brown
      break;
    case 'alienJungle':
      groundColor = color(50, 100, 70); // Dark greenish-purple
      break;
    case 'underwater':
      groundColor = color(0, 100, 150); // Deep blue ocean floor
      break;
    case 'theArena': // The Arena
      groundColor = color(50, 50, 60);
      break;
    default: // Forest (default)
      groundColor = color(40, 100, 40);
      break;
  }

  // Draw Arena specific structure
  if (worldType === 'theArena') {
    translate(0, 0, groundY); // Ensure platform is at ground level
    fill(groundColor);
    noStroke();
    cylinder(arenaRadius, 50); // Thick circular platform

    // Draw arena walls
    fill(80, 80, 90);
    noStroke();
    cylinder(arenaRadius + 50, 300); // Outer wall
    translate(0, 0, 150); // Move up slightly for inner wall
    fill(groundColor); // Same color as ground to make it look like a hollow cylinder
    cylinder(arenaRadius, 300); // Inner wall
  } else {
    // Draw standard plane for other worlds
    fill(groundColor);
    noStroke(); // No stroke on the plane
    plane(15000, 15000); // Larger plane for other worlds
  }

  pop();

  // Draw environment trees/structures for other worlds, not 'theArena'
  if (worldType !== 'theArena') {
    for (let t of trees) {
      push();
      translate(t.x, t.offsetY, t.z);
      scale(t.size);
      drawWorldTree(worldType); // Should now be defined
      pop();
    }
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

calculation Ground Color Switch switch(worldType) { case 'desert': groundColor = color(240, 220, 160); break; ... }

Sets the ground plane color based on world type. Desert = sandy yellow, arctic = icy blue, swamp = dark green, etc.

conditional Arena Platform Drawing if (worldType === 'theArena') { cylinder(arenaRadius, 50); ... }

If in The Arena, draws a thick circular platform and outer walls instead of a large ground plane

for-loop Tree Rendering Loop for (let t of trees) { push(); translate(t.x, t.offsetY, t.z); scale(t.size); drawWorldTree(worldType); pop(); }

Loops through all tree objects generated by generateEnvironmentTrees() and renders each one at its position with its scale

rotateX(HALF_PI);
Rotates the coordinate system 90° around the X-axis so the plane (normally vertical) lies flat as the ground
switch(worldType) { case 'desert': groundColor = color(240, 220, 160); ... }
Selects a ground color based on world type. Each world has a unique sandy, icy, murky, or rocky tone
fill(groundColor); noStroke(); plane(15000, 15000);
Draws a huge flat square (15000×15000 units) as the ground. No stroke keeps it smooth. Color was set by the switch above
if (worldType === 'theArena') { cylinder(arenaRadius, 50); }
If in The Arena, draws a thick circular disk (radius 2000, height 50) as the arena floor instead of a large plane
cylinder(arenaRadius + 50, 300); // Outer wall
Draws outer arena wall (radius 2050, height 300) to create a visible boundary. Players see the dark arena walls
for (let t of trees) { translate(t.x, t.offsetY, t.z); scale(t.size); drawWorldTree(worldType); }
For each tree object, moves to its (x, offsetY, z) position, scales it, and calls drawWorldTree() to render the specific model for that world

setWorldBackgroundAndLighting()

setWorldBackgroundAndLighting() is pure atmosphere. By varying background color, ambient light, and directional light per world, the entire visual tone changes without modifying any geometry. Desert feels hot and bright; underwater feels cool and dim; volcanic feels ominous. This is a key game dev technique: lighting is free mood-setting that dramatically enhances immersion. The directional light's direction vector (the last three parameters) controls shadow direction.

🔬 This sets desert lighting: warm yellowy background and ambient light. What happens if you change background to (100, 50, 20) (dark red)—does it look more volcanic or alien? Or change ambientLight to (255, 0, 0) for a red tint?

  switch(worldType) {
    case 'desert':
      background(240, 220, 160); // Yellowish sky
      ambientLight(150, 120, 80);
      directionalLight(255, 220, 180, 0, 1, -0.5);
function setWorldBackgroundAndLighting(worldType) {
  switch(worldType) {
    case 'desert':
      background(240, 220, 160); // Yellowish sky
      ambientLight(150, 120, 80);
      directionalLight(255, 220, 180, 0, 1, -0.5);
      break;
    case 'arctic':
      background(180, 220, 255); // Icy blue sky
      ambientLight(180, 200, 220);
      directionalLight(240, 240, 255, 0, 1, -0.5);
      break;
    case 'swamp':
      background(60, 80, 90); // Dark, murky sky
      ambientLight(60, 80, 90);
      directionalLight(100, 120, 130, 0, 1, -0.5);
      break;
    case 'volcanic':
      background(100, 50, 20); // Reddish-orange smoky sky
      ambientLight(100, 50, 20);
      directionalLight(255, 150, 0, 0, 1, -0.5);
      break;
    case 'spaceOutpost':
      background(20, 20, 40); // Dark space sky
      ambientLight(80, 80, 100);
      directionalLight(200, 200, 255, 0, 1, -0.5);
      break;
    case 'wasteland':
      background(150, 140, 130); // Dusty, muted sky
      ambientLight(100, 90, 80);
      directionalLight(200, 180, 160, 0, 1, -0.5);
      break;
    case 'alienJungle':
      background(80, 20, 120); // Deep purple sky
      ambientLight(100, 150, 200); // Blueish ambient light
      directionalLight(150, 255, 100, 0, 1, -0.5); // Greenish directional light
      break;
    case 'underwater':
      background(0, 50, 100); // Dark blue ocean
      ambientLight(50, 100, 150); // Lighter blue ambient light
      directionalLight(255, 255, 255, 0.2, -1, 0.2); // Light rays from above
      break;
    case 'theArena': // The Arena
      background(30, 30, 40); // Dark, intense sky
      ambientLight(150, 100, 50); // Dramatic ambient light
      directionalLight(255, 180, 100, 0, 1, -0.5); // Warm, strong directional light
      break;
    default: // Forest (default)
      background(120, 190, 240);
      ambientLight(120);
      directionalLight(255, 255, 240, 0.5, 1, -0.5);
      break;
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

switch-case Sky Color and Lighting switch(worldType) { case 'desert': background(240, 220, 160); ambientLight(150, 120, 80); directionalLight(255, 220, 180, 0, 1, -0.5); ... }

Sets background color (sky), ambient light (base illumination), and directional light (main light source with direction) per world

case 'desert': background(240, 220, 160);
Desert sky is warm yellowish (high R and G, medium B). Creates a hot, sandy atmosphere
ambientLight(150, 120, 80);
Ambient light is the base illumination in the scene. Desert ambient is warm (brownish), making everything glow with warmth
directionalLight(255, 220, 180, 0, 1, -0.5);
Directional light is a single strong light source (like the sun). RGB sets color (warm yellow), and (0, 1, -0.5) sets direction (mostly downward). This casts dramatic shadows
case 'arctic': background(180, 220, 255);
Arctic sky is icy blue (high B, medium G, lower R). Cool and crisp
ambientLight(180, 200, 220);
Arctic ambient light is cool white-blue, making objects glow with cold tones
case 'underwater': directionalLight(255, 255, 255, 0.2, -1, 0.2);
Underwater light comes from above at an angle (0.2, -1, 0.2 = mostly from above). Creates the effect of light filtering down through water
default: background(120, 190, 240); ambientLight(120); directionalLight(255, 255, 240, 0.5, 1, -0.5);
Forest (default) has a bright blue sky, neutral ambient light, and directional light from above-front. Classic outdoor lighting

📦 Key Variables

player object

Stores the player's 3D position (x, y, z), vertical velocity (vy), and ground state (isOnGround) for physics

let player = { x: 0, y: -150, z: 0, vy: 0, isOnGround: true };
yaw number

The player's horizontal rotation angle in radians (controls left/right view direction)

let yaw = -Math.PI / 2;
pitch number

The player's vertical rotation angle in radians (controls up/down view direction)

let pitch = 0;
moveSpeed number

How many pixels the player moves per frame when pressing WASD

let moveSpeed = 6;
jumpForce number

The initial upward velocity applied when jumping

let jumpForce = 15;
gravity number

How fast gravity pulls the player down each frame

let gravity = 1.0;
groundY number

The Y-coordinate of the ground level (where the player lands when falling)

let groundY = -150;
ducks array

Array of Target objects representing flying bird enemies to shoot

let ducks = [];
trees array

Array of tree/structure objects, each with position (x, z), size, and offsetY

let trees = [];
particles array

Array of Particle objects that animate and fade after explosions or gunfire

let particles = [];
score number

The player's current score, accumulated by killing targets and bosses

let score = 0;
recoil number

Current gun recoil amount, used to displace the gun and camera when firing

let recoil = 0;
lastFired number

The frameCount when the gun was last fired, used to enforce fire-rate limits

let lastFired = 0;
boss object (Boss)

Holds the current active boss enemy (null if no boss is spawned)

let boss = null;
bossThreshold number

The score needed to trigger a boss spawn (increases after each boss defeat)

let bossThreshold = 2500;
playerHealth number

The player's current health points (game over when reaching 0)

let playerHealth = 100;
playerMaxHealth number

The player's maximum health (used to fill health bar)

let playerMaxHealth = 100;
gameOver boolean

Flag indicating whether the game has ended (player dead or arena cleared)

let gameOver = false;
invincibilityTimer number

Countdown timer for invincibility frames after taking damage

let invincibilityTimer = 0;
invincibilityDuration number

How many frames of invincibility are granted after being hit

let invincibilityDuration = 60;
activeGun string

The currently equipped gun type: 'pistol', 'shotgun', 'ar', 'sniper', 'minigun', or 'laser'

let activeGun = 'pistol';
activeBird string

The currently selected bird target type: 'duck', 'pigeon', 'crow', 'goose', 'phoenix', or 'dragon'

let activeBird = 'duck';
activeWorld string

The currently active world/level: 'forest', 'desert', 'arctic', etc.

let activeWorld = 'forest';
items object

Shop data structure containing guns, birds, and worlds with their costs, descriptions, and stats

let items = { guns: { pistol: { cost: 0, unlocked: true, ... }, ... }, birds: { ... }, worlds: { ... } };
arenaBosses array

Array of all Boss objects active in The Arena mode (can have multiple bosses fighting simultaneously)

let arenaBosses = [];
arenaSpawnTimer number

Countdown timer before bosses spawn in The Arena (creates dramatic pause before battle)

let arenaSpawnTimer = 0;
arenaSpawnDelay number

Number of frames to wait before spawning arena bosses (300 frames = 5 seconds at 60fps)

let arenaSpawnDelay = 300;

🔧 Potential Improvements (7)

Here are some ways this code could be enhanced:

BUG fireGun() raycasting

Raycasting can miss fast-moving targets or penetrate through multiple targets inconsistently if the sniper rifle logic is mishandled

💡 Add clearer comments distinguishing arena vs. single-boss vs. duck hit logic. Consider consolidating raycasting code into a helper function to reduce duplication and bugs

PERFORMANCE draw() main loop

Arena bosses and ducks are looped separately; if both are active (shouldn't be), it causes redundant iteration. Additionally, raycasting happens every frame for all targets

💡 Use a single 'enemies' array that holds either ducks or bosses, not both. Optimize raycasting by breaking early when a non-sniper gun hits a target. Consider spatial partitioning for large maps

PERFORMANCE drawEnvironment() and drawWorldTree()

Trees are regenerated with random sizes/positions every time a world is switched, but drawWorldTree() is called on every frame for every tree, recalculating rotations and colors

💡 Cache tree rendering using createGraphics() to pre-render complex tree models, or use instancing. Pre-compute tree vertices to reduce per-frame calculations

STYLE Variables and naming

Mix of camelCase and inconsistent naming (e.g., 'activeGun' vs 'activeBird' vs 'activeWorld'). Also, items.guns, items.birds, items.worlds could be accessed more safely

💡 Use consistent camelCase throughout. Add a getItem(category, key) helper function to safely access shop items with error checking

FEATURE Boss AI and combat

Bosses all hover and slowly lerp toward the player; no variation in boss behavior, attack patterns, or difficulty scaling

💡 Add boss-type-specific AI: treant roots slow the player, scorpion tail swipes in an arc, yeti charges, etc. Implement attack pattern variety and difficulty scaling based on score progression

FEATURE Player progression

Health is fixed at 100; no upgrades or power-ups during gameplay. Bosses are always equally difficult regardless of player score

💡 Add shop upgrades for health, damage, fire rate. Implement difficulty scaling so later bosses are proportionally harder. Add consumable power-ups (invincibility, ammo, healing)

BUG setupAudio()

Audio synths are started globally with .start() in setupAudio(), but there's no cleanup. If the sketch is reset, audio may glitch or layer

💡 Store synth/oscillator references and add a stopAllAudio() function to call on game over or reload. Use p5.sound's disposal methods to prevent memory leaks

🔄 Code Flow

Code flow showing setup, draw, firegun, handleinput, drawgun, target, boss, generateenvironmenttrees, drawenvironment, setworldbackgroundandlighting

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

graph TD start[Start] --> setup[setup] setup --> canvas-creation[Canvas Creation] setup --> health-initialization[Health Initialization] setup --> pointer-lock[Pointer Lock Event] setup --> environment-generation[Environment Generation] setup --> duck-spawning[Duck Spawning] setup --> draw[draw loop] click setup href "#fn-setup" click canvas-creation href "#sub-canvas-creation" click health-initialization href "#sub-health-initialization" click pointer-lock href "#sub-pointer-lock" click environment-generation href "#sub-environment-generation" click duck-spawning href "#sub-duck-spawning" draw --> game-over-check[Game Over State Check] draw --> pointer-lock-check[Pointer Lock State] draw --> input-handling[Player Input Processing] draw --> gravity-physics[Gravity and Jump Physics] draw --> invincibility-countdown[Invincibility Timer] draw --> camera-setup[3D Camera and View] draw --> arena-logic[The Arena Gameplay] draw --> boss-spawning[Single-World Boss Threshold] draw --> duck-logic[Duck Spawning and Rendering] draw --> particle-system[Particle Update and Removal] click draw href "#fn-draw" click game-over-check href "#sub-game-over-check" click pointer-lock-check href "#sub-pointer-lock-check" click input-handling href "#sub-input-handling" click gravity-physics href "#sub-gravity-physics" click invincibility-countdown href "#sub-invincibility-countdown" click camera-setup href "#sub-camera-setup" click arena-logic href "#sub-arena-logic" click boss-spawning href "#sub-boss-spawning" click duck-logic href "#sub-duck-logic" click particle-system href "#sub-particle-system" input-handling --> mouse-look[Mouse Look (Yaw and Pitch)] input-handling --> wasd-movement[WASD Key Movement] input-handling --> direction-vectors[Walking Direction Vectors] input-handling --> pitch-constraint[Pitch Limiting] click mouse-look href "#sub-mouse-look" click wasd-movement href "#sub-wasd-movement" click direction-vectors href "#sub-direction-vectors" click pitch-constraint href "#sub-pitch-constraint" gravity-physics --> world-bounds[World Boundary Clamping] boss-spawning --> boss-constructor[Boss Initialization] boss-spawning --> boss-update[Boss AI and Attack] boss-spawning --> boss-draw[Boss Rendering] boss-spawning --> boss-takedamage[Damage Handling] click boss-constructor href "#sub-boss-constructor" click boss-update href "#sub-boss-update" click boss-draw href "#sub-boss-draw" click boss-takedamage href "#sub-boss-takedamage" duck-logic --> duck-hit-loop[Duck Collision Loop] duck-logic --> reset[Target Reset] click duck-hit-loop href "#sub-duck-hit-loop" click reset href "#sub-reset" particle-system --> fire-rate-check[Fire Rate Limiting] particle-system --> recoil-setup[Recoil and Sound] particle-system --> damage-and-particles[Hit Response] click fire-rate-check href "#sub-fire-rate-check" click recoil-setup href "#sub-recoil-setup" click damage-and-particles href "#sub-damage-and-particles" arena-logic --> arena-boss-check[Arena Boss Collision Loop] draw --> drawgun[drawGun] drawgun --> gun-positioning[Gun Screen Position and Rotation] drawgun --> hand-drawing[Hand Model] drawgun --> gun-switch[Gun Model Selection] drawgun --> muzzle-flash[Muzzle Flash Effect] click drawgun href "#fn-drawgun" click gun-positioning href "#sub-gun-positioning" click hand-drawing href "#sub-hand-drawing" click gun-switch href "#sub-gun-switch" click muzzle-flash href "#sub-muzzle-flash" draw --> drawenvironment[drawEnvironment] drawenvironment --> ground-plane[Ground Color Switch] drawenvironment --> tree-drawing[Tree Rendering Loop] click drawenvironment href "#fn-drawenvironment" click ground-plane href "#sub-ground-plane" click tree-drawing href "#sub-tree-drawing" setup --> generateenvironmenttrees[generateEnvironmentTrees] generateenvironmenttrees --> tree-clear[Tree Array Reset] generateenvironmenttrees --> world-switch[World-Specific Tree Counts and Sizes] generateenvironmenttrees --> tree-generation-loop[Procedural Tree Placement] click generateenvironmenttrees href "#fn-generateenvironmenttrees" click tree-clear href "#sub-tree-clear" click world-switch href "#sub-world-switch" click tree-generation-loop href "#sub-tree-generation-loop" draw --> setworldbackgroundandlighting[setWorldBackgroundAndLighting] click setworldbackgroundandlighting href "#fn-setworldbackgroundandlighting"

❓ Frequently Asked Questions

What visual experience does the Wings Slayer sketch offer?

The Wings Slayer sketch creates a dynamic, immersive environment featuring a player navigating through a 3D landscape filled with ducks, trees, and various effects.

How can users engage with the Wings Slayer interactive sketch?

Users can control the player character's movements, shoot at targets, and manage resources such as health and weapons in a game-like format.

What creative coding techniques are showcased in the Wings Slayer sketch?

This sketch demonstrates techniques such as 3D rendering, physics simulation for movement and gravity, and audio integration for an engaging gameplay experience.

Preview

Wings Slayer (Lucas) - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Wings Slayer (Lucas) - Code flow showing setup, draw, firegun, handleinput, drawgun, target, boss, generateenvironmenttrees, drawenvironment, setworldbackgroundandlighting
Code Flow Diagram