gameplay/behaviors.js

Gameplay — Dialogue

Branching dialogue trees — node structure, conditions, side effects, and authoring guide.

Gameplay — Dialogue

DialogueBehavior renders a branching conversation with an NPC. Dialogue is defined as a tree of nodes — each node has text, and optionally a list of player choices. Choices can be conditionally hidden and can trigger side effects (start quest, give item, set flag, etc.).

Dialogue Tree Structure

dialogue: {
  npcName: 'Elder Bjorn',
  tree: [
    // node 0 — entry point (always first)
    {
      id:   'root',
      text: 'Welcome back, traveller.',
      choices: [
        {
          label:  'Tell me about the goblins.',
          next:   'goblin_info',
        },
        {
          label:     'I have dealt with the goblins.',
          condition: { type: 'questStatus', questId: 'goblin_bounty', status: 'complete' },
          effect:    { type: 'turnInQuest', questId: 'goblin_bounty' },
          next:      'quest_complete',
        },
        {
          label: 'Nothing, goodbye.',
          next:  null,   // null closes the dialogue
        },
      ],
    },

    {
      id:   'goblin_info',
      text: 'They come from the eastern caves. Be careful.',
      choices: [],       // empty choices = "OK" button, then close
    },

    {
      id:   'quest_complete',
      text: 'Excellent work! Here is your reward.',
      choices: [],
    },
  ]
}

The conversation always starts at the first node (tree[0]). The NPC’s text is displayed; the player’s choices appear below. Clicking a choice runs its effect, then navigates to its next node. next: null closes the popup.

Conditions

Conditions hide a choice unless their expression is true. If a condition evaluates to false, the choice is invisible (not grayed out).

typeRequired fieldsDescription
hasFlagflag, valuegameMgr.flags.get(flag) === value
questStatusquestId, statusQuest has this status
hasItemitemId, countPlayer inventory has ≥ count
hasGoldamountPlayer gold ≥ amount
playerLevelminLevelPlayer level ≥ minLevel
noConditionAlways shown (default when condition omitted)
// Show "I'm ready to fight" only after the player reaches level 3
{
  label:     "I'm ready to fight.",
  condition: { type: 'playerLevel', minLevel: 3 },
  next:      'fight_accepted',
}

Side Effects

Effects fire immediately when the player selects a choice (before navigating to next).

Single Effects

// Set a world flag
effect: { type: 'setFlag', flag: 'bridge_rebuilt', value: true }

// Start a quest
effect: { type: 'startQuest', questId: 'goblin_bounty' }

// Turn in a completed quest (grants rewards)
effect: { type: 'turnInQuest', questId: 'goblin_bounty' }

// Give and equip an item (Armorer pattern — no loot window)
effect: { type: 'equipItem', itemId: 'iron_sword', slot: 'mainhand' }

// Deduct gold (pair with equipItem to charge for direct equips)
effect: { type: 'spendCurrency', currency: 'gold', amount: 50 }

Multiple Effects

// Apply several effects at once
effect: {
  type: 'multi',
  effects: [
    { type: 'spendCurrency', currency: 'gold', amount: 50 },
    { type: 'equipItem', itemId: 'iron_shield', slot: 'offhand' },
    { type: 'setFlag', flag: 'armorer_visited', value: true },
  ]
}

Authoring Tips

Keep entry node neutral. The first node is shown every time the player talks to the NPC. Use it as a hub with conditionally filtered choices, not a one-time intro.

// Good — entry is always valid, choices filter by quest state
{
  id: 'root',
  text: 'How can I help you?',
  choices: [
    { label: 'Tell me about your quests.', next: 'quest_list' },
    { label: '[Quest turn-in]', condition: { type: 'questStatus', ... }, effect: { ... }, next: 'reward' },
    { label: 'Nothing.', next: null },
  ],
}

One phase = one interaction. Because advancePhase() only completes a quest on the last phase, put the quest’s completion trigger (kill count or NPC interaction) on the last and only phase. See Gameplay — Quests for the full explanation.

Use null for next to close. Any choice with next: null closes the dialogue immediately after applying its effect.

Test condition visibility carefully. Conditions hide choices but don’t block interaction. If you need to prevent interaction entirely (e.g., a gate that requires a key), use a TriggerZoneBehavior with a flag condition, not dialogue.

The dialogue popup renders in the interactive popup type with a Talk tab. Other tabs (Missions, Craft, etc.) appear alongside it if the same entity also has MissionGiverBehavior, CrafterBehavior, etc.

The dialogue window shows:

  • NPC name (top)
  • Current node text (middle)
  • Choice buttons (bottom) — one button per visible, condition-passing choice

A choice with no text just shows an “OK” button.