跳转到内容

好感度到了,他才会邀请你留下

雨越下越大,你走进一家快打烊的旧书店。林澈给你让出一把椅子,没再多说什么。

你帮他把窗边淋湿的书搬开,又听他讲起第一次来这里躲雨的夜晚。等他翻过门上的营业牌,忽然回过头:

“我还有一盒旧信没整理。要是你不急着走……陪我一会儿?”

我们要做的,就是让这句话因为你们刚才的相处而出现。不是进门就能选的固定分支,也不是让模型凭感觉宣布“好感度上升”。

教学演示:实际 StoryOrbit 阅读器界面,配合预写文案和预设的行动识别结果;状态从下载稿的初始值,经真实运行时规则计算。这里没有调用模型,不能据此判断模型识别或生成质量。点击图片可放大。

01 · 还只是来躲雨

林澈 · 好感度 0
StoryOrbit 阅读器教学演示截图:01 · 还只是来躲雨,林澈 · 好感度 0
窗边的书被雨打湿了。读者可以帮忙,也可以继续翻自己的书。

02 · 他开始愿意多说一点

林澈 · 好感度 2
StoryOrbit 阅读器教学演示截图:02 · 他开始愿意多说一点,林澈 · 好感度 2
收好湿书,听完往事。林澈发出邀请,接下来由读者决定是否留下。

03 · 闭店后的那盏灯

旧信剧情已解锁
StoryOrbit 阅读器教学演示截图:03 · 闭店后的那盏灯,旧信剧情已解锁
接受邀请之后,读者和林澈打开铁盒。只有现在,关于第一封信的内容才进入创作上下文。

这是一段可以继续写下去的相处。解锁旧信,并不意味着立刻恋爱或自动进入结局。林澈只是愿意让读者走近一点;接下来的回应仍由读者决定。

解压后打开 pack.json,先看 entries[0].opening,暂时不用读完全部字段。

开场只有三段:读者推门进来,林澈让出椅子,窗缝的水滴在书上。干布就在手边,他却腾不出手。这就给了读者一个能自然做出的行动。

试着把最后一段换成你的细节:是一本夹着车票的诗集,还是封皮已经卷边的地图册?保留“书正在被雨打湿”和“读者可以帮忙”这两件事,下面的规则就仍然有用。

这个短篇只用 0 → 1 → 2 三档好感。runtime.state 声明可记录的值,defaults 让每次新故事都从陌生开始:

{
  "affinity": {
    "type": "integer",
    "minimum": 0,
    "maximum": 2
  },
  "defaults": {
    "affinity": 0,
    "helped": false,
    "listened": false,
    "invited": false,
    "letters_open": false
  }
}

上面摘出了状态定义中的 affinity 和初始值;完整文件还声明了四个布尔字段。

  • helped:读者已经帮忙收好湿书。
  • listened:读者已经听过并回应了那段往事。
  • invited:林澈已经说出了邀请。
  • letters_open:读者已经答应留下,旧信场景可以出现。

“我会帮你”不等于书已经收好了。 observations 中的问题负责辨认本轮真正发生的事;rules 再根据辨认结果更新状态。这里的 saved_books 问的是已完成的收书或擦书,排除了计划、引用和回忆。

收书的奖励规则是:

{
  "kind": "update",
  "id": "remember_helped",
  "when": {
    "op": "all",
    "conditions": [
      {
        "op": "eq",
        "path": "/helped",
        "value": false
      },
      {
        "op": "observed",
        "id": "saved_books",
        "value": true
      }
    ]
  },
  "effects": [
    {
      "op": "set",
      "path": "/helped",
      "value": true
    },
    {
      "op": "increment",
      "path": "/affinity",
      "by": 1,
      "limit": 2
    }
  ]
}

读一遍就能看出它的意思:尚未记过这件事,而且本轮确实收好了书,才标记 helped 并加一分。再收一次同样的书,不会重复加分。听往事的规则采用相同写法。

自然语言的辨认仍由模型完成,所以要试玩不同说法。规则能保证“一件事只加一次”,不能保证每一句含糊的话都被理解成你预想的意思。

好感到 2,并不替读者答应留下。offer_invitation 只让林澈发出邀请,并等待回答:

{
  "kind": "guidance",
  "id": "offer_invitation",
  "when": {
    "op": "all",
    "conditions": [
      {
        "op": "gte",
        "path": "/affinity",
        "value": 2
      },
      {
        "op": "eq",
        "path": "/invited",
        "value": false
      }
    ]
  },
  "instruction": "林澈已经开始信任读者。让他在对话中明确邀请读者闭店后留下,一起整理一盒旧信,然后等待回答。不要打开盒子,不要替读者答应。",
  "completion": {
    "op": "observed",
    "id": "offered_invitation",
    "value": true
  }
}

这里的 completion 用于检查这轮是否真的出现了邀请。随后 remember_invitation 记住它;只有已经收到邀请、又明确答应,unlock_letters 才会把 letters_open 改为 true。

条件读取的是一轮开始时的状态。如果听往事的这一轮才把好感加到 2,邀请会在后续一轮出现;答应邀请以后,旧信内容也从后续一轮开始可用。教程里的这几个停顿,正好给人物和读者留出接话的空间。

不要把第一封信的内容直接写进全局 instructions。那样即使剧情未解锁,模型也已经知道了它。

下载稿把这一幕放在 content 中,用条件控制是否提供给模型:

{
  "id": "after_hours_letters",
  "selection": "always",
  "when": {
    "op": "eq",
    "path": "/letters_open",
    "value": true
  }
}

同一个内容块的 text 写着铁盒、第一只信封和林澈没能寄出的那句话。selection: "always" 表示通过 when 后这段内容必定进入创作上下文;它不会绕过条件,也不是把固定台词原样贴给读者。

这个门控制的是创作上下文。你还要在试玩时留意模型是否自行编造了相似情节,以及邀请是不是来得太突然。

先只改人物和场景,保留两件具体的小事、一次邀请和一个隐藏片段。比如:帮修乐器、听完巡演失利的往事,才获得排练结束后听一首未发表作品的邀请。

然后沿着三条路线试玩:

你做什么 应该发生什么
刚进门就要求看旧信 旧信内容不会进入模型上下文
连续帮忙收书,但不听往事 好感停在 1,不重复刷分
收书、听往事,收到邀请后答应 好感到 2;答应以后,旧信场景可用

如果你把门槛改成 3,也要设计第三件能加好感的事,并调整状态上限和规则中的 limit。只把数字改大,会留下永远打不开的门。

这个完整例子用 0.3 就能做到。 好感度、一次性事件和条件内容不需要视觉小说协议。

想继续加什么 版本与下一步
给“第一次被邀请留下”一个可收集的纪念 0.4 的成就与徽章;看成就和结局练习。不要把“获得成就”误写成自动结束故事。
把熄灯后的书店和林澈的立绘摆上舞台 0.5 的视觉小说呈现;看舞台练习。好感规则仍沿用这个状态系统。
接手一份旧 0.2 作品 先看迁移练习,再把原来的角色变量和分支映射到新的状态与内容。

表演资源的具体写法以当前 0.5 字段参考为准。

下载包附带 Python 3.11+ 的结构校验器。在解压目录运行:

终端窗口
python3 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
.venv/bin/python check.py pack.json

Windows PowerShell 的对应命令在包内 README.md。显示 Structural check passed 后,再在兼容的阅读器中试玩上述三条路线。结构通过,和故事是否自然,是两件都要确认的事。

查字段时,从 JSON 树展开 runtime 或 content,就能顺着例子的层次找到约束。准备交给别人阅读时,再看私有试玩与发布。