节点与跳转
用节点把故事切成块,用跳转把它们连起来;子节点、带参节点,以及在运行时才决定去哪。
节点是故事的最小跳转单位,跳转把它们连起来。这两样东西加上正文,就够写一个完整的分支故事了。
=== 码头 ===
末班蒸汽船刚走。
-> 酒馆
=== 酒馆 ===
你推开那扇被海风泡胀的门。
-> END
节点
=== 雾港开场 ===
雾从港口涌上来,遮住了路灯。
你站在码头边。
=== 客栈 ===
你推开了客栈的门,暖气扑面而来。
- 声明写成
=== 名字 ===,左右各三个等号,两个或四个都不认。 - 名字可以用中文,但不能有空格。多词的话用下划线或直接连写:
雾港开场、first_class、头等舱。 - 名字在整个项目里全局唯一,跨文件也算。
- 节点正文从下一行开始,到下一个
===或=或文件末尾为止。
节点之间不会自动往下流。 执行到一个节点的底部如果没有跳转,编辑器会给你一条警告——那多半是漏写了 ->。
跳转
-> 目标 立刻跳到另一个节点,本节点剩下的内容不再执行。
两种写法,控制流上完全等价:
你走出了房间。
-> 走廊
你走出了房间。-> 走廊
独立成行更清楚;跟在文本后面更紧凑。(两种写法在换行上的差别见选项与分支里的粘连。)
两个特殊目标
-> END—— 故事正常结束。不需要你自己建一个叫END的节点。-> DONE—— 目前等同于END,留给以后扩展。
子节点
一个节点内容多了,用单个 = 把它切成子节点:
=== 火车上 ===
雾从车窗外掠过。
-> 头等舱
= 头等舱
丝绒座椅,一杯没动过的红茶。
-> END
= 三等舱
过道上坐满了人。
-> END
- 声明是
= 名字,单个等号、右边不加等号——和节点的===一眼分得开。 - 子节点名只在所属节点内唯一,不同节点下可以重名。
- 同一节点内直接
-> 子节点名;跨节点要写全路径-> 父节点名.子节点名。
最容易踩的一脚:进入父节点不会自动进入子节点。
=== 父节点 ===
AAA
= 子节点1
BBB
跑起来只输出 AAA,然后触底警告——父节点的正文是「从 === 到第一个 = 之间」那一段,走到第一个 = 父节点就结束了。要进子节点必须显式跳:
=== 父节点 ===
AAA
-> 子节点1
= 子节点1
BBB
-> END
带参节点
节点名后面可以带参数。跳进去时传实参,参数就是这个节点的局部变量:
-> 商店("灯笼", true)
=== 商店(item, sale) ===
老板把{item}推到你面前。
{sale ? "「今天便宜卖。」" : "「一分钱一分货。」"}
-> END
- 声明
=== 名字(参数1, 参数2) ===。节点名可以中文,但参数名必须是 ASCII(字母、数字、下划线)——参数是变量,跟着变量的命名规则走。写成=== 商店(货品, 打折) ===会报错。 - 实参是 JS 表达式,跳转时求值。个数必须对得上,否则报错。
- 参数在进入时绑定、离开时销毁,它的子节点里也看得见。
- 带参节点只能经
-> 名字(实参)进入,不能从外面直接跳进它的子节点(参数没处绑定)。节点内部子节点之间照常跳,参数仍然可见。
带参节点就是普通节点,里面写正文、选项、跳转、条件块都一样。
在运行时才决定去哪
跳转目标可以是一个 JS 表达式:
-> {next_stop}
{} 里的表达式求值后按类型分派,两种目标:
字符串——按节点名查表跳。只认节点名("大厅")、全路径("商店.内室")和 "END" / "DONE"。查不到当场报错;目标是带参节点时也报错——字符串里没处写实参,那种情况得走下面的 $nodes。
~ let next_room = "北屋"
-> {next_room}
=== 北屋 ===
北屋里空无一人。
-> END
=== 南屋 ===
南屋堆满了箱子。
-> END
节点引用——从内置的 $nodes 里取出来的值。这是更安全的一档:
~ let back = $nodes.码头
你决定原路返回。
-> {back}
=== 码头 ===
潮水已经退了。
-> END
$nodes:把节点当值来用
$nodes 是引擎内置的只读节点表,两级结构对应节点和子节点:
~ let x = $nodes.商店 // 节点引用
~ let y = $nodes.商店.内室 // 子节点引用
~ let z = $nodes["遭遇" + i] // 名字算出来的,用下标
~ let w = $nodes["商店.内室"] // 带点的字符串下标 = 两级访问
~ let map = { 北: $nodes.走廊, 南: $nodes.大厅 }
-> {map[dir]}
它最有用的性质是访问即校验:
- 访问一个不存在的节点名当场报错——错字在你写数据的地方就暴露,而不是潜伏到某次跳转时才炸。
- 名字是字面量时(
$nodes.商店)编译期就查;名字是算出来的($nodes[变量])留到运行时。 - 想知道某个节点在不在,用
in:{name in $nodes ? "有这地方" : "没有"}。 Object.keys($nodes)能列出全部顶层节点名(子节点走$nodes.父.子),适合做配置表驱动的地图——列出来的不含END/DONE,那两个是伪引用、不是节点。- 还有
$nodes.END/$nodes.DONE两个伪引用,给「返回点可能是结束」的场景用。
带参节点的引用是可调用的:
~ let quest = $nodes.商店("灯笼", 0.8) // 创建时就绑好实参、当场校验个数
-> {quest}
没绑实参的带参节点引用可以创建、可以传来传去,但直接跳它是运行时错误。
$nodes是保留名,不能拿它当变量名。节点引用经存档读档保真;读档时如果那个节点在新版故事里已经没了,会明确报错,而不是静默坏掉。
接下来
节点与跳转搭出骨架,读者的选择才让它分岔——下一篇讲选项与分支。