把代入名片从开场白拆到主角设定短码槽,并收紧对话落盘写法。
多开场可各自绑定名片;开玩挂载跳过开场产物并认用户需求类标签。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,7 +12,7 @@ name: 开场白与开场变量
|
|||||||
id: opening-setup
|
id: opening-setup
|
||||||
artifact: 设计.开场白与开场变量
|
artifact: 设计.开场白与开场变量
|
||||||
declaration: >
|
declaration: >
|
||||||
创作终节点:写出 1~多条开场(每条 = meta 初值/用户角色 + 正文 present.v1,壳与正文组成一致;须含字面 @玩家);选定后落库、结束创作并保存定稿
|
创作终节点:写出 1~多条开场(每条 = meta 开场变量初值 + 正文 present.v1,壳与正文组成一致;须含字面 @玩家);选定后落库、结束创作并保存定稿。用户代入名片走「主角设定」,勿把名字/简介写进开场正文。
|
||||||
when: |
|
when: |
|
||||||
收成链已齐(至少体验契约可引用,且「细化终稿」已产出或可立即收成运行规格),需要可开玩的第一段剧情与同真相初值。
|
收成链已齐(至少体验契约可引用,且「细化终稿」已产出或可立即收成运行规格),需要可开玩的第一段剧情与同真相初值。
|
||||||
排在收成之后作最后一步。用户要进游玩但还没有第一屏 → 排。
|
排在收成之后作最后一步。用户要进游玩但还没有第一屏 → 排。
|
||||||
@@ -22,9 +22,9 @@ when_not: |
|
|||||||
只要改真值规则不要开场文 → 「变量设计与更新规则」。
|
只要改真值规则不要开场文 → 「变量设计与更新规则」。
|
||||||
运行规格未齐 → 先「细化终稿」。尚未选定开场前若要补前序节点(如 NPC)→ 插在本步之前,勿以「收口已排入」拒绝追加。
|
运行规格未齐 → 先「细化终稿」。尚未选定开场前若要补前序节点(如 NPC)→ 插在本步之前,勿以「收口已排入」拒绝追加。
|
||||||
boundary: |
|
boundary: |
|
||||||
本技能是创作终节点:1~多条开场。每条 = meta(用户角色名字+简介、开场变量初值)+ 正文(present.v1,shell 与「设计.正文组成」一致)。
|
本技能是创作终节点:1~多条开场。每条 = meta(开场变量初值)+ 正文(present.v1,shell 与「设计.正文组成」一致)。
|
||||||
用户角色跟这条开场走;不同开场可有不同用户角色。初值与该条叙述同真相。
|
用户代入名片优先「设计.主角设定」;本步可不写用户角色。旧稿若在 meta 带了用户角色仍可读,但不要再往开场正文夹名字/简介。
|
||||||
必须遵守已验收的叙事指南、回复呈现、正文组成、变量设计;不重做这些契约。正文不得只交一段散文。
|
初值与该条叙述同真相。必须遵守已验收的叙事指南、回复呈现、正文组成、变量设计;不重做这些契约。正文不得只交一段散文。
|
||||||
具体实例:开场可引用已有实例名片,不在本步新造百科。
|
具体实例:开场可引用已有实例名片,不在本步新造百科。
|
||||||
选定后由程序写入 输出.开场白 / 运行.初始变量 / 运行.开场用户角色 / 变量.当前,关闭 DAG 并保存定稿;不要在本步之后再排其它能力。
|
选定后由程序写入 输出.开场白 / 运行.初始变量 / 运行.开场用户角色 / 变量.当前,关闭 DAG 并保存定稿;不要在本步之后再排其它能力。
|
||||||
feeds: narrator
|
feeds: narrator
|
||||||
@@ -55,8 +55,8 @@ feeds: narrator
|
|||||||
- 开场比一章短:愿点进游玩即可,不必凑游玩默认 1000~2000 字。
|
- 开场比一章短:愿点进游玩即可,不必凑游玩默认 1000~2000 字。
|
||||||
|
|
||||||
**meta = 这条开场的初始设定:**
|
**meta = 这条开场的初始设定:**
|
||||||
- `用户角色`:名字+简介(有代入则必填;不同开场可以不同)。
|
|
||||||
- `开场变量`:与该条叙述同真相的初值(名与变量设计一致)。顶层再抄一份推荐条的初值,供程序落库。
|
- `开场变量`:与该条叙述同真相的初值(名与变量设计一致)。顶层再抄一份推荐条的初值,供程序落库。
|
||||||
|
- `用户角色`:可选;优先已由「主角设定」钉好。若本条必须自带名片且尚无主角设定,可填名字+简介(程序会抬到主角设定节点)。禁止把名字/简介写进 `开场白全文` / 候选正文。
|
||||||
|
|
||||||
**用户角色占位(产物规格,不是可选项):**
|
**用户角色占位(产物规格,不是可选项):**
|
||||||
- `开场白全文`、每条候选 `正文`(含 `blocks.body` 及其它有字区域)里,凡指称用户扮演角色,第三人称点名处必须写字面量 `@玩家`(六个字符:`@` + `玩家`)。
|
- `开场白全文`、每条候选 `正文`(含 `blocks.body` 及其它有字区域)里,凡指称用户扮演角色,第三人称点名处必须写字面量 `@玩家`(六个字符:`@` + `玩家`)。
|
||||||
@@ -65,11 +65,11 @@ feeds: narrator
|
|||||||
- 进游玩时程序把 `@玩家` 换成当前用户角色名;创作稿必须保留字面 `@玩家`。
|
- 进游玩时程序把 `@玩家` 换成当前用户角色名;创作稿必须保留字面 `@玩家`。
|
||||||
|
|
||||||
执行顺序:
|
执行顺序:
|
||||||
1. 读依赖:worker 集/美学/叙事/**正文组成(先抄 shell_id)**/监控栏/变量设计/舞台与实例。
|
1. 读依赖:worker 集/美学/叙事/**正文组成(先抄 shell_id)**/监控栏/变量设计/舞台与实例/主角设定。
|
||||||
2. 写 1~3 条候选:每条 `meta`(用户角色+开场变量)+ `正文`(present.v1)。同真相、第一拍可不同。
|
2. 写 1~3 条候选:每条 `meta`(开场变量;用户角色仅在缺主角设定时补)+ `正文`(present.v1)。同真相、第一拍可不同。
|
||||||
3. 推荐条的 present.v1 同时写入 `开场白全文`(对象,不是字符串)。
|
3. 推荐条的 present.v1 同时写入 `开场白全文`(对象,不是字符串)。
|
||||||
4. 顶层 `开场变量` = 推荐条 meta 里那份初值。冲突以叙述为准。
|
4. 顶层 `开场变量` = 推荐条 meta 里那份初值。冲突以叙述为准。
|
||||||
5. 交稿前自检:每条 `正文.shell` 与正文组成一致;JSON 搜索 `@玩家`;再自评 + 追问;输出 JSON。
|
5. 交稿前自检:每条 `正文.shell` 与正文组成一致;JSON 搜索 `@玩家`;正文无「用户角色.名字」行;再自评 + 追问;输出 JSON。
|
||||||
|
|
||||||
缺上游契约时:能写则按已有材料写开场并标开放问题;不能成立第一拍则追问 1~2 点,勿交空壳。
|
缺上游契约时:能写则按已有材料写开场并标开放问题;不能成立第一拍则追问 1~2 点,勿交空壳。
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ summary:`开场白与开场变量 · …`(点题场景)。
|
|||||||
硬规则:
|
硬规则:
|
||||||
1. context-fragment.v1;自评含「玩家占位」与「版式符合」。
|
1. context-fragment.v1;自评含「玩家占位」与「版式符合」。
|
||||||
2. `开场白全文` 与每条候选 `正文` 必须是 present.v1 对象(含 `schema`/`shell`/`blocks.body`);`shell` 与「设计.正文组成」一致。禁止只交散文字符串。
|
2. `开场白全文` 与每条候选 `正文` 必须是 present.v1 对象(含 `schema`/`shell`/`blocks.body`);`shell` 与「设计.正文组成」一致。禁止只交散文字符串。
|
||||||
3. `开场白候选` 1~3 条,每条含 `meta`(用户角色有代入则必填;开场变量与该条同真相)+ `正文`。顶层 `开场变量` 必填(推荐条初值)。
|
3. `开场白候选` 1~3 条,每条含 `meta`(开场变量与该条同真相;用户角色可选,优先主角设定)+ `正文`。顶层 `开场变量` 必填(推荐条初值)。
|
||||||
4. 每条正文(含 body)必须含字面 `@玩家`;不发明新真值名。
|
4. 每条正文(含 body)必须含字面 `@玩家`;不发明新真值名。
|
||||||
5. summary:`开场白与开场变量 · {brief 缩略}`。
|
5. summary:`开场白与开场变量 · {brief 缩略}`。
|
||||||
|
|
||||||
@@ -219,11 +219,12 @@ summary:`开场白与开场变量 · …`(点题场景)。
|
|||||||
## examples
|
## examples
|
||||||
|
|
||||||
```examples
|
```examples
|
||||||
好:正文为 present.v1,shell 与正文组成相同;body 里有字面 @玩家;meta 带用户角色与开场变量。
|
好:正文为 present.v1,shell 与正文组成相同;body 里有字面 @玩家;meta 带开场变量;名片在主角设定。
|
||||||
好:prose 壳也包成 {"schema":"present.v1","shell":"prose","blocks":{"body":"冷风灌进来时,@玩家还握着空水瓶。"}}。
|
好:prose 壳也包成 {"schema":"present.v1","shell":"prose","blocks":{"body":"冷风灌进来时,@玩家还握着空水瓶。"}}。
|
||||||
好:「你(@玩家)站在闸机前,票夹是空的。」写在 blocks.body。
|
好:「你(@玩家)站在闸机前,票夹是空的。」写在 blocks.body。
|
||||||
坏:`开场白全文` 只是一段字符串,没有 present.v1。
|
坏:`开场白全文` 只是一段字符串,没有 present.v1。
|
||||||
坏:shell 与正文组成不一致,或灌了该壳没有的区域。
|
坏:shell 与正文组成不一致,或灌了该壳没有的区域。
|
||||||
坏:全文只有「你推开门」——零个 `@玩家`。
|
坏:全文只有「你推开门」——零个 `@玩家`。
|
||||||
坏:「林晚推开门」——写死姓名。
|
坏:「林晚推开门」——写死姓名。
|
||||||
|
坏:在正文或开场白全文里写「用户角色.名字: …」——名片应落「主角设定」。
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ when: |
|
|||||||
when_not: |
|
when_not: |
|
||||||
旁观或把角色当棋子、不要代入身份 → 不排。
|
旁观或把角色当棋子、不要代入身份 → 不排。
|
||||||
只要体验感觉、不要名片 → 「美学纲领与交互范式」。
|
只要体验感觉、不要名片 → 「美学纲领与交互范式」。
|
||||||
NPC 名片 → 「具体实例」。开场第一拍 → 「开场白与开场变量」。
|
NPC 名片 → 「具体实例」。开场第一拍(叙事)→ 「开场白与开场变量」;名片本身仍走本步,勿写进开场白。
|
||||||
boundary: |
|
boundary: |
|
||||||
本技能:@玩家 扮演的那一张名片(名字、背景、特殊设定)。
|
本技能:@玩家 扮演的那一张名片(名字、背景、特殊设定)。
|
||||||
美学纲领与交互范式:体验与轮转——本步不重定。
|
美学纲领与交互范式:体验与轮转——本步不重定。
|
||||||
实现机制:世界法则——金手指写在本步「它能做什么」,法则来源不写。
|
实现机制:世界法则——金手指写在本步「它能做什么」,法则来源不写。
|
||||||
开场白:第一屏怎么开始——本步不定时间地点冲突。
|
开场白:第一屏怎么开始——本步不定时间地点冲突;也不把名字/简介塞进开场正文。
|
||||||
feeds: gm
|
feeds: gm
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -9,20 +9,20 @@ when: >-
|
|||||||
core: >-
|
core: >-
|
||||||
把用户原话收成可开玩的一核短局规格,写入本局黑板;开玩后主世界层读产物写正文,默认原文即终稿。
|
把用户原话收成可开玩的一核短局规格,写入本局黑板;开玩后主世界层读产物写正文,默认原文即终稿。
|
||||||
必交:美学纲领(这一核是什么、怎么代入、轮转到哪停)。
|
必交:美学纲领(这一核是什么、怎么代入、轮转到哪停)。
|
||||||
用户代入有名字/简介 → 写进开场白的用户角色(meta:名字+简介),不要另开主角设定。
|
用户代入有名字/简介 → insert 设计.主角设定(名字+背景);不要写进开场白。
|
||||||
核是拿掉会换故事的变造法则 → 再交实现机制。
|
核是拿掉会换故事的变造法则 → 再交实现机制。
|
||||||
核依赖跨轮记住 → 再 declare 至多两三个真值。核可玩 → 交开场白。
|
核依赖跨轮记住 → 再 declare 至多两三个真值。核可玩 → 交开场白(只要叙事,勿夹用户角色 meta)。
|
||||||
某一能力能不能落对照【能力 · 何时落盘】。材料只够围着这一核开玩 → 收口开场白。
|
某一能力能不能落对照【能力 · 何时落盘】。材料只够围着这一核开玩 → 收口开场白。
|
||||||
用户改口要铺可经营世界 → 本局仍把已说的收成这一核,并告知完整世界用「数据化跑团体验」或工序编排「回合推演」。
|
用户改口要铺可经营世界 → 本局仍把已说的收成这一核,并告知完整世界用「数据化跑团体验」或工序编排「回合推演」。
|
||||||
|
|
||||||
process:
|
process:
|
||||||
- 每轮对照【能力 · 何时落盘】与已有产物:条件已成立的可同轮多落;核还糊 → 问挡住的 1 点。
|
- 每轮对照【能力 · 何时落盘】与已有产物:条件已成立的可同轮多落;核还糊 → 问挡住的 1 点。
|
||||||
- 【1】insert 设计.美学纲领与交互范式:用用户原话写清这一核、站位、何时等用户。缺哪件问哪件。
|
- 【1】insert 设计.美学纲领与交互范式:用用户原话写清这一核、站位、何时等用户。缺哪件问哪件。
|
||||||
- 【1b】代入且有名字/简介 → 写进开场白 meta 的用户角色(名字+简介),不要单独 insert 设计.主角设定。
|
- 【1b】代入且有名字/简介 → insert 设计.主角设定(或 设计.主角设定#短码,与开场同槽)。禁止写进开场白 meta。
|
||||||
- 【2】核=拿掉会换故事的变造法则(如任务失败即送走)→ insert 设计.实现机制,写短。只是身份或开场处境 → 写进开场白或主角设定,跳过机制。
|
- 【2】核=拿掉会换故事的变造法则(如任务失败即送走)→ insert 设计.实现机制,写短。只是身份或开场处境 → 写进主角设定,跳过机制。
|
||||||
- 用户点名文风/禁忌/写法档 → insert 叙事指南。点名推进/大纲/停笔 → insert 故事推进。二者条件独立,不要因为没谈推进就不落文风。用户要分区或壳 → 回复呈现再正文组成。未点名文风与推进 → 跳过二者,开玩吃默认散文。材料已齐且条件成立时可与上列同轮一并落。
|
- 用户点名文风/禁忌/写法档 → insert 叙事指南。点名推进/大纲/停笔 → insert 故事推进。二者条件独立,不要因为没谈推进就不落文风。用户要分区或壳 → 回复呈现再正文组成。未点名文风与推进 → 跳过二者,开玩吃默认散文。材料已齐且条件成立时可与上列同轮一并落。
|
||||||
- 核必须跨轮记住的数(时限/进度/好感,至多三个)→ declare_variable;到某值换文案再 declare_map。没有门控 → 不立真值。
|
- 核必须跨轮记住的数(时限/进度/好感,至多三个)→ declare_variable;到某值换文案再 declare_map。没有门控 → 不立真值。
|
||||||
- 核已可玩 → insert 设计.开场白(须含 @玩家;第一屏只写这一核怎么开始)。
|
- 核已可玩 → insert 设计.开场白(须含 @玩家)。多开局用 设计.开场白#短码,互不覆盖;改某一局 insert 同一完整 tag。禁止夹用户角色.名字/简介。
|
||||||
- 舞台骨架 / 生成规则 / 拓扑即使「何时用」成立也不 insert:地点与规则折进美学核或开场白。
|
- 舞台骨架 / 生成规则 / 拓扑即使「何时用」成立也不 insert:地点与规则折进美学核或开场白。
|
||||||
|
|
||||||
principles:
|
principles:
|
||||||
|
|||||||
+14
-12
@@ -48,6 +48,7 @@ import {
|
|||||||
/** 开场白进对话历史,不重复挂进每轮投影 */
|
/** 开场白进对话历史,不重复挂进每轮投影 */
|
||||||
const SKIP_BIND_TAGS = new Set([
|
const SKIP_BIND_TAGS = new Set([
|
||||||
"设计.开场白",
|
"设计.开场白",
|
||||||
|
"设计.开场白与开场变量",
|
||||||
"设计.worker集",
|
"设计.worker集",
|
||||||
"设计.worker集.草稿",
|
"设计.worker集.草稿",
|
||||||
CONTEXT_ORDER_TAG,
|
CONTEXT_ORDER_TAG,
|
||||||
@@ -57,9 +58,20 @@ const SKIP_BIND_TAGS = new Set([
|
|||||||
VALUE_MAP_TAG,
|
VALUE_MAP_TAG,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/** 偏文风/呈现:有叙事转述时挂转述;否则仍挂主世界层 */
|
function shouldSkipBindTag(tag: string): boolean {
|
||||||
|
const t = tag.trim();
|
||||||
|
if (!t) return true;
|
||||||
|
if (SKIP_BIND_TAGS.has(t)) return true;
|
||||||
|
if (t.startsWith("设计.开场白#")) return true;
|
||||||
|
if (t.startsWith("输出.")) return true;
|
||||||
|
if (t.startsWith("运行.")) return true;
|
||||||
|
if (t.startsWith("创作.")) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 偏文风/呈现/硬约束:有叙事转述时挂转述;否则仍挂主世界层 */
|
||||||
const STYLE_TAG_RE =
|
const STYLE_TAG_RE =
|
||||||
/叙事指南|故事推进|文风|美学|纲领|禁忌|示例|模仿|正文组成|回复格式|监控栏|篇幅|结构/;
|
/叙事指南|故事推进|文风|美学|纲领|禁忌|用户约束|用户需求|示例|模仿|正文组成|回复格式|监控栏|篇幅|结构/;
|
||||||
|
|
||||||
/** 这些配方开玩时刚需叙事转述(主世界出内容、转述出文风) */
|
/** 这些配方开玩时刚需叙事转述(主世界出内容、转述出文风) */
|
||||||
const RECIPES_REQUIRE_NARRATOR = new Set(["文本生成器"]);
|
const RECIPES_REQUIRE_NARRATOR = new Set(["文本生成器"]);
|
||||||
@@ -75,16 +87,6 @@ function isStyleProductTag(tag: string): boolean {
|
|||||||
return STYLE_TAG_RE.test(tag);
|
return STYLE_TAG_RE.test(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldSkipBindTag(tag: string): boolean {
|
|
||||||
const t = tag.trim();
|
|
||||||
if (!t) return true;
|
|
||||||
if (SKIP_BIND_TAGS.has(t)) return true;
|
|
||||||
if (t.startsWith("输出.")) return true;
|
|
||||||
if (t.startsWith("运行.")) return true;
|
|
||||||
if (t.startsWith("创作.")) return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 从黑板收集对话落盘产物(有正文的 用户.* / 设计.*) */
|
/** 从黑板收集对话落盘产物(有正文的 用户.* / 设计.*) */
|
||||||
export function collectDictateBindProducts(board: Blackboard): DictateProduct[] {
|
export function collectDictateBindProducts(board: Blackboard): DictateProduct[] {
|
||||||
const products: DictateProduct[] = [];
|
const products: DictateProduct[] = [];
|
||||||
|
|||||||
@@ -85,10 +85,10 @@ tool 跑完后写给用户看的 markdown。作用:确认本轮落了什么、
|
|||||||
|
|
||||||
## 分清落盘目标
|
## 分清落盘目标
|
||||||
- 正文组成(格式)→ insert \`设计.正文组成\`(须「回复呈现」等上游条件已满足,见能力表)
|
- 正文组成(格式)→ insert \`设计.正文组成\`(须「回复呈现」等上游条件已满足,见能力表)
|
||||||
- 开场白(内容)→ insert \`设计.开场白\`;落档/开玩同步 \`输出.开场白\`(收口;上游未齐则先补上游,不硬塞开场)
|
- 开场白(内容)→ insert \`设计.开场白\` 或 \`设计.开场白#场景短码\`(多开场互不覆盖;改某一条则 insert 同一完整 tag);落档/开玩同步 \`输出.开场白\`
|
||||||
|
- 代入名片 → insert \`设计.主角设定\` 或 \`设计.主角设定#同短码\`(与开场白同槽即绑定该开局;无槽则全局默认)
|
||||||
- 多篇无关联短文(生产在游玩)→ 创作期钉 \`设计.模仿范例\` / \`设计.模仿要点\` / \`设计.故事推进\`(大纲扩写);文风细则另 insert \`设计.叙事指南\`
|
- 多篇无关联短文(生产在游玩)→ 创作期钉 \`设计.模仿范例\` / \`设计.模仿要点\` / \`设计.故事推进\`(大纲扩写);文风细则另 insert \`设计.叙事指南\`
|
||||||
- 好感分档性格等 → declare_variable + declare_map;分档文案走映射,不 insert 整表散文
|
- 好感分档性格等 → declare_variable + declare_map;分档文案走映射,不 insert 整表散文
|
||||||
- 代入名片(名字/身份/金手指)→ insert \`设计.主角设定\`(对照「主角设定」何时用;名字写成具体称呼,不用 @玩家 / 用户 / 主角)
|
|
||||||
${recipeBlock}${moduleBlock}
|
${recipeBlock}${moduleBlock}
|
||||||
## 按用户本轮意图择一为主(仍须过「何时用」)
|
## 按用户本轮意图择一为主(仍须过「何时用」)
|
||||||
落盘后的可见回复一律按「对用户可见回复」写;下列各节只约定本轮落什么。
|
落盘后的可见回复一律按「对用户可见回复」写;下列各节只约定本轮落什么。
|
||||||
@@ -100,11 +100,11 @@ ${recipeBlock}${moduleBlock}
|
|||||||
「回复呈现」等上游已齐时才 insert \`设计.正文组成\`。
|
「回复呈现」等上游已齐时才 insert \`设计.正文组成\`。
|
||||||
|
|
||||||
### C · 生成开场白
|
### C · 生成开场白
|
||||||
收口:先有正文组成且体验已锚定;insert \`设计.开场白\`(岔路用 \`## 岔路\`)。
|
收口:体验已锚定。首条可用 \`设计.开场白\`;再写别的开局用 \`设计.开场白#场景短码\`(与生成规则同款:同 tag 覆盖,新短码新建)。禁止用 \`## 岔路\` 塞进同一份。
|
||||||
**形状:** meta + 正文。有代入名片时,开头 YAML \`用户角色.名字\` / \`用户角色.简介\`,再 \`---\`,下面才是开场正文。不同开场可有不同用户角色。
|
**形状:** 只要叙事正文。禁止把 \`用户角色.名字\` / \`用户角色.简介\` 写进开场白。代入名片 → \`设计.主角设定\` 或 \`设计.主角设定#同短码\`。
|
||||||
**正文格式:** 已有 \`设计.正文组成\` 时,正文必须是 present.v1 JSON(\`shell\` 与正文组成一致,主读写 \`blocks.body\`),不要只交裸 Markdown;尚未钉正文组成时才用 Markdown。
|
**正文格式:** 已有 \`设计.正文组成\` 时,正文必须是 present.v1 JSON(\`shell\` 与正文组成一致,主读写 \`blocks.body\`),不要只交裸 Markdown;尚未钉正文组成时才用 Markdown。
|
||||||
**content 规格:** 正文至少一处字面 \`@玩家\` 作用户角色名位;姓名位用 \`@玩家\`,不写死具体姓名;全文不能只有「你」而零个 \`@玩家\`。缺则改写后再 insert。进游玩时程序替换 \`@玩家\`。
|
**content 规格:** 正文至少一处字面 \`@玩家\` 作用户角色名位;姓名位用 \`@玩家\`,不写死具体姓名;全文不能只有「你」而零个 \`@玩家\`。缺则改写后再 insert。进游玩时程序替换 \`@玩家\`。
|
||||||
进玩时若有用户角色,程序提示是否切换到这个预设。
|
进玩时在游玩辅助「开场」Tab 点选开局;有绑定名片则可切到该预设。
|
||||||
|
|
||||||
### D · 钉变量与映射
|
### D · 钉变量与映射
|
||||||
仅当「变量设计」何时用成立(体验须跨轮记住状态等):declare_variable → 有分档换文再 declare_map。配方名带「数据化」也不自动开跑 D。
|
仅当「变量设计」何时用成立(体验须跨轮记住状态等):declare_variable → 有分档换文再 declare_map。配方名带「数据化」也不自动开跑 D。
|
||||||
@@ -113,7 +113,7 @@ ${recipeBlock}${moduleBlock}
|
|||||||
必须用 insert 写齐游玩上下文:\`设计.模仿范例\`、\`设计.模仿要点\`、\`设计.故事推进\`(用户输入用法:大纲扩写;扮演停笔关掉)。文风细则对照「叙事指南」何时用再 insert \`设计.叙事指南\`。缺范例/要点/故事推进任一就先补产物,再说可以开玩。创作期不写完整短文。开玩后程序只挂载这些产物:主世界出内容,转述出文风。
|
必须用 insert 写齐游玩上下文:\`设计.模仿范例\`、\`设计.模仿要点\`、\`设计.故事推进\`(用户输入用法:大纲扩写;扮演停笔关掉)。文风细则对照「叙事指南」何时用再 insert \`设计.叙事指南\`。缺范例/要点/故事推进任一就先补产物,再说可以开玩。创作期不写完整短文。开玩后程序只挂载这些产物:主世界出内容,转述出文风。
|
||||||
|
|
||||||
### F · 快穿短局
|
### F · 快穿短局
|
||||||
仅当本局配方是快穿短局,或用户只要一核短局。先 insert 美学纲领(这一核、怎么代入、何时等用户);代入有名字/简介则写进开场白的用户角色 meta(不单独 insert 主角设定)。核是变造法则再 insert 实现机制;有门控才 declare 至多三个真值。点名文风 → insert 叙事指南;点名推进/停笔 → insert 故事推进(条件独立)。核可玩即 insert 开场白:用户未点名分区/壳则跳过正文组成(覆盖 C),开玩吃默认散文。舞台骨架/生成规则/拓扑即使「何时用」成立也不 insert,把地点与规则折进美学核或开场白。用户改口要铺可经营世界 → 本局仍只收这一核,并告知改用「数据化跑团体验」或「回合推演」。
|
仅当本局配方是快穿短局,或用户只要一核短局。先 insert 美学纲领(这一核、怎么代入、何时等用户);代入有名字/简介 → insert \`设计.主角设定\`(不要写进开场白)。核是变造法则再 insert 实现机制;有门控才 declare 至多三个真值。点名文风 → insert 叙事指南;点名推进/停笔 → insert 故事推进(条件独立)。核可玩即 insert 开场白:用户未点名分区/壳则跳过正文组成(覆盖 C),开玩吃默认散文。舞台骨架/生成规则/拓扑即使「何时用」成立也不 insert,把地点与规则折进美学核或开场白。用户改口要铺可经营世界 → 本局仍只收这一核,并告知改用「数据化跑团体验」或「回合推演」。
|
||||||
|
|
||||||
## 常用序感
|
## 常用序感
|
||||||
- 需求/美学等不变设定:靠前(负)
|
- 需求/美学等不变设定:靠前(负)
|
||||||
|
|||||||
@@ -153,9 +153,11 @@ export function resolveRepeatableInsertTag(params: {
|
|||||||
.filter((t) => t && dictateProductFamily(t) === family);
|
.filter((t) => t && dictateProductFamily(t) === family);
|
||||||
|
|
||||||
if (!slot) {
|
if (!slot) {
|
||||||
if (siblings.length > 0 && raw === family) {
|
// 允许覆盖基名自身;若已有其它 #槽位,禁止再用裸基名(须写明 #id)
|
||||||
|
const otherSiblings = siblings.filter((t) => t !== family);
|
||||||
|
if (otherSiblings.length > 0 && raw === family) {
|
||||||
return {
|
return {
|
||||||
error: `「${family}」是可增殖产物,追加请用「${family}#唯一id」(生成规则用 rule_id,具体实例用 batch_id/姓名),不要覆盖基 tag。改某一条则 insert 同一完整 tag。`,
|
error: `「${family}」是可增殖产物,追加请用「${family}#唯一id」(生成规则用 rule_id,具体实例用 batch_id/姓名,开场白/主角设定用场景短码),不要覆盖基 tag。改某一条则 insert 同一完整 tag。`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { tag: family };
|
return { tag: family };
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ export const DICTATE_TOOL_DEFINITIONS: ToolDefinition[] = [
|
|||||||
position: {
|
position: {
|
||||||
type: "string",
|
type: "string",
|
||||||
description:
|
description:
|
||||||
"产物 tag,须以「用户.」或「设计.」开头。例:用户.需求、设计.正文组成、设计.开场白;可增殖例:设计.生成规则#rule-id、设计.具体实例#batch-id。勿用本工具写变量目录/映射。",
|
"产物 tag,须以「用户.」或「设计.」开头。例:用户.需求、设计.正文组成、设计.开场白、设计.开场白#dorm;可增殖例:设计.生成规则#rule-id、设计.具体实例#batch-id、设计.主角设定#dorm。勿用本工具写变量目录/映射。",
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
type: "string",
|
type: "string",
|
||||||
description:
|
description:
|
||||||
"落入该位置的正文。设计.正文组成=格式 JSON;设计.开场白=meta + 正文(有正文组成时正文为 present.v1 JSON;否则 Markdown),必须含字面 @玩家(用户角色名位,禁止写死姓名)",
|
"落入该位置的正文。设计.正文组成=格式 JSON;设计.开场白[#短码]=叙事正文(有正文组成时为 present.v1 JSON;否则 Markdown),必须含字面 @玩家,禁止写入用户角色.名字/简介;设计.主角设定[#同短码]=名字/背景/特殊设定 JSON,与开场同短码即绑定该开局",
|
||||||
},
|
},
|
||||||
order: {
|
order: {
|
||||||
type: "number",
|
type: "number",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* 开场白 meta 里的用户角色(名字+简介)是本条开场的初始设定。
|
* 用户代入名片写在「设计.主角设定」;开场白只留叙事。
|
||||||
|
* 旧稿若把名字/简介塞进开场 meta,读取时仍可回退,并建议提升到主角设定节点。
|
||||||
* 用户选用后只覆盖本局 @玩家 / @人设,不写进全局角色列表。
|
* 用户选用后只覆盖本局 @玩家 / @人设,不写进全局角色列表。
|
||||||
*/
|
*/
|
||||||
import type { Blackboard } from "../blackboard/blackboard.js";
|
import type { Blackboard } from "../blackboard/blackboard.js";
|
||||||
@@ -11,13 +12,116 @@ import {
|
|||||||
import {
|
import {
|
||||||
OPENING_PERSONA_CHOICE_TAG,
|
OPENING_PERSONA_CHOICE_TAG,
|
||||||
OPENING_PERSONA_TAG,
|
OPENING_PERSONA_TAG,
|
||||||
|
OPENING_PRODUCT_FAMILY,
|
||||||
|
OPENING_SETUP_ARTIFACT_TAG,
|
||||||
parseOpeningSealPayload,
|
parseOpeningSealPayload,
|
||||||
readSealedOpeningPersona,
|
readSealedOpeningPersona,
|
||||||
splitOpeningDocument,
|
splitOpeningDocument,
|
||||||
|
type OpeningUserPersona,
|
||||||
} from "../skills/opening-seal.js";
|
} from "../skills/opening-seal.js";
|
||||||
|
import {
|
||||||
|
buildRepeatableProductTag,
|
||||||
|
dictateProductFamily,
|
||||||
|
dictateProductSlot,
|
||||||
|
} from "../dictate/repeatable-tags.js";
|
||||||
|
|
||||||
export const PROTAGONIST_TAG = "设计.主角设定";
|
export const PROTAGONIST_TAG = "设计.主角设定";
|
||||||
|
|
||||||
|
/** 对话落盘额外可增殖族(catalog 未标 repeatable 的简化 tag) */
|
||||||
|
export const DICTATE_OPENING_REPEATABLE_FAMILIES = [
|
||||||
|
OPENING_PRODUCT_FAMILY,
|
||||||
|
PROTAGONIST_TAG,
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/** 与开场同槽:设计.开场白#dorm → 设计.主角设定#dorm;无槽则基名 */
|
||||||
|
export function protagonistTagForOpeningSlot(openingTag: string): string {
|
||||||
|
const family = dictateProductFamily(openingTag);
|
||||||
|
if (family !== OPENING_PRODUCT_FAMILY && family !== OPENING_SETUP_ARTIFACT_TAG) {
|
||||||
|
return PROTAGONIST_TAG;
|
||||||
|
}
|
||||||
|
const slot = dictateProductSlot(openingTag);
|
||||||
|
return slot ? buildRepeatableProductTag(PROTAGONIST_TAG, slot) : PROTAGONIST_TAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 把开场/运行里的名字+简介收成可复制的「主角设定」产物。 */
|
||||||
|
export function protagonistFragmentFromPersona(
|
||||||
|
persona: OpeningUserPersona,
|
||||||
|
): string {
|
||||||
|
const background = persona.description.trim() || "未定";
|
||||||
|
return JSON.stringify(
|
||||||
|
{
|
||||||
|
schema: "context-fragment.v1",
|
||||||
|
技能: "主角设定",
|
||||||
|
brief: `${persona.name} · 代入名片`,
|
||||||
|
mount: ["world-simulator"],
|
||||||
|
稳变: "stable",
|
||||||
|
正文: {
|
||||||
|
名字: persona.name,
|
||||||
|
背景: background,
|
||||||
|
特殊设定: { 身份: "无", 金手指: "无", 其它: "无" },
|
||||||
|
},
|
||||||
|
自评: {
|
||||||
|
维度: [
|
||||||
|
{ 名: "可代入", 分数: 8, 说明: "由开场/用户原话收成" },
|
||||||
|
{ 名: "够用", 分数: 7, 说明: "简介作背景;特殊设定未另钉" },
|
||||||
|
],
|
||||||
|
薄弱点: "特殊设定未展开",
|
||||||
|
},
|
||||||
|
追问: { 导语: "", 题目: [] },
|
||||||
|
开放问题: [],
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 黑板上尚无该 tag 的主角设定时写入(可指定开场同槽 tag)。 */
|
||||||
|
export function ensureProtagonistFromPersona(
|
||||||
|
board: Blackboard,
|
||||||
|
persona: OpeningUserPersona | null | undefined,
|
||||||
|
tag: string = PROTAGONIST_TAG,
|
||||||
|
): boolean {
|
||||||
|
if (!persona?.name?.trim()) return false;
|
||||||
|
const writeTag = tag.trim() || PROTAGONIST_TAG;
|
||||||
|
if (board.getContentByTag(writeTag)?.trim()) return false;
|
||||||
|
board.write({
|
||||||
|
tag: writeTag,
|
||||||
|
content: protagonistFragmentFromPersona(persona),
|
||||||
|
source: "runtime",
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readProtagonistPersonaAt(
|
||||||
|
board: Blackboard,
|
||||||
|
tag: string = PROTAGONIST_TAG,
|
||||||
|
): OpeningUserPersona | null {
|
||||||
|
const spec = parseProtagonistSpec(board.getContentByTag(tag));
|
||||||
|
if (!spec) return null;
|
||||||
|
return {
|
||||||
|
name: spec.name,
|
||||||
|
description: formatPersonaDescription(spec),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 某条开场绑定的名片:同槽主角设定 → 基名主角设定 → 开场旧 meta */
|
||||||
|
export function personaBoundToOpening(
|
||||||
|
board: Blackboard,
|
||||||
|
openingTag: string,
|
||||||
|
openingContent: string,
|
||||||
|
): OpeningUserPersona | null {
|
||||||
|
const slotted = readProtagonistPersonaAt(
|
||||||
|
board,
|
||||||
|
protagonistTagForOpeningSlot(openingTag),
|
||||||
|
);
|
||||||
|
if (slotted) return slotted;
|
||||||
|
if (dictateProductSlot(openingTag)) {
|
||||||
|
const base = readProtagonistPersonaAt(board, PROTAGONIST_TAG);
|
||||||
|
if (base) return base;
|
||||||
|
}
|
||||||
|
return splitOpeningDocument(openingContent).persona;
|
||||||
|
}
|
||||||
|
|
||||||
const RESERVED_NAMES = new Set(["@玩家", "用户", "主角", CREATION_PLACEHOLDER_NAME]);
|
const RESERVED_NAMES = new Set(["@玩家", "用户", "主角", CREATION_PLACEHOLDER_NAME]);
|
||||||
|
|
||||||
export type ProtagonistSpec = {
|
export type ProtagonistSpec = {
|
||||||
@@ -50,8 +154,22 @@ export function readSessionProtagonist(
|
|||||||
const sealed = readSealedOpeningPersona(
|
const sealed = readSealedOpeningPersona(
|
||||||
board.getContentByTag(OPENING_PERSONA_TAG),
|
board.getContentByTag(OPENING_PERSONA_TAG),
|
||||||
);
|
);
|
||||||
|
if (sealed) {
|
||||||
|
return {
|
||||||
|
name: sealed.name,
|
||||||
|
description: sealed.description,
|
||||||
|
summary: firstSentence(sealed.description),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const spec = parseProtagonistSpec(board.getContentByTag(PROTAGONIST_TAG));
|
||||||
|
if (spec) {
|
||||||
|
return {
|
||||||
|
name: spec.name,
|
||||||
|
description: formatPersonaDescription(spec),
|
||||||
|
summary: formatProtagonistSummary(spec),
|
||||||
|
};
|
||||||
|
}
|
||||||
const fromOpening =
|
const fromOpening =
|
||||||
sealed ??
|
|
||||||
parseOpeningSealPayload(board.getContentByTag("设计.开场白与开场变量"))
|
parseOpeningSealPayload(board.getContentByTag("设计.开场白与开场变量"))
|
||||||
?.persona ??
|
?.persona ??
|
||||||
splitOpeningDocument(board.getContentByTag("设计.开场白")).persona;
|
splitOpeningDocument(board.getContentByTag("设计.开场白")).persona;
|
||||||
@@ -62,13 +180,7 @@ export function readSessionProtagonist(
|
|||||||
summary: firstSentence(fromOpening.description),
|
summary: firstSentence(fromOpening.description),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const spec = parseProtagonistSpec(board.getContentByTag(PROTAGONIST_TAG));
|
return null;
|
||||||
if (!spec) return null;
|
|
||||||
return {
|
|
||||||
name: spec.name,
|
|
||||||
description: formatPersonaDescription(spec),
|
|
||||||
summary: formatProtagonistSummary(spec),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatPersonaDescription(spec: ProtagonistSpec): string {
|
export function formatPersonaDescription(spec: ProtagonistSpec): string {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import { toActiveSkillSnapshot } from "../skills/snapshot.js";
|
|||||||
import { runWorkerSkill } from "../worker/executor.js";
|
import { runWorkerSkill } from "../worker/executor.js";
|
||||||
import { resolveWorkerId } from "../worker/resolve-id.js";
|
import { resolveWorkerId } from "../worker/resolve-id.js";
|
||||||
import { resolveWorkerLlmProvider } from "../skills/worker-llm.js";
|
import { resolveWorkerLlmProvider } from "../skills/worker-llm.js";
|
||||||
import { overlayPlayPersona } from "../persona/protagonist-bind.js";
|
import { ensureProtagonistFromPersona, overlayPlayPersona } from "../persona/protagonist-bind.js";
|
||||||
import {
|
import {
|
||||||
DIALOGUE_HISTORY_TAG,
|
DIALOGUE_HISTORY_TAG,
|
||||||
appendDialogueHistoryTurn,
|
appendDialogueHistoryTurn,
|
||||||
@@ -1991,6 +1991,7 @@ export class PhaseRuntime {
|
|||||||
serializeOpeningPersona(payload.persona),
|
serializeOpeningPersona(payload.persona),
|
||||||
"opening-setup",
|
"opening-setup",
|
||||||
);
|
);
|
||||||
|
ensureProtagonistFromPersona(this.blackboard, payload.persona);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload?.variables.length) {
|
if (payload?.variables.length) {
|
||||||
@@ -2045,6 +2046,7 @@ export class PhaseRuntime {
|
|||||||
/** 收口后若还没有运行规格,用草稿或默认槽位补一份,才能进游玩 */
|
/** 收口后若还没有运行规格,用草稿或默认槽位补一份,才能进游玩 */
|
||||||
private ensurePlaySpecReady(): void {
|
private ensurePlaySpecReady(): void {
|
||||||
// 对话落盘:落档/开玩时自动挂载产物;种变量初值;按映射重投影
|
// 对话落盘:落档/开玩时自动挂载产物;种变量初值;按映射重投影
|
||||||
|
// 偏好/文风改由节点产物(设计.用户需求 / 设计.叙事指南*)挂载,不再全量自动摊库
|
||||||
if (this.isDictateIntakeMode()) {
|
if (this.isDictateIntakeMode()) {
|
||||||
const bound = applyDictatePlayBind(this.blackboard);
|
const bound = applyDictatePlayBind(this.blackboard);
|
||||||
if (bound.boundTags.length) {
|
if (bound.boundTags.length) {
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import {
|
|||||||
} from "./present-packet.js";
|
} from "./present-packet.js";
|
||||||
|
|
||||||
export const OPENING_SETUP_ARTIFACT_TAG = "设计.开场白与开场变量";
|
export const OPENING_SETUP_ARTIFACT_TAG = "设计.开场白与开场变量";
|
||||||
|
/** 对话落盘开场正文族(可 设计.开场白#槽位 增殖) */
|
||||||
|
export const OPENING_PRODUCT_FAMILY = "设计.开场白";
|
||||||
export const OPENING_OUTPUT_TAG = "输出.开场白";
|
export const OPENING_OUTPUT_TAG = "输出.开场白";
|
||||||
export const OPENING_INITIAL_VARS_TAG = "运行.初始变量";
|
export const OPENING_INITIAL_VARS_TAG = "运行.初始变量";
|
||||||
export const OPENING_CURRENT_VARS_TAG = "变量.当前";
|
export const OPENING_CURRENT_VARS_TAG = "变量.当前";
|
||||||
@@ -222,20 +224,75 @@ export function parseUserPersonaField(raw: unknown): OpeningUserPersona | null {
|
|||||||
return { name, description };
|
return { name, description };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 去掉文首用户角色 meta 行(含无 --- 篱笆的旧稿),避免 meta 进开场正文。 */
|
||||||
|
export function stripLeadingPersonaMeta(raw: string): string {
|
||||||
|
const lines = raw.split(/\r?\n/);
|
||||||
|
let i = 0;
|
||||||
|
while (i < lines.length && !lines[i]!.trim()) i++;
|
||||||
|
let saw = false;
|
||||||
|
let inUserRoleBlock = false;
|
||||||
|
while (i < lines.length) {
|
||||||
|
const line = lines[i]!;
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
if (saw) {
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (/^---+$/.test(trimmed) && saw) {
|
||||||
|
i++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (/^用户角色\s*:\s*$/.test(trimmed)) {
|
||||||
|
saw = true;
|
||||||
|
inUserRoleBlock = true;
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
inUserRoleBlock &&
|
||||||
|
/^\s+(?:名字|简介|name|description|人设|描述)\s*[::]/.test(line)
|
||||||
|
) {
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
inUserRoleBlock = false;
|
||||||
|
if (/^(?:用户角色\.)?(?:名字|简介)[::]/.test(trimmed)) {
|
||||||
|
saw = true;
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return lines.slice(i).join("\n").trim();
|
||||||
|
}
|
||||||
|
|
||||||
function splitYamlFrontmatter(raw: string): {
|
function splitYamlFrontmatter(raw: string): {
|
||||||
text: string;
|
text: string;
|
||||||
persona: OpeningUserPersona | null;
|
persona: OpeningUserPersona | null;
|
||||||
} {
|
} {
|
||||||
const text = raw.trim();
|
const text = raw.trim();
|
||||||
const fence = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
const fence = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
||||||
if (!fence) return { text, persona: parseOpeningUserPersona(text) };
|
if (!fence) {
|
||||||
|
const persona = parseOpeningUserPersona(text);
|
||||||
|
if (!persona) return { text, persona: null };
|
||||||
|
const stripped = stripLeadingPersonaMeta(text);
|
||||||
|
// 能认出文首 meta 才剥;正文里偶然出现「名字:」不剥
|
||||||
|
if (stripped !== text.trim()) return { text: stripped, persona };
|
||||||
|
if (/^(?:用户角色\.|用户角色\s*:)/m.test(text)) {
|
||||||
|
return { text: stripped, persona };
|
||||||
|
}
|
||||||
|
return { text, persona };
|
||||||
|
}
|
||||||
const persona =
|
const persona =
|
||||||
parseUserPersonaField(parseSimpleYamlMap(fence[1] ?? "")) ||
|
parseUserPersonaField(parseSimpleYamlMap(fence[1] ?? "")) ||
|
||||||
parseOpeningUserPersona(fence[1] ?? "");
|
parseOpeningUserPersona(fence[1] ?? "");
|
||||||
return { text: (fence[2] ?? "").trim(), persona };
|
return { text: (fence[2] ?? "").trim(), persona };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 开场正文前的 YAML 式 meta:用户角色名字+简介,其余当正文。 */
|
/** 开场正文与用户角色 meta 分离;meta 不应进「系统 开场白」。 */
|
||||||
export function splitOpeningDocument(raw: string | null | undefined): {
|
export function splitOpeningDocument(raw: string | null | undefined): {
|
||||||
text: string;
|
text: string;
|
||||||
persona: OpeningUserPersona | null;
|
persona: OpeningUserPersona | null;
|
||||||
|
|||||||
+56
-7
@@ -6,8 +6,17 @@ import {
|
|||||||
isTableCellEditable,
|
isTableCellEditable,
|
||||||
parseTableDoc,
|
parseTableDoc,
|
||||||
} from "../blackboard/table-cells.js";
|
} from "../blackboard/table-cells.js";
|
||||||
|
import {
|
||||||
|
dictateProductFamily,
|
||||||
|
dictateProductSlot,
|
||||||
|
} from "../dictate/repeatable-tags.js";
|
||||||
|
import {
|
||||||
|
DICTATE_ORDER_META_KEY,
|
||||||
|
effectiveDictateOrder,
|
||||||
|
} from "../dictate/types.js";
|
||||||
import {
|
import {
|
||||||
OPENING_PERSONA_CHOICE_TAG,
|
OPENING_PERSONA_CHOICE_TAG,
|
||||||
|
OPENING_PRODUCT_FAMILY,
|
||||||
OPENING_SETUP_ARTIFACT_TAG,
|
OPENING_SETUP_ARTIFACT_TAG,
|
||||||
SLOT_OPENING_SELECTED_INDEX,
|
SLOT_OPENING_SELECTED_INDEX,
|
||||||
parseOpeningSealPayload,
|
parseOpeningSealPayload,
|
||||||
@@ -15,7 +24,10 @@ import {
|
|||||||
type OpeningUserPersona,
|
type OpeningUserPersona,
|
||||||
} from "./opening-seal.js";
|
} from "./opening-seal.js";
|
||||||
import { CURRENT_VARS_TAG } from "./variable-catalog.js";
|
import { CURRENT_VARS_TAG } from "./variable-catalog.js";
|
||||||
import { readSessionProtagonist } from "../persona/protagonist-bind.js";
|
import {
|
||||||
|
personaBoundToOpening,
|
||||||
|
readSessionProtagonist,
|
||||||
|
} from "../persona/protagonist-bind.js";
|
||||||
import { presentReadablePlain } from "./present-packet.js";
|
import { presentReadablePlain } from "./present-packet.js";
|
||||||
|
|
||||||
export type PlayAuxTableRow = {
|
export type PlayAuxTableRow = {
|
||||||
@@ -29,6 +41,9 @@ export type PlayAuxTableRow = {
|
|||||||
|
|
||||||
export type PlayAuxOpening = {
|
export type PlayAuxOpening = {
|
||||||
index: number;
|
index: number;
|
||||||
|
/** 产物 tag:设计.开场白 或 设计.开场白#槽位;opening-setup 候选为假想 tag */
|
||||||
|
tag: string;
|
||||||
|
slot: string;
|
||||||
text: string;
|
text: string;
|
||||||
persona: OpeningUserPersona | null;
|
persona: OpeningUserPersona | null;
|
||||||
};
|
};
|
||||||
@@ -51,7 +66,8 @@ function selectedOpeningIndex(slots: Record<string, unknown> | undefined): numbe
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectOpenings(board: Blackboard): PlayAuxOpening[] {
|
/** 黑板上全部可选用开场(工序候选优先;否则对话落盘 设计.开场白[#槽]) */
|
||||||
|
export function collectOpenings(board: Blackboard): PlayAuxOpening[] {
|
||||||
const payload = parseOpeningSealPayload(
|
const payload = parseOpeningSealPayload(
|
||||||
board.getContentByTag(OPENING_SETUP_ARTIFACT_TAG),
|
board.getContentByTag(OPENING_SETUP_ARTIFACT_TAG),
|
||||||
0,
|
0,
|
||||||
@@ -62,18 +78,50 @@ function collectOpenings(board: Blackboard): PlayAuxOpening[] {
|
|||||||
board.getContentByTag(OPENING_SETUP_ARTIFACT_TAG),
|
board.getContentByTag(OPENING_SETUP_ARTIFACT_TAG),
|
||||||
index,
|
index,
|
||||||
);
|
);
|
||||||
|
const tag = `${OPENING_SETUP_ARTIFACT_TAG}#${index + 1}`;
|
||||||
return {
|
return {
|
||||||
index,
|
index,
|
||||||
|
tag,
|
||||||
|
slot: String(index + 1),
|
||||||
text: presentReadablePlain(text) || text,
|
text: presentReadablePlain(text) || text,
|
||||||
persona: one?.persona ?? payload.persona,
|
persona: one?.persona ?? payload.persona,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const split = splitOpeningDocument(board.getContentByTag("设计.开场白"));
|
|
||||||
if (split.text) {
|
const rows: Array<{
|
||||||
return [{ index: 0, text: split.text, persona: split.persona }];
|
tag: string;
|
||||||
|
content: string;
|
||||||
|
order: number;
|
||||||
|
}> = [];
|
||||||
|
for (const e of board.listTagIndex()) {
|
||||||
|
if (dictateProductFamily(e.tag) !== OPENING_PRODUCT_FAMILY) continue;
|
||||||
|
const item = board.getLatestByTag(e.tag);
|
||||||
|
const content = item?.content?.trim() ?? "";
|
||||||
|
if (!content) continue;
|
||||||
|
const rawOrder = item?.metadata?.[DICTATE_ORDER_META_KEY];
|
||||||
|
const order =
|
||||||
|
typeof rawOrder === "number" && Number.isFinite(rawOrder)
|
||||||
|
? rawOrder
|
||||||
|
: effectiveDictateOrder({ tag: e.tag });
|
||||||
|
rows.push({ tag: e.tag, content, order });
|
||||||
}
|
}
|
||||||
return [];
|
rows.sort((a, b) => {
|
||||||
|
if (a.order !== b.order) return a.order - b.order;
|
||||||
|
return a.tag.localeCompare(b.tag, "zh");
|
||||||
|
});
|
||||||
|
|
||||||
|
return rows.map((row, index) => {
|
||||||
|
const split = splitOpeningDocument(row.content);
|
||||||
|
const text = split.text.trim() || row.content;
|
||||||
|
return {
|
||||||
|
index,
|
||||||
|
tag: row.tag,
|
||||||
|
slot: dictateProductSlot(row.tag),
|
||||||
|
text: presentReadablePlain(text) || text,
|
||||||
|
persona: personaBoundToOpening(board, row.tag, row.content),
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildPlayAuxView(
|
export function buildPlayAuxView(
|
||||||
@@ -100,6 +148,7 @@ export function buildPlayAuxView(
|
|||||||
const selectedOpeningIndexClamped = openings.length
|
const selectedOpeningIndexClamped = openings.length
|
||||||
? Math.min(idx, openings.length - 1)
|
? Math.min(idx, openings.length - 1)
|
||||||
: 0;
|
: 0;
|
||||||
|
const picked = openings[selectedOpeningIndexClamped];
|
||||||
const sealed = readSessionProtagonist(board);
|
const sealed = readSessionProtagonist(board);
|
||||||
const choiceRaw = board.getContentByTag(OPENING_PERSONA_CHOICE_TAG)?.trim();
|
const choiceRaw = board.getContentByTag(OPENING_PERSONA_CHOICE_TAG)?.trim();
|
||||||
const openingPersonaChoice =
|
const openingPersonaChoice =
|
||||||
@@ -111,7 +160,7 @@ export function buildPlayAuxView(
|
|||||||
selectedOpeningIndex: selectedOpeningIndexClamped,
|
selectedOpeningIndex: selectedOpeningIndexClamped,
|
||||||
openingPersona: sealed
|
openingPersona: sealed
|
||||||
? { name: sealed.name, description: sealed.description }
|
? { name: sealed.name, description: sealed.description }
|
||||||
: openings[selectedOpeningIndexClamped]?.persona ?? null,
|
: picked?.persona ?? null,
|
||||||
openingPersonaChoice,
|
openingPersonaChoice,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ describe("dictate play-bind", () => {
|
|||||||
expect(isDictateStyleBindTag("设计.模仿要点")).toBe(true);
|
expect(isDictateStyleBindTag("设计.模仿要点")).toBe(true);
|
||||||
expect(isDictateStyleBindTag("设计.叙事指南")).toBe(true);
|
expect(isDictateStyleBindTag("设计.叙事指南")).toBe(true);
|
||||||
expect(isDictateStyleBindTag("设计.故事推进")).toBe(true);
|
expect(isDictateStyleBindTag("设计.故事推进")).toBe(true);
|
||||||
|
expect(isDictateStyleBindTag("设计.用户需求")).toBe(true);
|
||||||
|
expect(isDictateStyleBindTag("设计.用户约束")).toBe(true);
|
||||||
expect(isDictateStyleBindTag("用户.需求")).toBe(false);
|
expect(isDictateStyleBindTag("用户.需求")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -259,6 +259,36 @@ describe("dictate context", () => {
|
|||||||
});
|
});
|
||||||
expect("error" in overwriteBare).toBe(true);
|
expect("error" in overwriteBare).toBe(true);
|
||||||
|
|
||||||
|
const openingFamilies = new Set(["设计.开场白", "设计.主角设定"]);
|
||||||
|
const firstOpen = resolveRepeatableInsertTag({
|
||||||
|
tag: "设计.开场白",
|
||||||
|
content: "@玩家推开门。",
|
||||||
|
existingTags: [],
|
||||||
|
repeatableFamilies: openingFamilies,
|
||||||
|
});
|
||||||
|
expect("tag" in firstOpen && firstOpen.tag).toBe("设计.开场白");
|
||||||
|
const overwriteFirst = resolveRepeatableInsertTag({
|
||||||
|
tag: "设计.开场白",
|
||||||
|
content: "@玩家改写第一幕。",
|
||||||
|
existingTags: ["设计.开场白"],
|
||||||
|
repeatableFamilies: openingFamilies,
|
||||||
|
});
|
||||||
|
expect("tag" in overwriteFirst && overwriteFirst.tag).toBe("设计.开场白");
|
||||||
|
const secondOpen = resolveRepeatableInsertTag({
|
||||||
|
tag: "设计.开场白#gate",
|
||||||
|
content: "@玩家在闸机前。",
|
||||||
|
existingTags: ["设计.开场白"],
|
||||||
|
repeatableFamilies: openingFamilies,
|
||||||
|
});
|
||||||
|
expect("tag" in secondOpen && secondOpen.tag).toBe("设计.开场白#gate");
|
||||||
|
const bareOpenConflict = resolveRepeatableInsertTag({
|
||||||
|
tag: "设计.开场白",
|
||||||
|
content: "又写一条",
|
||||||
|
existingTags: ["设计.开场白", "设计.开场白#gate"],
|
||||||
|
repeatableFamilies: openingFamilies,
|
||||||
|
});
|
||||||
|
expect("error" in bareOpenConflict).toBe(true);
|
||||||
|
|
||||||
const catalog = await loadModuleCatalog("dialogue/world-simulator");
|
const catalog = await loadModuleCatalog("dialogue/world-simulator");
|
||||||
const index = await buildDictateInsertFeedbackIndex({
|
const index = await buildDictateInsertFeedbackIndex({
|
||||||
catalog: catalog!,
|
catalog: catalog!,
|
||||||
|
|||||||
@@ -175,6 +175,24 @@ describe("opening-seal", () => {
|
|||||||
expect(dotted.text).toBe("门外有人喊 @玩家。");
|
expect(dotted.text).toBe("门外有人喊 @玩家。");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("strips bare 用户角色 meta lines without YAML fence", () => {
|
||||||
|
const bare = splitOpeningDocument(`用户角色.名字: 李宗
|
||||||
|
用户角色.简介: 18cm 男大学生,家境贫穷
|
||||||
|
……@玩家推开宿舍门。
|
||||||
|
`);
|
||||||
|
expect(bare.persona).toEqual({
|
||||||
|
name: "李宗",
|
||||||
|
description: "18cm 男大学生,家境贫穷",
|
||||||
|
});
|
||||||
|
expect(bare.text).toBe("……@玩家推开宿舍门。");
|
||||||
|
|
||||||
|
const onlyMeta = splitOpeningDocument(`用户角色.名字: 李宗
|
||||||
|
用户角色.简介: 男大学生
|
||||||
|
`);
|
||||||
|
expect(onlyMeta.persona?.name).toBe("李宗");
|
||||||
|
expect(onlyMeta.text).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
it("builds table rows from opening variables", () => {
|
it("builds table rows from opening variables", () => {
|
||||||
const payload = parseOpeningSealPayload(fragment, 0);
|
const payload = parseOpeningSealPayload(fragment, 0);
|
||||||
const doc = openingVariablesToTableDoc(payload!.variables);
|
const doc = openingVariablesToTableDoc(payload!.variables);
|
||||||
|
|||||||
@@ -73,4 +73,39 @@ describe("play-aux view", () => {
|
|||||||
expect(view.openings[0]?.text).toBe("@玩家站在债契前,堂屋很静。");
|
expect(view.openings[0]?.text).toBe("@玩家站在债契前,堂屋很静。");
|
||||||
expect(view.openings[0]?.text).not.toContain("present.v1");
|
expect(view.openings[0]?.text).not.toContain("present.v1");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("lists dictate 设计.开场白#slot openings with same-slot 主角设定", () => {
|
||||||
|
const board = new Blackboard();
|
||||||
|
board.write({
|
||||||
|
tag: "设计.开场白#dorm",
|
||||||
|
content: "@玩家推开宿舍门。",
|
||||||
|
source: "agent",
|
||||||
|
});
|
||||||
|
board.write({
|
||||||
|
tag: "设计.开场白#gate",
|
||||||
|
content: "@玩家停在闸机前。",
|
||||||
|
source: "agent",
|
||||||
|
});
|
||||||
|
board.write({
|
||||||
|
tag: "设计.主角设定#dorm",
|
||||||
|
content: JSON.stringify({
|
||||||
|
正文: { 名字: "李宗", 背景: "男大学生", 特殊设定: { 身份: "无", 金手指: "无", 其它: "无" } },
|
||||||
|
}),
|
||||||
|
source: "agent",
|
||||||
|
});
|
||||||
|
board.write({
|
||||||
|
tag: "设计.主角设定#gate",
|
||||||
|
content: JSON.stringify({
|
||||||
|
正文: { 名字: "阿泽", 背景: "社畜", 特殊设定: { 身份: "无", 金手指: "无", 其它: "无" } },
|
||||||
|
}),
|
||||||
|
source: "agent",
|
||||||
|
});
|
||||||
|
const view = buildPlayAuxView(board, { [SLOT_OPENING_SELECTED_INDEX]: 0 });
|
||||||
|
expect(view.openings).toHaveLength(2);
|
||||||
|
expect(view.openings.map((o) => o.slot).sort()).toEqual(["dorm", "gate"]);
|
||||||
|
const dorm = view.openings.find((o) => o.slot === "dorm");
|
||||||
|
const gate = view.openings.find((o) => o.slot === "gate");
|
||||||
|
expect(dorm?.persona?.name).toBe("李宗");
|
||||||
|
expect(gate?.persona?.name).toBe("阿泽");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -155,6 +155,54 @@ describe("protagonist overlay", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("prefers 设计.主角设定 over legacy opening meta", async () => {
|
||||||
|
const {
|
||||||
|
PROTAGONIST_TAG,
|
||||||
|
readSessionProtagonist,
|
||||||
|
ensureProtagonistFromPersona,
|
||||||
|
protagonistFragmentFromPersona,
|
||||||
|
protagonistTagForOpeningSlot,
|
||||||
|
} = await import("../src/persona/protagonist-bind.js");
|
||||||
|
|
||||||
|
const board = new Blackboard();
|
||||||
|
board.write({
|
||||||
|
tag: "设计.开场白",
|
||||||
|
content: `用户角色.名字: 旧名
|
||||||
|
用户角色.简介: 旧简介
|
||||||
|
@玩家推开门。`,
|
||||||
|
source: "agent",
|
||||||
|
});
|
||||||
|
board.write({
|
||||||
|
tag: PROTAGONIST_TAG,
|
||||||
|
content: protagonistFragmentFromPersona({
|
||||||
|
name: "李宗",
|
||||||
|
description: "男大学生",
|
||||||
|
}),
|
||||||
|
source: "agent",
|
||||||
|
});
|
||||||
|
expect(readSessionProtagonist(board)?.name).toBe("李宗");
|
||||||
|
|
||||||
|
const empty = new Blackboard();
|
||||||
|
expect(
|
||||||
|
ensureProtagonistFromPersona(empty, {
|
||||||
|
name: "阿泽",
|
||||||
|
description: "社畜",
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
expect(readSessionProtagonist(empty)?.name).toBe("阿泽");
|
||||||
|
expect(
|
||||||
|
ensureProtagonistFromPersona(empty, {
|
||||||
|
name: "另一个",
|
||||||
|
description: "不覆盖",
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
|
||||||
|
expect(protagonistTagForOpeningSlot("设计.开场白#dorm")).toBe(
|
||||||
|
"设计.主角设定#dorm",
|
||||||
|
);
|
||||||
|
expect(protagonistTagForOpeningSlot("设计.开场白")).toBe(PROTAGONIST_TAG);
|
||||||
|
});
|
||||||
|
|
||||||
it("overlays opening meta 用户角色 when the user switches to that preset", async () => {
|
it("overlays opening meta 用户角色 when the user switches to that preset", async () => {
|
||||||
const { readSessionProtagonist, overlayPlayPersona } = await import(
|
const { readSessionProtagonist, overlayPlayPersona } = await import(
|
||||||
"../src/persona/protagonist-bind.js"
|
"../src/persona/protagonist-bind.js"
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ function openingPanelHtml(aux) {
|
|||||||
const selected = item.index === aux.selectedOpeningIndex;
|
const selected = item.index === aux.selectedOpeningIndex;
|
||||||
const persona = item.persona;
|
const persona = item.persona;
|
||||||
return `<article class="play-open-card${selected ? " is-selected" : ""}" data-opening-index="${item.index}" tabindex="0" role="button">
|
return `<article class="play-open-card${selected ? " is-selected" : ""}" data-opening-index="${item.index}" tabindex="0" role="button">
|
||||||
|
${item.slot ? `<p class="play-open-slot">${esc(item.slot)}</p>` : ""}
|
||||||
<p class="play-open-text">${esc(previewOpening(item.text))}</p>
|
<p class="play-open-text">${esc(previewOpening(item.text))}</p>
|
||||||
${
|
${
|
||||||
persona
|
persona
|
||||||
|
|||||||
Reference in New Issue
Block a user