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.
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.