Back to editor

Dialogue Forge

A visual editor for building branching dialogue trees for games and interactive fiction. Design conversations with a node graph, then export structured JSON your game engine can traverse at runtime.

The Interface

Sidebar

Drag node types onto the canvas or click a template to load a starter project.

Canvas

Your graph lives here. Pan with Space+drag, zoom with the scroll wheel.

Inspector

Click any node to open its detail panel on the right — edit name, dialogue, emotion, and custom attributes.

Validation bar

Runs continuously. Surfaces errors (disconnected nodes, empty dialogue) and warnings at the bottom of the screen.

Node Types

Every node is either a Character node or an Action node.

Character node

Represents a speaker delivering a line of dialogue. Fill in the character name, dialogue text, and optional emotion. Add custom attributes (numbers, dropdowns, flags) to track game state — e.g. Courage: 7 or Faction: Rebel.

Action node types

Branch

Presents outgoing edges as labelled player choices. Each edge label (double-click to edit) becomes a selectable option in your game UI.

Trigger

Fires a game event and auto-advances. Use it to unlock quests, set flags, or award items. Attach a Flag Name attribute to name the event.

Jump

Redirects the flow to another part of the graph — useful for looping back or skipping ahead without drawing a long edge.

End

Terminates the conversation. Connect the final node in every path to an End node so your runtime knows when to close the dialogue window.

Building a Graph

  1. 1Drag a Character or Action node from the sidebar onto the canvas.
  2. 2Click a node to open its inspector. Edit name, dialogue, and any attributes.
  3. 3Connect nodes by hovering over a node's right handle until the cursor changes, then drag to another node's left handle.
  4. 4For Branch nodes, double-click each outgoing edge label to type the player choice text.
  5. 5Every conversation path must end at an End action node. The validation bar will flag open paths.

Selection & Multi-Select

Click — select a single node and open it in the inspector.

Shift + Click — add a node or edge to the current selection.

Shift + drag on the canvas — draw a selection rectangle.

CtrlC — copy all selected nodes and the edges that connect them to each other.

CtrlV — paste the copied subgraph offset by 40 px. All internal edges are recreated with fresh IDs; connections to nodes outside the selection are not copied.

Del — delete all selected nodes and/or selected edges in one action.

Click an edge to select it, then press Del to remove just that edge without touching its nodes.

Keyboard Shortcuts

Undo
CtrlZ
Redo
CtrlY
Duplicate selected node
CtrlD
Copy selected nodes (preserves edges)
CtrlC
Paste copied subgraph
CtrlV
Search nodes
CtrlF
Auto layout
CtrlL
Save / export JSON
CtrlS
Delete selected node(s) or edge(s)
Del
Deselect / close panels
Esc
Pan the canvas
Space+ drag
Multi-select (click or drag)
Shift

Saving & Loading

Save / Export

Clicking Save (or pressing CtrlS) downloads a .forge.json file. This is your project file — keep it somewhere safe.

Import

Click the Import button and select a previously saved .forge.json (or any compatible JSON). If the canvas has content you'll be asked to confirm before replacing it.

Preview

The Preview button runs the dialogue in a lightweight simulator so you can walk through every branch before exporting.

Variables

Variables define the gameplay state that your dialogue can read and react to. Declare them in the Variables panel and they become available across every node in the project.

number

Integer values. Use for counters, scores, health points, or any whole-number stat.

float

Decimal numbers. Use for percentages, probabilities, or any value that needs fractional precision — e.g. a relationship score of 0.75.

boolean

True/false flags. Use for quest completion, feature toggles, or binary state — e.g. hasMetKing: true.

string

Text values. Use for character names, faction tags, dialogue keys, or any free-form label.

list

An ordered array of strings. Use for inventories, unlocked skills, completed objectives, or any variable-length collection.

object

Key-value pairs. Use for structured data like a stats block, an item with multiple properties, or a nested config bag.

Variable Interpolation

Reference any variable directly inside a Character node's dialogue text using {variableName} syntax. Tokens are resolved at preview and runtime — the raw text is stored as-is in the JSON.

Type "{" to trigger autocomplete

While editing dialogue, type an opening brace { to open an autocomplete dropdown listing every variable in the project. Select one to insert the full token.

Insert Variable button

Use the Insert Variable button in the node inspector to browse and insert a variable token without typing braces manually.

Nested paths

Object properties and list metadata are accessible with dot notation: {stats.level} reads the level key of an object variable, and {inventory.length} yields the number of items in a list variable.

Examples

"Greetings, {playerName}. You have {inventory.length} items."
"Your current level is {stats.level} and your speed is {stats.speed}."
"The password is {secretCode}."

Conditions

Attach conditions to edges to control which paths are available at runtime. An edge is only traversable when all its conditions evaluate to true against the current variable state.

Number & Float operators

equals / notEquals

Exact numeric match or mismatch

greaterThan / lessThan

Exclusive comparison

greaterThanOrEqual / lessThanOrEqual

Inclusive comparison

between / notBetween

Inclusive range check — e.g. gold between 10 and 50

Boolean operators

isTrue

Variable is true

isFalse

Variable is false

String operators

equals / notEquals

Case-sensitive match

contains / notContains

Substring presence

startsWith / endsWith

Prefix or suffix

isEmpty / isNotEmpty

Blank string check

List operators

listContains

List includes a specific string — e.g. inventory listContains "sword"

listIsEmpty / listIsNotEmpty

Zero-length check on the list

Object operators

hasProperty

Object has a given key — e.g. playerData hasProperty "level"

hasNoProperty

Object does not have a given key

State Preview

The in-editor preview simulator evaluates variable interpolation and conditions against a live state snapshot. You can set variable overrides before running to test specific branches.

number / float / boolean / string — displayed as their raw value in the state inspector.

list — shown as N items (e.g. 3 items) to keep the panel readable. Expand the variable to see the individual entries.

object — shown as N keys (e.g. 4 keys) in the collapsed view. Expand to browse individual properties.

Dialogue text in the preview has all {token} placeholders replaced with the current runtime values before display.

The Exported JSON

The export is a self-contained JSON object. Here's the top-level shape:

{
  "version": 1,
  "name": "My Dialogue",
  "exportedAt": "2025-01-01T00:00:00.000Z",
  "nodes": [ ...node objects... ],
  "edges": [ ...edge objects... ]
}

A character node:

{
  "id": "char-abc123",
  "type": "character",
  "position": { "x": 0, "y": 0 },        // editor layout only — ignore at runtime
  "data": {
    "name": "Guard",
    "dialogue": "Halt! Who goes there?",
    "emotion": "angry",                   // free-form string
    "portrait": "",                       // URL or asset path
    "attributeSchema": [
      { "id": "attr-hp", "name": "HP", "type": "number", "defaultValue": 100 }
    ],
    "attributes": { "attr-hp": 100 }      // current values, keyed by schema id
  }
}

An action node:

{
  "id": "act-def456",
  "type": "action",
  "position": { "x": 240, "y": 0 },
  "data": {
    "actionType": "branch",   // "branch" | "trigger" | "jump" | "end"
    "label": "Player Choice",
    "attributeSchema": [],
    "attributes": {}
  }
}

An edge:

{
  "id": "edge-ghi789",
  "source": "act-def456",      // id of the node this edge exits
  "target": "char-jkl012",     // id of the node this edge enters
  "type": "dialogue",
  "data": {
    "optionText": "I'm a traveler.",   // player-facing choice label (branch edges)
    "conditions": {},                  // reserved for runtime conditions
    "metadata": {}                     // reserved for custom data
  }
}

Using the JSON in Your Game

The graph is a directed acyclic graph (DAG). At runtime, load the JSON, find the entry node, and walk edges until you hit an end action. Below is a complete, copy-pasteable TypeScript runtime.

Step 1 — TypeScript types

interface NodeData {
  name?: string;
  dialogue?: string;
  emotion?: string;
  portrait?: string;
  actionType?: "branch" | "trigger" | "jump" | "end";
  label?: string;
  attributes?: Record<string, unknown>;
}

interface DialogueNode {
  id: string;
  type: "character" | "action";
  data: NodeData;
}

interface DialogueEdge {
  id: string;
  source: string;
  target: string;
  data: { optionText: string; conditions: object; metadata: object };
}

interface DialogueGraph {
  version: number;
  name: string;
  nodes: DialogueNode[];
  edges: DialogueEdge[];
}

Step 2 — Build lookup maps

import graphJson from "./my-dialogue.forge.json";

const graph = graphJson as DialogueGraph;

// O(1) lookups by id
const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));

// Outgoing edges grouped by source node
const edgesFrom = new Map<string, DialogueEdge[]>();
for (const edge of graph.edges) {
  if (!edgesFrom.has(edge.source)) edgesFrom.set(edge.source, []);
  edgesFrom.get(edge.source)!.push(edge);
}

Step 3 — Find the entry node

function findStartNode(graph: DialogueGraph): DialogueNode {
  const hasIncoming = new Set(graph.edges.map((e) => e.target));
  // Root nodes have no incoming edges
  return graph.nodes.find((n) => !hasIncoming.has(n.id)) ?? graph.nodes[0];
}

Step 4 — Dialogue runner

class DialogueRunner {
  private currentId: string;

  constructor(startNode: DialogueNode) {
    this.currentId = startNode.id;
  }

  get current(): DialogueNode        { return nodeById.get(this.currentId)!; }
  get choices(): DialogueEdge[]      { return edgesFrom.get(this.currentId) ?? []; }
  get isEnded(): boolean {
    const n = this.current;
    return n.type === "action" && n.data.actionType === "end";
  }

  /** Move to the next node. Pass a choiceIndex for branch nodes. */
  advance(choiceIndex = 0): boolean {
    if (this.isEnded) return false;
    const edge = this.choices[choiceIndex];
    if (!edge) return false;
    this.currentId = edge.target;
    return true;
  }
}

// ── Example traversal loop ─────────────────────────────────────
const runner = new DialogueRunner(findStartNode(graph));

while (!runner.isEnded) {
  const node = runner.current;

  if (node.type === "character") {
    console.log(`${node.data.name}: ${node.data.dialogue}`);
    runner.advance();                    // auto-advance (single outgoing edge)

  } else if (node.data.actionType === "branch") {
    runner.choices.forEach((e, i) =>
      console.log(`  [${i}] ${e.data.optionText}`)
    );
    const playerChoice = 0;             // ← replace with real player input
    runner.advance(playerChoice);

  } else if (node.data.actionType === "trigger") {
    fireGameEvent(node.data.label!);    // ← your event system here
    runner.advance();

  } else if (node.data.actionType === "jump") {
    runner.advance();                    // follows the single outgoing edge
  }
}

Other engines

Unity (C#) — use Newtonsoft.Json or System.Text.Json to deserialize into equivalent C# classes. The traversal logic maps 1-to-1.

Godot (GDScript) — use JSON.parse_string() and store nodes/edges in Dictionary arrays. GDScript's dynamic typing makes traversal straightforward.

Unreal (C++/Blueprints) — deserialize with FJsonSerializer into TMap structures. Blueprint-callable functions can wrap the runner pattern.

Tips

  • The position field on each node is only for the editor canvas — safely ignore it at runtime.
  • conditions and metadata on edges are reserved for future runtime logic, e.g. only show a branch choice if the player has enough gold.
  • Character attributes mirror the attributeSchema — use the schema to know each value's type before reading it.
  • Files are versioned ("version": 1) so you can handle format migrations as the tool evolves.