/** * 游玩辅助 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 ? `` : `

    没有可见字段。

    `; const hid = hidden.length ? `
    不可见 ${hidden.length}
    ` : ""; 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?.(); }); }