From e6b4cb9f5816c6ee239233bc85f74ee446a161c2 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Wed, 1 Jul 2026 16:37:45 -0400 Subject: feat: shop system overhauled, gui conversation tree editor overhauled --- internal/admin/api_mobs.go | 1 + internal/admin/api_objects.go | 1 + internal/admin/static/admin.css | 30 ++-- internal/admin/static/mobeditor.js | 32 +++- internal/admin/static/objecteditor.js | 7 +- internal/admin/static/talktree.js | 330 +++++++++++++++++----------------- 6 files changed, 221 insertions(+), 180 deletions(-) (limited to 'internal/admin') diff --git a/internal/admin/api_mobs.go b/internal/admin/api_mobs.go index da088d3..a575076 100644 --- a/internal/admin/api_mobs.go +++ b/internal/admin/api_mobs.go @@ -95,6 +95,7 @@ func (s *AdminServer) updateMob(w http.ResponseWriter, r *http.Request, id strin http.Error(w, `{"error":"failed to write mob"}`, http.StatusInternalServerError) return } + m["_raw"] = string(newContent) s.mobStore.ReloadDefs(s.dataDir) s.undoStack.Push(ChangeDesc{ Description: fmt.Sprintf("Update mob %s", id), diff --git a/internal/admin/api_objects.go b/internal/admin/api_objects.go index 01a0803..7bcda1c 100644 --- a/internal/admin/api_objects.go +++ b/internal/admin/api_objects.go @@ -95,6 +95,7 @@ func (s *AdminServer) updateObject(w http.ResponseWriter, r *http.Request, id st http.Error(w, `{"error":"failed to write object"}`, http.StatusInternalServerError) return } + m["_raw"] = string(newContent) s.undoStack.Push(ChangeDesc{ Description: fmt.Sprintf("Update object %s", id), diff --git a/internal/admin/static/admin.css b/internal/admin/static/admin.css index 43354f9..2bebf85 100644 --- a/internal/admin/static/admin.css +++ b/internal/admin/static/admin.css @@ -1,5 +1,7 @@ :root{--bg:#1a1a2e;--panel:#16213e;--text:#e0e0e0;--accent:#0f3460;--border:#2a2a4a;--hover:#1a3a6e;--danger:#c0392b;--success:#27ae60;--warn:#f39c12;--input-bg:#0d1b36;--input-border:#3a3a6a} *{box-sizing:border-box;margin:0;padding:0} +input[type=number]{-moz-appearance:textfield} +input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0} body{font-family:monospace;background:var(--bg);color:var(--text);min-height:100vh} .nav{background:var(--panel);border-bottom:1px solid var(--border);padding:8px 16px;display:flex;align-items:center;gap:12px;flex-wrap:wrap} .nav a{color:var(--text);text-decoration:none;padding:4px 10px;border-radius:3px;font-size:13px} @@ -19,8 +21,6 @@ body{font-family:monospace;background:var(--bg);color:var(--text);min-height:100 .form-group{margin-bottom:10px} .form-group label{display:block;font-size:11px;color:#aaa;margin-bottom:3px;text-transform:uppercase;letter-spacing:.5px} .form-group input,.form-group textarea,.form-group select{width:100%;padding:6px 8px;font-size:13px;background:var(--input-bg);color:var(--text);border:1px solid var(--input-border);border-radius:3px;font-family:monospace} -.form-group input[type=number]{-moz-appearance:textfield} -.form-group input[type=number]::-webkit-inner-spin-button,.form-group input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0} .form-group textarea{resize:vertical;min-height:60px} .form-group input[type=checkbox]{width:auto;margin-right:6px} .btn{padding:6px 14px;border:none;border-radius:3px;cursor:pointer;font-size:12px;font-weight:bold;font-family:monospace} @@ -76,14 +76,14 @@ body{font-family:monospace;background:var(--bg);color:var(--text);min-height:100 .talk-seq-line{display:flex;gap:6px;margin:2px 0 2px 10px;font-size:11px;color:#bbb;align-items:center} .talk-seq-num{color:#666;min-width:14px;text-align:right;flex-shrink:0} .talk-action-summary{color:#e8c47c;font-size:10px} -.talk-options{margin-top:6px;padding-top:4px;border-top:1px solid rgba(255,255,255,.05)} +.talk-options{margin-top:6px;padding-top:4px;padding-bottom:6px;border-top:1px solid rgba(255,255,255,.05)} .talk-options-label{font-size:10px;color:#888;text-transform:uppercase;letter-spacing:.3px} .talk-option{display:flex;align-items:center;gap:5px;margin:3px 0;padding:3px 6px;font-size:11px;border-left:1px solid #444;border-radius:0 3px 3px 0} .talk-option-empty{color:#666;font-style:italic;border-left-color:transparent} .talk-opt-arrow{color:#555;flex-shrink:0} .talk-opt-text{color:var(--text);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .talk-opt-goto{color:#8cf;font-weight:bold;white-space:nowrap;min-width:30px} -.talk-opt-tag{display:inline-block;padding:0 3px;font-size:9px;border-radius:2px;line-height:1.4;flex-shrink:0} +.talk-opt-tag{display:inline-block;padding:1px 6px;font-size:9px;border-radius:2px;line-height:1.4;flex-shrink:0;cursor:pointer;border:1px solid transparent} .talk-opt-tag-cond{background:rgba(100,160,255,.2);color:#6af} .talk-opt-tag-act{background:rgba(232,196,124,.2);color:#e8c47c} .talk-opt-del{padding:0 4px;font-size:10px;line-height:1.4;border:none;border-radius:2px;background:var(--danger);color:#fff;cursor:pointer;flex-shrink:0} @@ -119,18 +119,14 @@ body{font-family:monospace;background:var(--bg);color:var(--text);min-height:100 .talk-kv-del{font-size:9px;padding:0 4px;line-height:1.4;border:none;border-radius:2px;background:var(--danger);color:#fff;cursor:pointer;font-family:monospace;flex-shrink:0} .talk-action-block{margin-top:3px;padding:6px;background:rgba(255,255,255,.02);border:1px solid var(--border);border-radius:4px} .talk-action-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:3px 8px} -.talk-shop-block{margin-top:4px;padding:6px;background:rgba(232,196,124,.04);border:1px solid rgba(232,196,124,.15);border-radius:4px} -.talk-shop-header{font-size:10px;color:#e8c47c;text-transform:uppercase;letter-spacing:.3px;margin-bottom:4px} -.talk-shop-item{display:flex;gap:3px;align-items:center;margin-bottom:2px;padding:2px 4px;background:rgba(255,255,255,.02);border-radius:2px} -.talk-shop-item .talk-input{flex:1;min-width:0;padding:2px 4px;font-size:10px} -.talk-shop-item .talk-input-num{width:55px} .talk-add-cond-btn{font-size:9px;padding:1px 6px;line-height:1.4;border:1px solid rgba(100,160,255,.25);border-radius:2px;background:transparent;color:#6af;cursor:pointer;font-family:monospace;margin-bottom:6px} .talk-add-act-btn{font-size:9px;padding:1px 6px;line-height:1.4;border:1px solid rgba(232,196,124,.25);border-radius:2px;background:transparent;color:#e8c47c;cursor:pointer;font-family:monospace;margin-bottom:6px} .talk-cond-card{border:1px solid rgba(100,160,255,.25);border-radius:5px;margin-bottom:6px;overflow:hidden} .talk-cond-card-header{display:flex;align-items:center;justify-content:space-between;padding:6px 8px;background:rgba(100,160,255,.08);border-bottom:1px solid rgba(100,160,255,.15);font-size:10px;color:#aaa;text-transform:uppercase;letter-spacing:.3px} .talk-cond-card-body{padding:6px 8px} -.talk-cond-card-sep{border-top:1px solid rgba(100,160,255,.12);margin:1px 0} +.talk-cond-card-body .talk-action-block{margin-top:0;border:none;padding:0;border-radius:0;background:transparent} +.talk-cond-card-sep{border-top:1px solid rgba(100,160,255,.12);margin:1px 0 4px 0} .talk-cond-mode-row{display:flex;align-items:center;gap:6px;margin-bottom:4px} .talk-cond-mode-row .talk-prop-label{font-size:9px;color:#888;text-transform:uppercase;letter-spacing:.3px} @@ -141,7 +137,7 @@ body{font-family:monospace;background:var(--bg);color:var(--text);min-height:100 .talk-clause-label{color:#888;font-size:9px;white-space:nowrap} .talk-clause-del{padding:0 4px;font-size:9px;line-height:1.4;border:none;border-radius:2px;background:var(--danger);color:#fff;cursor:pointer;font-family:monospace;flex-shrink:0} -.talk-clause-add-bar{display:flex;gap:4px;margin-top:4px;flex-wrap:wrap} +.talk-clause-add-bar{display:flex;gap:4px;margin-top:4px;flex-wrap:wrap;padding-bottom:6px} .talk-clause-add-btn{font-size:9px;padding:1px 6px;line-height:1.4;border:1px solid rgba(100,160,255,.2);border-radius:2px;background:transparent;color:#6af;cursor:pointer;font-family:monospace} .talk-clause-add-btn:hover{background:rgba(100,160,255,.1)} @@ -149,24 +145,29 @@ body{font-family:monospace;background:var(--bg);color:var(--text);min-height:100 .talk-goto-fallback .talk-prop-label{font-size:9px;color:#888;text-transform:uppercase;letter-spacing:.3px;white-space:nowrap;flex-shrink:0} .talk-goto-fallback .talk-input{flex:1;min-width:0;padding:2px 4px;font-size:10px} -.talk-input-num::-webkit-inner-spin-button, -.talk-input-num::-webkit-outer-spin-button{-webkit-appearance:none;margin:0} -.talk-input-num{-moz-appearance:textfield} .talk-add-node-row{display:flex;gap:4px;align-items:center;margin-top:8px} .talk-add-node-row .talk-input{flex:1;max-width:200px} .talk-add-node-row .talk-add-btn{flex-shrink:0} .talk-opt-edit-row{display:flex;gap:4px;align-items:center;margin:3px 0;padding:3px 6px;font-size:11px} .talk-opt-edit-row .talk-input{flex:1;min-width:0;padding:2px 4px;font-size:10px} +.talk-opt-edit-row .talk-opt-text{flex:3} +.talk-opt-edit-row .talk-opt-goto{flex:1;max-width:130px} .talk-opt-edit-row .talk-opt-arrow{flex-shrink:0} .talk-opt-edit-actions{display:flex;gap:2px;align-items:center;flex-shrink:0} .talk-add-opt-row{display:flex;gap:4px;align-items:center;margin-top:4px;padding:0 6px} .talk-add-opt-row .talk-input{flex:1;min-width:0;padding:2px 4px;font-size:10px} +.talk-add-opt-row .talk-opt-text{flex:8} +.talk-add-opt-row .talk-opt-goto{flex:1;max-width:130px} .talk-add-opt-row .talk-add-btn{flex-shrink:0;margin-top:0} +.talk-opt-gap{width:52px;flex-shrink:0} .talk-seq-add-row{display:flex;gap:6px;align-items:center;margin:2px 0 2px 10px} .talk-seq-add-row .talk-input{flex:1;padding:2px 4px;font-size:10px} .talk-seq-add-row .talk-add-btn{flex-shrink:0;margin-top:0} .talk-seq-del{font-size:9px;padding:0 4px;line-height:1.4;border:none;border-radius:2px;background:var(--danger);color:#fff;cursor:pointer;font-family:monospace;flex-shrink:0} +#talktree .search-results{position:absolute;top:100%;left:0;right:0;max-height:160px;overflow-y:auto;background:var(--panel);border:1px solid var(--border);border-radius:3px;z-index:50;box-shadow:0 4px 12px rgba(0,0,0,.4);margin-top:2px} +#talktree .search-results .sr-item{padding:4px 8px;font-size:11px;cursor:pointer;color:var(--text);border-bottom:1px solid rgba(255,255,255,.03)} +#talktree .search-results .sr-item:hover,#talktree .search-results .sr-item.active{background:var(--accent)} .talk-orphans{margin-top:12px;padding-top:8px;border-top:1px dashed var(--border)} .talk-orphans-label{font-size:10px;color:#f39c12;text-transform:uppercase;letter-spacing:.3px;margin-bottom:2px} .talk-orphans-hint{font-size:9px;color:#888;margin-bottom:6px;font-style:italic} @@ -247,7 +248,6 @@ g.ud-hover:hover text{font-weight:bold} .exit-add{display:flex;gap:4px;align-items:center;margin-top:6px} .exit-add select,.exit-add input{width:auto;flex:1;padding:3px 5px;font-size:11px;background:var(--input-bg);color:var(--text);border:1px solid var(--input-border);border-radius:2px;font-family:monospace} .exit-add input[type=number]{-moz-appearance:textfield} -.exit-add input[type=number]::-webkit-inner-spin-button,.exit-add input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0} .exit-add .exit-add-btn{flex-shrink:0;padding:3px 8px;font-size:11px;background:var(--accent);color:var(--text);border:1px solid #1a5a8e;border-radius:2px;cursor:pointer;font-family:monospace} .exit-add .exit-add-btn:hover{background:var(--hover)}.panel-tab-content .entry-row{display:flex;gap:4px;align-items:flex-start;margin-bottom:6px;padding:4px;background:rgba(255,255,255,.02);border-radius:3px;border:1px solid transparent} .panel-tab-content .entry-row:hover{border-color:var(--border)} diff --git a/internal/admin/static/mobeditor.js b/internal/admin/static/mobeditor.js index a65e578..59eb16c 100644 --- a/internal/admin/static/mobeditor.js +++ b/internal/admin/static/mobeditor.js @@ -89,6 +89,24 @@ var MOB_SECTIONS = [ detect: function(d) { return d.talk && d.talk.nodes; }, fields: [] }, + { + id: 'shop', label: 'Shop', path: 'shop', + detect: function(d) { return !!d.shop; }, + fields: [ + ['message', 'Greeting', 'text', 'What would you like to buy or sell?'], + ['buy_percentage', 'Buy %', 'number', '40', 'narrow'], + ['change_percentage', 'Change %', 'number', '3', 'narrow'], + ['restock_ticks', 'Restock Ticks', 'number', '', 'narrow'], + ['buys_anything', 'Buys Anything', 'checkbox'], + ], + subtables: [ + {label:'Items', key:'items', addLabel:'+ Add Item', fields:[ + ['item_id', 'Item ID', 'search', '', '', null, 'items'], + ['stock', 'Stock', 'number', '', 'narrow'], + ['restock_ticks', 'Restock Ticks', 'number', '', 'narrow'], + ]} + ] + }, { id: 'drops', label: 'Drops', path: 'drops', detect: function(d) { return d.drops && (d.drops.remains || (d.drops.loot && d.drops.loot.length > 0)); }, @@ -197,7 +215,7 @@ function renderCurrent() { function renderMobCard(sec, data) { var collapsed = expandedCards[sec.id] === false; - var h = '
'; + var h = '
'; h += '
'; h += '' + (collapsed ? '▶' : '▼') + ''; h += '' + sec.label + ''; @@ -336,7 +354,7 @@ function renderMobSubtable(sec, st, data) { h += '
'; }); if (!st.single || arr.length === 0) { - h += ''; + h += ''; } h += '
'; return h; @@ -374,7 +392,9 @@ function mobAddSection() { if (sec.id === 'drops') { mobData[sec.path] = {remains:'', loot:[]}; } else if (sec.id === 'talk') { - mobData[sec.path] = {nodes:{start:{message:'',options:[]}}}; + mobData[sec.path] = {nodes:{start:{messages:[],options:[]}}}; + } else if (sec.id === 'shop') { + mobData[sec.path] = {buy_percentage:40, change_percentage:3, buys_anything:true, items:[]}; } else { mobData[sec.path] = {}; } @@ -467,7 +487,11 @@ function saveMob() { API.put('/api/mobs/' + encodeURIComponent(mobID), out).then(function(resp) { notify('Mob ' + mobID + ' saved', 'success'); updateUndoBar(); - if (resp && resp._raw) mobData._raw = resp._raw; + if (resp && resp._raw) { + mobData._raw = resp._raw; + var pre = document.querySelector('#tabYaml pre'); + if (pre) pre.textContent = mobData._raw; + } }).catch(function(e) { notify('Save failed: ' + e.message, 'error'); }); } diff --git a/internal/admin/static/objecteditor.js b/internal/admin/static/objecteditor.js index c2374a7..4d02295 100644 --- a/internal/admin/static/objecteditor.js +++ b/internal/admin/static/objecteditor.js @@ -775,9 +775,14 @@ function saveObject() { out.id = objectID; - API.put('/api/objects/' + encodeURIComponent(objectID), out).then(function() { + API.put('/api/objects/' + encodeURIComponent(objectID), out).then(function(resp) { notify('Object ' + objectID + ' saved', 'success'); updateUndoBar(); + if (resp && resp._raw) { + objectData._raw = resp._raw; + var pre = document.querySelector('#tabYaml pre'); + if (pre) pre.textContent = objectData._raw; + } }).catch(function(e) { notify('Save failed: ' + e.message, 'error'); }); } diff --git a/internal/admin/static/talktree.js b/internal/admin/static/talktree.js index e7079f5..4758918 100644 --- a/internal/admin/static/talktree.js +++ b/internal/admin/static/talktree.js @@ -86,11 +86,11 @@ } // 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] !== '') { + 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 && cond.value !== '') c.value = cond.value; + 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; @@ -100,11 +100,11 @@ // 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] !== '') { + if (cond[ck] !== undefined) { var cc = {}; cc[ck] = cond[ck]; if (cond.not) cc.not = cond.not; - if (cond.value !== undefined && cond.value !== '') cc.value = cond.value; + if (cond.value !== undefined) cc.value = cond.value; return { mode: 'single', clauses: [cc], groupKey: null }; } } @@ -115,7 +115,7 @@ 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; + if (clause[k] !== undefined) return k; } return null; } @@ -126,13 +126,13 @@ 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]; } + 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 && c[k] !== '') sub[k] = c[k]; } + for (var k in c) { if (c[k] !== undefined) sub[k] = c[k]; } return sub; }); return cond; @@ -141,21 +141,14 @@ // ---- 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: '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' }, - { 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' }, - ]; + var ACTION_BOOLS = []; // ---- Event delegation ---- @@ -173,6 +166,7 @@ else if (type === 'number') { var n = parseFloat(el.value); val = isNaN(n) ? 0 : n; } else { val = el.value; } talkSyncPath(path, val); + if (path.endsWith('.goto')) renderTalkTree(window._talkData); } else if (tag === 'SELECT') { talkSyncPath(path, el.value); } @@ -207,11 +201,11 @@ case 'add-node': addNode(); break; - case 'add-sequence': - addSequenceLine(nodeKey); + case 'add-message': + addMessageLine(nodeKey); break; - case 'delete-sequence': - deleteSequenceLine(nodeKey, parseInt(optIdx, 10)); + case 'delete-message': + deleteMessageLine(nodeKey, parseInt(optIdx, 10)); break; case 'add-condition': toggleCondition(nodeKey, optIdx !== null ? parseInt(optIdx, 10) : null); @@ -240,12 +234,6 @@ 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; } } @@ -255,6 +243,8 @@ var container = document.getElementById('talktree'); if (!container) return; window._talkData = data; + window._talkOpenNodes = window._talkOpenNodes || new Set(); + window._talkOpenNodes.add('start'); if (!data || !data.talk || !data.talk.nodes) { container.innerHTML = '

No conversation tree defined.

'; return; @@ -285,6 +275,9 @@ html += renderAddNodeRow(); html += '
'; container.innerHTML = html; + if (typeof ced_setupSearchFields === 'function') { + setTimeout(function() { ced_setupSearchFields(); }, 10); + } }; // ---- Add/Delete Node ---- @@ -302,7 +295,7 @@ var key = input.value.trim(); if (!key) return; if (talkNodes()[key]) { notify('Node "' + key + '" already exists.', 'error'); return; } - talkNodes()[key] = { message: '', options: [] }; + talkNodes()[key] = { messages: [], options: [] }; input.value = ''; renderTalkTree(window._talkData); talkSync(); @@ -328,7 +321,7 @@ function renderOrphanNode(key, node, nodes) { var h = '
'; - h += '
'; + h += '
'; h += ''; h += '' + esc(key) + ''; h += '' + esc((getNodePreview(node) || '').substring(0, 60)) + ''; @@ -349,16 +342,17 @@ var node = nodes[rootKey]; if (!node) return ''; if (seen[rootKey]) { - return '
' + esc(rootKey) + ' (already shown)
'; + return '
' + esc(rootKey) + ' (see above)
'; } seen[rootKey] = true; var previewText = getNodePreview(node); + var isOpen = window._talkOpenNodes.has(rootKey); - var html = '
'; + var html = '
'; // Header — fix: check for data-action before toggling - html += '
'; + html += '
'; html += ''; html += '' + esc(rootKey) + ''; html += '' + esc((previewText || '').substring(0, 60)) + ''; @@ -390,16 +384,19 @@ } 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]; + if (node.messages && node.messages.length) return node.messages[0]; return ''; } // ---- Node body: condition-gated content ---- + // Track open node state across re-renders + window._talkOpenNodes = window._talkOpenNodes || new Set(); + function renderNodeBody(nodeKey, node, nodes) { var ec = extractClauses(node.condition); - var hasCond = ec.mode !== 'none'; + var hasCond = ec.mode !== 'none' || (node.condition && typeof node.condition === 'object' && !Array.isArray(node.condition)); + var hasOpts = node.options && node.options.length > 0; var html = ''; @@ -412,23 +409,38 @@ html += '
'; html += renderConditionClauses(ec, nodeKey, null); html += '
'; - html += renderMessageSection(nodeKey, node); - html += renderSequenceSection(nodeKey, node); - html += renderActionSection(nodeKey, null, node.action); + html += renderMessagesSection(nodeKey, node); html += renderOptionsSection(nodeKey, node); + html += '
'; + + var nodeAction = node.action; + var actionPrefix = 'nodes.' + escAttr(nodeKey) + '.action'; + if (nodeAction && typeof nodeAction === 'object' && !Array.isArray(nodeAction)) { + html += '
'; + html += '
' + + 'Action' + + '
'; + html += '
'; + html += renderActionForm(nodeAction, actionPrefix, nodeKey, null); + html += '
'; + html += '
'; + } else { + html += ''; + } + + html += '
'; + html += renderGotoFallback(nodeKey, node, 'If condition fails, go to'); html += '
'; // .talk-cond-card-body html += '
'; // .talk-cond-card } else { html += ''; - html += renderMessageSection(nodeKey, node); - html += renderSequenceSection(nodeKey, node); - html += renderActionSection(nodeKey, null, node.action); + html += renderMessagesSection(nodeKey, node); html += renderOptionsSection(nodeKey, node); - } - - // Goto fallback — only when no options - if (!node.options || node.options.length === 0) { - html += renderGotoFallback(nodeKey, node); + html += '
'; + html += renderActionSection(nodeKey, null, node.action); + if (!hasOpts) { + html += renderGotoFallback(nodeKey, node, 'Auto-advance to'); + } } return html; @@ -436,39 +448,31 @@ // ---- Goto fallback ---- - function renderGotoFallback(nodeKey, node) { + function renderGotoFallback(nodeKey, node, label) { + label = label || 'Auto-advance to'; return '
' + - 'Auto-advance to' + - '' + + '' + esc(label) + '' + + '' + '
'; } - // ---- Message / Sequence ---- - - function renderMessageSection(nodeKey, node) { - var msg = node.message || ''; - if (Array.isArray(msg)) msg = msg.join('\n'); - return '
' + - '
Message
' + - '' + - '
'; - } + // ---- Messages section ---- - function renderSequenceSection(nodeKey, node) { - var seq = node.sequence || []; + function renderMessagesSection(nodeKey, node) { + var msgs = node.messages || []; var html = '
'; - html += '
Sequence
'; - seq.forEach(function(line, i) { + html += '
Messages
'; + msgs.forEach(function(line, i) { html += '
' + - '' + (i + 1) + '' + - '' + - '' + + '' + + '' + + '' + '
'; }); html += '
' + - '' + (seq.length + 1) + '' + - '' + - '' + + '' + + '' + + '' + '
'; html += '
'; return html; @@ -518,15 +522,16 @@ html += '' + esc(clauseTypeLabel(typeKey)) + ''; if (typeKey === 'flag' || typeKey === 'player_flag') { + var notLabel = (clause.value !== undefined && clause.value !== '') ? 'Not' : 'Not Exists'; html += 'key:'; html += ''; - html += ''; + html += ''; html += 'val:'; html += ''; } else if (typeKey === 'has_item') { html += 'item:'; html += ''; - html += ''; + html += ''; } else if (typeKey === 'min_credits') { html += 'cr:'; html += ''; @@ -620,6 +625,10 @@ delete target.condition; } else { target.condition = {}; + if (optIdx !== null) { + window._talkAutoOpenDetails = new Set(); + window._talkAutoOpenDetails.add('cond_' + optIdx + '_' + escAttr(nodeKey)); + } } renderTalkTree(window._talkData); talkSync(); @@ -661,10 +670,21 @@ ACTION_FIELDS.forEach(function(f) { var fpath = prefix + '.' + f.key; var val = (action && action[f.key] !== undefined) ? action[f.key] : ''; - html += '
' + - '' + esc(f.label) + '' + - '' + - '
'; + if (f.type === 'item_search') { + html += '
' + + '' + esc(f.label) + '' + + '
' + + '' + + '' + + '
' + + '
'; + } else { + var narrowStyle = (f.key === 'teleport' || f.key === 'heal') ? ' style="max-width:70px"' : ''; + html += '
' + + '' + esc(f.label) + '' + + '' + + '
'; + } }); html += '
'; @@ -678,7 +698,6 @@ 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; @@ -697,14 +716,14 @@ html += '
'; keys.forEach(function(k) { html += '
' + + '' + '' + - 'key: ' + esc(k) + '' + '' + '
'; }); html += '
' + - '' + - '' + + '' + + '' + '' + '' + '
'; @@ -712,62 +731,30 @@ return html; } - 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'; + html += 'Options'; if (options.length > 0) { options.forEach(function(opt, idx) { html += '
'; - html += ''; - html += ''; - html += ''; + html += '' + (idx + 1) + ''; + html += ''; + html += ''; html += '
'; if (opt.condition && typeof opt.condition === 'object' && !Array.isArray(opt.condition)) { html += 'cond'; } else { - html += ''; + html += ''; } if (opt.action) { html += 'act'; } else { - html += ''; + html += ''; } html += '
'; @@ -775,7 +762,9 @@ html += '
'; if (opt.condition && typeof opt.condition === 'object' && !Array.isArray(opt.condition)) { - html += '