# 叙事架构代码实现指南

本文档提供游戏叙事系统的核心代码实现示例,覆盖 Etudes 层级化状态机、对话树、变量管理和分支剧情等关键模块。所有代码均可直接运行或稍作修改后集成到项目中。

---

## 目录

1. [Etudes 系统(Owlcat Games)](#1-etudes-系统owlcat-games)
2. [对话系统代码结构](#2-对话系统代码结构)
3. [变量管理方案](#3-变量管理方案)
4. [分支剧情代码示例](#4-分支剧情代码示例)
5. [叙事工具集成](#5-叙事工具集成)

---

## 1. Etudes 系统(Owlcat Games)

Owlcat Games 在《开拓者》系列中使用 Etudes 系统替代传统全局 Flag,管理超过 30,000 个叙事变量。Etudes 是层级化的状态机,通过优先级排序和冲突检测实现可控的叙事状态管理。

### 1.1 核心概念:层级化状态机

与传统扁平的布尔 Flag 不同,Etudes 使用树状结构组织叙事状态。每个节点代表一个叙事片段或状态,子节点继承父节点的上下文。

```csharp
// Etudes系统核心数据模型(C#)
using System;
using System.Collections.Generic;
using System.Linq;

namespace NarrativeSystem.Etudes
{
///
/// Etude节点状态枚举
///

public enum EtudeState
{
Inactive, // 未激活(灰色)
Pending, // 等待条件满足(橙色)
Active // 已激活(绿色)
}

///
/// Etude优先级定义
/// 高优先级Etude可以覆盖低优先级的状态变更
///

public enum EtudePriority
{
Background = 0, // 背景叙事,最低优先级
Normal = 100, // 普通任务
Important = 200, // 重要剧情
Critical = 300, // 关键节点,最高优先级
Blocking = 400 // 阻塞级,暂停其他叙事
}

///
/// Etude节点:叙事状态的基本单位
///

public class EtudeNode
{
public string Id { get; set; } // 唯一标识符
public string Name { get; set; } // 显示名称
public string Description { get; set; } // 描述
public EtudePriority Priority { get; set; } // 优先级
public EtudeState State { get; private set; } // 当前状态
public EtudeNode Parent { get; set; } // 父节点
public List Children { get; set; } // 子节点列表
public List Conditions { get; set; } // 激活条件
public List OnActivate { get; set; } // 激活时动作
public List OnDeactivate { get; set; } // 取消激活时动作

// 冲突规则:哪些Etude与此节点互斥
public List ConflictsWith { get; set; }

public EtudeNode()
{
Children = new List();
Conditions = new List();
OnActivate = new List();
OnDeactivate = new List();
ConflictsWith = new List();
State = EtudeState.Inactive;
}

///
/// 评估当前节点是否应该激活
/// 同时检查父节点状态和自身条件
///

public bool Evaluate(EtudeManager manager)
{
// 如果父节点未激活,子节点不能激活
if (Parent != null && Parent.State != EtudeState.Active)
return false;

// 检查所有条件是否满足
return Conditions.All(c => c.Check(manager));
}

///
/// 激活此Etude节点
///

public void Activate(EtudeManager manager)
{
if (State == EtudeState.Active) return;

State = EtudeState.Active;
foreach (var action in OnActivate)
{
action.Execute(manager);
}
}

///
/// 取消激活此Etude节点
///

public void Deactivate(EtudeManager manager)
{
if (State == EtudeState.Inactive) return;

// 递归取消激活所有子节点
foreach (var child in Children)
{
child.Deactivate(manager);
}

State = EtudeState.Inactive;
foreach (var action in OnDeactivate)
{
action.Execute(manager);
}
}
}

///
/// 条件接口:所有激活条件必须实现
///

public interface IEtudeCondition
{
bool Check(EtudeManager manager);
}

///
/// 动作接口:激活/取消激活时执行的操作
///

public interface IEtudeAction
{
void Execute(EtudeManager manager);
}
}
```

### 1.2 JSON 配置示例

Etudes 适合通过数据驱动配置,策划可以在 JSON 中定义复杂的叙事状态树。

```json
{
"$schema": "etude-config-v1",
"description": "第一章主线任务Etudes配置",
"rootEtudes": [
{
"id": "chapter_01",
"name": "第一章:觉醒",
"description": "游戏开场章节",
"priority": "Normal",
"conditions": [
{ "type": "GameStarted", "value": true }
],
"onActivate": [
{ "type": "SetFlag", "key": "chapter", "value": 1 },
{ "type": "UnlockMap", "mapId": "village" }
],
"children": [
{
"id": "c01_intro",
"name": "开场动画",
"priority": "Critical",
"conditions": [
{ "type": "EtudeActive", "etudeId": "chapter_01" }
],
"onActivate": [
{ "type": "PlayCutscene", "cutsceneId": "intro_01" }
],
"children": [
{
"id": "c01_talk_elder",
"name": "与长老对话",
"priority": "Important",
"conditions": [
{ "type": "CutsceneFinished", "cutsceneId": "intro_01" }
],
"onActivate": [
{ "type": "SpawnNPC", "npcId": "elder", "location": "village_center" },
{ "type": "AddDialogueOption", "npcId": "elder", "dialogueId": "c01_greeting" }
]
},
{
"id": "c01_find_artifact",
"name": "寻找古代遗物",
"priority": "Important",
"conditions": [
{ "type": "DialogueCompleted", "dialogueId": "c01_greeting" }
],
"onActivate": [
{ "type": "AddQuest", "questId": "q01_artifact" },
{ "type": "SpawnItem", "itemId": "ancient_artifact", "location": "cave_depths" }
]
}
]
},
{
"id": "c01_secret_ending",
"name": "隐藏结局触发",
"priority": "Critical",
"conditions": [
{ "type": "FlagEquals", "key": "karma", "value": -100 },
{ "type": "EtudeActive", "etudeId": "chapter_01" }
],
"conflictsWith": ["c01_intro"],
"onActivate": [
{ "type": "TriggerEnding", "endingId": "secret_dark" }
]
}
]
}
]
}
```

### 1.3 优先级排序与冲突检测

```csharp
using System;
using System.Collections.Generic;
using System.Linq;

namespace NarrativeSystem.Etudes
{
///
/// Etudes管理器:核心控制器
/// 每帧或每个叙事事件发生时重新评估所有Etude状态
///

public class EtudeManager
{
// 所有已注册的Etude节点
private Dictionary _etudes = new Dictionary();

// 全局变量存储
private Dictionary _globalFlags = new Dictionary();

// 冲突日志,用于调试
public List ConflictLogs { get; private set; } = new List();

///
/// 注册Etude节点到管理器
///

public void RegisterEtude(EtudeNode etude)
{
if (_etudes.ContainsKey(etude.Id))
{
throw new ArgumentException($"Etude {etude.Id} 已存在!");
}
_etudes[etude.Id] = etude;
}

///
/// 设置全局变量
///

public void SetFlag(string key, object value)
{
_globalFlags[key] = value;
// 变量变更后重新评估所有Etude
ReevaluateAll();
}

///
/// 获取全局变量
///

public T GetFlag(string key)
{
if (_globalFlags.TryGetValue(key, out var value) && value is T typed)
{
return typed;
}
return default;
}

///
/// 核心评估方法
/// 1. 按优先级排序所有待激活Etude
/// 2. 检测冲突
/// 3. 执行状态变更
///

public void ReevaluateAll()
{
// 收集所有可以激活的Etude(条件满足)
var candidates = new List();
foreach (var etude in _etudes.Values)
{
if (etude.Evaluate(this))
{
candidates.Add(etude);
}
else if (etude.State == EtudeState.Active)
{
// 如果已激活但条件不再满足,取消激活
etude.Deactivate(this);
}
}

// 按优先级降序排序:高优先级先处理
candidates.Sort((a, b) => b.Priority.CompareTo(a.Priority));

// 已激活的Etude集合(用于冲突检测)
var activatedIds = new HashSet();
ConflictLogs.Clear();

foreach (var candidate in candidates)
{
// 检查是否与已激活的Etude冲突
bool hasConflict = false;
foreach (var conflictId in candidate.ConflictsWith)
{
if (activatedIds.Contains(conflictId))
{
hasConflict = true;
ConflictLogs.Add(new ConflictLog
{
EtudeA = candidate.Id,
EtudeB = conflictId,
Resolution = candidate.Priority >= _etudes[conflictId].Priority
? "高优先级覆盖"
: "低优先级被跳过"
});

// 如果当前优先级更高,覆盖冲突的Etude
if (candidate.Priority >= _etudes[conflictId].Priority)
{
_etudes[conflictId].Deactivate(this);
activatedIds.Remove(conflictId);
}
else
{
// 优先级低,跳过此候选
break;
}
}
}

if (!hasConflict || candidate.Priority >= candidate.ConflictsWith
.Where(c => activatedIds.Contains(c))
.Select(c => _etudes[c].Priority)
.DefaultIfEmpty(EtudePriority.Background)
.Max())
{
candidate.Activate(this);
activatedIds.Add(candidate.Id);
}
}
}
}

///
/// 冲突记录,用于可视化调试
///

public class ConflictLog
{
public string EtudeA { get; set; }
public string EtudeB { get; set; }
public string Resolution { get; set; }
public DateTime Timestamp { get; set; } = DateTime.Now;
}
}
```

### 1.4 可视化调试工具

```python
# Etudes 状态树可视化(Python + 命令行)
# 可用于运行时调试,输出类似文件树的结构
# 绿色 = Active, 橙色 = Pending, 灰色 = Inactive

from enum import Enum
from typing import List, Dict, Optional
from dataclasses import dataclass, field

class EtudeState(Enum):
INACTIVE = "inactive"
PENDING = "pending"
ACTIVE = "active"

# ANSI 颜色代码
COLOR_MAP = {
EtudeState.ACTIVE: "\033[32m", # 绿色
EtudeState.PENDING: "\033[33m", # 橙色
EtudeState.INACTIVE: "\033[90m", # 灰色
}
RESET = "\033[0m"

@dataclass
class EtudeNode:
id: str
name: str
state: EtudeState
priority: int = 100
children: List['EtudeNode'] = field(default_factory=list)
conflicts: List[str] = field(default_factory=list)

class EtudeVisualizer:
"""Etudes 状态树可视化器"""

def __init__(self, root_nodes: List[EtudeNode]):
self.roots = root_nodes
self.conflict_logs: List[Dict] = []

def _colorize(self, text: str, state: EtudeState) -> str:
"""根据状态给文本上色"""
return f"{COLOR_MAP[state]}{text}{RESET}"

def _render_node(self, node: EtudeNode, prefix: str = "", is_last: bool = True) -> str:
"""递归渲染单个节点"""
connector = "└── " if is_last else "├── "
state_icon = {
EtudeState.ACTIVE: "[+]",
EtudeState.PENDING: "[~]",
EtudeState.INACTIVE: "[ ]",
}[node.state]

line = f"{prefix}{connector}{state_icon} {node.name} ({node.id})"
colored_line = self._colorize(line, node.state)

# 如果有冲突,附加警告
if node.conflicts and node.state == EtudeState.ACTIVE:
conflict_info = f" [冲突: {', '.join(node.conflicts)}]"
colored_line += self._colorize(conflict_info, EtudeState.PENDING)

result = [colored_line]

# 递归渲染子节点
child_prefix = prefix + (" " if is_last else "│ ")
for i, child in enumerate(node.children):
is_last_child = (i == len(node.children) - 1)
result.append(self._render_node(child, child_prefix, is_last_child))

return "\n".join(result)

def render(self) -> str:
"""渲染完整的Etudes状态树"""
lines = []
lines.append("=" * 50)
lines.append("Etudes 状态树可视化")
lines.append(f"图例: {self._colorize('[+] 激活', EtudeState.ACTIVE)} | "
f"{self._colorize('[~] 等待', EtudeState.PENDING)} | "
f"{self._colorize('[ ] 未激活', EtudeState.INACTIVE)}")
lines.append("=" * 50)

for root in self.roots:
lines.append(self._render_node(root))
lines.append("")

# 输出冲突日志
if self.conflict_logs:
lines.append("-" * 50)
lines.append("冲突日志:")
for log in self.conflict_logs:
lines.append(f" [{log['timestamp']}] {log['a']} vs {log['b']}: {log['resolution']}")

return "\n".join(lines)


# ===== 使用示例 =====
if __name__ == "__main__":
# 构建示例Etudes树
child1 = EtudeNode("c01_talk", "与长老对话", EtudeState.ACTIVE, priority=200)
child2 = EtudeNode("c01_fight", "战斗事件", EtudeState.INACTIVE, priority=150)

root = EtudeNode("chapter_01", "第一章:觉醒", EtudeState.ACTIVE, priority=100,
children=[child1, child2],
conflicts=["chapter_02"])

visualizer = EtudeVisualizer([root])
visualizer.conflict_logs = [
{"timestamp": "10:23:05", "a": "chapter_01", "b": "chapter_02", "resolution": "高优先级覆盖"}
]

print(visualizer.render())
```

**可视化输出示例:**

```text
==================================================
Etudes 状态树可视化
图例: [+] 激活 | [~] 等待 | [ ] 未激活
==================================================
└── [+] 第一章:觉醒 (chapter_01) [冲突: chapter_02]
├── [+] 与长老对话 (c01_talk)
└── [ ] 战斗事件 (c01_fight)

--------------------------------------------------
冲突日志:
[10:23:05] chapter_01 vs chapter_02: 高优先级覆盖
```

---

## 2. 对话系统代码结构

### 2.1 对话树数据结构

```csharp
// 对话树核心数据结构(C#)
using System;
using System.Collections.Generic;
using System.Linq;

namespace NarrativeSystem.Dialogue
{
///
/// 对话节点基类
/// 所有对话节点(文本、选择、条件、动作)的抽象基类
///

public abstract class DialogueNode
{
public string Id { get; set; }
public string OwnerId { get; set; } // 说话者ID
public List NextNodes { get; set; } = new List();
public List Conditions { get; set; } = new List();

///
/// 检查此节点是否可以显示
///

public virtual bool CanDisplay(IDialogueContext context)
{
return Conditions.All(c => c.Evaluate(context));
}

///
/// 节点被访问时执行
///

public abstract void OnVisit(IDialogueContext context);
}

///
/// 文本节点:显示NPC对话文本
///

public class TextNode : DialogueNode
{
public string Text { get; set; } // 对话文本(支持本地化Key)
public string Emotion { get; set; } // 情绪标签(影响立绘/动画)
public float AutoAdvanceDelay { get; set; } // 自动推进延迟(-1表示手动)

public override void OnVisit(IDialogueContext context)
{
context.DisplayText(OwnerId, Text, Emotion);

if (AutoAdvanceDelay > 0)
{
context.ScheduleAdvance(AutoAdvanceDelay, NextNodes.FirstOrDefault());
}
}
}

///
/// 玩家选择节点:提供分支选项
///

public class ChoiceNode : DialogueNode
{
public List Choices { get; set; } = new List();

public override void OnVisit(IDialogueContext context)
{
// 过滤掉不满足条件的选项
var availableChoices = Choices.Where(c => c.CanDisplay(context)).ToList();
context.DisplayChoices(availableChoices);
}
}

///
/// 单个选项定义
///

public class DialogueChoice
{
public string Id { get; set; }
public string Text { get; set; }
public string Tooltip { get; set; } // 鼠标悬停提示
public List Conditions { get; set; } = new List();
public List Actions { get; set; } = new List();
public string NextNodeId { get; set; }

public bool CanDisplay(IDialogueContext context)
{
return Conditions.All(c => c.Evaluate(context));
}
}

///
/// 动作节点:执行无副作用的叙事动作
///

public class ActionNode : DialogueNode
{
public List Actions { get; set; } = new List();

public override void OnVisit(IDialogueContext context)
{
foreach (var action in Actions)
{
action.Execute(context);
}
// 自动跳转到下一个节点
context.AdvanceTo(NextNodes.FirstOrDefault());
}
}

///
/// 条件节点:根据条件走向不同分支
///

public class BranchNode : DialogueNode
{
public List Branches { get; set; } = new List();
public string DefaultNodeId { get; set; } // 默认分支(无条件满足时)

public override void OnVisit(IDialogueContext context)
{
foreach (var branch in Branches)
{
if (branch.Condition.Evaluate(context))
{
context.AdvanceTo(branch.TargetNodeId);
return;
}
}
context.AdvanceTo(DefaultNodeId);
}
}

public class ConditionalBranch
{
public DialogueCondition Condition { get; set; }
public string TargetNodeId { get; set; }
}
}
```

### 2.2 条件判断系统

```csharp
namespace NarrativeSystem.Dialogue
{
///
/// 条件接口:所有对话条件的基类
///

public abstract class DialogueCondition
{
public abstract bool Evaluate(IDialogueContext context);
}

///
/// 变量比较条件
///

public class VariableCondition : DialogueCondition
{
public string VariableName { get; set; }
public ComparisonOperator Operator { get; set; }
public object ExpectedValue { get; set; }

public override bool Evaluate(IDialogueContext context)
{
var actualValue = context.GetVariable(VariableName);
return CompareValues(actualValue, ExpectedValue, Operator);
}

private bool CompareValues(object actual, object expected, ComparisonOperator op)
{
if (actual == null || expected == null) return false;

var comparable = Comparer.Default;
int result = comparable.Compare(actual, expected);

return op switch
{
ComparisonOperator.Equals => result == 0,
ComparisonOperator.NotEquals => result != 0,
ComparisonOperator.GreaterThan => result > 0,
ComparisonOperator.LessThan => result < 0,
ComparisonOperator.GreaterOrEqual => result >= 0,
ComparisonOperator.LessOrEqual => result <= 0,
_ => false
};
}
}

///
/// 玩家背包物品检查
///

public class HasItemCondition : DialogueCondition
{
public string ItemId { get; set; }
public int Count { get; set; } = 1;

public override bool Evaluate(IDialogueContext context)
{
return context.GetItemCount(ItemId) >= Count;
}
}

///
/// 关系值条件
///

public class RelationshipCondition : DialogueCondition
{
public string NpcId { get; set; }
public ComparisonOperator Operator { get; set; }
public int Value { get; set; }

public override bool Evaluate(IDialogueContext context)
{
var relationship = context.GetRelationship(NpcId);
return VariableCondition.CompareValues(relationship, Value, Operator);
}
}

///
/// 复合条件:AND / OR / NOT
///

public class CompositeCondition : DialogueCondition
{
public LogicOperator LogicOp { get; set; }
public List SubConditions { get; set; } = new List();

public override bool Evaluate(IDialogueContext context)
{
return LogicOp switch
{
LogicOperator.And => SubConditions.All(c => c.Evaluate(context)),
LogicOperator.Or => SubConditions.Any(c => c.Evaluate(context)),
LogicOperator.Not => !SubConditions.FirstOrDefault()?.Evaluate(context) ?? true,
_ => false
};
}
}

public enum ComparisonOperator { Equals, NotEquals, GreaterThan, LessThan, GreaterOrEqual, LessOrEqual }
public enum LogicOperator { And, Or, Not }
}
```

### 2.3 变量绑定与上下文

```csharp
namespace NarrativeSystem.Dialogue
{
///
/// 对话上下文接口:提供对话系统与游戏世界的桥梁
///

public interface IDialogueContext
{
// 变量访问
object GetVariable(string name);
void SetVariable(string name, object value);

// 玩家数据
int GetItemCount(string itemId);
int GetRelationship(string npcId);

// UI 交互
void DisplayText(string speakerId, string text, string emotion);
void DisplayChoices(List choices);
void AdvanceTo(string nodeId);
void ScheduleAdvance(float delay, string nodeId);
}

///
/// 默认实现:对话上下文
///

public class DefaultDialogueContext : IDialogueContext
{
private readonly Dictionary _variables;
private readonly IGameState _gameState;
private readonly IDialogueUI _ui;

public DefaultDialogueContext(IGameState gameState, IDialogueUI ui)
{
_gameState = gameState;
_ui = ui;
_variables = new Dictionary();
}

public object GetVariable(string name)
{
// 优先从本地对话变量查找,再到全局游戏状态
if (_variables.TryGetValue(name, out var localValue))
return localValue;
return _gameState.GetGlobalVariable(name);
}

public void SetVariable(string name, object value)
{
_variables[name] = value;
// 同步到全局状态(如果需要持久化)
_gameState.SetGlobalVariable(name, value);
}

public int GetItemCount(string itemId) => _gameState.Inventory.GetCount(itemId);
public int GetRelationship(string npcId) => _gameState.Relationships.Get(npcId);

public void DisplayText(string speakerId, string text, string emotion)
{
_ui.ShowText(speakerId, Localize(text), emotion);
}

public void DisplayChoices(List choices)
{
_ui.ShowChoices(choices.Select(c => new UIChoice
{
Text = Localize(c.Text),
Tooltip = Localize(c.Tooltip),
OnSelect = () => SelectChoice(c)
}).ToList());
}

private void SelectChoice(DialogueChoice choice)
{
// 执行选项附带的所有动作
foreach (var action in choice.Actions)
{
action.Execute(this);
}
AdvanceTo(choice.NextNodeId);
}

public void AdvanceTo(string nodeId)
{
if (!string.IsNullOrEmpty(nodeId))
{
_ui.Clear();
_gameState.Dialogue.AdvanceToNode(nodeId);
}
else
{
_gameState.Dialogue.EndDialogue();
}
}

public void ScheduleAdvance(float delay, string nodeId)
{
_ui.ScheduleAfterDelay(delay, () => AdvanceTo(nodeId));
}

private string Localize(string key) => _gameState.Localization.Get(key);
}
}
```

### 2.4 对话数据 JSON 配置

```json
{
"dialogueId": "dlg_elder_greeting",
"participants": ["player", "elder"],
"entryNode": "node_01",
"nodes": {
"node_01": {
"type": "TextNode",
"ownerId": "elder",
"text": "TEXT_ELDER_GREETING",
"emotion": "neutral",
"nextNodes": ["node_02"]
},
"node_02": {
"type": "ChoiceNode",
"choices": [
{
"id": "choice_friendly",
"text": "TEXT_CHOICE_FRIENDLY",
"nextNodeId": "node_friendly",
"actions": [
{ "type": "ModifyRelationship", "npcId": "elder", "delta": 10 }
]
},
{
"id": "choice_rude",
"text": "TEXT_CHOICE_RUDE",
"nextNodeId": "node_rude",
"actions": [
{ "type": "ModifyRelationship", "npcId": "elder", "delta": -20 }
]
},
{
"id": "choice_quest",
"text": "TEXT_CHOICE_QUEST",
"conditions": [
{ "type": "HasItem", "itemId": "ancient_artifact", "count": 1 }
],
"nextNodeId": "node_quest_complete",
"actions": [
{ "type": "RemoveItem", "itemId": "ancient_artifact", "count": 1 },
{ "type": "CompleteQuest", "questId": "q01_artifact" }
]
}
]
},
"node_friendly": {
"type": "TextNode",
"ownerId": "elder",
"text": "TEXT_ELDER_FRIENDLY",
"emotion": "happy",
"nextNodes": ["node_end"]
},
"node_rude": {
"type": "TextNode",
"ownerId": "elder",
"text": "TEXT_ELDER_ANGRY",
"emotion": "angry",
"nextNodes": ["node_end"]
},
"node_quest_complete": {
"type": "TextNode",
"ownerId": "elder",
"text": "TEXT_ELDER_QUEST_DONE",
"emotion": "surprised",
"nextNodes": ["node_reward"]
},
"node_reward": {
"type": "ActionNode",
"actions": [
{ "type": "GiveItem", "itemId": "gold", "count": 100 },
{ "type": "SetFlag", "key": "elder_quest_done", "value": true }
],
"nextNodes": ["node_end"]
},
"node_end": {
"type": "ActionNode",
"actions": [
{ "type": "EndDialogue" }
],
"nextNodes": []
}
}
}
```

---

## 3. 变量管理方案

### 3.1 全局 Flag 方法(基础版)

```python
# 全局 Flag 管理器(Python)
# 适用于小型项目,简单直接

from typing import Any, Dict, Optional, Callable, List
from dataclasses import dataclass, field
from datetime import datetime
import json
import os

@dataclass
class FlagChangeEvent:
"""Flag 变更事件记录"""
flag_name: str
old_value: Any
new_value: Any
timestamp: datetime
source: str # 触发来源(对话、任务、脚本等)

class GlobalFlagManager:
"""
全局 Flag 管理器
适用场景:小型项目、原型开发
局限性:变量过多时难以维护,无命名空间隔离
"""

def __init__(self):
self._flags: Dict[str, Any] = {}
self._listeners: Dict[str, List[Callable]] = {}
self._history: List[FlagChangeEvent] = []
self._dirty = False # 是否有未保存的变更

def set(self, name: str, value: Any, source: str = "system") -> None:
"""设置Flag值"""
old_value = self._flags.get(name)
if old_value != value:
self._flags[name] = value
self._dirty = True

# 记录变更历史
event = FlagChangeEvent(name, old_value, value, datetime.now(), source)
self._history.append(event)

# 通知监听器
self._notify(name, old_value, value)

def get(self, name: str, default: Any = None) -> Any:
"""获取Flag值"""
return self._flags.get(name, default)

def toggle(self, name: str) -> bool:
"""切换布尔Flag"""
current = bool(self.get(name, False))
self.set(name, not current, source="toggle")
return not current

def increment(self, name: str, delta: int = 1) -> int:
"""数值Flag自增"""
current = int(self.get(name, 0))
new_value = current + delta
self.set(name, new_value, source="increment")
return new_value

def add_listener(self, name: str, callback: Callable) -> None:
"""添加Flag变更监听器"""
if name not in self._listeners:
self._listeners[name] = []
self._listeners[name].append(callback)

def _notify(self, name: str, old_val: Any, new_val: Any) -> None:
"""触发监听器回调"""
for listener in self._listeners.get(name, []):
try:
listener(name, old_val, new_val)
except Exception as e:
print(f"[FlagManager] 监听器异常: {e}")

def save(self, filepath: str) -> None:
"""持久化到JSON文件"""
data = {
"flags": self._flags,
"history": [
{
"flag": e.flag_name,
"from": e.old_value,
"to": e.new_value,
"time": e.timestamp.isoformat(),
"source": e.source
}
for e in self._history
]
}
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
self._dirty = False

def load(self, filepath: str) -> None:
"""从JSON文件加载"""
if not os.path.exists(filepath):
return
with open(filepath, 'r', encoding='utf-8') as f:
data = json.load(f)
self._flags = data.get("flags", {})
self._dirty = False


# ===== 使用示例 =====
if __name__ == "__main__":
flags = GlobalFlagManager()

# 基础操作
flags.set("player_level", 5, source="level_up")
flags.set("has_met_elder", True, source="dialogue")

# 数值操作
flags.increment("karma", -10)
print(f"当前业力值: {flags.get('karma')}")

# 监听变更
def on_karma_change(name, old, new):
print(f"[事件] {name} 从 {old} 变为 {new}")

flags.add_listener("karma", on_karma_change)
flags.increment("karma", 5) # 触发回调

# 保存进度
flags.save("save_data.json")
```

### 3.2 层级化状态机(进阶版)

```python
# 层级化命名空间状态机(Python)
# 适用于中型到大型项目,解决全局Flag混乱问题

from typing import Any, Dict, List, Optional, Set
from dataclasses import dataclass, field
from enum import Enum
import copy

class VariableType(Enum):
"""变量类型枚举,用于类型安全检查"""
BOOL = "bool"
INT = "int"
FLOAT = "float"
STRING = "string"
VECTOR = "vector" # 向量/列表

@dataclass
class VariableDefinition:
"""变量定义元数据"""
name: str
var_type: VariableType
default_value: Any
description: str = ""
persistent: bool = True # 是否持久化保存
sync_to_ui: bool = False # 是否同步到UI

class Namespace:
"""
命名空间:隔离不同系统的变量
例如:quest.*, player.*, world.*, dialogue.*
"""

def __init__(self, name: str, parent: Optional['Namespace'] = None):
self.name = name
self.parent = parent
self._variables: Dict[str, Any] = {}
self._definitions: Dict[str, VariableDefinition] = {}
self._children: Dict[str, 'Namespace'] = {}

def define(self, definition: VariableDefinition) -> None:
"""注册变量定义(应在初始化时调用)"""
self._definitions[definition.name] = definition
self._variables[definition.name] = copy.deepcopy(definition.default_value)

def set(self, name: str, value: Any) -> bool:
"""
设置变量值,包含类型检查
返回是否设置成功
"""
if name not in self._definitions:
raise KeyError(f"变量 {self.name}.{name} 未定义,请先调用 define()")

definition = self._definitions[name]
if not self._type_check(value, definition.var_type):
raise TypeError(
f"变量 {self.name}.{name} 类型错误,"
f"期望 {definition.var_type.value},实际 {type(value)}"
)

self._variables[name] = value
return True

def get(self, name: str) -> Any:
"""获取变量值,如不存在则向上查找父命名空间"""
if name in self._variables:
return self._variables[name]
if self.parent:
return self.parent.get(name)
raise KeyError(f"变量 {name} 在命名空间 {self.name} 及其父空间中不存在")

def get_child(self, name: str) -> 'Namespace':
"""获取或创建子命名空间"""
if name not in self._children:
self._children[name] = Namespace(f"{self.name}.{name}", parent=self)
return self._children[name]

def get_full_path(self, name: str) -> str:
"""获取变量完整路径"""
return f"{self.name}.{name}"

def list_all(self) -> Dict[str, Any]:
"""列出当前命名空间所有变量"""
return dict(self._variables)

@staticmethod
def _type_check(value: Any, expected: VariableType) -> bool:
"""类型检查"""
type_map = {
VariableType.BOOL: bool,
VariableType.INT: int,
VariableType.FLOAT: (int, float),
VariableType.STRING: str,
VariableType.VECTOR: list,
}
return isinstance(value, type_map.get(expected, object))


class HierarchicalStateManager:
"""
层级化状态管理器
组织形式:root -> player / world / quests / dialogue / etudes
"""

def __init__(self):
self._root = Namespace("root")
self._namespaces: Dict[str, Namespace] = {}
self._setup_default_namespaces()

def _setup_default_namespaces(self):
"""初始化默认命名空间结构"""
# player 命名空间:玩家相关状态
player = self.create_namespace("player")
player.define(VariableDefinition("level", VariableType.INT, 1, "玩家等级"))
player.define(VariableDefinition("exp", VariableType.INT, 0, "经验值"))
player.define(VariableDefinition("name", VariableType.STRING, "", "玩家名称"))

# world 命名空间:世界状态
world = self.create_namespace("world")
world.define(VariableDefinition("day", VariableType.INT, 1, "游戏天数"))
world.define(VariableDefinition("time", VariableType.FLOAT, 8.0, "当前时间"))
world.define(VariableDefinition("weather", VariableType.STRING, "sunny", "天气"))

# quests 命名空间:任务状态(支持子命名空间)
quests = self.create_namespace("quests")

# dialogue 命名空间:对话记录
dialogue = self.create_namespace("dialogue")
dialogue.define(VariableDefinition("last_speaker", VariableType.STRING, "", "最后对话NPC"))
dialogue.define(VariableDefinition("choices_made", VariableType.VECTOR, [], "已做选择记录"))

def create_namespace(self, path: str) -> Namespace:
"""创建命名空间,支持点号路径如 'quests.main'"""
parts = path.split(".")
current = self._root

for part in parts:
if part not in current._children:
current._children[part] = Namespace(part, parent=current)
current = current._children[part]

self._namespaces[path] = current
return current

def get(self, full_path: str) -> Any:
"""
通过完整路径获取变量值
格式:namespace.variable 或 namespace.child.variable
"""
parts = full_path.split(".")
if len(parts) < 2:
raise ValueError("路径格式错误,应为 'namespace.variable'")

# 逐级查找命名空间
current = self._root
for part in parts[:-1]:
if part not in current._children:
raise KeyError(f"命名空间 {part} 不存在")
current = current._children[part]

return current.get(parts[-1])

def set(self, full_path: str, value: Any) -> None:
"""通过完整路径设置变量值"""
parts = full_path.split(".")
if len(parts) < 2:
raise ValueError("路径格式错误")

current = self._root
for part in parts[:-1]:
if part not in current._children:
raise KeyError(f"命名空间 {part} 不存在")
current = current._children[part]

current.set(parts[-1], value)

def query(self, pattern: str) -> Dict[str, Any]:
"""
按模式查询变量
例如:query("quests.*.status") 返回所有任务状态
"""
results = {}
# 简化的通配符匹配实现
for ns_path, namespace in self._namespaces.items():
for var_name, value in namespace.list_all().items():
full = f"{ns_path}.{var_name}" if ns_path else var_name
if self._match_pattern(full, pattern):
results[full] = value
return results

def _match_pattern(self, text: str, pattern: str) -> bool:
"""简单的通配符匹配"""
import fnmatch
return fnmatch.fnmatch(text, pattern)

def export_namespace(self, path: str) -> Dict[str, Any]:
"""导出指定命名空间下所有变量(用于保存)"""
if path not in self._namespaces:
return {}
return self._namespaces[path].list_all()


# ===== 使用示例 =====
if __name__ == "__main__":
manager = HierarchicalStateManager()

# 设置玩家数据
manager.set("player.level", 5)
manager.set("player.name", "亚瑟")

# 设置世界状态
manager.set("world.day", 3)
manager.set("world.weather", "rainy")

# 动态创建任务子命名空间
q1 = manager.create_namespace("quests.q01_artifact")
q1.define(VariableDefinition("status", VariableType.STRING, "active", "任务状态"))
q1.define(VariableDefinition("progress", VariableType.INT, 0, "任务进度"))

manager.set("quests.q01_artifact.status", "completed")
manager.set("quests.q01_artifact.progress", 100)

print(f"玩家等级: {manager.get('player.level')}")
print(f"任务状态: {manager.get('quests.q01_artifact.status')}")
```

### 3.3 持久化存储方案

```csharp
// 持久化存储:存档系统(C#)
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace NarrativeSystem.Persistence
{
///
/// 存档数据容器
///

public class SaveGameData
{
[JsonPropertyName("version")]
public int Version { get; set; } = 1;

[JsonPropertyName("timestamp")]
public DateTime Timestamp { get; set; }

[JsonPropertyName("playTime")]
public float PlayTimeSeconds { get; set; }

[JsonPropertyName("variables")]
public Dictionary Variables { get; set; } = new();

[JsonPropertyName("etudes")]
public List Etudes { get; set; } = new();

[JsonPropertyName("quests")]
public List Quests { get; set; } = new();

[JsonPropertyName("dialogueHistory")]
public List DialogueHistory { get; set; } = new();

[JsonPropertyName("playerPosition")]
public Vector3SaveData PlayerPosition { get; set; }

[JsonPropertyName("currentScene")]
public string CurrentScene { get; set; }
}

public class EtudeSaveData
{
[JsonPropertyName("id")]
public string Id { get; set; }

[JsonPropertyName("state")]
public string State { get; set; }

[JsonPropertyName("activationTime")]
public DateTime? ActivationTime { get; set; }
}

public class QuestSaveData
{
[JsonPropertyName("id")]
public string Id { get; set; }

[JsonPropertyName("status")]
public string Status { get; set; }

[JsonPropertyName("objectives")]
public List Objectives { get; set; } = new();
}

public class ObjectiveSaveData
{
[JsonPropertyName("id")]
public string Id { get; set; }

[JsonPropertyName("completed")]
public bool Completed { get; set; }

[JsonPropertyName("progress")]
public int Progress { get; set; }
}

public class DialogueHistoryEntry
{
[JsonPropertyName("dialogueId")]
public string DialogueId { get; set; }

[JsonPropertyName("nodeId")]
public string NodeId { get; set; }

[JsonPropertyName("choiceId")]
public string ChoiceId { get; set; }

[JsonPropertyName("timestamp")]
public DateTime Timestamp { get; set; }
}

public class Vector3SaveData
{
[JsonPropertyName("x")]
public float X { get; set; }

[JsonPropertyName("y")]
public float Y { get; set; }

[JsonPropertyName("z")]
public float Z { get; set; }
}

///
/// 存档管理器
///

public class SaveManager
{
private readonly string _saveDirectory;
private readonly JsonSerializerOptions _jsonOptions;

public SaveManager(string saveDirectory)
{
_saveDirectory = saveDirectory;
_jsonOptions = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};

if (!Directory.Exists(_saveDirectory))
{
Directory.CreateDirectory(_saveDirectory);
}
}

///
/// 保存游戏
///

public void SaveGame(int slot, SaveGameData data)
{
data.Timestamp = DateTime.UtcNow;
var filepath = GetSaveFilePath(slot);
var json = JsonSerializer.Serialize(data, _jsonOptions);
File.WriteAllText(filepath, json);
}

///
/// 加载游戏
///

public SaveGameData LoadGame(int slot)
{
var filepath = GetSaveFilePath(slot);
if (!File.Exists(filepath))
{
throw new FileNotFoundException($"存档槽 {slot} 不存在");
}

var json = File.ReadAllText(filepath);
return JsonSerializer.Deserialize(json, _jsonOptions);
}

///
/// 列出所有存档
///

public List ListSaves()
{
var saves = new List();
foreach (var file in Directory.GetFiles(_saveDirectory, "save_*.json"))
{
var slotStr = Path.GetFileNameWithoutExtension(file).Replace("save_", "");
if (int.TryParse(slotStr, out int slot))
{
try
{
var data = LoadGame(slot);
saves.Add(new SaveInfo
{
Slot = slot,
Timestamp = data.Timestamp,
PlayTime = TimeSpan.FromSeconds(data.PlayTimeSeconds),
CurrentScene = data.CurrentScene
});
}
catch { /* 忽略损坏的存档 */ }
}
}
return saves;
}

private string GetSaveFilePath(int slot) =>
Path.Combine(_saveDirectory, $"save_{slot}.json");
}

public class SaveInfo
{
public int Slot { get; set; }
public DateTime Timestamp { get; set; }
public TimeSpan PlayTime { get; set; }
public string CurrentScene { get; set; }
}
}
```

---

## 4. 分支剧情代码示例

### 4.1 简单分支(if-else)

```python
# 简单分支剧情实现(Python)
# 适用场景:少量分支、快速原型

def simple_branching_example(player_state):
"""
简单的if-else分支示例
player_state: 包含玩家选择记录的字典
"""
has_saved_village = player_state.get("saved_village", False)
has_betrayed_king = player_state.get("betrayed_king", False)
karma = player_state.get("karma", 0)

# 第一幕:村庄事件
if has_saved_village:
print("村民热情地迎接你,孩子们围上来索要糖果。")
reward = "village_blessing"
else:
print("村庄一片废墟,幸存的村民用怨恨的眼神看着你。")
reward = "guilt_memory"

player_state["act1_reward"] = reward

# 第二幕:王座厅
if has_betrayed_king:
print("王座厅戒备森严,国王拒绝接见叛徒。")
if karma < -50:
print("卫兵冲上来逮捕你!")
return "arrest_ending"
else:
print("国王微笑着向你点头。")
if karma > 50:
print("国王授予你贵族头衔。")
player_state["noble_title"] = True

return "continue"


# 更优雅的数据驱动版本
BRANCH_TABLE = {
"village_event": {
"conditions": [
{"if": "saved_village", "then": "village_saved_branch"},
{"if": "burned_village", "then": "village_burned_branch"},
],
"default": "village_neutral_branch"
},
"village_saved_branch": {
"text": "村民热情地迎接你,孩子们围上来索要糖果。",
"rewards": ["village_blessing", "+10_karma"],
"next": "throne_room"
},
"village_burned_branch": {
"text": "村庄一片废墟,幸存的村民用怨恨的眼神看着你。",
"rewards": ["guilt_memory", "-20_karma"],
"next": "throne_room"
},
"village_neutral_branch": {
"text": "村庄宁静如常,似乎没人注意到你的到来。",
"next": "throne_room"
}
}

def evaluate_branch(branch_id, player_state):
"""评估分支表,返回对应的剧情内容"""
branch = BRANCH_TABLE.get(branch_id)
if not branch:
return None

# 检查是否有条件分支
conditions = branch.get("conditions", [])
for cond in conditions:
flag = cond["if"]
if player_state.get(flag, False):
return evaluate_branch(cond["then"], player_state)

# 返回默认分支
default_id = branch.get("default")
if default_id:
return evaluate_branch(default_id, player_state)

return branch
```

### 4.2 复杂分支(状态机)

```csharp
// 复杂分支剧情状态机(C#)
// 适用场景:多结局RPG、复杂叙事网络

using System;
using System.Collections.Generic;
using System.Linq;

namespace NarrativeSystem.Branching
{
///
/// 故事状态机:管理复杂分支剧情
///

public class StoryStateMachine
{
private readonly Dictionary _states;
private StoryState _currentState;
private readonly HashSet _visitedStates;
private readonly List _transitionHistory;

// 全局变量存储
private readonly Dictionary _variables;

// 结局注册表
private readonly Dictionary _endings;

public StoryStateMachine()
{
_states = new Dictionary();
_visitedStates = new HashSet();
_transitionHistory = new List();
_variables = new Dictionary();
_endings = new Dictionary();
}

///
/// 注册故事状态
///

public void RegisterState(StoryState state)
{
_states[state.Id] = state;
}

///
/// 注册结局
///

public void RegisterEnding(EndingDefinition ending)
{
_endings[ending.Id] = ending;
}

///
/// 初始化状态机,进入起始状态
///

public void Start(string initialStateId)
{
if (!_states.TryGetValue(initialStateId, out _currentState))
{
throw new ArgumentException($"初始状态 {initialStateId} 不存在");
}
EnterState(_currentState);
}

///
/// 推进剧情:根据当前状态和变量条件进行转移
///

public void Tick()
{
if (_currentState == null) return;

// 执行当前状态的OnUpdate
_currentState.OnUpdate(this);

// 检查所有出边转移条件
foreach (var transition in _currentState.Transitions)
{
if (transition.CanTransition(this))
{
ExecuteTransition(transition);
return;
}
}
}

///
/// 玩家做出选择
///

public void MakeChoice(string choiceId)
{
var choice = _currentState?.Choices.FirstOrDefault(c => c.Id == choiceId);
if (choice == null) return;

// 执行选择附带的效果
foreach (var effect in choice.Effects)
{
effect.Apply(this);
}

// 记录选择
RecordChoice(choiceId);

// 转移到选择指定的下一状态
if (!string.IsNullOrEmpty(choice.TargetStateId))
{
TransitionTo(choice.TargetStateId);
}
}

private void ExecuteTransition(StoryTransition transition)
{
var fromId = _currentState?.Id;
TransitionTo(transition.TargetStateId);

_transitionHistory.Add(new StoryTransition
{
FromStateId = fromId,
ToStateId = transition.TargetStateId,
TriggerType = transition.TriggerType,
Timestamp = DateTime.UtcNow
});
}

private void TransitionTo(string stateId)
{
if (!_states.TryGetValue(stateId, out var nextState))
{
// 检查是否是结局
if (_endings.TryGetValue(stateId, out var ending))
{
TriggerEnding(ending);
return;
}
throw new ArgumentException($"目标状态 {stateId} 不存在");
}

// 退出当前状态
_currentState?.OnExit(this);

// 进入新状态
_currentState = nextState;
EnterState(_currentState);
}

private void EnterState(StoryState state)
{
_visitedStates.Add(state.Id);
state.OnEnter(this);
}

private void TriggerEnding(EndingDefinition ending)
{
ending.OnTrigger(this);
_currentState = null; // 状态机结束
}

// 变量操作
public void SetVariable(string key, object value) => _variables[key] = value;
public T GetVariable(string key) => _variables.TryGetValue(key, out var v) ? (T)v : default;
public bool HasVisited(string stateId) => _visitedStates.Contains(stateId);
public IReadOnlyList GetHistory() => _transitionHistory;

private void RecordChoice(string choiceId)
{
var choices = GetVariable>("_choices") ?? new List();
choices.Add(choiceId);
SetVariable("_choices", choices);
}
}

///
/// 故事状态节点
///

public class StoryState
{
public string Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }

// 自动转移条件(无需玩家选择)
public List Transitions { get; set; } = new List();

// 玩家可做的选择
public List Choices { get; set; } = new List();

// 状态生命周期回调
public Action OnEnter { get; set; } = _ => { };
public Action OnUpdate { get; set; } = _ => { };
public Action OnExit { get; set; } = _ => { };
}

///
/// 状态转移定义
///

public class StoryTransition
{
public string TargetStateId { get; set; }
public TransitionTriggerType TriggerType { get; set; }
public Func Condition { get; set; }
public DateTime Timestamp { get; set; }
public string FromStateId { get; set; }
public string ToStateId { get; set; }

public bool CanTransition(StoryStateMachine machine) => Condition?.Invoke(machine) ?? true;
}

///
/// 玩家选择定义
///

public class PlayerChoice
{
public string Id { get; set; }
public string Text { get; set; }
public string Tooltip { get; set; }
public List Effects { get; set; } = new List();
public string TargetStateId { get; set; }
public Func VisibilityCondition { get; set; } = _ => true;
}

///
/// 选择效果
///

public class ChoiceEffect
{
public EffectType Type { get; set; }
public Dictionary Parameters { get; set; } = new Dictionary();

public void Apply(StoryStateMachine machine)
{
switch (Type)
{
case EffectType.SetVariable:
machine.SetVariable((string)Parameters["key"], Parameters["value"]);
break;
case EffectType.ModifyRelationship:
// 实现关系值修改
break;
case EffectType.AddItem:
// 实现添加物品
break;
case EffectType.TriggerEvent:
// 实现事件触发
break;
}
}
}

public class EndingDefinition
{
public string Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public EndingType Type { get; set; }
public Action OnTrigger { get; set; }
}

public enum TransitionTriggerType { Automatic, Conditional, Event }
public enum EffectType { SetVariable, ModifyRelationship, AddItem, TriggerEvent }
public enum EndingType { Good, Bad, Neutral, Secret, True }
}
```

### 4.3 延迟反馈实现

```python
# 延迟反馈系统(Python)
# 实现"早期的选择在后期产生回响"的设计目标

import time
from typing import Dict, List, Callable, Any
from dataclasses import dataclass, field
from datetime import datetime, timedelta

@dataclass
class DelayedConsequence:
"""
延迟后果:记录一个将在未来某个条件满足时触发的效果
"""
consequence_id: str
trigger_condition: Dict[str, Any] # 触发条件
effects: List[Dict[str, Any]] # 触发后的效果列表
source_choice: str # 来源于哪个选择
description: str = ""
expires_after: int = -1 # 多少秒后过期(-1表示不过期)
created_at: datetime = field(default_factory=datetime.now)
triggered: bool = False

class DelayedFeedbackSystem:
"""
延迟反馈系统

设计思路:
- 玩家做出选择时,不一定立即看到所有后果
- 某些后果被注册为"延迟后果",在特定条件满足时触发
- 条件可以是:到达某章节、进入某地图、与某NPC对话等
"""

def __init__(self):
self._pending: List[DelayedConsequence] = []
self._history: List[DelayedConsequence] = []
self._handlers: Dict[str, Callable] = {}
self._event_count = 0 # 用于追踪游戏内"时间"

def register_handler(self, effect_type: str, handler: Callable):
"""注册效果处理器"""
self._handlers[effect_type] = handler

def add_consequence(self, consequence: DelayedConsequence):
"""添加延迟后果"""
self._pending.append(consequence)
print(f"[延迟反馈] 注册后果: {consequence.description}")

def tick(self, current_state: Dict[str, Any]):
"""
每帧或每个叙事节点检查一次
current_state: 当前游戏状态快照
"""
self._event_count += 1
to_remove = []

for consequence in self._pending:
if consequence.triggered:
continue

# 检查是否过期
if consequence.expires_after > 0:
elapsed = (datetime.now() - consequence.created_at).total_seconds()
if elapsed > consequence.expires_after:
to_remove.append(consequence)
continue

# 检查触发条件
if self._check_condition(consequence.trigger_condition, current_state):
self._trigger(consequence, current_state)
to_remove.append(consequence)

for c in to_remove:
self._pending.remove(c)

def _check_condition(self, condition: Dict[str, Any], state: Dict[str, Any]) -> bool:
"""检查条件是否满足"""
condition_type = condition.get("type")

if condition_type == "chapter_reached":
return state.get("current_chapter") == condition["chapter"]

elif condition_type == "location_entered":
return state.get("current_location") == condition["location"]

elif condition_type == "npc_met":
met_npcs = state.get("met_npcs", [])
return condition["npc"] in met_npcs

elif condition_type == "flag_set":
return state.get(condition["flag"]) == condition["value"]

elif condition_type == "event_count":
return self._event_count >= condition["count"]

elif condition_type == "and":
return all(self._check_condition(c, state) for c in condition["conditions"])

elif condition_type == "or":
return any(self._check_condition(c, state) for c in condition["conditions"])

return False

def _trigger(self, consequence: DelayedConsequence, state: Dict[str, Any]):
"""触发后果效果"""
consequence.triggered = True
self._history.append(consequence)

print(f"\n[延迟反馈触发] {consequence.description}")
for effect in consequence.effects:
effect_type = effect["type"]
handler = self._handlers.get(effect_type)
if handler:
handler(effect, state)
else:
print(f" [效果] {effect_type}: {effect}")

def get_pending(self) -> List[DelayedConsequence]:
"""获取待触发的后果列表(用于调试UI)"""
return [c for c in self._pending if not c.triggered]

def get_history(self) -> List[DelayedConsequence]:
"""获取已触发的历史记录"""
return self._history


# ===== 使用示例:早期选择,后期回响 =====
def demo_delayed_feedback():
system = DelayedFeedbackSystem()

# 注册效果处理器
def handle_dialogue(effect, state):
print(f" -> 触发对话: {effect['dialogue_id']}")

def handle_flag(effect, state):
state[effect["key"]] = effect["value"]
print(f" -> 设置变量: {effect['key']} = {effect['value']}")

system.register_handler("show_dialogue", handle_dialogue)
system.register_handler("set_flag", handle_flag)

# 模拟游戏状态
game_state = {
"current_chapter": 1,
"current_location": "village",
"met_npcs": [],
"karma": 0
}

# 第1章:玩家在村庄做出选择——偷了商人的钱
print("=" * 50)
print("[第1章] 玩家偷了商人的钱...")

system.add_consequence(DelayedConsequence(
consequence_id="stolen_money_echo",
trigger_condition={
"type": "chapter_reached",
"chapter": 3
},
effects=[
{"type": "show_dialogue", "dialogue_id": "merchant_revenge"},
{"type": "set_flag", "key": "merchant_hates_you", "value": True}
],
source_choice="steal_from_merchant",
description="第1章偷钱的行为在第3章被商人记住"
))

# 第2章:正常推进
print("\n[第2章] 推进剧情...")
game_state["current_chapter"] = 2
system.tick(game_state) # 无触发

# 第3章:延迟反馈触发!
print("\n[第3章] 到达首都...")
game_state["current_chapter"] = 3
game_state["current_location"] = "capital"
system.tick(game_state) # 触发商人复仇

if __name__ == "__main__":
demo_delayed_feedback()
```

### 4.4 多结局管理

```python
# 多结局管理系统(Python)
# 根据玩家全程的选择组合计算最终结局

from typing import Dict, List, Callable, Optional
from dataclasses import dataclass
from enum import Enum

class EndingTier(Enum):
"""结局层级"""
NORMAL = "normal"
GOOD = "good"
BAD = "bad"
SECRET = "secret"
TRUE_END = "true_end"

@dataclass
class Ending:
"""结局定义"""
ending_id: str
title: str
description: str
tier: EndingTier
conditions: List[Dict] # 解锁条件
epilogue_text: str = ""

class MultipleEndingManager:
"""
多结局管理器

核心机制:
1. 每个结局有一组条件(变量值、选择记录、 visited状态等)
2. 结局按优先级排序(秘密结局 > 真结局 > 好/坏结局 > 普通结局)
3. 支持多条件组合:AND(必须全部满足)或 OR(满足任一)
"""

def __init__(self):
self._endings: Dict[str, Ending] = {}
self._priority_order = [
EndingTier.TRUE_END,
EndingTier.SECRET,
EndingTier.GOOD,
EndingTier.BAD,
EndingTier.NORMAL
]

def register_ending(self, ending: Ending):
"""注册结局"""
self._endings[ending.ending_id] = ending

def calculate_ending(self, player_state: Dict) -> Optional[Ending]:
"""
根据玩家状态计算最终结局
返回优先级最高的满足条件的结局
"""
candidates = []

for ending in self._endings.values():
if self._check_ending_conditions(ending.conditions, player_state):
candidates.append(ending)

if not candidates:
return None

# 按优先级排序,返回最高优先级
candidates.sort(key=lambda e: self._priority_order.index(e.tier))
return candidates[0]

def _check_ending_conditions(self, conditions: List[Dict], state: Dict) -> bool:
"""检查结局条件是否满足"""
for cond in conditions:
if not self._evaluate_condition(cond, state):
return False
return True

def _evaluate_condition(self, cond: Dict, state: Dict) -> bool:
"""评估单个条件"""
cond_type = cond.get("type")

if cond_type == "flag_equals":
return state.get(cond["flag"]) == cond["value"]

elif cond_type == "flag_gte":
return state.get(cond["flag"], 0) >= cond["value"]

elif cond_type == "flag_lte":
return state.get(cond["flag"], 0) <= cond["value"]

elif cond_type == "choice_made":
choices = state.get("choices_made", [])
return cond["choice_id"] in choices

elif cond_type == "all_choices":
choices = state.get("choices_made", [])
return all(c in choices for c in cond["choice_ids"])

elif cond_type == "any_choice":
choices = state.get("choices_made", [])
return any(c in choices for c in cond["choice_ids"])

elif cond_type == "visited_state":
visited = state.get("visited_states", [])
return cond["state_id"] in visited

elif cond_type == "not":
return not self._evaluate_condition(cond["condition"], state)

return False

def preview_endings(self, player_state: Dict) -> List[Dict]:
"""
预览:显示当前状态下各结局的解锁进度
用于游戏中的"结局收集"UI
"""
previews = []
for ending in self._endings.values():
total = len(ending.conditions)
met = sum(1 for c in ending.conditions if self._evaluate_condition(c, player_state))
previews.append({
"ending_id": ending.ending_id,
"title": ending.title,
"tier": ending.tier.value,
"progress": f"{met}/{total}",
"unlocked": met == total,
"percentage": (met / total * 100) if total > 0 else 0
})
return sorted(previews, key=lambda x: x["percentage"], reverse=True)


# ===== 使用示例:多结局配置 =====
def setup_endings():
manager = MultipleEndingManager()

# 真结局:最严格的条件
manager.register_ending(Ending(
ending_id="true_end",
title="世界守护者",
description="你拯救了所有人,包括你的敌人。",
tier=EndingTier.TRUE_END,
conditions=[
{"type": "flag_gte", "flag": "karma", "value": 80},
{"type": "all_choices", "choice_ids": ["spare_boss", "save_village", "redeem_rival"]},
{"type": "visited_state", "state_id": "secret_garden"},
{"type": "flag_equals", "flag": "true_artifact_restored", "value": True}
],
epilogue_text="在光芒中,世界获得了新生..."
))

# 好结局
manager.register_ending(Ending(
ending_id="good_end",
title="英雄归来",
description="你成为了人们口中的英雄。",
tier=EndingTier.GOOD,
conditions=[
{"type": "flag_gte", "flag": "karma", "value": 30},
{"type": "flag_equals", "flag": "boss_defeated", "value": True}
],
epilogue_text="王座上,新的传说开始了..."
))

# 坏结局
manager.register_ending(Ending(
ending_id="bad_end",
title="孤独的王座",
description="你获得了力量,但失去了所有。",
tier=EndingTier.BAD,
conditions=[
{"type": "flag_lte", "flag": "karma", "value": -30},
{"type": "flag_equals", "flag": "boss_defeated", "value": True}
],
epilogue_text="王座冰冷,无人分享..."
))

# 秘密结局
manager.register_ending(Ending(
ending_id="secret_end",
title="幕后黑手",
description="原来你才是最终的幕后操纵者。",
tier=EndingTier.SECRET,
conditions=[
{"type": "choice_made", "choice_id": "join_dark_cult"},
{"type": "flag_equals", "flag": "betrayed_all", "value": True},
{"type": "flag_equals", "flag": "secret_ritual_done", "value": True}
],
epilogue_text="在阴影中,你露出了微笑..."
))

# 普通结局(保底)
manager.register_ending(Ending(
ending_id="normal_end",
title="旅程的终点",
description="冒险结束,生活继续。",
tier=EndingTier.NORMAL,
conditions=[
{"type": "flag_equals", "flag": "game_completed", "value": True}
]
))

return manager


# 模拟结算
if __name__ == "__main__":
manager = setup_endings()

# 玩家A:走真结局路线
player_a = {
"karma": 90,
"choices_made": ["spare_boss", "save_village", "redeem_rival", "help_stranger"],
"visited_states": ["secret_garden", "capital", "dungeon"],
"true_artifact_restored": True,
"game_completed": True
}

ending = manager.calculate_ending(player_a)
print(f"玩家A结局: {ending.title} [{ending.tier.value}]")

# 预览所有结局进度
print("\n结局收集进度:")
for preview in manager.preview_endings(player_a):
status = "已解锁" if preview["unlocked"] else f"{preview['progress']}"
print(f" [{preview['tier']}] {preview['title']}: {status}")
```

---

## 5. 叙事工具集成

### 5.1 articy:draft X API 集成

```csharp
// articy:draft X 运行时集成(C# / Unity)
// articy 导出为 JSON 或 Unity Plugin 后,通过以下接口加载

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace NarrativeSystem.Integrations.Articy
{
///
/// articy:draft X 导出数据加载器
///

public class ArticyDataLoader
{
private JObject _root;
private Dictionary _nodes;
private Dictionary _dialogues;

///
/// 从 articy 导出的 JSON 文件加载
///

public void LoadFromJson(string filepath)
{
var json = File.ReadAllText(filepath);
_root = JObject.Parse(json);
_nodes = new Dictionary();
_dialogues = new Dictionary();

ParsePackages();
}

private void ParsePackages()
{
var packages = _root["Packages"] as JArray;
foreach (var pkg in packages)
{
ParseModels(pkg["Models"] as JArray);
}
}

private void ParseModels(JArray models)
{
foreach (var model in models)
{
var type = model["Type"]?.ToString();
var id = model["Id"]?.ToString();

switch (type)
{
case "Dialogue":
_dialogues[id] = ParseDialogue(model);
break;
case "DialogueFragment":
_nodes[id] = ParseFragment(model);
break;
case "Hub":
_nodes[id] = ParseHub(model);
break;
case "Jump":
_nodes[id] = ParseJump(model);
break;
}
}
}

private ArticyDialogue ParseDialogue(JToken model)
{
return new ArticyDialogue
{
Id = model["Id"].ToString(),
TechnicalName = model["TechnicalName"].ToString(),
DisplayName = model["DisplayName"]?.ToString(),
// 解析输入/输出引脚连接
InputPins = ParsePins(model["InputPins"] as JArray),
OutputPins = ParsePins(model["OutputPins"] as JArray)
};
}

private ArticyFragment ParseFragment(JToken model)
{
return new ArticyFragment
{
Id = model["Id"].ToString(),
TechnicalName = model["TechnicalName"].ToString(),
Text = model["Text"]?.ToString(),
StageDirections = model["StageDirections"]?.ToString(),
Speaker = model["Speaker"]?.ToString(),
InputPins = ParsePins(model["InputPins"] as JArray),
OutputPins = ParsePins(model["OutputPins"] as JArray)
};
}

private List ParsePins(JArray pins)
{
if (pins == null) return new List();

return pins.Select(p => new ArticyPin
{
Id = p["Id"]?.ToString(),
Text = p["Text"]?.ToString(),
Connections = (p["Connections"] as JArray)?.Select(c => c["Target"]?.ToString()).ToList()
?? new List()
}).ToList();
}

public ArticyDialogue GetDialogue(string id) => _dialogues.GetValueOrDefault(id);
public ArticyNode GetNode(string id) => _nodes.GetValueOrDefault(id);
}

// articy 数据模型
public class ArticyNode
{
public string Id { get; set; }
public string TechnicalName { get; set; }
public List InputPins { get; set; } = new List();
public List OutputPins { get; set; } = new List();
}

public class ArticyDialogue : ArticyNode
{
public string DisplayName { get; set; }
}

public class ArticyFragment : ArticyNode
{
public string Text { get; set; }
public string StageDirections { get; set; }
public string Speaker { get; set; }
}

public class ArticyHub : ArticyNode { }
public class ArticyJump : ArticyNode
{
public string Target { get; set; }
}

public class ArticyPin
{
public string Id { get; set; }
public string Text { get; set; }
public List Connections { get; set; } = new List();
}
}
```

### 5.2 ink (Inkle) 脚本集成

```csharp
// ink 运行时集成(C#)
// 使用官方 ink-engine-runtime 包:Install-Package Ink-engine-runtime

using Ink.Runtime;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace NarrativeSystem.Integrations.Ink
{
///
/// Ink 故事包装器
///

public class InkStoryRunner
{
private Story _story;
private readonly Dictionary> _externalFunctions;

public InkStoryRunner(string jsonFilePath)
{
var json = File.ReadAllText(jsonFilePath);
_story = new Story(json);
_externalFunctions = new Dictionary>();
}

///
/// 注册外部函数供 ink 脚本调用
/// ink 中使用:~ my_function(param)
///

public void BindExternalFunction(string name, Func func)
{
_story.BindExternalFunction(name, func);
}

public void BindExternalFunction(string name, Func func)
{
_story.BindExternalFunction(name, func);
}

///
/// 推进故事:获取当前段落文本
///

public string Continue()
{
if (_story.canContinue)
{
return _story.Continue();
}
return null;
}

///
/// 当前是否有选项
///

public bool HasChoices => _story.currentChoices.Count > 0;

///
/// 获取当前可选选项
///

public List GetCurrentChoices()
{
return _story.currentChoices.Select(c => new InkChoice
{
Index = c.index,
Text = c.text,
OriginalChoice = c
}).ToList();
}

///
/// 做出选择
///

public void ChooseChoiceIndex(int index)
{
_story.ChooseChoiceIndex(index);
}

///
/// 设置 ink 变量
///

public void SetVariable(string name, object value)
{
_story.variablesState[name] = value;
}

///
/// 获取 ink 变量
///

public object GetVariable(string name)
{
return _story.variablesState[name];
}

///
/// 观察变量变更
///

public void ObserveVariable(string name, Action callback)
{
_story.ObserveVariable(name, (varName, newValue) => callback(varName, newValue));
}

///
/// 故事是否结束
///

public bool IsEnded => !_story.canContinue && !HasChoices;

///
/// 获取当前标签(用于触发游戏事件)
/// ink 中使用:# trigger_event: battle_boss
///

public List CurrentTags => _story.currentTags ?? new List();
}

public class InkChoice
{
public int Index { get; set; }
public string Text { get; set; }
public object OriginalChoice { get; set; }
}
}
```

```ink
// 示例 ink 脚本(.ink 文件)
// ink 是 Inkle Studios 开发的叙事脚本语言

// === 变量定义 ===
VAR karma = 0
VAR has_artifact = false
VAR elder_trust = 50

// === knots(章节/段落) ===
-> elder_meeting

=== elder_meeting ===
# location: village_center
# music: peaceful

长老看着你,眼神中带着审视。

* [恭敬地打招呼]
~ elder_trust += 10
长老微微点头:"年轻人,你很有礼貌。"
-> elder_quest_offer

* [直接索要奖励]
~ elder_trust -= 20
~ karma -= 5
长老皱眉:"无礼!我们还没有正式认识。"
-> elder_cold

* {has_artifact} [展示古代遗物]
长老瞪大了眼睛:"这...这是传说中的圣物!"
~ elder_trust = 100
-> elder_artifact_reaction

=== elder_quest_offer ===
"村子的东边出现了怪物,你愿意帮忙吗?"

* [接受任务]
~ karma += 5
太好了,我会给你丰厚的报酬。
-> quest_accepted

* [拒绝]
~ elder_trust -= 10
好吧...我们找别人。
-> village_leave

=== elder_cold ===
{ elder_trust < 30:
长老转身不再理你。
# trigger_event: elder_ignores_player
- else:
虽然不满,长老还是保持了礼貌。
}
-> village_leave

=== quest_accepted ===
// 这里可以使用 ink 的线程和拼接功能
你接受了任务,准备前往东边的森林。
# trigger_event: spawn_forest_monsters
-> END

=== village_leave ===
你离开了村庄。
-> END

=== elder_artifact_reaction ===
长老激动地握住你的手...
// 特殊剧情分支
-> END
```

### 5.3 Yarn Spinner 集成

```csharp
// Yarn Spinner 集成(Unity / C#)
// 使用 Yarn Spinner Unity 包

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
using Yarn;
using Yarn.Unity;

namespace NarrativeSystem.Integrations.YarnSpinner
{
///
/// Yarn Spinner 对话系统桥接
///

public class YarnDialogueBridge : MonoBehaviour
{
[SerializeField] private DialogueRunner dialogueRunner;
[SerializeField] private DialogueUI dialogueUI;

// 游戏状态引用
private IGameState _gameState;

private void Start()
{
// 注册命令(Yarn 中的 <>)
RegisterCommands();

// 注册函数(Yarn 中的 {function()})
RegisterFunctions();

// 监听变量变更
dialogueRunner.VariableStorage.OnVariableChange += OnVariableChanged;
}

private void RegisterCommands()
{
// <>
dialogueRunner.AddCommandHandler(
"give_item",
(itemId, count) => _gameState.Inventory.Add(itemId, count)
);

// <>
dialogueRunner.AddCommandHandler(
"set_relationship",
(npcId, value) => _gameState.Relationships.Set(npcId, value)
);

// <>
dialogueRunner.AddCommandHandler(
"trigger_event",
(eventId) => _gameState.Events.Trigger(eventId)
);

// <>
dialogueRunner.AddCommandHandler(
"fade_in",
async (duration) => await ScreenFader.FadeIn(duration)
);

// <>
dialogueRunner.AddCommandHandler(
"wait",
async (seconds) => await Task.Delay(TimeSpan.FromSeconds(seconds))
);
}

private void RegisterFunctions()
{
// {has_item("sword")}
dialogueRunner.AddFunction("has_item", HasItem);

// {get_relationship("elder")}
dialogueRunner.AddFunction("get_relationship", GetRelationship);

// {random_range(1, 100)}
dialogueRunner.AddFunction("random_range", RandomRange);
}

private bool HasItem(string itemId)
{
return _gameState.Inventory.Has(itemId);
}

private int GetRelationship(string npcId)
{
return _gameState.Relationships.Get(npcId);
}

private int RandomRange(int min, int max)
{
return UnityEngine.Random.Range(min, max + 1);
}

private void OnVariableChanged(string name, Yarn.IVariableStorage.Value value)
{
// 同步 Yarn 变量到游戏状态
_gameState.SetVariable($"yarn.{name}", ConvertYarnValue(value));
}

private object ConvertYarnValue(Yarn.IVariableStorage.Value value)
{
return value.type switch
{
Yarn.IVariableStorage.Type.Number => value.numberValue,
Yarn.IVariableStorage.Type.String => value.stringValue,
Yarn.IVariableStorage.Type.Bool => value.boolValue,
_ => null
};
}

///
/// 启动 Yarn 对话
///

public void StartDialogue(string nodeName)
{
if (dialogueRunner.IsDialogueRunning)
{
dialogueRunner.Stop();
}

// 同步游戏变量到 Yarn
SyncVariablesToYarn();

dialogueRunner.StartDialogue(nodeName);
}

private void SyncVariablesToYarn()
{
// 将游戏状态中的变量注入 Yarn
foreach (var kvp in _gameState.GetAllVariables())
{
var yarnName = kvp.Key.Replace("yarn.", "");
switch (kvp.Value)
{
case int i:
dialogueRunner.VariableStorage.SetValue(yarnName, (float)i);
break;
case float f:
dialogueRunner.VariableStorage.SetValue(yarnName, f);
break;
case string s:
dialogueRunner.VariableStorage.SetValue(yarnName, s);
break;
case bool b:
dialogueRunner.VariableStorage.SetValue(yarnName, b);
break;
}
}
}
}
}
```

```yarn
// 示例 Yarn 脚本(.yarn 文件)
// Yarn Spinner 使用类似 Markdown 的语法

// === 节点定义 ===
title: ElderGreeting
tags: #location:village #npc:elder
---
<>
长老:你又来了,有什么事吗?
<>
长老:欢迎来到我们的村庄,旅行者。
<>
<>

-> 我想接一些任务
<>

-> 我想了解一下这里的历史
<= 5>>
长老:既然你已经有一定实力了,我可以告诉你关于古代遗迹的事...
<>
<>
<>
长老:你还太年轻了,等变强一些再来吧。
<>
<>

-> { $has_artifact } 我找到了这个古代遗物
长老:天啊!我以为这只是传说!
<>
<>
<>

-> 再见
长老:愿风指引你的道路。
<>
===

// === 另一个节点 ===
title: QuestBoard
---
任务公告板上贴着几张泛黄的纸:

-> [清理东边的狼群](推荐等级:3)
<>
你觉得自己还不够强大...
-> 还是算了
<>
<>
你接下了委托。
<>
<>

-> [寻找丢失的项链]
一个简单的小任务。
<>

<>
===
```

### 5.4 Twine 导出与解析

```python
# Twine (Harlowe/SugarCube) 故事解析器(Python)
# Twine 导出为 HTML 后,可以提取其中的故事数据

from html.parser import HTMLParser
from dataclasses import dataclass, field
from typing import Dict, List, Optional
import re
import json

@dataclass
class TwinePassage:
"""Twine 段落节点"""
pid: str
name: str
text: str
tags: List[str] = field(default_factory=list)
links: List[str] = field(default_factory=list) # [[链接文本->目标]]
position: tuple = (0, 0) # 编辑器中的位置

class TwineHtmlParser(HTMLParser):
"""
解析 Twine 导出的 HTML 文件
Twine 使用 标签存储故事数据
"""

def __init__(self):
super().__init__()
self.story_name = ""
self.story_data = ""
self.passages: List[TwinePassage] = []
self._current_passage = None
self._in_passage = False
self._capture_data = False

def handle_starttag(self, tag, attrs):
attrs_dict = dict(attrs)

if tag == "tw-storydata":
self.story_name = attrs_dict.get("name", "")
self.story_data = attrs_dict.get("options", "")

elif tag == "tw-passagedata":
self._in_passage = True
self._current_passage = {
"pid": attrs_dict.get("pid", ""),
"name": attrs_dict.get("name", ""),
"tags": attrs_dict.get("tags", "").split() if attrs_dict.get("tags") else [],
"position": (
int(attrs_dict.get("position", "0,0").split(",")[0]),
int(attrs_dict.get("position", "0,0").split(",")[1])
) if "position" in attrs_dict else (0, 0),
"text": ""
}

def handle_endtag(self, tag):
if tag == "tw-passagedata" and self._current_passage:
text = self._current_passage["text"]
links = self._extract_links(text)

passage = TwinePassage(
pid=self._current_passage["pid"],
name=self._current_passage["name"],
text=text,
tags=self._current_passage["tags"],
links=links,
position=self._current_passage["position"]
)
self.passages.append(passage)
self._in_passage = False
self._current_passage = None

def handle_data(self, data):
if self._in_passage and self._current_passage is not None:
self._current_passage["text"] += data

def _extract_links(self, text: str) -> List[str]:
"""提取 Twine 链接语法:[[目标]] 或 [[文本->目标]] 或 [[目标|文本]]"""
pattern = r'\[\[(.*?)\]\]'
matches = re.findall(pattern, text)
targets = []
for match in matches:
# 处理 "文本->目标" 或 "目标|文本" 格式
if '->' in match:
targets.append(match.split('->')[-1].strip())
elif '|' in match:
targets.append(match.split('|')[0].strip())
else:
targets.append(match.strip())
return targets


class TwineToCustomConverter:
"""
将 Twine 故事转换为自定义 JSON 格式
便于导入到自定义游戏引擎
"""

def convert(self, parser: TwineHtmlParser) -> dict:
passages = {}
for p in parser.passages:
passages[p.name] = {
"id": p.pid,
"text": p.text,
"tags": p.tags,
"links": [
{
"target": link,
"conditions": [] # Twine 基础版无内置条件
}
for link in p.links
],
"position": p.position
}

return {
"source": "twine",
"story_name": parser.story_name,
"format": "custom-v1",
"entry_point": parser.passages[0].name if parser.passages else "",
"passages": passages
}

def convert_with_macros(self, parser: TwineHtmlParser) -> dict:
"""
增强版本:解析 SugarCube 宏命令
支持 (if:), (set:), (goto:) 等
"""
passages = {}
for p in parser.passages:
parsed = self._parse_macros(p.text)
passages[p.name] = {
"id": p.pid,
"text": parsed["display_text"],
"tags": p.tags,
"conditions": parsed["conditions"],
"variable_ops": parsed["set_ops"],
"links": parsed["links"],
"position": p.position
}

return {
"source": "twine-sugarcube",
"story_name": parser.story_name,
"format": "custom-v1",
"entry_point": parser.passages[0].name if parser.passages else "",
"passages": passages
}

def _parse_macros(self, text: str) -> dict:
"""解析 SugarCube 宏"""
result = {
"display_text": text,
"conditions": [],
"set_ops": [],
"links": []
}

# 解析 (set: $var to value)
set_pattern = r'\(set:\s*\$(\w+)\s+to\s+([^)]+)\)'
for match in re.finditer(set_pattern, text):
result["set_ops"].append({
"variable": match.group(1),
"value": self._parse_value(match.group(2).strip())
})

# 解析 (if: $var > value)[内容]
if_pattern = r'\(if:\s*([^)]+)\)\[(.*?)\]'
for match in re.finditer(if_pattern, text, re.DOTALL):
result["conditions"].append({
"expression": match.group(1).strip(),
"content": match.group(2).strip()
})

# 重新提取纯文本(去除宏)
clean_text = re.sub(r'\([^)]+\)', '', text)
clean_text = re.sub(r'\[.*?\]', '', clean_text, flags=re.DOTALL)
result["display_text"] = clean_text.strip()

# 提取链接
link_pattern = r'\[\[(.*?)\]\]'
for match in re.finditer(link_pattern, text):
link_text = match.group(1)
if '->' in link_text:
target = link_text.split('->')[-1].strip()
elif '|' in link_text:
target = link_text.split('|')[0].strip()
else:
target = link_text.strip()
result["links"].append(target)

return result

def _parse_value(self, val_str: str):
"""解析字符串值为 Python 类型"""
val_str = val_str.strip().strip('"').strip("'")
if val_str.lower() == "true":
return True
if val_str.lower() == "false":
return False
try:
if "." in val_str:
return float(val_str)
return int(val_str)
except ValueError:
return val_str


# ===== 使用示例 =====
if __name__ == "__main__":
# 模拟 Twine 导出的 HTML 内容
sample_html = """


你站在一个分岔路口。
[[向左走->Forest]]
[[向右走->Village]]


森林里很暗,你听到了奇怪的声音。
(set: $fear to $fear + 1)
[[继续深入->DeepForest]]
[[返回->Start]]


村民们热情地欢迎了你。
(if: $visited_village)[欢迎回来!]
(set: $visited_village to true)
[[离开->Start]]


"""

parser = TwineHtmlParser()
parser.feed(sample_html)

print(f"故事名: {parser.story_name}")
print(f"段落数: {len(parser.passages)}")

for p in parser.passages:
print(f"\n--- {p.name} [{p.pid}] ---")
print(f"标签: {p.tags}")
print(f"链接: {p.links}")
print(f"文本:\n{p.text[:200]}...")

# 转换为自定义格式
converter = TwineToCustomConverter()
custom_format = converter.convert_with_macros(parser)
print("\n\n转换后的 JSON:")
print(json.dumps(custom_format, ensure_ascii=False, indent=2))
```

---

## 6. 综合示例:小型叙事系统完整实现

以下是一个可运行的最小完整示例,整合了上述多个概念。

```python
# mini_narrative_system.py
# 最小可运行叙事系统:整合 Etudes + 对话树 + 分支 + 变量

from typing import Dict, List, Optional, Callable, Any
from dataclasses import dataclass, field
from enum import Enum
import json

# ===== 核心枚举 =====
class NodeState(Enum):
INACTIVE = 0
ACTIVE = 1
COMPLETED = 2

class Priority(Enum):
LOW = 1
NORMAL = 2
HIGH = 3
CRITICAL = 4

# ===== 数据模型 =====
@dataclass
class NarrativeNode:
"""叙事节点(简化版 Etude)"""
id: str
name: str
priority: Priority = Priority.NORMAL
state: NodeState = NodeState.INACTIVE
required_flags: Dict[str, Any] = field(default_factory=dict)
set_flags_on_enter: Dict[str, Any] = field(default_factory=dict)
children: List[str] = field(default_factory=list)
dialogue_entry: Optional[str] = None
conflicts: List[str] = field(default_factory=list)

@dataclass
class DialogueEntry:
"""对话条目"""
id: str
speaker: str
text: str
choices: List['DialogueChoice'] = field(default_factory=list)
auto_jump: Optional[str] = None

@dataclass
class DialogueChoice:
"""玩家选择"""
text: str
target_dialogue: Optional[str] = None
target_node: Optional[str] = None # 选择后跳转到叙事节点
set_flags: Dict[str, Any] = field(default_factory=dict)

# ===== 核心系统 =====
class MiniNarrativeSystem:
"""
最小叙事系统
整合:Etudes式状态管理 + 对话树 + 变量系统
"""

def __init__(self):
self.flags: Dict[str, Any] = {}
self.nodes: Dict[str, NarrativeNode] = {}
self.dialogues: Dict[str, DialogueEntry] = {}
self.current_node: Optional[NarrativeNode] = None
self.current_dialogue: Optional[DialogueEntry] = None
self.choice_history: List[str] = []
self.event_log: List[str] = []

def register_node(self, node: NarrativeNode):
self.nodes[node.id] = node

def register_dialogue(self, dialogue: DialogueEntry):
self.dialogues[dialogue.id] = dialogue

def set_flag(self, key: str, value: Any):
old = self.flags.get(key)
self.flags[key] = value
self.event_log.append(f"[Flag] {key}: {old} -> {value}")

def get_flag(self, key: str, default=None):
return self.flags.get(key, default)

def evaluate_nodes(self):
"""评估所有节点,激活满足条件的"""
candidates = []
for node in self.nodes.values():
if node.state == NodeState.COMPLETED:
continue
if self._check_conditions(node.required_flags):
candidates.append(node)

# 按优先级排序
candidates.sort(key=lambda n: n.priority.value, reverse=True)

# 激活最高优先级且不冲突的
activated = []
for node in candidates:
if any(conflict in activated for conflict in node.conflicts):
continue
activated.append(node.id)
if node.state == NodeState.INACTIVE:
self._activate_node(node)

def _check_conditions(self, conditions: Dict[str, Any]) -> bool:
for key, expected in conditions.items():
if self.flags.get(key) != expected:
return False
return True

def _activate_node(self, node: NarrativeNode):
node.state = NodeState.ACTIVE
self.event_log.append(f"[Node] 激活: {node.name} ({node.id})")

# 设置进入标志
for k, v in node.set_flags_on_enter.items():
self.set_flag(k, v)

# 如果有关联对话,自动进入
if node.dialogue_entry and node.dialogue_entry in self.dialogues:
self.current_node = node
self.start_dialogue(node.dialogue_entry)

def start_dialogue(self, dialogue_id: str):
if dialogue_id not in self.dialogues:
return
self.current_dialogue = self.dialogues[dialogue_id]
self._render_dialogue()

def _render_dialogue(self):
if not self.current_dialogue:
return
d = self.current_dialogue
print(f"\n{'='*40}")
print(f"[{d.speaker}]")
print(f"{d.text}")
print(f"{'='*40}")

if d.auto_jump:
print(f"(自动跳转至: {d.auto_jump})")
self._jump_to(d.auto_jump)
return

if d.choices:
for i, choice in enumerate(d.choices, 1):
print(f" {i}. {choice.text}")

def select_choice(self, index: int):
"""玩家选择选项(0-based index)"""
if not self.current_dialogue:
return

choices = self.current_dialogue.choices
if index < 0 or index >= len(choices):
return

choice = choices[index]
self.choice_history.append(choice.text)
self.event_log.append(f"[Choice] 选择: {choice.text}")

# 应用选择效果
for k, v in choice.set_flags.items():
self.set_flag(k, v)

# 跳转
if choice.target_dialogue:
self._jump_to(choice.target_dialogue)
elif choice.target_node:
self.current_dialogue = None
self._activate_node(self.nodes[choice.target_node])
else:
self.current_dialogue = None
# 对话结束,重新评估节点
self.evaluate_nodes()

def _jump_to(self, target: str):
if target in self.dialogues:
self.start_dialogue(target)
elif target in self.nodes:
self.current_dialogue = None
self._activate_node(self.nodes[target])

def complete_current_node(self):
"""完成当前节点"""
if self.current_node:
self.current_node.state = NodeState.COMPLETED
self.event_log.append(f"[Node] 完成: {self.current_node.name}")
self.current_node = None
self.evaluate_nodes()

def save(self) -> str:
"""导出为 JSON 存档"""
data = {
"flags": self.flags,
"node_states": {nid: n.state.value for nid, n in self.nodes.items()},
"choice_history": self.choice_history,
"event_log": self.event_log
}
return json.dumps(data, ensure_ascii=False, indent=2)


# ===== 使用示例:完整流程 =====
def demo_full_system():
sys = MiniNarrativeSystem()

# 注册叙事节点
sys.register_node(NarrativeNode(
id="intro",
name="开场",
priority=Priority.CRITICAL,
dialogue_entry="dlg_intro"
))

sys.register_node(NarrativeNode(
id="village_quest",
name="村庄任务",
priority=Priority.NORMAL,
required_flags={"intro_completed": True},
dialogue_entry="dlg_village"
))

sys.register_node(NarrativeNode(
id="forest_quest",
name="森林任务",
priority=Priority.NORMAL,
required_flags={"intro_completed": True},
dialogue_entry="dlg_forest",
conflicts=["village_quest"] # 与村庄任务互斥
))

# 注册对话
sys.register_dialogue(DialogueEntry(
id="dlg_intro",
speaker="旁白",
text="你醒来在一个陌生的房间里。门开着,外面似乎有声音。",
choices=[
DialogueChoice(
text="走出去看看",
target_dialogue="dlg_outside",
set_flags={"intro_completed": True}
),
DialogueChoice(
text="先在房间里搜索",
target_dialogue="dlg_search_room"
)
]
))

sys.register_dialogue(DialogueEntry(
id="dlg_search_room",
speaker="旁白",
text="你发现了一把生锈的钥匙,但没有其他有价值的东西。",
choices=[
DialogueChoice(
text="走出去",
target_dialogue="dlg_outside",
set_flags={"intro_completed": True, "has_key": True}
)
]
))

sys.register_dialogue(DialogueEntry(
id="dlg_outside",
speaker="旁白",
text="你来到外面。左边是村庄,右边是森林。",
choices=[
DialogueChoice(
text="去村庄",
target_node="village_quest"
),
DialogueChoice(
text="去森林",
target_node="forest_quest"
)
]
))

sys.register_dialogue(DialogueEntry(
id="dlg_village",
speaker="村长",
text="欢迎,旅行者!我们正被狼群困扰,你能帮忙吗?",
choices=[
DialogueChoice(
text="接受任务",
set_flags={"quest_wolves": "accepted"}
),
DialogueChoice(
text="拒绝",
set_flags={"quest_wolves": "refused"}
)
]
))

sys.register_dialogue(DialogueEntry(
id="dlg_forest",
speaker="神秘声音",
text="森林深处传来低语...你确定要继续吗?",
choices=[
DialogueChoice(
text="深入探索",
set_flags={"forest_explored": True}
),
DialogueChoice(
text="退回去",
target_dialogue="dlg_outside"
)
]
))

# 启动系统
print("=" * 50)
print("启动叙事系统演示")
print("=" * 50)
sys.evaluate_nodes()

# 模拟玩家交互
# 在实际游戏中,这些来自UI输入
sys.select_choice(1) # 先搜索房间
sys.select_choice(0) # 走出去
sys.select_choice(0) # 去村庄
sys.select_choice(0) # 接受任务

print("\n" + "=" * 50)
print("事件日志:")
print("=" * 50)
for log in sys.event_log:
print(log)

print("\n" + "=" * 50)
print("存档数据:")
print("=" * 50)
print(sys.save())


if __name__ == "__main__":
demo_full_system()
```

---

## 总结

本文档覆盖了游戏叙事架构的五个核心领域:

| 模块 | 关键技术 | 适用规模 |
|------|----------|----------|
| Etudes 系统 | 层级化状态机、优先级排序、冲突检测 | 大型RPG |
| 对话系统 | 节点树、条件判断、变量绑定 | 通用 |
| 变量管理 | 全局Flag、命名空间、持久化 | 通用 |
| 分支剧情 | 状态机、延迟反馈、多结局 | 叙事驱动游戏 |
| 工具集成 | articy、ink、Yarn、Twine | 通用 |

**选型建议:**

- **小型项目/原型**:直接用 JSON + 简单状态机,或集成 Twine
- **中型项目**:使用 ink 或 Yarn Spinner,自研轻量状态机
- **大型项目**:参考 Etudes 架构,自研层级化状态管理,或集成 articy:draft X

---

## 参考来源

- Owlcat Games: GDC 2026 — Managing 30,000+ Narrative Variables
- Inkle Studios: ink 脚本语言官方文档 (https://github.com/inkle/ink)
- Yarn Spinner: 官方文档 (https://docs.yarnspinner.dev/)
- articy:draft X: 官方开发者文档
- Twine: Harlowe / SugarCube 格式规范