Sketch 2026-02-19 21:55

This sketch recreates the Cookie Clicker game mechanic with a giant clickable cookie on the left and upgradeable buildings on the right. It features a day/night cycle, twinkling stars, animated click effects, and a progression system where players earn cookies through clicking and passive generation.

🧪 Try This!

Experiment with the code by making these changes:

  1. Speed up the day/night cycle — Change the secondsPerDay constant from 1200 to 300, making a full day/night cycle happen in 5 minutes instead of 20 minutes—the sky will transition much faster
  2. Make the first upgrade cost 1 cookie — Change the 'Curseur' upgrade's base cost from 15 to 1, letting players afford their first mini upgrade instantly after just one click
  3. Turn the cookie blue — Change the hover color of the cookie from orange to a bright cyan color to give it a completely different vibe
  4. Double the building height variation — Change the building height range from (height * 0.15, height * 0.45) to (height * 0.05, height * 0.85) to make buildings much more varied and dramatic
  5. Inflate upgrade costs faster
  6. Fill the night sky with stars — Double the star count from 120 to 240 for a much denser, more crowded night sky
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch implements a fully functional Cookie Clicker-style game with a giant cookie button on the left side and an upgradeable building shop on the right. The game is visually rich, featuring a scrolling skyline that changes between day and night, twinkling stars at night, and animated ripple effects when you click. The code demonstrates key p5.js techniques including mouse interaction (mousePressed, isMouseOverCookie), game state management (cookies, upgrades with levels and costs), UI layout (split-panel design), and smooth animations (scaling, day/night lerping, particle effects).

The code is organized into three logical zones: the main cookie button area with mini upgrades at the top-right, a statistics header showing your current resources, and a scrollable right panel displaying building upgrades. You will learn how to build interactive game interfaces, manage complex game state with arrays of objects, create smooth color transitions, handle mouse wheel scrolling, and structure a larger p5.js project with helper functions for layout, cost calculations, and unlock conditions.

⚙️ How It Works

  1. When setup() runs, the game initializes all variables, creates the canvas at full window size, and populates the clickUpgrades array (small quick bonuses) and autoUpgrades array (buildings that generate cookies per second). It also generates a random cityscape with buildings and stars.
  2. Every frame, draw() updates the day/night cycle, adds passive cookie income, and redraws everything: the scrollable city background with day/night color lerping, the stats header, the giant cookie button, visible mini upgrades, the building shop panel, and floating tooltip boxes.
  3. When you click the giant cookie, mousePressed() checks if your cursor is within the cookie's circular hitbox using distance math. If so, it adds cookiesPerClick to your total, triggers a scaling animation, and creates a ripple effect.
  4. Clicking any upgrade card in the shop or mini upgrades at the top calls buyUpgrade(), which deducts the cost (which scales with exponential growth using Math.pow), increases the upgrade's level, and adds its bonus to either cookiesPerClick (click upgrades) or cookiesPerSecond (auto upgrades).
  5. The day/night cycle uses sin() to smoothly interpolate between night and day colors every 20 minutes. At night, stars twinkle using layered sine waves, and building windows glow orange. Scrolling the right panel works via mouseWheel(), which adjusts panelScroll and is constrained within calculated limits.
  6. Click effects are stored in an array and animated each frame—their radius grows and alpha fades until they disappear. Mini upgrades unlock when you reach certain maxCookiesEver thresholds, creating a progression reward system.

🎓 Concepts You'll Learn

Mouse interaction and collision detectionGame state management with objects and arraysColor interpolation and day/night cyclesUpgrade systems with exponential cost scalingUI layout and scrollable panelsAnimation loops and particle effectsUnlock conditions and progression gates

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It initializes your game state and prepares the canvas for drawing. Any code that should run only once belongs here.

function setup() {
  createCanvas(windowWidth, windowHeight);
  textFont('sans-serif');

  initGame();
  updateLayout();
  buildCity();
  buildStars();
}
Line-by-line explanation (6 lines)
createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire window, allowing the game to be responsive
textFont('sans-serif');
Sets the font for all text rendered in the sketch to a clean sans-serif typeface
initGame();
Initializes all game variables (cookies, upgrades arrays, click counters) to their starting values
updateLayout();
Calculates screen-dependent positions like panelSplitX and cookieButton dimensions based on current canvas size
buildCity();
Randomly generates the cityscape skyline with varying building heights, widths, and window positions
buildStars();
Creates an array of 120 stars with random positions and twinkling speeds for the night sky

draw()

draw() runs 60 times per second. It is the heart of animation in p5.js: update your game state, then redraw everything. Order matters—draw background first, then foreground elements, then UI on top.

function draw() {
  hoveredTooltip = null;

  updateDayNight();
  updateCookiesPassive();
  updateScrollLimits();

  drawBackgroundCity();
  drawStatsHeader();
  drawMiniUpgrades();
  drawCookie();
  drawUpgradePanels();
  drawSmallHelpText();
  drawClickEffects();
  drawTooltip();
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

assignment Tooltip reset hoveredTooltip = null;

Clears the tooltip at the start of each frame so it only appears when hovering over upgrades in THIS frame

function-calls State updates updateDayNight(); updateCookiesPassive(); updateScrollLimits();

Updates game logic: time of day, passive income generation, and scroll constraints before drawing

function-calls Rendering pipeline drawBackgroundCity(); drawStatsHeader(); drawMiniUpgrades(); drawCookie(); drawUpgradePanels(); drawSmallHelpText(); drawClickEffects(); drawTooltip();

Draws everything in order from back to front: background, UI, interactive elements, and tooltips

hoveredTooltip = null;
Resets the tooltip each frame so it only shows when currently hovering over an upgrade
updateDayNight();
Increments dayTime based on deltaTime to smoothly animate the day/night color transition
updateCookiesPassive();
Adds cookies based on cookiesPerSecond and deltaTime, rewarding passive income each frame
updateScrollLimits();
Recalculates how far the right panel can scroll based on how many upgrades are visible
drawBackgroundCity();
Renders the animated sky gradient, city skyline, and stars—the deepest background layer
drawStatsHeader();
Draws the white box in the top-left showing current cookies, per-click bonus, per-second income, and total clicks
drawMiniUpgrades();
Draws the small square buttons in the top-right for click upgrades, handling hover states and tooltips
drawCookie();
Draws the giant cookie button in the center-left with hover color change and click-to-scale animation
drawUpgradePanels();
Draws the building shop panel on the right side with scrollable upgrade cards
drawSmallHelpText();
Draws instructional text in the bottom-left corner telling players how to play
drawClickEffects();
Animates and draws expanding white circles that appear where the player clicks
drawTooltip();
If hoveredTooltip is set, draws a tooltip box showing upgrade name, description, cost, and level

initGame()

initGame() sets up all the game's data structures. The clickUpgrades and autoUpgrades arrays are the heart of the progression system—each upgrade is an object with properties that control its unlock condition, cost, name, and visual appearance. Understanding this structure helps you design new upgrades or modify existing ones.

🔬 Each upgrade is an object with properties like name, baseCost, bonus, and icon. Try changing the bonus from 1 to 5—how much stronger does the first upgrade become?

    {
      type: 'click',
      name: 'Curseur',
      baseCost: 15,
      level: 0,
      bonus: 1,
      description: '+1 cookie / clic',
      icon: '👆',
      color: [200, 150, 100],
      unlockAtMaxCookies: 0
    },
function initGame() {
  cookies = 0;
  cookiesPerClick = 1;
  cookiesPerSecond = 0;
  totalClicks = 0;
  dayTime = 0;
  maxCookiesEver = 0;

  // Améliorations de CLIC (petites en haut à droite)
  clickUpgrades = [
    {
      type: 'click',
      name: 'Curseur',
      baseCost: 15,
      level: 0,
      bonus: 1,
      description: '+1 cookie / clic',
      icon: '👆',
      color: [200, 150, 100],
      unlockAtMaxCookies: 0
    },
    {
      type: 'click',
      name: 'Pépites choco',
      baseCost: 50,
      level: 0,
      bonus: 2,
      description: '+2 cookies / clic',
      icon: '🍫',
      color: [160, 100, 60],
      unlockAtMaxCookies: 50
    },
    {
      type: 'click',
      name: 'Grand-mère',
      baseCost: 100,
      level: 0,
      bonus: 5,
      description: '+5 cookies / clic',
      icon: '👵',
      color: [220, 180, 140],
      unlockAtMaxCookies: 200
    },
    {
      type: 'click',
      name: 'Cookie doré',
      baseCost: 500,
      level: 0,
      bonus: 25,
      description: '+25 cookies / clic',
      icon: '⭐',
      color: [255, 215, 100],
      unlockAtMaxCookies: 1000
    },
    {
      type: 'click',
      name: 'Main magique',
      baseCost: 2000,
      level: 0,
      bonus: 100,
      description: '+100 cookies / clic',
      icon: '✨',
      color: [180, 140, 255],
      unlockAtMaxCookies: 5000
    },
    {
      type: 'click',
      name: 'Super curseur',
      baseCost: 10000,
      level: 0,
      bonus: 500,
      description: '+500 cookies / clic',
      icon: '💫',
      color: [255, 150, 200],
      unlockAtMaxCookies: 25000
    }
  ];

  // Améliorations AUTOMATIQUES (panneau à droite)
  autoUpgrades = [
    {
      type: 'auto',
      name: 'Mamie',
      baseCost: 100,
      level: 0,
      bonus: 1,
      description: '+1 cookie / s',
      icon: '👵',
      color: [220, 180, 140]
    },
    {
      type: 'auto',
      name: 'Ferme',
      baseCost: 500,
      level: 0,
      bonus: 8,
      description: '+8 cookies / s',
      icon: '🌾',
      color: [150, 200, 100]
    },
    {
      type: 'auto',
      name: 'Mine',
      baseCost: 3000,
      level: 0,
      bonus: 47,
      description: '+47 cookies / s',
      icon: '⛏️',
      color: [140, 140, 140]
    },
    {
      type: 'auto',
      name: 'Usine',
      baseCost: 10000,
      level: 0,
      bonus: 260,
      description: '+260 cookies / s',
      icon: '🏭',
      color: [120, 120, 150]
    },
    {
      type: 'auto',
      name: 'Banque',
      baseCost: 40000,
      level: 0,
      bonus: 1400,
      description: '+1400 cookies / s',
      icon: '🏦',
      color: [180, 200, 100]
    },
    {
      type: 'auto',
      name: 'Temple',
      baseCost: 200000,
      level: 0,
      bonus: 7800,
      description: '+7800 cookies / s',
      icon: '🛕',
      color: [200, 180, 250]
    },
    {
      type: 'auto',
      name: 'Tour magique',
      baseCost: 1000000,
      level: 0,
      bonus: 44000,
      description: '+44000 cookies / s',
      icon: '🗼',
      color: [150, 100, 200]
    }
  ];
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

assignment Game state reset cookies = 0; cookiesPerClick = 1; cookiesPerSecond = 0; totalClicks = 0; dayTime = 0; maxCookiesEver = 0;

Resets all core gameplay variables to their starting values for a fresh game

array-initialization Click upgrades array clickUpgrades = [...]

Defines the six small bonus upgrades that increase cookies-per-click, each with unlock thresholds

array-initialization Auto upgrades array autoUpgrades = [...]

Defines the seven buildings that generate passive income, each with increasing cost and production

cookies = 0;
Starts with 0 cookies; the player earns more by clicking and buying generators
cookiesPerClick = 1;
Base value: each click on the cookie button earns 1 cookie (upgrades add to this)
cookiesPerSecond = 0;
No passive income initially; buying auto upgrades increases this value
totalClicks = 0;
Tracks total number of times the player has clicked the cookie button (shown in stats)
dayTime = 0;
A value from 0 to 1 that cycles through a full day/night period; used in sin() for smooth color transitions
maxCookiesEver = 0;
Tracks the highest cookie count ever reached; used to unlock mini upgrades at certain thresholds

updateLayout()

updateLayout() makes the game responsive to window resizing. When windowResized() is called, this function recalculates all positions and sizes based on the new dimensions. The min() function ensures the cookie button stays reasonably proportioned no matter the screen size.

function updateLayout() {
  panelSplitX = width * 0.6;

  const btnSize = min(width * 0.25, height * 0.35, 280);

  cookieButton = {
    x: panelSplitX / 2 - btnSize / 2,
    y: height * 0.5 - btnSize / 2,
    size: btnSize
  };
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

calculation Panel split calculation panelSplitX = width * 0.6;

Divides the canvas at 60% width: left side for the cookie, right side for the shop

calculation Cookie button sizing const btnSize = min(width * 0.25, height * 0.35, 280);

Makes the cookie button responsive: scales with window but never smaller than 25% width, 35% height, or larger than 280 pixels

assignment Cookie button positioning cookieButton = { x: panelSplitX / 2 - btnSize / 2, y: height * 0.5 - btnSize / 2, size: btnSize };

Centers the cookie button in the left half of the screen at 50% height

panelSplitX = width * 0.6;
Calculates the vertical line that divides the play area (left) from the shop (right) at 60% of the canvas width
const btnSize = min(width * 0.25, height * 0.35, 280);
Makes the cookie button size responsive: it scales with the window but is capped at 280 pixels so it doesn't get too large on huge screens
x: panelSplitX / 2 - btnSize / 2,
Centers the cookie horizontally within the left half: half of 60% (30%) minus half the button's size
y: height * 0.5 - btnSize / 2,
Centers the cookie vertically: 50% down the canvas minus half the button's size

buildCity()

buildCity() generates a procedural cityscape. Each building is an object containing x, y, width, height, color, and an array of windows. This approach—storing visual elements as data and drawing them in draw()—lets you animate the whole city without regenerating it every frame.

🔬 These lines control building size variety. What happens if you change the width range to random(60, 120) to make taller, wider buildings?

    const w = random(40, 80);
    const h = random(height * 0.15, height * 0.45);
function buildCity() {
  buildings = [];
  const baseLine = height * 0.75;
  let x = -40;

  while (x < width + 50) {
    const w = random(40, 80);
    const h = random(height * 0.15, height * 0.45);
    const y = baseLine - h;
    const col = color(random(35, 70), random(35, 70), random(80, 160));

    const windows = [];
    const margin = 10;
    const winW = 8;
    const winH = 10;
    const stepX = 14;
    const stepY = 18;

    for (let wx = x + margin; wx < x + w - margin; wx += stepX) {
      for (let wy = y + margin; wy < y + h - margin; wy += stepY) {
        if (random() < 0.45) {
          windows.push({ x: wx, y: wy, w: winW, h: winH });
        }
      }
    }

    buildings.push({ x, y, w, h, col, windows });
    x += w + random(10, 25);
  }
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

while-loop Building generation loop while (x < width + 50) { ... }

Generates random buildings across the entire canvas width, starting off-screen left and continuing off-screen right

nested-for-loop Window placement grid for (let wx = x + margin; wx < x + w - margin; wx += stepX) { for (let wy = y + margin; wy < y + h - margin; wy += stepY) { if (random() < 0.45) { windows.push(...); } } }

Creates a grid of potential window positions on each building; 45% of them are randomly lit to add visual variety

buildings = [];
Clears the buildings array and starts fresh, allowing the skyline to regenerate when windowResized() calls this
const baseLine = height * 0.75;
Sets the ground line at 75% down the canvas; buildings are drawn from this line upward
let x = -40;
Starts building generation off-screen to the left so the skyline begins with a partial building
while (x < width + 50) {
Keeps generating buildings until they extend past the right edge of the canvas
const w = random(40, 80);
Each building gets a random width between 40 and 80 pixels for variation
const h = random(height * 0.15, height * 0.45);
Each building gets a random height between 15% and 45% of the canvas height
const y = baseLine - h;
Positions the top of the building so its bottom aligns with the baseLine
const col = color(random(35, 70), random(35, 70), random(80, 160));
Gives each building a random bluish color (with more variation in blue) for a nighttime cityscape feel
if (random() < 0.45) {
Only adds a window to this grid position 45% of the time, creating a scattered, realistic window pattern
x += w + random(10, 25);
Moves to the next building position: its width plus a random gap (10-25 pixels) for spacing

buildStars()

buildStars() pre-generates all star data so drawStars() can animate them efficiently every frame. Each star is an object with position, size, and animation parameters. This is the same pattern used for buildings—storing data, then drawing it.

function buildStars() {
  stars = [];
  const starCount = 120;
  for (let i = 0; i < starCount; i++) {
    stars.push({
      x: random(width),
      y: random(height * 0.65),
      size: random(1, 3),
      twinkleSpeed: random(0.5, 1.5),
      phase: random(TWO_PI)
    });
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

for-loop Star generation loop for (let i = 0; i < starCount; i++) { stars.push(...); }

Creates 120 star objects with randomized positions, sizes, and twinkling animations

stars = [];
Clears the stars array to regenerate them fresh when the window resizes
const starCount = 120;
Defines how many stars to create; this is a tunable that affects the density of the night sky
x: random(width),
Places each star at a random horizontal position across the entire canvas width
y: random(height * 0.65),
Places stars only in the upper 65% of the canvas (above the buildings) for a realistic sky effect
size: random(1, 3),
Each star gets a random size between 1 and 3 pixels for depth variation
twinkleSpeed: random(0.5, 1.5),
Controls how fast each star twinkles; faster values create quicker pulsing, slower values create gentler fading
phase: random(TWO_PI)
Each star starts at a random point in its twinkling cycle so they don't all pulse in sync

updateDayNight()

updateDayNight() creates a smooth, repeating time cycle using a fractional value from 0 to 1. This value is used in sin() to interpolate between night and day colors. The pattern—fractional timer → sin() → lerp()—is one of the most elegant ways to create smooth, cyclical animations in p5.js.

function updateDayNight() {
  const secondsPerDay = 1200;
  dayTime += (deltaTime / 1000) / secondsPerDay;
  if (dayTime > 1) {
    dayTime -= 1;
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

calculation Time increment dayTime += (deltaTime / 1000) / secondsPerDay;

Advances dayTime based on real time, looping once per 1200 seconds (20 minutes)

conditional Time wrapping if (dayTime > 1) { dayTime -= 1; }

Resets dayTime to 0 when it exceeds 1, creating a continuous cycle

const secondsPerDay = 1200;
Defines how many real seconds make up one full day/night cycle in the game; 1200 seconds = 20 minutes
dayTime += (deltaTime / 1000) / secondsPerDay;
Increments dayTime: deltaTime is milliseconds since last frame, divide by 1000 to get seconds, then divide by secondsPerDay to get a fractional day increment
if (dayTime > 1) {
Checks if the day cycle is complete (dayTime reached 1 or more)
dayTime -= 1;
Wraps dayTime back to 0, starting a new day/night cycle

addCookies()

addCookies() is a helper function that centralizes cookie changes. Whenever the game adds or removes cookies, it uses this function. This makes it easy to add side effects (like sounds, animations, or tracking) in one place.

function addCookies(amount) {
  cookies += amount;
  if (cookies < 0) cookies = 0;
  if (cookies > maxCookiesEver) {
    maxCookiesEver = cookies;
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

assignment Cookie increment cookies += amount;

Adds the given amount to the player's total cookies

conditional Clamp to zero if (cookies < 0) cookies = 0;

Prevents cookies from going negative (shouldn't happen, but defensive coding)

conditional Update highest ever if (cookies > maxCookiesEver) { maxCookiesEver = cookies; }

Tracks the all-time high to unlock mini upgrades at certain thresholds

cookies += amount;
Adds the amount to the player's current cookie count (can be positive from clicking/passive income or negative from buying upgrades)
if (cookies < 0) cookies = 0;
Safety check: prevents cookies from going below 0, though the game only deducts cookies when affordable
if (cookies > maxCookiesEver) {
Checks if the new cookie count is higher than any previous count
maxCookiesEver = cookies;
Updates the all-time maximum, used to unlock new mini upgrades as the player progresses

updateCookiesPassive()

updateCookiesPassive() runs every frame (60 times per second) and adds fractional cookies based on cookiesPerSecond. Because deltaTime changes slightly each frame, this creates smooth, frame-rate-independent passive income. The formula—(rate per second) × (time elapsed in seconds)—works for any animation using deltaTime.

function updateCookiesPassive() {
  const gain = cookiesPerSecond * (deltaTime / 1000);
  addCookies(gain);
}
Line-by-line explanation (2 lines)
const gain = cookiesPerSecond * (deltaTime / 1000);
Calculates passive income for this frame: multiplies per-second rate by the time elapsed (deltaTime in milliseconds, divided by 1000 to get seconds)
addCookies(gain);
Adds the calculated passive income to the player's cookie total using the addCookies helper function

drawBackgroundCity()

drawBackgroundCity() is the most complex drawing function, but it demonstrates powerful p5.js techniques: lerp() for smooth interpolation, sin() for cyclical animation, and for-loops to build gradients with many thin stripes. The day/night system is entirely driven by a single value (dayFactor) that influences every color in the scene.

🔬 Each line defines a color channel for day vs night. What if you change nightB from lerp(20, 70, t) to lerp(100, 150, t) to make the night sky much less blue and more gray?

    const dayR = lerp(100, 180, t);
    const dayG = lerp(150, 220, t);
    const dayB = lerp(200, 255, t);

    const nightR = lerp(3, 10, t);
    const nightG = lerp(8, 25, t);
    const nightB = lerp(20, 70, t);
function drawBackgroundCity() {
  noStroke();

  const angle = dayTime * TWO_PI;
  const dayFactor = (sin(angle - HALF_PI) + 1) / 2;

  for (let y = 0; y < height; y += 4) {
    const t = y / height;

    const dayR = lerp(100, 180, t);
    const dayG = lerp(150, 220, t);
    const dayB = lerp(200, 255, t);

    const nightR = lerp(3, 10, t);
    const nightG = lerp(8, 25, t);
    const nightB = lerp(20, 70, t);

    const r = lerp(nightR, dayR, dayFactor);
    const g = lerp(nightG, dayG, dayFactor);
    const b = lerp(nightB, dayB, dayFactor);

    fill(r, g, b);
    rect(0, y, width, 4);
  }

  drawStars(dayFactor);

  const groundDark = color(5, 10, 20);
  const groundLight = color(80, 120, 160);
  const groundCol = lerpColor(groundDark, groundLight, dayFactor * 0.9);
  fill(groundCol);
  rect(0, height * 0.75, width, height * 0.25);

  const windowAlpha = lerp(220, 20, dayFactor);
  for (const b of buildings) {
    const buildingBrightness = 1 + dayFactor * 0.5;
    fill(
      red(b.col) * buildingBrightness,
      green(b.col) * buildingBrightness,
      blue(b.col) * buildingBrightness
    );
    rect(b.x, b.y, b.w, b.h);

    const roofCol = color(
      red(b.col) * buildingBrightness + 30 * dayFactor,
      green(b.col) * buildingBrightness + 30 * dayFactor,
      blue(b.col) * buildingBrightness + 40 * dayFactor
    );
    fill(roofCol);
    rect(b.x, b.y - 3, b.w, 3);

    fill(255, 230, 150, windowAlpha);
    for (const w of b.windows) {
      rect(w.x, w.y, w.w, w.h, 2);
    }
  }

  fill(30, 35, 50, 250);
  rect(panelSplitX, 0, width - panelSplitX, height);
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

calculation Day/night interpolation factor const angle = dayTime * TWO_PI; const dayFactor = (sin(angle - HALF_PI) + 1) / 2;

Converts dayTime (0–1) into a dayFactor (0–1) using sin() for smooth oscillation between night (0) and day (1)

for-loop Sky gradient stripes for (let y = 0; y < height; y += 4) { ... }

Draws 4-pixel horizontal stripes from top to bottom, each lerped between night blue and day cyan for a gradient sky

for-loop Building rendering for (const b of buildings) { ... }

Draws each building with brightness affected by dayFactor, plus glowing windows that fade at night

rect-draw Shop panel background fill(30, 35, 50, 250); rect(panelSplitX, 0, width - panelSplitX, height);

Draws a dark overlay on the right side of the canvas where the building shop is displayed

const angle = dayTime * TWO_PI;
Converts dayTime (0–1) into an angle (0–2π) for use in sin() to create a smooth oscillation
const dayFactor = (sin(angle - HALF_PI) + 1) / 2;
Uses sin() to create a value that smoothly oscillates from 0 (night) to 1 (day) and back. sin() ranges from -1 to 1, so (sin() + 1) / 2 maps it to 0–1
for (let y = 0; y < height; y += 4) {
Draws 4-pixel horizontal stripes across the canvas height to create a smooth gradient
const t = y / height;
Calculates a normalized vertical position (0 at top, 1 at bottom) to vary color with height for atmospheric perspective
const dayR = lerp(100, 180, t);
In daytime, the red channel goes from 100 (darker at bottom due to haze) to 180 (lighter at top)
const nightR = lerp(3, 10, t);
At night, the red channel is much darker, ranging from 3 to 10 for a deep blue-black sky
const r = lerp(nightR, dayR, dayFactor);
Interpolates between night and day red values using dayFactor: 0 = night, 1 = day
fill(r, g, b); rect(0, y, width, 4);
Fills with the calculated color and draws a 4-pixel tall stripe across the entire width
drawStars(dayFactor);
Calls a helper function to draw stars; dayFactor is passed so stars only appear at night
const windowAlpha = lerp(220, 20, dayFactor);
Calculates window transparency: 220 (bright and visible) at night, 20 (nearly invisible) during day
const buildingBrightness = 1 + dayFactor * 0.5;
Multiplies building colors by brightness: 1 (normal) at night, 1.5 (brightened) during day
fill(30, 35, 50, 250); rect(panelSplitX, 0, width - panelSplitX, height);
Draws the dark background panel on the right side where upgrades are shown

drawStars()

drawStars() demonstrates layering animations: the day/night cycle controls visibility, and individual stars twinkle with their own offset phases. The pattern—sin() with offset phase—is how you create many animated objects that pulse independently.

function drawStars(dayFactor) {
  const nightFactor = 1 - dayFactor;
  if (!stars || nightFactor <= 0.01) return;

  noStroke();
  for (const s of stars) {
    const twinkle = 0.5 + 0.5 * sin(dayTime * TWO_PI * s.twinkleSpeed + s.phase);
    const alpha = 220 * nightFactor * twinkle;
    fill(255, 255, 255, alpha);
    circle(s.x, s.y, s.size);
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

calculation Night factor (inverse of day) const nightFactor = 1 - dayFactor;

Inverts dayFactor so stars are visible at night (nightFactor = 1) and invisible during day (nightFactor = 0)

conditional Skip if daytime if (!stars || nightFactor <= 0.01) return;

Exits early if it's daytime or stars array doesn't exist, avoiding unnecessary calculations

for-loop Star drawing loop for (const s of stars) { ... }

Iterates through all stars, calculates their twinkle animation, and draws them with fading alpha

const nightFactor = 1 - dayFactor;
Calculates the inverse of dayFactor: 1 at night (dayFactor = 0), 0 during day (dayFactor = 1)
if (!stars || nightFactor <= 0.01) return;
Skips all star drawing if it's nearly daytime (nightFactor < 0.01) or if the stars array doesn't exist
const twinkle = 0.5 + 0.5 * sin(dayTime * TWO_PI * s.twinkleSpeed + s.phase);
Creates a twinkling effect: sin() oscillates from -1 to 1, so (0.5 + 0.5 * sin()) ranges from 0 to 1. Each star twinkles at its own speed and phase
const alpha = 220 * nightFactor * twinkle;
Combines three factors: base brightness (220), time of night (nightFactor), and twinkling animation (twinkle) into a final transparency value
fill(255, 255, 255, alpha);
Sets fill to white with the calculated alpha; dimmer at twilight, brightest at midnight
circle(s.x, s.y, s.size);
Draws a white circle at the star's position with its pre-generated size

drawClickEffects()

drawClickEffects() manages a particle system: objects are created in mousePressed(), animated here each frame, and removed when they die. Looping backward (i--) is important when removing items so the index stays valid. This pattern scales to hundreds of particles efficiently.

function drawClickEffects() {
  if (clickEffects.length === 0) return;

  push();
  noFill();
  strokeWeight(2);
  for (let i = clickEffects.length - 1; i >= 0; i--) {
    const e = clickEffects[i];
    e.r += 4;
    e.alpha -= 12;
    if (e.alpha <= 0) {
      clickEffects.splice(i, 1);
      continue;
    }
    stroke(255, 255, 255, e.alpha);
    circle(e.x, e.y, e.r);
  }
  pop();
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

conditional Early exit if empty if (clickEffects.length === 0) return;

Skips the entire function if there are no active click effects

for-loop Effect animation and removal for (let i = clickEffects.length - 1; i >= 0; i--) { ... }

Iterates backward through the array, animates each effect, removes dead ones, and draws the rest

if (clickEffects.length === 0) return;
Performance optimization: if there are no effects, don't do any drawing or looping
push();
Saves the current graphics state (fill, stroke, etc.) so changes only affect this function
noFill();
Disables fill so only the circle outline is drawn
strokeWeight(2);
Sets the outline thickness to 2 pixels
for (let i = clickEffects.length - 1; i >= 0; i--) {
Loops backward through the array (important for safe removal: deleting forward items would skip elements)
e.r += 4;
Grows the ripple radius by 4 pixels each frame
e.alpha -= 12;
Fades the ripple by reducing its transparency by 12 each frame
if (e.alpha <= 0) { clickEffects.splice(i, 1); continue; }
Removes dead effects from the array and skips drawing them
stroke(255, 255, 255, e.alpha);
Sets the outline color to white with the current alpha
circle(e.x, e.y, e.r);
Draws an expanding circle at the click position
pop();
Restores the previous graphics state after drawing all effects

drawStatsHeader()

drawStatsHeader() demonstrates responsive text sizing, color formatting with nf(), and layout based on calculated positions. The same box-drawing technique is used throughout the sketch for UI panels.

function drawStatsHeader() {
  const boxW = panelSplitX - 40;
  const boxH = 120;

  noStroke();
  fill(255, 255, 255, 180);
  rect(20, 15, boxW, boxH, 12);

  fill(0);
  textAlign(LEFT, TOP);
  
  const titleSize = min(22, boxW * 0.055);
  textSize(titleSize);
  text('🍪 Cookies : ' + nf(floor(cookies), 0, 0), 40, 25);

  const infoSize = min(14, boxW * 0.035);
  textSize(infoSize);
  text('Par clic : ' + cookiesPerClick, 40, 55);
  text('Par seconde : ' + cookiesPerSecond.toFixed(1), 40, 75);
  text('Clics totaux : ' + totalClicks, 40, 95);
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

rect-draw Stats box background fill(255, 255, 255, 180); rect(20, 15, boxW, boxH, 12);

Draws a white semi-transparent box with rounded corners to contain the stats text

text-render Cookie count title text('🍪 Cookies : ' + nf(floor(cookies), 0, 0), 40, 25);

Displays the current cookie count formatted with thousands separators

text-render Secondary stats text('Par clic : ' + cookiesPerClick, 40, 55); text('Par seconde : ' + cookiesPerSecond.toFixed(1), 40, 75); text('Clics totaux : ' + totalClicks, 40, 95);

Displays per-click bonus, passive income rate, and total clicks made

const boxW = panelSplitX - 40;
Calculates box width as 60% of canvas width minus 40 pixels for margins
const boxH = 120;
Sets a fixed height of 120 pixels for the stats box
fill(255, 255, 255, 180);
Sets fill to white with 70% opacity (180/255) so it's semi-transparent
rect(20, 15, boxW, boxH, 12);
Draws a rounded rectangle (12px corner radius) at position (20, 15)
fill(0);
Changes fill to black for the text
const titleSize = min(22, boxW * 0.055);
Makes text size responsive: 5.5% of box width, but never larger than 22 pixels
text('🍪 Cookies : ' + nf(floor(cookies), 0, 0), 40, 25);
Displays the cookie count using nf() to format it with comma separators
text('Par seconde : ' + cookiesPerSecond.toFixed(1), 40, 75);
Displays per-second income rounded to 1 decimal place using toFixed(1)

isMiniUpgradeUnlocked()

isMiniUpgradeUnlocked() is a simple boolean check used to determine which mini upgrades should be visible. Its logic uses short-circuit evaluation: if the first condition is true, the second is never evaluated.

function isMiniUpgradeUnlocked(up) {
  return !up.unlockAtMaxCookies || maxCookiesEver >= up.unlockAtMaxCookies;
}
Line-by-line explanation (1 lines)
return !up.unlockAtMaxCookies || maxCookiesEver >= up.unlockAtMaxCookies;
Returns true if: (1) the upgrade has no unlock condition (unlockAtMaxCookies is falsy), OR (2) the player's all-time max cookies is at least the unlock threshold

getVisibleMiniUpgrades()

getVisibleMiniUpgrades() filters the clickUpgrades array based on two conditions. Many drawing functions use this pattern: calculate what to draw, then draw only what's in the filtered list. This separation makes the code cleaner and more reusable.

function getVisibleMiniUpgrades() {
  const visible = [];
  for (const up of clickUpgrades) {
    if (isMiniUpgradeUnlocked(up) && up.level === 0) {
      visible.push(up);
    }
  }
  return visible;
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

for-loop Upgrade filtering for (const up of clickUpgrades) { if (isMiniUpgradeUnlocked(up) && up.level === 0) { visible.push(up); } }

Iterates through all click upgrades and includes only those that are unlocked AND haven't been purchased yet (level === 0)

const visible = [];
Creates an empty array to collect visible upgrades
for (const up of clickUpgrades) {
Iterates through all click upgrades
if (isMiniUpgradeUnlocked(up) && up.level === 0) {
Only includes upgrades that are: (1) unlocked by cookie milestone, AND (2) haven't been purchased (level is still 0)
visible.push(up);
Adds the upgrade to the visible array
return visible;
Returns the filtered array of visible upgrades

drawMiniUpgrades()

drawMiniUpgrades() demonstrates responsive button design: position calculated dynamically, hover detection via rectangle bounds, and state-dependent coloring. The tooltip system is also visible here—hoveredTooltip is set during rendering and drawn later.

🔬 This controls the button color based on affordability and hover. What happens if you swap the 'hovering' line to fill(255, 100, 100, 245) to make affordable+hovering buttons bright red?

    if (affordable && hovering) {
      fill(up.color[0] + 40, up.color[1] + 40, up.color[2] + 40, 245);
    } else if (affordable) {
      fill(up.color[0], up.color[1], up.color[2], 235);
    } else {
      fill(50, 50, 60, 210);
    }
function drawMiniUpgrades() {
  const visible = getVisibleMiniUpgrades();
  if (visible.length === 0) return;

  textAlign(CENTER, CENTER);

  for (let i = 0; i < visible.length; i++) {
    const up = visible[i];
    const r = getMiniUpgradeRect(i, visible.length);
    const cost = getUpgradeCost(up);
    const affordable = cookies >= cost;
    const hovering = mouseX > r.x && mouseX < r.x + r.w && mouseY > r.y && mouseY < r.y + r.h;

    noStroke();
    if (affordable && hovering) {
      fill(up.color[0] + 40, up.color[1] + 40, up.color[2] + 40, 245);
    } else if (affordable) {
      fill(up.color[0], up.color[1], up.color[2], 235);
    } else {
      fill(50, 50, 60, 210);
    }
    rect(r.x, r.y, r.w, r.h, 8);

    // Icône emoji
    textSize(24);
    text(up.icon, r.x + r.w / 2, r.y + r.h / 2);

    // Niveau en bas à droite
    if (up.level > 0) {
      fill(30, 30, 40, 230);
      circle(r.x + r.w - 12, r.y + r.h - 12, 20);
      
      fill(255);
      textAlign(CENTER, CENTER);
      textSize(10);
      text(up.level, r.x + r.w - 12, r.y + r.h - 12);
    }

    if (hovering) {
      hoveredTooltip = {
        x: mouseX + 16,
        y: mouseY + 16,
        title: up.name,
        desc: up.description,
        cost: cost,
        level: up.level,
        affordable: affordable
      };
    }
  }
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

conditional Empty check if (visible.length === 0) return;

Skips rendering if no upgrades are currently visible

for-loop Mini upgrade rendering for (let i = 0; i < visible.length; i++) { ... }

Draws each visible mini upgrade button with hover state and tooltip logic

conditional Hover detection const hovering = mouseX > r.x && mouseX < r.x + r.w && mouseY > r.y && mouseY < r.y + r.h;

Detects if the mouse is within the upgrade button's rectangle

const visible = getVisibleMiniUpgrades();
Gets the list of mini upgrades that are unlocked and not yet purchased
if (visible.length === 0) return;
Early exit if there are no visible upgrades to draw
const r = getMiniUpgradeRect(i, visible.length);
Calculates the position and size of the i-th upgrade button
const cost = getUpgradeCost(up);
Calculates the current cost of this upgrade using the exponential scaling formula
const affordable = cookies >= cost;
Checks if the player has enough cookies to afford this upgrade
const hovering = mouseX > r.x && mouseX < r.x + r.w && ...;
Checks if the mouse is currently inside the button's rectangular bounds
if (affordable && hovering) { fill(up.color[0] + 40, ...); } else if (affordable) { fill(up.color[0], ...); } else { fill(50, 50, 60, 210); }
Three-way color logic: bright on hover (if affordable), normal if affordable but not hovering, gray if unaffordable
text(up.icon, r.x + r.w / 2, r.y + r.h / 2);
Draws the emoji icon centered in the button
if (up.level > 0) { ... circle(...); text(up.level, ...); }
If the upgrade has been purchased before (level > 0), draws a badge showing its current level
if (hovering) { hoveredTooltip = {...}; }
When hovering, sets hoveredTooltip to be rendered by drawTooltip() later in the draw loop

getMiniUpgradeRect()

getMiniUpgradeRect() is a layout helper that calculates button positions. This pattern—separating layout calculations into their own function—makes the code clearer and easier to adjust. The math ensures all buttons fit snugly in the top-right corner regardless of how many are visible.

function getMiniUpgradeRect(index, count) {
  const btnSize = 50;
  const spacing = 10;
  if (typeof count === 'undefined') {
    count = clickUpgrades.length;
  }
  const totalW = count * btnSize + (count - 1) * spacing;

  const marginRight = 24;
  const xStart = width - totalW - marginRight;
  const y = 18;

  const x = xStart + index * (btnSize + spacing);
  return { x: x, y: y, w: btnSize, h: btnSize };
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

calculation Total width calculation const totalW = count * btnSize + (count - 1) * spacing;

Calculates how wide all the buttons are together, including gaps

calculation Button position const x = xStart + index * (btnSize + spacing);

Calculates the x position of the button at the given index in the row

const btnSize = 50;
Each mini upgrade button is 50×50 pixels
const spacing = 10;
Buttons are separated by 10 pixels horizontally
if (typeof count === 'undefined') { count = clickUpgrades.length; }
If count is not provided, default to the total number of click upgrades (for backward compatibility)
const totalW = count * btnSize + (count - 1) * spacing;
Total width = (button count × button size) + (gaps between buttons)
const marginRight = 24;
Leaves 24 pixels of margin on the right edge of the canvas
const xStart = width - totalW - marginRight;
Calculates the x position of the first button so the row is right-aligned with margin
const x = xStart + index * (btnSize + spacing);
Positions the button at the correct horizontal offset within the row
return { x: x, y: y, w: btnSize, h: btnSize };
Returns an object describing the button's bounding box for hit detection and drawing

drawCookie()

drawCookie() combines several p5.js techniques: transform matrices (translate, scale, push/pop), conditional coloring, and responsive sizing. The shadow and chips use hard-coded positions relative to the cookie size, making them scale responsively.

🔬 These seven circles are chocolate chips. What happens if you add an eighth chip at the position circle(chipDist * 0.5, chipDist * 0.5, chipSize)?

  circle(-chipDist, -chipDist * 0.5, chipSize);
  circle(chipDist * 0.7, -chipDist * 0.8, chipSize);
  circle(-chipDist * 0.6, chipDist * 0.8, chipSize);
  circle(chipDist * 0.9, chipDist * 0.4, chipSize);
  circle(0, chipDist * 0.3, chipSize * 1.2);
  circle(-chipDist * 0.3, 0, chipSize * 0.9);
  circle(chipDist * 0.4, -chipDist * 0.2, chipSize * 0.8);
function drawCookie() {
  const hovering = isMouseOverCookie();

  if (cookieAnim > 0) {
    cookieAnim = max(0, cookieAnim - 0.08);
  }
  const scaleAmount = 1 + 0.12 * cookieAnim;

  push();
  translate(cookieButton.x + cookieButton.size / 2, cookieButton.y + cookieButton.size / 2);
  scale(scaleAmount);

  noStroke();
  fill(0, 0, 0, 100);
  circle(8, 8, cookieButton.size);

  if (hovering) {
    fill(230, 160, 90);
  } else {
    fill(210, 140, 70);
  }
  circle(0, 0, cookieButton.size);

  fill(100, 60, 30);
  const chipSize = cookieButton.size * 0.08;
  const chipDist = cookieButton.size * 0.25;
  
  circle(-chipDist, -chipDist * 0.5, chipSize);
  circle(chipDist * 0.7, -chipDist * 0.8, chipSize);
  circle(-chipDist * 0.6, chipDist * 0.8, chipSize);
  circle(chipDist * 0.9, chipDist * 0.4, chipSize);
  circle(0, chipDist * 0.3, chipSize * 1.2);
  circle(-chipDist * 0.3, 0, chipSize * 0.9);
  circle(chipDist * 0.4, -chipDist * 0.2, chipSize * 0.8);

  noFill();
  stroke(180, 110, 50);
  strokeWeight(3);
  circle(0, 0, cookieButton.size);

  noStroke();
  fill(255);
  textAlign(CENTER, CENTER);
  const textSize1 = min(16, cookieButton.size * 0.08);
  textSize(textSize1);
  text('+' + cookiesPerClick + ' / clic', 0, cookieButton.size * 0.55);

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

🔧 Subcomponents:

conditional Hover detection const hovering = isMouseOverCookie();

Checks if the mouse is over the cookie to change its color

calculation Click animation decay if (cookieAnim > 0) { cookieAnim = max(0, cookieAnim - 0.08); }

Decreases the animation scale value each frame, fading the click pulse

transform Centering and scaling push(); translate(cookieButton.x + cookieButton.size / 2, cookieButton.y + cookieButton.size / 2); scale(scaleAmount);

Centers the cookie at its position and applies a scale animation from the center point

circle-draw Shadow circle fill(0, 0, 0, 100); circle(8, 8, cookieButton.size);

Draws a subtle black shadow offset below and right of the cookie

circle-draws Chocolate chips circle(-chipDist, -chipDist * 0.5, chipSize); circle(chipDist * 0.7, -chipDist * 0.8, chipSize); // ... more circles

Draws 7 chocolate chips at various positions on the cookie

const hovering = isMouseOverCookie();
Checks if the mouse is within the cookie's circular hitbox
if (cookieAnim > 0) { cookieAnim = max(0, cookieAnim - 0.08); }
Decrements the animation timer each frame; when it reaches 0, the scale effect is gone
const scaleAmount = 1 + 0.12 * cookieAnim;
Maps cookieAnim (1 to 0) to a scale (1.12 to 1): starts 12% larger, shrinks back to normal
translate(cookieButton.x + cookieButton.size / 2, ...);
Moves the origin to the center of the cookie so scaling happens from the center
scale(scaleAmount);
Applies the scale animation: 1.12 when clicked, 1.0 when fully decayed
fill(0, 0, 0, 100); circle(8, 8, cookieButton.size);
Draws a semi-transparent black shadow offset 8 pixels down and right
if (hovering) { fill(230, 160, 90); } else { fill(210, 140, 70); }
Brighter orange (230, 160, 90) when hovering, normal orange (210, 140, 70) when not
circle(0, 0, cookieButton.size);
Draws the main cookie body at the origin (centered due to translate)
const chipSize = cookieButton.size * 0.08;
Makes chip size scale with the cookie: 8% of cookie diameter
const chipDist = cookieButton.size * 0.25;
Controls how far chips are from the center: 25% of cookie diameter
circle(-chipDist, -chipDist * 0.5, chipSize);
Places a chip at 25% left, 12.5% up from center
noFill(); stroke(180, 110, 50); strokeWeight(3); circle(0, 0, cookieButton.size);
Draws a brown outline around the cookie for definition
text('+' + cookiesPerClick + ' / clic', 0, cookieButton.size * 0.55);
Displays the click bonus inside the cookie, positioned 55% down from the center
pop();
Restores the graphics state, undoing translate() and scale() so other elements aren't affected

drawUpgradePanels()

drawUpgradePanels() is a thin wrapper that sets up the title and calls drawUpgradeListCards(). This separation makes the code modular: if you wanted to add a second panel (e.g., 'SPELLS'), you'd just add another section here.

function drawUpgradePanels() {
  const paddingX = 16;
  const paddingTop = 20;

  fill(255, 240, 200);
  textAlign(CENTER, TOP);
  
  const titleSize = min(18, (width - panelSplitX) * 0.06);
  textSize(titleSize);
  text('🏪 BÂTIMENTS', panelSplitX + (width - panelSplitX) / 2, paddingTop);

  const startY = paddingTop + 40 + panelScroll;
  
  drawUpgradeListCards(autoUpgrades, startY, paddingX);
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

text-render Panel title text('🏪 BÂTIMENTS', panelSplitX + (width - panelSplitX) / 2, paddingTop);

Draws the title centered horizontally in the shop panel

function-call Card rendering drawUpgradeListCards(autoUpgrades, startY, paddingX);

Draws all building upgrade cards starting at startY (which includes panelScroll offset)

const paddingX = 16;
Horizontal margin from the panel edges
const paddingTop = 20;
Top margin before the title
fill(255, 240, 200);
Sets text color to a light cream color for contrast on the dark panel background
const startY = paddingTop + 40 + panelScroll;
Calculates the starting y position: padding + title space + scroll offset
drawUpgradeListCards(autoUpgrades, startY, paddingX);
Delegates to a helper function that draws all the building upgrade cards

drawUpgradeListCards()

drawUpgradeListCards() is the most complex drawing function because it handles many UI concerns: layout, coloring, interactivity, and formatted text. The pattern—calculate bounds, detect interaction, draw with state-dependent appearance—repeats throughout the sketch.

🔬 This three-way conditional controls card colors. What if you remove the 'hovering' check entirely and change the second line to fill(up.color[0] + 50, up.color[1] + 50, up.color[2] + 50, 240) so affordable cards are always bright?

    if (affordable && hovering) {
      fill(up.color[0] + 30, up.color[1] + 30, up.color[2] + 30, 255);
    } else if (affordable) {
      fill(up.color[0], up.color[1], up.color[2], 240);
    } else {
      fill(60, 60, 70, 200);
    }
function drawUpgradeListCards(list, startY, paddingX) {
  const cardW = width - panelSplitX - paddingX * 2;
  const cardH = 80;
  const spacing = 10;

  const visible = getVisibleUpgrades(list);

  for (let i = 0; i < visible.length; i++) {
    const up = visible[i];
    const cost = getUpgradeCost(up);
    const affordable = cookies >= cost;
    
    const x = panelSplitX + paddingX;
    const y = startY + i * (cardH + spacing);
    
    const hovering = mouseX > x && mouseX < x + cardW && mouseY > y && mouseY < y + cardH;

    // Fond de carte
    noStroke();
    if (affordable && hovering) {
      fill(up.color[0] + 30, up.color[1] + 30, up.color[2] + 30, 255);
    } else if (affordable) {
      fill(up.color[0], up.color[1], up.color[2], 240);
    } else {
      fill(60, 60, 70, 200);
    }
    rect(x, y, cardW, cardH, 8);

    // Bordure
    if (affordable) {
      stroke(255, 255, 255, 100);
    } else {
      stroke(100, 100, 110, 150);
    }
    strokeWeight(2);
    noFill();
    rect(x, y, cardW, cardH, 8);

    // Zone icône (carré à gauche)
    const iconSize = 60;
    const iconX = x + 10;
    const iconY = y + 10;
    
    noStroke();
    if (affordable) {
      fill(up.color[0] - 30, up.color[1] - 30, up.color[2] - 30, 200);
    } else {
      fill(40, 40, 50, 200);
    }
    rect(iconX, iconY, iconSize, iconSize, 6);

    // Icône emoji
    textAlign(CENTER, CENTER);
    textSize(32);
    text(up.icon, iconX + iconSize / 2, iconY + iconSize / 2);

    // Nombre possédé (badge)
    if (up.level > 0) {
      noStroke();
      fill(50, 50, 60, 230);
      circle(iconX + iconSize - 10, iconY + 10, 24);
      
      fill(255);
      textSize(11);
      textAlign(CENTER, CENTER);
      text(up.level, iconX + iconSize - 10, iconY + 10);
    }

    // Texte à droite de l'icône
    const textX = iconX + iconSize + 12;
    const textY = y + 15;

    fill(255);
    textAlign(LEFT, TOP);
    
    const nameSize = min(16, cardW * 0.055);
    textSize(nameSize);
    text(up.name, textX, textY);

    const descSize = min(12, cardW * 0.042);
    textSize(descSize);
    fill(220, 220, 230);
    text(up.description, textX, textY + 24);

    // Coût en bas à droite
    textAlign(RIGHT, BOTTOM);
    if (affordable) {
      fill(100, 255, 100);
    } else {
      fill(255, 100, 100);
    }
    const costSize = min(14, cardW * 0.048);
    textSize(costSize);
    text(formatNumber(cost) + ' 🍪', x + cardW - 10, y + cardH - 10);
  }
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

for-loop Card rendering loop for (let i = 0; i < visible.length; i++) { ... }

Draws each visible upgrade as a card with background, icon, text, and cost

conditional Affordability logic const affordable = cookies >= cost; if (affordable && hovering) { ... } else if (affordable) { ... } else { ... }

Changes card colors based on whether the player can afford it and if it's being hovered

conditional Hover detection const hovering = mouseX > x && mouseX < x + cardW && ...;

Detects if the mouse is within the card's rectangular bounds

const cardW = width - panelSplitX - paddingX * 2;
Calculates card width as the panel width minus padding on both sides
const cardH = 80;
Sets a fixed card height of 80 pixels
const visible = getVisibleUpgrades(list);
Filters the upgrade list to only show unlocked upgrades
const y = startY + i * (cardH + spacing);
Calculates the card's y position: each card is 80 pixels tall plus 10 pixel gap
const hovering = mouseX > x && mouseX < x + cardW && ...;
Checks if the mouse is inside the card's rectangular bounds
if (affordable && hovering) { fill(up.color[0] + 30, ...); } else if (affordable) { fill(up.color[0], ...); } else { fill(60, 60, 70, 200); }
Three-state coloring: brightened when hovering, normal when affordable, dark when unaffordable
stroke(255, 255, 255, 100);
Draws a white border for affordable cards
stroke(100, 100, 110, 150);
Draws a dark gray border for unaffordable cards
const iconSize = 60;
The emoji icon area is a 60×60 square on the left side of the card
fill(up.color[0] - 30, up.color[1] - 30, up.color[2] - 30, 200);
Darkens the upgrade's color for the icon background box
if (up.level > 0) { ... circle(...); text(up.level, ...); }
If the upgrade has been purchased, draws a small badge showing the current level
text(formatNumber(cost) + ' 🍪', x + cardW - 10, y + cardH - 10);
Displays the cost in the bottom-right, formatted with K/M abbreviations for large numbers

formatNumber()

formatNumber() compresses large numbers into readable abbreviations. This is a common pattern in games and data visualization. You could extend it to add 'B' for billions or 'T' for trillions.

function formatNumber(num) {
  if (num >= 1000000) {
    return (num / 1000000).toFixed(1) + 'M';
  } else if (num >= 1000) {
    return (num / 1000).toFixed(1) + 'K';
  }
  return floor(num);
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional Millions formatting if (num >= 1000000) { return (num / 1000000).toFixed(1) + 'M'; }

Formats numbers >= 1 million as 'M' (e.g., 2500000 → '2.5M')

conditional Thousands formatting else if (num >= 1000) { return (num / 1000).toFixed(1) + 'K'; }

Formats numbers >= 1000 as 'K' (e.g., 45000 → '45.0K')

if (num >= 1000000) {
Checks if the number is at least 1 million
return (num / 1000000).toFixed(1) + 'M';
Divides by 1 million, rounds to 1 decimal place, and appends 'M' (e.g., 2500000 → 2.5M)
} else if (num >= 1000) {
Checks if the number is at least 1 thousand
return (num / 1000).toFixed(1) + 'K';
Divides by 1000, rounds to 1 decimal place, and appends 'K' (e.g., 45000 → 45.0K)
return floor(num);
For numbers under 1000, returns the integer value without decimal places

updateScrollLimits()

updateScrollLimits() calculates how far you can scroll based on content size. It runs every frame because new upgrades can be unlocked, changing the content height. The logic ensures you can't scroll past the bounds.

function updateScrollLimits() {
  const cardH = 80;
  const spacing = 10;
  const paddingTop = 80;
  const bottomMargin = 40;

  const autoCount = getVisibleUpgrades(autoUpgrades).length;

  const contentTop = paddingTop;
  const contentBottomNoScroll = paddingTop + autoCount * (cardH + spacing) + bottomMargin;

  const contentHeight = contentBottomNoScroll - contentTop;
  const viewHeight = height;

  if (contentHeight <= viewHeight) {
    panelScrollMin = 0;
    panelScrollMax = 0;
    panelScroll = 0;
  } else {
    panelScrollMax = 0;
    panelScrollMin = viewHeight - contentHeight;
    panelScroll = constrain(panelScroll, panelScrollMin, panelScrollMax);
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

calculation Content height calculation const contentHeight = contentBottomNoScroll - contentTop;

Calculates total height of all visible upgrade cards

conditional Scroll limit determination if (contentHeight <= viewHeight) { ... } else { ... }

Disables scrolling if content fits in view, otherwise constrains scroll range

const cardH = 80;
Must match the card height used in drawUpgradeListCards()
const autoCount = getVisibleUpgrades(autoUpgrades).length;
Counts how many buildings are currently visible (unlocked)
const contentBottomNoScroll = paddingTop + autoCount * (cardH + spacing) + bottomMargin;
Calculates where the last card would end if there were no scrolling
const contentHeight = contentBottomNoScroll - contentTop;
Total height of all content: number of cards × card height + gaps
const viewHeight = height;
The visible height of the panel is the full canvas height
if (contentHeight <= viewHeight) { panelScrollMin = 0; panelScrollMax = 0; panelScroll = 0; }
If content fits on screen, disable scrolling completely
panelScrollMin = viewHeight - contentHeight;
Minimum scroll (most scrolled up): negative value that brings bottom of content to bottom of view
panelScroll = constrain(panelScroll, panelScrollMin, panelScrollMax);
Clamps the current scroll value within the calculated limits

isUpgradeUnlocked()

isUpgradeUnlocked() is a simple unlock check. Currently, it looks for unlockAtClicks, but the code never sets this property—it's ready for future extension. Mini upgrades use unlockAtMaxCookies instead; the two systems can work together.

function isUpgradeUnlocked(up) {
  return !up.unlockAtClicks || totalClicks >= up.unlockAtClicks;
}
Line-by-line explanation (1 lines)
return !up.unlockAtClicks || totalClicks >= up.unlockAtClicks;
Returns true if: (1) the upgrade has no unlock condition, OR (2) the player's total clicks meets the threshold

getVisibleUpgrades()

getVisibleUpgrades() filters a list based on unlock status. This pattern—filter, then draw—separates data logic from presentation and is used throughout the sketch.

function getVisibleUpgrades(list) {
  const visible = [];
  for (const up of list) {
    if (isUpgradeUnlocked(up)) {
      visible.push(up);
    }
  }
  return visible;
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

for-loop Unlock filtering for (const up of list) { if (isUpgradeUnlocked(up)) { visible.push(up); } }

Collects only the upgrades that pass the unlock check

const visible = [];
Creates an empty array to collect unlocked upgrades
for (const up of list) {
Iterates through the provided list (either clickUpgrades or autoUpgrades)
if (isUpgradeUnlocked(up)) {
Checks if this upgrade meets its unlock conditions
visible.push(up);
Adds the upgrade to the visible array
return visible;
Returns the filtered list of only unlocked upgrades

getUpgradeCost()

getUpgradeCost() implements exponential cost scaling: each upgrade level costs 15% more than the previous. This is the standard formula in progression-based games like Cookie Clicker. Changing 1.15 to 1.25 would make costs rise faster; 1.10 would make them rise slower.

function getUpgradeCost(up) {
  return floor(up.baseCost * pow(1.15, up.level));
}
Line-by-line explanation (1 lines)
return floor(up.baseCost * pow(1.15, up.level));
Calculates cost using exponential scaling: base cost × 1.15^level, then rounds down to the nearest integer

drawSmallHelpText()

drawSmallHelpText() is a simple helper that displays instructions at the bottom of the screen. It's a good practice to include in-game help text so new players understand what to do.

function drawSmallHelpText() {
  fill(255, 230);
  const helpSize = min(11, width * 0.012);
  textSize(helpSize);
  textAlign(LEFT, BOTTOM);
  text(
    '🍪 Clique sur le cookie géant pour en gagner !\n' +
    'Achète des améliorations en haut à droite et des bâtiments à droite.',
    24,
    height - 24
  );
}
Line-by-line explanation (4 lines)
fill(255, 230);
Sets text color to white with slight transparency (230/255 ≈ 90% opacity)
const helpSize = min(11, width * 0.012);
Makes text size responsive: 1.2% of canvas width, capped at 11 pixels
textAlign(LEFT, BOTTOM);
Aligns text to the bottom-left corner
text(..., 24, height - 24);
Positions text 24 pixels from the left edge and 24 pixels from the bottom

drawTooltip()

drawTooltip() demonstrates advanced text handling: measuring text width, dynamic sizing, edge detection, and conditional styling. It uses textWidth() to calculate box size based on content, a useful pattern for responsive UI.

function drawTooltip() {
  if (!hoveredTooltip) return;

  push();
  textSize(11);
  textAlign(LEFT, TOP);

  const lines = [
    hoveredTooltip.title,
    hoveredTooltip.desc,
    'Coût : ' + hoveredTooltip.cost + ' cookies',
    'Niveau : ' + hoveredTooltip.level
  ];

  let maxW = 0;
  for (const line of lines) {
    maxW = max(maxW, textWidth(line));
  }

  const padding = 8;
  const lineH = 15;
  const boxW = maxW + padding * 2;
  const boxH = lineH * lines.length + padding * 2;

  let x = hoveredTooltip.x;
  let y = hoveredTooltip.y;

  if (x + boxW > width - 8) x = width - boxW - 8;
  if (y + boxH > height - 8) y = height - boxH - 8;

  if (hoveredTooltip.affordable) {
    fill(80, 50, 20, 240);
  } else {
    fill(15, 15, 25, 240);
  }
  stroke(255, 255, 255, 80);
  strokeWeight(1);
  rect(x, y, boxW, boxH, 8);

  noStroke();
  let ty = y + padding;
  for (let i = 0; i < lines.length; i++) {
    if (i === 0) {
      textStyle(BOLD);
    } else {
      textStyle(NORMAL);
    }
    fill(255);
    text(lines[i], x + padding, ty);
    ty += lineH;
  }

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

🔧 Subcomponents:

conditional Tooltip existence check if (!hoveredTooltip) return;

Exits if no tooltip is active

for-loop Max width calculation let maxW = 0; for (const line of lines) { maxW = max(maxW, textWidth(line)); }

Finds the widest text line to size the tooltip box

conditional Screen edge clamping if (x + boxW > width - 8) x = width - boxW - 8; if (y + boxH > height - 8) y = height - boxH - 8;

Moves the tooltip if it would extend past the right or bottom edges

for-loop Tooltip text rendering for (let i = 0; i < lines.length; i++) { ... }

Draws each line with bold styling for the first line (title)

if (!hoveredTooltip) return;
Performance optimization: skip all tooltip logic if there's nothing to show
const lines = [hoveredTooltip.title, hoveredTooltip.desc, 'Coût : ...', 'Niveau : ...'];
Builds an array of text lines to display in the tooltip
maxW = max(maxW, textWidth(line));
Measures each line's width and tracks the longest
const boxW = maxW + padding * 2;
Tooltip box width is the longest line plus padding on both sides
if (x + boxW > width - 8) x = width - boxW - 8;
If the tooltip would extend past the right edge, move it left
if (hoveredTooltip.affordable) { fill(80, 50, 20, 240); } else { fill(15, 15, 25, 240); }
Brown background if affordable, dark gray if not
if (i === 0) { textStyle(BOLD); } else { textStyle(NORMAL); }
Makes the first line (title) bold, others normal
ty += lineH;
Moves down 15 pixels for the next line

mousePressed()

mousePressed() is the main event handler for user interaction. It creates visual feedback (ripples), checks hit detection for all clickable elements in order (mini upgrades, cookie, buildings), and processes purchases. The structure—check in order, return early when something matches—avoids processing multiple clicks.

🔬 This creates a ripple at every mouse press. What happens if you increase alpha from 200 to 255 to make the ripple start more opaque?

  clickEffects.push({
    x: mouseX,
    y: mouseY,
    r: 0,
    alpha: 200
  });
function mousePressed() {
  clickEffects.push({
    x: mouseX,
    y: mouseY,
    r: 0,
    alpha: 200
  });

  // Clic sur petites améliorations (en haut à droite)
  const visibleMini = getVisibleMiniUpgrades();
  for (let i = 0; i < visibleMini.length; i++) {
    const r = getMiniUpgradeRect(i, visibleMini.length);
    if (mouseX > r.x && mouseX < r.x + r.w && mouseY > r.y && mouseY < r.y + r.h) {
      if (buyUpgrade(visibleMini[i])) {
        return;
      }
    }
  }

  if (isMouseOverCookie()) {
    addCookies(cookiesPerClick);
    totalClicks++;
    cookieAnim = 1;
    return;
  }

  // Clic sur les cartes de bâtiments (panneau de droite)
  const paddingX = 16;
  const paddingTop = 20;
  const cardW = width - panelSplitX - paddingX * 2;
  const cardH = 80;
  const spacing = 10;
  const startY = paddingTop + 40 + panelScroll;

  const visibleAuto = getVisibleUpgrades(autoUpgrades);
  for (let i = 0; i < visibleAuto.length; i++) {
    const x = panelSplitX + paddingX;
    const y = startY + i * (cardH + spacing);
    
    if (mouseX > x && mouseX < x + cardW && mouseY > y && mouseY < y + cardH) {
      buyUpgrade(visibleAuto[i]);
      return;
    }
  }
}
Line-by-line explanation (14 lines)

🔧 Subcomponents:

array-push Click ripple effect clickEffects.push({ x: mouseX, y: mouseY, r: 0, alpha: 200 });

Creates a new ripple animation at the mouse position

for-loop Mini upgrade hit detection for (let i = 0; i < visibleMini.length; i++) { ... if (buyUpgrade(...)) { return; } }

Checks if a mini upgrade button was clicked and buys if affordable

for-loop Building card hit detection for (let i = 0; i < visibleAuto.length; i++) { ... if (...) { buyUpgrade(...); return; } }

Checks if a building card was clicked and buys if affordable

clickEffects.push({...});
Adds a new ripple effect object to the clickEffects array whenever the mouse is pressed
const visibleMini = getVisibleMiniUpgrades();
Gets the list of visible mini upgrades to check for clicks
const r = getMiniUpgradeRect(i, visibleMini.length);
Gets the bounding box of the mini upgrade button
if (mouseX > r.x && mouseX < r.x + r.w && ...)
Checks if the click is within the button's rectangular bounds
if (buyUpgrade(visibleMini[i])) { return; }
Attempts to buy the upgrade; if successful, return early to skip other checks
if (isMouseOverCookie()) {
Checks if the click was on the giant cookie (using circular distance math)
addCookies(cookiesPerClick);
Adds cookies earned from this click
totalClicks++;
Increments the click counter
cookieAnim = 1;
Triggers the click pulse animation (sets animation timer to 1.0)
const visibleAuto = getVisibleUpgrades(autoUpgrades);
Gets the list of visible buildings to check for clicks
const startY = paddingTop + 40 + panelScroll;
Calculates the y position of the first building card, accounting for scroll offset
const y = startY + i * (cardH + spacing);
Calculates the y position of the building card at index i
if (mouseX > x && mouseX < x + cardW && mouseY > y && mouseY < y + cardH) {
Checks if the click is within the card's bounds
buyUpgrade(visibleAuto[i]);
Attempts to buy the building upgrade if the player can afford it

mouseWheel()

mouseWheel() implements scroll input. The event.delta value represents the scroll amount; the function only applies it when the mouse is over the shop panel. Returning false prevents default browser scrolling.

function mouseWheel(event) {
  if (mouseX > panelSplitX) {
    updateScrollLimits();
    panelScroll -= event.delta;
    panelScroll = constrain(panelScroll, panelScrollMin, panelScrollMax);
  }
  return false;
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional Right panel detection if (mouseX > panelSplitX) { ... }

Only scroll when the mouse is over the right panel (building shop)

calculation Scroll position update panelScroll -= event.delta;

Adds mouse wheel delta to scroll position (negative delta scrolls up, positive down)

constraint Scroll boundary clamping panelScroll = constrain(panelScroll, panelScrollMin, panelScrollMax);

Ensures scroll position stays within valid bounds

if (mouseX > panelSplitX) {
Only process scrolling if the mouse is on the right side of the screen (the shop panel)
updateScrollLimits();
Recalculates scroll limits (in case upgrades were unlocked since the last frame)
panelScroll -= event.delta;
Adjusts scroll position by the wheel delta; negative delta (up) increases panelScroll, positive (down) decreases it
panelScroll = constrain(panelScroll, panelScrollMin, panelScrollMax);
Clamps the scroll value to the calculated limits to prevent over-scrolling
return false;
Returns false to prevent the browser's default page-scroll behavior

buyUpgrade()

buyUpgrade() is the core game logic: it checks affordability, deducts cost, increments level, and applies bonuses. The upgrade's type (click vs. auto) determines which stat is boosted. The function returns a boolean to indicate success, used by mousePressed() to know when to stop checking other clickable elements.

function buyUpgrade(up) {
  const cost = getUpgradeCost(up);
  if (cookies >= cost) {
    cookies -= cost;
    up.level++;

    if (up.type === 'click') {
      cookiesPerClick += up.bonus;
    } else if (up.type === 'auto') {
      cookiesPerSecond += up.bonus;
    }
    return true;
  }
  return false;
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

conditional Affordability check if (cookies >= cost) { ... return true; }

Only processes the purchase if the player has enough cookies

assignment Payment and upgrade cookies -= cost; up.level++;

Deducts the cost and increments the upgrade's level

conditional Bonus type handling if (up.type === 'click') { cookiesPerClick += up.bonus; } else if (up.type === 'auto') { cookiesPerSecond += up.bonus; }

Applies the bonus to either per-click or per-second income based on upgrade type

const cost = getUpgradeCost(up);
Calculates the current cost using exponential scaling
if (cookies >= cost) {
Checks if the player has enough cookies
cookies -= cost;
Deducts the cost from the player's total
up.level++;
Increments the upgrade's level to reflect that it has been purchased
if (up.type === 'click') { cookiesPerClick += up.bonus; }
If it's a click upgrade, adds its bonus to cookiesPerClick
} else if (up.type === 'auto') { cookiesPerSecond += up.bonus; }
If it's an auto upgrade, adds its bonus to cookiesPerSecond
return true;
Returns true to indicate the purchase was successful
return false;
Returns false if the player couldn't afford the upgrade

isMouseOverCookie()

isMouseOverCookie() implements circular hit detection using distance math: if the distance from a point to the circle's center is less than the radius, the point is inside. This is more accurate than a bounding box for round objects.

function isMouseOverCookie() {
  const dx = mouseX - (cookieButton.x + cookieButton.size / 2);
  const dy = mouseY - (cookieButton.y + cookieButton.size / 2);
  const distance = sqrt(dx * dx + dy * dy);
  return distance < cookieButton.size / 2;
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

calculation Distance calculation const dx = mouseX - (cookieButton.x + cookieButton.size / 2); const dy = mouseY - (cookieButton.y + cookieButton.size / 2); const distance = sqrt(dx * dx + dy * dy);

Calculates the distance from the mouse to the cookie's center using Pythagorean theorem

conditional Circle boundary test return distance < cookieButton.size / 2;

Returns true if the distance is less than the cookie's radius

const dx = mouseX - (cookieButton.x + cookieButton.size / 2);
Calculates the horizontal distance from the mouse to the cookie's center
const dy = mouseY - (cookieButton.y + cookieButton.size / 2);
Calculates the vertical distance from the mouse to the cookie's center
const distance = sqrt(dx * dx + dy * dy);
Uses the Pythagorean theorem to calculate the straight-line distance from mouse to center
return distance < cookieButton.size / 2;
Returns true if the distance is less than the radius (cookie's half-size), meaning the point is inside the circle

windowResized()

windowResized() is called automatically by p5.js whenever the browser window is resized. It ensures the sketch stays responsive by regenerating the procedural elements (city and stars) and recalculating all layout positions.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  updateLayout();
  buildCity();
  buildStars();
}
Line-by-line explanation (4 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the canvas to match the new window dimensions
updateLayout();
Recalculates responsive layout values (panel split, button size and position)
buildCity();
Regenerates the cityscape with new random buildings for the new canvas width
buildStars();
Regenerates stars across the new canvas area

📦 Key Variables

cookies number

Stores the player's current cookie count; increases when clicking or from passive income

let cookies = 0;
cookiesPerClick number

How many cookies the player earns per click on the giant button; increased by click upgrades

let cookiesPerClick = 1;
cookiesPerSecond number

How many cookies are generated automatically per second; increased by building upgrades

let cookiesPerSecond = 0;
totalClicks number

Tracks the total number of times the player has clicked the cookie button (for stats display)

let totalClicks = 0;
dayTime number

A value from 0 to 1 that cycles smoothly to drive the day/night color animation; resets after 1200 seconds

let dayTime = 0;
maxCookiesEver number

Tracks the highest cookie count ever reached; used to unlock mini upgrades at milestones

let maxCookiesEver = 0;
panelSplitX number

The x coordinate where the canvas is divided between the play area (left) and shop (right), at 60% width

let panelSplitX;
clickUpgrades array

Array of six mini upgrades that increase cookies per click; displayed in small buttons at the top-right

let clickUpgrades = [...];
autoUpgrades array

Array of seven buildings that generate passive income; displayed as cards in the scrollable right panel

let autoUpgrades = [...];
buildings array

Array of randomly generated building objects, each with position, size, color, and windows data

let buildings = [];
stars array

Array of star objects for the night sky, each with position, size, and twinkling animation data

let stars = [];
panelScroll number

The current vertical scroll offset of the building shop panel; adjusted by mouseWheel()

let panelScroll = 0;
panelScrollMin number

The minimum (most scrolled up) value of panelScroll; prevents over-scrolling the top

let panelScrollMin = 0;
panelScrollMax number

The maximum (most scrolled down) value of panelScroll; typically 0 since content grows upward

let panelScrollMax = 0;
hoveredTooltip object

Stores data for the current tooltip being displayed (title, description, cost, level, affordability); set to null each frame then populated during drawMiniUpgrades() and drawUpgradeListCards()

let hoveredTooltip = null;
cookieAnim number

Animation timer (1 to 0) that controls the cookie's click-to-scale pulse effect; decreases by 0.08 each frame

let cookieAnim = 0;
clickEffects array

Array of expanding ripple circles that appear when the player clicks; each object stores position, radius, and alpha

let clickEffects = [];
cookieButton object

Stores the giant cookie button's position (x, y) and size; recalculated in updateLayout() when the window resizes

let cookieButton; // { x: 0, y: 0, size: 100 }

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG mousePressed() - mini upgrade handling

If a mini upgrade is purchased multiple times, getVisibleMiniUpgrades() should filter by level === 0, but it does (correctly). However, the mini upgrade is never hidden after first purchase—the filter condition removes the upgrade from display only when level > 0. This is actually correct, but confusing: mini upgrades show only once and disappear after first purchase.

💡 Consider whether mini upgrades should be repurchasable (multiple levels) like buildings. If so, remove the 'level === 0' filter from getVisibleMiniUpgrades(). If not, the current logic is fine but should be documented.

PERFORMANCE drawBackgroundCity() - sky gradient

The sky is redrawn as 4-pixel horizontal stripes every frame (roughly 270 stripes for a 1080px tall canvas). This recalculates colors via lerp() every frame unnecessarily.

💡 Pre-calculate the sky gradient once in setup() or when windowResized(), store it in a p5.Graphics buffer, and just draw that buffer each frame. This would eliminate hundreds of lerp() calls per frame.

STYLE drawUpgradeListCards() and drawMiniUpgrades()

Both functions contain nearly identical hover detection and color logic (three-state coloring for affordable/unaffordable/hovering). This duplication violates DRY (Don't Repeat Yourself).

💡 Extract a helper function like getUpgradeCardColor(up, affordable, hovering) to centralize the color logic, making it easier to adjust the hover appearance globally.

BUG updateScrollLimits()

The function assumes all upgrade cards have height 80 and spacing 10, but these values are also defined separately in drawUpgradeListCards(). If someone changes one and forgets the other, scrolling will break.

💡 Define cardH and spacing as global constants at the top of the file, then use them in both updateScrollLimits() and drawUpgradeListCards() to ensure they stay in sync.

FEATURE buyUpgrade()

There's no sound feedback or animation when an upgrade is purchased. Players can't tell if their click registered.

💡 Add a small scale animation or color flash to the card when purchased. Consider adding visual feedback like a 'PURCHASED' label that fades in and out, or a particle burst effect.

STYLE initGame()

The clickUpgrades and autoUpgrades arrays are defined inline with hardcoded values, making them very long (~100 lines). The file is harder to navigate.

💡 Move these arrays to a separate data file (e.g., upgrades.js) or define them more compactly using a helper function that builds upgrade objects from simpler configuration.

🔄 Code Flow

Code flow showing setup, draw, initgame, updatelayout, buildcity, buildstars, updatedaynight, addcookies, updatecookiespassive, drawbackgroundcity, drawstars, drawclickeffects, drawstatsheader, isminiupgradeunlocked, getvisibleminiupgrades, drawminiupgrades, getminiupgraderect, drawcookie, drawupgradepanels, drawupgradelistcards, formatnumber, updatescrolllimits, isupgradeunlocked, getvisibleupgrades, getupgradecost, drawsmallhelptext, drawtooltip, mousepressed, mousewheel, buyupgrade, ismouseovercookie, windowresized

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

graph TD start[Start] --> setup[setup] setup --> initgame[initGame] initgame --> click-upgrades-init[click-upgrades-init] initgame --> auto-upgrades-init[auto-upgrades-init] setup --> updatelayout[updateLayout] setup --> buildcity[buildCity] setup --> buildstars[buildStars] setup --> draw[draw loop] draw --> state-updates[state-updates] state-updates --> updatedaynight[updateDayNight] state-updates --> updatecookiespassive[updateCookiesPassive] state-updates --> updatescrolllimits[updateScrollLimits] draw --> render-calls[render-calls] render-calls --> drawbackgroundcity[drawBackgroundCity] render-calls --> drawstars[drawStars] render-calls --> drawclickeffects[drawClickEffects] render-calls --> drawstatsheader[drawStatsHeader] render-calls --> drawminiupgrades[drawMiniUpgrades] render-calls --> drawcookie[drawCookie] render-calls --> drawupgradepanels[drawUpgradePanels] drawminiupgrades --> filter-loop[filter-loop] filter-loop --> isminiupgradeunlocked[isMiniUpgradeUnlocked] filter-loop --> getvisibleminiupgrades[getVisibleMiniUpgrades] drawupgradepanels --> drawupgradelistcards[drawUpgradeListCards] drawupgradelistcards --> filter-loop[filter-loop] filter-loop --> isupgradeunlocked[isUpgradeUnlocked] filter-loop --> getvisibleupgrades[getVisibleUpgrades] draw --> mousepressed[mousePressed] mousepressed --> mini-upgrade-check[mini-upgrade-check] mini-upgrade-check --> isminiupgradeunlocked mini-upgrade-check --> buyupgrade[buyUpgrade] mousepressed --> cookie-click[cookie-click] mousepressed --> building-click-check[building-click-check] building-click-check --> isupgradeunlocked building-click-check --> buyupgrade draw --> windowresized[windowResized] windowresized --> updatelayout windowresized --> buildcity windowresized --> buildstars click setup href "#fn-setup" click initgame href "#fn-initgame" click updatelayout href "#fn-updateLayout" click buildcity href "#fn-buildCity" click buildstars href "#fn-buildStars" click draw href "#fn-draw" click updatedaynight href "#fn-updateDayNight" click updatecookiespassive href "#fn-updateCookiesPassive" click updatescrolllimits href "#fn-updateScrollLimits" click drawbackgroundcity href "#fn-drawBackgroundCity" click drawstars href "#fn-drawStars" click drawclickeffects href "#fn-drawClickEffects" click drawstatsheader href "#fn-drawStatsHeader" click drawminiupgrades href "#fn-drawMiniUpgrades" click drawcookie href "#fn-drawCookie" click drawupgradepanels href "#fn-drawUpgradePanels" click isminiupgradeunlocked href "#fn-isMiniUpgradeUnlocked" click getvisibleminiupgrades href "#fn-getVisibleMiniUpgrades" click isupgradeunlocked href "#fn-isUpgradeUnlocked" click getvisibleupgrades href "#fn-getVisibleUpgrades" click mousepressed href "#fn-mousePressed" click building-click-check href "#sub-building-click-check" click mini-upgrade-check href "#sub-mini-upgrade-check" click cookie-click href "#sub-cookie-click" click windowresized href "#fn-windowResized"

❓ Frequently Asked Questions

What visual elements does the Cookie Clicker sketch showcase?

The sketch features a giant cookie button, interactive upgrade panels, and day/night cycling effects, enhanced with animations and icons representing various upgrades.

How can users interact with the Cookie Clicker sketch?

Users can click on the giant cookie to collect cookies, purchase upgrades, and experience visual feedback through animations and tooltips.

What creative coding techniques are illustrated in the Cookie Clicker sketch?

The sketch demonstrates concepts such as game state management, user interaction through mouse events, and dynamic visual updates using p5.js.

Preview

Sketch 2026-02-19 21:55 - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Sketch 2026-02-19 21:55 - Code flow showing setup, draw, initgame, updatelayout, buildcity, buildstars, updatedaynight, addcookies, updatecookiespassive, drawbackgroundcity, drawstars, drawclickeffects, drawstatsheader, isminiupgradeunlocked, getvisibleminiupgrades, drawminiupgrades, getminiupgraderect, drawcookie, drawupgradepanels, drawupgradelistcards, formatnumber, updatescrolllimits, isupgradeunlocked, getvisibleupgrades, getupgradecost, drawsmallhelptext, drawtooltip, mousepressed, mousewheel, buyupgrade, ismouseovercookie, windowresized
Code Flow Diagram