This game is broken someone fix it, Theres a game after this i promise

This is a multilingual text adventure game where players explore an ancient temple, collect items, and solve puzzles through typed commands or clickable buttons. The game supports five languages (English, Spanish, French, German, Italian) and features a classic retro terminal aesthetic with green text on black.

🧪 Try This!

Experiment with the code by making these changes:

  1. Change the starting language to Spanish — Instead of randomly picking a language, force Spanish so you see all text translated immediately.
  2. Make the player start with the relic already in inventory — Begin the game with the relic collected, so you only need to return to the entrance to win—test the win condition instantly.
  3. Start in the treasure room where the relic is — Begin exploration in the room with the goal item instead of the entrance, so the game is shorter.
  4. Add a new location to the game — Extend the game world by adding a secret room—modify the Spanish language pack as an example.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates an interactive text-based adventure game with a cozy retro terminal interface. Players navigate through interconnected rooms, discover items, and pursue a central goal—finding an ancient relic and returning it to the temple entrance. The game brings together localization (supporting five languages automatically), game state management (tracking location, inventory, and win conditions), and interactive UI elements (both text input and clickable buttons).

The code is organized around a modular game loop with helper functions for displaying locations, processing commands, and managing player inventory. By studying it, you'll learn how to build a complete game within p5.js without a canvas, structure multilingual content using nested objects, parse user input in multiple languages, and create a polished user experience using DOM elements. The commented console.log statements throughout reveal the developer's debugging approach—a useful pattern for understanding code flow.

⚙️ How It Works

  1. When the sketch loads, setup() randomly selects one of five language packs, creates DOM elements (title, output area, input field, and button container), and immediately calls startGame() to begin gameplay.
  2. startGame() displays a welcome message, the game story intro, and the player's starting location (the temple entrance) using displayLocation(), which also calls createChoiceButtons() to generate clickable options.
  3. On every frame, the draw() function does nothing (it's a text adventure, not graphics-based), but the event listener on the input field watches for the Enter key and passes typed commands to processCommand().
  4. processCommand() parses the user's input, standardizes it across all five languages using lookup tables, and routes the command to the appropriate handler: goDirection() to move rooms, takeItem() to pick up objects, displayInventory() to view collected items, or other utility functions.
  5. Each action updates the game state (currentLocationKey, inventory array) and calls displayLocation() to refresh the visible room details and choice buttons.
  6. After every action, checkWinCondition() verifies if the player has collected the relic and returned to the entrance, triggering a victory message and disabling further input if the goal is reached.

🎓 Concepts You'll Learn

Localization and multilingual supportGame state managementDOM manipulation and event listenersObject-oriented data structuresString parsing and command processingConditional logic and win conditions

📝 Code Breakdown

preload()

preload() is a p5.js lifecycle function that runs before setup(). For text adventures, it's empty because we don't load external media files, but including it follows p5.js conventions.

function preload() {
  console.log("--- preload() called ---");
  // Not used for this text-based game, but required by p5.js structure
}
Line-by-line explanation (2 lines)
function preload() {
Declares the preload function, which p5.js calls before setup(); used for loading images and sounds, but not needed here
console.log("--- preload() called ---");
Logs a message to the browser console to indicate preload was called, helping trace code execution

setup()

setup() runs once when the sketch loads and is where you initialize all game state, create DOM elements, and set up event listeners. For text adventures, noCanvas() tells p5.js to skip the graphical canvas. The language selection here is the key to supporting multiple languages throughout the game.

🔬 These three lines create a div for holding clickable choice buttons. What happens if you comment out the line that sets the id? Will the buttons still appear? Why does the id matter?

  choicesContainerElement = createElement('div'); // Create choices container
  choicesContainerElement.id('choices-container');
  choicesContainerElement.parent('body');
function setup() {
  console.log("--- setup() called ---");
  noCanvas(); // No p5.js canvas needed for a pure text adventure

  console.log("setup(): Randomly selecting language...");
  const languageCodes = Object.keys(languagePacks);
  languageCode = random(languageCodes);
  selectedLanguage = languagePacks[languageCode];
  console.log("setup(): Selected language code:", languageCode);
  console.log("setup(): Selected language pack:", selectedLanguage);

  console.log("setup(): Initializing game state...");
  currentLocationKey = "entrance"; // Starting location
  inventory = [];
  gameOver = false;
  // gameStarted is now true by default

  console.log("setup(): Creating DOM elements...");
  gameTitleElement = createElement('h1', selectedLanguage.gameTitle);
  gameTitleElement.parent('body');
  console.log("setup(): gameTitleElement created:", gameTitleElement);

  outputElement = createElement('div');
  outputElement.id('output');
  outputElement.parent('body');
  console.log("setup(): outputElement created:", outputElement);

  choicesContainerElement = createElement('div'); // Create choices container
  choicesContainerElement.id('choices-container');
  choicesContainerElement.parent('body');
  console.log("setup(): choicesContainerElement created:", choicesContainerElement);

  const inputContainer = createElement('div');
  inputContainer.id('input-container');
  inputContainer.parent('body');
  console.log("setup(): inputContainer created:", inputContainer);

  promptElement = createElement('span', selectedLanguage.messages.prompt);
  promptElement.id('prompt');
  promptElement.parent(inputContainer);
  console.log("setup(): promptElement created:", promptElement);

  inputFieldElement = createInput('');
  inputFieldElement.id('input-field');
  inputFieldElement.attribute('placeholder', selectedLanguage.messages.typeHelp); // Set placeholder directly
  inputFieldElement.attribute('disabled', false); // Enable input field immediately
  inputFieldElement.parent(inputContainer);
  console.log("setup(): inputFieldElement created:", inputFieldElement);

  // Add an event listener to the input field to process commands when Enter is pressed
  inputFieldElement.elt.addEventListener('keydown', function(event) {
    console.log("inputFieldElement keydown event detected:", event.key);
    if (event.key === 'Enter') {
      console.log("Enter key pressed in input field.");
      const command = inputFieldElement.value.trim();
      inputFieldElement.value = ''; // Clear the input field
      if (command) {
        console.log("Command entered:", command);
        // Display the user's command in the output area
        displayOutput(`${selectedLanguage.messages.prompt}${command}`);
        processCommand(command); // Process the command
        // Scroll output to the bottom
        outputElement.elt.scrollTop = outputElement.elt.scrollHeight;
      }
    }
  });

  console.log("setup(): Calling startGame() directly...");
  startGame(); // Start the game immediately
  console.log("--- setup() finished ---");
}
Line-by-line explanation (13 lines)

🔧 Subcomponents:

calculation Random language selection languageCode = random(languageCodes);

Randomly picks one of the available language packs so each game starts in a different language

for-loop DOM element creation gameTitleElement = createElement('h1', selectedLanguage.gameTitle);

Creates HTML elements that will display the game interface without using a p5.js canvas

conditional Enter key detection if (event.key === 'Enter') {

Detects when the player presses Enter and triggers command processing

noCanvas();
Tells p5.js not to create a canvas element since this is a text-based game using only DOM elements
const languageCodes = Object.keys(languagePacks);
Extracts all language codes ('en', 'es', 'fr', 'de', 'it') from the languagePacks object into an array
languageCode = random(languageCodes);
Randomly selects one language code from the array using p5.js's random() function
selectedLanguage = languagePacks[languageCode];
Looks up the full language pack (all text for that language) using the selected code
currentLocationKey = "entrance";
Sets the player's starting location to the temple entrance
inventory = [];
Initializes an empty array to hold the keys of items the player has collected
gameTitleElement = createElement('h1', selectedLanguage.gameTitle);
Creates an HTML heading element containing the localized game title
outputElement.id('output');
Assigns the CSS id 'output' to the div so it can be styled and scrolled
inputFieldElement.elt.addEventListener('keydown', function(event) {
Attaches a listener that fires whenever a key is pressed in the input field
if (event.key === 'Enter') {
Checks if the pressed key is the Enter key, not any other key
const command = inputFieldElement.value.trim();
Extracts the text the player typed and removes whitespace from the start and end
inputFieldElement.value = '';
Clears the input field so it's ready for the next command
startGame();
Calls the startGame function to display the intro and show the first location

draw()

draw() is a p5.js lifecycle function that runs continuously. In text adventures, it's typically empty because all updates happen in response to player input (through event listeners) rather than continuous animation.

function draw() {
  // No continuous drawing needed for a text adventure
}
Line-by-line explanation (2 lines)
function draw() {
Declares the draw function, which p5.js calls 60 times per second by default
// No continuous drawing needed for a text adventure
Comment explaining that text adventures don't need frame-by-frame updates like graphics sketches do

startGame()

startGame() is the entry point to the actual gameplay. It displays the welcome message and initial location, then ensures the input field has focus so the player can start typing immediately. It's called once from setup().

function startGame() {
  console.log("--- startGame() called ---");
  // No need to clear output here as it's the first thing displayed,
  // but keeping it for robustness if called later.
  outputElement.html('');
  console.log("startGame(): Displaying welcome and intro messages...");
  displayOutput(formatText('messages.welcome') + ` ${selectedLanguage.gameTitle}`);
  displayOutput(selectedLanguage.intro);
  displayOutput(""); // Add a blank line for spacing
  console.log("startGame(): Displaying initial location...");
  displayLocation(); // Show the player's starting location

  // Input field is already enabled and focused from setup() for immediate start
  inputFieldElement.elt.focus(); // Ensure focus
  console.log("--- startGame() finished. Game is running ---");
}
Line-by-line explanation (6 lines)
outputElement.html('');
Clears any previous text from the output div, starting with a blank slate
displayOutput(formatText('messages.welcome') + ` ${selectedLanguage.gameTitle}`);
Displays 'Welcome to' in the current language, followed by the localized game title
displayOutput(selectedLanguage.intro);
Shows the game's introductory story text in the selected language
displayOutput("");
Adds a blank line for visual spacing between the intro and the first location description
displayLocation();
Calls the function to display the player's current location (the temple entrance), including exits and items
inputFieldElement.elt.focus();
Sets keyboard focus on the input field so the player can immediately start typing commands

formatText(keyPath, ...args)

formatText() is a utility function that bridges localization and dynamic text. It retrieves a translated string (like 'messages.itemTaken') and substitutes placeholders ('%s') with actual values like item names. This pattern allows a single text key to produce different messages depending on context.

function formatText(keyPath, ...args) {
  console.log("formatText() called for keyPath:", keyPath, "Args:", args);
  let text = getLocalizedText(keyPath);
  if (text) {
    args.forEach(arg => {
      text = text.replace('%s', arg);
    });
  }
  const result = text || `[MISSING TEXT KEY: ${keyPath}]`; // Fallback for missing keys
  console.log("formatText() result for keyPath:", keyPath, ":", result);
  return result;
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

calculation Localized text retrieval let text = getLocalizedText(keyPath);

Fetches the translated text string for the current language using the provided key path

for-loop Placeholder substitution args.forEach(arg => { text = text.replace('%s', arg); });

Replaces each '%s' placeholder in the text with the corresponding argument value

function formatText(keyPath, ...args) {
Declares a function that takes a keyPath string and any number of additional arguments (using the spread operator)
let text = getLocalizedText(keyPath);
Calls getLocalizedText to retrieve the translated string for the current language and key
if (text) {
Checks that the text was found before attempting to replace placeholders
args.forEach(arg => { text = text.replace('%s', arg); });
Loops through each argument and replaces the first '%s' in the text with that argument's value
const result = text || `[MISSING TEXT KEY: ${keyPath}]`;
Sets result to the formatted text, or a fallback error message if the key was not found
return result;
Returns the formatted and localized text string to the caller

getLocalizedText(keyPath)

getLocalizedText() implements a common pattern for accessing nested object properties using a dot-separated key path. It's the engine behind localization—given a path like 'locations.entrance.name', it navigates the language pack object to retrieve the translated name of the entrance location.

🔬 This loop navigates through nested objects using dot notation. If you call getLocalizedText('locations.entrance.exits.north'), it will split into ['locations', 'entrance', 'exits', 'north'] and drill down step by step. Try tracing through manually: what value should it return?

  for (let i = 0; i < parts.length; i++) {
    if (current.hasOwnProperty(parts[i])) {
      current = current[parts[i]];
    } else {
function getLocalizedText(keyPath) {
  console.log("getLocalizedText() called for keyPath:", keyPath);
  let parts = keyPath.split('.');
  let current = selectedLanguage;
  if (!current) {
    console.error("getLocalizedText() failed: selectedLanguage is undefined!");
    return undefined;
  }
  for (let i = 0; i < parts.length; i++) {
    if (current.hasOwnProperty(parts[i])) {
      current = current[parts[i]];
    } else {
      console.error("getLocalizedText() failed for keyPath:", keyPath, "at part:", parts[i], "Current object:", current);
      return undefined;
    }
  }
  console.log("getLocalizedText() result for keyPath:", keyPath, ":", current);
  return current;
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

calculation Key path splitting let parts = keyPath.split('.');

Breaks a dot-separated key path like 'messages.welcome' into an array ['messages', 'welcome']

for-loop Nested object navigation for (let i = 0; i < parts.length; i++) {

Steps through each part of the key path, drilling down into nested objects to find the final value

let parts = keyPath.split('.');
Converts a key path like 'messages.welcome' into an array like ['messages', 'welcome']
let current = selectedLanguage;
Starts at the top-level language pack object and will navigate down through nested objects
if (!current) {
Checks if selectedLanguage exists; if not, there's a critical error and the function returns undefined
for (let i = 0; i < parts.length; i++) {
Loops through each part of the key path to navigate deeper into nested objects
if (current.hasOwnProperty(parts[i])) {
Checks if the current object has a property with the current part's name
current = current[parts[i]];
Moves deeper into the object hierarchy by accessing the property with that name
return undefined;
If a key part is not found, returns undefined and logs an error for debugging

displayOutput(text)

displayOutput() is the main utility for writing text to the game's output area. It appends each new message to what's already displayed (building up a scrollable log) and auto-scrolls to show the latest text. This is the classic behavior of text adventure games.

function displayOutput(text) {
  console.log("displayOutput() called with text:", text, "outputElement:", outputElement);
  if (outputElement) {
    outputElement.html(outputElement.html() + text + '<br>', true); // Append text and a line break
    outputElement.elt.scrollTop = outputElement.elt.scrollHeight;    // Scroll to bottom
    console.log("displayOutput(): Text appended and scrolled.");
  } else {
    console.error("displayOutput(): outputElement is undefined when displayOutput() was called!");
  }
}
Line-by-line explanation (3 lines)
if (outputElement) {
Checks that the output div was created successfully before trying to append text to it
outputElement.html(outputElement.html() + text + '<br>', true);
Appends the new text plus an HTML line break to the existing output, preserving all previous text
outputElement.elt.scrollTop = outputElement.elt.scrollHeight;
Automatically scrolls the output div to the bottom so the newest text is always visible

processCommand(command)

processCommand() is the brain of the game's input system. It parses player input, standardizes it across five languages using lookup tables, and routes commands to the appropriate handlers. The use of a standardActions object decouples user input from logic, making it easy to add new languages without touching handler code.

🔬 This object maps each standard action to all its language translations. If the game is in English but you type 'ir' (Spanish for 'go'), what happens? Try adding a new translation to the 'go' array, like 'move', and see if it works.

  const standardActions = {
    go: ['go', 'ir', 'aller', 'gehen', 'vai'], // EN, ES, FR, DE, IT
    look: ['look', 'mirar', 'regarder', 'schauen', 'guarda'], // EN, ES, FR, DE, IT
    take: ['take', 'tomar', 'prendre', 'nehmen', 'prendi'], // EN, ES, FR, DE, IT
    inventory: ['inventory', 'inventario', 'inventaire', 'inventar', 'inventario'], // EN, ES, FR, DE, IT
    help: ['help', 'ayuda', 'aide', 'hilfe', 'aiuto'], // EN, ES, FR, DE, IT
    quit: ['quit', 'salir', 'quitter', 'beenden', 'esci'] // EN, ES, FR, DE, IT
  };
function processCommand(command) {
  console.log("processCommand() called with command:", command);
  if (gameOver) {
    displayOutput(formatText('messages.gameOverLose')); // Prevent new commands if game is over
    clearChoiceButtons(); // Remove choice buttons if game is over
    return;
  }

  const lowerCommand = command.toLowerCase();
  const parts = lowerCommand.split(' ');
  const action = parts[0];
  const target = parts.slice(1).join(' ');

  // Standardize actions for parsing, regardless of the selected language.
  // This allows us to use 'go' or 'ir' and still trigger the 'go' logic.
  const standardActions = {
    go: ['go', 'ir', 'aller', 'gehen', 'vai'], // EN, ES, FR, DE, IT
    look: ['look', 'mirar', 'regarder', 'schauen', 'guarda'], // EN, ES, FR, DE, IT
    take: ['take', 'tomar', 'prendre', 'nehmen', 'prendi'], // EN, ES, FR, DE, IT
    inventory: ['inventory', 'inventario', 'inventaire', 'inventar', 'inventario'], // EN, ES, FR, DE, IT
    help: ['help', 'ayuda', 'aide', 'hilfe', 'aiuto'], // EN, ES, FR, DE, IT
    quit: ['quit', 'salir', 'quitter', 'beenden', 'esci'] // EN, ES, FR, DE, IT
  };

  let recognizedAction = null;
  for (const stdAction in standardActions) {
    if (standardActions[stdAction].includes(action)) {
      recognizedAction = stdAction;
      break;
    }
  }

  switch (recognizedAction) {
    case 'go':
      goDirection(target);
      break;
    case 'look':
      displayLocation();
      break;
    case 'take':
      takeItem(target);
      break;
    case 'inventory':
      displayInventory();
      break;
    case 'help':
      displayHelp();
      break;
    case 'quit':
      gameOver = true;
      if (inventory.includes("relic")) {
        displayOutput(formatText('messages.goodbye') + " " + formatText('messages.gameOverWin'));
        gameTitleElement.html(selectedLanguage.gameTitle + ' - ' + formatText('messages.gameOverWin'));
      } else {
        displayOutput(formatText('messages.goodbye') + " " + formatText('messages.gameOverLose'));
        gameTitleElement.html(selectedLanguage.gameTitle + ' - ' + formatText('messages.gameOverLose'));
      }
      inputFieldElement.attribute('disabled', ''); // Disable input field
      clearChoiceButtons(); // Remove choice buttons
      break;
    default:
      displayOutput(formatText('messages.invalidAction'));
      break;
  }

  // After every action, check if the win condition has been met
  checkWinCondition();
}
Line-by-line explanation (13 lines)

🔧 Subcomponents:

conditional Early exit if game over if (gameOver) {

Prevents any new commands from being processed once the game has ended

calculation Command tokenization const parts = lowerCommand.split(' ');

Splits the user's input into an action word and target word(s) for routing

for-loop Language-agnostic action lookup for (const stdAction in standardActions) {

Matches the user's input to a standard action name, supporting all five languages

switch-case Action dispatch switch (recognizedAction) {

Routes the command to the appropriate handler function based on the action type

if (gameOver) {
Checks if the game has ended; if so, rejects any new commands and exits early
const lowerCommand = command.toLowerCase();
Converts the entire command to lowercase so 'GO', 'Go', and 'go' are treated the same
const parts = lowerCommand.split(' ');
Splits the command into words, e.g., 'go north' becomes ['go', 'north']
const action = parts[0];
Extracts the first word as the action (e.g., 'go')
const target = parts.slice(1).join(' ');
Combines all remaining words as the target (e.g., 'north' or 'torch of light')
for (const stdAction in standardActions) {
Loops through each standard action category (go, look, take, etc.)
if (standardActions[stdAction].includes(action)) {
Checks if the user's action word matches any translation of the current standard action
recognizedAction = stdAction;
Records that the user's input matches this standard action, stopping the search
switch (recognizedAction) {
Routes to the appropriate handler based on the recognized action type
case 'go': goDirection(target); break;
If the action was 'go' (or its translation), calls goDirection() to move the player
case 'quit': gameOver = true;
If the action was 'quit', sets gameOver to true and displays the ending message
if (inventory.includes("relic")) {
Checks if the player has the relic to determine a win or loss message
checkWinCondition();
After processing any command, checks if the player has met the game's win condition

clearChoiceButtons()

clearChoiceButtons() empties the container of clickable choice buttons. It's called before creating new buttons (to avoid duplicates) and after the game ends (to prevent further interaction).

function clearChoiceButtons() {
  console.log("clearChoiceButtons() called.");
  if (choicesContainerElement) {
    choicesContainerElement.html('');
    console.log("clearChoiceButtons(): choicesContainerElement cleared.");
  } else {
    console.error("clearChoiceButtons(): choicesContainerElement is undefined when clearChoiceButtons() was called!");
  }
}
Line-by-line explanation (2 lines)
if (choicesContainerElement) {
Checks that the choices container div exists before attempting to clear it
choicesContainerElement.html('');
Removes all HTML content (buttons) from the choices container, leaving it empty

createChoiceButtons()

createChoiceButtons() generates the interactive buttons that let players click instead of typing. It examines the current location's exits and available items, then creates a button for each one. When clicked, buttons trigger processCommand() as if the player had typed the command—seamlessly integrating typing and clicking as input methods.

🔬 This code finds items at the current location that aren't already in the player's inventory. The filter has two conditions: item.location matches currentLocationKey AND the item key is not in inventory. What happens if you remove the inventory check? Items would appear as buttons even after being collected!

  // Create buttons for items at this location
  const itemsAtLocation = Object.entries(selectedLanguage.items)
    .filter(([key, item]) => item.location === currentLocationKey && !inventory.includes(key));

  if (itemsAtLocation.length > 0) {
function createChoiceButtons() {
  console.log("createChoiceButtons() called.");
  clearChoiceButtons(); // Clear old buttons

  const location = selectedLanguage.locations[currentLocationKey];
  if (!location) {
    console.error("createChoiceButtons(): Location not found for key:", currentLocationKey);
    return;
  }
  console.log("createChoiceButtons(): Current location:", location);

  // Create buttons for exits
  const exits = Object.keys(location.exits);
  if (exits.length > 0) {
    console.log("createChoiceButtons(): Creating exit buttons for exits:", exits);
    exits.forEach(directionKey => {
      // Find the localized name for the direction key
      let localizedDirectionName = directionKey;
      // The languageCodes array is dynamically generated from languagePacks keys in setup()
      // so its index corresponds to the order of localized directions in standardDirections.
      const langIndex = Object.keys(languagePacks).indexOf(languageCode); // Re-get languageCodes for robustness
      if (langIndex !== -1 && standardDirections[directionKey] && standardDirections[directionKey][langIndex]) {
        localizedDirectionName = standardDirections[directionKey][langIndex];
      }
      const button = createButton(`${formatText('actions.go')} ${localizedDirectionName}`);
      button.mousePressed(() => processCommand(`${formatText('actions.go')} ${localizedDirectionName}`));
      button.parent(choicesContainerElement);
      console.log("createChoiceButtons(): Created exit button:", button.html());
    });
  } else {
    console.log("createChoiceButtons(): No exits to create buttons for.");
  }

  // Create buttons for items at this location
  const itemsAtLocation = Object.entries(selectedLanguage.items)
    .filter(([key, item]) => item.location === currentLocationKey && !inventory.includes(key));

  if (itemsAtLocation.length > 0) {
    console.log("createChoiceButtons(): Creating item buttons for items:", itemsAtLocation.map(([k,v])=>v.name));
    itemsAtLocation.forEach(([key, item]) => {
      const button = createButton(`${formatText('actions.take')} ${item.name}`);
      button.mousePressed(() => processCommand(`${formatText('actions.take')} ${item.name}`));
      button.parent(choicesContainerElement);
      console.log("createChoiceButtons(): Created item button:", button.html());
    });
  } else {
    console.log("createChoiceButtons(): No items to create buttons for.");
  }
  console.log("createChoiceButtons() finished.");
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

for-loop Exit button creation exits.forEach(directionKey => {

Creates a clickable button for each exit from the current location

for-loop Item button creation itemsAtLocation.forEach(([key, item]) => {

Creates a clickable button for each item available to pick up at this location

const location = selectedLanguage.locations[currentLocationKey];
Retrieves the location object for the player's current position
const exits = Object.keys(location.exits);
Extracts the direction keys (north, south, etc.) from the location's exits object
exits.forEach(directionKey => {
Loops through each exit direction to create a button for it
const langIndex = Object.keys(languagePacks).indexOf(languageCode);
Finds the position of the current language in the languagePacks array to index into standardDirections
localizedDirectionName = standardDirections[directionKey][langIndex];
Retrieves the translated direction name (e.g., 'norte' if Spanish is selected)
const button = createButton(`${formatText('actions.go')} ${localizedDirectionName}`);
Creates a button with text like 'go norte' or 'go north' depending on the language
button.mousePressed(() => processCommand(`${formatText('actions.go')} ${localizedDirectionName}`));
Attaches a click listener that runs the command as if the player had typed it
const itemsAtLocation = Object.entries(selectedLanguage.items).filter(([key, item]) => item.location === currentLocationKey && !inventory.includes(key));
Filters the items object to find only items present at the current location that the player hasn't already picked up
itemsAtLocation.forEach(([key, item]) => {
Loops through each available item to create a button for it

displayLocation()

displayLocation() is the heart of the game's moment-to-moment experience. Every time the player moves, looks around, or picks something up, this function runs to show the location's name, description, available items, and exits. It's also responsible for creating the clickable choice buttons, making it the bridge between displaying information and enabling interaction.

🔬 This chain of methods converts the items object into a list of names. The filter checks two things: does the item live at this location AND is it not in the player's inventory? Try removing one of these conditions and observe what appears—maybe items you've already collected still show up, or items from other rooms appear here.

  const itemsAtLocation = Object.entries(selectedLanguage.items)
    .filter(([key, item]) => item.location === currentLocationKey && !inventory.includes(key))
    .map(([key, item]) => item.name);
function displayLocation() {
  console.log("displayLocation() called. Current location key:", currentLocationKey);
  const location = selectedLanguage.locations[currentLocationKey];
  if (!location) {
    console.error(`displayLocation(): Location '${currentLocationKey}' not found in language pack '${languageCode}'`);
    displayOutput(`[ERROR: Location '${currentLocationKey}' not found in language pack '${languageCode}']`);
    return;
  }
  console.log("displayLocation(): Location data:", location);

  displayOutput(`--- ${formatText('messages.youAreHere')} ${location.name} ---`);
  displayOutput(location.description);

  // Display items currently at this location (and not in the player's inventory)
  const itemsAtLocation = Object.entries(selectedLanguage.items)
    .filter(([key, item]) => item.location === currentLocationKey && !inventory.includes(key))
    .map(([key, item]) => item.name);
  console.log("displayLocation(): Items at location:", itemsAtLocation);

  if (itemsAtLocation.length > 0) {
    displayOutput(formatText('messages.itemsHere') + ' ' + itemsAtLocation.join(', ') + '.');
  }

  // Display available exits
  const exits = Object.keys(location.exits);
  console.log("displayLocation(): Available exits keys:", exits);
  if (exits.length > 0) {
    displayOutput(formatText('messages.availableExits') + ' ' + exits.join(', ') + '.');
  } else {
    displayOutput(formatText('messages.noExits'));
  }

  createChoiceButtons(); // Create clickable buttons after displaying location details
  console.log("displayLocation() finished.");
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

calculation Current location retrieval const location = selectedLanguage.locations[currentLocationKey];

Fetches the location object for the player's current position

for-loop Available items filtering const itemsAtLocation = Object.entries(selectedLanguage.items).filter(([key, item]) => item.location === currentLocationKey && !inventory.includes(key))

Extracts only items at this location that the player hasn't already collected

calculation Exit direction list const exits = Object.keys(location.exits);

Gets all direction keys available from the current location

const location = selectedLanguage.locations[currentLocationKey];
Retrieves the location object from the selected language pack using the player's current location key
if (!location) {
Checks if the location exists; if not, displays an error and stops to prevent crashes
displayOutput(`--- ${formatText('messages.youAreHere')} ${location.name} ---`);
Displays a header with the location name, localized in the current language
displayOutput(location.description);
Displays the location's description, painting a picture for the player
.filter(([key, item]) => item.location === currentLocationKey && !inventory.includes(key))
Filters items to show only those at this location and not already in the player's inventory
.map(([key, item]) => item.name)
Extracts just the item names from the filtered items for display
displayOutput(formatText('messages.itemsHere') + ' ' + itemsAtLocation.join(', ') + '.');
Displays all available items in this location, or nothing if the list is empty
const exits = Object.keys(location.exits);
Extracts the direction keys (north, south, east, west) from the location's exits object
displayOutput(formatText('messages.availableExits') + ' ' + exits.join(', ') + '.');
Displays all available exits, or a 'no exits' message if there are none
createChoiceButtons();
Generates clickable buttons for each exit and item after displaying the location

takeItem(targetItemName)

takeItem() implements the 'take' command, which moves an item from the current location into the player's inventory. It uses find() to search for an item matching three criteria: the item must be at this location, not already collected, and match the player's typed name. By updating both the inventory array and the item's location property, it keeps the game state consistent.

🔬 This find() method searches with three conditions. What happens if you remove the !inventory.includes(key) check? The player would be able to pick up the same item multiple times! Try removing that line and see what appears in the inventory list.

  const foundItem = itemEntries.find(([key, item]) =>
    item.location === currentLocationKey &&
    !inventory.includes(key) &&
    item.name.toLowerCase() === targetItemName.toLowerCase()
  );
function takeItem(targetItemName) {
  console.log("takeItem() called with target:", targetItemName);
  const location = selectedLanguage.locations[currentLocationKey];
  if (!location) {
    console.error("takeItem(): Current location is undefined!");
    return;
  }

  const itemEntries = Object.entries(selectedLanguage.items);
  const foundItem = itemEntries.find(([key, item]) =>
    item.location === currentLocationKey &&
    !inventory.includes(key) &&
    item.name.toLowerCase() === targetItemName.toLowerCase()
  );
  console.log("takeItem(): Found item:", foundItem);

  if (foundItem) {
    const [itemKey, itemData] = foundItem;
    inventory.push(itemKey);          // Add item key to inventory
    itemData.location = 'inventory';  // Update item's location to 'inventory'
    displayOutput(formatText('messages.itemTaken', itemData.name));
    createChoiceButtons(); // Re-create choice buttons as item is no longer at location
    console.log("takeItem(): Item taken:", itemData.name, "Inventory:", inventory);
  } else {
    displayOutput(formatText('messages.itemNotFound'));
    console.log("takeItem(): Item not found:", targetItemName);
  }
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

const itemEntries = Object.entries(selectedLanguage.items);
Converts the items object into an array of [key, item] pairs for easier searching
const foundItem = itemEntries.find(([key, item]) =>
Uses the find() method to locate the first item matching all three conditions below
item.location === currentLocationKey &&
Checks that the item is present at the player's current location
!inventory.includes(key) &&
Checks that the player doesn't already have this item
item.name.toLowerCase() === targetItemName.toLowerCase()
Matches the item's name (case-insensitive) to what the player typed
if (foundItem) {
If an item was found, proceeds to add it to inventory
inventory.push(itemKey);
Adds the item's key to the inventory array
itemData.location = 'inventory';
Updates the item's location property to 'inventory' so it won't appear at the location anymore
displayOutput(formatText('messages.itemTaken', itemData.name));
Displays a localized message confirming the item was taken, inserting the item's name
createChoiceButtons();
Refreshes the choice buttons so the item no longer appears as an option to take

goDirection(direction)

goDirection() moves the player from one location to another. It first standardizes the player's typed direction (converting 'norte', 'nord', 'norden', etc. to 'north') using the standardDirections lookup table, then checks that the direction is a valid exit from the current location. If valid, it updates currentLocationKey and displays the new location.

🔬 This loop translates the player's input (like 'norte' in Spanish) into a standard direction ('north'). Try changing the loop to remove the 'break' statement—what happens if you delete it? The loop would continue running even after finding a match!

  let recognizedDirection = null;
  for (const stdDir in standardDirections) {
    if (standardDirections[stdDir].includes(direction.toLowerCase())) {
      recognizedDirection = stdDir;
      break;
    }
  }
function goDirection(direction) {
  console.log("goDirection() called with direction:", direction);
  const location = selectedLanguage.locations[currentLocationKey];
  if (!location) {
    console.error("goDirection(): Current location is undefined!");
    return;
  }
  console.log("goDirection(): Location exits:", location.exits);

  let recognizedDirection = null;
  for (const stdDir in standardDirections) {
    if (standardDirections[stdDir].includes(direction.toLowerCase())) {
      recognizedDirection = stdDir;
      break;
    }
  }
  console.log("goDirection(): Recognized direction:", recognizedDirection);

  if (recognizedDirection && location.exits[recognizedDirection]) {
    currentLocationKey = location.exits[recognizedDirection]; // Update current location
    displayLocation(); // Display new location and update choice buttons
    console.log("goDirection(): Moved to new location:", currentLocationKey);
  } else {
    displayOutput(formatText('messages.cannotGo'));
    console.log("goDirection(): Cannot go in direction:", direction);
  }
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

for-loop Direction standardization for (const stdDir in standardDirections) {

Matches the player's typed direction to a standard direction name across all five languages

const location = selectedLanguage.locations[currentLocationKey];
Retrieves the player's current location object
let recognizedDirection = null;
Initializes a variable to hold the standardized direction if found
for (const stdDir in standardDirections) {
Loops through each standard direction (north, south, east, west)
if (standardDirections[stdDir].includes(direction.toLowerCase())) {
Checks if the player's input matches any translation of the current standard direction
recognizedDirection = stdDir;
Records the standard direction name (e.g., 'north') and stops searching
if (recognizedDirection && location.exits[recognizedDirection]) {
Verifies both that the direction was recognized AND that it's a valid exit from the current location
currentLocationKey = location.exits[recognizedDirection];
Updates the player's current location to the destination specified in the exits object
displayLocation();
Calls displayLocation() to show the new room, description, items, and exits

displayInventory()

displayInventory() shows the player everything they've collected. It loops through the inventory array (which contains item keys) and looks up each item's name and description from the language pack, displaying them in a formatted list.

function displayInventory() {
  console.log("displayInventory() called.");
  if (inventory.length === 0) {
    displayOutput(formatText('messages.noItems'));
  } else {
    displayOutput(formatText('messages.inventoryList'));
    inventory.forEach(itemKey => {
      const item = selectedLanguage.items[itemKey];
      if (item) {
        displayOutput(`- ${item.name}: ${item.description}`);
      } else {
        displayOutput(`- [UNKNOWN ITEM: ${itemKey}]`);
        console.error("displayInventory(): Unknown item key in inventory:", itemKey);
      }
    });
  }
  console.log("displayInventory() finished.");
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

for-loop Item listing inventory.forEach(itemKey => {

Iterates through each item in the player's inventory and displays its details

if (inventory.length === 0) {
Checks if the player has collected any items yet
displayOutput(formatText('messages.noItems'));
If inventory is empty, displays a localized message like 'Your inventory is empty'
displayOutput(formatText('messages.inventoryList'));
Displays a header like 'Inventory:' before listing items
inventory.forEach(itemKey => {
Loops through each item key in the inventory array
const item = selectedLanguage.items[itemKey];
Looks up the item object using the item key to get its name and description
displayOutput(`- ${item.name}: ${item.description}`);
Displays the item as a bullet-point list entry with name and description

displayHelp()

displayHelp() displays a quick reference of all available commands, fully localized in the player's selected language. It's called when the player types 'help' and provides a gentle guide to the game's interface.

function displayHelp() {
  console.log("displayHelp() called.");
  displayOutput("--- " + formatText('actions.help') + " ---");
  displayOutput(formatText('messages.typeHelp'));
  displayOutput(`${formatText('actions.go')} [${formatText('messages.availableExits')}]`);
  displayOutput(formatText('actions.look'));
  displayOutput(`${formatText('actions.take')} [${formatText('messages.itemsHere')}]`);
  displayOutput(formatText('actions.inventory'));
  displayOutput(formatText('actions.quit'));
  displayOutput("---");
  console.log("displayHelp() finished.");
}
Line-by-line explanation (7 lines)
displayOutput("--- " + formatText('actions.help') + " ---");
Displays a header like '--- help ---' in the player's language
displayOutput(formatText('messages.typeHelp'));
Shows a brief explanation of how to use commands
displayOutput(`${formatText('actions.go')} [${formatText('messages.availableExits')}]`);
Displays the 'go' command syntax with a placeholder for available exits
displayOutput(formatText('actions.look'));
Lists the 'look' command to examine the current location
displayOutput(`${formatText('actions.take')} [${formatText('messages.itemsHere')}]`);
Lists the 'take' command syntax with a placeholder for items
displayOutput(formatText('actions.inventory'));
Lists the 'inventory' command to view collected items
displayOutput(formatText('actions.quit'));
Lists the 'quit' command to end the game

checkWinCondition()

checkWinCondition() implements the game's victory logic. It's called after every player action to check if the win condition has been met (having the relic and returning to the entrance). When the condition is true, it sets gameOver, displays a victory message, and disables further interaction.

🔬 This condition requires two things: being at the entrance AND having the relic. What if you remove the second part? The player would win just by returning to the start, regardless of whether they found the relic. Try changing it to check only location.

  if (currentLocationKey === "entrance" && inventory.includes("relic")) {
function checkWinCondition() {
  console.log("checkWinCondition() called. Current location:", currentLocationKey, "Inventory:", inventory);
  // Win condition: Player is at the 'entrance' and has the 'relic' in their inventory
  if (currentLocationKey === "entrance" && inventory.includes("relic")) {
    gameOver = true;
    displayOutput(formatText('messages.gameOverWin'));
    inputFieldElement.attribute('disabled', ''); // Disable input field
    gameTitleElement.html(selectedLanguage.gameTitle + ' - ' + formatText('messages.gameOverWin')); // Update title
    clearChoiceButtons(); // Remove choice buttons
    console.log("Win condition met! Game Over.");
  } else {
    console.log("checkWinCondition(): Win condition not met.");
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Victory condition evaluation if (currentLocationKey === "entrance" && inventory.includes("relic")) {

Verifies both location and inventory to determine if the player has won

if (currentLocationKey === "entrance" && inventory.includes("relic")) {
Checks two things: is the player at the entrance AND do they have the relic item
gameOver = true;
Sets the gameOver flag to true, which prevents further commands from being processed
displayOutput(formatText('messages.gameOverWin'));
Displays a congratulations message in the player's language
inputFieldElement.attribute('disabled', '');
Disables the input field so the player can't type more commands
gameTitleElement.html(selectedLanguage.gameTitle + ' - ' + formatText('messages.gameOverWin'));
Updates the page title to show the win message, e.g., 'The Lost Relic - Congratulations!'
clearChoiceButtons();
Removes all clickable choice buttons since the game has ended

📦 Key Variables

languagePacks object

A nested object containing all game text for five languages (en, es, fr, de, it), organized by category (locations, items, actions, messages)

languagePacks.en.gameTitle === 'The Lost Relic'
selectedLanguage object

Holds the currently active language pack, populated in setup() based on the random languageCode selection

selectedLanguage.gameTitle === 'La Reliquia Perdida' (if Spanish)
languageCode string

Stores the two-letter code of the active language ('en', 'es', 'fr', 'de', 'it')

languageCode = 'en'
currentLocationKey string

Stores the key of the player's current location (e.g., 'entrance', 'mainHall', 'treasureRoom')

currentLocationKey = 'entrance'
inventory array

An array of item keys that the player has collected (e.g., ['torch', 'relic'])

inventory = ['torch', 'relic']
gameOver boolean

A flag indicating whether the game has ended (true) or is still in progress (false)

gameOver = false
gameTitleElement p5.Renderer (HTML element)

The h1 DOM element displaying the game title; updated to show win/loss messages

gameTitleElement = createElement('h1')
outputElement p5.Renderer (HTML element)

The div DOM element where all game text is displayed; scrolls to show the latest messages

outputElement = createElement('div')
inputFieldElement p5.Renderer (HTML input element)

The text input field where the player types commands; has an event listener for Enter key

inputFieldElement = createInput('')
promptElement p5.Renderer (HTML element)

The span element displaying '> ' before the input field

promptElement = createElement('span', '> ')
choicesContainerElement p5.Renderer (HTML element)

The div element holding clickable choice buttons for exits and items

choicesContainerElement = createElement('div')
standardDirections object

Maps standard direction names (north, south, east, west) to arrays of translations for all five languages

standardDirections.north = ['north', 'norte', 'nord', 'norden', 'nord']

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG createChoiceButtons() exit button creation

The code tries to find a localized direction name by indexing into standardDirections using langIndex, but if the language order in languagePacks doesn't match the order in the standardDirections arrays, the wrong translation is shown. For example, if languages are reordered, Spanish 'norte' might display as 'nord' (French).

💡 Hardcode a mapping from languageCode to index, or restructure standardDirections to use language codes as keys instead of relying on array index order: `standardDirections.north = { en: 'north', es: 'norte', fr: 'nord', de: 'norden', it: 'nord' }`

PERFORMANCE createChoiceButtons()

Object.keys(languagePacks).indexOf(languageCode) is called for every exit button created. If a location has multiple exits, this lookups runs redundantly multiple times.

💡 Calculate langIndex once at the top of createChoiceButtons() and reuse it for all buttons.

STYLE processCommand() action matching

The standardActions object is redefined inside processCommand() every time a command is processed, creating unnecessary copies in memory. This same object should be global.

💡 Move standardActions to the top of the file as a global const (like standardDirections), so it's defined once and reused.

BUG takeItem()

The code stores item keys in the inventory array but doesn't validate that the item key exists in selectedLanguage.items. If inventory is corrupted with an invalid key, displayInventory() catches it but silently logs an error instead of preventing the state inconsistency.

💡 Add a validation step in takeItem() to verify the item exists before adding it: `if (!selectedLanguage.items[itemKey]) { console.error(...); return; }`

FEATURE Game state management

The game has no persistence—if the player refreshes the page, all progress is lost. Most text adventures save progress.

💡 Add localStorage to save currentLocationKey and inventory after every action, and restore them on setup() if they exist: `localStorage.setItem('gameState', JSON.stringify({ currentLocationKey, inventory }))`

BUG displayOutput()

The function appends text by concatenating HTML strings with `.html()`. If user input or item names contain HTML characters like '<' or '>', they'll be interpreted as HTML tags, potentially breaking the display or creating XSS vulnerabilities.

💡 Use `.text()` instead of `.html()` to safely escape special characters, or use a DOM method like `textContent` instead of string concatenation.

🔄 Code Flow

Code flow showing preload, setup, draw, startgame, formattext, getlocalizedtext, displayoutput, processcommand, clearchoicebuttons, createchoicebuttons, displaylocation, takeitem, godirection, displayinventory, displayhelp, checkwincondition

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> enter-key-listener[Enter Key Listener] draw --> game-over-check[Game Over Check] draw --> command-parsing[Command Parsing] command-parsing --> action-matching[Action Matching] action-matching --> command-routing[Command Routing] command-routing --> processcommand[processCommand] processcommand --> clearchoicebuttons[clearChoiceButtons] processcommand --> displayoutput[displayOutput] processcommand --> checkwincondition[checkWinCondition] checkwincondition -->|if win| displayoutput checkwincondition -->|if not win| displaylocation[displayLocation] displaylocation --> exit-button-loop[Exit Button Loop] displaylocation --> item-button-loop[Item Button Loop] exit-button-loop -->|for each exit| godirection[goDirection] item-button-loop -->|for each item| takeitem[takeItem] setup --> language-selection[Language Selection] setup --> dom-creation-loop[DOM Creation Loop] click setup href "#fn-setup" click draw href "#fn-draw" click enter-key-listener href "#sub-enter-key-listener" click game-over-check href "#sub-game-over-check" click command-parsing href "#sub-command-parsing" click action-matching href "#sub-action-matching" click command-routing href "#sub-command-routing" click processcommand href "#fn-processcommand" click clearchoicebuttons href "#fn-clearchoicebuttons" click displayoutput href "#fn-displayoutput" click checkwincondition href "#fn-checkwincondition" click displaylocation href "#fn-displaylocation" click exit-button-loop href "#sub-exit-button-loop" click item-button-loop href "#sub-item-button-loop" click godirection href "#fn-godirection" click takeitem href "#fn-takeitem" click language-selection href "#sub-language-selection" click dom-creation-loop href "#sub-dom-creation-loop"

❓ Frequently Asked Questions

What visual experience does 'The Lost Relic' sketch provide?

The sketch creates a cozy, illustrated text-based adventure interface, featuring detailed descriptions of various locations like the Temple Entrance and Treasure Room.

How can players interact with the game in 'The Lost Relic'?

Users can click on choices or type commands to navigate between rooms, collect items, and uncover the story of the ancient relic.

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

This sketch demonstrates localization for multiple languages and interactive storytelling through a text-based adventure format, allowing for dynamic user engagement.

Preview

This game is broken someone fix it, Theres a game after this i promise - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of This game is broken someone fix it, Theres a game after this i promise - Code flow showing preload, setup, draw, startgame, formattext, getlocalizedtext, displayoutput, processcommand, clearchoicebuttons, createchoicebuttons, displaylocation, takeitem, godirection, displayinventory, displayhelp, checkwincondition
Code Flow Diagram