DOCUMENTATION · 文本剧本语法

文本剧本语法

编辑器中间栏的「文本」模式用的就是这套语法:文本是真源,模型是它的产物。 (另外两种模式:表单是点选式编辑;JSON 直接显示引擎真正加载的那份剧情,可改 —— 见 ARCHITE…

同步自 docs/SCRIPT_TEXT.md(168 行) · commit b2b4124 · 读源文件 ↗

硬契约:parse(stringify(doc)) 必须深等于 doc(tests/editor-text.test.ts 钉住)。 所以本文档里的每种写法都有对应的回写规则 —— 你写的东西不会被静默改写。


0. 一张全貌

// 整行注释(也支持 > 开头)
# 剧名                          ← 第一个一级标题是剧名
作者:某某                       ← 可选

# 第一幕                        ← 之后的一级标题都是「幕」,一直延续到后面的场

## 门厅(内景 · 锈蚀的门厅 · 夜)   ← 场:## 场名(可选:打印用的场标题行)
铁门被推开,灰尘落下来。            ← 不带「:」的普通行 = 动作 / 舞台指示
铁颚(低声):进去,或者滚。        ← 「名字(表演提示):台词」——提示可省
  我说完了。                      ← 缩进的行 = 上一句对白的**续段**(同一人连续多段)
旁白:风从缝里灌进来。              ← 说话人写「旁白」就是旁白
设置:item.gold = item.gold + 10   ← 写入状态(右边是**表达式**)

选择:怎么办?                     ← 选择块
- 拿钥匙 -> 里间
- 空手进去(如果 item.gold >= 10)-> 里间
- 就此离开                        ← 省略 `->` 就是"选它剧本结束"

如果 item.gold >= 10 -> 富人区      ← 条件分支
否则 -> 贫民窟

跳到 门厅                          ← 跳转(目标写场名)
结局:好结局                       ← 结局块;其后的普通行是结局正文
你活着出来了。

1. 标题与幕

写法含义
# 剧名第一个一级标题 = 剧名
# 第一幕之后的一级标题 = 幕;幕会一直延续到后面的场,直到出现下一个幕
## 场名场
## 场名(内景 · 地点 · 夜)场 + 场标题行(括号里是给打印稿用的行头)

幕只影响结构与排版(大纲分组、打印稿的幕标题),不影响引擎。 体检会提醒"部分场有幕、部分没有"这种不一致。

2. 行与节拍

写法编译成引擎的说明
铁门被推开。say(无说话人)不带「:」的普通行 = 动作 / 舞台指示
铁颚:台词。say对白
铁颚(低声):台词。say(文本 (低声)台词。)表演提示;编译时并进台词,因为游戏里也要交代
旁白:……say(无说话人)就是说话人叫"旁白"
 缩进的行并入上一句对白续段:同一说话人连续多段(段落之间以换行分隔)
选择:提示 + - 选项 -> 场名choice见 §3
如果 表达式 -> 场名 + 否则 -> 场名observe/goto 分支见 §4
设置:item.gold = item.gold + 10set写入状态;右边就是表达式
跳到 场名goto目标写场名(不是 id)
结局:名字 + 正文行end结局正文可多行

续段的规则:行首有空白(半角/全角空格、制表符)就算续段。 如果它前面不是对白(例如前面是动作或什么都没有),这一行按动作处理 —— 不丢内容。

3. 选择

选择:怎么办?                 ← 提示可省:直接写「选择:」也行
- 拿钥匙 -> 里间
- 空手进去(如果 item.gold >= 10)-> 里间
- 就此离开                    ← 没有 `->` = 选它剧本结束

4. 条件分支

如果 item.gold >= 10 -> 富人区
如果 flag.kind -> 被救的人
否则 -> 贫民窟                 ← 兜底;没有 `->` 表示这一支结束

「如果」后面到箭头之前的整段就是条件表达式。 表达式合法与否由引擎求值器判定,编辑器会把不合法的地方指到具体行号。

5. 表达式(可用状态项)

状态分四个命名空间:var(数值)、flag(布尔)、item(物品计数)、actor(按角色的对象)。

item.gold >= 10
flag.brave == true
num(var.gold) + 10 > var.debt
actor.hero.hp <= 0

支持的运算符:== != >= <= > < + - * / && || !, 以及 num() / str() / bool() 等转换函数(详见剧情格式里的表达式一节)。

文本模式不做"人话翻译":条件就是表达式。编辑器负责的是把你写错的地方指出来 (行号 + 原因),而不是替你绕开它。想点选式构造条件,用「表单」模式。

6. 状态项:编辑器给你的是"信息",不是"翻译"

编辑器会自动收集本剧本里出现过的状态项(var. / flag. / item. / actor.), 在右侧「状态项」面板里列出谁写的、谁读的:

item.gold   写×1 · 读×1
var.debt    只读没写        ← 这种值得你确认一下:是不是忘了写?
flag.brave  读×2

它的用途是省去"去别处翻变量名",以及暴露"读了但从没写过"这种可疑情况。 它不会把表达式翻译成人话 —— 条件与写入就是表达式,编辑器负责把写错的地方指出来 (行号 + 原因),而不是替你绕开它。

7. 注释与空行

8. 出错时会怎样

解析不抛异常,返回带行号的诊断:

诊断例子
跳向不存在的场跳到 门厅 但没写过 ## 门厅(会指出这一行的行号)
场名重复两个 ## 门厅(会同时指出第一次在哪一行)
条件不合法如果 item.gold >= -> 场
选项没有选择块- xxx 前面没有 选择:
## 之前就写内容自动放进「未命名场」并提醒

只要还有 error,编辑器就不会用解析结果替换你的内容 —— 写坏了不会毁稿。 离开文本模式时如果内容有变,会压一次撤销栈(表单模式里 Ctrl+Z 能整段退回来)。

9. 编辑器表达不了的节点会怎样

导入别人的剧情时,遇到编辑器表达不了的东西(插件的 plugin、监听事件的 observe、 等待的 wait、自定义 ui 等),它们不会丢:编辑器把这一整段原样带走, 并在节拍列表里标成 ⟪原样保留:1 个节点(wait)⟫。你改你的台词,导出时它们接回原位, 引用也跟着重新指向(即使编辑器给你认识的节点重新编过号)。

一句话:你看不到它们的内容、也改不了,但不会把它们弄丢。

另外,"类型认识"的节拍上多带的字段(say 的 effects、choice 的 timeoutMs / timeoutNext…) 也一样保留:表单里只读标出,导出时原样写回、引用跟着重新指向。 但文本是真源 —— 你在文本里改动了某一场的节拍数量/顺序(结构变了),那一场的附加字段 就接不回去了(编辑器会明确告诉你,不会悄悄弄错)。要精确保住它们,用 JSON 模式编辑。

10. 有意不做的(以及为什么)