From 649ef4b7416ce7053145878841fc9b0bff2f5fec Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Wed, 1 Jul 2026 06:32:15 -0400 Subject: feat: admin gui card code unified, unhinged conversation tree GUI sort of implemented --- internal/admin/static/talktree.js | 1084 ++++++++++++++++++++++++++++++++++--- 1 file changed, 1000 insertions(+), 84 deletions(-) (limited to 'internal/admin/static/talktree.js') diff --git a/internal/admin/static/talktree.js b/internal/admin/static/talktree.js index f254c21..e7079f5 100644 --- a/internal/admin/static/talktree.js +++ b/internal/admin/static/talktree.js @@ -1,92 +1,1008 @@ -function renderTalkTree(data) { - var container = document.getElementById('talktree'); - if (!container) return; - if (!data || !data.talk || !data.talk.nodes) { - container.innerHTML = '

No conversation tree defined.

'; - return; - } - window._talkData = data; - var nodes = data.talk.nodes; - var html = '
'; - html += renderTreeNodes(nodes, 'start', {}); - html += '
'; - container.innerHTML = html; -} - -function renderTreeNodes(nodes, rootKey, seen) { - var node = nodes[rootKey]; - if (!node) return ''; - if (seen[rootKey]) { - return '
' + esc(rootKey) + ' (already shown)
'; - } - seen[rootKey] = true; - - var html = ''; - html += '
'; - html += '
'; - html += ''; - html += '' + esc(rootKey) + ''; - html += '' + esc((node.message || '').substring(0, 60)) + ''; - html += '
'; - html += '
'; - - html += '
'; - if (node.message) html += '
Message: ' + esc(node.message) + '
'; - if (node.condition) html += '
Condition: ' + esc(JSON.stringify(node.condition)) + '
'; - if (node.goto) html += '
Goto: ' + esc(node.goto) + '
'; - html += '
'; - - html += '
Options:'; - if (node.options && node.options.length > 0) { - node.options.forEach(function(opt, idx) { - html += '
'; - html += '' + esc(opt.text || '') + ' \u2192 ' + esc(opt.goto || '') + ''; - if (opt.condition) html += ' [cond]'; - if (opt.action) html += ' [action]'; - html += ' '; - html += '
'; +// 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 && cond[CLAUSE_TYPES[i].key] !== '') { + var c = {}; + c[CLAUSE_TYPES[i].key] = cond[CLAUSE_TYPES[i].key]; + if (cond.not) c.not = cond.not; + if (cond.value !== undefined && cond.value !== '') 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 && cond[ck] !== '') { + var cc = {}; + cc[ck] = cond[ck]; + if (cond.not) cc.not = cond.not; + if (cond.value !== undefined && cond.value !== '') 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 && clause[k] !== '') 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 && c[k] !== '') 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 && c[k] !== '') sub[k] = c[k]; } + return sub; }); - } else { - html += '
No options
'; + return cond; + } + + // ---- Action field definitions ---- + + var ACTION_FIELDS = [ + { key: 'give_item', label: 'Give Item', type: 'text' }, + { key: 'take_item', label: 'Take Item', type: 'text' }, + { key: 'teleport', label: 'Teleport Room', type: 'number' }, + { key: 'heal', label: 'Heal HP', type: 'number' }, + { key: 'cost', label: 'Cost (Credits)', type: 'number' }, + { key: 'reputation_cost', label: 'Rep Cost', type: 'number' }, + ]; + + var ACTION_BOOLS = [ + { key: 'assign_task', label: 'Assign Task' }, + { key: 'skip_task', label: 'Skip Task' }, + { key: 'extend_task', label: 'Extend Task' }, + { key: 'sawmill', label: 'Sawmill' }, + { key: 'aps_node', label: 'APS Node' }, + ]; + + // ---- 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); + } else if (tag === 'SELECT') { + talkSyncPath(path, el.value); + } + } + + function talkSyncPath(path, val) { + talkSet(path, val); + talkSync(); } - html += '
'; - html += '
'; - html += '
'; + function talkHandleClick(e) { + var el = e.target; + var action = el.getAttribute('data-action'); + if (!action) return; - if (node.options) { - node.options.forEach(function(opt) { - if (opt.goto && nodes[opt.goto]) { - html += renderTreeNodes(nodes, opt.goto, seen); + 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-sequence': + addSequenceLine(nodeKey); + break; + case 'delete-sequence': + deleteSequenceLine(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; + case 'add-shop-item': + addShopItem(nodeKey, optIdx !== null ? parseInt(optIdx, 10) : null); + break; + case 'remove-shop-item': + removeShopItem(nodeKey, optIdx !== null ? parseInt(optIdx, 10) : null, parseInt(subIdx, 10)); + break; + } + } + + // ---- Render entry point ---- + + window.renderTalkTree = function(data) { + var container = document.getElementById('talktree'); + if (!container) return; + window._talkData = data; + 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; + }; + + // ---- 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] = { message: '', 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) + ' (already shown)
'; + } + seen[rootKey] = true; + + var previewText = getNodePreview(node); + + 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.message) return typeof node.message === 'string' ? node.message : (Array.isArray(node.message) ? node.message[0] : ''); + if (node.sequence && node.sequence.length) return node.sequence[0]; + return ''; + } + + // ---- Node body: condition-gated content ---- + + function renderNodeBody(nodeKey, node, nodes) { + var ec = extractClauses(node.condition); + var hasCond = ec.mode !== 'none'; + + var html = ''; + + if (hasCond) { + html += '
'; + html += '
' + + 'Condition' + + '' + + '
'; + html += '
'; + html += renderConditionClauses(ec, nodeKey, null); + html += '
'; + html += renderMessageSection(nodeKey, node); + html += renderSequenceSection(nodeKey, node); + html += renderActionSection(nodeKey, null, node.action); + html += renderOptionsSection(nodeKey, node); + html += '
'; // .talk-cond-card-body + html += '
'; // .talk-cond-card + } else { + html += ''; + html += renderMessageSection(nodeKey, node); + html += renderSequenceSection(nodeKey, node); + html += renderActionSection(nodeKey, null, node.action); + html += renderOptionsSection(nodeKey, node); + } + + // Goto fallback — only when no options + if (!node.options || node.options.length === 0) { + html += renderGotoFallback(nodeKey, node); + } + + return html; + } + + // ---- Goto fallback ---- + + function renderGotoFallback(nodeKey, node) { + return '
' + + 'Auto-advance to' + + '' + + '
'; + } + + // ---- Message / Sequence ---- + + function renderMessageSection(nodeKey, node) { + var msg = node.message || ''; + if (Array.isArray(msg)) msg = msg.join('\n'); + return '
' + + '
Message
' + + '' + + '
'; + } + + function renderSequenceSection(nodeKey, node) { + var seq = node.sequence || []; + var html = '
'; + html += '
Sequence
'; + seq.forEach(function(line, i) { + html += '
' + + '' + (i + 1) + '' + + '' + + '' + + '
'; + }); + html += '
' + + '' + (seq.length + 1) + '' + + '' + + '' + + '
'; + 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') { + 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 = {}; + } + 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] : ''; + 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 += renderShopEditor(action, prefix, 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 += '
' + + '' + + 'key: ' + esc(k) + '' + + '' + + '
'; }); + html += '
' + + '' + + '' + + '' + + '' + + '
'; + html += '
'; + return html; } - return html; -} - -function addOption(e, nodeKey) { - if (e) e.stopPropagation(); - var data = window._talkData; - if (!data || !data.talk || !data.talk.nodes || !data.talk.nodes[nodeKey]) return; - var text = prompt('Option text:'); - if (text === null) return; - var dest = prompt('Goto node key:') || ''; - var node = data.talk.nodes[nodeKey]; - if (!node.options) node.options = []; - node.options.push({ text: text, goto: dest }); - renderTalkTree(data); - if (typeof window.onTalkTreeChange === 'function') window.onTalkTreeChange(data); -} - -function deleteOption(e, nodeKey, idx) { - if (e) e.stopPropagation(); - var data = window._talkData; - if (!data || !data.talk || !data.talk.nodes || !data.talk.nodes[nodeKey]) return; - var node = data.talk.nodes[nodeKey]; - if (!node.options || idx < 0 || idx >= node.options.length) return; - if (!confirm('Delete this option?')) return; - node.options.splice(idx, 1); - renderTalkTree(data); - if (typeof window.onTalkTreeChange === 'function') window.onTalkTreeChange(data); -} + function renderShopEditor(action, prefix, nodeKey, optIdx) { + var shop = action && action.shop; + var hasShop = shop && typeof shop === 'object' && !Array.isArray(shop); + var html = '
'; + html += '
Shop
'; + + if (hasShop) { + html += '
' + + 'Shop Message' + + '' + + '
'; + + var items = shop.items || []; + html += '
Items
'; + items.forEach(function(item, si) { + var ip = prefix + '.shop.items.' + si; + html += '
' + + '' + + '' + + '' + + '' + + '
'; + }); + html += ''; + } else { + 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 += ''; + 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)) { + html += ''; + } + if (opt.action) { + html += ''; + } + }); + } else { + html += '
none
'; + } + + html += '
' + + '' + + '' + + '' + + '' + + '' + + '
'; + + 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'; + }; + + // ---- 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 addSequenceLine(nodeKey) { + var input = document.getElementById('talkSeqAdd_' + nodeKey); + var text = input ? input.value.trim() : ''; + var node = talkNodes()[nodeKey]; + if (!node) return; + if (!node.sequence) node.sequence = []; + node.sequence.push(text); + renderTalkTree(window._talkData); + talkSync(); + } + + function deleteSequenceLine(nodeKey, idx) { + var node = talkNodes()[nodeKey]; + if (!node || !node.sequence) return; + node.sequence.splice(idx, 1); + if (node.sequence.length === 0) delete node.sequence; + renderTalkTree(window._talkData); + talkSync(); + } + + // ---- Add/Remove Action ---- + + function addAction(nodeKey, optIdx) { + var target = getConditionFor(nodeKey, optIdx); + if (!target) return; + target.action = {}; + 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 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] = true; + 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(); + } + + // ---- Add/Remove Shop Items ---- + + function addShopItem(nodeKey, optIdx) { + var actionPath = optIdx !== null ? 'nodes.' + nodeKey + '.options.' + optIdx + '.action' : 'nodes.' + nodeKey + '.action'; + var action = talkGet(actionPath); + if (!action || typeof action !== 'object') { + action = {}; + talkSet(actionPath, action); + } + if (!action.shop) { action.shop = { items: [] }; } + if (!action.shop.items) action.shop.items = []; + action.shop.items.push({ item_id: '', buy_price: 0, sell_price: 0 }); + renderTalkTree(window._talkData); + talkSync(); + } + + function removeShopItem(nodeKey, optIdx, idx) { + var actionPath = optIdx !== null ? 'nodes.' + nodeKey + '.options.' + optIdx + '.action' : 'nodes.' + nodeKey + '.action'; + var action = talkGet(actionPath); + if (!action || !action.shop || !action.shop.items) return; + action.shop.items.splice(idx, 1); + if (action.shop.items.length === 0) delete action.shop; + 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('talkSeqAdd_')) { + e.preventDefault(); + var nk = el.id.replace('talkSeqAdd_', ''); + addSequenceLine(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; + } + }); + +})(); -- cgit v1.2.3