# 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](DIALOGUE_SYSTEM_FLOWCHART.svg) ## 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 ```mermaid 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`