T.TAO
技術ドキュメント

Alfheim Node ライブラリ

TAGEngine(Text Adventure Game Engine)Node ライブラリ — ノードシステム設計ドキュメント

概要

Alfheim のストーリーシステムはノードグラフ(Node Graph)に基づいて構築されています。各 Episode(ストーリー章)は1つのノードグラフで記述され、ノード間の接続線がストーリーの流れを表します。プレイヤーの選択、条件分岐、ランダムイベントは、さまざまなタイプのノードによって実現されます。

本ドキュメントでは、Node ライブラリの 8種類のノードタイプ すべて、そのポートルール、データ構造、および Episode 間のトポロジー接続メカニズムを定義します。


アーキテクチャ:ポート駆動モデル

各ノードは、そのタイプ固有のデータによって出力ポートレイアウトを定義し、StoryBranch がポート間の接続を一元管理します。

Plain Textノードタイプ固有データ     →   出力ポートレイアウト   →   StoryBranch[](配線)
(選択肢、条件など)              (タイプ別ルール)           (targetNodeId, sourceHandle)

ノードタイプ別ポートルール:

ノードタイプ入力ポート出力ポート出力ソース
start01(デフォルト)固定
endN0–NepisodeLinks[]
dialogue11(デフォルト)または Nchoices[](存在する場合)
condition1N + 1conditionArms[] + else
random1NrandomOutputs[]
setVariable11(デフォルト)固定
event11(デフォルト)固定
comment00なし

ノードタイプ

1. Start ノード

カテゴリ: フロー制御

Episode のエントリーポイント。各 Episode には必ず1つの Start ノードが必要です。

  • 入力: 0
  • 出力: 1(デフォルト、常に存在)
  • 制約: 削除不可。Episode ごとに1つ。
  • ビジュアル: 緑(#22c55e)、Play アイコン

動作:

  • ランタイムは Start ノードから実行を開始します
  • 接続された Episode の Start ノードは、継承されたグローバル変数とキャラクター変数を受け取ります
  • 単一の出力は最初のコンテンツまたはロジックノードに接続されます

データフィールド: なし(基本の StoryNode フィールドのみを使用)。


2. End ノード

カテゴリ: フロー制御

Episode の終了ポイント。Episode のストーリーの流れが終了する、または別の Episode に遷移する地点を示します。

  • 入力: N(任意の数のノードが End に流れ込む)
  • 出力: 0–N(Episode Link ごとに1つ)
  • 制約: Episode ごとに少なくとも1つ推奨。Start タイプのような保護はないが、自由に作成・削除可能。
  • ビジュアル: 赤(#ef4444)、Square アイコン

動作:

  • episodeLinks が空の場合、ノードはターミナル — ストーリーはここで終了します。
  • episodeLinks にエントリがある場合、各リンクが出力ポートを生成します。これらのポートは他の Episode の Start ノードに接続されます。
  • 各リンクにはオプションで condition を設定できます。ランタイムでは、条件が true と評価された最初のリンクが選択されます。複数のリンクに条件がない場合、ランタイムはそれらを選択肢として表示します。

データフィールド:

Typescriptinterface EpisodeLink {
  id: string;
  targetEpisodeId: string;    // 遷移先の Episode の ID
  label: string;              // 表示ラベル(例:「第2章へ続く」)
  condition: string | null;   // オプションの条件式
}

Episode 遷移時の変数継承:

スコープ継承動作
globalはい接続されたすべての Episode で共有
episodeいいえ新しい Episode に入る際にデフォルト値にリセット
characterはいEpisode 間でキャラクターとともに永続化

3. Dialogue ノード

カテゴリ: コンテンツ

コアコンテンツノード。会話行(話者テキスト)とオプションのプレイヤー選択肢を含みます。

  • 入力: 1
  • 出力: 1(選択肢なし)または N(選択肢ごとに1つ)
  • ビジュアル: 青(#3b82f6)、MessageSquare アイコン

動作:

  • ランタイムは各 DialogueLine を順番に再生します(話者名、テキスト、感情、修飾子)。
  • choices が空の場合、ノードは単一のデフォルト出力を持ち、自動的に続行します。
  • choices にエントリがある場合、各選択肢がラベル付き出力ポートを生成します。プレイヤーが選択肢を選ぶと、対応する出力に沿ってフローが進みます。

データフィールド:

Typescriptinterface NodeChoice {
  id: string;
  label: string;                    // プレイヤーに表示される選択肢テキスト
  condition: string | null;         // 条件が true の場合のみこの選択肢を表示
  variableChanges: VariableChange[]; // 選択時に実行する変更
}

DialogueLine[] 構造は変更されていません。会話行内の既存の BrancherData 修飾子は、ノードレベルのフローに影響しない軽量なインラインオプション(例:フレーバーレスポンス、親密度の微調整)のために保持されています。

区別:NodeChoice と BrancherData

観点NodeChoice(ノードレベル)BrancherData(修飾子)
スコープ次に訪問するノードを決定会話行内に限定
出力ポートあり — ソースハンドルを生成なし — 行のレンダリング内でインライン
グラフ内の表示ラベル付き出力ポートとして表示ノード内に隠れている
ユースケースストーリー分岐(「左へ行く/右へ行く」)マイクロ選択(「微笑む/頷く」)

例:

Plain Text[Dialogue ノード:「分かれ道」]
  行:
    - ナレーター:「道が二手に分かれている。」
    - ナレーター:「西には暗い森。東には山道。」
  選択肢:
    - 「森に入る」  → ForestNode に接続
    - 「山を登る」 → MountainNode に接続
    - 「引き返す」 [条件: courage < 30] → RetreatNode に接続

4. Condition ノード(If / Elif / Else)

カテゴリ: ロジック

変数条件を評価し、最初に一致するブランチにフローをルーティングします。

  • 入力: 1
  • 出力: N + 1(条件アームごとに1つ、プラス Else 1つ)
  • ビジュアル: 琥珀色(#f59e0b)、GitBranch アイコン

動作:

  • 条件アームは順番に(上から下へ)評価されます。
  • expressiontrue と評価された最初のアームが選択されます。
  • 一致するアームがない場合、Else 出力が選択されます。
  • Else ポートは常に存在し、削除できません。

データフィールド:

Typescriptinterface ConditionArm {
  id: string;
  label: string;      // 表示ラベル(例:「高信頼」)
  expression: string;  // 条件式(例:「trust >= 80」)
}

式言語:

条件はシンプルな式構文を使用します:

  • 比較:variable > valuevariable == valuevariable != valuevariable >= valuevariable &lt;= value
  • ブール:flag == trueflag == false
  • 文字列:name == "Alice"
  • 論理演算子:&&||!
  • グループ化:(trust >= 50) && (visited_cave == true)

例:

Plain Text[Condition ノード:「信頼レベル確認」]
  アーム:
    - 「高信頼」    [trust >= 80]  → TrustPath に接続
    - 「中信頼」  [trust >= 40]  → NeutralPath に接続
    - Else                           → SuspicionPath に接続

5. Random ノード

カテゴリ: ロジック

重み付き確率に基づいて1つの出力をランダムに選択します。

  • 入力: 1
  • 出力: N(ランダム出力ごとに1つ)
  • ビジュアル: 紫(#8b5cf6)、Shuffle アイコン

動作:

  • ランタイムで、1つの出力がランダムに選択されます。
  • 重みは相対的です:重み3は重み1の3倍の確率です。
  • すべての重みが等しい場合、選択は均一です。

データフィールド:

Typescriptinterface RandomOutput {
  id: string;
  label: string;   // 表示ラベル(例:「宝を見つける」)
  weight: number;  // 相対確率重み(正の数)
}

例:

Plain Text[Random ノード:「森の遭遇」]
  出力:
    - 「商人に会う」  [weight: 3]  → MerchantNode に接続
    - 「狼に襲われる」 [weight: 2] → WolfNode に接続
    - 「隠れた遺跡を見つける」  [weight: 1] → RuinsNode に接続

確率:商人 50%、狼 33.3%、遺跡 16.7%。


6. SetVariable ノード

カテゴリ: アクション

1つ以上の変数を設定または変更します。会話の選択肢以外で発生するゲーム状態の変化を追跡するのに便利です。

  • 入力: 1
  • 出力: 1(デフォルト)
  • ビジュアル: シアン(#06b6d4)、Variable アイコン

動作:

  • すべての代入はノードに入ったときにアトミックに実行されます。
  • フローは即座に出力に続きます。

データフィールド:

Typescripttype AssignmentOperation = "set" | "add" | "subtract" | "multiply" | "toggle";

interface VariableAssignment {
  id: string;
  variableId: string;               // 対象変数 ID
  scope: VariableScope;             // "global" | "episode" | "character"
  characterId: string | null;       // scope が "character" の場合に必須
  operation: AssignmentOperation;
  expression: string;               // 値または式
}

操作:

操作動作
set値を置換health = 100
add現在の値に加算gold = gold + 50
subtract現在の値から減算stamina = stamina - 10
multiply現在の値を乗算damage = damage * 1.5
toggleブールを反転has_key = !has_key

例:

Plain Text[SetVariable ノード:「報酬を獲得」]
  代入:
    - gold [global]      set  += 100
    - trust [character: Alice]  add  += 20
    - visited_cave [episode]  set  = true

7. Event ノード

カテゴリ: アクション

音楽の再生、画像の表示、実績の解除など、ゲームイベントをトリガーします。

  • 入力: 1
  • 出力: 1(デフォルト)
  • ビジュアル: ピンク(#ec4899)、Zap アイコン

動作:

  • すべてのイベントアクションはノードに入ったときに実行されます。
  • ランタイムがブロッキング動作を実装しない限り、イベントはファイアアンドフォーゲットです。
  • フローは即座に出力に続きます。

データフィールド:

Typescriptinterface EventAction {
  id: string;
  eventType: string;                // イベント識別子
  params: Record<string, string>;   // キー・バリューパラメータ
}

組み込みイベントタイプ:

イベントタイプパラメータ説明
play_bgm{ track: "path/to/track.ogg" }BGM を再生
stop_bgm{}BGM を停止
play_sfx{ sound: "path/to/sfx.ogg" }効果音を再生
show_image{ path: "path/to/image.png", position: "center" }フルスクリーン画像を表示
screen_effect{ effect: "fade_black", duration: "1.0" }画面遷移エフェクト
unlock_achievement{ achievement: "first_choice" }実績を解除
custom任意のキー・バリューペアC++ スクリプト用カスタムイベント

例:

Plain Text[Event ノード:「洞窟に入る」]
  アクション:
    - play_bgm { track: "audio/bgm/cave_theme.ogg" }
    - screen_effect { effect: "fade_black", duration: "0.5" }

8. Comment ノード

カテゴリ: ユーティリティ

開発者専用の注釈ノード。ストーリーの流れには参加しません。

  • 入力: 0
  • 出力: 0
  • ビジュアル: グレー(#6b7280)、MessageCircle アイコン

動作:

  • ランタイムでは完全に無視されます。
  • 開発チームへのメモとしてキャンバス上の任意の場所に配置できます。
  • コンテンツには StoryNodenote フィールドを使用します。

Episode トポロジー

Episode 間接続

Episode は End ノードの episodeLinks を通じて接続されます。各リンクは ID で別の Episode を参照し、オプションで条件を指定できます。

Plain TextEpisode 1(プロローグ)                    Episode 2A(森の道)
┌─────────────────────────┐             ┌─────────────────────────┐
│ Start → ... → End       │──「森」──▷│ Start → ... → End       │
│                         │             └─────────────────────────┘
│                         │
│                         │             Episode 2B(洞窟の道)
│                         │             ┌─────────────────────────┐
│                         │──「洞窟」──▷│ Start → ... → End       │
└─────────────────────────┘             └─────────────────────────┘

接続ルール

  1. End ノードは 0 から N 個の episode リンクを持つことができます(ターミナルまたは分岐)。
  2. 各リンクは正確に 1つ の Episode をターゲットにします。
  3. 1つの Episode 内の複数の End ノードが、異なる Episode にリンクできます。
  4. ループストーリー構造のため、循環接続が許可されています(例:Episode A → Episode B → Episode A)。
  5. 1つの Episode は複数の Episode からのリンクのターゲットになることができます(収束)。

変数継承

Episode 間で遷移する際:

  • グローバル変数は現在の値で引き継がれます。
  • キャラクター変数は現在の値で引き継がれます。
  • Episode 変数は定義されたデフォルト値にリセットされます。

ターゲット Episode の Start ノードは、継承された変数状態で実行を開始します。

Yggdrasil ツリービュー

Yggdrasil パネルは、Episode の整理のために2つのビューを提供します:

タイムラインビュー(既存):Episode にリンクするカレンダーエントリを含む時系列タイムライン。

ツリービュー(新規):Episode レベルのノードグラフ。

  • 各 Episode は名前、説明、キャラクターサムネイルを表示するスーパーノードとしてレンダリングされます。
  • Episode 間のエッジは、すべての Episode にわたる End ノードの episodeLinks から導出されます。
  • ツリービューは読み取り専用の可視化です(編集は各 Episode の Planner 内で行われます)。
  • レイアウトは自動計算または手動調整可能です。

StoryBranch 配線

StoryNodeStoryBranch 配列は、実際の配線 — どの出力ポートがどのターゲットノードに接続されるか — を格納します。

Typescriptinterface StoryBranch {
  id: string;
  targetNodeId: string;
  label: string;
  condition: string | null;
}

ポートがブランチにマッピングされる方法:

動的出力を持つノード(選択肢付き会話、条件、ランダム)の場合、ReactFlow エッジのブランチ sourceHandle が、ブランチがどの出力ポートから発生するかをエンコードします:

ノードタイプsourceHandle 形式導出元
start(デフォルト)固定の単一出力
endlink-{index}episodeLinks[index]
dialoguechoice-{index}choices[index] またはデフォルト
conditionarm-{index}elseconditionArms[index] / else
randomout-{index}randomOutputs[index]
setVariable(デフォルト)固定の単一出力
event(デフォルト)固定の単一出力
commentN/A出力なし

データモデル概要

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[];
}

ビジュアル設定

ノードタイプアイコンラベル(ja)ラベル(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