欢迎光临
我们一直在努力

整了张修仙角色卡,能说的不能说的都在这了

先交代背景。

我想在酒馆里跑修仙。不是那种穿过去系统激活追妻火葬场的修仙,是那种你散修一个灵石掰两半花、路边野店可能黑吃黑、筑基丹要攒三年材料还得看丹阁脸色——的修仙。

但你我都知道普通角色卡做不到。AI记不住。昨天你刚筑基,今天问它你在什么境界,它:

“你是一名筑基期修士……不对你好像还在炼气?等等我再看看——你是什么境界来着?”

就这种感觉。

所以我去看MVU卡了。MagVarUpdate,一个让AI能读写变量的架构。

然后我花了大概……一天?从v0.1干到v0.7,搞了37个文件,一张PNG。过程中有好几次想摔键盘,也有几次觉得自己真牛逼。

下面这些都是我还记得的。


第一步是设计变量树

就这个:

stat_data
├── 主角
├── 修炼
├── 先天气运
├── 资源
├── 世界
├── NPC档案
└── 记忆

我一开始觉得这东西没啥好想的。后来AI教我做人了。

AI会在NPC档案里写这种东西:

/NPC档案/柳如烟/好感度:"一见倾心"(……不是大哥好感度是0-100的数字)
/主角/尾巴长度:"九尾天狐"(主角没有尾巴,也没有狐妖血统,你在写什么)
/修炼/修为进度:"我很快就要筑基了因为我吃了一颗丹药"(一串完整的汉字)

你把AI当成一个很聪明但完全没受过编码训练的新人工程师。它理解意图,但看不懂类型系统。

解决方案是Zod Schema。

我不知道你有没有用过Zod。简单说它是一个校验库——你定义"修为进度是一个0-100的数字",它就会确保修为进度永远是0-100的数字。哪怕AI传了"五十"、“-100”、“我很快就要筑基了”,Schema都能兜住。

const pct = z.coerce.number()
.transform(v => Math.min(Math.max(v, 0), 100))
.default(0)
.catch(0);

我最喜欢 .catch(0)。前面全失败了?没关系,我们是0。你的变量不会炸。

这件事教会我的东西是:变量树设计本质是在给AI划跑道。 跑道划得越清楚,AI开得越稳。跑道划得模糊,AI就——飞出去了。

后来我还学到一个东西,叫"封闭对象 vs 字典容器"。翻译成人话就是:

有些变量结构是固定的,不能随便增减字段——主角的字段是 {姓名, 性别, 境界, …},你不能今天给它加个"尾巴长度"明天加个"翅膀颜色"——这叫封闭对象。

有些变量数量是动态的,但每个个体结构固定——NPC档案今天有3个明天有30个,但每个NPC都长 {uid, 姓名, 好感度, …}——这叫字典容器。

这两个一旦不分清楚,AI会在 /世界/势力格局 底下写乱七八糟的东西。别问我怎么知道的。


世界书条目的套路

变量树搭好了,问题变成:怎么让AI知道它存在并且会用?

世界书条目就是干这个的。它们是一些嵌入在角色卡里的指令片段,每回合混进prompt喂给AI。

基本就四种:

第一种:初始变量。 禁用状态。只用来在卡加载时初始化变量树。如果它不小心触发了,你玩到一半的角色会被重置到1级。别问我为什么知道要把它禁用。

第二种:当前变量。 用EJS模板把stat_data渲染成JSON,插在prompt里让AI"看到"当前世界状态。

@@private
<%_
const stat = variables?.stat_data || {};
const keep = {
主角: { 姓名: stat.主角?.姓名, 境界: stat.修炼?.境界 },
NPC档案: stat.NPC档案 || {},
记忆: { 摘要: stat.记忆?.摘要 || [] }
};
_%>
<status_current_variables>
<%- JSON.stringify(keep, null, 2) %>
</status_current_variables>

这段代码运行后,AI看到的东西多了一层JSON。但用户看不到——有个正则会在AI输出后把这坨JSON静默移除。

AI看到的世界和用户看到的世界不一样。这个事实贯穿了整个项目。

第三种:更新规则。 告诉AI怎么改JSON。路径怎么写,UID怎么用,哪些操作允许哪些禁止。

<UpdateVariable>
<JSONPatch>
[{"op": "replace", "path": "/修炼/境界", "value": "筑基"}]
</JSONPatch>
</UpdateVariable>

禁止:
– 把JSON放进markdown代码块
– 用姓名做动态key
– 整对象覆盖中间对象(会吞字段)

每一条禁止项都对应一个AI真实犯过的错误。真的。

第四种:剧情指引。 这个最轻松。就是告诉AI这个世界是什么样的。

必须写:
– 资源的现实压力
– 修士的谨慎与试探
– 机缘背后的代价

禁止写:
– 主角无代价开挂
– 宗门无脑跪舔
– 好感度高=自动服从
– 堕落值高=人格崩坏

我后来发现一个规律:光说"应该写什么",AI会当耳旁风。同时说"禁止写什么",效果翻倍。可能是因为AI被训练得太注重"不要做你不被允许做的事"了。


正则才是真正的魔术

正则在MVU卡里不是"搜索替换工具",它是一个两个世界之间的接口。

AI看到的东西和用户看到的东西不一样。正则是那个让两边自洽的胶水。

隐藏类

最简单但最核心。

{
"findRegex": "/<status_current_variables>[\\\\s\\\\S]*?<\\\\/status_current_variables>/gm",
"replaceString": "",
"placement": [2]
}

AI能看到完整JSON。用户看到的是剧情。各自安好。

同样的做法用在变量更新标签(<UpdateVariable>)和选择栏标签(<选择>)上。AI每轮写,正则每轮删。不删的话用户会看到满屏的XML标签在对话里飘。

美化类

选择栏原本是原始的XML——能读但不好看。我用正则把它替换成一整套带CSS的HTML面板:深色半透明背景,金色边框,七种立场各有配色(合理/刚正/仁善/中庸/叛逆/唯我/视角切换)。

每个选项是一个小卡片,有数字编号、立场标签、行动描述。一眼能看出不同路线的区别。

做法就是提取7个选项文本和1个行动预估文本,拼进一段写好的HTML+CSS模板里。没什么高深的,就是正则有坑——我踩了。

我后来加了个兜底正则。 因为AI偶尔会输出格式不对的东西——七个选项变成六个,或者漏了标签。兜底正则匹配得更宽松一点,至少保证用户不会看到裸露的XML代码。

这个思路是从React Error Boundary偷的——与其让用户看到崩溃的白屏,不如优雅地降级。

注入类

这个最炸裂。

有个占位符叫 <StatusPlaceHolderImpl/>。正则会匹配到它,然后替换为:

<script>
// 一段完整的交互式前端
// 读取当前对话中的变量数据
// 渲染一个状态面板——名字、境界、修为、灵石、伤势
// 每轮更新
</script>

等一下,正则怎么执行JavaScript?

它不执行。它把<script>标签注入到页面上。页面渲染的时候,脚本就跑起来了——你在普通网页里写内联脚本也这样。

同样的套路用在开局向导:<GenesisPanelImpl/> 被替换成一个完整的角色创建表单。玩家填好了点击发送,生成 【开始游戏】姓名:xxx;…… 文本。AI看到文本,按世界书里的建档规则创建角色。

这里有个关键的设计决策:前端不直接写变量。 它只生成文本。AI看到文本后再去写stat_data。

这样前端和AI的规则是解耦的。你换了前端,只要输出格式不变,AI那边不用动。


v0.7修的那个bug是真有意思

从v0.1到v0.6我一直在加功能。到v0.7的时候出了个诡异的bug:

状态栏显示"未命名"、“未建档”。

但我检查了stat_data——主角.姓名 = "萧天",数据是对的。状态栏为什么读不到?

查了两小时。结果发现是酒馆不同版本的处理方式不同——有些版本在 /stat_data/主角/姓名 写,有些在 /主角/姓名 写。AI写的时候路径漂移了,状态栏读镜像的时候就乱了。状态栏看到的主角可能是外层空壳 { 姓名: "" },不是stat_data里的真实数据。

修复方案:

  • 所有JSONPatch如果以/stat_data/开头,自动去掉前缀
  • 如果外层有数据而stat_data里没有,合并到stat_data
  • 状态栏不读"第一个数据源",读"所有数据源里评分最高的"——有名字的加分,有境界的加分,分数高的用来渲染
  • 第3点我最得意。状态栏维护一个"变量快照池",每个快照算完整度分数,选分最高的那个。

    这让我意识到一个道理:当你控制不了外部系统的行为时,你需要的不是更精确的写入,而是更容错的读取。 酒馆版本升级你是不知道的。你管不了它怎么写。但你可以让你的读取多一层保险。

    修复完v0.7,"未命名"再也没出现过。


    装配脚本的诞生

    到v0.3的时候我已经受不了了。

    每次改一个部件——比如修了一个世界书条目——我就要:

  • 手动打开角色卡JSON
  • 找到对应的条目
  • 替换内容
  • 更新版本号
  • 检查有没有漏同步的字段
  • 大概第五次的时候我写了个Node.js脚本。

    const books = ["世界书.json", "跟随规则.json", "映射规则.json"].map(readJson);
    let entries = [];
    for (const b of books) {
    entries = entries.concat(convertEntries(b, entries.length + 1));
    }

    脚本把所有部件(世界书、脚本、正则)读进来,拼成一张完整的角色卡JSON。然后另一个脚本把它塞进PNG。

    验证脚本也一样——跑一次检查所有约束,保证你没漏掉什么。

    每次迭代完跑一次。世界书改了但忘了升版本号?脚本抓得到。装配脚本改了格式漏了字段?脚本也抓得到。


    版本迭代的节奏

    v0.1 搭骨架(变量树、Schema)
    v0.2 长血肉(前端、NPC跟随)
    v0.3 修bug
    v0.4 上保险
    v0.5 加交互(选项框架)
    v0.6 扩内容
    v0.7 加固核心

    每个版本只改一件事。加功能和修bug不放在一起。如果v0.5既加了选项框架又修了NPC跟随,出了问题你找都找不到。

    v0.7没加任何新功能,没加任何新内容。只修了一个bug。但修完之后整张卡稳定了很多。

    给修bug留一个单独的版本周期。不要总想着"顺便修一下"。


    一些不知道该放哪的话

    写这篇东西的时候我在想一件事。

    如果你刚接触制卡,不要从变量树开始。先写一个最简单的隐藏正则——匹配什么然后替换为空——跑通它。看到效果。获得正向反馈。然后写EJS读取变量条目。然后写更新规则。变量树放在第二步,不是第一步。第一步是"跑通一个能工作的最小系统",哪怕它只能存一个名字。

    最后一个事情。

    我花了一天做了37个文件一张PNG。如果问我值不值得,我只能说:做这件事的时候我进入了心流状态。我在解决一个个具体的问题,不是在做"任务"。当你做的事情不感觉像在工作的时候——其实也不用问值不值得了。


    赞(0)
    未经允许不得转载:171主机测评 » 整了张修仙角色卡,能说的不能说的都在这了
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址