Worldshaper/docs/DIALOGUE_SYSTEM_RUNTIME.md
2026-06-26 18:18:14 -04:00

7 KiB

Dialogue System Runtime Spec

This document is a code-aligned reference for how dialogue is parsed, evaluated, and executed in-game.

Quick Visual

Dialogue Runtime Flowchart

1) Data Model (Current Canonical Shape)

Dialogue nodes are authored primarily with arrays:

  • conditions[]
    • text
    • conditionType
    • conditionValue (for item, use itemId:quantity)
    • conditionStepId
    • conditionNot
    • nextId
  • reactions[]
    • reactionType
    • reactionValue
  • choices[]
    • text
    • nextId
    • conditionType
    • conditionValue
    • conditionStepId
    • conditionNot
    • reactionType
    • reactionValue

Compatibility fields still exist in runtime structures (text, conditionType, conditionValue, conditionStepId, conditionNot, nextId, reactionType, reactionValue) and are used as fallback when needed.

2) Parsing Order (JSON -> Runtime Content)

Parsing entrypoint:

  • ContentManager::loadAll()
  • readDialogueNodes() for each NPC

Per dialogue node, parsing order is:

  1. Read legacy/base fields first (id, text, legacy condition/reaction/next/order fields).
  2. Read conditions[].
    • Each condition uses condition.text, defaulting to base node.text if missing.
  3. If conditions[] is empty, synthesize exactly one condition from legacy/base fields.
  4. Read reactions[].
  5. If reactions[] is empty, only synthesize a fallback reaction if legacy reaction fields were present in JSON.
    • This allows intentionally empty reaction arrays.
  6. Read choices[].
    • Supports migration path for very old choice payloads missing conditionType.

3) Runtime Graph Build Order

At interaction start (E near NPC):

  1. openNpcDialogue() calls updateQuestProgression().
  2. NPC node defs are converted to runtime game::DialogueNode objects.
  3. Conditions, reactions, and choices are copied into runtime node vectors.
  4. Start node is selected with selectDialogueStartNodeId().
  5. DialogueSession::openGraph() is called.
  6. If a start node exists, node reactions are applied immediately (applyNodeReactions()).

4) Start Node Selection Logic

selectDialogueStartNodeId() order:

  1. If defaultDialogueNodeId exists and is valid, use it.
  2. Else, sort nodes by order, then id.
  3. Build inbound edge set from:
    • node nextId
    • choice nextId
  4. Candidate roots are nodes with no inbound references.
    • If none, all nodes become candidates.
  5. Filter candidates by dialogueNodeMatchesContext().
  6. Pick highest dialogueNodePriority().
    • Priority: quest_step_completed > quest_started/quest_completed > item/flag > default.
  7. Tie-break by smaller order.
  8. If no contextual match, fall back to first sorted node.

5) Condition Evaluation Order

Core function: doesConditionPass(type, value, stepId, conditionNot).

Evaluation sequence:

  1. Evaluate base condition type.
  2. Supported types include:
    • always
    • flag
    • item (supports itemId:quantity; quantity defaults to 1 if omitted)
    • level
    • currency (supports key:amount format)
    • skill
    • quest_started, quest_completed
    • quest_step_completed
  3. Apply NOT inversion if conditionNot == true.

Node-level match order:

  • resolveMatchedNodeCondition(node) scans node.conditions from index 0 upward.
  • First passing condition wins.

That winning condition is then used for:

  • Display text via resolveNodeConditionalText() (condition.text, else legacy node.text).
  • Continue target via resolveNodeConditionalNext() (condition.nextId, else legacy node.nextNodeId).

6) Choice Visibility and Selection Order

Choice visibility:

  • buildVisibleChoiceIndices(node) iterates choices in list order.
  • Includes only choices where doesChoiceMeetConditions(choice) returns true.

When user presses numeric choice key:

  1. Map key 1..9 to visible index.
  2. Resolve real choice index from visibleChoiceIndices.
  3. Apply choice reaction first (applyDialogueReaction).
  4. Resolve target with resolveChoiceTarget(choice):
    • If condition passes -> nextId
    • Else close
  5. Advance to target with advanceTo().
  6. Apply entered-node reactions.
  7. updateQuestProgression().

7) Reactions Execution Order

applyNodeReactions(node):

  1. If node.reactions is non-empty, execute each in array order.
  2. Else fallback to legacy single reaction fields.

applyDialogueReaction(type, value) supports:

  • grant_flag / grant_quest_flag
  • grant_item using itemId:quantity values such as copper_ore:1
  • start_quest
  • complete_quest

8) Input Execution Paths

A) Dialogue open + no choices

  • Continue key (E / Enter / Space):
    1. Resolve condition-based next node
    2. Advance or close
    3. Apply entered-node reactions (if advanced)

B) Dialogue open + choices exist

  • If visible choices exist:
    • Number keys choose branch
  • If no visible choices:
    • Continue key uses node condition-based next/close path

C) Escape

  • Esc closes dialogue immediately.

9) Rendering Order (Dialogue Box)

renderDialogueBox() draws:

  1. Speaker bar
  2. Body text from resolveNodeConditionalText()
  3. Visible choices list (if any)
  4. Footer hint:
    • [E] Continue if condition-resolved next exists
    • [E] Close otherwise

10) Flowchart

flowchart TD
  A[Player presses E near NPC] --> B[openNpcDialogue]
  B --> C[updateQuestProgression]
  C --> D[Build runtime graph from content nodes]
  D --> E[selectDialogueStartNodeId]
  E --> F[DialogueSession.openGraph]
  F --> G{Start node exists?}
  G -- No --> Z[Close]
  G -- Yes --> H[applyNodeReactions on start node]

  H --> I[Dialogue loop]
  I --> J{Current node has choices?}

  J -- No --> K{Continue key pressed?}
  K -- No --> I
  K -- Yes --> L[resolveNodeConditionalNext via first passing condition]
  L --> M{next exists?}
  M -- No --> Z
  M -- Yes --> N[advanceTo next]
  N --> O[applyNodeReactions on entered node]
  O --> I

  J -- Yes --> P[buildVisibleChoiceIndices]
  P --> Q{Any visible choices?}

  Q -- No --> R{Continue key pressed?}
  R -- No --> I
  R -- Yes --> L

  Q -- Yes --> S{Number key 1..9?}
  S -- No --> I
  S -- Yes --> T[Map visible index -> actual choice]
  T --> U[apply choice reaction]
  U --> V[resolveChoiceTarget]
  V --> W{target exists?}
  W -- No --> Z
  W -- Yes --> X[advanceTo target]
  X --> Y[applyNodeReactions on entered node]
  Y --> I

11) Practical Authoring Implications

  • Condition order is behavior-critical. Put the most specific conditions first.
  • Item conditions should be authored as itemId:quantity so runtime quantity checks are explicit.
  • In the editor, conditionType=item surfaces item picker + quantity input and writes conditionValue in that format.
  • Dialogue line text should be authored on condition entries (conditions[].text).
  • Empty reactions[] is valid and now preserved.
  • Choice order affects both visual order and numeric selection mapping.

12) Primary Source Files

  • src/content/ContentManager.cpp
  • src/game/Game.cpp
  • src/game/Dialogue.cpp
  • src/content/ContentTypes.hpp
  • src/game/Dialogue.hpp