Alfheim Node Library
TAG Engine (Text Adventure Game Engine) Node Library β Node System Design Document
Overview
Alfheim's story system is built on a Node Graph. Each Episode (story chapter) is described by a node graph, with connections between nodes representing the story flow. Player choices, condition checks, and random events are implemented through different node types.
This document defines all 8 node types in the Node Library, their port rules, data structures, and the topology connection mechanism between Episodes.
Architecture: Port-Driven Model
Each node defines its output port layout through type-specific data. StoryBranch uniformly handles wiring between ports.
Plain TextNode-type specific data β Output Ports Layout β StoryBranch[] (wiring)
(choices, conditions, etc.) (per-type rules) (targetNodeId, sourceHandle)
Port rules per node type:
| Node Type | Input Ports | Output Ports | Output source |
|---|---|---|---|
| start | 0 | 1 (default) | Fixed |
| end | N | 0βN | episodeLinks[] |
| dialogue | 1 | 1 (default) or N | choices[] (if any) |
| condition | 1 | N + 1 | conditionArms[] + else |
| random | 1 | N | randomOutputs[] |
| setVariable | 1 | 1 (default) | Fixed |
| event | 1 | 1 (default) | Fixed |
| comment | 0 | 0 | None |
Node Types
1. Start Node
Category: Flow Control
Entry point of an Episode. Every Episode must have exactly one Start Node.
- Inputs: 0
- Outputs: 1 (default, always present)
- Constraints: Cannot be deleted. One per Episode.
- Visual: Green (
#22c55e), Play icon
Behavior:
- Runtime begins execution at the Start Node
- Connected Episode's Start Node receives inherited global and character variables
- The single output connects to the first content or logic node
Data fields: None (uses base StoryNode fields only).
2. End Node
Category: Flow Control
Exit point of an Episode. Marks where the Episode's story flow terminates or transitions to another Episode.
- Inputs: N (any number of nodes can flow into End)
- Outputs: 0βN (one per Episode Link)
- Constraints: At least one per Episode recommended. Cannot be Start-type protected but can be freely created/deleted.
- Visual: Red (
#ef4444), Square icon
Behavior:
- When
episodeLinksis empty, the node is terminal β the story ends here. - When
episodeLinkshas entries, each link generates an output port. These ports connect to other Episodes' Start Nodes. - Each link can have an optional
condition. At runtime, the first link whose condition evaluates totrueis taken. If multiple links have no condition, the runtime presents them as choices.
Data fields:
Typescriptinterface EpisodeLink {
id: string;
targetEpisodeId: string; // ID of the Episode to transition to
label: string; // Display label (e.g., "Continue to Chapter 2")
condition: string | null; // Optional condition expression
}
Variable inheritance on Episode transition:
| Scope | Inherited | Behavior |
|---|---|---|
| global | Yes | Shared across all connected Episodes |
| episode | No | Resets to default values when entering a new Episode |
| character | Yes | Persists with the character across Episodes |
3. Dialogue Node
Category: Content
The core content node. Contains dialogue lines (speaker text) and optional player choices.
- Inputs: 1
- Outputs: 1 (no choices) or N (one per choice)
- Visual: Blue (
#3b82f6), MessageSquare icon
Behavior:
- Runtime plays each
DialogueLinein order (speaker name, text, emotion, modifiers). - If
choicesis empty, the node has a single default output and auto-continues. - If
choiceshas entries, each choice generates a labeled output port. The player selects a choice, and the flow follows the corresponding output.
Data fields:
Typescriptinterface NodeChoice {
id: string;
label: string; // Player-visible choice text
condition: string | null; // Only show this choice if condition is true
variableChanges: VariableChange[]; // Execute these changes when selected
}
The DialogueLine[] structure is unchanged. The existing BrancherData modifier within dialogue lines is retained for lightweight inline options that do not affect the node-level flow (e.g., flavor responses, affinity micro-adjustments).
Distinction: NodeChoice vs BrancherData
| Aspect | NodeChoice (node-level) | BrancherData (modifier) |
|---|---|---|
| Scope | Determines which node to visit next | Local to the dialogue line |
| Output ports | Yes β generates source handles | No β inline within line rendering |
| Visual in graph | Visible as labeled output ports | Hidden inside the node |
| Use case | Story branching ("Go left / right") | Micro choices ("Smile / Nod") |
Example:
Plain Text[Dialogue Node: "The Crossroads"]
Lines:
- Narrator: "You arrive at a fork in the road."
- Narrator: "To the west, a dark forest. To the east, a mountain pass."
Choices:
- "Enter the forest" β connects to ForestNode
- "Climb the mountain" β connects to MountainNode
- "Turn back" [condition: courage < 30] β connects to RetreatNode
4. Condition Node (If / Elif / Else)
Category: Logic
Evaluates variable conditions and routes flow to the first matching branch.
- Inputs: 1
- Outputs: N + 1 (one per condition arm, plus one Else)
- Visual: Amber (
#f59e0b), GitBranch icon
Behavior:
- Condition arms are evaluated in order (top to bottom).
- The first arm whose
expressionevaluates totrueis taken. - If no arm matches, the Else output is taken.
- The Else port is always present and cannot be removed.
Data fields:
Typescriptinterface ConditionArm {
id: string;
label: string; // Display label (e.g., "High Trust")
expression: string; // Condition expression (e.g., "trust >= 80")
}
Expression language:
Conditions use a simple expression syntax:
- Comparison:
variable > value,variable == value,variable != value,variable >= value,variable <= value - Boolean:
flag == true,flag == false - String:
name == "Alice" - Logical operators:
&&,||,! - Grouping:
(trust >= 50) && (visited_cave == true)
Example:
Plain Text[Condition Node: "Check Trust Level"]
Arms:
- "High trust" [trust >= 80] β connects to TrustPath
- "Medium trust" [trust >= 40] β connects to NeutralPath
- Else β connects to SuspicionPath
5. Random Node
Category: Logic
Randomly selects one output based on weighted probability.
- Inputs: 1
- Outputs: N (one per random output)
- Visual: Purple (
#8b5cf6), Shuffle icon
Behavior:
- At runtime, one output is selected randomly.
- Weights are relative: a weight of 3 is three times as likely as a weight of 1.
- If all weights are equal, selection is uniform.
Data fields:
Typescriptinterface RandomOutput {
id: string;
label: string; // Display label (e.g., "Find treasure")
weight: number; // Relative probability weight (positive number)
}
Example:
Plain Text[Random Node: "Forest Encounter"]
Outputs:
- "Meet a merchant" [weight: 3] β connects to MerchantNode
- "Ambushed by wolves" [weight: 2] β connects to WolfNode
- "Find hidden ruins" [weight: 1] β connects to RuinsNode
Probabilities: Merchant 50%, Wolves 33.3%, Ruins 16.7%.
6. SetVariable Node
Category: Action
Sets or modifies one or more variables. Useful for tracking game state changes that happen outside of dialogue choices.
- Inputs: 1
- Outputs: 1 (default)
- Visual: Cyan (
#06b6d4), Variable icon
Behavior:
- All assignments execute atomically when the node is entered.
- Flow immediately continues to the output.
Data fields:
Typescripttype AssignmentOperation = "set" | "add" | "subtract" | "multiply" | "toggle";
interface VariableAssignment {
id: string;
variableId: string; // Target variable ID
scope: VariableScope; // "global" | "episode" | "character"
characterId: string | null; // Required when scope is "character"
operation: AssignmentOperation;
expression: string; // Value or expression
}
Operations:
| Operation | Behavior | Example |
|---|---|---|
| set | Replace value | health = 100 |
| add | Add to current value | gold = gold + 50 |
| subtract | Subtract from current value | stamina = stamina - 10 |
| multiply | Multiply current value | damage = damage * 1.5 |
| toggle | Flip boolean | has_key = !has_key |
Example:
Plain Text[SetVariable Node: "Gain Reward"]
Assignments:
- gold [global] set += 100
- trust [character: Alice] add += 20
- visited_cave [episode] set = true
7. Event Node
Category: Action
Triggers game events such as playing music, showing images, or unlocking achievements.
- Inputs: 1
- Outputs: 1 (default)
- Visual: Pink (
#ec4899), Zap icon
Behavior:
- All event actions execute when the node is entered.
- Events are fire-and-forget unless the runtime implements blocking behavior.
- Flow immediately continues to the output.
Data fields:
Typescriptinterface EventAction {
id: string;
eventType: string; // Event identifier
params: Record<string, string>; // Key-value parameters
}
Built-in event types:
| Event Type | Params | Description |
|---|---|---|
| play_bgm | { track: "path/to/track.ogg" } | Play background music |
| stop_bgm | {} | Stop background music |
| play_sfx | { sound: "path/to/sfx.ogg" } | Play sound effect |
| show_image | { path: "path/to/image.png", position: "center" } | Display full-screen image |
| screen_effect | { effect: "fade_black", duration: "1.0" } | Screen transition effect |
| unlock_achievement | { achievement: "first_choice" } | Unlock an achievement |
| custom | Any key-value pairs | Custom event for C++ scripts |
Example:
Plain Text[Event Node: "Enter the Cave"]
Actions:
- play_bgm { track: "audio/bgm/cave_theme.ogg" }
- screen_effect { effect: "fade_black", duration: "0.5" }
8. Comment Node
Category: Utility
Developer-only annotation node. Does not participate in the story flow.
- Inputs: 0
- Outputs: 0
- Visual: Gray (
#6b7280), MessageCircle icon
Behavior:
- Completely ignored at runtime.
- Can be placed anywhere on the canvas as a note for the development team.
- Uses the
notefield onStoryNodefor content.
Episode Topology
Inter-Episode Connections
Episodes are connected through End Node episodeLinks. Each link references another Episode by ID and optionally specifies a condition.
Plain TextEpisode 1 (Prologue) Episode 2A (Forest Path)
βββββββββββββββββββββββββββ βββββββββββββββββββββββββββ
β Start β ... β End βββ"Forest"βββ·β Start β ... β End β
β β βββββββββββββββββββββββββββ
β β
β β Episode 2B (Cave Path)
β β βββββββββββββββββββββββββββ
β βββ"Cave"βββββ·β Start β ... β End β
βββββββββββββββββββββββββββ βββββββββββββββββββββββββββ
Connection Rules
- An End Node can have 0 to N episode links (terminal or branching).
- Each link targets exactly one Episode.
- Multiple End Nodes in one Episode can link to different Episodes.
- Circular connections are allowed (e.g., Episode A β Episode B β Episode A) for looping story structures.
- An Episode can be the target of links from multiple Episodes (convergence).
Variable Inheritance
When transitioning between Episodes:
- Global variables carry over with their current values.
- Character variables carry over with their current values.
- Episode variables reset to their defined default values.
The target Episode's Start Node begins execution with the inherited variable state.
Yggdrasil Tree View
The Yggdrasil panel provides two views for episode organization:
Timeline View (existing): Chronological timeline with calendar entries linking to Episodes.
Tree View (new): A node graph at the Episode level.
- Each Episode renders as a super-node showing name, description, and character thumbnails.
- Edges between Episodes are derived from End Node
episodeLinksacross all Episodes. - The tree view is read-only visualization (editing happens within each Episode's Planner).
- Layout is auto-computed or manually adjustable.
StoryBranch Wiring
The StoryBranch array on each StoryNode stores the actual wiring β which output port connects to which target node.
Typescriptinterface StoryBranch {
id: string;
targetNodeId: string;
label: string;
condition: string | null;
}
How ports map to branches:
For nodes with dynamic outputs (dialogue with choices, condition, random), the branch sourceHandle in the ReactFlow edge encodes which output port the branch originates from:
| Node Type | sourceHandle format | Derived from |
|---|---|---|
| start | (default) | Fixed single output |
| end | link-{index} | episodeLinks[index] |
| dialogue | choice-{index} | choices[index] or default |
| condition | arm-{index}, else | conditionArms[index] / else |
| random | out-{index} | randomOutputs[index] |
| setVariable | (default) | Fixed single output |
| event | (default) | Fixed single output |
| comment | N/A | No outputs |
Data Model Summary
TypeScript (editor/src/types/story.ts)
Typescriptexport type NodeType =
| "start"
| "end"
| "dialogue"
| "condition"
| "random"
| "setVariable"
| "event"
| "comment";
export interface NodeChoice {
id: string;
label: string;
condition: string | null;
variableChanges: VariableChange[];
}
export interface ConditionArm {
id: string;
label: string;
expression: string;
}
export interface RandomOutput {
id: string;
label: string;
weight: number;
}
export type AssignmentOperation = "set" | "add" | "subtract" | "multiply" | "toggle";
export interface VariableAssignment {
id: string;
variableId: string;
scope: VariableScope;
characterId: string | null;
operation: AssignmentOperation;
expression: string;
}
export interface EventAction {
id: string;
eventType: string;
params: Record<string, string>;
}
export interface EpisodeLink {
id: string;
targetEpisodeId: string;
label: string;
condition: string | null;
}
export interface StoryNode {
id: string;
title: string;
nodeType: NodeType;
position: NodePosition;
branches: StoryBranch[];
// Content (Dialogue Node)
dialogueLines: DialogueLine[];
choices?: NodeChoice[];
// Logic (Condition Node)
conditionArms?: ConditionArm[];
// Logic (Random Node)
randomOutputs?: RandomOutput[];
// Action (SetVariable Node)
assignments?: VariableAssignment[];
// Action (Event Node)
eventActions?: EventAction[];
// Flow (End Node)
episodeLinks?: EpisodeLink[];
// Common fields
note: string;
folderId: string | null;
metadata: Record<string, string>;
sceneTemplateId?: string | null;
slotAssignments?: Record<string, string>;
enterConditions?: NodeCondition[];
variableActions?: VariableActionEntry[];
}
Visual Config
| Node Type | Color | Icon | Label (zh) | Label (en) |
|---|---|---|---|---|
| start | #22c55e | Play | εΌε§ | Start |
| end | #ef4444 | Square | η»ζ | End |
| dialogue | #3b82f6 | MessageSquare | ε―Ήθ― | Dialogue |
| condition | #f59e0b | GitBranch | ζ‘δ»Ά | Condition |
| random | #8b5cf6 | Shuffle | ιζΊ | Random |
| setVariable | #06b6d4 | Variable | ει | Set Variable |
| event | #ec4899 | Zap | δΊδ»Ά | Event |
| comment | #6b7280 | MessageCircle | 注ι | Comment |