Lesson 28

Dialogue trees: choices that change state, not just text.

A dialogue tree is a map of conversation nodes. It becomes a game system when choices set flags, unlock quests, spend items or change future options.

IntermediateNarrative DesignChoices40-60 minutes

1. Store dialogue as nodes

A node has speaker text and choices. Each choice points to another node or ends the conversation. This is more flexible than a plain line queue because the player can branch. Keep node ids stable so save data and tests can refer to them.

const dialogueTrees = {
    mara_intro: {
        speaker: "Mara",
        text: "The old route is dark. Can you carry one more parcel?",
        choices: [
            { label: "I can fly.", next: "accept_route", setFlag: "acceptedRoute" },
            { label: "What happened?", next: "explain_storm" },
            { label: "Not now.", next: null }
        ]
    },
    explain_storm: {
        speaker: "Mara",
        text: "The storm ate three beacon lamps. We need glow fruit before dawn.",
        choices: [{ label: "Then I will help.", next: "accept_route", setFlag: "acceptedRoute" }]
    }
};

Source pattern: Supagames story games already use dialogue queues and quest flags; dialogue trees extend that pattern with player choice and conditional branches.

2. Render choices from data

The UI should not hard-code buttons for each conversation. Render the choices in the current node. When the player clicks one, apply effects and move to the next node. This keeps writing and code separate.

function showNode(id) {
    const node = dialogueTrees[id];
    state.dialogueNode = id;
    speakerEl.textContent = node.speaker;
    textEl.textContent = node.text;
    choicesEl.innerHTML = "";
    for (const choice of visibleChoices(node.choices)) {
        const button = document.createElement("button");
        button.textContent = choice.label;
        button.addEventListener("click", () => chooseDialogue(choice));
        choicesEl.appendChild(button);
    }
    dialoguePanel.hidden = false;
}

Buttons should be real buttons, not canvas text, when the conversation is HTML. This helps keyboard focus and mobile taps.

3. Choices can set flags and rewards

A choice matters when it changes something. Flags are the simplest consequence. They can unlock a route, mark an NPC as helped, start a quest or change future dialogue.

function chooseDialogue(choice) {
    if (choice.setFlag) save.flags[choice.setFlag] = true;
    if (choice.rewardCoins) save.coins += choice.rewardCoins;
    if (choice.startQuest) startQuest(choice.startQuest);
    persistSave();

    if (choice.next) {
        showNode(choice.next);
    } else {
        state.dialogueNode = null;
        dialoguePanel.hidden = true;
    }
}

Do not make every choice world-changing. Some choices can ask for context. But the main choices should affect goals, resources or relationships.

4. Conditions keep branches relevant

Some choices should appear only if the player has an item, knows a clue or finished a mission. Store conditions as small strings or functions. For simple games, string flags are easier to save and inspect.

function visibleChoices(choices) {
    return choices.filter(choice => {
        if (!choice.requiresFlag) return true;
        return !!save.flags[choice.requiresFlag];
    });
}

const clueChoice = {
    label: "Show the silver compass.",
    next: "compass_reveal",
    requiresFlag: "foundCompass",
    setFlag: "maraTrustsPlayer"
};

When a hidden choice becomes visible, the player feels that earlier exploration mattered. That is the core appeal of branching story.

5. Avoid branch explosion

Every branch costs writing, testing and maintenance. Use branches that rejoin after meaningful differences. For example, the player can accept a quest bravely or cautiously, setting a tone flag, but both routes can lead to the same mission. Later, the tone flag can change one reward line.

const acceptRoute = {
    speaker: "Mara",
    text: "Then take the blue parcel. The wind will argue, but the route remembers you.",
    choices: [
        { label: "Launch.", next: null, startQuest: "blueParcel" }
    ]
};

Branching story is not about infinite paths. It is about enough agency that the player sees themselves in the route.

6. Test dialogue like gameplay

Dialogue trees can break just like maps. A choice can point to a missing node. A condition can hide every choice. A flag can be misspelled. Add a small validation script that walks every node and checks references.

function validateDialogue(tree) {
    const errors = [];
    for (const [id, node] of Object.entries(tree)) {
        if (!node.text || !Array.isArray(node.choices)) errors.push(id + " is malformed");
        for (const choice of node.choices) {
            if (choice.next && !tree[choice.next]) {
                errors.push(id + " points to missing node " + choice.next);
            }
        }
    }
    return errors;
}

Story bugs are especially visible because players read them slowly. Validate the structure before asking people to care about the writing.

7. Give each character a job in the system

A useful dialogue character does more than speak. One NPC can teach controls, another can sell upgrades, another can reveal map lore and another can react to quest flags. This keeps story connected to play. If every character only says flavor text, players learn to skip conversations. If every character changes a variable, the world feels responsive.

Write a small character brief next to the tree: role, tone, secret, gameplay purpose and first reward. That brief helps future edits stay consistent. It also prevents dialogue from becoming interchangeable. A baker, a ghost captain and a repair drone should not all speak with the same rhythm or offer the same type of choice.

Previous: Card game mechanics. Next: Stealth mechanics.