diff options
Diffstat (limited to 'internal/admin/static')
| -rw-r--r-- | internal/admin/static/admin.css | 36 | ||||
| -rw-r--r-- | internal/admin/static/intereditor.js | 1074 | ||||
| -rw-r--r-- | internal/admin/static/map.js | 229 | ||||
| -rw-r--r-- | internal/admin/static/mobeditor.js | 9 | ||||
| -rw-r--r-- | internal/admin/static/objecteditor.js | 41 |
5 files changed, 876 insertions, 513 deletions
diff --git a/internal/admin/static/admin.css b/internal/admin/static/admin.css index b35d5e2..7c8f0a5 100644 --- a/internal/admin/static/admin.css +++ b/internal/admin/static/admin.css @@ -420,8 +420,7 @@ g.ud-hover:hover text{font-weight:bold} .ie-kv-row{display:flex;gap:4px;align-items:center} .ie-kv-key,.ie-kv-val{font-size:11px;padding:2px 4px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:3px;font-family:monospace;width:120px} .ie-kv-val{width:80px} -.ie-act-msg-delay,.ie-act-msg{font-size:11px;padding:3px 6px;background:var(--input-bg);color:var(--text);border:1px solid var(--input-border);border-radius:3px;font-family:monospace} -.ie-act-msg-delay{width:60px} +.ie-act-msg{font-size:11px;padding:3px 6px;background:var(--input-bg);color:var(--text);border:1px solid var(--input-border);border-radius:3px;font-family:monospace} .ie-act-msg{flex:1} /* Interaction row layout: column. The single flex header row holds the @@ -439,3 +438,36 @@ g.ud-hover:hover text{font-weight:bold} .ie-inter-panel-row{display:flex;gap:6px;align-items:flex-start} .ie-inter-panel-body{flex:1;min-width:0} .ie-inter-panel-x{margin-top:4px;flex-shrink:0} + +/* ── Step list: boxed steps with right-justified move/remove controls ── */ +.ie-inter-steps{margin-top:4px} +.ie-step-list{display:flex;flex-direction:column;gap:4px} +.ie-step-row{background:rgba(100,160,255,.03);border:1px solid rgba(100,160,255,.1);border-radius:6px;padding:8px;display:flex;flex-direction:column;gap:4px} +.ie-step-header{display:flex;align-items:center;gap:6px} +.ie-step-label{font-size:10px;color:#6af;text-transform:uppercase;letter-spacing:.3px;font-weight:bold;white-space:nowrap} +.ie-step-addmore{color:#6af;border:1px solid rgba(100,160,255,.25);background:transparent;font-size:10px;padding:1px 6px;border-radius:3px;cursor:pointer;font-family:monospace;line-height:1} +.ie-step-addmore:hover{background:rgba(100,160,255,.1)} +.ie-step-spacer{flex:1} +.ie-step-up,.ie-step-dn{padding:2px 6px;font-size:9px} +.ie-step-cond-panel{margin:2px 0} +.ie-step-effects{margin:2px 0} +.ie-step-messages{margin:2px 0} +.ie-step-addbar{display:none;flex-wrap:wrap;gap:3px;margin-top:2px} +.ie-step-row.show-addbar .ie-step-addbar{display:flex} +.ie-step-addbar-bottom{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px} +.ie-step-addbar-bottom .btn{padding:4px 10px;font-size:11px} + +/* ── Message rows within a step ── */ +.ie-msg-row{display:flex;align-items:center;gap:4px} +.ie-msg-up,.ie-msg-dn{padding:1px 4px;font-size:9px;line-height:1} + +/* ── Lock player checkbox ── */ +.ie-lock-lbl{font-size:10px;color:var(--text);display:inline-flex;align-items:center;gap:3px;cursor:pointer;white-space:nowrap;margin-right:6px} +.ie-lock-cb{accent-color:var(--accent);margin:0} + +/* ── Trigger row move up/down ── */ +.ie-trig-up,.ie-trig-dn{padding:2px 6px;font-size:9px} + +/* ── Empty condition group placeholder ── */ +.ie-cond-empty{margin-bottom:2px} +.ie-cond-empty-bar{display:flex;align-items:center;gap:6px;flex-wrap:wrap;padding-bottom:2px} diff --git a/internal/admin/static/intereditor.js b/internal/admin/static/intereditor.js index 5cf39a1..653bccf 100644 --- a/internal/admin/static/intereditor.js +++ b/internal/admin/static/intereditor.js @@ -1,38 +1,172 @@ -// intereditor.js — Structured condition & action editors for on_use/on_look/on_kill. -// Replaces free-form JSON textareas with button-driven pickers. -// Shared by objecteditor.js and mobeditor.js. +// intereditor.js — Structured condition & step editors for trigger blocks. +// Drives the unified Trigger model: +// Trigger = { lock, item_id?, condition?, steps[], on_player_flag?, on_global_flag?, value? } +// Step = { condition?, wait, messages[], broadcast, broadcast_global, +// spawn_mob, despawn_mob, set_global_flags, set_player_flags, +// give_item, take_item, teleport, heal, credits, aps_node } +// Shared by objecteditor.js, mobeditor.js, and map.js. // ── Condition leaf types ── var IE_COND_TYPES = [ {type: 'global_flag', label: 'Global Flag', hint: 'global flag name'}, {type: 'player_flag', label: 'Player Flag', hint: 'player flag name'}, + {type: 'room', label: 'In Room', hint: 'room ID'}, {type: 'has_item', label: 'Has Item', hint: 'item ID'}, {type: 'min_credits', label: 'Min Credits', hint: 'amount'}, ]; +// ── Effect field types ── +// +// Every block is sequence-scoped now: broadcast / broadcast_global / +// spawn_mob / despawn_mob are available everywhere. Messages and condition +// are handled separately (plain-string message rows; per-step condition). + var IE_ACT_TYPES = [ - {key: 'set_global_flags', label: 'Set Global Flags', kind: 'kv'}, - {key: 'set_player_flags', label: 'Set Player Flags', kind: 'kv'}, - {key: 'give_item', label: 'Give Item', kind: 'search', entity: 'items'}, - {key: 'take_item', label: 'Take Item', kind: 'search', entity: 'items'}, - {key: 'teleport', label: 'Teleport', kind: 'number', hint: 'room ID'}, - {key: 'heal', label: 'Heal', kind: 'number', hint: 'HP to restore'}, - {key: 'credits', label: 'Credits', kind: 'number', hint: 'amount (negative = cost)'}, - {key: 'aps_node', label: 'APS Node', kind: 'checkbox'}, + {key: 'wait', label: 'Wait', kind: 'number', hint: 'ticks before this step'}, + {key: 'broadcast', label: 'Broadcast', kind: 'text', hint: '%p = player'}, + {key: 'broadcast_global', label: 'Broadcast Global', kind: 'text', hint: '%p = player'}, + {key: 'spawn_mob', label: 'Spawn Mob', kind: 'search', entity: 'mobs'}, + {key: 'despawn_mob', label: 'Despawn Mob', kind: 'search', entity: 'mobs'}, + {key: 'set_global_flags', label: 'Set Global Flags', kind: 'kv'}, + {key: 'set_player_flags', label: 'Set Player Flags', kind: 'kv'}, + {key: 'give_item', label: 'Give Item', kind: 'search', entity: 'items'}, + {key: 'take_item', label: 'Take Item', kind: 'search', entity: 'items'}, + {key: 'teleport', label: 'Teleport', kind: 'number', hint: 'room ID'}, + {key: 'heal', label: 'Heal', kind: 'number', hint: 'HP to restore'}, + {key: 'credits', label: 'Credits', kind: 'number', hint: 'amount (negative = cost)'}, + {key: 'aps_node', label: 'APS Node', kind: 'checkbox'}, ]; -// Sequence-only effects (only shown for scope === 'seq'). -var IE_ACT_TYPES_SEQ = [ - {key: 'broadcast', label: 'Broadcast', kind: 'text', hint: '%p = player'}, - {key: 'broadcast_global', label: 'Broadcast Global', kind: 'text', hint: '%p = player'}, - {key: 'spawn_mob', label: 'Spawn Mob', kind: 'search', entity: 'mobs'}, - {key: 'despawn_mob', label: 'Despawn Mob', kind: 'search', entity: 'mobs'}, +// ── Step-kind buttons for the bottom add-bar (each click creates a new +// step initialised with that single action kind) and the per-step +// "+" toggle bar. messages is handled separately from IE_ACT_TYPES. ── + +var IE_STEP_KINDS = [ + {key: 'messages', label: 'Message', kind: 'messages'}, ]; +IE_ACT_TYPES.forEach(function(at) { + IE_STEP_KINDS.push(at); +}); + +// ── Rendering helpers ── + +// ie_renderStepAddbar: the per-step action add bar revealed by the "+" toggle. +// Only shows kinds not already present on this step. +function ie_renderStepAddbar(step, secId, idx, si, secPath) { + var h = ''; + IE_STEP_KINDS.forEach(function(sk) { + if (sk.key === 'messages') { + if (step && Array.isArray(step.messages)) return; + } else { + if (step && Object.prototype.hasOwnProperty.call(step, sk.key)) { + if (sk.kind === 'checkbox' && step[sk.key] !== true) { /* re-addable */ } + else return; + } + } + 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>'; + } 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>'; + } + }); + return h; +} + +// ie_renderBottomAddbar: the always-visible bar at the foot of the step list. +// Each button creates a new step initialised with that single action kind. +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) + ' Step</button>'; + }); + return h; +} + +// ie_addStepKind creates a new step initialised with a single action kind. +function ie_addStepKind(secId, secPath, idx, kind) { + var ed = window._editorData; + if (!ed) return; + ced_syncDOMToData(ed); + if (window._ieSyncAll) window._ieSyncAll(ed); + if (!ed[secPath]) ed[secPath] = []; + if (!ed[secPath][idx]) ed[secPath][idx] = {}; + if (!ed[secPath][idx].steps || !Array.isArray(ed[secPath][idx].steps)) ed[secPath][idx].steps = []; + var step = {}; + if (kind === 'messages') { + step.messages = ['']; + } else if (kind === 'wait') { + step.wait = 1; + } else { + var at = IE_ACT_TYPES.find(function(a) { return a.key === kind; }); + if (!at) return; + if (at.kind === 'kv') step[kind] = {}; + else if (at.kind === 'checkbox') step[kind] = true; + else if (at.kind === 'number') step[kind] = 0; + else step[kind] = ''; + } + ed[secPath][idx].steps.push(step); + window._ieRenderCurrent(); +} // Sentinel child-index for a root-level single leaf (no parent group). var IE_ROOT_LEAF = -1; +// ── Condition lens helpers (entry vs step) ── +// +// kind is the string-encoded condition anchor: 'entry' for the trigger's own +// condition, or 'step-<n>' for the nth step's condition. Internally we parse +// it to the numeric step index (-1 = entry). + +function _ie_kindStep(kind) { + if (kind === undefined || kind === null || kind === 'entry') return -1; + if (typeof kind === 'string' && kind.indexOf('step-') === 0) { + var n = parseInt(kind.slice(5), 10); + return isNaN(n) ? -1 : n; + } + return -1; +} + +function _ie_getCond(ed, secPath, idx, kind) { + var si = _ie_kindStep(kind); + if (!ed[secPath] || !ed[secPath][idx]) return null; + if (si < 0) return ed[secPath][idx].condition || null; + if (!ed[secPath][idx].steps || !ed[secPath][idx].steps[si]) return null; + return ed[secPath][idx].steps[si].condition || null; +} + +function _ie_setCond(ed, secPath, idx, kind, newCond) { + var si = _ie_kindStep(kind); + if (!ed[secPath]) ed[secPath] = []; + if (!ed[secPath][idx]) ed[secPath][idx] = {}; + if (si < 0) { + if (newCond) ed[secPath][idx].condition = newCond; + else delete ed[secPath][idx].condition; + } else { + if (!ed[secPath][idx].steps) ed[secPath][idx].steps = []; + if (!ed[secPath][idx].steps[si]) ed[secPath][idx].steps[si] = {}; + if (newCond) ed[secPath][idx].steps[si].condition = newCond; + else delete ed[secPath][idx].steps[si].condition; + } +} + +function _ie_syncCond(ed, secId, secPath, idx, kind) { + var card = document.querySelector('.obj-card[data-card="' + secId + '"]'); + if (!card) return; + var si = _ie_kindStep(kind); + var container; + if (si < 0) { + container = card.querySelector('.obj-sub-row.ie-inter-row[data-idx="' + idx + '"]'); + } else { + container = card.querySelector('.obj-sub-row.ie-inter-row[data-idx="' + idx + '"] .ie-step-row[data-step="' + si + '"]'); + } + if (!container) return; + var condRoot = container.querySelector('.ie-cond-root'); + if (!condRoot) return; + var cond = ie_collectCond(condRoot); + _ie_setCond(ed, secPath, idx, kind, cond); +} + // ── Navigation in condition tree ── function ie_condAt(cond, path) { @@ -48,7 +182,6 @@ function ie_condAt(cond, path) { return cur; } -// Replaces the condition at path within the overall tree. Returns new root. function ie_condReplace(cond, path, newVal) { if (!path) return newVal; if (!cond) return cond; @@ -72,13 +205,7 @@ function ie_condReplace(cond, path, newVal) { } // ── Clean condition tree for save ── -// -// The live editor tree intentionally holds empty leaves/groups while the user -// is building up a condition (so clicking a second leaf button adds a sibling -// rather than replacing the first). This walker strips those in-progress -// artifacts before save: drops leaves whose value is missing/empty, drops -// 0-child groups, and collapses 1-child non-NOT groups to their child. min_credits -// keeps the value 0 (falsy in JS but a valid threshold). + function ie_cleanCondition(cond) { if (!cond || typeof cond !== 'object') return null; if (cond.all_of || cond.any_of) { @@ -91,7 +218,6 @@ function ie_cleanCondition(cond) { r[mode] = cleaned; return r; } - // Leaf var not = !!cond.not; var leaf = null; if (cond.global_flag !== undefined) { @@ -104,6 +230,8 @@ function ie_cleanCondition(cond) { leaf = { player_flag: cond.player_flag }; if (cond.value !== undefined) leaf.value = cond.value; } + } else if (cond.room !== undefined) { + if (cond.room) leaf = { room: parseInt(cond.room, 10) || 0 }; } else if (cond.has_item !== undefined) { if (cond.has_item) leaf = { has_item: cond.has_item }; } else if (cond.min_credits !== undefined) { @@ -120,12 +248,6 @@ function ie_cleanCondition(cond) { function ie_collectCond(rootEl) { if (!rootEl) return null; - // A .ie-cond-root wraps either a single group (.ie-cond) or a single bare - // leaf (.ie-cond-leaf). Unwrap it so sync is the pure inverse of render: - // cond-root > .ie-cond(group) -> ie_collectCond(.ie-cond) - // cond-root > .ie-cond-leaf -> ie_collectLeaf(leafEl) - // The old version recursed into the .ie-cond child and then wrapped the - // result, producing {all_of:[{all_of:[...]}]} for any group condition. if (rootEl.classList.contains('ie-cond-root')) { var groupEl = rootEl.querySelector(':scope > .ie-cond'); if (groupEl) return ie_collectCond(groupEl); @@ -134,7 +256,6 @@ function ie_collectCond(rootEl) { return null; } - // rootEl is a .ie-cond group box. var mode = 'all_of'; var modeSel = rootEl.querySelector(':scope > .ie-cond-modebar .ie-cond-modesel'); if (modeSel) mode = modeSel.value; @@ -155,13 +276,6 @@ function ie_collectCond(rootEl) { }); } - // Preserve empty groups (no modebar rendered for <2 children, so empty groups - // always serialize as {all_of:[]}). Without this, "+ Group" would vanish on - // the next sync before the user could add their first leaf, and the - // subsequent "+ leaf" would turn it into a bare leaf — silently dropping - // the user's group intent. Empty {all_of:[]} is harmless game-side - // (checkCondition returns true, equivalent to no condition) and - // ced_cleanOutput strips it from saved YAML. var result = {}; if (not) result.not = true; result[mode] = items; @@ -178,11 +292,6 @@ function ie_collectLeaf(el) { var valInput = el.querySelector('.ie-cond-val'); var val = valInput ? valInput.value.trim() : ''; - // Always return the leaf (even when val is empty) so the in-memory - // condition tree keeps "in-progress" leaves the user just added. Without - // this, clicking a second leaf button would sync, see null, and wipe the - // first leaf — breaking multi-add and nested-group building. ie_cleanCondition - // strips empty leaves at save time. switch (leafType) { case 'global_flag': result.global_flag = val; @@ -200,6 +309,9 @@ function ie_collectLeaf(el) { try { result.value = JSON.parse(sv); } catch(e) { result.value = sv; } } break; + case 'room': + result.room = parseInt(val) || 0; + break; case 'has_item': result.has_item = val; break; @@ -212,123 +324,30 @@ function ie_collectLeaf(el) { return result; } -// ── Collect action from DOM ── - -function ie_collectAct(rootEl) { - if (!rootEl) return null; - var r = {}; - - // Messages — collected from the extendable delay+message rows. - var msgsKv = rootEl.querySelector('.ie-act-kv[data-ie-act-key="messages"]'); - if (msgsKv) { - var list = []; - msgsKv.querySelectorAll('.ie-kv-row').forEach(function(row) { - var delayEl = row.querySelector('.ie-act-msg-delay'); - var msgEl = row.querySelector('.ie-act-msg'); - var d = delayEl ? parseInt(delayEl.value) || 0 : 0; - var m = msgEl ? msgEl.value.trim() : ''; - list.push({ message: m, delay: d }); - }); - r.messages = list; - } - - // KV-type effects — include the map even when empty (as long as the - // .ie-act-kv element exists in the DOM). This lets a just-added section - // (e.g. "+ Set Player Flags" → no rows yet) survive the next sync instead - // of being dropped on the next add-effect call. ced_cleanOutput strips - // empty maps at save time. - rootEl.querySelectorAll('.ie-act-kv').forEach(function(kvEl) { - var key = kvEl.getAttribute('data-ie-act-key'); - if (!key) return; - if (key === 'messages') return; - var map = {}; - kvEl.querySelectorAll('.ie-kv-row').forEach(function(row) { - var kEl = row.querySelector('.ie-kv-key'); - var vEl = row.querySelector('.ie-kv-val'); - if (kEl && kEl.value.trim() && vEl) { - var v = vEl.value.trim(); - var num = parseFloat(v); - map[kEl.value.trim()] = v === 'true' ? true : (v === 'false' ? false : (!isNaN(num) ? num : v)); - } - }); - r[key] = map; - }); - - // Scalar effects — preserve empty strings so a present-but-unfilled input - // round-trips on every sync. - rootEl.querySelectorAll('.ie-act-input').forEach(function(el) { - var key = el.getAttribute('data-ie-act-key'); - if (!key) return; - var kind = el.getAttribute('data-ie-act-kind'); - if (kind === 'checkbox') { - r[key] = el.checked; - } else if (kind === 'number') { - var n = parseFloat(el.value); - if (isNaN(n)) return; - r[key] = n; - } else { - r[key] = el.value.trim(); - } - }); - - if (Object.keys(r).length === 0) return null; - return r; -} - -// ── Sync all interaction DOM back to data ── - -function ie_syncAllInteractions(data, secs) { - if (!data || !secs) return; - secs.forEach(function(sec) { - if (!sec.interactionStyle || !sec.isArray) return; - var p = sec.path; - if (!data[p]) return; - var card = document.querySelector('.obj-card[data-card="' + sec.id + '"]'); - if (!card) return; - var rows = card.querySelectorAll('.obj-sub-row'); - rows.forEach(function(row, idx) { - if (!data[p][idx]) data[p][idx] = {}; - var condRoot = row.querySelector('.ie-cond-root'); - if (condRoot) { - var cond = ie_collectCond(condRoot); - if (cond) data[p][idx].condition = cond; - else delete data[p][idx].condition; - } - var actRoot = row.querySelector('.ie-act-root'); - if (actRoot) { - var act = ie_collectAct(actRoot); - if (act) data[p][idx].action = act; - else delete data[p][idx].action; - } - }); - }); -} - // ── Render condition ── -function ie_renderCond(condObj, secId, idx, secPath) { +function ie_renderCond(condObj, secId, idx, secPath, kind) { + kind = kind || 'entry'; var h = ''; var isGroup = condObj && (condObj.all_of || condObj.any_of); if (condObj && typeof condObj === 'object') { h += '<div class="ie-cond-root">'; - h += ie_renderGroup(condObj, secId, secPath, idx, ''); + h += ie_renderGroup(condObj, secId, secPath, idx, '', kind); h += '</div>'; } - // For a group root, the group's own addbar (rendered inside _ie_renderGroupNode) - // already covers appending — don't emit a duplicate top-level addbar. var showAddbar = !isGroup; if (showAddbar) { h += '<div class="ie-cond-addbar">'; 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 + ',\'\',\'' + ct.type + '\')">+ ' + esc(ct.label) + '</button>'; + h += '<button class="btn btn-sm ie-add-btn ie-cond-add-leaf" onclick="ie_addLeaf(\'' + 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_addGroup(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'\')">+ Group</button>'; + h += '<button class="btn btn-sm ie-add-btn ie-cond-add-grp" onclick="ie_addGroup(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\',\'\')">+ Group</button>'; h += '</div>'; } return h; } -function ie_renderGroup(cond, secId, secPath, idx, gpath) { +function ie_renderGroup(cond, secId, secPath, idx, gpath, kind) { var mode = null, children = null; if (cond.all_of) { mode = 'all_of'; children = cond.all_of; } else if (cond.any_of) { mode = 'any_of'; children = cond.any_of; } @@ -336,74 +355,74 @@ function ie_renderGroup(cond, secId, secPath, idx, gpath) { var not = !!cond.not; if (mode && children) { - return _ie_renderGroupNode(mode, children, not, secId, secPath, idx, gpath); + return _ie_renderGroupNode(mode, children, not, secId, secPath, idx, gpath, kind); } - // Leaf - if (cond.global_flag !== undefined) return _ie_renderLeaf('global_flag', cond.global_flag, cond.value, not, secId, secPath, idx, gpath, IE_ROOT_LEAF); - if (cond.player_flag !== undefined) return _ie_renderLeaf('player_flag', cond.player_flag, cond.value, not, secId, secPath, idx, gpath, IE_ROOT_LEAF); - if (cond.has_item !== undefined) return _ie_renderLeaf('has_item', cond.has_item, '', not, secId, secPath, idx, gpath, IE_ROOT_LEAF); - if (cond.min_credits !== undefined) return _ie_renderLeaf('min_credits', cond.min_credits, '', not, secId, secPath, idx, gpath, IE_ROOT_LEAF); + if (cond.global_flag !== undefined) return _ie_renderLeaf('global_flag', cond.global_flag, cond.value, not, secId, secPath, idx, gpath, IE_ROOT_LEAF, kind); + if (cond.player_flag !== undefined) return _ie_renderLeaf('player_flag', cond.player_flag, cond.value, not, secId, secPath, idx, gpath, IE_ROOT_LEAF, kind); + if (cond.room !== undefined) return _ie_renderLeaf('room', cond.room, '', not, secId, secPath, idx, gpath, IE_ROOT_LEAF, kind); + if (cond.has_item !== undefined) return _ie_renderLeaf('has_item', cond.has_item, '', not, secId, secPath, idx, gpath, IE_ROOT_LEAF, kind); + if (cond.min_credits !== undefined) return _ie_renderLeaf('min_credits', cond.min_credits, '', not, secId, secPath, idx, gpath, IE_ROOT_LEAF, kind); return ''; } -function _ie_renderGroupNode(mode, children, not, secId, secPath, idx, gpath) { +function _ie_renderGroupNode(mode, children, not, secId, secPath, idx, gpath, kind) { var h = ''; if (children.length === 0) { - // Empty group: single line — addbar buttons left, X right. 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(gpath) + '\',\'' + ct.type + '\')">+ ' + esc(ct.label) + '</button>'; + 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-grp" onclick="ie_addGroup(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + 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(gpath) + '\')" title="Remove this group">X</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 += '</div>'; h += '</div>'; return h; } - // Non-empty group: modebar (no X) + children + addbar below. 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(gpath) + '\')">'; + h += '<select class="ie-cond-modesel" onchange="ie_toggleCondMode(\'' + 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(gpath) + '\')"' + (not ? ' checked' : '') + '> NOT</label>'; + 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 += '</div>'; h += '<div class="ie-cond-children">'; children.forEach(function(child, ci) { var cpath = gpath ? gpath + '-' + ci : String(ci); if (child.all_of || child.any_of) { - h += ie_renderGroup(child, secId, secPath, idx, cpath); + h += ie_renderGroup(child, secId, secPath, idx, cpath, kind); } else { var cval = '', csub = '', ctype = '', cnot = !!child.not; if (child.global_flag !== undefined) { ctype = 'global_flag'; cval = child.global_flag; csub = child.value; } else if (child.player_flag !== undefined) { ctype = 'player_flag'; cval = child.player_flag; csub = child.value; } + else if (child.room !== undefined) { ctype = 'room'; cval = child.room; } else if (child.has_item !== undefined) { ctype = 'has_item'; cval = child.has_item; } else if (child.min_credits !== undefined) { ctype = 'min_credits'; cval = child.min_credits; } - if (ctype) h += _ie_renderLeaf(ctype, cval, csub, cnot, secId, secPath, idx, gpath, ci); + if (ctype) h += _ie_renderLeaf(ctype, cval, csub, cnot, secId, secPath, idx, gpath, ci, kind); } }); 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(gpath) + '\',\'' + ct.type + '\')">+ ' + esc(ct.label) + '</button>'; + 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-grp" onclick="ie_addGroup(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(gpath) + '\')">+ Group</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 += '</div>'; h += '</div>'; return h; } -function _ie_renderLeaf(leafType, leafVal, leafSub, not, secId, secPath, idx, gpath, ci) { +function _ie_renderLeaf(leafType, leafVal, leafSub, not, secId, secPath, idx, gpath, ci, kind) { + kind = kind || 'entry'; var label = ''; IE_COND_TYPES.forEach(function(ct) { if (ct.type === leafType) label = ct.label; }); var hint = ''; @@ -420,7 +439,7 @@ function _ie_renderLeaf(leafType, leafVal, leafSub, not, secId, secPath, idx, gp h += '<input class="ie-cond-val search-input" data-search-type="items" placeholder="' + escAttr(hint) + '" value="' + escAttr(sv) + '">'; h += '<div class="search-results" style="display:none"></div>'; h += '</div>'; - } else if (leafType === 'min_credits') { + } else if (leafType === 'min_credits' || leafType === 'room') { h += '<input class="ie-cond-val" placeholder="' + escAttr(hint) + '" value="' + escAttr(sv) + '" type="number">'; } else { h += '<input class="ie-cond-val" placeholder="' + escAttr(hint) + '" value="' + escAttr(sv) + '">'; @@ -429,64 +448,33 @@ 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(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(gpath) + '\',' + ciToken + ')" title="Remove">X</button>'; + 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 += '</div>'; return h; } -// ── Render action ── +// ── Render step effects (everything except messages and condition). +// The add-bar is rendered separately via ie_renderStepAddbar. ── -function ie_renderAct(actionObj, scope, secId, idx, secPath) { +function ie_renderAct(stepObj, secId, idx, si, secPath) { var hasAny = false; var rows = ''; - // Messages — extendable list of {delay, message} entries. Mirrors the KV-section - // pattern: when the 'messages' key is present we render the section with rows - // and an internal + Add; when it goes away (last row removed) the section - // disappears and the addbar + Add Message button reappears. There is no - // section-level X. - var msgsPresent = actionObj && Object.prototype.hasOwnProperty.call(actionObj, 'messages') && Array.isArray(actionObj.messages); - if (msgsPresent) { - var msgs = actionObj.messages; - hasAny = true; - var rmFn = 'ie_removeMessage(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ')'; - rows += '<div class="ie-act-kv" data-ie-act-key="messages">'; - rows += '<div class="ie-act-kv-header"><span>Messages</span></div>'; - rows += '<div class="ie-kv-list">'; - if (msgs.length === 0) { - rows += '<div class="ie-kv-row"><input type="number" class="ie-act-msg-delay" value="" placeholder="delay" style="width:60px" title="Ticks before showing">'; - rows += '<input class="ie-act-msg" data-ie-msg-idx="0" value="" placeholder="Shown to player" style="flex:1">'; - rows += '<button class="btn btn-sm btn-danger" onclick="' + rmFn + '">X</button></div>'; - } else { - msgs.forEach(function(m, mi) { - rows += '<div class="ie-kv-row"><input type="number" class="ie-act-msg-delay" value="' + escAttr(m.delay ? String(m.delay) : '') + '" placeholder="delay" style="width:60px" title="Ticks before showing">'; - rows += '<input class="ie-act-msg" data-ie-msg-idx="' + mi + '" value="' + escAttr(String(m.message || '')) + '" placeholder="Shown to player" style="flex:1">'; - rows += '<button class="btn btn-sm btn-danger" onclick="' + rmFn + '">X</button></div>'; - }); - } - rows += '</div>'; - rows += '<button class="btn btn-sm ie-add-btn" style="margin-top:2px" onclick="ie_addMessage(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ')">+ Add Message</button>'; - rows += '</div>'; - } - - // Standard effects IE_ACT_TYPES.forEach(function(at) { - if (!actionObj || !Object.prototype.hasOwnProperty.call(actionObj, at.key)) return; - var val = actionObj[at.key]; + if (!stepObj || !Object.prototype.hasOwnProperty.call(stepObj, at.key)) return; + var val = stepObj[at.key]; if (at.kind === 'checkbox' && val !== true) return; hasAny = true; if (at.kind === 'kv') { var kvMap = (val && typeof val === 'object') ? val : {}; var kvKeys = Object.keys(kvMap); - var kvRmAttr = 'ie_removeKVRow(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(at.key) + '\')'; + var kvRmAttr = 'ie_removeKVRow(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',\'' + escAttr(at.key) + '\')'; rows += '<div class="ie-act-kv" data-ie-act-key="' + escAttr(at.key) + '">'; rows += '<div class="ie-act-kv-header"><span>' + esc(at.label) + '</span></div>'; rows += '<div class="ie-kv-list">'; if (kvKeys.length === 0) { - // Start with a blank row so there are input boxes immediately. The - // row's X acts as the section's only remove path (no header X). rows += '<div class="ie-kv-row"><input value="" placeholder="key" class="ie-kv-key">'; rows += '<input value="" placeholder="val" class="ie-kv-val">'; rows += '<button class="btn btn-sm btn-danger" onclick="' + kvRmAttr + '">X</button></div>'; @@ -498,92 +486,239 @@ function ie_renderAct(actionObj, scope, secId, idx, secPath) { }); } rows += '</div>'; - rows += '<button class="btn btn-sm ie-add-btn" style="margin-top:2px" onclick="ie_addKVRow(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(at.key) + '\')">+ Add</button>'; + 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 += '</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 + ',\'' + escAttr(at.key) + '\')">X</button>'; + 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 += '</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 + ',\'' + escAttr(at.key) + '\')">X</button>'; + 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 += '</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 + ',\'' + escAttr(at.key) + '\')">X</button>'; + 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 += '</div>'; } }); - // Seq-only effects - if (scope === 'seq') { - IE_ACT_TYPES_SEQ.forEach(function(at) { - if (!actionObj || !Object.prototype.hasOwnProperty.call(actionObj, at.key)) return; - var val = actionObj[at.key]; - if (val === undefined || val === null) return; - hasAny = true; - - 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 + ',\'' + escAttr(at.key) + '\')">X</button>'; - 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) + '" 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 + ',\'' + escAttr(at.key) + '\')">X</button>'; - rows += '</div>'; - } - }); + if (hasAny) { + return '<div class="ie-act-root">' + rows + '</div>'; } + return ''; +} + +// ── Render messages sub-list (plain strings) ── - // Add effect buttons - var addbar = '<div class="ie-act-addbar">'; - if (!msgsPresent) { - addbar += '<button class="btn btn-sm ie-add-btn" onclick="ie_addActEffect(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'messages\',\'' + escAttr(scope) + '\')">+ Add Message</button>'; +function ie_renderMessages(step, secId, idx, si, secPath) { + var present = step && Array.isArray(step.messages); + if (!present) { + return '<button class="btn btn-sm ie-add-btn" onclick="ie_addMessages(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ')">+ Add Message</button>'; } - IE_ACT_TYPES.forEach(function(at) { - if (actionObj && Object.prototype.hasOwnProperty.call(actionObj, at.key)) { - if (at.kind === 'checkbox') { - if (actionObj[at.key] === true) return; - } else { - return; + var msgs = step.messages; + var rmFn = 'ie_removeMessage(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ')'; + var h = '<div class="ie-act-kv" data-ie-act-key="messages">'; + h += '<div class="ie-act-kv-header"><span>Messages</span></div>'; + 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>' + : ''; + 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>' + : ''; + 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 += '</div>'; + return h; +} + +// ── Render steps list of a trigger entry ── + +function ie_renderStepList(steps, secId, idx, secPath) { + steps = steps || []; + var h = '<div class="ie-step-list" data-idx="' + idx + '">'; + steps.forEach(function(step, si) { + var stepCond = step && step.condition && typeof step.condition === 'object'; + h += '<div class="ie-step-row" data-step="' + si + '" data-idx="' + idx + '">'; + h += '<div class="ie-step-header">'; + 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>'; + 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>'; + h += '</div>'; + + h += '<div class="ie-step-cond-panel">'; + if (stepCond) { + h += ie_renderCond(step.condition || null, secId, idx, secPath, 'step-' + si); + } else { + h += '<button class="btn btn-sm ie-add-btn" onclick="ie_addStepCond(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ')">+ Add Step Condition</button>'; + } + h += '</div>'; + + h += '<div class="ie-step-effects">' + ie_renderAct(step, secId, idx, si, secPath) + '</div>'; + h += '<div class="ie-step-addbar">' + ie_renderStepAddbar(step, secId, idx, si, secPath) + '</div>'; + h += '<div class="ie-step-messages">' + ie_renderMessages(step, secId, idx, si, secPath) + '</div>'; + + h += '</div>'; + }); + h += '<div class="ie-step-addbar-bottom">' + ie_renderBottomAddbar(secId, secPath, idx) + '</div>'; + h += '</div>'; + return h; +} + +// ── Collect effects (everything except messages and condition) ── + +function ie_collectAct(container) { + if (!container) return null; + var r = {}; + + container.querySelectorAll('.ie-act-kv').forEach(function(kvEl) { + var key = kvEl.getAttribute('data-ie-act-key'); + if (!key) return; + if (key === 'messages') return; + var map = {}; + kvEl.querySelectorAll('.ie-kv-row').forEach(function(row) { + var kEl = row.querySelector('.ie-kv-key'); + var vEl = row.querySelector('.ie-kv-val'); + if (kEl && kEl.value.trim() && vEl) { + var v = vEl.value.trim(); + var num = parseFloat(v); + map[kEl.value.trim()] = v === 'true' ? true : (v === 'false' ? false : (!isNaN(num) ? num : v)); } + }); + r[key] = map; + }); + + container.querySelectorAll('.ie-act-input').forEach(function(el) { + var key = el.getAttribute('data-ie-act-key'); + if (!key) return; + var kind = el.getAttribute('data-ie-act-kind'); + if (kind === 'checkbox') { + r[key] = el.checked; + } else if (kind === 'number') { + var n = parseFloat(el.value); + if (isNaN(n)) return; + r[key] = n; + } else { + r[key] = el.value.trim(); } - addbar += '<button class="btn btn-sm ie-add-btn" onclick="ie_addActEffect(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(at.key) + '\',\'' + escAttr(scope) + '\')">+ ' + esc(at.label) + '</button>'; }); - if (scope === 'seq') { - IE_ACT_TYPES_SEQ.forEach(function(at) { - if (actionObj && Object.prototype.hasOwnProperty.call(actionObj, at.key)) return; - addbar += '<button class="btn btn-sm ie-add-btn" onclick="ie_addActEffect(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(at.key) + '\',\'' + escAttr(scope) + '\')">+ ' + esc(at.label) + '</button>'; + + if (Object.keys(r).length === 0) return null; + return r; +} + +// ── Collect messages (plain strings) ── + +function ie_collectMessages(container) { + if (!container) return null; + var msgsKv = container.querySelector('.ie-act-kv[data-ie-act-key="messages"]'); + if (!msgsKv) return null; + var list = []; + msgsKv.querySelectorAll('.ie-kv-row .ie-act-msg').forEach(function(el) { + list.push(el.value.trim()); + }); + return list; +} + +// ── Sync a single step's DOM back into data ── + +function ie_syncStep(ed, secId, secPath, idx, si) { + var card = document.querySelector('.obj-card[data-card="' + secId + '"]'); + if (!card) return; + var stepRow = card.querySelector('.obj-sub-row.ie-inter-row[data-idx="' + idx + '"] .ie-step-row[data-step="' + si + '"]'); + if (!stepRow) return; + if (!ed[secPath] || !ed[secPath][idx] || !ed[secPath][idx].steps || !ed[secPath][idx].steps[si]) return; + var step = ed[secPath][idx].steps[si]; + + var condRoot = stepRow.querySelector(':scope > .ie-step-cond-panel .ie-cond-root'); + if (condRoot) { + var cond = ie_collectCond(condRoot); + if (cond) step.condition = cond; else delete step.condition; + } + + var effContainer = stepRow.querySelector(':scope > .ie-step-effects'); + if (effContainer) { + var act = ie_collectAct(effContainer); + if (act) { + Object.keys(act).forEach(function(k) { step[k] = act[k]; }); + } + // Drop keys the user X'd out (no longer present in the DOM among the rendered IE_ACT_TYPES set). + IE_ACT_TYPES.forEach(function(at) { + if (at.key === 'messages') return; + if (!Object.prototype.hasOwnProperty.call(act || {}, at.key)) { + if (Object.prototype.hasOwnProperty.call(step, at.key)) delete step[at.key]; + } }); } - addbar += '</div>'; - // Only wrap the content + addbar in .ie-act-root when there's actual content. - // An empty action just emits the addbar (mirroring ie_renderCond, which - // emits an unwrapped addbar for an empty condition). This keeps the empty - // condition and empty action panels visually symmetric — one box, not two. - if (hasAny) { - return '<div class="ie-act-root">' + rows + addbar + '</div>'; + var msgsContainer = stepRow.querySelector(':scope > .ie-step-messages'); + if (msgsContainer) { + var list = ie_collectMessages(msgsContainer); + if (list !== null) step.messages = list; + else delete step.messages; + if (Array.isArray(step.messages) && step.messages.length === 0) { + // keep empty array — survives next render, ced_cleanOutput will strip + // it on save. + } } - return addbar; +} + +// ── Sync all interaction DOM back to data ── + +function ie_syncAllInteractions(data, secs) { + if (!data || !secs) return; + secs.forEach(function(sec) { + if (!sec.interactionStyle || !sec.isArray) return; + var p = sec.path; + if (!data[p]) return; + var card = document.querySelector('.obj-card[data-card="' + sec.id + '"]'); + if (!card) return; + var rows = card.querySelectorAll('.obj-sub-row.ie-inter-row'); + rows.forEach(function(row, idx) { + if (!data[p][idx]) data[p][idx] = {}; + var lockCb = row.querySelector(':scope > .ie-inter-header .ie-lock-cb'); + if (lockCb) { + if (lockCb.checked) data[p][idx].lock = true; + else delete data[p][idx].lock; + } + var entryCondRoot = row.querySelector(':scope > .ie-inter-panel .ie-cond-root'); + if (entryCondRoot) { + var cond = ie_collectCond(entryCondRoot); + if (cond) data[p][idx].condition = cond; + else delete data[p][idx].condition; + } + if (!data[p][idx].steps) data[p][idx].steps = []; + var stepRows = row.querySelectorAll(':scope > .ie-inter-steps .ie-step-row'); + stepRows.forEach(function(stepRow) { + var si = parseInt(stepRow.getAttribute('data-step'), 10); + if (isNaN(si)) return; + ie_syncStep(data, sec.id, p, idx, si); + }); + }); + }); } // ── Add/Remove/Toggle: Condition ── -function ie_addLeaf(secId, secPath, idx, groupPath, leafType) { +function ie_addLeaf(secId, secPath, idx, kind, groupPath, leafType) { var ed = window._editorData; if (!ed) return; - ie_syncCondEntry(ed, secId, secPath, idx); - var cond = (ed[secPath] && ed[secPath][idx]) ? ed[secPath][idx].condition : null; + _ie_syncCond(ed, secId, secPath, idx, kind); + var cond = _ie_getCond(ed, secPath, idx, kind); var group = ie_condAt(cond, groupPath); var leaf = {}; - leaf[leafType] = leafType === 'min_credits' ? 0 : ''; + leaf[leafType] = (leafType === 'min_credits' || leafType === 'room') ? 0 : ''; var newGroup; if (!group) { @@ -599,28 +734,21 @@ function ie_addLeaf(secId, secPath, idx, groupPath, leafType) { } var newCond = ie_condReplace(cond, groupPath, newGroup); - if (!ed[secPath][idx]) ed[secPath][idx] = {}; - if (newCond) ed[secPath][idx].condition = newCond; - else delete ed[secPath][idx].condition; + _ie_setCond(ed, secPath, idx, kind, newCond); ced_syncDOMToData(ed); window._ieRenderOnly(); } -function ie_removeCond(secId, secPath, idx, groupPath, childIdx) { +function ie_removeCond(secId, secPath, idx, kind, groupPath, childIdx) { var ed = window._editorData; if (!ed) return; - ie_syncCondEntry(ed, secId, secPath, idx); - var cond = (ed[secPath] && ed[secPath][idx]) ? ed[secPath][idx].condition : null; + _ie_syncCond(ed, secId, secPath, idx, kind); + var cond = _ie_getCond(ed, secPath, idx, kind); - // Removing the single root leaf. Note: children of a *root-level group* - // are also rendered with groupPath='' (and a real child index), so the - // test must be childIdx, not groupPath, otherwise removing any leaf from - // a root group wipes the entire condition. if (childIdx === IE_ROOT_LEAF) { - if (!ed[secPath][idx]) ed[secPath][idx] = {}; - delete ed[secPath][idx].condition; + _ie_setCond(ed, secPath, idx, kind, null); ced_syncDOMToData(ed); - window._ieRenderOnly(); + window._ieRenderOnly(); return; } @@ -635,8 +763,6 @@ function ie_removeCond(secId, secPath, idx, groupPath, childIdx) { if (children.length === 0) { newGroup = null; } else if (children.length === 1 && !group.not) { - // Collapse a single-child non-NOT group to the child itself so removal - // doesn't leave a one-child all_of/any_of wrapper in the live tree. newGroup = children[0]; } else { newGroup = {}; @@ -644,36 +770,28 @@ function ie_removeCond(secId, secPath, idx, groupPath, childIdx) { newGroup[mode] = children; } var newCond = ie_condReplace(cond, groupPath, newGroup); - if (!ed[secPath][idx]) ed[secPath][idx] = {}; - if (newCond) ed[secPath][idx].condition = newCond; - else delete ed[secPath][idx].condition; + _ie_setCond(ed, secPath, idx, kind, newCond); } else { var nc = ie_condReplace(cond, groupPath, null); - if (!ed[secPath][idx]) ed[secPath][idx] = {}; - if (nc) ed[secPath][idx].condition = nc; - else delete ed[secPath][idx].condition; + _ie_setCond(ed, secPath, idx, kind, nc); } ced_syncDOMToData(ed); window._ieRenderOnly(); } -function ie_removeGroup(secId, secPath, idx, groupPath) { +function ie_removeGroup(secId, secPath, idx, kind, groupPath) { var ed = window._editorData; if (!ed) return; - ie_syncCondEntry(ed, secId, secPath, idx); - if (!ed[secPath] || !ed[secPath][idx]) return; - var cond = ed[secPath][idx].condition; + _ie_syncCond(ed, secId, secPath, idx, kind); + var cond = _ie_getCond(ed, secPath, idx, kind); - // Root group: removing the entire condition. if (!groupPath) { - delete ed[secPath][idx].condition; + _ie_setCond(ed, secPath, idx, kind, null); ced_syncDOMToData(ed); - window._ieRenderOnly(); + window._ieRenderOnly(); return; } - // Non-root group: gpath is the group's own path. Parent path = gpath minus - // last segment; childIdx = last segment. var parts = groupPath.split('-').map(Number); var childIdx = parts[parts.length - 1]; var parentPath = parts.slice(0, -1).join('-'); @@ -696,41 +814,41 @@ function ie_removeGroup(secId, secPath, idx, groupPath) { } var newCond = ie_condReplace(cond, parentPath, newParent); - if (newCond) ed[secPath][idx].condition = newCond; - else delete ed[secPath][idx].condition; + _ie_setCond(ed, secPath, idx, kind, newCond); ced_syncDOMToData(ed); window._ieRenderOnly(); } -function ie_addRootConditionGroup(secPath, idx) { +function ie_addRootConditionGroup(secPath, idx, kind) { var ed = window._editorData; if (!ed) return; if (!ed[secPath]) ed[secPath] = []; if (!ed[secPath][idx]) ed[secPath][idx] = {}; - if (!ed[secPath][idx].condition || typeof ed[secPath][idx].condition !== 'object') { - ed[secPath][idx].condition = {all_of: []}; + var si = _ie_kindStep(kind); + if (si < 0) { + if (!ed[secPath][idx].condition || typeof ed[secPath][idx].condition !== 'object') { + ed[secPath][idx].condition = {all_of: []}; + } + } else { + if (!ed[secPath][idx].steps) ed[secPath][idx].steps = []; + if (!ed[secPath][idx].steps[si]) ed[secPath][idx].steps[si] = {}; + if (!ed[secPath][idx].steps[si].condition || typeof ed[secPath][idx].steps[si].condition !== 'object') { + ed[secPath][idx].steps[si].condition = {all_of: []}; + } } ced_syncDOMToData(ed); window._ieRenderOnly(); } -function ie_addEffectToInteraction(secPath, idx) { - var ed = window._editorData; - if (!ed) return; - if (!ed[secPath]) ed[secPath] = []; - if (!ed[secPath][idx]) ed[secPath][idx] = {}; - if (!ed[secPath][idx].action || typeof ed[secPath][idx].action !== 'object') { - ed[secPath][idx].action = {}; - } - ced_syncDOMToData(ed); - window._ieRenderOnly(); +function ie_addStepCond(secId, secPath, idx, si) { + ie_addRootConditionGroup(secPath, idx, 'step-' + si); } -function ie_addGroup(secId, secPath, idx, groupPath) { +function ie_addGroup(secId, secPath, idx, kind, groupPath) { var ed = window._editorData; if (!ed) return; - ie_syncCondEntry(ed, secId, secPath, idx); - var cond = (ed[secPath] && ed[secPath][idx]) ? ed[secPath][idx].condition : null; + _ie_syncCond(ed, secId, secPath, idx, kind); + var cond = _ie_getCond(ed, secPath, idx, kind); var group = ie_condAt(cond, groupPath); var newSub = {all_of: []}; @@ -749,17 +867,16 @@ function ie_addGroup(secId, secPath, idx, groupPath) { } var newCond = ie_condReplace(cond, groupPath, newGroup); - if (!ed[secPath][idx]) ed[secPath][idx] = {}; - ed[secPath][idx].condition = newCond; + _ie_setCond(ed, secPath, idx, kind, newCond); ced_syncDOMToData(ed); window._ieRenderOnly(); } -function ie_toggleCondMode(secId, secPath, idx, groupPath) { +function ie_toggleCondMode(secId, secPath, idx, kind, groupPath) { var ed = window._editorData; if (!ed) return; - ie_syncCondEntry(ed, secId, secPath, idx); - var cond = (ed[secPath] && ed[secPath][idx]) ? ed[secPath][idx].condition : null; + _ie_syncCond(ed, secId, secPath, idx, kind); + var cond = _ie_getCond(ed, secPath, idx, kind); var group = ie_condAt(cond, groupPath); if (!group || !group.all_of && !group.any_of) return; @@ -770,291 +887,257 @@ function ie_toggleCondMode(secId, secPath, idx, groupPath) { newGroup[oldMode === 'all_of' ? 'any_of' : 'all_of'] = children; var newCond = ie_condReplace(cond, groupPath, newGroup); - ed[secPath][idx].condition = newCond; + _ie_setCond(ed, secPath, idx, kind, newCond); ced_syncDOMToData(ed); window._ieRenderOnly(); } -function ie_toggleCondNot(secId, secPath, idx, groupPath) { +function ie_toggleCondNot(secId, secPath, idx, kind, groupPath) { + ced_syncDOMToData(window._editorData); + window._ieRenderOnly(); +} + +function ie_toggleNotLeaf(secId, secPath, idx, kind, groupPath, ci) { + ced_syncDOMToData(window._editorData); + window._ieRenderOnly(); +} + +// ── Add/Remove: Steps ── + +function ie_removeStep(secId, secPath, idx, si) { var ed = window._editorData; if (!ed) return; - ie_syncCondEntry(ed, secId, secPath, idx); ced_syncDOMToData(ed); - window._ieRenderOnly(); + if (window._ieSyncAll) window._ieSyncAll(ed); + if (!ed[secPath] || !ed[secPath][idx] || !ed[secPath][idx].steps) return; + ed[secPath][idx].steps.splice(si, 1); + window._ieRenderCurrent(); } -function ie_toggleNotLeaf(secId, secPath, idx, groupPath, ci) { +function ie_moveStep(secId, secPath, idx, fromSi, toSi) { var ed = window._editorData; if (!ed) return; - ie_syncCondEntry(ed, secId, secPath, idx); ced_syncDOMToData(ed); + if (window._ieSyncAll) window._ieSyncAll(ed); + var steps = ed[secPath] && ed[secPath][idx] && ed[secPath][idx].steps; + if (!steps || fromSi < 0 || fromSi >= steps.length || toSi < 0 || toSi >= steps.length) return; + var m = steps.splice(fromSi, 1)[0]; + steps.splice(toSi, 0, m); + window._ieRenderCurrent(); +} + +function ie_moveRow(secPath, fromIdx, toIdx) { + var ed = window._editorData; + if (!ed) return; + ced_syncDOMToData(ed); + if (window._ieSyncAll) window._ieSyncAll(ed); + var arr = ed[secPath]; + if (!arr || fromIdx < 0 || fromIdx >= arr.length || toIdx < 0 || toIdx >= arr.length) return; + var m = arr.splice(fromIdx, 1)[0]; + arr.splice(toIdx, 0, m); + window._ieRenderCurrent(); +} + +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]; + 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(); } -// ── Add/Remove: Action ── +// ── Add/Remove: Action effects ── -function ie_addActEffect(secId, secPath, idx, effectKey, scope) { +function ie_addActEffect(secId, secPath, idx, si, effectKey) { var ed = window._editorData; if (!ed) return; - ie_syncActEntry(ed, secId, secPath, idx); + ie_syncStep(ed, secId, secPath, idx, si); if (!ed[secPath]) ed[secPath] = []; if (!ed[secPath][idx]) ed[secPath][idx] = {}; - if (!ed[secPath][idx].action) ed[secPath][idx].action = {}; - - var act = ed[secPath][idx].action; - if (effectKey === 'messages') { - act.messages = []; + 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]; + + 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 { - var at = IE_ACT_TYPES.find(function(a) { return a.key === effectKey; }); - var at2 = IE_ACT_TYPES_SEQ.find(function(a) { return a.key === effectKey; }); - var info = at || at2; - if (!info) return; - if (info.kind === 'kv') { - act[effectKey] = {}; - } else if (info.kind === 'checkbox') { - act[effectKey] = true; - } else if (info.kind === 'number') { - act[effectKey] = 0; - } else { - act[effectKey] = ''; - } + step[effectKey] = ''; } ced_syncDOMToData(ed); window._ieRenderOnly(); } -function ie_removeActEffect(secId, secPath, idx, effectKey) { +function ie_removeActEffect(secId, secPath, idx, si, effectKey) { var ed = window._editorData; if (!ed) return; - ie_syncActEntry(ed, secId, secPath, idx); - if (!ed[secPath] || !ed[secPath][idx] || !ed[secPath][idx].action) return; - delete ed[secPath][idx].action[effectKey]; - if (Object.keys(ed[secPath][idx].action).length === 0) { - delete ed[secPath][idx].action; - } + 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; + delete step[effectKey]; ced_syncDOMToData(ed); window._ieRenderOnly(); } -// ── Add/Remove: KV action row ── -// -// KV rows are added/removed through these handlers rather than inline DOM -// manipulation so removal can drop the whole effect when the last row goes. -// "+ Add" appends a blank row to the DOM (no data change yet — it joins the -// map on the next sync, the same way typing into an existing row does). -// Removal deletes the row's key from the data map; if the map empties out the -// whole action key is dropped so the section "disappears when all flags are -// removed" (per the spec) and re-renders with a single starter row only when -// the user clicks the addbar "+ Set ... Flags" again. -function ie_addKVRow(secId, secPath, idx, effectKey) { +// ── Add/Remove: KV row ── + +function ie_addKVRow(secId, secPath, idx, si, effectKey) { var ed = window._editorData; if (!ed) return; - // Sync first so any in-progress typing in the existing rows is captured. - ie_syncActEntry(ed, secId, secPath, idx); - // Append a blank row directly to the DOM (no data change yet). + ie_syncStep(ed, secId, secPath, idx, si); var card = document.querySelector('.obj-card[data-card="' + secId + '"]'); if (!card) return; - var rows = card.querySelectorAll('.obj-sub-row'); - var row = rows[idx]; - if (!row) return; - var list = row.querySelector('.ie-act-kv[data-ie-act-key="' + effectKey + '"] .ie-kv-list'); + var stepRow = card.querySelector('.obj-sub-row.ie-inter-row[data-idx="' + idx + '"] .ie-step-row[data-step="' + si + '"]'); + if (!stepRow) return; + var list = stepRow.querySelector('.ie-act-kv[data-ie-act-key="' + effectKey + '"] .ie-kv-list'); if (!list) return; var rowEl = document.createElement('div'); rowEl.className = 'ie-kv-row'; - rowEl.innerHTML = '<input value="" placeholder="key" class="ie-kv-key"><input value="" placeholder="val" class="ie-kv-val"><button class="btn btn-sm btn-danger" onclick="ie_removeKVRow(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(effectKey) + '\')">X</button>'; + rowEl.innerHTML = '<input value="" placeholder="key" class="ie-kv-key"><input value="" placeholder="val" class="ie-kv-val"><button class="btn btn-sm btn-danger" onclick="ie_removeKVRow(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',\'' + escAttr(effectKey) + '\')">X</button>'; list.appendChild(rowEl); rowEl.querySelector('.ie-kv-key').focus(); } -function ie_removeKVRow(rowEl, secId, secPath, idx, effectKey) { +function ie_removeKVRow(rowEl, secId, secPath, idx, si, effectKey) { var ed = window._editorData; if (!ed) return; - // rowEl may be the X button itself (callers use `this` in inline onclick) — - // walk up to the actual .ie-kv-row so the key input below is found. if (rowEl && rowEl.closest) { var kvRow = rowEl.closest('.ie-kv-row'); if (kvRow) rowEl = kvRow; } - ie_syncActEntry(ed, secId, secPath, idx); - if (!ed[secPath] || !ed[secPath][idx] || !ed[secPath][idx].action) return; - var act = ed[secPath][idx].action; - if (!act.hasOwnProperty(effectKey)) 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 || !step.hasOwnProperty(effectKey)) return; var keyEl = rowEl.querySelector('.ie-kv-key'); var k = keyEl ? keyEl.value.trim() : ''; - if (k && act[effectKey].hasOwnProperty(k)) delete act[effectKey][k]; - // Empty-key rows are not in the data map at all; nothing to delete there. - if (Object.keys(act[effectKey]).length === 0) { - delete act[effectKey]; - if (Object.keys(act).length === 0) delete ed[secPath][idx].action; + 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(); } -// ── Add/Remove: Messages list row ── +// ── 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; + ed[secPath][idx].steps[si].messages = ['']; + ced_syncDOMToData(ed); + window._ieRenderOnly(); +} -// ie_addMessage appends a blank delay+message row to the DOM (no data change -// yet — it joins the list on the next sync, the same way typing into an -// existing row does). The Messages section must already be present (created -// via + Add Message in the addbar). Focuses the new row's message input. -function ie_addMessage(secId, secPath, idx) { +function ie_addMessage(secId, secPath, idx, si) { var ed = window._editorData; if (!ed) return; - ie_syncActEntry(ed, secId, secPath, idx); + ie_syncStep(ed, secId, secPath, idx, si); var card = document.querySelector('.obj-card[data-card="' + secId + '"]'); if (!card) return; - var rows = card.querySelectorAll('.obj-sub-row'); - var row = rows[idx]; - if (!row) return; - var list = row.querySelector('.ie-act-kv[data-ie-act-key="messages"] .ie-kv-list'); + var stepRow = card.querySelector('.obj-sub-row.ie-inter-row[data-idx="' + idx + '"] .ie-step-row[data-step="' + si + '"]'); + if (!stepRow) return; + var list = stepRow.querySelector('.ie-act-kv[data-ie-act-key="messages"] .ie-kv-list'); if (!list) return; var rowEl = document.createElement('div'); - rowEl.className = 'ie-kv-row'; - var rmFn = 'ie_removeMessage(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ')'; - rowEl.innerHTML = '<input type="number" class="ie-act-msg-delay" value="" placeholder="delay" style="width:60px" title="Ticks before showing"><input class="ie-act-msg" value="" placeholder="Shown to player" style="flex:1"><button class="btn btn-sm btn-danger" onclick="' + rmFn + '">X</button>'; + rowEl.className = 'ie-kv-row ie-msg-row'; + var rmFn = 'ie_removeMessage(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ')'; + rowEl.innerHTML = '<input class="ie-act-msg" value="" placeholder="Shown to player" style="flex:1"><button class="btn btn-sm btn-danger" onclick="' + rmFn + '">X</button>'; list.appendChild(rowEl); rowEl.querySelector('.ie-act-msg').focus(); } -// ie_removeMessage removes a message entry from the action's messages list. If -// the list becomes empty the entire messages key is dropped (and the section -// disappears on re-render). rowEl may be the X button itself. -function ie_removeMessage(rowEl, secId, secPath, idx) { +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_syncActEntry(ed, secId, secPath, idx); - if (!ed[secPath] || !ed[secPath][idx] || !ed[secPath][idx].action) return; - var act = ed[secPath][idx].action; - if (!act.hasOwnProperty('messages') || !Array.isArray(act.messages)) 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 || !Array.isArray(step.messages)) return; - // Find which row index to remove by locating our row in the list. var card = document.querySelector('.obj-card[data-card="' + secId + '"]'); - var subRows = card ? card.querySelectorAll('.obj-sub-row') : []; - var row = subRows[idx]; - if (row) { - var kvRows = row.querySelectorAll('.ie-act-kv[data-ie-act-key="messages"] .ie-kv-row'); + 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)) { - act.messages.splice(ri, 1); + step.messages.splice(ri, 1); break; } } } - if (act.messages.length === 0) { - delete act.messages; - if (Object.keys(act).length === 0) delete ed[secPath][idx].action; + if (step.messages.length === 0) { + delete step.messages; } ced_syncDOMToData(ed); window._ieRenderOnly(); } -// ── Pre-render sync helpers ── +// ── Shared trigger-style array-section renderer ── -function ie_syncCondEntry(ed, secId, secPath, idx) { - var card = document.querySelector('.obj-card[data-card="' + secId + '"]'); - if (!card) return; - var rows = card.querySelectorAll('.obj-sub-row'); - var row = rows[idx]; - if (!row) return; - var condRoot = row.querySelector('.ie-cond-root'); - if (!condRoot) return; - var cond = ie_collectCond(condRoot); - if (!ed[secPath]) ed[secPath] = []; - if (!ed[secPath][idx]) ed[secPath][idx] = {}; - if (cond) ed[secPath][idx].condition = cond; - else delete ed[secPath][idx].condition; -} - -function ie_syncActEntry(ed, secId, secPath, idx) { - var card = document.querySelector('.obj-card[data-card="' + secId + '"]'); - if (!card) return; - var rows = card.querySelectorAll('.obj-sub-row'); - var row = rows[idx]; - if (!row) return; - var actRoot = row.querySelector('.ie-act-root'); - if (!actRoot) return; - var act = ie_collectAct(actRoot); - if (!ed[secPath]) ed[secPath] = []; - if (!ed[secPath][idx]) ed[secPath][idx] = {}; - if (act) { - // Preserve any action keys that aren't rendered in the DOM (e.g. legacy - // seq-only effects on an inline section, or unknown future keys). Without - // this, the first sync after any edit would silently drop them. - var existing = (ed[secPath][idx].action && typeof ed[secPath][idx].action === 'object') ? ed[secPath][idx].action : null; - if (existing) { - Object.keys(existing).forEach(function(k) { - if (!Object.prototype.hasOwnProperty.call(act, k)) act[k] = existing[k]; - }); - } - ed[secPath][idx].action = act; - } else { - delete ed[secPath][idx].action; - } -} - -// ── Shared interaction-style array-section renderer ── -// -// Replaces the duplicated `renderArraySection` (objecteditor.js) and -// `renderMobArraySection` (mobeditor.js). -// -// ctx: { -// sec: section def (.id, .path, .isArray, .interactionStyle, .interScope, .fields, .label), -// data: editor's data root, -// visMap: per-editor vis object (e.g. window._interVis[sec.id]), -// showRemove(idx) => onclick string for "Remove Interaction", -// showField(secId, idx, key) => onclick string for "+ Add Required Item/...", -// hideField(secId, idx, key) => onclick string for the panel "X", -// renderField(sec, f, data, idx, optStyle) => HTML for the item_id field, -// onAdd() => onclick string for the bottom "+ Add ...", -// } function ie_renderArraySection(ctx) { var sec = ctx.sec; var data = ctx.data; var visMap = ctx.visMap; var arr = data[sec.path] || []; var h = ''; + var allowItem = sec.itemIDAllowed !== false; arr.forEach(function(item, idx) { visMap[idx] = visMap[idx] || {}; var vis = visMap[idx]; - var showItem = vis.item_id || (item.item_id && item.item_id !== ''); + var showItem = allowItem && (vis.item_id || (item.item_id && item.item_id !== '')); var condExists = !!(item.condition && typeof item.condition === 'object'); - var actExists = !!(item.action && typeof item.action === 'object'); + var lockVal = !!item.lock; + var steps = item.steps || []; h += '<div class="obj-sub-row ie-inter-row" data-idx="' + idx + '">'; h += '<div class="ie-inter-header">'; - if (!showItem) { - h += '<button class="btn btn-sm ie-add-btn" onclick="' + ctx.showField(sec.id, idx, 'item_id') + '">+ Add Required Item</button>'; - } else { - h += ctx.renderField(sec, sec.fields[0], data, idx, 'flex:1;min-width:0'); - h += '<button class="btn btn-sm btn-danger ie-inter-field-x" onclick="' + ctx.hideField(sec.id, idx, 'item_id') + '" title="Remove required item">X</button>'; + h += '<label class="ie-lock-lbl"><input type="checkbox" class="ie-lock-cb"' + (lockVal ? ' checked' : '') + '> Lock Player</label>'; + if (idx > 0) h += '<button class="btn btn-sm ie-trig-up" onclick="ie_moveRow(\'' + escAttr(sec.path) + '\',' + idx + ',' + (idx - 1) + ')" title="Move up">▲</button>'; + if (idx < arr.length - 1) h += '<button class="btn btn-sm ie-trig-dn" onclick="ie_moveRow(\'' + escAttr(sec.path) + '\',' + idx + ',' + (idx + 1) + ')" title="Move down">▼</button>'; + + if (allowItem) { + if (!showItem) { + h += '<button class="btn btn-sm ie-add-btn" onclick="' + ctx.showField(sec.id, idx, 'item_id') + '">+ Add Required Item</button>'; + } else { + h += ctx.renderField(sec, sec.fields[0], data, idx, 'flex:1;min-width:0'); + h += '<button class="btn btn-sm btn-danger ie-inter-field-x" onclick="' + ctx.hideField(sec.id, idx, 'item_id') + '" title="Remove required item">X</button>'; + } } if (!condExists) { - h += '<button class="btn btn-sm ie-add-btn" onclick="' + ctx.addCondition(sec.id, idx) + '">+ Add Condition</button>'; - } - if (!actExists) { - h += '<button class="btn btn-sm ie-add-btn" onclick="' + ctx.addEffect(sec.id, idx) + '">+ Add Action</button>'; + h += '<button class="btn btn-sm ie-add-btn" onclick="ie_addRootConditionGroup(\'' + escAttr(sec.path) + '\',' + idx + ',\'entry\')">+ Add Condition</button>'; } h += '<span class="ie-inter-header-spacer"></span>'; - h += '<button class="btn btn-sm btn-danger ie-inter-remove" onclick="' + ctx.showRemove(idx) + '">Remove Interaction</button>'; + h += '<button class="btn btn-sm btn-danger ie-inter-remove" onclick="' + ctx.showRemove(idx) + '">Remove Trigger</button>'; h += '</div>'; h += '<div class="ie-inter-panel" style="display:' + (condExists ? 'block' : 'none') + '">'; - h += '<div class="ie-inter-panel-body">' + ie_renderCond(item.condition || null, sec.id, idx, sec.path) + '</div>'; + h += '<div class="ie-inter-panel-body">' + ie_renderCond(item.condition || null, sec.id, idx, sec.path, 'entry') + '</div>'; h += '</div>'; - h += '<div class="ie-inter-panel" style="display:' + (actExists ? 'block' : 'none') + '">'; - h += '<div class="ie-inter-panel-body">' + ie_renderAct(item.action || null, sec.interScope || 'inline', sec.id, idx, sec.path) + '</div>'; + h += '<div class="ie-inter-steps">'; + h += ie_renderStepList(steps, sec.id, idx, sec.path); h += '</div>'; h += '</div>'; }); - h += '<button class="btn btn-sm obj-sub-add" onclick="' + ctx.onAdd() + '">+ Add ' + esc(sec.label) + '</button>'; + h += '<button class="btn btn-sm obj-sub-add" onclick="' + ctx.onAdd() + '">+ Add ' + esc(sec.label || 'Trigger') + '</button>'; return h; } @@ -1072,12 +1155,37 @@ function ie_hideField(visMap, secId, idx, key) { visMap[secId][idx][key] = false; } +// ── Step-cleaning helper used by save paths ── + +function ie_cleanStep(step) { + var s = {}; + if (step && step.condition) { + var c = ie_cleanCondition(step.condition); + if (c) s.condition = c; + } + if (step && step.wait !== undefined && step.wait !== null && step.wait !== 0) s.wait = step.wait; + if (step && Array.isArray(step.messages) && step.messages.filter(function(m){return m;}).length > 0) { + s.messages = step.messages.filter(function(m){return m;}); + } + IE_ACT_TYPES.forEach(function(at) { + if (at.key === 'wait' || at.key === 'messages') return; + if (!step || !Object.prototype.hasOwnProperty.call(step, at.key)) return; + var v = step[at.key]; + if (v === undefined || v === null || v === '') return; + if (typeof v === 'object') { + if (Object.keys(v).length === 0) return; + s[at.key] = v; + } else if (typeof v === 'boolean') { + if (!v) return; + s[at.key] = v; + } else { + s[at.key] = v; + } + }); + return s; +} + // ── Bindings (set by each editor) ── -// _ieRenderCurrent: full sync + render. Used by chrome toggles -// (show/hide panels, add row, remove row, add section, etc.). -// _ieRenderOnly: render only, no sync. Used by ie_* mutation handlers -// that have already synced + mutated the data and must not re-read the -// stale DOM before the next render. window._ieRenderCurrent = function() {}; -window._ieRenderOnly = function() {}; +window._ieRenderOnly = function() {};
\ No newline at end of file diff --git a/internal/admin/static/map.js b/internal/admin/static/map.js index 00230a4..ab20a86 100644 --- a/internal/admin/static/map.js +++ b/internal/admin/static/map.js @@ -575,6 +575,8 @@ function renderSidePanel(data) { html += '<button class="panel-tab' + (currentPanelTab === 5 ? ' active' : '') + '" onclick="switchPanelTab(5)">Items</button>'; html += '<button class="panel-tab' + (currentPanelTab === 6 ? ' active' : '') + '" onclick="switchPanelTab(6)">Hazard</button>'; html += '<button class="panel-tab' + (currentPanelTab === 7 ? ' active' : '') + '" onclick="switchPanelTab(7)">Agility</button>'; + html += '<button class="panel-tab' + (currentPanelTab === 8 ? ' active' : '') + '" onclick="switchPanelTab(8)">Enter/Exit</button>'; + html += '<button class="panel-tab' + (currentPanelTab === 9 ? ' active' : '') + '" onclick="switchPanelTab(9)">Triggers</button>'; html += '</div>'; html += '<div class="panel-tab-content">'; @@ -595,6 +597,8 @@ function renderSidePanel(data) { html += renderCourseTab(); } } + else if (currentPanelTab === 8) html += renderEnterExitTab(r); + else if (currentPanelTab === 9) html += renderTriggersTab(r); html += '</div>'; panel.innerHTML = html; @@ -1401,6 +1405,8 @@ function switchPanelTab(idx) { html = renderCourseTab(); } } + else if (idx === 8) html = renderEnterExitTab(r); + else if (idx === 9) html = renderTriggersTab(r); var content = document.querySelector('.panel-tab-content'); if (content) content.innerHTML = html; var tabs = document.querySelectorAll('.panel-tab'); @@ -2096,6 +2102,10 @@ async function doSavePanel() { r.mobs = currentRoomData.room.mobs; r.exits = currentRoomData.room.exits; r.hazard = currentRoomData.room.hazard || ''; + r.on_enter = currentRoomData.room.on_enter; + r.on_exit = currentRoomData.room.on_exit; + r.on_flag_change = currentRoomData.room.on_flag_change; + r.on_global_flag_change = currentRoomData.room.on_global_flag_change; if (r.block_transport === undefined) r.block_transport = currentRoomData.room.block_transport; } @@ -3350,3 +3360,222 @@ document.addEventListener('DOMContentLoaded', function() { loadRoomDirs(function window.addEventListener('keydown', function(e) { if (isCtrlKey(e)) { ctrlHeld = true; if (dragging) refreshDragMode(true); } }); window.addEventListener('keyup', function(e) { if (isCtrlKey(e)) { ctrlHeld = false; if (dragging) refreshDragMode(false); } }); }); + +// ── Enter/Exit tab ── +// Renders on_enter and on_exit trigger lists, wiring the shared ie_* helpers +// from intereditor.js so the same condition/step/message editors used by the +// object/mob editors are reused here. Edits mutate currentRoomData.room.* +// and the debounced autoSave() persists them. + +function renderEnterExitTab(r) { + var onEnter = Array.isArray(r.on_enter) ? r.on_enter : []; + var onExit = Array.isArray(r.on_exit) ? r.on_exit : []; + window._editorData = { on_enter: onEnter, on_exit: onExit }; + + window._ieRenderCurrent = function() { + if (window._ieSyncAll) window._ieSyncAll(window._editorData, [ + {id: 'on_enter', path: 'on_enter', isArray: true, interactionStyle: true, itemIDAllowed: false, label: 'Trigger'}, + {id: 'on_exit', path: 'on_exit', isArray: true, interactionStyle: true, itemIDAllowed: false, label: 'Trigger'} + ]); + if (currentRoomData && currentRoomData.room) { + currentRoomData.room.on_enter = window._editorData.on_enter; + currentRoomData.room.on_exit = window._editorData.on_exit; + } + switchPanelTab(8); + autoSave(); + }; + window._ieRenderOnly = window._ieRenderCurrent; + window._ieSyncAll = ie_syncAllInteractions; + + var h = '<h3 style="margin:8px 0 4px">On-Enter Triggers</h3>'; + h += '<p style="color:#888;font-size:11px;margin:0 0 6px">Fires when a player enters this room. First matching entry wins.</p>'; + h += renderTriggerListHTML(r.on_enter, 'on_enter', 'on_enter', false); + h += '<h3 style="margin:16px 0 4px">On-Exit Triggers</h3>'; + h += '<p style="color:#888;font-size:11px;margin:0 0 6px">Fires when a player leaves this room. First matching entry wins.</p>'; + h += renderTriggerListHTML(r.on_exit, 'on_exit', 'on_exit', false); + return h; +} + +// ── Triggers tab (on_flag_change / on_global_flag_change) ── + +function renderTriggersTab(r) { + var fc = Array.isArray(r.on_flag_change) ? r.on_flag_change : []; + var gfc = Array.isArray(r.on_global_flag_change) ? r.on_global_flag_change : []; + window._editorData = { on_flag_change: fc, on_global_flag_change: gfc }; + + window._ieRenderCurrent = function() { + if (window._ieSyncAll) window._ieSyncAll(window._editorData, [ + {id: 'on_flag_change', path: 'on_flag_change', isArray: true, interactionStyle: true, itemIDAllowed: false, label: 'Trigger'}, + {id: 'on_global_flag_change', path: 'on_global_flag_change', isArray: true, interactionStyle: true, itemIDAllowed: false, label: 'Trigger'} + ]); + if (currentRoomData && currentRoomData.room) { + currentRoomData.room.on_flag_change = window._editorData.on_flag_change; + currentRoomData.room.on_global_flag_change = window._editorData.on_global_flag_change; + } + switchPanelTab(9); + autoSave(); + }; + window._ieRenderOnly = window._ieRenderCurrent; + window._ieSyncAll = ie_syncAllInteractions; + + var h = '<h3 style="margin:8px 0 4px">On-Flag-Change Triggers</h3>'; + h += '<p style="color:#888;font-size:11px;margin:0 0 6px">Fires when a player flag changes. Use the condition\'s "In Room" gate to scope to this room (delete it to make global).</p>'; + h += renderFlagTriggerListHTML(r.on_flag_change, 'on_flag_change', 'on_flag_change', 'player'); + h += '<h3 style="margin:16px 0 4px">On-Global-Flag-Change Triggers</h3>'; + h += '<p style="color:#888;font-size:11px;margin:0 0 6px">Fires when a global flag changes (no player involved).</p>'; + h += renderFlagTriggerListHTML(r.on_global_flag_change, 'on_global_flag_change', 'on_global_flag_change', 'global'); + return h; +} + +// renderTriggerListHTML emits the HTML for a []Trigger block using the shared +// ie_renderArraySection. It's a thin shim that builds the ctx the card +// editors construct. +function renderTriggerListHTML(arr, secId, secPath, allowItem) { + var visMap = {}; + arr = Array.isArray(arr) ? arr : []; + var sec = { + id: secId, + path: secPath, + isArray: true, + interactionStyle: true, + itemIDAllowed: allowItem, + label: 'Trigger', + fields: [['item_id', 'Item ID', 'search', 'e.g. keycard', '', null, 'items']] + }; + var ctx = { + sec: sec, + data: window._editorData, + visMap: visMap, + showField: function(sid, idx, key) { + ie_showField(visMap, sid, idx, key); + return 'void(0)'; + }, + hideField: function(sid, idx, key) { + ie_hideField(visMap, sid, idx, key); + window._ieRenderCurrent(); + return 'void(0)'; + }, + addCondition: function(sid, idx) { + return 'ie_addRootConditionGroup(\'' + sid + '\',' + idx + ',\'entry\')'; + }, + showRemove: function(idx) { + return '_mapRemoveTrigger(\'' + secPath + '\',' + idx + ')'; + }, + renderField: function(s, field, data, idx, style) { + var val = (data[secPath] && data[secPath][idx] && data[secPath][idx].item_id) || ''; + return '<div class="obj-search" style="position:relative;flex:1;min-width:0"><input class="search-input" data-field-path="' + secPath + '.' + idx + '.item_id" data-search-type="items" placeholder="item ID" value="' + escAttr(val) + '"><div class="search-results" style="display:none"></div></div>'; + }, + onAdd: function() { + return '_mapAddTrigger(\'' + secPath + '\')'; + } + }; + return ie_renderArraySection(ctx); +} + +// renderFlagTriggerListHTML is like renderTriggerListHTML but adds the +// on_player_flag / on_global_flag subscription header per entry. +function renderFlagTriggerListHTML(arr, secId, secPath, kind) { + var visMap = {}; + arr = Array.isArray(arr) ? arr : []; + var sec = { + id: secId, + path: secPath, + isArray: true, + interactionStyle: true, + itemIDAllowed: false, + label: 'Trigger', + fields: [] + }; + var flagKey = kind === 'player' ? 'on_player_flag' : 'on_global_flag'; + + var h = ''; + arr.forEach(function(item, idx) { + visMap[idx] = visMap[idx] || {}; + var condExists = !!(item.condition && typeof item.condition === 'object'); + var lockVal = !!item.lock; + var steps = item.steps || []; + var flagName = item[flagKey] || ''; + + h += '<div class="obj-sub-row ie-inter-row" data-idx="' + idx + '">'; + h += '<div class="ie-inter-header">'; + h += '<label class="ie-lock-lbl"><input type="checkbox" class="ie-lock-cb"' + (lockVal ? ' checked' : '') + '> Lock Player</label>'; + if (idx > 0) h += '<button class="btn btn-sm ie-trig-up" onclick="ie_moveRow(\'' + escAttr(secPath) + '\',' + idx + ',' + (idx - 1) + ')" title="Move up">▲</button>'; + if (idx < arr.length - 1) h += '<button class="btn btn-sm ie-trig-dn" onclick="ie_moveRow(\'' + escAttr(secPath) + '\',' + idx + ',' + (idx + 1) + ')" title="Move down">▼</button>'; + h += '<span class="ie-cond-leaf-label">' + esc(kind === 'player' ? 'Player Flag' : 'Global Flag') + '</span>'; + h += '<input class="ie-flag-name" data-ie-flag-key="' + escAttr(flagKey) + '" data-idx="' + idx + '" value="' + escAttr(flagName) + '" placeholder="' + escAttr(kind === 'player' ? 'player flag name' : 'global flag name') + '" style="flex:1;min-width:80px" oninput="_mapSyncFlag(\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(flagKey) + '\')">'; + h += '<span class="ie-cond-eq">=</span>'; + h += '<input class="ie-flag-value" data-idx="' + idx + '" value="' + escAttr(item.value !== undefined ? String(item.value) : '') + '" placeholder="exists" style="width:60px" oninput="_mapSyncFlag(\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(flagKey) + '\')">'; + h += '<span class="ie-inter-header-spacer"></span>'; + if (!condExists) { + h += '<button class="btn btn-sm ie-add-btn" onclick="ie_addRootConditionGroup(\'' + escAttr(secPath) + '\',' + idx + ',\'entry\')">+ Add Condition</button>'; + } + h += '<button class="btn btn-sm btn-danger ie-inter-remove" onclick="_mapRemoveTrigger(\'' + escAttr(secPath) + '\',' + idx + ')">Remove Trigger</button>'; + h += '</div>'; + h += '<div class="ie-inter-panel" style="display:' + (condExists ? 'block' : 'none') + '">'; + h += '<div class="ie-inter-panel-body">' + ie_renderCond(item.condition || null, secId, idx, secPath, 'entry') + '</div>'; + h += '</div>'; + h += '<div class="ie-inter-steps">'; + h += ie_renderStepList(steps, secId, idx, secPath); + h += '</div>'; + h += '</div>'; + }); + h += '<button class="btn btn-sm obj-sub-add" onclick="_mapAddFlagTrigger(\'' + escAttr(secPath) + '\',\'' + escAttr(flagKey) + '\')">+ Add Trigger</button>'; + return h; +} + +// _mapAddTrigger / _mapRemoveTrigger / _mapAddFlagTrigger: mutation helpers +// called from the inline onclick handlers. They sync the DOM, splice the +// _editorData array, and re-render. +function _mapAddTrigger(secPath) { + if (!window._editorData) return; + if (window._ieSyncAll) window._ieSyncAll(window._editorData, []); + if (!window._editorData[secPath]) window._editorData[secPath] = []; + window._editorData[secPath].push({steps: []}); + if (currentRoomData && currentRoomData.room) { + currentRoomData.room[secPath] = window._editorData[secPath]; + } + window._ieRenderCurrent(); +} + +function _mapAddFlagTrigger(secPath, flagKey) { + if (!window._editorData) return; + if (!window._editorData[secPath]) window._editorData[secPath] = []; + var entry = {steps: []}; + entry[flagKey] = ''; + window._editorData[secPath].push(entry); + if (currentRoomData && currentRoomData.room) { + currentRoomData.room[secPath] = window._editorData[secPath]; + } + window._ieRenderCurrent(); +} + +function _mapRemoveTrigger(secPath, idx) { + if (!window._editorData || !window._editorData[secPath]) return; + window._editorData[secPath].splice(idx, 1); + if (currentRoomData && currentRoomData.room) { + currentRoomData.room[secPath] = window._editorData[secPath]; + } + window._ieRenderCurrent(); +} + +function _mapSyncFlag(secPath, idx, flagKey) { + if (!window._editorData || !window._editorData[secPath]) return; + var row = document.querySelector('.ie-inter-row[data-idx="' + idx + '"]'); + if (!row) return; + var nameEl = row.querySelector('.ie-flag-name'); + var valEl = row.querySelector('.ie-flag-value'); + if (nameEl) { + window._editorData[secPath][idx][flagKey] = nameEl.value.trim(); + } + if (valEl) { + var v = valEl.value.trim(); + if (v === '') delete window._editorData[secPath][idx].value; + else { + try { window._editorData[secPath][idx].value = JSON.parse(v); } catch(e) { window._editorData[secPath][idx].value = v; } + } + } + if (currentRoomData && currentRoomData.room) { + currentRoomData.room[secPath] = window._editorData[secPath]; + } + autoSave(); +} diff --git a/internal/admin/static/mobeditor.js b/internal/admin/static/mobeditor.js index c2364c8..463af0a 100644 --- a/internal/admin/static/mobeditor.js +++ b/internal/admin/static/mobeditor.js @@ -121,12 +121,10 @@ var MOB_SECTIONS = [ ] }, { - id: 'on_kill', label: 'On Kill', labelHint: '(fired when this mob is defeated or its task completes — additive over drops)', path: 'on_kill', isArray: true, fullWidth: true, interactionStyle: true, interScope: 'inline', + id: 'on_kill', label: 'On Kill', labelHint: '(fired when this mob is defeated or its task completes — additive over drops)', path: 'on_kill', isArray: true, fullWidth: true, interactionStyle: true, detect: function(d) { return d.on_kill && Array.isArray(d.on_kill); }, fields: [ ['item_id', 'Item ID', 'search', '(optional — only fires if you wield this weapon when the mob is defeated)', '', null, 'items'], - ['condition', 'Condition', 'inter_cond'], - ['action', 'Action', 'inter_act'], ] }, ]; @@ -813,8 +811,9 @@ function saveMob() { var cleanedCond = ie_cleanCondition(dataItem.condition); if (cleanedCond) item.condition = cleanedCond; } - if (dataItem.action && typeof dataItem.action === 'object') { - item.action = dataItem.action; + if (dataItem.lock) item.lock = true; + if (Array.isArray(dataItem.steps)) { + item.steps = dataItem.steps.map(ie_cleanStep).filter(function(s) { return s && Object.keys(s).length > 0; }); } } if (Object.keys(item).length > 0) arr.push(item); diff --git a/internal/admin/static/objecteditor.js b/internal/admin/static/objecteditor.js index 9c5a279..1abccbf 100644 --- a/internal/admin/static/objecteditor.js +++ b/internal/admin/static/objecteditor.js @@ -86,21 +86,17 @@ var SECTIONS = [ ] }, { - id: 'on_use', label: 'On Use', labelHint: '(what happens when you use items on this)', path: 'on_use', isArray: true, fullWidth: true, interactionStyle: true, interScope: 'inline', + id: 'on_use', label: 'On Use', labelHint: '(what happens when you use items on this)', path: 'on_use', isArray: true, fullWidth: true, interactionStyle: true, detect: function(d) { return d.on_use && Array.isArray(d.on_use); }, fields: [ ['item_id', 'Item ID', 'search', 'e.g. keycard (empty = bare use)', '', null, 'items'], - ['condition', 'Condition', 'inter_cond'], - ['action', 'Action', 'inter_act'], ] }, { - id: 'on_look', label: 'On Look', labelHint: '(what happens when you "look <this>")', path: 'on_look', isArray: true, fullWidth: true, interactionStyle: true, interScope: 'inline', + id: 'on_look', label: 'On Look', labelHint: '(what happens when you "look <this>")', path: 'on_look', isArray: true, fullWidth: true, interactionStyle: true, detect: function(d) { return d.on_look && Array.isArray(d.on_look); }, fields: [ ['item_id', 'Item ID', 'search', '(optional — only fires if you carry this item)', '', null, 'items'], - ['condition', 'Condition', 'inter_cond'], - ['action', 'Action', 'inter_act'], ] }, ]; @@ -517,8 +513,6 @@ function renderArraySection(sec, data) { showRemove: function(idx) { return 'removeArrayItem(\'' + sec.id + '\',' + idx + ')'; }, showField: function(secId, idx, key) { return 'showInterField(\'' + secId + '\',' + idx + ',\'' + key + '\')'; }, hideField: function(secId, idx, key) { return 'hideInterField(\'' + secId + '\',' + idx + ',\'' + key + '\')'; }, - addCondition: function(secId, idx) { return 'addInterRootCondition(\'' + secId + '\',' + idx + ')'; }, - addEffect: function(secId, idx) { return 'addInterAction(\'' + secId + '\',' + idx + ')'; }, renderField: function(sec, f, data, idx, optStyle) { return renderField(sec, f, data, idx, null, optStyle); }, onAdd: function() { return 'addArrayItem(\'' + sec.id + '\')'; }, }); @@ -822,10 +816,15 @@ function addArrayItem(cardID) { var sec = SECTIONS.find(function(s) { return s.id === cardID; }); if (!sec || !sec.isArray) return; var arr = objectData[sec.path] || []; - var empty = {}; - sec.fields.forEach(function(f) { - empty[f[0]] = f[2] === 'checkbox' ? false : (f[2] === 'number' ? 0 : ''); - }); + var empty; + if (sec.interactionStyle) { + empty = {steps: []}; + } else { + empty = {}; + sec.fields.forEach(function(f) { + empty[f[0]] = f[2] === 'checkbox' ? false : (f[2] === 'number' ? 0 : ''); + }); + } arr.push(empty); objectData[sec.path] = arr; renderCurrent(); @@ -915,13 +914,7 @@ function showInterField(secId, idx, key) { function addInterRootCondition(secId, idx) { var sec = SECTIONS.find(function(s) { return s.id === secId; }); if (!sec) return; - ie_addRootConditionGroup(sec.path, idx); -} - -function addInterAction(secId, idx) { - var sec = SECTIONS.find(function(s) { return s.id === secId; }); - if (!sec) return; - ie_addEffectToInteraction(sec.path, idx); + ie_addRootConditionGroup(sec.path, idx, 'entry'); } function hideInterField(secId, idx, key) { @@ -932,8 +925,8 @@ function hideInterField(secId, idx, key) { ced_syncDOMToData(objectData); ie_syncAllInteractions(objectData, SECTIONS); if (objectData[secPath] && objectData[secPath][idx]) { - if (key === 'condition' || key === 'action') { - delete objectData[secPath][idx][key]; + if (key === 'condition') { + delete objectData[secPath][idx].condition; } else { objectData[secPath][idx][key] = ''; } @@ -1030,8 +1023,10 @@ function saveObject() { var cleanedCond = ie_cleanCondition(dataItem.condition); if (cleanedCond) item.condition = cleanedCond; } - if (dataItem.action && typeof dataItem.action === 'object') { - item.action = dataItem.action; + if (dataItem.lock) item.lock = true; + if (dataItem.steps && Array.isArray(dataItem.steps)) { + var cleanedSteps = dataItem.steps.map(ie_cleanStep).filter(function(s) { return Object.keys(s).length > 0; }); + if (cleanedSteps.length > 0) item.steps = cleanedSteps; } } if (Object.keys(item).length > 0) arr.push(item); |
