aboutsummaryrefslogtreecommitdiff
path: root/internal/admin/static
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-07-06 17:41:12 -0400
committerhistoria <[not public]>2026-07-06 17:41:12 -0400
commit00554b66df9310073c3b5e1aebd125ea54d9d554 (patch)
tree27ce9751867a00a31d22b08452ddf08a3b4e1513 /internal/admin/static
parent94823b52168b44894fb5e9c960358ed02ebb122b (diff)
downloadthehouseoficarus-00554b66df9310073c3b5e1aebd125ea54d9d554.tar.gz
feat: shift+drag to create one-way links on admin map
Diffstat (limited to 'internal/admin/static')
-rw-r--r--internal/admin/static/admin.css2
-rw-r--r--internal/admin/static/map.js99
2 files changed, 66 insertions, 35 deletions
diff --git a/internal/admin/static/admin.css b/internal/admin/static/admin.css
index 376bf23..c620b46 100644
--- a/internal/admin/static/admin.css
+++ b/internal/admin/static/admin.css
@@ -51,6 +51,8 @@ body{font-family:monospace;background:var(--bg);color:var(--text);display:flex;f
.z-controls .z-sep{width:1px;height:20px;background:var(--border);flex-shrink:0}
.z-dir-form{display:none;position:absolute;left:0;top:calc(100% + 4px);background:var(--panel);border:1px solid var(--border);border-radius:4px;padding:8px 10px;z-index:11;gap:6px;align-items:center;white-space:nowrap}
.room-tooltip{position:absolute;background:var(--panel);border:1px solid var(--border);padding:6px 10px;border-radius:3px;font-size:11px;pointer-events:none;z-index:20;white-space:nowrap}
+.link-mode-badge{position:absolute;bottom:12px;right:12px;padding:6px 12px;border-radius:4px;font-size:12px;font-family:monospace;pointer-events:none;z-index:20;background:rgba(30,30,60,0.9);color:#ccc;border:1px solid var(--border)}
+.link-mode-badge.oneway{background:rgba(80,50,20,0.92);color:#fca;border:1px solid #a64}
.notification{position:fixed;bottom:16px;right:16px;background:var(--panel);border:1px solid var(--border);padding:10px 16px;border-radius:4px;font-size:12px;z-index:100;animation:fadeIn .2s}
.notification.error{border-color:var(--danger)}
.notification.success{border-color:var(--success)}
diff --git a/internal/admin/static/map.js b/internal/admin/static/map.js
index f0b4d87..f89a434 100644
--- a/internal/admin/static/map.js
+++ b/internal/admin/static/map.js
@@ -4,6 +4,8 @@ var panX = 0, panY = 0, scale = 1;
var dragging = false, startX = 0, startY = 0, prevX = 0, prevY = 0;
var dragRoom = false;
var linkDrag = false, linkFromId = null, linkTargetId = null, linkGhostDir = null;
+var linkDragOneWay = false;
+var shiftHeld = false;
var delDrag = false, delFromId = null, delTargetId = null;
var roomMap = {}, occupied = {};
var dirChangedByUser = false;
@@ -35,6 +37,7 @@ function onMapMouseMove(e) {
if (linkFromId && moved) {
linkDrag = true;
+ showDragBadge(linkDragOneWay ? 'One-way link' : 'Two-way link');
updateLinkDragTarget(e);
}
}
@@ -62,7 +65,7 @@ function onMapMouseUp(e) {
}
if (linkDrag && linkFromId) {
- if (linkTargetId) { createLink(linkFromId, linkTargetId); }
+ if (linkTargetId) { createLink(linkFromId, linkTargetId, linkDragOneWay); }
else if (linkGhostDir) { createRoom(linkFromId, linkGhostDir); }
} else if (!moved && !delDrag) {
if (e.target.classList.contains('rm')) {
@@ -74,8 +77,9 @@ function onMapMouseUp(e) {
clearLinkHighlight();
clearDelHighlight();
+ hideDragBadge();
dragging = false; dragRoom = false;
- linkDrag = false; linkFromId = null; linkTargetId = null; linkGhostDir = null;
+ linkDrag = false; linkFromId = null; linkTargetId = null; linkGhostDir = null; linkDragOneWay = false;
delDrag = false; delFromId = null; delTargetId = null;
}
@@ -86,11 +90,25 @@ function cancelMapDrag() {
_mapMouseCleanup = false;
clearLinkHighlight();
clearDelHighlight();
+ hideDragBadge();
dragging = false; dragRoom = false;
- linkDrag = false; linkFromId = null; linkTargetId = null; linkGhostDir = null;
+ linkDrag = false; linkFromId = null; linkTargetId = null; linkGhostDir = null; linkDragOneWay = false;
delDrag = false; delFromId = null; delTargetId = null;
}
+function showDragBadge(text) {
+ var badge = document.getElementById('linkModeBadge');
+ if (!badge) return;
+ badge.textContent = text;
+ badge.className = 'link-mode-badge' + (linkDragOneWay ? ' oneway' : '');
+ badge.style.display = '';
+}
+
+function hideDragBadge() {
+ var badge = document.getElementById('linkModeBadge');
+ if (badge) badge.style.display = 'none';
+}
+
function changeZ(dz) {
if (dz === 0) currentZ = 0; else currentZ += dz;
panX = 0; panY = 0; scale = 1;
@@ -299,6 +317,7 @@ function renderMap(data) {
return;
}
+ e.preventDefault();
dragRoom = onRoom || onGhost;
dragging = true;
startX = e.clientX; startY = e.clientY;
@@ -307,6 +326,7 @@ function renderMap(data) {
linkDrag = false;
linkTargetId = null;
linkGhostDir = null;
+ linkDragOneWay = onRoom ? (e.shiftKey || shiftHeld) : false;
delDrag = false; delFromId = null; delTargetId = null;
clearLinkHighlight();
document.addEventListener('mousemove', onMapMouseMove);
@@ -1160,13 +1180,16 @@ function renderExitsSection(r) {
var setPlayerFlags = (typeof exit === 'object' && exit.set_player_flags) ? exit.set_player_flags : null;
var hidden = (typeof exit === 'object' && exit.hidden) ? exit.hidden : false;
var isConditional = !!(cond || bmsg || setFlags || setPlayerFlags || hidden);
+ var isBidi = mapData && mapData.links && mapData.links.some(function(l) {
+ return l.bidirectional && ((l.from === r.id && l.to === target) || (l.from === target && l.to === r.id));
+ });
h += '<div class="exit-row" data-dir="' + d + '">';
h += '<span class="exit-dir"' + (isConditional ? ' style="color:#f55"' : '') + '>' + d + '</span>';
h += '<span class="exit-target" onclick="selectRoom(' + target + ')"># ' + target + '</span>';
- h += '<label style="font-size:11px;margin-left:4px;cursor:pointer;color:#aaa"><input type="checkbox" class="exit-hidden" style="vertical-align:middle"' + (hidden ? ' checked' : '') + ' onchange="toggleExitHidden(\'' + d + '\')"> Hidden</label>';
if (bmsg) h += '<span class="exit-meta">Blocked: ' + esc(bmsg) + '</span>';
if (cond) h += '<span class="exit-meta">Conditional</span>';
h += '<button class="btn btn-sm" onclick="editExitCondition(\'' + d + '\')">Edit</button>';
+ if (isBidi) h += '<button class="btn btn-sm" onclick="deleteLink(' + target + ',' + r.id + ',\'from\')" title="Remove reverse exit to make one-way">\u21C6</button>';
h += '<button class="btn btn-sm btn-danger" onclick="deleteExit(\'' + d + '\', ' + target + ')">X</button>';
h += '</div>';
});
@@ -1175,6 +1198,7 @@ function renderExitsSection(r) {
exitDirs.forEach(function(d) { h += '<option value="' + d + '">' + d + '</option>'; });
h += '</select>';
h += '<input id="exitTargetID" type="number" placeholder="Target ID">';
+ h += '<label style="font-size:11px;cursor:pointer;color:#aaa;margin:0 4px;display:flex;align-items:center;gap:3px"><input type="checkbox" id="exitReciprocal" checked style="vertical-align:middle"> Reciprocal</label>';
h += '<button class="exit-add-btn" onclick="addExit()">Add</button>';
h += '</div>';
h += '</div>';
@@ -1421,8 +1445,13 @@ function addExit() {
var dir = $('#exitDirSelect').value;
var target = parseInt($('#exitTargetID').value);
if (!dir || !target) { notify('Enter target room ID', 'error'); return; }
- API.post('/api/rooms/link', {from: selectedRoom, to: target, dir: dir}).then(function() {
- notify('Exit added: ' + dir + ' #' + target, 'success');
+ var reciprocalEl = $('#exitReciprocal');
+ var reciprocal = reciprocalEl ? reciprocalEl.checked : true;
+ var body = {from: selectedRoom, to: target, dir: dir};
+ if (!reciprocal) body.oneway = true;
+ API.post('/api/rooms/link', body).then(function(res) {
+ if (res && res.error) { notify('Add exit failed: ' + res.error, 'error'); return; }
+ notify('Exit added: ' + dir + ' #' + target + (reciprocal ? '' : ' (one-way)'), 'success');
updateUndoBar();
loadMap().then(function() { selectRoom(selectedRoom); });
}).catch(function(e) { notify('Add exit failed: ' + extractError(e), 'error'); });
@@ -1430,15 +1459,17 @@ function addExit() {
function deleteExit(dir, target) {
if (!selectedRoom || !currentRoomData) return;
- if (!confirm('Delete exit ' + dir + ' to #' + target + '?')) return;
fetch('/api/rooms/link', {
method: 'DELETE',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({from: selectedRoom, to: target}),
credentials: 'same-origin'
}).then(function(r) {
- if (!r.ok) return r.json().then(function(j) { throw new Error(j.error || 'request failed'); });
- return r.json();
+ return r.json().then(function(j) {
+ if (!r.ok) throw new Error(j.error || 'request failed');
+ if (j.error) throw new Error(j.error);
+ return j;
+ });
}).then(function() {
notify('Removed exit ' + dir + ' to #' + target, 'success');
updateUndoBar();
@@ -2014,21 +2045,16 @@ function clearLinkHighlight() {
}
}
-function createLink(fromId, toId) {
- API.post('/api/rooms/link', {from: fromId, to: toId}).then(function() {
- notify('Linked rooms #'+fromId+' '+toId, 'success');
+function createLink(fromId, toId, oneway) {
+ var body = {from: fromId, to: toId};
+ if (oneway) body.oneway = true;
+ API.post('/api/rooms/link', body).then(function(res) {
+ if (res && res.error) { notify('Link failed: ' + res.error, 'error'); return; }
+ notify('Linked rooms #'+fromId+' '+toId + (oneway ? ' (one-way)' : ''), 'success');
updateUndoBar();
- if (mapData && mapData.rooms) {
- var fr = mapData.rooms.find(function(r) { return r.id === fromId; });
- var tr = mapData.rooms.find(function(r) { return r.id === toId; });
- if (fr && tr) {
- mapData.links = mapData.links || [];
- mapData.links.push({from: fromId, to: toId, dir: gridDeltaToDir(tr.x - fr.x, tr.y - fr.y) || '', bidirectional: true});
- renderMap(mapData);
- }
- }
+ loadMap().then(function() { selectRoom(selectedRoom); });
}).catch(function(e) {
- notify('Link failed: ' + e.message, 'error');
+ notify('Link failed: ' + extractError(e), 'error');
});
}
@@ -2105,26 +2131,26 @@ function clearDelHighlight() {
}
}
-function deleteLink(fromId, toId) {
+function deleteLink(fromId, toId, side) {
+ var body = {from: fromId, to: toId};
+ if (side) body.side = side;
fetch('/api/rooms/link', {
method: 'DELETE',
headers: {'Content-Type': 'application/json'},
- body: JSON.stringify({from: fromId, to: toId}),
+ body: JSON.stringify(body),
credentials: 'same-origin'
}).then(function(r) {
- if (!r.ok) throw new Error('unlink failed');
- return r.json();
+ return r.json().then(function(j) {
+ if (!r.ok) throw new Error(j.error || 'unlink failed');
+ if (j.error) throw new Error(j.error);
+ return j;
+ });
}).then(function() {
- notify('Removed link #'+fromId+' '+toId, 'success');
+ notify('Removed link #'+fromId+' '+toId + (side ? ' (' + side + ' side only)' : ''), 'success');
updateUndoBar();
- if (mapData && mapData.links) {
- mapData.links = mapData.links.filter(function(l) {
- return !((l.from === fromId && l.to === toId) || (l.from === toId && l.to === fromId));
- });
- renderMap(mapData);
- }
+ loadMap().then(function() { selectRoom(selectedRoom); });
}).catch(function(e) {
- notify('Delete link failed: ' + e.message, 'error');
+ notify('Delete link failed: ' + extractError(e), 'error');
});
}
@@ -2368,4 +2394,7 @@ function reloadRoomDirs(cb) {
});
}
-document.addEventListener('DOMContentLoaded', function() { loadRoomDirs(function() { loadMap(); }); initPanelResize(); });
+document.addEventListener('DOMContentLoaded', function() { loadRoomDirs(function() { loadMap(); }); initPanelResize();
+ document.addEventListener('keydown', function(e) { if (e.key === 'Shift') shiftHeld = true; });
+ document.addEventListener('keyup', function(e) { if (e.key === 'Shift') shiftHeld = false; });
+});