Alpha Omega

Alpha Omega is a two-player cooperative battle game where a blue circle and red square team up to defeat a central boss enemy. Players move with keyboard controls, throw five types of projectiles (knives, daggers, fireballs, ice balls, and lightning) to damage the boss, manage cooldown timers, and avoid the boss's collision attacks while tracking health bars for all characters.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make the boss take more damage — Increase the knifeDamageAmount constant so each knife deals massive damage—now fewer hits will defeat the boss
  2. Make the blue circle move much faster — Increase circleSpeed so the blue circle zips around the canvas, making it easier to dodge the boss
  3. Make fireballs travel faster — When a fireball is created, pass a higher speed value so it zips toward the boss more quickly
  4. Give players more health — Increase the starting health values so players can take more hits before fading out
  5. Make the boss bob faster — Increase bobSpeed to make the boss's up-and-down motion more frantic and energetic
  6. Change the background color — Replace the black background with a different color to completely change the visual mood
Prefer the full editor? Open it there →

📖 About This Sketch

Alpha Omega is a fully-playable two-player cooperative battle game built with p5.js. Two players—a blue circle and a red square—spawn at the bottom of the screen and must work together to defeat a powerful boss enemy (Alpha Omega) positioned in the center. The game combines several advanced creative coding techniques: collision detection between circles and rectangles, image loading and rotation, class-based projectile systems, cooldown timer management, and visual feedback through health bars, invincibility flashing, and boss jiggle animations.

The code is organized into a game loop that updates player movement, projectile trajectories, collision detection, and cooldown timers every frame. You'll learn how to build a complete game structure with multiple classes (Knife, Dagger, FireBall, IceBall, StrongLightning), manage arrays of active projectiles, synchronize visual effects with game state, and create win/lose conditions. By studying this sketch, you'll understand the architecture needed for any multiplayer game, from player input handling to enemy AI feedback.

⚙️ How It Works

  1. When the sketch loads, preload() retrieves all the game images (Alpha Omega and the five projectile types), and setup() initializes the canvas and calls restartGame() to position both players at the bottom corners and reset all health and cooldown values to zero.
  2. Every frame, the draw() loop clears the black background and runs the game logic: the circle (W/A/S/D keys) and square (I/J/K/L keys) move within canvas bounds, and their positions are constrained to prevent leaving the screen.
  3. Collision detection runs continuously—two helper functions (rectCircleCollision and rectRectCollision) check if either player has touched Alpha Omega's hitbox; if so, that player takes 10 damage and becomes invincible for 2 seconds to prevent spam damage.
  4. When a player presses a projectile key (U for knife, O for dagger, Q for fireball, E for ice ball, Z for lightning), a new projectile object is created at that player's position and added to the corresponding array. Each projectile class has update() and display() methods that move it toward the boss and check for collision.
  5. When a projectile reaches Alpha Omega, it's marked inactive and removed from its array; Alpha Omega takes the appropriate damage (knives: 20, daggers/fireballs/ice/lightning: 30 each), and a jiggle animation triggers as visual feedback. If Alpha Omega's health reaches zero, the game ends with 'PLAYERS WIN!'
  6. When either player's health reaches zero, they fade out over several frames. If both players are fully transparent, the game ends with 'ALPHA OMEGA WINS!' and the player can press SPACE to restart.

🎓 Concepts You'll Learn

Collision detectionClass-based projectile systemsGame state managementCooldown timers with millis()Array management and deletionImage loading and rotationHealth bars and UIInvincibility frames

📝 Code Breakdown

preload()

preload() is a p5.js function that runs before setup() to load files like images and sounds. Without preload(), the game would try to draw images before they finished loading. p5.js waits for preload() to finish before moving forward.

function preload() {
  // Load the Alpha Omega.png image
  alphaOmegaImage = loadImage('Alpha Omega.png');
  // Load the Knife.png image
  knifeImage = loadImage('Knife.png');
  // Load the Dagger.png image
  daggerImage = loadImage('Dagger.png');
  // New: Load the Strong Fire Ball.png image
  strongFireBallImage = loadImage('Strong Fire Ball.png');
  // NEW: Load the Strong Ice.png image
  strongIceImage = loadImage('Strong Ice.png');
  // NEW: Load the Strong Lightning.png image
  strongLightningImage = loadImage('Strong Lightning.png');
}
Line-by-line explanation (6 lines)
alphaOmegaImage = loadImage('Alpha Omega.png');
Loads the boss character image from a file and stores it in a global variable so draw() can access it later
knifeImage = loadImage('Knife.png');
Loads the knife projectile image so the Knife class can display it when thrown
daggerImage = loadImage('Dagger.png');
Loads the dagger projectile image for the Dagger class to display
strongFireBallImage = loadImage('Strong Fire Ball.png');
Loads the fireball projectile image for the FireBall class
strongIceImage = loadImage('Strong Ice.png');
Loads the ice ball projectile image for the IceBall class
strongLightningImage = loadImage('Strong Lightning.png');
Loads the lightning projectile image for the StrongLightning class

setup()

setup() runs once when the sketch starts. Its job is to prepare the canvas and call initialization functions. By putting all the reset logic into restartGame(), the code can be reused both at startup and when the player presses SPACE after game over.

function setup() {
  createCanvas(windowWidth, windowHeight);
  // Tip: Use windowWidth/windowHeight for responsive canvas

  // Initial setup of player positions and health
  restartGame(); // Call restartGame to initialize all variables
}
Line-by-line explanation (2 lines)
createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window, automatically adjusting if the window is resized
restartGame();
Calls a helper function that initializes all game variables (player positions, health, cooldowns, projectile arrays) so the game is ready to play

draw()

draw() is the heart of every p5.js sketch—it runs 60 times per second. This function handles player movement input (keyIsDown), collision detection, projectile updates, and drawing. The structure shows how game engines work: input → physics/logic → collision → drawing. Notice how it separates game logic (updating) from rendering (drawing), which keeps complex code organized and debuggable.

🔬 The circle responds to WASD keys: W moves up, S moves down, A moves left, D moves right. What happens if you change all four `circleY -= circleSpeed` lines to `circleY -= circleSpeed * 2`? Why does that make sense?

    // --- Circle Logic (Movement, Boundary, Health, Fading) ---
    if (circleHealth > 0) { // Only process movement and collision if not defeated
      if (keyIsDown(87)) { // W key
        circleY -= circleSpeed;
      }
      if (keyIsDown(83)) { // S key
        circleY += circleSpeed;
      }
      if (keyIsDown(65)) { // A key
        circleX -= circleSpeed;
      }
      if (keyIsDown(68)) { // D key
        circleX += circleSpeed;
      }

🔬 This is where the circle takes damage when touching the boss. The `playerDamageAmount` variable controls how much damage (currently 10). What happens if you change `playerDamageAmount` to 1 in the constants at the top? Can you survive longer?

    // Check for circle collision with Alpha Omega ONLY if circle is alive and vulnerable
    if (circleHealth > 0 && !circleInvincible && rectCircleCollision(aoX, aoY, aoW, aoH, circleX, circleY, circleRadius)) {
      circleHealth -= playerDamageAmount;
function draw() {
  background(0); // Set background to black

  if (!isGameOver) { // Only run game logic if not game over
    // --- Circle Logic (Movement, Boundary, Health, Fading) ---
    if (circleHealth > 0) { // Only process movement and collision if not defeated
      if (keyIsDown(87)) { // W key
        circleY -= circleSpeed;
      }
      if (keyIsDown(83)) { // S key
        circleY += circleSpeed;
      }
      if (keyIsDown(65)) { // A key
        circleX -= circleSpeed;
      }
      if (keyIsDown(68)) { // D key
        circleX += circleSpeed;
      }

      // Circle Boundary Check
      circleX = constrain(circleX, circleRadius, width - circleRadius);
      circleY = constrain(circleY, circleRadius, height - circleRadius);
    } else {
      // If health is 0 or less, start fading out
      circleFading = true;
    }

    // Fade out the circle if fading flag is true
    if (circleFading) {
      circleAlpha -= fadeSpeed;
      circleAlpha = constrain(circleAlpha, 0, 255); // Ensure alpha stays between 0 and 255
      if (circleAlpha <= 0) {
        // Once fully transparent, stop fading and mark as gone
        circleFading = false;
      }
    }

    // --- Square Logic (Movement, Boundary, Health, Fading) ---
    if (squareHealth > 0) { // Only process movement and collision if not defeated
      if (keyIsDown(73)) { // I key
        squareY -= squareSpeed;
      }
      if (keyIsDown(75)) { // K key
        squareY += squareSpeed;
      }
      if (keyIsDown(74)) { // J key
        squareX -= squareSpeed;
      }
      if (keyIsDown(76)) { // L key
        squareX += squareSpeed;
      }

      // Square Boundary Check
      // Constrain based on half its size from the edges
      squareX = constrain(squareX, squareSize / 2, width - squareSize / 2);
      squareY = constrain(squareY, squareSize / 2, height - squareSize / 2);
    } else {
      // If health is 0 or less, start fading out
      squareFading = true;
    }

    // Fade out the square if fading flag is true
    if (squareFading) {
      squareAlpha -= fadeSpeed;
      squareAlpha = constrain(squareAlpha, 0, 255); // Ensure alpha stays between 0 and 255
      if (squareAlpha <= 0) {
        // Once fully transparent, stop fading and mark as gone
        squareFading = false;
      }
    }

    // --- Alpha Omega Hitbox Logic (stationary) ---
    const aoOriginalW = alphaOmegaImage.width * 0.5;
    const aoOriginalH = alphaOmegaImage.height * 0.5;
    // aoOriginalY is the center Y of the IMAGE *without* bobbing
    const aoOriginalY = height * 0.25;

    // Calculate hitbox dimensions and position to cut the bottom 20%
    const aoX = width / 2;
    const aoH = aoOriginalH * 0.8; // Hitbox height is 80% of original image height
    // To keep the hitbox's top edge in the same place, adjust aoY upwards
    // This aoY is for the HITBOX, so it does NOT include the bobOffset.
    const aoY = aoOriginalY - (aoOriginalH * 0.1);
    const aoW = aoOriginalW; // Hitbox width remains the same

    // Check for circle collision with Alpha Omega ONLY if circle is alive and vulnerable
    if (circleHealth > 0 && !circleInvincible && rectCircleCollision(aoX, aoY, aoW, aoH, circleX, circleY, circleRadius)) {
      circleHealth -= playerDamageAmount;
      circleInvincible = true;
      circleInvincibleTimer = millis(); // Record current time for invincibility
      console.log("Circle took damage! Health:", circleHealth, "Invincible until:", millis() + invincibilityDuration);
    }

    // Check for square collision with Alpha Omega ONLY if square is alive and vulnerable
    if (squareHealth > 0 && !squareInvincible && rectRectCollision(aoX, aoY, aoW, aoH, squareX, squareY, squareSize, squareSize)) {
      squareHealth -= playerDamageAmount;
      squareInvincible = true;
      squareInvincibleTimer = millis(); // Record current time for invincibility
      console.log("Square took damage! Health:", squareHealth, "Invincible until:", millis() + invincibilityDuration);
    }

    // Manage invincibility timers
    if (circleInvincible && millis() - circleInvincibleTimer > invincibilityDuration) {
      circleInvincible = false;
    }
    if (squareInvincible && millis() - squareInvincibleTimer > invincibilityDuration) {
      squareInvincible = false;
    }

    // --- Knife Logic ---
    // Loop backwards to safely remove knives from the array
    for (let i = knives.length - 1; i >= 0; i--) {
      let knife = knives[i];
      if (knife.active) {
        knife.display(); // Draw the knife
        let hit = knife.update(); // Update its position and check for target hit
        if (hit) {
          alphaOmegaHealth -= knifeDamageAmount; // Alpha Omega takes damage
          isAlphaOmegaJiggling = true; // Trigger jiggle effect!
          alphaOmegaJiggleTimer = millis(); // Record jiggle start time
          knife.active = false; // Mark knife for removal
          console.log("Alpha Omega took knife damage! Health:", alphaOmegaHealth);
          // Check for Alpha Omega defeat
          if (alphaOmegaHealth <= 0) {
            isGameOver = true;
            gameOverMessage = "PLAYERS WIN!";
            noLoop();
            console.log("Alpha Omega defeated! PLAYERS WIN!");
          }
        }
      } else {
        knives.splice(i, 1); // Remove inactive knife from array
      }
    }

    // --- Dagger Logic ---
    // Loop backwards to safely remove daggers from the array
    for (let i = daggers.length - 1; i >= 0; i--) {
      let dagger = daggers[i];
      if (dagger.active) {
        dagger.display(); // Draw the dagger
        let hit = dagger.update(); // Update its position and check for target hit
        if (hit) {
          alphaOmegaHealth -= daggerDamageAmount; // Alpha Omega takes dagger damage
          isAlphaOmegaJiggling = true; // Trigger jiggle effect!
          alphaOmegaJiggleTimer = millis(); // Record jiggle start time
          dagger.active = false; // Mark dagger for removal
          console.log("Alpha Omega took dagger damage! Health:", alphaOmegaHealth);
          // Check for Alpha Omega defeat
          if (alphaOmegaHealth <= 0) {
            isGameOver = true;
            gameOverMessage = "PLAYERS WIN!";
            noLoop();
            console.log("Alpha Omega defeated! PLAYERS WIN!");
          }
        }
      } else {
        daggers.splice(i, 1); // Remove inactive dagger from array
      }
    }

    // --- Fire Ball Logic (New!) ---
    // Loop backwards to safely remove fire balls from the array
    for (let i = fireBalls.length - 1; i >= 0; i--) {
      let fireBall = fireBalls[i];
      if (fireBall.active) {
        fireBall.display(); // Draw the fire ball
        let hit = fireBall.update(); // Update its position and check for target hit
        if (hit) {
          alphaOmegaHealth -= fireBallDamageAmount; // Alpha Omega takes fire ball damage
          isAlphaOmegaJiggling = true; // Trigger jiggle effect!
          alphaOmegaJiggleTimer = millis(); // Record jiggle start time
          fireBall.active = false; // Mark fire ball for removal
          console.log("Alpha Omega took fireball damage! Health:", alphaOmegaHealth);
          // Check for Alpha Omega defeat
          if (alphaOmegaHealth <= 0) {
            isGameOver = true;
            gameOverMessage = "PLAYERS WIN!";
            noLoop();
            console.log("Alpha Omega defeated! PLAYERS WIN!");
          }
        }
      } else {
        fireBalls.splice(i, 1); // Remove inactive fire ball from array
      }
    }

    // NEW: Ice Ball Logic
    // Loop backwards to safely remove ice balls from the array
    for (let i = ices.length - 1; i >= 0; i--) {
      let iceBall = ices[i];
      if (iceBall.active) {
        iceBall.display(); // Draw the ice ball
        let hit = iceBall.update(); // Update its position and check for target hit
        if (hit) {
          alphaOmegaHealth -= iceBallDamageAmount; // Alpha Omega takes ice ball damage
          isAlphaOmegaJiggling = true; // Trigger jiggle effect!
          alphaOmegaJiggleTimer = millis(); // Record jiggle start time
          iceBall.active = false; // Mark ice ball for removal
          console.log("Alpha Omega took iceball damage! Health:", alphaOmegaHealth);
          // Check for Alpha Omega defeat
          if (alphaOmegaHealth <= 0) {
            isGameOver = true;
            gameOverMessage = "PLAYERS WIN!";
            noLoop();
            console.log("Alpha Omega defeated! PLAYERS WIN!");
          }
        }
      } else {
        ices.splice(i, 1); // Remove inactive ice ball from array
      }
    }

    // NEW: Lightning Logic
    // Loop backwards to safely remove lightnings from the array
    for (let i = lightnings.length - 1; i >= 0; i--) {
      let lightning = lightnings[i];
      if (lightning.active) {
        lightning.display(); // Draw the lightning
        let hit = lightning.update(); // Update its position and check for target hit
        if (hit) {
          alphaOmegaHealth -= lightningDamageAmount; // Alpha Omega takes lightning damage
          isAlphaOmegaJiggling = true; // Trigger jiggle effect!
          alphaOmegaJiggleTimer = millis(); // Record jiggle start time
          lightning.active = false; // Mark lightning for removal
          console.log("Alpha Omega took lightning damage! Health:", alphaOmegaHealth);
          // Check for Alpha Omega defeat
          if (alphaOmegaHealth <= 0) {
            isGameOver = true;
            gameOverMessage = "PLAYERS WIN!";
            noLoop();
            console.log("Alpha Omega defeated! PLAYERS WIN!");
          }
        }
      } else {
        lightnings.splice(i, 1); // Remove inactive lightning from array
      }
    }


    // Manage Alpha Omega jiggling timer
    if (isAlphaOmegaJiggling && millis() - alphaOmegaJiggleTimer > alphaOmegaJiggleDuration) {
      isAlphaOmegaJiggling = false; // Stop jiggling
    }

    // Game Over Condition Check for Players
    if (circleAlpha <= 0 && squareAlpha <= 0) {
      isGameOver = true;
      gameOverMessage = "ALPHA OMEGA WINS!";
      noLoop(); // Stop the draw loop
      console.log("Both players defeated! ALPHA OMEGA WINS!");
    }
  }

  // --- Drawing Section (always draw, even if game is over, to show final state) ---

  // Only draw the circle if it's not completely faded out
  if (circleAlpha > 0) {
    noStroke(); // No border for the circle
    // If invincible, flash the circle
    if (circleInvincible && frameCount % 10 < 5) {
      fill(255, 255, 255, circleAlpha); // White with current alpha
    } else {
      fill(173, 216, 230, circleAlpha); // Light blue with current alpha
    }
    circle(circleX, circleY, circleRadius * 2); // Draw circle with diameter
    drawHealthBar(circleX, circleY - circleRadius - 10, circleHealth, 100);

    // New: Draw cooldown bars above the blue circle's health bar
    const barWidth = 100; // Same width as health bar
    const barHeight = 10; // Slightly thinner cooldown bar
    const padding = 5; // Space between health bar and cooldown bar

    // Fire Ball Cooldown Bar (highest)
    let fireBallCooldownRemaining = fireBallCooldownTimer + fireBallCooldownDuration - millis();
    if (fireBallCooldownRemaining > 0) {
      let fillWidth = map(fireBallCooldownRemaining, fireBallCooldownDuration, 0, 0, barWidth);
      fill(150); // Gray for cooldown
      rectMode(CENTER);
      let fbBarY = circleY - circleRadius - 10 - (barHeight * 3) - (padding * 3);
      rect(circleX, fbBarY, barWidth, barHeight); // Background bar
      fill(255, 150, 0); // Orange-yellow for fireball cooldown progress
      rect(circleX - (barWidth - fillWidth) / 2, fbBarY, fillWidth, barHeight); // Progress bar
      stroke(255); // White border
      noFill();
      rect(circleX, fbBarY, barWidth, barHeight); // Border
      noStroke();
    }

    // Ice Ball Cooldown Bar (middle)
    let iceBallCooldownRemaining = iceBallCooldownTimer + iceBallCooldownDuration - millis();
    if (iceBallCooldownRemaining > 0) {
      let fillWidth = map(iceBallCooldownRemaining, iceBallCooldownDuration, 0, 0, barWidth);
      fill(150); // Gray for cooldown
      rectMode(CENTER);
      let ibBarY = circleY - circleRadius - 10 - (barHeight * 2) - (padding * 2);
      rect(circleX, ibBarY, barWidth, barHeight); // Background bar
      fill(0, 150, 255); // Blue for iceball cooldown progress
      rect(circleX - (barWidth - fillWidth) / 2, ibBarY, fillWidth, barHeight); // Progress bar
      stroke(255); // White border
      noFill();
      rect(circleX, ibBarY, barWidth, barHeight); // Border
      noStroke();
    }

    // Lightning Cooldown Bar (lowest of the three, above health)
    let lightningCooldownRemaining = lightningCooldownTimer + lightningCooldownDuration - millis();
    if (lightningCooldownRemaining > 0) {
      let fillWidth = map(lightningCooldownRemaining, lightningCooldownDuration, 0, 0, barWidth);
      fill(150); // Gray for cooldown
      rectMode(CENTER);
      let lBarY = circleY - circleRadius - 10 - barHeight - padding;
      rect(circleX, lBarY, barWidth, barHeight); // Background bar
      fill(255, 255, 0); // Yellow for lightning cooldown progress
      rect(circleX - (barWidth - fillWidth) / 2, lBarY, fillWidth, barHeight); // Progress bar
      stroke(255); // White border
      noFill();
      rect(circleX, lBarY, barWidth, barHeight); // Border
      noStroke();
    }
  }

  // Only draw the square if it's not completely faded out
  if (squareAlpha > 0) {
    noStroke(); // No border for the square
    // If invincible, flash the square
    if (squareInvincible && frameCount % 10 < 5) {
      fill(255, 255, 255, squareAlpha); // White with current alpha
    } else {
      fill(255, 0, 0, squareAlpha); // Red with current alpha
    }
    rectMode(CENTER); // Draw rectangle from its center
    rect(squareX, squareY, squareSize, squareSize);
    drawHealthBar(squareX, squareY - squareSize / 2 - 10, squareHealth, 100);

    // New: Draw cooldown bars above the red square's health bar
    const barWidth = 100; // Same width as health bar
    const barHeight = 10; // Slightly thinner cooldown bar
    const padding = 5; // Space between health bar and cooldown bar

    // Knife Cooldown Bar
    let knifeCooldownRemaining = knifeCooldownTimer + knifeCooldownDuration - millis();
    if (knifeCooldownRemaining > 0) {
      let fillWidth = map(knifeCooldownRemaining, knifeCooldownDuration, 0, 0, barWidth);
      fill(150); // Gray for cooldown
      rectMode(CENTER);
      rect(squareX, squareY - squareSize / 2 - 10 - barHeight - padding, barWidth, barHeight); // Background bar
      fill(255, 100, 0); // Orange for knife cooldown progress
      rect(squareX - (barWidth - fillWidth) / 2, squareY - squareSize / 2 - 10 - barHeight - padding, fillWidth, barHeight); // Progress bar
      stroke(255); // White border
      noFill();
      rect(squareX, squareY - squareSize / 2 - 10 - barHeight - padding, barWidth, barHeight); // Border
      noStroke();
    }

    // Dagger Cooldown Bar
    let daggerCooldownRemaining = daggerCooldownTimer + daggerCooldownDuration - millis();
    if (daggerCooldownRemaining > 0) {
      let fillWidth = map(daggerCooldownRemaining, daggerCooldownDuration, 0, 0, barWidth);
      fill(150); // Gray for cooldown
      rectMode(CENTER);
      rect(squareX, squareY - squareSize / 2 - 10 - (barHeight * 2) - (padding * 2), barWidth, barHeight); // Background bar
      fill(100, 100, 255); // Blue for dagger cooldown progress
      rect(squareX - (barWidth - fillWidth) / 2, squareY - squareSize / 2 - 10 - (barHeight * 2) - (padding * 2), fillWidth, barHeight); // Progress bar
      stroke(255); // White border
      noFill();
      rect(squareX, squareY - squareSize / 2 - 10 - (barHeight * 2) - (padding * 2), barWidth, barHeight); // Border
      noStroke();
    }
  }

  // --- Draw the Alpha Omega image (with bobbing and jiggling) ---
  imageMode(CENTER);
  // Calculate bobbing offset for the IMAGE *only*
  let bobOffset = sin(frameCount * bobSpeed) * bobAmplitude;
  // Calculate jiggling offset for the IMAGE *only*
  let jiggleOffset = 0;
  if (isAlphaOmegaJiggling) {
    // Oscillate between -jiggleAmount and +jiggleAmount
    jiggleOffset = sin(frameCount * 0.5) * alphaOmegaJiggleAmount; // Adjust 0.5 for jiggle speed
  }

  // Use the original image Y position + bobOffset for drawing
  // Add jiggleOffset to the X position
  image(alphaOmegaImage, width / 2 + jiggleOffset, height * 0.25 + bobOffset, alphaOmegaImage.width * 0.5, alphaOmegaImage.height * 0.5);

  // --- Visualize Alpha Omega hitbox with a red border (stationary) ---
  const aoOriginalW = alphaOmegaImage.width * 0.5;
  const aoOriginalH = alphaOmegaImage.height * 0.5;
  const aoOriginalY = height * 0.25; // Base Y for hitbox calculation
  const aoX = width / 2;
  const aoH = aoOriginalH * 0.8;
  // This aoY is for the HITBOX border, so it does NOT include the bobOffset.
  const aoY = aoOriginalY - (aoOriginalH * 0.1);
  const aoW = aoOriginalW;
  noFill();
  stroke(255, 0, 0); // Red border
  rect(aoX, aoY, aoW, aoH);
  noStroke(); // Reset stroke for other drawings
  fill(0); // Reset fill for other drawings (or specific colors for players)

  // --- Draw Alpha Omega's health bar in the top-left corner ---
  // Using a barWidth of 200 and barHeight of 20, with 20 pixels padding from the edge
  // The drawHealthBar function takes x, y as the center of the bar
  const aoBarWidth = 200;
  const aoBarHeight = 20;
  const aoPadding = 20;
  const aoBarX = aoPadding + aoBarWidth / 2;
  const aoBarY = aoPadding + aoBarHeight / 2;
  drawHealthBar(aoBarX, aoBarY, alphaOmegaHealth, 10000, aoBarWidth, aoBarHeight);


  // Game Over Screen
  if (isGameOver) {
    fill(255);
    textSize(50);
    textAlign(CENTER, CENTER);
    text(gameOverMessage, width / 2, height / 2);
    textSize(20);
    text("Press SPACE to Restart", width / 2, height / 2 + 60);
  }
}
Line-by-line explanation (28 lines)

🔧 Subcomponents:

conditional Circle Player Movement if (keyIsDown(87)) { // W key circleY -= circleSpeed;

Responds to W/A/S/D key presses to move the blue circle in four directions

conditional Circle-Boss Collision Detection if (circleHealth > 0 && !circleInvincible && rectCircleCollision(aoX, aoY, aoW, aoH, circleX, circleY, circleRadius))

Checks if the blue circle touched Alpha Omega and applies damage if not invincible

for-loop Projectile Update Loops for (let i = knives.length - 1; i >= 0; i--) {

Loops through all projectiles in reverse order, updating their positions and checking for boss hits

conditional Game Over Condition if (circleAlpha <= 0 && squareAlpha <= 0)

Ends the game when both players have faded out completely

background(0);
Clears the entire canvas to black, erasing all previous drawings and starting fresh for this frame
if (!isGameOver) {
Only runs game logic (movement, collisions, projectiles) if the game is still active—skips this block when the game is over
if (keyIsDown(87)) { // W key circleY -= circleSpeed;
Checks if the W key is currently pressed; if so, moves the blue circle up by subtracting from circleY
circleX = constrain(circleX, circleRadius, width - circleRadius);
Clamps the circle's X position so it never goes beyond the left or right edges of the canvas
if (circleFading) { circleAlpha -= fadeSpeed;
If the circle is defeated, reduces its transparency each frame until it becomes invisible
if (circleHealth > 0 && !circleInvincible && rectCircleCollision(aoX, aoY, aoW, aoH, circleX, circleY, circleRadius))
Checks three conditions: circle is alive, circle is not invincible, and circle is touching Alpha Omega's hitbox; if all true, circle takes damage
circleInvincibleTimer = millis();
Records the current time in milliseconds so the code later knows when invincibility started
if (circleInvincible && millis() - circleInvincibleTimer > invincibilityDuration)
Checks if invincibility has lasted longer than 2000 milliseconds; if so, removes invincibility
for (let i = knives.length - 1; i >= 0; i--) {
Loops through the knives array backwards (from last to first) so knives can be safely removed during iteration
knife.display();
Calls the knife's display() method to draw it on screen
let hit = knife.update();
Calls the knife's update() method, which moves it forward and returns true if it reached the boss
if (hit) { alphaOmegaHealth -= knifeDamageAmount;
If the knife hit the boss, subtract 20 from Alpha Omega's health
isAlphaOmegaJiggling = true; alphaOmegaJiggleTimer = millis();
Starts the jiggle animation to give visual feedback that the boss took damage
if (alphaOmegaHealth <= 0) { isGameOver = true; gameOverMessage = "PLAYERS WIN!";
Checks if the boss is defeated; if so, sets game over flag to true and displays the win message
knives.splice(i, 1);
Removes the inactive knife from the array by removing 1 element starting at index i
if (circleAlpha > 0) {
Only draws the blue circle if it hasn't faded completely out; this prevents drawing invisible players
if (circleInvincible && frameCount % 10 < 5) { fill(255, 255, 255, circleAlpha);
Makes the circle flash white (every other frame) when invincible to show it's protected
circle(circleX, circleY, circleRadius * 2);
Draws the circle at its current position with diameter = circleRadius * 2
drawHealthBar(circleX, circleY - circleRadius - 10, circleHealth, 100);
Draws a health bar above the circle showing its current health (max 100)
let fireBallCooldownRemaining = fireBallCooldownTimer + fireBallCooldownDuration - millis();
Calculates how many milliseconds remain until the fireball can be thrown again
if (fireBallCooldownRemaining > 0) {
Only draws the cooldown bar if the ability is still on cooldown
let fillWidth = map(fireBallCooldownRemaining, fireBallCooldownDuration, 0, 0, barWidth);
Converts the remaining cooldown time into a visual bar width—longer cooldown = wider bar
let bobOffset = sin(frameCount * bobSpeed) * bobAmplitude;
Uses a sine wave based on frameCount to create a smooth up-and-down bobbing motion for the boss
image(alphaOmegaImage, width / 2 + jiggleOffset, height * 0.25 + bobOffset, alphaOmegaImage.width * 0.5, alphaOmegaImage.height * 0.5);
Draws the boss image at the center horizontally, with bobbing on the Y-axis and jiggling on the X-axis when hit
const aoY = aoOriginalY - (aoOriginalH * 0.1);
Shifts the hitbox upward by 10% so it cuts off the bottom 20% of the image (avoiding legs or tail)
rect(aoX, aoY, aoW, aoH);
Draws a red rectangle border showing Alpha Omega's collision hitbox for debugging
drawHealthBar(aoBarX, aoBarY, alphaOmegaHealth, 10000, aoBarWidth, aoBarHeight);
Draws a large health bar in the top-left corner showing the boss's remaining health out of 10000
if (isGameOver) { fill(255); textSize(50); textAlign(CENTER, CENTER); text(gameOverMessage, width / 2, height / 2);
When the game ends, displays a large white text message in the center of the screen (either 'PLAYERS WIN!' or 'ALPHA OMEGA WINS!')

restartGame()

restartGame() is called once at startup from setup(), and again when the player presses SPACE after game over. By putting all initialization into one function, the code stays DRY (Don't Repeat Yourself) and makes it easy to adjust starting values in one place. This is a common pattern in game development.

function restartGame() {
  // Reset player positions
  circleX = circleRadius;
  circleY = height - circleRadius;
  squareX = width - squareSize / 2;
  squareY = height - squareSize / 2;

  // Reset player health
  circleHealth = 100;
  squareHealth = 100;
  alphaOmegaHealth = 10000; // Reset Alpha Omega's health

  // Reset player invincibility
  circleInvincible = false;
  circleInvincibleTimer = 0;
  squareInvincible = false;
  squareInvincibleTimer = 0;

  // Reset player alpha (transparency) and fading state
  circleAlpha = 255;
  circleFading = false;
  squareAlpha = 255;
  squareFading = false;

  // Reset Alpha Omega jiggling
  isAlphaOmegaJiggling = false;
  alphaOmegaJiggleTimer = 0;

  // Clear all existing projectiles
  knives = [];
  daggers = [];
  fireBalls = []; // New: Clear fire balls
  ices = [];      // NEW: Clear ice balls
  lightnings = []; // NEW: Clear lightning bolts

  // Reset cooldown timers
  knifeCooldownTimer = 0;
  daggerCooldownTimer = 0;
  fireBallCooldownTimer = 0; // New: Reset fireball cooldown
  iceBallCooldownTimer = 0;  // NEW: Reset iceball cooldown timer
  lightningCooldownTimer = 0; // NEW: Reset lightning cooldown timer

  // Reset game over flag
  isGameOver = false;
  gameOverMessage = "";

  loop(); // Resume the draw loop if it was stopped
}
Line-by-line explanation (11 lines)
circleX = circleRadius;
Places the blue circle at the bottom-left corner of the canvas (X = circleRadius = 25 pixels from the left edge)
circleY = height - circleRadius;
Places the blue circle at the bottom of the canvas (Y = window height minus 25 pixels)
squareX = width - squareSize / 2;
Places the red square at the bottom-right corner of the canvas (X = window width minus half the square size)
circleHealth = 100;
Resets both players' health to full (100 HP each)
alphaOmegaHealth = 10000;
Resets the boss's health to 10000 HP—this is its starting value
circleInvincible = false;
Removes the invincibility flag so the player can take damage again
circleAlpha = 255;
Sets transparency back to fully opaque (255 = fully visible, 0 = invisible)
knives = [];
Empties the knives array by creating a new empty array, removing all projectiles
knifeCooldownTimer = 0;
Resets all cooldown timers to zero so all abilities are immediately available
isGameOver = false;
Clears the game over flag so the game logic in draw() runs again
loop();
Resumes the draw loop if it was stopped by noLoop() during the game over screen

keyPressed()

keyPressed() is called once every time a key is pressed. Unlike keyIsDown() (used in draw() for continuous checking), keyPressed() detects single key presses—perfect for actions like throwing projectiles. Notice how the code checks `!isGameOver` before spawning projectiles: this prevents the player from throwing while dead or after the game ends. Also notice the cooldown system using `millis()`: it records when an ability was last used and prevents use again until enough time has passed.

🔬 This checks `squareHealth > 0` before allowing a knife to be thrown. Why do you think the game prevents dead players from throwing? What would happen if you removed the `if (squareHealth > 0)` check?

  // Spawn knife on 'U' press
  // Check for knife cooldown
  if (!isGameOver && (key === 'u' || key === 'U') && millis() - knifeCooldownTimer > knifeCooldownDuration) {
    // Check if the square is alive to spawn a knife
    if (squareHealth > 0) {
function keyPressed() {
  // Check if game is over and spacebar was pressed
  if (isGameOver && key === ' ') { // key === ' ' checks for spacebar
    restartGame();
  }

  // Spawn knife on 'U' press
  // Check for knife cooldown
  if (!isGameOver && (key === 'u' || key === 'U') && millis() - knifeCooldownTimer > knifeCooldownDuration) {
    // Check if the square is alive to spawn a knife
    if (squareHealth > 0) {
      let alphaOmegaCenterX = width / 2;
      let alphaOmegaCenterY = height * 0.25; // Alpha Omega's base Y position
      // Create a new Knife instance at the square's position, targeting Alpha Omega's center
      let newKnife = new Knife(squareX, squareY, alphaOmegaCenterX, alphaOmegaCenterY, 15); // Adjust knife speed as needed
      knives.push(newKnife); // Add it to the array of knives
      knifeCooldownTimer = millis(); // Reset knife cooldown timer
    }
  }

  // Spawn dagger on 'O' press
  // Check for dagger cooldown
  if (!isGameOver && (key === 'o' || key === 'O') && millis() - daggerCooldownTimer > daggerCooldownDuration) {
    // Check if the square is alive to spawn a dagger
    if (squareHealth > 0) {
      let alphaOmegaCenterX = width / 2;
      let alphaOmegaCenterY = height * 0.25; // Alpha Omega's base Y position
      // Create a new Dagger instance at the square's position, targeting Alpha Omega's center
      let newDagger = new Dagger(squareX, squareY, alphaOmegaCenterX, alphaOmegaCenterY, 20); // Adjust dagger speed as needed
      daggers.push(newDagger); // Add it to the array of daggers
      daggerCooldownTimer = millis(); // Reset dagger cooldown timer
    }
  }

  // New: Spawn fireball on 'Q' press
  // Check for fireball cooldown
  if (!isGameOver && (key === 'q' || key === 'Q') && millis() - fireBallCooldownTimer > fireBallCooldownDuration) {
    // Check if the circle is alive to spawn a fireball
    if (circleHealth > 0) {
      let alphaOmegaCenterX = width / 2;
      let alphaOmegaCenterY = height * 0.25; // Alpha Omega's base Y position
      let newFireBall = new FireBall(circleX, circleY, alphaOmegaCenterX, alphaOmegaCenterY, 10); // Adjust fireball speed as needed
      fireBalls.push(newFireBall);
      fireBallCooldownTimer = millis(); // Reset fireball cooldown timer
    }
  }

  // NEW: Spawn iceball on 'E' press
  // Check for iceball cooldown
  if (!isGameOver && (key === 'e' || key === 'E') && millis() - iceBallCooldownTimer > iceBallCooldownDuration) {
    // Check if the circle is alive to spawn an iceball
    if (circleHealth > 0) {
      let alphaOmegaCenterX = width / 2;
      let alphaOmegaCenterY = height * 0.25; // Alpha Omega's base Y position
      let newIceBall = new IceBall(circleX, circleY, alphaOmegaCenterX, alphaOmegaCenterY, 12); // Adjust iceball speed (e.g., slightly faster than fireball)
      ices.push(newIceBall);
      iceBallCooldownTimer = millis(); // Reset iceball cooldown timer
    }
  }

  // NEW: Spawn lightning bolt on 'Z' press
  // Check for lightning cooldown
  if (!isGameOver && (key === 'z' || key === 'Z') && millis() - lightningCooldownTimer > lightningCooldownDuration) {
    // Check if the circle is alive to spawn a lightning bolt
    if (circleHealth > 0) {
      let alphaOmegaCenterX = width / 2;
      let alphaOmegaCenterY = height * 0.25; // Alpha Omega's base Y position
      let newLightning = new StrongLightning(circleX, circleY, alphaOmegaCenterX, alphaOmegaCenterY, 15); // Adjust lightning speed
      lightnings.push(newLightning);
      lightningCooldownTimer = millis(); // Reset lightning cooldown timer
    }
  }
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

conditional Restart on Space if (isGameOver && key === ' ') { restartGame();

Detects SPACE key press when game is over and restarts the game

conditional Knife Spawn if (!isGameOver && (key === 'u' || key === 'U') && millis() - knifeCooldownTimer > knifeCooldownDuration)

Creates a new Knife projectile when U is pressed (if square is alive and cooldown is ready)

conditional Dagger Spawn if (!isGameOver && (key === 'o' || key === 'O') && millis() - daggerCooldownTimer > daggerCooldownDuration)

Creates a new Dagger projectile when O is pressed (if square is alive and cooldown is ready)

conditional Fireball Spawn if (!isGameOver && (key === 'q' || key === 'Q') && millis() - fireBallCooldownTimer > fireBallCooldownDuration)

Creates a new FireBall projectile when Q is pressed (if circle is alive and cooldown is ready)

if (isGameOver && key === ' ') {
Checks if the game has ended AND the spacebar was just pressed; if both are true, restart
if (!isGameOver && (key === 'u' || key === 'U') && millis() - knifeCooldownTimer > knifeCooldownDuration) {
Checks three things: game is not over, U key was pressed (uppercase or lowercase), and enough time has passed since the last knife was thrown
if (squareHealth > 0) {
Only allows the red square to throw a knife if it's still alive (health > 0)
let alphaOmegaCenterX = width / 2;
Sets the target X position to the center of the screen (where Alpha Omega always is)
let newKnife = new Knife(squareX, squareY, alphaOmegaCenterX, alphaOmegaCenterY, 15);
Creates a new Knife object at the square's current position, targeting Alpha Omega's center, with a speed of 15 pixels per frame
knives.push(newKnife);
Adds the newly created knife to the knives array so it will be updated and drawn every frame
knifeCooldownTimer = millis();
Records the current time, so the next knife can't be thrown until 2 seconds have passed
let newFireBall = new FireBall(circleX, circleY, alphaOmegaCenterX, alphaOmegaCenterY, 10);
Creates a fireball at the circle's position (only the blue circle can throw fireballs) with a speed of 10 pixels per frame
let newIceBall = new IceBall(circleX, circleY, alphaOmegaCenterX, alphaOmegaCenterY, 12);
Creates an ice ball at the circle's position with a speed of 12 pixels per frame (slightly faster than fireball)
let newLightning = new StrongLightning(circleX, circleY, alphaOmegaCenterX, alphaOmegaCenterY, 15);
Creates a lightning bolt at the circle's position with a speed of 15 pixels per frame

drawHealthBar()

drawHealthBar() is a reusable helper function that draws a green-over-gray bar showing current health as a fraction of max health. It's called three times per frame: once for the blue circle, once for the red square, and once for Alpha Omega. Notice the function has optional parameters (barWidth = 100, barHeight = 15)—if you don't pass them, it uses these defaults. The `map()` function is the key to converting a health number into a visual bar width.

function drawHealthBar(x, y, currentHealth, maxHealth, barWidth = 100, barHeight = 15) {
  // Ensure health doesn't go below 0 or above maxHealth
  currentHealth = constrain(currentHealth, 0, maxHealth);

  // Calculate the width of the green health portion
  let healthWidth = map(currentHealth, 0, maxHealth, 0, barWidth);

  rectMode(CENTER); // Draw health bar rectangles from their center
  noStroke();

  // Draw the background of the health bar (e.g., gray)
  fill(100); // Gray color
  rect(x, y, barWidth, barHeight);

  // Draw the current health portion (e.g., green)
  fill(0, 255, 0); // Green color
  // Position the green bar correctly within the gray bar
  // The green bar's center is shifted left by half the remaining width
  rect(x - (barWidth - healthWidth) / 2, y, healthWidth, barHeight);

  // Optional: Draw a border for the health bar
  stroke(255); // White border
  noFill();
  rect(x, y, barWidth, barHeight);

  // Optional: Display health text
  fill(255); // White text
  noStroke();
  textAlign(CENTER, CENTER);
  textSize(barHeight * 0.7);
  text(currentHealth, x, y);
}
Line-by-line explanation (9 lines)
currentHealth = constrain(currentHealth, 0, maxHealth);
Clamps the health value between 0 and maxHealth so it never displays negative or over-max
let healthWidth = map(currentHealth, 0, maxHealth, 0, barWidth);
Converts the health value to a bar width: if health is at max, healthWidth equals barWidth; if health is half, healthWidth is half the bar
rectMode(CENTER);
Changes how rectangles are drawn so that the x,y coordinates represent the center, not the top-left corner
fill(100);
Sets the fill color to gray (100 on a 0-255 scale) for the background bar
rect(x, y, barWidth, barHeight);
Draws the gray background rectangle showing the full health bar size
fill(0, 255, 0);
Changes fill color to green (R=0, G=255, B=0) for the health portion
rect(x - (barWidth - healthWidth) / 2, y, healthWidth, barHeight);
Draws the green health portion, repositioning it to the left so it fills from the left edge as health decreases
stroke(255);
Changes the outline color to white for the border
text(currentHealth, x, y);
Displays the numerical health value (e.g., '75') in the center of the health bar

windowResized()

windowResized() is a p5.js function that automatically runs whenever the browser window is resized. Without it, the canvas would stretch awkwardly. Notice it repositions the players to the bottom corners—this keeps them on-screen after a resize. This is important in responsive web design, where sketches must adapt to different screen sizes.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  // Called when preview panel is resized

  // Re-initialize positions to maintain bottom-left/right placement
  // Note: restartGame() also does this, but windowResized might happen mid-game
  if (!isGameOver) {
    circleX = circleRadius;
    circleY = height - circleRadius;
    squareX = width - squareSize / 2;
    squareY = height - squareSize / 2;
  }
}
Line-by-line explanation (3 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the p5.js canvas to match the new browser window dimensions
if (!isGameOver) {
Only repositions players if the game is still running—doesn't move them if game is over
circleX = circleRadius;
Snaps the circle back to the bottom-left corner (X = circleRadius from the left edge)

rectCircleCollision()

rectCircleCollision() checks if a rectangle and circle are touching. The algorithm works by finding the closest point on the rectangle to the circle's center, then measuring the distance from that point to the circle's center. If that distance is less than the circle's radius, they must be colliding. This is much more accurate than simple distance checks and handles corners correctly.

function rectCircleCollision(rectX, rectY, rectW, rectH, circleX, circleY, circleR) {
  // Find the closest point on the rectangle to the center of the circle
  let testX = circleX;
  let testY = circleY;

  // Clamp the circle's center to the rectangle's bounds
  // If circle is left of rect, clamp to rect's left edge
  if (circleX < rectX - rectW / 2) {
    testX = rectX - rectW / 2;
  }
  // If circle is right of rect, clamp to rect's right edge
  else if (circleX > rectX + rectW / 2) {
    testX = rectX + rectW / 2;
  }

  // If circle is above rect, clamp to rect's top edge
  if (circleY < rectY - rectH / 2) {
    testY = rectY - rectH / 2;
  }
  // If circle is below rect, clamp to rect's bottom edge
  else if (circleY > rectY + rectH / 2) {
    testY = rectY + rectH / 2;
  }

  // Calculate the distance between the closest point and the circle's center
  let distX = circleX - testX;
  let distY = circleY - testY;
  let distance = sqrt((distX * distX) + (distY * distY));

  // If the distance is less than the circle's radius, there's a collision
  return (distance <= circleR);
}
Line-by-line explanation (4 lines)
if (circleX < rectX - rectW / 2) { testX = rectX - rectW / 2;
If the circle's center is to the left of the rectangle's left edge, snap testX to the left edge
if (circleY < rectY - rectH / 2) { testY = rectY - rectH / 2;
If the circle's center is above the rectangle's top edge, snap testY to the top edge
let distance = sqrt((distX * distX) + (distY * distY));
Uses the Pythagorean theorem to calculate the distance from the closest point to the circle's center
return (distance <= circleR);
Returns true if the distance is less than or equal to the circle's radius (meaning they're touching), false otherwise

rectRectCollision()

rectRectCollision() checks if two rectangles overlap. The trick is to test overlap on both axes separately: if they don't overlap on the X-axis, they can't be colliding no matter what. Same for Y. Only if both axes overlap can there be a true collision. This is called axis-aligned bounding box (AABB) collision detection and is used in almost every 2D game.

function rectRectCollision(r1x, r1y, r1w, r1h, r2x, r2y, r2w, r2h) {
  // Check if the rectangles overlap on the x-axis
  let xOverlap = (r1x - r1w / 2 < r2x + r2w / 2) && (r1x + r1w / 2 > r2x - r2w / 2);

  // Check if the rectangles overlap on the y-axis
  let yOverlap = (r1y - r1h / 2 < r2y + r2h / 2) && (r1y + r1h / 2 > r2y - r2h / 2);

  // If both overlaps are true, there's a collision
  return xOverlap && yOverlap;
}
Line-by-line explanation (3 lines)
let xOverlap = (r1x - r1w / 2 < r2x + r2w / 2) && (r1x + r1w / 2 > r2x - r2w / 2);
Checks if the rectangles overlap horizontally: rect1's right edge is past rect2's left edge AND rect1's left edge is before rect2's right edge
let yOverlap = (r1y - r1h / 2 < r2y + r2h / 2) && (r1y + r1h / 2 > r2y - r2h / 2);
Checks if the rectangles overlap vertically: rect1's bottom is past rect2's top AND rect1's top is before rect2's bottom
return xOverlap && yOverlap;
Returns true only if BOTH X and Y overlap exist—meaning the rectangles are truly colliding, not just near each other

Knife class

The Knife class represents a single thrown knife projectile. Each knife stores its position (x, y), direction vector, and speed. When update() is called, it moves the knife and checks for a hit. When display() is called, it draws the image rotated to point toward the target. Notice how createVector() and normalize() ensure the knife moves at constant speed regardless of distance to the target. This same pattern is reused for Dagger, FireBall, IceBall, and StrongLightning.

🔬 This update() method moves the knife and checks if it hit the target. The condition `if (d < this.speed)` checks if distance is less than one frame's movement. What happens if you change `<` to `<=`? What if you use `< this.speed * 2`?

  update() {
    if (!this.active) return false; // If not active, do nothing

    // Move the knife
    this.x += this.direction.x * this.speed;
    this.y += this.direction.y * this.speed;

    // Check if the knife has reached or passed its target
    // We use distance to the target, and consider it a hit if it's within a frame's movement
    let d = dist(this.x, this.y, this.targetX, this.targetY);
    if (d < this.speed) {
class Knife {
  constructor(x, y, targetX, targetY, speed) {
    this.x = x;
    this.y = y;
    this.targetX = targetX;
    this.targetY = targetY;
    this.speed = speed;
    this.active = true; // Is this knife currently in play?
    this.size = 25; // Approximate size for drawing and collision (half width/height of the image)

    // Calculate direction vector once to ensure straight movement
    this.direction = createVector(this.targetX - this.x, this.targetY - this.y);
    this.direction.normalize(); // Ensure constant speed regardless of distance
  }

  update() {
    if (!this.active) return false; // If not active, do nothing

    // Move the knife
    this.x += this.direction.x * this.speed;
    this.y += this.direction.y * this.speed;

    // Check if the knife has reached or passed its target
    // We use distance to the target, and consider it a hit if it's within a frame's movement
    let d = dist(this.x, this.y, this.targetX, this.targetY);
    if (d < this.speed) {
      this.active = false; // Mark as inactive
      return true; // Indicate that it hit the target
    }
    return false; // Indicate that it did not hit yet
  }

  display() {
    if (!this.active) return; // Only draw if active

    push();
    translate(this.x, this.y); // Move origin to knife's position

    // Calculate rotation angle to point the knife towards its target
    // The direction.heading() gives the angle from the positive X-axis
    // Add HALF_PI because the knife image might be oriented vertically by default
    let angle = this.direction.heading() + HALF_PI;
    rotate(angle);

    imageMode(CENTER); // Draw the image from its center
    // Scale the knife image to match its size property
    image(knifeImage, 0, 0, this.size * 2, this.size * 2);
    pop();

    // Optional: Draw a debug circle around the knife for visualization
    // noFill();
    // stroke(255, 0, 0);
    // circle(this.x, this.y, this.size * 2);
  }
}
Line-by-line explanation (8 lines)
this.direction = createVector(this.targetX - this.x, this.targetY - this.y);
Creates a vector pointing from the knife's starting position toward the target (the boss)
this.direction.normalize();
Scales the direction vector to have a length of 1, so multiplying it by speed gives consistent movement
this.x += this.direction.x * this.speed;
Moves the knife forward by multiplying the direction's X component by speed and adding it to current X
let d = dist(this.x, this.y, this.targetX, this.targetY);
Calculates the distance from the knife's current position to the target
if (d < this.speed) {
If the knife is within a single frame's worth of movement from the target, consider it a hit
let angle = this.direction.heading() + HALF_PI;
Gets the angle of the direction vector (heading()) and adds 90 degrees (HALF_PI) to rotate the image correctly
rotate(angle);
Rotates the canvas so the knife image points toward the target
image(knifeImage, 0, 0, this.size * 2, this.size * 2);
Draws the knife image at the origin (which has been translated to the knife's position and rotated), scaled to size * 2 pixels

Dagger class

The Dagger class is nearly identical to the Knife class—both travel in a straight line toward the boss and check for hits. The main difference is that daggers have a cooldown of 1 second (vs. 2 for knives) and do more damage (30 vs. 20). In a larger game, you might extract the common code into a base Projectile class that both Knife and Dagger inherit from, reducing duplication.

class Dagger {
  constructor(x, y, targetX, targetY, speed) {
    this.x = x;
    this.y = y;
    this.targetX = targetX;
    this.targetY = targetY;
    this.speed = speed;
    this.active = true; // Is this dagger currently in play?
    this.size = 25; // Approximate size for drawing and collision (half width/height of the image)

    // Calculate direction vector once to ensure straight movement
    this.direction = createVector(this.targetX - this.x, this.targetY - this.y);
    this.direction.normalize(); // Ensure constant speed regardless of distance
  }

  update() {
    if (!this.active) return false; // If not active, do nothing

    // Move the dagger
    this.x += this.direction.x * this.speed;
    this.y += this.direction.y * this.speed;

    // Check if the dagger has reached or passed its target
    // We use distance to the target, and consider it a hit if it's within a frame's movement
    let d = dist(this.x, this.y, this.targetX, this.targetY);
    if (d < this.speed) {
      this.active = false; // Mark as inactive
      return true; // Indicate that it hit the target
    }
    return false; // Indicate that it did not hit yet
  }

  display() {
    if (!this.active) return; // Only draw if active

    push();
    translate(this.x, this.y); // Move origin to dagger's position

    // Calculate rotation angle to point the dagger towards its target
    // The direction.heading() gives the angle from the positive X-axis
    // Add HALF_PI because the dagger image might be oriented vertically by default
    let angle = this.direction.heading() + HALF_PI;
    rotate(angle);

    imageMode(CENTER); // Draw the image from its center
    // Scale the dagger image to match its size property
    image(daggerImage, 0, 0, this.size * 2, this.size * 2);
    pop();

    // Optional: Draw a debug circle around the dagger for visualization
    // noFill();
    // stroke(255, 0, 0);
    // circle(this.x, this.y, this.size * 2);
  }
}
Line-by-line explanation (4 lines)
this.active = true;
Marks the dagger as active when created—inactive daggers are removed from the array
this.direction = createVector(this.targetX - this.x, this.targetY - this.y);
Creates a vector pointing from the dagger's start to its target
this.direction.normalize();
Normalizes the vector so it has length 1, allowing speed to be applied consistently
this.x += this.direction.x * this.speed;
Moves the dagger by adding direction * speed to its position

FireBall class

FireBall follows the same pattern as Knife and Dagger. The key differences are: it's spawned by the blue circle (not the red square), has a 5-second cooldown, a slower speed (10 vs. 15 or 20), and a larger visual size (50 vs. 25). This creates the game balance where the circle has fewer fast attacks and the square has multiple quick attacks.

class FireBall {
  constructor(x, y, targetX, targetY, speed) {
    this.x = x;
    this.y = y;
    this.targetX = targetX;
    this.targetY = targetY;
    this.speed = speed;
    this.active = true; // Is this fire ball currently in play?
    this.size = 50; // Approximate size for drawing and collision (half width/height of the image)

    // Calculate direction vector once to ensure straight movement
    this.direction = createVector(this.targetX - this.x, this.targetY - this.y);
    this.direction.normalize(); // Ensure constant speed regardless of distance
  }

  update() {
    if (!this.active) return false; // If not active, do nothing

    // Move the fire ball
    this.x += this.direction.x * this.speed;
    this.y += this.direction.y * this.speed;

    // Check if the fire ball has reached or passed its target
    // We use distance to the target, and consider it a hit if it's within a frame's movement
    let d = dist(this.x, this.y, this.targetX, this.targetY);
    if (d < this.speed) {
      this.active = false; // Mark as inactive
      return true; // Indicate that it hit the target
    }
    return false; // Indicate that it did not hit yet
  }

  display() {
    if (!this.active) return; // Only draw if active

    push();
    translate(this.x, this.y); // Move origin to fire ball's position

    // Calculate rotation angle to point the fire ball towards its target
    // The direction.heading() gives the angle from the positive X-axis
    // Add HALF_PI because the fire ball image might be oriented vertically by default
    let angle = this.direction.heading() + HALF_PI;
    rotate(angle);

    imageMode(CENTER); // Draw the image from its center
    // Scale the fire ball image to match its size property
    image(strongFireBallImage, 0, 0, this.size * 2, this.size * 2);
    pop();

    // Optional: Draw a debug circle around the fire ball for visualization
    // noFill();
    // stroke(255, 0, 0);
    // circle(this.x, this.y, this.size * 2);
  }
}
Line-by-line explanation (2 lines)
this.size = 50;
Fireballs are larger than knives (50 vs. 25)—they're visually more impressive and harder to miss
image(strongFireBallImage, 0, 0, this.size * 2, this.size * 2);
Draws the fireball image (instead of knifeImage) at the rotated position, scaled to 100x100 pixels (size * 2)

IceBall class

IceBall is identical in structure to FireBall—same size, same speed (12 pixels/frame), same 5-second cooldown, same 30 damage. The only difference is the image used for display. In a real game, you might add unique behavior like freezing the boss, slowing it down, or affecting enemy movement—but this sketch keeps all projectiles functionally identical.

class IceBall {
  constructor(x, y, targetX, targetY, speed) {
    this.x = x;
    this.y = y;
    this.targetX = targetX;
    this.targetY = targetY;
    this.speed = speed;
    this.active = true; // Is this ice ball currently in play?
    this.size = 50; // Approximate size for drawing and collision (half width/height of the image)

    // Calculate direction vector once to ensure straight movement
    this.direction = createVector(this.targetX - this.x, this.targetY - this.y);
    this.direction.normalize(); // Ensure constant speed regardless of distance
  }

  update() {
    if (!this.active) return false; // If not active, do nothing

    // Move the ice ball
    this.x += this.direction.x * this.speed;
    this.y += this.direction.y * this.speed;

    // Check if the ice ball has reached or passed its target
    // We use distance to the target, and consider it a hit if it's within a frame's movement
    let d = dist(this.x, this.y, this.targetX, this.targetY);
    if (d < this.speed) {
      this.active = false; // Mark as inactive
      return true; // Indicate that it hit the target
    }
    return false; // Indicate that it did not hit yet
  }

  display() {
    if (!this.active) return; // Only draw if active

    push();
    translate(this.x, this.y); // Move origin to ice ball's position

    // Calculate rotation angle to point the ice ball towards its target
    // The direction.heading() gives the angle from the positive X-axis
    // Add HALF_PI because the ice ball image might be oriented vertically by default
    let angle = this.direction.heading() + HALF_PI;
    rotate(angle);

    imageMode(CENTER); // Draw the image from its center
    // Scale the ice ball image to match its size property
    image(strongIceImage, 0, 0, this.size * 2, this.size * 2);
    pop();

    // Optional: Draw a debug circle around the ice ball for visualization
    // noFill();
    // stroke(255, 0, 0);
    // circle(this.x, this.y, this.size * 2);
  }
}
Line-by-line explanation (1 lines)
image(strongIceImage, 0, 0, this.size * 2, this.size * 2);
Draws the ice ball image instead of fireball or knife, allowing visual distinction between projectile types

StrongLightning class

StrongLightning follows the same projectile pattern and is functionally identical to FireBall and IceBall. It travels at 15 pixels per frame (fastest of the three special attacks) and does 30 damage. The class structure is intentionally repetitive here, which is great for beginners learning how classes work, but in production code you'd refactor into a base Projectile class to eliminate duplication.

class StrongLightning {
  constructor(x, y, targetX, targetY, speed) {
    this.x = x;
    this.y = y;
    this.targetX = targetX;
    this.targetY = targetY;
    this.speed = speed;
    this.active = true; // Is this lightning currently in play?
    this.size = 50; // Approximate size for drawing and collision (half width/height of the image)

    // Calculate direction vector once to ensure straight movement
    this.direction = createVector(this.targetX - this.x, this.targetY - this.y);
    this.direction.normalize(); // Ensure constant speed regardless of distance
  }

  update() {
    if (!this.active) return false; // If not active, do nothing

    // Move the lightning
    this.x += this.direction.x * this.speed;
    this.y += this.direction.y * this.speed;

    // Check if the lightning has reached or passed its target
    // We use distance to the target, and consider it a hit if it's within a frame's movement
    let d = dist(this.x, this.y, this.targetX, this.targetY);
    if (d < this.speed) {
      this.active = false; // Mark as inactive
      return true; // Indicate that it hit the target
    }
    return false; // Indicate that it did not hit yet
  }

  display() {
    if (!this.active) return; // Only draw if active

    push();
    translate(this.x, this.y); // Move origin to lightning's position

    // Calculate rotation angle to point the lightning towards its target
    // The direction.heading() gives the angle from the positive X-axis
    // Add HALF_PI because the lightning image might be oriented vertically by default
    let angle = this.direction.heading() + HALF_PI;
    rotate(angle);

    imageMode(CENTER); // Draw the image from its center
    // Scale the lightning image to match its size property
    image(strongLightningImage, 0, 0, this.size * 2, this.size * 2);
    pop();

    // Optional: Draw a debug circle around the lightning for visualization
    // noFill();
    // stroke(255, 0, 0);
    // circle(this.x, this.y, this.size * 2);
  }
}
Line-by-line explanation (1 lines)
image(strongLightningImage, 0, 0, this.size * 2, this.size * 2);
Draws the lightning image, completing the five-projectile system with distinct visuals for each attack type

📦 Key Variables

circleX, circleY number

Stores the blue player's current position on the canvas—updated every frame by WASD input

let circleX, circleY;
circleRadius number

Controls the size of the blue circle (visual diameter), used for boundary checking and collision detection

let circleRadius = 25;
circleHealth number

Tracks how much HP the blue player has remaining (decreases when touching boss or taking damage)

let circleHealth = 100;
circleInvincible boolean

Flag that becomes true when the circle takes damage, preventing multiple hits in rapid succession

let circleInvincible = false;
circleAlpha number

Transparency value (0-255) that decreases when the circle is defeated, fading it out gradually

let circleAlpha = 255;
squareX, squareY number

Stores the red player's current position on the canvas—updated every frame by IJKL input

let squareX, squareY;
squareHealth number

Tracks how much HP the red player has remaining

let squareHealth = 100;
alphaOmegaHealth number

Tracks the boss's remaining health (starts at 10000, decreases when hit by projectiles)

let alphaOmegaHealth = 10000;
knives, daggers, fireBalls, ices, lightnings array

Arrays that store all active projectiles of each type; projectiles are added when spawned and removed when they hit or go inactive

let knives = [];
let fireBalls = [];
knifeCooldownTimer, daggerCooldownTimer, fireBallCooldownTimer, etc. number

Records the timestamp (in milliseconds) when each ability was last used, preventing spam and enforcing cooldowns

let knifeCooldownTimer = 0;
isGameOver boolean

Flag that becomes true when either all players are defeated or the boss is defeated, stopping game logic

let isGameOver = false;
bobAmplitude, bobSpeed number

Control how far and how fast the boss bobs up and down as a visual effect

let bobAmplitude = 20;
let bobSpeed = 0.05;
isAlphaOmegaJiggling, alphaOmegaJiggleTimer boolean, number

Flag and timestamp that trigger and manage the boss's jiggle animation when hit by a projectile

let isAlphaOmegaJiggling = false;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG draw() and projectile loops

Projectiles can miss the boss if it bobs exactly at the moment they reach the target—the hitbox doesn't account for bobbing motion, but the visual image does

💡 Include bobOffset when calculating hitbox position for collision: const aoY = aoOriginalY - (aoOriginalH * 0.1) + bobOffset; This makes the hitbox follow the visual image.

BUG keyPressed()

Players can hold a key down and spawn multiple projectiles simultaneously if not careful, potentially flooding the screen and causing lag

💡 Consider adding per-key tracking (using a keys object) instead of relying only on keyPressed() events. This would prevent accidental rapid-fire.

PERFORMANCE draw() projectile loops

The code has five separate loops (for knives, daggers, fireballs, ice balls, lightning) that are nearly identical, leading to code duplication and harder maintenance

💡 Create a single updateProjectiles(array) helper function that handles any projectile type, or refactor all projectiles to inherit from a base Projectile class. This cuts loop code from 5 instances to 1.

STYLE cooldown timer rendering

The code that draws cooldown bars for each ability is repetitive—nearly identical code appears 5 times (once per ability per player)

💡 Extract a drawCooldownBar(x, y, cooldownRemaining, cooldownMax, color) helper function to reduce duplication and make changes easier.

FEATURE game overall

There's no difficulty progression—the boss has a fixed health pool and does fixed damage no matter how long the game lasts, so there's no escalation

💡 Add a score/wave system: after defeating the boss, spawn a new one with higher health and damage. This would make the game endless and increasingly challenging.

FEATURE player feedback

When a projectile hits the boss, only the jiggle animation and console log provide feedback—no visual explosion or sound

💡 Add a particle effect at the hit location (burst of small circles or rectangles that fade out) to give more satisfying visual feedback. Optional: add sound effects when projectiles hit.

🔄 Code Flow

Code flow showing preload, setup, draw, restartgame, keypressed, drawhealthbar, windowresized, rectcirclecollision, rectrectcollision, knife, dagger, fireball, iceball, stronglightning

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> circle-movement[Circle Movement] draw --> circle-collision[Circle-Boss Collision Detection] draw --> projectile-loops[Projectile Update Loops] draw --> game-over-check[Game Over Condition] draw --> drawhealthbar[Draw Health Bar] draw --> restart-check[Restart on Space] click setup href "#fn-setup" click draw href "#fn-draw" click circle-movement href "#sub-circle-movement" click circle-collision href "#sub-circle-collision" click projectile-loops href "#sub-projectile-loops" click game-over-check href "#sub-game-over-check" click restart-check href "#sub-restart-check" circle-movement --> keyIsDown[Check Key Presses] keyIsDown --> moveUp[Move Up] keyIsDown --> moveDown[Move Down] keyIsDown --> moveLeft[Move Left] keyIsDown --> moveRight[Move Right] circle-collision --> checkCollision[Check Collision] checkCollision --> applyDamage[Apply Damage] projectile-loops --> forLoop[For Loop Through Projectiles] forLoop --> updateProjectile[Update Projectile Position] forLoop --> checkHit[Check for Boss Hits] game-over-check --> checkFaded[Check if Players are Faded] checkFaded --> endGame[End Game] restart-check --> keyPressed[Check for SPACE Key Press] keyPressed --> restartGame[Call restartGame()] knife-spawn --> checkKnifeCooldown[Check Knife Cooldown] knife-spawn --> spawnKnife[Spawn Knife] dagger-spawn --> checkDaggerCooldown[Check Dagger Cooldown] dagger-spawn --> spawnDagger[Spawn Dagger] fireball-spawn --> checkFireballCooldown[Check Fireball Cooldown] fireball-spawn --> spawnFireball[Spawn Fireball] click knife-spawn href "#sub-knife-spawn" click dagger-spawn href "#sub-dagger-spawn" click fireball-spawn href "#sub-fireball-spawn"

❓ Frequently Asked Questions

What visual elements are featured in the Alpha Omega p5.js sketch?

The sketch visually showcases a circle and a square, both of which have health and invincibility mechanics, alongside various images representing weapons and elemental attacks.

How can users interact with the Alpha Omega sketch?

Users can interact with the sketch by controlling the movement of the circle and square, engaging in a dynamic gameplay experience where they can attack with different weapons.

What creative coding concepts are demonstrated in the Alpha Omega sketch?

The sketch demonstrates concepts such as health management, invincibility states, and fading effects, along with the implementation of damage mechanics through various attack types.

Preview

Alpha Omega - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Alpha Omega - Code flow showing preload, setup, draw, restartgame, keypressed, drawhealthbar, windowresized, rectcirclecollision, rectrectcollision, knife, dagger, fireball, iceball, stronglightning
Code Flow Diagram