@motioneffector/dialogue

Documentation

Your First Dialogue

Build a working branching dialogue in about 5 minutes.

By the end of this guide, you'll have a 3-node conversation where the player can choose between two paths, each leading to a different ending.

What We're Building

A simple NPC greeting where the player can ask for help or say goodbye:

NPC: "Welcome! Need any help?"
  → "Yes, I'm lost" → NPC: "The exit is north." → END
  → "No thanks"     → NPC: "Safe travels!"     → END

Step 1: Define the Dialogue Structure

A dialogue needs three things: an id, a startNode, and a nodes object containing all the conversation nodes.

import { createDialogueRunner, DialogueDefinition } from '@motioneffector/dialogue'

const dialogue: DialogueDefinition = {
  id: 'npc-greeting',
  startNode: 'welcome',
  nodes: {
    welcome: {
      text: 'Welcome! Need any help?',
      choices: [
        { text: 'Yes, I\'m lost', next: 'help' },
        { text: 'No thanks', next: 'goodbye' }
      ]
    },
    help: {
      text: 'The exit is to the north. Good luck!',
      isEnd: true
    },
    goodbye: {
      text: 'Safe travels!',
      isEnd: true
    }
  }
}

Each node has text (what's displayed) and either choices (player options) or isEnd: true (conversation ends here).

Step 2: Create the Runner and Start

The DialogueRunner manages the dialogue state. Create one and call start() with your dialogue.

const runner = createDialogueRunner()
const state = await runner.start(dialogue)

console.log(state.currentNode.text)
// Output: "Welcome! Need any help?"

The state object tells you the current node, available choices, and whether the dialogue has ended.

Step 3: Display Choices

Get available choices with getChoices(). These are the options the player can select.

const choices = runner.getChoices()

choices.forEach((choice, index) => {
  console.log(`${index}: ${choice.text}`)
})
// Output:
// 0: Yes, I'm lost
// 1: No thanks

Step 4: Make a Choice

When the player selects an option, call choose() with the choice index.

const newState = await runner.choose(0)  // Player selects "Yes, I'm lost"

console.log(newState.currentNode.text)
// Output: "The exit is to the north. Good luck!"

console.log(newState.isEnded)
// Output: true

Step 5: Check for End

Use isEnded() to know when the conversation is complete.

if (runner.isEnded()) {
  console.log('Dialogue complete!')
}

The Complete Code

Here's everything together:

import { createDialogueRunner, DialogueDefinition } from '@motioneffector/dialogue'

const dialogue: DialogueDefinition = {
  id: 'npc-greeting',
  startNode: 'welcome',
  nodes: {
    welcome: {
      text: 'Welcome! Need any help?',
      choices: [
        { text: 'Yes, I\'m lost', next: 'help' },
        { text: 'No thanks', next: 'goodbye' }
      ]
    },
    help: {
      text: 'The exit is to the north. Good luck!',
      isEnd: true
    },
    goodbye: {
      text: 'Safe travels!',
      isEnd: true
    }
  }
}

async function runDialogue() {
  const runner = createDialogueRunner()
  let state = await runner.start(dialogue)

  while (!state.isEnded) {
    // Display current text
    console.log(`NPC: ${state.currentNode.text}`)

    // Display choices
    const choices = runner.getChoices()
    choices.forEach((choice, i) => console.log(`  ${i}: ${choice.text}`))

    // For this example, always pick the first choice
    state = await runner.choose(0)
  }

  // Display final text
  console.log(`NPC: ${state.currentNode.text}`)
  console.log('--- Dialogue Complete ---')
}

runDialogue()

What's Next?

Now that you have the basics: