aboutsummaryrefslogtreecommitdiff
path: root/internal/admin/static
diff options
context:
space:
mode:
Diffstat (limited to 'internal/admin/static')
-rw-r--r--internal/admin/static/admin.css2
-rw-r--r--internal/admin/static/cardeditor.js4
-rw-r--r--internal/admin/static/intereditor.js4
-rw-r--r--internal/admin/static/map.js149
-rw-r--r--internal/admin/static/mobeditor.js1
5 files changed, 156 insertions, 4 deletions
diff --git a/internal/admin/static/admin.css b/internal/admin/static/admin.css
index 63a12e5..3d54e54 100644
--- a/internal/admin/static/admin.css
+++ b/internal/admin/static/admin.css
@@ -199,6 +199,8 @@ g.ud-hover:hover text{font-weight:bold}
.cm-menu{position:fixed;z-index:200;background:var(--panel);border:1px solid var(--border);border-radius:4px;padding:4px 0;min-width:140px;box-shadow:0 4px 16px rgba(0,0,0,.4)}
.cm-menu button:not(.btn){display:block;width:100%;padding:6px 12px;text-align:left;font-size:12px;font-family:monospace;background:none;border:none;color:var(--text);cursor:pointer}
.cm-menu button:not(.btn):hover{background:var(--accent)}
+.cm-menu button:not(.btn):disabled{opacity:.4;cursor:default;background:transparent}
+.cm-menu button:not(.btn):disabled:hover{background:transparent}
.cm-menu button.danger{color:#f55}
.cm-menu button.danger:hover{background:var(--danger)}
.cm-menu .cm-title{font-size:11px;color:#aaa;text-transform:uppercase;letter-spacing:.5px;padding:4px 12px 6px;border-bottom:1px solid var(--border);margin-bottom:4px}
diff --git a/internal/admin/static/cardeditor.js b/internal/admin/static/cardeditor.js
index cf3a8b7..83eed2e 100644
--- a/internal/admin/static/cardeditor.js
+++ b/internal/admin/static/cardeditor.js
@@ -682,6 +682,10 @@ function ced_renderTagHelp(type) {
h += 'Message fields support template variables:<br>%i1 = first ingredient name<br>%i2 = second ingredient name<br>%n = output item name.<br>Color tags are also supported: {NN}text{/} where NN is a hex color index (00–FF).';
} else if (type === 'gather') {
h += 'Message fields support template variables: {name} = object name.<br/>Color tags are also supported: {NN}text{/} where NN is a hex color index (00–FF).';
+ } else if (type === 'trigger') {
+ h += 'Supports %p (player) %v (flag value) and color tags: {NN}text{/} where NN = hex index 00–FF (bold, dim, underline).';
+ } else if (type === 'talk') {
+ h += 'Supports color tags: {NN}text{/} where NN = hex index 00–FF (bold, dim, underline). Surround NPC speech in "quotes" for automatic dialog color.';
}
h += '</div>';
return h;
diff --git a/internal/admin/static/intereditor.js b/internal/admin/static/intereditor.js
index da1812d..2023874 100644
--- a/internal/admin/static/intereditor.js
+++ b/internal/admin/static/intereditor.js
@@ -707,6 +707,9 @@ function ie_renderAct(stepObj, secId, idx, si, secPath) {
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(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ',\'' + escAttr(at.key) + '\')">X</button>';
rows += '</div>';
+ if (at.kind === 'text') {
+ rows += ced_renderTagHelp('trigger');
+ }
}
});
@@ -739,6 +742,7 @@ function ie_renderMessages(step, secId, idx, si, secPath) {
});
h += '</div>';
h += '<button class="btn btn-sm ie-add-btn" style="margin-top:2px" onclick="ie_addMessage(this,\'' + escAttr(secId) + '\',\'' + escAttr(secPath) + '\',' + idx + ',' + si + ')">+ Add Message</button>';
+ h += ced_renderTagHelp('trigger');
h += '</div>';
return h;
}
diff --git a/internal/admin/static/map.js b/internal/admin/static/map.js
index 259fac6..f8cee51 100644
--- a/internal/admin/static/map.js
+++ b/internal/admin/static/map.js
@@ -8,6 +8,7 @@ var linkDragOneWay = false;
var ctrlHeld = false;
var delOneWay = false;
var delDrag = false, delFromId = null, delTargetId = null;
+var roomClip = null; // { features:{...} }
var roomMap = {}, occupied = {};
var multiSelectedIds = new Set();
var multiDrag = false, multiSubtract = false, multiStartSVGX = 0, multiStartSVGY = 0;
@@ -2077,16 +2078,16 @@ async function doSavePanel() {
var idChanged = false;
var dirChanged = false;
- if (dirEl && newDir !== currentDir) {
- await API.post('/api/rooms/move', {id: id, dir: newDir});
- dirChanged = true;
- }
if (newID !== id) {
await API.post('/api/rooms/rename', {id: id, new_id: newID});
selectedRoom = newID;
id = newID;
idChanged = true;
}
+ if (dirEl && newDir !== currentDir) {
+ await API.post('/api/rooms/move', {id: id, dir: newDir});
+ dirChanged = true;
+ }
await API.put('/api/rooms/' + id, r);
notify('Room #' + id + ' saved', 'success');
@@ -2098,10 +2099,39 @@ async function doSavePanel() {
selectRoom(id);
}
} catch (e) {
+ if (selectedRoom) {
+ var idEl = $('#roomID');
+ if (idEl) idEl.value = String(selectedRoom);
+ }
notify('Save failed: ' + extractError(e), 'error');
}
}
+function copyRoomFeatures(id) {
+ API.get('/api/rooms/' + id).then(function(data) {
+ if (!data || !data.room) { notify('Copy failed: no room', 'error'); return; }
+ var feats = JSON.parse(JSON.stringify(data.room));
+ delete feats.id; delete feats.exits;
+ roomClip = { features: feats, sourceId: id };
+ notify('Copied Room #' + id, 'success');
+ }).catch(function(e) { notify('Copy failed: ' + extractError(e), 'error'); });
+}
+
+function pasteRoomFeatures(targetId) {
+ if (!roomClip) { notify('Nothing to paste', 'error'); return; }
+ API.get('/api/rooms/' + targetId).then(function(data) {
+ if (!data || !data.room) { notify('Paste failed: target room not found', 'error'); return; }
+ var merged = JSON.parse(JSON.stringify(roomClip.features));
+ merged.id = data.room.id;
+ merged.exits = data.room.exits || {};
+ return API.put('/api/rooms/' + targetId, merged);
+ }).then(function() {
+ notify('Pasted onto Room #' + targetId, 'success');
+ updateUndoBar();
+ loadMap().then(function() { if (selectedRoom === targetId) selectRoom(targetId); });
+ }).catch(function(e) { notify('Paste failed: ' + extractError(e), 'error'); });
+}
+
function deleteRoom(id) {
var cb = document.getElementById('confirmDelete');
if (!cb || cb.checked) {
@@ -2231,6 +2261,18 @@ function showRoomContextMenu(x, y, id) {
menu.style.top = y + 'px';
menu.oncontextmenu = function(e) { e.preventDefault(); };
+ var copyBtn = document.createElement('button');
+ copyBtn.textContent = 'Copy';
+ copyBtn.onclick = function() { closeAll(); copyRoomFeatures(id); };
+ menu.appendChild(copyBtn);
+
+ if (roomClip) {
+ var pasteBtn = document.createElement('button');
+ pasteBtn.textContent = 'Paste (Room #' + roomClip.sourceId + ')';
+ pasteBtn.onclick = function() { closeAll(); pasteRoomFeatures(id); };
+ menu.appendChild(pasteBtn);
+ }
+
var delBtn = document.createElement('button');
delBtn.textContent = 'Delete Room';
delBtn.className = 'danger';
@@ -2242,6 +2284,7 @@ function showRoomContextMenu(x, y, id) {
menu.appendChild(buildSubmenuItem('Insert Room', id, 'insert', closeAll, overlay, menu));
menu.appendChild(buildSubmenuItem('Remove Room', id, 'remove', closeAll, overlay, menu));
+ menu.appendChild(buildSwapSubmenuItem('Swap', id, closeAll, overlay, menu));
var selBtn = document.createElement('button');
selBtn.textContent = 'Room Details';
@@ -2408,6 +2451,94 @@ function buildSubmenuItem(label, id, mode, closeAll, overlay, menu) {
return item;
}
+// buildSwapSubmenuItem builds a "Swap" parent row whose nested submenu lists
+// every direction the room has an exit in (matching roomDirs ordering).
+function buildSwapSubmenuItem(label, id, closeAll, overlay, menu) {
+ var dirs = roomDirs(id);
+ var empty = dirs.length === 0;
+
+ var item = document.createElement('div');
+ item.className = 'cm-item-has-sub' + (empty ? ' disabled' : '');
+
+ var btn = document.createElement('button');
+ btn.textContent = label;
+ var arrow = document.createElement('span');
+ arrow.className = 'cm-arrow';
+ arrow.textContent = '\u25B6';
+ item.appendChild(btn);
+ item.appendChild(arrow);
+
+ if (empty) {
+ btn.onclick = function() {
+ notify('No exits to swap from #' + id, 'error');
+ };
+ return item;
+ }
+
+ var sub = document.createElement('div');
+ sub.className = 'cm-submenu';
+ var title = document.createElement('div');
+ title.className = 'cm-title';
+ title.textContent = 'Swap Room';
+ sub.appendChild(title);
+ dirs.forEach(function(d) {
+ var opt = document.createElement('button');
+ opt.textContent = dirLabel(d);
+ (function(dir) {
+ opt.onclick = function(e) {
+ e.stopPropagation();
+ closeAll();
+ swapRooms(id, dir);
+ };
+ })(d);
+ sub.appendChild(opt);
+ });
+ item.appendChild(sub);
+
+ var open = false;
+ function showSub() {
+ if (overlay._subHideTimer) { clearTimeout(overlay._subHideTimer); overlay._subHideTimer = null; }
+ var sibs = menu.querySelectorAll('.cm-submenu.open');
+ sibs.forEach(function(s) {
+ if (s !== sub) { s.classList.remove('open'); s.classList.remove('flip-left'); s.classList.remove('flip-up'); }
+ });
+ sub.classList.add('open');
+ flipSubmenu(sub);
+ open = true;
+ }
+ function hideSubSoon() {
+ if (overlay._subHideTimer) clearTimeout(overlay._subHideTimer);
+ overlay._subHideTimer = setTimeout(function() {
+ sub.classList.remove('open');
+ sub.classList.remove('flip-left');
+ sub.classList.remove('flip-up');
+ open = false;
+ overlay._subHideTimer = null;
+ }, 250);
+ }
+
+ item.addEventListener('mouseenter', showSub);
+ item.addEventListener('mouseleave', hideSubSoon);
+ sub.addEventListener('mouseenter', function() {
+ if (overlay._subHideTimer) { clearTimeout(overlay._subHideTimer); overlay._subHideTimer = null; }
+ });
+ sub.addEventListener('mouseleave', hideSubSoon);
+
+ btn.onclick = function(e) {
+ e.stopPropagation();
+ if (open) {
+ sub.classList.remove('open');
+ sub.classList.remove('flip-left');
+ sub.classList.remove('flip-up');
+ open = false;
+ } else {
+ showSub();
+ }
+ };
+
+ return item;
+}
+
// roomDirs returns the room's outward exits (horizontal + up/down) derived from
// mapData.links and mapData.{upLinks,downLinks}. Horizontal exits are ordered by
// gridDirs; up/down appear last. Empty when the room has no exits.
@@ -2502,6 +2633,16 @@ function removeRoom(fromID, dir) {
}).catch(function(e) { notify('Remove failed: ' + extractError(e), 'error'); });
}
+// swapRooms swaps the room contents (everything except id and exits) of
+// fromID with the room in the given direction.
+function swapRooms(fromID, dir) {
+ API.post('/api/rooms/swap', { from: fromID, dir: dir }).then(function(r) {
+ notify('Swapped #' + fromID + ' with #' + r.b + ' (' + dirLabel(dir) + ')', 'success');
+ updateUndoBar();
+ loadMap().then(function() { selectRoom(fromID); });
+ }).catch(function(e) { notify('Swap failed: ' + extractError(e), 'error'); });
+}
+
function mouseToGrid(e) {
var svgEl = $('#mapSvg');
if (!svgEl) return null;
diff --git a/internal/admin/static/mobeditor.js b/internal/admin/static/mobeditor.js
index 2bef881..849dcf9 100644
--- a/internal/admin/static/mobeditor.js
+++ b/internal/admin/static/mobeditor.js
@@ -284,6 +284,7 @@ function renderMobCard(sec, data) {
if (sec.id === 'talk') {
h += '<div id="talktree"></div>';
+ h += ced_renderTagHelp('talk');
}
}
h += '</div>';