@motioneffector/dialogue

Documentation

Actions

Actions are commands that execute when nodes are entered or choices are selected. They modify flags, trigger game callbacks, and enable dialogues to affect game state.

How It Works

Actions are defined in arrays on nodes or choices:

// Node actions: execute when entering the node
const node = {
  text: 'You found treasure!',
  actions: [
    { type: 'increment', flag: 'gold', value: 100 }
  ]
}

// Choice actions: execute when selecting the choice
const choice = {
  text: 'Bribe the guard',
  next: 'bribed',
  actions: [
    { type: 'decrement', flag: 'gold', value: 50 },
    { type: 'set', flag: 'guardBribed', value: true }
  ]
}

Actions execute in order. Node actions run before the node is displayed. Choice actions run before transitioning to the next node.

Basic Usage

import { createDialogueRunner } from '@motioneffector/dialogue'

const runner = createDialogueRunner({
  actionHandlers: {
    playSound: (args) => {
      const [soundName] = args as [string]
      console.log(`Playing sound: ${soundName}`)
    },
    giveItem: async (args) => {
      const [itemId, quantity] = args as [string, number]
      // Could be async database operation
      console.log(`Giving ${quantity}x ${itemId}`)
    }
  }
})

const dialogue = {
  id: 'reward',
  startNode: 'start',
  nodes: {
    start: {
      text: 'Here is your reward!',
      actions: [
        { type: 'increment', flag: 'gold', value: 100 },
        { type: 'callback', name: 'playSound', args: ['coins.wav'] },
        { type: 'callback', name: 'giveItem', args: ['sword', 1] }
      ],
      isEnd: true
    }
  }
}

Key Points

  • Action types: set, clear, increment, decrement, callback

  • Execution order: Actions run sequentially. Node actions before display, choice actions before transition.

  • Async support: Callback actions can be async. The runner awaits them.

  • Fail-fast: Missing callback handlers throw errors. Register all handlers before using them.

  • Flag scopes: Use conv:flagName for conversation flags, unprefixed for game flags.

Examples

Set Flag

{ type: 'set', flag: 'hasKey', value: true }
{ type: 'set', flag: 'playerName', value: 'Hero' }
{ type: 'set', flag: 'gold', value: 500 }
{ type: 'set', flag: 'conv:talkedOnce', value: true }  // Conversation flag

Clear Flag

{ type: 'clear', flag: 'tempBuff' }
{ type: 'clear', flag: 'conv:rememberedTopic' }

Removes the flag entirely. Subsequent condition checks for this flag return undefined.

Increment

{ type: 'increment', flag: 'visitCount' }              // +1
{ type: 'increment', flag: 'gold', value: 50 }         // +50
{ type: 'increment', flag: 'reputation', value: 10 }

If the flag doesn't exist, it's initialized to 0 before incrementing.

Decrement

{ type: 'decrement', flag: 'gold', value: 100 }
{ type: 'decrement', flag: 'health', value: 25 }
{ type: 'decrement', flag: 'stamina' }  // -1

Callback

{ type: 'callback', name: 'handlerName' }
{ type: 'callback', name: 'playSound', args: ['victory.mp3'] }
{ type: 'callback', name: 'addToInventory', args: ['potion', 3] }

Callbacks invoke registered action handlers:

const runner = createDialogueRunner({
  actionHandlers: {
    playSound: (args) => {
      const [fileName] = args as [string]
      audioEngine.play(fileName)
    },
    addToInventory: async (args) => {
      const [item, count] = args as [string, number]
      await inventory.add(item, count)
    },
    triggerCutscene: (args) => {
      const [cutsceneId] = args as [string]
      cutsceneManager.play(cutsceneId)
    }
  }
})

Multiple Actions

const node = {
  text: 'You completed the quest!',
  actions: [
    { type: 'set', flag: 'quest1Complete', value: true },
    { type: 'increment', flag: 'gold', value: 200 },
    { type: 'increment', flag: 'experience', value: 50 },
    { type: 'callback', name: 'playSound', args: ['quest_complete.wav'] },
    { type: 'callback', name: 'showAchievement', args: ['First Quest'] }
  ],
  isEnd: true
}

Actions on Choices

const node = {
  text: 'The merchant offers you a deal.',
  choices: [
    {
      text: 'Accept (pay 100 gold)',
      next: 'accepted',
      conditions: { check: ['gold', '>=', 100] },
      actions: [
        { type: 'decrement', flag: 'gold', value: 100 },
        { type: 'set', flag: 'hasMerchantDeal', value: true }
      ]
    },
    {
      text: 'Decline',
      next: 'declined'
    }
  ]
}

Listening to Actions

const runner = createDialogueRunner({
  onActionExecuted: (action, result) => {
    console.log(`Action executed: ${action.type}`, result)
  }
})

Related