diff options
| author | historia <[not public]> | 2026-07-10 18:53:26 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-07-10 18:53:26 -0400 |
| commit | b31958c6304a25279197c1a08b924b6b44798a99 (patch) | |
| tree | 35cea781069f4a5ccbd03f96cb8b0d35af056f90 | |
| parent | 141161781dd26bc0a84b0d50b291368b5bce0e54 (diff) | |
| download | thehouseoficarus-b31958c6304a25279197c1a08b924b6b44798a99.tar.gz | |
feat(admin): use intereditor.js for mob talk editor to further unify condition/action system
| -rw-r--r-- | internal/admin/static/admin.css | 3 | ||||
| -rw-r--r-- | internal/admin/static/intereditor.js | 523 | ||||
| -rw-r--r-- | internal/admin/static/talktree.js | 731 |
3 files changed, 504 insertions, 753 deletions
diff --git a/internal/admin/static/admin.css b/internal/admin/static/admin.css index 953c392..efe5abc 100644 --- a/internal/admin/static/admin.css +++ b/internal/admin/static/admin.css @@ -389,7 +389,7 @@ g.ud-hover:hover text{font-weight:bold} .ie-cond-addbar{display:grid;grid-template-columns:repeat(auto-fit,minmax(max(100%/5,110px),1fr));gap:4px;margin-top:4px} .ie-add-btn{font-size:10px!important;padding:2px 6px!important;background:var(--accent)!important;color:var(--text)!important;border:1px solid #1a5a8e!important;font-family:monospace!important;border-radius:3px!important;cursor:pointer;white-space:nowrap} .ie-add-btn:hover{background:#1a5a8e!important;color:#fff!important} -.ie-cond-addbar .ie-add-btn,.ie-step-addbar .ie-add-btn,.ie-step-addbar-bottom .ie-add-btn{width:100%;min-width:0} +.ie-cond-addbar .ie-add-btn,.ie-step-addbar .ie-add-btn,.ie-step-addbar-bottom .ie-add-btn,.ie-act-addbar .ie-add-btn{width:100%;min-width:0} /* Action editor */ .ie-act-root{background:rgba(100,160,255,.04);border:1px solid rgba(100,160,255,.12);border-radius:5px;padding:8px;display:flex;flex-direction:column;gap:6px} @@ -441,6 +441,7 @@ g.ud-hover:hover text{font-weight:bold} .ie-step-addbar{display:none;flex-wrap:wrap;gap:4px;margin-top:2px} .ie-step-row.show-addbar .ie-step-addbar{display:grid;grid-template-columns:repeat(auto-fit,minmax(max(100%/5,110px),1fr))} .ie-step-addbar-bottom{display:grid;grid-template-columns:repeat(auto-fit,minmax(max(100%/5,110px),1fr));gap:4px;margin-top:6px} +.ie-act-addbar{display:grid;grid-template-columns:repeat(auto-fit,minmax(max(100%/5,110px),1fr));gap:4px;margin-top:2px} /* ── Message rows within a step ── */ .ie-msg-row{display:flex;align-items:center;gap:4px} diff --git a/internal/admin/static/intereditor.js b/internal/admin/static/intereditor.js index 0e2c03d..152aef3 100644 --- a/internal/admin/static/intereditor.js +++ b/internal/admin/static/intereditor.js @@ -51,6 +51,132 @@ IE_ACT_TYPES.forEach(function(at) { IE_STEP_KINDS.push(at); }); +// ── Scope / Context system ── +// Lets condition & action editors target data models outside the +// default ed[secPath][idx].steps[si] arrangement (e.g. talk node/option +// conditions and single-step actions). When a scope is registered and its +// token is placed on a DOM root via data-ie-scope, all mutation onclicks +// resolve their data target through the scope instead of the legacy trigger +// path. + +window._ieScopes = {}; +var _ieScopeSeq = 0; + +function ie_registerScope(opts, token) { + if (!token) { + token = 'sc' + (++_ieScopeSeq); + } + window._ieScopes[token] = opts; + return token; +} +window.ie_registerScope = ie_registerScope; + +function ie_unregisterScope(token) { + delete window._ieScopes[token]; +} +window.ie_unregisterScope = ie_unregisterScope; + +function _ie_scopeFromEl(el) { + if (!el || !el.closest) return null; + var scopeEl = el.closest('[data-ie-scope]'); + if (!scopeEl) return null; + var token = scopeEl.getAttribute('data-ie-scope'); + return window._ieScopes[token] || null; +} + +// Resolve the .ie-cond-root DOM element scoped to a mutation element el. +// Handles both the root itself and the outer addbar (which sits outside +// .ie-cond-root as a sibling). +function _ie_condScopeRoot(el) { + var root = el.closest('.ie-cond-root'); + if (root) return root; + var addbar = el.closest('.ie-cond-addbar'); + if (!addbar) return null; + var parent = addbar.parentElement; + if (!parent) return null; + return parent.querySelector('.ie-cond-root'); +} + +// Unified condition-editing context. +// Returns { get, set, change } — `get` always syncs DOM → data store +// before returning. Routes through a registered scope when el lives inside +// [data-ie-scope], otherwise through the legacy ed[secPath][idx] trigger path. +function _ie_condCtx(el, secId, secPath, idx, kind) { + var scope = _ie_scopeFromEl(el); + if (scope) { + return { + get: function() { + var root = _ie_condScopeRoot(el); + if (root) { + var cond = ie_collectCond(root); + scope.setCond(cond); + } + return scope.getCond(); + }, + set: function(c) { scope.setCond(c); }, + change: function() { scope.onChange(); } + }; + } + return { + get: function() { + var ed = window._editorData; + if (!ed) return null; + _ie_syncCond(ed, secId, secPath, idx, kind); + return _ie_getCond(ed, secPath, idx, kind); + }, + set: function(c) { + var ed = window._editorData; + if (!ed) return; + _ie_setCond(ed, secPath, idx, kind, c); + ced_syncDOMToData(ed); + }, + change: function() { window._ieRenderOnly(); } + }; +} + +// Unified action/step-editing context. +// Returns { get, set, change }. In scope mode reads the .ie-act-root +// container; in legacy mode operates on ed[secPath][idx].steps[si]. +function _ie_actCtx(el, secId, secPath, idx, si) { + var scope = _ie_scopeFromEl(el); + if (scope) { + return { + get: function() { + var container = el.closest('.ie-act-root'); + if (!container) return scope.getStep() || {}; + var act = ie_collectAct(container); + var msgs = ie_collectMessages(container); + var step = scope.getStep() || {}; + if (act) Object.keys(act).forEach(function(k) { step[k] = act[k]; }); + if (msgs !== null) step.messages = msgs; else delete step.messages; + scope.setStep(step); + return scope.getStep(); + }, + set: function(s) { scope.setStep(s); }, + change: function() { scope.onChange(); } + }; + } + return { + get: function() { + var ed = window._editorData; + if (!ed) return {}; + ie_syncStep(ed, secId, secPath, idx, si); + if (!ed[secPath] || !ed[secPath][idx] || !ed[secPath][idx].steps || !ed[secPath][idx].steps[si]) return {}; + return ed[secPath][idx].steps[si]; + }, + set: function(s) { + var ed = window._editorData; + if (!ed) return; + if (!ed[secPath]) ed[secPath] = []; + if (!ed[secPath][idx]) ed[secPath][idx] = {steps: []}; + if (!ed[secPath][idx].steps) ed[secPath][idx].steps = []; + ed[secPath][idx].steps[si] = s; + ced_syncDOMToData(ed); + }, + change: function() { window._ieRenderOnly(); } + }; +} + // ── Rendering helpers ── // ie_renderStepAddbar: the per-step action add bar revealed by the "+" toggle. @@ -67,9 +193,9 @@ function ie_renderStepAddbar(step, secId, idx, si, secPath) { } } if (sk.key === 'messages') { - h += '<button class="btn btn-sm ie-add-btn" onclick="ie_addMessages(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ')">+ ' + esc(sk.label) + '</button>'; + h += '<button class="btn btn-sm ie-add-btn" onclick="ie_addMessages(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ')">+ ' + esc(sk.label) + '</button>'; } else { - h += '<button class="btn btn-sm ie-add-btn" onclick="ie_addActEffect(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',\'' + escAttr(sk.key) + '\')">+ ' + esc(sk.label) + '</button>'; + h += '<button class="btn btn-sm ie-add-btn" onclick="ie_addActEffect(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',\'' + escAttr(sk.key) + '\')">+ ' + esc(sk.label) + '</button>'; } }); return h; @@ -80,7 +206,7 @@ function ie_renderStepAddbar(step, secId, idx, si, secPath) { function ie_renderBottomAddbar(secId, secPath, idx) { var h = ''; IE_STEP_KINDS.forEach(function(sk) { - h += '<button class="btn btn-sm ie-add-btn" onclick="ie_addStepKind(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(sk.key) + '\')">+ ' + esc(sk.label) + '</button>'; + h += '<button class="btn btn-sm ie-add-btn" onclick="ie_addStepKind(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(sk.key) + '\')">+ ' + esc(sk.label) + '</button>'; }); return h; } @@ -142,7 +268,7 @@ function ie_collectDropTableBlock(container) { } // ie_addStepKind creates a new step initialised with a single action kind. -function ie_addStepKind(secId, secPath, idx, kind) { +function ie_addStepKind(el, secId, secPath, idx, kind) { var ed = window._editorData; if (!ed) return; ced_syncDOMToData(ed); @@ -387,6 +513,8 @@ function ie_collectLeaf(el) { function ie_renderCond(condObj, secId, idx, secPath, kind, opts) { kind = kind || 'entry'; + opts = opts || {}; + var scopeAttr = opts.scopeToken ? ' data-ie-scope="' + escAttr(opts.scopeToken) + '"' : ''; var h = ''; var isGroup = condObj && (condObj.all_of || condObj.any_of); var rootChildCount = isGroup ? (condObj.all_of || condObj.any_of || []).length : 0; @@ -395,21 +523,21 @@ function ie_renderCond(condObj, secId, idx, secPath, kind, opts) { if (kind && kind.indexOf('step-') === 0) { var sn = parseInt(kind.slice(5), 10); if (!isNaN(sn)) condTitleText = 'Condition for Step ' + (sn + 1); - } else if (kind === 'entry') { + } else if (kind === 'entry' && !opts.scopeToken) { condTitleText = 'Condition for Action Block'; } - h += '<div class="ie-cond-root">'; + h += '<div class="ie-cond-root"' + scopeAttr + '>'; if (condTitleText) h += '<div class="ie-cond-title">' + esc(condTitleText) + '</div>'; h += ie_renderGroup(condObj, secId, secPath, idx, '', kind); h += '</div>'; } - var showAddbar = !isGroup || (opts && opts.alwaysOuterAddbar && rootChildCount === 0); + var showAddbar = (!isGroup || (opts.alwaysOuterAddbar && rootChildCount === 0)) && typeof condObj === 'object'; if (showAddbar) { - h += '<div class="ie-cond-addbar">'; + h += '<div class="ie-cond-addbar"' + scopeAttr + '>'; IE_COND_TYPES.forEach(function(ct) { - h += '<button class="btn btn-sm ie-add-btn ie-cond-add-leaf" onclick="ie_addOuterLeaf(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\',\'' + ct.type + '\')">+ ' + esc(ct.label) + '</button>'; + h += '<button class="btn btn-sm ie-add-btn ie-cond-add-leaf" onclick="ie_addOuterLeaf(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\',\'' + ct.type + '\')">+ ' + esc(ct.label) + '</button>'; }); - h += '<button class="btn btn-sm ie-add-btn ie-cond-add-grp" onclick="ie_addOuterGroup(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\')">+ Group</button>'; + h += '<button class="btn btn-sm ie-add-btn ie-cond-add-grp" onclick="ie_addOuterGroup(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\')">+ Group</button>'; h += '</div>'; } return h; @@ -442,10 +570,10 @@ function _ie_renderGroupNode(mode, children, not, secId, secPath, idx, gpath, ki h += '<div class="ie-cond ie-cond-empty" data-ie-gpath="' + escAttr(gpath) + '">'; h += '<div class="ie-cond-modebar ie-cond-empty-bar">'; IE_COND_TYPES.forEach(function(ct) { - h += '<button class="btn btn-sm ie-add-btn ie-cond-add-leaf" onclick="ie_addLeaf(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\',\'' + escAttr(gpath) + '\',\'' + ct.type + '\')">+ ' + esc(ct.label) + '</button>'; + h += '<button class="btn btn-sm ie-add-btn ie-cond-add-leaf" onclick="ie_addLeaf(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\',\'' + escAttr(gpath) + '\',\'' + ct.type + '\')">+ ' + esc(ct.label) + '</button>'; }); - h += '<button class="btn btn-sm ie-add-btn ie-cond-add-grp" onclick="ie_addGroup(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\',\'' + escAttr(gpath) + '\')">+ Group</button>'; - h += '<button class="btn btn-sm btn-danger ie-cond-grp-rm" onclick="ie_removeGroup(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\',\'' + escAttr(gpath) + '\')" title="Remove this group">X</button>'; + h += '<button class="btn btn-sm ie-add-btn ie-cond-add-grp" onclick="ie_addGroup(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\',\'' + escAttr(gpath) + '\')">+ Group</button>'; + h += '<button class="btn btn-sm btn-danger ie-cond-grp-rm" onclick="ie_removeGroup(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\',\'' + escAttr(gpath) + '\')" title="Remove this group">X</button>'; h += '</div>'; h += '</div>'; return h; @@ -454,13 +582,13 @@ function _ie_renderGroupNode(mode, children, not, secId, secPath, idx, gpath, ki h += '<div class="ie-cond" data-ie-gpath="' + escAttr(gpath) + '">'; h += '<div class="ie-cond-modebar">'; if (children.length >= 2) { - h += '<select class="ie-cond-modesel" onchange="ie_toggleCondMode(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\',\'' + escAttr(gpath) + '\')">'; + h += '<select class="ie-cond-modesel" onchange="ie_toggleCondMode(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\',\'' + escAttr(gpath) + '\')">'; h += '<option value="all_of"' + (mode === 'all_of' ? ' selected' : '') + '>All of</option>'; h += '<option value="any_of"' + (mode === 'any_of' ? ' selected' : '') + '>Any of</option>'; h += '</select>'; } if (children.length >= 2 || not) { - h += '<label class="ie-cond-notlbl"><input type="checkbox" class="ie-cond-not" onchange="ie_toggleCondNot(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\',\'' + escAttr(gpath) + '\')"' + (not ? ' checked' : '') + '> NOT</label>'; + h += '<label class="ie-cond-notlbl"><input type="checkbox" class="ie-cond-not" onchange="ie_toggleCondNot(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\',\'' + escAttr(gpath) + '\')"' + (not ? ' checked' : '') + '> NOT</label>'; } h += '</div>'; h += '<div class="ie-cond-children">'; @@ -481,9 +609,9 @@ function _ie_renderGroupNode(mode, children, not, secId, secPath, idx, gpath, ki h += '</div>'; h += '<div class="ie-cond-addbar" data-ie-gpath="' + escAttr(gpath) + '">'; IE_COND_TYPES.forEach(function(ct) { - h += '<button class="btn btn-sm ie-add-btn ie-cond-add-leaf" onclick="ie_addLeaf(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\',\'' + escAttr(gpath) + '\',\'' + ct.type + '\')">+ ' + esc(ct.label) + '</button>'; + h += '<button class="btn btn-sm ie-add-btn ie-cond-add-leaf" onclick="ie_addLeaf(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\',\'' + escAttr(gpath) + '\',\'' + ct.type + '\')">+ ' + esc(ct.label) + '</button>'; }); - h += '<button class="btn btn-sm ie-add-btn ie-cond-add-grp" onclick="ie_addGroup(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\',\'' + escAttr(gpath) + '\')">+ Group</button>'; + h += '<button class="btn btn-sm ie-add-btn ie-cond-add-grp" onclick="ie_addGroup(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\',\'' + escAttr(gpath) + '\')">+ Group</button>'; h += '</div>'; h += '</div>'; return h; @@ -516,8 +644,8 @@ function _ie_renderLeaf(leafType, leafVal, leafSub, not, secId, secPath, idx, gp h += '<span class="ie-cond-eq">=</span>'; h += '<input class="ie-cond-subval" placeholder="exists" value="' + escAttr(ssv) + '">'; } - h += '<label class="ie-cond-notlbl"><input type="checkbox" class="ie-cond-not" onchange="ie_toggleNotLeaf(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\',\'' + escAttr(gpath) + '\',' + ciToken + ')"' + (not ? ' checked' : '') + '> NOT</label>'; - h += '<button class="btn btn-sm btn-danger ie-cond-rm" onclick="ie_removeCond(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\',\'' + escAttr(gpath) + '\',' + ciToken + ')" title="Remove">X</button>'; + h += '<label class="ie-cond-notlbl"><input type="checkbox" class="ie-cond-not" onchange="ie_toggleNotLeaf(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\',\'' + escAttr(gpath) + '\',' + ciToken + ')"' + (not ? ' checked' : '') + '> NOT</label>'; + h += '<button class="btn btn-sm btn-danger ie-cond-rm" onclick="ie_removeCond(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\',\'' + escAttr(gpath) + '\',' + ciToken + ')" title="Remove">X</button>'; h += '</div>'; return h; } @@ -554,27 +682,27 @@ function ie_renderAct(stepObj, secId, idx, si, secPath) { }); } rows += '</div>'; - rows += '<button class="btn btn-sm ie-add-btn" style="margin-top:2px" onclick="ie_addKVRow(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',\'' + escAttr(at.key) + '\')">+ Add</button>'; + rows += '<button class="btn btn-sm ie-add-btn" style="margin-top:2px" onclick="ie_addKVRow(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',\'' + escAttr(at.key) + '\')">+ Add</button>'; rows += '</div>'; } else if (at.kind === 'checkbox') { rows += '<div class="ie-act-row"><span class="ie-act-label">' + esc(at.label) + '</span>'; rows += '<input type="checkbox" class="ie-act-input" data-ie-act-key="' + escAttr(at.key) + '" data-ie-act-kind="checkbox" checked hidden>'; - rows += '<button class="btn btn-sm btn-danger ie-act-rm" onclick="ie_removeActEffect(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',\'' + escAttr(at.key) + '\')">X</button>'; + rows += '<button class="btn btn-sm btn-danger ie-act-rm" onclick="ie_removeActEffect(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',\'' + escAttr(at.key) + '\')">X</button>'; rows += '</div>'; } else if (at.kind === 'search') { rows += '<div class="ie-act-row"><span class="ie-act-label">' + esc(at.label) + '</span>'; rows += '<div class="obj-search" style="position:relative;min-width:0;flex:1"><input class="ie-act-input search-input" data-ie-act-key="' + escAttr(at.key) + '" data-search-type="' + escAttr(at.entity) + '" placeholder="' + escAttr(at.hint || 'Search ' + at.entity + '...') + '" value="' + escAttr(String(val)) + '"><div class="search-results" style="display:none"></div></div>'; - rows += '<button class="btn btn-sm btn-danger ie-act-rm" onclick="ie_removeActEffect(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',\'' + escAttr(at.key) + '\')">X</button>'; + rows += '<button class="btn btn-sm btn-danger ie-act-rm" onclick="ie_removeActEffect(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',\'' + escAttr(at.key) + '\')">X</button>'; rows += '</div>'; } else if (at.kind === 'droptable') { rows += '<div class="ie-act-kv" data-ie-act-key="drop_table">'; - rows += '<div class="ie-act-kv-header"><span>Drop Table</span><button class="btn btn-sm btn-danger ie-act-rm" style="margin-left:auto" onclick="ie_removeActEffect(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',\'drop_table\')">X</button></div>'; - rows += ie_renderDropTableBlock(val, 'ie_addDropRow(\''+escAttr(secId)+'\',\''+escAttr(secPath)+'\','+idx+','+si+',\'item\')', 'ie_addDropRow(\''+escAttr(secId)+'\',\''+escAttr(secPath)+'\','+idx+','+si+',\'table\')', 'ie_removeDropRow(\''+escAttr(secId)+'\',\''+escAttr(secPath)+'\','+idx+','+si+',__RI__)'); + rows += '<div class="ie-act-kv-header"><span>Drop Table</span><button class="btn btn-sm btn-danger ie-act-rm" style="margin-left:auto" onclick="ie_removeActEffect(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',\'drop_table\')">X</button></div>'; + rows += ie_renderDropTableBlock(val, 'ie_addDropRow(this,\''+escAttr(secId)+'\',\''+escAttr(secPath)+'\','+idx+','+si+',\'item\')', 'ie_addDropRow(this,\''+escAttr(secId)+'\',\''+escAttr(secPath)+'\','+idx+','+si+',\'table\')', 'ie_removeDropRow(this,\''+escAttr(secId)+'\',\''+escAttr(secPath)+'\','+idx+','+si+',__RI__)'); rows += '</div>'; } else { rows += '<div class="ie-act-row"><span class="ie-act-label">' + esc(at.label) + '</span>'; rows += '<input class="ie-act-input" data-ie-act-key="' + escAttr(at.key) + '"' + (at.kind === 'number' ? ' type="number"' : '') + ' data-ie-act-kind="' + escAttr(at.kind) + '" value="' + escAttr(String(val)) + '" placeholder="' + escAttr(at.hint || '') + '" style="flex:1">'; - rows += '<button class="btn btn-sm btn-danger ie-act-rm" onclick="ie_removeActEffect(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',\'' + escAttr(at.key) + '\')">X</button>'; + rows += '<button class="btn btn-sm btn-danger ie-act-rm" onclick="ie_removeActEffect(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',\'' + escAttr(at.key) + '\')">X</button>'; rows += '</div>'; } }); @@ -599,15 +727,15 @@ function ie_renderMessages(step, secId, idx, si, secPath) { h += '<div class="ie-kv-list">'; msgs.forEach(function(m, mi) { var upBtn = mi > 0 - ? '<button class="btn btn-sm ie-msg-up" onclick="ie_moveMessage(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',' + mi + ',' + (mi - 1) + ')" title="Move up">▲</button>' + ? '<button class="btn btn-sm ie-msg-up" onclick="ie_moveMessage(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',' + mi + ',' + (mi - 1) + ')" title="Move up">▲</button>' : ''; var dnBtn = mi < msgs.length - 1 - ? '<button class="btn btn-sm ie-msg-dn" onclick="ie_moveMessage(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',' + mi + ',' + (mi + 1) + ')" title="Move down">▼</button>' + ? '<button class="btn btn-sm ie-msg-dn" onclick="ie_moveMessage(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',' + mi + ',' + (mi + 1) + ')" title="Move down">▼</button>' : ''; h += '<div class="ie-kv-row ie-msg-row"><input class="ie-act-msg" data-ie-msg-idx="' + mi + '" value="' + escAttr(String(m)) + '" placeholder="Shown to player" style="flex:1">' + upBtn + dnBtn + '<button class="btn btn-sm btn-danger" onclick="' + rmFn + '">X</button></div>'; }); h += '</div>'; - h += '<button class="btn btn-sm ie-add-btn" style="margin-top:2px" onclick="ie_addMessage(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ')">+ Add Message</button>'; + h += '<button class="btn btn-sm ie-add-btn" style="margin-top:2px" onclick="ie_addMessage(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ')">+ Add Message</button>'; h += '</div>'; return h; } @@ -624,12 +752,12 @@ function ie_renderStepList(steps, secId, idx, secPath) { h += '<span class="ie-step-label">Step ' + (si + 1) + '</span>'; h += '<button class="btn btn-sm ie-step-addmore" onclick="this.closest(\'.ie-step-row\').classList.toggle(\'show-addbar\')" title="Add action to this step"></button>'; if (!stepCond) { - h += '<button class="btn btn-sm ie-add-btn" onclick="ie_addStepCond(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ')">+ Add Step Condition</button>'; + h += '<button class="btn btn-sm ie-add-btn" onclick="ie_addStepCond(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ')">+ Add Step Condition</button>'; } h += '<span class="ie-step-spacer"></span>'; - if (si > 0) h += '<button class="btn btn-sm ie-step-up" onclick="ie_moveStep(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',' + (si - 1) + ')" title="Move up">▲</button>'; - if (si < steps.length - 1) h += '<button class="btn btn-sm ie-step-dn" onclick="ie_moveStep(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',' + (si + 1) + ')" title="Move down">▼</button>'; - h += '<button class="btn btn-sm btn-danger" onclick="ie_removeStep(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ')" title="Remove step">X</button>'; + if (si > 0) h += '<button class="btn btn-sm ie-step-up" onclick="ie_moveStep(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',' + (si - 1) + ')" title="Move up">▲</button>'; + if (si < steps.length - 1) h += '<button class="btn btn-sm ie-step-dn" onclick="ie_moveStep(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',' + (si + 1) + ')" title="Move down">▼</button>'; + h += '<button class="btn btn-sm btn-danger" onclick="ie_removeStep(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ')" title="Remove step">X</button>'; h += '</div>'; if (stepCond) { @@ -789,16 +917,12 @@ function ie_syncAllInteractions(data, secs) { // ── Add/Remove/Toggle: Condition ── -function ie_addLeaf(secId, secPath, idx, kind, groupPath, leafType) { - var ed = window._editorData; - if (!ed) return; - _ie_syncCond(ed, secId, secPath, idx, kind); - var cond = _ie_getCond(ed, secPath, idx, kind); - var group = ie_condAt(cond, groupPath); - +function ie_addLeaf(el, secId, secPath, idx, kind, groupPath, leafType) { + var c = _ie_condCtx(el, secId, secPath, idx, kind); + var cond = c.get(); var leaf = {}; leaf[leafType] = (leafType === 'min_credits' || leafType === 'room') ? 0 : ''; - + var group = ie_condAt(cond, groupPath); var newGroup; if (!group) { newGroup = leaf; @@ -811,29 +935,21 @@ function ie_addLeaf(secId, secPath, idx, kind, groupPath, leafType) { } else { newGroup = {all_of: [group, leaf]}; } - - var newCond = ie_condReplace(cond, groupPath, newGroup); - _ie_setCond(ed, secPath, idx, kind, newCond); - ced_syncDOMToData(ed); - window._ieRenderOnly(); + cond = ie_condReplace(cond, groupPath, newGroup); + c.set(cond); + c.change(); } -function ie_removeCond(secId, secPath, idx, kind, groupPath, childIdx) { - var ed = window._editorData; - if (!ed) return; - _ie_syncCond(ed, secId, secPath, idx, kind); - var cond = _ie_getCond(ed, secPath, idx, kind); - +function ie_removeCond(el, secId, secPath, idx, kind, groupPath, childIdx) { + var c = _ie_condCtx(el, secId, secPath, idx, kind); + var cond = c.get(); if (childIdx === IE_ROOT_LEAF) { - _ie_setCond(ed, secPath, idx, kind, null); - ced_syncDOMToData(ed); - window._ieRenderOnly(); + c.set(null); + c.change(); return; } - var group = ie_condAt(cond, groupPath); if (!group) return; - if (group.all_of || group.any_of) { var mode = group.all_of ? 'all_of' : 'any_of'; var children = (group.all_of || group.any_of).slice(); @@ -848,39 +964,30 @@ function ie_removeCond(secId, secPath, idx, kind, groupPath, childIdx) { if (group.not) newGroup.not = true; newGroup[mode] = children; } - var newCond = ie_condReplace(cond, groupPath, newGroup); - _ie_setCond(ed, secPath, idx, kind, newCond); + cond = ie_condReplace(cond, groupPath, newGroup); } else { - var nc = ie_condReplace(cond, groupPath, null); - _ie_setCond(ed, secPath, idx, kind, nc); + cond = ie_condReplace(cond, groupPath, null); } - ced_syncDOMToData(ed); - window._ieRenderOnly(); + c.set(cond); + c.change(); } -function ie_removeGroup(secId, secPath, idx, kind, groupPath) { - var ed = window._editorData; - if (!ed) return; - _ie_syncCond(ed, secId, secPath, idx, kind); - var cond = _ie_getCond(ed, secPath, idx, kind); - +function ie_removeGroup(el, secId, secPath, idx, kind, groupPath) { + var c = _ie_condCtx(el, secId, secPath, idx, kind); + var cond = c.get(); if (!groupPath) { - _ie_setCond(ed, secPath, idx, kind, null); - ced_syncDOMToData(ed); - window._ieRenderOnly(); + c.set(null); + c.change(); return; } - var parts = groupPath.split('-').map(Number); var childIdx = parts[parts.length - 1]; var parentPath = parts.slice(0, -1).join('-'); var parent = ie_condAt(cond, parentPath); if (!parent || !(parent.all_of || parent.any_of)) return; - var mode = parent.all_of ? 'all_of' : 'any_of'; var children = parent[mode].slice(); children.splice(childIdx, 1); - var newParent; if (children.length === 0) { newParent = null; @@ -891,11 +998,9 @@ function ie_removeGroup(secId, secPath, idx, kind, groupPath) { if (parent.not) newParent.not = true; newParent[mode] = children; } - - var newCond = ie_condReplace(cond, parentPath, newParent); - _ie_setCond(ed, secPath, idx, kind, newCond); - ced_syncDOMToData(ed); - window._ieRenderOnly(); + cond = ie_condReplace(cond, parentPath, newParent); + c.set(cond); + c.change(); } function ie_addRootConditionGroup(secPath, idx, kind) { @@ -919,19 +1024,21 @@ function ie_addRootConditionGroup(secPath, idx, kind) { window._ieRenderOnly(); } -function ie_addStepCond(secId, secPath, idx, si) { +function ie_addStepCond(el, secId, secPath, idx, si) { + if (el && el.nodeType && _ie_scopeFromEl(el)) { + var c = _ie_condCtx(el, secId, secPath, idx, 'step-' + si); + c.get(); + c.change(); + return; + } ie_addRootConditionGroup(secPath, idx, 'step-' + si); } -function ie_addGroup(secId, secPath, idx, kind, groupPath) { - var ed = window._editorData; - if (!ed) return; - _ie_syncCond(ed, secId, secPath, idx, kind); - var cond = _ie_getCond(ed, secPath, idx, kind); +function ie_addGroup(el, secId, secPath, idx, kind, groupPath) { + var c = _ie_condCtx(el, secId, secPath, idx, kind); + var cond = c.get(); var group = ie_condAt(cond, groupPath); - var newSub = {all_of: []}; - var newGroup; if (!group) { newGroup = newSub; @@ -944,78 +1051,59 @@ function ie_addGroup(secId, secPath, idx, kind, groupPath) { } else { newGroup = {all_of: [group, newSub]}; } - - var newCond = ie_condReplace(cond, groupPath, newGroup); - _ie_setCond(ed, secPath, idx, kind, newCond); - ced_syncDOMToData(ed); - window._ieRenderOnly(); + cond = ie_condReplace(cond, groupPath, newGroup); + c.set(cond); + c.change(); } -function ie_addOuterLeaf(secId, secPath, idx, kind, leafType) { - var ed = window._editorData; - if (!ed) return; - _ie_syncCond(ed, secId, secPath, idx, kind); - var cond = _ie_getCond(ed, secPath, idx, kind); - +function ie_addOuterLeaf(el, secId, secPath, idx, kind, leafType) { + var c = _ie_condCtx(el, secId, secPath, idx, kind); + var cond = c.get(); var leaf = {}; leaf[leafType] = (leafType === 'min_credits' || leafType === 'room') ? 0 : ''; - - var newCond; if (!cond) { - newCond = leaf; + cond = leaf; } else { - newCond = {all_of: [cond, leaf]}; + cond = {all_of: [cond, leaf]}; } - - _ie_setCond(ed, secPath, idx, kind, newCond); - ced_syncDOMToData(ed); - window._ieRenderOnly(); + c.set(cond); + c.change(); } -function ie_addOuterGroup(secId, secPath, idx, kind) { - var ed = window._editorData; - if (!ed) return; - _ie_syncCond(ed, secId, secPath, idx, kind); - var cond = _ie_getCond(ed, secPath, idx, kind); - +function ie_addOuterGroup(el, secId, secPath, idx, kind) { + var c = _ie_condCtx(el, secId, secPath, idx, kind); + var cond = c.get(); var newSub = {all_of: []}; - - var newCond; if (!cond) { - newCond = newSub; + cond = newSub; } else { - newCond = {all_of: [cond, newSub]}; + cond = {all_of: [cond, newSub]}; } - - _ie_setCond(ed, secPath, idx, kind, newCond); - ced_syncDOMToData(ed); - window._ieRenderOnly(); + c.set(cond); + c.change(); } -function ie_toggleCondMode(secId, secPath, idx, kind, groupPath) { - var ed = window._editorData; - if (!ed) return; - _ie_syncCond(ed, secId, secPath, idx, kind); - window._ieRenderOnly(); +function ie_toggleCondMode(el, secId, secPath, idx, kind, groupPath) { + var c = _ie_condCtx(el, secId, secPath, idx, kind); + c.get(); + c.change(); } -function ie_toggleCondNot(secId, secPath, idx, kind, groupPath) { - var ed = window._editorData; - if (!ed) return; - _ie_syncCond(ed, secId, secPath, idx, kind); - window._ieRenderOnly(); +function ie_toggleCondNot(el, secId, secPath, idx, kind, groupPath) { + var c = _ie_condCtx(el, secId, secPath, idx, kind); + c.get(); + c.change(); } -function ie_toggleNotLeaf(secId, secPath, idx, kind, groupPath, ci) { - var ed = window._editorData; - if (!ed) return; - _ie_syncCond(ed, secId, secPath, idx, kind); - window._ieRenderOnly(); +function ie_toggleNotLeaf(el, secId, secPath, idx, kind, groupPath, ci) { + var c = _ie_condCtx(el, secId, secPath, idx, kind); + c.get(); + c.change(); } // ── Add/Remove: Steps ── -function ie_removeStep(secId, secPath, idx, si) { +function ie_removeStep(el, secId, secPath, idx, si) { var ed = window._editorData; if (!ed) return; ced_syncDOMToData(ed); @@ -1025,7 +1113,7 @@ function ie_removeStep(secId, secPath, idx, si) { window._ieRenderCurrent(); } -function ie_moveStep(secId, secPath, idx, fromSi, toSi) { +function ie_moveStep(el, secId, secPath, idx, fromSi, toSi) { var ed = window._editorData; if (!ed) return; ced_syncDOMToData(ed); @@ -1049,62 +1137,54 @@ function ie_moveRow(secPath, fromIdx, toIdx) { window._ieRenderOnly(); } -function ie_moveMessage(secId, secPath, idx, si, fromMi, toMi) { - var ed = window._editorData; - if (!ed) return; - ced_syncDOMToData(ed); - ie_syncStep(ed, secId, secPath, idx, si); - var step = ed[secPath] && ed[secPath][idx] && ed[secPath][idx].steps && ed[secPath][idx].steps[si]; +function ie_moveMessage(el, secId, secPath, idx, si, fromMi, toMi) { + var c = _ie_actCtx(el, secId, secPath, idx, si); + var step = c.get(); if (!step || !Array.isArray(step.messages)) return; if (fromMi < 0 || fromMi >= step.messages.length || toMi < 0 || toMi >= step.messages.length) return; var m = step.messages.splice(fromMi, 1)[0]; step.messages.splice(toMi, 0, m); - window._ieRenderOnly(); + c.set(step); + c.change(); } // ── Add/Remove: Action effects ── -function ie_addActEffect(secId, secPath, idx, si, effectKey) { - var ed = window._editorData; - if (!ed) return; - ie_syncStep(ed, secId, secPath, idx, si); - if (!ed[secPath]) ed[secPath] = []; - if (!ed[secPath][idx]) ed[secPath][idx] = {}; - if (!ed[secPath][idx].steps) ed[secPath][idx].steps = []; - if (!ed[secPath][idx].steps[si]) ed[secPath][idx].steps[si] = {}; - var step = ed[secPath][idx].steps[si]; - +function ie_addActEffect(el, secId, secPath, idx, si, effectKey) { + var c = _ie_actCtx(el, secId, secPath, idx, si); + var step = c.get(); var at = IE_ACT_TYPES.find(function(a) { return a.key === effectKey; }); if (!at) return; - if (at.kind === 'kv') { - step[effectKey] = {}; - } else if (at.kind === 'checkbox') { - step[effectKey] = true; - } else if (at.kind === 'number') { - step[effectKey] = 0; - } else if (at.kind === 'droptable') { - step[effectKey] = []; - } else { - step[effectKey] = ''; - } - ced_syncDOMToData(ed); - window._ieRenderOnly(); + if (at.kind === 'kv') step[effectKey] = {}; + else if (at.kind === 'checkbox') step[effectKey] = true; + else if (at.kind === 'number') step[effectKey] = 0; + else if (at.kind === 'droptable') step[effectKey] = []; + else step[effectKey] = ''; + c.set(step); + c.change(); } -function ie_removeActEffect(secId, secPath, idx, si, effectKey) { - var ed = window._editorData; - if (!ed) return; - ie_syncStep(ed, secId, secPath, idx, si); - var step = ed[secPath] && ed[secPath][idx] && ed[secPath][idx].steps && ed[secPath][idx].steps[si]; - if (!step) return; +function ie_removeActEffect(el, secId, secPath, idx, si, effectKey) { + var c = _ie_actCtx(el, secId, secPath, idx, si); + var step = c.get(); delete step[effectKey]; - ced_syncDOMToData(ed); - window._ieRenderOnly(); + c.set(step); + c.change(); } // ── Add/Remove: KV row ── -function ie_addKVRow(secId, secPath, idx, si, effectKey) { +function ie_addKVRow(el, secId, secPath, idx, si, effectKey) { + if (_ie_scopeFromEl(el)) { + var c = _ie_actCtx(el, secId, secPath, idx, si); + var step = c.get(); + if (!step[effectKey] || typeof step[effectKey] !== 'object') step[effectKey] = {}; + step[effectKey][''] = ''; + c.set(step); + c.change(); + return; + } + var ed = window._editorData; if (!ed) return; ie_syncStep(ed, secId, secPath, idx, si); @@ -1122,67 +1202,67 @@ function ie_addKVRow(secId, secPath, idx, si, effectKey) { } function ie_removeKVRow(rowEl, secId, secPath, idx, si, effectKey) { - var ed = window._editorData; - if (!ed) return; if (rowEl && rowEl.closest) { var kvRow = rowEl.closest('.ie-kv-row'); if (kvRow) rowEl = kvRow; } - ie_syncStep(ed, secId, secPath, idx, si); - var step = ed[secPath] && ed[secPath][idx] && ed[secPath][idx].steps && ed[secPath][idx].steps[si]; + var c = _ie_actCtx(rowEl, secId, secPath, idx, si); + var step = c.get(); if (!step || !step.hasOwnProperty(effectKey)) return; var keyEl = rowEl.querySelector('.ie-kv-key'); var k = keyEl ? keyEl.value.trim() : ''; if (k && step[effectKey].hasOwnProperty(k)) delete step[effectKey][k]; - if (Object.keys(step[effectKey]).length === 0) { - delete step[effectKey]; - } - ced_syncDOMToData(ed); - window._ieRenderOnly(); + if (Object.keys(step[effectKey]).length === 0) delete step[effectKey]; + c.set(step); + c.change(); } // ── Add/Remove: Messages ── -function ie_addMessages(secId, secPath, idx, si) { - var ed = window._editorData; - if (!ed) return; - ie_syncStep(ed, secId, secPath, idx, si); - if (!ed[secPath] || !ed[secPath][idx] || !ed[secPath][idx].steps || !ed[secPath][idx].steps[si]) return; - var step = ed[secPath][idx].steps[si]; +function ie_addMessages(el, secId, secPath, idx, si) { + var c = _ie_actCtx(el, secId, secPath, idx, si); + var step = c.get(); if (!step.messages || !Array.isArray(step.messages)) step.messages = []; step.messages.push(''); - window._ieRenderCurrent(); + c.set(step); + c.change(); } // ── Drop Table row add/remove ── -function ie_addDropRow(secId, secPath, idx, si, kind) { - var ed = window._editorData; - if (!ed) return; - ie_syncStep(ed, secId, secPath, idx, si); - if (!ed[secPath] || !ed[secPath][idx] || !ed[secPath][idx].steps || !ed[secPath][idx].steps[si]) return; - var step = ed[secPath][idx].steps[si]; +function ie_addDropRow(el, secId, secPath, idx, si, kind) { + var c = _ie_actCtx(el, secId, secPath, idx, si); + var step = c.get(); if (!Array.isArray(step.drop_table)) step.drop_table = []; if (kind === 'table') { step.drop_table.push({table: '', weight: 10, quantity: 1, _kind: 'table'}); } else { step.drop_table.push({item_id: '', weight: 10, quantity: 1, _kind: 'item'}); } - window._ieRenderOnly(); + c.set(step); + c.change(); } -function ie_removeDropRow(secId, secPath, idx, si, ri) { - var ed = window._editorData; - if (!ed) return; - ie_syncStep(ed, secId, secPath, idx, si); - if (!ed[secPath] || !ed[secPath][idx] || !ed[secPath][idx].steps || !ed[secPath][idx].steps[si]) return; - var step = ed[secPath][idx].steps[si]; +function ie_removeDropRow(el, secId, secPath, idx, si, ri) { + var c = _ie_actCtx(el, secId, secPath, idx, si); + var step = c.get(); if (!Array.isArray(step.drop_table)) return; step.drop_table.splice(ri, 1); - window._ieRenderOnly(); + c.set(step); + c.change(); } -function ie_addMessage(secId, secPath, idx, si) { +function ie_addMessage(el, secId, secPath, idx, si) { + if (_ie_scopeFromEl(el)) { + var c = _ie_actCtx(el, secId, secPath, idx, si); + var step = c.get(); + if (!step.messages || !Array.isArray(step.messages)) step.messages = []; + step.messages.push(''); + c.set(step); + c.change(); + return; + } + var ed = window._editorData; if (!ed) return; ie_syncStep(ed, secId, secPath, idx, si); @@ -1201,32 +1281,23 @@ function ie_addMessage(secId, secPath, idx, si) { } function ie_removeMessage(rowEl, secId, secPath, idx, si) { - var ed = window._editorData; - if (!ed) return; if (rowEl && rowEl.closest) { var kvRow = rowEl.closest('.ie-kv-row'); if (kvRow) rowEl = kvRow; } - ie_syncStep(ed, secId, secPath, idx, si); - var step = ed[secPath] && ed[secPath][idx] && ed[secPath][idx].steps && ed[secPath][idx].steps[si]; + var c = _ie_actCtx(rowEl, secId, secPath, idx, si); + var step = c.get(); if (!step || !Array.isArray(step.messages)) return; - - var card = document.querySelector('.obj-card[data-card="' + secId + '"]'); - var stepRow = card.querySelector('.obj-sub-row.ie-inter-row[data-idx="' + idx + '"] .ie-step-row[data-step="' + si + '"]'); - if (stepRow) { - var kvRows = stepRow.querySelectorAll('.ie-act-kv[data-ie-act-key="messages"] .ie-kv-row'); - for (var ri = 0; ri < kvRows.length; ri++) { - if (kvRows[ri] === rowEl || kvRows[ri].contains(rowEl)) { - step.messages.splice(ri, 1); - break; - } + var kvRows = document.querySelectorAll('.ie-act-kv[data-ie-act-key="messages"] .ie-kv-row'); + for (var ri = 0; ri < kvRows.length; ri++) { + if (kvRows[ri] === rowEl || kvRows[ri].contains(rowEl)) { + step.messages.splice(ri, 1); + break; } } - if (step.messages.length === 0) { - delete step.messages; - } - ced_syncDOMToData(ed); - window._ieRenderOnly(); + if (step.messages.length === 0) delete step.messages; + c.set(step); + c.change(); } // ── Shared trigger-style array-section renderer ── diff --git a/internal/admin/static/talktree.js b/internal/admin/static/talktree.js index 4758918..ef6efd0 100644 --- a/internal/admin/static/talktree.js +++ b/internal/admin/static/talktree.js @@ -1,6 +1,7 @@ -// talktree.js — Full-featured conversation tree editor. -// Inline editing for all TalkNode and TalkOption fields. -// No prompt() dialogs. Clause-based condition editor, action forms. +// talktree.js — Conversation tree editor with unified condition & action +// editors powered by intereditor.js. Tree navigation, messages, and options +// (text / goto) stay here; condition and action editing delegates to +// intereditor via talk-scoped contexts that target talk.nodes[key]... paths. (function() { 'use strict'; @@ -52,103 +53,143 @@ 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 } - ]; + // ---- Talk scopes for intereditor.js ---- + // Each scope targets a specific condition or action object inside the talk + // tree and handles re-render + talkSync on every mutation. + + // ---- Talk scopes for intereditor.js ---- + // Each scope targets a specific condition or action object inside the talk + // tree and handles re-render + talkSync on every mutation. + + var _talkScopeSeq = 0; + + function talkCondScope(nodeKey, optIdx) { + var token = 'tk' + (++_talkScopeSeq); + return ie_registerScope({ + getCond: function() { + var t = talkTarget(nodeKey, optIdx); + return t && t.condition || null; + }, + setCond: function(c) { + var t = talkTarget(nodeKey, optIdx); + if (!t) return; + if (c) t.condition = c; else delete t.condition; + }, + onChange: function() { + talkSync(); + renderTalkTree(window._talkData); + } + }, token); + } + + function talkActionScope(nodeKey, optIdx) { + var token = 'tk' + (++_talkScopeSeq); + return ie_registerScope({ + getStep: function() { + var t = talkTarget(nodeKey, optIdx); + return t && t.action || {}; + }, + setStep: function(s) { + var t = talkTarget(nodeKey, optIdx); + if (!t) return; + if (s && Object.keys(s).length > 0) t.action = s; else delete t.action; + }, + onChange: function() { + talkSync(); + renderTalkTree(window._talkData); + } + }, token); + } - function clauseTypeLabel(key) { - for (var i = 0; i < CLAUSE_TYPES.length; i++) { - if (CLAUSE_TYPES[i].key === key) return CLAUSE_TYPES[i].label; + function talkTarget(nodeKey, optIdx) { + var node = talkNodes() && talkNodes()[nodeKey]; + if (!node) return null; + if (optIdx !== null) { + if (!node.options || !node.options[optIdx]) return null; + return node.options[optIdx]; } - return key; + return node; } - 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; + // ---- Render a single action step for a talk node or option ---- + + function talkRenderActionStep(nodeKey, optIdx) { + var t = talkTarget(nodeKey, optIdx); + var step = (t && t.action) || {}; + var scopeToken = talkActionScope(nodeKey, optIdx); + var h = '<div class="ie-act-root" data-ie-scope="' + escAttr(scopeToken) + '">'; + h += ie_renderMessages(step, '', 0, 0, ''); + h += ie_renderAct(step, '', 0, 0, ''); + h += '<div class="ie-act-addbar">' + ie_renderStepAddbar(step, '', 0, 0, '') + '</div>'; + h += '</div>'; + return h; } - // 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 }; + // ---- talk cond/act add/remove helpers (called from onclick data-action) ---- + + function talkAddNodeCond(nodeKey) { + var node = talkNodes() && talkNodes()[nodeKey]; + if (!node || (node.condition && typeof node.condition === 'object')) return; + node.condition = {all_of: []}; + renderTalkTree(window._talkData); + talkSync(); } - // 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; + function talkRemoveNodeCond(nodeKey) { + var node = talkNodes() && talkNodes()[nodeKey]; + if (!node) return; + delete node.condition; + renderTalkTree(window._talkData); + talkSync(); } - // 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; + function talkAddNodeAct(nodeKey) { + var node = talkNodes() && talkNodes()[nodeKey]; + if (!node || (node.action && typeof node.action === 'object')) return; + node.action = {}; + renderTalkTree(window._talkData); + talkSync(); } - // ---- Action field definitions ---- + function talkRemoveNodeAct(nodeKey) { + var node = talkNodes() && talkNodes()[nodeKey]; + if (!node) return; + delete node.action; + renderTalkTree(window._talkData); + talkSync(); + } + + function talkAddOptionCond(nodeKey, optIdx) { + var t = talkTarget(nodeKey, optIdx); + if (!t || (t.condition && typeof t.condition === 'object')) return; + t.condition = {all_of: []}; + renderTalkTree(window._talkData); + talkSync(); + } + + function talkRemoveOptionCond(nodeKey, optIdx) { + var t = talkTarget(nodeKey, optIdx); + if (!t) return; + delete t.condition; + renderTalkTree(window._talkData); + talkSync(); + } - 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' }, - ]; + function talkAddOptionAct(nodeKey, optIdx) { + var t = talkTarget(nodeKey, optIdx); + if (!t || (t.action && typeof t.action === 'object')) return; + t.action = {}; + renderTalkTree(window._talkData); + talkSync(); + } - var ACTION_BOOLS = []; + function talkRemoveOptionAct(nodeKey, optIdx) { + var t = talkTarget(nodeKey, optIdx); + if (!t) return; + delete t.action; + renderTalkTree(window._talkData); + talkSync(); + } // ---- Event delegation ---- @@ -184,9 +225,6 @@ 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': @@ -207,32 +245,29 @@ 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); + case 'add-node-cond': + talkAddNodeCond(nodeKey); break; - case 'add-action': - addAction(nodeKey, optIdx !== null ? parseInt(optIdx, 10) : null); + case 'remove-node-cond': + talkRemoveNodeCond(nodeKey); break; - case 'remove-action': - removeAction(nodeKey, optIdx !== null ? parseInt(optIdx, 10) : null); + case 'add-node-act': + talkAddNodeAct(nodeKey); break; - case 'add-clause': - addClause(nodeKey, optIdx !== null ? parseInt(optIdx, 10) : null, el.getAttribute('data-ct')); + case 'remove-node-act': + talkRemoveNodeAct(nodeKey); break; - case 'remove-clause': - removeClause(nodeKey, optIdx !== null ? parseInt(optIdx, 10) : null, parseInt(subIdx, 10)); + case 'add-opt-cond': + talkAddOptionCond(nodeKey, parseInt(optIdx, 10)); break; - case 'set-cond-group': - setCondGroup(nodeKey, optIdx !== null ? parseInt(optIdx, 10) : null, condGroupKey); + case 'remove-opt-cond': + talkRemoveOptionCond(nodeKey, parseInt(optIdx, 10)); break; - case 'add-kv': - addKVPair(nodeKey, optIdx !== null ? parseInt(optIdx, 10) : null, el.getAttribute('data-kvk')); + case 'add-opt-act': + talkAddOptionAct(nodeKey, parseInt(optIdx, 10)); break; - case 'remove-kv': - removeKVPair(nodeKey, optIdx !== null ? parseInt(optIdx, 10) : null, el.getAttribute('data-kvk'), subKey); + case 'remove-opt-act': + talkRemoveOptionAct(nodeKey, parseInt(optIdx, 10)); break; } } @@ -242,6 +277,13 @@ window.renderTalkTree = function(data) { var container = document.getElementById('talktree'); if (!container) return; + // Purge old talk scopes before re-rendering (new ones will be registered). + if (window._ieScopes) { + Object.keys(window._ieScopes).forEach(function(k) { + if (k.indexOf('tk') === 0) delete window._ieScopes[k]; + }); + } + _talkScopeSeq = 0; window._talkData = data; window._talkOpenNodes = window._talkOpenNodes || new Set(); window._talkOpenNodes.add('start'); @@ -351,7 +393,6 @@ var html = '<div class="talk-node' + (isOpen ? ' open' : '') + '" id="node-' + escAttr(rootKey) + '">'; - // Header — fix: check for data-action before toggling html += '<div class="talk-node-header" onclick="if(!event.target.closest(\'[data-action]\'))talkToggleNode(\'' + escAttr(rootKey) + '\',this.parentElement)">'; html += '<span class="talk-node-arrow"></span>'; html += '<span class="talk-node-key">' + esc(rootKey) + '</span>'; @@ -360,12 +401,10 @@ html += '<button class="talk-node-btn talk-node-del" data-action="delete-node" data-nk="' + escAttr(rootKey) + '">Delete</button>'; html += '</div>'; - // Body html += '<div class="talk-node-body">'; html += renderNodeBody(rootKey, node, nodes); - html += '</div>'; // .talk-node-body + html += '</div>'; - // Children var childKeys = []; if (node.goto && nodes[node.goto]) childKeys.push(node.goto); if (node.options) { @@ -379,7 +418,7 @@ html += '</div>'; } - html += '</div>'; // .talk-node + html += '</div>'; return html; } @@ -388,59 +427,60 @@ return ''; } - // ---- Node body: condition-gated content ---- + // ---- Node body: condition, messages, options, action via intereditor ---- - // 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 hasCond = !!(node.condition && typeof node.condition === 'object'); + var hasAct = !!(node.action && typeof node.action === 'object' && !Array.isArray(node.action)); var hasOpts = node.options && node.options.length > 0; var html = ''; + // Node condition (via intereditor) if (hasCond) { html += '<div class="talk-cond-card">'; html += '<div class="talk-cond-card-header">' + - 'Condition' + - '<button class="talk-section-remove" data-action="remove-condition" data-nk="' + escAttr(nodeKey) + '">Remove</button>' + + 'Condition (for this node)' + + '<button class="talk-section-remove" data-action="remove-node-cond" data-nk="' + escAttr(nodeKey) + '">Remove</button>' + '</div>'; html += '<div class="talk-cond-card-body">'; - html += renderConditionClauses(ec, nodeKey, null); - html += '<div class="talk-cond-card-sep"></div>'; - html += renderMessagesSection(nodeKey, node); - html += renderOptionsSection(nodeKey, node); - html += '<div class="talk-cond-card-sep"></div>'; + html += ie_renderCond(node.condition, '', 0, '', '', {scopeToken: talkCondScope(nodeKey, null), alwaysOuterAddbar: true}); + html += '</div>'; + html += '</div>'; + } else { + html += '<button class="talk-add-cond-btn" data-action="add-node-cond" data-nk="' + escAttr(nodeKey) + '">+ Add Condition</button>'; + } - var nodeAction = node.action; - var actionPrefix = 'nodes.' + escAttr(nodeKey) + '.action'; - if (nodeAction && typeof nodeAction === 'object' && !Array.isArray(nodeAction)) { - html += '<div class="talk-cond-card" style="margin-top:4px">'; - html += '<div class="talk-cond-card-header">' + - '<span class="talk-prop-label">Action</span>' + - '<button class="talk-section-remove" data-action="remove-action" data-nk="' + escAttr(nodeKey) + '">Remove</button></div>'; - html += '<div class="talk-cond-card-body">'; - html += renderActionForm(nodeAction, actionPrefix, nodeKey, null); - html += '</div>'; - html += '</div>'; - } else { - html += '<button class="talk-add-act-btn" style="margin-top:4px" data-action="add-action" data-nk="' + escAttr(nodeKey) + '">+ Add Action</button>'; - } + // Node messages (unchanged) + html += renderMessagesSection(nodeKey, node); - html += '<div class="talk-cond-card-sep"></div>'; - html += renderGotoFallback(nodeKey, node, 'If condition fails, go to'); - html += '</div>'; // .talk-cond-card-body - html += '</div>'; // .talk-cond-card + // Node options + html += renderOptionsSection(nodeKey, node); + + html += '<div class="talk-cond-card-sep"></div>'; + + // Node action (via intereditor) + if (hasAct) { + html += '<div class="talk-cond-card" style="margin-top:4px">'; + html += '<div class="talk-cond-card-header">' + + 'Action (at the start of this node)' + + '<button class="talk-section-remove" data-action="remove-node-act" data-nk="' + escAttr(nodeKey) + '">Remove</button></div>'; + html += '<div class="talk-cond-card-body">'; + html += talkRenderActionStep(nodeKey, null); + html += '</div>'; + html += '</div>'; } else { - html += '<button class="talk-add-cond-btn" data-action="add-condition" data-nk="' + escAttr(nodeKey) + '">+ Add Condition</button>'; - html += renderMessagesSection(nodeKey, node); - html += renderOptionsSection(nodeKey, node); + html += '<button class="talk-add-act-btn" style="margin-top:4px" data-action="add-node-act" data-nk="' + escAttr(nodeKey) + '">+ Add Action</button>'; + } + + // Goto fallback + if (hasCond) { html += '<div class="talk-cond-card-sep"></div>'; - html += renderActionSection(nodeKey, null, node.action); - if (!hasOpts) { - html += renderGotoFallback(nodeKey, node, 'Auto-advance to'); - } + html += renderGotoFallback(nodeKey, node, 'If condition fails, go to'); + } else if (!hasOpts) { + html += renderGotoFallback(nodeKey, node, 'Auto-advance to'); } return html; @@ -478,259 +518,6 @@ 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 += '<div class="talk-cond-mode-row">' + - '<span class="talk-prop-label">Combine with</span>' + - '<select class="talk-select" data-action="set-cond-group" data-nk="' + escAttr(nodeKey) + '"' + (optIdx !== null ? ' data-oi="' + optIdx + '"' : '') + ' data-cgk="' + escAttr(groupKey) + '" onchange="event.stopPropagation(); talkCondGroupChange(this)">' + - '<option value="all_of"' + (groupKey === 'all_of' ? ' selected' : '') + '>All of (AND)</option>' + - '<option value="any_of"' + (groupKey === 'any_of' ? ' selected' : '') + '>Any of (OR)</option>' + - '</select>' + - '</div>'; - } - - // 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 += '<div class="talk-clause-add-bar">'; - CLAUSE_TYPES.forEach(function(ct) { - html += '<button class="talk-clause-add-btn" data-action="add-clause" data-nk="' + escAttr(nodeKey) + '"' + - (optIdx !== null ? ' data-oi="' + optIdx + '"' : '') + ' data-ct="' + escAttr(ct.key) + '">+ ' + esc(ct.label) + '</button>'; - }); - html += '</div>'; - - return html; - } - - function renderClauseRow(clause, pathPrefix, typeKey, nodeKey, optIdx, clauseIdx) { - var hasValue = clauseTypeHasValue(typeKey); - var html = '<div class="talk-clause-row">'; - html += '<span class="talk-clause-type">' + esc(clauseTypeLabel(typeKey)) + '</span>'; - - if (typeKey === 'flag' || typeKey === 'player_flag') { - var notLabel = (clause.value !== undefined && clause.value !== '') ? 'Not' : 'Not Exists'; - html += '<span class="talk-clause-label">key:</span>'; - html += '<input class="talk-input talk-input-sm" data-tp="' + escAttr(pathPrefix + '.' + typeKey) + '" value="' + escAttr(clause[typeKey] || '') + '" placeholder="Flag name...">'; - html += '<label class="talk-check-label"><input type="checkbox" data-tp="' + escAttr(pathPrefix + '.not') + '"' + (clause.not ? ' checked' : '') + '> ' + notLabel + '</label>'; - html += '<span class="talk-clause-label">val:</span>'; - html += '<input class="talk-input talk-input-sm" data-tp="' + escAttr(pathPrefix + '.value') + '" value="' + escAttr(clause.value || '') + '" placeholder="Value match..." style="max-width:80px">'; - } else if (typeKey === 'has_item') { - html += '<span class="talk-clause-label">item:</span>'; - html += '<input class="talk-input talk-input-sm" data-tp="' + escAttr(pathPrefix + '.has_item') + '" value="' + escAttr(clause.has_item || '') + '" placeholder="Item ID...">'; - html += '<label class="talk-check-label"><input type="checkbox" data-tp="' + escAttr(pathPrefix + '.not') + '"' + (clause.not ? ' checked' : '') + '> Not Exists</label>'; - } else if (typeKey === 'min_credits') { - html += '<span class="talk-clause-label">cr:</span>'; - html += '<input class="talk-input-num" data-tp="' + escAttr(pathPrefix + '.min_credits') + '" type="number" value="' + (clause.min_credits || 0) + '" placeholder="0">'; - html += '<label class="talk-check-label"><input type="checkbox" data-tp="' + escAttr(pathPrefix + '.not') + '"' + (clause.not ? ' checked' : '') + '> Not</label>'; - } - - html += '<button class="talk-clause-del" data-action="remove-clause" data-nk="' + escAttr(nodeKey) + '"' + (optIdx !== null ? ' data-oi="' + optIdx + '"' : '') + ' data-si="' + clauseIdx + '" title="Remove clause">x</button>'; - html += '</div>'; - 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 '<div class="talk-section">' + - '<button class="talk-add-act-btn" data-action="add-action" data-nk="' + escAttr(nodeKey) + '"' + (optIdx !== null ? ' data-oi="' + optIdx + '"' : '') + '>+ Add Action</button>' + - '</div>'; - } - - return '<div class="talk-section">' + - '<div class="talk-cond-card-header">' + - '<span class="talk-prop-label">Action</span>' + - '<button class="talk-section-remove" data-action="remove-action" data-nk="' + escAttr(nodeKey) + '"' + (optIdx !== null ? ' data-oi="' + optIdx + '"' : '') + '>Remove</button>' + - '</div>' + - renderActionForm(action, prefix, nodeKey, optIdx) + - '</div>'; - } - - function renderActionForm(action, prefix, nodeKey, optIdx) { - var html = '<div class="talk-action-block">'; - - html += '<div class="talk-action-grid">'; - 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 += '<div class="talk-field-row">' + - '<span class="talk-prop-label">' + esc(f.label) + '</span>' + - '<div style="position:relative">' + - '<input class="talk-input talk-input-sm search-input" data-tp="' + escAttr(fpath) + '" data-search-type="items" value="' + escAttr(String(val)) + '" placeholder="Search items...">' + - '<div class="search-results" style="display:none"></div>' + - '</div>' + - '</div>'; - } else { - var narrowStyle = (f.key === 'teleport' || f.key === 'heal') ? ' style="max-width:70px"' : ''; - html += '<div class="talk-field-row">' + - '<span class="talk-prop-label">' + esc(f.label) + '</span>' + - '<input class="talk-input talk-input-sm"' + narrowStyle + ' data-tp="' + escAttr(fpath) + '" type="' + f.type + '" value="' + escAttr(String(val)) + '">' + - '</div>'; - } - }); - html += '</div>'; - - html += '<div style="margin-top:4px">'; - ACTION_BOOLS.forEach(function(b) { - html += '<label class="talk-check-label">' + - '<input type="checkbox" data-tp="' + escAttr(prefix + '.' + b.key) + '"' + (action && action[b.key] ? ' checked' : '') + '> ' + esc(b.label) + - '</label>'; - }); - html += '</div>'; - - html += renderKVEditor(action, prefix, 'set_flags', 'Set Flags', nodeKey, optIdx); - html += renderKVEditor(action, prefix, 'set_player_flags', 'Set Player Flags', nodeKey, optIdx); - - html += '</div>'; // .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 = '<div class="talk-kv-section">'; - html += '<div class="talk-kv-header">'; - html += '<span class="talk-prop-label">' + esc(label) + '</span>'; - html += '</div>'; - keys.forEach(function(k) { - html += '<div class="talk-kv-row">' + - '<input class="talk-input" data-nk="' + nkAttr + '"' + oiAttr + ' data-kvk="' + escAttr(key) + '" data-oldkey="' + escAttr(k) + '" onchange="talkRenameKV(this)" value="' + escAttr(k) + '" placeholder="key" style="max-width:150px">' + - '<input class="talk-input" data-tp="' + escAttr(prefix + '.' + key + '.' + k) + '" value="' + escAttr(String(kv[k])) + '" placeholder="value">' + - '<button class="talk-kv-del" data-action="remove-kv" data-nk="' + nkAttr + '"' + oiAttr + ' data-kvk="' + escAttr(key) + '" data-sk="' + escAttr(k) + '">x</button>' + - '</div>'; - }); - html += '<div class="talk-kv-row">' + - '<input class="talk-input" id="' + escAttr(addKeyID) + '" placeholder="New key name" style="max-width:150px" data-nk="' + nkAttr + '"' + oiAttr + ' data-kvk="' + escAttr(key) + '">' + - '<input class="talk-input" id="' + escAttr(addKeyID) + '_val" placeholder="value" style="flex:1">' + - '<span style="font-size:9px;color:#888;white-space:nowrap;min-width:60px"></span>' + - '<button class="talk-kv-add" data-action="add-kv" data-nk="' + nkAttr + '"' + oiAttr + ' data-kvk="' + escAttr(key) + '">Add</button>' + - '</div>'; - html += '</div>'; - return html; - } - // ---- Options section ---- function renderOptionsSection(nodeKey, node) { @@ -747,43 +534,42 @@ html += '<div class="talk-opt-edit-actions">'; if (opt.condition && typeof opt.condition === 'object' && !Array.isArray(opt.condition)) { - html += '<span class="talk-opt-tag talk-opt-tag-cond" title="Has condition" onclick="toggleOptionDetail(event,\'cond\',' + idx + ',\'' + escAttr(nodeKey) + '\')">cond</span>'; + html += '<span class="talk-opt-tag talk-opt-tag-cond">cond</span>'; } else { - html += '<button class="talk-add-cond-btn" style="margin:0;margin-bottom:0" data-action="add-condition" data-nk="' + escAttr(nodeKey) + '" data-oi="' + idx + '" title="Add condition">cond</button>'; + html += '<button class="talk-add-cond-btn" style="margin:0;margin-bottom:0" data-action="add-opt-cond" data-nk="' + escAttr(nodeKey) + '" data-oi="' + idx + '" title="Add condition">cond</button>'; } if (opt.action) { - html += '<span class="talk-opt-tag talk-opt-tag-act" title="Has action" onclick="toggleOptionDetail(event,\'act\',' + idx + ',\'' + escAttr(nodeKey) + '\')">act</span>'; + html += '<span class="talk-opt-tag talk-opt-tag-act">act</span>'; } else { - html += '<button class="talk-add-act-btn" style="margin:0;margin-bottom:0" data-action="add-action" data-nk="' + escAttr(nodeKey) + '" data-oi="' + idx + '" title="Add action">act</button>'; + html += '<button class="talk-add-act-btn" style="margin:0;margin-bottom:0" data-action="add-opt-act" data-nk="' + escAttr(nodeKey) + '" data-oi="' + idx + '" title="Add action">act</button>'; } html += '</div>'; html += '<button class="talk-opt-del" data-action="delete-option" data-nk="' + escAttr(nodeKey) + '" data-oi="' + idx + '" title="Delete option">x</button>'; html += '</div>'; + // Option condition detail 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 += '<div class="talk-opt-detail" id="optDetail_' + condDetailKey + '" style="display:' + condOpen + ';margin-left:18px">'; + html += '<div class="talk-opt-detail" style="display:block;margin-left:18px">'; html += '<div class="talk-cond-card" style="margin-top:2px">'; - html += '<div class="talk-cond-card-header">Condition' + - '<button class="talk-section-remove" data-action="remove-condition" data-nk="' + escAttr(nodeKey) + '" data-oi="' + idx + '">Remove</button></div>'; + html += '<div class="talk-cond-card-header">Condition (for this option to show up)' + + '<button class="talk-section-remove" data-action="remove-opt-cond" data-nk="' + escAttr(nodeKey) + '" data-oi="' + idx + '">Remove</button></div>'; html += '<div class="talk-cond-card-body">'; - html += renderConditionClauses(extractClauses(opt.condition), nodeKey, idx); + html += ie_renderCond(opt.condition, '', 0, '', '', {scopeToken: talkCondScope(nodeKey, idx), alwaysOuterAddbar: true}); html += '</div>'; html += '</div>'; html += '</div>'; } + + // Option action detail if (opt.action) { - var actDetailKey = 'act_' + idx + '_' + escAttr(nodeKey); - var actOpen = window._talkAutoOpenDetails && window._talkAutoOpenDetails.has(actDetailKey) ? 'block' : 'none'; - html += '<div class="talk-opt-detail" id="optDetail_' + actDetailKey + '" style="display:' + actOpen + ';margin-left:18px">'; + html += '<div class="talk-opt-detail" style="display:block;margin-left:18px">'; html += '<div class="talk-cond-card" style="margin-top:2px">'; html += '<div class="talk-cond-card-header">' + - '<span class="talk-prop-label">Action</span>' + - '<button class="talk-section-remove" data-action="remove-action" data-nk="' + escAttr(nodeKey) + '" data-oi="' + idx + '">Remove</button></div>'; + 'Action (when this option is chosen)' + + '<button class="talk-section-remove" data-action="remove-opt-act" data-nk="' + escAttr(nodeKey) + '" data-oi="' + idx + '">Remove</button></div>'; html += '<div class="talk-cond-card-body">'; - html += renderActionForm(opt.action, 'nodes.' + escAttr(nodeKey) + '.options.' + idx + '.action', nodeKey, idx); + html += talkRenderActionStep(nodeKey, idx); html += '</div>'; html += '</div>'; html += '</div>'; @@ -801,16 +587,10 @@ '<button class="talk-add-btn" data-action="add-option" data-nk="' + escAttr(nodeKey) + '">Add</button>' + '</div>'; - html += '</div>'; // .talk-options + html += '</div>'; 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) { @@ -867,98 +647,6 @@ 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) { @@ -1004,15 +692,6 @@ 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; - } }); })(); |
