变量、条件与逻辑
让故事记住读者做过什么。声明与作用域、赋值、插值、条件块、多行 JS 与函数——逻辑就是 JavaScript。
到目前为止的故事都是「读者选,故事跳」。要让故事记住读者做过什么、据此改变后面的内容,就需要变量。
Kiny 的逻辑就是 JavaScript。 没有另一套表达式语言要学——+、?:、&&、Math.floor、数组、对象、模板字符串,会什么用什么。
三种嵌入方式
| 写法 | 做什么 | 有没有输出 |
|---|---|---|
~ 一行 JS 语句 |
执行一条语句(声明、赋值、调用) | 无 |
~~~ … ~~~ |
多行 JS 块 | 无 |
{ JS 表达式 } |
求值并把结果插进正文 | 有 |
记住一条就够:{ } 里永远是「一段表达式」,~ 开头的行永远是「一条语句」。
声明变量
用 JS 的 let 和 const:
~ let gold = 10
~ let has_lantern = false
~ const MAX_HP = 100
~ let player = { name: "无名氏", hp: 100 }
-> 开场
=== 开场 ===
{player.name} 摸了摸口袋,还有 {gold} 枚硬币。
-> END
let可变,const不可变。- 变量名必须是 ASCII(字母、数字、下划线,首字符不能是数字)。节点名和正文可以中文,变量名不行——一眼分清叙事与逻辑。
- 拼错变量名会立刻报错,不会悄悄创造一个新变量。
- 内置函数名(
random、seq、cycle、once、shuffle、turns、turns_since、seed_random)是保留字,不能拿来当变量名。
作用域:全局还是节点局部
写在开场里的是全局的——入口文件第一个节点之前那段(见项目结构):
~ let gold = 10 ← 全局,整个故事都看得见
~ const MAX_HP = 100
-> 第一章
写在节点里的是局部的,跳出这个节点就没了:
-> 战斗
=== 战斗 ===
~ let dice = random(1, 6)
你掷出了 {dice} 点。
{dice > 3 ? "你打中了!" : "你落空了。"}
-> END
节点(含它的所有子节点)等价于一个 JS 函数作用域:进去时建立、出来时销毁。
跨文件是同一个全局作用域。 所有
.kin文件里的~与~~~共享同一份全局变量;同名变量在两个文件里都声明会报错。所以起名时把整个项目当一个文件想。
赋值与运算
声明之后就是普通 JS:
~ gold = 5
~ gold += 1
~ gold--
~ player.hp -= 10
~ player.name = "Alice"
~ inventory.push("药水")
赋值、算术、比较、逻辑运算符全都支持。
插值:把值印进正文
正文里的 { 表达式 } 求值后插入:
~ let gold = 12
~ let hp = 40
-> 状态
=== 状态 ===
你还剩 {gold} 枚金币。
你的攻击力是 {gold * 2}。
你的状态:{hp > 50 ? "良好" : "虚弱"}。
-> END
几条要点:
- 里面是表达式,不是语句。想执行语句用
~行。 - 结果转成字符串插入;
undefined/null输出空字符串。 - 求值为空的独立插值不产生空行。整行只有一个插值、结果是空串时,这一行不成行——
{cond ? "文字" : ""}的假分支、用尽的once(…)都不会留下一段空白。 - 引用没声明过的变量 → 编译期报错。
条件块 @if
行内的单点判断用三元就够了({hp > 0 ? "活着" : "倒下了"})。要包住整段叙事才用 @if:
~ let met = 0
-> 客栈
=== 客栈 ===
@if {met === 0}
> 这是你第一次见他。
> 他从账本后面抬起眼。
@elif {met < 3}
> 你们算是脸熟了。
@else
> 老朋友似的,他朝你点头。
「想要点什么?」
-> END
@if/@elif/@else顶格写,条件在{ }里。- 分支体用行首
>,与选项体共用同一套层级(见选项与分支)。 - 没有结束符:某一行回到选择器所在层级、且不是
@elif/@else,整条链就结束、所有分支汇合。 @elif想写几个写几个,@else至多一个且必须在最后。
分支体里要执行语句,就 > 加 ~:
~ let gold = 8
-> 酒馆
=== 酒馆 ===
@if {gold >= 5}
> ~ gold -= 5
> 你接过酒杯,喝了一口。还剩 {gold} 枚。
@else
> 钱不够,你摇了摇头。
-> END
@if 和选项可以任意互嵌,> 的个数就是总深度。同样建议不超过三层。
多行 JS 块
复杂逻辑(多语句、循环、复杂初始化)用 ~~~ … ~~~:
~ let bag = { 绳子: 2, 火折子: 0, 干粮: 3 }
~ let list = ""
~~~
for (const [item, n] of Object.entries(bag)) {
if (n > 0) list += `- ${item}:${n}\n`
}
~~~
你翻了翻背包:
{list}
-> END
- 起止的
~~~各占一行,必须正好三个波浪号。 - 块里是任意 JS,不能嵌 Kiny 语法(
->、* [选项]、{ }插值都不行)。 - 副作用正常生效,但块本身不产出正文。
- 只能写在节点 / 子节点正文的顶层(开场也算顶层)。不能写进选项体或
@if分支体里——那里要多行逻辑,就在顶层用~~~定义一个函数,分支里~ f()调用。
函数
Kiny 没有自己的函数语法,函数就是 JS 函数:
~ let hp = 80
~~~
function describe_health(x) {
if (x === 100) return "健康"
if (x > 75) return "不错"
return "虚弱"
}
~~~
他看起来{describe_health(hp)}。
-> END
- 取文本用
{ describe_health(hp) },纯副作用用~ add_gold(10)。 - 默认参数、递归、闭包这些 JS 特性都能用。
- 函数与变量共享同一个全局作用域,名字全局唯一,且同样必须 ASCII。
要「可复用、带参、含分支的叙事段」不是用函数,是用带参节点。 函数返回值,节点承载剧情。
内置函数
纯计算直接用 JS(Math.floor、parseFloat、**…)。内置函数只补 JS 拿不到的引擎能力:
| 函数 | 做什么 |
|---|---|
random(min, max) |
[min, max] 闭区间随机整数,可复现 |
seed_random(n) |
设随机种子 |
turns() |
当前总回合数 |
turns_since("节点名") |
距上次访问该节点多少回合(没访问过返回 -1) |
seq(...) / cycle(...) / once(...) / shuffle(...) |
文本变体,见文本变体 |
~ seed_random(42)
~ let dice = random(1, 6)
你掷出了 {dice} 点。
-> END
几条边界
~ -> 节点不行。跳转就写-> 节点,独立成行。~行末尾可以写 JS 的//注释。~~~块不能嵌套。
接下来
变量让故事记住状态;下一篇讲一种不用变量也能让文字「活」起来的写法——文本变体。