// talktree.js — Full-featured conversation tree editor. // Inline editing for all TalkNode and TalkOption fields. // No prompt() dialogs. Clause-based condition editor, action forms. (function() { 'use strict'; // ---- Path helpers ---- function talkRoot() { return window._talkData && window._talkData.talk; } function talkNodes() { var r = talkRoot(); return r && r.nodes; } function talkSync() { if (typeof window.onTalkTreeChange === 'function') window.onTalkTreeChange(window._talkData); } function talkGet(path) { var parts = path.split('.'), obj = talkRoot(); for (var i = 0; i < parts.length; i++) { if (obj == null) return undefined; var k = parts[i]; obj = /^\d+$/.test(k) ? obj[parseInt(k, 10)] : obj[k]; } return obj; } function talkSet(path, value) { var parts = path.split('.'), root = talkRoot(); for (var i = 0; i < parts.length - 1; i++) { var k = parts[i]; if (/^\d+$/.test(k)) { var idx = parseInt(k, 10); if (root[idx] == null) root[idx] = /^\d+$/.test(parts[i + 1]) ? [] : {}; root = root[idx]; } else { if (root[k] == null) root[k] = /^\d+$/.test(parts[i + 1]) ? [] : {}; root = root[k]; } } var lk = parts[parts.length - 1]; if (/^\d+$/.test(lk)) { root[parseInt(lk, 10)] = value; } else { root[lk] = value; } } function talkDel(path) { var parts = path.split('.'), root = talkRoot(); for (var i = 0; i < parts.length - 1; i++) { if (root == null) return; var k = parts[i]; root = /^\d+$/.test(k) ? root[parseInt(k, 10)] : root[k]; } if (!root) return; var lk = parts[parts.length - 1]; if (/^\d+$/.test(lk)) { root.splice(parseInt(lk, 10), 1); } else { delete root[lk]; } } // ---- Clause types ---- var CLAUSE_TYPES = [ { key: 'flag', label: 'Flag', hasValue: true }, { key: 'player_flag', label: 'Player Flag', hasValue: true }, { key: 'has_item', label: 'Has Item', hasValue: false }, { key: 'min_credits', label: 'Min Credits', hasValue: false } ]; function clauseTypeLabel(key) { for (var i = 0; i < CLAUSE_TYPES.length; i++) { if (CLAUSE_TYPES[i].key === key) return CLAUSE_TYPES[i].label; } return key; } function clauseTypeHasValue(key) { for (var i = 0; i < CLAUSE_TYPES.length; i++) { if (CLAUSE_TYPES[i].key === key) return CLAUSE_TYPES[i].hasValue; } return false; } // Extract clauses from a condition object function extractClauses(cond) { if (!cond || typeof cond !== 'object' || Array.isArray(cond)) return { mode: 'none', clauses: [], groupKey: null }; if (cond.all_of && Array.isArray(cond.all_of) && cond.all_of.length > 0) { return { mode: 'multi', clauses: cond.all_of.slice(), groupKey: 'all_of' }; } if (cond.any_of && Array.isArray(cond.any_of) && cond.any_of.length > 0) { return { mode: 'multi', clauses: cond.any_of.slice(), groupKey: 'any_of' }; } // Single clause from flat fields for (var i = 0; i < CLAUSE_TYPES.length; i++) { if (cond[CLAUSE_TYPES[i].key] !== undefined) { var c = {}; c[CLAUSE_TYPES[i].key] = cond[CLAUSE_TYPES[i].key]; if (cond.not) c.not = cond.not; if (cond.value !== undefined) c.value = cond.value; if (cond.has_item && CLAUSE_TYPES[i].key !== 'has_item') c.has_item = cond.has_item; if (cond.player_flag && CLAUSE_TYPES[i].key !== 'player_flag') c.player_flag = cond.player_flag; if (cond.flag && CLAUSE_TYPES[i].key !== 'flag') c.flag = cond.flag; return { mode: 'single', clauses: [c], groupKey: null }; } } // Check for has_item or min_credits as the only key for (var j = 0; j < CLAUSE_TYPES.length; j++) { var ck = CLAUSE_TYPES[j].key; if (cond[ck] !== undefined) { var cc = {}; cc[ck] = cond[ck]; if (cond.not) cc.not = cond.not; if (cond.value !== undefined) cc.value = cond.value; return { mode: 'single', clauses: [cc], groupKey: null }; } } return { mode: 'none', clauses: [], groupKey: null }; } // Determine the type key for a clause object function clauseObjType(clause) { for (var i = 0; i < CLAUSE_TYPES.length; i++) { var k = CLAUSE_TYPES[i].key; if (clause[k] !== undefined) return k; } return null; } // Build a full condition object from clauses function buildCondition(clauses, groupKey) { if (!clauses || clauses.length === 0) return null; if (clauses.length === 1) { var flat = {}; var c = clauses[0]; for (var k in c) { if (c[k] !== undefined) flat[k] = c[k]; } return flat; } var cond = {}; cond[groupKey || 'all_of'] = clauses.map(function(c) { var sub = {}; for (var k in c) { if (c[k] !== undefined) sub[k] = c[k]; } return sub; }); return cond; } // ---- Action field definitions ---- var ACTION_FIELDS = [ { key: 'give_item', label: 'Give Item', type: 'item_search' }, { key: 'take_item', label: 'Take Item', type: 'item_search' }, { key: 'credits', label: 'Credits (+/-)', type: 'number' }, { key: 'teleport', label: 'TP Room', type: 'number' }, { key: 'heal', label: 'Heal HP', type: 'number' }, ]; var ACTION_BOOLS = []; // ---- Event delegation ---- function talkEvent(e) { var el = e.target; var path = el.getAttribute('data-tp'); if (!path) return; var tag = el.tagName; var type = el.type; if (tag === 'INPUT' || tag === 'TEXTAREA') { var val; if (type === 'checkbox') { val = el.checked; } else if (type === 'number') { var n = parseFloat(el.value); val = isNaN(n) ? 0 : n; } else { val = el.value; } talkSyncPath(path, val); if (path.endsWith('.goto')) renderTalkTree(window._talkData); } else if (tag === 'SELECT') { talkSyncPath(path, el.value); } } function talkSyncPath(path, val) { talkSet(path, val); talkSync(); } function talkHandleClick(e) { var el = e.target; var action = el.getAttribute('data-action'); if (!action) return; var nodeKey = el.getAttribute('data-nk'); var optIdx = el.getAttribute('data-oi'); var subIdx = el.getAttribute('data-si'); var subKey = el.getAttribute('data-sk'); var condGroupKey = el.getAttribute('data-cgk'); switch (action) { case 'delete-node': deleteNode(nodeKey); break; case 'add-option': addOption(nodeKey); break; case 'delete-option': deleteOption(nodeKey, parseInt(optIdx, 10)); break; case 'add-node': addNode(); break; case 'add-message': addMessageLine(nodeKey); break; case 'delete-message': deleteMessageLine(nodeKey, parseInt(optIdx, 10)); break; case 'add-condition': toggleCondition(nodeKey, optIdx !== null ? parseInt(optIdx, 10) : null); break; case 'remove-condition': removeCondition(nodeKey, optIdx !== null ? parseInt(optIdx, 10) : null); break; case 'add-action': addAction(nodeKey, optIdx !== null ? parseInt(optIdx, 10) : null); break; case 'remove-action': removeAction(nodeKey, optIdx !== null ? parseInt(optIdx, 10) : null); break; case 'add-clause': addClause(nodeKey, optIdx !== null ? parseInt(optIdx, 10) : null, el.getAttribute('data-ct')); break; case 'remove-clause': removeClause(nodeKey, optIdx !== null ? parseInt(optIdx, 10) : null, parseInt(subIdx, 10)); break; case 'set-cond-group': setCondGroup(nodeKey, optIdx !== null ? parseInt(optIdx, 10) : null, condGroupKey); break; case 'add-kv': addKVPair(nodeKey, optIdx !== null ? parseInt(optIdx, 10) : null, el.getAttribute('data-kvk')); break; case 'remove-kv': removeKVPair(nodeKey, optIdx !== null ? parseInt(optIdx, 10) : null, el.getAttribute('data-kvk'), subKey); break; } } // ---- Render entry point ---- window.renderTalkTree = function(data) { var container = document.getElementById('talktree'); if (!container) return; window._talkData = data; window._talkOpenNodes = window._talkOpenNodes || new Set(); window._talkOpenNodes.add('start'); if (!data || !data.talk || !data.talk.nodes) { container.innerHTML = '

No conversation tree defined.

'; return; } var nodes = data.talk.nodes; var visited = {}; var html = '
'; html += ''; Object.keys(nodes).forEach(function(k) { html += ''; html += renderTreeNodes(nodes, 'start', visited); var allKeys = Object.keys(nodes); var orphanKeys = []; for (var i = 0; i < allKeys.length; i++) { if (!visited[allKeys[i]]) orphanKeys.push(allKeys[i]); } if (orphanKeys.length > 0) { html += '
'; html += '
Unreferenced Nodes
'; html += '
These nodes are not reachable from start. Add a goto to them from another node\'s option.
'; for (var j = 0; j < orphanKeys.length; j++) { html += renderOrphanNode(orphanKeys[j], nodes[orphanKeys[j]], nodes); } html += '
'; } html += renderAddNodeRow(); html += '
'; container.innerHTML = html; if (typeof ced_setupSearchFields === 'function') { setTimeout(function() { ced_setupSearchFields(); }, 10); } }; // ---- Add/Delete Node ---- function renderAddNodeRow() { return '
' + '' + '' + '
'; } function addNode() { var input = document.getElementById('talkNewNodeKey'); if (!input) return; var key = input.value.trim(); if (!key) return; if (talkNodes()[key]) { notify('Node "' + key + '" already exists.', 'error'); return; } talkNodes()[key] = { messages: [], options: [] }; input.value = ''; renderTalkTree(window._talkData); talkSync(); } function deleteNode(nodeKey) { if (nodeKey === 'start') { notify('Cannot delete the start node.', 'error'); return; } var nodes = talkNodes(); if (!nodes || !nodes[nodeKey]) return; delete nodes[nodeKey]; Object.keys(nodes).forEach(function(k) { var n = nodes[k]; if (n.goto === nodeKey) delete n.goto; if (n.options) { n.options.forEach(function(opt) { if (opt.goto === nodeKey) opt.goto = ''; }); } }); renderTalkTree(window._talkData); talkSync(); } // ---- Orphan node ---- function renderOrphanNode(key, node, nodes) { var h = '
'; h += '
'; h += ''; h += '' + esc(key) + ''; h += '' + esc((getNodePreview(node) || '').substring(0, 60)) + ''; h += ''; h += ''; h += '
'; h += '
'; h += renderNodeBody(key, node, nodes); h += '
'; h += '
'; return h; } // ---- Recursive tree render ---- function renderTreeNodes(nodes, rootKey, seen, indent) { indent = indent || 0; var node = nodes[rootKey]; if (!node) return ''; if (seen[rootKey]) { return '
' + esc(rootKey) + ' (see above)
'; } seen[rootKey] = true; var previewText = getNodePreview(node); var isOpen = window._talkOpenNodes.has(rootKey); var html = '
'; // Header — fix: check for data-action before toggling html += '
'; html += ''; html += '' + esc(rootKey) + ''; html += '' + esc((previewText || '').substring(0, 60)) + ''; html += ''; html += ''; html += '
'; // Body html += '
'; html += renderNodeBody(rootKey, node, nodes); html += '
'; // .talk-node-body // Children var childKeys = []; if (node.goto && nodes[node.goto]) childKeys.push(node.goto); if (node.options) { node.options.forEach(function(opt) { if (opt.goto && nodes[opt.goto] && childKeys.indexOf(opt.goto) < 0) childKeys.push(opt.goto); }); } if (childKeys.length > 0) { html += '
'; childKeys.forEach(function(k) { html += renderTreeNodes(nodes, k, seen, indent + 1); }); html += '
'; } html += '
'; // .talk-node return html; } function getNodePreview(node) { if (node.messages && node.messages.length) return node.messages[0]; return ''; } // ---- Node body: condition-gated content ---- // Track open node state across re-renders window._talkOpenNodes = window._talkOpenNodes || new Set(); function renderNodeBody(nodeKey, node, nodes) { var ec = extractClauses(node.condition); var hasCond = ec.mode !== 'none' || (node.condition && typeof node.condition === 'object' && !Array.isArray(node.condition)); var hasOpts = node.options && node.options.length > 0; var html = ''; if (hasCond) { html += '
'; html += '
' + 'Condition' + '' + '
'; html += '
'; html += renderConditionClauses(ec, nodeKey, null); html += '
'; html += renderMessagesSection(nodeKey, node); html += renderOptionsSection(nodeKey, node); html += '
'; var nodeAction = node.action; var actionPrefix = 'nodes.' + escAttr(nodeKey) + '.action'; if (nodeAction && typeof nodeAction === 'object' && !Array.isArray(nodeAction)) { html += '
'; html += '
' + 'Action' + '
'; html += '
'; html += renderActionForm(nodeAction, actionPrefix, nodeKey, null); html += '
'; html += '
'; } else { html += ''; } html += '
'; html += renderGotoFallback(nodeKey, node, 'If condition fails, go to'); html += '
'; // .talk-cond-card-body html += '
'; // .talk-cond-card } else { html += ''; html += renderMessagesSection(nodeKey, node); html += renderOptionsSection(nodeKey, node); html += '
'; html += renderActionSection(nodeKey, null, node.action); if (!hasOpts) { html += renderGotoFallback(nodeKey, node, 'Auto-advance to'); } } return html; } // ---- Goto fallback ---- function renderGotoFallback(nodeKey, node, label) { label = label || 'Auto-advance to'; return '
' + '' + esc(label) + '' + '' + '
'; } // ---- Messages section ---- function renderMessagesSection(nodeKey, node) { var msgs = node.messages || []; var html = '
'; html += '
Messages
'; msgs.forEach(function(line, i) { html += '
' + '' + '' + '' + '
'; }); html += '
' + '' + '' + '' + '
'; html += '
'; return html; } // ---- Condition clauses ---- function renderConditionClauses(ec, nodeKey, optIdx) { var clauses = ec.clauses; var groupKey = ec.groupKey || 'all_of'; var html = ''; // AND/OR mode selector (only when 2+ clauses) if (clauses.length >= 2) { html += '
' + 'Combine with' + '' + '
'; } // Clause rows for (var i = 0; i < clauses.length; i++) { var typeKey = clauseObjType(clauses[i]) || 'flag'; var pathPrefix = clauses.length === 1 ? 'nodes.' + escAttr(nodeKey) + (optIdx !== null ? '.options.' + optIdx : '') + '.condition' : 'nodes.' + escAttr(nodeKey) + (optIdx !== null ? '.options.' + optIdx : '') + '.condition.' + groupKey + '.' + i; html += renderClauseRow(clauses[i], pathPrefix, typeKey, nodeKey, optIdx, i); } // Add clause buttons html += '
'; CLAUSE_TYPES.forEach(function(ct) { html += ''; }); html += '
'; return html; } function renderClauseRow(clause, pathPrefix, typeKey, nodeKey, optIdx, clauseIdx) { var hasValue = clauseTypeHasValue(typeKey); var html = '
'; html += '' + esc(clauseTypeLabel(typeKey)) + ''; if (typeKey === 'flag' || typeKey === 'player_flag') { var notLabel = (clause.value !== undefined && clause.value !== '') ? 'Not' : 'Not Exists'; html += 'key:'; html += ''; html += ''; html += 'val:'; html += ''; } else if (typeKey === 'has_item') { html += 'item:'; html += ''; html += ''; } else if (typeKey === 'min_credits') { html += 'cr:'; html += ''; html += ''; } html += ''; html += '
'; return html; } // ---- Clause add/remove/migrate ---- function getConditionFor(nodeKey, optIdx) { var node = talkNodes()[nodeKey]; if (!node) return null; if (optIdx !== null) { if (!node.options || !node.options[optIdx]) return null; return node.options[optIdx]; } return node; } function condAddClause(target, typeKey, optIdx, nodeKey) { var ec = extractClauses(target.condition); var newClause = {}; newClause[typeKey] = ''; ec.clauses.push(newClause); if (ec.clauses.length === 1) { // First clause: use flat fields target.condition = buildCondition(ec.clauses, null); } else { ec.groupKey = ec.groupKey || 'all_of'; target.condition = buildCondition(ec.clauses, ec.groupKey); } } function addClause(nodeKey, optIdx, typeKey) { var target = getConditionFor(nodeKey, optIdx); if (!target) return; if (!target.condition || typeof target.condition !== 'object' || Array.isArray(target.condition)) { target.condition = {}; } condAddClause(target, typeKey, optIdx, nodeKey); renderTalkTree(window._talkData); talkSync(); } function removeClause(nodeKey, optIdx, clauseIdx) { var target = getConditionFor(nodeKey, optIdx); if (!target) return; var ec = extractClauses(target.condition); if (clauseIdx < 0 || clauseIdx >= ec.clauses.length) return; ec.clauses.splice(clauseIdx, 1); if (ec.clauses.length === 0) { delete target.condition; } else { target.condition = buildCondition(ec.clauses, ec.groupKey); } renderTalkTree(window._talkData); talkSync(); } function setCondGroup(nodeKey, optIdx, newGroupKey) { var target = getConditionFor(nodeKey, optIdx); if (!target) return; var ec = extractClauses(target.condition); if (ec.clauses.length < 2) return; ec.groupKey = newGroupKey; target.condition = buildCondition(ec.clauses, ec.groupKey); renderTalkTree(window._talkData); talkSync(); } // Called from onchange on the AND/OR select window.talkCondGroupChange = function(select) { var nodeKey = select.getAttribute('data-nk'); var optIdx = select.getAttribute('data-oi'); setCondGroup(nodeKey, optIdx !== null ? parseInt(optIdx, 10) : null, select.value); }; // ---- Add/Remove whole Condition ---- function toggleCondition(nodeKey, optIdx) { var target = getConditionFor(nodeKey, optIdx); if (!target) return; if (target.condition && typeof target.condition === 'object' && !Array.isArray(target.condition)) { delete target.condition; } else { target.condition = {}; if (optIdx !== null) { window._talkAutoOpenDetails = new Set(); window._talkAutoOpenDetails.add('cond_' + optIdx + '_' + escAttr(nodeKey)); } } renderTalkTree(window._talkData); talkSync(); } function removeCondition(nodeKey, optIdx) { var target = getConditionFor(nodeKey, optIdx); if (!target) return; delete target.condition; renderTalkTree(window._talkData); talkSync(); } // ---- Action editor ---- function renderActionSection(nodeKey, optIdx, action) { var prefix = optIdx !== null ? 'nodes.' + escAttr(nodeKey) + '.options.' + optIdx + '.action' : 'nodes.' + escAttr(nodeKey) + '.action'; var hasAct = action && typeof action === 'object' && !Array.isArray(action); if (!hasAct) { return '
' + '' + '
'; } return '
' + '
' + 'Action' + '' + '
' + renderActionForm(action, prefix, nodeKey, optIdx) + '
'; } function renderActionForm(action, prefix, nodeKey, optIdx) { var html = '
'; html += '
'; ACTION_FIELDS.forEach(function(f) { var fpath = prefix + '.' + f.key; var val = (action && action[f.key] !== undefined) ? action[f.key] : ''; if (f.type === 'item_search') { html += '
' + '' + esc(f.label) + '' + '
' + '' + '' + '
' + '
'; } else { var narrowStyle = (f.key === 'teleport' || f.key === 'heal') ? ' style="max-width:70px"' : ''; html += '
' + '' + esc(f.label) + '' + '' + '
'; } }); html += '
'; html += '
'; ACTION_BOOLS.forEach(function(b) { html += ''; }); html += '
'; html += renderKVEditor(action, prefix, 'set_flags', 'Set Flags', nodeKey, optIdx); html += renderKVEditor(action, prefix, 'set_player_flags', 'Set Player Flags', nodeKey, optIdx); html += '
'; // .talk-action-block return html; } function renderKVEditor(action, prefix, key, label, nodeKey, optIdx) { var kv = (action && action[key]) || {}; var keys = Object.keys(kv); var fcID = prefix.replace(/\./g, '_') + '_' + key; var addKeyID = 'kvAddKey_' + fcID; var nkAttr = escAttr(nodeKey); var oiAttr = optIdx !== null ? ' data-oi="' + optIdx + '"' : ''; var html = '
'; html += '
'; html += '' + esc(label) + ''; html += '
'; keys.forEach(function(k) { html += '
' + '' + '' + '' + '
'; }); html += '
' + '' + '' + '' + '' + '
'; html += '
'; return html; } // ---- Options section ---- function renderOptionsSection(nodeKey, node) { var options = node.options || []; var html = '
'; html += 'Options'; if (options.length > 0) { options.forEach(function(opt, idx) { html += '
'; html += '' + (idx + 1) + ''; html += ''; html += ''; html += '
'; if (opt.condition && typeof opt.condition === 'object' && !Array.isArray(opt.condition)) { html += 'cond'; } else { html += ''; } if (opt.action) { html += 'act'; } else { html += ''; } html += '
'; html += ''; html += '
'; if (opt.condition && typeof opt.condition === 'object' && !Array.isArray(opt.condition)) { var condDetailKey = 'cond_' + idx + '_' + escAttr(nodeKey); var condOpen = window._talkAutoOpenDetails && window._talkAutoOpenDetails.has(condDetailKey) ? 'block' : 'none'; html += '
'; html += '
'; html += '
Condition' + '
'; html += '
'; html += renderConditionClauses(extractClauses(opt.condition), nodeKey, idx); html += '
'; html += '
'; html += '
'; } if (opt.action) { var actDetailKey = 'act_' + idx + '_' + escAttr(nodeKey); var actOpen = window._talkAutoOpenDetails && window._talkAutoOpenDetails.has(actDetailKey) ? 'block' : 'none'; html += '
'; html += '
'; html += '
' + 'Action' + '
'; html += '
'; html += renderActionForm(opt.action, 'nodes.' + escAttr(nodeKey) + '.options.' + idx + '.action', nodeKey, idx); html += '
'; html += '
'; html += '
'; } }); } else { html += '
none
'; } html += '
' + '' + (options.length + 1) + '' + '' + '' + '' + '' + '
'; html += '
'; // .talk-options return html; } window.toggleOptionDetail = function(e, type, idx, nodeKey) { e.stopPropagation(); var el = document.getElementById('optDetail_' + type + '_' + idx + '_' + nodeKey); if (el) el.style.display = el.style.display === 'none' ? 'block' : 'none'; }; window.talkToggleNode = function(nodeKey, nodeEl) { var open = nodeEl.classList.toggle('open'); if (open) { window._talkOpenNodes.add(nodeKey); } else { window._talkOpenNodes.delete(nodeKey); } }; // ---- Add/Delete Option ---- function addOption(nodeKey) { var textInput = document.getElementById('talkOptText_' + nodeKey); var gotoInput = document.getElementById('talkOptGoto_' + nodeKey); var text = textInput ? textInput.value.trim() : ''; var dest = gotoInput ? gotoInput.value.trim() : ''; var node = talkNodes()[nodeKey]; if (!node) return; if (!node.options) node.options = []; node.options.push({ text: text, goto: dest }); if (textInput) textInput.value = ''; if (gotoInput) gotoInput.value = ''; renderTalkTree(window._talkData); talkSync(); } function deleteOption(nodeKey, idx) { var node = talkNodes()[nodeKey]; if (!node || !node.options || idx < 0 || idx >= node.options.length) return; node.options.splice(idx, 1); renderTalkTree(window._talkData); talkSync(); } // ---- Add/Delete Sequence Line ---- function addMessageLine(nodeKey) { var input = document.getElementById('talkMsgAdd_' + nodeKey); var text = input ? input.value.trim() : ''; var node = talkNodes()[nodeKey]; if (!node) return; if (!node.messages) node.messages = []; node.messages.push(text); renderTalkTree(window._talkData); talkSync(); } function deleteMessageLine(nodeKey, idx) { var node = talkNodes()[nodeKey]; if (!node || !node.messages) return; node.messages.splice(idx, 1); if (node.messages.length === 0) delete node.messages; renderTalkTree(window._talkData); talkSync(); } // ---- Add/Remove Action ---- function addAction(nodeKey, optIdx) { var target = getConditionFor(nodeKey, optIdx); if (!target) return; target.action = {}; if (optIdx !== null) { window._talkAutoOpenDetails = new Set(); window._talkAutoOpenDetails.add('act_' + optIdx + '_' + escAttr(nodeKey)); } renderTalkTree(window._talkData); talkSync(); } function removeAction(nodeKey, optIdx) { var target = getConditionFor(nodeKey, optIdx); if (!target) return; delete target.action; renderTalkTree(window._talkData); talkSync(); } // ---- Add/Remove KV Pairs (set_flags/set_player_flags) ---- function kvPath(nodeKey, optIdx, kvKey, flagKey) { var base = optIdx !== null ? 'nodes.' + nodeKey + '.options.' + optIdx + '.action.' + kvKey : 'nodes.' + nodeKey + '.action.' + kvKey; return flagKey ? base + '.' + flagKey : base; } function addKVPair(nodeKey, optIdx, kvKey) { var prefixBase = optIdx !== null ? 'nodes.' + nodeKey + '.options.' + optIdx + '.action' : 'nodes.' + nodeKey + '.action'; var fcID = prefixBase.replace(/\./g, '_') + '_' + kvKey; var addKeyID = 'kvAddKey_' + fcID; var input = document.getElementById(addKeyID); var key = input ? input.value.trim() : ''; if (!key) return; var valInput = document.getElementById(addKeyID + '_val'); var rawVal = valInput ? valInput.value.trim() : 'true'; var val = rawVal === 'true' ? true : rawVal === 'false' ? false : rawVal; var numVal = parseFloat(rawVal); if (!isNaN(numVal) && String(numVal) === rawVal) val = numVal; var basePath = kvPath(nodeKey, optIdx, kvKey); var obj = talkGet(basePath); if (!obj || typeof obj !== 'object') { talkSet(basePath, {}); obj = talkGet(basePath); } if (obj[key] !== undefined) { notify('Flag "' + key + '" already exists.', 'error'); return; } obj[key] = val; renderTalkTree(window._talkData); talkSync(); } function removeKVPair(nodeKey, optIdx, kvKey, flagKey) { var fullPath = kvPath(nodeKey, optIdx, kvKey, flagKey); talkDel(fullPath); var basePath = kvPath(nodeKey, optIdx, kvKey); var obj = talkGet(basePath); if (obj && typeof obj === 'object' && Object.keys(obj).length === 0) { talkDel(basePath); } renderTalkTree(window._talkData); talkSync(); } window.talkRenameKV = function(el) { var nodeKey = el.getAttribute('data-nk'); var optIdxStr = el.getAttribute('data-oi'); var optIdx = optIdxStr !== null ? parseInt(optIdxStr, 10) : null; var kvKey = el.getAttribute('data-kvk'); var oldKey = el.getAttribute('data-oldkey'); var newKey = el.value.trim(); if (!newKey || newKey === oldKey) return; var basePath = optIdx !== null ? 'nodes.' + nodeKey + '.options.' + optIdx + '.action.' + kvKey : 'nodes.' + nodeKey + '.action.' + kvKey; var obj = talkGet(basePath); if (!obj || obj[newKey] !== undefined) { el.value = oldKey; if (obj && obj[newKey] !== undefined) notify('Key "' + newKey + '" already exists.', 'error'); return; } obj[newKey] = obj[oldKey]; delete obj[oldKey]; renderTalkTree(window._talkData); talkSync(); }; // ---- Global event listeners ---- document.addEventListener('input', function(e) { var el = e.target; if (!el.closest || !el.closest('#talktree')) return; if (!el.getAttribute('data-tp')) return; talkEvent(e); }); document.addEventListener('change', function(e) { var el = e.target; if (!el.closest || !el.closest('#talktree')) return; talkEvent(e); }); document.addEventListener('click', function(e) { var el = e.target; if (!el.closest || !el.closest('#talktree')) return; talkHandleClick(e); }); document.addEventListener('keydown', function(e) { if (e.key !== 'Enter') return; var el = e.target; if (!el.closest || !el.closest('#talktree')) return; if (el.id === 'talkNewNodeKey') { e.preventDefault(); addNode(); return; } if (el.id && (el.id.startsWith('talkOptText_') || el.id.startsWith('talkOptGoto_'))) { e.preventDefault(); var nodeKey = el.id.replace('talkOptText_', '').replace('talkOptGoto_', ''); addOption(nodeKey); return; } if (el.id && el.id.startsWith('talkMsgAdd_')) { e.preventDefault(); var nk = el.id.replace('talkMsgAdd_', ''); addMessageLine(nk); return; } if (el.id && el.id.startsWith('kvAddKey_')) { e.preventDefault(); var nk = el.getAttribute('data-nk'); var oi = el.getAttribute('data-oi'); var kvk = el.getAttribute('data-kvk'); addKVPair(nk, oi !== null ? parseInt(oi, 10) : null, kvk); return; } }); })();