接通主角绑定、开场封印与游玩辅助,让世界模拟器从创作连续落到本局。

直聘插入按模块回馈,并记录请求骨架便于排查。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-22 07:40:08 +08:00
co-authored by Cursor
parent 9306c8e659
commit dd0a9e87d8
88 changed files with 6869 additions and 786 deletions
+117
View File
@@ -0,0 +1,117 @@
/**
* 游玩辅助面:表格 / 开场与角色 等 Tab 的视图截面。
*/
import type { Blackboard } from "../blackboard/blackboard.js";
import {
isTableCellEditable,
parseTableDoc,
} from "../blackboard/table-cells.js";
import {
OPENING_PERSONA_CHOICE_TAG,
OPENING_SETUP_ARTIFACT_TAG,
SLOT_OPENING_SELECTED_INDEX,
parseOpeningSealPayload,
splitOpeningDocument,
type OpeningUserPersona,
} from "./opening-seal.js";
import { CURRENT_VARS_TAG } from "./variable-catalog.js";
import { readSessionProtagonist } from "../persona/protagonist-bind.js";
import { presentReadablePlain } from "./present-packet.js";
export type PlayAuxTableRow = {
key: string;
value: unknown;
rev: number;
visibility: "visible" | "hidden";
editable: boolean;
note?: string;
};
export type PlayAuxOpening = {
index: number;
text: string;
persona: OpeningUserPersona | null;
};
export type PlayAuxView = {
table: { tag: string; rows: PlayAuxTableRow[] } | null;
openings: PlayAuxOpening[];
selectedOpeningIndex: number;
openingPersona: OpeningUserPersona | null;
openingPersonaChoice: "opening" | "global" | null;
};
function selectedOpeningIndex(slots: Record<string, unknown> | undefined): number {
const raw = slots?.[SLOT_OPENING_SELECTED_INDEX];
if (typeof raw === "number" && Number.isFinite(raw)) return Math.max(0, Math.trunc(raw));
if (typeof raw === "string" && raw.trim()) {
const n = Number(raw);
if (Number.isFinite(n)) return Math.max(0, Math.trunc(n));
}
return 0;
}
function collectOpenings(board: Blackboard): PlayAuxOpening[] {
const payload = parseOpeningSealPayload(
board.getContentByTag(OPENING_SETUP_ARTIFACT_TAG),
0,
);
if (payload?.candidates.length) {
return payload.candidates.map((text, index) => {
const one = parseOpeningSealPayload(
board.getContentByTag(OPENING_SETUP_ARTIFACT_TAG),
index,
);
return {
index,
text: presentReadablePlain(text) || text,
persona: one?.persona ?? payload.persona,
};
});
}
const split = splitOpeningDocument(board.getContentByTag("设计.开场白"));
if (split.text) {
return [{ index: 0, text: split.text, persona: split.persona }];
}
return [];
}
export function buildPlayAuxView(
board: Blackboard,
slots?: Record<string, unknown>,
): PlayAuxView {
const tableDoc = parseTableDoc(board.getContentByTag(CURRENT_VARS_TAG));
const table = tableDoc?.rows.length
? {
tag: CURRENT_VARS_TAG,
rows: tableDoc.rows.map((row) => ({
key: row.key,
value: row.value,
rev: row.rev,
visibility: row.visibility === "hidden" ? "hidden" : "visible",
editable: isTableCellEditable(row),
...(row.note ? { note: row.note } : {}),
})),
}
: null;
const openings = collectOpenings(board);
const idx = selectedOpeningIndex(slots);
const selectedOpeningIndexClamped = openings.length
? Math.min(idx, openings.length - 1)
: 0;
const sealed = readSessionProtagonist(board);
const choiceRaw = board.getContentByTag(OPENING_PERSONA_CHOICE_TAG)?.trim();
const openingPersonaChoice =
choiceRaw === "opening" || choiceRaw === "global" ? choiceRaw : null;
return {
table,
openings,
selectedOpeningIndex: selectedOpeningIndexClamped,
openingPersona: sealed
? { name: sealed.name, description: sealed.description }
: openings[selectedOpeningIndexClamped]?.persona ?? null,
openingPersonaChoice,
};
}