引入上下文片段、固定槽与投影排序,并完善机遇裁定与游玩期 UI。

把创作产物收敛为可挂载片段与 play_slots/context_order,同步修订世界模拟器模块与运行时拼装。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-03 01:36:32 +08:00
parent 00dfcb6615
commit 94f67fa744
69 changed files with 7786 additions and 1283 deletions

View File

@@ -0,0 +1,231 @@
/**
* 主世界层 → 叙事转述的裁决包settlement / direction packet
* 存黑板 tag运行.本轮.裁决
*/
import { extractJsonObjectText } from "./worker-set-parse.js";
export const SETTLEMENT_SCHEMA = "settlement.v1" as const;
export const SETTLEMENT_TAG = "运行.本轮.裁决";
export type SettlementNpcMove = {
who: string;
move: string;
wants?: string;
refuses_to_say?: string;
};
export type SettlementVariableChange = {
key: string;
from?: unknown;
to?: unknown;
delta?: number;
note?: string;
};
export type SettlementPacket = {
schema: typeof SETTLEMENT_SCHEMA;
/** 玩家本轮行动摘要 */
player_action?: string;
/** 已落地的客观变化(短句列表) */
resolved?: string[];
/** 转述应呈现的场面现状(玩家安全) */
visible_now?: string;
/** NPC 主动动作binding move */
npc_moves?: SettlementNpcMove[];
/** 真值变更提议Runtime 合并进 变量.当前) */
variable_changes?: SettlementVariableChange[];
/** 禁止写入用户正文的内容 */
do_not_say?: string[];
/** 语气提示(通常来自 Progressive 投影) */
tone_hint?: string;
/** 可选:建议玩家选项(无主语动作短语) */
suggested_actions?: string[];
};
export type SettlementPacketView = {
ok: boolean;
parseError?: string;
packet?: SettlementPacket;
/** 用户友好分节,供 UI 渲染 */
sections: Array<{ title: string; lines: string[] }>;
};
function asString(v: unknown): string | undefined {
if (typeof v !== "string") return undefined;
const t = v.trim();
return t || undefined;
}
function asStringList(v: unknown): string[] {
if (!Array.isArray(v)) return [];
return v.map((x) => asString(x)).filter((x): x is string => Boolean(x));
}
function parseNpcMoves(raw: unknown): SettlementNpcMove[] | undefined {
if (!Array.isArray(raw)) return undefined;
const out: SettlementNpcMove[] = [];
for (const item of raw) {
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
const row = item as Record<string, unknown>;
const who = asString(row.who) ?? asString(row.actor) ?? asString(row.name);
const move = asString(row.move) ?? asString(row.action);
if (!who || !move) continue;
out.push({
who,
move,
wants: asString(row.wants),
refuses_to_say: asString(row.refuses_to_say) ?? asString(row.refusesToSay),
});
}
return out.length ? out : undefined;
}
function parseVariableChanges(
raw: unknown,
): SettlementVariableChange[] | undefined {
if (!Array.isArray(raw)) return undefined;
const out: SettlementVariableChange[] = [];
for (const item of raw) {
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
const row = item as Record<string, unknown>;
const key = asString(row.key) ?? asString(row.name) ?? asString(row.);
if (!key) continue;
const delta =
typeof row.delta === "number" && Number.isFinite(row.delta)
? row.delta
: undefined;
out.push({
key,
from: row.from ?? row.,
to: row.to ?? row. ?? row.value,
delta,
note: asString(row.note) ?? asString(row.),
});
}
return out.length ? out : undefined;
}
export function normalizeSettlementPacket(
row: Record<string, unknown>,
): SettlementPacket {
const resolved = asStringList(row.resolved);
const resolvedZh = asStringList(row.);
const doNot = asStringList(row.do_not_say);
const doNotZh = asStringList(row.);
const suggested = asStringList(row.suggested_actions);
const suggestedZh = asStringList(row.);
return {
schema: SETTLEMENT_SCHEMA,
player_action: asString(row.player_action) ?? asString(row.),
resolved: resolved.length ? resolved : resolvedZh.length ? resolvedZh : undefined,
visible_now: asString(row.visible_now) ?? asString(row.),
npc_moves: parseNpcMoves(row.npc_moves ?? row.npcMoves ?? row.),
variable_changes: parseVariableChanges(
row.variable_changes ?? row.variableChanges ?? row.,
),
do_not_say: doNot.length ? doNot : doNotZh.length ? doNotZh : undefined,
tone_hint: asString(row.tone_hint) ?? asString(row.),
suggested_actions: suggested.length
? suggested
: suggestedZh.length
? suggestedZh
: undefined,
};
}
export function parseSettlementPacket(raw: string | undefined | null): SettlementPacketView {
const text = raw?.trim();
if (!text) {
return { ok: false, parseError: "空裁决包", sections: [] };
}
const jsonText = extractJsonObjectText(text) ?? (text.startsWith("{") ? text : null);
if (!jsonText) {
// 兼容旧散文裁决:整段当作 visible_now
return {
ok: true,
packet: {
schema: SETTLEMENT_SCHEMA,
visible_now: text,
},
sections: [{ title: "场面(未结构化)", lines: [text] }],
};
}
try {
const doc = JSON.parse(jsonText) as unknown;
if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
return { ok: false, parseError: "裁决包不是 JSON 对象", sections: [] };
}
const packet = normalizeSettlementPacket(doc as Record<string, unknown>);
return { ok: true, packet, sections: settlementSections(packet) };
} catch (err) {
return {
ok: false,
parseError: err instanceof Error ? err.message : "裁决包 JSON 解析失败",
sections: [],
};
}
}
export function settlementSections(packet: SettlementPacket): Array<{
title: string;
lines: string[];
}> {
const sections: Array<{ title: string; lines: string[] }> = [];
if (packet.player_action) {
sections.push({ title: "玩家行动", lines: [packet.player_action] });
}
if (packet.resolved?.length) {
sections.push({ title: "已落地", lines: packet.resolved });
}
if (packet.visible_now) {
sections.push({ title: "可见现状", lines: [packet.visible_now] });
}
if (packet.npc_moves?.length) {
sections.push({
title: "角色动作",
lines: packet.npc_moves.map((m) => {
const extra = [m.wants && `想要:${m.wants}`, m.refuses_to_say && `回避:${m.refuses_to_say}`]
.filter(Boolean)
.join("");
return extra ? `${m.who}${m.move}${extra}` : `${m.who}${m.move}`;
}),
});
}
if (packet.variable_changes?.length) {
sections.push({
title: "变量变更",
lines: packet.variable_changes.map((c) => {
if (c.delta != null) {
return `${c.key} ${c.delta >= 0 ? "+" : ""}${c.delta}` + (c.note ? `${c.note}` : "");
}
if (c.from !== undefined || c.to !== undefined) {
return `${c.key}${String(c.from ?? "?")}${String(c.to ?? "?")}`;
}
return c.key;
}),
});
}
if (packet.tone_hint) {
sections.push({ title: "语气", lines: [packet.tone_hint] });
}
if (packet.do_not_say?.length) {
sections.push({ title: "勿写入正文", lines: packet.do_not_say });
}
if (packet.suggested_actions?.length) {
sections.push({ title: "建议行动", lines: packet.suggested_actions });
}
return sections;
}
export function isSettlementLikeObject(doc: unknown): boolean {
if (!doc || typeof doc !== "object" || Array.isArray(doc)) return false;
const row = doc as Record<string, unknown>;
if (row.schema === SETTLEMENT_SCHEMA) return true;
// 启发式:有裁决包典型键
const keys = ["player_action", "visible_now", "resolved", "npc_moves", "variable_changes"];
let hits = 0;
for (const k of keys) if (k in row) hits += 1;
return hits >= 2;
}