Inkle Inky ink 上手指南

术语表#

  • Inkle 制作和发行这个系列项目的公司
  • ink 他们推出的一种用于撰写互动小说(interactive stories)的编程语言
  • Inky 专门用来编写 ink 的编辑器工具软件 / 集成开发环境(IDE)
  • inklewriter 特指 inklewriter.com 这个在线编辑和预览 ink 项目的第三方工具网站

Tips#

  1. 「缩进」在代码层面没有任何意义,只是为了改善开发时的可读性。
  2. 「空行」当然也是(仅作代码的排版)。
  3. 但「换行」不是,换行会体现到实际游玩中的排版上。
  4. 针脚(stitches,子结点),如果结点直接接子结点默认运行第一个,否则不运行。
  5. 跳转目标可以作为参数传递。

ink 语法#

首先请打开示例项目,结合实际效果更好理解。

注释#

// 单行注释
/**
 * 多行注释
 * 想写几行写几行
 */
TODO: 高亮注释 提醒自己需要完成的待办事项

结点(Knots)===#

就是结点knot,不是节点node

类似一种标签(label),使用唯一的标识符(结点名称)来标记一段代码。

注意本来只要是 2 个及以上= 等号就行。
但为了和子结点(单个等号,=明显地区分开来,我们惯例使用 3 个等号 ===

=== a_knot ===

随便写些什么。

遇到另一个结点的标记时会自动视为结束。

=== another_knot ===

再随便写些什么。

需要注意的是和 Markdown 的标题 # 类似,解析时只要左边存在就作数。
因此右边是可以省略的,比如:

=== a_knot_without_closed

随便写些什么。

这种格式也是合法的,和上面的效果一样。

针脚(Stitches,即 子结点=

隶属于父级结点的子节点。

=== tavern ===

你来到了酒馆,要跟谁说话呢?

    + [向酒保搭话]你向酒保搭话。
        -> bartender

    + [向侍者搭话]你向侍者搭话。
        -> waiter

    + [向吟游诗人搭话]你向吟游诗人搭话。
        -> bard

    + [向沉默的酒客搭话]你向沉默的酒客搭话。
        -> silent_drinker

    = bartender
        “没有这种酒。换一个。”
        -> tavern

    = waiter
        “很抱歉,敝店没有提供这样的咨询服务呢。您需要点别的什么吗?”
        -> tavern

    = bard
        吟游诗人只是对你微笑,依旧弹唱着晦涩难懂的小调。
        -> tavern

    = silent_drinker
        他自顾自地喝着酒,好像完全没注意到你的招呼。
        -> tavern

父级结点可以非常轻松地访问子结点(就像访问同级结点一样)。

由于每个结点的标识符(名称)都具有唯一性(在同级下),所以每个结点也会拥有唯一的标识符(名称),即支持在任何地方调用它。

=== fire_magic ===

    = fire_ball
        你施放了火球术。


=== fast_cast ===

你想施放什么法术?

    + [火球术]
        // 使用子结点的唯一标识符
        -> fire_magic.fire_ball

    + [还是火球术]
        // 直接调用父结点
        // 由于父结点除了子结点以外没有其他内容 因此默认为第一个子结点
        -> fire_magic  // 默认为 .fire_ball

参数

结点和针脚可以拥有参数,也就是说某种程度上你可以把它们视为「函数」。

* [控诉黑斯廷]
    -> accuse("黑斯廷斯")
* [控诉布莱克女士]
    -> accuse("克劳迪娅")
* [控诉我自己]
    -> accuse("我自己")

=== accuse(who) ===

    “我控诉{who}!”波洛宣布。
    “真的吗?”杰普回道,“{who == "myself":你要这么做?|{who}?}”
    “为什么不可以?”波洛回击道。

跳转(Diverts)->#

用短横线(或者说 减号- 与右尖括号(或者说 大于符号> 组成的箭头符号 ->
可以跳转到指定的结点,有且仅有这个作用。

有一个需要注意的小细节是和换行的配合。

左边部分和 -> next

=== next ===

右边部分会连接起来显示在同一行。(中间有一个固定的空格)
第一行——
-> next

=== next ===

和第二行之间就会有换行了。

跳转到特殊结点 END DONE

有两个特殊的预设结点:ENDDONE,二者都同样代表「游戏结束」。

选择你的结局:

    + [Happy End] -> happd_end
    + [Bad End] -> bad_end
    + [Good End]就这样吧。 -> END
    + [True End]真相大白。 -> DONE

=== happd_end ===

可喜可贺可喜可贺。-> END

=== bad_end ===

人生长恨水长东。-> END

选项(Choices)/ 按钮 + *#

  • 使用 + 开头的为常驻选项
    每次来到这个节点一定会显示
  • 使用 * 开头的为一次性选项
    只要你点击过一次,下次再回到这个节点就不会显示了
=== choices ===

下面演示两种不同的选项用法。

    + [\+ 这个选项怎么点都不会消失]
        # CLEAR
        -> choices

    * [\* 这个选项点一次就会消失]
        # CLEAR
        注意:使用 \* 的选项消失了。
        -> choices

可变选项(Varying Choices)[ ]

官方起的这个名字有点抽象,我更愿意简单明了地称之为「阅后即焚」。

=== burn_after_reading ===

# CLEAR

下面演示 \[阅后即焚文本\] 的用法。

    // 选项全部删除
    + [“这个畜生。”你心里想。但现在还不是时候。]“没问题,先生。很高兴为您服务。”
        你展露出无可挑剔的微笑。
        -> burn_after_reading

    // 只删除选项最后的标点以延续句子
    + “我就是饿死、死外边、从这里跳下去,不会吃你们一点东西[!”]……烤全羊啊嗯。”
        “真香。”你擦了擦嘴,“想问什么?赶紧的吧。”
        -> burn_after_reading

    // 不删除任何东西 保留整个选项的原文
    + “爷今天必须给你整个活。”[]你深吸一口气,“草,走,忽略!”
        “漂亮!”路过的行人见状驻足鼓起掌来,有些人甚至吹起了口哨。
        -> burn_after_reading

后备选项(Fallback choices)+ -> / * ->

我们非常自然的能够联想到这样一种场景:
「如果某个结点内所有选项都是一次性的,重复多次之后用光了全部选项会怎么样?」

会卡住;如果是在编辑器内预览,会报错 ran out of content
为了避免这种情况发生,我们需要一个可以在「用光选项」之后应急的处理方式。
即「后备选项」:

“你竟然逃出来了?!你到底是怎么做到的?”

你不能暴露这个秘密。要如何回应?

    * [假装懊悔]“我用掉了最后一张闪光卷轴。”你捂着脑袋沉闷开口。
        -> next

    * [假装哽咽]“哈兰……哈兰他……”你说不出话了。
        -> next

    + ->
        “我也不太清楚。”[]你挠了挠头,试图蒙混过关,“就这样那样。”
        -> next

在上面这个例子中,你每次死里逃生之后,为了守护秘密,都必须用掉一个理由。
根据不同的理由,对方的信任度也会发生变化。
但当你用光所有理由后,游戏仍要继续下去。
因此必须准备「后备选项」,哪怕你已经没有理由可用了。

需要注意一点:后备选项仍然遵从 +(可重复使用)和 *(一次性)的规则。
而我要强烈建议的是,如无必要,永远不要使用 * -> 作为后备选项,一律用 + ->
试问如果连你的后备选项也是一次性的,那么用掉了后备选项之后,你再用什么呢?

除非你的后备选项能够保证你再也不会触发这个场景。
比如指向 END/DONE# RESTART、或者特意编排故事线。
既然你能够保证再也不会回到这里,那么用 * 还是 + 对你而言又有什么区别呢?
恕我愚钝,我是实在想不到 * -> 的存在意义,如果有想出用途的读者欢迎点醒我。

胶水(Glue)<>#

顾名思义,就是把「后面一行」强行粘到「涂了胶水的地方」。

“不好,我们回去!”<>  // 这里的胶水无效 因为选项必然换行

    + “去萨维尔街[”],<>
        -> as_fast_as_we_could

    + “去沈阳大街[”],<>
        -> as_fast_as_we_could

=== as_fast_as_we_could ===

要尽可能地快。”

逻辑判断(Conditional blocks){ }#

if/else 条件判断

类似别的语言里的 if-else

{ true_or_false:  // 判断条件
    这一行只有 true_or_false 为 true 的时候才会显示。
}

不止文本,选项也可以放在判断结果里执行(满足条件才出现选项)。

{ not injured and (got_the_key or super_powered):  // 支持逻辑运算符 and or not
    // 这一行只有「无伤抵达」并且「拿到钥匙 或者 力量超强」的时候才会显示。
    你打开了这个密室。
}

一个小坑:and 可以写成 &&,但 or 不能写成 ||(因为会和「序列」混淆)。

当然也可以加上 else:

{ true_or_false:
    结果为 true。
- else:
    结果为 false。
}

如果有 else if,if-else 也可以写成平铺的形式:

{
    - bless:
        你受到了神圣的庇佑,免疫了这个诅咒。
    - own_senior_curse:
        这个诅咒被一股比它更为邪恶的力量驱散了。
    - else:
        你无法抵御这个诅咒。你死了。
}

甚至可以扩展为类似 switch 的形式:

{ x:
    - 0: 零
    - 1: 一
    - 2: 二
    - else: 很多
}

用于正文

一个小 Tip:「途经的结点」可以作为判断条件。

当条件判断用于选项时,可以简写作为可选触发的特殊语句:

你要如何前往下一个城镇?

    + 你选择骑马前往[]。
        -> ride_a_horse

    + 你选择步行前往[]。
        -> just_walk

    + 你脑海里突然响起一个声音:“布响丸辣”[]。
        -> have_a_rest

=== ride_a_horse ===

你在骑马疾行的过程中被绊马索暗算,摔下了马。
    -> next_town

=== just_walk ===

走到半路,你被路边突然冒出来的一伙蒙面恶徒团团包围。
    -> next_town

=== have_a_rest ===

虽然搞不太懂是什么意思,总之还是在这个村子休息一阵子再出发吧。
你养精蓄锐后重新启程,意外发现路上的人烟出奇地少。
    -> next_town

=== next_town ===

{ride_a_horse: 你的四肢和脖子折成了不可思议的角度。}
{just_walk: 你竭力反抗。可是双拳难敌四手。}
{ride_a_horse or just_walk: 你死了。}
{have_a_rest: 沉默地不断前进着,你心里莫名生出的异样感越来越强烈。}
{not ride_a_horse and not just_walk: 你怀着不安的心情抵达了邻近的城镇。}
{not (ride_a_horse or just_walk): 你惊讶地发现这个地方已经被烧成了一片白地。}

用于选项

当条件判断用于选项时,可以简写在开头:

“好了,还有别的问题吗?”

    + {have_the_evidence} “等等,你得看看这个。”
    + {not have_the_evidence} “等等,我想起一件很重要的事。”
    + “没有,我们走吧。”

可变文本(variable text){ }#

当一对花括号 {} 用在文本区域时,视为可变文本。

序列

如果什么标记都没有加,默认视为序列文本,每次触发时依次显示序列中的文本。
当显示次数超过文本总数时,显示最后一条文本。

{我花掉四个银币买了一瓶药剂。|我又买了第二瓶。|我身上已经没有钱了。}

循环 &

类似序列,但显示次数超过总数时,会重新从第一条文本开始,无限循环下去。

今天是{&周一|周二|周三|周四|周五|周六|周日}。

一次性 !

类似序列,但显示次数超过总数时,不再继续显示。

他给我讲了个笑话。

{!我有礼貌的笑了。|我微笑了。|我做了下鬼脸。|我不想再做出反应了。}

随机 ~

每次触发都从可选的文本中随机选取一个显示。

硬币高高抛起,快速旋转着落到我手上。是{~正面|反面}。

进阶技巧

元素可以为空
你前进了一步{!|||,然后火把熄灭了。 -> into_dark}。
可以嵌套
这个怪兽{&{!看起来很不情愿,|犹豫了一下,}{没有选择|试图}攻击|抓}{&你|伤了你{~的腿|的胳膊|的胸}}。

看起来很复杂吗?让我们拆解一下。
首先是外层的循环(&)序列,显然可得前后两个循环是一一对应的:

  • {!看起来很不情愿,|犹豫了一下,}{没有选择|试图}攻击
  • 伤了你{~的腿|的胳膊|的胸}

后者没什么好说的,一个随机(~)序列,随机攻击腿、胳膊、胸三个部位其中之一。

重点分析前者,前面的部分又可以拆开:

  • {!看起来很不情愿,|犹豫了一下,} 这是一个一次性序列(!
    1. (用掉 看起来很不情愿,)最开始是不想动手的
    2. (用掉 犹豫了一下,)有点犹豫,但还是出手了
    3. (已经用光文本了)不再犹豫,果断出手
  • {没有选择|试图} 这是一个普通序列,用掉 没有选择 之后就会停留在 试图

所以最后的效果:

这个怪兽看起来很不情愿,没有选择攻击你。
这个怪兽抓伤了你的腿。
这个怪兽犹豫了一下,试图攻击你。
这个怪兽抓伤了你的胸。
这个怪兽试图攻击你。
这个怪兽抓伤了你的胳膊。
可以插入跳转
我{耐心等待着|等了一会|又等了一会|开始打盹了|醒来又等了会|放弃并离开了。 -> leave_office}。
可以用在选项中

不过不能用在开头,因为会和「条件判断」混淆,产生无法预知的错误。

在循环的选项内使用,无需特别的处理就可以让游戏看起来很智能。

-> whack_a_mole

=== whack_a_mole ===

{我举起了锤子。|{~没打中!|没有!|不好,它在哪?| 偏了!| 见鬼…| 啊,手滑了!| 嗷呜!|啊哈,抓住了!-> game_over }}
这个{&地鼠|{&不友好的|可恶的|低级的}{&生物|啮齿动物}}{在某处|藏在某处|仍然逍遥法外|嘲笑我|依旧没打中|注定会失败}。 <>
{!我会抓住它!|但这次它逃不掉了。}

    *  [{~打|砸|尝试}左上角]  -> whack_a_mole
    *  [{~就是|暴击|猛击}右上角] -> whack_a_mole
    *  [{~砍|锤}中间] -> whack_a_mole
    *  [{~痛击|肯定是}左下角]   -> whack_a_mole
    *  [{~掀开|重击}右下角] -> whack_a_mole
    * ->
        你累坏了!地鼠击败了你!
        -> game_over

    = game_over

    游戏结束  # CLASS: end

        + 再玩一次?
            # RESTART
            -> END  // 防止编辑器报语法错误用的 实际在上一行就重启了 永远不会执行到这一行

查询(Queries)#

计算当前存在的选项数量 CHOICE_COUNT()

返回到目前为止,当前区域内存在的选项数量。

* {false} Option A  // 条件判断未通过 不显示
* {true} Option B  // 条件判断通过 显示
* {CHOICE_COUNT() == 1} Option C  // 条件判断通过 显示

当执行 CHOICE_COUNT() 的时候 Option A 没有显示,只有 Option B 显示,因此 CHOICE_COUNT() 为 1。最后结果为显示 Option BOption C

计算已经经过的回合数 TURNS()

返回自游戏开始以来的回合数(跳转次数),重复的结点也算。

计算从某个结点开始经过的回合数 TURNS_SINCE(-> knot)

返回自上次访问特定结点(包括针脚子结点)以来的跳转次数。
若返回值为 0 表示「正在经历这个结点」。

=== gamble ===

你的赌金输掉了。

    + [继续下注]“至少把本金捞回来吧?”你想。
        -> gamble

    + {TURNS_SINCE(-> gamble) == 0} [自认倒霉]算了,以后再也不碰了。
        -> END

如上例所示,只有第一次赌博可以抽身离开,只要开始尝试「翻本」就再也回不了头了。

十赌十输,赌狗最后 100% 会赔掉所有人性。答应我,远离赌博,好吗?

随机数生成器种子 SEED_RANDOM()

如果你的游戏里存在随机数生成,测试的时候显然很不方便。
此时可以使用固定随机种子,保证每次随机的结果是一致的。
这里的一致指序列一致,结果本身还是随机的。

某些特殊的机制也可能会用到。

编织(Weave)-#

到目前为止,我们一直在以最基本的方式构建故事,从「选项」到「页面」。
但这要求我们:

  1. 必须命名故事中的每个分支的目的地(结点)
  2. 且保持唯一性(结点的标识符)

对像我这样的取名废来说简直是噩梦。
这大大拖慢了我们编写的速度,阻挠了我们创作更多分支(的意愿)。

因此 ink 推出了一种新的语法,旨在简化「始终向前的故事线」。
最后大部分时候写起来更像流式的「正常小说」,而不是「计算机程序」。
它就是「编织」:

这也是「结点」「针脚」等术语的命名原因。

“怎么了?”我的主人问。

    * “我有些累。”[]我重复道。
        “确实。”他答道,“这太糟了。”

    * “没事,先生!”[]我回应道。

    * “我说,这次旅行真可怕[。”],我不想再继续了。”
        “啊。”他有些动容,"你看起来很沮丧。到了明天,情况会好些的。"

- Fogg 先生离开了房间。  // 所有分支都会在这里收束

无论你选什么选项,都会走向同一个结果:「Fogg 先生离开房间」,这就是「收束」。

可以嵌套

- “讲个故事,船长!”

    * “好吧,你们这些海狗。这个故事是这样的……”
        * * “在一个黑灯瞎火狂风暴雨的夜晚……”
            * * * “……船员们都很不安……”
                * * * * “……他们对船长说……”
                    * * * * * “……给我们讲个故事!”

    * “不,已经过了你们睡觉的时间。”

- 无一例外,船员们开始打起哈欠。

总结一下,分支/收束这种方式的好处是你不必再耗费心力思考哪个选项该去哪里。
只需要专注于撰写故事,在拥有大量分支可供选择的情况下,不用额外处理,依旧可以自然而然地顺利从头走到尾。

可以定位 ( )

理论上整个 ink 文件都是一个大的编织,因此你可以在任何地方使用标签 (label_name) 来标注「发生过一件事」。

=== meet_the_guard ===

卫兵在盯着你。

    + (greet) [打招呼]“早。”

    + (get_out) “让开。”[]你对卫兵说。

- “嗯。”卫兵回应道。

    + {greet} “天气不错哈?”

    + “咦?”[]你回应道?

    + {get_out} [把他推到一旁]你把他推到一旁。作为礼尚往来,卫兵直接拔出了剑。
        -> END

- “嗯。” 卫兵给你一个小纸袋,“太妃糖?”

如果换成 galgame 的逻辑来说,就是「立起了 flag」。

“好安静啊。说点什么吧。”

    * (dead_flag_1) “打完这仗我就回老家结婚。”
    * (dead_flag_2) “干完这一票我就金盆洗手了。”
    * (dead_flag_3) “もう何も恐くない。”
    * [沉默]你默默擦拭剑身,什么都没说。

- {dead_flag_1 or dead_flag_2 or dead_flag_3: 你阵亡了。 -> END}
- 战争结束了。 -> END

标签也可以在任何地方被检查,同「结点 / 针脚子结点」一样使用 . 链式引用。

利用标签可以构建标准的 NPC 询问场景:

- (opts)
    * “我从哪能弄到制服吗?”[]你询问快乐的卫兵。
        “当然,在储物柜那。”他笑着说,“不过可能不太合身。”
    * “告诉我安防系统。”
        “它很老。”卫兵信誓旦旦地保证,“就像古董一样老。”
    * “这里的狗?”
        “很多很多。”卫兵咧嘴一笑,“也都是饿鬼。”
    * {loop} [问够了] // 只有经过了 loop 才会显示,即:要求玩家至少问一个问题
        -> done

- (loop) // 在警卫厌烦前循环
    { -> opts | -> opts | } // 用可变文本序列控制卫兵是否厌烦
    他挠挠头。
    “好吧,不能整天只站着说话。”他说道。

- (done)
    你谢过警卫,便离开了。 -> END

变量和逻辑运算(Variables and Logic)#

全局变量(Global Variables)

定义全局变量:

VAR knowledge_of_the_cure = false
VAR players_name = "Emilia"
VAR number_of_infected_people = 521
VAR current_epilogue = -> they_all_die_of_the_plague

使用全局变量:

=== the_train ===

火车颠簸了一下。{ mood > 0: 我觉得还好,没介意奇怪的颠簸|我受不了了}。

    * { not knows_about_wager } “但是,先生,我们为什么要旅行?”[]我问道。
    * { knows_about_wager} 我考虑了我们有些离奇的冒险[]。这可能吗?

打印全局变量:

VAR friendly_name_of_player = "Jackie"
VAR age = 23

我的名字叫 Jean Passepartout,但我的朋友都叫我 {friendly_name_of_player}。我 {age} 岁了。

进阶用法:
跳转命令本身是一种值,因此可以存在变量里。

VAR current_epilogue = -> everybody_dies

=== continue_or_quit ===

现在放弃,还是努力拯救你的国度?

    * [绝不言弃] -> more_hopeless_introspection
    * [我太累了] -> current_epilogue

再进一步,由于 ink 本身(解释器)和「文本」高度结合的特性,计算式也是一种「值」。
也就是说,你可以轻松使用类似别的语言里的 eval() 功能,把字符串当作代码执行。

VAR a_color = ""

~ a_color = "{~红色|蓝色|绿色|黄色}"

{a_color}  // 等于直接 {~红色|蓝色|绿色|黄色}

需要注意的是,当你输出过一次之后,这个式子输出的结果就确定了。

就好比薛定谔的猫,你不观测它,它就是生与死之间的叠加态;你一观测它,它就会坍缩为某个确定的本征态。

呆子撞到了你,你眼冒金星,{a_color}和{a_color}交替闪烁。
// 试图这样实现多次随机是不行的 两次输出的颜色一定会是同一种颜色

临时变量(Temporary Variables)

只在定义的结点内部生效,离开结点后里面的临时变量将被丢弃。

=== warn ===
    ~ temp number_of_warm_things = 0

常量(Constants)

互动小说往往需要状态机来追踪当前故事进程已经发展到哪个阶段。
有很多方式可以实现这一点,但最方便的还是使用常量。

CONST LOBBY = 1
CONST STAIRCASE = 2
CONST HALLWAY = 3

CONST HELD_BY_AGENT = -1

VAR secret_agent_location = LOBBY
VAR suitcase_location = HALLWAY

=== report_progress ===
{ secret_agent_location == suitcase_location:
    特工抓住了密码箱!
    ~ suitcase_location = HELD_BY_AGENT

- secret_agent_location < suitcase_location:
    特工继续前进。
    ~ secret_agent_location++
}

逻辑与数值运算(Logic and Numerical)

=== set_some_variables ===
    ~ knows_about_wager = true
    ~ x = (x * x) - (y * y) + c
    ~ y = 2 * x * y

=== estimate_variables ===
    { x == 1.2 }
    { x / 2 > 4 }
    { y - 1 <= x * x }

基本的数学运算都支持,加 +、减 -、乘 *、除 /,以及取余 %(或者 mod)。
甚至支持「(允许指数为小数的)幂运算 POW()」:

{POW(3, 2)} 的结果为 9。
{POW(16, 0.5)} 的结果为 4。

当然也可以生成随机数:

~ temp dice_roll = RANDOM(1, 6)
~ temp lazy_grading_for_test_paper = RANDOM(30, 75)
~ temp number_of_heads_the_serpent_has = RANDOM(3, 8)

注意 ink 是带有隐式类型转换的,尤其是除法——
整数除以整数自动取整、浮点数除以浮点数仍为浮点数。
当你不想要自动类型转换,或者需要对数值进行舍入时,可以强制指定类型转换:

{INT(3.2)} 的结果为 3。
{FLOOR(4.8)} 的结果为 4。
{INT(-4.8)} 的结果为 -4。
{FLOOR(-4.8)} 的结果为 -5。

{FLOAT(4)} 的结果为,呃,仍然为 4。

官方自承很奇怪的一点是,ink 作为一个文本游戏引擎,并没有那么多花里胡哨的字符串处理功能。只有最简单的三种判断:

{ "Yes, please." == "Yes, please." }  // 等价
{ "No, thank you." != "Yes, please." }  // 不等价
{ "Yes, please" ? "ease" }  // 包含
// 以上三个结果都为 true

多行块(Multiline blocks)#

其实就是展开的序列:

// 普通序列:依次显示序列中的文本,显示完后停留在最后一条
{ stopping:
    - 我进入了那个赌场。
    - 我又进入了那个赌场。
    - 又一次,我进去了。
}

// 循环序列:依次显示序列中的文本,显示完最后一条后回到开头
{ cycle:
    - 我屏住呼吸。
    - 我不耐烦地等着。
    - 我犹豫着。
}

// 一次性序列:依次显示序列中的文本,显示完最后一条后不再显示
{ once:
    - 我的运气如何?
    - 这一手我能赢吗?
}

// 随机序列:随机抽取其中一个显示
坐上赌桌后,我抽了一张牌。 <>
{ shuffle:
    - 红桃 A。
    - 黑桃 K。
    - 方片 2。
        “这局你输了!”庄家叫嚷道。
}

默认的「随机序列」确切地说其实是「随机循环序列」。
你可以视为每次洗牌后抽一张,然后在放回去等待下一次洗牌。
还有两种进阶的随机方式:

// 随机一次性序列:每次随机抽取一个,不放回去,抽完就没了
{ shuffle once:
    - 太阳很晒。
    - 天气很热。
}

// 随机停留序列(对应普通序列):每次随机抽走一个,抽到只剩最后一个
{ shuffle stopping:
    - 一辆银色宝马呼啸而过。
    - 一辆明黄色的野马正在转弯。
    - 这里有许多车。
}

函数(Functions)#

我们之前说过结点和针脚(子结点)近似函数,但毕竟不是真正的函数。
最主要的原因就是因为「不能返回值」;真正的函数是可以返回return值的,哪怕返回 void。

所以说起来,「结点/针脚」是类似「命令」的东西,「函数」才是可以直接返回值的真正的「行内函数」。用过 Emuera、写过 erb(era basic)的有没有既视感?

=== function lerp(a, b, k) ===
    ~ return ((b - a) * k) + a

=== function say_yes_to_everything ===
    ~ return true
~ x = lerp(2, 8, 0.3)

* {say_yes_to_everything()} 'Yes.'

至于返回另一个函数(的结果)或者递归调用自己都是允许的:

=== function say_no_to_nothing ===
    ~ return say_yes_to_everything()

以引用方式传递参数(ref argv

默认情况下,我们传递的参数是「深拷贝后的局部临时新值」。
但我们有时会需要对某个变量进行某种处理,此时引用就派上用场了。
直接传递变量本身,在函数内直接进行操作。

又一个我们熟知的 erb 里也有的特性,也许这就是解释型脚本语言的殊途同归吧。

=== function alter(ref x, k) ===
    ~ x = x + k

隧道(Tunnels)#

虽然结点允许传递「跳转目标」作为参数,但这种用法一旦多了还是显得繁琐和啰嗦:

=== crossing_the_date_line(-> return_to) ===
- -> return_to

=== outside_honolulu ===
我们抵达了 Honolulu 最大的岛。
- (postscript)
    -> crossing_the_date_line(-> done)
- (done)
    -> END

=== outside_pitcairn_island ===
水上的船驶向小岛。
- (postscript)
    -> crossing_the_date_line(-> done)
- (done)
    -> END

而且还有个问题就是:如果跳转跨越了多个结点怎么办?使用上面的写法,我们必须把 return_to 参数不断传递下去,以确保我们始终知道返回的位置。

为了解决这个问题,ink 为「执行完跳转回原处」的这类结点加入了一种语法。
也就是「隧道(Tunnel)」:

=== crossing_the_date_line ===
// 这里是隧道!
- ->->

用起来也很简单:

-> crossing_the_date_line ->

你甚至可以连续串起多个隧道:

-> crossing_the_date_line -> check_foggs_health -> done

多个隧道本身也是可以嵌套的。
所以类似「战斗结算」这种场景(分别结算经验值、道具、货币)写起来会很方便。

线程(Threads)#

  • TODO

不想写了,累了,咕了。
如果你需要这部分内容,请联系我催更。(文末有联系方式)

列表(Lists)#

  • TODO

杂项#

插入图片#

# IMAGE: image_name.png

如果你使用的图片达到一定数量,推荐使用单独的文件夹来存放。
此时可以通过相对路径访问:

# IMAGE: img/image_001.png

放在行尾的图片也可以正常插入,比如:

她拿出一张地图:“你看这里。” # IMAGE: img/level_3/map_01.png

图片总是会显示在文本上方(而不是重叠成一团 某 Emu 学废了吗?),因为所有标记都是和特定的文本行相关联的。当标记被单独放置在某一行时,它将自动与下方的文本行关联。

清空屏幕#

# CLEAR

重启游戏#

这个命令同时还会 清空玩家所有「经历过的结点」和「修改过的变量」

# RESTART

URL 链接跳转#

+ 跳转到网址
    // 注意必须用 \/\/ 转义 否则后面的 URL 会视为注释文本
    # LINK: https:/\/lackb.fun
    -> END

+ [在新窗口打开网址]已经在新窗口打开网址。
    # LINKOPEN: https:\/\/lackb.fun
    -> END

HTML 标签#

警告:这是一个我不清楚它到底是 特性feature 还是 漏洞bug 的功能,但到目前为止(2022.6.28,Inky v0.13.0,ink v1.0,ink.js v2.1.0),它是有效的。
但我不能保证它以后会一直可用,而且确实也不怎么 neat,完全跟优雅二字沾不上边。

你可以直接在正文里写 HTML 标签。

<span style="color: blue;">这是一条带有<b>加粗</b>、<i>斜体</i>、<u>下划线</u>、<s>删除线</s>的蓝色文本。</span>

它在 Inky 编辑器里会显示异常,但当你把它导出到 Web 再打开时,它是正常工作的。

合并多个脚本文件#

INCLUDE utils/player_status.ink
INCLUDE level_1/dungeon.ink

项目元(meta)信息#

暗黑(Deep Dark)模式:

# theme: dark

作者署名:

# author: 你の名字

自定义 CSS 的类(class)#

你推开房门。
立刻看到地上有一滩血。 # CLASS: danger

编辑导出项目的 style.css 文件,新增:

1
2
3
.danger {
   color: red;
}

你推开房门。
立刻看到地上有一滩血。

另外还有可以直接使用的内置默认样式:

# CLASS: end

lackbfun © 2021 - 2024