Sketch 2026-03-06 19:24

This sketch creates an interactive clicker game where players earn money by clicking a button, with passive income, auto-clicker, and a sophisticated mod menu system that lets players adjust game parameters in real-time. The game includes a comprehensive settings interface accessible by pressing 'M' that categories game tweaks, money cheats, and reset options.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make passive income 50% instead of 10% — Passive income flows in automatically—change the multiplier to earn faster without clicking
  2. Make the click button bigger and blue
  3. Toggle the mod menu with a different key — Press a different key to open the menu—useful if M is used for something else in your game
  4. Start with $10,000 instead of $0 — Give players a head start so they can immediately test multipliers and features
  5. Make auto-clicker start enabled — Players won't have to enable it manually—great for testing passive income
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch builds a fully playable clicker game with mechanics you'd recognize from games like Cookie Clicker. Players click a button to earn money, which increases automatically through passive income and an optional auto-clicker. The real power lies in its mod menu system: pressing 'M' opens a tabbed interface where you adjust click values, passive income rates, game speed, and more—all while the game runs. The code demonstrates advanced p5.js techniques including custom UI components (buttons, sliders, input fields), event handling for both mouse and keyboard input, and dynamic state management through a centralized configuration object.

The code is organized into two major sections: game state variables at the top that track money, multipliers, and settings, and a massive modMenu object that contains all UI logic for drawing and handling the menu. The p5.js functions—setup(), draw(), and the event handlers mouseClicked(), keyPressed(), and keyTyped()—tie it together. By studying this sketch, you'll learn how to build a complete UI system from scratch, manage complex application state, and create responsive, interactive games that reward player engagement.

⚙️ How It Works

  1. When the sketch loads, preload() fetches a custom Orbitron font from a CDN, and setup() creates a full-window canvas and centers the mod menu. The modMenu object is initialized with all available settings organized into categories.
  2. Every frame, draw() updates the game state: passive income flows in continuously based on clickValue and the passiveIncomeMultiplier, scaled by deltaTime to run at real speed. If the auto-clicker is active, it checks whether enough time has passed to trigger another automatic click, scaled by gameSpeedMultiplier.
  3. The main game UI displays the current money total and a clickable button showing the value per click. When clicked, mouseClicked() checks if the mod menu is open; if closed, it checks whether the click hit the button and awards money accordingly.
  4. Pressing 'M' toggles modMenuOpen, revealing a tabbed interface organized into Game, Money, and Misc categories. Each tab contains buttons (toggle on/off), sliders (drag to adjust), input fields (type and press Enter), and text displays.
  5. Sliders work by checking if the mouse is pressed over the slider track, then mapping the mouse X position to the value range. Input fields track which one is active (selectedInput), accept typed numbers, and update game state variables when Enter is pressed.
  6. The modMenu.handleMouseClick() function translates mouse coordinates relative to the menu, checks every clickable element (categories, buttons, toggles, sliders, inputs), and executes the appropriate action—all without closing the game loop underneath.

🎓 Concepts You'll Learn

Game state managementEvent handling (mouse and keyboard)Custom UI components (buttons, sliders, inputs)Tabbed menu interfaceReal-time parameter adjustmentDynamic evaluation with eval()Mouse collision detectionTime-based animation with deltaTime

📝 Code Breakdown

preload()

preload() runs before setup() and is the ideal place to load fonts, images, and other media files. Without it, loadFont() might fail because the sketch would try to draw text before the font arrives.

function preload() {
  // Load the custom Orbitron font for the mod menu title and text
  // This is a reliable URL from Fontsource CDN for the Orbitron Latin 400 Normal style.
  modMenuFont = loadFont("https://unpkg.com/@fontsource/orbitron@latest/files/orbitron-latin-400-normal.woff");
}
Line-by-line explanation (1 lines)
modMenuFont = loadFont("https://unpkg.com/@fontsource/orbitron@latest/files/orbitron-latin-400-normal.woff");
Fetches a custom font from a CDN before setup() runs—preload() is the only p5.js function that waits for assets to load before continuing

setup()

setup() runs once when the sketch starts. It's where you initialize your canvas, set starting values, and prepare any persistent data structures—in this case, the mod menu.

function setup() {
  // Create a 2D canvas that fills the window
  createCanvas(windowWidth, windowHeight);
  // Initialize the mod menu's position
  modMenu.updatePosition();
}
Line-by-line explanation (2 lines)
createCanvas(windowWidth, windowHeight);
Creates a canvas as wide and tall as the browser window—windowWidth and windowHeight are p5.js variables that always hold the current window size
modMenu.updatePosition();
Calls the mod menu's updatePosition() function to center it on screen based on the canvas size

draw()

draw() is called 60 times per second and is where all animation and updates happen. Notice how both passive income and auto-clicks use time-based checks (deltaTime and millis()) rather than frame counters—this ensures the game runs at the correct speed even if frame rate varies. The last three conditionals (showFPS, showDebugInfo) are purely optional UI features that don't affect gameplay but help players understand what's happening under the hood.

🔬 The auto-clicker only runs if autoClickerActive is true. What happens if you change the condition to always be true, like 'if (true) {'? Will the auto-clicker always run even if you toggle it off in the menu?

  // Auto clicker logic
  if (autoClickerActive) {
    const clickInterval = 1000 / autoClickSpeed; // Calculate time between clicks in milliseconds
    // Auto-click interval is scaled by gameSpeedMultiplier
    if (millis() - lastAutoClickTime > clickInterval / gameSpeedMultiplier) {

🔬 These two lines handle passive income. If you remove the 'gameSpeedMultiplier' from the second line, what happens to passive income when you adjust game speed in the mod menu? Does the speed multiplier still affect passive income?

  passiveIncome = (clickValue * 0.1) * passiveIncomeMultiplier;
  money += passiveIncome * (deltaTime / 1000) * gameSpeedMultiplier;
function draw() {
  background(20); // Dark background for the game
  
  // Update passive income
  // Passive income is scaled by gameSpeedMultiplier
  passiveIncome = (clickValue * 0.1) * passiveIncomeMultiplier;
  money += passiveIncome * (deltaTime / 1000) * gameSpeedMultiplier; // deltaTime is time since last frame in ms
  
  // Auto clicker logic
  if (autoClickerActive) {
    const clickInterval = 1000 / autoClickSpeed; // Calculate time between clicks in milliseconds
    // Auto-click interval is scaled by gameSpeedMultiplier
    if (millis() - lastAutoClickTime > clickInterval / gameSpeedMultiplier) {
      money += clickValue * clickValueMultiplier; // Add money based on click value and multiplier
      lastAutoClickTime = millis(); // Reset timer for next auto-click
    }
  }
  
  // Draw game elements (money display and click button)
  drawGame();
  
  // Draw the mod menu if it's open
  modMenu.draw();
  
  // Draw FPS display if enabled
  if (showFPS) {
    push();
    fill(0, 255, 0);
    textAlign(LEFT, TOP);
    textSize(18);
    text(`FPS: ${frameRate().toFixed(1)}`, 10, 10);
    pop();
  }
  
  // Draw Debug Info if enabled
  if (showDebugInfo) {
    push();
    fill(0, 255, 0);
    textAlign(RIGHT, TOP);
    textSize(16);
    let debugText = `Money: $${money.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 0 })}\n`;
    debugText += `Click Value: $${(clickValue * clickValueMultiplier).toLocaleString()}\n`;
    debugText += `Passive Income (per sec): $${(passiveIncome * gameSpeedMultiplier).toLocaleString()}\n`;
    debugText += `Auto Clicker Active: ${autoClickerActive}\n`;
    debugText += `Auto Click Speed: ${autoClickSpeed} clicks/sec\n`;
    debugText += `Game Speed: ${gameSpeedMultiplier}x`;
    text(debugText, width - 10, 10);
    pop();
  }
}
Line-by-line explanation (13 lines)

🔧 Subcomponents:

calculation Passive Income Update passiveIncome = (clickValue * 0.1) * passiveIncomeMultiplier;

Calculates how much money flows in per second based on click value and multiplier

calculation Apply Passive Income Over Time money += passiveIncome * (deltaTime / 1000) * gameSpeedMultiplier;

Adds passive income to the money total, scaled by real elapsed time and game speed

conditional Auto-Clicker Interval Check if (autoClickerActive) {

Only runs auto-click logic if the auto-clicker is enabled

conditional Auto-Click Trigger if (millis() - lastAutoClickTime > clickInterval / gameSpeedMultiplier) {

Checks if enough time has passed since the last auto-click to trigger another one

background(20); // Dark background for the game
Clears the canvas to a very dark gray (20 out of 255), creating the dark game background
passiveIncome = (clickValue * 0.1) * passiveIncomeMultiplier;
Passive income equals 10% of the click value, then multiplied by the passive income multiplier—this runs every frame to keep it in sync with clickValue
money += passiveIncome * (deltaTime / 1000) * gameSpeedMultiplier;
Adds money equal to passive income per second (deltaTime / 1000 converts milliseconds to seconds), scaled by game speed—this makes income smooth regardless of frame rate
if (autoClickerActive) {
Only attempts auto-clicks if the player has enabled the auto-clicker in the mod menu
const clickInterval = 1000 / autoClickSpeed;
Converts clicks per second into milliseconds between clicks—if autoClickSpeed is 2, clickInterval is 500ms
if (millis() - lastAutoClickTime > clickInterval / gameSpeedMultiplier) {
Checks if the time since the last auto-click exceeds the calculated interval, scaled by game speed—this triggers the next automatic click
money += clickValue * clickValueMultiplier;
Awards money as if the player clicked, multiplied by both the click value multiplier
lastAutoClickTime = millis();
Records the current time in milliseconds so the next auto-click trigger can measure the interval from this moment
drawGame();
Calls the drawGame() function to render the money display and click button
modMenu.draw();
Calls the mod menu's draw function to render the entire menu UI if it's open
if (showFPS) {
Only draws FPS information if the player has toggled the FPS display on in the mod menu
text(`FPS: ${frameRate().toFixed(1)}`, 10, 10);
Displays the current frame rate rounded to one decimal place in the top-left corner
if (showDebugInfo) {
Only draws debug information if the player has enabled it in the mod menu

drawGame()

drawGame() is a helper function that separates all game-specific rendering from the main draw() loop. It uses push() and pop() to avoid polluting the global drawing state. Notice that buttonX, buttonY, buttonWidth, and buttonHeight are also hardcoded into mouseClicked() to detect clicks—if you change the button size or position here, you must update those values in mouseClicked() or clicks won't register correctly.

🔬 These four lines define where and how big the button is. What happens if you change 'height / 2 + 50' to 'height / 2 - 50'? Will the button move up or down?

  // Draw the main click button
  const buttonWidth = 200;
  const buttonHeight = 80;
  const buttonX = width / 2;
  const buttonY = height / 2 + 50;

🔬 This block draws the button. If you change the first fill() to 'fill(80, 30, 30)' (swapping the red and green values), what color will the button become? Will it still look like a button?

  fill(30, 80, 30); // Dark green button background
  stroke(0, 150, 0); // Muted green border
  strokeWeight(2);
  rectMode(CENTER);
  rect(buttonX, buttonY, buttonWidth, buttonHeight, 10); // Rounded rectangle
function drawGame() {
  push();
  
  // Display current money
  fill(0, 255, 0); // Green text color
  textAlign(CENTER, CENTER);
  textSize(48);
  textFont('Arial'); // Use a standard font for game text
  text(`Money: $${money.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`, width / 2, height / 2 - 50);
  
  // Draw the main click button
  const buttonWidth = 200;
  const buttonHeight = 80;
  const buttonX = width / 2;
  const buttonY = height / 2 + 50;
  
  fill(30, 80, 30); // Dark green button background
  stroke(0, 150, 0); // Muted green border
  strokeWeight(2);
  rectMode(CENTER);
  rect(buttonX, buttonY, buttonWidth, buttonHeight, 10); // Rounded rectangle
  
  fill(0, 255, 0); // Bright green text color
  textAlign(CENTER, CENTER);
  textSize(24);
  textFont('Arial');
  text(`Click me!\n($${(clickValue * clickValueMultiplier).toLocaleString()}/click)`, buttonX, buttonY);
  
  pop();
}
Line-by-line explanation (19 lines)

🔧 Subcomponents:

calculation Money Display Text text(`Money: $${money.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`, width / 2, height / 2 - 50);

Renders the current money total centered near the top of the screen with comma formatting

calculation Click Button Rendering rect(buttonX, buttonY, buttonWidth, buttonHeight, 10);

Draws the clickable button rectangle with rounded corners in the center of the screen

calculation Button Label with Value text(`Click me!\n($${(clickValue * clickValueMultiplier).toLocaleString()}/click)`, buttonX, buttonY);

Displays the button text showing how much money each click will earn

push();
Saves the current drawing state (colors, fonts, transforms) so changes made in this function don't affect other functions
fill(0, 255, 0); // Green text color
Sets the text color to bright green (0 red, 255 green, 0 blue in RGB)
textAlign(CENTER, CENTER);
Centers text both horizontally and vertically at the coordinates where it's drawn
textSize(48);
Sets the text size to 48 pixels—large and prominent for the money display
textFont('Arial');
Uses the Arial system font (unlike the mod menu which uses the custom Orbitron font)
text(`Money: $${money.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`, width / 2, height / 2 - 50);
Renders 'Money: $' followed by the money variable formatted with commas every three digits, centered horizontally and positioned above the button
const buttonWidth = 200;
Stores the button width as 200 pixels—a local variable used only within this function
const buttonHeight = 80;
Stores the button height as 80 pixels
const buttonX = width / 2;
Positions the button horizontally at the center of the canvas
const buttonY = height / 2 + 50;
Positions the button vertically at the center, plus 50 pixels down
fill(30, 80, 30); // Dark green button background
Sets the fill color to a dark greenish tone for the button background
stroke(0, 150, 0); // Muted green border
Sets the outline color to a medium-bright green
strokeWeight(2);
Makes the button outline 2 pixels thick
rectMode(CENTER);
Changes how rectangles are drawn—now the x,y coordinates represent the center rather than the top-left corner
rect(buttonX, buttonY, buttonWidth, buttonHeight, 10); // Rounded rectangle
Draws a 200×80 rectangle centered on (buttonX, buttonY) with 10-pixel rounded corners
fill(0, 255, 0); // Bright green text color
Changes fill back to bright green for the button text
textSize(24);
Shrinks text size to 24 pixels for the button label
text(`Click me!\n($${(clickValue * clickValueMultiplier).toLocaleString()}/click)`, buttonX, buttonY);
Draws the button label on two lines showing the command and the money earned per click (with current multipliers applied)
pop();
Restores the drawing state saved by push(), so other functions aren't affected by color/font changes made here

mouseClicked()

mouseClicked() is called once each time the user clicks anywhere on the canvas. Notice the dual-path logic: if the menu is open, route the click there; otherwise, check if it hit the game button. The button collision detection must use the same coordinates and dimensions as drawGame(), or clicks won't land on what the player sees. This is a common source of bugs in interactive sketches—always sync your collision code with your drawing code.

🔬 This rectangle collision check uses 'buttonX - buttonWidth/2' and 'buttonX + buttonWidth/2' because rectMode is CENTER in drawGame(). What happens if you simplify it to 'mouseX > buttonX && mouseX < buttonX + buttonWidth'? Will clicks still register on the button, or will it be offset?

    // Check if the click was within the game's main click button
    if (mouseX > buttonX - buttonWidth/2 && mouseX < buttonX + buttonWidth/2 &&
        mouseY > buttonY - buttonHeight/2 && mouseY < buttonY + buttonHeight/2) {
      money += clickValue * clickValueMultiplier; // Add money based on click value and multiplier
function mouseClicked() {
  // If mod menu is open, handle its clicks
  if (modMenuOpen) {
    modMenu.handleMouseClick(mouseX, mouseY);
  } else {
    // If mod menu is closed, handle regular game click
    const buttonWidth = 200;
    const buttonHeight = 80;
    const buttonX = width / 2;
    const buttonY = height / 2 + 50;
    
    // Check if the click was within the game's main click button
    if (mouseX > buttonX - buttonWidth/2 && mouseX < buttonX + buttonWidth/2 &&
        mouseY > buttonY - buttonHeight/2 && mouseY < buttonY + buttonHeight/2) {
      money += clickValue * clickValueMultiplier; // Add money based on click value and multiplier
    }
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

conditional Button Hit Detection if (mouseX > buttonX - buttonWidth/2 && mouseX < buttonX + buttonWidth/2 && mouseY > buttonY - buttonHeight/2 && mouseY < buttonY + buttonHeight/2) {

Checks if the click landed inside the button's rectangular area

if (modMenuOpen) {
Checks whether the mod menu is currently open
modMenu.handleMouseClick(mouseX, mouseY);
If the menu is open, delegates the click to the mod menu's event handler, which processes button clicks, slider drags, input fields, etc.
const buttonWidth = 200;
Stores the button width—must match the value in drawGame() or clicks won't align with what's drawn
const buttonHeight = 80;
Stores the button height—must also match drawGame()
const buttonX = width / 2;
Button's horizontal center position—same as drawGame()
const buttonY = height / 2 + 50;
Button's vertical center position—same as drawGame()
if (mouseX > buttonX - buttonWidth/2 && mouseX < buttonX + buttonWidth/2 &&
Checks if the click's X coordinate is within the left and right edges of the button (center ± half-width)
mouseY > buttonY - buttonHeight/2 && mouseY < buttonY + buttonHeight/2) {
Checks if the click's Y coordinate is within the top and bottom edges of the button (center ± half-height)
money += clickValue * clickValueMultiplier; // Add money based on click value and multiplier
If the click hit the button, add money equal to the click value multiplied by the current multiplier

keyPressed()

keyPressed() is called once each time a key is held down (and then repeatedly if held). It's the right place to detect special keys like Backspace and Enter (use keyCode) as well as regular characters (use key). Notice the use of eval() to dynamically update global variables—this is a shortcut that works here but can be risky; in larger projects, use a state object or Map instead to avoid security issues.

🔬 This Backspace handler removes one character at a time. What happens if you change 'activeOption.value.length - 1' to 'activeOption.value.length - 2'? Will it delete one or two characters when you press Backspace?

      if (keyCode === BACKSPACE) {
        // Remove last character
        activeOption.value = activeOption.value.substring(0, activeOption.value.length - 1);
function keyPressed() {
  // Toggle the mod menu open/closed when the 'M' key is pressed
  if (key.toUpperCase() === 'M') {
    modMenuOpen = !modMenuOpen;
    selectedInput = null; // Deactivate any active input field
  }
  
  // Handle input field typing if one is active
  if (modMenuOpen && selectedInput !== null) {
    const activeOption = modMenu.findOptionByGameStateVar(selectedInput);
    if (activeOption && activeOption.type === "input") {
      if (keyCode === BACKSPACE) {
        // Remove last character
        activeOption.value = activeOption.value.substring(0, activeOption.value.length - 1);
      } else if (keyCode === ENTER) {
        // Apply the value
        const parsedValue = parseFloat(activeOption.value);
        if (!isNaN(parsedValue) && parsedValue >= 0) { // Basic validation
          eval(`${activeOption.gameStateVar} = parsedValue;`); // Update game state variable
        }
        selectedInput = null; // Deactivate input field
      }
    }
  }
}
Line-by-line explanation (13 lines)

🔧 Subcomponents:

conditional Backspace Handling if (keyCode === BACKSPACE) {

Removes the last character from an active input field when Backspace is pressed

conditional Enter to Submit Input } else if (keyCode === ENTER) {

Parses the input value and applies it to the corresponding game state variable when Enter is pressed

if (key.toUpperCase() === 'M') {
Checks if the pressed key is 'M' (converting to uppercase so 'm' also works)
modMenuOpen = !modMenuOpen;
Flips the modMenuOpen boolean—if it was true, it becomes false, and vice versa
selectedInput = null; // Deactivate any active input field
Clears any active input field when the menu is toggled to prevent text from being typed into an invisible input
if (modMenuOpen && selectedInput !== null) {
Only processes text input if the menu is open AND a specific input field is active (selectedInput is not null)
const activeOption = modMenu.findOptionByGameStateVar(selectedInput);
Looks up the mod menu option object that corresponds to the currently active input field
if (activeOption && activeOption.type === "input") {
Ensures the found option exists and is actually an input field type before processing keyboard input
if (keyCode === BACKSPACE) {
Checks if the Backspace key was pressed (keyCode is a p5.js variable that stores the numeric code of the last key pressed)
activeOption.value = activeOption.value.substring(0, activeOption.value.length - 1);
Removes the last character from the input field's value string by extracting everything except the final character
} else if (keyCode === ENTER) {
Checks if the Enter key was pressed
const parsedValue = parseFloat(activeOption.value);
Converts the input string to a number using parseFloat() (handles decimals)
if (!isNaN(parsedValue) && parsedValue >= 0) {
Basic validation: checks that the parsed value is a valid number and not negative
eval(`${activeOption.gameStateVar} = parsedValue;`);
Uses eval() to dynamically update the global game state variable (e.g., 'money = 5000') based on the variable name stored in gameStateVar
selectedInput = null; // Deactivate input field
Clears the active input field after the value is applied

keyTyped()

keyTyped() is called whenever a character is actually typed (as opposed to keyPressed(), which fires for special keys). It's better for text input because it handles repeated key presses and provides the actual character in the key variable. Here it enforces numeric-only input for number fields while allowing free text for others.

🔬 This block prevents typing letters into number fields but allows them in other fields. What happens if you remove the outer 'if (activeOption.inputType === "number")' and just use 'activeOption.value += key;' directly? Can you type 'abc' into the 'Set Money' input field in the Money tab?

      // Only allow numeric input if inputType is 'number'
      if (activeOption.inputType === "number") {
        if (key >= '0' && key <= '9') {
          activeOption.value += key;
        } else if (key === '.' && !activeOption.value.includes('.')) {
          activeOption.value += key;
        }
      } else {
        activeOption.value += key; // Allow any characters for other input types
function keyTyped() {
  // Handle typing into an active input field
  if (modMenuOpen && selectedInput !== null) {
    const activeOption = modMenu.findOptionByGameStateVar(selectedInput);
    if (activeOption && activeOption.type === "input") {
      // Only allow numeric input if inputType is 'number'
      if (activeOption.inputType === "number") {
        if (key >= '0' && key <= '9') {
          activeOption.value += key;
        } else if (key === '.' && !activeOption.value.includes('.')) {
          activeOption.value += key;
        }
      } else {
        activeOption.value += key; // Allow any characters for other input types
      }
    }
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

conditional Numeric Input Validation if (activeOption.inputType === "number") {

Only appends digits and decimals if the input field is marked as type 'number'

conditional Digit Character Check if (key >= '0' && key <= '9') {

Allows any digit from 0-9 to be typed into a numeric input

if (modMenuOpen && selectedInput !== null) {
Only processes typed characters if the menu is open and an input field is active
const activeOption = modMenu.findOptionByGameStateVar(selectedInput);
Retrieves the mod menu option object that corresponds to the active input field
if (activeOption && activeOption.type === "input") {
Ensures the option exists and is an input field before processing typed text
if (activeOption.inputType === "number") {
Checks if this input field is restricted to numeric input only
if (key >= '0' && key <= '9') {
Checks if the typed character is a digit by comparing it to the string characters '0' through '9'
activeOption.value += key;
Appends the digit to the input field's value
} else if (key === '.' && !activeOption.value.includes('.')) {
Allows a decimal point if one hasn't already been typed (prevents '5.5.5')
} else {
For non-numeric input types, allow any character to be typed

windowResized()

windowResized() is automatically called by p5.js whenever the browser window is resized. Without it, the canvas would stay its original size and not fill the new window. It's essential for full-screen responsive sketches.

function windowResized() {
  // Resize the canvas when the window (or preview panel) is resized
  resizeCanvas(windowWidth, windowHeight);
  // Update the mod menu's position to keep it centered
  modMenu.updatePosition();
}
Line-by-line explanation (2 lines)
resizeCanvas(windowWidth, windowHeight);
Adjusts the canvas size to match the new window dimensions (windowWidth and windowHeight are updated automatically by p5.js)
modMenu.updatePosition();
Recalculates the mod menu's position to keep it centered after the canvas has been resized

drawButton()

drawButton() is a reusable helper method inside the modMenu object that draws any clickable button. By abstracting it, the main menu drawing code stays clean and readable. The isSelected parameter allows the same function to render buttons in two distinct visual states—a key pattern for interactive UI design.

🔬 This if/else controls button colors based on whether they're selected. What happens if you swap the colors—put the 'if' colors on the 'else' side and vice versa? Will selected buttons appear darker than unselected ones?

    if (isSelected) {
      fill(50, 150, 50); // Darker green for active/selected
      stroke(0, 255, 0); // Bright green border
    } else {
      fill(30, 80, 30); // Dark green background
      stroke(0, 150, 0); // Muted green border
  // Helper function to draw a button
  drawButton: function(label, bx, by, bw, bh, isSelected = false) {
    push();
    if (isSelected) {
      fill(50, 150, 50); // Darker green for active/selected
      stroke(0, 255, 0); // Bright green border
    } else {
      fill(30, 80, 30); // Dark green background
      stroke(0, 150, 0); // Muted green border
    }
    strokeWeight(2);
    rectMode(CORNER);
    rect(bx, by, bw, bh, 5); // Rounded rectangle
    
    noStroke();
    fill(0, 255, 0); // Bright green text
    textAlign(CENTER, CENTER);
    textSize(18);
    textFont(modMenuFont); // Use custom font for menu text
    text(label, bx + bw/2, by + bh/2);
    pop();
  },
Line-by-line explanation (16 lines)

🔧 Subcomponents:

conditional Selected State Check if (isSelected) {

Renders the button in a brighter color if it's selected/active

drawButton: function(label, bx, by, bw, bh, isSelected = false) {
Defines a method on the modMenu object that takes a label text, position (bx, by), size (bw, bh), and optional isSelected flag (defaults to false)
push();
Saves the current drawing state to avoid affecting other drawings
if (isSelected) {
Checks whether this button should be drawn as selected (highlighted)
fill(50, 150, 50); // Darker green for active/selected
If selected, use a brighter green fill to highlight the button
stroke(0, 255, 0); // Bright green border
Also draws a bright green border to further highlight it
fill(30, 80, 30); // Dark green background
If not selected, use a darker, more subdued green fill
stroke(0, 150, 0); // Muted green border
And a muted green border for unselected buttons
strokeWeight(2);
All buttons have a 2-pixel border
rectMode(CORNER);
Use corner-based positioning for the rectangle (x, y is top-left)
rect(bx, by, bw, bh, 5); // Rounded rectangle
Draws the button rectangle with 5-pixel rounded corners
noStroke();
Removes the stroke before drawing text so the text has no outline
fill(0, 255, 0); // Bright green text
Sets text color to bright green
textSize(18);
Sets text size to 18 pixels
textFont(modMenuFont); // Use custom font for menu text
Uses the Orbitron font loaded in preload() for a distinctive look
text(label, bx + bw/2, by + bh/2);
Draws the button label centered in the button (since textAlign is CENTER, CENTER)
pop();
Restores the previous drawing state

drawSlider()

drawSlider() renders both the slider track and the knob's position. The key insight is the map() function, which converts between value space (0–10) and pixel space (screen coordinates). Without it, slider dragging would require complex math. This pattern is useful for any value-to-position conversion in interactive UI.

🔬 The map() function converts a value from one range (minVal to maxVal) to a pixel position (trackX to trackX + trackWidth). If you swap the last two arguments to map(val, minVal, maxVal, trackX + trackWidth, trackX), which direction does the knob move when you drag? Does high value go left or right?

    // Slider knob
    const val = option.value;
    const minVal = option.min;
    const maxVal = option.max;
    const knobX = map(val, minVal, maxVal, trackX, trackX + trackWidth);
  // Helper function to draw a slider
  drawSlider: function(label, bx, by, bw, bh, option) {
    push();
    fill(30, 80, 30); // Dark green background for slider area
    stroke(0, 150, 0);
    strokeWeight(2);
    rectMode(CORNER);
    rect(bx, by, bw, bh, 5);
    
    // Slider track
    const trackY = by + bh / 2;
    const trackWidth = bw - modMenu.padding * 2;
    const trackX = bx + modMenu.padding;
    stroke(0, 100, 0); // Darker green track line
    strokeWeight(4);
    line(trackX, trackY, trackX + trackWidth, trackY);
    
    // Slider knob
    const val = option.value;
    const minVal = option.min;
    const maxVal = option.max;
    const knobX = map(val, minVal, maxVal, trackX, trackX + trackWidth);
    fill(0, 255, 0); // Bright green knob
    noStroke();
    ellipse(knobX, trackY, 15);
    
    // Label and current value
    fill(0, 255, 0);
    textAlign(LEFT, CENTER);
    textSize(16);
    textFont(modMenuFont);
    text(label, bx + modMenu.padding, by + bh / 4);
    textAlign(RIGHT, CENTER);
    text(val.toFixed(option.step < 1 ? 1 : 0), bx + bw - modMenu.padding, by + bh / 4);
    pop();
  },
Line-by-line explanation (15 lines)

🔧 Subcomponents:

calculation Slider Container Background rect(bx, by, bw, bh, 5);

Draws the background rectangle containing the entire slider

calculation Slider Track Line line(trackX, trackY, trackX + trackWidth, trackY);

Draws the horizontal line along which the knob slides

calculation Slider Knob Position const knobX = map(val, minVal, maxVal, trackX, trackX + trackWidth);

Maps the slider's numeric value to a pixel position along the track using the map() function

drawSlider: function(label, bx, by, bw, bh, option) {
Defines a method that draws a slider UI element with a label, position, size, and the option object containing min/max/current value
push();
Saves drawing state
fill(30, 80, 30); // Dark green background for slider area
Sets the background color for the slider container
rect(bx, by, bw, bh, 5);
Draws the background rectangle for the entire slider
const trackY = by + bh / 2;
Calculates the vertical position of the slider track (middle of the container)
const trackWidth = bw - modMenu.padding * 2;
Calculates the usable width of the track (total width minus padding on both sides)
const trackX = bx + modMenu.padding;
Calculates the starting X position of the track (left edge plus padding)
line(trackX, trackY, trackX + trackWidth, trackY);
Draws a horizontal line representing the slider track
const val = option.value;
Stores the option's current value for easier reference
const minVal = option.min;
Stores the minimum value the slider can have
const maxVal = option.max;
Stores the maximum value the slider can have
const knobX = map(val, minVal, maxVal, trackX, trackX + trackWidth);
Uses p5.js map() to convert the value (between min and max) to a pixel position (between trackX and trackX + trackWidth)
ellipse(knobX, trackY, 15);
Draws a 15-pixel diameter circle (knob) at the calculated position along the track
text(label, bx + modMenu.padding, by + bh / 4);
Draws the slider's label text in the upper-left area of the container
text(val.toFixed(option.step < 1 ? 1 : 0), bx + bw - modMenu.padding, by + bh / 4);
Displays the current slider value on the right side, rounded to 1 decimal place if step < 1, otherwise to 0 (integer)

drawInput()

drawInput() renders a text input field with placeholder text and a blinking cursor. The cursor blink is a great example of using frameCount % cycle_length to create repeating animations without timers. The conditional logic for showing placeholder vs actual text is a common UX pattern in form design.

🔬 The cursor blinks because 'frameCount % 30' cycles from 0–29 each 30 frames. The condition '< 15' makes it visible for frames 0–14, invisible for 15–29. What happens if you change '< 15' to '< 20'? Will the cursor blink faster, slower, or spend more time visible?

      // Blinking cursor if this input is active
      if (selectedInput === option.gameStateVar && frameCount % 30 < 15) {
        stroke(0, 255, 0);
        strokeWeight(2);
        line(inputX + 5 + textWidth(option.value), inputY + 5, inputX + 5 + textWidth(option.value), inputY + inputHeight - 5);
  // Helper function to draw an input field
  drawInput: function(label, bx, by, bw, bh, option) {
    push();
    fill(30, 80, 30); // Dark green background for input area
    stroke(0, 150, 0);
    strokeWeight(2);
    rectMode(CORNER);
    rect(bx, by, bw, bh, 5);
    
    // Label
    fill(0, 255, 0);
    textAlign(LEFT, CENTER);
    textSize(16);
    textFont(modMenuFont);
    text(label, bx + modMenu.padding, by + bh / 4);
    
    // Input box
    const inputX = bx + modMenu.padding;
    const inputY = by + bh / 2 + 5;
    const inputWidth = bw - modMenu.padding * 2;
    const inputHeight = bh / 2 - 10;
    
    fill(0, 0, 0, 180); // Darker background for the actual input text
    stroke(0, 255, 0); // Bright green border
    strokeWeight(1);
    rect(inputX, inputY, inputWidth, inputHeight, 3);
    
    // Input text
    fill(0, 255, 0);
    textAlign(LEFT, CENTER);
    textSize(18);
    // Draw placeholder if value is empty and not active
    if (option.value === "" && selectedInput !== option.gameStateVar) {
      fill(0, 150, 0); // Muted green for placeholder
      text(option.placeholder, inputX + 5, inputY + inputHeight / 2);
    } else {
      fill(0, 255, 0); // Bright green for active text
      text(option.value, inputX + 5, inputY + inputHeight / 2);
      
      // Blinking cursor if this input is active
      if (selectedInput === option.gameStateVar && frameCount % 30 < 15) {
        stroke(0, 255, 0);
        strokeWeight(2);
        line(inputX + 5 + textWidth(option.value), inputY + 5, inputX + 5 + textWidth(option.value), inputY + inputHeight - 5);
      }
    }
    
    pop();
  },
Line-by-line explanation (15 lines)

🔧 Subcomponents:

conditional Placeholder vs Input Display if (option.value === "" && selectedInput !== option.gameStateVar) {

Shows placeholder text when field is empty and not active, otherwise shows the actual input value

drawInput: function(label, bx, by, bw, bh, option) {
Defines a method that draws an input field with a label, position, size, and the option object containing value, placeholder, etc.
fill(30, 80, 30); // Dark green background for input area
Sets the background color for the input container
rect(bx, by, bw, bh, 5);
Draws the background rectangle for the input field
text(label, bx + modMenu.padding, by + bh / 4);
Draws the label text in the upper portion of the input container
const inputX = bx + modMenu.padding;
Calculates the X position of the actual text input box (container X plus padding)
const inputY = by + bh / 2 + 5;
Calculates the Y position of the actual text input box (below the label)
const inputWidth = bw - modMenu.padding * 2;
Calculates the width of the text input box (total width minus padding)
const inputHeight = bh / 2 - 10;
Calculates the height of the text input box (half the container height minus spacing)
fill(0, 0, 0, 180); // Darker background for the actual input text
Sets the input box background to nearly black with some transparency (180 alpha)
rect(inputX, inputY, inputWidth, inputHeight, 3);
Draws the actual input text box with 3-pixel rounded corners
if (option.value === "" && selectedInput !== option.gameStateVar) {
Checks if the field is empty AND not currently selected
text(option.placeholder, inputX + 5, inputY + inputHeight / 2);
If empty and not active, displays the placeholder text in a muted color
text(option.value, inputX + 5, inputY + inputHeight / 2);
If not empty or active, displays the actual input text in bright green
if (selectedInput === option.gameStateVar && frameCount % 30 < 15) {
Draws a blinking cursor if this is the active input AND we're in the first half of a 30-frame cycle (creating a blink effect)
line(inputX + 5 + textWidth(option.value), inputY + 5, inputX + 5 + textWidth(option.value), inputY + inputHeight - 5);
Draws a vertical line cursor at the end of the text (position calculated using textWidth() to measure the text length)

modMenu.draw()

modMenu.draw() is the central rendering function for the entire UI. It uses translate() to offset all coordinates, then conditionally draws options based on the active category. The structure—check menu open, draw background, draw categories, draw options based on type—is a standard pattern for tabbed interfaces. Notice how it delegates to drawButton(), drawSlider(), and drawInput() rather than handling each type itself—this separation of concerns keeps the code readable.

🔬 This loop draws three category buttons. If you add 'Advanced' to the categories array like '"Game", "Money", "Misc", "Advanced"', will the buttons automatically resize to fit, or will they overflow?

    // Category buttons ("Game", "Money", "Misc")
    const categories = ["Game", "Money", "Misc"];
    const categoryButtonWidth = (this.width - this.padding * 4) / categories.length;
    let currentY = this.padding * 3.5;
    
    for (let i = 0; i < categories.length; i++) {
      const cat = categories[i];
      const catX = this.padding + (categoryButtonWidth + this.padding) * i;
      this.drawButton(cat, catX, currentY, categoryButtonWidth, 40, this.activeCategory === cat);
    }
  // Main function to draw the entire mod menu
  draw: function() {
    if (!modMenuOpen) return; // Only draw if menu is open
    
    push();
    translate(this.x, this.y); // Translate to menu's position
    
    // Background rectangle for the menu
    fill(0, 0, 0, 200); // Semi-transparent black
    stroke(0, 255, 0); // Bright green border
    strokeWeight(3);
    rectMode(CORNER);
    rect(0, 0, this.width, this.height, 10);
    
    // Title
    fill(0, 255, 0);
    textAlign(CENTER, CENTER);
    textSize(32);
    textFont(modMenuFont);
    text("Axel's Mod Menu", this.width / 2, this.padding + 15);
    
    // Category buttons ("Game", "Money", "Misc")
    const categories = ["Game", "Money", "Misc"];
    const categoryButtonWidth = (this.width - this.padding * 4) / categories.length;
    let currentY = this.padding * 3.5;
    
    for (let i = 0; i < categories.length; i++) {
      const cat = categories[i];
      const catX = this.padding + (categoryButtonWidth + this.padding) * i;
      this.drawButton(cat, catX, currentY, categoryButtonWidth, 40, this.activeCategory === cat);
    }
    
    currentY += 40 + this.padding; // Move Y down after category buttons
    
    // Determine which options to display based on the active category
    let optionsToDisplay = [];
    if (this.activeCategory === "Game") {
      optionsToDisplay = [
        this.options.toggleFPS,
        this.options.toggleDebug,
        this.options.gameSpeed,
        this.options.autoClicker,
        this.options.autoClickSpeed
      ];
    } else if (this.activeCategory === "Money") {
      optionsToDisplay = [
        { label: `Current Money: $${money.toLocaleString()}`, type: "text" }, // Display current money
        this.options.addMoney,
        this.options.setMoneyMillion,
        this.options.setMoneyCustom, // New
        this.options.setClickValueCustom, // New
        this.options.setPassiveIncomeCustom, // New
        this.options.clickValueMultiplier,
        this.options.passiveIncomeMultiplier
      ];
    } else if (this.activeCategory === "Misc") {
      optionsToDisplay = [
        this.options.resetGame
      ];
    }
    
    const optionWidth = this.width - this.padding * 2;
    const optionHeight = 40;
    
    // Draw each option
    for (const option of optionsToDisplay) {
      if (option.type === "text") {
        // Just display text, not clickable
        fill(0, 255, 0);
        textAlign(LEFT, CENTER);
        textSize(18);
        textFont(modMenuFont);
        text(option.label, this.padding, currentY + optionHeight / 2);
      } else if (option.type === "button") {
        this.drawButton(option.label, this.padding, currentY, optionWidth, optionHeight);
      } else if (option.type === "toggle") {
        const toggleLabel = `${option.label}: ${option.value ? "ON" : "OFF"}`;
        this.drawButton(toggleLabel, this.padding, currentY, optionWidth, optionHeight, option.value);
      } else if (option.type === "slider") {
        this.drawSlider(option.label, this.padding, currentY, optionWidth, optionHeight, option);
      } else if (option.type === "input") { // New input field type
        this.drawInput(option.label, this.padding, currentY, optionWidth, optionHeight, option);
      }
      currentY += optionHeight + this.padding / 2; // Move Y down for next option
    }
    
    pop();
  },
Line-by-line explanation (15 lines)

🔧 Subcomponents:

for-loop Draw Category Buttons Loop for (let i = 0; i < categories.length; i++) {

Iterates through Game/Money/Misc categories and draws a button for each one

for-loop Draw Menu Options Loop for (const option of optionsToDisplay) {

Draws each option (text, button, toggle, slider, or input) based on its type

if (!modMenuOpen) return;
If the mod menu isn't open, exit immediately without drawing anything—an optimization to avoid unnecessary work
push();
Saves the current drawing state
translate(this.x, this.y);
Shifts the origin (0,0) to the mod menu's position (this.x, this.y), so all subsequent coordinates are relative to the menu
fill(0, 0, 0, 200); // Semi-transparent black
Sets the menu background to mostly black but slightly transparent (200 alpha out of 255)
rect(0, 0, this.width, this.height, 10);
Draws the menu's background rectangle from (0,0) with the menu's width and height and 10-pixel rounded corners
text("Axel's Mod Menu", this.width / 2, this.padding + 15);
Draws the title centered at the top of the menu
const categories = ["Game", "Money", "Misc"];
Defines an array of the three category names
const categoryButtonWidth = (this.width - this.padding * 4) / categories.length;
Calculates the width of each category button so they fit evenly across the menu
for (let i = 0; i < categories.length; i++) {
Loops through each category to draw its button
const catX = this.padding + (categoryButtonWidth + this.padding) * i;
Calculates the X position of the button (spaced with padding between each)
this.drawButton(cat, catX, currentY, categoryButtonWidth, 40, this.activeCategory === cat);
Draws the category button, highlighting it if it's the currently active category
if (this.activeCategory === "Game") {
Checks which category is active and builds the appropriate options list
for (const option of optionsToDisplay) {
Iterates through each option to display
if (option.type === "text") {
If the option is just text (not interactive), draw it as plain text
const toggleLabel = `${option.label}: ${option.value ? "ON" : "OFF"}`;
For toggles, appends 'ON' or 'OFF' to the label based on the current value

modMenu.handleMouseClick()

handleMouseClick() is the event router for the entire mod menu. The key concept is coordinate transformation: because the menu uses translate(), the mouse coordinates must be adjusted by subtracting the menu's position. The function then tests hits against each interactive element in order (categories, then options), breaking as soon as it finds a match. The slider logic is especially interesting because it works on drag—it checks mouseIsPressed to detect when the user is actively dragging, updating the value in real-time.

  // Handles mouse clicks and drags on the mod menu
  handleMouseClick: function(mx, my) {
    if (!modMenuOpen) return;
    
    // Transform mouse coordinates relative to the mod menu's top-left corner
    const transformedX = mx - this.x;
    const transformedY = my - this.y;
    
    // Check if a category button was clicked
    const categories = ["Game", "Money", "Misc"];
    const categoryButtonWidth = (this.width - this.padding * 4) / categories.length;
    const categoryButtonY = this.padding * 3.5;
    
    for (let i = 0; i < categories.length; i++) {
      const cat = categories[i];
      const catX = this.padding + (categoryButtonWidth + this.padding) * i;
      if (transformedX > catX && transformedX < catX + categoryButtonWidth &&
          transformedY > categoryButtonY && transformedY < categoryButtonY + 40) {
        this.activeCategory = cat; // Switch active category
        selectedInput = null; // Deactivate any active input field
        return;
      }
    }
    
    // Check if a mod option was clicked or dragged
    let currentY = this.padding * 3.5 + 40 + this.padding;
    const optionWidth = this.width - this.padding * 2;
    const optionHeight = 40;
    
    let optionsToDisplay = [];
    if (this.activeCategory === "Game") {
      optionsToDisplay = [
        this.options.toggleFPS, // New
        this.options.toggleDebug, // New
        this.options.gameSpeed, // New
        this.options.autoClicker,
        this.options.autoClickSpeed
      ];
    } else if (this.activeCategory === "Money") {
      optionsToDisplay = [
        { label: `Current Money: $${money.toLocaleString()}`, type: "text" },
        this.options.addMoney,
        this.options.setMoneyMillion,
        this.options.setMoneyCustom, // New
        this.options.setClickValueCustom, // New
        this.options.setPassiveIncomeCustom, // New
        this.options.clickValueMultiplier,
        this.options.passiveIncomeMultiplier
      ];
    } else if (this.activeCategory === "Misc") {
      optionsToDisplay = [
        this.options.resetGame
      ];
    }
    
    let clickedOnOption = false;
    for (const option of optionsToDisplay) {
      if (option.type === "text") {
        // Not clickable
      } else if (option.type === "button") {
        if (transformedX > this.padding && transformedX < this.padding + optionWidth &&
            transformedY > currentY && transformedY < currentY + optionHeight) {
          option.action(); // Execute button's action
          clickedOnOption = true;
          selectedInput = null; // Deactivate input
          break;
        }
      } else if (option.type === "toggle") {
        if (transformedX > this.padding && transformedX < this.padding + optionWidth &&
            transformedY > currentY && transformedY < currentY + optionHeight) {
          option.value = !option.value; // Flip toggle state
          eval(`${option.gameStateVar} = option.value`); // Update corresponding game state variable
          clickedOnOption = true;
          selectedInput = null; // Deactivate input
          break;
        }
      } else if (option.type === "slider") {
        // Slider track area for clicking/dragging
        const trackY = currentY + optionHeight / 2;
        const trackWidth = optionWidth - modMenu.padding * 2;
        const trackX = this.padding + modMenu.padding;
        
        // This condition checks if the mouse is currently pressed AND over the slider track
        if (mouseIsPressed &&
            transformedY > trackY - 10 && transformedY < trackY + 10 && // Allow a small vertical tolerance
            transformedX > trackX && transformedX < trackX + trackWidth) {
          // Map mouse X position to the slider's value range
          const newValue = map(transformedX, trackX, trackX + trackWidth, option.min, option.max);
          option.value = round(newValue / option.step) * option.step; // Snap to the nearest step value
          eval(`${option.gameStateVar} = option.value`); // Update corresponding game state variable
          clickedOnOption = true;
          selectedInput = null; // Deactivate input
          break;
        }
      } else if (option.type === "input") { // New input field logic
        const inputX = this.padding;
        const inputY = currentY;
        const inputWidth = optionWidth;
        const inputHeight = optionHeight;
        
        if (transformedX > inputX && transformedX < inputX + inputWidth &&
            transformedY > inputY && transformedY < inputY + inputHeight) {
          selectedInput = option.gameStateVar; // Activate this input field
          clickedOnOption = true;
          break;
        }
      }
      currentY += optionHeight + modMenu.padding / 2; // Move Y down for next option
    }
    
    // If clicked outside any mod menu option, deactivate any active input field
    if (!clickedOnOption && selectedInput !== null) {
      selectedInput = null;
    }
  },
Line-by-line explanation (13 lines)

🔧 Subcomponents:

calculation Coordinate Transformation const transformedX = mx - this.x; const transformedY = my - this.y;

Converts mouse coordinates to be relative to the mod menu's position, compensating for menu translation

for-loop Category Button Click Detection for (let i = 0; i < categories.length; i++) {

Tests each category button to see if the click landed on it

conditional Slider Drag Detection if (mouseIsPressed && transformedY > trackY - 10 && transformedY < trackY + 10 && transformedX > trackX && transformedX < trackX + trackWidth) {

Detects when the user drags on the slider track and updates the value based on mouse position

if (!modMenuOpen) return;
If the menu is closed, there's nothing to click on, so exit immediately
const transformedX = mx - this.x;
Subtracts the menu's X position from the mouse X to get coordinates relative to the menu's top-left corner
const transformedY = my - this.y;
Same for Y coordinates—this is necessary because the menu draws with translate()
for (let i = 0; i < categories.length; i++) {
Loops through the three category buttons to check if one was clicked
if (transformedX > catX && transformedX < catX + categoryButtonWidth &&
Checks if the click's X is within the button's left and right edges
transformedY > categoryButtonY && transformedY < categoryButtonY + 40) {
Checks if the click's Y is within the button's top and bottom edges
this.activeCategory = cat;
If both X and Y match, switch to that category and update the menu
option.action();
If a button option was clicked, execute its action function (which might add money, reset the game, etc.)
option.value = !option.value;
If a toggle was clicked, flip its value from true to false or vice versa
eval(`${option.gameStateVar} = option.value`);
Dynamically updates the corresponding global variable based on the gameStateVar string—e.g., 'autoClickerActive = true'
const newValue = map(transformedX, trackX, trackX + trackWidth, option.min, option.max);
Converts the current mouse X position to a value within the slider's min/max range using map()
option.value = round(newValue / option.step) * option.step;
Snaps the value to the nearest step increment—e.g., if step is 0.5, only 0, 0.5, 1, 1.5 are allowed
selectedInput = option.gameStateVar;
When an input field is clicked, marks it as active by storing its gameStateVar name

modMenu.updatePosition()

updatePosition() is a simple helper that keeps the mod menu centered on screen. It's called in setup() when the canvas is first created and again in windowResized() whenever the window is resized. This pattern ensures the menu stays centered even if the user resizes the browser.

  // Updates the mod menu's position to keep it centered
  updatePosition: function() {
    this.x = (windowWidth - this.width) / 2;
    this.y = (windowHeight - this.height) / 2;
  },
Line-by-line explanation (2 lines)
this.x = (windowWidth - this.width) / 2;
Centers the menu horizontally by calculating (window width - menu width) / 2, so there's equal space on left and right
this.y = (windowHeight - this.height) / 2;
Centers the menu vertically using the same logic

modMenu.findOptionByGameStateVar()

findOptionByGameStateVar() is a lookup function that searches the mod menu options by the name of the game state variable they control (e.g., 'money', 'autoClickerActive'). It's used by keyTyped() and keyPressed() to find which input field is active so that typed text can be appended to the correct one. This pattern is much safer than using eval() without knowing which variable is being updated.

  // Helper to find an option by its gameStateVar (used for input fields)
  findOptionByGameStateVar: function(varName) {
    for (let key in this.options) {
      const option = this.options[key];
      if (option.gameStateVar === varName) {
        return option;
      }
    }
    return null;
  }
Line-by-line explanation (5 lines)

🔧 Subcomponents:

for-loop Options Search Loop for (let key in this.options) {

Iterates through every option in the mod menu to find one matching the variable name

for (let key in this.options) {
Loops through every key in the modMenu.options object
const option = this.options[key];
Retrieves the option object for this key
if (option.gameStateVar === varName) {
Checks if this option's gameStateVar matches the search parameter
return option;
If found, returns the option object immediately
return null;
If the loop completes without finding a match, returns null

📦 Key Variables

money number

Tracks the player's total money earned in the game

let money = 0;
clickValue number

The base amount of money earned per click before multipliers

let clickValue = 1;
passiveIncome number

Money earned automatically per second (10% of clickValue by default)

let passiveIncome = 0;
clickValueMultiplier number

Multiplier applied to click value—adjustable in mod menu Money tab

let clickValueMultiplier = 1;
passiveIncomeMultiplier number

Multiplier applied to passive income per second—adjustable in mod menu Money tab

let passiveIncomeMultiplier = 1;
autoClickerActive boolean

Whether the auto-clicker is enabled—toggleable in mod menu Game tab

let autoClickerActive = false;
autoClickSpeed number

Clicks per second when auto-clicker is active—adjustable in mod menu Game tab

let autoClickSpeed = 1;
lastAutoClickTime number

Timestamp in milliseconds of the last auto-click, used to measure interval between clicks

let lastAutoClickTime = 0;
gameSpeedMultiplier number

Global speed multiplier affecting passive income and auto-click timing—adjustable in mod menu Game tab

let gameSpeedMultiplier = 1;
modMenuOpen boolean

Whether the mod menu is currently visible—toggled by pressing M

let modMenuOpen = false;
modMenuFont p5.Font

Stores the loaded Orbitron custom font for mod menu text

let modMenuFont;
selectedInput string or null

Stores the gameStateVar name of the currently active input field in the mod menu, or null if none

let selectedInput = null;
showFPS boolean

Whether to display frame rate in the top-left corner—toggleable in mod menu Game tab

let showFPS = false;
showDebugInfo boolean

Whether to display detailed debug information in the top-right—toggleable in mod menu Game tab

let showDebugInfo = false;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG draw() auto-clicker and drawGame() collision detection

Button click detection uses hardcoded coordinates (200, 80, width/2, height/2+50) that must stay in sync with drawGame(). If one is updated and the other isn't, clicks won't register correctly. This is a maintenance nightmare.

💡 Define buttonWidth, buttonHeight, buttonX, buttonY as global constants at the top of the sketch, then use them in both drawGame() and mouseClicked(). E.g., const BUTTON_WIDTH = 200; const BUTTON_HEIGHT = 80;

PERFORMANCE handleMouseClick() and draw()

The optionsToDisplay array is rebuilt every frame in draw() and every click in handleMouseClick(). For a sketch with dozens of options, this wastes CPU time repeatedly creating the same arrays.

💡 Pre-compute optionsToDisplay once as a member of modMenu, then update it when activeCategory changes, rather than rebuilding it every frame/click.

STYLE keyPressed() and eval()

Using eval() to dynamically update global variables is dangerous (security risk) and hard to debug. If a variable name is misspelled, it silently fails or creates a new global.

💡 Replace eval() with a state object: const gameState = { money, clickValue, ... } and use gameState[option.gameStateVar] = newValue. This is safer, clearer, and faster.

FEATURE modMenu and game state

The mod menu options array is hardcoded into the modMenu object. Adding new options requires manually updating both the options config and the optionsToDisplay conditionals in draw() and handleMouseClick().

💡 Add a 'category' property to each option: { label: '...', type: '...', category: 'Game', ... }. Then filter optionsToDisplay by category: optionsToDisplay = Object.values(this.options).filter(opt => opt.category === this.activeCategory). This reduces duplication significantly.

BUG modMenu.resetGame action

After reset, input field values (like setMoneyCustom.value) are reset to '0' or '1', but the global variables they're supposed to control might not fully reset if the player never interacted with them.

💡 Explicitly reset both the input field values AND the global variables they control. Make sure resetGame() calls eval() for every gameStateVar to guarantee consistency.

PERFORMANCE draw() drawDebugInfo

Building debugText with string concatenation and calling toLocaleString() every frame is slightly wasteful when the debug display is rarely watched.

💡 Only build and render debugText if showDebugInfo is true (it currently does, but early return at the top of the block would be cleaner).

🔄 Code Flow

Code flow showing preload, setup, draw, drawgame, mouseclicked, keypressed, keytyped, windowresized, drawbutton, drawslider, drawinput, modmenu-draw, handleMouseClick, updateposition, findoptionbygamestatavar

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> passive_income_calc[Passive Income Update] draw --> passive_income_apply[Apply Passive Income Over Time] draw --> auto_clicker_check[Auto-Clicker Interval Check] draw --> money_display[Money Display Text] draw --> button_render[Click Button Rendering] draw --> button_label[Button Label with Value] draw --> menu_check[Menu Open Check] draw --> button_collision[Button Hit Detection] draw --> drawgame[drawGame] drawgame --> button_render drawgame --> button_label drawgame --> menu_check drawgame --> button_collision draw --> mouseclicked[mouseClicked] mouseclicked --> menu_toggle[Menu Toggle on M Key] mouseclicked --> button_collision mouseclicked --> drawgame mouseclicked --> handleMouseClick[handleMouseClick] handleMouseClick --> coordinate_transform[Coordinate Transformation] handleMouseClick --> category_click_check[Category Button Click Detection] handleMouseClick --> options_loop[Options Search Loop] handleMouseClick --> slider_drag_check[Slider Drag Detection] draw --> keypressed[keyPressed] keypressed --> input_backspace[Backspace Handling] keypressed --> input_enter[Enter to Submit Input] draw --> keytyped[keyTyped] keytyped --> numeric_filter[Numeric Input Validation] keytyped --> digit_check[Digit Character Check] draw --> windowresized[windowResized] windowresized --> updateposition[updatePosition] click setup href "#fn-setup" click draw href "#fn-draw" click passive_income_calc href "#sub-passive-income-calc" click passive_income_apply href "#sub-passive-income-apply" click auto_clicker_check href "#sub-auto-clicker-check" click money_display href "#sub-money-display" click button_render href "#sub-button-render" click button_label href "#sub-button-label" click menu_check href "#sub-menu-check" click button_collision href "#sub-button-collision" click drawgame href "#fn-drawgame" click mouseclicked href "#fn-mouseclicked" click menu_toggle href "#sub-menu-toggle" click handleMouseClick href "#fn-handleMouseClick" click coordinate_transform href "#sub-coordinate-transform" click category_click_check href "#sub-category-click-check" click options_loop href "#sub-loop-options" click slider_drag_check href "#sub-slider-drag-check" click keypressed href "#fn-keypressed" click input_backspace href "#sub-input-backspace" click input_enter href "#sub-input-enter" click keytyped href "#fn-keytyped" click numeric_filter href "#sub-numeric-filter" click digit_check href "#sub-digit-check" click windowresized href "#fn-windowresized" click updateposition href "#sub-updateposition"

❓ Frequently Asked Questions

What visual experience does the p5.js sketch titled 'Sketch 2026-03-06 19:24' offer?

The sketch likely features a dynamic interface for a clicker game, showcasing real-time updates of game elements like money and click values.

How can users engage with the clicker game in this sketch?

Users can interact with the game by clicking to earn money, adjusting game settings through a mod menu, and activating features like auto-clickers.

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

The sketch demonstrates the use of state management for game variables and UI elements, along with interactive sliders and toggles for user customization.

Preview

Sketch 2026-03-06 19:24 - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Sketch 2026-03-06 19:24 - Code flow showing preload, setup, draw, drawgame, mouseclicked, keypressed, keytyped, windowresized, drawbutton, drawslider, drawinput, modmenu-draw, handleMouseClick, updateposition, findoptionbygamestatavar
Code Flow Diagram