/** * 游玩辅助面:表格 / 开场与角色 等 Tab 的视图截面。 */ import type { Blackboard } from "../blackboard/blackboard.js"; import { isTableCellEditable, parseTableDoc, } from "../blackboard/table-cells.js"; import { dictateProductFamily, dictateProductSlot, } from "../dictate/repeatable-tags.js"; import { DICTATE_ORDER_META_KEY, effectiveDictateOrder, } from "../dictate/types.js"; import { OPENING_PERSONA_CHOICE_TAG, OPENING_PRODUCT_FAMILY, OPENING_SETUP_ARTIFACT_TAG, ZERO_LAYER_OPENING_FAMILY, SLOT_OPENING_SELECTED_INDEX, parseOpeningSealPayload, splitOpeningDocument, type OpeningUserPersona, } from "./opening-seal.js"; import { CURRENT_VARS_TAG } from "./variable-catalog.js"; import { personaBoundToOpening, 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; /** 产物 tag:设计.开场白 或 设计.开场白#槽位;opening-setup 候选为假想 tag */ tag: string; slot: string; 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 | 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; } /** 黑板上全部可选用开场(工序候选优先;否则对话落盘 设计.开场白[#槽]) */ export 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, ); const tag = `${OPENING_SETUP_ARTIFACT_TAG}#${index + 1}`; return { index, tag, slot: String(index + 1), text: presentReadablePlain(text) || text, persona: one?.persona ?? payload.persona, }; }); } const rows: Array<{ tag: string; content: string; order: number; }> = []; for (const e of board.listTagIndex()) { const family = dictateProductFamily(e.tag); if ( family !== OPENING_PRODUCT_FAMILY && family !== ZERO_LAYER_OPENING_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 }); } 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( board: Blackboard, slots?: Record, ): 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 picked = openings[selectedOpeningIndexClamped]; 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 } : picked?.persona ?? null, openingPersonaChoice, }; }