TypeScript Tutorial: Building a Simple Web-Based Interactive Story Game

Have you ever wanted to create your own interactive story game, where the reader’s choices shape the narrative? In this tutorial, we’ll dive into the world of TypeScript and build a simple, yet engaging, web-based interactive story. This project is perfect for beginners and intermediate developers looking to enhance their TypeScript skills while creating something fun and interactive.

Why Build an Interactive Story Game?

Interactive stories are a fantastic way to learn programming. They allow you to apply core programming concepts like variables, conditional statements, and functions in a creative and engaging way. Plus, you get the satisfaction of building something playable and fun! This tutorial will help you understand how to structure your code, manage user input, and control the flow of a narrative.

Prerequisites

Before we begin, make sure you have the following:

  • Node.js and npm (Node Package Manager) installed on your system.
  • A basic understanding of HTML, CSS, and JavaScript.
  • A code editor (like Visual Studio Code) to write your code.

Setting Up Your Project

Let’s start by setting up our project. Open your terminal or command prompt and follow these steps:

  1. Create a new project directory: mkdir interactive-story
  2. Navigate into the directory: cd interactive-story
  3. Initialize a new Node.js project: npm init -y (This creates a package.json file).
  4. Install TypeScript: npm install typescript --save-dev
  5. Initialize a TypeScript configuration file: npx tsc --init (This creates a tsconfig.json file).

Your project structure should now look something like this:

interactive-story/
├── node_modules/
├── package.json
├── package-lock.json
├── tsconfig.json
└──

Configuring TypeScript

Open tsconfig.json in your code editor. This file tells the TypeScript compiler how to compile your code. Here are some key configurations to consider:

{
  "compilerOptions": {
    "target": "ES2015", // or a later version like ES2018 or ES2020
    "module": "commonjs", // or "esnext" if you're using ES modules
    "outDir": "./dist", // Where compiled JavaScript files will be placed
    "rootDir": "./src", // Where your TypeScript source files are located
    "strict": true, // Enables strict type checking
    "esModuleInterop": true, // Allows you to import CommonJS modules as ES modules
    "skipLibCheck": true, // Skips type checking of declaration files
    "forceConsistentCasingInFileNames": true // Enforces consistent casing
  },
  "include": ["src/**/*"]
}

Make sure to create a src directory where you will put your TypeScript files.

Creating the HTML Structure

Create an index.html file in your project directory. This file will contain the basic structure of your interactive story game.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Interactive Story</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div id="story-container">
        <p id="story-text"></p>
        <div id="choices-container"></div>
    </div>
    <script src="dist/app.js"></script>
</body>
</html>

In this HTML, we have:

  • A title for the page.
  • A link to a style.css file (we’ll create this later for styling).
  • A story-container div to hold the entire story.
  • A story-text paragraph to display the story text.
  • A choices-container div to hold the choices the user can make.
  • A script tag that links to our compiled JavaScript file (dist/app.js).

Styling with CSS (style.css)

Create a style.css file in your project directory. This file will contain the CSS styling for your game. Here’s a basic example:

body {
    font-family: sans-serif;
    margin: 20px;
    background-color: #f0f0f0;
}

#story-container {
    background-color: #fff;
    padding: 20px;
    border-radius: 5px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

#story-text {
    margin-bottom: 15px;
}

#choices-container button {
    display: block;
    margin-bottom: 10px;
    padding: 10px;
    border: none;
    background-color: #4CAF50;
    color: white;
    cursor: pointer;
    border-radius: 3px;
}

#choices-container button:hover {
    background-color: #3e8e41;
}

Writing the TypeScript Code (app.ts)

Now, let’s write the core TypeScript code for our interactive story. Create an app.ts file inside the src directory. This file will contain the logic for the story, handling user choices, and updating the display.


// Define a type for the story node
interface StoryNode {
    text: string;
    choices?: Choice[]; // optional choices
    next?: string; // next node id
}

// Define a type for a choice
interface Choice {
    text: string;
    next: string;
}

// Define the story data
const storyData: { [key: string]: StoryNode } = {
    start: {
        text: "You wake up in a dark forest. You hear a rustling in the bushes. What do you do?",
        choices: [
            {
                text: "Investigate the noise.",
                next: "investigate"
            },
            {
                text: "Run away.",
                next: "run"
            }
        ]
    },
    investigate: {
        text: "You cautiously approach the bushes and see a small, injured animal. Do you help it?",
        choices: [
            {
                text: "Yes, help the animal.",
                next: "helpAnimal"
            },
            {
                text: "No, leave it.",
                next: "leaveAnimal"
            }
        ]
    },
    run: {
        text: "You run as fast as you can and stumble upon a clearing. The story ends here."
    },
    helpAnimal: {
        text: "The animal is grateful and leads you to a hidden path. You find a treasure!"
    },
    leaveAnimal: {
        text: "You continue wandering the forest, lost and alone. The story ends here."
    }
};

// Get the story text and choices elements from the DOM
const storyTextElement = document.getElementById('story-text') as HTMLParagraphElement;
const choicesContainerElement = document.getElementById('choices-container') as HTMLDivElement;

// Function to display a story node
function displayStoryNode(nodeId: string): void {
    const node = storyData[nodeId];

    if (!node) {
        storyTextElement.textContent = "The story has ended.";
        choicesContainerElement.innerHTML = "";
        return;
    }

    storyTextElement.textContent = node.text;
    choicesContainerElement.innerHTML = ""; // Clear previous choices

    if (node.choices) {
        node.choices.forEach(choice => {
            const button = document.createElement('button');
            button.textContent = choice.text;
            button.addEventListener('click', () => {
                displayStoryNode(choice.next);
            });
            choicesContainerElement.appendChild(button);
        });
    }
}

// Start the story
displayStoryNode('start');

Let’s break down the code:

  • Interfaces: StoryNode and Choice are interfaces that define the structure of our story nodes and choices, respectively. This helps with type safety and code readability.
  • Story Data: storyData is an object that holds the entire story. Each key represents a story node, and the value is an object containing the text and possible choices.
  • DOM Elements: We get references to the story-text paragraph and choices-container div from the HTML.
  • displayStoryNode Function: This function takes a nodeId as input, retrieves the corresponding story node from storyData, and updates the UI accordingly. It displays the story text and creates buttons for the user’s choices.
  • Event Listeners: Each choice button has an event listener that calls displayStoryNode with the ID of the next story node when clicked.
  • Starting the Story: We call displayStoryNode('start') to begin the story at the starting node.

Compiling and Running the Code

Now, compile your TypeScript code to JavaScript using the TypeScript compiler. Open your terminal in the project directory and run:

tsc

This command will compile your app.ts file into dist/app.js, as specified in your tsconfig.json. Now, open index.html in your web browser. You should see the first part of your story, with the choices presented as buttons. Clicking on the buttons will advance the story.

Adding More Story Elements

To make your story more engaging, you can add more story nodes, choices, and even incorporate variables to track the player’s progress or make decisions based on previous choices. Here’s how you can expand the story:

  1. Add More Nodes: Add new entries to the storyData object, each representing a new part of the story. Make sure each node has a unique ID.
  2. Connect Nodes with Choices: In existing nodes, add choices, each of which should have a next property that points to another node ID.
  3. Implement Conditional Logic: Use variables to track player choices or inventory. Then, use conditional statements (if/else) to change the story flow based on those variables.
  4. Add Styling: Enhance your CSS to improve the visual appeal of your story.

Example of adding a variable and conditional logic:


interface StoryNode {
    text: string;
    choices?: Choice[];
    next?: string;
    condition?: (playerState: PlayerState) => boolean; // added condition
}

interface PlayerState {
    hasSword: boolean;
}

const playerState: PlayerState = {
    hasSword: false
};

const storyData: { [key: string]: StoryNode } = {
    start: {
        text: "You are in a dark forest...",
        choices: [
            {
                text: "Go North",
                next: "north"
            },
            {
                text: "Go South",
                next: "south"
            }
        ]
    },
    north: {
        text: "You find a sword!",
        choices: [
            {
                text: "Take the sword",
                next: "takeSword"
            },
            {
                text: "Leave the sword",
                next: "leaveSword"
            }
        ]
    },
    takeSword: {
        text: "You have a sword now.",
        choices: [
            {
                text: "Continue",
                next: "continue1"
            }
        ]
    },
    leaveSword: {
        text: "You left the sword",
        choices: [
            {
                text: "Continue",
                next: "continue1"
            }
        ]
    },
    continue1: {
        text: "You encounter a troll.",
        choices: [
            {
                text: "Fight the troll",
                next: "fightTroll",
                condition: (state) => state.hasSword // add a condition
            },
            {
                text: "Run from the troll",
                next: "runFromTroll"
            }
        ]
    },
    fightTroll: {
        text: "You defeat the troll!"
    },
    runFromTroll: {
        text: "You run away from the troll."
    }
};

function displayStoryNode(nodeId: string): void {
    const node = storyData[nodeId];

    if (!node) {
        storyTextElement.textContent = "The story has ended.";
        choicesContainerElement.innerHTML = "";
        return;
    }

    storyTextElement.textContent = node.text;
    choicesContainerElement.innerHTML = "";

    if (node.choices) {
        node.choices.forEach(choice => {
            if (choice.condition && !choice.condition(playerState)) {
                return; // Skip choices that don't meet the condition
            }

            const button = document.createElement('button');
            button.textContent = choice.text;
            button.addEventListener('click', () => {
                if (choice.next) {
                    displayStoryNode(choice.next);
                }
            });
            choicesContainerElement.appendChild(button);
        });
    }

    //Update the player state based on choices
    switch (nodeId) {
        case "takeSword":
            playerState.hasSword = true;
            break;
    }
}

In this example, we’ve added a PlayerState interface to track the player’s inventory (e.g., whether they have a sword). We’ve also added a condition property to choices. The fightTroll choice is only displayed if the player has the sword. This allows you to create branching paths and more complex story interactions. Remember to update the playerState object based on the player’s choices.

Common Mistakes and How to Fix Them

Here are some common mistakes beginners make when building interactive stories and how to avoid them:

  • Typos in Node IDs: If you misspell a node ID in your next property, the story won’t advance. Double-check your IDs carefully.
  • Incorrect HTML Element Selection: Ensure you are selecting the correct HTML elements using document.getElementById(). If the ID in your JavaScript doesn’t match the ID in your HTML, your code won’t work.
  • Forgetting to Clear Choices: Make sure you clear the choices-container before displaying new choices. Otherwise, the old choices will remain, and the user will get confused. Use choicesContainerElement.innerHTML = "";.
  • Incorrect Pathing: Ensure the paths in your story make sense, and that the choices lead to the intended outcomes. It’s helpful to draw a flowchart or diagram to visualize your story’s structure.
  • Scope Issues: Be mindful of variable scope. Variables declared inside a function are only accessible within that function. If you need to access a variable globally, declare it outside of any function.
  • Type Errors: TypeScript helps prevent errors, but you still need to ensure your types are correct. Use the correct types for variables and function parameters. Carefully review the console for any type errors.

Key Takeaways

  • Structure is Key: Organize your story data logically using interfaces and objects to represent story nodes and choices.
  • Modular Code: Create functions to handle specific tasks (like displaying story nodes) to make your code more readable and maintainable.
  • User Experience: Think about how the user will interact with your story. Make the choices clear and the story engaging.
  • Testing and Iteration: Test your story frequently and make adjustments as you go. Iterate on your story and add new features.
  • Type Safety: Use TypeScript’s type system to catch errors early and write more robust code.

FAQ

  1. Can I use a different framework, like React or Vue? Yes, absolutely! This tutorial focuses on the core concepts using vanilla JavaScript and TypeScript. You can adapt the same principles to any JavaScript framework.
  2. How can I save the player’s progress? You can use local storage (localStorage) to save the player’s progress in the browser. You’ll need to save the player’s state (e.g., which node they’re on, their inventory) and load it when the game starts.
  3. How can I add images or multimedia? You can add images, videos, or audio to your story by adding HTML elements (<img>, <video>, <audio>) to your story text or choices. Make sure your CSS is formatted to handle the new elements.
  4. How do I debug my code? Use your browser’s developer tools (usually accessed by pressing F12). You can use the console to log variables, set breakpoints, and step through your code to identify and fix errors.
  5. How can I make the story more dynamic? You can incorporate random events, timers, or scoring systems to add more depth to your story.

By following these steps, you’ve created a basic interactive story game using TypeScript. This is just the beginning. You can expand on this foundation by adding more complex features, a richer narrative, and more interactive elements. Experiment with different story structures, choices, and conditional logic to create your own unique and engaging interactive experiences. The possibilities are endless, and the more you practice, the better you will become at crafting these interactive narratives. This project serves as a solid foundation for further exploration into game development and interactive content creation, opening doors to more complex and captivating projects.