From 00554b66df9310073c3b5e1aebd125ea54d9d554 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Mon, 6 Jul 2026 17:41:12 -0400 Subject: feat: shift+drag to create one-way links on admin map --- internal/admin/static/map.js | 99 ++++++++++++++++++++++++++++---------------- 1 file changed, 64 insertions(+), 35 deletions(-) (limited to 'internal/admin/static/map.js') 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 += '
'; h += '' + d + ''; h += '# ' + target + ''; - h += ''; if (bmsg) h += 'Blocked: ' + esc(bmsg) + ''; if (cond) h += 'Conditional'; h += ''; + if (isBidi) h += ''; h += ''; h += '
'; }); @@ -1175,6 +1198,7 @@ function renderExitsSection(r) { exitDirs.forEach(function(d) { h += ''; }); h += ''; h += ''; + h += ''; h += ''; h += ''; h += ''; @@ -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; }); +}); -- cgit v1.2.3