Worldshaper/Release/docs/DIALOGUE_SYSTEM_RUNTIME.md

234 lines
7 KiB
Markdown
Raw Normal View History

2026-06-26 18:18:14 -04:00
# 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`