T.TAO
技术文档

Alfheim Node Library

TAGEngine (Text Adventure Game Engine) Node Library — 节点系统设计文档

Overview

Alfheim 的故事系统基于节点图(Node Graph)构建。每个 Episode(故事章节)由一张节点图描述,节点之间的连线表示故事流程。玩家的选择、条件判断和随机事件通过不同类型的节点实现。

本文档定义了 Node Library 中全部 8 种节点类型、它们的端口规则、数据结构,以及 Episode 之间的拓扑连接机制。


Architecture: Port-Driven Model

每种节点通过其类型专属数据定义输出端口布局StoryBranch 统一负责端口间的连线

Plain TextNode-type specific data     →   Output Ports Layout   →   StoryBranch[] (wiring)
(choices, conditions, etc.)      (per-type rules)          (targetNodeId, sourceHandle)

Port rules per node type:

Node TypeInput PortsOutput PortsOutput source
start01 (default)Fixed
endN0–NepisodeLinks[]
dialogue11 (default) or Nchoices[] (if any)
condition1N + 1conditionArms[] + else
random1NrandomOutputs[]
setVariable11 (default)Fixed
event11 (default)Fixed
comment00None

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 episodeLinks is empty, the node is terminal — the story ends here.
  • When episodeLinks has 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 to true is 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:

ScopeInheritedBehavior
globalYesShared across all connected Episodes
episodeNoResets to default values when entering a new Episode
characterYesPersists 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 DialogueLine in order (speaker name, text, emotion, modifiers).
  • If choices is empty, the node has a single default output and auto-continues.
  • If choices has 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

AspectNodeChoice (node-level)BrancherData (modifier)
ScopeDetermines which node to visit nextLocal to the dialogue line
Output portsYes — generates source handlesNo — inline within line rendering
Visual in graphVisible as labeled output portsHidden inside the node
Use caseStory 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 expression evaluates to true is 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 &lt;= 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:

OperationBehaviorExample
setReplace valuehealth = 100
addAdd to current valuegold = gold + 50
subtractSubtract from current valuestamina = stamina - 10
multiplyMultiply current valuedamage = damage * 1.5
toggleFlip booleanhas_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 TypeParamsDescription
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
customAny key-value pairsCustom 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 note field on StoryNode for 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

  1. An End Node can have 0 to N episode links (terminal or branching).
  2. Each link targets exactly one Episode.
  3. Multiple End Nodes in one Episode can link to different Episodes.
  4. Circular connections are allowed (e.g., Episode A → Episode B → Episode A) for looping story structures.
  5. 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 episodeLinks across 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 TypesourceHandle formatDerived from
start(default)Fixed single output
endlink-{index}episodeLinks[index]
dialoguechoice-{index}choices[index] or default
conditionarm-{index}, elseconditionArms[index] / else
randomout-{index}randomOutputs[index]
setVariable(default)Fixed single output
event(default)Fixed single output
commentN/ANo 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 TypeColorIconLabel (zh)Label (en)
start#22c55ePlay开始Start
end#ef4444Square结束End
dialogue#3b82f6MessageSquare对话Dialogue
condition#f59e0bGitBranch条件Condition
random#8b5cf6Shuffle随机Random
setVariable#06b6d4Variable变量Set Variable
event#ec4899Zap事件Event
comment#6b7280MessageCircle注释Comment