/**
* 游玩辅助 Tab:表格 / 开场与角色 / 对话。
* 隐藏格默认挡住,点字段才露;手改带格子 rev。
*/
import { parsePresentDoc } from "./present-shells.js";
let playAuxTab = "table";
let revealedHidden = new Set();
let editingKey = null;
function esc(s) {
return String(s ?? "")
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """);
}
function formatValue(value) {
if (value === null || value === undefined) return "";
if (typeof value === "object") {
try {
return JSON.stringify(value);
} catch {
return String(value);
}
}
return String(value);
}
function coerceValue(prev, raw) {
const text = String(raw ?? "").trim();
if (typeof prev === "boolean") {
if (text === "true" || text === "是" || text === "1") return true;
if (text === "false" || text === "否" || text === "0") return false;
return prev;
}
if (typeof prev === "number") {
const n = Number(text);
return Number.isFinite(n) ? n : prev;
}
return text;
}
function previewOpening(text) {
let source = String(text || "").trim();
if (source.startsWith("{")) {
try {
const view = parsePresentDoc(JSON.parse(source));
const body = view?.packet?.blocks?.body;
if (typeof body === "string" && body.trim()) source = body.trim();
} catch {
/* 保持原文 */
}
}
const flat = source.replace(/\s+/g, " ").trim();
if (flat.length <= 72) return flat;
return `${flat.slice(0, 72)}…`;
}
function rowHtml(row, { hidden, revealed, editing, busy }) {
const shown = !hidden || revealed;
const val = shown ? formatValue(row.value) : "••••••";
const edit =
shown && row.editable && editing
? `
`
: "";
const action =
shown && row.editable && !editing
? ``
: "";
return `
${action}
${edit}
`;
}
function tablePanelHtml(table, busy) {
if (!table?.rows?.length) {
return `本局没有状态表。
`;
}
const visible = table.rows.filter((r) => r.visibility !== "hidden");
const hidden = table.rows.filter((r) => r.visibility === "hidden");
const vis =
visible.length > 0
? `${visible
.map((row) =>
rowHtml(row, {
hidden: false,
revealed: true,
editing: editingKey === row.key,
busy,
}),
)
.join("")}
`
: `没有可见字段。
`;
const hid = hidden.length
? `
不可见 ${hidden.length}
${hidden
.map((row) =>
rowHtml(row, {
hidden: true,
revealed: revealedHidden.has(row.key),
editing: editingKey === row.key,
busy,
}),
)
.join("")}
`
: "";
return `${vis}${hid}
`;
}
function openingPanelHtml(aux) {
const openings = aux?.openings ?? [];
if (!openings.length) {
return `没有可选开场。使用当前用户角色卡。
`;
}
const cards = openings
.map((item) => {
const selected = item.index === aux.selectedOpeningIndex;
const persona = item.persona;
return `
${esc(previewOpening(item.text))}
${
persona
? `${esc(persona.name)}${
persona.description
? ` · ${esc(persona.description)}`
: ""
}
`
: `无预设角色
`
}
`;
})
.join("");
const usingOpening = aux.openingPersonaChoice === "opening";
return `
${cards}
`;
}
function tabBtn(id, label, current) {
return ``;
}
export function fillPlayAux(view, handlers = {}, extra = {}) {
const rail = document.getElementById("coord-rail");
const drawerBody = document.getElementById("coord-drawer-body");
const title = document.getElementById("coord-rail-title");
const drawerCount = document.getElementById("coord-drawer-count");
if (!rail || !drawerBody) return;
rail.hidden = false;
const aux = view.playAux;
const head = rail.querySelector(".coord-rail-head");
if (head) {
let tabs = head.querySelector(".play-aux-tabs");
if (!tabs) {
tabs = document.createElement("nav");
tabs.className = "play-aux-tabs";
tabs.setAttribute("aria-label", "游玩辅助");
const collapse = head.querySelector(".coord-rail-collapse");
if (collapse) head.insertBefore(tabs, collapse);
else head.appendChild(tabs);
}
tabs.hidden = false;
tabs.innerHTML = [
tabBtn("table", "表格", playAuxTab),
tabBtn("opening", "开场", playAuxTab),
].join("");
tabs.onclick = (e) => {
const btn = e.target.closest("[data-play-aux-tab]");
if (!btn) return;
const id = btn.getAttribute("data-play-aux-tab");
if (!id || id === playAuxTab) return;
playAuxTab = id;
fillPlayAux(view, handlers, extra);
};
}
if (title) title.textContent = "游玩";
if (drawerCount) drawerCount.textContent = "";
const busy = Boolean(extra.busy);
if (playAuxTab === "table") {
drawerBody.innerHTML = tablePanelHtml(aux?.table, busy);
wireTable(drawerBody, aux?.table, handlers, busy, () =>
fillPlayAux(view, handlers, extra),
);
return;
}
if (playAuxTab === "opening") {
drawerBody.innerHTML = openingPanelHtml(aux);
wireOpening(drawerBody, handlers, busy);
}
}
export function currentPlayAuxTab() {
return playAuxTab;
}
export function hidePlayAuxTabs() {
const tabs = document.querySelector(".play-aux-tabs");
if (tabs) tabs.hidden = true;
}
function wireTable(root, table, handlers, busy, rerender) {
root.querySelectorAll("[data-table-reveal]").forEach((btn) => {
btn.addEventListener("click", () => {
const key = btn.getAttribute("data-table-reveal");
const row = btn.closest(".play-table-row");
if (!key || !row) return;
if (row.getAttribute("data-hidden") === "1" && !revealedHidden.has(key)) {
revealedHidden.add(key);
rerender?.();
}
});
});
root.querySelectorAll("[data-table-fix]").forEach((btn) => {
btn.addEventListener("click", (e) => {
e.preventDefault();
editingKey = btn.getAttribute("data-table-fix");
rerender?.();
});
});
root.querySelectorAll("[data-table-cancel]").forEach((btn) => {
btn.addEventListener("click", () => {
editingKey = null;
rerender?.();
});
});
root.querySelectorAll("[data-table-edit]").forEach((form) => {
form.addEventListener("submit", (e) => {
e.preventDefault();
if (busy) return;
const key = form.getAttribute("data-table-edit");
const row = table?.rows?.find((r) => r.key === key);
if (!key || !row) return;
const next = coerceValue(
row.value,
form.querySelector("input[name=value]")?.value,
);
editingKey = null;
handlers.onPatchTable?.(table.tag, [
{ key, value: next, expectedRev: row.rev },
]);
});
});
}
function wireOpening(root, handlers, busy) {
root.querySelectorAll("[data-opening-index]").forEach((card) => {
const go = () => {
if (busy) return;
const index = Number(card.getAttribute("data-opening-index"));
if (!Number.isFinite(index)) return;
const hasPersona = Boolean(card.querySelector(".play-open-persona strong"));
handlers.onSelectOpening?.(index, hasPersona ? true : undefined);
};
card.addEventListener("click", go);
card.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
go();
}
});
});
root.querySelector(".play-open-refuse")?.addEventListener("click", () => {
if (busy) return;
handlers.onRefuseOpeningPersona?.();
});
}