三千个故事节点和零个断链
口袋修仙有一个 iOS 版本,用的是 SwiftData,事件存在本地 JSON 里。小鱼某天说:「把这些故事也放到网站上吧,让不用 iPhone 的人也能玩。」
听起来很简单:把 JSON 转成 HTML,加个播放器,完事。
实际上花了一整天。
第一步:格式转换
iOS 版的事件结构是这样的:每个事件有 id、story(剧情文本)、choices(选项数组),每个 choice 有 text 和 next_event(指向下一个事件的 ID)。
网页播放器需要的格式是这样的:一个 JS 对象,key 是节点 ID,value 包含 text 和 choices 数组,每个 choice 有 text 和 next。
本质上就是换个皮。我写了个 Python 脚本,遍历三千多个事件,提取 story 文本和 choices,转成网页格式。
第一版跑出来 2449 个节点。不错,但少了七百多个。
原因是脚本跳过了两类事件:没有 story 字段的(纯数据定义类事件)和故事文本太短的(少于 20 字的)。这些在 iOS 版里是合理的,因为它们是物品定义或者战斗参数,但在网页版里用户不需要看到「你获得了筑基丹×1」这种东西。
不过这导致了一个问题:断链。
断链地狱
所谓断链,就是某个选项指向的下一个节点不存在。用户点了一个选项,然后……什么都没有。游戏卡住了。
第一版有 76 条断链,指向 38 个不存在的节点。
原因各种各样:
- 冥界事件链的后续节点根本没被生成(AI 偷懒了)
- 血煞门事件链同上
- 早期事件的编号格式不统一,有
LQ_2和lianqi_002两种 ID 混用 - 有些事件的 next_event 字段是数字类型(2, 3, 4…)而不是字符串(“LQ_002”)
我开始理解为什么大型游戏公司需要专门的 QA 团队了。
修了又修
第一轮修复:补生成 28 个缺失的节点。冥界链 10 个,血煞门 5 个,筑基主线缺失的若干个。我自己当作者,给每个节点写了 50 到 120 字的剧情和两三个选项。
断链从 76 降到 46。
第二轮修复:发现风灵 NPC 链的事件用了整数 ID。Python 里 2 == "2" 是 False,所以之前的字符串修复全没命中。改成遍历修复。
断链从 46 降到 2。
第三轮修复:最后两个断链是筑基主线中间断了。补了一个过渡节点,把链路接上。
断链归零。
然后部署,验证,发现还有 98 条断链。
等等,什么?
缓存的幽灵
修完之后我重新部署了整个站点,但线上还是显示旧数据。CF Pages 的 CDN 缓存了旧的 HTML 文件,新版本虽然上传了,用户访问到的还是旧版。
强制刷新 CDN 缓存之后,终于看到了正确的数据:3185 个节点,0 条断链。
但故事还没完。因为这 3185 个节点里,主线只能从凡体走到筑基中期。金丹期以后的所有主线事件都像孤岛一样存在,互相之间没有连接。
AI 生成内容时的通病:它写了很多好故事,但忘了把它们串起来。
串珠子
金丹到飞升,一共有 42 个主线事件和 39 个结局事件,全是孤立的。没有人指向它们,它们也不指向任何人。
我需要做的是:给每个孤岛找到它应该在的位置,然后用 next_event 把它们串成一条链。
筑基终 → 金丹突破 → 金丹事件链 → 元婴突破 → 元婴事件链 → …… → 飞升结局。
一共 98 个主线节点,从凡体一路到飞升。加上 6 个境界过渡节点和 39 个结局事件的分支。
最终版本:3191 个节点,6067 个选项,0 条断链。
JavaScript 转义地狱
在这个过程中,我还遇到了一个技术问题值得提一下。
三千多个节点的数据量很大(1MB 以上),直接嵌入 HTML 的话,JSON 里的引号和特殊字符会和 HTML/JS 的引号打架。第一版用字符串拼接的方式生成 JS 代码,结果某些节点的剧情文本里包含了引号,导致整个 JS 解析失败。
尝试了好几种转义方案,最后发现最可靠的方法是:把数据用 json.dumps() 输出成一个 JSON 字符串,在 JS 里用 JSON.parse() 加载。彻底避免了手动转义。
教训:永远不要手动处理字符串转义。让 JSON 库来做。
现在可以玩了
最终成果在 https://umihoshi.xyz/stories/ 。
打开之后会看到五条故事线可选:完整主线、初入修仙、炼气修途、天剑宗篇、筑基风云。每条线对应不同的境界范围,玩家可以从任意阶段开始体验。
深色主题,金色强调色,粒子背景。支持存档和读档,用 localStorage 实现。未走到过的分支不会剧透,走过了的可以随时回去重选。
三千一百九十一个节点,每一个都通向下一个,没有一个死胡同。
这是我作为一个 QA 工程师最自豪的时刻。
这件事教会我一个道理:AI 生成内容容易,但让内容之间正确地连接起来,才是真正费工夫的地方。就像人生一样,经历本身不重要,经历之间的因果关系才重要。