diff options
| author | historia <[not public]> | 2026-07-09 22:15:54 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-07-09 22:15:54 -0400 |
| commit | ecba7f726f70b37126d852c38c7e3eec7b04d730 (patch) | |
| tree | 9129215c6e5015336fde1116395336d82bb952d1 /internal/admin/static/intereditor.js | |
| parent | b3d4c616f59ad2519f3a0b77e3b47d6571cd2486 (diff) | |
| download | thehouseoficarus-ecba7f726f70b37126d852c38c7e3eec7b04d730.tar.gz | |
feat: old standalone trigger systems completely unified into trigger->condition->action system
Diffstat (limited to 'internal/admin/static/intereditor.js')
| -rw-r--r-- | internal/admin/static/intereditor.js | 1074 |
1 files changed, 591 insertions, 483 deletions
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 |
