174 lines
5.1 KiB
TypeScript
174 lines
5.1 KiB
TypeScript
/**
|
|
* 游玩辅助面:表格 / 开场与角色 等 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<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;
|
|
}
|
|
|
|
/** 黑板上全部可选用开场(工序候选优先;否则对话落盘 设计.开场白[#槽]) */
|
|
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<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 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,
|
|
};
|
|
}
|