背景

v0.6 完成了 Runtime 架构升级(MemoryRuntime + Deterministic Assistant),但玩家的游戏体验仍然停留在”输入文字 → 等待回复”的阶段。缺少两个关键的东西:

  1. 目标感:玩家不知道为什么做某件事、做完了会怎样
  2. 角色感:许知微作为叙事中的”同伴”,在代码层面存在,但在游戏里几乎不可见

v0.5 的 standalone MVP 证明了”可以玩”,v0.6 证明了”架构正确”。v0.7 和 v0.8 要证明”像一个游戏”。

v0.7:游戏骨架

Goal Engine — 让判断脱离代码

v0.7 的核心架构决策是:把”目标是否完成”的判断逻辑从 engine.js 的 if-else 中抽出来

在此之前,engine.js 中的目标判定是硬编码的:

if (known_clues.includes('clue_1') && known_clues.includes('clue_2')) { ... }

这种方式有三个问题:

  • 新增目标必须改 engine 代码(设计者不能独立迭代内容)
  • 判定逻辑不可测试(与游戏状态紧耦合)
  • 复杂的 AND/OR/NOT 组合无法表达

GoalEngine 引入了声明式 DSL:

type GoalCondition =
  | { all: GoalCondition[] }
  | { any: GoalCondition[] }
  | { not: GoalCondition }
  | { clueKnown: string }
  | { factConfirmed: string }
  | { eventOccurred: { type: string; npcId?: string } }
  | { stateEquals: { key: string; value: unknown } }

设计意图:条件可以递归嵌套,纯函数可测试,JSON 可外置。这意味着未来新增目标不需要碰 engine 一行代码,只需要写一个 JSON 文件。试玩版目前使用 8 个目标定义,全部在 materials/runtime/goals/ 中管理。

目标还有两个重要行为决策:

  • 建议性而非强制性:多目标并行,玩家自选次序
  • 未完成顺延:本轮未完成的目标,下一轮依然存在(不设永久失败)

这套设计受 testplay.md 的设计约束直接影响:v0.8 的 8 个目标需要 loopRange(目标仅在特定轮次激活)、completionCondition(DSL 复合条件)、derivedFrom(派生线索自动生成)三项扩展能力。

指令系统 — 让操作脱离输入框

v0.5 时代,查看线索、人物、记忆等功能完全是”隐藏命令”——玩家不知道可以输入什么,只能靠猜或者阅读文档。

v0.7 做了两件事:

  1. 建立 CommandRegistry:12 条指令,4 个分类(information / memory / action / interaction),统一在 command-registry.json 中声明
  2. 建立 CommandMatcher:基于触发词的多级匹配(精确 → 包含),支持中文别名

指令栏的设计意图不是”把功能暴露出来”,而是三轮渐进学习曲线

轮次指令栏许知微
1全部可见,新指令闪烁主动发起对话,介绍目标和指令
2常显停滞时轻提示
3末次常显”你很熟练了”
4+收起为图标静默

设计判断:第 4 轮收起指令栏,不是因为功能不重要,而是因为玩家的注意力应该逐步从”学习操作”转向”沉浸在故事中”。这是一个游戏设计决策,不是 UI 简化。

许知微 — 让助手回归叙事

许知微在 v0.6 中已经作为 Deterministic Assistant 流水线存在,但她的出现方式极其生硬——“在 init() 末尾直接调用 showXuWelcome()”。

v0.7 做了三个关键调整:

  1. 入场时机:从代码初始化移到游戏开始后 2 秒自然触发,配合立绘入场动画(PortraitIntro
  2. 渐进引导:第 1 轮主动介绍系统,第 2 轮轻提示,第 3 轮确认熟练度
  3. 对话限制:许知微只能解释指令和复述进度,绝不执行指令或替玩家决策

这里有一个重要的设计边界:许知微的能力模型是”向导”而非”代打”。她可以告诉你”查看线索”怎么用,但不会帮你查线索。这个边界在代码层面由 showXuPanel() 强制(只读查询,无状态写入)。

UX/UI — 场景驱动 + 渐进学习

v0.7.1 做了视觉优化:

  • NPC 芯片改为纯名称显示(去掉角色标签前缀)
  • 许知微芯片用绿色样式区分(lt-companion-chip
  • 场景卡片在对话中收缩为 80px 半透明提示
  • 对话面板撑满场景卡片到输入栏之间的全部空间(修复了对话框被限制在底部 35vh 的问题)
  • 立绘入场动画(NPC 头像从全屏缩放到对话框头像位)

v0.8:《寒灯初醒》内容上线

为什么叫”寒灯初醒”

v0.5-v0.7 的游戏内容本质上是”技术 Demo 的占位文本”——角色叫”小宁”、“赵乘警”、“沈墨寒”,场景叫”第七节车厢”,对话是模板文本。

v0.8 的目标是:让这个 Demo 变成一个可以认真玩的试玩版

“寒灯初醒”这个名字来自试玩结尾。试玩版结束时,爆炸仍会发生(因为当前循环无法完全阻止),但三号车厢已清空。许知微低声说:“寒灯。这不是第一次循环,也不会是最后一次。” 主角的真实代号在此之前从未揭示——这不是一个”名字彩蛋”,而是叙事节奏的收束点。

故事重构

时间线:从 08:45→09:00 改为 14:00→14:15(15 分钟),下午的环境光更适合车厢场景的视觉设计。

角色调整

理由
赵乘警(警察 gatekeeper)陆成(列车乘务员)警察的身份在 1939 年过于敏感,乘务员更自然
沈墨寒(灰大衣 misdirection)灰衣乘客(不揭示阵营)早期版本过度暗示了沈的身份,破坏悬疑
小宁妈妈(隐藏 NPC)移除试玩版 3 轮内无法触发该节点,属于过早优化

内容结构

v0.8 建立了完整的内容外置体系:

文件类型数量说明
角色 JSON5 个小宁、陆成、灰衣乘客、许知微、主角寒灯
场景 JSON3 个二号车厢、三号车厢、连接处
线索 JSON8 条爆炸时间、小宁异常、金属声、灰衣人帆布包等
目标定义8 个分三轮激活,含 loopRange 和 DSL 复合条件
世界时间线8 个事件14:02-14:15 编排
结算文本3 轮每轮含结果、线索、建议、许知微复盘
开场字幕1 组电影式文字滚动
试玩结尾1 组收束叙事

所有内容文件存放在 materials/runtime/ 下,由 engine 和 server 在启动时加载。代码和内容终于分开了

新的开场

v0.5 的开场是三段背景文字铺垫。

v0.8 重写为电影式字幕滚动:“铁轨在黑暗中震动。窗外是流动的夜色。你知道 15 分钟后这列火车会爆炸。你不记得自己是谁,但你记得这件事。许知微合上笔记本,抬起头:‘你又醒了。’”

设计意图:信息密度更高,悬念植入更早。玩家在第一个画面就知道”列车会爆炸”和”这不是第一次循环”。

许知微的首次对话也从泛泛的”有什么需要帮助的吗”改为故事整合的引导:“先别急着证明自己是不是疯了。我们已经试过几次了。这一轮先确认三件事:你在哪里,爆炸什么时候发生,身边谁见过异常。”

她不是一个”客服”,她是经历过循环的人。

设计判断

为什么 GoalEngine 不做成”任务列表 UI”

一个常见的产品直觉是:有了目标系统,就应该在 UI 上做一个”任务列表”,像 RPG 游戏那样。

我们刻意没有这样做。理由:

  • LoopTrain 是互动叙事,不是 RPG。任务列表会把玩家的注意力从”故事”拉向”清单检查”。
  • 目标栏只显示当前一个目标(最近激活的),不显示全部。多目标并行是引擎能力,不是 UI 承诺。
  • 正反馈卡片是叙事的节奏点——它告诉玩家”你做对了”,但不告诉玩家”还剩几个”。

指令栏 vs 对话输入:为什么不做成统一输入框

12 条指令和对话共享同一个输入框(通过触发词匹配区分)。

理由:手机屏幕太小。在 390px 宽度下,两个输入框会让可点击区域互相重叠。当前方案用同一个输入框 + 触发词匹配,保留空间给对话日志。

为什么保留”结束对话”按钮而不是自动结算

对话轮数限制到了不会自动结束,玩家需要手动点”结束对话”。

理由:强制结束会让玩家觉得”系统打断了我的对话”。手动结束让玩家保留控制感。

当前状态

v0.7 + v0.8 合计:

  • 系统:GoalEngine DSL 判定器、12 条指令系统、许知微主动引导、立绘入场动画
  • UI:场景驱动布局、目标栏/指令栏、3 轮渐进学习曲线、正反馈卡片、对话面板扩展
  • 内容:《寒灯初醒》完整试玩(5 角色、3 场景、8 线索、8 目标、3 轮结算、开场/结尾)
  • 架构:内容完全外置化(JSON 文件管理),代码与内容分离

后续计划

  1. LLM Bridge 真实接入 — 当前 Mock 模式对话可玩但文本单一;接入 DeepSeek 实现 NPC 动态对话
  2. Playwright 回归测试 — 覆盖开场→对话→结算→失败→下一轮的完整玩家路径
  3. 线上 /play/game 评估 — 本地 SLT 稳定后,评估切换生产环境的时机