// 分支树渲染(从 ir_to_html.py 的 TEMPLATE 抽出,加 onSelect 回调供编辑)。 // 用法: renderTree(ir, { onSelect:id=>{}, selected:'n1' }) (function () { const ROW = 150, SP = 232, NW = 188; const COLOR = { next:"#6a6256", option:"#7aa0d8", random:"#a07ad8", win:"#7ac88a", lose:"#d87878", ref:"#9e9ef0" }; function roleNames(ir) { const m = {}; (ir.roles || []).forEach(r => m[r.slot] = r.name + (r.archetype ? ("〔" + r.archetype + "〕") : "")); return m; } function nameOf(ir, names, s) { return (s === "P1" ? "玩家" : "") || names[s] || s; } function grantStr(ir, names, gr) { if (gr.kind === "银两") return "银两 " + (gr.value > 0 ? "+" : "") + gr.value; if (gr.kind === "道具") return "道具 " + gr.item + " ×" + gr.value; if (gr.kind === "友好度") return nameOf(ir, names, gr.target) + " 友好度+" + gr.value; if (gr.kind === "入门") return nameOf(ir, names, gr.target) + " 加入门派"; return JSON.stringify(gr); } function summary(ir, names, n) { if (n.kind === "narration") return ["旁白", (n.text || "").slice(0, 28)]; if (n.kind === "dialogue") return ["对话 · " + nameOf(ir, names, n.speaker), (n.text || "").slice(0, 24)]; if (n.kind === "choice") return ["选择 (" + (n.options || []).length + "项)", (n.options || []).map(o => o.text).join(" / ").slice(0, 30)]; if (n.kind === "choice_once") return ["一次性选择", (n.options || []).map(o => o.text).join(" / ").slice(0, 30)]; if (n.kind === "random") return ["随机分支", (n.branches || []).length + " 路"]; if (n.kind === "fight") return ["战斗", "vs " + (n.camp2 || []).map(s => nameOf(ir, names, s)).join("、")]; if (n.kind === "move") return ["走位 · " + nameOf(ir, names, n.actor), "→ " + (n.to || "")]; if (n.kind === "anim") return ["动画 · " + nameOf(ir, names, n.actor), n.ani || ""]; if (n.kind === "reward") return ["奖励结算", ""]; if (n.kind === "out_ref") return ["引用子序列", "→ " + (n.ref || "")]; if (n.kind === "ending") return ["★ 结局", n.summary || ""]; return [n.kind, ""]; } window.renderTree = function (ir, opts) { opts = opts || {}; const names = roleNames(ir); const layersDiv = document.getElementById("layers"); const svg = document.getElementById("svg"); layersDiv.innerHTML = ""; svg.innerHTML = ""; // 节点 (含结局) const nodes = {}; (ir.nodes || []).forEach(n => nodes[n.id] = Object.assign({ _end: false }, n)); (ir.endings || []).forEach(e => nodes[e.id] = Object.assign({ _end: true, kind: "ending" }, e)); // 边 const edges = []; const add = (u, v, type, label) => { if (v && nodes[v]) edges.push({ u, v, type, label: label || "" }); }; (ir.nodes || []).forEach(n => { if (n.next) add(n.id, n.next, n.kind === "out_ref" ? "ref" : "next"); (n.options || []).forEach(o => add(n.id, o.goto, "option", o.text)); (n.branches || []).forEach(b => add(n.id, b.goto, "random", "权重" + (b.weight != null ? b.weight : ""))); if (n.kind === "fight") { add(n.id, n.win, "win", "胜"); add(n.id, n.lose, "lose", "败"); } }); // 最长路径分层 const layer = {}; Object.keys(nodes).forEach(id => layer[id] = 0); let changed = true, guard = 0; while (changed && guard++ < 999) { changed = false; edges.forEach(e => { if (layer[e.v] < layer[e.u] + 1) { layer[e.v] = layer[e.u] + 1; changed = true; } }); } // 子树居中布局 const childMap = {}; Object.keys(nodes).forEach(id => childMap[id] = []); const indeg = {}; Object.keys(nodes).forEach(id => indeg[id] = 0); const seenE = new Set(); edges.forEach(e => { const k = e.u + ">" + e.v; if (!seenE.has(k)) { seenE.add(k); childMap[e.u].push(e.v); indeg[e.v]++; } }); let roots = Object.keys(nodes).filter(id => indeg[id] === 0); if (!roots.length) roots = [Object.keys(nodes)[0]]; const xpos = {}; let nextX = 0; const vis = new Set(); function assignX(id) { if (!id || vis.has(id)) return; vis.add(id); if (childMap[id].length === 0) { xpos[id] = nextX; nextX += SP; return; } childMap[id].forEach(assignX); const placed = childMap[id].map(c => xpos[c]).filter(v => v !== undefined); xpos[id] = placed.length ? (Math.min(...placed) + Math.max(...placed)) / 2 : (nextX += SP, nextX - SP); } roots.forEach(assignX); Object.keys(nodes).forEach(id => { if (xpos[id] === undefined) { xpos[id] = nextX; nextX += SP; } }); let maxX = 0, maxL = 0; Object.keys(nodes).forEach(id => { const n = nodes[id], [k, t] = summary(ir, names, n); const d = document.createElement("div"); d.className = "node kind-" + n.kind + (id === opts.selected ? " sel" : ""); d.id = "node-" + id; let inner = '