battles ageist mythical creatures

This is a turn-based battle game where players select a mythical creature and engage in strategic combat against randomly generated enemies. The game features character progression through a shop system, permanent ability upgrades, pet companions that fight alongside the player, and multiple battle sequences with increasing difficulty.

🧪 Try This!

Experiment with the code by making these changes:

  1. Create a new character species — Add a fifth mythical creature (Basilisk, Chimera, or Krakken) with unique stats and ability to both character selection and enemy generation.
  2. Add double gold rewards for consecutive wins — Track consecutive victories and increase gold rewards each time the player wins without losing—reset the multiplier on defeat.
  3. Allow players to purchase health between battles — Add a new consumable that can be bought during the shop screen to instantly heal the player mid-battle.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a full turn-based battle game where you choose between four mythical heroes—Dragon, Unicorn, Griffin, or Phoenix—and battle randomly generated enemies. The game showcases several advanced p5.js techniques including game state management (using a switch statement to control screens), object-oriented programming with classes for characters and items, the draw loop as a game engine, and event handling for both mouse clicks and keyboard input. You also learn how to manage complex game logic like turn order, damage calculation, and inventory systems.

The code is organized into several layers: game state variables at the top control which screen is displayed, three custom classes (Character, Pet, and ShopItem) model the game's entities, setup() initializes the shops and canvas, draw() dispatches to different screen-drawing functions based on gameState, and helper functions handle combat logic, purchasing, and turn management. By studying this sketch you will learn how real games are structured, how to coordinate multiple systems (UI, combat, inventory), and how to create engaging player feedback through battle logs and visual updates.

⚙️ How It Works

  1. When the sketch loads, setup() creates an 800x600 canvas and populates two shop systems: a consumable shop with temporary buffs and an ability shop with permanent upgrades specific to each character species.
  2. The player begins at the START_SCREEN, where four clickable character boxes represent Dragon, Unicorn, Griffin, and Phoenix. Clicking one calls startGame(), which creates a player character with two pet companions and generates a random enemy, then transitions to the BATTLE state.
  3. During BATTLE, the draw loop calls drawBattleScreen() every frame, displaying both combatants side-by-side with their HP bars, stats, and action buttons. Three clickable buttons—Attack, Ability, and Shop—let the player act, but only when currentTurn equals 'player'.
  4. When the player clicks Attack or Ability, playerTurn() executes their action, applies any active temporary boosts from consumables, deals damage or heals, logs the result, then sets currentTurn to 'enemy' to block further input. After a short delay, petAttack() lets each alive pet damage the enemy, then enemyTurn() lets the enemy retaliate.
  5. If the enemy survives, currentTurn returns to 'player' and the cycle repeats. If the enemy is defeated, endBattle(true) rewards 50 gold, heals the player and pets, generates a new enemy, and continues the battle loop; if the player is defeated, gameState transitions to GAME_OVER and displays a restart button.
  6. The SHOP state lets players spend gold on consumable potions and stat boosts that apply during the next battle; the ABILITY_SHOP lets them permanently upgrade their character's ability if they have enough gold and match the species restriction—these permanent upgrades carry across all future battles.

🎓 Concepts You'll Learn

Game state managementObject-oriented classesTurn-based game logicMouse and keyboard input handlingInventory and shop systemsDamage calculation and combat mechanicsEvent-driven architecture

📝 Code Breakdown

setup()

setup() runs exactly once when the sketch loads. Use it to initialize your game world—canvas size, starting variables, and object pools like shops. This is where you prepare everything the game needs before the draw loop begins.

function setup() {
  createCanvas(800, 600); // Standard p5 canvas size
  noSmooth(); // Keep pixel art sharp if using images

  // Initialize shop items (consumables/temporary buffs)
  shopItems.push(new ShopItem("Small Potion", "Restores 20 HP to player.", 10, "heal", 20));
  shopItems.push(new ShopItem("Attack Elixir", "Boosts player attack by 5 for battle.", 15, "attack_boost", 5));
  shopItems.push(new ShopItem("Large Potion", "Restores 50 HP to player.", 25, "heal", 50));
  shopItems.push(new ShopItem("Defense Brew", "Boosts player defense by 3 for battle.", 20, "defense_boost", 3));

  // Initialize ability shop items (permanent abilities)
  // Each ability is specific to a species and replaces the character's current ability
  abilityShopItems.push(new AbilityShopItem("Inferno Roar", "Dragon's Flame: Stronger Fire Breath.", 50, "Inferno Roar", "Dragon"));
  abilityShopItems.push(new AbilityShopItem("Divine Shield", "Unicorn's Blessing: Heals & temp DEF boost.", 60, "Divine Shield", "Unicorn"));
  abilityShopItems.push(new AbilityShopItem("Sky Razor", "Griffin's Fury: More defense ignored, higher crit chance.", 70, "Sky Razor", "Griffin"));
  abilityShopItems.push(new AbilityShopItem("Solar Flare", "Phoenix's Fire: AoE damage & minor heal.", 80, "Solar Flare", "Phoenix"));
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

function-call Canvas Setup createCanvas(800, 600);

Creates an 800x600 pixel canvas where all game drawing happens

loop Consumable Shop Initialization shopItems.push(new ShopItem("Small Potion", "Restores 20 HP to player.", 10, "heal", 20));

Populates the consumable shop with four buyable items: healing potions and stat-boost elixirs

loop Ability Shop Initialization abilityShopItems.push(new AbilityShopItem("Inferno Roar", "Dragon's Flame: Stronger Fire Breath.", 50, "Inferno Roar", "Dragon"));

Adds four permanent ability upgrades to the shop, one for each character species

createCanvas(800, 600);
Creates the game window—all p5.js drawing happens on this 800×600 pixel canvas
noSmooth();
Disables anti-aliasing so pixel art and shapes appear sharp and crisp instead of blurry
shopItems.push(new ShopItem("Small Potion", "Restores 20 HP to player.", 10, "heal", 20));
Creates a new consumable item and adds it to the shop array—players can buy potions to restore health during battle
abilityShopItems.push(new AbilityShopItem("Inferno Roar", "Dragon's Flame: Stronger Fire Breath.", 50, "Inferno Roar", "Dragon"));
Creates a permanent ability upgrade specific to the Dragon species and adds it to the ability shop—once bought, it replaces the character's current ability permanently

draw()

draw() runs 60 times per second, acting as the game engine. Each frame, it clears the canvas and dispatches to the correct screen-drawing function. This switch-statement pattern is how game engines organize multiple screens—each case is an entirely different visual state.

🔬 This switch statement is the game's brain—it decides which screen to draw. What happens if you temporarily comment out the 'case BATTLE:' block so the game skips the battle screen entirely?

  switch (gameState) {
    case 'START_SCREEN':
      drawStartScreen();
      break;
    case 'BATTLE':
      drawBattleScreen();
      break;
function draw() {
  background(28, 32, 38); // Dark background

  switch (gameState) {
    case 'START_SCREEN':
      drawStartScreen();
      break;
    case 'BATTLE':
      drawBattleScreen();
      break;
    case 'SHOP':
      drawShopScreen();
      break;
    case 'ABILITY_SHOP': // New state for ability shop
      drawAbilityShopScreen();
      break;
    case 'GAME_OVER':
      drawGameOverScreen();
      break;
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

function-call Canvas Clear background(28, 32, 38);

Erases the previous frame with a dark gray color, preventing motion trails

switch-case Game State Dispatcher switch (gameState) {

Routes to the correct screen-drawing function based on the current game state

background(28, 32, 38);
Fills the entire canvas with dark gray (RGB 28, 32, 38) each frame—this clears old drawings so animations appear smooth
switch (gameState) {
Checks the current gameState variable and routes to the appropriate drawing function—the game's state machine
case 'START_SCREEN':
If gameState is 'START_SCREEN', draw the character selection menu
case 'BATTLE':
If gameState is 'BATTLE', draw the battle screen with characters, enemies, and action buttons
case 'SHOP':
If gameState is 'SHOP', draw the consumable shop interface
case 'ABILITY_SHOP':
If gameState is 'ABILITY_SHOP', draw the permanent ability upgrade shop
case 'GAME_OVER':
If gameState is 'GAME_OVER', draw the victory or defeat screen with a restart button

drawStartScreen()

This function draws the game's opening screen where the player chooses their hero. It uses simple rect() and text() calls to build a clean character selection menu. The buttonData object stores each button's position so mouseClicked() can detect which character was chosen. This pattern—storing button positions during draw and checking them in input handlers—is essential for p5.js UI design.

🔬 The Dragon is positioned at startX, startY. What happens if you change it to startX + 200 so all four characters shift right? How would you need to adjust the other characters to keep them spaced evenly?

  // Dragon
  fill('#ff5722');
  rect(startX, startY, charWidth, charHeight);
  fill(255);
  textSize(20);
  text("Dragon", startX + charWidth / 2, startY + charHeight / 2 - 20);
function drawStartScreen() {
  textAlign(CENTER, CENTER);
  fill(255);
  textSize(32);
  text("Choose Your Mythical Hero!", width / 2, height / 4);

  let charWidth = 150;
  let charHeight = 150;
  let gap = 30; // Reduced gap for 4 characters
  let startX = (width - (charWidth * 4 + gap * 3)) / 2; // Adjusted startX for 4 characters
  let startY = height / 2 - charHeight / 2;

  // Dragon
  fill('#ff5722');
  rect(startX, startY, charWidth, charHeight);
  fill(255);
  textSize(20);
  text("Dragon", startX + charWidth / 2, startY + charHeight / 2 - 20);
  textSize(14);
  text("High ATK, Low DEF", startX + charWidth / 2, startY + charHeight / 2 + 10);
  // Store button data
  buttonData.dragon = { x: startX, y: startY, w: charWidth, h: charHeight, species: "Dragon" };

  // Unicorn
  fill('#61dafb');
  rect(startX + charWidth + gap, startY, charWidth, charHeight);
  fill(255);
  textSize(20);
  text("Unicorn", startX + charWidth + gap + charWidth / 2, startY + charHeight / 2 - 20);
  textSize(14);
  text("High HP, Healing Ability", startX + charWidth + gap + charWidth / 2, startY + charHeight / 2 + 10);
  // Store button data
  buttonData.unicorn = { x: startX + charWidth + gap, y: startY, w: charWidth, h: charHeight, species: "Unicorn" };

  // Griffin
  fill('#ffeb3b');
  rect(startX + (charWidth + gap) * 2, startY, charWidth, charHeight);
  fill(28, 32, 38); // Dark text for yellow background
  textSize(20);
  text("Griffin", startX + (charWidth + gap) * 2 + charWidth / 2, startY + charHeight / 2 - 20);
  textSize(14);
  text("Balanced, Ignores DEF", startX + (charWidth + gap) * 2 + charWidth / 2, startY + charHeight / 2 + 10);
  // Store button data
  buttonData.griffin = { x: startX + (charWidth + gap) * 2, y: startY, w: charWidth, h: charHeight, species: "Griffin" };

  // Phoenix (New Character)
  fill('#FF8C00'); // Orange
  rect(startX + (charWidth + gap) * 3, startY, charWidth, charHeight);
  fill(255);
  textSize(20);
  text("Phoenix", startX + (charWidth + gap) * 3 + charWidth / 2, startY + charHeight / 2 - 20);
  textSize(14);
  text("Rebirth Flame Ability", startX + (charWidth + gap) * 3 + charWidth / 2, startY + charHeight / 2 + 10);
  // Store button data
  buttonData.phoenix = { x: startX + (charWidth + gap) * 3, y: startY, w: charWidth, h: charHeight, species: "Phoenix" };
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

function-call Title Rendering text("Choose Your Mythical Hero!", width / 2, height / 4);

Displays the main title at the top center of the screen

calculation Character Box Positioning let startX = (width - (charWidth * 4 + gap * 3)) / 2;

Centers four character boxes horizontally on the canvas using math to distribute them evenly

shape-with-text Dragon Character Box fill('#ff5722'); rect(startX, startY, charWidth, charHeight);

Draws the Dragon selection button in red-orange and stores its position in buttonData for click detection

shape-with-text Unicorn Character Box fill('#61dafb'); rect(startX + charWidth + gap, startY, charWidth, charHeight);

Draws the Unicorn selection button in cyan and stores its position for click detection

shape-with-text Griffin Character Box fill('#ffeb3b'); rect(startX + (charWidth + gap) * 2, startY, charWidth, charHeight);

Draws the Griffin selection button in yellow and stores its position for click detection

shape-with-text Phoenix Character Box fill('#FF8C00'); rect(startX + (charWidth + gap) * 3, startY, charWidth, charHeight);

Draws the Phoenix selection button in orange and stores its position for click detection

textAlign(CENTER, CENTER);
Centers all text both horizontally and vertically—useful for centering text in boxes
text("Choose Your Mythical Hero!", width / 2, height / 4);
Draws the title at the center of the canvas (width/2 for x, height/4 for y)
let startX = (width - (charWidth * 4 + gap * 3)) / 2;
Calculates the left edge of the character selection grid so the four boxes are centered on the canvas
fill('#ff5722');
Sets the fill color to red-orange (hex code) for the Dragon box
rect(startX, startY, charWidth, charHeight);
Draws a rectangle for the Dragon button at the calculated position
buttonData.dragon = { x: startX, y: startY, w: charWidth, h: charHeight, species: "Dragon" };
Stores the Dragon button's position and size in buttonData so mouseClicked() can detect when it is clicked

drawBattleScreen()

drawBattleScreen() is the main battle interface—it displays both combatants, the player's inventory (pets and gold), and the battle log which communicates all game events to the player. The turn-based system is enforced visually by graying out action buttons when it is not the player's turn. This screen stays on-screen for the entire battle, with display() calls updating each character's HP bar every frame.

🔬 This loop spaces pets horizontally using petGap (60 pixels). What happens if you change petGap to 100? To 30? How does the spacing change? Why might you want to adjust this if you had 5 pets instead of 2?

  // Display player pets
  let petStartX = 100; // Pets are always next to player
  let petY = 380;
  let petGap = 60;
  for (let i = 0; i < playerCharacter.pets.length; i++) {
    playerCharacter.pets[i].display(petStartX + i * petGap, petY, i);
  }
function drawBattleScreen() {
  textAlign(CENTER, CENTER);
  fill(255);
  textSize(24);
  text("BATTLE!", width / 2, 50);

  // Display player and enemy
  playerCharacter.display(100, 150, true);
  enemyCharacter.display(width - 250, 150, false);

  // Display player pets
  let petStartX = 100; // Pets are always next to player
  let petY = 380;
  let petGap = 60;
  for (let i = 0; i < playerCharacter.pets.length; i++) {
    playerCharacter.pets[i].display(petStartX + i * petGap, petY, i);
  }

  // Display player gold
  textAlign(LEFT, TOP);
  textSize(16);
  text(`Gold: ${playerGold}`, 20, 20);

  // Ability Shop Button (New - up top)
  let abilityShopBtnX = 20;
  let abilityShopBtnY = 50;
  let abilityShopBtnW = 120;
  let abilityShopBtnH = 30;

  fill('#FFD700'); // Gold color for ability shop
  rect(abilityShopBtnX, abilityShopBtnY, abilityShopBtnW, abilityShopBtnH, 5);
  fill(28, 32, 38);
  textSize(14);
  textAlign(CENTER, CENTER);
  text("Ability Shop", abilityShopBtnX + abilityShopBtnW / 2, abilityShopBtnY + abilityShopBtnH / 2);
  buttonData.abilityShop = { x: abilityShopBtnX, y: abilityShopBtnY, w: abilityShopBtnW, h: abilityShopBtnH, action: "abilityShop" };

  // Display current turn
  textAlign(RIGHT, TOP);
  textSize(18);
  fill(255);
  text(`Turn: ${currentTurn.toUpperCase()}`, width - 20, 20);

  // Battle Log
  fill(35);
  rect(width / 2 - 200, height - 180, 400, 120);
  fill(255);
  textSize(14);
  textAlign(LEFT, TOP);
  let logY = height - 170;
  for (let i = 0; i < battleLog.length; i++) {
    text(battleLog[i], width / 2 - 190, logY + i * 20);
  }

  // Action Buttons
  textAlign(CENTER, CENTER);
  let btnWidth = 100;
  let btnHeight = 40;
  let btnY = height - 50;
  let btnGap = 20;

  // Attack Button
  let atkX = width / 2 - btnWidth * 1.5 - btnGap;
  fill(currentTurn === 'player' ? '#ff5722' : '#888'); // Gray out if not player's turn
  rect(atkX, btnY, btnWidth, btnHeight, 5);
  fill(255);
  textSize(16);
  text("Attack", atkX + btnWidth / 2, btnY + btnHeight / 2);
  buttonData.attack = { x: atkX, y: btnY, w: btnWidth, h: btnHeight, action: "attack" };

  // Ability Button
  let abiX = width / 2 - btnWidth / 2;
  fill(currentTurn === 'player' ? '#61dafb' : '#888'); // Gray out if not player's turn
  rect(abiX, btnY, btnWidth, btnHeight, 5);
  fill(28, 32, 38);
  textSize(16);
  text("Ability", abiX + btnWidth / 2, btnY + btnHeight / 2);
  textSize(12);
  text("(Spacebar)", abiX + btnWidth / 2, btnY + btnHeight / 2 + 15); // Hint for spacebar
  buttonData.ability = { x: abiX, y: btnY, w: btnWidth, h: btnHeight, action: "ability" };

  // Consumable Shop Button
  let shopX = width / 2 + btnWidth / 2 + btnGap;
  fill(currentTurn === 'player' ? '#4CAF50' : '#888'); // Gray out if not player's turn
  rect(shopX, btnY, btnWidth, btnHeight, 5);
  fill(255);
  textSize(16);
  text("Shop", shopX + btnWidth / 2, btnY + btnHeight / 2);
  buttonData.shop = { x: shopX, y: btnY, w: btnWidth, h: btnHeight, action: "shop" };
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

function-call Character Rendering playerCharacter.display(100, 150, true); enemyCharacter.display(width - 250, 150, false);

Calls the display() method on both characters to draw their sprites, HP bars, and stats side-by-side

for-loop Pet Display Loop for (let i = 0; i < playerCharacter.pets.length; i++) {

Iterates through each pet and displays them below the player character

shape-with-loop Battle Log Rendering for (let i = 0; i < battleLog.length; i++) {

Draws each battle message from the battleLog array in a text box on the screen

shape-with-conditional Action Buttons with Turn Control fill(currentTurn === 'player' ? '#ff5722' : '#888');

Creates three action buttons (Attack, Ability, Shop) and grays them out when it is not the player's turn

playerCharacter.display(100, 150, true);
Calls the Character's display() method to draw the player character at position (100, 150) with their HP bar, name, and stats
enemyCharacter.display(width - 250, 150, false);
Draws the enemy character on the right side of the canvas at position (width-250, 150), mirroring the player's layout
for (let i = 0; i < playerCharacter.pets.length; i++) {
Loops through each of the player's pets and displays them below the player character
text(`Gold: ${playerGold}`, 20, 20);
Displays the player's current gold amount in the top-left corner using a template string
text(`Turn: ${currentTurn.toUpperCase()}`, width - 20, 20);
Shows whose turn it is ('PLAYER' or 'ENEMY') in the top-right corner, helping the player understand game state
for (let i = 0; i < battleLog.length; i++) {
Loops through the battleLog array and displays each message, creating a running history of actions in the match
fill(currentTurn === 'player' ? '#ff5722' : '#888');
Uses a ternary operator to check if it's the player's turn—if yes, the button is bright red; if no, it's gray to indicate it's disabled

startGame(species)

startGame() initializes the game world when a player chooses a character. It uses a switch statement to route to the correct character creation, assigns starting pets, generates the first enemy, and transitions the game state to BATTLE. This is the bridge between the menu and the actual game loop.

function startGame(species) {
  switch (species) {
    case "Dragon":
      playerCharacter = new Character("Ignis", "Dragon", 100, 30, 10, "Fire Breath");
      break;
    case "Unicorn":
      playerCharacter = new Character("Celeste", "Unicorn", 120, 20, 15, "Healing Light");
      break;
    case "Griffin":
      playerCharacter = new Character("Aeron", "Griffin", 110, 25, 20, "Dive Bomb");
      break;
    case "Phoenix": // New Character
      playerCharacter = new Character("Ember", "Phoenix", 100, 25, 15, "Rebirth Flame");
      break;
  }
  
  // Give the player two pets
  playerCharacter.pets.push(new Pet("Sparky", "Imp", 40, 10, 5));
  playerCharacter.pets.push(new Pet("Pipsqueak", "Fairy", 30, 8, 8));

  generateEnemy();
  battleLog = []; // Clear log for new game
  playerGold = 100; // Reset gold
  gameState = 'BATTLE';
  currentTurn = 'player'; // Explicitly set turn for new game
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

switch-case Character Selection Logic switch (species) {

Routes to the correct character creation based on which species the player selected on the start screen

function-call Pet Assignment playerCharacter.pets.push(new Pet("Sparky", "Imp", 40, 10, 5));

Creates two companion pets that will fight alongside the player during all battles

function-call Enemy Spawning generateEnemy();

Creates a random enemy from one of the four species

assignment Game Reset gameState = 'BATTLE';

Transitions the game to the BATTLE state so draw() will call drawBattleScreen()

switch (species) {
Checks the species parameter (passed from the START_SCREEN button click) to decide which character to create
case "Dragon":
If the player chose Dragon, this case executes
playerCharacter = new Character("Ignis", "Dragon", 100, 30, 10, "Fire Breath");
Creates a new Character object with the name 'Ignis', species Dragon, 100 max HP, 30 attack, 10 defense, and Fire Breath ability
playerCharacter.pets.push(new Pet("Sparky", "Imp", 40, 10, 5));
Adds a pet named Sparky to the player's pets array—all players get the same two pets regardless of species
generateEnemy();
Calls generateEnemy() to create a random enemy from the four available species
playerGold = 100;
Resets the player's gold to 100 at the start of each new game—a fresh economy with each playthrough
gameState = 'BATTLE';
Changes gameState from 'START_SCREEN' to 'BATTLE', so draw() will now call drawBattleScreen()

generateEnemy()

generateEnemy() creates a random opponent by picking a species at random, then creating that character type. This ensures every battle feels fresh and unpredictable. Notice the enemy stats are slightly weaker than player-controlled versions—game balance intentionally favors the player slightly so they can progress through battles.

function generateEnemy() {
  let enemySpecies = random(["Dragon", "Unicorn", "Griffin", "Phoenix"]); // Phoenix can also be an enemy
  switch (enemySpecies) {
    case "Dragon":
      enemyCharacter = new Character("Drakon", "Dragon", 90, 25, 8, "Fire Breath");
      break;
    case "Unicorn":
      enemyCharacter = new Character("Luna", "Unicorn", 110, 18, 12, "Healing Light");
      break;
    case "Griffin":
      enemyCharacter = new Character("Gryphus", "Griffin", 100, 22, 18, "Dive Bomb");
      break;
    case "Phoenix": // Phoenix enemy
      enemyCharacter = new Character("Blaze", "Phoenix", 95, 23, 13, "Rebirth Flame");
      break;
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

function-call Random Species Selection let enemySpecies = random(["Dragon", "Unicorn", "Griffin", "Phoenix"]);

Uses p5.js random() to pick one of four species with equal probability, ensuring varied enemy encounters

switch-case Enemy Character Creation switch (enemySpecies) {

Routes to the correct Character creation based on the randomly selected species

let enemySpecies = random(["Dragon", "Unicorn", "Griffin", "Phoenix"]);
p5.js's random() function picks one item from the array at random—each species has a 25% chance of being selected
switch (enemySpecies) {
Routes to the correct enemy creation based on which species was randomly selected
case "Dragon":
If the enemy is a Dragon, this case executes
enemyCharacter = new Character("Drakon", "Dragon", 90, 25, 8, "Fire Breath");
Creates a Dragon enemy with slightly lower HP than the player's Dragon but same attack—balanced for competitive battles

playerTurn(action)

playerTurn() is the core of combat—it executes the player's chosen action, applies any temporary boosts, logs the result, and transitions control to the enemy. The temporary boost system is clever: boosts are only applied once and then reset, preventing infinite stacking. This demonstrates how game balance is enforced through careful state management.

🔬 This loop applies one-time boosts from the shop. What would happen if you removed the line 'item.effectAmount = 0'? The boost would apply every single turn—would that be overpowered, or a fun feature?

  // Apply temporary boosts from shop items
  shopItems.forEach(item => {
    if (item.type === "attack_boost" && item.effectAmount > 0) {
      tempAttackBoost = item.effectAmount;
      playerCharacter.attack += tempAttackBoost;
      item.effectAmount = 0; // Reset elixir after use
      message += `Player's attack is boosted by ${tempAttackBoost}! `;
    }
function playerTurn(action) {
  // Check if it's player's turn
  if (currentTurn !== 'player' || !playerCharacter.isAlive() || !enemyCharacter.isAlive()) return;

  // Immediately set turn to enemy to block further player input
  currentTurn = 'enemy';

  let message = "";
  let tempAttackBoost = 0; // For attack elixir effect
  let tempDefenseBoost = 0; // For defense brew effect

  // Apply temporary boosts from shop items
  shopItems.forEach(item => {
    if (item.type === "attack_boost" && item.effectAmount > 0) {
      tempAttackBoost = item.effectAmount;
      playerCharacter.attack += tempAttackBoost;
      item.effectAmount = 0; // Reset elixir after use
      message += `Player's attack is boosted by ${tempAttackBoost}! `;
    }
    if (item.type === "defense_boost" && item.effectAmount > 0) {
      tempDefenseBoost = item.effectAmount;
      playerCharacter.defense += tempDefenseBoost;
      item.effectAmount = 0; // Reset elixir after use
      message += `Player's defense is boosted by ${tempDefenseBoost}! `;
    }
  });

  // Apply temporary boosts from Divine Shield ability (if active)
  if (playerCharacter.ability === "Divine Shield") {
    tempDefenseBoost += 5; // Add to existing defense boost
    playerCharacter.defense += 5;
    message += `Player's defense is boosted by Divine Shield! `;
  }

  if (action === 'attack') {
    let damage = playerCharacter.dealDamage(enemyCharacter);
    message += `${playerCharacter.name} attacks ${enemyCharacter.name}, dealing ${damage} damage!`;
  } else if (action === 'ability') {
    message += playerCharacter.useAbility(enemyCharacter);
  }

  battleLog.push(message);

  // Remove temporary boosts after player's turn
  if (tempAttackBoost > 0) playerCharacter.attack -= tempAttackBoost;
  if (tempDefenseBoost > 0) playerCharacter.defense -= tempDefenseBoost;

  if (!enemyCharacter.isAlive()) {
    endBattle(true);
  } else {
    // Pets attack after player, before enemy
    setTimeout(petAttack, 500); // Small delay for readability
  }
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

conditional Turn Check and Guard if (currentTurn !== 'player' || !playerCharacter.isAlive() || !enemyCharacter.isAlive()) return;

Prevents the player from acting out of turn or if either combatant is dead

for-each-loop Temporary Boost Application shopItems.forEach(item => {

Loops through all shop items to find and apply any purchased attack or defense boosts

conditional Action Execution if (action === 'attack') {

Routes to either a basic attack or ability use based on the action parameter

conditional Temporary Boost Removal if (tempAttackBoost > 0) playerCharacter.attack -= tempAttackBoost;

Removes temporary stat boosts after they have been applied for this turn

if (currentTurn !== 'player' || !playerCharacter.isAlive() || !enemyCharacter.isAlive()) return;
Safety check—exits the function immediately if it is not the player's turn, or if either character is already dead
currentTurn = 'enemy';
Immediately sets the turn to enemy to prevent the player from acting again before the enemy has responded
shopItems.forEach(item => {
Loops through all consumable shop items looking for any that grant temporary stat boosts
if (item.type === "attack_boost" && item.effectAmount > 0) {
If an item is an attack boost and its effectAmount is greater than 0 (meaning it has been purchased but not used), apply it
playerCharacter.attack += tempAttackBoost;
Temporarily increases the player's attack stat for this turn's damage calculation
item.effectAmount = 0;
Resets the item's effectAmount to 0 so the boost will not apply again next turn
if (action === 'attack') {
If the player chose to attack, deal normal damage to the enemy
} else if (action === 'ability') {
If the player chose to use their ability, call useAbility() instead, which has special effects per species
battleLog.push(message);
Adds the action description to battleLog so the player can see what happened
if (tempAttackBoost > 0) playerCharacter.attack -= tempAttackBoost;
Removes the temporary attack boost after this turn so the stat returns to normal
setTimeout(petAttack, 500);
Waits 500 milliseconds (half a second), then calls petAttack() so the UI has time to update before pets attack

petAttack()

petAttack() gives the player's pet companions a turn to deal damage between the player's action and the enemy's retaliation. This creates a three-phase turn system: player → pets → enemy. Pets add strategic depth—they share the player's goal but are independent combatants. This demonstrates how game design uses multiple entities to create layered mechanics.

🔬 Pets attack in sequence because of the forEach loop. What happens if you add `let multiplier = 1.5;` before the loop and then change `let damage = pet.dealDamage(enemyCharacter);` to `let damage = pet.dealDamage(enemyCharacter) * multiplier;` to make pets deal 50% extra damage?

  playerCharacter.pets.forEach(pet => {
    if (pet.isAlive()) {
      let damage = pet.dealDamage(enemyCharacter);
      battleLog.push(`${pet.name} (pet) attacks ${enemyCharacter.name}, dealing ${damage} damage!`);
    }
  });
function petAttack() {
  if (!playerCharacter.isAlive() || !enemyCharacter.isAlive()) return;

  playerCharacter.pets.forEach(pet => {
    if (pet.isAlive()) {
      let damage = pet.dealDamage(enemyCharacter);
      battleLog.push(`${pet.name} (pet) attacks ${enemyCharacter.name}, dealing ${damage} damage!`);
    }
  });

  if (!enemyCharacter.isAlive()) {
    endBattle(true);
  } else {
    // Enemy turn after pets
    setTimeout(enemyTurn, 1000);
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

for-each-loop Pet Attack Loop playerCharacter.pets.forEach(pet => {

Iterates through each alive pet and has them attack the enemy in sequence

conditional Enemy Defeat Detection if (!enemyCharacter.isAlive()) {

Checks if the enemy was killed by pets; if so, the battle ends in the player's favor

if (!playerCharacter.isAlive() || !enemyCharacter.isAlive()) return;
Early exit if either character is already dead—prevents unnecessary pet attacks
playerCharacter.pets.forEach(pet => {
Loops through each pet in the player's pets array
if (pet.isAlive()) {
Only alive pets can attack—dead pets skip their turn
let damage = pet.dealDamage(enemyCharacter);
Calls the pet's dealDamage() method to calculate and apply damage to the enemy
battleLog.push(`${pet.name} (pet) attacks ${enemyCharacter.name}, dealing ${damage} damage!`);
Logs the pet's attack action so the player knows what happened
if (!enemyCharacter.isAlive()) {
Checks if the enemy's HP dropped to 0 or below from pet attacks
endBattle(true);
If the enemy is dead, the player wins—no enemy turn needed
setTimeout(enemyTurn, 1000);
If the enemy survived, wait 1 second then call enemyTurn() so the enemy can retaliate

enemyTurn()

enemyTurn() is the simplest turn function because the enemy uses only basic attacks—no abilities, no items, no complex strategy. This keeps the game simple enough for beginners to understand but still challenging because the enemy's damage scales with their attack and defense stats. A more advanced version could add enemy ability use or decision-making.

function enemyTurn() {
  if (!playerCharacter.isAlive() || !enemyCharacter.isAlive()) return;

  let message = "";
  // Enemy always attacks the player character
  let damage = enemyCharacter.dealDamage(playerCharacter);
  message += `${enemyCharacter.name} attacks ${playerCharacter.name}, dealing ${damage} damage!`;

  battleLog.push(message);

  if (!playerCharacter.isAlive()) {
    endBattle(false);
  } else {
    // Re-enable player's turn
    currentTurn = 'player';
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

function-call Enemy Attack let damage = enemyCharacter.dealDamage(playerCharacter);

Calculates and applies damage from the enemy to the player character

conditional Player Defeat Detection if (!playerCharacter.isAlive()) {

Checks if the player was killed by this attack; if so, the battle is lost

assignment Turn Reset currentTurn = 'player';

Returns control to the player for the next combat round

if (!playerCharacter.isAlive() || !enemyCharacter.isAlive()) return;
Early exit if either character is dead—no point in the enemy attacking a dead player or player attacking a dead enemy
let damage = enemyCharacter.dealDamage(playerCharacter);
The enemy uses their dealDamage() method to calculate their attack power minus the player's defense, then subtracts that from the player's HP
battleLog.push(message);
Logs the enemy's action to the battle log so the player sees what happened
if (!playerCharacter.isAlive()) {
Checks if the player's HP dropped to 0 or below
endBattle(false);
If the player is dead, the player loses the battle
currentTurn = 'player';
If the player survived, set the turn back to 'player' so the UI updates and they can take their next action

endBattle(playerWon)

endBattle() manages the victory/defeat state and determines what happens next. On victory, it uses setTimeout() to delay the next battle, giving the player time to see the victory message and feel satisfaction. The healing between battles is deliberate design—it encourages progression without making the game trivial. Notice pets are fully healed but the player is only partially healed, creating asymmetry.

function endBattle(playerWon) {
  if (playerWon) {
    playerGold += 50; // Reward gold for winning
    battleLog.push(`${playerCharacter.name} defeated ${enemyCharacter.name}! You gained 50 gold!`);
    battleLog.push("Prepare for the next battle!");
    // Short delay before starting next battle or showing shop
    setTimeout(() => {
      // Heal player slightly between battles
      playerCharacter.heal(playerCharacter.maxHp * 0.2); // Heal 20%
      // Revive and heal pets to full HP for the next battle
      playerCharacter.pets.forEach(pet => {
        pet.heal(pet.maxHp);
      });
      generateEnemy();
      battleLog = []; // Clear log for new battle
      gameState = 'BATTLE'; // Start next battle
      currentTurn = 'player'; // Reset turn for new battle
    }, 2000);
  } else {
    battleLog.push(`${enemyCharacter.name} defeated ${playerCharacter.name}!`);
    gameState = 'GAME_OVER';
  }
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

conditional-branch Player Victory Handling if (playerWon) {

Awards gold, heals the player and pets, generates the next enemy, and continues the battle loop

conditional-branch Player Defeat Handling } else {

Transitions the game state to GAME_OVER screen

if (playerWon) {
Checks if the player won (true) or lost (false) this battle
playerGold += 50;
Adds 50 gold to the player's total as a reward for winning
battleLog.push(`${playerCharacter.name} defeated ${enemyCharacter.name}! You gained 50 gold!`);
Announces the victory and gold reward in the battle log
setTimeout(() => {
Waits 2 seconds before the next battle starts, giving the player time to read the victory message
playerCharacter.heal(playerCharacter.maxHp * 0.2);
Heals the player for 20% of their max HP between battles—encourages continued play without full healing
playerCharacter.pets.forEach(pet => {
Loops through all pets and heals them back to full HP for the next battle
generateEnemy();
Creates a new random enemy for the next battle
battleLog = [];
Clears the battle log so the next battle starts with an empty message history
gameState = 'BATTLE';
Keeps gameState as 'BATTLE' so draw() continues calling drawBattleScreen()
} else {
If playerWon is false, the player lost this battle
gameState = 'GAME_OVER';
Changes gameState to 'GAME_OVER' so draw() calls drawGameOverScreen()

buyShopItem(index)

buyShopItem() handles the consumable shop transaction. Healing items apply immediately, while stat boosts are stored in the item object itself and applied at the start of the next turn. This deferred application prevents stacking and keeps balance in check. The gold cost is deducted whether the item is instant or deferred—good game design that feels fair.

function buyShopItem(index) {
  let item = shopItems[index];
  if (playerGold >= item.cost) {
    playerGold -= item.cost;
    battleLog.push(`You bought ${item.name} for ${item.cost} gold.`);
    switch (item.type) {
      case "heal":
        let healedAmount = playerCharacter.heal(item.effectAmount);
        battleLog.push(`${playerCharacter.name} healed ${healedAmount} HP.`);
        break;
      case "attack_boost":
        battleLog.push(`${item.name} will boost your attack in the next battle.`);
        // The boost is applied temporarily at the start of playerTurn()
        // The item's effectAmount is kept in the item itself for tracking
        break;
      case "defense_boost":
        battleLog.push(`${item.name} will boost your defense in the next battle.`);
        // The boost is applied temporarily at the start of playerTurn()
        // The item's effectAmount is kept in the item itself for tracking
        break;
    }
  } else {
    battleLog.push("Not enough gold to buy that!");
  }
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

conditional Gold Validation if (playerGold >= item.cost) {

Checks if the player has enough gold to afford the item before allowing the purchase

switch-case Item Effect Application switch (item.type) {

Routes to the correct effect handler based on the item type (heal, attack_boost, or defense_boost)

let item = shopItems[index];
Retrieves the shop item at the given index from the shopItems array
if (playerGold >= item.cost) {
Checks if the player has enough gold to afford this item
playerGold -= item.cost;
Subtracts the item's cost from the player's gold total
battleLog.push(`You bought ${item.name} for ${item.cost} gold.`);
Logs the purchase so the player can see what they just bought
switch (item.type) {
Routes to the correct effect handler: healing items apply immediately, stat boosts are stored for the next turn
case "heal":
If the item is a healing potion, restore HP immediately
let healedAmount = playerCharacter.heal(item.effectAmount);
Calls the player's heal() method with the item's effectAmount, which caps healing at max HP
case "attack_boost":
If the item is an attack elixir, store it for use in the next battle
} else {
If the player doesn't have enough gold
battleLog.push("Not enough gold to buy that!");
Log a failure message so the player knows why their purchase was blocked

buyAbilityShopItem(index)

buyAbilityShopItem() implements permanent upgrades with three validation layers: species restriction, duplicate prevention, and gold check. This careful validation prevents exploits and maintains game balance. Unlike consumables that apply once, abilities persist for the entire game, making them a major progression mechanic. The species restriction creates character uniqueness—each hero upgrades differently.

function buyAbilityShopItem(index) {
  let item = abilityShopItems[index];
  // Check species restriction and if player already has this ability
  if (playerCharacter.species !== item.speciesRestriction) {
    battleLog.push(`This ability is only for ${item.speciesRestriction}s.`);
    return;
  }
  if (playerCharacter.ability === item.abilityName) {
    battleLog.push(`You already have ${item.name}!`);
    return;
  }
  if (playerGold >= item.cost) {
    playerGold -= item.cost;
    playerCharacter.ability = item.abilityName; // Update player's ability
    battleLog.push(`You bought ${item.name} for ${item.cost} gold! Your ability is now ${playerCharacter.ability}.`);
    // Optional: Remove the bought ability from the shop if it's a unique upgrade/replacement
    // abilityShopItems.splice(index, 1);
  } else {
    battleLog.push("Not enough gold to buy that ability!");
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

conditional Species Restriction Validation if (playerCharacter.species !== item.speciesRestriction) {

Prevents purchasing abilities that are restricted to a different species

conditional Duplicate Purchase Prevention if (playerCharacter.ability === item.abilityName) {

Prevents the player from buying an ability they already have

conditional Gold Validation if (playerGold >= item.cost) {

Checks if the player has enough gold before completing the purchase

let item = abilityShopItems[index];
Retrieves the ability shop item at the given index
if (playerCharacter.species !== item.speciesRestriction) {
Checks if the player's species matches the ability's species restriction—Dragon abilities only work for Dragons, etc.
battleLog.push(`This ability is only for ${item.speciesRestriction}s.`);
If the species doesn't match, log a rejection message and exit early
if (playerCharacter.ability === item.abilityName) {
Checks if the player already owns this exact ability
battleLog.push(`You already have ${item.name}!`);
If they already have it, reject the duplicate purchase
if (playerGold >= item.cost) {
Final check: does the player have enough gold?
playerCharacter.ability = item.abilityName;
Permanently updates the player's ability to the new one—this persists for all future battles
battleLog.push(`You bought ${item.name} for ${item.cost} gold! Your ability is now ${playerCharacter.ability}.`);
Confirms the upgrade in the battle log with the new ability name

mouseClicked()

mouseClicked() is p5.js's built-in input handler that fires every time the player clicks. This function uses game state to control which buttons are active—each screen has its own click handlers. The button rectangles are stored during draw() in the buttonData object, and mouseClicked() checks if the click was inside any stored rectangle. This is the core UI pattern for p5.js games.

function mouseClicked() {
  if (gameState === 'START_SCREEN') {
    for (let key in buttonData) {
      let btn = buttonData[key];
      if (mouseX > btn.x && mouseX < btn.x + btn.w && mouseY > btn.y && mouseY < btn.y + btn.h) {
        startGame(btn.species);
        break;
      }
    }
  } else if (gameState === 'BATTLE') {
    // Ability Shop button is always clickable, regardless of turn
    let abilityShopBtn = buttonData.abilityShop;
    if (mouseX > abilityShopBtn.x && mouseX < abilityShopBtn.x + abilityShopBtn.w && mouseY > abilityShopBtn.y && mouseY < abilityShopBtn.y + abilityShopBtn.h) {
      gameState = 'ABILITY_SHOP';
      return; // Exit to prevent other battle actions
    }

    // Only allow player actions if it's their turn
    if (currentTurn !== 'player') return;

    for (let key in buttonData) {
      let btn = buttonData[key];
      // Exclude abilityShop button from turn-based actions
      if (btn.action === "abilityShop") continue;

      if (mouseX > btn.x && mouseX < btn.x + btn.w && mouseY > btn.y && mouseY < btn.y + btn.h) {
        if (btn.action === "shop") {
          gameState = 'SHOP';
        } else if (btn.action === "attack" || btn.action === "ability") {
          playerTurn(btn.action);
        }
        break;
      }
    }
  } else if (gameState === 'SHOP') {
    // No turn restriction in consumable shop
    for (let i = 0; i < shopItems.length; i++) {
      let btn = shopItems[i].button;
      if (mouseX > btn.x && mouseX < btn.x + btn.w && mouseY > btn.y && mouseY < btn.y + btn.h) {
        buyShopItem(btn.index);
        break;
      }
    }
    // Check "Back to Battle" button for consumable shop
    let backBtnConsumable = buttonData.backToBattleConsumable;
    if (mouseX > backBtnConsumable.x && mouseX < backBtnConsumable.x + backBtnConsumable.w && mouseY > backBtnConsumable.y && mouseY < backBtnConsumable.y + backBtnConsumable.h) {
      gameState = 'BATTLE';
    }
  } else if (gameState === 'ABILITY_SHOP') { // Handle Ability Shop clicks
    for (let i = 0; i < abilityShopItems.length; i++) {
      let btn = abilityShopItems[i].button;
      if (mouseX > btn.x && mouseX < btn.x + btn.w && mouseY > btn.y && mouseY < btn.y + btn.h) {
        buyAbilityShopItem(btn.index);
        break;
      }
    }
    // Check "Back to Battle" button for ability shop
    let backBtnAbility = buttonData.backToBattleAbility;
    if (mouseX > backBtnAbility.x && mouseX < backBtnAbility.x + backBtnAbility.w && mouseY > backBtnAbility.y && mouseY < backBtnAbility.y + backBtnAbility.h) {
      gameState = 'BATTLE';
    }
  } else if (gameState === 'GAME_OVER') {
    let restartBtn = buttonData.restart;
    if (mouseX > restartBtn.x && mouseX < restartBtn.x + restartBtn.w && mouseY > restartBtn.y && mouseY < restartBtn.y + restartBtn.h) {
      gameState = 'START_SCREEN'; // Go back to character selection
    }
  }
}
Line-by-line explanation (15 lines)

🔧 Subcomponents:

conditional-branch Start Screen Click Handler if (gameState === 'START_SCREEN') {

Detects clicks on character selection boxes and starts the game with the chosen species

conditional-branch Battle Screen Click Handler } else if (gameState === 'BATTLE') {

Detects clicks on Action buttons (Attack, Ability, Shop) and Ability Shop button

conditional-branch Consumable Shop Click Handler } else if (gameState === 'SHOP') {

Detects clicks on shop items to purchase consumables

conditional-branch Ability Shop Click Handler } else if (gameState === 'ABILITY_SHOP') {

Detects clicks on ability shop items to purchase permanent upgrades

conditional-branch Game Over Click Handler } else if (gameState === 'GAME_OVER') {

Detects clicks on the restart button to return to character selection

function mouseClicked() {
p5.js calls this function automatically whenever the mouse is clicked
if (gameState === 'START_SCREEN') {
If the game is on the start screen, check for character selection clicks
if (mouseX > btn.x && mouseX < btn.x + btn.w && mouseY > btn.y && mouseY < btn.y + btn.h) {
Checks if the mouse click was inside this button's rectangle using simple bounds checking
startGame(btn.species);
If clicked, start the game with the chosen species
// Ability Shop button is always clickable, regardless of turn
Unlike action buttons, the Ability Shop button works even when it's not the player's turn
if (currentTurn !== 'player') return;
Only allow battle actions (Attack, Ability, Shop) if it's the player's turn
if (btn.action === "shop") {
If the Shop button was clicked, transition to the SHOP screen
} else if (btn.action === "attack" || btn.action === "ability") {
If Attack or Ability was clicked, execute the player's turn with that action
} else if (gameState === 'SHOP') {
If the game is in the SHOP state, check for consumable item clicks
for (let i = 0; i < shopItems.length; i++) {
Loop through all consumable shop items to check if any were clicked
buyShopItem(btn.index);
If an item was clicked, call buyShopItem() to process the purchase
} else if (gameState === 'ABILITY_SHOP') {
If the game is in the ABILITY_SHOP state, check for ability clicks
buyAbilityShopItem(btn.index);
If an ability was clicked, call buyAbilityShopItem() to process the permanent upgrade
} else if (gameState === 'GAME_OVER') {
If the game is over, check for the restart button
gameState = 'START_SCREEN';
If restart was clicked, return to the start screen for a new game

keyPressed()

keyPressed() is p5.js's keyboard input handler. It shows that keyboard shortcuts are easy to add—this single function gives players an alternative way to use abilities without clicking. The keyCode variable holds a numeric code for each key (32 = spacebar, 65 = 'A', etc.). This demonstrates responsive, multi-input game design.

function keyPressed() {
  if (gameState === 'BATTLE' && keyCode === 32) { // Spacebar
    // Only allow ability use if it's player's turn
    if (currentTurn !== 'player') return;
    playerTurn('ability');
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Spacebar Detection if (gameState === 'BATTLE' && keyCode === 32) {

Checks if the game is in BATTLE state and the pressed key is spacebar (keyCode 32)

conditional Turn Validation if (currentTurn !== 'player') return;

Prevents ability use when it's not the player's turn

function keyPressed() {
p5.js calls this function automatically whenever any key is pressed
if (gameState === 'BATTLE' && keyCode === 32) {
Checks if the game is in BATTLE state AND the pressed key's code is 32 (spacebar)
if (currentTurn !== 'player') return;
If it's not the player's turn, exit early and ignore the spacebar press
playerTurn('ability');
If it's the player's turn, execute the ability action as if they had clicked the Ability button

📦 Key Variables

gameState string

Tracks which screen the game is currently displaying; switches between 'START_SCREEN', 'BATTLE', 'SHOP', 'ABILITY_SHOP', and 'GAME_OVER'

let gameState = 'START_SCREEN';
playerCharacter object (Character class)

Stores the player's hero, including name, species, HP, stats, ability, and pet companions

playerCharacter = new Character("Ignis", "Dragon", 100, 30, 10, "Fire Breath");
enemyCharacter object (Character class)

Stores the current enemy combatant with the same properties as playerCharacter

enemyCharacter = new Character("Drakon", "Dragon", 90, 25, 8, "Fire Breath");
shopItems array of ShopItem objects

Contains all consumable items available for purchase—potions, elixirs, and stat boosts

let shopItems = []; // Populated in setup()
abilityShopItems array of AbilityShopItem objects

Contains all permanent ability upgrades available for purchase; each restricted to a specific species

let abilityShopItems = []; // Populated in setup()
playerGold number

Tracks the player's total gold currency; earned by winning battles, spent in shops

let playerGold = 100;
battleLog array of strings

Stores battle messages describing each action (attacks, abilities, damage, healing) for the UI display

let battleLog = [];
currentTurn string

Tracks whose turn it is in combat; either 'player', 'pet', or 'enemy'; controls UI button states

let currentTurn = 'player';
characterImages object

Placeholder object for future image loading; currently unused but structured for expansion

let characterImages = {};
buttonData object with button properties

Stores the position (x, y, width, height) and action of every clickable button on screen for hit detection

let buttonData = {}; // Populated during draw functions

🔧 Potential Improvements (8)

Here are some ways this code could be enhanced:

BUG Character class display() method

HP bar can display numbers beyond 100% if healing exceeds max HP due to rounding; the visual bar is capped but the text is not

💡 Ensure hp never exceeds maxHp by using min(this.hp, this.maxHp) when displaying text, or cap healing in the heal() method

PERFORMANCE mouseClicked() function

Loops through buttonData object every click regardless of game state; could check multiple unnecessary buttons per frame

💡 Create separate button tracking objects per game state (battleButtons, shopButtons, etc.) and only check the active state's buttons

STYLE Character class and Pet class

takeDamage(), heal(), and dealDamage() methods are duplicated between Character and Pet classes

💡 Create a shared parent class (Fighter or Combatant) that both Character and Pet extend, eliminating code duplication

FEATURE useAbility() method in Character class

Enemy abilities are basic attacks only—enemies never use abilities, making them less threatening and less interesting

💡 Add enemy ability use: generate a random choice in enemyTurn() between attack and ability, calculate ability damage the same way the player does

BUG battleLog display in drawBattleScreen()

Battle log can overflow its text box if too many messages accumulate; text will render outside the box boundary

💡 Limit battleLog to the last 5-8 messages: use battleLog.slice(-8) when rendering, or splice old messages when the array exceeds a maximum length

FEATURE endBattle() function

Pets remain at full HP between battles even if they took damage, but the game resets their HP to full anyway—inconsistent healing behavior

💡 Modify pet healing between battles to match the player's healing: heal(maxHp * 0.5) so pets also take partial damage into the next battle, increasing difficulty

STYLE Global variable declarations

Mixed initialization patterns: some variables initialized with values, others as empty (buttonData = {}); creates inconsistency

💡 Consistently initialize all global variables at the top with meaningful defaults or comments explaining why they are empty

FEATURE Shop system

Players cannot sell items or manage inventory—they can only buy and use; no inventory UI or item management

💡 Add an inventory screen showing purchased items, a way to remove bought stat boosts, or sell items back for gold (at reduced price)

🔄 Code Flow

Code flow showing setup, draw, drawstartscreen, drawbattlescreen, startgame, generateenemy, playerturn, petattack, enemyturn, endbattle, buyshopitem, buyabilityshopitem, mouseclicked, keypressed

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

graph TD start[Start] --> setup[setup] setup --> canvas-creation[Canvas Setup] canvas-creation --> draw[draw loop] draw --> background-clear[Canvas Clear] background-clear --> state-dispatch[Game State Dispatcher] state-dispatch --> drawstartscreen[drawstartscreen] state-dispatch --> drawbattlescreen[drawbattlescreen] drawstartscreen --> title-text[Title Rendering] title-text --> character-layout[Character Box Positioning] character-layout --> dragon-box[Dragon Character Box] character-layout --> unicorn-box[Unicorn Character Box] character-layout --> griffin-box[Griffin Character Box] character-layout --> phoenix-box[Phoenix Character Box] dragon-box --> start-screen-clicks[Start Screen Click Handler] unicorn-box --> start-screen-clicks griffin-box --> start-screen-clicks phoenix-box --> start-screen-clicks drawbattlescreen --> character-display[Character Rendering] character-display --> pet-loop[Pet Display Loop] character-display --> battle-log[Battle Log Rendering] character-display --> action-buttons[Action Buttons with Turn Control] action-buttons --> turn-guard[Turn Check and Guard] action-buttons --> battle-clicks[Battle Screen Click Handler] pet-loop --> petattack[petattack] petattack --> pet-loop petattack --> enemyturn[enemyturn] enemyturn --> damage-calculation[Enemy Attack] damage-calculation --> player-death-check[Player Defeat Detection] player-death-check --> turn-return[Turn Reset] player-death-check --> defeat-path[Player Defeat Handling] player-death-check --> enemy-death-check[Enemy Defeat Detection] enemy-death-check --> victory-path[Player Victory Handling] victory-path --> endbattle[endbattle] endbattle --> game-state-reset[Game Reset] game-state-reset --> generateenemy[generateenemy] generateenemy --> enemy-generation[Enemy Spawning] enemy-generation --> random-species[Random Species Selection] random-species --> enemy-creation[Enemy Character Creation] endbattle --> spacebar-check[Spacebar Detection] spacebar-check --> turn-guard click setup href "#fn-setup" click draw href "#fn-draw" click canvas-creation href "#sub-canvas-creation" click background-clear href "#sub-background-clear" click state-dispatch href "#sub-state-dispatch" click drawstartscreen href "#fn-drawstartscreen" click title-text href "#sub-title-text" click character-layout href "#sub-character-layout" click dragon-box href "#sub-dragon-box" click unicorn-box href "#sub-unicorn-box" click griffin-box href "#sub-griffin-box" click phoenix-box href "#sub-phoenix-box" click start-screen-clicks href "#sub-start-screen-clicks" click drawbattlescreen href "#fn-drawbattlescreen" click character-display href "#sub-character-display" click pet-loop href "#sub-pet-loop" click battle-log href "#sub-battle-log" click action-buttons href "#sub-action-buttons" click turn-guard href "#sub-turn-guard" click battle-clicks href "#sub-battle-clicks" click petattack href "#fn-petattack" click enemyturn href "#fn-enemyturn" click damage-calculation href "#sub-damage-calculation" click player-death-check href "#sub-player-death-check" click turn-return href "#sub-turn-return" click defeat-path href "#sub-defeat-path" click enemy-death-check href "#sub-enemy-death-check" click victory-path href "#sub-victory-path" click endbattle href "#fn-endbattle" click game-state-reset href "#sub-game-state-reset" click generateenemy href "#fn-generateenemy" click enemy-generation href "#sub-enemy-generation" click random-species href "#sub-random-species" click enemy-creation href "#sub-enemy-creation" click spacebar-check href "#sub-spacebar-check"

❓ Frequently Asked Questions

What visual elements does the 'Battles Against Mythical Creatures' sketch feature?

The sketch includes character representations of various mythical creatures such as dragons and unicorns, along with a colorful battle interface that showcases health bars and attack animations.

How can players interact with the 'Battles Against Mythical Creatures' sketch?

Players can engage in battles by selecting actions for their character, using abilities, managing resources like gold, and potentially purchasing items from a shop.

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

This sketch showcases object-oriented programming through the use of a Character class, as well as game state management to facilitate different game phases like battles and shops.

Preview

battles ageist mythical creatures - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of battles ageist mythical creatures - Code flow showing setup, draw, drawstartscreen, drawbattlescreen, startgame, generateenemy, playerturn, petattack, enemyturn, endbattle, buyshopitem, buyabilityshopitem, mouseclicked, keypressed
Code Flow Diagram