aboutsummaryrefslogtreecommitdiff
path: root/internal/admin
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-07-09 22:15:54 -0400
committerhistoria <[not public]>2026-07-09 22:15:54 -0400
commitecba7f726f70b37126d852c38c7e3eec7b04d730 (patch)
tree9129215c6e5015336fde1116395336d82bb952d1 /internal/admin
parentb3d4c616f59ad2519f3a0b77e3b47d6571cd2486 (diff)
downloadthehouseoficarus-ecba7f726f70b37126d852c38c7e3eec7b04d730.tar.gz
feat: old standalone trigger systems completely unified into trigger->condition->action system
Diffstat (limited to 'internal/admin')
-rw-r--r--internal/admin/api_map.go38
-rw-r--r--internal/admin/api_room_courses.go24
-rw-r--r--internal/admin/api_room_insert_remove_test.go12
-rw-r--r--internal/admin/api_rooms_bulk.go10
-rw-r--r--internal/admin/server.go2
-rw-r--r--internal/admin/static/admin.css36
-rw-r--r--internal/admin/static/intereditor.js1074
-rw-r--r--internal/admin/static/map.js229
-rw-r--r--internal/admin/static/mobeditor.js9
-rw-r--r--internal/admin/static/objecteditor.js41
-rw-r--r--internal/admin/templates/map.html2
11 files changed, 919 insertions, 558 deletions
diff --git a/internal/admin/api_map.go b/internal/admin/api_map.go
index 6e62d9e..5b0464d 100644
--- a/internal/admin/api_map.go
+++ b/internal/admin/api_map.go
@@ -280,27 +280,27 @@ func (s *AdminServer) handleMap(w http.ResponseWriter, r *http.Request) {
continue
}
- bidirectional := false
- if targetRoom, ok := roomData[target]; ok {
- if oppExit, ok := targetRoom.Exits[world.OppositeExit[dir]]; ok && oppExit.Room == rid && !oppExit.Hidden && !oppExit.AlwaysBlocked {
- bidirectional = true
+ bidirectional := false
+ if targetRoom, ok := roomData[target]; ok {
+ if oppExit, ok := targetRoom.Exits[world.OppositeExit[dir]]; ok && oppExit.Room == rid && !oppExit.Hidden && !oppExit.AlwaysBlocked {
+ bidirectional = true
+ }
}
- }
- key := linkKey(rid, target)
- if bidirectional && seenLinks[key] {
- continue
- }
- seenLinks[key] = true
-
- links = append(links, LinkEntry{
- From: rid,
- To: target,
- Dir: string(dir),
- Bidirectional: bidirectional,
- Hidden: exit.Hidden,
- AlwaysBlocked: exit.AlwaysBlocked,
- })
+ key := linkKey(rid, target)
+ if bidirectional && seenLinks[key] {
+ continue
+ }
+ seenLinks[key] = true
+
+ links = append(links, LinkEntry{
+ From: rid,
+ To: target,
+ Dir: string(dir),
+ Bidirectional: bidirectional,
+ Hidden: exit.Hidden,
+ AlwaysBlocked: exit.AlwaysBlocked,
+ })
}
}
diff --git a/internal/admin/api_room_courses.go b/internal/admin/api_room_courses.go
index e4cf849..097b9e8 100644
--- a/internal/admin/api_room_courses.go
+++ b/internal/admin/api_room_courses.go
@@ -290,15 +290,15 @@ func (s *AdminServer) handleRoomCourse(w http.ResponseWriter, r *http.Request, r
m["id"] = id
delete(m, "_raw")
writeJSON(w, map[string]any{
- "course": m,
- "course_id": id,
- "course_name": m["name"],
- "obstacle_index": idx,
- "total_obstacles": len(obstacles),
- "obstacle": obstacles[idx],
- "obstacles": obstacles,
- "_raw": string(raw),
- "room_id": roomID,
+ "course": m,
+ "course_id": id,
+ "course_name": m["name"],
+ "obstacle_index": idx,
+ "total_obstacles": len(obstacles),
+ "obstacle": obstacles[idx],
+ "obstacles": obstacles,
+ "_raw": string(raw),
+ "room_id": roomID,
})
return
}
@@ -565,8 +565,6 @@ func exitIsAlwaysBlocked(v any) bool {
return false
}
-
-
// isHorizontalDir returns whether d is one of the 8 horizontal directions.
func isHorizontalDir(d string) bool {
for _, h := range horizontalExitDirs {
@@ -577,8 +575,6 @@ func isHorizontalDir(d string) bool {
return false
}
-
-
// reconcileExitOnRoom validates that roomID has an exit in dir.
// If expectedTarget > 0, also checks the exit points to that room.
// Returns an error string or empty string on success.
@@ -627,4 +623,4 @@ func (s *AdminServer) reconcileCourseExits(m map[string]any) []string {
}
return warnings
-} \ No newline at end of file
+}
diff --git a/internal/admin/api_room_insert_remove_test.go b/internal/admin/api_room_insert_remove_test.go
index 6d80595..f613313 100644
--- a/internal/admin/api_room_insert_remove_test.go
+++ b/internal/admin/api_room_insert_remove_test.go
@@ -31,9 +31,9 @@ func newTestAdminServer(t *testing.T, rooms map[int]string) *AdminServer {
}
}
return &AdminServer{
- world: world.New(dir),
- mobStore: world.NewMobStore(dir),
- dataDir: dir,
+ world: world.New(dir),
+ mobStore: world.NewMobStore(dir),
+ dataDir: dir,
undoStack: NewUndoStack(dir),
}
}
@@ -428,9 +428,9 @@ func TestNextRoomIDNoCrossSubdirCollision(t *testing.T) {
t.Fatal(err)
}
s := &AdminServer{
- world: world.New(dir),
- mobStore: world.NewMobStore(dir),
- dataDir: dir,
+ world: world.New(dir),
+ mobStore: world.NewMobStore(dir),
+ dataDir: dir,
undoStack: NewUndoStack(dir),
}
// Allocating near room 1 (root) must skip 2 (which lives in zone/).
diff --git a/internal/admin/api_rooms_bulk.go b/internal/admin/api_rooms_bulk.go
index 34ca209..6283067 100644
--- a/internal/admin/api_rooms_bulk.go
+++ b/internal/admin/api_rooms_bulk.go
@@ -64,11 +64,11 @@ func (s *AdminServer) handleBulkEdit(w http.ResponseWriter, r *http.Request) {
}
type roomWrite struct {
- oldContent []byte
- newContent []byte
- oldPath string
- newPath string
- wasMoved bool
+ oldContent []byte
+ newContent []byte
+ oldPath string
+ newPath string
+ wasMoved bool
}
writes := make([]roomWrite, 0, len(req.IDs))
diff --git a/internal/admin/server.go b/internal/admin/server.go
index 67de0ab..64c5592 100644
--- a/internal/admin/server.go
+++ b/internal/admin/server.go
@@ -19,8 +19,8 @@ import (
"math/big"
"net"
"net/http"
- "path/filepath"
"os"
+ "path/filepath"
"strconv"
"strings"
"time"
diff --git a/internal/admin/static/admin.css b/internal/admin/static/admin.css
index b35d5e2..7c8f0a5 100644
--- a/internal/admin/static/admin.css
+++ b/internal/admin/static/admin.css
@@ -420,8 +420,7 @@ g.ud-hover:hover text{font-weight:bold}
.ie-kv-row{display:flex;gap:4px;align-items:center}
.ie-kv-key,.ie-kv-val{font-size:11px;padding:2px 4px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:3px;font-family:monospace;width:120px}
.ie-kv-val{width:80px}
-.ie-act-msg-delay,.ie-act-msg{font-size:11px;padding:3px 6px;background:var(--input-bg);color:var(--text);border:1px solid var(--input-border);border-radius:3px;font-family:monospace}
-.ie-act-msg-delay{width:60px}
+.ie-act-msg{font-size:11px;padding:3px 6px;background:var(--input-bg);color:var(--text);border:1px solid var(--input-border);border-radius:3px;font-family:monospace}
.ie-act-msg{flex:1}
/* Interaction row layout: column. The single flex header row holds the
@@ -439,3 +438,36 @@ g.ud-hover:hover text{font-weight:bold}
.ie-inter-panel-row{display:flex;gap:6px;align-items:flex-start}
.ie-inter-panel-body{flex:1;min-width:0}
.ie-inter-panel-x{margin-top:4px;flex-shrink:0}
+
+/* ── Step list: boxed steps with right-justified move/remove controls ── */
+.ie-inter-steps{margin-top:4px}
+.ie-step-list{display:flex;flex-direction:column;gap:4px}
+.ie-step-row{background:rgba(100,160,255,.03);border:1px solid rgba(100,160,255,.1);border-radius:6px;padding:8px;display:flex;flex-direction:column;gap:4px}
+.ie-step-header{display:flex;align-items:center;gap:6px}
+.ie-step-label{font-size:10px;color:#6af;text-transform:uppercase;letter-spacing:.3px;font-weight:bold;white-space:nowrap}
+.ie-step-addmore{color:#6af;border:1px solid rgba(100,160,255,.25);background:transparent;font-size:10px;padding:1px 6px;border-radius:3px;cursor:pointer;font-family:monospace;line-height:1}
+.ie-step-addmore:hover{background:rgba(100,160,255,.1)}
+.ie-step-spacer{flex:1}
+.ie-step-up,.ie-step-dn{padding:2px 6px;font-size:9px}
+.ie-step-cond-panel{margin:2px 0}
+.ie-step-effects{margin:2px 0}
+.ie-step-messages{margin:2px 0}
+.ie-step-addbar{display:none;flex-wrap:wrap;gap:3px;margin-top:2px}
+.ie-step-row.show-addbar .ie-step-addbar{display:flex}
+.ie-step-addbar-bottom{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px}
+.ie-step-addbar-bottom .btn{padding:4px 10px;font-size:11px}
+
+/* ── Message rows within a step ── */
+.ie-msg-row{display:flex;align-items:center;gap:4px}
+.ie-msg-up,.ie-msg-dn{padding:1px 4px;font-size:9px;line-height:1}
+
+/* ── Lock player checkbox ── */
+.ie-lock-lbl{font-size:10px;color:var(--text);display:inline-flex;align-items:center;gap:3px;cursor:pointer;white-space:nowrap;margin-right:6px}
+.ie-lock-cb{accent-color:var(--accent);margin:0}
+
+/* ── Trigger row move up/down ── */
+.ie-trig-up,.ie-trig-dn{padding:2px 6px;font-size:9px}
+
+/* ── Empty condition group placeholder ── */
+.ie-cond-empty{margin-bottom:2px}
+.ie-cond-empty-bar{display:flex;align-items:center;gap:6px;flex-wrap:wrap;padding-bottom:2px}
diff --git a/internal/admin/static/intereditor.js b/internal/admin/static/intereditor.js
index 5cf39a1..653bccf 100644
--- a/internal/admin/static/intereditor.js
+++ b/internal/admin/static/intereditor.js
@@ -1,38 +1,172 @@
-// intereditor.js — Structured condition & action editors for on_use/on_look/on_kill.
-// Replaces free-form JSON textareas with button-driven pickers.
-// Shared by objecteditor.js and mobeditor.js.
+// intereditor.js — Structured condition & step editors for trigger blocks.
+// Drives the unified Trigger model:
+// Trigger = { lock, item_id?, condition?, steps[], on_player_flag?, on_global_flag?, value? }
+// Step = { condition?, wait, messages[], broadcast, broadcast_global,
+// spawn_mob, despawn_mob, set_global_flags, set_player_flags,
+// give_item, take_item, teleport, heal, credits, aps_node }
+// Shared by objecteditor.js, mobeditor.js, and map.js.
// ── Condition leaf types ──
var IE_COND_TYPES = [
{type: 'global_flag', label: 'Global Flag', hint: 'global flag name'},
{type: 'player_flag', label: 'Player Flag', hint: 'player flag name'},
+ {type: 'room', label: 'In Room', hint: 'room ID'},
{type: 'has_item', label: 'Has Item', hint: 'item ID'},
{type: 'min_credits', label: 'Min Credits', hint: 'amount'},
];
+// ── Effect field types ──
+//
+// Every block is sequence-scoped now: broadcast / broadcast_global /
+// spawn_mob / despawn_mob are available everywhere. Messages and condition
+// are handled separately (plain-string message rows; per-step condition).
+
var IE_ACT_TYPES = [
- {key: 'set_global_flags', label: 'Set Global Flags', kind: 'kv'},
- {key: 'set_player_flags', label: 'Set Player Flags', kind: 'kv'},
- {key: 'give_item', label: 'Give Item', kind: 'search', entity: 'items'},
- {key: 'take_item', label: 'Take Item', kind: 'search', entity: 'items'},
- {key: 'teleport', label: 'Teleport', kind: 'number', hint: 'room ID'},
- {key: 'heal', label: 'Heal', kind: 'number', hint: 'HP to restore'},
- {key: 'credits', label: 'Credits', kind: 'number', hint: 'amount (negative = cost)'},
- {key: 'aps_node', label: 'APS Node', kind: 'checkbox'},
+ {key: 'wait', label: 'Wait', kind: 'number', hint: 'ticks before this step'},
+ {key: 'broadcast', label: 'Broadcast', kind: 'text', hint: '%p = player'},
+ {key: 'broadcast_global', label: 'Broadcast Global', kind: 'text', hint: '%p = player'},
+ {key: 'spawn_mob', label: 'Spawn Mob', kind: 'search', entity: 'mobs'},
+ {key: 'despawn_mob', label: 'Despawn Mob', kind: 'search', entity: 'mobs'},
+ {key: 'set_global_flags', label: 'Set Global Flags', kind: 'kv'},
+ {key: 'set_player_flags', label: 'Set Player Flags', kind: 'kv'},
+ {key: 'give_item', label: 'Give Item', kind: 'search', entity: 'items'},
+ {key: 'take_item', label: 'Take Item', kind: 'search', entity: 'items'},
+ {key: 'teleport', label: 'Teleport', kind: 'number', hint: 'room ID'},
+ {key: 'heal', label: 'Heal', kind: 'number', hint: 'HP to restore'},
+ {key: 'credits', label: 'Credits', kind: 'number', hint: 'amount (negative = cost)'},
+ {key: 'aps_node', label: 'APS Node', kind: 'checkbox'},
];
-// Sequence-only effects (only shown for scope === 'seq').
-var IE_ACT_TYPES_SEQ = [
- {key: 'broadcast', label: 'Broadcast', kind: 'text', hint: '%p = player'},
- {key: 'broadcast_global', label: 'Broadcast Global', kind: 'text', hint: '%p = player'},
- {key: 'spawn_mob', label: 'Spawn Mob', kind: 'search', entity: 'mobs'},
- {key: 'despawn_mob', label: 'Despawn Mob', kind: 'search', entity: 'mobs'},
+// ── Step-kind buttons for the bottom add-bar (each click creates a new
+// step initialised with that single action kind) and the per-step
+// "+" toggle bar. messages is handled separately from IE_ACT_TYPES. ──
+
+var IE_STEP_KINDS = [
+ {key: 'messages', label: 'Message', kind: 'messages'},
];
+IE_ACT_TYPES.forEach(function(at) {
+ IE_STEP_KINDS.push(at);
+});
+
+// ── Rendering helpers ──
+
+// ie_renderStepAddbar: the per-step action add bar revealed by the "+" toggle.
+// Only shows kinds not already present on this step.
+function ie_renderStepAddbar(step, secId, idx, si, secPath) {
+ var h = '';
+ IE_STEP_KINDS.forEach(function(sk) {
+ if (sk.key === 'messages') {
+ if (step && Array.isArray(step.messages)) return;
+ } else {
+ if (step && Object.prototype.hasOwnProperty.call(step, sk.key)) {
+ if (sk.kind === 'checkbox' && step[sk.key] !== true) { /* re-addable */ }
+ else return;
+ }
+ }
+ if (sk.key === 'messages') {
+ h += '<button class="btn btn-sm ie-add-btn" onclick="ie_addMessages(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ')">+ ' + esc(sk.label) + '</button>';
+ } else {
+ h += '<button class="btn btn-sm ie-add-btn" onclick="ie_addActEffect(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',\'' + escAttr(sk.key) + '\')">+ ' + esc(sk.label) + '</button>';
+ }
+ });
+ return h;
+}
+
+// ie_renderBottomAddbar: the always-visible bar at the foot of the step list.
+// Each button creates a new step initialised with that single action kind.
+function ie_renderBottomAddbar(secId, secPath, idx) {
+ var h = '';
+ IE_STEP_KINDS.forEach(function(sk) {
+ h += '<button class="btn btn-sm ie-add-btn" onclick="ie_addStepKind(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(sk.key) + '\')">+ ' + esc(sk.label) + ' Step</button>';
+ });
+ return h;
+}
+
+// ie_addStepKind creates a new step initialised with a single action kind.
+function ie_addStepKind(secId, secPath, idx, kind) {
+ var ed = window._editorData;
+ if (!ed) return;
+ ced_syncDOMToData(ed);
+ if (window._ieSyncAll) window._ieSyncAll(ed);
+ if (!ed[secPath]) ed[secPath] = [];
+ if (!ed[secPath][idx]) ed[secPath][idx] = {};
+ if (!ed[secPath][idx].steps || !Array.isArray(ed[secPath][idx].steps)) ed[secPath][idx].steps = [];
+ var step = {};
+ if (kind === 'messages') {
+ step.messages = [''];
+ } else if (kind === 'wait') {
+ step.wait = 1;
+ } else {
+ var at = IE_ACT_TYPES.find(function(a) { return a.key === kind; });
+ if (!at) return;
+ if (at.kind === 'kv') step[kind] = {};
+ else if (at.kind === 'checkbox') step[kind] = true;
+ else if (at.kind === 'number') step[kind] = 0;
+ else step[kind] = '';
+ }
+ ed[secPath][idx].steps.push(step);
+ window._ieRenderCurrent();
+}
// Sentinel child-index for a root-level single leaf (no parent group).
var IE_ROOT_LEAF = -1;
+// ── Condition lens helpers (entry vs step) ──
+//
+// kind is the string-encoded condition anchor: 'entry' for the trigger's own
+// condition, or 'step-<n>' for the nth step's condition. Internally we parse
+// it to the numeric step index (-1 = entry).
+
+function _ie_kindStep(kind) {
+ if (kind === undefined || kind === null || kind === 'entry') return -1;
+ if (typeof kind === 'string' && kind.indexOf('step-') === 0) {
+ var n = parseInt(kind.slice(5), 10);
+ return isNaN(n) ? -1 : n;
+ }
+ return -1;
+}
+
+function _ie_getCond(ed, secPath, idx, kind) {
+ var si = _ie_kindStep(kind);
+ if (!ed[secPath] || !ed[secPath][idx]) return null;
+ if (si < 0) return ed[secPath][idx].condition || null;
+ if (!ed[secPath][idx].steps || !ed[secPath][idx].steps[si]) return null;
+ return ed[secPath][idx].steps[si].condition || null;
+}
+
+function _ie_setCond(ed, secPath, idx, kind, newCond) {
+ var si = _ie_kindStep(kind);
+ if (!ed[secPath]) ed[secPath] = [];
+ if (!ed[secPath][idx]) ed[secPath][idx] = {};
+ if (si < 0) {
+ if (newCond) ed[secPath][idx].condition = newCond;
+ else delete ed[secPath][idx].condition;
+ } else {
+ if (!ed[secPath][idx].steps) ed[secPath][idx].steps = [];
+ if (!ed[secPath][idx].steps[si]) ed[secPath][idx].steps[si] = {};
+ if (newCond) ed[secPath][idx].steps[si].condition = newCond;
+ else delete ed[secPath][idx].steps[si].condition;
+ }
+}
+
+function _ie_syncCond(ed, secId, secPath, idx, kind) {
+ var card = document.querySelector('.obj-card[data-card="' + secId + '"]');
+ if (!card) return;
+ var si = _ie_kindStep(kind);
+ var container;
+ if (si < 0) {
+ container = card.querySelector('.obj-sub-row.ie-inter-row[data-idx="' + idx + '"]');
+ } else {
+ container = card.querySelector('.obj-sub-row.ie-inter-row[data-idx="' + idx + '"] .ie-step-row[data-step="' + si + '"]');
+ }
+ if (!container) return;
+ var condRoot = container.querySelector('.ie-cond-root');
+ if (!condRoot) return;
+ var cond = ie_collectCond(condRoot);
+ _ie_setCond(ed, secPath, idx, kind, cond);
+}
+
// ── Navigation in condition tree ──
function ie_condAt(cond, path) {
@@ -48,7 +182,6 @@ function ie_condAt(cond, path) {
return cur;
}
-// Replaces the condition at path within the overall tree. Returns new root.
function ie_condReplace(cond, path, newVal) {
if (!path) return newVal;
if (!cond) return cond;
@@ -72,13 +205,7 @@ function ie_condReplace(cond, path, newVal) {
}
// ── Clean condition tree for save ──
-//
-// The live editor tree intentionally holds empty leaves/groups while the user
-// is building up a condition (so clicking a second leaf button adds a sibling
-// rather than replacing the first). This walker strips those in-progress
-// artifacts before save: drops leaves whose value is missing/empty, drops
-// 0-child groups, and collapses 1-child non-NOT groups to their child. min_credits
-// keeps the value 0 (falsy in JS but a valid threshold).
+
function ie_cleanCondition(cond) {
if (!cond || typeof cond !== 'object') return null;
if (cond.all_of || cond.any_of) {
@@ -91,7 +218,6 @@ function ie_cleanCondition(cond) {
r[mode] = cleaned;
return r;
}
- // Leaf
var not = !!cond.not;
var leaf = null;
if (cond.global_flag !== undefined) {
@@ -104,6 +230,8 @@ function ie_cleanCondition(cond) {
leaf = { player_flag: cond.player_flag };
if (cond.value !== undefined) leaf.value = cond.value;
}
+ } else if (cond.room !== undefined) {
+ if (cond.room) leaf = { room: parseInt(cond.room, 10) || 0 };
} else if (cond.has_item !== undefined) {
if (cond.has_item) leaf = { has_item: cond.has_item };
} else if (cond.min_credits !== undefined) {
@@ -120,12 +248,6 @@ function ie_cleanCondition(cond) {
function ie_collectCond(rootEl) {
if (!rootEl) return null;
- // A .ie-cond-root wraps either a single group (.ie-cond) or a single bare
- // leaf (.ie-cond-leaf). Unwrap it so sync is the pure inverse of render:
- // cond-root > .ie-cond(group) -> ie_collectCond(.ie-cond)
- // cond-root > .ie-cond-leaf -> ie_collectLeaf(leafEl)
- // The old version recursed into the .ie-cond child and then wrapped the
- // result, producing {all_of:[{all_of:[...]}]} for any group condition.
if (rootEl.classList.contains('ie-cond-root')) {
var groupEl = rootEl.querySelector(':scope > .ie-cond');
if (groupEl) return ie_collectCond(groupEl);
@@ -134,7 +256,6 @@ function ie_collectCond(rootEl) {
return null;
}
- // rootEl is a .ie-cond group box.
var mode = 'all_of';
var modeSel = rootEl.querySelector(':scope > .ie-cond-modebar .ie-cond-modesel');
if (modeSel) mode = modeSel.value;
@@ -155,13 +276,6 @@ function ie_collectCond(rootEl) {
});
}
- // Preserve empty groups (no modebar rendered for <2 children, so empty groups
- // always serialize as {all_of:[]}). Without this, "+ Group" would vanish on
- // the next sync before the user could add their first leaf, and the
- // subsequent "+ leaf" would turn it into a bare leaf — silently dropping
- // the user's group intent. Empty {all_of:[]} is harmless game-side
- // (checkCondition returns true, equivalent to no condition) and
- // ced_cleanOutput strips it from saved YAML.
var result = {};
if (not) result.not = true;
result[mode] = items;
@@ -178,11 +292,6 @@ function ie_collectLeaf(el) {
var valInput = el.querySelector('.ie-cond-val');
var val = valInput ? valInput.value.trim() : '';
- // Always return the leaf (even when val is empty) so the in-memory
- // condition tree keeps "in-progress" leaves the user just added. Without
- // this, clicking a second leaf button would sync, see null, and wipe the
- // first leaf — breaking multi-add and nested-group building. ie_cleanCondition
- // strips empty leaves at save time.
switch (leafType) {
case 'global_flag':
result.global_flag = val;
@@ -200,6 +309,9 @@ function ie_collectLeaf(el) {
try { result.value = JSON.parse(sv); } catch(e) { result.value = sv; }
}
break;
+ case 'room':
+ result.room = parseInt(val) || 0;
+ break;
case 'has_item':
result.has_item = val;
break;
@@ -212,123 +324,30 @@ function ie_collectLeaf(el) {
return result;
}
-// ── Collect action from DOM ──
-
-function ie_collectAct(rootEl) {
- if (!rootEl) return null;
- var r = {};
-
- // Messages — collected from the extendable delay+message rows.
- var msgsKv = rootEl.querySelector('.ie-act-kv[data-ie-act-key="messages"]');
- if (msgsKv) {
- var list = [];
- msgsKv.querySelectorAll('.ie-kv-row').forEach(function(row) {
- var delayEl = row.querySelector('.ie-act-msg-delay');
- var msgEl = row.querySelector('.ie-act-msg');
- var d = delayEl ? parseInt(delayEl.value) || 0 : 0;
- var m = msgEl ? msgEl.value.trim() : '';
- list.push({ message: m, delay: d });
- });
- r.messages = list;
- }
-
- // KV-type effects — include the map even when empty (as long as the
- // .ie-act-kv element exists in the DOM). This lets a just-added section
- // (e.g. "+ Set Player Flags" → no rows yet) survive the next sync instead
- // of being dropped on the next add-effect call. ced_cleanOutput strips
- // empty maps at save time.
- rootEl.querySelectorAll('.ie-act-kv').forEach(function(kvEl) {
- var key = kvEl.getAttribute('data-ie-act-key');
- if (!key) return;
- if (key === 'messages') return;
- var map = {};
- kvEl.querySelectorAll('.ie-kv-row').forEach(function(row) {
- var kEl = row.querySelector('.ie-kv-key');
- var vEl = row.querySelector('.ie-kv-val');
- if (kEl && kEl.value.trim() && vEl) {
- var v = vEl.value.trim();
- var num = parseFloat(v);
- map[kEl.value.trim()] = v === 'true' ? true : (v === 'false' ? false : (!isNaN(num) ? num : v));
- }
- });
- r[key] = map;
- });
-
- // Scalar effects — preserve empty strings so a present-but-unfilled input
- // round-trips on every sync.
- rootEl.querySelectorAll('.ie-act-input').forEach(function(el) {
- var key = el.getAttribute('data-ie-act-key');
- if (!key) return;
- var kind = el.getAttribute('data-ie-act-kind');
- if (kind === 'checkbox') {
- r[key] = el.checked;
- } else if (kind === 'number') {
- var n = parseFloat(el.value);
- if (isNaN(n)) return;
- r[key] = n;
- } else {
- r[key] = el.value.trim();
- }
- });
-
- if (Object.keys(r).length === 0) return null;
- return r;
-}
-
-// ── Sync all interaction DOM back to data ──
-
-function ie_syncAllInteractions(data, secs) {
- if (!data || !secs) return;
- secs.forEach(function(sec) {
- if (!sec.interactionStyle || !sec.isArray) return;
- var p = sec.path;
- if (!data[p]) return;
- var card = document.querySelector('.obj-card[data-card="' + sec.id + '"]');
- if (!card) return;
- var rows = card.querySelectorAll('.obj-sub-row');
- rows.forEach(function(row, idx) {
- if (!data[p][idx]) data[p][idx] = {};
- var condRoot = row.querySelector('.ie-cond-root');
- if (condRoot) {
- var cond = ie_collectCond(condRoot);
- if (cond) data[p][idx].condition = cond;
- else delete data[p][idx].condition;
- }
- var actRoot = row.querySelector('.ie-act-root');
- if (actRoot) {
- var act = ie_collectAct(actRoot);
- if (act) data[p][idx].action = act;
- else delete data[p][idx].action;
- }
- });
- });
-}
-
// ── Render condition ──
-function ie_renderCond(condObj, secId, idx, secPath) {
+function ie_renderCond(condObj, secId, idx, secPath, kind) {
+ kind = kind || 'entry';
var h = '';
var isGroup = condObj && (condObj.all_of || condObj.any_of);
if (condObj && typeof condObj === 'object') {
h += '<div class="ie-cond-root">';
- h += ie_renderGroup(condObj, secId, secPath, idx, '');
+ h += ie_renderGroup(condObj, secId, secPath, idx, '', kind);
h += '</div>';
}
- // For a group root, the group's own addbar (rendered inside _ie_renderGroupNode)
- // already covers appending — don't emit a duplicate top-level addbar.
var showAddbar = !isGroup;
if (showAddbar) {
h += '<div class="ie-cond-addbar">';
IE_COND_TYPES.forEach(function(ct) {
- h += '<button class="btn btn-sm ie-add-btn ie-cond-add-leaf" onclick="ie_addLeaf(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'\',\'' + ct.type + '\')">+ ' + esc(ct.label) + '</button>';
+ h += '<button class="btn btn-sm ie-add-btn ie-cond-add-leaf" onclick="ie_addLeaf(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\',\'\',\'' + ct.type + '\')">+ ' + esc(ct.label) + '</button>';
});
- h += '<button class="btn btn-sm ie-add-btn ie-cond-add-grp" onclick="ie_addGroup(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'\')">+ Group</button>';
+ h += '<button class="btn btn-sm ie-add-btn ie-cond-add-grp" onclick="ie_addGroup(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\',\'\')">+ Group</button>';
h += '</div>';
}
return h;
}
-function ie_renderGroup(cond, secId, secPath, idx, gpath) {
+function ie_renderGroup(cond, secId, secPath, idx, gpath, kind) {
var mode = null, children = null;
if (cond.all_of) { mode = 'all_of'; children = cond.all_of; }
else if (cond.any_of) { mode = 'any_of'; children = cond.any_of; }
@@ -336,74 +355,74 @@ function ie_renderGroup(cond, secId, secPath, idx, gpath) {
var not = !!cond.not;
if (mode && children) {
- return _ie_renderGroupNode(mode, children, not, secId, secPath, idx, gpath);
+ return _ie_renderGroupNode(mode, children, not, secId, secPath, idx, gpath, kind);
}
- // Leaf
- if (cond.global_flag !== undefined) return _ie_renderLeaf('global_flag', cond.global_flag, cond.value, not, secId, secPath, idx, gpath, IE_ROOT_LEAF);
- if (cond.player_flag !== undefined) return _ie_renderLeaf('player_flag', cond.player_flag, cond.value, not, secId, secPath, idx, gpath, IE_ROOT_LEAF);
- if (cond.has_item !== undefined) return _ie_renderLeaf('has_item', cond.has_item, '', not, secId, secPath, idx, gpath, IE_ROOT_LEAF);
- if (cond.min_credits !== undefined) return _ie_renderLeaf('min_credits', cond.min_credits, '', not, secId, secPath, idx, gpath, IE_ROOT_LEAF);
+ if (cond.global_flag !== undefined) return _ie_renderLeaf('global_flag', cond.global_flag, cond.value, not, secId, secPath, idx, gpath, IE_ROOT_LEAF, kind);
+ if (cond.player_flag !== undefined) return _ie_renderLeaf('player_flag', cond.player_flag, cond.value, not, secId, secPath, idx, gpath, IE_ROOT_LEAF, kind);
+ if (cond.room !== undefined) return _ie_renderLeaf('room', cond.room, '', not, secId, secPath, idx, gpath, IE_ROOT_LEAF, kind);
+ if (cond.has_item !== undefined) return _ie_renderLeaf('has_item', cond.has_item, '', not, secId, secPath, idx, gpath, IE_ROOT_LEAF, kind);
+ if (cond.min_credits !== undefined) return _ie_renderLeaf('min_credits', cond.min_credits, '', not, secId, secPath, idx, gpath, IE_ROOT_LEAF, kind);
return '';
}
-function _ie_renderGroupNode(mode, children, not, secId, secPath, idx, gpath) {
+function _ie_renderGroupNode(mode, children, not, secId, secPath, idx, gpath, kind) {
var h = '';
if (children.length === 0) {
- // Empty group: single line — addbar buttons left, X right.
h += '<div class="ie-cond ie-cond-empty" data-ie-gpath="' + escAttr(gpath) + '">';
h += '<div class="ie-cond-modebar ie-cond-empty-bar">';
IE_COND_TYPES.forEach(function(ct) {
- h += '<button class="btn btn-sm ie-add-btn ie-cond-add-leaf" onclick="ie_addLeaf(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(gpath) + '\',\'' + ct.type + '\')">+ ' + esc(ct.label) + '</button>';
+ h += '<button class="btn btn-sm ie-add-btn ie-cond-add-leaf" onclick="ie_addLeaf(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\',\'' + escAttr(gpath) + '\',\'' + ct.type + '\')">+ ' + esc(ct.label) + '</button>';
});
- h += '<button class="btn btn-sm ie-add-btn ie-cond-add-grp" onclick="ie_addGroup(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(gpath) + '\')">+ Group</button>';
- h += '<button class="btn btn-sm btn-danger ie-cond-grp-rm" onclick="ie_removeGroup(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(gpath) + '\')" title="Remove this group">X</button>';
+ h += '<button class="btn btn-sm ie-add-btn ie-cond-add-grp" onclick="ie_addGroup(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\',\'' + escAttr(gpath) + '\')">+ Group</button>';
+ h += '<button class="btn btn-sm btn-danger ie-cond-grp-rm" onclick="ie_removeGroup(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\',\'' + escAttr(gpath) + '\')" title="Remove this group">X</button>';
h += '</div>';
h += '</div>';
return h;
}
- // Non-empty group: modebar (no X) + children + addbar below.
h += '<div class="ie-cond" data-ie-gpath="' + escAttr(gpath) + '">';
h += '<div class="ie-cond-modebar">';
if (children.length >= 2) {
- h += '<select class="ie-cond-modesel" onchange="ie_toggleCondMode(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(gpath) + '\')">';
+ h += '<select class="ie-cond-modesel" onchange="ie_toggleCondMode(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\',\'' + escAttr(gpath) + '\')">';
h += '<option value="all_of"' + (mode === 'all_of' ? ' selected' : '') + '>All of</option>';
h += '<option value="any_of"' + (mode === 'any_of' ? ' selected' : '') + '>Any of</option>';
h += '</select>';
}
if (children.length >= 2 || not) {
- h += '<label class="ie-cond-notlbl"><input type="checkbox" class="ie-cond-not" onchange="ie_toggleCondNot(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(gpath) + '\')"' + (not ? ' checked' : '') + '> NOT</label>';
+ h += '<label class="ie-cond-notlbl"><input type="checkbox" class="ie-cond-not" onchange="ie_toggleCondNot(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\',\'' + escAttr(gpath) + '\')"' + (not ? ' checked' : '') + '> NOT</label>';
}
h += '</div>';
h += '<div class="ie-cond-children">';
children.forEach(function(child, ci) {
var cpath = gpath ? gpath + '-' + ci : String(ci);
if (child.all_of || child.any_of) {
- h += ie_renderGroup(child, secId, secPath, idx, cpath);
+ h += ie_renderGroup(child, secId, secPath, idx, cpath, kind);
} else {
var cval = '', csub = '', ctype = '', cnot = !!child.not;
if (child.global_flag !== undefined) { ctype = 'global_flag'; cval = child.global_flag; csub = child.value; }
else if (child.player_flag !== undefined) { ctype = 'player_flag'; cval = child.player_flag; csub = child.value; }
+ else if (child.room !== undefined) { ctype = 'room'; cval = child.room; }
else if (child.has_item !== undefined) { ctype = 'has_item'; cval = child.has_item; }
else if (child.min_credits !== undefined) { ctype = 'min_credits'; cval = child.min_credits; }
- if (ctype) h += _ie_renderLeaf(ctype, cval, csub, cnot, secId, secPath, idx, gpath, ci);
+ if (ctype) h += _ie_renderLeaf(ctype, cval, csub, cnot, secId, secPath, idx, gpath, ci, kind);
}
});
h += '</div>';
h += '<div class="ie-cond-addbar" data-ie-gpath="' + escAttr(gpath) + '">';
IE_COND_TYPES.forEach(function(ct) {
- h += '<button class="btn btn-sm ie-add-btn ie-cond-add-leaf" onclick="ie_addLeaf(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(gpath) + '\',\'' + ct.type + '\')">+ ' + esc(ct.label) + '</button>';
+ h += '<button class="btn btn-sm ie-add-btn ie-cond-add-leaf" onclick="ie_addLeaf(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\',\'' + escAttr(gpath) + '\',\'' + ct.type + '\')">+ ' + esc(ct.label) + '</button>';
});
- h += '<button class="btn btn-sm ie-add-btn ie-cond-add-grp" onclick="ie_addGroup(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(gpath) + '\')">+ Group</button>';
+ h += '<button class="btn btn-sm ie-add-btn ie-cond-add-grp" onclick="ie_addGroup(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\',\'' + escAttr(gpath) + '\')">+ Group</button>';
h += '</div>';
h += '</div>';
return h;
}
-function _ie_renderLeaf(leafType, leafVal, leafSub, not, secId, secPath, idx, gpath, ci) {
+function _ie_renderLeaf(leafType, leafVal, leafSub, not, secId, secPath, idx, gpath, ci, kind) {
+ kind = kind || 'entry';
var label = '';
IE_COND_TYPES.forEach(function(ct) { if (ct.type === leafType) label = ct.label; });
var hint = '';
@@ -420,7 +439,7 @@ function _ie_renderLeaf(leafType, leafVal, leafSub, not, secId, secPath, idx, gp
h += '<input class="ie-cond-val search-input" data-search-type="items" placeholder="' + escAttr(hint) + '" value="' + escAttr(sv) + '">';
h += '<div class="search-results" style="display:none"></div>';
h += '</div>';
- } else if (leafType === 'min_credits') {
+ } else if (leafType === 'min_credits' || leafType === 'room') {
h += '<input class="ie-cond-val" placeholder="' + escAttr(hint) + '" value="' + escAttr(sv) + '" type="number">';
} else {
h += '<input class="ie-cond-val" placeholder="' + escAttr(hint) + '" value="' + escAttr(sv) + '">';
@@ -429,64 +448,33 @@ function _ie_renderLeaf(leafType, leafVal, leafSub, not, secId, secPath, idx, gp
h += '<span class="ie-cond-eq">=</span>';
h += '<input class="ie-cond-subval" placeholder="exists" value="' + escAttr(ssv) + '">';
}
- h += '<label class="ie-cond-notlbl"><input type="checkbox" class="ie-cond-not" onchange="ie_toggleNotLeaf(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(gpath) + '\',' + ciToken + ')"' + (not ? ' checked' : '') + '> NOT</label>';
- h += '<button class="btn btn-sm btn-danger ie-cond-rm" onclick="ie_removeCond(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(gpath) + '\',' + ciToken + ')" title="Remove">X</button>';
+ h += '<label class="ie-cond-notlbl"><input type="checkbox" class="ie-cond-not" onchange="ie_toggleNotLeaf(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\',\'' + escAttr(gpath) + '\',' + ciToken + ')"' + (not ? ' checked' : '') + '> NOT</label>';
+ h += '<button class="btn btn-sm btn-danger ie-cond-rm" onclick="ie_removeCond(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(kind) + '\',\'' + escAttr(gpath) + '\',' + ciToken + ')" title="Remove">X</button>';
h += '</div>';
return h;
}
-// ── Render action ──
+// ── Render step effects (everything except messages and condition).
+// The add-bar is rendered separately via ie_renderStepAddbar. ──
-function ie_renderAct(actionObj, scope, secId, idx, secPath) {
+function ie_renderAct(stepObj, secId, idx, si, secPath) {
var hasAny = false;
var rows = '';
- // Messages — extendable list of {delay, message} entries. Mirrors the KV-section
- // pattern: when the 'messages' key is present we render the section with rows
- // and an internal + Add; when it goes away (last row removed) the section
- // disappears and the addbar + Add Message button reappears. There is no
- // section-level X.
- var msgsPresent = actionObj && Object.prototype.hasOwnProperty.call(actionObj, 'messages') && Array.isArray(actionObj.messages);
- if (msgsPresent) {
- var msgs = actionObj.messages;
- hasAny = true;
- var rmFn = 'ie_removeMessage(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ')';
- rows += '<div class="ie-act-kv" data-ie-act-key="messages">';
- rows += '<div class="ie-act-kv-header"><span>Messages</span></div>';
- rows += '<div class="ie-kv-list">';
- if (msgs.length === 0) {
- rows += '<div class="ie-kv-row"><input type="number" class="ie-act-msg-delay" value="" placeholder="delay" style="width:60px" title="Ticks before showing">';
- rows += '<input class="ie-act-msg" data-ie-msg-idx="0" value="" placeholder="Shown to player" style="flex:1">';
- rows += '<button class="btn btn-sm btn-danger" onclick="' + rmFn + '">X</button></div>';
- } else {
- msgs.forEach(function(m, mi) {
- rows += '<div class="ie-kv-row"><input type="number" class="ie-act-msg-delay" value="' + escAttr(m.delay ? String(m.delay) : '') + '" placeholder="delay" style="width:60px" title="Ticks before showing">';
- rows += '<input class="ie-act-msg" data-ie-msg-idx="' + mi + '" value="' + escAttr(String(m.message || '')) + '" placeholder="Shown to player" style="flex:1">';
- rows += '<button class="btn btn-sm btn-danger" onclick="' + rmFn + '">X</button></div>';
- });
- }
- rows += '</div>';
- rows += '<button class="btn btn-sm ie-add-btn" style="margin-top:2px" onclick="ie_addMessage(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ')">+ Add Message</button>';
- rows += '</div>';
- }
-
- // Standard effects
IE_ACT_TYPES.forEach(function(at) {
- if (!actionObj || !Object.prototype.hasOwnProperty.call(actionObj, at.key)) return;
- var val = actionObj[at.key];
+ if (!stepObj || !Object.prototype.hasOwnProperty.call(stepObj, at.key)) return;
+ var val = stepObj[at.key];
if (at.kind === 'checkbox' && val !== true) return;
hasAny = true;
if (at.kind === 'kv') {
var kvMap = (val && typeof val === 'object') ? val : {};
var kvKeys = Object.keys(kvMap);
- var kvRmAttr = 'ie_removeKVRow(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(at.key) + '\')';
+ var kvRmAttr = 'ie_removeKVRow(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',\'' + escAttr(at.key) + '\')';
rows += '<div class="ie-act-kv" data-ie-act-key="' + escAttr(at.key) + '">';
rows += '<div class="ie-act-kv-header"><span>' + esc(at.label) + '</span></div>';
rows += '<div class="ie-kv-list">';
if (kvKeys.length === 0) {
- // Start with a blank row so there are input boxes immediately. The
- // row's X acts as the section's only remove path (no header X).
rows += '<div class="ie-kv-row"><input value="" placeholder="key" class="ie-kv-key">';
rows += '<input value="" placeholder="val" class="ie-kv-val">';
rows += '<button class="btn btn-sm btn-danger" onclick="' + kvRmAttr + '">X</button></div>';
@@ -498,92 +486,239 @@ function ie_renderAct(actionObj, scope, secId, idx, secPath) {
});
}
rows += '</div>';
- rows += '<button class="btn btn-sm ie-add-btn" style="margin-top:2px" onclick="ie_addKVRow(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(at.key) + '\')">+ Add</button>';
+ rows += '<button class="btn btn-sm ie-add-btn" style="margin-top:2px" onclick="ie_addKVRow(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',\'' + escAttr(at.key) + '\')">+ Add</button>';
rows += '</div>';
} else if (at.kind === 'checkbox') {
rows += '<div class="ie-act-row"><span class="ie-act-label">' + esc(at.label) + '</span>';
rows += '<input type="checkbox" class="ie-act-input" data-ie-act-key="' + escAttr(at.key) + '" data-ie-act-kind="checkbox" checked hidden>';
- rows += '<button class="btn btn-sm btn-danger ie-act-rm" onclick="ie_removeActEffect(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(at.key) + '\')">X</button>';
+ rows += '<button class="btn btn-sm btn-danger ie-act-rm" onclick="ie_removeActEffect(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',\'' + escAttr(at.key) + '\')">X</button>';
rows += '</div>';
} else if (at.kind === 'search') {
rows += '<div class="ie-act-row"><span class="ie-act-label">' + esc(at.label) + '</span>';
rows += '<div class="obj-search" style="position:relative;min-width:0;flex:1"><input class="ie-act-input search-input" data-ie-act-key="' + escAttr(at.key) + '" data-search-type="' + escAttr(at.entity) + '" placeholder="' + escAttr(at.hint || 'Search ' + at.entity + '...') + '" value="' + escAttr(String(val)) + '"><div class="search-results" style="display:none"></div></div>';
- rows += '<button class="btn btn-sm btn-danger ie-act-rm" onclick="ie_removeActEffect(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(at.key) + '\')">X</button>';
+ rows += '<button class="btn btn-sm btn-danger ie-act-rm" onclick="ie_removeActEffect(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',\'' + escAttr(at.key) + '\')">X</button>';
rows += '</div>';
} else {
rows += '<div class="ie-act-row"><span class="ie-act-label">' + esc(at.label) + '</span>';
rows += '<input class="ie-act-input" data-ie-act-key="' + escAttr(at.key) + '"' + (at.kind === 'number' ? ' type="number"' : '') + ' data-ie-act-kind="' + escAttr(at.kind) + '" value="' + escAttr(String(val)) + '" placeholder="' + escAttr(at.hint || '') + '" style="flex:1">';
- rows += '<button class="btn btn-sm btn-danger ie-act-rm" onclick="ie_removeActEffect(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(at.key) + '\')">X</button>';
+ rows += '<button class="btn btn-sm btn-danger ie-act-rm" onclick="ie_removeActEffect(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',\'' + escAttr(at.key) + '\')">X</button>';
rows += '</div>';
}
});
- // Seq-only effects
- if (scope === 'seq') {
- IE_ACT_TYPES_SEQ.forEach(function(at) {
- if (!actionObj || !Object.prototype.hasOwnProperty.call(actionObj, at.key)) return;
- var val = actionObj[at.key];
- if (val === undefined || val === null) return;
- hasAny = true;
-
- if (at.kind === 'search') {
- rows += '<div class="ie-act-row"><span class="ie-act-label">' + esc(at.label) + '</span>';
- rows += '<div class="obj-search" style="position:relative;min-width:0;flex:1"><input class="ie-act-input search-input" data-ie-act-key="' + escAttr(at.key) + '" data-search-type="' + escAttr(at.entity) + '" placeholder="' + escAttr(at.hint || 'Search ' + at.entity + '...') + '" value="' + escAttr(String(val)) + '"><div class="search-results" style="display:none"></div></div>';
- rows += '<button class="btn btn-sm btn-danger ie-act-rm" onclick="ie_removeActEffect(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(at.key) + '\')">X</button>';
- rows += '</div>';
- } else {
- rows += '<div class="ie-act-row"><span class="ie-act-label">' + esc(at.label) + '</span>';
- rows += '<input class="ie-act-input" data-ie-act-key="' + escAttr(at.key) + '" value="' + escAttr(String(val)) + '" placeholder="' + escAttr(at.hint || '') + '" style="flex:1">';
- rows += '<button class="btn btn-sm btn-danger ie-act-rm" onclick="ie_removeActEffect(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(at.key) + '\')">X</button>';
- rows += '</div>';
- }
- });
+ if (hasAny) {
+ return '<div class="ie-act-root">' + rows + '</div>';
}
+ return '';
+}
+
+// ── Render messages sub-list (plain strings) ──
- // Add effect buttons
- var addbar = '<div class="ie-act-addbar">';
- if (!msgsPresent) {
- addbar += '<button class="btn btn-sm ie-add-btn" onclick="ie_addActEffect(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',\'messages\',\'' + escAttr(scope) + '\')">+ Add Message</button>';
+function ie_renderMessages(step, secId, idx, si, secPath) {
+ var present = step && Array.isArray(step.messages);
+ if (!present) {
+ return '<button class="btn btn-sm ie-add-btn" onclick="ie_addMessages(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ')">+ Add Message</button>';
}
- IE_ACT_TYPES.forEach(function(at) {
- if (actionObj && Object.prototype.hasOwnProperty.call(actionObj, at.key)) {
- if (at.kind === 'checkbox') {
- if (actionObj[at.key] === true) return;
- } else {
- return;
+ var msgs = step.messages;
+ var rmFn = 'ie_removeMessage(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ')';
+ var h = '<div class="ie-act-kv" data-ie-act-key="messages">';
+ h += '<div class="ie-act-kv-header"><span>Messages</span></div>';
+ h += '<div class="ie-kv-list">';
+ msgs.forEach(function(m, mi) {
+ var upBtn = mi > 0
+ ? '<button class="btn btn-sm ie-msg-up" onclick="ie_moveMessage(\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',' + mi + ',' + (mi - 1) + ')" title="Move up">&#9650;</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">&#9660;</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">&#9650;</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">&#9660;</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">&#9650;</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">&#9660;</button>';
+
+ if (allowItem) {
+ if (!showItem) {
+ h += '<button class="btn btn-sm ie-add-btn" onclick="' + ctx.showField(sec.id, idx, 'item_id') + '">+ Add Required Item</button>';
+ } else {
+ h += ctx.renderField(sec, sec.fields[0], data, idx, 'flex:1;min-width:0');
+ h += '<button class="btn btn-sm btn-danger ie-inter-field-x" onclick="' + ctx.hideField(sec.id, idx, 'item_id') + '" title="Remove required item">X</button>';
+ }
}
if (!condExists) {
- h += '<button class="btn btn-sm ie-add-btn" onclick="' + ctx.addCondition(sec.id, idx) + '">+ Add Condition</button>';
- }
- if (!actExists) {
- h += '<button class="btn btn-sm ie-add-btn" onclick="' + ctx.addEffect(sec.id, idx) + '">+ Add Action</button>';
+ h += '<button class="btn btn-sm ie-add-btn" onclick="ie_addRootConditionGroup(\'' + escAttr(sec.path) + '\',' + idx + ',\'entry\')">+ Add Condition</button>';
}
h += '<span class="ie-inter-header-spacer"></span>';
- h += '<button class="btn btn-sm btn-danger ie-inter-remove" onclick="' + ctx.showRemove(idx) + '">Remove Interaction</button>';
+ h += '<button class="btn btn-sm btn-danger ie-inter-remove" onclick="' + ctx.showRemove(idx) + '">Remove Trigger</button>';
h += '</div>';
h += '<div class="ie-inter-panel" style="display:' + (condExists ? 'block' : 'none') + '">';
- h += '<div class="ie-inter-panel-body">' + ie_renderCond(item.condition || null, sec.id, idx, sec.path) + '</div>';
+ h += '<div class="ie-inter-panel-body">' + ie_renderCond(item.condition || null, sec.id, idx, sec.path, 'entry') + '</div>';
h += '</div>';
- h += '<div class="ie-inter-panel" style="display:' + (actExists ? 'block' : 'none') + '">';
- h += '<div class="ie-inter-panel-body">' + ie_renderAct(item.action || null, sec.interScope || 'inline', sec.id, idx, sec.path) + '</div>';
+ h += '<div class="ie-inter-steps">';
+ h += ie_renderStepList(steps, sec.id, idx, sec.path);
h += '</div>';
h += '</div>';
});
- h += '<button class="btn btn-sm obj-sub-add" onclick="' + ctx.onAdd() + '">+ Add ' + esc(sec.label) + '</button>';
+ h += '<button class="btn btn-sm obj-sub-add" onclick="' + ctx.onAdd() + '">+ Add ' + esc(sec.label || 'Trigger') + '</button>';
return h;
}
@@ -1072,12 +1155,37 @@ function ie_hideField(visMap, secId, idx, key) {
visMap[secId][idx][key] = false;
}
+// ── Step-cleaning helper used by save paths ──
+
+function ie_cleanStep(step) {
+ var s = {};
+ if (step && step.condition) {
+ var c = ie_cleanCondition(step.condition);
+ if (c) s.condition = c;
+ }
+ if (step && step.wait !== undefined && step.wait !== null && step.wait !== 0) s.wait = step.wait;
+ if (step && Array.isArray(step.messages) && step.messages.filter(function(m){return m;}).length > 0) {
+ s.messages = step.messages.filter(function(m){return m;});
+ }
+ IE_ACT_TYPES.forEach(function(at) {
+ if (at.key === 'wait' || at.key === 'messages') return;
+ if (!step || !Object.prototype.hasOwnProperty.call(step, at.key)) return;
+ var v = step[at.key];
+ if (v === undefined || v === null || v === '') return;
+ if (typeof v === 'object') {
+ if (Object.keys(v).length === 0) return;
+ s[at.key] = v;
+ } else if (typeof v === 'boolean') {
+ if (!v) return;
+ s[at.key] = v;
+ } else {
+ s[at.key] = v;
+ }
+ });
+ return s;
+}
+
// ── Bindings (set by each editor) ──
-// _ieRenderCurrent: full sync + render. Used by chrome toggles
-// (show/hide panels, add row, remove row, add section, etc.).
-// _ieRenderOnly: render only, no sync. Used by ie_* mutation handlers
-// that have already synced + mutated the data and must not re-read the
-// stale DOM before the next render.
window._ieRenderCurrent = function() {};
-window._ieRenderOnly = function() {};
+window._ieRenderOnly = function() {}; \ No newline at end of file
diff --git a/internal/admin/static/map.js b/internal/admin/static/map.js
index 00230a4..ab20a86 100644
--- a/internal/admin/static/map.js
+++ b/internal/admin/static/map.js
@@ -575,6 +575,8 @@ function renderSidePanel(data) {
html += '<button class="panel-tab' + (currentPanelTab === 5 ? ' active' : '') + '" onclick="switchPanelTab(5)">Items</button>';
html += '<button class="panel-tab' + (currentPanelTab === 6 ? ' active' : '') + '" onclick="switchPanelTab(6)">Hazard</button>';
html += '<button class="panel-tab' + (currentPanelTab === 7 ? ' active' : '') + '" onclick="switchPanelTab(7)">Agility</button>';
+ html += '<button class="panel-tab' + (currentPanelTab === 8 ? ' active' : '') + '" onclick="switchPanelTab(8)">Enter/Exit</button>';
+ html += '<button class="panel-tab' + (currentPanelTab === 9 ? ' active' : '') + '" onclick="switchPanelTab(9)">Triggers</button>';
html += '</div>';
html += '<div class="panel-tab-content">';
@@ -595,6 +597,8 @@ function renderSidePanel(data) {
html += renderCourseTab();
}
}
+ else if (currentPanelTab === 8) html += renderEnterExitTab(r);
+ else if (currentPanelTab === 9) html += renderTriggersTab(r);
html += '</div>';
panel.innerHTML = html;
@@ -1401,6 +1405,8 @@ function switchPanelTab(idx) {
html = renderCourseTab();
}
}
+ else if (idx === 8) html = renderEnterExitTab(r);
+ else if (idx === 9) html = renderTriggersTab(r);
var content = document.querySelector('.panel-tab-content');
if (content) content.innerHTML = html;
var tabs = document.querySelectorAll('.panel-tab');
@@ -2096,6 +2102,10 @@ async function doSavePanel() {
r.mobs = currentRoomData.room.mobs;
r.exits = currentRoomData.room.exits;
r.hazard = currentRoomData.room.hazard || '';
+ r.on_enter = currentRoomData.room.on_enter;
+ r.on_exit = currentRoomData.room.on_exit;
+ r.on_flag_change = currentRoomData.room.on_flag_change;
+ r.on_global_flag_change = currentRoomData.room.on_global_flag_change;
if (r.block_transport === undefined) r.block_transport = currentRoomData.room.block_transport;
}
@@ -3350,3 +3360,222 @@ document.addEventListener('DOMContentLoaded', function() { loadRoomDirs(function
window.addEventListener('keydown', function(e) { if (isCtrlKey(e)) { ctrlHeld = true; if (dragging) refreshDragMode(true); } });
window.addEventListener('keyup', function(e) { if (isCtrlKey(e)) { ctrlHeld = false; if (dragging) refreshDragMode(false); } });
});
+
+// ── Enter/Exit tab ──
+// Renders on_enter and on_exit trigger lists, wiring the shared ie_* helpers
+// from intereditor.js so the same condition/step/message editors used by the
+// object/mob editors are reused here. Edits mutate currentRoomData.room.*
+// and the debounced autoSave() persists them.
+
+function renderEnterExitTab(r) {
+ var onEnter = Array.isArray(r.on_enter) ? r.on_enter : [];
+ var onExit = Array.isArray(r.on_exit) ? r.on_exit : [];
+ window._editorData = { on_enter: onEnter, on_exit: onExit };
+
+ window._ieRenderCurrent = function() {
+ if (window._ieSyncAll) window._ieSyncAll(window._editorData, [
+ {id: 'on_enter', path: 'on_enter', isArray: true, interactionStyle: true, itemIDAllowed: false, label: 'Trigger'},
+ {id: 'on_exit', path: 'on_exit', isArray: true, interactionStyle: true, itemIDAllowed: false, label: 'Trigger'}
+ ]);
+ if (currentRoomData && currentRoomData.room) {
+ currentRoomData.room.on_enter = window._editorData.on_enter;
+ currentRoomData.room.on_exit = window._editorData.on_exit;
+ }
+ switchPanelTab(8);
+ autoSave();
+ };
+ window._ieRenderOnly = window._ieRenderCurrent;
+ window._ieSyncAll = ie_syncAllInteractions;
+
+ var h = '<h3 style="margin:8px 0 4px">On-Enter Triggers</h3>';
+ h += '<p style="color:#888;font-size:11px;margin:0 0 6px">Fires when a player enters this room. First matching entry wins.</p>';
+ h += renderTriggerListHTML(r.on_enter, 'on_enter', 'on_enter', false);
+ h += '<h3 style="margin:16px 0 4px">On-Exit Triggers</h3>';
+ h += '<p style="color:#888;font-size:11px;margin:0 0 6px">Fires when a player leaves this room. First matching entry wins.</p>';
+ h += renderTriggerListHTML(r.on_exit, 'on_exit', 'on_exit', false);
+ return h;
+}
+
+// ── Triggers tab (on_flag_change / on_global_flag_change) ──
+
+function renderTriggersTab(r) {
+ var fc = Array.isArray(r.on_flag_change) ? r.on_flag_change : [];
+ var gfc = Array.isArray(r.on_global_flag_change) ? r.on_global_flag_change : [];
+ window._editorData = { on_flag_change: fc, on_global_flag_change: gfc };
+
+ window._ieRenderCurrent = function() {
+ if (window._ieSyncAll) window._ieSyncAll(window._editorData, [
+ {id: 'on_flag_change', path: 'on_flag_change', isArray: true, interactionStyle: true, itemIDAllowed: false, label: 'Trigger'},
+ {id: 'on_global_flag_change', path: 'on_global_flag_change', isArray: true, interactionStyle: true, itemIDAllowed: false, label: 'Trigger'}
+ ]);
+ if (currentRoomData && currentRoomData.room) {
+ currentRoomData.room.on_flag_change = window._editorData.on_flag_change;
+ currentRoomData.room.on_global_flag_change = window._editorData.on_global_flag_change;
+ }
+ switchPanelTab(9);
+ autoSave();
+ };
+ window._ieRenderOnly = window._ieRenderCurrent;
+ window._ieSyncAll = ie_syncAllInteractions;
+
+ var h = '<h3 style="margin:8px 0 4px">On-Flag-Change Triggers</h3>';
+ h += '<p style="color:#888;font-size:11px;margin:0 0 6px">Fires when a player flag changes. Use the condition\'s "In Room" gate to scope to this room (delete it to make global).</p>';
+ h += renderFlagTriggerListHTML(r.on_flag_change, 'on_flag_change', 'on_flag_change', 'player');
+ h += '<h3 style="margin:16px 0 4px">On-Global-Flag-Change Triggers</h3>';
+ h += '<p style="color:#888;font-size:11px;margin:0 0 6px">Fires when a global flag changes (no player involved).</p>';
+ h += renderFlagTriggerListHTML(r.on_global_flag_change, 'on_global_flag_change', 'on_global_flag_change', 'global');
+ return h;
+}
+
+// renderTriggerListHTML emits the HTML for a []Trigger block using the shared
+// ie_renderArraySection. It's a thin shim that builds the ctx the card
+// editors construct.
+function renderTriggerListHTML(arr, secId, secPath, allowItem) {
+ var visMap = {};
+ arr = Array.isArray(arr) ? arr : [];
+ var sec = {
+ id: secId,
+ path: secPath,
+ isArray: true,
+ interactionStyle: true,
+ itemIDAllowed: allowItem,
+ label: 'Trigger',
+ fields: [['item_id', 'Item ID', 'search', 'e.g. keycard', '', null, 'items']]
+ };
+ var ctx = {
+ sec: sec,
+ data: window._editorData,
+ visMap: visMap,
+ showField: function(sid, idx, key) {
+ ie_showField(visMap, sid, idx, key);
+ return 'void(0)';
+ },
+ hideField: function(sid, idx, key) {
+ ie_hideField(visMap, sid, idx, key);
+ window._ieRenderCurrent();
+ return 'void(0)';
+ },
+ addCondition: function(sid, idx) {
+ return 'ie_addRootConditionGroup(\'' + sid + '\',' + idx + ',\'entry\')';
+ },
+ showRemove: function(idx) {
+ return '_mapRemoveTrigger(\'' + secPath + '\',' + idx + ')';
+ },
+ renderField: function(s, field, data, idx, style) {
+ var val = (data[secPath] && data[secPath][idx] && data[secPath][idx].item_id) || '';
+ return '<div class="obj-search" style="position:relative;flex:1;min-width:0"><input class="search-input" data-field-path="' + secPath + '.' + idx + '.item_id" data-search-type="items" placeholder="item ID" value="' + escAttr(val) + '"><div class="search-results" style="display:none"></div></div>';
+ },
+ onAdd: function() {
+ return '_mapAddTrigger(\'' + secPath + '\')';
+ }
+ };
+ return ie_renderArraySection(ctx);
+}
+
+// renderFlagTriggerListHTML is like renderTriggerListHTML but adds the
+// on_player_flag / on_global_flag subscription header per entry.
+function renderFlagTriggerListHTML(arr, secId, secPath, kind) {
+ var visMap = {};
+ arr = Array.isArray(arr) ? arr : [];
+ var sec = {
+ id: secId,
+ path: secPath,
+ isArray: true,
+ interactionStyle: true,
+ itemIDAllowed: false,
+ label: 'Trigger',
+ fields: []
+ };
+ var flagKey = kind === 'player' ? 'on_player_flag' : 'on_global_flag';
+
+ var h = '';
+ arr.forEach(function(item, idx) {
+ visMap[idx] = visMap[idx] || {};
+ var condExists = !!(item.condition && typeof item.condition === 'object');
+ var lockVal = !!item.lock;
+ var steps = item.steps || [];
+ var flagName = item[flagKey] || '';
+
+ h += '<div class="obj-sub-row ie-inter-row" data-idx="' + idx + '">';
+ h += '<div class="ie-inter-header">';
+ h += '<label class="ie-lock-lbl"><input type="checkbox" class="ie-lock-cb"' + (lockVal ? ' checked' : '') + '> Lock Player</label>';
+ if (idx > 0) h += '<button class="btn btn-sm ie-trig-up" onclick="ie_moveRow(\'' + escAttr(secPath) + '\',' + idx + ',' + (idx - 1) + ')" title="Move up">&#9650;</button>';
+ if (idx < arr.length - 1) h += '<button class="btn btn-sm ie-trig-dn" onclick="ie_moveRow(\'' + escAttr(secPath) + '\',' + idx + ',' + (idx + 1) + ')" title="Move down">&#9660;</button>';
+ h += '<span class="ie-cond-leaf-label">' + esc(kind === 'player' ? 'Player Flag' : 'Global Flag') + '</span>';
+ h += '<input class="ie-flag-name" data-ie-flag-key="' + escAttr(flagKey) + '" data-idx="' + idx + '" value="' + escAttr(flagName) + '" placeholder="' + escAttr(kind === 'player' ? 'player flag name' : 'global flag name') + '" style="flex:1;min-width:80px" oninput="_mapSyncFlag(\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(flagKey) + '\')">';
+ h += '<span class="ie-cond-eq">=</span>';
+ h += '<input class="ie-flag-value" data-idx="' + idx + '" value="' + escAttr(item.value !== undefined ? String(item.value) : '') + '" placeholder="exists" style="width:60px" oninput="_mapSyncFlag(\'' + escAttr(secPath) + '\',' + idx + ',\'' + escAttr(flagKey) + '\')">';
+ h += '<span class="ie-inter-header-spacer"></span>';
+ if (!condExists) {
+ h += '<button class="btn btn-sm ie-add-btn" onclick="ie_addRootConditionGroup(\'' + escAttr(secPath) + '\',' + idx + ',\'entry\')">+ Add Condition</button>';
+ }
+ h += '<button class="btn btn-sm btn-danger ie-inter-remove" onclick="_mapRemoveTrigger(\'' + escAttr(secPath) + '\',' + idx + ')">Remove Trigger</button>';
+ h += '</div>';
+ h += '<div class="ie-inter-panel" style="display:' + (condExists ? 'block' : 'none') + '">';
+ h += '<div class="ie-inter-panel-body">' + ie_renderCond(item.condition || null, secId, idx, secPath, 'entry') + '</div>';
+ h += '</div>';
+ h += '<div class="ie-inter-steps">';
+ h += ie_renderStepList(steps, secId, idx, secPath);
+ h += '</div>';
+ h += '</div>';
+ });
+ h += '<button class="btn btn-sm obj-sub-add" onclick="_mapAddFlagTrigger(\'' + escAttr(secPath) + '\',\'' + escAttr(flagKey) + '\')">+ Add Trigger</button>';
+ return h;
+}
+
+// _mapAddTrigger / _mapRemoveTrigger / _mapAddFlagTrigger: mutation helpers
+// called from the inline onclick handlers. They sync the DOM, splice the
+// _editorData array, and re-render.
+function _mapAddTrigger(secPath) {
+ if (!window._editorData) return;
+ if (window._ieSyncAll) window._ieSyncAll(window._editorData, []);
+ if (!window._editorData[secPath]) window._editorData[secPath] = [];
+ window._editorData[secPath].push({steps: []});
+ if (currentRoomData && currentRoomData.room) {
+ currentRoomData.room[secPath] = window._editorData[secPath];
+ }
+ window._ieRenderCurrent();
+}
+
+function _mapAddFlagTrigger(secPath, flagKey) {
+ if (!window._editorData) return;
+ if (!window._editorData[secPath]) window._editorData[secPath] = [];
+ var entry = {steps: []};
+ entry[flagKey] = '';
+ window._editorData[secPath].push(entry);
+ if (currentRoomData && currentRoomData.room) {
+ currentRoomData.room[secPath] = window._editorData[secPath];
+ }
+ window._ieRenderCurrent();
+}
+
+function _mapRemoveTrigger(secPath, idx) {
+ if (!window._editorData || !window._editorData[secPath]) return;
+ window._editorData[secPath].splice(idx, 1);
+ if (currentRoomData && currentRoomData.room) {
+ currentRoomData.room[secPath] = window._editorData[secPath];
+ }
+ window._ieRenderCurrent();
+}
+
+function _mapSyncFlag(secPath, idx, flagKey) {
+ if (!window._editorData || !window._editorData[secPath]) return;
+ var row = document.querySelector('.ie-inter-row[data-idx="' + idx + '"]');
+ if (!row) return;
+ var nameEl = row.querySelector('.ie-flag-name');
+ var valEl = row.querySelector('.ie-flag-value');
+ if (nameEl) {
+ window._editorData[secPath][idx][flagKey] = nameEl.value.trim();
+ }
+ if (valEl) {
+ var v = valEl.value.trim();
+ if (v === '') delete window._editorData[secPath][idx].value;
+ else {
+ try { window._editorData[secPath][idx].value = JSON.parse(v); } catch(e) { window._editorData[secPath][idx].value = v; }
+ }
+ }
+ if (currentRoomData && currentRoomData.room) {
+ currentRoomData.room[secPath] = window._editorData[secPath];
+ }
+ autoSave();
+}
diff --git a/internal/admin/static/mobeditor.js b/internal/admin/static/mobeditor.js
index c2364c8..463af0a 100644
--- a/internal/admin/static/mobeditor.js
+++ b/internal/admin/static/mobeditor.js
@@ -121,12 +121,10 @@ var MOB_SECTIONS = [
]
},
{
- id: 'on_kill', label: 'On Kill', labelHint: '(fired when this mob is defeated or its task completes — additive over drops)', path: 'on_kill', isArray: true, fullWidth: true, interactionStyle: true, interScope: 'inline',
+ id: 'on_kill', label: 'On Kill', labelHint: '(fired when this mob is defeated or its task completes — additive over drops)', path: 'on_kill', isArray: true, fullWidth: true, interactionStyle: true,
detect: function(d) { return d.on_kill && Array.isArray(d.on_kill); },
fields: [
['item_id', 'Item ID', 'search', '(optional — only fires if you wield this weapon when the mob is defeated)', '', null, 'items'],
- ['condition', 'Condition', 'inter_cond'],
- ['action', 'Action', 'inter_act'],
]
},
];
@@ -813,8 +811,9 @@ function saveMob() {
var cleanedCond = ie_cleanCondition(dataItem.condition);
if (cleanedCond) item.condition = cleanedCond;
}
- if (dataItem.action && typeof dataItem.action === 'object') {
- item.action = dataItem.action;
+ if (dataItem.lock) item.lock = true;
+ if (Array.isArray(dataItem.steps)) {
+ item.steps = dataItem.steps.map(ie_cleanStep).filter(function(s) { return s && Object.keys(s).length > 0; });
}
}
if (Object.keys(item).length > 0) arr.push(item);
diff --git a/internal/admin/static/objecteditor.js b/internal/admin/static/objecteditor.js
index 9c5a279..1abccbf 100644
--- a/internal/admin/static/objecteditor.js
+++ b/internal/admin/static/objecteditor.js
@@ -86,21 +86,17 @@ var SECTIONS = [
]
},
{
- id: 'on_use', label: 'On Use', labelHint: '(what happens when you use items on this)', path: 'on_use', isArray: true, fullWidth: true, interactionStyle: true, interScope: 'inline',
+ id: 'on_use', label: 'On Use', labelHint: '(what happens when you use items on this)', path: 'on_use', isArray: true, fullWidth: true, interactionStyle: true,
detect: function(d) { return d.on_use && Array.isArray(d.on_use); },
fields: [
['item_id', 'Item ID', 'search', 'e.g. keycard (empty = bare use)', '', null, 'items'],
- ['condition', 'Condition', 'inter_cond'],
- ['action', 'Action', 'inter_act'],
]
},
{
- id: 'on_look', label: 'On Look', labelHint: '(what happens when you "look <this>")', path: 'on_look', isArray: true, fullWidth: true, interactionStyle: true, interScope: 'inline',
+ id: 'on_look', label: 'On Look', labelHint: '(what happens when you "look <this>")', path: 'on_look', isArray: true, fullWidth: true, interactionStyle: true,
detect: function(d) { return d.on_look && Array.isArray(d.on_look); },
fields: [
['item_id', 'Item ID', 'search', '(optional — only fires if you carry this item)', '', null, 'items'],
- ['condition', 'Condition', 'inter_cond'],
- ['action', 'Action', 'inter_act'],
]
},
];
@@ -517,8 +513,6 @@ function renderArraySection(sec, data) {
showRemove: function(idx) { return 'removeArrayItem(\'' + sec.id + '\',' + idx + ')'; },
showField: function(secId, idx, key) { return 'showInterField(\'' + secId + '\',' + idx + ',\'' + key + '\')'; },
hideField: function(secId, idx, key) { return 'hideInterField(\'' + secId + '\',' + idx + ',\'' + key + '\')'; },
- addCondition: function(secId, idx) { return 'addInterRootCondition(\'' + secId + '\',' + idx + ')'; },
- addEffect: function(secId, idx) { return 'addInterAction(\'' + secId + '\',' + idx + ')'; },
renderField: function(sec, f, data, idx, optStyle) { return renderField(sec, f, data, idx, null, optStyle); },
onAdd: function() { return 'addArrayItem(\'' + sec.id + '\')'; },
});
@@ -822,10 +816,15 @@ function addArrayItem(cardID) {
var sec = SECTIONS.find(function(s) { return s.id === cardID; });
if (!sec || !sec.isArray) return;
var arr = objectData[sec.path] || [];
- var empty = {};
- sec.fields.forEach(function(f) {
- empty[f[0]] = f[2] === 'checkbox' ? false : (f[2] === 'number' ? 0 : '');
- });
+ var empty;
+ if (sec.interactionStyle) {
+ empty = {steps: []};
+ } else {
+ empty = {};
+ sec.fields.forEach(function(f) {
+ empty[f[0]] = f[2] === 'checkbox' ? false : (f[2] === 'number' ? 0 : '');
+ });
+ }
arr.push(empty);
objectData[sec.path] = arr;
renderCurrent();
@@ -915,13 +914,7 @@ function showInterField(secId, idx, key) {
function addInterRootCondition(secId, idx) {
var sec = SECTIONS.find(function(s) { return s.id === secId; });
if (!sec) return;
- ie_addRootConditionGroup(sec.path, idx);
-}
-
-function addInterAction(secId, idx) {
- var sec = SECTIONS.find(function(s) { return s.id === secId; });
- if (!sec) return;
- ie_addEffectToInteraction(sec.path, idx);
+ ie_addRootConditionGroup(sec.path, idx, 'entry');
}
function hideInterField(secId, idx, key) {
@@ -932,8 +925,8 @@ function hideInterField(secId, idx, key) {
ced_syncDOMToData(objectData);
ie_syncAllInteractions(objectData, SECTIONS);
if (objectData[secPath] && objectData[secPath][idx]) {
- if (key === 'condition' || key === 'action') {
- delete objectData[secPath][idx][key];
+ if (key === 'condition') {
+ delete objectData[secPath][idx].condition;
} else {
objectData[secPath][idx][key] = '';
}
@@ -1030,8 +1023,10 @@ function saveObject() {
var cleanedCond = ie_cleanCondition(dataItem.condition);
if (cleanedCond) item.condition = cleanedCond;
}
- if (dataItem.action && typeof dataItem.action === 'object') {
- item.action = dataItem.action;
+ if (dataItem.lock) item.lock = true;
+ if (dataItem.steps && Array.isArray(dataItem.steps)) {
+ var cleanedSteps = dataItem.steps.map(ie_cleanStep).filter(function(s) { return Object.keys(s).length > 0; });
+ if (cleanedSteps.length > 0) item.steps = cleanedSteps;
}
}
if (Object.keys(item).length > 0) arr.push(item);
diff --git a/internal/admin/templates/map.html b/internal/admin/templates/map.html
index 0915aaa..79cd14f 100644
--- a/internal/admin/templates/map.html
+++ b/internal/admin/templates/map.html
@@ -50,5 +50,7 @@
</div>
</div>
</div>
+<script src="/static/cardeditor.js"></script>
+<script src="/static/intereditor.js"></script>
<script src="/static/map.js"></script>
{{end}}