diff options
| author | historia <[not public]> | 2026-07-14 01:36:31 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-07-14 01:36:31 -0400 |
| commit | d72957499f1e25741b8f7a91746fbe132fa68161 (patch) | |
| tree | 115302045c5bce0b62b812bbfcbe3737c9163bb5 /internal/admin/static/map.js | |
| parent | 5a6986a8c539364b7a166abdfb608b3cc00ecb3b (diff) | |
| download | thehouseoficarus-d72957499f1e25741b8f7a91746fbe132fa68161.tar.gz | |
feat(admin): copy paste rooms on map, swap rooms with adjacent rooms
Diffstat (limited to 'internal/admin/static/map.js')
| -rw-r--r-- | internal/admin/static/map.js | 149 |
1 files changed, 145 insertions, 4 deletions
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; |
