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.
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.
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.
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.
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 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.
numberInteger values. Use for counters, scores, health points, or any whole-number stat.
floatDecimal numbers. Use for percentages, probabilities, or any value that needs fractional precision — e.g. a relationship score of 0.75.
booleanTrue/false flags. Use for quest completion, feature toggles, or binary state — e.g. hasMetKing: true.
stringText values. Use for character names, faction tags, dialogue keys, or any free-form label.
listAn ordered array of strings. Use for inventories, unlocked skills, completed objectives, or any variable-length collection.
objectKey-value pairs. Use for structured data like a stats block, an item with multiple properties, or a nested config bag.
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}."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 / notEqualsExact numeric match or mismatch
greaterThan / lessThanExclusive comparison
greaterThanOrEqual / lessThanOrEqualInclusive comparison
between / notBetweenInclusive range check — e.g. gold between 10 and 50
Boolean operators
isTrueVariable is true
isFalseVariable is false
String operators
equals / notEqualsCase-sensitive match
contains / notContainsSubstring presence
startsWith / endsWithPrefix or suffix
isEmpty / isNotEmptyBlank string check
List operators
listContainsList includes a specific string — e.g. inventory listContains "sword"
listIsEmpty / listIsNotEmptyZero-length check on the list
Object operators
hasPropertyObject has a given key — e.g. playerData hasProperty "level"
hasNoPropertyObject does not have a given key
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 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
}
}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
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.attributes mirror the attributeSchema — use the schema to know each value's type before reading it."version": 1) so you can handle format migrations as the tool evolves.