引入上下文片段、固定槽与投影排序,并完善机遇裁定与游玩期 UI。
把创作产物收敛为可挂载片段与 play_slots/context_order,同步修订世界模拟器模块与运行时拼装。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
231
src/skills/settlement-packet.ts
Normal file
231
src/skills/settlement-packet.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user