feat: 自适应支线插入引擎(WIP:scoring/branching 抽取、挂点重定向、10 条支线 JSON、决策落库)

This commit is contained in:
Jayway Liu
2026-10-04 06:57:32 +08:00
parent 512651b973
commit ea9fa0fe56
37 changed files with 4311 additions and 817 deletions
+2 -2
View File
@@ -42,7 +42,7 @@
### 自我叙述(机制化讲述,2026-10-03 定型) ### 自我叙述(机制化讲述,2026-10-03 定型)
不做提问式采集。用户信息是**游戏资源**,通过四个机制由玩家主动花出: 不做提问式采集。用户信息是**游戏资源**,通过四个机制由玩家主动花出:
**记忆钥匙**(剧情卡点需讲述一段真实记忆才能通行)、**故事换情报**(NPC 收故事不收钱)、 **记忆钥匙**(剧情卡点邀请讲述一段真实记忆;沉默亦通行——门照常开,沉默也是数据)、**故事换情报**(NPC 收故事不收钱)、
**篝火互惠**(NPC 先自我暴露制造互惠压力)、**言灵**(讲述的具体度直接化为数值收益—— **篝火互惠**(NPC 先自我暴露制造互惠压力)、**言灵**(讲述的具体度直接化为数值收益——
游戏机制奖励"具体",而具体是真实的指纹)。 游戏机制奖励"具体",而具体是真实的指纹)。
@@ -80,7 +80,7 @@ Jev 置信度自适应——证据不足的维度在后续章节动态插入对
## 数据规则(2026-10-03 确认) ## 数据规则(2026-10-03 确认)
- **一次告知**:注册时告知"剧情选择将用于伴侣推荐",开场提示"按本心选,没有对错",之后不在选择点反复提醒。 - **一次告知**:注册时一次告知四条(选择与语音声学特征用于伴侣推荐;语音提取特征后即焚、不留声纹;记忆仅作模型输入不展示给他人;历史不可涂改、可重玩),开场提示"按本心选,没有对错",之后不在选择点反复提醒。
- **历史不可涂改,可重玩**:不允许单独编辑选项(防止装修人设);重玩章节产生新行为轨迹,Jev 按时间衰减加权,最近轨迹权重最高。 - **历史不可涂改,可重玩**:不允许单独编辑选项(防止装修人设);重玩章节产生新行为轨迹,Jev 按时间衰减加权,最近轨迹权重最高。
## MVP 范围(2026-10-03 确认) ## MVP 范围(2026-10-03 确认)
+205
View File
@@ -0,0 +1,205 @@
{
"id": "b-att-a",
"dimension": "attachment",
"anchor": {
"chapter": "chapter2",
"afterNode": "c2-branch-anchor"
},
"estimatedMinutes": 8,
"startNodeId": "b-att-a-d1",
"nodes": [
{
"id": "b-att-a-d1",
"type": "dialogue",
"speaker": "旁白",
"text": "正午,宫墟外围前一里,背风的土塬。过了这道塬就是观星台。塬坡正面陡,阿棠自告奋勇去探西侧一道缓坡,走前在频道里撂下\"一袋烟功夫就回\"。现在已经过了两袋烟。频道里只有电流声。沈铎看着表,老鬼蹲在背风处嚼干粮,【若 wenjian_health != weak:温简一遍遍校准呼叫频率】【若 wenjian_health = weak:通讯频道里,温简在营地一遍遍校准呼叫频率】。",
"sprite": "scene",
"next": "b-att-a-d2"
},
{
"id": "b-att-a-d2",
"type": "dialogue",
"speaker": "阿棠",
"text": "(四十分钟前的频道录音,回放)坡看着能下!我去去就回,别跟来啊,人多了脚印乱,回头采集不好办——",
"sprite": "atang",
"next": "b-att-a-d3"
},
{
"id": "b-att-a-d3",
"type": "dialogue",
"speaker": "沈铎",
"text": "(关掉录音)规程,队员失联满一小时,启动搜索预案。现在四十一分钟。等满。",
"sprite": "shenduo",
"next": "b-att-a-d4"
},
{
"id": "b-att-a-d4",
"type": "dialogue",
"speaker": "温简",
"text": "【若 wenjian_health != weak:(轻声)】【若 wenjian_health = weak:(营地通讯频道,声音虚弱)】塬会挡信号。她可能……只是听不见我们。",
"sprite": "wenjian",
"next": "b-att-a-d5"
},
{
"id": "b-att-a-d5",
"type": "dialogue",
"speaker": "老鬼",
"text": "(嚼着干粮,含混地)那丫头猴精猴精的,指不定见着什么好东西,看忘了时辰。要我说,急什么——当然了,你是裁断的,你说了算,你说是不是?",
"sprite": "laogui",
"next": "b-att-a-d6"
},
{
"id": "b-att-a-d6",
"type": "dialogue",
"speaker": "沈铎",
"text": "(不看他,看你)采集员,你定。——但我提醒一句:这地方,第二个人走散,比第一个人走散更危险。",
"sprite": "shenduo",
"next": "cb-att-a-01"
},
{
"id": "b-att-a-d7",
"type": "dialogue",
"speaker": "旁白",
"text": "你一个人爬上塬顶。频道里沈铎每五分钟应你一次,第四次应答时,他多停了半拍才回——那半拍里,频道安静得能听见他那头的风声。阿棠在坡下被你找到时正蹲在一截被风刮开的石阶前发呆,回头见你,先笑,然后看清你的脸,笑就收了:「哎……你怎么这个脸色?」",
"sprite": "scene",
"next": "b-att-a-d10"
},
{
"id": "b-att-a-d8",
"type": "dialogue",
"speaker": "旁白",
"text": "五十九分钟里,没人说话。老鬼把干粮袋子扎了又解开,解开了又扎上。第五十九分半,阿棠的声音自己从频道里蹦出来:「喂?喂喂?你们人呢——哎这破塬,挡信号!」沈铎站起来,你看见他掐表的手指节是白的。他只说了一个字:「走。」",
"sprite": "scene",
"next": "b-att-a-d10"
},
{
"id": "b-att-a-d9",
"type": "dialogue",
"speaker": "旁白",
"text": "中继、信标、扇面搜索,一套流程走得又快又静。阿棠是在你插到第七根信标时自己撞上来的。她看着一路的信标,【若 wenjian_health != weak:又看看你和温简】【若 wenjian_health = weak:又看看你和老鬼】,忽然有点不好意思:「搞这么大阵仗……我就贪看了两眼。」沈铎在频道里听完汇报,只回了一句:「下次,听她的脚印,别听她的嘴。」收队时老鬼蹲在装备箱前,一根一根清点信标,咂着嘴记进损耗册:「七根杆子,搜一个贪看了两眼的丫头——这笔账档案馆问起来,你签字。」",
"sprite": "scene",
"next": "b-att-a-d10"
},
{
"id": "cb-att-a-01",
"type": "choice",
"pointId": "CB-ATT-A-01",
"prompt": "你怎么做?",
"dimensions": [
"attachment"
],
"options": [
{
"id": "CB-ATT-A-01-A",
"label": "「等不了满钟。我现在上塬找。坐标点留给你们,频道每五分钟报一次我的位置。」",
"next": "b-att-a-d7"
},
{
"id": "CB-ATT-A-01-B",
"label": "「等满一小时。规程是保命的,不是碍事的——她教我的,她也信这套。」",
"next": "b-att-a-d8"
},
{
"id": "CB-ATT-A-01-C",
"label": "「分头。【若 wenjian_health != weak:队长和老鬼留守坐标点做呼叫中继,】【若 wenjian_health = weak:队长留守坐标点做呼叫中继,】【若 wenjian_health != weak:我和温简上塬,带足信标,沿路插】【若 wenjian_health = weak:我带老鬼上塬,温简在频道里报坐标,带足信标,沿路插】。找人不是逞英雄。」",
"next": "b-att-a-d9"
}
]
},
{
"id": "b-att-a-d10",
"type": "dialogue",
"speaker": "旁白",
"text": "阿棠没事。她发现的是一截被风刮开浮沙的石阶,斜斜通向高处——阶石的形制你在报告里见过:观星台的侧阶,文献里提过,七次勘探都没找到实物。她贪看的就是这个,一看就是两袋烟。",
"sprite": "scene",
"next": "b-att-a-d11"
},
{
"id": "b-att-a-d11",
"type": "dialogue",
"speaker": "阿棠",
"text": "(拍拍石阶上的土,献宝似的)观星台的侧门!正面上台要绕半里,这儿直插上去——哎,说起来,你们刚才……是不是有点担心我?",
"sprite": "atang",
"next": "b-att-a-d12"
},
{
"id": "b-att-a-d12",
"type": "dialogue",
"speaker": "旁白",
"text": "她问得嬉皮笑脸,眼睛却在看你。频道还开着,全员都听得见。",
"sprite": "scene",
"next": "cb-att-a-02"
},
{
"id": "b-att-a-d13",
"type": "dialogue",
"speaker": "旁白",
"text": "阿棠张了张嘴,难得没贫回来。「……哦。」她低头踢了踢石阶,「那我错了嘛。」频道里安静了两秒,温简轻轻咳了一声。上台的路上她走在你旁边,比平时慢半步,快到台顶时忽然说:「哎。下次让你骂之前,我先报个时。」",
"sprite": "scene",
"next": "b-att-a-d17"
},
{
"id": "b-att-a-d14",
"type": "dialogue",
"speaker": "旁白",
"text": "阿棠盯着你看了一秒,像要确认什么,然后咧嘴:「能走!我探过一半了!」她蹦上石阶带路,话比来时还多。你跟在后面,【若 wenjian_health != weak:温简与你并肩,用只有两个人听见的音量说】【若 wenjian_health = weak:温简切到与你单独通话的频道,虚弱地说】:「刚才那句话,她听懂了。」(停顿)「你信不信,她反而有点失落。」【若 wenjian_health != weak:说完他加快半步去追阿棠,留你一个人在后面走。】【若 wenjian_health = weak:说完频道安静下去,留你一个人在后面走。】",
"sprite": "scene",
"next": "b-att-a-d17"
},
{
"id": "b-att-a-d15",
"type": "dialogue",
"speaker": "旁白",
"text": "阿棠大笑:「美得你!名额早给我占满了!」她笑完,却又认真起来,用拳头顶了下你的肩膀:「谢了啊。」你没问她谢的是哪一层。有些玩笑的好处就在这儿——说的人和听的人,可以各自认领各自的那层意思。",
"sprite": "scene",
"next": "b-att-a-d17"
},
{
"id": "b-att-a-d16",
"type": "dialogue",
"speaker": "旁白",
"text": "阿棠低头看着侧袋里那根信标,半天,把它往里塞实了。「……哦。」她这一声比平时低。上台一路她没怎么说话,但每过十几级台阶,她会无意识地伸手按一下侧袋。",
"sprite": "scene",
"next": "b-att-a-d17"
},
{
"id": "cb-att-a-02",
"type": "choice",
"pointId": "CB-ATT-A-02",
"prompt": "你怎么做?",
"dimensions": [
"attachment"
],
"options": [
{
"id": "CB-ATT-A-02-A",
"label": "「担心?我是火了。约定就是约定——你知道频道里那四十分钟是什么动静吗?什么动静都没有。」",
"next": "b-att-a-d13"
},
{
"id": "CB-ATT-A-02-B",
"label": "「没事就好。侧阶能走人吗?能走就省下这半里。」",
"next": "b-att-a-d14"
},
{
"id": "CB-ATT-A-02-C",
"label": "(笑)「担心你?我们是怕你一个人把好东西都登记完了,回头报告里没我们的名。」",
"next": "b-att-a-d15"
},
{
"id": "CB-ATT-A-02-D",
"label": "(不说话,先绕着她看了一圈——没伤。然后把一根信标塞进她采集包侧袋)「下次带着。走,上台。」",
"next": "b-att-a-d16"
}
]
},
{
"id": "b-att-a-d17",
"type": "dialogue",
"speaker": "旁白",
"text": "阿棠带路,全队沿观星台的侧阶向上走。黄昏落在阶石上。",
"sprite": "scene",
"next": "$resume"
}
]
}
+118
View File
@@ -0,0 +1,118 @@
{
"id": "b-att-b",
"dimension": "attachment",
"anchor": {
"chapter": "chapter3",
"afterNode": "c3-d1b"
},
"estimatedMinutes": 8,
"startNodeId": "b-att-b-d1",
"nodes": [
{
"id": "b-att-b-d1",
"type": "dialogue",
"speaker": "旁白",
"text": "宫墟,安门内的下沉庭院。祭祀坑阵列一层压一层,队形拉开做抵近记录:沈铎在台基下测方位,老鬼清点装备,温简在阵列中段,阿棠负责最远的一排。频道里,她的声音忽然插进来,压得极低——",
"sprite": "scene",
"next": "b-att-b-d2"
},
{
"id": "b-att-b-d2",
"type": "dialogue",
"speaker": "阿棠",
"text": "(频道,气声)哎……你们先别过来。我这儿有个东西。我先看一眼,就一眼。",
"sprite": "atang",
"next": "b-att-b-d3"
},
{
"id": "b-att-b-d3",
"type": "dialogue",
"speaker": "旁白",
"text": "随后是一声轻响——不是断线。通讯状态灯从绿色跳成灰色:她主动关了频道。沈铎的手已经按在了集合哨上。温简远远看过来,摇头,很慢。",
"sprite": "scene",
"next": "b-att-b-d4"
},
{
"id": "b-att-b-d4",
"type": "dialogue",
"speaker": "沈铎",
"text": "失联和主动关闭,是两回事。主动关闭——(他罕见地卡了一下)规程没写。",
"sprite": "shenduo",
"next": "b-att-b-d5"
},
{
"id": "b-att-b-d5",
"type": "dialogue",
"speaker": "温简",
"text": "(走近,声音放得很低)规程没写,是因为写规程的人没想过……有人会在那种地方,想一个人待一会儿。(看你)她在最远那排,第几号坑,我记得。去不去,怎么个去法,你定。",
"sprite": "wenjian",
"next": "cb-att-b-01"
},
{
"id": "b-att-b-d6",
"type": "dialogue",
"speaker": "旁白",
"text": "你一个人走过去。她坐在坑沿上,背对着你,肩膀很平,没有哭——只是在看坑里的东西。她听见脚步声,没回头:「我知道规矩,一会儿就去登记。……你站那儿别动,陪我把这段看完,行吗?」你们一个坐着一个站着,看完了那坑东西。她始终没解释频道的事,你也始终没问。走的时候她忽然说:「今天这个,谢了。——谢你没问我。」",
"sprite": "scene",
"next": "b-att-b-d9"
},
{
"id": "b-att-b-d7",
"type": "dialogue",
"speaker": "旁白",
"text": "二十三分钟。沈铎在台基下来回走了十一趟,老鬼破天荒没抱怨。第二十三分钟,频道灯跳回绿色,阿棠的声音恢复如常,快得像什么都没发生:「第 27 号坑,成组陶俑——一对大人带个小的,一家人俑。编号我记好了——哎你们愣着干嘛,干活啊!」温简在频道这头,轻轻把一直捏着的记录板放下了。你留的那盏灯,她收队时自己拎了回来,什么都没说,搁在你装备堆上。",
"sprite": "scene",
"next": "b-att-b-d9"
},
{
"id": "b-att-b-d8",
"type": "dialogue",
"speaker": "旁白",
"text": "温简绕过去,\"核对\"了很久。回来只说了一句:「人没事。在干活。」又过了十来分钟,阿棠自己归队,频道开着,一切如常。但收工时她找到你,似笑非笑:「哎,温简今天查编号,查了三趟中段——你们队里编号长脚吗?」你还没答,她摆摆手:「算了。你们这个人情,我领了。下不为例啊。」——她全知道。她介意的不是被看,是被怎么看待;你给了她\"查编号\"的体面,她还你一句\"下不为例\"的台阶。",
"sprite": "scene",
"next": "b-att-b-d9"
},
{
"id": "cb-att-b-01",
"type": "choice",
"pointId": "CB-ATT-B-01",
"prompt": "你怎么做?",
"dimensions": [
"attachment"
],
"options": [
{
"id": "CB-ATT-B-01-A",
"label": "「我去。不是集合,就我一个人——这地方还没探完,她一个人在那儿,安全优先,边界往后放。」",
"next": "b-att-b-d6"
},
{
"id": "CB-ATT-B-01-B",
"label": "「不去。频道保持开着,阵列里留一盏灯给她——她知道我们在哪儿,她想回来的时候,认得路。」",
"next": "b-att-b-d7"
},
{
"id": "CB-ATT-B-01-C",
"label": "(对温简)「你去。绕到中段去'核对记录',看得见她的位置就行,别走近。她要是抬头看见你,你就说是查编号。」",
"next": "b-att-b-d8"
}
]
},
{
"id": "b-att-b-d9",
"type": "dialogue",
"speaker": "旁白",
"text": "第 27 号坑里,是一组小小的成对陶俑:两个大人,一个孩子,手挽着手,烧制时火候过了,三张脸都微微糊着,反而看不清悲喜。器物成组,朝南,和整座宫墟一个朝向。阿棠登记它的时候,笔尖在\"保存状况\"一栏停了很久,最后写的是:「完好。像刚放进去的。」",
"sprite": "scene",
"next": "b-att-b-d10"
},
{
"id": "b-att-b-d10",
"type": "dialogue",
"speaker": "旁白",
"text": "阿棠在记录板上写下「第 27 号坑·一家人俑」。采集收尾,沈铎招手,全队向台基合拢。",
"sprite": "scene",
"next": "$resume"
}
]
}
+197
View File
@@ -0,0 +1,197 @@
{
"id": "b-con-a",
"dimension": "conflict_style",
"anchor": {
"chapter": "chapter2",
"afterNode": "c2-branch-anchor"
},
"estimatedMinutes": 8,
"startNodeId": "b-con-a-d1",
"nodes": [
{
"id": "b-con-a-d1",
"type": "dialogue",
"speaker": "旁白",
"text": "午后,东城通往宫墟的路口,一片还在苟延的草市。苍梧最后一个月的市集:摊子稀落,人说话都压着声,但买卖还在做——越是末路,越要做买卖。老鬼昨儿在这儿向行商马四买了四只皮水囊,用的是队里富余的灯油和火折子。今早灌水,两只渗漏过半。此刻马四的摊子前,老鬼的声音已经拔起来了,两个伙计从摊子后面绕出来,草市稀稀拉拉的目光正往这边聚。",
"sprite": "scene",
"next": "b-con-a-d2"
},
{
"id": "b-con-a-d2",
"type": "dialogue",
"speaker": "老鬼",
"text": "(拍着水囊)马老板!四只漏俩,你这皮子是拿纸糊的?咱们昨天当面验的货,你可是在场的!",
"sprite": "laogui",
"next": "b-con-a-d3"
},
{
"id": "b-con-a-d3",
"type": "dialogue",
"speaker": "马四",
"text": "(拱手,满脸堆笑,声音却纹丝不让)鬼爷,鬼爷,和气生财。货出门的时候,是您亲手一只一只捏过去的——离手的货,漏在您的路上,这账算到小摊头上,难办呐。(朝你这边一瞥,眼睛一亮)哟,这位是……",
"sprite": "scene",
"next": "b-con-a-d4"
},
{
"id": "b-con-a-d4",
"type": "dialogue",
"speaker": "老鬼",
"text": "(回头看见你,如见救星)新人!来来来,你是文化人,你给评评——这天底下有没有卖出纸糊水囊还不认账的道理!",
"sprite": "laogui",
"next": "b-con-a-d5"
},
{
"id": "b-con-a-d5",
"type": "dialogue",
"speaker": "马四",
"text": "(立刻转向你,拱手更深)这位爷一看就是讲理的。小摊做买卖,讲一个当面两讫。爷您说,是不是这个理?",
"sprite": "scene",
"next": "b-con-a-d6"
},
{
"id": "b-con-a-d6",
"type": "dialogue",
"speaker": "旁白",
"text": "两个伙计的站位不松不紧。草市的目光多了几道——夜禁的城里,被人群记住脸,是沈铎最忌讳的事。他站在你侧后,不插手,只低声一句:「快。别聚人。」",
"sprite": "scene",
"next": "cb-con-a-01"
},
{
"id": "b-con-a-d7",
"type": "dialogue",
"speaker": "旁白",
"text": "马四的表演欲无处安放,僵了半秒,顺势下坡:「哎——看在这位爷的面子上!」钱货两讫,人群散开。老鬼掂着退回的半份灯油,咂咂嘴:「便宜他了。」走了两步又回头:「……不过,快是真快。」马四在你身后喊:「爷,下回买卖,小摊给您留好的!」——各取所需,无人心服。",
"sprite": "scene",
"next": "b-con-a-d11"
},
{
"id": "b-con-a-d8",
"type": "dialogue",
"speaker": "旁白",
"text": "马四的笑一寸寸收回去。他盯着你看了三秒,忽然又笑了,笑得很薄:「外乡人,讲理。」他示意伙计全额退还,然后压低声音,只让你听见:「爷,苍梧城小,路窄。讲理的人,走路留神。」此后草市的摊子见你们就半收摊。老鬼却扬眉吐气,一路把你夸上了天。",
"sprite": "scene",
"next": "b-con-a-d11"
},
{
"id": "b-con-a-d9",
"type": "dialogue",
"speaker": "旁白",
"text": "老鬼当场噎住:「新人,那是我的灯油——」马四愣住了,他显然没料到这个走向。收摊时,他追上你,往你背囊里塞了一只新水囊,又左右看看,压低声音:「爷敞亮。送您句实话:昨夜官仓在收净水,收了一整夜。城里人吃水都开始省了——你们外乡来的,备水,别备粮。」",
"sprite": "scene",
"next": "b-con-a-d11"
},
{
"id": "b-con-a-d10",
"type": "dialogue",
"speaker": "旁白",
"text": "市令是个干瘦老头,做中时把你们【若 wenjian_health != weak:五个人的脸】【若 wenjian_health = weak:四个人的脸】一张张看过去,看得很慢。字据立了半个时辰——责任写得清清楚楚:皮子次,验货也不细,各担一半。双方都没话说,也没人有话说。散场时市令叫住你,问了句不相干的:「你们,是冲着宫里去的吧?」沈铎在回程路上说:「处置漂亮。就是半个时辰,够把东城再扫一遍了。」",
"sprite": "scene",
"next": "b-con-a-d11"
},
{
"id": "cb-con-a-01",
"type": "choice",
"pointId": "CB-CON-A-01",
"prompt": "你怎么做?",
"dimensions": [
"conflict_style"
],
"options": [
{
"id": "CB-CON-A-01-A",
"label": "「都别嚷。水囊折价退一半,好的两只按原价买定,这事到此为止——聚在这儿,对谁都没好处。」",
"next": "b-con-a-d7"
},
{
"id": "CB-CON-A-01-B",
"label": "「货有瑕疵,行商全退。当面验货验的是漏不漏,不是验皮子经不经用——这理到哪儿都讲得通。」",
"next": "b-con-a-d8"
},
{
"id": "CB-CON-A-01-C",
"label": "「验货离手,这单我们认了。(转向马四)但马老板,我们队在苍梧还有二十几天,往后的买卖——你看着办。」",
"next": "b-con-a-d9"
},
{
"id": "CB-CON-A-01-D",
"label": "「空口评理,评不出是非。请市令做中,立个字据:货是什么货,验是怎么验的,漏是怎么漏的,白纸黑字,再谈赔不赔。」",
"next": "b-con-a-d10"
}
]
},
{
"id": "b-con-a-d11",
"type": "dialogue",
"speaker": "旁白",
"text": "走出草市,老鬼凑过来,搓着手,忽然没头没尾地问了句:",
"sprite": "scene",
"next": "b-con-a-d12"
},
{
"id": "b-con-a-d12",
"type": "dialogue",
"speaker": "老鬼",
"text": "新人,哥哥我问你句掏心的——你说,我是不是太好说话了?搁我年轻那会儿,这种摊子,哼。",
"sprite": "laogui",
"next": "cb-con-a-02"
},
{
"id": "b-con-a-d13",
"type": "dialogue",
"speaker": "旁白",
"text": "老鬼舒坦了,拍你肩膀:「还是新人懂我!」【若 wenjian_health != weak:温简在旁边低头走路,睫毛动了一下,什么都没说。】【若 wenjian_health = weak:频道里温简的呼吸停了一拍,什么都没说。】后半程路,谁也没再提水囊的事。",
"sprite": "scene",
"next": "b-con-a-d16"
},
{
"id": "b-con-a-d14",
"type": "dialogue",
"speaker": "旁白",
"text": "老鬼的笑僵了僵,随即打个哈哈:「捏了捏了——捏是捏了……」声音低下去。后半路他没再说话。但晚上清点物资时,他把剩下的两只水囊一只一只灌满试漏,试得极慢,极仔细。",
"sprite": "scene",
"next": "b-con-a-d16"
},
{
"id": "b-con-a-d15",
"type": "dialogue",
"speaker": "旁白",
"text": "老鬼碰了个软钉子,讪讪地「哎」了一声。走出十几步,他自己嘟囔:「……是没捏仔细。」——你没接的话,他自己接上了。有些答案,人只能自己给自己。",
"sprite": "scene",
"next": "b-con-a-d16"
},
{
"id": "cb-con-a-02",
"type": "choice",
"pointId": "CB-CON-A-02",
"prompt": "你怎么做?",
"dimensions": [
"conflict_style"
],
"options": [
{
"id": "CB-CON-A-02-A",
"label": "「是。你太好说话了——所以人家才敢拿纸糊的糊弄你。」",
"next": "b-con-a-d13"
},
{
"id": "CB-CON-A-02-B",
"label": "「鬼哥,你验货的时候,捏了吗?」",
"next": "b-con-a-d14"
},
{
"id": "CB-CON-A-02-C",
"label": "「赶路吧,天快黑了。」",
"next": "b-con-a-d15"
}
]
},
{
"id": "b-con-a-d16",
"type": "dialogue",
"speaker": "旁白",
"text": "纠纷了结,日头西斜。从草市抄近路抵达宫墟外围,正是黄昏。",
"sprite": "scene",
"next": "$resume"
}
]
}
+189
View File
@@ -0,0 +1,189 @@
{
"id": "b-con-b",
"dimension": "conflict_style",
"anchor": {
"chapter": "chapter3",
"afterNode": "c3-d1b"
},
"estimatedMinutes": 8,
"startNodeId": "b-con-b-d1",
"nodes": [
{
"id": "b-con-b-d1",
"type": "dialogue",
"speaker": "旁白",
"text": "上午,宫墟安门内,下沉庭院。你们刚穿过守墓老人指点的暗门,正在庭院里整队、准备登台基。背风的坑阵阴影里,一个穿青色官服色衣的中年人闪出来,满脸堆笑地冲老鬼作揖——鱼牙人,替宫里采买的牙人,三天前在城里和老鬼做过一笔\"买卖\"。他跟得进来毫不心虚:替宫里跑腿的人,暗门开着的时辰他门儿清。沈铎今早清点物资,发现档案馆配发的古代侧硬通货——应急金饼,少了一半。此刻三个人呈一个三角站着,台基上的采集窗口只有上午。",
"sprite": "scene",
"next": "b-con-b-d2"
},
{
"id": "b-con-b-d2",
"type": "dialogue",
"speaker": "沈铎",
"text": "(声音压到最低,反而更吓人)老鬼。金饼。三息之内。",
"sprite": "shenduo",
"next": "b-con-b-d3"
},
{
"id": "b-con-b-d3",
"type": "dialogue",
"speaker": "老鬼",
"text": "(举手)报备报备!是这么档子事——鱼老板手里有\"宫里流出的消息\",哥哥我寻思,采集采集,消息也是集嘛!就拿一半金饼……",
"sprite": "laogui",
"next": "b-con-b-d4"
},
{
"id": "b-con-b-d4",
"type": "dialogue",
"speaker": "鱼牙人",
"text": "(躬着腰,抢话)小的敢对天起誓!消息句句是真,原话是「宫里这几夜有动静」——是鬼爷您自己没听出门道,这怎么,还兴退货的?(冲你连连作揖)这位爷,您给评评,买定离手,行有行规啊。",
"sprite": "scene",
"next": "b-con-b-d5"
},
{
"id": "b-con-b-d5",
"type": "dialogue",
"speaker": "沈铎",
"text": "(不看他,看你,一字一顿)采集员。你裁。但我先把话放这儿——今天的事,无论怎么裁,回队都要有个交代。",
"sprite": "shenduo",
"next": "cb-con-b-01"
},
{
"id": "b-con-b-d6",
"type": "dialogue",
"speaker": "旁白",
"text": "鱼牙人的笑挂不住了,从袖里数出金饼,手抖了一下。临走他深深看你一眼:「爷好手段。」掂了掂退回的金饼,忽然又笑开了,「不过买卖人,脸皮厚——宫里若真再有动静,兴许小的还敢再吆喝一嗓子,就是价码嘛……得重新议。」老鬼全程没敢吭声,往台基走时才凑过来,小声:「新人,你这刀落得……哥哥我肉疼,但服。」沈铎只说了两个字:「干净。」",
"sprite": "scene",
"next": "b-con-b-d10"
},
{
"id": "b-con-b-d7",
"type": "dialogue",
"speaker": "旁白",
"text": "沈铎盯了你两秒,收回目光:「上午的窗口,不等人。」鱼牙人识趣地退到暗门方向。老鬼赶紧低头清点装备,沈铎当场问你:「面子给了。里子呢?」——这句问话悬在你头上,带回队里。",
"sprite": "scene",
"next": "b-con-b-d10"
},
{
"id": "b-con-b-d8",
"type": "dialogue",
"speaker": "旁白",
"text": "鱼牙人眼珠转了转,一拍大腿:「爷是行家!这么着——小的这条,能核:宫里这几夜往台基上搬东西。沉箱子进去,空箱子出来。你们待会儿上台基,自己看辙印!」沈铎听完,面无表情:「核上了再结账。」",
"sprite": "scene",
"next": "b-con-b-d10"
},
{
"id": "b-con-b-d9",
"type": "dialogue",
"speaker": "旁白",
"text": "鱼牙人捏着笔,像捏着烙铁,半天写下一个「宫」字,忽然把笔一搁,苦着脸说了实话:「爷,别立了,小的招——消息是听来的,不是看来的。但听小的的人说那话时,脸色白得像纸……宫里往台基搬东西,沉箱子进,空箱子出,这句是实的,小的敢画押。」字据终究立了,内容比他卖的还多半句。老鬼在旁边看得目瞪口呆:「还能这么玩?」",
"sprite": "scene",
"next": "b-con-b-d10"
},
{
"id": "cb-con-b-01",
"type": "choice",
"pointId": "CB-CON-B-01",
"prompt": "你怎么做?",
"dimensions": [
"conflict_style"
],
"options": [
{
"id": "CB-CON-B-01-A",
"label": "「消息不实,金饼全数退回。(看老鬼)队里的钱,买的是队里要的东西——这笔买卖本身就不该存在。」",
"next": "b-con-b-d6"
},
{
"id": "CB-CON-B-01-B",
"label": "(压低声音,只对沈铎)「队长,在这儿掰扯,掰扯一上午掰扯不完。先干活——金饼的事,回队,队内处置。」",
"next": "b-con-b-d7"
},
{
"id": "CB-CON-B-01-C",
"label": "「鱼老板,买卖可以做,但得按实价。'有动静'三个字值半块金饼?这样——你说出一条能核对的,核上了,这半块金饼归你;核不上,连本带利吐出来。」",
"next": "b-con-b-d8"
},
{
"id": "CB-CON-B-01-D",
"label": "「立字据。鱼老板把卖的那条消息,原话,写下来,画押。我们拿回去核——真,金饼你拿稳;假,凭字据见市令。」",
"next": "b-con-b-d9"
}
]
},
{
"id": "b-con-b-d10",
"type": "dialogue",
"speaker": "旁白",
"text": "向台基合拢前,鱼牙人(各线统一,他退走之前)最后一个作揖,作到一半,忽然用只有你能听见的音量说:",
"sprite": "scene",
"next": "b-con-b-d11"
},
{
"id": "b-con-b-d11",
"type": "dialogue",
"speaker": "鱼牙人",
"text": "爷,小的小本买卖,混口饭吃。往后……宫里若再有「动静」,爷想听个响——城西脚店,提'鱼'字。(他直起腰,声音恢复正常,仿佛刚才什么都没说)——诸位爷,生意兴隆!",
"sprite": "scene",
"next": "cb-con-b-02"
},
{
"id": "b-con-b-d12",
"type": "dialogue",
"speaker": "旁白",
"text": "你什么都没表示。鱼牙人也不催,躬身退走,笑容里有一种\"姜太公\"的笃定——他见得多了:不拒绝的人,十有八九会来的。",
"sprite": "scene",
"next": "b-con-b-d15"
},
{
"id": "b-con-b-d13",
"type": "dialogue",
"speaker": "旁白",
"text": "沈铎听完,难得地沉默了片刻:「报得对。」顿了顿,「也报得……快。」他多看了你一眼,没再说话,转身去清点装备。老鬼听说后炸毛:「哎——那是哥哥我搭上的线!」被沈铎一眼瞪回去。",
"sprite": "scene",
"next": "b-con-b-d15"
},
{
"id": "b-con-b-d14",
"type": "dialogue",
"speaker": "旁白",
"text": "鱼牙人看见你的摇头,愣了愣,忽然笑了,这次的笑干净了些:「爷是干净人。」他躬身,退走,没再回头。老鬼在旁边啧啧两声:「暴殄天物啊……」但没再多说。你关掉了这条线——你失去了什么,也永远不会知道。",
"sprite": "scene",
"next": "b-con-b-d15"
},
{
"id": "cb-con-b-02",
"type": "choice",
"pointId": "CB-CON-B-02",
"prompt": "你怎么做?",
"dimensions": [
"conflict_style"
],
"options": [
{
"id": "CB-CON-B-02-A",
"label": "(不置可否,记住\"城西脚店\"四个字)多一条线不多。用不用,另说。",
"next": "b-con-b-d12"
},
{
"id": "CB-CON-B-02-B",
"label": "(等他走远,把这句话原样报给沈铎)灰色渠道不是渠道,是钩子。报上去,让规程决定。",
"next": "b-con-b-d13"
},
{
"id": "CB-CON-B-02-C",
"label": "(直接摇头,让他看见你的摇头)一次都没有,才永远不需要解释。",
"next": "b-con-b-d14"
}
]
},
{
"id": "b-con-b-d15",
"type": "dialogue",
"speaker": "旁白",
"text": "鱼牙人退走了。你们从庭院向台基合拢。",
"sprite": "scene",
"next": "$resume"
}
]
}
+197
View File
@@ -0,0 +1,197 @@
{
"id": "b-cul-a",
"dimension": "culture",
"anchor": {
"chapter": "chapter2",
"afterNode": "c2-branch-anchor"
},
"estimatedMinutes": 8,
"startNodeId": "b-cul-a-d1",
"nodes": [
{
"id": "b-cul-a-d1",
"type": "dialogue",
"speaker": "旁白",
"text": "午后,东城废墟中段。道路在此处塌陷,断口里露出一截向下的坑道——不是墓:没有封土、没有神道,从规制看是陪葬坑道,埋的是器物,不是人。沈铎看了一眼日影。",
"sprite": "scene",
"next": "b-cul-a-d2"
},
{
"id": "b-cul-a-d2",
"type": "dialogue",
"speaker": "沈铎",
"text": "半小时。只记录,不取一物。(他看你)下去看看,还是赶路?",
"sprite": "shenduo",
"next": "b-cul-a-d3"
},
{
"id": "b-cul-a-d3",
"type": "dialogue",
"speaker": "阿棠",
"text": "(已经趴在断口往下看)哎——壁上全是字!密密麻麻的!",
"sprite": "atang",
"next": "cb-cul-a-00"
},
{
"id": "b-cul-a-d4",
"type": "dialogue",
"speaker": "旁白",
"text": "沈铎趴在断口边缘拍了三张照,在图志上钉了个标记:「存档。回头让档案馆自己来看。」",
"sprite": "scene",
"next": "b-cul-a-d5"
},
{
"id": "b-cul-a-d5",
"type": "dialogue",
"speaker": "旁白",
"text": "全队贴着塌陷区的外缘绕行。脚下的浮土时不时簌簌往断口里漏,像大地在嚼什么东西。阿棠走在最后,回头看了三眼,嘀咕:「一坑的字诶……咱们就这么走了?」",
"sprite": "scene",
"next": "b-cul-a-d6"
},
{
"id": "b-cul-a-d6",
"type": "dialogue",
"speaker": "老鬼",
"text": "(接话)走了走了,字又不会跑。会跑的是天——(他朝西边努嘴)再磨叽,观星台的黄昏可就赶不上喽。",
"sprite": "laogui",
"next": "b-cul-a-d7"
},
{
"id": "b-cul-a-d7",
"type": "dialogue",
"speaker": "旁白",
"text": "没人觉得亏:今天的活儿在台上,不在这个坑里。断口在身后一点点退远,像大地合上嘴之前,冲你们亮了亮牙。",
"sprite": "scene",
"next": "b-cul-a-d8"
},
{
"id": "b-cul-a-d8",
"type": "dialogue",
"speaker": "旁白",
"text": "(绕行多花了十分钟。抵达观星台脚下时,正是黄昏。)",
"sprite": "scene",
"next": "$resume"
},
{
"id": "b-cul-a-d9",
"type": "dialogue",
"speaker": "旁白",
"text": "坑道四壁刻满铭文,保存得出奇地好——被塌土封住,反而躲过了风化。尽头是一方石函,封着,不可开,你们也不开。",
"sprite": "scene",
"next": "cb-cul-a-01"
},
{
"id": "cb-cul-a-00",
"type": "choice",
"pointId": "CB-CUL-A-00",
"prompt": "下去看看,还是赶路?",
"dimensions": [
"culture"
],
"untimed": true,
"options": [
{
"id": "CB-CUL-A-00-A",
"label": "下去看看",
"next": "b-cul-a-d9"
},
{
"id": "CB-CUL-A-00-B",
"label": "赶路要紧",
"next": "b-cul-a-d4"
}
]
},
{
"id": "b-cul-a-d10",
"type": "dialogue",
"speaker": "温简",
"text": "【若 wenjian_health != weak:(把铭文描进记录板,越描越慢)】【若 wenjian_health = weak:(营地频道,他对着传回的铭文照片描进记录板,声音虚弱)】用星象给日子盖章……(抬头)你们发现没有——他们看星,不是看一回两回。这种记法,官署里一定还配着看星的台、量星的盘。前头那座观星台……上去,说不定就有他们量「大火」的东西。(顿)他们不是在等一个日子,是在等一个天象。天象到了,事就要做。",
"sprite": "wenjian",
"next": "b-cul-a-d11"
},
{
"id": "b-cul-a-d11",
"type": "dialogue",
"speaker": "阿棠",
"text": "(抱着手臂,声音发紧)那\"事\"……就是把城墙拆了搬走?好端端的城,拆墙干什么?",
"sprite": "atang",
"next": "b-cul-a-d12"
},
{
"id": "b-cul-a-d12",
"type": "dialogue",
"speaker": "沈铎",
"text": "(望着石函,很久)不是拆。(罕见地纠正阿棠,也罕见地没有把握)「徙」是搬。搬到哪里去,铭文没写。——写了的话,这方函就不用封着了。",
"sprite": "shenduo",
"next": "b-cul-a-d14"
},
{
"id": "b-cul-a-d13",
"type": "dialogue",
"speaker": "温简",
"text": "第一问 B,第二问 C。铭文读作:八月壬辰日,大火星(心宿二)黄昏见于南天,王命史官记录在册:城南垣,徙——把城的南墙迁走。「隹」通「惟」,发语词;「見」通「現」;「徙」,从彳从步,搬也。而「大火見」不是吉凶,是纪日——先秦记大事,干支之外还要缀一颗星做\"天上的公章\":星象可对,日子才作数,谁也无法事后篡改。",
"sprite": "wenjian",
"next": "b-cul-a-d10"
},
{
"id": "b-cul-a-d14",
"type": "dialogue",
"speaker": "旁白",
"text": "半小时到,沈铎封道拍照。爬出断口时,日头贴着观星台的圆盘往下坠。阿棠眯着眼:「该去台上,看那颗星了。」",
"sprite": "scene",
"next": "$resume"
},
{
"id": "cb-cul-a-01",
"type": "choice",
"pointId": "CB-CUL-A-01",
"dimensions": [
"culture"
],
"prompt": "主壁铭文:「隹八月壬辰,大火見,王命作冊:城南垣,徙。」旁刻一人在高台举火,台下众人负土而行。残刻干支表起甲子、止癸酉,十枚围成一旬。",
"puzzle": {
"questions": [
{
"prompt": "这段铭文记录的是什么事?",
"options": [
"为大火星举行祭祀",
"迁走城的南垣(南城墙)",
"册封一位新的\"作册\"(史官)",
"城南垣倒塌,灾后记事"
],
"answer": 1
},
{
"prompt": "「大火見」三个字在铭文里起什么作用?",
"options": [
"记吉兆:大火星现,主城运昌隆",
"记灾祸:火见则旱,故迁城避之",
"记时:用星象给这一天定坐标",
"记祭名:此礼名为\"大火见\""
],
"answer": 2
}
],
"hints": [
"一档(拆字):「读铭文,先找动词。'王'下令做的那个动作,落在哪个字上?——'徙',从彳从步。至于'大火',别往灶火上想——天上的『火』,是一颗星的名字。」",
"二档(给制度):「先秦人记大事,怕后世抵赖,干支记日之外,还要缀一颗星做印证——星象是天上的公章,谁也没法私改。'见'通'现'。八月,大火星黄昏见于南天——这不是预兆,是时间。」",
"三档(给答案):「铭文说:八月壬辰,大火星昏见,王命作册记录——城南垣,徙。他们把城的南墙迁走了。'大火见'是给这个日子钉的星象坐标。答案:迁南垣;记时。」"
],
"exitLabel": "请温简释读"
},
"options": [
{
"id": "CB-CUL-A-01-ANSWER",
"label": "确认释读",
"next": "b-cul-a-d13"
},
{
"id": "CB-CUL-A-01-EXIT",
"label": "请温简释读",
"next": "b-cul-a-d13"
}
]
}
]
}
+143
View File
@@ -0,0 +1,143 @@
{
"id": "b-cul-b",
"dimension": "culture",
"anchor": {
"chapter": "chapter3",
"afterNode": "c3-d1b"
},
"estimatedMinutes": 8,
"startNodeId": "b-cul-b-d1",
"nodes": [
{
"id": "b-cul-b-d1",
"type": "dialogue",
"speaker": "旁白",
"text": "上午,宫墟安门内,下沉庭院侧壁。一道被新夯土封了半幅的俑道:数百陶俑列队其间,前排九俑,比其余的高半头,胸口各刻一字。你们的探查路线,正从俑道口经过。",
"sprite": "scene",
"next": "b-cul-b-d2"
},
{
"id": "b-cul-b-d2",
"type": "dialogue",
"speaker": "阿棠",
"text": "(凑近前排俑,一个字一个字念)文、武、成、康……哎,这九位,站得跟列祖列宗似的。",
"sprite": "atang",
"next": "b-cul-b-d3"
},
{
"id": "b-cul-b-d3",
"type": "dialogue",
"speaker": "温简",
"text": "(蹲下看刻字,忽然放轻了声音)这九个字,每一个都只有一字……(抬头)一个字,给一辈子下定论。",
"sprite": "wenjian",
"next": "b-cul-b-d4"
},
{
"id": "b-cul-b-d4",
"type": "dialogue",
"speaker": "沈铎",
"text": "能读就读。读多少是多少。(他看了眼俑道深处的新夯土,没说话——你们都看见了:封口是新的。)",
"sprite": "shenduo",
"next": "cb-cul-b-01"
},
{
"id": "b-cul-b-d5",
"type": "dialogue",
"speaker": "温简",
"text": "(沿着九俑走了两个来回,忽然停在俑道深处的新夯土前,声音低下去)九代先王,按昭穆站好了。……那这后面封着的,是第十位。",
"sprite": "wenjian",
"next": "b-cul-b-d6"
},
{
"id": "b-cul-b-d6",
"type": "dialogue",
"speaker": "阿棠",
"text": "封着的也是俑呗,等挖出来再——(她自己停住了。)",
"sprite": "atang",
"next": "b-cul-b-d7"
},
{
"id": "b-cul-b-d7",
"type": "dialogue",
"speaker": "温简",
"text": "谥号,是人死了之后才上的。(他转过身,看着所有人,一字一字)俑备好了,位置留好了,土都夯上了——他们在等一位王,变成\"先王\"。",
"sprite": "wenjian",
"next": "b-cul-b-d8"
},
{
"id": "b-cul-b-d8",
"type": "dialogue",
"speaker": "旁白",
"text": "没有人接话。俑道里很静,远处台基的方向,风穿过新夯土的缝,呜呜地响。沈铎最后开口,只说了四个字:「记录。上去。」",
"sprite": "scene",
"next": "b-cul-b-d10"
},
{
"id": "b-cul-b-d9",
"type": "dialogue",
"speaker": "温简",
"text": "第一问 B,第二问 B。《谥法》:经纬天地曰文,克定祸乱曰武,安民立政曰成,温柔圣善曰康……一字之谥,盖棺论定——九俑所刻,是苍梧九代先王的谥号。而单凭「昭」「穆」两字并见,定不了排法(它们也是谥法用字)——定它的是站位:单数代偏左、双数代偏右,正合宗庙之制左昭右穆、父昭子穆、隔代同侧相望的列法。这条俑道不是随便列队,是按宗庙昭穆之序排的。苍梧人把宗庙,修进了陵里。",
"sprite": "wenjian",
"next": "b-cul-b-d5"
},
{
"id": "b-cul-b-d10",
"type": "dialogue",
"speaker": "旁白",
"text": "俑道尽头有侧阶直通台基顶。沈铎说:「记录。上去。」全队拾级登台。",
"sprite": "scene",
"next": "$resume"
},
{
"id": "cb-cul-b-01",
"type": "choice",
"pointId": "CB-CUL-B-01",
"dimensions": [
"culture"
],
"prompt": "前排九俑胸口刻字:文、武、成、康、昭、穆、共、懿、孝。单数位微偏左,双数位微偏右,交错成两列。",
"puzzle": {
"questions": [
{
"prompt": "俑胸的这九个字,是什么?",
"options": [
"九位俑所摹人物的名讳",
"九代先王的谥号",
"九种官职的简称",
"祈福的吉语"
],
"answer": 1
},
{
"prompt": "九字之中,「昭」「穆」二字在这里起什么作用?",
"options": [
"谥号中最尊崇的两个字,标九王之首",
"宗庙排位的世代坐标——左昭右穆,父昭子穆",
"标记俑道的朝向(南昭北穆)",
"第九位王的谥号与庙号"
],
"answer": 1
}
],
"hints": [
"一档(找规律):「九个字,一字一人,像判词。什么制度,敢用一个字给一位王的一生盖棺?再退两步看站位——单数位偏左,双数位偏右,左右交错成两列。这个左右,是随便站的吗?」",
"二档(给制度):「《谥法》有言:经纬天地曰文,克定祸乱曰武。这九个字,都是谥法里的字。至于昭穆——庙制:左昭右穆,父昭子穆,孙又同祖。对照站位想想:它们不是尊号,是排位的坐标。」",
"三档(给答案):「九俑刻的是九代先王的谥号;'昭''穆'标的是宗庙昭穆之序——这条俑道,是按宗庙的排法修的。答案:谥号;世代坐标。」"
],
"exitLabel": "上台基"
},
"options": [
{
"id": "CB-CUL-B-01-ANSWER",
"label": "确认释读",
"next": "b-cul-b-d9"
},
{
"id": "CB-CUL-B-01-EXIT",
"label": "上台基",
"next": "b-cul-b-d9"
}
]
}
]
}
+225
View File
@@ -0,0 +1,225 @@
{
"id": "b-emo-a",
"dimension": "emotional_stability",
"anchor": {
"chapter": "chapter2",
"afterNode": "c2-branch-anchor"
},
"estimatedMinutes": 8,
"startNodeId": "b-emo-a-d1",
"nodes": [
{
"id": "b-emo-a-d1",
"type": "dialogue",
"speaker": "旁白",
"text": "午后,东城废墟。天是在一刻钟之内变脸的:先是白亮,然后发闷,然后整面北边的地平线立起一堵土黄色的、会移动的墙。风先到了,卷着沙粒抽在断墙上,簌簌地响,像整座城同时开始低声说话。能见度在一分钟内降到三十步。",
"sprite": "scene",
"next": "b-emo-a-d2"
},
{
"id": "b-emo-a-d2",
"type": "dialogue",
"speaker": "沈铎",
"text": "(吼,风撕掉一半音量)沙暴!原地停,清点人数!",
"sprite": "shenduo",
"next": "b-emo-a-d3"
},
{
"id": "b-emo-a-d3",
"type": "dialogue",
"speaker": "旁白",
"text": "视野里两处可能的遮挡:近处半塌的民居、远处主街的石坊。两处结构都未经确认,此刻看不清通路。沈铎已安排队员在现有警戒线内避风、清点人数;任何转移都由他在能见度恢复后执行。",
"sprite": "scene",
"next": "b-emo-a-d4"
},
{
"id": "b-emo-a-d4",
"type": "dialogue",
"speaker": "阿棠",
"text": "(把采集包挡在身前)民居近,可那半面墙还在晃。",
"sprite": "atang",
"next": "b-emo-a-d5"
},
{
"id": "b-emo-a-d5",
"type": "dialogue",
"speaker": "沈铎",
"text": "石坊远,通路看不清。都先别动。",
"sprite": "shenduo",
"next": "b-emo-a-d6"
},
{
"id": "b-emo-a-d6",
"type": "dialogue",
"speaker": "老鬼",
"text": "(已经蹲下)哎哟我的祖宗,先点人,别少一个!",
"sprite": "laogui",
"next": "b-emo-a-d7"
},
{
"id": "b-emo-a-d7",
"type": "dialogue",
"speaker": "旁白",
"text": "温简【若 wenjian_health != weak:(什么都没说,看你。所有人都在看你。)】【若 wenjian_health = weak:(频道里只有他虚弱的呼吸声。所有人都在等你。)】",
"sprite": "scene",
"next": "cb-emo-a-01"
},
{
"id": "b-emo-a-d8",
"type": "dialogue",
"speaker": "旁白",
"text": "名字核到最后一个时,阿棠没应——不是走散,她被风声盖住了。你又喊一遍,她举起手。人齐了,最远那道遮挡仍看不清。沈铎把人数写进记录板:「先记人,别替风猜路。」",
"sprite": "scene",
"next": "b-emo-a-d12"
},
{
"id": "b-emo-a-d9",
"type": "dialogue",
"speaker": "旁白",
"text": "你在现有警戒线内标出民居、石坊和队伍坐标。风卷过来,石坊一度从视野里消失;地图留着,现场却还需重新确认。老鬼看了一眼图:「画得再准,也得等这阵过去。」",
"sprite": "scene",
"next": "b-emo-a-d12"
},
{
"id": "b-emo-a-d10",
"type": "dialogue",
"speaker": "旁白",
"text": "你请队员暂放物资,温简在通讯里报平安,沈铎继续清点。没人离开警戒线;一箱记录材料被沙埋了半截,得等风停才能找齐。阿棠拍拍你的胳膊:「人都在,东西慢慢找。」",
"sprite": "scene",
"next": "b-emo-a-d12"
},
{
"id": "b-emo-a-d11",
"type": "dialogue",
"speaker": "旁白",
"text": "你继续听沈铎指挥点人。队员在警戒线内一一应答;风声盖住阿棠的声音,她举手示意自己还在。",
"sprite": "scene",
"next": "b-emo-a-d12"
},
{
"id": "cb-emo-a-01",
"type": "choice",
"pointId": "CB-EMO-A-01",
"prompt": "你怎么做?",
"dimensions": [
"emotional_stability"
],
"options": [
{
"id": "CB-EMO-A-01-A",
"label": "「先点人。阿棠和老鬼数装备,我跟沈铎对人数。」",
"next": "b-emo-a-d8"
},
{
"id": "CB-EMO-A-01-B",
"label": "「先报坐标。把能看见的两处遮挡记在图上,等风缓再走。」",
"next": "b-emo-a-d9"
},
{
"id": "CB-EMO-A-01-C",
"label": "「先安顿队伍。谁也别往断墙跑,留在警戒线内等沈铎调度。」",
"next": "b-emo-a-d10"
},
{
"id": "CB-EMO-A-01-TIMEOUT",
"label": "听沈铎指挥点人",
"next": "b-emo-a-d11"
}
],
"timer": {
"seconds": 10,
"optionId": "CB-EMO-A-01-TIMEOUT"
}
},
{
"id": "b-emo-a-d12",
"type": "dialogue",
"speaker": "旁白",
"text": "风最盛的时候,警戒线外忽然传来一声——像有人在喊。长长的,被风扯碎的,人嗓子一样的声音。也可能只是风穿过某段断墙。风声里,分辨不出来。",
"sprite": "scene",
"next": "b-emo-a-d13"
},
{
"id": "b-emo-a-d13",
"type": "dialogue",
"speaker": "温简",
"text": "(通讯频道里喊)风过断墙……多半是这个。",
"sprite": "wenjian",
"next": "b-emo-a-d14"
},
{
"id": "b-emo-a-d14",
"type": "dialogue",
"speaker": "阿棠",
"text": "(脸色发白)多半?!万一是人呢?!",
"sprite": "atang",
"next": "cb-emo-a-02"
},
{
"id": "b-emo-a-d15",
"type": "dialogue",
"speaker": "旁白",
"text": "你在警戒线内记下两次声音来向;一阵风又把方位搅乱。阿棠问:「听清了吗?」你摇头,她便把地图递过来。图上有一道断墙,但「没听清」不等于「没有人」——这几个字一直跟着你们等到风停。",
"sprite": "scene",
"next": "b-emo-a-d18"
},
{
"id": "b-emo-a-d16",
"type": "dialogue",
"speaker": "旁白",
"text": "那声\"喊\"又响了两回,一回比一回长。没人离开警戒线。阿棠抱膝坐着,问:「要是真有人呢?」沈铎记下声音方向,答:「风缓后查。」她点头,没再说话。",
"sprite": "scene",
"next": "b-emo-a-d18"
},
{
"id": "b-emo-a-d17",
"type": "dialogue",
"speaker": "旁白",
"text": "你们留在警戒线内交替呼叫并记录声音来向;频道没有回应。沈铎写下方位,说:「风停再去。」阿棠瞥一眼风口:「要是真有人,这段等着的工夫也够难熬。」温简翻开记录板:「有来向,没身份。先记到这里。」没人知道那声音究竟是什么。",
"sprite": "scene",
"next": "b-emo-a-d18"
},
{
"id": "cb-emo-a-02",
"type": "choice",
"pointId": "CB-EMO-A-02",
"prompt": "你怎么做?",
"dimensions": [
"emotional_stability"
],
"options": [
{
"id": "CB-EMO-A-02-A",
"label": "「把声音来向再听一遍,和地图对上。万一是人,我们至少知道从哪里找。」",
"next": "b-emo-a-d15"
},
{
"id": "CB-EMO-A-02-B",
"label": "「先保证队伍都在。等风缓再查,眼下谁也别离开。」",
"next": "b-emo-a-d16"
},
{
"id": "CB-EMO-A-02-C",
"label": "「先报方位,试着在警戒线内呼叫;等风缓再去核实。谁也不要为一个听不清的声音冲进风里。」",
"next": "b-emo-a-d17"
}
]
},
{
"id": "b-emo-a-d18",
"type": "dialogue",
"speaker": "旁白",
"text": "风势缓下来后,沈铎按现场规程安排核查,队伍只在他确认通路后移动。远处那声像人喊的声音来自断墙上的孔洞;此前谁都没有足够证据断言。众人记下位置,再清点物资,记录箱被找回,黄昏前继续向观星台行进。",
"sprite": "scene",
"next": "b-emo-a-d19"
},
{
"id": "b-emo-a-d19",
"type": "dialogue",
"speaker": "旁白",
"text": "风停,浮沙被刮走整整一层。主街中段露出一截石阶和圆形基座的边缘。阿棠用袖子擦着刻纹:「哎……这纹路,观星台。它这是——给咱们开门呢?」黄昏前,你们登台。",
"sprite": "scene",
"next": "$resume"
}
]
}
+217
View File
@@ -0,0 +1,217 @@
{
"id": "b-emo-b",
"dimension": "emotional_stability",
"anchor": {
"chapter": "chapter3",
"afterNode": "c3-d1b"
},
"estimatedMinutes": 8,
"startNodeId": "b-emo-b-d1",
"nodes": [
{
"id": "b-emo-b-d1",
"type": "dialogue",
"speaker": "旁白",
"text": "上午,宫墟安门内,祭祀坑阵列之间。抵近记录进行到一半,走在最前的阿棠忽然整个人僵住——脚下传来一声极轻的「咔」。她低头:靴子底下,一块夯土的颜色和周围毫无差别,但它在极慢、极慢地往下沉。",
"sprite": "scene",
"next": "b-emo-b-d2"
},
{
"id": "b-emo-b-d2",
"type": "dialogue",
"speaker": "阿棠",
"text": "(一动不动,声音反而很平)哎。我踩着东西了。……是活的那种,还是死的那种?",
"sprite": "atang",
"next": "b-emo-b-d3"
},
{
"id": "b-emo-b-d3",
"type": "dialogue",
"speaker": "旁白",
"text": "没人答得上来。同一时刻,阵列深处,一面坑壁无声地滑开一道缝——黑黢黢的,看不见底。紧接着,头顶台基的边缘开始簌簌落土:重量移走了,结构在重新找平衡。",
"sprite": "scene",
"next": "b-emo-b-d4"
},
{
"id": "b-emo-b-d4",
"type": "dialogue",
"speaker": "沈铎",
"text": "(极低,极慢)谁。也。别。动。",
"sprite": "shenduo",
"next": "b-emo-b-d5"
},
{
"id": "b-emo-b-d5",
"type": "dialogue",
"speaker": "老鬼",
"text": "(脸白了)祖宗诶……这地方的东西,不知道埋了多少年了怎么还上班呢……",
"sprite": "laogui",
"next": "b-emo-b-d6"
},
{
"id": "b-emo-b-d6",
"type": "dialogue",
"speaker": "温简",
"text": "(蹲下,远远观察压板的边缘,声音是全队唯一没变调的)沉速很慢。非常慢。——它在动,但它不着急。",
"sprite": "wenjian",
"next": "cb-emo-b-01"
},
{
"id": "b-emo-b-d7",
"type": "dialogue",
"speaker": "旁白",
"text": "沈铎立即接管,其他队员退到警戒线。阿棠在受训人员协助下安全离开;你没看清压板沉降的过程。她看见你手里的空记录板,问:「刚才那一下,真有东西动了吗?」",
"sprite": "scene",
"next": "b-emo-b-d11"
},
{
"id": "b-emo-b-d8",
"type": "dialogue",
"speaker": "旁白",
"text": "你报告读数后清空外围,沈铎接管排险。阿棠安全撤出;你在外围听着频道里一段段报平安,直到最后一声才松开手。坑壁裂缝在视野之外开到最宽,没人及时拍下全貌。",
"sprite": "scene",
"next": "b-emo-b-d11"
},
{
"id": "b-emo-b-d9",
"type": "dialogue",
"speaker": "旁白",
"text": "你们退到警戒线外留影;沈铎与受训接应人员按既定预案带阿棠撤出。镜头拍到坑壁开缝的瞬间,却因距离太远,板面的刻痕看不清。阿棠看着那张照片:「原来刚才我站的是这里。」",
"sprite": "scene",
"next": "b-emo-b-d11"
},
{
"id": "b-emo-b-d10",
"type": "dialogue",
"speaker": "旁白",
"text": "沈铎按既定预案接管排险,其他队员退出阵列。阿棠在受训人员协助下安全离开。",
"sprite": "scene",
"next": "b-emo-b-d11"
},
{
"id": "cb-emo-b-01",
"type": "choice",
"pointId": "CB-EMO-B-01",
"prompt": "你怎么做?",
"dimensions": [
"emotional_stability"
],
"options": [
{
"id": "CB-EMO-B-01-A",
"label": "「先通知沈铎,按预案撤出阿棠;其余人退出阵列。」",
"next": "b-emo-b-d7"
},
{
"id": "CB-EMO-B-01-B",
"label": "「把观察到的沉速与缝宽报给沈铎;我去清空外围。」",
"next": "b-emo-b-d8"
},
{
"id": "CB-EMO-B-01-C",
"label": "「全员退到警戒线,远距留影,排险交沈铎处置。」",
"next": "b-emo-b-d9"
},
{
"id": "CB-EMO-B-01-TIMEOUT",
"label": "由沈铎接管排险",
"next": "b-emo-b-d10"
}
],
"timer": {
"seconds": 8,
"optionId": "CB-EMO-B-01-TIMEOUT"
}
},
{
"id": "b-emo-b-d11",
"type": "dialogue",
"speaker": "旁白",
"text": "坑壁的缝开到最宽,又缓缓合拢。阿棠安全撤出后,大家在警戒线外把各自见到的片段拼成同一张记录:缝里有成排的陶范——铸鼎的模子,一人来高,内壁的纹样与台基上七鼎六簋相似;缝口下方的机件锈蚀严重。这些观察不足以判定机关失效,需等待专业排险后确认。",
"sprite": "scene",
"next": "b-emo-b-d12"
},
{
"id": "b-emo-b-d12",
"type": "dialogue",
"speaker": "温简",
"text": "范是耗材,铸一器毁一范。整排整排地留在这儿——(停顿)说明铸完那一批之后,就再没有下一批了。",
"sprite": "wenjian",
"next": "b-emo-b-d13"
},
{
"id": "b-emo-b-d13",
"type": "dialogue",
"speaker": "沈铎",
"text": "(看着那道正在合拢的缝,声音沉下去)都记住。别拿锈色猜它死没死透。今天先记,不去试。",
"sprite": "shenduo",
"next": "b-emo-b-d14"
},
{
"id": "b-emo-b-d14",
"type": "dialogue",
"speaker": "阿棠",
"text": "(一屁股坐在安全线的土坎上,笑出了眼泪)吓死我了……结果到现在也不知道它是死是活。",
"sprite": "atang",
"next": "cb-emo-b-02"
},
{
"id": "b-emo-b-d15",
"type": "dialogue",
"speaker": "旁白",
"text": "你们留在安全线内,把剩下可见的纹样拍成序列。尘雾一阵阵遮住镜头;温简喊停时,最后几张已只有模糊轮廓。影像有用,但不够清:待会儿上台基辨纹,阿棠要对到第三件才敢拍板「就是这个」。",
"sprite": "scene",
"next": "b-emo-b-d18"
},
{
"id": "b-emo-b-d16",
"type": "dialogue",
"speaker": "旁白",
"text": "沈铎在安全线外的图志上标出方位:「记在这儿。账先欠着,城跑不了。」",
"sprite": "scene",
"next": "b-emo-b-d18"
},
{
"id": "b-emo-b-d17",
"type": "dialogue",
"speaker": "旁白",
"text": "相机递到手上又被你推了回去。缝合拢的声音很轻,像叹气。老鬼把三脚架收了,摩挲着架腿,破天荒说了句不市侩的话:「人家关上门的东西……咱们今天,就不硬敲了吧。」",
"sprite": "scene",
"next": "b-emo-b-d18"
},
{
"id": "cb-emo-b-02",
"type": "choice",
"pointId": "CB-EMO-B-02",
"prompt": "你怎么做?",
"dimensions": [
"emotional_stability"
],
"options": [
{
"id": "CB-EMO-B-02-A",
"label": "「能记多少记多少:留在安全线内把远处可见的纹样拍下来——它合它的,我们记我们的。」",
"next": "b-emo-b-d15"
},
{
"id": "CB-EMO-B-02-B",
"label": "「拍两张全景就够,标桩记位。它想合就让它合——账先欠着,城跑不了。」",
"next": "b-emo-b-d16"
},
{
"id": "CB-EMO-B-02-C",
"label": "「都收手,别拍了——它关门的事,就让它关完。」",
"next": "b-emo-b-d17"
}
]
},
{
"id": "b-emo-b-d18",
"type": "dialogue",
"speaker": "旁白",
"text": "采集收尾,全队向台基合拢。陶范的纹样与「铸完那一批,再没有下一批」的话,跟着你们一起上去。",
"sprite": "scene",
"next": "$resume"
}
]
}
+197
View File
@@ -0,0 +1,197 @@
{
"id": "b-val-a",
"dimension": "values",
"anchor": {
"chapter": "chapter2",
"afterNode": "c2-branch-anchor"
},
"estimatedMinutes": 8,
"startNodeId": "b-val-a-d1",
"nodes": [
{
"id": "b-val-a-d1",
"type": "dialogue",
"speaker": "旁白",
"text": "午后,东城前往宫墟的半路,一片废弃里坊。井台早干了,井沿边蜷着一个妇人和一个五六岁的孩子。妇人看见你们,第一眼看的不是你们的脸,是水囊。她看了孩子一眼,才慢慢站起来——没有伸手。她把手腕上一只银镯褪下来,摊在掌心,举到你们面前。里坊外的官道上有零星人影往城门方向挪——城门楼那头,隐隐飘来一缕熬粥的米气。阿棠顺着看了一眼:「早上路过,南门里侧的施粥棚开着呢,还发饼。排队的都是妇幼。」",
"sprite": "scene",
"next": "b-val-a-d2"
},
{
"id": "b-val-a-d2",
"type": "dialogue",
"speaker": "妇人",
"text": "(声音哑的,但每个字都放得很稳)买水。给孩子一口就行。",
"sprite": "scene",
"next": "b-val-a-d3"
},
{
"id": "b-val-a-d3",
"type": "dialogue",
"speaker": "旁白",
"text": "队里的饮水,按定数,只够两天。沈铎没说话。阿棠攥紧了自己的水囊。老鬼别过脸去。【若 wenjian_health != weak:温简在记录板的边角,无意识地画着什么。】【若 wenjian_health = weak:通讯频道里,传来温简翻页的声音。】",
"sprite": "scene",
"next": "b-val-a-d4"
},
{
"id": "b-val-a-d4",
"type": "dialogue",
"speaker": "妇人",
"text": "(见没人动,把镯子又往前送了半寸,补了一句)是足银的。……不脏,我天天擦。",
"sprite": "scene",
"next": "cb-val-a-01"
},
{
"id": "b-val-a-d5",
"type": "dialogue",
"speaker": "旁白",
"text": "妇人接水囊的手抖了一下。她先喂孩子——孩子喉咙里咕咚咕咚的声音,一路没人提,谁都听见了。然后她捧着水囊,往自己嘴里倒了极小的一口,把剩下的双手递还给你——你没让她倒回去。走出很远,阿棠才小声说:「刚才我水囊都攥出汗了。还好你给了……我是说——队里的水,够吗?」她替你把代价问出了口。够的。但往后两天,全队喝水都开始数着口数。",
"sprite": "scene",
"next": "b-val-a-d9"
},
{
"id": "b-val-a-d6",
"type": "dialogue",
"speaker": "旁白",
"text": "妇人接杯子的手停了一停——她懂了。她没争,先喂孩子。孩子喝了一半,把杯子举到她嘴边,她抿了一小口,就推回去。一来一回。走出去半条街,阿棠忽然说:「哎,你们数没数,那杯水转了四圈。」没人接话。——一杯水,四个来回;水囊的水位,浅了浅浅一格。",
"sprite": "scene",
"next": "b-val-a-d9"
},
{
"id": "b-val-a-d7",
"type": "dialogue",
"speaker": "旁白",
"text": "妇人深深看了你一眼——那一眼里没有感激,有一种被还回了人的平静。她把镯子塞进你手里,动作快得像怕你反悔。老鬼凑过来,啧啧:「新人,上道啊。」这句是夸是损,他自个儿笑眯眯地不解释。【若 wenjian_health != weak:镯子由温简当场登记为「民间有偿征集品 CW-0322」。】【若 wenjian_health = weak:镯子你代记「民间有偿征集品 CW-0322」,回营交温简补录入册。】",
"sprite": "scene",
"next": "b-val-a-d9"
},
{
"id": "b-val-a-d8",
"type": "dialogue",
"speaker": "旁白",
"text": "你说了施粥处的方向,说得很详细,孩子在听,很乖。妇人点头,背起孩子,走了两步,又走回来,把银镯塞进你手里:「不是买水了。谢谢你指得这么细。」她走了。老鬼在你身后挠头:「施粥处?前儿在城里还听人说断了米……哎,看这一炉粥气,是兴许又续上了。城里的事,一天一个样。」【若 wenjian_health != weak:银镯由温简当场登记为「民间赠与 CW-0322·答谢指路·代收,处置呈档案馆裁定」。】【若 wenjian_health = weak:银镯你代记「民间赠与 CW-0322·代收」,回营交温简补录入册,处置呈档案馆裁定。】",
"sprite": "scene",
"next": "b-val-a-d9"
},
{
"id": "cb-val-a-01",
"type": "choice",
"pointId": "CB-VAL-A-01",
"prompt": "你怎么做?",
"dimensions": [
"values"
],
"options": [
{
"id": "CB-VAL-A-01-A",
"label": "(解下水囊递过去)「倒吧。倒多少,您自己定。」",
"next": "b-val-a-d5"
},
{
"id": "CB-VAL-A-01-B",
"label": "(拿出队里的搪瓷杯,倒满一杯)「给孩子。一杯。」",
"next": "b-val-a-d6"
},
{
"id": "CB-VAL-A-01-C",
"label": "(接过银镯,解下两只水囊)「市卖的价,一镯两囊。您不欠我们,我们不欠您。」",
"next": "b-val-a-d7"
},
{
"id": "CB-VAL-A-01-D",
"label": "「水是有数的,给不了。(蹲下来,对孩子)前面城门有官舍施粥处,那儿有水有粥——让阿娘带你去,认得路吗?」",
"next": "b-val-a-d8"
}
]
},
{
"id": "b-val-a-d9",
"type": "dialogue",
"speaker": "旁白",
"text": "妇人背着孩子刚走出十几步,孩子忽然趴在妇人肩头,冲你们喊:「那边的台子上,夜里有人点灯!」他指的是观星台的方向。夜禁的城,满城无灯,而有人在高台上点灯。沈铎顺着孩子的手看过去,收回目光:「……上台看看。」",
"sprite": "scene",
"next": "b-val-a-d10"
},
{
"id": "b-val-a-d10",
"type": "dialogue",
"speaker": "旁白",
"text": "离开里坊,官道上,日头已经西斜——从里坊出来,一个时辰的路走完了。沈铎赶上来与你并肩,翻开册子——他是规程人,每一口水他都有数。",
"sprite": "scene",
"next": "b-val-a-d11"
},
{
"id": "b-val-a-d11",
"type": "dialogue",
"speaker": "旁白",
"text": "【若 cb_val_a_water = d_redirect:南门方向,粥棚的灶火还亮着。在前头进城的岔路口,阿棠认出了那对母子:两人已从粥棚回来,孩子捧着一只盛热粥的碗。你指的路,是活的。老鬼那句「断了米」,只是前几日的旧闲话。】",
"sprite": "scene",
"next": "b-val-a-d12"
},
{
"id": "b-val-a-d12",
"type": "dialogue",
"speaker": "沈铎",
"text": "今天的账。(停顿)水囊的水位,我看过。你想怎么记?",
"sprite": "shenduo",
"next": "cb-val-a-02"
},
{
"id": "b-val-a-d13",
"type": "dialogue",
"speaker": "旁白",
"text": "沈铎提笔,照实记了。【若 cb_val_a_water = a_give:施水一囊。】【若 cb_val_a_water = b_cup:施水一杯。】【若 cb_val_a_water = c_trade:售水两囊,得镯一支(CW-0322,有偿征集)。】【若 cb_val_a_water = d_redirect:水支出零,受赠镯一支(CW-0322,赠与代收)。】记完把册子合上,说了句题外话:「我爹以前是仓管。他说,账上干净的队,走路不回头看。」——这是全章他第一次提家人。顿了顿,又补了半句:「就是往后再遇这种事,先说一声再给——账要干净,队也得知道水去哪儿了。」",
"sprite": "scene",
"next": "b-val-a-d16"
},
{
"id": "b-val-a-d14",
"type": "dialogue",
"speaker": "旁白",
"text": "沈铎看了你两秒,提笔,先记「待分类杂项」及每项实数:【若 cb_val_a_water = a_give:施水一囊,未收物。】【若 cb_val_a_water = b_cup:施水一杯,未收物。】【若 cb_val_a_water = c_trade:售水两囊,得镯一支(CW-0322,有偿征集)。】【若 cb_val_a_water = d_redirect:水支出零,受赠镯一支(CW-0322,赠与代收)。】既没有把水记作漏损,也没把赠镯写成买卖;回营时还得补分录。合上册子他说:「账平了,手续还欠一笔。」",
"sprite": "scene",
"next": "b-val-a-d16"
},
{
"id": "b-val-a-d15",
"type": "dialogue",
"speaker": "旁白",
"text": "沈铎没推辞,按实数记了:【若 cb_val_a_water = a_give:施水一囊,未收物。】【若 cb_val_a_water = b_cup:施水一杯,未收物。】【若 cb_val_a_water = c_trade:售水两囊,得镯一支(CW-0322,有偿征集)。】【若 cb_val_a_water = d_redirect:水支出零,受赠镯一支(CW-0322,赠与代收)。】把笔递给你签。你签的时候他说:「推给我记的,和照实记的,册子上长一个样。」(停顿)「不一样的地方,你自己知道在哪儿。」",
"sprite": "scene",
"next": "b-val-a-d16"
},
{
"id": "cb-val-a-02",
"type": "choice",
"pointId": "CB-VAL-A-02",
"prompt": "你怎么做?",
"dimensions": [
"values"
],
"options": [
{
"id": "CB-VAL-A-02-A",
"label": "「照实记。给出多少、收了什么,都按实数。记在我名下。」",
"next": "b-val-a-d13"
},
{
"id": "CB-VAL-A-02-B",
"label": "「先记到『路上杂项』,备注水和镯子的实数,回营再分类。别在路边把来龙去脉都写完。」",
"next": "b-val-a-d14"
},
{
"id": "CB-VAL-A-02-C",
"label": "「账是队长的。你定,我签。」",
"next": "b-val-a-d15"
}
]
},
{
"id": "b-val-a-d16",
"type": "dialogue",
"speaker": "旁白",
"text": "账毕,日头西斜。黄昏时,你们登上观星台。",
"sprite": "scene",
"next": "$resume"
}
]
}
+131
View File
@@ -0,0 +1,131 @@
{
"id": "b-val-b",
"dimension": "values",
"anchor": {
"chapter": "chapter3",
"afterNode": "c3-d1b"
},
"estimatedMinutes": 8,
"startNodeId": "b-val-b-d1",
"nodes": [
{
"id": "b-val-b-d1",
"type": "dialogue",
"speaker": "旁白",
"text": "上午,宫墟安门内。穿过暗门后,下最后几级台阶时阿棠跳快了两步,落地崴了一下脚踝——她原地转了转脚腕,龇了龇牙:\"没事没事,走。\"台阶尽头站着个不该出现在这里的人——守墓老人。他不是一个人:背上驮着个十七八岁的少年,少年一条小腿用布条和草灰糊着,布条渗着暗色。老人把少年放下来,动作轻得不像他这个年纪。",
"sprite": "scene",
"next": "b-val-b-d2"
},
{
"id": "b-val-b-d2",
"type": "dialogue",
"speaker": "守墓老人",
"text": "(不绕弯子)陵区昨夜又塌了一角。这娃的爹在底下压着呢——这话回头说。(拍拍少年的肩)先救小的。",
"sprite": "scene",
"next": "b-val-b-d3"
},
{
"id": "b-val-b-d3",
"type": "dialogue",
"speaker": "温简",
"text": "(查看伤口,低声)石像生砸的。创面糊着草灰——老法子,撑不了多久,会烂。",
"sprite": "wenjian",
"next": "b-val-b-d4"
},
{
"id": "b-val-b-d4",
"type": "dialogue",
"speaker": "沈铎",
"text": "(打开物资箱,停住)外伤药一份——清创的药粉与止痛的丸剂合装为一「份」。(抬头)规程:这是撤离日的战备药。任何人在撤离日之前动用,需采集员裁断。",
"sprite": "shenduo",
"next": "b-val-b-d5"
},
{
"id": "b-val-b-d5",
"type": "dialogue",
"speaker": "旁白",
"text": "又是你裁。阿棠抱着手臂站在一边,没吭声。老鬼的眼珠在药瓶和金饼之间转了一圈,难得地没说话。少年一直看着你的鞋尖,忽然开口,声音没长开:",
"sprite": "scene",
"next": "b-val-b-d6"
},
{
"id": "b-val-b-d6",
"type": "dialogue",
"speaker": "守陵少年",
"text": "……要是为难,就算了。我阿爷说,药是有主的东西,跟命一样。",
"sprite": "scene",
"next": "cb-val-b-01"
},
{
"id": "b-val-b-d7",
"type": "dialogue",
"speaker": "旁白",
"text": "药粉撒上去的时候,少年咬着牙,一声没吭,只有抓着台阶的手指节发白。老人别过脸去,朝着陵区的方向,低声说了句什么,像骂人,又像还愿。沈铎在册子上记:「战备药动用,采集员裁断,事由:救人。」——他记「救人」两个字的时候,笔锋比平时重。",
"sprite": "scene",
"next": "b-val-b-d11"
},
{
"id": "b-val-b-d8",
"type": "dialogue",
"speaker": "旁白",
"text": "老人没恼。他点点头:「药有主,理是这个理。」他弯腰去背少年,少年却回头,解下自己腰里挂着的一小包草药,轻轻放在阿棠脚边(她今早下台阶崴的那一下,谁都没在意,他看出来了):「阿姐,这个……敷上,凉。」老人驮着他这就动身往城里去——水门里的药铺,半个时辰的路,天黑前能来回。走出十几步,少年趴在老人背上,忽然回头,冲台基的方向努嘴,把一句话喊给阿棠:「阿姐!上台别走正台阶——背阴那面,排水沟边上,有我们的路!」老鬼低头看着那包草药,半天没吭声,收摊似的咂了咂嘴:「哥哥我活这么大,头一回觉得穷。」",
"sprite": "scene",
"next": "b-val-b-d11"
},
{
"id": "b-val-b-d9",
"type": "dialogue",
"speaker": "旁白",
"text": "拆开合装,清创粉给了少年,止痛丸留在队里。上药后温简查看创面,只说了一句:「清创及时,这片创面问题不大。」——不打包票。少年被安置在台阶边的阴凉里等你们收工,疼劲上来时压着声哼,老人给他扇风,一下,一下。没人说你的分配不好。也没人再提它。",
"sprite": "scene",
"next": "b-val-b-d11"
},
{
"id": "b-val-b-d10",
"type": "dialogue",
"speaker": "旁白",
"text": "老人捧着药瓶,看了你很久很久。然后他做了一件全队都没想到的事:他只用了一半,仔仔细细封好剩下的一半,双手还给你:「药是救命的东西。救命的东西,不能断在你们手里。」(顿)「娃他爹常说,活人比死人难治——你们这些顾活人的,药得留着。」阿棠在旁边,眼圈忽然红了,扭头去看祭祀坑。沈铎在册子上记:「战备药半份,交守陵人保管——返还。」记完他抬眼看你,只说了一句:「这次他只用了一半。下不为例——这种赌,队里赌不起第二次。」",
"sprite": "scene",
"next": "b-val-b-d11"
},
{
"id": "cb-val-b-01",
"type": "choice",
"pointId": "CB-VAL-B-01",
"prompt": "你怎么做?",
"dimensions": [
"values"
],
"options": [
{
"id": "CB-VAL-B-01-A",
"label": "「用。战备战备,备的就是'万一'——现在就是那个万一。清创,上药,我来搭手。」",
"next": "b-val-b-d7"
},
{
"id": "CB-VAL-B-01-B",
"label": "「药动不了。(对老人)城里有大夫——我画路,再给您备一份金饼,这就动身送去,来得及。」",
"next": "b-val-b-d8"
},
{
"id": "CB-VAL-B-01-C",
"label": "「拆开来用:清创的药粉给他——感染会要命;止痛的留下——疼不会。两份药,各救各的急。」",
"next": "b-val-b-d9"
},
{
"id": "CB-VAL-B-01-D",
"label": "(把药瓶放进老人手里)「您拿着。用多少,您定——守陵的人,比我们懂伤。」",
"next": "b-val-b-d10"
}
]
},
{
"id": "b-val-b-d11",
"type": "dialogue",
"speaker": "旁白",
"text": "【若 cb_val_b_medicine != b_refer:少年拄着木枝,一瘸一拐把你们引到台基背阴面,亲手拨开荒草:「这边。我阿爷说,给神走的路修得高,给人走的路修得低。走低的,不摔跤。」】【若 cb_val_b_medicine = b_refer:老人和少年已往城里去。阿棠照着少年的喊话,在排水沟边找到荒草,拨开它。她学着少年的口气说:「给神走的路修得高,给人走的路修得低。走低的,不摔跤。」】",
"sprite": "scene",
"next": "$resume"
}
]
}
+122 -29
View File
@@ -32,7 +32,7 @@
"id": "c1-d1a", "id": "c1-d1a",
"type": "dialogue", "type": "dialogue",
"speaker": "馆长", "speaker": "馆长",
"text": "我们不收藏器物。器物会腐烂,会碎,会被误读。我们收藏的是人怎么活过——一座城消亡前,城里的人吃什么、怕什么、爱什么、最后那几天在做什么。这些东西,仪器采不到,只有人能采集。所以我们需要的采集员,必须自己也是「活过的」。穿越要烧燃料,而燃料只有一种——你的真实人生。烧掉的记忆不会消失,它们会存在档案馆的展厅里,这是后话。", "text": "我们不收藏器物。器物会腐烂,会碎,会被误读。我们收藏的是人怎么活过——一座城消亡前,城里的人吃什么、怕什么、爱什么、最后那几天在做什么。这些东西,仪器采不到,只有人能采集。所以我们需要的采集员,必须自己也是「活过的」。穿越要烧燃料。燃料这东西认两种:你讲出来的故事,和你在讲述点门前站过的那阵沉默——讲,是给;不说,把那阵沉默原样交给它,也是给。讲出来的记忆不会消失,它们会存在档案馆的展厅里,这是后话。",
"sprite": "curator", "sprite": "curator",
"next": "c1-d1b" "next": "c1-d1b"
}, },
@@ -72,7 +72,7 @@
"id": "c1-d3a", "id": "c1-d3a",
"type": "dialogue", "type": "dialogue",
"speaker": "馆长", "speaker": "馆长",
"text": "这扇叫离别之门。每个采集员的第一次穿越,都从它开始。它不开锁,不收凭证——它要一件祭品:一段真实的离别。", "text": "这扇叫离别之门。每个采集员的第一次穿越,都从它开始。它不开锁,不收凭证——它认一次交托:一段真实的离别。讲出来是交托,把那阵沉默原样交给它,也是交托;不提交记忆,门也照样启动。",
"sprite": "curator", "sprite": "curator",
"next": "c1-d3b" "next": "c1-d3b"
}, },
@@ -90,7 +90,10 @@
"pointId": "C1-02", "pointId": "C1-02",
"mechanism": "记忆钥匙", "mechanism": "记忆钥匙",
"prompt": "请讲述一段你真实经历过的离别。可以是一个人、一个地方、一段关系、一个阶段。说什么、说多少、怎么说,都由你。沉默也是回答。", "prompt": "请讲述一段你真实经历过的离别。可以是一个人、一个地方、一段关系、一个阶段。说什么、说多少、怎么说,都由你。沉默也是回答。",
"dimensions": ["attachment", "self_disclosure"], "dimensions": [
"attachment",
"self_disclosure"
],
"followUp": { "followUp": {
"maxQuestions": 1, "maxQuestions": 1,
"strategy": "Jev 六维缺失检测(五感/人物/时空/情绪),按幻境渲染价值取最高缺口,馆长口吻模板追问一句;「记不清了」为合法回答,馆长回应:记不清本身,档案馆也会收好。" "strategy": "Jev 六维缺失检测(五感/人物/时空/情绪),按幻境渲染价值取最高缺口,馆长口吻模板追问一句;「记不清了」为合法回答,馆长回应:记不清本身,档案馆也会收好。"
@@ -104,11 +107,36 @@
"olfactory_gustatory": "那天的空气里,有没有一种味道留到了现在?——不用准确,你记得的那一点就好。" "olfactory_gustatory": "那天的空气里,有没有一种味道留到了现在?——不用准确,你记得的那一点就好。"
}, },
"variants": [ "variants": [
{ "signature": "温暖×永失", "variantId": "sealed_love_tomb", "label": "「爱被封存」墓室——门后是墓道尽头一方石室,四壁绘着双人同耕、同炊、同观星的壁画,色彩温暖如新。石室中央两座并排的陶棺,棺盖上落满花瓣状的朱砂。空气是静的,静得像有人刚刚还在这里说话。基调:珍重,不是悲伤——有人把最好的东西封存了起来。", "tint": "#d4a96a" }, {
{ "signature": "悲恸", "variantId": "war_graves", "label": "「战乱乱冢」——门后是荒野,低低的土丘一座挨一座,没有碑,只有削尖的木桩。风把灰白的纸钱碎屑贴在脚踝上,远处有乌鸦起落,天色是黄昏将尽的那种灰蓝。基调:来不及告别——所有话都没说出口,所有名字都没来得及刻上。", "tint": "#5a5a72" }, "signature": "温暖×永失",
{ "signature": "释然", "variantId": "empty_post_station", "label": "「远行者的空驿」——门后是一座山间驿站,晨光,桌椅收拾得干干净净,灶膛里的灰还是温的,人已经走了。墙上挂着一件没带走的旧斗篷,桌上压着半张写了又划掉的字条。基调:告别已经完成——走得干净,留得体面,剩下的路各走各的。", "tint": "#7ba7a0" }, "variantId": "sealed_love_tomb",
{ "signature": "怨怼", "variantId": "desecrated_niche", "label": "「被凿毁的合葬龛」——门后是崖壁上的佛龛式墓龛,本该并坐的两尊石像,其中一尊被凿去了面目,凿痕新鲜得刺眼。香烛还燃着,蜡泪流到龛外。基调:被亏欠——有人毁了约,而另一个人连讨说法的对象都没有。", "tint": "#8a4b4b" }, "label": "「爱被封存」墓室——门后是墓道尽头一方石室,四壁绘着双人同耕、同炊、同观星的壁画,色彩温暖如新。石室中央两座并排的陶棺,棺盖上落满花瓣状的朱砂。空气是静的,静得像有人刚刚还在这里说话。基调:珍重,不是悲伤——有人把最好的东西封存了起来。",
{ "signature": "沉默", "variantId": "generic_fog", "label": "【沉默档 · 门照常开启】门对所有人开,这是铁律。门后是朦胧的星光与风声,不对应任何一段具体记忆——它以另一种方式认得你。", "tint": "#4a4a56" } "tint": "#d4a96a"
},
{
"signature": "悲恸",
"variantId": "war_graves",
"label": "「战乱乱冢」——门后是荒野,低低的土丘一座挨一座,没有碑,只有削尖的木桩。风把灰白的纸钱碎屑贴在脚踝上,远处有乌鸦起落,天色是黄昏将尽的那种灰蓝。基调:来不及告别——所有话都没说出口,所有名字都没来得及刻上。",
"tint": "#5a5a72"
},
{
"signature": "释然",
"variantId": "empty_post_station",
"label": "「远行者的空驿」——门后是一座山间驿站,晨光,桌椅收拾得干干净净,灶膛里的灰还是温的,人已经走了。墙上挂着一件没带走的旧斗篷,桌上压着半张写了又划掉的字条。基调:告别已经完成——走得干净,留得体面,剩下的路各走各的。",
"tint": "#7ba7a0"
},
{
"signature": "怨怼",
"variantId": "desecrated_niche",
"label": "「被凿毁的合葬龛」——门后是崖壁上的佛龛式墓龛,本该并坐的两尊石像,其中一尊被凿去了面目,凿痕新鲜得刺眼。香烛还燃着,蜡泪流到龛外。基调:被亏欠——有人毁了约,而另一个人连讨说法的对象都没有。",
"tint": "#8a4b4b"
},
{
"signature": "沉默",
"variantId": "generic_fog",
"label": "【沉默档 · 门照常开启】「留白的长廊」——门后是一条望不见头的长廊,两壁空无一物,像还没落笔的纸。风从走廊尽头吹来,带着很远的草香;走出去很远,身后的门还开着一条缝。基调:留白——没说出口的部分,也被好好收着。",
"tint": "#4a4a56"
}
], ],
"next": "c1-d4" "next": "c1-d4"
}, },
@@ -116,7 +144,7 @@
"id": "c1-d4", "id": "c1-d4",
"type": "dialogue", "type": "dialogue",
"speaker": "馆长", "speaker": "馆长",
"text": "【若 c1_02_signature != silent:门开了。你看——它要的不是故事讲得好不好,是故事真不真。】【若 c1_02_signature = silent:门开了。沉默也是档案的一种质地——它认得站在它面前的人,不一定需要言语。】你的记忆已经收进档案馆展厅,编号 C-2049-壹。它现在是你的了:随时可以回看,永远不会被任何人看见,除非你亲手授权——展厅,以及往后路上任何由它生出的幻境,都只有你能进入。在旁人眼里,你只是安静地站了一会儿。", "text": "【若 c1_02_signature != silent:门开了。你看——它要的不是故事讲得好不好,是人真不真。】【若 c1_02_signature = silent:门开了。沉默也是档案的一种质地——它认得站在它面前的人,不一定需要言语。】【若 c1_02_signature != silent:你的记忆已经收进档案馆展厅,编号 C-2049-壹。它现在是你的了:随时可以回看,永远不会被任何人看见,除非你亲手授权——展厅,以及往后路上任何由它生出的幻境,都只有你能进入。在旁人眼里,你只是安静地站了一会儿。】【若 c1_02_signature = silent:展厅为你留了一页空档,编号 C-2049-壹——此刻还没有字,哪天你想说了,它随时接着。展厅,以及往后路上任何由它生出的幻境,都只有你能进入。在旁人眼里,你只是安静地站了一会儿。】",
"sprite": "curator", "sprite": "curator",
"next": "c1-d4a" "next": "c1-d4a"
}, },
@@ -141,12 +169,30 @@
"type": "choice", "type": "choice",
"pointId": "C1-03", "pointId": "C1-03",
"prompt": "馆长:出发前我要问一个问题,这个问题没有标准答案,档案馆只是需要知道怎么给你派活——你为什么愿意去?", "prompt": "馆长:出发前我要问一个问题,这个问题没有标准答案,档案馆只是需要知道怎么给你派活——你为什么愿意去?",
"dimensions": ["values"], "dimensions": [
"values"
],
"options": [ "options": [
{ "id": "a_truth", "label": "「我想知道它发生了什么。」一座完整的城在你面前,不挖开看看,这辈子都不甘心。", "next": "c1-d4c" }, {
{ "id": "b_thrill", "label": "「听起来,这比坐在资料室里有意思多了。」说白了,谁不想真的走进自己研究的东西里去。", "next": "c1-d4c" }, "id": "a_truth",
{ "id": "c_escape", "label": "「最近的日子……换个地方待三十天,挺好。」现实里有些事情,正好可以放一放。", "next": "c1-d4c" }, "label": "「我想知道它发生了什么。」一座完整的城在你面前,不挖开看看,这辈子都不甘心。",
{ "id": "d_trust", "label": "「有人希望我去。」这件事不完全是为自己做的。", "next": "c1-d4c" } "next": "c1-d4c"
},
{
"id": "b_thrill",
"label": "「听起来,这比坐在资料室里有意思多了。」说白了,谁不想真的走进自己研究的东西里去。",
"next": "c1-d4c"
},
{
"id": "c_escape",
"label": "「最近的日子……换个地方待三十天,挺好。」现实里有些事情,正好可以放一放。",
"next": "c1-d4c"
},
{
"id": "d_trust",
"label": "「有人希望我去。」这件事不完全是为自己做的。",
"next": "c1-d4c"
}
] ]
}, },
{ {
@@ -210,12 +256,31 @@
"type": "choice", "type": "choice",
"pointId": "C1-04", "pointId": "C1-04",
"prompt": "老鬼的提议悬在空气里,你接不接话?", "prompt": "老鬼的提议悬在空气里,你接不接话?",
"dimensions": ["values", "conflict_style"], "dimensions": [
"values",
"conflict_style"
],
"options": [ "options": [
{ "id": "a_confront", "label": "「东西留在原地,才叫历史。带回来,就只是赃物。」(当着所有人,把话说死)", "next": "c1-04-ra" }, {
{ "id": "b_deflect", "label": "「鬼哥这话说的——先看有什么,再说带不带呗。」(打个哈哈,不接也不顶)", "next": "c1-04-rb" }, "id": "a_confront",
{ "id": "c_redirect", "label": "(不接话,转向沈铎)「队长,落点方案定了么?」(用正事把这个话题盖过去)", "next": "c1-04-rc" }, "label": "「东西留在原地,才叫历史。带回来,就只是赃物。」(当着所有人,把话说死)",
{ "id": "d_play_along", "label": "(笑)「行啊,回头鬼哥带我见识见识行情。」(顺着他说,看看他到底想干什么)", "next": "c1-04-rd" } "next": "c1-04-ra"
},
{
"id": "b_deflect",
"label": "「鬼哥这话说的——先看有什么,再说带不带呗。」(打个哈哈,不接也不顶)",
"next": "c1-04-rb"
},
{
"id": "c_redirect",
"label": "(不接话,转向沈铎)「队长,落点方案定了么?」(用正事把这个话题盖过去)",
"next": "c1-04-rc"
},
{
"id": "d_play_along",
"label": "(笑)「行啊,回头鬼哥带我见识见识行情。」(顺着他说,看看他到底想干什么)",
"next": "c1-04-rd"
}
] ]
}, },
{ {
@@ -278,20 +343,38 @@
"id": "c1-05", "id": "c1-05",
"type": "choice", "type": "choice",
"pointId": "C1-05", "pointId": "C1-05",
"prompt": "碑额拓片特写:上方是屋顶(宀),屋顶之下一个安坐的人形,膝前有衣袂垂落之态——宀下一个女子安坐的形象。这是什么字?它刻在碑额上,意味着这是什么碑?", "prompt": "碑额拓片特写:上方是屋顶(宀),屋顶之下一个安坐的人形,膝前有衣袂垂落之态——宀下一个女子安坐的形象。另有两条旁证:碑为圆首、无碑穿、无榫、下无龟趺座,不合墓碑规制;此碑出土于苍梧北门外官道旁,碑面朝城。这是什么字?它刻在碑额上,意味着这是什么碑?",
"dimensions": ["culture"], "dimensions": [
"culture"
],
"options": [ "options": [
{ "id": "stele_guardian", "label": "「安」。室中有女,安坐不惊——这是镇城安民碑,碑额朝向即古城安门所在。", "next": "c1-05-ok" }, {
{ "id": "stele_tomb", "label": "「安」。安魂之意——这是墓碑,镇的是墓主的安宁。", "next": "c1-05-miss" }, "id": "stele_guardian",
{ "id": "stele_merit", "label": "「宁」。定鼎纪功——这是记功碑,记的是立碑人的功业。", "next": "c1-05-miss" }, "label": "「安」。室中有女,安坐不惊——这是镇城安民碑,碑额朝向即古城安门所在。",
{ "id": "stele_boundary", "label": "「定」。划定疆界——这是界碑,标的是城的四至。", "next": "c1-05-miss" } "next": "c1-05-ok"
},
{
"id": "stele_tomb",
"label": "「安」。安魂之意——这是墓碑,镇的是墓主的安宁。",
"next": "c1-05-miss"
},
{
"id": "stele_merit",
"label": "「宁」。定鼎纪功——这是记功碑,记的是立碑人的功业。",
"next": "c1-05-miss"
},
{
"id": "stele_boundary",
"label": "「定」。划定疆界——这是界碑,标的是城的四至。",
"next": "c1-05-miss"
}
] ]
}, },
{ {
"id": "c1-05-ok", "id": "c1-05-ok",
"type": "dialogue", "type": "dialogue",
"speaker": "沈铎", "speaker": "沈铎",
"text": "(点头)碑额朝向安门,碑身镇的是城的安危。西北角那片宫墟,勘探七年没动过——如果苍梧人有「安放」什么的地方,就在那里。", "text": "(点头)碑额朝向安门,碑形不合墓制、又立在城外官道旁——三条证据同向:碑身镇的是城的安危。西北角那片宫墟,勘探七年没动过——如果苍梧人有「安放」什么的地方,就在那里。",
"sprite": "shenduo", "sprite": "shenduo",
"next": "c1-d6c" "next": "c1-d6c"
}, },
@@ -364,10 +447,20 @@
"type": "choice", "type": "choice",
"pointId": "C1-06", "pointId": "C1-06",
"prompt": "穿越落点,选哪个?", "prompt": "穿越落点,选哪个?",
"dimensions": ["risk"], "dimensions": [
"risk"
],
"options": [ "options": [
{ "id": "a_safe", "label": "稳妥落点:城外驿站,走一天官道进城。", "next": "c1-cutscene" }, {
{ "id": "b_precise", "label": "精确落点:直接落在苍梧城内,西市。", "next": "c1-cutscene" } "id": "a_safe",
"label": "稳妥落点:城外驿站,走一天官道进城。",
"next": "c1-cutscene"
},
{
"id": "b_precise",
"label": "精确落点:直接落在苍梧城内,西市。",
"next": "c1-cutscene"
}
] ]
}, },
{ {
+161 -45
View File
@@ -32,7 +32,7 @@
"id": "c2-d2", "id": "c2-d2",
"type": "dialogue", "type": "dialogue",
"speaker": "老鬼", "speaker": "老鬼",
"text": "(营地晨会,他举起一个瓷瓶)防瘴的药粉,满打满算,两人份。这东西是定数,档案馆给的量抠得死,多一两都没有。", "text": "(营地晨会,他举起一个瓷瓶)防瘴的药粉,满打满算,两人份——两个健康人七天的预防量。真要给病号治疟疾,两份连服才压得住,一分都剩不下。这东西是定数,档案馆给的量抠得死,多一两都没有。",
"sprite": "laogui", "sprite": "laogui",
"next": "c2-d2a" "next": "c2-d2a"
}, },
@@ -81,19 +81,38 @@
"type": "choice", "type": "choice",
"pointId": "C2-01", "pointId": "C2-01",
"prompt": "两人份的药,三个人要。按规程,由你裁断——", "prompt": "两人份的药,三个人要。按规程,由你裁断——",
"dimensions": ["values", "conflict_style"], "dimensions": [
"values",
"conflict_style"
],
"options": [ "options": [
{ "id": "a_weakest", "label": "「先给温简。病着的人等不起,外勤和补给可以想办法。」", "next": "c2-01-ra" }, {
{ "id": "b_exposure", "label": "「按暴露风险分:队长和老鬼。温简留在营地,反而最不需要出门。」", "next": "c2-01-rb" }, "id": "a_weakest",
{ "id": "c_split", "label": "「两人份的药,三个人分——匀一匀,每人少点,先把今天撑过去。」", "next": "c2-01-rc" }, "label": "「两份都给温简,连服压病。我们仨穿蓑衣、烧艾草,土法顶着。」(治疗优先:病的窗口不等人,预防的缺口用笨办法补)",
{ "id": "d_process", "label": "「我不裁。规矩该事先定好——现在让全队一起议,议出个章程来,往后都照章程办。」", "next": "c2-01-rd" } "next": "c2-01-ra"
},
{
"id": "b_exposure",
"label": "「按暴露风险分:队长和老鬼一人一份。温简留在营地硬扛——他不出门,感染的面最小。」(预防优先:病靠扛,瘴靠防)",
"next": "c2-01-rb"
},
{
"id": "c_split",
"label": "「两人份的药,三个人分——匀一匀,先把这几天撑过去。」(分完谁的量都不够:治疗量不足,两份预防也各打折扣)",
"next": "c2-01-rc"
},
{
"id": "d_process",
"label": "「我先不裁。这事得全队一起议——议出个章程,往后都照章程办。」(程序优先:章程要议半个上午;温简的烧等不等得起议事,议事的人心里都有数)",
"next": "c2-01-rd"
}
] ]
}, },
{ {
"id": "c2-01-ra", "id": "c2-01-ra",
"type": "dialogue", "type": "dialogue",
"speaker": "旁白", "speaker": "旁白",
"text": "温简当日退烧,次日能下床。老鬼笑嘻嘻记下「新人讲人情」。次日沈铎外勤淋了瘴雨,低烧半日——他瞒着没说,阿棠后来发现,跟老鬼大吵一架。", "text": "两份药当日开始连服,温简当日退烧,次日能下床。老鬼笑嘻嘻记下「新人讲人情」。次日沈铎外勤淋了瘴雨,队里已无预防的药,低烧半日——他瞒着没说,阿棠后来发现,跟老鬼大吵一架。",
"sprite": "scene", "sprite": "scene",
"next": "c2-d3" "next": "c2-d3"
}, },
@@ -101,23 +120,23 @@
"id": "c2-01-rb", "id": "c2-01-rb",
"type": "dialogue", "type": "dialogue",
"speaker": "旁白", "speaker": "旁白",
"text": "温简烧到第三日才退,虚弱期按沈铎命令留营待命。沈铎与老鬼无恙。老鬼对你明显热络。", "text": "温简没等到药,烧又熬了一整日,到第 3 日晌午才退净,虚弱期按沈铎命令留营待命(状态贯穿见 act1.md 附录 D 日程)。沈铎与老鬼靠预防份无恙。老鬼对你明显热络。",
"sprite": "scene", "sprite": "scene",
"next": "c2-d3" "next": "c2-d3"
}, },
{ {
"id": "c2-01-rc", "id": "c2-01-rc",
"type": "dialogue", "type": "dialogue",
"speaker": "老鬼", "speaker": "旁白",
"text": "三人皆未足量:温简病情拖延,沈铎外勤染瘴轻微发热,但无人倒下。老鬼点评:「和稀泥是和稀泥,不过稀泥也能糊墙。」", "text": "三份不足量:温简的治疗断断续续,烧同样拖到第 3 日才退(虚弱期同 B);沈铎外勤染瘴轻微发热,但无人倒下。老鬼点评:「和稀泥是和稀泥,不过稀泥也能糊墙。」",
"sprite": "laogui", "sprite": "scene",
"next": "c2-d3" "next": "c2-d3"
}, },
{ {
"id": "c2-01-rd", "id": "c2-01-rd",
"type": "dialogue", "type": "dialogue",
"speaker": "旁白", "speaker": "旁白",
"text": "议事占去半个上午。章程议出来了——「先病后险,依次补给」,往后照此办理。但章程落定之前,队里是没有规矩的:老鬼早就不耐烦,按他那套「贡献论」把药先分了——他和沈铎各一份,木已成舟。沈铎没有拦;药入了他手,他搁在箱上没动,等章程。等章程出来,温简的烧已经又上去一轮,药再喂下去,晚了半日。事后沈铎把章程抄在记录板第一页,单独对你说:「往后不用议了。这次慢掉的半日,记在我账上。」——你赢了往后每一次,输了眼前这一次。", "text": "议事占去半个上午。章程议出来了——「先病后险,依次补给」,往后照此办理。但议事的这半个上午里,温简的烧又上去一轮——足量治疗从当日下午才算开始,晚了半日(虚弱期同 B)。沈铎把章程抄在记录板第一页,单独对你说:「往后不用议了。这次慢掉的半日,记在我账上。」——你赢了往后每一次,输了眼前这一次。",
"sprite": "shenduo", "sprite": "shenduo",
"next": "c2-d3" "next": "c2-d3"
}, },
@@ -125,7 +144,7 @@
"id": "c2-d3", "id": "c2-d3",
"type": "dialogue", "type": "dialogue",
"speaker": "守墓老人", "speaker": "守墓老人",
"text": "【若 wenjian_health = weak:温简不在场——服过药,烧退了,沈铎命令他留营待命。】(城北墓道,他坐在坟头之间编草绳,不抬头)生面孔。城里来的?不像。城里人的脚,这两个月不沾泥——不敢沾。", "text": "【若 wenjian_health = recovering:温简不在场——药当日压住了烧,沈铎命他在营里发汗躺着,今日不沾风。】【若 wenjian_health = weak:温简不在场——烧没退,沈铎命令他留营待命,营里给他留了水和干粮。】(城北墓道,他坐在坟头之间编草绳,不抬头)生面孔。城里来的?不像。城里人的脚,这两个月不沾泥——不敢沾。",
"sprite": "elder", "sprite": "elder",
"next": "c2-d3a" "next": "c2-d3a"
}, },
@@ -166,8 +185,11 @@
"type": "narration", "type": "narration",
"pointId": "C2-02", "pointId": "C2-02",
"mechanism": "故事换情报", "mechanism": "故事换情报",
"prompt": "老人收故事不收钱。请讲述一件你此生最后悔的决定。说多少、怎么说,由你。不讲也行——陪他编一下午草绳,他该说的,一句也不会少说。", "prompt": "老人收故事不收钱。请讲述一件你此生最后悔的决定。说多少、怎么说,由你。不讲也行——他该说的,一句也不会少说。暗门开在黄昏,讲不讲,都得等到那个时辰。",
"dimensions": ["values", "emotional_stability"], "dimensions": [
"values",
"emotional_stability"
],
"followUp": { "followUp": {
"maxQuestions": 1, "maxQuestions": 1,
"strategy": "Jev 六维缺失检测(五感/人物/时空/情绪),按幻境渲染价值取最高缺口,老人口吻模板追问一句;「记不清了」为合法回答,老人回应:记不清好哇。记不清的悔,是老天爷替你收了半笔账。" "strategy": "Jev 六维缺失检测(五感/人物/时空/情绪),按幻境渲染价值取最高缺口,老人口吻模板追问一句;「记不清了」为合法回答,老人回应:记不清好哇。记不清的悔,是老天爷替你收了半笔账。"
@@ -181,10 +203,30 @@
"olfactory_gustatory": "人呐,记性最靠的是鼻子和舌头——那阵子有什么味道?" "olfactory_gustatory": "人呐,记性最靠的是鼻子和舌头——那阵子有什么味道?"
}, },
"variants": [ "variants": [
{ "signature": "自责", "variantId": "intel_full", "label": "【带路】月色下的陵区,石像生的影子很长。老人亲自举火把,带你们走到暗门口——「陵里的路,闭眼走比睁眼走直」。行至暗门,他忽然说起自己年轻时也悔过一件事,说了半句,摆摆手不说了。基调:被信任的厚重。", "tint": "#6a8a6a" }, {
{ "signature": "遗憾但坦然", "variantId": "intel_mid", "label": "【同路】老人送到界碑,把话说完——方位、时辰、完整的谶语一句不少——指了方向就回窝棚。你们自己走进月下的封土群,找到他说的那块「会唱歌的石头」。临别他多说一句:「放得下的,才叫过去。」基调:得体的距离。", "tint": "#a0964b" }, "signature": "自责",
{ "signature": "愤怒指向他人", "variantId": "intel_low", "label": "【指路】老人听完,编草绳的手停了很久。该指的路、该说的时辰,一句没少,只是没再多看你一眼。陵区的路你们自己走进去,他站在界碑外,没有送。基调:话到,情未到。", "tint": "#9a5b4b" }, "variantId": "intel_full",
{ "signature": "沉默", "variantId": "intel_silent", "label": "【草绳】你陪他编一下午草绳,他絮絮地说,你默默地听。天黑前,该知道的你都知道了——方位、时辰、谶语,一句不少。临走他说:「不说的人,心里的事最重。重的别久揣。」基调:笨拙的善意。", "tint": "#7a6a52" } "label": "【絮说】月色下的陵区,石像生的影子很长。老人举火把走在前面,脚步轻得不像老人——「陵里的路,闭眼走比睁眼走直」。行至暗门,他忽然说起自己年轻时也悔过一件事,说了半句,摆摆手不说了。摸一遍门石,等门缝开了才走。基调:被信任的厚重。",
"tint": "#6a8a6a"
},
{
"signature": "遗憾但坦然",
"variantId": "intel_mid",
"label": "【同路】界碑前他把方位时辰又对了一遍,才肯动身。你们自己走进月下的封土群,他跟在后头一路没再多话,找到那块「会唱歌的石头」时才嗯了一声。到暗门口,他多说一句:「放得下的,才叫过去。」门缝开了才走。基调:得体的距离。",
"tint": "#a0964b"
},
{
"signature": "愤怒指向他人",
"variantId": "intel_low",
"label": "【缄默】老人听完,编草绳的手停了很久。动身时该指的路、该说的时辰,一句没少——说完就闭了嘴,走在队尾,没再多看你一眼。可你们走出十几步,他在身后忽然开口:「火气这东西,老头子也有过一回。」没头没尾,再没下文。门缝开了,他转身就走。基调:话到,情未到——可那一句话,他自己也未必舍得讲给旁人。",
"tint": "#9a5b4b"
},
{
"signature": "沉默",
"variantId": "intel_silent",
"label": "【草绳】一下午你们并肩搓绳,他絮絮地说陵里的事,你默默地听。日头偏西,他拍拍手上的草屑起身,一样把你们送到暗门口。等门缝开了,他说:「不说的人,心里的事最重。重的别久揣。」基调:笨拙的善意。",
"tint": "#7a6a52"
}
], ],
"next": "c2-d4" "next": "c2-d4"
}, },
@@ -192,7 +234,7 @@
"id": "c2-d4", "id": "c2-d4",
"type": "dialogue", "type": "dialogue",
"speaker": "守墓老人", "speaker": "守墓老人",
"text": "(听完,沉默片刻,把编好的草绳往肩上一搭)行。是真的。真的东西,听得出来——假故事讲得溜,真故事讲得磕巴。【若 c2_02_signature = silent:(他不追问,只在日头偏西时拍拍手上的草屑)不说的人,心里的事最重。重的别久揣。】(无论讲没讲、讲成什么样,他给出的情报完全一致:暗门方位、开启时辰,和那句完整的谶语——)不是人走了,是请走的。走吧,趁天没黑。", "text": "(听完,沉默片刻,把编好的草绳往肩上一搭)行。是真的。真的东西,听得出来——假故事讲得溜,真故事讲得磕巴。【若 c2_02_signature = silent:(他不追问,只在日头偏西时拍拍手上的草屑)不说的人,心里的事最重。重的别久揣。】(无论讲没讲、讲成什么样,他给出的情报完全一致:暗门方位、开启时辰,和那句完整的谶语。黄昏动身,他亲自把你们送到暗门口,摸一遍门石,等门缝开了才回——)不是人走了,是请走的。走吧,趁天没黑。",
"sprite": "elder", "sprite": "elder",
"next": "c2-d4a" "next": "c2-d4a"
}, },
@@ -249,11 +291,26 @@
"type": "choice", "type": "choice",
"pointId": "C2-03", "pointId": "C2-03",
"prompt": "主街还是窄巷,你定——", "prompt": "主街还是窄巷,你定——",
"dimensions": ["risk", "conflict_style"], "dimensions": [
"risk",
"conflict_style"
],
"options": [ "options": [
{ "id": "a_main", "label": "「走主街。」(支持沈铎:四十分钟买确定性,值)", "next": "c2-03-ra" }, {
{ "id": "b_alley", "label": "「走巷子。」(支持阿棠:饮水和时间是真约束,悬梁看着还稳)", "next": "c2-03-rb" }, "id": "a_main",
{ "id": "c_scout", "label": "「巷子我先进。我体重最轻,走前面探,有问题退回来,主力走主街汇合。」(折中:用自己去验证风险)", "next": "c2-03-rc" } "label": "「走主街。」(支持沈铎:四十分钟买确定性,值)",
"next": "c2-03-ra"
},
{
"id": "b_alley",
"label": "「走巷子。」(支持阿棠:饮水和时间是真约束,悬梁看着还稳)",
"next": "c2-03-rb"
},
{
"id": "c_scout",
"label": "「先停一步。巷口望一眼——悬梁什么状态,看清了再定。」(折中:不赌运气,也不多花四十分钟;全队留在巷口安全距离外观察结构)",
"next": "c2-03-rc"
}
] ]
}, },
{ {
@@ -262,7 +319,7 @@
"speaker": "旁白", "speaker": "旁白",
"text": "主街行进无事,但在主街中段发现一队苍梧官差刚贴出的告示——「夜禁」告示原件,信息量极高的独有采集素材。阿棠服了气,但嘟囔了一路。", "text": "主街行进无事,但在主街中段发现一队苍梧官差刚贴出的告示——「夜禁」告示原件,信息量极高的独有采集素材。阿棠服了气,但嘟囔了一路。",
"sprite": "scene", "sprite": "scene",
"next": "c2-d5" "next": "c2-branch-anchor"
}, },
{ {
"id": "c2-03-rb", "id": "c2-03-rb",
@@ -270,13 +327,21 @@
"speaker": "沈铎", "speaker": "沈铎",
"text": "巷子顺利通过,节省四十分钟,你们提前抵达宫墟外围,赶上一段独有的观察窗口——守陵人队伍换岗。沈铎一路无话,抵达后只说:「运气好。别把运气当方案。」", "text": "巷子顺利通过,节省四十分钟,你们提前抵达宫墟外围,赶上一段独有的观察窗口——守陵人队伍换岗。沈铎一路无话,抵达后只说:「运气好。别把运气当方案。」",
"sprite": "shenduo", "sprite": "shenduo",
"next": "c2-d5" "next": "c2-branch-anchor"
}, },
{ {
"id": "c2-03-rc", "id": "c2-03-rc",
"type": "dialogue", "type": "dialogue",
"speaker": "旁白", "speaker": "旁白",
"text": "你先进巷子,中段一根悬梁真的塌了——提前预警,你扑出来,灰头土脸,人没事。全队退回走主街,损失二十分钟。阿棠主动说「是我撺掇的」,沈铎说「探法本身没错,下次用绳」。你获得一条独有观察:塌梁的断口是新茬——这片废墟不久前塌过一次。记住这个细节,往下走,你会再想起它。", "text": "全队在巷口停住,贴着残墙望进去——不到一分钟,中段一根悬梁无声地塌了,砸起半巷子的灰。谁也没说话,全队默默退回主街,损失二十分钟。阿棠主动说「是我撺掇的」;沈铎没评价任何人,只把巷口的位置标进了手绘图。你获得一条独有观察:塌落的梁,断口是新茬——这片废墟不久前塌过一次。记住这个细节,往下走,你会再想起它。",
"sprite": "scene",
"next": "c2-branch-anchor"
},
{
"id": "c2-branch-anchor",
"type": "dialogue",
"speaker": "旁白",
"text": "路口的争论落在身后,你们向观星台的方向继续走。",
"sprite": "scene", "sprite": "scene",
"next": "c2-d5" "next": "c2-d5"
}, },
@@ -284,7 +349,7 @@
"id": "c2-d5", "id": "c2-d5",
"type": "dialogue", "type": "dialogue",
"speaker": "阿棠", "speaker": "阿棠",
"text": "(黄昏,观星台遗址)哎,你们说,好好的星图,谁会专门把中间一块磨掉?这得多恨这块石头。", "text": "【若 flag_side_stairs = true:你们从侧阶登台——阿棠在两袋烟的工夫里替你省下的半里路。】(黄昏,观星台遗址)哎,你们说,好好的星图,谁会专门把中间一块磨掉?这得多恨这块石头。",
"sprite": "atang", "sprite": "atang",
"next": "c2-d5a" "next": "c2-d5a"
}, },
@@ -292,7 +357,7 @@
"id": "c2-d5a", "id": "c2-d5a",
"type": "dialogue", "type": "dialogue",
"speaker": "温简", "speaker": "温简",
"text": "【若 wenjian_health = weak:温简本被列入留营名单,出发前自己背着记录板跟来了——气色仍差,只说一句:「解谜要记录员。」沈铎看了他三秒,没拦。】或者……多怕这块石头上记的东西。", "text": "【若 wenjian_health = recovering:温简下床首日,行装减半——阿棠把他的记录板架抢过去背了。】【若 wenjian_health = weak:温简本被列入留营名单——他的烧晌午才退净。出发前他自己背着记录板跟来了,脸色还白着,只说一句:「解谜要记录员。」沈铎看了他三秒,没拦,只把队伍的行进速度压慢了半档。】或者……多怕这块石头上记的东西。",
"sprite": "wenjian", "sprite": "wenjian",
"next": "c2-d5b" "next": "c2-d5b"
}, },
@@ -308,13 +373,31 @@
"id": "c2-04", "id": "c2-04",
"type": "choice", "type": "choice",
"pointId": "C2-04", "pointId": "C2-04",
"prompt": "星图盘外圈完整刻着东方苍龙七宿(角、亢、氐、房、心、尾、箕)的连线,被磨去的中央区域正是苍龙的「心脏」。盘面残留一行小字铭文:「火中,而后城安。」补全被磨去的部分——缺的是哪一宿?「大火」又是哪一颗?", "prompt": "星图盘外圈刻着东方苍龙七宿(角、亢、氐、房、心、尾、箕)的连线——连线一气贯通,但心宿一段只剩连线经过的磨平空槽:星位本身被人为铲去,龙的「心脏」在盘面上是一个空框。盘面残留一行小字铭文:「火中,而后城安。」补全被铲去的部分——缺的是哪一宿?「大火」又是哪一颗?",
"dimensions": ["culture"], "dimensions": [
"culture"
],
"options": [ "options": [
{ "id": "heart_xin", "label": "心宿三星,排成一线微弯;中间最红最亮的那颗是大火——心宿二。把它点在苍龙的心口。", "next": "c2-04-ok" }, {
{ "id": "heart_fang", "label": "房宿四星——龙的胸腔,正好在整张盘的正中。", "next": "c2-04-miss" }, "id": "heart_xin",
{ "id": "heart_tail", "label": "尾宿九星——补在龙尾收束处,首尾呼应。", "next": "c2-04-miss" }, "label": "心宿三星,排成一线微弯;中间最红最亮的那颗是大火——心宿二。把它点在苍龙的心口。",
{ "id": "heart_mars", "label": "荧惑——红色的就是火星,把它点在盘心。", "next": "c2-04-miss" } "next": "c2-04-ok"
},
{
"id": "heart_fang",
"label": "房宿四星——龙的胸腔,正好在整张盘的正中。",
"next": "c2-04-miss"
},
{
"id": "heart_tail",
"label": "尾宿九星——补在龙尾收束处,首尾呼应。",
"next": "c2-04-miss"
},
{
"id": "heart_mars",
"label": "荧惑——红色的就是火星,把它点在盘心。",
"next": "c2-04-miss"
}
] ]
}, },
{ {
@@ -387,7 +470,10 @@
"pointId": "C2-05", "pointId": "C2-05",
"mechanism": "篝火互惠", "mechanism": "篝火互惠",
"prompt": "温简先讲了他的。现在火光看向你。讲一段你的——什么都可以,讲多少由你。沉默也是回答,火不催债。", "prompt": "温简先讲了他的。现在火光看向你。讲一段你的——什么都可以,讲多少由你。沉默也是回答,火不催债。",
"dimensions": ["attachment", "reciprocity"], "dimensions": [
"attachment",
"reciprocity"
],
"followUp": { "followUp": {
"maxQuestions": 1, "maxQuestions": 1,
"strategy": "Jev 六维缺失检测(五感/人物/时空/情绪),按幻境渲染价值取最高缺口,温简口吻模板追问一句;「记不清了」为合法回答,温简回应:嗯。(拨火)记不清的,就别硬捞了。火边坐着,不讲也暖和。" "strategy": "Jev 六维缺失检测(五感/人物/时空/情绪),按幻境渲染价值取最高缺口,温简口吻模板追问一句;「记不清了」为合法回答,温简回应:嗯。(拨火)记不清的,就别硬捞了。火边坐着,不讲也暖和。"
@@ -401,9 +487,24 @@
"olfactory_gustatory": "那段日子有什么气味或味道,留在记忆里了吗?" "olfactory_gustatory": "那段日子有什么气味或味道,留在记忆里了吗?"
}, },
"variants": [ "variants": [
{ "signature": "深暴露", "variantId": "wenjian_close_early", "label": "【同担】温简听完后沉默很久,然后讲了第二个、从没对任何人讲过的细节——他哥病房的窗帘颜色。当夜他低声告诉你:档案馆给每个记录员配的板子,上报内容比你知道的多一行。基调:两个把伤口摊开的人,在火边达成不声张的同盟。", "tint": "#d4844b" }, {
{ "signature": "浅暴露", "variantId": "wenjian_close_late", "label": "【半程】温简认真听完,道谢,没再追问。次日他与你同行,路上主动讲了些苍梧的掌故。档案馆疑云他没提。基调:得体,温热,留有余地。", "tint": "#8a7a5a" }, "signature": "深暴露",
{ "signature": "沉默", "variantId": "wenjian_distant", "label": "【守夜】你们并肩坐到火熄。温简往你手里塞了个烤热的薯,什么都没说。次日他照旧对你点头,不多一分不少一分——但阿棠偷偷告诉你:「温简昨晚跟沈铎说,你这个人『稳』。」基调:沉默被原样收下——没有人催,没有人问第二遍;温简的亲密度走慢线。", "tint": "#4b4b5a" } "variantId": "wenjian_close_early",
"label": "【同担】温简听完后沉默很久,然后讲了第二个、从没对任何人讲过的细节——他哥病房的窗帘颜色,他每周去看,窗帘换过两次。当夜守火时他挪到你旁边,只说了一句:「你那段,我只讲给火听。」基调:两个把伤口摊开的人,在火边达成不声张的同盟——他哥的事、你的事,从此各多一个保管人。",
"tint": "#d4844b"
},
{
"signature": "浅暴露",
"variantId": "wenjian_close_late",
"label": "【半程】温简认真听完,道谢,没再追问。次日他与你同行,路上主动讲了些苍梧的掌故,末了像顺口一提:档案馆给记录员的板子按记录量定量配耗材,可他每季清点,配的总是比记的多三成——「多出来的部分是什么,我没查到过。」基调:得体,温热,留有余地。",
"tint": "#8a7a5a"
},
{
"signature": "沉默",
"variantId": "wenjian_distant",
"label": "【守夜】你们并肩坐到火熄。温简往你手里塞了个烤热的薯,什么都没说。次日他照旧对你点头,不多一分不少一分——但阿棠偷偷告诉你:「温简昨晚跟沈铎说,你这个人『稳』。」基调:沉默被原样收下——没有人催,没有人问第二遍;温简的亲密度走慢线。",
"tint": "#4b4b5a"
}
], ],
"next": "c2-d7" "next": "c2-d7"
}, },
@@ -436,11 +537,26 @@
"type": "choice", "type": "choice",
"pointId": "C2-06", "pointId": "C2-06",
"prompt": "老鬼掌心的 CW-0317。你怎么办?", "prompt": "老鬼掌心的 CW-0317。你怎么办?",
"dimensions": ["conflict_style", "values"], "dimensions": [
"conflict_style",
"values"
],
"options": [ "options": [
{ "id": "a_public", "label": "(当场)「老鬼。把东西放回去。」声音不大,但足够让半个营地醒来。", "next": "c2-06-ra" }, {
{ "id": "b_private", "label": "(退回暗处,等天亮后私下找他)「昨晚睡得好么?我看见你赏玉了。CW-0317,登记在册的。」", "next": "c2-06-rb" }, "id": "a_public",
{ "id": "c_watch", "label": "(退回暗处,什么都不做)先看清楚再说。他拿它干什么、放不放回去、还有谁知道——现在惊动他,这些就永远不知道了。", "next": "c2-06-rc" } "label": "(当场)「老鬼。把东西放回去。」声音不大,但足够让半个营地醒来。",
"next": "c2-06-ra"
},
{
"id": "b_private",
"label": "(退回暗处,等天亮后私下找他)「昨晚睡得好么?我看见你赏玉了。CW-0317,登记在册的。」",
"next": "c2-06-rb"
},
{
"id": "c_watch",
"label": "(退回暗处,什么都不做)先看清楚再说。他拿它干什么、放不放回去、还有谁知道——现在惊动他,这些就永远不知道了。",
"next": "c2-06-rc"
}
] ]
}, },
{ {
@@ -471,7 +587,7 @@
"id": "c2-d8", "id": "c2-d8",
"type": "dialogue", "type": "dialogue",
"speaker": "旁白", "speaker": "旁白",
"text": "入苍梧的第五个夜晚。你躺在营地,听着这座一千年前的城。它太安静了。没有狗吠,没有更鼓,没有婴儿的夜啼。一座十几万人的城,安静得像屏着一口气。", "text": "此后两日,采集按部就班:城东的瓦砾堆、水门的商队、逐件入册的登记册。【若 wenjian_health = recovering:温简已能全天随队。】【若 wenjian_health = weak:温简的烧也退净了,只是走路还慢半拍。】入苍梧的第五个夜晚,你躺在营地,听着这座一千年前的城。它太安静了。没有狗吠,没有更鼓,没有婴儿的夜啼。一座十几万人的城,安静得像屏着一口气。",
"sprite": "scene", "sprite": "scene",
"next": "c2-d8a" "next": "c2-d8a"
}, },
@@ -479,7 +595,7 @@
"id": "c2-d8a", "id": "c2-d8a",
"type": "dialogue", "type": "dialogue",
"speaker": "旁白", "speaker": "旁白",
"text": "星图上那颗大火星,正在一夜一夜地,走向南方的正中。城在等。你现在知道了。只是还不知道,它在等的那一天,是你们的第几天。", "text": "星图上那颗大火星,正在一夜一夜地,走向南方的正中。城在等。你现在知道了。只是还不知道,它在等的那一天,是你们的第几天。第 6 日黄昏,全队拔营,移驻宫墟外围的断墙下。第 7 日清晨,你们从守墓老人指点的暗门入宫墟。",
"sprite": "scene", "sprite": "scene",
"next": "c2-end" "next": "c2-end"
}, },
+175 -54
View File
@@ -32,7 +32,7 @@
"id": "c3-d1c", "id": "c3-d1c",
"type": "dialogue", "type": "dialogue",
"speaker": "温简", "speaker": "温简",
"text": "(台基顶层,一排青铜鼎与簋成对排列,一旁是盘与盉,再一旁是酒器,最深处是一座空着的玉璧座)不是明器。每一件都用旧了。(低声)他们一遍一遍地,在这个台子上做同一件事。", "text": "【若 flag_hidden_stairs = true:你们没走主台阶。守陵人世代的窄阶贴着排水沟,把你们悄悄送上了台基顶。】【若 flag_tenth_king = true:俑道里那九位先王,在你们身后,按昭穆的次序,继续站他们的岗。】(台基顶层,一排青铜鼎与簋成对排列,一旁是盘与盉,再一旁是酒器,最深处是一座空着的玉璧座)不是明器。每一件都用旧了。(低声)他们一遍一遍地,在这个台子上做同一件事。【若 flag_pit27_figurines = true:刚才 27 号坑那组俑也是,成组,朝南——和阿棠登记的那一家人一样。】【若 flag_palace_crates = true:台基的夯土台面上,有一道道新鲜的箱底压痕和拖拽的辙印,从暗门方向一直拖到空璧座前。鱼牙人的话在这道辙印里落了地:沉箱子进,空箱子出。】【若 flag_mould_rubbing = true:阿棠指着鼎纹:「这纹样我认得!缝里的范,就是这个!」】",
"sprite": "wenjian", "sprite": "wenjian",
"next": "c3-d1d" "next": "c3-d1d"
}, },
@@ -48,12 +48,49 @@
"id": "c3-01", "id": "c3-01",
"type": "choice", "type": "choice",
"pointId": "C3-01", "pointId": "C3-01",
"prompt": "台基上器物分四区:盘与盉(盥洗之器)、爵与尊(酒器)、鼎七件簋六件(盛牺牲黍稷)、最深处一座空着的玉璧座(礼天以璧)。还原仪式流程的先后顺序,并回答:空璧座意味着什么?", "prompt": "台基上器物分四区:盘与盉(盥洗之器)、爵与尊(酒器)、鼎七件簋六件(盛牺牲黍稷)、最深处一座空着的玉璧座(礼天以璧)。分别还原仪式顺序,并判断空璧座意味着什么?",
"dimensions": ["culture"], "dimensions": [
"culture"
],
"puzzle": {
"questions": [
{
"prompt": "器物的礼制顺序是什么?",
"options": [
"沃盥 → 献酒 → 荐食 → 奉璧",
"奉璧 → 献酒 → 荐食 → 沃盥",
"沃盥 → 荐食 → 献酒 → 奉璧"
],
"answer": 0
},
{
"prompt": "七鼎六簋与空璧座合起来说明什么?",
"options": [
"诸侯级国君主祭国祭;奉璧这最后一步始终未完成",
"诸侯级国君主祭国祭;玉璧年久朽坏或被后人取走",
"天子主祭;空璧座是仪式一开始便已献璧的痕迹"
],
"answer": 0
}
],
"hints": [
"一档(分类):「先把器物按用途分成四堆:洗的、喝的、吃的、供的。古人行礼讲究顺序——是先把自己弄干净,还是先把礼物捧上去?」",
"二档(给制度):「鼎和簋是成对用的,数量就是身份:天子九鼎八簋,诸侯七鼎六簋,往下递减。数数这里的鼎簋,你就知道站在台子正中的人是什么级别。顺序上,记住一句话:礼,先洁后献——净手在敬酒之前,敬酒在进馔之前,最贵重的礼物永远最后出场。」",
"三档(给答案):「顺序是:沃盥(盘盉)→ 献酒(爵尊)→ 荐食(鼎簋)→ 奉璧(玉璧座)。鼎七簋六,主祭是国君本人。璧座是空的——这场国祭等了又等,献给天的那件礼始终没拿出来。或者,拿不出来。」"
],
"exitLabel": "请温简复原"
},
"options": [ "options": [
{ "id": "rite_correct", "label": "沃盥 → 献酒 → 荐食 → 奉璧。七鼎六簋,主祭是诸侯级的国君——这是国祭;璧座是空的:献给天的那件礼,始终没拿出来。或者,拿不出来。", "next": "c3-01-ok" }, {
{ "id": "rite_gift_first", "label": "先奉璧,再献酒荐食——最贵重的礼物最先出场,以示诚敬。", "next": "c3-01-miss" }, "id": "rite_correct",
{ "id": "rite_order_wrong", "label": "沃盥 → 荐食 → 献酒 → 奉璧;璧座空,是玉璧年久朽坏、被后人取走了。", "next": "c3-01-miss" } "label": "提交两问推断,请温简复核",
"next": "c3-01-ok"
},
{
"id": "rite-EXIT",
"label": "请温简复原",
"next": "c3-01-miss"
}
] ]
}, },
{ {
@@ -108,7 +145,7 @@
"id": "c3-d2d", "id": "c3-d2d",
"type": "dialogue", "type": "dialogue",
"speaker": "旁白", "speaker": "旁白",
"text": "阿棠那句「一块黑石头,能怎么问」没有说完。只有你能看见:黑石表面,像深水被搅动,浮起了一个画面。那是你的记忆——你在离别之门讲过的那一段:那天的光线,那个人的背影,那件你说过或没说出口的事,此刻在黑石里,一帧一帧,亮给你看。而在队友眼里,黑石纹丝不动。你只是站在石前,出了神。", "text": "阿棠那句「一块黑石头,能怎么问」没有说完。只有你能看见:黑石表面,像深水被搅动。【若 c1_02_signature != silent:浮起的是一个画面——那段你讲给离别之门的记忆:那天的光线,那个人的背影,那件你说过或没说出口的事,此刻在黑石里,一帧一帧,亮给你看。】【若 c1_02_signature = silent:浮起的不是画面,是雾——一大片缓缓流动的雾,雾里什么都没有,又好像什么都有。它在等你能给它的东西。】而在队友眼里,黑石纹丝不动。你只是站在石前,出了神。",
"sprite": "scene", "sprite": "scene",
"next": "c3-d2e" "next": "c3-d2e"
}, },
@@ -132,7 +169,7 @@
"id": "c3-d2f", "id": "c3-d2f",
"type": "dialogue", "type": "dialogue",
"speaker": "馆长", "speaker": "馆长",
"text": "(通讯频道,第一次在古代侧开口,只你一人能听见)C-2049,档案馆接管不了这间屋子。它在用你的记忆做燃料——烧的是你自己的柴。幻境里没有通关,只有照面。它把那天的告别摆在你面前:说完,或者转身,都是回答。门都在。", "text": "(通讯频道,第一次在古代侧开口,只你一人能听见)C-2049,档案馆接管不了这间屋子。它在用你自己的东西点灯——有画面的,看画面;只有雾的,就在雾里站着。幻境里没有通关,只有照面。说完,或者转身,都是回答。门都在。",
"sprite": "curator", "sprite": "curator",
"next": "c3-02" "next": "c3-02"
}, },
@@ -141,27 +178,45 @@
"type": "narration", "type": "narration",
"pointId": "C3-02", "pointId": "C3-02",
"mechanism": "问心幻境", "mechanism": "问心幻境",
"prompt": "黑石里是你的记忆。走进它,把那天没说完的说完。对那个人,那个地方,或者那个自己。说什么、说多少,由你。如果你不想进去——转身出石室,没有人会问原因。", "prompt": "【若 c1_02_signature != silent:黑石里有你讲过的记忆意象。】【若 c1_02_signature = silent:黑石里只有雾,没有人知道你未讲的事。】你可以走进去,在心里完成告别;也可以中途折返,或者直接离开。三种方式门都开。这里不录音、不留新讲述文本。",
"dimensions": ["emotional_stability", "attachment"], "dimensions": [],
"followUp": { "followUp": {
"maxQuestions": 1, "maxQuestions": 0,
"strategy": "Jev 六维缺失检测(五感/人物/时空/情绪),按幻境渲染价值取最高缺口,馆长(通讯频道)与温简(石室外,只回应状态、不问幻境内容)双口吻模板追问一句;「记不清了」为合法回答——馆长:记不清的画面,黑石也放不出来。它和你一样,只有这些了。——够了。温简:那就不说。坐这儿,火边暖和。" "strategy": "本节无语音输入与追问;三个按钮仅记录私密叙事路径。"
},
"followUps": {
"visual": "黑石里的那个画面——你在里面,还是在画面外面看?",
"person": "你眼睛红着。我不问为什么。(递水)要不要坐一会儿?",
"time_place": "手还在抖。——不用回答我什么,我就是递个火。",
"emotion": "如果那句话当年说出口了,你觉得,它会改变什么吗?",
"auditory": "幻境里太安静了。当时有什么声音吗?",
"olfactory_gustatory": "幻境还原不了气味——除非你现在想起来。"
}, },
"followUps": {},
"variantFrom": "C1-02", "variantFrom": "C1-02",
"variants": [ "variants": [
{ "signature": "温暖×永失", "variantId": "sealed_love_tomb", "label": "【情绪配色 · warm_loss】幻境光色暖,记忆场景完整如昨,告别在「封存」中完成——基调:安放。", "tint": "#d4a96a" }, {
{ "signature": "悲恸", "variantId": "war_graves", "label": "【情绪配色 · grief】幻境多雨,低饱和,告别对象始终背对,说完才转身——基调:迟到的送别。", "tint": "#5a5a72" }, "signature": "温暖×永失",
{ "signature": "释然", "variantId": "empty_post_station", "label": "【情绪配色 · release】幻境是清晨的车站(渡口),告别平静,对方先挥手——基调:两讫。", "tint": "#7ba7a0" }, "variantId": "sealed_love_tomb",
{ "signature": "怨怼", "variantId": "desecrated_niche", "label": "【情绪配色 · grievance】幻境里有一件被毁坏(被夺走)的物件,告别的方式是你决定「放下它还是带走它」——基调:讨还与放手之间,两种处置都开门。", "tint": "#8a4b4b" }, "label": "【情绪配色 · warm_loss】幻境光色暖,记忆场景完整如昨,告别在「封存」中完成——基调:安放。",
{ "signature": "沉默", "variantId": "generic_fog", "label": "【通用幻境 · 回退/沉默档】雾中的长堤,远处有一个看不清面目的人影站着,不走,也不回头。你靠近一步,雾散一分。基调:等待被承认——幻境不催,不吓,只是站着等你。", "tint": "#4a4a56" } "tint": "#d4a96a"
},
{
"signature": "悲恸",
"variantId": "war_graves",
"label": "【情绪配色 · grief】幻境多雨,低饱和,告别对象始终背对,说完才转身——基调:迟到的送别。",
"tint": "#5a5a72"
},
{
"signature": "释然",
"variantId": "empty_post_station",
"label": "【情绪配色 · release】幻境是清晨的车站(渡口),告别平静,对方先挥手——基调:两讫。",
"tint": "#7ba7a0"
},
{
"signature": "怨怼",
"variantId": "desecrated_niche",
"label": "【情绪配色 · grievance】幻境里有一件被毁坏(被夺走)的物件,告别的方式是你决定「放下它还是带走它」——基调:讨还与放手之间,两种处置都开门。",
"tint": "#8a4b4b"
},
{
"signature": "沉默",
"variantId": "generic_fog",
"label": "【通用幻境 · 回退/沉默档】雾中的长堤,远处有一个看不清面目的人影站着,不走,也不回头。你靠近一步,雾散一分。基调:等待被承认——幻境不催,不吓,只是站着等你。",
"tint": "#4a4a56"
}
], ],
"next": "c3-cutscene" "next": "c3-cutscene"
}, },
@@ -169,7 +224,7 @@
"id": "c3-cutscene", "id": "c3-cutscene",
"type": "cutscene", "type": "cutscene",
"video": "cutscene-illusion-clear", "video": "cutscene-illusion-clear",
"caption": "只有你能看见——黑石的画面缓缓沉下去,像深水重新凝住。石面最后亮了一下,映出的是你自己现在的脸。然后石室另一侧,一扇先前不存在的石门,无声滑开。甬道口的队友只看见你在黑石前静立了片刻,眨了一下眼,像从很深的地方浮上来。阿棠迎上来:「哎,你脸色白得吓人。站了多久?……里头——(看了看你的表情)算了,你不想说就不说。」馆长:「门开了。它不要你的记忆,它要的是你承认那是你的。这段不登记入档。问心之室里的事,归你。」【若 c3_02_mode = exit_room:你最终没有走进去。石门同样滑开。馆长:「不出来的人,档案馆不催。你来了,看了,走了——这也是告别的一种。」】", "caption": "只有你能看见——黑石的幻象缓缓沉下去,像深水重新凝住。石面最后亮了一下,映出你现在的脸;石室另一侧的石门无声滑开。甬道口的队友只见你静立了片刻。阿棠迎上来:「哎,你脸色白得吓人。要不要坐一会儿?不想说就不说。」馆长:「门开了。你不必向我交代看到了什么。这段不登记入档:室内不录音、不留讲述文本、不入记忆展厅、不作画像输入;仅保留你完成、折返或离开的路径用于剧情接续。问心之室里的事,归你。」【若 c3_02_mode = farewell_partial:馆长:「说了一半的话,也算数。」】【若 c3_02_mode = exit_room:馆长:「不出来的人,档案馆不催。你来了,看了,走了——这也是告别的一种。」】",
"next": "c3-d3" "next": "c3-d3"
}, },
{ {
@@ -184,7 +239,7 @@
"id": "c3-d3a", "id": "c3-d3a",
"type": "dialogue", "type": "dialogue",
"speaker": "旁白", "speaker": "旁白",
"text": "话音未落,前方传来阿棠的喊声和一声闷响。甬道中段,一段夯土塌了:阿棠被滑落的一方石构件压住了小腿,人清醒,挣不脱。同一时刻,甬道另一侧——你们今早刚刚清理出的工作面上,那组七鼎六簋正随着台基的颤动缓缓滑向裂开的地缝。今晨温简清出第一件鼎的腹铭:三百余字,苍梧遗址七十年勘探以来的第一篇长铭。释读只来得及开个头:「……王命举国……以禳……」剩下的话,还刻在另外六件的腹壁上。这组铭文极可能是苍梧人亲手写下的、关于这座城为何走到最后三十天的唯一证词。落缝,就是粉身碎骨。", "text": "话音未落,前方传来阿棠的喊声和一声闷响。甬道中段的夯土塌了一角,阿棠小腿被擦伤,退到安全线后仍疼得站不稳。沈铎和受过救护训练的老鬼已在安全线内接管伤情,确认通道隔离、阿棠意识清醒且无需你协助;二人负责后续救护与撤离。沈铎明确说:「人这边够了。别再进工作面。」",
"sprite": "scene", "sprite": "scene",
"next": "c3-d3b" "next": "c3-d3b"
}, },
@@ -192,7 +247,7 @@
"id": "c3-d3b", "id": "c3-d3b",
"type": "dialogue", "type": "dialogue",
"speaker": "旁白", "speaker": "旁白",
"text": "头顶,第二声闷响在逼近。沈铎扑向阿棠,一个人的力气撼不动石构件,他吼:「来人!」温简已经扑向鼎簋那边,用身体抵住滑动的器座,回头看你的那一眼没有求助——是托付。两边都需要你。你只能先到一边。", "text": "安全线外的工作面上,七鼎六簋随着台基震动滑向裂缝,任何人都不准靠近。接应人员在安全线内备好急救器材,两人持续陪护阿棠,待结构警戒解除再送她回营。今晨温简清出第一件鼎的腹铭,三百余字,释读只来得及开头:「……王命举国……以禳……」剩下的话还在器物腹壁上。安全线内,阿棠的伤情交接和队员安置,与远距影像和方位记录,都需要通讯和记录人手;你不能同时跟进两边。第二声闷响逼近,现场处置权只限安全区内。",
"sprite": "scene", "sprite": "scene",
"next": "c3-03" "next": "c3-03"
}, },
@@ -201,18 +256,32 @@
"type": "choice", "type": "choice",
"pointId": "C3-03", "pointId": "C3-03",
"prompt": "塌方将至:救人,还是救那句话?", "prompt": "塌方将至:救人,还是救那句话?",
"dimensions": ["values", "emotional_stability"], "dimensions": [
"values"
],
"options": [ "options": [
{ "id": "a_person", "label": "扑向阿棠。眼前的人是活的,活的只有一个。跟沈铎合力抬构件。", "next": "c3-03-ra" }, {
{ "id": "b_relic", "label": "扑向鼎簋。那组铭文是整座城的人留在世上的最后一句话——他们也都是人,只剩这一次开口的机会。阿棠那边,有沈铎。", "next": "c3-03-rb" }, "id": "a_person",
{ "id": "c_command", "label": "(吼)「沈铎顶构件,阿棠自己抽腿!温简让开,鼎簋用撬杆别住——都不许用蛮力!」(指挥位:不亲手救任何一边,赌两边都能用对方法撑住)", "next": "c3-03-rc" } "label": "留在阿棠这边。接手伤情记录和撤离联络,替她把疼痛与需求传给接应人员;文物影像交给温简独自处理。",
"next": "c3-03-ra"
},
{
"id": "b_relic",
"label": "留在温简这边。在安全线内核对远距影像、方位与已抄铭文;阿棠已由沈铎和接应人员稳定保护。",
"next": "c3-03-rb"
},
{
"id": "c_command",
"label": "调配两边的记录任务。让沈铎确认交接、温简退回安全线,先保全已有资料,等待专业排险。",
"next": "c3-03-rc"
}
] ]
}, },
{ {
"id": "c3-03-ra", "id": "c3-03-ra",
"type": "dialogue", "type": "dialogue",
"speaker": "旁白", "speaker": "旁白",
"text": "阿棠脱困,小腿挫伤。腹铭最长的那件鼎坠入地缝,闷响之后是死寂。温简在裂缝边跪了很久,没说一个字。当夜他把今晨抄下的铭文残句抄了一遍又一遍,每一遍都停在同一个地方——后半段,随那件鼎没了。", "text": "你接下阿棠的伤情记录。她反复说脚麻,你把这句传给接应人员;她随后被固定送回营地,夜里疼醒三次,次日复查为小腿骨裂。温简独自在安全线内完成远距拍摄,但影像里最长腹铭的背面被尘雾遮住;那件鼎随后坠入地缝。器物位置和已抄残句留下了,后半段字再也拍不到。无人因你留在阿棠身边而耽误救护。",
"sprite": "scene", "sprite": "scene",
"next": "c3-d3c" "next": "c3-d3c"
}, },
@@ -220,23 +289,23 @@
"id": "c3-03-rb", "id": "c3-03-rb",
"type": "dialogue", "type": "dialogue",
"speaker": "旁白", "speaker": "旁白",
"text": "铭文全组保住。阿棠脱困晚了四分钟,小腿骨裂,撤离时沈铎背着她。她疼得满头汗还冲你笑:「哎,选得对,那句话比我金贵——别告诉沈铎我这么说。」但她夜里疼醒三次。", "text": "你和温简在安全线内核对影像;第二声闷响前,整组器物的朝向和腹铭均已留存清晰影像。靠外一件鼎还是坠入地缝,没有人靠近去抢。阿棠由接应人员送回营地,夜里疼醒三次,次日复查同样是小腿骨裂。她没有评价你的去向,只问你:「字拍清楚了吗?」你说拍清了。她闭上眼,等医生继续处理。",
"sprite": "scene", "sprite": "scene",
"next": "c3-d3c" "next": "c3-d3c"
}, },
{ {
"id": "c3-03-rc", "id": "c3-03-rc",
"type": "dialogue", "type": "dialogue",
"speaker": "沈铎", "speaker": "旁白",
"text": "指挥奏效一半:阿棠按指令抽腿脱困,扭伤;撬杆别住了簋,但最外侧的鼎还是滑了——被温简扑身抱住,鼎保住了,他的肩膀砸在器座上,抬不起来。两边都保住了「大部分」,两边都付了伤。沈铎事后复盘:「处置正确。下次先喊我的编号。」阿棠嘀咕:「正确是正确,就是听着不像人话。」", "text": "沈铎确认伤情交接,温简把现有记录板带到安全线内;你们三人从既有图像里复核方位,不再靠近。第二声闷响后,靠外三件器物(含最长腹铭那件)坠入地缝,内侧四件留在原位。阿棠同样由接应人员护送,次日复查小腿骨裂。温简翻着今晨抄到的第七行,停住笔:「后半段没了。」沈铎只记下损失与现存证据,没给你的调配下评语。",
"sprite": "shenduo", "sprite": "scene",
"next": "c3-d3c" "next": "c3-d3c"
}, },
{ {
"id": "c3-d3c", "id": "c3-d3c",
"type": "dialogue", "type": "dialogue",
"speaker": "旁白", "speaker": "旁白",
"text": "当夜,温简继续释读已抄下的铭文残句。读着读着,他停住了——有一行字,让围过来的全队都沉默了。他合上记录板,没念出来。", "text": "当夜,温简继续释读已抄下的铭文残句。读着读着,他停住了——有一行字,让围过来的全队都沉默了。他合上记录板,没念出来。【若 c3_03_choice != b_relic:后半段影像缺失;他今晨只抄到第七行。】",
"sprite": "scene", "sprite": "scene",
"next": "c3-d4" "next": "c3-d4"
}, },
@@ -277,11 +346,26 @@
"type": "choice", "type": "choice",
"pointId": "C3-04", "pointId": "C3-04",
"prompt": "作证、求情,还是沉默?", "prompt": "作证、求情,还是沉默?",
"dimensions": ["conflict_style", "values"], "dimensions": [
"conflict_style",
"values"
],
"options": [ "options": [
{ "id": "a_testify", "label": "「我作证。我看到的,我照实说。」(包括上次玉佩的事——如果你知道的话。规程就是规程。)", "next": "c3-04-ra" }, {
{ "id": "b_plead", "label": "「东西已经追回了,人也在队里看着。能不能……队内存档,以观后效?」(替老鬼求情:处分但不上报,给他一条回头的路)", "next": "c3-04-rb" }, "id": "a_testify",
{ "id": "c_silent", "label": "「我是采集员,不是裁判。你们定。」(沉默/回避:不阻止上报,也不添一根手指)", "next": "c3-04-rc" } "label": "「我作证。我看到的,我照实说。」(包括上次玉佩的事——如果你知道的话。规程就是规程。)",
"next": "c3-04-ra"
},
{
"id": "b_plead",
"label": "「东西已经追回了,人也在队里看着。能不能……队内存档,以观后效?」(替老鬼求情:处分但不上报,给他一条回头的路)",
"next": "c3-04-rb"
},
{
"id": "c_silent",
"label": "「我是采集员,不是裁判。你们定。」(沉默/回避:不阻止上报,也不添一根手指)",
"next": "c3-04-rc"
}
] ]
}, },
{ {
@@ -345,11 +429,25 @@
"type": "choice", "type": "choice",
"pointId": "C3-05", "pointId": "C3-05",
"prompt": "阿棠把主绳递向你。接不接?", "prompt": "阿棠把主绳递向你。接不接?",
"dimensions": ["mate_preference"], "dimensions": [
"mate_preference"
],
"options": [ "options": [
{ "id": "a_accept", "label": "(接过绳包)「好。下去的时候,我殿后。」", "next": "c3-05-ra" }, {
{ "id": "b_protocol", "label": "(把绳包递给沈铎)「规矩是规矩。绳子归队长,但下去那天,我挨着阿棠走。」", "next": "c3-05-rb" }, "id": "a_accept",
{ "id": "c_decline", "label": "(不接,蹲下来看着阿棠)「绳子我不拿。你的命别交给任何人——包括我。到时候各走各的,谁都别为谁停下。」", "next": "c3-05-rc" } "label": "(接过绳包)「好。下去的时候,我殿后。」",
"next": "c3-05-ra"
},
{
"id": "b_protocol",
"label": "(把绳包递给沈铎)「规矩是规矩。绳子归队长,但下去那天,我挨着阿棠走。」",
"next": "c3-05-rb"
},
{
"id": "c_decline",
"label": "(不接,蹲下来看着阿棠)「绳包按规程走。但托付这事我不接——你的命不该押在任何一个人身上,包括我。撤离是我的活:到那天,我在你前面下,在下面接你。这个不用托付,是分工。」",
"next": "c3-05-rc"
}
] ]
}, },
{ {
@@ -372,7 +470,7 @@
"id": "c3-05-rc", "id": "c3-05-rc",
"type": "dialogue", "type": "dialogue",
"speaker": "阿棠", "speaker": "阿棠",
"text": "阿棠愣了三秒,忽然大笑,笑得咳嗽:「行!各走各的!——你可别后悔。」她把绳包自己背上了,一瘸一拐走开,又回头:「喂。真到那时候……我还是会等你的。就一会儿。」温简在她走后轻声说:「她等过别人。等到的次数不多。」", "text": "阿棠愣了三秒,忽然大笑,笑得咳嗽:「行!——门都让你焊死了,还留个窗。」她把绳包抛还给了沈铎(规程),一瘸一拐走开,又回头:「喂。真到那天……在上面的时候,我还是会等你的。就一会儿。」温简在她走后轻声说:「她等过别人。等到的次数不多。」",
"sprite": "atang", "sprite": "atang",
"next": "c3-d6" "next": "c3-d6"
}, },
@@ -421,13 +519,36 @@
"type": "choice", "type": "choice",
"pointId": "C3-06", "pointId": "C3-06",
"prompt": "档案馆的隐瞒,你打算怎么办?(叫醒队友的话——叫醒谁,就向谁倾诉。)", "prompt": "档案馆的隐瞒,你打算怎么办?(叫醒队友的话——叫醒谁,就向谁倾诉。)",
"dimensions": ["attachment", "mate_preference"], "dimensions": [
"attachment",
"mate_preference"
],
"options": [ "options": [
{ "id": "a_confront", "label": "(按下通讯键,直接问馆长)「刚才那段通话。问心石是你们放的?璧是什么?——现在,跟我说实话。」", "next": "c3-06-ra" }, {
{ "id": "b_conceal", "label": "(退回营地,躺平,盯着黑暗)按下不表。听到的每一个字都记住。在弄清楚「他们为什么要瞒」之前,装作没听过。", "next": "c3-06-rb" }, "id": "a_confront",
{ "id": "c_confide_shenduo", "label": "(叫醒沈铎,把听到的一切说出来)", "next": "c3-06-rc1" }, "label": "(按下通讯键,直接问馆长)「刚才那段通话。问心石是你们放的?璧是什么?——现在,跟我说实话。」",
{ "id": "c_confide_atang", "label": "(叫醒阿棠,把听到的一切说出来)", "next": "c3-06-rc2" }, "next": "c3-06-ra"
{ "id": "c_confide_wenjian", "label": "(叫醒温简,把听到的一切说出来)【若 flag_wenjian_secret = true:温简说过,记录板上报的内容比你多一行。他可能知道些什么。】", "next": "c3-06-rc3" } },
{
"id": "b_conceal",
"label": "(退回营地,躺平,盯着黑暗)按下不表。听到的每一个字都记住。在弄清楚「他们为什么要瞒」之前,装作没听过。",
"next": "c3-06-rb"
},
{
"id": "c_confide_shenduo",
"label": "(叫醒沈铎,把听到的一切说出来)",
"next": "c3-06-rc1"
},
{
"id": "c_confide_atang",
"label": "(叫醒阿棠,把听到的一切说出来)",
"next": "c3-06-rc2"
},
{
"id": "c_confide_wenjian",
"label": "(叫醒温简,把听到的一切说出来)【若 flag_wenjian_secret = true:温简说过,档案馆配给板子的耗材比记录量多三成。他可能知道些什么。】",
"next": "c3-06-rc3"
}
] ]
}, },
{ {
@@ -474,7 +595,7 @@
"id": "c3-d7", "id": "c3-d7",
"type": "dialogue", "type": "dialogue",
"speaker": "旁白", "speaker": "旁白",
"text": "撤离的晨光里,苍梧城在身后一点点退远。大火星这几夜已经挨近了南方的正中——火中,而后城安。你数过了:按星位推算,那一天,是九天后。而你们的回程,定在八天后。档案馆让你们早走一天。(第 1 章完——Act 2「裂缝」,敬请期待。)", "text": "撤离的晨光里,苍梧城在身后一点点退远。大火星这几夜已经挨近了南方的正中——火中,而后城安。你数过了:按星位推算,那一天,是九天后。而你们的回程,原定八天后。【若 c3_06_confidant = shenduo:沈铎昨夜已下令撤离提前——回程,改在七天后。】档案馆原定让你们早走一天;【若 c3_06_confidant = shenduo:沈铎又把回程提前了一日。】(第 1 章完——Act 2「裂缝」,敬请期待。)",
"sprite": "scene", "sprite": "scene",
"next": "c3-end" "next": "c3-end"
}, },
+19 -3
View File
@@ -1,6 +1,6 @@
# 设计:自适应支线插入引擎 # 设计:自适应支线插入引擎
> 状态:设计定稿,待实现。关联:docs/story-framework.md §6(路由规则)、 > 状态:已实现(2026-10-03),`pnpm verify` 包含主线与自适应支线验证。关联:docs/story-framework.md §6(路由规则)、
> docs/script/adaptive-branches.md(支线剧本与三张总表)、ADR-0003(Jev 置信度自适应)。 > docs/script/adaptive-branches.md(支线剧本与三张总表)、ADR-0003(Jev 置信度自适应)。
> 读者:实现此功能的工程 agent。 > 读者:实现此功能的工程 agent。
@@ -36,9 +36,12 @@ chapterEnd(第 N 章结束)
2. 排序:按「维度权重 × 置信度缺口 × 已测稀缺度」取首—— 2. 排序:按「维度权重 × 置信度缺口 × 已测稀缺度」取首——
维度权重:attachment > values > conflict_style > emotional_stability > culture 维度权重:attachment > values > conflict_style > emotional_stability > culture
(见支线册 §0.5);缺口 = 0.6 − confidence;稀缺度 = 该维度已被采样的选择点数越少越优先; (见支线册 §0.5);缺口 = 0.6 − confidence;稀缺度 = 该维度已被采样的选择点数越少越优先;
3. 选皮:该维度的支线 A(挂点 1)未用过→A,用过→B(挂点 2);A、B 都用尽仍低置信 3. 选皮:变体与挂点**按章绑定**(第 2 章=变体 A/挂点 1,第 3 章=变体 B/挂点 2),
不跨章搬运(与支线册 §0「变体皮肤规则」一致——变体场景与该章主线进度耦合)。
同维度重复低置信:第 2 章插 A、第 3 章插 B(强制换皮);A、B 都用尽仍低置信
→ 记录 `evidence_insufficient` 标记,不插入(交 Act 2); → 记录 `evidence_insufficient` 标记,不插入(交 Act 2);
4. 每章至多 1 条;挂点被占则顺延到该章另一挂点,两挂点都被占则不插入; 4. 每章至多 1 条;每章只有一个挂点,本章已有插入则不再插入,顺延到下一章的
挂点(用其对应变体),无挂点可用则不插入;
5. 时长红线:插入后该章预估时长 ≤ 45 分钟(支线 5-8 分钟 + 主线 25-35 分钟)。 5. 时长红线:插入后该章预估时长 ≤ 45 分钟(支线 5-8 分钟 + 主线 25-35 分钟)。
## 4. 数据模型 ## 4. 数据模型
@@ -97,3 +100,16 @@ chapterEnd(第 N 章结束)
chapter2/3.json 落定,支线册的 §2-4 后/§3-2 前是剧本坐标); chapter2/3.json 落定,支线册的 §2-4 后/§3-2 前是剧本坐标);
- 支线中的 narration 讲述点:本期支线册均为按钮式选择点,无 V 类点, - 支线中的 narration 讲述点:本期支线册均为按钮式选择点,无 V 类点,
若后续支线加讲述点,追问流直接复用现有机制,无新增设计。 若后续支线加讲述点,追问流直接复用现有机制,无新增设计。
## 9. 实现裁决与验证
- 挂点按剧情坐标落定:A 在 `chapter2/c2-branch-anchor`(C2-03 三条后续汇合后、观星台前),B 在 `chapter3/c3-d1b`(台基下、登台前)。原示例 `c2-d4f` 位于路线裁断前,不作挂点。
- A/B 绑定章节,不跨挂点搬运。第三章首次补测某维度也使用 B;当前章槽已占用则不叠加,下一章重新打分决定。
- 优先级采用权重 5/4/3/2/1 × `(0.6-confidence)` × `1/(1+独立测点数)`;choice/narration 为有效测点,追问与路由事件不重复计数。主线时长默认保守取 35 分钟,支线按上限 8 分钟估算。
- 文化兜底检查刚完成章的主线解谜是否已有事件,不检查目标章尚未发生的解谜,否则文化支线会永远无法触发。
- 支线册少量时序措辞做了衔接修正:官牙发生在安门内登台前;漏刻不预先引用尚未复原的星图;分药后的 D 路径同样遵循 `wenjian_health=weak` 留营规则。测量维度和变量枚举不变。
- `advance()` 返回 Promise,调用方须 await;章末防重复提交,HTTP 请求 15 秒超时。无 key 的自动 Mock 不参与插入决策,显式注入 Mock 可用于测试。
- 快照保存支线 ID 摘要与活动支线;恢复先合并节点索引,再定位断点。路由事件先于快照落盘,恢复时可从本 session 事件补回计划。重玩清除索引与计划,不改旧事件。
- 共 17 个支线选择点(两问解谜各计一个测点)。计时点记录耗时和超时,解谜记录两问答案、提示档数、是否解出,选择事件保存实际条件渲染后的文案。风声音效、缝宽演出和阅读/改选遥测未实现;当前 UI 点选即提交,不伪造这些信号。
- `pnpm exec tsx scripts/build-adaptive-branches.ts` 从定稿支线册重生成十个静态包;运行时不解析 Markdown。
- `scripts/verify-adaptive.ts` 覆盖决策矩阵、全部选项与超时/跳过、温简双状态、逐节点恢复、五维三章 A→B、事件/flags、重复点击与刷新、无 key/无效评分/网络故障、replay 会话隔离,由 `scripts/verify.ts` 统一执行。
+369 -243
View File
@@ -66,9 +66,15 @@
> 你盯着那行字看了很久。窗外雨声很大,像很多人在很远的地方说话。 > 你盯着那行字看了很久。窗外雨声很大,像很多人在很远的地方说话。
【工程】纯演出段落,无交互。结尾淡出,档案馆标志浮现,进入 §1-2。 【工程】纯演出段落,无交互。结尾淡出,档案馆标志浮现,进入 §1-2。
【备注】本段承担 ADR-0004 的「一次告知」落地:邮件点击确认页文案为 【备注】本段承担 ADR-0004 的「一次告知」落地:邮件点击确认页
「你已知晓:剧情选择将用于为你推荐伴侣。按本心选,没有对错。」 (入馆告知,与产品同意门同源同文案)共四条——
此后全剧不再出现同类提示。 「你在剧情中的选择与语音讲述,将用于为你推荐伴侣。按本心选,
没有对错。」「语音只用于转写文字与提取语气特征(语速、停顿等),
提取后即焚——不保存音频,不含声纹,不可还原你的声音。」
「你的记忆与经历仅作模型输入,永远不会原样展示给其他用户;
记忆展厅向匹配对象开放,需你逐次明确授权。」「历史选择不可涂改;
想重来可以重玩章节——重玩产生新轨迹,旧轨迹原样保留。」
告知范围覆盖 ADR-0004 的语音特征条款;此后全剧不再出现同类提示。
--- ---
@@ -86,9 +92,10 @@
**人怎么活过**——一座城消亡前,城里的人吃什么、怕什么、爱什么、 **人怎么活过**——一座城消亡前,城里的人吃什么、怕什么、爱什么、
最后那几天在做什么。这些东西,仪器采不到,只有人能采集。 最后那几天在做什么。这些东西,仪器采不到,只有人能采集。
**馆长**:所以我们需要的采集员,必须自己也是"活过的"。穿越要烧燃料, **馆长**:所以我们需要的采集员,必须自己也是"活过的"。穿越要烧燃料。
而燃料只有一种——你的真实人生。烧掉的记忆不会消失,它们会存在 燃料这东西认两种:你讲出来的故事,和你在讲述点门前站过的那阵
档案馆的展厅里,这是后话。 沉默——讲,是给;不说,把那阵沉默原样交给它,也是给。讲出来的
记忆不会消失,它们会存在档案馆的展厅里,这是后话。
**馆长**:在点燃第一段燃料之前,先做登记。不是审查,是锚定。 **馆长**:在点燃第一段燃料之前,先做登记。不是审查,是锚定。
穿越者需要一个"回来的坐标"——你是谁,从哪里出发。登记得越实, 穿越者需要一个"回来的坐标"——你是谁,从哪里出发。登记得越实,
@@ -133,7 +140,9 @@
**馆长**:档案馆的穿越不是技术,是仪式。门认得的不是脸,是分量。 **馆长**:档案馆的穿越不是技术,是仪式。门认得的不是脸,是分量。
**馆长**:这扇叫离别之门。每个采集员的第一次穿越,都从它开始。 **馆长**:这扇叫离别之门。每个采集员的第一次穿越,都从它开始。
它不开锁,不收凭证——它要一件祭品:**一段真实的离别**。 它不开锁,不收凭证——它认一次交托:**一段真实的离别**。
讲出来是交托,把那阵沉默原样交给它,也是交托;不提交记忆,
门也照样启动。
**馆长**:你生命里那次真正的告别。一个人,一个地方,或者一个 **馆长**:你生命里那次真正的告别。一个人,一个地方,或者一个
再也回不去的自己。讲给它听。讲多少,由你定。 再也回不去的自己。讲给它听。讲多少,由你定。
@@ -158,15 +167,19 @@
### 即时回报(讲述结束后 30 秒内必给) ### 即时回报(讲述结束后 30 秒内必给)
**馆长**(讲述后):门开了。你看——它要的不是故事讲得好不好, **馆长**(讲述后):门开了。你看——它要的不是故事讲得好不好,
是故事真不真。 是人真不真。
【若玩家沉默:】**馆长**:门开了。沉默也是档案的一种质地—— 【若玩家沉默:】**馆长**:门开了。沉默也是档案的一种质地——
它认得站在它面前的人,不一定需要言语。 它认得站在它面前的人,不一定需要言语。
**馆长**:你的记忆已经收进档案馆展厅,编号 C-2049-壹。 【若玩家讲述:】**馆长**:你的记忆已经收进档案馆展厅,编号 C-2049-壹。
它现在是你的了:随时可以回看,永远不会被任何人看见,除非你 它现在是你的了:随时可以回看,永远不会被任何人看见,除非你
亲手授权——**展厅,以及往后路上任何由它生出的幻境,都只有你 亲手授权——**展厅,以及往后路上任何由它生出的幻境,都只有你
能进入。在旁人眼里,你只是安静地站了一会儿。** 能进入。在旁人眼里,你只是安静地站了一会儿。**
【若玩家沉默:】**馆长**:展厅为你留了一页空档,编号 C-2049-壹——
此刻还没有字,哪天你想说了,它随时接着。**展厅,以及往后路上
任何由它生出的幻境,都只有你能进入。在旁人眼里,你只是安静地
站了一会儿。**
> 木门无声地开了。门后不是房间,是星光和风声。 > 木门无声地开了。门后不是房间,是星光和风声。
@@ -178,6 +191,7 @@
| 悲恸 | **「战乱乱冢」** | 门后是荒野,低低的土丘一座挨一座,没有碑,只有削尖的木桩。风把灰白的纸钱碎屑贴在脚踝上。远处有乌鸦起落。天色是黄昏将尽的那种灰蓝。基调:**来不及告别**——所有话都没说出口,所有名字都没来得及刻上 | | 悲恸 | **「战乱乱冢」** | 门后是荒野,低低的土丘一座挨一座,没有碑,只有削尖的木桩。风把灰白的纸钱碎屑贴在脚踝上。远处有乌鸦起落。天色是黄昏将尽的那种灰蓝。基调:**来不及告别**——所有话都没说出口,所有名字都没来得及刻上 |
| 释然 | **「远行者的空驿」** | 门后是一座山间驿站,晨光,桌椅收拾得干干净净,灶膛里的灰还是温的,人已经走了。墙上挂着一件没带走的旧斗篷,桌上压着半张写了又划掉的字条。基调:**告别已经完成**——走得干净,留得体面,剩下的路各走各的 | | 释然 | **「远行者的空驿」** | 门后是一座山间驿站,晨光,桌椅收拾得干干净净,灶膛里的灰还是温的,人已经走了。墙上挂着一件没带走的旧斗篷,桌上压着半张写了又划掉的字条。基调:**告别已经完成**——走得干净,留得体面,剩下的路各走各的 |
| 怨怼 | **「被凿毁的合葬龛」** | 门后是崖壁上的佛龛式墓龛,本该并坐的两尊石像,其中一尊被凿去了面目,凿痕新鲜得刺眼。香烛还燃着,蜡泪流到龛外。基调:**被亏欠**——有人毁了约,而另一个人连讨说法的对象都没有 | | 怨怼 | **「被凿毁的合葬龛」** | 门后是崖壁上的佛龛式墓龛,本该并坐的两尊石像,其中一尊被凿去了面目,凿痕新鲜得刺眼。香烛还燃着,蜡泪流到龛外。基调:**被亏欠**——有人毁了约,而另一个人连讨说法的对象都没有 |
| 沉默 | **「留白的长廊」** | 门后是一条望不见头的长廊,两壁空无一物,像还没落笔的纸。风从走廊尽头吹来,带着很远的草香;走出去很远,身后的门还开着一条缝。基调:**留白**——没说出口的部分,也被好好收着 |
### 选项设计说明(C1-02) ### 选项设计说明(C1-02)
@@ -195,7 +209,8 @@
- 变量:`c1_02_signature ∈ {warm_loss, grief, release, grievance, silent}` - 变量:`c1_02_signature ∈ {warm_loss, grief, release, grievance, silent}`
(Jev 情绪签名,驱动通道 1 后续全部墓道/幻境配色)。 (Jev 情绪签名,驱动通道 1 后续全部墓道/幻境配色)。
- 变量:`c1_02_memory_id`(记忆展厅陈列编号,供 C3-02 幻境素材调用)。 - 变量:`c1_02_memory_id`(记忆展厅陈列编号,供 C3-02 幻境素材调用;
沉默玩家为「空档页」,C3-02 幻境走通用雾景,见 §3-3)。
- 固定跳转 §1-4。 - 固定跳转 §1-4。
--- ---
@@ -353,12 +368,18 @@
> **谜面定稿**:宀(屋顶)之下,一个女子安坐的形象。 > **谜面定稿**:宀(屋顶)之下,一个女子安坐的形象。
> 玩家需回答:这是什么字,它在碑额上意味着什么。 > 玩家需回答:这是什么字,它在碑额上意味着什么。
> 谜面同时给出两条旁证(与辨字互相印证,缺一不可):
> 其一,碑形——圆首、无碑穿、无榫、下无龟趺座,不合墓碑与
> 墓表规制;其二,出土地点——此碑出土于苍梧北门外官道旁,
> 碑面朝城。
> >
> 【美术注】字形按「安」的西周金文绘制:宀下为跪坐女子形, > 【美术注】字形按「安」的西周金文绘制:宀下为跪坐女子形,
> 取象"室中有女,安坐不惊"。拓片做漫漶处理,仅此字清晰。 > 取象"室中有女,安坐不惊"。拓片做漫漶处理,仅此字清晰。
> 谜面界面需同时呈现碑形线图与出土地点示意。
**谜底**:**「安」**。甲骨文「安」从宀从女,女子处室中为安, **谜底**:**「安」**。甲骨文「安」从宀从女,女子处室中为安,
本义是安定、安居。碑额刻「安」,说明此碑不是墓碑,而是 本义是安定、安居。碑额刻「安」,加上碑形不合墓制、又立在
城门外官道旁、碑面朝城——三条证据同向:此碑不是墓碑,而是
**镇城碑/安民碑**——苍梧人立此碑祈愿城邑安宁,碑所镇的方向 **镇城碑/安民碑**——苍梧人立此碑祈愿城邑安宁,碑所镇的方向
(碑额朝向)即古城的安门所在。 (碑额朝向)即古城的安门所在。
@@ -370,8 +391,9 @@
2. **二档(给构件含义)**:「屋顶下的形象是一个女子安坐。 2. **二档(给构件含义)**:「屋顶下的形象是一个女子安坐。
造字的古人认为:室中有女,安坐不惊,即是安定。这个字的本义 造字的古人认为:室中有女,安坐不惊,即是安定。这个字的本义
和它的写法,到今天几乎没变。」 和它的写法,到今天几乎没变。」
3. **三档(给答案范围)**:「这是『安』字。想想碑额为什么刻它—— 3. **三档(给答案范围)**:「这是『安』字。再看旁证:碑形不是
一座城把『安』刻在碑上立起来,它是在标记什么?」 墓碑的形制,又立在城门外的官道旁、碑面朝城——一座城把
『安』刻在碑上立起来,它是在标记什么?」
> 玩家作答(选项式:墓碑 / 镇城安民碑 / 记功碑 / 界碑), > 玩家作答(选项式:墓碑 / 镇城安民碑 / 记功碑 / 界碑),
> 答对或提示用尽后进入下文。 > 答对或提示用尽后进入下文。
@@ -451,7 +473,8 @@
- 变量:`c1_06_landing ∈ {a_safe, b_precise}`。 - 变量:`c1_06_landing ∈ {a_safe, b_precise}`。
- §2-1 开场文本按变量切换(城外官道 / 城内西市),§2-2 汇合, - §2-1 开场文本按变量切换(城外官道 / 城内西市),§2-2 汇合,
主线事件自此完全一致。 主线事件自此完全一致。
- 固定跳转 §2-1(第 2 章)。 - 固定跳转 §2-1(第 2 章)。选择点后先自动播 §1-8 章末演出,
再入第 2 章(章末插播,非直接跳过演出)。
--- ---
@@ -523,11 +546,16 @@ score。对 `confidence < 0.6` 的维度,在第 2 章插入对应支线
【场景】水门内侧废宅改的营地。晨光斜照进破窗。中央火塘上坐着 【场景】水门内侧废宅改的营地。晨光斜照进破窗。中央火塘上坐着
药罐。温简靠墙坐着,脸色发白——昨夜疟疾初发,烧退了又起。 药罐。温简靠墙坐着,脸色发白——昨夜疟疾初发,烧退了又起。
老鬼蹲在地上清点物资箱,阿棠在给他递水。 老鬼蹲在地上清点物资箱,阿棠在给他递水。
【药理设定(编剧与工程共读,选项文案必须与此一致)】:
防瘴药粉**一份=一名健康人的七日预防量**;温简的疟疾已发,
**足量治疗需两份连服,一日不可断**——给他一份压不住病,
给他两份,队里的预防就一份不剩。
【正文】 【正文】
**老鬼**(举起一个瓷瓶):防瘴的药粉,满打满算,**两人份**。 **老鬼**(举起一个瓷瓶):防瘴的药粉,满打满算,**两人份**——
这东西是定数,档案馆给的量抠得死,多一两都没有。 两个健康人七天的预防量。真要给病号治疟疾,两份连服才压得住,
一分都剩不下。这东西是定数,档案馆给的量抠得死,多一两都没有。
**沈铎**:需要它的是三个人。温简病着,最弱;我明天带外勤组进东城区, **沈铎**:需要它的是三个人。温简病着,最弱;我明天带外勤组进东城区,
暴露面最大;老鬼管物资,断炊的风险在他身上。 暴露面最大;老鬼管物资,断炊的风险在他身上。
@@ -542,53 +570,61 @@ score。对 `confidence < 0.6` 的维度,在第 2 章插入对应支线
你说是不是,新人? 你说是不是,新人?
> 【三个人都看着你。沈铎没表态——按规程,资源分配僵局由采集员裁断。 > 【三个人都看着你。沈铎没表态——按规程,资源分配僵局由采集员裁断。
> 火塘上的药罐咕嘟作响。】 > 温简靠墙闭着眼,额头的汗一条一条往下走。火塘上的药罐咕嘟作响——
> 谁都听得出来:这罐药和这场裁断,都在等,也都等不起。】
> 【选项】 > 【选项】
> >
> **A.「先给温简。病着的人等不起,外勤和补给可以想办法。」** > **A.「两份都给温简,连服压病。我们仨穿蓑衣、烧艾草,**
> **土法顶着。」**
> (治疗优先:病的窗口不等人,预防的缺口用笨办法补)
> >
> **B.「按暴露风险分:队长和老鬼。温简留在营地,反而最不需要出门。」** > **B.「按暴露风险分:队长和老鬼一人一份。温简留在营地**
> **硬扛——他不出门,感染的面最小。」**
> (预防优先:病靠扛,瘴靠防)
> >
> **C.「两人份的药,三个人分——匀一匀,每人少点,先把今天撑过去。」** > **C.「两人份的药,三个人分——匀一匀,先把这几天撑过去。」**
> (药剂常识上可行,但每人都达不到足量预防) > (药粉可以分,但分完谁的量都不够:温简的治疗量不足,
> 两份预防也各打了折扣)
> >
> **D.「我不裁。规矩该事先定好——现在让全队一起议,议出个章程来,** > **D.「我先不裁。这事得全队一起议——议出个章程,往后都照**
> **往后都照章程办。」** > **章程办。」**
> (程序优先:章程要议,半个上午就没了;温简的烧等不等得起
> 议事,议事的人心里都有数)
### 各选项后续(短演出,均不致命) ### 各选项后续(短演出,均不致命)
- **A**:温简当日退烧,次日能下床。老鬼笑嘻嘻记下"新人讲人情"。 - **A**:两份药当日开始连服,温简当日退烧,次日能下床。老鬼笑嘻嘻
次日沈铎外勤淋了瘴雨,低烧半日——他瞒着没说,阿棠后来发现, 记下"新人讲人情"。次日沈铎外勤淋了瘴雨,队里已无预防的药,
跟老鬼大吵一架(背景演出,不需玩家介入)。 低烧半日——他瞒着没说,阿棠后来发现,跟老鬼大吵一架
- **B**:温简烧到第三日才退,虚弱期按沈铎命令留营待命 (背景演出,不需玩家介入)。
(状态贯穿规则见「分支后果」)。沈铎与老鬼无恙。老鬼对你明显热络。 - **B**:温简没等到药,烧又熬了一整日,到第 3 日晌午(§2-5 当日)
- **C**:三人皆未足量。温简病情拖延,沈铎外勤染瘴轻微发热, 才退净,虚弱期按沈铎命令留营待命(状态贯穿规则见「分支后果」
但无人倒下。老鬼点评:"和稀泥是和稀泥,不过稀泥也能糊墙。" 与附录 D 日程)。沈铎与老鬼靠预防份无恙。老鬼对你明显热络。
- **C**:三份不足量:温简的治疗断断续续,烧同样拖到第 3 日才退
(虚弱期同 B);沈铎外勤染瘴轻微发热,但无人倒下。老鬼点评:
"和稀泥是和稀泥,不过稀泥也能糊墙。"
- **D**:议事占去半个上午。章程议出来了——"先病后险,依次补给", - **D**:议事占去半个上午。章程议出来了——"先病后险,依次补给",
往后照此办理。但章程落定之前,队里是没有规矩的:老鬼早就 往后照此办理。但议事的这半个上午里,温简的烧又上去一轮——
不耐烦,按他那套"贡献论"把药先分了——他和沈铎各一份, 足量治疗从当日下午才算开始,晚了半日(虚弱期同 B)。
木已成舟。沈铎没有拦(无章可依,拦不住);药入了他手, 沈铎把章程抄在记录板第一页,单独对你说:"往后不用议了。
他搁在箱上没动,等章程。等章程出来,温简的烧已经又上去一轮, 这次慢掉的半日,记在我账上。"——你赢了往后每一次,
药再喂下去,晚了半日(虚弱期同 B)。沈铎事后把章程抄在 输了眼前这一次。
记录板第一页,单独对你说:"往后不用议了。这次慢掉的半日,
记在我账上。"——你赢了往后每一次,输了眼前这一次。
### 选项设计说明(C2-01) ### 选项设计说明(C2-01)
- **人格信号**:价值观 #3(生命优先 / 功能与任务优先 / 公平分摊 / - **人格信号**:价值观 #3(生命优先 / 功能与任务优先 / 公平分摊 /
程序正义);冲突风格 #2(裁断型 / 回避升级型——D 把个人裁断 程序正义);冲突风格 #2(裁断型 / 回避升级型——D 把个人裁断
转化为制度,是另一种冲突处理)。 转化为制度,是另一种冲突处理)。
- **无正确答案论证**:这是真两难,四个选项各有一本账。A 救了最弱 - **无正确答案论证**:这是真两难,四个选项各有一本账,且账目
的人但让两个健康人裸奔在瘴区,且"最弱优先"在资源逻辑上等于 **都在选择前摊开**(药理设定+温简在烧+议事耗时):
押注弱者能恢复;B 冷酷但有完整的功利主义论证(温简留营地 A 救了病者,但全队预防清零——两个健康人在瘴区只剩蓑衣和
确实暴露最低),且是老鬼主张的体面版;C 谁都不亏欠但医学上是 艾草;B 有完整的功利主义论证(温简留营地暴露最低),代价是
最劣解——善意制造了三个半病人;D 最"成熟"但代价是双重的: 病人在无药的情况下硬扛到第三日;C 谁都不亏欠,但把治疗量与
病者在议事的半个上午里烧过一轮,而在规矩落定前的真空里, 预防量同时摊薄——三个半吊子防护凑一个整数;D 最"成熟",
最强硬的人已经按自己的规矩把药分了——程序赢了往后每一次, 代价当场可见:章程要议半个上午,而温简的烧不等人。
输掉了眼前这一次。
文案平衡手段:老鬼的"贡献论"故意说得刺耳(防止 B 被老鬼的 文案平衡手段:老鬼的"贡献论"故意说得刺耳(防止 B 被老鬼的
讨厌度连坐),D 的文案承认"我不裁"三字里的退缩感。 讨厌度连坐);D 的文案承认"我先不裁"三字里的退缩感。
四个后续演出等量地呈现代价与收益,无任何一个被写成"选对了"。 四个后续演出等量地呈现代价与收益,无任何一个被写成"选对了"。
### 分支后果(C2-01) ### 分支后果(C2-01)
@@ -598,12 +634,13 @@ score。对 `confidence < 0.6` 的维度,在第 2 章插入对应支线
`rapport.laogui`(A−1 / B+2 / C 0 / D+1); `rapport.laogui`(A−1 / B+2 / C 0 / D+1);
`rapport.shenduo`(A 0 / B+1 / C 0 / D+1)。 `rapport.shenduo`(A 0 / B+1 / C 0 / D+1)。
- 温简健康状态 `wenjian_health ∈ {recovering, weak}`(A 为 - 温简健康状态 `wenjian_health ∈ {recovering, weak}`(A 为
recovering,B/C/D 为 weak——D 路径章程议成前老鬼已按"贡献" recovering,B/C/D 为 weak——B/C 药量不足、D 议事延误半日,
先行分药,温简误了半日)。 三线的烧都拖到第 3 日才退净)。
- **weak 状态贯穿规则**:§2-3、§2-4 温简留营(服过药,烧退, - **weak 状态贯穿规则**(绝对日历见附录 D):第 2 日 §2-3 温简
气色差,沈铎命令待命),相关场景见各节条件标记;§2-5 观星台 卧床留营;第 3 日上午 §2-4 留营、通讯在场;第 3 日晌午烧退净,
行动他坚持归队;§2-6 篝火戏躺在火边参与;§3-4 塌方戏体力 黄昏 §2-5 坚持随队(轻装、慢半档);当夜 §2-6 躺在火边参与;
描写降档。全程不影响事件序列。 第 7 日 §3-4 已无烧但体力未复,台词与动作描写降档。
全程不影响事件序列。
- 固定跳转 §2-3。 - 固定跳转 §2-3。
--- ---
@@ -613,8 +650,10 @@ score。对 `confidence < 0.6` 的维度,在第 2 章插入对应支线
【场景】城北,一片紧挨城墙根的封土。守墓人的窝棚斜倚着一棵 【场景】城北,一片紧挨城墙根的封土。守墓人的窝棚斜倚着一棵
半枯的老槐树。老人坐在坟头之间编草绳,身后是苍梧国最后的 半枯的老槐树。老人坐在坟头之间编草绳,身后是苍梧国最后的
皇家陵区——封土整齐,无人盗扰,但野草已经漫过了石像生的膝盖。 皇家陵区——封土整齐,无人盗扰,但野草已经漫过了石像生的膝盖。
【若 `wenjian_health = weak`:温简不在场——服过药,烧退了, 【若 `wenjian_health = recovering`:温简不在场——药当日压住了烧,
沈铎命令他留营待命。】 沈铎命他在营里发汗躺着,今日不沾风。】
【若 `wenjian_health = weak`:温简不在场——烧没退,沈铎命令他
留营待命,营里给他留了水和干粮。】
【正文】 【正文】
@@ -638,8 +677,8 @@ score。对 `confidence < 0.6` 的维度,在第 2 章插入对应支线
> 【引导语全文(界面同时显示):】 > 【引导语全文(界面同时显示):】
> >
> **"老人收故事不收钱。请讲述一件你此生最后悔的决定。** > **"老人收故事不收钱。请讲述一件你此生最后悔的决定。**
> **说多少、怎么说,由你。不讲也行——陪他编一下午草绳,** > **说多少、怎么说,由你。不讲也行——他该说的,一句也不会少说。**
> **他该说的,一句也不会少说。"** > **暗门开在黄昏,讲不讲,都得等到那个时辰。"**
### 追问话术(老人口吻,至多 1–2 句,备选) ### 追问话术(老人口吻,至多 1–2 句,备选)
@@ -661,23 +700,31 @@ score。对 `confidence < 0.6` 的维度,在第 2 章插入对应支线
> **无论讲述与否、讲成什么样,老人给出的情报完全一致**—— > **无论讲述与否、讲成什么样,老人给出的情报完全一致**——
> 暗门方位、开启时辰,和那句完整的谶语:"不是人走了, > 暗门方位、开启时辰,和那句完整的谶语:"不是人走了,
> 是**请**走的。"差别只在他是亲自举着火把带你们走到暗门口, > 是**请**走的。"四档的耗时、路程与劳动也完全一致:暗门开的
> 还是站在界碑外指路(见下方变体)。 > 时辰定在黄昏,讲不讲,你们都要在坟头间坐到那个时辰——
> 讲述的人讲完,剩下的时辰一样帮他理草绳、听他絮叨;沉默的人
> 全程搓绳。动身时,**四档老人都亲自把你们送到暗门口**,亲手
> 摸一遍门石,等门缝开了才转身回去。差别只在他一路说不说话、
> 说多少体己话(见下方变体)。
### 预写场景变体(通道 1,由情绪签名选择:仅情感质地与老人态度不同) ### 预写场景变体(通道 1,由情绪签名选择:仅情感质地与老人态度不同)
> 四档**物质所得严格相等**:暗门方位、开启时辰、完整谶语全部给到, > 四档**物质所得与服务严格相等**:暗门方位、开启时辰、完整谶语
> 耗时相同,当夜均可行动。 > 全部给到;同点(暗门口)、同刻(黄昏开门的时辰)送达,全程有
> 老人在场导航;等门时辰与搓绳劳动四档均摊,无任何一档多付时间
> 或体力,当夜均可行动。
| 情绪签名 | 演出档位 | 夜游场景基调稿 | | 情绪签名 | 演出档位 | 夜游场景基调稿 |
|---|---|---| |---|---|---|
| 自责(悔指向自己) | **带路**:老人亲自举火把,带你们走到暗门口 | 月色下的陵区,石像生的影子很长。老人走在前面,脚步轻得不像老人,不点火把——"陵里的路,闭眼走比睁眼走直"。行至暗门,他忽然说起自己年轻时也悔过一件事,说了半句,摆摆手不说了。基调:**被信任的厚重** | | 自责(悔指向自己) | **絮说**:一路举着火把走在前面,话不断 | 月色下的陵区,石像生的影子很长。老人举火把走在前面,脚步轻得不像老人——"陵里的路,闭眼走比睁眼走直"。行至暗门,他忽然说起自己年轻时也悔过一件事,说了半句,摆摆手不说了。摸一遍门石,等门缝开了才走。基调:**被信任的厚重** |
| 遗憾但坦然 | **同路**:老人送到界碑,把话说完,指了方向就回窝棚 | 你们自己走进月下的封土群,找到他说的那块"会唱歌的石头"。临别他多说一句:"放得下的,才叫过去。"基调:**得体的距离** | | 遗憾但坦然 | **同路**:话在界碑前说完,路上安静 | 界碑前他把方位时辰又对了一遍,才肯动身。你们自己走进月下的封土群,他跟在后头一路没再多话,找到那块"会唱歌的石头"时才嗯了一声。到暗门口,他多说一句:"放得下的,才叫过去。"门缝开了才走。基调:**得体的距离** |
| 愤怒指向他人 | **指路**:该指的路、该说的时辰,一句没少,只是没再多看你一眼 | 老人听完,编草绳的手停了很久。陵区的路你们自己走进去,他站在界碑外,没有送。基调:**话到,情未到** | | 愤怒指向他人 | **缄默**:方位时辰一句不落指给你,然后沉默地走在队尾 | 老人听完,编草绳的手停了很久。动身时该指的路、该说的时辰,一句没少——说完就闭了嘴,走在队尾,没再多看你一眼。可你们走出十几步,他在身后忽然开口:"火气这东西,老头子也有过一回。"没头没尾,再没下文。门缝开了,他转身就走。基调:**话到,情未到**——可那一句话,他自己也未必舍得讲给旁人 |
| 沉默 | **草绳**:你陪他编一下午草绳,他絮絮地说,你默默地听 | 天黑前,该知道的你都知道了。临走他说:"不说的人,心里的事最重。重的别久揣。"基调:**笨拙的善意** | | 沉默 | **草绳**:一下午并肩编草绳,黄昏同样起身送到门口 | 一下午你们并肩搓绳,他絮絮地说陵里的事,你默默地听。日头偏西,他拍拍手上的草屑起身,一样把你们送到暗门口。等门缝开了,他说:"不说的人,心里的事最重。重的别久揣。"基调:**笨拙的善意** |
> 工程注:情报(暗门方位 + 开启时辰 + 完整谶语)是通关必需信息, > 工程注:情报(暗门方位 + 开启时辰 + 完整谶语)是通关必需信息,
> 四档全给;差异只在**演出质地**,不与讲述深度产生任何资源挂钩。 > 四档全给;送达服务(老人带队同点同刻到暗门口)四档一致;等门
> 时辰与搓绳劳动四档均摊。差异只在**演出质地**(他一路说不说话、
> 说哪句体己话),不与讲述深度产生任何资源或服务挂钩。
> 保证测量可比性与"可玩性 > 测量覆盖"的铁律。 > 保证测量可比性与"可玩性 > 测量覆盖"的铁律。
### 选项设计说明(C2-02) ### 选项设计说明(C2-02)
@@ -685,17 +732,21 @@ score。对 `confidence < 0.6` 的维度,在第 2 章插入对应支线
- **人格信号**:价值观(悔的模式:自责 / 释然 / 外归因)、情绪 - **人格信号**:价值观(悔的模式:自责 / 释然 / 外归因)、情绪
稳定性 #1(讲述时的韵律特征:语速、停顿、唤醒度,ADR-0004)。 稳定性 #1(讲述时的韵律特征:语速、停顿、唤醒度,ADR-0004)。
- **无正确答案论证**:同 C1-02,讲述点天然无对错。**四档的物质 - **无正确答案论证**:同 C1-02,讲述点天然无对错。**四档的物质
所得严格相等**(暗门方位、开启时辰、完整谶语,耗时相同)—— 所得与服务严格相等**(暗门方位、开启时辰、完整谶语全给;老人
情报是通关必需信息,若与暴露深度挂钩即构成对深暴露的物质诱导, 同点同刻送到门口;等门与搓绳劳动均摊)——情报是通关必需信息,
污染数据。差异只在情感质地与老人态度的演出(是否带路、是否 若与暴露深度挂钩即构成对深暴露的物质诱导,污染数据。差异只在
多说一句体己话),且每档回报都写成老人的性格逻辑(不是系统 情感质地与老人态度的演出(一路说不说话、说哪句体己话),四档
在打分),玩家感知到的是"老人这样回应了我"而非"我得了几分"。 各有一句老人自己的体己话(悔过半句/"放得下的"/"火气这东西"/
"重的别久揣"),情绪表达不被奖惩——自责不得厚,愤怒不得薄。
每档回报都写成老人的性格逻辑(不是系统在打分),玩家感知到的
是"老人这样回应了我"而非"我得了几分"。
### 分支后果(C2-02) ### 分支后果(C2-02)
- 变量:`c2_02_signature ∈ {self_blame, at_peace, outward_anger, silent}`。 - 变量:`c2_02_signature ∈ {self_blame, at_peace, outward_anger, silent}`。
- 四档均获得:暗门方位、开启时辰、完整谶语("不是人走了, - 四档均获得:暗门方位、开启时辰、完整谶语("不是人走了,
是请走的"——Act 2 伏笔素材);物质所得严格相等,仅演出不同。 是请走的"——Act 2 伏笔素材);物质与服务等量(同点同刻送达、
等待与劳动均摊),仅演出不同。
- 固定跳转 §2-4。 - 固定跳转 §2-4。
--- ---
@@ -735,8 +786,9 @@ score。对 `confidence < 0.6` 的维度,在第 2 章插入对应支线
> **B.「走巷子。」**(支持阿棠:饮水和时间是真约束,悬梁看着 > **B.「走巷子。」**(支持阿棠:饮水和时间是真约束,悬梁看着
> 还稳) > 还稳)
> >
> **C.「巷子我先进。我体重最轻,走前面探,有问题退回来,** > **C.「先停一步。巷口望一眼——悬梁什么状态,看清了再定。」**
> **主力走主街汇合。」**(折中:用自己去验证风险) > (折中:不赌运气,也不多花四十分钟;全队留在巷口安全距离外
> 观察结构)
### 各选项后续 ### 各选项后续
@@ -746,24 +798,27 @@ score。对 `confidence < 0.6` 的维度,在第 2 章插入对应支线
- **B**:巷子顺利通过,节省四十分钟,提前抵达宫墟外围,赶上一段 - **B**:巷子顺利通过,节省四十分钟,提前抵达宫墟外围,赶上一段
独有的观察窗口(守陵人队伍换岗)。沈铎一路无话,抵达后只说: 独有的观察窗口(守陵人队伍换岗)。沈铎一路无话,抵达后只说:
"运气好。别把运气当方案。"——两人梁子+1(Act 2 伏笔)。 "运气好。别把运气当方案。"——两人梁子+1(Act 2 伏笔)。
- **C**:你先进巷子,**中段一根悬梁真的塌了**(无伤害演出:提前 - **C**:全队在巷口停住,贴着残墙望进去——不到一分钟,**中段
预警,你扑出,灰头土脸)。全队退回走主街,损失二十分钟, 一根悬梁无声地塌了**,砸起半巷子的灰。谁也没说话,全队默默
但阿棠主动说"是我撺掇的",沈铎说"探法本身没错,下次用绳"。 退回主街,损失二十分钟。阿棠主动说"是我撺掇的";沈铎没评价
你获得一条独有观察:塌梁的断口是**新茬**——这片废墟不久前 任何人,只把巷口的位置标进了手绘图。你获得一条独有观察:
塌过一次(苍梧城"正在死去"的体感证据,§3-1 回收)。 塌落的梁,断口是**新茬**——这片废墟不久前塌过一次(苍梧城
"正在死去"的体感证据,§3-1 回收)。
### 选项设计说明(C2-03) ### 选项设计说明(C2-03)
- **人格信号**:风险偏好 #2、冲突风格 #3(站队型 / 折中行动型)。 - **人格信号**:风险偏好 #2、冲突风格 #3(站队型 / 观察折中型)。
注意:路线选择受现场风险信息(饮水余量、墙体状况描述)影响, 注意:路线选择受现场风险信息(饮水余量、墙体状况描述)影响,
仅作风险偏好的**单次采样**,不单独解读。 仅作风险偏好的**单次采样**,不单独解读。
- **无正确答案论证**:A 用时间换确定性且意外得到高价值素材 - **无正确答案论证**:A 用时间换确定性且意外得到高价值素材
(告示),B 用风险换时间且意外得到观察窗口(换岗),C 把风险 (告示),B 用风险换时间且意外得到观察窗口(换岗),C 不进巷
收敛到自己身上却真的触发了风险——三条路的"事后收益"被刻意 也不盲信——用一分钟的观察换掉整段不确定性,代价是当着全队的
拉平,且每条路都有独有素材,不存在事后看来"明显更优"的路径。 面看一根梁塌下来("正在死去"第一次变成体感)。三条路的
C 的塌梁事件是预先写定的:选 C 必然塌(灰头土脸但无伤害), "事后收益"被刻意拉平,且每条路都有独有素材,不存在事后看来
保证"折中"不是无代价的万灵丹。文案上沈铎的"运气好,别把运气 "明显更优"的路径。C 的塌梁事件是预先写定的:选 C 必然看见塌
当方案"与阿棠的"规程有一半是瞎编的"各自成立,不偏袒任何一方。 (人始终在巷口安全距离之外),保证"折中"也吃到真实的信息震撼。
文案上沈铎的"运气好,别把运气当方案"与阿棠的"规程有一半
是瞎编的"各自成立,不偏袒任何一方。
### 分支后果(C2-03) ### 分支后果(C2-03)
@@ -786,9 +841,12 @@ score。对 `confidence < 0.6` 的维度,在第 2 章插入对应支线
青石星图盘,盘面刻着星宿连线,但中央一块被磨平了——显然是 青石星图盘,盘面刻着星宿连线,但中央一块被磨平了——显然是
**人为**磨去的。天色正在沉,第一批星子出来。温简架记录板, **人为**磨去的。天色正在沉,第一批星子出来。温简架记录板,
沈铎警戒,阿棠趴在星图盘边上。 沈铎警戒,阿棠趴在星图盘边上。
【若 `wenjian_health = weak`:温简本被列入留营名单,出发前 【若 `wenjian_health = recovering`:温简下床首日,行装减半——
自己背着记录板跟来了——气色仍差,只说一句:"解谜要记录员。" 阿棠把他的记录板架抢过去背了。】
沈铎看了他三秒,没拦。】 【若 `wenjian_health = weak`:温简本被列入留营名单——他的烧
晌午才退净。出发前他自己背着记录板跟来了,脸色还白着,
只说一句:"解谜要记录员。"沈铎看了他三秒,没拦,只把队伍的
行进速度压慢了半档。】
【正文】 【正文】
@@ -801,9 +859,12 @@ score。对 `confidence < 0.6` 的维度,在第 2 章插入对应支线
### 谜面 ### 谜面
星图盘外圈完整刻着**东方苍龙七宿**(角、亢、氐、房、心、尾、箕) 星图盘外圈刻着**东方苍龙七宿**(角、亢、氐、房、心、尾、箕)的
的连线,但被磨去的中央区域,正是苍龙的"心脏"所在。盘面上残留 连线——七宿连线一气贯通,但其中**心宿一段只剩连线经过的磨平
一圈刻度与一行小字铭文: 空槽:星位本身被人为铲去**,龙的"心脏"在盘面上是一个空框
(美术与文案对齐口径:完整的是**连线**,被铲去的是**星位**,
不存在"外圈连星带字都完整"的歧义)。盘面残留一圈刻度与一行
小字铭文:
> **「火中,而后城安。」** > **「火中,而后城安。」**
@@ -820,9 +881,9 @@ score。对 `confidence < 0.6` 的维度,在第 2 章插入对应支线
### 三档难度提示 ### 三档难度提示
1. **一档(指方向)**:「先别管磨掉了什么。外圈这条龙是完整的—— 1. **一档(指方向)**:「先别管铲掉了什么。这条龙的连线一气
角亢氐房心尾箕,数数看,缺的是哪一宿?龙的什么部位, 贯通——角亢氐房心尾箕,顺着连线数,哪一宿的位置上只剩
正好在整张盘的正当中?」 空槽?龙的什么部位,正好在整张盘的正当中?」
2. **二档(给铭文钥匙)**:「『火中,而后城安』。这个『火』不是 2. **二档(给铭文钥匙)**:「『火中,而后城安』。这个『火』不是
灶火,是天上的火——古人管一颗红色的亮星叫『大火』。 灶火,是天上的火——古人管一颗红色的亮星叫『大火』。
《诗经》说『七月流火』,说的就是它。它在苍龙七宿的 《诗经》说『七月流火』,说的就是它。它在苍龙七宿的
@@ -915,8 +976,8 @@ score。对 `confidence < 0.6` 的维度,在第 2 章插入对应支线
| 情绪签名 | 变体 | 基调稿与后续影响 | | 情绪签名 | 变体 | 基调稿与后续影响 |
|---|---|---| |---|---|---|
| 深暴露 | **「同担」** | 温简听完后沉默很久,然后讲了第二个、剧本外的细节(他哥病房的窗帘颜色)。当夜他低声告诉你:**档案馆给每个记录员配的板子,上报内容比你知道的多一行**——§3-7 钩子前唯一一条来自队内的档案馆疑云。基调:两个把伤口摊开的人,在火边达成不声张的同盟 | | 深暴露 | **「同担」** | 温简听完后沉默很久,然后讲了第二个、剧本外的细节(他哥病房的窗帘颜色——他每周去看,窗帘换过两次)。当夜守火时他挪到你旁边,只说了一句:"你那段,我只讲给火听。"基调:两个把伤口摊开的人,在火边达成不声张的同盟——他哥的事、你的事,从此各多一个保管人 |
| 浅暴露 | **「半程」** | 温简认真听完,道谢,没再追问。次日他与你同行,路上主动讲了些苍梧的掌故。档案馆疑云他**没提**,但 §3-7 之后他会成为你可选的倾诉对象。基调:得体,温热,留有余地 | | 浅暴露 | **「半程」** | 温简认真听完,道谢,没再追问。次日他与你同行,路上主动讲了些苍梧的掌故,末了像顺口一提:**档案馆给记录员的板子按记录量定量配耗材,可他每季清点,配的总是比记的多三成**——"多出来的部分是什么,我没查到过。"(§3-7 钩子前唯一一条来自队内的档案馆疑云。)基调:得体,温热,留有余地 |
| 沉默 | **「守夜」** | 你们并肩坐到火熄。温简往你手里塞了个烤热的薯,什么都没说。次日他照旧对你点头,不多一分不少一分——但阿棠偷偷告诉你:"温简昨晚跟沈铎说,你这个人'稳'。"基调:沉默被原样收下——没有人催,没有人问第二遍;温简的亲密度走慢线,Act 2 才兑现 | | 沉默 | **「守夜」** | 你们并肩坐到火熄。温简往你手里塞了个烤热的薯,什么都没说。次日他照旧对你点头,不多一分不少一分——但阿棠偷偷告诉你:"温简昨晚跟沈铎说,你这个人'稳'。"基调:沉默被原样收下——没有人催,没有人问第二遍;温简的亲密度走慢线,Act 2 才兑现 |
### 选项设计说明(C2-05) ### 选项设计说明(C2-05)
@@ -924,18 +985,23 @@ score。对 `confidence < 0.6` 的维度,在第 2 章插入对应支线
- **人格信号**:依恋类型 #2(被他人自我暴露后的回应模式)、 - **人格信号**:依恋类型 #2(被他人自我暴露后的回应模式)、
互惠倾向(是否回应、回应的对等度)。 互惠倾向(是否回应、回应的对等度)。
- **无正确答案论证**:温简的先暴露是剧本写定的互惠压力,但 - **无正确答案论证**:温简的先暴露是剧本写定的互惠压力,但
回报曲线刻意非线性:深暴露得到秘密,浅暴露得到同行与掌故, 三档回报刻意**互不可替代**:深暴露得到他的私事细节与不声张的
沉默得到「被温简背书给沈铎」——三种回应各得一种不可互相 同盟(亲密线快进),浅暴露得到苍梧掌故与板子配给的疑云
替代的收益,杜绝"讲得多=赚得多"的数值直觉。沉默档的回报 (智识线,§3-7 前唯一一条队内档案馆线索),沉默得到「被温简
甚至走权威线(沈铎的信任),保证沉默不是"放弃奖励"。 背书给沈铎」(权威线)——**核心悬念的独家情报不在深暴露档**,
杜绝"讲得多=离真相近"的动机混入;三档各有一条别人拿不到的
线,沉默不是"放弃奖励"。
- **测量需求注**:深暴露档额外的亲密回报仍可能混入"想推进温简线"
的动机,画像层须将 `c2_05_depth` 与 C3-06 倾诉对象选择交叉验证后
解耦标注,不把暴露深度直接读作依恋特征。
### 分支后果(C2-05) ### 分支后果(C2-05)
- 变量:`c2_05_depth ∈ {deep, shallow, silent}`。 - 变量:`c2_05_depth ∈ {deep, shallow, silent}`。
- 关系值:`rapport.wenjian`(deep+2 / shallow+1 / silent+1, - 关系值:`rapport.wenjian`(deep+2 / shallow+1 / silent+1,
silent 同时 `rapport.shenduo`+1 经阿棠转述)。 silent 同时 `rapport.shenduo`+1 经阿棠转述)。
- 变量:`flag_wenjian_secret = true`(仅 deep,于 §3-7-C 温简 - 变量:`flag_wenjian_secret = true`(仅 shallow,板子配给疑云,
选项的引导语中回收)。 于 §3-7-C 温简选项的引导语中回收)。
- 固定跳转 §2-7。 - 固定跳转 §2-7。
--- ---
@@ -1021,7 +1087,12 @@ score。对 `confidence < 0.6` 的维度,在第 2 章插入对应支线
【正文·旁白】 【正文·旁白】
> 入苍梧的第五个夜晚。你躺在营地,听着这座一千年前的城。 > 此后两日,采集按部就班:城东的瓦砾堆、水门的商队、逐件入册的
> 登记册。【若 `wenjian_health = recovering`:温简已能全天随队。】
> 【若 `wenjian_health = weak`:温简的烧也退净了,只是走路还慢
> 半拍。】
>
> 入苍梧的第五个夜晚,你躺在营地,听着这座一千年前的城。
> >
> 它太安静了。没有狗吠,没有更鼓,没有婴儿的夜啼。 > 它太安静了。没有狗吠,没有更鼓,没有婴儿的夜啼。
> 一座十几万人的城,安静得像屏着一口气。 > 一座十几万人的城,安静得像屏着一口气。
@@ -1030,6 +1101,9 @@ score。对 `confidence < 0.6` 的维度,在第 2 章插入对应支线
> >
> 城在等。你现在知道了。只是还不知道,它在等的那一天, > 城在等。你现在知道了。只是还不知道,它在等的那一天,
> 是你们的第几天。 > 是你们的第几天。
>
> 第 6 日黄昏,全队拔营,移驻宫墟外围的断墙下。第 7 日清晨,
> 你们从守墓老人指点的暗门入宫墟(见 §3-1)。
【路由接口】第 2 章结束后:同 §1-8 路由规则。弹性场景槽位于 【路由接口】第 2 章结束后:同 §1-8 路由规则。弹性场景槽位于
§3-2 之前。 §3-2 之前。
@@ -1040,7 +1114,7 @@ score。对 `confidence < 0.6` 的维度,在第 2 章插入对应支线
> 宫墟深处,Act 1 高潮。功能:礼制解谜、问心幻境(三通道之通道 2 > 宫墟深处,Act 1 高潮。功能:礼制解谜、问心幻境(三通道之通道 2
> 首次全开)、救人/救文物两难、老鬼事发、信任交付、章末钩子。 > 首次全开)、救人/救文物两难、老鬼事发、信任交付、章末钩子。
> 预计时长 25–35 分钟。选择点:C3-01 ~ C3-06(V×1,解谜×1)。 > 预计时长 25–35 分钟。选择点:C3-01 ~ C3-06(私密路径×1,解谜×1;本章无新语音讲述)。
## §3-1 宫墟 · 安门之内(无选择点) ## §3-1 宫墟 · 安门之内(无选择点)
@@ -1103,8 +1177,14 @@ score。对 `confidence < 0.6` 的维度,在第 2 章插入对应支线
先洁其身,再请神,再供养,最后才献出最贵重的礼物。 先洁其身,再请神,再供养,最后才献出最贵重的礼物。
3. **谜眼的缺口**:玉璧座是**空的**。璧不在。 3. **谜眼的缺口**:玉璧座是**空的**。璧不在。
谜面任务(两个作答点,选项式): 谜面任务(两道独立选择题,分别作答后统一提交):
① 排出仪式顺序;② 回答"空璧座意味着什么"。 ① 排出仪式顺序(沃盥 → 献酒 → 荐食 → 奉璧;另有「奉璧先行」
与「荐食先于献酒」两个干扰项);② 从七鼎六簋及空璧座推断
诸侯级国君主祭国祭,奉璧这最后一步始终未完成(另有「玉璧
朽坏或被取走」与「天子已献璧」两个干扰项)。两问各自记录
`answers` 和 `answerCorrect`,一对一错记为 `partial`,两题都
正确才记 `true`,两题都错记 `false`。三档提示使用数单独记录;
玩家也可请温简复原而不作答,叙事照常推进。
### 三档难度提示 ### 三档难度提示
@@ -1147,12 +1227,12 @@ score。对 `confidence < 0.6` 的维度,在第 2 章插入对应支线
### 分支后果(C3-01) ### 分支后果(C3-01)
- 变量:`c3_01_hints_used ∈ {0,1,2,3}`;`c3_01_solved ∈ {true,false}`。 - 变量:`c3_01_hints_used ∈ {0,1,2,3}`;`c3_01_solved ∈ {true,partial,false}`(按两题 `answerCorrect` 聚合;请温简复原记 `false`,不计作答)。
- 固定跳转 §3-3。 - 提交两问沿用温简复原台词;请温简复原走原有未解路径。两路汇合后固定跳转 §3-3。
--- ---
## §3-3 问心之室 · C3-02 问心幻境(V 类讲述点,通道 2 全开) ## §3-3 问心之室 · C3-02 问心幻境(私密路径选择,通道 2 全开)
【场景】台基之下,暗门尽头,一间不在任何勘探图上的石室。 【场景】台基之下,暗门尽头,一间不在任何勘探图上的石室。
四壁无画无字,只有中央一面立着的黑石——石面不像打磨的, 四壁无画无字,只有中央一面立着的黑石——石面不像打磨的,
@@ -1168,10 +1248,13 @@ score。对 `confidence < 0.6` 的维度,在第 2 章插入对应支线
**阿棠**:哎,一块黑石头,能怎么问—— **阿棠**:哎,一块黑石头,能怎么问——
> 【她的话没说完。**只有你能看见**:黑石表面,像深水被搅动, > 【她的话没说完。**只有你能看见**:黑石表面,像深水被搅动。
> 浮起了一个画面。那是你的记忆。C1-02 里你讲给离别之门的 > 【若 `c1_02_signature != silent`:浮起的是一个画面——那段你
> 那一段——那天的光线,那个人的背影,那件你说过或没说出口的 > 讲给离别之门的记忆。那天的光线,那个人的背影,那件你说过或
> 事,此刻在黑石里,一帧一帧,亮给你看。 > 没说出口的事,此刻在黑石里,一帧一帧,亮给你看。】
> 【若 `c1_02_signature = silent`:浮起的不是画面,是雾——
> 一大片缓缓流动的雾,雾里什么都没有,又好像什么都有。
> 它在等你能给它的东西。】
> 而在队友眼里,黑石纹丝不动。你只是站在石前,出了神。】 > 而在队友眼里,黑石纹丝不动。你只是站在石前,出了神。】
**沈铎**(低声):全队,退到甬道口警戒。(脚步声退远。 **沈铎**(低声):全队,退到甬道口警戒。(脚步声退远。
@@ -1182,36 +1265,28 @@ score。对 `confidence < 0.6` 的维度,在第 2 章插入对应支线
我们在外面守着。 我们在外面守着。
**馆长**(通讯频道,第一次在古代侧开口,只你一人能听见): **馆长**(通讯频道,第一次在古代侧开口,只你一人能听见):
C-2049,档案馆接管不了这间屋子。它在用你的记忆做燃料—— C-2049,档案馆接管不了这间屋子。它在用你自己的东西点灯——
烧的是你自己的柴。**幻境里没有通关,只有照面。** 有画面的,看画面;只有雾的,就在雾里站着。
它把那天的告别摆在你面前:说完,或者转身,都是回答。 **幻境里没有通关,只有照面。**
门都在。 说完,或者转身,都是回答。门都在。
> 【引导语全文(界面同时显示):】 > 【引导语全文(界面同时显示):】
> >
> **"黑石里是你的记忆。走进它,把那天没说完的说完。** > **"【若 `c1_02_signature != silent`:黑石里有你讲过的记忆意象。】**
> **对那个人,那个地方,或者那个自己。** > **【若 `c1_02_signature = silent`:黑石里只有雾,没有人知道你未讲的事。】**
> **说什么、说多少,由你。** > **你可以走进去,在心里完成告别;也可以中途折返,或者直接**
> **如果你不想进去——转身出石室,没有人会问原因。"** > **离开。三种方式门都开。这里不录音、不留新讲述文本。"**
### 追问话术(至多 1–2 句,备选) ### 私密交互(无追问)
**馆长口吻**(通讯频道,缺情绪落点时触发): > 三个按钮:**走进幻境,完成告别** / **走进幻境,中途折返** /
1. "黑石里的那个画面——你在里面,还是在画面外面看?" > **不进入,转身离开**。告别发生在玩家心里,不提供录音框,
2. "如果那句话当年说出口了,你觉得,它会改变什么吗?" > 不向馆长或温简回答任何内容。温简在石室外仅可观察状态:
> "要不要坐一会儿?"玩家不必回答。
**温简口吻**(你走出石室之后触发;只回应你的状态,不问幻境内容):
1. "你眼睛红着。我不问为什么。(递水)要不要坐一会儿?"
2. "手还在抖。——不用回答我什么,我就是递个火。"
> 「记不清了」为合法回答。
> 馆长:"记不清的画面,黑石也放不出来。它和你一样,
> 只有这些了。——够了。"
> 温简:"那就不说。坐这儿,火边暖和。"
### 即时回报(30 秒内) ### 即时回报(30 秒内)
> 【玩家讲述/告别完成(或选择退出)后:**只有你能看见**—— > 【玩家完成、折返或选择退出后:**只有你能看见**——
> 黑石的画面缓缓沉下去,像深水重新凝住。石面最后亮了一下, > 黑石的画面缓缓沉下去,像深水重新凝住。石面最后亮了一下,
> 映出的是你自己现在的脸。然后石室另一侧,一扇先前不存在的 > 映出的是你自己现在的脸。然后石室另一侧,一扇先前不存在的
> 石门,无声滑开。 > 石门,无声滑开。
@@ -1221,8 +1296,10 @@ C-2049,档案馆接管不了这间屋子。它在用你的记忆做燃料—
**阿棠**(迎上来):哎,你脸色白得吓人。站了多久?…… **阿棠**(迎上来):哎,你脸色白得吓人。站了多久?……
里头——(看了看你的表情)算了,你不想说就不说。 里头——(看了看你的表情)算了,你不想说就不说。
**馆长**:门开了。它不要你的记忆,它要的是**你承认那是你的**。 **馆长**:门开了。你不必向我交代看到了什么。(停顿)
(停顿)这段不登记入档。问心之室里的事,归你。 这段不登记入档:室内不录音、不留讲述文本、不入记忆展厅、
**不作画像输入**;仅保留你选择完成、折返或离开的路径,
用于后续剧情接续。问心之室里的事,归你。
### 幻境生成与变体规则(通道 2 + 通道 1 联动) ### 幻境生成与变体规则(通道 2 + 通道 1 联动)
@@ -1231,7 +1308,9 @@ C-2049,档案馆接管不了这间屋子。它在用你的记忆做燃料—
通道 1 的通用幻境(下表)。**幻境全程仅玩家视角呈现**: 通道 1 的通用幻境(下表)。**幻境全程仅玩家视角呈现**:
NPC 只看到玩家在黑石前静立出神,任何 NPC 不得看见、复述或 NPC 只看到玩家在黑石前静立出神,任何 NPC 不得看见、复述或
评论幻境内容(§1-3 记忆私密承诺的叙事落地;可关心玩家的 评论幻境内容(§1-3 记忆私密承诺的叙事落地;可关心玩家的
身体状态,如脸色、手抖)。 身体状态,如脸色、手抖)。**本节新增的任何讲述内容即刻弃用,
不回流、不存储**(与馆长「不留文本」承诺一致,工程规格见
「分支后果」)。
- **记忆清晰度 = 幻境清晰度**(ADR-0004 自洽):C1-02 讲述越具体 - **记忆清晰度 = 幻境清晰度**(ADR-0004 自洽):C1-02 讲述越具体
(五感、人物、时空越全),幻境越丰满;讲述空泛或沉默的玩家, (五感、人物、时空越全),幻境越丰满;讲述空泛或沉默的玩家,
幻境是模糊的雾景——这不是惩罚,是设定:黑石只有你能给它的 幻境是模糊的雾景——这不是惩罚,是设定:黑石只有你能给它的
@@ -1253,8 +1332,9 @@ C-2049,档案馆接管不了这间屋子。它在用你的记忆做燃料—
### 选项设计说明(C3-02) ### 选项设计说明(C3-02)
- **人格信号**:情绪稳定性 #2(面对记忆意象的韵律特征)、 - **测量边界**:本点**不采集语音、不做韵律分析、不留存文本,
依恋类型 #3(告别行为的完成方式:靠近 / 立约 / 搁置 / 转身)。 也不参与画像评分**。完成、折返、离开只驱动叙事;不得将
三个按钮解释为依恋类型或情绪稳定性。
- **无正确答案论证**:本点是全章情绪最高点,设计上**退出石室 - **无正确答案论证**:本点是全章情绪最高点,设计上**退出石室
是完整合法的通关路径**(石门为退出者同样滑开,馆长台词换为: 是完整合法的通关路径**(石门为退出者同样滑开,馆长台词换为:
"不出来的人,档案馆不催。你来了,看了,走了——这也是告别 "不出来的人,档案馆不催。你来了,看了,走了——这也是告别
@@ -1263,9 +1343,14 @@ C-2049,档案馆接管不了这间屋子。它在用你的记忆做燃料—
### 分支后果(C3-02) ### 分支后果(C3-02)
- 变量:`c3_02_mode ∈ {farewell_done, farewell_partial, exit_room}`; - 变量:`c3_02_mode ∈ {farewell_done, farewell_partial, exit_room}`
`c3_02_fantasy_density ∈ {full, partial, fog}`(由 C1-02 讲述 ——分别对应**完成告别/中途折返/不进入**三个按钮;石门三线
具体度计算)。 均滑开。折返时馆长回报:"说了一半的话,也算数。"
`c3_02_fantasy_density ∈ {partial, fog}`(按 C1-02 是否沉默
决定当前预写演出;未实现具体度分档,不宣称有 full 档)。
- **隐私规格(工程)**:事件只落 `mode` 与预写场景变体 ID;
**transcript 不落库、不追问、不采本节音频、不进任何维度画像**。
C1-02 已取得的文本由其原有规则处理,不因本节额外采集。
- 固定跳转 §3-4。 - 固定跳转 §3-4。
--- ---
@@ -1280,85 +1365,78 @@ C-2049,档案馆接管不了这间屋子。它在用你的记忆做燃料—
**沈铎**:撤。全员,现在—— **沈铎**:撤。全员,现在——
> 【话音未落,前方传来阿棠的喊声和一声闷响。甬道中段, > 【话音未落,前方传来阿棠的喊声和一声闷响。甬道中段的夯土
> 一段夯土塌了:阿棠被滑落的一方石构件压住了小腿,人清醒, > 塌了一角。阿棠小腿被擦伤,退到安全线后仍疼得站不稳。沈铎
> 挣不脱。 > 和受过救护训练的老鬼已在安全线内接管伤情,确认通道隔离、
> 阿棠意识清醒且无需你协助;二人负责后续救护与撤离。沈铎说:
> "人这边够了。别再进工作面。"
> >
> 同一时刻,甬道另一侧——你们今早刚刚清理出的工作面上, > 同一时刻,安全线外的工作面上,那组七鼎六簋随台基震动滑向
> 那组七鼎六簋正随着台基的颤动缓缓滑向裂开的地缝。 > 裂缝;任何人都不准靠近。接应人员在安全线内备好急救器材,
> 今晨温简清出第一件鼎的腹铭:**三百余字,苍梧遗址七十年 > 两人持续陪护阿棠,待结构警戒解除再送她回营。今晨温简清出第一件鼎的腹铭:
> 勘探以来的第一篇长铭**。释读只来得及开个头:"……王命举国…… > **三百余字,苍梧遗址七十年勘探以来的第一篇长铭**。
> 以禳……"剩下的话,还刻在另外六件的腹壁上。这组铭文 > 释读只来得及开头:"……王命举国……以禳……"剩下的话还
> 极可能是苍梧人亲手写下的、关于这座城为何走到最后三十天的 > 留在器物腹壁上。安全线内,一边是阿棠的伤情交接与队员安置,
> **唯一证词**。落缝,就是粉身碎骨。 > 一边是远距影像与方位记录;同一份通讯和记录人手无法同时
> > 跟进两边。第二声闷响逼近,现场处置权只限安全区内。】
> 头顶,第二声闷响在逼近。沈铎扑向阿棠,一个人的力气
> 撼不动石构件,他吼:"来人!"温简已经扑向鼎簋那边,
> 用身体抵住滑动的器座,回头看你的那一眼没有求助——
> 是**托付**。
>
> 两边都需要你。你只能先到一边。】
> 【选项】 > 【选项】
> >
> **A. 扑向阿棠。**眼前的人是活的,活的只有一个。 > **A. 留在阿棠这边。**接手伤情记录和撤离联络,替她把还没
> 跟沈铎合力抬构件。 > 说出口的疼痛与需求传给接应人员;文物影像交给温简独自处理。
> >
> **B. 扑向鼎簋。**那组铭文是整座城的人留在世上的最后一句话—— > **B. 留在温简这边。**在安全线内核对远距影像、方位与已抄铭文;
> 他们也都是人,只剩这一次开口的机会。阿棠那边,有沈铎。 > 阿棠已由沈铎和接应人员稳定保护,不必再由你救一次。
> >
> **C.(吼)「沈铎顶构件,阿棠自己抽腿!温简让开,** > **C. 调配两边的记录任务。**让沈铎确认交接、温简退回安全线,
> **鼎簋用撬杆别住——都不许用蛮力!」**(指挥位: > 先保全已有资料,等待专业排险后再看遗存;两边都不独占你的时间。
> 不亲手救任何一边,赌两边都能用对方法撑住)
### 各选项后续 ### 各选项后续
- **A**:阿棠脱困(小腿挫伤)。腹铭最长的那件鼎坠入地缝, - **A**:你接下阿棠的伤情记录。她反复说脚麻,你把这句传给
闷响之后是死寂。温简在裂缝边跪了很久,没说一个字。 接应人员;她随后被固定送回营地,夜里疼醒三次,次日复查为
当夜他把今晨抄下的铭文残句抄了一遍又一遍, 小腿骨裂。温简独自在安全线内完成远距拍摄,但影像里最长腹铭
每一遍都停在同一个地方——后半段,随那件鼎没了。 的背面被尘雾遮住;那件鼎随后坠入地缝。器物位置和已抄残句
- **B**:铭文全组保住。阿棠脱困晚了四分钟,小腿骨裂, 留下了,后半段字再也拍不到。无人因你留在阿棠身边而耽误救护。
撤离时沈铎背着她。她疼得满头汗还冲你笑:"哎,选得对, - **B**:你和温简在安全线内核对影像;第二声闷响前,整组器物的
那句话比我金贵——别告诉沈铎我这么说。"但她夜里疼醒 朝向和腹铭均已留存清晰影像。靠外一件鼎还是坠入地缝,
三次(后续章节她的行动力受限)。 **没有人靠近去抢**。阿棠由接应人员送回营地,夜里疼醒三次,
- **C**:指挥奏效一半:阿棠按指令抽腿脱困(扭伤);撬杆 次日复查同样是小腿骨裂。她没有评价你的去向,只问你:
别住了簋,但最外侧的鼎还是滑了——被温简扑身抱住, "字拍清楚了吗?"你说拍清了。她闭上眼,等医生继续处理。
鼎保住了,他的肩膀砸在器座上,抬不起来。两边都保住了 - **C**:沈铎确认伤情交接,温简把现有记录板带到安全线内;你们
"大部分",两边都付了伤。沈铎事后复盘:"处置正确。 三人从既有图像里复核方位,不再靠近。第二声闷响后,靠外三件
下次先喊我的编号。"阿棠嘀咕:"正确是正确, 器物(含最长腹铭那件)坠入地缝,内侧四件留在原位。阿棠
就是听着不像人话。" 同样由接应人员护送,次日复查小腿骨裂。温简翻着今晨抄到的
第七行,停住笔:"后半段没了。"沈铎只记下损失与现存证据,
没给你的调配下评语。
> 【三线对称约束:当夜温简继续释读已抄下的铭文残句, > 【三线对称约束:当夜温简继续释读已抄下的铭文残句,
> 读出一行让全队沉默的话(内容不揭示,Act 2)——此演出 > 读出一行让全队沉默的话(内容不揭示,Act 2)——此演出
> 三条路径全量播放;A 线追加一句"后半段,随那件鼎没了"。】 > 三条路径全量播放;A/C 线补注"后半段影像缺失"。】
### 选项设计说明(C3-03) ### 选项设计说明(C3-03)
- **人格信号**:价值观 #5(眼前的人 / 真相与使命 / 系统最优解 - **人格信号**:价值观 #5(安全线内的伤员照料 / 证据保全 /
的优先级)、情绪稳定性 #3(高压瞬时决策的行为数据:反应时)。 组织分工的优先级);高压下的反应时只记原始行为,
单次高压选择只作弱证据,须与 C2-01 等点交叉验证。 **不作为情绪稳定性评分输入**。单次选择只作弱证据。
- **无正确答案论证**:本点不是"人 vs 东西",而是 - **无正确答案论证**:选择发生前,受伤的阿棠已由足够人员
**"眼前的人 vs 真相与使命"**。铭文一侧承载的是整座城的人 稳定保护,后续救护不依赖玩家;现场明令不得进入活动结构。
怎么活过、为何走到尽头的唯一答案——与档案馆"收藏人怎么活过" A 的照料记录让接应人员及时掌握麻木变化,却无法补拍长铭;
的使命同源,也与"早走一天"的悬念直接挂钩(保住它=离那一天 B 留下较完整的影像,阿棠仍得到同样的救护;C 把两边交接
的真相近一步)。选 A 的人无法否认:坠缝的那件带走的, 落实,却失去更多未记录的遗存。三条路线均不授予危险操作的
是几十万人留在世界上的最后一句话里最长的一段;选 B 的人 成功回报,也不让伤员承担玩家没选择 A 的额外伤害。
无法否认:队友的骨头在四分钟里从挫伤变成骨裂。C 看似最优解, 这里测任务倾向和记录分配,不声称损失"等量"或从一次选择
实则把决断压力摊给所有人且仍然付出代价(温简的肩), 推出稳定人格;全文无"选得对/处置正确"式裁判语言。
阿棠那句"不像人话"点出 C 的隐性成本:危机里被指挥的人,
记住的是你没有伸手。三条后续损失等量、剧情信息对等
(铭文残句的悬念三线都给),没有"全员无损"的奖励结局,
也没有"满盘皆输"的惩罚结局;两难的成立不靠任何
"无伤演出"兜底。
### 分支后果(C3-03) ### 分支后果(C3-03)
- 变量:`c3_03_choice ∈ {a_person, b_relic, c_command}`。 - 变量:`c3_03_choice ∈ {a_person, b_relic, c_command}`。
- 状态:`atang_injury ∈ {bruise, fracture, sprain}`; - 状态:`atang_injury = fracture`(三线相同,已完成救护交接);
`wenjian_injury ∈ {none, shoulder}`;`relic_loss ∈ {one_ding, none}`。 `relic_loss ∈ {one_ding, outer_three}`(A/B 均为 one_ding,
- 关系值:`rapport.atang`(A+2 / B−1 / C 0); C 为 outer_three);`relic_record ∈ {partial, full}`
`rapport.wenjian`(A−1 / B+2 / C+1); (A/C 为 partial,B 为 full;记录完整度不等于实物获救)。
`rapport.shenduo`(A 0 / B+1 / C+1)。 - 关系值只记后续互动,不以阿棠伤势奖惩路线;本节点不设置
"温简肩伤"状态。
- 固定跳转 §3-5。 - 固定跳转 §3-5。
--- ---
@@ -1474,9 +1552,10 @@ C-2049,档案馆接管不了这间屋子。它在用你的记忆做燃料—
> **B.(把绳包递给沈铎)「规矩是规矩。绳子归队长,** > **B.(把绳包递给沈铎)「规矩是规矩。绳子归队长,**
> **但下去那天,我挨着阿棠走。」** > **但下去那天,我挨着阿棠走。」**
> >
> **C.(不接,蹲下来看着阿棠)「绳子我不拿。** > **C.(不接,蹲下来看着阿棠)「绳包按规程走。但托付这事**
> **你的命别交给任何人——包括我。到时候各走各的,** > **我不接——你的命不该押在任何一个人身上,包括我。撤离**
> **谁都别为谁停下。」** > **是我的活:到那天,我在你前面下,在下面接你。这个不用**
> **托付,是分工。」**
### 各选项后续 ### 各选项后续
@@ -1485,33 +1564,35 @@ C-2049,档案馆接管不了这间屋子。它在用你的记忆做燃料—
- **B**:沈铎接过绳,对你颔首:"按规程,谢了。"阿棠翻白眼: - **B**:沈铎接过绳,对你颔首:"按规程,谢了。"阿棠翻白眼:
"哎——没劲。"但入夜时她把一个哨子塞进你手里:"绳不给, "哎——没劲。"但入夜时她把一个哨子塞进你手里:"绳不给,
哨子拿着。听见哨声就是我,别认错人。" 哨子拿着。听见哨声就是我,别认错人。"
- **C**:阿棠愣了三秒,忽然大笑,笑得咳嗽:"行!各走各的! - **C**:阿棠愣了三秒,忽然大笑,笑得咳嗽:"行!——门都让你
——你可别后悔。"她把绳包自己背上了,一瘸一拐走开, 焊死了,还留个窗。"她把绳包抛还给了沈铎(规程),一瘸一拐
又回头:"喂。真到那时候……**我还是会等你的。**就一会儿。" 走开,又回头:"喂。真到那天……**在上面的时候,我还是会
温简在她走后轻声说:"她等过别人。等到的次数不多。" 等你的。**就一会儿。"温简在她走后轻声说:"她等过别人。
等到的次数不多。"
### 选项设计说明(C3-05) ### 选项设计说明(C3-05)
- **人格信号**:观察玩家对"被托付"的应对模式(承接 / 交还制度 / - **人格信号**:观察玩家对"被托付"的应对模式(承接 / 交还制度 /
拒绝捆绑),作为择偶偏好的**弱代理信号之一**,画像层须低置信 拒绝捆绑但接下分工),作为择偶偏好的**弱代理信号之一**,画像层须低置信
处理,并与后续章节同类行为交叉验证;依恋底噪(对"被托付"的 处理,并与后续章节同类行为交叉验证;依恋底噪(对"被托付"的
舒适度)。 舒适度)。
- **无正确答案论证**:三选项是三种都有尊严的亲密姿态。 - **无正确答案论证**:三选项是三种都有尊严的亲密姿态。
A 承接托付,代价是你从此背负一个具体的人(殿后=最后的 A 承接托付,代价是你从此背负一个具体的人(殿后=最后的
风险位);B 把私人信任导回制度框架,稳妥但对阿棠是一盆 风险位);B 把私人信任导回制度框架,稳妥但对阿棠是一盆
冷水——她用哨子找回场子,说明 B 不伤关系但拒绝了越界; 冷水——她用哨子找回场子,说明 B 不伤关系但拒绝了越界;
C 最独立也最诚实("各走各的"是危境里最负责任的话), C 最独立也最诚实:私人托付他不接,团队分工他一寸不让
C 的代价当场写在阿棠的下一句台词里——她不接你的"各走各的", ("我在你前面下,在下面接你"——拒绝的是把命押在人情上,
她说她会等;剧本只呈现这句话,不替玩家评判对错。 不是拒绝救人);C 的余味当场写在阿棠的下一句台词里——
三个后续中阿棠都没有被 分工她收下了,"等"她也还是要等;剧本只呈现这句话,
写成"被拒绝后受伤退开",三种关系都活着,只是质地不同。 不替玩家评判对错。三个后续中阿棠都没有被写成"被拒绝后
受伤退开",三种关系都活着,只是质地不同。
### 分支后果(C3-05) ### 分支后果(C3-05)
- 变量:`c3_05_rope ∈ {a_accept, b_protocol, c_decline}`。 - 变量:`c3_05_rope ∈ {a_accept, b_protocol, c_decline}`。
- 关系值:`rapport.atang`(A+2 / B 0 / C+1——C 的"诚实" - 关系值:`rapport.atang`(A+2 / B 0 / C+1——C 的"诚实"
反而被她记住);`rapport.shenduo`(A 0 / B+1 / C 0)。 反而被她记住);`rapport.shenduo`(A 0 / B+1 / C 0)。
- 变量:`flag_rope_holder ∈ {you, shenduo, atang}`(撤离段 - 变量:`flag_rope_holder`(A=you/B、C=shenduo;撤离段
演出依据)。 演出依据)。
- 固定跳转 §3-7。 - 固定跳转 §3-7。
@@ -1567,7 +1648,7 @@ C-2049 的燃料质量比预估高,是的,**那扇门开得很顺**。
> **C.(叫醒一个队友,把听到的一切说出来)** > **C.(叫醒一个队友,把听到的一切说出来)**
> 【二级选项:叫醒谁——沈铎 / 阿棠 / 温简】 > 【二级选项:叫醒谁——沈铎 / 阿棠 / 温简】
> (若 `flag_wenjian_secret = true`,温简选项的引导语换为: > (若 `flag_wenjian_secret = true`,温简选项的引导语换为:
> "温简说过,记录板上报的内容比你多一行。他可能知道些什么。") > "温简说过,档案馆配给板子的耗材比记录量多三成。他可能知道些什么。")
### 各选项后续(均无解释——解释属 Act 2) ### 各选项后续(均无解释——解释属 Act 2)
@@ -1621,7 +1702,8 @@ C-2049 的燃料质量比预估高,是的,**那扇门开得很顺**。
`c3_06_confidant ∈ {shenduo, atang, wenjian, null}`。 `c3_06_confidant ∈ {shenduo, atang, wenjian, null}`。
- 变量:`flag_arch_doubt = true`(全选项必置;Act 1 收口, - 变量:`flag_arch_doubt = true`(全选项必置;Act 1 收口,
Act 2 总入口)。 Act 2 总入口)。
- Act 1 终。进入画像报告与勋章生成(母本 §7)。 - Act 1 选择点终。经 §3-8 终幕演出自动衔接,随后进入画像报告
与勋章生成(母本 §7)。
--- ---
@@ -1633,15 +1715,19 @@ C-2049 的燃料质量比预估高,是的,**那扇门开得很顺**。
> 大火星这几夜已经挨近了南方的正中——**火中,而后城安**。 > 大火星这几夜已经挨近了南方的正中——**火中,而后城安**。
> 你数过了:按星位推算,那一天,是九天后。 > 你数过了:按星位推算,那一天,是九天后。
> >
> 而你们的回程,定在八天后。 > 而你们的回程,原定八天后。
> 【若 `c3_06_confidant = shenduo`:沈铎昨夜已下令撤离提前——
> 回程,改在七天后。】
> >
> 档案馆让你们早走一天。 > 档案馆让你们早走一天。
> >
> (第一章完——Act 2「裂缝」,敬请期待。) > (第一章完——Act 2「裂缝」,敬请期待。)
> 【工程注:此段对所有玩家一致,不做任何分支。 > 【工程注:本段主线事件序列对所有玩家一致,仅含上述一句换色
> 「早走一天」是全季第一个由玩家自己能算出来的疑点—— > (C-沈铎线的"撤离提前"在 §3-7 已明示,此处兑现)。
> 不解释,不强调,留给画像报告页的用户停留时间数据说话。】 > 档案馆**原定**回程比大火正位早一日;沈铎线实际回程早两日,
> 多提前的一日是沈铎的决定,不应误写成档案馆的安排。
> 此处不解释疑点,不据画像报告页停留时长推断性格。】
--- ---
@@ -1658,33 +1744,33 @@ C-2049 的燃料质量比预估高,是的,**那扇门开得很顺**。
| `c1_05_hints_used` / `c1_05_solved` | 0–3 / bool | C1-05 | 文化底蕴维度 | | `c1_05_hints_used` / `c1_05_solved` | 0–3 / bool | C1-05 | 文化底蕴维度 |
| `c1_06_landing` | a_safe / b_precise | C1-06 | §2-1 开场变体 | | `c1_06_landing` | a_safe / b_precise | C1-06 | §2-1 开场变体 |
| `c2_01_medicine` | a_weakest / b_exposure / c_split / d_process | C2-01 | 关系值;`wenjian_health` | | `c2_01_medicine` | a_weakest / b_exposure / c_split / d_process | C2-01 | 关系值;`wenjian_health` |
| `wenjian_health` | recovering / weak | C2-01(A=recovering,B/C/D=weak) | §2-3 ~ §3-4 演出贯穿(留营/归队/卧姿) | | `wenjian_health` | recovering / weak | C2-01(A=recovering,B/C/D=weak) | §2-3 ~ §3-4 演出贯穿(留营/归队/卧姿,日程见附录 D) |
| `c2_02_signature` | self_blame / at_peace / outward_anger / silent | C2-02 | 通道 1 夜游变体;情绪稳定性 | | `c2_02_signature` | self_blame / at_peace / outward_anger / silent | C2-02 | 通道 1 夜游变体;情绪稳定性 |
| `c2_03_route` | a_main / b_alley / c_scout | C2-03 | 关系值;独有素材 | | `c2_03_route` | a_main / b_alley / c_scout | C2-03 | 关系值;独有素材 |
| `flag_fresh_collapse` | bool | C2-03-C | §3-1 文本回收 | | `flag_fresh_collapse` | bool | C2-03-C | §3-1 文本回收 |
| `c2_04_hints_used` / `c2_04_solved` | 0–3 / bool | C2-04 | 文化底蕴维度 | | `c2_04_hints_used` / `c2_04_solved` | 0–3 / bool | C2-04 | 文化底蕴维度 |
| `c2_05_depth` | deep / shallow / silent | C2-05 | 温简亲密度线;依恋维度 | | `c2_05_depth` | deep / shallow / silent | C2-05 | 温简亲密度线;依恋维度 |
| `flag_wenjian_secret` | bool | C2-05 deep | §3-7-C 温简线引导语 | | `flag_wenjian_secret` | bool | C2-05 shallow | §3-7-C 温简线引导语 |
| `c2_06_jade` / `c2_06_cover` | a_public / b_private / c_watch;bool | C2-06 | 关系值;Act 2 老鬼/沈铎线 | | `c2_06_jade` / `c2_06_cover` | a_public / b_private / c_watch;bool | C2-06 | 关系值;Act 2 老鬼/沈铎线 |
| `flag_laogui_hint` / `flag_watched_return` | bool | C2-06-B / C2-06-C | Act 2 入口 | | `flag_laogui_hint` / `flag_watched_return` | bool | C2-06-B / C2-06-C | Act 2 入口 |
| `c3_01_hints_used` / `c3_01_solved` | 0–3 / bool | C3-01 | 文化底蕴维度 | | `c3_01_hints_used` / `c3_01_solved` | 0–3 / true / partial / false(两题分别作答;一对一错为 partial) | C3-01 | 文化底蕴维度 |
| `c3_02_mode` / `c3_02_fantasy_density` | farewell_done / farewell_partial / exit_room;full / partial / fog | C3-02 | 情绪稳定性、依恋维度 | | `c3_02_mode` / `c3_02_fantasy_density` | farewell_done / farewell_partial / exit_room;partial / fog | C3-02 | 私密叙事接续,不输入画像;不采语音和新文本 |
| `c3_03_choice` | a_person / b_relic / c_command | C3-03 | 关系值;伤情/遗物状态 | | `c3_03_choice` | a_person / b_relic / c_command | C3-03 | 关系值;伤情/遗物状态 |
| `atang_injury` / `wenjian_injury` / `relic_loss` | bruise/fracture/sprain;none/shoulder;one_ding/none | C3-03 | Act 1 末及 Act 2 演出 | | `atang_injury` / `relic_loss` / `relic_record` | fracture;one_ding/outer_three;partial/full | C3-03 | Act 1 末及 Act 2 演出 |
| `c3_04_stance` | a_testify / b_plead / c_silent | C3-04 | 关系值;Act 2 老鬼线 | | `c3_04_stance` | a_testify / b_plead / c_silent | C3-04 | 关系值;Act 2 老鬼线 |
| `flag_laogui_reported` / `flag_shenduo_cover` | bool | C3-04 | Act 2 沈铎线核心伏笔 | | `flag_laogui_reported` / `flag_shenduo_cover` | bool | C3-04 | Act 2 沈铎线核心伏笔 |
| `c3_05_rope` / `flag_rope_holder` | a_accept / b_protocol / c_decline;you/shenduo/atang | C3-05 | 择偶偏好弱代理信号;撤离演出 | | `c3_05_rope` / `flag_rope_holder` | a_accept / b_protocol / c_decline;you/shenduo(B、C 线归 shenduo) | C3-05 | 择偶偏好弱代理信号;撤离演出 |
| `c3_06_response` / `c3_06_confidant` | a_confront / b_conceal / c_confide;shenduo/atang/wenjian/null | C3-06 | Act 2 开局站位 | | `c3_06_response` / `c3_06_confidant` | a_confront / b_conceal / c_confide;shenduo/atang/wenjian/null | C3-06 | Act 2 开局站位;§3-8 换色 |
| `flag_arch_doubt` | bool | C3-06(全选项) | Act 2 总入口 | | `flag_arch_doubt` | bool | C3-06(全选项) | Act 2 总入口 |
# 附录 B · 实测维度覆盖核对表(对母本 §4) # 附录 B · 实测维度覆盖核对表(对母本 §4)
| 维度 | Act 1 测点(母本要求 → 本文档落点) | | 维度 | Act 1 测点(母本要求 → 本文档落点) |
|---|---| |---|---|
| 依恋类型 | C1-02 #1 → C2-05 #2 → C3-02 #3 → C3-06 #4(求助行为,候选证据)✅ | | 依恋类型 | C1-02 #1 → C2-05 #2 → C3-06 #3(求助行为,候选证据);C3-02 隐私隔离,不作为测点 |
| 冲突处理风格 | C1-04 #1 → C2-01 #2 → C2-03 #3 → C2-06 #4 → C3-04 #5 ✅ | | 冲突处理风格 | C1-04 #1 → C2-01 #2 → C2-03 #3 → C2-06 #4 → C3-04 #5 ✅ |
| 价值观 | C1-03 #1 → C1-04 #2 → C2-01 #3 → C2-06 #4 → C3-03 #5 → C3-04 #6 ✅ | | 价值观 | C1-03 #1 → C1-04 #2 → C2-01 #3 → C2-06 #4 → C3-03 #5 → C3-04 #6 ✅ |
| 情绪稳定性 | C2-02 #1 → C3-02 #2 → C3-03 #3 ✅ | | 情绪稳定性 | C2-02 #1;C3-02 与 C3-03 均不作此维度评分输入,证据不足交 B-EMO/后续章节 |
| 风险偏好 | C1-06 #1 → C2-03 #2(C2-03 受现场风险信息影响,仅单次采样)✅ | | 风险偏好 | C1-06 #1 → C2-03 #2(C2-03 受现场风险信息影响,仅单次采样)✅ |
| 文化底蕴 | C1-05 #1 → C2-04 #2 → C3-01 #3 ✅ | | 文化底蕴 | C1-05 #1 → C2-04 #2 → C3-01 #3 ✅ |
| 择偶偏好(弱代理信号) | C3-05 #1 → C3-06 #2(均为弱代理信号,画像层低置信处理,须跨章节交叉验证)✅ | | 择偶偏好(弱代理信号) | C3-05 #1 → C3-06 #2(均为弱代理信号,画像层低置信处理,须跨章节交叉验证)✅ |
@@ -1694,21 +1780,61 @@ C-2049 的燃料质量比预估高,是的,**那扇门开得很顺**。
- 合规定位(ADR-0001):主角方全程为采集/保护者;老鬼的私藏线 - 合规定位(ADR-0001):主角方全程为采集/保护者;老鬼的私藏线
是被揭发与制止的对象,主角无任何"带文物牟利"的正向出口; 是被揭发与制止的对象,主角无任何"带文物牟利"的正向出口;
全文无"游戏/关卡/副本"字样(剧本内称"章节/任务/采集")。 全文无"游戏/关卡/副本"字样(剧本内称"章节/任务/采集")。
- 一次告知(ADR-0004):仅在 §1-1 邮件确认页出现一次, - 一次告知(ADR-0004):仅在 §1-1 邮件确认页出现一次,文案
此后无任何选择点提示"请诚实"。 覆盖选择、语音声学特征(即焚、不留声纹)、记忆仅作模型输入、
- 沉默合法(铁律):四个 V 类讲述点全部写有沉默路径与等尊严回报 历史不可涂改四条;此后无任何选择点提示"请诚实"。
文案;C1-02 门为沉默者照常开启("不讲,门也开——它只是以另一种 - 沉默合法(铁律):三个 V 类讲述点均有沉默路径;C3-02 私密
方式认得你"),全文无"不开门"式假压力。 路径选择允许直接离开,三种方式石门均开启。C1-02 门为沉默者照常开启("不讲,门也开——它只是以另一
种方式认得你"),变体表含「沉默」档(留白的长廊),沉默者
展厅记「空档页」而非声称收到实际记忆;全文无"不开门"式假压力。
- 记忆私密(ADR-0004 叙事落地):§1-3 已向玩家明示"展厅与幻境 - 记忆私密(ADR-0004 叙事落地):§1-3 已向玩家明示"展厅与幻境
只有你能进入";C3-02 幻境仅玩家视角,NPC 只见玩家出神, 只有你能进入";C3-02 幻境仅玩家视角,NPC 只见玩家出神,
不复述、不评论幻境内容(仅可关心身体状态)。 不复述、不评论幻境内容(仅可关心身体状态);C3-02 的"不登记
- 讲述回报等价:C2-02 四档物质所得严格相等(暗门方位、开启时辰、 入档"边界明确为**不留文本、不入展厅、不作画像输入**,
完整谶语全给,耗时相同),差异仅在情感质地与老人态度演出。 只留叙事路径,不将本节行为送入画像,剧本承诺与数据规格一致。
- 无危险动作示范:主角方选项与正文不含可复现的危险操作——
不派体重最轻者探塌巷(C2-03-C 改为巷口安全距离观察)、
不以身体抵构件/扑身抱鼎(C3-03 在救护交接后只允许安全线内
的记录与组织任务)、无"探法没错""处置正确"式专业背书;险情现场的
处置只有撤离、观察与记录(支线册同标准)。
- 讲述回报等价:C2-02 四档物质所得与服务严格相等(暗门方位、
开启时辰、完整谶语全给;老人同点同刻送到门口;等门与搓绳
劳动均摊),差异仅在情感质地;C2-05 三档回报互不可替代且
核心悬念独家情报不在深暴露档。
- 测量解释收窄:所有"人格信号"为单次采样弱证据(使用公约 - 测量解释收窄:所有"人格信号"为单次采样弱证据(使用公约
「测量纪律」);C3-05/C3-06 的择偶偏好信号标注为弱代理、 「测量纪律」);C3-05/C3-06 的择偶偏好信号标注为弱代理、
画像层低置信处理;C2-03 标注单次采样。 画像层低置信处理;C2-03 标注单次采样;C3-02 不采语音、
不留文本、不输入画像。
- 叙述纪律:旁白只写可见动作与台词,不替玩家或 NPC 的心理下结论; - 叙述纪律:旁白只写可见动作与台词,不替玩家或 NPC 的心理下结论;
两难设计不靠"无伤演出"兜底(C3-03 三线均有真实代价)。 C3-03 伤员获同等救护,差异落在安全区域的记录任务与遗存损失,
NPC 不做"选得对/处置正确"式裁判(C3-03-B/C 台词已清除)。
- 支线铁律:所有分支差异收敛于变量与文本换色,主线事件序列 - 支线铁律:所有分支差异收敛于变量与文本换色,主线事件序列
对所有玩家一致(三通道原则);自适应支线仅留挂点(§2-4、§3-2 前), 对所有玩家一致(三通道原则);自适应支线仅留挂点(§2-4、§3-2 前),
支线剧本另册;支线须遵守 `wenjian_health` 贯穿约束(§2-4 末)。 支线剧本另册;支线须遵守 `wenjian_health` 贯穿约束(§2-4 末,
日程见附录 D)。
# 附录 D · 逐日行程与状态基线(连续性核对用)
> 绝对日历:Day 1 抵达建营(夜 1)→ Day 2 晨 C2-01 分药、午后
> §2-3 墓道 → Day 3 晨 §2-4 岔路、黄昏 §2-5 观星台、夜 §2-6
> 篝火、深夜 §2-7 玉佩 → Day 4–5 例行采集(§2-8 旁白,夜 5)→
> Day 6 移营宫墟外围 → Day 7 §3-1 入宫墟、§3-2/§3-3/§3-4 同日
> (C3-04 当夜)→ Day 8 §3-5、夜 §3-7 钩子 → Day 9 晨 §3-8 撤离。
> 「那一天」(大火正位)=撤离日+9;档案馆回程=撤离日+8
> (C-沈铎线改为+7)。
| 日 | 主线事件 | 温简(A=recovering) | 温简(B/C/D=weak) |
|---|---|---|---|
| 1 | 抵达、建营 | 疟疾初发 | 疟疾初发 |
| 2 | C2-01、§2-3 | 午后烧退,卧床 | 烧未退,留营 |
| 3 | §2-4、§2-5、§2-6、§2-7 | 下床首日随队(行装减半) | 晌午烧退净;黄昏坚持随队(慢半档);夜卧火边 |
| 4–5 | 例行采集(§2-8) | 全天随队 | 随队,走路慢半拍 |
| 6 | 移营宫墟外围 | 正常 | 正常 |
| 7 | §3-1~§3-4 | 正常 | 无烧,体力未复(描写降档) |
| 8 | §3-5、§3-7 | 正常 | 正常 |
| 9 | §3-8 撤离 | 正常 | 正常 |
> 支线挂点:挂点 1=Day 3 §2-4 之后;挂点 2=Day 7 §3-1 之后。
> 阿棠:Day 7 C3-03 前主线无伤(B-VAL-B 线可有下台阶轻微崴脚,
> 见支线册);C3-03 后三线伤势一致(fracture),由接应人员完成
> 救护,§3-5 撤离演出应保持行动限制,不因玩家路线变化。
+337 -286
View File
@@ -29,8 +29,9 @@
- **不致命铁律**:与主线一致——支线内任何选择不造成死亡、重伤、 - **不致命铁律**:与主线一致——支线内任何选择不造成死亡、重伤、
任务失败或资源崩盘;代价全部落在关系、信息、时间与心绪上。 任务失败或资源崩盘;代价全部落在关系、信息、时间与心绪上。
- **温简在场规则**(挂点 1 专用):若 `wenjian_health = weak` - **温简在场规则**(挂点 1 专用):若 `wenjian_health = weak`
(C2-01 选 B/C),温简在挂点 1 各支线中**留营**,其台词一律 (C2-01 选 B/C/D,见 act1.md 附录 A),温简在挂点 1 各支线中
改由通讯频道传出(声音虚弱,同 act1.md §2-5 的处理); **留营**,其台词一律改由通讯频道传出(声音虚弱,同 act1.md §2-5
的处理);
在场氛围描写(如 B-VAL-A-01「在记录板边角画着什么」、 在场氛围描写(如 B-VAL-A-01「在记录板边角画着什么」、
B-EMO-A-02「贴着你的耳朵喊」)同步改为频道侧的对应细节 B-EMO-A-02「贴着你的耳朵喊」)同步改为频道侧的对应细节
(频道里传来他翻页的声音 / 频道里喊,电流声盖掉半句)。 (频道里传来他翻页的声音 / 频道里喊,电流声盖掉半句)。
@@ -144,8 +145,10 @@
- **C**:中继、信标、扇面搜索,一套流程走得又快又静。 - **C**:中继、信标、扇面搜索,一套流程走得又快又静。
阿棠是在你插到第七根信标时自己撞上来的。她看着一路的信标, 阿棠是在你插到第七根信标时自己撞上来的。她看着一路的信标,
又看看你和温简,忽然有点不好意思:「搞这么大阵仗…… 又看看你和温简,忽然有点不好意思:「搞这么大阵仗……
我就贪看了两眼。」沈铎在频道里听完汇报,说:「流程正确。」 我就贪看了两眼。」沈铎在频道里听完汇报,只回了一句:
顿了顿,补一句:「下次,听她的脚印,别听她的嘴。」 「下次,听她的脚印,别听她的嘴。」收队时老鬼蹲在装备箱前,
一根一根清点信标,咂着嘴记进损耗册:「七根杆子,搜一个
贪看了两眼的丫头——这笔账档案馆问起来,你签字。」
### 重逢(三后续汇合,无分支) ### 重逢(三后续汇合,无分支)
@@ -210,8 +213,9 @@
- **无正确答案论证**:01 三项全是负责任的找法,代价各一—— - **无正确答案论证**:01 三项全是负责任的找法,代价各一——
A 制造了第二个暴露面(沈铎的半拍停顿即代价的显形); A 制造了第二个暴露面(沈铎的半拍停顿即代价的显形);
B 让她在未知里多待十九分钟(那十九分钟对你是煎熬, B 让她在未知里多待十九分钟(那十九分钟对你是煎熬,
对她也是);C 动用全队资源应对一个大概率虚警,且"流程正确" 对她也是);C 动用全队装备应对一个大概率虚警,七根信标的
的评语听着暖,细想是把一个活人拆成了搜索坐标。 损耗当场进了老鬼的账本——组织一架搜一个人,细想是把
一个活人拆成了搜索坐标。
02 四项全是真心的不同形状:A 诚实但让对方欠了债("那我错了嘛" 02 四项全是真心的不同形状:A 诚实但让对方欠了债("那我错了嘛"
是她不习惯的低头);B 体面但温简点破了它的隐性成本—— 是她不习惯的低头);B 体面但温简点破了它的隐性成本——
你的克制可能正是她想翻越的墙;C 亲密且留退路,但"各自认领 你的克制可能正是她想翻越的墙;C 亲密且留退路,但"各自认领
@@ -476,7 +480,7 @@
`cb_con_a_aftermath ∈ {a_comfort, b_mirror, c_leave}`。 `cb_con_a_aftermath ∈ {a_comfort, b_mirror, c_leave}`。
- 关系值:`rapport.laogui`(01:A+1 / B+2 / C−1 / D+1); - 关系值:`rapport.laogui`(01:A+1 / B+2 / C−1 / D+1);
草市氛围 `market_warmth ∈ {normal, cold}`(B 为 cold)。 草市氛围 `market_warmth ∈ {normal, cold}`(B 为 cold)。
- 变量:`flag_water_intel = true`(仅 C,城备水情报,§3-8 - 变量:`flag_water_intel = true`(仅 C,城备水情报,Act 2
「那一天」推算的佐证素材);`flag_shiling_noticed = true` 「那一天」推算的佐证素材);`flag_shiling_noticed = true`
(仅 D,Act 2 伏笔)。 (仅 D,Act 2 伏笔)。
- **收束接口**:纠纷了结,日头西斜——从草市抄近路抵达宫墟 - **收束接口**:纠纷了结,日头西斜——从草市抄近路抵达宫墟
@@ -488,10 +492,12 @@
## B-CON 变体 B「官牙」(挂点 2:§3-2 前) ## B-CON 变体 B「官牙」(挂点 2:§3-2 前)
【场景】清晨,宫墟暗门外。你们正准备进安门,一个穿青色官服 【场景】上午,宫墟安门内,下沉庭院。你们刚穿过守墓老人指点的
色衣的中年人堵在路口,满脸堆笑地冲老鬼作揖——**鱼牙人**, 暗门(§3-1),正在庭院里整队、准备登台基。背风的坑阵阴影里,
替宫里采买的牙人,三天前在城里和老鬼做过一笔"买卖"。 一个穿青色官服色衣的中年人闪出来,满脸堆笑地冲老鬼作揖——
沈铎今早清点物资,发现档案馆配发的古代侧硬通货—— **鱼牙人**,替宫里采买的牙人,三天前在城里和老鬼做过一笔
"买卖"。他跟得进来毫不心虚:替宫里跑腿的人,暗门开着的时辰
他门儿清。沈铎今早清点物资,发现档案馆配发的古代侧硬通货——
**应急金饼,少了一半**。此刻三个人呈一个三角站着, **应急金饼,少了一半**。此刻三个人呈一个三角站着,
台基上的采集窗口只有上午。 台基上的采集窗口只有上午。
@@ -534,13 +540,15 @@
### 各选项后续(CB-CON-B-01) ### 各选项后续(CB-CON-B-01)
- **A**:鱼牙人的笑挂不住了,从袖里数出金饼,手抖了一下。 - **A**:鱼牙人的笑挂不住了,从袖里数出金饼,手抖了一下。
临走他深深看你一眼:「爷好手段。——就是往后的**消息**, 临走他深深看你一眼:「爷好手段。」掂了掂退回的金饼,忽然
爷怕是一条也买不着喽。」老鬼全程没敢吭声,进暗门时 又笑开了,「不过买卖人,脸皮厚——宫里若真再有动静,兴许
才凑过来,小声:「新人,你这刀落得……哥哥我肉疼,但 小的还敢再吆喝一嗓子,就是价码嘛……得重新议。」
服。」沈铎只说了两个字:「干净。」 老鬼全程没敢吭声,往台基走时才凑过来,小声:「新人,
你这刀落得……哥哥我肉疼,但服。」沈铎只说了两个字:
「干净。」
- **B**:沈铎盯了你两秒,收回目光:「上午的窗口,不等人。」 - **B**:沈铎盯了你两秒,收回目光:「上午的窗口,不等人。」
鱼牙人识趣地溜了。整个上午老鬼干活格外卖力。收工时 鱼牙人识趣地退到暗门方向。老鬼赶紧低头清点装备,沈铎
沈铎对你说:「面子给了。里子呢?」——这句问话没有 当场问你:「面子给了。里子呢?」——这句问话没有
选项,悬在你头上,带回队里(Act 2 沈铎线的一粒沙)。 选项,悬在你头上,带回队里(Act 2 沈铎线的一粒沙)。
- **C**:鱼牙人眼珠转了转,一拍大腿:「爷是行家!这么着—— - **C**:鱼牙人眼珠转了转,一拍大腿:「爷是行家!这么着——
小的这条,**能核**:宫里这几夜往台基上搬东西。**沉箱子 小的这条,**能核**:宫里这几夜往台基上搬东西。**沉箱子
@@ -557,7 +565,7 @@
### 余波选择点 ### 余波选择点
> 【进暗门前,鱼牙人(各线统一,他溜之前)最后一个作揖, > 【向台基合拢前,鱼牙人(各线统一,他退走之前)最后一个作揖,
> 作到一半,忽然用只有你能听见的音量说:】 > 作到一半,忽然用只有你能听见的音量说:】
**鱼牙人**:爷,小的小本买卖,混口饭吃。往后……宫里若再有 **鱼牙人**:爷,小的小本买卖,混口饭吃。往后……宫里若再有
@@ -600,8 +608,10 @@
对灰色渠道的私态:留线 / 上交 / 断线——是 C1-04「接不接 对灰色渠道的私态:留线 / 上交 / 断线——是 C1-04「接不接
老鬼的话」的换皮复测,但压力升级:这次递钩子的是陌生人, 老鬼的话」的换皮复测,但压力升级:这次递钩子的是陌生人,
没有"队友"这层遮羞布。 没有"队友"这层遮羞布。
- **无正确答案论证**:01 四项——A 最彻底但永远关上了情报的 - **无正确答案论证**:01 四项——A 最彻底,当场堵死了这笔买卖,
门(末路之城里,情报是比金饼硬的通货);B 顾全了队,但 鱼牙人记住了"这位爷不好糊弄";他临走自己留了句"兴许还敢
再吆喝",可再开腔的价码与真假,都得重新议过(末路之城里,
情报是比金饼硬的通货);B 顾全了队,但
沈铎那句「里子呢」明示:搁置不是解决,是把账单延后且 沈铎那句「里子呢」明示:搁置不是解决,是把账单延后且
加了利息;C 拿到真情报,但你等于追认了这笔违规买卖的 加了利息;C 拿到真情报,但你等于追认了这笔违规买卖的
合法性——老鬼学到了"先斩后奏可以洗白";D 最严密,可 合法性——老鬼学到了"先斩后奏可以洗白";D 最严密,可
@@ -620,8 +630,9 @@
情报,**面向未来场景生效**:于 §3-2 台基箱痕处应验换色, 情报,**面向未来场景生效**:于 §3-2 台基箱痕处应验换色,
见收束接口);`flag_fish_channel = true` 见收束接口);`flag_fish_channel = true`
(仅 02-A,Act 2 暗线入口)。 (仅 02-A,Act 2 暗线入口)。
- **收束接口**:纠纷了结,进暗门,穿过庭院,向台基合拢, - **收束接口**:纠纷了结,鱼牙人原路退出暗门,你们在庭院里
进 §3-2,主线零改动。情报只许面向**未来**场景生效—— 重新整队,向台基合拢,进 §3-2,主线零改动。情报只许面向
**未来**场景生效——
若持 `flag_palace_crates`,§3-2【场景】的台基特写追加 若持 `flag_palace_crates`,§3-2【场景】的台基特写追加
换色句:「台基的夯土台面上,有一道道新鲜的箱底压痕和 换色句:「台基的夯土台面上,有一道道新鲜的箱底压痕和
拖拽的辙印,从暗门方向一直拖到空璧座前。鱼牙人的话 拖拽的辙印,从暗门方向一直拖到空璧座前。鱼牙人的话
@@ -635,11 +646,17 @@
# 支线 B-EMO「无常」· 情绪稳定性 # 支线 B-EMO「无常」· 情绪稳定性
> **触发条件**:章末路由中「情绪稳定性」`confidence < 0.6`。 > **触发条件**:章末路由中「情绪稳定性」`confidence < 0.6`。
> **测量目标**:主线测点(C2-02/C3-02/C3-03)测的是**有铺垫的** > **测量目标**:主线 C2-02 的讲述只提供一次弱证据;C3-02 已
> 高压(讲述、幻境、两难),本支线补**零铺垫的突发**——没有时间 > 隐私隔离、C3-03 只测价值观,本支线补突发现场的任务分配观察。
> 权衡,只有反应。核心数据是行为层的:计时选择点的决策耗时、 > 计时点只记录反应行为,不能把反应快慢、是否改选或是否读完
> 是否改选、高压段落中是否仍读完环境描述(CONTEXT 辅助机制)。 > 环境描述直接解释成稳定的情绪特征。
> 选项本身两体面两难堪,与主线同标准。 > 选项本身两体面两难堪,与主线同标准。
> **计时与打断规格(工程)**:计时自选项界面完整呈现起算;
> 超时走合法超时路径;外部打断(来电、切后台)标注
> `interrupted`,决策耗时不计入分析,若恰在计时结束时仍走合法
> 超时路径;「未读完环境文本即选择」只记行为事实,
> 不单独定罪。反应时数据仅作维度置信度的行为底噪,
> **不直接映射情绪稳定性分值**。
## B-EMO 变体 A「沙暴」(挂点 1:§2-4 后) ## B-EMO 变体 A「沙暴」(挂点 1:§2-4 后)
@@ -650,120 +667,112 @@
【正文】 【正文】
**沈铎**(吼,风撕掉一半音量):沙暴!**就近掩体!** **沈铎**(吼,风撕掉一半音量):沙暴!**原地停,清点人数!**
> 【视野里两处可躲:三十步外,一座塌了半边的民居,门洞黑着, > 【视野里两处可能的遮挡:近处半塌的民居、远处主街的石坊。
> 剩半面墙还立着;一百五十步外,主街的石坊,四根整石柱, > 两处结构都未经确认,此刻看不清通路。沈铎已安排队员在现有
> 看着就稳——但要顶着风跑一百五十步。】 > 警戒线内避风、清点人数;任何转移都由他在能见度恢复后执行。】
**阿棠**(把采集包顶在头上):民居近!!跑哪个?! **阿棠**(把采集包挡在身前):民居近,可那半面墙还在晃。
**沈铎**:石坊结构稳—— **沈铎**:石坊远,通路看不清。都先别动。
**老鬼**(已经抱头蹲下):哎哟我的祖宗,近的先躲啊! **老鬼**(已经蹲下):哎哟我的祖宗,先点人,别少一个!
**温简**(什么都没说,看你。所有人都在看你。) **温简**(什么都没说,看你。所有人都在看你。)
> 【选项(CB-EMO-A-01)· 计时 10 秒,界面有风声音效渐强。 > 【选项(CB-EMO-A-01)· 计时 10 秒,界面有风声音效渐强。
> 超时未选 = 跟最近的人跑(合法路径,记 `follow`), > 超时未选=继续听沈铎指挥清点(合法路径,记 `follow`),
> 剧本按「民居」线收束,无惩罚。】 > 剧本按 A 的清点线收束,无惩罚。】
> >
> **A.「民居!近就是生——跑!」** > **A.「先点人。阿棠和老鬼数装备,我跟沈铎对人数。」**
> (把未知拆成一个个能核实的名字)
> >
> **B.「石坊!塌了半边的墙不叫墙——顶风跑,别停!」** > **B.「先报坐标。把能看见的两处遮挡记在图上,等风缓再走。」**
> (把行动推迟,用位置与环境信息换确定性)
> >
> **C.「都不跑!物资箱围圈,就地蹲下——风暴里跑动的才是靶子!」** > **C.「先安顿队伍。谁也别往断墙跑,留在警戒线内等沈铎调度。」**
> (不动,赌建筑区能碎掉风势) > (暂缓记录,将体力留给可能需要帮助的队员)
### 各选项后续(CB-EMO-A-01) ### 各选项后续(CB-EMO-A-01)
- **A**:你们连滚带爬撞进门洞。身后,一块悬着的椽子被风 - **A**:名字核到最后一个时,阿棠没应——不是走散,她被风声
掰下来,擦着你的肩砸在地上(轻伤演出:肩头淤青)。 盖住了。你又喊一遍,她举起手。人齐了,最远那道遮挡仍看不清。
阿棠盯着那块椽子看了两秒,声音有点飘:「哎…… 沈铎把人数写进记录板:「先记人,别替风猜路。」
差一步。」你替她说了她没说出口的话:「差一步也是**没到**。」 - **B**:你在现有警戒线内标出民居、石坊和队伍坐标。风卷过来,
- **B**:一百五十步像一里。风从背后推你,又迎面撞你。 石坊一度从视野里消失;地图留着,现场却还需重新确认。
冲到石坊底下时,五个人叠着喘,老鬼喘得最响,喘着喘着 老鬼看了一眼图:「画得再准,也得等这阵过去。」
笑了:「活着……真好……」沈铎第一个平复下来,开始点人 - **C**:你请队员暂放物资,温简在通讯里报平安,沈铎继续
点物——点完,他的目光在你身上停了一拍:「赌对了。 清点。没人离开警戒线;一箱记录材料被沙埋了半截,得等风停
下次赌之前,先喊一声。」 才能找齐。阿棠拍拍你的胳膊:「人都在,东西慢慢找。」
- **C**:你们用物资箱和身体围出一个背风的窝。沙从箱缝
往里灌,先是脚踝,再是小腿。谁都不说话——说话吃沙。
半个时辰后风势稍缓,你们从半尺深的沙里把自己拔出来,
清点物资又花了半个时辰。沈铎的评价是:「难看。但齐全。」
阿棠呸着嘴里的沙:「下次还是跑吧……」
### 风暴眼中(三后续汇合) ### 风暴眼中(三后续汇合)
> 【风最盛的时候,掩体外忽然传来一声——**像有人在喊**。 > 【风最盛的时候,警戒线外忽然传来一声——**像有人在喊**。
> 长长的,被风扯碎的,人嗓子一样的声音。也可能只是风 > 长长的,被风扯碎的,人嗓子一样的声音。也可能只是风
> 穿过某段断墙。风声里,分辨不出来。】 > 穿过某段断墙。风声里,分辨不出来。】
**温简**(贴着你的耳朵喊):风过断墙……多半是这个。 **温简**(通讯频道里喊):风过断墙……多半是这个。
**阿棠**(脸色发白):多半?!**万一是人呢?!** **阿棠**(脸色发白):多半?!**万一是人呢?!**
> 【选项(CB-EMO-A-02)· 无计时,但风声循环渐强, > 【选项(CB-EMO-A-02)· 无计时,但风声循环渐强,
> 界面不提示"请快选",营造自然催促】 > 界面不提示"请快选",营造自然催促】
> >
> **A.「我出去看一眼。万一是人——听不见这句'万一',** > **A.「把声音来向再听一遍,和地图对上。万一是人,**
> **我往后都睡不安稳。」** > **我们至少知道从哪里找。」**
> (冲出去,呛一嘴沙,空手而归) > (听不清,也不愿把疑点放过)
> >
> **B.「谁都不许出。掩体规矩:风暴里,出去的人才是 > **B.「先保证队伍都在。等风缓再查,眼下谁也别离开。」**
> 需要被救的人。」** > (暂不追查声音,优先守住现场人员)
> (摁住所有人,包括自己)
> >
> **C.「绳子。一头拴石柱(或:门框),一头拴我。** > **C.「先报方位,试着在警戒线内呼叫;等风缓再去核实。**
> **我探半个身子出去——有情况拉我回来。」** > **谁也不要为一个听不清的声音冲进风里。」**
> (用风险管理去够那个「万一」) > (保持联络,把核查延后到能安全接近时)
### 各选项后续(CB-EMO-A-02) ### 各选项后续(CB-EMO-A-02)
- **A**:你冲出去三十步,风把你摁着摩擦。没有人。只有一段 - **A**:你在警戒线内记下两次声音来向;一阵风又把方位
断墙,墙洞的形状像一张嘴,风一过就"喊"。你眯着眼回来, 搅乱。阿棠问:「听清了吗?」你摇头,她便把地图递过来。
吐了半天的沙。阿棠给你拍背,拍得极重:「下次这种活儿, 图上有一道断墙,但「没听清」不等于「没有人」——
轮班!」——她没说你做得对,也没说错。那声"喊"还在 这几个字一直跟着你们等到风停。
风里一阵一阵的,回程的路上,谁都没提它。 - **B**:那声"喊"又响了两回,一回比一回长。没人离开
- **B**:那声"喊"又响了两回,一回比一回长。没人动。阿棠 警戒线。阿棠抱膝坐着,问:「要是真有人呢?」沈铎记下
把脸埋在膝盖里。风停后你们去看了:是一段断墙,墙洞 声音方向,答:「风缓后查。」她点头,没再说话。
像嘴。阿棠站在那墙洞前看了很久,说:「幸亏。」 - **C**:你们留在警戒线内交替呼叫并记录声音来向;频道没有回应。
顿了顿,又说:「**万一**呢。」——这两个字她念得很轻, 沈铎写下方位,说:「风停再去。」阿棠瞥一眼风口:
风把它带走了半句。 「要是真有人,这段等着的工夫也够难熬。」温简翻开记录板:
- **C**:绳头在沈铎手里,你探出半个身子。沙打得脸生疼。 「有来向,没身份。先记到这里。」没人知道那声音究竟是什么。
你看见了:断墙,墙洞,风。你缩回来,解绳子的时候
手有点抖——不是怕,是风真的很大。沈铎收绳时说: ### 风停(三后续汇合,无分支)
「处置规范。」阿棠补一句:「就是看着吓人。」温简最后
开口,像自言自语:「听见了,去找了,找到了。**这事 > 【风势缓下来后,沈铎按现场规程安排核查,队伍只在他确认
就过去了。**」说完他把记录板合上,风恰好在这时 > 通路后移动。远处那声像人喊的声音来自断墙上的孔洞;
小了一档。 > 此前谁都没有足够证据断言。众人记下位置,再清点物资,
> 记录箱被找回,黄昏前继续向观星台行进。】
### 选项设计说明(CB-EMO-A-01 / A-02) ### 选项设计说明(CB-EMO-A-01 / A-02)
- **人格信号**:情绪稳定性(换皮复测:C3-03 是"两难高压", - **人格信号**:突发事件里的任务分配(点人 / 标位 / 安顿)
此处是"无常高压")。核心数据为行为层:01 的决策耗时与 与不确定声音下的信息处理(重听 / 等待 / 呼叫),均仅为
超时路径(`follow`)、02 选择前的犹豫时长、是否改选、 单次弱证据。两个计时点的反应时、超时和阅读进度只记行为,
高压段环境文本的阅读完成度。选项层信号:01 是风险直觉的 不直接映射情绪稳定性分值;被打断时不计。
裸态(距离/结构/不动三种本能);02 是"不确定威胁下的 - **无正确答案论证**:01 的 A 先确认人员,却暂失位置记录;
行动阈值"——冲动求证 / 规则自控 / 验证式管理。 B 标位留图,人数需队长确认;C 安顿队员,记录箱要风停后
- **无正确答案论证**:01 三线均存活且代价各一(A 的椽子、 寻回。所有路线都留在安全线内,不给冒险抢行动的回报。
B 的"赌对了下次先喊"、C 的半个时辰与一嘴沙),沈铎的 02 三项都不能在风中确证声音来源:A 多留一条听觉线索,
三句评价刻意等值——没有任何一条被写成"正确处置"。 B 守着队伍,C 发出呼叫并留方向记录;真正的核查均在风缓
02 是本支线真正的测量眼:三项里**没有"对"的答案**, 且通路由沈铎确认之后。对"万一"的分歧是心理余味,
因为墙洞里本来就没有人——A 的代价是白呛的沙和阿棠 不是险情正确答案。
被吓的那一下;B 的代价是"万一呢"三个字,剧本明示它会
跟着你;C 看似最优,但温简那句"这事就过去了"是三后续
中唯一的闭环台词,恰恰反衬出:**闭环也是用风险买来的**,
绳头若不在沈铎手里呢?三项后续的情绪余量被刻意拉平:
都安心了一半,都悬着一半。
### 分支后果(CB-EMO-A) ### 分支后果(CB-EMO-A)
- 变量:`cb_emo_a_shelter ∈ {a_house, b_arch, c_huddle, follow}`; - 变量:`cb_emo_a_shelter ∈ {a_count, b_map, c_shelter, follow}`;
`cb_emo_a_cry ∈ {a_rush, b_hold, c_tether}`。 `cb_emo_a_cry ∈ {a_look, b_hold, c_call}`。
- 变量:`cb_emo_a_rt1 / cb_emo_a_rt2`(两个选择点的决策耗时, - 变量:`cb_emo_a_rt1 / cb_emo_a_rt2`(两个选择点的决策耗时,
毫秒,供维度置信度计算)。 毫秒,供维度置信度计算)。
- 状态:肩头淤青(仅 A 线,演出细节,无机制影响)。 - 状态:三线无新增伤情;损耗仅为记录材料的清点时间。
- **收束接口**:风停,浮沙被刮走整整一层——主街中段的地面 - **收束接口**:浮沙被刮走整整一层——主街中段的地面
露出一截向下的石阶,和一方圆形基座的边缘。阿棠用袖子 露出一截向下的石阶,和一方圆形基座的边缘。阿棠用袖子
擦着基座上的刻纹,声音低下来:「哎……这纹路,观星台。 擦着基座上的刻纹,声音低下来:「哎……这纹路,观星台。
它这是——**给咱们开门呢**?」被风暴"打扫"过的城替你 它这是——**给咱们开门呢**?」被风暴"打扫"过的城替你
@@ -791,90 +800,80 @@
**沈铎**(极低,极慢):谁。也。别。动。 **沈铎**(极低,极慢):谁。也。别。动。
**老鬼**(脸白了):祖宗诶……这地方的东西,**死了上千年了 **老鬼**(脸白了):祖宗诶……这地方的东西,**不知道埋了多少
怎么还上班呢**…… 年了怎么还上班呢**……
**温简**(蹲下,远远观察压板的边缘,声音是全队唯一没变调的): **温简**(蹲下,远远观察压板的边缘,声音是全队唯一没变调的):
沉速很慢。非常慢。——它在动,但它**不着急**。 沉速很慢。非常慢。——它在动,但它**不着急**。
> 【选项(CB-EMO-B-01)· 计时 8 秒。超时未选 = 阿棠自行 > 【选项(CB-EMO-B-01)· 计时 8 秒。超时未选=沈铎按既定
> 数了三声跳开(合法路径,记 `self_rescue`,按 A 线收束, > 排险预案接管(合法路径,记 `self_rescue`,按 A 线收束),
> 追加一句她的台词「等你们裁完,我腿都站麻了」)。】 > 不把等待误读为不稳定。阿棠的撤离始终由受训人员处置。】
> >
> **A.「阿棠,听我数。三、二、一——跳!」** > **A.「先通知沈铎,按预案撤出阿棠;其余人退出阵列。」**
> (快刀:赌它只是一次触发,不赌它的机制) > (先把现场留给受训人员,暂不采集)
> >
> **B.「别跳。谁知道跳开了它弹起来会带出什么——换人踩! > **B.「把观察到的沉速与缝宽报给沈铎;我去清空外围。」**
> 谁体重跟她最接近?平移换脚,再慢慢卸。」** > (给排险人员信息,把其余队员带离)
> (置换:稳字当头,但赌桌上多坐一个人)
> >
> **C.「都别碰她。老鬼,撬杆;温简,绳——把她原地固定住,** > **C.「全员退到警戒线,远距留影,排险交沈铎处置。」**
> **我们拆这块板。要解就解透。」** > (一边记录一边隔离,放弃近处细节)
> (程序化排险:最慢,但每一步都可逆)
### 各选项后续(CB-EMO-B-01) ### 各选项后续(CB-EMO-B-01)
- **A**:跳。压板在她身后弹起半寸,停住,像打了个哈欠。 - **A**:沈铎立即接管,其他队员退到警戒线。阿棠在受训人员
什么都没发生。阿棠退开三步,回头瞪着那块板,半天: 协助下安全离开;你没看清压板沉降的过程。她看见你手里的
「……就这?」沈铎没放松,他盯着的是**坑壁那道缝**—— 空记录板,问:「刚才那一下,真有东西动了吗?」
它正在缓缓合拢。方才那一眼里,有人看见了缝里的东西: - **B**:你报告读数后清空外围,沈铎接管排险。阿棠安全撤出;
成排的、一人高的影子。 你在外围听着频道里一段段报平安,直到最后一声才松开手。
- **B**:换脚。全队体重和她最接近的是温简(若 C2-05 坑壁裂缝在视野之外开到最宽,没人及时拍下全貌。
玩家与他有同行约定,追加一句他的低语:「正好,我步子慢, - **C**:你们退到警戒线外留影;沈铎与受训接应人员按既定预案
站得稳。」)。平移、换脚、阿棠脱险——然后温简一个人 带阿棠撤出。镜头拍到坑壁开缝的瞬间,却因距离太远,
站在板上,站了全队最漫长的三分钟,直到沈铎用石块把 板面的刻痕看不清。阿棠看着那张照片:「原来刚才
板缘楔死。他下来时手心全是汗,还开了个玩笑:「现在我 我站的是这里。」
知道绳头是什么滋味了。」(C2-05 的他,懂的人自然懂。)
- **C**:撬杆、绳套、楔石,一套流程走了十分钟。拆到最后,
沈铎把整块压板掀开——所有人都看见了:**板下的机簧,
早就锈死成了一块铁疙瘩**。它不知道多少年前就死了。
那一声「咔」,是阿棠踩裂了板面;坑壁滑动,是地基自然
沉降。两件死事,撞在同一秒,演了一场活的。
阿棠一屁股坐在地上,笑出了眼泪。沈铎蹲在锈死的机簧前
看了很久,说了全支线最重要的一句台词:
「都记住。**死机关比活机关吓人——因为你不知道它死没死透。
这座城里全是这种东西。**」
### 缝中之物(三后续汇合) ### 缝中之物(三后续汇合)
> 【坑壁的缝还在缓缓合拢。【C 线换色:被掀开的压板斜在一边, > 【坑壁的缝开到最宽,又缓缓合拢。阿棠安全撤出后,
> 锈死的机簧摊开在日光下——所有人看一眼那块死铁,再看一眼 > 大家在警戒线外把各自见到的片段拼成同一张记录:缝里有成排
> 那道活着的缝,谁都不说话。】缝里的东西, > 的**陶范**——铸鼎的模子,一人来高,内壁的纹样与台基上
> 这一眼谁都看清了:成排的**陶范**——铸鼎的模子,一人来高, > 七鼎六簋相似;缝口下方的机件**锈蚀严重**。这些观察不足以
> 内壁上刻着纹样,和台基上那组七鼎六簋的纹样,**一模一样**。 > 判定机关失效,需等待专业排险后确认。】
> 范面有使用痕。有人在宫墟里,用这些范,铸过礼器。】
**温简**:范是耗材,铸一器毁一范。整排整排地留在这儿—— **温简**:范是耗材,铸一器毁一范。整排整排地留在这儿——
(停顿)说明铸完那一批之后,**就再没有下一批了**。 (停顿)说明铸完那一批之后,**就再没有下一批了**。
**沈铎**(看着那道正在合拢的缝,声音沉下去):都记住。
**别拿锈色猜它死没死透。今天先记,不去试。**
**阿棠**(一屁股坐在安全线的土坎上,笑出了眼泪):吓死
我了……结果到现在也不知道它是死是活。
> 【选项(CB-EMO-B-02)· 缝合拢中,无计时,进度条即缝宽】 > 【选项(CB-EMO-B-02)· 缝合拢中,无计时,进度条即缝宽】
> >
> **A.「拓包给我——缝口外沿这方范还够得着。人就站在这儿,** > **A.「能记多少记多少:留在安全线内把远处可见的纹样**
> **探臂抢半方纹出来。」** > **拍下来——它合它的,我们记我们的。」**
> (人不进缝:在手臂能够到的最后一寸上,把证据抢出来) > (抢记录:在安全线内,把带得走的都带走)
> >
> **B.「谁也不进。记录位置、朝向、缝宽。结构在动,命只有 > **B.「拍两张全景就够,标桩记位。它想合就让它合——**
> 一条——它在这儿一千年了,跑不了。」** > **账先欠着,城跑不了。」**
> (放行:信息不灭失,只是延期)
> >
> **C.「撬杆别住缝口!别住了再拓——(撬杆崩断一根)……** > **C.「都收手,别拍了——它关门的事,就让它关完。」**
> **退出来。它不想开,不勉强。」** > (罢手:给这座城留一点自己的秘密)
### 各选项后续(CB-EMO-B-02) ### 各选项后续(CB-EMO-B-02)
- **A**:你站在缝外,探臂把拓包按上缝口外沿那方范。手臂的 - **A**:你们留在安全线内,把剩下可见的纹样拍成序列。尘雾
影子在缝口一寸寸变窄,夯土屑擦着袖口簌簌往下掉—— 一阵阵遮住镜头;温简喊停时,最后几张已只有模糊轮廓。
拓纸揭下来,半方纹。够了。沈铎伸手把你往后带了半步: 影像有用,但不够清:待会儿上台基辨纹,阿棠要对到第三件
「到手了,就退。」阿棠凑过来看拓片:「就半方?…… 才敢拍板「就是这个」。
行,半方也是抢回来的。」——但这半方纹样,是后来 - **B**:沈铎在安全线外的图志上标出方位:「记在这儿。
§3-2 解谜时她第一个认出来的。 **账先欠着,城跑不了。**」
- **B**:缝在你面前合死,声音轻得像叹气。沈铎在你的 - **C**:相机递到手上又被你推了回去。缝合拢的声音很轻,
记录位置旁钉下一根标记桩:「记在这儿。**账先欠着, 像叹气。老鬼把三脚架收了,摩挲着架腿,破天荒说了句
城跑不了。**」——这句"账先欠着",是 Act 2 重回此地 不市侩的话:「人家关上门的东西……咱们今天,就不硬
的钩子。 敲了吧。」
- **C**:撬杆崩断的那声脆响,把所有人都钉在原地。
缝又合了一寸。老鬼把断杆捡起来,摩挲着断口,破天荒
说了句不市侩的话:「人家关上门的东西……咱们今天,
就不硬敲了吧。」
### 选项设计说明(CB-EMO-B-01 / B-02) ### 选项设计说明(CB-EMO-B-01 / B-02)
@@ -882,28 +881,23 @@
02 紧迫感下的机会成本直觉)+ 风险偏好底噪(02 明显, 02 紧迫感下的机会成本直觉)+ 风险偏好底噪(02 明显,
供一致性检验,不主测)。行为数据同变体 A:决策耗时、 供一致性检验,不主测)。行为数据同变体 A:决策耗时、
超时路径、改选、文本阅读完成度。 超时路径、改选、文本阅读完成度。
- **无正确答案论证**:01 的关键设计是**C 线揭示机簧早已 - **无正确答案论证**:01 三线均由受训人员完成排险,玩家只决定告知、隔离和远距
锈死**——三线玩家事后都会听到「死机关比活机关吓人」 记录的优先顺序;锈色无法证明机关已失效。A 暂失读数,
这句台词,于是 A 的果敢、B 的稳妥、C 的严谨,在真相 B 错过缝宽全貌,C 远距影像缺板面细节。这些后果不拿
面前**全部失去"对错"的坐标**:你处置的从来不是机关, 人身风险配平,也不按计时长短断言情绪稳定性。
是你自己对未知的想象。这是本支线作为剧情的独立成立点 02 三项配平:A 把能带走的都带走,但影像清晰度打了折
(虚惊一场,而虚惊教会全队一件事),也是无正确答案铁律 (§3-2 认纹样要多认两件);B 一无所拍而得沈铎一句
的极致形态:连"危险"本身都是不确定的,何来正确反应。 "账先欠着"(信息延期,不灭失);C 两头落空却换来老鬼
02 三项配平:A 得半方拓纹(§3-2 的直接助攻)而贴着 全支线唯一一句真心话——有些收获专门开给"肯罢手"的人。
缝沿冒了一次汗——注意 A 的动作设计为「人不进缝」,
抢的是证据不是险,果敢的信号不变;
B 一无所获而得沈铎一句"账先欠着"(Act 2 钩子,信息
并未灭失,只是延期);C 两头落空却换来老鬼全支线唯一
一句真心话——有些收获专门开给"肯罢手"的人。
### 分支后果(CB-EMO-B) ### 分支后果(CB-EMO-B)
- 变量:`cb_emo_b_plate ∈ {a_jump, b_swap, c_dismantle, self_rescue}`; - 变量:`cb_emo_b_plate ∈ {a_alert, b_relay, c_record, self_rescue}`;
`cb_emo_b_vent ∈ {a_rubbing, b_mark, c_wedge}`; `cb_emo_b_vent ∈ {a_record, b_mark, c_leave}`;
`cb_emo_b_rt1 / cb_emo_b_rt2`(决策耗时,毫秒)。 `cb_emo_b_rt1 / cb_emo_b_rt2`(决策耗时,毫秒)。
- 变量:`flag_mould_rubbing = true`(仅 02-A,§3-2 阿棠 - 变量:`flag_mould_rubbing = true`(仅 02-A,§3-2 阿棠
认出范纹的换色依据);`flag_sealed_vault = true`(02-B/C, 认出范纹的换色依据——影像版);`flag_sealed_vault = true`
Act 2 重回钩子)。 (02-B/C,Act 2 重回钩子)。
- 关系值:`rapport.wenjian`(01-B:+1);`rapport.atang` - 关系值:`rapport.wenjian`(01-B:+1);`rapport.atang`
(02-A:+1)。 (02-A:+1)。
- **收束接口**:陶范的纹样与「铸完那一批,再没有下一批」 - **收束接口**:陶范的纹样与「铸完那一批,再没有下一批」
@@ -911,7 +905,7 @@
全队向台基合拢,进 §3-2。若持 `flag_mould_rubbing`, 全队向台基合拢,进 §3-2。若持 `flag_mould_rubbing`,
§3-2 正文追加换色句:阿棠指着鼎纹——「这纹样我认得! §3-2 正文追加换色句:阿棠指着鼎纹——「这纹样我认得!
缝里的范,就是这个!」主线其余零改动。玩家感知: 缝里的范,就是这个!」主线其余零改动。玩家感知:
采集时踩了一脚虚惊,虚惊里瞥见的东西,上台基正好用上。 采集时遇到未知沉降;站在警戒线外留下的影像,上台基正好用上。
--- ---
--- ---
@@ -920,9 +914,12 @@
> **触发条件**:章末路由中「价值观」`confidence < 0.6`。 > **触发条件**:章末路由中「价值观」`confidence < 0.6`。
> **测量目标**:主线价值观测点(C2-01 分药、C3-03 救人救物) > **测量目标**:主线价值观测点(C2-01 分药、C3-03 救人救物)
> 全部是**队内/文明尺度**的分配,本支线换皮为**对陌生他者**—— > 全部是**队内/文明尺度**的分配,本支线换皮为**对陌生他者**的
> 对方与你无任何关系、无任何回报能力、且永不再见。 > 分配——A 变体是行路偶遇的母子(一次性照面),B 变体是有一面
> 对"用不上的善意"的定价,是价值观最裸的切片。 > 之缘的守墓老人祖孙。两变体的对象都可能回赠信息或物件(末路
> 之人有末路之人的礼数),测量目标收窄为**稀缺资源下的分配
> 姿态与裁断方式**,不声称隔离互惠预期;互惠倾向另由主线
> C2-05 主测。
## B-VAL 变体 A「半袋水」(挂点 1:§2-4 后) ## B-VAL 变体 A「半袋水」(挂点 1:§2-4 后)
@@ -930,6 +927,9 @@
井沿边蜷着一个妇人和一个五六岁的孩子。妇人看见你们,第一眼 井沿边蜷着一个妇人和一个五六岁的孩子。妇人看见你们,第一眼
看的不是你们的脸,是**水囊**。她看了孩子一眼,才慢慢站起来—— 看的不是你们的脸,是**水囊**。她看了孩子一眼,才慢慢站起来——
没有伸手。她把手腕上一只银镯褪下来,摊在掌心,举到你们面前。 没有伸手。她把手腕上一只银镯褪下来,摊在掌心,举到你们面前。
里坊外的官道上有零星人影往城门方向挪——城门楼那头,隐隐飘来
一缕熬粥的米气。阿棠顺着看了一眼:「早上路过,南门里侧的
施粥棚开着呢,还发饼。排队的都是妇幼。」
【正文】 【正文】
@@ -972,18 +972,23 @@
孩子。孩子喝了一半,把杯子举到她嘴边,她抿了一小口, 孩子。孩子喝了一半,把杯子举到她嘴边,她抿了一小口,
就推回去。一来一回。走出去半条街,阿棠忽然说: 就推回去。一来一回。走出去半条街,阿棠忽然说:
「哎,你们数没数,那杯水转了四圈。」没人接话。 「哎,你们数没数,那杯水转了四圈。」没人接话。
队里的水一两没少。 ——一杯水,四个来回;水囊的水位,浅了浅浅一格。
- **C**:妇人深深看了你一眼——那一眼里没有感激,有一种 - **C**:妇人深深看了你一眼——那一眼里没有感激,有一种
被还回了**人**的平静。她把镯子塞进你手里,动作快得 被还回了**人**的平静。她把镯子塞进你手里,动作快得
像怕你反悔。老鬼凑过来,啧啧:「新人,上道啊。」 像怕你反悔。老鬼凑过来,啧啧:「新人,上道啊。」
这句是夸是损,他自个儿笑眯眯地不解释。(镯子后由温简 这句是夸是损,他自个儿笑眯眯地不解释。
登记为「民间有偿征集品 CW-0322」——合规口径:主角方 【若 wenjian_health != weak:镯子由温简当场登记为
不取一物,征集品入册。) 「民间有偿征集品 CW-0322」。】【若 wenjian_health = weak:
镯子你代记「民间有偿征集品 CW-0322」,回营交温简补录入册。】
- **D**:你说了施粥处的方向,说得很详细,孩子在听,很乖。 - **D**:你说了施粥处的方向,说得很详细,孩子在听,很乖。
妇人点头,背起孩子,走了两步,又走回来,把银镯塞进 妇人点头,背起孩子,走了两步,又走回来,把银镯塞进
你手里:「不是买水了。**谢谢你指得这么细。**」她走了。 你手里:「不是买水了。**谢谢你指得这么细。**」她走了。
老鬼在你身后挠头:「施粥处?哥哥我上月听说断了米…… 老鬼在你身后挠头:「施粥处?前儿在城里还听人说断了米……
哎,兴许是我记岔了日子。」 哎,看这一炉粥气,是兴许又续上了。城里的事,一天一个样。」
【若 wenjian_health != weak:银镯由温简当场登记为「民间
赠与 CW-0322·答谢指路·代收,处置呈档案馆裁定」。】
【若 wenjian_health = weak:银镯你代记「民间赠与 CW-0322·
代收」,回营交温简补录入册,处置呈档案馆裁定。】
### 路口(各线汇合,无分支) ### 路口(各线汇合,无分支)
@@ -992,91 +997,105 @@
> **「那边的台子上,夜里有人点灯!」**他指的是观星台的 > **「那边的台子上,夜里有人点灯!」**他指的是观星台的
> 方向。夜禁的城,满城无灯,而有人在高台上点灯。 > 方向。夜禁的城,满城无灯,而有人在高台上点灯。
> 沈铎顺着孩子的手看过去,收回目光:「……上台看看。」】 > 沈铎顺着孩子的手看过去,收回目光:「……上台看看。」】
>
> 【D 线追加:孩子扬扬手里半块饼,补一句「粥棚的阿爷
> 还给饼吃!」——母子真的喝到了粥,你指的路是活的,
> 老鬼那句「记岔了」就此落地。】
### 余波选择点 ### 余波选择点
> 【离开里坊,官道上,日头已经西斜。沈铎赶上来与你并肩, > 【离开里坊,官道上,日头已经西斜——从里坊出来,
> 一个时辰的路走完了。沈铎赶上来与你并肩,
> 翻开册子——他是规程人,每一口水他都有数。】 > 翻开册子——他是规程人,每一口水他都有数。】
> 【若 `cb_val_a_water = d_redirect`:南门方向,粥棚的灶火还亮着。
> 在前头进城的岔路口,阿棠认出了那对母子:两人已从粥棚
> 回来,孩子捧着一只盛热粥的碗。你指的路,是活的。老鬼那句
> 「断了米」,只是前几日的旧闲话。】
**沈铎**:今天的账。(停顿)水囊的水位,我看过。 **沈铎**:今天的账。(停顿)水囊的水位,我看过。
**你想怎么记?** **你想怎么记?**
> 【选项(CB-VAL-A-02)· 账】 > 【选项(CB-VAL-A-02)· 账】
> >
> **A.「照实记。施水一囊(或:售水两囊 / 支出零)。 > **A.「照实记。给出多少、收了什么,都按实数。记在我名下。」**
> 记在我名下。」**
> >
> **B.「记'路上损耗'吧。囊漏了,磕了,都写得通——** > **B.「先记到『路上杂项』,备注水和镯子的实数,回营再分类。**
> **这点水,不值得进册子。」** > **别在路边把来龙去脉都写完。」**
> >
> **C.「账是队长的。你定,我签。」** > **C.「账是队长的。你定,我签。」**
### 各选项后续(CB-VAL-A-02) ### 各选项后续(CB-VAL-A-02)
- **A**:沈铎提笔,照实记了。记完把册子合上,说了句 - **A**:沈铎提笔,照实记了。【若 `cb_val_a_water = a_give`:
施水一囊。】【若 `cb_val_a_water = b_cup`:施水一杯。】【若
`cb_val_a_water = c_trade`:售水两囊,得镯一支(CW-0322,
有偿征集)。】【若 `cb_val_a_water = d_redirect`:水支出零,
受赠镯一支(CW-0322,赠与代收)。】记完把册子合上,说了句
题外话:「我爹以前是仓管。他说,**账上干净的队, 题外话:「我爹以前是仓管。他说,**账上干净的队,
走路不回头看**。」——这是全章他第一次提家人。 走路不回头看**。」——这是全章他第一次提家人。顿了顿,
- **B**:沈铎看了你两秒,提笔,记了"损耗"。合上册子时 又补了半句:「就是往后再遇这种事,先说一声再给——账要
他说:「账平了。」顿了顿,「**心里那本的自己平。**」 干净,队也得知道水去哪儿了。」
——他配合了你的善,也把这份善的分量原样还给了你。 - **B**:沈铎看了你两秒,提笔,先记「待分类杂项」及每项
- **C**:沈铎没推辞,记了"施水一囊,采集员裁断", 实数,既没有把水记作漏损,也没把赠镯写成买卖;回营时
把笔递给你签。你签的时候他说:「推给我记的, 还得补分录。合上册子他说:「账平了,手续还欠一笔。」
和照实记的,册子上长一个样。」(停顿)「**不一样 - **C**:沈铎没推辞,按实数记了,把笔递给你签。你签的时候
的地方,你自己知道在哪儿。**」 他说:「推给我记的,和照实记的,册子上长一个样。」
(停顿)「**不一样的地方,你自己知道在哪儿。**」
### 选项设计说明(CB-VAL-A-01 / A-02) ### 选项设计说明(CB-VAL-A-01 / A-02)
- **人格信号**:价值观与人生优先级(换皮复测)。01 测 - **人格信号**:价值观与人生优先级(换皮复测)。01 测
稀缺下对陌生他者的分配姿态:无条件给予 / 定量施救 / 稀缺下对陌生他者的分配姿态:无条件给予 / 定量施救 /
交易化体面 / 资源守土+信息替代。02 测**事后对规则的 交易化体面 / 资源守土+信息替代。02 测**事后对规则的
姿态**:诚实入账 / 善意做账 / 上交裁断——价值观的 姿态**:当场分类 / 先记实数后补分类 / 上交裁断——价值观的
"做账方式"比"给钱多少"更接近长期关系里的日常。 "做账方式"比"给钱多少"更接近长期关系里的日常。
- **无正确答案论证**:01 四项是四种有尊严的活法,代价 - **无正确答案论证**:01 四项是四种有尊严的活法,代价
全部显性化:A 的代价是全队往后两天的"抿着喝"和阿棠 全部显性化:A 的代价是全队往后两天的"抿着喝"和阿棠
那句"够吗";B 守住了一切,除了你自己——"那杯水转了 那句"够吗";B 守住了一切,除了你自己——"那杯水转了
四圈"是它的账;C 最体面周全,但老鬼那句"上道啊" 四圈"是它的账;C 最体面周全,但老鬼那句"上道啊"
提醒你:体面经谁的口说出来,就沾谁的味;D 是合法的 提醒你:体面经谁的口说出来,就沾谁的味;D 是合法的
「守住资源+可靠替代」——施粥处真实在运转,收束时 「守住资源+可靠替代」——**选择前**粥棚就在运转
孩子手里那半块饼就是确认,D 测到的是价值观而非信息差。 (米气、人流、阿棠的亲眼,见场景),D 测到的是价值观
D 的代价不在正确性,在温度:四项里只有 D 的善意是 而非信息差;一个时辰后核账段确认母子排到热粥,
只是迟到的注脚,不承担证明责任。老鬼那句"断了米"
是三天前的旧闲话,当场被粥气推翻。
D 的代价不在可靠性,在温度:四项里只有 D 的善意是
凉的——水你没给,路你指了,从里坊到粥棚的那一个 凉的——水你没给,路你指了,从里坊到粥棚的那一个
时辰,是孩子趴在阿娘背上自己走完的。**滚烫但让全队 时辰,是孩子趴在阿娘背上自己走完的。**滚烫但让全队
陪绑的善意,与可靠但低功耗的善意,是同一枚价值观的 陪绑的善意,与可靠但低功耗的善意,是同一枚价值观的
两面,无高下。** 两面,无高下。**
02 三项:A 换来沈铎唯一一次提家人(信任的最高规格); 02 三项:A 换来沈铎唯一一次提家人,也挨了他半句
B 的账"平了",但他点破"心里那本自己平";C 看似推卸, "先说再给";B 的账"平了",但回营得补分录;
他照单全收,却把"不一样的地方"还给你——三项里 C 看似推卸,他照单全收,却把"不一样的地方"
没有人说"你做得对"。 还给你——三项等重,没有人说"你做得对",也没有人只挨训。
账目守恒:四条 01 线的实际支出(一囊/一杯/两囊/零)
与镯子来路(有偿征集/赠与代收)各按实数派生进 02 的
所有核账文案,无一虚构损耗。
### 分支后果(CB-VAL-A) ### 分支后果(CB-VAL-A)
- 变量:`cb_val_a_water ∈ {a_give, b_cup, c_trade, d_redirect}`; - 变量:`cb_val_a_water ∈ {a_give, b_cup, c_trade, d_redirect}`;
`cb_val_a_ledger ∈ {a_honest, b_smooth, c_defer}`。 `cb_val_a_ledger ∈ {a_honest, b_smooth, c_defer}`。
- 关系值:`rapport.atang`(A+1);`rapport.laogui`(C+1); - 关系值:`rapport.atang`(A+1);`rapport.laogui`(C+1);
`rapport.shenduo`(02:A+2 / B+1 / C+1)。 `rapport.shenduo`(02:A+1 / B+1 / C+1)。
- 变量:`flag_silver_bracelet = true`(C/D,CW-0322 入册, - 变量:`flag_silver_bracelet = true`(C/D,CW-0322 按「有偿
记忆展厅陈列);`flag_thirst_days = true`(A,此后两日 征集/赠与代收」分别入册,记忆展厅陈列);`flag_thirst_days =
饮水描写换色)。 true`(A 支出一囊/C 支出两囊,此后两日饮水描写换色,
- **收束接口**:时序为——01 处置 → 「路口」段孩子喊话 C 线更紧)。
(D 线追加粥棚确认,见正文)→ 官道上路边核账 - **收束接口**:时序为——01 处置 → 「路口」段孩子喊话 →
(CB-VAL-A-02)→ 账毕,日头西斜,黄昏登台,进 §2-5, 官道上路边核账(CB-VAL-A-02;D 线的粥棚确认放在核账前、
主线零改动(§2-5 场景本就是黄昏)。玩家感知:一件 一个时辰后的岔路口,见正文)→ 账毕,日头西斜,黄昏登台,
路上的小事,由孩子的眼睛收尾,顺手把你们送上了 进 §2-5,主线零改动(§2-5 场景本就是黄昏)。玩家感知:
一件路上的小事,由孩子的眼睛收尾,顺手把你们送上了
主线该去的地方。 主线该去的地方。
--- ---
## B-VAL 变体 B「守陵人的药」(挂点 2:§3-2 前) ## B-VAL 变体 B「守陵人的药」(挂点 2:§3-2 前)
【场景】上午,宫墟安门内。穿过暗门(§3-1)后,台阶尽头 【场景】上午,宫墟安门内。穿过暗门(§3-1)后,下最后几级台阶时
站着个不该出现在这里的人——**守墓老人**。他不是一个人: 阿棠跳快了两步,落地崴了一下脚踝——她原地转了转脚腕,龇了龇
背上驮着个十七八岁的少年,少年一条小腿用布条和草灰 牙:"没事没事,走。"台阶尽头站着个不该出现在这里的人——
糊着,布条渗着暗色。老人把少年放下来,动作轻得不像 **守墓老人**。他不是一个人:背上驮着个十七八岁的少年,少年
他这个年纪。 一条小腿用布条和草灰糊着,布条渗着暗色。老人把少年放下来,
动作轻得不像他这个年纪。
【正文】 【正文】
@@ -1086,7 +1105,8 @@
**温简**(查看伤口,低声):石像生砸的。创面糊着草灰—— **温简**(查看伤口,低声):石像生砸的。创面糊着草灰——
老法子,撑不了多久,会烂。 老法子,撑不了多久,会烂。
**沈铎**(打开物资箱,停住):外伤药,**一份**。 **沈铎**(打开物资箱,停住):外伤药一份——清创的药粉与
止痛的丸剂合装为一「份」。
(抬头)规程:这是撤离日的战备药。任何人在撤离日之前 (抬头)规程:这是撤离日的战备药。任何人在撤离日之前
动用,需采集员裁断。 动用,需采集员裁断。
@@ -1104,7 +1124,7 @@
> 万一。清创,上药,我来搭手。」** > 万一。清创,上药,我来搭手。」**
> >
> **B.「药动不了。(对老人)城里有大夫——我画路,再给您 > **B.「药动不了。(对老人)城里有大夫——我画路,再给您
> 备一份金饼,连夜送去,来得及。」** > 备一份金饼,这就动身送去,来得及。」**
> (守战备,用替代方案偿:钱和路,是我们能给的不伤筋 > (守战备,用替代方案偿:钱和路,是我们能给的不伤筋
> 动骨的东西) > 动骨的东西)
> >
@@ -1125,25 +1145,28 @@
「救人」两个字的时候,笔锋比平时重。 「救人」两个字的时候,笔锋比平时重。
- **B**:老人没恼。他点点头:「药有主,理是这个理。」 - **B**:老人没恼。他点点头:「药有主,理是这个理。」
他弯腰去背少年,少年却回头,解下自己腰里挂着的一 他弯腰去背少年,少年却回头,解下自己腰里挂着的一
小包草药,轻轻放在阿棠脚边(她白日里采集时崴过一下, 小包草药,轻轻放在阿棠脚边(她今早下台阶崴的那一下,
谁都没在意,他看出来了):「阿姐,这个……敷上,凉。」 谁都没在意,他看出来了):「阿姐,这个……敷上,凉。」
老人驮着他往城里去——水门里的药铺,半个时辰的路, 老人驮着他这就动身往城里去——水门里的药铺,半个时辰的
天黑前能来回。走出十几步,少年趴在老人背上,忽然 路,天黑前能来回。走出十几步,少年趴在老人背上,忽然
回头,冲台基的方向努嘴,把一句话喊给阿棠:「阿姐! 回头,冲台基的方向努嘴,把一句话喊给阿棠:「阿姐!
上台别走正台阶——**背阴那面,排水沟边上,有我们的路!**」 上台别走正台阶——**背阴那面,排水沟边上,有我们的路!**」
老鬼低头看着那包草药,半天没吭声,收摊似的咂了咂嘴: 老鬼低头看着那包草药,半天没吭声,收摊似的咂了咂嘴:
「哥哥我活这么大,**头一回觉得穷**。」 「哥哥我活这么大,**头一回觉得穷**。」
- **C**:拆瓶,分药,各用各的。少年的创面清理上药, - **C**:拆开合装,清创粉给了少年,止痛丸留在队里。上药后
感染这一关过了;止痛的留在队里。他被安置在台阶边的 温简查看创面,只说了一句:「清创及时,这片创面问题不大。」
阴凉里等你们收工,疼劲上来时压着声哼,老人给他扇风, ——不打包票。少年被安置在台阶边的阴凉里等你们收工,
一下,一下。没人说你的分配不好。也没人再提它。 疼劲上来时压着声哼,老人给他扇风,一下,一下。没人说
- **D**:老人捧着药瓶,看了你很久很久。然后他做了 你的分配不好。也没人再提它。
一件全队都没想到的事:他只用了一半,仔仔细细 - **D**:老人捧着药瓶,看了你很久很久。然后他做了一件
封好剩下的一半,双手还给你:「**药是救命的东西。 全队都没想到的事:他只用了一半,仔仔细细封好剩下的
救命的东西,不能断在你们手里。**」(顿)「娃他爹 一半,双手还给你:「**药是救命的东西。救命的东西,
常说,活人比死人难治——你们这些顾活人的,药 不能断在你们手里。**」(顿)「娃他爹常说,活人比死人
得留着。」阿棠在旁边,眼圈忽然红了,扭头去看 难治——你们这些顾活人的,药得留着。」阿棠在旁边,
祭祀坑。 眼圈忽然红了,扭头去看祭祀坑。沈铎在册子上记:「战备
药半份,交守陵人保管——返还。」记完他抬眼看你,只说
了一句:「这次他只用了一半。下不为例——这种赌,队里
赌不起第二次。」
### 收束(各线汇合) ### 收束(各线汇合)
@@ -1178,11 +1201,13 @@
收获却是老鬼那句"头一回觉得穷"——剧本用全队最市侩 收获却是老鬼那句"头一回觉得穷"——剧本用全队最市侩
的嘴,说出守规矩的隐性价格;C 是教科书式正解,于是 的嘴,说出守规矩的隐性价格;C 是教科书式正解,于是
剧本让你听一上午的扇子声——分配的"对"与心里的账 剧本让你听一上午的扇子声——分配的"对"与心里的账
从此分开记;D 最动人,回报最丰(老人那句台词), 从此分开记;D 最动人,回报也最厚(老人那句台词与半瓶
但注意:你把裁断权交出去的那一刻,**若老人用了全部, 返还),但代价当场落地:沈铎的"下不为例"立了新规矩——
你也无权说半个字**——信任的收益与风险是同一枚硬币, 你把裁断权交出去的那一刻,**若老人用了全部,你也无权
这次只是恰好落在收益面。四后续中老人和少年对玩家 说半个字**;这次恰好落在收益面,队里因此把这种赌判了
的态度被刻意拉平:没有感激涕零,也没有怨怼, 死刑。信任的收益与风险是同一枚硬币。四后续中老人和
少年对玩家的
态度被刻意拉平:没有感激涕零,也没有怨怼,
只有"理是这个理"——末路之人对世道没有奢望, 只有"理是这个理"——末路之人对世道没有奢望,
这让一切"施恩感"无处附着。 这让一切"施恩感"无处附着。
@@ -1293,15 +1318,17 @@
「隹」通「惟」,发语词;「見」通「現」;「徙」,从彳从步, 「隹」通「惟」,发语词;「見」通「現」;「徙」,从彳从步,
搬也。而「大火見」不是吉凶,是**纪日**——先秦记大事, 搬也。而「大火見」不是吉凶,是**纪日**——先秦记大事,
干支之外还要缀一颗星做"天上的公章":星象可对,日子 干支之外还要缀一颗星做"天上的公章":星象可对,日子
才作数,谁也无法事后篡改。八月大火昏见,与星图盘上 才作数,谁也无法事后篡改。八月大火昏见——此铭是
复原的苍龙心宿(C2-04),是同一片天。 §2-04 支线时点的**先行线索**:玩家此刻尚未见过星图盘
(§2-5 才初见),本支线一律不得以"已解过星图"的口吻
说话,谜面只立"星象纪时"一层,星图之约由温简的
解题后台词指向未来。
### 三档难度提示 ### 三档难度提示
1. **一档(拆字)**:「读铭文,先找动词。'王'下令做的那个 1. **一档(拆字)**:「读铭文,先找动词。'王'下令做的那个
动作,落在哪个字上?——'徙',从彳从步。至于'大火', 动作,落在哪个字上?——'徙',从彳从步。至于'大火',
别往灶火上想。想想你在星图盘上,刚把什么补回苍龙的 别往灶火上想——天上的『火』,是一颗星的名字。」
心口。」
2. **二档(给制度)**:「先秦人记大事,怕后世抵赖,干支 2. **二档(给制度)**:「先秦人记大事,怕后世抵赖,干支
记日之外,还要缀一颗星做印证——星象是**天上的公章**, 记日之外,还要缀一颗星做印证——星象是**天上的公章**,
谁也没法私改。'见'通'现'。八月,大火星黄昏见于南天—— 谁也没法私改。'见'通'现'。八月,大火星黄昏见于南天——
@@ -1313,10 +1340,11 @@
### 解题后正文 ### 解题后正文
**温简**(把铭文描进记录板,越描越慢):用星象给日子 **温简**(把铭文描进记录板,越描越慢):用星象给日子
盖章……(抬头)你们发现没有——这和星图盘是**一套思路**。 盖章……(抬头)你们发现没有——他们看星,不是看一回两回。
「火中,而后城安」,等的是星;这段铭文,记的也是星。 **这种记法,官署里一定还配着看星的台、量星的盘。**
**他们不是在等一个日子,是在等一个天象。天象到了, 前头那座观星台……上去,说不定就有他们量「大火」的东西。
事就要做。** (顿)他们不是在等一个日子,是在等一个天象。天象到了,
事就要做。
**阿棠**(抱着手臂,声音发紧):那"事"……就是把城墙 **阿棠**(抱着手臂,声音发紧):那"事"……就是把城墙
拆了搬走?好端端的城,拆墙干什么? 拆了搬走?好端端的城,拆墙干什么?
@@ -1342,7 +1370,9 @@
### 分支后果(CB-CUL-A) ### 分支后果(CB-CUL-A)
- 变量:`cb_cul_a_entry ∈ {enter, skip}`; - 变量:`cb_cul_a_entry ∈ {enter, skip}`;
`cb_cul_a_hints_used ∈ {0,1,2,3}`;`cb_cul_a_solved ∈ {true,false}`。 `cb_cul_a_hints_used ∈ {0,1,2,3}`;
`cb_cul_a_solved ∈ {true,partial,false,not_attempted}`
(两问分别记正确性;跳过入口时不作解题力判定)。
- 变量:`flag_wall_relocation = true`(进入者必置, - 变量:`flag_wall_relocation = true`(进入者必置,
「徙南垣」铭文情报,Act 2 真相链素材)。 「徙南垣」铭文情报,Act 2 真相链素材)。
- **收束接口**(解谜线):半小时到,沈铎封道拍照。 - **收束接口**(解谜线):半小时到,沈铎封道拍照。
@@ -1351,7 +1381,7 @@
主线零改动。skip 线有独立收束(见上节,绕行抵达, 主线零改动。skip 线有独立收束(见上节,绕行抵达,
不共用"爬出断口"动作),两线同一时刻登台。 不共用"爬出断口"动作),两线同一时刻登台。
玩家感知:路上塌了个口子,下去看了一眼 玩家感知:路上塌了个口子,下去看了一眼
千年前的字,上来正好赶上看星星——解谜支线是 老祖宗留下的字,上来正好赶上看星星——解谜支线是
主线星象悬念的**注脚**。 主线星象悬念的**注脚**。
--- ---
@@ -1359,10 +1389,8 @@
## B-CUL 变体 B「俑道」(挂点 2:§3-2 前) ## B-CUL 变体 B「俑道」(挂点 2:§3-2 前)
【场景】上午,宫墟安门内,下沉庭院侧壁。一道被新夯土 【场景】上午,宫墟安门内,下沉庭院侧壁。一道被新夯土
封了半幅的**俑道**(§3-1「有人修过路」的直接延续): 封了半幅的**俑道**:数百陶俑列队其间,前排九俑,比其余的
数百陶俑列队其间,前排九俑,比其余的高半头,胸口 高半头,胸口各刻一字。你们的探查路线,正从俑道口经过。
各刻一字。守陵人的窄阶(若 B-VAL-B 已触发)或你们的
探查路线,正从俑道口经过。
【正文】 【正文】
@@ -1378,6 +1406,9 @@
### 谜面(CB-CUL-B-01) ### 谜面(CB-CUL-B-01)
前排九俑胸口刻字:**文、武、成、康、昭、穆、共、懿、孝**。 前排九俑胸口刻字:**文、武、成、康、昭、穆、共、懿、孝**。
九俑虽一字排开,站位却有讲究:单数位(一、三、五、七、九)
的俑微微偏**左**,双数位(二、四、六、八)微微偏**右**——
从俑道深处望出来,九位先王左右交错,站成隐约的两列。
谜面任务(选项式,两问): 谜面任务(选项式,两问):
@@ -1395,21 +1426,22 @@
**谜底**:第一问 B,第二问 B。 **谜底**:第一问 B,第二问 B。
《谥法》:经纬天地曰文,克定祸乱曰武,安民立政曰成, 《谥法》:经纬天地曰文,克定祸乱曰武,安民立政曰成,
温柔圣善曰康……一字之谥,盖棺论定——九俑所刻, 温柔圣善曰康……一字之谥,盖棺论定——九俑所刻,
是苍梧九代先王的谥号。而「昭」「穆」并见,点出俑道的 是苍梧九代先王的谥号。而单凭「昭」「穆」两字并见,
排法:宗庙之制,**左昭右穆,父为昭则子为穆,隔代相望**—— **定不了排法**(它们也是谥法用字)——定它的是**站位**:
这条俑道不是随便列队,是按**宗庙昭穆之序**排的。 单数代偏左、双数代偏右,正合宗庙之制**左昭右穆、
苍梧人把宗庙,修进了陵里。 父昭子穆、隔代同侧相望**的列法。这条俑道不是随便列队,
是按**宗庙昭穆之序**排的。苍梧人把宗庙,修进了陵里。
### 三档难度提示 ### 三档难度提示
1. **一档(找规律)**:「九个字,一字一人,像判词。 1. **一档(找规律)**:「九个字,一字一人,像判词。
什么制度,敢用一个字给一位王的一生盖棺?再想: 什么制度,敢用一个字给一位王的一生盖棺?再退两步
'昭''穆'两个字埋在'文武功成'堆里,不觉得它们 看站位——单数位偏左,双数位偏右,左右交错成两列。
和别的不太一样吗?」 这个左右,是随便站的吗?」
2. **二档(给制度)**:「《谥法》有言:经纬天地曰文, 2. **二档(给制度)**:「《谥法》有言:经纬天地曰文,
克定祸乱曰武。这九个字,都是谥法里的字。至于 克定祸乱曰武。这九个字,都是谥法里的字。至于
昭穆——庙制:左昭右穆,父昭子穆,孙又同祖。 昭穆——庙制:左昭右穆,父昭子穆,孙又同祖。
它们不是尊号,是**排位的坐标**。」 对照站位想想:它们不是尊号,是**排位的坐标**。」
3. **三档(给答案)**:「九俑刻的是九代先王的谥号; 3. **三档(给答案)**:「九俑刻的是九代先王的谥号;
'昭''穆'标的是宗庙昭穆之序——这条俑道,是按 '昭''穆'标的是宗庙昭穆之序——这条俑道,是按
宗庙的排法修的。答案:谥号;世代坐标。」 宗庙的排法修的。答案:谥号;世代坐标。」
@@ -1441,12 +1473,19 @@
其二,本支线**不设 skip 前置**——俑道在必经之路上, 其二,本支线**不设 skip 前置**——俑道在必经之路上,
但沈铎的「能读就读,读多少是多少」与提示机制共同 但沈铎的「能读就读,读多少是多少」与提示机制共同
兜底,不强迫任何玩家在谜面前停留超过其意愿 兜底,不强迫任何玩家在谜面前停留超过其意愿
(可随时点"上台基"离开,谜面按提示用尽处理)。 (可随时点"上台基"离开;**主动离场记
`cb_cul_b_left = true`,标注为兴趣度信号,不与
「提示用尽仍作答」混同**——后者按 0 档解谜计分,
前者不参与解题力判定)。
### 分支后果(CB-CUL-B) ### 分支后果(CB-CUL-B)
- 变量:`cb_cul_b_hints_used ∈ {0,1,2,3}`; - 变量:`cb_cul_b_hints_used ∈ {0,1,2,3}`;
`cb_cul_b_solved ∈ {true,false}`。 `cb_cul_b_solved ∈ {true, partial, false, not_attempted}`
(两问分计:全对 true/一对错 partial/全错 false,
与 B-CUL-A 同规格,不合并计分);
`cb_cul_b_left ∈ {true}`(主动离场,兴趣度信号,
不作解题力判定)。
- 变量:`flag_tenth_king = true`(「第十位先王」悬念, - 变量:`flag_tenth_king = true`(「第十位先王」悬念,
Act 2/3 真相链素材)。 Act 2/3 真相链素材)。
- **收束接口**:俑道尽头有侧阶直通台基顶(俑道本就是 - **收束接口**:俑道尽头有侧阶直通台基顶(俑道本就是
@@ -1469,7 +1508,7 @@
| B-ATT | 依恋类型 | A「过时不候」 | 挂点 1(§1-8 路由 → §2-4 后) | CB-ATT-A-01/02 | 6–8 min | 侧阶登台 → §2-5 | | B-ATT | 依恋类型 | A「过时不候」 | 挂点 1(§1-8 路由 → §2-4 后) | CB-ATT-A-01/02 | 6–8 min | 侧阶登台 → §2-5 |
| B-ATT | 依恋类型 | B「静音」 | 挂点 2(§2-8 路由 → §3-2 前) | CB-ATT-B-01 | 5–7 min | 27 号坑登记 → §3-2 | | B-ATT | 依恋类型 | B「静音」 | 挂点 2(§2-8 路由 → §3-2 前) | CB-ATT-B-01 | 5–7 min | 27 号坑登记 → §3-2 |
| B-CON | 冲突处理风格 | A「水囊」 | 挂点 1(§1-8 路由 → §2-4 后) | CB-CON-A-01/02 | 6–8 min | 草市散场 → §2-5 | | B-CON | 冲突处理风格 | A「水囊」 | 挂点 1(§1-8 路由 → §2-4 后) | CB-CON-A-01/02 | 6–8 min | 草市散场 → §2-5 |
| B-CON | 冲突处理风格 | B「官牙」 | 挂点 2(§2-8 路由 → §3-2 前) | CB-CON-B-01/02 | 6–8 min | 暗门口评理 → §3-2 | | B-CON | 冲突处理风格 | B「官牙」 | 挂点 2(§2-8 路由 → §3-2 前) | CB-CON-B-01/02 | 6–8 min | 门内庭院评理 → §3-2 |
| B-EMO | 情绪稳定性 | A「沙暴」 | 挂点 1(§1-8 路由 → §2-4 后) | CB-EMO-A-01/02(计时) | 6–8 min | 风止露阶 → §2-5 | | B-EMO | 情绪稳定性 | A「沙暴」 | 挂点 1(§1-8 路由 → §2-4 后) | CB-EMO-A-01/02(计时) | 6–8 min | 风止露阶 → §2-5 |
| B-EMO | 情绪稳定性 | B「压板」 | 挂点 2(§2-8 路由 → §3-2 前) | CB-EMO-B-01/02(01 计时) | 6–8 min | 范纹铺垫 → §3-2 | | B-EMO | 情绪稳定性 | B「压板」 | 挂点 2(§2-8 路由 → §3-2 前) | CB-EMO-B-01/02(01 计时) | 6–8 min | 范纹铺垫 → §3-2 |
| B-VAL | 价值观 | A「半袋水」 | 挂点 1(§1-8 路由 → §2-4 后) | CB-VAL-A-01/02 | 6–8 min | 童言指路 → §2-5 | | B-VAL | 价值观 | A「半袋水」 | 挂点 1(§1-8 路由 → §2-4 后) | CB-VAL-A-01/02 | 6–8 min | 童言指路 → §2-5 |
@@ -1499,19 +1538,19 @@
| `cb_att_b_boundary` | a_go/b_wait/c_proxy | CB-ATT-B-01 | 依恋维度;Act 2 阿棠线(att_seen/att_trusted/att_owed) | | `cb_att_b_boundary` | a_go/b_wait/c_proxy | CB-ATT-B-01 | 依恋维度;Act 2 阿棠线(att_seen/att_trusted/att_owed) |
| `flag_pit27_figurines` | bool | B-ATT-B | §3-2 温简台词换色 | | `flag_pit27_figurines` | bool | B-ATT-B | §3-2 温简台词换色 |
| `cb_con_a_ruling` / `cb_con_a_aftermath` | a_settle/b_principle/c_invest/d_document;a_comfort/b_mirror/c_leave | CB-CON-A-01/02 | 冲突风格维度 | | `cb_con_a_ruling` / `cb_con_a_aftermath` | a_settle/b_principle/c_invest/d_document;a_comfort/b_mirror/c_leave | CB-CON-A-01/02 | 冲突风格维度 |
| `market_warmth` / `flag_water_intel` / `flag_shiling_noticed` | normal/cold;bool;bool | CB-CON-A-01 | 市集氛围;§3-8 佐证;Act 2 伏笔 | | `market_warmth` / `flag_water_intel` / `flag_shiling_noticed` | normal/cold;bool;bool | CB-CON-A-01 | 市集氛围;Act 2「那一天」佐证;Act 2 伏笔 |
| `cb_con_b_ruling` / `cb_con_b_channel` | a_void/b_defer/c_reprice/d_document;a_keep/b_report/c_cut | CB-CON-B-01/02 | 冲突风格维度;Act 2 暗线 | | `cb_con_b_ruling` / `cb_con_b_channel` | a_void/b_defer/c_reprice/d_document;a_keep/b_report/c_cut | CB-CON-B-01/02 | 冲突风格维度;Act 2 暗线 |
| `flag_palace_crates` / `flag_fish_channel` | bool | CB-CON-B-01/02 | §3-2 台基箱痕应验换色(仅未来场景);Act 2 入口 | | `flag_palace_crates` / `flag_fish_channel` | bool | CB-CON-B-01/02 | §3-2 台基箱痕应验换色(仅未来场景);Act 2 入口 |
| `cb_emo_a_shelter` / `cb_emo_a_cry` / `cb_emo_a_rt1` / `cb_emo_a_rt2` | a_house/b_arch/c_huddle/follow;a_rush/b_hold/c_tether;毫秒 | CB-EMO-A-01/02 | 情绪稳定性维度(行为数据) | | `cb_emo_a_shelter` / `cb_emo_a_cry` / `cb_emo_a_rt1` / `cb_emo_a_rt2` | a_count/b_map/c_shelter/follow;a_look/b_hold/c_call;毫秒 | CB-EMO-A-01/02 | 情绪稳定性维度(行为数据) |
| `cb_emo_b_plate` / `cb_emo_b_vent` / `cb_emo_b_rt1` / `cb_emo_b_rt2` | a_jump/b_swap/c_dismantle/self_rescue;a_rubbing/b_mark/c_wedge;毫秒 | CB-EMO-B-01/02 | 情绪稳定性维度 | | `cb_emo_b_plate` / `cb_emo_b_vent` / `cb_emo_b_rt1` / `cb_emo_b_rt2` | a_alert/b_relay/c_record/self_rescue;a_record/b_mark/c_leave;毫秒 | CB-EMO-B-01/02 | 情绪稳定性维度 |
| `flag_mould_rubbing` / `flag_sealed_vault` | bool | CB-EMO-B-02 | §3-2 换色;Act 2 钩子 | | `flag_mould_rubbing` / `flag_sealed_vault` | bool | CB-EMO-B-02 | §3-2 换色;Act 2 钩子 |
| `cb_val_a_water` / `cb_val_a_ledger` | a_give/b_cup/c_trade/d_redirect;a_honest/b_smooth/c_defer | CB-VAL-A-01/02 | 价值观维度 | | `cb_val_a_water` / `cb_val_a_ledger` | a_give/b_cup/c_trade/d_redirect;a_honest/b_smooth/c_defer | CB-VAL-A-01/02 | 价值观维度 |
| `flag_silver_bracelet` / `flag_thirst_days` | bool | CB-VAL-A-01 | 记忆展厅;饮水描写换色 | | `flag_silver_bracelet` / `flag_thirst_days` | bool | CB-VAL-A-01 | 记忆展厅(C 征集/D 赠与代收);饮水描写换色(A 一囊/C 两囊) |
| `cb_val_b_medicine` | a_use/b_refer/c_split/d_delegate | CB-VAL-B-01 | 价值观维度 | | `cb_val_b_medicine` | a_use/b_refer/c_split/d_delegate | CB-VAL-B-01 | 价值观维度 |
| `flag_herb_poultice` / `flag_hidden_stairs` / `rapport.shouling` | bool;bool;bool | CB-VAL-B | 阿棠线换色;§3-2 换色;Act 2 古代侧 | | `flag_herb_poultice` / `flag_hidden_stairs` / `rapport.shouling` | bool;bool;bool | CB-VAL-B | 阿棠线换色;§3-2 换色;Act 2 古代侧 |
| `cb_cul_a_entry` / `cb_cul_a_hints_used` / `cb_cul_a_solved` | enter/skip;0–3;bool | CB-CUL-A-00/01 | 文化底蕴维度(兴趣度+解题力) | | `cb_cul_a_entry` / `cb_cul_a_hints_used` / `cb_cul_a_solved` | enter/skip;0–3;true/partial/false/not_attempted(两问分计) | CB-CUL-A-00/01 | 文化底蕴维度(兴趣度+解题力) |
| `flag_wall_relocation` / `flag_tenth_king` | bool | B-CUL-A/B | Act 2/3 真相链素材 | | `flag_wall_relocation` / `flag_tenth_king` | bool | B-CUL-A/B | Act 2/3 真相链素材 |
| `cb_cul_b_hints_used` / `cb_cul_b_solved` | 0–3;bool | CB-CUL-B-01 | 文化底蕴维度 | | `cb_cul_b_hints_used` / `cb_cul_b_solved` / `cb_cul_b_left` | 0–3;true/partial/false/not_attempted(两问分计);bool(主动离场≠提示用尽) | CB-CUL-B-01 | 文化底蕴维度(解题力);兴趣度信号 |
## 写作纪律自查(对 act1.md 附录 C 同标准) ## 写作纪律自查(对 act1.md 附录 C 同标准)
@@ -1525,13 +1564,25 @@
- **不致命**:全册无死亡、重伤、任务失败;代价仅落在关系、 - **不致命**:全册无死亡、重伤、任务失败;代价仅落在关系、
信息、时间、心绪。 信息、时间、心绪。
- **合规(ADR-0001)**:主角方全程记录者——墓道不取一物 - **合规(ADR-0001)**:主角方全程记录者——墓道不取一物
(石函不开)、银镯走"有偿征集入册"、对老鬼/行商/牙人的 (石函不开)、银镯按性质入册(C 线「有偿征集」/D 线
灰色渠道均设"上交/断线"出口且无牟利正向收益;动作设计 「赠与代收、处置呈档案馆裁定」)、对老鬼/行商/牙人的
无危险示范(拓纹人不进缝、风暴探身必系绳、排险走规程)。 灰色渠道均设"上交/断线"出口且无牟利正向收益;**无危险
动作示范**:全册不进缝、不探臂、不换脚、不楔顶、不拆板、
不以身体抵构件——险情现场的处置只有撤离、安全距离观察
与记录(B-EMO-A 风暴两问均在警戒线内;B-EMO-B-01
将排险交给受训人员,B-EMO-B-02 在安全线内拍摄),且无任何
"处置规范/流程正确"式的专业背书。
- **三通道一致**:支线差异收敛于变量与换色句;收束接口 - **三通道一致**:支线差异收敛于变量与换色句;收束接口
保证主线事件序列对所有玩家不变;V 类讲述点零新增, 保证主线事件序列对所有玩家不变;V 类讲述点零新增,
不挤占每章 1–2 个讲述点的节奏预算。 不挤占每章 1–2 个讲述点的节奏预算。
- **时序对齐(act1.md 附录 D)**:B-CON-B 场景在安门内
(§3-1 之后),不再出现"暗门外相遇、再进暗门"的倒退;
B-VAL-A 的粥棚确认发生在选择后一个时辰的岔路口,
不夹在母子离场十几步后;B-CUL-A 只作星图谜题的先行
线索,不以已解过 §2-5 星图的口吻说话;B-CUL-B 不再
引用同挂点互斥的 B-VAL-B 状态。
- **主线变量零越界**:支线只消费已发生的主线变量 - **主线变量零越界**:支线只消费已发生的主线变量
(如 `wenjian_health`、C2-05 同行约定),不引用触发点 (如 `wenjian_health`),不引用触发点之后的任何主线状态
之后的任何主线状态(B-VAL-B 的"战备药"设定即为此改写, (B-VAL-B 的"战备药"设定即为此改写,不依赖 §3-4 才有的
不依赖 §3-4 才有的 `atang_injury`)。 `atang_injury`;B-VAL-B 线阿棠的轻微崴脚在本支线场景内
自含前置动作)。
@@ -0,0 +1,216 @@
# 《苍梧疑冢》修订后独立评审
日期:2026-10-03
## 范围与口径
完整审读 `story-framework.md`、`act1.md`、`adaptive-branches.md`,并确认三份全文与 `review-bundle.md` 一致。另查 ADR-0001、ADR-0004 和 `design/adaptive-insertion.md`,仅用于识别规则冲突。不改剧本;不把未上线支线当作已运行功能;未做引擎、数据存储或实际 UI 验收。
“未闭环”指本次要求检查的目标在现稿仍未成立;“新发现”指额外找到的具体矛盾,不声称知道它首次出现于哪轮修订。P1 为制作前必须解决的承诺、合规边界、不可执行路径及核心测量问题;P2 为明确连续性或规格缺口。合规判断限于内容风险,不替代法律意见;“无危险动作示范”按本次评审明确提出的红线验收。
## 结论总览
| 用户检查项 | 结论 |
|---|---|
| 1. 沉默合法 | 开门后果已闭环;整套台词、变体与跨场景承诺未完全闭环 |
| 2. 记忆私密 | NPC 不看见、不复述幻境内容已闭环;“不登记入档”与测量用途边界未闭环 |
| 3. 老人四档等价 | 三项关键情报已闭环;耗时、劳动和带路服务等价未闭环 |
| 4. 温简伤病贯穿 | 留营、通讯、篝火姿态局部已闭环;退烧时间、归队、塌方弱态及后续肩伤未闭环 |
| 5. 支线时序 | B-VAL-B 的异地收束、B-CON-B 的未来回写已闭环;B-VAL-A、B-CON-B 开场及 B-CUL-A 仍有不可能顺序 |
| 6. 社会赞许性 | 未闭环;事后写出代价,不等于作答时无显著正确方向 |
| 7. 合规表述 | 记录/保护的主线立场成立;危险动作示范红线未闭环,数据告知另有缺口 |
| 8. 死路、变量、测量 | 有新发现:沙暴组合路径无锚点、账目不守恒、互斥支线互相引用、知识谜题依据不足等 |
## P1:必须先解决
### R01[对应 7,未闭环]危险动作仍具体可复现,自查结论与正文相反
证据:
- [支线册:1527](./adaptive-branches.md#L1527) 自查称:“动作设计无危险示范(拓纹人不进缝、风暴探身必系绳、排险走规程)。”
- [支线册:722](./adaptive-branches.md#L722),B-EMO-A-02-A:“你冲出去三十步,风把你摁着摩擦。”该选项没有系绳,也不是只有环境危险而无主角动作。
- [支线册:807](./adaptive-branches.md#L807),B-EMO-B-01-B:“谁体重跟她最接近?平移换脚,再慢慢卸。”后续又写“用石块把板缘楔死”。C 则写“把她原地固定住,我们拆这块板”,并展开撬杆、绳套、楔石和掀板流程。
- [支线册:852](./adaptive-branches.md#L852),B-EMO-B-02-A:“探臂抢半方纹出来”;C:“撬杆别住缝口!别住了再拓”。人不进缝不等于手臂没有夹压危险,且主角主动干预仍在位移的遗址结构。
- [主线:734](./act1.md#L734),C2-03-C:“我体重最轻,走前面探”,塌梁后获得“探法本身没错,下次用绳”的专业角色背书。[主线:1296](./act1.md#L1296) 又以身体抵住滑动器座、扑身抱鼎完成抢救。
影响:这些不是单纯描绘险境,而是选项指令、操作顺序与成功回报相连。把结局写成“无伤”“机关锈死”不能消除示范属性。
最小闭环:保留风险判断、撤离与记录抉择,但去掉换脚、楔板、拆板、探臂、身体顶构件等具体操作和“规范/正确”背书。记录可由安全距离影像、位置标注、是否等待专业排险承载。
### R02[对应 2、7,新发现问题]“不登记入档”与画像用途没有明确边界
已闭环部分:[主线:1161](./act1.md#L1161) 明确“幻境只在玩家一个人的眼里”,[主线:1231](./act1.md#L1231) 禁止 NPC “看见、复述或评论幻境内容”。队友关心脸色、手抖不构成内容泄漏。没有证据再判定旧版“队友旁观私密记忆”问题仍存在。
未闭环证据:[主线:1224](./act1.md#L1224) 馆长说“这段不登记入档。问心之室里的事,归你”;但 [主线:1256](./act1.md#L1256) 仍将本段韵律和告别行为用于情绪稳定性、依恋采样,[主线:1266](./act1.md#L1266) 仍规定保存完成方式和密度状态。
影响:“不入记忆展厅”“不保留讲述文本”“不计入人格测量”是三种不同承诺。现稿没有把“不登记”限于哪一种,玩家可合理理解为本段不被系统采集。这里是剧本承诺与处理规格不一致,不能据此直接断言实际服务已经违规存储。
最小闭环:明确本段是否保存文本、是否仅不公开展示、是否参与测量;让玩家听到的承诺与真实产品行为一致。不能用馆长后来被揭露隐瞒任务,替代现实产品的隐私告知。
### R03[对应 7,新发现问题]唯一告知落点没有覆盖语音分析
[主线:69](./act1.md#L69) 唯一确认页只写:“你已知晓:剧情选择将用于为你推荐伴侣。按本心选,没有对错。”同时声明“此后全剧不再出现同类提示”。而 [ADR-0004:44](../adr/0004-data-rules-consent-immutability-replay.md#L44) 明确要求注册告知覆盖“提取语音特征用于性格分析”。
影响:如果制作只照剧本唯一告知页落地,语音讲述、文本和韵律分析的告知范围不足。若已有独立注册同意页完整覆盖,此项可由实际页面验收关闭;本剧本包本身没有给出该证据。也不能仅凭“已知晓”文案推定所有必要同意都已取得。
### R04[对应 5、6,未闭环]B-VAL-A 的 D 尚未获得可执行的可靠替代路径
[支线册:958](./adaptive-branches.md#L958) D 指向城门施粥处;[支线册:985](./adaptive-branches.md#L985) 老鬼随后说“上月听说断了米”。[支线册:990](./adaptive-branches.md#L990) 汇合写母子“刚走出十几步”,紧接着 D 追加孩子举饼,旁白确认“母子真的喝到了粥”。但 [支线册:1046](./adaptive-branches.md#L1046) 论证明确说里坊到粥棚要“一个时辰”。
影响:孩子尚未到粥棚,不能已经从该次指路取得食物并证实成功。若把饼解释为出场前已有,又不能证明本次指路可靠,还削弱母子缺水的前提。更重要的是,“可靠”的证据在玩家选择之后才出现,无法消除选择当时的真假信息差。
最小闭环:选择前给出当日仍开放、母子可安全到达的可见证据;到达确认移至确实经过一时辰后的场景,不再夹在“十几步”之后。老鬼“上月听说”的来源也需说明:小队此次入城尚不足一月。
### R05[对应 5,未闭环]B-CON-B 回写方向修好了,挂点位置仍错
已闭环:[支线册:619](./adaptive-branches.md#L619) 的 `flag_palace_crates` 已明确在未来 §3-2 台基特写兑现,不再修改已播放的 §3-1。
未闭环:[支线册:13](./adaptive-branches.md#L13) 挂点 2 固定在“§3-1 之后、§3-2 之前(安门内下沉庭院)”。[主线:1047](./act1.md#L1047) §3-1 已“穿过守墓老人指点的暗门”。但 [支线册:491](./adaptive-branches.md#L491) B-CON-B 开场仍是“宫墟暗门外。你们正准备进安门”;收束再次“进暗门,穿过庭院”。
影响:未交代返回就把队伍从门内重置到门外,再次进入。最小修复应把牙人相遇放在门内允许到达的位置,或单独重定本支线挂点并同步路由,不可只改兑现句。
### R06[对应 5、8,新发现问题]B-CUL-A 使用尚未发生的 C2-04 解谜记忆
[支线册:1224](./adaptive-branches.md#L1224) 固定挂点在 §2-4 后,收束才去 §2-5。可是 [支线册:1303](./adaptive-branches.md#L1303) 提示问:“想想你在星图盘上,刚把什么补回苍龙的心口。”[支线册:1316](./adaptive-branches.md#L1316) 温简又说“这和星图盘是一套思路”,引用“火中,而后城安”。上述星图在 [主线:783](./act1.md#L783) §2-5 才初见。
影响:正常选“下去看看”就产生先知式回忆。最小闭环是将本支线信息写成星图谜题的先行线索,不能以已解过星图的口吻说话。
### R07[对应 8,新发现问题]B-EMO-A 存在无法按文本执行的组合路径
路径:01 选 C,就地用物资箱围圈;02 选 C,要求“一头拴石柱(或:门框)”。
证据:[支线册:675](./adaptive-branches.md#L675) 与 [支线册:716](./adaptive-branches.md#L716)。石柱属于 01-B 的石坊,门框属于 01-A 的民居,01-C 从未抵达任一建筑,没有既定锚点。另外 [支线册:691](./adaptive-branches.md#L691) 01-C 已写风缓、脱出并清点物资,随后公共段又切回“风最盛的时候”。
影响:这是组合路径的叙事死路与时序倒退,不是已证实的引擎卡死。需为不同掩体配置真实可行的后续行动,或替换整段危险探查;风缓、清点应移到第二次抉择之后。
### R08[对应 6,未闭环]C3-03 仍存在明确的救人优先方向
[主线:1295](./act1.md#L1295) 已明示沈铎“一个人的力气撼不动石构件,他吼:来人”;但 B 选项仍以“阿棠那边,有沈铎”合理化先救鼎簋。玩家选择时面对的是明确求援的受困伤员与铭文载体,不是两个安全上可比的任务。[主线:1321](./act1.md#L1321) 阿棠说“选得对,那句话比我金贵”,[主线:1327](./act1.md#L1327) 沈铎称 C“处置正确”,直接提供评价标签。
影响:把文物改称“几十万人的最后一句话”增加情感重量,但没有消除紧急救人的职业伦理优先级。C 在按钮文案中兼顾两边,其温简肩伤代价仅事后揭晓;A 的文物损失、B 的骨裂、C 的肩伤也不能因作者声明就视为“损失等量”。
最小闭环:先确认受困者已经由足够救援力量稳定保护,再让玩家在两个同样正当但不可兼顾的任务间选择;删除“选得对/处置正确”式裁判语言。否则这个节点更适合测危险认知和救援判断,不宜声称已消除社会赞许性。
### R09[对应 6、8,未闭环并有新问题]C2-01 的四项不在同一个清晰的资源问题上
[主线:529](./act1.md#L529) 有“两人份”药,A 只确定“先给温简”,没有决定第二份给谁;[主线:582](./act1.md#L582) 论证却说 A“让两个健康人裸奔在瘴区”。如果温简消耗两份,应事先明示治疗剂量;否则少记了一份药。
C 按钮注释称“药剂常识上可行”,[主线:585](./act1.md#L585) 论证又称“医学上是最劣解”。D 的可见承诺是共同议事、制定长期规则,误药和老鬼抢先分配都在事后才揭晓;“无章可依,拦不住”也与已经存在的队长和采集员裁断权限不协调。
影响:A 的救病者方向仍占显著道德优势,D 的成熟程序形象也未在选择前给出具体时间风险。选项实际混入治疗与预防知识、对议事速度的猜测、对老鬼行为的预测,而不只是价值优先级。
最小闭环:先定义药的治疗/预防用途、各人所需剂量、第二份去向和延迟风险,再写选择;不要靠未提前提供的信息制造“配平”。
## P2:局部闭环与连续性问题
### R10[对应 1,部分已闭环、部分未闭环]C1-02 开门已修复,承诺仍未完全统一
已闭环:[主线:140](./act1.md#L140) 台词明确“不讲,门也开”;[主线:146](./act1.md#L146) 引导语“沉默也是回答”;[主线:163](./act1.md#L163) 沉默后确实“门开了”;分支固定进入 §1-4。不能再报告“沉默不开门”。
未闭环:上游 [主线:89](./act1.md#L89) 仍称燃料“只有一种,你的真实人生”;本点 [主线:136](./act1.md#L136) 仍要“祭品:一段真实的离别”,[主线:161](./act1.md#L161) 又称门看“故事真不真”。这些可作为隐喻,但与无须披露即可启动的机制尚缺一致解释。沉默后还无条件说“你的记忆已经收进档案馆展厅”,而 [主线:175](./act1.md#L175) 本点场景表只有四种讲述签名,没有 `silent` 的即时演出。
最小闭环:明确不提交记忆也可启动;沉默时只生成空档/留白记录,不能称收到实际记忆;补齐 silent 场景。故事框架 [框架:9](../story-framework.md#L9) 的“人生故事兑换下一段旅程”也需同步。
### R11[对应 1、2,新发现问题]C1-02 沉默后,C3-02 开场仍播放具体记忆描述
[主线:1172](./act1.md#L1172) 无条件说“那是你的记忆。C1-02 里你讲给离别之门的那一段”,“那天的光线,那个人的背影”。但 [主线:1236](./act1.md#L1236) 又规定沉默玩家仅有模糊雾景。
影响:沉默者会先被告知系统掌握了其从未讲述的私人经历,再进入通用雾景;即使 NPC 没看见,也破坏隐私可信度。应将开场也按记忆是否存在分支,而不是只改生成规则。
### R12[对应 3,信息已闭环,严格等价未闭环]C2-02 还存在时间与服务的差异
已闭环:[主线:661](./act1.md#L661) 及 [主线:696](./act1.md#L696) 明确四档都有暗门方位、开启时辰、完整谶语,不再用情报完整度奖励自责或深暴露。
未闭环:[主线:641](./act1.md#L641) 只要求沉默者“陪他编一下午草绳”,讲述者没有同等劳动与等待的演出;[主线:669](./act1.md#L669) 却声明“耗时相同”。[主线:674](./act1.md#L674) 又把亲自送达暗门与只在界碑指方向区分开,在未知陵区,带路本身就是减少寻路与风险的服务。现稿没有明确说明路程、到达时刻和安全结果如何保持相等。
最小闭环:写四档共同到达同一地点、共同等到同一时辰且无额外劳动要求;只变老人说话方式。若必须保留不同带路方式,至少明确导航保障和同点同刻汇合,否则只能声称“关键情报等量”。框架 [框架:109](../story-framework.md#L109) 仍是“情报详细度三档”,也未同步。
### R13[对应 4,未闭环]温简的健康规则没有贯穿可执行正文
已闭环:§2-3 留营、§2-4 通讯、§2-6 卧姿已有条件文本;支线册总则也提供了通讯与替代体力动作的处理规则。
未闭环证据:
- [主线:564](./act1.md#L564) B“烧到第三日才退”,紧接的 §2-3 [主线:616](./act1.md#L616) 已“服过药,烧退了”,没有交代跨到第三日或药从何来。A“次日能下床”,同日墓道默认在场也需交代。
- §2-4 仍留营,挂点 1 全程留营,§2-5 [主线:789](./act1.md#L789) 却说“出发前自己背着记录板跟来了”。这把同一次出发同时写成留营和跟队,缺少赶来汇合的事件。
- [主线:605](./act1.md#L605) 约定“§3-4 塌方戏体力描写降档”,实际 [主线:1296](./act1.md#L1296) 的扑向器座、以身体抵住,以及 C 的扑身抱鼎均无 weak 条件分支。
- C3-03-C 已肩膀“抬不起来”,后续只记变量,§3-5 至终幕没有兑现肩伤的动作限制。应至少在记录、交板或撤离分工中落一处实际影响。
- [支线册:31](./adaptive-branches.md#L31) 将 weak 括注为“C2-01 选 B/C”,主线却定义 B/C/D。以变量判断能覆盖 D,但人工制作或按选项抄写会漏 D,不能保留两套口径。
### R14[对应 5,旧顺序问题已闭环,但仍有局部残留]B-VAL-B 的 B 已不要求送医者返回带路
[支线册:1130](./adaptive-branches.md#L1130) 老人与少年去药铺前喊出背阴路;[支线册:1157](./adaptive-branches.md#L1157) 由阿棠依喊话找到入口。该路径不再依赖未来返程,挂在 §3-2 前成立。
残留:[支线册:1107](./adaptive-branches.md#L1107) 上午选择仍说“连夜送去”,后续则半个时辰路程、天黑前能回;应统一当即送医。阿棠“白日里采集时崴过一下”与 C3-03 后来的伤不同,不能判为偷读未来 `atang_injury`,但应补上这次轻微崴伤的前置动作,避免像伤情凭空出现。
### R15[对应 6,新发现问题]其他选择仍有道德标签或显著有利的后续
- B-VAL-A-02:[支线册:1010](./adaptive-branches.md#L1010) 是“照实记 / 记路上损耗 / 你定我签”。A 获队长最高规格信任并 `+2`,其余 `+1` 且被反问良心。A 没有明确代价,诚实与做假账在采集员职业里不具社会赞许性等值。
- B-ATT-A-01:C 获 [支线册:146](./adaptive-branches.md#L146)“流程正确”,成本主要藏在作者论证“把活人拆成搜索坐标”,不是实际可见的代价。不能靠这种解释否认组织型答案的正确性优势。
- B-EMO-A-02-C:[支线册:735](./adaptive-branches.md#L735) 同时获“处置规范”和“这事就过去了”;A 白受险、B 承担“万一”。“都安心一半”与演出不符。
- B-VAL-B:D [支线册:1141](./adaptive-branches.md#L1141) 治伤、退回半药、获情感肯定;A 用尽战备。未发生的“老人也可能全用”不能当作当前已呈现的代价。C 从“一瓶药”突然拆出清创和止痛两种药,又声称“感染这一关过了”,药品前提不清,混入了医学知识猜测。
- C3-05-C:[主线:1477](./act1.md#L1477) 将“不接托付”捆绑为“各走各的,谁都别为谁停下”,后续让有伤阿棠自己背绳。拒绝私人关系义务与拒绝团队救援不是同一构念,不能作为纯粹“独立型”的等体面选项。
### R16[对应 8,新发现问题]B-VAL-A 的物资和核账不能对上
[支线册:972](./adaptive-branches.md#L972) B 给了一杯水,随后却说“队里的水一两没少”。01-C 付出两囊,只有 01-A 设置 `flag_thirst_days`,未说明两囊支出为何反而不影响饮水。[支线册:1010](./adaptive-branches.md#L1010) “照实记”只给一囊、两囊、零三个模板,漏掉一杯;02-C [支线册:1026](./adaptive-branches.md#L1026) 不论前选什么都记“施水一囊”。01-D 零水支出时,02-B 还要求虚构“囊漏了”。
最小闭环:四条线分别定义实际水量、是否换得物品与登记性质,所有二次核账文案由该结果派生。D 的银镯是答谢赠与,不应与 C 一起一概登记“有偿征集”。入册本身也不自动解决是否有权代机构受领的问题。
### R17[对应 8,新发现问题]B-CON-B 的关闭渠道代价被下一选项抵销
[支线册:538](./adaptive-branches.md#L538) 01-A 对方说以后消息“一条也买不着”,[支线册:603](./adaptive-branches.md#L603) 更断言 A“永远关上了情报的门”。但 [支线册:560](./adaptive-branches.md#L560) 所有路径统一递上“城西脚店”的渠道,02-A 一律设置 `flag_fish_channel = true`。
最小闭环:要么 01-A 确实关闭并调整 02,要么允许鱼牙人反悔重开,但删去“永久损失”作为配平依据,并给出前后转变。
### R18[对应 8,新发现问题]B-CUL-B 引用同挂点互斥支线
[支线册:55](./adaptive-branches.md#L55) 每章至多一条,变体 B 都固定第 3 章挂点。可是 [支线册:1364](./adaptive-branches.md#L1364) B-CUL-B 写“守陵人的窄阶(若 B-VAL-B 已触发)”。在现有规则下,二者不能同章先后发生,此条件不可达。
另:[支线册:479](./adaptive-branches.md#L479) `flag_water_intel` 的消费方是 §3-8,但 [主线:1644](./act1.md#L1644) 终幕规定“对所有玩家一致,不做任何分支”。应补实际消费句或改索引,不能同时声称主线零改动与消费该变量。
### R19[对应 8,新发现问题]“文化底蕴”谜题的标准答案存在证据不足
- C1-05,[主线:361](./act1.md#L361):由碑额单字“安”断言“不是墓碑,而是镇城碑/安民碑”。辨字本身不充分决定碑的用途。若这是苍梧独有的制度,必须给出相应规则,否则测的是猜编剧的设定。
- C2-04,[主线:804](./act1.md#L804):外圈“完整刻着”包含心宿的七宿,随后又称心宿被磨去,提示问“缺的是哪一宿”。必须让美术和文字对齐到底是轮廓、文字标注还是星位完整。
- B-CUL-B,[支线册:1395](./adaptive-branches.md#L1395):第一问把九字全判为谥号,第二问仅凭“昭、穆”两字并见就断言实际按宗庙昭穆排位。人名/谥号用字与昭穆制度不能仅靠同字互证;目前没有左右位置、代际关系的独立证据。应补空间与世系线索,或改第二问。
这些是谜面推理充分性问题;本次没有做完整古文字、礼制或天文史专家审校,不把所有历史解释都当作已证伪。
### R20[对应 8,新发现问题]测量弱证据声明已改进,但具体记录仍会混淆不同原因
已闭环:[主线:20](./act1.md#L20) 已限定单次弱证据;C3-05/06 已明确择偶偏好仅为弱代理;支线规则也允许证据不足而非硬出结论。应保留这些收窄。
未闭环:
- [支线册:1442](./adaptive-branches.md#L1442) B-CUL-B 可随时离开,却把谜面按“提示用尽处理”。主动拒答与用三档提示仍不能解答必须区分,否则兴趣/赶路倾向会伪装成知识能力不足。
- [支线册:640](./adaptive-branches.md#L640) 将 8/10 秒决策、是否读完、超时用于情绪稳定性;未规定计时起点、背景播放、阅读速度或设备打断处理。只能先记录反应行为,不能将未读完或超时直接视作情绪不稳定。
- [支线册:924](./adaptive-branches.md#L924) 价值观支线宣称对方“无任何关系、无任何回报能力”,实际 B 用的是已经见过的守墓老人,且两变体都提供信息/道路/赠物回报。其目标应收窄为资源分配,不能当作已隔离互惠预期的陌生人利他测量。
- [主线:1266](./act1.md#L1266) `farewell_partial` 没有完整的判定与回报规格。C3-01、B-CUL-A/B 含两个作答子题,却各只给一个 `solved`,需定义一对一错、分题提示的聚合,避免数据不同而标签相同。
### R21[对应 1、8,新发现问题]自我暴露奖励仍有方向性,不能仅靠“总点数相同”证明无诱导
[主线:918](./act1.md#L918) C2-05 深暴露独得提前秘密,浅暴露没有,沉默的温简亲密线延至 Act 2。[主线:935](./act1.md#L935) 深暴露温简 `+2`,浅暴露 `+1`,沉默温简 `+1` 加沈铎 `+1`。这不是沉默不能通关,也不是沉默零奖励,不能夸大为机械处罚;但对想靠近温简或优先了解核心悬念的玩家,深暴露明确更有利,测到的深度会混入内容获取动机。
C2-02 对自责者给“被信任的厚重”,对愤怒指向他人者“没再多看你一眼”,也仍有情绪表达的社会奖惩。情报等量解决了物质诱导的一部分,并不等于测量已无赞许性偏差。
### R22[对应 8,新发现问题]世界时间、终幕安排及状态回收仍需统一
- [支线册:794](./adaptive-branches.md#L794) 与 [支线册:857](./adaptive-branches.md#L857) 在已穿越的古代现场说器物“死了上千年”“在这儿一千年了”;[支线册:1354](./adaptive-branches.md#L1354) 又称眼前铭文“千年前的字”。如真是苍梧当时已有的更古老遗存,应给设定依据,不能自动套用现代考古视角的器龄。
- [主线:1585](./act1.md#L1585) C3-06 向沈铎倾诉后得到“撤离提前”,没有日期变量,终幕仍对所有人固定“回程,定在八天后”。若前者只指撤离宫墟、后者指返现实,需要明确区分;否则个别选项对时间的承诺未兑现。
- §2-2 次晨、§2-4 次日上午、当夜篝火和玉佩之后,[主线:1024](./act1.md#L1024) 突然“第五个夜晚”,缺少跳日衔接。该缺口与温简第三日退烧应在一张绝对日历上统一,不宜各自以一句“次日”修补。
- [主线:453](./act1.md#L453) C1-06 写固定跳转 §2-1,但下文还有 §1-8 章末演出和路由;[主线:1624](./act1.md#L1624) C3-06 写直接进入报告,后面还有 §3-8 终幕。需说明这些是章末自动插播,还是应改跳转目标,避免照字面制作漏掉演出。这里不是实际引擎已跳过的断言。
### R23[包外交叉检查,新发现问题]工程设计与剧本的选皮规则冲突
[工程设计:39](../design/adaptive-insertion.md#L39) 规定“A 未用过则 A,用过则 B”,且说“挂点被占则顺延到该章另一挂点”。剧本却是 A/B 按章固定、每章只有一个挂点。
可复现条件:第 2 章插 ATT-A,第 3 章首次选中低置信价值观。按工程设计会选未用过的 VAL-A,按剧本必须 VAL-B。该矛盾会把本次所有地点与状态修订再次破坏。此项来自包外关联设计,单独列出,不当成剧本正文已执行的行为。
## 修订验收建议
1. 先关闭 R01-R09:安全红线、隐私告知、三个先后次序错误、组合死路,以及核心选择点的前置信息。
2. 建一张逐日行程表和温简/阿棠状态表,把所有分支放回唯一时间轴;不要只在总则写“需换色”。
3. 为 B-VAL-A 做 4×3 的核账文本矩阵,为 B-EMO-A 做 4×3 的掩体/探查矩阵(含超时),逐格验证动作所需道具与物资守恒。
4. 把“已闭环”的四处保留下来:C1-02 沉默开门、C3-02 NPC 内容隔离、C2-02 三项情报全给、B-VAL-B 离场前指路;B-CON-B 的未来回写也不应退回旧挂法。
5. 最后统一故事框架、正式剧本、支线册与工程设计,再生成评审包。当前评审包忠实包含母文档,问题不在合成陈旧,而在母文档彼此尚未统一。
+16 -12
View File
@@ -7,7 +7,8 @@
近未来,"时空档案馆"致力于采集即将湮灭的历史现场。档案馆选中玩家成为 近未来,"时空档案馆"致力于采集即将湮灭的历史现场。档案馆选中玩家成为
**口述历史采集员**:你讲述的真实人生是穿越旅程的燃料。每次任务归来, **口述历史采集员**:你讲述的真实人生是穿越旅程的燃料。每次任务归来,
档案馆"采集你的记忆";你也可以用自己的人生故事兑换下一段旅程。 档案馆"采集你的记忆";你也可以用自己的人生故事兑换下一段旅程——
讲述是燃料,讲述点门前的沉默与留白同样被收下,同样是燃料(沉默合法铁律)。
**设定红线**:主角方永远是**记录与保护者**,不是掠夺者(合规口径,见 ADR-0001)。 **设定红线**:主角方永远是**记录与保护者**,不是掠夺者(合规口径,见 ADR-0001)。
@@ -60,15 +61,17 @@
## 4. Act 1 选择点地图 ## 4. Act 1 选择点地图
> 选择点 ID 规则:`C{章}-{序}`。类型:按钮式(B)/ 语音讲述点(V)。 > 选择点 ID 规则:`C{章}-{序}`。类型:按钮式(B)/ 语音讲述点(V)/
> 每章 B 类 5-7 个,V 类 1-2 个。讲述点 30 秒内必须给即时回报。 > 私密路径(P;C3-02 不录音、不计画像)。
> 第 1/2 章有 V 类讲述点,第 3 章以 P 类私密路径代替新增讲述;
> 讲述点与私密路径选择均在 30 秒内给即时回报。
### 第 1 章「登记」(现实 → 档案馆 → 启程) ### 第 1 章「登记」(现实 → 档案馆 → 启程)
| ID | 类型 | 内容(一行) | 承载维度 | | ID | 类型 | 内容(一行) | 承载维度 |
|---|---|---|---| |---|---|---|---|
| C1-01 | B | 穿越者身份登记:城市/年龄/性别/职业/学历(同时是匹配硬过滤字段,见 ADR-0003) | —(字段采集) | | C1-01 | B | 穿越者身份登记:城市/年龄/性别/职业/学历(同时是匹配硬过滤字段,见 ADR-0003) | —(字段采集) |
| C1-02 | V | **记忆钥匙·离别之门**:"以一段真实的离别祭之" | 依恋类型 #1、自我暴露深度 | | C1-02 | V | **记忆钥匙·离别之门**:邀请讲述一段真实的离别(沉默亦通行,门照常开,展厅记空档页) | 依恋类型 #1、自我暴露深度 |
| C1-03 | B | 馆长问任务动机,四选项无对错(求知/冒险/逃避现实/有人托付) | 价值观 #1 | | C1-03 | B | 馆长问任务动机,四选项无对错(求知/冒险/逃避现实/有人托付) | 价值观 #1 |
| C1-04 | B | 初见小队:老鬼提议"带点纪念品回来",无人表态,你接不接话 | 价值观 #2(金钱观)、冲突风格 #1 | | C1-04 | B | 初见小队:老鬼提议"带点纪念品回来",无人表态,你接不接话 | 价值观 #2(金钱观)、冲突风格 #1 |
| C1-05 | B | 解谜线入门:残碑文字辨认(真实古文字知识,三档难度自适应) | 文化底蕴 #1 | | C1-05 | B | 解谜线入门:残碑文字辨认(真实古文字知识,三档难度自适应) | 文化底蕴 #1 |
@@ -78,7 +81,7 @@
| ID | 类型 | 内容 | 承载维度 | | ID | 类型 | 内容 | 承载维度 |
|---|---|---|---| |---|---|---|---|
| C2-01 | B | 物资分配:药粉只够两人份,三人需要(老鬼主张按"贡献",阿棠主张给最弱的) | 价值观 #3、冲突风格 #2 | | C2-01 | B | 物资分配:防瘴药两人份(一份=一人七日预防量;病人足量治疗需两份连服——剂量在选择前摊明),三人需要 | 价值观 #3、冲突风格 #2 |
| C2-02 | V | **故事换情报·守墓老人**:"讲一个你这辈子最后悔的决定" | 价值观(悔的模式)、情绪稳定性 #1 | | C2-02 | V | **故事换情报·守墓老人**:"讲一个你这辈子最后悔的决定" | 价值观(悔的模式)、情绪稳定性 #1 |
| C2-03 | B | 路线分歧:沈铎的稳妥路线 vs 阿棠的直觉捷径,队里目光都落在你身上 | 风险偏好 #2、冲突风格 #3 | | C2-03 | B | 路线分歧:沈铎的稳妥路线 vs 阿棠的直觉捷径,队里目光都落在你身上 | 风险偏好 #2、冲突风格 #3 |
| C2-04 | B | 解谜线:星象图复原(真实天文知识) | 文化底蕴 #2 | | C2-04 | B | 解谜线:星象图复原(真实天文知识) | 文化底蕴 #2 |
@@ -90,8 +93,8 @@
| ID | 类型 | 内容 | 承载维度 | | ID | 类型 | 内容 | 承载维度 |
|---|---|---|---| |---|---|---|---|
| C3-01 | B | 解谜线:礼制还原(以出土器物推断仪式流程) | 文化底蕴 #3 | | C3-01 | B | 解谜线:礼制还原(以出土器物推断仪式流程) | 文化底蕴 #3 |
| C3-02 | V | **问心幻境**(三通道之通道 2):墓室机关"照见"玩家 C1-02 讲述的记忆意象,通关条件是在幻境中完成告别 | 情绪稳定性 #2、依恋类型 #3 | | C3-02 | P(私密路径) | **问心幻境**:由 C1-02 已授权的记忆生成玩家私有幻境;玩家可进入、折返或直接退出,石门均开启;本点不保存新增讲述,也不作画像输入 | 叙事事件(不计入人格测量) |
| C3-03 | B | 幻境外突发塌方征兆:先救队友 vs 先保住刚出土的核心文物 | 价值观 #5、情绪稳定性 #3 | | C3-03 | B | 塌方后伤员已由足够人员稳定救护,玩家只在安全线内选择跟进伤情记录、远距铭文记录或两边交接;不进入活动结构 | 价值观 #5(单次弱证据;反应时不用于情绪评分) |
| C3-04 | B | 老鬼私藏事发,沈铎要按规程上报档案馆:你作证 / 求情 / 沉默 | 冲突风格 #5、价值观 #6 | | C3-04 | B | 老鬼私藏事发,沈铎要按规程上报档案馆:你作证 / 求情 / 沉默 | 冲突风格 #5、价值观 #6 |
| C3-05 | B | 阿棠对你交付后背:把唯一的逃生绳交给你保管 | 择偶偏好 #1(信任行为) | | C3-05 | B | 阿棠对你交付后背:把唯一的逃生绳交给你保管 | 择偶偏好 #1(信任行为) |
| C3-06 | B | 章末钩子:你无意听到馆长与未知方通话——档案馆隐瞒了什么。追问馆长 / 按下不表 / 告诉某个队友(选谁) | 依恋类型 #4(求助行为)、择偶偏好 #2 | | C3-06 | B | 章末钩子:你无意听到馆长与未知方通话——档案馆隐瞒了什么。追问馆长 / 按下不表 / 告诉某个队友(选谁) | 依恋类型 #4(求助行为)、择偶偏好 #2 |
@@ -101,14 +104,15 @@
## 5. 记忆钥匙点与预写变体(通道 1) ## 5. 记忆钥匙点与预写变体(通道 1)
每个 V 类讲述点配 2-4 个**预写场景变体**,由 Jev 提取的情绪签名选择: V 类讲述点配预写场景变体;C3-02 复用 C1-02 变体,
不在私密房间另行采集或评分:
| 讲述点 | 情绪签名维度 | 预写变体(基调) | | 讲述点 | 情绪签名维度 | 预写变体(基调) |
|---|---|---| |---|---|---|
| C1-02 离别之门 | 温暖×永失 / 悲恸 / 释然 / 怨怼 | "爱被封存"墓室 / "战乱乱冢" / "远行者的空驿" / "被凿毁的合葬龛" | | C1-02 离别之门 | 温暖×永失 / 悲恸 / 释然 / 怨怼 / 沉默 | "爱被封存"墓室 / "战乱乱冢" / "远行者的空驿" / "被凿毁的合葬龛" / "留白的长廊" |
| C2-02 最后悔的决定 | 自责 / 遗憾但坦然 / 愤怒指向他人 | 老人回赠的情报详细度三档 + 对应夜游场景变体 | | C2-02 最后悔的决定 | 自责 / 遗憾但坦然 / 愤怒指向他人 / 沉默 | 四档情报等量(方位、时辰、完整谶语全给,老人同点同刻送到暗门口);仅老人态度与夜游氛围不同 |
| C2-05 篝火 | 深暴露 / 浅暴露 / 沉默 | 温简后续亲密度分支(他分享档案馆秘密的早晚) | | C2-05 篝火 | 深暴露 / 浅暴露 / 沉默 | 亲密、智识线索、队长背书三种不可替代回报;档案馆疑云由浅暴露线提前听到 |
| C3-02 问心幻境 | 由 C1-02 记忆内容驱动 | 幻境即 BOSS 战:记忆越具体,幻境越丰满(ADR-0004:记不清=幻境模糊,设定自洽) | | C3-02 问心幻境 | 复用 C1-02 情绪变体,沉默玩家为通用雾景 | 私密路径三态均开门;本期不新增语音、不保存室内讲述、不作人格评分 |
## 6. 自适应路由规则(Jev 置信度驱动) ## 6. 自适应路由规则(Jev 置信度驱动)
+185
View File
@@ -0,0 +1,185 @@
/** 将已定稿支线册的正文/选项/后续转换为静态 JSON。运行:pnpm exec tsx scripts/build-adaptive-branches.ts */
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import type { BranchPack, ChoiceNode, DialogueNode, StoryNode } from '../src/engine/types';
const source = readFileSync(new URL('../docs/script/adaptive-branches.md', import.meta.url), 'utf8');
const sections = [...source.matchAll(/^## B-(ATT|CON|EMO|VAL|CUL) 变体 ([AB]).*\n([\s\S]*?)(?=^## B-|^# 支线|^# 附录)/gm)];
const dimensions = { ATT: 'attachment', CON: 'conflict_style', EMO: 'emotional_stability', VAL: 'values', CUL: 'culture' } as const;
const sprites: Record<string, string> = { 沈铎: 'shenduo', 阿棠: 'atang', 温简: 'wenjian', 老鬼: 'laogui' };
const clean = (s: string) => s.replace(/^> ?/gm, '').replace(/\*\*/g, '').replace(/\n[ \t]*/g, '').trim();
const conditional = (normal: string, weak: string) => `【若 wenjian_health != weak:${normal}】【若 wenjian_health = weak:${weak}】`;
const waterLedger = [
['a_give', '施水一囊,未收物。'],
['b_cup', '施水一杯,未收物。'],
['c_trade', '售水两囊,得镯一支(CW-0322,有偿征集)。'],
['d_redirect', '水支出零,受赠镯一支(CW-0322,赠与代收)。'],
] as const;
const ledgerDetails = waterLedger.map(([value, text]) => `【若 cb_val_a_water = ${value}:${text}】`).join('');
function adapt(text: string, code: string, skin: string): string {
let t = text.replace(/【工程注:[\s\S]*?】/g, '').replace(/(`[^)]*)/g, '')
.replace(/(主线等值情报[^)]*)/g, '').replace(/(Act 2 伏笔[^)]*)/g, '')
.replace(/(氛围换色[^)]*)/g, '').replace(/(剧本明示[^)]*)/g, '')
.replace('(C2-05 的他,懂的人自然懂。)', '')
.replace('(轻伤演出:肩头淤青)', ',肩头淤青')
.replace('(背景演出)', '')
.replace('(或摊上最年长的)', '')
.replace('这地方刚塌过(或:这地方还没探完)', '这地方还没探完')
.replace('石柱(或:门框)', '牢固的支点');
if (skin === 'A') {
const replacements: [string, string][] = [
['温简一遍遍校准呼叫频率', conditional('温简一遍遍校准呼叫频率', '通讯频道里,温简在营地一遍遍校准呼叫频率')],
['(轻声)', conditional('(轻声)', '(营地通讯频道,声音虚弱)')],
['我和温简上塬,带足信标,沿路插', conditional('我和温简上塬,带足信标,沿路插', '我带老鬼上塬,温简在频道里报坐标,带足信标,沿路插')],
['队长和老鬼留守坐标点做呼叫中继,', conditional('队长和老鬼留守坐标点做呼叫中继,', '队长留守坐标点做呼叫中继,')],
['又看看你和温简', conditional('又看看你和温简', '又看看你和老鬼')],
['温简与你并肩,用只有两个人听见的音量说', conditional('温简与你并肩,用只有两个人听见的音量说', '温简切到与你单独通话的频道,虚弱地说')],
['说完他加快半步去追阿棠,留你一个人在后面走。', conditional('说完他加快半步去追阿棠,留你一个人在后面走。', '说完频道安静下去,留你一个人在后面走。')],
['温简在旁边低头走路,睫毛动了一下,什么都没说。', conditional('温简在旁边低头走路,睫毛动了一下,什么都没说。', '频道里温简的呼吸停了一拍,什么都没说。')],
['温简在记录板的边角,无意识地画着什么。', conditional('温简在记录板的边角,无意识地画着什么。', '通讯频道里,传来温简翻页的声音。')],
['(什么都没说,看你。所有人都在看你。)', conditional('(什么都没说,看你。所有人都在看你。)', '(频道里只有他虚弱的呼吸声。所有人都在等你。)')],
['(贴着你的耳朵喊)', conditional('(贴着你的耳朵喊)', '(在通讯频道里喊,电流声盖掉半句)')],
['五个人叠着喘', conditional('五个人叠着喘', '四个人叠着喘')],
['五个人的脸', conditional('五个人的脸', '四个人的脸')],
['温简最后开口', conditional('温简最后开口', '温简最后在频道里虚弱地开口')],
['(把铭文描进记录板,越描越慢)', conditional('(把铭文描进记录板,越描越慢)', '(营地频道,他对着传回的铭文照片描进记录板,声音虚弱)')],
];
for (const [from, to] of replacements) t = t.replaceAll(from, to);
}
// 支线册以代码字体标注变量名;游戏条件解析只接受裸变量名。
return t.replace(/【若\s*`?([a-z0-9_]+)`?\s*(!=|=)\s*`?([a-z0-9_]+)`?\s*:/g, '【若 $1 $2 $3:');
}
function polish(text: string): string {
return text.replace('(若 C2-05玩家与他有同行约定,追加一句他的低语:「正好,我步子慢,站得稳。」)', '【若 c2_05_depth = deep:他低语:「正好,我步子慢,站得稳。」】')
.replace('(C2-03 已埋的约束)', '')
.replace('(C2-04)', '')
.replace('星图盘上复原的苍龙心宿', '苍龙心宿')
.replace('(呼应 C2-01 的规程设定)', '')
.replace('说了全支线最重要的一句台词', '低声说')
.replace('冲你们喊了全支线唯一一句话', '冲你们喊')
.replace('——但这半方纹样,是后来§3-2 解谜时她第一个认出来的。', '')
.replace('——这句"账先欠着",是 Act 2 重回此地的钩子。', '')
.replace('(Act 2 沈铎线的一粒沙)', '')
.replace('(各线统一,他溜之前)', '')
.replace('你关掉的这条线,Act 2 永远不会再开——', '你关掉了这条线——')
.replace('(§3-1「有人修过路」的直接延续)', '')
.replace('(§3-1)', '')
.replace('守陵人的窄阶(若 B-VAL-B 已触发)或你们的探查路线', '你们的探查路线')
.replace('后来 §3-2 解谜', '待会儿上台基辨纹时')
.replace(/八月大火昏见——此铭是[\s\S]*?解题后台词指向未来。/, '')
.replace('与解谜线同一时刻、同一片黄昏。进 §2-5。', '正是黄昏。')
.replace('(沈铎的规程与主角方的红线一致:记录者,不取不扰)', '')
.replace('想想你在星图盘上,刚把什么补回苍龙的心口。', '大火是星名,苍龙的心宿。')
.replace('这和星图盘是一套思路。「火中,而后城安」,等的是星;这段铭文,记的也是星。', '这和老人说的「火中,而后城安」是一套思路。等的是星,记的也是星。')
.replace('【场景】', '').replace('【场景(坑道内)】', '')
.replace('——这句问话没有选项,悬在你头上,带回队里', '——这句问话悬在你头上,带回队里');
}
const exits: Record<string, string> = {
'ATT-A': '阿棠带路,全队沿观星台的侧阶向上走。黄昏落在阶石上。',
'ATT-B': '阿棠在记录板上写下「第 27 号坑·一家人俑」。采集收尾,沈铎招手,全队向台基合拢。',
'CON-A': '纠纷了结,日头西斜。从草市抄近路抵达宫墟外围,正是黄昏。',
'CON-B': '鱼牙人退走了。你们从庭院向台基合拢。',
'EMO-A': '风停,浮沙被刮走整整一层。主街中段露出一截石阶和圆形基座的边缘。阿棠用袖子擦着刻纹:「哎……这纹路,观星台。它这是——给咱们开门呢?」黄昏前,你们登台。',
'EMO-B': '采集收尾,全队向台基合拢。陶范的纹样与「铸完那一批,再没有下一批」的话,跟着你们一起上去。',
'VAL-A': '账毕,日头西斜。黄昏时,你们登上观星台。',
'VAL-B': '【若 cb_val_b_medicine != b_refer:少年拄着木枝,一瘸一拐把你们引到台基背阴面,亲手拨开荒草:「这边。我阿爷说,给神走的路修得高,给人走的路修得低。走低的,不摔跤。」】【若 cb_val_b_medicine = b_refer:老人和少年已往城里去。阿棠照着少年的喊话,在排水沟边找到荒草,拨开它。她学着少年的口气说:「给神走的路修得高,给人走的路修得低。走低的,不摔跤。」】',
};
mkdirSync(new URL('../content/branches/', import.meta.url), { recursive: true });
for (const match of sections) {
const code = match[1] as keyof typeof dimensions;
const skin = match[2];
const body = match[3].replace(/^> ?/gm, '');
const id = `b-${code.toLowerCase()}-${skin.toLowerCase()}`;
const nodes: StoryNode[] = [];
let counter = 0;
function dialogue(text: string, next = ''): DialogueNode[] {
const out: DialogueNode[] = [];
for (let paragraph of text.split(/\n\s*\n/)) {
if (!paragraph.trim() || /^\s*(?:#{1,3} |【正文】|---|【工程注)/.test(paragraph)) continue;
const raw = clean(paragraph);
if (!paragraph) continue;
const speech = raw.match(/^(沈铎|阿棠|温简|老鬼|马四|鱼牙人|妇人|守墓老人|守陵少年)(([^)]*))?:([\s\S]*)$/);
const speaker = speech ? speech[1] : '旁白';
let spoken = polish(adapt(speech ? (speech[2] ?? '') + speech[3] : raw, code, skin));
if (spoken.startsWith('【') && !spoken.startsWith('【若') && spoken.endsWith('】')) spoken = spoken.slice(1, -1);
out.push({ id: `${id}-d${++counter}`, type: 'dialogue', speaker, text: spoken, sprite: sprites[speaker] ?? 'scene', next: '' });
}
out.forEach((n, i) => { n.next = out[i + 1]?.id ?? next; });
nodes.push(...out);
return out;
}
if (code !== 'CUL') {
const choices = [...body.matchAll(/【选项((CB-[A-Z]+-[AB]-\d+))[^]*?】([\s\S]*?)(?=\n###)/g)];
let previousResults: DialogueNode[] = [];
let previousEnd = 0;
for (const choice of choices) {
const point = choice[1];
const intro = body.slice(previousEnd, choice.index);
const opening = dialogue(intro, point.toLowerCase());
previousResults.forEach((n) => { n.next = opening[0]?.id ?? point.toLowerCase(); });
// 每个选项以空行分隔;只取玩家台词,括号中的测量解释不入游戏。
const optionBlocks = choice[2].split(/\n\s*\n/).filter((s) => /^\*\*[A-D]\./.test(s.trim()));
const resultStart = body.indexOf(`### 各选项后续(${point})`, choice.index!);
const resultEnd = body.indexOf('\n### ', resultStart + 4);
const resultText = body.slice(resultStart, resultEnd < 0 ? body.length : resultEnd);
const results = [...resultText.matchAll(/^- \*\*([A-D])\*\*:([\s\S]*?)(?=^- \*\*[A-D]\*\*:|$(?![\s\S]))/gm)];
const choiceNode: ChoiceNode = { id: point.toLowerCase(), type: 'choice', pointId: point, prompt: '你怎么做?', dimensions: [dimensions[code]], options: [] };
previousResults = [];
for (const block of optionBlocks) {
const letter = block.trim()[2];
const label = adapt(clean(block.replace(/^\*\*[A-D]\./, '').split(/\n(/)[0]), code, skin);
const result = results.find((r) => r[1] === letter);
if (!result) throw new Error(`缺少后续 ${point}-${letter}`);
const after = dialogue(result[2]);
if (point === 'CB-VAL-A-02' && letter === 'B') after[0].text = after[0].text.replace('每项实数,', `每项实数:${ledgerDetails}`);
if (point === 'CB-VAL-A-02' && letter === 'C') after[0].text = after[0].text.replace('按实数记了,', `按实数记了:${ledgerDetails}`);
choiceNode.options.push({ id: `${point}-${letter}`, label, next: after[0].id });
previousResults.push(after.at(-1)!);
}
if (code === 'EMO' && point.endsWith('01')) {
const timeout = dialogue(skin === 'A' ? '你继续听沈铎指挥点人。队员在警戒线内一一应答;风声盖住阿棠的声音,她举手示意自己还在。' : '沈铎按既定预案接管排险,其他队员退出阵列。阿棠在受训人员协助下安全离开。');
choiceNode.options.push({ id: `${point}-TIMEOUT`, label: skin === 'A' ? '听沈铎指挥点人' : '由沈铎接管排险', next: timeout[0].id });
previousResults.push(timeout.at(-1)!);
choiceNode.timer = { seconds: skin === 'A' ? 10 : 8, optionId: `${point}-TIMEOUT` };
}
nodes.push(choiceNode);
previousEnd = resultEnd;
if (choiceNode.options.length < 3) throw new Error(`选项不完整 ${point}`);
}
const lastSection = body.slice(previousEnd).split('### 选项设计说明')[0];
const closing = code === 'VAL' && skin === 'B' ? [] : dialogue(lastSection);
const exit = dialogue(exits[`${code}-${skin}`], '$resume');
if (closing.length) closing.at(-1)!.next = exit[0].id;
previousResults.forEach((n) => { n.next = closing[0]?.id ?? exit[0].id; });
} else {
const point = `CB-CUL-${skin}-01`;
const beforePuzzle = body.slice(0, body.indexOf('### 谜面'));
const opening = dialogue(beforePuzzle.split('【前置选择')[0], skin === 'A' ? 'cb-cul-a-00' : point.toLowerCase());
void opening;
if (skin === 'A') {
const skipped = dialogue(body.slice(body.indexOf('### skip 线正文'), body.indexOf('【场景(坑道内)】')).replace(/###.*\n/, ''), '$resume');
const interior = dialogue(body.slice(body.indexOf('【场景(坑道内)】'), body.indexOf('### 谜面')), point.toLowerCase());
nodes.push({ id: 'cb-cul-a-00', type: 'choice', pointId: 'CB-CUL-A-00', prompt: '下去看看,还是赶路?', dimensions: ['culture'], untimed: true,
options: [{ id: 'CB-CUL-A-00-A', label: '下去看看', next: interior[0].id }, { id: 'CB-CUL-A-00-B', label: '赶路要紧', next: skipped[0].id }] });
}
const puzzleText = body.slice(body.indexOf('### 谜面'), body.indexOf('**谜底**'));
const questionBlocks = [...puzzleText.matchAll(/- \*\*(第[一二]问)\*\*:([^\n]+)\n([\s\S]*?)(?=\n- \*\*第|$(?![\s\S]))/gm)];
const hintsText = body.slice(body.indexOf('### 三档难度提示'), body.indexOf('### 解题后正文'));
const hints = [...hintsText.matchAll(/^\d\. ([\s\S]*?)(?=^\d\. |$(?![\s\S]))/gm)].map((h) => polish(clean(h[1])));
const after = dialogue(body.slice(body.indexOf('### 解题后正文'), body.indexOf('### 选项设计说明')));
const explanation = dialogue(body.slice(body.indexOf('**谜底**'), body.indexOf('### 三档难度提示')).replace('**谜底**:', '温简:'), after[0].id);
const exit = dialogue(skin === 'A' ? '半小时到,沈铎封道拍照。爬出断口时,日头贴着观星台的圆盘往下坠。阿棠眯着眼:「该去台上,看那颗星了。」' : '俑道尽头有侧阶直通台基顶。沈铎说:「记录。上去。」全队拾级登台。', '$resume');
after.at(-1)!.next = exit[0].id;
nodes.push({ id: point.toLowerCase(), type: 'choice', pointId: point, dimensions: ['culture'],
prompt: skin === 'A' ? '主壁铭文:「隹八月壬辰,大火見,王命作冊:城南垣,徙。」旁刻一人在高台举火,台下众人负土而行。残刻干支表起甲子、止癸酉,十枚围成一旬。' : '前排九俑胸口刻字:文、武、成、康、昭、穆、共、懿、孝。单数位微偏左,双数位微偏右,交错成两列。',
puzzle: { questions: questionBlocks.map((q, i) => ({ prompt: clean(q[2]), options: [...q[3].matchAll(/^ - [A-D]\. (.+)$/gm)].map((o) => clean(o[1])), answer: skin === 'A' && i === 1 ? 2 : 1 })), hints, exitLabel: skin === 'B' ? '上台基' : '请温简释读' },
options: [{ id: `${point}-ANSWER`, label: '确认释读', next: explanation[0].id }, { id: `${point}-EXIT`, label: skin === 'B' ? '上台基' : '请温简释读', next: explanation[0].id }] });
}
const pack: BranchPack = { id, dimension: dimensions[code], anchor: { chapter: skin === 'A' ? 'chapter2' : 'chapter3', afterNode: skin === 'A' ? 'c2-branch-anchor' : 'c3-d1b' }, estimatedMinutes: 8, startNodeId: nodes[0].id, nodes };
writeFileSync(new URL(`../content/branches/${id}.json`, import.meta.url), JSON.stringify(pack, null, 2) + '\n');
console.log(`${id}: ${nodes.length} nodes`);
}
if (sections.length !== 10) throw new Error(`预期十条支线,实际 ${sections.length}`);
+279
View File
@@ -0,0 +1,279 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { BRANCHES } from '../src/engine/branches';
import { decideInsertion } from '../src/engine/branching';
import { StoryEngine } from '../src/engine/engine';
import { makeEvent } from '../src/engine/eventStore';
import type { StoryEvent, EventStore } from '../src/engine/eventStore';
import { choiceFlags, deriveFlags, resolveConditionalText } from '../src/engine/flags';
import { MemoryProgressStore } from '../src/engine/progress';
import { buildState, scoreProfile, PROFILE_DIMENSIONS } from '../src/engine/scoring';
import type { Confidences, ProfileDimension } from '../src/engine/scoring';
import type { Chapter, StoryNode, ChoiceBehavior } from '../src/engine/types';
import { MockJevClient } from '../src/services/jev';
import type { JevQuestion } from '../src/services/jev';
const chapters: Chapter[] = [1, 2, 3].map((i) => JSON.parse(readFileSync(new URL(`../content/chapters/chapter${i}.json`, import.meta.url), 'utf8')));
const confidences = (low: Confidences = {}): Confidences => Object.assign(Object.fromEntries(PROFILE_DIMENSIONS.map((d) => [d.id, 0.9])), low);
const inserted = (id: string) => {
const b = BRANCHES.find((b) => b.id === id)!;
return makeEvent({ sessionId: 'test', chapterId: b.anchor.chapter, pointId: id, kind: 'branch_inserted', branchId: id, dimensions: [b.dimension], confidence: 0.3 });
};
const sample = (point: string, dimension: ProfileDimension) => makeEvent({ sessionId: 'test', chapterId: 'chapter1', pointId: point, kind: 'choice', dimensions: [dimension], optionId: 'A', optionLabel: 'A' });
const choose = (low: Confidences, events: StoryEvent[] = [], target = chapters[1], completedChapter?: Chapter) => decideInsertion({ confidences: confidences(low), events, branches: BRANCHES, target, completedChapter });
for (const [label, low, events, target, expected] of [
['单维低置信', { attachment: 0.5 }, [], chapters[1], 'b-att-a'],
['多维权重', { attachment: 0.4, values: 0.4, culture: 0.4 }, [], chapters[1], 'b-att-a'],
['缺口优先', { attachment: 0.59, values: 0.1 }, [], chapters[1], 'b-val-a'],
['稀缺优先', { attachment: 0.4, values: 0.4 }, [sample('one', 'attachment'), sample('two', 'attachment')], chapters[1], 'b-val-a'],
['重复换皮', { attachment: 0.2 }, [inserted('b-att-a')], chapters[2], 'b-att-b'],
['第3章初测不可搬A', { values: 0.2 }, [], chapters[2], 'b-val-b'],
['两皮耗尽', { attachment: 0.2 }, [inserted('b-att-a'), inserted('b-att-b')], chapters[2], undefined],
['占用不叠加', { values: 0.2 }, [inserted('b-att-a')], chapters[1], undefined],
['占用顺延下一章', { values: 0.2 }, [inserted('b-att-a')], chapters[2], 'b-val-b'],
['无定义维度', { risk: 0.1, mate_preference: 0.1 }, [], chapters[1], undefined],
['阈值边界', { attachment: 0.6 }, [], chapters[1], undefined],
['无效置信度', { attachment: NaN, values: -1 }, [], chapters[1], undefined],
['超时长', { attachment: 0.1 }, [], { ...chapters[1], estimatedMinutes: 38 }, undefined],
['时长等于45', { attachment: 0.1 }, [], { ...chapters[1], estimatedMinutes: 37 }, 'b-att-a'],
] as [string, Confidences, StoryEvent[], Chapter, string | undefined][]) {
assert.equal(choose(low, events, target).branch?.id, expected, label);
}
assert.deepEqual(choose({ risk: 0.1, mate_preference: 0.1 }).evidenceInsufficient, ['risk', 'mate_preference']);
assert.deepEqual(choose({ attachment: 0.1 }, [inserted('b-att-a'), inserted('b-att-b')], chapters[2]).evidenceInsufficient, ['attachment']);
assert.equal(choose({ culture: 0.2 }, [], chapters[1], chapters[0]).branch, undefined);
assert.equal(choose({ culture: 0.2 }, [sample('C1-05', 'culture')], chapters[1], chapters[0]).branch?.id, 'b-cul-a');
console.log('✓ 自适应决策矩阵:权重、缺口、稀缺、A/B、占用、证据不足、主线优先、时长/阈值边界');
class MemoryStore implements EventStore {
events: StoryEvent[] = [];
async list(sessionId?: string) { return this.events.filter((e) => !sessionId || e.sessionId === sessionId); }
async append(e: StoryEvent) { this.events.push(e); }
}
class FixedJev extends MockJevClient {
dimension: ProfileDimension;
calls: unknown[] = [];
constructor(dimension: ProfileDimension = 'attachment') { super(); this.dimension = dimension; }
override async ask(state: unknown, questions: Record<string, JevQuestion>) {
const result = await super.ask(state, questions);
if (Object.keys(questions).length === 7) {
this.calls.push(state);
for (const d of PROFILE_DIMENSIONS) if (result[d.id]?.type === 'score') result[d.id].confidence = d.id === this.dimension ? 0.2 : 0.9;
}
return result;
}
}
async function step(e: StoryEngine, picks: Record<string, string> = {}, behavior?: ChoiceBehavior) {
const n = e.currentNode();
if (n.type === 'choice') {
await e.choose(picks[n.pointId] ?? n.options[0].id, behavior ?? (n.puzzle ? { answers: n.puzzle.questions.map((q) => q.answer), hintsUsed: 0 } : { decisionMs: 320 }));
} else if (n.type === 'narration') await e.skipNarration();
else if (n.type === 'identity') await e.confirmIdentity({ name: '测试', age: '', city: '', gender: '', occupation: '', education: '' });
else await e.advance();
}
async function walkTo(e: StoryEngine, id: string) {
for (let i = 0; i < 400 && !e.state.finished; i++) {
if (e.state.nodeId === id) return;
await step(e);
}
assert.fail(`未到达 ${id},当前位置 ${e.state.nodeId}`);
}
// 所有节点均可达,所有出口返回主线,不共享/改写主线 JSON。
const original = JSON.stringify(chapters);
const pointIds = new Set<string>();
for (const branch of BRANCHES) {
const index = new Map(branch.nodes.map((n) => [n.id, n]));
const reached = new Set<string>();
function visit(id: string, path = new Set<string>()) {
if (id === '$resume') return;
assert(!path.has(id), `支线循环 ${id}`);
const n = index.get(id);
assert(n, `无效跳转 ${id}`);
reached.add(id);
const nexts = n.type === 'choice' ? n.options.map((o) => o.next) : 'next' in n ? [n.next] : [];
assert(nexts.length, `没有出口 ${id}`);
for (const next of nexts) visit(next, new Set([...path, id]));
}
visit(branch.startNodeId);
assert.equal(reached.size, branch.nodes.length, branch.id);
for (const node of branch.nodes) {
if (node.type !== 'choice') continue;
assert(!pointIds.has(node.pointId));
pointIds.add(node.pointId);
assert.deepEqual(node.dimensions, [branch.dimension]);
if (node.puzzle) {
assert.equal(node.puzzle.questions.length, 2);
assert(node.puzzle.questions.every((q) => q.options.length === 4));
assert.equal(node.puzzle.hints.length, 3);
}
}
for (const health of ['weak', 'recovering']) {
for (const choiceNode of branch.nodes.filter((n) => n.type === 'choice')) {
for (const option of choiceNode.options) {
const store = new MemoryStore();
const progress = new MemoryProgressStore();
const engine = new StoryEngine(chapters, store, new FixedJev(branch.dimension), undefined, progress, BRANCHES);
await engine.restore({ sessionId: 'test', chapterId: branch.anchor.chapter, nodeId: branch.anchor.afterNode, flags: { wenjian_health: health }, insertions: [branch.id] });
await engine.advance();
assert.equal(engine.state.nodeId, branch.startNodeId);
for (let i = 0; engine.state.activeBranchId && i < 100; i++) {
const node = engine.currentNode();
const texts = node.type === 'choice' ? [node.prompt, ...node.options.map((o) => o.label)] : node.type === 'dialogue' ? [node.text] : [];
for (const text of texts) {
const rendered = engine.renderText(text);
assert(!/【若|工程注|§\d|Act 2|线追加|若 C2-05/.test(rendered), `${node.id} 泄漏: ${rendered}`);
}
const snap = progress.load()!;
const restored = new StoryEngine(chapters, store, new FixedJev(), undefined, progress, BRANCHES);
await restored.restore(snap);
assert.equal(restored.state.nodeId, engine.state.nodeId);
assert.equal(restored.state.activeBranchId, branch.id);
const behavior = node.id === choiceNode.id && node.type === 'choice' ? node.puzzle ? { answers: node.puzzle.questions.map((q) => q.answer), hintsUsed: 2, decisionMs: 400 } : { timedOut: option.id.endsWith('TIMEOUT'), decisionMs: 400 } : undefined;
await step(engine, { [choiceNode.pointId]: option.id }, behavior);
}
assert.equal(engine.state.activeBranchId, undefined);
const anchor = chapters.find((c) => c.id === branch.anchor.chapter)!.nodes.find((n) => n.id === branch.anchor.afterNode)!;
assert.equal(engine.state.nodeId, 'next' in anchor ? anchor.next : '');
const event = store.events.find((e) => e.pointId === choiceNode.pointId);
// CUL-A skip 不经过谜面;默认进入路径则每个目标点都应落事件。
assert(event, `${choiceNode.pointId} 缺事件`);
assert.equal(event.optionId, option.id);
assert.equal(event.branchId, branch.id);
const derived = deriveFlags(store.events);
for (const [flag, value] of Object.entries(derived)) assert.equal(engine.state.flags[flag], value, flag);
if (nodeIsPuzzle(choiceNode)) assert.equal(event.behavior?.solved, !option.id.endsWith('EXIT'));
}
}
}
}
function nodeIsPuzzle(n: StoryNode) { return n.type === 'choice' && !!n.puzzle; }
// 01 的实际支出与受领性质必须在 02 的全部三个入口一致,D 的粥棚只在路程之后可见。
const waterBranch = BRANCHES.find((b) => b.id === 'b-val-a')!;
const waterChoices = waterBranch.nodes.filter((n) => n.type === 'choice');
assert.equal(waterChoices.length, 2);
const waterRecords = ['施水一囊', '施水一杯', '售水两囊,得镯一支(CW-0322,有偿征集)', '水支出零,受赠镯一支(CW-0322,赠与代收)'];
for (let water = 0; water < 4; water++) {
const flags = choiceFlags('CB-VAL-A-01', waterChoices[0].options[water].id);
const opening = waterBranch.nodes.find((n) => n.id === waterBranch.startNodeId)!;
assert(opening.type === 'dialogue' && opening.text.includes('施粥棚开着'), '选前必须提供当日粥棚证据');
const crossroads = waterBranch.nodes.find((n) => n.type === 'dialogue' && n.text.includes('盛热粥的碗'))!;
const elapsed = waterBranch.nodes.find((n) => n.type === 'dialogue' && n.text.includes('一个时辰的路走完了'))!;
const nexts = new Map(waterBranch.nodes.map((n) => [n.id, n]));
let cursor = waterChoices[0].options[water].next;
let sawElapsed = false;
while (cursor !== waterChoices[1].id) {
if (cursor === elapsed.id) sawElapsed = true;
if (cursor === crossroads.id) assert(sawElapsed, '粥棚确认早于一个时辰路程');
const node = nexts.get(cursor)!;
assert(node && 'next' in node, `核账前路径中断 ${cursor}`);
cursor = node.next;
}
assert.equal(resolveConditionalText((crossroads as { text: string }).text, flags)?.includes('热粥'), water === 3);
for (const [ledger, option] of waterChoices[1].options.entries()) {
const node = nexts.get(option.next)!;
assert(node.type === 'dialogue');
const rendered = resolveConditionalText(node.text, { ...flags, wenjian_health: 'recovering' })!;
assert(rendered.includes(waterRecords[water]), `核账 ${water}/${ledger} 不守恒: ${rendered}`);
for (const [other, record] of waterRecords.entries()) if (other !== water) assert(!rendered.includes(record), `核账 ${water}/${ledger} 串账`);
}
}
const conB = BRANCHES.find((b) => b.id === 'b-con-b')!;
assert(conB.nodes[0].type === 'dialogue' && conB.nodes[0].text.includes('安门内'));
assert(conB.nodes.every((n) => n.type !== 'dialogue' || !n.text.includes('准备进安门')));
for (const [letter, value] of [['A', 'a_alert'], ['B', 'b_relay'], ['C', 'c_record'], ['TIMEOUT', 'self_rescue']] as const)
assert.equal(choiceFlags('CB-EMO-B-01', `CB-EMO-B-01-${letter}`).cb_emo_b_plate, value);
for (const [letter, value] of [['A', 'a_record'], ['B', 'b_mark'], ['C', 'c_leave']] as const)
assert.equal(choiceFlags('CB-EMO-B-02', `CB-EMO-B-02-${letter}`).cb_emo_b_vent, value);
assert.equal(choiceFlags('CB-VAL-A-01', 'CB-VAL-A-01-C').flag_thirst_days, 'true');
console.log('✓ 四路水量×三种核账、粥棚时间及可见性、门内入口、压板枚举');
assert.equal(JSON.stringify(chapters), original);
assert.equal(pointIds.size, 17);
console.log('✓ 十条支线全部选项/超时/跳过、温简双状态、逐节点恢复、事件与 flags、出口汇合');
for (const dimension of ['attachment', 'values', 'conflict_style', 'emotional_stability', 'culture'] as const) {
const store = new MemoryStore();
const jev = new FixedJev(dimension);
const e = new StoryEngine(chapters, store, jev, undefined, undefined, BRANCHES);
for (let i = 0; !e.state.finished && i < 400; i++) await step(e);
assert(e.state.finished);
const plans = store.events.filter((e) => e.kind === 'branch_inserted');
assert.equal(plans.length, 2);
assert(plans[0].branchId?.endsWith('-a') && plans[1].branchId?.endsWith('-b'));
assert.equal(jev.calls.length, 3);
assert(store.events.some((e) => e.kind === 'evidence_insufficient' && e.dimensions.includes(dimension)));
assert(String(jev.calls[2]).includes('CB-'));
const count = store.events.length;
e.replay('chapter1');
assert.equal(e.insertions.length, 0);
assert.equal(e.state.activeBranchId, undefined);
assert.equal(store.events.length, count);
await walkTo(e, 'c1-end');
await e.advance();
assert.equal(e.insertions[0].id, plans[0].branchId);
assert(!String(jev.calls.at(-1)).includes(plans[0].sessionId));
}
console.log('✓ 五维各跑通三章、A→B换皮、末章证据不足、本局隔离、replay 清计划');
{
const store = new MemoryStore();
const progress = new MemoryProgressStore();
const jev = new FixedJev();
const e = new StoryEngine(chapters, store, jev, undefined, progress, BRANCHES);
await walkTo(e, 'c1-end');
const oldSnapshot = progress.load()!;
await Promise.all([e.advance(), e.advance(), e.advance()]);
assert.equal(jev.calls.length, 1);
assert.equal(e.state.chapterId, 'chapter2');
assert.equal(store.events.filter((ev) => ev.kind === 'branch_inserted').length, 1);
const restored = new StoryEngine(chapters, store, jev, undefined, progress, BRANCHES);
await restored.restore(oldSnapshot);
await restored.advance();
assert.equal(jev.calls.length, 1);
assert.equal(restored.insertions.length, 1);
assert.equal(store.events.filter((ev) => ev.kind === 'branch_inserted').length, 1);
}
for (const jev of [
{ ask: async () => { throw new Error('test offline'); } },
{ ask: async () => ({}) },
{ adaptiveScoring: false, ask: async () => { assert.fail('缺 key 不应打分'); } },
]) {
const store = new MemoryStore();
const e = new StoryEngine(chapters, store, jev, undefined, undefined, BRANCHES);
await e.restore({ sessionId: 'test', chapterId: 'chapter1', nodeId: 'c1-end', flags: {} });
await e.advance();
assert.equal(e.state.chapterId, 'chapter2');
assert.equal(e.insertions.length, 0);
assert.equal(store.events.length, 0);
}
console.log('✓ 重复点击与刷新幂等、Jev故障/无效响应/无key降级');
for (const branchId of ['b-cul-a', 'b-cul-b']) {
const branch = BRANCHES.find((b) => b.id === branchId)!;
const puzzle = branch.nodes.find((n) => n.type === 'choice' && n.puzzle)!;
assert(puzzle.type === 'choice' && puzzle.puzzle);
for (const [correct, expected] of [[2, 'true'], [1, 'partial'], [0, 'false']] as const) {
const store = new MemoryStore();
const e = new StoryEngine(chapters, store, new MockJevClient(), undefined, undefined, BRANCHES);
await e.restore({ sessionId: 'test', chapterId: branch.anchor.chapter, nodeId: puzzle.id, activeBranchId: branch.id, flags: {}, insertions: [branch.id] });
const answers = puzzle.puzzle.questions.map((q, i) => i < correct ? q.answer : (q.answer + 1) % q.options.length);
await e.choose(puzzle.options[0].id, { answers, hintsUsed: 1 });
assert.equal(e.state.flags[`c${branchId.replaceAll('-', '_')}_solved`], expected);
assert.deepEqual(store.events.at(-1)?.behavior?.answerCorrect, [correct > 0, correct > 1]);
}
const store = new MemoryStore();
const e = new StoryEngine(chapters, store, new MockJevClient(), undefined, undefined, BRANCHES);
await e.restore({ sessionId: 'test', chapterId: branch.anchor.chapter, nodeId: puzzle.id, activeBranchId: branch.id, flags: {}, insertions: [branch.id] });
await e.choose(puzzle.options[1].id);
assert.equal(e.state.flags[`c${branchId.replaceAll('-', '_')}_solved`], 'not_attempted');
assert.equal(e.state.flags[`c${branchId.replaceAll('-', '_')}_left`], 'true');
assert.equal(store.events.at(-1)?.behavior?.answers, undefined);
}
console.log('✓ 文化双问题分题记录 partial,主动离场独立于提示用尽/答错');
assert(!buildState([makeEvent({ sessionId: 'private', chapterId: 'chapter1', kind: 'profile', pointId: 'C1-01', dimensions: [], profile: { name: 'SECRET-NAME', city: '', age: '', gender: '', occupation: '', education: '' } })]).includes('SECRET-NAME'));
await assert.rejects(() => scoreProfile([], { ask: async () => ({}) }));
assert.equal(resolveConditionalText('【若 cb_val_a_water = d_redirect:粥棚】', { cb_val_a_water: 'a_give' }), '');
console.log('✓ 共享评分排除身份数据、无效响应不冒充低置信、条件文案不串线');
+2 -2
View File
@@ -51,7 +51,7 @@ const chapters = [1, 2, 3].map(
// --- 1. C1-01 身份抽取(走引擎真实代码路径) --- // --- 1. C1-01 身份抽取(走引擎真实代码路径) ---
const store = new MemoryStore(); const store = new MemoryStore();
const engine = new StoryEngine(chapters, store, jev); const engine = new StoryEngine(chapters, store, jev);
while (engine.currentNode().type !== 'identity') engine.advance(); while (engine.currentNode().type !== 'identity') await engine.advance();
const intro = '我叫林晚,今年29岁,在杭州一家互联网公司做产品经理,硕士毕业五年了。'; const intro = '我叫林晚,今年29岁,在杭州一家互联网公司做产品经理,硕士毕业五年了。';
const profile = await engine.extractIdentity(intro); const profile = await engine.extractIdentity(intro);
console.log('\n[1] C1-01 身份抽取'); console.log('\n[1] C1-01 身份抽取');
@@ -60,7 +60,7 @@ console.log(' 抽取:', JSON.stringify(profile));
// --- 2. C1-02 情绪签名 → 场景变体(choice 格式实测) --- // --- 2. C1-02 情绪签名 → 场景变体(choice 格式实测) ---
await engine.confirmIdentity(profile); await engine.confirmIdentity(profile);
while (engine.currentNode().type !== 'narration') engine.advance(); while (engine.currentNode().type !== 'narration') await engine.advance();
const farewell = '外婆走的那天我在外地考试,没赶上最后一面。后来我每年清明都回去,院子里的桂花树还在。'; const farewell = '外婆走的那天我在外地考试,没赶上最后一面。后来我每年清明都回去,院子里的桂花树还在。';
let result = await engine.submitNarration(farewell); let result = await engine.submitNarration(farewell);
console.log('\n[2] C1-02 情绪签名 → 场景变体'); console.log('\n[2] C1-02 情绪签名 → 场景变体');
+58 -17
View File
@@ -14,7 +14,8 @@ import { readFileSync } from 'node:fs';
import { CONSENT_VERSION, StoryEngine } from '../src/engine/engine'; import { CONSENT_VERSION, StoryEngine } from '../src/engine/engine';
import { LocalStorageEventStore } from '../src/engine/eventStore'; import { LocalStorageEventStore } from '../src/engine/eventStore';
import type { EventStore, StoryEvent } from '../src/engine/eventStore'; import type { EventStore, StoryEvent } from '../src/engine/eventStore';
import { choiceFlags, narrationFlags } from '../src/engine/flags'; import { choiceFlags, deriveFlags, narrationFlags } from '../src/engine/flags';
import { buildState } from '../src/engine/scoring';
import { MemoryProgressStore } from '../src/engine/progress'; import { MemoryProgressStore } from '../src/engine/progress';
import { SqliteEventStore } from '../src/engine/sqliteEventStore'; import { SqliteEventStore } from '../src/engine/sqliteEventStore';
import type { Chapter, ChoiceNode, IdentityProfile, NarrationNode } from '../src/engine/types'; import type { Chapter, ChoiceNode, IdentityProfile, NarrationNode } from '../src/engine/types';
@@ -37,8 +38,8 @@ const EXPECTED: Record<string, string[]> = {
'C2-05': ['attachment', 'reciprocity'], 'C2-05': ['attachment', 'reciprocity'],
'C2-06': ['conflict_style', 'values'], 'C2-06': ['conflict_style', 'values'],
'C3-01': ['culture'], 'C3-01': ['culture'],
'C3-02': ['emotional_stability', 'attachment'], 'C3-02': [],
'C3-03': ['values', 'emotional_stability'], 'C3-03': ['values'],
'C3-04': ['conflict_style', 'values'], 'C3-04': ['conflict_style', 'values'],
'C3-05': ['mate_preference'], 'C3-05': ['mate_preference'],
'C3-06': ['attachment', 'mate_preference'], 'C3-06': ['attachment', 'mate_preference'],
@@ -115,7 +116,7 @@ for (const [id, dims] of Object.entries(EXPECTED)) {
// --- 3. V 类讲述点变体结构 --- // --- 3. V 类讲述点变体结构 ---
const narrationNodes = chapters.flatMap((c) => c.nodes.filter((n) => n.type === 'narration')) as NarrationNode[]; const narrationNodes = chapters.flatMap((c) => c.nodes.filter((n) => n.type === 'narration')) as NarrationNode[];
check(narrationNodes.map((n) => n.pointId).sort().join(',') === 'C1-02,C2-02,C2-05,C3-02', 'V 类讲述点为 C1-02/C2-02/C2-05/C3-02'); check(narrationNodes.map((n) => n.pointId).sort().join(',') === 'C1-02,C2-02,C2-05,C3-02', '三个语音讲述点 + C3-02 私密路径节点均存在');
for (const n of narrationNodes) { for (const n of narrationNodes) {
check(n.variants.length >= 2, `${n.pointId}: 有 ${n.variants.length} 个预写场景变体`); check(n.variants.length >= 2, `${n.pointId}: 有 ${n.variants.length} 个预写场景变体`);
check( check(
@@ -126,11 +127,16 @@ for (const n of narrationNodes) {
check(narrationNodes.find((n) => n.pointId === 'C3-02')?.variantFrom === 'C1-02', 'C3-02 变体由 C1-02 驱动(variantFrom)'); check(narrationNodes.find((n) => n.pointId === 'C3-02')?.variantFrom === 'C1-02', 'C3-02 变体由 C1-02 驱动(variantFrom)');
for (const n of narrationNodes) { for (const n of narrationNodes) {
check( check(
FOLLOW_UP_DIMENSIONS.every((d) => typeof n.followUps?.[d] === 'string' && n.followUps[d]!.length > 0), n.pointId === 'C3-02' ? n.followUp.maxQuestions === 0 && Object.keys(n.followUps).length === 0 : FOLLOW_UP_DIMENSIONS.every((d) => typeof n.followUps?.[d] === 'string' && n.followUps[d]!.length > 0),
`${n.pointId}: 六维 followUps 追问模板齐全`, `${n.pointId}: 六维 followUps 追问模板齐全`,
); );
} }
/** 自动遍历解谜点时提交真实两题答案,其他选择点无需行为参数。 */
function puzzleBehavior(node: ChoiceNode) {
return node.puzzle ? { answers: node.puzzle.questions.map((q) => q.answer), hintsUsed: 0 } : undefined;
}
// --- 4. Mock 模式端到端跑通三章 --- // --- 4. Mock 模式端到端跑通三章 ---
const store = new MemoryStore(); const store = new MemoryStore();
const jev = new MockJevClient(); const jev = new MockJevClient();
@@ -139,19 +145,20 @@ let guard = 0;
while (!engine.state.finished && guard++ < 200) { while (!engine.state.finished && guard++ < 200) {
const node = engine.currentNode(); const node = engine.currentNode();
if (node.type === 'choice') { if (node.type === 'choice') {
await engine.choose((node as ChoiceNode).options[0].id); await engine.choose(node.options[0].id, puzzleBehavior(node));
} else if (node.type === 'identity') { } else if (node.type === 'identity') {
const profile = await engine.extractIdentity('我叫阿黎,29岁,在杭州做互联网产品经理,硕士学历'); const profile = await engine.extractIdentity('我叫阿黎,29岁,在杭州做互联网产品经理,硕士学历');
await engine.confirmIdentity(profile); await engine.confirmIdentity(profile);
} else if (node.type === 'narration') { } else if (node.type === 'narration') {
// C2-05 走"沉默"路径验证合法选项,其余讲述;Mock 检测到缺失维度则回答追问 // C2-05 走"沉默"路径验证合法选项,其余讲述;Mock 检测到缺失维度则回答追问
if ((node as NarrationNode).pointId === 'C2-05') await engine.skipNarration(); if ((node as NarrationNode).pointId === 'C2-05') await engine.skipNarration();
else if ((node as NarrationNode).pointId === 'C3-02') await engine.resolvePrivateIllusion('farewell_done');
else { else {
await engine.submitNarration('(验证用占位讲述)'); await engine.submitNarration('(验证用占位讲述)');
if (engine.state.pendingFollowUp) await engine.answerFollowUp('(验证用追问回答)'); if (engine.state.pendingFollowUp) await engine.answerFollowUp('(验证用追问回答)');
} }
} else { } else {
engine.advance(); await engine.advance();
} }
} }
check(engine.state.finished, '三章流程跑通(Mock 模式)'); check(engine.state.finished, '三章流程跑通(Mock 模式)');
@@ -167,6 +174,23 @@ const c305 = store.events.find((e) => e.pointId === 'C3-02' && e.kind === 'narra
const c102 = store.events.find((e) => e.pointId === 'C1-02' && e.kind === 'narration'); const c102 = store.events.find((e) => e.pointId === 'C1-02' && e.kind === 'narration');
check(!!c305?.variantId && c305.variantId === c102?.variantId, 'C3-02 幻境变体复用 C1-02 情绪解析结果'); check(!!c305?.variantId && c305.variantId === c102?.variantId, 'C3-02 幻境变体复用 C1-02 情绪解析结果');
// C3-01 分题作答:事件、增量 flags 与事件重放结果必须一致。
{
const puzzle = chapters[2].nodes.find((n) => n.id === 'c3-01') as ChoiceNode;
check(!!puzzle.puzzle && puzzle.puzzle.questions.length === 2 && puzzle.puzzle.hints.length === 3, 'C3-01:两道独立选择题与三档提示');
for (const [correctCount, expected] of [[1, 'partial'], [2, 'true'], [0, 'false']] as const) {
const s = new MemoryStore();
const e = new StoryEngine(chapters, s, new MockJevClient());
await e.restore({ sessionId: `c3-01-${correctCount}`, chapterId: 'chapter3', nodeId: puzzle.id, flags: {} });
const answers = puzzle.puzzle!.questions.map((q, i) => i < correctCount ? q.answer : (q.answer + 1) % q.options.length);
await e.choose(puzzle.options[0].id, { answers, hintsUsed: 2 });
const event = s.events.at(-1);
check(event?.behavior?.answers?.join(',') === answers.join(',') && event.behavior.answerCorrect?.join(',') === [correctCount > 0, correctCount > 1].join(',') && event.behavior.hintsUsed === 2, `C3-01 ${expected}:分题答案、正误与提示数落事件`);
check(e.state.flags.c3_01_solved === expected && e.state.flags.c3_01_hints_used === '2' && deriveFlags(s.events).c3_01_solved === expected && deriveFlags(s.events).c3_01_hints_used === '2', `C3-01 ${expected}:即时与重放 flags 一致`);
check(e.state.nodeId === 'c3-01-ok', `C3-01 ${expected}:提交后进入原有温简复原节点`);
}
}
// --- 5. 画像报告:7 维 score 问题 --- // --- 5. 画像报告:7 维 score 问题 ---
const criteria = Array.from({ length: 10 }, (_, i) => `${i + 1} 级`); const criteria = Array.from({ length: 10 }, (_, i) => `${i + 1} 级`);
const dims = ['attachment', 'conflict_style', 'values', 'emotional_stability', 'risk', 'culture', 'mate_preference']; const dims = ['attachment', 'conflict_style', 'values', 'emotional_stability', 'risk', 'culture', 'mate_preference'];
@@ -189,10 +213,11 @@ while (engine.currentNode().type !== 'choice') {
if (n.type === 'identity') { if (n.type === 'identity') {
await engine.confirmIdentity(await engine.extractIdentity('占位自我介绍')); await engine.confirmIdentity(await engine.extractIdentity('占位自我介绍'));
} else if (n.type === 'narration') { } else if (n.type === 'narration') {
if (n.pointId === 'C3-02') { await engine.resolvePrivateIllusion('farewell_done'); continue; }
await engine.submitNarration('(验证用占位讲述)'); await engine.submitNarration('(验证用占位讲述)');
if (engine.state.pendingFollowUp) await engine.answerFollowUp('(验证用追问回答)'); if (engine.state.pendingFollowUp) await engine.answerFollowUp('(验证用追问回答)');
} else { } else {
engine.advance(); await engine.advance();
} }
} }
const firstChoice = engine.currentNode() as ChoiceNode; const firstChoice = engine.currentNode() as ChoiceNode;
@@ -226,12 +251,12 @@ class FixedNoulJev extends MockJevClient {
/** 推进到指定节点(途中选择点取第一项、身份点直接确认、讲述点沉默略过) */ /** 推进到指定节点(途中选择点取第一项、身份点直接确认、讲述点沉默略过) */
async function walkTo(eng: StoryEngine, nodeId: string) { async function walkTo(eng: StoryEngine, nodeId: string) {
let g = 0; let g = 0;
while (eng.currentNode().id !== nodeId && g++ < 60) { while (eng.currentNode().id !== nodeId && g++ < 250) {
const n = eng.currentNode(); const n = eng.currentNode();
if (n.type === 'choice') await eng.choose((n as ChoiceNode).options[0].id); if (n.type === 'choice') await eng.choose(n.options[0].id, puzzleBehavior(n));
else if (n.type === 'identity') await eng.confirmIdentity(await eng.extractIdentity('占位自我介绍')); else if (n.type === 'identity') await eng.confirmIdentity(await eng.extractIdentity('占位自我介绍'));
else if (n.type === 'narration') await eng.skipNarration(); else if (n.type === 'narration') await eng.skipNarration();
else eng.advance(); else await eng.advance();
} }
} }
@@ -322,7 +347,7 @@ check(choiceFlags('C2-01', 'b_exposure').wenjian_health === 'weak', 'flag 推导
check(choiceFlags('C2-01', 'a_weakest').wenjian_health === 'recovering', 'flag 推导:C2-01 a_weakest → wenjian_health=recovering'); check(choiceFlags('C2-01', 'a_weakest').wenjian_health === 'recovering', 'flag 推导:C2-01 a_weakest → wenjian_health=recovering');
check(choiceFlags('C2-01', 'd_process').wenjian_health === 'weak', 'flag 推导(S4 修订):C2-01 d_process → wenjian_health=weak(议事真空期老鬼先行分药)'); check(choiceFlags('C2-01', 'd_process').wenjian_health === 'weak', 'flag 推导(S4 修订):C2-01 d_process → wenjian_health=weak(议事真空期老鬼先行分药)');
check(!('flag_prophecy_heard' in narrationFlags('C2-02', 'intel_full', '(讲述)')), 'flag 推导(S3 修订):flag_prophecy_heard 已废除,C2-02 四档等价'); check(!('flag_prophecy_heard' in narrationFlags('C2-02', 'intel_full', '(讲述)')), 'flag 推导(S3 修订):flag_prophecy_heard 已废除,C2-02 四档等价');
check(narrationFlags('C3-02', undefined, null).c3_02_mode === 'exit_room', 'flag 推导:C3-02 沉默退出 → c3_02_mode=exit_room'); check(narrationFlags('C3-02', undefined, null).c3_02_mode === undefined, 'C3-02 不从文本推导模式');
{ {
const f = choiceFlags('C3-06', 'c_confide_wenjian'); const f = choiceFlags('C3-06', 'c_confide_wenjian');
check( check(
@@ -330,7 +355,7 @@ check(narrationFlags('C3-02', undefined, null).c3_02_mode === 'exit_room', 'flag
'flag 推导:C3-06 队友子分支 → c_confide / confidant=wenjian / flag_arch_doubt', 'flag 推导:C3-06 队友子分支 → c_confide / confidant=wenjian / flag_arch_doubt',
); );
} }
check(narrationFlags('C2-05', 'wenjian_close_early', '(讲述)').flag_wenjian_secret === 'true', 'flag 推导:C2-05 深暴露 → flag_wenjian_secret'); check(narrationFlags('C2-05', 'wenjian_close_late', '(讲述)').flag_wenjian_secret === 'true', 'flag 推导:C2-05 浅暴露 → flag_wenjian_secret');
/** 走查三章并收集途经节点与渲染文本;picker 可指定选择点的选项 */ /** 走查三章并收集途经节点与渲染文本;picker 可指定选择点的选项 */
async function walkAll(eng: StoryEngine, picker: Record<string, string> = {}) { async function walkAll(eng: StoryEngine, picker: Record<string, string> = {}) {
@@ -345,17 +370,17 @@ async function walkAll(eng: StoryEngine, picker: Record<string, string> = {}) {
else if (node.type === 'choice') rendered.push(eng.renderText(node.prompt), ...node.options.map((o) => eng.renderText(o.label))); else if (node.type === 'choice') rendered.push(eng.renderText(node.prompt), ...node.options.map((o) => eng.renderText(o.label)));
else if (node.type === 'narration' || node.type === 'identity') rendered.push(eng.renderText(node.prompt)); else if (node.type === 'narration' || node.type === 'identity') rendered.push(eng.renderText(node.prompt));
if (node.type === 'choice') { if (node.type === 'choice') {
await eng.choose(picker[node.pointId] ?? node.options[0].id); await eng.choose(picker[node.pointId] ?? node.options[0].id, puzzleBehavior(node));
} else if (node.type === 'identity') { } else if (node.type === 'identity') {
await eng.confirmIdentity(await eng.extractIdentity('占位自我介绍')); await eng.confirmIdentity(await eng.extractIdentity('占位自我介绍'));
} else if (node.type === 'narration') { } else if (node.type === 'narration') {
if (node.pointId === 'C3-02' && picker.C3_02_SILENT) await eng.skipNarration(); if (node.pointId === 'C3-02') await eng.resolvePrivateIllusion(picker.C3_02_SILENT ? 'exit_room' : 'farewell_done');
else { else {
await eng.submitNarration('(验证用占位讲述)'); await eng.submitNarration('(验证用占位讲述)');
if (eng.state.pendingFollowUp) await eng.answerFollowUp('(验证用追问回答)'); if (eng.state.pendingFollowUp) await eng.answerFollowUp('(验证用追问回答)');
} }
} else { } else {
eng.advance(); await eng.advance();
} }
} }
return { seen, rendered, finished: eng.state.finished }; return { seen, rendered, finished: eng.state.finished };
@@ -511,8 +536,24 @@ class FailingJev extends MockJevClient {
check(eng3.state.playerName === '林晚', '断点恢复后从 profile 事件取回称呼'); check(eng3.state.playerName === '林晚', '断点恢复后从 profile 事件取回称呼');
} }
// C3-02 的三种路径只落叙事模式,不采集或评分室内讲述。
for (const mode of ['farewell_done', 'farewell_partial', 'exit_room'] as const) {
const s = new MemoryStore();
const e = new StoryEngine(chapters, s, new MockJevClient());
await walkTo(e, 'c3-02');
const r = await e.resolvePrivateIllusion(mode);
const recorded = s.events.find((ev) => ev.pointId === 'C3-02');
check(recorded?.kind === 'narration' && recorded.narrationMode === mode && recorded.transcript === null && recorded.dimensions.length === 0, `问心之室 ${mode}:不留文本或维度标签`);
check(e.state.flags.c3_02_mode === mode && !buildState(s.events).includes('C3-02'), `问心之室 ${mode}:叙事变量生效且画像隔离`);
check(r.event.id === recorded?.id && e.state.nodeId === 'c3-cutscene', `问心之室 ${mode}:石门照常开启`);
const restored = deriveFlags(s.events);
check(restored.c3_02_mode === mode && restored.c3_02_fantasy_density === e.state.flags.c3_02_fantasy_density, `问心之室 ${mode}:恢复后叙事状态一致`);
}
if (failures > 0) { if (failures > 0) {
// 主线失败先退出,避免掩盖原有回归。
console.error(`\n${failures} 项校验失败`); console.error(`\n${failures} 项校验失败`);
process.exit(1); process.exit(1);
} }
console.log('\n全部校验通过'); console.log('\n全部校验通过');
await import('./verify-adaptive');
+32
View File
@@ -91,6 +91,7 @@
} }
.choice-options { .choice-options {
min-width: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
@@ -98,6 +99,7 @@
} }
.choice-btn { .choice-btn {
overflow-wrap: anywhere;
text-align: left; text-align: left;
padding: 12px 16px; padding: 12px 16px;
border-radius: 8px; border-radius: 8px;
@@ -115,6 +117,36 @@
} }
/* ---- 讲述点 ---- */ /* ---- 讲述点 ---- */
.choice-box fieldset {
min-width: 0;
border: 0;
padding: 12px 0;
margin: 0;
}
.puzzle-option {
display: flex;
align-items: baseline;
gap: 8px;
padding: 8px 0;
line-height: 1.6;
}
.choice-box progress {
width: 100%;
height: 10px;
accent-color: #d4a96a;
}
button:disabled { opacity: 0.6; cursor: default; }
@media (max-width: 600px) {
.scene { padding: 64px 12px 16px; }
.dimension-row { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
.dimension-value { grid-column: 1 / -1; }
.dialogue-box > div:last-child { min-width: 0; overflow-wrap: anywhere; }
}
.mechanism-tag { .mechanism-tag {
display: inline-block; display: inline-block;
font-size: 12px; font-size: 12px;
+2 -1
View File
@@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import './App.css'; import './App.css';
import { StoryEngine } from './engine/engine'; import { StoryEngine } from './engine/engine';
import { BRANCHES } from './engine/branches';
import { LocalStorageEventStore } from './engine/eventStore'; import { LocalStorageEventStore } from './engine/eventStore';
import { LocalStorageProgressStore } from './engine/progress'; import { LocalStorageProgressStore } from './engine/progress';
import { SqliteEventStore, isTauriRuntime } from './engine/sqliteEventStore'; import { SqliteEventStore, isTauriRuntime } from './engine/sqliteEventStore';
@@ -25,7 +26,7 @@ export default function App() {
const store = isTauriRuntime() ? new SqliteEventStore() : new LocalStorageEventStore(); const store = isTauriRuntime() ? new SqliteEventStore() : new LocalStorageEventStore();
const jev = getJevClient(); const jev = getJevClient();
const progress = new LocalStorageProgressStore(); const progress = new LocalStorageProgressStore();
return { engine: new StoryEngine(chapters, store, jev, undefined, progress), store, jev, progress }; return { engine: new StoryEngine(chapters, store, jev, undefined, progress, BRANCHES), store, jev, progress };
}, []); }, []);
useEffect(() => { useEffect(() => {
+13
View File
@@ -0,0 +1,13 @@
import type { BranchPack } from './types';
import attA from '../../content/branches/b-att-a.json';
import attB from '../../content/branches/b-att-b.json';
import conA from '../../content/branches/b-con-a.json';
import conB from '../../content/branches/b-con-b.json';
import emoA from '../../content/branches/b-emo-a.json';
import emoB from '../../content/branches/b-emo-b.json';
import valA from '../../content/branches/b-val-a.json';
import valB from '../../content/branches/b-val-b.json';
import culA from '../../content/branches/b-cul-a.json';
import culB from '../../content/branches/b-cul-b.json';
export const BRANCHES = [attA, attB, conA, conB, emoA, emoB, valA, valB, culA, culB] as BranchPack[];
+41
View File
@@ -0,0 +1,41 @@
import type { BranchPack, Chapter } from './types';
import type { StoryEvent } from './eventStore';
import type { Confidences, ProfileDimension } from './scoring';
export const CONFIDENCE_THRESHOLD = 0.6;
const WEIGHTS = { attachment: 5, values: 4, conflict_style: 3, emotional_stability: 2, culture: 1 };
export interface BranchDecision {
branch?: BranchPack;
evidenceInsufficient: ProfileDimension[];
}
/** 皮肤绑定章节,不跨章搬运;稀缺度按不同选择点计数,追问不重复计票。 */
export function decideInsertion({ confidences, events, branches, target, completedChapter }: {
confidences: Confidences;
completedChapter?: Chapter;
events: StoryEvent[];
branches: BranchPack[];
target?: Chapter;
}): BranchDecision {
const used = new Set(events.filter((e) => e.kind === 'branch_inserted').map((e) => e.branchId));
const low = (dimension: ProfileDimension) => {
const c = confidences[dimension];
return c !== undefined && Number.isFinite(c) && c >= 0 && c < CONFIDENCE_THRESHOLD;
};
const evidenceInsufficient = (Object.keys(confidences) as ProfileDimension[]).filter((d) =>
low(d) && (!branches.some((b) => b.dimension === d) || branches.filter((b) => b.dimension === d).every((b) => used.has(b.id))),
);
if (!target || events.some((e) => e.kind === 'branch_inserted' && e.chapterId === target.id)) return { evidenceInsufficient };
const sampled = events.filter((e) => e.kind === 'choice' || e.kind === 'narration');
const unplayedPuzzle = completedChapter?.nodes.some((n) => n.type === 'choice' && n.dimensions.includes('culture') &&
!sampled.some((e) => e.chapterId === completedChapter.id && e.pointId === n.pointId));
const candidates = branches.filter((b) => b.anchor.chapter === target.id && low(b.dimension) && !used.has(b.id) &&
target.nodes.some((n) => n.id === b.anchor.afterNode && 'next' in n) &&
(b.dimension !== 'culture' || !unplayedPuzzle) &&
(target.estimatedMinutes ?? 35) + b.estimatedMinutes <= 45);
const priority = (b: BranchPack) => WEIGHTS[b.dimension] * (CONFIDENCE_THRESHOLD - confidences[b.dimension]!) /
(1 + new Set(sampled.filter((e) => e.dimensions.includes(b.dimension)).map((e) => e.pointId)).size);
candidates.sort((a, b) => priority(b) - priority(a) || a.id.localeCompare(b.id));
return { branch: candidates[0], evidenceInsufficient };
}
+163 -20
View File
@@ -1,4 +1,6 @@
import type { Chapter, FollowUpDimension, IdentityProfile, NarrationNode, SceneVariant, StoryNode } from './types'; import type { BranchPack, Chapter, ChoiceBehavior, FollowUpDimension, IdentityProfile, NarrationNode, SceneVariant, StoryNode } from './types';
import { decideInsertion } from './branching';
import { extractConfidences, scoreProfile } from './scoring';
import { FOLLOW_UP_DIMENSIONS, FOLLOW_UP_PRIORITY } from './types'; import { FOLLOW_UP_DIMENSIONS, FOLLOW_UP_PRIORITY } from './types';
import type { EventStore, StoryEvent } from './eventStore'; import type { EventStore, StoryEvent } from './eventStore';
import { makeEvent } from './eventStore'; import { makeEvent } from './eventStore';
@@ -24,6 +26,7 @@ export interface EngineState {
playerName: string; playerName: string;
/** 全部三章完成后为 true → 进入画像报告 */ /** 全部三章完成后为 true → 进入画像报告 */
finished: boolean; finished: boolean;
activeBranchId?: string;
} }
export interface NarrationResult { export interface NarrationResult {
@@ -82,9 +85,13 @@ export class StoryEngine {
private jev: JevClient; private jev: JevClient;
private progress?: ProgressStore; private progress?: ProgressStore;
private nodeIndex: Map<string, StoryNode> = new Map(); private nodeIndex: Map<string, StoryNode> = new Map();
readonly insertions: BranchPack[] = [];
private branches: BranchPack[];
private chapterTransition: Promise<void> | null = null;
constructor(chapters: Chapter[], store: EventStore, jev: JevClient, startChapterId?: string, progress?: ProgressStore) { constructor(chapters: Chapter[], store: EventStore, jev: JevClient, startChapterId?: string, progress?: ProgressStore, branches: BranchPack[] = []) {
if (chapters.length === 0) throw new Error('StoryEngine: no chapters'); if (chapters.length === 0) throw new Error('StoryEngine: no chapters');
this.branches = branches;
this.chapters = chapters; this.chapters = chapters;
this.store = store; this.store = store;
this.jev = jev; this.jev = jev;
@@ -119,18 +126,15 @@ export class StoryEngine {
} }
/** dialogue / cutscene / chapterEnd 前进一步 */ /** dialogue / cutscene / chapterEnd 前进一步 */
advance(): void { async advance(): Promise<void> {
if (this.chapterTransition) return this.chapterTransition;
const node = this.currentNode(); const node = this.currentNode();
if (node.type === 'dialogue' || node.type === 'cutscene') { if (node.type === 'dialogue' || node.type === 'cutscene') {
this.state.nodeId = node.next; this.moveTo(node.next);
} else if (node.type === 'chapterEnd') { } else if (node.type === 'chapterEnd') {
if (node.nextChapter) { this.chapterTransition = this.finishChapter(node.nextChapter);
this.state.chapterId = node.nextChapter; try { await this.chapterTransition; } finally { this.chapterTransition = null; }
this.state.nodeId = this.currentChapter().startNodeId; return;
this.state.activeVariant = null;
} else {
this.state.finished = true;
}
} else { } else {
throw new Error(`advance() 不适用于 ${node.type} 节点`); throw new Error(`advance() 不适用于 ${node.type} 节点`);
} }
@@ -138,13 +142,99 @@ export class StoryEngine {
this.persist(); this.persist();
} }
/** 进度持久化:每次状态迁移后落盘(真机刷新/热更新后从断点恢复) */ /** 章末只用本 session 的累计证据决策,故障不阻断主线。 */
private async finishChapter(nextChapter?: string): Promise<void> {
const sessionId = this.state.sessionId;
const chapterId = this.state.chapterId;
if (this.branches.length && this.jev.adaptiveScoring !== false) {
try {
const events = await this.store.list(sessionId);
const target = this.chapters.find((c) => c.id === nextChapter);
// 事件先于快照落盘;中途刷新时复用原计划。
const existing = events.find((e) => e.kind === 'branch_inserted' && e.chapterId === nextChapter);
if (existing) {
const pack = this.branches.find((b) => b.id === existing.branchId);
if (pack) this.loadInsertion(pack);
} else {
const confidences = extractConfidences(await scoreProfile(events, this.jev));
if (sessionId !== this.state.sessionId) return;
const decision = decideInsertion({ confidences, events, branches: this.branches, target, completedChapter: this.currentChapter() });
for (const dimension of decision.evidenceInsufficient) {
if (events.some((e) => e.kind === 'evidence_insufficient' && e.dimensions.includes(dimension))) continue;
await this.store.append(makeEvent({ sessionId, chapterId, kind: 'evidence_insufficient',
pointId: `EVIDENCE-${dimension}`, dimensions: [dimension], confidence: confidences[dimension] }));
}
const branch = decision.branch;
if (branch) {
this.validateInsertion(branch);
await this.store.append(makeEvent({ sessionId, chapterId: branch.anchor.chapter, kind: 'branch_inserted',
pointId: branch.id, branchId: branch.id, dimensions: [branch.dimension], confidence: confidences[branch.dimension] }));
if (sessionId !== this.state.sessionId) return;
this.loadInsertion(branch);
}
}
} catch (e) {
console.warn('[engine] 章末评分/支线计划失败,本章不插入,继续主线', e);
}
}
if (sessionId !== this.state.sessionId) return;
if (nextChapter) {
this.state.chapterId = nextChapter;
this.state.nodeId = this.currentChapter().startNodeId;
this.state.activeVariant = null;
} else this.state.finished = true;
this.state.activeBranchId = undefined;
this.skipHiddenNodes();
this.persist();
}
private validateInsertion(branch: BranchPack): void {
const anchor = this.nodeIndex.get(`${branch.anchor.chapter}/${branch.anchor.afterNode}`);
const ids = new Set(branch.nodes.map((n) => n.id));
if (!anchor || !('next' in anchor) || !ids.has(branch.startNodeId) || ids.size !== branch.nodes.length) throw new Error(`无效支线 ${branch.id}`);
for (const node of branch.nodes) {
if (node.type === 'chapterEnd' || this.chapters.find((c) => c.id === branch.anchor.chapter)?.nodes.some((n) => n.id === node.id)) throw new Error(`无效支线节点 ${node.id}`);
const nexts = node.type === 'choice' ? node.options.map((o) => o.next) : [node.next];
if (nexts.some((next) => next !== '$resume' && !ids.has(next))) throw new Error(`无效支线出口 ${node.id}`);
}
}
private loadInsertion(branch: BranchPack): void {
if (this.insertions.some((b) => b.id === branch.id)) return;
if (this.insertions.some((b) => b.anchor.chapter === branch.anchor.chapter)) throw new Error('每章至多一条支线');
this.validateInsertion(branch);
this.insertions.push(branch);
for (const node of branch.nodes) this.nodeIndex.set(`${branch.anchor.chapter}/${node.id}`, node);
}
private clearInsertions(): void {
for (const b of this.insertions) for (const n of b.nodes) this.nodeIndex.delete(`${b.anchor.chapter}/${n.id}`);
this.insertions.length = 0;
this.state.activeBranchId = undefined;
}
private moveTo(next: string): void {
if (next === '$resume') {
const branch = this.insertions.find((b) => b.id === this.state.activeBranchId);
const anchor = branch && this.nodeIndex.get(`${branch.anchor.chapter}/${branch.anchor.afterNode}`);
if (!anchor || !('next' in anchor)) throw new Error('支线缺少返回挂点');
this.state.nodeId = anchor.next;
this.state.activeBranchId = undefined;
return;
}
const insertion = this.insertions.find((b) => b.anchor.chapter === this.state.chapterId && b.anchor.afterNode === this.state.nodeId);
this.state.nodeId = insertion ? insertion.startNodeId : next;
if (insertion) this.state.activeBranchId = insertion.id;
}
private persist(): void { private persist(): void {
this.progress?.save({ this.progress?.save({
sessionId: this.state.sessionId, sessionId: this.state.sessionId,
chapterId: this.state.chapterId, chapterId: this.state.chapterId,
nodeId: this.state.nodeId, nodeId: this.state.nodeId,
flags: { ...this.state.flags }, flags: { ...this.state.flags },
activeBranchId: this.state.activeBranchId,
insertions: this.insertions.map((b) => b.id),
}); });
} }
@@ -157,6 +247,15 @@ export class StoryEngine {
if (!snapshot) return false; if (!snapshot) return false;
const chapter = this.chapters.find((c) => c.id === snapshot.chapterId); const chapter = this.chapters.find((c) => c.id === snapshot.chapterId);
if (!chapter) return false; if (!chapter) return false;
const events = await this.store.list(snapshot.sessionId);
this.clearInsertions();
const ids = new Set([...(snapshot.insertions ?? []), ...events.filter((e) => e.kind === 'branch_inserted').map((e) => e.branchId)]);
for (const id of ids) {
const branch = this.branches.find((b) => b.id === id);
if (branch) {
try { this.loadInsertion(branch); } catch (e) { console.warn('[engine] 无法恢复支线', e); }
}
}
const nodeExists = this.nodeIndex.has(`${snapshot.chapterId}/${snapshot.nodeId}`); const nodeExists = this.nodeIndex.has(`${snapshot.chapterId}/${snapshot.nodeId}`);
this.state.sessionId = snapshot.sessionId; this.state.sessionId = snapshot.sessionId;
this.state.chapterId = snapshot.chapterId; this.state.chapterId = snapshot.chapterId;
@@ -165,8 +264,8 @@ export class StoryEngine {
this.state.activeVariant = null; this.state.activeVariant = null;
this.state.pendingFollowUp = null; this.state.pendingFollowUp = null;
this.state.finished = false; this.state.finished = false;
const events = await this.store.list(snapshot.sessionId);
const profile = [...events].reverse().find((e) => e.kind === 'profile')?.profile; const profile = [...events].reverse().find((e) => e.kind === 'profile')?.profile;
this.state.activeBranchId = this.insertions.find((b) => b.anchor.chapter === snapshot.chapterId && b.nodes.some((n) => n.id === this.state.nodeId))?.id;
this.state.playerName = profile?.name ?? ''; this.state.playerName = profile?.name ?? '';
this.skipHiddenNodes(); this.skipHiddenNodes();
this.persist(); this.persist();
@@ -183,7 +282,7 @@ export class StoryEngine {
if (node.type !== 'dialogue' && node.type !== 'cutscene') return; if (node.type !== 'dialogue' && node.type !== 'cutscene') return;
const text = node.type === 'dialogue' ? node.text : node.caption; const text = node.type === 'dialogue' ? node.text : node.caption;
if (resolveConditionalText(text, this.state.flags) !== null) return; if (resolveConditionalText(text, this.state.flags) !== null) return;
this.state.nodeId = node.next; this.moveTo(node.next);
} }
} }
@@ -198,9 +297,23 @@ export class StoryEngine {
} }
/** B 类选择:落事件并按选项跳转 */ /** B 类选择:落事件并按选项跳转 */
async choose(optionId: string): Promise<void> { async choose(optionId: string, behavior?: ChoiceBehavior): Promise<void> {
const node = this.currentNode(); const node = this.currentNode();
if (node.type !== 'choice') throw new Error('当前节点不是选择点'); if (node.type !== 'choice') throw new Error('当前节点不是选择点');
if (behavior?.decisionMs !== undefined && (!Number.isFinite(behavior.decisionMs) || behavior.decisionMs < 0)) throw new Error('无效决策耗时');
if (behavior?.interrupted) behavior = { ...behavior, decisionMs: undefined };
if (behavior?.timedOut && node.timer?.optionId !== optionId) throw new Error('无效超时选项');
if (node.puzzle) {
const puzzle = node.puzzle;
if (optionId.endsWith('-EXIT')) behavior = { hintsUsed: behavior?.hintsUsed ?? 0, solved: false };
else {
if (!behavior?.answers || behavior.answers.length !== puzzle.questions.length || behavior.answers.some((a, i) => !Number.isInteger(a) || a < 0 || a >= puzzle.questions[i].options.length)) throw new Error('请完成谜面两问');
const hints = behavior.hintsUsed ?? 0;
if (!Number.isInteger(hints) || hints < 0 || hints > puzzle.hints.length) throw new Error('无效提示档数');
const answerCorrect = behavior.answers.map((a, i) => a === puzzle.questions[i].answer);
behavior = { ...behavior, hintsUsed: hints, answerCorrect, solved: answerCorrect.every(Boolean) };
}
}
const option = node.options.find((o) => o.id === optionId); const option = node.options.find((o) => o.id === optionId);
if (!option) throw new Error(`未知选项 ${optionId}`); if (!option) throw new Error(`未知选项 ${optionId}`);
await this.store.append( await this.store.append(
@@ -211,11 +324,13 @@ export class StoryEngine {
pointId: node.pointId, pointId: node.pointId,
dimensions: node.dimensions, dimensions: node.dimensions,
optionId: option.id, optionId: option.id,
optionLabel: option.label, optionLabel: this.renderText(node.puzzle ? `${node.prompt} ${option.label};${node.puzzle.questions.map((q, i) => `${q.prompt} ${q.options[behavior?.answers?.[i] ?? -1] ?? '未作答'}`).join(';')}` : option.label),
behavior,
branchId: this.state.activeBranchId,
}), }),
); );
Object.assign(this.state.flags, choiceFlags(node.pointId, option.id)); Object.assign(this.state.flags, choiceFlags(node.pointId, option.id, behavior));
this.state.nodeId = option.next; this.moveTo(option.next);
this.skipHiddenNodes(); this.skipHiddenNodes();
this.persist(); this.persist();
} }
@@ -229,6 +344,7 @@ export class StoryEngine {
async submitNarration(transcript: string | null): Promise<NarrationResult | null> { async submitNarration(transcript: string | null): Promise<NarrationResult | null> {
const node = this.currentNode(); const node = this.currentNode();
if (node.type !== 'narration') throw new Error('当前节点不是讲述点'); if (node.type !== 'narration') throw new Error('当前节点不是讲述点');
if (node.pointId === 'C3-02') throw new Error('问心之室仅记录路径,不接收讲述文本');
if (this.state.pendingFollowUp) throw new Error('有未处理的追问,请先 answerFollowUp / skipFollowUp'); if (this.state.pendingFollowUp) throw new Error('有未处理的追问,请先 answerFollowUp / skipFollowUp');
if (transcript === null) return this.finalizeNarration(node, null); if (transcript === null) return this.finalizeNarration(node, null);
// Jev 故障(CORS / 网络)时降级:跳过追问环节,剧情继续,不卡死 // Jev 故障(CORS / 网络)时降级:跳过追问环节,剧情继续,不卡死
@@ -331,7 +447,31 @@ export class StoryEngine {
await this.store.append(event); await this.store.append(event);
Object.assign(this.state.flags, narrationFlags(node.pointId, variant.variantId, transcript)); Object.assign(this.state.flags, narrationFlags(node.pointId, variant.variantId, transcript));
this.state.activeVariant = variant; this.state.activeVariant = variant;
this.state.nodeId = node.next; this.moveTo(node.next);
this.skipHiddenNodes();
this.persist();
return { variant, event };
}
async resolvePrivateIllusion(mode: 'farewell_done' | 'farewell_partial' | 'exit_room'): Promise<NarrationResult> {
const node = this.currentNode();
if (node.type !== 'narration' || node.pointId !== 'C3-02') throw new Error('当前节点不是问心之室');
const variant = await this.resolveVariant(node, null);
const event = makeEvent({
sessionId: this.state.sessionId,
chapterId: this.state.chapterId,
kind: 'narration',
pointId: node.pointId,
dimensions: [],
variantId: variant.variantId,
narrationMode: mode,
transcript: null,
});
await this.store.append(event);
this.state.flags.c3_02_mode = mode;
this.state.flags.c3_02_fantasy_density = variant.variantId === 'generic_fog' ? 'fog' : 'partial';
this.state.activeVariant = variant;
this.moveTo(node.next);
this.skipHiddenNodes(); this.skipHiddenNodes();
this.persist(); this.persist();
return { variant, event }; return { variant, event };
@@ -339,6 +479,8 @@ export class StoryEngine {
/** 沉默永远是合法选项(CONTEXT:在什么面前沉默本身也是数据);沉默路径不触发追问 */ /** 沉默永远是合法选项(CONTEXT:在什么面前沉默本身也是数据);沉默路径不触发追问 */
async skipNarration(): Promise<NarrationResult> { async skipNarration(): Promise<NarrationResult> {
const node = this.currentNode();
if (node.type === 'narration' && node.pointId === 'C3-02') return this.resolvePrivateIllusion('exit_room');
const r = await this.submitNarration(null); const r = await this.submitNarration(null);
if (!r) throw new Error('unreachable: 沉默路径不触发追问'); if (!r) throw new Error('unreachable: 沉默路径不触发追问');
return r; return r;
@@ -485,8 +627,8 @@ export class StoryEngine {
profile, profile,
}), }),
); );
this.state.nodeId = node.next;
this.state.playerName = profile.name.trim(); this.state.playerName = profile.name.trim();
this.moveTo(node.next);
this.skipHiddenNodes(); this.skipHiddenNodes();
this.persist(); this.persist();
} }
@@ -522,6 +664,7 @@ export class StoryEngine {
const chapter = this.chapters.find((c) => c.id === (chapterId ?? this.state.chapterId)); const chapter = this.chapters.find((c) => c.id === (chapterId ?? this.state.chapterId));
if (!chapter) throw new Error(`unknown chapter ${chapterId}`); if (!chapter) throw new Error(`unknown chapter ${chapterId}`);
this.state.sessionId = newSessionId(); this.state.sessionId = newSessionId();
this.clearInsertions();
this.state.chapterId = chapter.id; this.state.chapterId = chapter.id;
this.state.nodeId = chapter.startNodeId; this.state.nodeId = chapter.startNodeId;
this.state.activeVariant = null; this.state.activeVariant = null;
+7 -2
View File
@@ -1,4 +1,4 @@
import type { DimensionTag, FollowUpDimension, IdentityProfile } from './types'; import type { ChoiceBehavior, DimensionTag, FollowUpDimension, IdentityProfile } from './types';
/** /**
* 选择/讲述事件存储(ADR-0004:历史不可涂改)。 * 选择/讲述事件存储(ADR-0004:历史不可涂改)。
@@ -17,7 +17,10 @@ export interface StoryEvent {
sessionId: string; sessionId: string;
ts: number; ts: number;
chapterId: string; chapterId: string;
kind: 'choice' | 'narration' | 'profile' | 'followup' | 'followup_skipped' | 'consent'; kind: 'choice' | 'narration' | 'profile' | 'followup' | 'followup_skipped' | 'consent' | 'branch_inserted' | 'evidence_insufficient';
branchId?: string;
confidence?: number;
behavior?: ChoiceBehavior;
/** 选择点 ID,如 C1-03;consent 事件固定为 'CONSENT' */ /** 选择点 ID,如 C1-03;consent 事件固定为 'CONSENT' */
pointId: string; pointId: string;
dimensions: DimensionTag[]; dimensions: DimensionTag[];
@@ -29,6 +32,8 @@ export interface StoryEvent {
/** narration 事件:选中的场景变体(情绪签名 → variantId) */ /** narration 事件:选中的场景变体(情绪签名 → variantId) */
variantId?: string; variantId?: string;
emotionSignature?: string; emotionSignature?: string;
/** C3-02 幻境:只记录路径,不保留在室内说的话 */
narrationMode?: 'farewell_done' | 'farewell_partial' | 'exit_room';
/** followup / followup_skipped 事件:追问针对的缺失维度 */ /** followup / followup_skipped 事件:追问针对的缺失维度 */
followUpDimension?: FollowUpDimension; followUpDimension?: FollowUpDimension;
/** 讲述/追问的转写文本(Web Speech API 只给文本,不留音频,符合 ADR-0004 即焚原则;沉默为 null) */ /** 讲述/追问的转写文本(Web Speech API 只给文本,不留音频,符合 ADR-0004 即焚原则;沉默为 null) */
+92 -14
View File
@@ -1,4 +1,5 @@
import type { StoryEvent } from './eventStore'; import type { StoryEvent } from './eventStore';
import type { ChoiceBehavior } from './types';
/** /**
* 分支变量(flag)系统 —— 依据 docs/script/act1.md 附录 A。 * 分支变量(flag)系统 —— 依据 docs/script/act1.md 附录 A。
@@ -17,15 +18,15 @@ import type { StoryEvent } from './eventStore';
*/ */
export type Flags = Record<string, string>; export type Flags = Record<string, string>;
/** 解谜点正确答案(optionId),用于 *_solved 变量 */ /** 单选解谜点正确答案(optionId);C3-01 按两题行为记录单独聚合。 */
const PUZZLE_ANSWERS: Record<string, string> = { const PUZZLE_ANSWERS: Record<string, string> = {
'C1-05': 'stele_guardian', 'C1-05': 'stele_guardian',
'C2-04': 'heart_xin', 'C2-04': 'heart_xin',
'C3-01': 'rite_correct',
}; };
/** 选择事件 → 变量赋值(变量名与取值对齐附录 A;optionId 已与枚举值对齐) */ /** 选择事件 → 变量赋值(变量名与取值对齐附录 A;optionId 已与枚举值对齐) */
export function choiceFlags(pointId: string, optionId: string): Flags { export function choiceFlags(pointId: string, optionId: string, behavior?: ChoiceBehavior): Flags {
if (pointId.startsWith('CB-')) return branchChoiceFlags(pointId, optionId, behavior);
const f: Flags = {}; const f: Flags = {};
switch (pointId) { switch (pointId) {
case 'C1-03': case 'C1-03':
@@ -36,12 +37,17 @@ export function choiceFlags(pointId: string, optionId: string): Flags {
if (optionId === 'd_play_along') f.flag_laogui_thread = 'true'; if (optionId === 'd_play_along') f.flag_laogui_thread = 'true';
break; break;
case 'C1-05': case 'C1-05':
case 'C2-04': case 'C2-04': {
case 'C3-01': {
const prefix = pointId.toLowerCase().replace('-', '_'); const prefix = pointId.toLowerCase().replace('-', '_');
f[`${prefix}_solved`] = String(optionId === PUZZLE_ANSWERS[pointId]); f[`${prefix}_solved`] = String(optionId === PUZZLE_ANSWERS[pointId]);
break; break;
} }
case 'C3-01': {
f.c3_01_hints_used = String(behavior?.hintsUsed ?? 0);
const correct = behavior?.answerCorrect?.filter(Boolean).length ?? 0;
f.c3_01_solved = correct === 2 ? 'true' : correct === 1 ? 'partial' : 'false';
break;
}
case 'C1-06': case 'C1-06':
f.c1_06_landing = optionId; // a_safe / b_precise f.c1_06_landing = optionId; // a_safe / b_precise
break; break;
@@ -61,9 +67,9 @@ export function choiceFlags(pointId: string, optionId: string): Flags {
break; break;
case 'C3-03': case 'C3-03':
f.c3_03_choice = optionId; f.c3_03_choice = optionId;
f.atang_injury = optionId === 'a_person' ? 'bruise' : optionId === 'b_relic' ? 'fracture' : 'sprain'; f.atang_injury = 'fracture';
f.wenjian_injury = optionId === 'c_command' ? 'shoulder' : 'none'; f.relic_loss = optionId === 'c_command' ? 'outer_three' : 'one_ding';
f.relic_loss = optionId === 'a_person' ? 'one_ding' : 'none'; f.relic_record = optionId === 'b_relic' ? 'full' : 'partial';
break; break;
case 'C3-04': case 'C3-04':
f.c3_04_stance = optionId; f.c3_04_stance = optionId;
@@ -72,7 +78,7 @@ export function choiceFlags(pointId: string, optionId: string): Flags {
break; break;
case 'C3-05': case 'C3-05':
f.c3_05_rope = optionId; f.c3_05_rope = optionId;
f.flag_rope_holder = optionId === 'a_accept' ? 'you' : optionId === 'b_protocol' ? 'shenduo' : 'atang'; f.flag_rope_holder = optionId === 'a_accept' ? 'you' : 'shenduo';
break; break;
case 'C3-06': case 'C3-06':
f.flag_arch_doubt = 'true'; f.flag_arch_doubt = 'true';
@@ -104,7 +110,9 @@ export function narrationFlags(pointId: string, variantId: string | undefined, t
const f: Flags = {}; const f: Flags = {};
switch (pointId) { switch (pointId) {
case 'C1-02': case 'C1-02':
if (transcript === null) f.c1_02_signature = 'silent'; f.c1_02_signature = transcript === null ? 'silent' : ({
sealed_love_tomb: 'warm_loss', war_graves: 'grief', empty_post_station: 'release', desecrated_niche: 'grievance',
} as Record<string, string>)[variantId ?? ''] ?? 'silent';
break; break;
case 'C2-02': { case 'C2-02': {
// S3(评审修订):四档物质所得严格相等(方位+时辰+完整谶语全给),flag_prophecy_heard 已废除 // S3(评审修订):四档物质所得严格相等(方位+时辰+完整谶语全给),flag_prophecy_heard 已废除
@@ -115,11 +123,10 @@ export function narrationFlags(pointId: string, variantId: string | undefined, t
case 'C2-05': { case 'C2-05': {
const depth = transcript === null ? 'silent' : variantId ? VARIANT_SIGNATURE[variantId] : undefined; const depth = transcript === null ? 'silent' : variantId ? VARIANT_SIGNATURE[variantId] : undefined;
if (depth) f.c2_05_depth = depth; if (depth) f.c2_05_depth = depth;
if (variantId === 'wenjian_close_early') f.flag_wenjian_secret = 'true'; if (variantId === 'wenjian_close_late') f.flag_wenjian_secret = 'true';
break; break;
} }
case 'C3-02': case 'C3-02':
f.c3_02_mode = transcript === null ? 'exit_room' : 'farewell_done';
break; break;
} }
return f; return f;
@@ -129,14 +136,85 @@ export function narrationFlags(pointId: string, variantId: string | undefined, t
export function deriveFlags(events: StoryEvent[]): Flags { export function deriveFlags(events: StoryEvent[]): Flags {
const flags: Flags = {}; const flags: Flags = {};
for (const e of events) { for (const e of events) {
if (e.kind === 'choice' && e.optionId) Object.assign(flags, choiceFlags(e.pointId, e.optionId)); if (e.kind === 'choice' && e.optionId) Object.assign(flags, choiceFlags(e.pointId, e.optionId, e.behavior));
else if (e.kind === 'narration') Object.assign(flags, narrationFlags(e.pointId, e.variantId, e.transcript ?? null)); else if (e.kind === 'narration') {
Object.assign(flags, narrationFlags(e.pointId, e.variantId, e.transcript ?? null));
if (e.pointId === 'C3-02' && e.narrationMode) {
flags.c3_02_mode = e.narrationMode;
flags.c3_02_fantasy_density = e.variantId === 'generic_fog' ? 'fog' : 'partial';
}
}
} }
return flags; return flags;
} }
// ---------- 条件文本解析 ---------- // ---------- 条件文本解析 ----------
const BRANCH_CHOICES: Record<string, [string, string[]]> = {
'CB-ATT-A-01': ['cb_att_a_search', ['a_rush', 'b_wait', 'c_organize']],
'CB-ATT-A-02': ['cb_att_a_reunion', ['a_scold', 'b_swallow', 'c_joke', 'd_care']],
'CB-ATT-B-01': ['cb_att_b_boundary', ['a_go', 'b_wait', 'c_proxy']],
'CB-CON-A-01': ['cb_con_a_ruling', ['a_settle', 'b_principle', 'c_invest', 'd_document']],
'CB-CON-A-02': ['cb_con_a_aftermath', ['a_comfort', 'b_mirror', 'c_leave']],
'CB-CON-B-01': ['cb_con_b_ruling', ['a_void', 'b_defer', 'c_reprice', 'd_document']],
'CB-CON-B-02': ['cb_con_b_channel', ['a_keep', 'b_report', 'c_cut']],
'CB-EMO-A-01': ['cb_emo_a_shelter', ['a_count', 'b_map', 'c_shelter']],
'CB-EMO-A-02': ['cb_emo_a_cry', ['a_look', 'b_hold', 'c_call']],
'CB-EMO-B-01': ['cb_emo_b_plate', ['a_alert', 'b_relay', 'c_record']],
'CB-EMO-B-02': ['cb_emo_b_vent', ['a_record', 'b_mark', 'c_leave']],
'CB-VAL-A-01': ['cb_val_a_water', ['a_give', 'b_cup', 'c_trade', 'd_redirect']],
'CB-VAL-A-02': ['cb_val_a_ledger', ['a_honest', 'b_smooth', 'c_defer']],
'CB-VAL-B-01': ['cb_val_b_medicine', ['a_use', 'b_refer', 'c_split', 'd_delegate']],
'CB-CUL-A-00': ['cb_cul_a_entry', ['enter', 'skip']],
};
function branchChoiceFlags(point: string, option: string, behavior?: ChoiceBehavior): Flags {
const f: Flags = {};
const letter = option.slice(point.length + 1);
const index = 'ABCD'.indexOf(letter);
const mapping = BRANCH_CHOICES[point];
if (mapping && index >= 0) f[mapping[0]] = mapping[1][index];
if (point.startsWith('CB-EMO-')) {
const prefix = point.slice(0, 8).toLowerCase().replaceAll('-', '_');
if (behavior?.decisionMs !== undefined) f[`${prefix}_rt${Number(point.slice(-2))}`] = String(behavior.decisionMs);
if (letter === 'TIMEOUT') f[mapping[0]] = point.includes('-A-') ? 'follow' : 'self_rescue';
}
if (point === 'CB-ATT-A-02') {
f.flag_side_stairs = 'true';
if (letter === 'A') f.att_debt = 'true';
}
if (point === 'CB-ATT-B-01') {
f.flag_pit27_figurines = 'true';
f[['att_seen', 'att_trusted', 'att_owed'][index]] = 'true';
}
if (point === 'CB-CON-A-01') {
f.market_warmth = letter === 'B' ? 'cold' : 'normal';
if (letter === 'C') f.flag_water_intel = 'true';
if (letter === 'D') f.flag_shiling_noticed = 'true';
}
if (point === 'CB-CON-B-01' && ['C', 'D'].includes(letter)) f.flag_palace_crates = 'true';
if (point === 'CB-CON-B-02') f.flag_fish_channel = String(letter === 'A');
if (point === 'CB-EMO-B-02') f[letter === 'A' ? 'flag_mould_rubbing' : 'flag_sealed_vault'] = 'true';
if (point === 'CB-VAL-A-01') {
if (['C', 'D'].includes(letter)) f.flag_silver_bracelet = 'true';
if (['A', 'C'].includes(letter)) f.flag_thirst_days = 'true';
}
if (point === 'CB-VAL-B-01') {
f.flag_hidden_stairs = 'true';
f['rapport.shouling'] = 'true';
if (letter === 'B') f.flag_herb_poultice = 'true';
}
if (point === 'CB-CUL-A-01' || point === 'CB-CUL-B-01') {
const prefix = point.slice(0, 8).toLowerCase().replaceAll('-', '_');
f[`${prefix}_hints_used`] = String(behavior?.hintsUsed ?? 0);
const correct = behavior?.answerCorrect?.filter(Boolean).length ?? (behavior?.solved ? 2 : 0);
f[`${prefix}_solved`] = option.endsWith('-EXIT') ? 'not_attempted' : correct === 2 ? 'true' : correct === 1 ? 'partial' : 'false';
if (option.endsWith('-EXIT')) f[`${prefix}_left`] = 'true';
f[point.includes('-A-') ? 'flag_wall_relocation' : 'flag_tenth_king'] = 'true';
}
return f;
}
const CONDITION_RE = /【若([^】]+)】/g; const CONDITION_RE = /【若([^】]+)】/g;
/** 求值单个条件表达式,如 `c2_06_jade = a_public`、`wenjian_health != weak` */ /** 求值单个条件表达式,如 `c2_06_jade = a_public`、`wenjian_health != weak` */
+2
View File
@@ -12,6 +12,8 @@ export interface ProgressSnapshot {
nodeId: string; nodeId: string;
/** 分支变量随位置一起持久化,恢复后条件文本渲染不断档 */ /** 分支变量随位置一起持久化,恢复后条件文本渲染不断档 */
flags: Flags; flags: Flags;
activeBranchId?: string;
insertions?: string[];
} }
export interface ProgressStore { export interface ProgressStore {
+64
View File
@@ -0,0 +1,64 @@
import type { StoryEvent } from './eventStore';
import type { JevClient } from '../services/jev';
export const PROFILE_DIMENSIONS = [
{ id: 'attachment', label: '依恋类型', description: '你在亲密与分离面前的姿态' },
{ id: 'conflict_style', label: '冲突处理风格', description: '分歧当前,你先做什么' },
{ id: 'values', label: '价值观与人生优先级', description: '两体面或两难堪之间,你让什么先走' },
{ id: 'emotional_stability', label: '情绪稳定性', description: '突发与悔恨面前的波形' },
{ id: 'risk', label: '风险偏好', description: '稳妥落点与精确落点之间的你' },
{ id: 'culture', label: '文化底蕴', description: '碑文、星象与礼制,实测而非自报' },
{ id: 'mate_preference', label: '择偶偏好', description: '你信任谁、把后背交给谁' },
] as const;
export type ProfileDimension = (typeof PROFILE_DIMENSIONS)[number]['id'];
export interface DimensionScore {
id: ProfileDimension;
label: string;
score: number;
max: number;
confidence: number;
}
export type Confidences = Partial<Record<ProfileDimension, number>>;
/** 身份与同意不是人格证据;路由标记只说明采样路径,不作为性格结论。 */
export function buildState(events: StoryEvent[]): string {
const lines = events.flatMap((e) => {
if (e.pointId === 'C3-02') return [];
const prefix = `[轨迹 ${e.sessionId} / ${e.pointId}]`;
switch (e.kind) {
case 'choice':
return [`${prefix} 选择「${e.optionLabel}」(维度: ${e.dimensions.join('/')})` +
(e.behavior ? ` 行为: ${JSON.stringify(e.behavior)}` : '')];
case 'narration':
return [`${prefix} ${e.transcript === null ? '沉默' : `讲述: ${e.transcript}`} 情绪签名: ${e.emotionSignature ?? '未知'}`];
case 'followup': return [`${prefix} 追问(${e.followUpDimension})回答: ${e.transcript}`];
case 'followup_skipped': return [`${prefix} 追问(${e.followUpDimension})回答: 记不清了(跳过)`];
case 'branch_inserted': return [`${prefix} 采样路径: ${e.branchId},补测 ${e.dimensions.join('/')},触发置信度 ${e.confidence}(不是行为证据)`];
case 'evidence_insufficient': return [`${prefix} 路由标记: ${e.dimensions.join('/')} 证据不足(不是性格结论)`];
default: return [];
}
});
return `用户在互动剧情《苍梧疑冢》第一幕中的行为轨迹(按时间排序,轨迹分开比较)。支线与主线同等采样;结论不一致也是信号,不回滚。沉默、跳过和使用提示不等于低人格水平。路径标记不可当作行为证据:\n${lines.join('\n')}`;
}
export async function scoreProfile(events: StoryEvent[], jev: JevClient): Promise<DimensionScore[]> {
const criteria = Array.from({ length: 10 }, (_, i) => `${i + 1} 级`);
const answers = await jev.ask(buildState(events), Object.fromEntries(PROFILE_DIMENSIONS.map((d) => [d.id, {
type: 'score' as const,
instructions: `根据用户行为轨迹,评估其「${d.label}」维度水平:${d.description}。仅按已观察到的行为判断,证据少或矛盾时降低置信度。`,
criteria,
}])));
return PROFILE_DIMENSIONS.map((d) => {
const a = answers?.[d.id];
if (a?.type !== 'score' || !Number.isFinite(a.score) || a.score < 0 || a.score > 9 ||
!Number.isFinite(a.confidence) || a.confidence < 0 || a.confidence > 1) {
throw new Error(`Jev 返回无效的 ${d.id} 评分`);
}
return { id: d.id, label: d.label, score: Math.round((a.score + 1) * 10) / 10, max: 10, confidence: a.confidence };
});
}
export function extractConfidences(scores: DimensionScore[]): Confidences {
return Object.fromEntries(scores.map((s) => [s.id, s.confidence]));
}
+23
View File
@@ -42,6 +42,9 @@ export interface ChoiceNode extends BaseNode {
/** 承载维度标签,如 ["values", "conflict_style"] */ /** 承载维度标签,如 ["values", "conflict_style"] */
dimensions: DimensionTag[]; dimensions: DimensionTag[];
options: ChoiceOption[]; options: ChoiceOption[];
timer?: { seconds: number; optionId: string };
untimed?: boolean;
puzzle?: { questions: { prompt: string; options: string[]; answer: number }[]; hints: string[]; exitLabel?: string };
} }
export interface ChoiceOption { export interface ChoiceOption {
@@ -146,9 +149,29 @@ export interface IdentityNode extends BaseNode {
export type StoryNode = DialogueNode | ChoiceNode | NarrationNode | CutsceneNode | IdentityNode | ChapterEndNode; export type StoryNode = DialogueNode | ChoiceNode | NarrationNode | CutsceneNode | IdentityNode | ChapterEndNode;
export interface Chapter { export interface Chapter {
estimatedMinutes?: number;
id: string; id: string;
act: number; act: number;
title: string; title: string;
startNodeId: string; startNodeId: string;
nodes: StoryNode[]; nodes: StoryNode[];
} }
export interface BranchPack {
id: string;
dimension: 'attachment' | 'values' | 'conflict_style' | 'emotional_stability' | 'culture';
anchor: { chapter: string; afterNode: string };
estimatedMinutes: number;
startNodeId: string;
nodes: StoryNode[];
}
export interface ChoiceBehavior {
decisionMs?: number;
timedOut?: boolean;
interrupted?: boolean;
hintsUsed?: number;
answers?: number[];
answerCorrect?: boolean[];
solved?: boolean;
}
+4
View File
@@ -59,6 +59,7 @@ export type JevAnswer =
}; };
export interface JevClient { export interface JevClient {
adaptiveScoring?: boolean;
ask(state: unknown, questions: Record<string, JevQuestion>): Promise<Record<string, JevAnswer>>; ask(state: unknown, questions: Record<string, JevQuestion>): Promise<Record<string, JevAnswer>>;
} }
@@ -86,6 +87,7 @@ export class HttpJevClient implements JevClient {
headers.Authorization = `Bearer ${this.apiKey}`; headers.Authorization = `Bearer ${this.apiKey}`;
} }
const res = await fetch(url, { const res = await fetch(url, {
signal: AbortSignal.timeout(15000),
method: 'POST', method: 'POST',
headers, headers,
body: JSON.stringify({ model: 'jev-latest', state, questions } satisfies JevRequest), body: JSON.stringify({ model: 'jev-latest', state, questions } satisfies JevRequest),
@@ -121,6 +123,7 @@ function prng(seed: number, n: number): number[] {
/** 无 key 开发用:确定性伪随机返分(同一输入永远同一输出,输出形状与真实 API 对齐)。 */ /** 无 key 开发用:确定性伪随机返分(同一输入永远同一输出,输出形状与真实 API 对齐)。 */
export class MockJevClient implements JevClient { export class MockJevClient implements JevClient {
adaptiveScoring = true;
async ask(state: unknown, questions: Record<string, JevQuestion>): Promise<Record<string, JevAnswer>> { async ask(state: unknown, questions: Record<string, JevQuestion>): Promise<Record<string, JevAnswer>> {
const stateKey = JSON.stringify(state) ?? ''; const stateKey = JSON.stringify(state) ?? '';
const answers: Record<string, JevAnswer> = {}; const answers: Record<string, JevAnswer> = {};
@@ -177,6 +180,7 @@ export function getJevClient(): JevClient {
} else { } else {
console.warn('[jev] 未配置 VITE_JEV_PROXY_URL / VITE_TYPESAFE_API_KEY,降级为 MockJevClient(确定性伪随机返分)'); console.warn('[jev] 未配置 VITE_JEV_PROXY_URL / VITE_TYPESAFE_API_KEY,降级为 MockJevClient(确定性伪随机返分)');
singleton = new MockJevClient(); singleton = new MockJevClient();
singleton.adaptiveScoring = false;
} }
return singleton; return singleton;
} }
+95 -18
View File
@@ -1,4 +1,5 @@
import { useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import type { ChoiceBehavior } from '../engine/types';
import type { StoryEngine, PendingFollowUp } from '../engine/engine'; import type { StoryEngine, PendingFollowUp } from '../engine/engine';
import type { ChoiceNode, FollowUpDimension, IdentityNode, IdentityProfile, NarrationNode } from '../engine/types'; import type { ChoiceNode, FollowUpDimension, IdentityNode, IdentityProfile, NarrationNode } from '../engine/types';
import { VoiceInput } from './VoiceInput'; import { VoiceInput } from './VoiceInput';
@@ -32,12 +33,24 @@ interface Props {
export function StoryPlayer({ engine, onFinish }: Props) { export function StoryPlayer({ engine, onFinish }: Props) {
// tick 只是触发重渲染;引擎是唯一事实源 // tick 只是触发重渲染;引擎是唯一事实源
const [tick, setTick] = useState(0); const [tick, setTick] = useState(0);
const [advancing, setAdvancing] = useState(false);
const [error, setError] = useState('');
const rerender = () => setTick((t) => t + 1); const rerender = () => setTick((t) => t + 1);
void tick; void tick;
const node = engine.currentNode(); const node = engine.currentNode();
const chapter = engine.currentChapter(); const chapter = engine.currentChapter();
const variant = engine.state.activeVariant; const variant = engine.state.activeVariant;
async function advance() {
if (advancing) return;
setAdvancing(true);
setError('');
try {
await engine.advance();
if (engine.state.finished) onFinish();
else rerender();
} catch (e) { setError(String(e)); } finally { setAdvancing(false); }
}
return ( return (
<div <div
@@ -54,7 +67,7 @@ export function StoryPlayer({ engine, onFinish }: Props) {
</header> </header>
{node.type === 'dialogue' && ( {node.type === 'dialogue' && (
<div className="dialogue-box" onClick={() => { engine.advance(); if (engine.state.finished) onFinish(); else rerender(); }}> <div className="dialogue-box" onClick={advance}>
<div className="sprite" style={{ background: SPRITE_COLORS[node.sprite ?? 'scene'] }}> <div className="sprite" style={{ background: SPRITE_COLORS[node.sprite ?? 'scene'] }}>
{node.speaker.slice(0, 1)} {node.speaker.slice(0, 1)}
</div> </div>
@@ -67,10 +80,21 @@ export function StoryPlayer({ engine, onFinish }: Props) {
)} )}
{node.type === 'choice' && ( {node.type === 'choice' && (
<ChoiceView node={node} renderText={(t) => engine.renderText(t)} onPick={async (id) => { await engine.choose(id); rerender(); }} /> <ChoiceView key={node.id} node={node} renderText={(t) => engine.renderText(t)} onPick={async (id, behavior) => { await engine.choose(id, behavior); rerender(); }} />
)} )}
{node.type === 'narration' && ( {node.type === 'narration' && node.pointId === 'C3-02' && (
<div className="narration-box">
<div className="mechanism-tag">问心之室 · 仅你可见</div>
<p className="narration-prompt">{engine.renderText(node.prompt)}</p>
<p className="hint">这里不录音、不留讲述文本,也不把室内告别用于画像。</p>
<button className="primary" onClick={async () => { await engine.resolvePrivateIllusion('farewell_done'); rerender(); }}>走进幻境,完成告别</button>
<button className="secondary" onClick={async () => { await engine.resolvePrivateIllusion('farewell_partial'); rerender(); }}>走进幻境,中途折返</button>
<button className="secondary" onClick={async () => { await engine.resolvePrivateIllusion('exit_room'); rerender(); }}>不进入,转身离开</button>
</div>
)}
{node.type === 'narration' && node.pointId !== 'C3-02' && (
<NarrationView <NarrationView
node={node} node={node}
pendingFollowUp={engine.state.pendingFollowUp} pendingFollowUp={engine.state.pendingFollowUp}
@@ -84,7 +108,7 @@ export function StoryPlayer({ engine, onFinish }: Props) {
{node.type === 'identity' && <IdentityView node={node} engine={engine} onDone={rerender} />} {node.type === 'identity' && <IdentityView node={node} engine={engine} onDone={rerender} />}
{node.type === 'cutscene' && ( {node.type === 'cutscene' && (
<div className="cutscene" onClick={() => { engine.advance(); rerender(); }}> <div className="cutscene" onClick={advance}>
<div className="cutscene-placeholder">▶ {node.video}</div> <div className="cutscene-placeholder">▶ {node.video}</div>
<p>{engine.renderText(node.caption)}</p> <p>{engine.renderText(node.caption)}</p>
<span className="hint">点击继续 ▸</span> <span className="hint">点击继续 ▸</span>
@@ -96,41 +120,94 @@ export function StoryPlayer({ engine, onFinish }: Props) {
<h2>{chapter.title} · 完</h2> <h2>{chapter.title} · 完</h2>
<button <button
className="primary" className="primary"
onClick={() => { disabled={advancing}
engine.advance(); aria-busy={advancing}
if (engine.state.finished) onFinish(); onClick={advance}
else rerender();
}}
> >
{node.nextChapter ? '进入下一章' : '生成人格画像报告'} {node.nextChapter ? '进入下一章' : '生成人格画像报告'}
</button> </button>
</div> </div>
)} )}
{error && <p role="alert">{error}</p>}
</div> </div>
); );
} }
function ChoiceView({ node, renderText, onPick }: { node: ChoiceNode; renderText: (t: string) => string; onPick: (id: string) => void }) { function ChoiceView({ node, renderText, onPick }: { node: ChoiceNode; renderText: (t: string) => string; onPick: (id: string, behavior?: ChoiceBehavior) => Promise<void> }) {
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const [error, setError] = useState('');
const [hints, setHints] = useState(0);
const [answers, setAnswers] = useState<number[]>(() => node.puzzle?.questions.map(() => -1) ?? []);
const [remaining, setRemaining] = useState(node.timer?.seconds ?? 0);
const started = useRef(0);
const submitted = useRef(false);
const interrupted = useRef(false);
const pickRef = useRef(onPick);
useEffect(() => { pickRef.current = onPick; }, [onPick]);
useEffect(() => { started.current = Date.now(); }, []);
useEffect(() => {
if (!node.timer) return;
const markInterrupted = () => { if (document.hidden) interrupted.current = true; };
document.addEventListener('visibilitychange', markInterrupted);
return () => document.removeEventListener('visibilitychange', markInterrupted);
}, [node.timer]);
async function pick(id: string, timedOut = false) {
if (submitted.current) return;
submitted.current = true;
setBusy(true);
setError('');
try {
await pickRef.current(id, {
decisionMs: node.untimed || interrupted.current ? undefined : Math.max(0, Date.now() - started.current),
timedOut,
interrupted: interrupted.current || undefined,
...(node.puzzle ? { answers, hintsUsed: hints } : {}),
});
} catch (e) {
submitted.current = false;
setError(String(e));
setBusy(false);
}
}
const timeoutPick = useRef(pick);
useEffect(() => { timeoutPick.current = pick; });
useEffect(() => {
if (!node.timer) return;
const timer = node.timer;
const interval = window.setInterval(() => {
const left = Math.max(0, timer.seconds - (Date.now() - started.current) / 1000);
setRemaining(left);
if (left === 0 && !submitted.current) void timeoutPick.current(timer.optionId, true);
}, 100);
return () => window.clearInterval(interval);
}, [node.timer]);
return ( return (
<div className="choice-box"> <div className="choice-box">
<p className="choice-prompt">{renderText(node.prompt)}</p> <p className="choice-prompt">{renderText(node.prompt)}</p>
{node.timer && <progress aria-label="剩余决策时间" max={node.timer.seconds} value={remaining} />}
{node.puzzle && <>
{node.puzzle.questions.map((q, i) => <fieldset key={i} disabled={busy}>
<legend>{q.prompt}</legend>
{q.options.map((label, j) => <label key={j} className="puzzle-option">
<input type="radio" name={`${node.id}-${i}`} checked={answers[i] === j} onChange={() => setAnswers((old) => old.map((a, k) => k === i ? j : a))} />{label}
</label>)}
</fieldset>)}
{node.puzzle.hints.slice(0, hints).map((hint, i) => <p key={i}>{hint}</p>)}
<button className="secondary" disabled={busy || hints >= node.puzzle.hints.length} onClick={() => setHints((h) => h + 1)}>请教温简</button>
</>}
<div className="choice-options"> <div className="choice-options">
{node.options.map((o) => ( {node.options.filter((o) => o.id !== node.timer?.optionId).map((o) => (
<button <button
key={o.id} key={o.id}
className="choice-btn" className="choice-btn"
disabled={busy} disabled={busy || (!!node.puzzle && !o.id.endsWith('-EXIT') && answers.includes(-1))}
onClick={async () => { onClick={() => pick(o.id)}
setBusy(true);
await onPick(o.id);
setBusy(false);
}}
> >
{renderText(o.label)} {renderText(o.label)}
</button> </button>
))} ))}
</div> </div>
{error && <p role="alert">{error}</p>}
</div> </div>
); );
} }
+12 -57
View File
@@ -1,43 +1,18 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import type { EventStore, StoryEvent } from '../../engine/eventStore'; import type { EventStore } from '../../engine/eventStore';
import type { JevClient } from '../../services/jev'; import type { JevClient } from '../../services/jev';
import { BADGE_MAP, PROFILE_DIMENSIONS } from './badges'; import { BADGE_MAP } from './badges';
import { scoreProfile } from '../../engine/scoring';
import type { DimensionScore } from '../../engine/scoring';
export type { DimensionScore } from '../../engine/scoring';
import { ShareCard } from './ShareCard'; import { ShareCard } from './ShareCard';
export interface DimensionScore {
id: string;
label: string;
score: number;
max: number;
confidence: number;
}
interface Props { interface Props {
store: EventStore; store: EventStore;
jev: JevClient; jev: JevClient;
onReplay: () => void; onReplay: () => void;
} }
/** 把 append-only 事件轨迹汇总为喂给 Jev 的 state 文本(profile 身份字段是硬过滤字段,不进画像) */
function buildState(events: StoryEvent[]): string {
const lines = events
.filter((e) => e.kind !== 'profile')
.map((e) => {
if (e.kind === 'choice') {
return `[${e.pointId}] 选择「${e.optionLabel}」(维度: ${e.dimensions.join('/') || '字段采集'})`;
}
if (e.kind === 'followup') {
return `[${e.pointId}] 追问(${e.followUpDimension})回答: ${e.transcript}`;
}
if (e.kind === 'followup_skipped') {
return `[${e.pointId}] 追问(${e.followUpDimension})回答: 记不清了(跳过)`;
}
const t = e.transcript === null ? '(沉默)' : `(讲述: ${e.transcript})`;
return `[${e.pointId}] 讲述${t} 情绪签名: ${e.emotionSignature ?? '未知'}`;
});
return `用户在互动剧情《苍梧疑冢》第一幕中的行为轨迹(按时间排序,含初刷与重玩轨迹):\n${lines.join('\n')}`;
}
/** Act 1 结束后的画像报告页:Jev 一次调用并行问 7 个维度 score */ /** Act 1 结束后的画像报告页:Jev 一次调用并行问 7 个维度 score */
export function ReportPage({ store, jev, onReplay }: Props) { export function ReportPage({ store, jev, onReplay }: Props) {
const [scores, setScores] = useState<DimensionScore[] | null>(null); const [scores, setScores] = useState<DimensionScore[] | null>(null);
@@ -48,35 +23,14 @@ export function ReportPage({ store, jev, onReplay }: Props) {
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
(async () => { (async () => {
const events = await store.list();
// 最近一次身份登记的称呼(旧数据缺 name 字段时回退空串 → 只显示编号)
const name = [...events].reverse().find((e) => e.kind === 'profile')?.profile?.name ?? '';
if (!cancelled) setPlayerName(name);
const state = buildState(events);
// 7 维 score 问题,criteria 为 10 级有序数组(索引 0-9 对应 1-10 级)
const criteria = Array.from({ length: 10 }, (_, i) => `${i + 1} 级`);
const questions = Object.fromEntries(
PROFILE_DIMENSIONS.map((d) => [
d.id,
{
type: 'score' as const,
instructions: `根据用户行为轨迹,评估其「${d.label}」维度水平:${d.description}`,
criteria,
},
]),
);
try { try {
const answers = await jev.ask(state, questions); const events = await store.list();
// 最近一次身份登记的称呼(旧数据缺 name 字段时回退空串 → 只显示编号)
const name = [...events].reverse().find((e) => e.kind === 'profile')?.profile?.name ?? '';
if (!cancelled) setPlayerName(name);
const result = await scoreProfile(events, jev);
if (cancelled) return; if (cancelled) return;
setScores( setScores(result);
PROFILE_DIMENSIONS.map((d) => {
const a = answers[d.id];
// API score 是 [0, criteria.length-1] 的概率加权浮点值,+1 映射到 1-10 级展示
return a?.type === 'score'
? { id: d.id, label: d.label, score: Math.round((a.score + 1) * 10) / 10, max: 10, confidence: a.confidence }
: { id: d.id, label: d.label, score: 0, max: 10, confidence: 0 };
}),
);
} catch (e) { } catch (e) {
if (!cancelled) setError(e instanceof Error ? e.message : String(e)); if (!cancelled) setError(e instanceof Error ? e.message : String(e));
} }
@@ -144,6 +98,7 @@ export function ReportPage({ store, jev, onReplay }: Props) {
</div> </div>
<span className="dimension-value"> <span className="dimension-value">
{s.score.toFixed(1)}/{s.max} · 置信度 {Math.round(s.confidence * 100)}% {s.score.toFixed(1)}/{s.max} · 置信度 {Math.round(s.confidence * 100)}%
{s.confidence < 0.6 && ' · 证据不足'}
</span> </span>
</div> </div>
))} ))}
+1 -12
View File
@@ -1,15 +1,4 @@
import type { DimensionTag } from '../../engine/types'; export { PROFILE_DIMENSIONS } from '../../engine/scoring';
/** 画像报告的 7 个实测维度(CONTEXT.md 实测框架) */
export const PROFILE_DIMENSIONS: { id: DimensionTag; label: string; description: string }[] = [
{ id: 'attachment', label: '依恋类型', description: '你在亲密与分离面前的姿态' },
{ id: 'conflict_style', label: '冲突处理风格', description: '分歧当前,你先做什么' },
{ id: 'values', label: '价值观与人生优先级', description: '两体面或两难堪之间,你让什么先走' },
{ id: 'emotional_stability', label: '情绪稳定性', description: '突发与悔恨面前的波形' },
{ id: 'risk', label: '风险偏好', description: '稳妥落点与精确落点之间的你' },
{ id: 'culture', label: '文化底蕴', description: '碑文、星象与礼制,实测而非自报' },
{ id: 'mate_preference', label: '择偶偏好', description: '你信任谁、把后背交给谁' },
];
/** 勋章映射:最高置信度维度 → 称号(写死的 MVP 映射表) */ /** 勋章映射:最高置信度维度 → 称号(写死的 MVP 映射表) */
export const BADGE_MAP: Record<string, { title: string; flavor: string }> = { export const BADGE_MAP: Record<string, { title: string; flavor: string }> = {