Alfheim Node ライブラリ
TAGEngine(Text Adventure Game Engine)Node ライブラリ — ノードシステム設計ドキュメント
概要
Alfheim のストーリーシステムはノードグラフ(Node Graph)に基づいて構築されています。各 Episode(ストーリー章)は1つのノードグラフで記述され、ノード間の接続線がストーリーの流れを表します。プレイヤーの選択、条件分岐、ランダムイベントは、さまざまなタイプのノードによって実現されます。
本ドキュメントでは、Node ライブラリの 8種類のノードタイプ すべて、そのポートルール、データ構造、および Episode 間のトポロジー接続メカニズムを定義します。
アーキテクチャ:ポート駆動モデル
各ノードは、そのタイプ固有のデータによって出力ポートレイアウトを定義し、StoryBranch がポート間の接続を一元管理します。
Plain Textノードタイプ固有データ → 出力ポートレイアウト → StoryBranch[](配線)
(選択肢、条件など) (タイプ別ルール) (targetNodeId, sourceHandle)
ノードタイプ別ポートルール:
| ノードタイプ | 入力ポート | 出力ポート | 出力ソース |
|---|---|---|---|
| start | 0 | 1(デフォルト) | 固定 |
| end | N | 0–N | episodeLinks[] |
| dialogue | 1 | 1(デフォルト)または N | choices[](存在する場合) |
| condition | 1 | N + 1 | conditionArms[] + else |
| random | 1 | N | randomOutputs[] |
| setVariable | 1 | 1(デフォルト) | 固定 |
| event | 1 | 1(デフォルト) | 固定 |
| comment | 0 | 0 | なし |
ノードタイプ
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 アイコン
動作:
- 条件アームは順番に(上から下へ)評価されます。
expressionがtrueと評価された最初のアームが選択されます。- 一致するアームがない場合、Else 出力が選択されます。
- Else ポートは常に存在し、削除できません。
データフィールド:
Typescriptinterface ConditionArm {
id: string;
label: string; // 表示ラベル(例:「高信頼」)
expression: string; // 条件式(例:「trust >= 80」)
}
式言語:
条件はシンプルな式構文を使用します:
- 比較:
variable > value、variable == value、variable != value、variable >= value、variable <= value - ブール:
flag == true、flag == 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 アイコン
動作:
- ランタイムでは完全に無視されます。
- 開発チームへのメモとしてキャンバス上の任意の場所に配置できます。
- コンテンツには
StoryNodeのnoteフィールドを使用します。
Episode トポロジー
Episode 間接続
Episode は End ノードの episodeLinks を通じて接続されます。各リンクは ID で別の Episode を参照し、オプションで条件を指定できます。
Plain TextEpisode 1(プロローグ) Episode 2A(森の道)
┌─────────────────────────┐ ┌─────────────────────────┐
│ Start → ... → End │──「森」──▷│ Start → ... → End │
│ │ └─────────────────────────┘
│ │
│ │ Episode 2B(洞窟の道)
│ │ ┌─────────────────────────┐
│ │──「洞窟」──▷│ Start → ... → End │
└─────────────────────────┘ └─────────────────────────┘
接続ルール
- End ノードは 0 から N 個の episode リンクを持つことができます(ターミナルまたは分岐)。
- 各リンクは正確に 1つ の Episode をターゲットにします。
- 1つの Episode 内の複数の End ノードが、異なる Episode にリンクできます。
- ループストーリー構造のため、循環接続が許可されています(例:Episode A → Episode B → Episode A)。
- 1つの Episode は複数の Episode からのリンクのターゲットになることができます(収束)。
変数継承
Episode 間で遷移する際:
- グローバル変数は現在の値で引き継がれます。
- キャラクター変数は現在の値で引き継がれます。
- Episode 変数は定義されたデフォルト値にリセットされます。
ターゲット Episode の Start ノードは、継承された変数状態で実行を開始します。
Yggdrasil ツリービュー
Yggdrasil パネルは、Episode の整理のために2つのビューを提供します:
タイムラインビュー(既存):Episode にリンクするカレンダーエントリを含む時系列タイムライン。
ツリービュー(新規):Episode レベルのノードグラフ。
- 各 Episode は名前、説明、キャラクターサムネイルを表示するスーパーノードとしてレンダリングされます。
- Episode 間のエッジは、すべての Episode にわたる End ノードの
episodeLinksから導出されます。 - ツリービューは読み取り専用の可視化です(編集は各 Episode の Planner 内で行われます)。
- レイアウトは自動計算または手動調整可能です。
StoryBranch 配線
各 StoryNode の StoryBranch 配列は、実際の配線 — どの出力ポートがどのターゲットノードに接続されるか — を格納します。
Typescriptinterface StoryBranch {
id: string;
targetNodeId: string;
label: string;
condition: string | null;
}
ポートがブランチにマッピングされる方法:
動的出力を持つノード(選択肢付き会話、条件、ランダム)の場合、ReactFlow エッジのブランチ sourceHandle が、ブランチがどの出力ポートから発生するかをエンコードします:
| ノードタイプ | sourceHandle 形式 | 導出元 |
|---|---|---|
| start | (デフォルト) | 固定の単一出力 |
| end | link-{index} | episodeLinks[index] |
| dialogue | choice-{index} | choices[index] またはデフォルト |
| condition | arm-{index}、else | conditionArms[index] / else |
| random | out-{index} | randomOutputs[index] |
| setVariable | (デフォルト) | 固定の単一出力 |
| event | (デフォルト) | 固定の単一出力 |
| comment | N/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 | #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 |