FarmHeart

Dialog · Narrative · Quests · 9 min read · October 8, 2026

Dialog and Narrative Systems for Browser Games

Dialog and narrative systems transform a game from a mechanical exercise into a story. Whether it’s a simple shopkeeper conversation or a branching quest with consequences, the underlying system needs to be data-driven, extensible, and easy for non-programmers to author content for.

Dialog Data Format

Store dialog as data, not code. A simple JSON format that covers most needs:

{
  "npc_mayor": {
    "greeting": {
      "text": "Welcome to Willowbrook, farmer!",
      "speaker": "Mayor Oakley",
      "portrait": "mayor_happy",
      "choices": [
        { "text": "What can I do here?", "next": "tutorial_intro" },
        { "text": "I'm just passing through.", "next": "farewell" }
      ]
    },
    "tutorial_intro": {
      "text": "This town needs a good farmer. The old fields are overgrown, but with some work they'll yield fine crops.",
      "speaker": "Mayor Oakley",
      "portrait": "mayor_serious",
      "next": "tutorial_quest",
      "setFlag": "met_mayor"
    },
    "farewell": {
      "text": "Safe travels, friend.",
      "speaker": "Mayor Oakley",
      "portrait": "mayor_wave"
    }
  }
}

The Dialog Engine

class DialogEngine {
  constructor(dialogData) {
    this.data = dialogData;
    this.current = null;
    this.flags = new Set();
  }

  start(npcId, nodeId = 'greeting') {
    this.current = this.data[npcId]?.[nodeId];
    if (!this.current) return null;
    if (this.current.setFlag) this.flags.add(this.current.setFlag);
    return this.current;
  }

  choose(choiceIndex) {
    if (!this.current?.choices) return null;
    const choice = this.current.choices[choiceIndex];
    if (!choice?.next) return null;
    // Find the next node in the same NPC's dialog
    return this.start(this.currentNpc, choice.next);
  }

  advance() {
    if (this.current?.next) {
      return this.start(this.currentNpc, this.current.next);
    }
    return null; // dialog ended
  }
}

Conditional Dialog

Real games need dialog that changes based on player state:

"quest_check": {
  "branches": [
    {
      "condition": { "flag": "has_golden_pumpkin" },
      "text": "You found it! The Golden Pumpkin!",
      "next": "quest_complete"
    },
    {
      "condition": { "item": "pumpkin", "min": 5 },
      "text": "Those are nice pumpkins, but not THE pumpkin...",
      "next": "quest_hint"
    },
    {
      "default": true,
      "text": "Still looking for that Golden Pumpkin?",
      "next": "quest_reminder"
    }
  ]
}

The engine evaluates branches top-to-bottom and uses the first matching condition. The default branch catches everything else.

Typewriter Effect

Characters appearing one at a time is a classic game dialog feel:

class Typewriter {
  constructor(element, speed = 30) {
    this.el = element;
    this.speed = speed;
    this.text = '';
    this.index = 0;
    this.timer = null;
    this.done = false;
  }

  start(text) {
    this.text = text;
    this.index = 0;
    this.done = false;
    this.el.textContent = '';
    this.timer = setInterval(() => {
      if (this.index >= this.text.length) {
        clearInterval(this.timer);
        this.done = true;
        return;
      }
      this.el.textContent += this.text[this.index++];
    }, this.speed);
  }

  skip() {
    clearInterval(this.timer);
    this.el.textContent = this.text;
    this.done = true;
  }
}

Quest System Integration

Quests tie dialog to gameplay objectives:

Branching Narrative Best Practices

Localization

If you plan to translate your game, separate text from dialog logic from the start:

Related Articles

Ready to Play?

Jump into FarmHeart — free, no download, runs in your browser.

Play FarmHeart