neon pong

This is a souped-up Pong game that explodes with 10+ game modes, AI opponents, boss battles, gravity wells, portals, and a gun upgrade shop. Players earn coins by scoring, buy gun upgrades with coins, and battle through escalating levels in single-player mode or face off against a friend.

🧪 Try This!

Experiment with the code by making these changes:

  1. Speed up the ball in Turbo mode — Find the Turbo mode config and raise speedScale to 2.5 for an impossibly fast game.
  2. Make boss paddles huge — In boss modes, paddles are scaled up 20%. Change it to 2.0 for extra-wide targets.
  3. Give bosses more health — Bosses start with 10 HP. Change it to 20 for longer, grueling boss fights.
  4. Make guns cheaper to upgrade — Gun upgrades cost 3 coins. Change it to 1 so you max out guns instantly.
  5. Disable reverse controls in Mayhem — Mayhem combines all modes. Remove modeReverseControls to keep controls normal.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates an elaborately modded version of classic Pong where two paddles bat a ball back and forth, but with ten distinct game modes that each twist the rules in wild ways. Modes range from the timeless Classic to chaotic Mayhem (which layers gravity, portals, and reversed controls all at once), plus a Black Hole mode where a gravity well pulls both ball and bullets toward the center. The visuals pulse with neon-colored paddles, glowing bullets, and dramatic boss health bars. The code demonstrates how a single sketch can support radically different rule sets by using mode-specific configuration variables, physics modifiers, and conditional rendering.

The code is organized around two main gameplay archetypes: standard paddle-vs-paddle matches and boss battles. It manages game state with many variables tracking coins, gun levels, cooldowns, and AI behavior, all wrapped in a robust input handler that supports both single-player (human vs AI) and two-player (human vs human) modes. By reading this sketch you will learn how to compose complex game logic from simpler building blocks—how collision detection adapts to different object types, how physics constants scale with mode, how upgrade systems work, and how AI makes intelligent decisions based on ball position and velocity.

⚙️ How It Works

  1. When the sketch loads, setup() creates the canvas and calls initGameObjects(), which sizes paddles, balls, and bosses based on the current game mode and scales physics constants accordingly.
  2. Every frame, draw() clears the background, updates paddle positions (via handleInput for player 1, or updateRightPaddleAI for AI), and animates the ball and any bullets in flight.
  3. The ball physics engine checks collisions with paddles and walls; in boss modes, the ball bounces off bosses instead of scoring points. Bullets fired by pressing F or J travel straight and damage the ball's momentum or (in boss modes) reduce boss health.
  4. Black Hole mode applies inverse-square gravity to both ball and bullets, pulling them toward the center—the stronger the black hole strength values, the more dramatic the pull. Event horizons swallow objects and trigger re-serves.
  5. A coin and gun system lets players earn currency by scoring and spend it on gun upgrades (up to level 3), reducing shoot cooldown. In single-player mode, levels escalate difficulty; beat level 5 to win. Bosses have hit points and drop coins when damaged; destroy both bosses to complete a boss-mode level.
  6. The shop overlay (toggled with B) displays upgrade costs and gun level progress. Mode switching (keys 1–0 or H) instantly resets scores, coins, and gun levels, then reconfigures the arena via initGameObjects().

🎓 Concepts You'll Learn

Game state managementCollision detection (ball-paddle, ball-boss, bullet-ball)Physics and velocityAI pathfinding and decision makingMode-based configuration and scalingGravity and inverse-square forcesCooldown and rate limitingUpgrade and economy systemsHealth bars and visual feedback

📝 Code Breakdown

setup()

setup() runs once when p5.js starts. It configures the canvas size and calls helper functions to prepare game objects. This sketch uses setup() lightly because most real initialization happens in initGameObjects(), which gets called every time the game mode changes.

function setup() {
  createCanvas(windowWidth, windowHeight);
  rectMode(CENTER);
  textAlign(CENTER, CENTER);
  noStroke();
  initGameObjects();
}
Line-by-line explanation (5 lines)
createCanvas(windowWidth, windowHeight);
Creates a full-screen canvas that will resize if the browser window changes.
rectMode(CENTER);
Tells all rect() calls to position rectangles from their center, not their top-left corner—needed for rotating and centering paddles.
textAlign(CENTER, CENTER);
Centers all text horizontally and vertically around the coordinates you provide, making HUD layout cleaner.
noStroke();
Disables outlines on shapes; we only want filled colors for this sketch.
initGameObjects();
Calls the initialization function that sizes paddles, balls, and bosses based on the current game mode.

initGameObjects()

initGameObjects() is the heart of mode management. Every time the game mode changes, this function is called to reconfigure paddles, ball, bosses, and physics using mode-specific scale factors. This design—applying multipliers instead of hardcoding values—makes adding new modes (or tweaking existing ones) very straightforward. Study how the Mayhem mode combines flags from many other modes; that's the pattern to use when you want to build composite game states.

🔬 Giant mode makes the ball huge and slows it down. What if you change ballSizeScale to 3.0 and speedScale to 0.6—does the giant ball become even more forgiving?

  } else if (gameMode === 'giant') {
    ballSizeScale = 1.8;
    speedScale = 0.9;
  }
function initGameObjects() {
  const shorter = min(width, height);

  // Default mode config
  let paddleScale = 1;
  let speedScale = 1;
  let ballSizeScale = 1;
  let paddleSpeedScale = 1;

  modeGravity = 0;
  modeVerticalWrap = false;
  modeReverseControls = false;
  modeBulletRadiusScale = 1;
  modeBulletSpeedScale = 1;
  modeBallMaxSpeedMult = BASE_BALL_MAX_SPEED_MULT;

  // Per-mode tweaks
  if (gameMode === 'turbo') {
    paddleScale = 0.7;
    speedScale = 1.7;
    ballSizeScale = 0.9;
    paddleSpeedScale = 1.33;
  } else if (gameMode === 'boss') {
    paddleScale = 1.2;
    speedScale = 1.0;
  } else if (gameMode === 'mini') {
    paddleScale = 0.55;
    speedScale = 1.15;
    paddleSpeedScale = 1.1;
  } else if (gameMode === 'giant') {
    ballSizeScale = 1.8;
    speedScale = 0.9;
  } else if (gameMode === 'gravity') {
    speedScale = 1.1;
    modeGravity = shorter * 0.00035;
  } else if (gameMode === 'portal') {
    speedScale = 1.2;
    modeVerticalWrap = true;
  } else if (gameMode === 'reverse') {
    paddleSpeedScale = 1.0;
    modeReverseControls = true;
  } else if (gameMode === 'sniper') {
    paddleScale = 0.7;
    speedScale = 1.1;
    ballSizeScale = 0.85;
    paddleSpeedScale = 0.9;
    modeBulletRadiusScale = 1.3;
    modeBulletSpeedScale = 1.4;
    modeBallMaxSpeedMult = 2.8;
  } else if (gameMode === 'mayhem') {
    // ALL modes combined
    paddleScale = 0.55;
    ballSizeScale = 1.8;
    speedScale = 1.9;
    paddleSpeedScale = 1.5;

    modeGravity = shorter * 0.00035;
    modeVerticalWrap = true;
    modeReverseControls = true;

    modeBulletRadiusScale = 1.3;
    modeBulletSpeedScale = 1.4;
    modeBallMaxSpeedMult = 3.8;
  } else if (gameMode === 'blackhole') {
    paddleScale = 1.0;
    ballSizeScale = 1.0;
    speedScale = 1.15;
    paddleSpeedScale = 1.0;
  }

  // Extra difficulty scaling for 1P levels
  if (isSinglePlayer) {
    const levelSpeedMult = constrain(1 + 0.08 * (currentLevel - 1), 1, 2);
    speedScale *= levelSpeedMult;

    const maxMultBoost = constrain(1 + 0.05 * (currentLevel - 1), 1, 1.8);
    modeBallMaxSpeedMult *= maxMultBoost;
  }

  paddleH = shorter * 0.25 * paddleScale;
  paddleW = shorter * 0.02;
  paddleSpeed = shorter * 0.012 * paddleSpeedScale;

  if (isBossStyleMode()) {
    leftX = width * 0.28;
    rightX = width * 0.72;
  } else {
    leftX = paddleW * 2;
    rightX = width - paddleW * 2;
  }

  leftY = height / 2;
  rightY = height / 2;

  ballSize = shorter * 0.03 * ballSizeScale;
  ballSpeed = shorter * 0.007 * speedScale;
  resetBall(true);

  if (isBossStyleMode()) {
    initBosses();
  } else {
    boss1 = null;
    boss2 = null;
    bossFightOver = false;
    bossWinner = null;
  }

  // Setup black hole (Black Hole mode only)
  if (gameMode === 'blackhole') {
    blackHoleX = width / 2;
    blackHoleY = height / 2;
    blackHoleRadius = shorter * 0.07;

    // Stronger gravity so the effect is clearly visible
    blackHoleStrengthBall = shorter * 0.6;
    blackHoleStrengthBullet = shorter * 0.9;
  } else {
    blackHoleRadius = 0;
  }

  bullets = [];
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

switch-case Mode-specific configuration if (gameMode === 'turbo') { ... } else if (gameMode === 'boss') { ... }

Applies scaling factors (paddle size, ball speed, physics tweaks) for each of the 11 game modes so each mode feels unique.

conditional 1P level difficulty scaling if (isSinglePlayer) { const levelSpeedMult = ...; speedScale *= levelSpeedMult; }

Boosts game speed and ball max velocity as the player progresses through levels 1–∞, making later levels harder.

conditional Paddle positioning (normal vs boss) if (isBossStyleMode()) { leftX = width * 0.28; rightX = width * 0.72; }

In boss modes, moves paddles toward the center to make room for large bosses on the edges.

const shorter = min(width, height);
Stores the smaller of canvas width or height, used as a reference to scale all game objects proportionally to screen size.
let paddleScale = 1; let speedScale = 1; let ballSizeScale = 1; let paddleSpeedScale = 1;
Declares temporary scale multipliers that will be modified per-mode, then applied to all physical sizes and speeds.
modeGravity = 0; modeVerticalWrap = false; modeReverseControls = false;
Resets mode-specific physics flags to defaults (no gravity, no wrapping, normal controls) before applying mode tweaks.
if (gameMode === 'turbo') { paddleScale = 0.7; speedScale = 1.7; ... }
For Turbo mode, shrinks paddles and speeds up the ball to create a fast, intense version of Pong.
if (isSinglePlayer) { const levelSpeedMult = constrain(1 + 0.08 * (currentLevel - 1), 1, 2); speedScale *= levelSpeedMult; }
In 1P mode, multiplies ball speed by an amount that grows with the current level, capping at 2x—each level is 8% faster than the last.
paddleH = shorter * 0.25 * paddleScale; paddleW = shorter * 0.02; paddleSpeed = shorter * 0.012 * paddleSpeedScale;
Applies the accumulated scale factors to compute final paddle height, width, and movement speed.
if (isBossStyleMode()) { leftX = width * 0.28; rightX = width * 0.72; } else { leftX = paddleW * 2; rightX = width - paddleW * 2; }
Positions paddles closer to the center in boss modes (leaving room for large boss sprites on the edges) or near the edges in normal modes.
ballSize = shorter * 0.03 * ballSizeScale; ballSpeed = shorter * 0.007 * speedScale; resetBall(true);
Applies scale factors to ball size and initial speed, then calls resetBall() to place the ball at center with a randomized launch angle.
if (isBossStyleMode()) { initBosses(); } else { boss1 = null; boss2 = null; bossFightOver = false; bossWinner = null; }
If the mode includes bosses, calls initBosses() to spawn them with full health; otherwise clears all boss state.
if (gameMode === 'blackhole') { blackHoleX = width / 2; blackHoleY = height / 2; blackHoleRadius = shorter * 0.07; blackHoleStrengthBall = shorter * 0.6; blackHoleStrengthBullet = shorter * 0.9; }
In Black Hole mode, places the gravity well at canvas center and sets its radius and pull strength; otherwise sets blackHoleRadius to 0 (disabled).

draw()

draw() runs 60 times per second and orchestrates the entire game loop: it gathers input, updates physics, renders all visuals, and displays the UI. Notice how it checks state flags (isRunning, isShopOpen, isBossStyleMode) to conditionally execute subsystems. This is a common pattern in game engines—each subsystem (physics, rendering, input, shop) is independent and toggled by state flags.

🔬 This is the render order: paddles first, then bosses, then ball, then bullets. What happens if you move drawBall() before drawPaddles()—does the ball draw behind or in front?

  drawPaddles();
  drawGunIndicators();
  if (isBossStyleMode()) {
    drawBosses();
  }
  drawBall();
  drawBullets();
function draw() {
  drawBackground();
  rectMode(CENTER);

  updateShootCooldowns();

  drawCenterLine();
  drawBlackHole(); // only in Black Hole mode

  if (!isShopOpen) {
    handleInput();
  }

  if (isRunning && !isShopOpen) {
    updateBall();
    updateBullets();
  }

  drawPaddles();
  drawGunIndicators();
  if (isBossStyleMode()) {
    drawBosses();
  }
  drawBall();
  drawBullets();
  drawHUD();
  drawInstructions();

  if (levelMessageTimer > 0) {
    drawLevelMessageOverlay();
  }

  if (isShopOpen) {
    drawShop();
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

conditional Active game state check if (isRunning && !isShopOpen) { updateBall(); updateBullets(); }

Only updates physics (ball and bullet movement) when the game is running and the shop is not open.

conditional Input blocking when shop is open if (!isShopOpen) { handleInput(); }

Prevents paddle control while browsing the shop, ensuring players must close it to resume playing.

drawBackground();
Clears the canvas each frame and paints a gradient background that transitions from dark blue at the top to darker blue at the bottom.
rectMode(CENTER);
Re-establishes CENTER mode (set in setup) in case any function changed it—ensures consistent rect() positioning.
updateShootCooldowns();
Decrements the cooldown counters for both players' guns, allowing them to shoot again once the counter hits zero.
drawCenterLine(); drawBlackHole();
Draws the dashed center dividing line and (if in Black Hole mode) the gravity well at the center.
if (!isShopOpen) { handleInput(); }
Reads player keyboard input to move paddles and fire bullets, but skips this if the shop is open.
if (isRunning && !isShopOpen) { updateBall(); updateBullets(); }
Only updates ball and bullet positions when the game is actively running and not paused by the shop.
drawPaddles(); drawGunIndicators(); if (isBossStyleMode()) { drawBosses(); } drawBall(); drawBullets(); drawHUD(); drawInstructions();
Renders all game objects in order: paddles, gun charge bars, bosses (if applicable), ball, bullets, score/level HUD, and control instructions.
if (levelMessageTimer > 0) { drawLevelMessageOverlay(); }
If a level-start or level-fail message is active, draws it centered on screen (only in 1P mode).
if (isShopOpen) { drawShop(); }
If the shop is open, renders the upgrade panel on top of everything else.

handleInput()

handleInput() is called every frame and reads the keyboard state in real-time using keyIsDown(). This allows smooth, responsive paddle movement because we check which keys are held at the moment, not waiting for a key-press event. The dirSign multiplier is an elegant way to implement Reverse mode: instead of duplicating the input code, we just flip the sign. The clamping at the end prevents visual bugs where paddles could partially leave the screen.

🔬 These two lines move the left paddle with W and S. What if you change -= to += on the W line—does the paddle move opposite?

  if (keyIsDown(87)) { // W
    leftY -= paddleSpeed * dirSign;
  }
  if (keyIsDown(83)) { // S
    leftY += paddleSpeed * dirSign;
  }
function handleInput() {
  const dirSign = modeReverseControls ? -1 : 1;

  if (keyIsDown(87)) { // W
    leftY -= paddleSpeed * dirSign;
  }
  if (keyIsDown(83)) { // S
    leftY += paddleSpeed * dirSign;
  }

  if (!isSinglePlayer) {
    if (keyIsDown(UP_ARROW)) {
      rightY -= paddleSpeed * dirSign;
    }
    if (keyIsDown(DOWN_ARROW)) {
      rightY += paddleSpeed * dirSign;
    }
  } else {
    updateRightPaddleAI();
  }

  const halfH = paddleH / 2;
  leftY = constrain(leftY, halfH, height - halfH);
  rightY = constrain(rightY, halfH, height - halfH);
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

calculation Control inversion for Reverse mode const dirSign = modeReverseControls ? -1 : 1;

Flips the direction multiplier in Reverse mode, so up movements go down and vice versa.

conditional Player 1 paddle input (W/S) if (keyIsDown(87)) { leftY -= paddleSpeed * dirSign; }

Reads W and S key presses to move the left paddle up and down (or down and up if dirSign is -1).

conditional Player 2 vs AI decision if (!isSinglePlayer) { ... } else { updateRightPaddleAI(); }

In 2P mode, moves the right paddle from arrow keys; in 1P mode, calls AI logic instead.

calculation Keep paddles on screen leftY = constrain(leftY, halfH, height - halfH); rightY = constrain(rightY, halfH, height - halfH);

Prevents paddles from moving off the top or bottom edges of the canvas.

const dirSign = modeReverseControls ? -1 : 1;
Creates a multiplier that flips to -1 in Reverse mode, so W becomes down and S becomes up.
if (keyIsDown(87)) { leftY -= paddleSpeed * dirSign; }
Checks if the W key (ASCII 87) is currently held down; if so, moves the left paddle up (or down if dirSign is -1).
if (keyIsDown(83)) { leftY += paddleSpeed * dirSign; }
Checks if the S key (ASCII 83) is held; if so, moves the left paddle down (or up if reversed).
if (!isSinglePlayer) { if (keyIsDown(UP_ARROW)) { rightY -= paddleSpeed * dirSign; } }
In 2P mode, reads arrow keys to move the right paddle; skipped entirely in 1P mode.
} else { updateRightPaddleAI(); }
In 1P mode, calls the AI function to move the right paddle instead of reading human input.
const halfH = paddleH / 2;
Calculates the paddle's half-height, used to clamp the paddle center so the edges don't go off-screen.
leftY = constrain(leftY, halfH, height - halfH);
Clamps the left paddle's Y position to stay within the screen, preventing it from sliding off the top or bottom.

updateBall()

updateBall() is the physics engine. It applies forces (gravity, black hole pull), moves the ball, checks for collisions with walls and paddles, and handles mode-specific behaviors (wrapping, boss damage, coin rewards). Each collision bounces the ball by flipping velocity and/or angling it based on where it hit. The paddle-angle calculation is a clever touch that lets skilled players control the ball's trajectory by hitting it at different heights—a classic Pong mechanic that feels good to master.

🔬 This block either bounces or wraps the ball vertically. What if you delete the else-block and always wrap, even in normal modes—does the ball wrap in Classic mode?

  if (!modeVerticalWrap) {
    if (ballY - ballSize / 2 <= 0 || ballY + ballSize / 2 >= height) {
      ballVY *= -1;
      ballY = constrain(ballY, ballSize / 2, height - ballSize / 2);
      if (gameMode === 'mayhem') boostBallSpeed();
    }
  } else {
    if (ballY + ballSize / 2 < 0) {
      ballY = height + ballSize / 2;
    } else if (ballY - ballSize / 2 > height) {
      ballY = -ballSize / 2;
    }
  }
function updateBall() {
  if (isBossStyleMode() && bossFightOver) return;

  if (modeGravity !== 0) {
    ballVY += modeGravity;
  }

  if (gameMode === 'blackhole') {
    applyBlackHoleGravityToBall();
  }

  ballX += ballVX;
  ballY += ballVY;

  // Event horizon: ball swallowed, re-serve (no score)
  if (gameMode === 'blackhole' && blackHoleRadius > 0) {
    const dxBH = ballX - blackHoleX;
    const dyBH = ballY - blackHoleY;
    const distSqBH = dxBH * dxBH + dyBH * dyBH;
    const swallowR = blackHoleRadius * 0.5;
    if (distSqBH < swallowR * swallowR) {
      resetBall(random([true, false]));
      return;
    }
  }

  if (!modeVerticalWrap) {
    if (ballY - ballSize / 2 <= 0 || ballY + ballSize / 2 >= height) {
      ballVY *= -1;
      ballY = constrain(ballY, ballSize / 2, height - ballSize / 2);
      if (gameMode === 'mayhem') boostBallSpeed();
    }
  } else {
    if (ballY + ballSize / 2 < 0) {
      ballY = height + ballSize / 2;
    } else if (ballY - ballSize / 2 > height) {
      ballY = -ballSize / 2;
    }
  }

  // Paddle collisions
  if (
    ballX - ballSize / 2 <= leftX + paddleW / 2 &&
    ballX > leftX &&
    ballY > leftY - paddleH / 2 &&
    ballY < leftY + paddleH / 2
  ) {
    ballX = leftX + paddleW / 2 + ballSize / 2;
    ballVX *= -1;
    const offset = (ballY - leftY) / (paddleH / 2);
    ballVY = ballSpeed * offset;
    if (gameMode === 'mayhem') boostBallSpeed();
  }

  if (
    ballX + ballSize / 2 >= rightX - paddleW / 2 &&
    ballX < rightX &&
    ballY > rightY - paddleH / 2 &&
    ballY < rightY + paddleH / 2
  ) {
    ballX = rightX - paddleW / 2 - ballSize / 2;
    ballVX *= -1;
    const offset = (ballY - rightY) / (paddleH / 2);
    ballVY = ballSpeed * offset;
    if (gameMode === 'mayhem') boostBallSpeed();
  }

  if (isBossStyleMode()) {
    handleBossCollisions();

    if (ballX - ballSize / 2 <= 0 || ballX + ballSize / 2 >= width) {
      ballVX *= -1;
      ballX = constrain(ballX, ballSize / 2, width - ballSize / 2);
      if (gameMode === 'mayhem') boostBallSpeed();
    }
  } else {
    if (ballX < 0) {
      p2Score++;
      p2Coins += 2;
      isRunning = false;
      resetBall(false);
      if (isSinglePlayer) handleScoreLevelProgress('P2');
    } else if (ballX > width) {
      p1Score++;
      p1Coins += 2;
      isRunning = false;
      resetBall(true);
      if (isSinglePlayer) handleScoreLevelProgress('P1');
    }
  }
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

conditional Apply gravity acceleration if (modeGravity !== 0) { ballVY += modeGravity; }

In Gravity mode, accelerates the ball downward each frame (simulating gravity).

conditional Black Hole gravity pull if (gameMode === 'blackhole') { applyBlackHoleGravityToBall(); }

In Black Hole mode, applies inverse-square gravitational pull toward the center.

conditional Event horizon detection if (gameMode === 'blackhole' && blackHoleRadius > 0) { ... if (distSqBH < swallowR * swallowR) { resetBall(...); } }

Checks if the ball crosses the event horizon and respawns it if it does.

conditional Top/bottom wall collision if (!modeVerticalWrap) { ... } else { ... }

Either bounces the ball off top/bottom walls or wraps it to the opposite side (Portal mode).

conditional Left paddle hit detection if ( ballX - ballSize / 2 <= leftX + paddleW / 2 && ballX > leftX && ... ) { ... }

Detects collision with the left paddle using AABB (axis-aligned bounding box) and bounces the ball, angling it based on hit position.

conditional Right paddle hit detection if ( ballX + ballSize / 2 >= rightX - paddleW / 2 && ballX < rightX && ... ) { ... }

Detects collision with the right paddle and bounces it, angling based on where the ball hit.

conditional Boss mode vs normal scoring if (isBossStyleMode()) { handleBossCollisions(); ... } else { if (ballX < 0) { p2Score++; ... } }

In boss modes, damages bosses instead of awarding points; in normal modes, scores points when the ball exits the sides.

if (isBossStyleMode() && bossFightOver) return;
Early exit: if a boss fight has ended, stop updating the ball until the player resets.
if (modeGravity !== 0) { ballVY += modeGravity; }
In Gravity mode, adds a downward acceleration to the ball's vertical velocity each frame.
if (gameMode === 'blackhole') { applyBlackHoleGravityToBall(); }
In Black Hole mode, calls a helper that calculates the gravitational pull force from the center and updates the ball's velocity.
ballX += ballVX; ballY += ballVY;
Updates the ball's position by adding its velocity—the fundamental physics update.
if (gameMode === 'blackhole' && blackHoleRadius > 0) { const dxBH = ballX - blackHoleX; ... if (distSqBH < swallowR * swallowR) { resetBall(random([true, false])); } }
Checks if the ball has entered the event horizon (the inner sphere of the black hole); if so, respawns it at center with a random direction.
if (!modeVerticalWrap) { if (ballY - ballSize / 2 <= 0 || ballY + ballSize / 2 >= height) { ballVY *= -1; ... } }
In normal modes, bounces the ball off the top and bottom edges by flipping its vertical velocity.
} else { if (ballY + ballSize / 2 < 0) { ballY = height + ballSize / 2; } else if (ballY - ballSize / 2 > height) { ballY = -ballSize / 2; } }
In Portal mode (modeVerticalWrap), wraps the ball to the opposite vertical edge instead of bouncing it.
if ( ballX - ballSize / 2 <= leftX + paddleW / 2 && ballX > leftX && ballY > leftY - paddleH / 2 && ballY < leftY + paddleH / 2 ) { ... }
Four conditions check if the ball overlaps the left paddle's AABB (axis-aligned bounding box); if all true, it's a hit.
const offset = (ballY - leftY) / (paddleH / 2); ballVY = ballSpeed * offset;
Calculates where on the paddle the ball hit (0 = center, ±1 = edge) and angles the ball's vertical velocity proportionally—hitting the edge creates more angle.
if (gameMode === 'mayhem') boostBallSpeed();
In Mayhem mode, each paddle bounce increases the ball's speed to keep the chaos escalating.
if (isBossStyleMode()) { handleBossCollisions(); ... } else { if (ballX < 0) { p2Score++; p2Coins += 2; } }
In boss modes, handles ball-vs-boss collisions; in normal modes, awards points to a player when the ball exits the opposite side.

updateBullets()

updateBullets() manages the lifecycle of all fired bullets: moving them, detecting collisions (with ball, bosses, walls, black hole), and removing them when their job is done. The backward loop is a classic pattern in game dev—it safely allows removing items during iteration. Notice how bullets apply force to the ball (0.6 × ball speed) rather than teleporting it; this creates satisfying "gun deflects ball" moments. The speed-cap logic prevents bullets from launching the ball at infinite speed when you fire multiple shots in quick succession.

function updateBullets() {
  for (let i = bullets.length - 1; i >= 0; i--) {
    const b = bullets[i];

    if (gameMode === 'blackhole') {
      applyBlackHoleGravityToBullet(b);
    }

    b.x += b.vx;
    b.y += b.vy;

    let remove = false;

    // Black hole swallow bullets
    if (!remove && gameMode === 'blackhole' && blackHoleRadius > 0) {
      const dxBH = b.x - blackHoleX;
      const dyBH = b.y - blackHoleY;
      const distSqBH = dxBH * dxBH + dyBH * dyBH;
      const swallowR = blackHoleRadius * 0.6;
      if (distSqBH < swallowR * swallowR) {
        remove = true;
      }
    }

    if (!remove && (b.x < -50 || b.x > width + 50 || b.y < -50 || b.y > height + 50)) {
      remove = true;
    }

    // Bullet vs ball
    if (!remove) {
      const dx = b.x - ballX;
      const dy = b.y - ballY;
      const rad = b.r + ballSize / 2;
      if (dx * dx + dy * dy <= rad * rad) {
        const pushDir = b.owner === 1 ? 1 : -1;
        ballVX += pushDir * ballSpeed * 0.6;
        ballVY += dy * 0.04;

        const maxSpeed = ballSpeed * modeBallMaxSpeedMult;
        const speed = sqrt(ballVX * ballVX + ballVY * ballVY);
        if (speed > maxSpeed) {
          const s = maxSpeed / speed;
          ballVX *= s;
          ballVY *= s;
        }

        remove = true;
      }
    }

    // Bullet vs bosses (Boss + Mayhem)
    if (!remove && isBossStyleMode()) {
      if (b.owner === 1 && boss2 && boss2.hp > 0) {
        const hit =
          b.x + b.r >= boss2.x - boss2.w / 2 &&
          b.x - b.r <= boss2.x + boss2.w / 2 &&
          b.y + b.r >= boss2.y - boss2.h / 2 &&
          b.y - b.r <= boss2.y + boss2.h / 2;
        if (hit) {
          boss2.hp = max(0, boss2.hp - 1);
          p1Coins += 1;
          if (boss2.hp === 0 && !bossFightOver) {
            bossFightOver = true;
            bossWinner = 'P1';
            isRunning = false;
            if (isSinglePlayer) handleBossLevelEnd('P1');
          }
          remove = true;
        }
      } else if (b.owner === 2 && boss1 && boss1.hp > 0) {
        const hit =
          b.x + b.r >= boss1.x - boss1.w / 2 &&
          b.x - b.r <= boss1.x + boss1.w / 2 &&
          b.y + b.r >= boss1.y - boss1.h / 2 &&
          b.y - b.r <= boss1.y + boss1.h / 2;
        if (hit) {
          boss1.hp = max(0, boss1.hp - 1);
          p2Coins += 1;
          if (boss1.hp === 0 && !bossFightOver) {
            bossFightOver = true;
            bossWinner = 'P2';
            isRunning = false;
            if (isSinglePlayer) handleBossLevelEnd('P2');
          }
          remove = true;
        }
      }
    }

    if (remove) bullets.splice(i, 1);
  }
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

for-loop Reverse iteration over bullets for (let i = bullets.length - 1; i >= 0; i--) { ... if (remove) bullets.splice(i, 1); }

Iterates backward through the bullets array so removing items with splice() doesn't skip any bullets.

conditional Black Hole gravity on bullets if (gameMode === 'blackhole') { applyBlackHoleGravityToBullet(b); }

In Black Hole mode, pulls bullets toward the center.

conditional Event horizon swallows bullets if (!remove && gameMode === 'blackhole' && blackHoleRadius > 0) { ... if (distSqBH < swallowR * swallowR) { remove = true; } }

Removes bullets that enter the black hole's event horizon.

conditional Despawn bullets off-screen if (!remove && (b.x < -50 || b.x > width + 50 || b.y < -50 || b.y > height + 50)) { remove = true; }

Removes bullets that have traveled far beyond the screen edges to prevent memory buildup.

conditional Bullet-to-ball collision if (!remove) { const dx = b.x - ballX; const dy = b.y - ballY; const rad = b.r + ballSize / 2; if (dx * dx + dy * dy <= rad * rad) { ... } }

Detects circular collision between bullet and ball, pushing the ball away and capping its speed.

conditional Bullet-to-boss collision if (!remove && isBossStyleMode()) { if (b.owner === 1 && boss2 && boss2.hp > 0) { ... } }

Detects AABB collision between bullet and enemy boss, reducing boss HP and awarding coins.

for (let i = bullets.length - 1; i >= 0; i--) {
Loops backward through the bullets array (from last to first) so that removing items with splice() doesn't skip any bullets.
if (gameMode === 'blackhole') { applyBlackHoleGravityToBullet(b); }
In Black Hole mode, applies gravitational pull to each bullet toward the center.
b.x += b.vx; b.y += b.vy;
Updates the bullet's position by adding its velocity.
let remove = false;
Declares a flag that tracks whether this bullet should be removed at the end of the loop.
if (!remove && gameMode === 'blackhole' && blackHoleRadius > 0) { ... if (distSqBH < swallowR * swallowR) { remove = true; } }
Checks if the bullet has entered the event horizon (60% of black hole radius); if so, marks it for removal.
if (!remove && (b.x < -50 || b.x > width + 50 || b.y < -50 || b.y > height + 50)) { remove = true; }
Removes bullets that have traveled far off-screen (with a 50-pixel margin) to avoid an unbounded array.
const dx = b.x - ballX; const dy = b.y - ballY; const rad = b.r + ballSize / 2; if (dx * dx + dy * dy <= rad * rad) {
Calculates the distance between bullet and ball centers; if less than the sum of their radii, they collide.
const pushDir = b.owner === 1 ? 1 : -1; ballVX += pushDir * ballSpeed * 0.6; ballVY += dy * 0.04;
Pushes the ball away from the bullet in the direction it was fired, plus a small vertical component based on the offset.
const maxSpeed = ballSpeed * modeBallMaxSpeedMult; const speed = sqrt(ballVX * ballVX + ballVY * ballVY); if (speed > maxSpeed) { const s = maxSpeed / speed; ballVX *= s; ballVY *= s; }
Caps the ball's speed to prevent it from accelerating infinitely; if it exceeds the max, scales both velocity components down proportionally.
if (b.owner === 1 && boss2 && boss2.hp > 0) { const hit = b.x + b.r >= boss2.x - boss2.w / 2 && ... if (hit) { boss2.hp = max(0, boss2.hp - 1); p1Coins += 1; } }
Checks if player 1's bullet overlaps boss 2's AABB; if so, reduces boss HP by 1 and awards a coin.
if (boss2.hp === 0 && !bossFightOver) { bossFightOver = true; bossWinner = 'P1'; isRunning = false; ... }
When a boss reaches 0 HP, marks the fight as over, sets the winner, and stops the game until the player resets.
if (remove) bullets.splice(i, 1);
At the end of each iteration, removes this bullet if the remove flag was set.

shoot(player)

shoot() is the gun system. It checks preconditions (gun bought, cooldown expired, game running), calculates bullet properties based on gun level, spawns the bullet, and sets the cooldown. The cooldown formula (SHOOT_COOLDOWN_FRAMES / (0.6 + 0.4 * level)) is elegant: higher levels increase the divisor, making the cooldown smaller—exactly what you want for a progression system. The bullet spawns just outside the paddle to avoid self-collision, which is a small but important detail.

🔬 These lines set bullet size and speed based on gun level. What if you remove the level scaling (change (1.4 + 0.3 * (level - 1)) to just 1.4)—do all gun levels shoot equally fast?

  const radius = ballSize * 0.35 * modeBulletRadiusScale;
  const speedMultiplier = (1.4 + 0.3 * (level - 1)) * modeBulletSpeedScale;
  const vx = ballSpeed * speedMultiplier * dir;
function shoot(player) {
  if (isBossStyleMode() && bossFightOver) return;
  if (!isRunning) return;

  if (player === 1) {
    if (p1GunLevel <= 0 || p1ShootCooldown > 0) return;
  } else {
    if (p2GunLevel <= 0 || p2ShootCooldown > 0) return;
  }

  const level = player === 1 ? p1GunLevel : p2GunLevel;
  const paddleX = player === 1 ? leftX : rightX;
  const paddleY = player === 1 ? leftY : rightY;
  const dir = player === 1 ? 1 : -1;

  const radius = ballSize * 0.35 * modeBulletRadiusScale;
  const speedMultiplier = (1.4 + 0.3 * (level - 1)) * modeBulletSpeedScale;
  const vx = ballSpeed * speedMultiplier * dir;

  bullets.push({
    x: paddleX + dir * (paddleW / 2 + radius + 2),
    y: paddleY,
    vx,
    vy: 0,
    r: radius,
    owner: player
  });

  const cooldown = Math.max(8, Math.floor(SHOOT_COOLDOWN_FRAMES / (0.6 + 0.4 * level)));
  if (player === 1) p1ShootCooldown = cooldown;
  else p2ShootCooldown = cooldown;
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

conditional Early exit checks if (isBossStyleMode() && bossFightOver) return; if (!isRunning) return; if (p1GunLevel <= 0 || p1ShootCooldown > 0) return;

Prevents shooting if the boss fight is over, the game is not running, the gun is not bought, or the cooldown is still active.

calculation Bullet object creation bullets.push({ x: paddleX + dir * (...), y: paddleY, vx, vy: 0, r: radius, owner: player });

Spawns a bullet at the paddle's position with velocity toward the enemy side.

calculation Gun cooldown calculation const cooldown = Math.max(8, Math.floor(SHOOT_COOLDOWN_FRAMES / (0.6 + 0.4 * level)));

Calculates the cooldown frames based on gun level: higher levels shoot faster.

if (isBossStyleMode() && bossFightOver) return;
Prevents shooting after a boss fight ends, forcing the player to reset with R.
if (!isRunning) return;
Prevents shooting while the game is paused.
if (player === 1) { if (p1GunLevel <= 0 || p1ShootCooldown > 0) return; }
If player 1 hasn't bought a gun (level ≤ 0) or the cooldown is still active, exits early.
const level = player === 1 ? p1GunLevel : p2GunLevel;
Gets the gun level for the player (1–3).
const paddleX = player === 1 ? leftX : rightX; const paddleY = player === 1 ? leftY : rightY;
Gets the paddle's current position to spawn the bullet from there.
const dir = player === 1 ? 1 : -1;
Direction multiplier: +1 for player 1 (shoots right), -1 for player 2 (shoots left).
const radius = ballSize * 0.35 * modeBulletRadiusScale;
Calculates the bullet's radius; larger in Sniper mode (modeBulletRadiusScale = 1.3) for more impact.
const speedMultiplier = (1.4 + 0.3 * (level - 1)) * modeBulletSpeedScale;
Calculates the speed multiplier based on gun level: level 1 = 1.4×, level 2 = 1.7×, level 3 = 2.0×. Sniper mode further multiplies by 1.4.
const vx = ballSpeed * speedMultiplier * dir;
Combines the ball's base speed with the gun-specific multiplier and direction to get the bullet's horizontal velocity.
bullets.push({ x: paddleX + dir * (paddleW / 2 + radius + 2), y: paddleY, vx, vy: 0, r: radius, owner: player });
Creates a bullet object and adds it to the bullets array, spawning it just outside the paddle so it doesn't immediately collide with the paddle.
const cooldown = Math.max(8, Math.floor(SHOOT_COOLDOWN_FRAMES / (0.6 + 0.4 * level)));
Calculates cooldown: as level increases, the divisor increases, making the cooldown shorter (faster fire rate).
if (player === 1) p1ShootCooldown = cooldown; else p2ShootCooldown = cooldown;
Sets the cooldown counter for the player, preventing another shot until it counts down to zero.

applyBlackHoleGravityToBall()

applyBlackHoleGravityToBall() implements a simplified physics model of gravity. The inverse-square law (force ∝ 1/d²) is a real physics principle that creates a realistic-feeling pull. The softening at the center (clamping minimum distance) prevents numerical instability—without it, the force would grow infinite as the ball approaches the center. This is a common trick in N-body simulations and game physics.

function applyBlackHoleGravityToBall() {
  if (blackHoleRadius <= 0) return;
  const dx = blackHoleX - ballX;
  const dy = blackHoleY - ballY;
  const distSq = dx * dx + dy * dy;

  // Soften singularity near center, but allow strong pull
  const minDist = blackHoleRadius * 0.6;
  const softSq = minDist * minDist;
  const dSq = max(distSq, softSq);
  const dist = sqrt(dSq);

  const force = blackHoleStrengthBall / dSq;
  ballVX += (dx / dist) * force;
  ballVY += (dy / dist) * force;
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

calculation Inverse-square gravity computation const force = blackHoleStrengthBall / dSq; ballVX += (dx / dist) * force; ballVY += (dy / dist) * force;

Applies a force proportional to 1/distance² and in the direction toward the black hole center.

if (blackHoleRadius <= 0) return;
Early exit if Black Hole mode is not active.
const dx = blackHoleX - ballX; const dy = blackHoleY - ballY;
Calculates the displacement vector from the ball to the black hole center.
const distSq = dx * dx + dy * dy;
Calculates the squared distance (avoiding a sqrt for efficiency at this step).
const minDist = blackHoleRadius * 0.6; const softSq = minDist * minDist; const dSq = max(distSq, softSq);
Clamps the minimum distance to 60% of the black hole radius, preventing the force from blowing up at the singularity.
const dist = sqrt(dSq);
Calculates the actual distance (clamped) for use in the direction vector.
const force = blackHoleStrengthBall / dSq;
Calculates the magnitude of the gravitational force using inverse-square law: F ∝ 1/d².
ballVX += (dx / dist) * force; ballVY += (dy / dist) * force;
Adds the force vector to the ball's velocity. (dx / dist) is the unit direction vector toward the black hole.

drawHUD()

drawHUD() is pure UI/visualization. It renders the game state information (scores, coins, gun levels, boss health) in a semi-transparent panel at the top of the screen. Notice how it uses textAlign(LEFT, CENTER) and textAlign(RIGHT, CENTER) to position text asymmetrically—a key technique for creating readable, balanced layouts. The HUD height changes based on game mode, adapting its layout to show boss health bars when relevant.

function drawHUD() {
  push();
  const pad = 16;
  const hudHeight = height * (isBossStyleMode() ? 0.2 : 0.16);

  rectMode(CORNER);
  noStroke();
  fill(0, 0, 0, 170);
  rect(pad, pad, width - pad * 2, hudHeight, 18);

  const cx = width / 2;
  const centerYTop = pad + hudHeight * 0.4;
  const fontBig = min(width, height) * 0.06;
  const fontSmall = min(width, height) * 0.022;

  textAlign(CENTER, CENTER);
  textSize(min(width, height) * 0.028);
  fill(200);
  const label = getModeLabel();
  const labelY = pad + hudHeight * 0.2;
  text(label, cx, labelY);

  textSize(fontSmall * 0.9);
  fill(180);
  const controlText = isSinglePlayer ? 'Players: 1P (P1 vs AI)' : 'Players: 2P (P1 vs P2)';
  text(controlText, cx, labelY + fontSmall * 1.3);

  if (isSinglePlayer) {
    textSize(fontSmall * 0.9);
    fill(210);
    text(`Level: ${currentLevel}`, cx, labelY + fontSmall * 2.5);
  }

  textSize(fontBig);
  const leftLabelX = pad + 26;
  const rightLabelX = width - pad - 26;

  textAlign(LEFT, CENTER);
  fill(80, 200, 255);
  text('P1', leftLabelX, centerYTop);
  fill(245);
  text(p1Score, leftLabelX + 60, centerYTop);

  textAlign(RIGHT, CENTER);
  fill(255, 140, 180);
  text('P2', rightLabelX, centerYTop);
  fill(245);
  text(p2Score, rightLabelX - 60, centerYTop);

  const econY = pad + hudHeight * 0.78;
  textSize(fontSmall);
  fill(220);

  textAlign(LEFT, CENTER);
  const p1GunTxt = p1GunLevel >= GUN_MAX_LEVEL ? 'Gun MAX' : `Gun Lv.${p1GunLevel}/${GUN_MAX_LEVEL}`;
  text(`Coins: ${p1Coins}   ${p1GunTxt}`, leftLabelX, econY);

  textAlign(RIGHT, CENTER);
  const p2GunTxt = p2GunLevel >= GUN_MAX_LEVEL ? 'Gun MAX' : `Gun Lv.${p2GunLevel}/${GUN_MAX_LEVEL}`;
  text(`Coins: ${p2Coins}   ${p2GunTxt}`, rightLabelX, econY);

  if (isBossStyleMode()) {
    const barWidth = (width - pad * 4) / 2.2;
    const barHeight = 14;
    const barY = pad + hudHeight - barHeight - 10;

    textSize(fontSmall * 0.95);
    fill(230);

    textAlign(LEFT, BOTTOM);
    const b1x = pad * 2;
    text('P1 Boss', b1x, barY - 3);
    drawHealthBar(b1x, barY, barWidth, barHeight, boss1 ? boss1.hp : 0, bossMaxHP, boss1 ? boss1.col : color(80));

    textAlign(RIGHT, BOTTOM);
    const b2x = width - pad * 2;
    text('P2 Boss', b2x, barY - 3);
    drawHealthBar(b2x - barWidth, barY, barWidth, barHeight, boss2 ? boss2.hp : 0, bossMaxHP, boss2 ? boss2.col : color(80));
  }

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

🔧 Subcomponents:

calculation HUD background panel fill(0, 0, 0, 170); rect(pad, pad, width - pad * 2, hudHeight, 18);

Draws a semi-transparent dark background for the HUD to make text readable.

conditional Boss health bars (boss modes only) if (isBossStyleMode()) { ... drawHealthBar(...); }

In boss modes, renders HP bars for both bosses at the bottom of the HUD.

push(); ... pop();
Saves and restores the graphics state (colors, text settings, rectMode, etc.) so HUD drawing doesn't affect later code.
const hudHeight = height * (isBossStyleMode() ? 0.2 : 0.16);
HUD is taller (20%) in boss modes to accommodate boss health bars; otherwise 16%.
const label = getModeLabel(); text(label, cx, labelY);
Displays the current game mode (e.g., 'Mode: Turbo (fast ball, small paddles)').
const controlText = isSinglePlayer ? 'Players: 1P (P1 vs AI)' : 'Players: 2P (P1 vs P2)'; text(controlText, ...);
Shows whether the game is in 1P or 2P mode.
if (isSinglePlayer) { text(`Level: ${currentLevel}`, ...); }
Displays the current level number only in 1P mode.
textAlign(LEFT, CENTER); fill(80, 200, 255); text('P1', leftLabelX, centerYTop); fill(245); text(p1Score, leftLabelX + 60, centerYTop);
Draws P1's label in cyan and their score in white on the left side.
textAlign(RIGHT, CENTER); fill(255, 140, 180); text('P2', rightLabelX, centerYTop); fill(245); text(p2Score, rightLabelX - 60, centerYTop);
Draws P2's label in pink and their score in white on the right side (right-aligned).
const p1GunTxt = p1GunLevel >= GUN_MAX_LEVEL ? 'Gun MAX' : `Gun Lv.${p1GunLevel}/${GUN_MAX_LEVEL}`; text(`Coins: ${p1Coins} ${p1GunTxt}`, ...);
Displays P1's coin count and gun level (or 'Gun MAX' if fully upgraded).
if (isBossStyleMode()) { ... drawHealthBar(...); }
In boss modes, renders health bars for both bosses at the bottom of the HUD panel.

drawShop()

drawShop() is the shop UI shell. It darkens the background, frames the shop panel, and calls drawShopCard() to render the individual upgrade cards. The layout uses proportional sizing (percentages of canvas size) so the shop adapts to any screen resolution. The semi-transparent overlay is a common UX pattern that communicates 'the game world is paused; focus on this dialog.'

function drawShop() {
  push();
  rectMode(CORNER);
  noStroke();

  fill(0, 0, 0, 190);
  rect(0, 0, width, height);

  const panelW = width * 0.78;
  const panelH = height * 0.6;
  const panelX = (width - panelW) / 2;
  const panelY = (height - panelH) / 2;

  stroke(255, 255, 255, 60);
  strokeWeight(2);
  fill(10, 12, 30, 240);
  rect(panelX, panelY, panelW, panelH, 20);
  noStroke();

  const cx = width / 2;

  textAlign(CENTER, TOP);
  fill(255);
  textSize(min(width, height) * 0.045);
  text('GUN SHOP', cx, panelY + 18);

  textSize(min(width, height) * 0.022);
  fill(210);
  text(
    'Earn coins by scoring and damaging the enemy boss. Spend coins to upgrade your gun.',
    cx,
    panelY + 60
  );
  fill(180);
  text('Press B to close shop.', cx, panelY + 86);

  const cardW = panelW * 0.38;
  const cardH = panelH * 0.46;
  const gap = panelW * 0.04;
  const cardY = panelY + panelH * 0.26;
  const p1X = panelX + panelW * 0.08;
  const p2X = p1X + cardW + gap;

  drawShopCard(p1X, cardY, cardW, cardH, 1);
  drawShopCard(p2X, cardY, cardW, cardH, 2);

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

🔧 Subcomponents:

calculation Darkened background overlay fill(0, 0, 0, 190); rect(0, 0, width, height);

Dims the game world by drawing a semi-opaque black rectangle over the entire canvas.

calculation Shop panel frame stroke(255, 255, 255, 60); strokeWeight(2); fill(10, 12, 30, 240); rect(panelX, panelY, panelW, panelH, 20);

Draws the central shop panel with a subtle white border and dark background.

calculation Side-by-side gun upgrade cards drawShopCard(p1X, cardY, cardW, cardH, 1); drawShopCard(p2X, cardY, cardW, cardH, 2);

Renders two upgrade cards (one per player) with their gun level, coins, and upgrade button.

fill(0, 0, 0, 190); rect(0, 0, width, height);
Draws a semi-transparent black overlay over the entire canvas, darkening the game world to focus attention on the shop.
const panelW = width * 0.78; const panelH = height * 0.6; const panelX = (width - panelW) / 2; const panelY = (height - panelH) / 2;
Calculates the shop panel's size and position: 78% of canvas width and 60% of height, centered.
stroke(255, 255, 255, 60); strokeWeight(2); fill(10, 12, 30, 240); rect(panelX, panelY, panelW, panelH, 20);
Draws the shop panel with a thin white border and a dark blue-black background.
textSize(min(width, height) * 0.045); text('GUN SHOP', cx, panelY + 18);
Draws the title 'GUN SHOP' in large white text.
const cardW = panelW * 0.38; const cardH = panelH * 0.46; const gap = panelW * 0.04; const p1X = panelX + panelW * 0.08; const p2X = p1X + cardW + gap;
Calculates the dimensions and positions of the two upgrade cards: each is 38% of panel width, with a 4% gap between them.
drawShopCard(p1X, cardY, cardW, cardH, 1); drawShopCard(p2X, cardY, cardW, cardH, 2);
Calls drawShopCard() twice to render the upgrade cards for players 1 and 2.

keyPressed()

keyPressed() is the main input dispatcher. It runs once per key press and routes the input to the appropriate handler (shop, mode selection, shooting, pausing). Notice the careful order: shop keys are handled first and return early, preventing them from being interpreted as game commands while the shop is open. The isSinglePlayer checks on P2 shooting and M-key upgrades enforce single-player mode restrictions. This function is the nerve center of user interaction.

function keyPressed() {
  if (key === 'b' || key === 'B') {
    isShopOpen = !isShopOpen;
    isRunning = false;
    return;
  }

  if (isShopOpen) {
    if (key === 'z' || key === 'Z') {
      buyGun(1);
    } else if (key === 'm' || key === 'M') {
      if (!isSinglePlayer) buyGun(2);
    }
    return;
  }

  if (key === ' ') {
    if (!(isBossStyleMode() && bossFightOver)) {
      isRunning = !isRunning;
    }
  } else if (key === '1') {
    setMode('classic');
  } else if (key === '2') {
    setMode('turbo');
  } else if (key === '3') {
    setMode('boss');
  } else if (key === '4') {
    setMode('mini');
  } else if (key === '5') {
    setMode('giant');
  } else if (key === '6') {
    setMode('gravity');
  } else if (key === '7') {
    setMode('portal');
  } else if (key === '8') {
    setMode('reverse');
  } else if (key === '9') {
    setMode('sniper');
  } else if (key === '0') {
    setMode('mayhem');
  } else if (key === 'h' || key === 'H') {
    setMode('blackhole');
  } else if (key === 'p' || key === 'P') {
    toggleSinglePlayer();
  } else if ((key === 'r' || key === 'R') && isBossStyleMode()) {
    resetBossFight();
  } else if (key === 'f' || key === 'F') {
    shoot(1);
  } else if (key === 'j' || key === 'J') {
    if (!isSinglePlayer) shoot(2);
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

conditional Shop open/close (B key) if (key === 'b' || key === 'B') { isShopOpen = !isShopOpen; isRunning = false; return; }

Toggles the shop and pauses the game.

conditional In-shop gun purchases if (isShopOpen) { if (key === 'z' || key === 'Z') { buyGun(1); } else if (key === 'm' || key === 'M') { if (!isSinglePlayer) buyGun(2); } return; }

When the shop is open, Z and M trigger gun upgrades (M disabled in 1P mode).

switch-case Mode switching (keys 1–0 and H) else if (key === '1') { setMode('classic'); } ... else if (key === 'h' || key === 'H') { setMode('blackhole'); }

Pressing a number or H instantly switches the game mode.

if (key === 'b' || key === 'B') { isShopOpen = !isShopOpen; isRunning = false; return; }
Pressing B toggles the shop open/closed and pauses the game. The return statement skips all other key checks.
if (isShopOpen) { if (key === 'z' || key === 'Z') { buyGun(1); } ... return; }
If the shop is open, Z and M trigger gun upgrades for players 1 and 2 (M is disabled in 1P mode).
if (key === ' ') { if (!(isBossStyleMode() && bossFightOver)) { isRunning = !isRunning; } }
SPACE toggles pause, but not if a boss fight has ended (forcing the player to press R instead).
else if (key === '1') { setMode('classic'); } ... else if (key === '0') { setMode('mayhem'); }
Keys 1–9 and 0 switch between the 10 main game modes.
else if (key === 'h' || key === 'H') { setMode('blackhole'); }
Pressing H switches to Black Hole mode.
else if (key === 'p' || key === 'P') { toggleSinglePlayer(); }
Pressing P toggles between 1P (AI) and 2P (human) modes.
else if ((key === 'r' || key === 'R') && isBossStyleMode()) { resetBossFight(); }
In boss modes, pressing R resets the boss fight (useful when the fight ends or you want to retry).
else if (key === 'f' || key === 'F') { shoot(1); } else if (key === 'j' || key === 'J') { if (!isSinglePlayer) shoot(2); }
F fires player 1's gun; J fires player 2's gun (disabled in 1P mode).

setMode(mode)

setMode() is the mode-switching function. It's called when the player presses a mode key (1–0 or H) and performs a complete game reset: clears state, resets scores and coins, and reconfigures game objects via initGameObjects(). This design makes modes truly independent—no carryover of power-ups or scores between modes, which keeps the experience fresh each time you switch.

function setMode(mode) {
  if (mode === gameMode) return;
  gameMode = mode;
  isRunning = false;
  isShopOpen = false;
  p1Score = 0;
  p2Score = 0;
  bossFightOver = false;
  bossWinner = null;
  bullets = [];
  p1Coins = 0;
  p2Coins = 0;
  p1GunLevel = 0;
  p2GunLevel = 0;
  p1ShootCooldown = 0;
  p2ShootCooldown = 0;
  initGameObjects();
}
Line-by-line explanation (8 lines)
if (mode === gameMode) return;
If the requested mode is already active, exits early—no need to reset everything.
gameMode = mode;
Sets the new game mode.
isRunning = false; isShopOpen = false;
Stops the game and closes the shop.
p1Score = 0; p2Score = 0;
Resets both players' scores to 0.
bossFightOver = false; bossWinner = null;
Resets boss fight state in case we were in a boss mode.
bullets = [];
Clears all in-flight bullets from the previous mode.
p1Coins = 0; p2Coins = 0; p1GunLevel = 0; p2GunLevel = 0; p1ShootCooldown = 0; p2ShootCooldown = 0;
Resets all gun/economy state, forcing players to re-earn coins and re-buy upgrades in the new mode.
initGameObjects();
Reconfigures paddles, ball, bosses, and physics based on the new mode.

📦 Key Variables

gameMode string

Stores the current game mode ('classic', 'turbo', 'boss', 'mini', 'giant', 'gravity', 'portal', 'reverse', 'sniper', 'mayhem', 'blackhole'). Controls which rules and physics apply.

let gameMode = 'classic';
isRunning boolean

True when the game is actively playing; false when paused. Controls whether ball and bullet physics update.

let isRunning = false;
isSinglePlayer boolean

True in 1P mode (P1 vs AI); false in 2P mode (P1 vs P2). Controls paddle input and AI behavior.

let isSinglePlayer = false;
ballX, ballY, ballVX, ballVY number

Ball position (X, Y) and velocity (VX, VY in pixels per frame). Updated every frame to create animation.

let ballX = width / 2; let ballVY = 0;
ballSize, ballSpeed number

Ball radius and base movement speed. Scaled per mode.

let ballSize = 15; let ballSpeed = 3;
leftX, leftY, rightX, rightY number

Paddle positions. Left paddle is player 1; right paddle is player 2 (or AI in 1P).

let leftX = 20; let rightY = height / 2;
paddleW, paddleH, paddleSpeed number

Paddle width, height, and movement speed per frame. Scaled per mode.

let paddleH = 80; let paddleSpeed = 2;
p1Score, p2Score number

Current score for players 1 and 2. Incremented when opponent fails to defend.

let p1Score = 0;
p1Coins, p2Coins number

Earned currency. Incremented by scoring and damaging bosses. Spent to upgrade guns.

let p1Coins = 0;
p1GunLevel, p2GunLevel number

Gun upgrade level (0 = no gun, 1–3 = levels). Controls bullet speed and cooldown.

let p1GunLevel = 0;
p1ShootCooldown, p2ShootCooldown number

Countdown frames until the next shot is allowed. Decremented each frame; blocks shooting when > 0.

let p1ShootCooldown = 0;
bullets array

Array of bullet objects {x, y, vx, vy, r, owner}. Updated and drawn every frame.

let bullets = [];
boss1, boss2 object

Boss objects {x, y, w, h, hp, col} in boss modes. Null in normal modes.

let boss1 = null; // {x: 50, y: 300, w: 20, h: 400, hp: 10, col: blue}
bossMaxHP number

Maximum health for each boss. Used to calculate health bar width.

let bossMaxHP = 10;
bossFightOver boolean

True when a boss fight ends (a boss reaches 0 HP). Prevents further gameplay until reset.

let bossFightOver = false;
modeGravity number

Vertical acceleration in Gravity mode. Added to ballVY each frame. 0 in other modes.

let modeGravity = 0.2;
modeVerticalWrap boolean

True in Portal mode; makes ball wrap to opposite vertical edge instead of bouncing.

let modeVerticalWrap = false;
modeReverseControls boolean

True in Reverse mode; flips paddle direction (W becomes down, S becomes up).

let modeReverseControls = false;
blackHoleX, blackHoleY, blackHoleRadius number

Position and size of the central gravity well in Black Hole mode. Zero when inactive.

let blackHoleX = width / 2; let blackHoleRadius = 50;
blackHoleStrengthBall, blackHoleStrengthBullet number

Gravity force magnitude for ball and bullets in Black Hole mode. Higher = stronger pull.

let blackHoleStrengthBall = 300;
isShopOpen boolean

True when the gun shop is displayed. Pauses the game and blocks normal input.

let isShopOpen = false;
currentLevel number

Current level in 1P mode (1, 2, 3, ...). Incremented on victory; decremented on defeat.

let currentLevel = 1;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG updateBall() paddle collision

Ball can rarely get stuck inside a paddle if velocity is very high or frame rate drops

💡 Use continuous collision detection or increase the collision buffer distance. For example, in the left paddle check, change ballX > leftX to ballX >= leftX - ballSize to catch collisions earlier.

PERFORMANCE updateBullets() main loop

Looping backward and using splice() is O(n) per bullet; with many bullets this becomes slow.

💡 Instead of splice(), mark bullets for deletion and filter the array once after the loop: bullets = bullets.filter(b => !b.remove). Or use an object pool to reuse bullet objects.

STYLE Global variables (top of sketch.js)

Over 30 global variables make the code hard to track; state is scattered across the scope

💡 Organize globals into logical objects: const gameState = {isRunning, currentLevel, ...}, const paddleState = {leftX, leftY, paddleSpeed, ...}, const bulletState = {bullets, p1ShootCooldown, ...}. This reduces pollution and groups related state.

BUG handleInput() and updateRightPaddleAI()

Reverse mode flips controls but the AI doesn't know about it; AI moves opposite in Reverse mode

💡 Pass modeReverseControls to updateRightPaddleAI() and flip the AI's target Y accordingly, or apply dirSign to the AI's step calculation.

FEATURE drawBall() and drawPaddles()

Ball and paddles have fixed colors per mode; no visual feedback when health is low or in danger

💡 Make paddle color pulse or lighten as the ball approaches, or tint the ball red when it's moving very fast. This improves visual clarity and player anticipation.

PERFORMANCE drawBackground()

Gradient is recalculated every frame using a loop that runs height/6 times (e.g., 150+ times per frame)

💡 Pre-render the gradient to a createGraphics() buffer once in setup(), then draw that buffer every frame. This drops a constant-time loop to a single image() call.

🔄 Code Flow

Code flow showing setup, initgameobjects, draw, handleinput, updateball, updatebullets, shoot, applyblackholegravitytoball, drawhud, drawshop, keypressed, setmode

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

graph TD start[Start] --> setup[setup] setup --> initgameobjects[initGameObjects] setup --> draw[draw loop] click setup href "#fn-setup" click initgameobjects href "#fn-initgameobjects" click draw href "#fn-draw" draw --> game-loop-logic[Game Loop Logic] game-loop-logic --> input-gate[Input Gate] input-gate --> handleinput[handleInput] click game-loop-logic href "#sub-game-loop-logic" click input-gate href "#sub-input-gate" click handleinput href "#fn-handleinput" handleinput --> reverse-mode-check[Reverse Mode Check] reverse-mode-check --> p1-controls[P1 Controls] reverse-mode-check --> p2-or-ai[P2 or AI] p1-controls --> clamp-paddles[Clamp Paddles] p2-or-ai --> clamp-paddles click reverse-mode-check href "#sub-reverse-mode-check" click p1-controls href "#sub-p1-controls" click p2-or-ai href "#sub-p2-or-ai" click clamp-paddles href "#sub-clamp-paddles" draw --> updateball[updateBall] click updateball href "#fn-updateball" updateball --> gravity-apply[Gravity Apply] gravity-apply --> blackhole-gravity[Black Hole Gravity] blackhole-gravity --> blackhole-swallow[Black Hole Swallow] blackhole-swallow --> vertical-wall[Vertical Wall] vertical-wall --> left-paddle-collision[Left Paddle Collision] vertical-wall --> right-paddle-collision[Right Paddle Collision] left-paddle-collision --> boss-vs-normal[Boss vs Normal] click gravity-apply href "#sub-gravity-apply" click blackhole-gravity href "#sub-blackhole-gravity" click blackhole-swallow href "#sub-blackhole-swallow" click vertical-wall href "#sub-vertical-wall" click left-paddle-collision href "#sub-left-paddle-collision" click right-paddle-collision href "#sub-right-paddle-collision" click boss-vs-normal href "#sub-boss-vs-normal" draw --> updatebullets[updateBullets] click updatebullets href "#fn-updatebullets" updatebullets --> bullet-loop[Bullet Loop] bullet-loop --> bullet-gravity[Bullet Gravity] bullet-gravity --> bullet-event-horizon[Bullet Event Horizon] bullet-event-horizon --> bullet-offscreen[Bullet Offscreen] bullet-offscreen --> bullet-ball-collision[Bullet to Ball Collision] bullet-ball-collision --> bullet-boss-collision[Bullet to Boss Collision] click bullet-loop href "#sub-bullet-loop" click bullet-gravity href "#sub-bullet-gravity" click bullet-event-horizon href "#sub-bullet-event-horizon" click bullet-offscreen href "#sub-bullet-offscreen" click bullet-ball-collision href "#sub-bullet-ball-collision" click bullet-boss-collision href "#sub-bullet-boss-collision" draw --> drawhud[drawHUD] click drawhud href "#fn-drawhud" drawhud --> hud-bg[HUD Background] hud-bg --> boss-health-section[Boss Health Section] click hud-bg href "#sub-hud-bg" click boss-health-section href "#sub-boss-health-section" draw --> drawshop[drawShop] click drawshop href "#fn-drawshop" drawshop --> shop-overlay[Shop Overlay] shop-overlay --> shop-panel[Shop Panel] shop-panel --> shop-cards[Shop Cards] click shop-overlay href "#sub-shop-overlay" click shop-panel href "#sub-shop-panel" click shop-cards href "#sub-shop-cards" keypressed[keyPressed] --> shop-toggle[Shop Toggle] shop-toggle --> shop-purchases[Shop Purchases] click keypressed href "#fn-keypressed" click shop-toggle href "#sub-shop-toggle" click shop-purchases href "#sub-shop-purchases" setmode[setMode] --> mode-selection[Mode Selection] mode-selection --> mode-config-switch[Mode Config Switch] click setmode href "#fn-setmode" click mode-selection href "#sub-mode-selection" click mode-config-switch href "#sub-mode-config-switch"

❓ Frequently Asked Questions

What visual elements can users expect from the Neon Pong sketch?

The Neon Pong sketch features a vibrant, animated Pong arena filled with neon colors, dynamic paddle movements, and exciting effects from various game modes and power-ups.

How can players engage with the Neon Pong game?

Players can engage by choosing between single-player against an AI or two-player mode, moving paddles using designated keys, shooting bullets, and upgrading their guns in the shop.

What creative coding techniques are showcased in the Neon Pong sketch?

The sketch demonstrates techniques such as game state management, collision detection, and dynamic game modes, all while utilizing interactive controls for an engaging gameplay experience.

Preview

neon pong - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of neon pong - Code flow showing setup, initgameobjects, draw, handleinput, updateball, updatebullets, shoot, applyblackholegravitytoball, drawhud, drawshop, keypressed, setmode
Code Flow Diagram