From ecba7f726f70b37126d852c38c7e3eec7b04d730 Mon Sep 17 00:00:00 2001
From: historia <[not public]>
Date: Thu, 9 Jul 2026 22:15:54 -0400
Subject: feat: old standalone trigger systems completely unified into
trigger->condition->action system
---
internal/admin/static/admin.css | 36 +-
internal/admin/static/intereditor.js | 1074 ++++++++++++++++++---------------
internal/admin/static/map.js | 229 +++++++
internal/admin/static/mobeditor.js | 9 +-
internal/admin/static/objecteditor.js | 41 +-
5 files changed, 876 insertions(+), 513 deletions(-)
(limited to 'internal/admin/static')
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 += '';
+ } else {
+ h += '';
+ }
+ });
+ 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 += '';
+ });
+ 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-' 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 += '
';
- h += ie_renderGroup(condObj, secId, secPath, idx, '');
+ h += ie_renderGroup(condObj, secId, secPath, idx, '', kind);
h += '
';
}
- // 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 += '
';
IE_COND_TYPES.forEach(function(ct) {
- h += '';
+ h += '';
});
- h += '';
+ h += '';
h += '
';
- }
-
- // 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 += '
';
rows += '
' + esc(at.label) + '
';
rows += '
';
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 += '
';
+ 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 += '';
});
- if (scope === 'seq') {
- IE_ACT_TYPES_SEQ.forEach(function(at) {
- if (actionObj && Object.prototype.hasOwnProperty.call(actionObj, at.key)) return;
- addbar += '';
+
+ 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 += '
';
- // 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 '
' + rows + addbar + '
';
+ 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 = '';
+ rowEl.innerHTML = '';
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 = '';
+ rowEl.className = 'ie-kv-row ie-msg-row';
+ var rmFn = 'ie_removeMessage(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ')';
+ rowEl.innerHTML = '';
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 += '
';
h += '
';
- if (!showItem) {
- h += '';
- } else {
- h += ctx.renderField(sec, sec.fields[0], data, idx, 'flex:1;min-width:0');
- h += '';
+ h += '';
+ if (idx > 0) h += '';
+ if (idx < arr.length - 1) h += '';
+
+ if (allowItem) {
+ if (!showItem) {
+ h += '';
+ } else {
+ h += ctx.renderField(sec, sec.fields[0], data, idx, 'flex:1;min-width:0');
+ h += '';
+ }
}
if (!condExists) {
- h += '';
- }
- if (!actExists) {
- h += '';
+ h += '';
}
h += '';
- h += '';
+ h += '';
h += '
';
+ h += ie_renderStepList(steps, sec.id, idx, sec.path);
h += '
';
h += '
';
});
- h += '';
+ h += '';
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 += '';
html += '';
html += '';
+ html += '';
+ html += '';
html += '
';
html += '
';
@@ -595,6 +597,8 @@ function renderSidePanel(data) {
html += renderCourseTab();
}
}
+ else if (currentPanelTab === 8) html += renderEnterExitTab(r);
+ else if (currentPanelTab === 9) html += renderTriggersTab(r);
html += '
';
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 = '
On-Enter Triggers
';
+ h += '
Fires when a player enters this room. First matching entry wins.
';
+ h += renderTriggerListHTML(r.on_enter, 'on_enter', 'on_enter', false);
+ h += '
On-Exit Triggers
';
+ h += '
Fires when a player leaves this room. First matching entry wins.