节点与跳转

用节点把故事切成块,用跳转把它们连起来;子节点、带参节点,以及在运行时才决定去哪。

节点是故事的最小跳转单位,跳转把它们连起来。这两样东西加上正文,就够写一个完整的分支故事了。

=== 码头 ===
末班蒸汽船刚走。
-> 酒馆

=== 酒馆 ===
你推开那扇被海风泡胀的门。
-> 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 是保留名,不能拿它当变量名。

节点引用经存档读档保真;读档时如果那个节点在新版故事里已经没了,会明确报错,而不是静默坏掉。

接下来

节点与跳转搭出骨架,读者的选择才让它分岔——下一篇讲选项与分支