变量、条件与逻辑

让故事记住读者做过什么。声明与作用域、赋值、插值、条件块、多行 JS 与函数——逻辑就是 JavaScript。

到目前为止的故事都是「读者选,故事跳」。要让故事记住读者做过什么、据此改变后面的内容,就需要变量。

Kiny 的逻辑就是 JavaScript。 没有另一套表达式语言要学——+?:&&Math.floor、数组、对象、模板字符串,会什么用什么。

三种嵌入方式

写法 做什么 有没有输出
~ 一行 JS 语句 执行一条语句(声明、赋值、调用)
~~~ … ~~~ 多行 JS 块
{ JS 表达式 } 求值并把结果插进正文

记住一条就够:{ } 里永远是「一段表达式」,~ 开头的行永远是「一条语句」。

声明变量

用 JS 的 letconst

~ let gold = 10
~ let has_lantern = false
~ const MAX_HP = 100
~ let player = { name: "无名氏", hp: 100 }
-> 开场

=== 开场 ===
{player.name} 摸了摸口袋,还有 {gold} 枚硬币。
-> END
  • let 可变,const 不可变。
  • 变量名必须是 ASCII(字母、数字、下划线,首字符不能是数字)。节点名和正文可以中文,变量名不行——一眼分清叙事与逻辑。
  • 拼错变量名会立刻报错,不会悄悄创造一个新变量。
  • 内置函数名(randomseqcycleonceshuffleturnsturns_sinceseed_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.floorparseFloat**…)。内置函数只补 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 的 // 注释。
  • ~~~ 块不能嵌套。

接下来

变量让故事记住状态;下一篇讲一种不用变量也能让文字「活」起来的写法——文本变体