Lesson 12

Dialogue and quests: give the player a reason to keep going.

A story game does not need a giant engine. It needs a clean way to show lines, remember what happened and move the player from one objective to the next without trapping them in confusing state.

IntermediateStory StateQuest Design35-50 minutes

1. Treat dialogue as a queue

A dialogue box is usually just a small queue of lines. The player presses Continue, the index moves forward, and the panel closes when the queue is empty. The important part is what happens after the final line. Sometimes you start a mission, sometimes you reward the player, sometimes you unlock a new room.

function openDialogue(speaker, lines, finished) {
    state.dialogue = {
        speaker: speaker,
        lines: lines.slice(),
        index: 0,
        finished: finished || null
    };
    dialogueSpeaker.textContent = speaker;
    dialogueText.textContent = state.dialogue.lines[0];
    dialoguePanel.hidden = false;
}

function advanceDialogue() {
    if (!state.dialogue) return;

    state.dialogue.index += 1;
    if (state.dialogue.index < state.dialogue.lines.length) {
        dialogueText.textContent = state.dialogue.lines[state.dialogue.index];
        return;
    }

    const done = state.dialogue.finished;
    state.dialogue = null;
    dialoguePanel.hidden = true;
    if (typeof done === "function") done();
}

Source pattern: Supagames Little Sky Courier uses `openDialogue` and `advanceDialogue` to load cargo, finish deliveries and move into the ending sequence.

The callback is small but powerful. It lets the dialogue system remain generic while the quest system decides what changes. Without that separation, dialogue code quickly becomes a pile of special cases.

2. Keep quest state explicit

Quest state should be readable at a glance. Avoid hiding progress in many unrelated booleans. A courier game can track the current mission, cargo, destination and completed count. A mystery game can track collected evidence, solved puzzles and seen dialogues. A dungeon game can track floor, key count, opened gates and boss defeated.

const saveData = {
    completed: 0,
    coins: 0,
    upgrades: { fuel: 0, hull: 0 },
    seenDialogues: {},
    ending: false
};

function currentMission() {
    return MISSIONS[Math.min(saveData.completed, MISSIONS.length - 1)];
}

function completeMission(reward) {
    saveData.coins += reward;
    saveData.completed += 1;
    saveData.ending = saveData.completed >= MISSIONS.length;
    persistSave();
}

When a bug report says "after the first quest nothing happens," explicit state gives you somewhere to look. Is `completed` increasing? Is the next mission available? Did the dialogue callback run? This is much easier than guessing which scene variable got stuck.

3. Write objectives that map to actions

A quest description should not only be flavor text. It should tell the player what verb to use. "Find the missing hour" is atmospheric, but "Collect three witness notes, then return to the clock room" is playable. Strong games can use both: a title for mood and a checklist for action.

const QUESTS = [
    {
        id: "bakery",
        title: "Warm Bread for the Watch",
        objective: "Deliver bread to Moss Bakery",
        startNpc: "Mara",
        finishNpc: "Old Fen",
        reward: 20,
        intro: [
            "The beacon chain is dark.",
            "Old Fen still has lamp oil, but his oven chimney is stuck."
        ],
        finish: [
            "You made it before the bread cooled.",
            "The watch can light the eastern tower again."
        ]
    }
];

Notice that the data object contains both story and mechanics. The UI can show `title` and `objective`; the dialogue system can show `intro` and `finish`; the reward system can use `reward`. One object becomes the source of truth for a mission.

4. Prevent repeated story triggers

Story triggers often run inside update loops or collision checks. If the player stands on an NPC or trigger zone for several frames, you do not want to start the same dialogue repeatedly. Track seen dialogue IDs or mark a trigger as consumed.

function showStoryBeat(id) {
    if (saveData.seenDialogues[id]) return;
    const beat = STORY_BEATS[id];
    if (!beat) return;

    saveData.seenDialogues[id] = true;
    persistSave();
    openDialogue(beat.speaker, beat.lines, beat.after);
}

function updateNpcTrigger(player, npc) {
    if (rectsCollide(player, npc) && keys.interact) {
        showStoryBeat(npc.dialogueId);
    }
}

This pattern also helps with saving. If the player reloads after a scene, they should not be forced to repeat every introductory line unless it is important. For long games, repeated dialogue is one of the fastest ways to make the game feel unfinished.

5. Pause carefully during dialogue

Dialogue can pause action, but it should not break input. If your game uses Space or Enter for gameplay, make sure those keys advance dialogue only when the dialogue panel is open. If the comment form or search box is focused, do not hijack the key. This lesson connects directly to the input lesson: global keyboard handlers must respect forms.

function isTypingTarget(el) {
    return el && (
        el.tagName === "INPUT" ||
        el.tagName === "TEXTAREA" ||
        el.isContentEditable
    );
}

document.addEventListener("keydown", function (event) {
    if (isTypingTarget(event.target)) return;

    if (state.dialogue && (event.key === " " || event.key === "Enter")) {
        event.preventDefault();
        advanceDialogue();
        return;
    }

    keys[event.key.toLowerCase()] = true;
});

This exact concern appeared in Supagames comment and control fixes: a game should not steal the spacebar while the player is writing. Story UI and site UI must cooperate.

6. Make the story visible in the HUD

Players should not need to remember every line. Show the current objective in the HUD, pause menu or quest log. For short games, one line is enough. For bigger games, include completed steps and hints. The goal is not to remove discovery; it is to prevent confusion after an interruption.

A good quest loop is simple: introduce a problem, give the player a clear action, let gameplay create friction, then reward success with a small story change. When that loop works, even a small canvas game can hold attention far longer than a bare score chase.

Previous: Audio for browser games. Next: Platformer feel and coyote time.