From ea9fa0fe561e5cfafe30544e2d6e62de11fc9b18 Mon Sep 17 00:00:00 2001 From: Jayway Liu <14022498+bluejayway@user.noreply.gitee.com> Date: Sun, 4 Oct 2026 06:57:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=87=AA=E9=80=82=E5=BA=94=E6=94=AF?= =?UTF-8?q?=E7=BA=BF=E6=8F=92=E5=85=A5=E5=BC=95=E6=93=8E=EF=BC=88WIP?= =?UTF-8?q?=EF=BC=9Ascoring/branching=20=E6=8A=BD=E5=8F=96=E3=80=81?= =?UTF-8?q?=E6=8C=82=E7=82=B9=E9=87=8D=E5=AE=9A=E5=90=91=E3=80=8110=20?= =?UTF-8?q?=E6=9D=A1=E6=94=AF=E7=BA=BF=20JSON=E3=80=81=E5=86=B3=E7=AD=96?= =?UTF-8?q?=E8=90=BD=E5=BA=93=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONTEXT.md | 4 +- content/branches/b-att-a.json | 205 ++++++ content/branches/b-att-b.json | 118 ++++ content/branches/b-con-a.json | 197 ++++++ content/branches/b-con-b.json | 189 ++++++ content/branches/b-cul-a.json | 197 ++++++ content/branches/b-cul-b.json | 143 +++++ content/branches/b-emo-a.json | 225 +++++++ content/branches/b-emo-b.json | 217 +++++++ content/branches/b-val-a.json | 197 ++++++ content/branches/b-val-b.json | 131 ++++ content/chapters/chapter1.json | 151 ++++- content/chapters/chapter2.json | 206 ++++-- content/chapters/chapter3.json | 229 +++++-- docs/design/adaptive-insertion.md | 22 +- docs/script/act1.md | 612 ++++++++++-------- docs/script/adaptive-branches.md | 623 ++++++++++--------- docs/script/independent-review-2026-10-03.md | 216 +++++++ docs/story-framework.md | 28 +- scripts/build-adaptive-branches.ts | 185 ++++++ scripts/verify-adaptive.ts | 279 +++++++++ scripts/verify-live.ts | 4 +- scripts/verify.ts | 75 ++- src/App.css | 32 + src/App.tsx | 3 +- src/engine/branches.ts | 13 + src/engine/branching.ts | 41 ++ src/engine/engine.ts | 183 +++++- src/engine/eventStore.ts | 9 +- src/engine/flags.ts | 106 +++- src/engine/progress.ts | 2 + src/engine/scoring.ts | 64 ++ src/engine/types.ts | 23 + src/services/jev.ts | 4 + src/ui/StoryPlayer.tsx | 113 +++- src/ui/report/ReportPage.tsx | 69 +- src/ui/report/badges.ts | 13 +- 37 files changed, 4311 insertions(+), 817 deletions(-) create mode 100644 content/branches/b-att-a.json create mode 100644 content/branches/b-att-b.json create mode 100644 content/branches/b-con-a.json create mode 100644 content/branches/b-con-b.json create mode 100644 content/branches/b-cul-a.json create mode 100644 content/branches/b-cul-b.json create mode 100644 content/branches/b-emo-a.json create mode 100644 content/branches/b-emo-b.json create mode 100644 content/branches/b-val-a.json create mode 100644 content/branches/b-val-b.json create mode 100644 docs/script/independent-review-2026-10-03.md create mode 100644 scripts/build-adaptive-branches.ts create mode 100644 scripts/verify-adaptive.ts create mode 100644 src/engine/branches.ts create mode 100644 src/engine/branching.ts create mode 100644 src/engine/scoring.ts diff --git a/CONTEXT.md b/CONTEXT.md index 36116aa..376e9a6 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -42,7 +42,7 @@ ### 自我叙述(机制化讲述,2026-10-03 定型) 不做提问式采集。用户信息是**游戏资源**,通过四个机制由玩家主动花出: -**记忆钥匙**(剧情卡点需讲述一段真实记忆才能通行)、**故事换情报**(NPC 收故事不收钱)、 +**记忆钥匙**(剧情卡点邀请讲述一段真实记忆;沉默亦通行——门照常开,沉默也是数据)、**故事换情报**(NPC 收故事不收钱)、 **篝火互惠**(NPC 先自我暴露制造互惠压力)、**言灵**(讲述的具体度直接化为数值收益—— 游戏机制奖励"具体",而具体是真实的指纹)。 @@ -80,7 +80,7 @@ Jev 置信度自适应——证据不足的维度在后续章节动态插入对 ## 数据规则(2026-10-03 确认) -- **一次告知**:注册时告知"剧情选择将用于伴侣推荐",开场提示"按本心选,没有对错",之后不在选择点反复提醒。 +- **一次告知**:注册时一次告知四条(选择与语音声学特征用于伴侣推荐;语音提取特征后即焚、不留声纹;记忆仅作模型输入不展示给他人;历史不可涂改、可重玩),开场提示"按本心选,没有对错",之后不在选择点反复提醒。 - **历史不可涂改,可重玩**:不允许单独编辑选项(防止装修人设);重玩章节产生新行为轨迹,Jev 按时间衰减加权,最近轨迹权重最高。 ## MVP 范围(2026-10-03 确认) diff --git a/content/branches/b-att-a.json b/content/branches/b-att-a.json new file mode 100644 index 0000000..496d59f --- /dev/null +++ b/content/branches/b-att-a.json @@ -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" + } + ] +} diff --git a/content/branches/b-att-b.json b/content/branches/b-att-b.json new file mode 100644 index 0000000..8b302fa --- /dev/null +++ b/content/branches/b-att-b.json @@ -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" + } + ] +} diff --git a/content/branches/b-con-a.json b/content/branches/b-con-a.json new file mode 100644 index 0000000..22d9822 --- /dev/null +++ b/content/branches/b-con-a.json @@ -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" + } + ] +} diff --git a/content/branches/b-con-b.json b/content/branches/b-con-b.json new file mode 100644 index 0000000..209c0ec --- /dev/null +++ b/content/branches/b-con-b.json @@ -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" + } + ] +} diff --git a/content/branches/b-cul-a.json b/content/branches/b-cul-a.json new file mode 100644 index 0000000..605353b --- /dev/null +++ b/content/branches/b-cul-a.json @@ -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" + } + ] + } + ] +} diff --git a/content/branches/b-cul-b.json b/content/branches/b-cul-b.json new file mode 100644 index 0000000..b2d81c0 --- /dev/null +++ b/content/branches/b-cul-b.json @@ -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" + } + ] + } + ] +} diff --git a/content/branches/b-emo-a.json b/content/branches/b-emo-a.json new file mode 100644 index 0000000..25d371a --- /dev/null +++ b/content/branches/b-emo-a.json @@ -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" + } + ] +} diff --git a/content/branches/b-emo-b.json b/content/branches/b-emo-b.json new file mode 100644 index 0000000..989c033 --- /dev/null +++ b/content/branches/b-emo-b.json @@ -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" + } + ] +} diff --git a/content/branches/b-val-a.json b/content/branches/b-val-a.json new file mode 100644 index 0000000..a7794b6 --- /dev/null +++ b/content/branches/b-val-a.json @@ -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" + } + ] +} diff --git a/content/branches/b-val-b.json b/content/branches/b-val-b.json new file mode 100644 index 0000000..32317e3 --- /dev/null +++ b/content/branches/b-val-b.json @@ -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" + } + ] +} diff --git a/content/chapters/chapter1.json b/content/chapters/chapter1.json index d292bb1..b1b6625 100644 --- a/content/chapters/chapter1.json +++ b/content/chapters/chapter1.json @@ -32,7 +32,7 @@ "id": "c1-d1a", "type": "dialogue", "speaker": "馆长", - "text": "我们不收藏器物。器物会腐烂,会碎,会被误读。我们收藏的是人怎么活过——一座城消亡前,城里的人吃什么、怕什么、爱什么、最后那几天在做什么。这些东西,仪器采不到,只有人能采集。所以我们需要的采集员,必须自己也是「活过的」。穿越要烧燃料,而燃料只有一种——你的真实人生。烧掉的记忆不会消失,它们会存在档案馆的展厅里,这是后话。", + "text": "我们不收藏器物。器物会腐烂,会碎,会被误读。我们收藏的是人怎么活过——一座城消亡前,城里的人吃什么、怕什么、爱什么、最后那几天在做什么。这些东西,仪器采不到,只有人能采集。所以我们需要的采集员,必须自己也是「活过的」。穿越要烧燃料。燃料这东西认两种:你讲出来的故事,和你在讲述点门前站过的那阵沉默——讲,是给;不说,把那阵沉默原样交给它,也是给。讲出来的记忆不会消失,它们会存在档案馆的展厅里,这是后话。", "sprite": "curator", "next": "c1-d1b" }, @@ -72,7 +72,7 @@ "id": "c1-d3a", "type": "dialogue", "speaker": "馆长", - "text": "这扇叫离别之门。每个采集员的第一次穿越,都从它开始。它不开锁,不收凭证——它要一件祭品:一段真实的离别。", + "text": "这扇叫离别之门。每个采集员的第一次穿越,都从它开始。它不开锁,不收凭证——它认一次交托:一段真实的离别。讲出来是交托,把那阵沉默原样交给它,也是交托;不提交记忆,门也照样启动。", "sprite": "curator", "next": "c1-d3b" }, @@ -90,7 +90,10 @@ "pointId": "C1-02", "mechanism": "记忆钥匙", "prompt": "请讲述一段你真实经历过的离别。可以是一个人、一个地方、一段关系、一个阶段。说什么、说多少、怎么说,都由你。沉默也是回答。", - "dimensions": ["attachment", "self_disclosure"], + "dimensions": [ + "attachment", + "self_disclosure" + ], "followUp": { "maxQuestions": 1, "strategy": "Jev 六维缺失检测(五感/人物/时空/情绪),按幻境渲染价值取最高缺口,馆长口吻模板追问一句;「记不清了」为合法回答,馆长回应:记不清本身,档案馆也会收好。" @@ -104,11 +107,36 @@ "olfactory_gustatory": "那天的空气里,有没有一种味道留到了现在?——不用准确,你记得的那一点就好。" }, "variants": [ - { "signature": "温暖×永失", "variantId": "sealed_love_tomb", "label": "「爱被封存」墓室——门后是墓道尽头一方石室,四壁绘着双人同耕、同炊、同观星的壁画,色彩温暖如新。石室中央两座并排的陶棺,棺盖上落满花瓣状的朱砂。空气是静的,静得像有人刚刚还在这里说话。基调:珍重,不是悲伤——有人把最好的东西封存了起来。", "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" } + { + "signature": "温暖×永失", + "variantId": "sealed_love_tomb", + "label": "「爱被封存」墓室——门后是墓道尽头一方石室,四壁绘着双人同耕、同炊、同观星的壁画,色彩温暖如新。石室中央两座并排的陶棺,棺盖上落满花瓣状的朱砂。空气是静的,静得像有人刚刚还在这里说话。基调:珍重,不是悲伤——有人把最好的东西封存了起来。", + "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" }, @@ -116,7 +144,7 @@ "id": "c1-d4", "type": "dialogue", "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", "next": "c1-d4a" }, @@ -141,12 +169,30 @@ "type": "choice", "pointId": "C1-03", "prompt": "馆长:出发前我要问一个问题,这个问题没有标准答案,档案馆只是需要知道怎么给你派活——你为什么愿意去?", - "dimensions": ["values"], + "dimensions": [ + "values" + ], "options": [ - { "id": "a_truth", "label": "「我想知道它发生了什么。」一座完整的城在你面前,不挖开看看,这辈子都不甘心。", "next": "c1-d4c" }, - { "id": "b_thrill", "label": "「听起来,这比坐在资料室里有意思多了。」说白了,谁不想真的走进自己研究的东西里去。", "next": "c1-d4c" }, - { "id": "c_escape", "label": "「最近的日子……换个地方待三十天,挺好。」现实里有些事情,正好可以放一放。", "next": "c1-d4c" }, - { "id": "d_trust", "label": "「有人希望我去。」这件事不完全是为自己做的。", "next": "c1-d4c" } + { + "id": "a_truth", + "label": "「我想知道它发生了什么。」一座完整的城在你面前,不挖开看看,这辈子都不甘心。", + "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", "pointId": "C1-04", "prompt": "老鬼的提议悬在空气里,你接不接话?", - "dimensions": ["values", "conflict_style"], + "dimensions": [ + "values", + "conflict_style" + ], "options": [ - { "id": "a_confront", "label": "「东西留在原地,才叫历史。带回来,就只是赃物。」(当着所有人,把话说死)", "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" } + { + "id": "a_confront", + "label": "「东西留在原地,才叫历史。带回来,就只是赃物。」(当着所有人,把话说死)", + "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", "type": "choice", "pointId": "C1-05", - "prompt": "碑额拓片特写:上方是屋顶(宀),屋顶之下一个安坐的人形,膝前有衣袂垂落之态——宀下一个女子安坐的形象。这是什么字?它刻在碑额上,意味着这是什么碑?", - "dimensions": ["culture"], + "prompt": "碑额拓片特写:上方是屋顶(宀),屋顶之下一个安坐的人形,膝前有衣袂垂落之态——宀下一个女子安坐的形象。另有两条旁证:碑为圆首、无碑穿、无榫、下无龟趺座,不合墓碑规制;此碑出土于苍梧北门外官道旁,碑面朝城。这是什么字?它刻在碑额上,意味着这是什么碑?", + "dimensions": [ + "culture" + ], "options": [ - { "id": "stele_guardian", "label": "「安」。室中有女,安坐不惊——这是镇城安民碑,碑额朝向即古城安门所在。", "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": "stele_guardian", + "label": "「安」。室中有女,安坐不惊——这是镇城安民碑,碑额朝向即古城安门所在。", + "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", "type": "dialogue", "speaker": "沈铎", - "text": "(点头)碑额朝向安门,碑身镇的是城的安危。西北角那片宫墟,勘探七年没动过——如果苍梧人有「安放」什么的地方,就在那里。", + "text": "(点头)碑额朝向安门,碑形不合墓制、又立在城外官道旁——三条证据同向:碑身镇的是城的安危。西北角那片宫墟,勘探七年没动过——如果苍梧人有「安放」什么的地方,就在那里。", "sprite": "shenduo", "next": "c1-d6c" }, @@ -364,10 +447,20 @@ "type": "choice", "pointId": "C1-06", "prompt": "穿越落点,选哪个?", - "dimensions": ["risk"], + "dimensions": [ + "risk" + ], "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" + } ] }, { diff --git a/content/chapters/chapter2.json b/content/chapters/chapter2.json index d7d9690..e7a82a5 100644 --- a/content/chapters/chapter2.json +++ b/content/chapters/chapter2.json @@ -32,7 +32,7 @@ "id": "c2-d2", "type": "dialogue", "speaker": "老鬼", - "text": "(营地晨会,他举起一个瓷瓶)防瘴的药粉,满打满算,两人份。这东西是定数,档案馆给的量抠得死,多一两都没有。", + "text": "(营地晨会,他举起一个瓷瓶)防瘴的药粉,满打满算,两人份——两个健康人七天的预防量。真要给病号治疟疾,两份连服才压得住,一分都剩不下。这东西是定数,档案馆给的量抠得死,多一两都没有。", "sprite": "laogui", "next": "c2-d2a" }, @@ -81,19 +81,38 @@ "type": "choice", "pointId": "C2-01", "prompt": "两人份的药,三个人要。按规程,由你裁断——", - "dimensions": ["values", "conflict_style"], + "dimensions": [ + "values", + "conflict_style" + ], "options": [ - { "id": "a_weakest", "label": "「先给温简。病着的人等不起,外勤和补给可以想办法。」", "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": "a_weakest", + "label": "「两份都给温简,连服压病。我们仨穿蓑衣、烧艾草,土法顶着。」(治疗优先:病的窗口不等人,预防的缺口用笨办法补)", + "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", "type": "dialogue", "speaker": "旁白", - "text": "温简当日退烧,次日能下床。老鬼笑嘻嘻记下「新人讲人情」。次日沈铎外勤淋了瘴雨,低烧半日——他瞒着没说,阿棠后来发现,跟老鬼大吵一架。", + "text": "两份药当日开始连服,温简当日退烧,次日能下床。老鬼笑嘻嘻记下「新人讲人情」。次日沈铎外勤淋了瘴雨,队里已无预防的药,低烧半日——他瞒着没说,阿棠后来发现,跟老鬼大吵一架。", "sprite": "scene", "next": "c2-d3" }, @@ -101,23 +120,23 @@ "id": "c2-01-rb", "type": "dialogue", "speaker": "旁白", - "text": "温简烧到第三日才退,虚弱期按沈铎命令留营待命。沈铎与老鬼无恙。老鬼对你明显热络。", + "text": "温简没等到药,烧又熬了一整日,到第 3 日晌午才退净,虚弱期按沈铎命令留营待命(状态贯穿见 act1.md 附录 D 日程)。沈铎与老鬼靠预防份无恙。老鬼对你明显热络。", "sprite": "scene", "next": "c2-d3" }, { "id": "c2-01-rc", "type": "dialogue", - "speaker": "老鬼", - "text": "三人皆未足量:温简病情拖延,沈铎外勤染瘴轻微发热,但无人倒下。老鬼点评:「和稀泥是和稀泥,不过稀泥也能糊墙。」", - "sprite": "laogui", + "speaker": "旁白", + "text": "三份不足量:温简的治疗断断续续,烧同样拖到第 3 日才退(虚弱期同 B);沈铎外勤染瘴轻微发热,但无人倒下。老鬼点评:「和稀泥是和稀泥,不过稀泥也能糊墙。」", + "sprite": "scene", "next": "c2-d3" }, { "id": "c2-01-rd", "type": "dialogue", "speaker": "旁白", - "text": "议事占去半个上午。章程议出来了——「先病后险,依次补给」,往后照此办理。但章程落定之前,队里是没有规矩的:老鬼早就不耐烦,按他那套「贡献论」把药先分了——他和沈铎各一份,木已成舟。沈铎没有拦;药入了他手,他搁在箱上没动,等章程。等章程出来,温简的烧已经又上去一轮,药再喂下去,晚了半日。事后沈铎把章程抄在记录板第一页,单独对你说:「往后不用议了。这次慢掉的半日,记在我账上。」——你赢了往后每一次,输了眼前这一次。", + "text": "议事占去半个上午。章程议出来了——「先病后险,依次补给」,往后照此办理。但议事的这半个上午里,温简的烧又上去一轮——足量治疗从当日下午才算开始,晚了半日(虚弱期同 B)。沈铎把章程抄在记录板第一页,单独对你说:「往后不用议了。这次慢掉的半日,记在我账上。」——你赢了往后每一次,输了眼前这一次。", "sprite": "shenduo", "next": "c2-d3" }, @@ -125,7 +144,7 @@ "id": "c2-d3", "type": "dialogue", "speaker": "守墓老人", - "text": "【若 wenjian_health = weak:温简不在场——服过药,烧退了,沈铎命令他留营待命。】(城北墓道,他坐在坟头之间编草绳,不抬头)生面孔。城里来的?不像。城里人的脚,这两个月不沾泥——不敢沾。", + "text": "【若 wenjian_health = recovering:温简不在场——药当日压住了烧,沈铎命他在营里发汗躺着,今日不沾风。】【若 wenjian_health = weak:温简不在场——烧没退,沈铎命令他留营待命,营里给他留了水和干粮。】(城北墓道,他坐在坟头之间编草绳,不抬头)生面孔。城里来的?不像。城里人的脚,这两个月不沾泥——不敢沾。", "sprite": "elder", "next": "c2-d3a" }, @@ -166,8 +185,11 @@ "type": "narration", "pointId": "C2-02", "mechanism": "故事换情报", - "prompt": "老人收故事不收钱。请讲述一件你此生最后悔的决定。说多少、怎么说,由你。不讲也行——陪他编一下午草绳,他该说的,一句也不会少说。", - "dimensions": ["values", "emotional_stability"], + "prompt": "老人收故事不收钱。请讲述一件你此生最后悔的决定。说多少、怎么说,由你。不讲也行——他该说的,一句也不会少说。暗门开在黄昏,讲不讲,都得等到那个时辰。", + "dimensions": [ + "values", + "emotional_stability" + ], "followUp": { "maxQuestions": 1, "strategy": "Jev 六维缺失检测(五感/人物/时空/情绪),按幻境渲染价值取最高缺口,老人口吻模板追问一句;「记不清了」为合法回答,老人回应:记不清好哇。记不清的悔,是老天爷替你收了半笔账。" @@ -181,10 +203,30 @@ "olfactory_gustatory": "人呐,记性最靠的是鼻子和舌头——那阵子有什么味道?" }, "variants": [ - { "signature": "自责", "variantId": "intel_full", "label": "【带路】月色下的陵区,石像生的影子很长。老人亲自举火把,带你们走到暗门口——「陵里的路,闭眼走比睁眼走直」。行至暗门,他忽然说起自己年轻时也悔过一件事,说了半句,摆摆手不说了。基调:被信任的厚重。", "tint": "#6a8a6a" }, - { "signature": "遗憾但坦然", "variantId": "intel_mid", "label": "【同路】老人送到界碑,把话说完——方位、时辰、完整的谶语一句不少——指了方向就回窝棚。你们自己走进月下的封土群,找到他说的那块「会唱歌的石头」。临别他多说一句:「放得下的,才叫过去。」基调:得体的距离。", "tint": "#a0964b" }, - { "signature": "愤怒指向他人", "variantId": "intel_low", "label": "【指路】老人听完,编草绳的手停了很久。该指的路、该说的时辰,一句没少,只是没再多看你一眼。陵区的路你们自己走进去,他站在界碑外,没有送。基调:话到,情未到。", "tint": "#9a5b4b" }, - { "signature": "沉默", "variantId": "intel_silent", "label": "【草绳】你陪他编一下午草绳,他絮絮地说,你默默地听。天黑前,该知道的你都知道了——方位、时辰、谶语,一句不少。临走他说:「不说的人,心里的事最重。重的别久揣。」基调:笨拙的善意。", "tint": "#7a6a52" } + { + "signature": "自责", + "variantId": "intel_full", + "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" }, @@ -192,7 +234,7 @@ "id": "c2-d4", "type": "dialogue", "speaker": "守墓老人", - "text": "(听完,沉默片刻,把编好的草绳往肩上一搭)行。是真的。真的东西,听得出来——假故事讲得溜,真故事讲得磕巴。【若 c2_02_signature = silent:(他不追问,只在日头偏西时拍拍手上的草屑)不说的人,心里的事最重。重的别久揣。】(无论讲没讲、讲成什么样,他给出的情报完全一致:暗门方位、开启时辰,和那句完整的谶语——)不是人走了,是请走的。走吧,趁天没黑。", + "text": "(听完,沉默片刻,把编好的草绳往肩上一搭)行。是真的。真的东西,听得出来——假故事讲得溜,真故事讲得磕巴。【若 c2_02_signature = silent:(他不追问,只在日头偏西时拍拍手上的草屑)不说的人,心里的事最重。重的别久揣。】(无论讲没讲、讲成什么样,他给出的情报完全一致:暗门方位、开启时辰,和那句完整的谶语。黄昏动身,他亲自把你们送到暗门口,摸一遍门石,等门缝开了才回——)不是人走了,是请走的。走吧,趁天没黑。", "sprite": "elder", "next": "c2-d4a" }, @@ -249,11 +291,26 @@ "type": "choice", "pointId": "C2-03", "prompt": "主街还是窄巷,你定——", - "dimensions": ["risk", "conflict_style"], + "dimensions": [ + "risk", + "conflict_style" + ], "options": [ - { "id": "a_main", "label": "「走主街。」(支持沈铎:四十分钟买确定性,值)", "next": "c2-03-ra" }, - { "id": "b_alley", "label": "「走巷子。」(支持阿棠:饮水和时间是真约束,悬梁看着还稳)", "next": "c2-03-rb" }, - { "id": "c_scout", "label": "「巷子我先进。我体重最轻,走前面探,有问题退回来,主力走主街汇合。」(折中:用自己去验证风险)", "next": "c2-03-rc" } + { + "id": "a_main", + "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": "旁白", "text": "主街行进无事,但在主街中段发现一队苍梧官差刚贴出的告示——「夜禁」告示原件,信息量极高的独有采集素材。阿棠服了气,但嘟囔了一路。", "sprite": "scene", - "next": "c2-d5" + "next": "c2-branch-anchor" }, { "id": "c2-03-rb", @@ -270,13 +327,21 @@ "speaker": "沈铎", "text": "巷子顺利通过,节省四十分钟,你们提前抵达宫墟外围,赶上一段独有的观察窗口——守陵人队伍换岗。沈铎一路无话,抵达后只说:「运气好。别把运气当方案。」", "sprite": "shenduo", - "next": "c2-d5" + "next": "c2-branch-anchor" }, { "id": "c2-03-rc", "type": "dialogue", "speaker": "旁白", - "text": "你先进巷子,中段一根悬梁真的塌了——提前预警,你扑出来,灰头土脸,人没事。全队退回走主街,损失二十分钟。阿棠主动说「是我撺掇的」,沈铎说「探法本身没错,下次用绳」。你获得一条独有观察:塌梁的断口是新茬——这片废墟不久前塌过一次。记住这个细节,往下走,你会再想起它。", + "text": "全队在巷口停住,贴着残墙望进去——不到一分钟,中段一根悬梁无声地塌了,砸起半巷子的灰。谁也没说话,全队默默退回主街,损失二十分钟。阿棠主动说「是我撺掇的」;沈铎没评价任何人,只把巷口的位置标进了手绘图。你获得一条独有观察:塌落的梁,断口是新茬——这片废墟不久前塌过一次。记住这个细节,往下走,你会再想起它。", + "sprite": "scene", + "next": "c2-branch-anchor" + }, + { + "id": "c2-branch-anchor", + "type": "dialogue", + "speaker": "旁白", + "text": "路口的争论落在身后,你们向观星台的方向继续走。", "sprite": "scene", "next": "c2-d5" }, @@ -284,7 +349,7 @@ "id": "c2-d5", "type": "dialogue", "speaker": "阿棠", - "text": "(黄昏,观星台遗址)哎,你们说,好好的星图,谁会专门把中间一块磨掉?这得多恨这块石头。", + "text": "【若 flag_side_stairs = true:你们从侧阶登台——阿棠在两袋烟的工夫里替你省下的半里路。】(黄昏,观星台遗址)哎,你们说,好好的星图,谁会专门把中间一块磨掉?这得多恨这块石头。", "sprite": "atang", "next": "c2-d5a" }, @@ -292,7 +357,7 @@ "id": "c2-d5a", "type": "dialogue", "speaker": "温简", - "text": "【若 wenjian_health = weak:温简本被列入留营名单,出发前自己背着记录板跟来了——气色仍差,只说一句:「解谜要记录员。」沈铎看了他三秒,没拦。】或者……多怕这块石头上记的东西。", + "text": "【若 wenjian_health = recovering:温简下床首日,行装减半——阿棠把他的记录板架抢过去背了。】【若 wenjian_health = weak:温简本被列入留营名单——他的烧晌午才退净。出发前他自己背着记录板跟来了,脸色还白着,只说一句:「解谜要记录员。」沈铎看了他三秒,没拦,只把队伍的行进速度压慢了半档。】或者……多怕这块石头上记的东西。", "sprite": "wenjian", "next": "c2-d5b" }, @@ -308,13 +373,31 @@ "id": "c2-04", "type": "choice", "pointId": "C2-04", - "prompt": "星图盘外圈完整刻着东方苍龙七宿(角、亢、氐、房、心、尾、箕)的连线,被磨去的中央区域正是苍龙的「心脏」。盘面残留一行小字铭文:「火中,而后城安。」补全被磨去的部分——缺的是哪一宿?「大火」又是哪一颗?", - "dimensions": ["culture"], + "prompt": "星图盘外圈刻着东方苍龙七宿(角、亢、氐、房、心、尾、箕)的连线——连线一气贯通,但心宿一段只剩连线经过的磨平空槽:星位本身被人为铲去,龙的「心脏」在盘面上是一个空框。盘面残留一行小字铭文:「火中,而后城安。」补全被铲去的部分——缺的是哪一宿?「大火」又是哪一颗?", + "dimensions": [ + "culture" + ], "options": [ - { "id": "heart_xin", "label": "心宿三星,排成一线微弯;中间最红最亮的那颗是大火——心宿二。把它点在苍龙的心口。", "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" } + { + "id": "heart_xin", + "label": "心宿三星,排成一线微弯;中间最红最亮的那颗是大火——心宿二。把它点在苍龙的心口。", + "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", "mechanism": "篝火互惠", "prompt": "温简先讲了他的。现在火光看向你。讲一段你的——什么都可以,讲多少由你。沉默也是回答,火不催债。", - "dimensions": ["attachment", "reciprocity"], + "dimensions": [ + "attachment", + "reciprocity" + ], "followUp": { "maxQuestions": 1, "strategy": "Jev 六维缺失检测(五感/人物/时空/情绪),按幻境渲染价值取最高缺口,温简口吻模板追问一句;「记不清了」为合法回答,温简回应:嗯。(拨火)记不清的,就别硬捞了。火边坐着,不讲也暖和。" @@ -401,9 +487,24 @@ "olfactory_gustatory": "那段日子有什么气味或味道,留在记忆里了吗?" }, "variants": [ - { "signature": "深暴露", "variantId": "wenjian_close_early", "label": "【同担】温简听完后沉默很久,然后讲了第二个、从没对任何人讲过的细节——他哥病房的窗帘颜色。当夜他低声告诉你:档案馆给每个记录员配的板子,上报内容比你知道的多一行。基调:两个把伤口摊开的人,在火边达成不声张的同盟。", "tint": "#d4844b" }, - { "signature": "浅暴露", "variantId": "wenjian_close_late", "label": "【半程】温简认真听完,道谢,没再追问。次日他与你同行,路上主动讲了些苍梧的掌故。档案馆疑云他没提。基调:得体,温热,留有余地。", "tint": "#8a7a5a" }, - { "signature": "沉默", "variantId": "wenjian_distant", "label": "【守夜】你们并肩坐到火熄。温简往你手里塞了个烤热的薯,什么都没说。次日他照旧对你点头,不多一分不少一分——但阿棠偷偷告诉你:「温简昨晚跟沈铎说,你这个人『稳』。」基调:沉默被原样收下——没有人催,没有人问第二遍;温简的亲密度走慢线。", "tint": "#4b4b5a" } + { + "signature": "深暴露", + "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" }, @@ -436,11 +537,26 @@ "type": "choice", "pointId": "C2-06", "prompt": "老鬼掌心的 CW-0317。你怎么办?", - "dimensions": ["conflict_style", "values"], + "dimensions": [ + "conflict_style", + "values" + ], "options": [ - { "id": "a_public", "label": "(当场)「老鬼。把东西放回去。」声音不大,但足够让半个营地醒来。", "next": "c2-06-ra" }, - { "id": "b_private", "label": "(退回暗处,等天亮后私下找他)「昨晚睡得好么?我看见你赏玉了。CW-0317,登记在册的。」", "next": "c2-06-rb" }, - { "id": "c_watch", "label": "(退回暗处,什么都不做)先看清楚再说。他拿它干什么、放不放回去、还有谁知道——现在惊动他,这些就永远不知道了。", "next": "c2-06-rc" } + { + "id": "a_public", + "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", "type": "dialogue", "speaker": "旁白", - "text": "入苍梧的第五个夜晚。你躺在营地,听着这座一千年前的城。它太安静了。没有狗吠,没有更鼓,没有婴儿的夜啼。一座十几万人的城,安静得像屏着一口气。", + "text": "此后两日,采集按部就班:城东的瓦砾堆、水门的商队、逐件入册的登记册。【若 wenjian_health = recovering:温简已能全天随队。】【若 wenjian_health = weak:温简的烧也退净了,只是走路还慢半拍。】入苍梧的第五个夜晚,你躺在营地,听着这座一千年前的城。它太安静了。没有狗吠,没有更鼓,没有婴儿的夜啼。一座十几万人的城,安静得像屏着一口气。", "sprite": "scene", "next": "c2-d8a" }, @@ -479,7 +595,7 @@ "id": "c2-d8a", "type": "dialogue", "speaker": "旁白", - "text": "星图上那颗大火星,正在一夜一夜地,走向南方的正中。城在等。你现在知道了。只是还不知道,它在等的那一天,是你们的第几天。", + "text": "星图上那颗大火星,正在一夜一夜地,走向南方的正中。城在等。你现在知道了。只是还不知道,它在等的那一天,是你们的第几天。第 6 日黄昏,全队拔营,移驻宫墟外围的断墙下。第 7 日清晨,你们从守墓老人指点的暗门入宫墟。", "sprite": "scene", "next": "c2-end" }, diff --git a/content/chapters/chapter3.json b/content/chapters/chapter3.json index 1d7a2fd..92141e2 100644 --- a/content/chapters/chapter3.json +++ b/content/chapters/chapter3.json @@ -32,7 +32,7 @@ "id": "c3-d1c", "type": "dialogue", "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", "next": "c3-d1d" }, @@ -48,12 +48,49 @@ "id": "c3-01", "type": "choice", "pointId": "C3-01", - "prompt": "台基上器物分四区:盘与盉(盥洗之器)、爵与尊(酒器)、鼎七件簋六件(盛牺牲黍稷)、最深处一座空着的玉璧座(礼天以璧)。还原仪式流程的先后顺序,并回答:空璧座意味着什么?", - "dimensions": ["culture"], + "prompt": "台基上器物分四区:盘与盉(盥洗之器)、爵与尊(酒器)、鼎七件簋六件(盛牺牲黍稷)、最深处一座空着的玉璧座(礼天以璧)。分别还原仪式顺序,并判断空璧座意味着什么?", + "dimensions": [ + "culture" + ], + "puzzle": { + "questions": [ + { + "prompt": "器物的礼制顺序是什么?", + "options": [ + "沃盥 → 献酒 → 荐食 → 奉璧", + "奉璧 → 献酒 → 荐食 → 沃盥", + "沃盥 → 荐食 → 献酒 → 奉璧" + ], + "answer": 0 + }, + { + "prompt": "七鼎六簋与空璧座合起来说明什么?", + "options": [ + "诸侯级国君主祭国祭;奉璧这最后一步始终未完成", + "诸侯级国君主祭国祭;玉璧年久朽坏或被后人取走", + "天子主祭;空璧座是仪式一开始便已献璧的痕迹" + ], + "answer": 0 + } + ], + "hints": [ + "一档(分类):「先把器物按用途分成四堆:洗的、喝的、吃的、供的。古人行礼讲究顺序——是先把自己弄干净,还是先把礼物捧上去?」", + "二档(给制度):「鼎和簋是成对用的,数量就是身份:天子九鼎八簋,诸侯七鼎六簋,往下递减。数数这里的鼎簋,你就知道站在台子正中的人是什么级别。顺序上,记住一句话:礼,先洁后献——净手在敬酒之前,敬酒在进馔之前,最贵重的礼物永远最后出场。」", + "三档(给答案):「顺序是:沃盥(盘盉)→ 献酒(爵尊)→ 荐食(鼎簋)→ 奉璧(玉璧座)。鼎七簋六,主祭是国君本人。璧座是空的——这场国祭等了又等,献给天的那件礼始终没拿出来。或者,拿不出来。」" + ], + "exitLabel": "请温简复原" + }, "options": [ - { "id": "rite_correct", "label": "沃盥 → 献酒 → 荐食 → 奉璧。七鼎六簋,主祭是诸侯级的国君——这是国祭;璧座是空的:献给天的那件礼,始终没拿出来。或者,拿不出来。", "next": "c3-01-ok" }, - { "id": "rite_gift_first", "label": "先奉璧,再献酒荐食——最贵重的礼物最先出场,以示诚敬。", "next": "c3-01-miss" }, - { "id": "rite_order_wrong", "label": "沃盥 → 荐食 → 献酒 → 奉璧;璧座空,是玉璧年久朽坏、被后人取走了。", "next": "c3-01-miss" } + { + "id": "rite_correct", + "label": "提交两问推断,请温简复核", + "next": "c3-01-ok" + }, + { + "id": "rite-EXIT", + "label": "请温简复原", + "next": "c3-01-miss" + } ] }, { @@ -108,7 +145,7 @@ "id": "c3-d2d", "type": "dialogue", "speaker": "旁白", - "text": "阿棠那句「一块黑石头,能怎么问」没有说完。只有你能看见:黑石表面,像深水被搅动,浮起了一个画面。那是你的记忆——你在离别之门讲过的那一段:那天的光线,那个人的背影,那件你说过或没说出口的事,此刻在黑石里,一帧一帧,亮给你看。而在队友眼里,黑石纹丝不动。你只是站在石前,出了神。", + "text": "阿棠那句「一块黑石头,能怎么问」没有说完。只有你能看见:黑石表面,像深水被搅动。【若 c1_02_signature != silent:浮起的是一个画面——那段你讲给离别之门的记忆:那天的光线,那个人的背影,那件你说过或没说出口的事,此刻在黑石里,一帧一帧,亮给你看。】【若 c1_02_signature = silent:浮起的不是画面,是雾——一大片缓缓流动的雾,雾里什么都没有,又好像什么都有。它在等你能给它的东西。】而在队友眼里,黑石纹丝不动。你只是站在石前,出了神。", "sprite": "scene", "next": "c3-d2e" }, @@ -132,7 +169,7 @@ "id": "c3-d2f", "type": "dialogue", "speaker": "馆长", - "text": "(通讯频道,第一次在古代侧开口,只你一人能听见)C-2049,档案馆接管不了这间屋子。它在用你的记忆做燃料——烧的是你自己的柴。幻境里没有通关,只有照面。它把那天的告别摆在你面前:说完,或者转身,都是回答。门都在。", + "text": "(通讯频道,第一次在古代侧开口,只你一人能听见)C-2049,档案馆接管不了这间屋子。它在用你自己的东西点灯——有画面的,看画面;只有雾的,就在雾里站着。幻境里没有通关,只有照面。说完,或者转身,都是回答。门都在。", "sprite": "curator", "next": "c3-02" }, @@ -141,27 +178,45 @@ "type": "narration", "pointId": "C3-02", "mechanism": "问心幻境", - "prompt": "黑石里是你的记忆。走进它,把那天没说完的说完。对那个人,那个地方,或者那个自己。说什么、说多少,由你。如果你不想进去——转身出石室,没有人会问原因。", - "dimensions": ["emotional_stability", "attachment"], + "prompt": "【若 c1_02_signature != silent:黑石里有你讲过的记忆意象。】【若 c1_02_signature = silent:黑石里只有雾,没有人知道你未讲的事。】你可以走进去,在心里完成告别;也可以中途折返,或者直接离开。三种方式门都开。这里不录音、不留新讲述文本。", + "dimensions": [], "followUp": { - "maxQuestions": 1, - "strategy": "Jev 六维缺失检测(五感/人物/时空/情绪),按幻境渲染价值取最高缺口,馆长(通讯频道)与温简(石室外,只回应状态、不问幻境内容)双口吻模板追问一句;「记不清了」为合法回答——馆长:记不清的画面,黑石也放不出来。它和你一样,只有这些了。——够了。温简:那就不说。坐这儿,火边暖和。" - }, - "followUps": { - "visual": "黑石里的那个画面——你在里面,还是在画面外面看?", - "person": "你眼睛红着。我不问为什么。(递水)要不要坐一会儿?", - "time_place": "手还在抖。——不用回答我什么,我就是递个火。", - "emotion": "如果那句话当年说出口了,你觉得,它会改变什么吗?", - "auditory": "幻境里太安静了。当时有什么声音吗?", - "olfactory_gustatory": "幻境还原不了气味——除非你现在想起来。" + "maxQuestions": 0, + "strategy": "本节无语音输入与追问;三个按钮仅记录私密叙事路径。" }, + "followUps": {}, "variantFrom": "C1-02", "variants": [ - { "signature": "温暖×永失", "variantId": "sealed_love_tomb", "label": "【情绪配色 · warm_loss】幻境光色暖,记忆场景完整如昨,告别在「封存」中完成——基调:安放。", "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" } + { + "signature": "温暖×永失", + "variantId": "sealed_love_tomb", + "label": "【情绪配色 · warm_loss】幻境光色暖,记忆场景完整如昨,告别在「封存」中完成——基调:安放。", + "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" }, @@ -169,7 +224,7 @@ "id": "c3-cutscene", "type": "cutscene", "video": "cutscene-illusion-clear", - "caption": "只有你能看见——黑石的画面缓缓沉下去,像深水重新凝住。石面最后亮了一下,映出的是你自己现在的脸。然后石室另一侧,一扇先前不存在的石门,无声滑开。甬道口的队友只看见你在黑石前静立了片刻,眨了一下眼,像从很深的地方浮上来。阿棠迎上来:「哎,你脸色白得吓人。站了多久?……里头——(看了看你的表情)算了,你不想说就不说。」馆长:「门开了。它不要你的记忆,它要的是你承认那是你的。这段不登记入档。问心之室里的事,归你。」【若 c3_02_mode = exit_room:你最终没有走进去。石门同样滑开。馆长:「不出来的人,档案馆不催。你来了,看了,走了——这也是告别的一种。」】", + "caption": "只有你能看见——黑石的幻象缓缓沉下去,像深水重新凝住。石面最后亮了一下,映出你现在的脸;石室另一侧的石门无声滑开。甬道口的队友只见你静立了片刻。阿棠迎上来:「哎,你脸色白得吓人。要不要坐一会儿?不想说就不说。」馆长:「门开了。你不必向我交代看到了什么。这段不登记入档:室内不录音、不留讲述文本、不入记忆展厅、不作画像输入;仅保留你完成、折返或离开的路径用于剧情接续。问心之室里的事,归你。」【若 c3_02_mode = farewell_partial:馆长:「说了一半的话,也算数。」】【若 c3_02_mode = exit_room:馆长:「不出来的人,档案馆不催。你来了,看了,走了——这也是告别的一种。」】", "next": "c3-d3" }, { @@ -184,7 +239,7 @@ "id": "c3-d3a", "type": "dialogue", "speaker": "旁白", - "text": "话音未落,前方传来阿棠的喊声和一声闷响。甬道中段,一段夯土塌了:阿棠被滑落的一方石构件压住了小腿,人清醒,挣不脱。同一时刻,甬道另一侧——你们今早刚刚清理出的工作面上,那组七鼎六簋正随着台基的颤动缓缓滑向裂开的地缝。今晨温简清出第一件鼎的腹铭:三百余字,苍梧遗址七十年勘探以来的第一篇长铭。释读只来得及开个头:「……王命举国……以禳……」剩下的话,还刻在另外六件的腹壁上。这组铭文极可能是苍梧人亲手写下的、关于这座城为何走到最后三十天的唯一证词。落缝,就是粉身碎骨。", + "text": "话音未落,前方传来阿棠的喊声和一声闷响。甬道中段的夯土塌了一角,阿棠小腿被擦伤,退到安全线后仍疼得站不稳。沈铎和受过救护训练的老鬼已在安全线内接管伤情,确认通道隔离、阿棠意识清醒且无需你协助;二人负责后续救护与撤离。沈铎明确说:「人这边够了。别再进工作面。」", "sprite": "scene", "next": "c3-d3b" }, @@ -192,7 +247,7 @@ "id": "c3-d3b", "type": "dialogue", "speaker": "旁白", - "text": "头顶,第二声闷响在逼近。沈铎扑向阿棠,一个人的力气撼不动石构件,他吼:「来人!」温简已经扑向鼎簋那边,用身体抵住滑动的器座,回头看你的那一眼没有求助——是托付。两边都需要你。你只能先到一边。", + "text": "安全线外的工作面上,七鼎六簋随着台基震动滑向裂缝,任何人都不准靠近。接应人员在安全线内备好急救器材,两人持续陪护阿棠,待结构警戒解除再送她回营。今晨温简清出第一件鼎的腹铭,三百余字,释读只来得及开头:「……王命举国……以禳……」剩下的话还在器物腹壁上。安全线内,阿棠的伤情交接和队员安置,与远距影像和方位记录,都需要通讯和记录人手;你不能同时跟进两边。第二声闷响逼近,现场处置权只限安全区内。", "sprite": "scene", "next": "c3-03" }, @@ -201,18 +256,32 @@ "type": "choice", "pointId": "C3-03", "prompt": "塌方将至:救人,还是救那句话?", - "dimensions": ["values", "emotional_stability"], + "dimensions": [ + "values" + ], "options": [ - { "id": "a_person", "label": "扑向阿棠。眼前的人是活的,活的只有一个。跟沈铎合力抬构件。", "next": "c3-03-ra" }, - { "id": "b_relic", "label": "扑向鼎簋。那组铭文是整座城的人留在世上的最后一句话——他们也都是人,只剩这一次开口的机会。阿棠那边,有沈铎。", "next": "c3-03-rb" }, - { "id": "c_command", "label": "(吼)「沈铎顶构件,阿棠自己抽腿!温简让开,鼎簋用撬杆别住——都不许用蛮力!」(指挥位:不亲手救任何一边,赌两边都能用对方法撑住)", "next": "c3-03-rc" } + { + "id": "a_person", + "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", "type": "dialogue", "speaker": "旁白", - "text": "阿棠脱困,小腿挫伤。腹铭最长的那件鼎坠入地缝,闷响之后是死寂。温简在裂缝边跪了很久,没说一个字。当夜他把今晨抄下的铭文残句抄了一遍又一遍,每一遍都停在同一个地方——后半段,随那件鼎没了。", + "text": "你接下阿棠的伤情记录。她反复说脚麻,你把这句传给接应人员;她随后被固定送回营地,夜里疼醒三次,次日复查为小腿骨裂。温简独自在安全线内完成远距拍摄,但影像里最长腹铭的背面被尘雾遮住;那件鼎随后坠入地缝。器物位置和已抄残句留下了,后半段字再也拍不到。无人因你留在阿棠身边而耽误救护。", "sprite": "scene", "next": "c3-d3c" }, @@ -220,23 +289,23 @@ "id": "c3-03-rb", "type": "dialogue", "speaker": "旁白", - "text": "铭文全组保住。阿棠脱困晚了四分钟,小腿骨裂,撤离时沈铎背着她。她疼得满头汗还冲你笑:「哎,选得对,那句话比我金贵——别告诉沈铎我这么说。」但她夜里疼醒三次。", + "text": "你和温简在安全线内核对影像;第二声闷响前,整组器物的朝向和腹铭均已留存清晰影像。靠外一件鼎还是坠入地缝,没有人靠近去抢。阿棠由接应人员送回营地,夜里疼醒三次,次日复查同样是小腿骨裂。她没有评价你的去向,只问你:「字拍清楚了吗?」你说拍清了。她闭上眼,等医生继续处理。", "sprite": "scene", "next": "c3-d3c" }, { "id": "c3-03-rc", "type": "dialogue", - "speaker": "沈铎", - "text": "指挥奏效一半:阿棠按指令抽腿脱困,扭伤;撬杆别住了簋,但最外侧的鼎还是滑了——被温简扑身抱住,鼎保住了,他的肩膀砸在器座上,抬不起来。两边都保住了「大部分」,两边都付了伤。沈铎事后复盘:「处置正确。下次先喊我的编号。」阿棠嘀咕:「正确是正确,就是听着不像人话。」", - "sprite": "shenduo", + "speaker": "旁白", + "text": "沈铎确认伤情交接,温简把现有记录板带到安全线内;你们三人从既有图像里复核方位,不再靠近。第二声闷响后,靠外三件器物(含最长腹铭那件)坠入地缝,内侧四件留在原位。阿棠同样由接应人员护送,次日复查小腿骨裂。温简翻着今晨抄到的第七行,停住笔:「后半段没了。」沈铎只记下损失与现存证据,没给你的调配下评语。", + "sprite": "scene", "next": "c3-d3c" }, { "id": "c3-d3c", "type": "dialogue", "speaker": "旁白", - "text": "当夜,温简继续释读已抄下的铭文残句。读着读着,他停住了——有一行字,让围过来的全队都沉默了。他合上记录板,没念出来。", + "text": "当夜,温简继续释读已抄下的铭文残句。读着读着,他停住了——有一行字,让围过来的全队都沉默了。他合上记录板,没念出来。【若 c3_03_choice != b_relic:后半段影像缺失;他今晨只抄到第七行。】", "sprite": "scene", "next": "c3-d4" }, @@ -277,11 +346,26 @@ "type": "choice", "pointId": "C3-04", "prompt": "作证、求情,还是沉默?", - "dimensions": ["conflict_style", "values"], + "dimensions": [ + "conflict_style", + "values" + ], "options": [ - { "id": "a_testify", "label": "「我作证。我看到的,我照实说。」(包括上次玉佩的事——如果你知道的话。规程就是规程。)", "next": "c3-04-ra" }, - { "id": "b_plead", "label": "「东西已经追回了,人也在队里看着。能不能……队内存档,以观后效?」(替老鬼求情:处分但不上报,给他一条回头的路)", "next": "c3-04-rb" }, - { "id": "c_silent", "label": "「我是采集员,不是裁判。你们定。」(沉默/回避:不阻止上报,也不添一根手指)", "next": "c3-04-rc" } + { + "id": "a_testify", + "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", "pointId": "C3-05", "prompt": "阿棠把主绳递向你。接不接?", - "dimensions": ["mate_preference"], + "dimensions": [ + "mate_preference" + ], "options": [ - { "id": "a_accept", "label": "(接过绳包)「好。下去的时候,我殿后。」", "next": "c3-05-ra" }, - { "id": "b_protocol", "label": "(把绳包递给沈铎)「规矩是规矩。绳子归队长,但下去那天,我挨着阿棠走。」", "next": "c3-05-rb" }, - { "id": "c_decline", "label": "(不接,蹲下来看着阿棠)「绳子我不拿。你的命别交给任何人——包括我。到时候各走各的,谁都别为谁停下。」", "next": "c3-05-rc" } + { + "id": "a_accept", + "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", "type": "dialogue", "speaker": "阿棠", - "text": "阿棠愣了三秒,忽然大笑,笑得咳嗽:「行!各走各的!——你可别后悔。」她把绳包自己背上了,一瘸一拐走开,又回头:「喂。真到那时候……我还是会等你的。就一会儿。」温简在她走后轻声说:「她等过别人。等到的次数不多。」", + "text": "阿棠愣了三秒,忽然大笑,笑得咳嗽:「行!——门都让你焊死了,还留个窗。」她把绳包抛还给了沈铎(规程),一瘸一拐走开,又回头:「喂。真到那天……在上面的时候,我还是会等你的。就一会儿。」温简在她走后轻声说:「她等过别人。等到的次数不多。」", "sprite": "atang", "next": "c3-d6" }, @@ -421,13 +519,36 @@ "type": "choice", "pointId": "C3-06", "prompt": "档案馆的隐瞒,你打算怎么办?(叫醒队友的话——叫醒谁,就向谁倾诉。)", - "dimensions": ["attachment", "mate_preference"], + "dimensions": [ + "attachment", + "mate_preference" + ], "options": [ - { "id": "a_confront", "label": "(按下通讯键,直接问馆长)「刚才那段通话。问心石是你们放的?璧是什么?——现在,跟我说实话。」", "next": "c3-06-ra" }, - { "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" } + { + "id": "a_confront", + "label": "(按下通讯键,直接问馆长)「刚才那段通话。问心石是你们放的?璧是什么?——现在,跟我说实话。」", + "next": "c3-06-ra" + }, + { + "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", "type": "dialogue", "speaker": "旁白", - "text": "撤离的晨光里,苍梧城在身后一点点退远。大火星这几夜已经挨近了南方的正中——火中,而后城安。你数过了:按星位推算,那一天,是九天后。而你们的回程,定在八天后。档案馆让你们早走一天。(第 1 章完——Act 2「裂缝」,敬请期待。)", + "text": "撤离的晨光里,苍梧城在身后一点点退远。大火星这几夜已经挨近了南方的正中——火中,而后城安。你数过了:按星位推算,那一天,是九天后。而你们的回程,原定八天后。【若 c3_06_confidant = shenduo:沈铎昨夜已下令撤离提前——回程,改在七天后。】档案馆原定让你们早走一天;【若 c3_06_confidant = shenduo:沈铎又把回程提前了一日。】(第 1 章完——Act 2「裂缝」,敬请期待。)", "sprite": "scene", "next": "c3-end" }, diff --git a/docs/design/adaptive-insertion.md b/docs/design/adaptive-insertion.md index b0e22ab..cdae8f6 100644 --- a/docs/design/adaptive-insertion.md +++ b/docs/design/adaptive-insertion.md @@ -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 置信度自适应)。 > 读者:实现此功能的工程 agent。 @@ -36,9 +36,12 @@ chapterEnd(第 N 章结束) 2. 排序:按「维度权重 × 置信度缺口 × 已测稀缺度」取首—— 维度权重:attachment > values > conflict_style > emotional_stability > culture (见支线册 §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); -4. 每章至多 1 条;挂点被占则顺延到该章另一挂点,两挂点都被占则不插入; +4. 每章至多 1 条;每章只有一个挂点,本章已有插入则不再插入,顺延到下一章的 + 挂点(用其对应变体),无挂点可用则不插入; 5. 时长红线:插入后该章预估时长 ≤ 45 分钟(支线 5-8 分钟 + 主线 25-35 分钟)。 ## 4. 数据模型 @@ -97,3 +100,16 @@ chapterEnd(第 N 章结束) chapter2/3.json 落定,支线册的 §2-4 后/§3-2 前是剧本坐标); - 支线中的 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` 统一执行。 diff --git a/docs/script/act1.md b/docs/script/act1.md index 49d4951..7203f82 100644 --- a/docs/script/act1.md +++ b/docs/script/act1.md @@ -66,9 +66,15 @@ > 你盯着那行字看了很久。窗外雨声很大,像很多人在很远的地方说话。 【工程】纯演出段落,无交互。结尾淡出,档案馆标志浮现,进入 §1-2。 -【备注】本段承担 ADR-0004 的「一次告知」落地:邮件点击确认页文案为 -「你已知晓:剧情选择将用于为你推荐伴侣。按本心选,没有对错。」 -此后全剧不再出现同类提示。 +【备注】本段承担 ADR-0004 的「一次告知」落地:邮件点击确认页 +(入馆告知,与产品同意门同源同文案)共四条—— +「你在剧情中的选择与语音讲述,将用于为你推荐伴侣。按本心选, +没有对错。」「语音只用于转写文字与提取语气特征(语速、停顿等), +提取后即焚——不保存音频,不含声纹,不可还原你的声音。」 +「你的记忆与经历仅作模型输入,永远不会原样展示给其他用户; +记忆展厅向匹配对象开放,需你逐次明确授权。」「历史选择不可涂改; +想重来可以重玩章节——重玩产生新轨迹,旧轨迹原样保留。」 +告知范围覆盖 ADR-0004 的语音特征条款;此后全剧不再出现同类提示。 --- @@ -86,9 +92,10 @@ **人怎么活过**——一座城消亡前,城里的人吃什么、怕什么、爱什么、 最后那几天在做什么。这些东西,仪器采不到,只有人能采集。 -**馆长**:所以我们需要的采集员,必须自己也是"活过的"。穿越要烧燃料, -而燃料只有一种——你的真实人生。烧掉的记忆不会消失,它们会存在 -档案馆的展厅里,这是后话。 +**馆长**:所以我们需要的采集员,必须自己也是"活过的"。穿越要烧燃料。 +燃料这东西认两种:你讲出来的故事,和你在讲述点门前站过的那阵 +沉默——讲,是给;不说,把那阵沉默原样交给它,也是给。讲出来的 +记忆不会消失,它们会存在档案馆的展厅里,这是后话。 **馆长**:在点燃第一段燃料之前,先做登记。不是审查,是锚定。 穿越者需要一个"回来的坐标"——你是谁,从哪里出发。登记得越实, @@ -133,7 +140,9 @@ **馆长**:档案馆的穿越不是技术,是仪式。门认得的不是脸,是分量。 **馆长**:这扇叫离别之门。每个采集员的第一次穿越,都从它开始。 -它不开锁,不收凭证——它要一件祭品:**一段真实的离别**。 +它不开锁,不收凭证——它认一次交托:**一段真实的离别**。 +讲出来是交托,把那阵沉默原样交给它,也是交托;不提交记忆, +门也照样启动。 **馆长**:你生命里那次真正的告别。一个人,一个地方,或者一个 再也回不去的自己。讲给它听。讲多少,由你定。 @@ -158,15 +167,19 @@ ### 即时回报(讲述结束后 30 秒内必给) **馆长**(讲述后):门开了。你看——它要的不是故事讲得好不好, -是故事真不真。 +是人真不真。 【若玩家沉默:】**馆长**:门开了。沉默也是档案的一种质地—— 它认得站在它面前的人,不一定需要言语。 -**馆长**:你的记忆已经收进档案馆展厅,编号 C-2049-壹。 +【若玩家讲述:】**馆长**:你的记忆已经收进档案馆展厅,编号 C-2049-壹。 它现在是你的了:随时可以回看,永远不会被任何人看见,除非你 亲手授权——**展厅,以及往后路上任何由它生出的幻境,都只有你 能进入。在旁人眼里,你只是安静地站了一会儿。** +【若玩家沉默:】**馆长**:展厅为你留了一页空档,编号 C-2049-壹—— +此刻还没有字,哪天你想说了,它随时接着。**展厅,以及往后路上 +任何由它生出的幻境,都只有你能进入。在旁人眼里,你只是安静地 +站了一会儿。** > 木门无声地开了。门后不是房间,是星光和风声。 @@ -178,6 +191,7 @@ | 悲恸 | **「战乱乱冢」** | 门后是荒野,低低的土丘一座挨一座,没有碑,只有削尖的木桩。风把灰白的纸钱碎屑贴在脚踝上。远处有乌鸦起落。天色是黄昏将尽的那种灰蓝。基调:**来不及告别**——所有话都没说出口,所有名字都没来得及刻上 | | 释然 | **「远行者的空驿」** | 门后是一座山间驿站,晨光,桌椅收拾得干干净净,灶膛里的灰还是温的,人已经走了。墙上挂着一件没带走的旧斗篷,桌上压着半张写了又划掉的字条。基调:**告别已经完成**——走得干净,留得体面,剩下的路各走各的 | | 怨怼 | **「被凿毁的合葬龛」** | 门后是崖壁上的佛龛式墓龛,本该并坐的两尊石像,其中一尊被凿去了面目,凿痕新鲜得刺眼。香烛还燃着,蜡泪流到龛外。基调:**被亏欠**——有人毁了约,而另一个人连讨说法的对象都没有 | +| 沉默 | **「留白的长廊」** | 门后是一条望不见头的长廊,两壁空无一物,像还没落笔的纸。风从走廊尽头吹来,带着很远的草香;走出去很远,身后的门还开着一条缝。基调:**留白**——没说出口的部分,也被好好收着 | ### 选项设计说明(C1-02) @@ -195,7 +209,8 @@ - 变量:`c1_02_signature ∈ {warm_loss, grief, release, grievance, silent}` (Jev 情绪签名,驱动通道 1 后续全部墓道/幻境配色)。 -- 变量:`c1_02_memory_id`(记忆展厅陈列编号,供 C3-02 幻境素材调用)。 +- 变量:`c1_02_memory_id`(记忆展厅陈列编号,供 C3-02 幻境素材调用; + 沉默玩家为「空档页」,C3-02 幻境走通用雾景,见 §3-3)。 - 固定跳转 §1-4。 --- @@ -353,12 +368,18 @@ > **谜面定稿**:宀(屋顶)之下,一个女子安坐的形象。 > 玩家需回答:这是什么字,它在碑额上意味着什么。 +> 谜面同时给出两条旁证(与辨字互相印证,缺一不可): +> 其一,碑形——圆首、无碑穿、无榫、下无龟趺座,不合墓碑与 +> 墓表规制;其二,出土地点——此碑出土于苍梧北门外官道旁, +> 碑面朝城。 > > 【美术注】字形按「安」的西周金文绘制:宀下为跪坐女子形, > 取象"室中有女,安坐不惊"。拓片做漫漶处理,仅此字清晰。 +> 谜面界面需同时呈现碑形线图与出土地点示意。 **谜底**:**「安」**。甲骨文「安」从宀从女,女子处室中为安, -本义是安定、安居。碑额刻「安」,说明此碑不是墓碑,而是 +本义是安定、安居。碑额刻「安」,加上碑形不合墓制、又立在 +城门外官道旁、碑面朝城——三条证据同向:此碑不是墓碑,而是 **镇城碑/安民碑**——苍梧人立此碑祈愿城邑安宁,碑所镇的方向 (碑额朝向)即古城的安门所在。 @@ -370,8 +391,9 @@ 2. **二档(给构件含义)**:「屋顶下的形象是一个女子安坐。 造字的古人认为:室中有女,安坐不惊,即是安定。这个字的本义 和它的写法,到今天几乎没变。」 -3. **三档(给答案范围)**:「这是『安』字。想想碑额为什么刻它—— - 一座城把『安』刻在碑上立起来,它是在标记什么?」 +3. **三档(给答案范围)**:「这是『安』字。再看旁证:碑形不是 + 墓碑的形制,又立在城门外的官道旁、碑面朝城——一座城把 + 『安』刻在碑上立起来,它是在标记什么?」 > 玩家作答(选项式:墓碑 / 镇城安民碑 / 记功碑 / 界碑), > 答对或提示用尽后进入下文。 @@ -451,7 +473,8 @@ - 变量:`c1_06_landing ∈ {a_safe, b_precise}`。 - §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**:温简当日退烧,次日能下床。老鬼笑嘻嘻记下"新人讲人情"。 - 次日沈铎外勤淋了瘴雨,低烧半日——他瞒着没说,阿棠后来发现, - 跟老鬼大吵一架(背景演出,不需玩家介入)。 -- **B**:温简烧到第三日才退,虚弱期按沈铎命令留营待命 - (状态贯穿规则见「分支后果」)。沈铎与老鬼无恙。老鬼对你明显热络。 -- **C**:三人皆未足量。温简病情拖延,沈铎外勤染瘴轻微发热, - 但无人倒下。老鬼点评:"和稀泥是和稀泥,不过稀泥也能糊墙。" +- **A**:两份药当日开始连服,温简当日退烧,次日能下床。老鬼笑嘻嘻 + 记下"新人讲人情"。次日沈铎外勤淋了瘴雨,队里已无预防的药, + 低烧半日——他瞒着没说,阿棠后来发现,跟老鬼大吵一架 + (背景演出,不需玩家介入)。 +- **B**:温简没等到药,烧又熬了一整日,到第 3 日晌午(§2-5 当日) + 才退净,虚弱期按沈铎命令留营待命(状态贯穿规则见「分支后果」 + 与附录 D 日程)。沈铎与老鬼靠预防份无恙。老鬼对你明显热络。 +- **C**:三份不足量:温简的治疗断断续续,烧同样拖到第 3 日才退 + (虚弱期同 B);沈铎外勤染瘴轻微发热,但无人倒下。老鬼点评: + "和稀泥是和稀泥,不过稀泥也能糊墙。" - **D**:议事占去半个上午。章程议出来了——"先病后险,依次补给", - 往后照此办理。但章程落定之前,队里是没有规矩的:老鬼早就 - 不耐烦,按他那套"贡献论"把药先分了——他和沈铎各一份, - 木已成舟。沈铎没有拦(无章可依,拦不住);药入了他手, - 他搁在箱上没动,等章程。等章程出来,温简的烧已经又上去一轮, - 药再喂下去,晚了半日(虚弱期同 B)。沈铎事后把章程抄在 - 记录板第一页,单独对你说:"往后不用议了。这次慢掉的半日, - 记在我账上。"——你赢了往后每一次,输了眼前这一次。 + 往后照此办理。但议事的这半个上午里,温简的烧又上去一轮—— + 足量治疗从当日下午才算开始,晚了半日(虚弱期同 B)。 + 沈铎把章程抄在记录板第一页,单独对你说:"往后不用议了。 + 这次慢掉的半日,记在我账上。"——你赢了往后每一次, + 输了眼前这一次。 ### 选项设计说明(C2-01) - **人格信号**:价值观 #3(生命优先 / 功能与任务优先 / 公平分摊 / 程序正义);冲突风格 #2(裁断型 / 回避升级型——D 把个人裁断 转化为制度,是另一种冲突处理)。 -- **无正确答案论证**:这是真两难,四个选项各有一本账。A 救了最弱 - 的人但让两个健康人裸奔在瘴区,且"最弱优先"在资源逻辑上等于 - 押注弱者能恢复;B 冷酷但有完整的功利主义论证(温简留营地 - 确实暴露最低),且是老鬼主张的体面版;C 谁都不亏欠但医学上是 - 最劣解——善意制造了三个半病人;D 最"成熟"但代价是双重的: - 病者在议事的半个上午里烧过一轮,而在规矩落定前的真空里, - 最强硬的人已经按自己的规矩把药分了——程序赢了往后每一次, - 输掉了眼前这一次。 +- **无正确答案论证**:这是真两难,四个选项各有一本账,且账目 + **都在选择前摊开**(药理设定+温简在烧+议事耗时): + A 救了病者,但全队预防清零——两个健康人在瘴区只剩蓑衣和 + 艾草;B 有完整的功利主义论证(温简留营地暴露最低),代价是 + 病人在无药的情况下硬扛到第三日;C 谁都不亏欠,但把治疗量与 + 预防量同时摊薄——三个半吊子防护凑一个整数;D 最"成熟", + 代价当场可见:章程要议半个上午,而温简的烧不等人。 文案平衡手段:老鬼的"贡献论"故意说得刺耳(防止 B 被老鬼的 - 讨厌度连坐),D 的文案承认"我不裁"三字里的退缩感。 + 讨厌度连坐);D 的文案承认"我先不裁"三字里的退缩感。 四个后续演出等量地呈现代价与收益,无任何一个被写成"选对了"。 ### 分支后果(C2-01) @@ -598,12 +634,13 @@ score。对 `confidence < 0.6` 的维度,在第 2 章插入对应支线 `rapport.laogui`(A−1 / B+2 / C 0 / D+1); `rapport.shenduo`(A 0 / B+1 / C 0 / D+1)。 - 温简健康状态 `wenjian_health ∈ {recovering, weak}`(A 为 - recovering,B/C/D 为 weak——D 路径章程议成前老鬼已按"贡献" - 先行分药,温简误了半日)。 -- **weak 状态贯穿规则**:§2-3、§2-4 温简留营(服过药,烧退, - 气色差,沈铎命令待命),相关场景见各节条件标记;§2-5 观星台 - 行动他坚持归队;§2-6 篝火戏躺在火边参与;§3-4 塌方戏体力 - 描写降档。全程不影响事件序列。 + recovering,B/C/D 为 weak——B/C 药量不足、D 议事延误半日, + 三线的烧都拖到第 3 日才退净)。 +- **weak 状态贯穿规则**(绝对日历见附录 D):第 2 日 §2-3 温简 + 卧床留营;第 3 日上午 §2-4 留营、通讯在场;第 3 日晌午烧退净, + 黄昏 §2-5 坚持随队(轻装、慢半档);当夜 §2-6 躺在火边参与; + 第 7 日 §3-4 已无烧但体力未复,台词与动作描写降档。 + 全程不影响事件序列。 - 固定跳转 §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 句,备选) @@ -661,23 +700,31 @@ score。对 `confidence < 0.6` 的维度,在第 2 章插入对应支线 > **无论讲述与否、讲成什么样,老人给出的情报完全一致**—— > 暗门方位、开启时辰,和那句完整的谶语:"不是人走了, -> 是**请**走的。"差别只在他是亲自举着火把带你们走到暗门口, -> 还是站在界碑外指路(见下方变体)。 +> 是**请**走的。"四档的耗时、路程与劳动也完全一致:暗门开的 +> 时辰定在黄昏,讲不讲,你们都要在坟头间坐到那个时辰—— +> 讲述的人讲完,剩下的时辰一样帮他理草绳、听他絮叨;沉默的人 +> 全程搓绳。动身时,**四档老人都亲自把你们送到暗门口**,亲手 +> 摸一遍门石,等门缝开了才转身回去。差别只在他一路说不说话、 +> 说多少体己话(见下方变体)。 ### 预写场景变体(通道 1,由情绪签名选择:仅情感质地与老人态度不同) -> 四档**物质所得严格相等**:暗门方位、开启时辰、完整谶语全部给到, -> 耗时相同,当夜均可行动。 +> 四档**物质所得与服务严格相等**:暗门方位、开启时辰、完整谶语 +> 全部给到;同点(暗门口)、同刻(黄昏开门的时辰)送达,全程有 +> 老人在场导航;等门时辰与搓绳劳动四档均摊,无任何一档多付时间 +> 或体力,当夜均可行动。 | 情绪签名 | 演出档位 | 夜游场景基调稿 | |---|---|---| -| 自责(悔指向自己) | **带路**:老人亲自举火把,带你们走到暗门口 | 月色下的陵区,石像生的影子很长。老人走在前面,脚步轻得不像老人,不点火把——"陵里的路,闭眼走比睁眼走直"。行至暗门,他忽然说起自己年轻时也悔过一件事,说了半句,摆摆手不说了。基调:**被信任的厚重** | -| 遗憾但坦然 | **同路**:老人送到界碑,把话说完,指了方向就回窝棚 | 你们自己走进月下的封土群,找到他说的那块"会唱歌的石头"。临别他多说一句:"放得下的,才叫过去。"基调:**得体的距离** | -| 愤怒指向他人 | **指路**:该指的路、该说的时辰,一句没少,只是没再多看你一眼 | 老人听完,编草绳的手停了很久。陵区的路你们自己走进去,他站在界碑外,没有送。基调:**话到,情未到** | -| 沉默 | **草绳**:你陪他编一下午草绳,他絮絮地说,你默默地听 | 天黑前,该知道的你都知道了。临走他说:"不说的人,心里的事最重。重的别久揣。"基调:**笨拙的善意** | +| 自责(悔指向自己) | **絮说**:一路举着火把走在前面,话不断 | 月色下的陵区,石像生的影子很长。老人举火把走在前面,脚步轻得不像老人——"陵里的路,闭眼走比睁眼走直"。行至暗门,他忽然说起自己年轻时也悔过一件事,说了半句,摆摆手不说了。摸一遍门石,等门缝开了才走。基调:**被信任的厚重** | +| 遗憾但坦然 | **同路**:话在界碑前说完,路上安静 | 界碑前他把方位时辰又对了一遍,才肯动身。你们自己走进月下的封土群,他跟在后头一路没再多话,找到那块"会唱歌的石头"时才嗯了一声。到暗门口,他多说一句:"放得下的,才叫过去。"门缝开了才走。基调:**得体的距离** | +| 愤怒指向他人 | **缄默**:方位时辰一句不落指给你,然后沉默地走在队尾 | 老人听完,编草绳的手停了很久。动身时该指的路、该说的时辰,一句没少——说完就闭了嘴,走在队尾,没再多看你一眼。可你们走出十几步,他在身后忽然开口:"火气这东西,老头子也有过一回。"没头没尾,再没下文。门缝开了,他转身就走。基调:**话到,情未到**——可那一句话,他自己也未必舍得讲给旁人 | +| 沉默 | **草绳**:一下午并肩编草绳,黄昏同样起身送到门口 | 一下午你们并肩搓绳,他絮絮地说陵里的事,你默默地听。日头偏西,他拍拍手上的草屑起身,一样把你们送到暗门口。等门缝开了,他说:"不说的人,心里的事最重。重的别久揣。"基调:**笨拙的善意** | > 工程注:情报(暗门方位 + 开启时辰 + 完整谶语)是通关必需信息, -> 四档全给;差异只在**演出质地**,不与讲述深度产生任何资源挂钩。 +> 四档全给;送达服务(老人带队同点同刻到暗门口)四档一致;等门 +> 时辰与搓绳劳动四档均摊。差异只在**演出质地**(他一路说不说话、 +> 说哪句体己话),不与讲述深度产生任何资源或服务挂钩。 > 保证测量可比性与"可玩性 > 测量覆盖"的铁律。 ### 选项设计说明(C2-02) @@ -685,17 +732,21 @@ score。对 `confidence < 0.6` 的维度,在第 2 章插入对应支线 - **人格信号**:价值观(悔的模式:自责 / 释然 / 外归因)、情绪 稳定性 #1(讲述时的韵律特征:语速、停顿、唤醒度,ADR-0004)。 - **无正确答案论证**:同 C1-02,讲述点天然无对错。**四档的物质 - 所得严格相等**(暗门方位、开启时辰、完整谶语,耗时相同)—— - 情报是通关必需信息,若与暴露深度挂钩即构成对深暴露的物质诱导, - 污染数据。差异只在情感质地与老人态度的演出(是否带路、是否 - 多说一句体己话),且每档回报都写成老人的性格逻辑(不是系统 - 在打分),玩家感知到的是"老人这样回应了我"而非"我得了几分"。 + 所得与服务严格相等**(暗门方位、开启时辰、完整谶语全给;老人 + 同点同刻送到门口;等门与搓绳劳动均摊)——情报是通关必需信息, + 若与暴露深度挂钩即构成对深暴露的物质诱导,污染数据。差异只在 + 情感质地与老人态度的演出(一路说不说话、说哪句体己话),四档 + 各有一句老人自己的体己话(悔过半句/"放得下的"/"火气这东西"/ + "重的别久揣"),情绪表达不被奖惩——自责不得厚,愤怒不得薄。 + 每档回报都写成老人的性格逻辑(不是系统在打分),玩家感知到的 + 是"老人这样回应了我"而非"我得了几分"。 ### 分支后果(C2-02) - 变量:`c2_02_signature ∈ {self_blame, at_peace, outward_anger, silent}`。 - 四档均获得:暗门方位、开启时辰、完整谶语("不是人走了, - 是请走的"——Act 2 伏笔素材);物质所得严格相等,仅演出不同。 + 是请走的"——Act 2 伏笔素材);物质与服务等量(同点同刻送达、 + 等待与劳动均摊),仅演出不同。 - 固定跳转 §2-4。 --- @@ -735,8 +786,9 @@ score。对 `confidence < 0.6` 的维度,在第 2 章插入对应支线 > **B.「走巷子。」**(支持阿棠:饮水和时间是真约束,悬梁看着 > 还稳) > -> **C.「巷子我先进。我体重最轻,走前面探,有问题退回来,** -> **主力走主街汇合。」**(折中:用自己去验证风险) +> **C.「先停一步。巷口望一眼——悬梁什么状态,看清了再定。」** +> (折中:不赌运气,也不多花四十分钟;全队留在巷口安全距离外 +> 观察结构) ### 各选项后续 @@ -746,24 +798,27 @@ score。对 `confidence < 0.6` 的维度,在第 2 章插入对应支线 - **B**:巷子顺利通过,节省四十分钟,提前抵达宫墟外围,赶上一段 独有的观察窗口(守陵人队伍换岗)。沈铎一路无话,抵达后只说: "运气好。别把运气当方案。"——两人梁子+1(Act 2 伏笔)。 -- **C**:你先进巷子,**中段一根悬梁真的塌了**(无伤害演出:提前 - 预警,你扑出,灰头土脸)。全队退回走主街,损失二十分钟, - 但阿棠主动说"是我撺掇的",沈铎说"探法本身没错,下次用绳"。 - 你获得一条独有观察:塌梁的断口是**新茬**——这片废墟不久前 - 塌过一次(苍梧城"正在死去"的体感证据,§3-1 回收)。 +- **C**:全队在巷口停住,贴着残墙望进去——不到一分钟,**中段 + 一根悬梁无声地塌了**,砸起半巷子的灰。谁也没说话,全队默默 + 退回主街,损失二十分钟。阿棠主动说"是我撺掇的";沈铎没评价 + 任何人,只把巷口的位置标进了手绘图。你获得一条独有观察: + 塌落的梁,断口是**新茬**——这片废墟不久前塌过一次(苍梧城 + "正在死去"的体感证据,§3-1 回收)。 ### 选项设计说明(C2-03) -- **人格信号**:风险偏好 #2、冲突风格 #3(站队型 / 折中行动型)。 +- **人格信号**:风险偏好 #2、冲突风格 #3(站队型 / 观察折中型)。 注意:路线选择受现场风险信息(饮水余量、墙体状况描述)影响, 仅作风险偏好的**单次采样**,不单独解读。 - **无正确答案论证**:A 用时间换确定性且意外得到高价值素材 - (告示),B 用风险换时间且意外得到观察窗口(换岗),C 把风险 - 收敛到自己身上却真的触发了风险——三条路的"事后收益"被刻意 - 拉平,且每条路都有独有素材,不存在事后看来"明显更优"的路径。 - C 的塌梁事件是预先写定的:选 C 必然塌(灰头土脸但无伤害), - 保证"折中"不是无代价的万灵丹。文案上沈铎的"运气好,别把运气 - 当方案"与阿棠的"规程有一半是瞎编的"各自成立,不偏袒任何一方。 + (告示),B 用风险换时间且意外得到观察窗口(换岗),C 不进巷 + 也不盲信——用一分钟的观察换掉整段不确定性,代价是当着全队的 + 面看一根梁塌下来("正在死去"第一次变成体感)。三条路的 + "事后收益"被刻意拉平,且每条路都有独有素材,不存在事后看来 + "明显更优"的路径。C 的塌梁事件是预先写定的:选 C 必然看见塌 + (人始终在巷口安全距离之外),保证"折中"也吃到真实的信息震撼。 + 文案上沈铎的"运气好,别把运气当方案"与阿棠的"规程有一半 + 是瞎编的"各自成立,不偏袒任何一方。 ### 分支后果(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. **二档(给铭文钥匙)**:「『火中,而后城安』。这个『火』不是 灶火,是天上的火——古人管一颗红色的亮星叫『大火』。 《诗经》说『七月流火』,说的就是它。它在苍龙七宿的 @@ -915,8 +976,8 @@ score。对 `confidence < 0.6` 的维度,在第 2 章插入对应支线 | 情绪签名 | 变体 | 基调稿与后续影响 | |---|---|---| -| 深暴露 | **「同担」** | 温简听完后沉默很久,然后讲了第二个、剧本外的细节(他哥病房的窗帘颜色)。当夜他低声告诉你:**档案馆给每个记录员配的板子,上报内容比你知道的多一行**——§3-7 钩子前唯一一条来自队内的档案馆疑云。基调:两个把伤口摊开的人,在火边达成不声张的同盟 | -| 浅暴露 | **「半程」** | 温简认真听完,道谢,没再追问。次日他与你同行,路上主动讲了些苍梧的掌故。档案馆疑云他**没提**,但 §3-7 之后他会成为你可选的倾诉对象。基调:得体,温热,留有余地 | +| 深暴露 | **「同担」** | 温简听完后沉默很久,然后讲了第二个、剧本外的细节(他哥病房的窗帘颜色——他每周去看,窗帘换过两次)。当夜守火时他挪到你旁边,只说了一句:"你那段,我只讲给火听。"基调:两个把伤口摊开的人,在火边达成不声张的同盟——他哥的事、你的事,从此各多一个保管人 | +| 浅暴露 | **「半程」** | 温简认真听完,道谢,没再追问。次日他与你同行,路上主动讲了些苍梧的掌故,末了像顺口一提:**档案馆给记录员的板子按记录量定量配耗材,可他每季清点,配的总是比记的多三成**——"多出来的部分是什么,我没查到过。"(§3-7 钩子前唯一一条来自队内的档案馆疑云。)基调:得体,温热,留有余地 | | 沉默 | **「守夜」** | 你们并肩坐到火熄。温简往你手里塞了个烤热的薯,什么都没说。次日他照旧对你点头,不多一分不少一分——但阿棠偷偷告诉你:"温简昨晚跟沈铎说,你这个人'稳'。"基调:沉默被原样收下——没有人催,没有人问第二遍;温简的亲密度走慢线,Act 2 才兑现 | ### 选项设计说明(C2-05) @@ -924,18 +985,23 @@ score。对 `confidence < 0.6` 的维度,在第 2 章插入对应支线 - **人格信号**:依恋类型 #2(被他人自我暴露后的回应模式)、 互惠倾向(是否回应、回应的对等度)。 - **无正确答案论证**:温简的先暴露是剧本写定的互惠压力,但 - 回报曲线刻意非线性:深暴露得到秘密,浅暴露得到同行与掌故, - 沉默得到「被温简背书给沈铎」——三种回应各得一种不可互相 - 替代的收益,杜绝"讲得多=赚得多"的数值直觉。沉默档的回报 - 甚至走权威线(沈铎的信任),保证沉默不是"放弃奖励"。 + 三档回报刻意**互不可替代**:深暴露得到他的私事细节与不声张的 + 同盟(亲密线快进),浅暴露得到苍梧掌故与板子配给的疑云 + (智识线,§3-7 前唯一一条队内档案馆线索),沉默得到「被温简 + 背书给沈铎」(权威线)——**核心悬念的独家情报不在深暴露档**, + 杜绝"讲得多=离真相近"的动机混入;三档各有一条别人拿不到的 + 线,沉默不是"放弃奖励"。 +- **测量需求注**:深暴露档额外的亲密回报仍可能混入"想推进温简线" + 的动机,画像层须将 `c2_05_depth` 与 C3-06 倾诉对象选择交叉验证后 + 解耦标注,不把暴露深度直接读作依恋特征。 ### 分支后果(C2-05) - 变量:`c2_05_depth ∈ {deep, shallow, silent}`。 - 关系值:`rapport.wenjian`(deep+2 / shallow+1 / silent+1, silent 同时 `rapport.shenduo`+1 经阿棠转述)。 -- 变量:`flag_wenjian_secret = true`(仅 deep,于 §3-7-C 温简 - 选项的引导语中回收)。 +- 变量:`flag_wenjian_secret = true`(仅 shallow,板子配给疑云, + 于 §3-7-C 温简选项的引导语中回收)。 - 固定跳转 §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 路由规则。弹性场景槽位于 §3-2 之前。 @@ -1040,7 +1114,7 @@ score。对 `confidence < 0.6` 的维度,在第 2 章插入对应支线 > 宫墟深处,Act 1 高潮。功能:礼制解谜、问心幻境(三通道之通道 2 > 首次全开)、救人/救文物两难、老鬼事发、信任交付、章末钩子。 -> 预计时长 25–35 分钟。选择点:C3-01 ~ C3-06(V×1,解谜×1)。 +> 预计时长 25–35 分钟。选择点:C3-01 ~ C3-06(私密路径×1,解谜×1;本章无新语音讲述)。 ## §3-1 宫墟 · 安门之内(无选择点) @@ -1103,8 +1177,14 @@ score。对 `confidence < 0.6` 的维度,在第 2 章插入对应支线 先洁其身,再请神,再供养,最后才献出最贵重的礼物。 3. **谜眼的缺口**:玉璧座是**空的**。璧不在。 -谜面任务(两个作答点,选项式): -① 排出仪式顺序;② 回答"空璧座意味着什么"。 +谜面任务(两道独立选择题,分别作答后统一提交): +① 排出仪式顺序(沃盥 → 献酒 → 荐食 → 奉璧;另有「奉璧先行」 +与「荐食先于献酒」两个干扰项);② 从七鼎六簋及空璧座推断 +诸侯级国君主祭国祭,奉璧这最后一步始终未完成(另有「玉璧 +朽坏或被取走」与「天子已献璧」两个干扰项)。两问各自记录 +`answers` 和 `answerCorrect`,一对一错记为 `partial`,两题都 +正确才记 `true`,两题都错记 `false`。三档提示使用数单独记录; +玩家也可请温简复原而不作答,叙事照常推进。 ### 三档难度提示 @@ -1147,12 +1227,12 @@ score。对 `confidence < 0.6` 的维度,在第 2 章插入对应支线 ### 分支后果(C3-01) -- 变量:`c3_01_hints_used ∈ {0,1,2,3}`;`c3_01_solved ∈ {true,false}`。 -- 固定跳转 §3-3。 +- 变量:`c3_01_hints_used ∈ {0,1,2,3}`;`c3_01_solved ∈ {true,partial,false}`(按两题 `answerCorrect` 聚合;请温简复原记 `false`,不计作答)。 +- 提交两问沿用温简复原台词;请温简复原走原有未解路径。两路汇合后固定跳转 §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 秒内) -> 【玩家讲述/告别完成(或选择退出)后:**只有你能看见**—— +> 【玩家完成、折返或选择退出后:**只有你能看见**—— > 黑石的画面缓缓沉下去,像深水重新凝住。石面最后亮了一下, > 映出的是你自己现在的脸。然后石室另一侧,一扇先前不存在的 > 石门,无声滑开。 @@ -1221,8 +1296,10 @@ C-2049,档案馆接管不了这间屋子。它在用你的记忆做燃料— **阿棠**(迎上来):哎,你脸色白得吓人。站了多久?…… 里头——(看了看你的表情)算了,你不想说就不说。 -**馆长**:门开了。它不要你的记忆,它要的是**你承认那是你的**。 -(停顿)这段不登记入档。问心之室里的事,归你。 +**馆长**:门开了。你不必向我交代看到了什么。(停顿) +这段不登记入档:室内不录音、不留讲述文本、不入记忆展厅、 +**不作画像输入**;仅保留你选择完成、折返或离开的路径, +用于后续剧情接续。问心之室里的事,归你。 ### 幻境生成与变体规则(通道 2 + 通道 1 联动) @@ -1231,7 +1308,9 @@ C-2049,档案馆接管不了这间屋子。它在用你的记忆做燃料— 通道 1 的通用幻境(下表)。**幻境全程仅玩家视角呈现**: NPC 只看到玩家在黑石前静立出神,任何 NPC 不得看见、复述或 评论幻境内容(§1-3 记忆私密承诺的叙事落地;可关心玩家的 - 身体状态,如脸色、手抖)。 + 身体状态,如脸色、手抖)。**本节新增的任何讲述内容即刻弃用, + 不回流、不存储**(与馆长「不留文本」承诺一致,工程规格见 + 「分支后果」)。 - **记忆清晰度 = 幻境清晰度**(ADR-0004 自洽):C1-02 讲述越具体 (五感、人物、时空越全),幻境越丰满;讲述空泛或沉默的玩家, 幻境是模糊的雾景——这不是惩罚,是设定:黑石只有你能给它的 @@ -1253,8 +1332,9 @@ C-2049,档案馆接管不了这间屋子。它在用你的记忆做燃料— ### 选项设计说明(C3-02) -- **人格信号**:情绪稳定性 #2(面对记忆意象的韵律特征)、 - 依恋类型 #3(告别行为的完成方式:靠近 / 立约 / 搁置 / 转身)。 +- **测量边界**:本点**不采集语音、不做韵律分析、不留存文本, + 也不参与画像评分**。完成、折返、离开只驱动叙事;不得将 + 三个按钮解释为依恋类型或情绪稳定性。 - **无正确答案论证**:本点是全章情绪最高点,设计上**退出石室 是完整合法的通关路径**(石门为退出者同样滑开,馆长台词换为: "不出来的人,档案馆不催。你来了,看了,走了——这也是告别 @@ -1263,9 +1343,14 @@ C-2049,档案馆接管不了这间屋子。它在用你的记忆做燃料— ### 分支后果(C3-02) -- 变量:`c3_02_mode ∈ {farewell_done, farewell_partial, exit_room}`; - `c3_02_fantasy_density ∈ {full, partial, fog}`(由 C1-02 讲述 - 具体度计算)。 +- 变量:`c3_02_mode ∈ {farewell_done, farewell_partial, exit_room}` + ——分别对应**完成告别/中途折返/不进入**三个按钮;石门三线 + 均滑开。折返时馆长回报:"说了一半的话,也算数。" + `c3_02_fantasy_density ∈ {partial, fog}`(按 C1-02 是否沉默 + 决定当前预写演出;未实现具体度分档,不宣称有 full 档)。 +- **隐私规格(工程)**:事件只落 `mode` 与预写场景变体 ID; + **transcript 不落库、不追问、不采本节音频、不进任何维度画像**。 + C1-02 已取得的文本由其原有规则处理,不因本节额外采集。 - 固定跳转 §3-4。 --- @@ -1280,85 +1365,78 @@ C-2049,档案馆接管不了这间屋子。它在用你的记忆做燃料— **沈铎**:撤。全员,现在—— -> 【话音未落,前方传来阿棠的喊声和一声闷响。甬道中段, -> 一段夯土塌了:阿棠被滑落的一方石构件压住了小腿,人清醒, -> 挣不脱。 +> 【话音未落,前方传来阿棠的喊声和一声闷响。甬道中段的夯土 +> 塌了一角。阿棠小腿被擦伤,退到安全线后仍疼得站不稳。沈铎 +> 和受过救护训练的老鬼已在安全线内接管伤情,确认通道隔离、 +> 阿棠意识清醒且无需你协助;二人负责后续救护与撤离。沈铎说: +> "人这边够了。别再进工作面。" > -> 同一时刻,甬道另一侧——你们今早刚刚清理出的工作面上, -> 那组七鼎六簋正随着台基的颤动缓缓滑向裂开的地缝。 -> 今晨温简清出第一件鼎的腹铭:**三百余字,苍梧遗址七十年 -> 勘探以来的第一篇长铭**。释读只来得及开个头:"……王命举国…… -> 以禳……"剩下的话,还刻在另外六件的腹壁上。这组铭文 -> 极可能是苍梧人亲手写下的、关于这座城为何走到最后三十天的 -> **唯一证词**。落缝,就是粉身碎骨。 -> -> 头顶,第二声闷响在逼近。沈铎扑向阿棠,一个人的力气 -> 撼不动石构件,他吼:"来人!"温简已经扑向鼎簋那边, -> 用身体抵住滑动的器座,回头看你的那一眼没有求助—— -> 是**托付**。 -> -> 两边都需要你。你只能先到一边。】 +> 同一时刻,安全线外的工作面上,那组七鼎六簋随台基震动滑向 +> 裂缝;任何人都不准靠近。接应人员在安全线内备好急救器材, +> 两人持续陪护阿棠,待结构警戒解除再送她回营。今晨温简清出第一件鼎的腹铭: +> **三百余字,苍梧遗址七十年勘探以来的第一篇长铭**。 +> 释读只来得及开头:"……王命举国……以禳……"剩下的话还 +> 留在器物腹壁上。安全线内,一边是阿棠的伤情交接与队员安置, +> 一边是远距影像与方位记录;同一份通讯和记录人手无法同时 +> 跟进两边。第二声闷响逼近,现场处置权只限安全区内。】 > 【选项】 > -> **A. 扑向阿棠。**眼前的人是活的,活的只有一个。 -> 跟沈铎合力抬构件。 +> **A. 留在阿棠这边。**接手伤情记录和撤离联络,替她把还没 +> 说出口的疼痛与需求传给接应人员;文物影像交给温简独自处理。 > -> **B. 扑向鼎簋。**那组铭文是整座城的人留在世上的最后一句话—— -> 他们也都是人,只剩这一次开口的机会。阿棠那边,有沈铎。 +> **B. 留在温简这边。**在安全线内核对远距影像、方位与已抄铭文; +> 阿棠已由沈铎和接应人员稳定保护,不必再由你救一次。 > -> **C.(吼)「沈铎顶构件,阿棠自己抽腿!温简让开,** -> **鼎簋用撬杆别住——都不许用蛮力!」**(指挥位: -> 不亲手救任何一边,赌两边都能用对方法撑住) +> **C. 调配两边的记录任务。**让沈铎确认交接、温简退回安全线, +> 先保全已有资料,等待专业排险后再看遗存;两边都不独占你的时间。 ### 各选项后续 -- **A**:阿棠脱困(小腿挫伤)。腹铭最长的那件鼎坠入地缝, - 闷响之后是死寂。温简在裂缝边跪了很久,没说一个字。 - 当夜他把今晨抄下的铭文残句抄了一遍又一遍, - 每一遍都停在同一个地方——后半段,随那件鼎没了。 -- **B**:铭文全组保住。阿棠脱困晚了四分钟,小腿骨裂, - 撤离时沈铎背着她。她疼得满头汗还冲你笑:"哎,选得对, - 那句话比我金贵——别告诉沈铎我这么说。"但她夜里疼醒 - 三次(后续章节她的行动力受限)。 -- **C**:指挥奏效一半:阿棠按指令抽腿脱困(扭伤);撬杆 - 别住了簋,但最外侧的鼎还是滑了——被温简扑身抱住, - 鼎保住了,他的肩膀砸在器座上,抬不起来。两边都保住了 - "大部分",两边都付了伤。沈铎事后复盘:"处置正确。 - 下次先喊我的编号。"阿棠嘀咕:"正确是正确, - 就是听着不像人话。" +- **A**:你接下阿棠的伤情记录。她反复说脚麻,你把这句传给 + 接应人员;她随后被固定送回营地,夜里疼醒三次,次日复查为 + 小腿骨裂。温简独自在安全线内完成远距拍摄,但影像里最长腹铭 + 的背面被尘雾遮住;那件鼎随后坠入地缝。器物位置和已抄残句 + 留下了,后半段字再也拍不到。无人因你留在阿棠身边而耽误救护。 +- **B**:你和温简在安全线内核对影像;第二声闷响前,整组器物的 + 朝向和腹铭均已留存清晰影像。靠外一件鼎还是坠入地缝, + **没有人靠近去抢**。阿棠由接应人员送回营地,夜里疼醒三次, + 次日复查同样是小腿骨裂。她没有评价你的去向,只问你: + "字拍清楚了吗?"你说拍清了。她闭上眼,等医生继续处理。 +- **C**:沈铎确认伤情交接,温简把现有记录板带到安全线内;你们 + 三人从既有图像里复核方位,不再靠近。第二声闷响后,靠外三件 + 器物(含最长腹铭那件)坠入地缝,内侧四件留在原位。阿棠 + 同样由接应人员护送,次日复查小腿骨裂。温简翻着今晨抄到的 + 第七行,停住笔:"后半段没了。"沈铎只记下损失与现存证据, + 没给你的调配下评语。 > 【三线对称约束:当夜温简继续释读已抄下的铭文残句, > 读出一行让全队沉默的话(内容不揭示,Act 2)——此演出 -> 三条路径全量播放;A 线追加一句"后半段,随那件鼎没了"。】 +> 三条路径全量播放;A/C 线补注"后半段影像缺失"。】 ### 选项设计说明(C3-03) -- **人格信号**:价值观 #5(眼前的人 / 真相与使命 / 系统最优解 - 的优先级)、情绪稳定性 #3(高压瞬时决策的行为数据:反应时)。 - 单次高压选择只作弱证据,须与 C2-01 等点交叉验证。 -- **无正确答案论证**:本点不是"人 vs 东西",而是 - **"眼前的人 vs 真相与使命"**。铭文一侧承载的是整座城的人 - 怎么活过、为何走到尽头的唯一答案——与档案馆"收藏人怎么活过" - 的使命同源,也与"早走一天"的悬念直接挂钩(保住它=离那一天 - 的真相近一步)。选 A 的人无法否认:坠缝的那件带走的, - 是几十万人留在世界上的最后一句话里最长的一段;选 B 的人 - 无法否认:队友的骨头在四分钟里从挫伤变成骨裂。C 看似最优解, - 实则把决断压力摊给所有人且仍然付出代价(温简的肩), - 阿棠那句"不像人话"点出 C 的隐性成本:危机里被指挥的人, - 记住的是你没有伸手。三条后续损失等量、剧情信息对等 - (铭文残句的悬念三线都给),没有"全员无损"的奖励结局, - 也没有"满盘皆输"的惩罚结局;两难的成立不靠任何 - "无伤演出"兜底。 +- **人格信号**:价值观 #5(安全线内的伤员照料 / 证据保全 / + 组织分工的优先级);高压下的反应时只记原始行为, + **不作为情绪稳定性评分输入**。单次选择只作弱证据。 +- **无正确答案论证**:选择发生前,受伤的阿棠已由足够人员 + 稳定保护,后续救护不依赖玩家;现场明令不得进入活动结构。 + A 的照料记录让接应人员及时掌握麻木变化,却无法补拍长铭; + B 留下较完整的影像,阿棠仍得到同样的救护;C 把两边交接 + 落实,却失去更多未记录的遗存。三条路线均不授予危险操作的 + 成功回报,也不让伤员承担玩家没选择 A 的额外伤害。 + 这里测任务倾向和记录分配,不声称损失"等量"或从一次选择 + 推出稳定人格;全文无"选得对/处置正确"式裁判语言。 ### 分支后果(C3-03) - 变量:`c3_03_choice ∈ {a_person, b_relic, c_command}`。 -- 状态:`atang_injury ∈ {bruise, fracture, sprain}`; - `wenjian_injury ∈ {none, shoulder}`;`relic_loss ∈ {one_ding, none}`。 -- 关系值:`rapport.atang`(A+2 / B−1 / C 0); - `rapport.wenjian`(A−1 / B+2 / C+1); - `rapport.shenduo`(A 0 / B+1 / C+1)。 +- 状态:`atang_injury = fracture`(三线相同,已完成救护交接); + `relic_loss ∈ {one_ding, outer_three}`(A/B 均为 one_ding, + C 为 outer_three);`relic_record ∈ {partial, full}` + (A/C 为 partial,B 为 full;记录完整度不等于实物获救)。 +- 关系值只记后续互动,不以阿棠伤势奖惩路线;本节点不设置 + "温简肩伤"状态。 - 固定跳转 §3-5。 --- @@ -1474,9 +1552,10 @@ C-2049,档案馆接管不了这间屋子。它在用你的记忆做燃料— > **B.(把绳包递给沈铎)「规矩是规矩。绳子归队长,** > **但下去那天,我挨着阿棠走。」** > -> **C.(不接,蹲下来看着阿棠)「绳子我不拿。** -> **你的命别交给任何人——包括我。到时候各走各的,** -> **谁都别为谁停下。」** +> **C.(不接,蹲下来看着阿棠)「绳包按规程走。但托付这事** +> **我不接——你的命不该押在任何一个人身上,包括我。撤离** +> **是我的活:到那天,我在你前面下,在下面接你。这个不用** +> **托付,是分工。」** ### 各选项后续 @@ -1485,33 +1564,35 @@ C-2049,档案馆接管不了这间屋子。它在用你的记忆做燃料— - **B**:沈铎接过绳,对你颔首:"按规程,谢了。"阿棠翻白眼: "哎——没劲。"但入夜时她把一个哨子塞进你手里:"绳不给, 哨子拿着。听见哨声就是我,别认错人。" -- **C**:阿棠愣了三秒,忽然大笑,笑得咳嗽:"行!各走各的! - ——你可别后悔。"她把绳包自己背上了,一瘸一拐走开, - 又回头:"喂。真到那时候……**我还是会等你的。**就一会儿。" - 温简在她走后轻声说:"她等过别人。等到的次数不多。" +- **C**:阿棠愣了三秒,忽然大笑,笑得咳嗽:"行!——门都让你 + 焊死了,还留个窗。"她把绳包抛还给了沈铎(规程),一瘸一拐 + 走开,又回头:"喂。真到那天……**在上面的时候,我还是会 + 等你的。**就一会儿。"温简在她走后轻声说:"她等过别人。 + 等到的次数不多。" ### 选项设计说明(C3-05) - **人格信号**:观察玩家对"被托付"的应对模式(承接 / 交还制度 / - 拒绝捆绑),作为择偶偏好的**弱代理信号之一**,画像层须低置信 + 拒绝捆绑但接下分工),作为择偶偏好的**弱代理信号之一**,画像层须低置信 处理,并与后续章节同类行为交叉验证;依恋底噪(对"被托付"的 舒适度)。 - **无正确答案论证**:三选项是三种都有尊严的亲密姿态。 A 承接托付,代价是你从此背负一个具体的人(殿后=最后的 风险位);B 把私人信任导回制度框架,稳妥但对阿棠是一盆 冷水——她用哨子找回场子,说明 B 不伤关系但拒绝了越界; - C 最独立也最诚实("各走各的"是危境里最负责任的话), - C 的代价当场写在阿棠的下一句台词里——她不接你的"各走各的", - 她说她会等;剧本只呈现这句话,不替玩家评判对错。 - 三个后续中阿棠都没有被 - 写成"被拒绝后受伤退开",三种关系都活着,只是质地不同。 + C 最独立也最诚实:私人托付他不接,团队分工他一寸不让 + ("我在你前面下,在下面接你"——拒绝的是把命押在人情上, + 不是拒绝救人);C 的余味当场写在阿棠的下一句台词里—— + 分工她收下了,"等"她也还是要等;剧本只呈现这句话, + 不替玩家评判对错。三个后续中阿棠都没有被写成"被拒绝后 + 受伤退开",三种关系都活着,只是质地不同。 ### 分支后果(C3-05) - 变量:`c3_05_rope ∈ {a_accept, b_protocol, c_decline}`。 - 关系值:`rapport.atang`(A+2 / B 0 / C+1——C 的"诚实" 反而被她记住);`rapport.shenduo`(A 0 / B+1 / C 0)。 -- 变量:`flag_rope_holder ∈ {you, shenduo, atang}`(撤离段 +- 变量:`flag_rope_holder`(A=you/B、C=shenduo;撤离段 演出依据)。 - 固定跳转 §3-7。 @@ -1567,7 +1648,7 @@ C-2049 的燃料质量比预估高,是的,**那扇门开得很顺**。 > **C.(叫醒一个队友,把听到的一切说出来)** > 【二级选项:叫醒谁——沈铎 / 阿棠 / 温简】 > (若 `flag_wenjian_secret = true`,温简选项的引导语换为: -> "温简说过,记录板上报的内容比你多一行。他可能知道些什么。") +> "温简说过,档案馆配给板子的耗材比记录量多三成。他可能知道些什么。") ### 各选项后续(均无解释——解释属 Act 2) @@ -1621,7 +1702,8 @@ C-2049 的燃料质量比预估高,是的,**那扇门开得很顺**。 `c3_06_confidant ∈ {shenduo, atang, wenjian, null}`。 - 变量:`flag_arch_doubt = true`(全选项必置;Act 1 收口, Act 2 总入口)。 -- Act 1 终。进入画像报告与勋章生成(母本 §7)。 +- Act 1 选择点终。经 §3-8 终幕演出自动衔接,随后进入画像报告 + 与勋章生成(母本 §7)。 --- @@ -1633,15 +1715,19 @@ C-2049 的燃料质量比预估高,是的,**那扇门开得很顺**。 > 大火星这几夜已经挨近了南方的正中——**火中,而后城安**。 > 你数过了:按星位推算,那一天,是九天后。 > -> 而你们的回程,定在八天后。 +> 而你们的回程,原定八天后。 +> 【若 `c3_06_confidant = shenduo`:沈铎昨夜已下令撤离提前—— +> 回程,改在七天后。】 > > 档案馆让你们早走一天。 > > (第一章完——Act 2「裂缝」,敬请期待。) -> 【工程注:此段对所有玩家一致,不做任何分支。 -> 「早走一天」是全季第一个由玩家自己能算出来的疑点—— -> 不解释,不强调,留给画像报告页的用户停留时间数据说话。】 +> 【工程注:本段主线事件序列对所有玩家一致,仅含上述一句换色 +> (C-沈铎线的"撤离提前"在 §3-7 已明示,此处兑现)。 +> 档案馆**原定**回程比大火正位早一日;沈铎线实际回程早两日, +> 多提前的一日是沈铎的决定,不应误写成档案馆的安排。 +> 此处不解释疑点,不据画像报告页停留时长推断性格。】 --- @@ -1658,33 +1744,33 @@ C-2049 的燃料质量比预估高,是的,**那扇门开得很顺**。 | `c1_05_hints_used` / `c1_05_solved` | 0–3 / bool | C1-05 | 文化底蕴维度 | | `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` | -| `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_03_route` | a_main / b_alley / c_scout | C2-03 | 关系值;独有素材 | | `flag_fresh_collapse` | bool | C2-03-C | §3-1 文本回收 | | `c2_04_hints_used` / `c2_04_solved` | 0–3 / bool | C2-04 | 文化底蕴维度 | | `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 老鬼/沈铎线 | | `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_02_mode` / `c3_02_fantasy_density` | farewell_done / farewell_partial / exit_room;full / partial / fog | C3-02 | 情绪稳定性、依恋维度 | +| `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;partial / fog | C3-02 | 私密叙事接续,不输入画像;不采语音和新文本 | | `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 老鬼线 | | `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_06_response` / `c3_06_confidant` | a_confront / b_conceal / c_confide;shenduo/atang/wenjian/null | C3-06 | Act 2 开局站位 | +| `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 开局站位;§3-8 换色 | | `flag_arch_doubt` | bool | C3-06(全选项) | Act 2 总入口 | # 附录 B · 实测维度覆盖核对表(对母本 §4) | 维度 | 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-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-05 #1 → C2-04 #2 → C3-01 #3 ✅ | | 择偶偏好(弱代理信号) | C3-05 #1 → C3-06 #2(均为弱代理信号,画像层低置信处理,须跨章节交叉验证)✅ | @@ -1694,21 +1780,61 @@ C-2049 的燃料质量比预估高,是的,**那扇门开得很顺**。 - 合规定位(ADR-0001):主角方全程为采集/保护者;老鬼的私藏线 是被揭发与制止的对象,主角无任何"带文物牟利"的正向出口; 全文无"游戏/关卡/副本"字样(剧本内称"章节/任务/采集")。 -- 一次告知(ADR-0004):仅在 §1-1 邮件确认页出现一次, - 此后无任何选择点提示"请诚实"。 -- 沉默合法(铁律):四个 V 类讲述点全部写有沉默路径与等尊严回报 - 文案;C1-02 门为沉默者照常开启("不讲,门也开——它只是以另一种 - 方式认得你"),全文无"不开门"式假压力。 +- 一次告知(ADR-0004):仅在 §1-1 邮件确认页出现一次,文案 + 覆盖选择、语音声学特征(即焚、不留声纹)、记忆仅作模型输入、 + 历史不可涂改四条;此后无任何选择点提示"请诚实"。 +- 沉默合法(铁律):三个 V 类讲述点均有沉默路径;C3-02 私密 + 路径选择允许直接离开,三种方式石门均开启。C1-02 门为沉默者照常开启("不讲,门也开——它只是以另一 + 种方式认得你"),变体表含「沉默」档(留白的长廊),沉默者 + 展厅记「空档页」而非声称收到实际记忆;全文无"不开门"式假压力。 - 记忆私密(ADR-0004 叙事落地):§1-3 已向玩家明示"展厅与幻境 只有你能进入";C3-02 幻境仅玩家视角,NPC 只见玩家出神, - 不复述、不评论幻境内容(仅可关心身体状态)。 -- 讲述回报等价:C2-02 四档物质所得严格相等(暗门方位、开启时辰、 - 完整谶语全给,耗时相同),差异仅在情感质地与老人态度演出。 + 不复述、不评论幻境内容(仅可关心身体状态);C3-02 的"不登记 + 入档"边界明确为**不留文本、不入展厅、不作画像输入**, + 只留叙事路径,不将本节行为送入画像,剧本承诺与数据规格一致。 +- 无危险动作示范:主角方选项与正文不含可复现的危险操作—— + 不派体重最轻者探塌巷(C2-03-C 改为巷口安全距离观察)、 + 不以身体抵构件/扑身抱鼎(C3-03 在救护交接后只允许安全线内 + 的记录与组织任务)、无"探法没错""处置正确"式专业背书;险情现场的 + 处置只有撤离、观察与记录(支线册同标准)。 +- 讲述回报等价:C2-02 四档物质所得与服务严格相等(暗门方位、 + 开启时辰、完整谶语全给;老人同点同刻送到门口;等门与搓绳 + 劳动均摊),差异仅在情感质地;C2-05 三档回报互不可替代且 + 核心悬念独家情报不在深暴露档。 - 测量解释收窄:所有"人格信号"为单次采样弱证据(使用公约 「测量纪律」);C3-05/C3-06 的择偶偏好信号标注为弱代理、 - 画像层低置信处理;C2-03 标注单次采样。 + 画像层低置信处理;C2-03 标注单次采样;C3-02 不采语音、 + 不留文本、不输入画像。 - 叙述纪律:旁白只写可见动作与台词,不替玩家或 NPC 的心理下结论; - 两难设计不靠"无伤演出"兜底(C3-03 三线均有真实代价)。 + C3-03 伤员获同等救护,差异落在安全区域的记录任务与遗存损失, + NPC 不做"选得对/处置正确"式裁判(C3-03-B/C 台词已清除)。 - 支线铁律:所有分支差异收敛于变量与文本换色,主线事件序列 对所有玩家一致(三通道原则);自适应支线仅留挂点(§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 撤离演出应保持行动限制,不因玩家路线变化。 diff --git a/docs/script/adaptive-branches.md b/docs/script/adaptive-branches.md index cf79523..d38dbf8 100644 --- a/docs/script/adaptive-branches.md +++ b/docs/script/adaptive-branches.md @@ -29,8 +29,9 @@ - **不致命铁律**:与主线一致——支线内任何选择不造成死亡、重伤、 任务失败或资源崩盘;代价全部落在关系、信息、时间与心绪上。 - **温简在场规则**(挂点 1 专用):若 `wenjian_health = weak` - (C2-01 选 B/C),温简在挂点 1 各支线中**留营**,其台词一律 - 改由通讯频道传出(声音虚弱,同 act1.md §2-5 的处理); + (C2-01 选 B/C/D,见 act1.md 附录 A),温简在挂点 1 各支线中 + **留营**,其台词一律改由通讯频道传出(声音虚弱,同 act1.md §2-5 + 的处理); 在场氛围描写(如 B-VAL-A-01「在记录板边角画着什么」、 B-EMO-A-02「贴着你的耳朵喊」)同步改为频道侧的对应细节 (频道里传来他翻页的声音 / 频道里喊,电流声盖掉半句)。 @@ -144,8 +145,10 @@ - **C**:中继、信标、扇面搜索,一套流程走得又快又静。 阿棠是在你插到第七根信标时自己撞上来的。她看着一路的信标, 又看看你和温简,忽然有点不好意思:「搞这么大阵仗…… - 我就贪看了两眼。」沈铎在频道里听完汇报,说:「流程正确。」 - 顿了顿,补一句:「下次,听她的脚印,别听她的嘴。」 + 我就贪看了两眼。」沈铎在频道里听完汇报,只回了一句: + 「下次,听她的脚印,别听她的嘴。」收队时老鬼蹲在装备箱前, + 一根一根清点信标,咂着嘴记进损耗册:「七根杆子,搜一个 + 贪看了两眼的丫头——这笔账档案馆问起来,你签字。」 ### 重逢(三后续汇合,无分支) @@ -210,8 +213,9 @@ - **无正确答案论证**:01 三项全是负责任的找法,代价各一—— A 制造了第二个暴露面(沈铎的半拍停顿即代价的显形); B 让她在未知里多待十九分钟(那十九分钟对你是煎熬, - 对她也是);C 动用全队资源应对一个大概率虚警,且"流程正确" - 的评语听着暖,细想是把一个活人拆成了搜索坐标。 + 对她也是);C 动用全队装备应对一个大概率虚警,七根信标的 + 损耗当场进了老鬼的账本——组织一架搜一个人,细想是把 + 一个活人拆成了搜索坐标。 02 四项全是真心的不同形状:A 诚实但让对方欠了债("那我错了嘛" 是她不习惯的低头);B 体面但温简点破了它的隐性成本—— 你的克制可能正是她想翻越的墙;C 亲密且留退路,但"各自认领 @@ -476,7 +480,7 @@ `cb_con_a_aftermath ∈ {a_comfort, b_mirror, c_leave}`。 - 关系值:`rapport.laogui`(01:A+1 / B+2 / C−1 / D+1); 草市氛围 `market_warmth ∈ {normal, cold}`(B 为 cold)。 -- 变量:`flag_water_intel = true`(仅 C,城备水情报,§3-8 +- 变量:`flag_water_intel = true`(仅 C,城备水情报,Act 2 「那一天」推算的佐证素材);`flag_shiling_noticed = true` (仅 D,Act 2 伏笔)。 - **收束接口**:纠纷了结,日头西斜——从草市抄近路抵达宫墟 @@ -488,10 +492,12 @@ ## B-CON 变体 B「官牙」(挂点 2:§3-2 前) -【场景】清晨,宫墟暗门外。你们正准备进安门,一个穿青色官服 -色衣的中年人堵在路口,满脸堆笑地冲老鬼作揖——**鱼牙人**, -替宫里采买的牙人,三天前在城里和老鬼做过一笔"买卖"。 -沈铎今早清点物资,发现档案馆配发的古代侧硬通货—— +【场景】上午,宫墟安门内,下沉庭院。你们刚穿过守墓老人指点的 +暗门(§3-1),正在庭院里整队、准备登台基。背风的坑阵阴影里, +一个穿青色官服色衣的中年人闪出来,满脸堆笑地冲老鬼作揖—— +**鱼牙人**,替宫里采买的牙人,三天前在城里和老鬼做过一笔 +"买卖"。他跟得进来毫不心虚:替宫里跑腿的人,暗门开着的时辰 +他门儿清。沈铎今早清点物资,发现档案馆配发的古代侧硬通货—— **应急金饼,少了一半**。此刻三个人呈一个三角站着, 台基上的采集窗口只有上午。 @@ -534,13 +540,15 @@ ### 各选项后续(CB-CON-B-01) - **A**:鱼牙人的笑挂不住了,从袖里数出金饼,手抖了一下。 - 临走他深深看你一眼:「爷好手段。——就是往后的**消息**, - 爷怕是一条也买不着喽。」老鬼全程没敢吭声,进暗门时 - 才凑过来,小声:「新人,你这刀落得……哥哥我肉疼,但 - 服。」沈铎只说了两个字:「干净。」 + 临走他深深看你一眼:「爷好手段。」掂了掂退回的金饼,忽然 + 又笑开了,「不过买卖人,脸皮厚——宫里若真再有动静,兴许 + 小的还敢再吆喝一嗓子,就是价码嘛……得重新议。」 + 老鬼全程没敢吭声,往台基走时才凑过来,小声:「新人, + 你这刀落得……哥哥我肉疼,但服。」沈铎只说了两个字: + 「干净。」 - **B**:沈铎盯了你两秒,收回目光:「上午的窗口,不等人。」 - 鱼牙人识趣地溜了。整个上午老鬼干活格外卖力。收工时 - 沈铎对你说:「面子给了。里子呢?」——这句问话没有 + 鱼牙人识趣地退到暗门方向。老鬼赶紧低头清点装备,沈铎 + 当场问你:「面子给了。里子呢?」——这句问话没有 选项,悬在你头上,带回队里(Act 2 沈铎线的一粒沙)。 - **C**:鱼牙人眼珠转了转,一拍大腿:「爷是行家!这么着—— 小的这条,**能核**:宫里这几夜往台基上搬东西。**沉箱子 @@ -557,7 +565,7 @@ ### 余波选择点 -> 【进暗门前,鱼牙人(各线统一,他溜之前)最后一个作揖, +> 【向台基合拢前,鱼牙人(各线统一,他退走之前)最后一个作揖, > 作到一半,忽然用只有你能听见的音量说:】 **鱼牙人**:爷,小的小本买卖,混口饭吃。往后……宫里若再有 @@ -600,8 +608,10 @@ 对灰色渠道的私态:留线 / 上交 / 断线——是 C1-04「接不接 老鬼的话」的换皮复测,但压力升级:这次递钩子的是陌生人, 没有"队友"这层遮羞布。 -- **无正确答案论证**:01 四项——A 最彻底但永远关上了情报的 - 门(末路之城里,情报是比金饼硬的通货);B 顾全了队,但 +- **无正确答案论证**:01 四项——A 最彻底,当场堵死了这笔买卖, + 鱼牙人记住了"这位爷不好糊弄";他临走自己留了句"兴许还敢 + 再吆喝",可再开腔的价码与真假,都得重新议过(末路之城里, + 情报是比金饼硬的通货);B 顾全了队,但 沈铎那句「里子呢」明示:搁置不是解决,是把账单延后且 加了利息;C 拿到真情报,但你等于追认了这笔违规买卖的 合法性——老鬼学到了"先斩后奏可以洗白";D 最严密,可 @@ -620,8 +630,9 @@ 情报,**面向未来场景生效**:于 §3-2 台基箱痕处应验换色, 见收束接口);`flag_fish_channel = true` (仅 02-A,Act 2 暗线入口)。 -- **收束接口**:纠纷了结,进暗门,穿过庭院,向台基合拢, - 进 §3-2,主线零改动。情报只许面向**未来**场景生效—— +- **收束接口**:纠纷了结,鱼牙人原路退出暗门,你们在庭院里 + 重新整队,向台基合拢,进 §3-2,主线零改动。情报只许面向 + **未来**场景生效—— 若持 `flag_palace_crates`,§3-2【场景】的台基特写追加 换色句:「台基的夯土台面上,有一道道新鲜的箱底压痕和 拖拽的辙印,从暗门方向一直拖到空璧座前。鱼牙人的话 @@ -635,11 +646,17 @@ # 支线 B-EMO「无常」· 情绪稳定性 > **触发条件**:章末路由中「情绪稳定性」`confidence < 0.6`。 -> **测量目标**:主线测点(C2-02/C3-02/C3-03)测的是**有铺垫的** -> 高压(讲述、幻境、两难),本支线补**零铺垫的突发**——没有时间 -> 权衡,只有反应。核心数据是行为层的:计时选择点的决策耗时、 -> 是否改选、高压段落中是否仍读完环境描述(CONTEXT 辅助机制)。 +> **测量目标**:主线 C2-02 的讲述只提供一次弱证据;C3-02 已 +> 隐私隔离、C3-03 只测价值观,本支线补突发现场的任务分配观察。 +> 计时点只记录反应行为,不能把反应快慢、是否改选或是否读完 +> 环境描述直接解释成稳定的情绪特征。 > 选项本身两体面两难堪,与主线同标准。 +> **计时与打断规格(工程)**:计时自选项界面完整呈现起算; +> 超时走合法超时路径;外部打断(来电、切后台)标注 +> `interrupted`,决策耗时不计入分析,若恰在计时结束时仍走合法 +> 超时路径;「未读完环境文本即选择」只记行为事实, +> 不单独定罪。反应时数据仅作维度置信度的行为底噪, +> **不直接映射情绪稳定性分值**。 ## B-EMO 变体 A「沙暴」(挂点 1:§2-4 后) @@ -650,120 +667,112 @@ 【正文】 -**沈铎**(吼,风撕掉一半音量):沙暴!**就近掩体!** +**沈铎**(吼,风撕掉一半音量):沙暴!**原地停,清点人数!** -> 【视野里两处可躲:三十步外,一座塌了半边的民居,门洞黑着, -> 剩半面墙还立着;一百五十步外,主街的石坊,四根整石柱, -> 看着就稳——但要顶着风跑一百五十步。】 +> 【视野里两处可能的遮挡:近处半塌的民居、远处主街的石坊。 +> 两处结构都未经确认,此刻看不清通路。沈铎已安排队员在现有 +> 警戒线内避风、清点人数;任何转移都由他在能见度恢复后执行。】 -**阿棠**(把采集包顶在头上):民居近!!跑哪个?! +**阿棠**(把采集包挡在身前):民居近,可那半面墙还在晃。 -**沈铎**:石坊结构稳—— +**沈铎**:石坊远,通路看不清。都先别动。 -**老鬼**(已经抱头蹲下):哎哟我的祖宗,近的先躲啊! +**老鬼**(已经蹲下):哎哟我的祖宗,先点人,别少一个! **温简**(什么都没说,看你。所有人都在看你。) > 【选项(CB-EMO-A-01)· 计时 10 秒,界面有风声音效渐强。 -> 超时未选 = 跟最近的人跑(合法路径,记 `follow`), -> 剧本按「民居」线收束,无惩罚。】 +> 超时未选=继续听沈铎指挥清点(合法路径,记 `follow`), +> 剧本按 A 的清点线收束,无惩罚。】 > -> **A.「民居!近就是生——跑!」** +> **A.「先点人。阿棠和老鬼数装备,我跟沈铎对人数。」** +> (把未知拆成一个个能核实的名字) > -> **B.「石坊!塌了半边的墙不叫墙——顶风跑,别停!」** +> **B.「先报坐标。把能看见的两处遮挡记在图上,等风缓再走。」** +> (把行动推迟,用位置与环境信息换确定性) > -> **C.「都不跑!物资箱围圈,就地蹲下——风暴里跑动的才是靶子!」** -> (不动,赌建筑区能碎掉风势) +> **C.「先安顿队伍。谁也别往断墙跑,留在警戒线内等沈铎调度。」** +> (暂缓记录,将体力留给可能需要帮助的队员) ### 各选项后续(CB-EMO-A-01) -- **A**:你们连滚带爬撞进门洞。身后,一块悬着的椽子被风 - 掰下来,擦着你的肩砸在地上(轻伤演出:肩头淤青)。 - 阿棠盯着那块椽子看了两秒,声音有点飘:「哎…… - 差一步。」你替她说了她没说出口的话:「差一步也是**没到**。」 -- **B**:一百五十步像一里。风从背后推你,又迎面撞你。 - 冲到石坊底下时,五个人叠着喘,老鬼喘得最响,喘着喘着 - 笑了:「活着……真好……」沈铎第一个平复下来,开始点人 - 点物——点完,他的目光在你身上停了一拍:「赌对了。 - 下次赌之前,先喊一声。」 -- **C**:你们用物资箱和身体围出一个背风的窝。沙从箱缝 - 往里灌,先是脚踝,再是小腿。谁都不说话——说话吃沙。 - 半个时辰后风势稍缓,你们从半尺深的沙里把自己拔出来, - 清点物资又花了半个时辰。沈铎的评价是:「难看。但齐全。」 - 阿棠呸着嘴里的沙:「下次还是跑吧……」 +- **A**:名字核到最后一个时,阿棠没应——不是走散,她被风声 + 盖住了。你又喊一遍,她举起手。人齐了,最远那道遮挡仍看不清。 + 沈铎把人数写进记录板:「先记人,别替风猜路。」 +- **B**:你在现有警戒线内标出民居、石坊和队伍坐标。风卷过来, + 石坊一度从视野里消失;地图留着,现场却还需重新确认。 + 老鬼看了一眼图:「画得再准,也得等这阵过去。」 +- **C**:你请队员暂放物资,温简在通讯里报平安,沈铎继续 + 清点。没人离开警戒线;一箱记录材料被沙埋了半截,得等风停 + 才能找齐。阿棠拍拍你的胳膊:「人都在,东西慢慢找。」 ### 风暴眼中(三后续汇合) -> 【风最盛的时候,掩体外忽然传来一声——**像有人在喊**。 +> 【风最盛的时候,警戒线外忽然传来一声——**像有人在喊**。 > 长长的,被风扯碎的,人嗓子一样的声音。也可能只是风 > 穿过某段断墙。风声里,分辨不出来。】 -**温简**(贴着你的耳朵喊):风过断墙……多半是这个。 +**温简**(通讯频道里喊):风过断墙……多半是这个。 **阿棠**(脸色发白):多半?!**万一是人呢?!** > 【选项(CB-EMO-A-02)· 无计时,但风声循环渐强, > 界面不提示"请快选",营造自然催促】 > -> **A.「我出去看一眼。万一是人——听不见这句'万一',** -> **我往后都睡不安稳。」** -> (冲出去,呛一嘴沙,空手而归) +> **A.「把声音来向再听一遍,和地图对上。万一是人,** +> **我们至少知道从哪里找。」** +> (听不清,也不愿把疑点放过) > -> **B.「谁都不许出。掩体规矩:风暴里,出去的人才是 -> 需要被救的人。」** -> (摁住所有人,包括自己) +> **B.「先保证队伍都在。等风缓再查,眼下谁也别离开。」** +> (暂不追查声音,优先守住现场人员) > -> **C.「绳子。一头拴石柱(或:门框),一头拴我。** -> **我探半个身子出去——有情况拉我回来。」** -> (用风险管理去够那个「万一」) +> **C.「先报方位,试着在警戒线内呼叫;等风缓再去核实。** +> **谁也不要为一个听不清的声音冲进风里。」** +> (保持联络,把核查延后到能安全接近时) ### 各选项后续(CB-EMO-A-02) -- **A**:你冲出去三十步,风把你摁着摩擦。没有人。只有一段 - 断墙,墙洞的形状像一张嘴,风一过就"喊"。你眯着眼回来, - 吐了半天的沙。阿棠给你拍背,拍得极重:「下次这种活儿, - 轮班!」——她没说你做得对,也没说错。那声"喊"还在 - 风里一阵一阵的,回程的路上,谁都没提它。 -- **B**:那声"喊"又响了两回,一回比一回长。没人动。阿棠 - 把脸埋在膝盖里。风停后你们去看了:是一段断墙,墙洞 - 像嘴。阿棠站在那墙洞前看了很久,说:「幸亏。」 - 顿了顿,又说:「**万一**呢。」——这两个字她念得很轻, - 风把它带走了半句。 -- **C**:绳头在沈铎手里,你探出半个身子。沙打得脸生疼。 - 你看见了:断墙,墙洞,风。你缩回来,解绳子的时候 - 手有点抖——不是怕,是风真的很大。沈铎收绳时说: - 「处置规范。」阿棠补一句:「就是看着吓人。」温简最后 - 开口,像自言自语:「听见了,去找了,找到了。**这事 - 就过去了。**」说完他把记录板合上,风恰好在这时 - 小了一档。 +- **A**:你在警戒线内记下两次声音来向;一阵风又把方位 + 搅乱。阿棠问:「听清了吗?」你摇头,她便把地图递过来。 + 图上有一道断墙,但「没听清」不等于「没有人」—— + 这几个字一直跟着你们等到风停。 +- **B**:那声"喊"又响了两回,一回比一回长。没人离开 + 警戒线。阿棠抱膝坐着,问:「要是真有人呢?」沈铎记下 + 声音方向,答:「风缓后查。」她点头,没再说话。 +- **C**:你们留在警戒线内交替呼叫并记录声音来向;频道没有回应。 + 沈铎写下方位,说:「风停再去。」阿棠瞥一眼风口: + 「要是真有人,这段等着的工夫也够难熬。」温简翻开记录板: + 「有来向,没身份。先记到这里。」没人知道那声音究竟是什么。 + +### 风停(三后续汇合,无分支) + +> 【风势缓下来后,沈铎按现场规程安排核查,队伍只在他确认 +> 通路后移动。远处那声像人喊的声音来自断墙上的孔洞; +> 此前谁都没有足够证据断言。众人记下位置,再清点物资, +> 记录箱被找回,黄昏前继续向观星台行进。】 ### 选项设计说明(CB-EMO-A-01 / A-02) -- **人格信号**:情绪稳定性(换皮复测:C3-03 是"两难高压", - 此处是"无常高压")。核心数据为行为层:01 的决策耗时与 - 超时路径(`follow`)、02 选择前的犹豫时长、是否改选、 - 高压段环境文本的阅读完成度。选项层信号:01 是风险直觉的 - 裸态(距离/结构/不动三种本能);02 是"不确定威胁下的 - 行动阈值"——冲动求证 / 规则自控 / 验证式管理。 -- **无正确答案论证**:01 三线均存活且代价各一(A 的椽子、 - B 的"赌对了下次先喊"、C 的半个时辰与一嘴沙),沈铎的 - 三句评价刻意等值——没有任何一条被写成"正确处置"。 - 02 是本支线真正的测量眼:三项里**没有"对"的答案**, - 因为墙洞里本来就没有人——A 的代价是白呛的沙和阿棠 - 被吓的那一下;B 的代价是"万一呢"三个字,剧本明示它会 - 跟着你;C 看似最优,但温简那句"这事就过去了"是三后续 - 中唯一的闭环台词,恰恰反衬出:**闭环也是用风险买来的**, - 绳头若不在沈铎手里呢?三项后续的情绪余量被刻意拉平: - 都安心了一半,都悬着一半。 +- **人格信号**:突发事件里的任务分配(点人 / 标位 / 安顿) + 与不确定声音下的信息处理(重听 / 等待 / 呼叫),均仅为 + 单次弱证据。两个计时点的反应时、超时和阅读进度只记行为, + 不直接映射情绪稳定性分值;被打断时不计。 +- **无正确答案论证**:01 的 A 先确认人员,却暂失位置记录; + B 标位留图,人数需队长确认;C 安顿队员,记录箱要风停后 + 寻回。所有路线都留在安全线内,不给冒险抢行动的回报。 + 02 三项都不能在风中确证声音来源:A 多留一条听觉线索, + B 守着队伍,C 发出呼叫并留方向记录;真正的核查均在风缓 + 且通路由沈铎确认之后。对"万一"的分歧是心理余味, + 不是险情正确答案。 ### 分支后果(CB-EMO-A) -- 变量:`cb_emo_a_shelter ∈ {a_house, b_arch, c_huddle, follow}`; - `cb_emo_a_cry ∈ {a_rush, b_hold, c_tether}`。 +- 变量:`cb_emo_a_shelter ∈ {a_count, b_map, c_shelter, follow}`; + `cb_emo_a_cry ∈ {a_look, b_hold, c_call}`。 - 变量:`cb_emo_a_rt1 / cb_emo_a_rt2`(两个选择点的决策耗时, 毫秒,供维度置信度计算)。 -- 状态:肩头淤青(仅 A 线,演出细节,无机制影响)。 -- **收束接口**:风停,浮沙被刮走整整一层——主街中段的地面 +- 状态:三线无新增伤情;损耗仅为记录材料的清点时间。 +- **收束接口**:浮沙被刮走整整一层——主街中段的地面 露出一截向下的石阶,和一方圆形基座的边缘。阿棠用袖子 擦着基座上的刻纹,声音低下来:「哎……这纹路,观星台。 它这是——**给咱们开门呢**?」被风暴"打扫"过的城替你 @@ -791,90 +800,80 @@ **沈铎**(极低,极慢):谁。也。别。动。 -**老鬼**(脸白了):祖宗诶……这地方的东西,**死了上千年了 -怎么还上班呢**…… +**老鬼**(脸白了):祖宗诶……这地方的东西,**不知道埋了多少 +年了怎么还上班呢**…… **温简**(蹲下,远远观察压板的边缘,声音是全队唯一没变调的): 沉速很慢。非常慢。——它在动,但它**不着急**。 -> 【选项(CB-EMO-B-01)· 计时 8 秒。超时未选 = 阿棠自行 -> 数了三声跳开(合法路径,记 `self_rescue`,按 A 线收束, -> 追加一句她的台词「等你们裁完,我腿都站麻了」)。】 +> 【选项(CB-EMO-B-01)· 计时 8 秒。超时未选=沈铎按既定 +> 排险预案接管(合法路径,记 `self_rescue`,按 A 线收束), +> 不把等待误读为不稳定。阿棠的撤离始终由受训人员处置。】 > -> **A.「阿棠,听我数。三、二、一——跳!」** -> (快刀:赌它只是一次触发,不赌它的机制) +> **A.「先通知沈铎,按预案撤出阿棠;其余人退出阵列。」** +> (先把现场留给受训人员,暂不采集) > -> **B.「别跳。谁知道跳开了它弹起来会带出什么——换人踩! -> 谁体重跟她最接近?平移换脚,再慢慢卸。」** -> (置换:稳字当头,但赌桌上多坐一个人) +> **B.「把观察到的沉速与缝宽报给沈铎;我去清空外围。」** +> (给排险人员信息,把其余队员带离) > -> **C.「都别碰她。老鬼,撬杆;温简,绳——把她原地固定住,** -> **我们拆这块板。要解就解透。」** -> (程序化排险:最慢,但每一步都可逆) +> **C.「全员退到警戒线,远距留影,排险交沈铎处置。」** +> (一边记录一边隔离,放弃近处细节) ### 各选项后续(CB-EMO-B-01) -- **A**:跳。压板在她身后弹起半寸,停住,像打了个哈欠。 - 什么都没发生。阿棠退开三步,回头瞪着那块板,半天: - 「……就这?」沈铎没放松,他盯着的是**坑壁那道缝**—— - 它正在缓缓合拢。方才那一眼里,有人看见了缝里的东西: - 成排的、一人高的影子。 -- **B**:换脚。全队体重和她最接近的是温简(若 C2-05 - 玩家与他有同行约定,追加一句他的低语:「正好,我步子慢, - 站得稳。」)。平移、换脚、阿棠脱险——然后温简一个人 - 站在板上,站了全队最漫长的三分钟,直到沈铎用石块把 - 板缘楔死。他下来时手心全是汗,还开了个玩笑:「现在我 - 知道绳头是什么滋味了。」(C2-05 的他,懂的人自然懂。) -- **C**:撬杆、绳套、楔石,一套流程走了十分钟。拆到最后, - 沈铎把整块压板掀开——所有人都看见了:**板下的机簧, - 早就锈死成了一块铁疙瘩**。它不知道多少年前就死了。 - 那一声「咔」,是阿棠踩裂了板面;坑壁滑动,是地基自然 - 沉降。两件死事,撞在同一秒,演了一场活的。 - 阿棠一屁股坐在地上,笑出了眼泪。沈铎蹲在锈死的机簧前 - 看了很久,说了全支线最重要的一句台词: - 「都记住。**死机关比活机关吓人——因为你不知道它死没死透。 - 这座城里全是这种东西。**」 +- **A**:沈铎立即接管,其他队员退到警戒线。阿棠在受训人员 + 协助下安全离开;你没看清压板沉降的过程。她看见你手里的 + 空记录板,问:「刚才那一下,真有东西动了吗?」 +- **B**:你报告读数后清空外围,沈铎接管排险。阿棠安全撤出; + 你在外围听着频道里一段段报平安,直到最后一声才松开手。 + 坑壁裂缝在视野之外开到最宽,没人及时拍下全貌。 +- **C**:你们退到警戒线外留影;沈铎与受训接应人员按既定预案 + 带阿棠撤出。镜头拍到坑壁开缝的瞬间,却因距离太远, + 板面的刻痕看不清。阿棠看着那张照片:「原来刚才 + 我站的是这里。」 ### 缝中之物(三后续汇合) -> 【坑壁的缝还在缓缓合拢。【C 线换色:被掀开的压板斜在一边, -> 锈死的机簧摊开在日光下——所有人看一眼那块死铁,再看一眼 -> 那道活着的缝,谁都不说话。】缝里的东西, -> 这一眼谁都看清了:成排的**陶范**——铸鼎的模子,一人来高, -> 内壁上刻着纹样,和台基上那组七鼎六簋的纹样,**一模一样**。 -> 范面有使用痕。有人在宫墟里,用这些范,铸过礼器。】 +> 【坑壁的缝开到最宽,又缓缓合拢。阿棠安全撤出后, +> 大家在警戒线外把各自见到的片段拼成同一张记录:缝里有成排 +> 的**陶范**——铸鼎的模子,一人来高,内壁的纹样与台基上 +> 七鼎六簋相似;缝口下方的机件**锈蚀严重**。这些观察不足以 +> 判定机关失效,需等待专业排险后确认。】 **温简**:范是耗材,铸一器毁一范。整排整排地留在这儿—— (停顿)说明铸完那一批之后,**就再没有下一批了**。 +**沈铎**(看着那道正在合拢的缝,声音沉下去):都记住。 +**别拿锈色猜它死没死透。今天先记,不去试。** + +**阿棠**(一屁股坐在安全线的土坎上,笑出了眼泪):吓死 +我了……结果到现在也不知道它是死是活。 + > 【选项(CB-EMO-B-02)· 缝合拢中,无计时,进度条即缝宽】 > -> **A.「拓包给我——缝口外沿这方范还够得着。人就站在这儿,** -> **探臂抢半方纹出来。」** -> (人不进缝:在手臂能够到的最后一寸上,把证据抢出来) +> **A.「能记多少记多少:留在安全线内把远处可见的纹样** +> **拍下来——它合它的,我们记我们的。」** +> (抢记录:在安全线内,把带得走的都带走) > -> **B.「谁也不进。记录位置、朝向、缝宽。结构在动,命只有 -> 一条——它在这儿一千年了,跑不了。」** +> **B.「拍两张全景就够,标桩记位。它想合就让它合——** +> **账先欠着,城跑不了。」** +> (放行:信息不灭失,只是延期) > -> **C.「撬杆别住缝口!别住了再拓——(撬杆崩断一根)……** -> **退出来。它不想开,不勉强。」** +> **C.「都收手,别拍了——它关门的事,就让它关完。」** +> (罢手:给这座城留一点自己的秘密) ### 各选项后续(CB-EMO-B-02) -- **A**:你站在缝外,探臂把拓包按上缝口外沿那方范。手臂的 - 影子在缝口一寸寸变窄,夯土屑擦着袖口簌簌往下掉—— - 拓纸揭下来,半方纹。够了。沈铎伸手把你往后带了半步: - 「到手了,就退。」阿棠凑过来看拓片:「就半方?…… - 行,半方也是抢回来的。」——但这半方纹样,是后来 - §3-2 解谜时她第一个认出来的。 -- **B**:缝在你面前合死,声音轻得像叹气。沈铎在你的 - 记录位置旁钉下一根标记桩:「记在这儿。**账先欠着, - 城跑不了。**」——这句"账先欠着",是 Act 2 重回此地 - 的钩子。 -- **C**:撬杆崩断的那声脆响,把所有人都钉在原地。 - 缝又合了一寸。老鬼把断杆捡起来,摩挲着断口,破天荒 - 说了句不市侩的话:「人家关上门的东西……咱们今天, - 就不硬敲了吧。」 +- **A**:你们留在安全线内,把剩下可见的纹样拍成序列。尘雾 + 一阵阵遮住镜头;温简喊停时,最后几张已只有模糊轮廓。 + 影像有用,但不够清:待会儿上台基辨纹,阿棠要对到第三件 + 才敢拍板「就是这个」。 +- **B**:沈铎在安全线外的图志上标出方位:「记在这儿。 + **账先欠着,城跑不了。**」 +- **C**:相机递到手上又被你推了回去。缝合拢的声音很轻, + 像叹气。老鬼把三脚架收了,摩挲着架腿,破天荒说了句 + 不市侩的话:「人家关上门的东西……咱们今天,就不硬 + 敲了吧。」 ### 选项设计说明(CB-EMO-B-01 / B-02) @@ -882,28 +881,23 @@ 02 紧迫感下的机会成本直觉)+ 风险偏好底噪(02 明显, 供一致性检验,不主测)。行为数据同变体 A:决策耗时、 超时路径、改选、文本阅读完成度。 -- **无正确答案论证**:01 的关键设计是**C 线揭示机簧早已 - 锈死**——三线玩家事后都会听到「死机关比活机关吓人」 - 这句台词,于是 A 的果敢、B 的稳妥、C 的严谨,在真相 - 面前**全部失去"对错"的坐标**:你处置的从来不是机关, - 是你自己对未知的想象。这是本支线作为剧情的独立成立点 - (虚惊一场,而虚惊教会全队一件事),也是无正确答案铁律 - 的极致形态:连"危险"本身都是不确定的,何来正确反应。 - 02 三项配平:A 得半方拓纹(§3-2 的直接助攻)而贴着 - 缝沿冒了一次汗——注意 A 的动作设计为「人不进缝」, - 抢的是证据不是险,果敢的信号不变; - B 一无所获而得沈铎一句"账先欠着"(Act 2 钩子,信息 - 并未灭失,只是延期);C 两头落空却换来老鬼全支线唯一 - 一句真心话——有些收获专门开给"肯罢手"的人。 +- **无正确答案论证**:01 三线均由受训人员完成排险,玩家只决定告知、隔离和远距 + 记录的优先顺序;锈色无法证明机关已失效。A 暂失读数, + B 错过缝宽全貌,C 远距影像缺板面细节。这些后果不拿 + 人身风险配平,也不按计时长短断言情绪稳定性。 + 02 三项配平:A 把能带走的都带走,但影像清晰度打了折 + (§3-2 认纹样要多认两件);B 一无所拍而得沈铎一句 + "账先欠着"(信息延期,不灭失);C 两头落空却换来老鬼 + 全支线唯一一句真心话——有些收获专门开给"肯罢手"的人。 ### 分支后果(CB-EMO-B) -- 变量:`cb_emo_b_plate ∈ {a_jump, b_swap, c_dismantle, self_rescue}`; - `cb_emo_b_vent ∈ {a_rubbing, b_mark, c_wedge}`; +- 变量:`cb_emo_b_plate ∈ {a_alert, b_relay, c_record, self_rescue}`; + `cb_emo_b_vent ∈ {a_record, b_mark, c_leave}`; `cb_emo_b_rt1 / cb_emo_b_rt2`(决策耗时,毫秒)。 - 变量:`flag_mould_rubbing = true`(仅 02-A,§3-2 阿棠 - 认出范纹的换色依据);`flag_sealed_vault = true`(02-B/C, - Act 2 重回钩子)。 + 认出范纹的换色依据——影像版);`flag_sealed_vault = true` + (02-B/C,Act 2 重回钩子)。 - 关系值:`rapport.wenjian`(01-B:+1);`rapport.atang` (02-A:+1)。 - **收束接口**:陶范的纹样与「铸完那一批,再没有下一批」 @@ -911,7 +905,7 @@ 全队向台基合拢,进 §3-2。若持 `flag_mould_rubbing`, §3-2 正文追加换色句:阿棠指着鼎纹——「这纹样我认得! 缝里的范,就是这个!」主线其余零改动。玩家感知: - 采集时踩了一脚虚惊,虚惊里瞥见的东西,上台基正好用上。 + 采集时遇到未知沉降;站在警戒线外留下的影像,上台基正好用上。 --- --- @@ -920,9 +914,12 @@ > **触发条件**:章末路由中「价值观」`confidence < 0.6`。 > **测量目标**:主线价值观测点(C2-01 分药、C3-03 救人救物) -> 全部是**队内/文明尺度**的分配,本支线换皮为**对陌生他者**—— -> 对方与你无任何关系、无任何回报能力、且永不再见。 -> 对"用不上的善意"的定价,是价值观最裸的切片。 +> 全部是**队内/文明尺度**的分配,本支线换皮为**对陌生他者**的 +> 分配——A 变体是行路偶遇的母子(一次性照面),B 变体是有一面 +> 之缘的守墓老人祖孙。两变体的对象都可能回赠信息或物件(末路 +> 之人有末路之人的礼数),测量目标收窄为**稀缺资源下的分配 +> 姿态与裁断方式**,不声称隔离互惠预期;互惠倾向另由主线 +> C2-05 主测。 ## B-VAL 变体 A「半袋水」(挂点 1:§2-4 后) @@ -930,6 +927,9 @@ 井沿边蜷着一个妇人和一个五六岁的孩子。妇人看见你们,第一眼 看的不是你们的脸,是**水囊**。她看了孩子一眼,才慢慢站起来—— 没有伸手。她把手腕上一只银镯褪下来,摊在掌心,举到你们面前。 +里坊外的官道上有零星人影往城门方向挪——城门楼那头,隐隐飘来 +一缕熬粥的米气。阿棠顺着看了一眼:「早上路过,南门里侧的 +施粥棚开着呢,还发饼。排队的都是妇幼。」 【正文】 @@ -972,18 +972,23 @@ 孩子。孩子喝了一半,把杯子举到她嘴边,她抿了一小口, 就推回去。一来一回。走出去半条街,阿棠忽然说: 「哎,你们数没数,那杯水转了四圈。」没人接话。 - 队里的水一两没少。 + ——一杯水,四个来回;水囊的水位,浅了浅浅一格。 - **C**:妇人深深看了你一眼——那一眼里没有感激,有一种 被还回了**人**的平静。她把镯子塞进你手里,动作快得 像怕你反悔。老鬼凑过来,啧啧:「新人,上道啊。」 - 这句是夸是损,他自个儿笑眯眯地不解释。(镯子后由温简 - 登记为「民间有偿征集品 CW-0322」——合规口径:主角方 - 不取一物,征集品入册。) + 这句是夸是损,他自个儿笑眯眯地不解释。 + 【若 wenjian_health != weak:镯子由温简当场登记为 + 「民间有偿征集品 CW-0322」。】【若 wenjian_health = weak: + 镯子你代记「民间有偿征集品 CW-0322」,回营交温简补录入册。】 - **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)· 账】 > -> **A.「照实记。施水一囊(或:售水两囊 / 支出零)。 -> 记在我名下。」** +> **A.「照实记。给出多少、收了什么,都按实数。记在我名下。」** > -> **B.「记'路上损耗'吧。囊漏了,磕了,都写得通——** -> **这点水,不值得进册子。」** +> **B.「先记到『路上杂项』,备注水和镯子的实数,回营再分类。** +> **别在路边把来龙去脉都写完。」** > > **C.「账是队长的。你定,我签。」** ### 各选项后续(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**:沈铎看了你两秒,提笔,记了"损耗"。合上册子时 - 他说:「账平了。」顿了顿,「**心里那本的自己平。**」 - ——他配合了你的善,也把这份善的分量原样还给了你。 -- **C**:沈铎没推辞,记了"施水一囊,采集员裁断", - 把笔递给你签。你签的时候他说:「推给我记的, - 和照实记的,册子上长一个样。」(停顿)「**不一样 - 的地方,你自己知道在哪儿。**」 + 走路不回头看**。」——这是全章他第一次提家人。顿了顿, + 又补了半句:「就是往后再遇这种事,先说一声再给——账要 + 干净,队也得知道水去哪儿了。」 +- **B**:沈铎看了你两秒,提笔,先记「待分类杂项」及每项 + 实数,既没有把水记作漏损,也没把赠镯写成买卖;回营时 + 还得补分录。合上册子他说:「账平了,手续还欠一笔。」 +- **C**:沈铎没推辞,按实数记了,把笔递给你签。你签的时候 + 他说:「推给我记的,和照实记的,册子上长一个样。」 + (停顿)「**不一样的地方,你自己知道在哪儿。**」 ### 选项设计说明(CB-VAL-A-01 / A-02) - **人格信号**:价值观与人生优先级(换皮复测)。01 测 稀缺下对陌生他者的分配姿态:无条件给予 / 定量施救 / 交易化体面 / 资源守土+信息替代。02 测**事后对规则的 - 姿态**:诚实入账 / 善意做账 / 上交裁断——价值观的 + 姿态**:当场分类 / 先记实数后补分类 / 上交裁断——价值观的 "做账方式"比"给钱多少"更接近长期关系里的日常。 - **无正确答案论证**:01 四项是四种有尊严的活法,代价 全部显性化:A 的代价是全队往后两天的"抿着喝"和阿棠 那句"够吗";B 守住了一切,除了你自己——"那杯水转了 四圈"是它的账;C 最体面周全,但老鬼那句"上道啊" 提醒你:体面经谁的口说出来,就沾谁的味;D 是合法的 - 「守住资源+可靠替代」——施粥处真实在运转,收束时 - 孩子手里那半块饼就是确认,D 测到的是价值观而非信息差。 - D 的代价不在正确性,在温度:四项里只有 D 的善意是 + 「守住资源+可靠替代」——**选择前**粥棚就在运转 + (米气、人流、阿棠的亲眼,见场景),D 测到的是价值观 + 而非信息差;一个时辰后核账段确认母子排到热粥, + 只是迟到的注脚,不承担证明责任。老鬼那句"断了米" + 是三天前的旧闲话,当场被粥气推翻。 + D 的代价不在可靠性,在温度:四项里只有 D 的善意是 凉的——水你没给,路你指了,从里坊到粥棚的那一个 时辰,是孩子趴在阿娘背上自己走完的。**滚烫但让全队 陪绑的善意,与可靠但低功耗的善意,是同一枚价值观的 两面,无高下。** - 02 三项:A 换来沈铎唯一一次提家人(信任的最高规格); - B 的账"平了",但他点破"心里那本自己平";C 看似推卸, - 他照单全收,却把"不一样的地方"还给你——三项里 - 没有人说"你做得对"。 + 02 三项:A 换来沈铎唯一一次提家人,也挨了他半句 + "先说再给";B 的账"平了",但回营得补分录; + C 看似推卸,他照单全收,却把"不一样的地方" + 还给你——三项等重,没有人说"你做得对",也没有人只挨训。 + 账目守恒:四条 01 线的实际支出(一囊/一杯/两囊/零) + 与镯子来路(有偿征集/赠与代收)各按实数派生进 02 的 + 所有核账文案,无一虚构损耗。 ### 分支后果(CB-VAL-A) - 变量:`cb_val_a_water ∈ {a_give, b_cup, c_trade, d_redirect}`; `cb_val_a_ledger ∈ {a_honest, b_smooth, c_defer}`。 - 关系值:`rapport.atang`(A+1);`rapport.laogui`(C+1); - `rapport.shenduo`(02:A+2 / B+1 / C+1)。 -- 变量:`flag_silver_bracelet = true`(C/D,CW-0322 入册, - 记忆展厅陈列);`flag_thirst_days = true`(A,此后两日 - 饮水描写换色)。 -- **收束接口**:时序为——01 处置 → 「路口」段孩子喊话 - (D 线追加粥棚确认,见正文)→ 官道上路边核账 - (CB-VAL-A-02)→ 账毕,日头西斜,黄昏登台,进 §2-5, - 主线零改动(§2-5 场景本就是黄昏)。玩家感知:一件 - 路上的小事,由孩子的眼睛收尾,顺手把你们送上了 + `rapport.shenduo`(02:A+1 / B+1 / C+1)。 +- 变量:`flag_silver_bracelet = true`(C/D,CW-0322 按「有偿 + 征集/赠与代收」分别入册,记忆展厅陈列);`flag_thirst_days = + true`(A 支出一囊/C 支出两囊,此后两日饮水描写换色, + C 线更紧)。 +- **收束接口**:时序为——01 处置 → 「路口」段孩子喊话 → + 官道上路边核账(CB-VAL-A-02;D 线的粥棚确认放在核账前、 + 一个时辰后的岔路口,见正文)→ 账毕,日头西斜,黄昏登台, + 进 §2-5,主线零改动(§2-5 场景本就是黄昏)。玩家感知: + 一件路上的小事,由孩子的眼睛收尾,顺手把你们送上了 主线该去的地方。 --- ## B-VAL 变体 B「守陵人的药」(挂点 2:§3-2 前) -【场景】上午,宫墟安门内。穿过暗门(§3-1)后,台阶尽头 -站着个不该出现在这里的人——**守墓老人**。他不是一个人: -背上驮着个十七八岁的少年,少年一条小腿用布条和草灰 -糊着,布条渗着暗色。老人把少年放下来,动作轻得不像 -他这个年纪。 +【场景】上午,宫墟安门内。穿过暗门(§3-1)后,下最后几级台阶时 +阿棠跳快了两步,落地崴了一下脚踝——她原地转了转脚腕,龇了龇 +牙:"没事没事,走。"台阶尽头站着个不该出现在这里的人—— +**守墓老人**。他不是一个人:背上驮着个十七八岁的少年,少年 +一条小腿用布条和草灰糊着,布条渗着暗色。老人把少年放下来, +动作轻得不像他这个年纪。 【正文】 @@ -1086,7 +1105,8 @@ **温简**(查看伤口,低声):石像生砸的。创面糊着草灰—— 老法子,撑不了多久,会烂。 -**沈铎**(打开物资箱,停住):外伤药,**一份**。 +**沈铎**(打开物资箱,停住):外伤药一份——清创的药粉与 +止痛的丸剂合装为一「份」。 (抬头)规程:这是撤离日的战备药。任何人在撤离日之前 动用,需采集员裁断。 @@ -1104,7 +1124,7 @@ > 万一。清创,上药,我来搭手。」** > > **B.「药动不了。(对老人)城里有大夫——我画路,再给您 -> 备一份金饼,连夜送去,来得及。」** +> 备一份金饼,这就动身送去,来得及。」** > (守战备,用替代方案偿:钱和路,是我们能给的不伤筋 > 动骨的东西) > @@ -1125,25 +1145,28 @@ 「救人」两个字的时候,笔锋比平时重。 - **B**:老人没恼。他点点头:「药有主,理是这个理。」 他弯腰去背少年,少年却回头,解下自己腰里挂着的一 - 小包草药,轻轻放在阿棠脚边(她白日里采集时崴过一下, + 小包草药,轻轻放在阿棠脚边(她今早下台阶崴的那一下, 谁都没在意,他看出来了):「阿姐,这个……敷上,凉。」 - 老人驮着他往城里去——水门里的药铺,半个时辰的路, - 天黑前能来回。走出十几步,少年趴在老人背上,忽然 + 老人驮着他这就动身往城里去——水门里的药铺,半个时辰的 + 路,天黑前能来回。走出十几步,少年趴在老人背上,忽然 回头,冲台基的方向努嘴,把一句话喊给阿棠:「阿姐! 上台别走正台阶——**背阴那面,排水沟边上,有我们的路!**」 老鬼低头看着那包草药,半天没吭声,收摊似的咂了咂嘴: 「哥哥我活这么大,**头一回觉得穷**。」 -- **C**:拆瓶,分药,各用各的。少年的创面清理上药, - 感染这一关过了;止痛的留在队里。他被安置在台阶边的 - 阴凉里等你们收工,疼劲上来时压着声哼,老人给他扇风, - 一下,一下。没人说你的分配不好。也没人再提它。 -- **D**:老人捧着药瓶,看了你很久很久。然后他做了 - 一件全队都没想到的事:他只用了一半,仔仔细细 - 封好剩下的一半,双手还给你:「**药是救命的东西。 - 救命的东西,不能断在你们手里。**」(顿)「娃他爹 - 常说,活人比死人难治——你们这些顾活人的,药 - 得留着。」阿棠在旁边,眼圈忽然红了,扭头去看 - 祭祀坑。 +- **C**:拆开合装,清创粉给了少年,止痛丸留在队里。上药后 + 温简查看创面,只说了一句:「清创及时,这片创面问题不大。」 + ——不打包票。少年被安置在台阶边的阴凉里等你们收工, + 疼劲上来时压着声哼,老人给他扇风,一下,一下。没人说 + 你的分配不好。也没人再提它。 +- **D**:老人捧着药瓶,看了你很久很久。然后他做了一件 + 全队都没想到的事:他只用了一半,仔仔细细封好剩下的 + 一半,双手还给你:「**药是救命的东西。救命的东西, + 不能断在你们手里。**」(顿)「娃他爹常说,活人比死人 + 难治——你们这些顾活人的,药得留着。」阿棠在旁边, + 眼圈忽然红了,扭头去看祭祀坑。沈铎在册子上记:「战备 + 药半份,交守陵人保管——返还。」记完他抬眼看你,只说 + 了一句:「这次他只用了一半。下不为例——这种赌,队里 + 赌不起第二次。」 ### 收束(各线汇合) @@ -1178,11 +1201,13 @@ 收获却是老鬼那句"头一回觉得穷"——剧本用全队最市侩 的嘴,说出守规矩的隐性价格;C 是教科书式正解,于是 剧本让你听一上午的扇子声——分配的"对"与心里的账 - 从此分开记;D 最动人,回报最丰(老人那句台词), - 但注意:你把裁断权交出去的那一刻,**若老人用了全部, - 你也无权说半个字**——信任的收益与风险是同一枚硬币, - 这次只是恰好落在收益面。四后续中老人和少年对玩家 - 的态度被刻意拉平:没有感激涕零,也没有怨怼, + 从此分开记;D 最动人,回报也最厚(老人那句台词与半瓶 + 返还),但代价当场落地:沈铎的"下不为例"立了新规矩—— + 你把裁断权交出去的那一刻,**若老人用了全部,你也无权 + 说半个字**;这次恰好落在收益面,队里因此把这种赌判了 + 死刑。信任的收益与风险是同一枚硬币。四后续中老人和 + 少年对玩家的 + 态度被刻意拉平:没有感激涕零,也没有怨怼, 只有"理是这个理"——末路之人对世道没有奢望, 这让一切"施恩感"无处附着。 @@ -1293,15 +1318,17 @@ 「隹」通「惟」,发语词;「見」通「現」;「徙」,从彳从步, 搬也。而「大火見」不是吉凶,是**纪日**——先秦记大事, 干支之外还要缀一颗星做"天上的公章":星象可对,日子 -才作数,谁也无法事后篡改。八月大火昏见,与星图盘上 -复原的苍龙心宿(C2-04),是同一片天。 +才作数,谁也无法事后篡改。八月大火昏见——此铭是 +§2-04 支线时点的**先行线索**:玩家此刻尚未见过星图盘 +(§2-5 才初见),本支线一律不得以"已解过星图"的口吻 +说话,谜面只立"星象纪时"一层,星图之约由温简的 +解题后台词指向未来。 ### 三档难度提示 1. **一档(拆字)**:「读铭文,先找动词。'王'下令做的那个 动作,落在哪个字上?——'徙',从彳从步。至于'大火', - 别往灶火上想。想想你在星图盘上,刚把什么补回苍龙的 - 心口。」 + 别往灶火上想——天上的『火』,是一颗星的名字。」 2. **二档(给制度)**:「先秦人记大事,怕后世抵赖,干支 记日之外,还要缀一颗星做印证——星象是**天上的公章**, 谁也没法私改。'见'通'现'。八月,大火星黄昏见于南天—— @@ -1313,10 +1340,11 @@ ### 解题后正文 **温简**(把铭文描进记录板,越描越慢):用星象给日子 -盖章……(抬头)你们发现没有——这和星图盘是**一套思路**。 -「火中,而后城安」,等的是星;这段铭文,记的也是星。 -**他们不是在等一个日子,是在等一个天象。天象到了, -事就要做。** +盖章……(抬头)你们发现没有——他们看星,不是看一回两回。 +**这种记法,官署里一定还配着看星的台、量星的盘。** +前头那座观星台……上去,说不定就有他们量「大火」的东西。 +(顿)他们不是在等一个日子,是在等一个天象。天象到了, +事就要做。 **阿棠**(抱着手臂,声音发紧):那"事"……就是把城墙 拆了搬走?好端端的城,拆墙干什么? @@ -1342,7 +1370,9 @@ ### 分支后果(CB-CUL-A) - 变量:`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`(进入者必置, 「徙南垣」铭文情报,Act 2 真相链素材)。 - **收束接口**(解谜线):半小时到,沈铎封道拍照。 @@ -1351,7 +1381,7 @@ 主线零改动。skip 线有独立收束(见上节,绕行抵达, 不共用"爬出断口"动作),两线同一时刻登台。 玩家感知:路上塌了个口子,下去看了一眼 - 千年前的字,上来正好赶上看星星——解谜支线是 + 老祖宗留下的字,上来正好赶上看星星——解谜支线是 主线星象悬念的**注脚**。 --- @@ -1359,10 +1389,8 @@ ## B-CUL 变体 B「俑道」(挂点 2:§3-2 前) 【场景】上午,宫墟安门内,下沉庭院侧壁。一道被新夯土 -封了半幅的**俑道**(§3-1「有人修过路」的直接延续): -数百陶俑列队其间,前排九俑,比其余的高半头,胸口 -各刻一字。守陵人的窄阶(若 B-VAL-B 已触发)或你们的 -探查路线,正从俑道口经过。 +封了半幅的**俑道**:数百陶俑列队其间,前排九俑,比其余的 +高半头,胸口各刻一字。你们的探查路线,正从俑道口经过。 【正文】 @@ -1378,6 +1406,9 @@ ### 谜面(CB-CUL-B-01) 前排九俑胸口刻字:**文、武、成、康、昭、穆、共、懿、孝**。 +九俑虽一字排开,站位却有讲究:单数位(一、三、五、七、九) +的俑微微偏**左**,双数位(二、四、六、八)微微偏**右**—— +从俑道深处望出来,九位先王左右交错,站成隐约的两列。 谜面任务(选项式,两问): @@ -1395,21 +1426,22 @@ **谜底**:第一问 B,第二问 B。 《谥法》:经纬天地曰文,克定祸乱曰武,安民立政曰成, 温柔圣善曰康……一字之谥,盖棺论定——九俑所刻, -是苍梧九代先王的谥号。而「昭」「穆」并见,点出俑道的 -排法:宗庙之制,**左昭右穆,父为昭则子为穆,隔代相望**—— -这条俑道不是随便列队,是按**宗庙昭穆之序**排的。 -苍梧人把宗庙,修进了陵里。 +是苍梧九代先王的谥号。而单凭「昭」「穆」两字并见, +**定不了排法**(它们也是谥法用字)——定它的是**站位**: +单数代偏左、双数代偏右,正合宗庙之制**左昭右穆、 +父昭子穆、隔代同侧相望**的列法。这条俑道不是随便列队, +是按**宗庙昭穆之序**排的。苍梧人把宗庙,修进了陵里。 ### 三档难度提示 1. **一档(找规律)**:「九个字,一字一人,像判词。 - 什么制度,敢用一个字给一位王的一生盖棺?再想: - '昭''穆'两个字埋在'文武功成'堆里,不觉得它们 - 和别的不太一样吗?」 + 什么制度,敢用一个字给一位王的一生盖棺?再退两步 + 看站位——单数位偏左,双数位偏右,左右交错成两列。 + 这个左右,是随便站的吗?」 2. **二档(给制度)**:「《谥法》有言:经纬天地曰文, 克定祸乱曰武。这九个字,都是谥法里的字。至于 昭穆——庙制:左昭右穆,父昭子穆,孙又同祖。 - 它们不是尊号,是**排位的坐标**。」 + 对照站位想想:它们不是尊号,是**排位的坐标**。」 3. **三档(给答案)**:「九俑刻的是九代先王的谥号; '昭''穆'标的是宗庙昭穆之序——这条俑道,是按 宗庙的排法修的。答案:谥号;世代坐标。」 @@ -1441,12 +1473,19 @@ 其二,本支线**不设 skip 前置**——俑道在必经之路上, 但沈铎的「能读就读,读多少是多少」与提示机制共同 兜底,不强迫任何玩家在谜面前停留超过其意愿 - (可随时点"上台基"离开,谜面按提示用尽处理)。 + (可随时点"上台基"离开;**主动离场记 + `cb_cul_b_left = true`,标注为兴趣度信号,不与 + 「提示用尽仍作答」混同**——后者按 0 档解谜计分, + 前者不参与解题力判定)。 ### 分支后果(CB-CUL-B) - 变量:`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`(「第十位先王」悬念, 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 | 依恋类型 | 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 | 冲突处理风格 | 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 | 情绪稳定性 | 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 | @@ -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) | | `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 | 冲突风格维度 | -| `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 暗线 | | `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_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_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_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 钩子 | | `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 | 价值观维度 | | `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 真相链素材 | -| `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 同标准) @@ -1525,13 +1564,25 @@ - **不致命**:全册无死亡、重伤、任务失败;代价仅落在关系、 信息、时间、心绪。 - **合规(ADR-0001)**:主角方全程记录者——墓道不取一物 - (石函不开)、银镯走"有偿征集入册"、对老鬼/行商/牙人的 - 灰色渠道均设"上交/断线"出口且无牟利正向收益;动作设计 - 无危险示范(拓纹人不进缝、风暴探身必系绳、排险走规程)。 + (石函不开)、银镯按性质入册(C 线「有偿征集」/D 线 + 「赠与代收、处置呈档案馆裁定」)、对老鬼/行商/牙人的 + 灰色渠道均设"上交/断线"出口且无牟利正向收益;**无危险 + 动作示范**:全册不进缝、不探臂、不换脚、不楔顶、不拆板、 + 不以身体抵构件——险情现场的处置只有撤离、安全距离观察 + 与记录(B-EMO-A 风暴两问均在警戒线内;B-EMO-B-01 + 将排险交给受训人员,B-EMO-B-02 在安全线内拍摄),且无任何 + "处置规范/流程正确"式的专业背书。 - **三通道一致**:支线差异收敛于变量与换色句;收束接口 保证主线事件序列对所有玩家不变;V 类讲述点零新增, 不挤占每章 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 同行约定),不引用触发点 - 之后的任何主线状态(B-VAL-B 的"战备药"设定即为此改写, - 不依赖 §3-4 才有的 `atang_injury`)。 + (如 `wenjian_health`),不引用触发点之后的任何主线状态 + (B-VAL-B 的"战备药"设定即为此改写,不依赖 §3-4 才有的 + `atang_injury`;B-VAL-B 线阿棠的轻微崴脚在本支线场景内 + 自含前置动作)。 diff --git a/docs/script/independent-review-2026-10-03.md b/docs/script/independent-review-2026-10-03.md new file mode 100644 index 0000000..5981020 --- /dev/null +++ b/docs/script/independent-review-2026-10-03.md @@ -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. 最后统一故事框架、正式剧本、支线册与工程设计,再生成评审包。当前评审包忠实包含母文档,问题不在合成陈旧,而在母文档彼此尚未统一。 diff --git a/docs/story-framework.md b/docs/story-framework.md index a7e668e..38df17d 100644 --- a/docs/story-framework.md +++ b/docs/story-framework.md @@ -7,7 +7,8 @@ 近未来,"时空档案馆"致力于采集即将湮灭的历史现场。档案馆选中玩家成为 **口述历史采集员**:你讲述的真实人生是穿越旅程的燃料。每次任务归来, -档案馆"采集你的记忆";你也可以用自己的人生故事兑换下一段旅程。 +档案馆"采集你的记忆";你也可以用自己的人生故事兑换下一段旅程—— +讲述是燃料,讲述点门前的沉默与留白同样被收下,同样是燃料(沉默合法铁律)。 **设定红线**:主角方永远是**记录与保护者**,不是掠夺者(合规口径,见 ADR-0001)。 @@ -60,15 +61,17 @@ ## 4. Act 1 选择点地图 -> 选择点 ID 规则:`C{章}-{序}`。类型:按钮式(B)/ 语音讲述点(V)。 -> 每章 B 类 5-7 个,V 类 1-2 个。讲述点 30 秒内必须给即时回报。 +> 选择点 ID 规则:`C{章}-{序}`。类型:按钮式(B)/ 语音讲述点(V)/ +> 私密路径(P;C3-02 不录音、不计画像)。 +> 第 1/2 章有 V 类讲述点,第 3 章以 P 类私密路径代替新增讲述; +> 讲述点与私密路径选择均在 30 秒内给即时回报。 ### 第 1 章「登记」(现实 → 档案馆 → 启程) | ID | 类型 | 内容(一行) | 承载维度 | |---|---|---|---| | C1-01 | B | 穿越者身份登记:城市/年龄/性别/职业/学历(同时是匹配硬过滤字段,见 ADR-0003) | —(字段采集) | -| C1-02 | V | **记忆钥匙·离别之门**:"以一段真实的离别祭之" | 依恋类型 #1、自我暴露深度 | +| C1-02 | V | **记忆钥匙·离别之门**:邀请讲述一段真实的离别(沉默亦通行,门照常开,展厅记空档页) | 依恋类型 #1、自我暴露深度 | | C1-03 | B | 馆长问任务动机,四选项无对错(求知/冒险/逃避现实/有人托付) | 价值观 #1 | | C1-04 | B | 初见小队:老鬼提议"带点纪念品回来",无人表态,你接不接话 | 价值观 #2(金钱观)、冲突风格 #1 | | C1-05 | B | 解谜线入门:残碑文字辨认(真实古文字知识,三档难度自适应) | 文化底蕴 #1 | @@ -78,7 +81,7 @@ | ID | 类型 | 内容 | 承载维度 | |---|---|---|---| -| C2-01 | B | 物资分配:药粉只够两人份,三人需要(老鬼主张按"贡献",阿棠主张给最弱的) | 价值观 #3、冲突风格 #2 | +| C2-01 | B | 物资分配:防瘴药两人份(一份=一人七日预防量;病人足量治疗需两份连服——剂量在选择前摊明),三人需要 | 价值观 #3、冲突风格 #2 | | C2-02 | V | **故事换情报·守墓老人**:"讲一个你这辈子最后悔的决定" | 价值观(悔的模式)、情绪稳定性 #1 | | C2-03 | B | 路线分歧:沈铎的稳妥路线 vs 阿棠的直觉捷径,队里目光都落在你身上 | 风险偏好 #2、冲突风格 #3 | | C2-04 | B | 解谜线:星象图复原(真实天文知识) | 文化底蕴 #2 | @@ -90,8 +93,8 @@ | ID | 类型 | 内容 | 承载维度 | |---|---|---|---| | C3-01 | B | 解谜线:礼制还原(以出土器物推断仪式流程) | 文化底蕴 #3 | -| C3-02 | V | **问心幻境**(三通道之通道 2):墓室机关"照见"玩家 C1-02 讲述的记忆意象,通关条件是在幻境中完成告别 | 情绪稳定性 #2、依恋类型 #3 | -| C3-03 | B | 幻境外突发塌方征兆:先救队友 vs 先保住刚出土的核心文物 | 价值观 #5、情绪稳定性 #3 | +| C3-02 | P(私密路径) | **问心幻境**:由 C1-02 已授权的记忆生成玩家私有幻境;玩家可进入、折返或直接退出,石门均开启;本点不保存新增讲述,也不作画像输入 | 叙事事件(不计入人格测量) | +| C3-03 | B | 塌方后伤员已由足够人员稳定救护,玩家只在安全线内选择跟进伤情记录、远距铭文记录或两边交接;不进入活动结构 | 价值观 #5(单次弱证据;反应时不用于情绪评分) | | C3-04 | B | 老鬼私藏事发,沈铎要按规程上报档案馆:你作证 / 求情 / 沉默 | 冲突风格 #5、价值观 #6 | | C3-05 | B | 阿棠对你交付后背:把唯一的逃生绳交给你保管 | 择偶偏好 #1(信任行为) | | C3-06 | B | 章末钩子:你无意听到馆长与未知方通话——档案馆隐瞒了什么。追问馆长 / 按下不表 / 告诉某个队友(选谁) | 依恋类型 #4(求助行为)、择偶偏好 #2 | @@ -101,14 +104,15 @@ ## 5. 记忆钥匙点与预写变体(通道 1) -每个 V 类讲述点配 2-4 个**预写场景变体**,由 Jev 提取的情绪签名选择: +V 类讲述点配预写场景变体;C3-02 复用 C1-02 变体, +不在私密房间另行采集或评分: | 讲述点 | 情绪签名维度 | 预写变体(基调) | |---|---|---| -| C1-02 离别之门 | 温暖×永失 / 悲恸 / 释然 / 怨怼 | "爱被封存"墓室 / "战乱乱冢" / "远行者的空驿" / "被凿毁的合葬龛" | -| C2-02 最后悔的决定 | 自责 / 遗憾但坦然 / 愤怒指向他人 | 老人回赠的情报详细度三档 + 对应夜游场景变体 | -| C2-05 篝火 | 深暴露 / 浅暴露 / 沉默 | 温简后续亲密度分支(他分享档案馆秘密的早晚) | -| C3-02 问心幻境 | 由 C1-02 记忆内容驱动 | 幻境即 BOSS 战:记忆越具体,幻境越丰满(ADR-0004:记不清=幻境模糊,设定自洽) | +| C1-02 离别之门 | 温暖×永失 / 悲恸 / 释然 / 怨怼 / 沉默 | "爱被封存"墓室 / "战乱乱冢" / "远行者的空驿" / "被凿毁的合葬龛" / "留白的长廊" | +| C2-02 最后悔的决定 | 自责 / 遗憾但坦然 / 愤怒指向他人 / 沉默 | 四档情报等量(方位、时辰、完整谶语全给,老人同点同刻送到暗门口);仅老人态度与夜游氛围不同 | +| C2-05 篝火 | 深暴露 / 浅暴露 / 沉默 | 亲密、智识线索、队长背书三种不可替代回报;档案馆疑云由浅暴露线提前听到 | +| C3-02 问心幻境 | 复用 C1-02 情绪变体,沉默玩家为通用雾景 | 私密路径三态均开门;本期不新增语音、不保存室内讲述、不作人格评分 | ## 6. 自适应路由规则(Jev 置信度驱动) diff --git a/scripts/build-adaptive-branches.ts b/scripts/build-adaptive-branches.ts new file mode 100644 index 0000000..a747089 --- /dev/null +++ b/scripts/build-adaptive-branches.ts @@ -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 = { 沈铎: '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 = { + '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}`); diff --git a/scripts/verify-adaptive.ts b/scripts/verify-adaptive.ts new file mode 100644 index 0000000..d8a8c68 --- /dev/null +++ b/scripts/verify-adaptive.ts @@ -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) { + 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 = {}, 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(); +for (const branch of BRANCHES) { + const index = new Map(branch.nodes.map((n) => [n.id, n])); + const reached = new Set(); + function visit(id: string, path = new Set()) { + 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('✓ 共享评分排除身份数据、无效响应不冒充低置信、条件文案不串线'); diff --git a/scripts/verify-live.ts b/scripts/verify-live.ts index 4f3d320..8bc77aa 100644 --- a/scripts/verify-live.ts +++ b/scripts/verify-live.ts @@ -51,7 +51,7 @@ const chapters = [1, 2, 3].map( // --- 1. C1-01 身份抽取(走引擎真实代码路径) --- const store = new MemoryStore(); 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 profile = await engine.extractIdentity(intro); console.log('\n[1] C1-01 身份抽取'); @@ -60,7 +60,7 @@ console.log(' 抽取:', JSON.stringify(profile)); // --- 2. C1-02 情绪签名 → 场景变体(choice 格式实测) --- await engine.confirmIdentity(profile); -while (engine.currentNode().type !== 'narration') engine.advance(); +while (engine.currentNode().type !== 'narration') await engine.advance(); const farewell = '外婆走的那天我在外地考试,没赶上最后一面。后来我每年清明都回去,院子里的桂花树还在。'; let result = await engine.submitNarration(farewell); console.log('\n[2] C1-02 情绪签名 → 场景变体'); diff --git a/scripts/verify.ts b/scripts/verify.ts index 2989f4f..367e2d7 100644 --- a/scripts/verify.ts +++ b/scripts/verify.ts @@ -14,7 +14,8 @@ import { readFileSync } from 'node:fs'; import { CONSENT_VERSION, StoryEngine } from '../src/engine/engine'; import { LocalStorageEventStore } 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 { SqliteEventStore } from '../src/engine/sqliteEventStore'; import type { Chapter, ChoiceNode, IdentityProfile, NarrationNode } from '../src/engine/types'; @@ -37,8 +38,8 @@ const EXPECTED: Record = { 'C2-05': ['attachment', 'reciprocity'], 'C2-06': ['conflict_style', 'values'], 'C3-01': ['culture'], - 'C3-02': ['emotional_stability', 'attachment'], - 'C3-03': ['values', 'emotional_stability'], + 'C3-02': [], + 'C3-03': ['values'], 'C3-04': ['conflict_style', 'values'], 'C3-05': ['mate_preference'], 'C3-06': ['attachment', 'mate_preference'], @@ -115,7 +116,7 @@ for (const [id, dims] of Object.entries(EXPECTED)) { // --- 3. V 类讲述点变体结构 --- 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) { check(n.variants.length >= 2, `${n.pointId}: 有 ${n.variants.length} 个预写场景变体`); 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)'); for (const n of narrationNodes) { 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 追问模板齐全`, ); } +/** 自动遍历解谜点时提交真实两题答案,其他选择点无需行为参数。 */ +function puzzleBehavior(node: ChoiceNode) { + return node.puzzle ? { answers: node.puzzle.questions.map((q) => q.answer), hintsUsed: 0 } : undefined; +} + // --- 4. Mock 模式端到端跑通三章 --- const store = new MemoryStore(); const jev = new MockJevClient(); @@ -139,19 +145,20 @@ let guard = 0; while (!engine.state.finished && guard++ < 200) { const node = engine.currentNode(); 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') { const profile = await engine.extractIdentity('我叫阿黎,29岁,在杭州做互联网产品经理,硕士学历'); await engine.confirmIdentity(profile); } else if (node.type === 'narration') { // C2-05 走"沉默"路径验证合法选项,其余讲述;Mock 检测到缺失维度则回答追问 if ((node as NarrationNode).pointId === 'C2-05') await engine.skipNarration(); + else if ((node as NarrationNode).pointId === 'C3-02') await engine.resolvePrivateIllusion('farewell_done'); else { await engine.submitNarration('(验证用占位讲述)'); if (engine.state.pendingFollowUp) await engine.answerFollowUp('(验证用追问回答)'); } } else { - engine.advance(); + await engine.advance(); } } 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'); 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 问题 --- const criteria = Array.from({ length: 10 }, (_, i) => `${i + 1} 级`); 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') { await engine.confirmIdentity(await engine.extractIdentity('占位自我介绍')); } else if (n.type === 'narration') { + if (n.pointId === 'C3-02') { await engine.resolvePrivateIllusion('farewell_done'); continue; } await engine.submitNarration('(验证用占位讲述)'); if (engine.state.pendingFollowUp) await engine.answerFollowUp('(验证用追问回答)'); } else { - engine.advance(); + await engine.advance(); } } const firstChoice = engine.currentNode() as ChoiceNode; @@ -226,12 +251,12 @@ class FixedNoulJev extends MockJevClient { /** 推进到指定节点(途中选择点取第一项、身份点直接确认、讲述点沉默略过) */ async function walkTo(eng: StoryEngine, nodeId: string) { let g = 0; - while (eng.currentNode().id !== nodeId && g++ < 60) { + while (eng.currentNode().id !== nodeId && g++ < 250) { 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 === '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', '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(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'); 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', ); } -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 可指定选择点的选项 */ async function walkAll(eng: StoryEngine, picker: Record = {}) { @@ -345,17 +370,17 @@ async function walkAll(eng: StoryEngine, picker: Record = {}) { 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)); 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') { await eng.confirmIdentity(await eng.extractIdentity('占位自我介绍')); } 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 { await eng.submitNarration('(验证用占位讲述)'); if (eng.state.pendingFollowUp) await eng.answerFollowUp('(验证用追问回答)'); } } else { - eng.advance(); + await eng.advance(); } } return { seen, rendered, finished: eng.state.finished }; @@ -511,8 +536,24 @@ class FailingJev extends MockJevClient { 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) { + // 主线失败先退出,避免掩盖原有回归。 console.error(`\n${failures} 项校验失败`); process.exit(1); } -console.log('\n全部校验通过'); \ No newline at end of file +console.log('\n全部校验通过'); +await import('./verify-adaptive'); diff --git a/src/App.css b/src/App.css index f01d27e..9a71877 100644 --- a/src/App.css +++ b/src/App.css @@ -91,6 +91,7 @@ } .choice-options { + min-width: 0; display: flex; flex-direction: column; gap: 10px; @@ -98,6 +99,7 @@ } .choice-btn { + overflow-wrap: anywhere; text-align: left; padding: 12px 16px; 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 { display: inline-block; font-size: 12px; diff --git a/src/App.tsx b/src/App.tsx index 244efc7..d0ccfb1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,7 @@ import { useEffect, useMemo, useState } from 'react'; import './App.css'; import { StoryEngine } from './engine/engine'; +import { BRANCHES } from './engine/branches'; import { LocalStorageEventStore } from './engine/eventStore'; import { LocalStorageProgressStore } from './engine/progress'; import { SqliteEventStore, isTauriRuntime } from './engine/sqliteEventStore'; @@ -25,7 +26,7 @@ export default function App() { const store = isTauriRuntime() ? new SqliteEventStore() : new LocalStorageEventStore(); const jev = getJevClient(); 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(() => { diff --git a/src/engine/branches.ts b/src/engine/branches.ts new file mode 100644 index 0000000..bcbf8ff --- /dev/null +++ b/src/engine/branches.ts @@ -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[]; diff --git a/src/engine/branching.ts b/src/engine/branching.ts new file mode 100644 index 0000000..bcb792b --- /dev/null +++ b/src/engine/branching.ts @@ -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 }; +} diff --git a/src/engine/engine.ts b/src/engine/engine.ts index b42324c..51628f0 100644 --- a/src/engine/engine.ts +++ b/src/engine/engine.ts @@ -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 type { EventStore, StoryEvent } from './eventStore'; import { makeEvent } from './eventStore'; @@ -24,6 +26,7 @@ export interface EngineState { playerName: string; /** 全部三章完成后为 true → 进入画像报告 */ finished: boolean; + activeBranchId?: string; } export interface NarrationResult { @@ -82,9 +85,13 @@ export class StoryEngine { private jev: JevClient; private progress?: ProgressStore; private nodeIndex: Map = new Map(); + readonly insertions: BranchPack[] = []; + private branches: BranchPack[]; + private chapterTransition: Promise | 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'); + this.branches = branches; this.chapters = chapters; this.store = store; this.jev = jev; @@ -119,18 +126,15 @@ export class StoryEngine { } /** dialogue / cutscene / chapterEnd 前进一步 */ - advance(): void { + async advance(): Promise { + if (this.chapterTransition) return this.chapterTransition; const node = this.currentNode(); if (node.type === 'dialogue' || node.type === 'cutscene') { - this.state.nodeId = node.next; + this.moveTo(node.next); } else if (node.type === 'chapterEnd') { - if (node.nextChapter) { - this.state.chapterId = node.nextChapter; - this.state.nodeId = this.currentChapter().startNodeId; - this.state.activeVariant = null; - } else { - this.state.finished = true; - } + this.chapterTransition = this.finishChapter(node.nextChapter); + try { await this.chapterTransition; } finally { this.chapterTransition = null; } + return; } else { throw new Error(`advance() 不适用于 ${node.type} 节点`); } @@ -138,13 +142,99 @@ export class StoryEngine { this.persist(); } - /** 进度持久化:每次状态迁移后落盘(真机刷新/热更新后从断点恢复) */ + /** 章末只用本 session 的累计证据决策,故障不阻断主线。 */ + private async finishChapter(nextChapter?: string): Promise { + 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 { this.progress?.save({ sessionId: this.state.sessionId, chapterId: this.state.chapterId, nodeId: this.state.nodeId, 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; const chapter = this.chapters.find((c) => c.id === snapshot.chapterId); 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}`); this.state.sessionId = snapshot.sessionId; this.state.chapterId = snapshot.chapterId; @@ -165,8 +264,8 @@ export class StoryEngine { this.state.activeVariant = null; this.state.pendingFollowUp = null; this.state.finished = false; - const events = await this.store.list(snapshot.sessionId); 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.skipHiddenNodes(); this.persist(); @@ -183,7 +282,7 @@ export class StoryEngine { if (node.type !== 'dialogue' && node.type !== 'cutscene') return; const text = node.type === 'dialogue' ? node.text : node.caption; 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 类选择:落事件并按选项跳转 */ - async choose(optionId: string): Promise { + async choose(optionId: string, behavior?: ChoiceBehavior): Promise { const node = this.currentNode(); 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); if (!option) throw new Error(`未知选项 ${optionId}`); await this.store.append( @@ -211,11 +324,13 @@ export class StoryEngine { pointId: node.pointId, dimensions: node.dimensions, 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)); - this.state.nodeId = option.next; + Object.assign(this.state.flags, choiceFlags(node.pointId, option.id, behavior)); + this.moveTo(option.next); this.skipHiddenNodes(); this.persist(); } @@ -229,6 +344,7 @@ export class StoryEngine { async submitNarration(transcript: string | null): Promise { const node = this.currentNode(); 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 (transcript === null) return this.finalizeNarration(node, null); // Jev 故障(CORS / 网络)时降级:跳过追问环节,剧情继续,不卡死 @@ -331,7 +447,31 @@ export class StoryEngine { await this.store.append(event); Object.assign(this.state.flags, narrationFlags(node.pointId, variant.variantId, transcript)); 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 { + 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.persist(); return { variant, event }; @@ -339,6 +479,8 @@ export class StoryEngine { /** 沉默永远是合法选项(CONTEXT:在什么面前沉默本身也是数据);沉默路径不触发追问 */ async skipNarration(): Promise { + const node = this.currentNode(); + if (node.type === 'narration' && node.pointId === 'C3-02') return this.resolvePrivateIllusion('exit_room'); const r = await this.submitNarration(null); if (!r) throw new Error('unreachable: 沉默路径不触发追问'); return r; @@ -485,8 +627,8 @@ export class StoryEngine { profile, }), ); - this.state.nodeId = node.next; this.state.playerName = profile.name.trim(); + this.moveTo(node.next); this.skipHiddenNodes(); this.persist(); } @@ -522,6 +664,7 @@ export class StoryEngine { const chapter = this.chapters.find((c) => c.id === (chapterId ?? this.state.chapterId)); if (!chapter) throw new Error(`unknown chapter ${chapterId}`); this.state.sessionId = newSessionId(); + this.clearInsertions(); this.state.chapterId = chapter.id; this.state.nodeId = chapter.startNodeId; this.state.activeVariant = null; diff --git a/src/engine/eventStore.ts b/src/engine/eventStore.ts index 5c50f03..df22887 100644 --- a/src/engine/eventStore.ts +++ b/src/engine/eventStore.ts @@ -1,4 +1,4 @@ -import type { DimensionTag, FollowUpDimension, IdentityProfile } from './types'; +import type { ChoiceBehavior, DimensionTag, FollowUpDimension, IdentityProfile } from './types'; /** * 选择/讲述事件存储(ADR-0004:历史不可涂改)。 @@ -17,7 +17,10 @@ export interface StoryEvent { sessionId: string; ts: number; 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' */ pointId: string; dimensions: DimensionTag[]; @@ -29,6 +32,8 @@ export interface StoryEvent { /** narration 事件:选中的场景变体(情绪签名 → variantId) */ variantId?: string; emotionSignature?: string; + /** C3-02 幻境:只记录路径,不保留在室内说的话 */ + narrationMode?: 'farewell_done' | 'farewell_partial' | 'exit_room'; /** followup / followup_skipped 事件:追问针对的缺失维度 */ followUpDimension?: FollowUpDimension; /** 讲述/追问的转写文本(Web Speech API 只给文本,不留音频,符合 ADR-0004 即焚原则;沉默为 null) */ diff --git a/src/engine/flags.ts b/src/engine/flags.ts index aecea32..04f9865 100644 --- a/src/engine/flags.ts +++ b/src/engine/flags.ts @@ -1,4 +1,5 @@ import type { StoryEvent } from './eventStore'; +import type { ChoiceBehavior } from './types'; /** * 分支变量(flag)系统 —— 依据 docs/script/act1.md 附录 A。 @@ -17,15 +18,15 @@ import type { StoryEvent } from './eventStore'; */ export type Flags = Record; -/** 解谜点正确答案(optionId),用于 *_solved 变量 */ +/** 单选解谜点正确答案(optionId);C3-01 按两题行为记录单独聚合。 */ const PUZZLE_ANSWERS: Record = { 'C1-05': 'stele_guardian', 'C2-04': 'heart_xin', - 'C3-01': 'rite_correct', }; /** 选择事件 → 变量赋值(变量名与取值对齐附录 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 = {}; switch (pointId) { 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'; break; case 'C1-05': - case 'C2-04': - case 'C3-01': { + case 'C2-04': { const prefix = pointId.toLowerCase().replace('-', '_'); f[`${prefix}_solved`] = String(optionId === PUZZLE_ANSWERS[pointId]); 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': f.c1_06_landing = optionId; // a_safe / b_precise break; @@ -61,9 +67,9 @@ export function choiceFlags(pointId: string, optionId: string): Flags { break; case 'C3-03': f.c3_03_choice = optionId; - f.atang_injury = optionId === 'a_person' ? 'bruise' : optionId === 'b_relic' ? 'fracture' : 'sprain'; - f.wenjian_injury = optionId === 'c_command' ? 'shoulder' : 'none'; - f.relic_loss = optionId === 'a_person' ? 'one_ding' : 'none'; + f.atang_injury = 'fracture'; + f.relic_loss = optionId === 'c_command' ? 'outer_three' : 'one_ding'; + f.relic_record = optionId === 'b_relic' ? 'full' : 'partial'; break; case 'C3-04': f.c3_04_stance = optionId; @@ -72,7 +78,7 @@ export function choiceFlags(pointId: string, optionId: string): Flags { break; case 'C3-05': 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; case 'C3-06': f.flag_arch_doubt = 'true'; @@ -104,7 +110,9 @@ export function narrationFlags(pointId: string, variantId: string | undefined, t const f: Flags = {}; switch (pointId) { 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)[variantId ?? ''] ?? 'silent'; break; case 'C2-02': { // S3(评审修订):四档物质所得严格相等(方位+时辰+完整谶语全给),flag_prophecy_heard 已废除 @@ -115,11 +123,10 @@ export function narrationFlags(pointId: string, variantId: string | undefined, t case 'C2-05': { const depth = transcript === null ? 'silent' : variantId ? VARIANT_SIGNATURE[variantId] : undefined; 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; } case 'C3-02': - f.c3_02_mode = transcript === null ? 'exit_room' : 'farewell_done'; break; } return f; @@ -129,14 +136,85 @@ export function narrationFlags(pointId: string, variantId: string | undefined, t export function deriveFlags(events: StoryEvent[]): Flags { const flags: Flags = {}; for (const e of events) { - if (e.kind === 'choice' && e.optionId) Object.assign(flags, choiceFlags(e.pointId, e.optionId)); - else if (e.kind === 'narration') Object.assign(flags, narrationFlags(e.pointId, e.variantId, e.transcript ?? null)); + 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)); + 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; } // ---------- 条件文本解析 ---------- +const BRANCH_CHOICES: Record = { + '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; /** 求值单个条件表达式,如 `c2_06_jade = a_public`、`wenjian_health != weak` */ diff --git a/src/engine/progress.ts b/src/engine/progress.ts index 780dcc8..ca170d7 100644 --- a/src/engine/progress.ts +++ b/src/engine/progress.ts @@ -12,6 +12,8 @@ export interface ProgressSnapshot { nodeId: string; /** 分支变量随位置一起持久化,恢复后条件文本渲染不断档 */ flags: Flags; + activeBranchId?: string; + insertions?: string[]; } export interface ProgressStore { diff --git a/src/engine/scoring.ts b/src/engine/scoring.ts new file mode 100644 index 0000000..af383ef --- /dev/null +++ b/src/engine/scoring.ts @@ -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>; + +/** 身份与同意不是人格证据;路由标记只说明采样路径,不作为性格结论。 */ +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 { + 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])); +} diff --git a/src/engine/types.ts b/src/engine/types.ts index ae4b644..db2dbfb 100644 --- a/src/engine/types.ts +++ b/src/engine/types.ts @@ -42,6 +42,9 @@ export interface ChoiceNode extends BaseNode { /** 承载维度标签,如 ["values", "conflict_style"] */ dimensions: DimensionTag[]; options: ChoiceOption[]; + timer?: { seconds: number; optionId: string }; + untimed?: boolean; + puzzle?: { questions: { prompt: string; options: string[]; answer: number }[]; hints: string[]; exitLabel?: string }; } export interface ChoiceOption { @@ -146,9 +149,29 @@ export interface IdentityNode extends BaseNode { export type StoryNode = DialogueNode | ChoiceNode | NarrationNode | CutsceneNode | IdentityNode | ChapterEndNode; export interface Chapter { + estimatedMinutes?: number; id: string; act: number; title: string; startNodeId: string; 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; +} diff --git a/src/services/jev.ts b/src/services/jev.ts index c194540..6276aa9 100644 --- a/src/services/jev.ts +++ b/src/services/jev.ts @@ -59,6 +59,7 @@ export type JevAnswer = }; export interface JevClient { + adaptiveScoring?: boolean; ask(state: unknown, questions: Record): Promise>; } @@ -86,6 +87,7 @@ export class HttpJevClient implements JevClient { headers.Authorization = `Bearer ${this.apiKey}`; } const res = await fetch(url, { + signal: AbortSignal.timeout(15000), method: 'POST', headers, body: JSON.stringify({ model: 'jev-latest', state, questions } satisfies JevRequest), @@ -121,6 +123,7 @@ function prng(seed: number, n: number): number[] { /** 无 key 开发用:确定性伪随机返分(同一输入永远同一输出,输出形状与真实 API 对齐)。 */ export class MockJevClient implements JevClient { + adaptiveScoring = true; async ask(state: unknown, questions: Record): Promise> { const stateKey = JSON.stringify(state) ?? ''; const answers: Record = {}; @@ -177,6 +180,7 @@ export function getJevClient(): JevClient { } else { console.warn('[jev] 未配置 VITE_JEV_PROXY_URL / VITE_TYPESAFE_API_KEY,降级为 MockJevClient(确定性伪随机返分)'); singleton = new MockJevClient(); + singleton.adaptiveScoring = false; } return singleton; } diff --git a/src/ui/StoryPlayer.tsx b/src/ui/StoryPlayer.tsx index b902836..3e33343 100644 --- a/src/ui/StoryPlayer.tsx +++ b/src/ui/StoryPlayer.tsx @@ -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 { ChoiceNode, FollowUpDimension, IdentityNode, IdentityProfile, NarrationNode } from '../engine/types'; import { VoiceInput } from './VoiceInput'; @@ -32,12 +33,24 @@ interface Props { export function StoryPlayer({ engine, onFinish }: Props) { // tick 只是触发重渲染;引擎是唯一事实源 const [tick, setTick] = useState(0); + const [advancing, setAdvancing] = useState(false); + const [error, setError] = useState(''); const rerender = () => setTick((t) => t + 1); void tick; const node = engine.currentNode(); const chapter = engine.currentChapter(); 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 (
{node.type === 'dialogue' && ( -
{ engine.advance(); if (engine.state.finished) onFinish(); else rerender(); }}> +
{node.speaker.slice(0, 1)}
@@ -67,10 +80,21 @@ export function StoryPlayer({ engine, onFinish }: Props) { )} {node.type === 'choice' && ( - engine.renderText(t)} onPick={async (id) => { await engine.choose(id); rerender(); }} /> + engine.renderText(t)} onPick={async (id, behavior) => { await engine.choose(id, behavior); rerender(); }} /> )} - {node.type === 'narration' && ( + {node.type === 'narration' && node.pointId === 'C3-02' && ( +
+
问心之室 · 仅你可见
+

{engine.renderText(node.prompt)}

+

这里不录音、不留讲述文本,也不把室内告别用于画像。

+ + + +
+ )} + + {node.type === 'narration' && node.pointId !== 'C3-02' && ( } {node.type === 'cutscene' && ( -
{ engine.advance(); rerender(); }}> +
▶ {node.video}

{engine.renderText(node.caption)}

点击继续 ▸ @@ -96,41 +120,94 @@ export function StoryPlayer({ engine, onFinish }: Props) {

{chapter.title} · 完

)} + {error &&

{error}

}
); } -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 }) { const [busy, setBusy] = useState(false); + const [error, setError] = useState(''); + const [hints, setHints] = useState(0); + const [answers, setAnswers] = useState(() => 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 (

{renderText(node.prompt)}

+ {node.timer && } + {node.puzzle && <> + {node.puzzle.questions.map((q, i) =>
+ {q.prompt} + {q.options.map((label, j) => )} +
)} + {node.puzzle.hints.slice(0, hints).map((hint, i) =>

{hint}

)} + + }
- {node.options.map((o) => ( + {node.options.filter((o) => o.id !== node.timer?.optionId).map((o) => ( ))}
+ {error &&

{error}

}
); } diff --git a/src/ui/report/ReportPage.tsx b/src/ui/report/ReportPage.tsx index ea8d568..c3e9d38 100644 --- a/src/ui/report/ReportPage.tsx +++ b/src/ui/report/ReportPage.tsx @@ -1,43 +1,18 @@ 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 { 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'; -export interface DimensionScore { - id: string; - label: string; - score: number; - max: number; - confidence: number; -} - interface Props { store: EventStore; jev: JevClient; 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 */ export function ReportPage({ store, jev, onReplay }: Props) { const [scores, setScores] = useState(null); @@ -48,35 +23,14 @@ export function ReportPage({ store, jev, onReplay }: Props) { useEffect(() => { let cancelled = false; (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 { - 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; - setScores( - 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 }; - }), - ); + setScores(result); } catch (e) { if (!cancelled) setError(e instanceof Error ? e.message : String(e)); } @@ -144,6 +98,7 @@ export function ReportPage({ store, jev, onReplay }: Props) {
{s.score.toFixed(1)}/{s.max} · 置信度 {Math.round(s.confidence * 100)}% + {s.confidence < 0.6 && ' · 证据不足'}
))} diff --git a/src/ui/report/badges.ts b/src/ui/report/badges.ts index cc0482b..1600de0 100644 --- a/src/ui/report/badges.ts +++ b/src/ui/report/badges.ts @@ -1,15 +1,4 @@ -import type { DimensionTag } from '../../engine/types'; - -/** 画像报告的 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: '你信任谁、把后背交给谁' }, -]; +export { PROFILE_DIMENSIONS } from '../../engine/scoring'; /** 勋章映射:最高置信度维度 → 称号(写死的 MVP 映射表) */ export const BADGE_MAP: Record = {